diff --git a/sdk/agent/package.sh b/sdk/agent/package.sh index d063c176ba..609b1d6e4a 100755 --- a/sdk/agent/package.sh +++ b/sdk/agent/package.sh @@ -19,6 +19,7 @@ mkdir -p "$TEMP_DIR/agent/skills/wechat" \ "$TEMP_DIR/agent/skills/douyin" \ "$TEMP_DIR/agent/skills/xhs" \ "$TEMP_DIR/agent/skills/xianyu" \ + "$TEMP_DIR/agent/skills/soul" \ "$TEMP_DIR/agent/hawk" # 复制核心文件 @@ -47,7 +48,7 @@ for f in README.md setup_redmi11.sh wechat_hook_v1.js; do [ -f "$SCRIPT_DIR/hook/$f" ] && cp "$SCRIPT_DIR/hook/$f" "$TEMP_DIR/agent/hook/" done -for sub in wechat douyin xhs xianyu; do +for sub in wechat douyin xhs xianyu soul; do if [ -d "$SCRIPT_DIR/skills/$sub" ]; then cp "$SCRIPT_DIR/skills/$sub/__init__.py" "$TEMP_DIR/agent/skills/$sub/" 2>/dev/null || true cp "$SCRIPT_DIR/skills/$sub/skill.py" "$TEMP_DIR/agent/skills/$sub/" 2>/dev/null || true diff --git a/sdk/app/data/hook/device_modules.json b/sdk/app/data/hook/device_modules.json index a689cbf7e4..f2adba732f 100644 --- a/sdk/app/data/hook/device_modules.json +++ b/sdk/app/data/hook/device_modules.json @@ -34,5 +34,21 @@ }, "hook_framework": "frida-server", "updated_at": "2026-02-24T08:22:19.844907+00:00" + }, + "dc9c23e00510": { + "supports_hook": false, + "frida_version": "", + "root_status": true, + "hook_framework": "frida-server", + "updated_at": "2026-03-16T01:50:07.133711+00:00", + "modules": [] + }, + "b0f1322b4e3be135e0000798d1587e26": { + "supports_hook": false, + "frida_version": "", + "root_status": false, + "hook_framework": "frida-server", + "updated_at": "2026-03-16T01:50:27.104574+00:00", + "modules": [] } } \ No newline at end of file diff --git a/sdk/app/main.py b/sdk/app/main.py index 8f17c43cb3..8227480fb9 100644 --- a/sdk/app/main.py +++ b/sdk/app/main.py @@ -3,7 +3,7 @@ 存客宝的AI手机控制引擎 """ -from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse, Response @@ -12,6 +12,9 @@ import logging import os import subprocess import asyncio +from pathlib import Path + +import markdown from config import settings from routers import devices, unified, agent, adb, experience, projects, qrcode, voice, capture, hook_modules, connection @@ -25,6 +28,341 @@ logging.basicConfig( ) logger = logging.getLogger(__name__) +_APP_DIR = os.path.dirname(__file__) +_STATIC_DIR = os.path.join(_APP_DIR, "static") +_DOCS_ROOT = os.path.normpath(os.path.join(_APP_DIR, "..", "..", "开发文档")) +_DOC_PAGE_MAP = { + "api-architecture": os.path.join(_DOCS_ROOT, "5、接口", "API架构与交互流程图.html"), + "anti-ban-architecture": os.path.join(_DOCS_ROOT, "2、架构", "防封模块架构图.html"), + "wechat-server-flow": os.path.join(_DOCS_ROOT, "2、架构", "微信控制-设备服务器交互图.html"), +} +_AGENT_ROOT = os.path.normpath(os.path.join(_APP_DIR, "..", "agent")) +_AGENT_DOCS = [ + { + "id": "agent-readme", + "title": "Agent 架构与部署", + "summary": "设备端 Agent 总览:WebSocket 连接、技能引擎、目录结构、Termux 一键安装。", + "category": "agent", + "section": "Agent 集群", + "folder": "agent", + "file": "README.md", + "keywords": ["agent", "termux", "websocket", "部署"], + }, + { + "id": "agent-skills", + "title": "技能引擎 Skills", + "summary": "6 大 Skill(微信/抖音/小红书/闲鱼/语音/搜索)· SkillExecutor 调度 · 复合命令。", + "category": "agent", + "section": "Agent 集群", + "folder": "agent/skills", + "file": "README.md", + "keywords": ["skill", "微信", "抖音", "技能", "executor"], + }, + { + "id": "agent-hawk", + "title": "Hawk 网络恢复模块", + "summary": "Agent 与 Hawk 边界协同:断网自动恢复、WiFi 开关、合法操作边界。", + "category": "agent", + "section": "Agent 集群", + "folder": "agent/docs", + "file": "AGENT_HAWK.md", + "keywords": ["hawk", "网络", "wifi", "恢复", "断网"], + }, + { + "id": "agent-hook", + "title": "Hook 脚本系统", + "summary": "微信 Hook 初版脚本(Frida):RPC 骨架、消息监听、脚本部署流程。", + "category": "agent", + "section": "Agent 集群", + "folder": "agent/hook", + "file": "README.md", + "keywords": ["hook", "frida", "rpc", "消息监听"], + }, +] +_WORKBENCH_DOCS = [ + { + "id": "sdk-manual", + "title": "SDK操作手册", + "summary": "工作手机 SDK 的整体使用说明、核心能力和调用入口。", + "category": "manual", + "section": "说明文档", + "folder": "9、手册", + "file": "SDK操作手册.md", + "keywords": ["SDK", "手册", "调用", "接入"], + }, + { + "id": "usage-solution", + "title": "使用与落地方案", + "summary": "从部署到业务落地的整体使用路径与建议方案。", + "category": "manual", + "section": "说明文档", + "folder": "9、手册", + "file": "使用与落地方案.md", + "keywords": ["方案", "落地", "部署", "使用"], + }, + { + "id": "wechat-e2e", + "title": "微信消息E2E验证指南", + "summary": "微信消息链路的端到端验证步骤和排查指引。", + "category": "manual", + "section": "说明文档", + "folder": "9、手册", + "file": "微信消息E2E验证指南.md", + "keywords": ["微信", "E2E", "验证", "排查"], + }, + { + "id": "agent-install", + "title": "设备端Agent安装与公司设备说明", + "summary": "设备端 Agent 的安装方式、公司设备规范和运行说明。", + "category": "manual", + "section": "说明文档", + "folder": "9、手册", + "file": "设备端Agent安装与公司设备说明.md", + "keywords": ["Agent", "安装", "设备", "说明"], + }, + { + "id": "install-checklist", + "title": "安装前配置检查规范", + "summary": "安装前需要确认的配置、环境与风险检查项。", + "category": "manual", + "section": "说明文档", + "folder": "9、手册", + "file": "安装前配置检查规范.md", + "keywords": ["安装", "检查", "规范", "配置"], + }, + { + "id": "system-architecture", + "title": "系统架构", + "summary": "工作手机 SDK 的整体架构、模块分层与链路设计。", + "category": "architecture", + "section": "架构与经验", + "folder": "2、架构", + "file": "系统架构.md", + "keywords": ["架构", "模块", "分层", "链路"], + }, + { + "id": "hook-architecture", + "title": "Hook通道与多设备多服务器架构", + "summary": "Hook 通道、Frida 集成、多设备与多服务器的设计说明。", + "category": "architecture", + "section": "架构与经验", + "folder": "2、架构", + "file": "Hook通道与多设备多服务器架构.md", + "keywords": ["Hook", "Frida", "多设备", "多服务器"], + }, + { + "id": "api-spec", + "title": "接口规范", + "summary": "统一接口的字段约定、协议与调用方式。", + "category": "api", + "section": "接口与协议", + "folder": "5、接口", + "file": "接口规范.md", + "keywords": ["接口", "协议", "字段", "规范"], + }, + { + "id": "cunkebao-spec", + "title": "存客宝对接规范", + "summary": "存客宝与工作手机 SDK 的对接方式和聚合规则。", + "category": "api", + "section": "接口与协议", + "folder": "5、接口", + "file": "存客宝对接规范.md", + "keywords": ["存客宝", "对接", "SDK", "聚合"], + }, + { + "id": "progress-overview", + "title": "开发进度总表", + "summary": "当前阶段、完成率、里程碑与下一步开发重点。", + "category": "project", + "section": "项目管理", + "folder": "10、项目管理", + "file": "开发进度总表.md", + "keywords": ["进度", "里程碑", "阶段", "总表"], + }, +] +def _scan_arch_assets(): + import urllib.parse + arch_dir = Path(_DOCS_ROOT) / "2、架构" + known = [ + ("arch-sdk", "工作手机SDK架构图", "系统全景四层架构,适合做整体汇报入口。"), + ("arch-flow", "设备与服务器交互流程(微信控制)", "设备、服务器与微信控制的交互过程。"), + ("arch-device", "工作手机设备端运转逻辑", "设备端从启动到心跳到 Skill 执行的完整运转。"), + ("arch-agent-total", "Agent与服务器工作流总图", "业务入口、服务器 Agent、设备 Agent 和消息闭环。"), + ("arch-hook", "Hook与服务器交互流程图", "Hook 通道的完整交互链路与功能细节。"), + ("arch-ban", "防封模块架构图", "防封中心、设备侧、行为层与服务端策略。"), + ("arch-ban-detail", "防封服务端核心能力拆细图", "防封服务端 7 大模块逐一拆细。"), + ("mode-hook", "本地控制模式一:Hook/Frida", "Frida 深度控制微信的模式示意。"), + ("mode-adb", "本地控制模式二:ADB静默控制", "无需 Root 的 ADB 静默控制模式。"), + ("mode-agent", "本地控制模式三:Agent WebSocket远控", "远程统一控制多设备。"), + ("mode-ai", "本地控制模式四:AI Agent自然语言", "自然语言编排复杂任务。"), + ("mode-scrcpy", "本地控制模式五:scrcpy可视化控制", "投屏与人工接管。"), + ] + fname_map = { + "arch-sdk": "工作手机SDK架构图.png", + "arch-flow": "设备与服务器交互流程_微信控制.png", + "arch-device": "工作手机设备端运转逻辑.png", + "arch-agent-total": "卡若AI手机_Agent与服务器工作流总图.png", + "arch-hook": "Hook与服务器交互流程图.png", + "arch-ban": "防封模块架构图.png", + "arch-ban-detail": "防封服务端核心能力拆细图.png", + "mode-hook": "本地控制模式_1_Hook_Frida模式.png", + "mode-adb": "本地控制模式_2_ADB静默控制模式.png", + "mode-agent": "本地控制模式_3_Agent_WebSocket远控模式.png", + "mode-ai": "本地控制模式_4_AI_Agent自然语言模式.png", + "mode-scrcpy": "本地控制模式_5_scrcpy可视化控制模式.png", + } + result = [] + for aid, title, summary in known: + fname = fname_map.get(aid, "") + fpath = arch_dir / fname + if fpath.exists(): + encoded = urllib.parse.quote(f"2、架构/{fname}") + result.append({ + "id": aid, + "title": title, + "summary": summary, + "category": "architecture", + "url": f"/workbench-docs/{encoded}", + }) + return result + +_WORKBENCH_ARCH_ASSETS = _scan_arch_assets() + + +def _doc_path(meta: dict) -> Path: + if meta.get("category") == "agent": + return Path(_AGENT_ROOT) / meta["file"] if meta["folder"] == "agent" else Path(_AGENT_ROOT) / meta["folder"].replace("agent/", "", 1) / meta["file"] + return Path(_DOCS_ROOT) / meta["folder"] / meta["file"] + + +def _doc_meta_with_state(meta: dict) -> dict: + path = _doc_path(meta) + exists = path.exists() + return { + **meta, + "exists": exists, + "path": str(path), + "updated_at": path.stat().st_mtime if exists else None, + "raw_url": f"/workbench-docs/{meta['folder']}/{meta['file']}" if exists else None, + } + + +def _render_markdown_html(content: str) -> str: + return markdown.markdown( + content, + extensions=["extra", "tables", "fenced_code", "toc", "sane_lists"], + output_format="html5", + ) + + +async def _build_workbench_overview() -> dict: + from services.adb_device import adb_manager + from services.ai_command import ai_engine + from services.connection_priority import connection_priority + + adb_devices = adb_manager.scan_devices() + + db_devices = [] + try: + db_devices = await device_manager.get_all_devices(limit=200) + except Exception: + db_devices = [] + + online_devices = {d["device_id"]: d for d in ws_hub.get_online_devices()} + merged = {} + + for device in db_devices: + device_id = device["device_id"] + merged[device_id] = {**device} + + for device_id, info in online_devices.items(): + base = merged.get(device_id, {}) + merged[device_id] = {**base, **info, "status": "online"} + + for serial in adb_devices: + if serial not in merged: + adb_dev = adb_manager.get_device(serial) + if adb_dev: + info = adb_dev.get_info() + merged[serial] = { + "device_id": serial, + "model": info.get("model", "Unknown"), + "brand": info.get("brand", "Unknown"), + "android_version": info.get("android_version", "Unknown"), + "status": "adb", + "connection_type": "adb", + "display": info.get("display", {}), + } + + devices = list(merged.values()) + online_count = len([d for d in devices if d.get("status") == "online"]) + adb_count = len([d for d in devices if d.get("status") == "adb"]) + offline_count = len([d for d in devices if d.get("status") == "offline"]) + + try: + ai_status = await ai_engine.get_status() + except Exception: + ai_status = {"available": False, "backend": "", "model": ""} + + docs = [_doc_meta_with_state(meta) for meta in _WORKBENCH_DOCS + _AGENT_DOCS] + + # 为每台设备附加连接模式信息 + device_modes = {} + for dev in devices: + did = dev.get("device_id", "") + if did: + try: + modes = connection_priority.evaluate(did) + best = next((m for m in modes if m.available), None) + dev["connection_modes"] = [m.to_dict() for m in modes] + dev["best_mode"] = best.to_dict() if best else None + device_modes[did] = dev + except Exception: + pass + + return { + "sdk_online": True, + "sdk_version": "3.0.0", + "ws_online": len(ws_hub.connections), + "adb_count": len(adb_devices), + "device_total": len(devices), + "device_online": online_count, + "device_offline": offline_count + adb_count, + "devices": devices, + "summary": { + "sdk_online": True, + "sdk_version": "3.0.0", + "ws_online": len(ws_hub.connections), + "adb_count": len(adb_devices), + "device_total": len(devices), + "device_online": online_count, + "device_offline": offline_count, + "manual_docs": len([d for d in docs if d["category"] == "manual" and d["exists"]]), + "architecture_assets": len(_WORKBENCH_ARCH_ASSETS), + }, + "agent": ai_status, + "docs": docs, + "architecture_assets": _WORKBENCH_ARCH_ASSETS, + "sections": [ + {"id": "overview", "title": "首页总览"}, + {"id": "docs", "title": "说明文档"}, + {"id": "devices", "title": "设备控制"}, + {"id": "hook", "title": "Hook 控制"}, + {"id": "agent", "title": "AI Agent"}, + {"id": "connection", "title": "连接与协议"}, + {"id": "architecture", "title": "架构与经验"}, + {"id": "project", "title": "项目管理"}, + ], + "control_modes": [ + {"id": "hook", "name": "Hook / Frida", "desc": "读写内部能力强,适合微信深度控制", "priority": 1}, + {"id": "agent", "name": "Agent WebSocket", "desc": "实时双向,设备端App常驻", "priority": 2}, + {"id": "adb", "name": "ADB 静默控制", "desc": "无需 Root,部署简单,适合稳定兜底", "priority": 3}, + {"id": "ai", "name": "AI Agent", "desc": "自然语言编排复杂任务", "priority": 4}, + {"id": "scrcpy", "name": "scrcpy 可视化", "desc": "投屏与人工接管", "priority": 5}, + ], + } + @asynccontextmanager async def lifespan(app: FastAPI): @@ -174,37 +512,107 @@ async def ready(): @app.get("/") async def root(): - """根路由 → 控制面板""" - static_path = os.path.join(os.path.dirname(__file__), "static", "index.html") + """根路由 → 聚合总控台""" + static_path = os.path.join(_STATIC_DIR, "hub.html") if os.path.exists(static_path): return FileResponse(static_path) return { "name": "工作手机SDK v3.0", - "description": "存客宝的AI手机控制引擎", + "description": "统一聚合总控台", "docs": "/docs", "health": "/health", "ready": "/ready", - "voice_control": "/voice" + "voice_control": "/voice", + "control_center": "/control", } +@app.get("/hub") +async def hub_page(): + """统一聚合总控台""" + static_path = os.path.join(_STATIC_DIR, "hub.html") + if os.path.exists(static_path): + return FileResponse(static_path) + raise HTTPException(status_code=404, detail="hub.html 不存在") + + +@app.get("/control") +async def control_page(): + """兼容旧入口,统一回到工作台""" + static_path = os.path.join(_STATIC_DIR, "hub.html") + if os.path.exists(static_path): + return FileResponse(static_path) + raise HTTPException(status_code=404, detail="hub.html 不存在") + + @app.get("/voice") async def voice_control_page(): """语音控制页面""" - static_path = os.path.join(os.path.dirname(__file__), "static", "voice_control.html") + static_path = os.path.join(_STATIC_DIR, "voice_control.html") return FileResponse(static_path) +@app.get("/pages/{page_key}") +async def docs_page(page_key: str): + """聚合页内嵌的项目文档页面""" + doc_path = _DOC_PAGE_MAP.get(page_key) + if not doc_path or not os.path.exists(doc_path): + raise HTTPException(status_code=404, detail="页面不存在") + return FileResponse(doc_path) + + +@app.get("/api/v3/workbench/overview") +async def workbench_overview(): + """工作台统一概览 DTO,供工作台前端与存客宝页面共用。""" + return {"code": 200, "data": await _build_workbench_overview()} + + +@app.get("/api/v3/workbench/docs") +async def workbench_docs(): + """工作台文档清单。""" + return { + "code": 200, + "data": { + "docs": [_doc_meta_with_state(meta) for meta in _WORKBENCH_DOCS + _AGENT_DOCS], + "architecture_assets": _WORKBENCH_ARCH_ASSETS, + }, + } + + +@app.get("/api/v3/workbench/docs/{doc_id}") +async def workbench_doc_detail(doc_id: str): + """读取工作台文档正文并返回渲染后的 HTML。""" + meta = next((item for item in _WORKBENCH_DOCS + _AGENT_DOCS if item["id"] == doc_id), None) + if not meta: + raise HTTPException(status_code=404, detail="文档不存在") + + path = _doc_path(meta) + if not path.exists(): + raise HTTPException(status_code=404, detail="文档文件不存在") + + raw = path.read_text(encoding="utf-8") + return { + "code": 200, + "data": { + **_doc_meta_with_state(meta), + "content_markdown": raw, + "content_html": _render_markdown_html(raw), + }, + } + + # 挂载静态文件目录 -static_dir = os.path.join(os.path.dirname(__file__), "static") -if os.path.exists(static_dir): - app.mount("/static", StaticFiles(directory=static_dir), name="static") +if os.path.exists(_STATIC_DIR): + app.mount("/static", StaticFiles(directory=_STATIC_DIR), name="static") + +if os.path.exists(_DOCS_ROOT): + app.mount("/workbench-docs", StaticFiles(directory=_DOCS_ROOT), name="workbench-docs") # ========== Agent 分发接口 ========== # Agent 代码根目录 -_AGENT_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "agent")) +_AGENT_DIR = os.path.normpath(os.path.join(_APP_DIR, "..", "agent")) _AGENT_DIST = os.path.join(_AGENT_DIR, "dist", "agent.tar.gz") diff --git a/sdk/app/routers/connection.py b/sdk/app/routers/connection.py index c134cadba4..6664464527 100644 --- a/sdk/app/routers/connection.py +++ b/sdk/app/routers/connection.py @@ -14,6 +14,7 @@ from pydantic import BaseModel, Field from services.ws_hub import ws_hub from services.adb_device import adb_manager from services.hook_module_service import hook_module_service +from services.connection_priority import connection_priority router = APIRouter() @@ -199,3 +200,40 @@ async def simulate_hook_event(req: SimHookEventRequest) -> Dict[str, Any]: } ) return {"code": 200, "data": event} + + +# ========== 连接优先级 ========== + +@router.get("/connection/modes/{device_id}") +async def get_device_connection_modes(device_id: str) -> Dict[str, Any]: + """获取设备所有控制模式的可用性与优先级。""" + modes = connection_priority.evaluate(device_id) + best = next((m for m in modes if m.available), None) + return { + "code": 200, + "data": { + "device_id": device_id, + "best_mode": best.to_dict() if best else None, + "modes": [m.to_dict() for m in modes], + }, + } + + +@router.get("/connection/modes") +async def get_all_connection_modes() -> Dict[str, Any]: + """获取所有已知设备的控制模式概览。""" + online_ids = list(ws_hub.connections.keys()) + adb_serials = adb_manager.scan_devices() + all_ids = list(set(online_ids + adb_serials)) + + result = [] + for did in all_ids: + modes = connection_priority.evaluate(did) + best = next((m for m in modes if m.available), None) + result.append({ + "device_id": did, + "best_mode": best.to_dict() if best else None, + "available_count": sum(1 for m in modes if m.available), + "modes": [m.to_dict() for m in modes], + }) + return {"code": 200, "data": result} diff --git a/sdk/app/routers/unified.py b/sdk/app/routers/unified.py index 6616111e5b..c92d102838 100644 --- a/sdk/app/routers/unified.py +++ b/sdk/app/routers/unified.py @@ -13,7 +13,7 @@ from fastapi import APIRouter, HTTPException from typing import Optional, List -from pydantic import BaseModel +from pydantic import BaseModel, Field from enum import Enum import asyncio import logging @@ -343,27 +343,21 @@ class ChannelRouter: # ========== 辅助函数 ========== def _check_device_online(device_id: str): - """检查设备是否在线(WebSocket或ADB)""" - # 先检查WebSocket连接 - if ws_hub.is_online(device_id): - return "websocket" - - # 再检查ADB设备 - adb_device = adb_manager.get_device(device_id) - if adb_device and adb_device.is_online(): - return "adb" - + """检查设备是否在线(按优先级:Hook > WebSocket > ADB)""" + from services.connection_priority import connection_priority + best = connection_priority.get_best_mode(device_id) + if best: + return best.to_dict()["id"] # hook / agent / adb raise HTTPException(status_code=503, detail=f"设备不在线: {device_id}") def _get_device_mode(device_id: str) -> str: - """获取设备连接模式""" - if ws_hub.is_online(device_id): + """获取设备最优连接模式""" + from services.connection_priority import connection_priority + channel = connection_priority.choose_execution_channel(device_id) + if channel == "agent": return "websocket" - adb_device = adb_manager.get_device(device_id) - if adb_device and adb_device.is_online(): - return "adb" - return "offline" + return channel def _parse_messages_from_xml(xml: str, limit: int) -> list: @@ -521,6 +515,39 @@ async def _execute_via_adb(device, platform: str, action: str, params: dict) -> return {"success": False, "error": str(e), "mode": "adb"} +# ============================================================================= +# 〇、Hawk Hook 统一执行入口(丝滑控制) +# ============================================================================= + +class HookExecuteRequest(BaseModel): + """Hawk Hook 格式 — 单端点执行任意操作,任何应用可直连""" + device_id: str + platform: Platform = Platform.WECHAT + action: str # send_message | get_contacts | post_moments | ... 共 96 个 + params: dict = Field(default_factory=dict, description="action 所需参数") + + +@router.post("/hook/execute", response_model=dict, tags=["Hook模块管理"]) +async def hook_execute(req: HookExecuteRequest): + """ + Hawk Hook 统一执行 — 单接口控制整台手机 + + 任意应用只需调此端点,传 action + params 即可执行 96 种操作。 + 支持 WebSocket(Agent) 与 ADB 双通道,执行更丝滑。 + + 示例: + ```json + {"device_id":"xgfe65eimrrofyws","platform":"wechat","action":"send_message","params":{"to_id":"阿猫","content":"你好"}} + ``` + """ + _check_device_online(req.device_id) + result = await _execute_skill( + req.device_id, req.platform.value, req.action, req.params or {} + ) + payload = result.get("data") if result.get("data") is not None else result + return {"code": 200, "data": payload, "channel_used": "sdk_control"} + + # ============================================================================= # 一、消息管理接口 # ============================================================================= diff --git a/sdk/app/services/connection_priority.py b/sdk/app/services/connection_priority.py new file mode 100644 index 0000000000..4b5625000d --- /dev/null +++ b/sdk/app/services/connection_priority.py @@ -0,0 +1,209 @@ +""" +连接优先级管理器 +5种控制模式的可用性检测、优先级路由和智能降级。 + +优先级(高→低): + 1. Hook/Frida — 深度控制,读写微信内部数据 + 2. Agent WebSocket — 实时双向,设备端App常驻 + 3. ADB 静默控制 — 无需App,ADB直连兜底 + 4. AI Agent — 自然语言编排,依赖以上通道 + 5. scrcpy 可视化 — 投屏+人工接管 +""" + +from __future__ import annotations + +import logging +import subprocess +from dataclasses import dataclass, field +from enum import IntEnum +from typing import Any, Dict, List, Optional + +logger = logging.getLogger(__name__) + + +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 + + def evaluate(self, device_id: str) -> List[ModeStatus]: + 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 + hook_ok = self._check_hook(device_id, hook_module_service) + results.append(ModeStatus( + 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: + adb_serials = adb_manager.scan_devices() + for serial in adb_serials: + adb_dev = adb_manager.get_device(serial) + if adb_dev: + android_id = adb_dev._shell("settings get secure android_id").strip() + import hashlib + md5_id = hashlib.md5(android_id.encode()).hexdigest() + if md5_id == device_id: + ws_detail = f"设备在线(ADB:{serial})但Agent App未连接WebSocket" + break + else: + ws_detail = "设备Agent未连接" + results.append(ModeStatus( + mode=ControlMode.AGENT_WS, + available=ws_ok, + priority=2, + detail=ws_detail, + )) + + # 3. ADB + adb_ok = False + adb_serial = "" + adb_dev_obj = adb_manager.get_device(device_id) + if adb_dev_obj and adb_dev_obj.is_online(): + adb_ok = True + adb_serial = device_id + else: + for serial in adb_manager.scan_devices(): + dev = adb_manager.get_device(serial) + if dev and dev.is_online(): + adb_ok = True + adb_serial = serial + break + 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(依赖底层通道) + has_base = ws_ok or adb_ok or hook_ok + results.append(ModeStatus( + mode=ControlMode.AI_AGENT, + available=has_base, + priority=4, + detail="底层通道就绪" if has_base else "需要至少一个底层通道", + )) + + # 5. scrcpy + scrcpy_ok = adb_ok and self._check_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) + 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 + + 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_hook(device_id: str, hook_service) -> bool: + try: + modules = hook_service.get_device_modules(device_id) if hasattr(hook_service, "get_device_modules") else [] + return bool(modules) + except Exception: + 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 + + +connection_priority = ConnectionPriorityManager() diff --git a/sdk/app/services/wechat_adb_engine.py b/sdk/app/services/wechat_adb_engine.py index 3124bb4167..0235e193aa 100644 --- a/sdk/app/services/wechat_adb_engine.py +++ b/sdk/app/services/wechat_adb_engine.py @@ -150,10 +150,13 @@ class WeChatADBEngine: def _input(self, text: str, clear: bool = False): if clear: - self._sh("input keyevent KEYCODE_CTRL_A") - time.sleep(0.1) - self._sh("input keyevent KEYCODE_DEL") - time.sleep(0.1) + try: + self._sh("input keyevent KEYCODE_CTRL_A", timeout=2) + time.sleep(0.1) + self._sh("input keyevent KEYCODE_DEL", timeout=2) + time.sleep(0.1) + except Exception as e: + logger.info(f"清空输入框失败,继续输入: {e}") has_cjk = any(ord(c) > 127 for c in text) if has_cjk: self._input_cjk(text) @@ -165,9 +168,18 @@ class WeChatADBEngine: def _input_cjk(self, text: str): escaped = text.replace("'", "'\\''").replace('"', '\\"') - result = self._sh(f"am broadcast -a ADB_INPUT_TEXT --es msg '{escaped}' 2>&1") - if "Broadcast completed" not in result: - self._sh(f"input text '{escaped}'") + try: + result = self._sh( + f"am broadcast -a ADB_INPUT_TEXT --es msg '{escaped}' 2>&1", + timeout=2, + ) + if "Broadcast completed" in result: + return + except Exception as e: + logger.info(f"ADB_INPUT_TEXT 不可用,回退系统输入: {e}") + + # 部分 MIUI/HyperOS 设备支持直接 input text 中文,这里作为兜底。 + self._sh(f"input text '{escaped}'", timeout=5) def _ensure_wechat(self) -> bool: cur = self._sh("dumpsys window | grep -E 'mCurrentFocus'") diff --git a/sdk/app/static/hub.html b/sdk/app/static/hub.html new file mode 100644 index 0000000000..21a2f2f1f3 --- /dev/null +++ b/sdk/app/static/hub.html @@ -0,0 +1,1327 @@ + + +
+ + +说明文档、设备控制、Hook、AI Agent、连接协议、架构图和项目管理全部融合到一个站内页面
+