feat(admin): add ruxa image preset model bootstrap
This commit is contained in:
parent
663ff7b41c
commit
5a04304ea9
@ -153,6 +153,7 @@ import {
|
|||||||
normalizeDetailAgentLocalMoonshotConfig,
|
normalizeDetailAgentLocalMoonshotConfig,
|
||||||
} from "@/lib/ai/detail-agent-model-config";
|
} from "@/lib/ai/detail-agent-model-config";
|
||||||
import { FAL_IMAGE_MODEL_PRESETS } from "@/lib/ai/fal-image-models";
|
import { FAL_IMAGE_MODEL_PRESETS } from "@/lib/ai/fal-image-models";
|
||||||
|
import { RUXA_IMAGE_MODEL_PRESETS } from "@/lib/ai/ruxa-image-models";
|
||||||
import {
|
import {
|
||||||
formatImageModelCreditDiscountLabel,
|
formatImageModelCreditDiscountLabel,
|
||||||
getGenerationCreditCostFromImageGenConfig,
|
getGenerationCreditCostFromImageGenConfig,
|
||||||
@ -6095,6 +6096,7 @@ export function AdminSettingsClient() {
|
|||||||
const isVolcanoVideo = providerForm.type === "volcano-video";
|
const isVolcanoVideo = providerForm.type === "volcano-video";
|
||||||
const isKreaProvider = providerForm.type === "krea";
|
const isKreaProvider = providerForm.type === "krea";
|
||||||
const isFalImageProvider = providerForm.type === "fal-image";
|
const isFalImageProvider = providerForm.type === "fal-image";
|
||||||
|
const isRuxaImageProvider = providerForm.type === "ruxa-image";
|
||||||
const isBltcyVideoProvider = providerForm.type === "bltcy-video";
|
const isBltcyVideoProvider = providerForm.type === "bltcy-video";
|
||||||
const isLemonProvider =
|
const isLemonProvider =
|
||||||
providerForm.type === "openai" &&
|
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) {
|
if (isNewProvider && isBltcyVideoProvider && providerId) {
|
||||||
try {
|
try {
|
||||||
await handleQuickAddBltcyVideoModels(providerId, {
|
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 = () => {
|
const handleUseFalImageProviderPreset = () => {
|
||||||
setProviderForm({
|
setProviderForm({
|
||||||
id: "",
|
id: "",
|
||||||
@ -15113,6 +15170,17 @@ export function AdminSettingsClient() {
|
|||||||
快速添加 Krea 预设模型
|
快速添加 Krea 预设模型
|
||||||
</Button>
|
</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" && (
|
{provider.type === "fal-image" && (
|
||||||
<Button
|
<Button
|
||||||
className="mt-2"
|
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