ForcePilot/backend/package/yuxi/channel/gateway/channel_plugin.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

193 lines
6.7 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.

import logging
from yuxi.channel.runtime.backoff import BackoffConfig, ErrorBackoff
from yuxi.channel.extensions.base import BaseChannelPlugin
from yuxi.channel.gateway.client import GatewayClient
logger = logging.getLogger(__name__)
DEFAULT_RECONNECT_LEVELS = [1.0, 2.0, 4.0, 8.0, 15.0, 30.0]
class GatewayChannelPlugin(BaseChannelPlugin):
"""使用 Gateway Client SDK 的渠道插件基类。
封装常用连接模式:
- 自动重连(基于 ErrorBackoff 的状态管理)
- 请求重试(基于 ErrorBackoff.execute
- 设备身份管理(自动生成/注册/刷新)
- 生命周期回调on_connect / on_disconnect
用法::
class MyChannelPlugin(GatewayChannelPlugin):
id = "my_channel"
name = "MyChannel"
async def on_connect(self, client: GatewayClient) -> None:
await client.request("channel.start")
async def on_event(self, client: GatewayClient, event) -> None:
...
"""
id: str = ""
name: str = ""
def __init__(
self,
*,
gateway_url: str = "ws://127.0.0.1:9001",
token: str | None = None,
device_token: str | None = None,
request_timeout: float = 30.0,
reconnect_base_ms: float = 1000.0,
reconnect_max_ms: float = 30000.0,
reconnect_factor: float = 2.0,
reconnect_jitter: float = 0.1,
):
self._gateway_url = gateway_url
self._token = token
self._device_token = device_token
self._request_timeout = request_timeout
self._reconnect_base_ms = reconnect_base_ms
self._reconnect_max_ms = reconnect_max_ms
self._reconnect_factor = reconnect_factor
self._reconnect_jitter = reconnect_jitter
self._client: GatewayClient | None = None
self._backoff = ErrorBackoff(
config=BackoffConfig(
base_delay=reconnect_base_ms / 1000.0,
max_delay=reconnect_max_ms / 1000.0,
exponent=reconnect_factor,
jitter=True,
jitter_factor=reconnect_jitter,
max_retries=0,
),
)
@property
def client(self) -> GatewayClient | None:
return self._client
@property
def connected(self) -> bool:
return self._client is not None and self._client.connected
async def start(self, ctx) -> object:
self._client = GatewayClient(
url=self._gateway_url,
token=self._token,
device_token=self._device_token,
request_timeout=self._request_timeout,
reconnect_base_ms=self._reconnect_base_ms,
reconnect_max_ms=self._reconnect_max_ms,
reconnect_factor=self._reconnect_factor,
reconnect_jitter=self._reconnect_jitter,
on_connect=self._on_gw_connect,
on_disconnect=self._on_gw_disconnect,
on_event=self._on_gw_event,
on_error=self._on_gw_error,
on_close=self._on_gw_close,
)
try:
await self._client.start()
except ValueError as e:
logger.error("[%s] Gateway client start failed: %s", self.id, e)
raise
logger.info("[%s] Gateway channel plugin started", self.id)
return self._client
async def stop(self, ctx) -> None:
if self._client:
await self._client.stop()
self._client = None
self._backoff.reset(f"ch-{self.id}")
logger.info("[%s] Gateway channel plugin stopped", self.id)
async def send_request(
self,
method: str,
params: dict | None = None,
*,
timeout: float | None = None,
max_attempts: int = 3,
) -> dict:
"""发送 RPC 请求,带自动重试。"""
if self._client is None:
raise RuntimeError(f"[{self.id}] gateway client not started")
return await self._client.request_with_retry(
method,
params=params,
timeout=timeout,
max_attempts=max_attempts,
)
async def init_device_identity(self) -> str:
"""初始化设备身份,返回 device_token。"""
if self._client is None:
self._client = GatewayClient(url=self._gateway_url, token=self._token)
return await self._client.init_device_identity()
async def refresh_device_token(self) -> str | None:
"""刷新设备令牌。"""
if self._client is None:
return None
return await self._client.refresh_device_token()
def create_backoff(self, name: str, levels: list[float] | None = None) -> ErrorBackoff:
"""创建 ErrorBackoff 实例,供子类管理独立退避状态。"""
return ErrorBackoff(
config=BackoffConfig(
base_delay=levels[0] if levels else 1.0,
max_delay=levels[-1] if levels else 30.0,
exponent=2.0,
jitter=True,
jitter_factor=0.1,
max_retries=0,
),
)
async def on_connect(self, client: GatewayClient) -> None:
"""子类可重写Gateway 连接成功时回调。"""
async def on_disconnect(self, client: GatewayClient, code: int, reason: str) -> None:
"""子类可重写Gateway 断开连接时回调。"""
async def on_event(self, client: GatewayClient, event) -> None:
"""子类可重写:收到 Gateway 事件时回调。"""
async def on_error(self, client: GatewayClient, error: Exception) -> None:
"""子类可重写Gateway 错误时回调。"""
async def _on_gw_connect(self, client: GatewayClient) -> None:
try:
await self.on_connect(client)
except Exception:
logger.exception("[%s] on_connect callback failed", self.id)
async def _on_gw_disconnect(self, client: GatewayClient, code: int, reason: str) -> None:
try:
await self.on_disconnect(client, code, reason)
except Exception:
logger.exception("[%s] on_disconnect callback failed", self.id)
async def _on_gw_event(self, client: GatewayClient, event) -> None:
try:
await self.on_event(client, event)
except Exception:
logger.exception("[%s] on_event callback failed", self.id)
async def _on_gw_error(self, client: GatewayClient, error: Exception) -> None:
try:
await self.on_error(client, error)
except Exception:
logger.exception("[%s] on_error callback failed", self.id)
async def _on_gw_close(self, client: GatewayClient, code: int, reason: str) -> None:
logger.info("[%s] Gateway connection closed: code=%d reason=%s", self.id, code, reason)