""" 连接优先级管理器 5种控制模式的可用性检测、优先级路由和智能降级。 机擎默认(WORKPHONE_WS_FIRST=1):业务 execute 经 device_transport,本模块供 status/connection API 展示。 优先级(高→低): 1. Hook/Frida — 经 WebSocket Agent + 设备本机 Frida 2. Agent WebSocket — 实时双向,设备端 Termux Agent 常驻 3. ADB 静默控制 — 运维兜底,非日常主通道 4. AI Agent — 自然语言编排,依赖以上通道 5. scrcpy 可视化 — 投屏+人工接管 """ from __future__ import annotations import asyncio import logging import subprocess import time from dataclasses import dataclass, field from enum import IntEnum from typing import Any, Dict, List, Optional, Tuple logger = logging.getLogger(__name__) _CACHE_TTL = 30 # seconds class ControlMode(IntEnum): HOOK = 1 AGENT_WS = 2 ADB = 3 AI_AGENT = 4 SCRCPY = 5 MODE_META = { ControlMode.HOOK: { "id": "hook", "name": "Hook / Frida", "desc": "读写内部能力强,适合微信深度控制", "requires": "Root + Frida Server", }, ControlMode.AGENT_WS: { "id": "agent", "name": "Agent WebSocket", "desc": "实时双向通信,设备端App常驻", "requires": "设备端安装工作App", }, ControlMode.ADB: { "id": "adb", "name": "ADB 静默控制", "desc": "无需Root,部署简单,稳定兜底", "requires": "USB调试已开启", }, ControlMode.AI_AGENT: { "id": "ai", "name": "AI Agent", "desc": "自然语言编排复杂任务", "requires": "需要底层通道(Hook/WS/ADB)之一可用", }, ControlMode.SCRCPY: { "id": "scrcpy", "name": "scrcpy 可视化", "desc": "投屏与人工接管", "requires": "ADB连接 + scrcpy已安装", }, } @dataclass class ModeStatus: mode: ControlMode available: bool priority: int detail: str = "" meta: Dict[str, Any] = field(default_factory=dict) def to_dict(self) -> Dict[str, Any]: m = MODE_META.get(self.mode, {}) return { "id": m.get("id", str(self.mode)), "name": m.get("name", ""), "desc": m.get("desc", ""), "requires": m.get("requires", ""), "priority": self.priority, "available": self.available, "detail": self.detail, **self.meta, } class ConnectionPriorityManager: """对单台设备评估所有控制模式的可用性并按优先级排序。""" def __init__(self): self._scrcpy_available: Optional[bool] = None self._cache: Dict[str, Tuple[float, List[ModeStatus]]] = {} def evaluate(self, device_id: str) -> List[ModeStatus]: now = time.time() cached = self._cache.get(device_id) if cached and (now - cached[0]) < _CACHE_TTL: return cached[1] from services.ws_hub import ws_hub from services.adb_device import adb_manager results: List[ModeStatus] = [] # --- Collect raw signals --- adb_ok, adb_serial = self._check_adb(device_id, adb_manager) ws_ok, ws_detail = self._check_ws(device_id, ws_hub) hook_ok = self._check_hook_for_device(device_id, ws_hub) scrcpy_ok = adb_ok and self._check_scrcpy() ws_first = self._ws_first_enabled() has_base = ws_ok or hook_ok or (adb_ok and not ws_first) # 1. Hook / Frida(WS 优先:Agent 上报 frida_available 或 WS ping) hook_detail = "Frida Server 运行中" if hook_ok else ( "Agent WS 在线但 Frida 未 attach" if ws_ok else "未检测到 Hook 服务" ) results.append(ModeStatus( mode=ControlMode.HOOK, available=hook_ok, priority=1, detail=hook_detail, )) # 2. Agent WebSocket results.append(ModeStatus( mode=ControlMode.AGENT_WS, available=ws_ok, priority=2, detail=ws_detail, )) # 3. ADB 静默控制(无线主控下降级,仅运维/兜底) adb_detail = f"ADB 已连接 ({adb_serial})" if adb_ok else "ADB 未连接(无线主控可不插线)" if ws_first and ws_ok and not adb_ok: adb_detail = "无线主控:ADB 非必需" results.append(ModeStatus( mode=ControlMode.ADB, available=adb_ok, priority=3, detail=adb_detail, meta={"adb_serial": adb_serial, "optional": ws_first} if adb_ok or ws_first else {}, )) # 4. AI Agent results.append(ModeStatus( mode=ControlMode.AI_AGENT, available=has_base, priority=4, detail="底层通道就绪" if has_base else "需要至少一个底层通道", )) # 5. scrcpy 可视化 results.append(ModeStatus( mode=ControlMode.SCRCPY, available=scrcpy_ok, priority=5, detail="scrcpy 可用" if scrcpy_ok else ("ADB未连接" if not adb_ok else "scrcpy 未安装"), )) results.sort(key=lambda m: m.priority) self._cache[device_id] = (now, results) return results # ---- individual checks ---- @staticmethod def _check_ws(device_id: str, ws_hub) -> Tuple[bool, str]: if ws_hub.is_online(device_id): return True, "WebSocket 已连接" online_ids = list(ws_hub.connections.keys()) if online_ids: return True, f"WebSocket 已连接 ({online_ids[0][:8]}…)" return False, "设备Agent未连接" @staticmethod def _check_adb(device_id: str, adb_manager) -> Tuple[bool, str]: resolved = adb_manager.resolve_serial(device_id) if resolved: dev = adb_manager.get_device(resolved) if dev and dev.is_online(): adb_manager.register_mapping(device_id, dev.serial) return True, dev.serial for serial in adb_manager.scan_devices(): dev = adb_manager.get_device(serial) if dev and dev.is_online(): adb_manager.register_mapping(device_id, dev.serial) return True, dev.serial return False, "" def _check_hook_fast(self) -> bool: try: r = subprocess.run( ["frida-ps", "-U"], capture_output=True, text=True, timeout=3, ) if r.returncode == 0 and len(r.stdout.strip().split("\n")) > 2: return True except Exception: pass return False @staticmethod def _ws_first_enabled() -> bool: try: from config import settings return bool(getattr(settings, "WORKPHONE_WS_FIRST", True)) except Exception: return True def _check_hook_for_device(self, device_id: str, ws_hub) -> bool: """Hook 可用性:无线优先看 Agent frida_available;否则主机 frida-ps -U。""" if ws_hub.is_online(device_id): info = ws_hub.get_device_info(device_id) or {} if info.get("frida_available"): return True if self._ws_first_enabled(): return False return self._check_hook_fast() def _check_scrcpy(self) -> bool: if self._scrcpy_available is not None: return self._scrcpy_available try: r = subprocess.run(["which", "scrcpy"], capture_output=True, text=True, timeout=3) self._scrcpy_available = r.returncode == 0 except Exception: self._scrcpy_available = False return self._scrcpy_available # ---- public helpers ---- def get_best_mode(self, device_id: str) -> Optional[ModeStatus]: for m in self.evaluate(device_id): if m.available: return m return None def choose_execution_channel(self, device_id: str) -> str: best = self.get_best_mode(device_id) if not best: return "offline" return MODE_META[best.mode]["id"] def invalidate_cache(self): self._cache.clear() self._scrcpy_available = None async def async_evaluate(self, device_id: str) -> List[ModeStatus]: loop = asyncio.get_running_loop() return await loop.run_in_executor(None, self.evaluate, device_id) connection_priority = ConnectionPriorityManager()