修复生图模型返回base64图片原文问题
This commit is contained in:
parent
c4ca7e4282
commit
be1ad6be1d
@ -2,6 +2,7 @@ import { geolocation } from "@vercel/functions";
|
|||||||
import {
|
import {
|
||||||
convertToModelMessages,
|
convertToModelMessages,
|
||||||
createUIMessageStream,
|
createUIMessageStream,
|
||||||
|
generateText,
|
||||||
JsonToSseTransformStream,
|
JsonToSseTransformStream,
|
||||||
smoothStream,
|
smoothStream,
|
||||||
stepCountIs,
|
stepCountIs,
|
||||||
@ -49,6 +50,7 @@ import { db } from "@/lib/db";
|
|||||||
import type { DBMessage } from "@/lib/db/schema";
|
import type { DBMessage } from "@/lib/db/schema";
|
||||||
import { generatedImage } from "@/lib/db/schema";
|
import { generatedImage } from "@/lib/db/schema";
|
||||||
import { ChatSDKError } from "@/lib/errors";
|
import { ChatSDKError } from "@/lib/errors";
|
||||||
|
import { uploadFile } from "@/lib/storage/service";
|
||||||
import type { ChatMessage } from "@/lib/types";
|
import type { ChatMessage } from "@/lib/types";
|
||||||
import type { AppUsage } from "@/lib/usage";
|
import type { AppUsage } from "@/lib/usage";
|
||||||
import { convertToUIMessages, generateUUID } from "@/lib/utils";
|
import { convertToUIMessages, generateUUID } from "@/lib/utils";
|
||||||
@ -447,6 +449,76 @@ function extractOriginalImageUrlsFromText(text: string): string[] {
|
|||||||
return imageUrls;
|
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 并保存图片记录到数据库
|
* 解析生图 Markdown 并保存图片记录到数据库
|
||||||
* 支持三种格式:thumb|||url|||size、thumb$$url$$size、thumb|url|size
|
* 支持三种格式: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;
|
const providerType = (languageModel as any).provider;
|
||||||
console.log("[Chat API] Provider type for tool detection:", providerType);
|
console.log("[Chat API] Provider type for tool detection:", providerType);
|
||||||
const isVolcanoVideoModel = providerType === "volcano-video" || actualModelId.includes("seedance");
|
const isVolcanoVideoModel = providerType === "volcano-video" || actualModelId.includes("seedance");
|
||||||
|
const isOpenAICompatImageModel =
|
||||||
|
resolvedModelSupportsImageGen && providerType === "openai.chat";
|
||||||
console.log("[Chat API] Is volcano video model:", isVolcanoVideoModel);
|
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) {
|
if (isVolcanoVideoModel) {
|
||||||
console.log("[Chat API] Using video generation API");
|
console.log("[Chat API] Using video generation API");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user