Files
workphone-sdk/sdk/app/services/connection_priority.py

251 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
连接优先级管理器
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 / FridaWS 优先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()