251 lines
8.3 KiB
Python
251 lines
8.3 KiB
Python
"""
|
||
连接优先级管理器
|
||
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()
|