diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index b5a1638a3..b55835116 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -167,6 +167,11 @@ import { isImageGenerationModelDescriptor, isVideoModelDescriptor, } from "@/lib/ai/model-classification"; +import { + buildVideoDurationCostMap, + getConfiguredVideoDurationOptions, + normalizeVideoDurationOptions, +} from "@/lib/ai/video-model-config"; import type { AgentWorkflowUiConfig } from "@/types/agent"; const fetcher = (url: string) => fetch(url).then((res) => res.json()); @@ -360,10 +365,59 @@ type ImageGenConfig = { supportsSeed?: boolean; // 是否支持随机种子 creditCostPerImage?: number; // 每张图片积分消耗 creditCostPerVideo?: number; // 每条视频积分消耗 + videoDurationOptions?: number[]; // 视频秒数选项 + creditCostPerVideoByDuration?: Record; // 各秒数积分 enableLoadingPlaceholder?: boolean; // 是否启用加载占位图 loadingColor?: string; // 占位图主题色,如 "purple", "blue", "green" }; +function createDefaultImageGenConfig(): ImageGenConfig { + return { + maxImages: 2, + supportsSize: false, + supports1K: true, + supports2K: true, + supports4K: true, + supportsPromptExtend: false, + supportsNegativePrompt: false, + supportsWatermark: false, + supportsSeed: false, + creditCostPerImage: 0, + creditCostPerVideo: 0, + videoDurationOptions: [], + creditCostPerVideoByDuration: {}, + enableLoadingPlaceholder: true, + loadingColor: "purple", + }; +} + +function formatVideoDurationOptionsInput(options?: number[]) { + return getConfiguredVideoDurationOptions({ videoDurationOptions: options }).join(", "); +} + +function parseVideoDurationOptionsInput(value: string) { + return normalizeVideoDurationOptions( + String(value || "") + .split(/[,\n,、\s]+/) + .map((item) => item.trim()) + .filter(Boolean) + ); +} + +function syncVideoDurationPricing(config: ImageGenConfig | undefined, durationOptions: number[]) { + const nextConfig = config || createDefaultImageGenConfig(); + return { + ...nextConfig, + videoDurationOptions: durationOptions, + creditCostPerVideoByDuration: + buildVideoDurationCostMap({ + durationOptions, + costMap: nextConfig.creditCostPerVideoByDuration, + fallbackCost: nextConfig.creditCostPerVideo, + }) || {}, + }; +} + type AiModel = { id: string; providerId: string; @@ -2316,21 +2370,7 @@ export function AdminSettingsClient() { fallbackModelId2: "", // 备用模型 ID 2 excludeFromFailover: false, // 🆕 图片生成模型特性配置 - imageGenConfig: { - maxImages: 2, - supportsSize: false, - supports1K: true, - supports2K: true, - supports4K: true, - supportsPromptExtend: false, - supportsNegativePrompt: false, - supportsWatermark: false, - supportsSeed: false, - creditCostPerImage: 0, - creditCostPerVideo: 0, - enableLoadingPlaceholder: true, - loadingColor: "purple", - } as ImageGenConfig, + imageGenConfig: createDefaultImageGenConfig(), }); const [showModelDialog, setShowModelDialog] = useState(false); const [editingModel, setEditingModel] = useState(null); @@ -2394,6 +2434,9 @@ export function AdminSettingsClient() { modelId: modelForm.modelId, supportsImageGen: modelForm.supportsImageGen, }); + const configuredVideoDurationOptions = getConfiguredVideoDurationOptions( + modelForm.imageGenConfig + ); const modelKindHint = selectedModelProviderType === "runway" && !modelForm.modelId.trim() ? "Runway 同时支持图片与视频模型,填写模型 ID 后系统会自动识别标签。" @@ -4830,18 +4873,7 @@ export function AdminSettingsClient() { fallbackModelId1: "", fallbackModelId2: "", excludeFromFailover: false, - imageGenConfig: { - maxImages: 2, - supportsSize: false, - supportsPromptExtend: false, - supportsNegativePrompt: false, - supportsWatermark: false, - supportsSeed: false, - creditCostPerImage: 0, - creditCostPerVideo: 0, - enableLoadingPlaceholder: true, - loadingColor: "purple", - }, + imageGenConfig: createDefaultImageGenConfig(), }); setEditingModel(null); }; @@ -11596,6 +11628,104 @@ export function AdminSettingsClient() {

+ {isVideoProviderForModelConfig && ( +
+
+ + { + const durationOptions = parseVideoDurationOptionsInput( + e.target.value + ); + setModelForm({ + ...modelForm, + imageGenConfig: syncVideoDurationPricing( + modelForm.imageGenConfig, + durationOptions + ), + }); + }} + placeholder="例如:4, 6, 8 或 5, 10, 15" + /> +

+ 前端会按这里显示可选视频秒数。留空时继续使用服务商默认能力。 +

+
+ + {configuredVideoDurationOptions.length > 0 ? ( +
+
+ 各秒数积分消耗 +
+
+ {configuredVideoDurationOptions.map((duration) => { + const durationKey = String(duration); + return ( +
+ + { + const nextValue = Math.max( + 0, + Number.parseInt( + e.target.value, + 10 + ) || 0 + ); + setModelForm({ + ...modelForm, + imageGenConfig: { + ...syncVideoDurationPricing( + modelForm.imageGenConfig, + configuredVideoDurationOptions + ), + creditCostPerVideoByDuration: { + ...(syncVideoDurationPricing( + modelForm.imageGenConfig, + configuredVideoDurationOptions + ) + .creditCostPerVideoByDuration || + {}), + [durationKey]: nextValue, + }, + }, + }); + }} + /> +
+ ); + })} +
+

+ 配置后会优先按用户选择的秒数扣积分;未命中的秒数才回退到“每条视频积分消耗”。 +

+
+ ) : ( +

+ 当前还没有配置秒数档位,将按“每条视频积分消耗”统一扣费。 +

+ )} +
+ )} +