""" 工作手机SDK v3.0 - 主入口 存客宝的AI手机控制引擎 """ from fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse, JSONResponse, Response import hmac from contextlib import asynccontextmanager 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, gateway, registry_cluster, discovery, cunke_bao, frida_wireless, wechat_full, connection_provider, integration, process, fleet from services.ws_hub import ws_hub from services.device_manager import device_manager # 配置日志 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) 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、接口", "05-交互图", "API架构与交互流程图.html"), "anti-ban-architecture": os.path.join(_DOCS_ROOT, "2、架构", "04-交互图", "防封模块架构图.html"), "wechat-server-flow": os.path.join(_DOCS_ROOT, "2、架构", "04-交互图", "微信控制-设备服务器交互图.html"), "dual-channel-device-map": os.path.join(_DOCS_ROOT, "2、架构", "04-交互图", "四端双通道绑定与device-map.html"), } _AGENT_ROOT_CANDIDATES = [ os.path.join(_APP_DIR, "agent"), os.path.normpath(os.path.join(_APP_DIR, "..", "agent")), ] _AGENT_ROOT = next((p for p in _AGENT_ROOT_CANDIDATES if os.path.exists(p)), _AGENT_ROOT_CANDIDATES[0]) _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、手册/02-操作指南", "file": "SDK操作手册.md", "keywords": ["SDK", "手册", "调用", "接入"], }, { "id": "usage-solution", "title": "使用与落地方案", "summary": "从部署到业务落地的整体使用路径与建议方案。", "category": "manual", "section": "说明文档", "folder": "9、手册/02-操作指南", "file": "使用与落地方案.md", "keywords": ["方案", "落地", "部署", "使用"], }, { "id": "wechat-e2e", "title": "微信消息E2E验证指南", "summary": "微信消息链路的端到端验证步骤和排查指引。", "category": "manual", "section": "说明文档", "folder": "9、手册/03-验证", "file": "微信消息E2E验证指南.md", "keywords": ["微信", "E2E", "验证", "排查"], }, { "id": "agent-install", "title": "设备端Agent安装与公司设备说明", "summary": "设备端 Agent 的安装方式、公司设备规范和运行说明。", "category": "manual", "section": "说明文档", "folder": "9、手册/04-专项", "file": "设备端Agent安装与公司设备说明.md", "keywords": ["Agent", "安装", "设备", "说明"], }, { "id": "install-checklist", "title": "安装前配置检查规范", "summary": "安装前需要确认的配置、环境与风险检查项。", "category": "manual", "section": "说明文档", "folder": "9、手册/04-专项", "file": "安装前配置检查规范.md", "keywords": ["安装", "检查", "规范", "配置"], }, { "id": "system-architecture", "title": "系统架构", "summary": "工作手机 SDK 的整体架构、模块分层与链路设计。", "category": "architecture", "section": "架构与经验", "folder": "2、架构", "file": "01-总览/系统架构.md", "keywords": ["架构", "模块", "分层", "链路"], }, { "id": "hook-architecture", "title": "Hook通道与多设备多服务器架构", "summary": "Hook 通道、Frida 集成、多设备与多服务器的设计说明。", "category": "architecture", "section": "架构与经验", "folder": "2、架构", "file": "03-通道与设备/Hook通道与多设备多服务器架构.md", "keywords": ["Hook", "Frida", "多设备", "多服务器"], }, { "id": "api-spec", "title": "接口规范", "summary": "统一接口的字段约定、协议与调用方式。", "category": "api", "section": "接口与协议", "folder": "5、接口", "file": "01-规范与统一层/接口规范.md", "keywords": ["接口", "协议", "字段", "规范"], }, { "id": "cunkebao-spec", "title": "存客宝对接规范", "summary": "存客宝与工作手机 SDK 的对接方式和聚合规则。", "category": "api", "section": "接口与协议", "folder": "5、接口", "file": "02-业务对接/存客宝对接规范.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-project-tree", "项目目录总览", "仓库顶层 sdk / 开发文档 / 机擎 结构一览。"), ("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-project-tree": "images/01-整体总览/工作手机项目目录总览.png", "arch-sdk": "images/01-整体总览/工作手机SDK架构图.png", "arch-flow": "images/03-微信与Hook/设备与服务器交互流程_微信控制.png", "arch-device": "images/01-整体总览/工作手机设备端运转逻辑.png", "arch-agent-total": "images/01-整体总览/卡若AI手机_Agent与服务器工作流总图.png", "arch-hook": "images/03-微信与Hook/Hook与服务器交互流程图.png", "arch-ban": "images/04-防封/防封模块架构图.png", "arch-ban-detail": "images/04-防封/防封服务端核心能力拆细图.png", "mode-hook": "images/02-控制通道/本地控制模式_1_Hook_Frida模式.png", "mode-adb": "images/02-控制通道/本地控制模式_2_ADB静默控制模式.png", "mode-agent": "images/02-控制通道/本地控制模式_3_Agent_WebSocket远控模式.png", "mode-ai": "images/02-控制通道/本地控制模式_4_AI_Agent自然语言模式.png", "mode-scrcpy": "images/02-控制通道/本地控制模式_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", ) import time as _time _overview_cache = {"data": None, "ts": 0} async def _build_workbench_overview() -> dict: now = _time.time() if _overview_cache["data"] and (now - _overview_cache["ts"]) < 10: return _overview_cache["data"] from services.adb_device import adb_manager from services.ai_command import ai_engine from services.connection_priority import connection_priority # ADB 扫描加超时,避免 adb devices 卡住导致 overview 整体超时 try: adb_authorized = await asyncio.wait_for(adb_manager.async_scan_devices(), timeout=5.0) adb_all = await asyncio.wait_for(adb_manager.async_scan_all_devices(), timeout=5.0) except asyncio.TimeoutError: adb_authorized, adb_all = [], [] db_devices = [] try: db_devices = await asyncio.wait_for(device_manager.get_all_devices(limit=200), timeout=3.0) except (asyncio.TimeoutError, Exception): db_devices = [] online_devices = {d["device_id"]: d for d in ws_hub.get_online_devices()} # device_id → serial 映射,用于去重 id_serial_map = dict(adb_manager._deviceid_to_serial) merged = {} # ADB 设备优先(用 serial 做 key) for serial in adb_authorized: adb_dev = adb_manager.get_device(serial) info = adb_dev.get_info() if adb_dev else {} merged[serial] = { "device_id": serial, "serial": serial, "model": info.get("model", "Unknown"), "brand": info.get("brand", "Unknown"), "android_version": info.get("android_version", "Unknown"), "status": "adb", "connection_type": "adb", "controllable": True, "display": info.get("display", {}), } # DB 设备(跳过已通过 serial 匹配到的) existing_device_ids = {m.get("device_id", "") for m in merged.values()} for device in db_devices: device_id = device["device_id"] serial = ( id_serial_map.get(device_id) or device.get("serial") or device.get("adb_serial") or device.get("wifi_serial") or "" ) if serial and serial in merged: merged[serial].update({k: v for k, v in device.items() if v and k not in ("status", "device_id")}) continue if device_id in existing_device_ids: continue merged[device_id] = {**device, "serial": serial} # WebSocket 在线设备 for device_id, info in online_devices.items(): serial = id_serial_map.get(device_id, "") if serial and serial in merged: merged[serial].update({**info, "status": "online"}) elif device_id in merged: merged[device_id].update({**info, "status": "online"}) else: merged[device_id] = {**info, "status": "online", "serial": serial} # adb_all 中未授权/离线设备 known_serials = {m.get("serial", "") for m in merged.values()} | set(merged.keys()) for adb_entry in adb_all: serial = adb_entry["serial"] if serial in known_serials: continue if adb_entry["adb_status"] != "device": merged[serial] = { "device_id": serial, "serial": serial, "model": adb_entry.get("model") or "未知设备", "status": adb_entry["adb_status"], "connection_type": "usb", "controllable": False, "adb_status": adb_entry["adb_status"], "usb": adb_entry.get("usb", ""), "hint": "请在手机上点击「允许 USB 调试」" if adb_entry["adb_status"] == "unauthorized" else "", } # 最终去重:同一物理设备可能有 serial key 和 device_id key seen_serials = set() deduped = {} for key, dev in merged.items(): serial = dev.get("serial", "") if serial and serial in seen_serials: for k2, d2 in deduped.items(): if d2.get("serial") == serial: # 同一物理设备优先保留 WS 在线态,其次保留 controllable=True。 if dev.get("status") == "online" and d2.get("status") != "online": d2.update(dev) elif dev.get("controllable") and not d2.get("controllable"): d2.update(dev) else: for fk, fv in dev.items(): if fv and not d2.get(fk): d2[fk] = fv break continue if serial: seen_serials.add(serial) deduped[key] = dev from services.device_id_util import enrich_device_id_fields physical_serial_models = { device.get("model") for device in deduped.values() if device.get("serial") and ":" not in str(device.get("serial")) and device.get("model") } devices = [] for device in deduped.values(): device_id = str(device.get("device_id") or "") serial = str(device.get("serial") or "") model = device.get("model") is_hex_alias = len(device_id) == 32 and all(char in "0123456789abcdef" for char in device_id.lower()) if model in physical_serial_models and ((is_hex_alias and not serial) or (serial and ":" in serial)): continue if serial and len(device_id) == 32 and all(char in "0123456789abcdef" for char in device_id.lower()): device["device_id"] = serial devices.append(enrich_device_id_fields(device)) online_count = len([d for d in devices if d.get("status") in ("online", "adb")]) adb_count = len([d for d in devices if d.get("status") == "adb"]) unauthorized_count = len([d for d in devices if d.get("status") == "unauthorized"]) offline_count = len([d for d in devices if d.get("status") == "offline"]) try: ai_status = await asyncio.wait_for(ai_engine.get_status(), timeout=3.0) except (asyncio.TimeoutError, Exception): ai_status = {"available": False, "backend": "", "model": ""} docs = [_doc_meta_with_state(meta) for meta in _WORKBENCH_DOCS + _AGENT_DOCS] # 为每台设备附加连接模式信息(并行+单设备超时,避免 overview 整体超时) _MODE_EVAL_TIMEOUT = 5.0 # 单设备评估超时秒数 async def _eval_modes(did: str): try: return await asyncio.wait_for(connection_priority.async_evaluate(did), timeout=_MODE_EVAL_TIMEOUT) except (asyncio.TimeoutError, Exception): return [] device_modes = {} tasks = [_eval_modes(did) for dev in devices if (did := dev.get("device_id", ""))] results = await asyncio.gather(*tasks, return_exceptions=True) idx = 0 for dev in devices: did = dev.get("device_id", "") if not did: continue modes = results[idx] if idx < len(results) and not isinstance(results[idx], Exception) else [] idx += 1 try: 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 result = { "sdk_online": True, "sdk_version": "3.0.0", "ws_online": len(ws_hub.connections), "adb_count": len(adb_authorized), "adb_all": len(adb_all), "device_total": len(devices), "device_online": online_count, "device_offline": offline_count + adb_count, "unauthorized_count": unauthorized_count, "devices": devices, "summary": { "sdk_online": True, "sdk_version": "3.0.0", "ws_online": len(ws_hub.connections), "adb_count": len(adb_authorized), "adb_all": len(adb_all), "unauthorized_count": unauthorized_count, "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}, ], } _overview_cache["data"] = result _overview_cache["ts"] = _time.time() return result @asynccontextmanager async def lifespan(app: FastAPI): """应用生命周期管理""" from services.ai_heartbeat import ai_heartbeat from services.discovery_service import discovery_service logger.info("🚀 工作手机SDK v3.0 启动中...") from services.process_state import mark_process_started mark_process_started() await device_manager.init() logger.info("✅ 数据库连接成功") await ai_heartbeat.start() logger.info("✅ AI 心跳监控已启动") await discovery_service.start_beacon(port=8899) logger.info("✅ 设备发现 beacon 已启动") heartbeat_task = asyncio.create_task(_heartbeat_sweeper()) ai_hb_task = asyncio.create_task(_ai_heartbeat_loop()) yield logger.info("🛑 工作手机SDK 关闭中...") heartbeat_task.cancel() ai_hb_task.cancel() for t in (heartbeat_task, ai_hb_task): try: await t except asyncio.CancelledError: pass await discovery_service.stop_beacon() await ai_heartbeat.stop() await device_manager.close() async def _heartbeat_sweeper(): """后台心跳巡检:清理长时间未上报心跳的设备""" while True: await asyncio.sleep(max(5, settings.WS_HEARTBEAT_INTERVAL)) await ws_hub.sweep_stale_devices(timeout_seconds=max(settings.WS_TIMEOUT, settings.WS_HEARTBEAT_INTERVAL * 3)) async def _ai_heartbeat_loop(): """AI 心跳监控循环:健康评分 + 异常检测 + 任务下发""" from services.ai_heartbeat import ai_heartbeat while True: try: await ai_heartbeat.tick() except Exception as e: logger.error(f"AI 心跳监控异常: {e}") await asyncio.sleep(ai_heartbeat.HEALTH_INTERVAL_SEC) TAGS_METADATA = [ {"name": "消息管理", "description": "发送/获取/转发/撤回/名片/语音消息 — 8个端点"}, {"name": "好友管理", "description": "添加/通过/备注/删除/搜索/批量 — 8个端点"}, {"name": "群聊管理", "description": "创建/邀请/移除/公告/群名/群消息/欢迎语/退群 — 10个端点"}, {"name": "标签管理", "description": "创建/删除/打标/取标/查人 — 6个端点"}, {"name": "朋友圈管理", "description": "发布/点赞/评论/删除/封面/可见天数/分享链接 — 8个端点"}, {"name": "个人设置", "description": "获取资料/昵称/签名/头像/性别/地区 — 6个端点"}, {"name": "账号安全", "description": "状态/解封/安全中心/改密码/申诉/限制检查 — 9个端点"}, {"name": "收藏管理", "description": "收藏/收藏列表 — 2个端点"}, {"name": "支付", "description": "红包/转账/付款码/收款/钱包/账单/领红包 — 7个端点"}, {"name": "聊天设置", "description": "置顶/免打扰/清空记录 — 3个端点"}, {"name": "小程序", "description": "打开小程序 — 1个端点"}, {"name": "公众号", "description": "关注公众号 — 1个端点"}, {"name": "视频号", "description": "列表/点赞/评论/关注/分享 — 5个端点"}, {"name": "扫一扫", "description": "扫码/加好友/我的二维码/图片识别 — 4个端点"}, {"name": "通话", "description": "语音通话/视频通话 — 2个端点"}, {"name": "群发助手", "description": "群发消息 — 1个端点"}, {"name": "搜一搜", "description": "微信搜一搜 — 1个端点"}, {"name": "看一看", "description": "微信看一看 — 1个端点"}, {"name": "微信运动", "description": "步数/点赞 — 2个端点"}, {"name": "位置", "description": "发送位置/共享实时位置 — 2个端点"}, {"name": "表情", "description": "发送表情/表情包列表 — 2个端点"}, {"name": "文件管理", "description": "发送文件/下载文件 — 2个端点"}, {"name": "设置", "description": "勿扰/清缓存/检查更新/退出/切换账号 — 5个端点"}, {"name": "设备管理", "description": "设备列表/状态/截图/ADB控制"}, {"name": "AI Agent", "description": "自然语言控制手机"}, {"name": "防封监控", "description": "操作频率/内容/账号生命周期 — 2个端点"}, ] app = FastAPI( title="机擎 v3.0 · 工作手机SDK API", description=""" ## 工作手机SDK — 微信全功能控制接口 通过 ADB UI 自动化后端静默控制 Android 手机上的微信,**无需 Root**。 ### 核心能力 - **98 个 API 端点**,覆盖微信全部功能 - **96 个引擎方法**(WeChatADBEngine) - **23 个功能模块**(消息/好友/群聊/支付/解封/视频号/扫一扫...) - **导航缓存系统**:首次录制坐标,后续免 UI dump - **操作日志**:每次操作自动记录步骤/耗时/结果 - **容错机制**:自动重试 3 次 + 状态恢复 ### 支持设备 - Redmi Note 13 5G (Android 13, 1080×2400) - 微信 8.0.56 ### 调用方式 ``` POST http://{服务器IP}:8899/api/v3/message/send Content-Type: application/json { "device_id": "xgfe65eimrrofyws", "platform": "wechat", "to_id": "好友昵称", "content": "你好" } ``` ### 公共参数 | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | device_id | string | ✅ | ADB 设备序列号 | | platform | string | ✅ | 固定 `wechat` | """, version="3.0.0", lifespan=lifespan, openapi_tags=TAGS_METADATA, docs_url="/docs", redoc_url="/redoc", ) # CORS配置 app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) _PUBLIC_HTTP_PATHS = { "/", "/health", "/ready", "/openapi.json", "/docs", "/redoc", "/docs/oauth2-redirect", "/llms.txt", "/favicon.ico", } @app.middleware("http") async def require_production_api_key(request: Request, call_next): """生产环境保护控制接口;健康探针和文档保持可读。""" path = request.url.path.rstrip("/") or "/" # 浏览器跨域调用会先发送不携带业务密钥的 OPTIONS 预检;应交给 # CORSMiddleware 返回允许头,真实 GET/POST 请求仍必须通过 API Key。 public = request.method == "OPTIONS" or path in _PUBLIC_HTTP_PATHS or path.startswith("/static/") if settings.API_AUTH_ENABLED and not public: supplied = request.headers.get("X-API-Key", "").strip() auth = request.headers.get("Authorization", "").strip() if not supplied and auth.lower().startswith("bearer "): supplied = auth[7:].strip() expected = settings.API_KEY.strip() if not expected or not hmac.compare_digest(supplied, expected): return JSONResponse(status_code=401, content={"code": 401, "message": "API Key 无效"}) return await call_next(request) # 注册路由 app.include_router(devices.router, prefix="/api/v3", tags=["设备管理"]) app.include_router(unified.router, prefix="/api/v3", tags=["统一接口"]) app.include_router(agent.router, prefix="/api/v3", tags=["AI Agent"]) app.include_router(adb.router, tags=["ADB设备控制"]) app.include_router(experience.router, tags=["经验库"]) app.include_router(projects.router, prefix="/api/v3", tags=["项目管理"]) app.include_router(qrcode.router, prefix="/api/v3", tags=["二维码"]) app.include_router(voice.router, prefix="/api/v3", tags=["语音控制"]) app.include_router(capture.router, tags=["抓包"]) app.include_router(hook_modules.router, prefix="/api/v3", tags=["Hook模块管理"]) app.include_router(connection.router, prefix="/api/v3", tags=["连接协议"]) app.include_router(connection_provider.router, prefix="/api/v3", tags=["连接方案可切换驱动"]) app.include_router(gateway.router, prefix="/api/v3", tags=["外部对接网关"]) app.include_router(registry_cluster.router, tags=["多服务器注册中心"]) app.include_router(discovery.router, tags=["设备发现"]) app.include_router(cunke_bao.router, prefix="/api/v3", tags=["存客宝对接"]) app.include_router(frida_wireless.router, prefix="/api/v3", tags=["Frida无线管理"]) app.include_router(wechat_full.router, prefix="/api/v3", tags=["微信全量操作"]) app.include_router(integration.router, prefix="/api/v3", tags=["对外接口统一清单"]) app.include_router(process.router, prefix="/api/v3", tags=["进程状态"]) app.include_router(fleet.router, prefix="/api/v3", tags=["多设备管理"]) # ========== 健康检查 ========== @app.get("/health") async def health_check(): """健康检查""" from services.adb_device import adb_manager adb_devices = await adb_manager.async_scan_devices() return { "status": "healthy", "version": "3.0.0", "devices_online": len(ws_hub.connections), "device_ids": list(ws_hub.connections.keys()), "adb_devices": len(adb_devices), "adb_serials": adb_devices } @app.get("/ready") async def ready(): """就绪探针(部署/负载均衡用):进程已启动且可接收流量""" return {"ready": True, "version": "3.0.0"} @app.get("/llms.txt") async def llms_txt(): """AI/LLM 可读入口:声明接口网站、OpenAPI、MCP 与对接手册。""" static_path = os.path.join(_STATIC_DIR, "llms.txt") if os.path.exists(static_path): return FileResponse(static_path, media_type="text/plain; charset=utf-8") raise HTTPException(status_code=404, detail="llms.txt 不存在") @app.get("/") async def root(): """根路由 → 聚合总控台""" 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": "统一聚合总控台", "docs": "/docs", "health": "/health", "ready": "/ready", "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(_STATIC_DIR, "voice_control.html") return FileResponse(static_path) @app.get("/wechat") async def wechat_control_page(): """微信全链路控制面板(96个action / 29个功能模块)""" static_path = os.path.join(_STATIC_DIR, "wechat_control.html") if os.path.exists(static_path): return FileResponse(static_path) raise HTTPException(status_code=404, detail="wechat_control.html 不存在") @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) _WORKBENCH_OVERVIEW_TIMEOUT = 15.0 # 整体超时秒数,避免 ADB/设备端操作阻塞 def _overview_fallback() -> dict: """overview 超时或异常时的降级响应""" return { "sdk_online": True, "sdk_version": "3.0.0", "ws_online": len(ws_hub.connections), "adb_count": 0, "adb_all": 0, "device_total": 0, "device_online": 0, "device_offline": 0, "unauthorized_count": 0, "devices": [], "summary": {"sdk_online": True, "sdk_version": "3.0.0", "manual_docs": 0, "architecture_assets": 0}, "agent": {"available": False, "backend": "", "model": ""}, "docs": [], "architecture_assets": [], "sections": [], "control_modes": [], "timeout_fallback": True, } @app.get("/api/v3/workbench/overview", tags=["工作台概览"]) async def workbench_overview(): """工作台统一概览 DTO,供工作台前端与存客宝页面共用。""" try: data = await asyncio.wait_for(_build_workbench_overview(), timeout=_WORKBENCH_OVERVIEW_TIMEOUT) return {"code": 200, "data": data} except asyncio.TimeoutError: logger.warning("workbench/overview 构建超时,返回缓存或降级响应") if _overview_cache["data"]: return {"code": 200, "data": _overview_cache["data"]} return {"code": 200, "data": _overview_fallback()} @app.get("/api/v3/workbench/docs", tags=["工作台概览"]) 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}", tags=["工作台概览"]) 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), }, } # 挂载静态文件目录 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(_APP_DIR, "..", "agent")) _AGENT_DIST = os.path.join(_AGENT_DIR, "dist", "agent.tar.gz") @app.get("/api/v3/agent/download", tags=["设备管理"]) async def download_agent(): """ 下载设备端 Agent 打包文件(agent.tar.gz) 设备端 install.sh 会调用此接口自动下载代码。 如果 dist/agent.tar.gz 不存在,自动运行 package.sh 打包。 """ # 自动打包(如果 dist 不存在或过期) if not os.path.exists(_AGENT_DIST): package_sh = os.path.join(_AGENT_DIR, "package.sh") if os.path.exists(package_sh): try: subprocess.run(["bash", package_sh], cwd=_AGENT_DIR, check=True, timeout=30) logger.info("Agent 代码已自动打包") except Exception as e: logger.error(f"自动打包失败: {e}") return {"error": "Agent 打包失败,请在服务器手动执行 agent/package.sh"} if os.path.exists(_AGENT_DIST): return FileResponse( _AGENT_DIST, media_type="application/gzip", filename="agent.tar.gz", ) return {"error": "agent.tar.gz 不存在"} @app.get("/install.sh") async def serve_install_script(): """ 提供 Termux 一键安装脚本 用法: curl -sL http://服务器IP:8899/install.sh | bash -s -- --server ws://服务器IP:8899/ws/device """ install_sh = os.path.join(_AGENT_DIR, "install.sh") if os.path.exists(install_sh): return FileResponse(install_sh, media_type="text/plain", filename="install.sh") return Response(content="echo '❌ install.sh 不存在'\n", media_type="text/plain") # ========== WebSocket 设备连接 ========== @app.websocket("/ws/device/{device_id}") async def device_websocket(websocket: WebSocket, device_id: str): """设备WebSocket连接入口""" expected_token = settings.DEVICE_PAIRING_TOKEN.strip() supplied_token = websocket.query_params.get("token", "").strip() if expected_token and not hmac.compare_digest(supplied_token, expected_token): await websocket.close(code=4401, reason="invalid pairing token") return await ws_hub.connect(websocket, device_id) try: while True: data = await websocket.receive_json() await ws_hub.handle_message(device_id, data) except WebSocketDisconnect: await ws_hub.disconnect(device_id) except Exception as e: logger.error(f"WebSocket错误 [{device_id}]: {e}") await ws_hub.disconnect(device_id) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8899)