3D 提交体改用端点契约类型而非 Record
- Model3dSubmissionPlan.body 与 submitModel3dGenerationRequest 的 body 收紧为两个端点请求契约 - 提交计划按端点分支分别构造 text-to-model / image-to-model 请求体 - 幂等键入参同步收紧,测试夹具按契约补全 body
This commit is contained in:
@@ -320,7 +320,10 @@ describe('Idempotency-Key', () => {
|
||||
resolveModel3dRequestKey({
|
||||
attemptNonce: null,
|
||||
endpoint: 'text-to-model',
|
||||
body: { generation: { model: 'v3.1-20260211' } },
|
||||
body: {
|
||||
generation: { model: 'v3.1-20260211', prompt: '木椅' },
|
||||
target: { kind: 'projectResource', projectId: 'project-1' },
|
||||
},
|
||||
}),
|
||||
).toMatch(/^[\x21-\x7e]{1,128}$/);
|
||||
});
|
||||
@@ -329,12 +332,18 @@ describe('Idempotency-Key', () => {
|
||||
const left = resolveModel3dRequestKey({
|
||||
attemptNonce: 'model3d-fixed',
|
||||
endpoint: 'text-to-model',
|
||||
body: { generation: { texture: true, model: 'v3.1-20260211' } },
|
||||
body: {
|
||||
generation: { texture: true, model: 'v3.1-20260211', prompt: '木椅' },
|
||||
target: { kind: 'projectResource', projectId: 'project-1' },
|
||||
},
|
||||
});
|
||||
const right = resolveModel3dRequestKey({
|
||||
attemptNonce: 'model3d-fixed',
|
||||
endpoint: 'text-to-model',
|
||||
body: { generation: { model: 'v3.1-20260211', texture: true } },
|
||||
body: {
|
||||
generation: { model: 'v3.1-20260211', texture: true, prompt: '木椅' },
|
||||
target: { kind: 'projectResource', projectId: 'project-1' },
|
||||
},
|
||||
});
|
||||
expect(right).toBe(left);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import type { ExternalGenerationJobStatusRecord } from '../../../../packages/shared/src/contracts/externalGeneration';
|
||||
import type { Model3dGenerationSource } from '../../../../packages/shared/src/contracts/model3d';
|
||||
import type {
|
||||
Model3dGenerationSource,
|
||||
Model3dGenerationTarget,
|
||||
Model3dImageToModelRequest,
|
||||
Model3dTextToModelRequest,
|
||||
} from '../../../../packages/shared/src/contracts/model3d';
|
||||
import { isAbortError, isTimeoutError } from '../../../services/apiClient';
|
||||
import type {
|
||||
EditorCanvasGenerationCompletionInput,
|
||||
@@ -41,7 +46,8 @@ export type Model3dSubmissionPlan = {
|
||||
attemptNonce: string;
|
||||
requestKey: string;
|
||||
title: string;
|
||||
body: Record<string, unknown>;
|
||||
/** 提交体就是端点契约本身:形状错误在编译期暴露,而不是等服务端拒绝。 */
|
||||
body: Model3dTextToModelRequest | Model3dImageToModelRequest;
|
||||
};
|
||||
|
||||
export function resolveModel3dResultTitle(dialog: GenerateDialogState): string {
|
||||
@@ -106,43 +112,28 @@ export function buildModel3dSubmissionPlan({
|
||||
return { ok: false, message: paramsResult.reason };
|
||||
}
|
||||
const params = paramsResult.params;
|
||||
const prompt = dialog.prompt.trim();
|
||||
const source =
|
||||
dialog.mode === 'model3d-image-to-model'
|
||||
? resolveModel3dImageSource(dialog)
|
||||
: null;
|
||||
if (dialog.mode === 'model3d-text-to-model' && !prompt) {
|
||||
return { ok: false, message: MODEL3D_TEXT_PROMPT_REQUIRED_MESSAGE };
|
||||
}
|
||||
if (dialog.mode === 'model3d-image-to-model' && !source) {
|
||||
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 的隐式默认值会直接改变价格,
|
||||
// 唯一例外是 texture=false 档位不带 textureQuality(后端显式拒绝这个组合)。
|
||||
generation: {
|
||||
model: DEFAULT_MODEL3D_MODEL_VERSION,
|
||||
...(dialog.mode === 'model3d-text-to-model' ? { prompt } : {}),
|
||||
texture: params.texture,
|
||||
pbr: params.pbr,
|
||||
...(params.textureQuality
|
||||
? { textureQuality: params.textureQuality }
|
||||
: {}),
|
||||
geometryQuality: params.geometryQuality,
|
||||
quad: params.quad,
|
||||
smartLowPoly: params.smartLowPoly,
|
||||
generateParts: params.generateParts,
|
||||
},
|
||||
target: {
|
||||
kind: 'projectResource',
|
||||
projectId: trimmedProjectId,
|
||||
...(canvasCompletion ? { canvasCompletion } : {}),
|
||||
},
|
||||
// 价格相关参数永远全量给出:provider 的隐式默认值会直接改变价格,
|
||||
// 唯一例外是 texture=false 档位不带 textureQuality(后端显式拒绝这个组合)。
|
||||
const generation = {
|
||||
model: DEFAULT_MODEL3D_MODEL_VERSION,
|
||||
texture: params.texture,
|
||||
pbr: params.pbr,
|
||||
...(params.textureQuality ? { textureQuality: params.textureQuality } : {}),
|
||||
geometryQuality: params.geometryQuality,
|
||||
quad: params.quad,
|
||||
smartLowPoly: params.smartLowPoly,
|
||||
generateParts: params.generateParts,
|
||||
};
|
||||
return {
|
||||
const target: Model3dGenerationTarget = {
|
||||
kind: 'projectResource',
|
||||
projectId: trimmedProjectId,
|
||||
...(canvasCompletion ? { canvasCompletion } : {}),
|
||||
};
|
||||
const finish = (
|
||||
body: Model3dTextToModelRequest | Model3dImageToModelRequest,
|
||||
): { ok: true; plan: Model3dSubmissionPlan } => ({
|
||||
ok: true,
|
||||
plan: {
|
||||
endpoint,
|
||||
@@ -155,7 +146,21 @@ export function buildModel3dSubmissionPlan({
|
||||
title: resolveModel3dResultTitle(dialog),
|
||||
body,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
if (dialog.mode === 'model3d-image-to-model') {
|
||||
const source = resolveModel3dImageSource(dialog);
|
||||
if (!source) {
|
||||
return { ok: false, message: MODEL3D_IMAGE_SOURCE_REQUIRED_MESSAGE };
|
||||
}
|
||||
return finish({ source, generation, target });
|
||||
}
|
||||
|
||||
const prompt = dialog.prompt.trim();
|
||||
if (!prompt) {
|
||||
return { ok: false, message: MODEL3D_TEXT_PROMPT_REQUIRED_MESSAGE };
|
||||
}
|
||||
return finish({ generation: { ...generation, prompt }, target });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,7 +179,7 @@ export function resolveModel3dRequestKey({
|
||||
}: {
|
||||
attemptNonce?: string | null;
|
||||
endpoint: Model3dPricingEndpoint;
|
||||
body: Record<string, unknown>;
|
||||
body: Model3dTextToModelRequest | Model3dImageToModelRequest;
|
||||
}): string {
|
||||
const nonce = resolveModel3dAttemptNonce(attemptNonce);
|
||||
return `${nonce}-${fingerprintModel3dRequest(endpoint, body)}`;
|
||||
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
} from '../../../packages/shared/src/contracts/editorAudio';
|
||||
import type { EditorSceneGenerationRequest } from '../../../packages/shared/src/contracts/editorScene';
|
||||
import type { ExternalGenerationJobStatusRecord } from '../../../packages/shared/src/contracts/externalGeneration';
|
||||
import type {
|
||||
Model3dImageToModelRequest,
|
||||
Model3dTextToModelRequest,
|
||||
} from '../../../packages/shared/src/contracts/model3d';
|
||||
import { requestJson } from '../apiClient';
|
||||
import { EDITOR_GENERATION_REQUEST_RETRY_OPTIONS } from './editorRetryOptions';
|
||||
|
||||
@@ -1742,7 +1746,8 @@ export async function submitModel3dGenerationRequest({
|
||||
idempotencyKey,
|
||||
}: {
|
||||
endpoint: Model3dPricingEndpoint;
|
||||
body: Record<string, unknown>;
|
||||
// 提交体直接用端点契约:形状不符(缺 target、generation 结构错)在编译期就会暴露。
|
||||
body: Model3dTextToModelRequest | Model3dImageToModelRequest;
|
||||
idempotencyKey: string;
|
||||
}) {
|
||||
return requestJson<Model3dGenerationSubmissionResponse>(
|
||||
|
||||
Reference in New Issue
Block a user