diff --git a/CHANGELOG.md b/CHANGELOG.md index efa0ff53db1..be6e036a9d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai - Slack/DMs: send text/block-only proactive DMs directly with `chat.postMessage(channel=)` while keeping conversation resolution for uploads and threaded sends. Fixes #62042. Thanks @MarkMolina. - Slack/routing: match route bindings written with Slack target syntax such as `channel:C...`, `user:U...`, or `<@U...>`, so bound Slack peers route to the configured agent instead of `main`. Fixes #41608. Thanks @Winnsolutionsadmin. - Slack/delivery: retry Slack Web API writes only when the SDK wraps a DNS request failure such as `EAI_AGAIN`, so transient resolver hiccups can recover without retrying platform errors that may duplicate messages. Fixes #68789. Thanks @sonnyb9. +- Slack/message actions: forward agent-scoped media roots through the bundled upload-file action path, so workspace files can be attached without failing the local-media guard. Fixes #64625. Thanks @benpchandler. - Slack/mentions: resolve `` 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. - PDF/Gemini: send native PDF analysis API keys in the `x-goog-api-key` header instead of the request URL, keeping secrets out of proxy and access logs. Supersedes #60600. Thanks @garagon. diff --git a/extensions/slack/src/channel-actions.ts b/extensions/slack/src/channel-actions.ts index 2156ea6a4b7..788b499cc72 100644 --- a/extensions/slack/src/channel-actions.ts +++ b/extensions/slack/src/channel-actions.ts @@ -19,6 +19,21 @@ async function loadSlackActionRuntime() { return await slackActionRuntimePromise; } +function resolveSlackActionContext(params: { + toolContext: unknown; + mediaLocalRoots: readonly string[] | undefined; + mediaReadFile: ((filePath: string) => Promise) | undefined; +}): SlackActionContext | undefined { + if (!params.toolContext && !params.mediaLocalRoots && !params.mediaReadFile) { + return undefined; + } + return { + ...(params.toolContext as SlackActionContext | undefined), + ...(params.mediaLocalRoots ? { mediaLocalRoots: params.mediaLocalRoots } : {}), + ...(params.mediaReadFile ? { mediaReadFile: params.mediaReadFile } : {}), + }; +} + export function createSlackActions( providerId: string, options?: { invoke?: SlackActionInvoke }, @@ -32,14 +47,16 @@ export function createSlackActions( ctx, normalizeChannelId: resolveSlackChannelId, includeReadThreadId: true, - invoke: async (action, cfg, toolContext) => - await (options?.invoke - ? options.invoke(action, cfg, toolContext) - : (await loadSlackActionRuntime()).handleSlackAction(action, cfg, { - ...(toolContext as SlackActionContext | undefined), - mediaLocalRoots: ctx.mediaLocalRoots, - mediaReadFile: ctx.mediaReadFile, - })), + invoke: async (action, cfg, toolContext) => { + const actionContext = resolveSlackActionContext({ + toolContext, + mediaLocalRoots: ctx.mediaLocalRoots, + mediaReadFile: ctx.mediaReadFile, + }); + return await (options?.invoke + ? options.invoke(action, cfg, actionContext) + : (await loadSlackActionRuntime()).handleSlackAction(action, cfg, actionContext)); + }, }); }, }; diff --git a/extensions/slack/src/channel.test.ts b/extensions/slack/src/channel.test.ts index 924fe1cdf5f..70d013da7ef 100644 --- a/extensions/slack/src/channel.test.ts +++ b/extensions/slack/src/channel.test.ts @@ -283,6 +283,47 @@ describe("slackPlugin actions", () => { undefined, ); }); + + it("forwards media access through the bundled Slack action invoke path", async () => { + handleSlackActionMock.mockResolvedValueOnce({ ok: true }); + const handleAction = requireSlackHandleAction(); + const mediaLocalRoots = ["/tmp/workspace-agent"]; + const mediaReadFile = vi.fn(async () => Buffer.from("file")); + + await handleAction({ + action: "upload-file", + channel: "slack", + accountId: "default", + cfg: {}, + params: { + to: "channel:C123", + filePath: "/tmp/workspace-agent/renders/file.wav", + initialComment: "render", + }, + mediaLocalRoots, + mediaReadFile, + toolContext: { + currentChannelId: "C123", + replyToMode: "all", + }, + } as never); + + expect(handleSlackActionMock).toHaveBeenCalledWith( + expect.objectContaining({ + action: "uploadFile", + to: "channel:C123", + filePath: "/tmp/workspace-agent/renders/file.wav", + initialComment: "render", + }), + {}, + expect.objectContaining({ + currentChannelId: "C123", + replyToMode: "all", + mediaLocalRoots, + mediaReadFile, + }), + ); + }); }); describe("slackPlugin status", () => {