mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-05-06 20:32:47 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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 小时内按模型统计的调用次数。
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -1109,9 +1109,9 @@ endblock %} {% block head_extra_styles %}
|
||||
</style>
|
||||
{% endblock %} {% block head_extra_scripts %}
|
||||
<!-- Chart.js for time-series chart -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
<!-- keys_status.js needs to be loaded in head because it might be used by inline scripts -->
|
||||
<script src="/static/js/keys_status.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js" defer></script>
|
||||
<!-- Load page script with defer to guarantee DOM is ready and keep execution order -->
|
||||
<script src="/static/js/keys_status.js" defer></script>
|
||||
{% endblock %} {% block content %}
|
||||
<div class="container max-w-6xl mx-auto px-4">
|
||||
<!-- Increased max-width -->
|
||||
@@ -1276,7 +1276,35 @@ endblock %} {% block head_extra_styles %}
|
||||
<div class="p-4 chart-container">
|
||||
<canvas id="apiStatsChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 值得注意的 Key 卡片(错误码统计,可切换) -->
|
||||
<div class="stats-card chart-wide">
|
||||
<div class="stats-card-header">
|
||||
<h3 class="stats-card-title">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<span>值得注意的Key(24h内错误码最多)</span>
|
||||
</h3>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<button id="attentionErr429" class="px-2 py-1 rounded bg-gray-200 hover:bg-gray-300 text-gray-700" title="429 Too Many Requests">429</button>
|
||||
<button id="attentionErr403" class="px-2 py-1 rounded bg-gray-200 hover:bg-gray-300 text-gray-700" title="403 Forbidden">403</button>
|
||||
<button id="attentionErr400" class="px-2 py-1 rounded bg-gray-200 hover:bg-gray-300 text-gray-700" title="400 Bad Request">400</button>
|
||||
<div class="flex items-center gap-1 ml-2">
|
||||
<input id="attentionErrCustom" type="number" min="100" max="599" placeholder="自定义" class="form-input h-7 w-20 px-2 py-1 text-xs border rounded focus:ring-primary-500 focus:border-primary-500" />
|
||||
<button id="attentionErrGo" class="px-2 py-1 rounded bg-blue-500 hover:bg-blue-600 text-white" title="查询">查询</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 ml-3">
|
||||
<label for="attentionLimitInput" class="text-xs text-gray-600">数量</label>
|
||||
<input id="attentionLimitInput" type="number" min="1" max="1000" value="10" class="form-input h-7 w-20 px-2 py-1 text-xs border rounded focus:ring-primary-500 focus:border-primary-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<ul id="attentionKeysList" class="space-y-2">
|
||||
<li class="text-center text-gray-500 py-2">加载中...</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 有效密钥区域 -->
|
||||
|
||||
Reference in New Issue
Block a user