import { App, Button, Form, Input, InputNumber, Select } from "antd"; import { ArrowLeft, Boxes, Bug, Cloud, MessageSquareText, MonitorUp, RadioTower, SlidersHorizontal, SquareTerminal, Workflow } from "lucide-react"; import { useEffect, useMemo, useState, type ReactNode } from "react"; import { useNavigate, useSearchParams } from "react-router"; import { UserOSSSettingsForm } from "@/components/layout/user-oss-settings-form"; import { audioFormatOptions, audioVoiceOptions, normalizeAudioSpeedValue } from "@/lib/audio-generation"; import { refreshSystemChannels } from "@/lib/user-session"; import { defaultConfig, useConfigStore, useEffectiveConfig } from "@/stores/use-config-store"; import { useUserStore } from "@/stores/use-user-store"; import { ChannelSettingsPane, channelValidationError, focusInvalidChannelField, isChannelReady } from "./channel-settings-pane"; export { UserLocalChannelFields, UserLocalChannelSwitch, userLocalChannelChangePatch, userLocalChannelFormOwner } from "./channel-settings-pane"; import { ComfyUIBridgeSettingsPane } from "./comfyui-bridge-settings-pane"; import { ModelDefaultGrid } from "./model-default-grid"; import { LocalCliSettings } from "./local-cli-settings"; import { PromptPreferencesPane } from "./prompt-preferences-pane"; import DiagnosticsPanel from "./diagnostics-panel"; import { RunningHubSettingsPane } from "./runninghub-settings-pane"; import { COMFYUI_PLUGIN_ID, RUNNINGHUB_PLUGIN_ID } from "@/lib/plugins/builtin/workflows"; import { usePluginStore } from "@/stores/use-plugin-store"; type ConfigSectionKey = "local-cli" | "channels" | "models" | "runninghub" | "comfyui" | "preferences" | "prompts" | "storage" | "diagnostics"; const configSections: Array<{ key: ConfigSectionKey; label: string; description: string; icon: ReactNode }> = [ { key: "local-cli", label: "本机工具", description: "连接 Runtime 与官方 CLI", icon: }, { key: "channels", label: "个人渠道", description: "模型服务与个人工作流", icon: }, { key: "runninghub", label: "RunningHub 工作流", description: "个人渠道的云端工作流配置", icon: }, { key: "comfyui", label: "ComfyUI Bridge", description: "个人渠道的 Bridge 工作流配置", icon: }, { key: "models", label: "模型选择", description: "按领域选择默认模型", icon: }, { key: "preferences", label: "生成偏好", description: "画布、视频与音频默认值", icon: }, { key: "prompts", label: "提示词偏好", description: "按任务定制平台模板", icon: }, { key: "storage", label: "我的对象存储", description: "管理个人媒体存储", icon: }, { key: "diagnostics", label: "问题诊断", description: "导出日志协助排查", icon: }, ]; export function isConfigSection(value: string | null): value is ConfigSectionKey { return configSections.some((section) => section.key === value); } export default function SettingsPage() { const { message } = App.useApp(); const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const requestedSection = searchParams.get("section"); const customChannelsEnabled = useUserStore((state) => state.features.customChannelsEnabled); const runtimeStatuses = usePluginStore((state) => state.runtimeStatuses); const runningHubPluginEnabled = runtimeStatuses[RUNNINGHUB_PLUGIN_ID] === "enabled"; const comfyUIPluginEnabled = runtimeStatuses[COMFYUI_PLUGIN_ID] === "enabled"; const requestedSectionEnabled = requestedSection !== "runninghub" && requestedSection !== "comfyui" || requestedSection === "runninghub" && runningHubPluginEnabled || requestedSection === "comfyui" && comfyUIPluginEnabled; const initialSection = isConfigSection(requestedSection) && requestedSectionEnabled ? requestedSection : customChannelsEnabled ? "channels" : "models"; const [activeTab, setActiveTab] = useState(initialSection === "channels" && !customChannelsEnabled ? "models" : initialSection); const config = useConfigStore((state) => state.config); const effectiveConfig = useEffectiveConfig(); const updateConfig = useConfigStore((state) => state.updateConfig); const shouldPromptContinue = searchParams.get("continue") === "1"; const userId = useUserStore((state) => state.user?.id); const userChannels = config.channels.filter((channel) => channel.scope !== "system"); const visibleConfigSections = useMemo(() => (customChannelsEnabled ? configSections : configSections.filter((section) => section.key !== "channels")) .filter((section) => section.key !== "runninghub" || runningHubPluginEnabled) .filter((section) => section.key !== "comfyui" || comfyUIPluginEnabled), [comfyUIPluginEnabled, customChannelsEnabled, runningHubPluginEnabled]); const isVisibleConfigSection = (value: string | null): value is ConfigSectionKey => isConfigSection(value) && visibleConfigSections.some((section) => section.key === value); useEffect(() => { if (isVisibleConfigSection(requestedSection)) { setActiveTab(requestedSection); return; } setActiveTab((current) => visibleConfigSections.some((section) => section.key === current) ? current : customChannelsEnabled ? "channels" : "models"); }, [customChannelsEnabled, requestedSection, visibleConfigSections]); useEffect(() => { if (!userId) return; let cancelled = false; void refreshSystemChannels().catch((error) => { if (!cancelled) message.warning(error instanceof Error ? `系统模型刷新失败:${error.message}` : "系统模型刷新失败,继续使用本地缓存"); }); return () => { cancelled = true; }; }, [message, userId]); const selectSection = (section: ConfigSectionKey) => { if ((section === "runninghub" && !runningHubPluginEnabled) || (section === "comfyui" && !comfyUIPluginEnabled)) return; setActiveTab(section); const next = new URLSearchParams(searchParams); next.set("section", section); setSearchParams(next, { replace: true }); }; const finishConfig = () => { const invalidChannel = customChannelsEnabled ? userChannels.find((channel) => channelValidationError(channel)) : undefined; if (invalidChannel) { selectSection("channels"); message.warning(`${invalidChannel.name || "未命名渠道"}:${channelValidationError(invalidChannel)}`); focusInvalidChannelField(invalidChannel); return; } const hasReadyLocalRuntime = effectiveConfig.channels.some((channel) => channel.transport === "local-runtime" && channel.enabled !== false && Boolean(channel.localModels?.length)); const workflowReady = Boolean( (runningHubPluginEnabled && config.runningHub.enabled && config.runningHub.workflowId.trim() && config.runningHub.baseUrl.trim() && config.runningHub.apiKey.trim()) || (comfyUIPluginEnabled && config.comfyBridge.enabled && config.comfyBridge.bridgeId.trim() && config.comfyBridge.workflowId.trim()), ); if (!effectiveConfig.channels.some(isChannelReady) && !hasReadyLocalRuntime && !workflowReady) { selectSection(customChannelsEnabled ? "channels" : "models"); message.error(customChannelsEnabled ? (shouldPromptContinue ? "请先完成至少一个渠道的 Base URL、API Key 和模型配置" : "当前没有可用渠道,请先完成连接信息和模型配置") : "当前没有可用的系统模型,请联系管理员配置系统渠道"); return; } message.success("配置已保存,正在返回创作页面"); navigate(-1); }; const panes: Record = { "local-cli": , channels: selectSection("models")} onOpenRunningHub={runningHubPluginEnabled ? () => selectSection("runninghub") : undefined} onOpenComfyUI={comfyUIPluginEnabled ? () => selectSection("comfyui") : undefined} />, models: ( 模型选择 按领域选择默认模型;模型能力与请求协议在渠道“模型与能力”中配置。 updateConfig(key, model)} /> ), runninghub: , comfyui: , preferences: ( 生成偏好 画布、视频与音频默认值,节点内仍可单独覆盖。 画布生成 设置新建生成任务时使用的初始值,节点内仍可单独覆盖。 updateConfig("canvasImageCount", normalizeImageCount(String(value ?? defaultConfig.canvasImageCount)))} /> 音频默认值 用于新建音频节点和未单独设置参数的生成任务。 updateConfig("audioVoice", value)} /> updateConfig("audioFormat", value)} /> updateConfig("audioSpeed", normalizeAudioSpeedValue(String(value ?? defaultConfig.audioSpeed)))} /> 音频指令 在音频节点没有单独填写时使用。 updateConfig("audioInstructions", event.target.value)} /> ), prompts: , diagnostics: , storage: ( ), }; return ( {shouldPromptContinue ? ( navigate(-1)} aria-label="返回创作页面" title="返回创作页面"> ) : null} 设置 {shouldPromptContinue ? 保存并返回 : null} {panes[activeTab]} ); } function SettingsPane({ children, fill = false }: { children: ReactNode; fill?: boolean }) { return {children}; } function normalizeImageCount(value: string) { return String(Math.max(1, Math.min(15, Math.floor(Math.abs(Number(value)) || Number(defaultConfig.canvasImageCount))))); }
按领域选择默认模型;模型能力与请求协议在渠道“模型与能力”中配置。
画布、视频与音频默认值,节点内仍可单独覆盖。
设置新建生成任务时使用的初始值,节点内仍可单独覆盖。
用于新建音频节点和未单独设置参数的生成任务。
在音频节点没有单独填写时使用。