from yuxi.channel.capabilities import ChannelCapabilities class BaseChannelPlugin: """可选的渠道插件基类,提供默认实现。渠道不强制继承,满足 Protocol 即可。""" id: str = "" name: str = "" order: int = 99 label: str | None = None aliases: list[str] | None = None resolve_reply_to_mode: str | None = None @property def capabilities(self) -> ChannelCapabilities: return ChannelCapabilities() async def resolve_account(self, account_id: str) -> dict: return {} async def list_accounts(self) -> list[dict]: return [] def list_account_ids(self, config: dict) -> list[str]: return ["default"] def is_configured(self, account: dict) -> bool: return bool(account) def is_enabled(self, account: dict) -> bool: return True def disabled_reason(self, account: dict) -> str: return "" async def resolve_allow_from(self, config: dict, account_id: str) -> list[str] | None: return None def describe_account(self, account: dict) -> dict: return {"account_id": account.get("account_id", "")} async def start(self, ctx) -> object: pass async def stop(self, ctx) -> None: pass async def send_text( self, target_id: str, content: str, *, reply_to_id: str | None = None, thread_id: str | None = None, account_id: str | None = None, ) -> None: pass async def send_media( self, target_id: str, media_url: str, media_type: str, reply_to_id: str | None = None, thread_id: str | None = None, ) -> None: pass async def probe(self, account: dict) -> bool: return True def build_summary(self, snapshot: object) -> dict: return {} async def resolve_thread_id(self, account_id: str, reply_to_id: str | None) -> str | None: return None async def send_typing(self, target_id: str, thread_id: str | None = None) -> None: pass async def clear_typing(self, target_id: str, thread_id: str | None = None) -> None: pass async def check_ready(self, account_id: str) -> bool: return True # ── HeartbeatProtocol ──────────────────────────────── @property def ping_interval_ms(self) -> int: return 30000 async def on_ping(self, account: dict) -> bool: return True async def start_typing(self, target_id: str, account: dict) -> object: return None async def stop_typing(self, handle: object) -> None: pass async def typing_indicator( self, target_id: str, account: dict, _stop_event, ) -> None: pass # ── AgentPromptProtocol ────────────────────────────── def build_system_prompt(self, context) -> str | None: return None def build_context_note(self, context) -> str: return "" @property def channel_format_instructions(self) -> str | None: return None # ── ThreadingProtocol ──────────────────────────────── def extract_thread_id(self, msg: object) -> str | None: return None def resolve_reply_transport(self, msg: object, thread_id: str | None): from yuxi.channel.protocols import ReplyTransport return ReplyTransport() def get_thread_info(self, thread_id: str): return None # ── MessagingProtocol ──────────────────────────────── def resolve_session(self, msg: object): from yuxi.channel.message.models import PeerKind from yuxi.channel.protocols import SessionResolution if hasattr(msg, "sender") and hasattr(msg.sender, "kind"): if msg.sender.kind == PeerKind.DIRECT: return SessionResolution(kind="direct", conversation_id=msg.sender.id) gid = msg.group.id if hasattr(msg, "group") and msg.group and msg.group.id else "unknown" return SessionResolution(kind="group", conversation_id=gid) def parse_explicit_target(self, content: str) -> str | None: return None def build_reply_payload( self, target_id: str, content: str, *, reply_to_id: str | None = None, thread_id: str | None = None, session_key: str | None = None, agent_config_id: str | None = None, **kwargs, ): from yuxi.channel.protocols import BoundReplyPayload return BoundReplyPayload(target_id=target_id, content=content, reply_to_id=reply_to_id, thread_id=thread_id) def build_attachment_refs(self, attachments: list[dict]) -> str: lines = [] for att in attachments: ft = att.get("file_type", "file") fp = att.get("file_path", "") label = {"image": "图片", "video": "视频"}.get(ft, "文件") lines.append(f"[{label}: {fp}]") return "\n".join(lines) def resolve_focused_binding(self, msg: object, session, current_binding: object) -> dict | None: return None @property def supported_explicit_targets(self) -> list[str]: return [] # ── AgentToolProtocol ──────────────────────────────── def get_agent_tools(self) -> list: return [] async def execute_agent_tool(self, tool_name: str, params: dict, context: dict) -> dict: return {"success": False, "error": f"Unknown tool: {tool_name}"} # ── MessageActionProtocol ──────────────────────────── @property def actions(self): """消息动作适配器。子类重写以注册支持的动作。 用法:: class MyPlugin(BaseChannelPlugin): @property def actions(self): if self._actions is None: self._actions = MessageActionRegistry() self._actions.register( MessageAction.SEND, self._handle_send, description="发送文本消息", ) return self._actions """ return None def get_message_actions(self) -> list: if self.actions: return self.actions.get_message_actions() return [] async def execute_message_action(self, action: str, params: dict, context: dict) -> dict: if self.actions: result = await self.actions.execute_action(action, params, context) return result return {"success": False, "error": f"Unknown action: {action}"} # ── SecurityProtocol ──────────────────────────────── async def check_allowlist(self, peer_id: str, channel_type: str) -> bool: return True def resolve_dm_policy(self) -> dict: return {"mode": "open", "allow_from": []} # ── PairingProtocol ───────────────────────────────── async def generate_code(self, peer_id: str) -> str: return "" async def verify_code(self, peer_id: str, code: str) -> bool: return True # ── GroupsProtocol ────────────────────────────────── async def list_groups(self) -> list[dict]: return [] # ── MentionsProtocol ──────────────────────────────── def extract_mentions(self, raw_message: dict) -> list[str]: return [] # ── LifecycleProtocol ─────────────────────────────── async def on_config_changed(self, prev_cfg: dict, next_cfg: dict, account_id: str) -> None: pass async def on_account_removed(self, account_id: str) -> None: pass async def on_retire(self) -> None: pass async def run_startup_maintenance(self, cfg: dict) -> None: pass # ── ApprovalProtocol ──────────────────────────────── async def check_approval_required(self, config: dict, action, initiator_peer_id: str) -> bool: approval_cfg = config.get("approval", {}) if not approval_cfg.get("enabled", False): return False approvers = approval_cfg.get("approvers", []) if not approvers or initiator_peer_id in approvers: return False return True async def create_approval_request( self, config: dict, action, initiator_peer_id: str, description: str, context: dict ): from yuxi.channel.protocols import ApprovalRequest return ApprovalRequest( id="", channel_type="", account_id="", initiator_peer_id=initiator_peer_id, action=action, description=description, ) async def send_approval_notification(self, config: dict, request, approver_peer_ids: list[str]) -> bool: return False async def check_approval_status(self, config: dict, request_id: str): return None def get_approver_ids(self, config: dict) -> list[str]: return config.get("approval", {}).get("approvers", []) # ── ConfigSchemaProtocol ──────────────────────────── def config_schema(self) -> dict: return {} # ── SetupProtocol ─────────────────────────────────── async def setup(self, config: dict) -> bool: return True # ── SetupWizardProtocol ───────────────────────────── def setup_wizard_steps(self) -> list: return [] async def validate_wizard_input(self, step_key: str, value: str) -> str | None: return None # ── AuthProtocol ──────────────────────────────────── async def login(self, config: dict) -> dict: return {} async def logout(self, config: dict) -> None: pass async def refresh_token(self, config: dict) -> dict | None: return None async def qr_auth_start(self, config: dict) -> str: return "" async def qr_auth_wait(self, config: dict) -> dict | None: return None # ── ElevatedProtocol ──────────────────────────────── async def elevate(self, account: dict, reason: str) -> bool: return True async def revoke(self, account: dict) -> None: pass @property def is_elevated(self) -> bool: return False # ── AllowlistManagementProtocol ───────────────────── async def list_allow_entries(self, config: dict, scope: str) -> list[str]: return [] async def add_allow_entry(self, config: dict, scope: str, entry: str) -> bool: return False async def remove_allow_entry(self, config: dict, scope: str, entry: str) -> bool: return False # ── ReactionProtocol ───────────────────────────────── async def send_reaction( self, target_id: str, message_id: str, emoji: str, *, account_id: str | None = None, ) -> None: pass async def remove_reaction( self, target_id: str, message_id: str, emoji: str, *, account_id: str | None = None, ) -> None: pass async def fetch_reactions( self, target_id: str, message_id: str, *, account_id: str | None = None, ) -> list[dict]: return [] # ── WebhookProtocol ────────────────────────────────── async def handle_webhook( self, request: object, account_id: str | None = None, ) -> object: return None def verify_signature(self, body: bytes, signature: str, secret: str) -> bool: return False def resolve_webhook_auth_bypass_paths(self) -> list[str]: return [] # ── InboundHandlerProtocol ─────────────────────────── async def handle_raw_event( self, event: dict, account: dict, ) -> object | None: return None def parse_to_unified( self, raw_event: dict, account_id: str, ) -> object | None: return None def resolve_event_type(self, raw_event: dict) -> str: return "unknown" # ── MediaProtocol ──────────────────────────────────── async def upload_media( self, file_path: str, media_type: str, *, account_id: str | None = None, ) -> dict: return {} async def download_media( self, media_url: str, *, account_id: str | None = None, ) -> bytes: return b"" def detect_media_type(self, filename: str, content_type: str = "") -> str: return "file" def validate_media_url(self, url: str) -> bool: return False def resolve_media_size_limit(self, media_type: str) -> int: return 0 # ── FormatProtocol ─────────────────────────────────── def markdown_to_native(self, md_text: str) -> dict | str: return md_text def native_to_markdown(self, native_content: dict | str) -> str: if isinstance(native_content, dict): return native_content.get("text", "") return str(native_content) # ── DedupeProtocol ─────────────────────────────────── def is_duplicate(self, key: str) -> bool: return False def mark_seen(self, key: str) -> None: pass def reset(self) -> None: pass @property def ttl_seconds(self) -> int: return 300 @property def max_entries(self) -> int: return 10000 # ── CardProtocol ───────────────────────────────────── async def send_card( self, target_id: str, card_content: dict, *, reply_to_id: str | None = None, account_id: str | None = None, ) -> str | None: return None async def update_card( self, message_id: str, card_content: dict, *, account_id: str | None = None, ) -> bool: return False async def edit_card( self, target_id: str, message_id: str, card_content: dict, *, thread_id: str | None = None, account_id: str | None = None, ) -> str | None: ok = await self.update_card(message_id, card_content, account_id=account_id) return message_id if ok else None def build_text_card(self, text: str, title: str | None = None, **kwargs) -> dict: return {"text": text} def supports_card_interactions(self) -> bool: return False # ── ErrorHandlingProtocol ──────────────────────────── def classify_error(self, error: BaseException) -> object: from yuxi.channel.errors import classify_error return classify_error(error) def is_retryable(self, error: BaseException) -> bool: from yuxi.channel.errors import is_retryable return is_retryable(error) def should_backoff(self, error: BaseException, attempt: int) -> int: from yuxi.channel.errors import should_backoff return should_backoff(error, attempt) # ── PollingProtocol ────────────────────────────────── poll_interval_seconds: int = 60 async def poll_once(self, account: dict) -> list[dict]: return [] async def should_poll(self, account: dict) -> bool: return False def get_poll_cursor(self, account_id: str) -> str | None: return None def set_poll_cursor(self, account_id: str, cursor: str) -> None: pass # ── PinProtocol ────────────────────────────────────── async def pin_message( self, message_id: str, *, account_id: str | None = None, ) -> bool: return False async def unpin_message( self, message_id: str, *, account_id: str | None = None, ) -> bool: return False async def list_pins( self, chat_id: str, *, account_id: str | None = None, ) -> list[dict]: return [] # ── WebsocketProtocol ──────────────────────────────── async def connect_ws(self, account: dict) -> object: return None async def disconnect_ws(self) -> None: pass async def on_ws_message(self, message: str | bytes) -> None: pass @property def ws_reconnect_interval_ms(self) -> int: return 5000 @property def ws_heartbeat_interval_ms(self) -> int: return 30000 # ── Hooks ─────────────────────────────────────────── async def on_message_sending(self, msg: object, content: str) -> str | None: return content async def on_message_received(self, msg: object) -> object | None: return msg async def before_send_attempt(self, target_id: str, content: str) -> None: pass async def after_send_success(self, target_id: str, receipt: object) -> None: pass async def after_send_failure(self, target_id: str, error: str) -> None: pass async def after_commit(self, receipt: object) -> None: pass