"""设备池管理 API(本地 SQLite 清单)。""" import threading from concurrent.futures import ThreadPoolExecutor, as_completed from flask import Blueprint, jsonify, request from core import device_pool from core.adb_helper import adb_connect from core.logger import get_logger from web.auth import perm_required, PERM_DEVICES _log = get_logger("web") bp = Blueprint("devices", __name__) @bp.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}) @bp.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 # 后台采集型号(不阻塞添加响应) 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}) @bp.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": "已删除"}) @bp.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 "停用")}) @bp.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)} 台)") # 重连后顺手刷新型号 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 秒)"}) @bp.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 无法修改,列表只读展示。