232 lines
6.3 KiB
Python
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。"""
|