204 lines
8.1 KiB
Python
204 lines
8.1 KiB
Python
"""设备池管理 API(本地 SQLite 清单)。"""
|
||
import threading
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
|
||
from flask import Blueprint, jsonify, request
|
||
|
||
from core import device_pool, device_discovery
|
||
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:设备自动发现(扫描 → 待连接池 → 确认) ==================
|
||
# 扫描只把设备放进待连接池(pending),用户确认后才加入正式设备池并连接。
|
||
@bp.route("/api/devices/discovery", methods=["GET"])
|
||
@perm_required(PERM_DEVICES)
|
||
def api_devices_discovery():
|
||
"""发现状态 + 待连接列表(前端 10s 轮询一次请求)。"""
|
||
try:
|
||
status = device_discovery.get_status()
|
||
pending = device_discovery.list_pending()
|
||
except Exception as e:
|
||
_log.warning(f"discovery 状态获取失败: {e}")
|
||
return jsonify({"ok": False, "error": str(e)[:120]}), 503
|
||
status["pending"] = pending
|
||
return jsonify({"ok": True, **status})
|
||
|
||
|
||
@bp.route("/api/devices/discovery/scan", methods=["POST"])
|
||
@perm_required(PERM_DEVICES)
|
||
def api_devices_discovery_scan():
|
||
"""手动触发一轮扫描(后台执行,约 5-30 秒)。"""
|
||
ok, result = device_discovery.scan_once(manual=True)
|
||
if not ok:
|
||
return jsonify({"ok": False, "error": result.get("error", "扫描失败")}), 409
|
||
if "error" in result:
|
||
return jsonify({"ok": False, "error": result["error"]}), 400
|
||
return jsonify({"ok": True, "msg": "扫描已启动(后台执行,约 5-30 秒)",
|
||
"result": result})
|
||
|
||
|
||
@bp.route("/api/devices/discovery/confirm", methods=["POST"])
|
||
@perm_required(PERM_DEVICES)
|
||
def api_devices_discovery_confirm():
|
||
"""确认连接:待连接 → 正式设备池 + 后台 adb connect + 采型号。"""
|
||
data = request.json or {}
|
||
serial = (data.get("serial") or "").strip()
|
||
if not serial:
|
||
return jsonify({"ok": False, "error": "缺少 serial"}), 400
|
||
ok, msg, is_new = device_discovery.confirm_pending(
|
||
serial, name=(data.get("name") or "").strip())
|
||
if not ok:
|
||
return jsonify({"ok": False, "error": msg}), 404
|
||
if ":" in serial:
|
||
def _connect():
|
||
try:
|
||
adb_connect(serial)
|
||
except Exception:
|
||
pass
|
||
try:
|
||
device_pool.refresh_model(serial)
|
||
except Exception:
|
||
pass
|
||
threading.Thread(target=_connect, daemon=True).start()
|
||
_log.info(f"设备池管理: 发现确认 {serial}")
|
||
return jsonify({"ok": True, "msg": msg, "is_new": is_new})
|
||
|
||
|
||
@bp.route("/api/devices/discovery/ignore", methods=["POST"])
|
||
@perm_required(PERM_DEVICES)
|
||
def api_devices_discovery_ignore():
|
||
"""忽略:从待连接池删除(下轮扫描可能再次发现)。"""
|
||
serial = (request.json or {}).get("serial", "").strip()
|
||
if not serial:
|
||
return jsonify({"ok": False, "error": "缺少 serial"}), 400
|
||
ok, msg = device_discovery.ignore_pending(serial)
|
||
if not ok:
|
||
return jsonify({"ok": False, "error": msg}), 404
|
||
return jsonify({"ok": True, "msg": msg})
|
||
|
||
|
||
@bp.route("/api/devices/discovery/settings", methods=["POST"])
|
||
@perm_required(PERM_DEVICES)
|
||
def api_devices_discovery_settings():
|
||
"""保存发现配置(部分字段更新):{enabled?, subnets?, interval?, port?}。"""
|
||
data = request.json or {}
|
||
ok, msg = device_discovery.save_settings(
|
||
enabled=data.get("enabled"),
|
||
subnets=data.get("subnets"),
|
||
interval=data.get("interval"),
|
||
port=data.get("port"))
|
||
if not ok:
|
||
return jsonify({"ok": False, "error": msg}), 400
|
||
return jsonify({"ok": True, "msg": msg})
|
||
|
||
|
||
# ================== API:Tailscale 管理(仅管理员) ==================
|
||
# 通过 Tailscale 官方 API v2 管理 tailnet 设备(列表/改名/授权/密钥不过期/删除/生成 auth key)。
|
||
# 设备 IP 由 tailnet 自动分配,API 无法修改,列表只读展示。
|
||
|