// @ts-nocheck import * as mcp from '../utils/mcp-client.js'; import * as http from './http-client.js'; import { loadModelSource, saveModelSource, loadOllamaConfig, loadAnthropicConfig, loadOmlxConfig, getCurrentModelInfo, CONFIG, loadCrossModuleEnabled, saveCrossModuleEnabled, } from '../utils/config.js'; import { shortName as shortModelName } from '../utils/metrics-client.js'; const STORAGE_KEY = 'ccsparkle_enabled_tools'; const CUSTOM_PRESETS_KEY = 'ccsparkle_custom_presets'; const PRESETS_INIT_KEY = 'ccsparkle_custom_presets_v4_initialized'; const DEFAULT_CUSTOM_PRESETS = [ { id: 'default_prod', name: '生产主链路', tools: [ 'metro_load_timetable_full', 'metro_confirm_debug_train_delete', 'metro_setup_rest_rules', 'metro_setup_handover_rules', 'metro_get_station_list', 'metro_get_app_state', 'metro_get_rest_rules', 'metro_get_handover_rules', 'metro_validate_all_tasks', 'metro_query_driver_segments', 'metro_health_check', ], createdAt: 0, isDefault: true, }, ]; let SUBSYSTEMS = [ { id: 'assignment', name: 'Assignment', descPrefix: '司机任务排班', healthUrl: 'http://127.0.0.1:7777/health' }, { id: 'profile', name: 'Profile', descPrefix: '员工能力画像', healthUrl: 'http://127.0.0.1:7778/health' }, { id: 'profile+', name: 'Profile+', descPrefix: '乘务派班系统', healthUrl: null }, { id: 'center', name: 'Center', descPrefix: '任务中心', healthUrl: null }, { id: 'git', name: 'Git', descPrefix: '代码托管平台', healthUrl: null }, { id: 'inspection', name: 'Inspection', descPrefix: '安全分析系统', healthUrl: null }, ]; async function refreshSubsystemsFromRegistry() { try { const r = await fetch(`${CONFIG.ZHONGTAI_BASE}/subsystems`); if (!r.ok) return; const data = await r.json(); if (!Array.isArray(data.subsystems) || data.subsystems.length === 0) return; const remoteById = new Map(data.subsystems.map(s => [s.id, s])); const localIds = new Set(SUBSYSTEMS.map(s => s.id)); const merged = SUBSYSTEMS.map(local => { const remote = remoteById.get(local.id); if (!remote) return local; return { ...local, ...remote }; }); for (const remote of data.subsystems) { if (!localIds.has(remote.id)) merged.push(remote); } SUBSYSTEMS = merged; } catch (e) { } } let _allTools = []; let _enabled = new Set(); let _expandedGroups = new Set(); let _collapsedSubsystems = new Set(); const COLLAPSE_STORAGE_KEY = 'ccsparkle_collapse_state'; function loadCollapseState() { try { const s = localStorage.getItem(COLLAPSE_STORAGE_KEY); if (s) { const d = JSON.parse(s); _collapsedSubsystems = new Set(d.subsystems || ['assignment', 'profile', 'profile+', 'center', 'git', 'inspection']); _expandedGroups = new Set(d.groups || []); } else { _collapsedSubsystems = new Set(['assignment', 'profile', 'profile+', 'center', 'git', 'inspection']); } } catch (e) { _collapsedSubsystems = new Set(['assignment', 'profile', 'profile+', 'center', 'git', 'inspection']); } } function saveCollapseState() { try { localStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify({ subsystems: [..._collapsedSubsystems], groups: [..._expandedGroups], })); } catch (e) {} } loadCollapseState(); let _subsystemStates = {}; let _customPresets = []; let _sidebarEl = null; let _subsystemsListEl = null; let _enabledCountEl = null; let _totalCountEl = null; let _sourceInfoEl = null; let _subsystemSummaryEl = null; let _onToolsChange = null; let _monitorTimer = null; let _registryTimer = null; function groupOf(name) { if (name.startsWith('profile_')) { if (name === 'profile_health_check' || name === 'profile_get_app_state') return '元工具'; if (name.includes('employee')) return '员工'; if (name.includes('exam')) return '考试'; if (name.includes('kilometer')) return '公里数'; if (name.includes('dashboard')) return '仪表盘'; return '其他'; } if (name === 'metro_list_tools' || name === 'metro_health_check' || name === 'metro_get_app_state') return '元工具'; if (name.includes('setup')) return '高阶工作流'; if (name.includes('timetable') || name.includes('debug_train')) return '加载时刻表'; if (name.includes('big_station')) return '大站点'; if (name.includes('rest')) return '轮乘规则'; if (name.includes('handover')) return '交接规则'; if (name.includes('segment') || name.includes('driver')) return '司机段'; if (name.includes('station')) return '站点'; if (name.includes('validate')) return '校验'; if (name.includes('export') || name.includes('backup') || name.includes('workspace') || name.includes('import')) return 'IO/备份'; if (name.includes('stat') || name.includes('radar') || name.includes('workload') || name.includes('depot')) return '统计'; if (name.includes('apply')) return '应用规则'; return '其他'; } const TOOL_NAME_LABELS = { metro_list_tools: '列工具', metro_health_check: '健康检查', metro_get_app_state: '应用状态', metro_load_timetable_full: '加载运营图', metro_setup_rest_rules: '设置轮乘规则', metro_setup_handover_rules: '设置交接规则', metro_search_tools: '搜索工具', metro_get_tool_detail: '工具详情', metro_list_timetable_files: '列时刻表文件', metro_load_timetable: '加载时刻表', metro_confirm_debug_train_delete: '确认删调试车', metro_import_timetable_file: '导入时刻表', metro_delete_timetable: '删除时刻表', metro_open_timetable_folder: '打开时刻表目录', metro_add_debug_train: '添加调试车', metro_remove_debug_train: '移除调试车', metro_add_big_station: '添加大站点', metro_edit_big_station: '编辑大站点', metro_delete_big_station: '删除大站点', metro_get_big_station_counts: '大站点统计', metro_get_big_stations: '大站点列表', metro_add_rest_rule: '添加轮乘规则', metro_edit_rest_rule: '编辑轮乘规则', metro_delete_rest_rule: '删除轮乘规则', metro_save_rest_config_as: '另存轮乘配置', metro_load_rest_config: '加载轮乘配置', metro_get_rest_configs: '轮乘配置列表', metro_get_rest_rules_by_station: '按站点查轮乘', metro_validate_rest_rule: '预检轮乘冲突', metro_add_handover_rule: '添加交接规则', metro_edit_handover_rule: '编辑交接规则', metro_delete_handover_rule: '删除交接规则', metro_get_handover_rule: '查询单条交接', metro_get_handover_rules: '交接规则列表', metro_apply_rest_rules: '应用轮乘规则', metro_apply_handover_rules: '应用交接规则', metro_apply_flexible_rules: '应用灵活规则', metro_apply_special_shifts: '应用特殊班次', metro_apply_circle_rules: '应用圈环规则', metro_auto_sort: '自动排序匹配表', metro_add_flexible_entry: '添加灵活规则', metro_remove_flexible_entry: '删除灵活规则', metro_add_overnight_train: '添加过夜车', metro_add_circle_history: '加入圈环历史', metro_delete_circle_history: '删除圈环历史', metro_merge_segments: '合并分段', metro_swap_driver_ids: '交换司机编号', metro_update_driver_shift: '修改司机班制', metro_update_segment_reason: '修改分段原因', metro_split_segment_by_time: '按时间拆分段', metro_split_segment_by_station: '按站点拆分段', metro_delete_segments: '批量删除段', metro_assign_driver_to_section: '给未分配段加司机', metro_get_station_list: '站点列表', metro_get_station_name_by_id: 'ID 查站名', metro_get_station_id_by_name: '站名查 ID', metro_get_station_order: '站点排序索引', metro_find_path_between_stations: '查站点路径', metro_validate_all_tasks: '校验所有任务', metro_query_complete_data: '完整分段数据', metro_query_unassigned: '未分配段', metro_query_duplicates: '重复分段', metro_query_station_workload: '站点 workload', metro_get_timetable_summary: '运营图速看', metro_calculate_segment_distances: '计算分段距离', metro_calculate_driver_work_time: '司机工时', metro_get_depot_counts: '出回库统计', metro_analyze_rest_points: '分析休息点', metro_export_workspace: '导出工作区', metro_export_csv: '导出 CSV', metro_list_backups: '列出备份', metro_save_backup: '保存备份', metro_load_backup: '加载备份', metro_delete_backup: '删除备份', profile_health_check: '画像健康检查', profile_get_app_state: '画像应用状态', profile_query_employees: '查员工列表', profile_get_employee: '查单个员工', profile_query_online_exams: '查在线考试', profile_query_practical_exams: '查实操考试', profile_get_practical_exam_stats: '实操考试统计', profile_query_kilometers: '查公里数', profile_get_kilometers_stats: '公里数统计', profile_get_kilometers_by_month: '按月查公里数', profile_get_dashboard: '画像仪表盘', }; const _toolLabels = new Map(); function refreshToolLabels(tools) { _toolLabels.clear(); for (const t of tools || []) { if (t?.name && typeof t.label === 'string' && t.label.trim()) { _toolLabels.set(t.name, t.label.trim()); } } } export function getToolNameLabel(name) { if (!name) return ''; return _toolLabels.get(name) || TOOL_NAME_LABELS[name] || name; } export function getSubsystemDisplayName(id) { const sys = SUBSYSTEMS.find((s) => s.id === id); return String(sys?.name || '').trim(); } export function resolveAppNamesFromToolNames(toolNames) { const names = []; const seen = new Set(); for (const tool of toolNames || []) { const id = mcp.getToolSubsystemId(tool) || subsysIdOfTool(tool); const label = getSubsystemDisplayName(id); if (!label || seen.has(label)) continue; seen.add(label); names.push(label); } return names; } function subsysIdOfTool(name) { if (name.startsWith('metro_')) return 'assignment'; if (name.startsWith('profile_')) return 'profile'; return 'assignment'; } function getToolLabel(t) { if (typeof t.label === 'string' && t.label.trim()) { return t.label.trim(); } if (_toolLabels.has(t.name)) return _toolLabels.get(t.name); if (TOOL_NAME_LABELS[t.name]) return TOOL_NAME_LABELS[t.name]; let d = (t.description || '').trim(); if (d.startsWith('【')) { const idx = d.indexOf('】'); if (idx > 0) d = d.slice(idx + 1).trim(); } const m = d.match(/^([^。,!!\n,;]+)/); if (m) d = m[1].trim(); if (d.length > 35) d = d.slice(0, 35) + '...'; return d || t.name; } export async function loadTools(mode, force = false) { if (_allTools.length > 0 && !force) return _allTools; if (mode === 'http') { _allTools = await http.listTools(); } else { _allTools = await mcp.listTools(); } refreshToolLabels(_allTools); const stored = loadStored(); _enabled = new Set(stored.filter(n => _allTools.some(t => t.name === n))); if (_enabled.size === 0) { const prod = DEFAULT_CUSTOM_PRESETS.find(p => p.id === 'default_prod'); const fallbackNames = (prod?.tools || []).filter(n => _allTools.some(t => t.name === n)); if (fallbackNames.length > 0) { fallbackNames.forEach(n => _enabled.add(n)); } else if (_allTools.length <= 30) { _allTools.forEach(t => _enabled.add(t.name)); } saveStored(); } renderSubsystems(); return _allTools; } export function clearTools() { _allTools = []; _toolLabels.clear(); _enabled = new Set(); renderSubsystems(); } export function getEnabledTools() { return _allTools.filter(t => _enabled.has(t.name)); } export function applyPreset(preset, subsystId = null) { const targetTools = subsystId ? _allTools.filter(t => subsysIdOfTool(t.name) === subsystId) : _allTools; if (preset === 'all') { targetTools.forEach(t => _enabled.add(t.name)); } else if (preset === 'none') { targetTools.forEach(t => _enabled.delete(t.name)); } saveStored(); renderSubsystems(); } function toggleTool(name, enabled) { if (enabled) _enabled.add(name); else _enabled.delete(name); saveStored(); renderSubsystems(); } function loadCustomPresets() { const initialized = localStorage.getItem(PRESETS_INIT_KEY); if (!initialized) { _customPresets = DEFAULT_CUSTOM_PRESETS.map(p => ({ ...p, tools: [...p.tools] })); saveCustomPresets(); localStorage.setItem(PRESETS_INIT_KEY, '1'); return; } try { const s = localStorage.getItem(CUSTOM_PRESETS_KEY); _customPresets = s ? JSON.parse(s) : []; } catch (e) { _customPresets = []; } } export function restoreDefaultPresets() { const existingIds = new Set(_customPresets.map(p => p.id)); let added = 0; for (const def of DEFAULT_CUSTOM_PRESETS) { if (!existingIds.has(def.id)) { _customPresets.push({ ...def, tools: [...def.tools] }); added++; } } saveCustomPresets(); renderCustomPresets(); return added; } function saveCustomPresets() { try { localStorage.setItem(CUSTOM_PRESETS_KEY, JSON.stringify(_customPresets)); } catch (e) {} } export function saveCurrentAsPreset() { const enabledTools = getEnabledTools(); if (enabledTools.length === 0) { alert('请先勾选工具,再保存为预设'); return; } let name = null; try { name = prompt(`保存当前 ${enabledTools.length} 个工具为预设,请输入名称:`); } catch (e) { alert('当前环境( Electron 打包版)不支持 prompt,请在「配」中保存预设'); return; } if (!name || !name.trim()) return; _customPresets.push({ id: `preset_${Date.now()}`, name: name.trim(), tools: enabledTools.map(t => t.name), createdAt: Date.now(), }); saveCustomPresets(); renderCustomPresets(); } export function deleteCustomPreset(id) { _customPresets = _customPresets.filter(p => p.id !== id); saveCustomPresets(); renderCustomPresets(); } export function applyCustomPreset(id) { const p = _customPresets.find(x => x.id === id); if (!p) return; _enabled.clear(); p.tools.forEach(name => { if (_allTools.some(t => t.name === name)) _enabled.add(name); }); saveStored(); renderSubsystems(); } function renderCustomPresets() { const container = _sidebarEl?.querySelector('#custom-presets'); if (!container) return; if (_customPresets.length === 0) { container.innerHTML = '
勾选工具后点 + 保存为预设
'; return; } container.innerHTML = ''; for (const p of _customPresets) { const item = document.createElement('div'); item.className = 'custom-preset-item'; item.innerHTML = ` `; item.querySelector('.custom-preset-apply').addEventListener('click', () => applyCustomPreset(p.id)); item.querySelector('.custom-preset-delete').addEventListener('click', (e) => { e.stopPropagation(); if (confirm(`删除预设 "${p.name}"?`)) deleteCustomPreset(p.id); }); container.appendChild(item); } } export function init(sidebarEl, opts = {}) { _sidebarEl = sidebarEl; _subsystemsListEl = sidebarEl.querySelector('#subsystems-list'); _enabledCountEl = sidebarEl.querySelector('[data-role="enabled-count"]'); _totalCountEl = sidebarEl.querySelector('[data-role="total-count"]'); _sourceInfoEl = sidebarEl.querySelector('#source-info'); _subsystemSummaryEl = sidebarEl.querySelector('#subsystem-summary'); _onToolsChange = opts.onToolsChange; const onSourceChange = opts.onSourceChange; sidebarEl.querySelector('#sidebar-toggle').addEventListener('click', () => { sidebarEl.classList.toggle('collapsed'); }); sidebarEl.querySelector('#subsystems-title').addEventListener('click', (e) => { if (e.target.closest('.collapse-btn') || e.target.classList.contains('sidebar-collapsible-title') || e.currentTarget === e.target) { sidebarEl.querySelector('.subsystems-section').classList.toggle('section-collapsed'); const btn = sidebarEl.querySelector('#subsystems-title .collapse-btn'); if (btn) btn.textContent = sidebarEl.querySelector('.subsystems-section').classList.contains('section-collapsed') ? '▶' : '▼'; } }); sidebarEl.querySelectorAll('[data-preset]').forEach(btn => { btn.addEventListener('click', () => applyPreset(btn.dataset.preset)); }); const btnSave = sidebarEl.querySelector('#btn-save-preset'); btnSave?.addEventListener('click', () => saveCurrentAsPreset()); const btnRestore = sidebarEl.querySelector('#btn-restore-presets'); btnRestore?.addEventListener('click', () => { const added = restoreDefaultPresets(); alert(added > 0 ? `已恢复 ${added} 个默认预设` : '默认预设已全部存在'); }); const presetsTitle = sidebarEl.querySelector('#presets-title'); presetsTitle?.addEventListener('click', (e) => { if (e.target.closest('.config-btn')) return; const section = e.currentTarget.closest('.presets-section'); section.classList.toggle('section-collapsed'); const btn = e.currentTarget.querySelector('.collapse-btn'); if (btn) btn.textContent = section.classList.contains('section-collapsed') ? '▶' : '▼'; }); let currentSource = loadModelSource(); sidebarEl.querySelectorAll('[data-source]').forEach(btn => { btn.classList.toggle('active', btn.dataset.source === currentSource); btn.addEventListener('click', async () => { const newSource = btn.dataset.source; if (newSource === currentSource) return; if (newSource === 'anthropic') { const cfg = loadAnthropicConfig(); if (!cfg.authToken) { alert('请先点配置按钮(配)配置云端 API 的 Auth Token'); return; } } if (newSource === 'omlx') { const cfg = loadOmlxConfig(); if (!cfg.apiKey) { alert('请先点配置按钮(配)配置 oMLX 的 API Key'); return; } } const labels = { ollama: 'Ollama', omlx: 'oMLX', sparkle: 'Sparkle', anthropic: '云端 API' }; const fromLabel = labels[currentSource] || currentSource; const toLabel = labels[newSource] || newSource; const isLocalSwitch = (currentSource !== 'anthropic' || newSource !== 'anthropic'); const msg = `确认从「${fromLabel}」切换到「${toLabel}」?` + (isLocalSwitch ? '\n\n切换会卸载当前模型 + 加载新模型(约 5-15 秒)。\n频繁切换会消耗内存和加载时间。' : '') + (newSource === 'sparkle' ? '\nSparkle 引擎加载约需 90 秒。' : ''); if (!confirm(msg)) return; saveModelSource(newSource); currentSource = newSource; sidebarEl.querySelectorAll('[data-source]').forEach(b => { b.classList.toggle('active', b.dataset.source === newSource); }); updateSourceInfo(); if (onSourceChange) onSourceChange(newSource); }); }); loadCustomPresets(); renderCustomPresets(); updateSourceInfo(); renderSubsystems(); } function updateSourceInfo() { if (!_sourceInfoEl) return; const info = getCurrentModelInfo(); const model = shortModelName(info.model); const tierLabel = ({ fast: '快速档', standard: '平衡档', pro: '增强档' })[info.tier] || ''; if (info.source === 'anthropic') { _sourceInfoEl.textContent = `${model} · ${tierLabel}`; _sourceInfoEl.className = 'source-info cloud'; } else if (info.source === 'omlx') { _sourceInfoEl.textContent = model; _sourceInfoEl.className = 'source-info local'; } else if (info.source === 'sparkle') { _sourceInfoEl.textContent = model; _sourceInfoEl.className = 'source-info local'; } else { _sourceInfoEl.textContent = model; _sourceInfoEl.className = 'source-info local'; } } export function refreshSourceDisplay() { if (!_sidebarEl) return; const currentSource = loadModelSource(); _sidebarEl.querySelectorAll('[data-source]').forEach(btn => { btn.classList.toggle('active', btn.dataset.source === currentSource); }); updateSourceInfo(); } export function startSubsystemMonitor(onSubsystemsReady) { if (_monitorTimer) return; refreshSubsystemsFromRegistry().finally(() => { checkSubsystems(onSubsystemsReady); _monitorTimer = setInterval(() => checkSubsystems(onSubsystemsReady), 5000); _registryTimer = setInterval(refreshSubsystemsFromRegistry, 30000); }); } export function stopSubsystemMonitor() { if (_monitorTimer) { clearInterval(_monitorTimer); _monitorTimer = null; } if (_registryTimer) { clearInterval(_registryTimer); _registryTimer = null; } } async function checkSubsystems(onReady) { let connectedCount = 0; const wasAnyConnected = Object.values(_subsystemStates).some(s => s === 'active'); const prevStates = { ..._subsystemStates }; for (const sys of SUBSYSTEMS) { if (!sys.healthUrl) { _subsystemStates[sys.id] = 'disabled'; continue; } try { const r = await fetch(sys.healthUrl, { signal: AbortSignal.timeout(2000) }); if (r.ok) { const data = await r.json(); let healthy = data.ok === true; if (sys.id === 'assignment') { if (data.service === 'profile-mcp-http') healthy = false; else if (data.service === 'assignment-mcp-http') healthy = true; else if (!data.service && data.result && typeof data.result.appStarted !== 'undefined') healthy = true; else healthy = false; } if (healthy) { _subsystemStates[sys.id] = 'active'; connectedCount++; } else { _subsystemStates[sys.id] = 'error'; } } else { _subsystemStates[sys.id] = 'error'; } } catch (e) { _subsystemStates[sys.id] = 'offline'; } } if (_subsystemSummaryEl) { _subsystemSummaryEl.textContent = `${connectedCount}/${SUBSYSTEMS.length}`; } const nowAnyConnected = Object.values(_subsystemStates).some(s => s === 'active'); const anyJustCameOnline = SUBSYSTEMS.some(sys => prevStates[sys.id] !== 'active' && _subsystemStates[sys.id] === 'active' ); if (anyJustCameOnline && onReady) { onReady(); } if (wasAnyConnected && !nowAnyConnected && _allTools.length > 0) { clearTools(); } renderSubsystems(); } function renderSubsystems() { if (!_subsystemsListEl) return; const enabledCount = _enabled.size; const totalCount = _allTools.length; if (_enabledCountEl) _enabledCountEl.textContent = enabledCount; if (_totalCountEl) _totalCountEl.textContent = totalCount; _subsystemsListEl.innerHTML = ''; for (const sys of SUBSYSTEMS) { const state = _subsystemStates[sys.id] || 'offline'; const collapsed = _collapsedSubsystems.has(sys.id); const isActive = state === 'active'; const block = document.createElement('div'); block.className = `subsystem-block ${state} ${collapsed ? 'collapsed' : ''}`; block.dataset.subsystem = sys.id; const statusText = getStatusText(sys, state); block.innerHTML = `
${sys.name} ${getDotEmoji(state)} ${escapeHtml(statusText)}
`; const body = block.querySelector('.subsystem-block-body'); if (isActive) { const sysTools = _allTools.filter(t => subsysIdOfTool(t.name) === sys.id); const sysEnabled = sysTools.filter(t => _enabled.has(t.name)).length; const crossDiagBtn = sys.id === 'profile' ? `` : ''; body.innerHTML = `
${crossDiagBtn} ${sysEnabled}/${sysTools.length}
`; body.querySelectorAll('[data-preset]').forEach(btn => { btn.addEventListener('click', () => applyPreset(btn.dataset.preset, btn.dataset.subsyst)); }); const toggleBtn = body.querySelector('[data-action="cross-module-toggle"]'); if (toggleBtn) { toggleBtn.addEventListener('click', () => { const next = !loadCrossModuleEnabled(); saveCrossModuleEnabled(next); toggleBtn.classList.toggle('active', next); toggleBtn.title = next ? '已开启:综合评价升级为跨模块深度诊断(点击关闭)' : '开启后,综合评价模块升级为跨模块深度诊断(1200-1500 字,跨模块关联+根因+优先级+风险)'; }); } renderToolGroups(body.querySelector('.tool-groups'), sys.id); } else if (state === 'offline') { body.innerHTML = '
等待接入...
'; } else if (state === 'disabled') { body.innerHTML = `
${sys.descPrefix} · 未接入
`; } else if (state === 'error') { body.innerHTML = '
健康检查失败
'; } block.querySelector('.subsystem-block-toggle').addEventListener('click', (e) => { e.stopPropagation(); if (_collapsedSubsystems.has(sys.id)) { _collapsedSubsystems.delete(sys.id); } else { _collapsedSubsystems.add(sys.id); } saveCollapseState(); renderSubsystems(); }); _subsystemsListEl.appendChild(block); } if (_onToolsChange) _onToolsChange(getEnabledTools()); } function getStatusText(sys, state) { if (state === 'active') { const sysTools = _allTools.filter(t => subsysIdOfTool(t.name) === sys.id); if (sysTools.length > 0) return `${sysTools.length} 工具`; return '已连接'; } if (state === 'error') return '错误'; if (state === 'disabled') return '未接入'; if (state === 'offline') return '未启动'; return ''; } function getDotEmoji(state) { return ``; } function renderToolGroups(container, subsystemId) { const tools = _allTools.filter(t => subsysIdOfTool(t.name) === subsystemId); if (tools.length === 0) { container.innerHTML = '
该子系统暂无工具
'; return; } const groups = new Map(); for (const t of tools) { const g = groupOf(t.name); if (!groups.has(g)) groups.set(g, []); groups.get(g).push(t); } const orderedGroups = [...groups.entries()].sort((a, b) => a[0].localeCompare(b[0], 'zh')); container.innerHTML = ''; for (const [gname, tools] of orderedGroups) { tools.sort((a, b) => a.name.localeCompare(b.name)); const enabledCount = tools.filter(t => _enabled.has(t.name)).length; const allEnabled = enabledCount === tools.length; const expanded = _expandedGroups.has(gname); const groupEl = document.createElement('div'); groupEl.className = 'tool-group' + (expanded ? ' expanded' : ''); groupEl.innerHTML = `
${escapeHtml(gname)} ${enabledCount}/${tools.length}
`; const bodyEl = groupEl.querySelector('.tool-group-body'); for (const t of tools) { const checked = _enabled.has(t.name); const label = getToolLabel(t); const itemEl = document.createElement('label'); itemEl.className = 'tool-item'; itemEl.title = t.name; itemEl.innerHTML = ` ${escapeHtml(label)} `; const cb = itemEl.querySelector('input'); cb.addEventListener('change', () => { toggleTool(t.name, cb.checked); }); bodyEl.appendChild(itemEl); } groupEl.querySelector('.tool-group-header').addEventListener('click', () => { if (_expandedGroups.has(gname)) { _expandedGroups.delete(gname); } else { _expandedGroups.add(gname); } saveCollapseState(); renderSubsystems(); }); container.appendChild(groupEl); } } function loadStored() { try { const s = localStorage.getItem(STORAGE_KEY); return s ? JSON.parse(s) : []; } catch (e) { return []; } } function saveStored() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify([..._enabled])); } catch (e) {} } function escapeHtml(s) { if (s == null) return ''; return String(s).replace(/&/g, '&').replace(//g, '>'); }