diff --git a/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.test.ts b/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.test.ts index 3fc42c3e6..7f63af339 100644 --- a/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.test.ts +++ b/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.test.ts @@ -11,9 +11,12 @@ import { } from './Model3dGenerationFormModel'; import { buildModel3dSubmissionPlan, + MODEL3D_GENERATION_FAILURE_MESSAGE, + MODEL3D_GENERATION_TIMEOUT_MESSAGE, MODEL3D_IMAGE_SOURCE_REQUIRED_MESSAGE, MODEL3D_PROJECT_REQUIRED_MESSAGE, MODEL3D_TEXT_PROMPT_REQUIRED_MESSAGE, + resolveModel3dGenerationErrorMessage, resolveModel3dImageSource, resolveModel3dRequestKey, resolveModel3dResultTitle, @@ -336,3 +339,39 @@ describe('Idempotency-Key', () => { expect(right).toBe(left); }); }); + +describe('resolveModel3dGenerationErrorMessage', () => { + it('服务端给的文案原样透出', () => { + expect( + resolveModel3dGenerationErrorMessage( + new Error('3D 生成服务当前限流,请稍后重试。'), + ), + ).toBe('3D 生成服务当前限流,请稍后重试。'); + expect(resolveModel3dGenerationErrorMessage('落点不可用')).toBe( + '落点不可用', + ); + }); + + it('传输层英文技术文案换成中文兜底', () => { + expect( + resolveModel3dGenerationErrorMessage(new TypeError('Failed to fetch')), + ).toBe(MODEL3D_GENERATION_FAILURE_MESSAGE); + expect( + resolveModel3dGenerationErrorMessage( + new DOMException('The operation was aborted.', 'AbortError'), + ), + ).toBe(MODEL3D_GENERATION_FAILURE_MESSAGE); + expect(resolveModel3dGenerationErrorMessage(null)).toBe( + MODEL3D_GENERATION_FAILURE_MESSAGE, + ); + }); + + it('超时单独给出可重试的提示', () => { + const timeout = new Error('请求超时:60000ms'); + timeout.name = 'TimeoutError'; + + expect(resolveModel3dGenerationErrorMessage(timeout)).toBe( + MODEL3D_GENERATION_TIMEOUT_MESSAGE, + ); + }); +}); diff --git a/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.ts b/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.ts index 85d218eb1..01f579827 100644 --- a/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.ts +++ b/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.ts @@ -1,5 +1,6 @@ import type { ExternalGenerationJobStatusRecord } from '../../../../packages/shared/src/contracts/externalGeneration'; import type { Model3dGenerationSource } from '../../../../packages/shared/src/contracts/model3d'; +import { isAbortError, isTimeoutError } from '../../../services/apiClient'; import type { EditorCanvasGenerationCompletionInput, Model3dGenerationSubmissionResponse, @@ -22,6 +23,10 @@ export const MODEL3D_IMAGE_SOURCE_REQUIRED_MESSAGE = '请先选择一张画布图片或素材库图片'; export const MODEL3D_PROJECT_REQUIRED_MESSAGE = '请先保存当前画布工程,再生成 3D 模型'; +export const MODEL3D_GENERATION_FAILURE_MESSAGE = + '生成 3D 模型失败,请稍后重试。'; +export const MODEL3D_GENERATION_TIMEOUT_MESSAGE = + '3D 生成请求超时,请稍后重试。'; export type Model3dSubmissionPlan = { endpoint: Model3dPricingEndpoint; @@ -213,11 +218,26 @@ export function model3dQueueStateFromResponse( } export function resolveModel3dGenerationErrorMessage(error: unknown): string { + if (isTimeoutError(error)) { + return MODEL3D_GENERATION_TIMEOUT_MESSAGE; + } + // 传输层失败的 message 是英文技术文案(`Failed to fetch` / `The operation was + // aborted.`),不能直接当用户提示;只有服务端给出的 message 才是面向用户的。 + if (isAbortError(error) || isFetchFailureError(error)) { + return MODEL3D_GENERATION_FAILURE_MESSAGE; + } if (error instanceof Error && error.message.trim()) { return error.message.trim(); } if (typeof error === 'string' && error.trim()) { return error.trim(); } - return '生成 3D 模型失败,请稍后重试。'; + return MODEL3D_GENERATION_FAILURE_MESSAGE; +} + +/** fetch 的网络层失败统一是 `TypeError: Failed to fetch`。 */ +function isFetchFailureError(error: unknown) { + return ( + error instanceof TypeError && /failed to fetch|network/i.test(error.message) + ); }