/** * 神之一手设置 section * * 结构(R-011 迭代 09): * - 三个子 tab:Tab 设置 / 策略分组 / QMT 连接配置 * - Tab 设置:统一管理所有会话 tab(内置 + 策略分组混排),拖动排序(落点立即持久化)+ 显示/隐藏开关; * 任何 tab 均不支持重命名/删除(产品约束-009、UI约束-003) * - 策略分组:策略 CRUD(新增/重命名/删除确认)+ 提示「顺序与显示请在 Tab 设置中调整」 * - QMT 连接配置:R-004 卡片形式(迭代 03) */ import { useState, useEffect, useCallback, Fragment } from 'react'; import { useRpc } from './connection.jsx'; import { ToastProvider, useToast } from './Toast.jsx'; import { StrategyFieldsEditor } from './StrategyFieldsEditor.jsx'; // R-013:策略自定义字段编辑器 import { ExpandChevron } from './ExpandChevron.jsx'; // 展开箭头与策略 tab 表格行图标统一 /** 确认弹窗 */ function ConfirmDialog({ title, message, onConfirm, onCancel }) { return (

{title}

{message}
); } /** switch 组件 */ function Switch({ checked, onChange, disabled }) { return ( ); } /** Tab 设置子 tab(R-011:统一管理所有会话 tab —— 内置 + 策略分组混排,拖动排序 + 显隐开关) */ function TabSettings() { const [tabs, setTabs] = useState(null); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [dragId, setDragId] = useState(null); const [overId, setOverId] = useState(null); const call = useRpc(); const toast = useToast(); const load = useCallback(async () => { try { const res = await call('one-divine-lot/tabs', {}); if (res && res.ok && Array.isArray(res.value)) setTabs(res.value); else setError((res && res.error && res.error.message) || '加载失败'); } catch (e) { setError(e.message); } }, [call]); useEffect(() => { load(); }, [load]); // 整表持久化(拖动落点 / 显隐切换共用;Q1 落点立即持久化) const persist = async (nextTabs, msg) => { setSaving(true); try { const res = await call('one-divine-lot/tabs/update', { args: { tabs: nextTabs } }); if (res && res.ok) { setTabs(res.value); toast.success(msg + '(刷新页面后生效)'); } else { toast.error((res && res.error && res.error.message) || '保存失败'); await load(); } } catch (e) { toast.error(e.message); await load(); } finally { setSaving(false); } }; const toggleVisible = async (t) => { if (!tabs) return; const next = tabs.map((x) => (x.id === t.id ? { ...x, visible: !x.visible } : x)); await persist(next, '「' + t.name + '」已' + (next.find((x) => x.id === t.id).visible ? '显示' : '隐藏')); }; // 原生 HTML5 拖动:落点重排 + 立即持久化(Q1) const handleDrop = async (targetId) => { if (!tabs || !dragId || dragId === targetId) { setDragId(null); setOverId(null); return; } const from = tabs.findIndex((x) => x.id === dragId); const to = tabs.findIndex((x) => x.id === targetId); if (from < 0 || to < 0) { setDragId(null); setOverId(null); return; } const next = tabs.slice(); const [moved] = next.splice(from, 1); next.splice(to, 0, moved); // 重写 order(整表连续) const ordered = next.map((x, i) => ({ ...x, order: i })); setDragId(null); setOverId(null); await persist(ordered, '「' + moved.name + '」已移动'); }; const badgeStyle = (kind) => ({ fontSize: 11, borderRadius: 8, padding: '0 6px', marginRight: 6, flex: 'none', color: kind === 'builtin' ? 'var(--dsw-alias-state-business-primary, #1565c0)' : 'var(--dsw-alias-state-warn-primary, #6a1b9a)', border: kind === 'builtin' ? '1px solid var(--dsw-alias-border-l3, #90caf9)' : '1px solid var(--dsw-alias-border-l3, #ce93d8)', }); return (
拖动调整所有标签页顺序,开关控制显示/隐藏;内置与策略标签可任意混排。名称不支持在此修改(策略名称在「策略分组」中修改)。
{error &&
{error}
} {!tabs && !error &&
加载中...
} {tabs && ( {tabs.map((t) => ( { setDragId(t.id); e.dataTransfer.effectAllowed = 'move'; }} onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setOverId(t.id); }} onDragLeave={() => { if (overId === t.id) setOverId(null); }} onDrop={(e) => { e.preventDefault(); handleDrop(t.id); }} style={{ borderBottom: '1px solid var(--dsw-alias-border-l1, #eee)', cursor: saving ? 'default' : 'grab', background: overId === t.id ? 'color-mix(in srgb, var(--dsw-alias-state-business-primary, #1565c0) 12%, transparent)' : 'transparent', }} > ))}
标签页 显示
{t.kind === 'builtin' ? '内置' : '策略'} {t.name} toggleVisible(t)} disabled={saving} />
)}
); } /** 策略分组子 tab(原策略 CRUD) */ function StrategyGroupSettings() { const [strategies, setStrategies] = useState(null); const [error, setError] = useState(null); const [saving, setSaving] = useState(false); const [newName, setNewName] = useState(''); const [editingId, setEditingId] = useState(null); const [editingName, setEditingName] = useState(''); const [confirmDelete, setConfirmDelete] = useState(null); const [changedMsg, setChangedMsg] = useState(null); // R-013:当前展开配置字段的策略 id(null=无展开) const [expandedId, setExpandedId] = useState(null); const [fieldSaving, setFieldSaving] = useState(false); const call = useRpc(); const toast = useToast(); const load = useCallback(async () => { try { const res = await call('one-divine-lot/strategies', {}); if (res && res.ok) setStrategies(res.value); else setError((res && res.error && res.error.message) || '加载失败'); } catch (e) { setError(e.message); } }, [call]); useEffect(() => { load(); }, [load]); const handleChanged = useCallback((msg) => { setChangedMsg(msg); toast.success(msg + '(刷新页面后生效)'); load(); }, [load, toast]); const handleAdd = async () => { const name = newName.trim(); if (!name) { toast.error('请输入策略名称'); return; } setSaving(true); try { const res = await call('one-divine-lot/strategies/add', { args: { name } }); if (res && res.ok) { setNewName(''); handleChanged('策略「' + name + '」已添加'); } else { toast.error((res && res.error && res.error.message) || '添加失败'); } } catch (e) { toast.error(e.message); } finally { setSaving(false); } }; const handleRename = async (id) => { const name = editingName.trim(); if (!name) { toast.error('名称不能为空'); return; } setSaving(true); try { const res = await call('one-divine-lot/strategies/update', { args: { strategies: strategies.map((s) => (s.id === id ? { ...s, name } : s)) }, }); if (res && res.ok) { setEditingId(null); handleChanged('策略已重命名'); } else { toast.error((res && res.error && res.error.message) || '重命名失败'); } } catch (e) { toast.error(e.message); } finally { setSaving(false); } }; /** R-013:保存某策略的自定义字段定义(整表 strategies/update) */ const handleSaveConfig = async (id, configSchema) => { setFieldSaving(true); try { const res = await call('one-divine-lot/strategies/update', { args: { strategies: strategies.map((s) => (s.id === id ? { ...s, configSchema } : s)) }, }); if (res && res.ok) { handleChanged('策略自定义字段已保存'); } else { toast.error((res && res.error && res.error.message) || '保存失败'); } } catch (e) { toast.error(e.message); } finally { setFieldSaving(false); } }; const handleDelete = async (id) => { setSaving(true); try { const res = await call('one-divine-lot/strategies/remove', { args: { strategyId: id } }); if (res && res.ok) { setConfirmDelete(null); handleChanged('策略已删除,份额已回到未分配'); } else { toast.error((res && res.error && res.error.message) || '删除失败'); setConfirmDelete(null); } } catch (e) { toast.error(e.message); setConfirmDelete(null); } finally { setSaving(false); } }; return (
{changedMsg && (
{changedMsg} —— 刷新页面后生效
)} {error &&
{error}
}
setNewName(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleAdd()} style={{ flex: 1, padding: '6px 10px', border: '1px solid var(--dsw-alias-border-l2, #ccc)', borderRadius: 4 }} />
顺序与显示请在「Tab 设置」中调整。
{!strategies && !error &&
加载中...
} {strategies && ( {strategies.map((s) => ( {expandedId === s.id && ( )} ))}
策略 ID 操作
{editingId === s.id ? ( setEditingName(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleRename(s.id)} style={{ padding: '4px 6px', border: '1px solid #ccc', borderRadius: 3, marginRight: 6 }} /> ) : ( {s.name} )} {s.id}
handleSaveConfig(s.id, fields)} />
)} {confirmDelete && ( handleDelete(confirmDelete.id)} onCancel={() => setConfirmDelete(null)} /> )}
); } /** 设置 section 主组件:三个子 tab */ function SettingsSectionInner(props) { const [activeTab, setActiveTab] = useState('tabs'); // tabs | strategies | qmt return (

神之一手

{/* 子 tab 导航 */}
{[ { key: 'tabs', label: 'Tab 设置' }, { key: 'strategies', label: '策略分组' }, { key: 'qmt', label: 'QMT 连接配置' }, ].map((tab) => ( ))}
{activeTab === 'tabs' && } {activeTab === 'strategies' && } {activeTab === 'qmt' && }
); } /** 导出组件(包 ToastProvider) */ export function SettingsSection(props) { return ( ); } /** 长文本省略单元格:单行省略 + 悬停显示完整内容(避免错误信息等长文本撑行换行) */ function EllipsisText({ text, maxWidth = 220, title, style }) { return ( {text} ); } /** QMT 连接配置卡片(R-004;产品约束-005、UI约束-002;2026-08-29 老师反馈改卡片形式) */ function QmtConnectionCard({ conn, isActive, isDefault, busy, testResult, onActivate, onSetDefault, onTest, onEdit, onDelete }) { const btn = { padding: '3px 10px', cursor: 'pointer', border: '1px solid var(--dsw-alias-border-l2, #ccc)', borderRadius: 4, background: 'var(--dsw-alias-bg-layer-1, #fff)', fontSize: 12 }; return (
{/* 标题行:名称 + 徽标 */}
{isActive && 生效中} {isDefault && 默认}
{/* 地址行:省略+悬停 */}
{/* 测试结果行:可换行但受卡片约束;超长悬停看全文 */} {testResult && (
)} {/* 操作行:按钮自动换行排列,不挤压卡片 */}
{!isActive && } {!isDefault && }
); } /** QMT 连接配置子 tab —— 卡片版(R-004;产品约束-005、UI约束-002) */ function QmtConnectionsSettings() { const [state, setState] = useState(null); // { list, activeId, defaultId, active } const [error, setError] = useState(null); const [saving, setSaving] = useState(false); const [testingId, setTestingId] = useState(null); const [testResult, setTestResult] = useState(null); // { key, ok, text, fullText } const [confirmDelete, setConfirmDelete] = useState(null); const [formOpen, setFormOpen] = useState(false); const [editingId, setEditingId] = useState(null); const [formName, setFormName] = useState(''); const [formUrl, setFormUrl] = useState(''); const [mcpStatus, setMcpStatus] = useState(null); // R-020/迭代18:MCP 状态 const [mcpChecking, setMcpChecking] = useState(false); const call = useRpc(); const toast = useToast(); const load = useCallback(async () => { try { const res = await call('one-divine-lot/qmt-connections', {}); if (res && res.ok) setState(res.value); else setError((res && res.error && res.error.message) || '加载失败'); } catch (e) { setError(e.message); } }, [call]); const loadMcp = useCallback(async (refresh) => { try { const res = await call('one-divine-lot/qmt-connections/mcp-status', { args: { refresh: !!refresh } }); if (res && res.ok) setMcpStatus(res.value); } catch { /* 静默 */ } }, [call]); useEffect(() => { load(); loadMcp(false); }, [load, loadMcp]); const openAdd = () => { setEditingId(null); setFormName(''); setFormUrl(''); setFormOpen(true); setTestResult(null); }; const openEdit = (c) => { setEditingId(c.id); setFormName(c.name); setFormUrl(c.baseUrl); setFormOpen(true); setTestResult(null); }; const closeForm = () => { setFormOpen(false); setEditingId(null); }; const validateUrl = (raw) => { const url = String(raw || '').trim().replace(/\/+$/, ''); if (!/^https?:\/\//i.test(url) || url.length <= 8) return null; return url; }; const handleSave = async () => { const name = formName.trim(); const url = validateUrl(formUrl); if (!name) { toast.error('请输入连接名称'); return; } if (!url) { toast.error('HTTP 地址格式无效(需以 http:// 或 https:// 开头)'); return; } setSaving(true); try { const res = editingId === null ? await call('one-divine-lot/qmt-connections/add', { args: { name, baseUrl: url } }) : await call('one-divine-lot/qmt-connections/update', { args: { id: editingId, name, baseUrl: url } }); if (res && res.ok) { closeForm(); toast.success(editingId === null ? '连接配置已添加' : '连接配置已保存'); await load(); } else { toast.error((res && res.error && res.error.message) || '保存失败'); } } catch (e) { toast.error(e.message); } finally { setSaving(false); } }; const handleActivate = async (c) => { if (state && c.id === state.activeId) return; setSaving(true); try { const res = await call('one-divine-lot/qmt-connections/activate', { args: { id: c.id } }); if (res && res.ok) { setState(res.value); loadMcp(false); // R-020/迭代18:激活切换后刷新 MCP 状态 toast.success('已激活:' + c.name + '(立即生效)'); } else { toast.error((res && res.error && res.error.message) || '激活失败'); } } catch (e) { toast.error(e.message); } finally { setSaving(false); } }; const handleSetDefault = async (c) => { if (state && c.id === state.defaultId) return; setSaving(true); try { const res = await call('one-divine-lot/qmt-connections/set-default', { args: { id: c.id } }); if (res && res.ok) { setState((prev) => prev ? { ...prev, defaultId: c.id } : prev); toast.success('已设为默认:' + c.name + '(启动时自动激活)'); } else { toast.error((res && res.error && res.error.message) || '设置默认失败'); } } catch (e) { toast.error(e.message); } finally { setSaving(false); } }; const showTest = (key, v) => { const text = v.reachable ? '可达 · ' + v.latencyMs + 'ms' + (v.status ? ' · status=' + v.status : '') : '不可达 · ' + (v.error || ('HTTP ' + v.httpStatus)) + ' · ' + v.latencyMs + 'ms'; setTestResult({ key, ok: v.reachable === true, text, fullText: text }); }; const handleTest = async (c) => { setTestingId(c.id); try { const res = await call('one-divine-lot/qmt-connections/test', { args: { id: c.id } }); if (res && res.ok) showTest(c.id, res.value); else { const msg = (res && res.error && res.error.message) || '测试失败'; setTestResult({ key: c.id, ok: false, text: '不可达 · ' + msg, fullText: msg }); } } catch (e) { setTestResult({ key: c.id, ok: false, text: '不可达 · ' + e.message, fullText: e.message }); } finally { setTestingId(null); } }; const handleTestForm = async () => { const url = validateUrl(formUrl); if (!url) { toast.error('HTTP 地址格式无效(需以 http:// 或 https:// 开头)'); return; } setTestingId('__form__'); try { const res = await call('one-divine-lot/qmt-connections/test', { args: { baseUrl: url } }); if (res && res.ok) showTest('__form__', res.value); else { const msg = (res && res.error && res.error.message) || '测试失败'; setTestResult({ key: '__form__', ok: false, text: '不可达 · ' + msg, fullText: msg }); } } catch (e) { setTestResult({ key: '__form__', ok: false, text: '不可达 · ' + e.message, fullText: e.message }); } finally { setTestingId(null); } }; const handleDelete = async (id) => { setSaving(true); try { const res = await call('one-divine-lot/qmt-connections/remove', { args: { id } }); setConfirmDelete(null); if (res && res.ok) { setState(res.value); const msgs = []; if (res.value.removedWasActive) msgs.push('激活已自动切换'); if (res.value.removedWasDefault) msgs.push('默认标记已转移'); toast.success('连接配置已删除' + (msgs.length ? '(' + msgs.join(';') + ')' : '')); } else { toast.error((res && res.error && res.error.message) || '删除失败'); } } catch (e) { setConfirmDelete(null); toast.error(e.message); } finally { setSaving(false); } }; return (
激活配置为当前生效连接(切换立即生效);默认配置在插件启动时自动激活。
{/* R-020/迭代18:MCP 连接状态条(随激活连接;绿=已连接+N工具 / 黄=连接中 / 红=断开 / 灰=未启用) */}
MCP 连接
{error &&
} {!state && !error &&
加载中...
} {state && state.list.length === 0 && (
暂无连接配置。点击「新增连接」录入第一个 QMT Bridge 地址(如 http://192.168.3.43:8610)。
)} {state && state.list.length > 0 && (
{state.list.map((c) => ( handleActivate(c)} onSetDefault={() => handleSetDefault(c)} onTest={() => handleTest(c)} onEdit={() => openEdit(c)} onDelete={() => setConfirmDelete({ id: c.id, name: c.name })} /> ))}
)} {formOpen && (
{editingId === null ? '新增连接配置' : '编辑连接配置'}
{testResult && testResult.key === '__form__' && ( )}
)} {confirmDelete && ( handleDelete(confirmDelete.id)} onCancel={() => setConfirmDelete(null)} /> )}
); }