添加模型后备功能和优化图片生成
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2025-12-19 15:30:18 +08:00
parent b36edf51f1
commit 712041bbeb
14 changed files with 232 additions and 54 deletions

View File

@ -486,6 +486,8 @@ export function AdminSettingsClient() {
isEnabled: true, isEnabled: true,
isDefault: false, isDefault: false,
sortOrder: 0, sortOrder: 0,
fallbackModelId1: "", // 🆕 备用模型 ID 1
fallbackModelId2: "", // 🆕 备用模型 ID 2
}); });
const [showModelDialog, setShowModelDialog] = useState(false); const [showModelDialog, setShowModelDialog] = useState(false);
const [editingModel, setEditingModel] = useState<string | null>(null); const [editingModel, setEditingModel] = useState<string | null>(null);
@ -805,6 +807,8 @@ export function AdminSettingsClient() {
isEnabled: true, isEnabled: true,
isDefault: false, isDefault: false,
sortOrder: 0, sortOrder: 0,
fallbackModelId1: "",
fallbackModelId2: "",
}); });
setEditingModel(null); setEditingModel(null);
}; };
@ -2471,7 +2475,7 @@ export function AdminSettingsClient() {
<DialogHeader> <DialogHeader>
<DialogTitle>{editingModel ? "编辑模型" : "添加模型"}</DialogTitle> <DialogTitle>{editingModel ? "编辑模型" : "添加模型"}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4 max-h-96 overflow-y-auto"> <div className="space-y-4 max-h-[70vh] overflow-y-auto">
<div className="space-y-2"> <div className="space-y-2">
<Label>所属提供商</Label> <Label>所属提供商</Label>
<Select <Select
@ -2575,6 +2579,31 @@ export function AdminSettingsClient() {
onChange={(e) => setModelForm({ ...modelForm, sortOrder: parseInt(e.target.value) || 0 })} onChange={(e) => setModelForm({ ...modelForm, sortOrder: parseInt(e.target.value) || 0 })}
/> />
</div> </div>
{/* 🆕 备用模型 ID 配置 */}
<div className="space-y-2 border-t pt-4 mt-4">
<div className="flex items-center gap-2 mb-2">
<Label className="text-sm font-medium">备用模型配置</Label>
<span className="text-xs text-muted-foreground">(主模型请求失败时自动切换)</span>
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">备用模型 ID 1(优先使用)</Label>
<Input
value={modelForm.fallbackModelId1}
onChange={(e) => setModelForm({ ...modelForm, fallbackModelId1: e.target.value })}
placeholder="例如: nano-banana-pro-backup"
/>
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">备用模型 ID 2(最后备用)</Label>
<Input
value={modelForm.fallbackModelId2}
onChange={(e) => setModelForm({ ...modelForm, fallbackModelId2: e.target.value })}
placeholder="例如: nano-banana-lite"
/>
</div>
</div>
<div className="flex gap-4"> <div className="flex gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Switch <Switch
@ -2645,6 +2674,8 @@ export function AdminSettingsClient() {
isEnabled: model.isEnabled, isEnabled: model.isEnabled,
isDefault: model.isDefault, isDefault: model.isDefault,
sortOrder: model.sortOrder || 0, sortOrder: model.sortOrder || 0,
fallbackModelId1: model.fallbackModelId1 || "",
fallbackModelId2: model.fallbackModelId2 || "",
}); });
setEditingModel(model.id); setEditingModel(model.id);
setShowModelDialog(true); setShowModelDialog(true);

View File

@ -635,11 +635,21 @@ export async function POST(request: Request) {
console.log("[Chat API] Using Gemini image generation"); console.log("[Chat API] Using Gemini image generation");
console.log("[Chat API] Image params:", requestBody.imageParams); console.log("[Chat API] Image params:", requestBody.imageParams);
// 🆕 获取备用模型配置
const currentModelConfig = await getAiModelById({ id: finalModelId });
const fallbackModelIds = [
currentModelConfig?.fallbackModelId1,
currentModelConfig?.fallbackModelId2,
].filter(Boolean) as string[];
console.log("[Chat API] Fallback model IDs:", fallbackModelIds);
const geminiStream = createUIMessageStream({ const geminiStream = createUIMessageStream({
execute: async ({ writer }) => { execute: async ({ writer }) => {
try { // 🆕 尝试生成图片的函数(支持备用模型切换)
// 🆕 只传递最后一条用户消息,避免上下文历史干扰生图效果 const tryGenerateImage = async (modelIdToUse: string, isRetry = false): Promise<string> => {
// 用户反馈:带上下文记录会影响生图效果 console.log(`[Chat API] ${isRetry ? "🔄 Retry with fallback" : "🚀 Trying"} model: ${modelIdToUse}`);
// 只传递最后一条用户消息,避免上下文历史干扰生图效果
const lastUserMessage = uiMessages.filter((m) => m.role === "user").pop(); const lastUserMessage = uiMessages.filter((m) => m.role === "user").pop();
const messagesForGemini = lastUserMessage ? [lastUserMessage] : uiMessages; const messagesForGemini = lastUserMessage ? [lastUserMessage] : uiMessages;
console.log("[Chat API] Gemini: Using only last user message (no history context)"); console.log("[Chat API] Gemini: Using only last user message (no history context)");
@ -647,15 +657,50 @@ export async function POST(request: Request) {
// 转换消息,Gemini 需要 base64 格式的图片 // 转换消息,Gemini 需要 base64 格式的图片
const modelMessages = await convertMessagesWithImages(messagesForGemini, "gemini"); const modelMessages = await convertMessagesWithImages(messagesForGemini, "gemini");
// 调用 doGenerate 获取图片 // 调用 doGenerate 获取图片(如果是备用模型,传入 overrideModelId)
const result = await (languageModel as any).doGenerate({ const result = await (languageModel as any).doGenerate({
messages: modelMessages, messages: modelMessages,
imageParams: requestBody.imageParams || { aspectRatio: "auto", imageSize: "2K" }, // 默认2K imageParams: requestBody.imageParams || { aspectRatio: "auto", imageSize: "2K" },
overrideModelId: isRetry ? modelIdToUse : undefined, // 🆕 使用备用模型 ID
}); });
const imageMarkdown = result.text || "图片生成失败"; return result.text || "图片生成失败";
};
try {
let imageMarkdown = ""; // 初始化为空字符串
let usedModelId = actualModelId;
try {
// 首先尝试主模型
imageMarkdown = await tryGenerateImage(actualModelId, false);
} catch (primaryError) {
console.error("[Chat API] Primary model failed:", primaryError);
// 🆕 尝试备用模型(使用同一个 provider,但切换 modelId)
let fallbackSuccess = false;
for (const fallbackModelId of fallbackModelIds) {
try {
console.log(`[Chat API] 🔄 Trying fallback model: ${fallbackModelId}`);
imageMarkdown = await tryGenerateImage(fallbackModelId, true);
usedModelId = fallbackModelId;
fallbackSuccess = true;
console.log(`[Chat API] ✅ Fallback model ${fallbackModelId} succeeded!`);
break;
} catch (fallbackError) {
console.error(`[Chat API] ❌ Fallback model ${fallbackModelId} failed:`, fallbackError);
}
}
// 如果所有备用模型都失败,抛出原始错误触发火山引擎故障转移
if (!fallbackSuccess) {
throw primaryError;
}
}
console.log("[Chat API] Gemini generated, length:", imageMarkdown.length); console.log("[Chat API] Gemini generated, length:", imageMarkdown.length);
console.log("[Chat API] Gemini image markdown:", imageMarkdown.substring(0, 200)); console.log("[Chat API] Gemini image markdown:", imageMarkdown.substring(0, 200));
console.log("[Chat API] Used model ID:", usedModelId);
// 生成一个统一的消息 ID // 生成一个统一的消息 ID
const messageId = generateUUID(); const messageId = generateUUID();

View File

@ -21,6 +21,8 @@ const createModelSchema = z.object({
isEnabled: z.boolean().optional(), isEnabled: z.boolean().optional(),
isDefault: z.boolean().optional(), isDefault: z.boolean().optional(),
sortOrder: z.number().optional(), sortOrder: z.number().optional(),
fallbackModelId1: z.string().max(128).optional(), // 🆕 备用模型 ID 1
fallbackModelId2: z.string().max(128).optional(), // 🆕 备用模型 ID 2
}); });
const updateModelSchema = z.object({ const updateModelSchema = z.object({
@ -34,6 +36,8 @@ const updateModelSchema = z.object({
isEnabled: z.boolean().optional(), isEnabled: z.boolean().optional(),
isDefault: z.boolean().optional(), isDefault: z.boolean().optional(),
sortOrder: z.number().optional(), sortOrder: z.number().optional(),
fallbackModelId1: z.string().max(128).optional(), // 🆕 备用模型 ID 1
fallbackModelId2: z.string().max(128).optional(), // 🆕 备用模型 ID 2
}); });
export async function GET(request: Request) { export async function GET(request: Request) {

View File

@ -478,6 +478,7 @@ export function Chat({
onImageEdit={handleImageEdit} onImageEdit={handleImageEdit}
onDrawingComplete={handleDrawingComplete} onDrawingComplete={handleDrawingComplete}
pendingGeneration={pendingGeneration} pendingGeneration={pendingGeneration}
imageCount={imageParams?.imageCount || 1}
/> />
<div className="sticky bottom-0 z-1 mx-auto w-full max-w-4xl bg-background px-2 pb-3 md:px-4 md:pb-4"> <div className="sticky bottom-0 z-1 mx-auto w-full max-w-4xl bg-background px-2 pb-3 md:px-4 md:pb-4">
@ -508,6 +509,7 @@ export function Chat({
imageModelName={currentModel?.displayName} imageModelName={currentModel?.displayName}
imageParams={imageParams} imageParams={imageParams}
onImageParamsChange={handleImageParamsChange} onImageParamsChange={handleImageParamsChange}
imageProviderType={currentModel?.providerType}
/> />
)} )}
</div> </div>

View File

@ -3,11 +3,18 @@
import { memo } from "react"; import { memo } from "react";
import { SparklesIcon } from "./icons"; import { SparklesIcon } from "./icons";
interface GeminiImageLoadingProps {
imageCount?: number; // 🆕 用户选择的图片数量,默认1张
}
/** /**
* Gemini 图片生成加载占位符组件 * Gemini 图片生成加载占位符组件
* 显示 2 个 170x200 像素的模糊动态效果 * 显示动态数量的 170x200 像素模糊动态效果
*/ */
export const GeminiImageLoading = memo(() => { export const GeminiImageLoading = memo(({ imageCount = 1 }: GeminiImageLoadingProps) => {
// 确保数量在 1-2 范围内
const count = Math.min(Math.max(imageCount, 1), 2);
return ( return (
<div <div
className="group/message fade-in w-full animate-in duration-300" className="group/message fade-in w-full animate-in duration-300"
@ -30,7 +37,7 @@ export const GeminiImageLoading = memo(() => {
{/* 文本提示 */} {/* 文本提示 */}
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="text-base font-medium text-foreground"> <span className="text-base font-medium text-foreground">
正在生成 2 张图片 正在生成 {count} 张图片
</span> </span>
<span className="flex gap-1"> <span className="flex gap-1">
<span <span
@ -48,9 +55,9 @@ export const GeminiImageLoading = memo(() => {
</span> </span>
</div> </div>
{/* 2 个图片占位符 - 横向排列,每个 170x200px */} {/* 图片占位符 - 横向排列,每个 170x200px */}
<div className="flex gap-3 flex-wrap"> <div className="flex gap-3 flex-wrap">
{[0, 1].map((index) => ( {Array.from({ length: count }, (_, index) => index).map((index) => (
<div <div
key={index} key={index}
className="relative overflow-hidden rounded-xl" className="relative overflow-hidden rounded-xl"
@ -119,7 +126,7 @@ export const GeminiImageLoading = memo(() => {
{/* 提示文字 */} {/* 提示文字 */}
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
NanoBananaPro 正在创作 2 张图片,请稍候... NanoBananaPro 正在创作 {count} 张图片,请稍候...
</span> </span>
</div> </div>
</div> </div>

View File

@ -8,13 +8,14 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Check, Image, Ratio, Maximize2 } from "lucide-react"; import { Check, Image, Ratio, Maximize2, Images } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
// 图片生成参数接口 // 图片生成参数接口
export interface ImageGenerationParams { export interface ImageGenerationParams {
aspectRatio: string; aspectRatio: string;
imageSize: string; imageSize: string;
imageCount?: number; // 🆕 生成图片数量
} }
interface ImageParamsBarProps { interface ImageParamsBarProps {
@ -22,6 +23,7 @@ interface ImageParamsBarProps {
initialParams?: Partial<ImageGenerationParams>; initialParams?: Partial<ImageGenerationParams>;
onParamsChange?: (params: ImageGenerationParams) => void; onParamsChange?: (params: ImageGenerationParams) => void;
inline?: boolean; // 内联模式:只显示参数按钮,不显示外层容器 inline?: boolean; // 内联模式:只显示参数按钮,不显示外层容器
providerType?: string; // 🆕 模型类型:gemini, volcano-vision 等
} }
// 图片比例选项(根据 Nano Banana 文档) // 图片比例选项(根据 Nano Banana 文档)
@ -51,13 +53,27 @@ export function ImageParamsBar({
initialParams, initialParams,
onParamsChange, onParamsChange,
inline = false, inline = false,
providerType = "gemini", // 默认 gemini
}: ImageParamsBarProps) { }: ImageParamsBarProps) {
const [params, setParams] = useState<ImageGenerationParams>({ const [params, setParams] = useState<ImageGenerationParams>({
aspectRatio: "auto", aspectRatio: "auto",
imageSize: "2K", // 默认 2K imageSize: "2K", // 默认 2K
imageCount: 1, // 🆕 默认 1 张
...initialParams, ...initialParams,
}); });
// 🆕 根据模型类型确定可选的图片数量
const getImageCountOptions = () => {
if (providerType === "volcano-vision") {
// 即梦4.5(豆包):1~3 张
return [1, 2, 3];
}
// Gemini:1~2 张
return [1, 2];
};
const imageCountOptions = getImageCountOptions();
// 🆕 监听 initialParams 变化,确保状态同步 // 🆕 监听 initialParams 变化,确保状态同步
useEffect(() => { useEffect(() => {
console.log("[ImageParamsBar] Mount/Update - initialParams:", initialParams); console.log("[ImageParamsBar] Mount/Update - initialParams:", initialParams);
@ -65,6 +81,7 @@ export function ImageParamsBar({
setParams({ setParams({
aspectRatio: "auto", aspectRatio: "auto",
imageSize: "2K", imageSize: "2K",
imageCount: 1,
...initialParams, ...initialParams,
}); });
} }
@ -131,6 +148,28 @@ export function ImageParamsBar({
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
{/* 🆕 图片数量 */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 px-2 gap-1 focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0">
<Images size={14} />
<span className="font-medium text-xs">{params.imageCount || 1}张</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="center" className="min-w-[80px]">
{imageCountOptions.map((count) => (
<DropdownMenuItem
key={count}
onClick={() => updateParam("imageCount", count)}
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0"
>
<Check className={cn("mr-2 h-4 w-4", (params.imageCount || 1) === count ? "opacity-100" : "opacity-0")} />
{count}张
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
); );
} }

View File

@ -38,6 +38,7 @@ type MessagesProps = {
onImageEdit?: (imageUrl: string) => void; onImageEdit?: (imageUrl: string) => void;
onDrawingComplete?: (annotatedImageUrl: string) => void; onDrawingComplete?: (annotatedImageUrl: string) => void;
pendingGeneration?: PendingGeneration | null; // 🆕 持久化的生成状态 pendingGeneration?: PendingGeneration | null; // 🆕 持久化的生成状态
imageCount?: number; // 🆕 用户选择的图片数量
}; };
function PureMessages({ function PureMessages({
@ -53,6 +54,7 @@ function PureMessages({
onImageEdit, onImageEdit,
onDrawingComplete, onDrawingComplete,
pendingGeneration, pendingGeneration,
imageCount = 1,
}: MessagesProps) { }: MessagesProps) {
const { const {
containerRef: messagesContainerRef, containerRef: messagesContainerRef,
@ -258,8 +260,8 @@ function PureMessages({
const showVideo = modelType === "volcano-video" || modelType === "qianwen-video" || (!modelType && (isQianwenVideoModel || isVideoModel)); const showVideo = modelType === "volcano-video" || modelType === "qianwen-video" || (!modelType && (isQianwenVideoModel || isVideoModel));
if (showVideo) return <QianwenVideoLoading />; if (showVideo) return <QianwenVideoLoading />;
if (showGemini) return <GeminiImageLoading />; if (showGemini) return <GeminiImageLoading imageCount={imageCount} />;
if (showVolcano) return <VolcanoVisionLoading />; if (showVolcano) return <VolcanoVisionLoading imageCount={imageCount} />;
return <ThinkingMessage />; return <ThinkingMessage />;
})()} })()}

View File

@ -87,6 +87,7 @@ function PureMultimodalInput({
imageModelName, imageModelName,
imageParams, imageParams,
onImageParamsChange, onImageParamsChange,
imageProviderType,
}: { }: {
chatId: string; chatId: string;
input: string; input: string;
@ -111,6 +112,7 @@ function PureMultimodalInput({
imageModelName?: string; imageModelName?: string;
imageParams?: ImageGenerationParams | null; imageParams?: ImageGenerationParams | null;
onImageParamsChange?: (params: ImageGenerationParams) => void; onImageParamsChange?: (params: ImageGenerationParams) => void;
imageProviderType?: string; // 🆕 图片模型提供商类型
}) { }) {
// 🔍 调试日志:检查图片模型相关props // 🔍 调试日志:检查图片模型相关props
console.log('[MultimodalInput] Render - isImageModel:', isImageModel); console.log('[MultimodalInput] Render - isImageModel:', isImageModel);
@ -836,6 +838,7 @@ function PureMultimodalInput({
initialParams={imageParams || undefined} initialParams={imageParams || undefined}
onParamsChange={onImageParamsChange} onParamsChange={onImageParamsChange}
inline={true} inline={true}
providerType={imageProviderType}
/> />
)} )}
</PromptInputTools> </PromptInputTools>

View File

@ -3,11 +3,18 @@
import { memo } from "react"; import { memo } from "react";
import { SparklesIcon } from "./icons"; import { SparklesIcon } from "./icons";
interface VolcanoVisionLoadingProps {
imageCount?: number; // 🆕 用户选择的图片数量,默认1张
}
/** /**
* 火山引擎视觉图片生成加载占位符组件 * 火山引擎视觉图片生成加载占位符组件
* 显示一个 170x200 像素的模糊动态效果 * 显示动态数量的 170x200 像素模糊动态效果
*/ */
export const VolcanoVisionLoading = memo(() => { export const VolcanoVisionLoading = memo(({ imageCount = 1 }: VolcanoVisionLoadingProps) => {
// 确保数量在 1-3 范围内
const count = Math.min(Math.max(imageCount, 1), 3);
return ( return (
<div <div
className="group/message fade-in w-full animate-in duration-300" className="group/message fade-in w-full animate-in duration-300"
@ -30,7 +37,7 @@ export const VolcanoVisionLoading = memo(() => {
{/* 文本提示 */} {/* 文本提示 */}
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="text-base font-medium text-foreground"> <span className="text-base font-medium text-foreground">
正在生成 3 张图片 正在生成 {count} 张图片
</span> </span>
<span className="flex gap-1"> <span className="flex gap-1">
<span <span
@ -48,9 +55,9 @@ export const VolcanoVisionLoading = memo(() => {
</span> </span>
</div> </div>
{/* 3 个图片占位符 - 横向排列,每个 170x200px */} {/* 图片占位符 - 横向排列,每个 170x200px */}
<div className="flex gap-3 flex-wrap"> <div className="flex gap-3 flex-wrap">
{[0, 1, 2].map((index) => ( {Array.from({ length: count }, (_, index) => index).map((index) => (
<div <div
key={index} key={index}
className="relative overflow-hidden rounded-xl" className="relative overflow-hidden rounded-xl"
@ -119,7 +126,7 @@ export const VolcanoVisionLoading = memo(() => {
{/* 提示文字 */} {/* 提示文字 */}
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
火山引擎正在创作 3 张图片,请稍候... 火山引擎正在创作 {count} 张图片,请稍候...
</span> </span>
</div> </div>
</div> </div>

View File

@ -563,7 +563,9 @@ export function createGeminiImage({ apiKey, baseURL }: GeminiImageConfig) {
doGenerate: async (options: any) => { doGenerate: async (options: any) => {
console.log("[Gemini Image] doGenerate called!"); // 调试日志 console.log("[Gemini Image] doGenerate called!"); // 调试日志
console.log("[Gemini Image] Messages received:", JSON.stringify(options.messages).substring(0, 500) + "..."); console.log("[Gemini Image] Messages received:", JSON.stringify(options.messages).substring(0, 500) + "...");
console.log("[Gemini Image] Using modelId:", modelId); // 🆕 支持覆盖 modelId(用于备用模型切换)
const effectiveModelId = options.overrideModelId || modelId;
console.log("[Gemini Image] Using modelId:", effectiveModelId, options.overrideModelId ? "(overridden)" : "(default)");
console.log("[Gemini Image] Image params from options:", options.imageParams); console.log("[Gemini Image] Image params from options:", options.imageParams);
try { try {
@ -573,31 +575,34 @@ export function createGeminiImage({ apiKey, baseURL }: GeminiImageConfig) {
aspectRatio: options.imageParams?.aspectRatio || "auto", aspectRatio: options.imageParams?.aspectRatio || "auto",
imageSize: options.imageParams?.imageSize || "2K" imageSize: options.imageParams?.imageSize || "2K"
}; };
// 🆕 获取用户选择的图片数量,默认 1 张
const imageCount = Math.min(Math.max(options.imageParams?.imageCount || 1, 1), 2); // Gemini 限制 1-2 张
console.log("[Gemini Image] Final imageParams after defaults:", imageParams); console.log("[Gemini Image] Final imageParams after defaults:", imageParams);
console.log(`[Gemini Image] Image count: ${imageCount}`);
// 🆕 并发发起两个请求,生成2张图片(类似豆包模型) // 🆕 根据用户选择的数量发起请求
console.log("[Gemini Image] Starting 2 concurrent requests..."); console.log(`[Gemini Image] Starting ${imageCount} concurrent request(s)...`);
const startTime = Date.now(); const startTime = Date.now();
const [response1, response2] = await Promise.all([ // 创建请求数组(使用 effectiveModelId)
sendGeminiRequest(baseURL, apiKey, options.messages, modelId, imageParams), const requestPromises = Array.from({ length: imageCount }, () =>
sendGeminiRequest(baseURL, apiKey, options.messages, modelId, imageParams), sendGeminiRequest(baseURL, apiKey, options.messages, effectiveModelId, imageParams)
]); );
console.log(`[Gemini Image] Both requests completed in ${Date.now() - startTime}ms`); const responses = await Promise.all(requestPromises);
console.log(`[Gemini Image] All ${imageCount} request(s) completed in ${Date.now() - startTime}ms`);
// 提取图片并上传 base64 到 S3(并行处理) // 提取图片并上传 base64 到 S3(并行处理)
const [imageUrls1, imageUrls2] = await Promise.all([ const extractPromises = responses.map(response => extractAndUploadImages(response));
extractAndUploadImages(response1), const imageUrlsArrays = await Promise.all(extractPromises);
extractAndUploadImages(response2),
]);
// 合并两次请求的图片结果 // 合并所有请求的图片结果
const allImageUrls = [...imageUrls1, ...imageUrls2]; const allImageUrls = imageUrlsArrays.flat();
const markdownText = formatImageResponse(allImageUrls, imageParams.imageSize); const markdownText = formatImageResponse(allImageUrls, imageParams.imageSize);
console.log( console.log(
`[Gemini Image] Generated ${allImageUrls.length} images from 2 concurrent requests (uploaded to S3)` `[Gemini Image] Generated ${allImageUrls.length} images from ${imageCount} concurrent request(s) (uploaded to S3)`
); );
// 返回 AI SDK v1 格式 // 返回 AI SDK v1 格式
@ -620,7 +625,9 @@ export function createGeminiImage({ apiKey, baseURL }: GeminiImageConfig) {
doStream: async function* (options: any) { doStream: async function* (options: any) {
console.log("[Gemini Image] ===== doStream ENTERED! ====="); // 调试日志 console.log("[Gemini Image] ===== doStream ENTERED! ====="); // 调试日志
console.log("[Gemini Image] Messages received:", JSON.stringify(options.messages).substring(0, 500) + "..."); console.log("[Gemini Image] Messages received:", JSON.stringify(options.messages).substring(0, 500) + "...");
console.log("[Gemini Image] Using modelId:", modelId); // 调试日志 // 🆕 支持覆盖 modelId(用于备用模型切换)
const effectiveModelId = options.overrideModelId || modelId;
console.log("[Gemini Image] Using modelId:", effectiveModelId, options.overrideModelId ? "(overridden)" : "(default)");
console.log("[Gemini Image] Image params from options:", options.imageParams); console.log("[Gemini Image] Image params from options:", options.imageParams);
try { try {
@ -630,31 +637,34 @@ export function createGeminiImage({ apiKey, baseURL }: GeminiImageConfig) {
aspectRatio: options.imageParams?.aspectRatio || "auto", aspectRatio: options.imageParams?.aspectRatio || "auto",
imageSize: options.imageParams?.imageSize || "2K" imageSize: options.imageParams?.imageSize || "2K"
}; };
// 🆕 获取用户选择的图片数量,默认 1 张
const imageCount = Math.min(Math.max(options.imageParams?.imageCount || 1, 1), 2); // Gemini 限制 1-2 张
console.log("[Gemini Image] Final imageParams after defaults:", imageParams); console.log("[Gemini Image] Final imageParams after defaults:", imageParams);
console.log(`[Gemini Image Stream] Image count: ${imageCount}`);
// 🆕 并发发起两个请求,生成2张图片(类似豆包模型) // 🆕 根据用户选择的数量发起请求
console.log("[Gemini Image Stream] Starting 2 concurrent requests..."); console.log(`[Gemini Image Stream] Starting ${imageCount} concurrent request(s)...`);
const startTime = Date.now(); const startTime = Date.now();
const [response1, response2] = await Promise.all([ // 创建请求数组(使用 effectiveModelId)
sendGeminiRequest(baseURL, apiKey, options.messages, modelId, imageParams), const requestPromises = Array.from({ length: imageCount }, () =>
sendGeminiRequest(baseURL, apiKey, options.messages, modelId, imageParams), sendGeminiRequest(baseURL, apiKey, options.messages, effectiveModelId, imageParams)
]); );
console.log(`[Gemini Image Stream] Both requests completed in ${Date.now() - startTime}ms`); const responses = await Promise.all(requestPromises);
console.log(`[Gemini Image Stream] All ${imageCount} request(s) completed in ${Date.now() - startTime}ms`);
// 提取图片并上传 base64 到 S3(并行处理) // 提取图片并上传 base64 到 S3(并行处理)
const [imageUrls1, imageUrls2] = await Promise.all([ const extractPromises = responses.map(response => extractAndUploadImages(response));
extractAndUploadImages(response1), const imageUrlsArrays = await Promise.all(extractPromises);
extractAndUploadImages(response2),
]);
// 合并两次请求的图片结果 // 合并所有请求的图片结果
const allImageUrls = [...imageUrls1, ...imageUrls2]; const allImageUrls = imageUrlsArrays.flat();
const markdownText = formatImageResponse(allImageUrls, imageParams.imageSize); const markdownText = formatImageResponse(allImageUrls, imageParams.imageSize);
console.log( console.log(
`[Gemini Image Stream] Generated ${allImageUrls.length} images from 2 concurrent requests (uploaded to S3)` `[Gemini Image Stream] Generated ${allImageUrls.length} images from ${imageCount} concurrent request(s) (uploaded to S3)`
); );
// 一次性发送整个图片 markdown(不再分块,因为 base64 图片很大) // 一次性发送整个图片 markdown(不再分块,因为 base64 图片很大)

View File

@ -543,9 +543,11 @@ export function createVolcanoVision({ apiKey, baseURL }: VolcanoVisionConfig) {
console.log("[Volcano Vision] doGenerate called!"); console.log("[Volcano Vision] doGenerate called!");
console.log("[Volcano Vision] Messages received:", JSON.stringify(options.messages).substring(0, 500) + "..."); console.log("[Volcano Vision] Messages received:", JSON.stringify(options.messages).substring(0, 500) + "...");
console.log("[Volcano Vision] Using modelId:", modelId); console.log("[Volcano Vision] Using modelId:", modelId);
console.log("[Volcano Vision] Image params from options:", options.imageParams);
// 支持可配置的图片数量(故障转移时传 1,正常使用时传 3 或不传) // 🆕 优先使用用户选择的图片数量,其次是 maxImages 参数,默认 1 张
const maxImages = options.maxImages ?? 3; const imageCount = options.imageParams?.imageCount || options.maxImages || 1;
const maxImages = Math.min(Math.max(imageCount, 1), 3); // 限制 1-3 张
console.log("[Volcano Vision] Max images:", maxImages); console.log("[Volcano Vision] Max images:", maxImages);
try { try {

View File

@ -0,0 +1,10 @@
-- 为 AI 模型添加备用模型 ID 字段
-- 当主模型请求失败时,自动切换到备用模型
ALTER TABLE "AiModel"
ADD COLUMN IF NOT EXISTS "fallbackModelId1" VARCHAR(128),
ADD COLUMN IF NOT EXISTS "fallbackModelId2" VARCHAR(128);
COMMENT ON COLUMN "AiModel"."fallbackModelId1" IS '备用模型 ID 1,当主模型请求失败时优先使用';
COMMENT ON COLUMN "AiModel"."fallbackModelId2" IS '备用模型 ID 2,当备用模型 1 也失败时使用';

View File

@ -1111,6 +1111,8 @@ export async function createAiModel({
isEnabled, isEnabled,
isDefault, isDefault,
sortOrder, sortOrder,
fallbackModelId1,
fallbackModelId2,
}: { }: {
providerId: string; providerId: string;
modelId: string; modelId: string;
@ -1121,6 +1123,8 @@ export async function createAiModel({
isEnabled?: boolean; isEnabled?: boolean;
isDefault?: boolean; isDefault?: boolean;
sortOrder?: number; sortOrder?: number;
fallbackModelId1?: string;
fallbackModelId2?: string;
}): Promise<AiModel[]> { }): Promise<AiModel[]> {
try { try {
// 如果设置为默认,先取消其他默认 // 如果设置为默认,先取消其他默认
@ -1143,6 +1147,8 @@ export async function createAiModel({
isEnabled: isEnabled ?? true, isEnabled: isEnabled ?? true,
isDefault: isDefault ?? false, isDefault: isDefault ?? false,
sortOrder: sortOrder ?? 0, sortOrder: sortOrder ?? 0,
fallbackModelId1: fallbackModelId1 || null,
fallbackModelId2: fallbackModelId2 || null,
}) })
.returning(); .returning();
} catch (_error) { } catch (_error) {
@ -1164,6 +1170,8 @@ export async function updateAiModel({
isEnabled, isEnabled,
isDefault, isDefault,
sortOrder, sortOrder,
fallbackModelId1,
fallbackModelId2,
}: { }: {
id: string; id: string;
providerId?: string; providerId?: string;
@ -1175,6 +1183,8 @@ export async function updateAiModel({
isEnabled?: boolean; isEnabled?: boolean;
isDefault?: boolean; isDefault?: boolean;
sortOrder?: number; sortOrder?: number;
fallbackModelId1?: string;
fallbackModelId2?: string;
}): Promise<AiModel[]> { }): Promise<AiModel[]> {
try { try {
// 如果设置为默认,先取消其他默认 // 如果设置为默认,先取消其他默认
@ -1196,6 +1206,9 @@ export async function updateAiModel({
if (isEnabled !== undefined) updateData.isEnabled = isEnabled; if (isEnabled !== undefined) updateData.isEnabled = isEnabled;
if (isDefault !== undefined) updateData.isDefault = isDefault; if (isDefault !== undefined) updateData.isDefault = isDefault;
if (sortOrder !== undefined) updateData.sortOrder = sortOrder; if (sortOrder !== undefined) updateData.sortOrder = sortOrder;
// 🆕 备用模型 ID(空字符串转为 null)
if (fallbackModelId1 !== undefined) updateData.fallbackModelId1 = fallbackModelId1 || null;
if (fallbackModelId2 !== undefined) updateData.fallbackModelId2 = fallbackModelId2 || null;
return await db return await db
.update(aiModel) .update(aiModel)

View File

@ -247,6 +247,9 @@ export const aiModel = pgTable("AiModel", {
// 🆕 子模型相关字段(用于火山引擎视频等需要智能选择的模型) // 🆕 子模型相关字段(用于火山引擎视频等需要智能选择的模型)
isSubModel: boolean("is_sub_model").notNull().default(false), // 是否为子模型(不在模型列表中显示) isSubModel: boolean("is_sub_model").notNull().default(false), // 是否为子模型(不在模型列表中显示)
parentModelId: uuid("parent_model_id"), // 父模型ID(如果是子模型),注:暂不添加外键约束避免循环引用 parentModelId: uuid("parent_model_id"), // 父模型ID(如果是子模型),注:暂不添加外键约束避免循环引用
// 🆕 备用模型 ID(当主模型请求失败时自动切换)
fallbackModelId1: varchar("fallbackModelId1", { length: 128 }), // 备用模型 ID 1
fallbackModelId2: varchar("fallbackModelId2", { length: 128 }), // 备用模型 ID 2
createdAt: timestamp("createdAt").notNull().defaultNow(), createdAt: timestamp("createdAt").notNull().defaultNow(),
updatedAt: timestamp("updatedAt").notNull().defaultNow(), updatedAt: timestamp("updatedAt").notNull().defaultNow(),
}); });