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

224 lines
7.0 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种控制模式的可用性检测、优先级路由和智能降级。
优先级(高→低):
1. Hook/Frida — 深度控制,读写微信内部数据
2. Agent WebSocket — 实时双向设备端App常驻
3. ADB 静默控制 — 无需AppADB直连兜底
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_fast()
scrcpy_ok = adb_ok and self._check_scrcpy()
has_base = ws_ok or adb_ok or hook_ok
# 1. Hook / Frida
results.append(ModeStatus(
mode=ControlMode.HOOK, available=hook_ok, priority=1,
detail="Frida Server 运行中" if hook_ok else "未检测到 Hook 服务",
))
# 2. Agent WebSocket
results.append(ModeStatus(
mode=ControlMode.AGENT_WS, available=ws_ok, priority=2,
detail=ws_detail,
))
# 3. ADB 静默控制
results.append(ModeStatus(
mode=ControlMode.ADB, available=adb_ok, priority=3,
detail=f"ADB 已连接 ({adb_serial})" if adb_ok else "ADB 未连接",
meta={"adb_serial": adb_serial} if adb_ok 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
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()