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 %}
-
+
+
+
+