From 3b14d59dcd821e17b3bd3da499daa1d1652a3e94 Mon Sep 17 00:00:00 2001 From: chengyongru <2755839590@qq.com> Date: Tue, 14 Jul 2026 00:04:28 +0800 Subject: [PATCH] fix(telegram): fall back on overflow HTML rejection --- nanobot/channels/telegram.py | 50 ++++++++++++++++++------- tests/channels/test_telegram_channel.py | 37 ++++++++++++++++++ 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/nanobot/channels/telegram.py b/nanobot/channels/telegram.py index 6496463a..c87eda01 100644 --- a/nanobot/channels/telegram.py +++ b/nanobot/channels/telegram.py @@ -1065,7 +1065,7 @@ class TelegramChannel(BaseChannel): chunks = _split_telegram_markdown_html_chunks(buf.text, TELEGRAM_HTML_MAX_LEN) if len(chunks) <= 1: return - _, first_html = chunks[0] + first_markdown, first_html = chunks[0] try: await self._call_with_retry( self._app.bot.edit_message_text, @@ -1073,20 +1073,44 @@ class TelegramChannel(BaseChannel): text=first_html, parse_mode="HTML", ) - except Exception as e: + except BadRequest as e: if not self._is_not_modified_error(e): - self.logger.warning("Stream overflow edit failed: {}", e) - raise - for _, html in chunks[1:-1]: - await self._call_with_retry( - self._app.bot.send_message, - chat_id=chat_id, text=html, parse_mode="HTML", **thread_kwargs, - ) + self.logger.warning( + "Stream overflow HTML edit failed, falling back to plain text: {}", e + ) + try: + await self._call_with_retry( + self._app.bot.edit_message_text, + chat_id=chat_id, message_id=buf.message_id, + text=first_markdown, + ) + except Exception as plain_error: + if not self._is_not_modified_error(plain_error): + self.logger.warning("Stream overflow plain edit failed: {}", plain_error) + raise + except Exception as e: + self.logger.warning("Stream overflow edit failed: {}", e) + raise + + async def send_chunk(markdown: str, html: str) -> Any: + try: + return await self._call_with_retry( + self._app.bot.send_message, + chat_id=chat_id, text=html, parse_mode="HTML", **thread_kwargs, + ) + except BadRequest as e: + self.logger.warning( + "Stream overflow HTML send failed, falling back to plain text: {}", e + ) + return await self._call_with_retry( + self._app.bot.send_message, + chat_id=chat_id, text=markdown, **thread_kwargs, + ) + + for markdown, html in chunks[1:-1]: + await send_chunk(markdown, html) markdown_tail, tail_html = chunks[-1] - sent = await self._call_with_retry( - self._app.bot.send_message, - chat_id=chat_id, text=tail_html, parse_mode="HTML", **thread_kwargs, - ) + sent = await send_chunk(markdown_tail, tail_html) buf.message_id = sent.message_id buf.text = markdown_tail diff --git a/tests/channels/test_telegram_channel.py b/tests/channels/test_telegram_channel.py index fbbeb944..c052f6e7 100644 --- a/tests/channels/test_telegram_channel.py +++ b/tests/channels/test_telegram_channel.py @@ -917,6 +917,43 @@ async def test_send_delta_incremental_html_expansion_does_not_overflow() -> None assert "" not in buf.text +@pytest.mark.asyncio +async def test_send_delta_incremental_html_parse_failure_falls_back_to_plain() -> None: + """Telegram HTML rejections retry overflow chunks as plain text.""" + from telegram.error import BadRequest + + channel = TelegramChannel( + TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), + MessageBus(), + ) + channel._app = _FakeApp(lambda: None) + channel._app.bot.edit_message_text = AsyncMock( + side_effect=[BadRequest("Can't parse entities"), None] + ) + channel._app.bot.send_message = AsyncMock( + side_effect=[BadRequest("Can't parse entities"), SimpleNamespace(message_id=99)] + ) + + first_chunk = f"**{'x' * 3900}**\n" + tail = "**tail** " * 50 + channel._stream_bufs["123"] = _StreamBuf( + text=first_chunk + tail, message_id=7, last_edit=0.0, stream_id="s:0" + ) + + await channel.send_delta("123", "y", stream_id="s:0") + + edit_calls = channel._app.bot.edit_message_text.call_args_list + assert edit_calls[0].kwargs["parse_mode"] == "HTML" + assert edit_calls[1].kwargs["text"] == first_chunk.rstrip() + assert "parse_mode" not in edit_calls[1].kwargs + + send_calls = channel._app.bot.send_message.call_args_list + assert send_calls[0].kwargs["parse_mode"] == "HTML" + assert send_calls[1].kwargs["text"] == tail + "y" + assert "parse_mode" not in send_calls[1].kwargs + assert channel._stream_bufs["123"].text == tail + "y" + + @pytest.mark.asyncio async def test_send_delta_initial_send_keeps_message_in_thread() -> None: channel = TelegramChannel(