From c5010adb828db0e2fbb48fc97593b732cf5bd682 Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Thu, 20 Nov 2025 14:08:10 +0800 Subject: [PATCH] refactor(stats): enhance key statistics handling by introducing source and auth index categorization --- src/modules/ai-providers.js | 39 ++++++++++++++++----------- src/modules/auth-files.js | 25 ++++++++++++++--- src/modules/usage.js | 53 +++++++++++++++++++++++++++---------- 3 files changed, 84 insertions(+), 33 deletions(-) diff --git a/src/modules/ai-providers.js b/src/modules/ai-providers.js index 25b5e74..a91173a 100644 --- a/src/modules/ai-providers.js +++ b/src/modules/ai-providers.js @@ -2,6 +2,13 @@ // 这些函数依赖于 CLIProxyManager 实例上的 makeRequest/getConfig/clearCache/showNotification 等能力, // 以及 apiKeysModule 中的工具方法(如 applyHeadersToConfig/renderHeaderBadges)。 +const getStatsBySource = (stats) => { + if (stats && typeof stats === 'object' && stats.bySource) { + return stats.bySource; + } + return stats || {}; +}; + export async function loadGeminiKeys() { try { const config = await this.getConfig(); @@ -77,13 +84,13 @@ export async function renderGeminiKeys(keys, keyStats = null) { if (!keyStats) { keyStats = await this.getKeyStats(); } - const stats = keyStats; + const statsBySource = getStatsBySource(keyStats); container.innerHTML = normalizedList.map((config, index) => { const rawKey = config['api-key'] || ''; const masked = this.maskApiKey(rawKey || ''); const maskedDisplay = this.escapeHtml(masked); - const keyStats = (rawKey && (stats[rawKey] || stats[masked])) || { success: 0, failure: 0 }; + const usageStats = (rawKey && (statsBySource[rawKey] || statsBySource[masked])) || { success: 0, failure: 0 }; const configJson = JSON.stringify(config).replace(/"/g, '"'); const apiKeyJson = JSON.stringify(rawKey || '').replace(/"/g, '"'); const baseUrl = config['base-url'] || config['base_url'] || ''; @@ -96,10 +103,10 @@ export async function renderGeminiKeys(keys, keyStats = null) { ${this.renderHeaderBadges(config.headers)}
- ${i18n.t('stats.success')}: ${keyStats.success} + ${i18n.t('stats.success')}: ${usageStats.success} - ${i18n.t('stats.failure')}: ${keyStats.failure} + ${i18n.t('stats.failure')}: ${usageStats.failure}
@@ -412,13 +419,13 @@ export async function renderCodexKeys(keys, keyStats = null) { if (!keyStats) { keyStats = await this.getKeyStats(); } - const stats = keyStats; + const statsBySource = getStatsBySource(keyStats); container.innerHTML = list.map((config, index) => { const rawKey = config['api-key'] || ''; const masked = this.maskApiKey(rawKey || ''); const maskedDisplay = this.escapeHtml(masked); - const keyStats = (rawKey && (stats[rawKey] || stats[masked])) || { success: 0, failure: 0 }; + const usageStats = (rawKey && (statsBySource[rawKey] || statsBySource[masked])) || { success: 0, failure: 0 }; const deleteArg = JSON.stringify(rawKey).replace(/"/g, '"'); return `
@@ -430,10 +437,10 @@ export async function renderCodexKeys(keys, keyStats = null) { ${this.renderHeaderBadges(config.headers)}
- ${i18n.t('stats.success')}: ${keyStats.success} + ${i18n.t('stats.success')}: ${usageStats.success} - ${i18n.t('stats.failure')}: ${keyStats.failure} + ${i18n.t('stats.failure')}: ${usageStats.failure}
@@ -641,13 +648,13 @@ export async function renderClaudeKeys(keys, keyStats = null) { if (!keyStats) { keyStats = await this.getKeyStats(); } - const stats = keyStats; + const statsBySource = getStatsBySource(keyStats); container.innerHTML = list.map((config, index) => { const rawKey = config['api-key'] || ''; const masked = this.maskApiKey(rawKey || ''); const maskedDisplay = this.escapeHtml(masked); - const keyStats = (rawKey && (stats[rawKey] || stats[masked])) || { success: 0, failure: 0 }; + const usageStats = (rawKey && (statsBySource[rawKey] || statsBySource[masked])) || { success: 0, failure: 0 }; const deleteArg = JSON.stringify(rawKey).replace(/"/g, '"'); const models = Array.isArray(config.models) ? config.models : []; const modelsCountHtml = models.length @@ -666,10 +673,10 @@ export async function renderClaudeKeys(keys, keyStats = null) { ${modelsBadgesHtml}
- ${i18n.t('stats.success')}: ${keyStats.success} + ${i18n.t('stats.success')}: ${usageStats.success} - ${i18n.t('stats.failure')}: ${keyStats.failure} + ${i18n.t('stats.failure')}: ${usageStats.failure}
@@ -909,7 +916,7 @@ export async function renderOpenAIProviders(providers, keyStats = null) { if (!keyStats) { keyStats = await this.getKeyStats(); } - const stats = keyStats; + const statsBySource = getStatsBySource(keyStats); container.innerHTML = list.map((provider, index) => { const item = typeof provider === 'object' && provider !== null ? provider : {}; @@ -935,9 +942,9 @@ export async function renderOpenAIProviders(providers, keyStats = null) { const key = entry && entry['api-key'] ? entry['api-key'] : ''; if (!key) return; const masked = this.maskApiKey(key); - const keyStats = stats[key] || stats[masked] || { success: 0, failure: 0 }; - totalSuccess += keyStats.success; - totalFailure += keyStats.failure; + const usageStats = statsBySource[key] || statsBySource[masked] || { success: 0, failure: 0 }; + totalSuccess += usageStats.success; + totalFailure += usageStats.failure; }); const deleteArg = JSON.stringify(name).replace(/"/g, '"'); diff --git a/src/modules/auth-files.js b/src/modules/auth-files.js index ece2100..32e085b 100644 --- a/src/modules/auth-files.js +++ b/src/modules/auth-files.js @@ -27,7 +27,7 @@ export const authFilesModule = { const stats = keyStats || await this.getKeyStats(); this.cachedAuthFiles = visibleFiles.map(file => ({ ...file })); - this.authFileStatsCache = stats || {}; + this.authFileStatsCache = stats || { bySource: {}, byAuthIndex: {} }; this.syncAuthFileControls(); if (this.cachedAuthFiles.length === 0) { @@ -76,13 +76,21 @@ export const authFilesModule = { }, resolveAuthFileStats(file, stats = {}) { + const statsBySource = (stats && stats.bySource) || stats || {}; + const statsByAuthIndex = (stats && stats.byAuthIndex) || {}; const rawFileName = typeof file?.name === 'string' ? file.name : ''; const defaultStats = { success: 0, failure: 0 }; + const authIndexKey = this.normalizeAuthIndexValue(file?.auth_index); + + if (authIndexKey && statsByAuthIndex[authIndexKey]) { + return statsByAuthIndex[authIndexKey]; + } + if (!rawFileName) { return defaultStats; } - const fromName = stats[rawFileName]; + const fromName = statsBySource[rawFileName]; if (fromName && (fromName.success > 0 || fromName.failure > 0)) { return fromName; } @@ -115,7 +123,7 @@ export const authFilesModule = { }); for (const candidate of candidateNames) { - const candidateStats = stats[candidate]; + const candidateStats = statsBySource[candidate]; if (candidateStats && (candidateStats.success > 0 || candidateStats.failure > 0)) { fileStats = candidateStats; break; @@ -127,6 +135,17 @@ export const authFilesModule = { return fileStats || defaultStats; }, + normalizeAuthIndexValue(value) { + if (typeof value === 'number' && Number.isFinite(value)) { + return value.toString(); + } + if (typeof value === 'string') { + const trimmed = value.trim(); + return trimmed ? trimmed : null; + } + return null; + }, + buildAuthFileItemHtml(file) { const rawFileName = typeof file?.name === 'string' ? file.name : ''; const safeFileName = this.escapeHtml(rawFileName); diff --git a/src/modules/usage.js b/src/modules/usage.js index 27d4fd2..89d5083 100644 --- a/src/modules/usage.js +++ b/src/modules/usage.js @@ -8,10 +8,27 @@ export async function getKeyStats(usageData = null) { } if (!usage) { - return {}; + return { bySource: {}, byAuthIndex: {} }; } const sourceStats = {}; + const authIndexStats = {}; + const ensureBucket = (bucket, key) => { + if (!bucket[key]) { + bucket[key] = { success: 0, failure: 0 }; + } + return bucket[key]; + }; + const normalizeAuthIndex = (value) => { + if (typeof value === 'number' && Number.isFinite(value)) { + return value.toString(); + } + if (typeof value === 'string') { + const trimmed = value.trim(); + return trimmed ? trimmed : null; + } + return null; + }; const apis = usage.apis || {}; Object.values(apis).forEach(apiEntry => { @@ -22,29 +39,37 @@ export async function getKeyStats(usageData = null) { details.forEach(detail => { const source = detail.source; - if (!source) return; + const authIndexKey = normalizeAuthIndex(detail?.auth_index); + const isFailed = detail.failed === true; - if (!sourceStats[source]) { - sourceStats[source] = { - success: 0, - failure: 0 - }; + if (source) { + const bucket = ensureBucket(sourceStats, source); + if (isFailed) { + bucket.failure += 1; + } else { + bucket.success += 1; + } } - const isFailed = detail.failed === true; - if (isFailed) { - sourceStats[source].failure += 1; - } else { - sourceStats[source].success += 1; + if (authIndexKey) { + const bucket = ensureBucket(authIndexStats, authIndexKey); + if (isFailed) { + bucket.failure += 1; + } else { + bucket.success += 1; + } } }); }); }); - return sourceStats; + return { + bySource: sourceStats, + byAuthIndex: authIndexStats + }; } catch (error) { console.error('获取统计信息失败:', error); - return {}; + return { bySource: {}, byAuthIndex: {} }; } }