




























@@ -0,0 +1,167 @@
1+const DATA_URL_PREFIX = "data:";
2+const IMAGE_OMITTED_TEXT = "omitted image payload: invalid inline image data";
3+4+function startsWithDataUrl(value: string): boolean {
5+return value.slice(0, DATA_URL_PREFIX.length).toLowerCase() === DATA_URL_PREFIX;
6+}
7+8+function canonicalizeBase64(base64: string): string | undefined {
9+let cleaned = "";
10+let padding = 0;
11+let sawPadding = false;
12+for (let i = 0; i < base64.length; i += 1) {
13+const code = base64.charCodeAt(i);
14+if (code <= 0x20) {
15+continue;
16+}
17+if (code === 0x3d) {
18+padding += 1;
19+if (padding > 2) {
20+return undefined;
21+}
22+sawPadding = true;
23+cleaned += "=";
24+continue;
25+}
26+const isBase64DataChar =
27+(code >= 0x41 && code <= 0x5a) ||
28+(code >= 0x61 && code <= 0x7a) ||
29+(code >= 0x30 && code <= 0x39) ||
30+code === 0x2b ||
31+code === 0x2f;
32+if (sawPadding || !isBase64DataChar) {
33+return undefined;
34+}
35+cleaned += base64[i];
36+}
37+if (!cleaned || cleaned.length % 4 !== 0) {
38+return undefined;
39+}
40+return cleaned;
41+}
42+43+function sniffImageMime(buffer: Buffer): string | undefined {
44+if (
45+buffer.length >= 8 &&
46+buffer[0] === 0x89 &&
47+buffer[1] === 0x50 &&
48+buffer[2] === 0x4e &&
49+buffer[3] === 0x47 &&
50+buffer[4] === 0x0d &&
51+buffer[5] === 0x0a &&
52+buffer[6] === 0x1a &&
53+buffer[7] === 0x0a
54+) {
55+return "image/png";
56+}
57+if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
58+return "image/jpeg";
59+}
60+if (
61+buffer.length >= 12 &&
62+buffer.subarray(0, 4).toString("ascii") === "RIFF" &&
63+buffer.subarray(8, 12).toString("ascii") === "WEBP"
64+) {
65+return "image/webp";
66+}
67+if (
68+buffer.length >= 6 &&
69+(buffer.subarray(0, 6).toString("ascii") === "GIF87a" ||
70+buffer.subarray(0, 6).toString("ascii") === "GIF89a")
71+) {
72+return "image/gif";
73+}
74+return undefined;
75+}
76+77+export function sanitizeInlineImageDataUrl(imageUrl: string): string | undefined {
78+if (!startsWithDataUrl(imageUrl)) {
79+return imageUrl;
80+}
81+const commaIndex = imageUrl.indexOf(",");
82+if (commaIndex < 0) {
83+return undefined;
84+}
85+86+const metadata = imageUrl.slice(DATA_URL_PREFIX.length, commaIndex);
87+const payload = imageUrl.slice(commaIndex + 1);
88+const metadataParts = metadata.split(";").map((part) => part.trim());
89+const declaredMimeType = metadataParts[0]?.toLowerCase();
90+if (!declaredMimeType?.startsWith("image/")) {
91+return undefined;
92+}
93+if (!metadataParts.slice(1).some((part) => part.toLowerCase() === "base64")) {
94+return undefined;
95+}
96+97+const canonicalPayload = canonicalizeBase64(payload);
98+if (!canonicalPayload) {
99+return undefined;
100+}
101+const sniffedMimeType = sniffImageMime(Buffer.from(canonicalPayload, "base64"));
102+if (!sniffedMimeType) {
103+return undefined;
104+}
105+return `data:${sniffedMimeType};base64,${canonicalPayload}`;
106+}
107+108+export function invalidInlineImageText(label: string): string {
109+return `[${label}] ${IMAGE_OMITTED_TEXT}`;
110+}
111+112+function isRecord(value: unknown): value is Record<string, unknown> {
113+return Boolean(value && typeof value === "object" && !Array.isArray(value));
114+}
115+116+function sanitizeImageContentRecord(
117+record: Record<string, unknown>,
118+label: string,
119+): Record<string, unknown> | undefined {
120+if (record.type === "image" && typeof record.data === "string") {
121+const mimeType = typeof record.mimeType === "string" ? record.mimeType : "image/png";
122+const imageUrl = sanitizeInlineImageDataUrl(`data:${mimeType};base64,${record.data}`);
123+if (!imageUrl) {
124+return { type: "text", text: invalidInlineImageText(label) };
125+}
126+const commaIndex = imageUrl.indexOf(",");
127+const metadata = imageUrl.slice(DATA_URL_PREFIX.length, commaIndex);
128+const mime = metadata.split(";")[0] ?? mimeType;
129+return { ...record, mimeType: mime, data: imageUrl.slice(commaIndex + 1) };
130+}
131+132+if (record.type === "inputImage" && typeof record.imageUrl === "string") {
133+const imageUrl = sanitizeInlineImageDataUrl(record.imageUrl);
134+return imageUrl
135+ ? { ...record, imageUrl }
136+ : { type: "inputText", text: invalidInlineImageText(label) };
137+}
138+139+if (record.type === "input_image" && typeof record.image_url === "string") {
140+const imageUrl = sanitizeInlineImageDataUrl(record.image_url);
141+return imageUrl
142+ ? { ...record, image_url: imageUrl }
143+ : { type: "input_text", text: invalidInlineImageText(label) };
144+}
145+146+return undefined;
147+}
148+149+export function sanitizeCodexHistoryImagePayloads<T>(value: T, label: string): T {
150+if (Array.isArray(value)) {
151+return value.map((entry) => sanitizeCodexHistoryImagePayloads(entry, label)) as T;
152+}
153+if (!isRecord(value)) {
154+return value;
155+}
156+157+const imageRecord = sanitizeImageContentRecord(value, label);
158+if (imageRecord) {
159+return imageRecord as T;
160+}
161+162+const next: Record<string, unknown> = {};
163+for (const [key, child] of Object.entries(value)) {
164+next[key] = sanitizeCodexHistoryImagePayloads(child, label);
165+}
166+return next as T;
167+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。