ForcePilot/backend/package/yuxi/channel/gateway/protocol.py
Kris ecd3c90e80 feat(channel/gateway): 新增完整网关通道模块
新增设备身份管理、认证限流、并发通道、Webhook路由、RBAC权限控制、SSE/轮询降级等全套网关通道功能,包含:
1. 设备身份生成与签名验证
2. 设备令牌认证与速率限制
3. 内存+数据库双重设备注册表
4. 并发通道限流管理
5. Webhook安全处理与路由
6. RBAC权限校验系统
7. OpenAI API兼容适配层
8. Tailscale认证支持
9. HTTP轮询降级机制
2026-05-21 10:26:33 +08:00

208 lines
6.8 KiB
Python

import json as _json
import time as _time
import uuid as _uuid
from dataclasses import dataclass, field
from enum import StrEnum
class FrameType(StrEnum):
REQUEST = "request"
RESPONSE = "response"
EVENT = "event"
class GatewayErrorCode(StrEnum):
INVALID_REQUEST = "INVALID_REQUEST"
METHOD_NOT_FOUND = "METHOD_NOT_FOUND"
INVALID_PARAMS = "INVALID_PARAMS"
INTERNAL_ERROR = "INTERNAL_ERROR"
AUTH_ERROR = "AUTH_ERROR"
PERMISSION_DENIED = "PERMISSION_DENIED"
RATE_LIMITED = "RATE_LIMITED"
TIMEOUT = "TIMEOUT"
NOT_CONNECTED = "NOT_CONNECTED"
UNAVAILABLE = "UNAVAILABLE"
DEVICE_IDENTITY_REQUIRED = "DEVICE_IDENTITY_REQUIRED"
class GatewayRpcMethod(StrEnum):
CONNECT = "connect"
CHAT_SEND = "chat.send"
CHAT_STREAM = "chat.stream"
CHANNELS_LIST = "channels.list"
CHANNELS_STATUS = "channels.status"
CHANNELS_CONFIGURE = "channels.configure"
SESSIONS_LIST = "sessions.list"
SESSIONS_HISTORY = "sessions.history"
PLUGINS_LIST = "plugins.list"
PLUGINS_INSTALL = "plugins.install"
SYSTEM_HEALTH = "system.health"
SYSTEM_CONFIG = "system.config"
SYSTEM_VERSION = "system.version"
CHANNELS_START = "channels.start"
CHANNELS_STOP = "channels.stop"
CHANNELS_RESTART = "channels.restart"
PAIRING_LIST = "pairing.list"
PAIRING_APPROVE = "pairing.approve"
PAIRING_REJECT = "pairing.reject"
ALLOWLIST_GET = "allowlist.get"
ALLOWLIST_ADD = "allowlist.add"
ALLOWLIST_REMOVE = "allowlist.remove"
CHAT_HISTORY = "chat.history"
CHAT_CANCEL = "chat.cancel"
CRON_LIST = "cron.list"
CRON_CREATE = "cron.create"
CRON_UPDATE = "cron.update"
CRON_DELETE = "cron.delete"
CRON_FORCE_RUN = "cron.force_run"
CRON_PAUSE = "cron.pause"
CRON_RESUME = "cron.resume"
CRON_DIAGNOSTICS = "cron.diagnostics"
CRON_RUN_LOG = "cron.run_log"
AGENT_TOOLS_LIST = "agentTools.list"
AGENT_TOOLS_EXECUTE = "agentTools.execute"
SYSTEM_LOG_TAIL = "system.log.tail"
SESSIONS_CREATE = "sessions.create"
SESSIONS_DELETE = "sessions.delete"
CONFIG_GET = "config.get"
CONFIG_SET = "config.set"
MESSAGE_SEND = "message.send"
MESSAGE_ACTION = "message.action"
CHANNELS_PROBE = "channels.probe"
IDENTITY_LINKS_LIST = "identity_links.list"
IDENTITY_LINKS_ADD = "identity_links.add"
IDENTITY_LINKS_REMOVE = "identity_links.remove"
class DeliveryMode(StrEnum):
DIRECT = "direct"
GATEWAY = "gateway"
HYBRID = "hybrid"
@dataclass
class RpcFrame:
frame_type: FrameType
@dataclass
class RpcRequest(RpcFrame):
frame_type: FrameType = FrameType.REQUEST
id: str = field(default_factory=lambda: _uuid.uuid4().hex[:12])
method: str = ""
params: dict | None = None
session_id: str | None = None
caller_user_id: str | None = None
@dataclass
class RpcResponse(RpcFrame):
frame_type: FrameType = FrameType.RESPONSE
id: str = ""
ok: bool = True
result: dict | None = None
error_code: GatewayErrorCode | None = None
error_message: str | None = None
error_data: dict | None = None
retryable: bool = False
retry_after_ms: int | None = None
@dataclass
class HelloOk(RpcResponse):
frame_type: FrameType = FrameType.RESPONSE
id: str = ""
ok: bool = True
result: dict | None = field(default_factory=lambda: {
"protocolVersion": 1,
"server": {"version": "1.0.0", "connId": ""},
"features": {"methods": [], "events": []},
"auth": {"role": "", "scopes": []},
"policy": {
"maxPayload": 25 * 1024 * 1024,
"maxBufferedBytes": 256 * 1024,
"tickIntervalMs": 30_000,
},
})
@dataclass
class RpcEvent(RpcFrame):
frame_type: FrameType = FrameType.EVENT
event: str = ""
data: dict | None = None
timestamp: float = field(default_factory=_time.time)
seq: int | None = None
state_version: int | None = None
def marshal_frame(frame: RpcFrame) -> str:
if isinstance(frame, RpcRequest):
payload = {"type": frame.frame_type.value, "id": frame.id, "method": frame.method}
if frame.params is not None:
payload["params"] = frame.params
if frame.session_id is not None:
payload["sessionId"] = frame.session_id
elif isinstance(frame, RpcResponse):
payload = {"type": frame.frame_type.value, "id": frame.id, "ok": frame.ok}
if frame.result is not None:
payload["result"] = frame.result
if frame.error_code is not None:
error_payload = {
"code": frame.error_code.value,
"message": frame.error_message or "",
}
if frame.error_data is not None:
error_payload["data"] = frame.error_data
if frame.retryable:
error_payload["retryable"] = True
if frame.retry_after_ms is not None:
error_payload["retryAfterMs"] = frame.retry_after_ms
payload["error"] = error_payload
elif isinstance(frame, RpcEvent):
payload = {"type": frame.frame_type.value, "event": frame.event, "timestamp": frame.timestamp}
if frame.data is not None:
payload["data"] = frame.data
if frame.seq is not None:
payload["seq"] = frame.seq
if frame.state_version is not None:
payload["stateVersion"] = frame.state_version
else:
raise TypeError(f"Unknown frame type: {type(frame)}")
return _json.dumps(payload, ensure_ascii=False, default=str)
def unmarshal_frame(data: str | bytes) -> RpcFrame:
if isinstance(data, bytes):
data = data.decode("utf-8")
raw = _json.loads(data)
frame_type = raw.get("type", FrameType.REQUEST.value)
if frame_type == FrameType.REQUEST.value:
return RpcRequest(
id=raw.get("id", ""),
method=raw.get("method", ""),
params=raw.get("params"),
session_id=raw.get("sessionId"),
)
elif frame_type == FrameType.RESPONSE.value:
error = raw.get("error")
return RpcResponse(
id=raw.get("id", ""),
ok=raw.get("ok", True),
result=raw.get("result"),
error_code=GatewayErrorCode(error["code"]) if error else None,
error_message=error.get("message") if error else None,
error_data=error.get("data") if error else None,
retryable=error.get("retryable", False) if error else False,
retry_after_ms=error.get("retryAfterMs") if error else None,
)
elif frame_type == FrameType.EVENT.value:
return RpcEvent(
event=raw.get("event", ""),
data=raw.get("data"),
timestamp=raw.get("timestamp", _time.time()),
seq=raw.get("seq"),
state_version=raw.get("stateVersion"),
)
raise ValueError(f"Unknown frame type: {frame_type}")