import { App, Button, Form, Input, InputNumber, Select } from "antd"; import { ArrowLeft, Boxes, Cloud, MessageSquareText, RadioTower, SlidersHorizontal, SquareTerminal } from "lucide-react"; import { useEffect, 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 { ModelDefaultGrid } from "./model-default-grid"; import { LocalCliSettings } from "./local-cli-settings"; import { PromptPreferencesPane } from "./prompt-preferences-pane"; type ConfigSectionKey = "local-cli" | "channels" | "models" | "preferences" | "prompts" | "storage"; 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: "models", label: "模型选择", description: "按领域选择默认模型", icon: }, { key: "preferences", label: "生成偏好", description: "画布、视频与音频默认值", icon: }, { key: "prompts", label: "提示词偏好", description: "按任务定制平台模板", icon: }, { key: "storage", 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 initialSection = isConfigSection(requestedSection) ? 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 = customChannelsEnabled ? configSections : configSections.filter((section) => section.key !== "channels"); useEffect(() => { if (isConfigSection(requestedSection) && (requestedSection !== "channels" || customChannelsEnabled)) { setActiveTab(requestedSection); return; } if (!customChannelsEnabled) setActiveTab((current) => (current === "channels" ? "models" : current)); }, [customChannelsEnabled, requestedSection]); 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) => { 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)); if (!effectiveConfig.channels.some(isChannelReady) && !hasReadyLocalRuntime) { 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")} />, models: ( 模型选择 按领域选择默认模型;模型能力与请求协议在渠道“模型与能力”中配置。 updateConfig(key, model)} /> ), 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: , 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))))); }
按领域选择默认模型;模型能力与请求协议在渠道“模型与能力”中配置。
画布、视频与音频默认值,节点内仍可单独覆盖。
设置新建生成任务时使用的初始值,节点内仍可单独覆盖。
用于新建音频节点和未单独设置参数的生成任务。
在音频节点没有单独填写时使用。