1. 自然语言默认提示文本使用多行textarea 2. 默认GPU/版本/模式使用实时下拉列表选择(非手动输入) 3. 新增 show_nl_section 设置控制前端是否显示自然语言区域 4. LLM系统提示词也使用textarea 5. 布尔类设置项使用是/否下拉选择
364 lines
22 KiB
JavaScript
364 lines
22 KiB
JavaScript
// ===== 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 => `
|
|
<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>
|
|
</tr>`).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 => `<option value="${v.id}">${v.version_tag}</option>`).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 => `
|
|
<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>
|
|
<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>
|
|
<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>
|
|
</tr>`).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 `
|
|
<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><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>`;
|
|
}).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 => `
|
|
<tr>
|
|
<td>${m.id}</td>
|
|
<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:200px"></td>
|
|
<td><input type="number" value="${m.size_gb}" step="0.1" onchange="updateModel(${m.id}, 'size_gb', this.value)" style="width:60px"></td>
|
|
<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="text" value="${m.quant || ''}" onchange="updateModel(${m.id}, 'quant', 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>
|
|
<td><button class="btn-action btn-delete" onclick="deleteModel(${m.id})">删除</button></td>
|
|
</tr>`).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 `<div class="setting-item setting-item-wide"><label>${label}</label><textarea rows="4" onchange="adminState.settings['${key}'] = this.value">${value}</textarea></div>`;
|
|
}
|
|
// Select with GPU list
|
|
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>`;
|
|
}
|
|
// Select with version list
|
|
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>`;
|
|
}
|
|
// Select with mode
|
|
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>`;
|
|
}
|
|
// Boolean
|
|
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>`;
|
|
}
|
|
// Password or text
|
|
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('');
|
|
}
|
|
|
|
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);
|