mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 18:54:58 +00:00
feat(slack): add download-file action for on-demand file attachment access (#24723)
* feat(slack): add download-file action for on-demand file attachment access Adds a new `download-file` message tool action that allows the agent to download Slack file attachments by file ID on demand. This is a prerequisite for accessing images posted in thread history, where file attachments are not automatically resolved. Changes: - Add `files` field to `SlackMessageSummary` type so file IDs are visible in message read results - Add `downloadSlackFile()` to fetch a file by ID via `files.info` and resolve it through the existing `resolveSlackMedia()` pipeline - Register `download-file` in `CHANNEL_MESSAGE_ACTION_NAMES`, `MESSAGE_ACTION_TARGET_MODE`, and `listSlackMessageActions` - Add `downloadFile` dispatch case in `handleSlackAction` - Wire agent-facing `download-file` → internal `downloadFile` in `handleSlackMessageAction` Closes #24681 * style: fix formatting in slack-actions and actions * test(slack): cover download-file action path --------- Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { handleSlackAction } from "./slack-actions.js";
|
||||
|
||||
const deleteSlackMessage = vi.fn(async (..._args: unknown[]) => ({}));
|
||||
const downloadSlackFile = vi.fn(async (..._args: unknown[]) => null);
|
||||
const editSlackMessage = vi.fn(async (..._args: unknown[]) => ({}));
|
||||
const getSlackMemberInfo = vi.fn(async (..._args: unknown[]) => ({}));
|
||||
const listSlackEmojis = vi.fn(async (..._args: unknown[]) => ({}));
|
||||
@@ -19,6 +20,7 @@ const unpinSlackMessage = vi.fn(async (..._args: unknown[]) => ({}));
|
||||
vi.mock("../../slack/actions.js", () => ({
|
||||
deleteSlackMessage: (...args: Parameters<typeof deleteSlackMessage>) =>
|
||||
deleteSlackMessage(...args),
|
||||
downloadSlackFile: (...args: Parameters<typeof downloadSlackFile>) => downloadSlackFile(...args),
|
||||
editSlackMessage: (...args: Parameters<typeof editSlackMessage>) => editSlackMessage(...args),
|
||||
getSlackMemberInfo: (...args: Parameters<typeof getSlackMemberInfo>) =>
|
||||
getSlackMemberInfo(...args),
|
||||
@@ -194,6 +196,26 @@ describe("handleSlackAction", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a friendly error when downloadFile cannot fetch the attachment", async () => {
|
||||
downloadSlackFile.mockResolvedValueOnce(null);
|
||||
const result = await handleSlackAction(
|
||||
{
|
||||
action: "downloadFile",
|
||||
fileId: "F123",
|
||||
},
|
||||
slackConfig(),
|
||||
);
|
||||
expect(downloadSlackFile).toHaveBeenCalledWith(
|
||||
"F123",
|
||||
expect.objectContaining({ maxBytes: 20 * 1024 * 1024 }),
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
details: expect.objectContaining({ ok: false }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "JSON blocks",
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { resolveSlackAccount } from "../../slack/accounts.js";
|
||||
import {
|
||||
deleteSlackMessage,
|
||||
downloadSlackFile,
|
||||
editSlackMessage,
|
||||
getSlackMemberInfo,
|
||||
listSlackEmojis,
|
||||
@@ -22,13 +23,20 @@ import { parseSlackTarget, resolveSlackChannelId } from "../../slack/targets.js"
|
||||
import { withNormalizedTimestamp } from "../date-time.js";
|
||||
import {
|
||||
createActionGate,
|
||||
imageResultFromFile,
|
||||
jsonResult,
|
||||
readNumberParam,
|
||||
readReactionParams,
|
||||
readStringParam,
|
||||
} from "./common.js";
|
||||
|
||||
const messagingActions = new Set(["sendMessage", "editMessage", "deleteMessage", "readMessages"]);
|
||||
const messagingActions = new Set([
|
||||
"sendMessage",
|
||||
"editMessage",
|
||||
"deleteMessage",
|
||||
"readMessages",
|
||||
"downloadFile",
|
||||
]);
|
||||
|
||||
const reactionsActions = new Set(["react", "reactions"]);
|
||||
const pinActions = new Set(["pinMessage", "unpinMessage", "listPins"]);
|
||||
@@ -280,6 +288,28 @@ export async function handleSlackAction(
|
||||
);
|
||||
return jsonResult({ ok: true, messages, hasMore: result.hasMore });
|
||||
}
|
||||
case "downloadFile": {
|
||||
const fileId = readStringParam(params, "fileId", { required: true });
|
||||
const maxBytes = account.config?.mediaMaxMb
|
||||
? account.config.mediaMaxMb * 1024 * 1024
|
||||
: 20 * 1024 * 1024;
|
||||
const downloaded = await downloadSlackFile(fileId, {
|
||||
...readOpts,
|
||||
maxBytes,
|
||||
});
|
||||
if (!downloaded) {
|
||||
return jsonResult({
|
||||
ok: false,
|
||||
error: "File could not be downloaded (not found, too large, or inaccessible).",
|
||||
});
|
||||
}
|
||||
return await imageResultFromFile({
|
||||
label: "slack-file",
|
||||
path: downloaded.path,
|
||||
extraText: downloaded.placeholder,
|
||||
details: { fileId, path: downloaded.path },
|
||||
});
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user