diff --git a/CHANGELOG.md b/CHANGELOG.md index 685bb183318..2e1f4de9f75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai - Feishu: use the shared channel progress formatter for streaming-card tool status lines, including raw command/detail output and message-tool filtering. Thanks @vincentkoc. - Mattermost: use the shared progress draft formatter for tool status previews, including raw command/detail output when `agents.defaults.toolProgressDetail: "raw"` is enabled. Thanks @vincentkoc. - Mattermost: suppress standalone default tool-progress messages while draft previews are active, including when draft tool lines are disabled. Thanks @vincentkoc. +- Telegram: deliver button-only interactive replies by sending the shared fallback button-label text with the inline keyboard instead of dropping the reply as empty. Thanks @vincentkoc. - OpenAI Codex: honor `auth.order.openai-codex` when starting app-server clients without an explicit auth profile, so status/model probes and implicit startup use the configured Codex account instead of falling back to the default profile. Thanks @vincentkoc. - OpenAI Codex: let SSRF-guarded provider requests inherit OpenClaw's undici IPv4/IPv6 fallback policy, so ChatGPT-backed Codex runs recover on IPv4-working hosts when DNS still returns unreachable IPv6 addresses. Fixes #76857. Thanks @jplavoiemtl and @SymbolStar. - Gateway/systemd: preserve operator-added secrets in the Gateway env file across re-stage while clearing OpenClaw-managed keys (such as `OPENCLAW_GATEWAY_TOKEN`) so a fresh staging value is never shadowed by a stale env-file copy; operator secrets are also retained when the state-dir `.env` is empty. Fixes #76860. Thanks @hclsys. diff --git a/extensions/telegram/src/bot/delivery.replies.ts b/extensions/telegram/src/bot/delivery.replies.ts index 4c09b20b88c..32388d98a77 100644 --- a/extensions/telegram/src/bot/delivery.replies.ts +++ b/extensions/telegram/src/bot/delivery.replies.ts @@ -36,6 +36,7 @@ import { renderTelegramHtmlText, wrapFileReferencesInHtml, } from "../format.js"; +import { resolveTelegramInteractiveTextFallback } from "../interactive-fallback.js"; import { buildInlineKeyboard } from "../send.js"; import { resolveTelegramVoiceSend } from "../voice.js"; import { @@ -751,7 +752,17 @@ export async function deliverReplies(params: { ? [reply.mediaUrl] : []; const hasMedia = mediaList.length > 0; - if (!reply?.text && !hasMedia) { + const resolvedReplyText = + resolveTelegramInteractiveTextFallback({ + text: reply?.text, + interactive: reply?.interactive, + }) ?? + reply?.text ?? + ""; + if (reply && resolvedReplyText !== (reply.text ?? "")) { + reply = { ...reply, text: resolvedReplyText }; + } + if (!resolvedReplyText && !hasMedia) { if (reply?.audioAsVoice) { logVerbose("telegram reply has audioAsVoice without media/text; skipping"); continue; @@ -760,7 +771,7 @@ export async function deliverReplies(params: { continue; } - const rawContent = reply.text || ""; + const rawContent = resolvedReplyText; const replyToId = params.replyToMode === "off" ? undefined : resolveTelegramReplyId(reply.replyToId); const replyQuote = resolveReplyQuoteForSend({ diff --git a/extensions/telegram/src/bot/delivery.test.ts b/extensions/telegram/src/bot/delivery.test.ts index 28965d458c0..0526e72894a 100644 --- a/extensions/telegram/src/bot/delivery.test.ts +++ b/extensions/telegram/src/bot/delivery.test.ts @@ -257,6 +257,35 @@ describe("deliverReplies", () => { ); }); + it("uses interactive button labels as fallback text for button-only replies", async () => { + const runtime = createRuntime(false); + const sendMessage = vi.fn().mockResolvedValue({ message_id: 3, chat: { id: "123" } }); + const bot = createBot({ sendMessage }); + + await deliverWith({ + replies: [ + { + interactive: { + blocks: [{ type: "buttons", buttons: [{ label: "Retry", value: "cmd:retry" }] }], + }, + }, + ], + runtime, + bot, + }); + + expect(runtime.error).not.toHaveBeenCalled(); + expect(sendMessage).toHaveBeenCalledWith( + "123", + expect.stringContaining("Retry"), + expect.objectContaining({ + reply_markup: { + inline_keyboard: [[{ text: "Retry", callback_data: "cmd:retry" }]], + }, + }), + ); + }); + it("reports message_sent success=false when hooks blank out a text-only reply", async () => { messageHookRunner.hasHooks.mockImplementation( (name: string) => name === "message_sending" || name === "message_sent",