From 5ebc6df6ae9e5028cd435582b8339d346a1452a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 23 Sep 2026 18:51:15 +0800 Subject: [PATCH] =?UTF-8?q?3D=20=E5=A4=B1=E8=B4=A5=E6=80=81=E4=B8=8B?= =?UTF-8?q?=E4=B8=BB=E6=8C=89=E9=92=AE=E4=B9=9F=E5=BC=80=E6=96=B0=E4=B8=80?= =?UTF-8?q?=E4=BB=A3=EF=BC=8C=E4=B8=8D=E5=86=8D=E9=87=8D=E7=8E=B0=E5=90=8C?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=E5=A4=B1=E8=B4=A5=20-=20Model3dGenerationMod?= =?UTF-8?q?al=20=E4=B8=BB=E6=8C=89=E9=92=AE=E4=B8=8E=E3=80=8C=E9=87=8D?= =?UTF-8?q?=E8=AF=95=E3=80=8D=E5=85=B1=E7=94=A8=E5=A4=B1=E8=B4=A5=E6=80=81?= =?UTF-8?q?=E9=87=8D=E7=BD=AE=E5=90=8E=E7=9A=84=20dialog=EF=BC=88refreshMo?= =?UTF-8?q?del3dAttemptNonce=EF=BC=89=EF=BC=8C=E5=A4=B1=E8=B4=A5=E6=80=81?= =?UTF-8?q?=E7=82=B9=E4=B8=BB=E6=8C=89=E9=92=AE=E4=B8=8D=E5=86=8D=E6=B2=BF?= =?UTF-8?q?=E7=94=A8=E6=97=A7=E5=B9=82=E7=AD=89=E9=94=AE=20-=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E7=94=A8=E4=BE=8B=EF=BC=9A=E5=A4=B1=E8=B4=A5=E6=80=81?= =?UTF-8?q?=E4=B8=BB=E6=8C=89=E9=92=AE=E6=8F=90=E4=BA=A4=E7=9A=84=E4=BB=A3?= =?UTF-8?q?=E6=AC=A1=E5=BF=85=E9=A1=BB=E5=8F=98=E5=8C=96=E3=80=81=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E5=9B=9E=E5=88=B0=20idle=EF=BC=8C=E4=B8=94=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E4=BF=9D=E6=8C=81=E5=8F=AF=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Model3dGenerationModal.test.tsx | 24 +++++++++++++++++++ .../Model3dGenerationModal.tsx | 9 +++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/components/image-editor/model3d-generation/Model3dGenerationModal.test.tsx b/src/components/image-editor/model3d-generation/Model3dGenerationModal.test.tsx index b4b57f8e2..56d97f232 100644 --- a/src/components/image-editor/model3d-generation/Model3dGenerationModal.test.tsx +++ b/src/components/image-editor/model3d-generation/Model3dGenerationModal.test.tsx @@ -298,4 +298,28 @@ describe('Model3dGenerationModal', () => { ); expect(onSubmit.mock.calls[0][0].model3dAttemptNonce).toBeTruthy(); }); + + it('失败态下主按钮也开新一代,不会重现同一次失败', () => { + const onSubmit = vi.fn(); + const failedDialog = createDialog('model3d-text-to-model', { + prompt: '一把木椅', + status: 'failed', + errorMessage: '3D 模型生成失败,请稍后重试。', + }); + render( + , + ); + + // 主按钮在失败态仍是「再生成一次」的自然入口,可以点;但不能沿用旧代次, + // 否则幂等键不变、服务端只会把原来那个终态 operation 还回来。 + expect((submitButton() as HTMLButtonElement).disabled).toBe(false); + fireEvent.click(submitButton()); + + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(onSubmit.mock.calls[0][0].model3dAttemptNonce).not.toBe( + failedDialog.model3dAttemptNonce, + ); + expect(onSubmit.mock.calls[0][0].model3dAttemptNonce).toBeTruthy(); + expect(onSubmit.mock.calls[0][0].status).toBe('idle'); + }); }); diff --git a/src/components/image-editor/model3d-generation/Model3dGenerationModal.tsx b/src/components/image-editor/model3d-generation/Model3dGenerationModal.tsx index 67f9a9c74..57d8ffa5c 100644 --- a/src/components/image-editor/model3d-generation/Model3dGenerationModal.tsx +++ b/src/components/image-editor/model3d-generation/Model3dGenerationModal.tsx @@ -71,6 +71,11 @@ export function Model3dGenerationModal({ hasRequiredInput && !isGenerating && !hasPendingImageReferenceUploads; + // 幂等键 = 代次 + 请求内容指纹,同键同请求只会拿回原来那个终态 operation。 + // 所以失败态下的任何提交(主按钮或「重试」)都要开新一代,否则点下去只是重现同一次失败; + // 参数一改本来就会换代号次,这里补的是「不改参数直接再点一次」这条路。 + const submitDialog = + dialog.status === 'failed' ? refreshModel3dAttemptNonce(dialog) : dialog; useImageCanvasFloatingOptionDismiss({ isOpen: isImageMode && isGenerationReferenceMenuOpen, @@ -114,7 +119,7 @@ export function Model3dGenerationModal({ className="image-canvas-editor__generation-submit" disabled={!canSubmit} aria-label={MODEL3D_GENERATION_SUBMIT_LABEL} - onClick={() => onSubmit(dialog)} + onClick={() => onSubmit(submitDialog)} > {isGenerating ? ( '生成中' @@ -135,7 +140,7 @@ export function Model3dGenerationModal({ size="xs" shape="pill" aria-label="重试生成 3D 模型" - onClick={() => onSubmit(refreshModel3dAttemptNonce(dialog))} + onClick={() => onSubmit(submitDialog)} > 重试