Add recent API request recorder with admin tab and /api/recent endpoint
build / build (push) Successful in 2m31s
build / build (push) Successful in 2m31s
Record the last 10 /v1/ API requests (including errors) in an in-memory ring buffer. Each entry captures request headers (Authorization masked), request body, response status, and response body — all up to 1 MB. - recorder.go: RecentRecorder ring buffer, recordingResponseWriter, withRecording middleware, getRecent handler - server.go: add recorder to Server, wrap /v1/ routes, add /api/recent - admin.html: new 最近请求 tab with lazy-load and expandable cards - recorder_test.go: 5 tests (ring buffer, ordering, capture, errors, API)
This commit is contained in:
@@ -101,6 +101,16 @@
|
||||
@media(prefers-reduced-motion:reduce){.status[data-state="busy"]::before{animation:none}}
|
||||
.footer{margin-top:22px;padding-top:18px;border-top:1px solid var(--border);font-size:12px;color:var(--muted);line-height:1.7}
|
||||
.footer code{font-family:"SF Mono",ui-monospace,Consolas,monospace;font-size:12px;color:var(--body);word-break:break-all}
|
||||
.recent-card{border:1px solid var(--border);border-radius:12px;padding:16px;margin-bottom:12px;background:var(--bg)}
|
||||
.recent-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:4px}
|
||||
.recent-time{font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums}
|
||||
.recent-method{font-size:11.5px;font-weight:700;color:var(--accent);background:var(--accent-soft);padding:2px 8px;border-radius:999px}
|
||||
.recent-path{font-size:13px;color:var(--ink);font-family:"SF Mono",ui-monospace,Consolas,monospace;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.recent-duration{font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums}
|
||||
.recent-card details{margin-top:6px}
|
||||
.recent-card summary{cursor:pointer;font-size:12.5px;font-weight:600;color:var(--body);padding:4px 0;user-select:none}
|
||||
.recent-card summary:hover{color:var(--accent)}
|
||||
.recent-card pre{background:#1e1e2e;color:#cdd6f4;padding:12px;border-radius:8px;font-size:12px;overflow-x:auto;max-height:400px;overflow-y:auto;font-family:"SF Mono",ui-monospace,Consolas,monospace;line-height:1.5;margin:8px 0 0;white-space:pre-wrap;word-break:break-all}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -116,6 +126,7 @@
|
||||
<div class="tabs" role="tablist">
|
||||
<button class="tab" role="tab" data-tab="stats" aria-selected="true">Token 统计</button>
|
||||
<button class="tab" role="tab" data-tab="models" aria-selected="false">可用模型</button>
|
||||
<button class="tab" role="tab" data-tab="recent" aria-selected="false">最近请求</button>
|
||||
<button class="tab" role="tab" data-tab="login" aria-selected="false">凭据登录</button>
|
||||
</div>
|
||||
|
||||
@@ -191,6 +202,17 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tabpanel" data-tab="recent" role="tabpanel">
|
||||
<div class="sub-actions">
|
||||
<button class="btn btn-ghost" id="recent-refresh" type="button">刷新</button>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h2>最近 API 请求</h2>
|
||||
<p class="muted" style="margin:0 0 16px">记录最近 10 条 /v1/ 请求的请求头和返回结果(含报错)。</p>
|
||||
<div id="recent-list"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tabpanel" data-tab="login" role="tabpanel">
|
||||
<div class="login-card">
|
||||
<p class="lede">输入手机号获取验证码,按插件默认的移动云登录接口换取凭据和模型 API Key。凭据保存到本地数据库,后续 OpenAI 兼容接口自动使用。</p>
|
||||
@@ -225,6 +247,7 @@
|
||||
});
|
||||
var hash = location.hash.replace('#', '');
|
||||
if (hash === 'models') activate('models');
|
||||
else if (hash === 'recent') activate('recent');
|
||||
else if (hash === 'login') activate('login');
|
||||
|
||||
var rows = document.querySelectorAll('#daily .bar');
|
||||
@@ -521,6 +544,66 @@
|
||||
if (modelsTab) modelsTab.addEventListener('click', loadIfActive);
|
||||
loadIfActive();
|
||||
})();
|
||||
|
||||
(function () {
|
||||
var recentList = document.getElementById('recent-list');
|
||||
if (!recentList) return;
|
||||
var loaded = false;
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s).replace(/[&<>"']/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||||
});
|
||||
}
|
||||
function formatTime(ts) {
|
||||
var d = new Date(ts);
|
||||
return d.toLocaleString('zh-CN', { hour12: false });
|
||||
}
|
||||
async function loadRecent() {
|
||||
recentList.innerHTML = '<p class="muted">加载中…</p>';
|
||||
try {
|
||||
var res = await fetch('/api/recent');
|
||||
var data = await res.json();
|
||||
if (!data.ok || !data.requests || data.requests.length === 0) {
|
||||
recentList.innerHTML = '<p class="empty">暂无数据</p>';
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
for (var i = 0; i < data.requests.length; i++) {
|
||||
var r = data.requests[i];
|
||||
var statusClass = r.status >= 200 && r.status < 300 ? 'ok' : 'err';
|
||||
var headers = r.request_headers || {};
|
||||
var headerStr = Object.keys(headers).map(function (k) {
|
||||
return k + ': ' + headers[k];
|
||||
}).join('\n');
|
||||
html += '<div class="recent-card">' +
|
||||
'<div class="recent-head">' +
|
||||
'<span class="recent-time">' + escapeHtml(formatTime(r.timestamp)) + '</span>' +
|
||||
'<span class="recent-method">' + escapeHtml(r.method) + '</span>' +
|
||||
'<span class="recent-path">' + escapeHtml(r.path) + '</span>' +
|
||||
'<span class="badge ' + statusClass + '">' + r.status + '</span>' +
|
||||
'<span class="recent-duration">' + r.duration_ms + 'ms</span>' +
|
||||
'</div>' +
|
||||
(headerStr ? '<details><summary>请求头</summary><pre>' + escapeHtml(headerStr) + '</pre></details>' : '') +
|
||||
'<details><summary>请求体</summary><pre>' + escapeHtml(r.request_body || '(空)') + '</pre></details>' +
|
||||
'<details><summary>响应体</summary><pre>' + escapeHtml(r.response_body || '(空)') + '</pre></details>' +
|
||||
'</div>';
|
||||
}
|
||||
recentList.innerHTML = html;
|
||||
} catch (e) {
|
||||
recentList.innerHTML = '<p class="empty">加载失败: ' + escapeHtml(e.message) + '</p>';
|
||||
}
|
||||
}
|
||||
var recentTab = document.querySelector('.tab[data-tab="recent"]');
|
||||
if (recentTab) recentTab.addEventListener('click', function () {
|
||||
if (!loaded) { loaded = true; loadRecent(); }
|
||||
});
|
||||
var refreshBtn = document.getElementById('recent-refresh');
|
||||
if (refreshBtn) refreshBtn.addEventListener('click', loadRecent);
|
||||
// auto-load if the tab is active on page load
|
||||
var panel = document.querySelector('.tabpanel[data-tab="recent"]');
|
||||
if (panel && panel.classList.contains('active')) { loaded = true; loadRecent(); }
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user