fix(slack): forward media roots for uploads

This commit is contained in:
Peter Steinberger
2026-05-02 05:34:49 +01:00
parent dc2396ba13
commit 72c8764d32
3 changed files with 67 additions and 8 deletions

View File

@@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai
- Slack/DMs: send text/block-only proactive DMs directly with `chat.postMessage(channel=<user id>)` 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 `<!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.
- 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.

View File

@@ -19,6 +19,21 @@ async function loadSlackActionRuntime() {
return await slackActionRuntimePromise;
}
function resolveSlackActionContext(params: {
toolContext: unknown;
mediaLocalRoots: readonly string[] | undefined;
mediaReadFile: ((filePath: string) => Promise<Buffer>) | 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));
},
});
},
};

View File

@@ -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", () => {