diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index 7d3a934f09fc..ef432132c1d9 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -369,9 +369,9 @@ curl "https://api.telegram.org/bot/getUpdates" - Outbound text uses standard Telegram HTML messages by default, readable across current clients: bold, italic, links, code, spoilers, quotes — not Bot API 10.1 rich-only blocks (native tables, details, rich media, formulas). + Outbound text uses standard Telegram HTML messages by default, readable across current clients: bold, italic, links, code, spoilers, quotes — not Bot API 10.2 rich-only blocks (native tables, details, rich media, formulas). - Opt into Bot API 10.1 rich messages: + Opt into Bot API 10.2 rich messages: ```json5 { @@ -383,7 +383,7 @@ curl "https://api.telegram.org/bot/getUpdates" } ``` - When enabled: the agent is told rich messages are available for this bot/account; Markdown text renders through OpenClaw's Markdown IR as Telegram rich HTML; explicit rich HTML payloads preserve supported Bot API 10.1 tags (headings, tables, details, rich media, formulas); media captions still use Telegram HTML captions (rich messages do not replace captions, and captions cap at 1024 characters). + When enabled: the agent is told rich messages are available for this bot/account (with the supported Markdown + HTML-island authoring contract); Markdown text renders through OpenClaw's Markdown IR as typed Bot API 10.2 rich blocks (headings, tables, details, checklists, rich media, formulas, maps, collages); media captions still use Telegram HTML captions (rich messages do not replace captions, and captions cap at 1024 characters). This keeps model text away from Telegram's rich-Markdown sigils, so currency like `$400-600K` is not parsed as math. Long rich text splits automatically across Telegram's limits. Tables over the 20-column limit fall back to a code block. diff --git a/extensions/telegram/src/channel-actions.contract.test.ts b/extensions/telegram/src/channel-actions.contract.test.ts index cc884171012e..29504761f3d3 100644 --- a/extensions/telegram/src/channel-actions.contract.test.ts +++ b/extensions/telegram/src/channel-actions.contract.test.ts @@ -24,23 +24,64 @@ describe("telegram actions contract", () => { }); it.each([ - { richMessages: undefined, expected: false }, - { richMessages: false, expected: false }, - { richMessages: true, expected: true }, - ])("advertises Telegram rich text only when enabled", ({ richMessages, expected }) => { + { + richMessages: undefined as boolean | undefined, + expectedMarkup: "markdown", + expectedOn: false, + }, + { + richMessages: false as boolean | undefined, + expectedMarkup: "markdown", + expectedOn: false, + }, + { + richMessages: true as boolean | undefined, + expectedMarkup: "markdown_telegram_rich", + expectedOn: true, + }, + ])( + "returns inbound formatting hints for richMessages=$richMessages", + ({ richMessages, expectedMarkup, expectedOn }) => { + const hints = telegramPlugin.agentPrompt?.inboundFormattingHints?.({ + cfg: { + channels: { + telegram: { + botToken: "test-token-placeholder", + richMessages, + }, + }, + } as OpenClawConfig, + }); + + expect(hints?.text_markup).toBe(expectedMarkup); + if (expectedOn) { + expect(hints?.rules.join(" ")).toContain("Telegram rich ON"); + expect(hints?.rules.join(" ")).toContain("Bot API 10.2 blocks"); + expect(hints?.rules.join(" ")).toContain("
"); + expect(hints?.rules.join(" ")).toContain("Not MarkdownV2/parse_mode"); + expect(hints?.rules.join(" ")).toContain("Media https URLs only, block-level only"); + } else { + expect(hints?.rules.join(" ")).toContain("Telegram rich OFF"); + expect(hints?.rules.join(" ")).toContain("richMessages"); + expect(hints?.rules.join(" ")).not.toContain("Telegram rich ON"); + } + }, + ); + + it("does not advertise a richText message-tool capability", () => { const capabilities = telegramPlugin.agentPrompt?.messageToolCapabilities?.({ cfg: { channels: { telegram: { botToken: "test-token-placeholder", - richMessages, + richMessages: true, }, }, } as OpenClawConfig, }); expect(capabilities).toContain("inlineButtons"); - expect(capabilities?.includes("richText")).toBe(expected); + expect(capabilities).not.toContain("richText"); }); it("advertises inline buttons when legacy Telegram capabilities are empty", () => { @@ -89,8 +130,8 @@ describe("telegram actions contract", () => { expect(capabilities).not.toContain("inlineButtons"); }); - it("uses the selected Telegram account's rich text setting", () => { - const capabilities = telegramPlugin.agentPrompt?.messageToolCapabilities?.({ + it("uses the selected Telegram account's richMessages for inbound formatting hints", () => { + const hints = telegramPlugin.agentPrompt?.inboundFormattingHints?.({ cfg: { channels: { telegram: { @@ -107,12 +148,13 @@ describe("telegram actions contract", () => { accountId: "ops", }); - expect(capabilities).not.toContain("richText"); + expect(hints?.text_markup).toBe("markdown"); + expect(hints?.rules.join(" ")).toContain("Telegram rich OFF"); }); - it("does not resolve Telegram credentials while checking prompt capabilities", () => { + it("does not resolve Telegram credentials while checking inbound formatting hints", () => { expect(() => - telegramPlugin.agentPrompt?.messageToolCapabilities?.({ + telegramPlugin.agentPrompt?.inboundFormattingHints?.({ cfg: { channels: { telegram: { @@ -125,8 +167,8 @@ describe("telegram actions contract", () => { ).not.toThrow(); }); - it("uses the configured default Telegram account for prompt capabilities", () => { - const capabilities = telegramPlugin.agentPrompt?.messageToolCapabilities?.({ + it("uses the configured default Telegram account for inbound formatting hints", () => { + const hints = telegramPlugin.agentPrompt?.inboundFormattingHints?.({ cfg: { channels: { telegram: { @@ -146,7 +188,8 @@ describe("telegram actions contract", () => { } as OpenClawConfig, }); - expect(capabilities).toContain("richText"); + expect(hints?.text_markup).toBe("markdown_telegram_rich"); + expect(hints?.rules.join(" ")).toContain("Telegram rich ON"); }); it("exposes Telegram thread create CLI remapping through the exported plugin", () => { diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index 2c80c1ea3ab3..566e7dbf5263 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -815,12 +815,34 @@ export const telegramPlugin = createChatChannelPlugin({ cfg, accountId: accountId ?? undefined, }); - const capabilities = inlineButtonsScope === "off" ? [] : ["inlineButtons"]; + return inlineButtonsScope === "off" ? [] : ["inlineButtons"]; + }, + // Authoring contract lives here so every runtime (including native Codex) + // sees it via inbound-meta response_format; core system-prompt no longer owns it. + inboundFormattingHints: ({ cfg, accountId }) => { const selectedAccountId = accountId ?? resolveDefaultTelegramAccountId(cfg); - if (mergeTelegramAccountConfig(cfg, selectedAccountId).richMessages === true) { - capabilities.push("richText"); + const richMessages = + mergeTelegramAccountConfig(cfg, selectedAccountId).richMessages === true; + if (richMessages) { + return { + text_markup: "markdown_telegram_rich", + rules: [ + "Telegram rich ON (Bot API 10.2 blocks; OpenClaw maps markdown + these HTML islands to typed blocks).", + 'Supported: headings, tables (markdown, or `` HTML for caption/colspan/rowspan/align), block/pull quotes (`
` HTML for caption/colspan/rowspan/align), block/pull quotes (`