| | <!DOCTYPE html> |
| | <html> |
| | <head> |
| | <title>代理端点管理</title> |
| | <style> |
| | body { |
| | font-family: Arial, sans-serif; |
| | max-width: 1200px; |
| | margin: 20px auto; |
| | padding: 0 20px; |
| | } |
| | .endpoint-group { |
| | margin: 20px 0; |
| | padding: 15px; |
| | border: 1px solid #ddd; |
| | border-radius: 8px; |
| | } |
| | .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); |
| | } |
| | button { |
| | padding: 8px 15px; |
| | margin: 5px; |
| | cursor: pointer; |
| | border: none; |
| | border-radius: 4px; |
| | background-color: #4CAF50; |
| | color: white; |
| | } |
| | 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: #f5f5f5; |
| | border-radius: 4px; |
| | text-align: right; |
| | } |
| | .stats-group { |
| | margin: 20px 0; |
| | } |
| | .stat-item { |
| | margin: 10px 0; |
| | padding: 10px; |
| | border: 1px solid #ddd; |
| | border-radius: 4px; |
| | background-color: #f9f9f9; |
| | } |
| | .stat-item p { |
| | margin: 5px 0; |
| | } |
| | .blocked { |
| | background-color: #ffebee; |
| | } |
| | </style> |
| | </head> |
| | <body> |
| | <h1>代理端点管理</h1> |
| |
|
| | <div class="endpoint-group"> |
| | <h2>Models 端点</h2> |
| | <div id="modelsEndpoints"></div> |
| | <button onclick="addEndpoint('models')">添加 Models 端点</button> |
| | </div> |
| |
|
| | <div class="endpoint-group"> |
| | <h2>Chat 端点</h2> |
| | <div id="chatEndpoints"></div> |
| | <button onclick="addEndpoint('chat')">添加 Chat 端点</button> |
| | </div> |
| |
|
| | <div class="stats-group"> |
| | <h2>请求统计</h2> |
| | <div id="statsData"></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 addEndpoint(type, url = '', weight = 1, enabled = true) { |
| | const container = document.getElementById(type + 'Endpoints'); |
| | const div = document.createElement('div'); |
| | div.className = 'endpoint'; |
| | div.innerHTML = ` |
| | <input type="text" placeholder="输入端点URL" value="${url}"> |
| | <input type="number" placeholder="权重" value="${weight}" min="1"> |
| | <label> |
| | <input type="checkbox" ${enabled ? 'checked' : ''}> |
| | 启用 |
| | </label> |
| | <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') |
| | }; |
| | |
| | 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('配置已保存'); |
| | loadConfig(); |
| | } else { |
| | showStatus('保存失败: ' + await response.text(), true); |
| | } |
| | } 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 = ''; |
| | document.getElementById('statsData').innerHTML = ''; |
| | |
| | |
| | config.models.forEach(ep => { |
| | addEndpoint('models', ep.url, ep.weight, ep.enabled); |
| | }); |
| | |
| | |
| | config.chat.forEach(ep => { |
| | addEndpoint('chat', ep.url, ep.weight, ep.enabled); |
| | }); |
| | |
| | |
| | const statsHtml = Object.entries(config.stats).map(([fingerprint, stat]) => ` |
| | <div class="stat-item ${stat.blocked ? 'blocked' : ''}"> |
| | <p><strong>指纹:</strong> ${fingerprint}</p> |
| | <p><strong>IP:</strong> ${stat.ip}</p> |
| | <p><strong>设备:</strong> ${stat.user_agent}</p> |
| | <p><strong>请求次数:</strong> ${stat.chat_count}</p> |
| | <p><strong>最后访问:</strong> ${new Date(stat.last_access).toLocaleString()}</p> |
| | <p><strong>状态:</strong> ${stat.blocked ? '已拉黑' : '正常'}</p> |
| | </div> |
| | `).join(''); |
| | |
| | document.getElementById('statsData').innerHTML = statsHtml || '<p>暂无统计数据</p>'; |
| | } catch (error) { |
| | showStatus('加载配置失败: ' + error.message, true); |
| | } |
| | } |
| | |
| | |
| | function logout() { |
| | localStorage.removeItem('apiKey'); |
| | window.location.reload(); |
| | } |
| | |
| | |
| | document.addEventListener('DOMContentLoaded', loadConfig); |
| | |
| | </script> |
| | </body> |
| | </html> |
| |
|