From df530b4a0cbf4a60c847ca370e08e538808c36ed Mon Sep 17 00:00:00 2001 From: kunci115 Date: Sun, 17 May 2026 03:45:06 +0800 Subject: [PATCH] fix(send_message): add thread-not-found retry for Telegram forum topic sends The standalone _send_telegram path in send_message_tool lacked the thread-not-found fallback that the gateway adapter has. When a forum topic thread_id was stale or deleted, the send would fail entirely instead of retrying to the General topic. Changes: - Add _is_telegram_thread_not_found() helper matching gateway adapter - Add thread-not-found retry in text send path - Add thread-not-found retry in media send path (with f.seek(0)) - Separate text_kwargs from thread_kwargs to prevent disable_web_page_preview leaking into send_photo/send_video calls Closes #27012 --- tools/send_message_tool.py | 111 ++++++++++++++++++++++++++++--------- 1 file changed, 86 insertions(+), 25 deletions(-) diff --git a/tools/send_message_tool.py b/tools/send_message_tool.py index 08fa20292e1..302ddb16b4c 100644 --- a/tools/send_message_tool.py +++ b/tools/send_message_tool.py @@ -786,6 +786,15 @@ async def _send_to_platform(platform, pconfig, chat_id, message, thread_id=None, return last_result +def _is_telegram_thread_not_found(error: Exception) -> bool: + """Check if a Telegram error is a thread-not-found failure. + + Matches the gateway adapter's ``_is_thread_not_found_error`` for + the standalone ``_send_telegram`` path (issue #27012). + """ + return "thread not found" in str(error).lower() + + async def _send_telegram(token, chat_id, message, media_files=None, thread_id=None, disable_link_previews=False, force_document=False): """Send via Telegram Bot API (one-shot, no polling needed). @@ -873,8 +882,12 @@ async def _send_telegram(token, chat_id, message, media_files=None, thread_id=No ) if effective_thread_id is not None: thread_kwargs["message_thread_id"] = effective_thread_id + # disable_web_page_preview is only valid for send_message, not + # send_photo/send_video/etc. Keep it separate so media sends + # don't inherit an invalid parameter (issue #27012). + text_kwargs = dict(thread_kwargs) if disable_link_previews: - thread_kwargs["disable_web_page_preview"] = True + text_kwargs["disable_web_page_preview"] = True last_msg = None warnings = [] @@ -884,11 +897,24 @@ async def _send_telegram(token, chat_id, message, media_files=None, thread_id=No last_msg = await _send_telegram_message_with_retry( bot, chat_id=int_chat_id, text=formatted, - parse_mode=send_parse_mode, entities=_entities, **thread_kwargs + parse_mode=send_parse_mode, entities=_entities, **text_kwargs ) except Exception as md_error: - # Parse failed, fall back to plain text - if "parse" in str(md_error).lower() or "markdown" in str(md_error).lower() or "html" in str(md_error).lower(): + # Thread not found — retry without message_thread_id so the + # message still delivers (matching the gateway adapter's + # fallback behaviour, issue #27012). + if _is_telegram_thread_not_found(md_error) and thread_kwargs: + logger.warning( + "Thread %s not found in _send_telegram, retrying without message_thread_id", + thread_kwargs.get("message_thread_id"), + ) + text_kwargs.pop("message_thread_id", None) + last_msg = await _send_telegram_message_with_retry( + bot, + chat_id=int_chat_id, text=formatted, + parse_mode=send_parse_mode, **text_kwargs + ) + elif "parse" in str(md_error).lower() or "markdown" in str(md_error).lower() or "html" in str(md_error).lower(): logger.warning( "Parse mode %s failed in _send_telegram, falling back to plain text: %s", send_parse_mode, @@ -905,7 +931,7 @@ async def _send_telegram(token, chat_id, message, media_files=None, thread_id=No last_msg = await _send_telegram_message_with_retry( bot, chat_id=int_chat_id, text=plain, - parse_mode=None, entities=_entities, **thread_kwargs + parse_mode=None, entities=_entities, **text_kwargs ) else: raise @@ -920,26 +946,61 @@ async def _send_telegram(token, chat_id, message, media_files=None, thread_id=No ext = os.path.splitext(media_path)[1].lower() try: with open(media_path, "rb") as f: - if ext in _IMAGE_EXTS and not force_document: - last_msg = await bot.send_photo( - chat_id=int_chat_id, photo=f, **thread_kwargs - ) - elif ext in _VIDEO_EXTS: - last_msg = await bot.send_video( - chat_id=int_chat_id, video=f, **thread_kwargs - ) - elif ext in _VOICE_EXTS and is_voice: - last_msg = await bot.send_voice( - chat_id=int_chat_id, voice=f, **thread_kwargs - ) - elif ext in _TELEGRAM_SEND_AUDIO_EXTS: - last_msg = await bot.send_audio( - chat_id=int_chat_id, audio=f, **thread_kwargs - ) - else: - last_msg = await bot.send_document( - chat_id=int_chat_id, document=f, **thread_kwargs - ) + media_kwargs = dict(thread_kwargs) + try: + if ext in _IMAGE_EXTS and not force_document: + last_msg = await bot.send_photo( + chat_id=int_chat_id, photo=f, **media_kwargs + ) + elif ext in _VIDEO_EXTS: + last_msg = await bot.send_video( + chat_id=int_chat_id, video=f, **media_kwargs + ) + elif ext in _VOICE_EXTS and is_voice: + last_msg = await bot.send_voice( + chat_id=int_chat_id, voice=f, **media_kwargs + ) + elif ext in _TELEGRAM_SEND_AUDIO_EXTS: + last_msg = await bot.send_audio( + chat_id=int_chat_id, audio=f, **media_kwargs + ) + else: + last_msg = await bot.send_document( + chat_id=int_chat_id, document=f, **media_kwargs + ) + except Exception as media_err: + if _is_telegram_thread_not_found(media_err) and media_kwargs.get("message_thread_id"): + # Thread not found for media — retry without + # message_thread_id (issue #27012). + logger.warning( + "Thread %s not found for media send, retrying without message_thread_id", + media_kwargs["message_thread_id"], + ) + # Re-seek the file since the first attempt consumed it + f.seek(0) + media_kwargs.pop("message_thread_id", None) + if ext in _IMAGE_EXTS and not force_document: + last_msg = await bot.send_photo( + chat_id=int_chat_id, photo=f, **media_kwargs + ) + elif ext in _VIDEO_EXTS: + last_msg = await bot.send_video( + chat_id=int_chat_id, video=f, **media_kwargs + ) + elif ext in _VOICE_EXTS and is_voice: + last_msg = await bot.send_voice( + chat_id=int_chat_id, voice=f, **media_kwargs + ) + elif ext in _TELEGRAM_SEND_AUDIO_EXTS: + last_msg = await bot.send_audio( + chat_id=int_chat_id, audio=f, **media_kwargs + ) + else: + last_msg = await bot.send_document( + chat_id=int_chat_id, document=f, **media_kwargs + ) + else: + raise except Exception as e: warning = _sanitize_error_text(f"Failed to send media {media_path}: {e}") logger.error(warning)