From 1aa3d267bbc3baeb094f9545a255df3665be77dd Mon Sep 17 00:00:00 2001 From: snaily Date: Mon, 18 Aug 2025 06:28:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(api,ui):=20=E6=96=B0=E5=A2=9E24h=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=A0=81=E6=9C=80=E9=AB=98Key=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E4=B8=8E=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 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 就绪与执行顺序 - 修复:补充获取数量输入框引用,避免初始化未声明变量报错 - 其他:微调日志输出格式 --- app/router/routes.py | 33 +++++++++- app/service/stats/stats_service.py | 35 +++++++++++ app/static/js/keys_status.js | 99 ++++++++++++++++++++++++++++++ app/templates/keys_status.html | 36 +++++++++-- 4 files changed, 198 insertions(+), 5 deletions(-) diff --git a/app/router/routes.py b/app/router/routes.py index 07b25f4..03848f6 100644 --- a/app/router/routes.py +++ b/app/router/routes.py @@ -231,6 +231,35 @@ def setup_api_stats_routes(app: FastAPI) -> None: ) return {"error": "Internal server error"}, 500 + @app.get("/api/stats/attention-keys") + async def api_stats_attention_keys( + request: Request, limit: int = 20, status_code: int = 429 + ): + """返回最近24小时指定错误码次数最多的Key(仅包含内存Key列表中的)。默认错误码429。""" + try: + auth_token = request.cookies.get("auth_token") + if not auth_token or not verify_auth_token(auth_token): + logger.warning("Unauthorized access attempt to attention-keys") + return {"error": "Unauthorized"}, 401 + + # 支持所有标准HTTP状态码范围 + # if not isinstance(status_code, int) or status_code < 100 or status_code > 599: + # return {"error": f"Unsupported status_code: {status_code}"}, 400 + + key_manager = await get_key_manager_instance() + keys_status = await key_manager.get_keys_by_status() + in_memory_keys = set(keys_status.get("valid_keys", [])) | set( + keys_status.get("invalid_keys", []) + ) + stats_service = StatsService() + data = await stats_service.get_attention_keys_last_24h( + in_memory_keys, limit, status_code + ) + return data + except Exception as e: + logger.error(f"Error fetching attention keys: {e}") + return {"error": "Internal server error"}, 500 + @app.get("/api/stats/key-details") async def api_stats_key_details(request: Request, key: str, period: str): """获取指定密钥在指定时间段内的调用详情""" @@ -240,7 +269,9 @@ def setup_api_stats_routes(app: FastAPI) -> None: logger.warning("Unauthorized access attempt to API key stats details") return {"error": "Unauthorized"}, 401 - logger.info(f"Fetching key call details for key=...{key[-4:] if key else ''}, period: {period}") + logger.info( + f"Fetching key call details for key=...{key[-4:] if key else ''}, period: {period}" + ) stats_service = StatsService() details = await stats_service.get_key_call_details(key, period) return details diff --git a/app/service/stats/stats_service.py b/app/service/stats/stats_service.py index 43de5b3..9e2cc42 100644 --- a/app/service/stats/stats_service.py +++ b/app/service/stats/stats_service.py @@ -312,6 +312,41 @@ class StatsService: ) raise + async def get_attention_keys_last_24h(self, include_keys: set[str], limit: int = 20, status_code: int = 429) -> list[dict]: + """返回最近24小时内指定状态码(默认429)最多的Key列表,仅包含include_keys中的Key。 + + Returns: [{"key": str, "count": int, "status_code": int}, ...] 按次数降序 + """ + try: + now = datetime.datetime.now() + start_time = now - datetime.timedelta(hours=24) + if not include_keys: + return [] + query = ( + select( + RequestLog.api_key.label("key"), + func.count(RequestLog.id).label("count"), + ) + .where( + RequestLog.request_time >= start_time, + RequestLog.status_code == status_code, + RequestLog.api_key.isnot(None), + RequestLog.api_key.in_(list(include_keys)), + ) + .group_by(RequestLog.api_key) + .order_by(func.count(RequestLog.id).desc()) + .limit(limit) + ) + rows = await database.fetch_all(query) + return [ + {"key": row["key"], "count": row["count"], "status_code": status_code} + for row in rows + if row["key"] + ] + except Exception as e: + logger.error(f"Failed to get attention keys ({status_code}) in last 24h: {e}") + return [] + async def get_key_usage_details_last_24h(self, key: str) -> Union[dict, None]: """ 获取指定 API 密钥在过去 24 小时内按模型统计的调用次数。 diff --git a/app/static/js/keys_status.js b/app/static/js/keys_status.js index 3cebac1..209bcc2 100644 --- a/app/static/js/keys_status.js +++ b/app/static/js/keys_status.js @@ -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 = '
  • 暂无需要注意的Key
  • '; + 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 = `
  • 加载失败: ${e.message}
  • `; + } +} + 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'); diff --git a/app/templates/keys_status.html b/app/templates/keys_status.html index 4639478..343f62c 100644 --- a/app/templates/keys_status.html +++ b/app/templates/keys_status.html @@ -1109,9 +1109,9 @@ endblock %} {% block head_extra_styles %} {% endblock %} {% block head_extra_scripts %} - - - + + + {% endblock %} {% block content %}
    @@ -1276,7 +1276,35 @@ endblock %} {% block head_extra_styles %}
    -
    + + + +
    +
    +

    + + 值得注意的Key(24h内错误码最多) +

    +
    + + + +
    + + +
    +
    + + +
    +
    +
    +
    +
      +
    • 加载中...
    • +
    +
    +