// @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 = `
U
${escapeHtml(m.content)}
`; els.messages.appendChild(msg); } else if (m.role === 'assistant') { assistantCount++; const msg = document.createElement('div'); msg.className = 'msg assistant'; const thinking = String(m.thinking || '').trim(); msg.innerHTML = `
A
思考过程 ${thinking ? '▶' : '▼'}

                        
`; const thinkPanel = msg.querySelector('.think-panel'); const thinkBody = msg.querySelector('.think-panel-body'); const header = msg.querySelector('.think-panel-header'); if (thinking && thinkBody) { thinkBody.textContent = thinking; } header?.addEventListener('click', () => { thinkPanel.classList.toggle('collapsed'); const chev = header.querySelector('.think-panel-chevron'); if (chev) chev.textContent = thinkPanel.classList.contains('collapsed') ? '▶' : '▼'; }); const textEl = msg.querySelector('.answer-panel'); if (m.content) { textEl.innerHTML = renderMarkdown(m.content); if (!m.toolCalls || m.toolCalls.length === 0) { const textWrap = msg.querySelector('.assistant-text'); mountExportBar(textWrap, m.content, { messages: history.slice(0, i + 1) }); } } const toolContainer = msg.querySelector('.tool-cards-container'); if (m.toolCalls && m.toolCalls.length > 0) { for (const tc of m.toolCalls) { toolCount++; const result = toolResults[tc.id] || {}; const isOk = result.ok !== false; const isMeta = tc.name === 'metro_search_tools' || tc.name === 'metro_get_tool_detail'; const tag = document.createElement('div'); tag.className = `tool-card ${isOk ? 'success' : 'error'}${isMeta ? ' meta' : ''}`; tag.innerHTML = `
${escapeHtml(sidebar.getToolNameLabel(tc.name))} ${isOk ? '成功' : '失败'}
`; toolContainer.appendChild(tag); } } els.messages.appendChild(msg); } } if (userCount > 0 || assistantCount > 0) { const banner = document.createElement('div'); banner.className = 'restore-banner'; banner.innerHTML = `已恢复上次对话(用户 ${userCount} / 模型 ${assistantCount} / 工具 ${toolCount} 条)工具调用详情见审计日志`; els.messages.insertBefore(banner, els.messages.firstChild); } scrollToBottom(); } function autoResize() { els.input.style.height = 'auto'; els.input.style.height = Math.min(200, els.input.scrollHeight) + 'px'; } async function send() { const text = els.input.value.trim(); if (!text || _isRunning) return; if (els.emptyHint.parentNode) { els.emptyHint.style.display = 'none'; } appendUserMessage(text); els.input.value = ''; autoResize(); setRunning(true); const enabledTools = sidebar.getEnabledTools(); if (enabledTools.length === 0) { showErrorBanner('没有启用任何工具,请在左侧面板勾选工具或点预设'); setRunning(false); return; } try { await ensureLocalSourceReady(loadModelSource()); } catch (e) { showErrorBanner(e.message); showHint(e.message, 'error'); setRunning(false); return; } const sourceLabel = loadModelSource() === 'anthropic' ? '云端' : '本地'; showHint(`${sourceLabel} ${_mode.toUpperCase()} · ${enabledTools.length} 工具 · Esc 停止`, 'busy'); try { await agentLoop.run({ userInput: text, mode: _mode, history: _history, enabledTools, parentEl: els.messages, hooks: { onAssistantTextDelta: () => scrollToBottom(), onError: (msg) => showErrorBanner(msg), onAbort: (msg) => { showHint(msg || '已停止生成'); auditLog('system', msg || '已停止生成'); }, onComplete: (newHistory) => { _history = dedupeHistory(newHistory); conversationStore.save(_history); if (!_isRunning) return; showHint('完成'); }, }, }); } catch (e) { if (e.name !== 'AbortError') { showErrorBanner(`运行异常: ${e.message}`); } } finally { setRunning(false); if (els.inputHint.textContent === '已停止生成' || els.inputHint.textContent.startsWith('已停止')) { setTimeout(() => { if (!_isRunning) showHint('就绪'); }, 1500); } else { showHint('就绪'); } scrollToBottom(); els.input.focus(); } } function stopGeneration() { if (!_isRunning) return; agentLoop.abort(); showHint('正在停止...', 'busy'); } function appendUserMessage(text) { const msg = document.createElement('div'); msg.className = 'msg user'; msg.innerHTML = `
U
${escapeHtml(text)}
`; els.messages.appendChild(msg); scrollToBottom(); } function showErrorBanner(message) { const banner = document.createElement('div'); banner.className = 'error-banner'; banner.innerHTML = `错误${escapeHtml(message)}`; els.messages.appendChild(banner); scrollToBottom(); setTimeout(() => { banner.style.transition = 'opacity 0.5s'; banner.style.opacity = '0'; setTimeout(() => banner.remove(), 500); }, 8000); } function showHint(text, type) { els.inputHint.textContent = text; els.inputHint.className = 'input-hint' + (type ? ' ' + type : ''); } function setRunning(running) { _isRunning = running; els.send.classList.toggle('is-stop', running); els.send.title = running ? '停止生成 (Esc)' : '发送 (Enter)'; els.send.setAttribute('aria-label', running ? '停止生成' : '发送'); els.input.disabled = false; els.input.placeholder = running ? '正在生成… 点停止按钮或按 Esc 终止' : '对 ccSparkle Agent 说点什么... (Enter 发送,Shift+Enter 换行)'; } function scrollToBottom() { const el = els.messages; if (!el) return; const top = el.scrollHeight; if (typeof el.scrollTo === 'function') { el.scrollTo({ top, behavior: 'auto' }); } else { el.scrollTop = top; } } function escapeHtml(s) { if (s == null) return ''; return String(s).replace(/&/g, '&').replace(//g, '>'); } function dedupeHistory(messages) { const out = []; for (const m of messages) { const prev = out[out.length - 1]; if (prev && m.role === 'user' && prev.role === 'user' && m.content === prev.content) { continue; } out.push(m); } return out; }