mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 21:51:24 +00:00
feat(telegram): support inline button styles (#18241)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 239cb3552e
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
This commit is contained in:
@@ -371,6 +371,20 @@ describe("buildAgentSystemPrompt", () => {
|
||||
expect(prompt).toContain(`respond with ONLY: ${SILENT_REPLY_TOKEN}`);
|
||||
});
|
||||
|
||||
it("includes inline button style guidance when runtime supports inline buttons", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
toolNames: ["message"],
|
||||
runtimeInfo: {
|
||||
channel: "telegram",
|
||||
capabilities: ["inlineButtons"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(prompt).toContain("buttons=[[{text,callback_data,style?}]]");
|
||||
expect(prompt).toContain("`style` can be `primary`, `success`, or `danger`");
|
||||
});
|
||||
|
||||
it("includes runtime provider capabilities when present", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
|
||||
@@ -123,7 +123,7 @@ function buildMessagingSection(params: {
|
||||
`- If multiple channels are configured, pass \`channel\` (${params.messageChannelOptions}).`,
|
||||
`- If you use \`message\` (\`action=send\`) to deliver your user-visible reply, respond with ONLY: ${SILENT_REPLY_TOKEN} (avoid duplicate replies).`,
|
||||
params.inlineButtonsEnabled
|
||||
? "- Inline buttons supported. Use `action=send` with `buttons=[[{text,callback_data}]]` (callback_data routes back as a user message)."
|
||||
? "- Inline buttons supported. Use `action=send` with `buttons=[[{text,callback_data,style?}]]`; `style` can be `primary`, `success`, or `danger`."
|
||||
: params.runtimeChannel
|
||||
? `- Inline buttons not enabled for ${params.runtimeChannel}. If you need them, ask to set ${params.runtimeChannel}.capabilities.inlineButtons ("dm"|"group"|"all"|"allowlist").`
|
||||
: "",
|
||||
|
||||
@@ -155,6 +155,13 @@ describe("message tool schema scoping", () => {
|
||||
|
||||
expect(properties.components).toBeUndefined();
|
||||
expect(properties.buttons).toBeDefined();
|
||||
const buttonItemProps =
|
||||
(
|
||||
properties.buttons as {
|
||||
items?: { items?: { properties?: Record<string, unknown> } };
|
||||
}
|
||||
)?.items?.items?.properties ?? {};
|
||||
expect(buttonItemProps.style).toBeDefined();
|
||||
expect(actionEnum).toContain("send");
|
||||
expect(actionEnum).toContain("react");
|
||||
expect(actionEnum).not.toContain("poll");
|
||||
|
||||
@@ -187,6 +187,7 @@ function buildSendSchema(options: {
|
||||
Type.Object({
|
||||
text: Type.String(),
|
||||
callback_data: Type.String(),
|
||||
style: Type.Optional(stringEnum(["danger", "success", "primary"])),
|
||||
}),
|
||||
),
|
||||
{
|
||||
|
||||
@@ -508,6 +508,46 @@ describe("handleTelegramAction", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards optional button style", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: { botToken: "tok", capabilities: { inlineButtons: "all" } },
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
await handleTelegramAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "@testchannel",
|
||||
content: "Choose",
|
||||
buttons: [
|
||||
[
|
||||
{
|
||||
text: "Option A",
|
||||
callback_data: "cmd:a",
|
||||
style: "primary",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
expect(sendMessageTelegram).toHaveBeenCalledWith(
|
||||
"@testchannel",
|
||||
"Choose",
|
||||
expect.objectContaining({
|
||||
buttons: [
|
||||
[
|
||||
{
|
||||
text: "Option A",
|
||||
callback_data: "cmd:a",
|
||||
style: "primary",
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("readTelegramButtons", () => {
|
||||
@@ -517,4 +557,35 @@ describe("readTelegramButtons", () => {
|
||||
});
|
||||
expect(result).toEqual([[{ text: "Option A", callback_data: "cmd:a" }]]);
|
||||
});
|
||||
|
||||
it("normalizes optional style", () => {
|
||||
const result = readTelegramButtons({
|
||||
buttons: [
|
||||
[
|
||||
{
|
||||
text: "Option A",
|
||||
callback_data: "cmd:a",
|
||||
style: " PRIMARY ",
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
text: "Option A",
|
||||
callback_data: "cmd:a",
|
||||
style: "primary",
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects unsupported button style", () => {
|
||||
expect(() =>
|
||||
readTelegramButtons({
|
||||
buttons: [[{ text: "Option A", callback_data: "cmd:a", style: "secondary" }]],
|
||||
}),
|
||||
).toThrow(/style must be one of danger, success, primary/i);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { TelegramButtonStyle, TelegramInlineButtons } from "../../telegram/button-types.js";
|
||||
import {
|
||||
resolveTelegramInlineButtonsScope,
|
||||
resolveTelegramTargetChatType,
|
||||
@@ -23,14 +24,11 @@ import {
|
||||
readStringParam,
|
||||
} from "./common.js";
|
||||
|
||||
type TelegramButton = {
|
||||
text: string;
|
||||
callback_data: string;
|
||||
};
|
||||
const TELEGRAM_BUTTON_STYLES: readonly TelegramButtonStyle[] = ["danger", "success", "primary"];
|
||||
|
||||
export function readTelegramButtons(
|
||||
params: Record<string, unknown>,
|
||||
): TelegramButton[][] | undefined {
|
||||
): TelegramInlineButtons | undefined {
|
||||
const raw = params.buttons;
|
||||
if (raw == null) {
|
||||
return undefined;
|
||||
@@ -62,7 +60,21 @@ export function readTelegramButtons(
|
||||
`buttons[${rowIndex}][${buttonIndex}] callback_data too long (max 64 chars)`,
|
||||
);
|
||||
}
|
||||
return { text, callback_data: callbackData };
|
||||
const styleRaw = (button as { style?: unknown }).style;
|
||||
const style = typeof styleRaw === "string" ? styleRaw.trim().toLowerCase() : undefined;
|
||||
if (styleRaw !== undefined && !style) {
|
||||
throw new Error(`buttons[${rowIndex}][${buttonIndex}] style must be string`);
|
||||
}
|
||||
if (style && !TELEGRAM_BUTTON_STYLES.includes(style as TelegramButtonStyle)) {
|
||||
throw new Error(
|
||||
`buttons[${rowIndex}][${buttonIndex}] style must be one of ${TELEGRAM_BUTTON_STYLES.join(", ")}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
text,
|
||||
callback_data: callbackData,
|
||||
...(style ? { style: style as TelegramButtonStyle } : {}),
|
||||
};
|
||||
});
|
||||
});
|
||||
const filtered = rows.filter((row) => row.length > 0);
|
||||
|
||||
Reference in New Issue
Block a user