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("audioFormat", value)} /> updateConfig("audioSpeed", normalizeAudioSpeedValue(String(value ?? defaultConfig.audioSpeed)))} />

音频指令

在音频节点没有单独填写时使用。

updateConfig("audioInstructions", event.target.value)} />
), prompts: , diagnostics: , storage: (
), }; return (
{shouldPromptContinue ? ( ) : 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))))); }