diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json
index 88f8655fde..d732fc6b77 100644
--- a/src/renderer/src/i18n/locales/zh-cn.json
+++ b/src/renderer/src/i18n/locales/zh-cn.json
@@ -831,6 +831,7 @@
"invalid": "无效的MCP服务器"
}
},
+ "cause": "错误原因",
"chat": {
"chunk": {
"non_json": "返回了无效的数据格式"
@@ -840,6 +841,7 @@
"quota_exceeded": "您今日免费配额已用尽,请前往 {{provider}} 获取API密钥,配置API密钥后继续使用",
"response": "出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥"
},
+ "data": "数据",
"detail": "错误详情",
"details": "详细信息",
"http": {
@@ -859,6 +861,7 @@
"exists": "模型已存在",
"not_exists": "模型不存在"
},
+ "name": "错误名称",
"no_api_key": "API 密钥未配置",
"pause_placeholder": "已中断",
"provider_disabled": "模型提供商未启用",
@@ -867,9 +870,13 @@
"title": "渲染错误"
},
"requestBody": "请求内容",
+ "requestBodyValues": "请求体",
"requestUrl": "请求路径",
+ "responseBody": "响应内容",
+ "responseHeaders": "响应首部",
"stack": "堆栈信息",
"status": "状态码",
+ "statusCode": "状态码",
"unknown": "未知错误",
"user_message_not_found": "无法找到原始用户消息"
},
diff --git a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx
index d82f7ca751..68b2987cc9 100644
--- a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx
+++ b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx
@@ -5,6 +5,8 @@ import { getProviderById } from '@renderer/services/ProviderService'
import { useAppDispatch } from '@renderer/store'
import { removeBlocksThunk } from '@renderer/store/thunk/messageThunk'
import type { ErrorMessageBlock, Message } from '@renderer/types/newMessage'
+import { formatAiSdkError, formatError, safeToString } from '@renderer/utils/error'
+import { AISDKError, APICallError } from 'ai'
import { Alert as AntdAlert, Button, Modal } from 'antd'
import React, { useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
@@ -131,53 +133,41 @@ const ErrorDetailModal: React.FC = ({ open, onClose, erro
const copyErrorDetails = () => {
if (!error) return
-
- const errorText = `
-${t('error.message')}: ${error.message || 'N/A'}
-${t('error.requestUrl')}: ${error.url || 'N/A'}
-${t('error.requestBody')}: ${error.requestBody ? JSON.stringify(error.requestBody, null, 2) : 'N/A'}
-${t('error.stack')}: ${error.stack || 'N/A'}
- `.trim()
+ let errorText: string
+ if (AISDKError.isInstance(error)) {
+ errorText = formatAiSdkError(error)
+ } else if (error instanceof Error) {
+ errorText = formatError(error)
+ } else {
+ // fallback
+ errorText = safeToString(error)
+ }
navigator.clipboard.writeText(errorText)
}
- const renderErrorDetails = (error: any) => {
+ const renderErrorDetails = (error?: Record) => {
if (!error) return {t('error.unknown')}
+ if (APICallError.isInstance(error)) {
+ return
+ }
+ if (AISDKError.isInstance(error)) {
+ return
+ }
+ if (error instanceof Error) {
+ return (
+
+
+
+ )
+ }
+ // default
return (
- {error.message && (
-
- {t('error.message')}:
- {error.message}
-
- )}
-
- {error.url && (
-
- {t('error.requestUrl')}:
- {error.url}
-
- )}
-
- {error.requestBody && (
-
- {t('error.requestBody')}:
-
- {JSON.stringify(error.requestBody, null, 2)}
-
-
- )}
-
- {error.stack && (
-
- {t('error.stack')}:
-
- {error.stack}
-
-
- )}
+
+ {t('error.unknown')}:
+
)
}
@@ -262,4 +252,109 @@ const Alert = styled(AntdAlert)`
}
`
+// 作为 base,渲染公共字段,应当在 ErrorDetailList 中渲染
+const BuiltinError = ({ error }: { error: Error }) => {
+ const { t } = useTranslation()
+ return (
+ <>
+ {error.name && (
+
+ {t('error.name')}:
+ {error.message}
+
+ )}
+ {error.message && (
+
+ {t('error.message')}:
+ {error.message}
+
+ )}
+ {error.stack && (
+
+ {t('error.stack')}:
+
+ {error.stack}
+
+
+ )}
+ >
+ )
+}
+
+// 作为 base,渲染公共字段,应当在 ErrorDetailList 中渲染
+const AiSdkError = ({ error }: { error: AISDKError }) => {
+ const { t } = useTranslation()
+ const cause = safeToString(error.cause)
+ return (
+ <>
+
+ {cause && (
+
+ {t('error.cause')}:
+ {error.message}
+
+ )}
+ >
+ )
+}
+
+const AiApiCallError = ({ error }: { error: APICallError }) => {
+ const { t } = useTranslation()
+
+ // 这些字段是 unknown 类型,暂且不清楚都可能是什么类型,总之先覆盖下大部分场景
+ const requestBodyValues = safeToString(error.requestBodyValues)
+ const data = safeToString(error.data)
+
+ return (
+
+
+
+ {error.url && (
+
+ {t('error.requestUrl')}:
+ {error.url}
+
+ )}
+
+ {requestBodyValues && (
+
+ {t('error.requestBodyValues')}:
+ {error.message}
+
+ )}
+
+ {error.statusCode && (
+
+ {t('error.statusCode')}:
+ {error.message}
+
+ )}
+ {error.responseHeaders && (
+
+ {t('error.responseHeaders')}:
+ {error.message}
+
+ )}
+
+ {error.responseBody && (
+
+ {t('error.responseBody')}:
+
+ {JSON.stringify(error.responseBody, null, 2)}
+
+
+ )}
+
+ {data && (
+
+ {t('error.data')}:
+
+ {error.stack}
+
+
+ )}
+
+ )
+}
+
export default React.memo(ErrorBlock)
diff --git a/src/renderer/src/types/aiCoreTypes.ts b/src/renderer/src/types/aiCoreTypes.ts
index e93218ab9e..614211a5c7 100644
--- a/src/renderer/src/types/aiCoreTypes.ts
+++ b/src/renderer/src/types/aiCoreTypes.ts
@@ -1,4 +1,4 @@
-import type { ImageModel, LanguageModel } from 'ai'
+import type { AISDKError, APICallError, ImageModel, LanguageModel } from 'ai'
import { generateObject, generateText, ModelMessage, streamObject, streamText } from 'ai'
export type StreamTextParams = Omit[0], 'model' | 'messages'> &
@@ -27,3 +27,6 @@ export type StreamObjectParams = Omit[0], 'model
export type GenerateObjectParams = Omit[0], 'model'>
export type AiSdkModel = LanguageModel | ImageModel
+
+// 该类型用于格式化错误信息,目前只处理 APICallError,待扩展
+export type AiSdkErrorUnion = AISDKError | APICallError
diff --git a/src/renderer/src/utils/error.ts b/src/renderer/src/utils/error.ts
index 44f04aa036..5c9279c827 100644
--- a/src/renderer/src/utils/error.ts
+++ b/src/renderer/src/utils/error.ts
@@ -1,4 +1,5 @@
import { loggerService } from '@logger'
+import { AiSdkErrorUnion } from '@renderer/types/aiCoreTypes'
import { AISDKError, APICallError } from 'ai'
import { t } from 'i18next'
import z from 'zod'
@@ -123,3 +124,102 @@ export const formatZodError = (error: z.ZodError, title?: string) => {
const errorMessage = readableErrors.join('\n')
return title ? `${title}: \n${errorMessage}` : errorMessage
}
+
+/**
+ * 将任意值安全地转换为字符串
+ * @param value - 需要转换的值,unknown 类型
+ * @returns 转换后的字符串
+ *
+ * @description
+ * 该函数可以安全地处理以下情况:
+ * - null 和 undefined 会被转换为 'null'
+ * - 字符串直接返回
+ * - 原始类型(数字、布尔值、bigint等)使用 String() 转换
+ * - 对象和数组会尝试使用 JSON.stringify 序列化,并处理循环引用
+ * - 如果序列化失败,返回错误信息
+ *
+ * @example
+ * ```ts
+ * safeToString(null) // 'null'
+ * safeToString('test') // 'test'
+ * safeToString(123) // '123'
+ * safeToString({a: 1}) // '{"a":1}'
+ * ```
+ */
+export function safeToString(value: unknown): string {
+ // 处理 null 和 undefined
+ if (value == null) {
+ return 'null'
+ }
+
+ // 字符串直接返回
+ if (typeof value === 'string') {
+ return value
+ }
+
+ // 数字、布尔值、bigint 等原始类型,安全用 String()
+ if (typeof value !== 'object' && typeof value !== 'function') {
+ return String(value)
+ }
+
+ // 处理对象(包括数组)
+ if (typeof value === 'object') {
+ // 处理函数
+ if (typeof value === 'function') {
+ return value.toString()
+ }
+ // 其他对象
+ try {
+ return JSON.stringify(value, getCircularReplacer())
+ } catch (err) {
+ return '[Unserializable: ' + err + ']'
+ }
+ }
+
+ return String(value)
+}
+
+// 防止循环引用导致的 JSON.stringify 崩溃
+function getCircularReplacer() {
+ const seen = new WeakSet()
+ return (_key: string, value: unknown) => {
+ if (typeof value === 'object' && value !== null) {
+ if (seen.has(value)) {
+ return '[Circular]'
+ }
+ seen.add(value)
+ }
+ return value
+ }
+}
+
+export function formatError(error: Error): string {
+ return `${t('error.name')}: ${error.name}\n${t('error.message')}: ${error.message}\n${t('error.stack')}: ${error.stack}`
+}
+
+export function formatAiSdkError(error: AiSdkErrorUnion): string {
+ let text = formatError(error) + '\n'
+ if (error.cause) {
+ text += `${t('error.cause')}: ${error.cause}\n`
+ }
+ if (APICallError.isInstance(error)) {
+ if (error.statusCode) {
+ text += `${t('error.statusCode')}: ${error.statusCode}\n`
+ }
+ text += `${t('error.requestUrl')}: ${error.url}\n`
+ const requestBodyValues = safeToString(error.requestBodyValues)
+ text += `${t('error.requestBodyValues')}: ${requestBodyValues}\n`
+ if (error.responseHeaders) {
+ text += `${t('error.responseHeaders')}: ${error.responseHeaders}\n`
+ }
+ if (error.responseBody) {
+ text += `${t('error.responseBody')}: ${error.responseBody}\n`
+ }
+ if (error.data) {
+ const data = safeToString(error.data)
+ text += `${t('error.data')}: ${data}\n`
+ }
+ }
+
+ return text.trim()
+}