From cb6b810d6d2e32eed06ee35c117d3d7ccf8c804a Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Sat, 27 Dec 2025 15:26:38 +0800 Subject: [PATCH] perf(providers,auth-files): cache status bar data and add auto-refresh --- src/pages/AiProvidersPage.tsx | 59 +++++++++++++++++++++++++++++------ src/pages/AuthFilesPage.tsx | 41 +++++++++++++++++++----- 2 files changed, 83 insertions(+), 17 deletions(-) diff --git a/src/pages/AiProvidersPage.tsx b/src/pages/AiProvidersPage.tsx index 3b7e8a9..18097e6 100644 --- a/src/pages/AiProvidersPage.tsx +++ b/src/pages/AiProvidersPage.tsx @@ -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 { useInterval } from '@/hooks/useInterval'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; @@ -204,6 +205,7 @@ export function AiProvidersPage() { const [openaiProviders, setOpenaiProviders] = useState([]); const [keyStats, setKeyStats] = useState({ bySource: {}, byAuthIndex: {} }); const [usageDetails, setUsageDetails] = useState([]); + const loadingKeyStatsRef = useRef(false); const [modal, setModal] = useState(null); @@ -275,8 +277,11 @@ export function AiProvidersPage() { [openaiForm.modelEntries] ); - // 加载 key 统计和 usage 明细 + // 加载 key 统计和 usage 明细(API 层已有60秒超时) const loadKeyStats = useCallback(async () => { + // 防止重复请求 + if (loadingKeyStatsRef.current) return; + loadingKeyStatsRef.current = true; try { const usageResponse = await usageApi.getUsage(); const usageData = usageResponse?.usage ?? usageResponse; @@ -287,6 +292,8 @@ export function AiProvidersPage() { setUsageDetails(details); } catch { // 静默失败 + } finally { + loadingKeyStatsRef.current = false; } }, []); @@ -318,6 +325,9 @@ export function AiProvidersPage() { loadKeyStats(); }, [loadKeyStats]); + // 定时刷新状态数据(每240秒) + useInterval(loadKeyStats, 240_000); + useEffect(() => { if (config?.geminiApiKeys) setGeminiKeys(config.geminiApiKeys); if (config?.codexApiKeys) setCodexConfigs(config.codexApiKeys); @@ -1097,9 +1107,43 @@ export function AiProvidersPage() { ); }; + // 预计算所有 apiKey 的状态栏数据(避免每次渲染重复计算) + const statusBarCache = useMemo(() => { + const cache = new Map>(); + + // 收集所有需要计算的 apiKey + const allApiKeys = new Set(); + 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>(); + + 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 statusData = calculateStatusBarData(usageDetails, apiKey); + const statusData = statusBarCache.get(apiKey) || calculateStatusBarData([], apiKey); const hasData = statusData.totalSuccess + statusData.totalFailure > 0; const rateClass = !hasData ? '' @@ -1132,11 +1176,8 @@ export function AiProvidersPage() { }; // 渲染 OpenAI 提供商的状态栏(汇总多个 apiKey) - const renderOpenAIStatusBar = (apiKeyEntries: ApiKeyEntry[] | undefined) => { - // 合并所有 apiKey 的 usage details - const allKeys = (apiKeyEntries || []).map((e) => e.apiKey).filter(Boolean); - const filteredDetails = usageDetails.filter((detail) => allKeys.includes(detail.source)); - const statusData = calculateStatusBarData(filteredDetails); + const renderOpenAIStatusBar = (providerName: string) => { + const statusData = openaiStatusBarCache.get(providerName) || calculateStatusBarData([]); const hasData = statusData.totalSuccess + statusData.totalFailure > 0; const rateClass = !hasData ? '' @@ -1806,7 +1847,7 @@ export function AiProvidersPage() { {/* 状态监测栏(汇总) */} - {renderOpenAIStatusBar(item.apiKeyEntries)} + {renderOpenAIStatusBar(item.name)} ); }, diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx index 87f8263..67ad8e5 100644 --- a/src/pages/AuthFilesPage.tsx +++ b/src/pages/AuthFilesPage.tsx @@ -1,5 +1,6 @@ import { useEffect, useMemo, useRef, useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +import { useInterval } from '@/hooks/useInterval'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; @@ -166,6 +167,7 @@ export function AuthFilesPage() { const [savingExcluded, setSavingExcluded] = useState(false); const fileInputRef = useRef(null); + const loadingKeyStatsRef = useRef(false); const excludedUnsupportedRef = useRef(false); const disableControls = connectionStatus !== 'connected'; @@ -197,8 +199,11 @@ export function AuthFilesPage() { } }, [t]); - // 加载 key 统计和 usage 明细 + // 加载 key 统计和 usage 明细(API 层已有60秒超时) const loadKeyStats = useCallback(async () => { + // 防止重复请求 + if (loadingKeyStatsRef.current) return; + loadingKeyStatsRef.current = true; try { const usageResponse = await usageApi.getUsage(); const usageData = usageResponse?.usage ?? usageResponse; @@ -209,6 +214,8 @@ export function AuthFilesPage() { setUsageDetails(details); } catch { // 静默失败 + } finally { + loadingKeyStatsRef.current = false; } }, []); @@ -244,6 +251,9 @@ export function AuthFilesPage() { loadExcluded(); }, [loadFiles, loadKeyStats, loadExcluded]); + // 定时刷新状态数据(每240秒) + useInterval(loadKeyStats, 240_000); + // 提取所有存在的类型 const existingTypes = useMemo(() => { const types = new Set(['all']); @@ -577,19 +587,34 @@ export function AuthFilesPage() { ); + // 预计算所有认证文件的状态栏数据(避免每次渲染重复计算) + const statusBarCache = useMemo(() => { + const cache = new Map>(); + + 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) => { // 认证文件使用 authIndex 来匹配 usage 数据 const rawAuthIndex = item['auth_index'] ?? item.authIndex; const authIndexKey = normalizeAuthIndexValue(rawAuthIndex); - // 过滤出属于该认证文件的 usage 明细 - const filteredDetails = usageDetails.filter((detail) => { - const detailAuthIndex = normalizeAuthIndexValue(detail.auth_index); - return detailAuthIndex !== null && detailAuthIndex === authIndexKey; - }); - - const statusData = calculateStatusBarData(filteredDetails); + const statusData = (authIndexKey && statusBarCache.get(authIndexKey)) || calculateStatusBarData([]); const hasData = statusData.totalSuccess + statusData.totalFailure > 0; const rateClass = !hasData ? ''