feat(admin): add ruxa image preset model bootstrap

This commit is contained in:
Your Name 2026-06-08 19:04:13 +08:00
parent 663ff7b41c
commit 5a04304ea9
3 changed files with 271 additions and 0 deletions

View File

@ -153,6 +153,7 @@ import {
normalizeDetailAgentLocalMoonshotConfig,
} from "@/lib/ai/detail-agent-model-config";
import { FAL_IMAGE_MODEL_PRESETS } from "@/lib/ai/fal-image-models";
import { RUXA_IMAGE_MODEL_PRESETS } from "@/lib/ai/ruxa-image-models";
import {
formatImageModelCreditDiscountLabel,
getGenerationCreditCostFromImageGenConfig,
@ -6095,6 +6096,7 @@ export function AdminSettingsClient() {
const isVolcanoVideo = providerForm.type === "volcano-video";
const isKreaProvider = providerForm.type === "krea";
const isFalImageProvider = providerForm.type === "fal-image";
const isRuxaImageProvider = providerForm.type === "ruxa-image";
const isBltcyVideoProvider = providerForm.type === "bltcy-video";
const isLemonProvider =
providerForm.type === "openai" &&
@ -6215,6 +6217,17 @@ export function AdminSettingsClient() {
}
}
if (isNewProvider && isRuxaImageProvider && providerId) {
try {
await handleQuickAddRuxaImageModels(providerId, { skipConfirm: true });
} catch (modelError) {
console.error("[Auto Add RUXA Image Models] Error:", modelError);
toast.warning(
'RUXA 生图预设模型添加失败,请手动点击"快速添加 RUXA 图片预设"按钮'
);
}
}
if (isNewProvider && isBltcyVideoProvider && providerId) {
try {
await handleQuickAddBltcyVideoModels(providerId, {
@ -7099,6 +7112,50 @@ export function AdminSettingsClient() {
}
};
const handleQuickAddRuxaImageModels = async (
providerId: string,
options?: { skipConfirm?: boolean }
) => {
if (
!options?.skipConfirm &&
!confirm(
`将为此服务商添加 ${RUXA_IMAGE_MODEL_PRESETS.length} 个 RUXA 生图预设模型(含 Nano Banana 2「专线」、Nano Banana Pro「专线」)。\n\n继续吗?`
)
) {
return;
}
try {
toast.info("正在添加 RUXA 生图预设模型...");
const response = await fetch("/api/admin/ruxa-image-models", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ providerId }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "添加失败");
}
const { addedCount, errors } = data;
if (addedCount > 0) {
toast.success(`成功添加 ${addedCount} 个 RUXA 生图预设模型`);
mutateModels();
}
if (errors && errors.length > 0) {
toast.warning(`${errors.length} 个模型添加失败(可能已存在)`);
}
} catch (error: any) {
console.error("[Quick Add RUXA Image Models] Error:", error);
toast.error(error.message || "添加 RUXA 生图预设模型失败");
}
};
const handleUseFalImageProviderPreset = () => {
setProviderForm({
id: "",
@ -15113,6 +15170,17 @@ export function AdminSettingsClient() {
快速添加 Krea 预设模型
</Button>
)}
{provider.type === "ruxa-image" && (
<Button
className="mt-2"
onClick={() => handleQuickAddRuxaImageModels(provider.id)}
size="sm"
variant="secondary"
>
<ImageIcon className="mr-1 h-4 w-4" />
快速添加 RUXA 图片预设
</Button>
)}
{provider.type === "fal-image" && (
<Button
className="mt-2"

View File

@ -0,0 +1,129 @@
import { and, eq } from "drizzle-orm";
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { getAiProviderById } from "@/lib/db/queries";
import { type AiModel, aiModel } from "@/lib/db/schema";
import { RUXA_IMAGE_MODEL_PRESETS } from "@/lib/ai/ruxa-image-models";
import { requireAdmin } from "@/lib/middleware/admin";
export async function GET() {
try {
await requireAdmin();
return NextResponse.json({
presets: RUXA_IMAGE_MODEL_PRESETS,
counts: {
total: RUXA_IMAGE_MODEL_PRESETS.length,
},
});
} catch (error: any) {
console.error("[RUXA Image Models API] GET error:", error);
return NextResponse.json(
{ error: error.message || "Failed to fetch RUXA image model presets" },
{ status: 500 }
);
}
}
export async function POST(request: Request) {
try {
await requireAdmin();
const body = await request.json();
const providerId = String(body?.providerId || "").trim();
const modelIds = Array.isArray(body?.modelIds)
? body.modelIds
.map((item: unknown) => String(item || "").trim())
.filter(Boolean)
: null;
if (!providerId) {
return NextResponse.json(
{ error: "Provider ID is required" },
{ status: 400 }
);
}
const provider = await getAiProviderById({ id: providerId });
if (!provider || provider.type !== "ruxa-image") {
return NextResponse.json(
{ error: "Provider must be a RUXA image provider" },
{ status: 400 }
);
}
const presetsToAdd = modelIds
? RUXA_IMAGE_MODEL_PRESETS.filter((preset) =>
modelIds.includes(preset.modelId)
)
: RUXA_IMAGE_MODEL_PRESETS;
const addedModels: AiModel[] = [];
const errors: { modelId: string; error: string }[] = [];
for (const preset of presetsToAdd) {
try {
const existing = await db.query.aiModel.findFirst({
where: and(
eq(aiModel.providerId, providerId),
eq(aiModel.modelId, preset.modelId)
),
});
if (existing) {
errors.push({ modelId: preset.modelId, error: "Model already exists" });
continue;
}
const [newModel] = await db
.insert(aiModel)
.values({
providerId,
modelId: preset.modelId,
displayName: preset.displayName,
description: preset.description,
supportsVision: preset.supportsVision,
supportsImageGen: preset.supportsImageGen,
isEnabled: true,
isHidden: false,
isDefault: false,
sortOrder: preset.sortOrder,
capabilities: preset.capabilities || null,
defaultParams: preset.defaultParams || null,
imageGenConfig: preset.imageGenConfig || null,
isSubModel: false,
parentModelId: null,
})
.returning();
addedModels.push(newModel);
} catch (error: any) {
console.error(
`[RUXA Image Models API] Failed to add ${preset.modelId}:`,
error
);
errors.push({
modelId: preset.modelId,
error: error.message || "Insert failed",
});
}
}
return NextResponse.json({
success: true,
addedCount: addedModels.length,
addedModels,
errors: errors.length > 0 ? errors : undefined,
counts: {
requested: presetsToAdd.length,
total: RUXA_IMAGE_MODEL_PRESETS.length,
},
});
} catch (error: any) {
console.error("[RUXA Image Models API] POST error:", error);
return NextResponse.json(
{ error: error.message || "Failed to add RUXA image models" },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,74 @@
export interface RuxaImageModelPreset {
modelId: string;
displayName: string;
description: string;
supportsVision: boolean;
supportsImageGen: boolean;
capabilities?: Record<string, any>;
defaultParams?: Record<string, any>;
imageGenConfig?: Record<string, any>;
sortOrder: number;
}
export const RUXA_NANO_BANANA_2_MODEL_ID = "google/nano-banana-2";
export const RUXA_NANO_BANANA_PRO_MODEL_ID = "google/nano-banana-pro";
const RUXA_IMAGE_CONFIG = {
maxImages: 1,
supportsSize: true,
supports1K: true,
supports2K: true,
supports4K: true,
supportsPromptExtend: false,
supportsNegativePrompt: false,
supportsWatermark: false,
supportsSeed: false,
creditCostPerImage: 0,
enableLoadingPlaceholder: true,
loadingColor: "blue",
};
export const RUXA_IMAGE_MODEL_PRESETS: RuxaImageModelPreset[] = [
{
modelId: RUXA_NANO_BANANA_2_MODEL_ID,
displayName: "Nano Banana 2「专线」",
description:
"RUXA 专线 Nano Banana 2。支持文生图和图像编辑输入。",
supportsVision: true,
supportsImageGen: true,
defaultParams: {
provider: "ruxa",
endpoint: RUXA_NANO_BANANA_2_MODEL_ID,
imageSize: "2K",
aspectRatio: "1:1",
},
imageGenConfig: {
...RUXA_IMAGE_CONFIG,
},
sortOrder: 10,
},
{
modelId: RUXA_NANO_BANANA_PRO_MODEL_ID,
displayName: "Nano Banana Pro「专线」",
description: "RUXA 专线 Nano Banana Pro。支持文生图和图像编辑输入。",
supportsVision: true,
supportsImageGen: true,
defaultParams: {
provider: "ruxa",
endpoint: RUXA_NANO_BANANA_PRO_MODEL_ID,
imageSize: "2K",
aspectRatio: "1:1",
},
imageGenConfig: {
...RUXA_IMAGE_CONFIG,
},
sortOrder: 20,
},
];
export function getRuxaImageModelPresetById(modelId: string) {
return RUXA_IMAGE_MODEL_PRESETS.find(
(preset) => preset.modelId === String(modelId || "").trim()
);
}