From 3c87d3f01d3eecd707f690a3e70298c55eb81d2e Mon Sep 17 00:00:00 2001 From: icarus Date: Thu, 4 Sep 2025 01:11:19 +0800 Subject: [PATCH] =?UTF-8?q?refactor(types):=20=E9=87=8D=E6=9E=84=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=B1=BB=E5=9E=8B=E5=AE=9A=E4=B9=89=E5=B9=B6=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=BA=8F=E5=88=97=E5=8C=96=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 SerializedError 从 @reduxjs/toolkit 移至本地定义 - 添加 Serializable 类型和序列化工具函数 - 更新相关文件中的错误类型引用 - 实现安全序列化功能用于错误处理 --- .../pages/home/Messages/Blocks/ErrorBlock.tsx | 14 +-- src/renderer/src/types/error.ts | 25 +++-- src/renderer/src/types/newMessage.ts | 2 +- src/renderer/src/types/serialize.ts | 63 +++++++++++++ src/renderer/src/utils/error.ts | 19 ++-- src/renderer/src/utils/serialize.ts | 93 +++++++++++++++++++ 6 files changed, 188 insertions(+), 28 deletions(-) create mode 100644 src/renderer/src/types/serialize.ts create mode 100644 src/renderer/src/utils/serialize.ts diff --git a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx index 146e6d5c95..0d2a600d37 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx @@ -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 = ({ 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 ( } 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()) } diff --git a/src/renderer/src/types/error.ts b/src/renderer/src/types/error.ts index de4aadbaf5..fd1eb30a8f 100644 --- a/src/renderer/src/types/error.ts +++ b/src/renderer/src/types/error.ts @@ -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 - readonly responseBody?: string + readonly requestBodyValues: Serializable + readonly statusCode: number | null + readonly responseHeaders: Record | null + readonly responseBody: string | null readonly isRetryable: boolean - readonly data?: unknown + readonly data: Serializable | null } export const isSerializedAiSdkAPICallError = (error: SerializedError): error is SerializedAiSdkAPICallError => { diff --git a/src/renderer/src/types/newMessage.ts b/src/renderer/src/types/newMessage.ts index f0b49ce336..a8a037a851 100644 --- a/src/renderer/src/types/newMessage.ts +++ b/src/renderer/src/types/newMessage.ts @@ -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 { diff --git a/src/renderer/src/types/serialize.ts b/src/renderer/src/types/serialize.ts new file mode 100644 index 0000000000..8d8e9e7a33 --- /dev/null +++ b/src/renderer/src/types/serialize.ts @@ -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 // 如出现循环引用错误等 + } +} diff --git a/src/renderer/src/utils/error.ts b/src/renderer/src/utils/error.ts index 55cb6936f4..7953c8ca1f 100644 --- a/src/renderer/src/utils/error.ts +++ b/src/renderer/src/utils/error.ts @@ -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 } diff --git a/src/renderer/src/utils/serialize.ts b/src/renderer/src/utils/serialize.ts new file mode 100644 index 0000000000..862545647c --- /dev/null +++ b/src/renderer/src/utils/serialize.ts @@ -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 +}