(() => { /** * 文件职责:配置渲染核心组件,基于 Schema 动态生成表单并处理保存/会话差异配置逻辑。 */ const { Box, TextField, Switch, Typography, Button, Accordion, AccordionSummary, AccordionDetails, Paper, Chip, Slider, MenuItem, ToggleButton, ToggleButtonGroup } = MaterialUI; const { useState, useEffect, useRef, useLayoutEffect } = React; // 递归扫描 Schema 中所有 object 节点,生成“全部展开 / 收起”功能需要的路径列表。 const getAllExpandablePaths = (schema, prefix = '') => { let paths = []; Object.entries(schema).forEach(([key, value]) => { const currentPath = prefix ? `${prefix}.${key}` : key; if (value.type === 'object' && value.items) { paths.push(currentPath); paths = paths.concat(getAllExpandablePaths(value.items, currentPath)); } }); return paths; }; // 为常见配置组与字段定义 emoji 图标,提升配置页的可扫读性。 const CONFIG_ICONS = { friend_settings: '👤', group_settings: '👥', web_admin: '🌐', auto_trigger_settings: '🤖', schedule_settings: '🕒', tts_settings: '🔊', segmented_reply_settings: '🔪', enable: '✅', session_list: '📋', proactive_prompt: '🧠', group_idle_trigger_minutes: '⏳', enable_auto_trigger: '✅', auto_trigger_after_minutes: '⏱️', min_interval_minutes: '⏱️', max_interval_minutes: '⏱️', quiet_hours: '🌙', max_unanswered_times: '🛑', enable_tts: '💬', always_send_text: '🔤', words_count_threshold: '📏', split_mode: 'Ⓜ️', regex: '🧩', split_words: '📝', interval_method: '⏱️', interval: '🎲', log_base: '📈', enabled: '✅', host: '📡', port: '🔌', password: '🔑', session_name: '🏷️' }; // 某些 Schema 描述文本自身已带 emoji;这里剥离前缀,避免界面上图标重复出现。 const LEADING_EMOJI_REGEX = /^\s*(?:\p{Extended_Pictographic}(?:\uFE0F|\u200D\p{Extended_Pictographic})*)\s*/u; const stripLeadingEmoji = (text) => { if (typeof text !== 'string') return text; return text.replace(LEADING_EMOJI_REGEX, '').trimStart(); }; /** * 配置字段渲染组件 * 根据后端返回的 Schema 动态渲染不同类型的输入控件 */ function ConfigField({ fieldKey, schema, value, onChange, depth = 0, path = '', expandedKeys = [], onToggleExpand = () => {} }) { // hidden 字段完全不渲染,通常用于后端保留字段或不希望前端直接编辑的项。 if (schema.hidden) return null; // 对输入控件使用一层本地态,便于处理 Slider / 文本输入中的过渡值。 const [localValue, setLocalValue] = useState(value); useEffect(() => { // 当外部配置值变化(如切换会话 / 回滚 / 重新加载)时,同步重置局部编辑态。 setLocalValue(value); }, [value]); const handleChange = (newValue) => { // 所有字段都通过统一出口把最新值同时写入本地态与父级配置对象。 setLocalValue(newValue); onChange(newValue); }; const rawTitle = schema.description || fieldKey; const titleText = stripLeadingEmoji(rawTitle) || rawTitle; const icon = CONFIG_ICONS[fieldKey] || '⚙️'; const currentPath = path ? `${path}.${fieldKey}` : fieldKey; // object + items 表示一个可折叠的配置分组,内部再递归渲染子字段。 if (schema.type === 'object' && schema.items) { const isExpanded = expandedKeys.includes(currentPath); return ( onToggleExpand(currentPath)} elevation={0} sx={{ '&:before': { display: 'none' }, bgcolor: 'transparent' }} > {isExpanded ? '▲' : '▼'} } sx={{ px: 2, py: 1, minHeight: '48px !important', bgcolor: depth === 0 ? 'rgba(0, 90, 193, 0.06)' : depth === 1 ? 'rgba(0, 90, 193, 0.02)' : 'transparent', '&:hover': { bgcolor: depth === 0 ? 'rgba(0, 90, 193, 0.1)' : 'rgba(0, 90, 193, 0.05)' }, transition: 'all 0.2s ease', '& .MuiAccordionSummary-expandIconWrapper': { transform: 'none !important' }, '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { transform: 'none !important' } }} > {icon} 0 ? 'primary.dark' : 'text.primary', fontSize: depth === 0 ? '0.95rem' : '0.875rem' }} > {titleText} {schema.hint && depth === 0 && ( {schema.hint} )} {depth === 0 && ( )} {depth > 0 && ( {isExpanded ? '收起' : '展开'} )} {Object.entries(schema.items).map(([key, subSchema]) => ( handleChange({ ...localValue, [key]: newValue })} depth={depth + 1} path={currentPath} expandedKeys={expandedKeys} onToggleExpand={onToggleExpand} /> ))} ); } // 通用左侧描述区,统一标题与 hint 的排版逻辑,减少各字段类型重复 JSX。 const DescriptionSection = ({ flex = 8 }) => ( {schema.description || fieldKey} {schema.hint && ( {schema.hint} )} ); // 布尔配置映射为开关组件,适合表示 enable / always_send_text 这类 on-off 项。 if (schema.type === 'bool' || schema.type === 'boolean') { return ( handleChange(e.target.checked)} sx={{ width: 52, height: 32, padding: 0, '& .MuiSwitch-switchBase': { padding: 0, margin: '4px', transitionDuration: '300ms', '&.Mui-checked': { transform: 'translateX(20px)', color: '#fff', '& + .MuiSwitch-track': { backgroundColor: 'primary.main', opacity: 1, border: 0, }, }, }, '& .MuiSwitch-thumb': { boxSizing: 'border-box', width: 24, height: 24, boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)', }, '& .MuiSwitch-track': { borderRadius: 16, backgroundColor: 'rgba(0, 0, 0, 0.12)', opacity: 1, transition: 'background-color 300ms', }, }} /> ); } // 数值字段支持“滑杆 + 数字输入框”的双通道编辑,兼顾直观性与精确输入。 if (['integer', 'int', 'number', 'float', 'double'].includes(schema.type)) { const sliderConfig = schema.slider; const hasRange = sliderConfig !== undefined; // 如果有范围,展示 Slider,布局 5:3:2;否则回退到 8:2 const descFlex = hasRange ? 5 : 8; const min = sliderConfig?.min ?? schema.minimum; const max = sliderConfig?.max ?? schema.maximum; const step = sliderConfig?.step ?? (schema.type === 'integer' || schema.type === 'int' ? 1 : 0.1); const fallbackNumber = schema.default ?? min ?? 0; const parsedLocalNumber = Number(localValue); const sliderValueRaw = Number.isFinite(parsedLocalNumber) ? parsedLocalNumber : Number(fallbackNumber); const sliderValue = Math.min( max ?? sliderValueRaw, Math.max(min ?? sliderValueRaw, sliderValueRaw) ); return ( {hasRange && ( setLocalValue(newValue)} onChangeCommitted={(e, newValue) => handleChange(newValue)} min={min} max={max} step={step} size="small" valueLabelDisplay="auto" sx={{ color: 'primary.main' }} /> )} { const v = e.target.value; // 允许输入空值或负号 if (v === '' || v === '-') { setLocalValue(v); return; } const num = schema.type === 'integer' || schema.type === 'int' ? parseInt(v) : parseFloat(v); handleChange(isNaN(num) ? 0 : num); }} inputProps={{ min: min, max: max, step: step }} variant="outlined" sx={{ '& .MuiOutlinedInput-root': { borderRadius: 1.5, bgcolor: 'background.paper', '&.Mui-focused > fieldset': { borderColor: 'primary.main' } } }} /> ); } // 列表字段使用“每行一项”的多行文本输入,适合编辑 session_list / split_words 等数组数据。 if (schema.type === 'list' || schema.type === 'array') { return ( {schema.description || fieldKey} {schema.hint && ( {schema.hint} )} handleChange(e.target.value.split('\n'))} placeholder="每行一项" variant="outlined" sx={{ '& .MuiOutlinedInput-root': { borderRadius: 1.5, bgcolor: 'background.paper', '&.Mui-focused > fieldset': { borderColor: 'primary.main' } } }} /> ); } // 若 Schema 给出了 options,则优先渲染为下拉选择框,而不是自由文本输入。 if (schema.options && Array.isArray(schema.options)) { return ( handleChange(e.target.value)} variant="outlined" SelectProps={{ MenuProps: { PaperProps: { sx: { borderRadius: 1.5, mt: 0.5, boxShadow: '0 4px 16px rgba(0,0,0,0.1)' } } } }} sx={{ '& .MuiOutlinedInput-root': { borderRadius: 1.5, bgcolor: 'background.paper', '&.Mui-focused > fieldset': { borderColor: 'primary.main' } }, '& .MuiSelect-select': { textAlign: 'center', pr: '32px !important' // 给箭头图标留出空间,确保视觉居中 } }} > {schema.options.map((option) => ( {option} ))} ); } // 其余情况按字符串处理,并依据字段语义决定是否切换为多行文本框。 const shouldBeMultiline = schema.type === 'text' || (schema.hint && schema.hint.length > 100) || fieldKey.includes('prompt') || fieldKey.includes('format') || fieldKey.includes('template') || fieldKey.includes('pattern') || fieldKey.includes('message') || fieldKey.includes('body') || fieldKey.includes('content'); const multilineRows = fieldKey.includes('prompt') || schema.type === 'text' ? 8 : 4; // 如果是长文本,保持上下布局;否则使用 8:2 布局 if (shouldBeMultiline) { return ( {schema.description || fieldKey} {schema.hint && ( {schema.hint} )} handleChange(e.target.value)} placeholder={fieldKey.includes('prompt') ? '请输入该会话/全局场景下的 Prompt 内容…' : ''} variant="outlined" sx={{ '& .MuiOutlinedInput-root': { alignItems: 'flex-start', borderRadius: 2, bgcolor: 'background.paper', fontFamily: 'inherit', lineHeight: 1.65, '& textarea': { lineHeight: 1.7, fontSize: '0.95rem', resize: 'vertical' }, '&.Mui-focused > fieldset': { borderColor: 'primary.main' } } }} /> ); } return ( handleChange(e.target.value)} variant="outlined" sx={{ '& .MuiOutlinedInput-root': { borderRadius: 1.5, bgcolor: 'background.paper', '&.Mui-focused > fieldset': { borderColor: 'primary.main' } } }} /> ); } // 根据会话 ID 的消息类型片段判断是私聊还是群聊,用于筛选对应的配置 Schema。 function detectSessionType(sessionId) { const raw = String(sessionId || ''); if (raw.includes(':GroupMessage:') || raw.includes(':GuildMessage:')) return 'group'; if (raw.includes(':FriendMessage:') || raw.includes(':PrivateMessage:')) return 'friend'; return 'friend'; } // 会话差异配置页并不暴露完整全局 Schema,而是按会话类型抽取可编辑字段子集。 // 因此像 web_admin、notification_settings 这类纯全局配置块不会出现在会话差异编辑视图中。 function getSessionSchemaEntries(schema, sessionType) { const rootKey = sessionType === 'group' ? 'group_settings' : 'friend_settings'; const rootItems = schema?.[rootKey]?.items || {}; const orderedKeys = [ 'auto_trigger_settings', 'group_idle_trigger_minutes', 'proactive_prompt', 'schedule_settings', 'tts_settings', 'segmented_reply_settings', ]; const hiddenKeys = new Set(['enable', 'session_list']); const entries = []; const sessionNameSchema = rootItems.session_name || { type: 'string', default: '', description: '会话备注名', hint: '用于日志和管理端展示。为空时将回退显示 UMO。', }; entries.push(['session_name', sessionNameSchema]); orderedKeys .filter((key) => !hiddenKeys.has(key) && rootItems[key]) .forEach((key) => { entries.push([key, rootItems[key]]); }); return entries; } /** * 主配置渲染器组件 * 负责加载、显示和保存插件的完整配置 * 包含了配置的获取、状态管理和保存逻辑 */ function ConfigRenderer() { const { state } = useAppContext(); // 获取全局状态 // schema: 后端返回的配置结构定义;config: 当前正在编辑的配置草稿。 const [schema, setSchema] = useState(null); const [config, setConfig] = useState(null); const [expandedKeys, setExpandedKeys] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [saveFeedback, setSaveFeedback] = useState({ type: '', text: '' }); // 多会话模式相关状态:global 表示编辑全局配置,session 表示编辑单会话差异配置。 const [mode, setMode] = useState('global'); // global | session const [sessions, setSessions] = useState([]); const [selectedSession, setSelectedSession] = useState(''); const [sessionLoading, setSessionLoading] = useState(false); const [sessionConfigState, setSessionConfigState] = useState({ baseAvailable: true, message: '' }); const api = useApi(); const scrollContainerRef = useRef(null); const loadConfigSeqRef = useRef(0); const isDirtyRef = useRef(false); // 标记用户是否有未保存的修改 // 动态缓存 key:不同模式 / 不同会话使用不同 localStorage 键,避免草稿串线。 const getDraftKey = (currentMode = mode, currentSession = selectedSession) => { if (currentMode === 'session' && currentSession) { return `astrbot_plugin_proactive_draft_config_session_${currentSession}`; } return 'astrbot_plugin_proactive_draft_config_global'; }; const getExpandedKey = (currentMode = mode, currentSession = selectedSession) => { if (currentMode === 'session' && currentSession) { return `astrbot_plugin_proactive_expanded_keys_session_${currentSession}`; } return 'astrbot_plugin_proactive_expanded_keys_global'; }; const getScrollKey = (currentMode = mode, currentSession = selectedSession) => { if (currentMode === 'session' && currentSession) { return `astrbot_scroll_config_list_session_${currentSession}`; } return 'astrbot_scroll_config_list_global'; }; useEffect(() => { // 首次进入页面时只初始化一次:加载 Schema,并准备会话列表。 initializePage(); }, []); useEffect(() => { if (!schema) return; // 切换模式或切换会话时,需要重新从服务端拉取当前上下文下的配置快照。 setSaveFeedback({ type: '', text: '' }); loadConfig(mode, selectedSession); }, [mode, selectedSession]); // 使用 useLayoutEffect 恢复滚动位置,尽量让用户在绘制完成前回到上次浏览位置。 useLayoutEffect(() => { if (!loading && scrollContainerRef.current) { const savedPos = localStorage.getItem(getScrollKey()); if (savedPos) { // 尝试恢复 const pos = parseInt(savedPos, 10); if (pos > 0) { scrollContainerRef.current.scrollTop = pos; // 双重保障 requestAnimationFrame(() => { if (scrollContainerRef.current) { scrollContainerRef.current.scrollTop = pos; } }); } } } }, [loading, mode, selectedSession]); // 监听滚动并节流写入 localStorage,便于大配置表单下次打开时恢复阅读位置。 useEffect(() => { const el = scrollContainerRef.current; if (!el || loading) return; const handleScroll = () => { localStorage.setItem(getScrollKey(), el.scrollTop); }; let timeout; const debouncedScroll = () => { clearTimeout(timeout); timeout = setTimeout(handleScroll, 100); }; el.addEventListener('scroll', debouncedScroll); return () => { el.removeEventListener('scroll', debouncedScroll); clearTimeout(timeout); }; }, [loading, mode, selectedSession]); // 自动保存草稿,但只有发生本地修改时才写入,避免把服务端新值反向覆盖成本地草稿。 useEffect(() => { if (config && isDirtyRef.current) { localStorage.setItem(getDraftKey(), JSON.stringify(config)); } }, [config, mode, selectedSession]); // 记忆折叠 / 展开状态,让用户在大型 Schema 中保持稳定的浏览结构。 useEffect(() => { if (schema) { // 确保 schema 加载后再保存,避免初始化时的空状态覆盖 localStorage.setItem(getExpandedKey(), JSON.stringify(expandedKeys)); } }, [expandedKeys, schema, mode, selectedSession]); const initializePage = async () => { setLoading(true); try { // 初始化时优先拿 Schema;没有 Schema 就无法安全地动态构建表单。 const schemaData = await api.getConfigSchema(); setSchema(schemaData); await loadSessions(); } catch (e) { console.error('初始化配置页失败', e); // showToast('初始化配置页失败,请检查控制台', 'error'); } finally { setLoading(false); } }; const loadSessions = async () => { try { // 会话列表用于会话差异配置模式下的下拉框、状态提示与覆写标记展示。 const result = await api.listSessions(); const sessionList = result?.sessions || []; // 确保 sessionList 中的每一项都是包含 session, has_override 的对象形式 const normalizedSessions = sessionList.map(item => typeof item === 'string' ? { session: item, has_override: false } : item ); setSessions(normalizedSessions); // 默认选中第一个会话 if (!selectedSession && normalizedSessions.length > 0) { setSelectedSession(normalizedSessions[0].session); } } catch (e) { console.error('加载会话列表失败', e); // showToast('加载会话列表失败,请检查控制台', 'error'); } }; const loadConfig = async (currentMode = mode, currentSession = selectedSession) => { // 所有配置加载都以当前 Schema 为前提,避免出现“无结构定义的盲编辑”。 if (!schema) return; // 递增请求序号,防止快速切换会话时后返回的旧请求覆盖新状态。 const requestSeq = ++loadConfigSeqRef.current; setLoading(true); try { let configData = null; if (currentMode === 'session') { if (!currentSession) { if (requestSeq === loadConfigSeqRef.current) { setConfig(null); setSessionConfigState({ baseAvailable: false, message: '请先选择一个会话' }); } return; } setSessionLoading(true); const sessionData = await api.getSessionConfig(currentSession); const hasBaseConfig = Boolean(sessionData?.base); setSessionConfigState( hasBaseConfig ? { baseAvailable: true, message: '' } : { baseAvailable: false, message: '该会话尚未命中对应类型的全局 session_list,因此暂时无法保存会话差异配置。请先在对应的全局配置中加入该会话。' } ); configData = sessionData?.effective || sessionData?.override || {}; } else { setSessionConfigState({ baseAvailable: true, message: '' }); configData = await api.getConfig(); } // 如果不是最新请求,丢弃过期响应,避免会话串改 if (requestSeq !== loadConfigSeqRef.current) { return; } // 1. 配置加载以服务端返回值为准 // 旧版本这里会无条件恢复本地草稿,导致真实已保存配置被默认草稿覆盖 // 因此改为始终优先使用服务端配置;草稿仅在当前编辑会话中用于暂存,不参与初始化覆盖 const finalConfig = configData; isDirtyRef.current = false; // 2. 处理展开状态记忆 const cachedExpandedStr = localStorage.getItem(getExpandedKey(currentMode, currentSession)); let finalExpandedKeys = []; if (cachedExpandedStr) { try { finalExpandedKeys = JSON.parse(cachedExpandedStr); } catch (e) { console.error('解析展开状态失败', e); } } else { // 默认全展开 finalExpandedKeys = getAllExpandablePaths(schema); } setConfig(finalConfig); setExpandedKeys(finalExpandedKeys); } catch (e) { if (requestSeq === loadConfigSeqRef.current) { console.error('加载配置失败', e); // showToast('加载配置失败,请检查控制台', 'error'); setConfig(null); } } finally { if (requestSeq === loadConfigSeqRef.current) { setSessionLoading(false); setLoading(false); } } }; const handleToggleExpand = (path) => { // 单个折叠面板的展开态切换逻辑集中处理,便于后续扩展批量控制。 setExpandedKeys(prev => { if (prev.includes(path)) { return prev.filter(p => p !== path); } else { return [...prev, path]; } }); }; const handleToggleAll = () => { // 会话模式只对会话可编辑字段做“全部展开”,不污染全局 Schema 的路径集合。 const expandableSchema = mode === 'session' ? Object.fromEntries(sessionSchemaEntries) : schema; if (expandedKeys.length > 0) { setExpandedKeys([]); // 全部收起 } else { setExpandedKeys(getAllExpandablePaths(expandableSchema)); // 全部展开 } }; const cleanConfig = (obj) => { // 保存前递归清理配置:数组中的字符串去首尾空白,并去掉空项,减小脏数据概率。 if (Array.isArray(obj)) { return obj .map(item => typeof item === 'string' ? item.trim() : item) .filter(item => item !== ''); } if (obj && typeof obj === 'object') { const newObj = {}; for (const key in obj) { newObj[key] = cleanConfig(obj[key]); } return newObj; } return obj; }; const handleSave = async () => { // 保存逻辑按 mode 分叉:全局配置直接写配置块,会话模式则保存差异配置。 setSaving(true); try { const cleanedConfig = cleanConfig(config); const currentMode = mode; const currentSession = selectedSession; if (currentMode === 'session') { if (!currentSession) { // showToast('请先选择会话', 'warning'); return; } const response = await api.updateSessionConfig(currentSession, { mode: 'effective', effective: cleanedConfig }); isDirtyRef.current = false; localStorage.removeItem(getDraftKey(currentMode, currentSession)); setConfig(response?.effective || cleanedConfig); setSaveFeedback({ type: 'success', text: '会话差异配置已保存' }); await loadSessions(); // 会话模式保存后强制回读服务端 effective,确保会话隔离与覆写状态显示正确 await loadConfig(currentMode, currentSession); } else { const payload = { friend_settings: cleanedConfig.friend_settings, group_settings: cleanedConfig.group_settings, web_admin: cleanedConfig.web_admin, }; await api.updateConfig(payload); isDirtyRef.current = false; setConfig(cleanedConfig); // 全局模式可直接更新界面 localStorage.removeItem(getDraftKey(currentMode, currentSession)); // 已保存,清除草稿 setSaveFeedback({ type: 'success', text: '全局配置已保存' }); } } catch (e) { console.error('保存配置失败', e); setSaveFeedback({ type: 'error', text: e?.message || '保存配置失败,请检查后端返回信息' }); window.alert(e?.message || '保存配置失败,请检查后端返回信息'); } finally { setSaving(false); } }; const handleResetOverride = async () => { // 仅会话模式下可用:清空后该会话重新完全继承全局配置。 if (!selectedSession) { // showToast('请先选择会话', 'warning'); return; } const ok = confirm('确定要清空该会话的差异配置吗?\n\n清空后将完全继承全局默认配置。'); if (!ok) return; setSaving(true); try { await api.resetSessionConfig(selectedSession); localStorage.removeItem(getDraftKey(mode, selectedSession)); setSaveFeedback({ type: 'success', text: '会话差异配置已清空' }); await loadSessions(); await loadConfig(mode, selectedSession); } catch (e) { console.error('清空会话差异配置失败', e); setSaveFeedback({ type: 'error', text: e?.message || '清空会话差异配置失败' }); window.alert(e?.message || '清空会话差异配置失败'); } finally { setSaving(false); } }; if (loading) { return ( ⚙️ 加载配置中... ); } if (!schema || !config) { return ( 无法加载配置 ); } // 以下派生变量统一在渲染前计算,避免 JSX 中出现过多内联判断,降低维护复杂度。 const selectedSessionMeta = sessions.find(s => (s.session || s) === selectedSession); const hasOverride = selectedSessionMeta?.has_override; const currentSessionType = detectSessionType(selectedSession); const sessionSchemaEntries = mode === 'session' ? getSessionSchemaEntries(schema, currentSessionType) : []; const schemaEntries = mode === 'session' ? sessionSchemaEntries : Object.entries(schema); const sessionEnabled = mode === 'session' ? Boolean(config?.enable) : false; const sessionTypeLabel = currentSessionType === 'group' ? '群聊' : '私聊'; const canSaveSessionConfig = mode !== 'session' || sessionConfigState.baseAvailable; return ( {/* 顶部控制区:负责在“全局配置”和“会话差异配置”两种编辑模式间切换。 */} { if (!val) return; setMode(val); }} > 全局配置 会话差异配置 {mode === 'session' && ( <> setSelectedSession(e.target.value)} sx={{ minWidth: 420, maxWidth: '100%' }} > {sessions.map((item) => { const displayName = item.session_display_name || item.session_name || item.session; const hasAlias = Boolean(item.session_name && item.session_name.trim()); const optionText = hasAlias ? `${displayName}(${item.session})` : displayName; return ( {optionText} ); })} {hasOverride && ( )} {sessionLoading && ( 会话配置加载中... )} )} {mode === 'session' && selectedSessionMeta && ( 当前会话:{selectedSessionMeta.session_display_name || selectedSessionMeta.session_name || selectedSessionMeta.session || selectedSessionMeta} {selectedSessionMeta.session_name ? ` | UMO:${selectedSessionMeta.session}` : ''} {' | '}未回复次数:{selectedSessionMeta.unanswered_count ?? 0} )} {mode === 'session' && !sessionConfigState.baseAvailable && sessionConfigState.message && ( {sessionConfigState.message} )} {saveFeedback.text && ( {saveFeedback.text} )} {mode === 'session' && config && ( {sessionTypeLabel}会话启用状态 这是当前会话的独立开关。关闭后,该会话会暂停主动消息,但不会影响同类型的全局配置与其他会话。 { isDirtyRef.current = true; setConfig({ ...config, enable: e.target.checked }); }} /> )} {/* 主编辑区:根据 schemaEntries 动态递归渲染所有当前可见配置项。 */} {schemaEntries.map(([key, subSchema]) => ( { isDirtyRef.current = true; setConfig({ ...config, [key]: newValue }); }} path="" expandedKeys={expandedKeys} onToggleExpand={handleToggleExpand} /> ))} {/* 底部操作栏:集中放置折叠控制、恢复默认、撤销、清空覆写与保存操作。 */} {schemaEntries.length} 个配置组 {mode === 'session' && ( )} ); } window.ConfigRenderer = ConfigRenderer; })();