本次提交新增了通道模块的完整基础实现: 1. 搭建了channel包的顶层导出结构,整合所有核心子模块接口 2. 实现了错误处理工具类与重试退避逻辑 3. 新增UI表单/页面/字段的schema定义与自动生成工具 4. 完成上下文管理模块,支持会话上下文、可见性过滤与运行时状态注册 5. 实现通道能力配置类,支持流式传输、功能开关等多维度能力定义
2333 lines
70 KiB
Python
2333 lines
70 KiB
Python
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
|
||
"""审批插件:支持执行/阅读审批流程的渠道"""
|