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
Unverified
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;
+1 -10
View File
@@ -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 = [
+1 -10
View File
@@ -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<string, unknown> {
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;
+1 -3
View File
@@ -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<string, unknown> =>
value !== null && typeof value === 'object';
const status = result.statusCode;
const body = result.body;
const bodyText = result.bodyText;
+1 -3
View File
@@ -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<string, unknown> =>
value !== null && typeof value === 'object';
if (axios.isAxiosError(error)) {
const responseData: unknown = error.response?.data;
const responseRecord = isRecord(responseData) ? responseData : null;
+1 -3
View File
@@ -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<string, unknown> =>
value !== null && typeof value === 'object';
const stringValue = (value: unknown): string => (typeof value === 'string' ? value.trim() : '');
const unixSecondsFromValue = (value: unknown): number => {
+1 -3
View File
@@ -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<string, Promise<ReturnType<typeof normalizeModelList>>>();
const GEMINI_MODELS_IN_FLIGHT = new Map<string, Promise<ReturnType<typeof normalizeModelList>>>();
const isRecord = (value: unknown): value is Record<string, unknown> =>
value !== null && typeof value === 'object' && !Array.isArray(value);
const buildRequestSignature = (
url: string,
headers: Record<string, string>,
+1 -3
View File
@@ -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<string, unknown> =>
value !== null && typeof value === 'object' && !Array.isArray(value);
const asString = (value: unknown): string => {
if (value === undefined || value === null) return '';
return String(value);
+1 -3
View File
@@ -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<string, string>) =>
headers && Object.keys(headers).length ? headers : undefined;
const isRecord = (value: unknown): value is Record<string, unknown> =>
value !== null && typeof value === 'object' && !Array.isArray(value);
const RESPONSE_ONLY_FIELDS = ['auth-index'] as const;
const PROVIDER_KEY_FIELDS = [
+1 -3
View File
@@ -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<string, unknown> =>
value !== null && typeof value === 'object' && !Array.isArray(value);
import { isRecord } from '@/utils/helpers';
const normalizeBoolean = (value: unknown): boolean | undefined =>
typeof value === 'boolean' ? value : undefined;
+1 -3
View File
@@ -4,9 +4,7 @@
import { apiClient } from './client';
import type { ServerRuntimeKind } from '@/types';
const isRecord = (value: unknown): value is Record<string, unknown> =>
value !== null && typeof value === 'object';
import { isRecord } from '@/utils/helpers';
export const versionApi = {
checkLatest: () => apiClient.get<Record<string, unknown>>('/latest-version'),
+16
View File
@@ -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<string, unknown> =>
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;
};
+2 -3
View File
@@ -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<string, unknown> =>
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') {