Download src/admin_index.html.tmp from pigracing/sd: direct link, hf CLI and curl.
- Browser
- Download file 18.8 kB
-
https://huggingface.co/spaces/pigracing/sd/resolve/main/src/admin_index.html.tmp
- Command line
-
hf download hf://spaces/pigracing/sd/src/admin_index.html.tmp
-
curl -L -o admin_index.html.tmp https://huggingface.co/spaces/pigracing/sd/resolve/main/src/admin_index.html.tmp
18.8 kB
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>DeepSeek-2API 管理面板</title> | |
| <style> | |
| :root { | |
| --bg: #1a1a2e; | |
| --surface: #16213e; | |
| --border: #0f3460; | |
| --text: #e0e0e0; | |
| --text-dim: #8892a0; | |
| --green: #00e676; | |
| --yellow: #ffd600; | |
| --red: #ff1744; | |
| --accent: #00b0ff; | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| line-height: 1.6; | |
| min-height: 100vh; | |
| } | |
| .container { | |
| max-width: 960px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 16px 20px; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| margin-bottom: 20px; | |
| } | |
| header h1 { | |
| font-size: 1.3em; | |
| color: var(--accent); | |
| } | |
| header .meta { | |
| font-size: 0.85em; | |
| color: var(--text-dim); | |
| } | |
| .controls { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .controls select, .controls button { | |
| background: var(--bg); | |
| color: var(--text); | |
| border: 1px solid var(--border); | |
| border-radius: 4px; | |
| padding: 4px 10px; | |
| font-size: 0.85em; | |
| cursor: pointer; | |
| } | |
| .controls button:hover { border-color: var(--accent); } | |
| .card { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 16px 20px; | |
| margin-bottom: 16px; | |
| } | |
| .card h2 { | |
| font-size: 1.05em; | |
| margin-bottom: 12px; | |
| color: var(--accent); | |
| } | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); | |
| gap: 12px; | |
| } | |
| .stat-item { | |
| background: var(--bg); | |
| border-radius: 6px; | |
| padding: 12px; | |
| text-align: center; | |
| } | |
| .stat-item .label { | |
| font-size: 0.75em; | |
| color: var(--text-dim); | |
| margin-bottom: 4px; | |
| } | |
| .stat-item .value { | |
| font-size: 1.4em; | |
| font-weight: 700; | |
| } | |
| .status-ok { color: var(--green); } | |
| .status-warn { color: var(--yellow); } | |
| .status-err { color: var(--red); } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 0.88em; | |
| } | |
| th, td { | |
| padding: 8px 10px; | |
| text-align: left; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| th { | |
| color: var(--text-dim); | |
| font-weight: 600; | |
| font-size: 0.8em; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| td { font-family: 'SF Mono', 'Consolas', monospace; font-size: 0.85em; } | |
| .dot { | |
| display: inline-block; | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| margin-right: 6px; | |
| } | |
| .dot-green { background: var(--green); } | |
| .dot-yellow { background: var(--yellow); } | |
| .dot-red { background: var(--red); } | |
| .bar-wrap { | |
| background: var(--bg); | |
| border-radius: 4px; | |
| height: 22px; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .bar-fill { | |
| height: 100%; | |
| border-radius: 4px; | |
| transition: width 0.4s, background 0.4s; | |
| } | |
| .bar-label { | |
| position: absolute; | |
| top: 0; left: 10px; right: 10px; | |
| line-height: 22px; | |
| font-size: 0.78em; | |
| color: var(--text); | |
| text-shadow: 0 1px 2px rgba(0,0,0,0.6); | |
| } | |
| .forms-row { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 16px; | |
| } | |
| @media (max-width: 640px) { | |
| .forms-row { grid-template-columns: 1fr; } | |
| } | |
| .form-group { margin-bottom: 10px; } | |
| .form-group label { | |
| display: block; | |
| font-size: 0.82em; | |
| color: var(--text-dim); | |
| margin-bottom: 4px; | |
| } | |
| .form-group input { | |
| width: 100%; | |
| background: var(--bg); | |
| color: var(--text); | |
| border: 1px solid var(--border); | |
| border-radius: 4px; | |
| padding: 7px 10px; | |
| font-size: 0.9em; | |
| } | |
| .form-group input:focus { outline: none; border-color: var(--accent); } | |
| .btn { | |
| background: var(--accent); | |
| color: #fff; | |
| border: none; | |
| border-radius: 4px; | |
| padding: 7px 18px; | |
| font-size: 0.9em; | |
| cursor: pointer; | |
| } | |
| .btn:hover { opacity: 0.85; } | |
| .msg { | |
| font-size: 0.82em; | |
| margin-top: 6px; | |
| min-height: 1.4em; | |
| } | |
| .msg-ok { color: var(--green); } | |
| .msg-err { color: var(--red); } | |
| details { | |
| margin-bottom: 10px; | |
| } | |
| summary { | |
| cursor: pointer; | |
| color: var(--accent); | |
| font-size: 0.92em; | |
| padding: 4px 0; | |
| } | |
| pre { | |
| background: var(--bg); | |
| border: 1px solid var(--border); | |
| border-radius: 4px; | |
| padding: 10px 12px; | |
| font-size: 0.82em; | |
| overflow-x: auto; | |
| color: var(--text); | |
| margin: 6px 0; | |
| } | |
| .model-table { | |
| width: auto; | |
| margin-bottom: 12px; | |
| } | |
| .model-table td, .model-table th { | |
| padding: 6px 16px; | |
| } | |
| footer { | |
| text-align: center; | |
| color: var(--text-dim); | |
| font-size: 0.78em; | |
| padding: 12px; | |
| } | |
| .auth-overlay { | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0,0,0,0.7); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 100; | |
| } | |
| .auth-box { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 24px; | |
| width: 340px; | |
| } | |
| .auth-box h3 { margin-bottom: 12px; color: var(--accent); } | |
| .auth-box .form-group { margin-bottom: 14px; } | |
| .auth-box .msg { margin-bottom: 8px; } | |
| .loading { color: var(--text-dim); font-style: italic; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <div> | |
| <h1>DeepSeek-2API 管理面板</h1> | |
| <div class="meta">版本 <span id="version">-</span> · 运行 <span id="uptime">-</span></div> | |
| </div> | |
| <div class="controls"> | |
| <select id="interval" title="自动刷新间隔"> | |
| <option value="5000">5秒</option> | |
| <option value="10000" selected>10秒</option> | |
| <option value="30000">30秒</option> | |
| <option value="0">关闭</option> | |
| </select> | |
| <button id="refreshBtn">刷新</button> | |
| </div> | |
| </header> | |
| <!-- 总览 --> | |
| <div class="card"> | |
| <h2>总览</h2> | |
| <div class="stats-grid"> | |
| <div class="stat-item"> | |
| <div class="label">状态</div> | |
| <div class="value" id="status">-</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="label">总容量</div> | |
| <div class="value" id="capacity">-</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="label">令牌数</div> | |
| <div class="value" id="tokenCount">-</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="label">会话缓存</div> | |
| <div class="value" id="sessionCount">-</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 请求队列 --> | |
| <div class="card"> | |
| <h2>请求队列</h2> | |
| <div class="bar-wrap"> | |
| <div class="bar-fill" id="queueBar" style="width:0%;background:var(--green)"></div> | |
| <div class="bar-label" id="queueLabel">0 / 100</div> | |
| </div> | |
| </div> | |
| <!-- 令牌池 --> | |
| <div class="card"> | |
| <h2>令牌池</h2> | |
| <div style="overflow-x:auto"> | |
| <table> | |
| <thead> | |
| <tr><th>令牌</th><th>视觉能力</th><th>错误数</th><th>活跃/最大</th><th>健康</th></tr> | |
| </thead> | |
| <tbody id="poolBody"> | |
| <tr><td colspan="5" class="loading">加载中...</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- 会话缓存 --> | |
| <div class="card"> | |
| <h2>会话缓存</h2> | |
| <div id="sessionSummary" style="font-size:0.88em;color:var(--text-dim);margin-bottom:8px">-</div> | |
| <div style="overflow-x:auto"> | |
| <table> | |
| <thead> | |
| <tr><th>缓存键</th><th>模型</th><th>已用时间</th><th>剩余 TTL</th></tr> | |
| </thead> | |
| <tbody id="sessionBody"> | |
| <tr><td colspan="4" class="loading">加载中...</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- 快速入门 --> | |
| <div class="card"> | |
| <h2>快速入门</h2> | |
| <h3 style="font-size:0.92em;margin-bottom:8px">模型映射</h3> | |
| <table class="model-table"> | |
| <thead><tr><th>API 模型名</th><th>DeepSeek 模型类型</th></tr></thead> | |
| <tbody> | |
| <tr><td>deepseek-v4-flash</td><td>default(默认/免费)</td></tr> | |
| <tr><td>deepseek-v4-pro</td><td>expert(深度思考/付费)</td></tr> | |
| <tr><td>deepseek-v4-vision</td><td>vision(视觉/图片理解)</td></tr> | |
| </tbody> | |
| </table> | |
| <details> | |
| <summary>OpenAI 格式 - 非流式请求</summary> | |
| <pre>curl http://localhost:3000/v1/chat/completions \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: Bearer YOUR_API_KEY" \ | |
| -d '{ | |
| "model": "deepseek-v4-flash", | |
| "messages": [{"role":"user","content":"你好"}] | |
| }'</pre> | |
| </details> | |
| <details> | |
| <summary>OpenAI 格式 - 流式请求(SSE)</summary> | |
| <pre>curl http://localhost:3000/v1/chat/completions \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: Bearer YOUR_API_KEY" \ | |
| -d '{ | |
| "model": "deepseek-v4-pro", | |
| "stream": true, | |
| "messages": [{"role":"user","content":"解释量子计算"}] | |
| }'</pre> | |
| </details> | |
| <details> | |
| <summary>DeepSeek 原生格式</summary> | |
| <pre>curl http://localhost:3000/api/v0/chat/completion \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: Bearer YOUR_API_KEY" \ | |
| -d '{ | |
| "model_type": "default", | |
| "stream": true, | |
| "chat_session_id": "SESSION_ID", | |
| "prompt": "你好" | |
| }'</pre> | |
| </details> | |
| <details> | |
| <summary>查看可用模型</summary> | |
| <pre>curl http://localhost:3000/v1/models \ | |
| -H "Authorization: Bearer YOUR_API_KEY"</pre> | |
| </details> | |
| </div> | |
| <!-- 令牌管理 --> | |
| <div class="card"> | |
| <h2>令牌管理</h2> | |
| <div class="forms-row"> | |
| <div> | |
| <h3 style="font-size:0.92em;margin-bottom:8px">添加令牌</h3> | |
| <form id="addTokenForm"> | |
| <div class="form-group"> | |
| <label>令牌字符串</label> | |
| <input type="text" id="addTokenInput" placeholder="粘贴 DeepSeek 令牌..." required> | |
| </div> | |
| <button type="submit" class="btn">添加</button> | |
| <div class="msg" id="addTokenMsg"></div> | |
| </form> | |
| </div> | |
| <div> | |
| <h3 style="font-size:0.92em;margin-bottom:8px">登录添加</h3> | |
| <form id="loginForm"> | |
| <div class="form-group"> | |
| <label>邮箱</label> | |
| <input type="email" id="loginEmail" placeholder="DeepSeek 账号邮箱" required> | |
| </div> | |
| <div class="form-group"> | |
| <label>密码</label> | |
| <input type="password" id="loginPassword" placeholder="DeepSeek 账号密码" required> | |
| </div> | |
| <button type="submit" class="btn">登录</button> | |
| <div class="msg" id="loginMsg"></div> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| <footer>上次刷新: <span id="lastRefresh">-</span></footer> | |
| </div> | |
| <!-- Auth overlay --> | |
| <div class="auth-overlay" id="authOverlay" style="display:none"> | |
| <div class="auth-box"> | |
| <h3>需要 API Key</h3> | |
| <p style="font-size:0.85em;color:var(--text-dim);margin-bottom:12px">此服务器已配置 API_KEY 验证,请输入密钥。</p> | |
| <div class="form-group"> | |
| <label>API Key</label> | |
| <input type="password" id="authKeyInput" placeholder="输入 API Key..."> | |
| </div> | |
| <button class="btn" id="authSubmitBtn">确认</button> | |
| <div class="msg" id="authMsg"></div> | |
| </div> | |
| </div> | |
| <script> | |
| const $ = id => document.getElementById(id); | |
| let refreshTimer = null; | |
| function getApiKey() { | |
| return sessionStorage.getItem('adminApiKey') || ''; | |
| } | |
| function authHeaders() { | |
| const key = getApiKey(); | |
| return key ? { 'Authorization': 'Bearer ' + key } : {}; | |
| } | |
| async function apiFetch(url, options = {}) { | |
| const headers = { ...authHeaders(), ...(options.headers || {}) }; | |
| const res = await fetch(url, { ...options, headers }); | |
| if (res.status === 401) { | |
| sessionStorage.removeItem('adminApiKey'); | |
| $('authOverlay').style.display = 'flex'; | |
| throw new Error('unauthorized'); | |
| } | |
| return res; | |
| } | |
| function formatUptime(sec) { | |
| const d = Math.floor(sec / 86400); | |
| const h = Math.floor((sec % 86400) / 3600); | |
| const m = Math.floor((sec % 3600) / 60); | |
| const s = sec % 60; | |
| const parts = []; | |
| if (d) parts.push(d + '天'); | |
| if (h) parts.push(h + '时'); | |
| if (m) parts.push(m + '分'); | |
| parts.push(s + '秒'); | |
| return parts.join(' '); | |
| } | |
| function formatDuration(sec) { | |
| if (sec < 60) return sec + '秒'; | |
| if (sec < 3600) return Math.floor(sec / 60) + '分' + (sec % 60) + '秒'; | |
| if (sec < 86400) return Math.floor(sec / 3600) + '时' + Math.floor((sec % 3600) / 60) + '分'; | |
| return Math.floor(sec / 86400) + '天' + Math.floor((sec % 86400) / 3600) + '时'; | |
| } | |
| function healthDot(errorCount, active, max) { | |
| if (errorCount >= 3) return '<span class="dot dot-red"></span>不可用'; | |
| if (errorCount >= 1) return '<span class="dot dot-yellow"></span>异常'; | |
| if (active >= max) return '<span class="dot dot-yellow"></span>满载'; | |
| return '<span class="dot dot-green"></span>正常'; | |
| } | |
| function visionLabel(v) { | |
| if (v === true) return '<span style="color:var(--green)">支持</span>'; | |
| if (v === false) return '<span style="color:var(--text-dim)">不支持</span>'; | |
| return '<span style="color:var(--yellow)">未知</span>'; | |
| } | |
| function render(data) { | |
| $('version').textContent = data.version; | |
| $('uptime').textContent = formatUptime(data.uptimeSeconds); | |
| $('status').innerHTML = '<span class="status-ok">正常</span>'; | |
| $('capacity').textContent = data.totalCapacity; | |
| $('tokenCount').textContent = data.pool.length; | |
| $('sessionCount').textContent = data.sessions.count; | |
| // Queue bar | |
| const q = data.queue.queued; | |
| const qMax = data.queue.maxQueueSize; | |
| const pct = Math.min(100, (q / qMax) * 100); | |
| const qBar = $('queueBar'); | |
| qBar.style.width = pct + '%'; | |
| qBar.style.background = pct > 80 ? 'var(--red)' : pct > 50 ? 'var(--yellow)' : 'var(--green)'; | |
| $('queueLabel').textContent = q + ' / ' + qMax; | |
| // Token pool | |
| const poolBody = $('poolBody'); | |
| if (data.pool.length === 0) { | |
| poolBody.innerHTML = '<tr><td colspan="5" style="color:var(--text-dim)">无令牌</td></tr>'; | |
| } else { | |
| poolBody.innerHTML = data.pool.map(t => `<tr> | |
| <td>${t.token}</td> | |
| <td>${visionLabel(t.visionCapable)}</td> | |
| <td>${t.errorCount}</td> | |
| <td>${t.activeRequests} / ${t.maxConcurrent}</td> | |
| <td>${healthDot(t.errorCount, t.activeRequests, t.maxConcurrent)}</td> | |
| </tr>`).join(''); | |
| } | |
| // Sessions | |
| const sessions = data.sessions; | |
| $('sessionSummary').textContent = sessions.count + ' 个会话已缓存 · TTL: ' + formatDuration(sessions.ttl); | |
| const sBody = $('sessionBody'); | |
| if (sessions.sessions.length === 0) { | |
| sBody.innerHTML = '<tr><td colspan="4" style="color:var(--text-dim)">无缓存会话</td></tr>'; | |
| } else { | |
| sBody.innerHTML = sessions.sessions.map(s => { | |
| const ttlStyle = s.ttlRemainingSeconds < 3600 ? 'color:var(--yellow)' : ''; | |
| const expiredStyle = s.ttlRemainingSeconds === 0 ? 'color:var(--red)' : ''; | |
| return `<tr> | |
| <td>${s.key}</td> | |
| <td>${s.modelType}</td> | |
| <td>${formatDuration(s.ageSeconds)}</td> | |
| <td style="${expiredStyle || ttlStyle}">${s.ttlRemainingSeconds === 0 ? '已过期' : formatDuration(s.ttlRemainingSeconds)}</td> | |
| </tr>`; | |
| }).join(''); | |
| } | |
| $('lastRefresh').textContent = new Date().toLocaleTimeString(); | |
| } | |
| async function fetchStats() { | |
| try { | |
| const res = await apiFetch('/admin/api/stats'); | |
| if (!res.ok) throw new Error('HTTP ' + res.status); | |
| const data = await res.json(); | |
| render(data); | |
| } catch (e) { | |
| if (e.message !== 'unauthorized') { | |
| console.error('fetchStats error:', e); | |
| } | |
| } | |
| } | |
| // Auto-refresh | |
| function setupRefresh() { | |
| if (refreshTimer) clearInterval(refreshTimer); | |
| const ms = parseInt($('interval').value); | |
| if (ms > 0) { | |
| refreshTimer = setInterval(fetchStats, ms); | |
| } | |
| } | |
| $('interval').addEventListener('change', setupRefresh); | |
| $('refreshBtn').addEventListener('click', fetchStats); | |
| // Auth overlay | |
| $('authSubmitBtn').addEventListener('click', () => { | |
| const key = $('authKeyInput').value.trim(); | |
| if (!key) { | |
| $('authMsg').className = 'msg msg-err'; | |
| $('authMsg').textContent = '请输入 API Key'; | |
| return; | |
| } | |
| sessionStorage.setItem('adminApiKey', key); | |
| $('authOverlay').style.display = 'none'; | |
| $('authMsg').textContent = ''; | |
| fetchStats(); | |
| }); | |
| $('authKeyInput').addEventListener('keydown', e => { | |
| if (e.key === 'Enter') $('authSubmitBtn').click(); | |
| }); | |
| // Token management forms | |
| $('addTokenForm').addEventListener('submit', async e => { | |
| e.preventDefault(); | |
| const token = $('addTokenInput').value.trim(); | |
| const msgEl = $('addTokenMsg'); | |
| msgEl.textContent = '添加中...'; | |
| msgEl.className = 'msg'; | |
| try { | |
| const res = await apiFetch('/admin/api/token/add', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ token }), | |
| }); | |
| const data = await res.json(); | |
| if (data.success) { | |
| msgEl.className = 'msg msg-ok'; | |
| msgEl.textContent = '添加成功!视觉能力: ' + (data.visionCapable === true ? '支持' : data.visionCapable === false ? '不支持' : '未知'); | |
| $('addTokenInput').value = ''; | |
| fetchStats(); | |
| } else { | |
| msgEl.className = 'msg msg-err'; | |
| msgEl.textContent = data.error?.message || '添加失败'; | |
| } | |
| } catch (err) { | |
| msgEl.className = 'msg msg-err'; | |
| msgEl.textContent = err.message === 'unauthorized' ? '认证失败' : '请求失败'; | |
| } | |
| }); | |
| $('loginForm').addEventListener('submit', async e => { | |
| e.preventDefault(); | |
| const email = $('loginEmail').value.trim(); | |
| const password = $('loginPassword').value; | |
| const msgEl = $('loginMsg'); | |
| msgEl.textContent = '登录中...'; | |
| msgEl.className = 'msg'; | |
| try { | |
| const res = await apiFetch('/admin/api/token/login', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ email, password }), | |
| }); | |
| const data = await res.json(); | |
| if (data.success) { | |
| msgEl.className = 'msg msg-ok'; | |
| msgEl.textContent = '登录成功!令牌: ' + data.token; | |
| $('loginEmail').value = ''; | |
| $('loginPassword').value = ''; | |
| fetchStats(); | |
| } else { | |
| msgEl.className = 'msg msg-err'; | |
| msgEl.textContent = data.error?.message || '登录失败'; | |
| } | |
| } catch (err) { | |
| msgEl.className = 'msg msg-err'; | |
| msgEl.textContent = err.message === 'unauthorized' ? '认证失败' : '请求失败'; | |
| } | |
| }); | |
| // Init | |
| fetchStats(); | |
| setupRefresh(); | |
| </script> | |
| </body> | |
| </html> | |