From caafb8baa38615ad0f6d42c40c8903a3d7bfc3aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 22 Sep 2026 17:04:11 +0800 Subject: [PATCH] =?UTF-8?q?3D=20=E6=8F=90=E4=BA=A4=E4=BD=93=E6=94=B9?= =?UTF-8?q?=E7=94=A8=E7=AB=AF=E7=82=B9=E5=A5=91=E7=BA=A6=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E8=80=8C=E9=9D=9E=20Record=20-=20Model3dSubmissionPlan.body=20?= =?UTF-8?q?=E4=B8=8E=20submitModel3dGenerationRequest=20=E7=9A=84=20body?= =?UTF-8?q?=20=E6=94=B6=E7=B4=A7=E4=B8=BA=E4=B8=A4=E4=B8=AA=E7=AB=AF?= =?UTF-8?q?=E7=82=B9=E8=AF=B7=E6=B1=82=E5=A5=91=E7=BA=A6=20-=20=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E8=AE=A1=E5=88=92=E6=8C=89=E7=AB=AF=E7=82=B9=E5=88=86?= =?UTF-8?q?=E6=94=AF=E5=88=86=E5=88=AB=E6=9E=84=E9=80=A0=20text-to-model?= =?UTF-8?q?=20/=20image-to-model=20=E8=AF=B7=E6=B1=82=E4=BD=93=20-=20?= =?UTF-8?q?=E5=B9=82=E7=AD=89=E9=94=AE=E5=85=A5=E5=8F=82=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E6=94=B6=E7=B4=A7=EF=BC=8C=E6=B5=8B=E8=AF=95=E5=A4=B9=E5=85=B7?= =?UTF-8?q?=E6=8C=89=E5=A5=91=E7=BA=A6=E8=A1=A5=E5=85=A8=20body?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Model3dGenerationSubmission.test.ts | 15 +++- .../Model3dGenerationSubmission.ts | 81 ++++++++++--------- .../image-editor/editorProjectClient.ts | 7 +- 3 files changed, 61 insertions(+), 42 deletions(-) diff --git a/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.test.ts b/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.test.ts index 7f63af339..08c12bb1c 100644 --- a/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.test.ts +++ b/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.test.ts @@ -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); }); diff --git a/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.ts b/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.ts index e2ee10ed7..f5d894301 100644 --- a/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.ts +++ b/src/components/image-editor/model3d-generation/Model3dGenerationSubmission.ts @@ -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; + /** 提交体就是端点契约本身:形状错误在编译期暴露,而不是等服务端拒绝。 */ + 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 = { - ...(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; + body: Model3dTextToModelRequest | Model3dImageToModelRequest; }): string { const nonce = resolveModel3dAttemptNonce(attemptNonce); return `${nonce}-${fingerprintModel3dRequest(endpoint, body)}`; diff --git a/src/services/image-editor/editorProjectClient.ts b/src/services/image-editor/editorProjectClient.ts index 9f463d45c..2e04bd5f6 100644 --- a/src/services/image-editor/editorProjectClient.ts +++ b/src/services/image-editor/editorProjectClient.ts @@ -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; + // 提交体直接用端点契约:形状不符(缺 target、generation 结构错)在编译期就会暴露。 + body: Model3dTextToModelRequest | Model3dImageToModelRequest; idempotencyKey: string; }) { return requestJson(