// @ts-nocheck import { CONFIG, loadOllamaConfig, saveOllamaConfig, loadAnthropicConfig, saveAnthropicConfig, loadOmlxConfig, saveOmlxConfig, loadSparkleConfig, saveSparkleConfig, loadDefaultSource, saveDefaultSource, loadContextThreshold, saveContextThreshold, THRESHOLD_OPTIONS, CLOUD_API_PRESETS, DEFAULT_OLLAMA, DEFAULT_ANTHROPIC, DEFAULT_OMLX, } from '../utils/config.js'; import { shortName } from '../utils/metrics-client.js'; function thresholdSelect(source) { const current = loadContextThreshold(source); return ``; } export function showConfigDialog() { return new Promise((resolve) => { const ollama = loadOllamaConfig(); const anthropic = loadAnthropicConfig(); const omlx = loadOmlxConfig(); const sparkle = loadSparkleConfig(); const defaultSource = loadDefaultSource(); const overlay = document.createElement('div'); overlay.className = 'dialog-overlay'; overlay.innerHTML = ` `; document.body.appendChild(overlay); document.body.classList.add('modal-open'); const close = (result) => { stopEngineTimers(); overlay.remove(); document.body.classList.remove('modal-open'); resolve(result); }; overlay.querySelectorAll('[data-tab]').forEach(btn => { btn.addEventListener('click', () => { const tab = btn.dataset.tab; overlay.querySelectorAll('[data-tab]').forEach(b => b.classList.toggle('active', b === btn)); overlay.querySelectorAll('[data-pane]').forEach(p => p.classList.toggle('active', p.dataset.pane === tab)); }); }); overlay.querySelectorAll('.btn-toggle-pw').forEach(btn => { btn.addEventListener('click', () => { const input = document.getElementById(btn.dataset.target); input.type = input.type === 'password' ? 'text' : 'password'; }); }); const urlInput = overlay.querySelector('#cfg-anthropic-url'); const presetHint = overlay.querySelector('#cfg-anthropic-preset-hint'); const urlHint = overlay.querySelector('#cfg-anthropic-url-hint'); const versionRow = overlay.querySelector('#cfg-anthropic-version-row'); function currentProtocol() { const sel = overlay.querySelector('#cfg-anthropic-protocol-select'); return sel ? sel.value : 'anthropic'; } function currentPreset() { const sel = overlay.querySelector('#cfg-anthropic-preset-select'); return sel ? sel.value : 'custom'; } function setPreset(presetId) { const sel = overlay.querySelector('#cfg-anthropic-preset-select'); if (sel) sel.value = presetId; } function setProtocol(protocol) { const sel = overlay.querySelector('#cfg-anthropic-protocol-select'); if (!sel) return; sel.value = protocol; syncProtocolUi(protocol); } function syncProtocolUi(protocol) { if (urlHint) { urlHint.textContent = protocol === 'openai' ? '将请求 {Base}/chat/completions' : '将请求 {Base}/v1/messages'; } if (versionRow) { versionRow.style.display = protocol === 'openai' ? 'none' : ''; } } function applyPreset(presetId, { fillUrl = true } = {}) { const preset = CLOUD_API_PRESETS.find(p => p.id === presetId) || CLOUD_API_PRESETS.find(p => p.id === 'custom') || CLOUD_API_PRESETS[CLOUD_API_PRESETS.length - 1]; const tokenInput = overlay.querySelector('#cfg-anthropic-token'); const fastEl = overlay.querySelector('#cfg-anthropic-fast'); const stdEl = overlay.querySelector('#cfg-anthropic-standard'); const proEl = overlay.querySelector('#cfg-anthropic-pro'); const tierChecked = overlay.querySelector('input[name="tier"]:checked'); const currentPresetId = anthropic.preset || 'custom'; const snapshot = { authToken: tokenInput?.value || '', fastModel: fastEl?.value || '', standardModel: stdEl?.value || '', proModel: proEl?.value || '', currentTier: tierChecked?.value || anthropic.currentTier || 'fast', }; if (currentPresetId !== 'custom' && snapshot.authToken) { anthropic.presets = anthropic.presets || {}; anthropic.presets[currentPresetId] = snapshot; } if (currentPresetId === 'custom' && (snapshot.authToken || snapshot.fastModel)) { anthropic.presets = anthropic.presets || {}; anthropic.presets['custom'] = snapshot; } setPreset(preset.id); if (presetHint) presetHint.textContent = preset.hint || ''; if (preset.protocol) setProtocol(preset.protocol); if (fillUrl && preset.baseUrl && urlInput) urlInput.value = preset.baseUrl; if (preset.models) { if (fastEl && 'fastModel' in preset.models) fastEl.value = preset.models.fastModel || ''; if (stdEl && 'standardModel' in preset.models) stdEl.value = preset.models.standardModel || ''; if (proEl && 'proModel' in preset.models) proEl.value = preset.models.proModel || ''; if (preset.models.currentTier) { const tierRadio = overlay.querySelector(`input[name="tier"][value="${preset.models.currentTier}"]`); if (tierRadio) tierRadio.checked = true; } } anthropic.presets = anthropic.presets || {}; const saved = anthropic.presets[preset.id]; if (saved) { if (tokenInput) tokenInput.value = saved.authToken || ''; if (saved.fastModel && fastEl) fastEl.value = saved.fastModel; if (saved.standardModel && stdEl) stdEl.value = saved.standardModel; if (saved.proModel && proEl) proEl.value = saved.proModel; if (saved.currentTier) { const tierRadio = overlay.querySelector(`input[name="tier"][value="${saved.currentTier}"]`); if (tierRadio) tierRadio.checked = true; } } else if (preset.id !== 'custom') { if (tokenInput) tokenInput.value = ''; } syncProtocolUi(preset.protocol || currentProtocol()); } const presetSelect = overlay.querySelector('#cfg-anthropic-preset-select'); if (presetSelect) { presetSelect.addEventListener('change', () => { applyPreset(presetSelect.value, { fillUrl: true }); }); } const protocolSelect = overlay.querySelector('#cfg-anthropic-protocol-select'); if (protocolSelect) { protocolSelect.addEventListener('change', () => { const newProtocol = protocolSelect.value; if (currentPreset() !== 'custom') { const preset = CLOUD_API_PRESETS.find(p => p.id === currentPreset()); if (preset?.protocol && preset.protocol !== newProtocol) { setPreset('custom'); if (presetHint) { presetHint.textContent = (CLOUD_API_PRESETS.find(p => p.id === 'custom') || {}).hint || ''; } } } syncProtocolUi(newProtocol); }); } if (urlInput) { urlInput.addEventListener('input', () => { if (currentPreset() === 'custom') return; const preset = CLOUD_API_PRESETS.find(p => p.id === currentPreset()); if (preset?.baseUrl && urlInput.value.trim().replace(/\/+$/, '') !== preset.baseUrl.replace(/\/+$/, '')) { setPreset('custom'); if (presetHint) { presetHint.textContent = (CLOUD_API_PRESETS.find(p => p.id === 'custom') || {}).hint || ''; } } }); } syncProtocolUi(currentProtocol()); async function autoFetchModels(target) { const urlInput = document.getElementById(`cfg-${target}-url`); const select = document.getElementById(`cfg-${target}-model`); if (!urlInput || !select) return; const baseUrl = (urlInput.value || '').trim().replace(/\/+$/, ''); if (!baseUrl) return; const currentVal = select.dataset.current || select.value || ''; select.innerHTML = ''; try { let models = []; if (target === 'ollama') { const r = await fetch(`${baseUrl}/api/tags`); if (!r.ok) throw new Error(`HTTP ${r.status}`); const data = await r.json(); models = (data.models || []).map(m => m.name); } else if (target === 'omlx') { const keyInput = document.getElementById('cfg-omlx-key'); const apiKey = (keyInput?.value || '').trim(); if (!apiKey) { select.innerHTML = ''; return; } const r = await fetch(`${baseUrl}/v1/models`, { headers: { 'Authorization': `Bearer ${apiKey}` }, }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const data = await r.json(); models = (data.data || []).map(m => m.id); } else if (target === 'sparkle') { const keyInput = document.getElementById('cfg-sparkle-key'); const apiKey = (keyInput?.value || '').trim() || 'sparkle'; const r = await fetch(`${baseUrl}/v1/models`, { headers: { 'Authorization': `Bearer ${apiKey}` }, }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const data = await r.json(); models = (data.data || []).map(m => m.id); } if (models.length === 0) { select.innerHTML = ''; } else { select.innerHTML = models.map(m => `` ).join(''); } } catch (e) { select.innerHTML = ``; } } autoFetchModels('ollama'); autoFetchModels('omlx'); autoFetchModels('sparkle'); overlay.querySelectorAll('.btn-refresh-models').forEach(btn => { btn.addEventListener('click', () => autoFetchModels(btn.dataset.target)); }); const ollamaUrlInput = overlay.querySelector('#cfg-ollama-url'); if (ollamaUrlInput) ollamaUrlInput.addEventListener('change', () => autoFetchModels('ollama')); const omlxUrlInput = overlay.querySelector('#cfg-omlx-url'); const omlxKeyInput = overlay.querySelector('#cfg-omlx-key'); if (omlxUrlInput) omlxUrlInput.addEventListener('change', () => autoFetchModels('omlx')); if (omlxKeyInput) omlxKeyInput.addEventListener('change', () => autoFetchModels('omlx')); const sparkleUrlInput = overlay.querySelector('#cfg-sparkle-url'); const sparkleKeyInput = overlay.querySelector('#cfg-sparkle-key'); if (sparkleUrlInput) sparkleUrlInput.addEventListener('change', () => autoFetchModels('sparkle')); if (sparkleKeyInput) sparkleKeyInput.addEventListener('change', () => autoFetchModels('sparkle')); const engineEls = { dot: overlay.querySelector('#sparkle-engine-dot'), status: overlay.querySelector('#sparkle-engine-status'), live: overlay.querySelector('#sparkle-engine-live'), btnLoad: overlay.querySelector('#sparkle-engine-load'), btnUnload: overlay.querySelector('#sparkle-engine-unload'), slots: overlay.querySelector('#sparkle-cfg-slots'), slotsMem: overlay.querySelector('#sparkle-cfg-slots-mem'), k: overlay.querySelector('#sparkle-cfg-k'), maxTokens: overlay.querySelector('#sparkle-cfg-maxtokens'), apply: overlay.querySelector('#sparkle-cfg-apply'), note: overlay.querySelector('#sparkle-engine-note'), }; let enginePollTimer = null; let engineStatsTimer = null; let engineConfigLoaded = false; let engineCurrentSlots = null; let pendingParamConfig = null; function stopEngineTimers() { if (enginePollTimer) { clearInterval(enginePollTimer); enginePollTimer = null; } if (engineStatsTimer) { clearInterval(engineStatsTimer); engineStatsTimer = null; } } function setEngineNote(text) { if (engineEls.note) engineEls.note.textContent = text || ''; } function updateSlotsMem() { if (!engineEls.slots || !engineEls.slotsMem) return; const slots = parseInt(engineEls.slots.value, 10) || 0; engineEls.slotsMem.textContent = `${slots} 槽 × 160MB ≈ ${(slots * 160 / 1024).toFixed(1)}GB 池内存`; } function renderEngineState(state, statusText) { if (!engineEls.dot) return; engineEls.dot.className = 'engine-dot' + (state === 'loading' ? ' is-loading' : state === 'ready' ? ' is-ready' : ''); engineEls.status.textContent = statusText; const canLoad = state === 'stopped'; const canUnload = state === 'loading' || state === 'ready'; const canEdit = state === 'ready'; engineEls.btnLoad.disabled = !canLoad; engineEls.btnUnload.disabled = !canUnload; engineEls.slots.disabled = !canEdit; engineEls.k.disabled = !canEdit; engineEls.maxTokens.disabled = !canEdit; engineEls.apply.disabled = !canEdit; if (!canEdit) engineConfigLoaded = false; } function updateEngineLive(stats) { if (!engineEls.live) return; if (!stats) { engineEls.live.textContent = ''; return; } const parts = []; if (stats.rss_bytes) parts.push(`RSS ${(stats.rss_bytes / 1073741824).toFixed(1)}GB`); if (stats.tok_per_s != null) parts.push(`${Number(stats.tok_per_s).toFixed(1)} tok/s`); engineEls.live.textContent = parts.join(' · '); } async function refreshEngineStatus() { let st = null; let timedOut = false; try { const r = await fetch(`${CONFIG.ZHONGTAI_BASE}/sparkle-engine/status`, { signal: AbortSignal.timeout(10000), }); if (r.ok) st = await r.json(); } catch (e) { timedOut = !!e && (e.name === 'TimeoutError' || e.name === 'AbortError'); } if (!st) { if (timedOut) { if (engineStatsTimer) { clearInterval(engineStatsTimer); engineStatsTimer = null; } renderEngineState('querying', '引擎繁忙,状态查询超时,重试中...'); if (!enginePollTimer) enginePollTimer = setInterval(refreshEngineStatus, 3000); return; } stopEngineTimers(); renderEngineState('zhongtai_down', '中台未启动,引擎管理不可用'); setEngineNote(''); updateEngineLive(null); return; } if (!st.installed) { stopEngineTimers(); renderEngineState('not_installed', '未检测到本地引擎安装'); setEngineNote('引擎为可选组件,未安装不影响其他模型源的使用。'); updateEngineLive(null); return; } if (!st.running) { stopEngineTimers(); renderEngineState('stopped', '未启动'); setEngineNote('点击「加载」启动引擎,首次加载约需 90 秒。'); updateEngineLive(null); return; } if (!st.loaded) { renderEngineState('loading', '加载中...'); updateEngineLive(null); if (engineStatsTimer) { clearInterval(engineStatsTimer); engineStatsTimer = null; } if (!enginePollTimer) enginePollTimer = setInterval(refreshEngineStatus, 3000); return; } if (enginePollTimer) { clearInterval(enginePollTimer); enginePollTimer = null; } renderEngineState('ready', '已就绪'); setEngineNote(''); updateEngineLive(st.stats); if (st.expert_slots != null) { engineCurrentSlots = st.expert_slots; engineEls.slots.value = st.expert_slots; updateSlotsMem(); } if (!engineConfigLoaded) { engineConfigLoaded = true; loadEngineConfig(); } if (pendingParamConfig) { const p = pendingParamConfig; pendingParamConfig = null; postEngineParams(p); } if (!engineStatsTimer) engineStatsTimer = setInterval(refreshEngineStats, 5000); } async function refreshEngineStats() { try { const r = await fetch(`${CONFIG.ZHONGTAI_BASE}/sparkle-engine/api/stats`, { signal: AbortSignal.timeout(4000), }); if (!r.ok) throw new Error(`HTTP ${r.status}`); updateEngineLive(await r.json()); } catch (e) { refreshEngineStatus(); } } async function loadEngineConfig() { try { const r = await fetch(`${CONFIG.ZHONGTAI_BASE}/sparkle-engine/api/config`, { signal: AbortSignal.timeout(4000), }); if (!r.ok) return; const cfg = await r.json(); if (cfg.expert_slots != null && engineCurrentSlots == null) { engineCurrentSlots = cfg.expert_slots; engineEls.slots.value = cfg.expert_slots; } if (cfg.k != null) engineEls.k.value = cfg.k; if (cfg.max_tokens != null) engineEls.maxTokens.value = cfg.max_tokens; updateSlotsMem(); } catch (e) {} } async function postEngineParams(params) { try { const r = await fetch(`${CONFIG.ZHONGTAI_BASE}/sparkle-engine/api/config`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params), }); setEngineNote(r.ok ? '参数已应用。' : `应用失败: HTTP ${r.status}`); } catch (e) { setEngineNote('应用失败:引擎不可达。'); } } engineEls.btnLoad?.addEventListener('click', async () => { renderEngineState('loading', '加载中...'); setEngineNote('正在启动引擎...'); let data = null; try { const r = await fetch(`${CONFIG.ZHONGTAI_BASE}/sparkle-engine/start`, { method: 'POST' }); data = r.ok ? await r.json() : null; } catch (e) { renderEngineState('zhongtai_down', '中台未启动,引擎管理不可用'); setEngineNote(''); return; } if (data && data.ok) { setEngineNote('引擎加载中,约需 90 秒。'); } else if (data && data.error === 'not_installed') { renderEngineState('not_installed', '未检测到本地引擎安装'); setEngineNote('引擎为可选组件,未安装不影响其他模型源的使用。'); return; } else { setEngineNote(`启动失败: ${(data && data.error) || '中台无响应'}`); } if (!enginePollTimer) enginePollTimer = setInterval(refreshEngineStatus, 3000); }); engineEls.btnUnload?.addEventListener('click', async () => { engineEls.btnUnload.disabled = true; setEngineNote('正在停止引擎...'); try { const r = await fetch(`${CONFIG.ZHONGTAI_BASE}/sparkle-engine/stop`, { method: 'POST' }); const data = r.ok ? await r.json() : null; setEngineNote(data && data.ok ? '引擎已停止。' : `停止失败: ${(data && data.error) || '中台无响应'}`); } catch (e) { setEngineNote('中台未启动,无法停止引擎。'); } stopEngineTimers(); refreshEngineStatus(); }); engineEls.slots?.addEventListener('input', updateSlotsMem); engineEls.apply?.addEventListener('click', async () => { const newSlots = parseInt(engineEls.slots.value, 10); const kVal = parseInt(engineEls.k.value, 10); const mtVal = parseInt(engineEls.maxTokens.value, 10); const slotsChanged = engineCurrentSlots != null && newSlots !== engineCurrentSlots; engineEls.apply.disabled = true; if (slotsChanged) { try { const r = await fetch(`${CONFIG.ZHONGTAI_BASE}/sparkle-engine/restart`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ expert_slots: newSlots }), }); const data = r.ok ? await r.json().catch(() => null) : null; if (data && data.ok) { pendingParamConfig = { k: kVal, max_tokens: mtVal }; engineCurrentSlots = data.expert_slots != null ? data.expert_slots : newSlots; renderEngineState('loading', '加载中...'); setEngineNote('引擎重启中,约需 90 秒。'); if (!enginePollTimer) enginePollTimer = setInterval(refreshEngineStatus, 3000); } else { setEngineNote(`重启失败: ${(data && data.error) || `HTTP ${r.status}`}`); } } catch (e) { setEngineNote('重启失败:中台不可达。'); } } else { await postEngineParams({ k: kVal, max_tokens: mtVal }); } engineEls.apply.disabled = engineEls.slots.disabled; }); updateSlotsMem(); refreshEngineStatus(); overlay.querySelector('.dialog-close').addEventListener('click', () => close(false)); overlay.querySelector('.btn-cancel').addEventListener('click', () => close(false)); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(false); }); overlay.querySelector('.btn-save').addEventListener('click', async () => { const defaultSelect = overlay.querySelector('#cfg-default-source'); if (defaultSelect) { saveDefaultSource(defaultSelect.value); } for (const src of ['ollama', 'omlx', 'sparkle', 'anthropic']) { const sel = overlay.querySelector(`#cfg-threshold-${src}`); if (sel) { saveContextThreshold(src, parseInt(sel.value, 10)); } } const newOllama = { baseUrl: val('cfg-ollama-url'), model: val('cfg-ollama-model'), }; const newOmlx = { baseUrl: val('cfg-omlx-url'), apiKey: val('cfg-omlx-key'), model: val('cfg-omlx-model'), }; const newSparkle = { baseUrl: val('cfg-sparkle-url'), apiKey: val('cfg-sparkle-key'), model: val('cfg-sparkle-model'), }; const tierChecked = overlay.querySelector('input[name="tier"]:checked'); const protocolSelect = overlay.querySelector('#cfg-anthropic-protocol-select'); const presetSelect = overlay.querySelector('#cfg-anthropic-preset-select'); const protocolChecked = protocolSelect ? { value: protocolSelect.value } : null; const presetChecked = presetSelect ? { value: presetSelect.value } : null; const newAnthropic = { baseUrl: val('cfg-anthropic-url'), authToken: val('cfg-anthropic-token'), protocol: protocolChecked ? protocolChecked.value : 'anthropic', preset: presetChecked ? presetChecked.value : 'custom', fastModel: val('cfg-anthropic-fast'), standardModel: val('cfg-anthropic-standard'), proModel: val('cfg-anthropic-pro'), currentTier: tierChecked ? tierChecked.value : 'fast', apiVersion: val('cfg-anthropic-version') || '2023-06-01', maxTokens: parseInt(val('cfg-anthropic-maxtokens'), 10) || 4096, presets: (() => { const saved = { ...(anthropic.presets || {}) }; saved[presetChecked?.value || 'custom'] = { authToken: val('cfg-anthropic-token'), fastModel: val('cfg-anthropic-fast'), standardModel: val('cfg-anthropic-standard'), proModel: val('cfg-anthropic-pro'), currentTier: tierChecked ? tierChecked.value : 'fast', }; return saved; })(), }; if (ollama.model && newOllama.model && ollama.model !== newOllama.model) { const baseUrl = (ollama.baseUrl || '').replace(/\/+$/, ''); if (baseUrl) { try { await fetch(`${baseUrl}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: ollama.model, keep_alive: 0 }), }); console.log(`[config] 已卸载 Ollama 旧模型: ${ollama.model}`); } catch (e) {} } } if (omlx.model && newOmlx.model && omlx.model !== newOmlx.model) { const baseUrl = (newOmlx.baseUrl || omlx.baseUrl || '').replace(/\/+$/, ''); const apiKey = newOmlx.apiKey || omlx.apiKey || ''; if (baseUrl && apiKey) { try { const r = await fetch(`${baseUrl}/v1/models/${encodeURIComponent(omlx.model)}/unload`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}` }, }); if (r.ok) { console.log(`[config] 已卸载 oMLX 旧模型: ${omlx.model}`); } } catch (e) {} } } saveOllamaConfig(newOllama); saveOmlxConfig(newOmlx); saveSparkleConfig(newSparkle); saveAnthropicConfig(newAnthropic); close(true); }); function val(id) { const el = document.getElementById(id); return el ? el.value.trim() : ''; } }); } function escape(s) { if (s == null) return ''; return String(s).replace(/"/g, '"').replace(//g, '>'); }