refactor(types): 重构错误类型定义并添加序列化功能

- 将 SerializedError 从 @reduxjs/toolkit 移至本地定义
- 添加 Serializable 类型和序列化工具函数
- 更新相关文件中的错误类型引用
- 实现安全序列化功能用于错误处理
This commit is contained in:
icarus 2025-09-04 01:11:19 +08:00
parent a681c28a0a
commit 3c87d3f01d
6 changed files with 188 additions and 28 deletions

View File

@ -1,4 +1,3 @@
import { SerializedError } from '@reduxjs/toolkit'
import CodeViewer from '@renderer/components/CodeViewer'
import { useTimer } from '@renderer/hooks/useTimer'
import { getHttpMessageLabel, getProviderLabel } from '@renderer/i18n/label'
@ -9,7 +8,8 @@ import {
isSerializedAiSdkAPICallError,
isSerializedAiSdkError,
SerializedAiSdkAPICallError,
SerializedAiSdkError
SerializedAiSdkError,
SerializedError
} from '@renderer/types/error'
import type { ErrorMessageBlock, Message } from '@renderer/types/newMessage'
import { formatAiSdkError, formatError, safeToString } from '@renderer/utils/error'
@ -34,12 +34,12 @@ const ErrorBlock: React.FC<Props> = ({ block, message }) => {
const ErrorMessage: React.FC<{ block: ErrorMessageBlock }> = ({ block }) => {
const { t, i18n } = useTranslation()
const i18nKey = `error.${block.error?.i18nKey}`
const i18nKey = block.error && 'i18nKey' in block.error ? `error.${block.error?.i18nKey}` : ''
const errorKey = `error.${block.error?.message}`
const errorStatus = block.error?.status
const errorStatus = block.error && 'status' in block.error ? block.error?.status : undefined
if (i18n.exists(i18nKey)) {
const providerId = block.error?.providerId
const providerId = block.error && 'providerId' in block.error ? block.error?.providerId : undefined
if (providerId && typeof providerId === 'string') {
return (
<Trans
@ -89,7 +89,7 @@ const MessageErrorInfo: React.FC<{ block: ErrorMessageBlock; message: Message }>
}
const getAlertMessage = () => {
const status = block.error?.status
const status = block.error && 'status' in block.error ? block.error?.status : undefined
if (block.error && typeof status === 'number' && HTTP_ERROR_CODES.includes(status)) {
return block.error.message
}
@ -97,7 +97,7 @@ const MessageErrorInfo: React.FC<{ block: ErrorMessageBlock; message: Message }>
}
const getAlertDescription = () => {
const status = block.error?.status
const status = block.error && 'status' in block.error ? block.error?.status : undefined
if (block.error && typeof status === 'number' && HTTP_ERROR_CODES.includes(status)) {
return getHttpMessageLabel(status.toString())
}

View File

@ -1,14 +1,13 @@
import { SerializedError } from '@reduxjs/toolkit'
import { Serializable } from './serialize'
// 定义模块增强以扩展 @reduxjs/toolkit 的 SerializedError
declare module '@reduxjs/toolkit' {
interface SerializedError {
[key: string]: unknown
}
export interface SerializedError {
name: string | null
message: string | null
stack: string | null
[key: string]: Serializable
}
export interface SerializedAiSdkError extends SerializedError {
readonly cause?: unknown
readonly cause: string | null
}
export const isSerializedAiSdkError = (error: SerializedError): error is SerializedAiSdkError => {
@ -17,12 +16,12 @@ export const isSerializedAiSdkError = (error: SerializedError): error is Seriali
export interface SerializedAiSdkAPICallError extends SerializedAiSdkError {
readonly url: string
readonly requestBodyValues: unknown
readonly statusCode?: number
readonly responseHeaders?: Record<string, string>
readonly responseBody?: string
readonly requestBodyValues: Serializable
readonly statusCode: number | null
readonly responseHeaders: Record<string, string> | null
readonly responseBody: string | null
readonly isRetryable: boolean
readonly data?: unknown
readonly data: Serializable | null
}
export const isSerializedAiSdkAPICallError = (error: SerializedError): error is SerializedAiSdkAPICallError => {

View File

@ -1,4 +1,3 @@
import { SerializedError } from '@reduxjs/toolkit'
import type { CompletionUsage } from 'openai/resources'
import type {
@ -16,6 +15,7 @@ import type {
WebSearchResponse,
WebSearchSource
} from '.'
import { SerializedError } from './error'
// MessageBlock 类型枚举 - 根据实际API返回特性优化
export enum MessageBlockType {

View File

@ -0,0 +1,63 @@
export type Serializable = null | boolean | number | string | { [key: string]: Serializable } | Serializable[] /**
* Redux
*
*/
export function isSerializable(value: unknown): boolean {
const seen = new Set() // 用于防止循环引用
function _isSerializable(val: unknown): boolean {
if (val === null || val === undefined) {
return val !== undefined // null ✅, undefined ❌
}
const type = typeof val
if (type === 'string' || type === 'number' || type === 'boolean') {
return true
}
if (type === 'object') {
// 检查循环引用
if (seen.has(val)) {
return true // 避免无限递归,假设循环引用对象本身结构合法(但实际 JSON.stringify 会报错)
}
seen.add(val)
if (Array.isArray(val)) {
return val.every((item) => _isSerializable(item))
}
// 检查是否为纯对象plain object
const proto = Object.getPrototypeOf(val)
if (proto !== null && proto !== Object.prototype && proto !== Array.prototype) {
return false // 不是 plain object比如 class 实例
}
// 检查内置对象(如 Date、RegExp、Map、Set 等)
if (
val instanceof Date ||
val instanceof RegExp ||
val instanceof Map ||
val instanceof Set ||
val instanceof Error ||
val instanceof File ||
val instanceof Blob
) {
return false
}
// 递归检查所有属性值
return Object.values(val).every((v) => _isSerializable(v))
}
// function、symbol 不可序列化
return false
}
try {
return _isSerializable(value)
} catch {
return false // 如出现循环引用错误等
}
}

View File

@ -1,10 +1,12 @@
import { loggerService } from '@logger'
import { SerializedError } from '@reduxjs/toolkit'
import { AiSdkErrorUnion } from '@renderer/types/aiCoreTypes'
import { SerializedAiSdkAPICallError, SerializedError } from '@renderer/types/error'
import { AISDKError, APICallError } from 'ai'
import { t } from 'i18next'
import z from 'zod'
import { safeSerialize } from './serialize'
const logger = loggerService.withContext('Utils:error')
export function getErrorDetails(err: any, seen = new WeakSet()): any {
@ -93,8 +95,8 @@ export const serializeError = (error: AISDKError): SerializedError => {
const baseError = {
name: error.name,
message: error.message,
stack: error.stack,
cause: error.cause ? String(error.cause) : undefined
stack: error.stack ?? null,
cause: error.cause ? String(error.cause) : null
}
if (APICallError.isInstance(error)) {
let content = error.message === '' ? error.responseBody || 'Unknown error' : error.message
@ -106,11 +108,14 @@ export const serializeError = (error: AISDKError): SerializedError => {
}
return {
...baseError,
status: error.statusCode,
url: error.url,
message: content,
requestBody: error.requestBodyValues
}
requestBodyValues: safeSerialize(error.requestBodyValues),
statusCode: error.statusCode ?? null,
responseBody: content,
isRetryable: error.isRetryable,
data: safeSerialize(error.data),
responseHeaders: error.responseHeaders ?? null
} satisfies SerializedAiSdkAPICallError
}
return baseError
}

View File

@ -0,0 +1,93 @@
import { isSerializable } from '@renderer/types/serialize'
/**
* JSON
* `Serializable` `isSerializable`
*
* @param value
* @param options
* @returns null
*/
export function safeSerialize(
value: unknown,
options: {
/**
*
* - 'error':
* - 'omit':
* - 'serialize': Date ISO
*/
onError?: 'error' | 'omit' | 'serialize'
/**
*
*/
pretty?: boolean
} = {}
): string | null {
const { onError = 'serialize', pretty = false } = options
const space = pretty ? 2 : undefined
// 1. 如果本身就是合法的 Serializable 值,直接序列化
if (isSerializable(value)) {
try {
return JSON.stringify(value, null, space)
} catch (err) {
// 理论上不会发生,但以防万一(比如极深嵌套栈溢出)
if (onError === 'error') {
throw new Error(`Failed to stringify serializable value: ${err instanceof Error ? err.message : err}`)
}
return null
}
}
// 2. 不是可序列化的,根据策略处理
switch (onError) {
case 'error':
throw new TypeError('Value is not serializable and cannot be safely serialized.')
case 'omit':
// 注意:这里不能“修复”对象,只能返回 null 表示跳过
return null
case 'serialize': {
// 宽容模式:尝试做一些安全转换
return tryLenientSerialize(value, space)
}
}
}
/**
* 使 Serializable
*
*/
function tryLenientSerialize(value: unknown, space?: string | number): string {
const seen = new WeakSet()
const serialized = JSON.stringify(
value,
(_, val: any) => {
// 处理循环引用
if (typeof val === 'object' && val !== null) {
if (seen.has(val)) {
return '[Circular]'
}
seen.add(val)
}
// 处理特殊类型
if (val instanceof Date) return val.toISOString()
if (val instanceof RegExp) return `{RegExp: "${val.toString()}"}`
if (typeof val === 'function') return `[Function: ${val.name || 'anonymous'}]`
if (typeof val === 'symbol') return `Symbol(${String(val.description)})`
if (val instanceof Map) return Object.fromEntries(val.entries())
if (val instanceof Set) return Array.from(val)
if (val === undefined) return '[undefined]'
return val
},
space
)
return serialized
}