v2.1.0: 能力路由体系 + 模型管理 + 生图/语音/视频端点 + 大量修复
- 提供商重构: Local Qwen(文本+视觉) / SiliconFlow LLM(文本) / Autodl(文本+视觉) / Autodl Image(生图) - 能力标签: text/vision/audio_out/audio_in/image_gen/video_gen,模型级可配 - AUTO配置绑定能力: auto/auto-text/auto-vision/auto-image/auto-voice-out/auto-voice-in/auto-video - 新端点: /v1/images/generations /v1/audio/speech /v1/audio/transcriptions /v1/video/generations - 模型管理页: 能力切换/增删改/设默认/别名管理 - 修复: 失败切换不再替换用户模型; 熔断冷却后自动恢复; 重试不再白等; embeddings按模型路由 - 修复: 后台模板API路径(/api/admin); 删除v1旧admin目录与双端口混乱 - 修复: run.sh硬编码路径; 死配置RETRY/LOG接线; engine_completions空body防护 - 端口: 16003(16001-16100白名单区间); debug关闭; start.sh部署脚本
This commit is contained in:
+425
-18
@@ -8,6 +8,10 @@
|
||||
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
|
||||
<style>
|
||||
.gradient-bg { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); }
|
||||
.cap-chip { transition: all 0.15s; cursor: pointer; }
|
||||
.cap-chip.on { background: #6366f1; color: #fff; }
|
||||
.cap-chip.off { background: #f3f4f6; color: #6b7280; }
|
||||
.cap-chip.off:hover { background: #e5e7eb; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
@@ -46,17 +50,35 @@
|
||||
</aside>
|
||||
|
||||
<main class="ml-64 flex-1 p-8">
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6">模型管理</h1>
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-800">模型管理</h1>
|
||||
<p class="text-gray-500 text-sm mt-1">为每个模型配置能力标签(点击可切换),AUTO配置按能力自动筛选模型</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<select id="providerFilter" onchange="renderModels()" class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500">
|
||||
<option value="">全部提供商</option>
|
||||
</select>
|
||||
<button onclick="showAddModelModal()" class="px-4 py-2 gradient-bg text-white rounded-lg hover:opacity-90 transition">
|
||||
<i class="ri-add-line mr-1"></i> 添加模型
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 能力图例 -->
|
||||
<div id="capabilityLegend" class="flex flex-wrap gap-2 mb-4"></div>
|
||||
|
||||
<!-- 模型表格 -->
|
||||
<div class="bg-white rounded-xl border border-gray-100 overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50 border-b border-gray-100">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-sm font-medium text-gray-500">模型别名</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-medium text-gray-500">目标模型</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-medium text-gray-500">模型名称</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-medium text-gray-500">提供商</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-medium text-gray-500">优先级</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-medium text-gray-500">说明</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-medium text-gray-500">能力标签</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-medium text-gray-500">默认</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-medium text-gray-500">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="modelTable">
|
||||
@@ -64,34 +86,419 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 模型别名管理 -->
|
||||
<div class="bg-white rounded-xl border border-gray-100 p-6 mt-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-800">模型别名</h2>
|
||||
<button onclick="showAddAliasModal()" class="px-3 py-1.5 bg-indigo-500 text-white rounded-lg text-sm hover:bg-indigo-600">
|
||||
<i class="ri-add-line mr-1"></i> 添加别名
|
||||
</button>
|
||||
</div>
|
||||
<div id="aliasList" class="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||
<p class="text-gray-500 col-span-full">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 添加模型弹窗 -->
|
||||
<div id="addModelModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50 p-4">
|
||||
<div class="bg-white rounded-xl w-full max-w-lg max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<div class="p-6 border-b flex justify-between items-center">
|
||||
<h2 class="text-xl font-bold text-gray-800">添加模型</h2>
|
||||
<button onclick="closeAddModelModal()" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="ri-close-line text-2xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 space-y-4 overflow-auto flex-1">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">所属提供商 *</label>
|
||||
<select id="addModelProvider" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">模型名称 *</label>
|
||||
<input type="text" id="addModelName" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||
placeholder="例如: gpt-4o, deepseek-ai/DeepSeek-V4">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">能力标签</label>
|
||||
<div id="addModelCaps" class="flex flex-wrap gap-2"></div>
|
||||
<p class="text-xs text-gray-500 mt-1">不勾选则默认继承提供商的全部能力</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 border-t bg-gray-50 flex justify-end gap-3">
|
||||
<button onclick="closeAddModelModal()" class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-100">取消</button>
|
||||
<button onclick="saveAddModel()" class="px-4 py-2 gradient-bg text-white rounded-lg hover:opacity-90">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑模型弹窗 -->
|
||||
<div id="editModelModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50 p-4">
|
||||
<div class="bg-white rounded-xl w-full max-w-lg max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<div class="p-6 border-b flex justify-between items-center">
|
||||
<h2 class="text-xl font-bold text-gray-800">编辑模型</h2>
|
||||
<button onclick="closeEditModelModal()" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="ri-close-line text-2xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 space-y-4 overflow-auto flex-1">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">模型名称</label>
|
||||
<input type="text" id="editModelName" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">能力标签(点击切换)</label>
|
||||
<div id="editModelCaps" class="flex flex-wrap gap-2"></div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
所属提供商: <span id="editModelProviderName"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 border-t bg-gray-50 flex justify-end gap-3">
|
||||
<button onclick="closeEditModelModal()" class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-100">取消</button>
|
||||
<button onclick="saveEditModel()" class="px-4 py-2 gradient-bg text-white rounded-lg hover:opacity-90">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加别名弹窗 -->
|
||||
<div id="addAliasModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50 p-4">
|
||||
<div class="bg-white rounded-xl w-full max-w-md">
|
||||
<div class="p-6 border-b flex justify-between items-center">
|
||||
<h2 class="text-xl font-bold text-gray-800">添加模型别名</h2>
|
||||
<button onclick="closeAddAliasModal()" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="ri-close-line text-2xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">别名</label>
|
||||
<input type="text" id="aliasKey" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||
placeholder="例如: qwen, deepseek, glm">
|
||||
<p class="text-xs text-gray-500 mt-1">调用时使用 model="此别名"(auto- 前缀保留给Auto配置)</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">目标模型</label>
|
||||
<input type="text" id="aliasTarget" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||
placeholder="例如: unsloth/Qwen3.8-27B-Q6_K">
|
||||
<p class="text-xs text-gray-500 mt-1">可为具体模型名,也可为 auto 或 auto-xxx 配置</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 border-t bg-gray-50 flex justify-end gap-3">
|
||||
<button onclick="closeAddAliasModal()" class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-100">取消</button>
|
||||
<button onclick="saveAlias()" class="px-4 py-2 gradient-bg text-white rounded-lg hover:opacity-90">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function loadModels() {
|
||||
const res = await fetch('/api/admin/models');
|
||||
const models = await res.json();
|
||||
|
||||
let capabilityDefs = {};
|
||||
let allModels = [];
|
||||
let allProviders = [];
|
||||
let editingModel = null;
|
||||
|
||||
// 初始化
|
||||
async function init() {
|
||||
const [capsRes, modelsRes, providersRes] = await Promise.all([
|
||||
fetch('/api/admin/capabilities'),
|
||||
fetch('/api/admin/models'),
|
||||
fetch('/api/admin/providers')
|
||||
]);
|
||||
capabilityDefs = await capsRes.json();
|
||||
allModels = await modelsRes.json();
|
||||
allProviders = await providersRes.json();
|
||||
|
||||
renderLegend();
|
||||
renderProviderFilter();
|
||||
renderModels();
|
||||
loadAliases();
|
||||
}
|
||||
|
||||
function renderLegend() {
|
||||
document.getElementById('capabilityLegend').innerHTML = `
|
||||
<span class="text-sm text-gray-500 mr-1">能力说明:</span>
|
||||
${Object.entries(capabilityDefs).map(([key, label]) => `
|
||||
<span class="px-2 py-1 bg-indigo-50 text-indigo-700 rounded text-xs font-medium">
|
||||
${label} <code class="text-indigo-400">${key}</code>
|
||||
</span>
|
||||
`).join('')}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderProviderFilter() {
|
||||
const sel = document.getElementById('providerFilter');
|
||||
sel.innerHTML = '<option value="">全部提供商</option>' +
|
||||
allProviders.map(p => `<option value="${p.id}">${p.name}</option>`).join('');
|
||||
}
|
||||
|
||||
function renderModels() {
|
||||
const filter = document.getElementById('providerFilter').value;
|
||||
const tbody = document.getElementById('modelTable');
|
||||
|
||||
|
||||
const models = filter ? allModels.filter(m => m.provider_id === filter) : allModels;
|
||||
|
||||
if (models.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="5" class="px-6 py-8 text-center text-gray-500">暂无模型</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = models.map(m => `
|
||||
<tr class="border-b border-gray-50 hover:bg-gray-50">
|
||||
<td class="px-6 py-4 font-medium text-gray-800">
|
||||
<code class="px-2 py-1 bg-purple-100 text-purple-700 rounded">${m.alias}</code>
|
||||
<td class="px-6 py-4">
|
||||
<code class="px-2 py-1 bg-purple-100 text-purple-700 rounded text-sm">${m.name}</code>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<code class="text-gray-600">${m.target}</code>
|
||||
<span class="text-gray-600 text-sm">${m.provider_name}</span>
|
||||
${m.provider_enabled ? '' : '<span class="ml-1 text-xs text-red-500">(停用)</span>'}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-gray-600">${m.provider || '-'}</td>
|
||||
<td class="px-6 py-4">
|
||||
${m.priority ? `<span class="px-2 py-1 bg-blue-100 text-blue-600 rounded text-sm">优先级 ${m.priority}</span>` : '-'}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
${Object.keys(capabilityDefs).map(cap => {
|
||||
const on = m.capabilities.includes(cap);
|
||||
return `<span class="cap-chip px-2 py-0.5 rounded text-xs ${on ? 'on' : 'off'}"
|
||||
onclick="toggleCap('${m.provider_id}', '${escapeStr(m.name)}', '${cap}', this)">${capabilityDefs[cap]}</span>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
${m.is_default
|
||||
? '<span class="px-2 py-1 bg-green-100 text-green-700 rounded text-xs">✓ 默认</span>'
|
||||
: `<button onclick="setDefault('${m.provider_id}', '${escapeStr(m.name)}')" class="px-2 py-1 bg-gray-100 text-gray-600 rounded text-xs hover:bg-gray-200">设为默认</button>`}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex gap-2">
|
||||
<button onclick="openEditModel('${m.provider_id}', '${escapeStr(m.name)}')" class="px-2 py-1 bg-blue-500 text-white rounded text-xs hover:bg-blue-600">编辑</button>
|
||||
<button onclick="deleteModel('${m.provider_id}', '${escapeStr(m.name)}')" class="px-2 py-1 bg-red-500 text-white rounded text-xs hover:bg-red-600">删除</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">${m.description || ''}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
// 渲染别名
|
||||
document.querySelectorAll('tr[data-aliases]').forEach(() => {});
|
||||
}
|
||||
|
||||
function escapeStr(s) {
|
||||
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
||||
}
|
||||
|
||||
// 点击切换能力
|
||||
async function toggleCap(providerId, modelName, cap, el) {
|
||||
const model = allModels.find(m => m.provider_id === providerId && m.name === modelName);
|
||||
if (!model) return;
|
||||
|
||||
if (model.capabilities.includes(cap)) {
|
||||
model.capabilities = model.capabilities.filter(c => c !== cap);
|
||||
} else {
|
||||
model.capabilities.push(cap);
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/admin/models/${providerId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: modelName, capabilities: model.capabilities })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
el.classList.toggle('on', model.capabilities.includes(cap));
|
||||
el.classList.toggle('off', !model.capabilities.includes(cap));
|
||||
} else {
|
||||
alert('更新失败: ' + (data.error || ''));
|
||||
init();
|
||||
}
|
||||
}
|
||||
|
||||
async function setDefault(providerId, modelName) {
|
||||
const res = await fetch(`/api/admin/models/${providerId}/default`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: modelName })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
init();
|
||||
} else {
|
||||
alert('设置失败: ' + (data.error || ''));
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteModel(providerId, modelName) {
|
||||
if (!confirm(`确定删除模型 "${modelName}" 吗?`)) return;
|
||||
const res = await fetch(`/api/admin/models/${providerId}?name=${encodeURIComponent(modelName)}`, { method: 'DELETE' });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
init();
|
||||
} else {
|
||||
alert('删除失败: ' + (data.error || ''));
|
||||
}
|
||||
}
|
||||
|
||||
// 添加模型
|
||||
function showAddModelModal() {
|
||||
const sel = document.getElementById('addModelProvider');
|
||||
sel.innerHTML = allProviders.map(p => `<option value="${p.id}">${p.name}</option>`).join('');
|
||||
document.getElementById('addModelName').value = '';
|
||||
renderCapChips('addModelCaps', []);
|
||||
document.getElementById('addModelModal').classList.remove('hidden');
|
||||
document.getElementById('addModelModal').classList.add('flex');
|
||||
}
|
||||
|
||||
function closeAddModelModal() {
|
||||
document.getElementById('addModelModal').classList.add('hidden');
|
||||
document.getElementById('addModelModal').classList.remove('flex');
|
||||
}
|
||||
|
||||
async function saveAddModel() {
|
||||
const providerId = document.getElementById('addModelProvider').value;
|
||||
const name = document.getElementById('addModelName').value.trim();
|
||||
if (!name) { alert('请输入模型名称'); return; }
|
||||
|
||||
const caps = [];
|
||||
document.querySelectorAll('#addModelCaps .cap-chip.on').forEach(c => caps.push(c.dataset.cap));
|
||||
if (caps.length === 0) caps.push('text');
|
||||
|
||||
const res = await fetch('/api/admin/models', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ provider_id: providerId, name, capabilities: caps })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
closeAddModelModal();
|
||||
init();
|
||||
alert('✅ 模型已添加');
|
||||
} else {
|
||||
alert('添加失败: ' + (data.error || ''));
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑模型
|
||||
function openEditModel(providerId, modelName) {
|
||||
const model = allModels.find(m => m.provider_id === providerId && m.name === modelName);
|
||||
if (!model) return;
|
||||
editingModel = model;
|
||||
document.getElementById('editModelName').value = model.name;
|
||||
document.getElementById('editModelProviderName').textContent = model.provider_name;
|
||||
renderCapChips('editModelCaps', model.capabilities);
|
||||
document.getElementById('editModelModal').classList.remove('hidden');
|
||||
document.getElementById('editModelModal').classList.add('flex');
|
||||
}
|
||||
|
||||
function closeEditModelModal() {
|
||||
document.getElementById('editModelModal').classList.add('hidden');
|
||||
document.getElementById('editModelModal').classList.remove('flex');
|
||||
editingModel = null;
|
||||
}
|
||||
|
||||
async function saveEditModel() {
|
||||
if (!editingModel) return;
|
||||
const newName = document.getElementById('editModelName').value.trim();
|
||||
if (!newName) { alert('模型名称不能为空'); return; }
|
||||
|
||||
const caps = [];
|
||||
document.querySelectorAll('#editModelCaps .cap-chip.on').forEach(c => caps.push(c.dataset.cap));
|
||||
if (caps.length === 0) caps.push('text');
|
||||
|
||||
const body = { old_name: editingModel.name, capabilities: caps };
|
||||
if (newName !== editingModel.name) body.new_name = newName;
|
||||
|
||||
const res = await fetch(`/api/admin/models/${editingModel.provider_id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
closeEditModelModal();
|
||||
init();
|
||||
alert('✅ 模型已更新');
|
||||
} else {
|
||||
alert('更新失败: ' + (data.error || ''));
|
||||
}
|
||||
}
|
||||
|
||||
// 能力chips渲染(通用)
|
||||
function renderCapChips(containerId, selectedCaps) {
|
||||
const container = document.getElementById(containerId);
|
||||
container.innerHTML = Object.entries(capabilityDefs).map(([key, label]) => `
|
||||
<span class="cap-chip px-3 py-1 rounded-lg text-sm ${selectedCaps.includes(key) ? 'on' : 'off'}"
|
||||
data-cap="${key}" onclick="this.classList.toggle('on'); this.classList.toggle('off')">${label}</span>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
loadModels();
|
||||
// 别名管理
|
||||
async function loadAliases() {
|
||||
const res = await fetch('/api/admin/aliases');
|
||||
const aliases = await res.json();
|
||||
|
||||
const container = document.getElementById('aliasList');
|
||||
const entries = Object.entries(aliases);
|
||||
|
||||
if (entries.length === 0) {
|
||||
container.innerHTML = '<p class="text-gray-500 col-span-full text-sm">暂无别名</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = entries.map(([alias, target]) => `
|
||||
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg group">
|
||||
<div class="min-w-0">
|
||||
<code class="text-purple-600 text-sm">${alias}</code>
|
||||
<span class="text-gray-400 mx-1">→</span>
|
||||
<code class="text-gray-700 text-xs">${target}</code>
|
||||
</div>
|
||||
${alias === 'auto' ? '' : `
|
||||
<button onclick="deleteAlias('${alias}')" class="p-1 text-gray-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition">
|
||||
<i class="ri-delete-bin-line"></i>
|
||||
</button>`}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function showAddAliasModal() {
|
||||
document.getElementById('aliasKey').value = '';
|
||||
document.getElementById('aliasTarget').value = '';
|
||||
document.getElementById('addAliasModal').classList.remove('hidden');
|
||||
document.getElementById('addAliasModal').classList.add('flex');
|
||||
}
|
||||
|
||||
function closeAddAliasModal() {
|
||||
document.getElementById('addAliasModal').classList.add('hidden');
|
||||
document.getElementById('addAliasModal').classList.remove('flex');
|
||||
}
|
||||
|
||||
async function saveAlias() {
|
||||
const alias = document.getElementById('aliasKey').value.trim();
|
||||
const target = document.getElementById('aliasTarget').value.trim();
|
||||
if (!alias || !target) { alert('请填写别名和目标模型'); return; }
|
||||
|
||||
const res = await fetch('/api/admin/aliases', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ alias, target })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
closeAddAliasModal();
|
||||
loadAliases();
|
||||
} else {
|
||||
alert('保存失败: ' + (data.error || ''));
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAlias(alias) {
|
||||
if (!confirm(`确定删除别名 "${alias}" 吗?`)) return;
|
||||
const res = await fetch(`/api/admin/aliases/${alias}`, { method: 'DELETE' });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
loadAliases();
|
||||
} else {
|
||||
alert('删除失败: ' + (data.error || ''));
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
Reference in New Issue
Block a user