diff --git a/src/infra/outbound/message-action-param-keys.ts b/src/infra/outbound/message-action-param-keys.ts new file mode 100644 index 00000000000..e6eda89e585 --- /dev/null +++ b/src/infra/outbound/message-action-param-keys.ts @@ -0,0 +1,57 @@ +import { normalizeOptionalString } from "../../shared/string-coerce.js"; + +const STANDARD_MESSAGE_ACTION_PARAM_KEYS = new Set([ + "accountId", + "asDocument", + "base64", + "bestEffort", + "blocks", + "buttons", + "caption", + "card", + "channel", + "channelId", + "components", + "contentType", + "dryRun", + "filePath", + "fileUrl", + "filename", + "forceDocument", + "gifPlayback", + "image", + "interactive", + "media", + "mediaUrl", + "message", + "mimeType", + "path", + "pollAnonymous", + "pollDurationHours", + "pollMulti", + "pollOption", + "pollPublic", + "pollQuestion", + "replyTo", + "silent", + "target", + "targets", + "text", + "threadId", + "to", +]); + +export function hasPotentialPluginActionParam(params: Record): boolean { + return Object.entries(params).some(([key, value]) => { + if (STANDARD_MESSAGE_ACTION_PARAM_KEYS.has(key)) { + return false; + } + if (typeof value === "string") { + return Boolean(normalizeOptionalString(value)); + } + if (typeof value === "number") { + return Number.isFinite(value); + } + return value !== undefined; + }); +} diff --git a/src/infra/outbound/message-action-params.test.ts b/src/infra/outbound/message-action-params.test.ts index ad418b5aa69..9bda17441dd 100644 --- a/src/infra/outbound/message-action-params.test.ts +++ b/src/infra/outbound/message-action-params.test.ts @@ -1,13 +1,23 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; + +const { resolveChannelMessageToolMediaSourceParamKeysMock } = vi.hoisted(() => ({ + resolveChannelMessageToolMediaSourceParamKeysMock: vi.fn(() => ["avatarPath", "avatarUrl"]), +})); + +vi.mock("../../channels/plugins/message-action-discovery.js", () => ({ + resolveChannelMessageToolMediaSourceParamKeys: resolveChannelMessageToolMediaSourceParamKeysMock, +})); + import { collectActionMediaSourceHints, hydrateAttachmentParamsForAction, normalizeSandboxMediaList, normalizeSandboxMediaParams, + resolveExtraActionMediaSourceParamKeys, resolveAttachmentMediaPolicy, } from "./message-action-params.js"; @@ -16,6 +26,52 @@ const maybeIt = process.platform === "win32" ? it.skip : it; const matrixMediaSourceParamKeys = ["avatarPath", "avatarUrl"] as const; describe("message action media helpers", () => { + beforeEach(() => { + resolveChannelMessageToolMediaSourceParamKeysMock.mockClear(); + }); + + it("skips plugin media discovery when args only use standard action params", () => { + expect( + resolveExtraActionMediaSourceParamKeys({ + cfg, + action: "send", + channel: "slack", + args: { + channel: "slack", + target: "#C12345678", + message: "hi", + media: "https://example.com/photo.png", + }, + }), + ).toEqual([]); + expect(resolveChannelMessageToolMediaSourceParamKeysMock).not.toHaveBeenCalled(); + }); + + it("discovers plugin media params when args include an extension-owned field", () => { + expect( + resolveExtraActionMediaSourceParamKeys({ + cfg, + action: "set-profile", + channel: "matrix", + args: { + channel: "matrix", + avatarPath: "/workspace/avatars/profile.png", + }, + }), + ).toEqual(["avatarPath", "avatarUrl"]); + expect(resolveChannelMessageToolMediaSourceParamKeysMock).toHaveBeenCalledWith({ + cfg, + action: "set-profile", + channel: "matrix", + accountId: undefined, + sessionKey: undefined, + sessionId: undefined, + agentId: undefined, + requesterSenderId: undefined, + senderIsOwner: undefined, + }); + }); + it("prefers sandbox media policy when sandbox roots are non-blank", () => { expect( resolveAttachmentMediaPolicy({ diff --git a/src/infra/outbound/message-action-params.ts b/src/infra/outbound/message-action-params.ts index a81ba45d99a..83027ccccdb 100644 --- a/src/infra/outbound/message-action-params.ts +++ b/src/infra/outbound/message-action-params.ts @@ -17,6 +17,7 @@ import { loadWebMedia } from "../../media/web-media.js"; import { resolveSnakeCaseParamKey } from "../../param-key.js"; import { readBooleanParam as readBooleanParamShared } from "../../plugin-sdk/boolean-param.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; +import { hasPotentialPluginActionParam } from "./message-action-param-keys.js"; export const readBooleanParam = readBooleanParamShared; @@ -60,6 +61,7 @@ function buildActionMediaSourceParamKeys(extraParamKeys?: readonly string[]): st export function resolveExtraActionMediaSourceParamKeys(params: { cfg: OpenClawConfig; action?: ChannelMessageActionName; + args: Record; channel?: string; accountId?: string | null; sessionKey?: string | null; @@ -68,6 +70,9 @@ export function resolveExtraActionMediaSourceParamKeys(params: { requesterSenderId?: string | null; senderIsOwner?: boolean; }): string[] { + if (!hasPotentialPluginActionParam(params.args)) { + return []; + } return resolveChannelMessageToolMediaSourceParamKeys({ cfg: params.cfg, action: params.action, diff --git a/src/infra/outbound/message-action-runner.ts b/src/infra/outbound/message-action-runner.ts index 0e613bed9dc..31d2c9acdd6 100644 --- a/src/infra/outbound/message-action-runner.ts +++ b/src/infra/outbound/message-action-runner.ts @@ -860,6 +860,7 @@ export async function runMessageAction( const extraActionMediaSourceParamKeys = resolveExtraActionMediaSourceParamKeys({ cfg, action, + args: params, channel, accountId, sessionKey: input.sessionKey, diff --git a/src/infra/outbound/message-action-spec.ts b/src/infra/outbound/message-action-spec.ts index dba62f954af..fef1bd9254f 100644 --- a/src/infra/outbound/message-action-spec.ts +++ b/src/infra/outbound/message-action-spec.ts @@ -4,6 +4,7 @@ import { normalizeOptionalLowercaseString, normalizeOptionalString, } from "../../shared/string-coerce.js"; +import { hasPotentialPluginActionParam } from "./message-action-param-keys.js"; export type MessageActionTargetMode = "to" | "channelId" | "none"; @@ -84,6 +85,7 @@ const ACTION_TARGET_ALIASES: Partial, channel?: string, ): ActionTargetAliasSpec[] { const specs: ActionTargetAliasSpec[] = []; @@ -92,7 +94,7 @@ function listActionTargetAliasSpecs( specs.push(coreSpec); } const normalizedChannel = normalizeOptionalLowercaseString(channel); - if (!normalizedChannel) { + if (!normalizedChannel || !hasPotentialPluginActionParam(params)) { return specs; } const plugin = getBootstrapChannelPlugin(normalizedChannel); @@ -120,7 +122,7 @@ export function actionHasTarget( if (channelId) { return true; } - const specs = listActionTargetAliasSpecs(action, options?.channel); + const specs = listActionTargetAliasSpecs(action, params, options?.channel); if (specs.length === 0) { return false; }