新增设备身份管理、认证限流、并发通道、Webhook路由、RBAC权限控制、SSE/轮询降级等全套网关通道功能,包含: 1. 设备身份生成与签名验证 2. 设备令牌认证与速率限制 3. 内存+数据库双重设备注册表 4. 并发通道限流管理 5. Webhook安全处理与路由 6. RBAC权限校验系统 7. OpenAI API兼容适配层 8. Tailscale认证支持 9. HTTP轮询降级机制
193 lines
6.7 KiB
Python
193 lines
6.7 KiB
Python
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)
|