"use client"; import Link from "next/link"; import { useState, useEffect, useMemo, type ChangeEvent, type DragEvent, } from "react"; import useSWR from "swr"; import { toast } from "sonner"; import { cn, formatBeijingTime } from "@/lib/utils"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL, DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID, DETAIL_AGENT_LOCAL_MOONSHOT_OPTION_ID, DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY, normalizeDetailAgentLocalMoonshotConfig, } from "@/lib/ai/detail-agent-model-config"; import { ENHANCE_PRO_KREA_DEFAULT_BASE_URL, ENHANCE_PRO_KREA_DEFAULT_MODEL, ENHANCE_PRO_KREA_DEFAULT_OUTPUT_FORMAT, ENHANCE_PRO_KREA_SETTING_KEY, defaultEnhanceProKreaConfig, defaultEnhanceProPresets, normalizeEnhanceProKreaConfig, type EnhanceProPreset, } from "@/lib/enhance-pro/config"; import { defaultThirdPartySqlServerConfig, THIRD_PARTY_SQLSERVER_CONFIG_KEY, } from "@/lib/sqlserver/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 { defaultWorldLabsKeyPoolConfig, normalizeWorldLabsKeyPoolConfig, WORLDLABS_KEY_POOL_CONFIG_KEY, } from "@/lib/worldlabs/key-pool-config-shared"; import { defaultThreeDModelAliasConfig, normalizeThreeDModelAliasConfig, THREED_MODEL_ALIAS_CONFIG_SETTING_KEY, type ThreeDModelAliasEntry, } from "@/lib/model3d/model-alias-config"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Separator } from "@/components/ui/separator"; import { HomeAnimationEmbed } from "@/components/home/home-animation-embed"; import { Trash2, Save, User, Settings, Wrench, Bot, HardDrive, Plus, Edit, Eye, EyeOff, Menu, Key, MessageSquare, Bell, Image as ImageIcon, BarChart3, Search, Calendar, TrendingUp, Users, Clock, Sparkles, RefreshCw, ArrowUp, ArrowDown, Scissors, Expand, Move3D, Layers, Upload, Video, Loader2, Check, Paintbrush, Database, } from "lucide-react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Textarea } from "@/components/ui/textarea"; import { Switch } from "@/components/ui/switch"; import { defaultMembershipConfig, defaultMembershipUserOverrides, type MembershipFaqItem, type MembershipPlanConfig, normalizeMembershipConfig, normalizeMembershipUserOverrides, } from "@/lib/membership/config"; import { defaultPointsPackages, defaultPointsSystemConfig, normalizePointsPackages, normalizePointsSystemConfig, type EnhanceProPointsResolutionBucket, type PointsPackage, type PointsSystemConfig, } from "@/lib/points/config"; import { defaultDulupayConfig, normalizeDulupayConfig, type DulupayConfig, } from "@/lib/payment/dulupay-config"; import { DEFAULT_CANVAS_SKILL_TAGS, DEFAULT_CANVAS_SKILL_TAGS_TITLE, normalizeCanvasSkillTags, type CanvasSkillTag, } from "@/lib/canvas-skill-tags"; import { defaultAgentWorkflowUiConfig, normalizeAgentWorkflowUiConfig, } from "@/lib/agents/workflow-ui-config"; import { CANVAS_SKILL_ICON_OPTIONS, getCanvasSkillIconComponent, } from "@/components/canvas/canvas-skill-icons"; import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry"; import { isImageGenerationModelDescriptor, isVideoModelDescriptor, } from "@/lib/ai/model-classification"; import { buildVideoDurationCostMap, getConfiguredVideoDurationOptions, normalizeVideoDurationOptions, } from "@/lib/ai/video-model-config"; import type { AgentWorkflowUiConfig } from "@/types/agent"; const fetcher = (url: string) => fetch(url).then((res) => res.json()); 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"; function createEmptyCanvasSkillTag(): CanvasSkillTag { return { id: `tag-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, name: "", prompt: "", placeholder: "", iconName: "message-circle", iconUrl: "", }; } 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, }; } function LocalAiSectionToggleButton({ expanded, onClick, }: { expanded: boolean; onClick: () => void; }) { return ( ); } 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" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "bfl-image" | "ruxa-image" | "fal-image" | "openai-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "worldlabs-3d" | "kittycad-cad" | "runway" | "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; // 是否支持随机种子 creditCostPerImage?: number; // 每张图片积分消耗 creditCostPerVideo?: number; // 每条视频积分消耗 videoDurationOptions?: number[]; // 视频秒数选项 creditCostPerVideoByDuration?: Record; // 各秒数积分 enableLoadingPlaceholder?: boolean; // 是否启用加载占位图 loadingColor?: string; // 占位图主题色,如 "purple", "blue", "green" }; function createDefaultImageGenConfig(): ImageGenConfig { return { maxImages: 2, supportsSize: false, supports1K: true, supports2K: true, supports4K: true, supportsPromptExtend: false, supportsNegativePrompt: false, supportsWatermark: false, supportsSeed: false, creditCostPerImage: 0, creditCostPerVideo: 0, videoDurationOptions: [], creditCostPerVideoByDuration: {}, enableLoadingPlaceholder: true, loadingColor: "purple", }; } function formatVideoDurationOptionsInput(options?: number[]) { return getConfiguredVideoDurationOptions({ videoDurationOptions: options }).join(", "); } function parseVideoDurationOptionsInput(value: string) { return normalizeVideoDurationOptions( String(value || "") .split(/[,\n,、\s]+/) .map((item) => item.trim()) .filter(Boolean) ); } function syncVideoDurationPricing(config: ImageGenConfig | undefined, durationOptions: number[]) { const nextConfig = config || createDefaultImageGenConfig(); return { ...nextConfig, videoDurationOptions: durationOptions, creditCostPerVideoByDuration: buildVideoDurationCostMap({ durationOptions, costMap: nextConfig.creditCostPerVideoByDuration, fallbackCost: nextConfig.creditCostPerVideo, }) || {}, }; } type AiModel = { id: string; providerId: string; modelId: string; displayName: string; avatarUrl?: string; description?: string; supportsVision: boolean; supportsImageGen: boolean; isEnabled: boolean; isHidden: boolean; isDefault: boolean; sortOrder: number; fallbackModelId1?: string; fallbackModelId2?: string; excludeFromFailover?: boolean; isAgentModel?: boolean; agentCodingModelId?: string; isMultimodal?: boolean; imageGenConfig?: ImageGenConfig; // 🆕 图片生成特性配置 }; type StorageConfig = { id: string; name: string; type: "s3" | "local" | "aliyun-oss"; endpoint?: string; region?: string; bucket?: string; accessKeyId?: string; secretAccessKey?: string; publicUrl?: string; isDefault: boolean; isEnabled: boolean; }; 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; toolbox1Name: string; toolbox1Enabled: boolean; toolbox2Name: string; toolbox2Enabled: boolean; tutorialVideoUrl: string | null; tutorialEnabled: boolean; rankingEnabled: boolean; productPlanningEnabled: boolean; canvasEnabled: boolean; agentEnabled: boolean; canvasAgentModeEnabled: boolean; videoCanvasSidebarEnabled: boolean; agentCategoryTreeConfig: { productCategories: string[]; audienceTags: string[]; usageScenarios: string[]; brands: string[]; }; agentSecondaryTypeConfig: { image: string[]; video: string[]; text: string[]; }; agentWorkflowUiConfig: AgentWorkflowUiConfig; realtimeCanvasEnabled: boolean; aiDetailGenerationEnabled: boolean; canvasDefaultModelId: string | null; canvasAutoModeDefaultModelId: 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; homeVariant: "classic" | "lovart"; 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, }; const defaultContactConfig: ContactConfig = { phone: "", wechatQrUrl: "", }; 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; count: number; }>; monthlyStats: Array<{ month: string; count: number }>; hourlyStats: Array<{ hour: number; count: number }>; }; 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: "openai-image", label: "OpenAI 图片兼容" }, { 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: "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", "openai-image": "https://api.openai.com/v1", midjourney: "https://your-midjourney-api.example.com", "fal-video": "https://fal.run", "tripo-3d": "https://api.tripo3d.ai/v2/openapi", "worldlabs-3d": "https://api.worldlabs.ai", "kittycad-cad": "https://api.zoo.dev", runway: "https://api.dev.runwayml.com", 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 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 /^https?:\/\//i.test(value.trim()); } function isLemonApiBaseUrl(value?: string | null) { return ( String(value || "") .trim() .replace(/\/+$/, "") === LEMON_OPENAI_BASE_URL ); } function isMoonshotOfficialBaseUrl(value?: string | null) { return ( String(value || "") .trim() .replace(/\/+$/, "") === DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL ); } export function AdminSettingsClient() { const [activeTab, setActiveTab] = useState("users"); // 用户管理状态 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: 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: 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: homeAnimationSettingData, mutate: mutateHomeAnimationSetting } = useSWR<{ setting?: { value?: unknown } }>( `/api/admin/settings?key=${HOME_CHAT_ANIMATION_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 detailAgentModelOptions = useMemo( () => (modelsData?.models || []).filter( (model) => !model.supportsImageGen && model.isEnabled && !model.isHidden ), [modelsData?.models] ); // 网站设置状态 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: "升级会员, Nano Banana Pro 免费365天!", bannerLink: null, toolbox1Name: "工具箱", toolbox1Enabled: true, toolbox2Name: "工具箱2", toolbox2Enabled: false, tutorialVideoUrl: null, tutorialEnabled: false, rankingEnabled: true, productPlanningEnabled: true, canvasEnabled: true, agentEnabled: true, canvasAgentModeEnabled: true, videoCanvasSidebarEnabled: true, agentCategoryTreeConfig: { productCategories: [], audienceTags: [], usageScenarios: [], brands: [], }, agentSecondaryTypeConfig: { image: [], video: [], text: [], }, agentWorkflowUiConfig: defaultAgentWorkflowUiConfig, realtimeCanvasEnabled: true, aiDetailGenerationEnabled: false, canvasDefaultModelId: null, canvasAutoModeDefaultModelId: null, canvasDetailDefaultModelId: null, canvasDetailAgentModelId: null, lovartComposerPrefix: "让Buyi-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: "懂你的设计代理,帮你搞定一切", homeVariant: "classic", surpriseCategories: ["全部", "精选"], membershipConfig: defaultMembershipConfig, membershipUserOverrides: defaultMembershipUserOverrides, }); const [homeAnimationConfig, setHomeAnimationConfig] = useState({ enabled: false, fileName: "", htmlContent: "", updatedAt: 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 ); setSiteSettings({ 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 || "升级会员, Nano Banana Pro 免费365天!", bannerLink: siteSettingsData.bannerLink || null, 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, 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, aiDetailGenerationEnabled: siteSettingsData.aiDetailGenerationEnabled ?? false, canvasDefaultModelId: siteSettingsData.canvasDefaultModelId || null, canvasAutoModeDefaultModelId: siteSettingsData.canvasAutoModeDefaultModelId || null, canvasDetailDefaultModelId: siteSettingsData.canvasDetailDefaultModelId || null, canvasDetailAgentModelId: (siteSettingsData as any).canvasDetailAgentModelId || null, lovartComposerPrefix: String((siteSettingsData as any).lovartComposerPrefix || "").trim() || "让Buyi-ai", lovartComposerPreviewTexts: Array.isArray( siteSettingsData.lovartComposerPreviewTexts ) ? 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 || "懂你的设计代理,帮你搞定一切", homeVariant: siteSettingsData.homeVariant === "lovart" ? "lovart" : "classic", surpriseCategories: Array.isArray(siteSettingsData.surpriseCategories) ? siteSettingsData.surpriseCategories .map((item: any) => String(item).trim()) .filter(Boolean) : ["全部", "惊喜"], membershipConfig: normalizedMembershipConfig, membershipUserOverrides: normalizedMembershipOverrides, }); } }, [siteSettingsData]); useEffect(() => { const raw = homeAnimationSettingData?.setting?.value && typeof homeAnimationSettingData.setting.value === "object" && !Array.isArray(homeAnimationSettingData.setting.value) ? (homeAnimationSettingData.setting.value as Record) : {}; setHomeAnimationConfig({ 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, }); }, [homeAnimationSettingData]); 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: "rmbg" as "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({ 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 [enhanceProPresetEditorMode, setEnhanceProPresetEditorMode] = useState< "simple" | "advanced" >("simple"); const [selectedEnhanceProPresetId, setSelectedEnhanceProPresetId] = useState(""); const [draggingEnhanceProPresetId, setDraggingEnhanceProPresetId] = useState(""); // 图像拓展API配置状态 const [extendConfig, setExtendConfig] = useState({ 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 "replicate", replicateApiToken: "", replicateModel: "qwen/qwen-image-layered", layers: 4, resolution: 640, trueCfgScale: 4, numInferenceSteps: 30, cfgNormalize: true, useEnPrompt: true, negativePrompt: "", timeoutMs: 300000, }); // 实时画布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 [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 [threeDModelAliasConfig, setThreeDModelAliasConfig] = useState( defaultThreeDModelAliasConfig() ); 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: "Moonshot AI (Official)", baseUrl: DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL, apiKey: "", modelId: DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID, displayName: "官方 Kimi K2.5", isEnabled: true, }); const [thirdPartySqlServerConfig, setThirdPartySqlServerConfig] = useState({ ...defaultThirdPartySqlServerConfig, }); const [isTestingThirdPartySqlServer, setIsTestingThirdPartySqlServer] = useState(false); const [expandedLocalAiSections, setExpandedLocalAiSections] = useState< Record >({}); const [ isThirdPartySqlServerFieldsVisible, setIsThirdPartySqlServerFieldsVisible, ] = useState(false); const [productPlanningApiConfig, setProductPlanningApiConfig] = useState(defaultProductPlanningApiConfig()); const [productPlanningAiModelConfig, setProductPlanningAiModelConfig] = useState( defaultProductPlanningAiModelConfig() ); // 加载抠图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?: "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 || "rmbg", 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 { apiUrl?: string; apiKey?: string; type?: string; scaleFactor?: number; useBackupApi?: boolean; backupApiUrl?: string; backupApiKey?: string; backupScale?: string; backupModel?: string; }; setEnhanceConfig({ 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) ); }, [enhanceProConfigData]); 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 { apiKey?: string; xScale?: number; yScale?: number; }; setExtendConfig({ 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( 30000, Math.min(900000, Number(config.timeoutMs) || 300000) ), }); } }, [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(65535, 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(120000, Number(config.connectionTimeoutMs) || 15000) ), requestTimeoutMs: Math.max( 1000, Math.min(120000, Number(config.requestTimeoutMs) || 30000) ), }); } }, [thirdPartySqlServerConfigData]); const isThirdPartySqlServerSectionExpanded = Boolean( expandedLocalAiSections["third-party-sqlserver"] ); useEffect(() => { if (!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 (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 worldlabsProvider = (providersData?.providers || []).find( (provider) => provider.type === "worldlabs-3d" ); if (worldlabsProvider) { setWorldlabs3dConfig({ providerId: worldlabsProvider.id, name: worldlabsProvider.name || "World Labs", baseUrl: worldlabsProvider.baseUrl || getProviderDefaultBaseUrl("worldlabs-3d"), apiKey: "", isEnabled: worldlabsProvider.isEnabled, }); return; } setWorldlabs3dConfig({ providerId: "", name: "World Labs", baseUrl: getProviderDefaultBaseUrl("worldlabs-3d"), apiKey: "", isEnabled: true, }); }, [providersData?.providers]); useEffect(() => { setWorldlabsKeyPoolConfig( normalizeWorldLabsKeyPoolConfig(worldlabsKeyPoolConfigData?.setting?.value) ); }, [worldlabsKeyPoolConfigData]); useEffect(() => { setThreeDModelAliasConfig( normalizeThreeDModelAliasConfig(threeDModelAliasConfigData?.setting?.value) ); }, [threeDModelAliasConfigData]); 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 [canvasSkillTags, setCanvasSkillTags] = useState( DEFAULT_CANVAS_SKILL_TAGS ); const [canvasSkillTagUploadingId, setCanvasSkillTagUploadingId] = useState< string | null >(null); const { data: canvasSkillTagsData, mutate: mutateCanvasSkillTags } = useSWR<{ enabled?: boolean; title?: string; tags?: CanvasSkillTag[]; }>("/api/canvas-skill-tags", fetcher); // 系统通知状态 const [notification, setNotification] = useState<{ enabled: boolean; title: string; content: string; }>({ enabled: false, title: "", content: "", }); useEffect(() => { fetch("/api/admin/settings?key=login_notification") .then((res) => res.json()) .then((data) => { if (data.setting?.value) { setNotification(data.setting.value); } }); }, []); const handleSaveNotification = async () => { try { await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "login_notification", value: notification, }), }); toast.success("系统通知已保存"); } catch (error) { toast.error("保存失败"); } }; useEffect(() => { if (questionsData) { setQuestions(questionsData); } }, [questionsData]); useEffect(() => { if (canvasSkillTagsData) { setCanvasSkillTagsEnabled(canvasSkillTagsData.enabled !== false); setCanvasSkillTagsTitle( String(canvasSkillTagsData.title || "").trim() || DEFAULT_CANVAS_SKILL_TAGS_TITLE ); 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", 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 handleAddCanvasSkillTag = () => { setCanvasSkillTags((prev) => [...prev, createEmptyCanvasSkillTag()]); }; const handleRemoveCanvasSkillTag = (index: number) => { setCanvasSkillTags((prev) => prev.filter((_, itemIndex) => itemIndex !== index) ); }; const handleSaveCanvasSkillTags = async () => { try { 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, 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 ); 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" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "bfl-image" | "ruxa-image" | "fal-image" | "openai-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "worldlabs-3d" | "kittycad-cad" | "runway" | "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, isAgentModel: false, agentCodingModelId: "", isMultimodal: false, sortOrder: 0, fallbackModelId1: "", // 备用模型 ID 1 fallbackModelId2: "", // 备用模型 ID 2 excludeFromFailover: false, // 🆕 图片生成模型特性配置 imageGenConfig: createDefaultImageGenConfig(), }); 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 isVideoProviderForModelConfig = isVideoModelDescriptor({ providerType: selectedModelProviderType, modelId: modelForm.modelId, supportsImageGen: modelForm.supportsImageGen, }); const isImageProviderForModelConfig = isImageGenerationModelDescriptor({ providerType: selectedModelProviderType, modelId: modelForm.modelId, supportsImageGen: modelForm.supportsImageGen, }); const configuredVideoDurationOptions = getConfiguredVideoDurationOptions( modelForm.imageGenConfig ); const modelKindHint = selectedModelProviderType === "runway" && !modelForm.modelId.trim() ? "Runway 同时支持图片与视频模型,填写模型 ID 后系统会自动识别标签。" : isVideoProviderForModelConfig ? "当前识别为视频模型,将使用视频能力、视频积分和视频路由。" : isImageProviderForModelConfig ? "当前识别为图片模型,将使用生图能力、图片积分和生图路由。" : "当前识别为对话/分析模型。"; // 存储配置状态 const [storageForm, setStorageForm] = useState({ id: "", name: "", type: "s3" as "s3" | "local" | "aliyun-oss", 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 : 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 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 payload = { ...siteSettings, homeAnimationConfig: { enabled: homeAnimationConfig.enabled, fileName: homeAnimationConfig.fileName.trim(), htmlContent: normalizedHomeAnimationHtml, updatedAt: homeAnimationConfig.updatedAt, }, membershipConfig: parsedMembershipConfig, membershipUserOverrides: parsedMembershipOverrides, pointsSystemConfig: parsedPointsSystemConfig, pointsPackages: parsedPointsPackages, }; 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(nextSiteSettings); setHomeAnimationConfig((prev) => ({ ...prev, htmlContent: normalizedHomeAnimationHtml, updatedAt: new Date().toISOString(), })); toast.success("网站设置保存成功"); mutateSiteSettings(); 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 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 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.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: { 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 { if (enhanceProConfig.enabled) { if (!enhanceProConfig.baseUrl.trim()) { toast.error("请输入 Krea API 地址"); return; } if (!enhanceProConfig.apiKey.trim()) { toast.error("请输入 Krea API 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: { enabled: enhanceProConfig.enabled, providerName: enhanceProConfig.providerName.trim() || "Krea Topaz Generative", baseUrl: enhanceProConfig.baseUrl.trim() || ENHANCE_PRO_KREA_DEFAULT_BASE_URL, apiKey: enhanceProConfig.apiKey.trim(), defaultModel: enhanceProConfig.defaultModel, defaultOutputFormat: enhanceProConfig.defaultOutputFormat, allowBatchSize: enhanceProConfig.allowBatchSize, maxBatchSize: enhanceProConfig.maxBatchSize, maxOutputEdge: enhanceProConfig.maxOutputEdge, enableAdvancedControls: enhanceProConfig.enableAdvancedControls, presets: enhanceProConfig.presets, }, }), }); 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 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.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: { apiKey: extendConfig.apiKey.trim(), xScale: xScale, yScale: 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 response = await 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, }, }), }); if (!response.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( 30000, Math.min(900000, Number(layerSplitConfig.timeoutMs) || 300000) ), }, }), }); 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(), qaApiUrl: productPlanningApiConfig.qaApiUrl.trim(), qaToken: productPlanningApiConfig.qaToken.trim(), saleApiUrl: productPlanningApiConfig.saleApiUrl.trim(), saleToken: productPlanningApiConfig.saleToken.trim(), }, }), }), 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(65535, 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( 120000, Number(thirdPartySqlServerConfig.connectionTimeoutMs) || 15000 ) ), requestTimeoutMs: Math.max( 1000, Math.min( 120000, Number(thirdPartySqlServerConfig.requestTimeoutMs) || 30000 ) ), }, }), }); 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(65535, 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( 120000, Number(thirdPartySqlServerConfig.connectionTimeoutMs) || 15000 ) ), requestTimeoutMs: Math.max( 1000, Math.min( 120000, Number(thirdPartySqlServerConfig.requestTimeoutMs) || 30000 ) ), }, }), }); 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 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 isLemonProvider = providerForm.type === "openai" && isLemonApiBaseUrl( 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 && isLemonProvider && providerId) { try { await handleQuickAddLemonModels(providerId); } catch (modelError) { console.error("[Auto Add Lemon Models] Error:", modelError); toast.warning( 'LemonAPI 预设模型添加失败,请手动点击"快速添加 Gemini 预设模型"按钮' ); } } } catch (error) { toast.error("保存提供商失败"); } }; const handleDeleteProvider = async (id: string) => { if (!confirm("确定删除?关联模型也会被删除。")) return; try { const response = await fetch(`/api/admin/ai-providers?id=${id}`, { method: "DELETE", }); 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 handleSaveWorldlabs3dConfig = async () => { try { const trimmedName = worldlabs3dConfig.name.trim() || "World Labs"; const trimmedBaseUrl = worldlabs3dConfig.baseUrl.trim() || getProviderDefaultBaseUrl("worldlabs-3d"); const hasProvider = Boolean(worldlabs3dConfig.providerId); if (!hasProvider && !worldlabs3dConfig.apiKey.trim()) { toast.error("请填写 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: normalizeWorldLabsKeyPoolConfig(worldlabsKeyPoolConfig), }), }); 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: "" })); mutateProviders(); mutateWorldlabsKeyPoolConfig(); } catch (error) { console.error("保存 World Labs 配置失败:", error); toast.error( error instanceof Error ? error.message : "保存 World Labs 配置失败" ); } }; 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 = moonshotDetailAgentConfig.baseUrl.trim() || DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL; const trimmedModelId = moonshotDetailAgentConfig.modelId.trim() || DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID; const trimmedDisplayName = moonshotDetailAgentConfig.displayName.trim() || "官方 Kimi K2.5"; const trimmedProviderName = moonshotDetailAgentConfig.providerName.trim() || "Moonshot AI (Official)"; if (!isMoonshotOfficialBaseUrl(trimmedBaseUrl)) { toast.error( "请使用 Moonshot 官方 API 地址:https://api.moonshot.cn/v1" ); return; } if (!trimmedModelId) { toast.error("请填写官方 Kimi K2.5 的 API 模型 ID"); return; } if ( !moonshotDetailAgentConfig.providerId && !moonshotDetailAgentConfig.apiKey.trim() ) { toast.error("请填写 Moonshot 官方 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("Moonshot 官方 Kimi K2.5 已绑定为 AI详情 Agent 专用模型"); } catch (error) { console.error("[Moonshot Detail Agent Config] Error:", error); toast.error( error instanceof Error ? error.message : "保存 Moonshot Kimi 配置失败" ); } }; // 快速添加火山引擎视频预设模型 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 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, isAgentModel: false, agentCodingModelId: "", isMultimodal: false, sortOrder: 0, fallbackModelId1: "", fallbackModelId2: "", excludeFromFailover: false, imageGenConfig: createDefaultImageGenConfig(), }); 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 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); // 兼容处理:如果是阿里云 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("保存存储配置失败"); } }; 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="例如:保真修复" />
handleEnhanceProPresetChange( index, "description", e.target.value ) } placeholder="告诉用户这个预设适合什么图片" />
{allowReorderControls ? ( <> ) : null}
handleEnhanceProPresetChange(index, "isDefault", checked) } /> 默认预设