diff --git a/docs/project-memory/plans/【实施计划】Tripo生成API契约与数据模型-2026-09-21.md b/docs/project-memory/plans/【实施计划】Tripo生成API契约与数据模型-2026-09-21.md index 42f430239..65561da1a 100644 --- a/docs/project-memory/plans/【实施计划】Tripo生成API契约与数据模型-2026-09-21.md +++ b/docs/project-memory/plans/【实施计划】Tripo生成API契约与数据模型-2026-09-21.md @@ -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 目录绑定** diff --git a/docs/project-memory/plans/【实施计划】Tripo生成Worker执行链路与API路由-2026-09-21.md b/docs/project-memory/plans/【实施计划】Tripo生成Worker执行链路与API路由-2026-09-21.md index 14a945668..3d34215fa 100644 --- a/docs/project-memory/plans/【实施计划】Tripo生成Worker执行链路与API路由-2026-09-21.md +++ b/docs/project-memory/plans/【实施计划】Tripo生成Worker执行链路与API路由-2026-09-21.md @@ -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 写入失败按失败收口。 diff --git a/docs/project-memory/plans/【实施计划】Tripo生成前端入口-2026-09-21.md b/docs/project-memory/plans/【实施计划】Tripo生成前端入口-2026-09-21.md index c557c5881..81e8250a7 100644 --- a/docs/project-memory/plans/【实施计划】Tripo生成前端入口-2026-09-21.md +++ b/docs/project-memory/plans/【实施计划】Tripo生成前端入口-2026-09-21.md @@ -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` 一致。 diff --git a/docs/project-memory/plans/【里程碑】Tripo生成API契约与数据模型-2026-09-21.md b/docs/project-memory/plans/【里程碑】Tripo生成API契约与数据模型-2026-09-21.md index 0b4d37c4c..9a9210771 100644 --- a/docs/project-memory/plans/【里程碑】Tripo生成API契约与数据模型-2026-09-21.md +++ b/docs/project-memory/plans/【里程碑】Tripo生成API契约与数据模型-2026-09-21.md @@ -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 被拒。 diff --git a/docs/project-memory/plans/【里程碑】Tripo生成前端入口-2026-09-21.md b/docs/project-memory/plans/【里程碑】Tripo生成前端入口-2026-09-21.md index c5f8c3a90..91f8e4dc3 100644 --- a/docs/project-memory/plans/【里程碑】Tripo生成前端入口-2026-09-21.md +++ b/docs/project-memory/plans/【里程碑】Tripo生成前端入口-2026-09-21.md @@ -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 价格兜底数值;不承诺移动端画布(跟随现状)。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 0e6523195..9cbfb3315 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -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)。 diff --git a/docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md b/docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md index 1760ccde4..d5715160c 100644 --- a/docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md +++ b/docs/technical/【技术方案】Tripo 3D生成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` 一致。