From 6c2cd761ba081d06478c3c36e6bb2be852c8e349 Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Sun, 8 Feb 2026 09:42:00 +0800 Subject: [PATCH] refactor(core): harden API parsing and improve type safety --- src/components/common/PageTransition.tsx | 15 +- src/components/common/PageTransitionLayer.ts | 15 ++ src/components/layout/MainLayout.tsx | 10 +- .../modelAlias/ModelMappingDiagram.tsx | 2 - .../providers/ProviderNav/ProviderNav.tsx | 8 +- src/components/usage/hooks/useUsageData.ts | 4 +- src/hooks/useApi.ts | 7 +- src/hooks/useVisualConfig.ts | 175 +++++++++----- src/pages/AiProvidersOpenAIEditLayout.tsx | 2 +- src/pages/AuthFilesPage.tsx | 15 +- src/pages/DashboardPage.tsx | 15 +- src/pages/LoginPage.tsx | 25 +- src/pages/OAuthPage.tsx | 53 +++-- src/pages/SystemPage.tsx | 43 +++- src/services/api/ampcode.ts | 2 +- src/services/api/apiCall.ts | 21 +- src/services/api/apiKeys.ts | 6 +- src/services/api/authFiles.ts | 18 +- src/services/api/client.ts | 49 ++-- src/services/api/config.ts | 15 +- src/services/api/configFile.ts | 2 +- src/services/api/providers.ts | 37 +-- src/services/api/transformers.ts | 182 ++++++++------ src/services/api/usage.ts | 6 +- src/services/api/version.ts | 2 +- src/services/storage/secureStorage.ts | 6 +- src/stores/useAuthStore.ts | 10 +- src/stores/useConfigStore.ts | 57 +++-- src/stores/useModelsStore.ts | 5 +- src/types/api.ts | 8 +- src/types/authFile.ts | 2 +- src/types/common.ts | 2 +- src/types/config.ts | 2 +- src/types/log.ts | 2 +- src/types/provider.ts | 2 +- src/types/visualConfig.ts | 2 +- src/utils/helpers.ts | 31 +-- src/utils/models.ts | 11 +- src/utils/usage.ts | 224 +++++++++++------- 39 files changed, 689 insertions(+), 404 deletions(-) create mode 100644 src/components/common/PageTransitionLayer.ts diff --git a/src/components/common/PageTransition.tsx b/src/components/common/PageTransition.tsx index 5efd7ad..2231120 100644 --- a/src/components/common/PageTransition.tsx +++ b/src/components/common/PageTransition.tsx @@ -1,14 +1,13 @@ import { ReactNode, - createContext, useCallback, - useContext, useLayoutEffect, useRef, useState, } from 'react'; import { useLocation, type Location } from 'react-router-dom'; import gsap from 'gsap'; +import { PageTransitionLayerContext, type LayerStatus } from './PageTransitionLayer'; import './PageTransition.scss'; interface PageTransitionProps { @@ -27,8 +26,6 @@ const IOS_EXIT_TO_X_PERCENT_BACKWARD = 100; const IOS_ENTER_FROM_X_PERCENT_BACKWARD = -30; const IOS_EXIT_DIM_OPACITY = 0.72; -type LayerStatus = 'current' | 'exiting' | 'stacked'; - type Layer = { key: string; location: Location; @@ -39,16 +36,6 @@ type TransitionDirection = 'forward' | 'backward'; type TransitionVariant = 'vertical' | 'ios'; -type PageTransitionLayerContextValue = { - status: LayerStatus; -}; - -const PageTransitionLayerContext = createContext(null); - -export function usePageTransitionLayer() { - return useContext(PageTransitionLayerContext); -} - export function PageTransition({ render, getRouteOrder, diff --git a/src/components/common/PageTransitionLayer.ts b/src/components/common/PageTransitionLayer.ts new file mode 100644 index 0000000..3e33e86 --- /dev/null +++ b/src/components/common/PageTransitionLayer.ts @@ -0,0 +1,15 @@ +import { createContext, useContext } from 'react'; + +export type LayerStatus = 'current' | 'exiting' | 'stacked'; + +type PageTransitionLayerContextValue = { + status: LayerStatus; +}; + +export const PageTransitionLayerContext = + createContext(null); + +export function usePageTransitionLayer() { + return useContext(PageTransitionLayerContext); +} + diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index 074d9ea..982e78b 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -441,7 +441,8 @@ export function MainLayout() { setCheckingVersion(true); try { const data = await versionApi.checkLatest(); - const latest = data?.['latest-version'] ?? data?.latest_version ?? data?.latest ?? ''; + const latestRaw = data?.['latest-version'] ?? data?.latest_version ?? data?.latest ?? ''; + const latest = typeof latestRaw === 'string' ? latestRaw : String(latestRaw ?? ''); const comparison = compareVersions(latest, serverVersion); if (!latest) { @@ -459,8 +460,11 @@ export function MainLayout() { } else { showNotification(t('system_info.version_is_latest'), 'success'); } - } catch (error: any) { - showNotification(`${t('system_info.version_check_error')}: ${error?.message || ''}`, 'error'); + } catch (error: unknown) { + const message = + error instanceof Error ? error.message : typeof error === 'string' ? error : ''; + const suffix = message ? `: ${message}` : ''; + showNotification(`${t('system_info.version_check_error')}${suffix}`, 'error'); } finally { setCheckingVersion(false); } diff --git a/src/components/modelAlias/ModelMappingDiagram.tsx b/src/components/modelAlias/ModelMappingDiagram.tsx index 209a181..b207882 100644 --- a/src/components/modelAlias/ModelMappingDiagram.tsx +++ b/src/components/modelAlias/ModelMappingDiagram.tsx @@ -285,7 +285,6 @@ export const ModelMappingDiagram = forwardRef { // updateLines is called after layout is calculated, ensuring elements are in place. - updateLines(); const raf = requestAnimationFrame(updateLines); window.addEventListener('resize', updateLines); return () => { @@ -295,7 +294,6 @@ export const ModelMappingDiagram = forwardRef { - updateLines(); const raf = requestAnimationFrame(updateLines); return () => cancelAnimationFrame(raf); }, [providerGroupHeights, updateLines]); diff --git a/src/components/providers/ProviderNav/ProviderNav.tsx b/src/components/providers/ProviderNav/ProviderNav.tsx index 1dd7960..a6ee99f 100644 --- a/src/components/providers/ProviderNav/ProviderNav.tsx +++ b/src/components/providers/ProviderNav/ProviderNav.tsx @@ -1,7 +1,7 @@ import { CSSProperties, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { useLocation } from 'react-router-dom'; -import { usePageTransitionLayer } from '@/components/common/PageTransition'; +import { usePageTransitionLayer } from '@/components/common/PageTransitionLayer'; import { useThemeStore } from '@/stores'; import iconGemini from '@/assets/icons/gemini.svg'; import iconOpenaiLight from '@/assets/icons/openai-light.svg'; @@ -135,8 +135,9 @@ export function ProviderNav() { window.addEventListener('scroll', handleScroll, { passive: true }); contentScroller?.addEventListener('scroll', handleScroll, { passive: true }); window.addEventListener('resize', handleScroll); - handleScroll(); + const raf = requestAnimationFrame(handleScroll); return () => { + cancelAnimationFrame(raf); window.removeEventListener('scroll', handleScroll); window.removeEventListener('resize', handleScroll); contentScroller?.removeEventListener('scroll', handleScroll); @@ -168,7 +169,8 @@ export function ProviderNav() { useLayoutEffect(() => { if (!shouldShow) return; - updateIndicator(activeProvider); + const raf = requestAnimationFrame(() => updateIndicator(activeProvider)); + return () => cancelAnimationFrame(raf); }, [activeProvider, shouldShow, updateIndicator]); // Expose overlay height to the page, so it can reserve bottom padding and avoid being covered. diff --git a/src/components/usage/hooks/useUsageData.ts b/src/components/usage/hooks/useUsageData.ts index 06e669d..82bf526 100644 --- a/src/components/usage/hooks/useUsageData.ts +++ b/src/components/usage/hooks/useUsageData.ts @@ -45,8 +45,8 @@ export function useUsageData(): UseUsageDataReturn { setError(''); try { const data = await usageApi.getUsage(); - const payload = data?.usage ?? data; - setUsage(payload); + const payload = (data?.usage ?? data) as unknown; + setUsage(payload && typeof payload === 'object' ? (payload as UsagePayload) : null); } catch (err: unknown) { const message = err instanceof Error ? err.message : t('usage_stats.loading_error'); setError(message); diff --git a/src/hooks/useApi.ts b/src/hooks/useApi.ts index 7dc3150..8d5565f 100644 --- a/src/hooks/useApi.ts +++ b/src/hooks/useApi.ts @@ -13,7 +13,7 @@ interface UseApiOptions { successMessage?: string; } -export function useApi( +export function useApi( apiFunction: (...args: Args) => Promise, options: UseApiOptions = {} ) { @@ -38,8 +38,9 @@ export function useApi( options.onSuccess?.(result); return result; - } catch (err) { - const errorObj = err as Error; + } catch (err: unknown) { + const errorObj = + err instanceof Error ? err : new Error(typeof err === 'string' ? err : 'Unknown error'); setError(errorObj); if (options.showErrorNotification !== false) { diff --git a/src/hooks/useVisualConfig.ts b/src/hooks/useVisualConfig.ts index 47d3cce..dac858c 100644 --- a/src/hooks/useVisualConfig.ts +++ b/src/hooks/useVisualConfig.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; import type { PayloadFilterRule, @@ -123,20 +123,47 @@ function parsePayloadParamValue(raw: unknown): { valueType: PayloadParamValueTyp return { valueType: 'string', value: String(raw ?? '') }; } +const PAYLOAD_PROTOCOL_VALUES = [ + 'openai', + 'openai-response', + 'gemini', + 'claude', + 'codex', + 'antigravity', +] as const; +type PayloadProtocol = (typeof PAYLOAD_PROTOCOL_VALUES)[number]; + +function parsePayloadProtocol(raw: unknown): PayloadProtocol | undefined { + if (typeof raw !== 'string') return undefined; + return PAYLOAD_PROTOCOL_VALUES.includes(raw as PayloadProtocol) + ? (raw as PayloadProtocol) + : undefined; +} + function parsePayloadRules(rules: unknown): PayloadRule[] { if (!Array.isArray(rules)) return []; - return rules.map((rule, index) => ({ - id: `payload-rule-${index}`, - models: Array.isArray((rule as any)?.models) - ? ((rule as any).models as unknown[]).map((model: any, modelIndex: number) => ({ - id: `model-${index}-${modelIndex}`, - name: typeof model === 'string' ? model : model?.name || '', - protocol: typeof model === 'object' ? (model?.protocol as any) : undefined, - })) - : [], - params: (rule as any)?.params - ? Object.entries((rule as any).params as Record).map(([path, value], pIndex) => { + return rules.map((rule, index) => { + const record = asRecord(rule) ?? {}; + + const modelsRaw = record.models; + const models = Array.isArray(modelsRaw) + ? modelsRaw.map((model, modelIndex) => { + const modelRecord = asRecord(model); + const nameRaw = + typeof model === 'string' ? model : (modelRecord?.name ?? modelRecord?.id ?? ''); + const name = typeof nameRaw === 'string' ? nameRaw : String(nameRaw ?? ''); + return { + id: `model-${index}-${modelIndex}`, + name, + protocol: parsePayloadProtocol(modelRecord?.protocol), + }; + }) + : []; + + const paramsRecord = asRecord(record.params); + const params = paramsRecord + ? Object.entries(paramsRecord).map(([path, value], pIndex) => { const parsedValue = parsePayloadParamValue(value); return { id: `param-${index}-${pIndex}`, @@ -145,41 +172,55 @@ function parsePayloadRules(rules: unknown): PayloadRule[] { value: parsedValue.value, }; }) - : [], - })); + : []; + + return { id: `payload-rule-${index}`, models, params }; + }); } function parsePayloadFilterRules(rules: unknown): PayloadFilterRule[] { if (!Array.isArray(rules)) return []; - return rules.map((rule, index) => ({ - id: `payload-filter-rule-${index}`, - models: Array.isArray((rule as any)?.models) - ? ((rule as any).models as unknown[]).map((model: any, modelIndex: number) => ({ - id: `filter-model-${index}-${modelIndex}`, - name: typeof model === 'string' ? model : model?.name || '', - protocol: typeof model === 'object' ? (model?.protocol as any) : undefined, - })) - : [], - params: Array.isArray((rule as any)?.params) ? ((rule as any).params as unknown[]).map(String) : [], - })); + return rules.map((rule, index) => { + const record = asRecord(rule) ?? {}; + + const modelsRaw = record.models; + const models = Array.isArray(modelsRaw) + ? modelsRaw.map((model, modelIndex) => { + const modelRecord = asRecord(model); + const nameRaw = + typeof model === 'string' ? model : (modelRecord?.name ?? modelRecord?.id ?? ''); + const name = typeof nameRaw === 'string' ? nameRaw : String(nameRaw ?? ''); + return { + id: `filter-model-${index}-${modelIndex}`, + name, + protocol: parsePayloadProtocol(modelRecord?.protocol), + }; + }) + : []; + + const paramsRaw = record.params; + const params = Array.isArray(paramsRaw) ? paramsRaw.map(String) : []; + + return { id: `payload-filter-rule-${index}`, models, params }; + }); } -function serializePayloadRulesForYaml(rules: PayloadRule[]): any[] { +function serializePayloadRulesForYaml(rules: PayloadRule[]): Array> { return rules .map((rule) => { const models = (rule.models || []) .filter((m) => m.name?.trim()) .map((m) => { - const obj: Record = { name: m.name.trim() }; + const obj: Record = { name: m.name.trim() }; if (m.protocol) obj.protocol = m.protocol; return obj; }); - const params: Record = {}; + const params: Record = {}; for (const param of rule.params || []) { if (!param.path?.trim()) continue; - let value: any = param.value; + let value: unknown = param.value; if (param.valueType === 'number') { const num = Number(param.value); value = Number.isFinite(num) ? num : param.value; @@ -200,13 +241,15 @@ function serializePayloadRulesForYaml(rules: PayloadRule[]): any[] { .filter((rule) => rule.models.length > 0); } -function serializePayloadFilterRulesForYaml(rules: PayloadFilterRule[]): any[] { +function serializePayloadFilterRulesForYaml( + rules: PayloadFilterRule[] +): Array> { return rules .map((rule) => { const models = (rule.models || []) .filter((m) => m.name?.trim()) .map((m) => { - const obj: Record = { name: m.name.trim() }; + const obj: Record = { name: m.name.trim() }; if (m.protocol) obj.protocol = m.protocol; return obj; }); @@ -225,33 +268,45 @@ export function useVisualConfig() { ...DEFAULT_VISUAL_VALUES, }); - const baselineValues = useRef({ ...DEFAULT_VISUAL_VALUES }); + const [baselineValues, setBaselineValues] = useState({ + ...DEFAULT_VISUAL_VALUES, + }); const visualDirty = useMemo(() => { - return JSON.stringify(visualValues) !== JSON.stringify(baselineValues.current); - }, [visualValues]); + return JSON.stringify(visualValues) !== JSON.stringify(baselineValues); + }, [baselineValues, visualValues]); const loadVisualValuesFromYaml = useCallback((yamlContent: string) => { try { - const parsed: any = parseYaml(yamlContent) || {}; + const parsedRaw: unknown = parseYaml(yamlContent) || {}; + const parsed = asRecord(parsedRaw) ?? {}; + const tls = asRecord(parsed.tls); + const remoteManagement = asRecord(parsed['remote-management']); + const quotaExceeded = asRecord(parsed['quota-exceeded']); + const routing = asRecord(parsed.routing); + const payload = asRecord(parsed.payload); + const streaming = asRecord(parsed.streaming); const newValues: VisualConfigValues = { - host: parsed.host || '', + host: typeof parsed.host === 'string' ? parsed.host : '', port: String(parsed.port ?? ''), - tlsEnable: Boolean(parsed.tls?.enable), - tlsCert: parsed.tls?.cert || '', - tlsKey: parsed.tls?.key || '', + tlsEnable: Boolean(tls?.enable), + tlsCert: typeof tls?.cert === 'string' ? tls.cert : '', + tlsKey: typeof tls?.key === 'string' ? tls.key : '', - rmAllowRemote: Boolean(parsed['remote-management']?.['allow-remote']), - rmSecretKey: parsed['remote-management']?.['secret-key'] || '', - rmDisableControlPanel: Boolean(parsed['remote-management']?.['disable-control-panel']), + rmAllowRemote: Boolean(remoteManagement?.['allow-remote']), + rmSecretKey: + typeof remoteManagement?.['secret-key'] === 'string' ? remoteManagement['secret-key'] : '', + rmDisableControlPanel: Boolean(remoteManagement?.['disable-control-panel']), rmPanelRepo: - parsed['remote-management']?.['panel-github-repository'] ?? - parsed['remote-management']?.['panel-repo'] ?? - '', + typeof remoteManagement?.['panel-github-repository'] === 'string' + ? remoteManagement['panel-github-repository'] + : typeof remoteManagement?.['panel-repo'] === 'string' + ? remoteManagement['panel-repo'] + : '', - authDir: parsed['auth-dir'] || '', + authDir: typeof parsed['auth-dir'] === 'string' ? parsed['auth-dir'] : '', apiKeysText: parseApiKeysText(parsed['api-keys']), debug: Boolean(parsed.debug), @@ -260,35 +315,36 @@ export function useVisualConfig() { logsMaxTotalSizeMb: String(parsed['logs-max-total-size-mb'] ?? ''), usageStatisticsEnabled: Boolean(parsed['usage-statistics-enabled']), - proxyUrl: parsed['proxy-url'] || '', + proxyUrl: typeof parsed['proxy-url'] === 'string' ? parsed['proxy-url'] : '', forceModelPrefix: Boolean(parsed['force-model-prefix']), requestRetry: String(parsed['request-retry'] ?? ''), maxRetryInterval: String(parsed['max-retry-interval'] ?? ''), wsAuth: Boolean(parsed['ws-auth']), - quotaSwitchProject: Boolean(parsed['quota-exceeded']?.['switch-project'] ?? true), + quotaSwitchProject: Boolean(quotaExceeded?.['switch-project'] ?? true), quotaSwitchPreviewModel: Boolean( - parsed['quota-exceeded']?.['switch-preview-model'] ?? true + quotaExceeded?.['switch-preview-model'] ?? true ), - routingStrategy: (parsed.routing?.strategy || 'round-robin') as 'round-robin' | 'fill-first', + routingStrategy: + routing?.strategy === 'fill-first' ? 'fill-first' : 'round-robin', - payloadDefaultRules: parsePayloadRules(parsed.payload?.default), - payloadOverrideRules: parsePayloadRules(parsed.payload?.override), - payloadFilterRules: parsePayloadFilterRules(parsed.payload?.filter), + payloadDefaultRules: parsePayloadRules(payload?.default), + payloadOverrideRules: parsePayloadRules(payload?.override), + payloadFilterRules: parsePayloadFilterRules(payload?.filter), streaming: { - keepaliveSeconds: String(parsed.streaming?.['keepalive-seconds'] ?? ''), - bootstrapRetries: String(parsed.streaming?.['bootstrap-retries'] ?? ''), + keepaliveSeconds: String(streaming?.['keepalive-seconds'] ?? ''), + bootstrapRetries: String(streaming?.['bootstrap-retries'] ?? ''), nonstreamKeepaliveInterval: String(parsed['nonstream-keepalive-interval'] ?? ''), }, }; setVisualValuesState(newValues); - baselineValues.current = deepClone(newValues); + setBaselineValues(deepClone(newValues)); } catch { setVisualValuesState({ ...DEFAULT_VISUAL_VALUES }); - baselineValues.current = deepClone(DEFAULT_VISUAL_VALUES); + setBaselineValues(deepClone(DEFAULT_VISUAL_VALUES)); } }, []); @@ -331,7 +387,7 @@ export function useVisualConfig() { } setString(parsed, 'auth-dir', values.authDir); - if (values.apiKeysText !== baselineValues.current.apiKeysText) { + if (values.apiKeysText !== baselineValues.apiKeysText) { const apiKeys = values.apiKeysText .split('\n') .map((key) => key.trim()) @@ -419,7 +475,7 @@ export function useVisualConfig() { return currentYaml; } }, - [visualValues] + [baselineValues, visualValues] ); const setVisualValues = useCallback((newValues: Partial) => { @@ -444,6 +500,7 @@ export function useVisualConfig() { export const VISUAL_CONFIG_PROTOCOL_OPTIONS = [ { value: '', label: '默认' }, { value: 'openai', label: 'OpenAI' }, + { value: 'openai-response', label: 'OpenAI Response' }, { value: 'gemini', label: 'Gemini' }, { value: 'claude', label: 'Claude' }, { value: 'codex', label: 'Codex' }, diff --git a/src/pages/AiProvidersOpenAIEditLayout.tsx b/src/pages/AiProvidersOpenAIEditLayout.tsx index 321b0c4..3ecf6d0 100644 --- a/src/pages/AiProvidersOpenAIEditLayout.tsx +++ b/src/pages/AiProvidersOpenAIEditLayout.tsx @@ -243,7 +243,7 @@ export function AiProvidersOpenAIEditLayout() { setTestStatus('idle'); setTestMessage(''); } - }, [availableModels, loading, testModel]); + }, [availableModels, loading, setTestMessage, setTestModel, setTestStatus, testModel]); const mergeDiscoveredModels = useCallback( (selectedModels: ModelInfo[]) => { diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx index 10b6a6b..49f992d 100644 --- a/src/pages/AuthFilesPage.tsx +++ b/src/pages/AuthFilesPage.tsx @@ -3,7 +3,7 @@ import { Trans, useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { useInterval } from '@/hooks/useInterval'; import { useHeaderRefresh } from '@/hooks/useHeaderRefresh'; -import { usePageTransitionLayer } from '@/components/common/PageTransition'; +import { usePageTransitionLayer } from '@/components/common/PageTransitionLayer'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; @@ -1475,11 +1475,14 @@ export function AuthFilesPage() { return GEMINI_CLI_CONFIG; }; - const getQuotaState = (type: QuotaProviderType, fileName: string) => { - if (type === 'antigravity') return antigravityQuota[fileName]; - if (type === 'codex') return codexQuota[fileName]; - return geminiCliQuota[fileName]; - }; + const getQuotaState = useCallback( + (type: QuotaProviderType, fileName: string) => { + if (type === 'antigravity') return antigravityQuota[fileName]; + if (type === 'codex') return codexQuota[fileName]; + return geminiCliQuota[fileName]; + }, + [antigravityQuota, codexQuota, geminiCliQuota] + ); const updateQuotaState = useCallback( ( diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx index 8054bfc..a6608c6 100644 --- a/src/pages/DashboardPage.tsx +++ b/src/pages/DashboardPage.tsx @@ -62,14 +62,23 @@ export function DashboardPage() { apiKeysCache.current = []; }, [apiBase, config?.apiKeys]); - const normalizeApiKeyList = (input: any): string[] => { + const normalizeApiKeyList = (input: unknown): string[] => { if (!Array.isArray(input)) return []; const seen = new Set(); const keys: string[] = []; input.forEach((item) => { - const value = typeof item === 'string' ? item : item?.['api-key'] ?? item?.apiKey ?? ''; - const trimmed = String(value || '').trim(); + 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); diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 8e8daf1..32068dc 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -15,11 +15,20 @@ import styles from './LoginPage.module.scss'; /** * 将 API 错误转换为本地化的用户友好消息 */ -function getLocalizedErrorMessage(error: any, t: (key: string) => string): string { - const apiError = error as ApiError; - const status = apiError?.status; - const code = apiError?.code; - const message = apiError?.message || ''; +type RedirectState = { from?: { pathname?: string } }; + +function getLocalizedErrorMessage(error: unknown, t: (key: string) => string): string { + const apiError = error as Partial; + const status = typeof apiError.status === 'number' ? apiError.status : undefined; + const code = typeof apiError.code === 'string' ? apiError.code : undefined; + const message = + error instanceof Error + ? error.message + : typeof apiError.message === 'string' + ? apiError.message + : typeof error === 'string' + ? error + : ''; // 根据 HTTP 状态码判断 if (status === 401) { @@ -99,7 +108,7 @@ export function LoginPage() { setAutoLoginSuccess(true); // 延迟跳转,让用户看到成功动画 setTimeout(() => { - const redirect = (location.state as any)?.from?.pathname || '/'; + const redirect = (location.state as RedirectState | null)?.from?.pathname || '/'; navigate(redirect, { replace: true }); }, 1500); } else { @@ -135,7 +144,7 @@ export function LoginPage() { }); showNotification(t('common.connected_status'), 'success'); navigate('/', { replace: true }); - } catch (err: any) { + } catch (err: unknown) { const message = getLocalizedErrorMessage(err, t); setError(message); showNotification(`${t('notification.login_failed')}: ${message}`, 'error'); @@ -155,7 +164,7 @@ export function LoginPage() { ); if (isAuthenticated && !autoLoading && !autoLoginSuccess) { - const redirect = (location.state as any)?.from?.pathname || '/'; + const redirect = (location.state as RedirectState | null)?.from?.pathname || '/'; return ; } diff --git a/src/pages/OAuthPage.tsx b/src/pages/OAuthPage.tsx index f0bc72d..d92bf9a 100644 --- a/src/pages/OAuthPage.tsx +++ b/src/pages/OAuthPage.tsx @@ -56,6 +56,21 @@ interface VertexImportState { result?: VertexImportResult; } +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === 'object'; +} + +function getErrorMessage(error: unknown): string { + if (error instanceof Error) return error.message; + if (isRecord(error) && typeof error.message === 'string') return error.message; + return typeof error === 'string' ? error : ''; +} + +function getErrorStatus(error: unknown): number | undefined { + if (!isRecord(error)) return undefined; + return typeof error.status === 'number' ? error.status : undefined; +} + const PROVIDERS: { id: OAuthProvider; titleKey: string; hintKey: string; urlLabelKey: string; icon: string | { light: string; dark: string } }[] = [ { id: 'codex', titleKey: 'auth_login.codex_oauth_title', hintKey: 'auth_login.codex_oauth_hint', urlLabelKey: 'auth_login.codex_oauth_url_label', icon: { light: iconCodexLight, dark: iconCodexDark } }, { id: 'anthropic', titleKey: 'auth_login.anthropic_oauth_title', hintKey: 'auth_login.anthropic_oauth_hint', urlLabelKey: 'auth_login.anthropic_oauth_url_label', icon: iconClaude }, @@ -127,8 +142,8 @@ export function OAuthPage() { window.clearInterval(timer); delete timers.current[provider]; } - } catch (err: any) { - updateProviderState(provider, { status: 'error', error: err?.message, polling: false }); + } catch (err: unknown) { + updateProviderState(provider, { status: 'error', error: getErrorMessage(err), polling: false }); window.clearInterval(timer); delete timers.current[provider]; } @@ -159,9 +174,13 @@ export function OAuthPage() { if (res.state) { startPolling(provider, res.state); } - } catch (err: any) { - updateProviderState(provider, { status: 'error', error: err?.message, polling: false }); - showNotification(`${t(getAuthKey(provider, 'oauth_start_error'))} ${err?.message || ''}`, 'error'); + } catch (err: unknown) { + const message = getErrorMessage(err); + updateProviderState(provider, { status: 'error', error: message, polling: false }); + showNotification( + `${t(getAuthKey(provider, 'oauth_start_error'))}${message ? ` ${message}` : ''}`, + 'error' + ); } }; @@ -190,13 +209,15 @@ export function OAuthPage() { await oauthApi.submitCallback(provider, redirectUrl); updateProviderState(provider, { callbackSubmitting: false, callbackStatus: 'success' }); showNotification(t('auth_login.oauth_callback_success'), 'success'); - } catch (err: any) { + } catch (err: unknown) { + const status = getErrorStatus(err); + const message = getErrorMessage(err); const errorMessage = - err?.status === 404 + status === 404 ? t('auth_login.oauth_callback_upgrade_hint', { defaultValue: 'Please update CLI Proxy API or check the connection.' }) - : err?.message; + : message || undefined; updateProviderState(provider, { callbackSubmitting: false, callbackStatus: 'error', @@ -236,15 +257,19 @@ export function OAuthPage() { })); showNotification(`${t('auth_login.iflow_cookie_status_error')} ${res.error || ''}`, 'error'); } - } catch (err: any) { - if (err?.status === 409) { + } catch (err: unknown) { + if (getErrorStatus(err) === 409) { const message = t('auth_login.iflow_cookie_config_duplicate'); setIflowCookie((prev) => ({ ...prev, loading: false, error: message, errorType: 'warning' })); showNotification(message, 'warning'); return; } - setIflowCookie((prev) => ({ ...prev, loading: false, error: err?.message, errorType: 'error' })); - showNotification(`${t('auth_login.iflow_cookie_start_error')} ${err?.message || ''}`, 'error'); + const message = getErrorMessage(err); + setIflowCookie((prev) => ({ ...prev, loading: false, error: message, errorType: 'error' })); + showNotification( + `${t('auth_login.iflow_cookie_start_error')}${message ? ` ${message}` : ''}`, + 'error' + ); } }; @@ -292,8 +317,8 @@ export function OAuthPage() { }; setVertexState((prev) => ({ ...prev, loading: false, result })); showNotification(t('vertex_import.success'), 'success'); - } catch (err: any) { - const message = err?.message || ''; + } catch (err: unknown) { + const message = getErrorMessage(err); setVertexState((prev) => ({ ...prev, loading: false, diff --git a/src/pages/SystemPage.tsx b/src/pages/SystemPage.tsx index e4f3069..b1bea3a 100644 --- a/src/pages/SystemPage.tsx +++ b/src/pages/SystemPage.tsx @@ -83,14 +83,23 @@ export function SystemPage() { return resolvedTheme === 'dark' ? iconEntry.dark : iconEntry.light; }; - const normalizeApiKeyList = (input: any): string[] => { + const normalizeApiKeyList = (input: unknown): string[] => { if (!Array.isArray(input)) return []; const seen = new Set(); const keys: string[] = []; input.forEach((item) => { - const value = typeof item === 'string' ? item : item?.['api-key'] ?? item?.apiKey ?? ''; - const trimmed = String(value || '').trim(); + 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); @@ -151,9 +160,12 @@ export function SystemPage() { type: hasModels ? 'success' : 'warning', message: hasModels ? t('system_info.models_count', { count: list.length }) : t('system_info.models_empty') }); - } catch (err: any) { - const message = `${t('system_info.models_error')}: ${err?.message || ''}`; - setModelStatus({ type: 'error', message }); + } catch (err: unknown) { + const message = + err instanceof Error ? err.message : typeof err === 'string' ? err : ''; + const suffix = message ? `: ${message}` : ''; + const text = `${t('system_info.models_error')}${suffix}`; + setModelStatus({ type: 'error', message: text }); } }; @@ -219,9 +231,14 @@ export function SystemPage() { clearCache('request-log'); showNotification(t('notification.request_log_updated'), 'success'); setRequestLogModalOpen(false); - } catch (error: any) { + } catch (error: unknown) { + const message = + error instanceof Error ? error.message : typeof error === 'string' ? error : ''; updateConfigValue('request-log', previous); - showNotification(`${t('notification.update_failed')}: ${error?.message || ''}`, 'error'); + showNotification( + `${t('notification.update_failed')}${message ? `: ${message}` : ''}`, + 'error' + ); } finally { setRequestLogSaving(false); } @@ -282,11 +299,11 @@ export function SystemPage() {
{buildTime}
-
-
{t('connection.status')}
-
{t(`common.${auth.connectionStatus}_status` as any)}
-
{auth.apiBase || '-'}
-
+
+
{t('connection.status')}
+
{t(`common.${auth.connectionStatus}_status`)}
+
{auth.apiBase || '-'}
+
diff --git a/src/services/api/ampcode.ts b/src/services/api/ampcode.ts index 668c558..392df6b 100644 --- a/src/services/api/ampcode.ts +++ b/src/services/api/ampcode.ts @@ -19,7 +19,7 @@ export const ampcodeApi = { clearUpstreamApiKey: () => apiClient.delete('/ampcode/upstream-api-key'), async getModelMappings(): Promise { - const data = await apiClient.get('/ampcode/model-mappings'); + const data = await apiClient.get>('/ampcode/model-mappings'); const list = data?.['model-mappings'] ?? data?.modelMappings ?? data?.items ?? data; return normalizeAmpcodeModelMappings(list); }, diff --git a/src/services/api/apiCall.ts b/src/services/api/apiCall.ts index 8bd7d20..043ae7c 100644 --- a/src/services/api/apiCall.ts +++ b/src/services/api/apiCall.ts @@ -13,14 +13,14 @@ export interface ApiCallRequest { data?: string; } -export interface ApiCallResult { +export interface ApiCallResult { statusCode: number; header: Record; bodyText: string; body: T | null; } -const normalizeBody = (input: unknown): { bodyText: string; body: any | null } => { +const normalizeBody = (input: unknown): { bodyText: string; body: unknown | null } => { if (input === undefined || input === null) { return { bodyText: '', body: null }; } @@ -46,13 +46,24 @@ const normalizeBody = (input: unknown): { bodyText: string; body: any | null } = }; export const getApiCallErrorMessage = (result: ApiCallResult): string => { + const isRecord = (value: unknown): value is Record => + value !== null && typeof value === 'object'; + const status = result.statusCode; const body = result.body; const bodyText = result.bodyText; let message = ''; - if (body && typeof body === 'object') { - message = body?.error?.message || body?.error || body?.message || ''; + if (isRecord(body)) { + const errorValue = body.error; + if (isRecord(errorValue) && typeof errorValue.message === 'string') { + message = errorValue.message; + } else if (typeof errorValue === 'string') { + message = errorValue; + } + if (!message && typeof body.message === 'string') { + message = body.message; + } } else if (typeof body === 'string') { message = body; } @@ -71,7 +82,7 @@ export const apiCallApi = { payload: ApiCallRequest, config?: AxiosRequestConfig ): Promise => { - const response = await apiClient.post('/api-call', payload, config); + const response = await apiClient.post>('/api-call', payload, config); const statusCode = Number(response?.status_code ?? response?.statusCode ?? 0); const header = (response?.header ?? response?.headers ?? {}) as Record; const { bodyText, body } = normalizeBody(response?.body); diff --git a/src/services/api/apiKeys.ts b/src/services/api/apiKeys.ts index a88ceb4..2672630 100644 --- a/src/services/api/apiKeys.ts +++ b/src/services/api/apiKeys.ts @@ -6,9 +6,9 @@ import { apiClient } from './client'; export const apiKeysApi = { async list(): Promise { - const data = await apiClient.get('/api-keys'); - const keys = (data && (data['api-keys'] ?? data.apiKeys)) as unknown; - return Array.isArray(keys) ? (keys as string[]) : []; + const data = await apiClient.get>('/api-keys'); + const keys = data['api-keys'] ?? data.apiKeys; + return Array.isArray(keys) ? keys.map((key) => String(key)) : []; }, replace: (keys: string[]) => apiClient.put('/api-keys', keys), diff --git a/src/services/api/authFiles.ts b/src/services/api/authFiles.ts index 05a6dd7..8d165ba 100644 --- a/src/services/api/authFiles.ts +++ b/src/services/api/authFiles.ts @@ -171,15 +171,25 @@ export const authFilesApi = { // 获取认证凭证支持的模型 async getModelsForAuthFile(name: string): Promise<{ id: string; display_name?: string; type?: string; owned_by?: string }[]> { - const data = await apiClient.get(`/auth-files/models?name=${encodeURIComponent(name)}`); - return (data && Array.isArray(data['models'])) ? data['models'] : []; + const data = await apiClient.get>( + `/auth-files/models?name=${encodeURIComponent(name)}` + ); + const models = data.models ?? data['models']; + return Array.isArray(models) + ? (models as { id: string; display_name?: string; type?: string; owned_by?: string }[]) + : []; }, // 获取指定 channel 的模型定义 async getModelDefinitions(channel: string): Promise<{ id: string; display_name?: string; type?: string; owned_by?: string }[]> { const normalizedChannel = String(channel ?? '').trim().toLowerCase(); if (!normalizedChannel) return []; - const data = await apiClient.get(`/model-definitions/${encodeURIComponent(normalizedChannel)}`); - return (data && Array.isArray(data['models'])) ? data['models'] : []; + const data = await apiClient.get>( + `/model-definitions/${encodeURIComponent(normalizedChannel)}` + ); + const models = data.models ?? data['models']; + return Array.isArray(models) + ? (models as { id: string; display_name?: string; type?: string; owned_by?: string }[]) + : []; } }; diff --git a/src/services/api/client.ts b/src/services/api/client.ts index 79bc591..38f89ab 100644 --- a/src/services/api/client.ts +++ b/src/services/api/client.ts @@ -62,7 +62,10 @@ class ApiClient { return `${normalized}${MANAGEMENT_API_PREFIX}`; } - private readHeader(headers: Record | undefined, keys: string[]): string | null { + private readHeader( + headers: Record | undefined, + keys: string[] + ): string | null { if (!headers) return null; const normalizeValue = (value: unknown): string | null => { @@ -75,7 +78,7 @@ class ApiClient { return text ? text : null; }; - const headerGetter = (headers as { get?: (name: string) => any }).get; + const headerGetter = (headers as { get?: (name: string) => unknown }).get; if (typeof headerGetter === 'function') { for (const key of keys) { const match = normalizeValue(headerGetter.call(headers, key)); @@ -84,8 +87,8 @@ class ApiClient { } const entries = - typeof (headers as { entries?: () => Iterable<[string, any]> }).entries === 'function' - ? Array.from((headers as { entries: () => Iterable<[string, any]> }).entries()) + typeof (headers as { entries?: () => Iterable<[string, unknown]> }).entries === 'function' + ? Array.from((headers as { entries: () => Iterable<[string, unknown]> }).entries()) : Object.entries(headers); const normalized = Object.fromEntries( @@ -147,10 +150,22 @@ class ApiClient { /** * 错误处理 */ - private handleError(error: any): ApiError { + private handleError(error: unknown): ApiError { + const isRecord = (value: unknown): value is Record => + value !== null && typeof value === 'object'; + if (axios.isAxiosError(error)) { - const responseData = error.response?.data as any; - const message = responseData?.error || responseData?.message || error.message || 'Request failed'; + const responseData: unknown = error.response?.data; + const responseRecord = isRecord(responseData) ? responseData : null; + const errorValue = responseRecord?.error; + const message = + typeof errorValue === 'string' + ? errorValue + : isRecord(errorValue) && typeof errorValue.message === 'string' + ? errorValue.message + : typeof responseRecord?.message === 'string' + ? responseRecord.message + : error.message || 'Request failed'; const apiError = new Error(message) as ApiError; apiError.name = 'ApiError'; apiError.status = error.response?.status; @@ -166,7 +181,9 @@ class ApiClient { return apiError; } - const fallback = new Error(error?.message || 'Unknown error occurred') as ApiError; + const fallbackMessage = + error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error occurred'; + const fallback = new Error(fallbackMessage) as ApiError; fallback.name = 'ApiError'; return fallback; } @@ -174,7 +191,7 @@ class ApiClient { /** * GET 请求 */ - async get(url: string, config?: AxiosRequestConfig): Promise { + async get(url: string, config?: AxiosRequestConfig): Promise { const response = await this.instance.get(url, config); return response.data; } @@ -182,7 +199,7 @@ class ApiClient { /** * POST 请求 */ - async post(url: string, data?: any, config?: AxiosRequestConfig): Promise { + async post(url: string, data?: unknown, config?: AxiosRequestConfig): Promise { const response = await this.instance.post(url, data, config); return response.data; } @@ -190,7 +207,7 @@ class ApiClient { /** * PUT 请求 */ - async put(url: string, data?: any, config?: AxiosRequestConfig): Promise { + async put(url: string, data?: unknown, config?: AxiosRequestConfig): Promise { const response = await this.instance.put(url, data, config); return response.data; } @@ -198,7 +215,7 @@ class ApiClient { /** * PATCH 请求 */ - async patch(url: string, data?: any, config?: AxiosRequestConfig): Promise { + async patch(url: string, data?: unknown, config?: AxiosRequestConfig): Promise { const response = await this.instance.patch(url, data, config); return response.data; } @@ -206,7 +223,7 @@ class ApiClient { /** * DELETE 请求 */ - async delete(url: string, config?: AxiosRequestConfig): Promise { + async delete(url: string, config?: AxiosRequestConfig): Promise { const response = await this.instance.delete(url, config); return response.data; } @@ -221,7 +238,11 @@ class ApiClient { /** * 发送 FormData */ - async postForm(url: string, formData: FormData, config?: AxiosRequestConfig): Promise { + async postForm( + url: string, + formData: FormData, + config?: AxiosRequestConfig + ): Promise { const response = await this.instance.post(url, formData, { ...config, headers: { diff --git a/src/services/api/config.ts b/src/services/api/config.ts index 84df137..16096dc 100644 --- a/src/services/api/config.ts +++ b/src/services/api/config.ts @@ -72,8 +72,10 @@ export const configApi = { * 获取日志总大小上限(MB) */ async getLogsMaxTotalSizeMb(): Promise { - const data = await apiClient.get('/logs-max-total-size-mb'); - return data?.['logs-max-total-size-mb'] ?? data?.logsMaxTotalSizeMb ?? 0; + const data = await apiClient.get>('/logs-max-total-size-mb'); + const value = data?.['logs-max-total-size-mb'] ?? data?.logsMaxTotalSizeMb ?? 0; + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; }, /** @@ -91,8 +93,8 @@ export const configApi = { * 获取强制模型前缀开关 */ async getForceModelPrefix(): Promise { - const data = await apiClient.get('/force-model-prefix'); - return data?.['force-model-prefix'] ?? data?.forceModelPrefix ?? false; + const data = await apiClient.get>('/force-model-prefix'); + return Boolean(data?.['force-model-prefix'] ?? data?.forceModelPrefix ?? false); }, /** @@ -104,8 +106,9 @@ export const configApi = { * 获取路由策略 */ async getRoutingStrategy(): Promise { - const data = await apiClient.get('/routing/strategy'); - return data?.strategy ?? data?.['routing-strategy'] ?? data?.routingStrategy ?? 'round-robin'; + const data = await apiClient.get>('/routing/strategy'); + const strategy = data?.strategy ?? data?.['routing-strategy'] ?? data?.routingStrategy; + return typeof strategy === 'string' ? strategy : 'round-robin'; }, /** diff --git a/src/services/api/configFile.ts b/src/services/api/configFile.ts index d09e912..4e1734f 100644 --- a/src/services/api/configFile.ts +++ b/src/services/api/configFile.ts @@ -10,7 +10,7 @@ export const configFileApi = { responseType: 'text', headers: { Accept: 'application/yaml, text/yaml, text/plain' } }); - const data = response.data as any; + const data: unknown = response.data; if (typeof data === 'string') return data; if (data === undefined || data === null) return ''; return String(data); diff --git a/src/services/api/providers.ts b/src/services/api/providers.ts index 960852e..d56de47 100644 --- a/src/services/api/providers.ts +++ b/src/services/api/providers.ts @@ -18,12 +18,22 @@ import type { const serializeHeaders = (headers?: Record) => (headers && Object.keys(headers).length ? headers : undefined); +const isRecord = (value: unknown): value is Record => + value !== null && typeof value === 'object' && !Array.isArray(value); + +const extractArrayPayload = (data: unknown, key: string): unknown[] => { + if (Array.isArray(data)) return data; + if (!isRecord(data)) return []; + const candidate = data[key] ?? data.items ?? data.data ?? data; + return Array.isArray(candidate) ? candidate : []; +}; + const serializeModelAliases = (models?: ModelAlias[]) => Array.isArray(models) ? models .map((model) => { if (!model?.name) return null; - const payload: Record = { name: model.name }; + const payload: Record = { name: model.name }; if (model.alias && model.alias !== model.name) { payload.alias = model.alias; } @@ -39,7 +49,7 @@ const serializeModelAliases = (models?: ModelAlias[]) => : undefined; const serializeApiKeyEntry = (entry: ApiKeyEntry) => { - const payload: Record = { 'api-key': entry.apiKey }; + const payload: Record = { 'api-key': entry.apiKey }; if (entry.proxyUrl) payload['proxy-url'] = entry.proxyUrl; const headers = serializeHeaders(entry.headers); if (headers) payload.headers = headers; @@ -47,7 +57,7 @@ const serializeApiKeyEntry = (entry: ApiKeyEntry) => { }; const serializeProviderKey = (config: ProviderKeyConfig) => { - const payload: Record = { 'api-key': config.apiKey }; + const payload: Record = { 'api-key': config.apiKey }; if (config.prefix?.trim()) payload.prefix = config.prefix.trim(); if (config.baseUrl) payload['base-url'] = config.baseUrl; if (config.proxyUrl) payload['proxy-url'] = config.proxyUrl; @@ -74,7 +84,7 @@ const serializeVertexModelAliases = (models?: ModelAlias[]) => : undefined; const serializeVertexKey = (config: ProviderKeyConfig) => { - const payload: Record = { 'api-key': config.apiKey }; + const payload: Record = { 'api-key': config.apiKey }; if (config.prefix?.trim()) payload.prefix = config.prefix.trim(); if (config.baseUrl) payload['base-url'] = config.baseUrl; if (config.proxyUrl) payload['proxy-url'] = config.proxyUrl; @@ -86,7 +96,7 @@ const serializeVertexKey = (config: ProviderKeyConfig) => { }; const serializeGeminiKey = (config: GeminiKeyConfig) => { - const payload: Record = { 'api-key': config.apiKey }; + const payload: Record = { 'api-key': config.apiKey }; if (config.prefix?.trim()) payload.prefix = config.prefix.trim(); if (config.baseUrl) payload['base-url'] = config.baseUrl; const headers = serializeHeaders(config.headers); @@ -98,7 +108,7 @@ const serializeGeminiKey = (config: GeminiKeyConfig) => { }; const serializeOpenAIProvider = (provider: OpenAIProviderConfig) => { - const payload: Record = { + const payload: Record = { name: provider.name, 'base-url': provider.baseUrl, 'api-key-entries': Array.isArray(provider.apiKeyEntries) @@ -118,8 +128,7 @@ const serializeOpenAIProvider = (provider: OpenAIProviderConfig) => { export const providersApi = { async getGeminiKeys(): Promise { const data = await apiClient.get('/gemini-api-key'); - const list = (data && (data['gemini-api-key'] ?? data.items ?? data)) as any; - if (!Array.isArray(list)) return []; + const list = extractArrayPayload(data, 'gemini-api-key'); return list.map((item) => normalizeGeminiKeyConfig(item)).filter(Boolean) as GeminiKeyConfig[]; }, @@ -134,8 +143,7 @@ export const providersApi = { async getCodexConfigs(): Promise { const data = await apiClient.get('/codex-api-key'); - const list = (data && (data['codex-api-key'] ?? data.items ?? data)) as any; - if (!Array.isArray(list)) return []; + const list = extractArrayPayload(data, 'codex-api-key'); return list.map((item) => normalizeProviderKeyConfig(item)).filter(Boolean) as ProviderKeyConfig[]; }, @@ -150,8 +158,7 @@ export const providersApi = { async getClaudeConfigs(): Promise { const data = await apiClient.get('/claude-api-key'); - const list = (data && (data['claude-api-key'] ?? data.items ?? data)) as any; - if (!Array.isArray(list)) return []; + const list = extractArrayPayload(data, 'claude-api-key'); return list.map((item) => normalizeProviderKeyConfig(item)).filter(Boolean) as ProviderKeyConfig[]; }, @@ -166,8 +173,7 @@ export const providersApi = { async getVertexConfigs(): Promise { const data = await apiClient.get('/vertex-api-key'); - const list = (data && (data['vertex-api-key'] ?? data.items ?? data)) as any; - if (!Array.isArray(list)) return []; + const list = extractArrayPayload(data, 'vertex-api-key'); return list.map((item) => normalizeProviderKeyConfig(item)).filter(Boolean) as ProviderKeyConfig[]; }, @@ -182,8 +188,7 @@ export const providersApi = { async getOpenAIProviders(): Promise { const data = await apiClient.get('/openai-compatibility'); - const list = (data && (data['openai-compatibility'] ?? data.items ?? data)) as any; - if (!Array.isArray(list)) return []; + const list = extractArrayPayload(data, 'openai-compatibility'); return list.map((item) => normalizeOpenAIProvider(item)).filter(Boolean) as OpenAIProviderConfig[]; }, diff --git a/src/services/api/transformers.ts b/src/services/api/transformers.ts index c99686c..b6b5830 100644 --- a/src/services/api/transformers.ts +++ b/src/services/api/transformers.ts @@ -10,7 +10,10 @@ import type { import type { Config } from '@/types/config'; import { buildHeaderObject } from '@/utils/headers'; -const normalizeBoolean = (value: any): boolean | undefined => { +const isRecord = (value: unknown): value is Record => + value !== null && typeof value === 'object' && !Array.isArray(value); + +const normalizeBoolean = (value: unknown): boolean | undefined => { if (value === undefined || value === null) return undefined; if (typeof value === 'boolean') return value; if (typeof value === 'number') return value !== 0; @@ -22,11 +25,17 @@ const normalizeBoolean = (value: any): boolean | undefined => { return Boolean(value); }; -const normalizeModelAliases = (models: any): ModelAlias[] => { +const normalizeModelAliases = (models: unknown): ModelAlias[] => { if (!Array.isArray(models)) return []; return models .map((item) => { - if (!item) return null; + if (item === undefined || item === null) return null; + if (typeof item === 'string') { + const trimmed = item.trim(); + return trimmed ? ({ name: trimmed } satisfies ModelAlias) : null; + } + if (!isRecord(item)) return null; + const name = item.name || item.id || item.model; if (!name) return null; const alias = item.alias || item.display_name || item.displayName; @@ -37,7 +46,10 @@ const normalizeModelAliases = (models: any): ModelAlias[] => { entry.alias = String(alias); } if (priority !== undefined) { - entry.priority = Number(priority); + const parsed = Number(priority); + if (Number.isFinite(parsed)) { + entry.priority = parsed; + } } if (testModel) { entry.testModel = String(testModel); @@ -47,13 +59,17 @@ const normalizeModelAliases = (models: any): ModelAlias[] => { .filter(Boolean) as ModelAlias[]; }; -const normalizeHeaders = (headers: any) => { +const normalizeHeaders = (headers: unknown) => { if (!headers || typeof headers !== 'object') return undefined; - const normalized = buildHeaderObject(headers as Record); + const normalized = buildHeaderObject( + Array.isArray(headers) + ? (headers as Array<{ key: string; value: string }>) + : (headers as Record) + ); return Object.keys(normalized).length ? normalized : undefined; }; -const normalizeExcludedModels = (input: any): string[] => { +const normalizeExcludedModels = (input: unknown): string[] => { const rawList = Array.isArray(input) ? input : typeof input === 'string' ? input.split(/[\n,]/) : []; const seen = new Set(); const normalized: string[] = []; @@ -70,20 +86,22 @@ const normalizeExcludedModels = (input: any): string[] => { return normalized; }; -const normalizePrefix = (value: any): string | undefined => { +const normalizePrefix = (value: unknown): string | undefined => { if (value === undefined || value === null) return undefined; const trimmed = String(value).trim(); return trimmed ? trimmed : undefined; }; -const normalizeApiKeyEntry = (entry: any): ApiKeyEntry | null => { - if (!entry) return null; - const apiKey = entry['api-key'] ?? entry.apiKey ?? entry.key ?? (typeof entry === 'string' ? entry : ''); +const normalizeApiKeyEntry = (entry: unknown): ApiKeyEntry | null => { + if (entry === undefined || entry === null) return null; + const record = isRecord(entry) ? entry : null; + const apiKey = + record?.['api-key'] ?? record?.apiKey ?? record?.key ?? (typeof entry === 'string' ? entry : ''); const trimmed = String(apiKey || '').trim(); if (!trimmed) return null; - const proxyUrl = entry['proxy-url'] ?? entry.proxyUrl; - const headers = normalizeHeaders(entry.headers); + const proxyUrl = record ? record['proxy-url'] ?? record.proxyUrl : undefined; + const headers = record ? normalizeHeaders(record.headers) : undefined; return { apiKey: trimmed, @@ -92,33 +110,38 @@ const normalizeApiKeyEntry = (entry: any): ApiKeyEntry | null => { }; }; -const normalizeProviderKeyConfig = (item: any): ProviderKeyConfig | null => { - if (!item) return null; - const apiKey = item['api-key'] ?? item.apiKey ?? (typeof item === 'string' ? item : ''); +const normalizeProviderKeyConfig = (item: unknown): ProviderKeyConfig | null => { + if (item === undefined || item === null) return null; + const record = isRecord(item) ? item : null; + const apiKey = record?.['api-key'] ?? record?.apiKey ?? (typeof item === 'string' ? item : ''); const trimmed = String(apiKey || '').trim(); if (!trimmed) return null; const config: ProviderKeyConfig = { apiKey: trimmed }; - const prefix = normalizePrefix(item.prefix ?? item['prefix']); + const prefix = normalizePrefix(record?.prefix ?? record?.['prefix']); if (prefix) config.prefix = prefix; - const baseUrl = item['base-url'] ?? item.baseUrl; - const proxyUrl = item['proxy-url'] ?? item.proxyUrl; + const baseUrl = record ? record['base-url'] ?? record.baseUrl : undefined; + const proxyUrl = record ? record['proxy-url'] ?? record.proxyUrl : undefined; if (baseUrl) config.baseUrl = String(baseUrl); if (proxyUrl) config.proxyUrl = String(proxyUrl); - const headers = normalizeHeaders(item.headers); + const headers = normalizeHeaders(record?.headers); if (headers) config.headers = headers; - const models = normalizeModelAliases(item.models); + const models = normalizeModelAliases(record?.models); if (models.length) config.models = models; const excludedModels = normalizeExcludedModels( - item['excluded-models'] ?? item.excludedModels ?? item['excluded_models'] ?? item.excluded_models + record?.['excluded-models'] ?? + record?.excludedModels ?? + record?.['excluded_models'] ?? + record?.excluded_models ); if (excludedModels.length) config.excludedModels = excludedModels; return config; }; -const normalizeGeminiKeyConfig = (item: any): GeminiKeyConfig | null => { - if (!item) return null; - let apiKey = item['api-key'] ?? item.apiKey; +const normalizeGeminiKeyConfig = (item: unknown): GeminiKeyConfig | null => { + if (item === undefined || item === null) return null; + const record = isRecord(item) ? item : null; + let apiKey = record?.['api-key'] ?? record?.apiKey; if (!apiKey && typeof item === 'string') { apiKey = item; } @@ -126,19 +149,19 @@ const normalizeGeminiKeyConfig = (item: any): GeminiKeyConfig | null => { if (!trimmed) return null; const config: GeminiKeyConfig = { apiKey: trimmed }; - const prefix = normalizePrefix(item.prefix ?? item['prefix']); + const prefix = normalizePrefix(record?.prefix ?? record?.['prefix']); if (prefix) config.prefix = prefix; - const baseUrl = item['base-url'] ?? item.baseUrl ?? item['base_url']; + const baseUrl = record ? record['base-url'] ?? record.baseUrl ?? record['base_url'] : undefined; if (baseUrl) config.baseUrl = String(baseUrl); - const headers = normalizeHeaders(item.headers); + const headers = normalizeHeaders(record?.headers); if (headers) config.headers = headers; - const excludedModels = normalizeExcludedModels(item['excluded-models'] ?? item.excludedModels); + const excludedModels = normalizeExcludedModels(record?.['excluded-models'] ?? record?.excludedModels); if (excludedModels.length) config.excludedModels = excludedModels; return config; }; -const normalizeOpenAIProvider = (provider: any): OpenAIProviderConfig | null => { - if (!provider || typeof provider !== 'object') return null; +const normalizeOpenAIProvider = (provider: unknown): OpenAIProviderConfig | null => { + if (!isRecord(provider)) return null; const name = provider.name || provider.id; const baseUrl = provider['base-url'] ?? provider.baseUrl; if (!name || !baseUrl) return null; @@ -146,11 +169,11 @@ const normalizeOpenAIProvider = (provider: any): OpenAIProviderConfig | null => let apiKeyEntries: ApiKeyEntry[] = []; if (Array.isArray(provider['api-key-entries'])) { apiKeyEntries = provider['api-key-entries'] - .map((entry: any) => normalizeApiKeyEntry(entry)) + .map((entry) => normalizeApiKeyEntry(entry)) .filter(Boolean) as ApiKeyEntry[]; } else if (Array.isArray(provider['api-keys'])) { apiKeyEntries = provider['api-keys'] - .map((key: any) => normalizeApiKeyEntry({ 'api-key': key })) + .map((key) => normalizeApiKeyEntry({ 'api-key': key })) .filter(Boolean) as ApiKeyEntry[]; } @@ -174,10 +197,10 @@ const normalizeOpenAIProvider = (provider: any): OpenAIProviderConfig | null => return result; }; -const normalizeOauthExcluded = (payload: any): Record | undefined => { - if (!payload || typeof payload !== 'object') return undefined; +const normalizeOauthExcluded = (payload: unknown): Record | undefined => { + if (!isRecord(payload)) return undefined; const source = payload['oauth-excluded-models'] ?? payload.items ?? payload; - if (!source || typeof source !== 'object') return undefined; + if (!isRecord(source)) return undefined; const map: Record = {}; Object.entries(source).forEach(([provider, models]) => { const key = String(provider || '').trim(); @@ -188,13 +211,13 @@ const normalizeOauthExcluded = (payload: any): Record | undefi return map; }; -const normalizeAmpcodeModelMappings = (input: any): AmpcodeModelMapping[] => { +const normalizeAmpcodeModelMappings = (input: unknown): AmpcodeModelMapping[] => { if (!Array.isArray(input)) return []; const seen = new Set(); const mappings: AmpcodeModelMapping[] = []; input.forEach((entry) => { - if (!entry || typeof entry !== 'object') return; + if (!isRecord(entry)) return; const from = String(entry.from ?? entry['from'] ?? '').trim(); const to = String(entry.to ?? entry['to'] ?? '').trim(); if (!from || !to) return; @@ -207,9 +230,10 @@ const normalizeAmpcodeModelMappings = (input: any): AmpcodeModelMapping[] => { return mappings; }; -const normalizeAmpcodeConfig = (payload: any): AmpcodeConfig | undefined => { - const source = payload?.ampcode ?? payload; - if (!source || typeof source !== 'object') return undefined; +const normalizeAmpcodeConfig = (payload: unknown): AmpcodeConfig | undefined => { + const sourceRaw = isRecord(payload) ? (payload.ampcode ?? payload) : payload; + if (!isRecord(sourceRaw)) return undefined; + const source = sourceRaw; const config: AmpcodeConfig = {}; const upstreamUrl = source['upstream-url'] ?? source.upstreamUrl ?? source['upstream_url']; @@ -237,70 +261,94 @@ const normalizeAmpcodeConfig = (payload: any): AmpcodeConfig | undefined => { /** * 规范化 /config 返回值 */ -export const normalizeConfigResponse = (raw: any): Config => { - const config: Config = { raw: raw || {} }; - if (!raw || typeof raw !== 'object') { +export const normalizeConfigResponse = (raw: unknown): Config => { + const config: Config = { raw: isRecord(raw) ? raw : {} }; + if (!isRecord(raw)) { return config; } - config.debug = raw.debug; - config.proxyUrl = raw['proxy-url'] ?? raw.proxyUrl; - config.requestRetry = raw['request-retry'] ?? raw.requestRetry; + config.debug = normalizeBoolean(raw.debug); + const proxyUrl = raw['proxy-url'] ?? raw.proxyUrl; + config.proxyUrl = + typeof proxyUrl === 'string' ? proxyUrl : proxyUrl === undefined || proxyUrl === null ? undefined : String(proxyUrl); + const requestRetry = raw['request-retry'] ?? raw.requestRetry; + if (typeof requestRetry === 'number' && Number.isFinite(requestRetry)) { + config.requestRetry = requestRetry; + } else if (typeof requestRetry === 'string' && requestRetry.trim() !== '') { + const parsed = Number(requestRetry); + if (Number.isFinite(parsed)) { + config.requestRetry = parsed; + } + } const quota = raw['quota-exceeded'] ?? raw.quotaExceeded; - if (quota && typeof quota === 'object') { + if (isRecord(quota)) { config.quotaExceeded = { - switchProject: quota['switch-project'] ?? quota.switchProject, - switchPreviewModel: quota['switch-preview-model'] ?? quota.switchPreviewModel + switchProject: normalizeBoolean(quota['switch-project'] ?? quota.switchProject), + switchPreviewModel: normalizeBoolean(quota['switch-preview-model'] ?? quota.switchPreviewModel) }; } - config.usageStatisticsEnabled = raw['usage-statistics-enabled'] ?? raw.usageStatisticsEnabled; - config.requestLog = raw['request-log'] ?? raw.requestLog; - config.loggingToFile = raw['logging-to-file'] ?? raw.loggingToFile; - config.logsMaxTotalSizeMb = raw['logs-max-total-size-mb'] ?? raw.logsMaxTotalSizeMb; - config.wsAuth = raw['ws-auth'] ?? raw.wsAuth; - config.forceModelPrefix = raw['force-model-prefix'] ?? raw.forceModelPrefix; - const routing = raw.routing; - if (routing && typeof routing === 'object') { - config.routingStrategy = routing.strategy ?? routing['strategy']; - } else { - config.routingStrategy = raw['routing-strategy'] ?? raw.routingStrategy; + config.usageStatisticsEnabled = normalizeBoolean( + raw['usage-statistics-enabled'] ?? raw.usageStatisticsEnabled + ); + config.requestLog = normalizeBoolean(raw['request-log'] ?? raw.requestLog); + config.loggingToFile = normalizeBoolean(raw['logging-to-file'] ?? raw.loggingToFile); + const logsMaxTotalSizeMb = raw['logs-max-total-size-mb'] ?? raw.logsMaxTotalSizeMb; + if (typeof logsMaxTotalSizeMb === 'number' && Number.isFinite(logsMaxTotalSizeMb)) { + config.logsMaxTotalSizeMb = logsMaxTotalSizeMb; + } else if (typeof logsMaxTotalSizeMb === 'string' && logsMaxTotalSizeMb.trim() !== '') { + const parsed = Number(logsMaxTotalSizeMb); + if (Number.isFinite(parsed)) { + config.logsMaxTotalSizeMb = parsed; + } + } + config.wsAuth = normalizeBoolean(raw['ws-auth'] ?? raw.wsAuth); + config.forceModelPrefix = normalizeBoolean(raw['force-model-prefix'] ?? raw.forceModelPrefix); + const routing = raw.routing; + const strategyRaw = isRecord(routing) + ? (routing.strategy ?? routing['strategy']) + : (raw['routing-strategy'] ?? raw.routingStrategy); + if (strategyRaw !== undefined && strategyRaw !== null) { + config.routingStrategy = String(strategyRaw); + } + const apiKeysRaw = raw['api-keys'] ?? raw.apiKeys; + if (Array.isArray(apiKeysRaw)) { + config.apiKeys = apiKeysRaw.map((key) => String(key)).filter((key) => key.trim() !== ''); } - config.apiKeys = Array.isArray(raw['api-keys']) ? raw['api-keys'].slice() : raw.apiKeys; const geminiList = raw['gemini-api-key'] ?? raw.geminiApiKey ?? raw.geminiApiKeys; if (Array.isArray(geminiList)) { config.geminiApiKeys = geminiList - .map((item: any) => normalizeGeminiKeyConfig(item)) + .map((item) => normalizeGeminiKeyConfig(item)) .filter(Boolean) as GeminiKeyConfig[]; } const codexList = raw['codex-api-key'] ?? raw.codexApiKey ?? raw.codexApiKeys; if (Array.isArray(codexList)) { config.codexApiKeys = codexList - .map((item: any) => normalizeProviderKeyConfig(item)) + .map((item) => normalizeProviderKeyConfig(item)) .filter(Boolean) as ProviderKeyConfig[]; } const claudeList = raw['claude-api-key'] ?? raw.claudeApiKey ?? raw.claudeApiKeys; if (Array.isArray(claudeList)) { config.claudeApiKeys = claudeList - .map((item: any) => normalizeProviderKeyConfig(item)) + .map((item) => normalizeProviderKeyConfig(item)) .filter(Boolean) as ProviderKeyConfig[]; } const vertexList = raw['vertex-api-key'] ?? raw.vertexApiKey ?? raw.vertexApiKeys; if (Array.isArray(vertexList)) { config.vertexApiKeys = vertexList - .map((item: any) => normalizeProviderKeyConfig(item)) + .map((item) => normalizeProviderKeyConfig(item)) .filter(Boolean) as ProviderKeyConfig[]; } const openaiList = raw['openai-compatibility'] ?? raw.openaiCompatibility ?? raw.openAICompatibility; if (Array.isArray(openaiList)) { config.openaiCompatibility = openaiList - .map((item: any) => normalizeOpenAIProvider(item)) + .map((item) => normalizeOpenAIProvider(item)) .filter(Boolean) as OpenAIProviderConfig[]; } diff --git a/src/services/api/usage.ts b/src/services/api/usage.ts index 029ce09..6041c23 100644 --- a/src/services/api/usage.ts +++ b/src/services/api/usage.ts @@ -26,7 +26,7 @@ export const usageApi = { /** * 获取使用统计原始数据 */ - getUsage: () => apiClient.get('/usage', { timeout: USAGE_TIMEOUT_MS }), + getUsage: () => apiClient.get>('/usage', { timeout: USAGE_TIMEOUT_MS }), /** * 导出使用统计快照 @@ -42,10 +42,10 @@ export const usageApi = { /** * 计算密钥成功/失败统计,必要时会先获取 usage 数据 */ - async getKeyStats(usageData?: any): Promise { + async getKeyStats(usageData?: unknown): Promise { let payload = usageData; if (!payload) { - const response = await apiClient.get('/usage', { timeout: USAGE_TIMEOUT_MS }); + const response = await apiClient.get>('/usage', { timeout: USAGE_TIMEOUT_MS }); payload = response?.usage ?? response; } return computeKeyStats(payload); diff --git a/src/services/api/version.ts b/src/services/api/version.ts index feaa6ba..1731feb 100644 --- a/src/services/api/version.ts +++ b/src/services/api/version.ts @@ -5,5 +5,5 @@ import { apiClient } from './client'; export const versionApi = { - checkLatest: () => apiClient.get('/latest-version') + checkLatest: () => apiClient.get>('/latest-version') }; diff --git a/src/services/storage/secureStorage.ts b/src/services/storage/secureStorage.ts index 73852d3..8f6ea05 100644 --- a/src/services/storage/secureStorage.ts +++ b/src/services/storage/secureStorage.ts @@ -13,7 +13,7 @@ class SecureStorageService { /** * 存储数据 */ - setItem(key: string, value: any, options: StorageOptions = {}): void { + setItem(key: string, value: unknown, options: StorageOptions = {}): void { const { encrypt = true } = options; if (value === null || value === undefined) { @@ -30,7 +30,7 @@ class SecureStorageService { /** * 获取数据 */ - getItem(key: string, options: StorageOptions = {}): T | null { + getItem(key: string, options: StorageOptions = {}): T | null { const { encrypt = true } = options; const raw = localStorage.getItem(key); @@ -84,7 +84,7 @@ class SecureStorageService { return; } - let parsed: any = raw; + let parsed: unknown = raw; try { parsed = JSON.parse(raw); } catch { diff --git a/src/stores/useAuthStore.ts b/src/stores/useAuthStore.ts index dcd5721..cf3470b 100644 --- a/src/stores/useAuthStore.ts +++ b/src/stores/useAuthStore.ts @@ -117,10 +117,16 @@ export const useAuthStore = create()( } else { localStorage.removeItem('isLoggedIn'); } - } catch (error: any) { + } catch (error: unknown) { + const message = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : 'Connection failed'; set({ connectionStatus: 'error', - connectionError: error.message || 'Connection failed' + connectionError: message || 'Connection failed' }); throw error; } diff --git a/src/stores/useConfigStore.ts b/src/stores/useConfigStore.ts index 8df0d46..da8f2a0 100644 --- a/src/stores/useConfigStore.ts +++ b/src/stores/useConfigStore.ts @@ -10,7 +10,7 @@ import { configApi } from '@/services/api/config'; import { CACHE_EXPIRY_MS } from '@/utils/constants'; interface ConfigCache { - data: any; + data: unknown; timestamp: number; } @@ -21,8 +21,11 @@ interface ConfigState { error: string | null; // 操作 - fetchConfig: (section?: RawConfigSection, forceRefresh?: boolean) => Promise; - updateConfigValue: (section: RawConfigSection, value: any) => void; + fetchConfig: { + (section?: undefined, forceRefresh?: boolean): Promise; + (section: RawConfigSection, forceRefresh?: boolean): Promise; + }; + updateConfigValue: (section: RawConfigSection, value: unknown) => void; clearCache: (section?: RawConfigSection) => void; isCacheValid: (section?: RawConfigSection) => boolean; } @@ -105,7 +108,7 @@ export const useConfigStore = create((set, get) => ({ loading: false, error: null, - fetchConfig: async (section, forceRefresh = false) => { + fetchConfig: (async (section?: RawConfigSection, forceRefresh: boolean = false) => { const { cache, isCacheValid } = get(); // 检查缓存 @@ -163,10 +166,12 @@ export const useConfigStore = create((set, get) => ({ }); return section ? extractSectionValue(data, section) : data; - } catch (error: any) { + } catch (error: unknown) { + const message = + error instanceof Error ? error.message : typeof error === 'string' ? error : 'Failed to fetch config'; if (requestId === configRequestToken) { set({ - error: error.message || 'Failed to fetch config', + error: message || 'Failed to fetch config', loading: false }); } @@ -176,7 +181,7 @@ export const useConfigStore = create((set, get) => ({ inFlightConfigRequest = null; } } - }, + }) as ConfigState['fetchConfig'], updateConfigValue: (section, value) => { set((state) => { @@ -186,61 +191,61 @@ export const useConfigStore = create((set, get) => ({ switch (section) { case 'debug': - nextConfig.debug = value; + nextConfig.debug = value as Config['debug']; break; case 'proxy-url': - nextConfig.proxyUrl = value; + nextConfig.proxyUrl = value as Config['proxyUrl']; break; case 'request-retry': - nextConfig.requestRetry = value; + nextConfig.requestRetry = value as Config['requestRetry']; break; case 'quota-exceeded': - nextConfig.quotaExceeded = value; + nextConfig.quotaExceeded = value as Config['quotaExceeded']; break; case 'usage-statistics-enabled': - nextConfig.usageStatisticsEnabled = value; + nextConfig.usageStatisticsEnabled = value as Config['usageStatisticsEnabled']; break; case 'request-log': - nextConfig.requestLog = value; + nextConfig.requestLog = value as Config['requestLog']; break; case 'logging-to-file': - nextConfig.loggingToFile = value; + nextConfig.loggingToFile = value as Config['loggingToFile']; break; case 'logs-max-total-size-mb': - nextConfig.logsMaxTotalSizeMb = value; + nextConfig.logsMaxTotalSizeMb = value as Config['logsMaxTotalSizeMb']; break; case 'ws-auth': - nextConfig.wsAuth = value; + nextConfig.wsAuth = value as Config['wsAuth']; break; case 'force-model-prefix': - nextConfig.forceModelPrefix = value; + nextConfig.forceModelPrefix = value as Config['forceModelPrefix']; break; case 'routing/strategy': - nextConfig.routingStrategy = value; + nextConfig.routingStrategy = value as Config['routingStrategy']; break; case 'api-keys': - nextConfig.apiKeys = value; + nextConfig.apiKeys = value as Config['apiKeys']; break; case 'ampcode': - nextConfig.ampcode = value; + nextConfig.ampcode = value as Config['ampcode']; break; case 'gemini-api-key': - nextConfig.geminiApiKeys = value; + nextConfig.geminiApiKeys = value as Config['geminiApiKeys']; break; case 'codex-api-key': - nextConfig.codexApiKeys = value; + nextConfig.codexApiKeys = value as Config['codexApiKeys']; break; case 'claude-api-key': - nextConfig.claudeApiKeys = value; + nextConfig.claudeApiKeys = value as Config['claudeApiKeys']; break; case 'vertex-api-key': - nextConfig.vertexApiKeys = value; + nextConfig.vertexApiKeys = value as Config['vertexApiKeys']; break; case 'openai-compatibility': - nextConfig.openaiCompatibility = value; + nextConfig.openaiCompatibility = value as Config['openaiCompatibility']; break; case 'oauth-excluded-models': - nextConfig.oauthExcludedModels = value; + nextConfig.oauthExcludedModels = value as Config['oauthExcludedModels']; break; default: break; diff --git a/src/stores/useModelsStore.ts b/src/stores/useModelsStore.ts index 7a70d80..2c5a08d 100644 --- a/src/stores/useModelsStore.ts +++ b/src/stores/useModelsStore.ts @@ -52,8 +52,9 @@ export const useModelsStore = create((set, get) => ({ }); return list; - } catch (error: any) { - const message = error?.message || 'Failed to fetch models'; + } catch (error: unknown) { + const message = + error instanceof Error ? error.message : typeof error === 'string' ? error : 'Failed to fetch models'; set({ error: message, loading: false, diff --git a/src/types/api.ts b/src/types/api.ts index d5b3d50..7596f71 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -17,8 +17,8 @@ export interface ApiClientConfig { export interface RequestOptions { method?: HttpMethod; headers?: Record; - params?: Record; - data?: any; + params?: Record; + data?: unknown; } // 服务器版本信息 @@ -31,6 +31,6 @@ export interface ServerVersion { export type ApiError = Error & { status?: number; code?: string; - details?: any; - data?: any; + details?: unknown; + data?: unknown; }; diff --git a/src/types/authFile.ts b/src/types/authFile.ts index 2a56284..6431725 100644 --- a/src/types/authFile.ts +++ b/src/types/authFile.ts @@ -26,7 +26,7 @@ export interface AuthFileItem { runtimeOnly?: boolean | string; disabled?: boolean; modified?: number; - [key: string]: any; + [key: string]: unknown; } export interface AuthFilesResponse { diff --git a/src/types/common.ts b/src/types/common.ts index f78aa62..2ee1624 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -15,7 +15,7 @@ export interface Notification { duration?: number; } -export interface ApiResponse { +export interface ApiResponse { data?: T; error?: string; message?: string; diff --git a/src/types/config.ts b/src/types/config.ts index ddf29ef..a248efb 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -31,7 +31,7 @@ export interface Config { vertexApiKeys?: ProviderKeyConfig[]; openaiCompatibility?: OpenAIProviderConfig[]; oauthExcludedModels?: Record; - raw?: Record; + raw?: Record; } export type RawConfigSection = diff --git a/src/types/log.ts b/src/types/log.ts index bba56f8..73a0a4c 100644 --- a/src/types/log.ts +++ b/src/types/log.ts @@ -11,7 +11,7 @@ export interface LogEntry { timestamp: string; level: LogLevel; message: string; - details?: any; + details?: unknown; } // 日志筛选 diff --git a/src/types/provider.ts b/src/types/provider.ts index 85feacf..c0999f5 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -43,5 +43,5 @@ export interface OpenAIProviderConfig { models?: ModelAlias[]; priority?: number; testModel?: string; - [key: string]: any; + [key: string]: unknown; } diff --git a/src/types/visualConfig.ts b/src/types/visualConfig.ts index ff4242a..5aa91fe 100644 --- a/src/types/visualConfig.ts +++ b/src/types/visualConfig.ts @@ -10,7 +10,7 @@ export type PayloadParamEntry = { export type PayloadModelEntry = { id: string; name: string; - protocol?: 'openai' | 'gemini' | 'claude' | 'codex' | 'antigravity'; + protocol?: 'openai' | 'openai-response' | 'gemini' | 'claude' | 'codex' | 'antigravity'; }; export type PayloadRule = { diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 298e938..2b90768 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -15,13 +15,13 @@ export function normalizeArrayResponse(data: T | T[] | null | undefined): T[] /** * 防抖函数 */ -export function debounce any>( - func: T, +export function debounce( + func: (this: This, ...args: Args) => Return, delay: number -): (...args: Parameters) => void { +): (this: This, ...args: Args) => void { let timeoutId: ReturnType; - return function (this: any, ...args: Parameters) { + return function (this: This, ...args: Args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; @@ -30,13 +30,13 @@ export function debounce any>( /** * 节流函数 */ -export function throttle any>( - func: T, +export function throttle( + func: (this: This, ...args: Args) => Return, limit: number -): (...args: Parameters) => void { +): (this: This, ...args: Args) => void { let inThrottle: boolean; - return function (this: any, ...args: Parameters) { + return function (this: This, ...args: Args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; @@ -67,16 +67,17 @@ export function generateId(): string { export function deepClone(obj: T): T { if (obj === null || typeof obj !== 'object') return obj; - if (obj instanceof Date) return new Date(obj.getTime()) as any; - if (obj instanceof Array) return obj.map((item) => deepClone(item)) as any; + if (obj instanceof Date) return new Date(obj.getTime()) as unknown as T; + if (Array.isArray(obj)) return obj.map((item) => deepClone(item)) as unknown as T; - const clonedObj = {} as T; - for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - clonedObj[key] = deepClone((obj as any)[key]); + const source = obj as Record; + const cloned: Record = {}; + for (const key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + cloned[key] = deepClone(source[key]); } } - return clonedObj; + return cloned as unknown as T; } /** diff --git a/src/utils/models.ts b/src/utils/models.ts index d3d48a2..5049fa0 100644 --- a/src/utils/models.ts +++ b/src/utils/models.ts @@ -30,12 +30,15 @@ const matchCategory = (text: string) => { return null; }; -export function normalizeModelList(payload: any, { dedupe = false } = {}): ModelInfo[] { - const toModel = (entry: any): ModelInfo | null => { +const isRecord = (value: unknown): value is Record => + value !== null && typeof value === 'object' && !Array.isArray(value); + +export function normalizeModelList(payload: unknown, { dedupe = false } = {}): ModelInfo[] { + const toModel = (entry: unknown): ModelInfo | null => { if (typeof entry === 'string') { return { name: entry }; } - if (!entry || typeof entry !== 'object') { + if (!isRecord(entry)) { return null; } const name = entry.id || entry.name || entry.model || entry.value; @@ -57,7 +60,7 @@ export function normalizeModelList(payload: any, { dedupe = false } = {}): Model if (Array.isArray(payload)) { models = payload.map(toModel); - } else if (payload && typeof payload === 'object') { + } else if (isRecord(payload)) { if (Array.isArray(payload.data)) { models = payload.data.map(toModel); } else if (Array.isArray(payload.models)) { diff --git a/src/utils/usage.ts b/src/utils/usage.ts index ca50ed2..32888b1 100644 --- a/src/utils/usage.ts +++ b/src/utils/usage.ts @@ -64,7 +64,16 @@ export interface ApiStats { const TOKENS_PER_PRICE_UNIT = 1_000_000; const MODEL_PRICE_STORAGE_KEY = 'cli-proxy-model-prices-v2'; -const normalizeAuthIndex = (value: any) => { +const isRecord = (value: unknown): value is Record => + value !== null && typeof value === 'object' && !Array.isArray(value); + +const getApisRecord = (usageData: unknown): Record | null => { + const usageRecord = isRecord(usageData) ? usageData : null; + const apisRaw = usageRecord ? usageRecord.apis : null; + return isRecord(apisRaw) ? apisRaw : null; +}; + +const normalizeAuthIndex = (value: unknown) => { if (typeof value === 'number' && Number.isFinite(value)) { return value.toString(); } @@ -306,24 +315,29 @@ export function formatUsd(value: number): string { /** * 从使用数据中收集所有请求明细 */ -export function collectUsageDetails(usageData: any): UsageDetail[] { - if (!usageData) { - return []; - } - const apis = usageData.apis || {}; +export function collectUsageDetails(usageData: unknown): UsageDetail[] { + const apis = getApisRecord(usageData); + if (!apis) return []; const details: UsageDetail[] = []; - Object.values(apis as Record).forEach((apiEntry) => { - const models = apiEntry?.models || {}; - Object.entries(models as Record).forEach(([modelName, modelEntry]) => { - const modelDetails = Array.isArray(modelEntry.details) ? modelEntry.details : []; - modelDetails.forEach((detail: any) => { - if (detail && detail.timestamp) { - details.push({ - ...detail, - source: normalizeUsageSourceId(detail.source), - __modelName: modelName - }); - } + Object.values(apis).forEach((apiEntry) => { + if (!isRecord(apiEntry)) return; + const modelsRaw = apiEntry.models; + const models = isRecord(modelsRaw) ? modelsRaw : null; + if (!models) return; + + Object.entries(models).forEach(([modelName, modelEntry]) => { + if (!isRecord(modelEntry)) return; + const modelDetailsRaw = modelEntry.details; + const modelDetails = Array.isArray(modelDetailsRaw) ? modelDetailsRaw : []; + + modelDetails.forEach((detailRaw) => { + if (!isRecord(detailRaw) || typeof detailRaw.timestamp !== 'string') return; + const detail = detailRaw as unknown as UsageDetail; + details.push({ + ...detail, + source: normalizeUsageSourceId(detail.source), + __modelName: modelName, + }); }); }); }); @@ -333,8 +347,10 @@ export function collectUsageDetails(usageData: any): UsageDetail[] { /** * 从单条明细提取总 tokens */ -export function extractTotalTokens(detail: any): number { - const tokens = detail?.tokens || {}; +export function extractTotalTokens(detail: unknown): number { + const record = isRecord(detail) ? detail : null; + const tokensRaw = record?.tokens; + const tokens = isRecord(tokensRaw) ? tokensRaw : {}; if (typeof tokens.total_tokens === 'number') { return tokens.total_tokens; } @@ -352,7 +368,7 @@ export function extractTotalTokens(detail: any): number { /** * 计算 token 分类统计 */ -export function calculateTokenBreakdown(usageData: any): TokenBreakdown { +export function calculateTokenBreakdown(usageData: unknown): TokenBreakdown { const details = collectUsageDetails(usageData); if (!details.length) { return { cachedTokens: 0, reasoningTokens: 0 }; @@ -361,8 +377,8 @@ export function calculateTokenBreakdown(usageData: any): TokenBreakdown { let cachedTokens = 0; let reasoningTokens = 0; - details.forEach(detail => { - const tokens = detail?.tokens || {}; + details.forEach((detail) => { + const tokens = detail.tokens; cachedTokens += Math.max( typeof tokens.cached_tokens === 'number' ? Math.max(tokens.cached_tokens, 0) : 0, typeof tokens.cache_tokens === 'number' ? Math.max(tokens.cache_tokens, 0) : 0 @@ -378,7 +394,10 @@ export function calculateTokenBreakdown(usageData: any): TokenBreakdown { /** * 计算最近 N 分钟的 RPM/TPM */ -export function calculateRecentPerMinuteRates(windowMinutes: number = 30, usageData: any): RateStats { +export function calculateRecentPerMinuteRates( + windowMinutes: number = 30, + usageData: unknown +): RateStats { const details = collectUsageDetails(usageData); const effectiveWindow = Number.isFinite(windowMinutes) && windowMinutes > 0 ? windowMinutes : 30; @@ -391,7 +410,7 @@ export function calculateRecentPerMinuteRates(windowMinutes: number = 30, usageD let requestCount = 0; let tokenCount = 0; - details.forEach(detail => { + details.forEach((detail) => { const timestamp = Date.parse(detail.timestamp); if (Number.isNaN(timestamp) || timestamp < windowStart) { return; @@ -413,15 +432,16 @@ export function calculateRecentPerMinuteRates(windowMinutes: number = 30, usageD /** * 从使用数据获取模型名称列表 */ -export function getModelNamesFromUsage(usageData: any): string[] { - if (!usageData) { - return []; - } - const apis = usageData.apis || {}; +export function getModelNamesFromUsage(usageData: unknown): string[] { + const apis = getApisRecord(usageData); + if (!apis) return []; const names = new Set(); - Object.values(apis as Record).forEach(apiEntry => { - const models = apiEntry?.models || {}; - Object.keys(models).forEach(modelName => { + Object.values(apis).forEach((apiEntry) => { + if (!isRecord(apiEntry)) return; + const modelsRaw = apiEntry.models; + const models = isRecord(modelsRaw) ? modelsRaw : null; + if (!models) return; + Object.keys(models).forEach((modelName) => { if (modelName) { names.add(modelName); } @@ -433,13 +453,13 @@ export function getModelNamesFromUsage(usageData: any): string[] { /** * 计算成本数据 */ -export function calculateCost(detail: any, modelPrices: Record): number { +export function calculateCost(detail: UsageDetail, modelPrices: Record): number { const modelName = detail.__modelName || ''; const price = modelPrices[modelName]; if (!price) { return 0; } - const tokens = detail?.tokens || {}; + const tokens = detail.tokens; const rawInputTokens = Number(tokens.input_tokens); const rawCompletionTokens = Number(tokens.output_tokens); const rawCachedTokensPrimary = Number(tokens.cached_tokens); @@ -463,7 +483,7 @@ export function calculateCost(detail: any, modelPrices: Record): number { +export function calculateTotalCost(usageData: unknown, modelPrices: Record): number { const details = collectUsageDetails(usageData); if (!details.length || !Object.keys(modelPrices).length) { return 0; @@ -483,16 +503,17 @@ export function loadModelPrices(): Record { if (!raw) { return {}; } - const parsed = JSON.parse(raw); - if (!parsed || typeof parsed !== 'object') { + const parsed: unknown = JSON.parse(raw); + if (!isRecord(parsed)) { return {}; } const normalized: Record = {}; - Object.entries(parsed).forEach(([model, price]: [string, any]) => { + Object.entries(parsed).forEach(([model, price]: [string, unknown]) => { if (!model) return; - const promptRaw = Number(price?.prompt); - const completionRaw = Number(price?.completion); - const cacheRaw = Number(price?.cache); + const priceRecord = isRecord(price) ? price : null; + const promptRaw = Number(priceRecord?.prompt); + const completionRaw = Number(priceRecord?.completion); + const cacheRaw = Number(priceRecord?.cache); if (!Number.isFinite(promptRaw) && !Number.isFinite(completionRaw) && !Number.isFinite(cacheRaw)) { return; @@ -536,21 +557,21 @@ export function saveModelPrices(prices: Record): void { /** * 获取 API 统计数据 */ -export function getApiStats(usageData: any, modelPrices: Record): ApiStats[] { - if (!usageData?.apis) { - return []; - } - const apis = usageData.apis; +export function getApiStats(usageData: unknown, modelPrices: Record): ApiStats[] { + const apis = getApisRecord(usageData); + if (!apis) return []; const result: ApiStats[] = []; - Object.entries(apis as Record).forEach(([endpoint, apiData]) => { + Object.entries(apis).forEach(([endpoint, apiData]) => { + if (!isRecord(apiData)) return; const models: Record = {}; let derivedSuccessCount = 0; let derivedFailureCount = 0; let totalCost = 0; - const modelsData = apiData?.models || {}; - Object.entries(modelsData as Record).forEach(([modelName, modelData]) => { + const modelsData = isRecord(apiData.models) ? apiData.models : {}; + Object.entries(modelsData).forEach(([modelName, modelData]) => { + if (!isRecord(modelData)) return; const details = Array.isArray(modelData.details) ? modelData.details : []; const hasExplicitCounts = typeof modelData.success_count === 'number' || typeof modelData.failure_count === 'number'; @@ -564,46 +585,50 @@ export function getApiStats(usageData: any, modelPrices: Record 0 && (!hasExplicitCounts || price)) { - details.forEach((detail: any) => { + details.forEach((detail) => { + const detailRecord = isRecord(detail) ? detail : null; if (!hasExplicitCounts) { - if (detail?.failed === true) { + if (detailRecord?.failed === true) { failureCount += 1; } else { successCount += 1; } } - if (price) { - totalCost += calculateCost({ ...detail, __modelName: modelName }, modelPrices); + if (price && detailRecord) { + totalCost += calculateCost( + { ...(detailRecord as unknown as UsageDetail), __modelName: modelName }, + modelPrices + ); } }); } models[modelName] = { - requests: modelData.total_requests || 0, + requests: Number(modelData.total_requests) || 0, successCount, failureCount, - tokens: modelData.total_tokens || 0 + tokens: Number(modelData.total_tokens) || 0 }; derivedSuccessCount += successCount; derivedFailureCount += failureCount; }); const hasApiExplicitCounts = - typeof apiData?.success_count === 'number' || typeof apiData?.failure_count === 'number'; + typeof apiData.success_count === 'number' || typeof apiData.failure_count === 'number'; const successCount = hasApiExplicitCounts - ? (Number(apiData?.success_count) || 0) + ? (Number(apiData.success_count) || 0) : derivedSuccessCount; const failureCount = hasApiExplicitCounts - ? (Number(apiData?.failure_count) || 0) + ? (Number(apiData.failure_count) || 0) : derivedFailureCount; result.push({ endpoint: maskUsageSensitiveValue(endpoint) || endpoint, - totalRequests: apiData.total_requests || 0, + totalRequests: Number(apiData.total_requests) || 0, successCount, failureCount, - totalTokens: apiData.total_tokens || 0, + totalTokens: Number(apiData.total_tokens) || 0, totalCost, models }); @@ -615,7 +640,7 @@ export function getApiStats(usageData: any, modelPrices: Record): Array<{ +export function getModelStats(usageData: unknown, modelPrices: Record): Array<{ model: string; requests: number; successCount: number; @@ -623,18 +648,22 @@ export function getModelStats(usageData: any, modelPrices: Record { - if (!usageData?.apis) { - return []; - } + const apis = getApisRecord(usageData); + if (!apis) return []; const modelMap = new Map(); - Object.values(usageData.apis as Record).forEach(apiData => { - const models = apiData?.models || {}; - Object.entries(models as Record).forEach(([modelName, modelData]) => { + Object.values(apis).forEach((apiData) => { + if (!isRecord(apiData)) return; + const modelsRaw = apiData.models; + const models = isRecord(modelsRaw) ? modelsRaw : null; + if (!models) return; + + Object.entries(models).forEach(([modelName, modelData]) => { + if (!isRecord(modelData)) return; const existing = modelMap.get(modelName) || { requests: 0, successCount: 0, failureCount: 0, tokens: 0, cost: 0 }; - existing.requests += modelData.total_requests || 0; - existing.tokens += modelData.total_tokens || 0; + existing.requests += Number(modelData.total_requests) || 0; + existing.tokens += Number(modelData.total_tokens) || 0; const details = Array.isArray(modelData.details) ? modelData.details : []; @@ -648,17 +677,21 @@ export function getModelStats(usageData: any, modelPrices: Record 0 && (!hasExplicitCounts || price)) { - details.forEach((detail: any) => { + details.forEach((detail) => { + const detailRecord = isRecord(detail) ? detail : null; if (!hasExplicitCounts) { - if (detail?.failed === true) { + if (detailRecord?.failed === true) { existing.failureCount += 1; } else { existing.successCount += 1; } } - if (price) { - existing.cost += calculateCost({ ...detail, __modelName: modelName }, modelPrices); + if (price && detailRecord) { + existing.cost += calculateCost( + { ...(detailRecord as unknown as UsageDetail), __modelName: modelName }, + modelPrices + ); } }); } @@ -700,7 +733,10 @@ export function formatDayLabel(date: Date): string { /** * 构建小时级别的数据序列 */ -export function buildHourlySeriesByModel(usageData: any, metric: 'requests' | 'tokens' = 'requests'): { +export function buildHourlySeriesByModel( + usageData: unknown, + metric: 'requests' | 'tokens' = 'requests' +): { labels: string[]; dataByModel: Map; hasData: boolean; @@ -728,7 +764,7 @@ export function buildHourlySeriesByModel(usageData: any, metric: 'requests' | 't return { labels, dataByModel, hasData }; } - details.forEach(detail => { + details.forEach((detail) => { const timestamp = Date.parse(detail.timestamp); if (Number.isNaN(timestamp)) { return; @@ -767,7 +803,10 @@ export function buildHourlySeriesByModel(usageData: any, metric: 'requests' | 't /** * 构建日级别的数据序列 */ -export function buildDailySeriesByModel(usageData: any, metric: 'requests' | 'tokens' = 'requests'): { +export function buildDailySeriesByModel( + usageData: unknown, + metric: 'requests' | 'tokens' = 'requests' +): { labels: string[]; dataByModel: Map; hasData: boolean; @@ -781,7 +820,7 @@ export function buildDailySeriesByModel(usageData: any, metric: 'requests' | 'to return { labels: [], dataByModel: new Map(), hasData }; } - details.forEach(detail => { + details.forEach((detail) => { const timestamp = Date.parse(detail.timestamp); if (Number.isNaN(timestamp)) { return; @@ -885,7 +924,7 @@ const buildAreaGradient = (context: ScriptableContext<'line'>, baseHex: string, * 构建图表数据 */ export function buildChartData( - usageData: any, + usageData: unknown, period: 'hour' | 'day' = 'day', metric: 'requests' | 'tokens' = 'requests', selectedModels: string[] = [] @@ -1034,8 +1073,9 @@ export function calculateStatusBarData( }; } -export function computeKeyStats(usageData: any, masker: (val: string) => string = maskApiKey): KeyStats { - if (!usageData) { +export function computeKeyStats(usageData: unknown, masker: (val: string) => string = maskApiKey): KeyStats { + const apis = getApisRecord(usageData); + if (!apis) { return { bySource: {}, byAuthIndex: {} }; } @@ -1049,17 +1089,21 @@ export function computeKeyStats(usageData: any, masker: (val: string) => string return bucket[key]; }; - const apis = usageData.apis || {}; - Object.values(apis as any).forEach((apiEntry: any) => { - const models = apiEntry?.models || {}; + Object.values(apis).forEach((apiEntry) => { + if (!isRecord(apiEntry)) return; + const modelsRaw = apiEntry.models; + const models = isRecord(modelsRaw) ? modelsRaw : null; + if (!models) return; - Object.values(models as any).forEach((modelEntry: any) => { - const details = modelEntry?.details || []; + Object.values(models).forEach((modelEntry) => { + if (!isRecord(modelEntry)) return; + const details = Array.isArray(modelEntry.details) ? modelEntry.details : []; - details.forEach((detail: any) => { - const source = normalizeUsageSourceId(detail?.source, masker); - const authIndexKey = normalizeAuthIndex(detail?.auth_index); - const isFailed = detail?.failed === true; + details.forEach((detail) => { + const detailRecord = isRecord(detail) ? detail : null; + const source = normalizeUsageSourceId(detailRecord?.source, masker); + const authIndexKey = normalizeAuthIndex(detailRecord?.auth_index); + const isFailed = detailRecord?.failed === true; if (source) { const bucket = ensureBucket(sourceStats, source);