feat(admin): add ruxa image preset model bootstrap
This commit is contained in:
parent
663ff7b41c
commit
5a04304ea9
@ -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"
|
||||
|
||||
129
app/api/admin/ruxa-image-models/route.ts
Normal file
129
app/api/admin/ruxa-image-models/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
74
lib/ai/ruxa-image-models.ts
Normal file
74
lib/ai/ruxa-image-models.ts
Normal 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()
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user