Files
llm-proxy/admin/templates/auto-profiles.html
hubian 8086d76e93 feat: 添加浏览器标签图标 favicon
- 创建 SVG 格式 favicon(深色背景+大脑/网络节点设计)
- 在所有后台管理页面添加 favicon
2026-04-11 11:31:17 +08:00

366 lines
19 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Auto配置管理 - LLM Proxy</title>
<link rel="icon" href="/static/img/favicon.svg" type="image/svg+xml">
<script src="https://cdn.tailwindcss.com"></script>
<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%); }
.dragging { opacity: 0.5; transform: scale(1.02); }
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="flex">
<aside class="w-64 bg-slate-800 min-h-screen fixed left-0 top-0">
<div class="p-6">
<h1 class="text-white text-xl font-bold flex items-center gap-2">
<i class="ri-route-line text-2xl text-purple-400"></i>
LLM Proxy
</h1>
<p class="text-slate-400 text-sm mt-1">后台管理</p>
</div>
<nav class="mt-6">
<a href="/" class="flex items-center gap-3 px-6 py-3 text-slate-300 hover:bg-slate-700 hover:text-white">
<i class="ri-dashboard-line"></i><span>仪表盘</span>
</a>
<a href="/providers" class="flex items-center gap-3 px-6 py-3 text-slate-300 hover:bg-slate-700 hover:text-white">
<i class="ri-server-line"></i><span>提供商管理</span>
</a>
<a href="/models" class="flex items-center gap-3 px-6 py-3 text-slate-300 hover:bg-slate-700 hover:text-white">
<i class="ri-cpu-line"></i><span>模型管理</span>
</a>
<a href="/auto-profiles" class="flex items-center gap-3 px-6 py-3 bg-slate-700 text-white">
<i class="ri-shuffle-line"></i><span>Auto配置</span>
</a>
<a href="/chat" class="flex items-center gap-3 px-6 py-3 text-slate-300 hover:bg-slate-700 hover:text-white">
<i class="ri-chat-3-line"></i><span>对话</span>
</a>
<a href="/logs" class="flex items-center gap-3 px-6 py-3 text-slate-300 hover:bg-slate-700 hover:text-white">
<i class="ri-file-list-line"></i><span>日志查看</span>
</a>
<a href="/config" class="flex items-center gap-3 px-6 py-3 text-slate-300 hover:bg-slate-700 hover:text-white">
<i class="ri-settings-3-line"></i><span>系统配置</span>
</a>
</nav>
</aside>
<main class="ml-64 flex-1 p-8">
<!-- 顶部操作栏 -->
<div class="flex justify-between items-center mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-800">Auto配置管理</h1>
<p class="text-gray-500 text-sm mt-1">创建自定义的自动选择模式,指定候选提供商和优先级</p>
</div>
<button onclick="showCreateModal()" class="px-4 py-2 gradient-bg text-white rounded-lg hover:opacity-90 transition">
<i class="ri-add-line mr-1"></i> 创建Auto配置
</button>
</div>
<!-- 说明卡片 -->
<div class="mb-6 p-4 bg-indigo-50 rounded-lg text-sm text-indigo-600">
<i class="ri-information-line mr-1"></i>
<strong>Auto配置</strong>创建自定义的模型自动选择模式。例如 <code>model="auto-fast"</code> 只选择响应快的提供商,<code>model="auto-cheap"</code> 只选择便宜的提供商。
</div>
<!-- 配置列表 -->
<div id="profilesList" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<p class="text-gray-500 col-span-full text-center py-8">加载中...</p>
</div>
</main>
</div>
<!-- 创建/编辑弹窗 -->
<div id="editModal" 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-2xl 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" id="modalTitle">创建Auto配置</h2>
<button onclick="closeModal()" class="text-gray-400 hover:text-gray-600">
<i class="ri-close-line text-2xl"></i>
</button>
</div>
<div class="p-6 overflow-y-auto flex-1">
<form id="profileForm">
<input type="hidden" id="profileId">
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">配置名称</label>
<input type="text" id="profileName" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
placeholder="例如: auto-fast, auto-cheap">
<p class="text-xs text-gray-500 mt-1">调用时使用 model="此名称"</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">显示名称</label>
<input type="text" id="displayName" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
placeholder="例如: 快速模式">
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">描述</label>
<textarea id="profileDesc" rows="2" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
placeholder="描述这个配置的用途"></textarea>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">选择策略</label>
<select id="profileStrategy" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
<option value="priority">按优先级选择(推荐)</option>
<option value="random">随机选择</option>
</select>
<p class="text-xs text-gray-500 mt-1">按优先级会选择列表中第一个可用的提供商</p>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">候选提供商(可拖拽调整优先级)</label>
<div id="providerList" class="border border-gray-200 rounded-lg p-3 space-y-2 min-h-[100px] bg-gray-50">
<!-- 提供商列表 -->
</div>
<p class="text-xs text-gray-500 mt-1">勾选参与自动选择的提供商,拖拽调整优先级</p>
</div>
</form>
</div>
<div class="p-6 border-t bg-gray-50 flex justify-end gap-3">
<button onclick="closeModal()" class="px-4 py-2 text-gray-600 hover:text-gray-800 transition">取消</button>
<button onclick="saveProfile()" class="px-4 py-2 gradient-bg text-white rounded-lg hover:opacity-90 transition">
<i class="ri-save-line mr-1"></i> 保存
</button>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<script>
let providers = [];
let profiles = [];
// 加载数据
async function loadData() {
const [providersRes, profilesRes] = await Promise.all([
fetch('/api/providers'),
fetch('/api/auto-profiles')
]);
providers = await providersRes.json();
profiles = await profilesRes.json();
renderProfiles();
}
// 渲染配置列表
function renderProfiles() {
const container = document.getElementById('profilesList');
if (profiles.length === 0) {
container.innerHTML = `
<div class="col-span-full text-center py-12">
<i class="ri-inbox-line text-4xl text-gray-300"></i>
<p class="text-gray-500 mt-2">暂无Auto配置</p>
<button onclick="showCreateModal()" class="mt-4 px-4 py-2 gradient-bg text-white rounded-lg">
<i class="ri-add-line mr-1"></i> 创建第一个配置
</button>
</div>
`;
return;
}
container.innerHTML = profiles.map(profile => `
<div class="bg-white rounded-xl border border-gray-200 hover:shadow-md transition">
<div class="p-4 border-b flex justify-between items-center">
<div>
<code class="text-indigo-600 font-semibold">${profile.name}</code>
${profile.name === 'auto' ? '<span class="ml-2 px-2 py-0.5 bg-gray-100 text-gray-600 text-xs rounded">默认</span>' : ''}
</div>
<div class="flex gap-1">
${profile.name !== 'auto' ? `
<button onclick="editProfile('${profile.name}')" class="p-2 text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 rounded-lg transition">
<i class="ri-pencil-line"></i>
</button>
<button onclick="deleteProfile('${profile.name}')" class="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition">
<i class="ri-delete-bin-line"></i>
</button>
` : `
<button onclick="editProfile('${profile.name}')" class="p-2 text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 rounded-lg transition">
<i class="ri-pencil-line"></i>
</button>
`}
</div>
</div>
<div class="p-4">
<h3 class="font-medium text-gray-800">${profile.display_name || profile.name}</h3>
<p class="text-sm text-gray-500 mt-1">${profile.description || '暂无描述'}</p>
<div class="flex gap-2 mt-3">
<span class="px-2 py-1 bg-indigo-50 text-indigo-600 text-xs rounded">
${profile.strategy === 'priority' ? '按优先级' : '随机'}
</span>
<span class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded">
${profile.provider_details?.length || 0} 个提供商
</span>
</div>
<div class="mt-3 flex flex-wrap gap-1">
${profile.provider_details?.slice(0, 4).map(p =>
`<span class="px-2 py-0.5 bg-slate-100 text-slate-600 text-xs rounded">${p.name}</span>`
).join('') || ''}
${(profile.provider_details?.length || 0) > 4 ?
`<span class="text-xs text-gray-400">+${profile.provider_details.length - 4}</span>` : ''}
</div>
</div>
</div>
`).join('');
}
// 显示创建弹窗
function showCreateModal() {
document.getElementById('modalTitle').textContent = '创建Auto配置';
document.getElementById('profileId').value = '';
document.getElementById('profileForm').reset();
document.getElementById('profileName').disabled = false;
renderProviderList([]);
document.getElementById('editModal').classList.remove('hidden');
document.getElementById('editModal').classList.add('flex');
}
// 编辑配置
async function editProfile(profileName) {
const res = await fetch(`/api/auto-profiles/${profileName}`);
const profile = await res.json();
document.getElementById('modalTitle').textContent = '编辑Auto配置';
document.getElementById('profileId').value = profileName;
document.getElementById('profileName').value = profileName;
document.getElementById('profileName').disabled = true;
document.getElementById('displayName').value = profile.display_name || '';
document.getElementById('profileDesc').value = profile.description || '';
document.getElementById('profileStrategy').value = profile.strategy;
renderProviderList(profile.provider_details || []);
document.getElementById('editModal').classList.remove('hidden');
document.getElementById('editModal').classList.add('flex');
}
// 关闭弹窗
function closeModal() {
document.getElementById('editModal').classList.add('hidden');
document.getElementById('editModal').classList.remove('flex');
}
// 渲染提供商列表
function renderProviderList(selectedProviders) {
const container = document.getElementById('providerList');
const selectedIds = selectedProviders.map(p => p.id);
const sortedProviders = [...providers].sort((a, b) => a.priority - b.priority);
container.innerHTML = sortedProviders.map(p => {
const isSelected = selectedIds.includes(p.id) || (selectedIds.includes('*') && p.id !== '*');
const isAll = p.id === '*';
return `
<div class="flex items-center gap-3 p-3 bg-white rounded-lg border ${isSelected ? 'border-indigo-500 bg-indigo-50' : 'border-gray-200'} hover:border-indigo-300 transition cursor-move"
data-id="${p.id}" data-priority="${p.priority}">
<i class="ri-drag-move-2 text-gray-400 cursor-grab"></i>
<input type="checkbox" class="provider-check w-4 h-4 text-indigo-600 rounded"
id="provider-${p.id}" ${isSelected ? 'checked' : ''}
onchange="this.closest('[data-id]').classList.toggle('border-indigo-500', this.checked);this.closest('[data-id]').classList.toggle('bg-indigo-50', this.checked)">
<div class="flex-1 min-w-0">
<div class="font-medium text-gray-800 truncate">${p.name}</div>
<div class="text-xs text-gray-500">
优先级 ${p.priority} · ${p.models?.slice(0, 2).join(', ')}${p.models?.length > 2 ? '...' : ''}
</div>
</div>
${p.available ?
'<span class="px-2 py-0.5 bg-green-100 text-green-700 text-xs rounded">可用</span>' :
'<span class="px-2 py-0.5 bg-red-100 text-red-700 text-xs rounded">不可用</span>'}
</div>
`;
}).join('');
// 初始化拖拽排序
new Sortable(container, {
animation: 150,
handle: '.ri-drag-move-2',
ghostClass: 'dragging',
});
}
// 保存配置
async function saveProfile() {
const profileName = document.getElementById('profileName').value.trim();
const displayName = document.getElementById('displayName').value.trim();
const description = document.getElementById('profileDesc').value.trim();
const strategy = document.getElementById('profileStrategy').value;
if (!profileName) {
alert('请输入配置名称');
return;
}
// 收集选中的提供商
const selectedProviders = [];
document.querySelectorAll('.provider-check:checked').forEach(c => {
selectedProviders.push(c.id.replace('provider-', ''));
});
const data = {
name: profileName,
display_name: displayName || profileName,
description: description,
strategy: strategy,
providers: selectedProviders.length > 0 ? selectedProviders : ['*']
};
const profileId = document.getElementById('profileId').value;
const url = profileId ? `/api/auto-profiles/${profileId}` : '/api/auto-profiles';
const method = profileId ? 'PUT' : 'POST';
try {
const res = await fetch(url, {
method: method,
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const result = await res.json();
if (result.success) {
closeModal();
loadData();
} else {
alert('保存失败: ' + (result.error || '未知错误'));
}
} catch (e) {
alert('保存失败: ' + e.message);
}
}
// 删除配置
async function deleteProfile(profileName) {
if (!confirm(`确定删除配置 "${profileName}" 吗?`)) return;
try {
const res = await fetch(`/api/auto-profiles/${profileName}`, {method: 'DELETE'});
const result = await res.json();
if (result.success) {
loadData();
} else {
alert('删除失败: ' + (result.error || '未知错误'));
}
} catch (e) {
alert('删除失败: ' + e.message);
}
}
// 初始化
loadData();
</script>
</body>
</html>