From cd44dca9c08348f57910fe4002233d9d7054129b Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Sat, 13 Jun 2026 02:11:21 +0800 Subject: [PATCH] refactor(utils): share isRecord and getErrorMessage helpers isRecord was declared locally in 15 modules (with two divergent shapes) and getErrorMessage in 7. Move a single canonical pair into utils/helpers and import it everywhere. The shared isRecord excludes arrays; the call sites that previously allowed them only read named properties, so behavior is unchanged. --- src/features/plugins/PluginResourcePage.tsx | 7 +------ src/features/plugins/PluginStorePage.tsx | 7 +------ src/features/plugins/PluginsPage.tsx | 7 +------ .../providers/ProvidersWorkbenchPage.tsx | 4 +--- .../sheets/forms/useConnectivityTest.ts | 9 ++------- .../providers/sheets/forms/useModelDiscovery.ts | 9 ++------- src/features/providers/uiState.ts | 4 +--- src/features/providers/useProviderWorkbench.ts | 7 +------ src/pages/LogsPage.tsx | 11 +---------- src/pages/OAuthPage.tsx | 11 +---------- src/services/api/apiCall.ts | 4 +--- src/services/api/client.ts | 4 +--- src/services/api/logs.ts | 4 +--- src/services/api/models.ts | 4 +--- src/services/api/plugins.ts | 4 +--- src/services/api/providers.ts | 4 +--- src/services/api/transformers.ts | 4 +--- src/services/api/version.ts | 4 +--- src/utils/helpers.ts | 16 ++++++++++++++++ src/utils/models.ts | 5 ++--- 20 files changed, 38 insertions(+), 91 deletions(-) diff --git a/src/features/plugins/PluginResourcePage.tsx b/src/features/plugins/PluginResourcePage.tsx index 1f1f7c9..ab6e885 100644 --- a/src/features/plugins/PluginResourcePage.tsx +++ b/src/features/plugins/PluginResourcePage.tsx @@ -5,6 +5,7 @@ import { EmptyState } from '@/components/ui/EmptyState'; import { useHeaderRefresh } from '@/hooks/useHeaderRefresh'; import { pluginsApi } from '@/services/api'; import { useAuthStore } from '@/stores'; +import { getErrorMessage, isRecord } from '@/utils/helpers'; import type { PluginListResponse } from '@/types'; import { collectPluginResourceEntries, @@ -12,15 +13,9 @@ import { } from './pluginResources'; import styles from './PluginResourcePage.module.scss'; -const isRecord = (value: unknown): value is Record => - value !== null && typeof value === 'object' && !Array.isArray(value); - const hasStatus = (error: unknown, status: number) => isRecord(error) && error.status === status; -const getErrorMessage = (error: unknown, fallback: string) => - error instanceof Error ? error.message : typeof error === 'string' ? error : fallback; - const safeDecodeURIComponent = (value = '') => { try { return decodeURIComponent(value); diff --git a/src/features/plugins/PluginStorePage.tsx b/src/features/plugins/PluginStorePage.tsx index fd71d36..99f77ef 100644 --- a/src/features/plugins/PluginStorePage.tsx +++ b/src/features/plugins/PluginStorePage.tsx @@ -16,6 +16,7 @@ import { import { useHeaderRefresh } from '@/hooks/useHeaderRefresh'; import { pluginStoreApi } from '@/services/api'; import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores'; +import { getErrorMessage, isRecord } from '@/utils/helpers'; import type { PluginStoreEntry, PluginStoreResponse } from '@/types'; import { buildRepositoryURL, resolvePluginAssetURL } from './pluginResources'; import styles from './PluginStorePage.module.scss'; @@ -27,12 +28,6 @@ interface StoreLoadError { message: string; } -const isRecord = (value: unknown): value is Record => - value !== null && typeof value === 'object' && !Array.isArray(value); - -const getErrorMessage = (error: unknown, fallback: string) => - error instanceof Error ? error.message : typeof error === 'string' ? error : fallback; - const getErrorStatus = (error: unknown): number | undefined => isRecord(error) && typeof error.status === 'number' ? error.status : undefined; diff --git a/src/features/plugins/PluginsPage.tsx b/src/features/plugins/PluginsPage.tsx index 47959d5..4429035 100644 --- a/src/features/plugins/PluginsPage.tsx +++ b/src/features/plugins/PluginsPage.tsx @@ -20,6 +20,7 @@ import { import { useHeaderRefresh } from '@/hooks/useHeaderRefresh'; import { pluginsApi } from '@/services/api'; import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores'; +import { getErrorMessage, isRecord } from '@/utils/helpers'; import type { PluginConfigField, PluginListEntry, PluginListResponse } from '@/types'; import { getPluginTitle, resolvePluginAssetURL } from './pluginResources'; import styles from './PluginsPage.module.scss'; @@ -44,15 +45,9 @@ function PluginCardLogo({ src }: { src: string }) { ); } -const isRecord = (value: unknown): value is Record => - value !== null && typeof value === 'object' && !Array.isArray(value); - const cloneRecord = (value: unknown): Record => isRecord(value) ? { ...value } : {}; -const getErrorMessage = (error: unknown, fallback: string) => - error instanceof Error ? error.message : typeof error === 'string' ? error : fallback; - const hasStatus = (error: unknown, status: number) => isRecord(error) && error.status === status; diff --git a/src/features/providers/ProvidersWorkbenchPage.tsx b/src/features/providers/ProvidersWorkbenchPage.tsx index 4300d0d..0c6fa82 100644 --- a/src/features/providers/ProvidersWorkbenchPage.tsx +++ b/src/features/providers/ProvidersWorkbenchPage.tsx @@ -11,6 +11,7 @@ import { type ProviderRecentUsageMap, } from '@/components/providers/utils'; import type { OpenAIProviderConfig } from '@/types'; +import { isRecord } from '@/utils/helpers'; import { ProviderHeaderCard } from './components/ProviderHeaderCard'; import { ProviderCategoryList } from './components/ProviderCategoryList'; import { ProviderResourcePanel } from './components/ProviderResourcePanel'; @@ -66,9 +67,6 @@ const matchesFilter = (r: ProviderResource, normalized: string): boolean => { return haystack.some((v) => v.includes(normalized)); }; -const isRecord = (value: unknown): value is Record => - Boolean(value && typeof value === 'object' && !Array.isArray(value)); - const getResourceModels = (resource: ProviderResource): string[] => { if (!isRecord(resource.raw)) return []; if (resource.brand === 'ampcode') { diff --git a/src/features/providers/sheets/forms/useConnectivityTest.ts b/src/features/providers/sheets/forms/useConnectivityTest.ts index 07b1de5..0eb4ad3 100644 --- a/src/features/providers/sheets/forms/useConnectivityTest.ts +++ b/src/features/providers/sheets/forms/useConnectivityTest.ts @@ -6,6 +6,7 @@ import { buildOpenAIChatCompletionsEndpoint, } from '@/components/providers/utils'; import { buildHeaderObject, hasHeader } from '@/utils/headers'; +import { getErrorMessage } from '@/utils/helpers'; import type { ApiKeyEntryInput, ModelEntryInput, ProviderBrand } from '../../types'; const DEFAULT_TIMEOUT_MS = 30_000; @@ -20,14 +21,8 @@ export interface ConnectivityStatus { const IDLE: ConnectivityStatus = { state: 'idle', message: '' }; -const errorMessage = (err: unknown): string => { - if (err instanceof Error) return err.message; - if (typeof err === 'string') return err; - return ''; -}; - const requestFailureMessage = (err: unknown, messages: ConnectivityErrorMessages): string => { - const raw = errorMessage(err); + const raw = getErrorMessage(err); const isTimeout = (typeof err === 'object' && err !== null && diff --git a/src/features/providers/sheets/forms/useModelDiscovery.ts b/src/features/providers/sheets/forms/useModelDiscovery.ts index 3650ddf..dbb2e11 100644 --- a/src/features/providers/sheets/forms/useModelDiscovery.ts +++ b/src/features/providers/sheets/forms/useModelDiscovery.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { modelsApi } from '@/services/api'; import { buildHeaderObject } from '@/utils/headers'; +import { getErrorMessage } from '@/utils/helpers'; import type { ModelInfo } from '@/utils/models'; import type { ApiKeyEntryInput, ProviderBrand } from '../../types'; @@ -14,12 +15,6 @@ export const MODEL_DISCOVERY_BRANDS: ReadonlyArray = [ export const isModelDiscoveryBrand = (brand: ProviderBrand): boolean => MODEL_DISCOVERY_BRANDS.includes(brand); -const toErrorMessage = (err: unknown): string => { - if (err instanceof Error) return err.message; - if (typeof err === 'string') return err; - return ''; -}; - export interface UseModelDiscoveryArgs { brand: ProviderBrand; baseUrl: string; @@ -111,7 +106,7 @@ export function useModelDiscovery(args: UseModelDiscoveryArgs): UseModelDiscover setHasFetched(true); } catch (err) { setModels([]); - setError(toErrorMessage(err) || 'Failed to fetch models'); + setError(getErrorMessage(err) || 'Failed to fetch models'); setHasFetched(true); } finally { setLoading(false); diff --git a/src/features/providers/uiState.ts b/src/features/providers/uiState.ts index d655932..84b2201 100644 --- a/src/features/providers/uiState.ts +++ b/src/features/providers/uiState.ts @@ -1,3 +1,4 @@ +import { isRecord } from '@/utils/helpers'; import { PROVIDER_BRAND_ORDER } from './descriptors'; import { PROVIDER_SORT_BY_VALUES, @@ -32,9 +33,6 @@ export interface ProvidersWorkbenchUiState { filtersByBrand: Partial>; } -const isRecord = (value: unknown): value is Record => - Boolean(value && typeof value === 'object' && !Array.isArray(value)); - const isProviderBrand = (value: unknown): value is ProviderBrand => typeof value === 'string' && PROVIDER_BRAND_SET.has(value as ProviderBrand); diff --git a/src/features/providers/useProviderWorkbench.ts b/src/features/providers/useProviderWorkbench.ts index 12ee11c..451225c 100644 --- a/src/features/providers/useProviderWorkbench.ts +++ b/src/features/providers/useProviderWorkbench.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ampcodeApi, providersApi } from '@/services/api'; +import { getErrorMessage } from '@/utils/helpers'; import { useAuthStore, useConfigStore } from '@/stores'; import { withDisableAllModelsRule, @@ -28,12 +29,6 @@ import type { ProviderSnapshot, } from './types'; -const getErrorMessage = (err: unknown): string => { - if (err instanceof Error) return err.message; - if (typeof err === 'string') return err; - return ''; -}; - export interface UseProviderWorkbenchResult { connected: boolean; isPending: boolean; diff --git a/src/pages/LogsPage.tsx b/src/pages/LogsPage.tsx index 26fef47..66eb6c8 100644 --- a/src/pages/LogsPage.tsx +++ b/src/pages/LogsPage.tsx @@ -30,6 +30,7 @@ import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores'; import { logsApi, type LogsQuery } from '@/services/api/logs'; import { versionApi } from '@/services/api/version'; import { copyToClipboard } from '@/utils/clipboard'; +import { getErrorMessage } from '@/utils/helpers'; import { downloadBlob } from '@/utils/download'; import { MANAGEMENT_API_PREFIX } from '@/utils/constants'; import { formatUnixTimestamp } from '@/utils/format'; @@ -82,16 +83,6 @@ const mergeIncrementalLines = (currentLines: string[], incomingLines: string[]): return [...currentLines, ...incomingLines.slice(overlap)]; }; -const getErrorMessage = (err: unknown): string => { - if (err instanceof Error) return err.message; - if (typeof err === 'string') return err; - if (typeof err !== 'object' || err === null) return ''; - if (!('message' in err)) return ''; - - const message = (err as { message?: unknown }).message; - return typeof message === 'string' ? message : ''; -}; - const getErrorPayloadText = (err: unknown): string => { if (typeof err !== 'object' || err === null) return ''; const payloads = [ diff --git a/src/pages/OAuthPage.tsx b/src/pages/OAuthPage.tsx index c4ea8bc..ce62806 100644 --- a/src/pages/OAuthPage.tsx +++ b/src/pages/OAuthPage.tsx @@ -8,6 +8,7 @@ import { useNotificationStore, useThemeStore } from '@/stores'; import { oauthApi, type OAuthProvider } from '@/services/api/oauth'; import { vertexApi, type VertexImportResponse } from '@/services/api/vertex'; import { copyToClipboard } from '@/utils/clipboard'; +import { getErrorMessage, isRecord } from '@/utils/helpers'; import styles from './OAuthPage.module.scss'; import iconCodex from '@/assets/icons/codex.svg'; import iconClaude from '@/assets/icons/claude.svg'; @@ -49,16 +50,6 @@ 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; diff --git a/src/services/api/apiCall.ts b/src/services/api/apiCall.ts index c88e63a..1f2cc66 100644 --- a/src/services/api/apiCall.ts +++ b/src/services/api/apiCall.ts @@ -4,6 +4,7 @@ import type { AxiosRequestConfig } from 'axios'; import { apiClient } from './client'; +import { isRecord } from '@/utils/helpers'; export interface ApiCallRequest { authIndex?: string; @@ -46,9 +47,6 @@ const normalizeBody = (input: unknown): { bodyText: string; body: unknown | 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; diff --git a/src/services/api/client.ts b/src/services/api/client.ts index 7a3f692..af3b51c 100644 --- a/src/services/api/client.ts +++ b/src/services/api/client.ts @@ -15,6 +15,7 @@ import { VERSION_HEADER_KEYS } from '@/utils/constants'; import { computeApiUrl } from '@/utils/connection'; +import { isRecord } from '@/utils/helpers'; import type { ServerRuntimeKind } from '@/types'; class ApiClient { @@ -143,9 +144,6 @@ class ApiClient { * 错误处理 */ private handleError(error: unknown): ApiError { - const isRecord = (value: unknown): value is Record => - value !== null && typeof value === 'object'; - if (axios.isAxiosError(error)) { const responseData: unknown = error.response?.data; const responseRecord = isRecord(responseData) ? responseData : null; diff --git a/src/services/api/logs.ts b/src/services/api/logs.ts index d1e1122..2a5a613 100644 --- a/src/services/api/logs.ts +++ b/src/services/api/logs.ts @@ -4,6 +4,7 @@ import { apiClient } from './client'; import { LOGS_TIMEOUT_MS } from '@/utils/constants'; +import { isRecord } from '@/utils/helpers'; export type LogCursor = number | string; export type LogBackendKind = 'unknown' | 'file' | 'home-db'; @@ -59,9 +60,6 @@ export interface ErrorLogsResponse { files?: ErrorLogFile[]; } -const isRecord = (value: unknown): value is Record => - value !== null && typeof value === 'object'; - const stringValue = (value: unknown): string => (typeof value === 'string' ? value.trim() : ''); const unixSecondsFromValue = (value: unknown): number => { diff --git a/src/services/api/models.ts b/src/services/api/models.ts index f8bebf0..dbeee42 100644 --- a/src/services/api/models.ts +++ b/src/services/api/models.ts @@ -6,6 +6,7 @@ import axios from 'axios'; import { normalizeModelList } from '@/utils/models'; import { normalizeApiBase } from '@/utils/connection'; import { apiCallApi, getApiCallErrorMessage } from './apiCall'; +import { isRecord } from '@/utils/helpers'; const DEFAULT_CLAUDE_BASE_URL = 'https://api.anthropic.com'; const DEFAULT_GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com'; @@ -13,9 +14,6 @@ const DEFAULT_ANTHROPIC_VERSION = '2023-06-01'; const CLAUDE_MODELS_IN_FLIGHT = new Map>>(); const GEMINI_MODELS_IN_FLIGHT = new Map>>(); -const isRecord = (value: unknown): value is Record => - value !== null && typeof value === 'object' && !Array.isArray(value); - const buildRequestSignature = ( url: string, headers: Record, diff --git a/src/services/api/plugins.ts b/src/services/api/plugins.ts index 95cf7ee..59854c1 100644 --- a/src/services/api/plugins.ts +++ b/src/services/api/plugins.ts @@ -1,4 +1,5 @@ import { apiClient } from './client'; +import { isRecord } from '@/utils/helpers'; import type { PluginConfigField, PluginListEntry, @@ -10,9 +11,6 @@ import type { PluginStoreResponse, } from '@/types'; -const isRecord = (value: unknown): value is Record => - value !== null && typeof value === 'object' && !Array.isArray(value); - const asString = (value: unknown): string => { if (value === undefined || value === null) return ''; return String(value); diff --git a/src/services/api/providers.ts b/src/services/api/providers.ts index 6c197e7..4d1ce6a 100644 --- a/src/services/api/providers.ts +++ b/src/services/api/providers.ts @@ -3,6 +3,7 @@ */ import { apiClient } from './client'; +import { isRecord } from '@/utils/helpers'; import { normalizeGeminiKeyConfig, normalizeOpenAIProvider, @@ -19,9 +20,6 @@ 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 RESPONSE_ONLY_FIELDS = ['auth-index'] as const; const PROVIDER_KEY_FIELDS = [ diff --git a/src/services/api/transformers.ts b/src/services/api/transformers.ts index 598fd3d..b0dcf27 100644 --- a/src/services/api/transformers.ts +++ b/src/services/api/transformers.ts @@ -11,9 +11,7 @@ import type { } from '@/types'; import type { Config } from '@/types/config'; import { buildHeaderObject } from '@/utils/headers'; - -const isRecord = (value: unknown): value is Record => - value !== null && typeof value === 'object' && !Array.isArray(value); +import { isRecord } from '@/utils/helpers'; const normalizeBoolean = (value: unknown): boolean | undefined => typeof value === 'boolean' ? value : undefined; diff --git a/src/services/api/version.ts b/src/services/api/version.ts index c503e7e..7e047bf 100644 --- a/src/services/api/version.ts +++ b/src/services/api/version.ts @@ -4,9 +4,7 @@ import { apiClient } from './client'; import type { ServerRuntimeKind } from '@/types'; - -const isRecord = (value: unknown): value is Record => - value !== null && typeof value === 'object'; +import { isRecord } from '@/utils/helpers'; export const versionApi = { checkLatest: () => apiClient.get>('/latest-version'), diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 947b4fa..992ed53 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -9,3 +9,19 @@ export function generateId(): string { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } + +/** + * 判断是否为普通对象(排除 null 与数组) + */ +export const isRecord = (value: unknown): value is Record => + value !== null && typeof value === 'object' && !Array.isArray(value); + +/** + * 从 unknown 错误中提取可读消息 + */ +export const getErrorMessage = (error: unknown, fallback = ''): string => { + if (error instanceof Error) return error.message || fallback; + if (typeof error === 'string') return error || fallback; + if (isRecord(error) && typeof error.message === 'string') return error.message || fallback; + return fallback; +}; diff --git a/src/utils/models.ts b/src/utils/models.ts index 5049fa0..a133c57 100644 --- a/src/utils/models.ts +++ b/src/utils/models.ts @@ -3,6 +3,8 @@ * 迁移自基线 utils/models.js */ +import { isRecord } from './helpers'; + export interface ModelInfo { name: string; alias?: string; @@ -30,9 +32,6 @@ const matchCategory = (text: string) => { return 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') {