- 支持多版本 llama.cpp 参数 (b6310, b10068) - GPU 模式 / GPU+CPU 模式 - 多 GPU 支持 (最多4张) - 实时显存/内存估算 - 自然语言解析生成命令 - 参数分级显示 (重要/隐藏) - 仅输出非默认值参数 - 后台管理 (GPU/版本/参数 CRUD) - 模型预设 (7B-70B)
395 lines
16 KiB
JavaScript
395 lines
16 KiB
JavaScript
// ===== Admin State =====
|
|
let adminState = {
|
|
gpus: [],
|
|
versions: [],
|
|
params: [],
|
|
currentVersionId: null,
|
|
settings: {},
|
|
};
|
|
|
|
// ===== Init =====
|
|
async function adminInit() {
|
|
await loadAdminGpus();
|
|
await loadAdminVersions();
|
|
await loadAdminSettings();
|
|
}
|
|
|
|
// ===== 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');
|
|
adminState.gpus = await res.json();
|
|
renderGpuTable();
|
|
}
|
|
|
|
function renderGpuTable() {
|
|
const tbody = document.getElementById('gpu-table-body');
|
|
tbody.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)
|
|
});
|
|
// Clear inputs
|
|
document.getElementById('gpu-name').value = '';
|
|
document.getElementById('gpu-vram').value = '';
|
|
document.getElementById('gpu-cc').value = '';
|
|
document.getElementById('gpu-desc').value = '';
|
|
document.getElementById('gpu-order').value = '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,
|
|
};
|
|
if (field === 'vram_mb' || field === 'sort_order') {
|
|
data[field] = parseInt(value) || 0;
|
|
} else {
|
|
data[field] = 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');
|
|
adminState.versions = await res.json();
|
|
renderVersionTable();
|
|
// Populate version select in params tab
|
|
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() {
|
|
const tbody = document.getElementById('version-table-body');
|
|
tbody.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)
|
|
});
|
|
document.getElementById('ver-tag').value = '';
|
|
document.getElementById('ver-desc').value = '';
|
|
document.getElementById('ver-date').value = '';
|
|
document.getElementById('ver-order').value = '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,
|
|
};
|
|
if (field === 'is_active' || field === 'sort_order') {
|
|
data[field] = parseInt(value);
|
|
} else {
|
|
data[field] = 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`);
|
|
adminState.params = await res.json();
|
|
renderParamTable();
|
|
}
|
|
|
|
function renderParamTable() {
|
|
const tbody = document.getElementById('param-table-body');
|
|
tbody.innerHTML = adminState.params.map(p => {
|
|
const options = Array.isArray(p.options) ? p.options.join(', ') : (p.options || '');
|
|
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)
|
|
});
|
|
|
|
// Clear inputs
|
|
['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,
|
|
};
|
|
|
|
if (field === 'is_important' || field === 'affects_vram' || field === 'sort_order') {
|
|
data[field] = parseInt(value);
|
|
} else {
|
|
data[field] = 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;
|
|
// Parse "short/long" format
|
|
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();
|
|
}
|
|
|
|
// ===== Settings =====
|
|
async function loadAdminSettings() {
|
|
const res = await fetch('/api/admin/settings');
|
|
adminState.settings = await res.json();
|
|
renderSettings();
|
|
}
|
|
|
|
function renderSettings() {
|
|
const container = document.getElementById('settings-form');
|
|
container.innerHTML = Object.entries(adminState.settings).map(([key, value]) => `
|
|
<div class="setting-item">
|
|
<label>${key}</label>
|
|
<input type="text" 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);
|