diff --git a/app/agent/__init__.py b/app/agent/__init__.py index 848f5dd1..71561d6e 100644 --- a/app/agent/__init__.py +++ b/app/agent/__init__.py @@ -69,8 +69,6 @@ class MoviePilotAgent: """初始化LLM模型""" provider = settings.LLM_PROVIDER.lower() api_key = settings.LLM_API_KEY - if not api_key: - raise ValueError("未配置 LLM_API_KEY") if provider == "google": if settings.PROXY_HOST: diff --git a/app/chain/message.py b/app/chain/message.py index 5162a75d..25469417 100644 --- a/app/chain/message.py +++ b/app/chain/message.py @@ -931,19 +931,11 @@ class MessageChain(ChainBase): )) return - # 检查LLM配置 - if not settings.LLM_API_KEY: - self.post_message(Notification( - channel=channel, - source=source, - userid=userid, - username=username, - title="MoviePilot智能助未配置,请在系统设置中配置" - )) - return - # 提取用户消息 - user_message = text[3:].strip() # 移除 "/ai" 前缀 + if text.lower().startswith("/ai"): + user_message = text[3:].strip() # 移除 "/ai" 前缀(大小写不敏感) + else: + user_message = text.strip() # 按原消息处理 if not user_message: self.post_message(Notification( channel=channel, diff --git a/app/core/cache.py b/app/core/cache.py index 0982d5a0..2bb1f1e9 100644 --- a/app/core/cache.py +++ b/app/core/cache.py @@ -1024,13 +1024,11 @@ def fresh(fresh: bool = True): with fresh(): result = some_cached_function() """ - token = _fresh.set(fresh) - logger.debug(f"Setting fresh mode to {fresh}. {id(token):#x}") + token = _fresh.set(fresh or is_fresh()) try: yield finally: _fresh.reset(token) - logger.debug(f"Reset fresh mode. {id(token):#x}") @asynccontextmanager async def async_fresh(fresh: bool = True): @@ -1041,13 +1039,11 @@ async def async_fresh(fresh: bool = True): async with async_fresh(): result = await some_async_cached_function() """ - token = _fresh.set(fresh) - logger.debug(f"Setting async_fresh mode to {fresh}. {id(token):#x}") + token = _fresh.set(fresh or is_fresh()) try: yield finally: _fresh.reset(token) - logger.debug(f"Reset async_fresh mode. {id(token):#x}") def is_fresh() -> bool: """ diff --git a/app/modules/telegram/telegram.py b/app/modules/telegram/telegram.py index a3c72898..b4ba7c6d 100644 --- a/app/modules/telegram/telegram.py +++ b/app/modules/telegram/telegram.py @@ -1,19 +1,21 @@ +import asyncio import re import threading -import uuid -from pathlib import Path from threading import Event from typing import Optional, List, Dict, Callable from urllib.parse import urljoin import telebot +from telegramify_markdown import standardize, telegramify +from telegramify_markdown.type import ContentTypes, SentType from telebot import apihelper -from telebot.types import InputFile, InlineKeyboardMarkup, InlineKeyboardButton +from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton from telebot.types import InputMediaPhoto 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.log import logger from app.utils.common import retry from app.utils.http import RequestUtils @@ -52,7 +54,7 @@ class Telegram: else: apihelper.proxy = settings.PROXY # bot - _bot = telebot.TeleBot(self._telegram_token, parse_mode="Markdown") + _bot = telebot.TeleBot(self._telegram_token, parse_mode="MarkdownV2") # 记录句柄 self._bot = _bot # 获取并存储bot用户名用于@检测 @@ -236,12 +238,14 @@ class Telegram: return False try: - if text: - # 对text进行Markdown特殊字符转义 - text = re.sub(r"([_`])", r"\\\1", text) - caption = f"*{title}*\n{text}" + if title and text: + caption = f"**{title}**\n{text}" + elif title: + caption = f"**{title}**" + elif text: + caption = text else: - caption = f"*{title}*" + caption = "" if link: caption = f"{caption}\n[查看详情]({link})" @@ -499,7 +503,7 @@ class Telegram: if image: # 如果有图片,使用edit_message_media - media = InputMediaPhoto(media=image, caption=text, parse_mode="Markdown") + media = InputMediaPhoto(media=image, caption=standardize(text), parse_mode="MarkdownV2") self._bot.edit_message_media( chat_id=chat_id, message_id=message_id, @@ -511,8 +515,8 @@ class Telegram: self._bot.edit_message_text( chat_id=chat_id, message_id=message_id, - text=text, - parse_mode="Markdown", + text=standardize(text), + parse_mode="MarkdownV2", reply_markup=reply_markup ) return True @@ -520,49 +524,120 @@ class Telegram: logger.error(f"编辑消息失败:{str(e)}") return False - @retry(RetryException, logger=logger) def __send_request(self, userid: Optional[str] = None, image="", caption="", reply_markup: Optional[InlineKeyboardMarkup] = None) -> bool: """ 向Telegram发送报文 :param reply_markup: 内联键盘 """ - if image: - res = RequestUtils(proxies=settings.PROXY, ua=settings.NORMAL_USER_AGENT).get_res(image) - if res is None: - raise Exception("获取图片失败") - if res.content: - # 使用随机标识构建图片文件的完整路径,并写入图片内容到文件 - image_file = Path(settings.TEMP_PATH) / "telegram" / str(uuid.uuid4()) - if not image_file.parent.exists(): - image_file.parent.mkdir(parents=True, exist_ok=True) - image_file.write_bytes(res.content) - photo = InputFile(image_file) - # 发送图片到Telegram - ret = self._bot.send_photo(chat_id=userid or self._telegram_chat_id, - photo=photo, - caption=caption, - parse_mode="Markdown", - reply_markup=reply_markup) - if ret is None: - raise RetryException("发送图片消息失败") - return True - # 按4096分段循环发送消息 - ret = None - if len(caption) > 4095: - for i in range(0, len(caption), 4095): - ret = self._bot.send_message(chat_id=userid or self._telegram_chat_id, - text=caption[i:i + 4095], - parse_mode="Markdown", - reply_markup=reply_markup if i == 0 else None) - else: - ret = self._bot.send_message(chat_id=userid or self._telegram_chat_id, - text=caption, - parse_mode="Markdown", - reply_markup=reply_markup) - if ret is None: - raise RetryException("发送文本消息失败") - return True if ret else False + kwargs = { + 'chat_id': userid or self._telegram_chat_id, + 'parse_mode': "MarkdownV2", + 'reply_markup': reply_markup + } + + try: + # 处理图片 + image = self.__process_image(image) if image else None + + # 图片消息的标题长度限制为1024,文本消息为4096 + caption_limit = 1024 if image else 4096 + if len(caption) < caption_limit: + ret = self.__send_short_message(image, caption, **kwargs) + else: + sent_idx = set() + ret = self.__send_long_message(image, caption, sent_idx, **kwargs) + + return ret is not None + except Exception as e: + logger.error(f"发送Telegram消息失败: {e}") + return False + + @retry(RetryException, logger=logger) + def __process_image(self, image_url: str) -> bytes: + """ + 处理图片URL,获取图片内容 + """ + try: + res = RequestUtils( + proxies=settings.PROXY, + ua=settings.NORMAL_USER_AGENT + ).get_res(image_url) + + if not res or not res.content: + raise RetryException("获取图片失败") + + return res.content + except Exception as e: + raise + + @retry(RetryException, logger=logger) + def __send_short_message(self, image: Optional[bytes], caption: str, **kwargs): + """ + 发送短消息 + """ + try: + if image: + return self._bot.send_photo( + photo=image, + caption=standardize(caption), + **kwargs + ) + else: + return self._bot.send_message( + text=standardize(caption), + **kwargs + ) + except Exception as e: + raise RetryException(f"发送{'图片' if image else '文本'}消息失败") + + @retry(RetryException, logger=logger) + def __send_long_message(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() + + ret = None + for i, item in enumerate(boxs): + if i in sent_idx: + # 跳过已发送消息 + continue + + current_reply_markup = reply_markup if i == 0 else None + + if item.content_type == ContentTypes.TEXT and (i != 0 or not image): + ret = self._bot.send_message(**kwargs, + text=item.content, + reply_markup=current_reply_markup + ) + + elif item.content_type == ContentTypes.PHOTO or (image and i == 0): + ret = self._bot.send_photo(**kwargs, + photo=(getattr(item, "file_name", ""), + getattr(item, "file_data", image)), + caption=getattr(item, "caption", item.content), + reply_markup=current_reply_markup + ) + + elif item.content_type == ContentTypes.FILE: + ret = self._bot.send_document(**kwargs, + document=(item.file_name, item.file_data), + caption=item.caption, + reply_markup=current_reply_markup + ) + + sent_idx.add(i) + + return ret + except Exception as e: + try: + raise RetryException(f"消息 [{i + 1}/{len(boxs)}] 发送失败") from e + except NameError: + raise RetryException("发送长消息失败") from e def register_commands(self, commands: Dict[str, dict]): """