diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index d1d1a72..27964ad 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -367,6 +367,86 @@ async function convertMessagesWithImages(messages: ChatMessage[], providerType: return convertedMessages; } +function parseImageAltMetadata(altText: string, thumbnailUrl: string): { + originalUrl: string; + imageSize: string; +} { + let originalUrl = thumbnailUrl; + let imageSize = "2K"; + + // 格式1: thumb|||originalUrl|||size (新格式) + if (altText.includes("thumb|||")) { + const content = altText.substring(altText.indexOf("thumb|||") + 8); + const parts = content.split("|||"); + originalUrl = parts[0] || thumbnailUrl; + imageSize = parts[1] || "2K"; + } + // 格式2: thumb$$originalUrl$$size (中间格式,可能被 LaTeX 解析器破坏) + else if (altText.includes("thumb$$")) { + const content = altText.substring(altText.indexOf("thumb$$") + 7); + const parts = content.split("$$"); + originalUrl = parts[0] || thumbnailUrl; + imageSize = parts[1] || "2K"; + } + // 格式3: thumb|originalUrl|size (旧格式) + else if (altText.includes("thumb|")) { + const content = altText.substring(altText.indexOf("thumb|") + 6); + const parts = content.split("|"); + originalUrl = parts[0] || thumbnailUrl; + imageSize = parts[1] || "2K"; + } + + // 回退策略:从缩略图 URL 推断原图 URL + if (originalUrl === thumbnailUrl) { + if (thumbnailUrl.includes("gemini-thumb-")) { + originalUrl = thumbnailUrl.replace("gemini-thumb-", "gemini-orig-"); + } else if (thumbnailUrl.includes("openai-thumb-")) { + originalUrl = thumbnailUrl.replace("openai-thumb-", "openai-orig-"); + } else if (thumbnailUrl.includes("volcano-thumb-")) { + originalUrl = thumbnailUrl.replace("volcano-thumb-", "volcano-orig-"); + } else if (thumbnailUrl.includes("alibaba-image-thumb-")) { + originalUrl = thumbnailUrl.replace("alibaba-image-thumb-", "alibaba-image-orig-"); + } else if (thumbnailUrl.includes("ruxa-image-thumb-")) { + originalUrl = thumbnailUrl.replace("ruxa-image-thumb-", "ruxa-image-orig-"); + } + } + + return { originalUrl, imageSize }; +} + +function extractOriginalImageUrlsFromText(text: string): string[] { + const imageRegex = /!\[([^\]]*)\]\((https?:\/\/[^\s)]+)\)/g; + const imageUrls: string[] = []; + const added = new Set(); + let hasMarkdownImages = false; + let match: RegExpExecArray | null; + + while ((match = imageRegex.exec(text)) !== null) { + hasMarkdownImages = true; + const altText = match[1] || ""; + const thumbnailUrl = match[2]; + const { originalUrl } = parseImageAltMetadata(altText, thumbnailUrl); + const finalUrl = originalUrl || thumbnailUrl; + if (finalUrl && /^https?:\/\//i.test(finalUrl) && !added.has(finalUrl)) { + added.add(finalUrl); + imageUrls.push(finalUrl); + } + } + + if (!hasMarkdownImages) { + const urlRegex = /https?:\/\/[^\s"'<>]+\.(?:jpg|jpeg|png|gif|webp|bmp|svg)(?:\?[^\s"'<>]*)?/gi; + const matchedUrls = text.match(urlRegex) || []; + for (const url of matchedUrls) { + if (!added.has(url)) { + added.add(url); + imageUrls.push(url); + } + } + } + + return imageUrls; +} + /** * 解析生图 Markdown 并保存图片记录到数据库 * 支持三种格式:thumb|||url|||size、thumb$$url$$size、thumb|url|size @@ -386,48 +466,7 @@ async function saveGeneratedImages( for (const match of matches) { const altText = match[1] || ""; const thumbnailUrl = match[2]; - let originalUrl = thumbnailUrl; - let imageSize = "2K"; - - // 解析 alt 中的原图 URL 和尺寸 - // 格式1: thumb|||originalUrl|||size (新格式) - if (altText.includes("thumb|||")) { - const content = altText.substring(altText.indexOf("thumb|||") + 8); - const parts = content.split("|||"); - originalUrl = parts[0] || thumbnailUrl; - imageSize = parts[1] || "2K"; - } - // 格式2: thumb$$originalUrl$$size (中间格式,可能被 LaTeX 解析器破坏) - else if (altText.includes("thumb$$")) { - const content = altText.substring(altText.indexOf("thumb$$") + 7); - const parts = content.split("$$"); - originalUrl = parts[0] || thumbnailUrl; - imageSize = parts[1] || "2K"; - } - // 格式3: thumb|originalUrl|size (旧格式) - else if (altText.includes("thumb|")) { - const content = altText.substring(altText.indexOf("thumb|") + 6); - const parts = content.split("|"); - originalUrl = parts[0] || thumbnailUrl; - imageSize = parts[1] || "2K"; - } - - // 回退策略:从缩略图 URL 推断原图 URL - if (originalUrl === thumbnailUrl) { - if (thumbnailUrl.includes("gemini-thumb-")) { - originalUrl = thumbnailUrl.replace("gemini-thumb-", "gemini-orig-"); - } else if (thumbnailUrl.includes("openai-thumb-")) { - originalUrl = thumbnailUrl.replace("openai-thumb-", "openai-orig-"); - } else if (thumbnailUrl.includes("volcano-thumb-")) { - originalUrl = thumbnailUrl.replace("volcano-thumb-", "volcano-orig-"); - } else if (thumbnailUrl.includes("alibaba-image-thumb-")) { - // 🆕 阿里生图模型 - originalUrl = thumbnailUrl.replace("alibaba-image-thumb-", "alibaba-image-orig-"); - } else if (thumbnailUrl.includes("ruxa-image-thumb-")) { - // 🆕 RUXA 生图模型 - originalUrl = thumbnailUrl.replace("ruxa-image-thumb-", "ruxa-image-orig-"); - } - } + const { originalUrl, imageSize } = parseImageAltMetadata(altText, thumbnailUrl); try { await db.insert(generatedImage).values({ @@ -595,11 +634,11 @@ export async function POST(request: Request) { if (msg.role === "assistant") { for (const part of parts) { if (part.type === "text" && part.text) { - // 匹配所有Markdown图片格式: ![alt](url) - const imgMatches = part.text.matchAll(/!\[.*?\]\((https?:\/\/[^\s)]+)\)/g); - for (const match of imgMatches) { - if (match[1] && !smartAutoHistoryImages.includes(match[1])) { - smartAutoHistoryImages.push(match[1]); + // 从 markdown 中优先提取原图 URL,避免把缩略图当作下一轮输入导致画质下降 + const parsedImageUrls = extractOriginalImageUrlsFromText(part.text); + for (const parsedUrl of parsedImageUrls) { + if (!smartAutoHistoryImages.includes(parsedUrl)) { + smartAutoHistoryImages.push(parsedUrl); } } }