mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 03:00:49 +08:00
fix(usage): include auth-index-only usage in credential stats
This commit is contained in:
@@ -1,7 +1,12 @@
|
|||||||
import { useMemo, useState, useEffect } from 'react';
|
import { useMemo, useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { computeKeyStats, collectUsageDetails, buildCandidateUsageSourceIds, formatCompactNumber } from '@/utils/usage';
|
import {
|
||||||
|
computeKeyStats,
|
||||||
|
collectUsageDetails,
|
||||||
|
buildCandidateUsageSourceIds,
|
||||||
|
formatCompactNumber
|
||||||
|
} from '@/utils/usage';
|
||||||
import { authFilesApi } from '@/services/api/authFiles';
|
import { authFilesApi } from '@/services/api/authFiles';
|
||||||
import type { GeminiKeyConfig, ProviderKeyConfig, OpenAIProviderConfig } from '@/types';
|
import type { GeminiKeyConfig, ProviderKeyConfig, OpenAIProviderConfig } from '@/types';
|
||||||
import type { AuthFileItem } from '@/types/authFile';
|
import type { AuthFileItem } from '@/types/authFile';
|
||||||
@@ -33,6 +38,11 @@ interface CredentialRow {
|
|||||||
successRate: number;
|
successRate: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CredentialBucket {
|
||||||
|
success: number;
|
||||||
|
failure: number;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeAuthIndexValue(value: unknown): string | null {
|
function normalizeAuthIndexValue(value: unknown): string | null {
|
||||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||||
return value.toString();
|
return value.toString();
|
||||||
@@ -87,8 +97,21 @@ export function CredentialStatsCard({
|
|||||||
const rows = useMemo((): CredentialRow[] => {
|
const rows = useMemo((): CredentialRow[] => {
|
||||||
if (!usage) return [];
|
if (!usage) return [];
|
||||||
const { bySource } = computeKeyStats(usage);
|
const { bySource } = computeKeyStats(usage);
|
||||||
|
const details = collectUsageDetails(usage);
|
||||||
const result: CredentialRow[] = [];
|
const result: CredentialRow[] = [];
|
||||||
const consumedSourceIds = new Set<string>();
|
const consumedSourceIds = new Set<string>();
|
||||||
|
const authIndexToRowIndex = new Map<string, number>();
|
||||||
|
const sourceToAuthIndex = new Map<string, string>();
|
||||||
|
const fallbackByAuthIndex = new Map<string, CredentialBucket>();
|
||||||
|
|
||||||
|
const mergeBucketToRow = (index: number, bucket: CredentialBucket) => {
|
||||||
|
const target = result[index];
|
||||||
|
if (!target) return;
|
||||||
|
target.success += bucket.success;
|
||||||
|
target.failure += bucket.failure;
|
||||||
|
target.total = target.success + target.failure;
|
||||||
|
target.successRate = target.total > 0 ? (target.success / target.total) * 100 : 100;
|
||||||
|
};
|
||||||
|
|
||||||
// Aggregate all candidate source IDs for one provider config into a single row
|
// Aggregate all candidate source IDs for one provider config into a single row
|
||||||
const addConfigRow = (
|
const addConfigRow = (
|
||||||
@@ -140,26 +163,38 @@ export function CredentialStatsCard({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Build source → auth file name mapping for remaining unmatched entries.
|
// Build source → auth file name mapping for remaining unmatched entries.
|
||||||
// Cross-reference via usage details: each detail has both source and auth_index.
|
// Also collect fallback stats for details without source but with auth_index.
|
||||||
const sourceToAuthFile = new Map<string, CredentialInfo>();
|
const sourceToAuthFile = new Map<string, CredentialInfo>();
|
||||||
if (authFileMap.size > 0) {
|
details.forEach((d) => {
|
||||||
const details = collectUsageDetails(usage);
|
const authIdx = normalizeAuthIndexValue(d.auth_index);
|
||||||
details.forEach((d) => {
|
if (!d.source) {
|
||||||
if (consumedSourceIds.has(d.source) || sourceToAuthFile.has(d.source)) return;
|
if (!authIdx) return;
|
||||||
const authIdx = normalizeAuthIndexValue(d.auth_index);
|
const fallback = fallbackByAuthIndex.get(authIdx) ?? { success: 0, failure: 0 };
|
||||||
if (authIdx) {
|
if (d.failed === true) {
|
||||||
const mapped = authFileMap.get(authIdx);
|
fallback.failure += 1;
|
||||||
if (mapped) sourceToAuthFile.set(d.source, mapped);
|
} else {
|
||||||
|
fallback.success += 1;
|
||||||
}
|
}
|
||||||
});
|
fallbackByAuthIndex.set(authIdx, fallback);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authIdx || consumedSourceIds.has(d.source)) return;
|
||||||
|
if (!sourceToAuthIndex.has(d.source)) {
|
||||||
|
sourceToAuthIndex.set(d.source, authIdx);
|
||||||
|
}
|
||||||
|
if (!sourceToAuthFile.has(d.source)) {
|
||||||
|
const mapped = authFileMap.get(authIdx);
|
||||||
|
if (mapped) sourceToAuthFile.set(d.source, mapped);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Remaining unmatched bySource entries — resolve name from auth files if possible
|
// Remaining unmatched bySource entries — resolve name from auth files if possible
|
||||||
Object.entries(bySource).forEach(([key, bucket]) => {
|
Object.entries(bySource).forEach(([key, bucket]) => {
|
||||||
if (consumedSourceIds.has(key)) return;
|
if (consumedSourceIds.has(key)) return;
|
||||||
const total = bucket.success + bucket.failure;
|
const total = bucket.success + bucket.failure;
|
||||||
const authFile = sourceToAuthFile.get(key);
|
const authFile = sourceToAuthFile.get(key);
|
||||||
result.push({
|
const row = {
|
||||||
key,
|
key,
|
||||||
displayName: authFile?.name || (key.startsWith('t:') ? key.slice(2) : key),
|
displayName: authFile?.name || (key.startsWith('t:') ? key.slice(2) : key),
|
||||||
type: authFile?.type || '',
|
type: authFile?.type || '',
|
||||||
@@ -167,7 +202,46 @@ export function CredentialStatsCard({
|
|||||||
failure: bucket.failure,
|
failure: bucket.failure,
|
||||||
total,
|
total,
|
||||||
successRate: total > 0 ? (bucket.success / total) * 100 : 100,
|
successRate: total > 0 ? (bucket.success / total) * 100 : 100,
|
||||||
});
|
};
|
||||||
|
const rowIndex = result.push(row) - 1;
|
||||||
|
const authIdx = sourceToAuthIndex.get(key);
|
||||||
|
if (authIdx && !authIndexToRowIndex.has(authIdx)) {
|
||||||
|
authIndexToRowIndex.set(authIdx, rowIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Include requests that have auth_index but missing source.
|
||||||
|
fallbackByAuthIndex.forEach((bucket, authIdx) => {
|
||||||
|
if (bucket.success + bucket.failure === 0) return;
|
||||||
|
|
||||||
|
const mapped = authFileMap.get(authIdx);
|
||||||
|
let targetRowIndex = authIndexToRowIndex.get(authIdx);
|
||||||
|
if (targetRowIndex === undefined && mapped) {
|
||||||
|
const matchedIndex = result.findIndex(
|
||||||
|
(row) => row.displayName === mapped.name && row.type === mapped.type
|
||||||
|
);
|
||||||
|
if (matchedIndex >= 0) {
|
||||||
|
targetRowIndex = matchedIndex;
|
||||||
|
authIndexToRowIndex.set(authIdx, matchedIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetRowIndex !== undefined) {
|
||||||
|
mergeBucketToRow(targetRowIndex, bucket);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = bucket.success + bucket.failure;
|
||||||
|
const rowIndex = result.push({
|
||||||
|
key: `auth:${authIdx}`,
|
||||||
|
displayName: mapped?.name || authIdx,
|
||||||
|
type: mapped?.type || '',
|
||||||
|
success: bucket.success,
|
||||||
|
failure: bucket.failure,
|
||||||
|
total,
|
||||||
|
successRate: (bucket.success / total) * 100
|
||||||
|
}) - 1;
|
||||||
|
authIndexToRowIndex.set(authIdx, rowIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
return result.sort((a, b) => b.total - a.total);
|
return result.sort((a, b) => b.total - a.total);
|
||||||
|
|||||||
Reference in New Issue
Block a user