XCAPI commited on
Commit
b09e08d
·
verified ·
1 Parent(s): 8903812

Upload 2 files

Browse files
Files changed (2) hide show
  1. admin.html +542 -413
  2. workers +2 -2
admin.html CHANGED
@@ -1,413 +1,542 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>代理端点管理</title>
5
- <style>
6
- body {
7
- font-family: Arial, sans-serif;
8
- max-width: 1200px;
9
- margin: 20px auto;
10
- padding: 0 20px;
11
- background-color: #f5f5f5;
12
- }
13
- .endpoint-group {
14
- margin: 20px 0;
15
- padding: 15px;
16
- border: 1px solid #ddd;
17
- border-radius: 8px;
18
- background-color: white;
19
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
20
- }
21
- .endpoint {
22
- display: flex;
23
- align-items: center;
24
- margin: 10px 0;
25
- padding: 10px;
26
- border: 1px solid #eee;
27
- border-radius: 4px;
28
- background-color: #f9f9f9;
29
- }
30
- .endpoint input[type="text"] {
31
- flex: 1;
32
- margin-right: 10px;
33
- padding: 8px;
34
- border: 1px solid #ddd;
35
- border-radius: 4px;
36
- }
37
- .endpoint input[type="number"] {
38
- width: 80px;
39
- margin: 0 10px;
40
- padding: 8px;
41
- border: 1px solid #ddd;
42
- border-radius: 4px;
43
- }
44
- .endpoint input[type="checkbox"] {
45
- margin: 0 10px;
46
- transform: scale(1.2);
47
- }
48
- .endpoint-status {
49
- margin-left: 10px;
50
- font-size: 0.9em;
51
- color: #666;
52
- }
53
- .frozen {
54
- background-color: #ffe6e6;
55
- }
56
- button {
57
- padding: 8px 15px;
58
- margin: 5px;
59
- cursor: pointer;
60
- border: none;
61
- border-radius: 4px;
62
- background-color: #4CAF50;
63
- color: white;
64
- transition: background-color 0.3s;
65
- }
66
- button:hover {
67
- background-color: #45a049;
68
- }
69
- button.delete {
70
- background-color: #f44336;
71
- }
72
- button.delete:hover {
73
- background-color: #da190b;
74
- }
75
- .status {
76
- position: fixed;
77
- top: 20px;
78
- right: 20px;
79
- padding: 15px;
80
- border-radius: 4px;
81
- display: none;
82
- z-index: 1000;
83
- }
84
- .success {
85
- background-color: #4CAF50;
86
- color: white;
87
- }
88
- .error {
89
- background-color: #f44336;
90
- color: white;
91
- }
92
- h2 {
93
- color: #333;
94
- border-bottom: 2px solid #4CAF50;
95
- padding-bottom: 10px;
96
- }
97
- .controls {
98
- margin: 20px 0;
99
- padding: 10px;
100
- background-color: white;
101
- border-radius: 4px;
102
- text-align: right;
103
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
104
- }
105
- .settings {
106
- display: grid;
107
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
108
- gap: 10px;
109
- padding: 15px;
110
- background-color: #fff;
111
- border-radius: 4px;
112
- margin: 10px 0;
113
- }
114
- .setting-item {
115
- display: flex;
116
- flex-direction: column;
117
- gap: 5px;
118
- }
119
- .setting-item label {
120
- font-weight: bold;
121
- color: #333;
122
- }
123
- .refresh-button {
124
- background-color: #2196F3;
125
- margin-left: 10px;
126
- }
127
- .refresh-button:hover {
128
- background-color: #1976D2;
129
- }
130
- .error-count {
131
- color: #f44336;
132
- font-weight: bold;
133
- margin-left: 10px;
134
- }
135
- </style>
136
- </head>
137
- <body>
138
- <h1>代理端点管理</h1>
139
-
140
- <div class="endpoint-group">
141
- <h2>全局设置</h2>
142
- <div class="settings">
143
- <div class="setting-item">
144
- <label for="frozenDuration">冷却时间(分钟)</label>
145
- <input type="number" id="frozenDuration" min="1" value="3">
146
- </div>
147
- <div class="setting-item">
148
- <label for="maxRetries">最大重试次数</label>
149
- <input type="number" id="maxRetries" min="1" value="3">
150
- </div>
151
- </div>
152
- </div>
153
-
154
- <div class="endpoint-group">
155
- <h2>Models 端点</h2>
156
- <div id="modelsEndpoints"></div>
157
- <button onclick="addEndpoint('models')">添加 Models 端点</button>
158
- <button class="refresh-button" onclick="refreshEndpoints('models')">刷新状态</button>
159
- </div>
160
-
161
- <div class="endpoint-group">
162
- <h2>Chat 端点</h2>
163
- <div id="chatEndpoints"></div>
164
- <button onclick="addEndpoint('chat')">添加 Chat 端点</button>
165
- <button class="refresh-button" onclick="refreshEndpoints('chat')">刷新状态</button>
166
- </div>
167
-
168
- <div class="endpoint-group">
169
- <h2>IP 黑名单</h2>
170
- <div class="endpoint">
171
- <input type="text" id="ipInput" placeholder="输入要封禁的IP地址">
172
- <button onclick="addToBlacklist(document.getElementById('ipInput').value)">添加到黑名单</button>
173
- </div>
174
- <div id="blacklistEntries"></div>
175
- </div>
176
-
177
- <div class="controls">
178
- <button onclick="saveConfig()">保存所有配置</button>
179
- <button onclick="logout()" style="background-color: #f44336;">退出登录</button>
180
- </div>
181
-
182
- <div id="status" class="status"></div>
183
-
184
- <script>
185
- let apiKey = localStorage.getItem('apiKey');
186
-
187
- if (!apiKey) {
188
- apiKey = prompt('请输入访问密钥:');
189
- if (apiKey) {
190
- localStorage.setItem('apiKey', apiKey);
191
- } else {
192
- window.location.href = '/';
193
- }
194
- }
195
-
196
- function showStatus(message, isError = false) {
197
- const status = document.getElementById('status');
198
- status.textContent = message;
199
- status.className = 'status ' + (isError ? 'error' : 'success');
200
- status.style.display = 'block';
201
- setTimeout(() => status.style.display = 'none', 3000);
202
- }
203
-
204
- function formatTime(timeString) {
205
- if (!timeString) return '无';
206
- const date = new Date(timeString);
207
- return date.toLocaleString();
208
- }
209
-
210
- function addEndpoint(type, endpoint = null) {
211
- const container = document.getElementById(type + 'Endpoints');
212
- const div = document.createElement('div');
213
- div.className = 'endpoint';
214
- if (endpoint && endpoint.frozen_until && new Date(endpoint.frozen_until) > new Date()) {
215
- div.className += ' frozen';
216
- }
217
-
218
- const statusInfo = endpoint ?
219
- `<div class="endpoint-status">
220
- 错误次数: <span class="error-count">${endpoint.error_count || 0}</span>
221
- 最后错误: ${formatTime(endpoint.last_error_time)}
222
- 冷却至: ${formatTime(endpoint.frozen_until)}
223
- </div>` : '';
224
-
225
- div.innerHTML = `
226
- <input type="text" placeholder="输入端点URL" value="${endpoint ? endpoint.url : ''}">
227
- <input type="number" placeholder="权重" value="${endpoint ? endpoint.weight : 1}" min="1">
228
- <label>
229
- <input type="checkbox" ${endpoint && endpoint.enabled ? 'checked' : ''}>
230
- 启用
231
- </label>
232
- ${statusInfo}
233
- <button class="delete" onclick="this.parentElement.remove()">删除</button>
234
- `;
235
- container.appendChild(div);
236
- }
237
-
238
- function getEndpointsConfig(type) {
239
- const endpoints = [];
240
- document.querySelectorAll(`#${type}Endpoints .endpoint`).forEach(el => {
241
- endpoints.push({
242
- url: el.querySelector('input[type="text"]').value.trim(),
243
- weight: parseInt(el.querySelector('input[type="number"]').value) || 1,
244
- enabled: el.querySelector('input[type="checkbox"]').checked
245
- });
246
- });
247
- return endpoints;
248
- }
249
-
250
- async function fetchWithAuth(url, options = {}) {
251
- const headers = {
252
- ...options.headers,
253
- 'Authorization': apiKey
254
- };
255
-
256
- const response = await fetch(url, { ...options, headers });
257
-
258
- if (response.status === 401) {
259
- localStorage.removeItem('apiKey');
260
- window.location.reload();
261
- return null;
262
- }
263
-
264
- return response;
265
- }
266
-
267
- async function saveConfig() {
268
- const config = {
269
- models: getEndpointsConfig('models'),
270
- chat: getEndpointsConfig('chat'),
271
- frozen_duration: parseInt(document.getElementById('frozenDuration').value),
272
- max_retries: parseInt(document.getElementById('maxRetries').value)
273
- };
274
-
275
- try {
276
- const response = await fetchWithAuth('/admin/config', {
277
- method: 'POST',
278
- headers: {'Content-Type': 'application/json'},
279
- body: JSON.stringify(config)
280
- });
281
-
282
- if (!response) return;
283
-
284
- if (response.ok) {
285
- showStatus('配置已保存');
286
- refreshEndpoints('models');
287
- refreshEndpoints('chat');
288
- } else {
289
- showStatus('保存失败: ' + await response.text(), true);
290
- }
291
- } catch (error) {
292
- showStatus('保存失败: ' + error.message, true);
293
- }
294
- }
295
-
296
- async function refreshEndpoints(type) {
297
- try {
298
- const response = await fetchWithAuth('/admin/status');
299
- if (!response) return;
300
-
301
- const status = await response.json();
302
- const container = document.getElementById(type + 'Endpoints');
303
- container.innerHTML = '';
304
-
305
- status[type].endpoints.forEach(ep => {
306
- addEndpoint(type, ep);
307
- });
308
- } catch (error) {
309
- showStatus('刷新状态失败: ' + error.message, true);
310
- }
311
- }
312
-
313
- async function loadConfig() {
314
- try {
315
- const response = await fetchWithAuth('/admin/config');
316
- if (!response) return;
317
-
318
- const config = await response.json();
319
-
320
- document.getElementById('modelsEndpoints').innerHTML = '';
321
- document.getElementById('chatEndpoints').innerHTML = '';
322
-
323
- config.models.forEach(ep => {
324
- addEndpoint('models', ep);
325
- });
326
-
327
- config.chat.forEach(ep => {
328
- addEndpoint('chat', ep);
329
- });
330
-
331
- document.getElementById('frozenDuration').value = config.frozen_duration;
332
- document.getElementById('maxRetries').value = config.max_retries;
333
-
334
- loadBlacklist();
335
- } catch (error) {
336
- showStatus('加载配置失败: ' + error.message, true);
337
- }
338
- }
339
-
340
- async function loadBlacklist() {
341
- try {
342
- const response = await fetchWithAuth('/admin/blacklist');
343
- const blacklist = await response.json();
344
-
345
- const container = document.getElementById('blacklistEntries');
346
- container.innerHTML = '';
347
-
348
- blacklist.forEach(ip => {
349
- const div = document.createElement('div');
350
- div.className = 'endpoint';
351
- div.innerHTML = `
352
- <span>${ip}</span>
353
- <button class="delete" onclick="removeFromBlacklist('${ip}')">解除封禁</button>
354
- `;
355
- container.appendChild(div);
356
- });
357
- } catch (error) {
358
- showStatus('加载黑名单失败: ' + error.message, true);
359
- }
360
- }
361
-
362
- async function addToBlacklist(ip) {
363
- if (!ip) return;
364
-
365
- try {
366
- const response = await fetchWithAuth('/admin/blacklist', {
367
- method: 'POST',
368
- headers: {'Content-Type': 'application/json'},
369
- body: JSON.stringify({ip: ip, action: 'add'})
370
- });
371
-
372
- if (response.ok) {
373
- showStatus('IP已添加到黑名单');
374
- document.getElementById('ipInput').value = '';
375
- loadBlacklist();
376
- }
377
- } catch (error) {
378
- showStatus('添加失败: ' + error.message, true);
379
- }
380
- }
381
-
382
- async function removeFromBlacklist(ip) {
383
- try {
384
- const response = await fetchWithAuth('/admin/blacklist', {
385
- method: 'POST',
386
- headers: {'Content-Type': 'application/json'},
387
- body: JSON.stringify({ip: ip, action: 'remove'})
388
- });
389
-
390
- if (response.ok) {
391
- showStatus('IP已从黑名单移除');
392
- loadBlacklist();
393
- }
394
- } catch (error) {
395
- showStatus('移除失败: ' + error.message, true);
396
- }
397
- }
398
-
399
- function logout() {
400
- localStorage.removeItem('apiKey');
401
- window.location.reload();
402
- }
403
-
404
- // 定期刷新端点状态
405
- // setInterval(() => {
406
- // refreshEndpoints('models');
407
- // refreshEndpoints('chat');
408
- // }, 30000); // 每30秒刷新一次
409
-
410
- document.addEventListener('DOMContentLoaded', loadConfig);
411
- </script>
412
- </body>
413
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>代理端点管理</title>
5
+ <style>
6
+ body {
7
+ font-family: Arial, sans-serif;
8
+ max-width: 1200px;
9
+ margin: 20px auto;
10
+ padding: 0 20px;
11
+ background-color: #f5f5f5;
12
+ }
13
+ .endpoint-group {
14
+ margin: 20px 0;
15
+ padding: 15px;
16
+ border: 1px solid #ddd;
17
+ border-radius: 8px;
18
+ background-color: white;
19
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
20
+ }
21
+ .endpoint {
22
+ display: flex;
23
+ align-items: center;
24
+ margin: 10px 0;
25
+ padding: 10px;
26
+ border: 1px solid #eee;
27
+ border-radius: 4px;
28
+ background-color: #f9f9f9;
29
+ }
30
+ .endpoint input[type="text"] {
31
+ flex: 1;
32
+ margin-right: 10px;
33
+ padding: 8px;
34
+ border: 1px solid #ddd;
35
+ border-radius: 4px;
36
+ }
37
+ .endpoint input[type="number"] {
38
+ width: 80px;
39
+ margin: 0 10px;
40
+ padding: 8px;
41
+ border: 1px solid #ddd;
42
+ border-radius: 4px;
43
+ }
44
+ .endpoint input[type="checkbox"] {
45
+ margin: 0 10px;
46
+ transform: scale(1.2);
47
+ }
48
+ .endpoint-status {
49
+ margin-left: 10px;
50
+ font-size: 0.9em;
51
+ color: #666;
52
+ }
53
+ .frozen {
54
+ background-color: #ffe6e6;
55
+ }
56
+ button {
57
+ padding: 8px 15px;
58
+ margin: 5px;
59
+ cursor: pointer;
60
+ border: none;
61
+ border-radius: 4px;
62
+ background-color: #4CAF50;
63
+ color: white;
64
+ transition: background-color 0.3s;
65
+ }
66
+ button:hover {
67
+ background-color: #45a049;
68
+ }
69
+ button.delete {
70
+ background-color: #f44336;
71
+ }
72
+ button.delete:hover {
73
+ background-color: #da190b;
74
+ }
75
+ .status {
76
+ position: fixed;
77
+ top: 20px;
78
+ right: 20px;
79
+ padding: 15px;
80
+ border-radius: 4px;
81
+ display: none;
82
+ z-index: 1000;
83
+ }
84
+ .success {
85
+ background-color: #4CAF50;
86
+ color: white;
87
+ }
88
+ .error {
89
+ background-color: #f44336;
90
+ color: white;
91
+ }
92
+ h2 {
93
+ color: #333;
94
+ border-bottom: 2px solid #4CAF50;
95
+ padding-bottom: 10px;
96
+ }
97
+ .controls {
98
+ margin: 20px 0;
99
+ padding: 10px;
100
+ background-color: white;
101
+ border-radius: 4px;
102
+ text-align: right;
103
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
104
+ }
105
+ .settings {
106
+ display: grid;
107
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
108
+ gap: 10px;
109
+ padding: 15px;
110
+ background-color: #fff;
111
+ border-radius: 4px;
112
+ margin: 10px 0;
113
+ }
114
+ .setting-item {
115
+ display: flex;
116
+ flex-direction: column;
117
+ gap: 5px;
118
+ }
119
+ .setting-item label {
120
+ font-weight: bold;
121
+ color: #333;
122
+ }
123
+ .refresh-button {
124
+ background-color: #2196F3;
125
+ margin-left: 10px;
126
+ }
127
+ .refresh-button:hover {
128
+ background-color: #1976D2;
129
+ }
130
+ .error-count {
131
+ color: #f44336;
132
+ font-weight: bold;
133
+ margin-left: 10px;
134
+ }
135
+ .endpoint-details {
136
+ width: 100%;
137
+ margin-top: 10px;
138
+ padding: 10px;
139
+ background: #f5f5f5;
140
+ border-radius: 4px;
141
+ }
142
+
143
+ .model-mappings {
144
+ margin-top: 10px;
145
+ }
146
+
147
+ .mapping-entry {
148
+ display: flex;
149
+ align-items: center;
150
+ margin: 5px 0;
151
+ gap: 10px;
152
+ }
153
+
154
+ .mapping-entry input {
155
+ flex: 1;
156
+ padding: 8px;
157
+ border: 1px solid #ddd;
158
+ border-radius: 4px;
159
+ }
160
+
161
+ .mapping-arrow {
162
+ color: #666;
163
+ font-weight: bold;
164
+ }
165
+
166
+ .api-key-input {
167
+ width: 100%;
168
+ margin: 5px 0;
169
+ padding: 8px;
170
+ border: 1px solid #ddd;
171
+ border-radius: 4px;
172
+ }
173
+ .path-rewrite {
174
+ margin-top: 10px;
175
+ padding: 10px;
176
+ }
177
+
178
+ .rewrite-entry {
179
+ display: flex;
180
+ align-items: center;
181
+ margin: 5px 0;
182
+ gap: 10px;
183
+ }
184
+
185
+ .rewrite-entry input {
186
+ flex: 1;
187
+ padding: 8px;
188
+ border: 1px solid #ddd;
189
+ border-radius: 4px;
190
+ }
191
+ </style>
192
+ </head>
193
+ <body>
194
+ <h1>代理端点管理</h1>
195
+
196
+ <div class="endpoint-group">
197
+ <h2>全局设置</h2>
198
+ <div class="settings">
199
+ <div class="setting-item">
200
+ <label for="frozenDuration">冷却时间(分钟)</label>
201
+ <input type="number" id="frozenDuration" min="1" value="3">
202
+ </div>
203
+ <div class="setting-item">
204
+ <label for="maxRetries">最大重试次数</label>
205
+ <input type="number" id="maxRetries" min="1" value="3">
206
+ </div>
207
+ </div>
208
+ </div>
209
+
210
+ <div class="endpoint-group">
211
+ <h2>Models 端点</h2>
212
+ <div id="modelsEndpoints"></div>
213
+ <button onclick="addEndpoint('models')">添加 Models 端点</button>
214
+ <button class="refresh-button" onclick="refreshEndpoints('models')">刷新状态</button>
215
+ </div>
216
+
217
+ <div class="endpoint-group">
218
+ <h2>Chat 端点</h2>
219
+ <div id="chatEndpoints"></div>
220
+ <button onclick="addEndpoint('chat')">添加 Chat 端点</button>
221
+ <button class="refresh-button" onclick="refreshEndpoints('chat')">刷新状态</button>
222
+ </div>
223
+
224
+ <div class="endpoint-group">
225
+ <h2>IP 黑名单</h2>
226
+ <div class="endpoint">
227
+ <input type="text" id="ipInput" placeholder="输入要封禁的IP地址">
228
+ <button onclick="addToBlacklist(document.getElementById('ipInput').value)">添加到黑名单</button>
229
+ </div>
230
+ <div id="blacklistEntries"></div>
231
+ </div>
232
+
233
+ <div class="controls">
234
+ <button onclick="saveConfig()">保存所有配置</button>
235
+ <button onclick="logout()" style="background-color: #f44336;">退出登录</button>
236
+ </div>
237
+
238
+ <div id="status" class="status"></div>
239
+
240
+ <script>
241
+ let apiKey = localStorage.getItem('apiKey');
242
+
243
+ if (!apiKey) {
244
+ apiKey = prompt('请输入访问密钥:');
245
+ if (apiKey) {
246
+ localStorage.setItem('apiKey', apiKey);
247
+ } else {
248
+ window.location.href = '/';
249
+ }
250
+ }
251
+ function getEndpointsConfig(type) {
252
+ const endpoints = [];
253
+ document.querySelectorAll(`#${type}Endpoints .endpoint`).forEach(el => {
254
+ const endpoint = {
255
+ url: el.querySelector('.endpoint-basic input[type="text"]').value.trim(),
256
+ weight: parseInt(el.querySelector('.endpoint-basic input[type="number"]').value) || 1,
257
+ enabled: el.querySelector('.endpoint-basic input[type="checkbox"]').checked,
258
+ api_key: el.querySelector('.api-key-input').value.trim()
259
+ };
260
+
261
+ // 获取路径重写规则
262
+ const rewriteFrom = el.querySelector('.rewrite-from').value.trim();
263
+ const rewriteTo = el.querySelector('.rewrite-to').value.trim();
264
+ if (rewriteFrom && rewriteTo) {
265
+ endpoint.path_rewrite = {
266
+ from: rewriteFrom,
267
+ to: rewriteTo
268
+ };
269
+ }
270
+
271
+ // 如果是chat端点,添加模型映射
272
+ if (type === 'chat') {
273
+ const modelMapping = {};
274
+ el.querySelectorAll('.mapping-entry').forEach(mapping => {
275
+ const from = mapping.querySelector('.mapping-from').value.trim();
276
+ const to = mapping.querySelector('.mapping-to').value.trim();
277
+ if (from && to) {
278
+ modelMapping[from] = to;
279
+ }
280
+ });
281
+ endpoint.model_mapping = modelMapping;
282
+ }
283
+
284
+ endpoints.push(endpoint);
285
+ });
286
+ return endpoints;
287
+ }
288
+ function showStatus(message, isError = false) {
289
+ const status = document.getElementById('status');
290
+ status.textContent = message;
291
+ status.className = 'status ' + (isError ? 'error' : 'success');
292
+ status.style.display = 'block';
293
+ setTimeout(() => status.style.display = 'none', 3000);
294
+ }
295
+
296
+ function formatTime(timeString) {
297
+ if (!timeString) return '无';
298
+ const date = new Date(timeString);
299
+ return date.toLocaleString();
300
+ }
301
+ function addModelMapping(button) {
302
+ const container = button.previousElementSibling;
303
+ const mappingDiv = document.createElement('div');
304
+ mappingDiv.className = 'mapping-entry';
305
+ mappingDiv.innerHTML = `
306
+ <input type="text" class="mapping-from" placeholder="原始模型名称">
307
+ <span class="mapping-arrow">→</span>
308
+ <input type="text" class="mapping-to" placeholder="目标模型名称">
309
+ <button class="delete" onclick="this.parentElement.remove()">删除</button>
310
+ `;
311
+ container.appendChild(mappingDiv);
312
+ }
313
+ function addEndpoint(type, endpoint = null) {
314
+ const container = document.getElementById(type + 'Endpoints');
315
+ const div = document.createElement('div');
316
+ div.className = 'endpoint';
317
+ if (endpoint && endpoint.frozen_until && new Date(endpoint.frozen_until) > new Date()) {
318
+ div.className += ' frozen';
319
+ }
320
+
321
+ const statusInfo = endpoint ?
322
+ `<div class="endpoint-status">
323
+ 错误次数: <span class="error-count">${endpoint.error_count || 0}</span>
324
+ 最后错误: ${formatTime(endpoint.last_error_time)}
325
+ 冷却至: ${formatTime(endpoint.frozen_until)}
326
+ </div>` : '';
327
+
328
+ // 基础配置部分
329
+ const basicConfig = `
330
+ <div class="endpoint-basic">
331
+ <input type="text" placeholder="输入端点URL" value="${endpoint ? endpoint.url : ''}">
332
+ <input type="number" placeholder="权重" value="${endpoint ? endpoint.weight : 1}" min="1">
333
+ <label>
334
+ <input type="checkbox" ${endpoint && endpoint.enabled ? 'checked' : ''}>
335
+ 启用
336
+ </label>
337
+ ${statusInfo}
338
+ <button class="delete" onclick="this.parentElement.parentElement.remove()">删除</button>
339
+ </div>`;
340
+
341
+ // 详细配置部分
342
+ // 详细配置部分
343
+ const detailsConfig = `
344
+ <div class="endpoint-details">
345
+ <div class="api-key-section">
346
+ <label>端点密钥:</label>
347
+ <input type="text" class="api-key-input" placeholder="输入端点密钥"
348
+ value="${endpoint && endpoint.api_key ? endpoint.api_key : ''}">
349
+ </div>
350
+ <div class="path-rewrite">
351
+ <label>路径重写规则:</label>
352
+ <div class="rewrite-entry">
353
+ <input type="text" class="rewrite-from" placeholder="原始路径(例:/proxies/v1)"
354
+ value="${endpoint && endpoint.path_rewrite ? endpoint.path_rewrite.from : ''}">
355
+ <span class="mapping-arrow">→</span>
356
+ <input type="text" class="rewrite-to" placeholder="目标路径(例:/v1)"
357
+ value="${endpoint && endpoint.path_rewrite ? endpoint.path_rewrite.to : ''}">
358
+ </div>
359
+ </div>
360
+ ${type === 'chat' ? `
361
+ <div class="model-mappings">
362
+ <label>模型名称映射:</label>
363
+ <div class="mappings-container">
364
+ ${endpoint && endpoint.model_mapping ?
365
+ Object.entries(endpoint.model_mapping).map(([from, to]) => `
366
+ <div class="mapping-entry">
367
+ <input type="text" class="mapping-from" placeholder="原始模型名称" value="${from}">
368
+ <span class="mapping-arrow">→</span>
369
+ <input type="text" class="mapping-to" placeholder="目标模型名称" value="${to}">
370
+ <button class="delete" onclick="this.parentElement.remove()">删除</button>
371
+ </div>
372
+ `).join('') : ''
373
+ }
374
+ </div>
375
+ <button onclick="addModelMapping(this)">添加模型映射</button>
376
+ </div>
377
+ ` : ''}
378
+ </div>`;
379
+
380
+
381
+ div.innerHTML = basicConfig + detailsConfig;
382
+ container.appendChild(div);
383
+ }
384
+
385
+ async function fetchWithAuth(url, options = {}) {
386
+ const headers = {
387
+ ...options.headers,
388
+ 'Authorization': apiKey
389
+ };
390
+
391
+ const response = await fetch(url, { ...options, headers });
392
+
393
+ if (response.status === 401) {
394
+ localStorage.removeItem('apiKey');
395
+ window.location.reload();
396
+ return null;
397
+ }
398
+
399
+ return response;
400
+ }
401
+
402
+ async function saveConfig() {
403
+ const config = {
404
+ models: getEndpointsConfig('models'),
405
+ chat: getEndpointsConfig('chat'),
406
+ frozen_duration: parseInt(document.getElementById('frozenDuration').value),
407
+ max_retries: parseInt(document.getElementById('maxRetries').value)
408
+ };
409
+
410
+ try {
411
+ const response = await fetchWithAuth('/admin/config', {
412
+ method: 'POST',
413
+ headers: {'Content-Type': 'application/json'},
414
+ body: JSON.stringify(config)
415
+ });
416
+
417
+ if (!response) return;
418
+
419
+ if (response.ok) {
420
+ showStatus('配置已保存');
421
+ refreshEndpoints('models');
422
+ refreshEndpoints('chat');
423
+ } else {
424
+ showStatus('保存失败: ' + await response.text(), true);
425
+ }
426
+ } catch (error) {
427
+ showStatus('保存失败: ' + error.message, true);
428
+ }
429
+ }
430
+
431
+ async function refreshEndpoints(type) {
432
+ try {
433
+ const response = await fetchWithAuth('/admin/status');
434
+ if (!response) return;
435
+
436
+ const status = await response.json();
437
+ const container = document.getElementById(type + 'Endpoints');
438
+ container.innerHTML = '';
439
+
440
+ status[type].endpoints.forEach(ep => {
441
+ addEndpoint(type, ep);
442
+ });
443
+ } catch (error) {
444
+ showStatus('刷新状态失败: ' + error.message, true);
445
+ }
446
+ }
447
+
448
+ async function loadConfig() {
449
+ try {
450
+ const response = await fetchWithAuth('/admin/config');
451
+ if (!response) return;
452
+
453
+ const config = await response.json();
454
+
455
+ document.getElementById('modelsEndpoints').innerHTML = '';
456
+ document.getElementById('chatEndpoints').innerHTML = '';
457
+
458
+ config.models.forEach(ep => {
459
+ addEndpoint('models', ep);
460
+ });
461
+
462
+ config.chat.forEach(ep => {
463
+ addEndpoint('chat', ep);
464
+ });
465
+
466
+ document.getElementById('frozenDuration').value = config.frozen_duration;
467
+ document.getElementById('maxRetries').value = config.max_retries;
468
+
469
+ loadBlacklist();
470
+ } catch (error) {
471
+ showStatus('加载配置失败: ' + error.message, true);
472
+ }
473
+ }
474
+
475
+ async function loadBlacklist() {
476
+ try {
477
+ const response = await fetchWithAuth('/admin/blacklist');
478
+ const blacklist = await response.json();
479
+
480
+ const container = document.getElementById('blacklistEntries');
481
+ container.innerHTML = '';
482
+
483
+ blacklist.forEach(ip => {
484
+ const div = document.createElement('div');
485
+ div.className = 'endpoint';
486
+ div.innerHTML = `
487
+ <span>${ip}</span>
488
+ <button class="delete" onclick="removeFromBlacklist('${ip}')">解除封禁</button>
489
+ `;
490
+ container.appendChild(div);
491
+ });
492
+ } catch (error) {
493
+ showStatus('加载黑名单失败: ' + error.message, true);
494
+ }
495
+ }
496
+
497
+ async function addToBlacklist(ip) {
498
+ if (!ip) return;
499
+
500
+ try {
501
+ const response = await fetchWithAuth('/admin/blacklist', {
502
+ method: 'POST',
503
+ headers: {'Content-Type': 'application/json'},
504
+ body: JSON.stringify({ip: ip, action: 'add'})
505
+ });
506
+
507
+ if (response.ok) {
508
+ showStatus('IP已添加到黑名单');
509
+ document.getElementById('ipInput').value = '';
510
+ loadBlacklist();
511
+ }
512
+ } catch (error) {
513
+ showStatus('添加失败: ' + error.message, true);
514
+ }
515
+ }
516
+
517
+ async function removeFromBlacklist(ip) {
518
+ try {
519
+ const response = await fetchWithAuth('/admin/blacklist', {
520
+ method: 'POST',
521
+ headers: {'Content-Type': 'application/json'},
522
+ body: JSON.stringify({ip: ip, action: 'remove'})
523
+ });
524
+
525
+ if (response.ok) {
526
+ showStatus('IP已从黑名单移除');
527
+ loadBlacklist();
528
+ }
529
+ } catch (error) {
530
+ showStatus('移除失败: ' + error.message, true);
531
+ }
532
+ }
533
+
534
+ function logout() {
535
+ localStorage.removeItem('apiKey');
536
+ window.location.reload();
537
+ }
538
+
539
+ document.addEventListener('DOMContentLoaded', loadConfig);
540
+ </script>
541
+ </body>
542
+ </html>
workers CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:e5c498847caad7c4f6743afda871f8a0b6f9c79e2ebcbf9a6310478cfeb7f2fe
3
- size 8728249
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ddac72cfb17a0ba9c8930573d0f3a87e19e773415be2e3768a495c8a45bc59ee
3
+ size 8761957