修复生图模型返回base64图片原文问题
This commit is contained in:
parent
c4ca7e4282
commit
be1ad6be1d
@ -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 += ``;
|
||||
} 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");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user