From a4fcd76998384134f7f1cec2d6f996cc4a376c43 Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Sat, 13 Jun 2026 02:21:46 +0800 Subject: [PATCH] refactor(hooks): extract shared api-key resolution for model fetching DashboardPage and SystemPage carried byte-identical copies of normalizeApiKeyList plus a per-connection cache for resolving the key used by /models probes. Move the logic into useApiKeysForModels and reuse it from both pages. --- src/hooks/useApiKeysForModels.ts | 71 ++++++++++++++++++++++++++++++++ src/pages/DashboardPage.tsx | 57 ++----------------------- src/pages/SystemPage.tsx | 58 ++------------------------ 3 files changed, 77 insertions(+), 109 deletions(-) create mode 100644 src/hooks/useApiKeysForModels.ts diff --git a/src/hooks/useApiKeysForModels.ts b/src/hooks/useApiKeysForModels.ts new file mode 100644 index 0000000..01e1d58 --- /dev/null +++ b/src/hooks/useApiKeysForModels.ts @@ -0,0 +1,71 @@ +import { useCallback, useEffect, useRef } from 'react'; +import { apiKeysApi } from '@/services/api/apiKeys'; +import { useAuthStore, useConfigStore } from '@/stores'; + +const normalizeApiKeyList = (input: unknown): string[] => { + if (!Array.isArray(input)) return []; + const seen = new Set(); + const keys: string[] = []; + + input.forEach((item) => { + const record = + item !== null && typeof item === 'object' && !Array.isArray(item) + ? (item as Record) + : null; + const value = + typeof item === 'string' + ? item + : record + ? (record['api-key'] ?? record['apiKey'] ?? record.key ?? record.Key) + : ''; + const trimmed = String(value ?? '').trim(); + if (!trimmed || seen.has(trimmed)) return; + seen.add(trimmed); + keys.push(trimmed); + }); + + return keys; +}; + +/** + * 解析用于 /models 探测的 API 密钥列表: + * 优先取已缓存配置中的 api-keys,否则回退到 /api-keys 接口,结果按连接缓存。 + */ +export function useApiKeysForModels() { + const apiBase = useAuthStore((state) => state.apiBase); + const configApiKeys = useConfigStore((state) => state.config?.apiKeys); + const cacheRef = useRef([]); + + useEffect(() => { + cacheRef.current = []; + }, [apiBase, configApiKeys]); + + return useCallback( + async ({ force = false }: { force?: boolean } = {}) => { + if (force) { + cacheRef.current = []; + } + if (cacheRef.current.length) { + return cacheRef.current; + } + + const configKeys = normalizeApiKeyList(configApiKeys); + if (configKeys.length) { + cacheRef.current = configKeys; + return configKeys; + } + + try { + const list = await apiKeysApi.list(); + const normalized = normalizeApiKeyList(list); + if (normalized.length) { + cacheRef.current = normalized; + } + return normalized; + } catch { + return []; + } + }, + [configApiKeys] + ); +} diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx index b53378b..e4cebc1 100644 --- a/src/pages/DashboardPage.tsx +++ b/src/pages/DashboardPage.tsx @@ -1,9 +1,10 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { IconKey, IconBot, IconFileText, IconSatellite } from '@/components/ui/icons'; import { useAuthStore, useConfigStore, useModelsStore } from '@/stores'; import { apiKeysApi, providersApi, authFilesApi, ampcodeApi } from '@/services/api'; +import { useApiKeysForModels } from '@/hooks/useApiKeysForModels'; import type { AmpcodeConfig } from '@/types'; import { formatDateValue } from '@/utils/format'; import styles from './DashboardPage.module.scss'; @@ -81,12 +82,6 @@ export function DashboardPage() { const [timeOfDay, setTimeOfDay] = useState(getTimeOfDay); const [currentTime, setCurrentTime] = useState(() => new Date()); - const apiKeysCache = useRef([]); - - useEffect(() => { - apiKeysCache.current = []; - }, [apiBase, config?.apiKeys]); - // Update time every 60 seconds useEffect(() => { const id = setInterval(() => { @@ -96,53 +91,7 @@ export function DashboardPage() { return () => clearInterval(id); }, []); - const normalizeApiKeyList = (input: unknown): string[] => { - if (!Array.isArray(input)) return []; - const seen = new Set(); - const keys: string[] = []; - - input.forEach((item) => { - const record = - item !== null && typeof item === 'object' && !Array.isArray(item) - ? (item as Record) - : null; - const value = - typeof item === 'string' - ? item - : record - ? (record['api-key'] ?? record['apiKey'] ?? record.key ?? record.Key) - : ''; - const trimmed = String(value ?? '').trim(); - if (!trimmed || seen.has(trimmed)) return; - seen.add(trimmed); - keys.push(trimmed); - }); - - return keys; - }; - - const resolveApiKeysForModels = useCallback(async () => { - if (apiKeysCache.current.length) { - return apiKeysCache.current; - } - - const configKeys = normalizeApiKeyList(config?.apiKeys); - if (configKeys.length) { - apiKeysCache.current = configKeys; - return configKeys; - } - - try { - const list = await apiKeysApi.list(); - const normalized = normalizeApiKeyList(list); - if (normalized.length) { - apiKeysCache.current = normalized; - } - return normalized; - } catch { - return []; - } - }, [config?.apiKeys]); + const resolveApiKeysForModels = useApiKeysForModels(); const fetchModels = useCallback(async () => { if (connectionStatus !== 'connected' || !apiBase) { diff --git a/src/pages/SystemPage.tsx b/src/pages/SystemPage.tsx index 391b35e..c4a5b25 100644 --- a/src/pages/SystemPage.tsx +++ b/src/pages/SystemPage.tsx @@ -13,7 +13,7 @@ import { useThemeStore, } from '@/stores'; import { configApi, versionApi } from '@/services/api'; -import { apiKeysApi } from '@/services/api/apiKeys'; +import { useApiKeysForModels } from '@/hooks/useApiKeysForModels'; import { formatDateTimeValue } from '@/utils/format'; import { classifyModels } from '@/utils/models'; import { STORAGE_KEY_AUTH } from '@/utils/constants'; @@ -95,7 +95,6 @@ export function SystemPage() { const [requestLogSaving, setRequestLogSaving] = useState(false); const [checkingVersion, setCheckingVersion] = useState(false); - const apiKeysCache = useRef([]); const versionTapCount = useRef(0); const versionTapTimer = useRef | null>(null); @@ -120,54 +119,7 @@ export function SystemPage() { return resolvedTheme === 'dark' ? iconEntry.dark : iconEntry.light; }; - const normalizeApiKeyList = (input: unknown): string[] => { - if (!Array.isArray(input)) return []; - const seen = new Set(); - const keys: string[] = []; - - input.forEach((item) => { - const record = - item !== null && typeof item === 'object' && !Array.isArray(item) - ? (item as Record) - : null; - const value = - typeof item === 'string' - ? item - : record - ? (record['api-key'] ?? record['apiKey'] ?? record.key ?? record.Key) - : ''; - const trimmed = String(value ?? '').trim(); - if (!trimmed || seen.has(trimmed)) return; - seen.add(trimmed); - keys.push(trimmed); - }); - - return keys; - }; - - const resolveApiKeysForModels = useCallback(async () => { - if (apiKeysCache.current.length) { - return apiKeysCache.current; - } - - const configKeys = normalizeApiKeyList(config?.apiKeys); - if (configKeys.length) { - apiKeysCache.current = configKeys; - return configKeys; - } - - try { - const list = await apiKeysApi.list(); - const normalized = normalizeApiKeyList(list); - if (normalized.length) { - apiKeysCache.current = normalized; - } - return normalized; - } catch (err) { - console.warn('Auto loading API keys for models failed:', err); - return []; - } - }, [config?.apiKeys]); + const resolveApiKeysForModels = useApiKeysForModels(); const fetchModels = async ({ forceRefresh = false }: { forceRefresh?: boolean } = {}) => { if (auth.connectionStatus !== 'connected') { @@ -183,13 +135,9 @@ export function SystemPage() { return; } - if (forceRefresh) { - apiKeysCache.current = []; - } - setModelStatus({ type: 'muted', message: t('system_info.models_loading') }); try { - const apiKeys = await resolveApiKeysForModels(); + const apiKeys = await resolveApiKeysForModels({ force: forceRefresh }); const primaryKey = apiKeys[0]; const list = await fetchModelsFromStore(auth.apiBase, primaryKey, forceRefresh); const hasModels = list.length > 0;