feat: 设备型号采集展示(devices 表 model 列+getprop 采集+管理页型号列/采集按钮+监控页兜底)+ fix: 监控页设备列错位(离线标签残留 td)、appver 端点误删恢复、appver 只查在线设备用轻量连接防挂起
This commit is contained in:
+117
-2
@@ -64,8 +64,7 @@ login_manager.login_view = "login"
|
||||
|
||||
# 先初始化数据库(含旧 JSON 迁移),再创建 TaskManager(需要 app context 读写 DB)
|
||||
init_db(app)
|
||||
# 设备池(本地清单 + adb 在线状态):阶段 0 起作为 STF 的替代数据源,
|
||||
# 首次启动自动从 STF 导入现有设备;阶段 3 摘除 STF 后此模块独立运行
|
||||
# 设备池(本地清单 + adb 在线状态):任务调度的数据源
|
||||
from core import device_pool
|
||||
device_pool.init_app(app)
|
||||
mgr = TaskManager(app=app)
|
||||
@@ -1294,6 +1293,104 @@ def _set_device_clipboard(serial, text):
|
||||
return False, f"{type(e).__name__}: {str(e)[:120]}"
|
||||
|
||||
|
||||
def _app_ver_on_device(serial, pkg):
|
||||
"""单设备包版本查询:pm path 检查安装 → dumpsys 取 versionName/versionCode。
|
||||
|
||||
IP:port 设备先轻量 adb connect(单次尝试,绝不 disconnect,红线)。
|
||||
查询是只读 shell 命令,无需 adb server 锁(锁只保护 connect/kill-server 类操作)。
|
||||
"""
|
||||
if ":" in serial:
|
||||
try:
|
||||
adb_connect_light(serial)
|
||||
except Exception as e:
|
||||
return {"installed": False, "error": f"adb 连接失败: {e}"}
|
||||
try:
|
||||
r = subprocess.run([ADB_PATH, "-s", serial, "shell", "pm", "path", pkg],
|
||||
capture_output=True, timeout=15)
|
||||
if b"package:" not in (r.stdout or b""):
|
||||
return {"installed": False, "version_name": "", "version_code": ""}
|
||||
r2 = subprocess.run([ADB_PATH, "-s", serial, "shell", "dumpsys", "package", pkg],
|
||||
capture_output=True, timeout=20)
|
||||
out2 = (r2.stdout or b"").decode("utf-8", errors="replace")
|
||||
vm = re.search(r"versionName=(\S+)", out2)
|
||||
vc = re.search(r"versionCode=(\d+)", out2)
|
||||
return {"installed": True,
|
||||
"version_name": vm.group(1) if vm else "",
|
||||
"version_code": vc.group(1) if vc else ""}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"installed": False, "error": "查询超时"}
|
||||
except Exception as e:
|
||||
return {"installed": False, "error": str(e)}
|
||||
|
||||
|
||||
@app.route("/api/tools/appver", methods=["POST"])
|
||||
@admin_required
|
||||
def api_tools_appver():
|
||||
"""工具-应用版本管理:查询所有设备上指定包名的安装情况与版本号。
|
||||
|
||||
请求: {"pkg": "com.ss.android.ugc.aweme"}
|
||||
设备来源:本地 adb(含 USB)+ 设备池,并发查询(最多 10 台同时)。
|
||||
"""
|
||||
pkg = (request.json or {}).get("pkg", "").strip()
|
||||
if not re.match(r"^[A-Za-z0-9_.]+$", pkg or ""):
|
||||
return jsonify({"ok": False, "error": "包名格式不正确(仅字母/数字/._)"}), 400
|
||||
# 只查在线设备:池内离线条目(如陈旧记录)不连,避免 connect 重试拖慢查询
|
||||
try:
|
||||
online = set(device_pool.list_online())
|
||||
devices = [d for d in _merged_device_list() if d["serial"] in online]
|
||||
except Exception:
|
||||
devices = []
|
||||
if not devices:
|
||||
return jsonify({"ok": False, "error": "无在线设备"}), 404
|
||||
results = {}
|
||||
with ThreadPoolExecutor(max_workers=min(10, len(devices))) as pool:
|
||||
futures = {pool.submit(_app_ver_on_device, d["serial"], pkg): d for d in devices}
|
||||
for fut in as_completed(futures, timeout=90):
|
||||
d = futures[fut]
|
||||
try:
|
||||
results[d["serial"]] = fut.result()
|
||||
except Exception as e:
|
||||
results[d["serial"]] = {"installed": False, "error": str(e)}
|
||||
fail = sum(1 for v in results.values() if v.get("error"))
|
||||
return jsonify({"ok": True, "results": results, "fail": fail,
|
||||
"total": len(results)})
|
||||
|
||||
|
||||
@app.route("/api/stf/devmgmt/delete", methods=["POST"])
|
||||
@admin_required
|
||||
def api_stf_devmgmt_delete():
|
||||
"""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)
|
||||
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"])
|
||||
@admin_required
|
||||
def api_stf_devmgmt_reconnect():
|
||||
"""STF 设备管理:一键重连(手动运行 220 的 connect_devices.sh)。"""
|
||||
try:
|
||||
ok, summary = stf_device_mgmt.reconnect_all()
|
||||
return jsonify({"ok": True, "output": summary})
|
||||
except StfDevError as e:
|
||||
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():
|
||||
@@ -1328,6 +1425,10 @@ def api_devices_pool_add():
|
||||
adb_connect(serial)
|
||||
except Exception:
|
||||
pass
|
||||
# 后台采集型号(不阻塞添加响应)
|
||||
import threading
|
||||
threading.Thread(target=device_pool.refresh_model, args=(serial,),
|
||||
daemon=True).start()
|
||||
_log.info(f"设备池管理: {msg} {serial}")
|
||||
return jsonify({"ok": True, "msg": msg, "is_new": is_new})
|
||||
|
||||
@@ -1377,11 +1478,25 @@ def api_devices_pool_reconnect():
|
||||
for _ in as_completed(futures):
|
||||
pass
|
||||
_log.info(f"设备池一键重连完成({len(serials)} 台)")
|
||||
# 重连后顺手刷新型号
|
||||
try:
|
||||
device_pool.refresh_all_models()
|
||||
except Exception:
|
||||
pass
|
||||
import threading
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
return jsonify({"ok": True, "msg": "重连已启动(后台并发,约 10-20 秒)"})
|
||||
|
||||
|
||||
@app.route("/api/devices/pool/refresh_models", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_devices_pool_refresh_models():
|
||||
"""批量采集池内在线设备的型号(后台执行,不阻塞)。"""
|
||||
import threading
|
||||
threading.Thread(target=device_pool.refresh_all_models, daemon=True).start()
|
||||
return jsonify({"ok": True, "msg": "型号采集已启动(后台并发,约 10 秒)"})
|
||||
|
||||
|
||||
# ================== API:Tailscale 管理(仅管理员) ==================
|
||||
# 通过 Tailscale 官方 API v2 管理 tailnet 设备(列表/改名/授权/密钥不过期/删除/生成 auth key)。
|
||||
# 设备 IP 由 tailnet 自动分配,API 无法修改,列表只读展示。
|
||||
|
||||
Reference in New Issue
Block a user