添加模型后备功能和优化图片生成
This commit is contained in:
parent
b36edf51f1
commit
712041bbeb
@ -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);
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 />;
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 图片很大)
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
10
lib/db/migrations/0026_add_fallback_model_ids.sql
Normal file
10
lib/db/migrations/0026_add_fallback_model_ids.sql
Normal 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 也失败时使用';
|
||||||
|
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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(),
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user