mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:20:43 +00:00
fix(media): tolerate missing image optimizer for in-limit images
This commit is contained in:
committed by
Peter Steinberger
parent
361737d1f1
commit
ac09ec00e8
@@ -163,6 +163,53 @@ describe("loadWebMedia", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function withUnavailableImageOptimizer<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
|
vi.resetModules();
|
||||||
|
vi.doMock("./image-ops.js", () => ({
|
||||||
|
convertHeicToJpeg: vi.fn(async (buffer: Buffer) => buffer),
|
||||||
|
hasAlphaChannel: vi.fn(async () => {
|
||||||
|
throw new Error(
|
||||||
|
"Optional dependency sharp is required for image attachment processing | Cannot find package 'sharp' imported from image-ops.js",
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
optimizeImageToPng: vi.fn(async () => {
|
||||||
|
throw new Error("should not optimize png");
|
||||||
|
}),
|
||||||
|
resizeToJpeg: vi.fn(async () => {
|
||||||
|
throw new Error("should not resize jpeg");
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} finally {
|
||||||
|
vi.doUnmock("./image-ops.js");
|
||||||
|
vi.resetModules();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("sends an in-limit original image when optional sharp optimization is unavailable", async () => {
|
||||||
|
await withUnavailableImageOptimizer(async () => {
|
||||||
|
const { loadWebMedia: loadWebMediaWithMissingOptimizer } = await import("./web-media.js");
|
||||||
|
const result = await loadWebMediaWithMissingOptimizer(
|
||||||
|
tinyPngFile,
|
||||||
|
createLocalWebMediaOptions(),
|
||||||
|
);
|
||||||
|
expect(result.kind).toBe("image");
|
||||||
|
expect(result.contentType).toBe("image/png");
|
||||||
|
expect(result.fileName).toBe("tiny.png");
|
||||||
|
expect(result.buffer.equals(Buffer.from(TINY_PNG_BASE64, "base64"))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not bypass the size cap when optional sharp optimization is unavailable", async () => {
|
||||||
|
await withUnavailableImageOptimizer(async () => {
|
||||||
|
const { loadWebMedia: loadWebMediaWithMissingOptimizer } = await import("./web-media.js");
|
||||||
|
await expect(
|
||||||
|
loadWebMediaWithMissingOptimizer(tinyPngFile, { maxBytes: 8, localRoots: [fixtureRoot] }),
|
||||||
|
).rejects.toThrow(/Optional dependency sharp is required/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("resolves relative local media paths against the provided workspace directory", async () => {
|
it("resolves relative local media paths against the provided workspace directory", async () => {
|
||||||
const result = await loadWebMedia("chart.png", {
|
const result = await loadWebMedia("chart.png", {
|
||||||
maxBytes: 1024 * 1024,
|
maxBytes: 1024 * 1024,
|
||||||
|
|||||||
@@ -205,6 +205,23 @@ function formatCapReduce(label: string, cap: number, size: number): string {
|
|||||||
return `${label} could not be reduced below ${formatMb(cap, 0)}MB (got ${formatMb(size)}MB)`;
|
return `${label} could not be reduced below ${formatMb(cap, 0)}MB (got ${formatMb(size)}MB)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isOptionalImageOptimizerUnavailable(err: unknown): boolean {
|
||||||
|
const messages: string[] = [];
|
||||||
|
let current: unknown = err;
|
||||||
|
while (current instanceof Error) {
|
||||||
|
messages.push(current.message);
|
||||||
|
current = current.cause;
|
||||||
|
}
|
||||||
|
const detail = messages.join("\n").toLowerCase();
|
||||||
|
return (
|
||||||
|
detail.includes("optional dependency sharp is required") ||
|
||||||
|
detail.includes("cannot find package 'sharp'") ||
|
||||||
|
detail.includes('cannot find package "sharp"') ||
|
||||||
|
detail.includes("cannot find module 'sharp'") ||
|
||||||
|
detail.includes('cannot find module "sharp"')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isHeicSource(opts: { contentType?: string; fileName?: string }): boolean {
|
function isHeicSource(opts: { contentType?: string; fileName?: string }): boolean {
|
||||||
if (opts.contentType && HEIC_MIME_RE.test(opts.contentType.trim())) {
|
if (opts.contentType && HEIC_MIME_RE.test(opts.contentType.trim())) {
|
||||||
return true;
|
return true;
|
||||||
@@ -392,7 +409,25 @@ async function loadWebMediaInternal(
|
|||||||
meta?: { contentType?: string; fileName?: string },
|
meta?: { contentType?: string; fileName?: string },
|
||||||
) => {
|
) => {
|
||||||
const originalSize = buffer.length;
|
const originalSize = buffer.length;
|
||||||
const optimized = await optimizeImageWithFallback({ buffer, cap, meta });
|
let optimized: OptimizedImage;
|
||||||
|
try {
|
||||||
|
optimized = await optimizeImageWithFallback({ buffer, cap, meta });
|
||||||
|
} catch (err) {
|
||||||
|
if (isOptionalImageOptimizerUnavailable(err) && buffer.length <= cap) {
|
||||||
|
if (shouldLogVerbose()) {
|
||||||
|
logVerbose(
|
||||||
|
`Image optimizer unavailable; sending original ${formatMb(buffer.length)}MB media without optimization`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
buffer,
|
||||||
|
contentType: meta?.contentType,
|
||||||
|
kind: "image" as const,
|
||||||
|
fileName: meta?.fileName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
logOptimizedImage({ originalSize, optimized });
|
logOptimizedImage({ originalSize, optimized });
|
||||||
|
|
||||||
if (optimized.buffer.length > cap) {
|
if (optimized.buffer.length > cap) {
|
||||||
|
|||||||
Reference in New Issue
Block a user