refactor(stats): enhance key statistics handling by introducing source and auth index categorization

This commit is contained in:
Supra4E8C
2025-11-20 14:08:10 +08:00
parent 8f4320c837
commit c5010adb82
3 changed files with 84 additions and 33 deletions

View File

@@ -2,6 +2,13 @@
// 这些函数依赖于 CLIProxyManager 实例上的 makeRequest/getConfig/clearCache/showNotification 等能力, // 这些函数依赖于 CLIProxyManager 实例上的 makeRequest/getConfig/clearCache/showNotification 等能力,
// 以及 apiKeysModule 中的工具方法(如 applyHeadersToConfig/renderHeaderBadges // 以及 apiKeysModule 中的工具方法(如 applyHeadersToConfig/renderHeaderBadges
const getStatsBySource = (stats) => {
if (stats && typeof stats === 'object' && stats.bySource) {
return stats.bySource;
}
return stats || {};
};
export async function loadGeminiKeys() { export async function loadGeminiKeys() {
try { try {
const config = await this.getConfig(); const config = await this.getConfig();
@@ -77,13 +84,13 @@ export async function renderGeminiKeys(keys, keyStats = null) {
if (!keyStats) { if (!keyStats) {
keyStats = await this.getKeyStats(); keyStats = await this.getKeyStats();
} }
const stats = keyStats; const statsBySource = getStatsBySource(keyStats);
container.innerHTML = normalizedList.map((config, index) => { container.innerHTML = normalizedList.map((config, index) => {
const rawKey = config['api-key'] || ''; const rawKey = config['api-key'] || '';
const masked = this.maskApiKey(rawKey || ''); const masked = this.maskApiKey(rawKey || '');
const maskedDisplay = this.escapeHtml(masked); 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 configJson = JSON.stringify(config).replace(/"/g, '"');
const apiKeyJson = JSON.stringify(rawKey || '').replace(/"/g, '"'); const apiKeyJson = JSON.stringify(rawKey || '').replace(/"/g, '"');
const baseUrl = config['base-url'] || config['base_url'] || ''; const baseUrl = config['base-url'] || config['base_url'] || '';
@@ -96,10 +103,10 @@ export async function renderGeminiKeys(keys, keyStats = null) {
${this.renderHeaderBadges(config.headers)} ${this.renderHeaderBadges(config.headers)}
<div class="item-stats"> <div class="item-stats">
<span class="stat-badge stat-success"> <span class="stat-badge stat-success">
<i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${keyStats.success} <i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${usageStats.success}
</span> </span>
<span class="stat-badge stat-failure"> <span class="stat-badge stat-failure">
<i class="fas fa-times-circle"></i> ${i18n.t('stats.failure')}: ${keyStats.failure} <i class="fas fa-times-circle"></i> ${i18n.t('stats.failure')}: ${usageStats.failure}
</span> </span>
</div> </div>
</div> </div>
@@ -412,13 +419,13 @@ export async function renderCodexKeys(keys, keyStats = null) {
if (!keyStats) { if (!keyStats) {
keyStats = await this.getKeyStats(); keyStats = await this.getKeyStats();
} }
const stats = keyStats; const statsBySource = getStatsBySource(keyStats);
container.innerHTML = list.map((config, index) => { container.innerHTML = list.map((config, index) => {
const rawKey = config['api-key'] || ''; const rawKey = config['api-key'] || '';
const masked = this.maskApiKey(rawKey || ''); const masked = this.maskApiKey(rawKey || '');
const maskedDisplay = this.escapeHtml(masked); 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, '&quot;'); const deleteArg = JSON.stringify(rawKey).replace(/"/g, '&quot;');
return ` return `
<div class="provider-item"> <div class="provider-item">
@@ -430,10 +437,10 @@ export async function renderCodexKeys(keys, keyStats = null) {
${this.renderHeaderBadges(config.headers)} ${this.renderHeaderBadges(config.headers)}
<div class="item-stats"> <div class="item-stats">
<span class="stat-badge stat-success"> <span class="stat-badge stat-success">
<i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${keyStats.success} <i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${usageStats.success}
</span> </span>
<span class="stat-badge stat-failure"> <span class="stat-badge stat-failure">
<i class="fas fa-times-circle"></i> ${i18n.t('stats.failure')}: ${keyStats.failure} <i class="fas fa-times-circle"></i> ${i18n.t('stats.failure')}: ${usageStats.failure}
</span> </span>
</div> </div>
</div> </div>
@@ -641,13 +648,13 @@ export async function renderClaudeKeys(keys, keyStats = null) {
if (!keyStats) { if (!keyStats) {
keyStats = await this.getKeyStats(); keyStats = await this.getKeyStats();
} }
const stats = keyStats; const statsBySource = getStatsBySource(keyStats);
container.innerHTML = list.map((config, index) => { container.innerHTML = list.map((config, index) => {
const rawKey = config['api-key'] || ''; const rawKey = config['api-key'] || '';
const masked = this.maskApiKey(rawKey || ''); const masked = this.maskApiKey(rawKey || '');
const maskedDisplay = this.escapeHtml(masked); 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, '&quot;'); const deleteArg = JSON.stringify(rawKey).replace(/"/g, '&quot;');
const models = Array.isArray(config.models) ? config.models : []; const models = Array.isArray(config.models) ? config.models : [];
const modelsCountHtml = models.length const modelsCountHtml = models.length
@@ -666,10 +673,10 @@ export async function renderClaudeKeys(keys, keyStats = null) {
${modelsBadgesHtml} ${modelsBadgesHtml}
<div class="item-stats"> <div class="item-stats">
<span class="stat-badge stat-success"> <span class="stat-badge stat-success">
<i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${keyStats.success} <i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${usageStats.success}
</span> </span>
<span class="stat-badge stat-failure"> <span class="stat-badge stat-failure">
<i class="fas fa-times-circle"></i> ${i18n.t('stats.failure')}: ${keyStats.failure} <i class="fas fa-times-circle"></i> ${i18n.t('stats.failure')}: ${usageStats.failure}
</span> </span>
</div> </div>
</div> </div>
@@ -909,7 +916,7 @@ export async function renderOpenAIProviders(providers, keyStats = null) {
if (!keyStats) { if (!keyStats) {
keyStats = await this.getKeyStats(); keyStats = await this.getKeyStats();
} }
const stats = keyStats; const statsBySource = getStatsBySource(keyStats);
container.innerHTML = list.map((provider, index) => { container.innerHTML = list.map((provider, index) => {
const item = typeof provider === 'object' && provider !== null ? provider : {}; 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'] : ''; const key = entry && entry['api-key'] ? entry['api-key'] : '';
if (!key) return; if (!key) return;
const masked = this.maskApiKey(key); const masked = this.maskApiKey(key);
const keyStats = stats[key] || stats[masked] || { success: 0, failure: 0 }; const usageStats = statsBySource[key] || statsBySource[masked] || { success: 0, failure: 0 };
totalSuccess += keyStats.success; totalSuccess += usageStats.success;
totalFailure += keyStats.failure; totalFailure += usageStats.failure;
}); });
const deleteArg = JSON.stringify(name).replace(/"/g, '&quot;'); const deleteArg = JSON.stringify(name).replace(/"/g, '&quot;');

View File

@@ -27,7 +27,7 @@ export const authFilesModule = {
const stats = keyStats || await this.getKeyStats(); const stats = keyStats || await this.getKeyStats();
this.cachedAuthFiles = visibleFiles.map(file => ({ ...file })); this.cachedAuthFiles = visibleFiles.map(file => ({ ...file }));
this.authFileStatsCache = stats || {}; this.authFileStatsCache = stats || { bySource: {}, byAuthIndex: {} };
this.syncAuthFileControls(); this.syncAuthFileControls();
if (this.cachedAuthFiles.length === 0) { if (this.cachedAuthFiles.length === 0) {
@@ -76,13 +76,21 @@ export const authFilesModule = {
}, },
resolveAuthFileStats(file, stats = {}) { resolveAuthFileStats(file, stats = {}) {
const statsBySource = (stats && stats.bySource) || stats || {};
const statsByAuthIndex = (stats && stats.byAuthIndex) || {};
const rawFileName = typeof file?.name === 'string' ? file.name : ''; const rawFileName = typeof file?.name === 'string' ? file.name : '';
const defaultStats = { success: 0, failure: 0 }; const defaultStats = { success: 0, failure: 0 };
const authIndexKey = this.normalizeAuthIndexValue(file?.auth_index);
if (authIndexKey && statsByAuthIndex[authIndexKey]) {
return statsByAuthIndex[authIndexKey];
}
if (!rawFileName) { if (!rawFileName) {
return defaultStats; return defaultStats;
} }
const fromName = stats[rawFileName]; const fromName = statsBySource[rawFileName];
if (fromName && (fromName.success > 0 || fromName.failure > 0)) { if (fromName && (fromName.success > 0 || fromName.failure > 0)) {
return fromName; return fromName;
} }
@@ -115,7 +123,7 @@ export const authFilesModule = {
}); });
for (const candidate of candidateNames) { for (const candidate of candidateNames) {
const candidateStats = stats[candidate]; const candidateStats = statsBySource[candidate];
if (candidateStats && (candidateStats.success > 0 || candidateStats.failure > 0)) { if (candidateStats && (candidateStats.success > 0 || candidateStats.failure > 0)) {
fileStats = candidateStats; fileStats = candidateStats;
break; break;
@@ -127,6 +135,17 @@ export const authFilesModule = {
return fileStats || defaultStats; 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) { buildAuthFileItemHtml(file) {
const rawFileName = typeof file?.name === 'string' ? file.name : ''; const rawFileName = typeof file?.name === 'string' ? file.name : '';
const safeFileName = this.escapeHtml(rawFileName); const safeFileName = this.escapeHtml(rawFileName);

View File

@@ -8,10 +8,27 @@ export async function getKeyStats(usageData = null) {
} }
if (!usage) { if (!usage) {
return {}; return { bySource: {}, byAuthIndex: {} };
} }
const sourceStats = {}; 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 || {}; const apis = usage.apis || {};
Object.values(apis).forEach(apiEntry => { Object.values(apis).forEach(apiEntry => {
@@ -22,29 +39,37 @@ export async function getKeyStats(usageData = null) {
details.forEach(detail => { details.forEach(detail => {
const source = detail.source; const source = detail.source;
if (!source) return; const authIndexKey = normalizeAuthIndex(detail?.auth_index);
const isFailed = detail.failed === true;
if (!sourceStats[source]) { if (source) {
sourceStats[source] = { const bucket = ensureBucket(sourceStats, source);
success: 0, if (isFailed) {
failure: 0 bucket.failure += 1;
}; } else {
bucket.success += 1;
}
} }
const isFailed = detail.failed === true; if (authIndexKey) {
if (isFailed) { const bucket = ensureBucket(authIndexStats, authIndexKey);
sourceStats[source].failure += 1; if (isFailed) {
} else { bucket.failure += 1;
sourceStats[source].success += 1; } else {
bucket.success += 1;
}
} }
}); });
}); });
}); });
return sourceStats; return {
bySource: sourceStats,
byAuthIndex: authIndexStats
};
} catch (error) { } catch (error) {
console.error('获取统计信息失败:', error); console.error('获取统计信息失败:', error);
return {}; return { bySource: {}, byAuthIndex: {} };
} }
} }