diff --git a/src/components/image-editor/model3d-generation/useModel3dGenerationTask.test.tsx b/src/components/image-editor/model3d-generation/useModel3dGenerationTask.test.tsx index b2398bb0e..ddd1b3065 100644 --- a/src/components/image-editor/model3d-generation/useModel3dGenerationTask.test.tsx +++ b/src/components/image-editor/model3d-generation/useModel3dGenerationTask.test.tsx @@ -276,6 +276,23 @@ describe('useModel3dGenerationTask', () => { expect(result.current.dialog.composerOpen).toBe(true); }); + it('构造提交计划时抛的错也落到失败态,而不是漏成未捕获的 rejection', async () => { + // 老快照缺字段:resolveModel3dResultTitle 会读 dialog.prompt.trim(),在计划构造前就抛。 + const { result } = renderTask({ + dialog: createDialog({ prompt: undefined }), + }); + + await act(async () => { + await result.current.submitModel3dGeneration( + createDialog({ prompt: undefined }), + ); + }); + + expect(result.current.dialog.status).toBe('failed'); + expect(result.current.dialog.composerOpen).toBe(true); + expect(submitModel3dTextToModelRequestMock).not.toHaveBeenCalled(); + }); + it('定价段缺失时不发请求,只把不可提交的原因写回面板', async () => { applyEditorGenerationPricingConfig({ models: EDITOR_MODEL_MUD_POINT_CONFIG, diff --git a/src/components/image-editor/model3d-generation/useModel3dGenerationTask.ts b/src/components/image-editor/model3d-generation/useModel3dGenerationTask.ts index 506214383..1a8c19925 100644 --- a/src/components/image-editor/model3d-generation/useModel3dGenerationTask.ts +++ b/src/components/image-editor/model3d-generation/useModel3dGenerationTask.ts @@ -74,41 +74,43 @@ export function useModel3dGenerationTask({ if (!dialogId || !isModel3dGenerationMode(dialog.mode)) { return; } - const placeholder = getGeneratingDialogPlaceholder(dialog); - const canvasCompletion = - projectId && placeholder - ? { - dialogId, - title: resolveModel3dResultTitle(dialog), - placeholder, - } - : null; - const planResult = buildModel3dSubmissionPlan({ - dialog, - projectId, - canvasCompletion, - assetFolderId, - assetLabel: resolveModel3dResultTitle(dialog), - pricing: readEditorModel3dPricingConfig(), - }); - if (!planResult.ok) { + // 整个提交编排都落在 try 里:取占位框、算标题、构造提交计划同样可能抛 + // (注入的回调、老快照缺字段),漏出去就成了一次没有用户提示的未捕获 rejection。 + try { + const placeholder = getGeneratingDialogPlaceholder(dialog); + const canvasCompletion = + projectId && placeholder + ? { + dialogId, + title: resolveModel3dResultTitle(dialog), + placeholder, + } + : null; + const planResult = buildModel3dSubmissionPlan({ + dialog, + projectId, + canvasCompletion, + assetFolderId, + assetLabel: resolveModel3dResultTitle(dialog), + pricing: readEditorModel3dPricingConfig(), + }); + if (!planResult.ok) { + updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({ + ...currentDialog, + status: 'failed', + composerOpen: true, + errorMessage: planResult.message, + })); + return; + } updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({ ...currentDialog, - status: 'failed', - composerOpen: true, - errorMessage: planResult.message, + // 兜底铸的代次要写回状态:否则老快照每次提交都换幂等键,去重保护失效。 + model3dAttemptNonce: planResult.plan.attemptNonce, + status: 'generating', + composerOpen: false, + errorMessage: undefined, })); - return; - } - updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({ - ...currentDialog, - // 兜底铸的代次要写回状态:否则老快照每次提交都换幂等键,去重保护失效。 - model3dAttemptNonce: planResult.plan.attemptNonce, - status: 'generating', - composerOpen: false, - errorMessage: undefined, - })); - try { const plan = planResult.plan; // 判别联合在这里收窄:文生与图生各走自己的客户端函数,端点与提交体的配对由类型保证。 const response =