Your Name afaab632ed
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add per-model 1K/2K/4K resolution toggles & unify toolbar UI style
- Add supports1K/supports2K/supports4K fields to imageGenConfig for
  per-model resolution control in admin settings
- Unify video-params-bar inline mode to match image-params-bar glass
  panel popover style (rounded corners, consistent colors, grid layout)
- Unify image-params-bar count selector and advanced settings to same
  glass panel style
- Update imageGenConfig types across all consuming components

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 11:59:25 +08:00

209 lines
6.9 KiB
TypeScript

import { z } from "zod";
import { requireAdmin } from "@/lib/middleware/admin";
import {
getAllAiModels,
getAiModelById,
getAiModelsByProviderId,
createAiModel,
updateAiModel,
deleteAiModel,
getAiProviderById,
} from "@/lib/db/queries";
import { ChatSDKError } from "@/lib/errors";
import {
getAiModelAgentCodingModelId,
isAiModelAgentAuto,
isAiModelMultimodal,
} from "@/lib/db/ai-model-flags";
// 🆕 图片生成模型特性配置 Schema
const imageGenConfigSchema = z.object({
maxImages: z.number().min(1).max(10).optional(),
supportsSize: z.boolean().optional(),
supports1K: z.boolean().optional(),
supports2K: z.boolean().optional(),
supports4K: z.boolean().optional(),
supportsPromptExtend: z.boolean().optional(),
supportsNegativePrompt: z.boolean().optional(),
supportsWatermark: z.boolean().optional(),
supportsSeed: z.boolean().optional(),
creditCostPerImage: z.number().min(0).max(100000).optional(),
creditCostPerVideo: z.number().min(0).max(100000).optional(),
enableLoadingPlaceholder: z.boolean().optional(),
loadingColor: z.string().optional(),
}).optional();
const createModelSchema = z.object({
providerId: z.string().uuid(),
modelId: z.string().min(1).max(128),
displayName: z.string().min(1).max(128),
avatarUrl: z.string().url().max(1000).optional().or(z.literal("")),
description: z.string().optional(),
supportsVision: z.boolean().optional(),
supportsImageGen: z.boolean().optional(),
isEnabled: z.boolean().optional(),
isHidden: z.boolean().optional(),
isDefault: z.boolean().optional(),
sortOrder: z.number().optional(),
fallbackModelId1: z.string().max(128).optional(),
fallbackModelId2: z.string().max(128).optional(),
isAgentModel: z.boolean().optional(),
agentCodingModelId: z.string().max(128).optional(),
isMultimodal: z.boolean().optional(),
imageGenConfig: imageGenConfigSchema, // 🆕 图片生成特性配置
});
const updateModelSchema = z.object({
id: z.string().uuid(),
providerId: z.string().uuid().optional(),
modelId: z.string().min(1).max(128).optional(),
displayName: z.string().min(1).max(128).optional(),
avatarUrl: z.string().url().max(1000).optional().or(z.literal("")),
description: z.string().optional(),
supportsVision: z.boolean().optional(),
supportsImageGen: z.boolean().optional(),
isEnabled: z.boolean().optional(),
isHidden: z.boolean().optional(),
isDefault: z.boolean().optional(),
sortOrder: z.number().optional(),
fallbackModelId1: z.string().max(128).optional(),
fallbackModelId2: z.string().max(128).optional(),
isAgentModel: z.boolean().optional(),
agentCodingModelId: z.string().max(128).optional(),
isMultimodal: z.boolean().optional(),
imageGenConfig: imageGenConfigSchema, // 🆕 图片生成特性配置
});
function normalizeModel(model: any) {
return {
...model,
isAgentModel: isAiModelAgentAuto(model),
agentCodingModelId: getAiModelAgentCodingModelId(model) || "",
isMultimodal: isAiModelMultimodal(model),
};
}
function sortModelsForDisplay<T extends { sortOrder?: number | null; displayName?: string | null }>(
models: T[]
) {
return [...models].sort((a, b) => {
const leftOrder = Number.isFinite(a.sortOrder as number) ? Number(a.sortOrder) : 0;
const rightOrder = Number.isFinite(b.sortOrder as number) ? Number(b.sortOrder) : 0;
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
const leftName = String(a.displayName || "");
const rightName = String(b.displayName || "");
return leftName.localeCompare(rightName, "zh-Hans-CN");
});
}
export async function GET(request: Request) {
try {
await requireAdmin();
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
const providerId = searchParams.get("providerId");
if (id) {
const model = await getAiModelById({ id });
if (!model) {
return new ChatSDKError("bad_request:api", "Model not found").toResponse();
}
return Response.json({ model: normalizeModel(model) });
}
if (providerId) {
const models = await getAiModelsByProviderId({ providerId });
return Response.json({ models: sortModelsForDisplay(models.map(normalizeModel)) });
}
const models = await getAllAiModels();
return Response.json({ models: sortModelsForDisplay(models.map(normalizeModel)) });
} catch (error) {
if (error instanceof ChatSDKError) {
return error.toResponse();
}
return new ChatSDKError("offline:admin").toResponse();
}
}
export async function POST(request: Request) {
try {
await requireAdmin();
const body = await request.json();
const data = createModelSchema.parse(body);
// 验证 provider 是否存在
const provider = await getAiProviderById({ id: data.providerId });
if (!provider) {
return new ChatSDKError("bad_request:api", "Provider not found").toResponse();
}
const model = await createAiModel(data);
return Response.json({ model: normalizeModel(model[0]) });
} catch (error) {
if (error instanceof ChatSDKError) {
return error.toResponse();
}
if (error instanceof z.ZodError) {
return new ChatSDKError("bad_request:api", error.message).toResponse();
}
return new ChatSDKError("offline:admin").toResponse();
}
}
export async function PUT(request: Request) {
try {
await requireAdmin();
const body = await request.json();
console.log("[Admin API] Update model request body:", JSON.stringify(body)); // Debug log
const data = updateModelSchema.parse(body);
console.log("[Admin API] Validation passed, updating model..."); // Debug log
const model = await updateAiModel(data);
if (model.length === 0) {
console.error("[Admin API] Model not found:", data.id); // Debug log
return new ChatSDKError("bad_request:api", "Model not found").toResponse();
}
console.log("[Admin API] Model updated successfully:", model[0].id); // Debug log
return Response.json({ model: normalizeModel(model[0]) });
} catch (error) {
console.error("[Admin API] Update model error:", error); // Debug log
if (error instanceof ChatSDKError) {
return error.toResponse();
}
if (error instanceof z.ZodError) {
console.error("[Admin API] Validation error details:", error.errors); // Debug log
return new ChatSDKError("bad_request:api", error.message).toResponse();
}
return new ChatSDKError("offline:admin").toResponse();
}
}
export async function DELETE(request: Request) {
try {
await requireAdmin();
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
if (!id) {
return new ChatSDKError("bad_request:api", "Model ID is required").toResponse();
}
await deleteAiModel({ id });
return Response.json({ success: true });
} catch (error) {
if (error instanceof ChatSDKError) {
return error.toResponse();
}
return new ChatSDKError("offline:admin").toResponse();
}
}