3D 落点契约文档同步为平坦可选字段
- 技术方案的请求契约段改成平坦可选 projectId / canvasCompletion / assetFolderId / assetLabel,并把「二选一 + 同现拒绝」的口径与落点预检路径写清。 - 里程碑与实施计划把 tagged enum 的表述改成平坦字段与二选一验收口径。 - 前端入口与 Worker 计划的落点表述去掉 target 分支写法,改为只发 projectId / 素材库落点产出 assetId。 - 决策记录补一条:撤销请求侧 tagged enum、复用统一定点预检、素材名缺省回落默认名,并记下未做的目录归一与原因。
This commit is contained in:
@@ -8,12 +8,12 @@ Milestone Spec: `docs/project-memory/plans/【里程碑】Tripo生成API契约
|
||||
## 步骤
|
||||
|
||||
1. **API 契约**
|
||||
- `shared-contracts::model3d` 分两层:provider 生成参数(`Model3dTextToModelParams` / `Model3dImageToModelParams`)与 API 层请求(`Model3dTextToModelRequest` / `Model3dImageToModelRequest` = `generation` + 平台字段 + `target`)。
|
||||
- `shared-contracts::model3d` 分两层:provider 生成参数(`Model3dTextToModelParams` / `Model3dImageToModelParams`)与 API 层请求(`Model3dTextToModelRequest` / `Model3dImageToModelRequest` = `generation` + 平台字段)。
|
||||
- 两层都保持 `deny_unknown_fields`,不使用 `flatten`:serde 的 `deny_unknown_fields` 与 `flatten` 不兼容,展开会让顶层未知字段静默通过。
|
||||
- `common/` 新增平台字段类型:`Model3dGenerationSource`(resource / asset 二选一)、`Model3dGenerationTarget`(projectResource / assetLibrary 二选一,画布占位框复用既有载荷)、结果落点引用与产物元数据类型。
|
||||
- `common/` 新增平台字段类型:`Model3dGenerationSource`(resource / asset 二选一)、结果落点引用(`Model3dGenerationTargetRef`,projectResource / assetLibrary 二选一)与产物元数据类型;请求侧落点与其它生成接口同形,用平坦的可选 `projectId` / `assetFolderId` / `assetLabel` / `canvasCompletion`,不引入 tagged enum。
|
||||
- 图片输入不属于 provider 参数:客户端只能给 `source`,`input` 由服务端解析后注入 provider 调用,避免绕过归属校验传任意 URL。
|
||||
- 新增按端点的严格结果 payload 类型(模型 artifact + 预览 artifact),二者都只携带正式资源引用与对象元数据。
|
||||
- 交付:类型定义、serde 组合测试(缺 `folderId` / `label` / `projectId` 拒绝,`source` 两个 ID 同现拒绝)。
|
||||
- 交付:类型定义、serde 组合测试(`projectId` 与 `assetFolderId` 都不给或都给拒绝、`canvasCompletion` 与 `assetFolderId` 同现拒绝、`source` 两个 ID 同现拒绝)。
|
||||
- 验收:`cargo test --locked -p shared-contracts --manifest-path server-rs/Cargo.toml` 通过。
|
||||
|
||||
2. **ts-rs 目录绑定**
|
||||
|
||||
@@ -53,7 +53,7 @@ Milestone Spec: `docs/project-memory/plans/【里程碑】Tripo生成Worker执
|
||||
- 复用现有原子落库路径写入 `assetKind = "model3d"` 的资源 / 素材、预览图引用与 job 终态;不新增资源列。
|
||||
- 按「新增编辑器 `assetKind` 接入清单」对照表执行:把两个 Tripo job kind 加进 `EDITOR_GENERATION_OPERATION_KINDS`(否则原子落库整笔事务被拒);画布回填只走 placement-only,`model3d` 不进 `EDITOR_CANVAS_ASSET_KINDS`,也不动前端 `CANVAS_ASSET_KIND_TAG_OPTIONS`。
|
||||
- 验收:两个 job kind 在白名单内;`canvasCompletion` 回填的 layer 只有 `layerId` / `resourceId`,无 `assetKind` / `src` / `objectKey`;用户标签覆盖、快速编辑、改造 capability 三项保持不进入。
|
||||
- 验收:`assetLibrary` 分支产出 `assetId`,`projectResource` 分支产出 `resourceId` 且 `content_length` 与实际对象一致。
|
||||
- 验收:素材库落点产出 `assetId`,项目资源落点产出 `resourceId` 且 `content_length` 与实际对象一致。
|
||||
|
||||
6. **worker 执行与收口**
|
||||
- 执行分支:无 checkpoint 才 submit,submit 成功后先落 checkpoint 再轮询;已有 checkpoint 只查询、下载与落库;checkpoint 写入失败按失败收口。
|
||||
|
||||
@@ -16,7 +16,7 @@ Parent Spec: `docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-2
|
||||
2. 入口形态照 `music` / `spec`:工具栏一项入口 → 浮动子选项 → 各自一个提交面板。子项文案沿用后端 `phaseLabel`:「文生 3D 模型」「图生 3D 模型」。
|
||||
3. 价格**只**来自 `GET /api/editor/generation-pricing` 的 `model3d` 段(前端已有 `loadEditorGenerationPricing` 与运行时缓存)。段缺失即两个子面板都不可提交且不发请求;前端不写任何 3D 兜底数值,也不把缺价当 0。
|
||||
4. 参数永远全量非空提交:`model`、`texture`、`pbr`、`geometryQuality`、`quad`、`smartLowPoly`、`generateParts` 一律显式给出,不依赖后端默认值(provider 隐式默认会改变价格)。唯一例外是 `textureQuality`:后端 `api-server::tripo3d::validation` 显式拒绝 `texture=false` 时出现贴图档位,因此纯几何档位必须**不带**该字段,而不是给 `null`。
|
||||
5. 落点固定 `target = { kind: "projectResource", projectId, canvasCompletion: { dialogId, title, placeholder } }`,照现有生成工具的做法(发送门禁为 `projectId && canvasCompletionPlaceholder`)。
|
||||
5. 落点固定为项目资源(发 `projectId` + `canvasCompletion: { dialogId, title, placeholder }`,不发 `assetFolderId`),照现有生成工具的做法(发送门禁为 `projectId && canvasCompletionPlaceholder`)。
|
||||
6. 图生输入照现有参考图来源菜单:从画布中选择 / 上传图片 / 从项目素材中选择,最终收敛成契约允许的 `{ kind: "resource", resourceId }` 或 `{ kind: "asset", assetId }`;只允许一张,不接 multiview。
|
||||
7. `Idempotency-Key` 由前端铸造并绑定当次请求,用户主动重试铸造新键、不复用旧键。落地形态是「尝试代次 + 请求内容指纹」(`Model3dGenerationSubmission.resolveModel3dRequestKey`):同一份内容重复提交(双击)仍命中同一个 operation,内容一变(含换参考图、换上传图、换素材库图片)自动换键;重试换代次即换键。之所以把内容指纹并进键里,是因为会改内容的入口散落在 `ImageCanvasGenerationDialogModel` / `ImageCanvasUploadModel` 的通用函数中,靠「每处都记得重铸键」迟早漏一处。
|
||||
8. 失败与退款沿用既有链路:文案由后端归一为「3D 模型生成失败,请稍后重试。」,侧栏与面板展示失败原因与已退还泥点。
|
||||
@@ -31,7 +31,7 @@ Parent Spec: `docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-2
|
||||
| `model3d-generation/Model3dGenerationForm.tsx` | 子项专属字段:文生提示词、图生参考图来源(用例并入 `Model3dGenerationModal.test.tsx`:这些字段只在面板里出现,单独挂一个 harness 只是重复) |
|
||||
| `model3d-generation/Model3dGenerationFormModel.ts` | 档位 → 六个必填参数的唯一映射、价格计算(读运行时定价缓存) |
|
||||
| `model3d-generation/Model3dGenerationFormModel.test.ts` | 映射、价格、缺价不可提交的用例 |
|
||||
| `model3d-generation/Model3dGenerationSubmission.ts` | 校验后的请求体组装、`target`、幂等键铸造(代次 + 内容指纹) |
|
||||
| `model3d-generation/Model3dGenerationSubmission.ts` | 校验后的请求体组装、落点字段、幂等键铸造(代次 + 内容指纹) |
|
||||
| `model3d-generation/Model3dGenerationSubmission.test.ts` | 请求体全量非空、参考图收敛、幂等键铸造与换键用例 |
|
||||
| `model3d-generation/useModel3dGenerationTask.ts` | 调用客户端 submit、排队、失败与重试收口(复用既有排队链路) |
|
||||
| `model3d-generation/useModel3dGenerationTask.test.tsx` | 入队、失败、缺价与缺工程不发请求的用例 |
|
||||
@@ -60,7 +60,7 @@ Parent Spec: `docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-2
|
||||
- 验收:同一组合在用例中给出与后端 `model3d_add_ons` 相同的 add-on 集合。
|
||||
3. **请求与幂等身份**
|
||||
- 两个客户端函数分别打 `/api/assets/tripo/text-to-model` 与 `/api/assets/tripo/image-to-model`,带 Bearer、`Content-Type: application/json`、`Idempotency-Key`,请求体类型直接消费 `packages/shared/src/contracts/model3d/*`。
|
||||
- `target` 固定 `projectResource` 分支并携带 `canvasCompletion`;图生把选中图片收敛成 `resource` / `asset` 引用。
|
||||
- 落点固定为项目资源:只发 `projectId`,并携带 `canvasCompletion`;图生把选中图片收敛成 `resource` / `asset` 引用。
|
||||
- 验收:请求体全量非空;同一次提交重放同键;重试换新键。
|
||||
4. **入口与子选项**
|
||||
- 工具栏入口 + 浮动子选项菜单 + 对话框两个 mode,形态与 `music` / `spec` 一致。
|
||||
|
||||
@@ -29,7 +29,7 @@ Implementation Plan: `docs/project-memory/plans/【实施计划】Tripo生成API
|
||||
## 验收标准
|
||||
|
||||
1. API 请求与结果契约可编译,并通过 ts-rs 生成到 `packages/shared/src/contracts/model3d/` 的对应目录;生成产物经 prettier 后提交。
|
||||
2. `target` 的 `assetLibrary` 分支缺少 `folderId` 或 `label` 时反序列化即失败;`projectResource` 分支缺少 `projectId` 同理。
|
||||
2. 请求落点用平坦可选字段:`projectId` 与 `assetFolderId` 都不给或都给都失败;`canvasCompletion` 与 `assetFolderId` 同现失败。
|
||||
3. `source` 同时给出两个 ID、或使用裸字符串时被拒绝。
|
||||
4. `external_generation_job` 新字段追加在结构体末尾且带显式默认值;`migration.rs`、表目录与生成绑定同步,`npm run check:spacetime-schema` 通过。
|
||||
5. checkpoint procedure 只在租约有效且 lease token 匹配时写入;过期或错误 token 被拒。
|
||||
|
||||
@@ -24,7 +24,7 @@ Related: `docs/adr/【ADR】0004-3D生成入口价格与幂等身份-2026-09-21.
|
||||
|
||||
- 不新建独立生成页、结果页、runtime、作品架、广场、作品统计;3D 是资产型产出,不进玩法闭环。
|
||||
- 不接 multiview-to-model、Splat、rig、animation、texture、convert。
|
||||
- 不做素材库落点入口(前端不暴露 `target = assetLibrary`)、后台 renderer、精选 read model、公开 grant、External v1。
|
||||
- 不做素材库落点入口(前端只发 `projectId`,不发 `assetFolderId`)、后台 renderer、精选 read model、公开 grant、External v1。
|
||||
- 不接画布 Agent 对话调度。
|
||||
- 不改后端、不改 schema、不新增公开 API。
|
||||
- 不在前端内置任何 3D 价格兜底数值;不承诺移动端画布(跟随现状)。
|
||||
|
||||
@@ -9383,3 +9383,13 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 影响面:`server-rs/crates/api-server/src/{tripo3d/pricing.rs,editor_generation_config.rs,editor_generation_model3d_records.rs,state.rs,admin.rs,app.rs,editor_project.rs}`、`server-rs/crates/spacetime-module/src/editor_project_storage.rs`、`server-rs/crates/spacetime-client/src/active/mapper/editor_project.rs` 与生成绑定、`server-rs/crates/shared-contracts/src/editor_generation.rs`、`apps/admin-web/src/{api/adminApiTypes.ts,api/adminApiClient.ts,pages/AdminEditorGenerationPricingPage.tsx,pages/AdminEditorGenerationModel3dPricingSection.tsx,pages/adminEditorGenerationPricing.ts}`、主规范、里程碑与实施计划、`pitfalls.md`。
|
||||
- 验证方式:`cargo test --locked -p api-server`(1183 passed;另有一条与本改动无关的时序敏感用例在整包并行下偶发失败、单跑通过)、`cargo test --locked -p spacetime-module editor_generation`、`npm run check:spacetime-schema`、`npm run check:encoding`、`node scripts/check-doc-index.mjs`、`git diff --check`、`npx vitest run apps/admin-web`(210 passed)、`npx tsc --noEmit -p apps/admin-web/tsconfig.json` 与改动文件 eslint。真实环境手工验收与发布尚未执行。
|
||||
- 关联文档:[ADR 0005](../../adr/【ADR】0005-3D生成定价迁入SpacetimeDB与后台编辑-2026-09-23.md)、[编辑器模型定价配置管理方案](../../【编辑器】模型定价配置管理方案-2026-06-22.md)、[实施计划](../plans/【实施计划】3D生成定价后台可编辑-2026-09-23.md)。
|
||||
|
||||
## 2026-09-23 3D 提交落点改为平坦可选字段:`target` tagged enum 作废,服务端按二选一校验
|
||||
|
||||
- 背景:3D 生成最初把结果落点设计成请求里的 tagged enum(`target = { kind: "projectResource", … } | { kind: "assetLibrary", … }`),理由是“落项目还是落素材库”是判别分支。落地后发现,其它所有生成接口(图片、音乐、图标、音效等)的落点都是平坦的可选字段 `projectId` / `canvasCompletion` / `assetFolderId` / `assetLabel`,只有 3D 多一层判别结构:同一件事在仓库里有两套形状,前端要为 3D 单独拼一次判别对象,服务端也要单独维护一套 tagged 校验。
|
||||
- 决策:撤销 tagged enum,请求契约改为与其它生成接口同形的平坦字段 `projectId?` / `canvasCompletion?` / `assetFolderId?` / `assetLabel?`;两个端点(text-to-model、image-to-model)完全同形。服务端在 `tripo3d/validation.rs` 按「`projectId` 与 `assetFolderId` 二选一」校验:都不给报 `projectId` 缺失、都给报 `assetFolderId` 冲突、`canvasCompletion` 与 `assetFolderId` 同现报冲突,字段名直接进错误响应,便于前端定位。结果侧的 `Model3dGenerationResult` 与 `Model3dGenerationTargetRef` 仍是按端点的 tagged enum,本次不动。同时把落点预检换成其它付费编辑器生成共用的 `preflight_editor_generation_target_and_return`(原先素材库落点只能读整库再匹配),`assetLabel` 缺省落 `MODEL3D_DEFAULT_ASSET_LABEL`。
|
||||
- 原因:**同一语义只保留一套形状**——判别结构并没有带来额外表达力(“二选一”在两侧都能表达),却让客户端多一层拼装、服务端多一套类型与校验,也让 3D 与其它生成接口的请求体不能共用同一套前端提交路径与错误提示;扁平字段还能天然复用既有的 `preflight_editor_billable_generation_target` / 归属预检口径。
|
||||
- 代价与取舍:`deny_unknown_fields` 在请求顶层继续生效,旧的 `target` 字段会被当成未知字段直接拒绝,因此这是一次**破坏性契约变更**(本分支未合并,无外部调用方,不需要兼容层);「二选一」从此是运行期校验而不是类型系统保证,缺两个或多个同时给要在服务端显式报错(已有定向用例钉住)。
|
||||
- 影响面:`server-rs/crates/shared-contracts/src/model3d/{text_to_model/request.rs,image_to_model/request.rs,common/mod.rs}`(删除 `common/generation_target.rs`)、`server-rs/crates/api-server/src/tripo3d/{job.rs,validation.rs,target.rs,routes.rs,worker.rs}`、`packages/shared/src/contracts/model3d/`(删除 `common/Model3dGenerationTarget.ts` 与 barrel 导出)、`src/components/image-editor/model3d-generation/{Model3dGenerationSubmission.ts,useModel3dGenerationTask.test.tsx}`、`src/services/image-editor/editorProjectClient.ts`、技术方案 / 里程碑 / 实施计划与共享记忆。
|
||||
- 验证方式:`cargo test -p shared-contracts`、`cargo test -p api-server tripo3d::`(54 passed,含 `target_requires_exactly_one_flat_locator` 与 `flat_locator_maps_to_point_lookup_without_rewriting_ids`)、`npx vitest run src/components/image-editor/model3d-generation src/components/image-editor/model3d-preview`(63 passed)、`npm run typecheck`、`npm run contracts:model3d:generate`、`npm run check:encoding`、`npm run check:rustfmt`、`git diff --check`。
|
||||
- 关联文档:[技术方案 Tripo 3D生成API集成](../../technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md)、[实施计划 Tripo生成API契约与数据模型](../plans/【实施计划】Tripo生成API契约与数据模型-2026-09-21.md)。
|
||||
|
||||
@@ -51,14 +51,18 @@ source = { kind: "resource", resourceId } | { kind: "asset", assetId }
|
||||
|
||||
引用解析分两步,提交与执行互不代替:**提交时只做元数据预检**——按 `source.kind` 的分支做定点归属校验,确认记录类型与对象键存在,不读图片正文、不调用 provider,因此跨 owner、未登记、已删除与 `kind` 不符的引用都在扣费与入队之前返回 400;跨 owner、未登记与已删除对外收敛成同一句不可用,不泄漏他人 ID 是否存在,`kind` 与解析结果不符则单独报错(两个值都由调用方给出,不涉及探测他人数据);**worker 执行时重新确认同一事实**,通过后才读一次图片正文并上传换 `file_token`。预检只按主键定点查引用,不允许拉取当前用户的完整工程列表或素材库。
|
||||
|
||||
**结果落点**(两个端点都必填)使用 tagged enum:
|
||||
**结果落点**(两个端点都必填)与其它生成接口同形,是平坦的可选字段,不用 tagged enum;客户端不必为“落项目还是落素材库”多拼一层判别结构:
|
||||
|
||||
```text
|
||||
target = { kind: "projectResource", projectId, canvasCompletion? }
|
||||
| { kind: "assetLibrary", folderId, label }
|
||||
projectId? 项目资源落点
|
||||
assetFolderId? 素材库落点
|
||||
assetLabel? 素材名称,缺省用平台默认名
|
||||
canvasCompletion? 画布占位框回填,只在项目资源落点下生效
|
||||
```
|
||||
|
||||
`projectResource` 分支必须给 `projectId`,可选 `canvasCompletion` 表达画布占位框回填;`assetLibrary` 分支的 `folderId` 与 `label` 必填。不存在“都不给就默认落素材库”的口径。
|
||||
`projectId` 与 `assetFolderId` 二选一且只能给一个:两个都不给、两个都给都拒绝;`assetLabel` 只在素材库落点下有默认值,`canvasCompletion` 与 `assetFolderId` 同现拒绝。不存在“都不给就默认落素材库”的口径。
|
||||
|
||||
落点预检与其它付费编辑器生成共用同一条路径:提交时调只读 `preflight_editor_generation_target_and_return`,按认证 owner 对给定的那个落点做定点归属校验(不再为了一个目录 ID 读整个素材库),跨 owner / 已删除 / 不存在收敛成同一句 400;预检传下去的就是 trim 后的原值,校验值与入队、落库的值逐字一致。`project` 与 owner 默认素材目录的归一由落库 procedure 负责,因此本期不做 API 层的 `folder-*` → 默认目录改写——那需要把归一值一路带进入队载荷,等真正打开素材库落点入口时再补。
|
||||
|
||||
**定价相关参数在 API 层必填并做组合校验**:`texture`、`textureQuality`、`geometryQuality`、`quad`、`smartLowPoly`、`generateParts` 必须显式给出。`texture=false` 时禁止出现 `textureQuality`,且 `pbr` 必须显式 `false`;`texture=true` 时 `textureQuality` 必填。理由是 provider 的隐式默认值会直接改变价格,一旦依赖默认值,报价与扣费会在“调用方少传字段”时分叉。
|
||||
|
||||
@@ -193,7 +197,7 @@ width/height = 预览图像素尺寸
|
||||
7. submit 成功但 checkpoint 写入失败的 attempt 终态失败且不自动重提。
|
||||
8. provider 成功但落库失败时 job 失败、该 attempt 退款,checkpoint 保留供对账。
|
||||
9. 完成结果按端点严格类型返回,只含模型与预览的正式资源引用,不含 provider task ID 与带签名的临时 URL。
|
||||
10. `assetLibrary` 分支产出 `assetId`;`projectResource` 分支产出 `resourceId`,且 `assetKind=model3d`、`AssetObject.content_type` 为按字节识别出的模型 mime、`content_length` 与实际对象一致。
|
||||
10. 素材库落点产出 `assetId`;项目资源落点产出 `resourceId`,且 `assetKind=model3d`、`AssetObject.content_type` 为按字节识别出的模型 mime、`content_length` 与实际对象一致。
|
||||
11. 客户端媒体投影对 `model3d` 只暴露预览图:`imageSrc` / `thumbnailSrc` 指向预览对象、`objectKey` 指向模型本体;预览缺失时才回落到模型对象,且该回落不得让图片入口加载模型文件。
|
||||
12. 格式判定的顺序在查看器包内可被用例钉住:声明 `Content-Type` 优先于字节魔数、字节魔数优先于地址扩展名;三者都判不出来时模态展示 `unsupported-format` 原因与支持的格式清单,画布侧不做任何格式判断、也不在 `assetKind` 为空时按扩展名兜底。
|
||||
13. 真实 Provider smoke 覆盖 text-to-model 与 image-to-model 各一次,下载字节数与 `Content-Length` 一致。
|
||||
|
||||
Reference in New Issue
Block a user