Spaces:
Running
Running
huanx520 commited on
Commit ·
998a91e
1
Parent(s): 027af26
feat: 全新控制台 - 双栏布局 + 内置API测试
Browse files- 左右双栏:左侧接口测试,右侧账号管理
- 内置 /v1/chat/completions 实时测试(模型选择+流式开关+响应计时)
- 全中文界面,幽灵终端配色,响应式手机适配
- 顶部内联统计栏(账号/活跃/可用/在线/排队)
- 删除旧 test.html
- 状态 badge:在线/离线/使用中/空闲
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 ·
|
| 484 |
<style>
|
| 485 |
-
:root{--bg:#
|
| 486 |
*{box-sizing:border-box;margin:0;padding:0}
|
| 487 |
-
body{font-family:'JetBrains Mono','Sarasa Mono SC','
|
| 488 |
-
body::
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
.topbar{
|
| 494 |
-
.topbar .
|
| 495 |
-
.topbar .
|
| 496 |
-
.topbar .
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
.
|
| 502 |
-
@media(max-width:
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
.
|
| 506 |
-
.
|
| 507 |
-
.
|
| 508 |
-
|
| 509 |
-
.
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
|
| 541 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 542 |
.spacer{flex:1}
|
| 543 |
-
|
| 544 |
-
.
|
| 545 |
-
.
|
| 546 |
-
|
| 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 |
-
<
|
| 562 |
-
<span class="
|
| 563 |
-
<div class="
|
| 564 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
</div>
|
| 566 |
|
| 567 |
<div class="main">
|
| 568 |
-
|
| 569 |
-
<
|
| 570 |
-
|
| 571 |
-
<div class="
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
</div>
|
| 576 |
|
| 577 |
-
<!-- 账号
|
| 578 |
-
<div class="
|
| 579 |
-
<div class="
|
| 580 |
-
<h2>
|
| 581 |
-
<button class="btn btn-sm" onclick="
|
| 582 |
</div>
|
| 583 |
-
<div class="
|
| 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">
|
| 587 |
</table>
|
| 588 |
</div>
|
| 589 |
</div>
|
| 590 |
|
| 591 |
-
<
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
<h2><span class="prompt">></span>导入账号</h2>
|
| 595 |
</div>
|
| 596 |
-
<div class="
|
| 597 |
-
<div class="help">格式:邮箱:密码 ,每行一个
|
| 598 |
-
<textarea id="inp" placeholder="user@gmail.com:password"></textarea>
|
| 599 |
-
<div class="
|
| 600 |
<button class="btn btn-accent" onclick="doImport()">▸ 导入</button>
|
| 601 |
-
<span id="msg" style="font-size:
|
| 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(),
|
| 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
|
| 621 |
return r.json()
|
| 622 |
}
|
| 623 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 624 |
try{
|
| 625 |
const s=await api('/readyz')
|
| 626 |
-
document.getElementById('
|
| 627 |
-
`<
|
| 628 |
-
<
|
| 629 |
-
<
|
| 630 |
-
<
|
| 631 |
-
<
|
| 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?'
|
| 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+'
|
| 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)">
|
| 648 |
}
|
| 649 |
}
|
|
|
|
|
|
|
| 650 |
async function doImport(){
|
| 651 |
const v=document.getElementById('inp').value.trim()
|
| 652 |
-
if(!v)return toast('请
|
| 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('格式错误
|
| 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='
|
| 668 |
-
toast('
|
| 669 |
loadAll()
|
| 670 |
-
}catch(e){toast(
|
| 671 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 672 |
loadAll()
|
| 673 |
-
setInterval(loadAll,
|
| 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 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 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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|