| <!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 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); |
| } |
| .endpoint-status { |
| margin-left: 10px; |
| font-size: 0.9em; |
| color: #666; |
| } |
| .frozen { |
| background-color: #ffe6e6; |
| } |
| 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: 15px; |
| border-radius: 4px; |
| display: none; |
| z-index: 1000; |
| } |
| .success { |
| background-color: #4CAF50; |
| color: white; |
| } |
| .error { |
| background-color: #f44336; |
| color: white; |
| } |
| h2 { |
| color: #333; |
| border-bottom: 2px solid #4CAF50; |
| padding-bottom: 10px; |
| } |
| .controls { |
| margin: 20px 0; |
| padding: 10px; |
| background-color: white; |
| border-radius: 4px; |
| text-align: right; |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
| } |
| .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; |
| } |
| .refresh-button { |
| background-color: #2196F3; |
| margin-left: 10px; |
| } |
| .refresh-button:hover { |
| background-color: #1976D2; |
| } |
| .error-count { |
| color: #f44336; |
| font-weight: bold; |
| margin-left: 10px; |
| } |
| </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> |
| </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>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> |
| let apiKey = localStorage.getItem('apiKey'); |
| |
| if (!apiKey) { |
| apiKey = prompt('请输入访问密钥:'); |
| if (apiKey) { |
| localStorage.setItem('apiKey', apiKey); |
| } else { |
| window.location.href = '/'; |
| } |
| } |
| |
| 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); |
| } |
| |
| function formatTime(timeString) { |
| if (!timeString) return '无'; |
| const date = new Date(timeString); |
| return date.toLocaleString(); |
| } |
| |
| function addEndpoint(type, endpoint = null) { |
| const container = document.getElementById(type + 'Endpoints'); |
| const div = document.createElement('div'); |
| div.className = 'endpoint'; |
| if (endpoint && endpoint.frozen_until && new Date(endpoint.frozen_until) > new Date()) { |
| div.className += ' frozen'; |
| } |
| |
| const statusInfo = endpoint ? |
| `<div class="endpoint-status"> |
| 错误次数: <span class="error-count">${endpoint.error_count || 0}</span> |
| 最后错误: ${formatTime(endpoint.last_error_time)} |
| 冷却至: ${formatTime(endpoint.frozen_until)} |
| </div>` : ''; |
| |
| div.innerHTML = ` |
| <input type="text" placeholder="输入端点URL" value="${endpoint ? endpoint.url : ''}"> |
| <input type="number" placeholder="权重" value="${endpoint ? endpoint.weight : 1}" min="1"> |
| <label> |
| <input type="checkbox" ${endpoint && endpoint.enabled ? 'checked' : ''}> |
| 启用 |
| </label> |
| ${statusInfo} |
| <button class="delete" onclick="this.parentElement.remove()">删除</button> |
| `; |
| container.appendChild(div); |
| } |
| |
| function getEndpointsConfig(type) { |
| const endpoints = []; |
| document.querySelectorAll(`#${type}Endpoints .endpoint`).forEach(el => { |
| endpoints.push({ |
| url: el.querySelector('input[type="text"]').value.trim(), |
| weight: parseInt(el.querySelector('input[type="number"]').value) || 1, |
| enabled: el.querySelector('input[type="checkbox"]').checked |
| }); |
| }); |
| return endpoints; |
| } |
| |
| async function fetchWithAuth(url, options = {}) { |
| const headers = { |
| ...options.headers, |
| 'Authorization': apiKey |
| }; |
| |
| const response = await fetch(url, { ...options, headers }); |
| |
| if (response.status === 401) { |
| localStorage.removeItem('apiKey'); |
| window.location.reload(); |
| return null; |
| } |
| |
| return response; |
| } |
| |
| 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) return; |
| |
| if (response.ok) { |
| showStatus('配置已保存'); |
| refreshEndpoints('models'); |
| refreshEndpoints('chat'); |
| } else { |
| showStatus('保存失败: ' + await response.text(), true); |
| } |
| } catch (error) { |
| showStatus('保存失败: ' + error.message, true); |
| } |
| } |
| |
| async function refreshEndpoints(type) { |
| try { |
| const response = await fetchWithAuth('/admin/status'); |
| if (!response) return; |
| |
| const status = await response.json(); |
| const container = document.getElementById(type + 'Endpoints'); |
| container.innerHTML = ''; |
| |
| status[type].endpoints.forEach(ep => { |
| addEndpoint(type, ep); |
| }); |
| } 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 = ''; |
| |
| config.models.forEach(ep => { |
| addEndpoint('models', ep); |
| }); |
| |
| config.chat.forEach(ep => { |
| addEndpoint('chat', ep); |
| }); |
| |
| document.getElementById('frozenDuration').value = config.frozen_duration; |
| document.getElementById('maxRetries').value = config.max_retries; |
| |
| loadBlacklist(); |
| } catch (error) { |
| showStatus('加载配置失败: ' + error.message, true); |
| } |
| } |
| |
| async function loadBlacklist() { |
| try { |
| const response = await fetchWithAuth('/admin/blacklist'); |
| 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.ok) { |
| showStatus('IP已添加到黑名单'); |
| document.getElementById('ipInput').value = ''; |
| 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.ok) { |
| showStatus('IP已从黑名单移除'); |
| loadBlacklist(); |
| } |
| } catch (error) { |
| showStatus('移除失败: ' + error.message, true); |
| } |
| } |
| |
| function logout() { |
| localStorage.removeItem('apiKey'); |
| window.location.reload(); |
| } |
| |
| |
| |
| |
| |
| |
| |
| document.addEventListener('DOMContentLoaded', loadConfig); |
| </script> |
| </body> |
| </html> |
|
|