Files
workphone-sdk/sdk/app/main.py

843 lines
33 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
工作手机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、架构", "防封模块架构图.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 初版脚本FridaRPC 骨架、消息监听、脚本部署流程。",
"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",
)
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)