Files
MoviePilot/app/schemas/message.py
T

388 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)