295 lines
11 KiB
Python
295 lines
11 KiB
Python
"""
|
||
对外接口统一清单 · 集成中心路由
|
||
|
||
路由前缀:/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
|
||
|
||
from fastapi import APIRouter, HTTPException, Request
|
||
|
||
from services.integration_manifest import (
|
||
build_manifest,
|
||
build_capability,
|
||
filter_by_consumer,
|
||
list_modules,
|
||
CONSUMER_LABELS,
|
||
)
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
@router.get("/integration/manifest", tags=["对外接口统一清单"])
|
||
async def integration_manifest(request: Request) -> Dict[str, Any]:
|
||
"""全量对外接口清单(按模块 + 消费方归类,机器可读)。"""
|
||
manifest = build_manifest(request.app)
|
||
return {"code": 200, "data": manifest}
|
||
|
||
|
||
@router.get("/integration/modules", tags=["对外接口统一清单"])
|
||
async def integration_modules(request: Request) -> Dict[str, Any]:
|
||
"""模块目录(精简:模块/标签/消费方/端点数)。"""
|
||
manifest = build_manifest(request.app)
|
||
return {"code": 200, "data": {
|
||
"module_count": manifest["module_count"],
|
||
"total_endpoints": manifest["total_endpoints"],
|
||
"modules": list_modules(manifest),
|
||
}}
|
||
|
||
|
||
@router.get("/integration/consumers", tags=["对外接口统一清单"])
|
||
async def integration_consumers() -> Dict[str, Any]:
|
||
"""消费方列表(存客宝/超管/AI数字员工/通用)。"""
|
||
return {"code": 200, "data": {
|
||
"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 {"code": 200, "data": 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:
|
||
from services.ws_hub import ws_hub
|
||
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
|
||
# 退化补充:用 ws_hub 上报的 frida_available
|
||
if not supports_hook:
|
||
try:
|
||
from services.ws_hub import ws_hub
|
||
info = ws_hub.get_device_info(device_id) or {}
|
||
supports_hook = bool(info.get("frida_available"))
|
||
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 {"code": 200, "data": cap}
|
||
|
||
|
||
@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:
|
||
from services.ws_hub import ws_hub
|
||
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 网关协议
|
||
health["checks"]["ai_gateway"] = {
|
||
"ok": True,
|
||
"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 {"code": 200, "data": 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
|
||
from services.ws_hub import ws_hub
|
||
from services.hook_module_service import hook_module_service
|
||
|
||
devices = await list_merged_local_devices()
|
||
normalized = []
|
||
for item in devices:
|
||
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"),
|
||
"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 {
|
||
"code": 200,
|
||
"data": {
|
||
"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 {"code": 200, "data": {"total": len(events), "events": events}}
|
||
except Exception as exc: # noqa: BLE001
|
||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|