mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 21:01:32 +08:00
refactor(types): 重构错误类型定义并添加序列化功能
- 将 SerializedError 从 @reduxjs/toolkit 移至本地定义 - 添加 Serializable 类型和序列化工具函数 - 更新相关文件中的错误类型引用 - 实现安全序列化功能用于错误处理
This commit is contained in:
parent
a681c28a0a
commit
3c87d3f01d
@ -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())
|
||||
}
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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 {
|
||||
|
||||
63
src/renderer/src/types/serialize.ts
Normal file
63
src/renderer/src/types/serialize.ts
Normal 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 // 如出现循环引用错误等
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
93
src/renderer/src/utils/serialize.ts
Normal file
93
src/renderer/src/utils/serialize.ts
Normal 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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user