Spaces:
XCAPI
/
Sleeping

XCAPI commited on
Commit
6914b41
·
verified ·
1 Parent(s): 8fef30a

Upload 2 files

Browse files
Files changed (2) hide show
  1. admin.html +413 -542
  2. workers +2 -2
admin.html CHANGED
@@ -1,542 +1,413 @@
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>
 
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>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
workers CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:ddac72cfb17a0ba9c8930573d0f3a87e19e773415be2e3768a495c8a45bc59ee
3
- size 8761957
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e5c498847caad7c4f6743afda871f8a0b6f9c79e2ebcbf9a6310478cfeb7f2fe
3
+ size 8728249