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}
+
+
+
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():