From bd474922cfa7a809721bb6d789d8f7f6e400a759 Mon Sep 17 00:00:00 2001 From: sunlit-deng Date: Wed, 26 Aug 2026 13:00:28 +0800 Subject: [PATCH] fix(telegram): describe reaction emoji syntax (#129080) * fix(telegram): describe reaction emoji syntax * fix(telegram): keep reactions cross-channel * test(telegram): verify disabled reactions remove emoji guidance --------- Co-authored-by: Peter Steinberger --- .../telegram/src/channel-actions.test.ts | 50 +++++++++++++++++++ extensions/telegram/src/channel-actions.ts | 9 ++++ .../telegram/src/message-tool-schema.ts | 13 +++++ src/agents/tools/message-tool.test.ts | 42 ++++++++++++++++ 4 files changed, 114 insertions(+) diff --git a/extensions/telegram/src/channel-actions.test.ts b/extensions/telegram/src/channel-actions.test.ts index f639aa70083f..8ec7a4d809ae 100644 --- a/extensions/telegram/src/channel-actions.test.ts +++ b/extensions/telegram/src/channel-actions.test.ts @@ -498,6 +498,56 @@ describe("telegramMessageActions", () => { }); }); + it("advertises Telegram reaction syntax and emoji discovery in message tool schema", () => { + const cfg = { + channels: { + telegram: { + botToken: "tok", + actions: { reactions: true }, + }, + }, + } as OpenClawConfig; + + const discovery = telegramMessageActions.describeMessageTool?.({ cfg }); + const contributions = Array.isArray(discovery?.schema) + ? discovery.schema + : discovery?.schema + ? [discovery.schema] + : []; + const reactionSchema = contributions.find((entry) => "emoji" in entry.properties); + const emojiDescription = ( + reactionSchema?.properties.emoji as { description?: string } | undefined + )?.description; + + expect(discovery?.actions).toEqual(expect.arrayContaining(["react", "emoji-list"])); + expect(reactionSchema?.actions).toEqual([]); + expect(reactionSchema?.properties.emoji).toMatchObject({ + type: "string", + description: expect.stringContaining("custom_emoji_id"), + }); + expect(emojiDescription).toContain('action:"emoji-list"'); + expect(emojiDescription).toContain("arbitrary Unicode may be rejected"); + + const disabledDiscovery = telegramMessageActions.describeMessageTool?.({ + cfg: { + channels: { + telegram: { + botToken: "tok", + actions: { reactions: false }, + }, + }, + } as OpenClawConfig, + }); + const disabledContributions = Array.isArray(disabledDiscovery?.schema) + ? disabledDiscovery.schema + : disabledDiscovery?.schema + ? [disabledDiscovery.schema] + : []; + expect(disabledDiscovery?.actions).not.toContain("react"); + expect(disabledDiscovery?.actions).not.toContain("emoji-list"); + expect(disabledContributions.find((entry) => "emoji" in entry.properties)).toBeUndefined(); + }); + it("matches runtime account-key normalization during SecretRef-tolerant discovery", () => { const cfg = { channels: { diff --git a/extensions/telegram/src/channel-actions.ts b/extensions/telegram/src/channel-actions.ts index afbc12db9e3e..9965cc008176 100644 --- a/extensions/telegram/src/channel-actions.ts +++ b/extensions/telegram/src/channel-actions.ts @@ -24,6 +24,7 @@ import { import { isTelegramInlineButtonsEnabled } from "./inline-buttons.js"; import { createTelegramPollExtraToolSchemas, + createTelegramReactionEmojiSchema, createTelegramRichSendExtraToolSchemas, } from "./message-tool-schema.js"; import { rejectTelegramNativeButtonParams } from "./native-button-params.js"; @@ -204,6 +205,14 @@ function describeTelegramMessageTool({ visibility: "all-configured", }); } + if (discovery.isEnabled("reactions")) { + schema.push({ + properties: createTelegramReactionEmojiSchema(), + // The shared emoji parameter keeps react valid across channels; this + // contribution only adds Telegram-specific guidance for that parameter. + actions: [], + }); + } if (discovery.isEnabled("sendMessage")) { schema.push({ properties: createTelegramRichSendExtraToolSchemas(), diff --git a/extensions/telegram/src/message-tool-schema.ts b/extensions/telegram/src/message-tool-schema.ts index 0b648db6c127..2a17ee7279f8 100644 --- a/extensions/telegram/src/message-tool-schema.ts +++ b/extensions/telegram/src/message-tool-schema.ts @@ -20,6 +20,19 @@ export function createTelegramPollExtraToolSchemas() { }; } +/** Schema additions for Telegram reactions through the existing react action. */ +export function createTelegramReactionEmojiSchema() { + return { + emoji: Type.Optional( + Type.String({ + description: + 'Telegram reaction emoji: use a supported Unicode reaction, or pass the numeric custom_emoji_id identifier returned by action:"emoji-list" directly as emoji. ' + + 'Use action:"emoji-list" to inspect reactions allowed in the current chat; arbitrary Unicode may be rejected by Telegram.', + }), + ), + }; +} + /** Schema additions for Telegram-native rich sends through the existing send action. */ export function createTelegramRichSendExtraToolSchemas() { return { diff --git a/src/agents/tools/message-tool.test.ts b/src/agents/tools/message-tool.test.ts index aa2dc67ce007..f522e5461336 100644 --- a/src/agents/tools/message-tool.test.ts +++ b/src/agents/tools/message-tool.test.ts @@ -4191,6 +4191,48 @@ describe("message tool description", () => { expect(tool.description).not.toContain("telegram ("); }); + it("keeps cross-channel Telegram reactions available when emoji schema is metadata-only", () => { + const signalPlugin = createChannelPlugin({ + id: "signal", + label: "Signal", + docsPath: "/channels/signal", + blurb: "Signal test plugin.", + actions: ["send"], + }); + const telegramPlugin = createChannelPlugin({ + id: "telegram", + label: "Telegram", + docsPath: "/channels/telegram", + blurb: "Telegram test plugin.", + actions: ["send", "react"], + toolSchema: { + actions: [], + properties: { + emoji: Type.Optional(Type.String()), + }, + }, + }); + + setActivePluginRegistry( + createTestRegistry([ + { pluginId: "signal", source: "test", plugin: signalPlugin }, + { pluginId: "telegram", source: "test", plugin: telegramPlugin }, + ]), + ); + + const tool = createMessageTool({ + config: {} as never, + currentChannelProvider: "signal", + }); + + const properties = getToolProperties(tool); + expect(getActionEnum(properties)).toContain("react"); + expect(properties.emoji).toMatchObject({ type: "string" }); + expect((properties.emoji as { description?: string }).description).not.toContain( + "custom_emoji_id", + ); + }); + it("does not advertise cross-channel actions whose params are hidden by current-channel schema", () => { const signalPlugin = createChannelPlugin({ id: "signal",