add admin log

This commit is contained in:
root 2026-05-05 13:42:35 +08:00
parent bec7b3e5ef
commit e373295cf5
8 changed files with 495 additions and 51 deletions

View file

@ -83,3 +83,11 @@ main{padding:28px 0 60px}
.toast-ok{background:#065f46;color:#a7f3d0}
.toast-err{background:#7f1d1d;color:#fca5a5}
@keyframes slideIn{from{transform:translateX(100px);opacity:0}to{transform:none;opacity:1}}
.request-logs-wrap{overflow:auto}
.request-logs-table{min-width:1100px}
.request-logs-table td{vertical-align:top}
.log-url{max-width:320px;word-break:break-all;color:var(--muted)}
.log-status{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:12px;font-weight:600}
.status-ok{background:rgba(34,197,94,.15);color:var(--green)}
.status-error{background:rgba(239,68,68,.15);color:var(--red)}

View file

@ -90,6 +90,16 @@
</div>
<div id="statsContent"><div class="empty">加载中…</div></div>
</div>
<!-- 请求日志 -->
<div class="card">
<div class="card-header">
<h2>最近 500 条请求日志</h2>
<button class="btn btn-ghost btn-sm" onclick="loadRequestLogs()">刷新</button>
</div>
<div class="hint" style="margin-top:-12px;margin-bottom:16px">显示请求时间、请求模型、实际上游模型、上游 URL、Token 统计、耗时和状态。</div>
<div id="requestLogsContent"><div class="empty">加载中…</div></div>
</div>
</main>
</div>

View file

@ -72,6 +72,7 @@ async function loadDashboard() {
await loadMappings();
checkHealth();
loadStats();
loadRequestLogs();
} catch (e) {
toast('加载设置失败: ' + e.message, false);
}
@ -104,6 +105,55 @@ async function loadStats() {
}
}
async function loadRequestLogs() {
const el = document.getElementById('requestLogsContent');
try {
const data = await api('/api/admin/request-logs');
const items = data.items || [];
if (!items.length) {
el.innerHTML = '<div class="empty">暂无请求日志</div>';
return;
}
let html = '<div class="request-logs-wrap"><table class="stats-table request-logs-table"><thead><tr><th>请求时间</th><th>请求模型</th><th>实际模型</th><th>上游 URL</th><th>Tokens</th><th>耗时</th><th>状态</th></tr></thead><tbody>';
for (const item of items) {
const usage = item.usage || {};
const tokens = '输 ' + fmtNum(usage.input_tokens) + ' / 出 ' + fmtNum(usage.output_tokens) + ' / 总 ' + fmtNum(usage.total_tokens);
const statusClass = item.status === 'ok' ? 'status-ok' : 'status-error';
const statusText = item.status === 'ok' ? '成功' : '异常';
html += '<tr>'
+ '<td>' + esc(fmtTime(item.requested_at)) + '</td>'
+ '<td>' + esc(item.requested_model || '-') + '</td>'
+ '<td>' + esc(item.actual_model || '-') + '</td>'
+ '<td class="log-url" title="' + esc(item.upstream_url || '') + '">' + esc(item.upstream_url || '-') + '</td>'
+ '<td>' + esc(tokens) + '</td>'
+ '<td>' + fmtNum(item.duration_ms) + ' ms</td>'
+ '<td><span class="log-status ' + statusClass + '">' + statusText + '</span></td>'
+ '</tr>';
}
html += '</tbody></table></div>';
el.innerHTML = html;
} catch (e) {
el.innerHTML = '<div class="empty">加载请求日志失败</div>';
}
}
function fmtNum(value) {
return Number(value || 0).toLocaleString();
}
function fmtTime(value) {
if (!value) return '-';
const d = new Date(value);
if (Number.isNaN(d.getTime())) return String(value);
const pad = n => String(n).padStart(2, '0');
return d.getFullYear() + '-'
+ pad(d.getMonth() + 1) + '-'
+ pad(d.getDate()) + ' '
+ pad(d.getHours()) + ':'
+ pad(d.getMinutes()) + ':'
+ pad(d.getSeconds());
}
async function checkHealth() {
try {
const r = await fetch(API + '/health');