From b349aa26939cbc29025ec7b2b8f7c2e8e70c447f Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 29 Mar 2026 09:56:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(agent):=20=E6=94=AF=E6=8C=81=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E6=B6=88=E6=81=AF=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/agent/__init__.py | 95 ++-- app/chain/message.py | 909 +++++++++++++++++++++---------- app/modules/discord/__init__.py | 22 +- app/modules/slack/__init__.py | 22 +- app/modules/telegram/__init__.py | 48 +- app/schemas/message.py | 2 + 6 files changed, 745 insertions(+), 353 deletions(-) diff --git a/app/agent/__init__.py b/app/agent/__init__.py index 99cd03ec..780f75e8 100644 --- a/app/agent/__init__.py +++ b/app/agent/__init__.py @@ -10,7 +10,7 @@ from langchain.agents.middleware import ( SummarizationMiddleware, LLMToolSelectorMiddleware, ) -from langchain_core.messages import ( +from langchain_core.messages import ( # noqa: F401 HumanMessage, BaseMessage, ) @@ -42,12 +42,12 @@ class MoviePilotAgent: """ def __init__( - self, - session_id: str, - user_id: str = None, - channel: str = None, - source: str = None, - username: str = None, + self, + session_id: str, + user_id: str = None, + channel: str = None, + source: str = None, + username: str = None, ): self.session_id = session_id self.user_id = user_id @@ -92,10 +92,10 @@ class MoviePilotAgent: if block.get("thought"): continue if block.get("type") in ( - "thinking", - "reasoning_content", - "reasoning", - "thought", + "thinking", + "reasoning_content", + "reasoning", + "thought", ): continue if block.get("type") == "text": @@ -174,20 +174,32 @@ class MoviePilotAgent: logger.error(f"创建 Agent 失败: {e}") raise e - async def process(self, message: str) -> str: + async def process(self, message: str, images: List[str] = None) -> str: """ 处理用户消息,流式推理并返回 Agent 回复 """ try: - logger.info(f"Agent推理: session_id={self.session_id}, input={message}") + logger.info( + f"Agent推理: session_id={self.session_id}, input={message}, images={len(images) if images else 0}" + ) # 获取历史消息 messages = memory_manager.get_agent_messages( session_id=self.session_id, user_id=self.user_id ) - # 增加用户消息 - messages.append(HumanMessage(content=message)) + # 构建用户消息内容 + if images: + from langchain_core.messages import HumanMessage + + content = [] + if message: + content.append({"type": "text", "text": message}) + for img in images: + content.append({"type": "image_url", "image_url": {"url": img}}) + messages.append(HumanMessage(content=content)) + else: + messages.append(HumanMessage(content=message)) # 执行推理 await self._execute_agent(messages) @@ -199,7 +211,7 @@ class MoviePilotAgent: return error_message async def _stream_agent_tokens( - self, agent, messages: dict, config: dict, on_token: Callable[[str], None] + self, agent, messages: dict, config: dict, on_token: Callable[[str], None] ): """ 流式运行智能体,过滤工具调用token和思考内容,将模型生成的内容通过回调输出。 @@ -212,18 +224,18 @@ class MoviePilotAgent: buffer = "" async for chunk in agent.astream( - messages, - stream_mode="messages", - config=config, - subgraphs=False, - version="v2", + messages, + stream_mode="messages", + config=config, + subgraphs=False, + version="v2", ): if chunk["type"] == "messages": token, metadata = chunk["data"] if ( - token - and hasattr(token, "tool_call_chunks") - and not token.tool_call_chunks + token + and hasattr(token, "tool_call_chunks") + and not token.tool_call_chunks ): # 跳过模型思考/推理内容(如 DeepSeek R1 的 reasoning_content) additional = getattr(token, "additional_kwargs", None) @@ -241,7 +253,7 @@ class MoviePilotAgent: if start_idx > 0: on_token(buffer[:start_idx]) in_think_tag = True - buffer = buffer[start_idx + 7:] + buffer = buffer[start_idx + 7 :] else: # 检查是否以 的前缀结尾 partial_match = False @@ -259,7 +271,7 @@ class MoviePilotAgent: end_idx = buffer.find("") if end_idx != -1: in_think_tag = False - buffer = buffer[end_idx + 8:] + buffer = buffer[end_idx + 8 :] else: # 检查是否以 的前缀结尾 partial_match = False @@ -421,6 +433,7 @@ class _MessageTask: session_id: str user_id: str message: str + images: Optional[List[str]] = None channel: Optional[str] = None source: Optional[str] = None username: Optional[str] = None @@ -467,13 +480,14 @@ class AgentManager: self.active_agents.clear() async def process_message( - self, - session_id: str, - user_id: str, - message: str, - channel: str = None, - source: str = None, - username: str = None, + self, + session_id: str, + user_id: str, + message: str, + images: List[str] = None, + channel: str = None, + source: str = None, + username: str = None, ) -> str: """ 处理用户消息:将消息放入会话队列,按顺序依次处理。 @@ -483,6 +497,7 @@ class AgentManager: session_id=session_id, user_id=user_id, message=message, + images=images, channel=channel, source=source, username=username, @@ -497,8 +512,8 @@ class AgentManager: # 如果队列中已有等待的消息,通知用户消息已排队 if queue_size > 0 or ( - session_id in self._session_workers - and not self._session_workers[session_id].done() + session_id in self._session_workers + and not self._session_workers[session_id].done() ): logger.info( f"会话 {session_id} 有任务正在处理,消息已排队等待 " @@ -510,8 +525,8 @@ class AgentManager: # 确保该会话有一个worker在运行 if ( - session_id not in self._session_workers - or self._session_workers[session_id].done() + session_id not in self._session_workers + or self._session_workers[session_id].done() ): self._session_workers[session_id] = asyncio.create_task( self._session_worker(session_id) @@ -552,8 +567,8 @@ class AgentManager: self._session_workers.pop(session_id, None) # noqa # 如果队列为空,清理队列 if ( - session_id in self._session_queues - and self._session_queues[session_id].empty() + session_id in self._session_queues + and self._session_queues[session_id].empty() ): self._session_queues.pop(session_id, None) @@ -584,7 +599,7 @@ class AgentManager: if task.username: agent.username = task.username - return await agent.process(task.message) + return await agent.process(task.message, images=task.images) async def clear_session(self, session_id: str, user_id: str): """ diff --git a/app/chain/message.py b/app/chain/message.py index c8c8d4d7..fa2606d4 100644 --- a/app/chain/message.py +++ b/app/chain/message.py @@ -20,6 +20,7 @@ from app.schemas import Notification, NotExistMediaInfo, CommingMessage from app.schemas.message import ChannelCapabilityManager from app.schemas.types import EventType, MessageChannel, MediaType from app.utils.string import StringUtils +from app.utils.http import RequestUtils # 当前页面 _current_page: int = 0 @@ -33,6 +34,7 @@ class MessageChain(ChainBase): """ 外来消息处理链 """ + # 缓存的用户数据 {userid: {type: str, items: list}} _cache_file = "__user_messages__" # 每页数据量 @@ -44,31 +46,35 @@ class MessageChain(ChainBase): @staticmethod def __get_noexits_info( - _meta: MetaBase, - _mediainfo: MediaInfo) -> Dict[Union[int, str], Dict[int, NotExistMediaInfo]]: + _meta: MetaBase, _mediainfo: MediaInfo + ) -> Dict[Union[int, str], Dict[int, NotExistMediaInfo]]: """ 获取缺失的媒体信息 """ if _mediainfo.type == MediaType.TV: if not _mediainfo.seasons: # 补充媒体信息 - _mediainfo = MediaChain().recognize_media(mtype=_mediainfo.type, - tmdbid=_mediainfo.tmdb_id, - doubanid=_mediainfo.douban_id, - cache=False) + _mediainfo = MediaChain().recognize_media( + mtype=_mediainfo.type, + tmdbid=_mediainfo.tmdb_id, + doubanid=_mediainfo.douban_id, + cache=False, + ) if not _mediainfo: - logger.warn(f"{_mediainfo.tmdb_id or _mediainfo.douban_id} 媒体信息识别失败!") + logger.warn( + f"{_mediainfo.tmdb_id or _mediainfo.douban_id} 媒体信息识别失败!" + ) return {} if not _mediainfo.seasons: - logger.warn(f"媒体信息中没有季集信息," - f"标题:{_mediainfo.title}," - f"tmdbid:{_mediainfo.tmdb_id},doubanid:{_mediainfo.douban_id}") + logger.warn( + f"媒体信息中没有季集信息," + f"标题:{_mediainfo.title}," + f"tmdbid:{_mediainfo.tmdb_id},doubanid:{_mediainfo.douban_id}" + ) return {} # KEY _mediakey = _mediainfo.tmdb_id or _mediainfo.douban_id - _no_exists = { - _mediakey: {} - } + _no_exists = {_mediakey: {}} if _meta.begin_season: # 指定季 episodes = _mediainfo.seasons.get(_meta.begin_season) @@ -78,7 +84,7 @@ class MessageChain(ChainBase): season=_meta.begin_season, episodes=[], total_episode=len(episodes), - start_episode=episodes[0] + start_episode=episodes[0], ) else: # 所有季 @@ -89,7 +95,7 @@ class MessageChain(ChainBase): season=sea, episodes=[], total_episode=len(eps), - start_episode=eps[0] + start_episode=eps[0], ) else: _no_exists = {} @@ -113,83 +119,118 @@ class MessageChain(ChainBase): # 用户ID userid = info.userid # 用户名(当渠道未提供公开用户名时,回退为 userid 的字符串,避免后续类型校验异常) - username = str(info.username) if info.username not in (None, "") else str(userid) - if userid is None or userid == '': - logger.debug(f'未识别到用户ID:{body}{form}{args}') + username = ( + str(info.username) if info.username not in (None, "") else str(userid) + ) + if userid is None or userid == "": + logger.debug(f"未识别到用户ID:{body}{form}{args}") return # 消息内容 text = str(info.text).strip() if info.text else None if not text: - logger.debug(f'未识别到消息内容::{body}{form}{args}') + logger.debug(f"未识别到消息内容::{body}{form}{args}") return # 获取原消息ID信息 original_message_id = info.message_id original_chat_id = info.chat_id + images = info.images # 处理消息 - self.handle_message(channel=channel, source=source, userid=userid, username=username, text=text, - original_message_id=original_message_id, original_chat_id=original_chat_id) + self.handle_message( + channel=channel, + source=source, + userid=userid, + username=username, + text=text, + original_message_id=original_message_id, + original_chat_id=original_chat_id, + images=images, + ) - def handle_message(self, channel: MessageChannel, source: str, - userid: Union[str, int], username: str, text: str, - original_message_id: Optional[Union[str, int]] = None, - original_chat_id: Optional[str] = None) -> None: + def handle_message( + self, + channel: MessageChannel, + source: str, + userid: Union[str, int], + username: str, + text: str, + original_message_id: Optional[Union[str, int]] = None, + original_chat_id: Optional[str] = None, + images: Optional[List[str]] = None, + ) -> None: """ 识别消息内容,执行操作 """ # 申明全局变量 global _current_page, _current_meta, _current_media # 处理消息 - logger.info(f'收到用户消息内容,用户:{userid},内容:{text}') + logger.info(f"收到用户消息内容,用户:{userid},内容:{text}") # 加载缓存 user_cache: Dict[str, dict] = self.load_cache(self._cache_file) or {} try: # 保存消息 - if not text.startswith('CALLBACK:'): + if not text.startswith("CALLBACK:"): self.messagehelper.put( CommingMessage( userid=userid, username=username, channel=channel, source=source, - text=text - ), role="user") + text=text, + ), + role="user", + ) self.messageoper.add( channel=channel, source=source, userid=username or userid, text=text, - action=0 + action=0, ) # 处理消息 - if text.startswith('CALLBACK:'): + if text.startswith("CALLBACK:"): # 处理按钮回调(适配支持回调的渠),优先级最高 if ChannelCapabilityManager.supports_callbacks(channel): - self._handle_callback(text=text, channel=channel, source=source, - userid=userid, username=username, - original_message_id=original_message_id, original_chat_id=original_chat_id) + self._handle_callback( + text=text, + channel=channel, + source=source, + userid=userid, + username=username, + original_message_id=original_message_id, + original_chat_id=original_chat_id, + ) else: - logger.warning(f"渠道 {channel.value} 不支持回调,但收到了回调消息:{text}") - elif text.startswith('/') and not text.lower().startswith('/ai'): + logger.warning( + f"渠道 {channel.value} 不支持回调,但收到了回调消息:{text}" + ) + elif text.startswith("/") and not text.lower().startswith("/ai"): # 执行特定命令命令(但不是/ai) self.eventmanager.send_event( EventType.CommandExcute, - { - "cmd": text, - "user": userid, - "channel": channel, - "source": source - } + {"cmd": text, "user": userid, "channel": channel, "source": source}, ) - elif text.lower().startswith('/ai'): + elif text.lower().startswith("/ai"): # 用户指定AI智能体消息响应 - self._handle_ai_message(text=text, channel=channel, source=source, - userid=userid, username=username) + self._handle_ai_message( + text=text, + channel=channel, + source=source, + userid=userid, + username=username, + images=images, + ) elif settings.AI_AGENT_ENABLE and settings.AI_AGENT_GLOBAL: # 普通消息,全局智能体响应 - self._handle_ai_message(text=text, channel=channel, source=source, - userid=userid, username=username) + self._handle_ai_message( + text=text, + channel=channel, + source=source, + userid=userid, + username=username, + images=images, + ) else: # 非智能体普通消息响应 if text.isdigit(): @@ -198,22 +239,37 @@ class MessageChain(ChainBase): cache_data: dict = user_cache.get(userid) if not cache_data: # 发送消息 - self.post_message(Notification(channel=channel, source=source, title="输入有误!", userid=userid)) + self.post_message( + Notification( + channel=channel, + source=source, + title="输入有误!", + userid=userid, + ) + ) return cache_data = cache_data.copy() # 选择项目 - if not cache_data.get('items') \ - or len(cache_data.get('items')) < int(text): + if not cache_data.get("items") or len( + cache_data.get("items") + ) < int(text): # 发送消息 - self.post_message(Notification(channel=channel, source=source, title="输入有误!", userid=userid)) + self.post_message( + Notification( + channel=channel, + source=source, + title="输入有误!", + userid=userid, + ) + ) return try: # 选择的序号 _choice = int(text) + _current_page * self._page_size - 1 # 缓存类型 - cache_type: str = cache_data.get('type') + cache_type: str = cache_data.get("type") # 缓存列表 - cache_list: list = cache_data.get('items').copy() + cache_list: list = cache_data.get("items").copy() # 选择 try: if cache_type in ["Search", "ReSearch"]: @@ -221,20 +277,28 @@ class MessageChain(ChainBase): mediainfo: MediaInfo = cache_list[_choice] _current_media = mediainfo # 查询缺失的媒体信息 - exist_flag, no_exists = DownloadChain().get_no_exists_info(meta=_current_meta, - mediainfo=_current_media) + exist_flag, no_exists = ( + DownloadChain().get_no_exists_info( + meta=_current_meta, mediainfo=_current_media + ) + ) if exist_flag and cache_type == "Search": # 媒体库中已存在 self.post_message( - Notification(channel=channel, - source=source, - title=f"【{_current_media.title_year}" - f"{_current_meta.sea} 媒体库中已存在,如需重新下载请发送:搜索 名称 或 下载 名称】", - userid=userid)) + Notification( + channel=channel, + source=source, + title=f"【{_current_media.title_year}" + f"{_current_meta.sea} 媒体库中已存在,如需重新下载请发送:搜索 名称 或 下载 名称】", + userid=userid, + ) + ) return elif exist_flag: # 没有缺失,但要全量重新搜索和下载 - no_exists = self.__get_noexits_info(_current_meta, _current_media) + no_exists = self.__get_noexits_info( + _current_meta, _current_media + ) # 发送缺失的媒体信息 messages = [] if no_exists and cache_type == "Search": @@ -242,36 +306,54 @@ class MessageChain(ChainBase): mediakey = mediainfo.tmdb_id or mediainfo.douban_id messages = [ f"第 {sea} 季缺失 {StringUtils.str_series(no_exist.episodes) if no_exist.episodes else no_exist.total_episode} 集" - for sea, no_exist in no_exists.get(mediakey).items()] + for sea, no_exist in no_exists.get( + mediakey + ).items() + ] elif no_exists: # 发送总集数的消息 mediakey = mediainfo.tmdb_id or mediainfo.douban_id messages = [ f"第 {sea} 季总 {no_exist.total_episode} 集" - for sea, no_exist in no_exists.get(mediakey).items()] + for sea, no_exist in no_exists.get( + mediakey + ).items() + ] if messages: - self.post_message(Notification(channel=channel, - source=source, - title=f"{mediainfo.title_year}:\n" + "\n".join(messages), - userid=userid)) + self.post_message( + Notification( + channel=channel, + source=source, + title=f"{mediainfo.title_year}:\n" + + "\n".join(messages), + userid=userid, + ) + ) # 搜索种子,过滤掉不需要的剧集,以便选择 logger.info(f"开始搜索 {mediainfo.title_year} ...") self.post_message( - Notification(channel=channel, - source=source, - title=f"开始搜索 {mediainfo.type.value} {mediainfo.title_year} ...", - userid=userid)) - # 开始搜索 - contexts = SearchChain().process(mediainfo=mediainfo, - no_exists=no_exists) - if not contexts: - # 没有数据 - self.post_message(Notification( + Notification( channel=channel, source=source, - title=f"{mediainfo.title}" - f"{_current_meta.sea} 未搜索到需要的资源!", - userid=userid)) + title=f"开始搜索 {mediainfo.type.value} {mediainfo.title_year} ...", + userid=userid, + ) + ) + # 开始搜索 + contexts = SearchChain().process( + mediainfo=mediainfo, no_exists=no_exists + ) + if not contexts: + # 没有数据 + self.post_message( + Notification( + channel=channel, + source=source, + title=f"{mediainfo.title}" + f"{_current_meta.sea} 未搜索到需要的资源!", + userid=userid, + ) + ) return # 搜索结果排序 contexts = TorrentHelper().sort_torrents(contexts) @@ -279,43 +361,60 @@ class MessageChain(ChainBase): # 判断是否设置自动下载 auto_download_user = settings.AUTO_DOWNLOAD_USER # 匹配到自动下载用户 - if auto_download_user \ - and (auto_download_user == "all" - or any(userid == user for user in auto_download_user.split(","))): - logger.info(f"用户 {userid} 在自动下载用户中,开始自动择优下载 ...") + if auto_download_user and ( + auto_download_user == "all" + or any( + userid == user + for user in auto_download_user.split(",") + ) + ): + logger.info( + f"用户 {userid} 在自动下载用户中,开始自动择优下载 ..." + ) # 自动选择下载 - self.__auto_download(channel=channel, - source=source, - cache_list=contexts, - userid=userid, - username=username, - no_exists=no_exists) + self.__auto_download( + channel=channel, + source=source, + cache_list=contexts, + userid=userid, + username=username, + no_exists=no_exists, + ) else: # 更新缓存 user_cache[userid] = { "type": "Torrent", - "items": contexts + "items": contexts, } _current_page = 0 # 保存缓存 self.save_cache(user_cache, self._cache_file) # 删除原消息 - if (original_message_id and original_chat_id and - ChannelCapabilityManager.supports_deletion(channel)): + if ( + original_message_id + and original_chat_id + and ChannelCapabilityManager.supports_deletion( + channel + ) + ): self.delete_message( channel=channel, source=source, message_id=original_message_id, - chat_id=original_chat_id + chat_id=original_chat_id, ) # 发送种子数据 - logger.info(f"搜索到 {len(contexts)} 条数据,开始发送选择消息 ...") - self.__post_torrents_message(channel=channel, - source=source, - title=mediainfo.title, - items=contexts[:self._page_size], - userid=userid, - total=len(contexts)) + logger.info( + f"搜索到 {len(contexts)} 条数据,开始发送选择消息 ..." + ) + self.__post_torrents_message( + channel=channel, + source=source, + title=mediainfo.title, + items=contexts[: self._page_size], + userid=userid, + total=len(contexts), + ) finally: contexts.clear() del contexts @@ -326,46 +425,64 @@ class MessageChain(ChainBase): best_version = False # 查询缺失的媒体信息 if cache_type == "Subscribe": - exist_flag, _ = DownloadChain().get_no_exists_info(meta=_current_meta, - mediainfo=mediainfo) + exist_flag, _ = DownloadChain().get_no_exists_info( + meta=_current_meta, mediainfo=mediainfo + ) if exist_flag: - self.post_message(Notification( - channel=channel, - source=source, - title=f"【{mediainfo.title_year}" - f"{_current_meta.sea} 媒体库中已存在,如需洗版请发送:洗版 XXX】", - userid=userid)) + self.post_message( + Notification( + channel=channel, + source=source, + title=f"【{mediainfo.title_year}" + f"{_current_meta.sea} 媒体库中已存在,如需洗版请发送:洗版 XXX】", + userid=userid, + ) + ) return else: best_version = True # 转换用户名 - mp_name = UserOper().get_name( - **{f"{channel.name.lower()}_userid": userid}) if channel else None + mp_name = ( + UserOper().get_name( + **{f"{channel.name.lower()}_userid": userid} + ) + if channel + else None + ) # 添加订阅,状态为N - SubscribeChain().add(title=mediainfo.title, - year=mediainfo.year, - mtype=mediainfo.type, - tmdbid=mediainfo.tmdb_id, - season=_current_meta.begin_season, - channel=channel, - source=source, - userid=userid, - username=mp_name or username, - best_version=best_version) + SubscribeChain().add( + title=mediainfo.title, + year=mediainfo.year, + mtype=mediainfo.type, + tmdbid=mediainfo.tmdb_id, + season=_current_meta.begin_season, + channel=channel, + source=source, + userid=userid, + username=mp_name or username, + best_version=best_version, + ) elif cache_type == "Torrent": if int(text) == 0: # 自动选择下载,强制下载模式 - self.__auto_download(channel=channel, - source=source, - cache_list=cache_list, - userid=userid, - username=username) + self.__auto_download( + channel=channel, + source=source, + cache_list=cache_list, + userid=userid, + username=username, + ) else: # 下载种子 context: Context = cache_list[_choice] # 下载 - DownloadChain().download_single(context, channel=channel, source=source, - userid=userid, username=username) + DownloadChain().download_single( + context, + channel=channel, + source=source, + userid=userid, + username=username, + ) finally: cache_list.clear() del cache_list @@ -377,21 +494,33 @@ class MessageChain(ChainBase): cache_data: dict = user_cache.get(userid) if not cache_data: # 没有缓存 - self.post_message(Notification( - channel=channel, source=source, title="输入有误!", userid=userid)) + self.post_message( + Notification( + channel=channel, + source=source, + title="输入有误!", + userid=userid, + ) + ) return cache_data = cache_data.copy() try: if _current_page == 0: # 第一页 - self.post_message(Notification( - channel=channel, source=source, title="已经是第一页了!", userid=userid)) + self.post_message( + Notification( + channel=channel, + source=source, + title="已经是第一页了!", + userid=userid, + ) + ) return # 减一页 _current_page -= 1 - cache_type: str = cache_data.get('type') + cache_type: str = cache_data.get("type") # 产生副本,避免修改原值 - cache_list: list = cache_data.get('items').copy() + cache_list: list = cache_data.get("items").copy() try: if _current_page == 0: start = 0 @@ -401,24 +530,28 @@ class MessageChain(ChainBase): end = start + self._page_size if cache_type == "Torrent": # 发送种子数据 - self.__post_torrents_message(channel=channel, - source=source, - title=_current_media.title, - items=cache_list[start:end], - userid=userid, - total=len(cache_list), - original_message_id=original_message_id, - original_chat_id=original_chat_id) + self.__post_torrents_message( + channel=channel, + source=source, + title=_current_media.title, + items=cache_list[start:end], + userid=userid, + total=len(cache_list), + original_message_id=original_message_id, + original_chat_id=original_chat_id, + ) else: # 发送媒体数据 - self.__post_medias_message(channel=channel, - source=source, - title=_current_meta.name, - items=cache_list[start:end], - userid=userid, - total=len(cache_list), - original_message_id=original_message_id, - original_chat_id=original_chat_id) + self.__post_medias_message( + channel=channel, + source=source, + title=_current_meta.name, + items=cache_list[start:end], + userid=userid, + total=len(cache_list), + original_message_id=original_message_id, + original_chat_id=original_chat_id, + ) finally: cache_list.clear() del cache_list @@ -430,21 +563,36 @@ class MessageChain(ChainBase): cache_data: dict = user_cache.get(userid) if not cache_data: # 没有缓存 - self.post_message(Notification( - channel=channel, source=source, title="输入有误!", userid=userid)) + self.post_message( + Notification( + channel=channel, + source=source, + title="输入有误!", + userid=userid, + ) + ) return cache_data = cache_data.copy() try: - cache_type: str = cache_data.get('type') + cache_type: str = cache_data.get("type") # 产生副本,避免修改原值 - cache_list: list = cache_data.get('items').copy() + cache_list: list = cache_data.get("items").copy() total = len(cache_list) # 加一页 - cache_list = cache_list[(_current_page + 1) * self._page_size:(_current_page + 2) * self._page_size] + cache_list = cache_list[ + (_current_page + 1) * self._page_size : (_current_page + 2) + * self._page_size + ] if not cache_list: # 没有数据 - self.post_message(Notification( - channel=channel, source=source, title="已经是最后一页了!", userid=userid)) + self.post_message( + Notification( + channel=channel, + source=source, + title="已经是最后一页了!", + userid=userid, + ) + ) return else: try: @@ -452,24 +600,28 @@ class MessageChain(ChainBase): _current_page += 1 if cache_type == "Torrent": # 发送种子数据 - self.__post_torrents_message(channel=channel, - source=source, - title=_current_media.title, - items=cache_list, - userid=userid, - total=total, - original_message_id=original_message_id, - original_chat_id=original_chat_id) + self.__post_torrents_message( + channel=channel, + source=source, + title=_current_media.title, + items=cache_list, + userid=userid, + total=total, + original_message_id=original_message_id, + original_chat_id=original_chat_id, + ) else: # 发送媒体数据 - self.__post_medias_message(channel=channel, - source=source, - title=_current_meta.name, - items=cache_list, - userid=userid, - total=total, - original_message_id=original_message_id, - original_chat_id=original_chat_id) + self.__post_medias_message( + channel=channel, + source=source, + title=_current_meta.name, + items=cache_list, + userid=userid, + total=total, + original_message_id=original_message_id, + original_chat_id=original_chat_id, + ) finally: cache_list.clear() del cache_list @@ -508,33 +660,44 @@ class MessageChain(ChainBase): meta, medias = MediaChain().search(content) # 识别 if not meta.name: - self.post_message(Notification( - channel=channel, source=source, title="无法识别输入内容!", userid=userid)) + self.post_message( + Notification( + channel=channel, + source=source, + title="无法识别输入内容!", + userid=userid, + ) + ) return # 开始搜索 if not medias: - self.post_message(Notification( - channel=channel, source=source, title=f"{meta.name} 没有找到对应的媒体信息!", - userid=userid)) + self.post_message( + Notification( + channel=channel, + source=source, + title=f"{meta.name} 没有找到对应的媒体信息!", + userid=userid, + ) + ) return logger.info(f"搜索到 {len(medias)} 条相关媒体信息") try: # 记录当前状态 _current_meta = meta # 保存缓存 - user_cache[userid] = { - 'type': action, - 'items': medias - } + user_cache[userid] = {"type": action, "items": medias} self.save_cache(user_cache, self._cache_file) _current_page = 0 _current_media = None # 发送媒体列表 - self.__post_medias_message(channel=channel, - source=source, - title=meta.name, - items=medias[:self._page_size], - userid=userid, total=len(medias)) + self.__post_medias_message( + channel=channel, + source=source, + title=meta.name, + items=medias[: self._page_size], + userid=userid, + total=len(medias), + ) finally: medias.clear() del medias @@ -546,17 +709,23 @@ class MessageChain(ChainBase): "text": content, "userid": userid, "channel": channel, - "source": source - } + "source": source, + }, ) finally: user_cache.clear() del user_cache - def _handle_callback(self, text: str, channel: MessageChannel, source: str, - userid: Union[str, int], username: str, - original_message_id: Optional[Union[str, int]] = None, - original_chat_id: Optional[str] = None) -> None: + def _handle_callback( + self, + text: str, + channel: MessageChannel, + source: str, + userid: Union[str, int], + username: str, + original_message_id: Optional[Union[str, int]] = None, + original_chat_id: Optional[str] = None, + ) -> None: """ 处理按钮回调 """ @@ -568,7 +737,7 @@ class MessageChain(ChainBase): logger.info(f"处理按钮回调:{callback_data}") # 插件消息的事件回调 [PLUGIN]插件ID|内容 - if callback_data.startswith('[PLUGIN]'): + if callback_data.startswith("[PLUGIN]"): # 提取插件ID和内容 plugin_id, content = callback_data.split("|", 1) # 广播给插件处理 @@ -581,30 +750,44 @@ class MessageChain(ChainBase): "channel": channel, "source": source, "original_message_id": original_message_id, - "original_chat_id": original_chat_id - } + "original_chat_id": original_chat_id, + }, ) return # 解析系统回调数据 try: page_text = callback_data.split("_", 1)[1] - self.handle_message(channel=channel, source=source, userid=userid, username=username, - text=page_text, - original_message_id=original_message_id, original_chat_id=original_chat_id) - except IndexError: - logger.error(f"回调数据格式错误:{callback_data}") - self.post_message(Notification( + self.handle_message( channel=channel, source=source, userid=userid, username=username, - title="回调数据格式错误,请检查!" - )) + text=page_text, + original_message_id=original_message_id, + original_chat_id=original_chat_id, + ) + except IndexError: + logger.error(f"回调数据格式错误:{callback_data}") + self.post_message( + Notification( + channel=channel, + source=source, + userid=userid, + username=username, + title="回调数据格式错误,请检查!", + ) + ) - def __auto_download(self, channel: MessageChannel, source: str, cache_list: list[Context], - userid: Union[str, int], username: str, - no_exists: Optional[Dict[Union[int, str], Dict[int, NotExistMediaInfo]]] = None): + def __auto_download( + self, + channel: MessageChannel, + source: str, + cache_list: list[Context], + userid: Union[str, int], + username: str, + no_exists: Optional[Dict[Union[int, str], Dict[int, NotExistMediaInfo]]] = None, + ): """ 自动择优下载 """ @@ -612,52 +795,69 @@ class MessageChain(ChainBase): if no_exists is None: # 查询缺失的媒体信息 exist_flag, no_exists = downloadchain.get_no_exists_info( - meta=_current_meta, - mediainfo=_current_media + meta=_current_meta, mediainfo=_current_media ) if exist_flag: # 媒体库中已存在,查询全量 no_exists = self.__get_noexits_info(_current_meta, _current_media) # 批量下载 - downloads, lefts = downloadchain.batch_download(contexts=cache_list, - no_exists=no_exists, - channel=channel, - source=source, - userid=userid, - username=username) + downloads, lefts = downloadchain.batch_download( + contexts=cache_list, + no_exists=no_exists, + channel=channel, + source=source, + userid=userid, + username=username, + ) if downloads and not lefts: # 全部下载完成 - logger.info(f'{_current_media.title_year} 下载完成') + logger.info(f"{_current_media.title_year} 下载完成") else: # 未完成下载 - logger.info(f'{_current_media.title_year} 未下载未完整,添加订阅 ...') + logger.info(f"{_current_media.title_year} 未下载未完整,添加订阅 ...") if downloads and _current_media.type == MediaType.TV: # 获取已下载剧集 - downloaded = [download.meta_info.begin_episode for download in downloads - if download.meta_info.begin_episode] + downloaded = [ + download.meta_info.begin_episode + for download in downloads + if download.meta_info.begin_episode + ] note = downloaded else: note = None # 转换用户名 - mp_name = UserOper().get_name(**{f"{channel.name.lower()}_userid": userid}) if channel else None + mp_name = ( + UserOper().get_name(**{f"{channel.name.lower()}_userid": userid}) + if channel + else None + ) # 添加订阅,状态为R - SubscribeChain().add(title=_current_media.title, - year=_current_media.year, - mtype=_current_media.type, - tmdbid=_current_media.tmdb_id, - season=_current_meta.begin_season, - channel=channel, - source=source, - userid=userid, - username=mp_name or username, - state="R", - note=note) + SubscribeChain().add( + title=_current_media.title, + year=_current_media.year, + mtype=_current_media.type, + tmdbid=_current_media.tmdb_id, + season=_current_meta.begin_season, + channel=channel, + source=source, + userid=userid, + username=mp_name or username, + state="R", + note=note, + ) - def __post_medias_message(self, channel: MessageChannel, source: str, - title: str, items: list, userid: str, total: int, - original_message_id: Optional[Union[str, int]] = None, - original_chat_id: Optional[str] = None): + def __post_medias_message( + self, + channel: MessageChannel, + source: str, + title: str, + items: list, + userid: str, + total: int, + original_message_id: Optional[Union[str, int]] = None, + original_chat_id: Optional[str] = None, + ): """ 发送媒体列表消息 """ @@ -671,7 +871,9 @@ class MessageChain(ChainBase): else: title = f"【{title}】共找到{total}条相关信息,请选择操作" - buttons = self._create_media_buttons(channel=channel, items=items, total=total) + buttons = self._create_media_buttons( + channel=channel, items=items, total=total + ) else: # 不支持按钮的渠道,使用文本提示 if total > self._page_size: @@ -687,12 +889,14 @@ class MessageChain(ChainBase): userid=userid, buttons=buttons, original_message_id=original_message_id, - original_chat_id=original_chat_id + original_chat_id=original_chat_id, ) self.post_medias_message(notification, medias=items) - def _create_media_buttons(self, channel: MessageChannel, items: list, total: int) -> List[List[Dict]]: + def _create_media_buttons( + self, channel: MessageChannel, items: list, total: int + ) -> List[List[Dict]]: """ 创建媒体选择按钮 """ @@ -711,20 +915,18 @@ class MessageChain(ChainBase): # 每行一个按钮,使用完整文本 button_text = f"{i + 1}. {media.title_year}" if len(button_text) > max_text_length: - button_text = button_text[:max_text_length - 3] + "..." + button_text = button_text[: max_text_length - 3] + "..." - buttons.append([{ - "text": button_text, - "callback_data": f"select_{i + 1}" - }]) + buttons.append( + [{"text": button_text, "callback_data": f"select_{i + 1}"}] + ) else: # 多按钮一行的情况,使用简化文本 button_text = f"{i + 1}" - current_row.append({ - "text": button_text, - "callback_data": f"select_{i + 1}" - }) + current_row.append( + {"text": button_text, "callback_data": f"select_{i + 1}"} + ) # 如果当前行已满或者是最后一个按钮,添加到按钮列表 if len(current_row) == max_per_row or i == len(items) - 1: @@ -743,10 +945,17 @@ class MessageChain(ChainBase): return buttons - def __post_torrents_message(self, channel: MessageChannel, source: str, - title: str, items: list, userid: str, total: int, - original_message_id: Optional[Union[str, int]] = None, - original_chat_id: Optional[str] = None): + def __post_torrents_message( + self, + channel: MessageChannel, + source: str, + title: str, + items: list, + userid: str, + total: int, + original_message_id: Optional[Union[str, int]] = None, + original_chat_id: Optional[str] = None, + ): """ 发送种子列表消息 """ @@ -760,7 +969,9 @@ class MessageChain(ChainBase): else: title = f"【{title}】共找到{total}条相关资源,请选择下载" - buttons = self._create_torrent_buttons(channel=channel, items=items, total=total) + buttons = self._create_torrent_buttons( + channel=channel, items=items, total=total + ) else: # 不支持按钮的渠道,使用文本提示 if total > self._page_size: @@ -774,15 +985,17 @@ class MessageChain(ChainBase): source=source, title=title, userid=userid, - link=settings.MP_DOMAIN('#/resource'), + link=settings.MP_DOMAIN("#/resource"), buttons=buttons, original_message_id=original_message_id, - original_chat_id=original_chat_id + original_chat_id=original_chat_id, ) self.post_torrents_message(notification, torrents=items) - def _create_torrent_buttons(self, channel: MessageChannel, items: list, total: int) -> List[List[Dict]]: + def _create_torrent_buttons( + self, channel: MessageChannel, items: list, total: int + ) -> List[List[Dict]]: """ 创建种子下载按钮 """ @@ -806,20 +1019,18 @@ class MessageChain(ChainBase): # 每行一个按钮,使用完整文本 button_text = f"{i + 1}. {torrent.site_name} - {torrent.seeders}↑" if len(button_text) > max_text_length: - button_text = button_text[:max_text_length - 3] + "..." + button_text = button_text[: max_text_length - 3] + "..." - buttons.append([{ - "text": button_text, - "callback_data": f"download_{i + 1}" - }]) + buttons.append( + [{"text": button_text, "callback_data": f"download_{i + 1}"}] + ) else: # 多按钮一行的情况,使用简化文本 button_text = f"{i + 1}" - current_row.append({ - "text": button_text, - "callback_data": f"download_{i + 1}" - }) + current_row.append( + {"text": button_text, "callback_data": f"download_{i + 1}"} + ) # 如果当前行已满或者是最后一个按钮,添加到按钮列表 if len(current_row) == max_per_row or i == len(items) - 1: @@ -857,7 +1068,8 @@ class MessageChain(ChainBase): # 更新最后使用时间 self._user_sessions[userid] = (session_id, current_time) logger.info( - f"复用会话ID: {session_id}, 用户: {userid}, 距离上次会话: {time_diff.total_seconds() / 60:.1f}分钟") + f"复用会话ID: {session_id}, 用户: {userid}, 距离上次会话: {time_diff.total_seconds() / 60:.1f}分钟" + ) return session_id # 创建新的会话ID @@ -877,7 +1089,12 @@ class MessageChain(ChainBase): return True return False - def remote_clear_session(self, channel: MessageChannel, userid: Union[str, int], source: Optional[str] = None): + def remote_clear_session( + self, + channel: MessageChannel, + userid: Union[str, int], + source: Optional[str] = None, + ): """ 清除用户会话(远程命令接口) """ @@ -892,43 +1109,55 @@ class MessageChain(ChainBase): try: asyncio.run_coroutine_threadsafe( agent_manager.clear_session( - session_id=session_id, - user_id=str(userid) + session_id=session_id, user_id=str(userid) ), - global_vars.loop + global_vars.loop, ) except Exception as e: logger.warning(f"清除智能体会话记忆失败: {e}") - self.post_message(Notification( - channel=channel, - source=source, - title="智能体会话已清除,下次将创建新的会话", - userid=userid - )) + self.post_message( + Notification( + channel=channel, + source=source, + title="智能体会话已清除,下次将创建新的会话", + userid=userid, + ) + ) else: - self.post_message(Notification( - channel=channel, - source=source, - title="您当前没有活跃的智能体会话", - userid=userid - )) + self.post_message( + Notification( + channel=channel, + source=source, + title="您当前没有活跃的智能体会话", + userid=userid, + ) + ) - def _handle_ai_message(self, text: str, channel: MessageChannel, source: str, - userid: Union[str, int], username: str) -> None: + def _handle_ai_message( + self, + text: str, + channel: MessageChannel, + source: str, + userid: Union[str, int], + username: str, + images: Optional[List[str]] = None, + ) -> None: """ 处理AI智能体消息 """ try: # 检查AI智能体是否启用 if not settings.AI_AGENT_ENABLE: - self.post_message(Notification( - channel=channel, - source=source, - userid=userid, - username=username, - title="MoviePilot智能助手未启用,请在系统设置中启用" - )) + self.post_message( + Notification( + channel=channel, + source=source, + userid=userid, + username=username, + title="MoviePilot智能助手未启用,请在系统设置中启用", + ) + ) return # 提取用户消息 @@ -936,32 +1165,110 @@ class MessageChain(ChainBase): user_message = text[3:].strip() # 移除 "/ai" 前缀(大小写不敏感) else: user_message = text.strip() # 按原消息处理 - if not user_message: - self.post_message(Notification( - channel=channel, - source=source, - userid=userid, - username=username, - title="请输入您的问题或需求" - )) + if not user_message and not images: + self.post_message( + Notification( + channel=channel, + source=source, + userid=userid, + username=username, + title="请输入您的问题或需求", + ) + ) return # 生成或复用会话ID session_id = self._get_or_create_session_id(userid) + # 下载图片并转为base64 + if images: + images = self._download_images_to_base64(images, channel, source) + # 在事件循环中处理 asyncio.run_coroutine_threadsafe( agent_manager.process_message( session_id=session_id, user_id=str(userid), message=user_message, + images=images, channel=channel.value if channel else None, source=source, - username=username + username=username, ), - global_vars.loop + global_vars.loop, ) except Exception as e: logger.error(f"处理AI智能体消息失败: {e}") - self.messagehelper.put(f"AI智能体处理失败: {str(e)}", role="system", title="MoviePilot助手") + self.messagehelper.put( + f"AI智能体处理失败: {str(e)}", role="system", title="MoviePilot助手" + ) + + @staticmethod + def _download_images_to_base64( + images: List[str], channel: MessageChannel, source: str + ) -> List[str]: + """ + 下载图片并转为base64 + """ + if not images: + return None + base64_images = [] + for img in images: + try: + if img.startswith("tg://file_id/"): + file_id = img.replace("tg://file_id/", "") + base64_data = MessageChain._download_telegram_file(file_id, source) + if base64_data: + base64_images.append(f"data:image/jpeg;base64,{base64_data}") + elif img.startswith("http"): + resp = RequestUtils(timeout=30).get_res(img) + if resp and resp.content: + import base64 + + base64_data = base64.b64encode(resp.content).decode() + mime_type = resp.headers.get("Content-Type", "image/jpeg") + base64_images.append(f"data:{mime_type};base64,{base64_data}") + except Exception as e: + logger.error(f"下载图片失败: {img}, error: {e}") + return base64_images if base64_images else None + + @staticmethod + def _download_telegram_file(file_id: str, source: str) -> Optional[str]: + """ + 下载Telegram文件并转为base64 + """ + from app.modules.telegram import TelegramModule + + module = TelegramModule() + config = module.get_config(source) + if not config: + return None + client = module.get_instance(config.name) + if not client or not client._bot: + return None + try: + file_info = client._bot.get_file(file_id) + file_url = f"https://api.telegram.org/file/bot{client._telegram_token}/{file_info.file_path}" + resp = RequestUtils(timeout=30).get_res(file_url) + if resp and resp.content: + import base64 + + return base64.b64encode(resp.content).decode() + except Exception as e: + logger.error(f"下载Telegram文件失败: {e}") + return None + client = module.get_instance(config.name) + if not client: + return None + try: + file_info = client.bot.get_file(file_id) + file_url = f"https://api.telegram.org/file/bot{client._telegram_token}/{file_info.file_path}" + resp = RequestUtils(timeout=30).get_res(file_url) + if resp and resp.content: + import base64 + + return base64.b64encode(resp.content).decode() + except Exception as e: + logger.error(f"下载Telegram文件失败: {e}") + return None diff --git a/app/modules/discord/__init__.py b/app/modules/discord/__init__.py index c2e9f2b4..af1607f2 100644 --- a/app/modules/discord/__init__.py +++ b/app/modules/discord/__init__.py @@ -130,10 +130,11 @@ class DiscordModule(_ModuleBase, _MessageBase[Discord]): if msg_type == "message": text = msg_json.get("text") chat_id = msg_json.get("chat_id") - if text and userid: + images = self._extract_images(msg_json) + if (text or images) and userid: logger.info( f"收到来自 {client_config.name} 的 Discord 消息:" - f"userid={userid}, username={username}, text={text}" + f"userid={userid}, username={username}, text={text}, images={len(images) if images else 0}" ) return CommingMessage( channel=MessageChannel.Discord, @@ -142,9 +143,26 @@ class DiscordModule(_ModuleBase, _MessageBase[Discord]): username=username, text=text, chat_id=str(chat_id) if chat_id else None, + images=images, ) return None + @staticmethod + def _extract_images(msg_json: dict) -> Optional[List[str]]: + """ + 从Discord消息中提取图片URL + """ + attachments = msg_json.get("attachments", []) + if not attachments: + return None + images = [] + for attachment in attachments: + if attachment.get("type") == "image": + url = attachment.get("url") + if url: + images.append(url) + return images if images else None + def post_message(self, message: Notification, **kwargs) -> None: """ 发送通知消息 diff --git a/app/modules/slack/__init__.py b/app/modules/slack/__init__.py index 61889ad9..cdcd7e4b 100644 --- a/app/modules/slack/__init__.py +++ b/app/modules/slack/__init__.py @@ -198,10 +198,12 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]): logger.debug(f"解析Slack消息失败:{str(err)}") return None if msg_json: + images = None if msg_json.get("type") == "message": userid = msg_json.get("user") text = msg_json.get("text") username = msg_json.get("user") + images = self._extract_images(msg_json) elif msg_json.get("type") == "block_actions": userid = msg_json.get("user", {}).get("id") callback_data = msg_json.get("actions")[0].get("value") @@ -243,6 +245,7 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]): flags=re.IGNORECASE, ).strip() username = "" + images = self._extract_images(msg_json.get("event", {})) elif msg_json.get("type") == "shortcut": userid = msg_json.get("user", {}).get("id") text = msg_json.get("callback_id") @@ -254,7 +257,7 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]): else: return None logger.info( - f"收到来自 {client_config.name} 的Slack消息:userid={userid}, username={username}, text={text}" + f"收到来自 {client_config.name} 的Slack消息:userid={userid}, username={username}, text={text}, images={len(images) if images else 0}" ) return CommingMessage( channel=MessageChannel.Slack, @@ -262,9 +265,26 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]): userid=userid, username=username, text=text, + images=images, ) return None + @staticmethod + def _extract_images(msg_json: dict) -> Optional[List[str]]: + """ + 从Slack消息中提取图片URL + """ + files = msg_json.get("files", []) + if not files: + return None + images = [] + for file in files: + if file.get("type") in ("image", "jpg", "jpeg", "png", "gif", "webp"): + url = file.get("url_private") or file.get("url_private_download") + if url: + images.append(url) + return images if images else None + def post_message(self, message: Notification, **kwargs) -> None: """ 发送消息 diff --git a/app/modules/telegram/__init__.py b/app/modules/telegram/__init__.py index 9a81ac7a..79370600 100644 --- a/app/modules/telegram/__init__.py +++ b/app/modules/telegram/__init__.py @@ -194,26 +194,33 @@ class TelegramModule(_ModuleBase, _MessageBase[Telegram]): text = msg.get("text") user_id = msg.get("from", {}).get("id") user_name = msg.get("from", {}).get("username") - # Extract chat_id to enable correct reply targeting chat_id = msg.get("chat", {}).get("id") - if text and user_id: + images = self._extract_images(msg) + + if user_id: + if not text and not images: + logger.debug( + f"收到来自 {client_config.name} 的Telegram消息无文本和图片" + ) + return None + logger.info( f"收到来自 {client_config.name} 的Telegram消息:" - f"userid={user_id}, username={user_name}, chat_id={chat_id}, text={text}" + f"userid={user_id}, username={user_name}, chat_id={chat_id}, text={text}, images={len(images) if images else 0}" ) - # Clean bot mentions from text to ensure consistent processing - cleaned_text = self._clean_bot_mention( - text, client.bot_username if client else None + cleaned_text = ( + self._clean_bot_mention(text, client.bot_username if client else None) + if text + else None ) - # 检查权限 admin_users = client_config.config.get("TELEGRAM_ADMINS") user_list = client_config.config.get("TELEGRAM_USERS") config_chat_id = client_config.config.get("TELEGRAM_CHAT_ID") - if cleaned_text.startswith("/"): + if cleaned_text and cleaned_text.startswith("/"): if ( admin_users and str(user_id) not in admin_users.split(",") @@ -236,11 +243,34 @@ class TelegramModule(_ModuleBase, _MessageBase[Telegram]): source=client_config.name, userid=user_id, username=user_name, - text=cleaned_text, # Use cleaned text + text=cleaned_text, chat_id=str(chat_id) if chat_id else None, + images=images if images else None, ) return None + @staticmethod + def _extract_images(msg: dict) -> Optional[List[str]]: + """ + 从Telegram消息中提取图片file_id + """ + images = [] + photo = msg.get("photo") + if photo and isinstance(photo, list): + largest_photo = photo[-1] + file_id = largest_photo.get("file_id") + if file_id: + images.append(file_id) + + document = msg.get("document") + if document: + file_id = document.get("file_id") + mime_type = document.get("mime_type", "") + if file_id and mime_type.startswith("image/"): + images.append(file_id) + + return images if images else None + @staticmethod def _clean_bot_mention(text: str, bot_username: Optional[str]) -> str: """ diff --git a/app/schemas/message.py b/app/schemas/message.py index 17c54e6a..cd1670a6 100644 --- a/app/schemas/message.py +++ b/app/schemas/message.py @@ -53,6 +53,8 @@ class CommingMessage(BaseModel): chat_id: Optional[str] = None # 完整的回调查询信息(原始数据) callback_query: Optional[Dict] = None + # 图片列表(图片URL或file_id) + images: Optional[List[str]] = None def to_dict(self): """