mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:40:42 +00:00
Scope media discovery to the current action
This commit is contained in:
@@ -178,6 +178,9 @@ local path or remote media URL, the plugin should also return
|
||||
`mediaSourceParams` from `describeMessageTool(...)`. Core uses that explicit
|
||||
list to apply sandbox path normalization and outbound media-access hints
|
||||
without hardcoding plugin-owned param names.
|
||||
Prefer action-scoped maps there, not one channel-wide flat list, so a
|
||||
profile-only media param does not get normalized on unrelated actions like
|
||||
`send`.
|
||||
|
||||
Core passes runtime scope into that discovery step. Important fields include:
|
||||
|
||||
|
||||
@@ -40,6 +40,10 @@ param names through `describeMessageTool(...).mediaSourceParams`. Core uses
|
||||
that explicit list for sandbox path normalization and outbound media-access
|
||||
policy, so plugins do not need shared-core special cases for provider-specific
|
||||
avatar, attachment, or cover-image params.
|
||||
Prefer returning an action-keyed map such as
|
||||
`{ "set-profile": ["avatarUrl", "avatarPath"] }` so unrelated actions do not
|
||||
inherit another action's media args. A flat array still works for params that
|
||||
are intentionally shared across every exposed action.
|
||||
|
||||
If your platform stores extra scope inside conversation ids, keep that parsing
|
||||
in the plugin with `messaging.resolveSessionConversation(...)`. That is the
|
||||
|
||||
@@ -92,12 +92,9 @@ describe("matrixMessageActions", () => {
|
||||
|
||||
expect(actions).toContain(profileAction);
|
||||
expect(supportsAction({ action: profileAction } as never)).toBe(true);
|
||||
expect(discovery.mediaSourceParams).toEqual([
|
||||
"avatarUrl",
|
||||
"avatar_url",
|
||||
"avatarPath",
|
||||
"avatar_path",
|
||||
]);
|
||||
expect(discovery.mediaSourceParams).toEqual({
|
||||
"set-profile": ["avatarUrl", "avatar_url", "avatarPath", "avatar_path"],
|
||||
});
|
||||
expect(properties.displayName).toBeDefined();
|
||||
expect(properties.avatarUrl).toBeDefined();
|
||||
expect(properties.avatarPath).toBeDefined();
|
||||
|
||||
@@ -140,8 +140,8 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
|
||||
capabilities: [],
|
||||
schema: listedActions.includes("set-profile") ? buildMatrixProfileToolSchema() : null,
|
||||
mediaSourceParams: listedActions.includes("set-profile")
|
||||
? MATRIX_PROFILE_MEDIA_SOURCE_PARAMS
|
||||
: [],
|
||||
? { "set-profile": MATRIX_PROFILE_MEDIA_SOURCE_PARAMS }
|
||||
: null,
|
||||
};
|
||||
},
|
||||
supportsAction: ({ action }) => MATRIX_PLUGIN_HANDLED_ACTIONS.has(action),
|
||||
|
||||
@@ -111,10 +111,51 @@ type ResolvedChannelMessageActionDiscovery = {
|
||||
mediaSourceParams: readonly string[];
|
||||
};
|
||||
|
||||
type MessageToolMediaSourceParamMap = Partial<Record<ChannelMessageActionName, readonly string[]>>;
|
||||
|
||||
function normalizeMessageToolMediaSourceParams(
|
||||
mediaSourceParams: ChannelMessageToolDiscovery["mediaSourceParams"],
|
||||
action?: ChannelMessageActionName,
|
||||
): readonly string[] {
|
||||
if (Array.isArray(mediaSourceParams)) {
|
||||
return mediaSourceParams;
|
||||
}
|
||||
if (!mediaSourceParams || typeof mediaSourceParams !== "object") {
|
||||
return [];
|
||||
}
|
||||
const scopedMediaSourceParams = mediaSourceParams as MessageToolMediaSourceParamMap;
|
||||
if (action) {
|
||||
const scoped = scopedMediaSourceParams[action];
|
||||
return Array.isArray(scoped) ? scoped : [];
|
||||
}
|
||||
return Object.values(scopedMediaSourceParams).flatMap((scoped) =>
|
||||
Array.isArray(scoped) ? scoped : [],
|
||||
);
|
||||
}
|
||||
|
||||
function resolveCurrentChannelPluginActions(channel?: string): {
|
||||
pluginId: string;
|
||||
actions: ChannelActions;
|
||||
} | null {
|
||||
const channelId = resolveMessageActionDiscoveryChannelId(channel);
|
||||
if (!channelId) {
|
||||
return null;
|
||||
}
|
||||
const plugin = getChannelPlugin(channelId as Parameters<typeof getChannelPlugin>[0]);
|
||||
if (!plugin?.actions) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
pluginId: plugin.id,
|
||||
actions: plugin.actions,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveMessageActionDiscoveryForPlugin(params: {
|
||||
pluginId: string;
|
||||
actions?: ChannelActions;
|
||||
context: ChannelMessageActionDiscoveryContext;
|
||||
action?: ChannelMessageActionName;
|
||||
includeActions?: boolean;
|
||||
includeCapabilities?: boolean;
|
||||
includeSchema?: boolean;
|
||||
@@ -144,9 +185,10 @@ export function resolveMessageActionDiscoveryForPlugin(params: {
|
||||
schemaContributions: params.includeSchema
|
||||
? normalizeToolSchemaContributions(described?.schema)
|
||||
: [],
|
||||
mediaSourceParams: Array.isArray(described?.mediaSourceParams)
|
||||
? described.mediaSourceParams
|
||||
: [],
|
||||
mediaSourceParams: normalizeMessageToolMediaSourceParams(
|
||||
described?.mediaSourceParams,
|
||||
params.action,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -193,21 +235,18 @@ export function listChannelMessageCapabilitiesForChannel(params: {
|
||||
requesterSenderId?: string | null;
|
||||
senderIsOwner?: boolean;
|
||||
}): ChannelMessageCapability[] {
|
||||
const channelId = resolveMessageActionDiscoveryChannelId(params.channel);
|
||||
if (!channelId) {
|
||||
const pluginActions = resolveCurrentChannelPluginActions(params.channel);
|
||||
if (!pluginActions) {
|
||||
return [];
|
||||
}
|
||||
const plugin = getChannelPlugin(channelId as Parameters<typeof getChannelPlugin>[0]);
|
||||
return plugin?.actions
|
||||
? Array.from(
|
||||
resolveMessageActionDiscoveryForPlugin({
|
||||
pluginId: plugin.id,
|
||||
actions: plugin.actions,
|
||||
context: createMessageActionDiscoveryContext(params),
|
||||
includeCapabilities: true,
|
||||
}).capabilities,
|
||||
)
|
||||
: [];
|
||||
return Array.from(
|
||||
resolveMessageActionDiscoveryForPlugin({
|
||||
pluginId: pluginActions.pluginId,
|
||||
actions: pluginActions.actions,
|
||||
context: createMessageActionDiscoveryContext(params),
|
||||
includeCapabilities: true,
|
||||
}).capabilities,
|
||||
);
|
||||
}
|
||||
|
||||
function mergeToolSchemaProperties(
|
||||
@@ -267,6 +306,7 @@ export function resolveChannelMessageToolSchemaProperties(params: {
|
||||
|
||||
export function resolveChannelMessageToolMediaSourceParamKeys(params: {
|
||||
cfg: OpenClawConfig;
|
||||
action?: ChannelMessageActionName;
|
||||
channel?: string;
|
||||
currentChannelId?: string | null;
|
||||
currentThreadTs?: string | null;
|
||||
@@ -278,19 +318,15 @@ export function resolveChannelMessageToolMediaSourceParamKeys(params: {
|
||||
requesterSenderId?: string | null;
|
||||
senderIsOwner?: boolean;
|
||||
}): string[] {
|
||||
const channelId = resolveMessageActionDiscoveryChannelId(params.channel);
|
||||
if (!channelId) {
|
||||
const pluginActions = resolveCurrentChannelPluginActions(params.channel);
|
||||
if (!pluginActions) {
|
||||
return [];
|
||||
}
|
||||
const plugin = getChannelPlugin(channelId as Parameters<typeof getChannelPlugin>[0]);
|
||||
if (!plugin?.actions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const described = resolveMessageActionDiscoveryForPlugin({
|
||||
pluginId: plugin.id,
|
||||
actions: plugin.actions,
|
||||
pluginId: pluginActions.pluginId,
|
||||
actions: pluginActions.actions,
|
||||
context: createMessageActionDiscoveryContext(params),
|
||||
action: params.action,
|
||||
includeSchema: false,
|
||||
});
|
||||
return Array.from(new Set(described.mediaSourceParams));
|
||||
|
||||
@@ -200,7 +200,7 @@ describe("message action capability checks", () => {
|
||||
).toHaveProperty("components");
|
||||
});
|
||||
|
||||
it("derives plugin-owned media-source params from message-tool discovery", () => {
|
||||
it("derives plugin-owned media-source params for the current action", () => {
|
||||
const mediaPlugin: ChannelPlugin = {
|
||||
...createChannelTestPluginBase({
|
||||
id: "demo-media",
|
||||
@@ -212,8 +212,10 @@ describe("message action capability checks", () => {
|
||||
}),
|
||||
actions: {
|
||||
describeMessageTool: () => ({
|
||||
actions: ["set-profile"],
|
||||
mediaSourceParams: ["avatarUrl", "avatarPath"],
|
||||
actions: ["send", "set-profile"],
|
||||
mediaSourceParams: {
|
||||
"set-profile": ["avatarUrl", "avatarPath"],
|
||||
},
|
||||
schema: {
|
||||
properties: {
|
||||
avatarUrl: Type.Optional(Type.String({ description: "Remote avatar URL" })),
|
||||
@@ -231,9 +233,47 @@ describe("message action capability checks", () => {
|
||||
expect(
|
||||
resolveChannelMessageToolMediaSourceParamKeys({
|
||||
cfg: {} as OpenClawConfig,
|
||||
action: "set-profile",
|
||||
channel: "demo-media",
|
||||
}),
|
||||
).toEqual(["avatarUrl", "avatarPath"]);
|
||||
expect(
|
||||
resolveChannelMessageToolMediaSourceParamKeys({
|
||||
cfg: {} as OpenClawConfig,
|
||||
action: "send",
|
||||
channel: "demo-media",
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps flat media-source param discovery for backward compatibility", () => {
|
||||
const mediaPlugin: ChannelPlugin = {
|
||||
...createChannelTestPluginBase({
|
||||
id: "demo-media-flat",
|
||||
label: "Demo Media Flat",
|
||||
capabilities: { chatTypes: ["direct", "group"] },
|
||||
config: {
|
||||
listAccountIds: () => ["default"],
|
||||
},
|
||||
}),
|
||||
actions: {
|
||||
describeMessageTool: () => ({
|
||||
actions: ["set-profile"],
|
||||
mediaSourceParams: ["avatarUrl", "avatarPath"],
|
||||
}),
|
||||
},
|
||||
};
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([{ pluginId: "demo-media-flat", source: "test", plugin: mediaPlugin }]),
|
||||
);
|
||||
|
||||
expect(
|
||||
resolveChannelMessageToolMediaSourceParamKeys({
|
||||
cfg: {} as OpenClawConfig,
|
||||
action: "set-profile",
|
||||
channel: "demo-media-flat",
|
||||
}),
|
||||
).toEqual(["avatarUrl", "avatarPath"]);
|
||||
});
|
||||
|
||||
it("skips crashing action/capability discovery paths and logs once", () => {
|
||||
|
||||
@@ -62,6 +62,10 @@ export type ChannelMessageToolSchemaContribution = {
|
||||
visibility?: "current-channel" | "all-configured";
|
||||
};
|
||||
|
||||
type ChannelMessageToolMediaSourceParams =
|
||||
| readonly string[]
|
||||
| Partial<Record<ChannelMessageActionName, readonly string[]>>;
|
||||
|
||||
export type ChannelMessageToolDiscovery = {
|
||||
actions?: readonly ChannelMessageActionName[] | null;
|
||||
capabilities?: readonly ChannelMessageCapability[] | null;
|
||||
@@ -69,9 +73,10 @@ export type ChannelMessageToolDiscovery = {
|
||||
/**
|
||||
* Plugin-owned message-tool params that carry media sources.
|
||||
* Core uses this to derive sandbox path normalization and host media-access
|
||||
* hints without hardcoding plugin-specific param names.
|
||||
* hints without hardcoding plugin-specific param names. Prefer scoping keys
|
||||
* by action so unrelated actions do not inherit another action's media args.
|
||||
*/
|
||||
mediaSourceParams?: readonly string[] | null;
|
||||
mediaSourceParams?: ChannelMessageToolMediaSourceParams | null;
|
||||
};
|
||||
|
||||
/** Shared setup input bag used by CLI, onboarding, and setup adapters. */
|
||||
|
||||
@@ -39,6 +39,7 @@ function buildActionMediaSourceParamKeys(extraParamKeys?: readonly string[]): st
|
||||
|
||||
export function resolveExtraActionMediaSourceParamKeys(params: {
|
||||
cfg: OpenClawConfig;
|
||||
action?: ChannelMessageActionName;
|
||||
channel?: string;
|
||||
accountId?: string | null;
|
||||
sessionKey?: string | null;
|
||||
@@ -49,6 +50,7 @@ export function resolveExtraActionMediaSourceParamKeys(params: {
|
||||
}): string[] {
|
||||
return resolveChannelMessageToolMediaSourceParamKeys({
|
||||
cfg: params.cfg,
|
||||
action: params.action,
|
||||
channel: params.channel,
|
||||
accountId: params.accountId,
|
||||
sessionKey: params.sessionKey,
|
||||
|
||||
@@ -536,10 +536,18 @@ describe("runMessageAction media behavior", () => {
|
||||
isConfigured: () => true,
|
||||
},
|
||||
}),
|
||||
outbound: {
|
||||
deliveryMode: "direct",
|
||||
resolveTarget: ({ to }) => ({ ok: true, to: to?.trim() ?? "profile-demo-target" }),
|
||||
sendText: async () => ({ channel: "profile-demo", messageId: "msg-test" }),
|
||||
sendMedia: async () => ({ channel: "profile-demo", messageId: "msg-test" }),
|
||||
},
|
||||
actions: {
|
||||
describeMessageTool: () => ({
|
||||
actions: ["set-profile"],
|
||||
mediaSourceParams: ["avatarPath", "avatarUrl"],
|
||||
actions: ["send", "set-profile"],
|
||||
mediaSourceParams: {
|
||||
"set-profile": ["avatarPath", "avatarUrl"],
|
||||
},
|
||||
schema: {
|
||||
properties: {
|
||||
avatarPath: Type.Optional(Type.String({ description: "Local avatar path" })),
|
||||
@@ -548,7 +556,7 @@ describe("runMessageAction media behavior", () => {
|
||||
},
|
||||
},
|
||||
}),
|
||||
supportsAction: ({ action }) => action === "set-profile",
|
||||
supportsAction: ({ action }) => action === "set-profile" || action === "send",
|
||||
handleAction: async ({ params, mediaLocalRoots }) =>
|
||||
jsonResult({
|
||||
ok: true,
|
||||
@@ -622,6 +630,29 @@ describe("runMessageAction media behavior", () => {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not apply set-profile media params to send actions", async () => {
|
||||
await withSandbox(async (sandboxDir) => {
|
||||
const avatarUrl = "data:text/plain;base64,SGVsbG8=";
|
||||
const result = await runMessageAction({
|
||||
cfg: {} as OpenClawConfig,
|
||||
action: "send",
|
||||
dryRun: true,
|
||||
params: {
|
||||
channel: "profile-demo",
|
||||
target: "@profile-demo",
|
||||
message: "hi",
|
||||
avatarUrl,
|
||||
},
|
||||
sandboxRoot: sandboxDir,
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("send");
|
||||
expect(result.sendResult).toMatchObject({
|
||||
channel: "profile-demo",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("sandboxed media validation", () => {
|
||||
|
||||
@@ -852,6 +852,7 @@ export async function runMessageAction(
|
||||
});
|
||||
const extraActionMediaSourceParamKeys = resolveExtraActionMediaSourceParamKeys({
|
||||
cfg,
|
||||
action,
|
||||
channel,
|
||||
accountId,
|
||||
sessionKey: input.sessionKey,
|
||||
|
||||
Reference in New Issue
Block a user