// @ts-nocheck import { CONFIG, loadModelSource, clearHistoryStorage, loadThinkingEnabled, saveThinkingEnabled, getCurrentModelInfo, modelThinkingCapability } from './utils/config.js'; import { start as startMetrics, resetPromptCache } from './utils/metrics-client.js'; import { warmup as warmupOllama } from './clients/ollama-client.js'; import { warmup as warmupSparkle, warmupReportAnalysis } from './clients/sparkle-client.js'; import * as sidebar from './ui/sidebar.js'; import { showConfigDialog } from './ui/config-dialog.js'; import { log as auditLog } from './utils/audit-log.js'; import { initBadge as initAuditBadge } from './ui/audit-ui.js'; import { render as renderMarkdown } from './reports/markdown.js'; import * as pidmapStore from './utils/pidmap-store.js'; import { mountExportBar } from './ui/export-report.js'; import * as conversationStore from './utils/conversation-store.js'; import * as mcp from './utils/mcp-client.js'; import * as sessionContext from './core/session-context.js'; import { unloadOllamaModels, unloadOmlxModels, unloadSparkleModels, unloadAllLocalModels, ensureLocalSourceReady, probeOllama, probeOmlx, probeSparkle } from './utils/model-manager.js'; import { installLeaveGuards } from './ui/leave-confirm.js'; import * as agentLoop from './core/agent-loop.js'; const els = { sidebar: document.getElementById('sidebar'), btnModelConfig: document.getElementById('btn-model-config'), btnThinkingToggle: document.getElementById('btn-thinking-toggle'), messages: document.getElementById('messages'), emptyHint: document.getElementById('empty-hint'), input: document.getElementById('input'), send: document.getElementById('send'), btnClear: document.getElementById('btn-clear'), inputHint: document.getElementById('input-hint'), }; const _mode = 'mcp'; let _history = []; let _isRunning = false; init(); function init() { pidmapStore.restore(); const maskingEl = document.getElementById('masking-status'); const maskingText = document.getElementById('masking-status-text'); const maskingDot = document.getElementById('masking-status-dot'); let _lastMaskingState = null; function refreshMaskingStatus() { const s = pidmapStore.formatStatus(); if (maskingText) maskingText.textContent = s.text; if (maskingDot) maskingDot.textContent = s.dot; if (maskingEl) { maskingEl.title = s.title; maskingEl.className = `masking-status state-${s.cls}`; } if (_lastMaskingState !== 'expired' && s.cls === 'expired') { import('./reports/markdown.js').then(({ clearMaskingPidMap }) => { clearMaskingPidMap(); auditLog('system', '脱敏会话已过期,清内存 pidMap(后续渲染显示密文)'); }).catch(() => {}); } _lastMaskingState = s.cls; } refreshMaskingStatus(); setInterval(refreshMaskingStatus, 60 * 1000); if (maskingEl) { maskingEl.addEventListener('click', async () => { const ok = confirm( '重新更新映射表?\n\n' + '• 当前历史对话将变为密文(token 显示)\n' + '• profile 假名映射将被清空\n' + '• 重新查询后,新对话显示明文,历史保持密文\n' + '• 需要重新发起查询继续操作\n\n' + '确认继续?' ); if (!ok) return; try { if (_isRunning) agentLoop.abort(); auditLog('system', '用户手动重置映射表'); await mcp.notifyContextCleared(); pidmapStore.clear(); location.reload(); } catch (e) { showErrorBanner(`重置失败: ${e.message}`); } }); } els.input.addEventListener('input', autoResize); els.input.addEventListener('keydown', (e) => { if (e.isComposing || e.keyCode === 229) return; if (e.key === 'Escape' && _isRunning) { e.preventDefault(); stopGeneration(); return; } if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (_isRunning) stopGeneration(); else send(); } }); els.send.addEventListener('click', () => { if (_isRunning) stopGeneration(); else send(); }); els.btnClear.addEventListener('click', async () => { if (!confirm('确定清空所有对话?')) return; if (_isRunning) agentLoop.abort(); _history = []; els.messages.innerHTML = ''; els.messages.appendChild(els.emptyHint); els.emptyHint.style.display = ''; clearHistoryStorage(); resetPromptCache(); sessionContext.clear(); pidmapStore.clear(); try { await mcp.notifyContextCleared(); } catch (e) {} const ok = await conversationStore.clear(); if (ok) { auditLog('system', '对话已清空'); showHint('对话已清空'); } else { showErrorBanner('清空持久化失败,刷新后对话可能仍在'); } }); if (els.btnThinkingToggle) { const syncThinkBtn = () => { const on = loadThinkingEnabled(); const model = getCurrentModelInfo()?.model || ''; const cap = modelThinkingCapability(model); els.btnThinkingToggle.classList.toggle('active', on); let title = on ? '思考过程:开(点击关闭)。思考会随对话保存,刷新可恢复,但不回传给模型' : '思考过程:关(点击开启)。生产建议保持关闭'; if (on && cap === 'no') { title = `当前模型「${model}」为 Instruct-2507 非思考版,无法显示思考。请换 Thinking-2507`; } els.btnThinkingToggle.title = title; els.btnThinkingToggle.setAttribute('aria-pressed', on ? 'true' : 'false'); }; syncThinkBtn(); els.btnThinkingToggle.addEventListener('click', () => { const next = !loadThinkingEnabled(); saveThinkingEnabled(next); syncThinkBtn(); auditLog('system', `Thinking ${next ? '开' : '关'}`); const model = getCurrentModelInfo()?.model || ''; const cap = modelThinkingCapability(model); if (next && cap === 'no') { showHint(`已开「想」,但 ${model || '当前模型'} 是非思考版,不会有思考过程。请换 Thinking-2507`); } else { showHint(next ? '已开启thinking模式' : '已关闭thinking模式(仅意图理解)'); } }); } els.btnModelConfig.addEventListener('click', async () => { const saved = await showConfigDialog(); if (saved) { sidebar.refreshSourceDisplay(); showHint('配置已保存'); } }); startMetrics(); sidebar.init(els.sidebar, { onToolsChange: (tools) => { if (!_isRunning && tools.length > 0) { showHint(`已启用 ${tools.length} 个工具`); } }, onSourceChange: async (newSource) => { auditLog('system', `模型源切换 → ${newSource}`); const sourceLabels = { ollama: 'Ollama', omlx: 'oMLX', sparkle: 'Sparkle', anthropic: '云端 API' }; showHint(`正在切换到 ${sourceLabels[newSource] || newSource},卸载旧模型...`, 'busy'); if (newSource !== 'ollama') { const n = await unloadOllamaModels(); if (n > 0) auditLog('system', `已卸载 Ollama ${n} 个模型`); } if (newSource !== 'omlx') { const n = await unloadOmlxModels(); if (n > 0) auditLog('system', `已卸载 oMLX ${n} 个模型`); } if (newSource !== 'sparkle') { const n = await unloadSparkleModels(); if (n > 0) auditLog('system', `已卸载 Sparkle ${n} 个模型`); } if (newSource === 'ollama') { const p = await probeOllama(); if (!p.ok) { showHint(`已切到 Ollama,但未检测到服务(${p.baseUrl})。请安装启动 Ollama,或改用云端 API。`, 'error'); return; } showHint('正在预热 Ollama 模型...', 'busy'); await warmupOllama().catch(() => false); } else if (newSource === 'omlx') { const p = await probeOmlx(); if (!p.ok) { showHint(p.reason === 'no_key' ? '已切到 oMLX,请先在「配」中填写 API Key。' : `已切到 oMLX,但未检测到服务(${p.baseUrl})。请启动 oMLX,或改用云端 API。`, 'error'); return; } } else if (newSource === 'sparkle') { let engineStarted = false; let engineInstalled = true; try { const r = await fetch(`${CONFIG.ZHONGTAI_BASE}/sparkle-engine/status`, { signal: AbortSignal.timeout(3000), }); if (r.ok) { const st = await r.json(); engineInstalled = !!st.installed; if (st.installed && !st.running) { showHint('正在启动 Sparkle 引擎,模型加载约需 90 秒...', 'busy'); const sr = await fetch(`${CONFIG.ZHONGTAI_BASE}/sparkle-engine/start`, { method: 'POST', }).catch(() => null); engineStarted = !!(sr && sr.ok); } } } catch (e) { } if (engineStarted) { showHint('已切到 Sparkle,引擎启动中,模型加载约需 90 秒,加载完成后即可对话。'); return; } if (!engineInstalled) { showHint('已切到 Sparkle,但未检测到本地引擎安装。引擎为可选组件,可在「配」中查看说明,或改用其他模型源。', 'error'); return; } const p = await probeSparkle(); if (!p.ok) { showHint(`已切到 Sparkle,但未检测到服务(${p.baseUrl})。请在「配 → 本地 Sparkle」中点「加载」启动引擎,或改用云端 API。`, 'error'); return; } warmupSparkle(sidebar.getEnabledTools()) .then(() => warmupReportAnalysis()) .catch(() => false); } const label = sourceLabels[newSource] || newSource; showHint(`已切换到 ${label}`); }, }); showHint('中台已启动,等待子系统接入...'); auditLog('system', '中台启动'); sidebar.startSubsystemMonitor(async () => { try { const tools = await sidebar.loadTools(_mode, true); auditLog('system', `子系统接入 · ${tools.length} 工具`); showHint(`子系统已接入 · ${tools.length} 工具`); const source = loadModelSource(); if (source === 'ollama') { const p = await probeOllama(); if (p.ok) warmupOllama().catch(() => false); } else if (source === 'sparkle') { const p = await probeSparkle(); if (p.ok) { warmupSparkle(tools) .then(() => warmupReportAnalysis()) .catch(() => false); } } } catch (e) { auditLog('error', `加载工具失败: ${e.message}`); showHint(`加载工具失败: ${e.message}`, 'error'); } }); initAuditBadge(els.sidebar); mcp.onMaskingConfigChanged(async (payload) => { auditLog('error', `脱敏配置变更(profile MASKING_ENABLED=${payload?.maskingEnabled}) — 销毁会话`, payload); if (_isRunning) agentLoop.abort(); _history = []; els.messages.innerHTML = ''; els.messages.appendChild(els.emptyHint); els.emptyHint.style.display = ''; clearHistoryStorage(); sessionContext.clear(); pidmapStore.clear(); try { await conversationStore.clear(); } catch (e) {} showErrorBanner('脱敏配置已变更,已开启新会话(避免历史 token 与新明文混合导致 AI 混淆)'); }); installLeaveGuards(); (async () => { const source = loadModelSource(); if (source !== 'ollama' && source !== 'omlx' && source !== 'sparkle') return; try { await ensureLocalSourceReady(source); } catch (e) { auditLog('system', e.message); showHint(e.message, 'error'); } })(); (async () => { try { const raw = await conversationStore.load(); const restored = dedupeHistory(raw); if (restored.length > 0) { _history = restored; renderRestoredMessages(restored); if (restored.length !== raw.length) { conversationStore.save(restored); } auditLog('system', `已恢复上次对话(${restored.length} 条消息,从中台)`); } } catch (e) { console.warn('[history] 恢复失败:', e); } els.input.focus(); })(); } function renderRestoredMessages(history) { if (els.emptyHint.parentNode) { els.emptyHint.style.display = 'none'; } const toolResults = {}; for (const m of history) { if (m.role === 'tool' && m.toolCallId) { try { toolResults[m.toolCallId] = JSON.parse(m.content); } catch (e) { toolResults[m.toolCallId] = { ok: false }; } } } let userCount = 0; let assistantCount = 0; let toolCount = 0; for (let i = 0; i < history.length; i++) { const m = history[i]; if (m.role === 'user') { userCount++; const msg = document.createElement('div'); msg.className = 'msg user'; msg.innerHTML = `