hedgehog / admin.html
XCAPI's picture
Upload 2 files
b8d265c verified
<!DOCTYPE html>
<html>
<head>
<title>代理端点管理</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 20px auto;
padding: 0 20px;
background-color: #f5f5f5;
}
.endpoint-group {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 8px;
background-color: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.endpoint {
display: flex;
align-items: center;
margin: 10px 0;
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
background-color: #f9f9f9;
}
.endpoint.frozen {
background-color: #fff3e0; /* 更柔和的冻结状态颜色 */
border: 1px solid #ffe0b2;
}
.endpoint input[type="text"] {
flex: 1;
margin-right: 10px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.endpoint input[type="number"] {
width: 80px;
margin: 0 10px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.endpoint input[type="checkbox"] {
margin: 0 10px;
transform: scale(1.2);
}
.channel-input {
width: 120px !important;
}
.endpoint-status {
margin-left: 10px;
font-size: 0.9em;
color: #666;
}
.error-count {
color: #f57c00; /* 更柔和的错误计数颜色 */
margin-left: 10px;
font-size: 0.9em;
}
.frozen-until {
color: #1976d2; /* 冻结时间显示颜色 */
margin-left: 10px;
font-size: 0.9em;
}
button {
padding: 8px 15px;
margin: 5px;
cursor: pointer;
border: none;
border-radius: 4px;
background-color: #4CAF50;
color: white;
transition: background-color 0.3s;
}
button:hover {
background-color: #45a049;
}
button.delete {
background-color: #f44336;
}
button.delete:hover {
background-color: #da190b;
}
.status {
position: fixed;
top: 20px;
right: 20px;
padding: 10px 20px;
border-radius: 4px;
display: none;
z-index: 1000;
}
.status.success {
background-color: #4CAF50;
color: white;
}
.status.error {
background-color: #f44336;
color: white;
}
.model-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 10px;
margin-top: 10px;
}
.model-item {
padding: 10px;
background-color: #f9f9f9;
border-radius: 4px;
border: 1px solid #ddd;
}
.controls {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
gap: 10px;
}
.refresh-button {
background-color: #2196F3;
}
.refresh-button:hover {
background-color: #1976D2;
}
.settings {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
padding: 15px;
background-color: #fff;
border-radius: 4px;
margin: 10px 0;
}
.setting-item {
display: flex;
flex-direction: column;
gap: 5px;
}
.setting-item label {
font-weight: bold;
color: #333;
}
.model-rename {
display: flex;
align-items: center;
gap: 5px;
margin-top: 5px;
}
.model-rename input {
flex: 1;
padding: 4px 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.model-info {
margin-bottom: 5px;
}
.model-channel {
color: #666;
font-size: 0.9em;
}
</style>
</head>
<body>
<h1>代理端点管理</h1>
<div class="endpoint-group">
<h2>全局设置</h2>
<div class="settings">
<div class="setting-item">
<label for="frozenDuration">冷却时间(分钟)</label>
<input type="number" id="frozenDuration" min="1" value="3">
</div>
<div class="setting-item">
<label for="maxRetries">最大重试次数</label>
<input type="number" id="maxRetries" min="1" value="3">
</div>
</div>
</div>
<div class="endpoint-group">
<h2>Models 端点</h2>
<div id="modelsEndpoints"></div>
<button onclick="addEndpoint('models')">添加 Models 端点</button>
<button class="refresh-button" onclick="refreshEndpoints('models')">刷新状态</button>
<button onclick="updateAllModels()">更新所有模型信息</button>
</div>
<div class="endpoint-group">
<h2>Chat 端点</h2>
<div id="chatEndpoints"></div>
<button onclick="addEndpoint('chat')">添加 Chat 端点</button>
<button class="refresh-button" onclick="refreshEndpoints('chat')">刷新状态</button>
</div>
<div class="endpoint-group">
<h2>模型列表</h2>
<div class="settings">
<div class="setting-item">
<label>默认模型名称格式</label>
<select id="modelNameFormat" onchange="updateModelNameFormat()">
<option value="original">原始名称</option>
<option value="lowercase">全小写</option>
<option value="uppercase">全大写</option>
<option value="custom">自定义格式</option>
</select>
</div>
</div>
<div id="modelList" class="model-list"></div>
</div>
<div class="endpoint-group">
<h2>IP 黑名单</h2>
<div class="endpoint">
<input type="text" id="ipInput" placeholder="输入要封禁的IP地址">
<button onclick="addToBlacklist(document.getElementById('ipInput').value)">添加到黑名单</button>
</div>
<div id="blacklistEntries"></div>
</div>
<div class="controls">
<button onclick="saveConfig()">保存所有配置</button>
<button onclick="logout()" style="background-color: #f44336;">退出登录</button>
</div>
<div id="status" class="status"></div>
<script>
function showStatus(message, isError = false) {
const status = document.getElementById('status');
status.textContent = message;
status.className = 'status ' + (isError ? 'error' : 'success');
status.style.display = 'block';
setTimeout(() => status.style.display = 'none', 3000);
}
async function fetchWithAuth(url, options = {}) {
const apiKey = localStorage.getItem('apiKey');
if (!apiKey) {
const key = prompt('请输入API密钥:');
if (!key) return null;
localStorage.setItem('apiKey', key);
}
options.headers = {
...options.headers,
'Authorization': localStorage.getItem('apiKey')
};
try {
const response = await fetch(url, options);
if (response.status === 401) {
localStorage.removeItem('apiKey');
showStatus('认证失败,请重新登录', true);
return null;
}
return response;
} catch (error) {
showStatus('请求失败: ' + error.message, true);
return null;
}
}
function addEndpoint(type, endpoint = null) {
const container = document.getElementById(type + 'Endpoints');
const div = document.createElement('div');
// 检查端点状态
const now = new Date();
const frozenUntil = endpoint?.frozen_until ? new Date(endpoint.frozen_until) : null;
const isFrozen = frozenUntil && frozenUntil > now;
div.className = 'endpoint' + (isFrozen ? ' frozen' : '');
// 构建状态信息
let statusInfo = '';
if (frozenUntil && frozenUntil > now) {
statusInfo += `<span class="frozen-until">冻结至: ${frozenUntil.toLocaleTimeString()}</span>`;
}
if (endpoint?.error_count > 0) {
statusInfo += `<span class="error-count">错误次数: ${endpoint.error_count}</span>`;
}
div.innerHTML = `
<div class="endpoint-inputs">
<input type="text" placeholder="输入端点URL" value="${endpoint?.url || ''}" class="url-input">
<input type="number" placeholder="权重" value="${endpoint?.weight || 1}" min="1" class="weight-input">
<input type="text" placeholder="渠道分类" value="${endpoint?.channel || ''}" class="channel-input">
<label>
<input type="checkbox" ${endpoint?.enabled ? 'checked' : ''} class="enabled-input">
启用
</label>
</div>
<div class="endpoint-status-info">
${statusInfo}
</div>
<button class="delete" onclick="this.parentElement.remove()">删除</button>
`;
container.appendChild(div);
}
async function saveConfig() {
const config = {
models: getEndpointsConfig('models'),
chat: getEndpointsConfig('chat'),
frozen_duration: parseInt(document.getElementById('frozenDuration').value),
max_retries: parseInt(document.getElementById('maxRetries').value)
};
try {
const response = await fetchWithAuth('/admin/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(config)
});
if (response && response.ok) {
showStatus('配置已保存');
await loadModelList();
}
} catch (error) {
showStatus('保存失败: ' + error.message, true);
}
}
function getEndpointsConfig(type) {
const endpoints = [];
document.querySelectorAll(`#${type}Endpoints .endpoint`).forEach(el => {
endpoints.push({
url: el.querySelector('.url-input').value.trim(),
weight: parseInt(el.querySelector('.weight-input').value) || 1,
enabled: el.querySelector('.enabled-input').checked,
channel: el.querySelector('.channel-input').value.trim()
});
});
return endpoints;
}
async function refreshEndpoints(type) {
try {
const response = await fetchWithAuth('/admin/status');
if (!response) return;
const status = await response.json();
if (!status || !status[type] || !status[type].endpoints) {
showStatus('获取端点状态失败', true);
return;
}
const container = document.getElementById(type + 'Endpoints');
container.innerHTML = '';
// 使用status[type].endpoints而不是status[type]
status[type].endpoints.forEach(ep => {
const div = document.createElement('div');
div.className = 'endpoint';
// 检查端点状态
const now = new Date();
const frozenUntil = ep.frozen_until ? new Date(ep.frozen_until) : null;
const isFrozen = frozenUntil && frozenUntil > now;
if (isFrozen) {
div.classList.add('frozen');
}
// 状态信息
let statusInfo = '';
if (ep.error_count > 0) {
statusInfo += `<span class="error-count">错误次数: ${ep.error_count}</span>`;
}
if (isFrozen) {
statusInfo += `<span class="frozen-until">冻结至: ${frozenUntil.toLocaleTimeString()}</span>`;
}
div.innerHTML = `
<input type="text" placeholder="输入端点URL" value="${ep.url}" class="url-input">
<input type="number" placeholder="权重" value="${ep.weight || 1}" min="1" class="weight-input">
<input type="text" placeholder="渠道分类" value="${ep.channel || ''}" class="channel-input">
<label>
<input type="checkbox" ${ep.enabled ? 'checked' : ''} class="enabled-input">
启用
</label>
${statusInfo}
<button class="delete" onclick="this.parentElement.remove()">删除</button>
`;
container.appendChild(div);
});
showStatus(`${type}端点状态已更新`, false);
} catch (error) {
showStatus('刷新状态失败: ' + error.message, true);
}
}
async function loadConfig() {
try {
const response = await fetchWithAuth('/admin/config');
if (!response) return;
const config = await response.json();
// 清空现有端点
document.getElementById('modelsEndpoints').innerHTML = '';
document.getElementById('chatEndpoints').innerHTML = '';
// 添加端点
if (Array.isArray(config.models)) {
config.models.forEach(ep => addEndpoint('models', ep));
}
if (Array.isArray(config.chat)) {
config.chat.forEach(ep => addEndpoint('chat', ep));
}
// 设置全局配置
document.getElementById('frozenDuration').value = config.frozen_duration || 3;
document.getElementById('maxRetries').value = config.max_retries || 3;
await loadBlacklist();
await loadModelList();
await loadModelMappings();
showStatus('配置已加载', false);
} catch (error) {
showStatus('加载配置失败: ' + error.message, true);
}
}
async function loadBlacklist() {
try {
const response = await fetchWithAuth('/admin/blacklist');
if (!response) return;
const blacklist = await response.json();
const container = document.getElementById('blacklistEntries');
container.innerHTML = '';
blacklist.forEach(ip => {
const div = document.createElement('div');
div.className = 'endpoint';
div.innerHTML = `
<span>${ip}</span>
<button class="delete" onclick="removeFromBlacklist('${ip}')">解除封禁</button>
`;
container.appendChild(div);
});
} catch (error) {
showStatus('加载黑名单失败: ' + error.message, true);
}
}
async function addToBlacklist(ip) {
if (!ip) return;
try {
const response = await fetchWithAuth('/admin/blacklist', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ip: ip, action: 'add'})
});
if (response && response.ok) {
showStatus('IP已添加到黑名单');
document.getElementById('ipInput').value = '';
await loadBlacklist();
}
} catch (error) {
showStatus('添加失败: ' + error.message, true);
}
}
async function removeFromBlacklist(ip) {
try {
const response = await fetchWithAuth('/admin/blacklist', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ip: ip, action: 'remove'})
});
if (response && response.ok) {
showStatus('IP已从黑名单移除');
await loadBlacklist();
}
} catch (error) {
showStatus('移除失败: ' + error.message, true);
}
}
async function updateAllModels() {
try {
const response = await fetchWithAuth('/admin/update-models', {
method: 'POST'
});
if (response && response.ok) {
showStatus('模型信息已更新');
await loadModelList();
}
} catch (error) {
showStatus('更新失败: ' + error.message, true);
}
}
async function loadModelList() {
try {
const response = await fetchWithAuth('/v1/models');
if (!response) return;
const data = await response.json();
const container = document.getElementById('modelList');
container.innerHTML = '';
data.data.forEach(model => {
const div = document.createElement('div');
div.className = 'model-item';
div.innerHTML = `
<div class="model-info">
<strong>${model.id}</strong>
<span class="model-channel">[${model.owned_by.replace('-adapter', '')}]</span>
</div>
<div class="model-rename">
<input type="text"
value="${model.id}"
placeholder="输入新的模型名称"
class="model-name-input"
data-original-id="${model.id}">
<button onclick="saveModelMapping('${model.id}', this.parentElement)">保存</button>
<button class="delete" onclick="resetModelMapping('${model.id}', this.parentElement)">重置</button>
</div>
`;
container.appendChild(div);
});
} catch (error) {
showStatus('加载模型列表失败: ' + error.message, true);
}
}
async function saveModelMapping(originalId, element) {
const input = element.querySelector('.model-name-input');
const newName = input.value.trim();
if (!newName) {
showStatus('模型名称不能为空', true);
return;
}
try {
const response = await fetchWithAuth('/admin/model-mapping', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
original_id: originalId,
mapped_name: newName
})
});
if (response && response.ok) {
showStatus('模型名称映射已保存');
await loadModelList();
}
} catch (error) {
showStatus('保存失败: ' + error.message, true);
}
}
async function resetModelMapping(originalId, element) {
try {
const response = await fetchWithAuth('/admin/model-mapping', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
original_id: originalId,
mapped_name: originalId
})
});
if (response && response.ok) {
showStatus('模型名称已重置');
await loadModelList();
}
} catch (error) {
showStatus('重置失败: ' + error.message, true);
}
}
async function updateModelNameFormat() {
const format = document.getElementById('modelNameFormat').value;
const modelItems = document.querySelectorAll('.model-name-input');
modelItems.forEach(input => {
const originalId = input.dataset.originalId;
let newName = originalId;
switch(format) {
case 'lowercase':
newName = originalId.toLowerCase();
break;
case 'uppercase':
newName = originalId.toUpperCase();
break;
case 'custom':
// 可以添加自定义格式逻辑
break;
default:
newName = originalId;
}
input.value = newName;
});
}
async function loadModelMappings() {
try {
const response = await fetchWithAuth('/admin/model-mappings');
if (response && response.ok) {
const mappings = await response.json();
// 更新输入框的值
Object.entries(mappings).forEach(([mappedName, originalId]) => {
const input = document.querySelector(`.model-name-input[data-original-id="${originalId}"]`);
if (input) {
input.value = mappedName;
}
});
}
} catch (error) {
showStatus('加载模型映射失败: ' + error.message, true);
}
}
function logout() {
localStorage.removeItem('apiKey');
window.location.reload();
}
document.addEventListener('DOMContentLoaded', loadConfig);
</script>
</body>
</html>