feat(api,ui): 新增24h错误码最高Key统计与面板

- 新增 GET /api/stats/attention-keys 接口,统计最近24小时指定
  状态码(默认429)错误次数最多的 Key,仅统计内存中的 Key,
  支持 limit 与 status_code 参数
- StatsService 新增 get_attention_keys_last_24h,按 api_key 分组计数并
  降序返回
- UI 新增“值得注意的Key”卡片:支持 429/403/400 快捷切换、自定义状态码
  与数量限制,默认展示 429 前 10
- 列表项支持验证、查看 24h 详情、复制、删除等快捷操作
- 将 Chart.js 与页面脚本改为 defer,保证 DOM 就绪与执行顺序
- 修复:补充获取数量输入框引用,避免初始化未声明变量报错
- 其他:微调日志输出格式
This commit is contained in:
snaily
2025-08-18 06:28:48 +08:00
parent e9601ca76c
commit 1aa3d267bb
4 changed files with 198 additions and 5 deletions

View File

@@ -1559,6 +1559,56 @@ async function renderApiChart(period) {
}
}
// --- Helpers for Attention Keys panel ---
// track current active status code tab
let currentStatus = 429;
function getLimit() {
const el = document.getElementById('attentionLimitInput');
const v = parseInt(el && el.value, 10);
if (isNaN(v)) return 10;
// clamp between 1 and 1000 to match input limits
return Math.min(1000, Math.max(1, v));
}
async function fetchAndRenderAttentionKeys(statusCode = 429, limit = 10) {
const listEl = document.getElementById('attentionKeysList');
if (!listEl) return;
try {
const data = await fetchAPI(`/api/stats/attention-keys?status_code=${statusCode}&limit=${limit}`);
listEl.innerHTML = '';
if (!data || (Array.isArray(data) && data.length === 0) || data.error) {
listEl.innerHTML = '<li class="text-center text-gray-500 py-2">暂无需要注意的Key</li>';
return;
}
data.forEach(item => {
const li = document.createElement('li');
li.className = 'flex items-center justify-between bg-white rounded border px-3 py-2';
const masked = item.key ? `${item.key.substring(0,4)}...${item.key.substring(item.key.length-4)}` : 'N/A';
const code = item.status_code ?? statusCode;
li.innerHTML = `
\u003cdiv class=\"flex items-center gap-3\"\u003e
\u003cspan class=\"font-mono text-sm\"\u003e${masked}\u003c/span\u003e
\u003cspan class=\"text-xs bg-red-100 text-red-700 px-2 py-0.5 rounded\"\u003e${code}: ${item.count}\u003c/span\u003e
\u003c/div\u003e
\u003cdiv class=\"flex items-center gap-2\"\u003e
\u003cbutton class=\"px-2 py-1 text-xs rounded bg-success-600 hover:bg-success-700 text-white\" title=\"验证此Key\"\u003e验证\u003c/button\u003e
\u003cbutton class=\"px-2 py-1 text-xs rounded bg-blue-600 hover:bg-blue-700 text-white\" title=\"查看24小时详情\"\u003e详情\u003c/button\u003e
\u003cbutton class=\"px-2 py-1 text-xs rounded bg-blue-500 hover:bg-blue-600 text-white\" title=\"复制Key\"\u003e复制\u003c/button\u003e
\u003cbutton class=\"px-2 py-1 text-xs rounded bg-red-800 hover:bg-red-900 text-white\" title=\"删除此Key\"\u003e删除\u003c/button\u003e
\u003c/div\u003e`;
const [verifyBtn, detailBtn, copyBtn, deleteBtn] = li.querySelectorAll('button');
verifyBtn.addEventListener('click', (e) => verifyKey(item.key, e.currentTarget));
detailBtn.addEventListener('click', () => window.showKeyUsageDetails(item.key));
copyBtn.addEventListener('click', () => copyKey(item.key));
deleteBtn.addEventListener('click', (e) => showSingleKeyDeleteConfirmModal(item.key, e.currentTarget));
listEl.appendChild(li);
});
} catch (e) {
listEl.innerHTML = `<li class="text-center text-red-500 py-2">加载失败: ${e.message}</li>`;
}
}
function initChartControls() {
const btn1h = document.getElementById('chartBtn1h');
const btn8h = document.getElementById('chartBtn8h');
@@ -1585,6 +1635,53 @@ function initChartControls() {
renderApiChart('1h');
}
function initAttentionKeysControls() {
const btn429 = document.getElementById('attentionErr429');
const btn403 = document.getElementById('attentionErr403');
const btn400 = document.getElementById('attentionErr400');
// 修复:补充获取数量输入框,避免未声明变量导致初始化报错
const limitInput = document.getElementById('attentionLimitInput');
const setActive = (activeBtn) => {
[btn429, btn403, btn400].forEach(btn => {
if (!btn) return;
if (btn === activeBtn) {
btn.classList.remove('bg-gray-200');
btn.classList.add('bg-primary-600','text-white');
} else {
btn.classList.add('bg-gray-200');
btn.classList.remove('bg-primary-600','text-white');
}
});
};
if (btn429) btn429.addEventListener('click', () => { setActive(btn429); currentStatus = 429; fetchAndRenderAttentionKeys(429, getLimit()); });
if (btn403) btn403.addEventListener('click', () => { setActive(btn403); currentStatus = 403; fetchAndRenderAttentionKeys(403, getLimit()); });
if (btn400) btn400.addEventListener('click', () => { setActive(btn400); currentStatus = 400; fetchAndRenderAttentionKeys(400, getLimit()); });
// 自定义查询
const input = document.getElementById('attentionErrCustom');
const go = document.getElementById('attentionErrGo');
const trigger = () => {
if (!input) return;
const val = parseInt(input.value, 10);
if (!isNaN(val) && val >= 100 && val <= 599) {
setActive(null);
[btn429, btn403, btn400].forEach(btn=>{ if(btn){ btn.classList.add('bg-gray-200'); btn.classList.remove('bg-primary-600','text-white'); }});
currentStatus = val;
fetchAndRenderAttentionKeys(val, getLimit());
} else {
showNotification('请输入100-599之间的HTTP状态码', 'warning');
}
};
if (go) go.addEventListener('click', trigger);
if (input) input.addEventListener('keydown', (e)=>{ if(e.key==='Enter'){ trigger(); }});
// limit变化实时刷新当前状态码
if (limitInput) limitInput.addEventListener('change', () => {
fetchAndRenderAttentionKeys(currentStatus, getLimit());
});
if (btn429) setActive(btn429); // default active
}
// 初始化
document.addEventListener("DOMContentLoaded", () => {
initializePageAnimationsAndEffects();
@@ -1596,6 +1693,8 @@ document.addEventListener("DOMContentLoaded", () => {
registerServiceWorker();
initializeDropdownMenu(); // 初始化下拉菜单
initChartControls(); // 初始化图表与时间区间切换
initAttentionKeysControls(); // 初始化值得注意的Key错误码切换
fetchAndRenderAttentionKeys(429, 10); // 默认渲染429数量10
// Initial batch actions update might be needed if not covered by displayPage
// updateBatchActions('valid');