diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 3d2506a8f..5bf6881dd 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -158,6 +158,14 @@ import { defaultAgentWorkflowUiConfig, normalizeAgentWorkflowUiConfig, } from "@/lib/agents/workflow-ui-config"; +import { + BLTCY_BFL_BASE_URL, + BLTCY_BFL_MODEL_PRESETS, + BLTCY_BFL_PROVIDER_PRESET, + BLTCY_IMAGE_MODEL_PRESETS, + BLTCY_IMAGE_OPENAI_BASE_URL, + BLTCY_IMAGE_PROVIDER_PRESET, +} from "@/lib/ai/bltcy-image-models"; import { CANVAS_SKILL_ICON_OPTIONS, getCanvasSkillIconComponent, @@ -170,6 +178,7 @@ import { import { buildVideoDurationCostMap, getConfiguredVideoDurationOptions, + getVideoCreditCostFromConfig, normalizeVideoDurationOptions, recommendedVideoDurationOptions, recommendedVideoDurationReferenceCosts, @@ -182,6 +191,7 @@ const DEFAULT_BFL_REALTIME_MODEL = "flux-2-klein-4b"; const DEFAULT_BFL_BASE_URL = "https://api.bfl.ai"; const HOME_CHAT_ANIMATION_MAX_BYTES = 1024 * 1024; const HOME_CHAT_ANIMATION_CONFIG_KEY = "home_chat_animation_config"; +const VIDEO_PRICING_PREVIEW_RESOLUTIONS = ["480p", "720p", "1080p"] as const; function createEmptyCanvasSkillTag(): CanvasSkillTag { return { @@ -438,6 +448,21 @@ function applyRecommendedVideoPricing(config: ImageGenConfig | undefined) { }; } +function applyRecommendedEnhanceProPricing( + pricing: PointsSystemConfig["enhanceProPricing"] +): PointsSystemConfig["enhanceProPricing"] { + const recommended = defaultPointsSystemConfig.enhanceProPricing; + return { + ...pricing, + chargeMode: recommended.chargeMode, + fixedCost: recommended.fixedCost, + chargeOnlyWhenUpscaling: recommended.chargeOnlyWhenUpscaling, + batchSizeMultiplierEnabled: recommended.batchSizeMultiplierEnabled, + scaleFactorCosts: { ...recommended.scaleFactorCosts }, + modelMultipliers: { ...recommended.modelMultipliers }, + }; +} + type AiModel = { id: string; providerId: string; @@ -785,6 +810,21 @@ const PROVIDER_DEFAULT_BASE_URL: Record = { }; const LEMON_OPENAI_BASE_URL = "https://new.lemonapi.site/v1"; +const HTTP_URL_REGEX = /^https?:\/\//i; +const TRAILING_SLASHES_REGEX = /\/+$/; +const TRAILING_V1_REGEX = /\/v1$/i; +const BLTCY_IMAGE_NORMALIZED_BASE_URL = BLTCY_IMAGE_OPENAI_BASE_URL.replace( + TRAILING_SLASHES_REGEX, + "" +); +const BLTCY_IMAGE_BASE_URL_WITHOUT_V1 = BLTCY_IMAGE_OPENAI_BASE_URL.replace( + TRAILING_V1_REGEX, + "" +); +const BLTCY_BFL_NORMALIZED_BASE_URL = BLTCY_BFL_BASE_URL.replace( + TRAILING_SLASHES_REGEX, + "" +); const LEMON_PROVIDER_PRESET = { name: "LemonAPI Gemini", type: "openai" as const, @@ -808,23 +848,42 @@ function getProviderDefaultBaseUrl(type?: string) { } function isHttpUrlLike(value: string) { - return /^https?:\/\//i.test(value.trim()); + return HTTP_URL_REGEX.test(value.trim()); } function isLemonApiBaseUrl(value?: string | null) { + const normalized = String(value || "") + .trim() + .replace(TRAILING_SLASHES_REGEX, ""); + return normalized === LEMON_OPENAI_BASE_URL; +} + +function isBltcyImageBaseUrl(value?: string | null) { + const normalized = String(value || "") + .trim() + .replace(TRAILING_SLASHES_REGEX, ""); return ( - String(value || "") - .trim() - .replace(/\/+$/, "") === LEMON_OPENAI_BASE_URL + normalized === BLTCY_IMAGE_NORMALIZED_BASE_URL || + normalized === BLTCY_IMAGE_BASE_URL_WITHOUT_V1 + ); +} + +function isBltcyBflBaseUrl(value?: string | null) { + const normalized = String(value || "") + .trim() + .replace(TRAILING_SLASHES_REGEX, ""); + return ( + normalized === BLTCY_BFL_NORMALIZED_BASE_URL || + normalized === `${BLTCY_BFL_NORMALIZED_BASE_URL}/bfl` || + normalized === `${BLTCY_BFL_NORMALIZED_BASE_URL}/bfl/v1` ); } function isMoonshotOfficialBaseUrl(value?: string | null) { - return ( - String(value || "") - .trim() - .replace(/\/+$/, "") === DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL - ); + const normalized = String(value || "") + .trim() + .replace(TRAILING_SLASHES_REGEX, ""); + return normalized === DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL; } export function AdminSettingsClient() { @@ -4276,6 +4335,16 @@ export function AdminSettingsClient() { isLemonApiBaseUrl( providerForm.baseUrl || getProviderDefaultBaseUrl(providerForm.type) ); + const isBltcyImageProvider = + providerForm.type === "openai-image" && + isBltcyImageBaseUrl( + providerForm.baseUrl || getProviderDefaultBaseUrl(providerForm.type) + ); + const isBltcyBflProvider = + providerForm.type === "bfl-image" && + isBltcyBflBaseUrl( + providerForm.baseUrl || getProviderDefaultBaseUrl(providerForm.type) + ); const defaultBaseUrl = getProviderDefaultBaseUrl(providerForm.type); const normalizedBaseUrl = providerForm.baseUrl.trim(); @@ -4375,6 +4444,28 @@ export function AdminSettingsClient() { ); } } + + if (isNewProvider && isBltcyImageProvider && providerId) { + try { + await handleQuickAddBltcyImageModels(providerId, { skipConfirm: true }); + } catch (modelError) { + console.error("[Auto Add BLTCY Image Models] Error:", modelError); + toast.warning( + '柏拉图 Flux 预设模型添加失败,请手动点击"快速添加图片编辑预设"按钮' + ); + } + } + + if (isNewProvider && isBltcyBflProvider && providerId) { + try { + await handleQuickAddBltcyBflModels(providerId, { skipConfirm: true }); + } catch (modelError) { + console.error("[Auto Add BLTCY BFL Models] Error:", modelError); + toast.warning( + '柏拉图 BFL 预设模型添加失败,请手动点击"快速添加 BFL 异步预设"按钮' + ); + } + } } catch (error) { toast.error("保存提供商失败"); } @@ -5028,6 +5119,120 @@ export function AdminSettingsClient() { } }; + const handleUseBltcyImageProviderPreset = () => { + setProviderForm({ + id: "", + name: BLTCY_IMAGE_PROVIDER_PRESET.name, + type: BLTCY_IMAGE_PROVIDER_PRESET.type, + baseUrl: BLTCY_IMAGE_PROVIDER_PRESET.baseUrl, + apiKey: "", + isEnabled: true, + }); + setEditingProvider(null); + setShowProviderDialog(true); + }; + + const handleUseBltcyBflProviderPreset = () => { + setProviderForm({ + id: "", + name: BLTCY_BFL_PROVIDER_PRESET.name, + type: BLTCY_BFL_PROVIDER_PRESET.type, + baseUrl: BLTCY_BFL_PROVIDER_PRESET.baseUrl, + apiKey: "", + isEnabled: true, + }); + setEditingProvider(null); + setShowProviderDialog(true); + }; + + const handleQuickAddBltcyImageModels = async ( + providerId: string, + options?: { skipConfirm?: boolean } + ) => { + if ( + !options?.skipConfirm && + !confirm( + `将为此服务商添加 ${BLTCY_IMAGE_MODEL_PRESETS.length} 个柏拉图 Flux OpenAI Images 兼容图片编辑模型预设。\n\n继续吗?` + ) + ) { + return; + } + + try { + toast.info("正在添加柏拉图 Flux 图片编辑预设模型..."); + + const response = await fetch("/api/admin/bltcy-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} 个柏拉图 Flux 预设模型`); + mutateModels(); + } + + if (errors && errors.length > 0) { + toast.warning(`${errors.length} 个模型添加失败(可能已存在)`); + } + } catch (error: any) { + console.error("[Quick Add BLTCY Image Models] Error:", error); + toast.error(error.message || "添加柏拉图 Flux 预设模型失败"); + } + }; + + const handleQuickAddBltcyBflModels = async ( + providerId: string, + options?: { skipConfirm?: boolean } + ) => { + if ( + !options?.skipConfirm && + !confirm( + `将为此服务商添加 ${BLTCY_BFL_MODEL_PRESETS.length} 个柏拉图 BFL 异步图片编辑模型预设。\n\n继续吗?` + ) + ) { + return; + } + + try { + toast.info("正在添加柏拉图 BFL 异步图片编辑预设模型..."); + + const response = await fetch("/api/admin/bltcy-bfl-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} 个柏拉图 BFL 预设模型`); + mutateModels(); + } + + if (errors && errors.length > 0) { + toast.warning(`${errors.length} 个模型添加失败(可能已存在)`); + } + } catch (error: any) { + console.error("[Quick Add BLTCY BFL Models] Error:", error); + toast.error(error.message || "添加柏拉图 BFL 预设模型失败"); + } + }; + const handleUseLemonProviderPreset = () => { setProviderForm({ id: "", @@ -8329,21 +8534,42 @@ export function AdminSettingsClient() { 默认按基础 20 积分计算;启用放大后按放大倍数折算,例如 1.25x 为 25 积分、2x 为 40 积分。 - - setSiteSettings({ - ...siteSettings, - pointsSystemConfig: { - ...pointsSystemConfig, - enhanceProPricing: { - ...pointsSystemConfig.enhanceProPricing, - enabled: checked, +
+ + + setSiteSettings({ + ...siteSettings, + pointsSystemConfig: { + ...pointsSystemConfig, + enhanceProPricing: { + ...pointsSystemConfig.enhanceProPricing, + enabled: checked, + }, + }, + }) + } + /> +
@@ -11253,6 +11479,18 @@ export function AdminSettingsClient() { 配置 AI 服务提供商
+ +
)} + {provider.type === "openai-image" && + isBltcyImageBaseUrl(provider.baseUrl) && ( + + )} + {provider.type === "bfl-image" && + isBltcyBflBaseUrl(provider.baseUrl) && ( + + )} {provider.type === "openai" && isLemonApiBaseUrl(provider.baseUrl) && (