ForcePilot/backend/package/yuxi/channel/protocols.py

2333 lines
70 KiB
Python
Raw Normal View History

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
"""审批插件:支持执行/阅读审批流程的渠道"""