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
This commit is contained in:
kunci115 2026-05-17 03:45:06 +08:00 committed by Teknium
parent fc42bb918b
commit df530b4a0c

View file

@ -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)