Telegram CJK rich message rendering fixes
NousResearch/hermes-agent · cluster 47653 · gateway
Data as of
Nothing open on GitHub. Fix merged in #50222. Check it covers your case.
The short answer
- Known?
- Yes. 6 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #50222. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #50222. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These issues and pull requests address garbled or overlapping CJK character rendering in Telegram rich messages, including fallback handling, opt-out configuration options, and separate gating for draft previews.
Open issues0
Open PRs0
Fixes on record
-
#50222 fix(telegram): avoid rich messages for CJK text (#47653)
files:
plugins/platforms/telegram/adapter.py,tests/gateway/test_telegram_rich_messages.pyShow the diff
-
plugins/platforms/telegram/adapter.py@@ -1048,6 +1048,16 @@ def _bot_supports_rich(self) -> bool: r"int|prod|sqrt|lim|infty|begin\{(?:equation|align|matrix|cases)\}))", re.IGNORECASE | re.DOTALL, ) + _RICH_CJK_RE = re.compile( + "[" + "\u3040-\u30ff" # Hiragana, Katakana + "\u3400-\u4dbf" # CJK Extension A + "\u4e00-\u9fff" # CJK Unified Ideographs + "\uac00-\ud7af" # Hangul syllables + "\uf900-\ufaff" # CJK Compatibility Ideographs + "\U00020000-\U000323af" # CJK extensions and compatibility supplement + "]" + ) def _has_telegram_desktop_details_math_crash_shape(self, content: str) -> bool: """Return True for rich-message details+math content that crashes TDesktop. @@ -1065,6 +1075,16 @@ def _has_telegram_desktop_details_math_crash_shape(self, content: str) -> bool: return True return False + def _has_telegram_desktop_cjk_rich_garble_shape(self, content: str) -> bool: + """Return True for CJK content that current TDesktop rich drafts garble. + + Telegram Mac/Desktop Bot API 10.1 rich-message rendering currently + leaves overlapping draft/overlay glyph artifacts for CJK text (#47653). + The legacy MarkdownV2 path renders the same text cleanly, so skip rich + delivery up front until affected clients age out. + """ + return bool(content and self._RICH_CJK_RE.search(content)) + def _needs_rich_rendering(self, content: str) -> bool: """Return True for markdown constructs that the legacy path degrades. @@ -1103,6 +1123,7 @@ def _rich_eligible(self, content: str) -> bool: and content.strip() and self._needs_rich_rendering(content) and not self._has_telegram_desktop_details_math_crash_shape(content) + and not self._has_telegram_desktop_cjk_rich_garble_shape(content) and self._content_fits_rich_limits(content) and self._bot_supports_rich() ) @@ -1424,6 +1445,7 @@ def _should_attempt_rich_draft(self, content: str) -> bool: and content and content.strip() and not self._has_telegram_desktop_details_math_crash_shape(content) + and not self._has_telegram_desktop_cjk_rich_garble_shape(content) and self._content_fits_rich_limits(content) and self._bot_supports_rich() ) -
tests/gateway/test_telegram_rich_messages.py@@ -24,6 +24,8 @@ # Content exercising rich-only constructs: a heading, a real Markdown table, # and a task list. Pipes / brackets must survive untouched into the payload. RICH_CONTENT = "## Results\n\n| Case | Status |\n|---|---|\n| rich | ✅ |\n\n- [x] table renders" +CJK_RICH_CONTENT = "## 持仓\n\n| 项目 | 状态 |\n|---|---|\n| 早盘 | 正常 |" +ASTRAL_CJK_RICH_CONTENT = "## Rare Han\n\n| glyph | status |\n|---|---|\n| \U00030000 | ok |" DANGEROUS_DETAILS_MATH = ( "<details><summary>Complex proof</summary>\n\n" "$$\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}$$\n\n" @@ -159,6 +161,28 @@ async def test_math_outside_details_still_uses_rich_send(): bot.send_message.assert_not_called() [redacted:email] +async def test_cjk_rich_content_skips_rich_send_to_avoid_tdesktop_garble(): + adapter = _make_adapter() + + result = await adapter.send("12345", CJK_RICH_CONTENT) + + assert result.success is True + adapter._bot.do_api_request.assert_not_called() + adapter._bot.send_message.assert_awaited_once() + + [redacted:email] +async def test_astral_cjk_rich_content_skips_rich_send_to_avoid_tdesktop_garble(): + adapter = _make_adapter() + + result = await adapter.send("12345", ASTRAL_CJK_RICH_CONTENT) + + assert result.success is True + adapter._bot.do_api_request.assert_not_called() + adapter._bot.send_message.assert_awaited_once() + + @pytest.mark.asyncio async def test_rich_messages_opt_out_uses_legacy_send_path(): adapter = _make_adapter(extra={"rich_messages": False}) @@ -281,13 +305,15 @@ async def test_oversized_content_skips_rich_and_chunks(): async def test_rich_limit_is_characters_not_bytes(): """Telegram's rich limit is UTF-8 characters, not encoded bytes.""" adapter = _make_adapter() - # Rich-eligible (table) so the content takes the rich path; the CJK body - # is 20k chars / 60k UTF-8 bytes — over the byte count, under the char cap. - cjk = "| a | b |\n|---|---|\n" + "测" * 20000 # 20k chars, ~60k UTF-8 bytes - assert len(cjk.encode("utf-8")) > TelegramAdapter.RICH_MESSAGE_MAX_BYTES - assert len(cjk) <= TelegramAdapter.RICH_MESSAGE_MAX_CHARS + # Rich-eligible (table) so the content takes the rich path; the accented + # body is 20k chars / 40k UTF-8 bytes — over the byte count, under the + # character cap. CJK is intentionally avoided here because affected + # Telegram Desktop clients render CJK rich drafts incorrectly. + accented = "| a | b |\n|---|---|\n" + "é" * 20000 + assert len(accented.encode("utf-8")) > TelegramAdapter.RICH_MESSAGE_MAX_BYTES + assert len(accented) <= TelegramAdapter.RICH_MESSAGE_MAX_CHARS - result = await adapter.send("12345", cjk) + result = await adapter.send("12345", accented) assert result.success is True bot = adapter._bot @@ -528,6 +554,18 @@ async def test_rich_draft_happy_path_sends_raw_markdown(): adapter._bot.send_message_draft.assert_not_called() [redacted:email] +async def test_cjk_rich_content_skips_rich_draft_to_avoid_tdesktop_garble(): + adapter = _make_adapter() + adapter._bot.do_api_request = AsyncMock(return_value=True) + + result = await adapter.send_draft("12345", draft_id=7, content=CJK_RICH_CONTENT) + + assert result.success is True + adapter._bot.do_api_request.assert_not_called() + adapter._bot.send_message_draft.assert_awaited_once() + + @pytest.mark.asyncio async def test_rich_draft_capability_failure_falls_back_and_latches_off(): adapter = _make_adapter() @@ -673,6 +711,19 @@ async def test_finalize_edit_plain_content_stays_legacy(): adapter._bot.edit_message_text.assert_awaited() [redacted:email] +async def test_finalize_edit_cjk_rich_content_stays_legacy_to_avoid_tdesktop_garble(): + adapter = _make_adapter() + + result = await adapter.edit_message( + "12345", "555", CJK_RICH_CONTENT, finalize=True, + ) + + assert result.success is True + adapter._bot.do_api_request.assert_not_called() + adapter._bot.edit_message_text.assert_awaited_once() + + @pytest.mark.asyncio async def test_finalize_edit_rich_capability_error_falls_back_to_legacy(): """A capability error on the rich edit latches rich off and falls back to
-
-
#111831 fix(telegram): CJK content can opt in to native rich messages (salvage #56155)
files:
cli-config.yaml.example,contributors/emails/[redacted:email],hermes_cli/config_defaults.py,plugins/platforms/telegram/adapter.py,tests/gateway/test_telegram_rich_messages.py,website/docs/user-guide/messaging/telegram.md,website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/telegram.md
3 PRs closed without merging
From the close comments: done elsewhere 2 · merged via another change 1
-
#47681 fix(telegram): avoid rich messages for CJK text · merged via another change →
#50222,#47653 -
#62448 feat(telegram): add config option to disable CJK rich-message guard · done elsewhere →
7e42fc3,#111831,#56155,#47653 -
#85197 fix(telegram): make CJK rich-message gate opt-out via extra.rich_cjk · done elsewhere →
7e42fc3,#111831,#56155,#47653
Closed issues · 1
- Closed#47653Bug: Mac Telegram Rich Messages render CJK characters as garbled overlapping symbols
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.33: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab