Files
workphone-sdk/sdk/app/routers/integration.py

295 lines
11 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
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