pigracing commited on
Commit
898397c
·
verified ·
1 Parent(s): bd27f3c

Upload 21 files

Browse files
Files changed (3) hide show
  1. src/admin/index.html +753 -655
  2. src/auth.js +7 -16
  3. src/index.js +154 -154
src/admin/index.html CHANGED
@@ -1,655 +1,753 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>DeepSeek-2API 管理面板</title>
7
- <style>
8
- :root {
9
- --bg: #1a1a2e;
10
- --surface: #16213e;
11
- --border: #0f3460;
12
- --text: #e0e0e0;
13
- --text-dim: #8892a0;
14
- --green: #00e676;
15
- --yellow: #ffd600;
16
- --red: #ff1744;
17
- --accent: #00b0ff;
18
- }
19
- * { margin: 0; padding: 0; box-sizing: border-box; }
20
- body {
21
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
22
- background: var(--bg);
23
- color: var(--text);
24
- line-height: 1.6;
25
- min-height: 100vh;
26
- }
27
- .container {
28
- max-width: 960px;
29
- margin: 0 auto;
30
- padding: 20px;
31
- }
32
- header {
33
- display: flex;
34
- align-items: center;
35
- justify-content: space-between;
36
- padding: 16px 20px;
37
- background: var(--surface);
38
- border: 1px solid var(--border);
39
- border-radius: 8px;
40
- margin-bottom: 20px;
41
- }
42
- header h1 {
43
- font-size: 1.3em;
44
- color: var(--accent);
45
- }
46
- header .meta {
47
- font-size: 0.85em;
48
- color: var(--text-dim);
49
- }
50
- .controls {
51
- display: flex;
52
- align-items: center;
53
- gap: 8px;
54
- }
55
- .controls select, .controls button {
56
- background: var(--bg);
57
- color: var(--text);
58
- border: 1px solid var(--border);
59
- border-radius: 4px;
60
- padding: 4px 10px;
61
- font-size: 0.85em;
62
- cursor: pointer;
63
- }
64
- .controls button:hover { border-color: var(--accent); }
65
- .card {
66
- background: var(--surface);
67
- border: 1px solid var(--border);
68
- border-radius: 8px;
69
- padding: 16px 20px;
70
- margin-bottom: 16px;
71
- }
72
- .card h2 {
73
- font-size: 1.05em;
74
- margin-bottom: 12px;
75
- color: var(--accent);
76
- }
77
- .stats-grid {
78
- display: grid;
79
- grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
80
- gap: 12px;
81
- }
82
- .stat-item {
83
- background: var(--bg);
84
- border-radius: 6px;
85
- padding: 12px;
86
- text-align: center;
87
- }
88
- .stat-item .label {
89
- font-size: 0.75em;
90
- color: var(--text-dim);
91
- margin-bottom: 4px;
92
- }
93
- .stat-item .value {
94
- font-size: 1.4em;
95
- font-weight: 700;
96
- }
97
- .status-ok { color: var(--green); }
98
- .status-warn { color: var(--yellow); }
99
- .status-err { color: var(--red); }
100
-
101
- table {
102
- width: 100%;
103
- border-collapse: collapse;
104
- font-size: 0.88em;
105
- }
106
- th, td {
107
- padding: 8px 10px;
108
- text-align: left;
109
- border-bottom: 1px solid var(--border);
110
- }
111
- th {
112
- color: var(--text-dim);
113
- font-weight: 600;
114
- font-size: 0.8em;
115
- text-transform: uppercase;
116
- letter-spacing: 0.5px;
117
- }
118
- td { font-family: 'SF Mono', 'Consolas', monospace; font-size: 0.85em; }
119
- .dot {
120
- display: inline-block;
121
- width: 8px;
122
- height: 8px;
123
- border-radius: 50%;
124
- margin-right: 6px;
125
- }
126
- .dot-green { background: var(--green); }
127
- .dot-yellow { background: var(--yellow); }
128
- .dot-red { background: var(--red); }
129
-
130
- .bar-wrap {
131
- background: var(--bg);
132
- border-radius: 4px;
133
- height: 22px;
134
- overflow: hidden;
135
- position: relative;
136
- }
137
- .bar-fill {
138
- height: 100%;
139
- border-radius: 4px;
140
- transition: width 0.4s, background 0.4s;
141
- }
142
- .bar-label {
143
- position: absolute;
144
- top: 0; left: 10px; right: 10px;
145
- line-height: 22px;
146
- font-size: 0.78em;
147
- color: var(--text);
148
- text-shadow: 0 1px 2px rgba(0,0,0,0.6);
149
- }
150
-
151
- .forms-row {
152
- display: grid;
153
- grid-template-columns: 1fr 1fr;
154
- gap: 16px;
155
- }
156
- @media (max-width: 640px) {
157
- .forms-row { grid-template-columns: 1fr; }
158
- }
159
- .form-group { margin-bottom: 10px; }
160
- .form-group label {
161
- display: block;
162
- font-size: 0.82em;
163
- color: var(--text-dim);
164
- margin-bottom: 4px;
165
- }
166
- .form-group input {
167
- width: 100%;
168
- background: var(--bg);
169
- color: var(--text);
170
- border: 1px solid var(--border);
171
- border-radius: 4px;
172
- padding: 7px 10px;
173
- font-size: 0.9em;
174
- }
175
- .form-group input:focus { outline: none; border-color: var(--accent); }
176
- .btn {
177
- background: var(--accent);
178
- color: #fff;
179
- border: none;
180
- border-radius: 4px;
181
- padding: 7px 18px;
182
- font-size: 0.9em;
183
- cursor: pointer;
184
- }
185
- .btn:hover { opacity: 0.85; }
186
- .msg {
187
- font-size: 0.82em;
188
- margin-top: 6px;
189
- min-height: 1.4em;
190
- }
191
- .msg-ok { color: var(--green); }
192
- .msg-err { color: var(--red); }
193
-
194
- details {
195
- margin-bottom: 10px;
196
- }
197
- summary {
198
- cursor: pointer;
199
- color: var(--accent);
200
- font-size: 0.92em;
201
- padding: 4px 0;
202
- }
203
- pre {
204
- background: var(--bg);
205
- border: 1px solid var(--border);
206
- border-radius: 4px;
207
- padding: 10px 12px;
208
- font-size: 0.82em;
209
- overflow-x: auto;
210
- color: var(--text);
211
- margin: 6px 0;
212
- }
213
- .model-table {
214
- width: auto;
215
- margin-bottom: 12px;
216
- }
217
- .model-table td, .model-table th {
218
- padding: 6px 16px;
219
- }
220
-
221
- footer {
222
- text-align: center;
223
- color: var(--text-dim);
224
- font-size: 0.78em;
225
- padding: 12px;
226
- }
227
-
228
- .auth-overlay {
229
- position: fixed;
230
- inset: 0;
231
- background: rgba(0,0,0,0.7);
232
- display: flex;
233
- align-items: center;
234
- justify-content: center;
235
- z-index: 100;
236
- }
237
- .auth-box {
238
- background: var(--surface);
239
- border: 1px solid var(--border);
240
- border-radius: 8px;
241
- padding: 24px;
242
- width: 340px;
243
- }
244
- .auth-box h3 { margin-bottom: 12px; color: var(--accent); }
245
- .auth-box .form-group { margin-bottom: 14px; }
246
- .auth-box .msg { margin-bottom: 8px; }
247
-
248
- .loading { color: var(--text-dim); font-style: italic; }
249
- </style>
250
- </head>
251
- <body>
252
- <div class="container">
253
- <header>
254
- <div>
255
- <h1>DeepSeek-2API 管理面板</h1>
256
- <div class="meta">版本 <span id="version">-</span> · 运行 <span id="uptime">-</span></div>
257
- </div>
258
- <div class="controls">
259
- <select id="interval" title="自动刷新间隔">
260
- <option value="5000">5秒</option>
261
- <option value="10000" selected>10秒</option>
262
- <option value="30000">30秒</option>
263
- <option value="0">关闭</option>
264
- </select>
265
- <button id="refreshBtn">刷新</button>
266
- </div>
267
- </header>
268
-
269
- <!-- 总览 -->
270
- <div class="card">
271
- <h2>总览</h2>
272
- <div class="stats-grid">
273
- <div class="stat-item">
274
- <div class="label">状态</div>
275
- <div class="value" id="status">-</div>
276
- </div>
277
- <div class="stat-item">
278
- <div class="label">总容量</div>
279
- <div class="value" id="capacity">-</div>
280
- </div>
281
- <div class="stat-item">
282
- <div class="label">令牌数</div>
283
- <div class="value" id="tokenCount">-</div>
284
- </div>
285
- <div class="stat-item">
286
- <div class="label">会话缓存</div>
287
- <div class="value" id="sessionCount">-</div>
288
- </div>
289
- </div>
290
- </div>
291
-
292
- <!-- 请求队列 -->
293
- <div class="card">
294
- <h2>请求队列</h2>
295
- <div class="bar-wrap">
296
- <div class="bar-fill" id="queueBar" style="width:0%;background:var(--green)"></div>
297
- <div class="bar-label" id="queueLabel">0 / 100</div>
298
- </div>
299
- </div>
300
-
301
- <!-- 令牌池 -->
302
- <div class="card">
303
- <h2>令牌池</h2>
304
- <div style="overflow-x:auto">
305
- <table>
306
- <thead>
307
- <tr><th>令牌</th><th>视觉能力</th><th>错误数</th><th>活跃/最大</th><th>健康</th></tr>
308
- </thead>
309
- <tbody id="poolBody">
310
- <tr><td colspan="5" class="loading">加载中...</td></tr>
311
- </tbody>
312
- </table>
313
- </div>
314
- </div>
315
-
316
- <!-- 会话缓存 -->
317
- <div class="card">
318
- <h2>会话缓存</h2>
319
- <div id="sessionSummary" style="font-size:0.88em;color:var(--text-dim);margin-bottom:8px">-</div>
320
- <div style="overflow-x:auto">
321
- <table>
322
- <thead>
323
- <tr><th>缓存键</th><th>模型</th><th>已用时间</th><th>剩余 TTL</th></tr>
324
- </thead>
325
- <tbody id="sessionBody">
326
- <tr><td colspan="4" class="loading">加载中...</td></tr>
327
- </tbody>
328
- </table>
329
- </div>
330
- </div>
331
-
332
- <!-- 快速入门 -->
333
- <div class="card">
334
- <h2>快速入门</h2>
335
- <h3 style="font-size:0.92em;margin-bottom:8px">模型映射</h3>
336
- <table class="model-table">
337
- <thead><tr><th>API 模型名</th><th>DeepSeek 模型类型</th></tr></thead>
338
- <tbody>
339
- <tr><td>deepseek-v4-flash</td><td>default(默认/免费)</td></tr>
340
- <tr><td>deepseek-v4-pro</td><td>expert(深度思考/付费)</td></tr>
341
- <tr><td>deepseek-v4-vision</td><td>vision(视觉/图片理解)</td></tr>
342
- </tbody>
343
- </table>
344
-
345
- <details>
346
- <summary>OpenAI 格式 - 非流式请求</summary>
347
- <pre>curl http://localhost:3000/v1/chat/completions \
348
- -H "Content-Type: application/json" \
349
- -H "Authorization: Bearer YOUR_API_KEY" \
350
- -d '{
351
- "model": "deepseek-v4-flash",
352
- "messages": [{"role":"user","content":"你好"}]
353
- }'</pre>
354
- </details>
355
-
356
- <details>
357
- <summary>OpenAI 格式 - 流式请求(SSE)</summary>
358
- <pre>curl http://localhost:3000/v1/chat/completions \
359
- -H "Content-Type: application/json" \
360
- -H "Authorization: Bearer YOUR_API_KEY" \
361
- -d '{
362
- "model": "deepseek-v4-pro",
363
- "stream": true,
364
- "messages": [{"role":"user","content":"解释量子计算"}]
365
- }'</pre>
366
- </details>
367
-
368
- <details>
369
- <summary>DeepSeek 原生格式</summary>
370
- <pre>curl http://localhost:3000/api/v0/chat/completion \
371
- -H "Content-Type: application/json" \
372
- -H "Authorization: Bearer YOUR_API_KEY" \
373
- -d '{
374
- "model_type": "default",
375
- "stream": true,
376
- "chat_session_id": "SESSION_ID",
377
- "prompt": "你好"
378
- }'</pre>
379
- </details>
380
-
381
- <details>
382
- <summary>查看可用模型</summary>
383
- <pre>curl http://localhost:3000/v1/models \
384
- -H "Authorization: Bearer YOUR_API_KEY"</pre>
385
- </details>
386
- </div>
387
-
388
- <!-- 令牌管理 -->
389
- <div class="card">
390
- <h2>令牌管理</h2>
391
- <div class="forms-row">
392
- <div>
393
- <h3 style="font-size:0.92em;margin-bottom:8px">添加令牌</h3>
394
- <form id="addTokenForm">
395
- <div class="form-group">
396
- <label>令牌字符串</label>
397
- <input type="text" id="addTokenInput" placeholder="粘贴 DeepSeek 令牌..." required>
398
- </div>
399
- <button type="submit" class="btn">添加</button>
400
- <div class="msg" id="addTokenMsg"></div>
401
- </form>
402
- </div>
403
- <div>
404
- <h3 style="font-size:0.92em;margin-bottom:8px">登录添加</h3>
405
- <form id="loginForm">
406
- <div class="form-group">
407
- <label>邮箱</label>
408
- <input type="email" id="loginEmail" placeholder="DeepSeek 账号邮箱" required>
409
- </div>
410
- <div class="form-group">
411
- <label>密码</label>
412
- <input type="password" id="loginPassword" placeholder="DeepSeek 账号密码" required>
413
- </div>
414
- <button type="submit" class="btn">登录</button>
415
- <div class="msg" id="loginMsg"></div>
416
- </form>
417
- </div>
418
- </div>
419
- </div>
420
-
421
- <footer>上次刷新: <span id="lastRefresh">-</span></footer>
422
- </div>
423
-
424
- <!-- Auth overlay -->
425
- <div class="auth-overlay" id="authOverlay" style="display:none">
426
- <div class="auth-box">
427
- <h3>需要 API Key</h3>
428
- <p style="font-size:0.85em;color:var(--text-dim);margin-bottom:12px">此服务器已配置 API_KEY 验证,请输入密钥。</p>
429
- <div class="form-group">
430
- <label>API Key</label>
431
- <input type="password" id="authKeyInput" placeholder="输入 API Key...">
432
- </div>
433
- <button class="btn" id="authSubmitBtn">确认</button>
434
- <div class="msg" id="authMsg"></div>
435
- </div>
436
- </div>
437
-
438
- <script>
439
- const $ = id => document.getElementById(id);
440
- let refreshTimer = null;
441
-
442
- function getApiKey() {
443
- return sessionStorage.getItem('adminApiKey') || '';
444
- }
445
-
446
- function authHeaders() {
447
- const key = getApiKey();
448
- return key ? { 'Authorization': 'Bearer ' + key } : {};
449
- }
450
-
451
- async function apiFetch(url, options = {}) {
452
- const headers = { ...authHeaders(), ...(options.headers || {}) };
453
- const res = await fetch(url, { ...options, headers });
454
- if (res.status === 401) {
455
- sessionStorage.removeItem('adminApiKey');
456
- $('authOverlay').style.display = 'flex';
457
- throw new Error('unauthorized');
458
- }
459
- return res;
460
- }
461
-
462
- function formatUptime(sec) {
463
- const d = Math.floor(sec / 86400);
464
- const h = Math.floor((sec % 86400) / 3600);
465
- const m = Math.floor((sec % 3600) / 60);
466
- const s = sec % 60;
467
- const parts = [];
468
- if (d) parts.push(d + '天');
469
- if (h) parts.push(h + '时');
470
- if (m) parts.push(m + '分');
471
- parts.push(s + '秒');
472
- return parts.join(' ');
473
- }
474
-
475
- function formatDuration(sec) {
476
- if (sec < 60) return sec + '秒';
477
- if (sec < 3600) return Math.floor(sec / 60) + '分' + (sec % 60) + '秒';
478
- if (sec < 86400) return Math.floor(sec / 3600) + '时' + Math.floor((sec % 3600) / 60) + '分';
479
- return Math.floor(sec / 86400) + '天' + Math.floor((sec % 86400) / 3600) + '时';
480
- }
481
-
482
- function healthDot(errorCount, active, max) {
483
- if (errorCount >= 3) return '<span class="dot dot-red"></span>不可用';
484
- if (errorCount >= 1) return '<span class="dot dot-yellow"></span>异常';
485
- if (active >= max) return '<span class="dot dot-yellow"></span>满载';
486
- return '<span class="dot dot-green"></span>正常';
487
- }
488
-
489
- function visionLabel(v) {
490
- if (v === true) return '<span style="color:var(--green)">支持</span>';
491
- if (v === false) return '<span style="color:var(--text-dim)">不支持</span>';
492
- return '<span style="color:var(--yellow)">未知</span>';
493
- }
494
-
495
- function render(data) {
496
- $('version').textContent = data.version;
497
- $('uptime').textContent = formatUptime(data.uptimeSeconds);
498
- $('status').innerHTML = '<span class="status-ok">正常</span>';
499
- $('capacity').textContent = data.totalCapacity;
500
- $('tokenCount').textContent = data.pool.length;
501
- $('sessionCount').textContent = data.sessions.count;
502
-
503
- // Queue bar
504
- const q = data.queue.queued;
505
- const qMax = data.queue.maxQueueSize;
506
- const pct = Math.min(100, (q / qMax) * 100);
507
- const qBar = $('queueBar');
508
- qBar.style.width = pct + '%';
509
- qBar.style.background = pct > 80 ? 'var(--red)' : pct > 50 ? 'var(--yellow)' : 'var(--green)';
510
- $('queueLabel').textContent = q + ' / ' + qMax;
511
-
512
- // Token pool
513
- const poolBody = $('poolBody');
514
- if (data.pool.length === 0) {
515
- poolBody.innerHTML = '<tr><td colspan="5" style="color:var(--text-dim)">无令牌</td></tr>';
516
- } else {
517
- poolBody.innerHTML = data.pool.map(t => `<tr>
518
- <td>${t.token}</td>
519
- <td>${visionLabel(t.visionCapable)}</td>
520
- <td>${t.errorCount}</td>
521
- <td>${t.activeRequests} / ${t.maxConcurrent}</td>
522
- <td>${healthDot(t.errorCount, t.activeRequests, t.maxConcurrent)}</td>
523
- </tr>`).join('');
524
- }
525
-
526
- // Sessions
527
- const sessions = data.sessions;
528
- $('sessionSummary').textContent = sessions.count + ' 个会话已缓存 · TTL: ' + formatDuration(sessions.ttl);
529
- const sBody = $('sessionBody');
530
- if (sessions.sessions.length === 0) {
531
- sBody.innerHTML = '<tr><td colspan="4" style="color:var(--text-dim)">无缓存会话</td></tr>';
532
- } else {
533
- sBody.innerHTML = sessions.sessions.map(s => {
534
- const ttlStyle = s.ttlRemainingSeconds < 3600 ? 'color:var(--yellow)' : '';
535
- const expiredStyle = s.ttlRemainingSeconds === 0 ? 'color:var(--red)' : '';
536
- return `<tr>
537
- <td>${s.key}</td>
538
- <td>${s.modelType}</td>
539
- <td>${formatDuration(s.ageSeconds)}</td>
540
- <td style="${expiredStyle || ttlStyle}">${s.ttlRemainingSeconds === 0 ? '已过期' : formatDuration(s.ttlRemainingSeconds)}</td>
541
- </tr>`;
542
- }).join('');
543
- }
544
-
545
- $('lastRefresh').textContent = new Date().toLocaleTimeString();
546
- }
547
-
548
- async function fetchStats() {
549
- try {
550
- const res = await apiFetch('/admin/api/stats');
551
- if (!res.ok) throw new Error('HTTP ' + res.status);
552
- const data = await res.json();
553
- render(data);
554
- } catch (e) {
555
- if (e.message !== 'unauthorized') {
556
- console.error('fetchStats error:', e);
557
- }
558
- }
559
- }
560
-
561
- // Auto-refresh
562
- function setupRefresh() {
563
- if (refreshTimer) clearInterval(refreshTimer);
564
- const ms = parseInt($('interval').value);
565
- if (ms > 0) {
566
- refreshTimer = setInterval(fetchStats, ms);
567
- }
568
- }
569
-
570
- $('interval').addEventListener('change', setupRefresh);
571
- $('refreshBtn').addEventListener('click', fetchStats);
572
-
573
- // Auth overlay
574
- $('authSubmitBtn').addEventListener('click', () => {
575
- const key = $('authKeyInput').value.trim();
576
- if (!key) {
577
- $('authMsg').className = 'msg msg-err';
578
- $('authMsg').textContent = '请输入 API Key';
579
- return;
580
- }
581
- sessionStorage.setItem('adminApiKey', key);
582
- $('authOverlay').style.display = 'none';
583
- $('authMsg').textContent = '';
584
- fetchStats();
585
- });
586
-
587
- $('authKeyInput').addEventListener('keydown', e => {
588
- if (e.key === 'Enter') $('authSubmitBtn').click();
589
- });
590
-
591
- // Token management forms
592
- $('addTokenForm').addEventListener('submit', async e => {
593
- e.preventDefault();
594
- const token = $('addTokenInput').value.trim();
595
- const msgEl = $('addTokenMsg');
596
- msgEl.textContent = '添加中...';
597
- msgEl.className = 'msg';
598
- try {
599
- const res = await apiFetch('/admin/api/token/add', {
600
- method: 'POST',
601
- headers: { 'Content-Type': 'application/json' },
602
- body: JSON.stringify({ token }),
603
- });
604
- const data = await res.json();
605
- if (data.success) {
606
- msgEl.className = 'msg msg-ok';
607
- msgEl.textContent = '添加成功!视觉能力: ' + (data.visionCapable === true ? '支持' : data.visionCapable === false ? '不支持' : '未知');
608
- $('addTokenInput').value = '';
609
- fetchStats();
610
- } else {
611
- msgEl.className = 'msg msg-err';
612
- msgEl.textContent = data.error?.message || '添加失败';
613
- }
614
- } catch (err) {
615
- msgEl.className = 'msg msg-err';
616
- msgEl.textContent = err.message === 'unauthorized' ? '认证失败' : '请求失败';
617
- }
618
- });
619
-
620
- $('loginForm').addEventListener('submit', async e => {
621
- e.preventDefault();
622
- const email = $('loginEmail').value.trim();
623
- const password = $('loginPassword').value;
624
- const msgEl = $('loginMsg');
625
- msgEl.textContent = '登录中...';
626
- msgEl.className = 'msg';
627
- try {
628
- const res = await apiFetch('/admin/api/token/login', {
629
- method: 'POST',
630
- headers: { 'Content-Type': 'application/json' },
631
- body: JSON.stringify({ email, password }),
632
- });
633
- const data = await res.json();
634
- if (data.success) {
635
- msgEl.className = 'msg msg-ok';
636
- msgEl.textContent = '登录成功!令牌: ' + data.token;
637
- $('loginEmail').value = '';
638
- $('loginPassword').value = '';
639
- fetchStats();
640
- } else {
641
- msgEl.className = 'msg msg-err';
642
- msgEl.textContent = data.error?.message || '登录失败';
643
- }
644
- } catch (err) {
645
- msgEl.className = 'msg msg-err';
646
- msgEl.textContent = err.message === 'unauthorized' ? '认证失败' : '请求失败';
647
- }
648
- });
649
-
650
- // Init
651
- fetchStats();
652
- setupRefresh();
653
- </script>
654
- </body>
655
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>DeepSeek-2API 管理面板</title>
7
+ <style>
8
+ :root {
9
+ --bg: #1a1a2e;
10
+ --surface: #16213e;
11
+ --border: #0f3460;
12
+ --text: #e0e0e0;
13
+ --text-dim: #8892a0;
14
+ --green: #00e676;
15
+ --yellow: #ffd600;
16
+ --red: #ff1744;
17
+ --accent: #00b0ff;
18
+ }
19
+ * { margin: 0; padding: 0; box-sizing: border-box; }
20
+ body {
21
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
22
+ background: var(--bg);
23
+ color: var(--text);
24
+ line-height: 1.6;
25
+ min-height: 100vh;
26
+ }
27
+ .container {
28
+ max-width: 960px;
29
+ margin: 0 auto;
30
+ padding: 20px;
31
+ }
32
+ header {
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: space-between;
36
+ padding: 16px 20px;
37
+ background: var(--surface);
38
+ border: 1px solid var(--border);
39
+ border-radius: 8px;
40
+ margin-bottom: 20px;
41
+ }
42
+ header h1 {
43
+ font-size: 1.3em;
44
+ color: var(--accent);
45
+ }
46
+ header .meta {
47
+ font-size: 0.85em;
48
+ color: var(--text-dim);
49
+ }
50
+ .controls {
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 8px;
54
+ }
55
+ .controls select, .controls button {
56
+ background: var(--bg);
57
+ color: var(--text);
58
+ border: 1px solid var(--border);
59
+ border-radius: 4px;
60
+ padding: 4px 10px;
61
+ font-size: 0.85em;
62
+ cursor: pointer;
63
+ }
64
+ .controls button:hover { border-color: var(--accent); }
65
+ .card {
66
+ background: var(--surface);
67
+ border: 1px solid var(--border);
68
+ border-radius: 8px;
69
+ padding: 16px 20px;
70
+ margin-bottom: 16px;
71
+ }
72
+ .card h2 {
73
+ font-size: 1.05em;
74
+ margin-bottom: 12px;
75
+ color: var(--accent);
76
+ }
77
+ .stats-grid {
78
+ display: grid;
79
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
80
+ gap: 12px;
81
+ }
82
+ .stat-item {
83
+ background: var(--bg);
84
+ border-radius: 6px;
85
+ padding: 12px;
86
+ text-align: center;
87
+ }
88
+ .stat-item .label {
89
+ font-size: 0.75em;
90
+ color: var(--text-dim);
91
+ margin-bottom: 4px;
92
+ }
93
+ .stat-item .value {
94
+ font-size: 1.4em;
95
+ font-weight: 700;
96
+ }
97
+ .status-ok { color: var(--green); }
98
+ .status-warn { color: var(--yellow); }
99
+ .status-err { color: var(--red); }
100
+
101
+ table {
102
+ width: 100%;
103
+ border-collapse: collapse;
104
+ font-size: 0.88em;
105
+ }
106
+ th, td {
107
+ padding: 8px 10px;
108
+ text-align: left;
109
+ border-bottom: 1px solid var(--border);
110
+ }
111
+ th {
112
+ color: var(--text-dim);
113
+ font-weight: 600;
114
+ font-size: 0.8em;
115
+ text-transform: uppercase;
116
+ letter-spacing: 0.5px;
117
+ }
118
+ td { font-family: 'SF Mono', 'Consolas', monospace; font-size: 0.85em; }
119
+ .dot {
120
+ display: inline-block;
121
+ width: 8px;
122
+ height: 8px;
123
+ border-radius: 50%;
124
+ margin-right: 6px;
125
+ }
126
+ .dot-green { background: var(--green); }
127
+ .dot-yellow { background: var(--yellow); }
128
+ .dot-red { background: var(--red); }
129
+
130
+ .bar-wrap {
131
+ background: var(--bg);
132
+ border-radius: 4px;
133
+ height: 22px;
134
+ overflow: hidden;
135
+ position: relative;
136
+ }
137
+ .bar-fill {
138
+ height: 100%;
139
+ border-radius: 4px;
140
+ transition: width 0.4s, background 0.4s;
141
+ }
142
+ .bar-label {
143
+ position: absolute;
144
+ top: 0; left: 10px; right: 10px;
145
+ line-height: 22px;
146
+ font-size: 0.78em;
147
+ color: var(--text);
148
+ text-shadow: 0 1px 2px rgba(0,0,0,0.6);
149
+ }
150
+
151
+ .forms-row {
152
+ display: grid;
153
+ grid-template-columns: 1fr 1fr;
154
+ gap: 16px;
155
+ }
156
+ @media (max-width: 640px) {
157
+ .forms-row { grid-template-columns: 1fr; }
158
+ }
159
+ .form-group { margin-bottom: 10px; }
160
+ .form-group label {
161
+ display: block;
162
+ font-size: 0.82em;
163
+ color: var(--text-dim);
164
+ margin-bottom: 4px;
165
+ }
166
+ .form-group input {
167
+ width: 100%;
168
+ background: var(--bg);
169
+ color: var(--text);
170
+ border: 1px solid var(--border);
171
+ border-radius: 4px;
172
+ padding: 7px 10px;
173
+ font-size: 0.9em;
174
+ }
175
+ .form-group input:focus { outline: none; border-color: var(--accent); }
176
+ .btn {
177
+ background: var(--accent);
178
+ color: #fff;
179
+ border: none;
180
+ border-radius: 4px;
181
+ padding: 7px 18px;
182
+ font-size: 0.9em;
183
+ cursor: pointer;
184
+ }
185
+ .btn:hover { opacity: 0.85; }
186
+ .msg {
187
+ font-size: 0.82em;
188
+ margin-top: 6px;
189
+ min-height: 1.4em;
190
+ }
191
+ .msg-ok { color: var(--green); }
192
+ .msg-err { color: var(--red); }
193
+
194
+ details {
195
+ margin-bottom: 10px;
196
+ }
197
+ summary {
198
+ cursor: pointer;
199
+ color: var(--accent);
200
+ font-size: 0.92em;
201
+ padding: 4px 0;
202
+ }
203
+ pre {
204
+ background: var(--bg);
205
+ border: 1px solid var(--border);
206
+ border-radius: 4px;
207
+ padding: 10px 12px;
208
+ font-size: 0.82em;
209
+ overflow-x: auto;
210
+ color: var(--text);
211
+ margin: 6px 0;
212
+ }
213
+ .model-table {
214
+ width: auto;
215
+ margin-bottom: 12px;
216
+ }
217
+ .model-table td, .model-table th {
218
+ padding: 6px 16px;
219
+ }
220
+
221
+ footer {
222
+ text-align: center;
223
+ color: var(--text-dim);
224
+ font-size: 0.78em;
225
+ padding: 12px;
226
+ }
227
+
228
+ .auth-overlay {
229
+ position: fixed;
230
+ inset: 0;
231
+ background: rgba(0,0,0,0.7);
232
+ display: flex;
233
+ align-items: center;
234
+ justify-content: center;
235
+ z-index: 100;
236
+ }
237
+ .auth-box {
238
+ background: var(--surface);
239
+ border: 1px solid var(--border);
240
+ border-radius: 8px;
241
+ padding: 24px;
242
+ width: 340px;
243
+ }
244
+ .auth-box h3 { margin-bottom: 12px; color: var(--accent); }
245
+ .auth-box .form-group { margin-bottom: 14px; }
246
+ .auth-box .msg { margin-bottom: 8px; }
247
+
248
+ .loading { color: var(--text-dim); font-style: italic; }
249
+ </style>
250
+ </head>
251
+ <body>
252
+ <div class="container">
253
+ <header>
254
+ <div>
255
+ <h1>DeepSeek-2API 管理面板</h1>
256
+ <div class="meta">版本 <span id="version">-</span> · 运行 <span id="uptime">-</span></div>
257
+ </div>
258
+ <div class="controls">
259
+ <select id="interval" title="自动刷新间隔">
260
+ <option value="5000">5秒</option>
261
+ <option value="10000" selected>10秒</option>
262
+ <option value="30000">30秒</option>
263
+ <option value="0">关闭</option>
264
+ </select>
265
+ <button id="refreshBtn">刷新</button>
266
+ </div>
267
+ </header>
268
+
269
+ <!-- 总览 -->
270
+ <div class="card">
271
+ <h2>总览</h2>
272
+ <div class="stats-grid">
273
+ <div class="stat-item">
274
+ <div class="label">状态</div>
275
+ <div class="value" id="status">-</div>
276
+ </div>
277
+ <div class="stat-item">
278
+ <div class="label">总容量</div>
279
+ <div class="value" id="capacity">-</div>
280
+ </div>
281
+ <div class="stat-item">
282
+ <div class="label">令牌数</div>
283
+ <div class="value" id="tokenCount">-</div>
284
+ </div>
285
+ <div class="stat-item">
286
+ <div class="label">会话缓存</div>
287
+ <div class="value" id="sessionCount">-</div>
288
+ </div>
289
+ </div>
290
+ </div>
291
+
292
+ <!-- 请求队列 -->
293
+ <div class="card">
294
+ <h2>请求队列</h2>
295
+ <div class="bar-wrap">
296
+ <div class="bar-fill" id="queueBar" style="width:0%;background:var(--green)"></div>
297
+ <div class="bar-label" id="queueLabel">0 / 100</div>
298
+ </div>
299
+ </div>
300
+
301
+ <!-- 令牌池 -->
302
+ <div class="card">
303
+ <h2>令牌池</h2>
304
+ <div style="overflow-x:auto">
305
+ <table>
306
+ <thead>
307
+ <tr><th>令牌</th><th>视觉能力</th><th>错误数</th><th>活跃/最大</th><th>健康</th></tr>
308
+ </thead>
309
+ <tbody id="poolBody">
310
+ <tr><td colspan="5" class="loading">加载中...</td></tr>
311
+ </tbody>
312
+ </table>
313
+ </div>
314
+ </div>
315
+
316
+ <!-- 会话缓存 -->
317
+ <div class="card">
318
+ <h2>会话缓存</h2>
319
+ <div id="sessionSummary" style="font-size:0.88em;color:var(--text-dim);margin-bottom:8px">-</div>
320
+ <div style="overflow-x:auto">
321
+ <table>
322
+ <thead>
323
+ <tr><th>缓存键</th><th>模型</th><th>已用时间</th><th>剩余 TTL</th></tr>
324
+ </thead>
325
+ <tbody id="sessionBody">
326
+ <tr><td colspan="4" class="loading">加载中...</td></tr>
327
+ </tbody>
328
+ </table>
329
+ </div>
330
+ </div>
331
+
332
+ <!-- 快速入门 -->
333
+ <div class="card">
334
+ <h2>快速入门</h2>
335
+ <h3 style="font-size:0.92em;margin-bottom:8px">模型映射</h3>
336
+ <table class="model-table">
337
+ <thead><tr><th>API 模型名</th><th>DeepSeek 模型类型</th></tr></thead>
338
+ <tbody>
339
+ <tr><td>deepseek-v4-flash</td><td>default(默认/免费)</td></tr>
340
+ <tr><td>deepseek-v4-pro</td><td>expert(深度思考/付费)</td></tr>
341
+ <tr><td>deepseek-v4-vision</td><td>vision(视觉/图片理解)</td></tr>
342
+ </tbody>
343
+ </table>
344
+
345
+ <details>
346
+ <summary>OpenAI 格式 - 非流式请求</summary>
347
+ <pre>curl http://localhost:3000/v1/chat/completions \
348
+ -H "Content-Type: application/json" \
349
+ -H "Authorization: Bearer YOUR_API_KEY" \
350
+ -d '{
351
+ "model": "deepseek-v4-flash",
352
+ "messages": [{"role":"user","content":"你好"}]
353
+ }'</pre>
354
+ </details>
355
+
356
+ <details>
357
+ <summary>OpenAI 格式 - 流式请求(SSE)</summary>
358
+ <pre>curl http://localhost:3000/v1/chat/completions \
359
+ -H "Content-Type: application/json" \
360
+ -H "Authorization: Bearer YOUR_API_KEY" \
361
+ -d '{
362
+ "model": "deepseek-v4-pro",
363
+ "stream": true,
364
+ "messages": [{"role":"user","content":"解释量子计算"}]
365
+ }'</pre>
366
+ </details>
367
+
368
+ <details>
369
+ <summary>DeepSeek 原生格式</summary>
370
+ <pre>curl http://localhost:3000/api/v0/chat/completion \
371
+ -H "Content-Type: application/json" \
372
+ -H "Authorization: Bearer YOUR_API_KEY" \
373
+ -d '{
374
+ "model_type": "default",
375
+ "stream": true,
376
+ "chat_session_id": "SESSION_ID",
377
+ "prompt": "你好"
378
+ }'</pre>
379
+ </details>
380
+
381
+ <details>
382
+ <summary>查看可用模型</summary>
383
+ <pre>curl http://localhost:3000/v1/models \
384
+ -H "Authorization: Bearer YOUR_API_KEY"</pre>
385
+ </details>
386
+ </div>
387
+
388
+ <!-- 令牌管理 -->
389
+ <div class="card">
390
+ <h2>令牌管理</h2>
391
+ <div class="forms-row">
392
+ <div>
393
+ <h3 style="font-size:0.92em;margin-bottom:8px">添加令牌</h3>
394
+ <form id="addTokenForm">
395
+ <div class="form-group">
396
+ <label>令牌字符串</label>
397
+ <input type="text" id="addTokenInput" placeholder="粘贴 DeepSeek 令牌..." required>
398
+ </div>
399
+ <button type="submit" class="btn">添加</button>
400
+ <div class="msg" id="addTokenMsg"></div>
401
+ </form>
402
+ </div>
403
+ <div>
404
+ <h3 style="font-size:0.92em;margin-bottom:8px">登录添加</h3>
405
+ <form id="loginForm">
406
+ <div class="form-group">
407
+ <label>邮箱</label>
408
+ <input type="email" id="loginEmail" placeholder="DeepSeek 账号邮箱" required>
409
+ </div>
410
+ <div class="form-group">
411
+ <label>密码</label>
412
+ <input type="password" id="loginPassword" placeholder="DeepSeek 账号密码" required>
413
+ </div>
414
+ <button type="submit" class="btn">登录</button>
415
+ <div class="msg" id="loginMsg"></div>
416
+ </form>
417
+ </div>
418
+ </div>
419
+ </div>
420
+
421
+ <!-- 请求日志 -->
422
+ <div class="card">
423
+ <h2>请求日志</h2>
424
+ <div class="controls" style="margin-bottom:10px">
425
+ <select id="logDateSelect"><option value="">今天</option></select>
426
+ <button id="loadLogsBtn">加载</button>
427
+ <span style="font-size:0.82em;color:var(--text-dim);margin-left:8px" id="logStatsText"></span>
428
+ </div>
429
+ <div style="overflow-x:auto;max-height:320px;overflow-y:auto">
430
+ <table>
431
+ <thead><tr><th>时间</th><th>方法</th><th>路径</th><th>模型</th><th>状态</th><th>耗时</th></tr></thead>
432
+ <tbody id="logBody"><tr><td colspan="6" class="loading">点击"加载"查看日志</td></tr></tbody>
433
+ </table>
434
+ </div>
435
+ </div>
436
+
437
+ <!-- 对话记录 -->
438
+ <div class="card">
439
+ <h2>对话记录</h2>
440
+ <div class="controls" style="margin-bottom:10px">
441
+ <select id="chatDateSelect"><option value="">今天</option></select>
442
+ <button id="loadChatsBtn">加载</button>
443
+ <span style="font-size:0.82em;color:var(--text-dim);margin-left:8px" id="chatCountText"></span>
444
+ </div>
445
+ <div id="chatList" style="max-height:400px;overflow-y:auto"></div>
446
+ </div>
447
+
448
+ <footer>上次刷新: <span id="lastRefresh">-</span></footer>
449
+ </div>
450
+
451
+ <!-- Auth overlay -->
452
+ <div class="auth-overlay" id="authOverlay" style="display:none">
453
+ <div class="auth-box">
454
+ <h3>需要 API Key</h3>
455
+ <p style="font-size:0.85em;color:var(--text-dim);margin-bottom:12px">此服务器已配置 API_KEY 验证,请输入密钥。</p>
456
+ <div class="form-group">
457
+ <label>API Key</label>
458
+ <input type="password" id="authKeyInput" placeholder="输入 API Key...">
459
+ </div>
460
+ <button class="btn" id="authSubmitBtn">确认</button>
461
+ <div class="msg" id="authMsg"></div>
462
+ </div>
463
+ </div>
464
+
465
+ <script>
466
+ const $ = id => document.getElementById(id);
467
+ let refreshTimer = null;
468
+
469
+ function getApiKey() {
470
+ return sessionStorage.getItem('adminApiKey') || '';
471
+ }
472
+
473
+ function authHeaders() {
474
+ const key = getApiKey();
475
+ return key ? { 'Authorization': 'Bearer ' + key } : {};
476
+ }
477
+
478
+ async function apiFetch(url, options = {}) {
479
+ const headers = { ...authHeaders(), ...(options.headers || {}) };
480
+ const res = await fetch(url, { ...options, headers });
481
+ if (res.status === 401) {
482
+ sessionStorage.removeItem('adminApiKey');
483
+ $('authOverlay').style.display = 'flex';
484
+ throw new Error('unauthorized');
485
+ }
486
+ return res;
487
+ }
488
+
489
+ function formatUptime(sec) {
490
+ const d = Math.floor(sec / 86400);
491
+ const h = Math.floor((sec % 86400) / 3600);
492
+ const m = Math.floor((sec % 3600) / 60);
493
+ const s = sec % 60;
494
+ const parts = [];
495
+ if (d) parts.push(d + '天');
496
+ if (h) parts.push(h + '时');
497
+ if (m) parts.push(m + '分');
498
+ parts.push(s + '秒');
499
+ return parts.join(' ');
500
+ }
501
+
502
+ function formatDuration(sec) {
503
+ if (sec < 60) return sec + '秒';
504
+ if (sec < 3600) return Math.floor(sec / 60) + '分' + (sec % 60) + '秒';
505
+ if (sec < 86400) return Math.floor(sec / 3600) + '时' + Math.floor((sec % 3600) / 60) + '分';
506
+ return Math.floor(sec / 86400) + '天' + Math.floor((sec % 86400) / 3600) + '时';
507
+ }
508
+
509
+ function healthDot(errorCount, active, max) {
510
+ if (errorCount >= 3) return '<span class="dot dot-red"></span>不可用';
511
+ if (errorCount >= 1) return '<span class="dot dot-yellow"></span>异常';
512
+ if (active >= max) return '<span class="dot dot-yellow"></span>满载';
513
+ return '<span class="dot dot-green"></span>正常';
514
+ }
515
+
516
+ function visionLabel(v) {
517
+ if (v === true) return '<span style="color:var(--green)">支持</span>';
518
+ if (v === false) return '<span style="color:var(--text-dim)">不支持</span>';
519
+ return '<span style="color:var(--yellow)">未知</span>';
520
+ }
521
+
522
+ function render(data) {
523
+ $('version').textContent = data.version;
524
+ $('uptime').textContent = formatUptime(data.uptimeSeconds);
525
+ $('status').innerHTML = '<span class="status-ok">正常</span>';
526
+ $('capacity').textContent = data.totalCapacity;
527
+ $('tokenCount').textContent = data.pool.length;
528
+ $('sessionCount').textContent = data.sessions.count;
529
+
530
+ // Queue bar
531
+ const q = data.queue.queued;
532
+ const qMax = data.queue.maxQueueSize;
533
+ const pct = Math.min(100, (q / qMax) * 100);
534
+ const qBar = $('queueBar');
535
+ qBar.style.width = pct + '%';
536
+ qBar.style.background = pct > 80 ? 'var(--red)' : pct > 50 ? 'var(--yellow)' : 'var(--green)';
537
+ $('queueLabel').textContent = q + ' / ' + qMax;
538
+
539
+ // Token pool
540
+ const poolBody = $('poolBody');
541
+ if (data.pool.length === 0) {
542
+ poolBody.innerHTML = '<tr><td colspan="5" style="color:var(--text-dim)">无令牌</td></tr>';
543
+ } else {
544
+ poolBody.innerHTML = data.pool.map(t => `<tr>
545
+ <td>${t.token}</td>
546
+ <td>${visionLabel(t.visionCapable)}</td>
547
+ <td>${t.errorCount}</td>
548
+ <td>${t.activeRequests} / ${t.maxConcurrent}</td>
549
+ <td>${healthDot(t.errorCount, t.activeRequests, t.maxConcurrent)}</td>
550
+ </tr>`).join('');
551
+ }
552
+
553
+ // Sessions
554
+ const sessions = data.sessions;
555
+ $('sessionSummary').textContent = sessions.count + ' 个会话已缓存 · TTL: ' + formatDuration(sessions.ttl);
556
+ const sBody = $('sessionBody');
557
+ if (sessions.sessions.length === 0) {
558
+ sBody.innerHTML = '<tr><td colspan="4" style="color:var(--text-dim)">无缓存会话</td></tr>';
559
+ } else {
560
+ sBody.innerHTML = sessions.sessions.map(s => {
561
+ const ttlStyle = s.ttlRemainingSeconds < 3600 ? 'color:var(--yellow)' : '';
562
+ const expiredStyle = s.ttlRemainingSeconds === 0 ? 'color:var(--red)' : '';
563
+ return `<tr>
564
+ <td>${s.key}</td>
565
+ <td>${s.modelType}</td>
566
+ <td>${formatDuration(s.ageSeconds)}</td>
567
+ <td style="${expiredStyle || ttlStyle}">${s.ttlRemainingSeconds === 0 ? '已过期' : formatDuration(s.ttlRemainingSeconds)}</td>
568
+ </tr>`;
569
+ }).join('');
570
+ }
571
+
572
+ $('lastRefresh').textContent = new Date().toLocaleTimeString();
573
+ }
574
+
575
+ async function fetchStats() {
576
+ try {
577
+ const res = await apiFetch('/admin/api/stats');
578
+ if (!res.ok) throw new Error('HTTP ' + res.status);
579
+ const data = await res.json();
580
+ render(data);
581
+ } catch (e) {
582
+ if (e.message !== 'unauthorized') {
583
+ console.error('fetchStats error:', e);
584
+ }
585
+ }
586
+ }
587
+
588
+ // Auto-refresh
589
+ function setupRefresh() {
590
+ if (refreshTimer) clearInterval(refreshTimer);
591
+ const ms = parseInt($('interval').value);
592
+ if (ms > 0) {
593
+ refreshTimer = setInterval(fetchStats, ms);
594
+ }
595
+ }
596
+
597
+ $('interval').addEventListener('change', setupRefresh);
598
+ $('refreshBtn').addEventListener('click', fetchStats);
599
+
600
+ // Auth overlay
601
+ $('authSubmitBtn').addEventListener('click', () => {
602
+ const key = $('authKeyInput').value.trim();
603
+ if (!key) {
604
+ $('authMsg').className = 'msg msg-err';
605
+ $('authMsg').textContent = '请输入 API Key';
606
+ return;
607
+ }
608
+ sessionStorage.setItem('adminApiKey', key);
609
+ $('authOverlay').style.display = 'none';
610
+ $('authMsg').textContent = '';
611
+ fetchStats();
612
+ });
613
+
614
+ $('authKeyInput').addEventListener('keydown', e => {
615
+ if (e.key === 'Enter') $('authSubmitBtn').click();
616
+ });
617
+
618
+ // Token management forms
619
+ $('addTokenForm').addEventListener('submit', async e => {
620
+ e.preventDefault();
621
+ const token = $('addTokenInput').value.trim();
622
+ const msgEl = $('addTokenMsg');
623
+ msgEl.textContent = '添加中...';
624
+ msgEl.className = 'msg';
625
+ try {
626
+ const res = await apiFetch('/admin/api/token/add', {
627
+ method: 'POST',
628
+ headers: { 'Content-Type': 'application/json' },
629
+ body: JSON.stringify({ token }),
630
+ });
631
+ const data = await res.json();
632
+ if (data.success) {
633
+ msgEl.className = 'msg msg-ok';
634
+ msgEl.textContent = '添加成功!视觉能力: ' + (data.visionCapable === true ? '支持' : data.visionCapable === false ? '不支持' : '未知');
635
+ $('addTokenInput').value = '';
636
+ fetchStats();
637
+ } else {
638
+ msgEl.className = 'msg msg-err';
639
+ msgEl.textContent = data.error?.message || '添加失败';
640
+ }
641
+ } catch (err) {
642
+ msgEl.className = 'msg msg-err';
643
+ msgEl.textContent = err.message === 'unauthorized' ? '认证失败' : '请求失败';
644
+ }
645
+ });
646
+
647
+ $('loginForm').addEventListener('submit', async e => {
648
+ e.preventDefault();
649
+ const email = $('loginEmail').value.trim();
650
+ const password = $('loginPassword').value;
651
+ const msgEl = $('loginMsg');
652
+ msgEl.textContent = '登录中...';
653
+ msgEl.className = 'msg';
654
+ try {
655
+ const res = await apiFetch('/admin/api/token/login', {
656
+ method: 'POST',
657
+ headers: { 'Content-Type': 'application/json' },
658
+ body: JSON.stringify({ email, password }),
659
+ });
660
+ const data = await res.json();
661
+ if (data.success) {
662
+ msgEl.className = 'msg msg-ok';
663
+ msgEl.textContent = '登录成功!令牌: ' + data.token;
664
+ $('loginEmail').value = '';
665
+ $('loginPassword').value = '';
666
+ fetchStats();
667
+ } else {
668
+ msgEl.className = 'msg msg-err';
669
+ msgEl.textContent = data.error?.message || '登录失败';
670
+ }
671
+ } catch (err) {
672
+ msgEl.className = 'msg msg-err';
673
+ msgEl.textContent = err.message === 'unauthorized' ? '认证失败' : '请求失败';
674
+ }
675
+ });
676
+
677
+ // Load log dates for dropdowns
678
+ async function loadLogDates() {
679
+ try {
680
+ const res = await apiFetch('/admin/api/logs/dates');
681
+ if (!res.ok) return;
682
+ const data = await res.json();
683
+ const dates = data.dates || [];
684
+ ['logDateSelect','chatDateSelect'].forEach(selId => {
685
+ const sel = $(selId);
686
+ sel.innerHTML = '<option value="">今天</option>';
687
+ dates.forEach(d => { const o = document.createElement('option'); o.value = d; o.textContent = d; sel.appendChild(o); });
688
+ });
689
+ } catch {}
690
+ }
691
+
692
+ // Load request logs
693
+ async function loadLogs() {
694
+ const date = $('logDateSelect').value;
695
+ const tbody = $('logBody');
696
+ tbody.innerHTML = '<tr><td colspan="6" class="loading">加载中...</td></tr>';
697
+ try {
698
+ const url = date ? '/admin/api/logs/history?date=' + date + '&count=100' : '/admin/api/logs?count=100';
699
+ const res = await apiFetch(url);
700
+ const data = await res.json();
701
+ const logs = data.logs || [];
702
+ $('logStatsText').textContent = data.stats ? `总计 ${data.stats.totalLogged} 条 · 5分钟内 ${data.stats.last5min} 条 · 平均 ${data.stats.avgDuration5min}ms` : `${logs.length} 条`;
703
+ if (logs.length === 0) {
704
+ tbody.innerHTML = '<tr><td colspan="6" style="color:var(--text-dim)">无记录</td></tr>';
705
+ return;
706
+ }
707
+ tbody.innerHTML = logs.reverse().map(l => {
708
+ const statusColor = l.status >= 400 ? 'var(--red)' : 'var(--green)';
709
+ const time = l.time ? l.time.replace('T',' ').slice(11,19) : '-';
710
+ return `<tr><td>${time}</td><td>${l.method}</td><td>${l.path}</td><td>${l.model}</td><td style="color:${statusColor}">${l.status}</td><td>${l.duration}ms</td></tr>`;
711
+ }).join('');
712
+ } catch (e) {
713
+ tbody.innerHTML = '<tr><td colspan="6" style="color:var(--red)">加载失败</td></tr>';
714
+ }
715
+ }
716
+
717
+ // Load chat logs
718
+ async function loadChats() {
719
+ const date = $('chatDateSelect').value;
720
+ const list = $('chatList');
721
+ list.innerHTML = '<div class="loading">加载中...</div>';
722
+ try {
723
+ const url = date ? '/admin/api/logs/chats?date=' + date + '&count=20' : '/admin/api/logs/chats?count=20';
724
+ const res = await apiFetch(url);
725
+ const data = await res.json();
726
+ const chats = data.chats || [];
727
+ $('chatCountText').textContent = chats.length + ' 条';
728
+ if (chats.length === 0) {
729
+ list.innerHTML = '<div style="color:var(--text-dim);padding:8px">无记录</div>';
730
+ return;
731
+ }
732
+ list.innerHTML = chats.reverse().map(c => {
733
+ const time = c.time ? c.time.replace('T',' ').slice(11,19) : '-';
734
+ const model = c.model || '-';
735
+ const duration = c.duration || 0;
736
+ const preview = (c.response || '').slice(0, 200).replace(/</g,'&lt;');
737
+ return `<details style="margin-bottom:6px"><summary style="font-size:0.85em">${time} · ${model} · ${duration}ms${c.stream ? ' · stream' : ''}</summary><pre style="font-size:0.8em;max-height:200px;overflow-y:auto">${preview}</pre></details>`;
738
+ }).join('');
739
+ } catch {
740
+ list.innerHTML = '<div style="color:var(--red);padding:8px">加载失败</div>';
741
+ }
742
+ }
743
+
744
+ $('loadLogsBtn').addEventListener('click', loadLogs);
745
+ $('loadChatsBtn').addEventListener('click', loadChats);
746
+
747
+ // Init
748
+ fetchStats();
749
+ setupRefresh();
750
+ loadLogDates();
751
+ </script>
752
+ </body>
753
+ </html>
src/auth.js CHANGED
@@ -28,21 +28,12 @@ export function loadTokens() {
28
 
29
  export function loadAccounts() {
30
  const accountsStr = process.env.DS_ACCOUNTS?.trim();
31
- if (accountsStr) {
32
- return accountsStr.split(',').map(entry => {
33
- const [email, ...passParts] = entry.trim().split(':');
34
- const password = passParts.join(':');
35
- return email && password ? { email, password } : null;
36
- }).filter(Boolean);
37
- }
38
-
39
- const email = process.env.DS_EMAIL?.trim();
40
- const password = process.env.DS_PASSWORD?.trim();
41
- if (email && password) {
42
- return [{ email, password }];
43
- }
44
-
45
- return [];
46
  }
47
 
48
  const tokens = loadTokens();
@@ -55,7 +46,7 @@ function generateDeviceId() {
55
  }
56
 
57
  if (tokens.length === 0 && accounts.length === 0) {
58
- throw new Error('No DS_TOKEN/DS_TOKENS or DS_EMAIL+DS_PASSWORD/DS_ACCOUNTS configured');
59
  }
60
 
61
  // DS_ACCOUNTS_EXTENDED=email:password:token_prefix — links existing tokens to accounts
 
28
 
29
  export function loadAccounts() {
30
  const accountsStr = process.env.DS_ACCOUNTS?.trim();
31
+ if (!accountsStr) return [];
32
+ return accountsStr.split(',').map(entry => {
33
+ const [email, ...passParts] = entry.trim().split(':');
34
+ const password = passParts.join(':');
35
+ return email && password ? { email, password } : null;
36
+ }).filter(Boolean);
 
 
 
 
 
 
 
 
 
37
  }
38
 
39
  const tokens = loadTokens();
 
46
  }
47
 
48
  if (tokens.length === 0 && accounts.length === 0) {
49
+ throw new Error('No DS_TOKEN/DS_TOKENS or DS_ACCOUNTS configured');
50
  }
51
 
52
  // DS_ACCOUNTS_EXTENDED=email:password:token_prefix — links existing tokens to accounts
src/index.js CHANGED
@@ -1,154 +1,154 @@
1
- import { config } from 'dotenv';
2
- config();
3
-
4
- import express from 'express';
5
- import { initTokenPool, getPoolInfo, getTotalCapacity, addTokenToPool, loginAndAddToken, getAliveTokens, startHealthCheck } from './auth.js';
6
- import { prewarmSessions, getSessionInfo } from './session.js';
7
- import { handleOpenAICompletion, handleOpenAIModels } from './openai.js';
8
- import { handleDeepSeekCompletion } from './deepseek.js';
9
- import { getQueueInfo } from './queue.js';
10
- import { requestLogger, getRecentLogs, getLogStats, readHistoricalLogs, readChatLogs, listLogDates } from './logger.js';
11
- import { getMetrics, getTimeseries } from './metrics.js';
12
- import { fileURLToPath } from 'url';
13
- import { dirname, join } from 'path';
14
-
15
- const __dirname = dirname(fileURLToPath(import.meta.url));
16
- const startTime = Date.now();
17
-
18
- const app = express();
19
- const PORT = Number(process.env.PORT || 7860);
20
-
21
- app.use(express.json({ limit: '50mb' }));
22
-
23
- // Request logging (writes to /srv/threadripper-backups/newapi/logs/deepseek-2api/)
24
- app.use(requestLogger('deepseek-2api'));
25
-
26
- // API Key auth middleware
27
- app.use((req, res, next) => {
28
- const apiKey = process.env.API_KEY;
29
- if (!apiKey) return next();
30
- if (req.method === 'GET' && req.path === '/admin') return next();
31
-
32
- const auth = req.headers['authorization'];
33
- if (auth === `Bearer ${apiKey}`) return next();
34
-
35
- res.status(401).json({ error: { message: 'Invalid API key' } });
36
- });
37
-
38
- // OpenAI format
39
- app.post('/v1/chat/completions', handleOpenAICompletion);
40
- app.get('/v1/models', handleOpenAIModels);
41
-
42
- // DeepSeek native format
43
- app.post('/api/v0/chat/completion', handleDeepSeekCompletion);
44
-
45
- // Health check + pool info
46
- app.get('/', (req, res) => {
47
- res.json({
48
- status: 'ok',
49
- version: '2.0.0',
50
- pool: getPoolInfo(),
51
- totalCapacity: getTotalCapacity(),
52
- queue: getQueueInfo(),
53
- });
54
- });
55
-
56
- // Admin panel
57
- app.get('/admin', (req, res) => {
58
- res.sendFile(join(__dirname, 'admin', 'index.html'));
59
- });
60
-
61
- app.get('/admin/api/stats', (req, res) => {
62
- const uptimeSeconds = Math.floor((Date.now() - startTime) / 1000);
63
- res.json({
64
- status: 'ok',
65
- version: '2.0.0',
66
- uptimeSeconds,
67
- pool: getPoolInfo(),
68
- totalCapacity: getTotalCapacity(),
69
- queue: getQueueInfo(),
70
- sessions: getSessionInfo(),
71
- logStats: getLogStats(),
72
- });
73
- });
74
-
75
- app.get('/admin/api/logs', (req, res) => {
76
- const count = Math.min(parseInt(req.query.count) || 50, 200);
77
- res.json({ logs: getRecentLogs(count), stats: getLogStats() });
78
- });
79
-
80
- app.get('/admin/api/logs/dates', (req, res) => {
81
- res.json({ dates: listLogDates() });
82
- });
83
-
84
- app.get('/admin/api/logs/history', (req, res) => {
85
- const { date } = req.query;
86
- if (!date) return res.status(400).json({ error: { message: 'date param required (YYYY-MM-DD)' } });
87
- const count = Math.min(parseInt(req.query.count) || 100, 10000);
88
- res.json({ logs: readHistoricalLogs(date, count) });
89
- });
90
-
91
- app.get('/admin/api/logs/chats', (req, res) => {
92
- const date = req.query.date || new Date().toISOString().slice(0, 10);
93
- const count = Math.min(parseInt(req.query.count) || 20, 500);
94
- res.json({ chats: readChatLogs(date, count) });
95
- });
96
-
97
- app.post('/admin/api/token/add', async (req, res) => {
98
- const { token } = req.body;
99
- if (!token || typeof token !== 'string') {
100
- return res.status(400).json({ error: { message: 'token required' } });
101
- }
102
- try {
103
- const added = await addTokenToPool(token);
104
- res.json({ success: true, visionCapable: added.visionCapable });
105
- } catch (err) {
106
- res.status(500).json({ error: { message: err.message } });
107
- }
108
- });
109
-
110
- app.post('/admin/api/token/login', async (req, res) => {
111
- const { email, password } = req.body;
112
- if (!email || !password) {
113
- return res.status(400).json({ error: { message: 'email and password required' } });
114
- }
115
- try {
116
- const token = await loginAndAddToken(email, password);
117
- res.json({ success: true, token: token.slice(0, 12) + '...' });
118
- } catch (err) {
119
- res.status(500).json({ error: { message: err.message } });
120
- }
121
- });
122
-
123
- // Performance monitoring
124
- app.get('/performance', (req, res) => {
125
- res.sendFile(join(__dirname, 'performance', 'index.html'));
126
- });
127
-
128
- app.get('/performance/api/metrics', (req, res) => {
129
- res.json(getMetrics());
130
- });
131
-
132
- app.get('/performance/api/timeseries', (req, res) => {
133
- const range = req.query.range || '6h';
134
- const points = getTimeseries(range);
135
- const pool = getPoolInfo();
136
- const totalCap = getTotalCapacity();
137
- const queue = getQueueInfo();
138
- res.json({ points, pool, totalCapacity: totalCap, queue });
139
- });
140
-
141
- app.listen(PORT, async () => {
142
- console.log(`DeepSeek 2API running on http://localhost:${PORT}`);
143
- console.log(`OpenAI format: POST /v1/chat/completions`);
144
- console.log(`DeepSeek format: POST /api/v0/chat/completion`);
145
- console.log(`Models: GET /v1/models`);
146
- console.log(`Admin panel: http://localhost:${PORT}/admin`);
147
-
148
- await initTokenPool();
149
-
150
- const aliveTokens = getAliveTokens();
151
- await prewarmSessions(aliveTokens);
152
-
153
- startHealthCheck();
154
- });
 
1
+ import { config } from 'dotenv';
2
+ config();
3
+
4
+ import express from 'express';
5
+ import { initTokenPool, getPoolInfo, getTotalCapacity, addTokenToPool, loginAndAddToken, getAliveTokens, startHealthCheck } from './auth.js';
6
+ import { prewarmSessions, getSessionInfo } from './session.js';
7
+ import { handleOpenAICompletion, handleOpenAIModels } from './openai.js';
8
+ import { handleDeepSeekCompletion } from './deepseek.js';
9
+ import { getQueueInfo } from './queue.js';
10
+ import { requestLogger, getRecentLogs, getLogStats, readHistoricalLogs, readChatLogs, listLogDates } from './logger.js';
11
+ import { getMetrics, getTimeseries } from './metrics.js';
12
+ import { fileURLToPath } from 'url';
13
+ import { dirname, join } from 'path';
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const startTime = Date.now();
17
+
18
+ const app = express();
19
+ const PORT = process.env.PORT || 3000;
20
+
21
+ app.use(express.json({ limit: '50mb' }));
22
+
23
+ // Request logging (writes to /srv/threadripper-backups/newapi/logs/deepseek-2api/)
24
+ app.use(requestLogger('deepseek-2api'));
25
+
26
+ // API Key auth middleware
27
+ app.use((req, res, next) => {
28
+ const apiKey = process.env.API_KEY;
29
+ if (!apiKey) return next();
30
+ if (req.method === 'GET' && req.path === '/admin') return next();
31
+
32
+ const auth = req.headers['authorization'];
33
+ if (auth === `Bearer ${apiKey}`) return next();
34
+
35
+ res.status(401).json({ error: { message: 'Invalid API key' } });
36
+ });
37
+
38
+ // OpenAI format
39
+ app.post('/v1/chat/completions', handleOpenAICompletion);
40
+ app.get('/v1/models', handleOpenAIModels);
41
+
42
+ // DeepSeek native format
43
+ app.post('/api/v0/chat/completion', handleDeepSeekCompletion);
44
+
45
+ // Health check + pool info
46
+ app.get('/', (req, res) => {
47
+ res.json({
48
+ status: 'ok',
49
+ version: '2.0.0',
50
+ pool: getPoolInfo(),
51
+ totalCapacity: getTotalCapacity(),
52
+ queue: getQueueInfo(),
53
+ });
54
+ });
55
+
56
+ // Admin panel
57
+ app.get('/admin', (req, res) => {
58
+ res.sendFile(join(__dirname, 'admin', 'index.html'));
59
+ });
60
+
61
+ app.get('/admin/api/stats', (req, res) => {
62
+ const uptimeSeconds = Math.floor((Date.now() - startTime) / 1000);
63
+ res.json({
64
+ status: 'ok',
65
+ version: '2.0.0',
66
+ uptimeSeconds,
67
+ pool: getPoolInfo(),
68
+ totalCapacity: getTotalCapacity(),
69
+ queue: getQueueInfo(),
70
+ sessions: getSessionInfo(),
71
+ logStats: getLogStats(),
72
+ });
73
+ });
74
+
75
+ app.get('/admin/api/logs', (req, res) => {
76
+ const count = Math.min(parseInt(req.query.count) || 50, 200);
77
+ res.json({ logs: getRecentLogs(count), stats: getLogStats() });
78
+ });
79
+
80
+ app.get('/admin/api/logs/dates', (req, res) => {
81
+ res.json({ dates: listLogDates() });
82
+ });
83
+
84
+ app.get('/admin/api/logs/history', (req, res) => {
85
+ const { date } = req.query;
86
+ if (!date) return res.status(400).json({ error: { message: 'date param required (YYYY-MM-DD)' } });
87
+ const count = Math.min(parseInt(req.query.count) || 100, 10000);
88
+ res.json({ logs: readHistoricalLogs(date, count) });
89
+ });
90
+
91
+ app.get('/admin/api/logs/chats', (req, res) => {
92
+ const date = req.query.date || new Date().toISOString().slice(0, 10);
93
+ const count = Math.min(parseInt(req.query.count) || 20, 500);
94
+ res.json({ chats: readChatLogs(date, count) });
95
+ });
96
+
97
+ app.post('/admin/api/token/add', async (req, res) => {
98
+ const { token } = req.body;
99
+ if (!token || typeof token !== 'string') {
100
+ return res.status(400).json({ error: { message: 'token required' } });
101
+ }
102
+ try {
103
+ const added = await addTokenToPool(token);
104
+ res.json({ success: true, visionCapable: added.visionCapable });
105
+ } catch (err) {
106
+ res.status(500).json({ error: { message: err.message } });
107
+ }
108
+ });
109
+
110
+ app.post('/admin/api/token/login', async (req, res) => {
111
+ const { email, password } = req.body;
112
+ if (!email || !password) {
113
+ return res.status(400).json({ error: { message: 'email and password required' } });
114
+ }
115
+ try {
116
+ const token = await loginAndAddToken(email, password);
117
+ res.json({ success: true, token: token.slice(0, 12) + '...' });
118
+ } catch (err) {
119
+ res.status(500).json({ error: { message: err.message } });
120
+ }
121
+ });
122
+
123
+ // Performance monitoring
124
+ app.get('/performance', (req, res) => {
125
+ res.sendFile(join(__dirname, 'performance', 'index.html'));
126
+ });
127
+
128
+ app.get('/performance/api/metrics', (req, res) => {
129
+ res.json(getMetrics());
130
+ });
131
+
132
+ app.get('/performance/api/timeseries', (req, res) => {
133
+ const range = req.query.range || '6h';
134
+ const points = getTimeseries(range);
135
+ const pool = getPoolInfo();
136
+ const totalCap = getTotalCapacity();
137
+ const queue = getQueueInfo();
138
+ res.json({ points, pool, totalCapacity: totalCap, queue });
139
+ });
140
+
141
+ app.listen(PORT, async () => {
142
+ console.log(`DeepSeek 2API running on http://localhost:${PORT}`);
143
+ console.log(`OpenAI format: POST /v1/chat/completions`);
144
+ console.log(`DeepSeek format: POST /api/v0/chat/completion`);
145
+ console.log(`Models: GET /v1/models`);
146
+ console.log(`Admin panel: http://localhost:${PORT}/admin`);
147
+
148
+ await initTokenPool();
149
+
150
+ const aliveTokens = getAliveTokens();
151
+ await prewarmSessions(aliveTokens);
152
+
153
+ startHealthCheck();
154
+ });