mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(gateway): allow downloading zero-byte artifacts (#122928)
* fix(gateway): preserve zero-byte artifacts * fix(gateway): keep non-string data out of artifact lists * test(gateway): compact zero-byte artifact coverage
This commit is contained in:
committed by
GitHub
parent
495f295f25
commit
132299bcfa
@@ -52,7 +52,7 @@ export function readArtifactBase64Payload(
|
||||
value: string | undefined,
|
||||
opts: { includeData: boolean },
|
||||
): ArtifactBase64Payload | undefined {
|
||||
if (!value) {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
let encodedLength = 0;
|
||||
@@ -83,9 +83,6 @@ export function readArtifactBase64Payload(
|
||||
data += normalizeArtifactBase64Char(char);
|
||||
}
|
||||
}
|
||||
if (encodedLength === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const remainder = encodedLength % 4;
|
||||
if ((padding > 0 && remainder !== 0) || remainder === 1) {
|
||||
return undefined;
|
||||
|
||||
@@ -409,6 +409,47 @@ describe("artifacts RPC handlers", () => {
|
||||
expectFields(downloadPayload.artifact, { id: artifactId });
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ type: "file", data: "", sizeBytes: 0, title: "direct.bin" },
|
||||
{
|
||||
type: "file",
|
||||
source: { data: "", media_type: "application/octet-stream", sizeBytes: 0 },
|
||||
title: "source.bin",
|
||||
},
|
||||
{
|
||||
type: "file",
|
||||
data: " data:application/octet-stream;base64, ",
|
||||
sizeBytes: 0,
|
||||
title: "data-url.bin",
|
||||
},
|
||||
{ data: "", sizeBytes: 0, title: "untyped.bin" },
|
||||
])("lists, gets, and downloads the zero-byte $title artifact", async (block) => {
|
||||
mockedMessages([{ role: "assistant", content: [block], __openclaw: { seq: 2 } }]);
|
||||
const artifact = expectFirstArtifact(
|
||||
(await listArtifacts({ sessionKey: "agent:main:main" })).calls,
|
||||
);
|
||||
const artifactId = requireNonEmptyString(artifact?.id, "expected zero-byte artifact id");
|
||||
const expected = { id: artifactId, sizeBytes: 0, download: { mode: "bytes" } };
|
||||
expect(artifact).toMatchObject(expected);
|
||||
expect(artifact).not.toHaveProperty("data");
|
||||
const get = await getArtifact({ sessionKey: "agent:main:main", artifactId });
|
||||
const getPayload = expectOkPayload(get.calls) as { artifact?: Record<string, unknown> };
|
||||
expect(getPayload.artifact).toMatchObject(expected);
|
||||
expect(getPayload.artifact).not.toHaveProperty("data");
|
||||
const download = await downloadArtifact({ sessionKey: "agent:main:main", artifactId });
|
||||
const payload = expectOkPayload(download.calls) as { artifact?: Record<string, unknown> };
|
||||
expectFields(payload, { encoding: "base64", data: "" });
|
||||
expect(payload.artifact).toMatchObject(expected);
|
||||
});
|
||||
it.each([null, 0, false, {}])(
|
||||
"does not discover untyped non-string data as an artifact: %j",
|
||||
async (data) => {
|
||||
mockedMessages([{ role: "assistant", content: [{ data }], __openclaw: { seq: 2 } }]);
|
||||
const listed = await listArtifacts({ sessionKey: "agent:main:main" });
|
||||
expect(expectArtifactList(listed.calls)).toEqual({ artifacts: [] });
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves managed artifact identity and returns a ticketed download URL", async () => {
|
||||
const artifactId = "artifact_managed_image_11111111-1111-4111-8111-111111111111";
|
||||
mockedMessages([
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { isHttpUrl } from "@openclaw/net-policy/url-protocol";
|
||||
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { normalizeOptionalString as asNonEmptyString } from "@openclaw/normalization-core/string-coerce";
|
||||
import {
|
||||
normalizeOptionalString as asNonEmptyString,
|
||||
readStringValue,
|
||||
} from "@openclaw/normalization-core/string-coerce";
|
||||
import {
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
@@ -247,13 +250,13 @@ function resolveBlockDownload(
|
||||
mimeType?: string;
|
||||
sizeBytes?: number;
|
||||
} {
|
||||
const data = asNonEmptyString(block.data);
|
||||
const content = asNonEmptyString(block.content);
|
||||
const data = readStringValue(block.data)?.trim();
|
||||
const content = readStringValue(block.content)?.trim();
|
||||
const url = asNonEmptyString(block.url) ?? asNonEmptyString(block.openUrl);
|
||||
const imageUrl = mediaUrlValue(block.image_url);
|
||||
const audioUrl = asNonEmptyString(block.audio_url);
|
||||
const source = asOptionalRecord(block.source);
|
||||
const sourceData = asNonEmptyString(source?.data);
|
||||
const sourceData = readStringValue(source?.data)?.trim();
|
||||
const sourceUrl = asNonEmptyString(source?.url);
|
||||
const dataUrl = [url, sourceUrl, imageUrl, audioUrl, data, content, sourceData].find(
|
||||
(value) => typeof value === "string" && /^data:/i.test(value),
|
||||
@@ -282,12 +285,7 @@ function resolveBlockDownload(
|
||||
? Math.floor(explicitSize)
|
||||
: base64?.sizeBytes;
|
||||
if (base64) {
|
||||
return {
|
||||
mode: "bytes",
|
||||
...(base64.data ? { data: base64.data } : {}),
|
||||
mimeType,
|
||||
sizeBytes,
|
||||
};
|
||||
return { mode: "bytes", data: base64.data, mimeType, sizeBytes };
|
||||
}
|
||||
if (remoteUrl) {
|
||||
return { mode: "url", url: remoteUrl, mimeType, sizeBytes };
|
||||
@@ -310,8 +308,9 @@ function isArtifactBlock(block: Record<string, unknown>): boolean {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return Boolean(
|
||||
block.url || block.openUrl || block.data || block.source || block.image_url || block.audio_url,
|
||||
return (
|
||||
typeof block.data === "string" ||
|
||||
Boolean(block.url || block.openUrl || block.source || block.image_url || block.audio_url)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -378,7 +377,7 @@ function collectArtifactsFromMessage(params: {
|
||||
messageSeq,
|
||||
source: "session-transcript",
|
||||
download: { mode: download.mode },
|
||||
...(download.data ? { data: download.data } : {}),
|
||||
...(download.data !== undefined ? { data: download.data } : {}),
|
||||
...(download.url ? { url: download.url } : {}),
|
||||
};
|
||||
params.artifacts.push(summary);
|
||||
|
||||
Reference in New Issue
Block a user