热修复生图链接失效导致图片不显示
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-02-12 12:15:28 +08:00
parent 83f7f3d6d1
commit 2913bf77be

View File

@ -481,6 +481,96 @@ function mimeTypeToExtension(mimeType: string) {
return "jpg";
}
function getExtensionFromUrl(url: string): string {
try {
const pathname = new URL(url).pathname;
const ext = pathname.split(".").pop()?.toLowerCase();
if (!ext) return "jpg";
if (["jpg", "jpeg", "png", "webp", "gif", "bmp"].includes(ext)) return ext;
} catch {
// ignore
}
return "jpg";
}
async function uploadRemoteImageToStorage(imageUrl: string): Promise<string> {
const response = await fetch(imageUrl, {
method: "GET",
signal: AbortSignal.timeout(30_000),
});
if (!response.ok) {
throw new Error(`Remote image fetch failed: ${response.status} ${response.statusText}`);
}
const contentType = response.headers.get("content-type")?.toLowerCase() || "";
if (contentType && !contentType.startsWith("image/")) {
throw new Error(`Remote url is not an image: ${contentType}`);
}
const arrayBuffer = await response.arrayBuffer();
if (!arrayBuffer || arrayBuffer.byteLength === 0) {
throw new Error("Remote image content is empty");
}
const ext = contentType ? mimeTypeToExtension(contentType) : getExtensionFromUrl(imageUrl);
const fileName = `remote-image-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
const uploaded = await uploadFile(fileName, arrayBuffer);
return uploaded.url;
}
async function isRemoteImageAccessible(imageUrl: string): Promise<boolean> {
try {
const headResponse = await fetch(imageUrl, {
method: "HEAD",
signal: AbortSignal.timeout(10_000),
});
if (headResponse.ok) {
return true;
}
if (headResponse.status !== 405 && headResponse.status !== 403) {
return false;
}
const probeResponse = await fetch(imageUrl, {
method: "GET",
headers: {
Range: "bytes=0-0",
},
signal: AbortSignal.timeout(10_000),
});
return probeResponse.ok;
} catch {
return false;
}
}
async function ensureHostedImageUrl(imageUrl: string): Promise<string> {
const normalizedUrl = imageUrl.trim();
if (!normalizedUrl) {
throw new Error("Image URL is empty");
}
if (normalizedUrl.startsWith("data:image")) {
return uploadDataUrlImage(normalizedUrl);
}
if (!/^https?:\/\//i.test(normalizedUrl)) {
throw new Error(`Unsupported image URL: ${normalizedUrl}`);
}
if (/\/uploads\//i.test(normalizedUrl)) {
const accessible = await isRemoteImageAccessible(normalizedUrl);
if (accessible) {
return normalizedUrl;
}
}
return uploadRemoteImageToStorage(normalizedUrl);
}
async function uploadDataUrlImage(dataUrl: string): Promise<string> {
const match = dataUrl.match(/^data:([^;]+);base64,([\s\S]+)$/i);
if (!match) {
@ -508,7 +598,7 @@ async function normalizeAssistantImageMarkdown(
text: string,
imageSize = "2K"
): Promise<string> {
if (!text || !text.includes("data:image")) return text;
if (!text) return text;
const markdownDataUrlRegex = /!\[([^\]]*)\]\s*\(\s*(data:image\/[a-z0-9+.-]+;base64,[^)]+)\s*\)/gim;
const markdownMatches = [...text.matchAll(markdownDataUrlRegex)];
@ -558,11 +648,53 @@ async function normalizeAssistantImageMarkdown(
}
}
const markdownHttpImageRegex = /!\[([^\]]*)\]\s*\(\s*(https?:\/\/[^)\s]+)\s*\)/gim;
const markdownHttpMatches = [...normalizedText.matchAll(markdownHttpImageRegex)];
if (markdownHttpMatches.length > 0) {
let rebuilt = "";
let cursor = 0;
for (const match of markdownHttpMatches) {
const matchText = match[0];
const altText = match[1] || "";
const thumbnailUrl = (match[2] || "").trim();
const start = match.index ?? -1;
if (start < 0) continue;
rebuilt += normalizedText.slice(cursor, start);
try {
const { originalUrl, imageSize: parsedImageSize } = parseImageAltMetadata(
altText,
thumbnailUrl
);
const hostedThumbnailUrl = await ensureHostedImageUrl(thumbnailUrl);
const hostedOriginalUrl =
originalUrl && originalUrl !== thumbnailUrl
? await ensureHostedImageUrl(originalUrl)
: hostedThumbnailUrl;
uploadedUrls.push(hostedThumbnailUrl);
uploadedUrls.push(hostedOriginalUrl);
rebuilt += `![thumb|||${hostedOriginalUrl}|||${parsedImageSize || imageSize}](${hostedThumbnailUrl})`;
} catch (error) {
console.error("[Chat API] Failed to normalize remote image URL:", error);
rebuilt += "";
}
cursor = start + matchText.length;
}
rebuilt += normalizedText.slice(cursor);
normalizedText = rebuilt;
}
const hasMarkdownImage = /!\[[^\]]*\]\((https?:\/\/[^)]+)\)/i.test(normalizedText);
if (!hasMarkdownImage && uploadedUrls.length > 0) {
const uniqueUrls = [...new Set(uploadedUrls)];
console.log(
`[Chat API] Converted ${uniqueUrls.length} data URL image(s) to hosted link(s)`
`[Chat API] Converted ${uniqueUrls.length} image link(s) to hosted link(s)`
);
return uniqueUrls
.map((url) => `![thumb|||${url}|||${imageSize}](${url})`)