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、连接协议、架构图和项目管理全部融合到一个站内页面

+
+
+
+ 检测中 + WS 0 + ADB 0 + v- +
+
+ +
+ +
+
+
+
+
+ +
机擎 v3.0 · 工作手机SDK · 统一工作台 · 站内文档渲染 · Agent 集群
+
+ + + + + + diff --git a/sdk/data/nav_cache/nav_v8.0.56_1080x2400.json b/sdk/data/nav_cache/nav_v8.0.56_1080x2400.json index 34172af3b8..6fc3c222e2 100644 --- a/sdk/data/nav_cache/nav_v8.0.56_1080x2400.json +++ b/sdk/data/nav_cache/nav_v8.0.56_1080x2400.json @@ -1,7 +1,7 @@ { "version": "8.0.56", "screen": "1080x2400", - "updated": "2026-03-14 17:57:15", + "updated": "2026-03-16 03:26:29", "pages": { "me": { "recorded_at": "2026-03-14 16:32", @@ -323,9 +323,9 @@ } ], "target_page": "account_security", - "success_count": 1, + "success_count": 3, "fail_count": 0, - "last_success": "2026-03-14 17:57:15" + "last_success": "2026-03-16 03:26:29" }, "to_safety_center": { "name": "to_safety_center", diff --git a/sdk/data/operation_logs/ops_20260316.jsonl b/sdk/data/operation_logs/ops_20260316.jsonl new file mode 100644 index 0000000000..a0227003de --- /dev/null +++ b/sdk/data/operation_logs/ops_20260316.jsonl @@ -0,0 +1,7 @@ +{"action": "get_profile", "params": {}, "device": "dc9c23e00510", "started_at": "2026-03-16 03:19:20", "steps": [{"type": "tap_tab", "detail": "我", "coords": [945, 2148], "success": true, "ts": 7.69}], "success": true, "duration_ms": 12246, "ended_at": "2026-03-16 03:19:33", "result_summary": "{'success': True, 'mode': 'adb', 'timestamp': 1773602373089, 'profile': {}}"} +{"action": "get_contacts", "params": {"limit": 5}, "device": "dc9c23e00510", "started_at": "2026-03-16 03:19:58", "steps": [{"type": "tap_tab", "detail": "通讯录", "coords": [405, 2148], "success": true, "ts": 7.16}], "success": true, "duration_ms": 8164, "ended_at": "2026-03-16 03:20:07", "result_summary": "{'success': True, 'mode': 'adb', 'timestamp': 1773602407116, 'contacts': [], 'count': 0}"} +{"action": "check_account_status", "params": {}, "device": "dc9c23e00510", "started_at": "2026-03-16 03:19:33", "steps": [{"type": "tap_tab", "detail": "我", "coords": [945, 2148], "success": true, "ts": 8.07}, {"type": "tap_tab", "detail": "我", "coords": [945, 2148], "success": true, "ts": 9.58}, {"type": "scroll_up", "detail": "", "coords": null, "success": true, "ts": 13.0}, {"type": "tap_text", "detail": "设置", "coords": [197, 1676], "success": true, "ts": 17.3}, {"type": "scroll_to_top", "detail": "", "coords": null, "success": true, "ts": 26.49}, {"type": "tap_text", "detail": "账号与安全", "coords": [250, 300], "success": true, "ts": 30.81}], "success": true, "duration_ms": 38876, "ended_at": "2026-03-16 03:20:12", "result_summary": "{'success': True, 'mode': 'adb', 'timestamp': 1773602412573, 'status': {'account_active': True}}"} +{"action": "send_message", "params": {"to_id": "文件传输助手", "content": "新设备 dc9c23e00510 已接入,可直接控制。"}, "device": "dc9c23e00510", "started_at": "2026-03-16 03:25:30", "steps": [], "success": false, "duration_ms": 24899, "ended_at": "2026-03-16 03:25:55", "result_summary": "{'success': False, 'error': 'Command \\'[\\'adb\\', \\'-s\\', \\'dc9c23e00510\\', \\'shell\\', \"am broadcast -a ADB_INPUT_TEXT --es msg \\'文件传输助手\\' 2>&1\"]\\' timed out after 10 seconds', 'mode': 'adb'}", "error": "Command '['adb', '-s', 'dc9c23e00510', 'shell', \"am broadcast -a ADB_INPUT_TEXT --es msg '文件传输助手' 2>&1\"]' timed out after 10 seconds"} +{"action": "check_account_status", "params": {}, "device": "dc9c23e00510", "started_at": "2026-03-16 03:25:56", "steps": [{"type": "tap_tab", "detail": "我", "coords": [945, 2148], "success": true, "ts": 7.69}, {"type": "tap_tab", "detail": "我", "coords": [945, 2148], "success": true, "ts": 9.2}, {"type": "scroll_up", "detail": "", "coords": null, "success": true, "ts": 13.09}, {"type": "tap_text", "detail": "设置", "coords": [197, 1676], "success": true, "ts": 18.37}, {"type": "scroll_to_top", "detail": "", "coords": null, "success": true, "ts": 28.15}, {"type": "tap_text", "detail": "账号与安全", "coords": [250, 300], "success": true, "ts": 32.43}], "success": true, "duration_ms": 40568, "ended_at": "2026-03-16 03:26:37", "result_summary": "{'success': True, 'mode': 'adb', 'timestamp': 1773602797298, 'status': {'account_active': True}}"} +{"action": "send_message", "params": {"to_id": "阿猫", "content": "新设备已接入,现在可以直接控制。"}, "device": "dc9c23e00510", "started_at": "2026-03-16 03:28:06", "steps": [], "success": false, "duration_ms": 19579, "ended_at": "2026-03-16 03:28:26", "result_summary": "{'success': False, 'error': \"Command '['adb', '-s', 'dc9c23e00510', 'shell', 'input keyevent KEYCODE_CTRL_A']' timed out after 10 seconds\", 'mode': 'adb'}", "error": "Command '['adb', '-s', 'dc9c23e00510', 'shell', 'input keyevent KEYCODE_CTRL_A']' timed out after 10 seconds"} +{"action": "send_message", "params": {"to_id": "阿猫", "content": "新设备已接入,现在可以直接控制。"}, "device": "dc9c23e00510", "started_at": "2026-03-16 03:29:06", "steps": [], "success": true, "duration_ms": 30823, "ended_at": "2026-03-16 03:29:36", "result_summary": "{'success': True, 'mode': 'adb', 'timestamp': 1773602976885, 'message_id': 'adb_1773602976885'}"} diff --git a/sdk/requirements.txt b/sdk/requirements.txt index 7bbdb1a63d..819e157afe 100644 --- a/sdk/requirements.txt +++ b/sdk/requirements.txt @@ -15,6 +15,7 @@ pydantic-settings # HTTP客户端 httpx +markdown # 设备控制 uiautomator2 diff --git a/开发文档/10、项目管理/工作日志.md b/开发文档/10、项目管理/工作日志.md index 3e13c37a22..c797de8031 100644 --- a/开发文档/10、项目管理/工作日志.md +++ b/开发文档/10、项目管理/工作日志.md @@ -5,6 +5,38 @@ --- +## 2026-03-16 | 设备部署:Redmi 21121119SC Soul + Hook + Agent 安装 + +### 执行人: 阿机(设备部署)+ 阿服(环境) + +### 设备信息 +- 型号: Redmi 21121119SC (selene) +- Android: 11 | 架构: arm64-v8a | Root: Magisk +- 序列号: dc9c23e00510 +- 连接方式: Type-C USB + +### 完成内容 +1. **Soul APP 安装** — 从 APKPure 下载 XAPK (284MB),拆包后 `adb install-multiple` 安装成功 + - 包名: cn.soulapp.android + - 启动 Activity: `.component.startup.main.MainActivity` +2. **Frida Server 部署** — v17.8.1 arm64,已推送到 `/data/local/tmp/frida-server` + - 非 root 下可枚举进程;root attach 需手机上 Magisk 授权(待操作) +3. **uiautomator2 UI 控制验证** — 验证通过,可完全控制设备 + Soul APP +4. **Agent 终端安装**(Termux 内 `~/workphone-agent/`) + - Python 3.13.12 + websockets 16.0 + uiautomator2 3.5.0 + httpx 0.28.1 + Pillow 12.1.1 + - config.json 已配置(device_id=dc9c23e00510, server=ws://192.168.1.100:8899/ws/device) + - Soul Skill 已包含在 agent 包中 +5. **package.sh 更新** — 新增 Soul 技能目录到打包脚本 + +### 待操作 +- 手机上 Magisk 授权 root → Frida 可 attach 到 Soul 进程进行深度 Hook +- 启动 SDK 服务器后运行 `cd ~/workphone-agent && python3 agent.py` 连接 +- Soul APP 首次打开需登录账号 + +### 进度: 设备部署 100% | 整体项目 96%(不变) + +--- + ## 2026-03-15 | 防封模块深度验证+13项BUG修复 ### 执行人: 火炬(机擎全员) diff --git a/开发文档/2、架构/Hook与服务器交互流程图.png b/开发文档/2、架构/Hook与服务器交互流程图.png new file mode 100644 index 0000000000..14c61c5e8c Binary files /dev/null and b/开发文档/2、架构/Hook与服务器交互流程图.png differ diff --git a/开发文档/2、架构/卡若AI手机_Agent与服务器工作流总图.png b/开发文档/2、架构/卡若AI手机_Agent与服务器工作流总图.png new file mode 100644 index 0000000000..dd610c3534 Binary files /dev/null and b/开发文档/2、架构/卡若AI手机_Agent与服务器工作流总图.png differ diff --git a/开发文档/2、架构/工作手机SDKv3_系统架构图.png b/开发文档/2、架构/工作手机SDKv3_系统架构图.png deleted file mode 100644 index 9d4191b861..0000000000 Binary files a/开发文档/2、架构/工作手机SDKv3_系统架构图.png and /dev/null differ diff --git a/开发文档/2、架构/工作手机微信控制系统架构图.png b/开发文档/2、架构/工作手机微信控制系统架构图.png deleted file mode 100644 index 64d76b05fa..0000000000 Binary files a/开发文档/2、架构/工作手机微信控制系统架构图.png and /dev/null differ diff --git a/开发文档/2、架构/本地控制模式_1_Hook_Frida模式.png b/开发文档/2、架构/本地控制模式_1_Hook_Frida模式.png new file mode 100644 index 0000000000..7849c5284a Binary files /dev/null and b/开发文档/2、架构/本地控制模式_1_Hook_Frida模式.png differ diff --git a/开发文档/2、架构/本地控制模式_2_ADB静默控制模式.png b/开发文档/2、架构/本地控制模式_2_ADB静默控制模式.png new file mode 100644 index 0000000000..f16fd3b471 Binary files /dev/null and b/开发文档/2、架构/本地控制模式_2_ADB静默控制模式.png differ diff --git a/开发文档/2、架构/本地控制模式_3_Agent_WebSocket远控模式.png b/开发文档/2、架构/本地控制模式_3_Agent_WebSocket远控模式.png new file mode 100644 index 0000000000..17e514d419 Binary files /dev/null and b/开发文档/2、架构/本地控制模式_3_Agent_WebSocket远控模式.png differ diff --git a/开发文档/2、架构/本地控制模式_4_AI_Agent自然语言模式.png b/开发文档/2、架构/本地控制模式_4_AI_Agent自然语言模式.png new file mode 100644 index 0000000000..051a3b9dec Binary files /dev/null and b/开发文档/2、架构/本地控制模式_4_AI_Agent自然语言模式.png differ diff --git a/开发文档/2、架构/本地控制模式_5_scrcpy可视化控制模式.png b/开发文档/2、架构/本地控制模式_5_scrcpy可视化控制模式.png new file mode 100644 index 0000000000..e9730fa7d2 Binary files /dev/null and b/开发文档/2、架构/本地控制模式_5_scrcpy可视化控制模式.png differ