From 98244d8161b3557e4c3452a3f966c4ce83be8642 Mon Sep 17 00:00:00 2001 From: butubb <1422726308@qq.com> Date: Tue, 18 Aug 2026 08:02:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AE=BE=E5=A4=87=E6=B1=A0=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E9=9D=A2=E6=9D=BF=EF=BC=88=E6=96=B0=E5=A2=9E/?= =?UTF-8?q?=E5=81=9C=E7=94=A8/=E5=88=A0=E9=99=A4/=E4=B8=80=E9=94=AE?= =?UTF-8?q?=E9=87=8D=E8=BF=9E=EF=BC=8C=E8=B0=83=E5=BA=A6=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=BA=90=E8=A7=84=E8=8C=83=E5=8C=96=E5=85=A5=E5=8F=A3=EF=BC=89?= =?UTF-8?q?+=20STF=20=E8=AE=BE=E5=A4=87=E7=AE=A1=E7=90=86=E5=A2=9E?= =?UTF-8?q?=E5=88=A0=E6=A1=A5=E6=8E=A5=E5=90=8C=E6=AD=A5=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E6=B1=A0=EF=BC=88=E8=BF=81=E7=A7=BB=E6=9C=9F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/admin/base.js | 1 + static/admin/tools.js | 68 +++++++++++++++++++ templates/admin/monitor.html | 27 ++++++++ web_server.py | 124 +++++++++++++++++++++++++++++++++-- 4 files changed, 214 insertions(+), 6 deletions(-) diff --git a/static/admin/base.js b/static/admin/base.js index e72fe50..228b2b2 100644 --- a/static/admin/base.js +++ b/static/admin/base.js @@ -111,6 +111,7 @@ function showSubTab(tabId, name){ tab.querySelectorAll('.sub-tab').forEach(b=>b.classList.toggle('active', b.dataset.sub===name)); tab.querySelectorAll('.sub-panel').forEach(p=>p.classList.toggle('active', p.id===tabId+'-sub-'+name)); if(name==='screen' && typeof loadScreenDevices==='function') loadScreenDevices(); + if(name==='devpool' && typeof loadDevPool==='function') loadDevPool(); } // ================== 模态框 ================== diff --git a/static/admin/tools.js b/static/admin/tools.js index 09a4f67..2dc147f 100644 --- a/static/admin/tools.js +++ b/static/admin/tools.js @@ -546,3 +546,71 @@ function screenSendText(){ onUp({clientX:t.clientX,clientY:t.clientY,preventDefault(){}}); },{passive:false}); })(); + +// ================== Tab: 工具 - 设备池管理(本地 SQLite 清单) ================== +async function loadDevPool(){ + const tb=document.getElementById('tb-devpool'); + const status=document.getElementById('devpool-status'); + if(!tb)return; + tb.innerHTML='加载中...'; + const r=await apiGet('/api/devices/pool'); + if(!r||!r.ok){tb.innerHTML='加载失败';return;} + const rows=r.devices||[]; + const onlineCnt=rows.filter(x=>x.online).length; + status.textContent='共 '+rows.length+' 台,在线 '+onlineCnt+' 台'; + tb.innerHTML=rows.map(d=>{ + const st= d.enabled + ? '启用' + : '停用'; + const online= d.online + ? '在线' + : '离线'; + return ''+ + ''+esc(d.serial)+''+ + ''+esc(d.name||'')+''+ + ''+online+''+ + ''+st+''+ + ''+esc(d.created_at||'')+''+ + ''+ + ' '+ + ''+ + ''; + }).join('')||'设备池为空,添加第一台设备开始'; +} + +async function addPoolDev(){ + const serial=document.getElementById('devpool-serial').value.trim(); + const name=document.getElementById('devpool-name').value.trim(); + if(!serial){showToast('请输入 serial','error');return;} + const r=await apiPost('/api/devices/pool/add',{serial,name}); + if(!r)return; + if(r.ok){ + showToast(r.msg+(serial.includes(':')?'(已尝试连接)':''),'success'); + document.getElementById('devpool-serial').value=''; + document.getElementById('devpool-name').value=''; + loadDevPool(); + }else showToast(r.error,'error'); +} + +async function removePoolDev(serial){ + if(!confirm('从设备池删除 '+serial+'?\n\n删除后不再参与任务调度(不影响设备本身与其他功能)。'))return; + const r=await apiPost('/api/devices/pool/remove',{serial}); + if(!r)return; + if(r.ok){showToast(r.msg,'success');loadDevPool();} + else showToast(r.error,'error'); +} + +async function togglePoolDev(serial,enabled){ + const r=await apiPost('/api/devices/pool/toggle',{serial,enabled}); + if(!r)return; + if(r.ok){showToast(r.msg,'success');loadDevPool();} + else showToast(r.error,'error'); +} + +async function reconnectPoolDevices(){ + if(!confirm('并发 adb connect 设备池内全部设备?\n\n已连接的自动跳过。'))return; + const r=await apiPost('/api/devices/pool/reconnect',{}); + if(!r)return; + if(r.ok){showToast(r.msg,'success');setTimeout(loadDevPool,12000);} + else showToast(r.error,'error'); +} diff --git a/templates/admin/monitor.html b/templates/admin/monitor.html index aba83d6..e124e97 100644 --- a/templates/admin/monitor.html +++ b/templates/admin/monitor.html @@ -481,6 +481,7 @@ select.form-control{cursor:pointer} + @@ -638,6 +639,32 @@ select.form-control{cursor:pointer}
+ +
+
+ + + + + + +
+ + + + + +
serial名称在线状态添加时间操作
+
+ 新增设备流程:手机开 USB 调试 → 连上后「adb 终端」执行 tcpip 5555 转网络调试 → + Tailscale 上线(同一账号)→ 在此添加 IP:5555(自动尝试连接)→ + 任务运行时 u2 自动推送 atx-agent。停用 = 不参与任务调度,删除 = 从池中移除(不影响其他系统)。 + 迁移期如需 STF 侧也接入,再到「STF 设备管理」添加同一 IP。 +
+
+
diff --git a/web_server.py b/web_server.py index 9239968..d884fae 100644 --- a/web_server.py +++ b/web_server.py @@ -1333,29 +1333,45 @@ def api_stf_devmgmt_status(): @app.route("/api/stf/devmgmt/add", methods=["POST"]) @admin_required def api_stf_devmgmt_add(): - """STF 设备管理:添加设备(写入脚本 + 立即 connect)。""" + """STF 设备管理:添加设备(写入脚本 + 立即 connect)。 + + 迁移期桥接:同步加入本地设备池(SQLite),保证新设备可被调度。 + 阶段 3 摘除 STF 后此接口随设备池管理面板退役。 + """ ip = (request.json or {}).get("ip", "").strip() if not ip: return jsonify({"ok": False, "error": "请输入设备 IP"}), 400 try: msgs = stf_device_mgmt.add_device(ip) - return jsonify({"ok": True, "msgs": msgs}) except StfDevError as e: return jsonify({"ok": False, "error": str(e)}), 502 + # 桥接:同步本地设备池(serial 统一 IP:5555) + try: + device_pool.add_device(f"{ip}:5555" if ":" not in ip else ip) + except Exception as e: + _log.warning(f"STF 添加设备桥接设备池失败: {e}") + return jsonify({"ok": True, "msgs": msgs}) @app.route("/api/stf/devmgmt/remove", methods=["POST"]) @admin_required def api_stf_devmgmt_remove(): - """STF 设备管理:移除设备(脚本删除 + disconnect)。""" + """STF 设备管理:移除设备(脚本删除 + disconnect)。 + + 迁移期桥接:同步从本地设备池删除。阶段 3 此接口退役。 + """ ip = (request.json or {}).get("ip", "").strip() if not ip: return jsonify({"ok": False, "error": "请输入设备 IP"}), 400 try: msgs = stf_device_mgmt.remove_device(ip) - return jsonify({"ok": True, "msgs": msgs}) except StfDevError as e: return jsonify({"ok": False, "error": str(e)}), 502 + try: + device_pool.remove_device(f"{ip}:5555" if ":" not in ip else ip) + except Exception: + pass + return jsonify({"ok": True, "msgs": msgs}) def _app_ver_on_device(serial, pkg): @@ -1419,15 +1435,22 @@ def api_tools_appver(): @app.route("/api/stf/devmgmt/delete", methods=["POST"]) @admin_required def api_stf_devmgmt_delete(): - """STF 设备管理:彻底删除(脚本移除 + disconnect + 清 STF 池记录)。""" + """STF 设备管理:彻底删除(脚本移除 + disconnect + 清 STF 池记录)。 + + 迁移期桥接:同步从本地设备池删除。阶段 3 此接口退役。 + """ ip = (request.json or {}).get("ip", "").strip() if not ip: return jsonify({"ok": False, "error": "请输入设备 IP"}), 400 try: msgs = stf_device_mgmt.delete_device(ip, stf=stf) - return jsonify({"ok": True, "msgs": msgs}) except StfDevError as e: return jsonify({"ok": False, "error": str(e)}), 502 + try: + device_pool.remove_device(f"{ip}:5555" if ":" not in ip else ip) + except Exception: + pass + return jsonify({"ok": True, "msgs": msgs}) @app.route("/api/stf/devmgmt/reconnect", methods=["POST"]) @@ -1441,6 +1464,95 @@ def api_stf_devmgmt_reconnect(): return jsonify({"ok": False, "error": str(e)}), 502 +# ================== API:设备池管理(本地 SQLite 清单,新增设备的规范化入口) ================== +@app.route("/api/devices/pool", methods=["GET"]) +@perm_required(PERM_DEVICES) +def api_devices_pool_list(): + """设备池清单(SQLite,含实时在线状态)。""" + try: + online = set(device_pool.list_online()) + except Exception: + online = set() + rows = device_pool.list_devices() + for r in rows: + r["online"] = r["serial"] in online + return jsonify({"ok": True, "devices": rows}) + + +@app.route("/api/devices/pool/add", methods=["POST"]) +@perm_required(PERM_DEVICES) +def api_devices_pool_add(): + """添加/更新设备:{serial, name?, note?}。 + + IP:5555 设备添加后立即尝试 adb connect(不可达显示离线,不影响其他设备)。 + """ + data = request.json or {} + serial = (data.get("serial") or "").strip() + if not serial: + return jsonify({"ok": False, "error": "请输入设备 serial(如 100.100.10.20:5555)"}), 400 + is_new = device_pool.add_device(serial, + name=(data.get("name") or "").strip(), + note=(data.get("note") or "").strip()) + msg = "已添加" if is_new else "已更新" + if ":" in serial: + try: + adb_connect(serial) + except Exception: + pass + _log.info(f"设备池管理: {msg} {serial}") + return jsonify({"ok": True, "msg": msg, "is_new": is_new}) + + +@app.route("/api/devices/pool/remove", methods=["POST"]) +@perm_required(PERM_DEVICES) +def api_devices_pool_remove(): + """从设备池删除(不再参与调度;不影响其他系统)。""" + serial = (request.json or {}).get("serial", "").strip() + if not serial: + return jsonify({"ok": False, "error": "缺少 serial"}), 400 + ok = device_pool.remove_device(serial) + if not ok: + return jsonify({"ok": False, "error": "设备不存在"}), 404 + return jsonify({"ok": True, "msg": "已删除"}) + + +@app.route("/api/devices/pool/toggle", methods=["POST"]) +@perm_required(PERM_DEVICES) +def api_devices_pool_toggle(): + """启用/停用设备(停用后不参与调度)。""" + data = request.json or {} + serial = (data.get("serial") or "").strip() + enabled = data.get("enabled") + if not serial or enabled is None: + return jsonify({"ok": False, "error": "缺少参数"}), 400 + ok = device_pool.set_enabled(serial, bool(enabled)) + if not ok: + return jsonify({"ok": False, "error": "设备不存在"}), 404 + return jsonify({"ok": True, "msg": "已" + ("启用" if enabled else "停用")}) + + +@app.route("/api/devices/pool/reconnect", methods=["POST"]) +@perm_required(PERM_DEVICES) +def api_devices_pool_reconnect(): + """一键重连:并发 adb connect 池内全部 IP:5555 设备(后台执行,不阻塞)。""" + def _run(): + try: + serials = [s for s in device_pool.list_configured() if ":" in s] + except Exception: + return + if not serials: + return + from concurrent.futures import ThreadPoolExecutor, as_completed + with ThreadPoolExecutor(max_workers=10) as pool: + futures = {pool.submit(adb_connect, s): s for s in serials} + for _ in as_completed(futures): + pass + _log.info(f"设备池一键重连完成({len(serials)} 台)") + import threading + threading.Thread(target=_run, daemon=True).start() + return jsonify({"ok": True, "msg": "重连已启动(后台并发,约 10-20 秒)"}) + + @app.route("/api/stf/restart", methods=["POST"]) @admin_required def api_stf_restart():