From be1ad6be1d86b6daabdbc83bea5a43d7caa1841f Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 8 Feb 2026 23:59:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=94=9F=E5=9B=BE=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E8=BF=94=E5=9B=9Ebase64=E5=9B=BE=E7=89=87=E5=8E=9F?= =?UTF-8?q?=E6=96=87=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(chat)/api/chat/route.ts | 200 +++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 6b915ff..a3ffbea 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -2,6 +2,7 @@ import { geolocation } from "@vercel/functions"; import { convertToModelMessages, createUIMessageStream, + generateText, JsonToSseTransformStream, smoothStream, stepCountIs, @@ -49,6 +50,7 @@ import { db } from "@/lib/db"; import type { DBMessage } from "@/lib/db/schema"; import { generatedImage } from "@/lib/db/schema"; import { ChatSDKError } from "@/lib/errors"; +import { uploadFile } from "@/lib/storage/service"; import type { ChatMessage } from "@/lib/types"; import type { AppUsage } from "@/lib/usage"; import { convertToUIMessages, generateUUID } from "@/lib/utils"; @@ -447,6 +449,76 @@ function extractOriginalImageUrlsFromText(text: string): string[] { return imageUrls; } +function mimeTypeToExtension(mimeType: string) { + const normalized = mimeType.toLowerCase(); + if (normalized.includes("png")) return "png"; + if (normalized.includes("webp")) return "webp"; + if (normalized.includes("gif")) return "gif"; + if (normalized.includes("bmp")) return "bmp"; + return "jpg"; +} + +async function uploadDataUrlImage(dataUrl: string): Promise { + const match = dataUrl.match(/^data:([^;]+);base64,([\s\S]+)$/i); + if (!match) { + throw new Error("Invalid data URL image format"); + } + + const mimeType = match[1] || "image/jpeg"; + const base64Data = (match[2] || "").replace(/\s+/g, ""); + if (!base64Data) { + throw new Error("Empty base64 image content"); + } + + const buffer = Buffer.from(base64Data, "base64"); + const fileExt = mimeTypeToExtension(mimeType); + const fileName = `inline-image-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${fileExt}`; + const arrayBuffer = buffer.buffer.slice( + buffer.byteOffset, + buffer.byteOffset + buffer.byteLength + ) as ArrayBuffer; + const uploaded = await uploadFile(fileName, arrayBuffer); + return uploaded.url; +} + +async function normalizeAssistantImageMarkdown( + text: string, + imageSize = "2K" +): Promise { + if (!text || !text.includes("data:image")) return text; + + const markdownDataUrlRegex = /!\[([^\]]*)\]\s*\(\s*(data:image\/[a-z0-9+.-]+;base64,[^)]+)\s*\)/gim; + const matches = [...text.matchAll(markdownDataUrlRegex)]; + if (matches.length === 0) { + return text; + } + + let normalizedText = ""; + let cursor = 0; + + for (const match of matches) { + const matchText = match[0]; + const dataUrl = match[2]; + const start = match.index ?? -1; + if (start < 0) continue; + + normalizedText += text.slice(cursor, start); + + try { + const uploadedUrl = await uploadDataUrlImage(dataUrl); + normalizedText += `![thumb|||${uploadedUrl}|||${imageSize}](${uploadedUrl})`; + } catch (error) { + console.error("[Chat API] Failed to upload inline data URL image:", error); + normalizedText += matchText; + } + + cursor = start + matchText.length; + } + + normalizedText += text.slice(cursor); + return normalizedText; +} + /** * 解析生图 Markdown 并保存图片记录到数据库 * 支持三种格式:thumb|||url|||size、thumb$$url$$size、thumb|url|size @@ -2182,8 +2254,136 @@ export async function POST(request: Request) { const providerType = (languageModel as any).provider; console.log("[Chat API] Provider type for tool detection:", providerType); const isVolcanoVideoModel = providerType === "volcano-video" || actualModelId.includes("seedance"); + const isOpenAICompatImageModel = + resolvedModelSupportsImageGen && providerType === "openai.chat"; console.log("[Chat API] Is volcano video model:", isVolcanoVideoModel); + // 🆕 处理 OpenAI 兼容的生图模型输出(防止返回 data:image;base64 原文) + if (isOpenAICompatImageModel) { + console.log("[Chat API] Using generateText for OpenAI-compatible image model"); + + // 生成开始标记 + dataStream.write({ + type: "data-appendMessage", + data: JSON.stringify({ + id: generateUUID(), + role: "assistant", + parts: [{ type: "text", text: "" }], + }), + }); + + try { + const imageRequestTimeoutMs = 180_000; + const generated = await generateText({ + model: languageModel, + system: systemPrompt({ selectedChatModel: finalModelId, requestHints }), + messages: convertedMessages, + abortSignal: AbortSignal.timeout(imageRequestTimeoutMs), + }); + + const rawText = generated.text?.trim() || "图片生成失败,请稍后重试"; + const normalizedText = await normalizeAssistantImageMarkdown( + rawText, + requestBody.imageParams?.imageSize || "2K" + ); + + const messageId = generateUUID(); + dataStream.write({ + type: "data-appendMessage", + data: JSON.stringify({ + id: messageId, + role: "assistant", + parts: [{ type: "text", text: normalizedText }], + }), + }); + + // 生成结束标记 + dataStream.write({ + type: "data-appendMessage", + data: JSON.stringify({ + id: generateUUID(), + role: "assistant", + parts: [{ type: "text", text: "" }], + }), + }); + + await saveMessages({ + messages: [ + { + id: messageId, + role: "assistant", + parts: [{ type: "text", text: normalizedText }], + createdAt: new Date(), + attachments: [], + chatId: id, + videoStorageStatus: null, + temporaryVideoUrl: null, + permanentVideoUrl: null, + archivedAt: null, + videoMetadata: null, + }, + ], + }); + + const latestUserMessage = uiMessages.filter((m) => m.role === "user").pop(); + const userPrompt = + latestUserMessage?.parts?.find( + (p): p is { type: "text"; text: string } => p.type === "text" + )?.text || ""; + await saveGeneratedImages( + normalizedText, + session.user.id, + id, + messageId, + userPrompt, + actualModelId + ); + return; + } catch (error) { + console.error("[Chat API] OpenAI-compatible image model generation error:", error); + + const errorMessageId = generateUUID(); + const errorText = + error instanceof Error ? error.message : "图片生成失败,请稍后重试"; + + dataStream.write({ + type: "data-appendMessage", + data: JSON.stringify({ + id: errorMessageId, + role: "assistant", + parts: [{ type: "text", text: errorText }], + }), + }); + dataStream.write({ + type: "data-appendMessage", + data: JSON.stringify({ + id: generateUUID(), + role: "assistant", + parts: [{ type: "text", text: "" }], + }), + }); + + await saveMessages({ + messages: [ + { + id: errorMessageId, + role: "assistant", + parts: [{ type: "text", text: errorText }], + createdAt: new Date(), + attachments: [], + chatId: id, + videoStorageStatus: null, + temporaryVideoUrl: null, + permanentVideoUrl: null, + archivedAt: null, + videoMetadata: null, + }, + ], + }); + return; + } + } + // 🎬 视频模型使用专门的生成流程 if (isVolcanoVideoModel) { console.log("[Chat API] Using video generation API");