feat(telegram): support web app presentation buttons · openclaw/openclaw@5f8e1dd
steipete
·
2026-05-13
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -71,6 +71,7 @@ Docs: https://docs.openclaw.ai
|
71 | 71 | ### Changes |
72 | 72 | |
73 | 73 | - Maintainers: add a Clawdtributor skill for Discrawl-backed contributor PR triage, live status checks, and compact review formatting. |
| 74 | +- Telegram: support Mini App `web_app` buttons in generic message presentation payloads, allowing `openclaw message send --presentation` to render Telegram Web App inline buttons for private chats. (#81356) Thanks @jzakirov. |
74 | 75 | - Scripts: add `OPENCLAW_HEAVY_CHECK_LOCK_SCOPE=worktree` so high-capacity local worktrees can use independent heavy-check locks while shared locks remain the default. Fixes #80729. (#80734) Thanks @samzong. |
75 | 76 | - Agents/subagents: deliver native `sessions_spawn` tasks in the child session's first visible `[Subagent Task]` message instead of hiding the task in the sub-agent system prompt, keeping delegation auditable without duplicating tokens. Fixes #78592. Thanks @bradestes and @stainlu. |
76 | 77 | - Voice Call/Telnyx: add realtime media-streaming call support for conversational voice calls. (#81024) Thanks @dynamite-bud. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -526,6 +526,28 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
526 | 526 | } |
527 | 527 | ``` |
528 | 528 | |
| 529 | +Mini App button example: |
| 530 | + |
| 531 | +```json5 |
| 532 | +{ |
| 533 | + action: "send", |
| 534 | + channel: "telegram", |
| 535 | + to: "123456789", |
| 536 | + message: "Open app:", |
| 537 | + presentation: { |
| 538 | + blocks: [ |
| 539 | + { |
| 540 | + type: "buttons", |
| 541 | + buttons: [{ label: "Launch", web_app: { url: "https://example.com/app" } }], |
| 542 | + }, |
| 543 | + ], |
| 544 | + }, |
| 545 | +} |
| 546 | +``` |
| 547 | + |
| 548 | +Telegram `web_app` buttons work only in private chats between a user and the |
| 549 | +bot. |
| 550 | + |
529 | 551 | Callback clicks are passed to the agent as text: |
530 | 552 | `callback_data: <value>` |
531 | 553 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -284,6 +284,16 @@ openclaw message send --channel telegram --target @mychat --message "Choose:" \
|
284 | 284 | --presentation '{"blocks":[{"type":"buttons","buttons":[{"label":"Yes","value":"cmd:yes"},{"label":"No","value":"cmd:no"}]}]}' |
285 | 285 | ``` |
286 | 286 | |
| 287 | +Send a Telegram Mini App button through generic presentation: |
| 288 | + |
| 289 | +``` |
| 290 | +openclaw message send --channel telegram --target 123456789 --message "Open app:" \ |
| 291 | + --presentation '{"blocks":[{"type":"buttons","buttons":[{"label":"Launch","web_app":{"url":"https://example.com/app"}}]}]}' |
| 292 | +``` |
| 293 | + |
| 294 | +Telegram `web_app` buttons are supported only in private chats between a user |
| 295 | +and the bot. |
| 296 | + |
287 | 297 | Send a Teams card through generic presentation: |
288 | 298 | |
289 | 299 | ```bash |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -56,6 +56,8 @@ type MessagePresentationButton = {
|
56 | 56 | label: string; |
57 | 57 | value?: string; |
58 | 58 | url?: string; |
| 59 | + webApp?: { url: string }; |
| 60 | + web_app?: { url: string }; |
59 | 61 | style?: "primary" | "secondary" | "success" | "danger"; |
60 | 62 | }; |
61 | 63 | |
@@ -80,6 +82,8 @@ Button semantics:
|
80 | 82 | - `value` is an application action value routed back through the channel's |
81 | 83 | existing interaction path when the channel supports clickable controls. |
82 | 84 | - `url` is a link button. It can exist without `value`. |
| 85 | +- `webApp` and `web_app` describe a channel-native web app button. Telegram |
| 86 | + renders this as `web_app` and only supports it in private chats. |
83 | 87 | - `label` is required and is also used in text fallback. |
84 | 88 | - `style` is advisory. Renderers should map unsupported styles to a safe |
85 | 89 | default, not fail the send. |
@@ -127,6 +131,19 @@ URL-only link button:
|
127 | 131 | } |
128 | 132 | ``` |
129 | 133 | |
| 134 | +Telegram Mini App button: |
| 135 | + |
| 136 | +```json |
| 137 | +{ |
| 138 | +"blocks": [ |
| 139 | + { |
| 140 | +"type": "buttons", |
| 141 | +"buttons": [{ "label": "Launch", "web_app": { "url": "https://example.com/app" } }] |
| 142 | + } |
| 143 | + ] |
| 144 | +} |
| 145 | +``` |
| 146 | + |
130 | 147 | Select menu: |
131 | 148 | |
132 | 149 | ```json |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1030,6 +1030,43 @@ describe("handleTelegramAction", () => {
|
1030 | 1030 | ], |
1031 | 1031 | ]); |
1032 | 1032 | }); |
| 1033 | + |
| 1034 | +it("forwards web app buttons from generic presentation", async () => { |
| 1035 | +await handleTelegramAction( |
| 1036 | +{ |
| 1037 | +action: "sendMessage", |
| 1038 | +to: "5232990709", |
| 1039 | +content: "Choose", |
| 1040 | +presentation: { |
| 1041 | +blocks: [ |
| 1042 | +{ |
| 1043 | +type: "buttons", |
| 1044 | +buttons: [ |
| 1045 | +{ |
| 1046 | +label: "Launch", |
| 1047 | +web_app: { url: "https://example.com/app" }, |
| 1048 | +style: "primary", |
| 1049 | +}, |
| 1050 | +], |
| 1051 | +}, |
| 1052 | +], |
| 1053 | +}, |
| 1054 | +}, |
| 1055 | +telegramConfig({ capabilities: { inlineButtons: "dm" } }), |
| 1056 | +); |
| 1057 | +const call = mockCall(sendMessageTelegram, 0, "inline keyboard web app"); |
| 1058 | +expect(call[0]).toBe("5232990709"); |
| 1059 | +expect(call[1]).toBe("Choose"); |
| 1060 | +expect(requireRecord(call[2], "inline keyboard web app options").buttons).toEqual([ |
| 1061 | +[ |
| 1062 | +{ |
| 1063 | +text: "Launch", |
| 1064 | +web_app: { url: "https://example.com/app" }, |
| 1065 | +style: "primary", |
| 1066 | +}, |
| 1067 | +], |
| 1068 | +]); |
| 1069 | +}); |
1033 | 1070 | }); |
1034 | 1071 | |
1035 | 1072 | describe("handleTelegramAction per-account gating", () => { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -13,6 +13,7 @@ export function describeTelegramInteractiveButtonBehavior(): void {
|
13 | 13 | { label: "Approve", value: "approve", style: "success" }, |
14 | 14 | { label: "Docs", url: "https://example.com/docs", style: "primary" }, |
15 | 15 | { label: "Reject", value: "reject", style: "danger" }, |
| 16 | +{ label: "Launch", webApp: { url: "https://example.com/app" } }, |
16 | 17 | { label: "Later", value: "later" }, |
17 | 18 | { label: "Archive", value: "archive" }, |
18 | 19 | ], |
@@ -30,6 +31,7 @@ export function describeTelegramInteractiveButtonBehavior(): void {
|
30 | 31 | { text: "Reject", callback_data: "reject", style: "danger" }, |
31 | 32 | ], |
32 | 33 | [ |
| 34 | +{ text: "Launch", web_app: { url: "https://example.com/app" }, style: undefined }, |
33 | 35 | { text: "Later", callback_data: "later", style: undefined }, |
34 | 36 | { text: "Archive", callback_data: "archive", style: undefined }, |
35 | 37 | ], |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -12,6 +12,7 @@ type TelegramInlineButton = {
|
12 | 12 | text: string; |
13 | 13 | callback_data?: string; |
14 | 14 | url?: string; |
| 15 | +web_app?: { url: string }; |
15 | 16 | style?: TelegramButtonStyle; |
16 | 17 | }; |
17 | 18 | |
@@ -35,13 +36,21 @@ function toTelegramInlineButton(button: InteractiveReplyButton): TelegramInlineB
|
35 | 36 | }; |
36 | 37 | } |
37 | 38 | const callbackData = button.value ? sanitizeTelegramCallbackData(button.value) : undefined; |
38 | | -return callbackData |
39 | | - ? { |
40 | | -text: button.label, |
41 | | -callback_data: callbackData, |
42 | | - style, |
43 | | -} |
44 | | - : undefined; |
| 39 | +if (callbackData) { |
| 40 | +return { |
| 41 | +text: button.label, |
| 42 | +callback_data: callbackData, |
| 43 | + style, |
| 44 | +}; |
| 45 | +} |
| 46 | +if (button.webApp?.url) { |
| 47 | +return { |
| 48 | +text: button.label, |
| 49 | +web_app: { url: button.webApp.url }, |
| 50 | + style, |
| 51 | +}; |
| 52 | +} |
| 53 | +return undefined; |
45 | 54 | } |
46 | 55 | |
47 | 56 | function chunkInteractiveButtons( |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -17,6 +17,11 @@ function toInlineKeyboardButton(
|
17 | 17 | ? { text: button.text, callback_data: button.callback_data, style: button.style } |
18 | 18 | : { text: button.text, callback_data: button.callback_data }; |
19 | 19 | } |
| 20 | +if (button.web_app?.url) { |
| 21 | +return button.style |
| 22 | + ? { text: button.text, web_app: { url: button.web_app.url }, style: button.style } |
| 23 | + : { text: button.text, web_app: { url: button.web_app.url } }; |
| 24 | +} |
20 | 25 | return undefined; |
21 | 26 | } |
22 | 27 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -150,6 +150,44 @@ describe("telegramOutbound", () => {
|
150 | 150 | expect(result).toEqual({ channel: "telegram", messageId: "tg-buttons", chatId: "12345" }); |
151 | 151 | }); |
152 | 152 | |
| 153 | +it("renders presentation web app buttons for payload sends", async () => { |
| 154 | +sendMessageTelegramMock.mockResolvedValueOnce({ messageId: "tg-web-app", chatId: "12345" }); |
| 155 | +const presentation = { |
| 156 | +blocks: [ |
| 157 | +{ |
| 158 | +type: "buttons" as const, |
| 159 | +buttons: [{ label: "Launch", webApp: { url: "https://example.com/app" } }], |
| 160 | +}, |
| 161 | +], |
| 162 | +}; |
| 163 | +const rendered = await telegramOutbound.renderPresentation?.({ |
| 164 | +payload: { text: "Open app:" }, |
| 165 | + presentation, |
| 166 | +ctx: {} as never, |
| 167 | +}); |
| 168 | +if (!rendered) { |
| 169 | +throw new Error("expected rendered Telegram presentation"); |
| 170 | +} |
| 171 | + |
| 172 | +await telegramOutbound.sendPayload!({ |
| 173 | +cfg: {} as never, |
| 174 | +to: "12345", |
| 175 | +text: "", |
| 176 | +payload: rendered, |
| 177 | +deps: { sendTelegram: sendMessageTelegramMock }, |
| 178 | +}); |
| 179 | + |
| 180 | +const options = callOptionsAt( |
| 181 | +sendMessageTelegramMock, |
| 182 | +0, |
| 183 | +"12345", |
| 184 | +"Open app:\n\n- Launch: https://example.com/app", |
| 185 | +); |
| 186 | +expect(options.buttons).toEqual([ |
| 187 | +[{ text: "Launch", web_app: { url: "https://example.com/app" } }], |
| 188 | +]); |
| 189 | +}); |
| 190 | + |
153 | 191 | it("forwards silent delivery options to Telegram sends", async () => { |
154 | 192 | sendMessageTelegramMock.mockResolvedValueOnce({ messageId: "tg-silent", chatId: "12345" }); |
155 | 193 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -345,6 +345,13 @@ describe("buildInlineKeyboard", () => {
|
345 | 345 | inline_keyboard: [[{ text: "Open", url: "https://example.com" }]], |
346 | 346 | }, |
347 | 347 | }, |
| 348 | +{ |
| 349 | +name: "keeps web app buttons", |
| 350 | +input: [[{ text: "Launch", web_app: { url: "https://example.com/app" } }]], |
| 351 | +expected: { |
| 352 | +inline_keyboard: [[{ text: "Launch", web_app: { url: "https://example.com/app" } }]], |
| 353 | +}, |
| 354 | +}, |
348 | 355 | { |
349 | 356 | name: "prefers url over callback data when both are present", |
350 | 357 | input: [[{ text: "Open", callback_data: "cmd:open", url: "https://example.com" }]], |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。