| | <!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;
|
| | }
|
| | .endpoint-details {
|
| | width: 100%;
|
| | margin-top: 10px;
|
| | padding: 10px;
|
| | background: #f5f5f5;
|
| | border-radius: 4px;
|
| | }
|
| |
|
| | .model-mappings {
|
| | margin-top: 10px;
|
| | }
|
| |
|
| | .mapping-entry {
|
| | display: flex;
|
| | align-items: center;
|
| | margin: 5px 0;
|
| | gap: 10px;
|
| | }
|
| |
|
| | .mapping-entry input {
|
| | flex: 1;
|
| | padding: 8px;
|
| | border: 1px solid #ddd;
|
| | border-radius: 4px;
|
| | }
|
| |
|
| | .mapping-arrow {
|
| | color: #666;
|
| | font-weight: bold;
|
| | }
|
| |
|
| | .api-key-input {
|
| | width: 100%;
|
| | margin: 5px 0;
|
| | padding: 8px;
|
| | border: 1px solid #ddd;
|
| | border-radius: 4px;
|
| | }
|
| | .path-rewrite {
|
| | margin-top: 10px;
|
| | padding: 10px;
|
| | }
|
| |
|
| | .rewrite-entry {
|
| | display: flex;
|
| | align-items: center;
|
| | margin: 5px 0;
|
| | gap: 10px;
|
| | }
|
| |
|
| | .rewrite-entry input {
|
| | flex: 1;
|
| | padding: 8px;
|
| | border: 1px solid #ddd;
|
| | border-radius: 4px;
|
| | }
|
| | </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 getEndpointsConfig(type) {
|
| | const endpoints = [];
|
| | document.querySelectorAll(`#${type}Endpoints .endpoint`).forEach(el => {
|
| | const endpoint = {
|
| | url: el.querySelector('.endpoint-basic input[type="text"]').value.trim(),
|
| | weight: parseInt(el.querySelector('.endpoint-basic input[type="number"]').value) || 1,
|
| | enabled: el.querySelector('.endpoint-basic input[type="checkbox"]').checked,
|
| | api_key: el.querySelector('.api-key-input').value.trim()
|
| | };
|
| |
|
| |
|
| | const rewriteFrom = el.querySelector('.rewrite-from').value.trim();
|
| | const rewriteTo = el.querySelector('.rewrite-to').value.trim();
|
| | if (rewriteFrom && rewriteTo) {
|
| | endpoint.path_rewrite = {
|
| | from: rewriteFrom,
|
| | to: rewriteTo
|
| | };
|
| | }
|
| |
|
| |
|
| | if (type === 'chat') {
|
| | const modelMapping = {};
|
| | el.querySelectorAll('.mapping-entry').forEach(mapping => {
|
| | const from = mapping.querySelector('.mapping-from').value.trim();
|
| | const to = mapping.querySelector('.mapping-to').value.trim();
|
| | if (from && to) {
|
| | modelMapping[from] = to;
|
| | }
|
| | });
|
| | endpoint.model_mapping = modelMapping;
|
| | }
|
| |
|
| | endpoints.push(endpoint);
|
| | });
|
| | return endpoints;
|
| | }
|
| | 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 addModelMapping(button) {
|
| | const container = button.previousElementSibling;
|
| | const mappingDiv = document.createElement('div');
|
| | mappingDiv.className = 'mapping-entry';
|
| | mappingDiv.innerHTML = `
|
| | <input type="text" class="mapping-from" placeholder="原始模型名称">
|
| | <span class="mapping-arrow">→</span>
|
| | <input type="text" class="mapping-to" placeholder="目标模型名称">
|
| | <button class="delete" onclick="this.parentElement.remove()">删除</button>
|
| | `;
|
| | container.appendChild(mappingDiv);
|
| | }
|
| | 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>` : '';
|
| |
|
| |
|
| | const basicConfig = `
|
| | <div class="endpoint-basic">
|
| | <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.parentElement.remove()">删除</button>
|
| | </div>`;
|
| |
|
| |
|
| |
|
| | const detailsConfig = `
|
| | <div class="endpoint-details">
|
| | <div class="api-key-section">
|
| | <label>端点密钥:</label>
|
| | <input type="text" class="api-key-input" placeholder="输入端点密钥"
|
| | value="${endpoint && endpoint.api_key ? endpoint.api_key : ''}">
|
| | </div>
|
| | <div class="path-rewrite">
|
| | <label>路径重写规则:</label>
|
| | <div class="rewrite-entry">
|
| | <input type="text" class="rewrite-from" placeholder="原始路径(例:/proxies/v1)"
|
| | value="${endpoint && endpoint.path_rewrite ? endpoint.path_rewrite.from : ''}">
|
| | <span class="mapping-arrow">→</span>
|
| | <input type="text" class="rewrite-to" placeholder="目标路径(例:/v1)"
|
| | value="${endpoint && endpoint.path_rewrite ? endpoint.path_rewrite.to : ''}">
|
| | </div>
|
| | </div>
|
| | ${type === 'chat' ? `
|
| | <div class="model-mappings">
|
| | <label>模型名称映射:</label>
|
| | <div class="mappings-container">
|
| | ${endpoint && endpoint.model_mapping ?
|
| | Object.entries(endpoint.model_mapping).map(([from, to]) => `
|
| | <div class="mapping-entry">
|
| | <input type="text" class="mapping-from" placeholder="原始模型名称" value="${from}">
|
| | <span class="mapping-arrow">→</span>
|
| | <input type="text" class="mapping-to" placeholder="目标模型名称" value="${to}">
|
| | <button class="delete" onclick="this.parentElement.remove()">删除</button>
|
| | </div>
|
| | `).join('') : ''
|
| | }
|
| | </div>
|
| | <button onclick="addModelMapping(this)">添加模型映射</button>
|
| | </div>
|
| | ` : ''}
|
| | </div>`;
|
| |
|
| |
|
| | div.innerHTML = basicConfig + detailsConfig;
|
| | container.appendChild(div);
|
| | }
|
| |
|
| | 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> |