| |
| |
| |
| |
| |
|
|
|
|
| export function getDashboardHTML() {
|
| return `<!DOCTYPE html>
|
| <html lang="zh-CN">
|
| <head>
|
| <meta charset="UTF-8">
|
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| <title>ChatAIBot API Proxy</title>
|
| <style>
|
| :root {
|
| --bg: #f0f2f5;
|
| --bg-card: #ffffff;
|
| --text: #1a1a2e;
|
| --text-sec: #6b7280;
|
| --border: #e5e7eb;
|
| --radius: 12px;
|
| --shadow: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
|
| --shadow-md: 0 4px 12px rgba(0,0,0,.08);
|
| --c-active: #10b981;
|
| --c-active-bg: #ecfdf5;
|
| --c-inuse: #3b82f6;
|
| --c-inuse-bg: #eff6ff;
|
| --c-login: #f59e0b;
|
| --c-login-bg: #fffbeb;
|
| --c-exhaust: #9ca3af;
|
| --c-exhaust-bg: #f3f4f6;
|
| --c-dead: #ef4444;
|
| --c-dead-bg: #fef2f2;
|
| --c-total: #8b5cf6;
|
| --c-total-bg: #f5f3ff;
|
| }
|
| * { margin: 0; padding: 0; box-sizing: border-box; }
|
| body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
|
|
|
| .header { display: flex; justify-content: space-between; align-items: center; padding: 16px 28px; background: var(--bg-card); border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 10; }
|
| .logo { font-size: 1.2rem; font-weight: 700; letter-spacing: -.5px; }
|
| .logo-sub { font-weight: 400; color: var(--text-sec); margin-left: 6px; font-size: .85rem; }
|
| .header-right { display: flex; align-items: center; gap: 16px; font-size: .85rem; color: var(--text-sec); }
|
| .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--c-active); display: inline-block; }
|
| .dot.loading { animation: pulse .6s ease-in-out infinite; }
|
| @keyframes pulse { 0%,100% { opacity:1 } 50% { opacity:.3 } }
|
|
|
| .container { max-width: 1200px; margin: 0 auto; padding: 24px; }
|
|
|
| .stats { display: grid; grid-template-columns: repeat(6, 1fr); gap: 16px; margin-bottom: 28px; }
|
| .card { background: var(--bg-card); border-radius: var(--radius); padding: 20px; box-shadow: var(--shadow); border-left: 4px solid transparent; text-align: center; transition: transform .15s, box-shadow .15s; cursor: default; }
|
| .card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
|
| .card-total { border-left-color: var(--c-total); }
|
| .card-total .card-val { color: var(--c-total); }
|
| .card-active { border-left-color: var(--c-active); }
|
| .card-active .card-val { color: var(--c-active); }
|
| .card-inuse { border-left-color: var(--c-inuse); }
|
| .card-inuse .card-val { color: var(--c-inuse); }
|
| .card-login { border-left-color: var(--c-login); }
|
| .card-login .card-val { color: var(--c-login); }
|
| .card-exhaust { border-left-color: var(--c-exhaust); }
|
| .card-exhaust .card-val { color: var(--c-exhaust); }
|
| .card-dead { border-left-color: var(--c-dead); }
|
| .card-dead .card-val { color: var(--c-dead); }
|
| .card-val { font-size: 2rem; font-weight: 700; line-height: 1; }
|
| .card-label { font-size: .82rem; color: var(--text-sec); margin-top: 8px; }
|
|
|
| .section { background: var(--bg-card); border-radius: var(--radius); box-shadow: var(--shadow); margin-bottom: 24px; overflow: hidden; }
|
| .section-head { padding: 18px 24px; font-size: .95rem; font-weight: 600; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
|
|
|
| .model-groups { padding: 20px 24px; }
|
| .model-group { margin-bottom: 18px; }
|
| .model-group:last-child { margin-bottom: 0; }
|
| .mg-title { font-size: .85rem; font-weight: 600; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
|
| .mg-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
| .mg-count { font-weight: 400; color: var(--text-sec); font-size: .78rem; }
|
| .mg-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
| .chip { display: inline-block; padding: 4px 14px; border-radius: 20px; font-size: .78rem; font-family: "SF Mono", "Cascadia Code", Consolas, monospace; background: #f8f9fa; border: 1px solid var(--border); transition: background .15s; cursor: default; }
|
| .chip:hover { background: #eef0f2; }
|
|
|
| .table-wrap { overflow-x: auto; }
|
| table { width: 100%; border-collapse: collapse; }
|
| th { background: #f9fafb; text-align: left; padding: 12px 20px; font-weight: 600; font-size: .82rem; color: var(--text-sec); border-bottom: 2px solid var(--border); white-space: nowrap; }
|
| td { padding: 11px 20px; border-bottom: 1px solid var(--border); font-size: .88rem; }
|
| tr:last-child td { border-bottom: none; }
|
| tr:hover td { background: #fafbfc; }
|
| .email { font-family: monospace; font-size: .82rem; }
|
|
|
| .badge { display: inline-block; padding: 2px 12px; border-radius: 12px; font-size: .75rem; font-weight: 500; }
|
| .badge-active { background: var(--c-active-bg); color: var(--c-active); }
|
| .badge-in_use { background: var(--c-inuse-bg); color: var(--c-inuse); }
|
| .badge-needs_login { background: var(--c-login-bg); color: var(--c-login); }
|
| .badge-exhausted { background: var(--c-exhaust-bg); color: var(--c-exhaust); }
|
| .badge-dead { background: var(--c-dead-bg); color: var(--c-dead); }
|
|
|
| .empty { text-align: center; padding: 40px; color: var(--text-sec); font-size: .9rem; }
|
|
|
| /* Register bar */
|
| .reg-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
| .reg-label { font-size: .82rem; font-weight: 400; color: var(--text-sec); }
|
| .reg-input { width: 60px; padding: 5px 10px; border: 1px solid var(--border); border-radius: 6px; font-size: .85rem; text-align: center; outline: none; }
|
| .reg-input:focus { border-color: var(--c-inuse); }
|
| .reg-select { padding: 5px 10px; border: 1px solid var(--border); border-radius: 6px; font-size: .82rem; outline: none; background: var(--bg-card); color: var(--text); cursor: pointer; max-width: 140px; }
|
| .reg-select:focus { border-color: var(--c-inuse); }
|
| .btn { padding: 6px 16px; border: none; border-radius: 6px; font-size: .82rem; font-weight: 500; cursor: pointer; transition: background .15s, opacity .15s; }
|
| .btn-primary { background: var(--c-inuse); color: #fff; }
|
| .btn-primary:hover { background: #2563eb; }
|
| .btn:disabled { opacity: .5; cursor: not-allowed; }
|
| .reg-msg { font-size: .78rem; color: var(--text-sec); }
|
| .reg-msg.ok { color: var(--c-active); }
|
| .reg-msg.err { color: var(--c-dead); }
|
|
|
| /* Log panel */
|
| .log-box { max-height: 260px; overflow-y: auto; padding: 14px 20px; background: #1a1a2e; font-family: "SF Mono", "Cascadia Code", Consolas, monospace; font-size: .75rem; line-height: 1.7; }
|
| .log-line { color: #94a3b8; }
|
| .log-line .log-time { color: #64748b; margin-right: 8px; }
|
| .log-line.log-success { color: #34d399; }
|
| .log-line.log-error { color: #f87171; }
|
| .log-line.log-warn { color: #fbbf24; }
|
| .log-empty { color: #475569; text-align: center; padding: 20px 0; }
|
|
|
| /* Pagination */
|
| .pager { display: flex; justify-content: space-between; align-items: center; padding: 12px 20px; border-top: 1px solid var(--border); font-size: .82rem; color: var(--text-sec); }
|
| .pager-btns { display: flex; gap: 4px; }
|
| .pager-btn { padding: 4px 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg-card); cursor: pointer; font-size: .78rem; color: var(--text); transition: background .15s; }
|
| .pager-btn:hover { background: #f0f2f5; }
|
| .pager-btn.active { background: var(--c-inuse); color: #fff; border-color: var(--c-inuse); }
|
| .pager-btn:disabled { opacity: .4; cursor: not-allowed; }
|
|
|
| /* Guide */
|
| .guide { padding: 20px 24px; }
|
| .guide-title { font-size: .88rem; font-weight: 600; margin-bottom: 12px; }
|
| .guide-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
|
| .guide-item { padding: 16px; background: #f8f9fa; border-radius: 8px; border: 1px solid var(--border); }
|
| .guide-item h4 { font-size: .82rem; font-weight: 600; margin-bottom: 8px; color: var(--text); }
|
| .guide-item p { font-size: .78rem; color: var(--text-sec); margin-bottom: 8px; line-height: 1.5; }
|
| .guide-item code { display: block; padding: 10px 14px; background: #1a1a2e; color: #e2e8f0; border-radius: 6px; font-size: .75rem; font-family: "SF Mono", "Cascadia Code", Consolas, monospace; overflow-x: auto; white-space: pre; line-height: 1.6; }
|
|
|
| @media (max-width: 900px) { .stats { grid-template-columns: repeat(3, 1fr); } .guide-grid { grid-template-columns: 1fr; } }
|
| @media (max-width: 500px) { .stats { grid-template-columns: repeat(2, 1fr); } .container { padding: 16px; } .reg-bar { flex-wrap: wrap; } }
|
| </style>
|
| </head>
|
| <body>
|
|
|
| <header class="header">
|
| <div><span class="logo">ChatAIBot<span class="logo-sub">API Proxy</span></span></div>
|
| <div class="header-right">
|
| <span id="uptime">\u8FD0\u884C\u65F6\u95F4: --</span>
|
| <span class="dot" id="dot"></span>
|
| </div>
|
| </header>
|
|
|
| <div class="container">
|
| <div class="stats">
|
| <div class="card card-total"><div class="card-val" id="s-total">--</div><div class="card-label">\u603B\u8BA1\u8D26\u53F7</div></div>
|
| <div class="card card-active"><div class="card-val" id="s-active">--</div><div class="card-label">\u53EF\u7528</div></div>
|
| <div class="card card-inuse"><div class="card-val" id="s-inuse">--</div><div class="card-label">\u4F7F\u7528\u4E2D</div></div>
|
| <div class="card card-login"><div class="card-val" id="s-login">--</div><div class="card-label">\u9700\u767B\u5F55</div></div>
|
| <div class="card card-exhaust"><div class="card-val" id="s-exhaust">--</div><div class="card-label">\u5DF2\u8017\u5C3D</div></div>
|
| <div class="card card-dead"><div class="card-val" id="s-dead">--</div><div class="card-label">\u5DF2\u5931\u6548</div></div>
|
| </div>
|
|
|
| <!-- Register + Logs -->
|
| <div class="section">
|
| <div class="section-head">
|
| <span>\u8D26\u53F7\u7BA1\u7406</span>
|
| <div class="reg-bar">
|
| <span class="reg-label">\u90AE\u7BB1</span>
|
| <select class="reg-select" id="regProvider">
|
| <option value="">auto (\u914D\u7F6E\u9ED8\u8BA4)</option>
|
| <option value="mailtm">Mail.tm</option>
|
| <option value="guerrilla">Guerrilla</option>
|
| <option value="tempmail">TempMail</option>
|
| <option value="tempmailio">TempMail.io</option>
|
| <option value="dropmail">Dropmail</option>
|
| <option value="linshiyou">linshiyou</option>
|
| <option value="gptmail">GPTMail</option>
|
| <option value="moemail">MoeMail</option>
|
| <option value="duckmail">DuckMail</option>
|
| <option value="catchall">CatchAll</option>
|
| </select>
|
| <span class="reg-label">\u6570\u91CF</span>
|
| <input type="number" class="reg-input" id="regCount" value="5" min="1" max="50">
|
| <span class="reg-label">\u5E76\u53D1</span>
|
| <input type="number" class="reg-input" id="regConcurrency" value="5" min="1" max="10">
|
| <button class="btn btn-primary" id="regBtn" onclick="doRegister()">\u624B\u52A8\u6CE8\u518C</button>
|
| <span class="reg-msg" id="regMsg"></span>
|
| </div>
|
| </div>
|
| <div class="log-box" id="logBox"><div class="log-empty">\u6682\u65E0\u65E5\u5FD7</div></div>
|
| </div>
|
|
|
| <div class="section">
|
| <div class="section-head">\u53EF\u7528\u6A21\u578B</div>
|
| <div class="model-groups" id="modelGroups"><div class="empty">\u52A0\u8F7D\u4E2D...</div></div>
|
| </div>
|
|
|
| <div class="section">
|
| <div class="section-head">\u8D26\u53F7\u8BE6\u60C5</div>
|
| <div class="table-wrap">
|
| <table>
|
| <thead><tr><th>#</th><th>\u90AE\u7BB1</th><th>\u72B6\u6001</th><th>\u5269\u4F59\u989D\u5EA6</th><th>\u6700\u540E\u4F7F\u7528</th></tr></thead>
|
| <tbody id="tbody"><tr><td colspan="5" class="empty">\u52A0\u8F7D\u4E2D...</td></tr></tbody>
|
| </table>
|
| </div>
|
| <div class="pager" id="pager" style="display:none">
|
| <span class="pager-info" id="pagerInfo"></span>
|
| <div class="pager-btns" id="pagerBtns"></div>
|
| </div>
|
| </div>
|
|
|
| <!-- Usage Guide -->
|
| <div class="section">
|
| <div class="section-head">\u4F7F\u7528\u65B9\u6CD5</div>
|
| <div class="guide">
|
| <div class="guide-grid">
|
| <div class="guide-item">
|
| <h4>OpenAI \u683C\u5F0F (\u975E\u6D41\u5F0F)</h4>
|
| <p>\u517C\u5BB9 OpenAI SDK / ChatBox / Cherry Studio \u7B49\u5BA2\u6237\u7AEF</p>
|
| <code>curl http://localhost:9090/v1/chat/completions \\
|
| -H "Content-Type: application/json" \\
|
| -d '{
|
| "model": "gpt-4o",
|
| "messages": [
|
| {"role": "user", "content": "Hello"}
|
| ]
|
| }'</code>
|
| </div>
|
| <div class="guide-item">
|
| <h4>OpenAI \u683C\u5F0F (\u6D41\u5F0F)</h4>
|
| <p>\u6DFB\u52A0 stream: true \u5F00\u542F SSE \u6D41\u5F0F\u8F93\u51FA</p>
|
| <code>curl http://localhost:9090/v1/chat/completions \\
|
| -H "Content-Type: application/json" \\
|
| -d '{
|
| "model": "gpt-4o",
|
| "messages": [
|
| {"role": "user", "content": "Hello"}
|
| ],
|
| "stream": true
|
| }'</code>
|
| </div>
|
| <div class="guide-item">
|
| <h4>Anthropic \u683C\u5F0F</h4>
|
| <p>\u517C\u5BB9 Anthropic SDK \u7684 Messages API</p>
|
| <code>curl http://localhost:9090/v1/messages \\
|
| -H "Content-Type: application/json" \\
|
| -d '{
|
| "model": "claude-4.6-sonnet",
|
| "max_tokens": 1024,
|
| "messages": [
|
| {"role": "user", "content": "Hello"}
|
| ]
|
| }'</code>
|
| </div>
|
| <div class="guide-item">
|
| <h4>\u5BA2\u6237\u7AEF\u914D\u7F6E</h4>
|
| <p>\u5728 Cherry Studio / ChatBox \u7B49\u5BA2\u6237\u7AEF\u4E2D\u914D\u7F6E\uFF1A</p>
|
| <code>API \u5730\u5740: http://localhost:9090
|
| API Key: \u7559\u7A7A (\u65E0\u9700\u8BA4\u8BC1)
|
| \u6A21\u578B: \u70B9\u51FB\u201C\u83B7\u53D6\u6A21\u578B\u5217\u8868\u201D\u81EA\u52A8\u62C9\u53D6
|
|
|
| \u652F\u6301\u7684\u63A5\u53E3:
|
| OpenAI: POST /v1/chat/completions
|
| Anthropic: POST /v1/messages
|
| \u6A21\u578B\u5217\u8868: GET /v1/models</code>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <script>
|
| var STATES = { active: "\u53EF\u7528", in_use: "\u4F7F\u7528\u4E2D", needs_login: "\u9700\u767B\u5F55", exhausted: "\u5DF2\u8017\u5C3D", dead: "\u5DF2\u5931\u6548" };
|
| var GCOLORS = { OpenAI: "#10a37f", Anthropic: "#d97706", Google: "#4285f4", "\u5176\u4ED6": "#6366f1" };
|
| var GORDER = ["OpenAI", "Anthropic", "Google", "\u5176\u4ED6"];
|
|
|
| var PAGE_SIZE = 20;
|
| var currentPage = 1;
|
| var allAccounts = [];
|
| var logSince = 0;
|
|
|
| function fmt(s) {
|
| s = Math.floor(s);
|
| var d = Math.floor(s/86400), h = Math.floor(s%86400/3600), m = Math.floor(s%3600/60), sec = s%60;
|
| var t = "";
|
| if (d) t += d + "\u5929 ";
|
| if (h || d) t += h + "\u65F6 ";
|
| t += m + "\u5206 " + sec + "\u79D2";
|
| return t;
|
| }
|
|
|
| function ago(iso) {
|
| if (!iso) return "\u4ECE\u672A\u4F7F\u7528";
|
| var ms = Date.now() - new Date(iso).getTime();
|
| if (ms < 60000) return "\u521A\u521A";
|
| if (ms < 3600000) return Math.floor(ms/60000) + "\u5206\u949F\u524D";
|
| if (ms < 86400000) return Math.floor(ms/3600000) + "\u5C0F\u65F6\u524D";
|
| return new Date(iso).toLocaleString("zh-CN", { month:"2-digit", day:"2-digit", hour:"2-digit", minute:"2-digit" });
|
| }
|
|
|
| function mask(e) {
|
| var p = e.split("@");
|
| if (p[0].length <= 4) return e;
|
| return p[0].substring(0,4) + "***@" + p[1];
|
| }
|
|
|
| function fmtLogTime(iso) {
|
| return new Date(iso).toLocaleString("zh-CN", { hour:"2-digit", minute:"2-digit", second:"2-digit", hour12:false });
|
| }
|
|
|
| function esc(s) {
|
| return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
| }
|
|
|
| function renderPage() {
|
| var total = allAccounts.length;
|
| var totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
| if (currentPage > totalPages) currentPage = totalPages;
|
| var start = (currentPage - 1) * PAGE_SIZE;
|
| var pageItems = allAccounts.slice(start, start + PAGE_SIZE);
|
|
|
| var html = "";
|
| for (var i = 0; i < pageItems.length; i++) {
|
| var a = pageItems[i];
|
| html += "<tr><td>" + (start + i + 1) + '</td><td class="email" title="' + a.email + '">' + mask(a.email) + '</td><td><span class="badge badge-' + a.state + '">' + (STATES[a.state]||a.state) + "</span></td><td>" + (a.remaining != null ? a.remaining : "--") + "</td><td>" + ago(a.lastUsed) + "</td></tr>";
|
| }
|
| document.getElementById("tbody").innerHTML = html || '<tr><td colspan="5" class="empty">\u6682\u65E0\u8D26\u53F7</td></tr>';
|
|
|
| var pager = document.getElementById("pager");
|
| if (total <= PAGE_SIZE) {
|
| pager.style.display = "none";
|
| return;
|
| }
|
| pager.style.display = "flex";
|
| document.getElementById("pagerInfo").textContent = "\u5171 " + total + " \u4E2A\u8D26\u53F7\uFF0C\u7B2C " + currentPage + "/" + totalPages + " \u9875";
|
|
|
| var btns = "";
|
| btns += '<button class="pager-btn" onclick="goPage(' + (currentPage-1) + ')"' + (currentPage <= 1 ? " disabled" : "") + '>\u4E0A\u4E00\u9875</button>';
|
| var lo = Math.max(1, currentPage - 2);
|
| var hi = Math.min(totalPages, currentPage + 2);
|
| if (lo > 1) btns += '<button class="pager-btn" onclick="goPage(1)">1</button>';
|
| if (lo > 2) btns += '<span style="padding:4px 6px;color:var(--text-sec)">...</span>';
|
| for (var pg = lo; pg <= hi; pg++) {
|
| btns += '<button class="pager-btn' + (pg === currentPage ? " active" : "") + '" onclick="goPage(' + pg + ')">' + pg + '</button>';
|
| }
|
| if (hi < totalPages - 1) btns += '<span style="padding:4px 6px;color:var(--text-sec)">...</span>';
|
| if (hi < totalPages) btns += '<button class="pager-btn" onclick="goPage(' + totalPages + ')">' + totalPages + '</button>';
|
| btns += '<button class="pager-btn" onclick="goPage(' + (currentPage+1) + ')"' + (currentPage >= totalPages ? " disabled" : "") + '>\u4E0B\u4E00\u9875</button>';
|
| document.getElementById("pagerBtns").innerHTML = btns;
|
| }
|
|
|
| function goPage(p) {
|
| var totalPages = Math.max(1, Math.ceil(allAccounts.length / PAGE_SIZE));
|
| if (p < 1 || p > totalPages) return;
|
| currentPage = p;
|
| renderPage();
|
| }
|
|
|
| async function refreshLogs() {
|
| try {
|
| var r = await fetch("/pool/logs?since=" + logSince).then(function(r){return r.json()});
|
| var logs = r.logs || [];
|
| if (logs.length === 0) return;
|
| logSince = logs[logs.length - 1].id;
|
|
|
| var box = document.getElementById("logBox");
|
| var empty = box.querySelector(".log-empty");
|
| if (empty) empty.remove();
|
|
|
| for (var i = 0; i < logs.length; i++) {
|
| var log = logs[i];
|
| var cls = "log-line";
|
| if (log.level === "success") cls += " log-success";
|
| else if (log.level === "error") cls += " log-error";
|
| else if (log.level === "warn") cls += " log-warn";
|
| var line = document.createElement("div");
|
| line.className = cls;
|
| line.innerHTML = '<span class="log-time">' + fmtLogTime(log.time) + "</span>" + esc(log.message);
|
| box.appendChild(line);
|
| }
|
|
|
| while (box.children.length > 200) {
|
| box.removeChild(box.firstChild);
|
| }
|
| box.scrollTop = box.scrollHeight;
|
| } catch(e) {}
|
| }
|
|
|
| async function doRegister() {
|
| var btn = document.getElementById("regBtn");
|
| var msg = document.getElementById("regMsg");
|
| var count = parseInt(document.getElementById("regCount").value) || 5;
|
| var provider = document.getElementById("regProvider").value || undefined;
|
| var concurrency = parseInt(document.getElementById("regConcurrency").value) || 5;
|
| btn.disabled = true;
|
| btn.textContent = "\u6CE8\u518C\u4E2D...";
|
| msg.className = "reg-msg";
|
| msg.textContent = "";
|
| try {
|
| var payload = { count: count, concurrency: concurrency };
|
| if (provider) payload.provider = provider;
|
| var r = await fetch("/pool/register", {
|
| method: "POST",
|
| headers: { "Content-Type": "application/json" },
|
| body: JSON.stringify(payload)
|
| }).then(function(r){ return r.json() });
|
| msg.className = "reg-msg " + (r.ok ? "ok" : "err");
|
| msg.textContent = r.message;
|
| } catch(e) {
|
| msg.className = "reg-msg err";
|
| msg.textContent = "\u8BF7\u6C42\u5931\u8D25: " + e.message;
|
| } finally {
|
| btn.disabled = false;
|
| btn.textContent = "\u624B\u52A8\u6CE8\u518C";
|
| }
|
| }
|
|
|
| async function refresh() {
|
| var dot = document.getElementById("dot");
|
| dot.classList.add("loading");
|
| try {
|
| var results = await Promise.allSettled([
|
| fetch("/health").then(function(r){return r.json()}),
|
| fetch("/pool/status").then(function(r){return r.json()}),
|
| fetch("/v1/models").then(function(r){return r.json()})
|
| ]);
|
|
|
| if (results[0].status === "fulfilled") {
|
| document.getElementById("uptime").textContent = "\u8FD0\u884C\u65F6\u95F4: " + fmt(results[0].value.uptime);
|
| }
|
|
|
| if (results[1].status === "fulfilled") {
|
| var p = results[1].value;
|
| document.getElementById("s-total").textContent = p.total;
|
| document.getElementById("s-active").textContent = p.active;
|
| document.getElementById("s-inuse").textContent = p.in_use;
|
| document.getElementById("s-login").textContent = p.needs_login;
|
| document.getElementById("s-exhaust").textContent = p.exhausted;
|
| document.getElementById("s-dead").textContent = p.dead;
|
|
|
| var order = ["in_use","active","needs_login","exhausted","dead"];
|
| allAccounts = (p.accounts||[]).slice().sort(function(a,b){ return order.indexOf(a.state) - order.indexOf(b.state); });
|
| renderPage();
|
| }
|
|
|
| if (results[2].status === "fulfilled") {
|
| var groups = {};
|
| var models = results[2].value.data || [];
|
| for (var j = 0; j < models.length; j++) {
|
| var g = models[j].owned_by || "\u5176\u4ED6";
|
| if (!groups[g]) groups[g] = [];
|
| groups[g].push(models[j].id);
|
| }
|
| var mhtml = "";
|
| for (var k = 0; k < GORDER.length; k++) {
|
| var name = GORDER[k];
|
| var items = groups[name];
|
| if (!items || !items.length) continue;
|
| var color = GCOLORS[name] || "#6366f1";
|
| mhtml += '<div class="model-group"><div class="mg-title"><span class="mg-dot" style="background:' + color + '"></span>' + name + ' <span class="mg-count">' + items.length + " \u4E2A\u6A21\u578B</span></div><div class='mg-chips'>";
|
| for (var l = 0; l < items.length; l++) {
|
| mhtml += '<span class="chip">' + items[l] + "</span>";
|
| }
|
| mhtml += "</div></div>";
|
| }
|
| document.getElementById("modelGroups").innerHTML = mhtml || '<div class="empty">\u6682\u65E0\u6A21\u578B</div>';
|
| }
|
| } catch(e) {
|
| console.error(e);
|
| } finally {
|
| dot.classList.remove("loading");
|
| }
|
| }
|
|
|
| refresh();
|
| refreshLogs();
|
| setInterval(refresh, 5000);
|
| setInterval(refreshLogs, 2000);
|
| </script>
|
| </body>
|
| </html>`;
|
| }
|
|
|