mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-02 19:00:49 +08:00
perf(providers,auth-files): cache status bar data and add auto-refresh
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { Fragment, useCallback, useEffect, useMemo, useState, type ReactNode } from 'react';
|
import { Fragment, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useInterval } from '@/hooks/useInterval';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
@@ -204,6 +205,7 @@ export function AiProvidersPage() {
|
|||||||
const [openaiProviders, setOpenaiProviders] = useState<OpenAIProviderConfig[]>([]);
|
const [openaiProviders, setOpenaiProviders] = useState<OpenAIProviderConfig[]>([]);
|
||||||
const [keyStats, setKeyStats] = useState<KeyStats>({ bySource: {}, byAuthIndex: {} });
|
const [keyStats, setKeyStats] = useState<KeyStats>({ bySource: {}, byAuthIndex: {} });
|
||||||
const [usageDetails, setUsageDetails] = useState<UsageDetail[]>([]);
|
const [usageDetails, setUsageDetails] = useState<UsageDetail[]>([]);
|
||||||
|
const loadingKeyStatsRef = useRef(false);
|
||||||
|
|
||||||
const [modal, setModal] = useState<ProviderModal | null>(null);
|
const [modal, setModal] = useState<ProviderModal | null>(null);
|
||||||
|
|
||||||
@@ -275,8 +277,11 @@ export function AiProvidersPage() {
|
|||||||
[openaiForm.modelEntries]
|
[openaiForm.modelEntries]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 加载 key 统计和 usage 明细
|
// 加载 key 统计和 usage 明细(API 层已有60秒超时)
|
||||||
const loadKeyStats = useCallback(async () => {
|
const loadKeyStats = useCallback(async () => {
|
||||||
|
// 防止重复请求
|
||||||
|
if (loadingKeyStatsRef.current) return;
|
||||||
|
loadingKeyStatsRef.current = true;
|
||||||
try {
|
try {
|
||||||
const usageResponse = await usageApi.getUsage();
|
const usageResponse = await usageApi.getUsage();
|
||||||
const usageData = usageResponse?.usage ?? usageResponse;
|
const usageData = usageResponse?.usage ?? usageResponse;
|
||||||
@@ -287,6 +292,8 @@ export function AiProvidersPage() {
|
|||||||
setUsageDetails(details);
|
setUsageDetails(details);
|
||||||
} catch {
|
} catch {
|
||||||
// 静默失败
|
// 静默失败
|
||||||
|
} finally {
|
||||||
|
loadingKeyStatsRef.current = false;
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -318,6 +325,9 @@ export function AiProvidersPage() {
|
|||||||
loadKeyStats();
|
loadKeyStats();
|
||||||
}, [loadKeyStats]);
|
}, [loadKeyStats]);
|
||||||
|
|
||||||
|
// 定时刷新状态数据(每240秒)
|
||||||
|
useInterval(loadKeyStats, 240_000);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config?.geminiApiKeys) setGeminiKeys(config.geminiApiKeys);
|
if (config?.geminiApiKeys) setGeminiKeys(config.geminiApiKeys);
|
||||||
if (config?.codexApiKeys) setCodexConfigs(config.codexApiKeys);
|
if (config?.codexApiKeys) setCodexConfigs(config.codexApiKeys);
|
||||||
@@ -1097,9 +1107,43 @@ export function AiProvidersPage() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 预计算所有 apiKey 的状态栏数据(避免每次渲染重复计算)
|
||||||
|
const statusBarCache = useMemo(() => {
|
||||||
|
const cache = new Map<string, ReturnType<typeof calculateStatusBarData>>();
|
||||||
|
|
||||||
|
// 收集所有需要计算的 apiKey
|
||||||
|
const allApiKeys = new Set<string>();
|
||||||
|
geminiKeys.forEach((k) => k.apiKey && allApiKeys.add(k.apiKey));
|
||||||
|
codexConfigs.forEach((k) => k.apiKey && allApiKeys.add(k.apiKey));
|
||||||
|
claudeConfigs.forEach((k) => k.apiKey && allApiKeys.add(k.apiKey));
|
||||||
|
openaiProviders.forEach((p) => {
|
||||||
|
(p.apiKeyEntries || []).forEach((e) => e.apiKey && allApiKeys.add(e.apiKey));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 预计算每个 apiKey 的状态数据
|
||||||
|
allApiKeys.forEach((apiKey) => {
|
||||||
|
cache.set(apiKey, calculateStatusBarData(usageDetails, apiKey));
|
||||||
|
});
|
||||||
|
|
||||||
|
return cache;
|
||||||
|
}, [usageDetails, geminiKeys, codexConfigs, claudeConfigs, openaiProviders]);
|
||||||
|
|
||||||
|
// 预计算 OpenAI 提供商的汇总状态栏数据
|
||||||
|
const openaiStatusBarCache = useMemo(() => {
|
||||||
|
const cache = new Map<string, ReturnType<typeof calculateStatusBarData>>();
|
||||||
|
|
||||||
|
openaiProviders.forEach((provider) => {
|
||||||
|
const allKeys = (provider.apiKeyEntries || []).map((e) => e.apiKey).filter(Boolean);
|
||||||
|
const filteredDetails = usageDetails.filter((detail) => allKeys.includes(detail.source));
|
||||||
|
cache.set(provider.name, calculateStatusBarData(filteredDetails));
|
||||||
|
});
|
||||||
|
|
||||||
|
return cache;
|
||||||
|
}, [usageDetails, openaiProviders]);
|
||||||
|
|
||||||
// 渲染状态监测栏
|
// 渲染状态监测栏
|
||||||
const renderStatusBar = (apiKey: string) => {
|
const renderStatusBar = (apiKey: string) => {
|
||||||
const statusData = calculateStatusBarData(usageDetails, apiKey);
|
const statusData = statusBarCache.get(apiKey) || calculateStatusBarData([], apiKey);
|
||||||
const hasData = statusData.totalSuccess + statusData.totalFailure > 0;
|
const hasData = statusData.totalSuccess + statusData.totalFailure > 0;
|
||||||
const rateClass = !hasData
|
const rateClass = !hasData
|
||||||
? ''
|
? ''
|
||||||
@@ -1132,11 +1176,8 @@ export function AiProvidersPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 渲染 OpenAI 提供商的状态栏(汇总多个 apiKey)
|
// 渲染 OpenAI 提供商的状态栏(汇总多个 apiKey)
|
||||||
const renderOpenAIStatusBar = (apiKeyEntries: ApiKeyEntry[] | undefined) => {
|
const renderOpenAIStatusBar = (providerName: string) => {
|
||||||
// 合并所有 apiKey 的 usage details
|
const statusData = openaiStatusBarCache.get(providerName) || calculateStatusBarData([]);
|
||||||
const allKeys = (apiKeyEntries || []).map((e) => e.apiKey).filter(Boolean);
|
|
||||||
const filteredDetails = usageDetails.filter((detail) => allKeys.includes(detail.source));
|
|
||||||
const statusData = calculateStatusBarData(filteredDetails);
|
|
||||||
const hasData = statusData.totalSuccess + statusData.totalFailure > 0;
|
const hasData = statusData.totalSuccess + statusData.totalFailure > 0;
|
||||||
const rateClass = !hasData
|
const rateClass = !hasData
|
||||||
? ''
|
? ''
|
||||||
@@ -1806,7 +1847,7 @@ export function AiProvidersPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/* 状态监测栏(汇总) */}
|
{/* 状态监测栏(汇总) */}
|
||||||
{renderOpenAIStatusBar(item.apiKeyEntries)}
|
{renderOpenAIStatusBar(item.name)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useInterval } from '@/hooks/useInterval';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
||||||
@@ -166,6 +167,7 @@ export function AuthFilesPage() {
|
|||||||
const [savingExcluded, setSavingExcluded] = useState(false);
|
const [savingExcluded, setSavingExcluded] = useState(false);
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
const loadingKeyStatsRef = useRef(false);
|
||||||
const excludedUnsupportedRef = useRef(false);
|
const excludedUnsupportedRef = useRef(false);
|
||||||
|
|
||||||
const disableControls = connectionStatus !== 'connected';
|
const disableControls = connectionStatus !== 'connected';
|
||||||
@@ -197,8 +199,11 @@ export function AuthFilesPage() {
|
|||||||
}
|
}
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
// 加载 key 统计和 usage 明细
|
// 加载 key 统计和 usage 明细(API 层已有60秒超时)
|
||||||
const loadKeyStats = useCallback(async () => {
|
const loadKeyStats = useCallback(async () => {
|
||||||
|
// 防止重复请求
|
||||||
|
if (loadingKeyStatsRef.current) return;
|
||||||
|
loadingKeyStatsRef.current = true;
|
||||||
try {
|
try {
|
||||||
const usageResponse = await usageApi.getUsage();
|
const usageResponse = await usageApi.getUsage();
|
||||||
const usageData = usageResponse?.usage ?? usageResponse;
|
const usageData = usageResponse?.usage ?? usageResponse;
|
||||||
@@ -209,6 +214,8 @@ export function AuthFilesPage() {
|
|||||||
setUsageDetails(details);
|
setUsageDetails(details);
|
||||||
} catch {
|
} catch {
|
||||||
// 静默失败
|
// 静默失败
|
||||||
|
} finally {
|
||||||
|
loadingKeyStatsRef.current = false;
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -244,6 +251,9 @@ export function AuthFilesPage() {
|
|||||||
loadExcluded();
|
loadExcluded();
|
||||||
}, [loadFiles, loadKeyStats, loadExcluded]);
|
}, [loadFiles, loadKeyStats, loadExcluded]);
|
||||||
|
|
||||||
|
// 定时刷新状态数据(每240秒)
|
||||||
|
useInterval(loadKeyStats, 240_000);
|
||||||
|
|
||||||
// 提取所有存在的类型
|
// 提取所有存在的类型
|
||||||
const existingTypes = useMemo(() => {
|
const existingTypes = useMemo(() => {
|
||||||
const types = new Set<string>(['all']);
|
const types = new Set<string>(['all']);
|
||||||
@@ -577,19 +587,34 @@ export function AuthFilesPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 预计算所有认证文件的状态栏数据(避免每次渲染重复计算)
|
||||||
|
const statusBarCache = useMemo(() => {
|
||||||
|
const cache = new Map<string, ReturnType<typeof calculateStatusBarData>>();
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
|
||||||
|
const authIndexKey = normalizeAuthIndexValue(rawAuthIndex);
|
||||||
|
|
||||||
|
if (authIndexKey) {
|
||||||
|
// 过滤出属于该认证文件的 usage 明细
|
||||||
|
const filteredDetails = usageDetails.filter((detail) => {
|
||||||
|
const detailAuthIndex = normalizeAuthIndexValue(detail.auth_index);
|
||||||
|
return detailAuthIndex !== null && detailAuthIndex === authIndexKey;
|
||||||
|
});
|
||||||
|
cache.set(authIndexKey, calculateStatusBarData(filteredDetails));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return cache;
|
||||||
|
}, [usageDetails, files]);
|
||||||
|
|
||||||
// 渲染状态监测栏
|
// 渲染状态监测栏
|
||||||
const renderStatusBar = (item: AuthFileItem) => {
|
const renderStatusBar = (item: AuthFileItem) => {
|
||||||
// 认证文件使用 authIndex 来匹配 usage 数据
|
// 认证文件使用 authIndex 来匹配 usage 数据
|
||||||
const rawAuthIndex = item['auth_index'] ?? item.authIndex;
|
const rawAuthIndex = item['auth_index'] ?? item.authIndex;
|
||||||
const authIndexKey = normalizeAuthIndexValue(rawAuthIndex);
|
const authIndexKey = normalizeAuthIndexValue(rawAuthIndex);
|
||||||
|
|
||||||
// 过滤出属于该认证文件的 usage 明细
|
const statusData = (authIndexKey && statusBarCache.get(authIndexKey)) || calculateStatusBarData([]);
|
||||||
const filteredDetails = usageDetails.filter((detail) => {
|
|
||||||
const detailAuthIndex = normalizeAuthIndexValue(detail.auth_index);
|
|
||||||
return detailAuthIndex !== null && detailAuthIndex === authIndexKey;
|
|
||||||
});
|
|
||||||
|
|
||||||
const statusData = calculateStatusBarData(filteredDetails);
|
|
||||||
const hasData = statusData.totalSuccess + statusData.totalFailure > 0;
|
const hasData = statusData.totalSuccess + statusData.totalFailure > 0;
|
||||||
const rateClass = !hasData
|
const rateClass = !hasData
|
||||||
? ''
|
? ''
|
||||||
|
|||||||
Reference in New Issue
Block a user