diff --git a/src/components/image-editor/model3d-generation/useModel3dGenerationTask.test.tsx b/src/components/image-editor/model3d-generation/useModel3dGenerationTask.test.tsx index ddd1b3065..c59097a23 100644 --- a/src/components/image-editor/model3d-generation/useModel3dGenerationTask.test.tsx +++ b/src/components/image-editor/model3d-generation/useModel3dGenerationTask.test.tsx @@ -99,11 +99,13 @@ function renderTask({ projectId = 'project-1', assetFolderId, applyQueuedGeneration = vi.fn(async () => true), + onWalletBalanceMayHaveChanged = vi.fn(), }: { dialog: GenerateDialogState; projectId?: string | null; assetFolderId?: string | null; applyQueuedGeneration?: (...args: unknown[]) => Promise; + onWalletBalanceMayHaveChanged?: () => void; }) { const onQueuedGenerationTask = vi.fn(); const view = renderHook(() => { @@ -126,6 +128,7 @@ function renderTask({ currentDialog.placeholder ?? null, applyQueuedGeneration: applyQueuedGeneration as never, onQueuedGenerationTask, + onWalletBalanceMayHaveChanged, }); return { submitModel3dGeneration, @@ -133,7 +136,7 @@ function renderTask({ dialog: dialogs[0] as GenerateDialogState, }; }); - return { ...view, onQueuedGenerationTask }; + return { ...view, onQueuedGenerationTask, onWalletBalanceMayHaveChanged }; } describe('useModel3dGenerationTask', () => { @@ -293,6 +296,32 @@ describe('useModel3dGenerationTask', () => { expect(submitModel3dTextToModelRequestMock).not.toHaveBeenCalled(); }); + it('提交失败与「2xx 无任务状态」都按可能已扣费刷新钱包', async () => { + submitModel3dTextToModelRequestMock.mockRejectedValue( + new ApiClientError({ + message: '3D 模型生成失败,请稍后重试。', + status: 502, + code: 'tripo-upstream-error', + }), + ); + const failed = renderTask({ dialog: createDialog() }); + + await act(async () => { + await failed.result.current.submitModel3dGeneration(createDialog()); + }); + + expect(failed.onWalletBalanceMayHaveChanged).toHaveBeenCalledTimes(1); + + submitModel3dTextToModelRequestMock.mockResolvedValue(null); + const emptyBody = renderTask({ dialog: createDialog() }); + + await act(async () => { + await emptyBody.result.current.submitModel3dGeneration(createDialog()); + }); + + expect(emptyBody.onWalletBalanceMayHaveChanged).toHaveBeenCalledTimes(1); + }); + 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 1a8c19925..00349e2e9 100644 --- a/src/components/image-editor/model3d-generation/useModel3dGenerationTask.ts +++ b/src/components/image-editor/model3d-generation/useModel3dGenerationTask.ts @@ -127,6 +127,8 @@ export function useModel3dGenerationTask({ if (!queueState) { // 2xx 但没有任务状态(空 body 或缺 status 的 payload)不能当成功静默复位: // 用户既看不到任务也没有任何提示,服务端细节被丢掉。 + // 服务端在这条路上也可能已经扣过费,钱包余额同样要按「可能变了」刷新。 + onWalletBalanceMayHaveChanged?.(); updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({ ...currentDialog, status: 'failed', @@ -155,6 +157,9 @@ export function useModel3dGenerationTask({ errorMessage: undefined, })); } catch (error) { + // 超时等失败也可能意味着服务端已经扣费(与其它生成链路的 + // runEditorGenerationWithWalletRefresh 同一口径),失败路径也要刷新钱包。 + onWalletBalanceMayHaveChanged?.(); updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({ ...currentDialog, status: 'failed',