2026-05-12 00:53:57 +08:00
|
|
|
|
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
|
2026-05-12 14:51:53 +08:00
|
|
|
|
auth_expired: bool = False
|
2026-05-12 00:53:57 +08:00
|
|
|
|
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),
|
|
|
|
|
|
)
|