"use client"; import { ArrowDown, ArrowLeft, ArrowUp, BarChart3, Bell, Bot, Box, Brain, Calendar, Check, CheckCircle2, ChevronLeft, ChevronRight, Clock, Copy, Crop, Crosshair, Database, Download, Edit, Eraser, Expand, ExternalLink, Eye, EyeOff, FlipHorizontal2, FolderOpen, GitBranch, Globe2, Grid3X3, Hand, HardDrive, Home, Image as ImageIcon, ImagePlus, Key, Layers, Layers3, Loader2, type LucideIcon, Maximize2, Menu, MessageSquare, MessageSquarePlus, MoreHorizontal, MoreVertical, MousePointer, Move3D, Paintbrush, Paperclip, Pause, PenTool, Play, Plus, Redo2, RefreshCw, Save, Scan, Scissors, Search, Settings, SlidersHorizontal, Sparkles, Square, StickyNote, Tag, Trash2, TrendingUp, Type, Undo2, Upload, User, Users, Video, Volume2, VolumeX, Wand2, Waypoints, Wrench, X, ZoomIn, ZoomOut, } from "lucide-react"; import Link from "next/link"; import { type ChangeEvent, type DragEvent, useEffect, useMemo, useRef, useState, } from "react"; import { toast } from "sonner"; import useSWR from "swr"; import { ExternalImageApiSettingsCard } from "@/components/admin/external-image-api-settings-card"; import { CANVAS_SKILL_ICON_OPTIONS, getCanvasSkillIconComponent, } from "@/components/canvas/canvas-skill-icons"; import { HomeAnimationEmbed } from "@/components/home/home-animation-embed"; import { DEFAULT_OPENCLAW_CHAT_BASE_URL, OPENCLAW_CHAT_CONFIG_KEY, type OpenClawChatConfig, } from "@/lib/openclaw-chat/config"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import { defaultAgentWorkflowUiConfig, normalizeAgentWorkflowUiConfig, } from "@/lib/agents/workflow-ui-config"; import { BLTCY_BFL_BASE_URL, BLTCY_BFL_MODEL_PRESETS, BLTCY_BFL_PROVIDER_PRESET, BLTCY_IMAGE_MODEL_PRESETS, BLTCY_IMAGE_OPENAI_BASE_URL, BLTCY_IMAGE_PROVIDER_PRESET, } from "@/lib/ai/bltcy-image-models"; import { BLTCY_VIDEO_MODEL_PRESETS, BLTCY_VIDEO_PROVIDER_PRESET, } from "@/lib/ai/bltcy-video-models"; import { DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL, DETAIL_AGENT_LOCAL_MOONSHOT_DISPLAY_NAME, DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID, DETAIL_AGENT_LOCAL_MOONSHOT_OPTION_ID, DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY, isDetailAgentLocalMoonshotBaseUrl, normalizeDetailAgentLocalMoonshotBaseUrl, normalizeDetailAgentLocalMoonshotConfig, } from "@/lib/ai/detail-agent-model-config"; import { FAL_IMAGE_MODEL_PRESETS } from "@/lib/ai/fal-image-models"; import { RUXA_IMAGE_MODEL_PRESETS } from "@/lib/ai/ruxa-image-models"; import { SNAPGEN_IMAGE_MODEL_PRESETS } from "@/lib/ai/snapgen-image-models"; import { BANANAIMAGE_MODEL_PRESETS } from "@/lib/ai/bananaimage-models"; import { VISIONARY_MODEL_PRESETS } from "@/lib/ai/visionary-models"; import { formatImageModelCreditDiscountLabel, getGenerationCreditCostFromImageGenConfig, getImageSizeCreditMultiplier, } from "@/lib/ai/image-model-pricing"; import { is3DModelDescriptor, isImageGenerationModelDescriptor, isVideoModelDescriptor, } from "@/lib/ai/model-classification"; import { buildVideoDurationCostMap, getConfiguredVideoDurationOptions, getVideoCreditCostFromConfig, normalizeVideoDurationOptions, recommendedVideoDurationOptions, recommendedVideoDurationReferenceCosts, recommendedVideoResolutionBaseCosts, } from "@/lib/ai/video-model-config"; import { CANVAS_IMAGE_TASK_QUEUE_CONFIG_KEY, type CanvasImageTaskQueueConfig, DEFAULT_CANVAS_IMAGE_BATCH_DISPATCH_INTERVAL_MS, DEFAULT_CANVAS_IMAGE_TASK_CONCURRENCY, MAX_CANVAS_IMAGE_BATCH_DISPATCH_INTERVAL_MS, MAX_CANVAS_IMAGE_TASK_CONCURRENCY, MIN_CANVAS_IMAGE_BATCH_DISPATCH_INTERVAL_MS, MIN_CANVAS_IMAGE_TASK_CONCURRENCY, normalizeCanvasImageTaskQueueConfig, } from "@/lib/canvas/image-task-config"; import { CANVAS_LOCAL_EDIT_CONFIG_KEY, defaultCanvasLocalEditConfig, normalizeCanvasLocalEditConfig, } from "@/lib/canvas/local-edit-config"; import { type CanvasSkillCategory, type CanvasSkillTag, DEFAULT_CANVAS_SKILL_TAGS, DEFAULT_CANVAS_SKILL_TAGS_TITLE, normalizeCanvasSkillCategories, normalizeCanvasSkillTags, } from "@/lib/canvas-skill-tags"; import { type CanvasToolbarConfig, type CanvasToolbarItemKey, DEFAULT_CANVAS_TOOLBAR_ITEMS, MAX_SELECTED_IMAGE_INLINE_ACTION_COUNT, MIN_SELECTED_IMAGE_INLINE_ACTION_COUNT, defaultCanvasToolbarConfig, getDefaultCanvasToolbarItem, normalizeCanvasToolbarConfig, } from "@/lib/canvas-toolbar-config"; import { defaultCanvasAgentFeatureSettings, normalizeCanvasAgentFeatureSettings, type CanvasAgentFeatureSettings, } from "@/lib/canvas/agent-feature-flags"; import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry"; import { AI_WASH_CONFIG_KEY, defaultAiWashConfig, normalizeAiWashConfig, } from "@/lib/ai-wash/config"; import { defaultEnhanceProKreaConfig, defaultEnhanceProPresets, ENHANCE_PRO_KREA_DEFAULT_BASE_URL, ENHANCE_PRO_KREA_DEFAULT_MODEL, ENHANCE_PRO_KREA_DEFAULT_OUTPUT_FORMAT, ENHANCE_PRO_KREA_MODELS, ENHANCE_PRO_KREA_SETTING_KEY, type EnhanceProKreaModel, type EnhanceProPreset, normalizeEnhanceProKreaConfig, } from "@/lib/enhance-pro/config"; import { DEFAULT_LOVART_HOME_SIDEBAR_ITEMS, defaultLovartHomeSidebarConfig, getDefaultLovartHomeSidebarItem, type LovartHomeSidebarConfig, type LovartHomeSidebarItemKey, normalizeLovartHomeSidebarConfig, } from "@/lib/home/lovart-sidebar-config"; import { defaultMembershipConfig, defaultMembershipUserOverrides, type MembershipFaqItem, type MembershipPlanConfig, normalizeMembershipConfig, normalizeMembershipUserOverrides, } from "@/lib/membership/config"; import { defaultThreeDModelAliasConfig, normalizeThreeDModelAliasConfig, THREED_MODEL_ALIAS_CONFIG_SETTING_KEY, type ThreeDModelAliasEntry, } from "@/lib/model3d/model-alias-config"; import { DINGTALK_NOTIFICATION_DRAFT_CONFIG_KEY, type DingTalkNotificationDraftConfig, defaultDingTalkNotificationDraftConfig, normalizeDingTalkNotificationDraftConfig, } from "@/lib/notifications/dingtalk-notification-draft-config"; import { DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY, type DingTalkRobotNotificationConfig, defaultDingTalkRobotNotificationConfig, normalizeDingTalkRobotNotificationConfig, } from "@/lib/notifications/dingtalk-robot-config"; import { type DulupayConfig, defaultDulupayConfig, normalizeDulupayConfig, } from "@/lib/payment/dulupay-config"; import { defaultPointsPackages, defaultPointsSystemConfig, type EnhanceProPointsResolutionBucket, normalizePointsPackages, normalizePointsSystemConfig, POINTS_SYSTEM_CONFIG_KEY, type PointsPackage, type PointsSystemConfig, } from "@/lib/points/config"; import { defaultProductPlanningApiConfig, normalizeProductPlanningApiConfig, PRODUCT_PLANNING_API_CONFIG_KEY, type ProductPlanningApiConfig, } from "@/lib/product-planning/config"; import { defaultProductPlanningAiModelConfig, normalizeProductPlanningAiModelConfig, PRODUCT_PLANNING_AI_MODEL_CONFIG_KEY, PRODUCT_PLANNING_AI_MODEL_DEFAULT_BASE_URL, PRODUCT_PLANNING_AI_MODEL_DEFAULT_MODEL_ID, type ProductPlanningAiModelConfig, } from "@/lib/product-planning/model-config"; import { defaultThirdPartySqlServerConfig, THIRD_PARTY_SQLSERVER_CONFIG_KEY, } from "@/lib/sqlserver/config"; import { cn, formatBeijingTime } from "@/lib/utils"; import { defaultWorldLabsKeyPoolConfig, normalizeWorldLabsKeyPoolConfig, WORLDLABS_KEY_POOL_CONFIG_KEY, } from "@/lib/worldlabs/key-pool-config-shared"; import type { AgentWorkflowUiConfig } from "@/types/agent"; import { AdminSettingsNavigation } from "./_components/admin-settings-navigation"; import { AccessSecurityDashboard } from "./_components/access-security-dashboard"; import { LocalAiCommandCenter } from "./_components/local-ai-command-center"; import { LocalAiGroupHeading } from "./_components/local-ai-section-shell"; import { LocalAiImagePipelineSections } from "./_components/local-ai-sections/image-pipeline-sections"; import { LocalAiModel3dSections } from "./_components/local-ai-sections/model3d-sections"; import { LocalAiPlanningSections } from "./_components/local-ai-sections/planning-sections"; import { LocalAiRuntimeSections } from "./_components/local-ai-sections/runtime-sections"; import { DEFAULT_LOCAL_AI_EXPANDED_SECTIONS, LOCAL_AI_EXPANDED_STORAGE_KEY, } from "./_lib/local-ai-sections"; import { ADMIN_SETTINGS_NAV_ITEMS } from "./_lib/settings-navigation"; import type { LocalAiOverviewItem, LocalAiPlanningSectionsProps, LocalAiQuickLink, } from "./_lib/types"; const fetcher = (url: string) => fetch(url).then((res) => res.json()); const DEFAULT_BFL_REALTIME_MODEL = "flux-2-klein-4b"; const DEFAULT_BFL_BASE_URL = "https://api.bfl.ai"; const HOME_CHAT_ANIMATION_MAX_BYTES = 1024 * 1024; const HOME_CHAT_ANIMATION_CONFIG_KEY = "home_chat_animation_config"; const VIDEO_PRICING_PREVIEW_RESOLUTIONS = ["480p", "720p", "1080p"] as const; const IMAGE_PRICING_PREVIEW_SIZES = ["1K", "2K", "4K"] as const; type AdminVideoResolution = (typeof VIDEO_PRICING_PREVIEW_RESOLUTIONS)[number]; const DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS: Record< AdminVideoResolution, number > = { "480p": 1, "720p": 1.5, "1080p": 2, }; function normalizeAdminVideoResolutions( value: unknown ): AdminVideoResolution[] { if (!Array.isArray(value)) return []; const allowed = new Set(VIDEO_PRICING_PREVIEW_RESOLUTIONS); return Array.from( new Set( value .map((item) => String(item || "") .trim() .toLowerCase() ) .filter((item): item is AdminVideoResolution => allowed.has(item)) ) ); } function getAdminVideoSupportedResolutions( capabilities: unknown ): AdminVideoResolution[] { if (!capabilities || typeof capabilities !== "object") { return [...VIDEO_PRICING_PREVIEW_RESOLUTIONS]; } const configured = normalizeAdminVideoResolutions( (capabilities as { supportedResolutions?: unknown }).supportedResolutions ); return configured.length ? configured : [...VIDEO_PRICING_PREVIEW_RESOLUTIONS]; } function updateAdminVideoSupportedResolutions( capabilities: Record | null | undefined, nextResolutions: AdminVideoResolution[] ) { return { ...(capabilities || {}), supportedResolutions: nextResolutions, }; } function formatCompactNumber(value: number, fractionDigits = 2) { if (!Number.isFinite(value)) return "0"; const fixed = value.toFixed(fractionDigits); return fixed.replace(/\.?0+$/, ""); } function normalizeAdminMultiplier(value: unknown, fallback = 1) { const parsedValue = Number(value); if (!Number.isFinite(parsedValue) || parsedValue <= 0) return fallback; return Math.min(20, Math.max(0.01, Number(parsedValue.toFixed(2)))); } function roundCreditCost(value: unknown) { const parsedValue = Number(value); return Number.isFinite(parsedValue) && parsedValue > 0 ? Math.max(1, Math.round(parsedValue)) : 0; } function createEmptyCanvasSkillTag(): CanvasSkillTag { return { id: `tag-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, name: "", prompt: "", placeholder: "", iconName: "message-circle", iconUrl: "", }; } function createEmptyCanvasSkillCategory(): CanvasSkillCategory { return { id: `category-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, name: "新分类", }; } function createEmptyEnhanceProPreset(): EnhanceProPreset { const timestamp = Date.now(); return { id: `enhance-pro-preset-${timestamp}-${Math.random().toString(36).slice(2, 6)}`, name: "新预设", description: "", isDefault: false, prompt: "", batchSize: 1, seed: 1337, outputFormat: ENHANCE_PRO_KREA_DEFAULT_OUTPUT_FORMAT, subjectDetection: "All", faceEnhancement: false, faceEnhancementCreativity: 0.5, faceEnhancementStrength: 0.5, cropToFill: false, model: ENHANCE_PRO_KREA_DEFAULT_MODEL, upscalingActivated: false, imageScalingFactor: 1, creativity: 3, texture: 3, sharpen: 0.5, denoise: 0.5, detail: 0.5, }; } type AdminDingTalkNotificationTaskStatus = | "pending" | "sending" | "sent" | "failed" | "cancelled"; type AdminDingTalkNotificationTask = { id: string; title: string; content: string; status: AdminDingTalkNotificationTaskStatus; scheduledAt: string; attempts: number; sentAt?: string | null; cancelledAt?: string | null; lastError?: string | null; createdAt: string; updatedAt: string; }; function formatDingTalkNotificationTaskStatus( status: AdminDingTalkNotificationTaskStatus ) { if (status === "pending") return "待发送"; if (status === "sending") return "发送中"; if (status === "sent") return "已发送"; if (status === "failed") return "发送失败"; if (status === "cancelled") return "已取消"; return status; } function getDingTalkNotificationTaskStatusClassName( status: AdminDingTalkNotificationTaskStatus ) { if (status === "sent") { return "border border-emerald-200 bg-emerald-50 text-emerald-700"; } if (status === "failed") { return "border border-red-200 bg-red-50 text-red-600"; } if (status === "cancelled") { return "border border-slate-200 bg-slate-100 text-slate-500"; } if (status === "sending") { return "border border-blue-200 bg-blue-50 text-blue-600"; } return "border border-amber-200 bg-amber-50 text-amber-700"; } function toDateTimeLocalInputValue(value: Date) { const offsetMs = value.getTimezoneOffset() * 60 * 1000; return new Date(value.getTime() - offsetMs).toISOString().slice(0, 16); } function loadLocalAiExpandedSections() { if (typeof window === "undefined") return DEFAULT_LOCAL_AI_EXPANDED_SECTIONS; try { const raw = window.localStorage.getItem(LOCAL_AI_EXPANDED_STORAGE_KEY); if (!raw) return DEFAULT_LOCAL_AI_EXPANDED_SECTIONS; const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return DEFAULT_LOCAL_AI_EXPANDED_SECTIONS; } return { ...DEFAULT_LOCAL_AI_EXPANDED_SECTIONS, ...(parsed as Record), }; } catch { return DEFAULT_LOCAL_AI_EXPANDED_SECTIONS; } } function maskAdminSecret(value: string) { const normalized = String(value || "").trim(); if (!normalized) return "未填写"; if (normalized.length <= 10) return "••••••"; return `${normalized.slice(0, 6)}••••${normalized.slice(-4)}`; } function reorderEnhanceProPresets( presets: EnhanceProPreset[], fromIndex: number, toIndex: number ) { if ( fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= presets.length || toIndex >= presets.length ) { return presets; } const nextPresets = [...presets]; const [movedPreset] = nextPresets.splice(fromIndex, 1); if (!movedPreset) { return presets; } nextPresets.splice(toIndex, 0, movedPreset); return nextPresets; } function getEnhanceProPresetSummary(preset: EnhanceProPreset) { const parts = [ preset.model, preset.subjectDetection === "Foreground" ? "前景" : preset.subjectDetection === "Background" ? "背景" : "全部", ]; if (preset.faceEnhancement) { parts.push("人像增强"); } if (preset.upscalingActivated) { parts.push(`${preset.imageScalingFactor}x`); } return parts.join(" · "); } // 类型定义 type User = { id: string; email: string; name: string | null; department: string | null; role: "guest" | "regular" | "admin" | "agent_admin"; isHighVip: boolean; isActive: boolean; monthlyImageLimit: number | null; pointsBalance: number; pointsGiftedTotal: number; createdAt: string; }; const ROLE_LABELS: Record = { guest: "访客用户", regular: "普通用户", admin: "管理员", agent_admin: "风格化管理员", }; type ToolConfig = { toolName: string; isEnabled: boolean; description?: string; config?: any; }; type AiProvider = { id: string; name: string; type: | "alibaba" | "openai" | "custom" | "local" | "gemini" | "bltcy-video" | "grsai-gpt-image" | "grsai-video" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "bfl-image" | "ruxa-image" | "fal-image" | "snapgen-image" | "openai-image" | "bananaimage" | "visionary" | "atlascloud" | "vip98k" | "stepfun-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "worldlabs-3d" | "kittycad-cad" | "runway" | "krea" | "volcano-multimodal"; baseUrl: string; apiKey: string; isEnabled: boolean; createdAt: string; }; // 🆕 图片生成模型特性配置 type ImageGenConfig = { 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; // 是否支持随机种子 supportedAspectRatios?: string[]; // 支持的图片比例 supportedImageSizes?: string[]; // 支持的分辨率/K 数 maxReferenceImages?: number; // 最大参考图数量 maxVideoReferences?: number; // 最大参考视频数量 supportsQuality?: boolean; // 是否支持质量档位 creditCostPerImage?: number; // 每张图片积分消耗 creditCostMultiplierBySize?: Record; // 各图片尺寸积分倍率 creditDiscountRate?: number; // 积分折扣,按“折”配置,如 5 = 5折 creditCostPerVideo?: number; // 每条视频积分消耗 videoDurationOptions?: number[]; // 视频秒数选项 creditCostPerVideoByDuration?: Record; // 各秒数积分 creditCostPerVideoByResolution?: Record; // 各分辨率积分 enableLoadingPlaceholder?: boolean; // 是否启用加载占位图 loadingColor?: string; // 占位图主题色,如 "purple", "blue", "green" vip98k?: Record; // VIP98K 目录元数据 }; function createDefaultImageGenConfig(): ImageGenConfig { return { maxImages: 2, supportsSize: false, supports1K: true, supports2K: true, supports4K: true, supportsPromptExtend: false, supportsNegativePrompt: false, supportsWatermark: false, supportsSeed: false, creditCostPerImage: 0, creditCostMultiplierBySize: { "1K": 1, "2K": 1, "4K": 1, }, creditCostPerVideo: 0, videoDurationOptions: [], creditCostPerVideoByDuration: {}, creditCostPerVideoByResolution: {}, enableLoadingPlaceholder: true, loadingColor: "purple", }; } function formatVideoDurationOptionsInput(options?: number[]) { return getConfiguredVideoDurationOptions({ videoDurationOptions: options, }).join(", "); } function parseVideoDurationOptionsInput(value: string) { return normalizeVideoDurationOptions( String(value || "") .split(/[,\n,、;;|/\\\s]+/) .map((item) => item.trim()) .filter(Boolean) ); } function syncVideoDurationPricing( config: ImageGenConfig | undefined, durationOptions: number[] ) { const nextConfig = config || createDefaultImageGenConfig(); return { ...nextConfig, videoDurationOptions: durationOptions, creditCostPerVideoByDuration: buildVideoDurationCostMap({ durationOptions, costMap: nextConfig.creditCostPerVideoByDuration, fallbackCost: nextConfig.creditCostPerVideo, }) || {}, }; } function applyRecommendedVideoPricing(config: ImageGenConfig | undefined) { return { ...(config || createDefaultImageGenConfig()), creditCostPerVideo: recommendedVideoResolutionBaseCosts["480p"], videoDurationOptions: [...recommendedVideoDurationOptions], creditCostPerVideoByDuration: { ...recommendedVideoDurationReferenceCosts, }, creditCostPerVideoByResolution: { ...recommendedVideoResolutionBaseCosts, }, }; } function getVideoReferenceDurationCost(config: ImageGenConfig | undefined) { const durationCostEntries = Object.entries( config?.creditCostPerVideoByDuration || {} ) .map(([durationKey, cost]) => ({ duration: Number(durationKey), cost: roundCreditCost(cost), })) .filter((item) => Number.isFinite(item.duration) && item.cost > 0) .sort((left, right) => left.duration - right.duration); return ( durationCostEntries[durationCostEntries.length - 1]?.cost || roundCreditCost(config?.creditCostPerVideo) || recommendedVideoDurationReferenceCosts[ String( recommendedVideoDurationOptions[ recommendedVideoDurationOptions.length - 1 ] ) ] || 1 ); } function getVideoResolutionMultiplier( config: ImageGenConfig | undefined, resolution: (typeof VIDEO_PRICING_PREVIEW_RESOLUTIONS)[number] ) { const referenceCost = getVideoReferenceDurationCost(config); const rawCost = config?.creditCostPerVideoByResolution?.[resolution] ?? referenceCost * DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS[resolution]; if (referenceCost <= 0) { return DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS[resolution]; } return normalizeAdminMultiplier( Number(rawCost) / referenceCost, DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS[resolution] ); } function getEnhanceProScaleMultiplier( pricing: PointsSystemConfig["enhanceProPricing"], factorKey: string, cost: number ) { const baseCost = roundCreditCost(pricing.fixedCost); if (baseCost <= 0) return normalizeAdminMultiplier(factorKey); return normalizeAdminMultiplier( Number(cost) / baseCost, normalizeAdminMultiplier(factorKey) ); } function applyRecommendedEnhanceProPricing( pricing: PointsSystemConfig["enhanceProPricing"] ): PointsSystemConfig["enhanceProPricing"] { const recommended = defaultPointsSystemConfig.enhanceProPricing; const baseCost = roundCreditCost(pricing.fixedCost || recommended.fixedCost); return { ...pricing, chargeMode: recommended.chargeMode, fixedCost: baseCost, chargeOnlyWhenUpscaling: recommended.chargeOnlyWhenUpscaling, batchSizeMultiplierEnabled: recommended.batchSizeMultiplierEnabled, scaleFactorCosts: Object.fromEntries( Object.keys(recommended.scaleFactorCosts).map((factorKey) => [ factorKey, roundCreditCost(baseCost * normalizeAdminMultiplier(factorKey)), ]) ), modelMultipliers: { ...recommended.modelMultipliers }, }; } type PointsCostField = | "creditCostPer3DModelPbr" | "creditCostPer3DModelSmartLowPoly" | "creditCostPer3DModelQuad" | "creditCostPer3DModelRefine" | "creditCostPer3DModelTexture" | "creditCostPer3DModelAutoRig" | "creditCostPer3DModelHighpolyToLowpoly" | "creditCostPerImageMultiView" | "creditCostPerImageRemoveBackground" | "creditCostPerImageBoxCutout" | "creditCostPerImageWatermarkRemove" | "creditCostPerImageExtend" | "creditCostPerImageRepaint" | "creditCostPerImageAngleEdit"; const THREE_D_GENERATION_POINT_FIELDS: Array<{ key: PointsCostField; label: string; description: string; }> = [ { key: "creditCostPer3DModelPbr", label: "PBR 材质", description: "勾选 PBR 材质时,在 3D 模型基础积分上额外扣除。", }, { key: "creditCostPer3DModelSmartLowPoly", label: "智能低模", description: "勾选智能低模时额外扣除。", }, { key: "creditCostPer3DModelQuad", label: "四边面优化", description: "勾选四边面优化时额外扣除。", }, ]; const THREE_D_EDIT_POINT_FIELDS: Array<{ key: PointsCostField; label: string; description: string; }> = [ { key: "creditCostPer3DModelRefine", label: "模型精修", description: "提交 refine_model 精修任务时扣除。", }, { key: "creditCostPer3DModelTexture", label: "纹理重绘", description: "提交 texture_model 纹理重绘任务时扣除。", }, { key: "creditCostPer3DModelAutoRig", label: "自动绑定", description: "提交 animate_rig 自动绑定任务时扣除。", }, { key: "creditCostPer3DModelHighpolyToLowpoly", label: "模型减面", description: "提交 highpoly_to_lowpoly 减面任务时扣除。", }, ]; const IMAGE_TOOL_POINT_FIELDS: Array<{ key: PointsCostField; label: string; description: string; }> = [ { key: "creditCostPerImageMultiView", label: "多视角", description: "角色三视图 / 多机位九宫格等多视角图像生成时扣除。", }, { key: "creditCostPerImageRemoveBackground", label: "抠图", description: "调用图片抠图功能时扣除。", }, { key: "creditCostPerImageBoxCutout", label: "框选抠图", description: "调用框选抠图功能时扣除。", }, { key: "creditCostPerImageWatermarkRemove", label: "去水印", description: "调用去水印功能并成功创建任务后扣除。", }, { key: "creditCostPerImageExtend", label: "扩图", description: "调用图片拓展 / 扩图功能时扣除。", }, { key: "creditCostPerImageRepaint", label: "重绘", description: "调用图片提示词重绘功能时扣除。", }, { key: "creditCostPerImageAngleEdit", label: "多角度", description: "调用 fal.ai 多角度 / 角度编辑功能时扣除。", }, ]; type AiModel = { id: string; providerId: string; modelId: string; displayName: string; avatarUrl?: string; description?: string; supportsVision: boolean; supportsImageGen: boolean; isEnabled: boolean; isHidden: boolean; isDefault: boolean; isDefaultTextEditModel?: boolean; isSubModel?: boolean; parentModelId?: string | null; sortOrder: number; fallbackModelId1?: string; fallbackModelId2?: string; excludeFromFailover?: boolean; excludeFromHealthBan?: boolean; healthStatus?: "healthy" | "degraded" | "auto_disabled"; healthConsecutiveFailures?: number; healthLastEventType?: string | null; healthLastError?: string | null; healthLastFailureAt?: string | null; healthLastSuccessAt?: string | null; healthAutoDisabledAt?: string | null; healthAutoDisabledReason?: string | null; healthRecoverySuccessStreak?: number; healthObservationStartedAt?: string | null; isAgentModel?: boolean; agentCodingModelId?: string; isMultimodal?: boolean; capabilities?: Record | null; imageGenConfig?: ImageGenConfig; // 🆕 图片生成特性配置 }; type AiModelHealthEvent = { id: string; modelId?: string | null; providerId?: string | null; modelRuntimeId?: string | null; modelDisplayName?: string | null; providerType?: string | null; providerName?: string | null; eventType: string; severity: string; context?: string | null; counted: boolean; consecutiveFailures: number; autoDisabled: boolean; elapsedMs?: number | null; requestId?: string | null; errorMessage?: string | null; createdAt: string; }; type StorageConfig = { id: string; name: string; type: "s3" | "local" | "aliyun-oss" | "qiniu-kodo"; endpoint?: string; region?: string; bucket?: string; accessKeyId?: string; secretAccessKey?: string; publicUrl?: string; isDefault: boolean; isEnabled: boolean; }; type RealtimeCanvasModeIconConfig = { realtimeIconUrl: string; enhanceIconUrl: string; realtimeLabel: string; enhanceLabel: string; }; type RealtimeCanvasModeIconKey = "realtimeIconUrl" | "enhanceIconUrl"; const CANVAS_TOOLBAR_FALLBACK_ICONS: Record = { selectTool: MousePointer, panTool: Hand, saveCanvas: Save, cloudFiles: FolderOpen, markerPanel: Tag, workflowConnection: GitBranch, styleMode: Wand2, stylePro: Sparkles, batchProcessing: Layers, addImage: ImagePlus, addAgentImageSlot: ImagePlus, addImageInput: ImagePlus, addText: Type, addScriptNode: Type, addVideoInput: Video, workflowNodeLibrary: Grid3X3, realtimeCanvas: PenTool, enhanceCanvas: Sparkles, model3d: Box, exportVideoCanvas: Download, importVideoCanvas: Upload, newCanvas: Plus, newVideoCanvas: Plus, undo: Undo2, redo: Redo2, zoomOut: ZoomOut, resetZoom: RefreshCw, zoomIn: ZoomIn, downloadFolder: Download, imageNodeMore: MoreVertical, imageAddToChat: MessageSquarePlus, imagePickObject: Crosshair, imageEnhance: Sparkles, imageRemoveBackground: Scissors, imageBoxCutout: Scan, imageLocalEdit: Wand2, imageRemoveWatermark: Eraser, imageResize: Maximize2, imageTextEdit: Type, imageMultiAngle: Box, imageAdjust: SlidersHorizontal, imageFlipRotate: FlipHorizontal2, imageGenerate3dModel: Box, imageGenerate3dWorld: Globe2, imageLayerSplit: Layers, imageExtend: Expand, imageCrop: Crop, imagePromptRegenerate: RefreshCw, imageNote: StickyNote, imageRename: Edit, imageDownload: Download, imageDelete: Trash2, imageCompare: Scan, imageConfirmCutout: CheckCircle2, imageCancelCutout: X, textDelete: Trash2, uiScreenReview: Wand2, uiScreenDelete: Trash2, uiScreenSaveText: Check, videoUpload: Video, videoPlay: Play, videoPause: Pause, videoMute: Volume2, videoUnmute: VolumeX, videoMore: MoreVertical, videoDownload: Download, videoDelete: Trash2, workflowDelete: Trash2, model3dOpen3d: ExternalLink, model3dOpenWorld: Globe2, model3dMore: MoreVertical, model3dOpenMesh: Box, model3dOpenImage: ImageIcon, model3dDownload: Download, model3dCopy: Copy, model3dDelete: Trash2, world3dViewSplat: Sparkles, world3dViewImage: ImageIcon, world3dViewMesh: Box, world3dFirstPerson: Waypoints, world3dHotspot: Crosshair, world3dMore: MoreHorizontal, world3dOpenMesh: Box, world3dOpenImage: ImageIcon, world3dDownload: Download, world3dCopy: Copy, world3dDelete: Trash2, realtimeReturnCanvas: ArrowLeft, realtimeModeRealtime: Paintbrush, realtimeModeEnhance: Wand2, realtimeLayerPanel: Layers, realtimeLayerSplitImport: Layers, realtimeAddImageLayer: Upload, realtimeDownloadPreview: Download, realtimeSelectTool: MousePointer, realtimeBrushTool: Paintbrush, realtimeEraserTool: Eraser, realtimeShapeTool: Square, realtimeSaveProject: Save, realtimeProjectList: FolderOpen, realtimeNewBlank: Plus, realtimeGenerate: Sparkles, realtimeSendToCanvas: ArrowUp, chatModeAgentActive: Sparkles, chatModePresetAgent: Bot, chatModeAgentMenu: Sparkles, chatModeDetailActive: Grid3X3, chatModeDetailMenu: Grid3X3, chatModeImageActive: ImagePlus, chatModeImageMenu: ImagePlus, chatModeVideoActive: Video, chatModeVideoMenu: Video, chatAttach: Paperclip, chatDeepThinking: Brain, chatModelSelector: Bot, chatPoints: Sparkles, chatSend: ArrowUp, chatStartGeneration: Sparkles, }; const LOVART_HOME_SIDEBAR_FALLBACK_ICONS: Record< LovartHomeSidebarItemKey, LucideIcon > = { newProject: Plus, home: Home, projects: FolderOpen, profile: User, gallery: ImageIcon, tutorial: Play, imageEditor: Edit, aiCad: Box, model3d: Box, scene: Layers3, agent: Wand2, videoCanvas: Video, flowBoard: GitBranch, productPlanning: BarChart3, toolbox1: Wrench, toolbox2: Wrench, settings: Settings, }; type SiteSettings = { siteName: string; siteLogo: string | null; siteLogoDark: string | null; favicon: string | null; showAutoModel: boolean; usageTimeEnabled: boolean; usageTimeStart: string; usageTimeEnd: string; usageTimeMessage: string; defaultTheme: "light" | "dark" | "system"; bannerEnabled: boolean; bannerText: string; bannerLink: string | null; bannerActionText: string; toolbox1Name: string; toolbox1Enabled: boolean; toolbox2Name: string; toolbox2Enabled: boolean; tutorialVideoUrl: string | null; tutorialEnabled: boolean; rankingEnabled: boolean; productPlanningEnabled: boolean; canvasEnabled: boolean; agentEnabled: boolean; canvasAgentModeEnabled: boolean; canvasAgentFeatureSettings: CanvasAgentFeatureSettings; videoCanvasEnabled: boolean; videoCanvasSidebarEnabled: boolean; agentCategoryTreeConfig: { productCategories: string[]; audienceTags: string[]; usageScenarios: string[]; brands: string[]; }; agentSecondaryTypeConfig: { image: string[]; video: string[]; text: string[]; }; agentWorkflowUiConfig: AgentWorkflowUiConfig; realtimeCanvasEnabled: boolean; realtimeCanvasModeIconConfig: RealtimeCanvasModeIconConfig; canvasToolbarConfig: CanvasToolbarConfig; lovartHomeSidebarConfig: LovartHomeSidebarConfig; aiDetailGenerationEnabled: boolean; canvasDefaultModelId: string | null; canvasAutoModeDefaultModelId: string | null; agentWorkflowDefaultModelId: string | null; canvasDetailDefaultModelId: string | null; canvasDetailAgentModelId: string | null; lovartComposerPrefix: string; lovartComposerPreviewTexts: string[]; lovartPromptTagVisibility: LovartPromptTagVisibility; contactConfig: ContactConfig; storageFailoverConfig: StorageFailoverConfig; ossImageMigrationConfig: OssImageMigrationConfig; pointsSystemConfig: PointsSystemConfig; pointsPackages: PointsPackage[]; dulupayConfig: DulupayConfig; lovartPromoText: string; lovartPromoActionText: string; lovartHeroTitle: string; lovartHeroSubtitle: string; surpriseCategories: string[]; membershipConfig: ReturnType; membershipUserOverrides: ReturnType; }; type LovartPromptTagVisibility = { nanoBananaPro: boolean; design: boolean; branding: boolean; illustration: boolean; ecommerce: boolean; video: boolean; }; type ContactConfig = { phone: string; wechatQrUrl: string; }; type StorageFailoverConfig = { enabled: boolean; backupStorageConfigId: string; }; type OssImageMigrationConfig = { enabled: boolean; preferAliyunForUploads: boolean; sourceStorageConfigId: string; targetStorageConfigId: string; migrateAfterHours: number; cleanupAfterHours: number; batchSize: number; }; type HomeAnimationConfig = { enabled: boolean; fileName: string; htmlContent: string; updatedAt: string | null; }; const defaultLovartPromptTagVisibility: LovartPromptTagVisibility = { nanoBananaPro: true, design: true, branding: true, illustration: true, ecommerce: true, video: true, }; function stableStringifyComparableValue(value: unknown): string { if (value === null || typeof value !== "object") { return JSON.stringify(value) ?? "null"; } if (Array.isArray(value)) { return `[${value.map((item) => stableStringifyComparableValue(item)).join(",")}]`; } const entries = Object.entries(value as Record) .filter(([, item]) => item !== undefined) .sort(([left], [right]) => left.localeCompare(right)); return `{${entries .map( ([key, item]) => `${JSON.stringify(key)}:${stableStringifyComparableValue(item)}` ) .join(",")}}`; } function areComparableValuesEqual(left: unknown, right: unknown) { return ( stableStringifyComparableValue(left) === stableStringifyComparableValue(right) ); } function pickChangedTopLevelFields>( current: T, baseline: Partial | null | undefined ) { if (!baseline) return { ...current }; const changed: Partial = {}; for (const [key, value] of Object.entries(current) as Array< [keyof T, T[keyof T]] >) { if (!areComparableValuesEqual(value, baseline[key])) { changed[key] = value; } } return changed; } const defaultContactConfig: ContactConfig = { phone: "", wechatQrUrl: "", }; const defaultRealtimeCanvasModeIconConfig: RealtimeCanvasModeIconConfig = { realtimeIconUrl: "", enhanceIconUrl: "", realtimeLabel: "实时画布模式", enhanceLabel: "图像增强模式", }; function normalizeRealtimeCanvasModeIconConfig( value: unknown ): RealtimeCanvasModeIconConfig { const raw = value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; const normalizeLabel = (input: unknown, fallback: string) => { const label = String(input || "") .trim() .slice(0, 32); return label || fallback; }; return { realtimeIconUrl: String(raw.realtimeIconUrl || "").trim(), enhanceIconUrl: String(raw.enhanceIconUrl || "").trim(), realtimeLabel: normalizeLabel(raw.realtimeLabel, "实时画布模式"), enhanceLabel: normalizeLabel(raw.enhanceLabel, "图像增强模式"), }; } const defaultStorageFailoverConfig: StorageFailoverConfig = { enabled: false, backupStorageConfigId: "", }; const defaultOssImageMigrationConfig: OssImageMigrationConfig = { enabled: true, preferAliyunForUploads: true, sourceStorageConfigId: "", targetStorageConfigId: "", migrateAfterHours: 48, cleanupAfterHours: 72, batchSize: 20, }; const lovartPromptTagOptions: Array<{ key: keyof LovartPromptTagVisibility; label: string; }> = [ { key: "nanoBananaPro", label: "Nano Banana Pro" }, { key: "design", label: "Design" }, { key: "branding", label: "Branding" }, { key: "illustration", label: "Illustration" }, { key: "ecommerce", label: "E-Commerce" }, { key: "video", label: "Video" }, ]; type NavLink = { id: string; name: string; url: string; order: number; isEnabled: boolean; toolboxGroup: number; // 1 或 2 }; type TutorialVideo = { id: string; title: string; videoUrl: string; coverUrl: string | null; description: string | null; order: number; isEnabled: boolean; }; type DingTalkOrganization = { id: string; name: string; appKey: string; appSecret: string; }; type AuthSettings = { // 保留单一配置用于兼容旧数据(后端会自动同步为第一个组织) dingTalkAppKey?: string | null; dingTalkAppSecret?: string | null; googleClientId?: string | null; googleClientSecret?: string | null; authUrl?: string | null; isDingTalkEnabled: boolean; isGoogleEnabled: boolean; isRegistrationEnabled?: boolean; isEmailVerificationEnabled?: boolean; emailVerificationExpiresMinutes?: number; resendApiKey?: string | null; resendFromEmail?: string | null; isEmailServiceConfigured?: boolean; dingTalkOrganizations: DingTalkOrganization[]; }; type GeneratedImageRecord = { id: string; sourceMessageId?: string; userId: string; userName: string | null; userEmail: string; userDepartment: string | null; thumbnailUrl: string; originalUrl: string; videoUrl?: string | null; recordType?: "image" | "video"; imageSize: string | null; prompt: string | null; modelId: string | null; isFeatured: boolean; featuredCategory: string | null; featuredAt: string | null; createdAt: string; chatId: string | null; }; type ImageAnalytics = { overview: { totalImages: number; totalUsers: number; todayImages: number; todayUsers: number; weekImages: number; weekUsers: number; monthImages: number; monthUsers: number; }; userStats: Array<{ userId: string; userName: string | null; userEmail: string; imageCount: number; }>; dailyStats: Array<{ date: string; count: number }>; sizeStats: Array<{ imageSize: string | null; count: number }>; modelStats: Array<{ modelId: string | null; count: number }>; modelDetailStats: Array<{ modelId: string | null; count: number; userCount: number; modelName: string | null; providerName: string | null; providerType: string | null; }>; providerTypeStats: Array<{ providerType: string | null; providerName: string | null; count: number; }>; modelTrendStats: Array<{ date: string; modelId: string | null; modelName: string | null; providerName: string | null; providerType: string | null; count: number; }>; monthlyStats: Array<{ month: string; count: number }>; hourlyStats: Array<{ hour: number; count: number }>; }; type RuntimeModelMeta = { modelName: string | null; providerName: string | null; providerType: string | null; }; type RuntimeModelCandidate = RuntimeModelMeta & { modelId: string | null; isEnabled?: boolean; isHidden?: boolean; isDefault?: boolean; isSubModel?: boolean; sortOrder?: number | null; }; const analyticsNumberFormatter = new Intl.NumberFormat("zh-CN"); const UUID_LIKE_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; function scoreRuntimeModelCandidate(candidate: RuntimeModelCandidate) { return ( (candidate.isEnabled ? 100 : 0) + (!candidate.isHidden ? 20 : 0) + (!candidate.isSubModel ? 10 : 0) + (candidate.isDefault ? 8 : 0) + Math.max(0, 1000 - Number(candidate.sortOrder ?? 999)) ); } function buildRuntimeModelMetaMap(candidates: RuntimeModelCandidate[]) { const map = new Map(); candidates.forEach((candidate) => { const modelId = String(candidate.modelId || "").trim(); if (!modelId) return; const score = scoreRuntimeModelCandidate(candidate); const current = map.get(modelId); if (current && current.score >= score) return; map.set(modelId, { score, modelName: candidate.modelName || null, providerName: candidate.providerName || null, providerType: candidate.providerType || null, }); }); return map; } function resolveRuntimeModelMeta( runtimeModelMetaById: Map, modelId: string | null ): RuntimeModelMeta { if (!modelId) { return { modelName: null, providerName: null, providerType: null, }; } const resolved = runtimeModelMetaById.get(modelId); return { modelName: resolved?.modelName || null, providerName: resolved?.providerName || null, providerType: resolved?.providerType || null, }; } function formatAnalyticsNumber(value: number | null | undefined) { return analyticsNumberFormatter.format(value || 0); } function formatAnalyticsRatio(count: number, total: number) { if (!total) return "0%"; return `${((count / total) * 100).toFixed(1)}%`; } function formatAnalyticsMonthLabel(value: string) { const [year, month] = String(value || "").split("-"); if (!year || !month) return value || "本月"; return `${year} 年 ${month} 月`; } function formatAnalyticsModelLabel( meta: RuntimeModelMeta | null | undefined, modelId: string | null | undefined ) { if (meta?.modelName) return meta.modelName; const normalizedModelId = String(modelId || "").trim(); if (!normalizedModelId) return "未知模型"; if (UUID_LIKE_REGEX.test(normalizedModelId)) return "未映射模型"; return normalizedModelId; } const AVAILABLE_TOOLS = [ { name: "getWeather", description: "获取天气信息" }, { name: "createDocument", description: "创建文档" }, { name: "updateDocument", description: "更新文档" }, { name: "requestSuggestions", description: "请求文档建议" }, ]; const PROVIDER_TYPES = [ { value: "alibaba", label: "阿里云通义千问" }, { value: "alibaba-image", label: "阿里生图模型" }, // 🆕 千问图片生成/编辑 { value: "bfl-image", label: "BFL FLUX.2 图片编辑" }, { value: "ruxa-image", label: "RUXA生图" }, { value: "fal-image", label: "FAL 图片生成/编辑" }, { value: "snapgen-image", label: "SnapGen 生图" }, { value: "openai-image", label: "OpenAI 图片兼容" }, { value: "bananaimage", label: "BananaImage 生图" }, { value: "visionary", label: "Visionary 生图" }, { value: "atlascloud", label: "Atlas Cloud 图片 / 视频 / 3D 模型" }, { value: "vip98k", label: "VIP98K 混合模型服务" }, { value: "stepfun-image", label: "阶跃星辰 Step Image Edit" }, { value: "bltcy-video", label: "柏拉图统一视频" }, { value: "grsai-gpt-image", label: "Grsai GPT Image" }, { value: "grsai-video", label: "Grsai 视频模型" }, { value: "midjourney", label: "Midjourney" }, { value: "tripo-3d", label: "Tripo3D 模型生成" }, { value: "worldlabs-3d", label: "World Labs 3D 世界生成" }, { value: "kittycad-cad", label: "KittyCAD / Zoo CAD" }, { value: "runway", label: "Runway API" }, { value: "krea", label: "Krea API" }, { value: "zhipu", label: "智普官方 4.7" }, { value: "volcano-multimodal", label: "火山引擎多模态" }, { value: "openai", label: "OpenAI 兼容" }, { value: "custom", label: "自定义 OpenAI 兼容" }, { value: "local", label: "本地模型" }, { value: "gemini", label: "Gemini中转" }, { value: "huanke-gemini", label: "环科Gemini生图" }, // 🆕 环科 Gemini 原生格式 API { value: "qianwen-video", label: "千问视频生成" }, { value: "doubao-video", label: "豆包AI视频" }, { value: "ruxa-video", label: "RUXA 视频生成" }, { value: "volcano-vision", label: "火山引擎视觉" }, { value: "volcano-video", label: "火山引擎视频" }, { value: "fal-video", label: "FAL 视频生成" }, ]; const PROVIDER_DEFAULT_BASE_URL: Record = { alibaba: "https://dashscope.aliyuncs.com/compatible-mode/v1", "alibaba-image": "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation", "bfl-image": "https://api.bfl.ai", "ruxa-image": "https://api.ruxa.ai", "fal-image": "https://fal.run", "snapgen-image": "https://api.snapgen.ai", "openai-image": "https://api.openai.com/v1", bananaimage: "https://bananaimage.aigenmedia.art", visionary: "https://api.visionary.beer", atlascloud: "https://api.atlascloud.ai", vip98k: "https://api.vip98k.com", "stepfun-image": "https://api.stepfun.com", "bltcy-video": "https://api.bltcy.ai", "grsai-gpt-image": "https://grsai.dakka.com.cn", "grsai-video": "https://grsai.dakka.com.cn", midjourney: "https://your-midjourney-api.example.com", "fal-video": "https://fal.run", "tripo-3d": "https://api.tripo3d.ai/v2/openapi", "worldlabs-3d": "https://api.vip98k.com", "kittycad-cad": "https://api.zoo.dev", runway: "https://api.dev.runwayml.com", krea: "https://api.krea.ai", zhipu: "https://open.bigmodel.cn/api/paas/v4", "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", "doubao-video": "https://ark.cn-beijing.volces.com/api/v3", "ruxa-video": "https://api.ruxa.ai", "volcano-vision": "https://ark.cn-beijing.volces.com", "volcano-video": "https://ark.cn-beijing.volces.com/api/v3", }; const LEMON_OPENAI_BASE_URL = "https://new.lemonapi.site/v1"; const HTTP_URL_REGEX = /^https?:\/\//i; const TRAILING_SLASHES_REGEX = /\/+$/; const TRAILING_V1_REGEX = /\/v1$/i; const BLTCY_IMAGE_NORMALIZED_BASE_URL = BLTCY_IMAGE_OPENAI_BASE_URL.replace( TRAILING_SLASHES_REGEX, "" ); const BLTCY_IMAGE_BASE_URL_WITHOUT_V1 = BLTCY_IMAGE_OPENAI_BASE_URL.replace( TRAILING_V1_REGEX, "" ); const BLTCY_BFL_NORMALIZED_BASE_URL = BLTCY_BFL_BASE_URL.replace( TRAILING_SLASHES_REGEX, "" ); const LEMON_PROVIDER_PRESET = { name: "LemonAPI Gemini", type: "openai" as const, baseUrl: LEMON_OPENAI_BASE_URL, }; const LEMON_MODEL_PRESETS = [ { modelId: "[L]gemini-3.1-pro-preview", displayName: "Gemini-3.1-pro-preview", supportsVision: true, supportsImageGen: false, isMultimodal: true, description: "LemonAPI OpenAI兼容 Gemini 3.1 Pro 预设模型", }, ]; function getProviderDefaultBaseUrl(type?: string) { if (!type) return ""; return PROVIDER_DEFAULT_BASE_URL[type] || ""; } function isHttpUrlLike(value: string) { return HTTP_URL_REGEX.test(value.trim()); } function isLemonApiBaseUrl(value?: string | null) { const normalized = String(value || "") .trim() .replace(TRAILING_SLASHES_REGEX, ""); return normalized === LEMON_OPENAI_BASE_URL; } function isBltcyImageBaseUrl(value?: string | null) { const normalized = String(value || "") .trim() .replace(TRAILING_SLASHES_REGEX, ""); return ( normalized === BLTCY_IMAGE_NORMALIZED_BASE_URL || normalized === BLTCY_IMAGE_BASE_URL_WITHOUT_V1 ); } function isBltcyBflBaseUrl(value?: string | null) { const normalized = String(value || "") .trim() .replace(TRAILING_SLASHES_REGEX, ""); return ( normalized === BLTCY_BFL_NORMALIZED_BASE_URL || normalized === `${BLTCY_BFL_NORMALIZED_BASE_URL}/bfl` || normalized === `${BLTCY_BFL_NORMALIZED_BASE_URL}/bfl/v1` ); } function isMoonshotOfficialBaseUrl(value?: string | null) { return isDetailAgentLocalMoonshotBaseUrl(value); } export function AdminSettingsClient() { const [activeTab, setActiveTab] = useState("users"); useEffect(() => { const requestedTab = new URLSearchParams(window.location.search).get("tab"); if ( requestedTab && ADMIN_SETTINGS_NAV_ITEMS.some( (item) => item.value === requestedTab && !item.href ) ) { setActiveTab(requestedTab); } }, []); const handleActiveTabChange = (value: string) => { setActiveTab(value); const url = new URL(window.location.href); url.searchParams.set("tab", value); window.history.replaceState(null, "", url); }; // 用户管理状态 const [userSearch, setUserSearch] = useState(""); const [userPage, setUserPage] = useState(1); const [userLimit] = useState(10); 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 [batchLimitValue, setBatchLimitValue] = useState(""); const [batchLimitDepartment, setBatchLimitDepartment] = useState(""); // 🆕 生图记录状态 const [imageSearch, setImageSearch] = useState(""); const [imagePage, setImagePage] = useState(1); const [imageStartDate, setImageStartDate] = useState(""); const [imageEndDate, setImageEndDate] = useState(""); const [selectedImage, setSelectedImage] = useState(null); // 🆕 数据分析状态 const [selectedMonth, setSelectedMonth] = useState(() => { const now = new Date(); 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); // API 数据获取 const { data: usersData, mutate: mutateUsers } = useSWR<{ users: User[]; total: number; page: number; totalPages: number; }>( `/api/admin/users?page=${userPage}&limit=${userLimit}&search=${encodeURIComponent(userSearch)}`, fetcher ); // 获取部门列表(用于批量设置) const { data: departmentsData } = useSWR<{ departments: string[] }>( "/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: modelHealthData, mutate: mutateModelHealth } = useSWR<{ events: AiModelHealthEvent[]; }>("/api/admin/ai-model-health?limit=120", 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 ); 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 ); const { data: worldlabsKeyPoolConfigData, mutate: mutateWorldlabsKeyPoolConfig, } = useSWR<{ setting?: { value?: unknown } }>( `/api/admin/settings?key=${WORLDLABS_KEY_POOL_CONFIG_KEY}`, fetcher ); const { data: dingTalkRobotNotificationConfigData, mutate: mutateDingTalkRobotNotificationConfig, } = useSWR<{ setting?: { value?: unknown } }>( `/api/admin/settings?key=${DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY}`, fetcher ); const { data: dingTalkNotificationDraftData, mutate: mutateDingTalkNotificationDraft, } = useSWR<{ setting?: { value?: unknown } }>( `/api/admin/settings?key=${DINGTALK_NOTIFICATION_DRAFT_CONFIG_KEY}`, fetcher ); const { data: dingTalkNotificationTasksData, mutate: mutateDingTalkNotificationTasks, } = useSWR<{ tasks?: AdminDingTalkNotificationTask[] }>( "/api/admin/dingtalk-robot/notifications?limit=30", fetcher ); const { data: threeDModelAliasConfigData, mutate: mutateThreeDModelAliasConfig, } = useSWR<{ setting?: { value?: unknown } }>( `/api/admin/settings?key=${THREED_MODEL_ALIAS_CONFIG_SETTING_KEY}`, fetcher ); const { data: enhanceProConfigData, mutate: mutateEnhanceProConfig } = useSWR<{ setting?: { value?: unknown } }>( `/api/admin/settings?key=${ENHANCE_PRO_KREA_SETTING_KEY}`, fetcher ); const { data: aiWashConfigData, mutate: mutateAiWashConfig } = useSWR<{ setting?: { value?: unknown }; }>(`/api/admin/settings?key=${AI_WASH_CONFIG_KEY}`, fetcher); const { data: canvasLocalEditConfigData, mutate: mutateCanvasLocalEditConfig, } = useSWR<{ setting?: { value?: unknown } }>( `/api/admin/settings?key=${CANVAS_LOCAL_EDIT_CONFIG_KEY}`, fetcher ); const { data: canvasImageTaskQueueConfigData, mutate: mutateCanvasImageTaskQueueConfig, } = useSWR<{ setting?: { value?: unknown } }>( `/api/admin/settings?key=${CANVAS_IMAGE_TASK_QUEUE_CONFIG_KEY}`, fetcher ); const { data: homeAnimationSettingData, mutate: mutateHomeAnimationSetting } = useSWR<{ setting?: { value?: unknown } }>( `/api/admin/settings?key=${HOME_CHAT_ANIMATION_CONFIG_KEY}`, fetcher ); const { data: openClawChatSettingData, mutate: mutateOpenClawChatSetting } = useSWR<{ setting?: { value?: unknown } }>( `/api/admin/settings?key=${OPENCLAW_CHAT_CONFIG_KEY}`, fetcher ); // 🆕 生图记录数据 const imageQueryParams = new URLSearchParams({ page: imagePage.toString(), pageSize: "200", ...(imageSearch && { search: imageSearch }), ...(imageStartDate && { startDate: imageStartDate }), ...(imageEndDate && { endDate: imageEndDate }), }); const { data: imagesData, mutate: mutateImages } = useSWR<{ images: GeneratedImageRecord[]; pagination: { page: number; pageSize: number; total: number; totalPages: number; totalImages?: number; totalVideos?: number; }; }>(`/api/admin/images?${imageQueryParams}`, fetcher); // 🆕 生图数据分析 const analyticsUrl = activeTab === "analytics" ? `/api/admin/images/analytics?month=${selectedMonth}${selectedDate ? `&date=${selectedDate}` : ""}` : null; const { data: analyticsData } = useSWR(analyticsUrl, fetcher); const runtimeModelMetaById = useMemo(() => { const providerMetaById = new Map< string, { providerName: string | null; providerType: string | null } >(); (providersData?.providers || []).forEach((provider) => { providerMetaById.set(provider.id, { providerName: provider.name, providerType: provider.type, }); }); const candidates: RuntimeModelCandidate[] = []; (modelsData?.models || []).forEach((model) => { const providerMeta = providerMetaById.get(model.providerId); const sharedMeta = { modelId: model.modelId, modelName: model.displayName, providerName: providerMeta?.providerName || null, providerType: providerMeta?.providerType || null, isEnabled: model.isEnabled, isHidden: model.isHidden, isDefault: model.isDefault, isSubModel: model.isSubModel, sortOrder: model.sortOrder, }; candidates.push(sharedMeta); candidates.push({ ...sharedMeta, modelId: model.id, }); }); (analyticsData?.modelDetailStats || []).forEach((stat) => { candidates.push({ modelId: stat.modelId, modelName: stat.modelName, providerName: stat.providerName, providerType: stat.providerType, }); }); (analyticsData?.modelTrendStats || []).forEach((stat) => { candidates.push({ modelId: stat.modelId, modelName: stat.modelName, providerName: stat.providerName, providerType: stat.providerType, }); }); return buildRuntimeModelMetaMap(candidates); }, [ analyticsData?.modelDetailStats, analyticsData?.modelTrendStats, modelsData?.models, providersData?.providers, ]); const analyticsOverviewCards = useMemo(() => { const currentMonth = `${new Date().getFullYear()}-${String( new Date().getMonth() + 1 ).padStart(2, "0")}`; return [ { key: "total-images", label: "总生成量", value: formatAnalyticsNumber(analyticsData?.overview.totalImages), note: "累计图片资产", icon: ImageIcon, tone: "from-zinc-950 via-zinc-900 to-zinc-800 text-white", }, { key: "total-users", label: "活跃用户", value: formatAnalyticsNumber(analyticsData?.overview.totalUsers), note: "参与生成的用户数", icon: Users, tone: "from-emerald-500/15 via-emerald-500/5 to-transparent text-emerald-700 dark:text-emerald-200", }, { key: "today-images", label: "今日生成", value: formatAnalyticsNumber(analyticsData?.overview.todayImages), note: `${formatAnalyticsNumber(analyticsData?.overview.todayUsers)} 位用户`, icon: TrendingUp, tone: "from-amber-500/20 via-amber-500/5 to-transparent text-amber-700 dark:text-amber-200", }, { key: "month-images", label: selectedMonth === currentMonth ? "本月生成" : formatAnalyticsMonthLabel(selectedMonth), value: formatAnalyticsNumber(analyticsData?.overview.monthImages), note: `${formatAnalyticsNumber(analyticsData?.overview.monthUsers)} 位用户`, icon: Calendar, tone: "from-lime-400/25 via-lime-300/10 to-transparent text-lime-700 dark:text-lime-200", }, ]; }, [analyticsData?.overview, selectedMonth]); const analyticsPeakHour = useMemo(() => { return (analyticsData?.hourlyStats || []).reduce<{ hour: number; count: number; } | null>((best, stat) => { if (!best || stat.count > best.count) { return { hour: stat.hour, count: stat.count }; } return best; }, null); }, [analyticsData?.hourlyStats]); const analyticsRecentMonths = useMemo( () => analyticsData?.monthlyStats?.slice(-6) || [], [analyticsData?.monthlyStats] ); const analyticsTrendDays = useMemo(() => { const grouped = new Map< string, { date: string; total: number; models: ImageAnalytics["modelTrendStats"]; } >(); (analyticsData?.modelTrendStats || []).forEach((stat) => { const current = grouped.get(stat.date) || { date: stat.date, total: 0, models: [], }; current.total += stat.count; current.models.push(stat); grouped.set(stat.date, current); }); return Array.from(grouped.values()) .sort((left, right) => left.date.localeCompare(right.date)) .slice(-7) .map((item) => ({ ...item, models: [...item.models].sort((left, right) => right.count - left.count), })); }, [analyticsData?.modelTrendStats]); const analyticsModelTotal = useMemo( () => (analyticsData?.modelDetailStats || []).reduce( (sum, stat) => sum + stat.count, 0 ), [analyticsData?.modelDetailStats] ); const detailAgentModelOptions = useMemo( () => (modelsData?.models || []).filter((model) => { if (model.supportsImageGen || !model.isEnabled) return false; const provider = (providersData?.providers || []).find( (item) => item.id === model.providerId ); const descriptor = { providerType: provider?.type, modelId: model.modelId, capabilities: (model as any).capabilities, supportsImageGen: model.supportsImageGen, }; return ( !is3DModelDescriptor(descriptor) && !isVideoModelDescriptor(descriptor) ); }), [modelsData?.models, providersData?.providers] ); const imageGenerationModelOptions = useMemo( () => (modelsData?.models || []).filter( (model) => model.supportsImageGen && model.isEnabled && !model.isSubModel ), [modelsData?.models] ); const modelProviderNameById = useMemo(() => { const map = new Map(); (providersData?.providers || []).forEach((provider) => { map.set(provider.id, provider.name); }); return map; }, [providersData?.providers]); const modelHealthEventsByModelId = useMemo(() => { const map = new Map(); (modelHealthData?.events || []).forEach((event) => { const modelId = String(event.modelId || ""); if (!modelId) return; const list = map.get(modelId) || []; if (list.length < 5) { list.push(event); } map.set(modelId, list); }); return map; }, [modelHealthData?.events]); const recentCriticalModelHealthEvents = useMemo( () => (modelHealthData?.events || []) .filter((event) => event.counted || event.autoDisabled) .slice(0, 12), [modelHealthData?.events] ); // 网站设置状态 const [siteSettings, setSiteSettings] = useState({ siteName: "AI Chatbot", siteLogo: "", siteLogoDark: "", favicon: "", showAutoModel: true, usageTimeEnabled: false, usageTimeStart: "08:00", usageTimeEnd: "20:00", usageTimeMessage: "由壹点累了也是需要休息的", defaultTheme: "system", bannerEnabled: true, bannerText: "限时特惠:升级会员最高立享 45% OFF!", bannerLink: null, bannerActionText: "立即查看", toolbox1Name: "工具箱", toolbox1Enabled: true, toolbox2Name: "工具箱2", toolbox2Enabled: false, tutorialVideoUrl: null, tutorialEnabled: false, rankingEnabled: true, productPlanningEnabled: true, canvasEnabled: true, agentEnabled: true, canvasAgentModeEnabled: true, canvasAgentFeatureSettings: defaultCanvasAgentFeatureSettings, videoCanvasEnabled: true, videoCanvasSidebarEnabled: true, agentCategoryTreeConfig: { productCategories: [], audienceTags: [], usageScenarios: [], brands: [], }, agentSecondaryTypeConfig: { image: [], video: [], text: [], }, agentWorkflowUiConfig: defaultAgentWorkflowUiConfig, realtimeCanvasEnabled: true, realtimeCanvasModeIconConfig: defaultRealtimeCanvasModeIconConfig, canvasToolbarConfig: defaultCanvasToolbarConfig, lovartHomeSidebarConfig: defaultLovartHomeSidebarConfig, aiDetailGenerationEnabled: false, canvasDefaultModelId: null, canvasAutoModeDefaultModelId: null, agentWorkflowDefaultModelId: null, canvasDetailDefaultModelId: null, canvasDetailAgentModelId: null, lovartComposerPrefix: "让「由壹点AI」", lovartComposerPreviewTexts: [ "打造引人注目的社交媒体视觉", "帮我把这张产品图做成高级电商海报", "基于参考图延展 3 套同风格方案", ], lovartPromptTagVisibility: defaultLovartPromptTagVisibility, contactConfig: defaultContactConfig, storageFailoverConfig: defaultStorageFailoverConfig, ossImageMigrationConfig: defaultOssImageMigrationConfig, pointsSystemConfig: defaultPointsSystemConfig, pointsPackages: defaultPointsPackages, dulupayConfig: defaultDulupayConfig, lovartPromoText: "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠", lovartPromoActionText: "立即升级", lovartHeroTitle: "由壹点AI 让设计更简单", lovartHeroSubtitle: "懂你的设计代理,帮你搞定一切", surpriseCategories: ["全部", "精选"], membershipConfig: defaultMembershipConfig, membershipUserOverrides: defaultMembershipUserOverrides, }); const [homeAnimationConfig, setHomeAnimationConfig] = useState({ enabled: false, fileName: "", htmlContent: "", updatedAt: null, }); const [openClawChatConfig, setOpenClawChatConfig] = useState({ enabled: false, baseUrl: DEFAULT_OPENCLAW_CHAT_BASE_URL, token: "", timeoutMs: 600_000, }); const [openClawChatTokenConfigured, setOpenClawChatTokenConfigured] = useState(false); const lastLoadedSiteSettingsRef = useRef(null); const lastLoadedHomeAnimationConfigRef = useRef( null ); const [newOverrideUserId, setNewOverrideUserId] = useState(""); const [newOverrideEmail, setNewOverrideEmail] = useState(""); const [newOverridePlanId, setNewOverridePlanId] = useState(""); const pointsSystemConfig = useMemo( () => normalizePointsSystemConfig(siteSettings.pointsSystemConfig), [siteSettings.pointsSystemConfig] ); useEffect(() => { if (siteSettingsData) { const normalizedMembershipConfig = normalizeMembershipConfig( (siteSettingsData as any).membershipConfig ); const normalizedMembershipOverrides = normalizeMembershipUserOverrides( (siteSettingsData as any).membershipUserOverrides ); const normalizedPointsSystemConfig = normalizePointsSystemConfig( (siteSettingsData as any).pointsSystemConfig ); const normalizedPointsPackages = normalizePointsPackages( (siteSettingsData as any).pointsPackages ); const normalizedDulupayConfig = normalizeDulupayConfig( (siteSettingsData as any).dulupayConfig ); const nextSiteSettings: SiteSettings = { siteName: siteSettingsData.siteName || "AI Chatbot", siteLogo: siteSettingsData.siteLogo || "", siteLogoDark: siteSettingsData.siteLogoDark || "", favicon: siteSettingsData.favicon || "", showAutoModel: siteSettingsData.showAutoModel ?? true, usageTimeEnabled: siteSettingsData.usageTimeEnabled ?? false, usageTimeStart: siteSettingsData.usageTimeStart || "08:00", usageTimeEnd: siteSettingsData.usageTimeEnd || "20:00", usageTimeMessage: siteSettingsData.usageTimeMessage || "由壹点累了也是需要休息的", defaultTheme: siteSettingsData.defaultTheme || "system", bannerEnabled: siteSettingsData.bannerEnabled ?? true, bannerText: siteSettingsData.bannerText || "限时特惠:升级会员最高立享 45% OFF!", bannerLink: siteSettingsData.bannerLink || null, bannerActionText: siteSettingsData.bannerActionText || "立即查看", toolbox1Name: siteSettingsData.toolbox1Name || "工具箱", toolbox1Enabled: siteSettingsData.toolbox1Enabled ?? true, toolbox2Name: siteSettingsData.toolbox2Name || "工具箱2", toolbox2Enabled: siteSettingsData.toolbox2Enabled ?? false, tutorialVideoUrl: siteSettingsData.tutorialVideoUrl || null, tutorialEnabled: siteSettingsData.tutorialEnabled ?? false, rankingEnabled: siteSettingsData.rankingEnabled ?? true, productPlanningEnabled: (siteSettingsData as any).productPlanningEnabled ?? true, canvasEnabled: siteSettingsData.canvasEnabled ?? true, agentEnabled: (siteSettingsData as any).agentEnabled ?? true, canvasAgentModeEnabled: (siteSettingsData as any).canvasAgentModeEnabled ?? true, canvasAgentFeatureSettings: normalizeCanvasAgentFeatureSettings( (siteSettingsData as any).canvasAgentFeatureSettings ), videoCanvasEnabled: (siteSettingsData as any).videoCanvasEnabled ?? (siteSettingsData as any).videoCanvasSidebarEnabled ?? true, videoCanvasSidebarEnabled: (siteSettingsData as any).videoCanvasSidebarEnabled ?? true, agentCategoryTreeConfig: { productCategories: Array.isArray( (siteSettingsData as any).agentCategoryTreeConfig?.productCategories ) ? (siteSettingsData as any).agentCategoryTreeConfig .productCategories : [], audienceTags: Array.isArray( (siteSettingsData as any).agentCategoryTreeConfig?.audienceTags ) ? (siteSettingsData as any).agentCategoryTreeConfig.audienceTags : [], usageScenarios: Array.isArray( (siteSettingsData as any).agentCategoryTreeConfig?.usageScenarios ) ? (siteSettingsData as any).agentCategoryTreeConfig.usageScenarios : [], brands: Array.isArray( (siteSettingsData as any).agentCategoryTreeConfig?.brands ) ? (siteSettingsData as any).agentCategoryTreeConfig.brands : [], }, agentSecondaryTypeConfig: { image: Array.isArray( (siteSettingsData as any).agentSecondaryTypeConfig?.image ) ? (siteSettingsData as any).agentSecondaryTypeConfig.image : [], video: Array.isArray( (siteSettingsData as any).agentSecondaryTypeConfig?.video ) ? (siteSettingsData as any).agentSecondaryTypeConfig.video : [], 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, realtimeCanvasModeIconConfig: normalizeRealtimeCanvasModeIconConfig( (siteSettingsData as any).realtimeCanvasModeIconConfig ), canvasToolbarConfig: normalizeCanvasToolbarConfig( (siteSettingsData as any).canvasToolbarConfig ), lovartHomeSidebarConfig: normalizeLovartHomeSidebarConfig( (siteSettingsData as any).lovartHomeSidebarConfig ), aiDetailGenerationEnabled: siteSettingsData.aiDetailGenerationEnabled ?? false, canvasDefaultModelId: siteSettingsData.canvasDefaultModelId || null, canvasAutoModeDefaultModelId: siteSettingsData.canvasAutoModeDefaultModelId || null, agentWorkflowDefaultModelId: (siteSettingsData as any).agentWorkflowDefaultModelId || null, canvasDetailDefaultModelId: siteSettingsData.canvasDetailDefaultModelId || null, canvasDetailAgentModelId: (siteSettingsData as any).canvasDetailAgentModelId || null, lovartComposerPrefix: String((siteSettingsData as any).lovartComposerPrefix || "").trim() || "让「由壹点AI」", lovartComposerPreviewTexts: Array.isArray( siteSettingsData.lovartComposerPreviewTexts ) ? siteSettingsData.lovartComposerPreviewTexts .map((item: any) => String(item).trim()) .filter(Boolean) : [ "打造引人注目的社交媒体视觉", "帮我把这张产品图做成高级电商海报", "基于参考图延展 3 套同风格方案", ], lovartPromptTagVisibility: { nanoBananaPro: (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, }, contactConfig: { phone: String( (siteSettingsData as any).contactConfig?.phone || "" ).trim(), wechatQrUrl: String( (siteSettingsData as any).contactConfig?.wechatQrUrl || "" ).trim(), }, storageFailoverConfig: { enabled: (siteSettingsData as any).storageFailoverConfig?.enabled === true, backupStorageConfigId: String( (siteSettingsData as any).storageFailoverConfig ?.backupStorageConfigId || "" ).trim(), }, ossImageMigrationConfig: { enabled: (siteSettingsData as any).ossImageMigrationConfig?.enabled !== false, preferAliyunForUploads: (siteSettingsData as any).ossImageMigrationConfig ?.preferAliyunForUploads !== false, sourceStorageConfigId: String( (siteSettingsData as any).ossImageMigrationConfig ?.sourceStorageConfigId || "" ).trim(), targetStorageConfigId: String( (siteSettingsData as any).ossImageMigrationConfig ?.targetStorageConfigId || "" ).trim(), migrateAfterHours: Math.max( 1, Math.min( 720, Number( (siteSettingsData as any).ossImageMigrationConfig ?.migrateAfterHours ) || defaultOssImageMigrationConfig.migrateAfterHours ) ), cleanupAfterHours: Math.max( 2, Math.min( 1440, Number( (siteSettingsData as any).ossImageMigrationConfig ?.cleanupAfterHours ) || defaultOssImageMigrationConfig.cleanupAfterHours ) ), batchSize: Math.max( 1, Math.min( 100, Number( (siteSettingsData as any).ossImageMigrationConfig?.batchSize ) || defaultOssImageMigrationConfig.batchSize ) ), }, pointsSystemConfig: normalizedPointsSystemConfig, pointsPackages: normalizedPointsPackages, dulupayConfig: normalizedDulupayConfig, lovartPromoText: siteSettingsData.lovartPromoText || "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠", lovartPromoActionText: siteSettingsData.lovartPromoActionText || "立即升级", lovartHeroTitle: siteSettingsData.lovartHeroTitle || "由壹点AI 让设计更简单", lovartHeroSubtitle: siteSettingsData.lovartHeroSubtitle || "懂你的设计代理,帮你搞定一切", surpriseCategories: Array.isArray(siteSettingsData.surpriseCategories) ? siteSettingsData.surpriseCategories .map((item: any) => String(item).trim()) .filter(Boolean) : ["全部", "惊喜"], membershipConfig: normalizedMembershipConfig, membershipUserOverrides: normalizedMembershipOverrides, }; setSiteSettings(nextSiteSettings); lastLoadedSiteSettingsRef.current = nextSiteSettings; } }, [siteSettingsData]); useEffect(() => { const raw = homeAnimationSettingData?.setting?.value && typeof homeAnimationSettingData.setting.value === "object" && !Array.isArray(homeAnimationSettingData.setting.value) ? (homeAnimationSettingData.setting.value as Record) : {}; const nextHomeAnimationConfig = { enabled: raw.enabled === true, fileName: String(raw.fileName || "").trim(), htmlContent: typeof raw.htmlContent === "string" ? raw.htmlContent.replace(/^\uFEFF/, "").replace(/\u0000/g, "") : "", updatedAt: String(raw.updatedAt || "").trim() || null, }; setHomeAnimationConfig(nextHomeAnimationConfig); lastLoadedHomeAnimationConfigRef.current = nextHomeAnimationConfig; }, [homeAnimationSettingData]); useEffect(() => { const raw = openClawChatSettingData?.setting?.value && typeof openClawChatSettingData.setting.value === "object" && !Array.isArray(openClawChatSettingData.setting.value) ? (openClawChatSettingData.setting.value as Record) : {}; setOpenClawChatConfig({ enabled: raw.enabled === true, baseUrl: String(raw.baseUrl || "").trim() || DEFAULT_OPENCLAW_CHAT_BASE_URL, token: "", timeoutMs: Math.min( 600_000, Math.max(5_000, Math.round(Number(raw.timeoutMs) || 600_000)) ), }); setOpenClawChatTokenConfigured( raw.hasToken === true || Boolean(String(raw.token || "").trim()) ); }, [openClawChatSettingData]); useEffect(() => { const normalized = normalizeDetailAgentLocalMoonshotConfig( moonshotDetailAgentSettingData?.setting?.value ); setMoonshotDetailAgentConfig({ providerId: "", modelEntryId: "", providerName: normalized.providerName, baseUrl: normalized.baseUrl, apiKey: "", modelId: normalized.modelId, displayName: normalized.displayName, isEnabled: normalized.enabled, }); }, [moonshotDetailAgentSettingData]); const featuredCategoryOptions = useMemo(() => { const values = (siteSettings.surpriseCategories || []) .map((item) => String(item || "").trim()) .filter((item) => item && item !== "全部"); return Array.from(new Set(values)); }, [siteSettings.surpriseCategories]); // 抠图API配置状态 const [rmbgConfig, setRmbgConfig] = useState({ provider: "vip98k" as "vip98k" | "rmbg" | "photoroom" | "aoscdn", // 抠图服务提供商 apiUrl: "", apiKey: "", // PhotoRoom 配置 photoroomApiKey: "", // AOS 配置 aosApiKey: "", aosApiBaseUrl: "https://techsz.aoscdn.com", aosForegroundType: "auto" as "auto" | "person" | "object" | "stamp", aosCrop: false, aosFormat: "png" as "png" | "jpg", }); // 变清晰API配置状态 const [enhanceConfig, setEnhanceConfig] = useState({ provider: "vip98k" as "vip98k" | "legacy", apiUrl: "", apiKey: "", type: "clean", // "clean" | "face" scaleFactor: "auto", // "auto" | "1" | "2" | "4" // 备用API配置 (Real-ESRGAN) useBackupApi: false, // 是否启用备用API backupApiUrl: "http://156.225.27.52:5233", // 备用API地址 backupApiKey: "", // 备用API密钥 backupScale: "4", // 备用API放大倍数 "2" | "4" | "8" backupModel: "RealESRGAN_x4plus", // 备用API模型 }); const [enhanceProConfig, setEnhanceProConfig] = useState( defaultEnhanceProKreaConfig() ); const [enhanceProApiKeysDraft, setEnhanceProApiKeysDraft] = useState(""); const [enhanceProPresetEditorMode, setEnhanceProPresetEditorMode] = useState< "simple" | "advanced" >("simple"); const [selectedEnhanceProPresetId, setSelectedEnhanceProPresetId] = useState(""); const [draggingEnhanceProPresetId, setDraggingEnhanceProPresetId] = useState(""); const [aiWashConfig, setAiWashConfig] = useState(defaultAiWashConfig()); const [canvasLocalEditConfig, setCanvasLocalEditConfig] = useState( defaultCanvasLocalEditConfig() ); // 图像拓展API配置状态 const [extendConfig, setExtendConfig] = useState({ provider: "vip98k" as "vip98k" | "dashscope", apiKey: "", xScale: "1.5", // X轴放大倍数 yScale: "1.5", // Y轴放大倍数 }); // 去水印API配置状态 const [watermarkRemoveConfig, setWatermarkRemoveConfig] = useState({ apiKey: "", // PicWish API密钥 }); // 角度编辑API配置状态 const [angleEditConfig, setAngleEditConfig] = useState({ enabled: false, falApiKey: "", loraScale: 1.0, // 官方默认值 inferenceSteps: 28, guidanceScale: 4.5, // 官方默认值 maxImageSize: 10, timeout: 120, }); const [layerSplitConfig, setLayerSplitConfig] = useState({ enabled: false, provider: "replicate" as const, replicateApiToken: "", replicateModel: "qwen/qwen-image-layered", layers: 4, resolution: 640, trueCfgScale: 4, numInferenceSteps: 30, cfgNormalize: true, useEnPrompt: true, negativePrompt: "", timeoutMs: 300_000, }); // 实时画布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", renderMode: "balanced" as "strict-structure" | "balanced" | "stylize", throttleInterval: 100, maxBuffering: 2, maxInputResolution: 1024, jpegQuality: 0.55, previewMegapixels: 0.4, detailMegapixels: 1.0, outputJpegQuality: 62, steps: 3, scheduleMu: 2.3, feedback: 0.95, imageSize: "square" as "square" | "square_hd", }); // 批量处理配置状态 const [sceneFusionConfig, setSceneFusionConfig] = useState({ enabled: true, preRmbgEnabled: true, defaultPrompt: "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。", maxScenes: 10, modelId: "", // 首选生图模型ID(多样性模式下优先尝试) }); const [canvasImageTaskQueueConfig, setCanvasImageTaskQueueConfig] = useState(() => normalizeCanvasImageTaskQueueConfig({ concurrency: DEFAULT_CANVAS_IMAGE_TASK_CONCURRENCY, batchDispatchIntervalMs: DEFAULT_CANVAS_IMAGE_BATCH_DISPATCH_INTERVAL_MS, }) ); const [tripo3dConfig, setTripo3dConfig] = useState({ providerId: "", name: "Tripo3D", baseUrl: getProviderDefaultBaseUrl("tripo-3d"), apiKey: "", isEnabled: true, }); const [worldlabs3dConfig, setWorldlabs3dConfig] = useState({ providerId: "", name: "World Labs", baseUrl: getProviderDefaultBaseUrl("worldlabs-3d"), apiKey: "", isEnabled: true, }); const [worldlabsKeyPoolConfig, setWorldlabsKeyPoolConfig] = useState( defaultWorldLabsKeyPoolConfig ); const [worldlabsApiKeysDraft, setWorldlabsApiKeysDraft] = useState(""); const [threeDModelAliasConfig, setThreeDModelAliasConfig] = useState( defaultThreeDModelAliasConfig() ); const [tripoProviderSearch, setTripoProviderSearch] = useState(""); const [tripoProviderBusyId, setTripoProviderBusyId] = useState( null ); const [worldlabsProviderSearch, setWorldlabsProviderSearch] = useState(""); const [worldlabsProviderBusyId, setWorldlabsProviderBusyId] = useState< string | null >(null); const [kittycadCadConfig, setKittycadCadConfig] = useState({ providerId: "", name: "KittyCAD CAD", baseUrl: getProviderDefaultBaseUrl("kittycad-cad"), apiKey: "", isEnabled: true, }); const [isTestingKittycadCad, setIsTestingKittycadCad] = useState(false); const [moonshotDetailAgentConfig, setMoonshotDetailAgentConfig] = useState({ providerId: "", modelEntryId: "", providerName: "Kimi Code 编程套餐", baseUrl: DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL, apiKey: "", modelId: DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID, displayName: DETAIL_AGENT_LOCAL_MOONSHOT_DISPLAY_NAME, isEnabled: true, }); const [thirdPartySqlServerConfig, setThirdPartySqlServerConfig] = useState({ ...defaultThirdPartySqlServerConfig, }); const [isTestingThirdPartySqlServer, setIsTestingThirdPartySqlServer] = useState(false); const [expandedLocalAiSections, setExpandedLocalAiSections] = useState< Record >(loadLocalAiExpandedSections); const [ isThirdPartySqlServerFieldsVisible, setIsThirdPartySqlServerFieldsVisible, ] = useState(false); const [productPlanningApiConfig, setProductPlanningApiConfig] = useState(defaultProductPlanningApiConfig()); const [productPlanningAiModelConfig, setProductPlanningAiModelConfig] = useState( defaultProductPlanningAiModelConfig() ); // 加载抠图API配置 const { data: rmbgConfigData } = useSWR( "/api/admin/settings?key=rmbg_api_config", fetcher ); useEffect(() => { if (rmbgConfigData?.setting?.value) { const config = rmbgConfigData.setting.value as { provider?: "vip98k" | "rmbg" | "photoroom" | "aoscdn"; apiUrl?: string; apiKey?: string; photoroomApiKey?: string; aosApiKey?: string; aosApiBaseUrl?: string; aosForegroundType?: "" | "person" | "object" | "stamp"; aosCrop?: 0 | 1; aosFormat?: "png" | "jpg"; }; setRmbgConfig({ provider: config.provider || "vip98k", apiUrl: config.apiUrl || "", apiKey: config.apiKey || "", photoroomApiKey: config.photoroomApiKey || "", aosApiKey: config.aosApiKey || "", aosApiBaseUrl: config.aosApiBaseUrl || "https://techsz.aoscdn.com", aosForegroundType: config.aosForegroundType === "person" || config.aosForegroundType === "object" || config.aosForegroundType === "stamp" ? config.aosForegroundType : "auto", aosCrop: config.aosCrop === 1, aosFormat: config.aosFormat === "jpg" ? "jpg" : "png", }); } }, [rmbgConfigData]); // 加载变清晰API配置 const { data: enhanceConfigData } = useSWR( "/api/admin/settings?key=enhance_api_config", fetcher ); useEffect(() => { if (enhanceConfigData?.setting?.value) { const config = enhanceConfigData.setting.value as { provider?: "vip98k" | "legacy"; apiUrl?: string; apiKey?: string; type?: string; scaleFactor?: number; useBackupApi?: boolean; backupApiUrl?: string; backupApiKey?: string; backupScale?: string; backupModel?: string; }; setEnhanceConfig({ provider: config.provider === "vip98k" ? "vip98k" : "legacy", apiUrl: config.apiUrl || "", apiKey: config.apiKey || "", type: config.type || "clean", scaleFactor: config.scaleFactor ? String(config.scaleFactor) : "auto", useBackupApi: config.useBackupApi || false, backupApiUrl: config.backupApiUrl || "http://156.225.27.52:5233", backupApiKey: config.backupApiKey || "", backupScale: config.backupScale || "4", backupModel: config.backupModel || "RealESRGAN_x4plus", }); } }, [enhanceConfigData]); useEffect(() => { if (!enhanceProConfigData) return; setEnhanceProConfig( normalizeEnhanceProKreaConfig(enhanceProConfigData.setting?.value) ); setEnhanceProApiKeysDraft(""); }, [enhanceProConfigData]); useEffect(() => { if (!aiWashConfigData) return; setAiWashConfig(normalizeAiWashConfig(aiWashConfigData.setting?.value)); }, [aiWashConfigData]); useEffect(() => { if (!canvasLocalEditConfigData) return; setCanvasLocalEditConfig( normalizeCanvasLocalEditConfig(canvasLocalEditConfigData.setting?.value) ); }, [canvasLocalEditConfigData]); const enhanceProApiKeysDraftCount = useMemo( () => normalizeEnhanceProKreaConfig({ apiKeys: enhanceProApiKeysDraft, }).apiKeys.length, [enhanceProApiKeysDraft] ); const savedEnhanceProKeyRows = useMemo( () => enhanceProConfig.apiKeys.map((apiKey, index) => ({ id: `saved-enhance-pro-key-${index}-${apiKey.slice(-6)}`, index, name: `Krea Key ${index + 1}`, maskedKey: maskAdminSecret(apiKey), })), [enhanceProConfig.apiKeys] ); const draftEnhanceProKeyRows = useMemo( () => normalizeEnhanceProKreaConfig({ apiKeys: enhanceProApiKeysDraft, }).apiKeys.map((apiKey, index) => ({ id: `draft-enhance-pro-key-${index}-${apiKey.slice(-6)}`, name: `待保存 Key ${index + 1}`, maskedKey: maskAdminSecret(apiKey), })), [enhanceProApiKeysDraft] ); useEffect(() => { if (enhanceProConfig.presets.length === 0) { if (selectedEnhanceProPresetId) { setSelectedEnhanceProPresetId(""); } return; } const selectedExists = enhanceProConfig.presets.some( (preset) => preset.id === selectedEnhanceProPresetId ); if (selectedExists) { return; } const fallbackPreset = enhanceProConfig.presets.find((preset) => preset.isDefault) || enhanceProConfig.presets[0]; setSelectedEnhanceProPresetId(fallbackPreset?.id || ""); }, [enhanceProConfig.presets, selectedEnhanceProPresetId]); const selectedEnhanceProPresetIndex = useMemo( () => enhanceProConfig.presets.findIndex( (preset) => preset.id === selectedEnhanceProPresetId ), [enhanceProConfig.presets, selectedEnhanceProPresetId] ); const selectedEnhanceProPreset = selectedEnhanceProPresetIndex >= 0 ? enhanceProConfig.presets[selectedEnhanceProPresetIndex] : null; // 加载图像拓展API配置 const { data: extendConfigData } = useSWR( "/api/admin/settings?key=extend_api_config", fetcher ); useEffect(() => { if (extendConfigData?.setting?.value) { const config = extendConfigData.setting.value as { provider?: "vip98k" | "dashscope"; apiKey?: string; xScale?: number; yScale?: number; }; setExtendConfig({ provider: config.provider === "vip98k" ? "vip98k" : "dashscope", apiKey: config.apiKey || "", xScale: config.xScale ? String(config.xScale) : "1.5", yScale: config.yScale ? String(config.yScale) : "1.5", }); } }, [extendConfigData]); // 加载去水印API配置 const { data: watermarkRemoveConfigData } = useSWR( "/api/admin/settings?key=watermark_remove_config", fetcher ); useEffect(() => { if (watermarkRemoveConfigData?.setting?.value) { const config = watermarkRemoveConfigData.setting.value as { apiKey?: string; }; setWatermarkRemoveConfig({ apiKey: config.apiKey || "", }); } }, [watermarkRemoveConfigData]); const { data: productPlanningApiConfigData } = useSWR( `/api/admin/settings?key=${PRODUCT_PLANNING_API_CONFIG_KEY}`, fetcher ); useEffect(() => { setProductPlanningApiConfig( normalizeProductPlanningApiConfig( productPlanningApiConfigData?.setting?.value ) ); }, [productPlanningApiConfigData]); useEffect(() => { setProductPlanningAiModelConfig( normalizeProductPlanningAiModelConfig( productPlanningAiModelConfigData?.setting?.value ) ); }, [productPlanningAiModelConfigData]); // 加载角度编辑API配置 const { data: angleEditConfigData } = useSWR( "/api/admin/settings?key=angle_edit_config", fetcher ); useEffect(() => { if (angleEditConfigData?.setting?.value) { const config = angleEditConfigData.setting.value as { enabled?: boolean; falApiKey?: string; loraScale?: number; inferenceSteps?: number; guidanceScale?: number; maxImageSize?: number; timeout?: number; }; setAngleEditConfig({ enabled: config.enabled || false, falApiKey: config.falApiKey || "", loraScale: config.loraScale ?? 1.0, inferenceSteps: config.inferenceSteps ?? 28, guidanceScale: config.guidanceScale ?? 4.5, maxImageSize: config.maxImageSize ?? 10, timeout: config.timeout ?? 120, }); } }, [angleEditConfigData]); const { data: layerSplitConfigData } = useSWR( "/api/admin/settings?key=layer_split_config", fetcher ); useEffect(() => { if (layerSplitConfigData?.setting?.value) { const config = layerSplitConfigData.setting.value as { enabled?: boolean; provider?: "replicate"; replicateApiToken?: string; replicateModel?: string; layers?: number; resolution?: number; trueCfgScale?: number; numInferenceSteps?: number; cfgNormalize?: boolean; useEnPrompt?: boolean; negativePrompt?: string; timeoutMs?: number; }; setLayerSplitConfig({ enabled: config.enabled === true, provider: "replicate", 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) ), cfgNormalize: config.cfgNormalize !== false, useEnPrompt: config.useEnPrompt !== false, negativePrompt: config.negativePrompt || "", timeoutMs: Math.max( 30_000, Math.min(900_000, Number(config.timeoutMs) || 300_000) ), }); } }, [layerSplitConfigData]); const { data: thirdPartySqlServerConfigData } = useSWR( `/api/admin/settings?key=${THIRD_PARTY_SQLSERVER_CONFIG_KEY}`, fetcher ); useEffect(() => { if (thirdPartySqlServerConfigData?.setting?.value) { const config = thirdPartySqlServerConfigData.setting.value as { enabled?: boolean; host?: string; port?: number; database?: string; username?: string; password?: string; encrypt?: boolean; trustServerCertificate?: boolean; connectionTimeoutMs?: number; requestTimeoutMs?: number; }; setThirdPartySqlServerConfig({ enabled: config.enabled !== false, host: config.host || "", port: Math.max(1, Math.min(65_535, Number(config.port) || 1433)), database: config.database || "", username: config.username || "", password: config.password || "", encrypt: config.encrypt === true, trustServerCertificate: config.trustServerCertificate !== false, connectionTimeoutMs: Math.max( 1000, Math.min(120_000, Number(config.connectionTimeoutMs) || 15_000) ), requestTimeoutMs: Math.max( 1000, Math.min(120_000, Number(config.requestTimeoutMs) || 30_000) ), }); } }, [thirdPartySqlServerConfigData]); const isThirdPartySqlServerSectionExpanded = Boolean( expandedLocalAiSections["third-party-sqlserver"] ); useEffect(() => { if (!isThirdPartySqlServerSectionExpanded) { setIsThirdPartySqlServerFieldsVisible(false); } }, [isThirdPartySqlServerSectionExpanded]); // 加载实时画布API配置 const { data: realtimeCanvasConfigData } = useSWR( "/api/admin/settings?key=realtime_canvas_config", fetcher ); useEffect(() => { if (realtimeCanvasConfigData?.setting?.value) { const config = realtimeCanvasConfigData.setting.value as { enabled?: boolean; providerType?: "bfl" | "replicate" | "fal"; bflApiKey?: string; bflBaseUrl?: string; bflModel?: string; falApiKey?: string; falBaseUrl?: string; falModel?: string; replicateApiToken?: string; replicateModel?: string; fallbackEnabled?: boolean; fallbackProviderType?: "bfl" | "replicate" | "fal"; fallbackBflApiKey?: string; fallbackBflBaseUrl?: string; fallbackBflModel?: string; fallbackRunwareApiKey?: string; fallbackFalApiKey?: string; fallbackFalBaseUrl?: string; fallbackFalModel?: string; fallbackRunwareModel?: string; fallbackReplicateApiToken?: string; fallbackReplicateModel?: string; runwareApiKey?: string; runwareModel?: string; renderMode?: "strict-structure" | "balanced" | "stylize"; throttleInterval?: number; maxBuffering?: number; maxInputResolution?: number; jpegQuality?: number; previewMegapixels?: number; detailMegapixels?: number; outputJpegQuality?: number; defaults?: { steps?: number; scheduleMu?: number; feedback?: number; imageSize?: "square" | "square_hd"; }; }; setRealtimeCanvasConfig({ enabled: config.enabled ?? true, providerType: config.providerType === "replicate" ? "replicate" : config.providerType === "fal" ? "fal" : "bfl", bflApiKey: config.bflApiKey || config.runwareApiKey || "", bflBaseUrl: config.bflBaseUrl || DEFAULT_BFL_BASE_URL, 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", fallbackEnabled: config.fallbackEnabled === true, fallbackProviderType: config.fallbackProviderType === "replicate" ? "replicate" : config.fallbackProviderType === "fal" ? "fal" : "bfl", fallbackBflApiKey: config.fallbackBflApiKey || config.fallbackRunwareApiKey || "", fallbackBflBaseUrl: config.fallbackBflBaseUrl || DEFAULT_BFL_BASE_URL, fallbackBflModel: config.fallbackBflModel || config.fallbackRunwareModel || DEFAULT_BFL_REALTIME_MODEL, fallbackFalApiKey: config.fallbackFalApiKey || "", fallbackFalBaseUrl: config.fallbackFalBaseUrl || "https://fal.run", fallbackFalModel: config.fallbackFalModel || "", fallbackReplicateApiToken: config.fallbackReplicateApiToken || "", fallbackReplicateModel: config.fallbackReplicateModel || "black-forest-labs/flux-2-klein-4b", renderMode: config.renderMode === "strict-structure" || config.renderMode === "stylize" ? config.renderMode : "balanced", throttleInterval: Number(config.throttleInterval || 100), maxBuffering: Number(config.maxBuffering || 2), maxInputResolution: Number(config.maxInputResolution || 1024), jpegQuality: Number(config.jpegQuality || 0.55), previewMegapixels: Number(config.previewMegapixels || 0.4), detailMegapixels: Number(config.detailMegapixels || 1.0), outputJpegQuality: Number(config.outputJpegQuality || 62), 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", }); } }, [realtimeCanvasConfigData]); // 加载批量处理配置 const { data: sceneFusionConfigData } = useSWR( "/api/admin/settings?key=scene_fusion_config", fetcher ); useEffect(() => { if (!canvasImageTaskQueueConfigData) return; setCanvasImageTaskQueueConfig( normalizeCanvasImageTaskQueueConfig( canvasImageTaskQueueConfigData.setting?.value ) ); }, [canvasImageTaskQueueConfigData]); useEffect(() => { if (sceneFusionConfigData?.setting?.value) { const config = sceneFusionConfigData.setting.value as { enabled?: boolean; preRmbgEnabled?: boolean; defaultPrompt?: string; maxScenes?: number; modelId?: string; }; setSceneFusionConfig({ enabled: config.enabled ?? true, preRmbgEnabled: config.preRmbgEnabled ?? true, defaultPrompt: config.defaultPrompt || "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。", maxScenes: Math.min(config.maxScenes ?? 10, 10), modelId: config.modelId ?? "", }); } }, [sceneFusionConfigData]); useEffect(() => { const tripoProvider = (providersData?.providers || []).find( (provider) => provider.type === "tripo-3d" ); if (tripoProvider) { setTripo3dConfig({ providerId: tripoProvider.id, name: tripoProvider.name || "Tripo3D", baseUrl: tripoProvider.baseUrl || getProviderDefaultBaseUrl("tripo-3d"), apiKey: "", isEnabled: tripoProvider.isEnabled, }); return; } setTripo3dConfig({ providerId: "", name: "Tripo3D", baseUrl: getProviderDefaultBaseUrl("tripo-3d"), apiKey: "", isEnabled: true, }); }, [providersData?.providers]); useEffect(() => { const savedWorldlabsConfig = normalizeWorldLabsKeyPoolConfig( worldlabsKeyPoolConfigData?.setting?.value ); const worldlabsProvider = (providersData?.providers || []).find( (provider) => provider.type === "worldlabs-3d" ); if (worldlabsProvider) { setWorldlabs3dConfig({ providerId: worldlabsProvider.id, name: worldlabsProvider.name || savedWorldlabsConfig.providerName, baseUrl: worldlabsProvider.baseUrl || savedWorldlabsConfig.baseUrl || getProviderDefaultBaseUrl("worldlabs-3d"), apiKey: "", isEnabled: worldlabsProvider.isEnabled, }); return; } setWorldlabs3dConfig((prev) => ({ providerId: "", name: savedWorldlabsConfig.providerName || "World Labs", baseUrl: savedWorldlabsConfig.baseUrl || getProviderDefaultBaseUrl("worldlabs-3d"), apiKey: prev.providerId ? "" : prev.apiKey, isEnabled: savedWorldlabsConfig.isEnabled, })); }, [providersData?.providers, worldlabsKeyPoolConfigData?.setting?.value]); useEffect(() => { setWorldlabsKeyPoolConfig( normalizeWorldLabsKeyPoolConfig( worldlabsKeyPoolConfigData?.setting?.value ) ); }, [worldlabsKeyPoolConfigData]); useEffect(() => { setThreeDModelAliasConfig( normalizeThreeDModelAliasConfig( threeDModelAliasConfigData?.setting?.value ) ); }, [threeDModelAliasConfigData]); const tripoProviders = useMemo( () => (providersData?.providers || []).filter( (provider) => provider.type === "tripo-3d" ), [providersData?.providers] ); const filteredTripoProviders = useMemo(() => { const keyword = tripoProviderSearch.trim().toLowerCase(); if (!keyword) return tripoProviders; return tripoProviders.filter((provider) => [provider.name, provider.apiKey, provider.baseUrl] .filter(Boolean) .some((value) => value.toLowerCase().includes(keyword)) ); }, [tripoProviderSearch, tripoProviders]); const enabledTripoProviderCount = useMemo( () => tripoProviders.filter((provider) => provider.isEnabled).length, [tripoProviders] ); const worldlabsProviders = useMemo( () => (providersData?.providers || []).filter( (provider) => provider.type === "worldlabs-3d" ), [providersData?.providers] ); const filteredWorldlabsProviders = useMemo(() => { const keyword = worldlabsProviderSearch.trim().toLowerCase(); if (!keyword) return worldlabsProviders; return worldlabsProviders.filter((provider) => [provider.name, provider.apiKey, provider.baseUrl] .filter(Boolean) .some((value) => value.toLowerCase().includes(keyword)) ); }, [worldlabsProviderSearch, worldlabsProviders]); const enabledWorldlabsProviderCount = useMemo( () => worldlabsProviders.filter((provider) => provider.isEnabled).length, [worldlabsProviders] ); useEffect(() => { const kittycadProvider = (providersData?.providers || []).find( (provider) => provider.type === "kittycad-cad" ); if (kittycadProvider) { setKittycadCadConfig({ providerId: kittycadProvider.id, name: kittycadProvider.name || "KittyCAD CAD", baseUrl: kittycadProvider.baseUrl || getProviderDefaultBaseUrl("kittycad-cad"), apiKey: "", isEnabled: kittycadProvider.isEnabled, }); return; } setKittycadCadConfig({ providerId: "", name: "KittyCAD CAD", baseUrl: getProviderDefaultBaseUrl("kittycad-cad"), apiKey: "", isEnabled: true, }); }, [providersData?.providers]); // 导航链接状态 const [navLinkForm, setNavLinkForm] = useState({ id: "", name: "", url: "", order: 0, isEnabled: true, toolboxGroup: 1, // 默认属于工具箱1 }); const [showNavLinkDialog, setShowNavLinkDialog] = useState(false); const [editingNavLink, setEditingNavLink] = useState(null); // 视频上传状态 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 [showTutorialVideoDialog, setShowTutorialVideoDialog] = useState(false); const [editingTutorialVideo, setEditingTutorialVideo] = useState< string | null >(null); const [tutorialVideoForm, setTutorialVideoForm] = useState({ title: "", videoUrl: "", coverUrl: "", description: "", order: 0, isEnabled: true, }); // 快捷短语状态 const [questions, setQuestions] = useState([]); const { data: questionsData, mutate: mutateQuestions } = useSWR( "/api/starter-questions", fetcher ); const [canvasSkillTagsTitle, setCanvasSkillTagsTitle] = useState( DEFAULT_CANVAS_SKILL_TAGS_TITLE ); const [canvasSkillTagsEnabled, setCanvasSkillTagsEnabled] = useState(true); const [canvasSkillCategories, setCanvasSkillCategories] = useState< CanvasSkillCategory[] >([]); const [canvasSkillTags, setCanvasSkillTags] = useState( DEFAULT_CANVAS_SKILL_TAGS ); const [canvasSkillTagUploadingId, setCanvasSkillTagUploadingId] = useState< string | null >(null); const [realtimeModeIconUploading, setRealtimeModeIconUploading] = useState(null); const [aiWashModeIconUploading, setAiWashModeIconUploading] = useState(false); const [canvasToolbarIconUploading, setCanvasToolbarIconUploading] = useState(null); const [lovartHomeSidebarIconUploading, setLovartHomeSidebarIconUploading] = useState(null); const { data: canvasSkillTagsData, mutate: mutateCanvasSkillTags } = useSWR<{ enabled?: boolean; title?: string; categories?: CanvasSkillCategory[]; tags?: CanvasSkillTag[]; }>("/api/canvas-skill-tags", fetcher); const [dingTalkNotificationDraft, setDingTalkNotificationDraft] = useState( defaultDingTalkNotificationDraftConfig ); const [dingTalkRobotNotificationConfig, setDingTalkRobotNotificationConfig] = useState( defaultDingTalkRobotNotificationConfig ); const [dingTalkNotificationScheduleAt, setDingTalkNotificationScheduleAt] = useState(""); const [ isSavingDingTalkNotificationDraft, setIsSavingDingTalkNotificationDraft, ] = useState(false); const [ isSavingDingTalkRobotNotificationConfig, setIsSavingDingTalkRobotNotificationConfig, ] = useState(false); const [isSendingDingTalkNotification, setIsSendingDingTalkNotification] = useState(false); const [ isSchedulingDingTalkNotification, setIsSchedulingDingTalkNotification, ] = useState(false); const [ cancellingDingTalkNotificationTaskId, setCancellingDingTalkNotificationTaskId, ] = useState(null); useEffect(() => { const value = dingTalkRobotNotificationConfigData?.setting?.value; setDingTalkRobotNotificationConfig( normalizeDingTalkRobotNotificationConfig(value) ); }, [dingTalkRobotNotificationConfigData]); useEffect(() => { const value = dingTalkNotificationDraftData?.setting?.value; setDingTalkNotificationDraft( normalizeDingTalkNotificationDraftConfig(value) ); }, [dingTalkNotificationDraftData]); const handleSaveDingTalkNotificationDraft = async () => { setIsSavingDingTalkNotificationDraft(true); try { const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: DINGTALK_NOTIFICATION_DRAFT_CONFIG_KEY, value: dingTalkNotificationDraft, }), }); if (!response.ok) { throw new Error("保存失败"); } toast.success("钉钉通知草稿已保存"); mutateDingTalkNotificationDraft(); } catch (error) { toast.error("保存钉钉通知草稿失败"); } finally { setIsSavingDingTalkNotificationDraft(false); } }; const handleSaveDingTalkRobotNotificationConfig = async () => { setIsSavingDingTalkRobotNotificationConfig(true); try { const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY, value: dingTalkRobotNotificationConfig, }), }); if (!response.ok) { throw new Error("保存失败"); } toast.success("钉钉群机器人配置已保存"); mutateDingTalkRobotNotificationConfig(); } catch (error) { toast.error("保存钉钉群机器人配置失败"); } finally { setIsSavingDingTalkRobotNotificationConfig(false); } }; const handleSendNotificationToDingTalk = async () => { const title = String(dingTalkNotificationDraft.title || "").trim(); const content = String(dingTalkNotificationDraft.content || "").trim(); if (!title) { toast.error("请先填写钉钉通知标题"); return; } if (!content) { toast.error("请先填写钉钉通知正文"); return; } setIsSendingDingTalkNotification(true); try { const response = await fetch("/api/admin/dingtalk-robot/send", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title, content }), }); const payload = await response.json().catch(() => null); if (!response.ok || payload?.success === false) { throw new Error(payload?.error || "发送失败"); } toast.success("钉钉通知已发送"); } catch (error) { toast.error( error instanceof Error ? error.message : "发送钉钉群通知失败" ); } finally { setIsSendingDingTalkNotification(false); } }; const handleScheduleDingTalkNotification = async () => { const title = String(dingTalkNotificationDraft.title || "").trim(); const content = String(dingTalkNotificationDraft.content || "").trim(); if (!title) { toast.error("请先填写钉钉通知标题"); return; } if (!content) { toast.error("请先填写钉钉通知正文"); return; } if (!dingTalkNotificationScheduleAt) { toast.error("请选择定时发送时间"); return; } const scheduledAt = new Date(dingTalkNotificationScheduleAt); if (Number.isNaN(scheduledAt.getTime())) { toast.error("定时发送时间格式不正确"); return; } setIsSchedulingDingTalkNotification(true); try { const response = await fetch("/api/admin/dingtalk-robot/notifications", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title, content, scheduledAt: scheduledAt.toISOString(), }), }); const payload = await response.json().catch(() => null); if (!response.ok || payload?.success === false) { throw new Error(payload?.error || "创建定时通知失败"); } toast.success("钉钉定时通知已创建"); setDingTalkNotificationScheduleAt(""); mutateDingTalkNotificationTasks(); } catch (error) { toast.error( error instanceof Error ? error.message : "创建钉钉定时通知失败" ); } finally { setIsSchedulingDingTalkNotification(false); } }; const handleCancelDingTalkNotificationTask = async (taskId: string) => { setCancellingDingTalkNotificationTaskId(taskId); try { const response = await fetch( `/api/admin/dingtalk-robot/notifications/${taskId}`, { method: "DELETE", } ); const payload = await response.json().catch(() => null); if (!response.ok || payload?.success === false) { throw new Error(payload?.error || "取消定时通知失败"); } toast.success("定时通知已取消"); mutateDingTalkNotificationTasks(); } catch (error) { toast.error( error instanceof Error ? error.message : "取消钉钉通知任务失败" ); } finally { setCancellingDingTalkNotificationTaskId(null); } }; useEffect(() => { if (questionsData) { setQuestions(questionsData); } }, [questionsData]); useEffect(() => { if (canvasSkillTagsData) { setCanvasSkillTagsEnabled(canvasSkillTagsData.enabled !== false); setCanvasSkillTagsTitle( String(canvasSkillTagsData.title || "").trim() || DEFAULT_CANVAS_SKILL_TAGS_TITLE ); setCanvasSkillCategories( normalizeCanvasSkillCategories(canvasSkillTagsData.categories) ); setCanvasSkillTags(normalizeCanvasSkillTags(canvasSkillTagsData.tags)); } }, [canvasSkillTagsData]); const handleQuestionChange = (index: number, value: string) => { const newQuestions = [...questions]; newQuestions[index] = value; setQuestions(newQuestions); }; const handleAddQuestion = () => { setQuestions([...questions, ""]); }; const handleRemoveQuestion = (index: number) => { const newQuestions = questions.filter((_, i) => i !== index); setQuestions(newQuestions); }; const handleSaveQuestions = async () => { try { const res = await fetch("/api/starter-questions", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(questions.filter((q) => q.trim() !== "")), }); if (!res.ok) throw new Error("Failed"); toast.success("快捷短语已更新"); mutateQuestions(); } catch (error) { toast.error("更新失败"); } }; const handleCanvasSkillTagChange = ( index: number, field: | "name" | "prompt" | "placeholder" | "iconName" | "iconUrl" | "categoryId", value: string ) => { setCanvasSkillTags((prev) => prev.map((item, itemIndex) => itemIndex === index ? { ...item, [field]: value } : item ) ); }; const handleCanvasSkillTagIconUpload = async (index: number, file: File) => { if (!file.type.startsWith("image/")) { toast.error("请上传图片文件"); return; } const targetTag = canvasSkillTags[index]; if (!targetTag) return; try { setCanvasSkillTagUploadingId(targetTag.id); const formData = new FormData(); formData.append("file", file); const response = await fetchUploadWithRetry(formData); const result = await response.json(); if (!response.ok || !result?.url) { throw new Error(result?.error || "上传失败"); } setCanvasSkillTags((prev) => prev.map((tag, tagIndex) => tagIndex === index ? { ...tag, iconUrl: result.url } : tag ) ); toast.success("标签图标上传成功"); } catch (error: any) { toast.error(error?.message || "标签图标上传失败"); } finally { setCanvasSkillTagUploadingId(null); } }; const handleRealtimeModeIconUpload = async ( field: RealtimeCanvasModeIconKey, file: File ) => { const isSvg = file.type === "image/svg+xml" || file.name.toLowerCase().endsWith(".svg"); if (!isSvg) { toast.error("请上传 SVG 图标文件"); return; } if (file.size > 512 * 1024) { toast.error("SVG 图标不能超过 512KB"); return; } try { setRealtimeModeIconUploading(field); const formData = new FormData(); formData.append("file", file); formData.append("purpose", "admin-svg-icon"); const response = await fetchUploadWithRetry(formData); const result = await response.json(); if (!response.ok || !result?.url) { throw new Error(result?.error || "上传失败"); } setSiteSettings((prev) => ({ ...prev, realtimeCanvasModeIconConfig: { ...prev.realtimeCanvasModeIconConfig, [field]: result.url, }, })); toast.success("模式图标上传成功,请保存配置后生效"); } catch (error: any) { toast.error(error?.message || "模式图标上传失败"); } finally { setRealtimeModeIconUploading(null); } }; const handleAiWashModeIconUpload = async (file: File) => { const isSvg = file.type === "image/svg+xml" || file.name.toLowerCase().endsWith(".svg"); if (!isSvg) { toast.error("请上传 SVG 图标文件"); return; } if (file.size > 512 * 1024) { toast.error("SVG 图标不能超过 512KB"); return; } try { setAiWashModeIconUploading(true); const formData = new FormData(); formData.append("file", file); formData.append("purpose", "admin-svg-icon"); const response = await fetchUploadWithRetry(formData); const result = await response.json(); if (!response.ok || !result?.url) { throw new Error(result?.error || "上传失败"); } setAiWashConfig((prev) => ({ ...prev, modeIconUrl: result.url, })); toast.success("AI洗图图标上传成功,请保存配置后生效"); } catch (error: any) { toast.error(error?.message || "AI洗图图标上传失败"); } finally { setAiWashModeIconUploading(false); } }; const updateCanvasToolbarItem = ( key: CanvasToolbarItemKey, updates: Partial<{ label: string; iconUrl: string; enabled: boolean }> ) => { setSiteSettings((prev) => { const normalized = normalizeCanvasToolbarConfig(prev.canvasToolbarConfig); return { ...prev, canvasToolbarConfig: { ...normalized, items: { ...normalized.items, [key]: { ...normalized.items[key], ...updates, }, }, }, }; }); }; const handleCanvasToolbarIconUpload = async ( key: CanvasToolbarItemKey, file: File ) => { const isSvg = file.type === "image/svg+xml" || file.name.toLowerCase().endsWith(".svg"); if (!isSvg) { toast.error("请上传 SVG 图标文件"); return; } if (file.size > 512 * 1024) { toast.error("SVG 图标不能超过 512KB"); return; } try { setCanvasToolbarIconUploading(key); const formData = new FormData(); formData.append("file", file); formData.append("purpose", "admin-svg-icon"); const response = await fetchUploadWithRetry(formData); const result = await response.json(); if (!response.ok || !result?.url) { throw new Error(result?.error || "上传失败"); } updateCanvasToolbarItem(key, { iconUrl: result.url }); toast.success("工具栏图标上传成功,请保存配置后生效"); } catch (error: any) { toast.error(error?.message || "工具栏图标上传失败"); } finally { setCanvasToolbarIconUploading(null); } }; const updateLovartHomeSidebarItem = ( key: LovartHomeSidebarItemKey, updates: Partial<{ label: string; iconUrl: string; enabled: boolean }> ) => { setSiteSettings((prev) => { const normalized = normalizeLovartHomeSidebarConfig( prev.lovartHomeSidebarConfig ); return { ...prev, lovartHomeSidebarConfig: { items: { ...normalized.items, [key]: { ...normalized.items[key], ...updates, }, }, }, }; }); }; const handleLovartHomeSidebarIconUpload = async ( key: LovartHomeSidebarItemKey, file: File ) => { const isSvg = file.type === "image/svg+xml" || file.name.toLowerCase().endsWith(".svg"); if (!isSvg) { toast.error("请上传 SVG 图标文件"); return; } if (file.size > 512 * 1024) { toast.error("SVG 图标不能超过 512KB"); return; } try { setLovartHomeSidebarIconUploading(key); const formData = new FormData(); formData.append("file", file); formData.append("purpose", "admin-svg-icon"); const response = await fetchUploadWithRetry(formData); const result = await response.json(); if (!response.ok || !result?.url) { throw new Error(result?.error || "上传失败"); } updateLovartHomeSidebarItem(key, { iconUrl: String(result.url || "").trim(), }); toast.success("首页侧边栏图标上传成功,请保存配置后生效"); } catch (error: any) { toast.error(error?.message || "首页侧边栏图标上传失败"); } finally { setLovartHomeSidebarIconUploading(null); } }; const handleAddCanvasSkillTag = () => { setCanvasSkillTags((prev) => [...prev, createEmptyCanvasSkillTag()]); }; const handleAddCanvasSkillCategory = () => { setCanvasSkillCategories((prev) => [ ...prev, createEmptyCanvasSkillCategory(), ]); }; const handleCanvasSkillCategoryNameChange = (index: number, name: string) => { setCanvasSkillCategories((prev) => prev.map((category, categoryIndex) => categoryIndex === index ? { ...category, name } : category ) ); }; const handleRemoveCanvasSkillCategory = (index: number) => { const categoryId = canvasSkillCategories[index]?.id; setCanvasSkillCategories((prev) => prev.filter((_, categoryIndex) => categoryIndex !== index) ); if (!categoryId) return; setCanvasSkillTags((prev) => prev.map((tag) => { if (tag.categoryId !== categoryId) return tag; const { categoryId: _categoryId, ...rest } = tag; return rest; }) ); }; const handleRemoveCanvasSkillTag = (index: number) => { setCanvasSkillTags((prev) => prev.filter((_, itemIndex) => itemIndex !== index) ); }; const handleSaveCanvasSkillTags = async () => { try { const categories = normalizeCanvasSkillCategories(canvasSkillCategories); const payload = normalizeCanvasSkillTags(canvasSkillTags); const res = await fetch("/api/canvas-skill-tags", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ enabled: canvasSkillTagsEnabled, title: canvasSkillTagsTitle, categories, tags: payload, }), }); if (!res.ok) throw new Error("Failed"); const data = await res.json().catch(() => ({})); setCanvasSkillTagsEnabled(data?.enabled !== false); setCanvasSkillTagsTitle( String(data?.title || canvasSkillTagsTitle || "").trim() || DEFAULT_CANVAS_SKILL_TAGS_TITLE ); setCanvasSkillCategories( normalizeCanvasSkillCategories(data?.categories ?? categories) ); setCanvasSkillTags(normalizeCanvasSkillTags(data?.tags ?? payload)); toast.success("快捷标签已更新"); mutateCanvasSkillTags(); } catch (error) { toast.error("快捷标签更新失败"); } }; // 认证设置状态 const [authSettings, setAuthSettings] = useState({ dingTalkAppKey: "", dingTalkAppSecret: "", googleClientId: "", googleClientSecret: "", authUrl: "", isDingTalkEnabled: false, isGoogleEnabled: false, isRegistrationEnabled: true, isEmailVerificationEnabled: false, emailVerificationExpiresMinutes: 10, resendApiKey: "", resendFromEmail: "", isEmailServiceConfigured: false, dingTalkOrganizations: [], }); const [isGoogleConfigured, setIsGoogleConfigured] = useState(false); useEffect(() => { if (!authSettingsData) return; const organizations: DingTalkOrganization[] = (authSettingsData.dingTalkOrganizations as | DingTalkOrganization[] | null) ?? []; // 如果没有多组织配置,但有单一 AppKey/Secret,则自动构造一个默认组织 const normalizedOrgs = organizations.length > 0 || !authSettingsData.dingTalkAppKey ? organizations : [ { id: "default", name: "默认组织", appKey: authSettingsData.dingTalkAppKey ?? "", appSecret: authSettingsData.dingTalkAppSecret ?? "", }, ]; setAuthSettings({ dingTalkAppKey: authSettingsData.dingTalkAppKey || "", dingTalkAppSecret: authSettingsData.dingTalkAppSecret || "", googleClientId: authSettingsData.googleClientId || "", googleClientSecret: authSettingsData.googleClientSecret || "", authUrl: authSettingsData.authUrl || "", isDingTalkEnabled: authSettingsData.isDingTalkEnabled || false, isGoogleEnabled: authSettingsData.isGoogleEnabled || false, isRegistrationEnabled: authSettingsData.isRegistrationEnabled ?? true, isEmailVerificationEnabled: authSettingsData.isEmailVerificationEnabled === true, emailVerificationExpiresMinutes: Number(authSettingsData.emailVerificationExpiresMinutes) || 10, resendApiKey: authSettingsData.resendApiKey || "", resendFromEmail: authSettingsData.resendFromEmail || "", isEmailServiceConfigured: authSettingsData.isEmailServiceConfigured === true, dingTalkOrganizations: normalizedOrgs, }); setIsGoogleConfigured(Boolean(authSettingsData.isGoogleConfigured)); }, [authSettingsData]); // AI 提供商 & 模型状态 const [providerForm, setProviderForm] = useState({ id: "", name: "", type: "openai" as | "alibaba" | "openai" | "custom" | "local" | "gemini" | "bltcy-video" | "grsai-gpt-image" | "grsai-video" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "bfl-image" | "ruxa-image" | "fal-image" | "snapgen-image" | "openai-image" | "bananaimage" | "visionary" | "atlascloud" | "vip98k" | "stepfun-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "worldlabs-3d" | "kittycad-cad" | "runway" | "krea" | "volcano-multimodal", baseUrl: "", apiKey: "", isEnabled: true, }); const [showProviderDialog, setShowProviderDialog] = useState(false); const [editingProvider, setEditingProvider] = useState(null); const [showApiKey, setShowApiKey] = useState(false); const [modelForm, setModelForm] = useState({ id: "", providerId: "", modelId: "", displayName: "", avatarUrl: "", description: "", supportsVision: false, supportsImageGen: false, isEnabled: true, isHidden: false, isDefault: false, isDefaultTextEditModel: false, isAgentModel: false, agentCodingModelId: "", isMultimodal: false, sortOrder: 0, fallbackModelId1: "", // 备用模型 ID 1 fallbackModelId2: "", // 备用模型 ID 2 excludeFromFailover: false, excludeFromHealthBan: false, capabilities: {} as Record, // 🆕 图片生成模型特性配置 imageGenConfig: createDefaultImageGenConfig(), }); const [videoDurationOptionsInput, setVideoDurationOptionsInput] = useState(""); const [showModelDialog, setShowModelDialog] = useState(false); const [editingModel, setEditingModel] = useState(null); const [modelAvatarUploading, setModelAvatarUploading] = useState(false); const [contactQrUploading, setContactQrUploading] = useState(false); // 🆕 火山引擎视频多模型表单(动态从API加载) 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 ) { // 从API获取预设模型配置(✅ 显示所有模型供管理员自定义) 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, // 标记是否为子模型 })); setVolcanoVideoModels(allModels); } }) .catch((err) => { console.error("[Load Volcano Models] Error:", err); toast.error("加载预设模型配置失败"); }); } }, [ modelForm.providerId, providersData?.providers, volcanoVideoModels.length, ]); const selectedModelProviderType = providersData?.providers?.find((p) => p.id === modelForm.providerId) ?.type || ""; const failoverModelOptions = useMemo(() => { const currentModelId = String(editingModel || modelForm.id || "").trim(); const currentRuntimeModelId = String(modelForm.modelId || "").trim(); const wantsImageFallback = modelForm.supportsImageGen; return (modelsData?.models || []) .filter((model) => { if (!model.isEnabled || model.isSubModel || model.excludeFromFailover) return false; if ( model.id === currentModelId || model.modelId === currentRuntimeModelId ) return false; if (wantsImageFallback && !model.supportsImageGen) return false; return true; }) .map((model) => ({ ...model, providerName: modelProviderNameById.get(model.providerId) || "未知服务商", })); }, [ editingModel, modelForm.id, modelForm.modelId, modelForm.supportsImageGen, modelProviderNameById, modelsData?.models, ]); const isVideoProviderForModelConfig = isVideoModelDescriptor({ providerType: selectedModelProviderType, modelId: modelForm.modelId, capabilities: modelForm.capabilities, supportsImageGen: modelForm.supportsImageGen, }); const is3DProviderForModelConfig = is3DModelDescriptor({ providerType: selectedModelProviderType, modelId: modelForm.modelId, capabilities: modelForm.capabilities, }); const isImageProviderForModelConfig = isImageGenerationModelDescriptor({ providerType: selectedModelProviderType, modelId: modelForm.modelId, capabilities: modelForm.capabilities, supportsImageGen: modelForm.supportsImageGen, }); const modelVideoSupportedResolutions = getAdminVideoSupportedResolutions( modelForm.capabilities ); const configuredVideoDurationOptions = getConfiguredVideoDurationOptions( modelForm.imageGenConfig ); const videoReferenceDurationCost = getVideoReferenceDurationCost( modelForm.imageGenConfig ); const modelKindHint = (selectedModelProviderType === "runway" || selectedModelProviderType === "krea") && !modelForm.modelId.trim() ? `${ selectedModelProviderType === "krea" ? "Krea" : "Runway" } 同时支持图片与视频模型,填写模型 ID 后系统会自动识别标签。` : is3DProviderForModelConfig ? "当前识别为 3D 模型,将与图片、视频和对话模型分开管理。" : isVideoProviderForModelConfig ? "当前识别为视频模型,将使用视频能力、视频积分和视频路由。" : isImageProviderForModelConfig ? "当前识别为图片模型,将使用生图能力、图片积分和生图路由。" : "当前识别为对话/分析模型。"; // 存储配置状态 const [storageForm, setStorageForm] = useState({ id: "", name: "", type: "s3" as "s3" | "local" | "aliyun-oss" | "qiniu-kodo", endpoint: "", region: "", bucket: "", accessKeyId: "", secretAccessKey: "", publicUrl: "", isDefault: false, isEnabled: true, }); const [showStorageDialog, setShowStorageDialog] = useState(false); const [editingStorage, setEditingStorage] = useState(null); // --- 用户管理逻辑 --- const handleUserSearch = (search: string) => { setUserSearch(search); setUserPage(1); // 搜索时重置到第一页 }; const handleUpdateUser = async (userId: string, updates: any) => { try { const response = await fetch("/api/admin/users", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: userId, ...updates }), }); if (!response.ok) throw new Error("更新失败"); toast.success("用户更新成功"); mutateUsers(); } catch (error) { toast.error("更新用户失败"); } }; const handleGiftPoints = async (userId: string) => { const rawValue = giftPointsDraftByUser[userId] || ""; const points = Number.parseInt(rawValue, 10); if (!Number.isFinite(points) || points <= 0) { toast.error("请输入有效的赠送积分(大于 0)"); return; } try { const response = await fetch("/api/admin/users", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: userId, giftPoints: points }), }); const data = await response.json(); if (!response.ok) { throw new Error(data?.message || data?.error || "赠送失败"); } toast.success(`已赠送 ${points} 积分`); setGiftPointsDraftByUser((prev) => ({ ...prev, [userId]: "" })); mutateUsers(); } catch (error) { toast.error(error instanceof Error ? error.message : "赠送积分失败"); } }; const handleDeductPoints = async (userId: string) => { const rawValue = deductPointsDraftByUser[userId] || ""; const points = Number.parseInt(rawValue, 10); if (!Number.isFinite(points) || points <= 0) { toast.error("请输入有效的扣减积分(大于 0)"); return; } try { const response = await fetch("/api/admin/users", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: userId, deductPoints: points }), }); const data = await response.json(); if (!response.ok) { throw new Error(data?.message || data?.error || "扣减失败"); } toast.success(`已扣减 ${points} 积分`); setDeductPointsDraftByUser((prev) => ({ ...prev, [userId]: "" })); mutateUsers(); } catch (error) { toast.error(error instanceof Error ? error.message : "扣减积分失败"); } }; const handleDeleteUser = async (userId: string) => { if (!confirm("确定要删除此用户吗?")) return; try { const response = await fetch(`/api/admin/users?id=${userId}`, { method: "DELETE", }); if (!response.ok) throw new Error("删除失败"); toast.success("用户删除成功"); mutateUsers(); } catch (error) { toast.error("删除用户失败"); } }; // 批量设置出图限额 const handleBatchSetLimit = async () => { try { const response = await fetch("/api/admin/users/batch-limit", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type: batchLimitType, limit: batchLimitValue === "" ? null : Number.parseInt(batchLimitValue, 10), department: batchLimitDepartment, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "批量设置失败"); } toast.success(data.message); setShowBatchLimitDialog(false); setBatchLimitValue(""); setBatchLimitDepartment(""); mutateUsers(); } catch (error) { toast.error(error instanceof Error ? error.message : "批量设置失败"); } }; const handleSaveOpenClawChatConfig = async () => { try { const normalizedConfig: OpenClawChatConfig = { enabled: openClawChatConfig.enabled, baseUrl: openClawChatConfig.baseUrl.trim() || DEFAULT_OPENCLAW_CHAT_BASE_URL, token: openClawChatConfig.token.trim(), timeoutMs: Math.min( 600_000, Math.max(5_000, Math.round(Number(openClawChatConfig.timeoutMs) || 600_000)) ), }; if (normalizedConfig.enabled) { if (!normalizedConfig.baseUrl) { toast.error("请填写大龙虾聊天接口地址"); return; } if (!normalizedConfig.token && !openClawChatTokenConfigured) { toast.error("请填写大龙虾聊天接口 Token"); return; } } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: OPENCLAW_CHAT_CONFIG_KEY, value: normalizedConfig, }), }); const responseData = await response.json().catch(() => null); if (!response.ok) { throw new Error(responseData?.message || responseData?.error || "保存失败"); } const savedValue = responseData?.setting?.value && typeof responseData.setting.value === "object" ? (responseData.setting.value as OpenClawChatConfig) : normalizedConfig; setOpenClawChatConfig({ enabled: savedValue.enabled, baseUrl: savedValue.baseUrl, token: "", timeoutMs: savedValue.timeoutMs, }); setOpenClawChatTokenConfigured( Boolean((responseData?.setting?.value as any)?.hasToken) || Boolean(normalizedConfig.token) ); toast.success("大龙虾聊天接口配置已保存"); mutateOpenClawChatSetting(); mutateSiteSettings(); } catch (error) { toast.error( error instanceof Error ? error.message : "保存大龙虾聊天接口配置失败" ); } }; // --- 网站设置逻辑 --- const handleSaveSiteSettings = async () => { try { const normalizedHomeAnimationHtml = homeAnimationConfig.htmlContent .replace(/^\uFEFF/, "") .replace(/\u0000/g, "") .replace(/\u2028|\u2029/g, "\n"); const homeAnimationBytes = new TextEncoder().encode( normalizedHomeAnimationHtml ).length; if (homeAnimationBytes > HOME_CHAT_ANIMATION_MAX_BYTES) { toast.error( `首页动画 HTML 过大,当前 ${(homeAnimationBytes / 1024).toFixed(1)}KB,最大 ${( HOME_CHAT_ANIMATION_MAX_BYTES / 1024 ).toFixed(0)}KB` ); return; } if ( siteSettings.storageFailoverConfig.enabled && !siteSettings.storageFailoverConfig.backupStorageConfigId ) { toast.error("请先选择备用存储配置"); return; } const selectedBackupConfig = (storageData?.configs || []).find( (config) => config.id === siteSettings.storageFailoverConfig.backupStorageConfigId ); if ( siteSettings.storageFailoverConfig.enabled && selectedBackupConfig?.isDefault ) { toast.error("备用存储配置不能与默认配置相同"); return; } if (siteSettings.ossImageMigrationConfig.enabled) { if ( siteSettings.ossImageMigrationConfig.cleanupAfterHours <= siteSettings.ossImageMigrationConfig.migrateAfterHours ) { toast.error("OSS 清理时间必须晚于转存时间"); return; } if ( siteSettings.ossImageMigrationConfig.sourceStorageConfigId && siteSettings.ossImageMigrationConfig.targetStorageConfigId && siteSettings.ossImageMigrationConfig.sourceStorageConfigId === siteSettings.ossImageMigrationConfig.targetStorageConfigId ) { toast.error("OSS 源桶和目标桶不能相同"); return; } const targetConfig = (storageData?.configs || []).find( (config) => config.id === siteSettings.ossImageMigrationConfig.targetStorageConfigId ); if ( targetConfig && targetConfig.type === "s3" && targetConfig.endpoint?.includes("aliyuncs.com") ) { toast.error("OSS 转存目标桶不能是阿里云 OSS"); return; } } const parsedMembershipConfig = normalizeMembershipConfig( siteSettings.membershipConfig ); const parsedMembershipOverrides = normalizeMembershipUserOverrides( siteSettings.membershipUserOverrides ); const parsedPointsPackages = normalizePointsPackages( siteSettings.pointsPackages ); const parsedPointsSystemConfig = normalizePointsSystemConfig( siteSettings.pointsSystemConfig ); const normalizedHomeAnimationConfig: HomeAnimationConfig = { enabled: homeAnimationConfig.enabled, fileName: homeAnimationConfig.fileName.trim(), htmlContent: normalizedHomeAnimationHtml, updatedAt: homeAnimationConfig.updatedAt, }; const siteSettingsPayload: Partial = { ...siteSettings, membershipConfig: parsedMembershipConfig, membershipUserOverrides: parsedMembershipOverrides, pointsSystemConfig: parsedPointsSystemConfig, pointsPackages: parsedPointsPackages, }; delete siteSettingsPayload.agentWorkflowUiConfig; const changedSiteSettingsPayload = pickChangedTopLevelFields( siteSettingsPayload as Record, lastLoadedSiteSettingsRef.current as Partial< Record > | null ) as Partial; const shouldSaveHomeAnimation = !areComparableValuesEqual( normalizedHomeAnimationConfig, lastLoadedHomeAnimationConfigRef.current ); const payload = { ...changedSiteSettingsPayload, ...(shouldSaveHomeAnimation ? { homeAnimationConfig: normalizedHomeAnimationConfig } : {}), }; if (Object.keys(payload).length === 0) { toast.info("没有需要保存的改动"); return; } const response = await fetch("/api/site-settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const responseData = await response.json().catch(() => null); if (!response.ok) throw new Error(responseData?.error || "保存失败"); const { homeAnimationConfig: _ignoredHomeAnimationConfig, ...nextSiteSettings } = payload; setSiteSettings((prev) => ({ ...prev, ...nextSiteSettings, })); lastLoadedSiteSettingsRef.current = { ...(lastLoadedSiteSettingsRef.current || siteSettings), ...siteSettingsPayload, }; if (shouldSaveHomeAnimation) { const committedHomeAnimationConfig: HomeAnimationConfig = { ...normalizedHomeAnimationConfig, updatedAt: new Date().toISOString(), }; setHomeAnimationConfig(committedHomeAnimationConfig); lastLoadedHomeAnimationConfigRef.current = committedHomeAnimationConfig; } toast.success("网站设置保存成功"); mutateSiteSettings(); if (shouldSaveHomeAnimation) { mutateHomeAnimationSetting(); } } catch (error) { toast.error(error instanceof Error ? error.message : "保存网站设置失败"); } }; const handleImportHomeAnimationFile = async ( event: ChangeEvent ) => { const file = event.target.files?.[0]; event.target.value = ""; if (!file) { return; } const normalizedFileName = String(file.name || "").trim(); const isHtmlFile = file.type === "text/html" || /\.html?$/i.test(normalizedFileName); if (!isHtmlFile) { toast.error("请上传 .html 文件"); return; } try { const htmlContent = (await file.text()) .replace(/^\uFEFF/, "") .replace(/\u0000/g, "") .replace(/\u2028|\u2029/g, "\n"); const bytes = new TextEncoder().encode(htmlContent).length; if (bytes > HOME_CHAT_ANIMATION_MAX_BYTES) { toast.error( `HTML 文件过大,当前 ${(bytes / 1024).toFixed(1)}KB,最大 ${( HOME_CHAT_ANIMATION_MAX_BYTES / 1024 ).toFixed(0)}KB` ); return; } setHomeAnimationConfig((prev) => ({ ...prev, fileName: normalizedFileName, htmlContent, updatedAt: null, })); toast.success("HTML 动效文件已导入,记得点击保存设置"); } catch (error) { toast.error("读取 HTML 文件失败"); } }; const updatePointsPackage = ( index: number, updater: (item: PointsPackage) => PointsPackage ) => { setSiteSettings((prev) => { const list = [...(prev.pointsPackages || [])]; if (!list[index]) return prev; list[index] = updater(list[index]); return { ...prev, pointsPackages: list }; }); }; const addPointsPackage = () => { setSiteSettings((prev) => { const list = [...(prev.pointsPackages || [])]; const nextIndex = list.length + 1; list.push({ id: `package-${Date.now()}`, name: `套餐${nextIndex}`, points: 1000, price: 9.9, currency: "CNY", isEnabled: true, }); return { ...prev, pointsPackages: list }; }); }; const removePointsPackage = (index: number) => { setSiteSettings((prev) => { const list = [...(prev.pointsPackages || [])]; if (list.length <= 1) { toast.error("至少保留一个积分套餐"); return prev; } list.splice(index, 1); return { ...prev, pointsPackages: list }; }); }; const updatePointsCostField = (key: keyof PointsSystemConfig, value: unknown) => { setSiteSettings((prev) => { const config = normalizePointsSystemConfig(prev.pointsSystemConfig); return { ...prev, pointsSystemConfig: { ...config, [key]: roundCreditCost(value), }, }; }); }; const updateEnhanceProPointsBucket = ( index: number, updater: ( item: EnhanceProPointsResolutionBucket ) => EnhanceProPointsResolutionBucket ) => { setSiteSettings((prev) => { const config = normalizePointsSystemConfig(prev.pointsSystemConfig); const list = [...config.enhanceProPricing.resolutionBuckets]; if (!list[index]) return prev; list[index] = updater(list[index]); return { ...prev, pointsSystemConfig: { ...config, enhanceProPricing: { ...config.enhanceProPricing, resolutionBuckets: list, }, }, }; }); }; const addEnhanceProPointsBucket = () => { setSiteSettings((prev) => { const config = normalizePointsSystemConfig(prev.pointsSystemConfig); const list = [...config.enhanceProPricing.resolutionBuckets]; const lastBucket = list[list.length - 1]; const nextMaxMegapixels = Math.min( 100, Math.max( 1, Number((Number(lastBucket?.maxMegapixels || 8) * 2).toFixed(2)) ) ); list.push({ id: `enhance-pro-bucket-${Date.now()}`, name: `${nextMaxMegapixels}MP 以内`, maxMegapixels: nextMaxMegapixels, cost: Math.max(1, Number(lastBucket?.cost || 45)), }); return { ...prev, pointsSystemConfig: { ...config, enhanceProPricing: { ...config.enhanceProPricing, resolutionBuckets: list, }, }, }; }); }; const removeEnhanceProPointsBucket = (index: number) => { setSiteSettings((prev) => { const config = normalizePointsSystemConfig(prev.pointsSystemConfig); const list = [...config.enhanceProPricing.resolutionBuckets]; if (list.length <= 1) { toast.error("至少保留一个分辨率档位"); return prev; } list.splice(index, 1); return { ...prev, pointsSystemConfig: { ...config, enhanceProPricing: { ...config.enhanceProPricing, resolutionBuckets: list, }, }, }; }); }; const updateEnhanceProFixedCost = (nextFixedCost: number) => { setSiteSettings((prev) => { const config = normalizePointsSystemConfig(prev.pointsSystemConfig); const pricing = config.enhanceProPricing; const fixedCost = roundCreditCost(nextFixedCost); const scaleFactorCosts = Object.fromEntries( Object.entries(pricing.scaleFactorCosts).map(([factorKey, cost]) => { const multiplier = getEnhanceProScaleMultiplier( pricing, factorKey, Number(cost) ); return [factorKey, roundCreditCost(fixedCost * multiplier)]; }) ); return { ...prev, pointsSystemConfig: { ...config, enhanceProPricing: { ...pricing, fixedCost, scaleFactorCosts, }, }, }; }); }; const updateEnhanceProScaleFactorMultiplier = ( factorKey: string, nextMultiplier: number ) => { setSiteSettings((prev) => { const config = normalizePointsSystemConfig(prev.pointsSystemConfig); const pricing = config.enhanceProPricing; const fixedCost = roundCreditCost(pricing.fixedCost); const multiplier = normalizeAdminMultiplier( nextMultiplier, Number(factorKey) || 1 ); return { ...prev, pointsSystemConfig: { ...config, enhanceProPricing: { ...pricing, scaleFactorCosts: { ...pricing.scaleFactorCosts, [factorKey]: roundCreditCost(fixedCost * multiplier), }, }, }, }; }); }; const updateMembershipConfigState = ( updater: ( config: ReturnType ) => ReturnType ) => { setSiteSettings((prev) => ({ ...prev, membershipConfig: updater(prev.membershipConfig), })); }; const updateMembershipOverridesState = ( updater: ( overrides: ReturnType ) => ReturnType ) => { setSiteSettings((prev) => ({ ...prev, membershipUserOverrides: updater(prev.membershipUserOverrides), })); }; const updateMembershipPlan = ( index: number, updater: (plan: MembershipPlanConfig) => MembershipPlanConfig ) => { updateMembershipConfigState((config) => { const plans = [...config.plans]; if (!plans[index]) return config; plans[index] = updater(plans[index]); return { ...config, plans }; }); }; const addMembershipPlan = () => { updateMembershipConfigState((config) => { const nextIndex = config.plans.length + 1; const id = `plan-${Date.now()}`; const plans = [ ...config.plans, { id, name: `套餐${nextIndex}`, monthlyPrice: 0, annualPrice: 0, monthlyCredits: 0, dailyFreeCredits: 0, features: [], }, ]; return { ...config, plans, defaultPlanId: config.defaultPlanId || id, }; }); }; const removeMembershipPlan = (index: number) => { updateMembershipConfigState((config) => { if (config.plans.length <= 1) { toast.error("至少保留一个会员套餐"); return config; } const target = config.plans[index]; if (!target) return config; const plans = [...config.plans]; plans.splice(index, 1); const nextDefaultPlanId = config.defaultPlanId === target.id ? plans[0]?.id || config.defaultPlanId : config.defaultPlanId; return { ...config, plans, defaultPlanId: nextDefaultPlanId, }; }); }; const updateMembershipFaq = ( index: number, updater: (faq: MembershipFaqItem) => MembershipFaqItem ) => { updateMembershipConfigState((config) => { const faqs = [...config.faqs]; if (!faqs[index]) return config; faqs[index] = updater(faqs[index]); return { ...config, faqs }; }); }; const addMembershipFaq = () => { updateMembershipConfigState((config) => ({ ...config, faqs: [...config.faqs, { question: "", answer: "" }], })); }; const removeMembershipFaq = (index: number) => { updateMembershipConfigState((config) => { if (config.faqs.length <= 1) { toast.error("至少保留一条 FAQ"); return config; } const faqs = [...config.faqs]; faqs.splice(index, 1); return { ...config, faqs }; }); }; const addMembershipOverride = (kind: "id" | "email") => { const key = kind === "id" ? newOverrideUserId.trim() : newOverrideEmail.trim().toLowerCase(); const planId = newOverridePlanId.trim(); if (!key || !planId) { toast.error("请先填写标识并选择套餐"); return; } updateMembershipOverridesState((overrides) => { if (kind === "id") { return { ...overrides, userPlansById: { ...overrides.userPlansById, [key]: planId, }, }; } return { ...overrides, userPlansByEmail: { ...overrides.userPlansByEmail, [key]: planId, }, }; }); if (kind === "id") { setNewOverrideUserId(""); } else { setNewOverrideEmail(""); } }; const removeMembershipOverride = (kind: "id" | "email", key: string) => { updateMembershipOverridesState((overrides) => { if (kind === "id") { const next = { ...overrides.userPlansById }; delete next[key]; return { ...overrides, userPlansById: next }; } const next = { ...overrides.userPlansByEmail }; delete next[key]; return { ...overrides, userPlansByEmail: next }; }); }; // --- 抠图API配置逻辑 --- const handleSaveRmbgConfig = async () => { try { // 根据选择的提供商验证配置 if (rmbgConfig.provider === "rmbg" && !rmbgConfig.apiUrl.trim()) { toast.error("请填写RMBG API地址"); return; } if ( rmbgConfig.provider === "photoroom" && !rmbgConfig.photoroomApiKey.trim() ) { toast.error("请填写PhotoRoom API密钥"); return; } if (rmbgConfig.provider === "aoscdn" && !rmbgConfig.aosApiKey.trim()) { toast.error("请填写AOS API密钥"); return; } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "rmbg_api_config", value: { provider: rmbgConfig.provider, apiUrl: rmbgConfig.apiUrl.trim(), 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, aosCrop: rmbgConfig.aosCrop ? 1 : 0, aosFormat: rmbgConfig.aosFormat, }, }), }); if (!response.ok) throw new Error("保存失败"); toast.success("抠图API配置保存成功"); } catch (error) { toast.error("保存抠图API配置失败"); } }; // --- 变清晰API配置逻辑 --- const handleSaveEnhanceConfig = async () => { try { // 如果启用了备用API,验证备用API配置 if (enhanceConfig.provider === "vip98k") { // VIP98K credentials are read from AiProvider and never duplicated here. } else if (enhanceConfig.useBackupApi) { if (!enhanceConfig.backupApiUrl.trim()) { toast.error("请输入备用API地址"); return; } if (!enhanceConfig.backupApiKey.trim()) { toast.error("请输入备用API密钥"); return; } } else { // 如果使用主API,验证主API配置 if (!enhanceConfig.apiUrl.trim()) { toast.error("请输入API地址"); return; } if (!enhanceConfig.apiKey.trim()) { toast.error("请输入API密钥"); return; } } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "enhance_api_config", value: { provider: enhanceConfig.provider, apiUrl: enhanceConfig.apiUrl.trim(), apiKey: enhanceConfig.apiKey.trim(), type: enhanceConfig.type, scaleFactor: enhanceConfig.scaleFactor && enhanceConfig.scaleFactor !== "auto" ? Number.parseInt(enhanceConfig.scaleFactor) : undefined, // 备用API配置 useBackupApi: enhanceConfig.useBackupApi, backupApiUrl: enhanceConfig.backupApiUrl.trim(), backupApiKey: enhanceConfig.backupApiKey.trim(), backupScale: enhanceConfig.backupScale, backupModel: enhanceConfig.backupModel, }, }), }); if (!response.ok) throw new Error("保存失败"); toast.success("变清晰API配置保存成功"); } catch (error) { toast.error("保存变清晰API配置失败"); } }; const handleSaveEnhanceProConfig = async () => { try { const hasEnhanceProApiKeysDraft = enhanceProApiKeysDraft.trim().length > 0; const normalizedConfig = normalizeEnhanceProKreaConfig({ ...enhanceProConfig, providerName: enhanceProConfig.providerName.trim() || "Krea Topaz Generative", baseUrl: enhanceProConfig.baseUrl.trim() || ENHANCE_PRO_KREA_DEFAULT_BASE_URL, apiKey: enhanceProConfig.apiKey.trim(), apiKeys: hasEnhanceProApiKeysDraft ? enhanceProApiKeysDraft : enhanceProConfig.apiKeys, }); if (enhanceProConfig.enabled) { if (!normalizedConfig.baseUrl) { toast.error("请输入 Krea API 地址"); return; } if (!normalizedConfig.apiKey && normalizedConfig.apiKeys.length === 0) { toast.error("请输入 Krea API Key 或 Key 池"); return; } } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: ENHANCE_PRO_KREA_SETTING_KEY, value: normalizedConfig, }), }); const payload = await response.json().catch(() => ({})); if (!response.ok) { throw new Error( String(payload?.message || payload?.error || "保存失败") ); } mutateEnhanceProConfig(); toast.success("图像增强 Pro 配置保存成功"); } catch (error) { toast.error( error instanceof Error ? error.message : "保存图像增强 Pro 配置失败" ); } }; const handleAddEnhanceProPoolKey = () => { const normalizedKey = normalizeEnhanceProKreaConfig({ apiKey: enhanceProConfig.apiKey, }).apiKey; if (!normalizedKey) { toast.error("请先填写要加入 Key 池的 Krea Bearer Token"); return; } setEnhanceProConfig((prev) => { const nextKeys = normalizeEnhanceProKreaConfig({ apiKeys: [...prev.apiKeys, normalizedKey], }).apiKeys; return { ...prev, apiKey: "", apiKeys: nextKeys, }; }); setEnhanceProApiKeysDraft(""); toast.success("Krea Key 已加入池,保存配置后生效"); }; const handleSaveAiWashConfig = async () => { try { const normalizedConfig = normalizeAiWashConfig(aiWashConfig); const enabledPresetCount = normalizedConfig.presets.filter( (preset) => preset.enabled ).length; if (normalizedConfig.enabled && enabledPresetCount === 0) { toast.error("请至少启用一个 AI洗图预设"); return; } if ( normalizedConfig.enabled && normalizedConfig.presets.some( (preset) => preset.enabled && !preset.prompt.trim() ) ) { toast.error("启用中的 AI洗图预设至少需要配置基础提示词"); return; } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: AI_WASH_CONFIG_KEY, value: normalizedConfig, }), }); const payload = await response.json().catch(() => ({})); if (!response.ok) { throw new Error( String(payload?.message || payload?.error || "保存失败") ); } mutateAiWashConfig(); toast.success("AI洗图配置保存成功"); } catch (error) { toast.error( error instanceof Error ? error.message : "保存 AI洗图配置失败" ); } }; const handleSaveCanvasLocalEditConfig = async () => { try { const normalizedConfig = normalizeCanvasLocalEditConfig(canvasLocalEditConfig); const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: CANVAS_LOCAL_EDIT_CONFIG_KEY, value: normalizedConfig, }), }); const payload = await response.json().catch(() => ({})); if (!response.ok) { throw new Error( String(payload?.message || payload?.error || "保存失败") ); } mutateCanvasLocalEditConfig(); toast.success("局部编辑模型配置保存成功"); } catch (error) { toast.error( error instanceof Error ? error.message : "保存局部编辑模型配置失败" ); } }; const handleDeleteEnhanceProPoolKey = (index: number) => { setEnhanceProConfig((prev) => ({ ...prev, apiKeys: prev.apiKeys.filter((_, itemIndex) => itemIndex !== index), })); }; const handleToggleEnhanceProModelVisible = ( model: EnhanceProKreaModel, visible: boolean ) => { setEnhanceProConfig((prev) => { const hiddenModels = new Set(prev.hiddenModels || []); if (visible) { hiddenModels.delete(model); } else { hiddenModels.add(model); } const nextHiddenModels = ENHANCE_PRO_KREA_MODELS.filter((item) => hiddenModels.has(item) ); const visibleModels = ENHANCE_PRO_KREA_MODELS.filter( (item) => !nextHiddenModels.includes(item) ); if (visibleModels.length === 0) { toast.error("至少需要保留一个前台可见模型"); return prev; } return { ...prev, hiddenModels: nextHiddenModels, defaultModel: visibleModels.includes(prev.defaultModel) ? prev.defaultModel : visibleModels[0] || ENHANCE_PRO_KREA_DEFAULT_MODEL, }; }); }; const handleEnhanceProPresetChange = ( index: number, key: K, value: EnhanceProPreset[K] ) => { setEnhanceProConfig((prev) => { const nextPresets = prev.presets.map((preset, presetIndex) => { if (presetIndex !== index) { if (key === "isDefault" && value === true) { return { ...preset, isDefault: false }; } return preset; } const nextPreset = { ...preset, [key]: value, } as EnhanceProPreset; if ( key === "name" && !nextPreset.id.startsWith("enhance-pro-preset-") ) { return nextPreset; } if (key === "name") { return { ...nextPreset, id: String(value || "") .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") || nextPreset.id, }; } return nextPreset; }); if ( key === "isDefault" && !nextPresets.some((preset) => preset.isDefault) ) { return { ...prev, presets: nextPresets.map((preset, presetIndex) => ({ ...preset, isDefault: presetIndex === 0, })), }; } return { ...prev, presets: nextPresets, }; }); }; const handleAddEnhanceProPreset = () => { const nextPreset = createEmptyEnhanceProPreset(); setSelectedEnhanceProPresetId(nextPreset.id); setEnhanceProConfig((prev) => ({ ...prev, presets: [ ...prev.presets, { ...nextPreset, isDefault: prev.presets.length === 0, }, ], })); }; const handleRemoveEnhanceProPreset = (index: number) => { setEnhanceProConfig((prev) => { const nextPresets = prev.presets.filter( (_, presetIndex) => presetIndex !== index ); if (nextPresets.length === 0) { return { ...prev, presets: [], }; } if (nextPresets.some((preset) => preset.isDefault)) { return { ...prev, presets: nextPresets, }; } return { ...prev, presets: nextPresets.map((preset, presetIndex) => ({ ...preset, isDefault: presetIndex === 0, })), }; }); }; const handleResetEnhanceProPresets = () => { setEnhanceProConfig((prev) => ({ ...prev, presets: defaultEnhanceProPresets(), })); }; const handleMoveEnhanceProPreset = (fromIndex: number, toIndex: number) => { setEnhanceProConfig((prev) => ({ ...prev, presets: reorderEnhanceProPresets(prev.presets, fromIndex, toIndex), })); }; const handleEnhanceProPresetDragStart = ( event: DragEvent, presetId: string ) => { setDraggingEnhanceProPresetId(presetId); event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", presetId); }; const handleEnhanceProPresetDragEnd = () => { setDraggingEnhanceProPresetId(""); }; const handleEnhanceProPresetDrop = ( event: DragEvent, targetPresetId: string ) => { event.preventDefault(); const sourcePresetId = draggingEnhanceProPresetId || event.dataTransfer.getData("text/plain"); setDraggingEnhanceProPresetId(""); if (!sourcePresetId || sourcePresetId === targetPresetId) { return; } const fromIndex = enhanceProConfig.presets.findIndex( (preset) => preset.id === sourcePresetId ); const toIndex = enhanceProConfig.presets.findIndex( (preset) => preset.id === targetPresetId ); if (fromIndex < 0 || toIndex < 0) { return; } handleMoveEnhanceProPreset(fromIndex, toIndex); }; // --- 图像拓展API配置逻辑 --- const handleSaveExtendConfig = async () => { try { if (extendConfig.provider !== "vip98k" && !extendConfig.apiKey.trim()) { toast.error("请输入API密钥"); return; } const xScale = Number.parseFloat(extendConfig.xScale); const yScale = Number.parseFloat(extendConfig.yScale); if (isNaN(xScale) || xScale < 1.0 || xScale > 2.0) { toast.error("X轴放大倍数必须在1.0-2.0之间"); return; } if (isNaN(yScale) || yScale < 1.0 || yScale > 2.0) { toast.error("Y轴放大倍数必须在1.0-2.0之间"); return; } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "extend_api_config", value: { provider: extendConfig.provider, apiKey: extendConfig.apiKey.trim(), xScale, yScale, }, }), }); if (!response.ok) throw new Error("保存失败"); toast.success("图像拓展API配置保存成功"); } catch (error) { toast.error("保存图像拓展API配置失败"); } }; // --- 去水印API配置逻辑 --- const handleSaveWatermarkRemoveConfig = async () => { try { if (!watermarkRemoveConfig.apiKey.trim()) { toast.error("请输入PicWish API密钥"); return; } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "watermark_remove_config", value: { apiKey: watermarkRemoveConfig.apiKey.trim(), }, }), }); if (!response.ok) throw new Error("保存失败"); toast.success("去水印API配置保存成功"); } catch (error) { toast.error("保存去水印API配置失败"); } }; // --- 角度编辑API配置逻辑 --- const handleSaveAngleEditConfig = async () => { try { if (angleEditConfig.enabled && !angleEditConfig.falApiKey.trim()) { toast.error("启用角度编辑功能需要配置 fal.ai API Key"); return; } const normalizedPointsSystemConfig = normalizePointsSystemConfig( siteSettings.pointsSystemConfig ); const [angleResponse, pointsResponse] = await Promise.all([ fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "angle_edit_config", value: { enabled: angleEditConfig.enabled, falApiKey: angleEditConfig.falApiKey.trim(), loraScale: angleEditConfig.loraScale, inferenceSteps: angleEditConfig.inferenceSteps, guidanceScale: angleEditConfig.guidanceScale, maxImageSize: angleEditConfig.maxImageSize, timeout: angleEditConfig.timeout, }, }), }), fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: POINTS_SYSTEM_CONFIG_KEY, value: normalizedPointsSystemConfig, }), }), ]); if (!angleResponse.ok || !pointsResponse.ok) throw new Error("保存失败"); toast.success("角度编辑API配置保存成功"); } catch (error) { toast.error("保存角度编辑API配置失败"); } }; const handleSaveLayerSplitConfig = async () => { try { if (layerSplitConfig.enabled) { if (!layerSplitConfig.replicateApiToken.trim()) { toast.error("启用图层拆分需要配置 Replicate API Token"); return; } if (!layerSplitConfig.replicateModel.trim()) { toast.error("请填写 Replicate 模型"); return; } } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "layer_split_config", value: { 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) ), numInferenceSteps: Math.max( 1, Math.min(80, Number(layerSplitConfig.numInferenceSteps) || 30) ), cfgNormalize: layerSplitConfig.cfgNormalize, useEnPrompt: layerSplitConfig.useEnPrompt, negativePrompt: layerSplitConfig.negativePrompt.trim(), timeoutMs: Math.max( 30_000, Math.min(900_000, Number(layerSplitConfig.timeoutMs) || 300_000) ), }, }), }); if (!response.ok) throw new Error("保存失败"); toast.success("图层拆分API配置保存成功"); } catch (error) { toast.error("保存图层拆分API配置失败"); } }; const handleSaveProductPlanningApiConfig = async () => { try { const [configResponse, featureResponse] = await Promise.all([ fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: PRODUCT_PLANNING_API_CONFIG_KEY, value: { oneboundBaseUrl: productPlanningApiConfig.oneboundBaseUrl.trim(), taobaoApiKey: productPlanningApiConfig.taobaoApiKey.trim(), taobaoApiSecret: productPlanningApiConfig.taobaoApiSecret.trim(), detailPlusBaseUrl: productPlanningApiConfig.detailPlusBaseUrl.trim(), detailPlusInternalBaseUrl: productPlanningApiConfig.detailPlusInternalBaseUrl.trim(), detailPlusApiKey: productPlanningApiConfig.detailPlusApiKey.trim(), qaApiUrl: productPlanningApiConfig.qaApiUrl.trim(), qaToken: productPlanningApiConfig.qaToken.trim(), saleApiUrl: productPlanningApiConfig.saleApiUrl.trim(), saleToken: productPlanningApiConfig.saleToken.trim(), planningDbHost: productPlanningApiConfig.planningDbHost.trim(), planningDbPort: productPlanningApiConfig.planningDbPort.trim(), planningDbDatabase: productPlanningApiConfig.planningDbDatabase.trim(), planningDbUser: productPlanningApiConfig.planningDbUser.trim(), planningDbPassword: productPlanningApiConfig.planningDbPassword.trim(), planningDbCharset: productPlanningApiConfig.planningDbCharset.trim(), planningDbSsl: productPlanningApiConfig.planningDbSsl, }, }), }), fetch("/api/site-settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ productPlanningEnabled: siteSettings.productPlanningEnabled, }), }), ]); const configPayload = await configResponse.json().catch(() => null); if (!configResponse.ok) { throw new 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 || "保存失败" ); } toast.success("商品策划工具设置保存成功"); } catch (error) { toast.error( error instanceof Error ? error.message : "保存商品策划工具设置失败" ); } }; const handleSaveProductPlanningAiModelConfig = async () => { try { const trimmedBaseUrl = productPlanningAiModelConfig.baseUrl.trim() || PRODUCT_PLANNING_AI_MODEL_DEFAULT_BASE_URL; const trimmedModelId = productPlanningAiModelConfig.modelId.trim() || PRODUCT_PLANNING_AI_MODEL_DEFAULT_MODEL_ID; const trimmedDisplayName = productPlanningAiModelConfig.displayName.trim() || "商品策划专用 Qwen3-VL"; const trimmedProviderName = productPlanningAiModelConfig.providerName.trim() || "Product Planning Qwen3-VL"; if ( productPlanningAiModelConfig.enabled && !productPlanningAiModelConfig.apiKey.trim() ) { toast.error("启用商品策划专用模型时,请填写 API Key"); return; } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: PRODUCT_PLANNING_AI_MODEL_CONFIG_KEY, value: { enabled: productPlanningAiModelConfig.enabled, providerName: trimmedProviderName, baseUrl: trimmedBaseUrl, apiKey: productPlanningAiModelConfig.apiKey.trim(), modelId: trimmedModelId, displayName: trimmedDisplayName, }, }), }); const payload = await response.json().catch(() => null); if (!response.ok) { throw new Error( payload?.cause || payload?.message || payload?.error || "保存失败" ); } setProductPlanningAiModelConfig((prev) => ({ ...prev, providerName: trimmedProviderName, baseUrl: trimmedBaseUrl, modelId: trimmedModelId, displayName: trimmedDisplayName, })); mutateProductPlanningAiModelConfig(); toast.success("商品策划专用 AI 模型配置保存成功"); } catch (error) { toast.error( error instanceof Error ? error.message : "保存商品策划专用 AI 模型配置失败" ); } }; const handleSaveThirdPartySqlServerConfig = async () => { try { if (thirdPartySqlServerConfig.enabled) { if (!thirdPartySqlServerConfig.host.trim()) { toast.error("请填写 SQL Server 主机地址"); return; } if (!thirdPartySqlServerConfig.database.trim()) { toast.error("请填写数据库名称"); return; } if (!thirdPartySqlServerConfig.username.trim()) { toast.error("请填写数据库账号"); return; } if (!thirdPartySqlServerConfig.password.trim()) { toast.error("请填写数据库密码"); return; } } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: THIRD_PARTY_SQLSERVER_CONFIG_KEY, value: { enabled: thirdPartySqlServerConfig.enabled, host: thirdPartySqlServerConfig.host.trim(), port: Math.max( 1, Math.min(65_535, Number(thirdPartySqlServerConfig.port) || 1433) ), database: thirdPartySqlServerConfig.database.trim(), username: thirdPartySqlServerConfig.username.trim(), password: thirdPartySqlServerConfig.password.trim(), encrypt: thirdPartySqlServerConfig.encrypt, trustServerCertificate: thirdPartySqlServerConfig.trustServerCertificate, connectionTimeoutMs: Math.max( 1000, Math.min( 120_000, Number(thirdPartySqlServerConfig.connectionTimeoutMs) || 15_000 ) ), requestTimeoutMs: Math.max( 1000, Math.min( 120_000, Number(thirdPartySqlServerConfig.requestTimeoutMs) || 30_000 ) ), }, }), }); const payload = await response.json().catch(() => null); if (!response.ok) { throw new Error( payload?.cause || payload?.message || payload?.error || "保存失败" ); } toast.success("SQL Server 配置保存成功"); } catch (error) { toast.error( error instanceof Error ? error.message : "保存 SQL Server 配置失败" ); } }; const handleTestThirdPartySqlServerConfig = async () => { try { setIsTestingThirdPartySqlServer(true); const response = await fetch("/api/admin/sqlserver/test", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ config: { enabled: thirdPartySqlServerConfig.enabled, host: thirdPartySqlServerConfig.host.trim(), port: Math.max( 1, Math.min(65_535, Number(thirdPartySqlServerConfig.port) || 1433) ), database: thirdPartySqlServerConfig.database.trim(), username: thirdPartySqlServerConfig.username.trim(), password: thirdPartySqlServerConfig.password.trim(), encrypt: thirdPartySqlServerConfig.encrypt, trustServerCertificate: thirdPartySqlServerConfig.trustServerCertificate, connectionTimeoutMs: Math.max( 1000, Math.min( 120_000, Number(thirdPartySqlServerConfig.connectionTimeoutMs) || 15_000 ) ), requestTimeoutMs: Math.max( 1000, Math.min( 120_000, Number(thirdPartySqlServerConfig.requestTimeoutMs) || 30_000 ) ), }, }), }); const payload = await response.json().catch(() => null); if (!response.ok) { 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 连接测试失败" ); } finally { setIsTestingThirdPartySqlServer(false); } }; // --- 实时画布API配置逻辑 --- const handleSaveRealtimeCanvasConfig = async () => { 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 falApiKey = realtimeCanvasConfig.falApiKey.trim(); 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"; 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; const fallbackFalApiKey = realtimeCanvasConfig.fallbackFalApiKey.trim(); const fallbackFalBaseUrl = realtimeCanvasConfig.fallbackFalBaseUrl.trim() || "https://fal.run"; const fallbackFalModel = realtimeCanvasConfig.fallbackFalModel.trim(); const fallbackReplicateApiToken = realtimeCanvasConfig.fallbackReplicateApiToken.trim(); const fallbackReplicateModel = realtimeCanvasConfig.fallbackReplicateModel.trim() || "black-forest-labs/flux-2-klein-4b"; if ( realtimeCanvasConfig.enabled && ((primaryProviderType === "bfl" && !bflApiKey) || (primaryProviderType === "replicate" && !replicateApiToken) || (primaryProviderType === "fal" && (!falApiKey || !falModel))) ) { toast.error( primaryProviderType === "replicate" ? "启用实时画布功能需要配置 Replicate API Token" : primaryProviderType === "fal" ? "启用实时画布功能需要配置 FAL API Key 和模型 ID" : "启用实时画布功能需要配置 BFL API Key" ); return; } if ( realtimeCanvasConfig.enabled && fallbackEnabled && ((fallbackProviderType === "bfl" && !fallbackBflApiKey) || (fallbackProviderType === "replicate" && !fallbackReplicateApiToken) || (fallbackProviderType === "fal" && (!fallbackFalApiKey || !fallbackFalModel))) ) { toast.error( fallbackProviderType === "replicate" ? "启用备用实时服务需要配置备用 Replicate API Token" : fallbackProviderType === "fal" ? "启用备用实时服务需要配置备用 FAL API Key 和模型 ID" : "启用备用实时服务需要配置备用 BFL API Key" ); return; } 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 probeData = await probeResponse.json().catch(() => null); if (!probeResponse.ok || probeData?.supported === false) { toast.error(probeData?.message || "模型能力探针失败,已阻止保存"); return; } 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 || "备用实时服务探针失败,已阻止保存" ); return; } } } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "realtime_canvas_config", value: { enabled: realtimeCanvasConfig.enabled, providerType: primaryProviderType, bflApiKey, bflBaseUrl, bflModel, falApiKey, falBaseUrl, falModel, replicateApiToken, replicateModel, fallbackEnabled, fallbackProviderType, fallbackBflApiKey, fallbackBflBaseUrl, fallbackBflModel, fallbackFalApiKey, fallbackFalBaseUrl, fallbackFalModel, fallbackReplicateApiToken, fallbackReplicateModel, runwareApiKey: bflApiKey, 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) ), maxInputResolution: Math.min( 1536, Math.max( 512, Number(realtimeCanvasConfig.maxInputResolution) || 1024 ) ), 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 ) ), detailMegapixels: Math.min( 1.2, 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) ), imageSize: realtimeCanvasConfig.imageSize, }, }, }), }); if (!response.ok) { const errorData = await response.json().catch(() => null); throw new Error(errorData?.message || "保存失败"); } toast.success("实时画布配置保存成功"); } catch (error) { toast.error( error instanceof Error ? error.message : "保存实时画布配置失败" ); } }; const handleSaveCanvasImageTaskQueueConfig = async () => { try { const normalized = normalizeCanvasImageTaskQueueConfig( canvasImageTaskQueueConfig ); const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: CANVAS_IMAGE_TASK_QUEUE_CONFIG_KEY, value: normalized, }), }); const payload = await response.json().catch(() => null); if (!response.ok) { throw new Error(payload?.message || payload?.error || "保存失败"); } setCanvasImageTaskQueueConfig( normalizeCanvasImageTaskQueueConfig( payload?.setting?.value || normalized ) ); await mutateCanvasImageTaskQueueConfig(); toast.success("全站生图并发与批次间隔配置保存成功"); } catch (error) { toast.error( error instanceof Error ? error.message : "保存全站生图并发与批次间隔配置失败" ); } }; // --- 批量处理配置逻辑 --- const handleSaveSceneFusionConfig = async () => { try { if (!sceneFusionConfig.defaultPrompt.trim()) { toast.error("默认提示词不能为空"); return; } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "scene_fusion_config", value: { enabled: sceneFusionConfig.enabled, preRmbgEnabled: sceneFusionConfig.preRmbgEnabled, defaultPrompt: sceneFusionConfig.defaultPrompt.trim(), maxScenes: Math.max( 1, Math.min(10, sceneFusionConfig.maxScenes || 10) ), modelId: sceneFusionConfig.modelId, }, }), }); if (!response.ok) throw new Error("保存失败"); toast.success("批量处理配置保存成功"); } catch (error) { toast.error("保存批量处理配置失败"); } }; // --- 导航链接逻辑 --- const resetNavLinkForm = () => { 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 response = await fetch("/api/nav-links", { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!response.ok) throw new Error("保存失败"); toast.success(editingNavLink ? "链接更新成功" : "链接创建成功"); mutateNavLinks(); setShowNavLinkDialog(false); 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", }); if (!response.ok) throw new Error("删除失败"); toast.success("链接删除成功"); mutateNavLinks(); } catch (error) { toast.error("删除链接失败"); } }; // --- 视频教程管理逻辑 --- const resetTutorialVideoForm = () => { setTutorialVideoForm({ title: "", videoUrl: "", coverUrl: "", description: "", order: 0, isEnabled: true, }); setEditingTutorialVideo(null); }; const handleSaveTutorialVideo = async () => { try { const method = editingTutorialVideo ? "PUT" : "POST"; const body = editingTutorialVideo ? { ...tutorialVideoForm, id: editingTutorialVideo } : tutorialVideoForm; const response = await fetch("/api/tutorial-videos", { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!response.ok) throw new Error("保存失败"); toast.success(editingTutorialVideo ? "视频更新成功" : "视频创建成功"); mutateTutorialVideos(); setShowTutorialVideoDialog(false); resetTutorialVideoForm(); } catch (error) { toast.error("保存视频失败"); } }; const handleDeleteTutorialVideo = async (id: string) => { if (!confirm("确定要删除此视频吗?")) return; try { const response = await fetch(`/api/tutorial-videos?id=${id}`, { method: "DELETE", }); if (!response.ok) throw new Error("删除失败"); toast.success("视频删除成功"); mutateTutorialVideos(); } catch (error) { toast.error("删除视频失败"); } }; // --- 认证设置逻辑 --- const handleSaveAuthSettings = async () => { try { const response = await fetch("/api/auth-settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(authSettings), }); if (!response.ok) throw new Error("保存失败"); toast.success("认证设置保存成功"); mutateAuthSettings(); } catch (error) { toast.error("保存认证设置失败"); } }; // --- AI 提供商逻辑 --- const resetProviderForm = () => { setProviderForm({ id: "", name: "", type: "openai", baseUrl: getProviderDefaultBaseUrl("openai"), apiKey: "", isEnabled: true, }); setEditingProvider(null); }; const handleSaveProvider = async () => { try { const method = editingProvider ? "PUT" : "POST"; const isNewProvider = !editingProvider; const isVolcanoVideo = providerForm.type === "volcano-video"; const isKreaProvider = providerForm.type === "krea"; const isFalImageProvider = providerForm.type === "fal-image"; const isRuxaImageProvider = providerForm.type === "ruxa-image"; const isSnapgenImageProvider = providerForm.type === "snapgen-image"; const isBananaImageProvider = providerForm.type === "bananaimage"; const isVisionaryProvider = providerForm.type === "visionary"; const isAtlasCloudProvider = providerForm.type === "atlascloud"; const isVip98kProvider = providerForm.type === "vip98k"; const isBltcyVideoProvider = providerForm.type === "bltcy-video"; const isLemonProvider = providerForm.type === "openai" && isLemonApiBaseUrl( providerForm.baseUrl || getProviderDefaultBaseUrl(providerForm.type) ); const isBltcyImageProvider = providerForm.type === "openai-image" && isBltcyImageBaseUrl( providerForm.baseUrl || getProviderDefaultBaseUrl(providerForm.type) ); const isBltcyBflProvider = providerForm.type === "bfl-image" && isBltcyBflBaseUrl( providerForm.baseUrl || getProviderDefaultBaseUrl(providerForm.type) ); const defaultBaseUrl = getProviderDefaultBaseUrl(providerForm.type); const normalizedBaseUrl = providerForm.baseUrl.trim(); let requestData: any = editingProvider ? { ...providerForm, id: editingProvider } : providerForm; if ( defaultBaseUrl && (!normalizedBaseUrl || !isHttpUrlLike(normalizedBaseUrl) || normalizedBaseUrl.includes("@")) ) { requestData.baseUrl = defaultBaseUrl; } else { requestData.baseUrl = normalizedBaseUrl; } if (editingProvider && !providerForm.apiKey) { const { apiKey, ...restData } = requestData; requestData = restData; } const response = await fetch("/api/admin/ai-providers", { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestData), }); if (!response.ok) throw new Error("保存失败"); const data = await response.json(); const providerId = data.provider?.id || editingProvider; toast.success(editingProvider ? "提供商更新成功" : "提供商创建成功"); mutateProviders(); setShowProviderDialog(false); resetProviderForm(); // 🆕 如果是新创建的火山引擎视频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 modelsData = await modelsResponse.json(); if (modelsResponse.ok && modelsData.addedCount > 0) { toast.success( `✅ 已自动添加 ${modelsData.addedCount} 个预设模型!` ); mutateModels(); // 刷新模型列表 } } catch (modelError) { console.error("[Auto Add Models] Error:", modelError); toast.warning('预设模型添加失败,请手动点击"快速添加预设模型"按钮'); } } if (isNewProvider && isKreaProvider && providerId) { toast.info("正在自动添加 Krea 官方预设模型..."); try { const modelsResponse = await fetch("/api/admin/krea-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} 个 Krea 预设模型` ); mutateModels(); } } catch (modelError) { console.error("[Auto Add Krea Models] Error:", modelError); toast.warning( 'Krea 预设模型添加失败,请手动点击"快速添加 Krea 预设模型"按钮' ); } } if (isNewProvider && isFalImageProvider && providerId) { try { await handleQuickAddFalImageModels(providerId, { skipConfirm: true }); } catch (modelError) { console.error("[Auto Add FAL Image Models] Error:", modelError); toast.warning( 'FAL 图片预设模型添加失败,请手动点击"快速添加 FAL 图片预设"按钮' ); } } if (isNewProvider && isRuxaImageProvider && providerId) { try { await handleQuickAddRuxaImageModels(providerId, { skipConfirm: true }); } catch (modelError) { console.error("[Auto Add RUXA Image Models] Error:", modelError); toast.warning( 'RUXA 生图预设模型添加失败,请手动点击"快速添加 RUXA 图片预设"按钮' ); } } if (isNewProvider && isSnapgenImageProvider && providerId) { try { await handleQuickAddSnapgenImageModels(providerId, { skipConfirm: true }); } catch (modelError) { console.error("[Auto Add SnapGen Image Models] Error:", modelError); toast.warning( 'SnapGen 生图预设模型添加失败,请手动点击"快速添加 SnapGen 图片预设"按钮' ); } } if (isNewProvider && isBananaImageProvider && providerId) { try { await handleQuickAddBananaImageModels(providerId, { skipConfirm: true }); } catch (modelError) { console.error("[Auto Add BananaImage Models] Error:", modelError); toast.warning( 'BananaImage 预设模型添加失败,请手动点击"快速添加 BananaImage 图片预设"按钮' ); } } if (isNewProvider && isVisionaryProvider && providerId) { try { await handleQuickAddVisionaryModels(providerId, { skipConfirm: true }); } catch (modelError) { console.error("[Auto Add Visionary Models] Error:", modelError); toast.warning( 'Visionary 预设模型添加失败,请手动点击"快速添加 Visionary 图片预设"按钮' ); } } if (isNewProvider && isAtlasCloudProvider && providerId) { try { await handleQuickAddAtlasCloudModels(providerId, { skipConfirm: true }); } catch (modelError) { console.error("[Auto Add Atlas Cloud Models] Error:", modelError); toast.warning( 'Atlas Cloud 模型同步失败,请手动点击"同步 Atlas Cloud 模型"按钮' ); } } if (isNewProvider && isVip98kProvider && providerId) { try { await handleQuickAddVip98kModels(providerId, { skipConfirm: true }); } catch (modelError) { console.error("[Auto Sync VIP98K Models] Error:", modelError); toast.warning( 'VIP98K 模型同步失败,请手动点击"同步 VIP98K 全部模型"按钮' ); } } if (isNewProvider && isBltcyVideoProvider && providerId) { try { await handleQuickAddBltcyVideoModels(providerId, { skipConfirm: true, }); } catch (modelError) { console.error("[Auto Add BLTCY Video Models] Error:", modelError); toast.warning( '柏拉图视频预设模型添加失败,请手动点击"快速添加视频预设"按钮' ); } } if (isNewProvider && isLemonProvider && providerId) { try { await handleQuickAddLemonModels(providerId); } catch (modelError) { console.error("[Auto Add Lemon Models] Error:", modelError); toast.warning( 'LemonAPI 预设模型添加失败,请手动点击"快速添加 Gemini 预设模型"按钮' ); } } if (isNewProvider && isBltcyImageProvider && providerId) { try { await handleQuickAddBltcyImageModels(providerId, { skipConfirm: true, }); } catch (modelError) { console.error("[Auto Add BLTCY Image Models] Error:", modelError); toast.warning( '柏拉图 Flux 预设模型添加失败,请手动点击"快速添加图片编辑预设"按钮' ); } } if (isNewProvider && isBltcyBflProvider && providerId) { try { await handleQuickAddBltcyBflModels(providerId, { skipConfirm: true }); } catch (modelError) { console.error("[Auto Add BLTCY BFL Models] Error:", modelError); toast.warning( '柏拉图 BFL 预设模型添加失败,请手动点击"快速添加 BFL 异步预设"按钮' ); } } } catch (error) { toast.error("保存提供商失败"); } }; const handleDeleteProvider = async (id: string) => { if (!confirm("确定删除?关联模型也会被删除。")) return; try { const response = await fetch(`/api/admin/ai-providers?id=${id}`, { method: "DELETE", }); if (!response.ok) throw new Error("删除失败"); toast.success("提供商删除成功"); mutateProviders(); mutateModels(); } catch (error) { toast.error("删除提供商失败"); } }; const handleSaveTripo3dConfig = async () => { try { const trimmedName = tripo3dConfig.name.trim() || "Tripo3D"; const trimmedBaseUrl = tripo3dConfig.baseUrl.trim() || getProviderDefaultBaseUrl("tripo-3d"); const hasProvider = Boolean(tripo3dConfig.providerId); if (!hasProvider && !tripo3dConfig.apiKey.trim()) { toast.error("请填写 Tripo3D API Key"); return; } const requestData: Record = hasProvider ? { id: tripo3dConfig.providerId, name: trimmedName, type: "tripo-3d", baseUrl: trimmedBaseUrl, isEnabled: tripo3dConfig.isEnabled, } : { name: trimmedName, type: "tripo-3d", baseUrl: trimmedBaseUrl, apiKey: tripo3dConfig.apiKey.trim(), isEnabled: tripo3dConfig.isEnabled, }; if (hasProvider && tripo3dConfig.apiKey.trim()) { requestData.apiKey = tripo3dConfig.apiKey.trim(); } const response = await fetch("/api/admin/ai-providers", { method: hasProvider ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestData), }); if (!response.ok) { throw new Error("保存失败"); } toast.success("Tripo3D 配置保存成功"); setTripo3dConfig((prev) => ({ ...prev, apiKey: "" })); mutateProviders(); } catch (error) { toast.error("保存 Tripo3D 配置失败"); } }; const handleAddTripo3dPoolKey = async () => { try { const trimmedName = tripo3dConfig.name.trim() || "Tripo3D Key"; const trimmedBaseUrl = tripo3dConfig.baseUrl.trim() || getProviderDefaultBaseUrl("tripo-3d"); const trimmedApiKey = tripo3dConfig.apiKey.trim(); if (!trimmedApiKey) { toast.error("请填写要加入 Key 池的 Tripo3D API Key"); return; } const response = await fetch("/api/admin/ai-providers", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: trimmedName, type: "tripo-3d", baseUrl: trimmedBaseUrl, apiKey: trimmedApiKey, isEnabled: tripo3dConfig.isEnabled, }), }); if (!response.ok) { throw new Error("添加失败"); } toast.success("Tripo3D Key 已加入池"); setTripo3dConfig((prev) => ({ ...prev, apiKey: "" })); mutateProviders(); } catch (error) { toast.error("添加 Tripo3D Key 失败"); } }; const handleEditTripoPoolProvider = (provider: AiProvider) => { setTripo3dConfig({ providerId: provider.id, name: provider.name || "Tripo3D", baseUrl: provider.baseUrl || getProviderDefaultBaseUrl("tripo-3d"), apiKey: "", isEnabled: provider.isEnabled, }); }; const handleToggleTripoPoolProvider = async ( provider: AiProvider, isEnabled: boolean ) => { setTripoProviderBusyId(provider.id); try { const response = await fetch("/api/admin/ai-providers", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: provider.id, isEnabled, }), }); if (!response.ok) { throw new Error("更新状态失败"); } toast.success( `已${isEnabled ? "启用" : "停用"} ${provider.name || "Tripo3D Key"}` ); mutateProviders(); } catch (error) { toast.error("更新 Tripo3D Key 状态失败"); } finally { setTripoProviderBusyId(null); } }; const handleSaveWorldlabs3dConfig = async () => { try { const trimmedName = worldlabs3dConfig.name.trim() || "World Labs"; const trimmedBaseUrl = worldlabs3dConfig.baseUrl.trim() || getProviderDefaultBaseUrl("worldlabs-3d"); const hasProvider = Boolean(worldlabs3dConfig.providerId); const normalizedKeyPoolConfig = normalizeWorldLabsKeyPoolConfig({ ...worldlabsKeyPoolConfig, providerName: trimmedName, baseUrl: trimmedBaseUrl, isEnabled: worldlabs3dConfig.isEnabled, }); if (!hasProvider && !worldlabs3dConfig.apiKey.trim()) { const keyPoolResponse = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: WORLDLABS_KEY_POOL_CONFIG_KEY, value: normalizedKeyPoolConfig, }), }); if (!keyPoolResponse.ok) { const payload = await keyPoolResponse.json().catch(() => ({})); throw new Error( String( payload?.message || payload?.error || "保存 World Labs 基础配置失败" ) ); } setWorldlabsKeyPoolConfig(normalizedKeyPoolConfig); mutateWorldlabsKeyPoolConfig(); toast.success("World Labs 基础配置已保存,添加 API Key 后即可生成"); return; } const requestData: Record = hasProvider ? { id: worldlabs3dConfig.providerId, name: trimmedName, type: "worldlabs-3d", baseUrl: trimmedBaseUrl, isEnabled: worldlabs3dConfig.isEnabled, } : { name: trimmedName, type: "worldlabs-3d", baseUrl: trimmedBaseUrl, apiKey: worldlabs3dConfig.apiKey.trim(), isEnabled: worldlabs3dConfig.isEnabled, }; if (hasProvider && worldlabs3dConfig.apiKey.trim()) { requestData.apiKey = worldlabs3dConfig.apiKey.trim(); } const response = await fetch("/api/admin/ai-providers", { method: hasProvider ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestData), }); if (!response.ok) { throw new Error("保存失败"); } const keyPoolResponse = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: WORLDLABS_KEY_POOL_CONFIG_KEY, value: normalizedKeyPoolConfig, }), }); if (!keyPoolResponse.ok) { const payload = await keyPoolResponse.json().catch(() => ({})); throw new Error( String( payload?.message || payload?.error || "保存 World Labs key 池导入令牌失败" ) ); } toast.success("World Labs 配置保存成功"); setWorldlabs3dConfig((prev) => ({ ...prev, apiKey: "" })); setWorldlabsKeyPoolConfig(normalizedKeyPoolConfig); mutateProviders(); mutateWorldlabsKeyPoolConfig(); } catch (error) { console.error("保存 World Labs 配置失败:", error); toast.error( error instanceof Error ? error.message : "保存 World Labs 配置失败" ); } }; const handleAddWorldlabs3dPoolKey = async () => { try { const trimmedName = worldlabs3dConfig.name.trim() || "World Labs Key"; const trimmedBaseUrl = worldlabs3dConfig.baseUrl.trim() || getProviderDefaultBaseUrl("worldlabs-3d"); const trimmedApiKey = worldlabs3dConfig.apiKey.trim(); if (!trimmedApiKey) { toast.error("请填写要加入 Key 池的 World Labs API Key"); return; } const response = await fetch("/api/worldlabs/key-pool/import", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ keys: [ { apiKey: trimmedApiKey, name: trimmedName, baseUrl: trimmedBaseUrl, isEnabled: worldlabs3dConfig.isEnabled, }, ], }), }); const payload = await response.json().catch(() => ({})); if (!response.ok || payload?.success === false) { throw new Error( String( payload?.error || payload?.message || "添加 World Labs Key 失败" ) ); } const importedCount = Number(payload?.data?.importedCount || 0); const skippedCount = Number(payload?.data?.skippedCount || 0); toast.success( importedCount > 0 ? "World Labs Key 已加入池" : skippedCount > 0 ? "这个 World Labs Key 已存在,已跳过重复添加" : "World Labs Key 处理完成" ); setWorldlabs3dConfig((prev) => ({ ...prev, apiKey: "" })); mutateProviders(); } catch (error) { toast.error( error instanceof Error ? error.message : "添加 World Labs Key 失败" ); } }; const handleImportWorldlabs3dPoolKeys = async () => { try { const apiKeysText = worldlabsApiKeysDraft.trim(); if (!apiKeysText) { toast.error("请先粘贴要批量导入的 World Labs API Key"); return; } const response = await fetch("/api/worldlabs/key-pool/import", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ apiKeysText, namePrefix: worldlabs3dConfig.name.trim() || "WorldLabsKey", baseUrl: worldlabs3dConfig.baseUrl.trim() || getProviderDefaultBaseUrl("worldlabs-3d"), isEnabled: worldlabs3dConfig.isEnabled, }), }); const payload = await response.json().catch(() => ({})); if (!response.ok || payload?.success === false) { throw new Error( String( payload?.error || payload?.message || "批量导入 World Labs Key 失败" ) ); } const importedCount = Number(payload?.data?.importedCount || 0); const skippedCount = Number(payload?.data?.skippedCount || 0); toast.success( `World Labs Key 导入完成:新增 ${importedCount} 个,跳过 ${skippedCount} 个` ); setWorldlabsApiKeysDraft(""); setWorldlabs3dConfig((prev) => ({ ...prev, apiKey: "" })); mutateProviders(); } catch (error) { toast.error( error instanceof Error ? error.message : "批量导入 World Labs Key 失败" ); } }; const updateThreeDModelAliasEntries = ( key: "tripoModels" | "worldModels", updater: (entries: ThreeDModelAliasEntry[]) => ThreeDModelAliasEntry[] ) => { setThreeDModelAliasConfig((prev) => ({ ...prev, [key]: updater(prev[key]), })); }; const handleThreeDModelAliasEntryChange = < K extends keyof ThreeDModelAliasEntry, >( key: "tripoModels" | "worldModels", index: number, field: K, value: ThreeDModelAliasEntry[K] ) => { updateThreeDModelAliasEntries(key, (entries) => entries.map((entry, entryIndex) => { if (entryIndex !== index) { if (field === "isDefault" && value === true) { return { ...entry, isDefault: false }; } return entry; } return { ...entry, [field]: value, } as ThreeDModelAliasEntry; }) ); }; const handleAddThreeDModelAliasEntry = ( key: "tripoModels" | "worldModels" ) => { updateThreeDModelAliasEntries(key, (entries) => [ ...entries, { id: `${key === "tripoModels" ? "tripo" : "world"}-custom-${crypto.randomUUID().slice(0, 8)}`, displayName: "", realModelId: "", description: "", enabled: true, isDefault: false, }, ]); }; const handleRemoveThreeDModelAliasEntry = ( key: "tripoModels" | "worldModels", index: number ) => { updateThreeDModelAliasEntries(key, (entries) => { if (entries.length <= 1) return entries; const nextEntries = entries.filter( (_, entryIndex) => entryIndex !== index ); const hasDefault = nextEntries.some( (entry) => entry.enabled !== false && entry.isDefault ); if (hasDefault) return nextEntries; const fallbackIndex = nextEntries.findIndex( (entry) => entry.enabled !== false ); const targetIndex = fallbackIndex >= 0 ? fallbackIndex : 0; return nextEntries.map((entry, entryIndex) => ({ ...entry, isDefault: entryIndex === targetIndex, })); }); }; const handleResetThreeDModelAliasConfig = () => { setThreeDModelAliasConfig(defaultThreeDModelAliasConfig()); }; const handleSaveThreeDModelAliasConfig = async () => { try { const configToSave = normalizeThreeDModelAliasConfig( threeDModelAliasConfig ); const invalidEntry = [ ...configToSave.tripoModels.map((entry, index) => ({ providerLabel: "Tripo3D", index, entry, })), ...configToSave.worldModels.map((entry, index) => ({ providerLabel: "World Labs", index, entry, })), ].find( ({ entry }) => !entry.displayName.trim() || !entry.realModelId.trim() ); if (invalidEntry) { throw new Error( `${invalidEntry.providerLabel} 第 ${invalidEntry.index + 1} 条映射缺少展示名或真实模型 ID` ); } const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: THREED_MODEL_ALIAS_CONFIG_SETTING_KEY, value: configToSave, }), }); const payload = await response.json().catch(() => ({})); if (!response.ok) { throw new Error( String(payload?.message || payload?.error || "保存失败") ); } setThreeDModelAliasConfig(configToSave); mutateThreeDModelAliasConfig(); toast.success("3D 模型显示名映射保存成功"); } catch (error) { toast.error( error instanceof Error ? error.message : "保存 3D 模型显示名映射失败" ); } }; const handleEditWorldlabsPoolProvider = (provider: AiProvider) => { setProviderForm({ id: provider.id, name: provider.name, type: provider.type, baseUrl: provider.baseUrl, apiKey: "", isEnabled: provider.isEnabled, }); setEditingProvider(provider.id); setShowProviderDialog(true); }; const handleToggleWorldlabsPoolProvider = async ( provider: AiProvider, isEnabled: boolean ) => { setWorldlabsProviderBusyId(provider.id); try { const response = await fetch("/api/admin/ai-providers", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: provider.id, isEnabled, }), }); if (!response.ok) { throw new Error("更新状态失败"); } toast.success( `已${isEnabled ? "启用" : "停用"} ${provider.name || "World Labs Key"}` ); mutateProviders(); } catch (error) { toast.error("更新 World Labs Key 状态失败"); } finally { setWorldlabsProviderBusyId(null); } }; const handleSaveKittycadCadConfig = async () => { try { const trimmedName = kittycadCadConfig.name.trim() || "KittyCAD CAD"; const trimmedBaseUrl = kittycadCadConfig.baseUrl.trim() || getProviderDefaultBaseUrl("kittycad-cad"); const hasProvider = Boolean(kittycadCadConfig.providerId); if (!hasProvider && !kittycadCadConfig.apiKey.trim()) { toast.error("请填写 KittyCAD API Token"); return; } const requestData: Record = hasProvider ? { id: kittycadCadConfig.providerId, name: trimmedName, type: "kittycad-cad", baseUrl: trimmedBaseUrl, isEnabled: kittycadCadConfig.isEnabled, } : { name: trimmedName, type: "kittycad-cad", baseUrl: trimmedBaseUrl, apiKey: kittycadCadConfig.apiKey.trim(), isEnabled: kittycadCadConfig.isEnabled, }; if (hasProvider && kittycadCadConfig.apiKey.trim()) { requestData.apiKey = kittycadCadConfig.apiKey.trim(); } const response = await fetch("/api/admin/ai-providers", { method: hasProvider ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestData), }); if (!response.ok) { throw new Error("保存失败"); } toast.success("KittyCAD CAD 配置保存成功"); setKittycadCadConfig((prev) => ({ ...prev, apiKey: "" })); mutateProviders(); } catch (error) { console.error("保存 KittyCAD CAD 配置失败:", error); toast.error("保存 KittyCAD CAD 配置失败"); } }; const handleTestKittycadCadConfig = async () => { try { setIsTestingKittycadCad(true); const response = await fetch("/api/admin/kittycad/test", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ config: { providerId: kittycadCadConfig.providerId || undefined, name: kittycadCadConfig.name.trim() || "KittyCAD CAD", baseUrl: kittycadCadConfig.baseUrl.trim() || getProviderDefaultBaseUrl("kittycad-cad"), apiKey: kittycadCadConfig.apiKey.trim() || undefined, }, }), }); const payload = await response.json().catch(() => null); if (!response.ok) { throw new Error( payload?.cause || payload?.message || payload?.error || "连接测试失败" ); } const accountLabel = payload?.userName || payload?.userEmail || payload?.userId || "当前账号"; toast.success( `连接成功:${accountLabel}(${payload?.latencyMs || 0}ms)` ); } catch (error) { toast.error( error instanceof Error ? error.message : "KittyCAD 连接测试失败" ); } finally { setIsTestingKittycadCad(false); } }; const handleSaveMoonshotDetailAgentConfig = async () => { try { const trimmedBaseUrl = normalizeDetailAgentLocalMoonshotBaseUrl( moonshotDetailAgentConfig.baseUrl ); const trimmedModelId = moonshotDetailAgentConfig.modelId.trim() || DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID; const trimmedDisplayName = moonshotDetailAgentConfig.displayName.trim() || DETAIL_AGENT_LOCAL_MOONSHOT_DISPLAY_NAME; const trimmedProviderName = moonshotDetailAgentConfig.providerName.trim() || "Kimi Code 编程套餐"; if (!isMoonshotOfficialBaseUrl(trimmedBaseUrl)) { toast.error( `请使用 Kimi Code 编程套餐专用 API 地址:${DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL}` ); return; } if (!trimmedModelId) { toast.error("请填写 Kimi Code API 模型 ID"); return; } if ( !moonshotDetailAgentConfig.providerId && !moonshotDetailAgentConfig.apiKey.trim() ) { toast.error("请填写 Kimi Code 控制台 API Key"); return; } const settingResponse = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY, value: { enabled: moonshotDetailAgentConfig.isEnabled, providerName: trimmedProviderName, baseUrl: trimmedBaseUrl, apiKey: moonshotDetailAgentConfig.apiKey.trim(), modelId: trimmedModelId, displayName: trimmedDisplayName, }, }), }); const settingResult = await settingResponse.json().catch(() => ({})); if (!settingResponse.ok) { throw new Error( String(settingResult?.message || "保存 Moonshot 本地AI配置失败") ); } const settingsResponse = await fetch("/api/site-settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ canvasDetailAgentModelId: DETAIL_AGENT_LOCAL_MOONSHOT_OPTION_ID, }), }); if (!settingsResponse.ok) { throw new Error("写入 AI详情 Agent 专用模型设置失败"); } setSiteSettings((prev) => ({ ...prev, canvasDetailAgentModelId: DETAIL_AGENT_LOCAL_MOONSHOT_OPTION_ID, })); setMoonshotDetailAgentConfig((prev) => ({ ...prev, providerName: trimmedProviderName, baseUrl: trimmedBaseUrl, modelId: trimmedModelId, displayName: trimmedDisplayName, apiKey: "", })); mutateMoonshotDetailAgentSetting(); mutateSiteSettings(); toast.success("Kimi Code 编程套餐已绑定为 AI详情 Agent 专用模型"); } catch (error) { console.error("[Moonshot Detail Agent Config] Error:", error); 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继续吗?" ) ) { return; } try { toast.info("正在添加预设模型..."); const response = await fetch("/api/admin/volcano-video-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerId }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "添加失败"); } const { addedCount, errors } = data; if (addedCount > 0) { toast.success(`成功添加 ${addedCount} 个预设模型!`); mutateModels(); // 刷新模型列表 } if (errors && errors.length > 0) { toast.warning(`${errors.length} 个模型添加失败(可能已存在)`); } } catch (error: any) { console.error("[Quick Add Models] Error:", error); toast.error(error.message || "添加预设模型失败"); } }; const handleQuickAddKreaModels = async (providerId: string) => { if ( !confirm( "将为此服务商添加 Krea 官方 API 的全部图片、图片增强和视频模型预设。\n\n继续吗?" ) ) { return; } try { toast.info("正在添加 Krea 官方预设模型..."); const response = await fetch("/api/admin/krea-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerId }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "添加失败"); } const { addedCount, errors } = data; if (addedCount > 0) { toast.success(`成功添加 ${addedCount} 个 Krea 预设模型`); mutateModels(); } if (errors && errors.length > 0) { toast.warning(`${errors.length} 个模型添加失败(可能已存在)`); } } catch (error: any) { console.error("[Quick Add Krea Models] Error:", error); toast.error(error.message || "添加 Krea 预设模型失败"); } }; const handleQuickAddFalImageModels = async ( providerId: string, options?: { skipConfirm?: boolean } ) => { if ( !options?.skipConfirm && !confirm( `将为此服务商添加 ${FAL_IMAGE_MODEL_PRESETS.length} 个 FAL 图片编辑模型预设(包含 GPT Image 2 Edit)。\n\n继续吗?` ) ) { return; } try { toast.info("正在添加 FAL 图片预设模型..."); const response = await fetch("/api/admin/fal-image-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerId }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "添加失败"); } const { addedCount, errors } = data; if (addedCount > 0) { toast.success(`成功添加 ${addedCount} 个 FAL 图片预设模型`); mutateModels(); } if (errors && errors.length > 0) { toast.warning(`${errors.length} 个模型添加失败(可能已存在)`); } } catch (error: any) { console.error("[Quick Add FAL Image Models] Error:", error); toast.error(error.message || "添加 FAL 图片预设模型失败"); } }; const handleQuickAddRuxaImageModels = async ( providerId: string, options?: { skipConfirm?: boolean } ) => { if ( !options?.skipConfirm && !confirm( `将为此服务商添加 ${RUXA_IMAGE_MODEL_PRESETS.length} 个 RUXA 生图预设模型(含 Nano Banana 2「专线」、Nano Banana Pro「专线」)。\n\n继续吗?` ) ) { return; } try { toast.info("正在添加 RUXA 生图预设模型..."); const response = await fetch("/api/admin/ruxa-image-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerId }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "添加失败"); } const { addedCount, errors } = data; if (addedCount > 0) { toast.success(`成功添加 ${addedCount} 个 RUXA 生图预设模型`); mutateModels(); } if (errors && errors.length > 0) { toast.warning(`${errors.length} 个模型添加失败(可能已存在)`); } } catch (error: any) { console.error("[Quick Add RUXA Image Models] Error:", error); toast.error(error.message || "添加 RUXA 生图预设模型失败"); } }; const handleQuickAddSnapgenImageModels = async ( providerId: string, options?: { skipConfirm?: boolean } ) => { if ( !options?.skipConfirm && !confirm( `将为此服务商添加 ${SNAPGEN_IMAGE_MODEL_PRESETS.length} 个 SnapGen 生图预设模型(Nano Banana Pro、Nano Banana 2、Grok Image、Meta AI Image、GPT Image 2)。\n\n继续吗?` ) ) { return; } try { toast.info("正在添加 SnapGen 生图预设模型..."); const response = await fetch("/api/admin/snapgen-image-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerId }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "添加失败"); } const { addedCount, updatedCount, errors } = data; const totalSynced = Number(addedCount || 0) + Number(updatedCount || 0); if (totalSynced > 0) { toast.success(`成功同步 ${totalSynced} 个 SnapGen 生图预设模型`); mutateModels(); } if (errors && errors.length > 0) { toast.warning(`${errors.length} 个模型同步失败`); } } catch (error: any) { console.error("[Quick Add SnapGen Image Models] Error:", error); toast.error(error.message || "添加 SnapGen 生图预设模型失败"); } }; const handleQuickAddBananaImageModels = async ( providerId: string, options?: { skipConfirm?: boolean } ) => { if ( !options?.skipConfirm && !confirm( `将同步 ${BANANAIMAGE_MODEL_PRESETS.length} 个 BananaImage 生图模型,支持多图参考、比例选择和 1K/2K/4K。\n\n继续吗?` ) ) { return; } try { toast.info("正在同步 BananaImage 生图预设模型..."); const response = await fetch("/api/admin/bananaimage-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerId }), }); const data = await response.json(); if (!response.ok) throw new Error(data.error || "同步失败"); const totalSynced = Number(data.addedCount || 0) + Number(data.updatedCount || 0); if (totalSynced > 0) { toast.success(`成功同步 ${totalSynced} 个 BananaImage 生图预设模型`); mutateModels(); } if (data.errors?.length) { toast.warning(`${data.errors.length} 个 BananaImage 模型同步失败`); } } catch (error: any) { console.error("[Quick Add BananaImage Models] Error:", error); toast.error(error.message || "同步 BananaImage 生图预设模型失败"); } }; const handleQuickAddVisionaryModels = async ( providerId: string, options?: { skipConfirm?: boolean } ) => { if ( !options?.skipConfirm && !confirm( `将同步 ${VISIONARY_MODEL_PRESETS.length} 个 Visionary 生图模型,支持最多 9 张参考图、比例选择、质量档位和 1K/2K/4K。\n\n继续吗?` ) ) { return; } try { toast.info("正在同步 Visionary 生图预设模型..."); const response = await fetch("/api/admin/visionary-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerId }), }); const data = await response.json(); if (!response.ok) throw new Error(data.error || "同步失败"); const totalSynced = Number(data.addedCount || 0) + Number(data.updatedCount || 0); if (totalSynced > 0) { toast.success(`成功同步 ${totalSynced} 个 Visionary 生图预设模型`); mutateModels(); } if (data.errors?.length) { toast.warning(`${data.errors.length} 个 Visionary 模型同步失败`); } } catch (error: any) { console.error("[Quick Add Visionary Models] Error:", error); toast.error(error.message || "同步 Visionary 生图预设模型失败"); } }; const handleQuickAddAtlasCloudModels = async ( providerId: string, options?: { skipConfirm?: boolean } ) => { if ( !options?.skipConfirm && !confirm( "将从 Atlas Cloud 官方目录同步图片、视频与 3D 模型,并读取每个模型的参数 Schema。已删除的模型不会被恢复。同步数量较多,可能需要几十秒。\n\n继续吗?" ) ) { return; } try { toast.info("正在读取 Atlas Cloud 官方目录和模型参数..."); const response = await fetch("/api/admin/atlascloud-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerId }), }); const data = await response.json(); if (!response.ok) throw new Error(data.error || "同步失败"); const totalSynced = Number(data.addedCount || 0) + Number(data.updatedCount || 0); toast.success( `已同步 ${totalSynced} 个 Atlas Cloud 图片 / 视频 / 3D 模型(官方目录共 ${Number(data.counts?.total || totalSynced)} 个)` ); mutateModels(); if (data.errors?.length) { toast.warning(`${data.errors.length} 个 Atlas Cloud 模型读取或同步失败`); } } catch (error: any) { console.error("[Quick Add Atlas Cloud Models] Error:", error); toast.error(error.message || "同步 Atlas Cloud 模型失败"); throw error; } }; const handleQuickAddVip98kModels = async ( providerId: string, options?: { skipConfirm?: boolean } ) => { if ( !options?.skipConfirm && !confirm( "将同步 VIP98K 当前可用目录,停用服务商已关闭的模型,并排除纯文生图和纯文生视频模型。图片编辑、图生视频、参考媒体、多模态、3D 与工具模型会保留;你此前手动删除的模型不会恢复。\n\n继续吗?" ) ) { return; } try { toast.info("正在读取 VIP98K 模型目录..."); const response = await fetch("/api/admin/vip98k-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerId }), }); const data = await response.json(); if (!response.ok) throw new Error(data.error || "同步失败"); const totalSynced = Number(data.addedCount || 0) + Number(data.updatedCount || 0); const totalCatalog = Number(data.counts?.total || totalSynced); const totalDisabled = Number(data.disabledCount || 0); toast.success( `已同步 ${totalSynced} 个 VIP98K 模型,停用 ${totalDisabled} 个旧模型(可用目录 ${totalCatalog} 个)` ); mutateModels(); if (data.errors?.length) { toast.warning(`${data.errors.length} 个 VIP98K 模型同步失败`); } } catch (error: any) { console.error("[Quick Add VIP98K Models] Error:", error); toast.error(error.message || "同步 VIP98K 模型失败"); throw error; } }; const handleUseFalImageProviderPreset = () => { setProviderForm({ id: "", name: "FAL 图片", type: "fal-image", baseUrl: PROVIDER_DEFAULT_BASE_URL["fal-image"], apiKey: "", isEnabled: true, }); setEditingProvider(null); setShowProviderDialog(true); }; const handleQuickAddFalImageModelsFromToolbar = async () => { const falProvider = (providersData?.providers || []).find( (provider) => provider.type === "fal-image" && provider.isEnabled ) || (providersData?.providers || []).find( (provider) => provider.type === "fal-image" ); if (falProvider) { await handleQuickAddFalImageModels(falProvider.id); return; } handleUseFalImageProviderPreset(); toast.info( "还没有 FAL 图片服务商,已为你预填创建表单。保存后会自动添加 GPT Image 2 Edit 预设。" ); }; const handleUseBltcyVideoProviderPreset = () => { setProviderForm({ id: "", name: BLTCY_VIDEO_PROVIDER_PRESET.name, type: BLTCY_VIDEO_PROVIDER_PRESET.type, baseUrl: BLTCY_VIDEO_PROVIDER_PRESET.baseUrl, apiKey: "", isEnabled: true, }); setEditingProvider(null); setShowProviderDialog(true); }; const handleQuickAddBltcyVideoModels = async ( providerId: string, options?: { skipConfirm?: boolean } ) => { if ( !options?.skipConfirm && !confirm( `将为此服务商添加 ${BLTCY_VIDEO_MODEL_PRESETS.length} 个柏拉图统一视频模型预设。\n\n继续吗?` ) ) { return; } try { toast.info("正在添加柏拉图统一视频预设模型..."); const response = await fetch("/api/admin/bltcy-video-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerId }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "添加失败"); } const { addedCount, errors } = data; if (addedCount > 0) { toast.success(`成功添加 ${addedCount} 个柏拉图视频预设模型`); mutateModels(); } if (errors && errors.length > 0) { toast.warning(`${errors.length} 个模型添加失败(可能已存在)`); } } catch (error: any) { console.error("[Quick Add BLTCY Video Models] Error:", error); toast.error(error.message || "添加柏拉图视频预设模型失败"); } }; const handleUseBltcyImageProviderPreset = () => { setProviderForm({ id: "", name: BLTCY_IMAGE_PROVIDER_PRESET.name, type: BLTCY_IMAGE_PROVIDER_PRESET.type, baseUrl: BLTCY_IMAGE_PROVIDER_PRESET.baseUrl, apiKey: "", isEnabled: true, }); setEditingProvider(null); setShowProviderDialog(true); }; const handleUseBltcyBflProviderPreset = () => { setProviderForm({ id: "", name: BLTCY_BFL_PROVIDER_PRESET.name, type: BLTCY_BFL_PROVIDER_PRESET.type, baseUrl: BLTCY_BFL_PROVIDER_PRESET.baseUrl, apiKey: "", isEnabled: true, }); setEditingProvider(null); setShowProviderDialog(true); }; const handleQuickAddBltcyImageModels = async ( providerId: string, options?: { skipConfirm?: boolean } ) => { if ( !options?.skipConfirm && !confirm( `将为此服务商添加 ${BLTCY_IMAGE_MODEL_PRESETS.length} 个柏拉图 Flux OpenAI Images 兼容图片编辑模型预设。\n\n继续吗?` ) ) { return; } try { toast.info("正在添加柏拉图 Flux 图片编辑预设模型..."); const response = await fetch("/api/admin/bltcy-image-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerId }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "添加失败"); } const { addedCount, errors } = data; if (addedCount > 0) { toast.success(`成功添加 ${addedCount} 个柏拉图 Flux 预设模型`); mutateModels(); } if (errors && errors.length > 0) { toast.warning(`${errors.length} 个模型添加失败(可能已存在)`); } } catch (error: any) { console.error("[Quick Add BLTCY Image Models] Error:", error); toast.error(error.message || "添加柏拉图 Flux 预设模型失败"); } }; const handleQuickAddBltcyBflModels = async ( providerId: string, options?: { skipConfirm?: boolean } ) => { if ( !options?.skipConfirm && !confirm( `将为此服务商添加 ${BLTCY_BFL_MODEL_PRESETS.length} 个柏拉图 BFL 异步图片编辑模型预设。\n\n继续吗?` ) ) { return; } try { toast.info("正在添加柏拉图 BFL 异步图片编辑预设模型..."); const response = await fetch("/api/admin/bltcy-bfl-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerId }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "添加失败"); } const { addedCount, errors } = data; if (addedCount > 0) { toast.success(`成功添加 ${addedCount} 个柏拉图 BFL 预设模型`); mutateModels(); } if (errors && errors.length > 0) { toast.warning(`${errors.length} 个模型添加失败(可能已存在)`); } } catch (error: any) { console.error("[Quick Add BLTCY BFL Models] Error:", error); toast.error(error.message || "添加柏拉图 BFL 预设模型失败"); } }; const handleUseLemonProviderPreset = () => { setProviderForm({ id: "", name: LEMON_PROVIDER_PRESET.name, type: LEMON_PROVIDER_PRESET.type, baseUrl: LEMON_PROVIDER_PRESET.baseUrl, apiKey: "", isEnabled: true, }); setEditingProvider(null); setShowProviderDialog(true); }; const handleQuickAddLemonModels = async (providerId: string) => { try { toast.info("正在添加 LemonAPI Gemini 预设模型..."); let addedCount = 0; let duplicateCount = 0; for (const preset of LEMON_MODEL_PRESETS) { const response = await fetch("/api/admin/ai-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerId, modelId: preset.modelId, displayName: preset.displayName, description: preset.description, supportsVision: preset.supportsVision, supportsImageGen: preset.supportsImageGen, isEnabled: true, isHidden: false, isDefault: false, isAgentModel: false, agentCodingModelId: "", isMultimodal: preset.isMultimodal, sortOrder: 0, }), }); if (response.ok) { addedCount += 1; continue; } duplicateCount += 1; } if (addedCount > 0) { toast.success(`成功添加 ${addedCount} 个 LemonAPI 预设模型`); mutateModels(); } if (addedCount === 0 && duplicateCount > 0) { toast.warning("LemonAPI 预设模型可能已存在"); } } catch (error) { console.error("[Quick Add Lemon Models] Error:", error); toast.error("添加 LemonAPI 预设模型失败"); } }; // --- AI 模型逻辑 --- const resetModelForm = () => { setModelForm({ id: "", providerId: "", modelId: "", displayName: "", avatarUrl: "", description: "", supportsVision: false, supportsImageGen: false, isEnabled: true, isHidden: false, isDefault: false, isDefaultTextEditModel: false, isAgentModel: false, agentCodingModelId: "", isMultimodal: false, sortOrder: 0, fallbackModelId1: "", fallbackModelId2: "", excludeFromFailover: false, excludeFromHealthBan: false, capabilities: {}, imageGenConfig: createDefaultImageGenConfig(), }); setVideoDurationOptionsInput(""); setEditingModel(null); }; const resizeImageToSquare = (file: File, size = 400): Promise => new Promise((resolve, reject) => { const image = new Image(); const objectUrl = URL.createObjectURL(file); image.onload = () => { try { const canvas = document.createElement("canvas"); canvas.width = size; canvas.height = size; const context = canvas.getContext("2d"); if (!context) { URL.revokeObjectURL(objectUrl); reject(new Error("无法处理图片")); return; } const minSide = Math.min(image.width, image.height); const sx = (image.width - minSide) / 2; const sy = (image.height - minSide) / 2; context.imageSmoothingEnabled = true; context.imageSmoothingQuality = "high"; context.drawImage(image, sx, sy, minSide, minSide, 0, 0, size, size); canvas.toBlob( (blob) => { URL.revokeObjectURL(objectUrl); if (!blob) { reject(new Error("头像压缩失败")); return; } resolve( new File([blob], `model-avatar-${Date.now()}.png`, { type: "image/png", }) ); }, "image/png", 0.95 ); } catch (error) { URL.revokeObjectURL(objectUrl); reject(error); } }; image.onerror = () => { URL.revokeObjectURL(objectUrl); reject(new Error("图片读取失败")); }; image.src = objectUrl; }); const handleModelAvatarUpload = async (file: File) => { if (!file.type.startsWith("image/")) { toast.error("请上传图片文件"); return; } try { setModelAvatarUploading(true); const resizedFile = await resizeImageToSquare(file, 400); const formData = new FormData(); formData.append("file", resizedFile); const response = await fetchUploadWithRetry(formData); const result = await response.json(); if (!response.ok || !result?.url) { throw new Error(result?.error || "上传失败"); } setModelForm((prev) => ({ ...prev, avatarUrl: result.url })); toast.success("模型头像上传成功"); } catch (error: any) { toast.error(error?.message || "模型头像上传失败"); } finally { setModelAvatarUploading(false); } }; const handleContactQrUpload = async (file: File) => { if (!file.type.startsWith("image/")) { toast.error("请上传图片文件"); return; } try { setContactQrUploading(true); const formData = new FormData(); formData.append("file", file); const response = await fetchUploadWithRetry(formData); const result = await response.json(); if (!response.ok || !result?.url) { throw new Error(result?.error || "上传失败"); } setSiteSettings((prev) => ({ ...prev, contactConfig: { ...prev.contactConfig, wechatQrUrl: result.url, }, })); toast.success("微信二维码上传成功"); } catch (error: any) { toast.error(error?.message || "微信二维码上传失败"); } finally { setContactQrUploading(false); } }; const handleSaveModel = async () => { try { // 🆕 检查是否是火山引擎视频Provider 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({ 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} 个子模型)!` ); mutateModels(); setShowModelDialog(false); resetModelForm(); return; } // 普通模型的单个创建/编辑逻辑 const method = editingModel ? "PUT" : "POST"; const body = editingModel ? { ...modelForm, id: editingModel } : modelForm; const response = await fetch("/api/admin/ai-models", { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!response.ok) throw new Error("保存失败"); toast.success(editingModel ? "模型更新成功" : "模型创建成功"); mutateModels(); setShowModelDialog(false); resetModelForm(); } catch (error: any) { console.error("[Save Model] Error:", error); toast.error(error.message || "保存模型失败"); } }; const handleDeleteModel = async (id: string) => { if (!confirm("确定删除此模型?")) return; try { const response = await fetch(`/api/admin/ai-models?id=${id}`, { method: "DELETE", }); if (!response.ok) throw new Error("删除失败"); toast.success("模型删除成功"); mutateModels(); } catch (error) { toast.error("删除模型失败"); } }; const handleRestoreModelHealth = async (model: AiModel) => { try { const response = await fetch("/api/admin/ai-model-health", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ modelId: model.id }), }); const data = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(data?.error || "恢复模型失败"); } toast.success(`已恢复 ${model.displayName},失败计数已清零`); mutateModels(); mutateModelHealth(); } catch (error: any) { toast.error(error?.message || "恢复模型失败"); } }; // --- 存储配置逻辑 --- const resetStorageForm = () => { setStorageForm({ id: "", name: "", type: "s3", endpoint: "", region: "", bucket: "", accessKeyId: "", secretAccessKey: "", publicUrl: "", isDefault: false, isEnabled: true, }); setEditingStorage(null); }; const handleSaveStorage = async () => { try { const method = editingStorage ? "PUT" : "POST"; const body: any = editingStorage ? { ...storageForm, id: editingStorage } : { ...storageForm }; const normalizeUrl = (value?: string) => { if (!value) return ""; return value.trim().replace(/\/+$/, ""); }; const endpoint = normalizeUrl(body.endpoint); body.endpoint = /aliyuncs\.com/i.test(endpoint) ? `https://${endpoint.replace(/^https?:\/\//i, "")}` : endpoint; body.region = String(body.region || "").trim(); body.bucket = String(body.bucket || "").trim(); body.accessKeyId = String(body.accessKeyId || "").trim(); body.secretAccessKey = String(body.secretAccessKey || "").trim(); body.publicUrl = normalizeUrl(body.publicUrl); if (body.type === "qiniu-kodo" && !body.publicUrl) { throw new Error("七牛云 Kodo 需要配置公开访问域名"); } // 兼容处理:如果是阿里云 OSS,转换为 s3 类型提交 if (body.type === "aliyun-oss") { body.type = "s3"; } // 编辑状态下,留空密钥代表不修改 if (editingStorage) { if (!body.accessKeyId) delete body.accessKeyId; if (!body.secretAccessKey) delete body.secretAccessKey; } const response = await fetch("/api/admin/storage", { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!response.ok) throw new Error("保存失败"); toast.success(editingStorage ? "存储配置更新成功" : "存储配置创建成功"); mutateStorage(); setShowStorageDialog(false); resetStorageForm(); } catch (error) { toast.error(error instanceof Error ? error.message : "保存存储配置失败"); } }; const handleDeleteStorage = async (id: string) => { if (!confirm("确定删除此存储配置?")) return; try { const response = await fetch(`/api/admin/storage?id=${id}`, { method: "DELETE", }); if (!response.ok) throw new Error("删除失败"); toast.success("存储配置删除成功"); mutateStorage(); } catch (error) { toast.error("删除存储配置失败"); } }; const renderEnhanceProPresetEditor = ( preset: EnhanceProPreset, index: number, allowReorderControls = false ) => (
handleEnhanceProPresetChange(index, "name", e.target.value) } placeholder="例如:保真修复" value={preset.name} />
handleEnhanceProPresetChange( index, "description", e.target.value ) } placeholder="告诉用户这个预设适合什么图片" value={preset.description} />
{allowReorderControls ? ( <> ) : null}
handleEnhanceProPresetChange(index, "isDefault", checked) } /> 默认预设