mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
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:
@@ -6,23 +6,21 @@ import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
||||
import iconClaude from '@/assets/icons/claude.svg';
|
||||
import type { ProviderKeyConfig } from '@/types';
|
||||
import { maskApiKey } from '@/utils/format';
|
||||
import { calculateStatusBarData, type KeyStats } from '@/utils/usage';
|
||||
import { type UsageDetailsByAuthIndex, type UsageDetailsBySource } from '@/utils/usageIndex';
|
||||
import { statusBarDataFromRecentRequests } from '@/utils/recentRequests';
|
||||
import styles from '@/pages/AiProvidersPage.module.scss';
|
||||
import { ProviderList } from '../ProviderList';
|
||||
import { ProviderStatusBar } from '../ProviderStatusBar';
|
||||
import {
|
||||
collectUsageDetailsForIdentity,
|
||||
getProviderConfigKey,
|
||||
getStatsForIdentity,
|
||||
getProviderRecentBuckets,
|
||||
getProviderRecentStats,
|
||||
hasDisableAllModelsRule,
|
||||
type ProviderRecentUsageMap,
|
||||
} from '../utils';
|
||||
|
||||
interface ClaudeSectionProps {
|
||||
configs: ProviderKeyConfig[];
|
||||
keyStats: KeyStats;
|
||||
usageDetailsBySource: UsageDetailsBySource;
|
||||
usageDetailsByAuthIndex: UsageDetailsByAuthIndex;
|
||||
usageByProvider: ProviderRecentUsageMap;
|
||||
loading: boolean;
|
||||
disableControls: boolean;
|
||||
isSwitching: boolean;
|
||||
@@ -34,9 +32,7 @@ interface ClaudeSectionProps {
|
||||
|
||||
export function ClaudeSection({
|
||||
configs,
|
||||
keyStats,
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex,
|
||||
usageByProvider,
|
||||
loading,
|
||||
disableControls,
|
||||
isSwitching,
|
||||
@@ -50,25 +46,21 @@ export function ClaudeSection({
|
||||
const toggleDisabled = disableControls || loading || isSwitching;
|
||||
|
||||
const statusBarCache = useMemo(() => {
|
||||
const cache = new Map<string, ReturnType<typeof calculateStatusBarData>>();
|
||||
const cache = new Map<string, ReturnType<typeof statusBarDataFromRecentRequests>>();
|
||||
|
||||
configs.forEach((config, index) => {
|
||||
if (!config.apiKey) return;
|
||||
const configKey = getProviderConfigKey(config, index);
|
||||
cache.set(
|
||||
configKey,
|
||||
calculateStatusBarData(
|
||||
collectUsageDetailsForIdentity(
|
||||
{ authIndex: config.authIndex, apiKey: config.apiKey, prefix: config.prefix },
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex
|
||||
)
|
||||
statusBarDataFromRecentRequests(
|
||||
getProviderRecentBuckets(usageByProvider, 'claude', config.apiKey, config.baseUrl)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return cache;
|
||||
}, [configs, usageDetailsByAuthIndex, usageDetailsBySource]);
|
||||
}, [configs, usageByProvider]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -104,15 +96,18 @@ export function ClaudeSection({
|
||||
/>
|
||||
)}
|
||||
renderContent={(item, index) => {
|
||||
const stats = getStatsForIdentity(
|
||||
{ authIndex: item.authIndex, apiKey: item.apiKey, prefix: item.prefix },
|
||||
keyStats
|
||||
const stats = getProviderRecentStats(
|
||||
usageByProvider,
|
||||
'claude',
|
||||
item.apiKey,
|
||||
item.baseUrl
|
||||
);
|
||||
const headerEntries = Object.entries(item.headers || {});
|
||||
const configDisabled = hasDisableAllModelsRule(item.excludedModels);
|
||||
const excludedModels = item.excludedModels ?? [];
|
||||
const statusData =
|
||||
statusBarCache.get(getProviderConfigKey(item, index)) || calculateStatusBarData([]);
|
||||
statusBarCache.get(getProviderConfigKey(item, index)) ||
|
||||
statusBarDataFromRecentRequests([]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
||||
@@ -6,23 +6,21 @@ import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
||||
import iconCodex from '@/assets/icons/codex.svg';
|
||||
import type { ProviderKeyConfig } from '@/types';
|
||||
import { maskApiKey } from '@/utils/format';
|
||||
import { calculateStatusBarData, type KeyStats } from '@/utils/usage';
|
||||
import { type UsageDetailsByAuthIndex, type UsageDetailsBySource } from '@/utils/usageIndex';
|
||||
import { statusBarDataFromRecentRequests } from '@/utils/recentRequests';
|
||||
import styles from '@/pages/AiProvidersPage.module.scss';
|
||||
import { ProviderList } from '../ProviderList';
|
||||
import { ProviderStatusBar } from '../ProviderStatusBar';
|
||||
import {
|
||||
collectUsageDetailsForIdentity,
|
||||
getProviderConfigKey,
|
||||
getStatsForIdentity,
|
||||
getProviderRecentBuckets,
|
||||
getProviderRecentStats,
|
||||
hasDisableAllModelsRule,
|
||||
type ProviderRecentUsageMap,
|
||||
} from '../utils';
|
||||
|
||||
interface CodexSectionProps {
|
||||
configs: ProviderKeyConfig[];
|
||||
keyStats: KeyStats;
|
||||
usageDetailsBySource: UsageDetailsBySource;
|
||||
usageDetailsByAuthIndex: UsageDetailsByAuthIndex;
|
||||
usageByProvider: ProviderRecentUsageMap;
|
||||
loading: boolean;
|
||||
disableControls: boolean;
|
||||
isSwitching: boolean;
|
||||
@@ -34,9 +32,7 @@ interface CodexSectionProps {
|
||||
|
||||
export function CodexSection({
|
||||
configs,
|
||||
keyStats,
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex,
|
||||
usageByProvider,
|
||||
loading,
|
||||
disableControls,
|
||||
isSwitching,
|
||||
@@ -50,25 +46,21 @@ export function CodexSection({
|
||||
const toggleDisabled = disableControls || loading || isSwitching;
|
||||
|
||||
const statusBarCache = useMemo(() => {
|
||||
const cache = new Map<string, ReturnType<typeof calculateStatusBarData>>();
|
||||
const cache = new Map<string, ReturnType<typeof statusBarDataFromRecentRequests>>();
|
||||
|
||||
configs.forEach((config, index) => {
|
||||
if (!config.apiKey) return;
|
||||
const configKey = getProviderConfigKey(config, index);
|
||||
cache.set(
|
||||
configKey,
|
||||
calculateStatusBarData(
|
||||
collectUsageDetailsForIdentity(
|
||||
{ authIndex: config.authIndex, apiKey: config.apiKey, prefix: config.prefix },
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex
|
||||
)
|
||||
statusBarDataFromRecentRequests(
|
||||
getProviderRecentBuckets(usageByProvider, 'codex', config.apiKey, config.baseUrl)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return cache;
|
||||
}, [configs, usageDetailsByAuthIndex, usageDetailsBySource]);
|
||||
}, [configs, usageByProvider]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -104,15 +96,18 @@ export function CodexSection({
|
||||
/>
|
||||
)}
|
||||
renderContent={(item, index) => {
|
||||
const stats = getStatsForIdentity(
|
||||
{ authIndex: item.authIndex, apiKey: item.apiKey, prefix: item.prefix },
|
||||
keyStats
|
||||
const stats = getProviderRecentStats(
|
||||
usageByProvider,
|
||||
'codex',
|
||||
item.apiKey,
|
||||
item.baseUrl
|
||||
);
|
||||
const headerEntries = Object.entries(item.headers || {});
|
||||
const configDisabled = hasDisableAllModelsRule(item.excludedModels);
|
||||
const excludedModels = item.excludedModels ?? [];
|
||||
const statusData =
|
||||
statusBarCache.get(getProviderConfigKey(item, index)) || calculateStatusBarData([]);
|
||||
statusBarCache.get(getProviderConfigKey(item, index)) ||
|
||||
statusBarDataFromRecentRequests([]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
||||
@@ -6,23 +6,21 @@ import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
||||
import iconGemini from '@/assets/icons/gemini.svg';
|
||||
import type { GeminiKeyConfig } from '@/types';
|
||||
import { maskApiKey } from '@/utils/format';
|
||||
import { calculateStatusBarData, type KeyStats } from '@/utils/usage';
|
||||
import { type UsageDetailsByAuthIndex, type UsageDetailsBySource } from '@/utils/usageIndex';
|
||||
import { statusBarDataFromRecentRequests } from '@/utils/recentRequests';
|
||||
import styles from '@/pages/AiProvidersPage.module.scss';
|
||||
import { ProviderList } from '../ProviderList';
|
||||
import { ProviderStatusBar } from '../ProviderStatusBar';
|
||||
import {
|
||||
collectUsageDetailsForIdentity,
|
||||
getProviderConfigKey,
|
||||
getStatsForIdentity,
|
||||
getProviderRecentBuckets,
|
||||
getProviderRecentStats,
|
||||
hasDisableAllModelsRule,
|
||||
type ProviderRecentUsageMap,
|
||||
} from '../utils';
|
||||
|
||||
interface GeminiSectionProps {
|
||||
configs: GeminiKeyConfig[];
|
||||
keyStats: KeyStats;
|
||||
usageDetailsBySource: UsageDetailsBySource;
|
||||
usageDetailsByAuthIndex: UsageDetailsByAuthIndex;
|
||||
usageByProvider: ProviderRecentUsageMap;
|
||||
loading: boolean;
|
||||
disableControls: boolean;
|
||||
isSwitching: boolean;
|
||||
@@ -34,9 +32,7 @@ interface GeminiSectionProps {
|
||||
|
||||
export function GeminiSection({
|
||||
configs,
|
||||
keyStats,
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex,
|
||||
usageByProvider,
|
||||
loading,
|
||||
disableControls,
|
||||
isSwitching,
|
||||
@@ -50,25 +46,21 @@ export function GeminiSection({
|
||||
const toggleDisabled = disableControls || loading || isSwitching;
|
||||
|
||||
const statusBarCache = useMemo(() => {
|
||||
const cache = new Map<string, ReturnType<typeof calculateStatusBarData>>();
|
||||
const cache = new Map<string, ReturnType<typeof statusBarDataFromRecentRequests>>();
|
||||
|
||||
configs.forEach((config, index) => {
|
||||
if (!config.apiKey) return;
|
||||
const configKey = getProviderConfigKey(config, index);
|
||||
cache.set(
|
||||
configKey,
|
||||
calculateStatusBarData(
|
||||
collectUsageDetailsForIdentity(
|
||||
{ authIndex: config.authIndex, apiKey: config.apiKey, prefix: config.prefix },
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex
|
||||
)
|
||||
statusBarDataFromRecentRequests(
|
||||
getProviderRecentBuckets(usageByProvider, 'gemini', config.apiKey, config.baseUrl)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return cache;
|
||||
}, [configs, usageDetailsByAuthIndex, usageDetailsBySource]);
|
||||
}, [configs, usageByProvider]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -104,15 +96,18 @@ export function GeminiSection({
|
||||
/>
|
||||
)}
|
||||
renderContent={(item, index) => {
|
||||
const stats = getStatsForIdentity(
|
||||
{ authIndex: item.authIndex, apiKey: item.apiKey, prefix: item.prefix },
|
||||
keyStats
|
||||
const stats = getProviderRecentStats(
|
||||
usageByProvider,
|
||||
'gemini',
|
||||
item.apiKey,
|
||||
item.baseUrl
|
||||
);
|
||||
const headerEntries = Object.entries(item.headers || {});
|
||||
const configDisabled = hasDisableAllModelsRule(item.excludedModels);
|
||||
const excludedModels = item.excludedModels ?? [];
|
||||
const statusData =
|
||||
statusBarCache.get(getProviderConfigKey(item, index)) || calculateStatusBarData([]);
|
||||
statusBarCache.get(getProviderConfigKey(item, index)) ||
|
||||
statusBarDataFromRecentRequests([]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
||||
@@ -18,16 +18,16 @@ import iconOpenaiLight from '@/assets/icons/openai-light.svg';
|
||||
import iconOpenaiDark from '@/assets/icons/openai-dark.svg';
|
||||
import type { OpenAIProviderConfig } from '@/types';
|
||||
import { maskApiKey } from '@/utils/format';
|
||||
import { calculateStatusBarData, type KeyStats } from '@/utils/usage';
|
||||
import { type UsageDetailsByAuthIndex, type UsageDetailsBySource } from '@/utils/usageIndex';
|
||||
import { statusBarDataFromRecentRequests } from '@/utils/recentRequests';
|
||||
import styles from '@/pages/AiProvidersPage.module.scss';
|
||||
import { ProviderStatusBar } from '../ProviderStatusBar';
|
||||
import { usePageTransitionLayer } from '@/components/common/PageTransitionLayer';
|
||||
import {
|
||||
collectOpenAIProviderUsageDetails,
|
||||
getOpenAIProviderRecentStats,
|
||||
getOpenAIProviderRecentStatusData,
|
||||
getOpenAIProviderKey,
|
||||
getOpenAIProviderStats,
|
||||
getStatsForIdentity,
|
||||
getProviderRecentStats,
|
||||
type ProviderRecentUsageMap,
|
||||
} from '../utils';
|
||||
|
||||
type SortOption = 'name' | 'priority' | 'recent-success';
|
||||
@@ -40,13 +40,11 @@ interface FloatingToolbarStyle {
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
const EMPTY_STATUS_BAR = calculateStatusBarData([]);
|
||||
const EMPTY_STATUS_BAR = statusBarDataFromRecentRequests([]);
|
||||
|
||||
interface OpenAISectionProps {
|
||||
configs: OpenAIProviderConfig[];
|
||||
keyStats: KeyStats;
|
||||
usageDetailsBySource: UsageDetailsBySource;
|
||||
usageDetailsByAuthIndex: UsageDetailsByAuthIndex;
|
||||
usageByProvider: ProviderRecentUsageMap;
|
||||
loading: boolean;
|
||||
disableControls: boolean;
|
||||
isSwitching: boolean;
|
||||
@@ -72,9 +70,7 @@ const getApiKeyEntryRenderKey = (
|
||||
|
||||
export function OpenAISection({
|
||||
configs,
|
||||
keyStats,
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex,
|
||||
usageByProvider,
|
||||
loading,
|
||||
disableControls,
|
||||
isSwitching,
|
||||
@@ -253,20 +249,15 @@ export function OpenAISection({
|
||||
: t('ai_providers.model_search_placeholder');
|
||||
|
||||
const statusBarCache = useMemo(() => {
|
||||
const cache = new Map<string, ReturnType<typeof calculateStatusBarData>>();
|
||||
const cache = new Map<string, ReturnType<typeof statusBarDataFromRecentRequests>>();
|
||||
|
||||
configs.forEach((provider, index) => {
|
||||
const providerKey = getOpenAIProviderKey(provider, index);
|
||||
cache.set(
|
||||
providerKey,
|
||||
calculateStatusBarData(
|
||||
collectOpenAIProviderUsageDetails(provider, usageDetailsBySource, usageDetailsByAuthIndex)
|
||||
)
|
||||
);
|
||||
cache.set(providerKey, getOpenAIProviderRecentStatusData(provider, usageByProvider));
|
||||
});
|
||||
|
||||
return cache;
|
||||
}, [configs, usageDetailsByAuthIndex, usageDetailsBySource]);
|
||||
}, [configs, usageByProvider]);
|
||||
|
||||
const sortOptions = useMemo(
|
||||
() => [
|
||||
@@ -288,7 +279,12 @@ export function OpenAISection({
|
||||
const direction = sortDirection === 'desc' ? -1 : 1;
|
||||
const providerStats =
|
||||
sortOption === 'recent-success'
|
||||
? new Map(sorted.map(({ config }) => [config, getOpenAIProviderStats(config, keyStats)]))
|
||||
? new Map(
|
||||
sorted.map(({ config }) => [
|
||||
config,
|
||||
getOpenAIProviderRecentStats(config, usageByProvider),
|
||||
])
|
||||
)
|
||||
: null;
|
||||
|
||||
switch (sortOption) {
|
||||
@@ -326,7 +322,7 @@ export function OpenAISection({
|
||||
}
|
||||
|
||||
return sorted;
|
||||
}, [configs, sortOption, sortDirection, keyStats, selectedModels]);
|
||||
}, [configs, sortOption, sortDirection, usageByProvider, selectedModels]);
|
||||
|
||||
const toggleModelSelection = (modelName: string) => {
|
||||
setSelectedModels((prev) => {
|
||||
@@ -528,7 +524,7 @@ export function OpenAISection({
|
||||
);
|
||||
|
||||
const renderProviderCard = ({ config: provider, originalIndex }: IndexedOpenAIProvider) => {
|
||||
const stats = getOpenAIProviderStats(provider, keyStats);
|
||||
const stats = getOpenAIProviderRecentStats(provider, usageByProvider);
|
||||
const headerEntries = Object.entries(provider.headers || {});
|
||||
const apiKeyEntries = provider.apiKeyEntries || [];
|
||||
const statusData =
|
||||
@@ -580,9 +576,11 @@ export function OpenAISection({
|
||||
</div>
|
||||
<div className={styles.apiKeyEntryList}>
|
||||
{apiKeyEntries.map((entry, entryIndex) => {
|
||||
const entryStats = getStatsForIdentity(
|
||||
{ authIndex: entry.authIndex, apiKey: entry.apiKey },
|
||||
keyStats
|
||||
const entryStats = getProviderRecentStats(
|
||||
usageByProvider,
|
||||
provider.name,
|
||||
entry.apiKey,
|
||||
provider.baseUrl
|
||||
);
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { StatusBarData, StatusBlockDetail } from '@/utils/usage';
|
||||
import type { StatusBarData, StatusBlockDetail } from '@/utils/recentRequests';
|
||||
import defaultStyles from '@/pages/AiProvidersPage.module.scss';
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,23 +6,21 @@ import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
||||
import iconVertex from '@/assets/icons/vertex.svg';
|
||||
import type { ProviderKeyConfig } from '@/types';
|
||||
import { maskApiKey } from '@/utils/format';
|
||||
import { calculateStatusBarData, type KeyStats } from '@/utils/usage';
|
||||
import { type UsageDetailsByAuthIndex, type UsageDetailsBySource } from '@/utils/usageIndex';
|
||||
import { statusBarDataFromRecentRequests } from '@/utils/recentRequests';
|
||||
import styles from '@/pages/AiProvidersPage.module.scss';
|
||||
import { ProviderList } from '../ProviderList';
|
||||
import { ProviderStatusBar } from '../ProviderStatusBar';
|
||||
import {
|
||||
collectUsageDetailsForIdentity,
|
||||
getProviderConfigKey,
|
||||
getStatsForIdentity,
|
||||
getProviderRecentBuckets,
|
||||
getProviderRecentStats,
|
||||
hasDisableAllModelsRule,
|
||||
type ProviderRecentUsageMap,
|
||||
} from '../utils';
|
||||
|
||||
interface VertexSectionProps {
|
||||
configs: ProviderKeyConfig[];
|
||||
keyStats: KeyStats;
|
||||
usageDetailsBySource: UsageDetailsBySource;
|
||||
usageDetailsByAuthIndex: UsageDetailsByAuthIndex;
|
||||
usageByProvider: ProviderRecentUsageMap;
|
||||
loading: boolean;
|
||||
disableControls: boolean;
|
||||
isSwitching: boolean;
|
||||
@@ -34,9 +32,7 @@ interface VertexSectionProps {
|
||||
|
||||
export function VertexSection({
|
||||
configs,
|
||||
keyStats,
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex,
|
||||
usageByProvider,
|
||||
loading,
|
||||
disableControls,
|
||||
isSwitching,
|
||||
@@ -50,25 +46,21 @@ export function VertexSection({
|
||||
const toggleDisabled = disableControls || loading || isSwitching;
|
||||
|
||||
const statusBarCache = useMemo(() => {
|
||||
const cache = new Map<string, ReturnType<typeof calculateStatusBarData>>();
|
||||
const cache = new Map<string, ReturnType<typeof statusBarDataFromRecentRequests>>();
|
||||
|
||||
configs.forEach((config, index) => {
|
||||
if (!config.apiKey) return;
|
||||
const configKey = getProviderConfigKey(config, index);
|
||||
cache.set(
|
||||
configKey,
|
||||
calculateStatusBarData(
|
||||
collectUsageDetailsForIdentity(
|
||||
{ authIndex: config.authIndex, apiKey: config.apiKey, prefix: config.prefix },
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex
|
||||
)
|
||||
statusBarDataFromRecentRequests(
|
||||
getProviderRecentBuckets(usageByProvider, 'vertex', config.apiKey, config.baseUrl)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return cache;
|
||||
}, [configs, usageDetailsByAuthIndex, usageDetailsBySource]);
|
||||
}, [configs, usageByProvider]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -104,15 +96,18 @@ export function VertexSection({
|
||||
/>
|
||||
)}
|
||||
renderContent={(item, index) => {
|
||||
const stats = getStatsForIdentity(
|
||||
{ authIndex: item.authIndex, apiKey: item.apiKey, prefix: item.prefix },
|
||||
keyStats
|
||||
const stats = getProviderRecentStats(
|
||||
usageByProvider,
|
||||
'vertex',
|
||||
item.apiKey,
|
||||
item.baseUrl
|
||||
);
|
||||
const headerEntries = Object.entries(item.headers || {});
|
||||
const configDisabled = hasDisableAllModelsRule(item.excludedModels);
|
||||
const excludedModels = item.excludedModels ?? [];
|
||||
const statusData =
|
||||
statusBarCache.get(getProviderConfigKey(item, index)) || calculateStatusBarData([]);
|
||||
statusBarCache.get(getProviderConfigKey(item, index)) ||
|
||||
statusBarDataFromRecentRequests([]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useInterval } from '@/hooks/useInterval';
|
||||
import { apiKeyUsageApi } from '@/services/api';
|
||||
import {
|
||||
normalizeRecentRequestBuckets,
|
||||
type ApiKeyUsageResponse,
|
||||
type RecentRequestBucket,
|
||||
} from '@/utils/recentRequests';
|
||||
|
||||
const PROVIDER_RECENT_REQUESTS_STALE_TIME_MS = 240_000;
|
||||
|
||||
export type ProviderRecentRequests = Map<string, Map<string, RecentRequestBucket[]>>;
|
||||
|
||||
export type UseProviderRecentRequestsOptions = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
const EMPTY_USAGE_BY_PROVIDER: ProviderRecentRequests = new Map();
|
||||
|
||||
let cachedUsageByProvider: ProviderRecentRequests = EMPTY_USAGE_BY_PROVIDER;
|
||||
let cachedAt = 0;
|
||||
let inFlightRequest: Promise<ProviderRecentRequests> | null = null;
|
||||
|
||||
const normalizeProviderKey = (value: unknown): string => String(value ?? '').trim().toLowerCase();
|
||||
|
||||
const normalizeApiKeyUsageResponse = (payload: ApiKeyUsageResponse): ProviderRecentRequests => {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
||||
return EMPTY_USAGE_BY_PROVIDER;
|
||||
}
|
||||
|
||||
const usageByProvider: ProviderRecentRequests = new Map();
|
||||
|
||||
Object.entries(payload).forEach(([provider, entries]) => {
|
||||
const providerKey = normalizeProviderKey(provider);
|
||||
if (!providerKey || !entries || typeof entries !== 'object' || Array.isArray(entries)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const usageByCompositeKey = new Map<string, RecentRequestBucket[]>();
|
||||
Object.entries(entries).forEach(([compositeKey, buckets]) => {
|
||||
usageByCompositeKey.set(compositeKey, normalizeRecentRequestBuckets(buckets));
|
||||
});
|
||||
|
||||
usageByProvider.set(providerKey, usageByCompositeKey);
|
||||
});
|
||||
|
||||
return usageByProvider;
|
||||
};
|
||||
|
||||
const fetchProviderRecentRequests = async (): Promise<ProviderRecentRequests> => {
|
||||
if (!inFlightRequest) {
|
||||
inFlightRequest = apiKeyUsageApi
|
||||
.getUsage()
|
||||
.then((payload) => {
|
||||
const normalized = normalizeApiKeyUsageResponse(payload);
|
||||
cachedUsageByProvider = normalized;
|
||||
cachedAt = Date.now();
|
||||
return normalized;
|
||||
})
|
||||
.finally(() => {
|
||||
inFlightRequest = null;
|
||||
});
|
||||
}
|
||||
|
||||
return inFlightRequest;
|
||||
};
|
||||
|
||||
export function useProviderRecentRequests(options: UseProviderRecentRequestsOptions = {}) {
|
||||
const enabled = options.enabled ?? true;
|
||||
const [usageByProvider, setUsageByProvider] = useState<ProviderRecentRequests>(
|
||||
cachedUsageByProvider
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const loadRecentRequests = useCallback(
|
||||
async (loadOptions: { force?: boolean } = {}) => {
|
||||
if (!enabled) {
|
||||
return EMPTY_USAGE_BY_PROVIDER;
|
||||
}
|
||||
|
||||
const hasFreshCache =
|
||||
cachedAt > 0 &&
|
||||
Date.now() - cachedAt < PROVIDER_RECENT_REQUESTS_STALE_TIME_MS;
|
||||
|
||||
if (!loadOptions.force && hasFreshCache) {
|
||||
setUsageByProvider(cachedUsageByProvider);
|
||||
return cachedUsageByProvider;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const nextUsage = await fetchProviderRecentRequests();
|
||||
setUsageByProvider(nextUsage);
|
||||
return nextUsage;
|
||||
} catch {
|
||||
if (cachedAt > 0) {
|
||||
setUsageByProvider(cachedUsageByProvider);
|
||||
}
|
||||
return cachedUsageByProvider;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[enabled]
|
||||
);
|
||||
|
||||
const refreshRecentRequests = useCallback(
|
||||
async () => loadRecentRequests({ force: true }),
|
||||
[loadRecentRequests]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setUsageByProvider(enabled ? cachedUsageByProvider : EMPTY_USAGE_BY_PROVIDER);
|
||||
}, [enabled]);
|
||||
|
||||
useInterval(() => {
|
||||
void refreshRecentRequests().catch(() => {});
|
||||
}, enabled ? PROVIDER_RECENT_REQUESTS_STALE_TIME_MS : null);
|
||||
|
||||
return {
|
||||
usageByProvider: enabled ? usageByProvider : EMPTY_USAGE_BY_PROVIDER,
|
||||
isLoading: enabled ? isLoading : false,
|
||||
loadRecentRequests,
|
||||
refreshRecentRequests,
|
||||
};
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useInterval } from '@/hooks/useInterval';
|
||||
import { USAGE_STATS_STALE_TIME_MS, useUsageStatsStore } from '@/stores';
|
||||
import type { KeyStats, UsageDetail } from '@/utils/usage';
|
||||
|
||||
const EMPTY_KEY_STATS: KeyStats = { bySource: {}, byAuthIndex: {} };
|
||||
const EMPTY_USAGE_DETAILS: UsageDetail[] = [];
|
||||
|
||||
export type UseProviderStatsOptions = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export const useProviderStats = (options: UseProviderStatsOptions = {}) => {
|
||||
const enabled = options.enabled ?? true;
|
||||
const keyStats = useUsageStatsStore((state) => (enabled ? state.keyStats : EMPTY_KEY_STATS));
|
||||
const usageDetails = useUsageStatsStore((state) =>
|
||||
enabled ? state.usageDetails : EMPTY_USAGE_DETAILS
|
||||
);
|
||||
const isLoading = useUsageStatsStore((state) => (enabled ? state.loading : false));
|
||||
const loadUsageStats = useUsageStatsStore((state) => state.loadUsageStats);
|
||||
|
||||
// 首次进入页面优先复用缓存,避免跨页面重复拉取 /usage。
|
||||
const loadKeyStats = useCallback(async () => {
|
||||
await loadUsageStats({ staleTimeMs: USAGE_STATS_STALE_TIME_MS });
|
||||
}, [loadUsageStats]);
|
||||
|
||||
// 定时器触发时强制刷新共享 usage。
|
||||
const refreshKeyStats = useCallback(async () => {
|
||||
await loadUsageStats({ force: true, staleTimeMs: USAGE_STATS_STALE_TIME_MS });
|
||||
}, [loadUsageStats]);
|
||||
|
||||
useInterval(() => {
|
||||
void refreshKeyStats().catch(() => {});
|
||||
}, enabled ? 240_000 : null);
|
||||
|
||||
return { keyStats, usageDetails, loadKeyStats, refreshKeyStats, isLoading };
|
||||
};
|
||||
@@ -7,6 +7,6 @@ export { VertexSection } from './VertexSection';
|
||||
export { ProviderList } from './ProviderList';
|
||||
export { ProviderStatusBar } from './ProviderStatusBar';
|
||||
export { ProviderNav } from './ProviderNav';
|
||||
export * from './hooks/useProviderStats';
|
||||
export * from './hooks/useProviderRecentRequests';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ApiKeyEntry, GeminiKeyConfig, ProviderKeyConfig } from '@/types';
|
||||
import type { HeaderEntry } from '@/utils/headers';
|
||||
import type { KeyStats, UsageDetail } from '@/utils/usage';
|
||||
|
||||
export interface ModelEntry {
|
||||
name: string;
|
||||
@@ -48,14 +47,3 @@ export type VertexFormState = Omit<ProviderKeyConfig, 'headers'> & {
|
||||
modelEntries: ModelEntry[];
|
||||
excludedText: string;
|
||||
};
|
||||
|
||||
export interface ProviderSectionProps<TConfig> {
|
||||
configs: TConfig[];
|
||||
keyStats: KeyStats;
|
||||
usageDetails: UsageDetail[];
|
||||
disabled: boolean;
|
||||
onEdit: (index: number) => void;
|
||||
onAdd: () => void;
|
||||
onDelete: (index: number) => void;
|
||||
onToggle?: (index: number, enabled: boolean) => void;
|
||||
}
|
||||
|
||||
@@ -6,18 +6,14 @@ import type {
|
||||
OpenAIProviderConfig,
|
||||
} from '@/types';
|
||||
import {
|
||||
buildCandidateUsageSourceIds,
|
||||
normalizeAuthIndex,
|
||||
type KeyStatBucket,
|
||||
type KeyStats,
|
||||
type UsageDetail,
|
||||
} from '@/utils/usage';
|
||||
import {
|
||||
collectUsageDetailsForAuthIndices,
|
||||
collectUsageDetailsForCandidates,
|
||||
type UsageDetailsByAuthIndex,
|
||||
type UsageDetailsBySource,
|
||||
} from '@/utils/usageIndex';
|
||||
buildRecentRequestCompositeKey,
|
||||
mergeRecentRequestBucketGroups,
|
||||
normalizeRecentRequestAuthIndex,
|
||||
statusBarDataFromRecentRequests,
|
||||
sumRecentRequests,
|
||||
type RecentRequestBucket,
|
||||
type StatusBarData,
|
||||
} from '@/utils/recentRequests';
|
||||
import type { AmpcodeFormState, AmpcodeUpstreamApiKeyEntry, ModelEntry } from './types';
|
||||
|
||||
export const DISABLE_ALL_MODELS_RULE = '*';
|
||||
@@ -103,161 +99,76 @@ export const buildClaudeMessagesEndpoint = (baseUrl: string): string => {
|
||||
return `${trimmed}/v1/messages`;
|
||||
};
|
||||
|
||||
// 根据 source (apiKey) 获取统计数据 - 与旧版逻辑一致
|
||||
export const getStatsBySource = (
|
||||
apiKey: string,
|
||||
keyStats: KeyStats,
|
||||
prefix?: string
|
||||
): KeyStatBucket => {
|
||||
const bySource = keyStats.bySource ?? {};
|
||||
const candidates = buildCandidateUsageSourceIds({ apiKey, prefix });
|
||||
if (!candidates.length) {
|
||||
return { success: 0, failure: 0 };
|
||||
}
|
||||
export type ProviderRecentUsageMap = Map<string, Map<string, RecentRequestBucket[]>>;
|
||||
|
||||
let success = 0;
|
||||
let failure = 0;
|
||||
candidates.forEach((candidate) => {
|
||||
const stats = bySource[candidate];
|
||||
if (!stats) return;
|
||||
success += stats.success;
|
||||
failure += stats.failure;
|
||||
});
|
||||
const normalizeProviderRecentKey = (value: unknown): string =>
|
||||
String(value ?? '').trim().toLowerCase();
|
||||
|
||||
return { success, failure };
|
||||
};
|
||||
|
||||
type UsageIdentity = {
|
||||
authIndex?: unknown;
|
||||
apiKey?: string;
|
||||
prefix?: string;
|
||||
};
|
||||
|
||||
export const getStatsForIdentity = (
|
||||
identity: UsageIdentity,
|
||||
keyStats: KeyStats
|
||||
): KeyStatBucket => {
|
||||
const authIndexKey = normalizeAuthIndex(identity.authIndex);
|
||||
if (authIndexKey) {
|
||||
const stats = keyStats.byAuthIndex?.[authIndexKey];
|
||||
if (stats) {
|
||||
return { success: stats.success, failure: stats.failure };
|
||||
}
|
||||
}
|
||||
|
||||
return getStatsBySource(identity.apiKey ?? '', keyStats, identity.prefix);
|
||||
};
|
||||
|
||||
export const collectUsageDetailsForIdentity = (
|
||||
identity: UsageIdentity,
|
||||
usageDetailsBySource: UsageDetailsBySource,
|
||||
usageDetailsByAuthIndex: UsageDetailsByAuthIndex
|
||||
): UsageDetail[] => {
|
||||
const authIndexKey = normalizeAuthIndex(identity.authIndex);
|
||||
if (authIndexKey) {
|
||||
const details = collectUsageDetailsForAuthIndices(usageDetailsByAuthIndex, [authIndexKey]);
|
||||
if (details.length > 0) {
|
||||
return details;
|
||||
}
|
||||
}
|
||||
|
||||
const candidates = buildCandidateUsageSourceIds({
|
||||
apiKey: identity.apiKey,
|
||||
prefix: identity.prefix,
|
||||
});
|
||||
if (!candidates.length) {
|
||||
export function getProviderRecentBuckets(
|
||||
usageByProvider: ProviderRecentUsageMap,
|
||||
provider: string,
|
||||
apiKey?: string,
|
||||
baseUrl?: string
|
||||
): RecentRequestBucket[] {
|
||||
if (!String(apiKey ?? '').trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return collectUsageDetailsForCandidates(usageDetailsBySource, candidates);
|
||||
};
|
||||
const providerKey = normalizeProviderRecentKey(provider);
|
||||
const compositeKey = buildRecentRequestCompositeKey(baseUrl, apiKey);
|
||||
return usageByProvider.get(providerKey)?.get(compositeKey) ?? [];
|
||||
}
|
||||
|
||||
const mergeUsageDetails = (groups: UsageDetail[][]): UsageDetail[] => {
|
||||
let firstDetails: UsageDetail[] | null = null;
|
||||
let merged: UsageDetail[] | null = null;
|
||||
export function getProviderRecentStats(
|
||||
usageByProvider: ProviderRecentUsageMap,
|
||||
provider: string,
|
||||
apiKey?: string,
|
||||
baseUrl?: string
|
||||
): { success: number; failure: number } {
|
||||
return sumRecentRequests(getProviderRecentBuckets(usageByProvider, provider, apiKey, baseUrl));
|
||||
}
|
||||
|
||||
groups.forEach((details) => {
|
||||
if (!details.length) return;
|
||||
if (!firstDetails) {
|
||||
firstDetails = details;
|
||||
return;
|
||||
}
|
||||
if (!merged) {
|
||||
merged = [...firstDetails];
|
||||
}
|
||||
merged.push(...details);
|
||||
});
|
||||
export function getProviderRecentStatusData(
|
||||
usageByProvider: ProviderRecentUsageMap,
|
||||
provider: string,
|
||||
apiKey?: string,
|
||||
baseUrl?: string
|
||||
): StatusBarData {
|
||||
return statusBarDataFromRecentRequests(
|
||||
getProviderRecentBuckets(usageByProvider, provider, apiKey, baseUrl)
|
||||
);
|
||||
}
|
||||
|
||||
return merged ?? firstDetails ?? [];
|
||||
};
|
||||
|
||||
// 对于 OpenAI 提供商,汇总所有 apiKeyEntries 的统计 - 与旧版逻辑一致
|
||||
export const getOpenAIProviderStats = (
|
||||
export function collectOpenAIProviderRecentBuckets(
|
||||
provider: OpenAIProviderConfig,
|
||||
keyStats: KeyStats
|
||||
): KeyStatBucket => {
|
||||
let success = 0;
|
||||
let failure = 0;
|
||||
|
||||
usageByProvider: ProviderRecentUsageMap
|
||||
): RecentRequestBucket[] {
|
||||
if (!provider.apiKeyEntries?.length) {
|
||||
const stats = getStatsForIdentity(
|
||||
{ authIndex: provider.authIndex, prefix: provider.prefix },
|
||||
keyStats
|
||||
);
|
||||
return { success: stats.success, failure: stats.failure };
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!normalizeAuthIndex(provider.authIndex) && provider.prefix) {
|
||||
const prefixStats = getStatsBySource('', keyStats, provider.prefix);
|
||||
success += prefixStats.success;
|
||||
failure += prefixStats.failure;
|
||||
}
|
||||
const groups = provider.apiKeyEntries.map((entry) =>
|
||||
getProviderRecentBuckets(usageByProvider, provider.name, entry.apiKey, provider.baseUrl)
|
||||
);
|
||||
|
||||
provider.apiKeyEntries.forEach((entry) => {
|
||||
const stats = getStatsForIdentity({ authIndex: entry.authIndex, apiKey: entry.apiKey }, keyStats);
|
||||
success += stats.success;
|
||||
failure += stats.failure;
|
||||
});
|
||||
return mergeRecentRequestBucketGroups(groups);
|
||||
}
|
||||
|
||||
return { success, failure };
|
||||
};
|
||||
|
||||
export const collectOpenAIProviderUsageDetails = (
|
||||
export function getOpenAIProviderRecentStats(
|
||||
provider: OpenAIProviderConfig,
|
||||
usageDetailsBySource: UsageDetailsBySource,
|
||||
usageDetailsByAuthIndex: UsageDetailsByAuthIndex
|
||||
): UsageDetail[] => {
|
||||
if (!provider.apiKeyEntries?.length) {
|
||||
return collectUsageDetailsForIdentity(
|
||||
{ authIndex: provider.authIndex, prefix: provider.prefix },
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex
|
||||
);
|
||||
}
|
||||
usageByProvider: ProviderRecentUsageMap
|
||||
): { success: number; failure: number } {
|
||||
return sumRecentRequests(collectOpenAIProviderRecentBuckets(provider, usageByProvider));
|
||||
}
|
||||
|
||||
const groups: UsageDetail[][] = [];
|
||||
if (!normalizeAuthIndex(provider.authIndex) && provider.prefix) {
|
||||
groups.push(
|
||||
collectUsageDetailsForIdentity(
|
||||
{ prefix: provider.prefix },
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
provider.apiKeyEntries.forEach((entry) => {
|
||||
groups.push(
|
||||
collectUsageDetailsForIdentity(
|
||||
{ authIndex: entry.authIndex, apiKey: entry.apiKey },
|
||||
usageDetailsBySource,
|
||||
usageDetailsByAuthIndex
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return mergeUsageDetails(groups);
|
||||
};
|
||||
export function getOpenAIProviderRecentStatusData(
|
||||
provider: OpenAIProviderConfig,
|
||||
usageByProvider: ProviderRecentUsageMap
|
||||
): StatusBarData {
|
||||
return statusBarDataFromRecentRequests(
|
||||
collectOpenAIProviderRecentBuckets(provider, usageByProvider)
|
||||
);
|
||||
}
|
||||
|
||||
export const getProviderConfigKey = (
|
||||
config: {
|
||||
@@ -268,7 +179,7 @@ export const getProviderConfigKey = (
|
||||
},
|
||||
index: number
|
||||
): string => {
|
||||
const authIndexKey = normalizeAuthIndex(config.authIndex);
|
||||
const authIndexKey = normalizeRecentRequestAuthIndex(config.authIndex);
|
||||
if (authIndexKey) {
|
||||
return authIndexKey;
|
||||
}
|
||||
@@ -276,7 +187,7 @@ export const getProviderConfigKey = (
|
||||
};
|
||||
|
||||
export const getOpenAIProviderKey = (provider: OpenAIProviderConfig, index: number): string => {
|
||||
const authIndexKey = normalizeAuthIndex(provider.authIndex);
|
||||
const authIndexKey = normalizeRecentRequestAuthIndex(provider.authIndex);
|
||||
if (authIndexKey) {
|
||||
return authIndexKey;
|
||||
}
|
||||
@@ -284,7 +195,7 @@ export const getOpenAIProviderKey = (provider: OpenAIProviderConfig, index: numb
|
||||
};
|
||||
|
||||
export const getOpenAIEntryKey = (entry: ApiKeyEntry, index: number): string => {
|
||||
const authIndexKey = normalizeAuthIndex(entry.authIndex);
|
||||
const authIndexKey = normalizeRecentRequestAuthIndex(entry.authIndex);
|
||||
if (authIndexKey) {
|
||||
return authIndexKey;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,12 @@ import {
|
||||
import { ProviderStatusBar } from '@/components/providers/ProviderStatusBar';
|
||||
import type { AuthFileItem } from '@/types';
|
||||
import { resolveAuthProvider } from '@/utils/quota';
|
||||
import { calculateStatusBarData, normalizeAuthIndex, type KeyStats } from '@/utils/usage';
|
||||
import {
|
||||
normalizeRecentRequestAuthIndex,
|
||||
normalizeRecentRequestBuckets,
|
||||
statusBarDataFromRecentRequests,
|
||||
sumRecentRequests,
|
||||
} from '@/utils/recentRequests';
|
||||
import { formatFileSize } from '@/utils/format';
|
||||
import {
|
||||
QUOTA_PROVIDER_TYPES,
|
||||
@@ -24,7 +29,6 @@ import {
|
||||
getTypeLabel,
|
||||
isRuntimeOnlyAuthFile,
|
||||
parsePriorityValue,
|
||||
resolveAuthFileStats,
|
||||
type QuotaProviderType,
|
||||
type ResolvedTheme,
|
||||
} from '@/features/authFiles/constants';
|
||||
@@ -43,7 +47,6 @@ export type AuthFileCardProps = {
|
||||
deleting: string | null;
|
||||
statusUpdating: Record<string, boolean>;
|
||||
quotaFilterType: QuotaProviderType | null;
|
||||
keyStats: KeyStats;
|
||||
statusBarCache: Map<string, AuthFileStatusBarData>;
|
||||
onShowModels: (file: AuthFileItem) => void;
|
||||
onDownload: (name: string) => void;
|
||||
@@ -70,7 +73,6 @@ export function AuthFileCard(props: AuthFileCardProps) {
|
||||
deleting,
|
||||
statusUpdating,
|
||||
quotaFilterType,
|
||||
keyStats,
|
||||
statusBarCache,
|
||||
onShowModels,
|
||||
onDownload,
|
||||
@@ -80,7 +82,8 @@ export function AuthFileCard(props: AuthFileCardProps) {
|
||||
onToggleSelect,
|
||||
} = props;
|
||||
|
||||
const fileStats = resolveAuthFileStats(file, keyStats);
|
||||
const recentBuckets = normalizeRecentRequestBuckets(file.recent_requests ?? file.recentRequests);
|
||||
const fileStats = sumRecentRequests(recentBuckets);
|
||||
const isRuntimeOnly = isRuntimeOnlyAuthFile(file);
|
||||
const isAistudio = (file.type || '').toLowerCase() === 'aistudio';
|
||||
const showModelsButton = !isRuntimeOnly || isAistudio;
|
||||
@@ -107,9 +110,10 @@ export function AuthFileCard(props: AuthFileCardProps) {
|
||||
: '';
|
||||
|
||||
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
|
||||
const authIndexKey = normalizeAuthIndex(rawAuthIndex);
|
||||
const authIndexKey = normalizeRecentRequestAuthIndex(rawAuthIndex);
|
||||
const statusData =
|
||||
(authIndexKey && statusBarCache.get(authIndexKey)) || calculateStatusBarData([]);
|
||||
(authIndexKey && statusBarCache.get(authIndexKey)) ||
|
||||
statusBarDataFromRecentRequests(recentBuckets);
|
||||
const rawStatusMessage = getAuthFileStatusMessage(file);
|
||||
const hasStatusWarning =
|
||||
Boolean(rawStatusMessage) && !HEALTHY_STATUS_MESSAGES.has(rawStatusMessage.toLowerCase());
|
||||
|
||||
@@ -10,12 +10,6 @@ import iconQwen from '@/assets/icons/qwen.svg';
|
||||
import iconVertex from '@/assets/icons/vertex.svg';
|
||||
import type { AuthFileItem } from '@/types';
|
||||
import { parseTimestamp } from '@/utils/timestamp';
|
||||
import {
|
||||
normalizeAuthIndex,
|
||||
normalizeUsageSourceId,
|
||||
type KeyStatBucket,
|
||||
type KeyStats,
|
||||
} from '@/utils/usage';
|
||||
|
||||
export type ThemeColors = { bg: string; text: string; border?: string };
|
||||
export type TypeColorSet = { light: ThemeColors; dark?: ThemeColors };
|
||||
@@ -233,46 +227,6 @@ export function isRuntimeOnlyAuthFile(file: AuthFileItem): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function resolveAuthFileStats(file: AuthFileItem, stats: KeyStats): KeyStatBucket {
|
||||
const defaultStats: KeyStatBucket = { success: 0, failure: 0 };
|
||||
const rawFileName = file?.name || '';
|
||||
|
||||
// 兼容 auth_index 和 authIndex 两种字段名(API 返回的是 auth_index)
|
||||
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
|
||||
const authIndexKey = normalizeAuthIndex(rawAuthIndex);
|
||||
|
||||
// 尝试根据 authIndex 匹配
|
||||
if (authIndexKey && stats.byAuthIndex?.[authIndexKey]) {
|
||||
return stats.byAuthIndex[authIndexKey];
|
||||
}
|
||||
|
||||
// 尝试根据 source (文件名) 匹配
|
||||
const fileNameId = rawFileName ? normalizeUsageSourceId(rawFileName) : '';
|
||||
if (fileNameId && stats.bySource?.[fileNameId]) {
|
||||
const fromName = stats.bySource[fileNameId];
|
||||
if (fromName.success > 0 || fromName.failure > 0) {
|
||||
return fromName;
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试去掉扩展名后匹配
|
||||
if (rawFileName) {
|
||||
const nameWithoutExt = rawFileName.replace(/\.[^/.]+$/, '');
|
||||
if (nameWithoutExt && nameWithoutExt !== rawFileName) {
|
||||
const nameWithoutExtId = normalizeUsageSourceId(nameWithoutExt);
|
||||
const fromNameWithoutExt = nameWithoutExtId ? stats.bySource?.[nameWithoutExtId] : undefined;
|
||||
if (
|
||||
fromNameWithoutExt &&
|
||||
(fromNameWithoutExt.success > 0 || fromNameWithoutExt.failure > 0)
|
||||
) {
|
||||
return fromNameWithoutExt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return defaultStats;
|
||||
}
|
||||
|
||||
export const formatModified = (item: AuthFileItem): string => {
|
||||
const raw = item['modtime'] ?? item.modified;
|
||||
if (!raw) return '-';
|
||||
|
||||
@@ -50,12 +50,7 @@ export type UseAuthFilesDataResult = {
|
||||
batchDelete: (names: string[]) => void;
|
||||
};
|
||||
|
||||
export type UseAuthFilesDataOptions = {
|
||||
refreshKeyStats: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFilesDataResult {
|
||||
const { refreshKeyStats } = options;
|
||||
export function useAuthFilesData(): UseAuthFilesDataResult {
|
||||
const { t } = useTranslation();
|
||||
const { showNotification, showConfirmation } = useNotificationStore();
|
||||
|
||||
@@ -230,7 +225,6 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles
|
||||
result.failed.length ? 'warning' : 'success'
|
||||
);
|
||||
await loadFiles();
|
||||
await refreshKeyStats();
|
||||
}
|
||||
|
||||
if (result.failed.length > 0) {
|
||||
@@ -247,7 +241,7 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles
|
||||
event.target.value = '';
|
||||
}
|
||||
},
|
||||
[loadFiles, refreshKeyStats, showNotification, t]
|
||||
[loadFiles, showNotification, t]
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
|
||||
@@ -58,7 +58,6 @@ export type PrefixProxyEditorState = {
|
||||
export type UseAuthFilesPrefixProxyEditorOptions = {
|
||||
disableControls: boolean;
|
||||
loadFiles: () => Promise<void>;
|
||||
loadKeyStats: () => Promise<void>;
|
||||
};
|
||||
|
||||
export type UseAuthFilesPrefixProxyEditorResult = {
|
||||
@@ -172,7 +171,7 @@ const buildPrefixProxyUpdatedText = (
|
||||
export function useAuthFilesPrefixProxyEditor(
|
||||
options: UseAuthFilesPrefixProxyEditorOptions
|
||||
): UseAuthFilesPrefixProxyEditorResult {
|
||||
const { disableControls, loadFiles, loadKeyStats } = options;
|
||||
const { disableControls, loadFiles } = options;
|
||||
const { t } = useTranslation();
|
||||
const showNotification = useNotificationStore((state) => state.showNotification);
|
||||
|
||||
@@ -383,7 +382,6 @@ export function useAuthFilesPrefixProxyEditor(
|
||||
await authFilesApi.saveText(name, payload);
|
||||
showNotification(t('auth_files.prefix_proxy_saved_success', { name }), 'success');
|
||||
await loadFiles();
|
||||
await loadKeyStats();
|
||||
setPrefixProxyEditor(null);
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : '';
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { USAGE_STATS_STALE_TIME_MS, useUsageStatsStore } from '@/stores';
|
||||
import type { KeyStats, UsageDetail } from '@/utils/usage';
|
||||
|
||||
export type UseAuthFilesStatsResult = {
|
||||
keyStats: KeyStats;
|
||||
usageDetails: UsageDetail[];
|
||||
loadKeyStats: () => Promise<void>;
|
||||
refreshKeyStats: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function useAuthFilesStats(): UseAuthFilesStatsResult {
|
||||
const keyStats = useUsageStatsStore((state) => state.keyStats);
|
||||
const usageDetails = useUsageStatsStore((state) => state.usageDetails);
|
||||
const loadUsageStats = useUsageStatsStore((state) => state.loadUsageStats);
|
||||
|
||||
const loadKeyStats = useCallback(async () => {
|
||||
await loadUsageStats({ staleTimeMs: USAGE_STATS_STALE_TIME_MS });
|
||||
}, [loadUsageStats]);
|
||||
|
||||
const refreshKeyStats = useCallback(async () => {
|
||||
await loadUsageStats({ force: true, staleTimeMs: USAGE_STATS_STALE_TIME_MS });
|
||||
}, [loadUsageStats]);
|
||||
|
||||
return { keyStats, usageDetails, loadKeyStats, refreshKeyStats };
|
||||
}
|
||||
@@ -1,41 +1,30 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { AuthFileItem } from '@/types';
|
||||
import { calculateStatusBarData, normalizeAuthIndex, type UsageDetail } from '@/utils/usage';
|
||||
import {
|
||||
normalizeRecentRequestAuthIndex,
|
||||
normalizeRecentRequestBuckets,
|
||||
statusBarDataFromRecentRequests,
|
||||
} from '@/utils/recentRequests';
|
||||
|
||||
export type AuthFileStatusBarData = ReturnType<typeof calculateStatusBarData>;
|
||||
export type AuthFileStatusBarData = ReturnType<typeof statusBarDataFromRecentRequests>;
|
||||
|
||||
export function useAuthFilesStatusBarCache(files: AuthFileItem[], usageDetails: UsageDetail[]) {
|
||||
export function useAuthFilesStatusBarCache(files: AuthFileItem[]) {
|
||||
return useMemo(() => {
|
||||
const cache = new Map<string, AuthFileStatusBarData>();
|
||||
|
||||
const usageDetailsByAuthIndex = new Map<string, UsageDetail[]>();
|
||||
usageDetails.forEach((detail) => {
|
||||
const authIndexKey = normalizeAuthIndex(detail.auth_index);
|
||||
if (!authIndexKey) return;
|
||||
|
||||
const list = usageDetailsByAuthIndex.get(authIndexKey);
|
||||
if (list) {
|
||||
list.push(detail);
|
||||
} else {
|
||||
usageDetailsByAuthIndex.set(authIndexKey, [detail]);
|
||||
}
|
||||
});
|
||||
|
||||
const uniqueAuthIndexKeys = new Set<string>();
|
||||
files.forEach((file) => {
|
||||
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
|
||||
const authIndexKey = normalizeAuthIndex(rawAuthIndex);
|
||||
const authIndexKey = normalizeRecentRequestAuthIndex(rawAuthIndex);
|
||||
if (!authIndexKey) return;
|
||||
uniqueAuthIndexKeys.add(authIndexKey);
|
||||
});
|
||||
|
||||
uniqueAuthIndexKeys.forEach((authIndexKey) => {
|
||||
cache.set(
|
||||
authIndexKey,
|
||||
calculateStatusBarData(usageDetailsByAuthIndex.get(authIndexKey) ?? [])
|
||||
statusBarDataFromRecentRequests(
|
||||
normalizeRecentRequestBuckets(file.recent_requests ?? file.recentRequests)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return cache;
|
||||
}, [files, usageDetails]);
|
||||
}, [files]);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
OpenAISection,
|
||||
VertexSection,
|
||||
ProviderNav,
|
||||
useProviderStats,
|
||||
useProviderRecentRequests,
|
||||
} from '@/components/providers';
|
||||
import {
|
||||
withDisableAllModelsRule,
|
||||
@@ -20,7 +20,6 @@ import { useHeaderRefresh } from '@/hooks/useHeaderRefresh';
|
||||
import { ampcodeApi, providersApi } from '@/services/api';
|
||||
import { useAuthStore, useConfigStore, useNotificationStore, useThemeStore } from '@/stores';
|
||||
import type { GeminiKeyConfig, OpenAIProviderConfig, ProviderKeyConfig } from '@/types';
|
||||
import { indexUsageDetailsByAuthIndex, indexUsageDetailsBySource } from '@/utils/usageIndex';
|
||||
import styles from './AiProvidersPage.module.scss';
|
||||
|
||||
export function AiProvidersPage() {
|
||||
@@ -64,17 +63,9 @@ export function AiProvidersPage() {
|
||||
const pageTransitionLayer = usePageTransitionLayer();
|
||||
const isCurrentLayer = pageTransitionLayer ? pageTransitionLayer.status === 'current' : true;
|
||||
|
||||
const { keyStats, usageDetails, loadKeyStats, refreshKeyStats } = useProviderStats({
|
||||
const { usageByProvider, loadRecentRequests, refreshRecentRequests } = useProviderRecentRequests({
|
||||
enabled: isCurrentLayer,
|
||||
});
|
||||
const usageDetailsBySource = useMemo(
|
||||
() => indexUsageDetailsBySource(usageDetails),
|
||||
[usageDetails]
|
||||
);
|
||||
const usageDetailsByAuthIndex = useMemo(
|
||||
() => indexUsageDetailsByAuthIndex(usageDetails),
|
||||
[usageDetails]
|
||||
);
|
||||
|
||||
const getErrorMessage = (err: unknown) => {
|
||||
if (err instanceof Error) return err.message;
|
||||
@@ -139,8 +130,8 @@ export function AiProvidersPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCurrentLayer) return;
|
||||
void loadKeyStats().catch(() => {});
|
||||
}, [isCurrentLayer, loadKeyStats]);
|
||||
void loadRecentRequests().catch(() => {});
|
||||
}, [isCurrentLayer, loadRecentRequests]);
|
||||
|
||||
useEffect(() => {
|
||||
if (config?.geminiApiKeys) setGeminiKeys(config.geminiApiKeys);
|
||||
@@ -156,7 +147,11 @@ export function AiProvidersPage() {
|
||||
config?.openaiCompatibility,
|
||||
]);
|
||||
|
||||
useHeaderRefresh(refreshKeyStats, isCurrentLayer);
|
||||
const handleRecentRequestsRefresh = useCallback(async () => {
|
||||
await refreshRecentRequests();
|
||||
}, [refreshRecentRequests]);
|
||||
|
||||
useHeaderRefresh(handleRecentRequestsRefresh, isCurrentLayer);
|
||||
|
||||
const openEditor = useCallback(
|
||||
(path: string) => {
|
||||
@@ -419,9 +414,7 @@ export function AiProvidersPage() {
|
||||
<div id="provider-gemini">
|
||||
<GeminiSection
|
||||
configs={geminiKeys}
|
||||
keyStats={keyStats}
|
||||
usageDetailsBySource={usageDetailsBySource}
|
||||
usageDetailsByAuthIndex={usageDetailsByAuthIndex}
|
||||
usageByProvider={usageByProvider}
|
||||
loading={loading}
|
||||
disableControls={disableControls}
|
||||
isSwitching={isSwitching}
|
||||
@@ -435,9 +428,7 @@ export function AiProvidersPage() {
|
||||
<div id="provider-codex">
|
||||
<CodexSection
|
||||
configs={codexConfigs}
|
||||
keyStats={keyStats}
|
||||
usageDetailsBySource={usageDetailsBySource}
|
||||
usageDetailsByAuthIndex={usageDetailsByAuthIndex}
|
||||
usageByProvider={usageByProvider}
|
||||
loading={loading}
|
||||
disableControls={disableControls}
|
||||
isSwitching={isSwitching}
|
||||
@@ -451,9 +442,7 @@ export function AiProvidersPage() {
|
||||
<div id="provider-claude">
|
||||
<ClaudeSection
|
||||
configs={claudeConfigs}
|
||||
keyStats={keyStats}
|
||||
usageDetailsBySource={usageDetailsBySource}
|
||||
usageDetailsByAuthIndex={usageDetailsByAuthIndex}
|
||||
usageByProvider={usageByProvider}
|
||||
loading={loading}
|
||||
disableControls={disableControls}
|
||||
isSwitching={isSwitching}
|
||||
@@ -467,9 +456,7 @@ export function AiProvidersPage() {
|
||||
<div id="provider-vertex">
|
||||
<VertexSection
|
||||
configs={vertexConfigs}
|
||||
keyStats={keyStats}
|
||||
usageDetailsBySource={usageDetailsBySource}
|
||||
usageDetailsByAuthIndex={usageDetailsByAuthIndex}
|
||||
usageByProvider={usageByProvider}
|
||||
loading={loading}
|
||||
disableControls={disableControls}
|
||||
isSwitching={isSwitching}
|
||||
@@ -493,9 +480,7 @@ export function AiProvidersPage() {
|
||||
<div id="provider-openai">
|
||||
<OpenAISection
|
||||
configs={openaiProviders}
|
||||
keyStats={keyStats}
|
||||
usageDetailsBySource={usageDetailsBySource}
|
||||
usageDetailsByAuthIndex={usageDetailsByAuthIndex}
|
||||
usageByProvider={usageByProvider}
|
||||
loading={loading}
|
||||
disableControls={disableControls}
|
||||
isSwitching={isSwitching}
|
||||
|
||||
@@ -48,7 +48,6 @@ import { useAuthFilesData } from '@/features/authFiles/hooks/useAuthFilesData';
|
||||
import { useAuthFilesModels } from '@/features/authFiles/hooks/useAuthFilesModels';
|
||||
import { useAuthFilesOauth } from '@/features/authFiles/hooks/useAuthFilesOauth';
|
||||
import { useAuthFilesPrefixProxyEditor } from '@/features/authFiles/hooks/useAuthFilesPrefixProxyEditor';
|
||||
import { useAuthFilesStats } from '@/features/authFiles/hooks/useAuthFilesStats';
|
||||
import { useAuthFilesStatusBarCache } from '@/features/authFiles/hooks/useAuthFilesStatusBarCache';
|
||||
import {
|
||||
isAuthFilesSortMode,
|
||||
@@ -106,7 +105,6 @@ export function AuthFilesPage() {
|
||||
const previousSelectionCountRef = useRef(0);
|
||||
const selectionCountRef = useRef(0);
|
||||
|
||||
const { keyStats, usageDetails, loadKeyStats, refreshKeyStats } = useAuthFilesStats();
|
||||
const {
|
||||
files,
|
||||
selectedFiles,
|
||||
@@ -133,9 +131,9 @@ export function AuthFilesPage() {
|
||||
batchDownload,
|
||||
batchSetStatus,
|
||||
batchDelete,
|
||||
} = useAuthFilesData({ refreshKeyStats });
|
||||
} = useAuthFilesData();
|
||||
|
||||
const statusBarCache = useAuthFilesStatusBarCache(files, usageDetails);
|
||||
const statusBarCache = useAuthFilesStatusBarCache(files);
|
||||
|
||||
const {
|
||||
excluded,
|
||||
@@ -176,7 +174,6 @@ export function AuthFilesPage() {
|
||||
} = useAuthFilesPrefixProxyEditor({
|
||||
disableControls: connectionStatus !== 'connected',
|
||||
loadFiles,
|
||||
loadKeyStats: refreshKeyStats,
|
||||
});
|
||||
|
||||
const disableControls = connectionStatus !== 'connected';
|
||||
@@ -330,22 +327,21 @@ export function AuthFilesPage() {
|
||||
);
|
||||
|
||||
const handleHeaderRefresh = useCallback(async () => {
|
||||
await Promise.all([loadFiles(), refreshKeyStats(), loadExcluded(), loadModelAlias()]);
|
||||
}, [loadFiles, refreshKeyStats, loadExcluded, loadModelAlias]);
|
||||
await Promise.all([loadFiles(), loadExcluded(), loadModelAlias()]);
|
||||
}, [loadFiles, loadExcluded, loadModelAlias]);
|
||||
|
||||
useHeaderRefresh(handleHeaderRefresh);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCurrentLayer) return;
|
||||
loadFiles();
|
||||
void loadKeyStats().catch(() => {});
|
||||
loadExcluded();
|
||||
loadModelAlias();
|
||||
}, [isCurrentLayer, loadFiles, loadKeyStats, loadExcluded, loadModelAlias]);
|
||||
}, [isCurrentLayer, loadFiles, loadExcluded, loadModelAlias]);
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
void refreshKeyStats().catch(() => {});
|
||||
void loadFiles().catch(() => {});
|
||||
},
|
||||
isCurrentLayer ? 240_000 : null
|
||||
);
|
||||
@@ -830,7 +826,6 @@ export function AuthFilesPage() {
|
||||
deleting={deleting}
|
||||
statusUpdating={statusUpdating}
|
||||
quotaFilterType={quotaFilterType}
|
||||
keyStats={keyStats}
|
||||
statusBarCache={statusBarCache}
|
||||
onShowModels={showModels}
|
||||
onDownload={handleDownload}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { apiClient } from './client';
|
||||
import type { ApiKeyUsageResponse } from '@/utils/recentRequests';
|
||||
|
||||
const API_KEY_USAGE_TIMEOUT_MS = 15 * 1000;
|
||||
|
||||
export const apiKeyUsageApi = {
|
||||
getUsage: () =>
|
||||
apiClient.get<ApiKeyUsageResponse>('/api-key-usage', {
|
||||
timeout: API_KEY_USAGE_TIMEOUT_MS,
|
||||
}),
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './client';
|
||||
export * from './apiCall';
|
||||
export * from './apiKeyUsage';
|
||||
export * from './config';
|
||||
export * from './configFile';
|
||||
export * from './apiKeys';
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* 基于原项目 src/modules/auth-files.js
|
||||
*/
|
||||
|
||||
import type { RecentRequestBucket } from '@/utils/recentRequests';
|
||||
|
||||
export type AuthFileType =
|
||||
| 'qwen'
|
||||
| 'kimi'
|
||||
@@ -30,6 +32,8 @@ export interface AuthFileItem {
|
||||
statusMessage?: string;
|
||||
lastRefresh?: string | number;
|
||||
modified?: number;
|
||||
recent_requests?: RecentRequestBucket[];
|
||||
recentRequests?: RecentRequestBucket[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user