from __future__ import annotations from datetime import datetime from enum import StrEnum from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field from yuxi.utils.datetime_utils import utc_now_naive class MessageType(StrEnum): TEXT = "text" IMAGE = "image" FILE = "file" AUDIO = "audio" VIDEO = "video" LOCATION = "location" STICKER = "sticker" CARD = "card" POLL = "poll" COMMAND = "command" class ChannelType(StrEnum): WEBCHAT = "webchat" TELEGRAM = "telegram" SLACK = "slack" DISCORD = "discord" NOSTR = "nostr" DINGDING = "dingding" FEISHU = "feishu" WHATSAPP = "whatsapp" LINE = "line" WECHAT = "wechat" QQ_BOT = "qq_bot" WECHAT_WORK = "wechat_work" ALIBABA = "alibaba" VK = "vk" ZALO = "zalo" ZALO_OA = "zalo_oa" ZALO_USER = "zalo_user" SIGNAL = "signal" IMO = "imo" MS_TEAMS = "ms_teams" GOOGLE_CHAT = "google_chat" ROCKETCHAT = "rocketchat" MATRIX = "matrix" MATTERMOST = "mattermost" SKYPE = "skype" KIK = "kik" THREADS = "threads" TWITTER = "twitter" SMS = "sms" EMAIL = "email" SYNOLOGYCHAT = "synologychat" BLUEBUBBLES = "bluebubbles" IMESSAGE = "imessage" NEXTCLOUDTALK = "nextcloud-talk" IRC = "irc" TWITCH = "twitch" URBIT = "urbit" YUANBAO = "yuanbao" class ChatType(StrEnum): DIRECT = "direct" CHANNEL = "channel" GROUP = "group" THREAD = "thread" FORUM = "forum" GUILD_CHANNEL = "guild_channel" SPACE = "space" CHAT_ROOM = "chat_room" class EventType(StrEnum): MESSAGE_RECEIVED = "message.received" MESSAGE_UPDATED = "message.updated" MESSAGE_DELETED = "message.deleted" BOT_ADDED = "bot.added" BOT_REMOVED = "bot.removed" MEMBER_JOINED = "member.joined" MEMBER_LEFT = "member.left" CARD_ACTION = "card.action" REACTION_ADDED = "reaction.added" REACTION_REMOVED = "reaction.removed" BOT_MENU = "bot.menu" TYPING = "typing" READ_RECEIPT = "read_receipt" SYSTEM_EVENT = "system.event" class RejectReason(StrEnum): UNAUTHORIZED = "unauthorized" RATE_LIMITED = "rate_limited" SIZE_EXCEEDED = "size_exceeded" UNSUPPORTED_TYPE = "unsupported_type" POLICY_DENIED = "policy_denied" INVALID_PAYLOAD = "invalid_payload" TIMEOUT = "timeout" INTERNAL_ERROR = "internal_error" BLOCKED_USER = "blocked_user" BLOCKED_GROUP = "blocked_group" class Attachment(BaseModel): type: str = "file" url: str | None = None filename: str | None = None size_bytes: int | None = None mime_type: str | None = None file_id: str | None = None metadata: dict[str, Any] = {} class MentionsInfo(BaseModel): mentioned_user_ids: list[str] = [] is_bot_mentioned: bool = False raw_text: str | None = None class ChannelIdentity(BaseModel): channel_id: str channel_type: ChannelType channel_user_id: str channel_chat_id: str channel_message_id: str | None = None class ChannelMessage(BaseModel): identity: ChannelIdentity event_type: EventType = EventType.MESSAGE_RECEIVED message_type: MessageType = MessageType.TEXT chat_type: ChatType = ChatType.DIRECT content: str attachments: list[Attachment] = [] mentions: MentionsInfo | None = None extracted_urls: list[str] = [] reply_to_message_id: str | None = None metadata: dict[str, Any] = {} timestamp: datetime = Field(default_factory=utc_now_naive) class ChannelResponse(BaseModel): identity: ChannelIdentity message_type: MessageType = MessageType.TEXT content: str attachments: list[Attachment] = [] reply_to_message_id: str | None = None metadata: dict[str, Any] = {} timestamp: datetime = Field(default_factory=utc_now_naive) class AgentRequest(BaseModel): user_id: str thread_id: str agent_id: str | None = None agent_config_id: int message: ChannelMessage context: dict[str, Any] = {} class AgentResult(BaseModel): response_text: str response_type: Literal["text", "stream", "error", "interrupt"] = "text" attachments: list[Attachment] = [] agent_state: dict[str, Any] | None = None metadata: dict[str, Any] = {} class DeliveryResult(BaseModel): success: bool message_id: str | None = None error: str | None = None auth_expired: bool = False metadata: dict[str, Any] = {} class ChannelStatus(StrEnum): DISCONNECTED = "disconnected" CONNECTING = "connecting" CONNECTED = "connected" RECONNECTING = "reconnecting" ERROR = "error" DISABLED = "disabled" class HealthStatus(BaseModel): status: Literal["healthy", "degraded", "unhealthy"] latency_ms: float | None = None last_error: str | None = None last_connected_at: datetime | None = None metadata: dict[str, Any] = {} class TokenStatus(BaseModel): """Token / 密钥状态 — 对齐清单 5.5.3""" source: str = "" status: str = "unknown" last_verified_at: float = 0.0 class ChannelAccountSnapshot(BaseModel): """渠道账户运行状态快照(所有适配器统一格式) 统一了 QQBot/WhatsApp/微信/飞书四份方案中的状态字段。 适配器可通过继承扩展渠道专属字段。 字段按语义分组: - 标识:account_id, name - 配置状态:configured, enabled - 连接状态:linked, running, connected, status_state, health_state - 时间线:8 个 last_* 时间戳 - 错误与重连:last_error, last_disconnect, reconnect_attempts - 安全策略:dm_policy, group_policy, allow_from_count - 负载:busy, active_runs, chain_stopped - Token/密钥:token_status, bot_token_status, app_token_status 等 - 网络配置:webhook_path, webhook_url, base_url, port 等 - 探测/审计:probe, last_probe_at, audit - Bot/应用信息:application, bot, profile, public_key """ account_id: str = "" name: str = "" configured: bool = False enabled: bool = True linked: bool = False running: bool = False connected: bool = False status_state: str = "not-configured" health_state: str = "stopped" last_start_at: float | None = None last_stop_at: float | None = None last_connected_at_s: float | None = None last_message_at: float | None = None last_event_at: float | None = None last_inbound_at: float | None = None last_outbound_at: float | None = None last_transport_activity_at: float | None = None last_error: str | None = None last_disconnect: dict | None = None reconnect_attempts: int = 0 dm_policy: str = "pairing" group_policy: str = "allowlist" allow_from_count: int = 0 busy: bool = False active_runs: int = 0 chain_stopped: bool = False token_source: str = "" token_status: TokenStatus | None = None bot_token_source: str = "" bot_token_status: TokenStatus | None = None app_token_source: str = "" app_token_status: TokenStatus | None = None signing_secret_source: str = "" signing_secret_status: TokenStatus | None = None user_token_status: TokenStatus | None = None credential_source: str = "" secret_source: str = "" webhook_path: str = "" webhook_url: str = "" base_url: str = "" cli_path: str = "" db_path: str = "" port: int = 0 probe: dict | None = None last_probe_at: float = 0.0 audit: dict | None = None application: dict | None = None bot: dict | None = None public_key: str = "" profile: dict | None = None model_config = ConfigDict(extra="allow") class MessageActionRequest(BaseModel): action: str message_id: str params: dict[str, Any] = {} class MessageActionResult(BaseModel): success: bool message: str = "" data: dict[str, Any] | None = None def build_snapshot_from_adapter(adapter) -> ChannelAccountSnapshot: """从适配器实例构建通用快照 使用 getattr 安全读取,兼容适配器未定义某个字段的场景。 适配器可以覆写此函数或直接继承 ChannelAccountSnapshot。 用法: snapshot = build_snapshot_from_adapter(self) snapshot.reconnect_attempts += 1 """ status = getattr(adapter, "_status", None) status_str = str(status.value) if hasattr(status, "value") else str(status or "stopped") return ChannelAccountSnapshot( account_id=getattr(adapter, "account_id", ""), name=getattr(adapter, "account_name", ""), configured=getattr(adapter, "_token_mgr", None) is not None, enabled=getattr(adapter, "_enabled", True), linked=getattr(adapter, "_linked", False), running=getattr(adapter, "_running", False), connected=getattr(adapter, "_connected", False), status_state=getattr(adapter, "_status_state", "not-configured"), health_state=status_str, last_connected_at_s=getattr(adapter, "_last_connected_at", None), last_message_at=getattr(adapter, "_last_message_at", None), last_error=getattr(adapter, "_last_error", None), reconnect_attempts=getattr(adapter, "_reconnect_attempts", 0), dm_policy=str(getattr(adapter, "dm_policy", "allowlist")), group_policy=str(getattr(adapter, "group_policy", "allowlist")), busy=getattr(adapter, "_busy", False), active_runs=getattr(adapter, "_active_runs", 0), )