diff --git a/src/channels/plugins/actions/actions.test.ts b/src/channels/plugins/actions/actions.test.ts index 1b33f41377c..de9aa4c4e18 100644 --- a/src/channels/plugins/actions/actions.test.ts +++ b/src/channels/plugins/actions/actions.test.ts @@ -526,4 +526,71 @@ describe("slack actions adapter", () => { limit: 2, }); }); + + it("forwards blocks JSON for send", async () => { + const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; + const actions = createSlackActions("slack"); + + await actions.handleAction?.({ + channel: "slack", + action: "send", + cfg, + params: { + to: "channel:C1", + message: "", + blocks: JSON.stringify([{ type: "divider" }]), + }, + }); + + const [params] = handleSlackAction.mock.calls[0] ?? []; + expect(params).toMatchObject({ + action: "sendMessage", + to: "channel:C1", + content: "", + blocks: [{ type: "divider" }], + }); + }); + + it("forwards blocks arrays for send", async () => { + const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; + const actions = createSlackActions("slack"); + + await actions.handleAction?.({ + channel: "slack", + action: "send", + cfg, + params: { + to: "channel:C1", + message: "", + blocks: [{ type: "section", text: { type: "mrkdwn", text: "hi" } }], + }, + }); + + const [params] = handleSlackAction.mock.calls[0] ?? []; + expect(params).toMatchObject({ + action: "sendMessage", + to: "channel:C1", + content: "", + blocks: [{ type: "section", text: { type: "mrkdwn", text: "hi" } }], + }); + }); + + it("rejects invalid blocks JSON for send", async () => { + const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; + const actions = createSlackActions("slack"); + + await expect( + actions.handleAction?.({ + channel: "slack", + action: "send", + cfg, + params: { + to: "channel:C1", + message: "", + blocks: "{bad-json", + }, + }), + ).rejects.toThrow(/blocks must be valid JSON/i); + expect(handleSlackAction).not.toHaveBeenCalled(); + }); }); diff --git a/src/plugin-sdk/slack-message-actions.ts b/src/plugin-sdk/slack-message-actions.ts index a98557d2c47..522aab3398a 100644 --- a/src/plugin-sdk/slack-message-actions.ts +++ b/src/plugin-sdk/slack-message-actions.ts @@ -8,6 +8,27 @@ type SlackActionInvoke = ( toolContext?: ChannelMessageActionContext["toolContext"], ) => Promise>; +function readSlackBlocksParam(actionParams: Record) { + const raw = actionParams.blocks; + if (raw == null) { + return undefined; + } + const parsed = + typeof raw === "string" + ? (() => { + try { + return JSON.parse(raw); + } catch { + throw new Error("blocks must be valid JSON"); + } + })() + : raw; + if (!Array.isArray(parsed)) { + throw new Error("blocks must be an array"); + } + return parsed as Record[]; +} + export async function handleSlackMessageAction(params: { providerId: string; ctx: ChannelMessageActionContext; @@ -28,18 +49,23 @@ export async function handleSlackMessageAction(params: { if (action === "send") { const to = readStringParam(actionParams, "to", { required: true }); const content = readStringParam(actionParams, "message", { - required: true, + required: false, allowEmpty: true, }); const mediaUrl = readStringParam(actionParams, "media", { trim: false }); + const blocks = readSlackBlocksParam(actionParams); + if (!content && !mediaUrl && !blocks) { + throw new Error("Slack send requires message, blocks, or media."); + } const threadId = readStringParam(actionParams, "threadId"); const replyTo = readStringParam(actionParams, "replyTo"); return await invoke( { action: "sendMessage", to, - content, + content: content ?? "", mediaUrl: mediaUrl ?? undefined, + blocks, accountId, threadTs: threadId ?? replyTo ?? undefined, },