fix: Optimize error message formatting (#5988)

* fix: Optimize error message formatting

* fix: improve error unit test

* refactor: simplify error handling in ErrorBlock component

- Replaced custom StyledAlert with a more streamlined Alert component for error messages.
- Reduced complexity by removing unnecessary JSX wrappers and improving readability.
- Adjusted styling for the Alert component to maintain visual consistency.

* fix: update error handling in ErrorBlock component

- Removed unnecessary message prop from Alert component to simplify error display.
- Maintained existing error handling logic while improving code clarity.
This commit is contained in:
SuYao 2025-05-27 21:45:04 +08:00 committed by GitHub
parent 6e50030237
commit aadcbc67cf
5 changed files with 55 additions and 30 deletions

View File

@ -84,6 +84,7 @@ export type OpenAIStreamChunk =
| { type: 'reasoning' | 'text-delta'; textDelta: string } | { type: 'reasoning' | 'text-delta'; textDelta: string }
| { type: 'tool-calls'; delta: any } | { type: 'tool-calls'; delta: any }
| { type: 'finish'; finishReason: any; usage: any; delta: any; chunk: any } | { type: 'finish'; finishReason: any; usage: any; delta: any; chunk: any }
| { type: 'unknown'; chunk: any }
export default class OpenAIProvider extends BaseOpenAIProvider { export default class OpenAIProvider extends BaseOpenAIProvider {
constructor(provider: Provider) { constructor(provider: Provider) {
@ -631,21 +632,25 @@ export default class OpenAIProvider extends BaseOpenAIProvider {
break break
} }
const delta = chunk.choices[0]?.delta if (chunk.choices && chunk.choices.length > 0) {
if (delta?.reasoning_content || delta?.reasoning) { const delta = chunk.choices[0]?.delta
yield { type: 'reasoning', textDelta: delta.reasoning_content || delta.reasoning } if (delta?.reasoning_content || delta?.reasoning) {
} yield { type: 'reasoning', textDelta: delta.reasoning_content || delta.reasoning }
if (delta?.content) { }
yield { type: 'text-delta', textDelta: delta.content } if (delta?.content) {
} yield { type: 'text-delta', textDelta: delta.content }
if (delta?.tool_calls) { }
yield { type: 'tool-calls', delta: delta } if (delta?.tool_calls) {
} yield { type: 'tool-calls', delta: delta }
}
const finishReason = chunk.choices[0]?.finish_reason const finishReason = chunk?.choices[0]?.finish_reason
if (!isEmpty(finishReason)) { if (!isEmpty(finishReason)) {
yield { type: 'finish', finishReason, usage: chunk.usage, delta, chunk } yield { type: 'finish', finishReason, usage: chunk.usage, delta, chunk }
break break
}
} else {
yield { type: 'unknown', chunk }
} }
} }
} }
@ -832,6 +837,12 @@ export default class OpenAIProvider extends BaseOpenAIProvider {
} }
break break
} }
case 'unknown': {
onChunk({
type: ChunkType.ERROR,
error: chunk.chunk
})
}
} }
} }

View File

@ -88,6 +88,9 @@ export function createStreamProcessor(callbacks: StreamProcessorCallbacks = {})
if (data.type === ChunkType.IMAGE_COMPLETE && callbacks.onImageGenerated) { if (data.type === ChunkType.IMAGE_COMPLETE && callbacks.onImageGenerated) {
callbacks.onImageGenerated(data.image) callbacks.onImageGenerated(data.image)
} }
if (data.type === ChunkType.ERROR && callbacks.onError) {
callbacks.onError(data.error)
}
// Note: Usage and Metrics are usually handled at the end or accumulated differently, // Note: Usage and Metrics are usually handled at the end or accumulated differently,
// so direct callbacks might not be the best fit here. They are often part of the final message state. // so direct callbacks might not be the best fit here. They are often part of the final message state.
} catch (error) { } catch (error) {

View File

@ -20,7 +20,7 @@ import type {
import { AssistantMessageStatus, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage' import { AssistantMessageStatus, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
import { Response } from '@renderer/types/newMessage' import { Response } from '@renderer/types/newMessage'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
import { isAbortError } from '@renderer/utils/error' import { formatErrorMessage, isAbortError } from '@renderer/utils/error'
import { extractUrlsFromMarkdown } from '@renderer/utils/linkConverter' import { extractUrlsFromMarkdown } from '@renderer/utils/linkConverter'
import { import {
createAssistantMessage, createAssistantMessage,
@ -587,7 +587,7 @@ const fetchAndProcessAssistantResponseImpl = async (
const serializableError = { const serializableError = {
name: error.name, name: error.name,
message: pauseErrorLanguagePlaceholder || error.message || 'Stream processing error', message: pauseErrorLanguagePlaceholder || error.message || formatErrorMessage(error),
originalMessage: error.message, originalMessage: error.message,
stack: error.stack, stack: error.stack,
status: error.status || error.code, status: error.status || error.code,

View File

@ -50,19 +50,21 @@ describe('error', () => {
}) })
describe('formatErrorMessage', () => { describe('formatErrorMessage', () => {
it('should format error as JSON string', () => { it('should format error with indentation and header', () => {
console.error = vi.fn() // Mock console.error console.error = vi.fn()
const error = new Error('Test error') const error = new Error('Test error')
const result = formatErrorMessage(error) const result = formatErrorMessage(error)
expect(console.error).toHaveBeenCalled() expect(console.error).toHaveBeenCalled()
expect(result).toContain('```json') expect(result).toContain('Error Details:')
expect(result).toContain('"message": "Test error"') expect(result).toContain(' {')
expect(result).toContain(' "message": "Test error"')
expect(result).toContain(' }')
expect(result).not.toContain('"stack":') expect(result).not.toContain('"stack":')
}) })
it('should remove sensitive information', () => { it('should remove sensitive information and format with proper indentation', () => {
console.error = vi.fn() console.error = vi.fn()
const error = { const error = {
@ -74,27 +76,30 @@ describe('error', () => {
const result = formatErrorMessage(error) const result = formatErrorMessage(error)
expect(result).toContain('"message": "API error"') expect(result).toContain('Error Details:')
expect(result).toContain(' {')
expect(result).toContain(' "message": "API error"')
expect(result).toContain(' }')
expect(result).not.toContain('Authorization') expect(result).not.toContain('Authorization')
expect(result).not.toContain('stack') expect(result).not.toContain('stack')
expect(result).not.toContain('request_id') expect(result).not.toContain('request_id')
}) })
it('should handle errors during formatting', () => { it('should handle errors during formatting with simple error message', () => {
console.error = vi.fn() console.error = vi.fn()
const problematicError = { const problematicError = {
get message() { get message() {
throw new Error('Cannot access message') throw new Error('Cannot access')
} }
} }
const result = formatErrorMessage(problematicError) const result = formatErrorMessage(problematicError)
expect(result).toContain('```') expect(result).toContain('Error Details:')
expect(result).toContain('Unable') expect(result).toContain('"message": "<Unable to access property>"')
}) })
it('should handle non-serializable errors', () => { it('should handle non-serializable errors with simple error message', () => {
console.error = vi.fn() console.error = vi.fn()
const nonSerializableError = { const nonSerializableError = {
@ -114,7 +119,8 @@ describe('error', () => {
} }
const result = formatErrorMessage(nonSerializableError) const result = formatErrorMessage(nonSerializableError)
expect(result).toBeTruthy() expect(result).toContain('Error Details:')
expect(result).toContain('"toString": "<Unable to access property>"')
}) })
}) })

View File

@ -35,10 +35,15 @@ export function formatErrorMessage(error: any): string {
delete detailedError?.headers delete detailedError?.headers
delete detailedError?.stack delete detailedError?.stack
delete detailedError?.request_id delete detailedError?.request_id
return '```json\n' + JSON.stringify(detailedError, null, 2) + '\n```'
const formattedJson = JSON.stringify(detailedError, null, 2)
.split('\n')
.map((line) => ` ${line}`)
.join('\n')
return `Error Details:\n${formattedJson}`
} catch (e) { } catch (e) {
try { try {
return '```\n' + String(error) + '\n```' return `Error: ${String(error)}`
} catch { } catch {
return 'Error: Unable to format error message' return 'Error: Unable to format error message'
} }