// ===== Admin State ===== let adminState = { gpus: [], versions: [], params: [], models: [], binaries: [], quants: [], currentVersionId: null, currentBinaryId: null, currentModelId: null, settings: {}, }; // ===== Init ===== async function adminInit() { const res = await fetch('/api/admin/check'); const data = await res.json(); if (data.logged_in) { showAdminContent(); } } async function showAdminContent() { document.getElementById('login-screen').classList.add('hidden'); document.getElementById('admin-content').classList.remove('hidden'); // IMPORTANT: Load GPUs first, then settings (settings dropdown depends on GPU list) await loadAdminGpus(); await loadAdminVersions(); await loadAdminModels(); await loadAdminSettings(); } // ===== Login ===== async function doLogin() { const password = document.getElementById('login-password').value; const res = await fetch('/api/admin/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }) }); if (res.ok) { showAdminContent(); } else { document.getElementById('login-error').textContent = '密码错误,请重试'; } } async function doLogout() { await fetch('/api/admin/logout', { method: 'POST' }); document.getElementById('login-screen').classList.remove('hidden'); document.getElementById('admin-content').classList.add('hidden'); document.getElementById('login-password').value = ''; document.getElementById('login-error').textContent = ''; } // ===== Tab Switching ===== function adminSwitchTab(tab) { document.querySelectorAll('.admin-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab)); document.querySelectorAll('.admin-section').forEach(s => s.classList.toggle('active', s.id === 'admin-' + tab)); } // ===== GPU Management ===== async function loadAdminGpus() { const res = await fetch('/api/admin/gpus'); if (!res.ok) return; adminState.gpus = await res.json(); renderGpuTable(); } function renderGpuTable() { document.getElementById('gpu-table-body').innerHTML = adminState.gpus.map(g => ` ${g.id} `).join(''); } async function addGpu() { const data = { name: document.getElementById('gpu-name').value, vram_mb: parseInt(document.getElementById('gpu-vram').value) || 0, compute_capability: document.getElementById('gpu-cc').value, description: document.getElementById('gpu-desc').value, sort_order: parseInt(document.getElementById('gpu-order').value) || 0, }; if (!data.name || !data.vram_mb) { alert('请填写GPU名称和显存大小'); return; } await fetch('/api/admin/gpus', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); ['gpu-name','gpu-vram','gpu-cc','gpu-desc','gpu-order'].forEach(id => document.getElementById(id).value = id === 'gpu-order' ? '0' : ''); await loadAdminGpus(); } async function updateGpu(id, field, value) { const gpu = adminState.gpus.find(g => g.id === id); if (!gpu) return; const data = { name: gpu.name, vram_mb: gpu.vram_mb, compute_capability: gpu.compute_capability, description: gpu.description, sort_order: gpu.sort_order }; data[field] = (field === 'vram_mb' || field === 'sort_order') ? (parseInt(value) || 0) : value; await fetch(`/api/admin/gpus/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); await loadAdminGpus(); } async function deleteGpu(id) { if (!confirm('确定删除这个GPU吗?')) return; await fetch(`/api/admin/gpus/${id}`, { method: 'DELETE' }); await loadAdminGpus(); } // ===== Version Management ===== async function loadAdminVersions() { const res = await fetch('/api/admin/versions'); if (!res.ok) return; adminState.versions = await res.json(); renderVersionTable(); // Populate version selectors const selParam = document.getElementById('admin-param-version'); const selBinary = document.getElementById('admin-binary-version'); const opts = adminState.versions.map(v => ``).join(''); selParam.innerHTML = opts; selBinary.innerHTML = opts; if (adminState.versions.length > 0) { adminState.currentVersionId = adminState.versions[0].id; selParam.value = adminState.currentVersionId; selBinary.value = adminState.currentVersionId; await loadAdminBinaries(); await onParamVersionChange(); } } function renderVersionTable() { document.getElementById('version-table-body').innerHTML = adminState.versions.map(v => ` ${v.id} `).join(''); } async function addVersion() { const data = { version_tag: document.getElementById('ver-tag').value, description: document.getElementById('ver-desc').value, release_date: document.getElementById('ver-date').value, sort_order: parseInt(document.getElementById('ver-order').value) || 0 }; if (!data.version_tag) { alert('请填写版本标签'); return; } await fetch('/api/admin/versions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); ['ver-tag','ver-desc','ver-date','ver-order'].forEach(id => document.getElementById(id).value = id === 'ver-order' ? '0' : ''); await loadAdminVersions(); } async function updateVersion(id, field, value) { const ver = adminState.versions.find(v => v.id === id); if (!ver) return; const data = { version_tag: ver.version_tag, description: ver.description, release_date: ver.release_date, is_active: ver.is_active, sort_order: ver.sort_order }; data[field] = (field === 'is_active' || field === 'sort_order') ? parseInt(value) : value; await fetch(`/api/admin/versions/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); await loadAdminVersions(); } async function deleteVersion(id) { if (!confirm('删除版本将同时删除该版本的所有参数和执行程序,确定继续吗?')) return; await fetch(`/api/admin/versions/${id}`, { method: 'DELETE' }); await loadAdminVersions(); } // ===== Binary Management ===== async function loadAdminBinaries() { const sel = document.getElementById('admin-binary-version'); adminState.currentVersionId = parseInt(sel.value); if (!adminState.currentVersionId) return; const res = await fetch(`/api/admin/versions/${adminState.currentVersionId}/binaries`); if (!res.ok) return; adminState.binaries = await res.json(); renderBinaryTable(); // Also update the param binary dropdown updateParamBinaryDropdown(); } function renderBinaryTable() { document.getElementById('binary-table-body').innerHTML = adminState.binaries.map(b => ` ${b.id} `).join(''); } function updateParamBinaryDropdown() { // Update the binary selector in param management const sel = document.getElementById('admin-param-binary'); const currentVal = sel.value; sel.innerHTML = '' + adminState.binaries.map(b => ``).join(''); sel.value = currentVal; // Also update the binary bind dropdown in add param form const bindSel = document.getElementById('param-binary-bind'); bindSel.innerHTML = '' + adminState.binaries.map(b => ``).join(''); } async function addBinary() { const data = { name: document.getElementById('binary-name').value, description: document.getElementById('binary-desc').value, sort_order: parseInt(document.getElementById('binary-order').value) || 0, }; if (!data.name) { alert('请填写程序名称'); return; } await fetch(`/api/admin/versions/${adminState.currentVersionId}/binaries`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); ['binary-name','binary-desc','binary-order'].forEach(id => document.getElementById(id).value = id === 'binary-order' ? '0' : ''); await loadAdminBinaries(); } async function updateBinary(id, field, value) { const b = adminState.binaries.find(b => b.id === id); if (!b) return; const data = { name: b.name, description: b.description, sort_order: b.sort_order }; data[field] = (field === 'sort_order') ? (parseInt(value) || 0) : value; await fetch(`/api/admin/binaries/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); await loadAdminBinaries(); } async function deleteBinary(id) { if (!confirm('确定删除这个执行程序吗?')) return; await fetch(`/api/admin/binaries/${id}`, { method: 'DELETE' }); await loadAdminBinaries(); } // ===== Parameter Management ===== async function onParamVersionChange() { const sel = document.getElementById('admin-param-version'); adminState.currentVersionId = parseInt(sel.value); // Load binaries for this version const res = await fetch(`/api/admin/versions/${adminState.currentVersionId}/binaries`); if (res.ok) { adminState.binaries = await res.json(); updateParamBinaryDropdown(); } await loadAdminParams(); } async function loadAdminParams() { if (!adminState.currentVersionId) return; const binarySel = document.getElementById('admin-param-binary'); const binaryId = binarySel.value; let url = `/api/admin/versions/${adminState.currentVersionId}/params`; if (binaryId) url += `?binary_id=${binaryId}`; const res = await fetch(url); if (!res.ok) return; adminState.params = await res.json(); renderParamTable(); } function renderParamTable() { document.getElementById('param-table-body').innerHTML = adminState.params.map(p => { const flag = p.short_flag ? `${p.short_flag}/${p.long_flag}` : p.long_flag; // Find binary name let binaryName = '全部'; if (p.binary_id) { const b = adminState.binaries.find(b => b.id === p.binary_id); binaryName = b ? b.name : '?'; } return ` ${p.id} ${binaryName} `; }).join(''); } async function addParam() { const optionsStr = document.getElementById('param-options').value; const options = optionsStr ? optionsStr.split(',').map(s => s.trim()).filter(s => s) : null; const binaryBind = document.getElementById('param-binary-bind').value; const data = { param_key: document.getElementById('param-key').value, short_flag: document.getElementById('param-short').value, long_flag: document.getElementById('param-long').value, description: document.getElementById('param-desc').value, category: document.getElementById('param-category').value, param_type: document.getElementById('param-type').value, default_value: document.getElementById('param-default').value, options: options, min_value: document.getElementById('param-min').value ? parseFloat(document.getElementById('param-min').value) : null, max_value: document.getElementById('param-max').value ? parseFloat(document.getElementById('param-max').value) : null, step: document.getElementById('param-step').value ? parseFloat(document.getElementById('param-step').value) : null, unit: document.getElementById('param-unit').value, binary_id: binaryBind ? parseInt(binaryBind) : null, is_important: parseInt(document.getElementById('param-important').value) || 0, affects_vram: parseInt(document.getElementById('param-vram').value) || 0, sort_order: parseInt(document.getElementById('param-order').value) || 0, }; if (!data.param_key || !data.long_flag) { alert('请填写参数键和长标志'); return; } await fetch(`/api/admin/versions/${adminState.currentVersionId}/params`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); ['param-key','param-short','param-long','param-desc','param-default','param-options','param-min','param-max','param-step','param-unit'].forEach(id => document.getElementById(id).value = ''); document.getElementById('param-important').value = '0'; document.getElementById('param-vram').value = '0'; document.getElementById('param-order').value = '0'; document.getElementById('param-binary-bind').value = ''; await loadAdminParams(); } async function updateParam(id, field, value) { const p = adminState.params.find(p => p.id === id); if (!p) return; const data = { param_key: p.param_key, short_flag: p.short_flag, long_flag: p.long_flag, description: p.description, category: p.category, param_type: p.param_type, default_value: p.default_value, options: p.options, min_value: p.min_value, max_value: p.max_value, step: p.step, unit: p.unit, is_important: p.is_important, affects_vram: p.affects_vram, sort_order: p.sort_order, binary_id: p.binary_id, }; data[field] = (field === 'is_important' || field === 'affects_vram' || field === 'sort_order') ? parseInt(value) : value; await fetch(`/api/admin/params/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); await loadAdminParams(); } async function updateParamFlag(id, value) { const p = adminState.params.find(p => p.id === id); if (!p) return; const parts = value.split('/').map(s => s.trim()); const data = { param_key: p.param_key, short_flag: parts[0] || '', long_flag: parts[1] || parts[0] || '', description: p.description, category: p.category, param_type: p.param_type, default_value: p.default_value, options: p.options, min_value: p.min_value, max_value: p.max_value, step: p.step, unit: p.unit, is_important: p.is_important, affects_vram: p.affects_vram, sort_order: p.sort_order, binary_id: p.binary_id, }; await fetch(`/api/admin/params/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); await loadAdminParams(); } async function deleteParam(id) { if (!confirm('确定删除这个参数吗?')) return; await fetch(`/api/admin/params/${id}`, { method: 'DELETE' }); await loadAdminParams(); } // ===== Model Management ===== async function loadAdminModels() { const res = await fetch('/api/models'); adminState.models = await res.json(); renderModelTable(); // Populate model selector for quants const sel = document.getElementById('admin-quant-model'); sel.innerHTML = adminState.models.map(m => ``).join(''); if (adminState.models.length > 0) { adminState.currentModelId = adminState.models[0].id; await loadAdminQuants(); } } function renderModelTable() { document.getElementById('model-table-body').innerHTML = adminState.models.map(m => { const typeBadge = m.model_type === 'moe' ? 'MoE' : 'Dense'; return ` ${m.id} `; }).join(''); } async function addModel() { const data = { base_model: document.getElementById('model-basemodel').value, name: document.getElementById('model-name').value || document.getElementById('model-basemodel').value, model_type: document.getElementById('model-type').value, num_experts: parseInt(document.getElementById('model-experts').value) || 0, layers: parseInt(document.getElementById('model-layers').value) || 0, embd: parseInt(document.getElementById('model-embd').value) || 0, kv_heads: parseInt(document.getElementById('model-kv').value) || 0, head_dim: parseInt(document.getElementById('model-hdim').value) || 0, attention_heads: parseInt(document.getElementById('model-heads').value) || 0, default_ctx: parseInt(document.getElementById('model-ctx').value) || 0, description: document.getElementById('model-desc').value, sort_order: parseInt(document.getElementById('model-order').value) || 0, }; if (!data.base_model || !data.layers) { alert('请填写基模型和层数'); return; } await fetch('/api/admin/models', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); ['model-basemodel','model-name','model-layers','model-embd','model-kv','model-hdim','model-heads','model-ctx','model-desc','model-order','model-experts'].forEach(id => document.getElementById(id).value = id === 'model-order' ? '0' : id === 'model-experts' ? '0' : ''); document.getElementById('model-type').value = 'dense'; await loadAdminModels(); } async function updateModel(id, field, value) { const m = adminState.models.find(m => m.id === id); if (!m) return; const data = { base_model: m.base_model, name: m.name, model_type: m.model_type, num_experts: m.num_experts, layers: m.layers, embd: m.embd, kv_heads: m.kv_heads, head_dim: m.head_dim, attention_heads: m.attention_heads, default_ctx: m.default_ctx || 0, description: m.description, sort_order: m.sort_order, }; if (['num_experts','layers','embd','kv_heads','head_dim','attention_heads','default_ctx','sort_order'].includes(field)) data[field] = parseInt(value) || 0; else data[field] = value; await fetch(`/api/admin/models/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); await loadAdminModels(); } async function deleteModel(id) { if (!confirm('删除模型将同时删除其所有量化版本,确定继续吗?')) return; await fetch(`/api/admin/models/${id}`, { method: 'DELETE' }); await loadAdminModels(); } // ===== Quant Management ===== async function loadAdminQuants() { const sel = document.getElementById('admin-quant-model'); adminState.currentModelId = parseInt(sel.value); if (!adminState.currentModelId) return; const res = await fetch(`/api/admin/models/${adminState.currentModelId}/quants`); if (!res.ok) return; adminState.quants = await res.json(); renderQuantTable(); } function renderQuantTable() { document.getElementById('quant-table-body').innerHTML = adminState.quants.map(q => ` ${q.id} `).join(''); } async function addQuant() { const data = { quant_type: document.getElementById('quant-type').value, size_gb: parseFloat(document.getElementById('quant-size').value) || 0, sort_order: parseInt(document.getElementById('quant-order').value) || 0, }; if (!data.quant_type || !data.size_gb) { alert('请填写量化类型和大小'); return; } await fetch(`/api/admin/models/${adminState.currentModelId}/quants`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); ['quant-type','quant-size','quant-order'].forEach(id => document.getElementById(id).value = id === 'quant-order' ? '0' : ''); await loadAdminQuants(); } async function updateQuant(id, field, value) { const q = adminState.quants.find(q => q.id === id); if (!q) return; const data = { quant_type: q.quant_type, size_gb: q.size_gb, sort_order: q.sort_order }; if (field === 'size_gb') data[field] = parseFloat(value) || 0; else if (field === 'sort_order') data[field] = parseInt(value) || 0; else data[field] = value; await fetch(`/api/admin/model_quants/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); await loadAdminQuants(); } async function deleteQuant(id) { if (!confirm('确定删除这个量化版本吗?')) return; await fetch(`/api/admin/model_quants/${id}`, { method: 'DELETE' }); await loadAdminQuants(); } // ===== Settings ===== async function loadAdminSettings() { const res = await fetch('/api/admin/settings'); if (!res.ok) return; adminState.settings = await res.json(); renderSettings(); } function renderSettings() { const labels = { 'admin_password': '管理密码', 'default_gpu': '默认GPU', 'default_version': '默认版本', 'default_mode': '默认模式', 'default_quant': '默认量化版本', 'show_nl_section': '显示自然语言区域 (true/false)', 'nl_default_text': '自然语言默认提示文本', 'llm_enabled': '启用LLM解析 (true/false)', 'llm_api_url': 'LLM API URL', 'llm_api_key': 'LLM API Key', 'llm_api_model': 'LLM 模型名', 'llm_system_prompt': 'LLM 系统提示词', }; const textareaKeys = ['nl_default_text', 'llm_system_prompt']; const gpuSelectKeys = ['default_gpu']; const versionSelectKeys = ['default_version']; const modeSelectKeys = ['default_mode']; const boolKeys = ['llm_enabled', 'show_nl_section']; document.getElementById('settings-form').innerHTML = Object.entries(adminState.settings).map(([key, value]) => { const label = labels[key] || key; if (textareaKeys.includes(key)) { return `
`; } // GPU select with live data from adminState.gpus if (gpuSelectKeys.includes(key)) { const opts = adminState.gpus.map(g => ``).join(''); return `
`; } if (versionSelectKeys.includes(key)) { const opts = adminState.versions.map(v => ``).join(''); return `
`; } if (modeSelectKeys.includes(key)) { return `
`; } if (boolKeys.includes(key)) { return `
`; } const type = key === 'llm_api_key' ? 'password' : 'text'; return `
`; }).join(''); } async function saveSettings() { await fetch('/api/admin/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(adminState.settings) }); alert('设置已保存'); } // ===== Init ===== window.addEventListener('DOMContentLoaded', adminInit);