将架构文档按 01-总览~05-规范 分层,12 张 PNG 迁入 images/ 四类子目录; 新增阿表开发文档结构规范 Skill 与 Obsidian 目录配色 snippet;同步 Hub 扫描路径。 Co-authored-by: Cursor <cursoragent@cursor.com>
843 lines
33 KiB
Python
843 lines
33 KiB
Python
"""
|
||
工作手机SDK v3.0 - 主入口
|
||
存客宝的AI手机控制引擎
|
||
"""
|
||
|
||
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.staticfiles import StaticFiles
|
||
from fastapi.responses import FileResponse, Response
|
||
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
|
||
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、接口", "API架构与交互流程图.html"),
|
||
"anti-ban-architecture": os.path.join(_DOCS_ROOT, "2、架构", "04-交互图", "防封模块架构图.html"),
|
||
"wechat-server-flow": os.path.join(_DOCS_ROOT, "2、架构", "04-交互图", "微信控制-设备服务器交互图.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": "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": "接口规范.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": "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)
|
||
serial_id_map = {v: k for k, v in id_serial_map.items()}
|
||
|
||
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 {}
|
||
device_id = serial_id_map.get(serial, serial)
|
||
merged[serial] = {
|
||
"device_id": device_id,
|
||
"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, "")
|
||
if serial and serial in merged:
|
||
merged[serial].update({k: v for k, v in device.items() if v and k != "status"})
|
||
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:
|
||
# 合并到已有条目(保留 controllable=True 的优先级)
|
||
if 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
|
||
|
||
devices = list(deduped.values())
|
||
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 启动中...")
|
||
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=["*"],
|
||
)
|
||
|
||
# 注册路由
|
||
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(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.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("/")
|
||
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")
|
||
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")
|
||
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),
|
||
},
|
||
}
|
||
|
||
|
||
# 挂载静态文件目录
|
||
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")
|
||
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连接入口"""
|
||
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)
|