feat: add image gen failover and loading placeholder

Enable model fallback for image generation and show the generation placeholder in the canvas chat.
This commit is contained in:
Your Name 2026-02-03 16:45:11 +08:00
parent bb4f51d198
commit b4c18bd554
4 changed files with 157 additions and 22 deletions

View File

@ -771,10 +771,23 @@ export async function POST(request: Request) {
// 🆕 获取备用模型配置
const currentModelConfig = await getAiModelById({ id: finalModelId });
const fallbackModelIds = [
const configuredFallbacks = [
currentModelConfig?.fallbackModelId1,
currentModelConfig?.fallbackModelId2,
].filter(Boolean) as string[];
const allModels = await getEnabledAiModelsWithProvider();
const sameProviderFallbacks = currentModelConfig
? allModels
.filter(m =>
m.provider.id === currentModelConfig.providerId &&
m.model.supportsImageGen &&
m.model.isEnabled &&
!m.model.isSubModel &&
m.model.modelId !== actualModelId
)
.map(m => m.model.modelId)
: [];
const fallbackModelIds = Array.from(new Set([...configuredFallbacks, ...sameProviderFallbacks]));
console.log("[Chat API] Fallback model IDs:", fallbackModelIds);
const geminiStream = createUIMessageStream({
@ -1073,6 +1086,24 @@ export async function POST(request: Request) {
// 🆕 获取模型显示名称,用于错误提示
const alibabaModelConfig = await getAiModelById({ id: finalModelId });
const modelDisplayName = alibabaModelConfig?.displayName || actualModelId;
const configuredFallbacks = [
alibabaModelConfig?.fallbackModelId1,
alibabaModelConfig?.fallbackModelId2,
].filter(Boolean) as string[];
const allModels = await getEnabledAiModelsWithProvider();
const sameProviderFallbacks = alibabaModelConfig
? allModels
.filter(m =>
m.provider.id === alibabaModelConfig.providerId &&
m.model.supportsImageGen &&
m.model.isEnabled &&
!m.model.isSubModel &&
m.model.modelId !== actualModelId
)
.map(m => m.model.modelId)
: [];
const fallbackModelIds = Array.from(new Set([...configuredFallbacks, ...sameProviderFallbacks]));
console.log("[Chat API] Alibaba fallback model IDs:", fallbackModelIds);
const alibabaStream = createUIMessageStream({
execute: async ({ writer }) => {
@ -1163,18 +1194,54 @@ export async function POST(request: Request) {
});
}
// 转换消息(阿里生图支持 URL,会自动上传 base64)
const modelMessages = await convertMessagesWithImages(messagesForAlibaba, "alibaba-image");
const tryGenerateImage = async (modelIdToUse: string, isRetry: boolean) => {
// 转换消息(阿里生图支持 URL,会自动上传 base64)
const modelMessages = await convertMessagesWithImages(messagesForAlibaba, "alibaba-image");
// 调用 doGenerate 获取图片,传递模型显示名称
const result = await (languageModel as any).doGenerate({
messages: modelMessages,
imageParams: requestBody.imageParams || { aspectRatio: "auto", imageSize: "2K", imageCount: 1 },
modelDisplayName, // 🆕 传递模型显示名称
overrideModelId: isRetry ? modelIdToUse : undefined,
});
if (result?.finishReason === "error") {
throw new Error(result.text || "图片生成失败");
}
return result.text || "图片生成失败";
};
// 调用 doGenerate 获取图片,传递模型显示名称
const result = await (languageModel as any).doGenerate({
messages: modelMessages,
imageParams: requestBody.imageParams || { aspectRatio: "auto", imageSize: "2K", imageCount: 1 },
modelDisplayName, // 🆕 传递模型显示名称
});
let imageMarkdown = "";
let usedModelId = actualModelId;
try {
imageMarkdown = await tryGenerateImage(actualModelId, false);
} catch (primaryError) {
console.error("[Chat API] Alibaba primary model failed:", primaryError);
let fallbackSuccess = false;
for (const fallbackModelId of fallbackModelIds) {
try {
console.log(`[Chat API] 🔄 Alibaba fallback model: ${fallbackModelId}`);
imageMarkdown = await tryGenerateImage(fallbackModelId, true);
usedModelId = fallbackModelId;
fallbackSuccess = true;
console.log(`[Chat API] ✅ Alibaba fallback model ${fallbackModelId} succeeded!`);
break;
} catch (fallbackError) {
console.error(`[Chat API] ❌ Alibaba fallback model ${fallbackModelId} failed:`, fallbackError);
}
}
if (!fallbackSuccess) {
throw primaryError;
}
}
const imageMarkdown = result.text || "图片生成失败";
console.log("[Chat API] Alibaba generated, length:", imageMarkdown.length);
console.log("[Chat API] Alibaba used model:", usedModelId);
// 生成消息 ID(图片消息)
const messageId = generateUUID();
@ -1220,7 +1287,7 @@ export async function POST(request: Request) {
id,
messageId,
userPrompt,
actualModelId
usedModelId
);
} catch (saveError) {
console.error("[Chat API] ❌ Failed to save Alibaba message:", saveError);
@ -1276,6 +1343,25 @@ export async function POST(request: Request) {
// 🆕 检测是否有标注图片,如果有则在消息中注入系统提示
const hasAnnotation = hasAnnotatedImage(uiMessages);
let messagesForVolcano = uiMessagesForModel;
const volcanoModelConfig = await getAiModelById({ id: finalModelId });
const configuredFallbacks = [
volcanoModelConfig?.fallbackModelId1,
volcanoModelConfig?.fallbackModelId2,
].filter(Boolean) as string[];
const allModels = await getEnabledAiModelsWithProvider();
const sameProviderFallbacks = volcanoModelConfig
? allModels
.filter(m =>
m.provider.id === volcanoModelConfig.providerId &&
m.model.supportsImageGen &&
m.model.isEnabled &&
!m.model.isSubModel &&
m.model.modelId !== actualModelId
)
.map(m => m.model.modelId)
: [];
const fallbackModelIds = Array.from(new Set([...configuredFallbacks, ...sameProviderFallbacks]));
console.log("[Chat API] Volcano fallback model IDs:", fallbackModelIds);
if (hasAnnotation) {
console.log("[Chat API] 🎨 Volcano Vision: 检测到标注图片,将注入系统提示");
@ -1344,12 +1430,40 @@ export async function POST(request: Request) {
// 转换消息,火山引擎支持 URL 和 base64
const modelMessages = await convertMessagesWithImages(messagesForVolcano, "volcano");
// 调用 doGenerate 获取图片
const result = await (languageModel as any).doGenerate({
messages: modelMessages,
});
const imageMarkdown = result.text || "图片生成失败";
const tryGenerateImage = async (modelIdToUse: string, isRetry: boolean) => {
const result = await (languageModel as any).doGenerate({
messages: modelMessages,
overrideModelId: isRetry ? modelIdToUse : undefined,
});
if (result?.finishReason === "error") {
throw new Error(result.text || "图片生成失败");
}
return result.text || "图片生成失败";
};
let imageMarkdown = "";
let usedModelId = actualModelId;
try {
imageMarkdown = await tryGenerateImage(actualModelId, false);
} catch (primaryError) {
console.error("[Chat API] Volcano primary model failed:", primaryError);
let fallbackSuccess = false;
for (const fallbackModelId of fallbackModelIds) {
try {
console.log(`[Chat API] 🔄 Volcano fallback model: ${fallbackModelId}`);
imageMarkdown = await tryGenerateImage(fallbackModelId, true);
usedModelId = fallbackModelId;
fallbackSuccess = true;
console.log(`[Chat API] ✅ Volcano fallback model ${fallbackModelId} succeeded!`);
break;
} catch (fallbackError) {
console.error(`[Chat API] ❌ Volcano fallback model ${fallbackModelId} failed:`, fallbackError);
}
}
if (!fallbackSuccess) {
throw primaryError;
}
}
console.log("[Chat API] Volcano Vision generated, length:", imageMarkdown.length);
console.log("[Chat API] Volcano Vision image markdown:", imageMarkdown.substring(0, 200));
@ -1398,7 +1512,7 @@ export async function POST(request: Request) {
id,
messageId,
userPrompt,
actualModelId
usedModelId
);
} catch (saveError) {
console.error("[Chat API] ❌ Failed to save Volcano Vision message to database:", saveError);

View File

@ -24,6 +24,7 @@ import { cn } from "@/lib/utils";
import { fetcher, fetchWithErrorHandlers, generateUUID } from "@/lib/utils";
import type { Attachment, ChatMessage } from "@/lib/types";
import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-params-bar";
import { ImageGenerationLoading } from "@/components/image-generation-loading";
// 标签悬停预览信息
interface TagPreviewInfo {
@ -1064,6 +1065,14 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
};
const isLoading = status === 'streaming' || status === 'submitted';
const hasGeneratingMarker = messages.some(msg =>
msg.role === "assistant" && msg.parts?.some(
(part: any) => part.type === "text" && part.text?.includes("<!-- GENERATING_IMAGE -->")
)
);
const currentModel = allModels.find(m => m.id === selectedModelId);
const shouldShowImageLoading = (isLoading || hasGeneratingMarker) && !!currentModel;
const loadingImageCount = imageParams.imageCount || 1;
return (
<div className="flex flex-col h-full bg-background/50 backdrop-blur-sm relative">
@ -1331,6 +1340,15 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
</div>
);
})}
{shouldShowImageLoading && (
<ImageGenerationLoading
imageCount={loadingImageCount}
maxImages={6}
providerType={currentModel?.providerType}
modelName={currentModel?.displayName}
/>
)}
{isLoading && (
<div className="flex gap-3">

View File

@ -288,9 +288,11 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) {
messages: Array<{ role: string; content: string | Array<{ type: string; text?: string; image?: string }> }>;
imageParams?: AlibabaImageParams & { aspectRatio?: string; imageSize?: string; imageCount?: number };
modelDisplayName?: string; // 🆕 模型显示名称,用于错误提示
overrideModelId?: string; // 🆕 备用模型 ID
}) => {
console.log("[Alibaba Image] doGenerate called");
console.log("[Alibaba Image] Model:", modelId);
const activeModelId = options.overrideModelId || modelId;
console.log("[Alibaba Image] Model:", activeModelId);
console.log("[Alibaba Image] Image params:", options.imageParams);
console.log("[Alibaba Image] Messages count:", options.messages.length);
console.log("[Alibaba Image] Messages:", JSON.stringify(options.messages, null, 2));
@ -481,7 +483,7 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) {
const response = await sendAlibabaImageRequest(
baseURL,
apiKey,
modelId,
activeModelId,
prompt,
imageUrls,
params

View File

@ -530,7 +530,8 @@ export function createVolcanoVision({ apiKey, baseURL }: VolcanoVisionConfig) {
doGenerate: async (options: any) => {
console.log("[Volcano Vision] doGenerate called!");
console.log("[Volcano Vision] Messages received:", JSON.stringify(options.messages).substring(0, 500) + "...");
console.log("[Volcano Vision] Using modelId:", modelId);
const activeModelId = options.overrideModelId || modelId;
console.log("[Volcano Vision] Using modelId:", activeModelId);
console.log("[Volcano Vision] Image params from options:", options.imageParams);
// 🆕 优先使用用户选择的图片数量,其次是 maxImages 参数,默认 1 张
@ -540,7 +541,7 @@ export function createVolcanoVision({ apiKey, baseURL }: VolcanoVisionConfig) {
try {
// 发送请求
const response = await sendVolcanoRequest(baseURL, apiKey, options.messages, modelId, maxImages);
const response = await sendVolcanoRequest(baseURL, apiKey, options.messages, activeModelId, maxImages);
// 提取图片并上传到 S3
const imageUrls = await extractAndUploadImages(response);