Files

530 lines
30 KiB
JavaScript
Raw Permalink Normal View History

2026-07-19 18:08:36 +08:00
// ===== Admin State =====
let adminState = {
gpus: [], versions: [], params: [], models: [], binaries: [], quants: [],
currentVersionId: null, currentBinaryId: null, currentModelId: null, settings: {},
2026-07-19 18:08:36 +08:00
};
// ===== Init =====
async function adminInit() {
2026-07-19 18:49:05 +08:00
const res = await fetch('/api/admin/check');
const data = await res.json();
if (data.logged_in) {
showAdminContent();
}
}
async function showAdminContent() {
2026-07-19 18:49:05 +08:00
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();
2026-07-19 18:49:05 +08:00
}
// ===== 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' },
2026-07-19 18:49:05 +08:00
body: JSON.stringify({ password })
});
if (res.ok) { showAdminContent(); }
else { document.getElementById('login-error').textContent = '密码错误,请重试'; }
2026-07-19 18:49:05 +08:00
}
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 = '';
2026-07-19 18:08:36 +08:00
}
// ===== Tab Switching =====
function adminSwitchTab(tab) {
2026-07-19 18:49:05 +08:00
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));
2026-07-19 18:08:36 +08:00
}
// ===== GPU Management =====
async function loadAdminGpus() {
const res = await fetch('/api/admin/gpus');
2026-07-19 18:49:05 +08:00
if (!res.ok) return;
2026-07-19 18:08:36 +08:00
adminState.gpus = await res.json();
renderGpuTable();
}
function renderGpuTable() {
2026-07-19 18:49:05 +08:00
document.getElementById('gpu-table-body').innerHTML = adminState.gpus.map(g => `
2026-07-19 18:08:36 +08:00
<tr>
<td>${g.id}</td>
<td><input type="text" value="${g.name}" onchange="updateGpu(${g.id}, 'name', this.value)"></td>
<td><input type="number" value="${g.vram_mb}" onchange="updateGpu(${g.id}, 'vram_mb', this.value)"></td>
<td><input type="text" value="${g.compute_capability || ''}" onchange="updateGpu(${g.id}, 'compute_capability', this.value)"></td>
<td><input type="text" value="${g.description || ''}" onchange="updateGpu(${g.id}, 'description', this.value)"></td>
<td><input type="number" value="${g.sort_order}" onchange="updateGpu(${g.id}, 'sort_order', this.value)" style="width:60px"></td>
<td><button class="btn-action btn-delete" onclick="deleteGpu(${g.id})">删除</button></td>
2026-07-19 18:49:05 +08:00
</tr>`).join('');
2026-07-19 18:08:36 +08:00
}
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,
};
2026-07-19 18:49:05 +08:00
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' : '');
2026-07-19 18:08:36 +08:00
await loadAdminGpus();
}
async function updateGpu(id, field, value) {
const gpu = adminState.gpus.find(g => g.id === id);
if (!gpu) return;
2026-07-19 18:49:05 +08:00
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) });
2026-07-19 18:08:36 +08:00
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');
2026-07-19 18:49:05 +08:00
if (!res.ok) return;
2026-07-19 18:08:36 +08:00
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 => `<option value="${v.id}">${v.version_tag}</option>`).join('');
selParam.innerHTML = opts;
selBinary.innerHTML = opts;
2026-07-19 18:08:36 +08:00
if (adminState.versions.length > 0) {
adminState.currentVersionId = adminState.versions[0].id;
selParam.value = adminState.currentVersionId;
selBinary.value = adminState.currentVersionId;
await loadAdminBinaries();
await onParamVersionChange();
2026-07-19 18:08:36 +08:00
}
}
function renderVersionTable() {
2026-07-19 18:49:05 +08:00
document.getElementById('version-table-body').innerHTML = adminState.versions.map(v => `
2026-07-19 18:08:36 +08:00
<tr>
<td>${v.id}</td>
<td><input type="text" value="${v.version_tag}" onchange="updateVersion(${v.id}, 'version_tag', this.value)"></td>
<td><input type="text" value="${v.description || ''}" onchange="updateVersion(${v.id}, 'description', this.value)"></td>
<td><input type="date" value="${v.release_date || ''}" onchange="updateVersion(${v.id}, 'release_date', this.value)"></td>
2026-07-19 18:49:05 +08:00
<td><select onchange="updateVersion(${v.id}, 'is_active', this.value)"><option value="1" ${v.is_active?'selected':''}>是</option><option value="0" ${!v.is_active?'selected':''}>否</option></select></td>
2026-07-19 18:08:36 +08:00
<td><input type="number" value="${v.sort_order}" onchange="updateVersion(${v.id}, 'sort_order', this.value)" style="width:60px"></td>
<td><button class="btn-action btn-delete" onclick="deleteVersion(${v.id})">删除</button></td>
2026-07-19 18:49:05 +08:00
</tr>`).join('');
2026-07-19 18:08:36 +08:00
}
async function addVersion() {
2026-07-19 18:49:05 +08:00
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' : '');
2026-07-19 18:08:36 +08:00
await loadAdminVersions();
}
async function updateVersion(id, field, value) {
const ver = adminState.versions.find(v => v.id === id);
if (!ver) return;
2026-07-19 18:49:05 +08:00
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) });
2026-07-19 18:08:36 +08:00
await loadAdminVersions();
}
async function deleteVersion(id) {
if (!confirm('删除版本将同时删除该版本的所有参数和执行程序,确定继续吗?')) return;
2026-07-19 18:08:36 +08:00
await fetch(`/api/admin/versions/${id}`, { method: 'DELETE' });
await loadAdminVersions();
}
// ===== Binary Management =====
async function loadAdminBinaries() {
const sel = document.getElementById('admin-binary-version');
2026-07-19 18:08:36 +08:00
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 => `
<tr>
<td>${b.id}</td>
<td><input type="text" value="${b.name}" onchange="updateBinary(${b.id}, 'name', this.value)"></td>
<td><input type="text" value="${b.description || ''}" onchange="updateBinary(${b.id}, 'description', this.value)"></td>
<td><input type="number" value="${b.sort_order}" onchange="updateBinary(${b.id}, 'sort_order', this.value)" style="width:60px"></td>
<td><button class="btn-action btn-delete" onclick="deleteBinary(${b.id})">删除</button></td>
</tr>`).join('');
}
function updateParamBinaryDropdown() {
// Update the binary selector in param management
const sel = document.getElementById('admin-param-binary');
const currentVal = sel.value;
sel.innerHTML = '<option value="">全部程序</option>' + adminState.binaries.map(b =>
`<option value="${b.id}">${b.name}</option>`).join('');
sel.value = currentVal;
// Also update the binary bind dropdown in add param form
const bindSel = document.getElementById('param-binary-bind');
bindSel.innerHTML = '<option value="">适用所有程序</option>' + adminState.binaries.map(b =>
`<option value="${b.id}">${b.name}</option>`).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);
2026-07-19 18:49:05 +08:00
if (!res.ok) return;
2026-07-19 18:08:36 +08:00
adminState.params = await res.json();
renderParamTable();
}
function renderParamTable() {
2026-07-19 18:49:05 +08:00
document.getElementById('param-table-body').innerHTML = adminState.params.map(p => {
2026-07-19 18:08:36 +08:00
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 : '?';
}
2026-07-19 18:08:36 +08:00
return `
2026-07-19 18:49:05 +08:00
<tr>
<td>${p.id}</td>
<td><input type="text" value="${p.param_key}" onchange="updateParam(${p.id}, 'param_key', this.value)"></td>
<td><input type="text" value="${flag}" style="width:150px" onchange="updateParamFlag(${p.id}, this.value)"></td>
<td><input type="text" value="${p.description || ''}" onchange="updateParam(${p.id}, 'description', this.value)"></td>
<td><select onchange="updateParam(${p.id}, 'category', this.value)"><option value="common" ${p.category==='common'?'selected':''}>通用</option><option value="sampling" ${p.category==='sampling'?'selected':''}>采样</option><option value="server" ${p.category==='server'?'selected':''}>服务器</option><option value="model_source" ${p.category==='model_source'?'selected':''}>模型来源</option><option value="lora" ${p.category==='lora'?'selected':''}>LoRA</option><option value="logging" ${p.category==='logging'?'selected':''}>日志</option><option value="advanced" ${p.category==='advanced'?'selected':''}>高级</option></select></td>
<td><select onchange="updateParam(${p.id}, 'param_type', this.value)"><option value="string" ${p.param_type==='string'?'selected':''}>字符串</option><option value="number" ${p.param_type==='number'?'selected':''}>数字</option><option value="boolean" ${p.param_type==='boolean'?'selected':''}>布尔</option><option value="select" ${p.param_type==='select'?'selected':''}>选择</option></select></td>
<td><input type="text" value="${p.default_value || ''}" onchange="updateParam(${p.id}, 'default_value', this.value)"></td>
<td><span style="font-size:12px;color:#666">${binaryName}</span></td>
2026-07-19 18:49:05 +08:00
<td><select onchange="updateParam(${p.id}, 'is_important', this.value)"><option value="0" ${!p.is_important?'selected':''}>否</option><option value="1" ${p.is_important?'selected':''}>是</option></select></td>
<td><select onchange="updateParam(${p.id}, 'affects_vram', this.value)"><option value="0" ${!p.affects_vram?'selected':''}>否</option><option value="1" ${p.affects_vram?'selected':''}>是</option></select></td>
<td><button class="btn-action btn-delete" onclick="deleteParam(${p.id})">删除</button></td>
</tr>`;
2026-07-19 18:08:36 +08:00
}).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;
2026-07-19 18:08:36 +08:00
const data = {
2026-07-19 18:49:05 +08:00
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,
2026-07-19 18:08:36 +08:00
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,
2026-07-19 18:49:05 +08:00
affects_vram: parseInt(document.getElementById('param-vram').value) || 0, sort_order: parseInt(document.getElementById('param-order').value) || 0,
2026-07-19 18:08:36 +08:00
};
2026-07-19 18:49:05 +08:00
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 = '';
2026-07-19 18:08:36 +08:00
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,
};
2026-07-19 18:49:05 +08:00
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) });
2026-07-19 18:08:36 +08:00
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,
};
2026-07-19 18:49:05 +08:00
await fetch(`/api/admin/params/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
2026-07-19 18:08:36 +08:00
await loadAdminParams();
}
async function deleteParam(id) {
if (!confirm('确定删除这个参数吗?')) return;
await fetch(`/api/admin/params/${id}`, { method: 'DELETE' });
await loadAdminParams();
}
2026-07-19 18:49:05 +08:00
// ===== 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 => `<option value="${m.id}">${m.base_model}</option>`).join('');
if (adminState.models.length > 0) {
adminState.currentModelId = adminState.models[0].id;
await loadAdminQuants();
}
2026-07-19 18:49:05 +08:00
}
function renderModelTable() {
document.getElementById('model-table-body').innerHTML = adminState.models.map(m => {
const typeBadge = m.model_type === 'moe'
? '<span style="color:#e67e22;font-weight:bold">MoE</span>'
: '<span style="color:#3498db">Dense</span>';
return `
2026-07-19 18:49:05 +08:00
<tr>
<td>${m.id}</td>
2026-07-19 22:57:38 +08:00
<td><input type="text" value="${m.base_model}" onchange="updateModel(${m.id}, 'base_model', this.value)" style="width:140px"></td>
<td><input type="text" value="${m.name}" onchange="updateModel(${m.id}, 'name', this.value)" style="width:160px"></td>
<td><select onchange="updateModel(${m.id}, 'model_type', this.value)"><option value="dense" ${m.model_type==='dense'?'selected':''}>Dense</option><option value="moe" ${m.model_type==='moe'?'selected':''}>MoE</option></select></td>
<td><input type="number" value="${m.num_experts || 0}" onchange="updateModel(${m.id}, 'num_experts', this.value)" style="width:50px"></td>
2026-07-20 00:05:18 +08:00
<td><input type="number" value="${m.layers}" onchange="updateModel(${m.id}, 'layers', this.value)" style="width:50px"></td>
<td><input type="number" value="${m.embd}" onchange="updateModel(${m.id}, 'embd', this.value)" style="width:60px"></td>
<td><input type="number" value="${m.kv_heads}" onchange="updateModel(${m.id}, 'kv_heads', this.value)" style="width:50px"></td>
<td><input type="number" value="${m.head_dim}" onchange="updateModel(${m.id}, 'head_dim', this.value)" style="width:50px"></td>
<td><input type="number" value="${m.attention_heads}" onchange="updateModel(${m.id}, 'attention_heads', this.value)" style="width:50px"></td>
<td><input type="number" value="${m.default_ctx || 0}" onchange="updateModel(${m.id}, 'default_ctx', this.value)" style="width:70px"></td>
<td><input type="number" value="${m.sort_order}" onchange="updateModel(${m.id}, 'sort_order', this.value)" style="width:40px"></td>
2026-07-19 18:49:05 +08:00
<td><button class="btn-action btn-delete" onclick="deleteModel(${m.id})">删除</button></td>
</tr>`;
}).join('');
2026-07-19 18:49:05 +08:00
}
async function addModel() {
const data = {
2026-07-19 22:57:38 +08:00
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,
2026-07-19 18:49:05 +08:00
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,
2026-07-20 00:05:18 +08:00
default_ctx: parseInt(document.getElementById('model-ctx').value) || 0,
2026-07-19 18:49:05 +08:00
description: document.getElementById('model-desc').value,
sort_order: parseInt(document.getElementById('model-order').value) || 0,
};
if (!data.base_model || !data.layers) { alert('请填写基模型和层数'); return; }
2026-07-19 18:49:05 +08:00
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';
2026-07-19 18:49:05 +08:00
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;
2026-07-19 18:49:05 +08:00
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;
2026-07-19 18:49:05 +08:00
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 => `
<tr>
<td>${q.id}</td>
<td><input type="text" value="${q.quant_type}" onchange="updateQuant(${q.id}, 'quant_type', this.value)"></td>
<td><input type="number" value="${q.size_gb}" step="0.1" onchange="updateQuant(${q.id}, 'size_gb', this.value)" style="width:80px"></td>
<td><input type="number" value="${q.sort_order}" onchange="updateQuant(${q.id}, 'sort_order', this.value)" style="width:50px"></td>
<td><button class="btn-action btn-delete" onclick="deleteQuant(${q.id})">删除</button></td>
</tr>`).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();
}
2026-07-19 18:08:36 +08:00
// ===== Settings =====
async function loadAdminSettings() {
const res = await fetch('/api/admin/settings');
2026-07-19 18:49:05 +08:00
if (!res.ok) return;
2026-07-19 18:08:36 +08:00
adminState.settings = await res.json();
renderSettings();
}
function renderSettings() {
2026-07-19 22:57:38 +08:00
const labels = {
'admin_password': '管理密码',
'default_gpu': '默认GPU',
'default_version': '默认版本',
'default_mode': '默认模式',
'default_quant': '默认量化版本',
2026-07-20 00:41:01 +08:00
'show_nl_section': '显示自然语言区域 (true/false)',
2026-07-20 00:05:18 +08:00
'nl_default_text': '自然语言默认提示文本',
2026-07-19 22:57:38 +08:00
'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 系统提示词',
};
2026-07-20 00:41:01 +08:00
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'];
2026-07-19 22:57:38 +08:00
document.getElementById('settings-form').innerHTML = Object.entries(adminState.settings).map(([key, value]) => {
const label = labels[key] || key;
2026-07-20 00:41:01 +08:00
if (textareaKeys.includes(key)) {
2026-07-19 22:57:38 +08:00
return `<div class="setting-item setting-item-wide"><label>${label}</label><textarea rows="4" onchange="adminState.settings['${key}'] = this.value">${value}</textarea></div>`;
}
// GPU select with live data from adminState.gpus
2026-07-20 00:41:01 +08:00
if (gpuSelectKeys.includes(key)) {
const opts = adminState.gpus.map(g => `<option value="${g.name}" ${value===g.name?'selected':''}>${g.name}</option>`).join('');
return `<div class="setting-item"><label>${label}</label><select onchange="adminState.settings['${key}'] = this.value">${opts}</select></div>`;
}
if (versionSelectKeys.includes(key)) {
const opts = adminState.versions.map(v => `<option value="${v.version_tag}" ${value===v.version_tag?'selected':''}>${v.version_tag}</option>`).join('');
return `<div class="setting-item"><label>${label}</label><select onchange="adminState.settings['${key}'] = this.value">${opts}</select></div>`;
}
if (modeSelectKeys.includes(key)) {
return `<div class="setting-item"><label>${label}</label><select onchange="adminState.settings['${key}'] = this.value"><option value="gpu" ${value==='gpu'?'selected':''}>GPU</option><option value="gpu_cpu" ${value==='gpu_cpu'?'selected':''}>GPU+CPU</option></select></div>`;
}
if (boolKeys.includes(key)) {
return `<div class="setting-item"><label>${label}</label><select onchange="adminState.settings['${key}'] = this.value"><option value="true" ${value==='true'?'selected':''}>是</option><option value="false" ${value==='false'?'selected':''}>否</option></select></div>`;
}
2026-07-19 22:57:38 +08:00
const type = key === 'llm_api_key' ? 'password' : 'text';
return `<div class="setting-item"><label>${label}</label><input type="${type}" value="${value}" onchange="adminState.settings['${key}'] = this.value"></div>`;
}).join('');
2026-07-19 18:08:36 +08:00
}
async function saveSettings() {
2026-07-19 18:49:05 +08:00
await fetch('/api/admin/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(adminState.settings) });
2026-07-19 18:08:36 +08:00
alert('设置已保存');
}
// ===== Init =====
window.addEventListener('DOMContentLoaded', adminInit);