热修复生图链接失效导致图片不显示
This commit is contained in:
parent
83f7f3d6d1
commit
2913bf77be
@ -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 += ``;
|
||||
} 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) => ``)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user