fix(evaluation): format provider error details
This commit is contained in:
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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in new issue
Block a user