feat: Android 重构 service/engine/ui/frida/antiban、AgentForegroundService、APK产品说明、架构图

Made-with: Cursor
This commit is contained in:
卡若
2026-03-17 10:05:46 +08:00
parent 3321e9d620
commit 3c97e7887f
114 changed files with 9709 additions and 5680 deletions

View File

@@ -68,11 +68,19 @@
"modules": []
},
"redmi-note11-test": {
"supports_hook": false,
"frida_version": "",
"root_status": false,
"hook_framework": "frida-server",
"updated_at": "2026-03-16T09:59:18.233188+00:00",
"modules": []
},
"273c1980866992cf800c00ada5cc6375": {
"supports_hook": false,
"frida_version": "",
"root_status": true,
"hook_framework": "frida-server",
"updated_at": "2026-03-16T09:26:13.470654+00:00",
"updated_at": "2026-03-16T21:35:57.473693+00:00",
"modules": []
}
}

View File

@@ -205,8 +205,10 @@ async def simulate_hook_event(req: SimHookEventRequest) -> Dict[str, Any]:
# ========== 连接优先级 ==========
@router.get("/connection/modes/{device_id}")
async def get_device_connection_modes(device_id: str) -> Dict[str, Any]:
"""获取设备所有控制模式的可用性与优先级。"""
async def get_device_connection_modes(device_id: str, refresh: bool = False) -> Dict[str, Any]:
"""获取设备所有控制模式的可用性与优先级。refresh=true 强制刷新缓存。"""
if refresh:
connection_priority.invalidate_cache()
modes = await connection_priority.async_evaluate(device_id)
best = next((m for m in modes if m.available), None)
return {

View File

@@ -26,6 +26,7 @@ class ProjectBindConfig(BaseModel):
project_id: str
project_name: Optional[str] = ""
server: Optional[str] = "ws://sdk.quwanzhi.com:8899/ws/device"
pwa: Optional[str] = ""
@router.post("/generate")
@@ -37,7 +38,8 @@ async def generate_qrcode(config: ProjectBindConfig):
{
"project_id": "project_001",
"project_name": "测试项目",
"server": "ws://sdk.quwanzhi.com:8899/ws/device"
"server": "ws://sdk.quwanzhi.com:8899/ws/device",
"pwa": ""
}
返回: Base64编码的PNG图片
@@ -48,12 +50,14 @@ async def generate_qrcode(config: ProjectBindConfig):
"message": "服务器未安装qrcode库请运行: pip install qrcode[pil]"
}
# 生成二维码内容JSON格式
qr_content = json.dumps({
qr_data = {
"server": config.server,
"project_id": config.project_id,
"project_name": config.project_name
}, ensure_ascii=False)
"project": config.project_id,
"project_name": config.project_name,
}
if config.pwa:
qr_data["pwa"] = config.pwa
qr_content = json.dumps(qr_data, ensure_ascii=False)
# 生成二维码
qr = qrcode.QRCode(
@@ -86,7 +90,8 @@ async def generate_qrcode(config: ProjectBindConfig):
async def get_qrcode_image(
project_id: str,
project_name: str = "",
server: str = "ws://sdk.quwanzhi.com:8899/ws/device"
server: str = "ws://sdk.quwanzhi.com:8899/ws/device",
pwa: str = ""
):
"""
直接获取二维码图片
@@ -102,12 +107,10 @@ async def get_qrcode_image(
status_code=500
)
# 生成二维码内容
qr_content = json.dumps({
"server": server,
"project_id": project_id,
"project_name": project_name
}, ensure_ascii=False)
qr_data = {"server": server, "project": project_id, "project_name": project_name}
if pwa:
qr_data["pwa"] = pwa
qr_content = json.dumps(qr_data, ensure_ascii=False)
# 生成二维码
qr = qrcode.QRCode(
@@ -136,7 +139,8 @@ async def get_qrcode_image(
async def get_qrcode_html(
project_id: str,
project_name: str = "",
server: str = "ws://sdk.quwanzhi.com:8899/ws/device"
server: str = "ws://sdk.quwanzhi.com:8899/ws/device",
pwa: str = ""
):
"""
获取带样式的二维码HTML页面
@@ -152,12 +156,10 @@ async def get_qrcode_html(
status_code=500
)
# 生成二维码内容
qr_content = json.dumps({
"server": server,
"project_id": project_id,
"project_name": project_name
}, ensure_ascii=False)
qr_data = {"server": server, "project": project_id, "project_name": project_name}
if pwa:
qr_data["pwa"] = pwa
qr_content = json.dumps(qr_data, ensure_ascii=False)
# 生成二维码
qr = qrcode.QRCode(

View File

@@ -15,12 +15,15 @@ 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
from typing import Any, Dict, List, Optional, Tuple
logger = logging.getLogger(__name__)
_CACHE_TTL = 30 # seconds
class ControlMode(IntEnum):
HOOK = 1
@@ -91,156 +94,99 @@ class ConnectionPriorityManager:
def __init__(self):
self._scrcpy_available: Optional[bool] = None
self._frida_available: Optional[bool] = None
self._serial_to_ws_id: Dict[str, str] = {}
def _resolve_ws_id(self, device_id: str) -> Optional[str]:
"""将 ADB serial 映射到 WS device_id带缓存。"""
import hashlib
if device_id in self._serial_to_ws_id:
return self._serial_to_ws_id[device_id]
from services.adb_device import adb_manager
adb_dev = adb_manager.get_device(device_id)
if not adb_dev:
return None
try:
android_id = adb_dev._shell("settings get secure android_id").strip()
if android_id and android_id != "null":
md5_id = hashlib.md5(android_id.encode()).hexdigest()
self._serial_to_ws_id[device_id] = md5_id
return md5_id
except Exception:
pass
return 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
from services.hook_module_service import hook_module_service
results: List[ModeStatus] = []
# 1. Hook / Frida (use cached result, refresh in background)
hook_ok = self._check_hook(device_id, hook_module_service)
# --- 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,
mode=ControlMode.HOOK, available=hook_ok, priority=1,
detail="Frida Server 运行中" if hook_ok else "未检测到 Hook 服务",
))
# 2. Agent WebSocket
ws_ok = ws_hub.is_online(device_id)
ws_detail = "WebSocket 已连接"
if not ws_ok:
resolved_ws_id = self._serial_to_ws_id.get(device_id)
if resolved_ws_id and resolved_ws_id in ws_hub.connections:
ws_ok = True
ws_detail = f"WebSocket 已连接 (Agent: {resolved_ws_id[:8]}…)"
else:
online_ids = list(ws_hub.connections.keys())
if online_ids:
ws_ok = True
self._serial_to_ws_id[device_id] = online_ids[0]
ws_detail = f"WebSocket 已连接 ({online_ids[0][:8]}…)"
else:
ws_detail = "设备Agent未连接"
results.append(ModeStatus(
mode=ControlMode.AGENT_WS,
available=ws_ok,
priority=2,
mode=ControlMode.AGENT_WS, available=ws_ok, priority=2,
detail=ws_detail,
))
# 3. ADB
adb_ok = False
adb_serial = ""
resolved = adb_manager.resolve_serial(device_id)
if resolved:
adb_dev_obj = adb_manager.get_device(resolved)
if adb_dev_obj and adb_dev_obj.is_online():
adb_ok = True
adb_serial = adb_dev_obj.serial
if not adb_ok:
for serial in adb_manager.scan_devices():
dev = adb_manager.get_device(serial)
if dev and dev.is_online():
adb_ok = True
adb_serial = dev.serial
break
if adb_ok and adb_serial:
adb_manager.register_mapping(device_id, adb_serial)
# 3. ADB 静默控制
results.append(ModeStatus(
mode=ControlMode.ADB,
available=adb_ok,
priority=3,
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(依赖底层通道)
has_base = ws_ok or adb_ok or hook_ok
# 4. AI Agent
results.append(ModeStatus(
mode=ControlMode.AI_AGENT,
available=has_base,
priority=4,
mode=ControlMode.AI_AGENT, available=has_base, priority=4,
detail="底层通道就绪" if has_base else "需要至少一个底层通道",
))
# 5. scrcpy
scrcpy_ok = adb_ok and self._check_scrcpy()
# 5. scrcpy 可视化
results.append(ModeStatus(
mode=ControlMode.SCRCPY,
available=scrcpy_ok,
priority=5,
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
def get_best_mode(self, device_id: str) -> Optional[ModeStatus]:
modes = self.evaluate(device_id)
for m in modes:
if m.available:
return m
return None
# ---- individual checks ----
def choose_execution_channel(self, device_id: str) -> str:
"""返回最优执行通道标识: hook / agent / adb / ai / offline"""
best = self.get_best_mode(device_id)
if not best:
return "offline"
return MODE_META[best.mode]["id"]
@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未连接"
def _check_hook(self, device_id: str, hook_service) -> bool:
if self._frida_available is True:
return True
@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:
if hasattr(hook_service, "_read_json"):
state = hook_service._read_json(hook_service.device_state_file, {})
item = state.get(device_id, {})
if item.get("supports_hook", False):
self._frida_available = True
return True
r = subprocess.run(
["frida-ps", "-U"],
capture_output=True, text=True, timeout=5,
capture_output=True, text=True, timeout=3,
)
if r.returncode == 0 and len(r.stdout.strip().split("\n")) > 2:
self._frida_available = True
return True
except Exception:
pass
return False
def invalidate_cache(self):
"""清除缓存,下次检测重新获取。"""
self._frida_available = None
self._scrcpy_available = None
self._serial_to_ws_id.clear()
def _check_scrcpy(self) -> bool:
if self._scrcpy_available is not None:
return self._scrcpy_available
@@ -251,9 +197,25 @@ class ConnectionPriorityManager:
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]:
"""异步版 evaluate — 将 ADB 阻塞操作放入线程池"""
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, self.evaluate, device_id)

File diff suppressed because it is too large Load Diff