sd / src /admin_index.html.tmp
pigracing's picture
Upload 22 files
bd27f3c verified
Raw History Blame Contribute Delete
18.8 kB
<!DOCTYPE html>
<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>