ForcePilot/backend/package/yuxi/channel/plugins/protocol.py
Kris bab30f2715
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Ruff Format Check / Ruff Format & Lint (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
feat:0715
2026-07-15 12:30:58 +08:00

232 lines
6.3 KiB
Python

from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass, field
from datetime import datetime
from enum import Flag, StrEnum, auto
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
from yuxi.channel.exceptions import ChannelErrorClassification
if TYPE_CHECKING:
from yuxi.channel.capabilities import CapabilityMatrix
from yuxi.channel.security.models import DmPolicy, GroupPolicy, RateLimitPolicy
from yuxi.channel.transport.protocol import Transport, TransportState
from yuxi.channel.transport.qr_login import QRLoginSession
__all__ = [
"BindingConversationRef",
"BindingRoute",
"ChannelCapability",
"ChannelErrorClassification",
"ChannelHealthStatus",
"ChannelMeta",
"ChannelPlugin",
"DeliveryCapabilities",
"DeliveryMode",
"DmPolicy",
"GroupPolicy",
"InboundMedia",
"InboundMessage",
"InboundRequest",
"OutboundMessage",
"QRLoginSession",
"RateLimitPolicy",
"SessionConversationRef",
"Transport",
"TransportState",
"TransportType",
]
@runtime_checkable
class InboundRequest(Protocol):
"""入站 HTTP 请求抽象,兼容 FastAPI Request 与 FakeRequest。"""
state: Any
headers: Mapping[str, str]
query_params: Mapping[str, str]
path_params: dict[str, str]
async def json(self) -> Any: ...
async def body(self) -> bytes: ...
class ChannelCapability(Flag):
TEXT = auto()
MARKDOWN = auto()
IMAGE = auto()
FILE = auto()
AUDIO = auto()
VIDEO = auto()
INTERACTIVE = auto()
REACTION = auto()
THREAD = auto()
EDIT = auto()
UNSEND = auto()
POLL = auto()
STREAMING = auto()
class DeliveryMode(StrEnum):
DIRECT = "direct"
GATEWAY = "gateway"
HYBRID = "hybrid"
class TransportType(StrEnum):
WEBHOOK = "webhook"
WEBSOCKET = "websocket"
POLLING = "polling"
@dataclass
class ChannelMeta:
channel_type: str
display_name: str
aliases: list[str] = field(default_factory=list)
capabilities: ChannelCapability = ChannelCapability.TEXT
capability_matrix: CapabilityMatrix | None = field(default=None)
delivery_mode: DeliveryMode = DeliveryMode.DIRECT
transport_type: TransportType = TransportType.WEBHOOK
config_schema: dict = field(default_factory=dict)
ui_hints: dict = field(default_factory=dict)
sort_weight: int = 0
icon: str | None = None
description: str = ""
def __post_init__(self) -> None:
if self.capability_matrix is None:
from yuxi.channel.capabilities import CapabilityMatrix
self.capability_matrix = CapabilityMatrix.from_channel_capability(self.capabilities)
@dataclass
class InboundMedia:
media_type: str
url: str | None = None
file_key: str | None = None
file_name: str | None = None
size: int | None = None
mime_type: str | None = None
file_content: bytes | None = None
@dataclass
class InboundMessage:
channel_type: str
account_id: str
channel_message_id: str | None = None
sender_id: str | None = None
sender_name: str | None = None
peer_id: str | None = None
session_key: str | None = None
chat_type: str | None = None
thread_id: str | None = None
content: str = ""
content_type: str = "text"
media: list[InboundMedia] = field(default_factory=list)
timestamp: datetime | None = None
raw_event: dict = field(default_factory=dict)
is_at_bot: bool = False
mentioned_user_ids: list[str] = field(default_factory=list)
is_scan_event: bool = False
@dataclass
class OutboundMessage:
content: str
content_type: str = "text"
media: list[dict] = field(default_factory=list)
reply_to_channel_message_id: str | None = None
thread_id: str | None = None
extra: dict = field(default_factory=dict)
@dataclass
class SessionConversationRef:
session_key: str
chat_type: str | None = None
channel_sender_id: str | None = None
parent_conversation_candidates: list[str] = field(default_factory=list)
channel_metadata: dict = field(default_factory=dict)
@dataclass
class BindingRoute:
agent_id: str | None
session_key: str
matched_by: str
binding_rule_hash: str | None = None
@dataclass
class BindingConversationRef:
session_key: str
channel_type: str
account_id: str
chat_type: str | None = None
peer_id: str | None = None
thread_id: str | None = None
binding_rule_hash: str = ""
@dataclass
class ChannelHealthStatus:
healthy: bool
state: str
enabled: bool
last_connected_at: datetime | None = None
last_message_at: datetime | None = None
last_error: str | None = None
reconnect_attempts: int = 0
def to_dict(self) -> dict[str, Any]:
"""将健康状态转换为可 JSON 序列化的字典。"""
return {
"healthy": self.healthy,
"state": self.state,
"enabled": self.enabled,
"last_connected_at": self.last_connected_at.isoformat() if self.last_connected_at else None,
"last_message_at": self.last_message_at.isoformat() if self.last_message_at else None,
"last_error": self.last_error,
"reconnect_attempts": self.reconnect_attempts,
}
@dataclass
class DeliveryCapabilities:
max_text_length: int = 4000
supports_markdown: bool = False
supports_interactive: bool = False
supports_media: bool = False
# 放在数据模型定义之后,按子模块分别导入,避免 ports 包 __init__ 循环导入
from yuxi.channel.ports._config import ConfigPort # noqa: E402
from yuxi.channel.ports._inbound import InboundPort # noqa: E402
from yuxi.channel.ports._lifecycle import LifecyclePort # noqa: E402
from yuxi.channel.ports._meta import MetaPort # noqa: E402
from yuxi.channel.ports._outbound import OutboundPort # noqa: E402
from yuxi.channel.ports._security import SecurityPort # noqa: E402
from yuxi.channel.ports._session import SessionPort # noqa: E402
from yuxi.channel.ports._status import StatusPort # noqa: E402
from yuxi.channel.ports._transport import TransportPort # noqa: E402
@runtime_checkable
class ChannelPlugin(
MetaPort,
ConfigPort,
InboundPort,
OutboundPort,
TransportPort,
SecurityPort,
StatusPort,
LifecyclePort,
SessionPort,
Protocol,
):
"""兼容旧代码的组合协议别名,继承所有核心 Port。"""