Fix Telegram presentation-only payload sends (#82449) · openclaw/openclaw@51f4a5e
joshavant
·
2026-05-16
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -53,6 +53,7 @@ Docs: https://docs.openclaw.ai
|
53 | 53 | - Providers/embeddings: reject malformed successful OpenAI-compatible, Google Gemini, and Amazon Bedrock embedding responses instead of silently returning empty or coerced vectors. |
54 | 54 | - Providers/catalogs: reject malformed successful LM Studio, GitHub Copilot, DeepInfra, Vercel AI Gateway, and Kilocode model-list responses with provider-owned errors instead of raw parser/type failures or silent fallback catalogs. |
55 | 55 | - Providers/polling: reject array, null, or scalar successful operation status responses with provider-owned malformed JSON errors instead of waiting until timeout. |
| 56 | +- Telegram: send presentation-only payloads by rendering fallback text and inline buttons instead of treating them as empty. Fixes #82404. (#82449) Thanks @joshavant. |
56 | 57 | - Trajectory export: skip and report malformed session/runtime JSONL rows in `manifest.json` instead of letting wrong-shaped session rows crash support bundle export. |
57 | 58 | - Voice calls: persist rejected inbound-call replay keys so duplicate carrier webhook retries stay ignored after a Gateway restart. |
58 | 59 | - Config/doctor: copy fallback-enabled channel `allowFrom` entries into explicit `groupAllowFrom` allowlists during `openclaw doctor --fix`, preserving current group access without adding runtime fallback-transition flags. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -10,6 +10,10 @@ import {
|
10 | 10 | toPluginMessageSentEvent, |
11 | 11 | } from "openclaw/plugin-sdk/hook-runtime"; |
12 | 12 | import type { ReplyPayloadDelivery } from "openclaw/plugin-sdk/interactive-runtime"; |
| 13 | +import { |
| 14 | +normalizeMessagePresentation, |
| 15 | +presentationToInteractiveReply, |
| 16 | +} from "openclaw/plugin-sdk/interactive-runtime"; |
13 | 17 | import { |
14 | 18 | buildOutboundMediaLoadOptions, |
15 | 19 | isGifMedia, |
@@ -755,10 +759,15 @@ export async function deliverReplies(params: {
|
755 | 759 | ? [reply.mediaUrl] |
756 | 760 | : []; |
757 | 761 | const hasMedia = mediaList.length > 0; |
| 762 | +const presentation = normalizeMessagePresentation(reply?.presentation); |
| 763 | +const interactive = |
| 764 | +reply?.interactive ?? |
| 765 | +(presentation ? presentationToInteractiveReply(presentation) : undefined); |
758 | 766 | const resolvedReplyText = |
759 | 767 | resolveTelegramInteractiveTextFallback({ |
760 | 768 | text: reply?.text, |
761 | | -interactive: reply?.interactive, |
| 769 | + interactive, |
| 770 | + presentation, |
762 | 771 | }) ?? |
763 | 772 | reply?.text ?? |
764 | 773 | ""; |
@@ -820,7 +829,7 @@ export async function deliverReplies(params: {
|
820 | 829 | const replyMarkup = buildInlineKeyboard( |
821 | 830 | resolveTelegramInlineButtons({ |
822 | 831 | buttons: telegramData?.buttons, |
823 | | -interactive: reply.interactive, |
| 832 | + interactive, |
824 | 833 | }), |
825 | 834 | ); |
826 | 835 | let firstDeliveredMessageId: number | undefined; |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -328,6 +328,33 @@ describe("deliverReplies", () => {
|
328 | 328 | }); |
329 | 329 | }); |
330 | 330 | |
| 331 | +it("uses presentation button labels as fallback text for presentation-only replies", async () => { |
| 332 | +const runtime = createRuntime(false); |
| 333 | +const sendMessage = vi.fn().mockResolvedValue({ message_id: 4, chat: { id: "123" } }); |
| 334 | +const bot = createBot({ sendMessage }); |
| 335 | + |
| 336 | +await deliverWith({ |
| 337 | +replies: [ |
| 338 | +{ |
| 339 | +presentation: { |
| 340 | +blocks: [{ type: "buttons", buttons: [{ label: "Retry", value: "cmd:retry" }] }], |
| 341 | +}, |
| 342 | +}, |
| 343 | +], |
| 344 | + runtime, |
| 345 | + bot, |
| 346 | +}); |
| 347 | + |
| 348 | +expect(runtime.error).not.toHaveBeenCalled(); |
| 349 | +expect(firstMockCallArg(sendMessage, 0)).toBe("123"); |
| 350 | +expect(firstMockCallArg(sendMessage, 1)).toContain("Retry"); |
| 351 | +expectRecordFields(mockCallArg(sendMessage, 0, 2), { |
| 352 | +reply_markup: { |
| 353 | +inline_keyboard: [[{ text: "Retry", callback_data: "cmd:retry" }]], |
| 354 | +}, |
| 355 | +}); |
| 356 | +}); |
| 357 | + |
331 | 358 | it("reports message_sent success=false when hooks blank out a text-only reply", async () => { |
332 | 359 | messageHookRunner.hasHooks.mockImplementation( |
333 | 360 | (name: string) => name === "message_sending" || name === "message_sent", |
|
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | 1 | import { |
2 | 2 | interactiveReplyToPresentation, |
| 3 | +normalizeMessagePresentation, |
3 | 4 | normalizeInteractiveReply, |
4 | 5 | renderMessagePresentationFallbackText, |
5 | 6 | resolveInteractiveTextFallback, |
@@ -8,6 +9,7 @@ import {
|
8 | 9 | export function resolveTelegramInteractiveTextFallback(params: { |
9 | 10 | text?: string | null; |
10 | 11 | interactive?: unknown; |
| 12 | +presentation?: unknown; |
11 | 13 | }): string | undefined { |
12 | 14 | const interactive = normalizeInteractiveReply(params.interactive); |
13 | 15 | const text = resolveInteractiveTextFallback({ |
@@ -17,13 +19,23 @@ export function resolveTelegramInteractiveTextFallback(params: {
|
17 | 19 | if (text?.trim()) { |
18 | 20 | return text; |
19 | 21 | } |
| 22 | +const presentation = normalizeMessagePresentation(params.presentation); |
| 23 | +if (presentation) { |
| 24 | +const fallback = renderMessagePresentationFallbackText({ |
| 25 | +text: params.text ?? undefined, |
| 26 | + presentation, |
| 27 | +}); |
| 28 | +if (fallback.trim()) { |
| 29 | +return fallback; |
| 30 | +} |
| 31 | +} |
20 | 32 | if (!interactive) { |
21 | 33 | return text; |
22 | 34 | } |
23 | | -const presentation = interactiveReplyToPresentation(interactive); |
24 | | -if (!presentation) { |
| 35 | +const interactivePresentation = interactiveReplyToPresentation(interactive); |
| 36 | +if (!interactivePresentation) { |
25 | 37 | return text; |
26 | 38 | } |
27 | | -const fallback = renderMessagePresentationFallbackText({ presentation }); |
| 39 | +const fallback = renderMessagePresentationFallbackText({ presentation: interactivePresentation }); |
28 | 40 | return fallback.trim() ? fallback : text; |
29 | 41 | } |
| Original file line number | Diff line number | Diff line change |
|---|
@@ -150,6 +150,33 @@ describe("telegramOutbound", () => {
|
150 | 150 | expect(result).toEqual({ channel: "telegram", messageId: "tg-buttons", chatId: "12345" }); |
151 | 151 | }); |
152 | 152 | |
| 153 | +it("uses presentation button labels as fallback text for presentation-only payloads", async () => { |
| 154 | +sendMessageTelegramMock.mockResolvedValueOnce({ |
| 155 | +messageId: "tg-presentation-buttons", |
| 156 | +chatId: "12345", |
| 157 | +}); |
| 158 | + |
| 159 | +const result = await telegramOutbound.sendPayload!({ |
| 160 | +cfg: {} as never, |
| 161 | +to: "12345", |
| 162 | +text: "", |
| 163 | +payload: { |
| 164 | +presentation: { |
| 165 | +blocks: [{ type: "buttons", buttons: [{ label: "Retry", value: "cmd:retry" }] }], |
| 166 | +}, |
| 167 | +}, |
| 168 | +deps: { sendTelegram: sendMessageTelegramMock }, |
| 169 | +}); |
| 170 | + |
| 171 | +const options = callOptionsAt(sendMessageTelegramMock, 0, "12345", "- Retry"); |
| 172 | +expect(options.buttons).toEqual([[{ text: "Retry", callback_data: "cmd:retry" }]]); |
| 173 | +expect(result).toEqual({ |
| 174 | +channel: "telegram", |
| 175 | +messageId: "tg-presentation-buttons", |
| 176 | +chatId: "12345", |
| 177 | +}); |
| 178 | +}); |
| 179 | + |
153 | 180 | it("renders presentation web app buttons for payload sends", async () => { |
154 | 181 | sendMessageTelegramMock.mockResolvedValueOnce({ messageId: "tg-web-app", chatId: "12345" }); |
155 | 182 | const presentation = { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -4,6 +4,7 @@ import {
|
4 | 4 | createAttachedChannelResultAdapter, |
5 | 5 | } from "openclaw/plugin-sdk/channel-send-result"; |
6 | 6 | import { |
| 7 | +normalizeMessagePresentation, |
7 | 8 | presentationToInteractiveReply, |
8 | 9 | renderMessagePresentationFallbackText, |
9 | 10 | } from "openclaw/plugin-sdk/interactive-runtime"; |
@@ -116,15 +117,20 @@ export async function sendTelegramPayloadMessages(params: {
|
116 | 117 | | undefined; |
117 | 118 | const quoteText = |
118 | 119 | typeof telegramData?.quoteText === "string" ? telegramData.quoteText : undefined; |
| 120 | +const presentation = normalizeMessagePresentation(params.payload.presentation); |
| 121 | +const interactive = |
| 122 | +params.payload.interactive ?? |
| 123 | +(presentation ? presentationToInteractiveReply(presentation) : undefined); |
119 | 124 | const text = |
120 | 125 | resolveTelegramInteractiveTextFallback({ |
121 | 126 | text: params.payload.text, |
122 | | -interactive: params.payload.interactive, |
| 127 | + interactive, |
| 128 | + presentation, |
123 | 129 | }) ?? ""; |
124 | 130 | const mediaUrls = resolvePayloadMediaUrls(params.payload); |
125 | 131 | const buttons = resolveTelegramInlineButtons({ |
126 | 132 | buttons: telegramData?.buttons, |
127 | | -interactive: params.payload.interactive, |
| 133 | + interactive, |
128 | 134 | }); |
129 | 135 | const payloadOpts = { |
130 | 136 | ...params.baseOpts, |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。