youbiaokachi commited on
Commit
bfdde3a
·
verified ·
1 Parent(s): 027b7b1

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +16 -0
  2. app.py +0 -0
  3. templates/login.html +77 -0
  4. templates/manager.html +835 -0
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM mcr.microsoft.com/playwright/python:v1.44.0-jammy
2
+
3
+ WORKDIR /app
4
+
5
+ RUN pip install --no-cache-dir flask requests curl_cffi werkzeug loguru python-dotenv patchright
6
+
7
+ RUN python -m patchright install --with-deps chrome
8
+
9
+ COPY . .
10
+
11
+ ENV PORT=5200
12
+ ENV PYTHONUNBUFFERED=1
13
+
14
+ EXPOSE 5200
15
+
16
+ CMD ["python", "app.py"]
app.py ADDED
The diff for this file is too large to render. See raw diff
 
templates/login.html ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>登录</title>
6
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
7
+ <style>
8
+ :root {
9
+ --bg-primary: #f4f6f9;
10
+ --card-bg: #ffffff;
11
+ --text-primary: #2c3e50;
12
+ --text-secondary: #6c757d;
13
+ --border-color: #e2e8f0;
14
+ --primary-color: #3498db;
15
+ }
16
+ * { box-sizing: border-box; margin: 0; padding: 0; }
17
+ body {
18
+ font-family: 'Inter', sans-serif;
19
+ background-color: var(--bg-primary);
20
+ color: var(--text-primary);
21
+ display: flex;
22
+ justify-content: center;
23
+ align-items: center;
24
+ height: 100vh;
25
+ }
26
+ .login-form {
27
+ background: var(--card-bg);
28
+ padding: 40px;
29
+ border-radius: 16px;
30
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
31
+ width: 100%;
32
+ max-width: 400px;
33
+ }
34
+ h2 {
35
+ text-align: center;
36
+ margin-bottom: 20px;
37
+ color: var(--text-primary);
38
+ }
39
+ input {
40
+ width: 100%;
41
+ padding: 12px 15px;
42
+ margin: 10px 0;
43
+ border: 1px solid var(--border-color);
44
+ border-radius: 8px;
45
+ font-size: 16px;
46
+ }
47
+ button {
48
+ width: 100%;
49
+ padding: 12px;
50
+ background-color: var(--primary-color);
51
+ color: white;
52
+ border: none;
53
+ border-radius: 8px;
54
+ cursor: pointer;
55
+ transition: background-color 0.3s;
56
+ }
57
+ button:hover { background-color: #2980b9; }
58
+ </style>
59
+ </head>
60
+ <body>
61
+ <div class="login-form">
62
+ <h2>管理员登录</h2>
63
+ <form action="/manager/login" method="post">
64
+ <input type="password" name="password" placeholder="输入管理员密码" required>
65
+ <button type="submit">登录</button>
66
+ </form>
67
+ </div>
68
+ {% if error %}
69
+ <div id="notification" style="position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background-color: #f44336; color: white; padding: 10px 20px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); display: block; z-index: 1000;">密码错误</div>
70
+ <script>
71
+ setTimeout(() => {
72
+ document.getElementById('notification').style.display = 'none';
73
+ }, 2000);
74
+ </script>
75
+ {% endif %}
76
+ </body>
77
+ </html>
templates/manager.html ADDED
@@ -0,0 +1,835 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <title>Grok Token 管理面板</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
8
+ <style>
9
+ :root {
10
+ --bg-primary: #f4f6f9;
11
+ --card-bg: #ffffff;
12
+ --text-primary: #2c3e50;
13
+ --text-secondary: #6c757d;
14
+ --border-color: #e2e8f0;
15
+ --primary-color: #3498db;
16
+ --success-color: #4caf50;
17
+ --danger-color: #f44336;
18
+ --warning-color: #ff9800;
19
+ --expired-color: #9e42db;
20
+ }
21
+
22
+ * {
23
+ box-sizing: border-box;
24
+ margin: 0;
25
+ padding: 0;
26
+ }
27
+
28
+ body {
29
+ font-family: 'Inter', sans-serif;
30
+ background-color: var(--bg-primary);
31
+ color: var(--text-primary);
32
+ line-height: 1.6;
33
+ }
34
+
35
+ .container {
36
+ max-width: 1400px;
37
+ margin: 0 auto;
38
+ padding: 20px;
39
+ }
40
+
41
+ .search-section {
42
+ margin-bottom: 20px;
43
+ }
44
+
45
+ #searchInput {
46
+ width: 100%;
47
+ padding: 10px;
48
+ border: 1px solid var(--border-color);
49
+ border-radius: 8px;
50
+ font-size: 16px;
51
+ }
52
+
53
+ .overview-panel {
54
+ background-color: var(--card-bg);
55
+ border-radius: 16px;
56
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
57
+ padding: 25px;
58
+ margin-bottom: 25px;
59
+ }
60
+
61
+ .header-actions {
62
+ display: flex;
63
+ justify-content: space-between;
64
+ align-items: center;
65
+ margin-bottom: 20px;
66
+ }
67
+
68
+ .overview-stats {
69
+ display: grid;
70
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
71
+ gap: 20px;
72
+ width: 100%;
73
+ margin-bottom: 20px;
74
+ }
75
+
76
+ .stat-item {
77
+ text-align: center;
78
+ }
79
+
80
+ .stat-value {
81
+ font-size: 28px;
82
+ font-weight: 600;
83
+ color: var(--primary-color);
84
+ }
85
+
86
+ .stat-label {
87
+ font-size: 14px;
88
+ color: var(--text-secondary);
89
+ }
90
+
91
+ .health-summary {
92
+ display: grid;
93
+ grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
94
+ gap: 15px;
95
+ padding: 15px;
96
+ background-color: #f8f9fa;
97
+ border-radius: 8px;
98
+ margin-bottom: 20px;
99
+ }
100
+
101
+ .health-stat {
102
+ text-align: center;
103
+ }
104
+
105
+ .health-stat .value {
106
+ font-size: 24px;
107
+ font-weight: 600;
108
+ }
109
+
110
+ .health-stat .label {
111
+ font-size: 12px;
112
+ color: var(--text-secondary);
113
+ margin-top: 5px;
114
+ }
115
+
116
+ .health-stat.healthy .value {
117
+ color: var(--success-color);
118
+ }
119
+
120
+ .health-stat.expired .value {
121
+ color: var(--expired-color);
122
+ }
123
+
124
+ .health-stat.rate-limited .value {
125
+ color: var(--warning-color);
126
+ }
127
+
128
+ .health-stat.failures .value {
129
+ color: var(--danger-color);
130
+ }
131
+
132
+ .model-remaining-stats {
133
+ display: grid;
134
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
135
+ gap: 15px;
136
+ }
137
+
138
+ .model-stat {
139
+ display: flex;
140
+ flex-direction: column;
141
+ align-items: center;
142
+ padding: 10px;
143
+ background-color: #f8f9fa;
144
+ border-radius: 8px;
145
+ }
146
+
147
+ .model-stat .stat-label {
148
+ font-size: 12px;
149
+ color: var(--text-secondary);
150
+ margin-bottom: 5px;
151
+ text-align: center;
152
+ }
153
+
154
+ .model-stat .stat-value {
155
+ font-size: 18px;
156
+ font-weight: 600;
157
+ color: var(--primary-color);
158
+ }
159
+
160
+ .refresh-icon {
161
+ cursor: pointer;
162
+ transition: transform 0.3s;
163
+ }
164
+
165
+ .refresh-icon:hover {
166
+ transform: rotate(180deg);
167
+ }
168
+
169
+ .token-management-section {
170
+ display: grid;
171
+ grid-template-columns: 1fr 1fr;
172
+ gap: 20px;
173
+ margin-bottom: 25px;
174
+ }
175
+
176
+ .token-management-section>div {
177
+ background-color: var(--card-bg);
178
+ border-radius: 16px;
179
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
180
+ padding: 25px;
181
+ }
182
+
183
+ .token-management-section h3 {
184
+ margin-bottom: 15px;
185
+ font-size: 16px;
186
+ color: var(--text-secondary);
187
+ }
188
+
189
+ .token-input-section {
190
+ display: flex;
191
+ gap: 15px;
192
+ }
193
+
194
+ #tokenInput,
195
+ #cfInput {
196
+ flex-grow: 1;
197
+ padding: 12px 15px;
198
+ border: 1px solid var(--border-color);
199
+ border-radius: 8px;
200
+ font-size: 16px;
201
+ }
202
+
203
+ #addTokenBtn,
204
+ #setCfBtn {
205
+ padding: 12px 25px;
206
+ background-color: var(--primary-color);
207
+ color: white;
208
+ border: none;
209
+ border-radius: 8px;
210
+ cursor: pointer;
211
+ transition: background-color 0.3s;
212
+ }
213
+
214
+ #addTokenBtn:hover,
215
+ #setCfBtn:hover {
216
+ background-color: #2980b9;
217
+ }
218
+
219
+ .token-grid {
220
+ display: grid;
221
+ grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
222
+ gap: 20px;
223
+ }
224
+
225
+ .token-card {
226
+ background-color: var(--card-bg);
227
+ border-radius: 16px;
228
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
229
+ padding: 20px;
230
+ transition: transform 0.3s;
231
+ }
232
+
233
+ .token-card:hover {
234
+ transform: translateY(-5px);
235
+ }
236
+
237
+ .token-header {
238
+ display: flex;
239
+ justify-content: space-between;
240
+ align-items: center;
241
+ margin-bottom: 15px;
242
+ padding-bottom: 10px;
243
+ border-bottom: 1px solid var(--border-color);
244
+ }
245
+
246
+ .token-title {
247
+ font-size: 14px;
248
+ font-weight: 500;
249
+ max-width: 250px;
250
+ overflow: hidden;
251
+ text-overflow: ellipsis;
252
+ white-space: nowrap;
253
+ }
254
+
255
+ .delete-btn {
256
+ background-color: var(--danger-color);
257
+ color: white;
258
+ border: none;
259
+ padding: 5px 10px;
260
+ border-radius: 4px;
261
+ cursor: pointer;
262
+ font-size: 12px;
263
+ }
264
+
265
+ .delete-btn:hover {
266
+ background-color: #c0392b;
267
+ }
268
+
269
+ .token-health-info {
270
+ display: flex;
271
+ justify-content: space-between;
272
+ align-items: center;
273
+ margin-bottom: 10px;
274
+ padding: 8px;
275
+ background-color: #f8f9fa;
276
+ border-radius: 6px;
277
+ font-size: 12px;
278
+ }
279
+
280
+ .token-health-info .health-indicator {
281
+ display: flex;
282
+ align-items: center;
283
+ gap: 5px;
284
+ }
285
+
286
+ .health-dot {
287
+ width: 8px;
288
+ height: 8px;
289
+ border-radius: 50%;
290
+ }
291
+
292
+ .health-dot.healthy {
293
+ background-color: var(--success-color);
294
+ }
295
+
296
+ .health-dot.expired {
297
+ background-color: var(--expired-color);
298
+ }
299
+
300
+ .health-dot.rate-limited {
301
+ background-color: var(--warning-color);
302
+ }
303
+
304
+ .model-row {
305
+ display: flex;
306
+ align-items: center;
307
+ margin-bottom: 10px;
308
+ gap: 15px;
309
+ }
310
+
311
+ .model-name {
312
+ flex: 2;
313
+ font-size: 14px;
314
+ color: var(--text-secondary);
315
+ }
316
+
317
+ .progress-container {
318
+ flex: 6;
319
+ display: flex;
320
+ align-items: center;
321
+ gap: 10px;
322
+ }
323
+
324
+ .progress-bar {
325
+ flex-grow: 1;
326
+ height: 8px;
327
+ background-color: #e0e0e0;
328
+ border-radius: 4px;
329
+ overflow: hidden;
330
+ }
331
+
332
+ .progress-bar-fill {
333
+ height: 100%;
334
+ transition: width 0.5s ease;
335
+ }
336
+
337
+ .progress-text {
338
+ font-size: 12px;
339
+ color: var(--text-secondary);
340
+ min-width: 50px;
341
+ text-align: right;
342
+ }
343
+
344
+ .status-badge {
345
+ font-size: 12px;
346
+ padding: 3px 8px;
347
+ border-radius: 12px;
348
+ font-weight: 600;
349
+ position: relative;
350
+ cursor: help;
351
+ }
352
+
353
+ .status-badge .tooltip {
354
+ visibility: hidden;
355
+ position: absolute;
356
+ z-index: 1;
357
+ bottom: 125%;
358
+ left: 50%;
359
+ transform: translateX(-50%);
360
+ background-color: rgba(0, 0, 0, 0.8);
361
+ color: white;
362
+ text-align: center;
363
+ border-radius: 6px;
364
+ padding: 5px 10px;
365
+ opacity: 0;
366
+ transition: opacity 0.3s;
367
+ white-space: nowrap;
368
+ }
369
+
370
+ .status-badge:hover .tooltip {
371
+ visibility: visible;
372
+ opacity: 1;
373
+ }
374
+
375
+ .status-active {
376
+ background-color: rgba(76, 175, 80, 0.1);
377
+ color: var(--success-color);
378
+ }
379
+
380
+ .status-expired {
381
+ background-color: rgba(158, 66, 219, 0.1);
382
+ color: var(--expired-color);
383
+ }
384
+
385
+ .status-rate-limited {
386
+ background-color: rgba(244, 67, 54, 0.1);
387
+ color: var(--danger-color);
388
+ }
389
+
390
+ .failure-info {
391
+ font-size: 11px;
392
+ color: var(--danger-color);
393
+ margin-top: 2px;
394
+ }
395
+
396
+ #notification {
397
+ position: fixed;
398
+ top: 20px;
399
+ left: 50%;
400
+ transform: translateX(-50%);
401
+ background-color: #3498db;
402
+ color: white;
403
+ padding: 10px 20px;
404
+ border-radius: 8px;
405
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
406
+ display: none;
407
+ z-index: 1000;
408
+ }
409
+ </style>
410
+ </head>
411
+
412
+ <body>
413
+ <div class="container">
414
+ <div class="search-section">
415
+ <input type="text" id="searchInput" placeholder="搜索 Token...">
416
+ </div>
417
+
418
+ <div class="overview-panel">
419
+ <div class="header-actions">
420
+ <h2>Token 管理面板</h2>
421
+ <div class="refresh-icon" id="refreshTokens">
422
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
423
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
424
+ <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
425
+ <path d="M21 3v5h-5" />
426
+ <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
427
+ <path d="M3 21v-5h5" />
428
+ </svg>
429
+ </div>
430
+ </div>
431
+
432
+ <div class="overview-stats">
433
+ <div class="stat-item">
434
+ <div class="stat-value" id="totalTokens">0</div>
435
+ <div class="stat-label">Token 总数</div>
436
+ </div>
437
+ </div>
438
+
439
+ <div class="health-summary">
440
+ <div class="health-stat healthy">
441
+ <div class="value" id="healthyTokens">0</div>
442
+ <div class="label">健康</div>
443
+ </div>
444
+ <div class="health-stat expired">
445
+ <div class="value" id="expiredTokens">0</div>
446
+ <div class="label">已过期</div>
447
+ </div>
448
+ <div class="health-stat rate-limited">
449
+ <div class="value" id="rateLimitedTokens">0</div>
450
+ <div class="label">限流中</div>
451
+ </div>
452
+ <div class="health-stat failures">
453
+ <div class="value" id="tokensWithFailures">0</div>
454
+ <div class="label">有失败</div>
455
+ </div>
456
+ <div class="health-stat failures">
457
+ <div class="value" id="totalFailures">0</div>
458
+ <div class="label">总失败数</div>
459
+ </div>
460
+ </div>
461
+
462
+ <div class="model-remaining-stats">
463
+ <div class="model-stat">
464
+ <div class="stat-label">grok-3 可用次数</div>
465
+ <div class="stat-value" id="grok-3-count">0</div>
466
+ </div>
467
+ <div class="model-stat">
468
+ <div class="stat-label">grok-3-deepsearch 可用次数</div>
469
+ <div class="stat-value" id="grok-3-deepsearch-count">0</div>
470
+ </div>
471
+ <div class="model-stat">
472
+ <div class="stat-label">grok-3-deepersearch 可用次数</div>
473
+ <div class="stat-value" id="grok-3-deepersearch-count">0</div>
474
+ </div>
475
+ <div class="model-stat">
476
+ <div class="stat-label">grok-3-reasoning 可用次数</div>
477
+ <div class="stat-value" id="grok-3-reasoning-count">0</div>
478
+ </div>
479
+ <div class="model-stat">
480
+ <div class="stat-label">grok-4 可用次数</div>
481
+ <div class="stat-value" id="grok-4-count">0</div>
482
+ </div>
483
+ </div>
484
+ </div>
485
+
486
+ <div class="token-management-section">
487
+ <div class="token-input-container">
488
+ <h3>添加 SSO Token</h3>
489
+ <div class="token-input-section">
490
+ <input type="text" id="tokenInput" placeholder="输入新的 SSO Token">
491
+ <button id="addTokenBtn">添加 Token</button>
492
+ </div>
493
+ </div>
494
+
495
+ <div class="cf-input-container">
496
+ <h3>设置 CF Clearance</h3>
497
+ <div class="token-input-section">
498
+ <input type="text" id="cfInput" placeholder="输入 CF Clearance">
499
+ <button id="setCfBtn">设置 CF</button>
500
+ </div>
501
+ </div>
502
+ </div>
503
+
504
+ <div id="tokenGrid" class="token-grid"></div>
505
+ </div>
506
+
507
+ <div id="notification"></div>
508
+
509
+ <script>
510
+
511
+ const modelConfig = {
512
+ "grok-3": { RequestFrequency: 20, ExpirationTime: 7200000 },
513
+ "grok-3-deepsearch": { RequestFrequency: 10, ExpirationTime: 86400000 },
514
+ "grok-3-deepersearch": { RequestFrequency: 3, ExpirationTime: 86400000 },
515
+ "grok-3-reasoning": { RequestFrequency: 8, ExpirationTime: 86400000 },
516
+ "grok-4": { RequestFrequency: 5, ExpirationTime: 11.5 * 60 * 60 * 1000 }
517
+ };
518
+
519
+ let tokenMap = {};
520
+ let healthSummary = {};
521
+
522
+
523
+ function getProgressColor(percentage) {
524
+ if (percentage > 70) return 'var(--danger-color)';
525
+ if (percentage > 30) return 'var(--warning-color)';
526
+ return 'var(--success-color)';
527
+ }
528
+
529
+ function calculateModelRemaining() {
530
+ const modelRemaining = {};
531
+ Object.keys(modelConfig).forEach(modelName => {
532
+ const maxRequests = modelConfig[modelName].RequestFrequency;
533
+ modelRemaining[modelName] = 0;
534
+
535
+ Object.values(tokenMap).forEach(tokenData => {
536
+ const modelData = tokenData[modelName];
537
+ if (modelData && modelData.isValid && !modelData.is_expired) {
538
+ const remaining = Math.max(0, maxRequests - (modelData.request_count || modelData.totalRequestCount || 0));
539
+ modelRemaining[modelName] += remaining;
540
+ }
541
+ });
542
+ });
543
+ return modelRemaining;
544
+ }
545
+
546
+ function updateTokenCounters() {
547
+ document.getElementById('totalTokens').textContent = Object.keys(tokenMap).length;
548
+
549
+ if (healthSummary) {
550
+ document.getElementById('healthyTokens').textContent = healthSummary.healthy_tokens || 0;
551
+ document.getElementById('expiredTokens').textContent = healthSummary.expired_tokens || 0;
552
+ document.getElementById('rateLimitedTokens').textContent = healthSummary.rate_limited_tokens || 0;
553
+ document.getElementById('tokensWithFailures').textContent = healthSummary.tokens_with_failures || 0;
554
+ document.getElementById('totalFailures').textContent = healthSummary.total_failures || 0;
555
+ }
556
+
557
+ const modelRemaining = calculateModelRemaining();
558
+
559
+ const modelIds = ['grok-3', 'grok-3-deepsearch', 'grok-3-deepersearch', 'grok-3-reasoning', 'grok-4'];
560
+ modelIds.forEach(modelName => {
561
+ const countElement = document.getElementById(`${modelName}-count`);
562
+ if (countElement) {
563
+ countElement.textContent = modelRemaining[modelName] || 0;
564
+ }
565
+ });
566
+ }
567
+
568
+
569
+ async function updateExpiredTokenTimers() {
570
+ const currentTime = Date.now();
571
+ const expiredBadges = document.querySelectorAll('.status-badge.status-rate-limited');
572
+ for (const badge of expiredBadges) {
573
+ const invalidatedTime = parseInt(badge.getAttribute('data-invalidated-time'), 10);
574
+ const expirationTime = parseInt(badge.getAttribute('data-expiration-time'), 10);
575
+ if (invalidatedTime && expirationTime) {
576
+ const recoveryTime = invalidatedTime + expirationTime;
577
+ const remainingTime = recoveryTime - currentTime;
578
+ const tooltip = badge.querySelector('.tooltip');
579
+ if (tooltip) {
580
+ if (remainingTime > 0) {
581
+ const minutes = Math.floor(remainingTime / 60000);
582
+ const seconds = Math.floor((remainingTime % 60000) / 1000);
583
+ tooltip.textContent = `${minutes}分${seconds}秒后恢复`;
584
+ } else {
585
+ tooltip.textContent = '已可恢复';
586
+ await fetchTokenMap();
587
+ }
588
+ }
589
+ }
590
+ }
591
+ }
592
+
593
+ function getTokenHealthStatus(ssoValue, tokenData) {
594
+ let isExpired = false;
595
+ let isRateLimited = false;
596
+ let hasFailures = false;
597
+ let totalFailures = 0;
598
+
599
+ Object.entries(tokenData).forEach(([model, modelData]) => {
600
+ if (modelData.is_expired) isExpired = true;
601
+ if (!modelData.isValid && !modelData.is_expired) isRateLimited = true;
602
+ if (modelData.failed_request_count > 0) {
603
+ hasFailures = true;
604
+ totalFailures += modelData.failed_request_count;
605
+ }
606
+ });
607
+
608
+ if (isExpired) return { status: 'expired', text: '已过期', failures: totalFailures };
609
+ if (isRateLimited) return { status: 'rate-limited', text: '限流中', failures: totalFailures };
610
+ if (hasFailures) return { status: 'healthy', text: '健康', failures: totalFailures };
611
+ return { status: 'healthy', text: '健康', failures: 0 };
612
+ }
613
+
614
+
615
+ function renderTokens() {
616
+ const tokenGrid = document.getElementById('tokenGrid');
617
+ tokenGrid.innerHTML = '';
618
+ Object.entries(tokenMap).forEach(([token, tokenData]) => {
619
+ const tokenCard = document.createElement('div');
620
+ tokenCard.className = 'token-card';
621
+ tokenCard.setAttribute('data-token', token);
622
+
623
+ const tokenHeader = document.createElement('div');
624
+ tokenHeader.className = 'token-header';
625
+ const tokenTitle = document.createElement('div');
626
+ tokenTitle.className = 'token-title';
627
+ tokenTitle.textContent = token;
628
+ tokenTitle.title = token;
629
+ tokenTitle.style.cursor = 'pointer';
630
+ tokenTitle.addEventListener('click', () => {
631
+ navigator.clipboard.writeText(token).then(() => {
632
+ showNotification('Token 已复制');
633
+ }).catch(err => {
634
+ showNotification('复制失败');
635
+ });
636
+ });
637
+ const deleteBtn = document.createElement('button');
638
+ deleteBtn.className = 'delete-btn';
639
+ deleteBtn.textContent = '删除';
640
+ deleteBtn.addEventListener('click', async () => {
641
+ if (confirm(`确认删除 token: ${token}?`)) {
642
+ try {
643
+ const response = await fetch('/manager/api/delete', {
644
+ method: 'POST',
645
+ headers: { 'Content-Type': 'application/json' },
646
+ body: JSON.stringify({ sso: token })
647
+ });
648
+ if (response.ok) {
649
+ await fetchTokenMap();
650
+ showNotification('Token 删除成功');
651
+ } else {
652
+ showNotification('删除 Token 失败');
653
+ }
654
+ } catch (error) {
655
+ showNotification('删除 Token 出错');
656
+ }
657
+ }
658
+ });
659
+ tokenHeader.appendChild(tokenTitle);
660
+ tokenHeader.appendChild(deleteBtn);
661
+ tokenCard.appendChild(tokenHeader);
662
+
663
+ const healthStatus = getTokenHealthStatus(token, tokenData);
664
+ const tokenHealthInfo = document.createElement('div');
665
+ tokenHealthInfo.className = 'token-health-info';
666
+ tokenHealthInfo.innerHTML = `
667
+ <div class="health-indicator">
668
+ <div class="health-dot ${healthStatus.status}"></div>
669
+ <span>${healthStatus.text}</span>
670
+ </div>
671
+ <div>失败次数: ${healthStatus.failures}</div>
672
+ `;
673
+ tokenCard.appendChild(tokenHealthInfo);
674
+
675
+ Object.entries(modelConfig).forEach(([modelName, config]) => {
676
+ const modelRow = document.createElement('div');
677
+ modelRow.className = 'model-row';
678
+ const modelNameSpan = document.createElement('div');
679
+ modelNameSpan.className = 'model-name';
680
+ modelNameSpan.textContent = modelName;
681
+ const progressContainer = document.createElement('div');
682
+ progressContainer.className = 'progress-container';
683
+ const progressBar = document.createElement('div');
684
+ progressBar.className = 'progress-bar';
685
+ const progressBarFill = document.createElement('div');
686
+ progressBarFill.className = 'progress-bar-fill';
687
+ const modelData = tokenData[modelName];
688
+ if (!modelData) {
689
+ return;
690
+ }
691
+ const requestCount = modelData.request_count || modelData.totalRequestCount || 0;
692
+ const maxRequests = config.RequestFrequency;
693
+ const percentage = (requestCount / maxRequests) * 100;
694
+ progressBarFill.style.width = `${percentage}%`;
695
+ progressBarFill.style.backgroundColor = getProgressColor(percentage);
696
+ progressBar.appendChild(progressBarFill);
697
+ const progressText = document.createElement('div');
698
+ progressText.className = 'progress-text';
699
+ progressText.textContent = `${requestCount}/${maxRequests}`;
700
+
701
+ const statusBadge = document.createElement('div');
702
+ statusBadge.className = 'status-badge';
703
+
704
+ if (modelData.is_expired) {
705
+ statusBadge.classList.add('status-expired');
706
+ statusBadge.textContent = '已过期';
707
+ const tooltip = document.createElement('div');
708
+ tooltip.className = 'tooltip';
709
+ tooltip.textContent = `失败次数: ${modelData.failed_request_count || 0}`;
710
+ statusBadge.appendChild(tooltip);
711
+ } else if (!modelData.isValid) {
712
+ statusBadge.classList.add('status-rate-limited');
713
+ statusBadge.textContent = '限流';
714
+ statusBadge.setAttribute('data-invalidated-time', modelData.invalidatedTime);
715
+ statusBadge.setAttribute('data-expiration-time', config.ExpirationTime);
716
+ const tooltip = document.createElement('div');
717
+ tooltip.className = 'tooltip';
718
+ statusBadge.appendChild(tooltip);
719
+ } else {
720
+ statusBadge.classList.add('status-active');
721
+ statusBadge.textContent = '活跃';
722
+ }
723
+
724
+ progressContainer.appendChild(progressBar);
725
+ progressContainer.appendChild(progressText);
726
+ modelRow.appendChild(modelNameSpan);
727
+ modelRow.appendChild(progressContainer);
728
+ modelRow.appendChild(statusBadge);
729
+
730
+
731
+ if (modelData.failed_request_count > 0) {
732
+ const failureInfo = document.createElement('div');
733
+ failureInfo.className = 'failure-info';
734
+ failureInfo.textContent = `失败 ${modelData.failed_request_count} 次`;
735
+ if (modelData.last_failure_reason) {
736
+ failureInfo.title = `最后失败: ${modelData.last_failure_reason}`;
737
+ }
738
+ modelRow.appendChild(failureInfo);
739
+ }
740
+
741
+ tokenCard.appendChild(modelRow);
742
+ });
743
+ tokenGrid.appendChild(tokenCard);
744
+ });
745
+ updateTokenCounters();
746
+ }
747
+
748
+ async function fetchTokenMap() {
749
+ try {
750
+ const response = await fetch('/manager/api/get');
751
+ if (!response.ok) throw new Error('获取 Token 失败');
752
+ const data = await response.json();
753
+ tokenMap = data.token_status || data;
754
+ healthSummary = data.health_summary || {};
755
+ renderTokens();
756
+ } catch (error) {
757
+ showNotification('获取 Token 出错');
758
+ }
759
+ }
760
+
761
+ document.getElementById('addTokenBtn').addEventListener('click', async () => {
762
+ const tokenInput = document.getElementById('tokenInput');
763
+ const newToken = tokenInput.value.trim();
764
+ if (newToken) {
765
+ try {
766
+ const response = await fetch('/manager/api/add', {
767
+ method: 'POST',
768
+ headers: { 'Content-Type': 'application/json' },
769
+ body: JSON.stringify({ sso: newToken })
770
+ });
771
+ if (response.ok) {
772
+ tokenInput.value = '';
773
+ await fetchTokenMap();
774
+ showNotification('Token 添加成功');
775
+ } else {
776
+ showNotification('添加 Token 失败');
777
+ }
778
+ } catch (error) {
779
+ showNotification('添加 Token 出错');
780
+ }
781
+ }
782
+ });
783
+
784
+ document.getElementById('setCfBtn').addEventListener('click', async () => {
785
+ const cfInput = document.getElementById('cfInput');
786
+ const newCf = cfInput.value.trim();
787
+ if (newCf) {
788
+ try {
789
+ const response = await fetch('/manager/api/cf_clearance', {
790
+ method: 'POST',
791
+ headers: { 'Content-Type': 'application/json' },
792
+ body: JSON.stringify({ cf_clearance: newCf })
793
+ });
794
+ if (response.ok) {
795
+ cfInput.value = '';
796
+ showNotification('CF Clearance 设置成功');
797
+ } else {
798
+ showNotification('设置 CF Clearance 失败');
799
+ }
800
+ } catch (error) {
801
+ showNotification('设置 CF Clearance 出错');
802
+ }
803
+ }
804
+ });
805
+
806
+ document.getElementById('searchInput').addEventListener('input', (e) => {
807
+ const searchTerm = e.target.value.toLowerCase();
808
+ const tokenCards = document.querySelectorAll('.token-card');
809
+ tokenCards.forEach(card => {
810
+ const token = card.getAttribute('data-token').toLowerCase();
811
+ card.style.display = token.includes(searchTerm) ? 'block' : 'none';
812
+ });
813
+ });
814
+
815
+ document.getElementById('refreshTokens').addEventListener('click', async () => {
816
+ await fetchTokenMap();
817
+ showNotification('Token 列表已刷新');
818
+ });
819
+
820
+ fetchTokenMap();
821
+
822
+ setInterval(updateExpiredTokenTimers, 1000);
823
+
824
+ function showNotification(message) {
825
+ const notification = document.getElementById('notification');
826
+ notification.textContent = message;
827
+ notification.style.display = 'block';
828
+ setTimeout(() => {
829
+ notification.style.display = 'none';
830
+ }, 2000);
831
+ }
832
+ </script>
833
+ </body>
834
+
835
+ </html>