这是一个批量整理提交,包含以下主要改动: 1. 删除多处冗余的空行和未使用的导入 2. 修复文件末尾缺少换行符的问题 3. 调整部分模块的导入顺序与代码排版 4. 修复部分配置默认值与策略逻辑 5. 新增多个功能模块与辅助工具 6. 完善异常处理与日志记录 7. 修复速率限制、消息缓存、权限校验等逻辑bug 8. 废弃部分旧有API与配置项并添加警告提示
334 lines
9.3 KiB
Python
334 lines
9.3 KiB
Python
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),
|
||
)
|