diff --git a/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.test.ts b/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.test.ts index 04e1960b6..1fb0dd227 100644 --- a/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.test.ts +++ b/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; +import { ApiClientError } from '../../../services/apiClient'; import type { Model3dPricingConfig } from '../../../services/image-editor/editorProjectClient'; import type { CharacterReferenceImage, @@ -478,7 +479,11 @@ describe('resolveModel3dGenerationErrorMessage', () => { it('服务端给的文案原样透出', () => { expect( resolveModel3dGenerationErrorMessage( - new Error('3D 生成服务当前限流,请稍后重试。'), + new ApiClientError({ + message: '3D 生成服务当前限流,请稍后重试。', + status: 429, + code: 'tripo-api-rate-limited', + }), ), ).toBe('3D 生成服务当前限流,请稍后重试。'); expect(resolveModel3dGenerationErrorMessage('落点不可用')).toBe( @@ -486,6 +491,20 @@ describe('resolveModel3dGenerationErrorMessage', () => { ); }); + it('非服务端来源的错误不把技术文案透给用户', () => { + // 2xx 但 body 不是 JSON 时 requestJson 抛的是原生 SyntaxError。 + expect( + resolveModel3dGenerationErrorMessage( + new SyntaxError('Unexpected token < in JSON at position 0'), + ), + ).toBe(MODEL3D_GENERATION_FAILURE_MESSAGE); + expect( + resolveModel3dGenerationErrorMessage( + new TypeError("Cannot read properties of undefined (reading 'ok')"), + ), + ).toBe(MODEL3D_GENERATION_FAILURE_MESSAGE); + }); + it('传输层英文技术文案换成中文兜底', () => { expect( resolveModel3dGenerationErrorMessage(new TypeError('Failed to fetch')), diff --git a/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.ts b/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.ts index 4a79d1940..669d8e48d 100644 --- a/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.ts +++ b/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.ts @@ -4,7 +4,11 @@ import type { Model3dImageToModelRequest, Model3dTextToModelRequest, } from '../../../../packages/shared/src/contracts/model3d'; -import { isAbortError, isTimeoutError } from '../../../services/apiClient'; +import { + ApiClientError, + isAbortError, + isTimeoutError, +} from '../../../services/apiClient'; import type { EditorCanvasGenerationCompletionInput, Model3dGenerationSubmissionResponse, @@ -307,7 +311,9 @@ export function resolveModel3dGenerationErrorMessage(error: unknown): string { if (isAbortError(error) || isFetchFailureError(error)) { return MODEL3D_GENERATION_FAILURE_MESSAGE; } - if (error instanceof Error && error.message.trim()) { + // 只有 ApiClientError 的 message 是按服务端响应拼出来的;`JSON.parse` 失败的 SyntaxError + // 或其它运行时 TypeError 的 message 是英文技术文案,同样不能当用户提示。 + if (error instanceof ApiClientError && error.message.trim()) { return error.message.trim(); } if (typeof error === 'string' && error.trim()) { diff --git a/src/components/image-editor/model3d-generation/useModel3dGenerationTask.test.tsx b/src/components/image-editor/model3d-generation/useModel3dGenerationTask.test.tsx index 59ddc6ac3..b2398bb0e 100644 --- a/src/components/image-editor/model3d-generation/useModel3dGenerationTask.test.tsx +++ b/src/components/image-editor/model3d-generation/useModel3dGenerationTask.test.tsx @@ -5,6 +5,7 @@ import { useState } from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { ExternalGenerationJobStatusRecord } from '../../../../packages/shared/src/contracts/externalGeneration'; +import { ApiClientError } from '../../../services/apiClient'; import type { EditorCanvasGenerationCompletionInput, Model3dGenerationSubmissionResponse, @@ -256,7 +257,11 @@ describe('useModel3dGenerationTask', () => { it('请求失败时留在面板上展示失败原因', async () => { submitModel3dTextToModelRequestMock.mockRejectedValue( - new Error('3D 模型生成失败,请稍后重试。'), + new ApiClientError({ + message: '3D 模型生成失败,请稍后重试。', + status: 502, + code: 'tripo-upstream-error', + }), ); const { result } = renderTask({ dialog: createDialog() });