Skip to content

Commit 9c14a50

Browse files
Shujakuinkuraudogemini-code-assist[bot]Soulter
authored
fix: split long telegram final segments (#7432)
* fix: split long telegram final segments * test: refine telegram adapter helpers * Update astrbot/core/platform/sources/telegram/tg_event.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update tg_event.py * chore: ruff format --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> Co-authored-by: Soulter <905617992@qq.com>
1 parent c791c81 commit 9c14a50

2 files changed

Lines changed: 110 additions & 40 deletions

File tree

astrbot/core/platform/sources/telegram/tg_event.py

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,30 @@ def _split_message(cls, text: str) -> list[str]:
105105

106106
return chunks
107107

108+
@classmethod
109+
async def _send_text_chunks(
110+
cls,
111+
client: ExtBot,
112+
text: str,
113+
payload: dict[str, Any],
114+
) -> None:
115+
"""按 Telegram 限制切分文本后逐段发送。"""
116+
for chunk in cls._split_message(text):
117+
try:
118+
markdown_text = telegramify_markdown.markdownify(
119+
chunk,
120+
)
121+
await client.send_message(
122+
text=markdown_text,
123+
parse_mode="MarkdownV2",
124+
**cast(Any, payload),
125+
)
126+
except (ValueError, BadRequest) as e:
127+
logger.warning(
128+
f"Failed to convert message to Markdown,using normal text: {e!s}"
129+
)
130+
await client.send_message(text=chunk, **cast(Any, payload))
131+
108132
@classmethod
109133
async def _send_chat_action(
110134
cls,
@@ -283,22 +307,7 @@ async def send_with_client(
283307
if at_user_id and not at_flag:
284308
i.text = f"@{at_user_id} {i.text}"
285309
at_flag = True
286-
chunks = cls._split_message(i.text)
287-
for chunk in chunks:
288-
try:
289-
md_text = telegramify_markdown.markdownify(
290-
chunk,
291-
)
292-
await client.send_message(
293-
text=md_text,
294-
parse_mode="MarkdownV2",
295-
**cast(Any, payload),
296-
)
297-
except Exception as e:
298-
logger.warning(
299-
f"MarkdownV2 send failed: {e}. Using plain text instead.",
300-
)
301-
await client.send_message(text=chunk, **cast(Any, payload))
310+
await cls._send_text_chunks(client, i.text, payload)
302311
elif isinstance(i, Image):
303312
image_path = await i.convert_to_file_path()
304313
if _is_gif(image_path):
@@ -479,18 +488,7 @@ async def _process_chain_items(
479488

480489
async def _send_final_segment(self, delta: str, payload: dict[str, Any]) -> None:
481490
"""将累积文本作为 MarkdownV2 真实消息发送,失败时回退到纯文本。"""
482-
try:
483-
markdown_text = telegramify_markdown.markdownify(
484-
delta,
485-
)
486-
await self.client.send_message(
487-
text=markdown_text,
488-
parse_mode="MarkdownV2",
489-
**cast(Any, payload),
490-
)
491-
except Exception as e:
492-
logger.warning(f"Markdown转换失败,使用普通文本: {e!s}")
493-
await self.client.send_message(text=delta, **cast(Any, payload))
491+
await self._send_text_chunks(self.client, delta, payload)
494492

495493
async def send_streaming(self, generator, use_fallback: bool = False):
496494
message_thread_id = None

tests/test_telegram_adapter.py

Lines changed: 84 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,13 @@
1414
from tests.fixtures.mocks.telegram import create_mock_telegram_modules
1515

1616
_TELEGRAM_PLATFORM_ADAPTER = None
17+
_TELEGRAM_PLATFORM_EVENT = None
18+
_TELEGRAM_MODULES: dict[str, object] = {}
1719

1820

19-
def _load_telegram_adapter():
20-
global _TELEGRAM_PLATFORM_ADAPTER
21-
if _TELEGRAM_PLATFORM_ADAPTER is not None:
22-
return _TELEGRAM_PLATFORM_ADAPTER
23-
21+
def _build_telegram_patched_modules():
2422
mocks = create_mock_telegram_modules()
25-
patched_modules = {
23+
return {
2624
"telegram": mocks["telegram"],
2725
"telegram.constants": mocks["telegram"].constants,
2826
"telegram.error": mocks["telegram"].error,
@@ -33,12 +31,41 @@ def _load_telegram_adapter():
3331
"apscheduler.schedulers.asyncio": mocks["apscheduler"].schedulers.asyncio,
3432
"apscheduler.schedulers.background": mocks["apscheduler"].schedulers.background,
3533
}
36-
with patch.dict(sys.modules, patched_modules):
37-
sys.modules.pop("astrbot.core.platform.sources.telegram.tg_adapter", None)
38-
module = importlib.import_module("astrbot.core.platform.sources.telegram.tg_adapter")
39-
_TELEGRAM_PLATFORM_ADAPTER = module.TelegramPlatformAdapter
34+
35+
36+
def _load_telegram_module(module_name: str):
37+
module = _TELEGRAM_MODULES.get(module_name)
38+
if module is not None:
39+
return module
40+
41+
with patch.dict(sys.modules, _build_telegram_patched_modules()):
42+
sys.modules.pop(module_name, None)
43+
module = importlib.import_module(module_name)
44+
45+
sys.modules[module_name] = module
46+
_TELEGRAM_MODULES[module_name] = module
47+
return module
48+
49+
50+
def _load_telegram_adapter():
51+
global _TELEGRAM_PLATFORM_ADAPTER
52+
if _TELEGRAM_PLATFORM_ADAPTER is not None:
4053
return _TELEGRAM_PLATFORM_ADAPTER
4154

55+
module = _load_telegram_module("astrbot.core.platform.sources.telegram.tg_adapter")
56+
_TELEGRAM_PLATFORM_ADAPTER = module.TelegramPlatformAdapter
57+
return _TELEGRAM_PLATFORM_ADAPTER
58+
59+
60+
def _load_telegram_platform_event():
61+
global _TELEGRAM_PLATFORM_EVENT
62+
if _TELEGRAM_PLATFORM_EVENT is not None:
63+
return _TELEGRAM_PLATFORM_EVENT
64+
65+
module = _load_telegram_module("astrbot.core.platform.sources.telegram.tg_event")
66+
_TELEGRAM_PLATFORM_EVENT = module.TelegramPlatformEvent
67+
return _TELEGRAM_PLATFORM_EVENT
68+
4269

4370
def _build_context() -> MagicMock:
4471
context = MagicMock()
@@ -71,8 +98,7 @@ async def test_telegram_document_caption_populates_message_text_and_plain():
7198
assert result.message_str == "@alice 请总结这份文档"
7299
assert any(isinstance(component, Comp.File) for component in result.message)
73100
assert any(
74-
isinstance(component, Comp.Plain)
75-
and component.text == "@alice 请总结这份文档"
101+
isinstance(component, Comp.Plain) and component.text == "@alice 请总结这份文档"
76102
for component in result.message
77103
)
78104
assert any(
@@ -140,3 +166,49 @@ async def test_telegram_voice_message_creates_record_component(tmp_path):
140166
assert result.message[0].file == str(wav_path)
141167
assert result.message[0].path == str(wav_path)
142168
assert result.message[0].url == str(wav_path)
169+
170+
171+
@pytest.mark.asyncio
172+
async def test_telegram_final_segment_splits_long_markdown_messages():
173+
TelegramPlatformEvent = _load_telegram_platform_event()
174+
client = MagicMock()
175+
client.send_message = AsyncMock()
176+
event = TelegramPlatformEvent("msg", MagicMock(), MagicMock(), "session", client)
177+
178+
delta = "A" * (TelegramPlatformEvent.MAX_MESSAGE_LENGTH + 32)
179+
payload = {"chat_id": "123456"}
180+
181+
await event._send_final_segment(delta, payload)
182+
183+
assert client.send_message.await_count == 2
184+
first_call = client.send_message.await_args_list[0].kwargs
185+
second_call = client.send_message.await_args_list[1].kwargs
186+
assert len(first_call["text"]) == TelegramPlatformEvent.MAX_MESSAGE_LENGTH
187+
assert len(second_call["text"]) == 32
188+
assert first_call["parse_mode"] == "MarkdownV2"
189+
assert second_call["parse_mode"] == "MarkdownV2"
190+
191+
192+
@pytest.mark.asyncio
193+
async def test_telegram_final_segment_splits_long_plaintext_when_markdown_fails():
194+
TelegramPlatformEvent = _load_telegram_platform_event()
195+
client = MagicMock()
196+
client.send_message = AsyncMock()
197+
event = TelegramPlatformEvent("msg", MagicMock(), MagicMock(), "session", client)
198+
199+
delta = "B" * (TelegramPlatformEvent.MAX_MESSAGE_LENGTH + 18)
200+
payload = {"chat_id": "123456"}
201+
202+
with patch(
203+
"astrbot.core.platform.sources.telegram.tg_event.telegramify_markdown.markdownify",
204+
side_effect=Exception("boom"),
205+
):
206+
await event._send_final_segment(delta, payload)
207+
208+
assert client.send_message.await_count == 2
209+
first_call = client.send_message.await_args_list[0].kwargs
210+
second_call = client.send_message.await_args_list[1].kwargs
211+
assert len(first_call["text"]) == TelegramPlatformEvent.MAX_MESSAGE_LENGTH
212+
assert len(second_call["text"]) == 18
213+
assert "parse_mode" not in first_call
214+
assert "parse_mode" not in second_call

0 commit comments

Comments
 (0)