418 lines
17 KiB
Python
418 lines
17 KiB
Python
"""
|
||
对外接口统一清单(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 类不可用",
|
||
},
|
||
}
|