Files
workphone-sdk/sdk/app/routers/integration.py
2026-08-10 22:18:38 +08:00

410 lines
16 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.

"""
对外接口统一清单 · 集成中心路由
路由前缀:/api/v3/integration
供存客宝 / 超级管理端 / AI 数字员工 直接发现并调用工作手机 SDK 的全部对外接口:
- GET /integration/manifest 全量接口清单(按模块归类)
- GET /integration/modules 模块目录(精简,不含端点明细)
- GET /integration/consumers 消费方列表
- GET /integration/consumers/{consumer} 某消费方可用的接口子集
- GET /integration/health 关键集成点健康聚合WS/连接方案/存客宝/网关)
清单从 FastAPI 路由动态生成,始终与代码同步;本路由只读、零副作用。
严禁修改存客宝 / AI 数字员工 / 超管代码——本中心仅暴露工作手机侧出口。
"""
from __future__ import annotations
from typing import Any, Dict
import hashlib
import json
from fastapi import APIRouter, HTTPException, Request
from services.integration_manifest import (
build_manifest,
build_capability,
filter_by_consumer,
list_modules,
CONSUMER_LABELS,
)
from services.evidence_receipt import build_receipt
from services.ai_provider import runtime_status
from services.karuo_device_ai import brain_runtime_status
router = APIRouter()
def _runtime_ws_hub(request: Request | None = None):
"""只取应用已经绑定的运行时Hub不另建连接对象。"""
if request is not None:
bound = getattr(getattr(request.app, "state", None), "ws_hub", None)
if bound is not None:
return bound
from services.ws_hub import ws_hub
return ws_hub
def _ok(data: Any, *, readback: Any = None) -> Dict[str, Any]:
return build_receipt(
data=data,
success=True,
code=200,
channel="control-plane",
readback=readback or {"verified": True, "write_performed": False, "source": "control_plane"},
)
@router.get("/integration/manifest", tags=["对外接口统一清单"])
async def integration_manifest(request: Request) -> Dict[str, Any]:
"""全量对外接口清单(按模块 + 消费方归类,机器可读)。"""
manifest = build_manifest(request.app)
return _ok(manifest)
@router.get("/integration/modules", tags=["对外接口统一清单"])
async def integration_modules(request: Request) -> Dict[str, Any]:
"""模块目录(精简:模块/标签/消费方/端点数)。"""
manifest = build_manifest(request.app)
return _ok({
"module_count": manifest["module_count"],
"total_endpoints": manifest["total_endpoints"],
"modules": list_modules(manifest),
})
@router.get("/integration/openapi-consistency", tags=["OpenAPI一致性"])
async def integration_openapi_consistency(request: Request) -> Dict[str, Any]:
"""只读检查接口清单与当前运行程序是否来自同一份路由。"""
schema = request.app.openapi()
paths = schema.get("paths") if isinstance(schema, dict) else {}
paths = paths if isinstance(paths, dict) else {}
method_count = sum(
1 for methods in paths.values()
if isinstance(methods, dict)
for method in methods
if str(method).lower() in {"get", "post", "put", "patch", "delete", "options", "head"}
)
@router.get("/integration/openapi-evidence", tags=["OpenAPI一致性"])
async def integration_openapi_evidence(request: Request) -> Dict[str, Any]:
"""OpenAPI验收回执不改变标准的 /openapi.json 文档地址。"""
schema = request.app.openapi()
encoded = json.dumps(schema, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
paths = schema.get("paths") if isinstance(schema, dict) else {}
paths = paths if isinstance(paths, dict) else {}
method_count = sum(
1
for methods in paths.values()
if isinstance(methods, dict)
for method in methods
if str(method).lower() in {"get", "post", "put", "patch", "delete", "options", "head"}
)
data = {
"standard_openapi_url": "/openapi.json",
"acceptance_url": "/api/v3/integration/openapi-evidence",
"schema_source": "fastapi_runtime_app",
"path_count": len(paths),
"method_count": method_count,
"schema_sha256": hashlib.sha256(encoded).hexdigest(),
"schema_read": True,
}
return build_receipt(
data=data,
success=True,
code=200,
channel="control-plane",
raw_receipt={
"kind": "control_plane_openapi_snapshot",
"source": "fastapi_runtime_app",
"schema_sha256": data["schema_sha256"],
"path_count": len(paths),
"method_count": method_count,
},
readback={
"verified": True,
"write_performed": False,
"source": "runtime_openapi_schema",
"standard_openapi_preserved": True,
"schema_sha256": data["schema_sha256"],
},
)
return _ok(
{
"schema_source": "fastapi_runtime_app",
"path_count": len(paths),
"method_count": method_count,
"consistent": True,
"error_model": "统一外层回执code/success/trace_id/channel_used/raw_receipt/readback",
"evidence_contract": {
"trace_id": True,
"channel_used": "control-plane",
"raw_receipt": True,
"readback_verified": True,
"write_performed": False,
},
},
readback={
"verified": True,
"write_performed": False,
"source": "runtime_openapi_schema",
"schema_read": True,
},
)
@router.get("/integration/consumers", tags=["对外接口统一清单"])
async def integration_consumers() -> Dict[str, Any]:
"""消费方列表(存客宝/超管/AI数字员工/通用)。"""
return _ok({
"consumers": [
{"id": cid, "label": label} for cid, label in CONSUMER_LABELS.items()
]
})
@router.get("/integration/consumers/{consumer}", tags=["对外接口统一清单"])
async def integration_consumer_view(consumer: str, request: Request) -> Dict[str, Any]:
"""
某消费方可直接调用的接口子集。
consumer ∈ {cunkebao, superadmin, ai_employee, common}
"""
if consumer not in CONSUMER_LABELS:
raise HTTPException(
status_code=404,
detail=f"未知消费方: {consumer},可选: {list(CONSUMER_LABELS.keys())}",
)
manifest = build_manifest(request.app)
return _ok(filter_by_consumer(manifest, consumer))
@router.get("/integration/capability/{device_id}", tags=["对外接口统一清单"])
async def integration_capability(
device_id: str,
request: Request,
consumer: str = "",
probe: bool = False,
) -> Dict[str, Any]:
"""
某设备的实时能力矩阵各模块此刻是否可直接调用ready/degraded/offline
- consumer: 可选仅看某消费方cunkebao/superadmin/ai_employee/common的模块
- probe=true: 触发一次轻量 Frida 探测刷新 supports_hook仅 ping/version/profile
不做全量 174 探针,避免 Frida 瞬断);默认读最近一次探测缓存。
"""
if consumer and consumer not in CONSUMER_LABELS:
raise HTTPException(
status_code=404,
detail=f"未知消费方: {consumer},可选: {list(CONSUMER_LABELS.keys())}",
)
# 1) 设备在线
online = False
try:
ws_hub = _runtime_ws_hub(request)
online = ws_hub.is_online(device_id)
except Exception: # noqa: BLE001
online = False
# 2) supports_hook默认读最近探测缓存probe=true 时轻量刷新
supports_hook = False
if online and probe:
try:
from routers.unified import hook_probe # 复用安全的轻量探测
pr = await hook_probe(device_id)
supports_hook = bool(pr.get("supports_hook"))
except Exception: # noqa: BLE001
supports_hook = False
else:
try:
from services.hook_module_service import hook_module_service
dev = await hook_module_service.get_device_modules(device_id)
if isinstance(dev, dict):
supports_hook = bool(dev.get("supports_hook"))
except Exception: # noqa: BLE001
supports_hook = False
# 退化补充:仅接受 Agent 明确上报的 Hook/RPC 能力。
# frida_available/frida_server 只能说明服务端口可用,不能证明微信已注入。
if not supports_hook:
try:
ws_hub = _runtime_ws_hub(request)
info = ws_hub.get_device_info(device_id) or {}
capabilities = set(info.get("capabilities") or [])
supports_hook = bool({"hook", "frida_rpc"} & capabilities)
except Exception: # noqa: BLE001
pass
manifest = build_manifest(request.app)
cap = build_capability(
manifest, device_id, online, supports_hook,
consumer=consumer or None,
)
cap["probed"] = bool(probe and online)
return _ok(cap, readback={"verified": True, "write_performed": False, "source": "capability_cache"})
@router.get("/integration/health", tags=["对外接口统一清单"])
async def integration_health(request: Request) -> Dict[str, Any]:
"""
关键集成点健康聚合:
- websocket 主控(在线设备数)
- 连接方案(当前生效 provider
- 存客宝对接(是否已配置启用)
- AI 网关(可用协议)
供超管/存客宝一眼看清「能不能直接对接」。
"""
health: Dict[str, Any] = {"ok": True, "checks": {}}
# 1) WebSocket 主控
try:
ws_hub = _runtime_ws_hub(request)
online = list(ws_hub.connections.keys())
health["checks"]["websocket"] = {
"ok": True,
"online_devices": len(online),
"device_ids": online,
}
except Exception as e: # noqa: BLE001
health["checks"]["websocket"] = {"ok": False, "error": str(e)}
health["ok"] = False
# 2) 连接方案(可切换驱动)
try:
from services.connection_provider import connection_provider_manager as mgr
active = mgr.active_config()
health["checks"]["connection_provider"] = {
"ok": True,
"active": active,
"providers": [p.get("id") for p in mgr.list_providers()],
}
except Exception as e: # noqa: BLE001
health["checks"]["connection_provider"] = {"ok": False, "error": str(e)}
# 3) 存客宝对接配置
try:
from services.cunke_bao_service import cunke_bao_service
cfg = cunke_bao_service.config
health["checks"]["cunkebao"] = {
"ok": True,
"enabled": bool(getattr(cfg, "enabled", False)),
"api_key_set": bool(getattr(cfg, "api_key", "")),
"base_url": getattr(cfg, "base_url", ""),
}
except Exception as e: # noqa: BLE001
health["checks"]["cunkebao"] = {"ok": False, "error": str(e)}
# 4) AI 网关协议
ai = runtime_status()
health["checks"]["ai_gateway"] = {
"ok": ai["available"],
"state": ai["state"],
"runtime": ai,
"brain": brain_runtime_status(),
"protocols": {
"openai_compatible": "/api/v3/gateway/v1/chat/completions",
"mcp_tools": "/api/v3/gateway/mcp/tools",
"mcp_call": "/api/v3/gateway/mcp/call",
"agent_execute": "/api/v3/agent/execute",
},
}
return _ok(health, readback={"verified": bool(health["ok"]), "write_performed": False, "source": "integration_health"})
@router.get("/integration/realtime/status", tags=["对外接口统一清单"])
async def integration_realtime_status(
request: Request,
consumer: str = "",
event_limit: int = 20,
) -> Dict[str, Any]:
"""
第三方对接实时状态入口。
面向存客宝/纯客宝/超管/AI 数字员工:一次返回在线设备、最近 WS/Hook/Agent 事件、
实时 WebSocket 订阅地址和推荐拉取接口。
"""
if consumer and consumer not in CONSUMER_LABELS:
raise HTTPException(
status_code=404,
detail=f"未知消费方: {consumer},可选: {list(CONSUMER_LABELS.keys())}",
)
try:
from services.device_fleet import list_merged_local_devices
ws_hub = _runtime_ws_hub(request)
from services.hook_module_service import hook_module_service
from services.wechat_device_map import normalize_wechat_device_fields
devices = await list_merged_local_devices()
normalized = []
for item in devices:
item = normalize_wechat_device_fields(item)
did = item.get("device_id", "")
normalized.append({
"device_id": did,
"device_id_md5": item.get("device_id_md5"),
"project_id": item.get("project_id"),
"model": item.get("model"),
"status": "online" if ws_hub.is_online(did) else item.get("status", "offline"),
"online": ws_hub.is_online(did),
"capabilities": item.get("capabilities") or [],
"frida": item.get("frida") or {},
"ai_brain": item.get("ai_brain") or {},
"wechat_version": (item.get("device_profile") or {}).get("wechat_version"),
"wxid": item.get("wxid"),
"wechat_id": item.get("wechat_id"),
"friend_count": item.get("friend_count"),
"last_active": item.get("last_active"),
"last_heartbeat": item.get("last_heartbeat"),
})
recent_events = await hook_module_service.list_events(
limit=max(1, min(int(event_limit or 20), 100)),
)
except Exception as exc: # noqa: BLE001
raise HTTPException(status_code=500, detail=str(exc)) from exc
base_url = str(request.base_url).rstrip("/")
return _ok({
"consumer": consumer or "common",
"online_count": sum(1 for item in normalized if item.get("online")),
"total_count": len(normalized),
"devices": normalized,
"recent_events": recent_events,
"pull_endpoints": {
"status": "/api/v3/integration/realtime/status",
"events": "/api/v3/integration/realtime/events",
"fleet_devices": "/api/v3/fleet/devices",
"fleet_execute": "/api/v3/fleet/execute",
},
"stream": {
"protocol": "websocket",
"url": f"{base_url}/api/v3/hook/events/stream",
"event_source": "真实 Agent WS / Frida Hook / AI Agent 事件总线",
},
})
@router.get("/integration/realtime/events", tags=["对外接口统一清单"])
async def integration_realtime_events(
event_type: str = "",
device_id: str = "",
platform: str = "",
limit: int = 100,
) -> Dict[str, Any]:
"""第三方对接实时事件列表:读取真实 Agent/Hook 事件总线。"""
try:
from services.hook_module_service import hook_module_service
events = await hook_module_service.list_events(
event_type=event_type or None,
device_id=device_id or None,
platform=platform or None,
limit=max(1, min(int(limit or 100), 500)),
)
return _ok({"total": len(events), "events": events})
except Exception as exc: # noqa: BLE001
raise HTTPException(status_code=500, detail=str(exc)) from exc