Files
workphone-sdk/sdk/app/services/integration_manifest.py

418 lines
17 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.

"""
对外接口统一清单Integration Manifest
目标:把工作手机 SDK 所有 /api/v3/* 对外接口,按「模块 + 消费方」自动归类,
生成一份机器可读、始终与代码同步的接口总清单,供:
- 存客宝cunkebao 直接对接业务/线索/Hook 事件接口
- 超级管理端superadmin 切换连接方案 / 配置开关 / 全局管控
- AI 数字员工ai_employee 通过 OpenAI / MCP / Agent 网关直接调用控机能力
- 通用common 设备、健康、发现等基础接口
设计原则(模块化 + 接口化 + 清晰化):
- 清单从 FastAPI app.routes 动态扫描生成 → 不手工维护、不会与代码漂移
- 模块归类按「路由前缀规则表 MODULE_RULES」判定
- 消费方归类按「模块 → 消费方映射 MODULE_CONSUMERS」判定
- 只读、零副作用;不依赖真机即可返回(设备相关字段会标注 online 与否)
严禁修改存客宝 / AI 数字员工 / 超级管理端代码——本模块仅暴露工作手机侧出口。
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, Tuple
# ─────────────────────────────────────────────
# 一、消费方定义
# ─────────────────────────────────────────────
CONSUMER_CUNKEBAO = "cunkebao"
CONSUMER_SUPERADMIN = "superadmin"
CONSUMER_AI_EMPLOYEE = "ai_employee"
CONSUMER_COMMON = "common"
CONSUMER_LABELS = {
CONSUMER_CUNKEBAO: "存客宝(直连业务/线索/Hook 事件)",
CONSUMER_SUPERADMIN: "超级管理端(连接方案切换/配置开关/全局管控)",
CONSUMER_AI_EMPLOYEE: "AI 数字员工OpenAI/MCP/Agent 网关调用控机)",
CONSUMER_COMMON: "通用(设备/健康/发现/经验等基础能力)",
}
# ─────────────────────────────────────────────
# 二、模块规则表:路径前缀(去掉 /api/v3→ 模块
# 顺序敏感:先匹配更具体的前缀
# ─────────────────────────────────────────────
# (path_prefix, module_id, module_label)
MODULE_RULES: List[Tuple[str, str, str]] = [
("/cunke-bao", "cunkebao_link", "存客宝对接"),
("/customer", "cunkebao_link", "存客宝对接"),
("/connection/provider", "connection_switch", "连接方案可切换驱动"),
("/connection", "connection", "连接协议与诊断"),
("/stability", "connection", "连接协议与诊断"),
("/gateway/v1", "gateway_openai", "AI 网关 · OpenAI 兼容"),
("/v1/chat", "gateway_openai", "AI 网关 · OpenAI 兼容"),
("/gateway/mcp", "gateway_mcp", "AI 网关 · MCP 协议"),
("/gateway", "gateway", "AI 网关 · REST 聚合/编队"),
("/ai/brain", "ai_brain", "AI Brain 技能注册与调度"),
("/ai", "ai_agent", "AI Agent 控机"),
("/agent", "ai_agent", "AI Agent 控机"),
("/wechat", "wechat_full", "微信全量操作"),
("/message", "wechat_message", "微信消息"),
("/friend", "wechat_friend", "微信好友"),
("/contacts", "wechat_contacts", "微信通讯录"),
("/group", "wechat_group", "微信群"),
("/moments", "wechat_moments", "微信朋友圈"),
("/tag", "wechat_tag", "微信标签"),
("/account", "wechat_account", "微信账号/登录/解封"),
("/auto-register", "wechat_account", "微信账号/登录/解封"),
("/payment", "wechat_payment", "微信支付/转账"),
("/mass-send", "wechat_message", "微信消息"),
# 微信扩展操作(资料/扫码/收藏/表情/文件/位置/通话/小程序/公众号/看一看/评论)
("/profile", "wechat_extra", "微信扩展操作"),
("/scan", "wechat_extra", "微信扩展操作"),
("/favorites", "wechat_extra", "微信扩展操作"),
("/emoji", "wechat_extra", "微信扩展操作"),
("/file", "wechat_extra", "微信扩展操作"),
("/location", "wechat_extra", "微信扩展操作"),
("/call", "wechat_extra", "微信扩展操作"),
("/chat", "wechat_extra", "微信扩展操作"),
("/miniprogram", "wechat_extra", "微信扩展操作"),
("/official-account", "wechat_extra", "微信扩展操作"),
("/discover", "wechat_extra", "微信扩展操作"),
("/comment", "wechat_extra", "微信扩展操作"),
("/search", "wechat_extra", "微信扩展操作"),
("/settings", "wechat_extra", "微信扩展操作"),
("/video-channel", "wechat_extra", "微信扩展操作"),
("/wechat-sport", "wechat_extra", "微信扩展操作"),
("/hook-modules", "hook_modules", "Hook 模块管理"),
("/modules", "hook_modules", "Hook 模块管理"),
("/scripts", "ai_brain", "AI Brain 技能注册与调度"),
("/hook", "hook", "Hook 探测/执行/动作"),
("/anti-ban", "anti_ban", "防风控"),
("/antiban", "anti_ban", "防风控"),
("/fleet", "fleet", "多设备 Fleet 管理"),
("/devices", "devices", "设备管理与控制"),
("/device", "devices", "设备管理与控制"),
("/guard-events", "devices", "设备管理与控制"),
("/heartbeat", "devices", "设备管理与控制"),
("/workbench", "console", "控制台/工作台聚合"),
("/process", "console", "控制台/工作台聚合"),
("/frida", "frida", "Frida 无线管理"),
("/adb", "adb", "ADB 直连(运维兜底,非主控)"),
("/registry", "registry", "多服务器注册中心"),
("/discovery", "discovery", "设备发现"),
("/qrcode", "qrcode", "二维码"),
("/voice", "voice", "语音控制"),
("/capture", "capture", "抓包"),
("/experience", "experience", "经验库"),
("/projects", "projects", "项目管理"),
("/integration", "integration", "对外接口统一清单"),
]
# 模块 → 主消费方(一个模块可服务多个消费方)
MODULE_CONSUMERS: Dict[str, List[str]] = {
"cunkebao_link": [CONSUMER_CUNKEBAO, CONSUMER_SUPERADMIN],
"connection_switch": [CONSUMER_SUPERADMIN, CONSUMER_CUNKEBAO],
"connection": [CONSUMER_SUPERADMIN, CONSUMER_COMMON],
"gateway_openai": [CONSUMER_AI_EMPLOYEE],
"gateway_mcp": [CONSUMER_AI_EMPLOYEE],
"gateway": [CONSUMER_AI_EMPLOYEE, CONSUMER_SUPERADMIN],
"ai_brain": [CONSUMER_AI_EMPLOYEE, CONSUMER_SUPERADMIN],
"ai_agent": [CONSUMER_AI_EMPLOYEE, CONSUMER_CUNKEBAO],
"wechat_full": [CONSUMER_CUNKEBAO, CONSUMER_AI_EMPLOYEE],
"wechat_message": [CONSUMER_CUNKEBAO, CONSUMER_AI_EMPLOYEE],
"wechat_friend": [CONSUMER_CUNKEBAO, CONSUMER_AI_EMPLOYEE],
"wechat_contacts": [CONSUMER_CUNKEBAO, CONSUMER_AI_EMPLOYEE],
"wechat_group": [CONSUMER_CUNKEBAO, CONSUMER_AI_EMPLOYEE],
"wechat_moments": [CONSUMER_CUNKEBAO, CONSUMER_AI_EMPLOYEE],
"wechat_tag": [CONSUMER_CUNKEBAO, CONSUMER_AI_EMPLOYEE],
"wechat_account": [CONSUMER_CUNKEBAO, CONSUMER_SUPERADMIN],
"wechat_extra": [CONSUMER_CUNKEBAO, CONSUMER_AI_EMPLOYEE],
"wechat_payment": [CONSUMER_CUNKEBAO, CONSUMER_AI_EMPLOYEE],
"hook": [CONSUMER_CUNKEBAO, CONSUMER_AI_EMPLOYEE, CONSUMER_SUPERADMIN],
"anti_ban": [CONSUMER_SUPERADMIN, CONSUMER_CUNKEBAO],
"fleet": [CONSUMER_CUNKEBAO, CONSUMER_SUPERADMIN, CONSUMER_AI_EMPLOYEE, CONSUMER_COMMON],
"devices": [CONSUMER_COMMON, CONSUMER_CUNKEBAO, CONSUMER_AI_EMPLOYEE],
"frida": [CONSUMER_SUPERADMIN, CONSUMER_COMMON],
"adb": [CONSUMER_SUPERADMIN, CONSUMER_COMMON],
"hook_modules": [CONSUMER_SUPERADMIN],
"registry": [CONSUMER_SUPERADMIN, CONSUMER_COMMON],
"discovery": [CONSUMER_COMMON, CONSUMER_SUPERADMIN],
"qrcode": [CONSUMER_COMMON],
"voice": [CONSUMER_AI_EMPLOYEE, CONSUMER_COMMON],
"capture": [CONSUMER_SUPERADMIN],
"experience": [CONSUMER_COMMON],
"projects": [CONSUMER_SUPERADMIN, CONSUMER_COMMON],
"console": [CONSUMER_SUPERADMIN, CONSUMER_CUNKEBAO, CONSUMER_COMMON],
"integration": [CONSUMER_SUPERADMIN, CONSUMER_CUNKEBAO, CONSUMER_AI_EMPLOYEE, CONSUMER_COMMON],
}
MODULE_UNKNOWN = ("other", "其它/内部")
# 模块运行时依赖:
# server → SDK 进程在即可用(不依赖具体设备)
# device → 需目标设备 WebSocket 在线u2/ADB 即可,微信类有 u2 兜底)
# hook → 需设备 Frida Hook 已 attach 才能发挥完整能力(否则 u2 部分降级)
MODULE_RUNTIME: Dict[str, str] = {
"cunkebao_link": "server",
"connection_switch": "device",
"connection": "device",
"gateway_openai": "device",
"gateway_mcp": "device",
"gateway": "device",
"ai_brain": "device",
"ai_agent": "device",
"wechat_full": "hook",
"wechat_message": "hook",
"wechat_friend": "hook",
"wechat_contacts": "hook",
"wechat_group": "hook",
"wechat_moments": "hook",
"wechat_tag": "hook",
"wechat_account": "device",
"wechat_extra": "hook",
"wechat_payment": "hook",
"hook": "hook",
"anti_ban": "server",
"fleet": "device",
"devices": "device",
"frida": "device",
"adb": "device",
"hook_modules": "server",
"registry": "server",
"discovery": "server",
"qrcode": "server",
"voice": "device",
"capture": "device",
"experience": "server",
"projects": "device",
"console": "server",
"integration": "server",
"other": "server",
}
# 仅纳入对外清单的方法
_PUBLIC_METHODS = {"GET", "POST", "PUT", "DELETE", "PATCH"}
# 这些路径片段视为内部/非业务,不纳入对外清单
_SKIP_PREFIXES = ("/openapi", "/docs", "/redoc", "/static")
def _classify_path(path: str) -> Tuple[str, str]:
"""返回 (module_id, module_label)。path 形如 /api/v3/cunke-bao/config。"""
rel = path
if rel.startswith("/api/v3"):
rel = rel[len("/api/v3"):]
if not rel.startswith("/"):
rel = "/" + rel
for prefix, mod_id, label in MODULE_RULES:
if rel == prefix or rel.startswith(prefix + "/") or rel.startswith(prefix):
# 更严格:前缀后须是 / 或结束,避免 /tag 误匹配 /tagx
tail = rel[len(prefix):]
if tail == "" or tail.startswith("/"):
return mod_id, label
return MODULE_UNKNOWN
def _consumers_for(module_id: str) -> List[str]:
return MODULE_CONSUMERS.get(module_id, [CONSUMER_COMMON])
def build_manifest(app) -> Dict[str, Any]:
"""
扫描 FastAPI app 路由,生成按模块归类的对外接口清单。
返回:
{
"version": "...",
"base_path": "/api/v3",
"total_endpoints": N,
"consumers": {...标签...},
"modules": [
{"module": "cunkebao_link", "label": "存客宝对接",
"consumers": ["cunkebao","superadmin"],
"endpoints": [{"method","path","summary"} ...]}
]
}
"""
modules: Dict[str, Dict[str, Any]] = {}
total = 0
seen: set[tuple[str, str]] = set()
def add_endpoint(path: str, method: str, summary: str = "") -> None:
nonlocal total
method = method.upper()
if method not in _PUBLIC_METHODS:
return
if not path or any(path.startswith(p) for p in _SKIP_PREFIXES):
return
key = (path, method)
if key in seen:
return
if path.startswith("/api/v3"):
mod_id, label = _classify_path(path)
elif path in ("/health", "/ready", "/"):
mod_id, label = ("devices", "设备管理与控制") if path == "/health" else ("integration", "对外接口统一清单")
else:
mod_id, label = _classify_path(path)
if mod_id == MODULE_UNKNOWN[0]:
return
bucket = modules.setdefault(mod_id, {
"module": mod_id,
"label": label,
"consumers": _consumers_for(mod_id),
"endpoints": [],
})
bucket["endpoints"].append({
"method": method,
"path": path,
"summary": summary,
})
seen.add(key)
total += 1
for route in getattr(app, "routes", []):
path = getattr(route, "path", "") or ""
methods = getattr(route, "methods", None) or set()
if not path or any(path.startswith(p) for p in _SKIP_PREFIXES):
continue
# 只纳入 /api/v3/* 与少量根级业务端点(/health 等归 common
public_methods = [m for m in methods if m in _PUBLIC_METHODS]
if not public_methods:
continue
summary = getattr(route, "summary", "") or ""
if not summary:
endpoint = getattr(route, "endpoint", None)
doc = (endpoint.__doc__ or "").strip() if endpoint else ""
summary = doc.splitlines()[0].strip() if doc else ""
for m in sorted(public_methods):
add_endpoint(path, m, summary)
# 某些运行态下 request.app.routes 可能只暴露局部路由,但 OpenAPI 已完整。
# 用 OpenAPI paths 做只读补齐,保证接口开放清单与 /docs 可见能力一致。
try:
openapi = app.openapi() if hasattr(app, "openapi") else {}
paths = openapi.get("paths", {}) if isinstance(openapi, dict) else {}
for path, path_item in paths.items():
if not isinstance(path_item, dict):
continue
for method, meta in path_item.items():
if str(method).upper() not in _PUBLIC_METHODS:
continue
summary = ""
if isinstance(meta, dict):
summary = str(meta.get("summary") or meta.get("description") or "")
add_endpoint(path, str(method), summary)
except Exception:
# manifest 是发现接口,不应因 OpenAPI 补齐失败影响主服务。
pass
# 排序:模块按 label端点按 path
module_list = []
for mod in modules.values():
mod["endpoints"].sort(key=lambda e: (e["path"], e["method"]))
mod["endpoint_count"] = len(mod["endpoints"])
module_list.append(mod)
module_list.sort(key=lambda m: m["label"])
return {
"version": "1.0.0",
"base_path": "/api/v3",
"generated_from": "fastapi.routes (dynamic, always in-sync)",
"total_endpoints": total,
"module_count": len(module_list),
"consumers": CONSUMER_LABELS,
"modules": module_list,
"rules": {
"note": "工作手机侧出口;严禁改存客宝/AI数字员工/超管代码,仅按本清单对接。",
"transport": "业务下发统一经 WebSocket 主控WORKPHONE_WS_FIRST=1",
},
}
def filter_by_consumer(manifest: Dict[str, Any], consumer: str) -> Dict[str, Any]:
"""从完整清单中筛出某消费方可用的模块与端点。"""
mods = [m for m in manifest.get("modules", []) if consumer in m.get("consumers", [])]
total = sum(m.get("endpoint_count", 0) for m in mods)
return {
"version": manifest.get("version"),
"base_path": manifest.get("base_path"),
"consumer": consumer,
"consumer_label": CONSUMER_LABELS.get(consumer, consumer),
"total_endpoints": total,
"module_count": len(mods),
"modules": mods,
}
def list_modules(manifest: Dict[str, Any]) -> List[Dict[str, Any]]:
"""精简模块目录(不含端点明细)。"""
return [
{
"module": m["module"],
"label": m["label"],
"consumers": m["consumers"],
"endpoint_count": m["endpoint_count"],
"runtime": MODULE_RUNTIME.get(m["module"], "server"),
}
for m in manifest.get("modules", [])
]
def build_capability(
manifest: Dict[str, Any],
device_id: str,
online: bool,
supports_hook: bool,
consumer: Optional[str] = None,
) -> Dict[str, Any]:
"""
把「接口清单(静态)」与「设备实时状态」打通,给出某设备各模块此刻是否可直接调用。
status 取值:
ready 可直接调用
degraded 设备在线但 Frida 未 attach微信类走 u2 部分降级
offline 设备不在线device/hook 类不可用
"""
modules = manifest.get("modules", [])
if consumer:
modules = [m for m in modules if consumer in m.get("consumers", [])]
result_modules = []
counts = {"ready": 0, "degraded": 0, "offline": 0}
for m in modules:
runtime = MODULE_RUNTIME.get(m["module"], "server")
if runtime == "server":
status = "ready"
elif not online:
status = "offline"
elif runtime == "hook":
status = "ready" if supports_hook else "degraded"
else: # device
status = "ready"
counts[status] += 1
result_modules.append({
"module": m["module"],
"label": m["label"],
"runtime": runtime,
"status": status,
"endpoint_count": m["endpoint_count"],
"consumers": m["consumers"],
})
return {
"device_id": device_id,
"consumer": consumer or "all",
"online": online,
"supports_hook": supports_hook,
"summary": counts,
"modules": result_modules,
"legend": {
"ready": "可直接调用",
"degraded": "设备在线但 Frida 未 attach微信类走 u2 部分降级",
"offline": "设备不在线device/hook 类不可用",
},
}