mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 23:48:28 +00:00
refactor(outbound): centralize attachment media policy
This commit is contained in:
@@ -169,6 +169,59 @@ function normalizeBase64Payload(params: { base64?: string; contentType?: string
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AttachmentMediaPolicy =
|
||||||
|
| {
|
||||||
|
mode: "sandbox";
|
||||||
|
sandboxRoot: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
mode: "host";
|
||||||
|
localRoots?: readonly string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolveAttachmentMediaPolicy(params: {
|
||||||
|
sandboxRoot?: string;
|
||||||
|
mediaLocalRoots?: readonly string[];
|
||||||
|
}): AttachmentMediaPolicy {
|
||||||
|
const sandboxRoot = params.sandboxRoot?.trim();
|
||||||
|
if (sandboxRoot) {
|
||||||
|
return {
|
||||||
|
mode: "sandbox",
|
||||||
|
sandboxRoot,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mode: "host",
|
||||||
|
localRoots: params.mediaLocalRoots,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAttachmentMediaLoadOptions(params: {
|
||||||
|
policy: AttachmentMediaPolicy;
|
||||||
|
maxBytes?: number;
|
||||||
|
}):
|
||||||
|
| {
|
||||||
|
maxBytes?: number;
|
||||||
|
sandboxValidated: true;
|
||||||
|
readFile: (filePath: string) => Promise<Buffer>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
maxBytes?: number;
|
||||||
|
localRoots?: readonly string[];
|
||||||
|
} {
|
||||||
|
if (params.policy.mode === "sandbox") {
|
||||||
|
return {
|
||||||
|
maxBytes: params.maxBytes,
|
||||||
|
sandboxValidated: true,
|
||||||
|
readFile: (filePath: string) => fs.readFile(filePath),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
maxBytes: params.maxBytes,
|
||||||
|
localRoots: params.policy.localRoots,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function hydrateAttachmentPayload(params: {
|
async function hydrateAttachmentPayload(params: {
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
channel: ChannelId;
|
channel: ChannelId;
|
||||||
@@ -178,8 +231,7 @@ async function hydrateAttachmentPayload(params: {
|
|||||||
contentTypeParam?: string | null;
|
contentTypeParam?: string | null;
|
||||||
mediaHint?: string | null;
|
mediaHint?: string | null;
|
||||||
fileHint?: string | null;
|
fileHint?: string | null;
|
||||||
sandboxRoot?: string;
|
mediaPolicy: AttachmentMediaPolicy;
|
||||||
mediaLocalRoots?: readonly string[];
|
|
||||||
}) {
|
}) {
|
||||||
const contentTypeParam = params.contentTypeParam ?? undefined;
|
const contentTypeParam = params.contentTypeParam ?? undefined;
|
||||||
const rawBuffer = readStringParam(params.args, "buffer", { trim: false });
|
const rawBuffer = readStringParam(params.args, "buffer", { trim: false });
|
||||||
@@ -203,17 +255,10 @@ async function hydrateAttachmentPayload(params: {
|
|||||||
channel: params.channel,
|
channel: params.channel,
|
||||||
accountId: params.accountId,
|
accountId: params.accountId,
|
||||||
});
|
});
|
||||||
const sandboxRoot = params.sandboxRoot?.trim();
|
const media = await loadWebMedia(
|
||||||
const media = sandboxRoot
|
mediaSource,
|
||||||
? await loadWebMedia(mediaSource, {
|
buildAttachmentMediaLoadOptions({ policy: params.mediaPolicy, maxBytes }),
|
||||||
maxBytes,
|
);
|
||||||
sandboxValidated: true,
|
|
||||||
readFile: (filePath: string) => fs.readFile(filePath),
|
|
||||||
})
|
|
||||||
: await loadWebMedia(mediaSource, {
|
|
||||||
maxBytes,
|
|
||||||
localRoots: params.mediaLocalRoots,
|
|
||||||
});
|
|
||||||
params.args.buffer = media.buffer.toString("base64");
|
params.args.buffer = media.buffer.toString("base64");
|
||||||
if (!contentTypeParam && media.contentType) {
|
if (!contentTypeParam && media.contentType) {
|
||||||
params.args.contentType = media.contentType;
|
params.args.contentType = media.contentType;
|
||||||
@@ -287,8 +332,7 @@ async function hydrateAttachmentActionPayload(params: {
|
|||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
/** If caption is missing, copy message -> caption. */
|
/** If caption is missing, copy message -> caption. */
|
||||||
allowMessageCaptionFallback?: boolean;
|
allowMessageCaptionFallback?: boolean;
|
||||||
sandboxRoot?: string;
|
mediaPolicy: AttachmentMediaPolicy;
|
||||||
mediaLocalRoots?: readonly string[];
|
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const mediaHint = readStringParam(params.args, "media", { trim: false });
|
const mediaHint = readStringParam(params.args, "media", { trim: false });
|
||||||
const fileHint =
|
const fileHint =
|
||||||
@@ -314,8 +358,7 @@ async function hydrateAttachmentActionPayload(params: {
|
|||||||
contentTypeParam,
|
contentTypeParam,
|
||||||
mediaHint,
|
mediaHint,
|
||||||
fileHint,
|
fileHint,
|
||||||
sandboxRoot: params.sandboxRoot,
|
mediaPolicy: params.mediaPolicy,
|
||||||
mediaLocalRoots: params.mediaLocalRoots,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,8 +369,7 @@ export async function hydrateSetGroupIconParams(params: {
|
|||||||
args: Record<string, unknown>;
|
args: Record<string, unknown>;
|
||||||
action: ChannelMessageActionName;
|
action: ChannelMessageActionName;
|
||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
sandboxRoot?: string;
|
mediaPolicy: AttachmentMediaPolicy;
|
||||||
mediaLocalRoots?: readonly string[];
|
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
if (params.action !== "setGroupIcon") {
|
if (params.action !== "setGroupIcon") {
|
||||||
return;
|
return;
|
||||||
@@ -342,8 +384,7 @@ export async function hydrateSendAttachmentParams(params: {
|
|||||||
args: Record<string, unknown>;
|
args: Record<string, unknown>;
|
||||||
action: ChannelMessageActionName;
|
action: ChannelMessageActionName;
|
||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
sandboxRoot?: string;
|
mediaPolicy: AttachmentMediaPolicy;
|
||||||
mediaLocalRoots?: readonly string[];
|
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
if (params.action !== "sendAttachment") {
|
if (params.action !== "sendAttachment") {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -512,6 +512,15 @@ describe("runMessageAction sendAttachment hydration", () => {
|
|||||||
expect((result.payload as { buffer?: string }).buffer).toBe(
|
expect((result.payload as { buffer?: string }).buffer).toBe(
|
||||||
Buffer.from("hello").toString("base64"),
|
Buffer.from("hello").toString("base64"),
|
||||||
);
|
);
|
||||||
|
const call = vi.mocked(loadWebMedia).mock.calls[0];
|
||||||
|
expect(call?.[1]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
localRoots: expect.any(Array),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect((call?.[1] as { sandboxValidated?: boolean } | undefined)?.sandboxValidated).not.toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rewrites sandboxed media paths for sendAttachment", async () => {
|
it("rewrites sandboxed media paths for sendAttachment", async () => {
|
||||||
@@ -530,6 +539,11 @@ describe("runMessageAction sendAttachment hydration", () => {
|
|||||||
|
|
||||||
const call = vi.mocked(loadWebMedia).mock.calls[0];
|
const call = vi.mocked(loadWebMedia).mock.calls[0];
|
||||||
expect(call?.[0]).toBe(path.join(sandboxDir, "data", "pic.png"));
|
expect(call?.[0]).toBe(path.join(sandboxDir, "data", "pic.png"));
|
||||||
|
expect(call?.[1]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
sandboxValidated: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
parseCardParam,
|
parseCardParam,
|
||||||
parseComponentsParam,
|
parseComponentsParam,
|
||||||
readBooleanParam,
|
readBooleanParam,
|
||||||
|
resolveAttachmentMediaPolicy,
|
||||||
resolveSlackAutoThreadId,
|
resolveSlackAutoThreadId,
|
||||||
resolveTelegramAutoThreadId,
|
resolveTelegramAutoThreadId,
|
||||||
} from "./message-action-params.js";
|
} from "./message-action-params.js";
|
||||||
@@ -759,10 +760,14 @@ export async function runMessageAction(
|
|||||||
}
|
}
|
||||||
const dryRun = Boolean(input.dryRun ?? readBooleanParam(params, "dryRun"));
|
const dryRun = Boolean(input.dryRun ?? readBooleanParam(params, "dryRun"));
|
||||||
const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, resolvedAgentId);
|
const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, resolvedAgentId);
|
||||||
|
const mediaPolicy = resolveAttachmentMediaPolicy({
|
||||||
|
sandboxRoot: input.sandboxRoot,
|
||||||
|
mediaLocalRoots,
|
||||||
|
});
|
||||||
|
|
||||||
await normalizeSandboxMediaParams({
|
await normalizeSandboxMediaParams({
|
||||||
args: params,
|
args: params,
|
||||||
sandboxRoot: input.sandboxRoot,
|
sandboxRoot: mediaPolicy.mode === "sandbox" ? mediaPolicy.sandboxRoot : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
await hydrateSendAttachmentParams({
|
await hydrateSendAttachmentParams({
|
||||||
@@ -772,8 +777,7 @@ export async function runMessageAction(
|
|||||||
args: params,
|
args: params,
|
||||||
action,
|
action,
|
||||||
dryRun,
|
dryRun,
|
||||||
sandboxRoot: input.sandboxRoot,
|
mediaPolicy,
|
||||||
mediaLocalRoots,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await hydrateSetGroupIconParams({
|
await hydrateSetGroupIconParams({
|
||||||
@@ -783,8 +787,7 @@ export async function runMessageAction(
|
|||||||
args: params,
|
args: params,
|
||||||
action,
|
action,
|
||||||
dryRun,
|
dryRun,
|
||||||
sandboxRoot: input.sandboxRoot,
|
mediaPolicy,
|
||||||
mediaLocalRoots,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const resolvedTarget = await resolveActionTarget({
|
const resolvedTarget = await resolveActionTarget({
|
||||||
|
|||||||
Reference in New Issue
Block a user