3D 提交把兜底铸的尝试代次写回对话框状态
- 新增 resolveModel3dAttemptNonce,提交计划带出本次实际使用的代次 - 提交编排在进入 generating 时把该代次写回 dialog,缺代次的老快照不再每次换键 - 补老快照场景用例,断言幂等键前缀与写回的代次一致
This commit is contained in:
@@ -30,6 +30,13 @@ export const MODEL3D_GENERATION_TIMEOUT_MESSAGE =
|
||||
|
||||
export type Model3dSubmissionPlan = {
|
||||
endpoint: Model3dPricingEndpoint;
|
||||
/**
|
||||
* 本次提交使用的尝试代次;调用方必须把它写回 dialog 状态。
|
||||
*
|
||||
* 老快照恢复出来的 dialog 可能没有代次,这里兜底铸一个;如果调用方不写回,
|
||||
* 每次提交都会换键,双击去重与重复计费保护就静默失效。
|
||||
*/
|
||||
attemptNonce: string;
|
||||
requestKey: string;
|
||||
title: string;
|
||||
body: Record<string, unknown>;
|
||||
@@ -109,6 +116,7 @@ export function buildModel3dSubmissionPlan({
|
||||
return { ok: false, message: MODEL3D_IMAGE_SOURCE_REQUIRED_MESSAGE };
|
||||
}
|
||||
const endpoint = resolveModel3dEndpoint(dialog.mode);
|
||||
const attemptNonce = resolveModel3dAttemptNonce(dialog.model3dAttemptNonce);
|
||||
const body: Record<string, unknown> = {
|
||||
...(source ? { source } : {}),
|
||||
// 价格相关参数永远全量给出:provider 的隐式默认值会直接改变价格,
|
||||
@@ -136,8 +144,9 @@ export function buildModel3dSubmissionPlan({
|
||||
ok: true,
|
||||
plan: {
|
||||
endpoint,
|
||||
attemptNonce,
|
||||
requestKey: resolveModel3dRequestKey({
|
||||
attemptNonce: dialog.model3dAttemptNonce,
|
||||
attemptNonce,
|
||||
endpoint,
|
||||
body,
|
||||
}),
|
||||
@@ -165,10 +174,17 @@ export function resolveModel3dRequestKey({
|
||||
endpoint: Model3dPricingEndpoint;
|
||||
body: Record<string, unknown>;
|
||||
}): string {
|
||||
const nonce = attemptNonce?.trim() || mintModel3dAttemptNonce();
|
||||
const nonce = resolveModel3dAttemptNonce(attemptNonce);
|
||||
return `${nonce}-${fingerprintModel3dRequest(endpoint, body)}`;
|
||||
}
|
||||
|
||||
/** 取本次提交使用的代次:已有代次直接用,缺失(例如老快照)才现场铸造。 */
|
||||
export function resolveModel3dAttemptNonce(
|
||||
attemptNonce?: string | null,
|
||||
): string {
|
||||
return attemptNonce?.trim() || mintModel3dAttemptNonce();
|
||||
}
|
||||
|
||||
/** 键排序只求稳定,不承载任何业务语义:同一份内容必须算出同一个键。 */
|
||||
function compareModel3dRequestEntryKeys(
|
||||
[left]: [string, unknown],
|
||||
|
||||
@@ -178,6 +178,26 @@ describe('useModel3dGenerationTask', () => {
|
||||
expect(result.current.dialog.composerOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('缺代次的老快照兜底铸一个并写回,幂等键跟着这个代次走', async () => {
|
||||
submitModel3dGenerationRequestMock.mockResolvedValue({
|
||||
operationId: 'task-1',
|
||||
jobKind: 'model3d_text_to_model',
|
||||
status: QUEUED_STATUS,
|
||||
statusUrl: '/api/runtime/external-generation/jobs/task-1',
|
||||
});
|
||||
const legacyDialog = createDialog({ model3dAttemptNonce: undefined });
|
||||
const { result } = renderTask({ dialog: legacyDialog });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitModel3dGeneration(legacyDialog);
|
||||
});
|
||||
|
||||
const request = submitModel3dGenerationRequestMock.mock.calls[0][0];
|
||||
const persistedNonce = result.current.dialog.model3dAttemptNonce;
|
||||
expect(persistedNonce).toBeTruthy();
|
||||
expect(request.idempotencyKey.startsWith(`${persistedNonce}-`)).toBe(true);
|
||||
});
|
||||
|
||||
it('排队链路没接手时回到可再次提交的空闲态', async () => {
|
||||
submitModel3dGenerationRequestMock.mockResolvedValue({
|
||||
operationId: 'task-1',
|
||||
|
||||
@@ -93,6 +93,8 @@ export function useModel3dGenerationTask({
|
||||
}
|
||||
updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({
|
||||
...currentDialog,
|
||||
// 兜底铸的代次要写回状态:否则老快照每次提交都换幂等键,去重保护失效。
|
||||
model3dAttemptNonce: planResult.plan.attemptNonce,
|
||||
status: 'generating',
|
||||
composerOpen: false,
|
||||
errorMessage: undefined,
|
||||
|
||||
Reference in New Issue
Block a user