ForcePilot/backend/package/yuxi/channel/protocols.py
Kris a48f7ebf2a feat(channel): 新增通道基础模块与UI schema定义
本次提交新增了通道模块的完整基础实现:
1.  搭建了channel包的顶层导出结构,整合所有核心子模块接口
2.  实现了错误处理工具类与重试退避逻辑
3.  新增UI表单/页面/字段的schema定义与自动生成工具
4.  完成上下文管理模块,支持会话上下文、可见性过滤与运行时状态注册
5.  实现通道能力配置类,支持流式传输、功能开关等多维度能力定义
2026-05-21 10:35:30 +08:00

2333 lines
70 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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