242 lines
8.4 KiB
Python
242 lines
8.4 KiB
Python
"""
|
||
连接协议与状态路由
|
||
用于展示设备端(Agent/Hook)与服务端的连接方式、消息协议和实时状态。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime, timezone
|
||
from typing import Any, Dict, List
|
||
|
||
from fastapi import APIRouter, HTTPException
|
||
from pydantic import BaseModel, Field
|
||
|
||
from services.ws_hub import ws_hub
|
||
from services.adb_device import adb_manager
|
||
from services.hook_module_service import hook_module_service
|
||
from services.connection_priority import connection_priority
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
class SimRegisterRequest(BaseModel):
|
||
device_id: str
|
||
project_id: str = "cunkebao"
|
||
model: str = "Simulator Device"
|
||
platform: str = "android"
|
||
capabilities: List[str] = Field(default_factory=lambda: ["event", "device_request"])
|
||
|
||
|
||
class SimHeartbeatRequest(BaseModel):
|
||
device_id: str
|
||
|
||
|
||
class SimHookEventRequest(BaseModel):
|
||
device_id: str
|
||
platform: str = "wechat"
|
||
event_type: str = "message_received"
|
||
payload: Dict[str, Any] = Field(default_factory=dict)
|
||
|
||
|
||
def _iso_now() -> str:
|
||
return datetime.now(timezone.utc).isoformat()
|
||
|
||
|
||
def _heartbeat_age_seconds(last_heartbeat: str) -> int:
|
||
if not last_heartbeat:
|
||
return -1
|
||
try:
|
||
dt = datetime.fromisoformat(last_heartbeat.replace("Z", "+00:00"))
|
||
return max(0, int((datetime.now(dt.tzinfo or timezone.utc) - dt).total_seconds()))
|
||
except Exception:
|
||
return -1
|
||
|
||
|
||
@router.get("/connection/protocol")
|
||
async def get_connection_protocol() -> Dict[str, Any]:
|
||
"""连接方式协议:给前端/对接方直观看结构。"""
|
||
return {
|
||
"code": 200,
|
||
"data": {
|
||
"version": "v1",
|
||
"generated_at": _iso_now(),
|
||
"transport": {
|
||
"agent_ws": "ws://<host>:8899/ws/device/{device_id}",
|
||
"hook_events_ws": "ws://<host>:8899/api/v3/hook/events/stream",
|
||
"rest_base": "http://<host>:8899/api/v3",
|
||
},
|
||
"handshake": {
|
||
"client_register": {
|
||
"type": "register",
|
||
"data": {
|
||
"project_id": "cunkebao",
|
||
"agent_version": "3.0.0",
|
||
"model": "Redmi 11",
|
||
"capabilities": ["u2", "skill_wechat", "event", "device_request"],
|
||
},
|
||
},
|
||
"server_ack": {
|
||
"type": "registered",
|
||
"success": True,
|
||
"device_id": "<device_id>",
|
||
"project_id": "<project_id>",
|
||
},
|
||
},
|
||
"heartbeat": {
|
||
"client_ping_type": "heartbeat",
|
||
"server_pong_type": "pong",
|
||
"recommended_interval_seconds": [5, 10, 30],
|
||
},
|
||
"message_types": {
|
||
"device_to_server": [
|
||
"register",
|
||
"heartbeat",
|
||
"response",
|
||
"status_report",
|
||
"event",
|
||
"device_request",
|
||
],
|
||
"server_to_device": [
|
||
"registered",
|
||
"pong",
|
||
"execute",
|
||
"agent_execute",
|
||
"config_update",
|
||
"device_request_ack",
|
||
],
|
||
},
|
||
"hook_rest": {
|
||
"list_modules": "GET /api/v3/modules",
|
||
"device_modules": "GET /api/v3/devices/{device_id}/modules",
|
||
"deploy_script": "POST /api/v3/scripts/{script_id}/deploy",
|
||
"ingest_event": "POST /api/v3/hook/events",
|
||
},
|
||
},
|
||
}
|
||
|
||
|
||
@router.get("/connection/status")
|
||
async def get_connection_status() -> Dict[str, Any]:
|
||
"""实时连接状态:用于控制台展示。"""
|
||
online_devices: List[Dict[str, Any]] = ws_hub.get_online_devices()
|
||
adb_devices = adb_manager.scan_devices()
|
||
|
||
device_rows: List[Dict[str, Any]] = []
|
||
for item in online_devices:
|
||
device_id = item.get("device_id", "")
|
||
last_heartbeat = item.get("last_heartbeat", "")
|
||
row = {
|
||
"device_id": device_id,
|
||
"project_id": item.get("project_id", ""),
|
||
"model": item.get("model", ""),
|
||
"platform": item.get("platform", ""),
|
||
"status": item.get("status", "online"),
|
||
"last_heartbeat": last_heartbeat,
|
||
"heartbeat_age_seconds": _heartbeat_age_seconds(last_heartbeat),
|
||
"capabilities": item.get("capabilities", []),
|
||
"wechat_running": item.get("wechat_running"),
|
||
"wechat_foreground": item.get("wechat_foreground"),
|
||
"network_type": item.get("network_type"),
|
||
"screen_on": item.get("screen_on"),
|
||
"current_app": item.get("current_app"),
|
||
}
|
||
device_rows.append(row)
|
||
|
||
return {
|
||
"code": 200,
|
||
"data": {
|
||
"server_time": _iso_now(),
|
||
"ws_path": "/ws/device/{device_id}",
|
||
"hook_events_stream": "/api/v3/hook/events/stream",
|
||
"online_ws_count": len(ws_hub.connections),
|
||
"online_device_ids": list(ws_hub.connections.keys()),
|
||
"adb_count": len(adb_devices),
|
||
"adb_serials": adb_devices,
|
||
"devices": device_rows,
|
||
},
|
||
}
|
||
|
||
|
||
@router.post("/connection/simulate/register")
|
||
async def simulate_register(req: SimRegisterRequest) -> Dict[str, Any]:
|
||
"""协议联调:模拟设备 register(不建立真实 WS,仅写入状态面板)。"""
|
||
await ws_hub.handle_message(
|
||
req.device_id,
|
||
{
|
||
"type": "register",
|
||
"data": {
|
||
"device_id": req.device_id,
|
||
"project_id": req.project_id,
|
||
"model": req.model,
|
||
"platform": req.platform,
|
||
"status": "simulated",
|
||
"capabilities": req.capabilities,
|
||
"source": "connection_simulator",
|
||
},
|
||
},
|
||
)
|
||
return {"code": 200, "data": {"device_id": req.device_id, "simulated": True, "action": "register"}}
|
||
|
||
|
||
@router.post("/connection/simulate/heartbeat")
|
||
async def simulate_heartbeat(req: SimHeartbeatRequest) -> Dict[str, Any]:
|
||
"""协议联调:模拟心跳。"""
|
||
if not ws_hub.get_device_info(req.device_id):
|
||
raise HTTPException(status_code=404, detail="设备未注册,先模拟 register")
|
||
await ws_hub.handle_message(req.device_id, {"type": "heartbeat", "device_id": req.device_id})
|
||
return {"code": 200, "data": {"device_id": req.device_id, "simulated": True, "action": "heartbeat"}}
|
||
|
||
|
||
@router.post("/connection/simulate/hook-event")
|
||
async def simulate_hook_event(req: SimHookEventRequest) -> Dict[str, Any]:
|
||
"""协议联调:模拟 hook 事件写入并广播到事件流。"""
|
||
event = await hook_module_service.add_event(
|
||
{
|
||
"event_type": req.event_type,
|
||
"device_id": req.device_id,
|
||
"platform": req.platform,
|
||
"payload": req.payload,
|
||
"source": "connection_simulator",
|
||
}
|
||
)
|
||
return {"code": 200, "data": event}
|
||
|
||
|
||
# ========== 连接优先级 ==========
|
||
|
||
@router.get("/connection/modes/{device_id}")
|
||
async def get_device_connection_modes(device_id: str, refresh: bool = False) -> Dict[str, Any]:
|
||
"""获取设备所有控制模式的可用性与优先级。refresh=true 强制刷新缓存。"""
|
||
if refresh:
|
||
connection_priority.invalidate_cache()
|
||
modes = await connection_priority.async_evaluate(device_id)
|
||
best = next((m for m in modes if m.available), None)
|
||
return {
|
||
"code": 200,
|
||
"data": {
|
||
"device_id": device_id,
|
||
"best_mode": best.to_dict() if best else None,
|
||
"modes": [m.to_dict() for m in modes],
|
||
},
|
||
}
|
||
|
||
|
||
@router.get("/connection/modes")
|
||
async def get_all_connection_modes() -> Dict[str, Any]:
|
||
"""获取所有已知设备的控制模式概览。"""
|
||
online_ids = list(ws_hub.connections.keys())
|
||
adb_serials = await adb_manager.async_scan_devices()
|
||
all_ids = list(set(online_ids + adb_serials))
|
||
|
||
result = []
|
||
for did in all_ids:
|
||
modes = await connection_priority.async_evaluate(did)
|
||
best = next((m for m in modes if m.available), None)
|
||
result.append({
|
||
"device_id": did,
|
||
"best_mode": best.to_dict() if best else None,
|
||
"available_count": sum(1 for m in modes if m.available),
|
||
"modes": [m.to_dict() for m in modes],
|
||
})
|
||
return {"code": 200, "data": result}
|