diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 17ed0bbc0..6d45fa735 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -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); diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 73c2ab94e..836da49e1 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -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("") + ) + ); + const currentModel = allModels.find(m => m.id === selectedModelId); + const shouldShowImageLoading = (isLoading || hasGeneratingMarker) && !!currentModel; + const loadingImageCount = imageParams.imageCount || 1; return (
@@ -1331,6 +1340,15 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
); })} + + {shouldShowImageLoading && ( + + )} {isLoading && (
diff --git a/lib/ai/providers/alibaba-image.ts b/lib/ai/providers/alibaba-image.ts index b0ab61dc4..60e88cfcf 100644 --- a/lib/ai/providers/alibaba-image.ts +++ b/lib/ai/providers/alibaba-image.ts @@ -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 diff --git a/lib/ai/providers/volcano-vision.ts b/lib/ai/providers/volcano-vision.ts index dbbe6e468..9d89e32ea 100644 --- a/lib/ai/providers/volcano-vision.ts +++ b/lib/ai/providers/volcano-vision.ts @@ -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);