mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 03:00:49 +08:00
refactor(stats): enhance key statistics handling by introducing source and auth index categorization
This commit is contained in:
@@ -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, '"');
|
const deleteArg = JSON.stringify(rawKey).replace(/"/g, '"');
|
||||||
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, '"');
|
const deleteArg = JSON.stringify(rawKey).replace(/"/g, '"');
|
||||||
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, '"');
|
const deleteArg = JSON.stringify(name).replace(/"/g, '"');
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
if (!sourceStats[source]) {
|
|
||||||
sourceStats[source] = {
|
|
||||||
success: 0,
|
|
||||||
failure: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isFailed = detail.failed === true;
|
const isFailed = detail.failed === true;
|
||||||
|
|
||||||
|
if (source) {
|
||||||
|
const bucket = ensureBucket(sourceStats, source);
|
||||||
if (isFailed) {
|
if (isFailed) {
|
||||||
sourceStats[source].failure += 1;
|
bucket.failure += 1;
|
||||||
} else {
|
} else {
|
||||||
sourceStats[source].success += 1;
|
bucket.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) {
|
} catch (error) {
|
||||||
console.error('获取统计信息失败:', error);
|
console.error('获取统计信息失败:', error);
|
||||||
return {};
|
return { bySource: {}, byAuthIndex: {} };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user