修复生图模型返回base64图片原文问题
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-08 23:59:07 +08:00
parent c4ca7e4282
commit be1ad6be1d

View File

@ -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<string> {
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<string> {
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: "<!-- IMAGE_GENERATION_START -->" }],
}),
});
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: "<!-- IMAGE_GENERATION_END -->" }],
}),
});
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: "<!-- IMAGE_GENERATION_END -->" }],
}),
});
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");