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.
This commit is contained in:
LTbinglingfeng
2026-06-13 02:11:21 +08:00
parent 93f3b6b7ab
commit cd44dca9c0
20 changed files with 38 additions and 91 deletions
+1 -6
View File
@@ -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<string, unknown> =>
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);
+1 -6
View File
@@ -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<string, unknown> =>
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;
+1 -6
View File
@@ -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<string, unknown> =>
value !== null && typeof value === 'object' && !Array.isArray(value);
const cloneRecord = (value: unknown): Record<string, unknown> =>
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;
@@ -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<string, unknown> =>
Boolean(value && typeof value === 'object' && !Array.isArray(value));
const getResourceModels = (resource: ProviderResource): string[] => {
if (!isRecord(resource.raw)) return [];
if (resource.brand === 'ampcode') {
@@ -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 &&
@@ -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<ProviderBrand> = [
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);
+1 -3
View File
@@ -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<Record<ProviderBrand, ProviderFilterState>>;
}
const isRecord = (value: unknown): value is Record<string, unknown> =>
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);
@@ -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;