// ===== Admin State ===== let adminState = { gpus: [], versions: [], params: [], models: [], currentVersionId: null, settings: {}, }; // ===== Init ===== async function adminInit() { // Check login status const res = await fetch('/api/admin/check'); const data = await res.json(); if (data.logged_in) { showAdminContent(); } } function showAdminContent() { document.getElementById('login-screen').classList.add('hidden'); document.getElementById('admin-content').classList.remove('hidden'); loadAdminGpus(); loadAdminVersions(); loadAdminModels(); 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(); const sel = document.getElementById('admin-param-version'); sel.innerHTML = adminState.versions.map(v => ``).join(''); if (adminState.versions.length > 0) { adminState.currentVersionId = adminState.versions[0].id; sel.value = adminState.currentVersionId; await loadAdminParams(); } } 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(); } // ===== Parameter Management ===== async function loadAdminParams() { const sel = document.getElementById('admin-param-version'); adminState.currentVersionId = parseInt(sel.value); if (!adminState.currentVersionId) return; const res = await fetch(`/api/admin/versions/${adminState.currentVersionId}/params`); 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; return ` ${p.id} `; }).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 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, 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'; 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 }; 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 }; 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(); } function renderModelTable() { document.getElementById('model-table-body').innerHTML = adminState.models.map(m => ` ${m.id} `).join(''); } async function addModel() { const data = { base_model: document.getElementById('model-basemodel').value, name: document.getElementById('model-name').value, size_gb: parseFloat(document.getElementById('model-size').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, quant: document.getElementById('model-quant').value, description: document.getElementById('model-desc').value, sort_order: parseInt(document.getElementById('model-order').value) || 0, }; if (!data.base_model || !data.name || !data.size_gb || !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-size','model-layers','model-embd','model-kv','model-hdim','model-heads','model-ctx','model-quant','model-desc','model-order'].forEach(id => document.getElementById(id).value = id === 'model-order' ? '0' : ''); 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, size_gb: m.size_gb, 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, quant: m.quant, description: m.description, sort_order: m.sort_order }; if (['size_gb'].includes(field)) data[field] = parseFloat(value) || 0; else if (['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(); } // ===== 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 系统提示词', }; // Keys that use multi-line textarea const textareaKeys = ['nl_default_text', 'llm_system_prompt']; // Keys that use select with live data const gpuSelectKeys = ['default_gpu']; const versionSelectKeys = ['default_version']; const modeSelectKeys = ['default_mode']; // Keys that are boolean-like const boolKeys = ['llm_enabled', 'show_nl_section']; document.getElementById('settings-form').innerHTML = Object.entries(adminState.settings).map(([key, value]) => { const label = labels[key] || key; // Textarea if (textareaKeys.includes(key)) { return `
`; } // Select with GPU list if (gpuSelectKeys.includes(key)) { const opts = adminState.gpus.map(g => ``).join(''); return `
`; } // Select with version list if (versionSelectKeys.includes(key)) { const opts = adminState.versions.map(v => ``).join(''); return `
`; } // Select with mode if (modeSelectKeys.includes(key)) { return `
`; } // Boolean if (boolKeys.includes(key)) { return `
`; } // Password or text 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);