fix(evaluation): format provider error details

This commit is contained in:
linshen committed 2026-06-07 10:28:39 +08:00
1 parent e039257871
commit ecbe788909
4 files changed
+224 -17

No files matched your search

@@ -200,6 +200,75 @@ export class EvaluationService implements IEvaluationService {
this.imageUnderstandingService = dependencies.imageUnderstandingService;
}
private formatExecutionErrorMessage(error: unknown, fallback = 'Unknown evaluation execution error'): string {
if (typeof error === 'string') {
return error.trim() || fallback;
}
if (error instanceof Error) {
const message = error.message?.trim();
if (message && !/^\[object .+\]$/.test(message)) {
return message;
}
const cause = (error as Error & { cause?: unknown }).cause;
if (cause !== undefined) {
return this.formatExecutionErrorMessage(cause, fallback);
}
return message || fallback;
}
if (error && typeof error === 'object') {
const record = error as Record<string, unknown>;
const directMessage = record.message;
if (typeof directMessage === 'string' && directMessage.trim() && !/^\[object .+\]$/.test(directMessage.trim())) {
return directMessage.trim();
}
if (directMessage !== undefined && directMessage !== error) {
return this.formatExecutionErrorMessage(directMessage, fallback);
}
const nestedError = record.error;
if (nestedError !== undefined && nestedError !== error) {
const nestedMessage = this.formatExecutionErrorMessage(nestedError, '');
if (nestedMessage) {
return nestedMessage;
}
}
const status = record.status ?? record.statusCode ?? (record.response as Record<string, unknown> | undefined)?.status;
const body = record.body ?? record.data ?? (record.response as Record<string, unknown> | undefined)?.data;
const bodyMessage = body !== undefined && body !== error
? this.formatExecutionErrorMessage(body, '')
: '';
if (status !== undefined) {
return bodyMessage
? `HTTP ${String(status)}: ${bodyMessage}`
: `HTTP ${String(status)} error`;
}
try {
return JSON.stringify(error);
} catch {
return fallback;
}
}
if (error === null || error === undefined) {
return fallback;
}
return String(error);
}
private toError(error: unknown): Error {
return error instanceof Error
? error
: new Error(this.formatExecutionErrorMessage(error));
}
/**
* 执行评估(非流式)
*/
@@ -251,7 +320,7 @@ export class EvaluationService implements IEvaluationService {
return this.parseEvaluationResult(result, request.type, responseMetadata);
} catch (error) {
throw new EvaluationExecutionError(
error instanceof Error ? error.message : String(error),
this.formatExecutionErrorMessage(error),
error instanceof Error ? error : undefined
);
}
@@ -267,7 +336,7 @@ export class EvaluationService implements IEvaluationService {
try {
this.validateRequest(request);
} catch (error) {
callbacks.onError(error instanceof Error ? error : new Error(String(error)));
callbacks.onError(this.toError(error));
return;
}
@@ -275,7 +344,7 @@ export class EvaluationService implements IEvaluationService {
const modelConfig = await this.validateModel(request.evaluationModelKey);
await this.evaluateStreamInternal(request, callbacks, modelConfig);
} catch (error) {
callbacks.onError(error instanceof Error ? error : new Error(String(error)));
callbacks.onError(this.toError(error));
}
}
@@ -291,7 +360,7 @@ export class EvaluationService implements IEvaluationService {
try {
await this.evaluateStructuredCompareStream(request, normalizedCompare, callbacks);
} catch (error) {
callbacks.onError(error instanceof Error ? error : new Error(String(error)));
callbacks.onError(this.toError(error));
}
return;
}
@@ -301,7 +370,7 @@ export class EvaluationService implements IEvaluationService {
try {
template = await this.getEvaluationTemplate(request.type, request.mode);
} catch (error) {
callbacks.onError(error instanceof Error ? error : new Error(String(error)));
callbacks.onError(this.toError(error));
return;
}
@@ -328,7 +397,7 @@ export class EvaluationService implements IEvaluationService {
);
callbacks.onComplete(response);
} catch (error) {
callbacks.onError(error instanceof Error ? error : new Error(String(error)));
callbacks.onError(this.toError(error));
}
return;
}
@@ -357,11 +426,11 @@ export class EvaluationService implements IEvaluationService {
);
callbacks.onComplete(response);
} catch (error) {
callbacks.onError(error instanceof Error ? error : new Error(String(error)));
callbacks.onError(this.toError(error));
}
},
onError: (error) => {
callbacks.onError(new EvaluationExecutionError(error.message, error));
callbacks.onError(new EvaluationExecutionError(this.formatExecutionErrorMessage(error), error));
},
};
@@ -370,7 +439,7 @@ export class EvaluationService implements IEvaluationService {
} catch (error) {
callbacks.onError(
new EvaluationExecutionError(
error instanceof Error ? error.message : String(error),
this.formatExecutionErrorMessage(error),
error instanceof Error ? error : undefined
)
);
@@ -2053,7 +2122,7 @@ export class EvaluationService implements IEvaluationService {
return this.parseEvaluationResult(synthesisResult, request.type, responseMetadata);
} catch (error) {
throw new EvaluationExecutionError(
error instanceof Error ? error.message : String(error),
this.formatExecutionErrorMessage(error),
error instanceof Error ? error : undefined
);
}
@@ -2114,17 +2183,17 @@ export class EvaluationService implements IEvaluationService {
);
callbacks.onComplete(response);
} catch (error) {
callbacks.onError(error instanceof Error ? error : new Error(String(error)));
callbacks.onError(this.toError(error));
}
},
onError: (error) => {
callbacks.onError(new EvaluationExecutionError(error.message, error));
callbacks.onError(new EvaluationExecutionError(this.formatExecutionErrorMessage(error), error));
},
});
} catch (error) {
callbacks.onError(
new EvaluationExecutionError(
error instanceof Error ? error.message : String(error),
this.formatExecutionErrorMessage(error),
error instanceof Error ? error : undefined
)
);
@@ -1,6 +1,7 @@
import { vi, describe, beforeEach, it, expect, afterEach } from 'vitest'
import { EvaluationService } from '../../../src/services/evaluation/service'
import {
EvaluationExecutionError,
EvaluationValidationError,
EvaluationModelError,
EvaluationTemplateError,
@@ -351,6 +352,30 @@ describe('EvaluationService', () => {
})
})
describe('execution errors', () => {
it('formats plain object provider errors without leaking [object Object]', async () => {
mockLLMService.sendMessage.mockRejectedValueOnce({
status: 429,
body: {
error: {
message: 'rate limit exceeded',
},
},
})
try {
await evaluationService.evaluate(createPromptOnlyRequest())
throw new Error('Expected evaluation to fail')
} catch (error) {
expect(error).toBeInstanceOf(EvaluationExecutionError)
expect((error as Error).message).toContain('HTTP 429')
expect((error as Error).message).toContain('rate limit exceeded')
expect((error as Error).message).not.toContain('[object Object]')
expect((error as EvaluationExecutionError).params?.details).not.toContain('[object Object]')
}
})
})
describe('evaluateStream', () => {
it('calls callbacks correctly for prompt-only', async () => {
const onToken = vi.fn()
+79 -4
View File
@@ -36,6 +36,19 @@ export class AppError extends Error {
}
}
function isObjectString(value: string): boolean {
return /^\[object .+\]$/.test(value)
}
function stringifyUnknownObject(value: unknown, fallback: string): string {
try {
const serialized = JSON.stringify(value)
return serialized && !isObjectString(serialized) ? serialized : fallback
} catch {
return fallback
}
}
/**
* 从未知类型的错误中提取错误消息
* @param error - 未知类型的错误对象
@@ -44,10 +57,20 @@ export class AppError extends Error {
*/
export function getErrorMessage(error: unknown, fallback = 'Unknown error'): string {
if (error instanceof Error) {
return error.message
const message = error.message?.trim()
if (message && !isObjectString(message)) {
return message
}
const cause = (error as Error & { cause?: unknown }).cause
if (cause !== undefined) {
return getErrorMessage(cause, fallback)
}
return message || fallback
}
if (typeof error === 'string') {
return error
return error.trim() && !isObjectString(error.trim()) ? error : fallback
}
if (error === null || error === undefined) {
return fallback
@@ -57,8 +80,30 @@ export function getErrorMessage(error: unknown, fallback = 'Unknown error'): str
if (typeof error === 'object') {
const maybeMessage = (error as { message?: unknown }).message
if (typeof maybeMessage === 'string' && maybeMessage.trim()) {
return maybeMessage
return isObjectString(maybeMessage.trim()) ? fallback : maybeMessage
}
if (maybeMessage !== undefined && maybeMessage !== error) {
return getErrorMessage(maybeMessage, fallback)
}
const maybeError = (error as { error?: unknown }).error
if (maybeError !== undefined && maybeError !== error) {
return getErrorMessage(maybeError, fallback)
}
const status = (error as { status?: unknown; statusCode?: unknown; response?: { status?: unknown } }).status
?? (error as { statusCode?: unknown }).statusCode
?? (error as { response?: { status?: unknown } }).response?.status
const body = (error as { body?: unknown; data?: unknown; response?: { data?: unknown } }).body
?? (error as { data?: unknown }).data
?? (error as { response?: { data?: unknown } }).response?.data
if (status !== undefined) {
const detail = body !== undefined && body !== error ? getErrorMessage(body, '') : ''
return detail ? `HTTP ${String(status)}: ${detail}` : `HTTP ${String(status)} error`
}
return stringifyUnknownObject(error, fallback)
}
try {
@@ -72,6 +117,30 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}
function hasReadableErrorShape(value: Record<string, unknown>): boolean {
return (
'message' in value ||
'error' in value ||
'status' in value ||
'statusCode' in value ||
'response' in value ||
'code' in value
)
}
function normalizeI18nParams(params: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
if (!params) return undefined
return Object.fromEntries(
Object.entries(params).map(([key, value]) => [
key,
isRecord(value) || Array.isArray(value)
? getErrorMessage(value, stringifyUnknownObject(value, 'Unknown error'))
: value,
])
)
}
/**
* 将结构化错误(code + params)转换为用户可读的 i18n 文案。
*
@@ -91,7 +160,7 @@ export function getI18nErrorMessage(error: unknown, fallback = 'Unknown error'):
const hasKey = i18n.global.te(code)
if (hasKey) {
try {
return i18n.global.t(code, params ?? {})
return i18n.global.t(code, normalizeI18nParams(params) ?? {})
} catch {
// If interpolation fails for any reason, fall back to raw error message.
}
@@ -111,7 +180,13 @@ export function getI18nErrorMessage(error: unknown, fallback = 'Unknown error'):
export function formatErrorSummary(summary: string, error: unknown, fallback = 'Unknown error'): string {
const normalizedSummary = summary.trim()
const detail = getI18nErrorMessage(error, fallback).trim()
const suppressPlainObjectDetail =
isRecord(error) &&
!(error instanceof Error) &&
!Array.isArray(error) &&
!hasReadableErrorShape(error)
const hasMeaningfulDetail =
!suppressPlainObjectDetail &&
detail &&
detail !== fallback &&
detail !== normalizedSummary &&
@@ -59,6 +59,44 @@ describe('getI18nErrorMessage', () => {
expect(msg).toBe('RAW_MESSAGE')
})
it('plain object provider errors are formatted without [object Object]', () => {
const msg = getI18nErrorMessage({
status: 429,
body: {
error: {
message: 'rate limit exceeded',
},
},
})
expect(msg).toContain('HTTP 429')
expect(msg).toContain('rate limit exceeded')
expect(msg).not.toContain('[object Object]')
})
it('normalizes object i18n params before interpolation', () => {
setLocale('zh-CN')
const msg = getI18nErrorMessage({
code: 'error.evaluation.execution',
params: {
details: {
status: 429,
body: {
error: {
message: 'rate limit exceeded',
},
},
},
},
})
expect(msg).toContain('评估执行错误')
expect(msg).toContain('HTTP 429')
expect(msg).toContain('rate limit exceeded')
expect(msg).not.toContain('[object Object]')
})
it('formatErrorSummary 在只有英文 fallback 时不重复拼接详情', () => {
const result = formatErrorSummary('Failed to save configuration', { foo: 'bar' }, 'Unknown error')
expect(result).toBe('Failed to save configuration')