diff --git a/src/cli/program/message/register.send.ts b/src/cli/program/message/register.send.ts index 360e5bcc0a..6c3d709f96 100644 --- a/src/cli/program/message/register.send.ts +++ b/src/cli/program/message/register.send.ts @@ -19,6 +19,7 @@ export function registerMessageSendCommand(message: Command, helpers: MessageCli "--buttons ", "Telegram inline keyboard buttons as JSON (array of button rows)", ) + .option("--components ", "Discord components payload as JSON") .option("--card ", "Adaptive Card JSON object (when supported by the channel)") .option("--reply-to ", "Reply-to message id") .option("--thread-id ", "Thread id (Telegram forum thread)") diff --git a/src/infra/outbound/message-action-params.ts b/src/infra/outbound/message-action-params.ts index 20376b2ed6..2b6a3186d6 100644 --- a/src/infra/outbound/message-action-params.ts +++ b/src/infra/outbound/message-action-params.ts @@ -369,3 +369,20 @@ export function parseCardParam(params: Record): void { throw new Error("--card must be valid JSON"); } } + +export function parseComponentsParam(params: Record): void { + const raw = params.components; + if (typeof raw !== "string") { + return; + } + const trimmed = raw.trim(); + if (!trimmed) { + delete params.components; + return; + } + try { + params.components = JSON.parse(trimmed) as unknown; + } catch { + throw new Error("--components must be valid JSON"); + } +} diff --git a/src/infra/outbound/message-action-runner.test.ts b/src/infra/outbound/message-action-runner.test.ts index c02b4009a8..358c202b98 100644 --- a/src/infra/outbound/message-action-runner.test.ts +++ b/src/infra/outbound/message-action-runner.test.ts @@ -740,6 +740,77 @@ describe("runMessageAction card-only send behavior", () => { }); }); +describe("runMessageAction components parsing", () => { + const handleAction = vi.fn(async ({ params }: { params: Record }) => + jsonResult({ + ok: true, + components: params.components ?? null, + }), + ); + + const componentsPlugin: ChannelPlugin = { + id: "discord", + meta: { + id: "discord", + label: "Discord", + selectionLabel: "Discord", + docsPath: "/channels/discord", + blurb: "Discord components send test plugin.", + }, + capabilities: { chatTypes: ["direct"] }, + config: { + listAccountIds: () => ["default"], + resolveAccount: () => ({}), + isConfigured: () => true, + }, + actions: { + listActions: () => ["send"], + supportsAction: ({ action }) => action === "send", + handleAction, + }, + }; + + beforeEach(() => { + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "discord", + source: "test", + plugin: componentsPlugin, + }, + ]), + ); + handleAction.mockClear(); + }); + + afterEach(() => { + setActivePluginRegistry(createTestRegistry([])); + vi.clearAllMocks(); + }); + + it("parses components JSON strings before plugin dispatch", async () => { + const components = { + text: "hello", + buttons: [{ label: "A", customId: "a" }], + }; + const result = await runMessageAction({ + cfg: {} as OpenClawConfig, + action: "send", + params: { + channel: "discord", + target: "channel:123", + message: "hi", + components: JSON.stringify(components), + }, + dryRun: false, + }); + + expect(result.kind).toBe("send"); + expect(handleAction).toHaveBeenCalled(); + expect(result.payload).toMatchObject({ ok: true, components }); + }); +}); + describe("runMessageAction accountId defaults", () => { const handleAction = vi.fn(async () => jsonResult({ ok: true })); const accountPlugin: ChannelPlugin = { diff --git a/src/infra/outbound/message-action-runner.ts b/src/infra/outbound/message-action-runner.ts index 412ba5c37b..bc249debfb 100644 --- a/src/infra/outbound/message-action-runner.ts +++ b/src/infra/outbound/message-action-runner.ts @@ -34,6 +34,7 @@ import { normalizeSandboxMediaParams, parseButtonsParam, parseCardParam, + parseComponentsParam, readBooleanParam, resolveSlackAutoThreadId, resolveTelegramAutoThreadId, @@ -697,6 +698,7 @@ export async function runMessageAction( : undefined); parseButtonsParam(params); parseCardParam(params); + parseComponentsParam(params); const action = input.action; if (action === "broadcast") {