mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-28 19:47:41 +08:00
366 lines
12 KiB
Python
366 lines
12 KiB
Python
"""通知渠道能力与 API 输出模型。"""
|
|
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from typing import Dict, Optional, Set
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app.schemas.types import NotificationChannel
|
|
|
|
|
|
class WechatClawBotKnownTarget(BaseModel):
|
|
"""微信 ClawBot 已知消息目标。"""
|
|
|
|
userid: str
|
|
username: str
|
|
last_active: Optional[int | float] = None
|
|
|
|
|
|
class WechatClawBotData(BaseModel):
|
|
"""微信 ClawBot 登录状态或操作结果。"""
|
|
|
|
success: bool
|
|
message: Optional[str] = None
|
|
connected: Optional[bool] = None
|
|
account_id: Optional[str] = None
|
|
qrcode: Optional[str] = None
|
|
qrcode_url: Optional[str] = None
|
|
qrcode_status: Optional[str] = None
|
|
qrcode_updated_at: Optional[int | float] = None
|
|
known_targets: list[WechatClawBotKnownTarget] = Field(default_factory=list)
|
|
default_target: Optional[str] = None
|
|
base_url: Optional[str] = None
|
|
|
|
|
|
class ChannelCapability(Enum):
|
|
"""
|
|
渠道能力枚举
|
|
"""
|
|
|
|
# 支持内联按钮
|
|
INLINE_BUTTONS = "inline_buttons"
|
|
# 支持菜单命令
|
|
MENU_COMMANDS = "menu_commands"
|
|
# 支持消息编辑
|
|
MESSAGE_EDITING = "message_editing"
|
|
# 支持消息删除
|
|
MESSAGE_DELETION = "message_deletion"
|
|
# 支持回调查询
|
|
CALLBACK_QUERIES = "callback_queries"
|
|
# 支持富文本
|
|
RICH_TEXT = "rich_text"
|
|
# 支持 Markdown
|
|
MARKDOWN = "markdown"
|
|
# 支持图片
|
|
IMAGES = "images"
|
|
# 支持链接
|
|
LINKS = "links"
|
|
# 支持原生语音输出
|
|
AUDIO_OUTPUT = "audio_output"
|
|
# 支持文件发送
|
|
FILE_SENDING = "file_sending"
|
|
# 支持可收口的消息处理状态提示,如 reaction 或 typing
|
|
PROCESSING_STATUS = "processing_status"
|
|
|
|
|
|
@dataclass
|
|
class ChannelCapabilities:
|
|
"""
|
|
渠道能力配置
|
|
"""
|
|
|
|
channel: NotificationChannel
|
|
capabilities: Set[ChannelCapability]
|
|
max_buttons_per_row: int = 5
|
|
max_button_rows: int = 10
|
|
max_button_text_length: int = 30
|
|
# 单条消息最大长度(0 表示不限制),用于流式输出时自动分段
|
|
max_message_length: int = 0
|
|
fallback_enabled: bool = True
|
|
|
|
|
|
class ChannelCapabilityManager:
|
|
"""
|
|
渠道能力管理器
|
|
"""
|
|
|
|
_capabilities: Dict[NotificationChannel, ChannelCapabilities] = {
|
|
NotificationChannel.Telegram: ChannelCapabilities(
|
|
channel=NotificationChannel.Telegram,
|
|
capabilities={
|
|
ChannelCapability.INLINE_BUTTONS,
|
|
ChannelCapability.MENU_COMMANDS,
|
|
ChannelCapability.MESSAGE_EDITING,
|
|
ChannelCapability.MESSAGE_DELETION,
|
|
ChannelCapability.CALLBACK_QUERIES,
|
|
ChannelCapability.MARKDOWN,
|
|
ChannelCapability.RICH_TEXT,
|
|
ChannelCapability.IMAGES,
|
|
ChannelCapability.LINKS,
|
|
ChannelCapability.AUDIO_OUTPUT,
|
|
ChannelCapability.FILE_SENDING,
|
|
ChannelCapability.PROCESSING_STATUS,
|
|
},
|
|
max_buttons_per_row=4,
|
|
max_button_rows=10,
|
|
max_button_text_length=30,
|
|
# Telegram 文本消息限制 4096 字符,预留空间给 MarkdownV2 转义和标题
|
|
max_message_length=3500,
|
|
),
|
|
NotificationChannel.Wechat: ChannelCapabilities(
|
|
channel=NotificationChannel.Wechat,
|
|
capabilities={
|
|
ChannelCapability.IMAGES,
|
|
ChannelCapability.LINKS,
|
|
ChannelCapability.AUDIO_OUTPUT,
|
|
ChannelCapability.MENU_COMMANDS,
|
|
},
|
|
fallback_enabled=True,
|
|
),
|
|
NotificationChannel.Feishu: ChannelCapabilities(
|
|
channel=NotificationChannel.Feishu,
|
|
capabilities={
|
|
ChannelCapability.INLINE_BUTTONS,
|
|
ChannelCapability.MESSAGE_EDITING,
|
|
ChannelCapability.CALLBACK_QUERIES,
|
|
ChannelCapability.MARKDOWN,
|
|
ChannelCapability.RICH_TEXT,
|
|
ChannelCapability.IMAGES,
|
|
ChannelCapability.LINKS,
|
|
ChannelCapability.AUDIO_OUTPUT,
|
|
ChannelCapability.FILE_SENDING,
|
|
ChannelCapability.PROCESSING_STATUS,
|
|
},
|
|
max_buttons_per_row=3,
|
|
max_button_rows=8,
|
|
max_button_text_length=20,
|
|
max_message_length=30000,
|
|
fallback_enabled=True,
|
|
),
|
|
NotificationChannel.WechatClawBot: ChannelCapabilities(
|
|
channel=NotificationChannel.WechatClawBot,
|
|
capabilities={
|
|
ChannelCapability.MARKDOWN,
|
|
ChannelCapability.IMAGES,
|
|
ChannelCapability.LINKS,
|
|
ChannelCapability.FILE_SENDING,
|
|
},
|
|
max_message_length=2800,
|
|
fallback_enabled=True,
|
|
),
|
|
NotificationChannel.Slack: ChannelCapabilities(
|
|
channel=NotificationChannel.Slack,
|
|
capabilities={
|
|
ChannelCapability.INLINE_BUTTONS,
|
|
ChannelCapability.MESSAGE_EDITING,
|
|
ChannelCapability.MESSAGE_DELETION,
|
|
ChannelCapability.CALLBACK_QUERIES,
|
|
ChannelCapability.MARKDOWN,
|
|
ChannelCapability.RICH_TEXT,
|
|
ChannelCapability.IMAGES,
|
|
ChannelCapability.LINKS,
|
|
ChannelCapability.MENU_COMMANDS,
|
|
ChannelCapability.FILE_SENDING,
|
|
ChannelCapability.PROCESSING_STATUS,
|
|
},
|
|
max_buttons_per_row=3,
|
|
max_button_rows=8,
|
|
max_button_text_length=25,
|
|
# Slack 消息限制 40000 字符,预留空间给格式化
|
|
max_message_length=39000,
|
|
fallback_enabled=True,
|
|
),
|
|
NotificationChannel.Discord: ChannelCapabilities(
|
|
channel=NotificationChannel.Discord,
|
|
capabilities={
|
|
ChannelCapability.INLINE_BUTTONS,
|
|
ChannelCapability.MESSAGE_EDITING,
|
|
ChannelCapability.MESSAGE_DELETION,
|
|
ChannelCapability.CALLBACK_QUERIES,
|
|
ChannelCapability.MARKDOWN,
|
|
ChannelCapability.RICH_TEXT,
|
|
ChannelCapability.IMAGES,
|
|
ChannelCapability.LINKS,
|
|
ChannelCapability.FILE_SENDING,
|
|
ChannelCapability.PROCESSING_STATUS,
|
|
},
|
|
max_buttons_per_row=5,
|
|
max_button_rows=5,
|
|
max_button_text_length=80,
|
|
# Discord 消息限制 2000 字符
|
|
max_message_length=1800,
|
|
fallback_enabled=True,
|
|
),
|
|
NotificationChannel.DingTalk: ChannelCapabilities(
|
|
channel=NotificationChannel.DingTalk,
|
|
capabilities={
|
|
ChannelCapability.MARKDOWN,
|
|
ChannelCapability.RICH_TEXT,
|
|
ChannelCapability.IMAGES,
|
|
ChannelCapability.LINKS,
|
|
},
|
|
# 自定义机器人 Markdown 文本上限为 20000 字节,预留标题和图片链接空间。
|
|
max_message_length=18000,
|
|
fallback_enabled=True,
|
|
),
|
|
NotificationChannel.SynologyChat: ChannelCapabilities(
|
|
channel=NotificationChannel.SynologyChat,
|
|
capabilities={
|
|
ChannelCapability.MARKDOWN,
|
|
ChannelCapability.RICH_TEXT,
|
|
ChannelCapability.IMAGES,
|
|
ChannelCapability.LINKS,
|
|
},
|
|
fallback_enabled=True,
|
|
),
|
|
NotificationChannel.VoceChat: ChannelCapabilities(
|
|
channel=NotificationChannel.VoceChat,
|
|
capabilities={
|
|
ChannelCapability.MARKDOWN,
|
|
ChannelCapability.RICH_TEXT,
|
|
ChannelCapability.IMAGES,
|
|
ChannelCapability.LINKS,
|
|
},
|
|
fallback_enabled=True,
|
|
),
|
|
NotificationChannel.WebPush: ChannelCapabilities(
|
|
channel=NotificationChannel.WebPush,
|
|
capabilities={ChannelCapability.LINKS},
|
|
fallback_enabled=True,
|
|
),
|
|
NotificationChannel.Web: ChannelCapabilities(
|
|
channel=NotificationChannel.Web,
|
|
capabilities={
|
|
ChannelCapability.MARKDOWN,
|
|
ChannelCapability.RICH_TEXT,
|
|
ChannelCapability.IMAGES,
|
|
ChannelCapability.LINKS,
|
|
},
|
|
fallback_enabled=True,
|
|
),
|
|
NotificationChannel.WebAgent: ChannelCapabilities(
|
|
channel=NotificationChannel.WebAgent,
|
|
capabilities={
|
|
ChannelCapability.INLINE_BUTTONS,
|
|
ChannelCapability.CALLBACK_QUERIES,
|
|
ChannelCapability.MESSAGE_EDITING,
|
|
ChannelCapability.MARKDOWN,
|
|
ChannelCapability.RICH_TEXT,
|
|
ChannelCapability.IMAGES,
|
|
ChannelCapability.LINKS,
|
|
ChannelCapability.AUDIO_OUTPUT,
|
|
ChannelCapability.FILE_SENDING,
|
|
},
|
|
fallback_enabled=False,
|
|
),
|
|
NotificationChannel.QQ: ChannelCapabilities(
|
|
channel=NotificationChannel.QQ,
|
|
capabilities={
|
|
ChannelCapability.RICH_TEXT,
|
|
ChannelCapability.IMAGES,
|
|
ChannelCapability.LINKS,
|
|
ChannelCapability.INLINE_BUTTONS,
|
|
ChannelCapability.CALLBACK_QUERIES,
|
|
},
|
|
max_buttons_per_row=5,
|
|
max_button_rows=5,
|
|
max_button_text_length=30,
|
|
fallback_enabled=True,
|
|
),
|
|
}
|
|
|
|
@classmethod
|
|
def get_capabilities(cls, channel: NotificationChannel) -> Optional[ChannelCapabilities]:
|
|
"""
|
|
获取渠道能力
|
|
"""
|
|
return cls._capabilities.get(channel)
|
|
|
|
@classmethod
|
|
def supports_capability(
|
|
cls, channel: NotificationChannel, capability: ChannelCapability
|
|
) -> bool:
|
|
"""
|
|
检查渠道是否支持某项能力
|
|
"""
|
|
channel_caps = cls.get_capabilities(channel)
|
|
if not channel_caps:
|
|
return False
|
|
return capability in channel_caps.capabilities
|
|
|
|
@classmethod
|
|
def supports_buttons(cls, channel: NotificationChannel) -> bool:
|
|
"""
|
|
检查渠道是否支持按钮
|
|
"""
|
|
return cls.supports_capability(channel, ChannelCapability.INLINE_BUTTONS)
|
|
|
|
@classmethod
|
|
def supports_callbacks(cls, channel: NotificationChannel) -> bool:
|
|
"""
|
|
检查渠道是否支持回调
|
|
"""
|
|
return cls.supports_capability(channel, ChannelCapability.CALLBACK_QUERIES)
|
|
|
|
@classmethod
|
|
def supports_editing(cls, channel: NotificationChannel) -> bool:
|
|
"""
|
|
检查渠道是否支持消息编辑
|
|
"""
|
|
return cls.supports_capability(channel, ChannelCapability.MESSAGE_EDITING)
|
|
|
|
@classmethod
|
|
def supports_markdown(cls, channel: NotificationChannel) -> bool:
|
|
"""
|
|
检查渠道是否支持 Markdown。
|
|
"""
|
|
return cls.supports_capability(channel, ChannelCapability.MARKDOWN)
|
|
|
|
@classmethod
|
|
def supports_deletion(cls, channel: NotificationChannel) -> bool:
|
|
"""
|
|
检查渠道是否支持消息删除
|
|
"""
|
|
return cls.supports_capability(channel, ChannelCapability.MESSAGE_DELETION)
|
|
|
|
@classmethod
|
|
def get_max_buttons_per_row(cls, channel: NotificationChannel) -> int:
|
|
"""
|
|
获取每行最大按钮数
|
|
"""
|
|
channel_caps = cls.get_capabilities(channel)
|
|
return channel_caps.max_buttons_per_row if channel_caps else 2
|
|
|
|
@classmethod
|
|
def get_max_button_rows(cls, channel: NotificationChannel) -> int:
|
|
"""
|
|
获取最大按钮行数
|
|
"""
|
|
channel_caps = cls.get_capabilities(channel)
|
|
return channel_caps.max_button_rows if channel_caps else 5
|
|
|
|
@classmethod
|
|
def get_max_button_text_length(cls, channel: NotificationChannel) -> int:
|
|
"""
|
|
获取按钮文本最大长度
|
|
"""
|
|
channel_caps = cls.get_capabilities(channel)
|
|
return channel_caps.max_button_text_length if channel_caps else 20
|
|
|
|
@classmethod
|
|
def get_max_message_length(cls, channel: NotificationChannel) -> int:
|
|
"""
|
|
获取单条消息最大长度(0 表示不限制)
|
|
"""
|
|
channel_caps = cls.get_capabilities(channel)
|
|
return channel_caps.max_message_length if channel_caps else 0
|
|
|
|
@classmethod
|
|
def should_use_fallback(cls, channel: NotificationChannel) -> bool:
|
|
"""
|
|
是否应该使用降级策略
|
|
"""
|
|
channel_caps = cls.get_capabilities(channel)
|
|
return channel_caps.fallback_enabled if channel_caps else True
|