huanx520 commited on
Commit
998a91e
·
1 Parent(s): 027af26

feat: 全新控制台 - 双栏布局 + 内置API测试

Browse files

- 左右双栏:左侧接口测试,右侧账号管理
- 内置 /v1/chat/completions 实时测试(模型选择+流式开关+响应计时)
- 全中文界面,幽灵终端配色,响应式手机适配
- 顶部内联统计栏(账号/活跃/可用/在线/排队)
- 删除旧 test.html
- 状态 badge:在线/离线/使用中/空闲

Files changed (2) hide show
  1. main.py +217 -114
  2. test.html +0 -292
main.py CHANGED
@@ -480,156 +480,251 @@ ADMIN_HTML = """<!DOCTYPE html>
480
  <head>
481
  <meta charset="UTF-8">
482
  <meta name="viewport" content="width=device-width,initial-scale=1">
483
- <title>DS2API · 终端</title>
484
  <style>
485
- :root{--bg:#080c12;--panel:#0f1620;--border:#1a2738;--text:#a8c0d8;--dim:#4a6078;--accent:#64d8ff;--green:#4ade80;--red:#f87171;--amber:#fbbf24}
486
  *{box-sizing:border-box;margin:0;padding:0}
487
- body{font-family:'JetBrains Mono','Sarasa Mono SC','Source Code Pro','Cascadia Code',monospace;background:var(--bg);color:var(--text);min-height:100vh;font-size:13px;line-height:1.6;-webkit-font-smoothing:antialiased}
488
- body::before{content:'';position:fixed;inset:0;background:
489
- radial-gradient(ellipse 80% 50% at 50% -20%,rgba(100,216,255,.04),transparent),
490
- linear-gradient(180deg,transparent 0%,rgba(100,216,255,.01) 50%,transparent 100%);
491
- pointer-events:none;z-index:0}
492
-
493
- .topbar{position:sticky;top:0;z-index:10;background:var(--panel);border-bottom:1px solid var(--border);padding:14px 24px;display:flex;align-items:center;gap:12px}
494
- .topbar .dot{width:7px;height:7px;background:var(--green);box-shadow:0 0 6px var(--green);animation:glow 2s ease-in-out infinite}
495
- .topbar .title{font-weight:800;font-size:14px;color:var(--accent);letter-spacing:2px}
496
- .topbar .tag{font-size:10px;color:var(--dim);letter-spacing:1px;border:1px solid var(--border);padding:3px 8px}
497
- @keyframes glow{0%,100%{box-shadow:0 0 4px var(--green)}50%{box-shadow:0 0 14px var(--green)}}
498
-
499
- .main{position:relative;z-index:1;max-width:960px;margin:0 auto;padding:28px 16px}
500
-
501
- .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin-bottom:28px}
502
- @media(max-width:600px){.grid{grid-template-columns:repeat(2,1fr);gap:8px}}
503
-
504
- .stat{background:var(--panel);border:1px solid var(--border);padding:18px 16px;position:relative;overflow:hidden}
505
- .stat::before{content:'';position:absolute;inset:0;background:linear-gradient(135deg,rgba(100,216,255,.03) 0%,transparent 60%);pointer-events:none}
506
- .stat .num{font-size:42px;font-weight:800;color:var(--accent);line-height:1}
507
- .stat .label{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:2px;margin-top:4px}
508
-
509
- .card{background:var(--panel);border:1px solid var(--border);margin-bottom:16px}
510
- .card-head{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid var(--border);background:rgba(100,216,255,.02)}
511
- .card-head h2{font-size:11px;color:var(--accent);letter-spacing:2px;font-weight:800}
512
- .card-head .prompt{color:var(--dim);font-weight:800;margin-right:6px}
513
- .card-body{padding:16px 18px}
514
-
515
- table{width:100%;border-collapse:collapse}
516
- thead{border-bottom:2px solid var(--border)}
517
- th{padding:10px 8px;text-align:left;color:var(--dim);font-weight:700;font-size:10px;letter-spacing:1px;white-space:nowrap}
518
- td{padding:9px 8px;border-bottom:1px solid rgba(26,39,56,.6);word-break:break-all;font-size:12px}
519
- tr:hover td{background:rgba(100,216,255,.02)}
520
- @media(max-width:600px){th{font-size:9px;padding:8px 4px}td{font-size:11px;padding:8px 4px}}
521
-
522
- .badge{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;font-size:10px;font-weight:700;letter-spacing:.5px;white-space:nowrap}
523
- .badge::before{content:'';width:5px;height:5px;display:inline-block}
524
- .badge-on{color:var(--green);border:1px solid rgba(74,222,128,.35)}.badge-on::before{background:var(--green)}
525
- .badge-off{color:var(--red);border:1px solid rgba(248,113,113,.3)}.badge-off::before{background:var(--red)}
526
- .badge-idle{color:var(--dim);border:1px solid var(--border)}.badge-idle::before{background:var(--dim)}
527
-
528
- .btn{display:inline-flex;align-items:center;gap:6px;padding:9px 18px;border:1px solid var(--border);background:transparent;color:var(--text);cursor:pointer;font-family:inherit;font-size:11px;font-weight:700;letter-spacing:1px;transition:all .15s;white-space:nowrap}
529
- .btn:hover{border-color:var(--accent);color:var(--accent);background:rgba(100,216,255,.04)}
530
- .btn-accent{background:var(--accent);color:var(--bg);border-color:var(--accent);font-weight:800}
531
- .btn-accent:hover{background:transparent;color:var(--accent)}
532
- .btn-sm{padding:6px 12px;font-size:10px}
533
- @media(max-width:600px){.btn{padding:7px 12px;font-size:10px}}
534
-
535
- textarea{width:100%;background:var(--bg);border:1px solid var(--border);padding:12px;color:var(--text);font-family:inherit;font-size:12px;line-height:1.7;min-height:90px;resize:vertical}
536
- textarea:focus{outline:none;border-color:var(--accent);box-shadow:inset 0 0 0 1px rgba(100,216,255,.1)}
537
  textarea::placeholder{color:var(--dim)}
 
538
 
539
- .help{font-size:10px;color:var(--dim);margin-bottom:10px;letter-spacing:.5px;opacity:.7}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
 
541
- .bar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:10px}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
542
  .spacer{flex:1}
543
-
544
- .toast{position:fixed;top:24px;right:24px;z-index:99;padding:12px 20px;font-size:12px;font-weight:700;letter-spacing:.5px;animation:slide .25s;backdrop-filter:blur(8px)}
545
- .toast-ok{background:rgba(74,222,128,.12);color:var(--green);border:1px solid rgba(74,222,128,.25)}
546
- .toast-err{background:rgba(248,113,113,.12);color:var(--red);border:1px solid rgba(248,113,113,.25)}
547
- @keyframes slide{from{transform:translateY(-10px);opacity:0}to{transform:translateY(0);opacity:1}}
548
-
549
- .empty{color:var(--dim);padding:32px 0;text-align:center;font-size:12px;letter-spacing:1px}
550
- .hide-mobile{display:table-cell}
551
- @media(max-width:600px){.hide-mobile{display:none}}
552
-
553
- .pulse{animation:flicker 3s ease-in-out infinite}
554
- @keyframes flicker{0%,100%{opacity:1}50%{opacity:.5}}
555
-
556
- .ellipsis{max-width:180px;overflow:hidden;text-overflow:ellipsis;display:block}
557
  </style>
558
  </head>
559
  <body>
560
  <div class="topbar">
561
- <div class="dot"></div>
562
- <span class="title">DS2API</span>
563
- <div class="spacer" style="flex:1"></div>
564
- <span class="tag"> 浏览器模式</span>
 
 
 
 
 
565
  </div>
566
 
567
  <div class="main">
568
- <!-- 统计卡片 -->
569
- <div class="grid" id="stats">
570
- <div class="stat"><div class="num">—</div><div class="label">账号总数</div></div>
571
- <div class="stat"><div class="num">—</div><div class="label">使用中</div></div>
572
- <div class="stat"><div class="num">—</div><div class="label">可用</div></div>
573
- <div class="stat"><div class="num">—</div><div class="label">已登录</div></div>
574
- <div class="stat"><div class="num">—</div><div class="label">排队中</div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
575
  </div>
576
 
577
- <!-- 账号列表 -->
578
- <div class="card">
579
- <div class="card-head">
580
- <h2><span class="prompt">&gt;</span>账号列表</h2>
581
- <button class="btn btn-sm" onclick="loadAll()">刷新</button>
582
  </div>
583
- <div class="card-body">
584
- <table>
585
  <thead><tr><th>邮箱</th><th class="hide-mobile">备注</th><th>登录</th><th>状态</th><th class="hide-mobile">错误</th></tr></thead>
586
- <tbody id="tbl"><tr><td colspan="5" class="empty">正在加载…</td></tr></tbody>
587
  </table>
588
  </div>
589
  </div>
590
 
591
- <!-- 导入账号 -->
592
- <div class="card">
593
- <div class="card-head">
594
- <h2><span class="prompt">&gt;</span>导入账号</h2>
595
  </div>
596
- <div class="card-body">
597
- <div class="help">格式:邮箱:密码 ,每行一个账号</div>
598
- <textarea id="inp" placeholder="user@gmail.com:password"></textarea>
599
- <div class="bar">
600
  <button class="btn btn-accent" onclick="doImport()">▸ 导入</button>
601
- <span id="msg" style="font-size:11px;color:var(--dim)"></span>
602
  </div>
603
  </div>
604
  </div>
 
605
  </div>
606
 
607
  <script>
608
  const H=location.origin
 
 
609
  function toast(m,ok){
610
  const e=document.createElement('div')
611
  e.className='toast toast-'+(ok?'ok':'err')
612
  e.textContent=m
613
  document.body.appendChild(e)
614
- setTimeout(()=>e.remove(),2800)
615
  }
 
616
  async function api(p,o={}){
617
  const hd={};if(o.json)hd['Content-Type']='application/json'
618
  Object.assign(hd,o.headers||{})
619
  const r=await fetch(H+p,{headers:hd,method:o.method||'GET',body:o.body})
620
- if(!r.ok){const e=await r.text();throw new Error(e||r.status)}
621
  return r.json()
622
  }
623
- async function loadAll(){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
624
  try{
625
  const s=await api('/readyz')
626
- document.getElementById('stats').innerHTML=
627
- `<div class="stat"><div class="num">${s.accounts.total}</div><div class="label">账号总数</div></div>
628
- <div class="stat"><div class="num">${s.accounts.in_use}</div><div class="label">使用中</div></div>
629
- <div class="stat"><div class="num">${s.accounts.available}</div><div class="label">可用</div></div>
630
- <div class="stat"><div class="num">${s.accounts.logged_in}</div><div class="label">已登录</div></div>
631
- <div class="stat"><div class="num">${s.accounts.queue_size}</div><div class="label">排队中</div></div>`
632
  }catch(e){}
 
 
633
  try{
634
  const d=await api('/admin/accounts',{headers:{'admin-key':'admin'}})
635
  let r=''
@@ -637,26 +732,28 @@ async function loadAll(){
637
  r+=`<tr>
638
  <td><span class="ellipsis">${a.email}</span></td>
639
  <td class="hide-mobile">${a.name||'—'}</td>
640
- <td><span class="badge ${a.logged_in?'badge-on':'badge-off'}">${a.logged_in?'已登录':'未登录'}</span></td>
641
  <td><span class="badge ${a.in_use?'badge-on':'badge-idle'}">${a.in_use?'使用中':'空闲'}</span></td>
642
- <td class="hide-mobile">${a.error_count>0?'<span class="badge badge-off">'+a.error_count+'</span>':'—'}</td>
643
  </tr>`
644
  }
645
  document.getElementById('tbl').innerHTML=r||'<tr><td colspan="5" class="empty">暂无账号</td></tr>'
646
  }catch(e){
647
- document.getElementById('tbl').innerHTML='<tr><td colspan="5" style="color:var(--red)">加载失败:'+e.message+'</td></tr>'
648
  }
649
  }
 
 
650
  async function doImport(){
651
  const v=document.getElementById('inp').value.trim()
652
- if(!v)return toast('请输入账号信息',0)
653
  const accts=[]
654
  for(const l of v.split('\\n')){
655
  const t=l.trim();if(!t)continue
656
  const p=t.split(':',3)
657
  if(p.length>=2)accts.push({email:p[0].trim(),password:p[1],name:p[2]||''})
658
  }
659
- if(!accts.length)return toast('格式错误,请用 邮箱:密码 格式',0)
660
  try{
661
  const d=await api('/admin/accounts/import',{
662
  method:'POST',json:true,
@@ -664,13 +761,19 @@ async function doImport(){
664
  headers:{'admin-key':'admin'}
665
  })
666
  document.getElementById('inp').value=''
667
- document.getElementById('msg').textContent='成功导入 '+d.imported+' 个账号'
668
- toast('导入 '+d.imported+' / '+accts.length+' 个',1)
669
  loadAll()
670
- }catch(e){toast('导入失败:'+e.message,0)}
671
  }
 
 
 
 
 
 
672
  loadAll()
673
- setInterval(loadAll,15000)
674
  </script>
675
  </body>
676
  </html>"""
 
480
  <head>
481
  <meta charset="UTF-8">
482
  <meta name="viewport" content="width=device-width,initial-scale=1">
483
+ <title>DS2API · 控制台</title>
484
  <style>
485
+ :root{--bg:#060b10;--panel:#0b1219;--border:#15202e;--text:#9bb5cf;--dim:#3d5268;--accent:#5cc8ff;--green:#3fb950;--red:#f85149;--amber:#d29922;--row-hover:rgba(92,200,255,.03)}
486
  *{box-sizing:border-box;margin:0;padding:0}
487
+ body{font-family:'JetBrains Mono','Sarasa Mono SC','Cascadia Code',Consolas,monospace;background:var(--bg);color:var(--text);font-size:12.5px;line-height:1.55;-webkit-font-smoothing:antialiased;min-height:100vh}
488
+ body::after{content:'';position:fixed;inset:0;background:radial-gradient(ellipse 60% 40% at 50% -10%,rgba(92,200,255,.025),transparent);pointer-events:none;z-index:0}
489
+
490
+ /* ── topbar ── */
491
+ .topbar{position:sticky;top:0;z-index:20;background:var(--panel);border-bottom:1px solid var(--border);padding:10px 20px;display:flex;align-items:center;gap:10px}
492
+ .topbar .logo{font-weight:800;font-size:13px;color:var(--accent);letter-spacing:1.5px}
493
+ .topbar .sep{color:var(--dim);margin:0 4px}
494
+ .topbar .mode{font-size:10px;color:var(--dim);border:1px solid var(--border);padding:2px 8px;letter-spacing:1px}
495
+ .topbar .stat-inline{display:flex;gap:16px;margin-left:auto;font-size:10px}
496
+ .topbar .stat-inline span{color:var(--dim)}
497
+ .topbar .stat-inline b{color:var(--accent);font-weight:800}
498
+ @media(max-width:700px){.topbar .stat-inline{display:none}}
499
+
500
+ /* ── main grid ── */
501
+ .main{position:relative;z-index:1;max-width:1100px;margin:0 auto;padding:20px 16px;display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start}
502
+ @media(max-width:800px){.main{grid-template-columns:1fr;padding:14px 10px;gap:12px}}
503
+
504
+ /* ── panel ── */
505
+ .panel{border:1px solid var(--border);background:var(--panel)}
506
+ .panel-head{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:rgba(92,200,255,.015)}
507
+ .panel-head h2{font-size:11px;color:var(--accent);letter-spacing:1.5px;font-weight:800}
508
+ .panel-head .hint{color:var(--dim);font-size:10px}
509
+ .panel-body{padding:14px}
510
+
511
+ /* ── form elements ── */
512
+ select,textarea,input[type=text]{width:100%;background:var(--bg);border:1px solid var(--border);padding:8px 10px;color:var(--text);font-family:inherit;font-size:12px;line-height:1.5}
513
+ select{padding:7px 10px;cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%233d5268'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;padding-right:28px}
514
+ select:focus,textarea:focus,input:focus{outline:none;border-color:var(--accent)}
515
+ textarea{min-height:80px;resize:vertical}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
  textarea::placeholder{color:var(--dim)}
517
+ .row{display:flex;gap:10px;align-items:center;margin-bottom:10px;flex-wrap:wrap}
518
 
519
+ /* ── buttons ── */
520
+ .btn{display:inline-flex;align-items:center;gap:5px;padding:7px 14px;border:1px solid var(--border);background:transparent;color:var(--text);cursor:pointer;font-family:inherit;font-size:11px;font-weight:700;letter-spacing:.8px;white-space:nowrap;transition:all .12s}
521
+ .btn:hover{border-color:var(--accent);color:var(--accent)}
522
+ .btn-accent{background:var(--accent);color:var(--bg);border-color:var(--accent)}
523
+ .btn-accent:hover{background:transparent;color:var(--accent)}
524
+ .btn-sm{padding:5px 10px;font-size:10px}
525
+
526
+ /* ── table ── */
527
+ .tbl{width:100%;border-collapse:collapse;font-size:11px}
528
+ .tbl thead{border-bottom:2px solid var(--border)}
529
+ .tbl th{padding:7px 6px;text-align:left;color:var(--dim);font-weight:700;font-size:9.5px;letter-spacing:.8px;white-space:nowrap}
530
+ .tbl td{padding:7px 6px;border-bottom:1px solid rgba(21,32,46,.6);word-break:break-all}
531
+ .tbl tr:hover td{background:var(--row-hover)}
532
+ @media(max-width:500px){.tbl th,.tbl td{font-size:10px;padding:6px 4px}}
533
+
534
+ /* ── badge ── */
535
+ .badge{display:inline-flex;align-items:center;gap:3px;padding:1px 7px;font-size:9.5px;font-weight:700;letter-spacing:.4px;white-space:nowrap}
536
+ .badge::before{content:'';width:4px;height:4px}
537
+ .badge-on{color:var(--green);border:1px solid rgba(63,185,80,.35)}.badge-on::before{background:var(--green)}
538
+ .badge-off{color:var(--red);border:1px solid rgba(248,81,73,.3)}.badge-off::before{background:var(--red)}
539
+ .badge-idle{color:var(--dim);border:1px solid var(--border)}.badge-idle::before{background:var(--dim)}
540
 
541
+ /* ── response area ── */
542
+ #response{background:var(--bg);border:1px solid var(--border);border-top:none;padding:12px;min-height:120px;max-height:400px;overflow-y:auto;font-size:12px;line-height:1.6;white-space:pre-wrap}
543
+ #response:empty::after{content:'等待发送…';color:var(--dim)}
544
+ .response-status{display:flex;justify-content:space-between;padding:6px 10px;font-size:10px;border-bottom:1px solid var(--border)}
545
+ .response-status .ok{color:var(--green)}.response-status .err{color:var(--red)}
546
+
547
+ /* ── toast ── */
548
+ .toast{position:fixed;top:20px;right:20px;z-index:99;padding:10px 18px;font-size:11px;font-weight:700;letter-spacing:.5px;animation:slide .25s;border:1px solid}
549
+ .toast-ok{background:rgba(63,185,80,.1);color:var(--green);border-color:rgba(63,185,80,.25)}
550
+ .toast-err{background:rgba(248,81,73,.1);color:var(--red);border-color:rgba(248,81,73,.25)}
551
+ @keyframes slide{from{transform:translateY(-8px);opacity:0}to{transform:translateY(0);opacity:1}}
552
+
553
+ /* ── misc ── */
554
+ .empty{color:var(--dim);padding:20px 0;text-align:center;font-size:11px}
555
+ .hidden{display:none}
556
  .spacer{flex:1}
557
+ .ellipsis{max-width:160px;overflow:hidden;text-overflow:ellipsis;display:block}
558
+ .help{font-size:10px;color:var(--dim);margin-bottom:8px;opacity:.7}
559
+ .imp{padding:12px;margin-top:12px}
560
+ @media(max-width:500px){.hide-mobile{display:none}}
 
 
 
 
 
 
 
 
 
 
561
  </style>
562
  </head>
563
  <body>
564
  <div class="topbar">
565
+ <span class="logo">▸ DS2API</span>
566
+ <span class="mode">浏览器模式</span>
567
+ <div class="stat-inline" id="topStats">
568
+ <span>账号 <b>—</b></span>
569
+ <span>活跃 <b>—</b></span>
570
+ <span>可用 <b>—</b></span>
571
+ <span>在线 <b>—</b></span>
572
+ <span>排队 <b>—</b></span>
573
+ </div>
574
  </div>
575
 
576
  <div class="main">
577
+
578
+ <!-- ═══ 左栏:接口测试 ═══ -->
579
+ <div class="panel" style="grid-row:span 1">
580
+ <div class="panel-head">
581
+ <h2>接口测试</h2>
582
+ <span class="hint">/v1/chat/completions</span>
583
+ </div>
584
+ <div class="panel-body">
585
+ <div class="row">
586
+ <select id="model" style="flex:1">
587
+ <option value="deepseek-v4-flash">deepseek-v4-flash</option>
588
+ <option value="deepseek-v4-pro">deepseek-v4-pro</option>
589
+ <option value="deepseek-chat">deepseek-chat</option>
590
+ <option value="deepseek-reasoner">deepseek-reasoner</option>
591
+ </select>
592
+ <label style="font-size:11px;color:var(--dim);display:flex;align-items:center;gap:4px;white-space:nowrap">
593
+ <input type="checkbox" id="stream" checked> 流式
594
+ </label>
595
+ </div>
596
+ <textarea id="prompt" placeholder="输入消息…">你好,用一句话介绍你自己</textarea>
597
+ <div class="row" style="margin-top:10px;margin-bottom:0">
598
+ <button class="btn btn-accent" onclick="sendMsg()" id="sendBtn">▸ 发送</button>
599
+ <button class="btn btn-sm" onclick="sendMsg()" id="sendBtn2" style="display:none">▸ 发送</button>
600
+ <span id="reqStatus" style="font-size:10px;color:var(--dim)"></span>
601
+ <div class="spacer"></div>
602
+ <button class="btn btn-sm" onclick="document.getElementById('response').textContent=''">清空</button>
603
+ </div>
604
+ <div style="margin-top:12px;border:1px solid var(--border);border-bottom:none">
605
+ <div class="response-status">
606
+ <span id="respLabel">响应</span>
607
+ <span id="respTime"></span>
608
+ </div>
609
+ <div id="response"></div>
610
+ </div>
611
+ </div>
612
  </div>
613
 
614
+ <!-- ═══ 右栏:账号管理 ═══ -->
615
+ <div class="panel">
616
+ <div class="panel-head">
617
+ <h2>账号管理</h2>
618
+ <button class="btn btn-sm" onclick="loadAccounts()">刷新</button>
619
  </div>
620
+ <div class="panel-body" style="padding-bottom:8px">
621
+ <table class="tbl">
622
  <thead><tr><th>邮箱</th><th class="hide-mobile">备注</th><th>登录</th><th>状态</th><th class="hide-mobile">错误</th></tr></thead>
623
+ <tbody id="tbl"><tr><td colspan="5" class="empty">加载…</td></tr></tbody>
624
  </table>
625
  </div>
626
  </div>
627
 
628
+ <div class="panel">
629
+ <div class="panel-head">
630
+ <h2>导入账号</h2>
 
631
  </div>
632
+ <div class="panel-body">
633
+ <div class="help">格式:邮箱:密码 ,每行一个</div>
634
+ <textarea id="inp" placeholder="user@gmail.com:password&#10;user2@gmail.com:password" style="min-height:70px"></textarea>
635
+ <div class="row" style="margin-top:10px;margin-bottom:0">
636
  <button class="btn btn-accent" onclick="doImport()">▸ 导入</button>
637
+ <span id="msg" style="font-size:10px;color:var(--dim)"></span>
638
  </div>
639
  </div>
640
  </div>
641
+
642
  </div>
643
 
644
  <script>
645
  const H=location.origin
646
+ const KEY='sbgptwcnmsbopenaiwdnmdcnmsbchat'
647
+
648
  function toast(m,ok){
649
  const e=document.createElement('div')
650
  e.className='toast toast-'+(ok?'ok':'err')
651
  e.textContent=m
652
  document.body.appendChild(e)
653
+ setTimeout(()=>e.remove(),2500)
654
  }
655
+
656
  async function api(p,o={}){
657
  const hd={};if(o.json)hd['Content-Type']='application/json'
658
  Object.assign(hd,o.headers||{})
659
  const r=await fetch(H+p,{headers:hd,method:o.method||'GET',body:o.body})
660
+ if(!r.ok){const t=await r.text();throw new Error(t||r.status)}
661
  return r.json()
662
  }
663
+
664
+ /* ── 接口测试 ── */
665
+ async function sendMsg(){
666
+ const model=document.getElementById('model').value
667
+ const prompt=document.getElementById('prompt').value.trim()
668
+ const stream=document.getElementById('stream').checked
669
+ const resp=document.getElementById('response')
670
+ const status=document.getElementById('reqStatus')
671
+ const timeEl=document.getElementById('respTime')
672
+ const btn=document.getElementById('sendBtn')
673
+
674
+ if(!prompt)return toast('请输入消息',0)
675
+ btn.disabled=true;btn.textContent='…'
676
+ resp.textContent='';timeEl.textContent=''
677
+
678
+ const t0=Date.now()
679
+ try{
680
+ const r=await fetch(H+'/v1/chat/completions',{
681
+ method:'POST',
682
+ headers:{'Content-Type':'application/json','Authorization':'Bearer '+KEY},
683
+ body:JSON.stringify({model,messages:[{role:'user',content:prompt}],stream})
684
+ })
685
+
686
+ if(stream){
687
+ const reader=r.body.getReader(),dec=new TextDecoder()
688
+ let full=''
689
+ while(1){
690
+ const{done,value}=await reader.read()
691
+ if(done)break
692
+ for(const line of dec.decode(value,{stream:true}).split('\\n')){
693
+ if(!line.startsWith('data: '))continue
694
+ const d=line.slice(6).trim()
695
+ if(d==='[DONE]')continue
696
+ try{const j=JSON.parse(d);const c=j.choices?.[0]?.delta?.content;if(c){full+=c;resp.textContent=full}}
697
+ catch(e){}
698
+ }
699
+ }
700
+ timeEl.textContent=((Date.now()-t0)/1000).toFixed(1)+'s'
701
+ status.textContent='流式完成';status.className='ok'
702
+ }else{
703
+ const d=await r.json()
704
+ resp.textContent=d.choices?.[0]?.message?.content||JSON.stringify(d,null,2)
705
+ timeEl.textContent=((Date.now()-t0)/1000).toFixed(1)+'s'
706
+ status.textContent=r.status+' OK';status.className='ok'
707
+ }
708
+ }catch(e){
709
+ resp.textContent='错误: '+e.message
710
+ status.textContent='失败';status.className='err'
711
+ }
712
+ btn.disabled=false;btn.textContent='▸ 发送'
713
+ }
714
+
715
+ /* ── 统计 & 账号 ── */
716
+ async function loadStats(){
717
  try{
718
  const s=await api('/readyz')
719
+ document.getElementById('topStats').innerHTML=
720
+ `<span>账号 <b>${s.accounts.total}</b></span>
721
+ <span>活跃 <b>${s.accounts.in_use}</b></span>
722
+ <span>可用 <b>${s.accounts.available}</b></span>
723
+ <span>在线 <b>${s.accounts.logged_in}</b></span>
724
+ <span>排队 <b>${s.accounts.queue_size}</b></span>`
725
  }catch(e){}
726
+ }
727
+ async function loadAccounts(){
728
  try{
729
  const d=await api('/admin/accounts',{headers:{'admin-key':'admin'}})
730
  let r=''
 
732
  r+=`<tr>
733
  <td><span class="ellipsis">${a.email}</span></td>
734
  <td class="hide-mobile">${a.name||'—'}</td>
735
+ <td><span class="badge ${a.logged_in?'badge-on':'badge-off'}">${a.logged_in?'在线':'离线'}</span></td>
736
  <td><span class="badge ${a.in_use?'badge-on':'badge-idle'}">${a.in_use?'使用中':'空闲'}</span></td>
737
+ <td class="hide-mobile">${a.error_count>0?'<span class="badge badge-off">'+a.error_count+'</span>':'—'}</td>
738
  </tr>`
739
  }
740
  document.getElementById('tbl').innerHTML=r||'<tr><td colspan="5" class="empty">暂无账号</td></tr>'
741
  }catch(e){
742
+ document.getElementById('tbl').innerHTML='<tr><td colspan="5" style="color:var(--red)">'+e.message+'</td></tr>'
743
  }
744
  }
745
+ async function loadAll(){await loadStats();await loadAccounts()}
746
+
747
  async function doImport(){
748
  const v=document.getElementById('inp').value.trim()
749
+ if(!v)return toast('请输入账号',0)
750
  const accts=[]
751
  for(const l of v.split('\\n')){
752
  const t=l.trim();if(!t)continue
753
  const p=t.split(':',3)
754
  if(p.length>=2)accts.push({email:p[0].trim(),password:p[1],name:p[2]||''})
755
  }
756
+ if(!accts.length)return toast('格式错误',0)
757
  try{
758
  const d=await api('/admin/accounts/import',{
759
  method:'POST',json:true,
 
761
  headers:{'admin-key':'admin'}
762
  })
763
  document.getElementById('inp').value=''
764
+ document.getElementById('msg').textContent='导入 '+d.imported+' 个'
765
+ toast('成功导入 '+d.imported+' 个',1)
766
  loadAll()
767
+ }catch(e){toast(e.message,0)}
768
  }
769
+
770
+ // 回车发送
771
+ document.getElementById('prompt').addEventListener('keydown',e=>{
772
+ if(e.ctrlKey&&e.key==='Enter')sendMsg()
773
+ })
774
+
775
  loadAll()
776
+ setInterval(loadAll,12000)
777
  </script>
778
  </body>
779
  </html>"""
test.html DELETED
@@ -1,292 +0,0 @@
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>DS2API Browser 测试</title>
7
- <style>
8
- * { box-sizing: border-box; margin: 0; padding: 0; }
9
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; padding: 20px; }
10
- .container { max-width: 800px; margin: 0 auto; }
11
- h1 { text-align: center; margin-bottom: 20px; color: #333; }
12
- .card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
13
- .form-group { margin-bottom: 15px; }
14
- label { display: block; margin-bottom: 5px; font-weight: 500; color: #555; }
15
- input, textarea, select { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
16
- textarea { min-height: 100px; resize: vertical; }
17
- button { background: #007bff; color: white; border: none; padding: 12px 24px; border-radius: 4px; cursor: pointer; font-size: 16px; width: 100%; }
18
- button:hover { background: #0056b3; }
19
- button:disabled { background: #ccc; cursor: not-allowed; }
20
- .response { background: #f8f9fa; border-radius: 4px; padding: 15px; margin-top: 15px; white-space: pre-wrap; word-wrap: break-word; min-height: 50px; }
21
- .loading { text-align: center; color: #666; }
22
- .error { color: #dc3545; }
23
- .success { color: #28a745; }
24
- .info { display: flex; justify-content: space-between; margin-top: 10px; font-size: 12px; color: #888; }
25
- .checkbox-group { display: flex; align-items: center; gap: 10px; }
26
- .checkbox-group input { width: auto; }
27
- </style>
28
- </head>
29
- <body>
30
- <div class="container">
31
- <h1>🤖 DS2API Browser 测试</h1>
32
-
33
- <div class="card">
34
- <div class="form-group">
35
- <label>API 地址</label>
36
- <input type="text" id="apiUrl" value="http://localhost:5002">
37
- </div>
38
- <div class="form-group">
39
- <label>API Key</label>
40
- <input type="text" id="apiKey" value="sk-test123456">
41
- </div>
42
- <div class="form-group">
43
- <label>模型</label>
44
- <select id="model">
45
- <option value="deepseek-v4-flash">deepseek-v4-flash (默认)</option>
46
- <option value="deepseek-v4-pro">deepseek-v4-pro (专家)</option>
47
- <option value="deepseek-v4-flash-search">deepseek-v4-flash-search</option>
48
- <option value="deepseek-v4-pro-search">deepseek-v4-pro-search</option>
49
- </select>
50
- </div>
51
- <div class="form-group">
52
- <div class="checkbox-group">
53
- <input type="checkbox" id="stream" checked>
54
- <label for="stream">流式响应</label>
55
- </div>
56
- </div>
57
- <div class="form-group">
58
- <label>消息</label>
59
- <textarea id="message" placeholder="输入你的消息...">你好,请介绍一下你自己</textarea>
60
- </div>
61
- <button id="sendBtn" onclick="sendMessage()">发送消息</button>
62
- </div>
63
-
64
- <div class="card">
65
- <label>响应</label>
66
- <div id="response" class="response">等待发送...</div>
67
- <div class="info">
68
- <span id="status"></span>
69
- <span id="time"></span>
70
- </div>
71
- </div>
72
-
73
- <div class="card">
74
- <label>账号管理</label>
75
- <div class="form-group" style="margin-top: 10px;">
76
- <label>导入账号 (格式: email:password,每行一个)</label>
77
- <textarea id="accountsInput" rows="4" placeholder="user1@gmail.com:password1&#10;user2@gmail.com:password2"></textarea>
78
- </div>
79
- <div style="display: flex; gap: 10px;">
80
- <button onclick="importAccounts()" style="flex: 1;">导入账号</button>
81
- <button onclick="loadAccounts()" style="flex: 1; background: #6c757d;">刷新账号列表</button>
82
- </div>
83
- <div id="accountsList" class="response" style="margin-top: 10px; max-height: 200px; overflow-y: auto;">点击"刷新账号列表"查看...</div>
84
- </div>
85
- </div>
86
-
87
- <script>
88
- async function sendMessage() {
89
- const apiUrl = document.getElementById('apiUrl').value;
90
- const apiKey = document.getElementById('apiKey').value;
91
- const model = document.getElementById('model').value;
92
- const message = document.getElementById('message').value;
93
- const isStream = document.getElementById('stream').checked;
94
- const responseDiv = document.getElementById('response');
95
- const statusSpan = document.getElementById('status');
96
- const timeSpan = document.getElementById('time');
97
- const sendBtn = document.getElementById('sendBtn');
98
-
99
- if (!message.trim()) {
100
- responseDiv.textContent = '请输入消息';
101
- responseDiv.className = 'response error';
102
- return;
103
- }
104
-
105
- sendBtn.disabled = true;
106
- sendBtn.textContent = '发送中...';
107
- responseDiv.textContent = '正在等待响应...';
108
- responseDiv.className = 'response loading';
109
- statusSpan.textContent = '';
110
- timeSpan.textContent = '';
111
-
112
- const startTime = Date.now();
113
-
114
- try {
115
- const response = await fetch(`${apiUrl}/v1/chat/completions`, {
116
- method: 'POST',
117
- headers: {
118
- 'Content-Type': 'application/json',
119
- 'Authorization': `Bearer ${apiKey}`
120
- },
121
- body: JSON.stringify({
122
- model: model,
123
- messages: [{ role: 'user', content: message }],
124
- stream: isStream
125
- })
126
- });
127
-
128
- if (isStream) {
129
- const reader = response.body.getReader();
130
- const decoder = new TextDecoder();
131
- let fullContent = '';
132
-
133
- while (true) {
134
- const { done, value } = await reader.read();
135
- if (done) break;
136
-
137
- const chunk = decoder.decode(value, { stream: true });
138
- const lines = chunk.split('\n');
139
-
140
- for (const line of lines) {
141
- if (line.startsWith('data: ')) {
142
- const data = line.slice(6).trim();
143
- if (data === '[DONE]') continue;
144
-
145
- try {
146
- const json = JSON.parse(data);
147
- const content = json.choices?.[0]?.delta?.content;
148
- if (content) {
149
- fullContent += content;
150
- responseDiv.textContent = fullContent;
151
- responseDiv.className = 'response success';
152
- }
153
- } catch (e) {
154
- // Skip invalid JSON
155
- }
156
- }
157
- }
158
- }
159
-
160
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
161
- statusSpan.textContent = `状态: 流式完成`;
162
- timeSpan.textContent = `耗时: ${elapsed}s`;
163
- } else {
164
- const data = await response.json();
165
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
166
-
167
- if (response.ok) {
168
- const content = data.choices?.[0]?.message?.content || '无响应内容';
169
- responseDiv.textContent = content;
170
- responseDiv.className = 'response success';
171
- statusSpan.textContent = `状态: ${response.status || 200} OK`;
172
- } else {
173
- responseDiv.textContent = `错误: ${data.detail || JSON.stringify(data)}`;
174
- responseDiv.className = 'response error';
175
- statusSpan.textContent = `状态: ${response.status || '错误'}`;
176
- }
177
-
178
- timeSpan.textContent = `耗时: ${elapsed}s`;
179
- }
180
-
181
- } catch (error) {
182
- responseDiv.textContent = `请求失败: ${error.message}`;
183
- responseDiv.className = 'response error';
184
- statusSpan.textContent = '错误';
185
- } finally {
186
- sendBtn.disabled = false;
187
- sendBtn.textContent = '发送消息';
188
- }
189
- }
190
-
191
- document.getElementById('message').addEventListener('keydown', function(e) {
192
- if (e.ctrlKey && e.key === 'Enter') {
193
- sendMessage();
194
- }
195
- });
196
-
197
- async function importAccounts() {
198
- const apiUrl = document.getElementById('apiUrl').value;
199
- const apiKey = document.getElementById('apiKey').value;
200
- const accountsText = document.getElementById('accountsInput').value.trim();
201
- const accountsList = document.getElementById('accountsList');
202
-
203
- if (!accountsText) {
204
- accountsList.textContent = '请输入账号信息';
205
- accountsList.className = 'response error';
206
- return;
207
- }
208
-
209
- const lines = accountsText.split('\n').filter(line => line.trim());
210
- const accounts = [];
211
-
212
- for (const line of lines) {
213
- const [email, password] = line.split(':');
214
- if (email && password) {
215
- accounts.push({ email: email.trim(), password: password.trim() });
216
- }
217
- }
218
-
219
- if (accounts.length === 0) {
220
- accountsList.textContent = '格式错误,请使用 email:password 格式';
221
- accountsList.className = 'response error';
222
- return;
223
- }
224
-
225
- accountsList.textContent = '导入中...';
226
- accountsList.className = 'response loading';
227
-
228
- try {
229
- const response = await fetch(`${apiUrl}/admin/accounts/import`, {
230
- method: 'POST',
231
- headers: {
232
- 'Content-Type': 'application/json',
233
- 'Authorization': `Bearer ${apiKey}`,
234
- 'admin-key': 'admin'
235
- },
236
- body: JSON.stringify({ accounts })
237
- });
238
-
239
- const data = await response.json();
240
-
241
- if (response.ok) {
242
- accountsList.textContent = JSON.stringify(data, null, 2);
243
- accountsList.className = 'response success';
244
- document.getElementById('accountsInput').value = '';
245
- } else {
246
- accountsList.textContent = `错误: ${data.detail || JSON.stringify(data)}`;
247
- accountsList.className = 'response error';
248
- }
249
- } catch (error) {
250
- accountsList.textContent = `请求失败: ${error.message}`;
251
- accountsList.className = 'response error';
252
- }
253
- }
254
-
255
- async function loadAccounts() {
256
- const apiUrl = document.getElementById('apiUrl').value;
257
- const apiKey = document.getElementById('apiKey').value;
258
- const accountsList = document.getElementById('accountsList');
259
-
260
- accountsList.textContent = '加载中...';
261
- accountsList.className = 'response loading';
262
-
263
- try {
264
- const response = await fetch(`${apiUrl}/admin/accounts`, {
265
- method: 'GET',
266
- headers: {
267
- 'Authorization': `Bearer ${apiKey}`,
268
- 'admin-key': 'admin'
269
- }
270
- });
271
-
272
- const data = await response.json();
273
-
274
- if (response.ok) {
275
- let html = `账号数量: ${data.total}\n\n`;
276
- for (const acc of data.accounts) {
277
- html += `邮箱: ${acc.email}\n状态: ${acc.status}\n使用次数: ${acc.usage_count}\n\n`;
278
- }
279
- accountsList.textContent = html;
280
- accountsList.className = 'response success';
281
- } else {
282
- accountsList.textContent = `错误: ${data.detail || JSON.stringify(data)}`;
283
- accountsList.className = 'response error';
284
- }
285
- } catch (error) {
286
- accountsList.textContent = `请求失败: ${error.message}`;
287
- accountsList.className = 'response error';
288
- }
289
- }
290
- </script>
291
- </body>
292
- </html>