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