mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix(slack): split media and block action sends
This commit is contained in:
@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
|
||||
- macOS/Voice Wake: send wake-word and Push-to-Talk transcripts through the selected macOS session target instead of always falling back to main WebChat. Fixes #51040. Thanks @carl-jeffrolc.
|
||||
- Providers/xAI: give Grok `web_search` a 60s default timeout, harden malformed xAI Responses parsing, and return structured timeout errors instead of aborting the tool call. Fixes #58063 and #58733. Thanks @dnishimura, @marvcasasola-svg, and @Nanako0129.
|
||||
- Providers/configure: preserve the existing default model when adding or reauthing a provider whose plugin returns a default-model config patch. Fixes #50268. Thanks @rixcorp-oc.
|
||||
- Slack/message actions: send media before the follow-up Block Kit message when Slack `send` includes a file plus presentation or interactive controls, so file attachments are no longer rejected. Fixes #51458. Thanks @HirokiKobayashi-R.
|
||||
- Slack/mentions: resolve `<!subteam^...>` user-group mentions through Slack `usergroups.users.list` and treat them as explicit mentions only when the bot user is a member, so mention-gated agent channels wake for real user-group mentions without config-only allowlists. Fixes #73827. Thanks @CG-Intelligence-Agent-Jack.
|
||||
- Slack/message tool: let `read` fetch an exact Slack message timestamp, including a specific thread reply when paired with `threadId`, instead of returning only the parent thread or recent channel history. Fixes #53943. Thanks @zomars.
|
||||
- Web search: point missing-key errors to `web_fetch` for known URLs and the browser tool for interactive pages. Thanks @zhaoyang97.
|
||||
|
||||
@@ -427,19 +427,48 @@ describe("handleSlackAction", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects blocks combined with mediaUrl", async () => {
|
||||
await expect(
|
||||
handleSlackAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "channel:C123",
|
||||
content: "hello",
|
||||
mediaUrl: "https://example.com/file.png",
|
||||
blocks: JSON.stringify([{ type: "divider" }]),
|
||||
},
|
||||
slackConfig(),
|
||||
),
|
||||
).rejects.toThrow(/does not support blocks with mediaUrl/i);
|
||||
it("sends media before a separate blocks message", async () => {
|
||||
sendSlackMessage.mockResolvedValueOnce({ channelId: "C123" });
|
||||
sendSlackMessage.mockResolvedValueOnce({ channelId: "C123" });
|
||||
|
||||
const result = await handleSlackAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "channel:C123",
|
||||
content: "hello",
|
||||
mediaUrl: "https://example.com/file.png",
|
||||
blocks: JSON.stringify([{ type: "divider" }]),
|
||||
},
|
||||
slackConfig(),
|
||||
);
|
||||
|
||||
expect(sendSlackMessage).toHaveBeenCalledTimes(2);
|
||||
expect(sendSlackMessage).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"channel:C123",
|
||||
"",
|
||||
expect.objectContaining({
|
||||
cfg: expect.any(Object),
|
||||
mediaUrl: "https://example.com/file.png",
|
||||
threadTs: undefined,
|
||||
}),
|
||||
);
|
||||
expect(sendSlackMessage.mock.calls[0]?.[2]).not.toHaveProperty("blocks");
|
||||
expect(sendSlackMessage).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"channel:C123",
|
||||
"hello",
|
||||
expect.objectContaining({
|
||||
cfg: expect.any(Object),
|
||||
blocks: [{ type: "divider" }],
|
||||
threadTs: undefined,
|
||||
}),
|
||||
);
|
||||
expect(sendSlackMessage.mock.calls[1]?.[2]).not.toHaveProperty("mediaUrl");
|
||||
expect(result.details).toEqual({
|
||||
ok: true,
|
||||
result: { channelId: "C123" },
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -244,22 +244,34 @@ export async function handleSlackAction(
|
||||
if (!content && !mediaUrl && !blocks) {
|
||||
throw new Error("Slack sendMessage requires content, blocks, or mediaUrl.");
|
||||
}
|
||||
if (mediaUrl && blocks) {
|
||||
throw new Error("Slack sendMessage does not support blocks with mediaUrl.");
|
||||
}
|
||||
const threadTs = resolveThreadTsFromContext(
|
||||
readStringParam(params, "threadTs"),
|
||||
to,
|
||||
context,
|
||||
);
|
||||
const result = await slackActionRuntime.sendSlackMessage(to, content ?? "", {
|
||||
const sendOpts = {
|
||||
...writeOpts,
|
||||
mediaUrl: mediaUrl ?? undefined,
|
||||
mediaLocalRoots: context?.mediaLocalRoots,
|
||||
mediaReadFile: context?.mediaReadFile,
|
||||
threadTs: threadTs ?? undefined,
|
||||
blocks,
|
||||
});
|
||||
};
|
||||
const result =
|
||||
mediaUrl && blocks
|
||||
? await (async () => {
|
||||
await slackActionRuntime.sendSlackMessage(to, "", {
|
||||
...sendOpts,
|
||||
mediaUrl,
|
||||
});
|
||||
return await slackActionRuntime.sendSlackMessage(to, content ?? "", {
|
||||
...sendOpts,
|
||||
blocks,
|
||||
});
|
||||
})()
|
||||
: await slackActionRuntime.sendSlackMessage(to, content ?? "", {
|
||||
...sendOpts,
|
||||
mediaUrl: mediaUrl ?? undefined,
|
||||
blocks,
|
||||
});
|
||||
|
||||
if (threadTs && result.channelId && account.accountId) {
|
||||
slackActionRuntime.recordSlackThreadParticipation(
|
||||
|
||||
@@ -99,6 +99,50 @@ describe("handleSlackMessageAction", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("passes media and rendered interactive blocks through for split Slack delivery", async () => {
|
||||
const invoke = createInvokeSpy();
|
||||
|
||||
await handleSlackMessageAction({
|
||||
providerId: "slack",
|
||||
ctx: {
|
||||
action: "send",
|
||||
cfg: {},
|
||||
params: {
|
||||
to: "channel:C1",
|
||||
message: "Approval required",
|
||||
media: "https://example.com/report.md",
|
||||
interactive: {
|
||||
blocks: [
|
||||
{
|
||||
type: "buttons",
|
||||
buttons: [{ label: "Approve", value: "approve" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
invoke: invoke as never,
|
||||
});
|
||||
|
||||
expect(invoke).toHaveBeenCalledOnce();
|
||||
expect(invoke).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "sendMessage",
|
||||
to: "channel:C1",
|
||||
content: "Approval required",
|
||||
mediaUrl: "https://example.com/report.md",
|
||||
blocks: [
|
||||
expect.objectContaining({
|
||||
type: "actions",
|
||||
elements: [expect.objectContaining({ value: "approve" })],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
expect.any(Object),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("maps upload-file to the internal uploadFile action", async () => {
|
||||
const invoke = createInvokeSpy();
|
||||
|
||||
|
||||
@@ -58,9 +58,6 @@ export async function handleSlackMessageAction(params: {
|
||||
if (!content && !mediaUrl && !blocks) {
|
||||
throw new Error("Slack send requires message, blocks, or media.");
|
||||
}
|
||||
if (mediaUrl && blocks) {
|
||||
throw new Error("Slack send does not support blocks with media.");
|
||||
}
|
||||
const threadId = readStringParam(actionParams, "threadId");
|
||||
const replyTo = readStringParam(actionParams, "replyTo");
|
||||
return await invoke(
|
||||
|
||||
Reference in New Issue
Block a user