mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
388 lines
12 KiB
Python
388 lines
12 KiB
Python
from enum import Enum
|
||
from typing import Optional, Union, List, Dict, Any
|
||
|
||
from pydantic import BaseModel, Field, field_validator
|
||
|
||
from app.schemas.common import JsonData
|
||
from app.schemas.types import ContentType, MessageType, NotificationChannel
|
||
|
||
|
||
class MessageClearScope(str, Enum):
|
||
"""
|
||
通知中心清理范围。
|
||
"""
|
||
|
||
# 全部消息
|
||
All = "all"
|
||
# 系统消息
|
||
System = "system"
|
||
# 媒体消息
|
||
Media = "media"
|
||
|
||
|
||
class MessageClearBefore(BaseModel):
|
||
"""
|
||
通知中心按范围记录的清理时间。
|
||
"""
|
||
|
||
# 全部消息清理时间
|
||
all: int = 0
|
||
# 系统消息清理时间
|
||
system: int = 0
|
||
# 媒体消息清理时间
|
||
media: int = 0
|
||
|
||
|
||
class MessageResponse(BaseModel):
|
||
"""
|
||
消息发送响应,包含消息ID等信息用于后续编辑
|
||
"""
|
||
|
||
# 消息ID
|
||
message_id: Optional[Union[str, int]] = None
|
||
# 聊天ID
|
||
chat_id: Optional[Union[str, int]] = None
|
||
# 消息渠道
|
||
channel: Optional[NotificationChannel] = None
|
||
# 消息来源
|
||
source: Optional[str] = None
|
||
# 渠道自定义上下文(如飞书流式卡片 card_id/element_id/sequence)
|
||
metadata: Optional[Dict[str, JsonData]] = None
|
||
# 是否发送成功
|
||
success: bool = False
|
||
|
||
|
||
class MessageHistoryItem(BaseModel):
|
||
"""
|
||
通知历史记录。
|
||
"""
|
||
|
||
# 消息ID
|
||
id: Optional[int] = None
|
||
# 消息渠道
|
||
channel: Optional[str] = None
|
||
# 消息来源
|
||
source: Optional[str] = None
|
||
# 消息类型
|
||
mtype: Optional[str] = None
|
||
# 标题
|
||
title: Optional[str] = None
|
||
# 文本内容
|
||
text: Optional[str] = None
|
||
# 图片
|
||
image: Optional[str] = None
|
||
# 链接
|
||
link: Optional[str] = None
|
||
# 用户ID
|
||
userid: Optional[str] = None
|
||
# 登记时间
|
||
reg_time: Optional[str] = None
|
||
# 消息方向:0-接收消息,1-发送消息
|
||
action: Optional[int] = None
|
||
# 附件json
|
||
note: Optional[JsonData] = None
|
||
|
||
|
||
class WebMessageItem(MessageHistoryItem):
|
||
"""Web 消息历史记录。"""
|
||
|
||
|
||
class MessageClearData(BaseModel):
|
||
"""消息中心各范围的清理时间。"""
|
||
|
||
clear_before: MessageClearBefore = Field(description="各范围清理时间")
|
||
|
||
|
||
class IncomingMessage(BaseModel):
|
||
"""
|
||
外来消息
|
||
"""
|
||
|
||
class MessageImage(BaseModel):
|
||
"""
|
||
外来消息图片
|
||
"""
|
||
|
||
ref: str
|
||
name: Optional[str] = None
|
||
mime_type: Optional[str] = None
|
||
size: Optional[int] = None
|
||
|
||
@classmethod
|
||
def from_value(cls, value: Any) -> Optional["IncomingMessage.MessageImage"]:
|
||
if value is None:
|
||
return None
|
||
if isinstance(value, cls):
|
||
return value
|
||
if isinstance(value, str):
|
||
return cls(ref=value)
|
||
if isinstance(value, dict):
|
||
ref = (
|
||
value.get("ref")
|
||
or value.get("url")
|
||
or value.get("image_url")
|
||
or value.get("file_url")
|
||
)
|
||
if not ref:
|
||
return None
|
||
size = value.get("size")
|
||
try:
|
||
size = int(size) if size is not None else None
|
||
except (TypeError, ValueError):
|
||
size = None
|
||
return cls(
|
||
ref=ref,
|
||
name=value.get("name") or value.get("filename"),
|
||
mime_type=value.get("mime_type") or value.get("content_type"),
|
||
size=size,
|
||
)
|
||
return None
|
||
|
||
@classmethod
|
||
def normalize_list(
|
||
cls, values: Optional[Any]
|
||
) -> Optional[List["IncomingMessage.MessageImage"]]:
|
||
if not values:
|
||
return None
|
||
if not isinstance(values, list):
|
||
values = [values]
|
||
normalized = []
|
||
for value in values:
|
||
item = cls.from_value(value)
|
||
if item:
|
||
normalized.append(item)
|
||
return normalized or None
|
||
|
||
class MessageAttachment(BaseModel):
|
||
"""
|
||
外来消息附件(非图片/非语音)
|
||
"""
|
||
|
||
ref: str
|
||
name: Optional[str] = None
|
||
mime_type: Optional[str] = None
|
||
size: Optional[int] = None
|
||
|
||
# 用户ID
|
||
userid: Optional[Union[str, int]] = None
|
||
# 用户名称
|
||
username: Optional[Union[str, int]] = None
|
||
# 渠道适配器依据稳定用户 ID、管理员名单及渠道主用户 ID 生成的授权事实
|
||
is_channel_admin: Optional[bool] = None
|
||
# 消息渠道
|
||
channel: Optional[NotificationChannel] = None
|
||
# 来源(渠道名称)
|
||
source: Optional[str] = None
|
||
# 消息体
|
||
text: Optional[str] = None
|
||
# 时间
|
||
date: Optional[str] = None
|
||
# 消息方向
|
||
action: Optional[int] = 0
|
||
# 是否为回调消息
|
||
is_callback: Optional[bool] = False
|
||
# 回调数据
|
||
callback_data: Optional[str] = None
|
||
# 消息ID(用于回调时定位原消息)
|
||
message_id: Optional[Union[str, int]] = None
|
||
# 聊天ID(用于回调时定位聊天)
|
||
chat_id: Optional[str] = None
|
||
# 回复目标消息ID(用于 ForceReply 等回复场景)
|
||
reply_to_message_id: Optional[Union[str, int]] = None
|
||
# 完整的回调查询信息(原始数据)
|
||
callback_query: Optional[Dict] = None
|
||
# 图片列表(图片URL或file_id)
|
||
images: Optional[List[MessageImage]] = None
|
||
# 语音/音频引用列表
|
||
audio_refs: Optional[List[str]] = None
|
||
# 文件附件列表
|
||
files: Optional[List[MessageAttachment]] = None
|
||
# 结构化按钮回调数据(优先于 CALLBACK: 文本前缀)
|
||
callback_data: Optional[str] = None
|
||
|
||
@field_validator("images", mode="before")
|
||
@classmethod
|
||
def _normalize_images(
|
||
cls, value: Any
|
||
) -> Optional[List["IncomingMessage.MessageImage"]]:
|
||
return cls.MessageImage.normalize_list(value)
|
||
|
||
def to_dict(self):
|
||
"""
|
||
转换为字典
|
||
"""
|
||
items = self.model_dump()
|
||
for k, v in items.items():
|
||
if isinstance(v, NotificationChannel):
|
||
items[k] = v.value
|
||
return items
|
||
|
||
|
||
class Message(BaseModel):
|
||
"""
|
||
消息
|
||
"""
|
||
|
||
# 消息渠道
|
||
channel: Optional[NotificationChannel] = None
|
||
# 消息来源
|
||
source: Optional[str] = None
|
||
# 消息类型
|
||
mtype: Optional[MessageType] = None
|
||
# 内容类型
|
||
ctype: Optional[ContentType] = None
|
||
# 标题
|
||
title: Optional[str] = None
|
||
# 文本内容
|
||
text: Optional[str] = None
|
||
# 图片
|
||
image: Optional[str] = None
|
||
# 语音文件路径
|
||
voice_path: Optional[str] = None
|
||
# 本地文件路径
|
||
file_path: Optional[str] = None
|
||
# 发送时展示的文件名
|
||
file_name: Optional[str] = None
|
||
# 语音消息附带说明文字
|
||
voice_caption: Optional[str] = None
|
||
# 链接
|
||
link: Optional[str] = None
|
||
# 用户ID
|
||
userid: Optional[Union[str, int]] = None
|
||
# 用户名称
|
||
username: Optional[Union[str, int]] = None
|
||
# 时间
|
||
date: Optional[str] = None
|
||
# 消息方向
|
||
action: Optional[int] = 1
|
||
# 消息目标用户ID字典,未指定用户ID时使用
|
||
targets: Optional[dict] = None
|
||
# 按钮列表,格式:[[{"text": "按钮文本", "callback_data": "回调数据", "url": "链接"}]]
|
||
buttons: Optional[List[List[dict]]] = None
|
||
# Telegram ForceReply 回复标记
|
||
force_reply: bool = False
|
||
# 原消息ID,用于编辑消息
|
||
original_message_id: Optional[Union[str, int]] = None
|
||
# 原消息的聊天ID,用于编辑消息
|
||
original_chat_id: Optional[str] = None
|
||
# 是否必须按用户身份投递到私聊,禁止回退原会话或最近会话映射
|
||
private_delivery: bool = False
|
||
# 是否禁用链接预览(仅Telegram支持)
|
||
disable_web_page_preview: Optional[bool] = None
|
||
# 消息文本格式;Telegram 支持 MarkdownV2、HTML、plain,飞书直发支持 plain
|
||
parse_mode: Optional[str] = None
|
||
# Telegram Rich Message 完整 Markdown 正文;其他渠道可使用 text 作为回退
|
||
rich_message: Optional[str] = None
|
||
# 是否写入消息历史
|
||
save_history: bool = True
|
||
|
||
def to_dict(self):
|
||
"""
|
||
转换为字典
|
||
"""
|
||
items = self.model_dump()
|
||
for k, v in items.items():
|
||
if isinstance(v, NotificationChannel) or isinstance(v, MessageType):
|
||
items[k] = v.value
|
||
return items
|
||
|
||
|
||
class NotificationSwitch(BaseModel):
|
||
"""
|
||
消息开关
|
||
"""
|
||
|
||
# 消息类型
|
||
mtype: Optional[str] = None
|
||
# 微信开关
|
||
wechat: Optional[bool] = False
|
||
# 飞书开关
|
||
feishu: Optional[bool] = False
|
||
# TG开关
|
||
telegram: Optional[bool] = False
|
||
# Slack开关
|
||
slack: Optional[bool] = False
|
||
# SynologyChat开关
|
||
synologychat: Optional[bool] = False
|
||
# VoceChat开关
|
||
vocechat: Optional[bool] = False
|
||
# WebPush开关
|
||
webpush: Optional[bool] = False
|
||
# QQ开关
|
||
qq: Optional[bool] = False
|
||
|
||
|
||
class Subscription(BaseModel):
|
||
"""
|
||
客户端消息订阅
|
||
"""
|
||
|
||
endpoint: Optional[str] = None
|
||
keys: Optional[dict[str, str]] = Field(default_factory=dict)
|
||
|
||
|
||
class SubscriptionMessage(BaseModel):
|
||
"""
|
||
客户端订阅消息体
|
||
"""
|
||
|
||
title: Optional[str] = None
|
||
body: Optional[str] = None
|
||
icon: Optional[str] = None
|
||
url: Optional[str] = None
|
||
data: Optional[dict[str, JsonData]] = Field(default_factory=dict)
|
||
|
||
|
||
class AgentWebChatRequest(BaseModel):
|
||
"""
|
||
Web 智能助手对话请求。
|
||
"""
|
||
|
||
class AgentWebChatFile(BaseModel):
|
||
"""
|
||
Web 智能助手输入附件。
|
||
"""
|
||
|
||
ref: str = Field(..., min_length=1)
|
||
name: Optional[str] = Field(None)
|
||
mime_type: Optional[str] = Field(None)
|
||
size: Optional[int] = Field(None)
|
||
local_path: Optional[str] = Field(None)
|
||
status: Optional[str] = Field(None)
|
||
|
||
# 用户本轮输入
|
||
text: str = Field(default="")
|
||
# 展示历史中记录的用户可读文本;为空时使用 text
|
||
display_text: Optional[str] = Field(None)
|
||
# 前端会话标识,相同标识复用同一段 Agent 记忆
|
||
session_id: Optional[str] = Field(None)
|
||
# 图片 URL 或 data URL 列表
|
||
images: Optional[List[str]] = Field(default_factory=list)
|
||
# 语音/音频引用列表
|
||
audio_refs: Optional[List[str]] = Field(default_factory=list)
|
||
# 文件附件列表
|
||
files: Optional[List[AgentWebChatFile]] = Field(default_factory=list)
|
||
# 用户通过按钮选择时的完整选择快照
|
||
choice_selection: Optional[Dict[str, JsonData]] = Field(default=None)
|
||
# WebAgent 按钮回调关联的原消息 ID,用于传统交互原地编辑卡片
|
||
original_message_id: Optional[Union[str, int]] = Field(default=None)
|
||
# WebAgent 按钮回调关联的原聊天 ID,用于传统交互原地编辑卡片
|
||
original_chat_id: Optional[Union[str, int]] = Field(default=None)
|
||
# 是否在展示历史中记录本轮用户消息
|
||
echo_user: bool = Field(default=True)
|
||
|
||
|
||
class AgentWebChoiceRequest(BaseModel):
|
||
"""
|
||
Web 智能助手按钮选择请求。
|
||
"""
|
||
|
||
# 前端会话标识,用于保持与原对话窗口的关联
|
||
session_id: Optional[str] = Field(None)
|
||
# Agent 工具生成的按钮回调数据
|
||
callback_data: str = Field(..., min_length=1)
|
||
# WebAgent 原助手消息 ID,用于传统按钮回调原地编辑
|
||
original_message_id: Optional[Union[str, int]] = Field(default=None)
|
||
# WebAgent 原聊天 ID,用于传统按钮回调原地编辑
|
||
original_chat_id: Optional[Union[str, int]] = Field(default=None)
|