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)