from __future__ import annotations import time from dataclasses import dataclass, field from enum import StrEnum from typing import TYPE_CHECKING, Any, Final, Protocol, runtime_checkable from yuxi.channel.context import ChannelContext if TYPE_CHECKING: from yuxi.channel.capabilities import ChannelCapabilities from yuxi.channel.doctor.models import ( ConnectivityCheckResult, CredentialCheckResult, DiagnosisResult, DiagnosisWarning, LegacyConfigRule, PermissionCheckResult, RepairResult, RepairStep, ) _SUPPORTED_SCHEMA_TYPES: Final[frozenset[str]] = frozenset( {"string", "number", "integer", "boolean", "object", "array", "null"} ) # ── Base ───────────────────────────────────────────────── @runtime_checkable class MetaProtocol(Protocol): id: str name: str order: int @runtime_checkable class CapabilitiesProtocol(Protocol): capabilities: ChannelCapabilities # ── Config ─────────────────────────────────────────────── @runtime_checkable class ConfigProtocol(Protocol): def list_account_ids(self, config: dict) -> list[str]: ... async def resolve_account(self, account_id: str) -> dict: ... def is_configured(self, account: dict) -> bool: ... def inspect_account(self, config: dict, account_id: str | None = None) -> dict: ... def default_account_id(self, config: dict) -> str: ... def set_account_enabled(self, config: dict, account_id: str, enabled: bool) -> dict: ... def delete_account(self, config: dict, account_id: str) -> dict: ... def is_enabled(self, account: dict, config: dict) -> bool: ... def disabled_reason(self, account: dict, config: dict) -> str: ... def unconfigured_reason(self, account: dict, config: dict) -> str: ... def describe_account(self, account: dict, config: dict) -> dict: ... def resolve_allow_from(self, config: dict, account_id: str | None = None) -> list[str | int] | None: ... def format_allow_from(self, config: dict, account_id: str | None, allow_from: list[str | int]) -> list[str]: ... def has_configured_state(self, config: dict) -> bool: ... def has_persisted_auth_state(self, config: dict) -> bool: ... def resolve_default_to(self, config: dict, account_id: str | None = None) -> str | None: ... # ── Gateway ────────────────────────────────────────────── @runtime_checkable class GatewayProtocol(Protocol): async def start(self, ctx: ChannelContext) -> Any: ... async def stop(self, ctx: ChannelContext) -> None: ... def resolve_gateway_auth_bypass_paths(self, config: dict) -> list[str]: ... async def login_with_qr_start( self, account_id: str | None = None, *, force: bool = False, timeout_ms: int | None = None ) -> dict: """返回 {"qr_data_url": str, "message": str, "connected": bool}""" ... async def login_with_qr_wait( self, account_id: str | None = None, *, timeout_ms: int | None = None, current_qr_data_url: str | None = None ) -> dict: """返回 {"connected": bool, "message": str, "qr_data_url": str | None}""" ... async def logout_account(self, ctx: ChannelContext) -> dict: """返回 {"cleared": bool, "logged_out": bool | None}""" ... # ── Outbound ───────────────────────────────────────────── class OutboundDeliveryMode(StrEnum): DIRECT = "direct" GATEWAY = "gateway" HYBRID = "hybrid" @dataclass class OutboundPresentationCapabilities: supported: bool = False buttons: bool = False selects: bool = False context: bool = False divider: bool = False @dataclass class OutboundDeliveryCapabilities: pin: bool = False durable_final_text: bool = False durable_final_media: bool = False durable_final_payload: bool = False durable_final_silent: bool = False durable_final_reply_to: bool = False durable_final_thread: bool = False durable_final_native_quote: bool = False durable_final_message_sending_hooks: bool = False durable_final_batch: bool = False durable_final_reconcile_unknown_send: bool = False durable_final_after_send_success: bool = False durable_final_after_commit: bool = False @runtime_checkable class OutboundProtocol(Protocol): delivery_mode: OutboundDeliveryMode def chunker(self, text: str, limit: int, ctx: Any | None = None) -> list[str]: ... chunker_mode: str | None text_chunk_limit: int | None 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: ... 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, account_id: str | None = None, ) -> None: ... async def send_payload(self, ctx: Any) -> Any: ... async def send_poll(self, ctx: Any) -> Any: ... async def edit_message( self, target_id: str, message_id: str, content: str, *, thread_id: str | None = None, account_id: str | None = None, ) -> str | None: ... async def send_card( self, target_id: str, card_content: dict, *, reply_to_id: str | None = None, thread_id: str | None = None, account_id: str | None = None, ) -> str | None: ... 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: ... def sanitize_text(self, text: str, payload: Any) -> str: ... def should_skip_plain_text_sanitization(self, payload: Any) -> bool: ... poll_max_options: int | None supports_poll_duration_seconds: bool supports_anonymous_polls: bool extract_markdown_images: bool def normalize_payload(self, payload: Any, config: dict, account_id: str | None = None) -> Any | None: ... def resolve_effective_text_chunk_limit( self, config: dict, account_id: str | None = None, fallback_limit: int | None = None ) -> int | None: ... presentation_capabilities: OutboundPresentationCapabilities | None delivery_capabilities: OutboundDeliveryCapabilities | None async def render_presentation(self, payload: Any, presentation: Any, ctx: Any) -> Any | None: ... async def pin_delivered_message(self, config: dict, target_ref: Any, message_id: str, pin: Any) -> None: ... async def before_deliver_payload( self, config: dict, target_ref: Any, payload: Any, hint: Any | None = None ) -> None: ... async def after_deliver_payload(self, config: dict, target_ref: Any, payload: Any, results: list) -> None: ... def resolve_target( self, to: str | None = None, *, config: dict | None = None, allow_from: list[str] | None = None, account_id: str | None = None, mode: str | None = None, ) -> tuple[bool, str]: """返回 (ok, to) 或 (False, error_message)""" ... def should_treat_delivered_text_as_visible(self, kind: str, text: str | None = None) -> bool: ... # ── Status ─────────────────────────────────────────────── @dataclass class ChannelAccountSnapshot: account_id: str name: str | None = None enabled: bool | None = None configured: bool | None = None status_state: str | None = None linked: bool | None = None running: bool | None = None connected: bool | None = None restart_pending: bool | None = None reconnect_attempts: int | None = None last_connected_at: float | None = None last_disconnect: dict | None = None last_message_at: float | None = None last_event_at: float | None = None last_transport_activity_at: float | None = None last_error: str | None = None health_state: str | None = None last_start_at: float | None = None last_stop_at: float | None = None last_inbound_at: float | None = None last_outbound_at: float | None = None busy: bool | None = None active_runs: int | None = None last_run_activity_at: float | None = None mode: str | None = None dm_policy: str | None = None allow_from: list[str] | None = None probe: Any | None = None last_probe_at: float | None = None audit: Any | None = None @dataclass class ChannelStatusIssue: channel: str account_id: str kind: str message: str fix: str | None = None @runtime_checkable class StatusProtocol(Protocol): default_runtime: ChannelAccountSnapshot | None async def probe(self, account: dict | None = None) -> bool: ... def build_summary(self, snapshot: Any) -> dict: ... def build_channel_summary( self, account: dict, config: dict, default_account_id: str, snapshot: ChannelAccountSnapshot, ) -> dict: ... def format_capabilities_probe(self, probe: Any) -> list[dict]: ... async def audit_account( self, account: dict, timeout_ms: int, config: dict, probe: Any | None = None ) -> Any: ... async def build_capabilities_diagnostics( self, account: dict, timeout_ms: int, config: dict, probe: Any | None = None, audit: Any | None = None, target: str | None = None, ) -> dict | None: ... def build_account_snapshot( self, account: dict, config: dict, runtime: ChannelAccountSnapshot | None = None, probe: Any | None = None, audit: Any | None = None, ) -> ChannelAccountSnapshot: ... def log_self_id( self, account: dict, config: dict, runtime: Any, include_channel_prefix: bool = False ) -> None: ... def resolve_account_state( self, account: dict, config: dict, configured: bool, enabled: bool, ) -> str: ... def collect_status_issues(self, accounts: list[ChannelAccountSnapshot]) -> list[ChannelStatusIssue]: ... def build_standard_summary( snapshot: ChannelAccountSnapshot, channel_id: str, ) -> dict: return { "channel": channel_id, "account_id": snapshot.account_id, "name": snapshot.name, "enabled": snapshot.enabled, "configured": snapshot.configured, "running": snapshot.running, "connected": snapshot.connected, "health_state": snapshot.health_state, "last_message_at": snapshot.last_message_at, "last_event_at": snapshot.last_event_at, "active_runs": snapshot.active_runs, "busy": snapshot.busy, "dm_policy": snapshot.dm_policy, "allow_from": snapshot.allow_from, "mode": snapshot.mode, "last_error": snapshot.last_error, } # ── Security ───────────────────────────────────────────── @runtime_checkable class SecurityProtocol(Protocol): async def check_allowlist(self, peer_id: str, channel_type: str) -> bool: ... def resolve_dm_policy(self) -> dict: ... def apply_config_fixes(self, config: dict) -> dict: ... def collect_warnings( self, config: dict, account_id: str | None = None, account: dict | None = None ) -> list[str]: ... def collect_audit_findings( self, config: dict, account_id: str | None = None, account: dict | None = None, source_config: dict | None = None, ordered_account_ids: list[str] | None = None, has_explicit_account_path: bool = False, ) -> list[dict]: ... # ── Pairing ────────────────────────────────────────────── @runtime_checkable class PairingProtocol(Protocol): id_label: str async def generate_code(self, peer_id: str) -> str: ... async def verify_code(self, peer_id: str, code: str) -> bool: ... def normalize_allow_entry(self, entry: str) -> str: ... async def notify_approval(self, config: dict, peer_id: str, account_id: str | None = None) -> None: ... # ── Groups ─────────────────────────────────────────────── @dataclass class ChannelGroupContext: config: dict group_id: str | None = None group_channel: str | None = None group_space: str | None = None account_id: str | None = None sender_id: str | None = None sender_name: str | None = None sender_username: str | None = None sender_e164: str | None = None @runtime_checkable class GroupsProtocol(Protocol): async def list_groups(self) -> list[dict]: ... def resolve_require_mention(self, ctx: ChannelGroupContext) -> bool | None: ... def resolve_group_intro_hint(self, ctx: ChannelGroupContext) -> str | None: ... def resolve_tool_policy(self, ctx: ChannelGroupContext) -> dict | None: ... # ── Mentions ───────────────────────────────────────────── @runtime_checkable class MentionsProtocol(Protocol): def extract_mentions(self, raw_message: dict) -> list[str]: ... def strip_mentions( self, text: str, ctx: object, config: dict | None = None, agent_id: str | None = None ) -> str: ... def strip_regexes(self, ctx: object, config: dict | None = None, agent_id: str | None = None) -> list: ... def strip_patterns(self, ctx: object, config: dict | None = None, agent_id: str | None = None) -> list[str]: ... # ── Streaming ──────────────────────────────────────────── class SseEventType(StrEnum): REASONING = "reasoning" DELTA = "delta" TOOL_START = "tool_start" TOOL_END = "tool_end" MESSAGE_END = "message_end" AGENT_END = "agent_end" PHASE = "phase" IMAGE = "image" FILE = "file" VIDEO = "video" DONE = "done" ERROR = "error" @runtime_checkable class StreamingEventProtocol(Protocol): def build_streaming_events(self, agent_output: dict) -> list[dict]: ... @property def supported_event_types(self) -> list[str]: ... @runtime_checkable class StreamingProtocol(Protocol): streaming_mode: str preview_stream_throttle_ms: int preview_min_initial_chars: int block_streaming_enabled: bool block_streaming_break: str block_streaming_chunk_min_chars: int block_streaming_chunk_max_chars: int block_streaming_chunk_break_preference: str block_streaming_coalesce_defaults: dict | None def create_draft_stream_session(self, target_id: str) -> object: ... def create_block_chunker(self) -> object: ... # ── Health ─────────────────────────────────────────────── @runtime_checkable class HealthProtocol(Protocol): async def health_check(self) -> bool: ... # ── Message ────────────────────────────────────────────── @runtime_checkable class MessageProtocol(Protocol): async def send_message(self, message: dict) -> None: ... # ── Routing ────────────────────────────────────────────── @runtime_checkable class RoutingProtocol(Protocol): pass # ── Startup ────────────────────────────────────────────── @runtime_checkable class StartupProtocol(Protocol): async def on_startup(self, config: dict) -> None: ... # ── Transport ──────────────────────────────────────────── @runtime_checkable class TransportProtocol(Protocol): async def connect(self) -> None: ... async def disconnect(self) -> None: ... # ── Lifecycle ──────────────────────────────────────────── @runtime_checkable class LifecycleProtocol(Protocol): async def on_config_changed(self, prev_cfg: dict, next_cfg: dict, account_id: str) -> None: ... async def on_account_removed(self, account_id: str) -> None: ... async def run_startup_maintenance(self, cfg: dict) -> None: ... async def on_retire(self) -> None: ... def detect_legacy_state_migrations( self, config: dict, state_dir: str, oauth_dir: str, ) -> list[dict]: ... # ── Secrets ────────────────────────────────────────────── @runtime_checkable class SecretsProtocol(Protocol): """密钥注册表与运行时配置注入适配器""" secret_target_registry_entries: list | None unsupported_secret_ref_surface_patterns: list[str] | None def collect_unsupported_secret_ref_config_candidates(self, raw: object) -> list[dict]: ... def collect_runtime_config_assignments( self, config: dict, defaults: object | None, context: object, ) -> None: ... # ── Approval ───────────────────────────────────────────── class ApprovalAction(StrEnum): APPROVE_EXEC = "approve_exec" APPROVE_READ = "approve_read" class ApprovalDecision(StrEnum): APPROVED = "approved" DENIED = "denied" CANCELLED = "cancelled" EXPIRED = "expired" @dataclass class ApprovalRequest: id: str channel_type: str account_id: str initiator_peer_id: str action: ApprovalAction description: str context: dict = field(default_factory=dict) created_at: float = field(default_factory=time.monotonic) expires_at: float | None = None @dataclass class ApprovalResult: request_id: str decision: ApprovalDecision decided_by: str | None = None decided_at: float = field(default_factory=time.monotonic) reason: str | None = None @dataclass class ApprovalForwardTarget: channel: str to: str account_id: str | None = None thread_id: str | int | None = None source: str | None = None class ApprovalKind(StrEnum): EXEC = "exec" PLUGIN = "plugin" @runtime_checkable class ApprovalProtocol(Protocol): async def check_approval_required(self, config: dict, action: ApprovalAction, initiator_peer_id: str) -> bool: ... async def create_approval_request( self, config: dict, action: ApprovalAction, initiator_peer_id: str, description: str, context: dict ) -> ApprovalRequest: ... async def send_approval_notification( self, config: dict, request: ApprovalRequest, approver_peer_ids: list[str] ) -> bool: ... async def check_approval_status(self, config: dict, request_id: str) -> ApprovalResult | None: ... def get_approver_ids(self, config: dict) -> list[str]: ... # ── Capability 扩展 ── def authorize_actor_action( self, config: dict, account_id: str | None, sender_id: str | None, action: str, approval_kind: str, ) -> dict: """返回 {"authorized": bool, "reason": str | None}""" ... def get_action_availability_state( self, config: dict, account_id: str | None, action: str, approval_kind: str | None = None, ) -> dict: """返回 {"kind": "enabled"|"disabled"|"unsupported"}""" ... def get_exec_initiating_surface_state( self, config: dict, account_id: str | None, action: str, ) -> dict: ... def resolve_approve_command_behavior( self, config: dict, account_id: str | None, sender_id: str | None, approval_kind: ApprovalKind, ) -> dict | None: ... # ── Delivery ── def has_configured_dm_route(self, config: dict) -> bool: ... def should_suppress_forwarding_fallback( self, config: dict, approval_kind: str, target: ApprovalForwardTarget, request: object, ) -> bool: ... # ── Render ── def build_exec_pending_payload( self, config: dict, request: object, target: ApprovalForwardTarget, now_ms: int, ) -> object | None: ... def build_exec_resolved_payload( self, config: dict, resolved: object, target: ApprovalForwardTarget, ) -> object | None: ... # ── Describe ── def describe_exec_approval_setup( self, channel: str, channel_label: str, account_id: str | None = None, ) -> str | None: ... # ── Commands ───────────────────────────────────────────── @dataclass class ChannelCommand: name: str aliases: list[str] = field(default_factory=list) description: str = "" usage: str = "" requires_admin: bool = False handler: str = "" @runtime_checkable class CommandsProtocol(Protocol): def get_commands(self) -> list[ChannelCommand]: ... async def handle_command(self, config: dict, command_name: str, args: list[str], msg: Any, ctx: Any) -> str: ... enforce_owner_for_commands: bool skip_when_config_empty: bool native_commands_auto_enabled: bool native_skills_auto_enabled: bool def resolve_native_command_name(self, command_key: str, default_name: str) -> str | None: ... def build_commands_list_channel_data( self, current_page: int, total_pages: int, agent_id: str | None = None, ) -> object | None: ... def build_models_menu_channel_data(self, providers: list[dict]) -> object | None: ... def build_models_provider_channel_data(self, providers: list[dict]) -> object | None: ... def build_models_add_provider_channel_data(self, providers: list[dict]) -> object | None: ... def build_models_list_channel_data( self, provider: str, models: list[str], current_model: str | None = None, current_page: int = 0, total_pages: int = 0, page_size: int | None = None, model_names: dict | None = None, ) -> object | None: ... def build_model_browse_channel_data(self) -> object | None: ... # ── Resolver ───────────────────────────────────────────── class ResolveKind(StrEnum): USER = "user" GROUP = "group" @dataclass class ResolveTarget: name: str resolved_id: str | None = None kind: str = "peer" confidence: float = 0.0 candidates: list[dict] = field(default_factory=list) @runtime_checkable class ResolverProtocol(Protocol): async def resolve_targets( self, config: dict, inputs: list[str], kind: ResolveKind, account_id: str | None = None, ) -> list[ResolveTarget]: ... async def resolve_peer(self, config: dict, name_or_alias: str) -> ResolveTarget: ... async def resolve_group(self, config: dict, name_or_alias: str) -> ResolveTarget: ... # ── Doctor ─────────────────────────────────────────────── @dataclass class DoctorConfigMutation: config: dict changes: list[str] = field(default_factory=list) warnings: list[str] | None = None @runtime_checkable class DoctorProtocol(Protocol): """配置诊断与自动修复适配器""" async def check_credentials(self, config: dict) -> CredentialCheckResult: """验证凭证有效性(AppKey/Secret 是否可获取 token)""" ... async def check_permissions(self, config: dict) -> PermissionCheckResult: """验证 API 权限(是否有收发消息权限)""" ... async def check_connectivity(self, config: dict) -> ConnectivityCheckResult: """连接测试(WebSocket/HTTP 端点可达性)""" ... def normalize_compatibility_config(self, config: dict) -> dict: """旧版配置格式兼容性迁移""" ... def collect_allowlist_warnings(self, allowlist_config: dict) -> list[DiagnosisWarning]: """白名单配置安全告警""" ... def generate_repair_plan(self, diagnosis: DiagnosisResult) -> list[RepairStep]: """根据诊断结果生成修复计划""" ... async def execute_repair(self, step: RepairStep, config: dict) -> RepairResult: """执行单个修复步骤""" ... def legacy_config_rules(self) -> list[LegacyConfigRule]: """声明旧版配置规则(供自动迁移使用)""" ... @property def dm_allow_from_mode(self) -> str: """DM 白名单模式""" ... @property def group_model(self) -> str: """群组权限模型""" ... group_allow_from_fallback_to_allow_from: bool warn_on_empty_group_sender_allowlist: bool def collect_preview_warnings(self, config: dict, doctor_fix_command: str) -> list[str]: ... def collect_mutable_allowlist_warnings(self, config: dict) -> list[str]: ... def repair_config(self, config: dict, doctor_fix_command: str) -> DoctorConfigMutation: ... def run_config_sequence(self, config: dict, should_repair: bool) -> dict: ... def clean_stale_config(self, config: dict) -> DoctorConfigMutation: ... def collect_empty_allowlist_extra_warnings(self, ctx: dict) -> list[str]: ... def should_skip_default_empty_group_allowlist_warning(self, ctx: dict) -> bool: ... # ── Directory ──────────────────────────────────────────── @dataclass class DirectoryPeer: """目录中的对等体(用户/机器人)""" id: str display_name: str kind: str = "user" avatar_url: str | None = None mention_name: str | None = None extra: dict = field(default_factory=dict) @dataclass class DirectoryGroup: """目录中的群组/频道""" id: str display_name: str kind: str = "group" member_count: int = 0 extra: dict = field(default_factory=dict) @dataclass class DirectoryEntry: """统一目录条目(对应 OpenClaw ChannelDirectoryEntry)""" kind: str id: str name: str | None = None handle: str | None = None avatar_url: str | None = None rank: int | None = None raw: object | None = None @dataclass class DirectorySearchParams: """目录搜索参数""" query: str kind: str = "user" limit: int = 20 offset: int = 0 include_bots: bool = False @dataclass class DirectorySearchResult: """目录搜索结果""" items: list[DirectoryPeer | DirectoryGroup] total: int has_more: bool offset: int = 0 @runtime_checkable class DirectoryProtocol(Protocol): """用户/群组/频道目录查询适配器""" async def self(self, config: dict, account_id: str | None = None) -> DirectoryEntry | None: """获取 bot 自身的目录条目""" ... async def list_peers(self, config: dict) -> list[DirectoryPeer]: """列出可用用户/联系人(静态,从配置解析)""" ... async def list_groups(self, config: dict) -> list[DirectoryGroup]: """列出可用群组/频道(静态,从配置解析)""" ... async def list_peers_live( self, config: dict, account_id: str | None = None, query: str | None = None, limit: int | None = None, ) -> list[DirectoryEntry]: """在线 API 查询用户/联系人""" ... async def list_groups_live( self, config: dict, account_id: str | None = None, query: str | None = None, limit: int | None = None, ) -> list[DirectoryEntry]: """在线 API 查询群组/频道""" ... async def list_group_members( self, config: dict, group_id: str, account_id: str | None = None, limit: int | None = None, ) -> list[DirectoryEntry]: """列出群组成员""" ... async def search_peers(self, config: dict, params: DirectorySearchParams) -> DirectorySearchResult: """搜索用户/联系人(在线,渠道 API 查询)""" ... async def search_groups(self, config: dict, params: DirectorySearchParams) -> DirectorySearchResult: """搜索群组/频道(在线,渠道 API 查询)""" ... async def get_peer(self, config: dict, peer_id: str) -> DirectoryPeer | None: """根据 ID 获取单个用户详情""" ... async def get_group(self, config: dict, group_id: str) -> DirectoryGroup | None: """根据 ID 获取单个群组详情""" ... # ── Heartbeat ──────────────────────────────────────────── @runtime_checkable class HeartbeatProtocol(Protocol): """心跳保活 + typing 指示器适配器""" @property def ping_interval_ms(self) -> int: """心跳间隔(毫秒),默认 30000""" ... async def check_ready( self, config: dict, account_id: str | None = None, deps: object | None = None, ) -> dict: """返回 {"ok": bool, "reason": str}""" ... async def on_ping(self, account: dict) -> bool: """执行心跳 ping,返回是否正常""" ... async def send_typing( self, config: dict, to: str, account_id: str | None = None, thread_id: str | int | None = None, deps: object | None = None, ) -> None: ... async def clear_typing( self, config: dict, to: str, account_id: str | None = None, thread_id: str | int | None = None, deps: object | None = None, ) -> None: ... async def start_typing(self, target_id: str, account: dict) -> object: """开始 typing 指示器,返回可用于 stop 的句柄""" ... async def stop_typing(self, handle: object) -> None: """停止 typing 指示器""" ... async def typing_indicator( self, target_id: str, account: dict, _stop_event, ) -> None: """完整 typing 生命周期:start → keepalive → stop""" ... # ── Agent Prompt ───────────────────────────────────────── @dataclass class AgentPromptContext: channel_type: str account_id: str peer_id: str | None = None peer_name: str | None = None group_id: str | None = None group_name: str | None = None thread_id: str | None = None @runtime_checkable class AgentPromptProtocol(Protocol): """渠道特有的 Agent 提示信息注入 不同渠道有不同的消息格式和交互约束,通过此协议向 LLM 注入 渠道特定的系统提示,让 Agent 知道当前在哪个渠道、有哪些能力可用。 """ def build_system_prompt(self, context: AgentPromptContext) -> str | None: """构建渠道特定的系统提示(注入到 Agent system prompt)""" ... def build_context_note(self, context: AgentPromptContext) -> str: """构建当前消息的上下文提示(注入到 user message 前缀)""" ... @property def channel_format_instructions(self) -> str | None: """渠道消息格式说明(如支持 Markdown、卡片格式、字数限制)""" ... def message_tool_hints(self, config: dict, account_id: str | None = None) -> list[str]: ... def message_tool_capabilities(self, config: dict, account_id: str | None = None) -> list[str] | None: ... def inbound_formatting_hints(self, account_id: str | None = None) -> dict | None: ... def reaction_guidance(self, config: dict, account_id: str | None = None) -> dict | None: ... # ── Threading ──────────────────────────────────────────── @dataclass class ThreadInfo: thread_id: str parent_message_id: str | None = None title: str | None = None is_threaded: bool = True @dataclass class ReplyTransport: mode: str = "inline" thread_id: str | None = None target_id: str | None = None @runtime_checkable class ThreadingProtocol(Protocol): """线程/话题解析适配器""" def extract_thread_id(self, msg: object) -> str | None: """从原始消息提取线程/话题 ID""" ... def resolve_reply_transport(self, msg: object, thread_id: str | None) -> ReplyTransport: """决定回复是以线程内回复还是广播方式""" ... def get_thread_info(self, thread_id: str) -> ThreadInfo | None: """获取线程详情""" ... def resolve_reply_to_mode( self, config: dict, account_id: str | None = None, chat_type: str | None = None, ) -> str: """返回 "off"|"first"|"all"|"batched" """ ... allow_explicit_reply_tags_when_off: bool def build_tool_context( self, config: dict, account_id: str | None, context: object, has_replied_ref: object | None = None, ) -> object | None: ... def resolve_auto_thread_id( self, config: dict, account_id: str | None, to: str, tool_context: object | None = None, reply_to_id: str | None = None, ) -> str | None: ... def resolve_focused_binding( self, config: dict, account_id: str | None, context: object, ) -> object | None: ... # ── Messaging ──────────────────────────────────────────── @dataclass class SessionResolution: """会话归属解析结果""" kind: str conversation_id: str label: str | None = None thread_id: str | None = None explicit_target: str | None = None @dataclass class BoundReplyPayload: """渠道感知的回复载荷""" target_id: str content: str reply_to_id: str | None = None thread_id: str | None = None routing_hint: dict | None = None metadata: dict | None = None @runtime_checkable class MessagingProtocol(Protocol): """会话感知路由适配器 渠道通过此协议自定义消息归属、会话路由、回复载荷构造。 不实现此协议的渠道使用默认逻辑(DM/群二分 + 纯文本回复)。 """ def resolve_session(self, msg: object) -> SessionResolution: """确定消息归属哪个会话""" ... def parse_explicit_target(self, content: str) -> str | None: """解析 @Bot 后缀的显式路由目标""" ... 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, ) -> BoundReplyPayload: """构建渠道感知的回复载荷""" ... def resolve_focused_binding(self, msg: object, session: SessionResolution, current_binding: object) -> dict | None: """会话内临时路由重定向(如话题内切换 Agent)""" ... @property def supported_explicit_targets(self) -> list[str]: """支持的显式路由目标关键词""" ... def build_attachment_refs(self, attachments: list[dict]) -> str: """将附件列表转换为文本引用 输入: [{"file_path": "/tmp/img.png", "file_type": "image", "file_name": "img.png"}] 输出: "\\n[图片: /tmp/img.png]\\n[文件: /tmp/doc.pdf]" """ ... # ── 扩展方法 ── target_prefixes: list[str] | None def normalize_target(self, raw: str) -> str | None: ... default_markdown_table_mode: str | None def normalize_explicit_session_key(self, session_key: str, ctx: object) -> str | None: ... def derive_legacy_session_chat_type(self, session_key: str) -> str | None: ... def is_legacy_group_session_key(self, key: str) -> bool: ... def canonicalize_legacy_session_key(self, key: str, agent_id: str) -> str | None: ... def resolve_legacy_group_session_key(self, ctx: object) -> dict | None: ... def resolve_inbound_attachment_roots(self, config: dict, account_id: str | None = None) -> list[str]: ... def resolve_remote_inbound_attachment_roots(self, config: dict, account_id: str | None = None) -> list[str]: ... def resolve_inbound_conversation( self, from_: str | None = None, to: str | None = None, conversation_id: str | None = None, thread_id: str | int | None = None, is_group: bool = False, ) -> dict | None: ... def resolve_delivery_target( self, conversation_id: str, parent_conversation_id: str | None = None, ) -> dict | None: ... def resolve_session_conversation(self, kind: str, raw_id: str) -> dict | None: ... def resolve_session_target( self, kind: str, id: str, thread_id: str | None = None, ) -> str | None: ... def parse_explicit_target_full(self, raw: str) -> dict | None: ... def infer_target_chat_type(self, to: str) -> str | None: ... preserve_heartbeat_thread_id_for_group_route: bool def build_cross_context_presentation( self, origin_label: str, message: str, config: dict, account_id: str | None = None ) -> object: ... def transform_reply_payload( self, payload: object, config: dict, account_id: str | None = None ) -> object | None: ... def enable_interactive_replies(self, config: dict, account_id: str | None = None) -> bool: ... def has_structured_reply_payload(self, payload: object) -> bool: ... def target_resolver_looks_like_id(self, raw: str, normalized: str | None = None) -> bool: ... async def target_resolver_resolve_target( self, config: dict, input_: str, normalized: str, account_id: str | None = None, preferred_kind: str | None = None, ) -> dict | None: ... def format_target_display(self, target: str, display: str | None = None, kind: str | None = None) -> str: ... async def resolve_outbound_session_route( self, config: dict, agent_id: str, target: str, account_id: str | None = None, current_session_key: str | None = None, resolved_target: dict | None = None, reply_to_id: str | None = None, thread_id: str | int | None = None, ) -> object | None: ... # ── Hooks ──────────────────────────────────────────────── @runtime_checkable class MessageSendingHook(Protocol): """入站消息发送前钩子 渠道插件可实现在回复发送前拦截/修改消息内容。 """ async def on_message_sending(self, msg: object, content: str) -> str | None: """回复发送前钩子,返回 None 表示阻止发送,返回 str 表示替换内容""" ... @runtime_checkable class InboundMessageHook(Protocol): """入站消息接收钩子 渠道插件可在消息被调度前进行预处理(过滤/转换)。 """ async def on_message_received(self, msg: object) -> object | None: """消息接收后、调度前钩子,返回 None 表示丢弃消息""" ... @runtime_checkable class SendLifecycleHook(Protocol): """发送生命周期钩子 渠道插件可监听发送各阶段事件。 """ async def before_send_attempt(self, target_id: str, content: str) -> None: """发送前钩子""" ... async def after_send_success(self, target_id: str, receipt: object) -> None: """发送成功后钩子""" ... async def after_send_failure(self, target_id: str, error: str) -> None: """发送失败后钩子""" ... async def after_commit(self, receipt: object) -> None: """提交确认后钩子""" ... # ── Config Schema ──────────────────────────────────────── @runtime_checkable class ConfigSchemaProtocol(Protocol): """渠道配置 JSON Schema 定义 用于前端表单生成和配置校验。每个渠道声明自己的配置结构。 """ def config_schema(self) -> dict: """返回 JSON Schema (draft-07) 描述的渠道配置结构""" ... # ── Defaults ───────────────────────────────────────────── @runtime_checkable class DefaultsProtocol(Protocol): """渠道默认值适配器 提供渠道级别的默认配置,如 debounce 时长等。 """ @property def debounce_ms(self) -> int: """消息去重 debounce 毫秒数,默认 5000""" ... # ── Reload ─────────────────────────────────────────────── @runtime_checkable class ReloadProtocol(Protocol): """配置热重载适配器 渠道声明哪些配置前缀变更时需要触发重新加载。 """ @property def config_prefixes(self) -> list[str]: """触发重载的配置键前缀列表""" ... # ── Setup ──────────────────────────────────────────────── @runtime_checkable class SetupProtocol(Protocol): """渠道初始化/安装适配器 渠道首次启用时执行的初始化流程。 """ async def setup(self, config: dict) -> bool: """执行渠道初始化,返回是否成功""" ... def resolve_account_id( self, config: dict, account_id: str | None = None, input_: object | None = None, ) -> str: ... def resolve_binding_account_id( self, config: dict, agent_id: str, account_id: str | None = None, ) -> str | None: ... def apply_account_name(self, config: dict, account_id: str, name: str | None = None) -> dict: ... def apply_account_config(self, config: dict, account_id: str, input_: object) -> dict: ... async def after_account_config_written( self, prev_cfg: dict, cfg: dict, account_id: str, input_: object, runtime: object, ) -> None: ... def validate_input(self, config: dict, account_id: str, input_: object) -> str | None: ... single_account_keys_to_move: list[str] | None named_account_promotion_keys: list[str] | None def resolve_single_account_promotion_target(self, channel: dict) -> str | None: ... # ── Setup Wizard ───────────────────────────────────────── @dataclass class SetupWizardStep: """安装向导步骤""" key: str label: str description: str = "" input_type: str = "text" required: bool = True default: str | None = None options: list[dict] = field(default_factory=list) @runtime_checkable class SetupWizardProtocol(Protocol): """渠道安装向导适配器 为渠道提供交互式配置引导流程。 """ def setup_wizard_steps(self) -> list[SetupWizardStep]: """返回安装向导步骤列表""" ... async def validate_wizard_input(self, step_key: str, value: str) -> str | None: """验证向导输入,返回错误消息或 None""" ... # ── Agent Tool ─────────────────────────────────────────── @dataclass class AgentToolParam: name: str type: str = "string" description: str = "" required: bool = False enum: list[str] | None = None default: Any = None items_type: str | None = None additional_properties: bool | None = None @dataclass class AgentTool: name: str description: str parameters: list[AgentToolParam] = field(default_factory=list) handler_name: str = "" scope: str = "current_channel" def to_openai_schema(self) -> dict: properties = {} required = [] for p in self.parameters: prop: dict[str, Any] = {"type": p.type, "description": p.description} if p.enum: prop["enum"] = p.enum if p.type == "array" and p.items_type: prop["items"] = {"type": p.items_type} if p.type == "object" and p.additional_properties is not None: prop["additionalProperties"] = p.additional_properties properties[p.name] = prop if p.required: required.append(p.name) return { "type": "function", "function": { "name": self.name, "description": self.description, "parameters": { "type": "object", "properties": properties, "required": required, }, }, } @runtime_checkable class AgentToolProtocol(Protocol): """渠道级 Agent 工具注册适配器 渠道通过此协议向 Agent 暴露可调用的工具,例如飞书的 wiki/doc/bitable/drive 等渠道专属能力。 每个工具包含 schema 定义和 handler 执行逻辑。 """ def get_agent_tools(self) -> list[AgentTool]: """返回当前渠道注册的 Agent 工具列表""" ... async def execute_agent_tool(self, tool_name: str, params: dict, context: dict) -> dict: """执行 Agent 工具调用 Args: tool_name: 工具名称 params: 工具参数 context: 执行上下文,包含 channel_type/account_id/peer_id 等 Returns: {"success": bool, "result": ..., "error": str | None} """ ... # ── Message Action ─────────────────────────────────────── @dataclass class MessageActionCapability: action: str description: str = "" parameters: list[AgentToolParam] = field(default_factory=list) requires_trusted_sender: bool = False scope: str = "current_channel" @runtime_checkable class MessageActionProtocol(Protocol): """统一消息操作命令适配器 渠道通过此协议声明支持的消息操作命令体系: send / edit / react / pin / unsend / channel-info 等。 与 AgentToolProtocol 协作,Agent 可通过 tool call 执行这些操作。 """ def get_message_actions(self) -> list[MessageActionCapability]: """返回渠道支持的消息操作列表""" ... async def execute_message_action( self, action: str, params: dict, context: dict, ) -> dict: """执行消息操作 Args: action: 操作名称 (send/edit/react/pin 等) params: 操作参数 context: 执行上下文 Returns: {"success": bool, "result": ..., "error": str | None} """ ... # ── 扩展方法 ── def describe_message_tool(self, ctx: dict) -> dict | None: ... def supports_action(self, action: str) -> bool: ... def resolve_execution_mode(self, action: str) -> str: ... def requires_trusted_requester_sender(self, action: str, tool_context: object | None = None) -> bool: ... def extract_tool_send(self, args: dict) -> dict | None: ... async def prepare_send_payload( self, ctx: object, to: str, payload: object, reply_to_id: str | None = None, thread_id: str | int | None = None ) -> object | None: ... async def handle_action(self, ctx: object) -> dict: ... # ── Auth ───────────────────────────────────────────────── @runtime_checkable class AuthProtocol(Protocol): """渠道认证/登录适配器 对应 OpenClaw ChannelAuthAdapter。渠道特有的登录认证流程, 如 OAuth 授权、QR 码登录、账号密码登录等。 """ async def login(self, config: dict) -> dict: """执行登录,返回认证凭据""" ... async def logout(self, config: dict) -> None: """执行登出,清除认证状态""" ... async def refresh_token(self, config: dict) -> dict | None: """刷新访问令牌,返回新的凭据或 None""" ... async def qr_auth_start(self, config: dict) -> str: """启动 QR 码认证,返回 QR 码 URL 或数据 Returns: QR 码数据字符串(URL 或 base64) """ ... async def qr_auth_wait(self, config: dict) -> dict | None: """等待 QR 码认证扫描结果 Returns: 认证凭据 dict 或 None(超时未扫描) """ ... # ── Elevated ───────────────────────────────────────────── @runtime_checkable class ElevatedProtocol(Protocol): """渠道提权适配器 对应 OpenClaw ChannelElevatedAdapter。 渠道需要以更高权限执行操作时使用(如管理群组、踢人)。 """ async def elevate(self, account: dict, reason: str) -> bool: """提升当前权限等级 Args: account: 渠道账户配置 reason: 提权原因 Returns: 是否成功提权 """ ... async def revoke(self, account: dict) -> None: """撤销提权""" ... @property def is_elevated(self) -> bool: """当前是否处于提权状态""" ... def allow_from_fallback( self, config: dict, account_id: str | None = None, ) -> list[str | int] | None: ... # ── Allowlist Management ───────────────────────────────── @runtime_checkable class AllowlistManagementProtocol(Protocol): """白名单管理适配器 对应 OpenClaw ChannelAllowlistAdapter。 渠道通过此协议管理 DM/群组白名单的增删改查。 """ async def list_allow_entries(self, config: dict, scope: str) -> list[str]: """列出白名单条目 Args: config: 渠道配置 scope: "dm" 或 "group" """ ... async def add_allow_entry(self, config: dict, scope: str, entry: str) -> bool: """添加白名单条目""" ... async def remove_allow_entry(self, config: dict, scope: str, entry: str) -> bool: """移除白名单条目""" ... async def resolve_allow_from(self, config: dict, account_id: str) -> list[str]: """解析完整的 allow_from 列表""" ... # ── 扩展方法 ── def apply_config_edit( self, config: dict, account_id: str | None, scope: str, action: str, entry: str, ) -> dict | None: """返回 {"kind": "ok", "changed": bool, "path_label": str, "write_target": object} 或 {"kind": "invalid-entry"} 或 None""" ... async def read_config(self, config: dict, account_id: str | None = None) -> dict: """返回 {"dm_allow_from": ..., "group_allow_from": ..., "dm_policy": str, ...}""" ... async def resolve_names( self, config: dict, account_id: str | None, scope: str, entries: list[str], ) -> list[dict]: ... def supports_scope(self, scope: str) -> bool: ... # ── Binding Provider ───────────────────────────────────── @dataclass class BindingConversationRef: conversation_id: str parent_conversation_id: str | None = None @runtime_checkable class BindingProviderProtocol(Protocol): """Agent 绑定编译与会话匹配适配器 对应 OpenClaw ChannelConfiguredBindingProvider。 """ self_parent_conversation_by_default: bool def compile_configured_binding( self, binding: dict, conversation_id: str, ) -> BindingConversationRef | None: ... def match_inbound_conversation( self, binding: dict, compiled_binding: BindingConversationRef, conversation_id: str, parent_conversation_id: str | None = None, ) -> object | None: ... def resolve_command_conversation(self, ctx: dict) -> BindingConversationRef | None: ... # ── Conversation Binding ───────────────────────────────── @runtime_checkable class ConversationBindingProtocol(Protocol): """会话绑定管理适配器 对应 OpenClaw ChannelConversationBindingSupport。 """ supports_current_conversation_binding: bool default_top_level_placement: str def resolve_conversation_ref( self, account_id: str | None, conversation_id: str, parent_conversation_id: str | None = None, thread_id: str | int | None = None, ) -> dict | None: ... async def build_bound_reply_payload( self, operation: str, placement: str, conversation: dict, ) -> object | None: ... def build_model_override_parent_candidates( self, parent_conversation_id: str | None = None, ) -> list[str] | None: ... def should_strip_thread_from_announce_origin(self, requester: dict, entry: dict) -> bool: ... def set_idle_timeout_by_session_key( self, target_session_key: str, account_id: str | None, idle_timeout_ms: int, ) -> list[dict]: ... def set_max_age_by_session_key( self, target_session_key: str, account_id: str | None, max_age_ms: int, ) -> list[dict]: ... async def create_manager(self, config: dict, account_id: str | None = None) -> object: """返回 {"stop": callable}""" ... # ── Message Adapter (Durable Final / Send / Live / Receive) ── class MessageDurabilityPolicy(StrEnum): REQUIRED = "required" BEST_EFFORT = "best_effort" DISABLED = "disabled" @runtime_checkable class MessageDurableFinalProtocol(Protocol): """持久化最终投递适配器 对应 OpenClaw ChannelMessageDurableFinalAdapter。 """ capabilities: dict | None async def reconcile_unknown_send(self, ctx: object) -> object | None: ... @runtime_checkable class MessageSendProtocol(Protocol): """消息发送适配器 对应 OpenClaw ChannelMessageSendAdapter。 """ async def send_text(self, ctx: object) -> object: ... async def send_media(self, ctx: object) -> object: ... async def send_payload(self, ctx: object) -> object: ... async def before_send_attempt(self, ctx: object) -> object: ... async def after_send_success(self, ctx: object) -> None: ... async def after_send_failure(self, ctx: object) -> None: ... async def after_commit(self, ctx: object) -> None: ... @runtime_checkable class MessageLiveProtocol(Protocol): """实时消息/流式预览适配器 对应 OpenClaw ChannelMessageLiveAdapterShape。 """ capabilities: dict | None finalizer_capabilities: dict | None @runtime_checkable class MessageReceiveProtocol(Protocol): """消息接收适配器 对应 OpenClaw ChannelMessageReceiveAdapterShape。 """ default_ack_policy: str supported_ack_policies: list[str] | None # ── File Handling ─────────────────────────────────────── @runtime_checkable class FileHandlingProtocol(Protocol): """文件上传/服务协议 渠道通过此协议可使用统一的文件上传、分类和服务能力, 替代各渠道分散实现的文件处理逻辑。 """ async def upload_file(self, file_data: bytes, file_name: str, session_id: str) -> dict: """上传文件,返回: { "file_path": "/abs/path/to/file", "file_name": "original.png", "file_type": "image" | "video" | "file", "preview_url": "/api/files/preview/{uuid}" } """ ... async def serve_file(self, file_path: str) -> tuple[bytes, str]: """服务文件,返回 (content_bytes, mime_type)""" ... @staticmethod def classify_file(extension: str) -> str: """根据扩展名分类:image / video / file""" ... # ── Reaction ───────────────────────────────────────────── @runtime_checkable class ReactionProtocol(Protocol): """消息反应/表情协议 渠道通过此协议支持对消息添加/移除表情反应。 对应 ChannelCapabilities.reactions 能力标志。 """ async def send_reaction( self, target_id: str, message_id: str, emoji: str, *, account_id: str | None = None, ) -> None: """为指定消息添加表情反应""" ... async def remove_reaction( self, target_id: str, message_id: str, emoji: str, *, account_id: str | None = None, ) -> None: """移除指定消息的表情反应""" ... async def fetch_reactions( self, target_id: str, message_id: str, *, account_id: str | None = None, ) -> list[dict]: """获取指定消息的反应列表""" ... # ── Webhook ────────────────────────────────────────────── @runtime_checkable class WebhookProtocol(Protocol): """Webhook 接收处理协议 渠道通过此协议注册和处理外部平台推送的 Webhook 事件。 """ async def handle_webhook( self, request: object, account_id: str | None = None, ) -> object: """处理 Webhook 请求,返回 HTTP Response""" ... def verify_signature(self, body: bytes, signature: str, secret: str) -> bool: """验证 Webhook 签名""" ... def resolve_webhook_auth_bypass_paths(self) -> list[str]: """返回不需要认证的 Webhook 路径列表""" ... # ── Inbound Handler ────────────────────────────────────── @runtime_checkable class InboundHandlerProtocol(Protocol): """入站消息处理协议 渠道通过此协议将原始平台事件转换为 UnifiedMessage。 对应各渠道的 Monitor 类核心职责。 """ async def handle_raw_event( self, event: dict, account: dict, ) -> object | None: """处理原始事件,返回 UnifiedMessage 或 None""" ... def parse_to_unified( self, raw_event: dict, account_id: str, ) -> object | None: """将原始事件解析为 UnifiedMessage""" ... def resolve_event_type(self, raw_event: dict) -> str: """解析原始事件类型(message/reaction/join/leave 等)""" ... # ── Media ──────────────────────────────────────────────── @runtime_checkable class MediaProtocol(Protocol): """媒体上传/下载/处理协议 渠道通过此协议统一管理媒体文件的上传、下载、类型检测和 URL 验证。 """ async def upload_media( self, file_path: str, media_type: str, *, account_id: str | None = None, ) -> dict: """上传媒体文件,返回 {"url": str, "type": str, "size": int}""" ... async def download_media( self, media_url: str, *, account_id: str | None = None, ) -> bytes: """下载媒体文件,返回文件字节""" ... def detect_media_type(self, filename: str, content_type: str = "") -> str: """根据文件名和 Content-Type 检测媒体类型(image/video/file)""" ... def validate_media_url(self, url: str) -> bool: """验证媒体 URL 是否有效""" ... def resolve_media_size_limit(self, media_type: str) -> int: """返回指定媒体类型的最大允许大小(字节)""" ... # ── Format ─────────────────────────────────────────────── @runtime_checkable class FormatProtocol(Protocol): """消息格式化协议 渠道通过此协议统一处理文本清理、Markdown 转换、提及剥离和分块。 """ def sanitize_text(self, text: str, payload: object | None = None) -> str: """清理文本中的非法字符或格式""" ... def chunker(self, text: str, limit: int, ctx: object | None = None) -> list[str]: """将长文本分块,返回文本块列表""" ... def markdown_to_native(self, md_text: str) -> dict | str: """将 Markdown 转换为目标平台的原生格式""" ... def native_to_markdown(self, native_content: dict | str) -> str: """将目标平台的原生格式转换为 Markdown""" ... def strip_mentions(self, text: str, ctx: object | None = None) -> str: """从文本中剥离提及标记""" ... # ── Dedupe ─────────────────────────────────────────────── @runtime_checkable class DedupeProtocol(Protocol): """消息去重协议 渠道通过此协议防止重复处理相同的消息或事件。 """ def is_duplicate(self, key: str) -> bool: """检查给定 key 是否已存在(重复)""" ... def mark_seen(self, key: str) -> None: """标记给定 key 为已处理""" ... def reset(self) -> None: """重置去重缓存""" ... @property def ttl_seconds(self) -> int: """去重缓存的 TTL(秒)""" ... @property def max_entries(self) -> int: """去重缓存的最大条目数""" ... # ── Card ───────────────────────────────────────────────── @runtime_checkable class CardProtocol(Protocol): """卡片/富文本消息协议 渠道通过此协议支持发送和更新结构化卡片消息。 """ async def send_card( self, target_id: str, card_content: dict, *, reply_to_id: str | None = None, account_id: str | None = None, ) -> str | None: """发送卡片消息,返回消息 ID""" ... async def update_card( self, message_id: str, card_content: dict, *, account_id: str | None = None, ) -> bool: """更新已发送的卡片消息""" ... def build_text_card(self, text: str, title: str | None = None, **kwargs) -> dict: """构建纯文本卡片""" ... def supports_card_interactions(self) -> bool: """是否支持卡片交互(按钮、选择器等)""" ... # ── Error Handling ─────────────────────────────────────── class ErrorSeverity(StrEnum): """错误严重级别""" RETRYABLE = "retryable" FATAL = "fatal" RATE_LIMITED = "rate_limited" FORBIDDEN = "forbidden" NETWORK = "network" @dataclass class ClassifiedError: """分类后的错误信息""" severity: ErrorSeverity retry_after_ms: int = 0 original_error: BaseException | None = None error_message: str = "" @runtime_checkable class ErrorHandlingProtocol(Protocol): """错误分类与重试协议 渠道通过此协议统一处理错误分类、重试策略和退避计算。 """ def classify_error(self, error: BaseException) -> ClassifiedError: """将异常分类为特定严重级别""" ... def is_retryable(self, error: BaseException) -> bool: """判断错误是否可重试""" ... def should_backoff(self, error: BaseException, attempt: int) -> int: """计算重试退避时间(毫秒),返回 0 表示不重试""" ... # ── Polling ────────────────────────────────────────────── @runtime_checkable class PollingProtocol(Protocol): """轮询消息接收协议 渠道通过此协议实现基于轮询的消息接收(替代 Webhook/Websocket)。 """ poll_interval_seconds: int async def poll_once(self, account: dict) -> list[dict]: """执行一次轮询,返回原始事件列表""" ... async def should_poll(self, account: dict) -> bool: """判断当前是否应该执行轮询""" ... def get_poll_cursor(self, account_id: str) -> str | None: """获取当前轮询游标""" ... def set_poll_cursor(self, account_id: str, cursor: str) -> None: """设置轮询游标""" ... # ── Pin ────────────────────────────────────────────────── @runtime_checkable class PinProtocol(Protocol): """消息置顶协议 渠道通过此协议支持消息的置顶和取消置顶操作。 """ async def pin_message( self, message_id: str, *, account_id: str | None = None, ) -> bool: """置顶指定消息""" ... async def unpin_message( self, message_id: str, *, account_id: str | None = None, ) -> bool: """取消置顶指定消息""" ... async def list_pins( self, chat_id: str, *, account_id: str | None = None, ) -> list[dict]: """列出指定聊天的置顶消息""" ... # ── Websocket ──────────────────────────────────────────── @runtime_checkable class WebsocketProtocol(Protocol): """WebSocket 生命周期协议 渠道通过此协议管理 WebSocket 连接的生命周期。 """ async def connect_ws(self, account: dict) -> object: """建立 WebSocket 连接,返回连接句柄""" ... async def disconnect_ws(self) -> None: """断开 WebSocket 连接""" ... async def on_ws_message(self, message: str | bytes) -> None: """处理收到的 WebSocket 消息""" ... @property def ws_reconnect_interval_ms(self) -> int: """WebSocket 重连间隔(毫秒)""" ... @property def ws_heartbeat_interval_ms(self) -> int: """WebSocket 心跳间隔(毫秒)""" ... # ── Protocol Composition Type Aliases ──────────────────── # # 渠道按能力分层,每类渠道只需标注其实现的能力层级。 # 使用示例: # def start_channel(plugin: GatewayPluginProtocol) -> None: ... # def deliver_message(plugin: FullMessagingPluginProtocol, msg: str) -> None: ... type BasePluginProtocol = MetaProtocol & CapabilitiesProtocol """最小插件协议:每个渠道插件必须满足 MetaProtocol + CapabilitiesProtocol""" type ConfigPluginProtocol = BasePluginProtocol & ConfigProtocol """可配置插件:支持多账户、配置校验、启用/禁用管理""" type GatewayPluginProtocol = ConfigPluginProtocol & GatewayProtocol & SecurityProtocol & StatusProtocol & HeartbeatProtocol """网关插件:具备网关连接、安全校验、状态探测、心跳保活能力""" type OutboundPluginProtocol = GatewayPluginProtocol & OutboundProtocol """外发插件:在网关基础上具备消息发送(文本/媒体/卡片/投票)能力""" type SessionPluginProtocol = OutboundPluginProtocol & MessagingProtocol & ThreadingProtocol """会话感知插件:具备会话路由、线程管理、回复载荷构建能力""" type FullMessagingPluginProtocol = ( SessionPluginProtocol & StreamingProtocol & AgentPromptProtocol & PairingProtocol & GroupsProtocol & MentionsProtocol & LifecycleProtocol ) """全功能消息插件:流程式消息 + 会话感知 + 完整生命周期""" type WebhookPluginProtocol = BasePluginProtocol & WebhookProtocol & InboundHandlerProtocol """Webhook 插件:通过 Webhook 接收消息的渠道""" type PollingPluginProtocol = BasePluginProtocol & PollingProtocol """轮询插件:通过定时轮询接收消息的渠道""" type ApprovalPluginProtocol = BasePluginProtocol & ApprovalProtocol """审批插件:支持执行/阅读审批流程的渠道"""