图片画布接入 3D 资源:新增 model3d 类别、3D 预览与 3D 生成入口
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 16s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 16s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 16s
Project CI / Native shell tests (pull_request) Failing after 16s
Project CI / Frontend tests (pull_request) Failing after 7s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / AI game creator shell web tests (pull_request) Failing after 12s

- 后端资源投影按 asset_kind 分叉,新增 project_editor_client_media_src 与 EDITOR_MODEL3D_ASSET_KIND:model3d 只投影预览图,模型本体只留 objectKey,预览缺失才回落历史口径
- tripo3d 存储新增按字节魔数识别模型 content_type 与扩展名映射,不采信 provider 声明与下载响应头
- editor_project_storage 把 model3d 加入 EDITOR_CANVAS_ASSET_KINDS 白名单
- 前端 CanvasAssetKind 与 image-canvas-core 类型新增 model3d:画布卡片渲染预览图或 3D 占位,类型角标新增「3D模型」,覆盖资格只在它与空类别之间成立
- 新增 model3d-preview 子目录(Model3dViewerModal / Model3dViewerModel),Tailwind 内联、不新增 CSS,失败原因分超限 / 格式不可识别 / 无 WebGL2 三类展示
- 选中工具条新增只读 preview-model3d 能力位,仅 objectKey 为空时置灰;打开预览同时选中图层并暂停画布快捷键与舞台交互
- 查看器包源对象 format 改为可选,新增「声明 Content-Type → 字节魔数 → 地址扩展名」三级格式判定并导出支持格式清单
- 导出工作流新增 3d_models/ 目录,模型下载按真实响应头 MIME 定扩展名、对象键扩展名兜底
- 新增 3D 生成入口:底部工具栏浮动子选项与文生 / 图生 3D 面板,落 projectResource + canvasCompletion,复用占位框与任务侧栏
- 3D 定价只读实时定价查询,段缺失即入口不可提交;幂等键由前端铸造,用户重试必须换键
- 新增 ADR 0003 / 0004 与实施计划、里程碑文档;更新 Tripo 技术方案、后端架构、CONTEXT 术语、decision-log 与 pitfalls
- .gitignore 忽略仓库根目录手工下载的 3D 样例模型
This commit is contained in:
2026-09-22 10:10:00 +08:00
parent 9fc55a5cac
commit 61fe4c48af
64 changed files with 4964 additions and 119 deletions
+2
View File
@@ -79,6 +79,8 @@
- [Tripo SDK 隔离与 Provider DTO 边界 ADR](./adr/【ADR】0001-Tripo%20SDK隔离与Provider%20DTO边界-2026-09-18.md):固定 SDK 只存在于 `platform-tripo` 内部,provider task 与产品资源结果分层。
- [Tripo 生成 API 集成边界与提交时定价 ADR](./adr/【ADR】0002-Tripo生成API集成边界与提交时定价-2026-09-21.md):全新 API 不复用 Hyper3Dat-most-once submit,按提交时定价真实扣费。
- [3D 资源客户端媒体投影与格式真实性 ADR](./adr/【ADR】0003-3D资源客户端媒体投影与格式真实性-2026-09-21.md):3D 资源的客户端投影按类别分叉,模型内容类型按字节识别,格式判定只在查看器包内(声明类型 → 字节魔数 → 地址扩展名),画布不判格式。
- [3D 生成入口的价格真相与幂等身份 ADR](./adr/【ADR】0004-3D生成入口价格与幂等身份-2026-09-21.md):3D 生成入口的价格只读实时定价查询,缺段即不可提交;幂等键由前端铸造,重试必须换键。
- [BgFilter 受限资源调度方案](./technical/【后端架构】BgFilter受限资源调度方案-2026-07-21.md)
- [Issue225 登录成功 AGC 用户归属修复](./technical/【后端架构】Issue225登录成功AGC用户归属修复方案-2026-09-03.md):登录 route tracking 的真实用户归属、`daily_login` 幂等边界和实施验收。
@@ -0,0 +1,13 @@
# 【ADR】0003-3D 资源客户端媒体投影与格式真实性-2026-09-21
状态:已接受
3D 生成结果在资源行里同时持有三个字段:`objectKey` 是模型本体,`imageSrc``thumbnailSrc` 是预览图。客户端可见媒体投影不能再沿用「`objectKey` 非空就以它充当 `imageSrc`」的历史口径,否则画布卡片、素材缩略图与后台都会把 `.glb` 交给图片元素加载;因此投影改为按 `asset_kind` 分叉:`model3d` 只把预览图投影给客户端(依次取 `thumbnail_src``image_src`),模型本体只留在 `objectKey` 里交给 3D 查看器,其它类别保持原样,两个来源都缺失时才回落到历史口径。
模型格式的真相同样只保留一处:后端写入 `asset_object.content_type` 之前按字节魔数识别真实产物(GLB 的 `glTF` 头、FBX 的 `Kaydara FBX Binary` 头、glTF 的 JSON 正文),对象键扩展名由该内容类型派生而不是反向推断。客户端不复制这份格式清单,也不在画布层按扩展名预先判定放行:画布只认「`assetKind=model3d``objectKey` 非空」,真正的格式判定跟着解析器走——`packages/model3d-viewer` 依次采信读接口声明的 `Content-Type`、字节魔数、地址扩展名,格式在该包里是可选输入,三者都判不出来时以 `unsupported-format` 失败,并把原因与支持的格式清单显示在模态里,而不是把入口藏起来去让用户猜。
被否决的做法:让画布或宿主外壳按对象键扩展名自行判断放行(客户端第二份格式清单,查看器扩了格式后画布仍会继续禁用入口);给资源表新增 `model_format` 列(同一真相两个来源,且 provider 上报的格式不可信);预览图缺失时把模型文件直接喂给图片元素(静默坏图、无法诊断,也说不出「该资源没有可预览的模型」这类错误信息);在画布侧补「`assetKind` 为空但对象键像模型」的扩展名兜底(这种资源本就不该出现,兜底只会掩盖后端投影缺陷)。
后果:下载 3D 资源时按真实响应头的 MIME 命名、对象键扩展名兜底、最后回落 `glb`,导出目录用 `3d_models/` 与图片、其它媒体分开;类型角标、缩略图和后台渲染器只要读 `imageSrc` 就能拿到预览图,不必各自识别媒体语义;查看器包单模型体积上限 64 MiB,超限与 `webgl-unavailable` 都作为失败原因展示给用户,而不是静默失败。
未进入项:AGC 项目画布自带的两份格式判断(`isResourceModelFbx``octet-stream` 当 FBX、资源卡预览用扩展名正则选解析器)本期不动,后续统一收敛到 `packages/model3d-viewer`;素材库行、后台 renderer、精选 read model 与公开 grant 的 3D 展示同样未进入。
@@ -0,0 +1,13 @@
# 【ADR】0004-3D 生成入口的价格真相与幂等身份-2026-09-21
状态:已接受
图片画布新增的 3D 生成入口,在提交前展示的泥点价格**只**来自编辑器生成定价查询(`GET /api/editor/generation-pricing`)返回的 `model3d` 段,前端不内置任何 3D 价格兜底数值;该段缺失时入口不可提交,既不发请求也不静默按 0 计价。这与图片 / 视频 / 音频一族相反——那些工具在前端保留内置兜底配置(`EDITOR_MODEL_MUD_POINT_CONFIG`),接口异常时价格照常展示。3D 刻意反着做:单次成本高一个量级,且后端在价格段缺失时是 fail closed503 `model3d-pricing-unavailable`),前端若拿兜底数值展示,用户会看到「面板有价、提交被拒」,或者更糟——按错的数字被扣费。
被否决的做法:**新增后端 quote / 预检接口**(价格权威,但要新增公开 API、契约与规范流程,而现有请求参数组合已足以在客户端算出同一结果);**前端写死 3D 兜底价格**(与后端 fail closed 冲突,价格出现两个真相)。
3D 生成入口同时是画布链路里**第一个必须自带 `Idempotency-Key` 的生成工具**:既有的画布生成 POST 只带 `x-request-id`,且显式禁止自动重试。两个 3D submit 端点缺少该头一律 400,因此键由前端铸造并绑定当次请求内容;用户主动重试必须铸造新键,因为同键同请求只会返回原来那个(已完成或已失败的)operation,复用旧键的「重试」不会真的重跑。键与 payload 都由服务端参与指纹,用户改了参数即视为新请求。
落地形态:键 = 尝试代次 + 请求内容指纹(`Model3dGenerationSubmission.resolveModel3dRequestKey`)。服务端按「键 + payload 指纹」dedupe(同键同请求返原 operation,同键不同请求 409),而会改请求内容的入口散落在 `ImageCanvasGenerationDialogModel` / `ImageCanvasUploadModel` 的通用函数里,因此把内容指纹并进键,而不是靠每处变更都记得重铸键。代次只负责让「用户主动重试」与上一次尝试区分开。
后果:`model3d` 定价段是 3D 入口的唯一价格真相,补价必须先落配置再开入口;前端必须保留一份「档位 → 六个必填参数」的映射与加和逻辑,它与后端 `model3d_add_ons` 是两份实现,靠同一组参数组合的用例对齐;提交接口不扣费,价格在提交时确定并写入 job,真正扣费发生在 worker 起跑、调用 provider 之前,失败按 attempt 冲正。
@@ -0,0 +1,113 @@
# Tripo 生成前端入口实施计划
Version: 1.0
Status: active
Date: 2026-09-21
Milestone Spec: `docs/project-memory/plans/【里程碑】Tripo生成前端入口-2026-09-21.md`
Parent Spec: `docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md`
## 边界
新代码只落在 `src/components/image-editor/model3d-generation/`,按 `src/components/image-editor/model3d-preview/` 的命名与切分方式组织:深目录、小文件、不带 `ImageCanvas` 前缀、测试文件与被测文件同名。画布其余部分只做必要接线(工具栏入口、子选项菜单、对话框模式、请求客户端)。不改后端、不改 schema、不新增公开 API、不接 `/api/external/v1`
## 已确认口径
1. 宿主是画布生成链路,不新建独立生成页 / 结果页 / runtime / 作品架;3D 是资产型产出。
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`)。
6. 图生输入照现有参考图来源菜单:从画布中选择 / 上传图片 / 从项目素材中选择,最终收敛成契约允许的 `{ kind: "resource", resourceId }``{ kind: "asset", assetId }`;只允许一张,不接 multiview。
7. `Idempotency-Key` 由前端铸造并绑定当次请求,用户主动重试铸造新键、不复用旧键。落地形态是「尝试代次 + 请求内容指纹」(`Model3dGenerationSubmission.resolveModel3dRequestKey`):同一份内容重复提交(双击)仍命中同一个 operation,内容一变(含换参考图、换上传图、换素材库图片)自动换键;重试换代次即换键。之所以把内容指纹并进键里,是因为会改内容的入口散落在 `ImageCanvasGenerationDialogModel` / `ImageCanvasUploadModel` 的通用函数中,靠「每处都记得重铸键」迟早漏一处。
8. 失败与退款沿用既有链路:文案由后端归一为「3D 模型生成失败,请稍后重试。」,侧栏与面板展示失败原因与已退还泥点。
9. 不做项见里程碑「不做」一节。
## 文件切分
| 文件 | 职责 |
| --- | --- |
| `model3d-generation/Model3dGenerationModal.tsx` | 提交面板外壳:两个子项共用的表单、状态区、提交按钮 |
| `model3d-generation/Model3dGenerationModal.test.tsx` | 面板行为用例 |
| `model3d-generation/Model3dGenerationForm.tsx` | 子项专属字段:文生提示词、图生参考图来源 |
| `model3d-generation/Model3dGenerationForm.test.tsx` | 字段与来源菜单用例 |
| `model3d-generation/Model3dGenerationFormModel.ts` | 档位 → 六个必填参数的唯一映射、价格计算(读运行时定价缓存) |
| `model3d-generation/Model3dGenerationFormModel.test.ts` | 映射、价格、缺价不可提交的用例 |
| `model3d-generation/Model3dGenerationSubmission.ts` | 校验后的请求体组装、`target`、幂等键铸造(代次 + 内容指纹) |
| `model3d-generation/Model3dGenerationSubmission.test.ts` | 请求体全量非空、参考图收敛、幂等键铸造与换键用例 |
| `model3d-generation/useModel3dGenerationTask.ts` | 调用客户端 submit、排队、失败与重试收口(复用既有排队链路) |
| `model3d-generation/useModel3dGenerationTask.test.tsx` | 入队、失败、缺价与缺工程不发请求的用例 |
外溢接线(各一处):
| 文件 | 接线 |
| --- | --- |
| `src/components/image-editor/ImageCanvasBottomToolbarView.tsx` | 新增工具栏入口(走 `isToolbarOptionTool` 那一档) |
| `src/components/image-editor/useImageCanvasGenerationSurface.tsx` | 子选项菜单与面板渲染分支 |
| `src/components/image-editor/ImageCanvasEditorTypes.ts` | `CanvasTool``CanvasGenerationAction``GenerateDialogState.mode` 增加 3D 分支 |
| `src/services/image-editor/editorProjectClient.ts` | 两个 submit 函数(含 `Idempotency-Key`)、定价 DTO 补 `model3d` 段 |
| `src/components/image-editor/ImageCanvasGenerationModel.ts` | 图生 3D 的单参考图槽位上限(`usesSingleImageReferenceSlot` / `resolveDialogExtraImageReferenceLimit` |
| `src/components/image-editor/ImageCanvasGenerationDialogModel.ts` | 画布点选与项目素材选中进入 `model3d-image-to-model` 的参考图字段;单槽位下素材库选择是替换 |
| `src/components/image-editor/ImageCanvasUploadModel.ts` | 上传图片进入 `model3d-image-to-model` 的参考图字段 |
后三处是必须的:参考图来源菜单(从画布中选择 / 上传图片 / 从项目素材中选择)的三条路径都按显式 mode 列表分流,新工具漏在列表外会表现为「点了菜单没反应」。
## 步骤
1. **定价段接入**
- `editorProjectClient.ts``EditorGenerationPricingConfig``model3d` 段(形状对齐 Rust `Model3dPricingConfig``basePrices` + `addOnPrices`),运行时缓存同步保存。
- 验收:payload 有段时面板能算出价格;无段时两个子面板都不可提交,且不发请求。
2. **档位与参数映射**
- `Model3dGenerationFormModel.ts` 定义用户可见档位与开关到六个必填参数的唯一映射,以及价格加和;不得在别处再写一份。
- 验收:同一组合在用例中给出与后端 `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` 引用。
- 验收:请求体全量非空;同一次提交重放同键;重试换新键。
4. **入口与子选项**
- 工具栏入口 + 浮动子选项菜单 + 对话框两个 mode,形态与 `music` / `spec` 一致。
- 验收:点开只有两个子项;选中后进入对应面板。
5. **提交与回填**
- 复用既有排队路径:置 `status: 'generating'`、下发占位框、`waitForEditorGenerationQueue` 轮询、终态 `loadEditorProject` + `applyProjectSnapshot` 回填。
- 验收:提交后画布出现占位框、任务侧栏出现任务且阶段文案正确、成功后占位框被正式资源图层替换。
6. **失败、退款与重试**
- 面板与侧栏展示失败文案与已退还泥点;重试按钮铸造新键重新提交。
- 验收:失败不残留占位框;重试产生新 operation。
7. **收口**
- 生成结果可立即用既有「3D 预览」打开;补齐里程碑验收标准 1 至 8 的证据。
- 验收:`npm run typecheck`、定向 vitest、`npm run check:encoding``node scripts/check-doc-index.mjs``git diff --check` 全部通过。
## 进展(2026-09-21
代码已落地,全部集中在 `src/components/image-editor/model3d-generation/` 与上表接线处;未改后端、未改 schema、未新增公开 API。
| 里程碑验收标准 | 证据 |
| --- | --- |
| 1 工具栏入口只有两个子项 | `ImageCanvasBottomToolbarView.test.tsx``useImageCanvasGenerationSurface.test.tsx``ImageCanvasEditorShellView.test.tsx`;子项文案与后端 `phaseLabel` 对照 |
| 2 请求体全量非空 | `Model3dGenerationSubmission.test.ts`(逐字段断言;纯几何档位断言 `textureQuality` 缺席);`Model3dGenerationFormModel.test.ts` 与后端 `model3d_add_ons` 组合对齐 |
| 3 价格只读实时查询、缺段不可提交 | `Model3dGenerationFormModel.test.ts``Model3dGenerationModal.test.tsx`(缺段时按钮禁用、显示原因、不发请求)、`useModel3dGenerationTask.test.tsx`(不发请求) |
| 4 `Idempotency-Key` 与重试换键 | `Model3dGenerationSubmission.test.ts`(同内容同键、换内容换键、重试换代次换键、键可作请求头) |
| 5 占位框与终态回填 | `useModel3dGenerationTask.test.tsx`(以对话框 id 交给 `applyQueuedEditorGenerationProject`);真实排队回填需联调环境 |
| 6 失败与退款文案 | `Model3dGenerationModal.test.tsx``useModel3dGenerationTask.test.tsx`;退款提示由既有任务侧栏承接 |
| 7 图生只接受一张平台内图片 | `ImageCanvasGenerationDialogModel.test.ts``projectAssetReferencePickerModel.test.ts``ImageCanvasUploadModel.test.ts``Model3dGenerationSubmission.test.ts`(上传未登记、空白 id 都不构成 `source` |
| 8 产物可用既有 3D 预览打开 | 由 [`【实施计划】Tripo生成结果前端预览接入-2026-09-21.md`](./【实施计划】Tripo生成结果前端预览接入-2026-09-21.md) 交付;本入口只负责产出 `model3d` 资源 |
仍未验证的部分需要环境:`model3d` 定价段写入运行配置后的真实扣费与排队回填、provider 端到端产物。
## 验证命令
```bash
npx vitest run src/components/image-editor/model3d-generation
npx vitest run src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx
npm run typecheck
npm run check:encoding
node scripts/check-doc-index.mjs
git diff --check
```
## 风险与回退
- **缺价即不可用**`model3d` 定价段未写入运行环境时,本期交付的入口表现为「不可提交」。这是设计如此(见 ADR 0004),不是缺陷;但演示与验收需要先落配置。
- **前端等待窗口**:客户端排队轮询上限 20 分钟,后端 3D 长任务超时 1800 秒;排队叠加执行可能顶到前端窗口,需要按真实耗时确认是否调整或改由任务侧栏兜底。
- **worker 并发**3D 任务在单次 attempt 内阻塞数分钟,默认并发 2,会占住并发位。
- **价格两份实现**:前端档位映射与后端 add-on 判定必须靠同一组参数组合的用例对齐,否则出现「面板一个价、账单另一个价」。
- **回退点**:删除工具栏接线与 `model3d-generation/` 新文件即可回退;后端、schema、定价配置均不受影响。
@@ -0,0 +1,43 @@
# Tripo 生成结果前端预览接入实施计划-2026-09-21
对应主规范:`docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md`;架构决策:`docs/adr/【ADR】0003-3D资源客户端媒体投影与格式真实性-2026-09-21.md`
## 交付结果
Tripo 生成的 3D 资源在图片画布里作为 `model3d` 类别落地:卡片显示预览图,选中后在工具条上出现「3D 预览」,打开桌面模态把模型本体交给 `packages/model3d-viewer` 渲染;资源行的客户端媒体投影改为按类别分叉,模型本体不再被图片入口加载。
## 已确认口径
1. `model3d` 是新的 `CanvasAssetKind`,画布媒体类型仍是 `image`:卡片、缩略图消费预览图,模型本体只进查看器。
2. 工具条入口落在资源上(选中图层的浮动工具条),不动信息角标;同族图层的像素级编辑组(裁扩、去背景、完美像素、改造)整体不适用。
3. 工具条按钮只在 `objectKey` 为空时置灰(标题「该资源没有可预览的 3D 模型文件」);画布不判格式,格式判不出来是查看器的事,模态里显示失败原因,把错误信息交给用户而不是让他猜功能是否上线。
4. 类型角标新增「3D模型」,与其它类别互为独立媒体族;覆盖资格只在 `model3d` 与空类别之间成立,图片族不能覆盖成 3D。
5. 后端 `imageSrc` / `thumbnailSrc` 都是预览图、`objectKey` 是模型本体;投影由 `project_editor_client_media_src` 统一决定,预览缺失才回落到历史口径。
6. 下载 3D 资源导出模型本体:MIME 优先、对象键扩展名兜底、最后回落 `glb`
7. 本期只做画布工具条入口,素材库行、后台 renderer、精选 read model、公开 grant 与 External v1 不进入。
8. 画布编辑器不承诺移动端,查看器模态固定 `lg` 尺寸、高度 `min(58vh, 30rem)`,不自动旋转,页脚「重置视角」仅在查看器状态为 `ready` 时可用。
9. 后端写入 `content_type` 前按字节魔数识别 3D 产物;查看器单模型上限 64 MiB;客户端不抄格式清单,格式判定在 `packages/model3d-viewer` 内按「声明 `Content-Type` → 字节魔数 → 地址扩展名」进行,判不出来即 `unsupported-format`
10. 「哪些宿主能开 3D 预览」通过工具条的 `preview-model3d` capability 位表达,AGC 项目画布本期不进入该能力集合。
11. 打开模态时同时选中该图层;画布快捷键与舞台交互在元数据面板或 3D 预览模态打开期间暂停。
12. 导出 3D 图层落到 `3d_models/` 目录(图片 `images/`、其它媒体 `media/`),文件名按真实响应头 MIME 定扩展名。
13. 失败原因逐条翻译给用户:超限(64 MiB)、格式不可识别(列出支持格式)、环境不支持 WebGL2;预览中不自动旋转。
## 落地改动
- 后端:`editor_project.rs` 新增 `project_editor_client_media_src``EDITOR_MODEL3D_ASSET_KIND`,改写资源、精选快照与素材三处投影;`tripo3d/storage.rs` 新增 `sniff_model_content_type` / `resolve_artifact_content_type` 与模型扩展名映射;`editor_project_storage.rs``model3d` 加入 `EDITOR_CANVAS_ASSET_KINDS`(白名单与前端标签选项同步)。
- 前端:`CanvasAssetKind``image-canvas-core` 类型同步新增 `model3d`;新增 `src/components/image-editor/model3d-preview/``Model3dViewerModel.ts` + `Model3dViewerModal.tsx`Tailwind 内联、无新增 CSS);画布卡片新增 3D 图层渲染(预览图或占位);工具条新增 `preview-model3d` 动作;导出工作流新增 `3d_models/` 目录与模型扩展名映射。
- 查看器包:`packages/model3d-viewer` 的源对象 `format` 改为可选,新增按声明 `Content-Type` / 字节魔数 / 地址扩展名三级判定格式的解析函数,并导出支持的格式清单供模态文案复用。
- 文档:新增本计划、ADR 0003,补 `CONTEXT.md` 术语,修正 Tripo 技术方案与后端架构文档里「`model3d` 是后端-only 类别 / 不进 `EDITOR_CANVAS_ASSET_KINDS`」的过期口径。
## 验证
- `npx vitest run src/components/image-editor` 中与本变更相关的用例(工具条、画布视图、编辑器模型、导出工作流、`model3d-preview`)全部通过;仓储内存不可用的既有用例在本地环境失败,与本次改动无关。
- 后端 `cargo test --locked -p api-server` 的媒体投影与 Tripo 存储用例通过。
- `npm run typecheck``npm run check:encoding``node scripts/check-doc-index.mjs``git diff --check`
## 风险与未进入项
- glTF 的多文件兄弟资源(`.bin`、贴图)暂不解析,只支持自包含模型;provider 若返回多文件 glTF,查看器会加载失败并提示。
- 一张模型只显示一张预览图;多视角、动画播放、材质切换与模型信息(面数、体积)都不在本次范围。
- 素材库行、后台列表、精选卡片与公开 grant 未接 3D renderer,只保证字段正确、不承诺展示。
- AGC 项目画布自带两份格式判断(`isResourceModelFbx``octet-stream` 当 FBX、资源卡预览的扩展名正则)本期不动,后续统一收敛到 `packages/model3d-viewer`
@@ -0,0 +1,51 @@
# Tripo 生成前端入口
Version: 1.0
Status: active
Date: 2026-09-21
Parent Spec: `docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md`
Implementation Plan: `docs/project-memory/plans/【实施计划】Tripo生成前端入口-2026-09-21.md`
Related: `docs/adr/【ADR】0004-3D生成入口价格与幂等身份-2026-09-21.md``docs/project-memory/plans/【实施计划】Tripo生成结果前端预览接入-2026-09-21.md`
## 目标
在图片画布工程 `/editor/canvas` 的底部工具栏新增「生成 3D 模型」入口,点开后给出「文生 3D 模型」与「图生 3D 模型」两个子项,按画布既有生成链路提交到 tripo3d 的两个 submit API;产物以 `model3d` 画布资源落地,并可直接用既有的「3D 预览」查看。
## 范围
- 底部工具栏入口与子选项菜单,形态照 `music` / `spec` 的「一项入口 → 浮动子选项 → 各自一个提交面板」。
- 两个提交面板:档位化的参数选择、提交前价格展示、文生提示词、图生参考图来源。
- 前端铸造 `Idempotency-Key`,并按契约全量非空提交必填参数。
- 画布占位框、入队、任务侧栏、失败与退款提示、终态刷新回填。
- 3D 定价段的前端消费:只读实时定价查询。
- 契约消费:`packages/shared/src/contracts/model3d/*` 只消费,不手写 TS。
## 不做
- 不新建独立生成页、结果页、runtime、作品架、广场、作品统计;3D 是资产型产出,不进玩法闭环。
- 不接 multiview-to-model、Splat、rig、animation、texture、convert。
- 不做素材库落点入口(前端不暴露 `target = assetLibrary`)、后台 renderer、精选 read model、公开 grant、External v1。
- 不接画布 Agent 对话调度。
- 不改后端、不改 schema、不新增公开 API。
- 不在前端内置任何 3D 价格兜底数值;不承诺移动端画布(跟随现状)。
## 验收标准
1. 工具栏出现「生成 3D 模型」,点开只有两个子项,子项文案与任务侧栏 `phaseLabel` 一致。
2. 每次提交的请求体包含全部必填参数(含六个定价参数),不存在缺字段或 `undefined`
3. 价格只来自 `GET /api/editor/generation-pricing``model3d` 段;该段缺失时两个子面板都不可提交,且不发任何请求。
4. 每次提交携带 `Idempotency-Key`;同一次提交重放不产生第二个 operation;用户主动重试铸造新键。
5. 提交后画布出现占位框,任务侧栏出现该任务并显示「文生 3D 模型」/「图生 3D 模型」;成功后占位框被正式资源图层替换。
6. 失败时面板与侧栏展示后端归一文案,并显示已退还泥点。
7. 图生只接受一张平台内图片(画布资源或素材库资产),不接受 URL、data URL 或本地字节直传。
8. 生成的 `model3d` 资源可立即用既有「3D 预览」打开。
9. `npm run typecheck`、定向 vitest、`npm run check:encoding``node scripts/check-doc-index.mjs``git diff --check` 通过。
## 进展
实现已落地(2026-09-21),逐条证据见 [`【实施计划】Tripo生成前端入口-2026-09-21.md`](./【实施计划】Tripo生成前端入口-2026-09-21.md) 的「进展」一节。验收标准 1 至 4、7 已由定向 vitest 覆盖;5、6 的排队回填与退款展示复用既有链路并已覆盖交接点,真实表现需在写入 `model3d` 定价段的联调环境确认;8 由预览接入计划交付。
## 依赖
- 后端里程碑 [`【里程碑】Tripo生成Worker执行链路与API路由-2026-09-21.md`](./【里程碑】Tripo生成Worker执行链路与API路由-2026-09-21.md) 的路由与 worker 分支已上线。
- `model3d` 定价段已写入运行环境配置;未写入时本入口保持不可提交(fail closed),这不是本期缺陷。
@@ -8909,3 +8909,27 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 边界:预检只按主键定点查引用,禁止拉取当前用户的完整工程列表或素材库,也禁止在提交路径上执行 provider 副作用(上传 `file_token`、submit)。该口径适用于所有「服务端站内引用 → provider 输入」的生成入口,Tripo 是首个按此收口的类别。
- 验证:`cargo test -p api-server` 1085 通过(含新增的 `tripo3d::image_source` 源码钉住用例,断言预检与定点解析只调用窄查询、不含列工程 / 列素材库 / 读正文 / 上传);`cargo check -p api-server` 无 tripo3d 警告;`npm run check:encoding``node scripts/check-doc-index.mjs``git diff --check` 通过。真实 provider 端到端仍未跑。
- 关联文档:[技术方案 Tripo 3D生成API集成](../../technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md)、[实施计划 Tripo生成Worker执行链路与API路由](../plans/【实施计划】Tripo生成Worker执行链路与API路由-2026-09-21.md)。
## 2026-09-21 3D 资源的客户端媒体投影按类别分叉,模型格式真相只在内容类型与查看器能力各一处
- 背景:Tripo 生成的 `model3d` 资源同时持有模型本体(`objectKey`)与预览图(`imageSrc` / `thumbnailSrc`),而客户端媒体投影历史口径是「`objectKey` 非空就以它充当 `imageSrc`」,于是画布卡片、素材缩略图与后台都会把 `.glb` 交给图片渲染器。
- 决策:投影收口成一个函数(`project_editor_client_media_src`):`model3d` 只投影预览图(`thumbnail_src``image_src`),模型本体只留在 `objectKey`;其它类别保持原口径,预览缺失才回落。写入 `asset_object.content_type` 前按字节魔数识别真实产物(GLB 的 `glTF` 头、FBX 的 `Kaydara FBX Binary` 头、glTF 的 JSON 正文),对象键扩展名由内容类型派生。客户端不抄格式清单、也不按扩展名预判放行:画布只认「`assetKind=model3d``objectKey` 非空」,格式判定跟着解析器走——`packages/model3d-viewer` 依次采信读接口声明的 `Content-Type`、字节魔数、地址扩展名,格式在该包里是可选输入,判不出来即 `unsupported-format` 并把原因显示在模态里。
- 边界:`model3d` 是独立媒体族,标签覆盖只在它与空类别之间成立;快速编辑与改造都不进入。画布工具条新增只读 capability 位 `preview-model3d`,AGC 项目画布不声明;导出 3D 图层落 `3d_models/` 目录;AGC 项目画布自带的两份格式判断本期不动,后续统一收敛到查看器包;素材库行、后台 renderer、精选 read model、公开 grant 与 External v1 本期不进入。
- 验证:`cargo test --locked -p api-server` 的媒体投影与 Tripo 存储用例通过;前端 `model3d-preview`、选中工具条、画布视图、编辑器模型与导出工作流用例通过;`npm run check:encoding``node scripts/check-doc-index.mjs``git diff --check` 通过。真实 provider 端到端与素材库 / 后台 renderer 仍未接。
- 关联文档:[ADR 0003](../../adr/【ADR】0003-3D资源客户端媒体投影与格式真实性-2026-09-21.md)、[技术方案 Tripo 3D生成API集成](../../technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md)、[实施计划 Tripo生成结果前端预览接入](../plans/【实施计划】Tripo生成结果前端预览接入-2026-09-21.md)。
## 2026-09-21 3D 生成入口落在画布生成链路,价格只读实时定价查询,幂等键由前端铸造
- 背景:Tripo 两个 submit API 与 worker 已就绪,需要在前端开一个「生成 3D 模型」入口。3D 是资产型产出(没有 runtime、单局、胜负、作品架语义),而画布已经把 `model3d` 当作 `CanvasAssetKind`,并由只读「3D 预览」承接消费。
- 决策:宿主固定为图片画布工程 `/editor/canvas` 的底部工具栏,形态照 `music` / `spec` 的「一项入口 → 浮动子选项 → 各自一个提交面板」,两个子项为「文生 3D 模型」「图生 3D 模型」(文案对齐后端 `phaseLabel`);落点固定 `projectResource` + `canvasCompletion`,复用既有占位框、入队、任务侧栏与终态刷新回填;参数永远全量非空提交;图生只接受一张平台内图片(画布资源或素材库资产,经既有参考图来源菜单收敛成 `resource` / `asset` 引用)。不新建独立生成页 / 结果页 / runtime / 作品架 / 广场 / 统计,不接 multiview,不接画布 Agent 对话,不做素材库落点入口(前端不暴露 `target = assetLibrary`)。
- 价格与幂等(见 ADR 0004):提交前价格只来自 `GET /api/editor/generation-pricing``model3d` 段,段缺失即入口不可提交且不发请求,前端零 3D 兜底数值;`Idempotency-Key` 由前端铸造(画布链路首次需要该头),用户主动重试必须铸造新键——同键同 payload 只会返回原 operation,而 Tripo 队列 `max_attempts = 1`,不存在「同键重跑」。
- 边界:该条记录的是文档先行阶段(ADR 0004、里程碑与实施计划、`CONTEXT.md` 术语「3D 模型生成工具」与「3D 生成定价」);前端实现已在同日落地,见下一条。后端 `model3d` 定价段仍未写入运行环境配置,入口上线前必须先把数值落到配置。
- 关联文档:[ADR 0004](../../adr/【ADR】0004-3D生成入口价格与幂等身份-2026-09-21.md)、[里程碑 Tripo生成前端入口](../plans/【里程碑】Tripo生成前端入口-2026-09-21.md)、[实施计划 Tripo生成前端入口](../plans/【实施计划】Tripo生成前端入口-2026-09-21.md)、[技术方案 Tripo 3D生成API集成](../../technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md)。
## 2026-09-21 3D 生成入口实现落地:新代码收在 `model3d-generation/` 深目录,幂等键按内容定址
- 背景:按上一条决策实现画布侧 3D 生成入口。既要沿用既有生成链路(占位框、入队、任务侧栏、终态回填),又要求新代码不混进画布那些千行大文件。
- 决策:新代码全部落在 `src/components/image-editor/model3d-generation/``Model3dGenerationFormModel` / `Model3dGenerationSubmission` / `useModel3dGenerationTask` / `Model3dGenerationForm` / `Model3dGenerationModal`,测试文件与被测文件同名、不带 `ImageCanvas` 前缀),画布只做工具栏入口、子选项菜单、对话框 mode、定价读取与请求客户端的接线。`Idempotency-Key` 的落地形态是「尝试代次 + 请求内容指纹」,而不是「在每处参数变更里重铸键」——后者要覆盖参考图的上传 / 画布点选 / 素材库三条路径,漏一处就会出现同键换内容的 409。图生 3D 的参考图上限收口成 `usesSingleImageReferenceSlot`,单槽位下素材库选择是替换而不是追加。
- 边界:不改后端、不改 schema、不新增公开 API;不动素材库落点、后台 renderer、精选 read model、公开 grant、External v1 与画布 Agent 对话;移动端画布不承诺(跟随现状)。
- 验证:`src/components/image-editor/model3d-generation` 四个测试文件 38 个用例通过;`ImageCanvasGenerationModel` / `ImageCanvasGenerationDialogModel` / `ImageCanvasUploadModel` / `projectAssetReferencePickerModel` 的定向用例通过;`npm run typecheck``npx eslint``model3d-generation` 与接线文件)、`npm run check:encoding``node scripts/check-doc-index.mjs``git diff --check` 通过。真实扣费与排队回填需在写入 `model3d` 定价段的联调环境验证。
- 关联文档:[实施计划 Tripo生成前端入口](../plans/【实施计划】Tripo生成前端入口-2026-09-21.md)、[ADR 0004](../../adr/【ADR】0004-3D生成入口价格与幂等身份-2026-09-21.md)、[技术方案 Tripo 3D生成API集成](../../technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md)。
@@ -5718,3 +5718,35 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- **处理(现行口径)**:改 `spacetime-client` 的模块前先 `rg -n '#\\[path = "active/' server-rs/crates/spacetime-client/src/active.rs` 确认该模块的真实文件;只有 `active/` 下的文件参与编译。历史死副本 `src/external_generation.rs` 已在 `ac08ccbe3` 删除,不要再以「两边都改一遍」的方式维持同步。
- **验证**`cargo check -p spacetime-client``cargo check -p api-server` 同时通过,且 `rg --files server-rs/crates/spacetime-client/src` 中同名文件只剩一份。
- **关联**`server-rs/crates/spacetime-client/src/active.rs``server-rs/crates/spacetime-client/src/active/external_generation.rs``server-rs/crates/spacetime-client/src/active/mapper.rs`
## 2026-09-21 「`objectKey` 非空就覆盖 `imageSrc`」的投影会把模型文件当图片
- **风险**3D 资源(`assetKind=model3d`)的 `objectKey` 是模型本体、`imageSrc` 是预览图,但后端历史投影不分类别地用 `objectKey` 覆盖 `imageSrc`,于是画布卡片、素材缩略图、后台列表会一起去加载 `.glb`
- **处理(现行口径)**:客户端可见媒体投影按 `asset_kind` 分叉(`model3d` 只投影预览图,模型本体只留 `objectKey`),模型格式的权威是写入前按字节魔数识别出的 `content_type`,扩展名由它派生。画布图层没有内容类型字段,所以画布只按「`assetKind=model3d``objectKey` 非空」放行,格式判定交给查看器包(声明 `Content-Type` → 字节魔数 → 地址扩展名,判不出来即 `unsupported-format` 并展示原因);下载侧用真实响应头 MIME、对象键扩展名兜底。
- **易错点**:① 不要为了「预览缺失也能开」把模型文件喂给图片元素,预览缺失应显示占位与错误信息;② 不要新增 `model_format` 列,也不要在画布 / 素材库各抄一份格式清单或在画布侧按扩展名预判放行,两处真相一定会分叉;③ `EDITOR_CANVAS_ASSET_KINDS` 与前端 `CANVAS_ASSET_KIND_TAG_OPTIONS` 必须同步,任一侧多出或少一份都算接入未完成;④ 不要在 `assetKind` 为空时按扩展名兜底(后端投影缺陷会被掩盖),也不要直接照抄 AGC 画布那两份旧判断。
- **关联**`server-rs/crates/api-server/src/editor_project.rs``server-rs/crates/api-server/src/tripo3d/storage.rs``src/components/image-editor/model3d-preview/`
## 2026-09-21 复用旧 `Idempotency-Key` 的「重试」不会重跑 3D 生成
- **现象**3D 生成失败后,用同一次请求(同 `Idempotency-Key` + 同 payload)再提交一次,接口返回 accepted 与**同一个** `operationId`,但任务永远停在原来的失败终态,用户以为「已经重试过了」。
- **成因**:两个 Tripo submit 端点把 `Idempotency-Key` 当作唯一幂等身份(dedupe key = sha256(owner + job_kind + key)),同键同 payload 直接返回既有 operation,同键不同 payload 才 409;而 Tripo job 固定 `max_attempts = 1`,失败即终态。因此这个键上不存在「再跑一次」的可能。
- **处理(现行口径)**:用户主动重试必须铸造新的 `Idempotency-Key`(画布侧由前端生成并绑定当次请求内容,改参数即视为新请求),不要把它做成「复用同键的可重试」。
- **实现口径**:画布侧的键是「尝试代次 + 请求内容指纹」两段拼成(`resolveModel3dRequestKey`)。不要改成「只在档位 / 开关 / 提示词的 onChange 里重铸键」——参考图还有上传、画布点选、素材库选择三条路径,漏一条就会出现同键换内容的 409;也不要去掉代次,那样用户点「重试」会回到原来那个终态 operation。
- **验证**`cargo test -p api-server` 的 tripo3d 队列用例;前端 `model3d-generation` 的重试用例断言两次提交的键不同。
- **关联**`server-rs/crates/api-server/src/tripo3d/queue.rs``server-rs/crates/api-server/src/editor_generation_queue.rs``src/components/image-editor/model3d-generation/`
## 2026-09-21 3D 生成价格不能照抄图片模型的「前端内置兜底」
- **风险**:图片 / 视频 / 音频一族在前端有内置兜底定价(`EDITOR_MODEL_MUD_POINT_CONFIG`),定价接口异常时价格照常显示。3D 入口若照抄这条习惯,会在 `model3d` 段缺失时继续展示一个价格,而后端是 fail closed503 `model3d-pricing-unavailable`,不扣费不入队),用户看到「面板有价、提交被拒」。
- **处理(现行口径)**3D 价格只读 `GET /api/editor/generation-pricing``model3d` 段;该段缺失即入口不可提交且不发请求,既不写死数值也不按 0 元放行。前端只保留「档位 → 六个必填参数」的映射与加和逻辑,所有数值来自查询。
- **易错点**:① 缺价时不要「先发请求让后端兜底」,那会把 503 变成用户可见的失败;② 不要拿图片模型的 `prices` 给 3D 兜底;③ 前端这份加和逻辑与后端 `model3d_add_ons` 是两份实现,必须用同一组参数组合的用例对齐。
- **关联**`docs/adr/【ADR】0004-3D生成入口价格与幂等身份-2026-09-21.md``src/services/image-editor/editorProjectClient.ts``src/components/image-editor/ImageCanvasGenerationModel.ts`
## 2026-09-21 画布新生成工具漏在「参考图来源」的 mode 列表外,表现为点了菜单没反应
- **现象**:新工具面板里的参考图来源菜单(从画布中选择 / 上传图片 / 从项目素材中选择)点下去没有任何反应,既不报错也不进参考图;`ImageCanvasGenerationSurface` 的入口与子选项菜单却一切正常。
- **成因**:参考图的三条写入路径都按**显式 mode 列表**分流,而不是按「谁的对话框打开了」:画布点选走 `ImageCanvasGenerationDialogModel.appendGenerationReference`,上传走 `ImageCanvasUploadModel.applyGenerationReferenceUpload``target === 'generation-reference'`),素材库走 `ImageCanvasGenerationDialogModel.replaceProjectAssetPickerReferences`。三处各自维护一份 `dialog.mode === 'generate' || 'scene' || ...` 列表,新 mode 不在列表里就被安静地原样返回。
- **处理(现行口径)**:新增画布生成工具时,除工具栏与子选项菜单外,必须把新 mode 同步进这三处,并在 `resolveDialogExtraImageReferenceLimit` 给出参考图上限;单图槽位(如 `model3d-image-to-model`)用 `usesSingleImageReferenceSlot` 标注,素材库选择按替换而不是追加处理。改完必须有用例覆盖「点选 / 上传 / 素材库各进一次参考图字段」。
- **易错点**:① 不要只在面板里 `console.warn` 或靠 UI 文案兜底,这类遗漏在自动检查里是静默的;② 单槽位工具不要让「画布点选 vs 素材库选择」互相挤占——留着旧的会让用户这次的选择看起来没生效;③ 参考图上限不要沿用图片生成的默认值(图生 3D 只接受一张)。
- **验证**`src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts``src/components/image-editor/ImageCanvasUploadModel.test.ts``src/components/image-editor/projectAssetReferencePickerModel.test.ts``src/components/image-editor/ImageCanvasGenerationModel.test.ts` 中针对 `model3d-image-to-model` 的用例。
- **关联**`src/components/image-editor/ImageCanvasGenerationDialogModel.ts``src/components/image-editor/ImageCanvasUploadModel.ts``src/components/image-editor/ImageCanvasGenerationModel.ts``docs/project-memory/plans/【实施计划】Tripo生成前端入口-2026-09-21.md`
@@ -13,7 +13,7 @@
- 不接 multiview-to-model、Gaussian Splat、rig、animation、texture、convert 等其它 Tripo 能力。
- 不进入 `/api/external/v1`,不改 External OpenAPI,不新增外部 scope 或 API Key 语义。
- 不复用、不修改、不删除 `platform-hyper3d` 及其 `/api/assets/hyper3d/*` 路由与契约;本能力是全新 API,不是 Hyper3D 的 provider 替换。
- 不接前端与 `packages/model3d-viewer`
- 本后端里程碑不接前端与 `packages/model3d-viewer`;前端入口与结果预览分别由 [`【里程碑】Tripo生成前端入口-2026-09-21.md`](../project-memory/plans/【里程碑】Tripo生成前端入口-2026-09-21.md) 与 [`【实施计划】Tripo生成结果前端预览接入-2026-09-21.md`](../project-memory/plans/【实施计划】Tripo生成结果前端预览接入-2026-09-21.md) 承接
- 不提供取消能力:客户端停止查询,worker 继续推进到 provider 终态。
- 本期不改 `platform-oss` 为流式或分片上传,沿用现有完整字节写入路径;流式上传作为后续 TODO。
@@ -153,26 +153,26 @@ thumbnailSrc = 预览图
width/height = 预览图像素尺寸
```
模型格式与体积由 `AssetObject` 承担(`content_type``content_length``content_hash`),因此不新增 `model_format``size_bytes``poly_count` 等列。
模型格式与体积由 `AssetObject` 承担(`content_type``content_length``content_hash`),因此不新增 `model_format``size_bytes``poly_count` 等列。`content_type` 在写入前按字节魔数识别真实产物(GLB 的 `glTF` 头、FBX 的 `Kaydara FBX Binary` 头、glTF 的 JSON 正文),不直接采信 provider 返回或下载响应头声明的类型;对象键扩展名由该内容类型派生。客户端不复制这份格式清单:画布只按「`assetKind=model3d``objectKey` 非空」放行,能否交给 3D 查看器由 `packages/model3d-viewer` 判定,判据依次是读接口声明的 `Content-Type`、字节魔数、地址扩展名,全判不出来即按 `unsupported-format` 报错。
`external_generation_job.phase` 的取值集合不变,仍只允许现有两种执行阶段;阶段文案由 api-server 映射,不扩展 schema 常量。
## assetKind 接入清单对照
对照 [`【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`](../【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md) 的「新增编辑器 `assetKind` 接入清单」,`model3d` 是**后端-only 类别**:本期不接编辑器前端,只对实际经过的链路做决策,其余行显式登记为不进入
对照 [`【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`](../【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md) 的「新增编辑器 `assetKind` 接入清单」,`model3d` 现已接图片画布的前端预览入口(工具条「3D 预览」+ 查看器模态,实施计划见 [`【实施计划】Tripo生成结果前端预览接入-2026-09-21.md`](../project-memory/plans/【实施计划】Tripo生成结果前端预览接入-2026-09-21.md)),但除画布预览外的前端面与后台面仍未进入。下表逐行登记本期口径
| 清单触碰点 | 本期口径 |
| --- | --- |
| 资源 / 素材 `assetKind` 与 upsert 参数 | `assetKind = "model3d"` 由 worker 在服务端写入,调用方无法指定;Rust 侧 `asset_kind` 是自由字符串,不动共享契约的闭集枚举 |
| schema 与 migration | 不新增类别专属列,复用 `imageSrc` / `thumbnailSrc` / `AssetObject` 元数据;无 `model3d` 存量数据,不写数据迁移 |
| 原子落库 `operation_kind` 白名单 | **必须**把两个 Tripo job kind 加入 `EDITOR_GENERATION_OPERATION_KINDS`,否则 `persist_editor_generation_result_and_return` 整笔事务被拒;这是本期唯一必须改的白名单 |
| 画布回填 | `projectResource``canvasCompletion` 只走 placement-only`build_editor_canvas_resource_placement_item`),layer 只拿 `layerId` / `resourceId``assetKind``src``objectKey` 全部归资源所有;因此 `model3d` 不进 `EDITOR_CANVAS_ASSET_KINDS` |
| 用户标签覆盖资格 | 允许。该白名单必须与前端 `CANVAS_ASSET_KIND_TAG_OPTIONS` 同步,本期无前端入口 |
| 快速编辑资格 | 不允许。3D 结果没有图片快速编辑语义,不进入快速编辑正向白名单 |
| “改造” capability 与 V2 配方 | 不进入改造 allowlist,不定义 V2 `action` / `fields[].id` / `references[].id`,不发 `canvasCompletion` 之外的可重放配方 |
| 画布回填 | `projectResource``canvasCompletion` 只走 placement-only`build_editor_canvas_resource_placement_item`),layer 只拿 `layerId` / `resourceId``assetKind``src``objectKey` 全部归资源所有;`model3d` 已按「白名单与前端 `CANVAS_ASSET_KIND_TAG_OPTIONS` 同步」的口径加入 `EDITOR_CANVAS_ASSET_KINDS` |
| 用户标签覆盖资格 | 允许,但只在 `model3d` 与空类别之间成立:3D 资源自成一族媒体语义,图片族不能覆盖成 3D,反之亦然 |
| 快速编辑资格 | 不允许。3D 结果没有图片快速编辑语义,不进入快速编辑正向白名单;画布上「3D 预览」是只读查看能力,不是快速编辑 |
| “改造” capability 与 V2 配方 | 不进入改造 allowlist,不定义 V2 `action` / `fields[].id` / `references[].id`,不发 `canvasCompletion` 之外的可重放配方;画布对 `model3d` 关闭改造入口 |
| 素材库 / 后台 / 精选 read model / 公开 grant / External OpenAPI | 本期不改。素材库与后台沿用现有按 `assetKind` 派生 renderer 的行为,`model3d` 未接 renderer 前只保证字段正确,不承诺展示;公开 grant 与 External v1 均不涉及 |
清单第 3 条要求的“用户标签覆盖”与“快速编辑”两项资格评估,以及 capability 决策,结论均为**不进入**,理由是本期为后端-only 集成、无前端入口、3D 无 quick-edit 语义。后续接前端时,四项都必须重新评估并单独补齐前端白名单与 renderer。
清单第 3 条要求的“用户标签覆盖”与“快速编辑”两项资格评估互不推导:标签覆盖进入(限 `model3d` 与空类别互转),快速编辑与改造均不进入,理由是 3D 资源没有像素级编辑语义。画布入口只新增只读的「3D 预览」capability 位,由宿主按 `supportedActions` 声明可用;素材库行、后台 renderer 与精选 read model 仍未接,后续进入时必须单独补齐 renderer 与验收
## 兼容与迁移
@@ -191,11 +191,14 @@ 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` 与实际对象一致。
11. 真实 Provider smoke 覆盖 text-to-model 与 image-to-model 各一次,下载字节数与 `Content-Length` 一致
12. 定向测试、`cargo check``npm run check:spacetime-schema``npm run check:encoding``node scripts/check-doc-index.mjs``git diff --check` 全部通过
10. `assetLibrary` 分支产出 `assetId``projectResource` 分支产出 `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` 一致。
14. 定向测试、`cargo check``npm run check:spacetime-schema``npm run check:encoding``node scripts/check-doc-index.mjs``git diff --check` 全部通过。
## 未决问题
- add-on 价格目前是跨模型统一一份;若 Tripo 后续对某个模型版本差异化加价,再补“按模型版本覆盖”一层,现在不预留。
- 前端与 `packages/model3d-viewer` 的接入另行安排,不在本规范范围
- 3D 模型的多文件 glTF 兄弟资源(`.bin`、贴图)、多视角与模型信息展示仍未安排;画布侧接入见 [`【实施计划】Tripo生成结果前端预览接入-2026-09-21.md`](../project-memory/plans/【实施计划】Tripo生成结果前端预览接入-2026-09-21.md)
- AGC 项目画布自带两份格式判断(`resourceModelScene.ts``isResourceModelFbx``octet-stream` 当 FBX、`resourceCardPreviewModel.ts` 的资源卡预览按扩展名正则选解析器),本期不动,后续统一收敛到 `packages/model3d-viewer`;素材库行、后台 renderer、精选 read model 与公开 grant 的 3D 展示同样未进入。
@@ -498,7 +498,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
- `assetKind` 是数据库、Rust DTO 和对外 JSON 的可选语义类别真相;普通静态图片必须为空。不要新增或返回并列的 `mediaType`;前端 renderer 可以保留内部派生媒体类型,但不得回写后端。
- 每次新增 `assetKind` 都必须分别完成“用户标签覆盖”和“快速编辑”两项资格评估;两者互不推导。可被用户选择不等于可快速编辑,属于图片媒体族也不等于自动进入快速编辑正向白名单。
- `assetKind` 只描述素材类别,不代表生成配方可执行或允许“改造”;新增类别必须单独完成上表的 capability 决策和验收。
- 不走编辑器前端的后端生成类别(当前只有 Tripo 3D 的 `model3d`同样适用本清单:四项资格评估、原子落库 `operation_kind` 白名单和画布回填口径都必须在对应技术方案里显式登记,未登记即视为未接入,不能靠“前端还没入口”跳过。生成类结果落库前 `EDITOR_GENERATION_OPERATION_KINDS` 必须已包含该 job kind,否则整笔事务被拒;画布回填只允许走 placement-only 口径(`build_editor_canvas_resource_placement_item`,不得把后端-only 类别写进 `EDITOR_CANVAS_ASSET_KINDS`,因为该白名单必须与前端 `CANVAS_ASSET_KIND_TAG_OPTIONS` 同步。
- 编辑器前端暂无入口的后端生成类别同样适用本清单:四项资格评估、原子落库 `operation_kind` 白名单和画布回填口径都必须在对应技术方案里显式登记,未登记即视为未接入,不能靠“前端还没入口”跳过。生成类结果落库前 `EDITOR_GENERATION_OPERATION_KINDS` 必须已包含该 job kind,否则整笔事务被拒;画布回填只允许走 placement-only 口径(`build_editor_canvas_resource_placement_item``EDITOR_CANVAS_ASSET_KINDS` 必须与前端 `CANVAS_ASSET_KIND_TAG_OPTIONS` 同步:前端已有标签入口而白名单缺项、或白名单多出前端不认的类别,都算接入未完成。Tripo 3D 的 `model3d` 已按该口径进入两端(见 `docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md`),其素材库行与后台 renderer 仍未进入
- 只是新增分类或 renderer 且现有媒体字段足够时,不改 schema。只有必须跨刷新、复用、审核或公开保留的数据才新增类别专属字段。
- legacy 数据必须先通过有界、可审计、带 dry-run/hash/apply 门禁的数据库迁移收口;迁移后的 api-server、mapper、主站、后台和画布只读取正式字段,不保留运行时 fallback。