From 2a322feea997614ef4779da4e69365ac718fb8fc Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 4 Apr 2026 23:23:03 +0800 Subject: [PATCH] fix: use per-card local ai setting toggles --- .../settings/admin-settings-client.tsx | 6138 ++++++++++++----- 1 file changed, 4354 insertions(+), 1784 deletions(-) diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 8b6e56364..4edf47d4f 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -1,7 +1,13 @@ "use client"; import Link from "next/link"; -import { useState, useEffect, useMemo, type ChangeEvent, type DragEvent } from "react"; +import { + useState, + useEffect, + useMemo, + type ChangeEvent, + type DragEvent, +} from "react"; import useSWR from "swr"; import { toast } from "sonner"; import { cn, formatBeijingTime } from "@/lib/utils"; @@ -195,10 +201,36 @@ function createEmptyEnhanceProPreset(): EnhanceProPreset { }; } +function LocalAiSectionToggleButton({ + expanded, + onClick, +}: { + expanded: boolean; + onClick: () => void; +}) { + return ( + + ); +} + function reorderEnhanceProPresets( presets: EnhanceProPreset[], fromIndex: number, - toIndex: number, + toIndex: number ) { if ( fromIndex === toIndex || @@ -272,7 +304,30 @@ type ToolConfig = { type AiProvider = { id: string; name: string; - type: "alibaba" | "openai" | "custom" | "local" | "gemini" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "bfl-image" | "ruxa-image" | "fal-image" | "openai-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "worldlabs-3d" | "runway" | "volcano-multimodal"; + type: + | "alibaba" + | "openai" + | "custom" + | "local" + | "gemini" + | "huanke-gemini" + | "qianwen-video" + | "doubao-video" + | "ruxa-video" + | "volcano-vision" + | "volcano-video" + | "alibaba-image" + | "bfl-image" + | "ruxa-image" + | "fal-image" + | "openai-image" + | "midjourney" + | "fal-video" + | "zhipu" + | "tripo-3d" + | "worldlabs-3d" + | "runway" + | "volcano-multimodal"; baseUrl: string; apiKey: string; isEnabled: boolean; @@ -281,19 +336,19 @@ type AiProvider = { // 🆕 图片生成模型特性配置 type ImageGenConfig = { - maxImages?: number; // 最大输出图片数,如阿里 1-6,Gemini 1-2 - supportsSize?: boolean; // 是否支持自定义分辨率 - supports1K?: boolean; // 是否支持 1K 分辨率 - supports2K?: boolean; // 是否支持 2K 分辨率 - supports4K?: boolean; // 是否支持 4K 分辨率 + maxImages?: number; // 最大输出图片数,如阿里 1-6,Gemini 1-2 + supportsSize?: boolean; // 是否支持自定义分辨率 + supports1K?: boolean; // 是否支持 1K 分辨率 + supports2K?: boolean; // 是否支持 2K 分辨率 + supports4K?: boolean; // 是否支持 4K 分辨率 supportsPromptExtend?: boolean; // 是否支持提示词智能优化 supportsNegativePrompt?: boolean; // 是否支持反向提示词 - supportsWatermark?: boolean; // 是否支持水印开关 - supportsSeed?: boolean; // 是否支持随机种子 - creditCostPerImage?: number; // 每张图片积分消耗 - creditCostPerVideo?: number; // 每条视频积分消耗 + supportsWatermark?: boolean; // 是否支持水印开关 + supportsSeed?: boolean; // 是否支持随机种子 + creditCostPerImage?: number; // 每张图片积分消耗 + creditCostPerVideo?: number; // 每条视频积分消耗 enableLoadingPlaceholder?: boolean; // 是否启用加载占位图 - loadingColor?: string; // 占位图主题色,如 "purple", "blue", "green" + loadingColor?: string; // 占位图主题色,如 "purple", "blue", "green" }; type AiModel = { @@ -628,7 +683,8 @@ const PROVIDER_DEFAULT_BASE_URL: Record = { "volcano-multimodal": "https://ark.cn-beijing.volces.com/api/v3", openai: "https://api.openai.com/v1", "huanke-gemini": "https://api.huandutech.com/v1", - "qianwen-video": "https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation", + "qianwen-video": + "https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation", "doubao-video": "https://ark.cn-beijing.volces.com/api/v3", "ruxa-video": "https://api.ruxa.ai", "volcano-vision": "https://ark.cn-beijing.volces.com", @@ -663,13 +719,18 @@ function isHttpUrlLike(value: string) { } function isLemonApiBaseUrl(value?: string | null) { - return String(value || "").trim().replace(/\/+$/, "") === LEMON_OPENAI_BASE_URL; + return ( + String(value || "") + .trim() + .replace(/\/+$/, "") === LEMON_OPENAI_BASE_URL + ); } function isMoonshotOfficialBaseUrl(value?: string | null) { return ( - String(value || "").trim().replace(/\/+$/, "") === - DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL + String(value || "") + .trim() + .replace(/\/+$/, "") === DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL ); } @@ -680,12 +741,18 @@ export function AdminSettingsClient() { const [userSearch, setUserSearch] = useState(""); const [userPage, setUserPage] = useState(1); const [userLimit] = useState(10); - const [giftPointsDraftByUser, setGiftPointsDraftByUser] = useState>({}); - const [deductPointsDraftByUser, setDeductPointsDraftByUser] = useState>({}); - + const [giftPointsDraftByUser, setGiftPointsDraftByUser] = useState< + Record + >({}); + const [deductPointsDraftByUser, setDeductPointsDraftByUser] = useState< + Record + >({}); + // 批量设置出图限额状态 const [showBatchLimitDialog, setShowBatchLimitDialog] = useState(false); - const [batchLimitType, setBatchLimitType] = useState<"all" | "department" | "no_department">("all"); + const [batchLimitType, setBatchLimitType] = useState< + "all" | "department" | "no_department" + >("all"); const [batchLimitValue, setBatchLimitValue] = useState(""); const [batchLimitDepartment, setBatchLimitDepartment] = useState(""); @@ -694,7 +761,8 @@ export function AdminSettingsClient() { const [imagePage, setImagePage] = useState(1); const [imageStartDate, setImageStartDate] = useState(""); const [imageEndDate, setImageEndDate] = useState(""); - const [selectedImage, setSelectedImage] = useState(null); + const [selectedImage, setSelectedImage] = + useState(null); // 🆕 数据分析状态 const [selectedMonth, setSelectedMonth] = useState(() => { @@ -702,16 +770,21 @@ export function AdminSettingsClient() { return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`; }); const [selectedDate, setSelectedDate] = useState(null); // 选中的日期,用于联动高峰时段 - const [hoveredBar, setHoveredBar] = useState<{ type: string; index: number; value: number; label: string } | null>(null); + const [hoveredBar, setHoveredBar] = useState<{ + type: string; + index: number; + value: number; + label: string; + } | null>(null); // API 数据获取 - const { data: usersData, mutate: mutateUsers } = useSWR<{ - users: User[]; - total: number; - page: number; + const { data: usersData, mutate: mutateUsers } = useSWR<{ + users: User[]; + total: number; + page: number; totalPages: number; }>( - `/api/admin/users?page=${userPage}&limit=${userLimit}&search=${encodeURIComponent(userSearch)}`, + `/api/admin/users?page=${userPage}&limit=${userLimit}&search=${encodeURIComponent(userSearch)}`, fetcher ); @@ -720,37 +793,50 @@ export function AdminSettingsClient() { "/api/admin/users/batch-limit", fetcher ); - const { data: providersData, mutate: mutateProviders } = useSWR<{ providers: AiProvider[] }>("/api/admin/ai-providers", fetcher); - const { data: modelsData, mutate: mutateModels } = useSWR<{ models: AiModel[] }>("/api/admin/ai-models", fetcher); - const { data: storageData, mutate: mutateStorage } = useSWR<{ configs: StorageConfig[] }>("/api/admin/storage", fetcher); - const { data: siteSettingsData, mutate: mutateSiteSettings } = useSWR("/api/site-settings", fetcher); - const { data: navLinksData, mutate: mutateNavLinks } = useSWR<{ links: NavLink[] }>("/api/nav-links", fetcher); - const { data: tutorialVideosData, mutate: mutateTutorialVideos } = useSWR<{ videos: TutorialVideo[] }>("/api/tutorial-videos", fetcher); + const { data: providersData, mutate: mutateProviders } = useSWR<{ + providers: AiProvider[]; + }>("/api/admin/ai-providers", fetcher); + const { data: modelsData, mutate: mutateModels } = useSWR<{ + models: AiModel[]; + }>("/api/admin/ai-models", fetcher); + const { data: storageData, mutate: mutateStorage } = useSWR<{ + configs: StorageConfig[]; + }>("/api/admin/storage", fetcher); + const { data: siteSettingsData, mutate: mutateSiteSettings } = + useSWR("/api/site-settings", fetcher); + const { data: navLinksData, mutate: mutateNavLinks } = useSWR<{ + links: NavLink[]; + }>("/api/nav-links", fetcher); + const { data: tutorialVideosData, mutate: mutateTutorialVideos } = useSWR<{ + videos: TutorialVideo[]; + }>("/api/tutorial-videos", fetcher); const { data: authSettingsData, mutate: mutateAuthSettings } = useSWR( "/api/auth-settings", - fetcher, + fetcher + ); + const { + data: moonshotDetailAgentSettingData, + mutate: mutateMoonshotDetailAgentSetting, + } = useSWR<{ setting?: { value?: unknown } }>( + `/api/admin/settings?key=${DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY}`, + fetcher ); - const { data: moonshotDetailAgentSettingData, mutate: mutateMoonshotDetailAgentSetting } = - useSWR<{ setting?: { value?: unknown } }>( - `/api/admin/settings?key=${DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY}`, - fetcher, - ); const { data: productPlanningAiModelConfigData, mutate: mutateProductPlanningAiModelConfig, } = useSWR<{ setting?: { value?: unknown } }>( `/api/admin/settings?key=${PRODUCT_PLANNING_AI_MODEL_CONFIG_KEY}`, - fetcher, + fetcher ); const { data: enhanceProConfigData, mutate: mutateEnhanceProConfig } = useSWR<{ setting?: { value?: unknown } }>( `/api/admin/settings?key=${ENHANCE_PRO_KREA_SETTING_KEY}`, - fetcher, + fetcher ); const { data: homeAnimationSettingData, mutate: mutateHomeAnimationSetting } = useSWR<{ setting?: { value?: unknown } }>( `/api/admin/settings?key=${HOME_CHAT_ANIMATION_CONFIG_KEY}`, - fetcher, + fetcher ); // 🆕 生图记录数据 @@ -774,17 +860,18 @@ export function AdminSettingsClient() { }>(`/api/admin/images?${imageQueryParams}`, fetcher); // 🆕 生图数据分析 - const analyticsUrl = activeTab === "analytics" - ? `/api/admin/images/analytics?month=${selectedMonth}${selectedDate ? `&date=${selectedDate}` : ""}` - : null; + const analyticsUrl = + activeTab === "analytics" + ? `/api/admin/images/analytics?month=${selectedMonth}${selectedDate ? `&date=${selectedDate}` : ""}` + : null; const { data: analyticsData } = useSWR(analyticsUrl, fetcher); const detailAgentModelOptions = useMemo( () => (modelsData?.models || []).filter( - (model) => !model.supportsImageGen && model.isEnabled && !model.isHidden, + (model) => !model.supportsImageGen && model.isEnabled && !model.isHidden ), - [modelsData?.models], + [modelsData?.models] ); // 网站设置状态 @@ -844,7 +931,8 @@ export function AdminSettingsClient() { pointsSystemConfig: defaultPointsSystemConfig, pointsPackages: defaultPointsPackages, dulupayConfig: defaultDulupayConfig, - lovartPromoText: "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠", + lovartPromoText: + "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠", lovartPromoActionText: "立即升级", lovartHeroTitle: "由壹点AI 让设计更简单", lovartHeroSubtitle: "懂你的设计代理,帮你搞定一切", @@ -853,12 +941,13 @@ export function AdminSettingsClient() { membershipConfig: defaultMembershipConfig, membershipUserOverrides: defaultMembershipUserOverrides, }); - const [homeAnimationConfig, setHomeAnimationConfig] = useState({ - enabled: false, - fileName: "", - htmlContent: "", - updatedAt: null, - }); + const [homeAnimationConfig, setHomeAnimationConfig] = + useState({ + enabled: false, + fileName: "", + htmlContent: "", + updatedAt: null, + }); const [newOverrideUserId, setNewOverrideUserId] = useState(""); const [newOverrideEmail, setNewOverrideEmail] = useState(""); const [newOverridePlanId, setNewOverridePlanId] = useState(""); @@ -890,10 +979,12 @@ export function AdminSettingsClient() { usageTimeEnabled: siteSettingsData.usageTimeEnabled ?? false, usageTimeStart: siteSettingsData.usageTimeStart || "08:00", usageTimeEnd: siteSettingsData.usageTimeEnd || "20:00", - usageTimeMessage: siteSettingsData.usageTimeMessage || "由壹点累了也是需要休息的", + usageTimeMessage: + siteSettingsData.usageTimeMessage || "由壹点累了也是需要休息的", defaultTheme: siteSettingsData.defaultTheme || "system", bannerEnabled: siteSettingsData.bannerEnabled ?? true, - bannerText: siteSettingsData.bannerText || "升级会员, Nano Banana Pro 免费365天!", + bannerText: + siteSettingsData.bannerText || "升级会员, Nano Banana Pro 免费365天!", bannerLink: siteSettingsData.bannerLink || null, toolbox1Name: siteSettingsData.toolbox1Name || "工具箱", toolbox1Enabled: siteSettingsData.toolbox1Enabled ?? true, @@ -902,48 +993,72 @@ export function AdminSettingsClient() { tutorialVideoUrl: siteSettingsData.tutorialVideoUrl || null, tutorialEnabled: siteSettingsData.tutorialEnabled ?? false, rankingEnabled: siteSettingsData.rankingEnabled ?? true, - productPlanningEnabled: (siteSettingsData as any).productPlanningEnabled ?? true, + productPlanningEnabled: + (siteSettingsData as any).productPlanningEnabled ?? true, canvasEnabled: siteSettingsData.canvasEnabled ?? true, agentEnabled: (siteSettingsData as any).agentEnabled ?? true, - canvasAgentModeEnabled: (siteSettingsData as any).canvasAgentModeEnabled ?? true, + canvasAgentModeEnabled: + (siteSettingsData as any).canvasAgentModeEnabled ?? true, agentCategoryTreeConfig: { - productCategories: Array.isArray((siteSettingsData as any).agentCategoryTreeConfig?.productCategories) - ? (siteSettingsData as any).agentCategoryTreeConfig.productCategories + productCategories: Array.isArray( + (siteSettingsData as any).agentCategoryTreeConfig?.productCategories + ) + ? (siteSettingsData as any).agentCategoryTreeConfig + .productCategories : [], - audienceTags: Array.isArray((siteSettingsData as any).agentCategoryTreeConfig?.audienceTags) + audienceTags: Array.isArray( + (siteSettingsData as any).agentCategoryTreeConfig?.audienceTags + ) ? (siteSettingsData as any).agentCategoryTreeConfig.audienceTags : [], - usageScenarios: Array.isArray((siteSettingsData as any).agentCategoryTreeConfig?.usageScenarios) + usageScenarios: Array.isArray( + (siteSettingsData as any).agentCategoryTreeConfig?.usageScenarios + ) ? (siteSettingsData as any).agentCategoryTreeConfig.usageScenarios : [], - brands: Array.isArray((siteSettingsData as any).agentCategoryTreeConfig?.brands) + brands: Array.isArray( + (siteSettingsData as any).agentCategoryTreeConfig?.brands + ) ? (siteSettingsData as any).agentCategoryTreeConfig.brands : [], }, agentSecondaryTypeConfig: { - image: Array.isArray((siteSettingsData as any).agentSecondaryTypeConfig?.image) + image: Array.isArray( + (siteSettingsData as any).agentSecondaryTypeConfig?.image + ) ? (siteSettingsData as any).agentSecondaryTypeConfig.image : [], - video: Array.isArray((siteSettingsData as any).agentSecondaryTypeConfig?.video) + video: Array.isArray( + (siteSettingsData as any).agentSecondaryTypeConfig?.video + ) ? (siteSettingsData as any).agentSecondaryTypeConfig.video : [], - text: Array.isArray((siteSettingsData as any).agentSecondaryTypeConfig?.text) + text: Array.isArray( + (siteSettingsData as any).agentSecondaryTypeConfig?.text + ) ? (siteSettingsData as any).agentSecondaryTypeConfig.text : [], }, agentWorkflowUiConfig: normalizeAgentWorkflowUiConfig( (siteSettingsData as any).agentWorkflowUiConfig ), - realtimeCanvasEnabled: (siteSettingsData as any).realtimeCanvasEnabled ?? true, - aiDetailGenerationEnabled: siteSettingsData.aiDetailGenerationEnabled ?? false, + realtimeCanvasEnabled: + (siteSettingsData as any).realtimeCanvasEnabled ?? true, + aiDetailGenerationEnabled: + siteSettingsData.aiDetailGenerationEnabled ?? false, canvasDefaultModelId: siteSettingsData.canvasDefaultModelId || null, - canvasAutoModeDefaultModelId: siteSettingsData.canvasAutoModeDefaultModelId || null, - canvasDetailDefaultModelId: siteSettingsData.canvasDetailDefaultModelId || null, - canvasDetailAgentModelId: (siteSettingsData as any).canvasDetailAgentModelId || null, + canvasAutoModeDefaultModelId: + siteSettingsData.canvasAutoModeDefaultModelId || null, + canvasDetailDefaultModelId: + siteSettingsData.canvasDetailDefaultModelId || null, + canvasDetailAgentModelId: + (siteSettingsData as any).canvasDetailAgentModelId || null, lovartComposerPrefix: String((siteSettingsData as any).lovartComposerPrefix || "").trim() || "让Buyi-ai", - lovartComposerPreviewTexts: Array.isArray(siteSettingsData.lovartComposerPreviewTexts) + lovartComposerPreviewTexts: Array.isArray( + siteSettingsData.lovartComposerPreviewTexts + ) ? siteSettingsData.lovartComposerPreviewTexts .map((item: any) => String(item).trim()) .filter(Boolean) @@ -954,56 +1069,82 @@ export function AdminSettingsClient() { ], lovartPromptTagVisibility: { nanoBananaPro: - (siteSettingsData as any).lovartPromptTagVisibility?.nanoBananaPro !== false, - design: (siteSettingsData as any).lovartPromptTagVisibility?.design !== false, - branding: (siteSettingsData as any).lovartPromptTagVisibility?.branding !== false, + (siteSettingsData as any).lovartPromptTagVisibility + ?.nanoBananaPro !== false, + design: + (siteSettingsData as any).lovartPromptTagVisibility?.design !== + false, + branding: + (siteSettingsData as any).lovartPromptTagVisibility?.branding !== + false, illustration: - (siteSettingsData as any).lovartPromptTagVisibility?.illustration !== false, - ecommerce: (siteSettingsData as any).lovartPromptTagVisibility?.ecommerce !== false, - video: (siteSettingsData as any).lovartPromptTagVisibility?.video !== false, + (siteSettingsData as any).lovartPromptTagVisibility + ?.illustration !== false, + ecommerce: + (siteSettingsData as any).lovartPromptTagVisibility?.ecommerce !== + false, + video: + (siteSettingsData as any).lovartPromptTagVisibility?.video !== + false, }, contactConfig: { - phone: String((siteSettingsData as any).contactConfig?.phone || "").trim(), - wechatQrUrl: String((siteSettingsData as any).contactConfig?.wechatQrUrl || "").trim(), + phone: String( + (siteSettingsData as any).contactConfig?.phone || "" + ).trim(), + wechatQrUrl: String( + (siteSettingsData as any).contactConfig?.wechatQrUrl || "" + ).trim(), }, storageFailoverConfig: { - enabled: (siteSettingsData as any).storageFailoverConfig?.enabled === true, + enabled: + (siteSettingsData as any).storageFailoverConfig?.enabled === true, backupStorageConfigId: String( - (siteSettingsData as any).storageFailoverConfig?.backupStorageConfigId || "" + (siteSettingsData as any).storageFailoverConfig + ?.backupStorageConfigId || "" ).trim(), }, ossImageMigrationConfig: { - enabled: (siteSettingsData as any).ossImageMigrationConfig?.enabled !== false, + enabled: + (siteSettingsData as any).ossImageMigrationConfig?.enabled !== + false, preferAliyunForUploads: - (siteSettingsData as any).ossImageMigrationConfig?.preferAliyunForUploads !== false, + (siteSettingsData as any).ossImageMigrationConfig + ?.preferAliyunForUploads !== false, sourceStorageConfigId: String( - (siteSettingsData as any).ossImageMigrationConfig?.sourceStorageConfigId || "" + (siteSettingsData as any).ossImageMigrationConfig + ?.sourceStorageConfigId || "" ).trim(), targetStorageConfigId: String( - (siteSettingsData as any).ossImageMigrationConfig?.targetStorageConfigId || "" + (siteSettingsData as any).ossImageMigrationConfig + ?.targetStorageConfigId || "" ).trim(), migrateAfterHours: Math.max( 1, Math.min( 720, - Number((siteSettingsData as any).ossImageMigrationConfig?.migrateAfterHours) || - defaultOssImageMigrationConfig.migrateAfterHours + Number( + (siteSettingsData as any).ossImageMigrationConfig + ?.migrateAfterHours + ) || defaultOssImageMigrationConfig.migrateAfterHours ) ), cleanupAfterHours: Math.max( 2, Math.min( 1440, - Number((siteSettingsData as any).ossImageMigrationConfig?.cleanupAfterHours) || - defaultOssImageMigrationConfig.cleanupAfterHours + Number( + (siteSettingsData as any).ossImageMigrationConfig + ?.cleanupAfterHours + ) || defaultOssImageMigrationConfig.cleanupAfterHours ) ), batchSize: Math.max( 1, Math.min( 100, - Number((siteSettingsData as any).ossImageMigrationConfig?.batchSize) || - defaultOssImageMigrationConfig.batchSize + Number( + (siteSettingsData as any).ossImageMigrationConfig?.batchSize + ) || defaultOssImageMigrationConfig.batchSize ) ), }, @@ -1013,12 +1154,18 @@ export function AdminSettingsClient() { lovartPromoText: siteSettingsData.lovartPromoText || "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠", - lovartPromoActionText: siteSettingsData.lovartPromoActionText || "立即升级", - lovartHeroTitle: siteSettingsData.lovartHeroTitle || "由壹点AI 让设计更简单", - lovartHeroSubtitle: siteSettingsData.lovartHeroSubtitle || "懂你的设计代理,帮你搞定一切", - homeVariant: siteSettingsData.homeVariant === "lovart" ? "lovart" : "classic", + lovartPromoActionText: + siteSettingsData.lovartPromoActionText || "立即升级", + lovartHeroTitle: + siteSettingsData.lovartHeroTitle || "由壹点AI 让设计更简单", + lovartHeroSubtitle: + siteSettingsData.lovartHeroSubtitle || "懂你的设计代理,帮你搞定一切", + homeVariant: + siteSettingsData.homeVariant === "lovart" ? "lovart" : "classic", surpriseCategories: Array.isArray(siteSettingsData.surpriseCategories) - ? siteSettingsData.surpriseCategories.map((item: any) => String(item).trim()).filter(Boolean) + ? siteSettingsData.surpriseCategories + .map((item: any) => String(item).trim()) + .filter(Boolean) : ["全部", "惊喜"], membershipConfig: normalizedMembershipConfig, membershipUserOverrides: normalizedMembershipOverrides, @@ -1047,7 +1194,7 @@ export function AdminSettingsClient() { useEffect(() => { const normalized = normalizeDetailAgentLocalMoonshotConfig( - moonshotDetailAgentSettingData?.setting?.value, + moonshotDetailAgentSettingData?.setting?.value ); setMoonshotDetailAgentConfig({ providerId: "", @@ -1096,12 +1243,16 @@ export function AdminSettingsClient() { backupScale: "4", // 备用API放大倍数 "2" | "4" | "8" backupModel: "RealESRGAN_x4plus", // 备用API模型 }); - const [enhanceProConfig, setEnhanceProConfig] = useState(defaultEnhanceProKreaConfig()); + const [enhanceProConfig, setEnhanceProConfig] = useState( + defaultEnhanceProKreaConfig() + ); const [enhanceProPresetEditorMode, setEnhanceProPresetEditorMode] = useState< "simple" | "advanced" >("simple"); - const [selectedEnhanceProPresetId, setSelectedEnhanceProPresetId] = useState(""); - const [draggingEnhanceProPresetId, setDraggingEnhanceProPresetId] = useState(""); + const [selectedEnhanceProPresetId, setSelectedEnhanceProPresetId] = + useState(""); + const [draggingEnhanceProPresetId, setDraggingEnhanceProPresetId] = + useState(""); // 图像拓展API配置状态 const [extendConfig, setExtendConfig] = useState({ @@ -1119,9 +1270,9 @@ export function AdminSettingsClient() { const [angleEditConfig, setAngleEditConfig] = useState({ enabled: false, falApiKey: "", - loraScale: 1.0, // 官方默认值 + loraScale: 1.0, // 官方默认值 inferenceSteps: 28, - guidanceScale: 4.5, // 官方默认值 + guidanceScale: 4.5, // 官方默认值 maxImageSize: 10, timeout: 120, }); @@ -1142,27 +1293,27 @@ export function AdminSettingsClient() { }); // 实时画布API配置状态 -const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ - enabled: true, - providerType: "bfl" as "bfl" | "replicate" | "fal", - bflApiKey: "", - bflBaseUrl: DEFAULT_BFL_BASE_URL, - bflModel: DEFAULT_BFL_REALTIME_MODEL, - falApiKey: "", - falModel: "", - falBaseUrl: "https://fal.run", - replicateApiToken: "", - replicateModel: "black-forest-labs/flux-2-klein-4b", - fallbackEnabled: false, - fallbackProviderType: "bfl" as "bfl" | "replicate" | "fal", - fallbackBflApiKey: "", - fallbackBflBaseUrl: DEFAULT_BFL_BASE_URL, - fallbackBflModel: DEFAULT_BFL_REALTIME_MODEL, - fallbackFalApiKey: "", - fallbackFalModel: "", - fallbackFalBaseUrl: "https://fal.run", - fallbackReplicateApiToken: "", - fallbackReplicateModel: "black-forest-labs/flux-2-klein-4b", + const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ + enabled: true, + providerType: "bfl" as "bfl" | "replicate" | "fal", + bflApiKey: "", + bflBaseUrl: DEFAULT_BFL_BASE_URL, + bflModel: DEFAULT_BFL_REALTIME_MODEL, + falApiKey: "", + falModel: "", + falBaseUrl: "https://fal.run", + replicateApiToken: "", + replicateModel: "black-forest-labs/flux-2-klein-4b", + fallbackEnabled: false, + fallbackProviderType: "bfl" as "bfl" | "replicate" | "fal", + fallbackBflApiKey: "", + fallbackBflBaseUrl: DEFAULT_BFL_BASE_URL, + fallbackBflModel: DEFAULT_BFL_REALTIME_MODEL, + fallbackFalApiKey: "", + fallbackFalModel: "", + fallbackFalBaseUrl: "https://fal.run", + fallbackReplicateApiToken: "", + fallbackReplicateModel: "black-forest-labs/flux-2-klein-4b", renderMode: "balanced" as "strict-structure" | "balanced" | "stylize", throttleInterval: 100, maxBuffering: 2, @@ -1181,7 +1332,8 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const [sceneFusionConfig, setSceneFusionConfig] = useState({ enabled: true, preRmbgEnabled: true, - defaultPrompt: "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。", + defaultPrompt: + "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。", maxScenes: 10, modelId: "", // 首选生图模型ID(多样性模式下优先尝试) }); @@ -1213,22 +1365,32 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const [thirdPartySqlServerConfig, setThirdPartySqlServerConfig] = useState({ ...defaultThirdPartySqlServerConfig, }); - const [isTestingThirdPartySqlServer, setIsTestingThirdPartySqlServer] = useState(false); - const [isLocalAiSettingsExpanded, setIsLocalAiSettingsExpanded] = useState(false); - const [isThirdPartySqlServerFieldsVisible, setIsThirdPartySqlServerFieldsVisible] = + const [isTestingThirdPartySqlServer, setIsTestingThirdPartySqlServer] = useState(false); + const [expandedLocalAiSections, setExpandedLocalAiSections] = useState< + Record + >({}); + const [ + isThirdPartySqlServerFieldsVisible, + setIsThirdPartySqlServerFieldsVisible, + ] = useState(false); const [productPlanningApiConfig, setProductPlanningApiConfig] = useState(defaultProductPlanningApiConfig()); const [productPlanningAiModelConfig, setProductPlanningAiModelConfig] = - useState(defaultProductPlanningAiModelConfig()); + useState( + defaultProductPlanningAiModelConfig() + ); // 加载抠图API配置 - const { data: rmbgConfigData } = useSWR("/api/admin/settings?key=rmbg_api_config", fetcher); + const { data: rmbgConfigData } = useSWR( + "/api/admin/settings?key=rmbg_api_config", + fetcher + ); useEffect(() => { if (rmbgConfigData?.setting?.value) { - const config = rmbgConfigData.setting.value as { + const config = rmbgConfigData.setting.value as { provider?: "rmbg" | "photoroom" | "aoscdn"; - apiUrl?: string; + apiUrl?: string; apiKey?: string; photoroomApiKey?: string; aosApiKey?: string; @@ -1257,11 +1419,14 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ }, [rmbgConfigData]); // 加载变清晰API配置 - const { data: enhanceConfigData } = useSWR("/api/admin/settings?key=enhance_api_config", fetcher); + const { data: enhanceConfigData } = useSWR( + "/api/admin/settings?key=enhance_api_config", + fetcher + ); useEffect(() => { if (enhanceConfigData?.setting?.value) { - const config = enhanceConfigData.setting.value as { - apiUrl?: string; + const config = enhanceConfigData.setting.value as { + apiUrl?: string; apiKey?: string; type?: string; scaleFactor?: number; @@ -1287,7 +1452,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ useEffect(() => { if (!enhanceProConfigData) return; - setEnhanceProConfig(normalizeEnhanceProKreaConfig(enhanceProConfigData.setting?.value)); + setEnhanceProConfig( + normalizeEnhanceProKreaConfig(enhanceProConfigData.setting?.value) + ); }, [enhanceProConfigData]); useEffect(() => { @@ -1299,7 +1466,7 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ } const selectedExists = enhanceProConfig.presets.some( - (preset) => preset.id === selectedEnhanceProPresetId, + (preset) => preset.id === selectedEnhanceProPresetId ); if (selectedExists) { return; @@ -1314,9 +1481,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const selectedEnhanceProPresetIndex = useMemo( () => enhanceProConfig.presets.findIndex( - (preset) => preset.id === selectedEnhanceProPresetId, + (preset) => preset.id === selectedEnhanceProPresetId ), - [enhanceProConfig.presets, selectedEnhanceProPresetId], + [enhanceProConfig.presets, selectedEnhanceProPresetId] ); const selectedEnhanceProPreset = @@ -1325,10 +1492,13 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ : null; // 加载图像拓展API配置 - const { data: extendConfigData } = useSWR("/api/admin/settings?key=extend_api_config", fetcher); + const { data: extendConfigData } = useSWR( + "/api/admin/settings?key=extend_api_config", + fetcher + ); useEffect(() => { if (extendConfigData?.setting?.value) { - const config = extendConfigData.setting.value as { + const config = extendConfigData.setting.value as { apiKey?: string; xScale?: number; yScale?: number; @@ -1342,10 +1512,13 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ }, [extendConfigData]); // 加载去水印API配置 - const { data: watermarkRemoveConfigData } = useSWR("/api/admin/settings?key=watermark_remove_config", fetcher); + const { data: watermarkRemoveConfigData } = useSWR( + "/api/admin/settings?key=watermark_remove_config", + fetcher + ); useEffect(() => { if (watermarkRemoveConfigData?.setting?.value) { - const config = watermarkRemoveConfigData.setting.value as { + const config = watermarkRemoveConfigData.setting.value as { apiKey?: string; }; setWatermarkRemoveConfig({ @@ -1360,21 +1533,28 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ ); useEffect(() => { setProductPlanningApiConfig( - normalizeProductPlanningApiConfig(productPlanningApiConfigData?.setting?.value) + normalizeProductPlanningApiConfig( + productPlanningApiConfigData?.setting?.value + ) ); }, [productPlanningApiConfigData]); useEffect(() => { setProductPlanningAiModelConfig( - normalizeProductPlanningAiModelConfig(productPlanningAiModelConfigData?.setting?.value) + normalizeProductPlanningAiModelConfig( + productPlanningAiModelConfigData?.setting?.value + ) ); }, [productPlanningAiModelConfigData]); // 加载角度编辑API配置 - const { data: angleEditConfigData } = useSWR("/api/admin/settings?key=angle_edit_config", fetcher); + const { data: angleEditConfigData } = useSWR( + "/api/admin/settings?key=angle_edit_config", + fetcher + ); useEffect(() => { if (angleEditConfigData?.setting?.value) { - const config = angleEditConfigData.setting.value as { + const config = angleEditConfigData.setting.value as { enabled?: boolean; falApiKey?: string; loraScale?: number; @@ -1395,7 +1575,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ } }, [angleEditConfigData]); - const { data: layerSplitConfigData } = useSWR("/api/admin/settings?key=layer_split_config", fetcher); + const { data: layerSplitConfigData } = useSWR( + "/api/admin/settings?key=layer_split_config", + fetcher + ); useEffect(() => { if (layerSplitConfigData?.setting?.value) { const config = layerSplitConfigData.setting.value as { @@ -1418,13 +1601,25 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ replicateApiToken: config.replicateApiToken || "", replicateModel: config.replicateModel || "qwen/qwen-image-layered", layers: Math.max(2, Math.min(12, Number(config.layers) || 4)), - resolution: Math.max(256, Math.min(1536, Number(config.resolution) || 640)), - trueCfgScale: Math.max(1, Math.min(12, Number(config.trueCfgScale) || 4)), - numInferenceSteps: Math.max(1, Math.min(80, Number(config.numInferenceSteps) || 30)), + resolution: Math.max( + 256, + Math.min(1536, Number(config.resolution) || 640) + ), + trueCfgScale: Math.max( + 1, + Math.min(12, Number(config.trueCfgScale) || 4) + ), + numInferenceSteps: Math.max( + 1, + Math.min(80, Number(config.numInferenceSteps) || 30) + ), cfgNormalize: config.cfgNormalize !== false, useEnPrompt: config.useEnPrompt !== false, negativePrompt: config.negativePrompt || "", - timeoutMs: Math.max(30000, Math.min(900000, Number(config.timeoutMs) || 300000)), + timeoutMs: Math.max( + 30000, + Math.min(900000, Number(config.timeoutMs) || 300000) + ), }); } }, [layerSplitConfigData]); @@ -1456,17 +1651,27 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ password: config.password || "", encrypt: config.encrypt === true, trustServerCertificate: config.trustServerCertificate !== false, - connectionTimeoutMs: Math.max(1000, Math.min(120000, Number(config.connectionTimeoutMs) || 15000)), - requestTimeoutMs: Math.max(1000, Math.min(120000, Number(config.requestTimeoutMs) || 30000)), + connectionTimeoutMs: Math.max( + 1000, + Math.min(120000, Number(config.connectionTimeoutMs) || 15000) + ), + requestTimeoutMs: Math.max( + 1000, + Math.min(120000, Number(config.requestTimeoutMs) || 30000) + ), }); } }, [thirdPartySqlServerConfigData]); + const isThirdPartySqlServerSectionExpanded = Boolean( + expandedLocalAiSections["third-party-sqlserver"] + ); + useEffect(() => { - if (!isLocalAiSettingsExpanded) { + if (!isThirdPartySqlServerSectionExpanded) { setIsThirdPartySqlServerFieldsVisible(false); } - }, [isLocalAiSettingsExpanded]); + }, [isThirdPartySqlServerSectionExpanded]); // 加载实时画布API配置 const { data: realtimeCanvasConfigData } = useSWR( @@ -1525,12 +1730,14 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ : "bfl", bflApiKey: config.bflApiKey || config.runwareApiKey || "", bflBaseUrl: config.bflBaseUrl || DEFAULT_BFL_BASE_URL, - bflModel: config.bflModel || config.runwareModel || DEFAULT_BFL_REALTIME_MODEL, + bflModel: + config.bflModel || config.runwareModel || DEFAULT_BFL_REALTIME_MODEL, falApiKey: config.falApiKey || "", falBaseUrl: config.falBaseUrl || "https://fal.run", falModel: config.falModel || "", replicateApiToken: config.replicateApiToken || "", - replicateModel: config.replicateModel || "black-forest-labs/flux-2-klein-4b", + replicateModel: + config.replicateModel || "black-forest-labs/flux-2-klein-4b", fallbackEnabled: config.fallbackEnabled === true, fallbackProviderType: config.fallbackProviderType === "replicate" @@ -1539,12 +1746,12 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ ? "fal" : "bfl", fallbackBflApiKey: - config.fallbackBflApiKey || - config.fallbackRunwareApiKey || - "", + config.fallbackBflApiKey || config.fallbackRunwareApiKey || "", fallbackBflBaseUrl: config.fallbackBflBaseUrl || DEFAULT_BFL_BASE_URL, fallbackBflModel: - config.fallbackBflModel || config.fallbackRunwareModel || DEFAULT_BFL_REALTIME_MODEL, + config.fallbackBflModel || + config.fallbackRunwareModel || + DEFAULT_BFL_REALTIME_MODEL, fallbackFalApiKey: config.fallbackFalApiKey || "", fallbackFalBaseUrl: config.fallbackFalBaseUrl || "https://fal.run", fallbackFalModel: config.fallbackFalModel || "", @@ -1552,7 +1759,8 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ fallbackReplicateModel: config.fallbackReplicateModel || "black-forest-labs/flux-2-klein-4b", renderMode: - config.renderMode === "strict-structure" || config.renderMode === "stylize" + config.renderMode === "strict-structure" || + config.renderMode === "stylize" ? config.renderMode : "balanced", throttleInterval: Number(config.throttleInterval || 100), @@ -1565,16 +1773,20 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ steps: Number(config.defaults?.steps || 3), scheduleMu: Number(config.defaults?.scheduleMu || 2.3), feedback: Number(config.defaults?.feedback || 0.95), - imageSize: config.defaults?.imageSize === "square_hd" ? "square_hd" : "square", + imageSize: + config.defaults?.imageSize === "square_hd" ? "square_hd" : "square", }); } }, [realtimeCanvasConfigData]); // 加载批量处理配置 - const { data: sceneFusionConfigData } = useSWR("/api/admin/settings?key=scene_fusion_config", fetcher); + const { data: sceneFusionConfigData } = useSWR( + "/api/admin/settings?key=scene_fusion_config", + fetcher + ); useEffect(() => { if (sceneFusionConfigData?.setting?.value) { - const config = sceneFusionConfigData.setting.value as { + const config = sceneFusionConfigData.setting.value as { enabled?: boolean; preRmbgEnabled?: boolean; defaultPrompt?: string; @@ -1584,7 +1796,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ setSceneFusionConfig({ enabled: config.enabled ?? true, preRmbgEnabled: config.preRmbgEnabled ?? true, - defaultPrompt: config.defaultPrompt || "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。", + defaultPrompt: + config.defaultPrompt || + "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。", maxScenes: Math.min(config.maxScenes ?? 10, 10), modelId: config.modelId ?? "", }); @@ -1624,7 +1838,8 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ providerId: worldlabsProvider.id, name: worldlabsProvider.name || "World Labs", baseUrl: - worldlabsProvider.baseUrl || getProviderDefaultBaseUrl("worldlabs-3d"), + worldlabsProvider.baseUrl || + getProviderDefaultBaseUrl("worldlabs-3d"), apiKey: "", isEnabled: worldlabsProvider.isEnabled, }); @@ -1656,12 +1871,18 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const [videoUploading, setVideoUploading] = useState(false); const [videoUploadProgress, setVideoUploadProgress] = useState(""); const [videoUploadPercent, setVideoUploadPercent] = useState(0); - const [videoUploadStage, setVideoUploadStage] = useState<"idle" | "uploading" | "compressing" | "saving" | "done">("idle"); - const [videoQuality, setVideoQuality] = useState<"low" | "medium" | "high">("medium"); + const [videoUploadStage, setVideoUploadStage] = useState< + "idle" | "uploading" | "compressing" | "saving" | "done" + >("idle"); + const [videoQuality, setVideoQuality] = useState<"low" | "medium" | "high">( + "medium" + ); // 视频教程管理状态 const [showTutorialVideoDialog, setShowTutorialVideoDialog] = useState(false); - const [editingTutorialVideo, setEditingTutorialVideo] = useState(null); + const [editingTutorialVideo, setEditingTutorialVideo] = useState< + string | null + >(null); const [tutorialVideoForm, setTutorialVideoForm] = useState({ title: "", videoUrl: "", @@ -1673,7 +1894,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ // 快捷短语状态 const [questions, setQuestions] = useState([]); - const { data: questionsData, mutate: mutateQuestions } = useSWR("/api/starter-questions", fetcher); + const { data: questionsData, mutate: mutateQuestions } = useSWR( + "/api/starter-questions", + fetcher + ); const [canvasSkillTagsTitle, setCanvasSkillTagsTitle] = useState( DEFAULT_CANVAS_SKILL_TAGS_TITLE ); @@ -1681,15 +1905,21 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const [canvasSkillTags, setCanvasSkillTags] = useState( DEFAULT_CANVAS_SKILL_TAGS ); - const [canvasSkillTagUploadingId, setCanvasSkillTagUploadingId] = useState(null); + const [canvasSkillTagUploadingId, setCanvasSkillTagUploadingId] = useState< + string | null + >(null); const { data: canvasSkillTagsData, mutate: mutateCanvasSkillTags } = useSWR<{ enabled?: boolean; title?: string; tags?: CanvasSkillTag[]; }>("/api/canvas-skill-tags", fetcher); - + // 系统通知状态 - const [notification, setNotification] = useState<{ enabled: boolean; title: string; content: string }>({ + const [notification, setNotification] = useState<{ + enabled: boolean; + title: string; + content: string; + }>({ enabled: false, title: "", content: "", @@ -1731,7 +1961,8 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ if (canvasSkillTagsData) { setCanvasSkillTagsEnabled(canvasSkillTagsData.enabled !== false); setCanvasSkillTagsTitle( - String(canvasSkillTagsData.title || "").trim() || DEFAULT_CANVAS_SKILL_TAGS_TITLE + String(canvasSkillTagsData.title || "").trim() || + DEFAULT_CANVAS_SKILL_TAGS_TITLE ); setCanvasSkillTags(normalizeCanvasSkillTags(canvasSkillTagsData.tags)); } @@ -1757,7 +1988,7 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const res = await fetch("/api/starter-questions", { method: "PUT", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(questions.filter(q => q.trim() !== "")), + body: JSON.stringify(questions.filter((q) => q.trim() !== "")), }); if (!res.ok) throw new Error("Failed"); toast.success("快捷短语已更新"); @@ -1818,7 +2049,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ }; const handleRemoveCanvasSkillTag = (index: number) => { - setCanvasSkillTags((prev) => prev.filter((_, itemIndex) => itemIndex !== index)); + setCanvasSkillTags((prev) => + prev.filter((_, itemIndex) => itemIndex !== index) + ); }; const handleSaveCanvasSkillTags = async () => { @@ -1872,12 +2105,13 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ if (!authSettingsData) return; const organizations: DingTalkOrganization[] = - (authSettingsData.dingTalkOrganizations as DingTalkOrganization[] | null) ?? []; + (authSettingsData.dingTalkOrganizations as + | DingTalkOrganization[] + | null) ?? []; // 如果没有多组织配置,但有单一 AppKey/Secret,则自动构造一个默认组织 const normalizedOrgs = - organizations.length > 0 || - !authSettingsData.dingTalkAppKey + organizations.length > 0 || !authSettingsData.dingTalkAppKey ? organizations : [ { @@ -1903,7 +2137,8 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ Number(authSettingsData.emailVerificationExpiresMinutes) || 10, resendApiKey: authSettingsData.resendApiKey || "", resendFromEmail: authSettingsData.resendFromEmail || "", - isEmailServiceConfigured: authSettingsData.isEmailServiceConfigured === true, + isEmailServiceConfigured: + authSettingsData.isEmailServiceConfigured === true, dingTalkOrganizations: normalizedOrgs, }); setIsGoogleConfigured(Boolean(authSettingsData.isGoogleConfigured)); @@ -1913,7 +2148,30 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const [providerForm, setProviderForm] = useState({ id: "", name: "", - type: "openai" as "alibaba" | "openai" | "custom" | "local" | "gemini" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "bfl-image" | "ruxa-image" | "fal-image" | "openai-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "worldlabs-3d" | "runway" | "volcano-multimodal", + type: "openai" as + | "alibaba" + | "openai" + | "custom" + | "local" + | "gemini" + | "huanke-gemini" + | "qianwen-video" + | "doubao-video" + | "ruxa-video" + | "volcano-vision" + | "volcano-video" + | "alibaba-image" + | "bfl-image" + | "ruxa-image" + | "fal-image" + | "openai-image" + | "midjourney" + | "fal-video" + | "zhipu" + | "tripo-3d" + | "worldlabs-3d" + | "runway" + | "volcano-multimodal", baseUrl: "", apiKey: "", isEnabled: true, @@ -1962,42 +2220,54 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const [editingModel, setEditingModel] = useState(null); const [modelAvatarUploading, setModelAvatarUploading] = useState(false); const [contactQrUploading, setContactQrUploading] = useState(false); - + // 🆕 火山引擎视频多模型表单(动态从API加载) - const [volcanoVideoModels, setVolcanoVideoModels] = useState>([]); + const [volcanoVideoModels, setVolcanoVideoModels] = useState< + Array<{ + modelId: string; + displayName: string; + description: string; + }> + >([]); // 🆕 当选择火山引擎视频Provider时,从API加载预设模型配置 useEffect(() => { - const selectedProvider = providersData?.providers?.find(p => p.id === modelForm.providerId); - if (selectedProvider?.type === 'volcano-video' && volcanoVideoModels.length === 0) { + const selectedProvider = providersData?.providers?.find( + (p) => p.id === modelForm.providerId + ); + if ( + selectedProvider?.type === "volcano-video" && + volcanoVideoModels.length === 0 + ) { // 从API获取预设模型配置(✅ 显示所有模型供管理员自定义) - fetch('/api/admin/volcano-video-models') - .then(res => res.json()) - .then(data => { + fetch("/api/admin/volcano-video-models") + .then((res) => res.json()) + .then((data) => { if (data.presets) { // ✅ 显示主模型和所有子模型,允许管理员自定义显示名称和描述 const allModels = data.presets.map((preset: any) => ({ modelId: preset.modelId, displayName: preset.displayName, description: preset.description, - isSubModel: preset.isSubModel || false, // 标记是否为子模型 + isSubModel: preset.isSubModel || false, // 标记是否为子模型 })); setVolcanoVideoModels(allModels); } }) - .catch(err => { - console.error('[Load Volcano Models] Error:', err); - toast.error('加载预设模型配置失败'); + .catch((err) => { + console.error("[Load Volcano Models] Error:", err); + toast.error("加载预设模型配置失败"); }); } - }, [modelForm.providerId, providersData?.providers, volcanoVideoModels.length]); + }, [ + modelForm.providerId, + providersData?.providers, + volcanoVideoModels.length, + ]); const selectedModelProviderType = - providersData?.providers?.find((p) => p.id === modelForm.providerId)?.type || ""; + providersData?.providers?.find((p) => p.id === modelForm.providerId) + ?.type || ""; const isVideoProviderForModelConfig = isVideoModelDescriptor({ providerType: selectedModelProviderType, modelId: modelForm.modelId, @@ -2110,7 +2380,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const handleDeleteUser = async (userId: string) => { if (!confirm("确定要删除此用户吗?")) return; try { - const response = await fetch(`/api/admin/users?id=${userId}`, { method: "DELETE" }); + const response = await fetch(`/api/admin/users?id=${userId}`, { + method: "DELETE", + }); if (!response.ok) throw new Error("删除失败"); toast.success("用户删除成功"); mutateUsers(); @@ -2131,13 +2403,13 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ department: batchLimitDepartment, }), }); - + const data = await response.json(); - + if (!response.ok) { throw new Error(data.error || "批量设置失败"); } - + toast.success(data.message); setShowBatchLimitDialog(false); setBatchLimitValue(""); @@ -2155,7 +2427,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ .replace(/^\uFEFF/, "") .replace(/\u0000/g, "") .replace(/\u2028|\u2029/g, "\n"); - const homeAnimationBytes = new TextEncoder().encode(normalizedHomeAnimationHtml).length; + const homeAnimationBytes = new TextEncoder().encode( + normalizedHomeAnimationHtml + ).length; if (homeAnimationBytes > HOME_CHAT_ANIMATION_MAX_BYTES) { toast.error( @@ -2175,7 +2449,8 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ } const selectedBackupConfig = (storageData?.configs || []).find( - (config) => config.id === siteSettings.storageFailoverConfig.backupStorageConfigId + (config) => + config.id === siteSettings.storageFailoverConfig.backupStorageConfigId ); if ( siteSettings.storageFailoverConfig.enabled && @@ -2205,7 +2480,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ } const targetConfig = (storageData?.configs || []).find( - (config) => config.id === siteSettings.ossImageMigrationConfig.targetStorageConfigId + (config) => + config.id === + siteSettings.ossImageMigrationConfig.targetStorageConfigId ); if ( targetConfig && @@ -2217,12 +2494,16 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ } } - const parsedMembershipConfig = normalizeMembershipConfig(siteSettings.membershipConfig); + const parsedMembershipConfig = normalizeMembershipConfig( + siteSettings.membershipConfig + ); const parsedMembershipOverrides = normalizeMembershipUserOverrides( siteSettings.membershipUserOverrides ); - const parsedPointsPackages = normalizePointsPackages(siteSettings.pointsPackages); + const parsedPointsPackages = normalizePointsPackages( + siteSettings.pointsPackages + ); const payload = { ...siteSettings, @@ -2244,7 +2525,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ }); const responseData = await response.json().catch(() => null); if (!response.ok) throw new Error(responseData?.error || "保存失败"); - const { homeAnimationConfig: _ignoredHomeAnimationConfig, ...nextSiteSettings } = payload; + const { + homeAnimationConfig: _ignoredHomeAnimationConfig, + ...nextSiteSettings + } = payload; setSiteSettings(nextSiteSettings); setHomeAnimationConfig((prev) => ({ ...prev, @@ -2271,8 +2555,7 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const normalizedFileName = String(file.name || "").trim(); const isHtmlFile = - file.type === "text/html" || - /\.html?$/i.test(normalizedFileName); + file.type === "text/html" || /\.html?$/i.test(normalizedFileName); if (!isHtmlFile) { toast.error("请上传 .html 文件"); @@ -2307,7 +2590,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ } }; - const updatePointsPackage = (index: number, updater: (item: PointsPackage) => PointsPackage) => { + const updatePointsPackage = ( + index: number, + updater: (item: PointsPackage) => PointsPackage + ) => { setSiteSettings((prev) => { const list = [...(prev.pointsPackages || [])]; if (!list[index]) return prev; @@ -2345,7 +2631,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ }; const updateMembershipConfigState = ( - updater: (config: ReturnType) => ReturnType + updater: ( + config: ReturnType + ) => ReturnType ) => { setSiteSettings((prev) => ({ ...prev, @@ -2413,7 +2701,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const plans = [...config.plans]; plans.splice(index, 1); const nextDefaultPlanId = - config.defaultPlanId === target.id ? plans[0]?.id || config.defaultPlanId : config.defaultPlanId; + config.defaultPlanId === target.id + ? plans[0]?.id || config.defaultPlanId + : config.defaultPlanId; return { ...config, @@ -2455,7 +2745,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ }; const addMembershipOverride = (kind: "id" | "email") => { - const key = kind === "id" ? newOverrideUserId.trim() : newOverrideEmail.trim().toLowerCase(); + const key = + kind === "id" + ? newOverrideUserId.trim() + : newOverrideEmail.trim().toLowerCase(); const planId = newOverridePlanId.trim(); if (!key || !planId) { @@ -2511,7 +2804,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ toast.error("请填写RMBG API地址"); return; } - if (rmbgConfig.provider === "photoroom" && !rmbgConfig.photoroomApiKey.trim()) { + if ( + rmbgConfig.provider === "photoroom" && + !rmbgConfig.photoroomApiKey.trim() + ) { toast.error("请填写PhotoRoom API密钥"); return; } @@ -2521,8 +2817,8 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ } const response = await fetch("/api/admin/settings", { - method: "POST", - headers: { "Content-Type": "application/json" }, + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "rmbg_api_config", value: { @@ -2531,8 +2827,12 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ apiKey: rmbgConfig.apiKey.trim() || undefined, photoroomApiKey: rmbgConfig.photoroomApiKey.trim() || undefined, aosApiKey: rmbgConfig.aosApiKey.trim() || undefined, - aosApiBaseUrl: rmbgConfig.aosApiBaseUrl.trim() || "https://techsz.aoscdn.com", - aosForegroundType: rmbgConfig.aosForegroundType === "auto" ? "" : rmbgConfig.aosForegroundType, + aosApiBaseUrl: + rmbgConfig.aosApiBaseUrl.trim() || "https://techsz.aoscdn.com", + aosForegroundType: + rmbgConfig.aosForegroundType === "auto" + ? "" + : rmbgConfig.aosForegroundType, aosCrop: rmbgConfig.aosCrop ? 1 : 0, aosFormat: rmbgConfig.aosFormat, }, @@ -2580,7 +2880,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ apiUrl: enhanceConfig.apiUrl.trim(), apiKey: enhanceConfig.apiKey.trim(), type: enhanceConfig.type, - scaleFactor: enhanceConfig.scaleFactor && enhanceConfig.scaleFactor !== "auto" ? Number.parseInt(enhanceConfig.scaleFactor) : undefined, + scaleFactor: + enhanceConfig.scaleFactor && enhanceConfig.scaleFactor !== "auto" + ? Number.parseInt(enhanceConfig.scaleFactor) + : undefined, // 备用API配置 useBackupApi: enhanceConfig.useBackupApi, backupApiUrl: enhanceConfig.backupApiUrl.trim(), @@ -2621,7 +2924,8 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ providerName: enhanceProConfig.providerName.trim() || "Krea Topaz Generative", baseUrl: - enhanceProConfig.baseUrl.trim() || ENHANCE_PRO_KREA_DEFAULT_BASE_URL, + enhanceProConfig.baseUrl.trim() || + ENHANCE_PRO_KREA_DEFAULT_BASE_URL, apiKey: enhanceProConfig.apiKey.trim(), defaultModel: enhanceProConfig.defaultModel, defaultOutputFormat: enhanceProConfig.defaultOutputFormat, @@ -2636,20 +2940,24 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const payload = await response.json().catch(() => ({})); if (!response.ok) { - throw new Error(String(payload?.message || payload?.error || "保存失败")); + throw new Error( + String(payload?.message || payload?.error || "保存失败") + ); } mutateEnhanceProConfig(); toast.success("图像增强 Pro 配置保存成功"); } catch (error) { - toast.error(error instanceof Error ? error.message : "保存图像增强 Pro 配置失败"); + toast.error( + error instanceof Error ? error.message : "保存图像增强 Pro 配置失败" + ); } }; const handleEnhanceProPresetChange = ( index: number, key: K, - value: EnhanceProPreset[K], + value: EnhanceProPreset[K] ) => { setEnhanceProConfig((prev) => { const nextPresets = prev.presets.map((preset, presetIndex) => { @@ -2665,7 +2973,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ [key]: value, } as EnhanceProPreset; - if (key === "name" && !nextPreset.id.startsWith("enhance-pro-preset-")) { + if ( + key === "name" && + !nextPreset.id.startsWith("enhance-pro-preset-") + ) { return nextPreset; } @@ -2684,7 +2995,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ return nextPreset; }); - if (key === "isDefault" && !nextPresets.some((preset) => preset.isDefault)) { + if ( + key === "isDefault" && + !nextPresets.some((preset) => preset.isDefault) + ) { return { ...prev, presets: nextPresets.map((preset, presetIndex) => ({ @@ -2718,7 +3032,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const handleRemoveEnhanceProPreset = (index: number) => { setEnhanceProConfig((prev) => { - const nextPresets = prev.presets.filter((_, presetIndex) => presetIndex !== index); + const nextPresets = prev.presets.filter( + (_, presetIndex) => presetIndex !== index + ); if (nextPresets.length === 0) { return { ...prev, @@ -2759,7 +3075,7 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const handleEnhanceProPresetDragStart = ( event: DragEvent, - presetId: string, + presetId: string ) => { setDraggingEnhanceProPresetId(presetId); event.dataTransfer.effectAllowed = "move"; @@ -2772,7 +3088,7 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const handleEnhanceProPresetDrop = ( event: DragEvent, - targetPresetId: string, + targetPresetId: string ) => { event.preventDefault(); const sourcePresetId = @@ -2784,10 +3100,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ } const fromIndex = enhanceProConfig.presets.findIndex( - (preset) => preset.id === sourcePresetId, + (preset) => preset.id === sourcePresetId ); const toIndex = enhanceProConfig.presets.findIndex( - (preset) => preset.id === targetPresetId, + (preset) => preset.id === targetPresetId ); if (fromIndex < 0 || toIndex < 0) { @@ -2918,10 +3234,21 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ enabled: layerSplitConfig.enabled, provider: "replicate", replicateApiToken: layerSplitConfig.replicateApiToken.trim(), - replicateModel: layerSplitConfig.replicateModel.trim() || "qwen/qwen-image-layered", - layers: Math.max(2, Math.min(12, Number(layerSplitConfig.layers) || 4)), - resolution: Math.max(256, Math.min(1536, Number(layerSplitConfig.resolution) || 640)), - trueCfgScale: Math.max(1, Math.min(12, Number(layerSplitConfig.trueCfgScale) || 4)), + replicateModel: + layerSplitConfig.replicateModel.trim() || + "qwen/qwen-image-layered", + layers: Math.max( + 2, + Math.min(12, Number(layerSplitConfig.layers) || 4) + ), + resolution: Math.max( + 256, + Math.min(1536, Number(layerSplitConfig.resolution) || 640) + ), + trueCfgScale: Math.max( + 1, + Math.min(12, Number(layerSplitConfig.trueCfgScale) || 4) + ), numInferenceSteps: Math.max( 1, Math.min(80, Number(layerSplitConfig.numInferenceSteps) || 30) @@ -2929,7 +3256,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ cfgNormalize: layerSplitConfig.cfgNormalize, useEnPrompt: layerSplitConfig.useEnPrompt, negativePrompt: layerSplitConfig.negativePrompt.trim(), - timeoutMs: Math.max(30000, Math.min(900000, Number(layerSplitConfig.timeoutMs) || 300000)), + timeoutMs: Math.max( + 30000, + Math.min(900000, Number(layerSplitConfig.timeoutMs) || 300000) + ), }, }), }); @@ -2972,35 +3302,50 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const configPayload = await configResponse.json().catch(() => null); if (!configResponse.ok) { throw new Error( - configPayload?.cause || configPayload?.message || configPayload?.error || "保存失败" + configPayload?.cause || + configPayload?.message || + configPayload?.error || + "保存失败" ); } const featurePayload = await featureResponse.json().catch(() => null); if (!featureResponse.ok) { throw new Error( - featurePayload?.cause || featurePayload?.message || featurePayload?.error || "保存失败" + featurePayload?.cause || + featurePayload?.message || + featurePayload?.error || + "保存失败" ); } toast.success("商品策划工具设置保存成功"); } catch (error) { - toast.error(error instanceof Error ? error.message : "保存商品策划工具设置失败"); + toast.error( + error instanceof Error ? error.message : "保存商品策划工具设置失败" + ); } }; const handleSaveProductPlanningAiModelConfig = async () => { try { const trimmedBaseUrl = - productPlanningAiModelConfig.baseUrl.trim() || PRODUCT_PLANNING_AI_MODEL_DEFAULT_BASE_URL; + productPlanningAiModelConfig.baseUrl.trim() || + PRODUCT_PLANNING_AI_MODEL_DEFAULT_BASE_URL; const trimmedModelId = - productPlanningAiModelConfig.modelId.trim() || PRODUCT_PLANNING_AI_MODEL_DEFAULT_MODEL_ID; + productPlanningAiModelConfig.modelId.trim() || + PRODUCT_PLANNING_AI_MODEL_DEFAULT_MODEL_ID; const trimmedDisplayName = - productPlanningAiModelConfig.displayName.trim() || "商品策划专用 Qwen3-VL"; + productPlanningAiModelConfig.displayName.trim() || + "商品策划专用 Qwen3-VL"; const trimmedProviderName = - productPlanningAiModelConfig.providerName.trim() || "Product Planning Qwen3-VL"; + productPlanningAiModelConfig.providerName.trim() || + "Product Planning Qwen3-VL"; - if (productPlanningAiModelConfig.enabled && !productPlanningAiModelConfig.apiKey.trim()) { + if ( + productPlanningAiModelConfig.enabled && + !productPlanningAiModelConfig.apiKey.trim() + ) { toast.error("启用商品策划专用模型时,请填写 API Key"); return; } @@ -3023,7 +3368,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const payload = await response.json().catch(() => null); if (!response.ok) { - throw new Error(payload?.cause || payload?.message || payload?.error || "保存失败"); + throw new Error( + payload?.cause || payload?.message || payload?.error || "保存失败" + ); } setProductPlanningAiModelConfig((prev) => ({ @@ -3036,7 +3383,11 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ mutateProductPlanningAiModelConfig(); toast.success("商品策划专用 AI 模型配置保存成功"); } catch (error) { - toast.error(error instanceof Error ? error.message : "保存商品策划专用 AI 模型配置失败"); + toast.error( + error instanceof Error + ? error.message + : "保存商品策划专用 AI 模型配置失败" + ); } }; @@ -3069,19 +3420,29 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ value: { enabled: thirdPartySqlServerConfig.enabled, host: thirdPartySqlServerConfig.host.trim(), - port: Math.max(1, Math.min(65535, Number(thirdPartySqlServerConfig.port) || 1433)), + port: Math.max( + 1, + Math.min(65535, Number(thirdPartySqlServerConfig.port) || 1433) + ), database: thirdPartySqlServerConfig.database.trim(), username: thirdPartySqlServerConfig.username.trim(), password: thirdPartySqlServerConfig.password.trim(), encrypt: thirdPartySqlServerConfig.encrypt, - trustServerCertificate: thirdPartySqlServerConfig.trustServerCertificate, + trustServerCertificate: + thirdPartySqlServerConfig.trustServerCertificate, connectionTimeoutMs: Math.max( 1000, - Math.min(120000, Number(thirdPartySqlServerConfig.connectionTimeoutMs) || 15000) + Math.min( + 120000, + Number(thirdPartySqlServerConfig.connectionTimeoutMs) || 15000 + ) ), requestTimeoutMs: Math.max( 1000, - Math.min(120000, Number(thirdPartySqlServerConfig.requestTimeoutMs) || 30000) + Math.min( + 120000, + Number(thirdPartySqlServerConfig.requestTimeoutMs) || 30000 + ) ), }, }), @@ -3089,12 +3450,16 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const payload = await response.json().catch(() => null); if (!response.ok) { - throw new Error(payload?.cause || payload?.message || payload?.error || "保存失败"); + throw new Error( + payload?.cause || payload?.message || payload?.error || "保存失败" + ); } toast.success("SQL Server 配置保存成功"); } catch (error) { - toast.error(error instanceof Error ? error.message : "保存 SQL Server 配置失败"); + toast.error( + error instanceof Error ? error.message : "保存 SQL Server 配置失败" + ); } }; @@ -3109,19 +3474,29 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ config: { enabled: thirdPartySqlServerConfig.enabled, host: thirdPartySqlServerConfig.host.trim(), - port: Math.max(1, Math.min(65535, Number(thirdPartySqlServerConfig.port) || 1433)), + port: Math.max( + 1, + Math.min(65535, Number(thirdPartySqlServerConfig.port) || 1433) + ), database: thirdPartySqlServerConfig.database.trim(), username: thirdPartySqlServerConfig.username.trim(), password: thirdPartySqlServerConfig.password.trim(), encrypt: thirdPartySqlServerConfig.encrypt, - trustServerCertificate: thirdPartySqlServerConfig.trustServerCertificate, + trustServerCertificate: + thirdPartySqlServerConfig.trustServerCertificate, connectionTimeoutMs: Math.max( 1000, - Math.min(120000, Number(thirdPartySqlServerConfig.connectionTimeoutMs) || 15000) + Math.min( + 120000, + Number(thirdPartySqlServerConfig.connectionTimeoutMs) || 15000 + ) ), requestTimeoutMs: Math.max( 1000, - Math.min(120000, Number(thirdPartySqlServerConfig.requestTimeoutMs) || 30000) + Math.min( + 120000, + Number(thirdPartySqlServerConfig.requestTimeoutMs) || 30000 + ) ), }, }), @@ -3129,14 +3504,18 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const payload = await response.json().catch(() => null); if (!response.ok) { - throw new Error(payload?.cause || payload?.message || payload?.error || "连接测试失败"); + throw new Error( + payload?.cause || payload?.message || payload?.error || "连接测试失败" + ); } toast.success( `连接成功:${payload?.serverName || thirdPartySqlServerConfig.host} / ${payload?.databaseName || thirdPartySqlServerConfig.database}(${payload?.latencyMs || 0}ms)` ); } catch (error) { - toast.error(error instanceof Error ? error.message : "SQL Server 连接测试失败"); + toast.error( + error instanceof Error ? error.message : "SQL Server 连接测试失败" + ); } finally { setIsTestingThirdPartySqlServer(false); } @@ -3147,28 +3526,35 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ try { const primaryProviderType = realtimeCanvasConfig.providerType; const bflApiKey = realtimeCanvasConfig.bflApiKey.trim(); - const bflBaseUrl = realtimeCanvasConfig.bflBaseUrl.trim() || DEFAULT_BFL_BASE_URL; - const bflModel = realtimeCanvasConfig.bflModel.trim() || DEFAULT_BFL_REALTIME_MODEL; + const bflBaseUrl = + realtimeCanvasConfig.bflBaseUrl.trim() || DEFAULT_BFL_BASE_URL; + const bflModel = + realtimeCanvasConfig.bflModel.trim() || DEFAULT_BFL_REALTIME_MODEL; const falApiKey = realtimeCanvasConfig.falApiKey.trim(); - const falBaseUrl = realtimeCanvasConfig.falBaseUrl.trim() || "https://fal.run"; + const falBaseUrl = + realtimeCanvasConfig.falBaseUrl.trim() || "https://fal.run"; const falModel = realtimeCanvasConfig.falModel.trim(); const replicateApiToken = realtimeCanvasConfig.replicateApiToken.trim(); const replicateModel = - realtimeCanvasConfig.replicateModel.trim() || "black-forest-labs/flux-2-klein-4b"; + realtimeCanvasConfig.replicateModel.trim() || + "black-forest-labs/flux-2-klein-4b"; const fallbackEnabled = realtimeCanvasConfig.fallbackEnabled; const fallbackProviderType = realtimeCanvasConfig.fallbackProviderType; const fallbackBflApiKey = realtimeCanvasConfig.fallbackBflApiKey.trim(); const fallbackBflBaseUrl = realtimeCanvasConfig.fallbackBflBaseUrl.trim() || DEFAULT_BFL_BASE_URL; const fallbackBflModel = - realtimeCanvasConfig.fallbackBflModel.trim() || DEFAULT_BFL_REALTIME_MODEL; + realtimeCanvasConfig.fallbackBflModel.trim() || + DEFAULT_BFL_REALTIME_MODEL; const fallbackFalApiKey = realtimeCanvasConfig.fallbackFalApiKey.trim(); const fallbackFalBaseUrl = realtimeCanvasConfig.fallbackFalBaseUrl.trim() || "https://fal.run"; const fallbackFalModel = realtimeCanvasConfig.fallbackFalModel.trim(); - const fallbackReplicateApiToken = realtimeCanvasConfig.fallbackReplicateApiToken.trim(); + const fallbackReplicateApiToken = + realtimeCanvasConfig.fallbackReplicateApiToken.trim(); const fallbackReplicateModel = - realtimeCanvasConfig.fallbackReplicateModel.trim() || "black-forest-labs/flux-2-klein-4b"; + realtimeCanvasConfig.fallbackReplicateModel.trim() || + "black-forest-labs/flux-2-klein-4b"; if ( realtimeCanvasConfig.enabled && @@ -3190,8 +3576,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ realtimeCanvasConfig.enabled && fallbackEnabled && ((fallbackProviderType === "bfl" && !fallbackBflApiKey) || - (fallbackProviderType === "replicate" && !fallbackReplicateApiToken) || - (fallbackProviderType === "fal" && (!fallbackFalApiKey || !fallbackFalModel))) + (fallbackProviderType === "replicate" && + !fallbackReplicateApiToken) || + (fallbackProviderType === "fal" && + (!fallbackFalApiKey || !fallbackFalModel))) ) { toast.error( fallbackProviderType === "replicate" @@ -3204,31 +3592,34 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ } if (realtimeCanvasConfig.enabled) { - const probeResponse = await fetch("/api/admin/realtime/model-capability", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - providerType: primaryProviderType, - apiKey: - primaryProviderType === "replicate" - ? replicateApiToken - : primaryProviderType === "fal" - ? falApiKey - : bflApiKey, - modelId: - primaryProviderType === "replicate" - ? replicateModel - : primaryProviderType === "fal" - ? falModel - : bflModel, - baseUrl: - primaryProviderType === "replicate" - ? undefined - : primaryProviderType === "fal" - ? falBaseUrl - : bflBaseUrl, - }), - }); + const probeResponse = await fetch( + "/api/admin/realtime/model-capability", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + providerType: primaryProviderType, + apiKey: + primaryProviderType === "replicate" + ? replicateApiToken + : primaryProviderType === "fal" + ? falApiKey + : bflApiKey, + modelId: + primaryProviderType === "replicate" + ? replicateModel + : primaryProviderType === "fal" + ? falModel + : bflModel, + baseUrl: + primaryProviderType === "replicate" + ? undefined + : primaryProviderType === "fal" + ? falBaseUrl + : bflBaseUrl, + }), + } + ); const probeData = await probeResponse.json().catch(() => null); if (!probeResponse.ok || probeData?.supported === false) { toast.error(probeData?.message || "模型能力探针失败,已阻止保存"); @@ -3236,34 +3627,44 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ } if (fallbackEnabled) { - const fallbackProbeResponse = await fetch("/api/admin/realtime/model-capability", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - providerType: fallbackProviderType, - apiKey: - fallbackProviderType === "replicate" - ? fallbackReplicateApiToken - : fallbackProviderType === "fal" - ? fallbackFalApiKey - : fallbackBflApiKey, - modelId: - fallbackProviderType === "replicate" - ? fallbackReplicateModel - : fallbackProviderType === "fal" - ? fallbackFalModel - : fallbackBflModel, - baseUrl: - fallbackProviderType === "replicate" - ? undefined - : fallbackProviderType === "fal" - ? fallbackFalBaseUrl - : fallbackBflBaseUrl, - }), - }); - const fallbackProbeData = await fallbackProbeResponse.json().catch(() => null); - if (!fallbackProbeResponse.ok || fallbackProbeData?.supported === false) { - toast.error(fallbackProbeData?.message || "备用实时服务探针失败,已阻止保存"); + const fallbackProbeResponse = await fetch( + "/api/admin/realtime/model-capability", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + providerType: fallbackProviderType, + apiKey: + fallbackProviderType === "replicate" + ? fallbackReplicateApiToken + : fallbackProviderType === "fal" + ? fallbackFalApiKey + : fallbackBflApiKey, + modelId: + fallbackProviderType === "replicate" + ? fallbackReplicateModel + : fallbackProviderType === "fal" + ? fallbackFalModel + : fallbackBflModel, + baseUrl: + fallbackProviderType === "replicate" + ? undefined + : fallbackProviderType === "fal" + ? fallbackFalBaseUrl + : fallbackBflBaseUrl, + }), + } + ); + const fallbackProbeData = await fallbackProbeResponse + .json() + .catch(() => null); + if ( + !fallbackProbeResponse.ok || + fallbackProbeData?.supported === false + ) { + toast.error( + fallbackProbeData?.message || "备用实时服务探针失败,已阻止保存" + ); return; } } @@ -3299,29 +3700,56 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ apiKey: primaryProviderType === "fal" ? falApiKey : bflApiKey, runwareModel: bflModel, renderMode: realtimeCanvasConfig.renderMode, - throttleInterval: Math.min(260, Math.max(60, Number(realtimeCanvasConfig.throttleInterval) || 100)), - maxBuffering: Math.min(6, Math.max(1, Number(realtimeCanvasConfig.maxBuffering) || 2)), + throttleInterval: Math.min( + 260, + Math.max(60, Number(realtimeCanvasConfig.throttleInterval) || 100) + ), + maxBuffering: Math.min( + 6, + Math.max(1, Number(realtimeCanvasConfig.maxBuffering) || 2) + ), maxInputResolution: Math.min( 1536, - Math.max(512, Number(realtimeCanvasConfig.maxInputResolution) || 1024) + Math.max( + 512, + Number(realtimeCanvasConfig.maxInputResolution) || 1024 + ) + ), + jpegQuality: Math.min( + 0.82, + Math.max(0.35, Number(realtimeCanvasConfig.jpegQuality) || 0.55) ), - jpegQuality: Math.min(0.82, Math.max(0.35, Number(realtimeCanvasConfig.jpegQuality) || 0.55)), previewMegapixels: Math.min( 0.8, - Math.max(0.2, Number(realtimeCanvasConfig.previewMegapixels) || 0.4) + Math.max( + 0.2, + Number(realtimeCanvasConfig.previewMegapixels) || 0.4 + ) ), detailMegapixels: Math.min( 1.2, - Math.max(0.6, Number(realtimeCanvasConfig.detailMegapixels) || 1.0) + Math.max( + 0.6, + Number(realtimeCanvasConfig.detailMegapixels) || 1.0 + ) ), outputJpegQuality: Math.min( 90, Math.max(35, Number(realtimeCanvasConfig.outputJpegQuality) || 62) ), defaults: { - steps: Math.min(6, Math.max(1, Number(realtimeCanvasConfig.steps) || 3)), - scheduleMu: Math.min(3.5, Math.max(1.5, Number(realtimeCanvasConfig.scheduleMu) || 2.3)), - feedback: Math.min(1, Math.max(0.7, Number(realtimeCanvasConfig.feedback) || 0.95)), + steps: Math.min( + 6, + Math.max(1, Number(realtimeCanvasConfig.steps) || 3) + ), + scheduleMu: Math.min( + 3.5, + Math.max(1.5, Number(realtimeCanvasConfig.scheduleMu) || 2.3) + ), + feedback: Math.min( + 1, + Math.max(0.7, Number(realtimeCanvasConfig.feedback) || 0.95) + ), imageSize: realtimeCanvasConfig.imageSize, }, }, @@ -3334,7 +3762,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ } toast.success("实时画布配置保存成功"); } catch (error) { - toast.error(error instanceof Error ? error.message : "保存实时画布配置失败"); + toast.error( + error instanceof Error ? error.message : "保存实时画布配置失败" + ); } }; @@ -3355,7 +3785,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ enabled: sceneFusionConfig.enabled, preRmbgEnabled: sceneFusionConfig.preRmbgEnabled, defaultPrompt: sceneFusionConfig.defaultPrompt.trim(), - maxScenes: Math.max(1, Math.min(10, sceneFusionConfig.maxScenes || 10)), + maxScenes: Math.max( + 1, + Math.min(10, sceneFusionConfig.maxScenes || 10) + ), modelId: sceneFusionConfig.modelId, }, }), @@ -3370,18 +3803,27 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ // --- 导航链接逻辑 --- const resetNavLinkForm = () => { - setNavLinkForm({ id: "", name: "", url: "", order: 0, isEnabled: true, toolboxGroup: 1 }); + setNavLinkForm({ + id: "", + name: "", + url: "", + order: 0, + isEnabled: true, + toolboxGroup: 1, + }); setEditingNavLink(null); }; const handleSaveNavLink = async () => { try { const method = editingNavLink ? "PUT" : "POST"; - const body = editingNavLink ? { ...navLinkForm, id: editingNavLink } : navLinkForm; - + const body = editingNavLink + ? { ...navLinkForm, id: editingNavLink } + : navLinkForm; + const response = await fetch("/api/nav-links", { method, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); @@ -3392,13 +3834,15 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ resetNavLinkForm(); } catch (error) { toast.error("保存导航链接失败"); - } + } }; const handleDeleteNavLink = async (id: string) => { if (!confirm("确定要删除此链接吗?")) return; try { - const response = await fetch(`/api/nav-links?id=${id}`, { method: "DELETE" }); + const response = await fetch(`/api/nav-links?id=${id}`, { + method: "DELETE", + }); if (!response.ok) throw new Error("删除失败"); toast.success("链接删除成功"); mutateNavLinks(); @@ -3446,7 +3890,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const handleDeleteTutorialVideo = async (id: string) => { if (!confirm("确定要删除此视频吗?")) return; try { - const response = await fetch(`/api/tutorial-videos?id=${id}`, { method: "DELETE" }); + const response = await fetch(`/api/tutorial-videos?id=${id}`, { + method: "DELETE", + }); if (!response.ok) throw new Error("删除失败"); toast.success("视频删除成功"); mutateTutorialVideos(); @@ -3488,18 +3934,24 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ try { const method = editingProvider ? "PUT" : "POST"; const isNewProvider = !editingProvider; - const isVolcanoVideo = providerForm.type === 'volcano-video'; + const isVolcanoVideo = providerForm.type === "volcano-video"; const isLemonProvider = providerForm.type === "openai" && - isLemonApiBaseUrl(providerForm.baseUrl || getProviderDefaultBaseUrl(providerForm.type)); + isLemonApiBaseUrl( + providerForm.baseUrl || getProviderDefaultBaseUrl(providerForm.type) + ); const defaultBaseUrl = getProviderDefaultBaseUrl(providerForm.type); const normalizedBaseUrl = providerForm.baseUrl.trim(); - - let requestData: any = editingProvider ? { ...providerForm, id: editingProvider } : providerForm; + + let requestData: any = editingProvider + ? { ...providerForm, id: editingProvider } + : providerForm; if ( defaultBaseUrl && - (!normalizedBaseUrl || !isHttpUrlLike(normalizedBaseUrl) || normalizedBaseUrl.includes("@")) + (!normalizedBaseUrl || + !isHttpUrlLike(normalizedBaseUrl) || + normalizedBaseUrl.includes("@")) ) { requestData.baseUrl = defaultBaseUrl; } else { @@ -3510,7 +3962,7 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const { apiKey, ...restData } = requestData; requestData = restData; } - + const response = await fetch("/api/admin/ai-providers", { method, headers: { "Content-Type": "application/json" }, @@ -3518,10 +3970,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ }); if (!response.ok) throw new Error("保存失败"); - + const data = await response.json(); const providerId = data.provider?.id || editingProvider; - + toast.success(editingProvider ? "提供商更新成功" : "提供商创建成功"); mutateProviders(); setShowProviderDialog(false); @@ -3530,22 +3982,27 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ // 🆕 如果是新创建的火山引擎视频Provider,自动添加预设模型 if (isNewProvider && isVolcanoVideo && providerId) { toast.info("正在自动添加预设模型..."); - + try { - const modelsResponse = await fetch("/api/admin/volcano-video-models", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ providerId }), - }); + const modelsResponse = await fetch( + "/api/admin/volcano-video-models", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ providerId }), + } + ); const modelsData = await modelsResponse.json(); if (modelsResponse.ok && modelsData.addedCount > 0) { - toast.success(`✅ 已自动添加 ${modelsData.addedCount} 个预设模型!`); + toast.success( + `✅ 已自动添加 ${modelsData.addedCount} 个预设模型!` + ); mutateModels(); // 刷新模型列表 } } catch (modelError) { - console.error('[Auto Add Models] Error:', modelError); + console.error("[Auto Add Models] Error:", modelError); toast.warning('预设模型添加失败,请手动点击"快速添加预设模型"按钮'); } } @@ -3555,18 +4012,22 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ await handleQuickAddLemonModels(providerId); } catch (modelError) { console.error("[Auto Add Lemon Models] Error:", modelError); - toast.warning('LemonAPI 预设模型添加失败,请手动点击"快速添加 Gemini 预设模型"按钮'); + toast.warning( + 'LemonAPI 预设模型添加失败,请手动点击"快速添加 Gemini 预设模型"按钮' + ); } } } catch (error) { toast.error("保存提供商失败"); - } + } }; const handleDeleteProvider = async (id: string) => { if (!confirm("确定删除?关联模型也会被删除。")) return; try { - const response = await fetch(`/api/admin/ai-providers?id=${id}`, { method: "DELETE" }); + const response = await fetch(`/api/admin/ai-providers?id=${id}`, { + method: "DELETE", + }); if (!response.ok) throw new Error("删除失败"); toast.success("提供商删除成功"); mutateProviders(); @@ -3689,10 +4150,13 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const trimmedDisplayName = moonshotDetailAgentConfig.displayName.trim() || "官方 Kimi K2.5"; const trimmedProviderName = - moonshotDetailAgentConfig.providerName.trim() || "Moonshot AI (Official)"; + moonshotDetailAgentConfig.providerName.trim() || + "Moonshot AI (Official)"; if (!isMoonshotOfficialBaseUrl(trimmedBaseUrl)) { - toast.error("请使用 Moonshot 官方 API 地址:https://api.moonshot.cn/v1"); + toast.error( + "请使用 Moonshot 官方 API 地址:https://api.moonshot.cn/v1" + ); return; } @@ -3701,7 +4165,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ return; } - if (!moonshotDetailAgentConfig.providerId && !moonshotDetailAgentConfig.apiKey.trim()) { + if ( + !moonshotDetailAgentConfig.providerId && + !moonshotDetailAgentConfig.apiKey.trim() + ) { toast.error("请填写 Moonshot 官方 API Key"); return; } @@ -3723,7 +4190,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ }); const settingResult = await settingResponse.json().catch(() => ({})); if (!settingResponse.ok) { - throw new Error(String(settingResult?.message || "保存 Moonshot 本地AI配置失败")); + throw new Error( + String(settingResult?.message || "保存 Moonshot 本地AI配置失败") + ); } const settingsResponse = await fetch("/api/site-settings", { @@ -3755,19 +4224,25 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ toast.success("Moonshot 官方 Kimi K2.5 已绑定为 AI详情 Agent 专用模型"); } catch (error) { console.error("[Moonshot Detail Agent Config] Error:", error); - toast.error(error instanceof Error ? error.message : "保存 Moonshot Kimi 配置失败"); + toast.error( + error instanceof Error ? error.message : "保存 Moonshot Kimi 配置失败" + ); } }; // 快速添加火山引擎视频预设模型 const handleQuickAddVolcanoModels = async (providerId: string) => { - if (!confirm("将为此服务商添加4个预设模型:\n- Seedance Pro (全功能)\n- Seedance Pro Fast (快速)\n- Seedance Lite I2V (图生视频)\n- Seedance Lite T2V (文生视频)\n\n继续吗?")) { + if ( + !confirm( + "将为此服务商添加4个预设模型:\n- Seedance Pro (全功能)\n- Seedance Pro Fast (快速)\n- Seedance Lite I2V (图生视频)\n- Seedance Lite T2V (文生视频)\n\n继续吗?" + ) + ) { return; } try { toast.info("正在添加预设模型..."); - + const response = await fetch("/api/admin/volcano-video-models", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -3790,9 +4265,8 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ if (errors && errors.length > 0) { toast.warning(`${errors.length} 个模型添加失败(可能已存在)`); } - } catch (error: any) { - console.error('[Quick Add Models] Error:', error); + console.error("[Quick Add Models] Error:", error); toast.error(error.message || "添加预设模型失败"); } }; @@ -4017,28 +4491,32 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const handleSaveModel = async () => { try { // 🆕 检查是否是火山引擎视频Provider - const selectedProvider = providersData?.providers?.find(p => p.id === modelForm.providerId); - const isVolcanoVideo = selectedProvider?.type === 'volcano-video'; + const selectedProvider = providersData?.providers?.find( + (p) => p.id === modelForm.providerId + ); + const isVolcanoVideo = selectedProvider?.type === "volcano-video"; // 如果是火山引擎视频且不是编辑模式,批量创建模型 if (isVolcanoVideo && !editingModel) { toast.info("正在添加火山引擎视频模型(1个主模型 + 4个子模型)..."); - + const response = await fetch("/api/admin/volcano-video-models", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ providerId: modelForm.providerId, // ✅ 传递所有模型配置(管理员可以自定义显示名称和描述) models: volcanoVideoModels, - }), - }); + }), + }); const data = await response.json(); if (!response.ok) throw new Error(data.error || "批量添加失败"); - toast.success(`✅ 成功添加 ${data.addedCount} 个模型(1个主模型 + ${data.addedCount - 1} 个子模型)!`); + toast.success( + `✅ 成功添加 ${data.addedCount} 个模型(1个主模型 + ${data.addedCount - 1} 个子模型)!` + ); mutateModels(); setShowModelDialog(false); resetModelForm(); @@ -4047,7 +4525,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ // 普通模型的单个创建/编辑逻辑 const method = editingModel ? "PUT" : "POST"; - const body = editingModel ? { ...modelForm, id: editingModel } : modelForm; + const body = editingModel + ? { ...modelForm, id: editingModel } + : modelForm; const response = await fetch("/api/admin/ai-models", { method, headers: { "Content-Type": "application/json" }, @@ -4060,7 +4540,7 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ setShowModelDialog(false); resetModelForm(); } catch (error: any) { - console.error('[Save Model] Error:', error); + console.error("[Save Model] Error:", error); toast.error(error.message || "保存模型失败"); } }; @@ -4068,7 +4548,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const handleDeleteModel = async (id: string) => { if (!confirm("确定删除此模型?")) return; try { - const response = await fetch(`/api/admin/ai-models?id=${id}`, { method: "DELETE" }); + const response = await fetch(`/api/admin/ai-models?id=${id}`, { + method: "DELETE", + }); if (!response.ok) throw new Error("删除失败"); toast.success("模型删除成功"); mutateModels(); @@ -4116,7 +4598,7 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ body.accessKeyId = String(body.accessKeyId || "").trim(); body.secretAccessKey = String(body.secretAccessKey || "").trim(); body.publicUrl = normalizeUrl(body.publicUrl); - + // 兼容处理:如果是阿里云 OSS,转换为 s3 类型提交 if (body.type === "aliyun-oss") { body.type = "s3"; @@ -4147,7 +4629,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const handleDeleteStorage = async (id: string) => { if (!confirm("确定删除此存储配置?")) return; try { - const response = await fetch(`/api/admin/storage?id=${id}`, { method: "DELETE" }); + const response = await fetch(`/api/admin/storage?id=${id}`, { + method: "DELETE", + }); if (!response.ok) throw new Error("删除失败"); toast.success("存储配置删除成功"); mutateStorage(); @@ -4159,13 +4643,15 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ const renderEnhanceProPresetEditor = ( preset: EnhanceProPreset, index: number, - allowReorderControls = false, + allowReorderControls = false ) => (
- + - handleEnhanceProPresetChange(index, "description", e.target.value) + handleEnhanceProPresetChange( + index, + "description", + e.target.value + ) } placeholder="告诉用户这个预设适合什么图片" /> @@ -4260,7 +4750,7 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ handleEnhanceProPresetChange( index, "model", - value as EnhanceProPreset["model"], + value as EnhanceProPreset["model"] ) } > @@ -4286,7 +4776,7 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ handleEnhanceProPresetChange( index, "outputFormat", - value as EnhanceProPreset["outputFormat"], + value as EnhanceProPreset["outputFormat"] ) } > @@ -4311,7 +4801,7 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ handleEnhanceProPresetChange( index, "subjectDetection", - value as EnhanceProPreset["subjectDetection"], + value as EnhanceProPreset["subjectDetection"] ) } > @@ -4331,12 +4821,14 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ 候选图数量 @@ -4477,7 +4974,7 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ handleEnhanceProPresetChange( index, "sharpen", - Math.min(1, Math.max(0, Number.parseFloat(e.target.value) || 0)), + Math.min(1, Math.max(0, Number.parseFloat(e.target.value) || 0)) ) } /> @@ -4498,7 +4995,7 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ handleEnhanceProPresetChange( index, "denoise", - Math.min(1, Math.max(0, Number.parseFloat(e.target.value) || 0)), + Math.min(1, Math.max(0, Number.parseFloat(e.target.value) || 0)) ) } /> @@ -4519,7 +5016,7 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ handleEnhanceProPresetChange( index, "faceEnhancementStrength", - Math.min(1, Math.max(0, Number.parseFloat(e.target.value) || 0)), + Math.min(1, Math.max(0, Number.parseFloat(e.target.value) || 0)) ) } /> @@ -4540,7 +5037,7 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ handleEnhanceProPresetChange( index, "faceEnhancementCreativity", - Math.min(1, Math.max(0, Number.parseFloat(e.target.value) || 0)), + Math.min(1, Math.max(0, Number.parseFloat(e.target.value) || 0)) ) } /> @@ -4600,8 +5097,16 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ thirdPartySqlServerConfig.host.trim() || thirdPartySqlServerConfig.database.trim() || thirdPartySqlServerConfig.username.trim() || - thirdPartySqlServerConfig.password.trim(), + thirdPartySqlServerConfig.password.trim() ); + const isLocalAiSectionExpanded = (sectionKey: string) => + Boolean(expandedLocalAiSections[sectionKey]); + const toggleLocalAiSection = (sectionKey: string) => { + setExpandedLocalAiSections((prev) => ({ + ...prev, + [sectionKey]: !prev[sectionKey], + })); + }; return (
@@ -4686,13 +5191,17 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ onClick={async () => { try { toast.info("正在从钉钉同步用户信息,可能需要几分钟..."); - const res = await fetch("/api/admin/sync-dingtalk", { method: "POST" }); + const res = await fetch("/api/admin/sync-dingtalk", { + method: "POST", + }); const data = await res.json(); if (res.ok) { toast.success( `同步完成!匹配 ${data.stats.matched}/${data.stats.total} 个用户,` + - `更新 ${data.stats.synced} 个,跳过 ${data.stats.skipped} 个` + - (data.stats.errors > 0 ? `,失败 ${data.stats.errors} 个` : "") + `更新 ${data.stats.synced} 个,跳过 ${data.stats.skipped} 个` + + (data.stats.errors > 0 + ? `,失败 ${data.stats.errors} 个` + : "") ); mutateUsers(); // 刷新用户列表 } else { @@ -4713,9 +5222,14 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ {/* 用户列表 */}
{usersData?.users?.map((user) => ( -
+
-
{user.name || user.email}
+
+ {user.name || user.email} +
邮箱: {user.email}
@@ -4723,16 +5237,22 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ 部门: {user.department || "未设置"}
- 角色: {ROLE_LABELS[user.role] ?? user.role} | 状态: {user.isActive ? "已激活" : "未激活"} + 角色: {ROLE_LABELS[user.role] ?? user.role} | 状态:{" "} + {user.isActive ? "已激活" : "未激活"}
标签: {user.isHighVip ? "高级VIP" : "普通"}
- 月度出图限额: {user.monthlyImageLimit ? `${user.monthlyImageLimit} 张/月` : "无限制"} + 月度出图限额:{" "} + {user.monthlyImageLimit + ? `${user.monthlyImageLimit} 张/月` + : "无限制"}
- 剩余积分: {Number(user.pointsBalance || 0).toLocaleString()} | 累计赠送:{" "} + 剩余积分:{" "} + {Number(user.pointsBalance || 0).toLocaleString()} | + 累计赠送:{" "} {Number(user.pointsGiftedTotal || 0).toLocaleString()}
@@ -4746,27 +5266,40 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ className="w-20 h-8 text-xs" defaultValue={user.monthlyImageLimit ?? ""} onBlur={(e) => { - const value = e.target.value ? parseInt(e.target.value, 10) : null; + const value = e.target.value + ? parseInt(e.target.value, 10) + : null; if (value !== user.monthlyImageLimit) { - handleUpdateUser(user.id, { monthlyImageLimit: value }); + handleUpdateUser(user.id, { + monthlyImageLimit: value, + }); } }} onKeyDown={(e) => { if (e.key === "Enter") { - const value = (e.target as HTMLInputElement).value - ? parseInt((e.target as HTMLInputElement).value, 10) + const value = (e.target as HTMLInputElement).value + ? parseInt( + (e.target as HTMLInputElement).value, + 10 + ) : null; if (value !== user.monthlyImageLimit) { - handleUpdateUser(user.id, { monthlyImageLimit: value }); + handleUpdateUser(user.id, { + monthlyImageLimit: value, + }); } } }} /> - 张 + + 张 +
@@ -4846,7 +5389,11 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ 扣减
-
@@ -4858,30 +5405,32 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ {usersData && usersData.totalPages > 1 && (
- 显示 {((userPage - 1) * userLimit) + 1} - {Math.min(userPage * userLimit, usersData.total)} 条,共 {usersData.total} 条 + 显示 {(userPage - 1) * userLimit + 1} -{" "} + {Math.min(userPage * userLimit, usersData.total)} 条,共{" "} + {usersData.total} 条
{/* 首页按钮 */} - - + + {/* 上一页按钮 */} - + {/* 页码显示和跳转 */}
第 @@ -4897,8 +5446,11 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ } }} onKeyDown={(e) => { - if (e.key === 'Enter') { - const page = Number.parseInt(e.currentTarget.value, 10); + if (e.key === "Enter") { + const page = Number.parseInt( + e.currentTarget.value, + 10 + ); if (page >= 1 && page <= usersData.totalPages) { setUserPage(page); } else { @@ -4908,19 +5460,25 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ }} className="w-16 h-8 text-center" /> - / {usersData.totalPages} 页 + + / {usersData.totalPages} 页 +
- + {/* 下一页按钮 */} - + {/* 末页按钮 */}
-
+
)} @@ -4943,7 +5501,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ 所有生图/视频记录 - 查看所有用户的 AI 图片与视频生成历史记录 + + 查看所有用户的 AI 图片与视频生成历史记录 + {/* 筛选条件 */} @@ -4955,7 +5515,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ { setImageSearch(e.target.value); setImagePage(1); }} + onChange={(e) => { + setImageSearch(e.target.value); + setImagePage(1); + }} className="pl-9" />
@@ -4965,7 +5528,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ { setImageStartDate(e.target.value); setImagePage(1); }} + onChange={(e) => { + setImageStartDate(e.target.value); + setImagePage(1); + }} />
@@ -4973,7 +5539,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ { setImageEndDate(e.target.value); setImagePage(1); }} + onChange={(e) => { + setImageEndDate(e.target.value); + setImagePage(1); + }} />
@@ -5042,7 +5611,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ {img.userName || img.userEmail?.split("@")[0]}
- {formatBeijingTime(img.createdAt, { month: "numeric", day: "numeric" })} + {formatBeijingTime(img.createdAt, { + month: "numeric", + day: "numeric", + })}
))} @@ -5052,7 +5624,8 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ {imagesData && imagesData.pagination.totalPages > 1 && (
- 第 {imagesData.pagination.page} / {imagesData.pagination.totalPages} 页 + 第 {imagesData.pagination.page} /{" "} + {imagesData.pagination.totalPages} 页
@@ -5785,7 +6594,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ 0 ? unique : ["全部", "精选"], + surpriseCategories: + unique.length > 0 ? unique : ["全部", "精选"], }); }} placeholder="全部, 精选, 海报, 电商" @@ -5843,9 +6657,13 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({
-
首页 / 无限画布对话框文案配置
+
+ 首页 / 无限画布对话框文案配置 +
- +
- 仅配置动态尾文案;前端会固定显示前缀后再进行打字与删字切换。支持最多 20 条。 + 仅配置动态尾文案;前端会固定显示前缀后再进行打字与删字切换。支持最多 + 20 条。
-
对话框下方标签按钮显示
+
+ 对话框下方标签按钮显示 +
{lovartPromptTagOptions.map((item) => ( -
- +
+ setSiteSettings({ ...siteSettings, @@ -5962,7 +6793,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ size="sm" className="h-8" disabled={contactQrUploading} - onClick={() => document.getElementById("contact-qr-upload")?.click()} + onClick={() => + document.getElementById("contact-qr-upload")?.click() + } > {contactQrUploading ? ( <> @@ -6007,18 +6840,26 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ id="lovartPromoText" value={siteSettings.lovartPromoText} onChange={(e) => - setSiteSettings({ ...siteSettings, lovartPromoText: e.target.value }) + setSiteSettings({ + ...siteSettings, + lovartPromoText: e.target.value, + }) } placeholder="现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠" />
- + - setSiteSettings({ ...siteSettings, lovartPromoActionText: e.target.value }) + setSiteSettings({ + ...siteSettings, + lovartPromoActionText: e.target.value, + }) } placeholder="立即升级" /> @@ -6029,7 +6870,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ id="lovartHeroTitle" value={siteSettings.lovartHeroTitle} onChange={(e) => - setSiteSettings({ ...siteSettings, lovartHeroTitle: e.target.value }) + setSiteSettings({ + ...siteSettings, + lovartHeroTitle: e.target.value, + }) } placeholder="由壹点AI 让设计更简单" /> @@ -6040,7 +6884,10 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ id="lovartHeroSubtitle" value={siteSettings.lovartHeroSubtitle} onChange={(e) => - setSiteSettings({ ...siteSettings, lovartHeroSubtitle: e.target.value }) + setSiteSettings({ + ...siteSettings, + lovartHeroSubtitle: e.target.value, + }) } placeholder="懂你的设计代理,帮你搞定一切" /> @@ -6049,7 +6896,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({
- +
支持配置套餐、FAQ、模型额度、用户套餐覆盖。保存后新版首页“升级”弹窗实时生效。
@@ -6074,7 +6923,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({
-
控制升级弹窗及会员规则是否生效
+
+ 控制升级弹窗及会员规则是否生效 +
- + @@ -6101,7 +6954,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ />
- + @@ -6114,7 +6969,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ />
- + @@ -6163,7 +7030,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ />
- + updateMembershipPlan(index, (item) => ({ ...item, - monthlyPrice: Math.max(0, Number.parseFloat(e.target.value || "0")), + monthlyPrice: Math.max( + 0, + Number.parseFloat(e.target.value || "0") + ), })) } />
- + updateMembershipPlan(index, (item) => ({ ...item, - annualPrice: Math.max(0, Number.parseFloat(e.target.value || "0")), + annualPrice: Math.max( + 0, + Number.parseFloat(e.target.value || "0") + ), })) } />
- + updateMembershipPlan(index, (item) => ({ ...item, - monthlyCredits: Math.max(0, Number.parseInt(e.target.value || "0", 10)), + monthlyCredits: Math.max( + 0, + Number.parseInt(e.target.value || "0", 10) + ), })) } />
- + updateMembershipPlan(index, (item) => ({ ...item, - dailyFreeCredits: Math.max(0, Number.parseInt(e.target.value || "0", 10)), + dailyFreeCredits: Math.max( + 0, + Number.parseInt(e.target.value || "0", 10) + ), })) } /> @@ -6235,7 +7122,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({
- + @@ -6247,7 +7136,9 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ />
- + @@ -6270,13 +7161,17 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ })) } /> - 高亮展示 + + 高亮展示 +
- +