ForcePilot/backend/package/yuxi/channels/models.py

334 lines
9.3 KiB
Python
Raw Normal View History

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),
)