diff --git a/cli-config.yaml.example b/cli-config.yaml.example index 8ce9ad8e19a..b1e46947953 100644 --- a/cli-config.yaml.example +++ b/cli-config.yaml.example @@ -719,6 +719,7 @@ platform_toolsets: # # allowed_chats: ["-1001234567890"] # extra: # disable_link_previews: false # Set true to suppress Telegram URL previews in bot messages +# rich_messages: true # Set false to force legacy MarkdownV2 for clients that cannot render Bot API rich messages # # Discord-specific settings (config.yaml top-level, not under platforms:): # diff --git a/gateway/platforms/telegram.py b/gateway/platforms/telegram.py index b8741edab99..314f7249e8a 100644 --- a/gateway/platforms/telegram.py +++ b/gateway/platforms/telegram.py @@ -421,6 +421,9 @@ class TelegramAdapter(BasePlatformAdapter): self._disable_link_previews: bool = self._coerce_bool_extra("disable_link_previews", False) # Bot API 10.1 Rich Messages: send final replies via sendRichMessage # with the raw agent markdown so tables/task lists/etc. render natively. + # Enabled by default; users can opt out for clients that accept but do + # not render rich messages via platforms.telegram.extra.rich_messages. + self._rich_messages_enabled: bool = self._coerce_bool_extra("rich_messages", True) # Latched off after a capability failure on sendRichMessage / # sendRichMessageDraft (e.g. older python-telegram-bot without the # endpoint) so later sends skip the doomed rich attempt entirely. @@ -954,7 +957,8 @@ class TelegramAdapter(BasePlatformAdapter): self, content: str, metadata: Optional[Dict[str, Any]] = None ) -> bool: return bool( - not getattr(self, "_rich_send_disabled", False) + getattr(self, "_rich_messages_enabled", True) + and not getattr(self, "_rich_send_disabled", False) and not (metadata or {}).get("expect_edits") and content and content.strip() @@ -995,7 +999,8 @@ class TelegramAdapter(BasePlatformAdapter): streams split exactly as before. """ if ( - not getattr(self, "_rich_send_disabled", False) + getattr(self, "_rich_messages_enabled", True) + and not getattr(self, "_rich_send_disabled", False) and self._bot_supports_rich() ): return self.RICH_MESSAGE_MAX_CHARS @@ -1184,7 +1189,8 @@ class TelegramAdapter(BasePlatformAdapter): def _should_attempt_rich_draft(self, content: str) -> bool: return bool( - not getattr(self, "_rich_send_disabled", False) + getattr(self, "_rich_messages_enabled", True) + and not getattr(self, "_rich_send_disabled", False) and not getattr(self, "_rich_draft_disabled", False) and content and content.strip() diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 5e6f75b8f51..390970daf10 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -1982,6 +1982,9 @@ DEFAULT_CONFIG = { "reactions": False, # Add 👀/✅/❌ reactions to messages during processing "channel_prompts": {}, # Per-chat/topic ephemeral system prompts (topics inherit from parent group) "allowed_chats": "", # If set, bot ONLY responds in these group/supergroup chat IDs (whitelist) + "extra": { + "rich_messages": True, # Bot API 10.1 rich messages; set false to force legacy MarkdownV2 + }, }, # Mattermost platform settings (gateway mode) diff --git a/tests/gateway/test_config.py b/tests/gateway/test_config.py index 267e2a20214..cae1d8e13e1 100644 --- a/tests/gateway/test_config.py +++ b/tests/gateway/test_config.py @@ -813,6 +813,37 @@ class TestLoadGatewayConfig: assert config.platforms[Platform.TELEGRAM].extra["disable_link_previews"] is True + def test_loads_telegram_rich_messages_from_gateway_platform_extra(self, tmp_path, monkeypatch): + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + config_path = hermes_home / "config.yaml" + config_path.write_text( + "gateway:\n" + " platforms:\n" + " telegram:\n" + " extra:\n" + " rich_messages: false\n", + encoding="utf-8", + ) + + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + + config = load_gateway_config() + + assert config.platforms[Platform.TELEGRAM].extra["rich_messages"] is False + + def test_load_config_default_includes_telegram_rich_messages(self, tmp_path, monkeypatch): + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + + from hermes_cli.config import load_config + + config = load_config() + + assert config["telegram"]["extra"]["rich_messages"] is True + def test_bridges_telegram_extra_base_url_from_config_yaml(self, tmp_path, monkeypatch): hermes_home = tmp_path / ".hermes" hermes_home.mkdir() diff --git a/tests/gateway/test_telegram_rich_messages.py b/tests/gateway/test_telegram_rich_messages.py index 0e2efe45caf..54827111a75 100644 --- a/tests/gateway/test_telegram_rich_messages.py +++ b/tests/gateway/test_telegram_rich_messages.py @@ -106,16 +106,42 @@ async def test_rich_happy_path_sends_raw_markdown(): @pytest.mark.asyncio -async def test_legacy_rich_messages_config_is_ignored(): +async def test_rich_messages_opt_out_uses_legacy_send_path(): adapter = _make_adapter(extra={"rich_messages": False}) result = await adapter.send("12345", RICH_CONTENT) assert result.success is True - # The legacy toggle was removed; stale config entries must not disable the - # rich path. - adapter._bot.do_api_request.assert_awaited_once() - adapter._bot.send_message.assert_not_called() + bot = adapter._bot + assert bot is not None + bot.do_api_request.assert_not_called() + bot.send_message.assert_awaited() + + +@pytest.mark.asyncio +async def test_rich_messages_opt_out_accepts_string_false(): + adapter = _make_adapter(extra={"rich_messages": "false"}) + + result = await adapter.send("12345", RICH_CONTENT) + + assert result.success is True + bot = adapter._bot + assert bot is not None + bot.do_api_request.assert_not_called() + bot.send_message.assert_awaited() + + +@pytest.mark.asyncio +async def test_rich_messages_default_is_enabled(): + adapter = _make_adapter() + + result = await adapter.send("12345", RICH_CONTENT) + + assert result.success is True + bot = adapter._bot + assert bot is not None + bot.do_api_request.assert_awaited_once() + bot.send_message.assert_not_called() @pytest.mark.asyncio @@ -441,9 +467,9 @@ def test_prefers_fresh_final_streaming_when_rich_enabled(): assert adapter.prefers_fresh_final_streaming(RICH_CONTENT) is True -def test_prefers_fresh_final_streaming_ignores_legacy_toggle(): +def test_prefers_fresh_final_streaming_honors_rich_opt_out(): adapter = _make_adapter(extra={"rich_messages": False}) - assert adapter.prefers_fresh_final_streaming(RICH_CONTENT) is True + assert adapter.prefers_fresh_final_streaming(RICH_CONTENT) is False # ---------------------------------------------------------------------- @@ -456,12 +482,25 @@ def test_streaming_overflow_limit_is_rich_cap_when_enabled(): assert adapter.streaming_overflow_limit() == TelegramAdapter.RICH_MESSAGE_MAX_CHARS -def test_streaming_overflow_limit_ignores_legacy_toggle(): +def test_streaming_overflow_limit_none_when_rich_opted_out(): adapter = _make_adapter(extra={"rich_messages": False}) - assert adapter.streaming_overflow_limit() == TelegramAdapter.RICH_MESSAGE_MAX_CHARS + assert adapter.streaming_overflow_limit() is None def test_streaming_overflow_limit_none_when_rich_latched_off(): adapter = _make_adapter() adapter._rich_send_disabled = True assert adapter.streaming_overflow_limit() is None + + +@pytest.mark.asyncio +async def test_rich_draft_opt_out_uses_legacy(): + adapter = _make_adapter(extra={"rich_messages": False}) + + result = await adapter.send_draft("12345", draft_id=7, content=RICH_CONTENT) + + assert result.success is True + bot = adapter._bot + assert bot is not None + bot.do_api_request.assert_not_called() + bot.send_message_draft.assert_awaited_once() diff --git a/website/docs/user-guide/messaging/telegram.md b/website/docs/user-guide/messaging/telegram.md index 31aeac88ae9..3f35d1689cb 100644 --- a/website/docs/user-guide/messaging/telegram.md +++ b/website/docs/user-guide/messaging/telegram.md @@ -909,7 +909,17 @@ The rich path is skipped automatically when content exceeds the 32,768-byte rich - **Small tables** are flattened into **row-group bullets** — each row becomes a readable bulleted list under the column headings. Good for 2–4 columns and short cells. - **Larger or wider tables** fall back to a **fenced code block** with aligned columns so nothing collapses. -There's nothing to configure for the fallback — the adapter picks the right rendering per message. If you want the legacy "always code-block" behavior, disable table normalization by setting `telegram.pretty_tables: false` in `config.yaml` (default: `true`). +There's nothing to configure for the API fallback — the adapter picks the right rendering per message. If a Telegram client accepts but cannot render rich messages (for example, a watch client that shows them as opaque media blocks), opt out and force the MarkdownV2 path: + +```yaml +gateway: + platforms: + telegram: + extra: + rich_messages: false +``` + +Rich messages are enabled by default (`rich_messages: true`). This setting is for client-rendering compatibility; Hermes already falls back automatically when Telegram rejects the rich API call. If you only want the legacy "always code-block" table behavior while keeping rich messages enabled, disable table normalization by setting `telegram.pretty_tables: false` in `config.yaml` (default: `true`). **Link previews.** Telegram auto-generates link previews for URLs in bot messages. If you'd rather suppress those (long `/tools` output, agent reply that mentions ten links, etc.): diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/telegram.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/telegram.md index 0a5503e9f7d..df92624dd73 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/telegram.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/telegram.md @@ -886,7 +886,17 @@ gateway: - **小表格**被展平为**行组项目符号**——每行在列标题下变为可读的项目符号列表。适合 2-4 列和短单元格。 - **较大或较宽的表格**回退为带对齐列的**围栏代码块**,以防内容折叠。 -回退无需配置——适配器会为每条消息选择正确的渲染方式。如果你想要旧版"始终使用代码块"行为,可在 `config.yaml` 中设置 `telegram.pretty_tables: false` 禁用表格规范化(默认:`true`)。 +API 回退无需配置——适配器会为每条消息选择正确的渲染方式。如果某个 Telegram 客户端能接收但不能渲染富消息(例如手表客户端把它们显示为不透明媒体块),可以选择退出并强制使用 MarkdownV2 路径: + +```yaml +gateway: + platforms: + telegram: + extra: + rich_messages: false +``` + +富消息默认启用(`rich_messages: true`)。这个设置用于客户端渲染兼容性;当 Telegram 拒绝富消息 API 调用时,Hermes 已经会自动回退。如果你只是想在保持富消息启用的同时恢复旧版「始终使用代码块」表格行为,可在 `config.yaml` 中设置 `telegram.pretty_tables: false` 禁用表格规范化(默认:`true`)。 **链接预览。** Telegram 会为机器人消息中的 URL 自动生成链接预览。如果你希望抑制这些预览(长 `/tools` 输出、提及十个链接的 Agent 回复等):