Skip to content

Commit 15772c0

Browse files
committed
refactor(error-handling): improve error message localization and standardize error mapping
- Add `getErrorMessage` import to LLM types for consistent error messaging - Update `LLMError.fromAISDKError()` to use English as default language with frontend localization - Enhance stream processor error handling to support error code-based internationalization - Integrate language preference from store in McpService error messages - Refactor `mapNodeError()` to return error metadata instead of AppError instances - Refactor `mapAISDKError()` to return original messages for logging, not user-facing messages - Extract detailed error information from API response bodies for better debugging - Separate concerns between error mapping (technical details) and message localization (user-facing text) - Improve error context preservation throughout the error handling pipeline
1 parent d60b1f4 commit 15772c0

File tree

4 files changed

+83
-62
lines changed

4 files changed

+83
-62
lines changed

src/main/services/llm/types.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import type { LanguageModelUsage } from 'ai'
6-
import { mapAISDKError, ErrorCode } from '@shared/utils/errorHandler'
6+
import { mapAISDKError, ErrorCode, getErrorMessage } from '@shared/utils/errorHandler'
77

88
// ============================================
99
// 基础类型
@@ -52,10 +52,13 @@ export class LLMError extends Error {
5252

5353
/**
5454
* 从 AI SDK 错误创建 LLMError
55+
* 默认使用英文消息(前端会根据用户语言设置转换)
5556
*/
5657
static fromAISDKError(error: Error, status?: number): LLMError {
5758
const mapped = mapAISDKError(error)
58-
return new LLMError(mapped.message, mapped.code, mapped.retryable, status, error)
59+
// 使用英文作为默认语言,前端会根据用户设置转换
60+
const friendlyMessage = getErrorMessage(mapped.code, 'en')
61+
return new LLMError(friendlyMessage, mapped.code, mapped.retryable, status, error)
5962
}
6063

6164
/**

src/renderer/agent/core/stream.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
import { api } from '@/renderer/services/electronAPI'
1111
import { logger } from '@utils/Logger'
1212
import { useAgentStore } from '../store/AgentStore'
13+
import { useStore } from '@store'
1314
import { parseXMLToolCalls } from '../utils/XMLToolParser'
1415
import { EventBus } from './EventBus'
16+
import { getErrorMessage, ErrorCode } from '@shared/utils/errorHandler'
1517
import type { ToolCall, TokenUsage } from '../types'
1618
import type { LLMCallResult } from './types'
1719

@@ -343,7 +345,19 @@ export function createStreamProcessor(assistantId: string | null): StreamProcess
343345

344346
// 处理错误事件
345347
const handleError = (err: { message?: string; code?: string } | string) => {
346-
const errorMsg = typeof err === 'string' ? err : err.message || 'Unknown error'
348+
let errorMsg: string
349+
350+
if (typeof err === 'string') {
351+
errorMsg = err
352+
} else {
353+
// 如果有错误码,使用国际化消息
354+
if (err.code && err.code in ErrorCode) {
355+
const language = useStore.getState().language
356+
errorMsg = getErrorMessage(err.code as ErrorCode, language)
357+
} else {
358+
errorMsg = err.message || 'Unknown error'
359+
}
360+
}
347361

348362
logger.agent.error('[StreamProcessor] Error:', errorMsg)
349363
error = errorMsg

src/renderer/services/mcpService.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ class McpService {
5353
} catch (err) {
5454
const error = toAppError(err)
5555
logger.agent.error(`[McpService] Initialize failed: ${error.code}`, error)
56-
const userMessage = getErrorMessage(error.code, 'zh')
56+
const language = store.getState().language
57+
const userMessage = getErrorMessage(error.code, language)
5758
store.setMcpError(userMessage)
5859
throw error
5960
} finally {
@@ -75,7 +76,8 @@ class McpService {
7576
} catch (err) {
7677
const error = toAppError(err)
7778
logger.agent.error(`[McpService] Reinitialize failed: ${error.code}`, error)
78-
const userMessage = getErrorMessage(error.code, 'zh')
79+
const language = store.getState().language
80+
const userMessage = getErrorMessage(error.code, language)
7981
store.setMcpError(userMessage)
8082
} finally {
8183
store.setMcpLoading(false)

src/shared/utils/errorHandler.ts

Lines changed: 59 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -195,71 +195,49 @@ export function getErrorMessage(code: ErrorCode, language: 'en' | 'zh' = 'en'):
195195

196196
/**
197197
* 映射 Node.js 系统错误
198+
* 返回错误码和原始消息,不返回友好消息
198199
*/
199-
export function mapNodeError(error: NodeJS.ErrnoException): AppError {
200+
export function mapNodeError(error: NodeJS.ErrnoException): { code: ErrorCode; originalMessage: string; retryable: boolean } {
200201
const code = error.code || ''
202+
const originalMessage = error.message
201203

202204
switch (code) {
203205
case 'ENOENT':
204-
return new AppError(
205-
error.message,
206-
ErrorCode.FILE_NOT_FOUND,
207-
false,
208-
error
209-
)
206+
return { code: ErrorCode.FILE_NOT_FOUND, originalMessage, retryable: false }
210207

211208
case 'EACCES':
212209
case 'EPERM':
213-
return new AppError(
214-
error.message,
215-
ErrorCode.FILE_ACCESS_DENIED,
216-
false,
217-
error
218-
)
210+
return { code: ErrorCode.FILE_ACCESS_DENIED, originalMessage, retryable: false }
219211

220212
case 'ETIMEDOUT':
221213
case 'ESOCKETTIMEDOUT':
222-
return new AppError(
223-
error.message,
224-
ErrorCode.TIMEOUT,
225-
true,
226-
error
227-
)
214+
return { code: ErrorCode.TIMEOUT, originalMessage, retryable: true }
228215

229216
case 'ECONNREFUSED':
230217
case 'ENOTFOUND':
231218
case 'ENETUNREACH':
232-
return new AppError(
233-
error.message,
234-
ErrorCode.NETWORK,
235-
true,
236-
error
237-
)
219+
return { code: ErrorCode.NETWORK, originalMessage, retryable: true }
238220

239221
default:
240-
return new AppError(
241-
error.message || 'System error',
242-
ErrorCode.UNKNOWN,
243-
false,
244-
error
245-
)
222+
return { code: ErrorCode.UNKNOWN, originalMessage: originalMessage || 'System error', retryable: false }
246223
}
247224
}
248225

249226
/**
250227
* 映射 AI SDK 错误(使用类型安全的 isInstance 方法)
228+
* 返回 ErrorCode 和原始错误消息(用于日志),不返回友好消息
251229
*/
252-
export function mapAISDKError(error: unknown): { code: ErrorCode; message: string; retryable: boolean } {
230+
export function mapAISDKError(error: unknown): { code: ErrorCode; originalMessage: string; retryable: boolean } {
253231
// 确保是 Error 对象
254232
if (!(error instanceof Error)) {
255233
return {
256234
code: ErrorCode.UNKNOWN,
257-
message: String(error),
235+
originalMessage: String(error),
258236
retryable: false,
259237
}
260238
}
261239

262-
const errorMessage = error.message
240+
const originalMessage = error.message
263241

264242
// NoOutputGeneratedError - 通常包装了其他错误,优先提取 cause
265243
if (NoOutputGeneratedError.isInstance(error)) {
@@ -269,7 +247,7 @@ export function mapAISDKError(error: unknown): { code: ErrorCode; message: strin
269247
}
270248
return {
271249
code: ErrorCode.LLM_NO_OUTPUT,
272-
message: errorMessage,
250+
originalMessage,
273251
retryable: true,
274252
}
275253
}
@@ -282,7 +260,7 @@ export function mapAISDKError(error: unknown): { code: ErrorCode; message: strin
282260
}
283261
return {
284262
code: ErrorCode.UNKNOWN,
285-
message: errorMessage,
263+
originalMessage,
286264
retryable: false,
287265
}
288266
}
@@ -291,31 +269,48 @@ export function mapAISDKError(error: unknown): { code: ErrorCode; message: strin
291269
if (NoContentGeneratedError.isInstance(error)) {
292270
return {
293271
code: ErrorCode.LLM_NO_CONTENT,
294-
message: errorMessage,
272+
originalMessage,
295273
retryable: true,
296274
}
297275
}
298276

299277
// APICallError - 根据状态码细分
300278
if (APICallError.isInstance(error)) {
301279
const statusCode = (error as any).statusCode
280+
const responseBody = (error as any).responseBody
281+
282+
// 尝试从 responseBody 提取详细信息
283+
let detailMessage = originalMessage
284+
if (responseBody && typeof responseBody === 'string') {
285+
try {
286+
const body = JSON.parse(responseBody)
287+
if (body.detail) {
288+
detailMessage = `${originalMessage}: ${body.detail}`
289+
} else if (body.message) {
290+
detailMessage = `${originalMessage}: ${body.message}`
291+
}
292+
} catch {
293+
// JSON 解析失败,使用原始消息
294+
}
295+
}
296+
302297
if (statusCode === 429) {
303298
return {
304299
code: ErrorCode.API_RATE_LIMIT,
305-
message: errorMessage,
300+
originalMessage: detailMessage,
306301
retryable: true,
307302
}
308303
}
309304
if (statusCode === 401 || statusCode === 403) {
310305
return {
311306
code: ErrorCode.API_KEY_INVALID,
312-
message: errorMessage,
307+
originalMessage: detailMessage,
313308
retryable: false,
314309
}
315310
}
316311
return {
317312
code: ErrorCode.API_CALL_FAILED,
318-
message: errorMessage,
313+
originalMessage: detailMessage,
319314
retryable: (error as any).isRetryable ?? true,
320315
}
321316
}
@@ -324,7 +319,7 @@ export function mapAISDKError(error: unknown): { code: ErrorCode; message: strin
324319
if (InvalidPromptError.isInstance(error)) {
325320
return {
326321
code: ErrorCode.LLM_INVALID_PROMPT,
327-
message: errorMessage,
322+
originalMessage,
328323
retryable: false,
329324
}
330325
}
@@ -333,7 +328,7 @@ export function mapAISDKError(error: unknown): { code: ErrorCode; message: strin
333328
if (InvalidResponseDataError.isInstance(error)) {
334329
return {
335330
code: ErrorCode.LLM_INVALID_RESPONSE,
336-
message: errorMessage,
331+
originalMessage,
337332
retryable: true,
338333
}
339334
}
@@ -342,7 +337,7 @@ export function mapAISDKError(error: unknown): { code: ErrorCode; message: strin
342337
if (EmptyResponseBodyError.isInstance(error)) {
343338
return {
344339
code: ErrorCode.LLM_EMPTY_RESPONSE,
345-
message: errorMessage,
340+
originalMessage,
346341
retryable: true,
347342
}
348343
}
@@ -351,7 +346,7 @@ export function mapAISDKError(error: unknown): { code: ErrorCode; message: strin
351346
if (LoadAPIKeyError.isInstance(error)) {
352347
return {
353348
code: ErrorCode.API_KEY_INVALID,
354-
message: errorMessage,
349+
originalMessage,
355350
retryable: false,
356351
}
357352
}
@@ -360,7 +355,7 @@ export function mapAISDKError(error: unknown): { code: ErrorCode; message: strin
360355
if (NoSuchModelError.isInstance(error)) {
361356
return {
362357
code: ErrorCode.LLM_NO_SUCH_MODEL,
363-
message: errorMessage,
358+
originalMessage,
364359
retryable: false,
365360
}
366361
}
@@ -369,7 +364,7 @@ export function mapAISDKError(error: unknown): { code: ErrorCode; message: strin
369364
if (TypeValidationError.isInstance(error)) {
370365
return {
371366
code: ErrorCode.LLM_VALIDATION_FAILED,
372-
message: errorMessage,
367+
originalMessage,
373368
retryable: false,
374369
}
375370
}
@@ -378,7 +373,7 @@ export function mapAISDKError(error: unknown): { code: ErrorCode; message: strin
378373
if (UnsupportedFunctionalityError.isInstance(error)) {
379374
return {
380375
code: ErrorCode.LLM_UNSUPPORTED,
381-
message: errorMessage,
376+
originalMessage,
382377
retryable: false,
383378
}
384379
}
@@ -387,40 +382,41 @@ export function mapAISDKError(error: unknown): { code: ErrorCode; message: strin
387382
if (error.name === 'AbortError') {
388383
return {
389384
code: ErrorCode.ABORTED,
390-
message: errorMessage,
385+
originalMessage,
391386
retryable: false,
392387
}
393388
}
394389

395390
// 检查错误消息中的关键词(兜底)
396-
const msg = errorMessage.toLowerCase()
391+
const msg = originalMessage.toLowerCase()
397392
if (msg.includes('network') || msg.includes('fetch') || msg.includes('econnrefused')) {
398393
return {
399394
code: ErrorCode.NETWORK,
400-
message: errorMessage,
395+
originalMessage,
401396
retryable: true,
402397
}
403398
}
404399
if (msg.includes('timeout')) {
405400
return {
406401
code: ErrorCode.TIMEOUT,
407-
message: errorMessage,
402+
originalMessage,
408403
retryable: true,
409404
}
410405
}
411406

412407
// 未知错误
413408
return {
414409
code: ErrorCode.UNKNOWN,
415-
message: errorMessage,
410+
originalMessage,
416411
retryable: false,
417412
}
418413
}
419414

420415
/**
421416
* 将任意错误转换为 AppError
417+
* 使用英文友好消息(前端可根据用户语言转换)
422418
*/
423-
export function toAppError(error: unknown): AppError {
419+
export function toAppError(error: unknown, language: 'en' | 'zh' = 'en'): AppError {
424420
if (error instanceof AppError) {
425421
return error
426422
}
@@ -429,15 +425,21 @@ export function toAppError(error: unknown): AppError {
429425
// Node.js 系统错误
430426
const nodeError = error as NodeJS.ErrnoException
431427
if (nodeError.code) {
432-
return mapNodeError(nodeError)
428+
const mapped = mapNodeError(nodeError)
429+
const friendlyMessage = getErrorMessage(mapped.code, language)
430+
return new AppError(friendlyMessage, mapped.code, mapped.retryable, error)
433431
}
434432

435-
return new AppError(error.message, ErrorCode.UNKNOWN, false, error)
433+
// 普通 Error
434+
const friendlyMessage = getErrorMessage(ErrorCode.UNKNOWN, language)
435+
return new AppError(friendlyMessage, ErrorCode.UNKNOWN, false, error)
436436
}
437437

438438
if (typeof error === 'string') {
439-
return new AppError(error, ErrorCode.UNKNOWN, false)
439+
const friendlyMessage = getErrorMessage(ErrorCode.UNKNOWN, language)
440+
return new AppError(friendlyMessage, ErrorCode.UNKNOWN, false)
440441
}
441442

442-
return new AppError('An unexpected error occurred', ErrorCode.UNKNOWN, false, error)
443+
const friendlyMessage = getErrorMessage(ErrorCode.UNKNOWN, language)
444+
return new AppError(friendlyMessage, ErrorCode.UNKNOWN, false, error)
443445
}

0 commit comments

Comments
 (0)