添加模型后备功能和优化图片生成
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,
isDefault: false,
sortOrder: 0,
fallbackModelId1: "", // 🆕 备用模型 ID 1
fallbackModelId2: "", // 🆕 备用模型 ID 2
});
const [showModelDialog, setShowModelDialog] = useState(false);
const [editingModel, setEditingModel] = useState<string | null>(null);
@ -805,6 +807,8 @@ export function AdminSettingsClient() {
isEnabled: true,
isDefault: false,
sortOrder: 0,
fallbackModelId1: "",
fallbackModelId2: "",
});
setEditingModel(null);
};
@ -2471,7 +2475,7 @@ export function AdminSettingsClient() {
<DialogHeader>
<DialogTitle>{editingModel ? "编辑模型" : "添加模型"}</DialogTitle>
</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">
<Label>所属提供商</Label>
<Select
@ -2575,6 +2579,31 @@ export function AdminSettingsClient() {
onChange={(e) => setModelForm({ ...modelForm, sortOrder: parseInt(e.target.value) || 0 })}
/>
</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 items-center gap-2">
<Switch
@ -2645,6 +2674,8 @@ export function AdminSettingsClient() {
isEnabled: model.isEnabled,
isDefault: model.isDefault,
sortOrder: model.sortOrder || 0,
fallbackModelId1: model.fallbackModelId1 || "",
fallbackModelId2: model.fallbackModelId2 || "",
});
setEditingModel(model.id);
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] 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({
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 messagesForGemini = lastUserMessage ? [lastUserMessage] : uiMessages;
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 格式的图片
const modelMessages = await convertMessagesWithImages(messagesForGemini, "gemini");
// 调用 doGenerate 获取图片
// 调用 doGenerate 获取图片(如果是备用模型,传入 overrideModelId)
const result = await (languageModel as any).doGenerate({
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 image markdown:", imageMarkdown.substring(0, 200));
console.log("[Chat API] Used model ID:", usedModelId);
// 生成一个统一的消息 ID
const messageId = generateUUID();

View File

@ -21,6 +21,8 @@ const createModelSchema = z.object({
isEnabled: z.boolean().optional(),
isDefault: z.boolean().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({
@ -34,6 +36,8 @@ const updateModelSchema = z.object({
isEnabled: z.boolean().optional(),
isDefault: z.boolean().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) {

View File

@ -478,6 +478,7 @@ export function Chat({
onImageEdit={handleImageEdit}
onDrawingComplete={handleDrawingComplete}
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">
@ -508,6 +509,7 @@ export function Chat({
imageModelName={currentModel?.displayName}
imageParams={imageParams}
onImageParamsChange={handleImageParamsChange}
imageProviderType={currentModel?.providerType}
/>
)}
</div>

View File

@ -3,11 +3,18 @@
import { memo } from "react";
import { SparklesIcon } from "./icons";
interface GeminiImageLoadingProps {
imageCount?: number; // 🆕 用户选择的图片数量,默认1张
}
/**
* 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 (
<div
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">
<span className="text-base font-medium text-foreground">
正在生成 2 张图片
正在生成 {count} 张图片
</span>
<span className="flex gap-1">
<span
@ -48,9 +55,9 @@ export const GeminiImageLoading = memo(() => {
</span>
</div>
{/* 2 个图片占位符 - 横向排列,每个 170x200px */}
{/* 图片占位符 - 横向排列,每个 170x200px */}
<div className="flex gap-3 flex-wrap">
{[0, 1].map((index) => (
{Array.from({ length: count }, (_, index) => index).map((index) => (
<div
key={index}
className="relative overflow-hidden rounded-xl"
@ -119,7 +126,7 @@ export const GeminiImageLoading = memo(() => {
{/* 提示文字 */}
<span className="text-xs text-muted-foreground">
NanoBananaPro 正在创作 2 张图片,请稍候...
NanoBananaPro 正在创作 {count} 张图片,请稍候...
</span>
</div>
</div>

View File

@ -8,13 +8,14 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} 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";
// 图片生成参数接口
export interface ImageGenerationParams {
aspectRatio: string;
imageSize: string;
imageCount?: number; // 🆕 生成图片数量
}
interface ImageParamsBarProps {
@ -22,6 +23,7 @@ interface ImageParamsBarProps {
initialParams?: Partial<ImageGenerationParams>;
onParamsChange?: (params: ImageGenerationParams) => void;
inline?: boolean; // 内联模式:只显示参数按钮,不显示外层容器
providerType?: string; // 🆕 模型类型:gemini, volcano-vision 等
}
// 图片比例选项(根据 Nano Banana 文档)
@ -51,13 +53,27 @@ export function ImageParamsBar({
initialParams,
onParamsChange,
inline = false,
providerType = "gemini", // 默认 gemini
}: ImageParamsBarProps) {
const [params, setParams] = useState<ImageGenerationParams>({
aspectRatio: "auto",
imageSize: "2K", // 默认 2K
imageCount: 1, // 🆕 默认 1 张
...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 变化,确保状态同步
useEffect(() => {
console.log("[ImageParamsBar] Mount/Update - initialParams:", initialParams);
@ -65,6 +81,7 @@ export function ImageParamsBar({
setParams({
aspectRatio: "auto",
imageSize: "2K",
imageCount: 1,
...initialParams,
});
}
@ -131,6 +148,28 @@ export function ImageParamsBar({
))}
</DropdownMenuContent>
</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>
);
}

View File

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

View File

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

View File

@ -3,11 +3,18 @@
import { memo } from "react";
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 (
<div
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">
<span className="text-base font-medium text-foreground">
正在生成 3 张图片
正在生成 {count} 张图片
</span>
<span className="flex gap-1">
<span
@ -48,9 +55,9 @@ export const VolcanoVisionLoading = memo(() => {
</span>
</div>
{/* 3 个图片占位符 - 横向排列,每个 170x200px */}
{/* 图片占位符 - 横向排列,每个 170x200px */}
<div className="flex gap-3 flex-wrap">
{[0, 1, 2].map((index) => (
{Array.from({ length: count }, (_, index) => index).map((index) => (
<div
key={index}
className="relative overflow-hidden rounded-xl"
@ -119,7 +126,7 @@ export const VolcanoVisionLoading = memo(() => {
{/* 提示文字 */}
<span className="text-xs text-muted-foreground">
火山引擎正在创作 3 张图片,请稍候...
火山引擎正在创作 {count} 张图片,请稍候...
</span>
</div>
</div>

View File

@ -563,7 +563,9 @@ export function createGeminiImage({ apiKey, baseURL }: GeminiImageConfig) {
doGenerate: async (options: any) => {
console.log("[Gemini Image] doGenerate called!"); // 调试日志
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);
try {
@ -573,31 +575,34 @@ export function createGeminiImage({ apiKey, baseURL }: GeminiImageConfig) {
aspectRatio: options.imageParams?.aspectRatio || "auto",
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] 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 [response1, response2] = await Promise.all([
sendGeminiRequest(baseURL, apiKey, options.messages, modelId, imageParams),
sendGeminiRequest(baseURL, apiKey, options.messages, modelId, imageParams),
]);
// 创建请求数组(使用 effectiveModelId)
const requestPromises = Array.from({ length: imageCount }, () =>
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(并行处理)
const [imageUrls1, imageUrls2] = await Promise.all([
extractAndUploadImages(response1),
extractAndUploadImages(response2),
]);
const extractPromises = responses.map(response => extractAndUploadImages(response));
const imageUrlsArrays = await Promise.all(extractPromises);
// 合并两次请求的图片结果
const allImageUrls = [...imageUrls1, ...imageUrls2];
// 合并所有请求的图片结果
const allImageUrls = imageUrlsArrays.flat();
const markdownText = formatImageResponse(allImageUrls, imageParams.imageSize);
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 格式
@ -620,7 +625,9 @@ export function createGeminiImage({ apiKey, baseURL }: GeminiImageConfig) {
doStream: async function* (options: any) {
console.log("[Gemini Image] ===== doStream ENTERED! ====="); // 调试日志
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);
try {
@ -630,31 +637,34 @@ export function createGeminiImage({ apiKey, baseURL }: GeminiImageConfig) {
aspectRatio: options.imageParams?.aspectRatio || "auto",
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 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 [response1, response2] = await Promise.all([
sendGeminiRequest(baseURL, apiKey, options.messages, modelId, imageParams),
sendGeminiRequest(baseURL, apiKey, options.messages, modelId, imageParams),
]);
// 创建请求数组(使用 effectiveModelId)
const requestPromises = Array.from({ length: imageCount }, () =>
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(并行处理)
const [imageUrls1, imageUrls2] = await Promise.all([
extractAndUploadImages(response1),
extractAndUploadImages(response2),
]);
const extractPromises = responses.map(response => extractAndUploadImages(response));
const imageUrlsArrays = await Promise.all(extractPromises);
// 合并两次请求的图片结果
const allImageUrls = [...imageUrls1, ...imageUrls2];
// 合并所有请求的图片结果
const allImageUrls = imageUrlsArrays.flat();
const markdownText = formatImageResponse(allImageUrls, imageParams.imageSize);
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 图片很大)

View File

@ -543,9 +543,11 @@ export function createVolcanoVision({ apiKey, baseURL }: VolcanoVisionConfig) {
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);
console.log("[Volcano Vision] Image params from options:", options.imageParams);
// 支持可配置的图片数量(故障转移时传 1,正常使用时传 3 或不传)
const maxImages = options.maxImages ?? 3;
// 🆕 优先使用用户选择的图片数量,其次是 maxImages 参数,默认 1 张
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);
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,
isDefault,
sortOrder,
fallbackModelId1,
fallbackModelId2,
}: {
providerId: string;
modelId: string;
@ -1121,6 +1123,8 @@ export async function createAiModel({
isEnabled?: boolean;
isDefault?: boolean;
sortOrder?: number;
fallbackModelId1?: string;
fallbackModelId2?: string;
}): Promise<AiModel[]> {
try {
// 如果设置为默认,先取消其他默认
@ -1143,6 +1147,8 @@ export async function createAiModel({
isEnabled: isEnabled ?? true,
isDefault: isDefault ?? false,
sortOrder: sortOrder ?? 0,
fallbackModelId1: fallbackModelId1 || null,
fallbackModelId2: fallbackModelId2 || null,
})
.returning();
} catch (_error) {
@ -1164,6 +1170,8 @@ export async function updateAiModel({
isEnabled,
isDefault,
sortOrder,
fallbackModelId1,
fallbackModelId2,
}: {
id: string;
providerId?: string;
@ -1175,6 +1183,8 @@ export async function updateAiModel({
isEnabled?: boolean;
isDefault?: boolean;
sortOrder?: number;
fallbackModelId1?: string;
fallbackModelId2?: string;
}): Promise<AiModel[]> {
try {
// 如果设置为默认,先取消其他默认
@ -1196,6 +1206,9 @@ export async function updateAiModel({
if (isEnabled !== undefined) updateData.isEnabled = isEnabled;
if (isDefault !== undefined) updateData.isDefault = isDefault;
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
.update(aiModel)

View File

@ -247,6 +247,9 @@ export const aiModel = pgTable("AiModel", {
// 🆕 子模型相关字段(用于火山引擎视频等需要智能选择的模型)
isSubModel: boolean("is_sub_model").notNull().default(false), // 是否为子模型(不在模型列表中显示)
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(),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
});