Refactor provider usage tracking to utilize recent requests data

- Introduced `useProviderRecentRequests` hook to manage recent request data for providers.
- Replaced usage of `useProviderStats` with `useProviderRecentRequests` in relevant components.
- Updated `VertexSection` and other provider components to consume recent request data instead of key stats.
- Removed obsolete `useProviderStats` hook and related usage details.
- Added new utility functions for handling recent request data, including normalization and aggregation.
- Updated API service to fetch recent request usage data.
- Refactored `AuthFileCard` and related components to utilize recent request statistics.
- Cleaned up unused code and types related to previous usage stats implementation.
This commit is contained in:
Supra4E8C
2026-05-02 02:53:15 +08:00
parent 8ed837c3d7
commit b25f722fef
23 changed files with 505 additions and 473 deletions
+157
View File
@@ -0,0 +1,157 @@
import type { StatusBarData, StatusBlockDetail, StatusBlockState } from '@/utils/usage';
export type { StatusBarData, StatusBlockDetail, StatusBlockState };
export interface RecentRequestBucket {
time?: string;
success: number;
failed: number;
}
export type ApiKeyUsageResponse = Record<string, Record<string, RecentRequestBucket[]>>;
const RECENT_REQUEST_BLOCK_COUNT = 20;
const RECENT_REQUEST_BLOCK_DURATION_MS = 10 * 60 * 1000;
const toFiniteNumber = (value: unknown): number => {
const numberValue = typeof value === 'number' ? value : Number(value);
return Number.isFinite(numberValue) ? numberValue : 0;
};
export function buildRecentRequestCompositeKey(baseUrl: unknown, apiKey: unknown): string {
const normalizedBaseUrl = String(baseUrl ?? '').trim();
const normalizedApiKey = String(apiKey ?? '').trim();
return `${normalizedBaseUrl}|${normalizedApiKey}`;
}
export function normalizeRecentRequestAuthIndex(value: unknown): string | null {
if (typeof value === 'number' && Number.isFinite(value)) {
return value.toString();
}
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed ? trimmed : null;
}
return null;
}
export function normalizeRecentRequestBuckets(input: unknown): RecentRequestBucket[] {
if (!Array.isArray(input)) {
return [];
}
return input.slice(-RECENT_REQUEST_BLOCK_COUNT).map((item) => {
const record = item && typeof item === 'object' ? (item as Record<string, unknown>) : {};
const time = typeof record.time === 'string' ? record.time : undefined;
return {
...(time ? { time } : {}),
success: toFiniteNumber(record.success),
failed: toFiniteNumber(record.failed),
};
});
}
export function mergeRecentRequestBucketGroups(
groups: RecentRequestBucket[][]
): RecentRequestBucket[] {
const normalizedGroups = groups
.map((group) => normalizeRecentRequestBuckets(group))
.filter((group) => group.length > 0);
if (normalizedGroups.length === 0) {
return [];
}
const mergedLength = Math.min(
RECENT_REQUEST_BLOCK_COUNT,
Math.max(...normalizedGroups.map((group) => group.length))
);
const merged: RecentRequestBucket[] = Array.from({ length: mergedLength }, () => ({
success: 0,
failed: 0,
}));
normalizedGroups.forEach((group) => {
const tail = group.slice(-mergedLength);
const offset = mergedLength - tail.length;
tail.forEach((bucket, index) => {
const target = merged[offset + index];
target.success += bucket.success;
target.failed += bucket.failed;
if (!target.time && bucket.time) {
target.time = bucket.time;
}
});
});
return merged;
}
export function sumRecentRequests(
buckets: RecentRequestBucket[]
): { success: number; failure: number } {
return normalizeRecentRequestBuckets(buckets).reduce(
(total, bucket) => ({
success: total.success + bucket.success,
failure: total.failure + bucket.failed,
}),
{ success: 0, failure: 0 }
);
}
export function statusBarDataFromRecentRequests(buckets: RecentRequestBucket[]): StatusBarData {
const normalizedBuckets = normalizeRecentRequestBuckets(buckets);
const emptyBucketCount = Math.max(0, RECENT_REQUEST_BLOCK_COUNT - normalizedBuckets.length);
const blockStats = [
...Array.from({ length: emptyBucketCount }, () => ({ success: 0, failed: 0 })),
...normalizedBuckets.slice(-RECENT_REQUEST_BLOCK_COUNT),
];
const now = Date.now();
const windowStart = now - RECENT_REQUEST_BLOCK_COUNT * RECENT_REQUEST_BLOCK_DURATION_MS;
const blocks: StatusBlockState[] = [];
const blockDetails: StatusBarData['blockDetails'] = [];
let totalSuccess = 0;
let totalFailure = 0;
blockStats.forEach((bucket, index) => {
const success = bucket.success;
const failure = bucket.failed;
const total = success + failure;
totalSuccess += success;
totalFailure += failure;
if (total === 0) {
blocks.push('idle');
} else if (failure === 0) {
blocks.push('success');
} else if (success === 0) {
blocks.push('failure');
} else {
blocks.push('mixed');
}
const blockStartTime = windowStart + index * RECENT_REQUEST_BLOCK_DURATION_MS;
blockDetails.push({
success,
failure,
rate: total > 0 ? success / total : -1,
startTime: blockStartTime,
endTime: blockStartTime + RECENT_REQUEST_BLOCK_DURATION_MS,
});
});
const total = totalSuccess + totalFailure;
return {
blocks,
blockDetails,
successRate: total > 0 ? (totalSuccess / total) * 100 : 100,
totalSuccess,
totalFailure,
};
}