mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
fix(gateway): ingest managed image file URLs (#103540)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
|
||||
import http from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createNoisyPngBuffer as createNoisyPngFixtureBuffer,
|
||||
@@ -779,35 +780,52 @@ describe("createManagedOutgoingImageBlocks", () => {
|
||||
).rejects.toThrow("Invalid image data URL");
|
||||
});
|
||||
|
||||
it("rewrites local image sources into managed display blocks without leaking the source path", async () => {
|
||||
const sourcePath = path.join(stateDir, "workspace", "fixtures", "dot.png");
|
||||
await fs.mkdir(path.dirname(sourcePath), { recursive: true });
|
||||
await fs.writeFile(sourcePath, Buffer.from(TINY_PNG_BASE64, "base64"));
|
||||
it.each([
|
||||
{ name: "local paths", sourceForPath: (sourcePath: string) => sourcePath },
|
||||
{
|
||||
name: "file URLs",
|
||||
sourceForPath: (sourcePath: string) => {
|
||||
const sourceUrl = pathToFileURL(sourcePath);
|
||||
sourceUrl.searchParams.set("sig", "secret");
|
||||
sourceUrl.hash = "preview";
|
||||
return sourceUrl.href;
|
||||
},
|
||||
},
|
||||
])(
|
||||
"rewrites $name into managed display blocks without leaking the source path",
|
||||
async ({ sourceForPath }) => {
|
||||
const sourcePath = path.join(stateDir, "workspace", "fixtures", "dot.png");
|
||||
await fs.mkdir(path.dirname(sourcePath), { recursive: true });
|
||||
await fs.writeFile(sourcePath, Buffer.from(TINY_PNG_BASE64, "base64"));
|
||||
|
||||
await withEnvAsync({ OPENCLAW_STATE_DIR: stateDir }, async () => {
|
||||
const blocks = await createManagedOutgoingImageBlocks({
|
||||
stateDir,
|
||||
sessionKey: "agent:main:main",
|
||||
mediaUrls: [sourcePath],
|
||||
localRoots: [path.join(stateDir, "workspace")],
|
||||
await withEnvAsync({ OPENCLAW_STATE_DIR: stateDir }, async () => {
|
||||
const blocks = await createManagedOutgoingImageBlocks({
|
||||
stateDir,
|
||||
sessionKey: "agent:main:main",
|
||||
mediaUrls: [sourceForPath(sourcePath)],
|
||||
localRoots: [path.join(stateDir, "workspace")],
|
||||
});
|
||||
|
||||
expect(blocks).toHaveLength(1);
|
||||
const block = requireBlock(blocks);
|
||||
expect(block.type).toBe("image");
|
||||
expect(block.url).toContain("/api/chat/media/outgoing/agent%3Amain%3Amain/");
|
||||
expect(block.openUrl).toContain("/full");
|
||||
expect(block.url).toBe(block.openUrl);
|
||||
expect(block.alt).toBe("dot.png");
|
||||
expect(JSON.stringify(block)).not.toContain(sourcePath);
|
||||
expect(JSON.stringify(block)).not.toContain("sig=secret");
|
||||
expect(JSON.stringify(block)).not.toContain("preview");
|
||||
|
||||
const attachmentId = requireAttachmentIdFromUrl(block.url);
|
||||
const record = readManagedImageRecord(attachmentId, stateDir);
|
||||
const originalPath = requireManagedOriginalPath(stateDir, attachmentId);
|
||||
expect(record?.original.filename).toMatch(/\.png$/);
|
||||
expect(originalPath).not.toBe(sourcePath);
|
||||
expect(originalPath).toContain(path.join(stateDir, "media", "outgoing", "originals"));
|
||||
});
|
||||
|
||||
expect(blocks).toHaveLength(1);
|
||||
const block = requireBlock(blocks);
|
||||
expect(block.type).toBe("image");
|
||||
expect(block.url).toContain("/api/chat/media/outgoing/agent%3Amain%3Amain/");
|
||||
expect(block.openUrl).toContain("/full");
|
||||
expect(block.url).toBe(block.openUrl);
|
||||
expect(JSON.stringify(block)).not.toContain(sourcePath);
|
||||
|
||||
const attachmentId = requireAttachmentIdFromUrl(block.url);
|
||||
const record = readManagedImageRecord(attachmentId, stateDir);
|
||||
const originalPath = requireManagedOriginalPath(stateDir, attachmentId);
|
||||
expect(record?.original.filename).toMatch(/\.png$/);
|
||||
expect(originalPath).not.toBe(sourcePath);
|
||||
expect(originalPath).toContain(path.join(stateDir, "media", "outgoing", "originals"));
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("ingests external image URLs into managed storage instead of hotlinking them", async () => {
|
||||
const imageBuffer = Buffer.from(TINY_PNG_BASE64, "base64");
|
||||
|
||||
@@ -1026,8 +1026,12 @@ export async function createManagedOutgoingImageBlocks(params: {
|
||||
for (const [index, mediaUrl] of mediaUrls.entries()) {
|
||||
const fallbackAlt = `Generated image ${index + 1}`;
|
||||
const parsedDataUrl = parseImageDataUrl(mediaUrl, fallbackAlt, limits);
|
||||
const localMediaPath =
|
||||
parsedDataUrl.kind === "image-data-url" ? undefined : resolveLocalMediaPath(mediaUrl);
|
||||
const alt =
|
||||
parsedDataUrl.kind === "image-data-url" ? fallbackAlt : deriveAltText(mediaUrl, index);
|
||||
parsedDataUrl.kind === "image-data-url"
|
||||
? fallbackAlt
|
||||
: deriveAltText(localMediaPath ?? mediaUrl, index);
|
||||
if (parsedDataUrl.kind === "non-image-data-url") {
|
||||
continue;
|
||||
}
|
||||
@@ -1048,7 +1052,6 @@ export async function createManagedOutgoingImageBlocks(params: {
|
||||
`generated-image-${index + 1}`,
|
||||
)
|
||||
: await (async () => {
|
||||
const localMediaPath = resolveLocalMediaPath(mediaUrl);
|
||||
if (localMediaPath) {
|
||||
const localRoots = params.localRoots;
|
||||
const localMediaOptions =
|
||||
@@ -1062,8 +1065,11 @@ export async function createManagedOutgoingImageBlocks(params: {
|
||||
};
|
||||
await assertLocalMediaAllowed(localMediaPath, localRoots, localMediaOptions);
|
||||
}
|
||||
// File URLs have already been normalized for display metadata and policy checks.
|
||||
// Pass that path to the store instead of treating URI syntax as a filename.
|
||||
const ingestSource = localMediaPath ?? mediaUrl;
|
||||
return await saveMediaSource(
|
||||
mediaUrl,
|
||||
ingestSource,
|
||||
undefined,
|
||||
"outgoing/originals",
|
||||
Math.max(limits.maxBytes, MEDIA_MAX_BYTES),
|
||||
|
||||
Reference in New Issue
Block a user