from typing import Any from yuxi.channel.exceptions import ( ChannelErrorClassification, ChannelPermanentError, ChannelRateLimitedError, ChannelRetryableError, ChannelValidationError, ) from yuxi.channel.plugins.protocol import ChannelMeta, ChannelPlugin, InboundMedia, InboundMessage, OutboundMessage from yuxi.channel.plugins.registry import get_registry from yuxi.channel.ports import ( AuthMixin, BindingMixin, ConfigMixin, InboundMixin, LifecycleMixin, MetaMixin, OutboundMixin, SecurityMixin, SessionMixin, StatusMixin, ThreadingMixin, TransportMixin, ) _META_FIELDS = { "aliases", "capabilities", "delivery_mode", "transport_type", "ui_hints", "sort_weight", "icon", "description", } class _BaseChannelPlugin( MetaMixin, ConfigMixin, InboundMixin, OutboundMixin, TransportMixin, SecurityMixin, StatusMixin, LifecycleMixin, SessionMixin, AuthMixin, BindingMixin, ThreadingMixin, ): """提供 ChannelPlugin Protocol 的默认实现,允许通过适配器覆盖指定方法。""" def __init__(self, meta: ChannelMeta, adapters: dict[str, Any]) -> None: MetaMixin.__init__(self, meta) for name, impl in adapters.items(): if callable(impl): setattr(self, name, impl) # === 动作交互(可选扩展点) === def describe_actions(self, config: dict, inbound: InboundMessage) -> list[dict]: """描述当前消息可触发的动作;默认无动作。""" return [] def handle_action(self, action: dict, config: dict, inbound: InboundMessage) -> dict | None: """执行动作;默认返回 None 表示未处理。""" return None # === 扫码配对(可选扩展点) === def supports_scan_pairing(self, config: dict | None = None) -> bool: return bool(config.get("pairing", {}).get("mode") in ("qr", "both")) if config else False async def normalize_scan_event( self, request: Any, config: dict, account_id: str, ) -> InboundMessage | None: return None async def build_pairing_qr_reply( self, pairing_code: str, qr_content: str, config: dict, account_id: str, ) -> OutboundMessage: return OutboundMessage( content=f"请扫描下方二维码完成绑定:{pairing_code}", content_type="text", ) # === 媒体附件 === async def download_attachment(self, inbound: InboundMessage, media: InboundMedia) -> tuple[bytes, str] | None: return None # === Webhook 管理(可选扩展点) === async def setup_webhook(self, config: dict, callback_url: str) -> bool: """向渠道平台注册 Webhook;默认返回 False,表示未实现。""" return False async def delete_webhook(self, config: dict) -> bool: """向渠道平台注销 Webhook;默认返回 False,表示未实现。""" return False # === 消息编辑与删除(可选扩展点) === async def edit_message( self, channel_session_id: str, channel_message_id: str, new_payload: dict, config: dict | None = None, ) -> bool: """编辑已发送消息;默认返回 False,表示未实现。""" return False async def delete_message( self, channel_session_id: str, channel_message_id: str, config: dict | None = None, ) -> bool: """删除已发送消息;默认返回 False,表示未实现。""" return False # === 用户资料解析(可选扩展点) === async def resolve_user_profile(self, sender_id: str, config: dict | None = None) -> dict | None: """解析发送者资料;默认返回 None,表示未实现。""" return None # === 入站/出站转换钩子(可选扩展点) === async def transform_inbound(self, raw_event: dict, config: dict | None = None) -> dict: """在 normalize_inbound 之前转换原始事件;默认原样返回。""" return raw_event # === 投递异常分类(可选扩展点) === def classify_error( self, exc: Exception, payload: dict | None = None, ) -> tuple[ChannelErrorClassification, int | None]: """对投递异常进行分类;默认按异常基类推断。""" if isinstance(exc, ChannelValidationError): return ChannelErrorClassification.PERMANENT, None if isinstance(exc, ChannelPermanentError): return ChannelErrorClassification.PERMANENT, None if isinstance(exc, ChannelRateLimitedError): return ChannelErrorClassification.RATE_LIMITED, exc.retry_after if isinstance(exc, ChannelRetryableError): return ChannelErrorClassification.RETRYABLE, None # 未知异常保守视为可重试 return ChannelErrorClassification.RETRYABLE, None def create_channel_plugin_base( channel_type: str, display_name: str, config_schema: dict, **adapters: Any, ) -> ChannelPlugin: meta_kwargs = {k: adapters.pop(k) for k in list(adapters.keys()) if k in _META_FIELDS} meta = ChannelMeta( channel_type=channel_type, display_name=display_name, config_schema=config_schema, **meta_kwargs, ) return _BaseChannelPlugin(meta=meta, adapters=adapters) def define_channel_plugin_entry(plugin: ChannelPlugin, register_mode: str = "full") -> None: registry = get_registry() if register_mode == "discovery": registry.register_primary(plugin) return registry.register(plugin)