diff --git a/app/agent/__init__.py b/app/agent/__init__.py index cd627295..a2fd9258 100644 --- a/app/agent/__init__.py +++ b/app/agent/__init__.py @@ -251,7 +251,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 @@ -269,7 +269,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 diff --git a/app/chain/message.py b/app/chain/message.py index fa2606d4..2900972f 100644 --- a/app/chain/message.py +++ b/app/chain/message.py @@ -1204,9 +1204,8 @@ class MessageChain(ChainBase): f"AI智能体处理失败: {str(e)}", role="system", title="MoviePilot助手" ) - @staticmethod def _download_images_to_base64( - images: List[str], channel: MessageChannel, source: str + self, images: List[str], channel: MessageChannel, source: str ) -> List[str]: """ 下载图片并转为base64 @@ -1218,7 +1217,9 @@ class MessageChain(ChainBase): try: if img.startswith("tg://file_id/"): file_id = img.replace("tg://file_id/", "") - base64_data = MessageChain._download_telegram_file(file_id, source) + base64_data = self.run_module( + "download_file_to_base64", file_id=file_id, source=source + ) if base64_data: base64_images.append(f"data:image/jpeg;base64,{base64_data}") elif img.startswith("http"): @@ -1232,43 +1233,3 @@ class MessageChain(ChainBase): 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/telegram/__init__.py b/app/modules/telegram/__init__.py index 79370600..2aa419a1 100644 --- a/app/modules/telegram/__init__.py +++ b/app/modules/telegram/__init__.py @@ -288,7 +288,7 @@ class TelegramModule(_ModuleBase, _MessageBase[Telegram]): # Remove mention at the beginning with optional following space if cleaned.startswith(mention_pattern): - cleaned = cleaned[len(mention_pattern) :].lstrip() + cleaned = cleaned[len(mention_pattern):].lstrip() # Remove mention at any other position cleaned = cleaned.replace(mention_pattern, "").strip() @@ -525,3 +525,23 @@ class TelegramModule(_ModuleBase, _MessageBase[Telegram]): f"Command set has changed, Updating new commands: {filtered_scoped_commands}" ) client.register_commands(filtered_scoped_commands) + + def download_file_to_base64(self, file_id: str, source: str) -> Optional[str]: + """ + 下载Telegram文件并转为base64 + :param file_id: Telegram文件ID + :param source: 来源名称 + :return: base64编码的图片数据 + """ + config = self.get_config(source) + if not config: + return None + client = self.get_instance(config.name) + if not client: + return None + file_content = client.download_file(file_id) + if file_content: + import base64 + + return base64.b64encode(file_content).decode() + return None diff --git a/app/modules/telegram/telegram.py b/app/modules/telegram/telegram.py index 0eb0322a..db2b9488 100644 --- a/app/modules/telegram/telegram.py +++ b/app/modules/telegram/telegram.py @@ -12,14 +12,14 @@ from telebot.types import ( InlineKeyboardButton, InputMediaPhoto, ) -from telegramify_markdown import standardize, telegramify +from telegramify_markdown import standardize, telegramify # noqa from telegramify_markdown.type import ContentTypes, SentType from app.core.config import settings from app.core.context import MediaInfo, Context from app.core.metainfo import MetaInfo -from app.helper.thread import ThreadHelper from app.helper.image import ImageHelper +from app.helper.thread import ThreadHelper from app.log import logger from app.utils.common import retry from app.utils.http import RequestUtils @@ -44,10 +44,10 @@ class Telegram: _typing_stop_flags: Dict[str, bool] = {} # chat_id -> 停止标志 def __init__( - self, - TELEGRAM_TOKEN: Optional[str] = None, - TELEGRAM_CHAT_ID: Optional[str] = None, - **kwargs, + self, + TELEGRAM_TOKEN: Optional[str] = None, + TELEGRAM_CHAT_ID: Optional[str] = None, + **kwargs, ): """ 初始化参数 @@ -171,6 +171,14 @@ class Telegram: self._polling_thread.start() logger.info("Telegram消息接收服务启动") + @property + def bot(self): + """ + 获取Telegram Bot实例 + :return: TeleBot实例或None + """ + return self._bot + @property def bot_username(self) -> Optional[str]: """ @@ -179,6 +187,24 @@ class Telegram: """ return self._bot_username + def download_file(self, file_id: str) -> Optional[bytes]: + """ + 下载Telegram文件 + :param file_id: 文件ID + :return: 文件字节数据 + """ + if not self._bot: + return None + try: + file_info = self._bot.get_file(file_id) + file_url = f"https://api.telegram.org/file/bot{self._telegram_token}/{file_info.file_path}" + resp = RequestUtils(timeout=30).get_res(file_url) + if resp and resp.content: + return resp.content + except Exception as e: + logger.error(f"下载Telegram文件失败: {e}") + return None + def _update_user_chat_mapping(self, userid: int, chat_id: int) -> None: """ 更新用户与聊天的映射关系 @@ -229,7 +255,7 @@ class Telegram: for entity in message.entities: if entity.type == "mention": mention_text = message.text[ - entity.offset : entity.offset + entity.length + entity.offset: entity.offset + entity.length ] if mention_text == f"@{self._bot_username}": logger.debug( @@ -295,15 +321,15 @@ class Telegram: task.join(timeout=1) def send_msg( - self, - title: str, - text: Optional[str] = None, - image: Optional[str] = None, - userid: Optional[str] = None, - link: Optional[str] = None, - buttons: Optional[List[List[dict]]] = None, - original_message_id: Optional[int] = None, - original_chat_id: Optional[str] = None, + self, + title: str, + text: Optional[str] = None, + image: Optional[str] = None, + userid: Optional[str] = None, + link: Optional[str] = None, + buttons: Optional[List[List[dict]]] = None, + original_message_id: Optional[int] = None, + original_chat_id: Optional[str] = None, ) -> Optional[dict]: """ 发送Telegram消息 @@ -324,6 +350,9 @@ class Telegram: logger.warn("标题和内容不能同时为空") return {"success": False} + # Determine target chat_id with improved logic using user mapping + chat_id = self._determine_target_chat_id(userid, original_chat_id) + try: # 标准化标题后再加粗,避免**符号被显示为文本 bold_title = ( @@ -341,9 +370,6 @@ class Telegram: if link: caption = f"{caption}\n[查看详情]({link})" - # Determine target chat_id with improved logic using user mapping - chat_id = self._determine_target_chat_id(userid, original_chat_id) - # 创建按钮键盘 reply_markup = None if buttons: @@ -386,7 +412,7 @@ class Telegram: return {"success": False} def _determine_target_chat_id( - self, userid: Optional[str] = None, original_chat_id: Optional[str] = None + self, userid: Optional[str] = None, original_chat_id: Optional[str] = None ) -> str: """ 确定目标聊天ID,使用用户映射确保回复到正确的聊天 @@ -410,14 +436,14 @@ class Telegram: return self._telegram_chat_id def send_medias_msg( - self, - medias: List[MediaInfo], - userid: Optional[str] = None, - title: Optional[str] = None, - link: Optional[str] = None, - buttons: Optional[List[List[Dict]]] = None, - original_message_id: Optional[int] = None, - original_chat_id: Optional[str] = None, + self, + medias: List[MediaInfo], + userid: Optional[str] = None, + title: Optional[str] = None, + link: Optional[str] = None, + buttons: Optional[List[List[Dict]]] = None, + original_message_id: Optional[int] = None, + original_chat_id: Optional[str] = None, ) -> Optional[bool]: """ 发送媒体列表消息 @@ -487,14 +513,14 @@ class Telegram: return False def send_torrents_msg( - self, - torrents: List[Context], - userid: Optional[str] = None, - title: Optional[str] = None, - link: Optional[str] = None, - buttons: Optional[List[List[Dict]]] = None, - original_message_id: Optional[int] = None, - original_chat_id: Optional[str] = None, + self, + torrents: List[Context], + userid: Optional[str] = None, + title: Optional[str] = None, + link: Optional[str] = None, + buttons: Optional[List[List[Dict]]] = None, + original_message_id: Optional[int] = None, + original_chat_id: Optional[str] = None, ) -> Optional[bool]: """ 发送种子列表消息 @@ -586,10 +612,10 @@ class Telegram: return InlineKeyboardMarkup(keyboard) def answer_callback_query( - self, - callback_query_id: int, - text: Optional[str] = None, - show_alert: bool = False, + self, + callback_query_id: int, + text: Optional[str] = None, + show_alert: bool = False, ) -> Optional[bool]: """ 回应回调查询 @@ -607,7 +633,7 @@ class Telegram: return False def delete_msg( - self, message_id: int, chat_id: Optional[int] = None + self, message_id: int, chat_id: Optional[int] = None ) -> Optional[bool]: """ 删除Telegram消息 @@ -644,11 +670,11 @@ class Telegram: return False def edit_msg( - self, - chat_id: Union[str, int], - message_id: Union[str, int], - text: str, - title: Optional[str] = None, + self, + chat_id: Union[str, int], + message_id: Union[str, int], + text: str, + title: Optional[str] = None, ) -> Optional[bool]: """ 编辑Telegram消息(公开方法) @@ -681,12 +707,12 @@ class Telegram: return False def __edit_message( - self, - chat_id: str, - message_id: int, - text: str, - buttons: Optional[List[List[dict]]] = None, - image: Optional[str] = None, + self, + chat_id: str, + message_id: int, + text: str, + buttons: Optional[List[List[dict]]] = None, + image: Optional[str] = None, ) -> Optional[bool]: """ 编辑已发送的消息 @@ -732,11 +758,11 @@ class Telegram: return False def __send_request( - self, - userid: Optional[str] = None, - image="", - caption="", - reply_markup: Optional[InlineKeyboardMarkup] = None, + self, + userid: Optional[str] = None, + image="", + caption="", + reply_markup: Optional[InlineKeyboardMarkup] = None, ): """ 向Telegram发送报文,返回发送的消息对象 @@ -795,26 +821,27 @@ class Telegram: @retry(RetryException, logger=logger) def __send_long_message( - self, image: Optional[bytes], caption: str, sent_idx: set, **kwargs + self, image: Optional[bytes], caption: str, sent_idx: set, **kwargs ): """ 发送长消息 """ - try: - reply_markup = kwargs.pop("reply_markup", None) - boxs: SentType = ( - ThreadHelper() - .submit(lambda x: asyncio.run(telegramify(x)), caption) - .result() - ) + reply_markup = kwargs.pop("reply_markup", None) - ret = None - for i, item in enumerate(boxs): - if i in sent_idx: - # 跳过已发送消息 - continue + boxs: SentType = ( + ThreadHelper() + .submit(lambda x: asyncio.run(telegramify(x)), caption) + .result() + ) + ret = None + for i, item in enumerate(boxs): + if i in sent_idx: + # 跳过已发送消息 + continue + + try: current_reply_markup = reply_markup if i == 0 else None if item.content_type == ContentTypes.TEXT and (i != 0 or not image): @@ -843,12 +870,13 @@ class Telegram: sent_idx.add(i) - return ret - except Exception as e: - try: - raise RetryException(f"消息 [{i + 1}/{len(boxs)}] 发送失败") from e - except NameError: - raise + except Exception as e: + try: + raise RetryException(f"消息 [{i + 1}/{len(boxs)}] 发送失败") from e + except NameError: + raise + + return ret def register_commands(self, commands: Dict[str, dict]): """