@@ -2,9 +2,9 @@
## 1. 范围
本方案用于改造 `生成抓大鹅草稿` 的首版生成链路:点击按钮后先进入独立生成过程页,生成结束后自动进入抓大鹅草稿 页,并在草稿 页 `3D素材` Tab 预览本次生成的 3 D 模型 。
本方案用于改造 `生成抓大鹅草稿` 的首版生成链路:点击按钮后先进入独立生成过程页,生成结束后自动进入抓大鹅结果 页,并在结果 页 `素材配置 > 物品` 预览本次生成的 2 D 多视角物品素材 。
本次只把任意难度都收敛为 `3` 件物品。后续难度曲线恢复时,再把物品数、网格数和手动 3D 任务数量从配置中放开 。
草稿生成不再调用 Hyper3D Rodin, 也不再生成 GLB 模型。物品素材继续沿用原来的“生成图片 -> 网格拆分 -> 上传 OSS -> 写回草稿”机制,但每个物品必须生成 `5` 个不同视角的 2D 视图。试玩和正式运行态的消除次数、总物品数和物品种类数以结果页 `难度配置` 保存的难度为准。难度对应物品种类固定为:轻松 `3` 种、标准 `9` 种、进阶 `15` 种、硬核 `21` 种。历史硬核草稿若仍保存 `clearCount = 20` ,运行态按新硬核升为 `21` 次消除、`63` 件总物品。正式发布前如果已生成 `image_ready` 物品种类不足当前难度要求,必须阻断发布;试玩不阻断,但启动时把物品种类自动降到当前可用 2D 素材数量 。
## 2. 前端流程
@@ -20,34 +20,36 @@
生成页步骤固定为:
``` text
生成游戏名称 -> 生成物品名称 -> 生成 素材图 -> 切割独立 图片 -> 上传图片资产 -> 生成3D模型 -> 写入草稿页
生成游戏名称 -> 生成物品名称与背景音乐名称 -> 生成背景提示词 -> 分批生成1K 素材图 -> 切割五视角 图片 -> 上传图片资产 -> 生成背景音乐 -> 生成背景图 -> 写入草稿页
```
生成页只展示题材和物品数量,不展示玩法规则说明。
当前 `match3d-generating` 进度页不是后端 task 状态订阅页,而是一个覆盖 `match3d_compile_draft` 长 action 的本地时间进度页:前端每 500ms 以本地时间刷新阶段展示,真正的生成完成仍以 action 返回为准。为避免长 action 未返回时页面完全无感,生成页在 `match3d_compile_draft` 执行期间每 3 秒旁路读取一次 session 和 work detail, 并用 profile 中已写回的 `generatedItemAssets` 更新 `生成3D模型` 的完成数量。Hyper3D 控制台中看到 3 个 Rodin 任务已经 `Done` 后,页面仍可能继续停留在 `生成3D模型` ,此时通常表示后端还在等待下载列表、下载 GLB、转存 OSS 或写回 `generated_item_assets_json` ; 若 `generatedItemAssets` 已出现 `model _ready` ,前端应逐步显示完成数量。排查时应看 api-server 日志中的 `抓大鹅 Rodin 状态轮询返回` 、`抓大鹅 Rodin 下载列表轮询返回` 、`抓大鹅 Rodin GLB 下载完成` 和 `抓大鹅 Rodin GLB 转存 OSS 完成` 。
当前 `match3d-generating` 进度页不是后端 task 状态订阅页,而是一个覆盖 `match3d_compile_draft` 长 action 的本地时间进度页:前端每 500ms 以本地时间刷新阶段展示,真正的生成完成仍以 action 返回为准。为避免长 action 未返回时页面完全无感,生成页在 `match3d_compile_draft` 执行期间每 3 秒旁路读取一次 session 和 work detail, 并用 profile 中已写回的 `generatedItemAssets` 更新图片素材完成数量。 若 `generatedItemAssets` 已出现 `image _ready` 且带 `imageViews` ,前端应逐步显示完成数量。
## 3. 后端编排边界
外部模型 和 OSS 上传全部由 `api-server` 编排,不进入 SpacetimeDB reducer。SpacetimeDB 继续只负责 Match3D 会话、草稿和作品 profile 的确定性写入。
外部生图、音频生成 和 OSS 上传全部由 `api-server` 编排,不进入 SpacetimeDB reducer。SpacetimeDB 继续只负责 Match3D 会话、草稿和作品 profile 的确定性写入。
`match3d_compile_draft` action 的后端顺序为:
1. 读取 session config。
2. 将本次 MVP 的 `clearCount` 固定为 `3 ` ,并同步用于草稿编译 。
3. 先调用 SpacetimeDB compile procedure 写入草稿。首次执行使用新 `profileId` ;重试时复用 session draft / work profile 中已有 `profileId` 。这一步不能等待 LLM、图片、OSS 或 Rodin 成功后才执行。
4. 基于入口页题材设定文本调用文本模型生成作品元信息 。模型固定请求 `gpt-4o` ,只返回 JSON, 其中 `gameName` 为 4 到 12 个中文字符的游戏名称,`tags` 为 3 到 6 个中文短标签;`summary` 首版必须保持空字符串,结果页 `作品描述` 默认留给用户填写。文本模型不可用时保留第 3 步的本地兜底,不阻断草稿。
5. 调用文本模型生成 `3` 个题材下的短物品名称 。
6. 调用项目当前图片链路 VectorEngine `gpt-image-2-all` 生成一张 `1:1` 素材图,提示词必须合入入口页选择的 `assetStylePrompt` 。历史 `nanobanana2` 图片选项当前按项目统一决策回落到 VectorEngine, 不重新接入 APIMart 图片网关。
7. 将素材图按 `n*n` 网格切割成独立图片。当前 `3 ` 件 物品使用 `2*2` 网格,取前 `3 ` 格 。
8. 将素材图和每张独立图片上传到 OSS, 其中独立图片作为草稿页素材预览和 Rodin 图生模型参考图;每次获得可恢复的图片资产后,都要回写 `match3d_work_profile.generated_item_assets_json ` 。
9. 使用每张独立图片作为参考图,并行调用 Hyper3D Rodin 图生模型;所有 3D 模型任务必须在同一阶段同时提交、同时轮询状态、同时下载并转存 OSS, 禁止逐个物品串行等待模型完成。每个任务按官方 `check-status_reset_v` / `download-results_reset_v` 文档轮询状态和下载:状态查询使用 `subscription_key` ,整体完成态以 `jobs[]` 聚合为准;下载查询使用生成响应顶层 `uuid` 作为 `task_uuid` ,不能使用 `jobs.uuids` 子任务 uuid。只有 `jobs` 全部进入 `Done` 才能视为任务完成,任一 job `Failed` 则失败。完成后选择 `.glb` 下载文件,并把 GLB 转存到 OSS。Rodin 的 `subscriptionKey` 是上游 opaque token, 不做 256 字符这类短文本长度限制。Rodin 任务状态进入完成态后,下载列表仍可能延迟发布;后端必须对下载列表继续轮询,并兼容 `url` 、`downloadUrl` 、`fileUrl ` 、 `signedUrl` 等下载字段别名,只有预览图而没有模型文件时不能伪装成 GLB 成功 。
10. Rodin 每批完成后继续回写 `generated_item_assets_json` 。成功素材状态为 `model_ready` ;失败素材保留图片引用并记录 `error` ,下次 `match3d_compile_draft` 只继续缺失模型的素材,不重复生成已完成的 GLB 。
11. 在 HTTP 返回的 draft/profile DTO 中附带本次生成的素材资产预览信息;后续重进草稿页时从 work profile 的持久化 `generatedItemAssets` 恢复同一批素材 。
2. 草稿编译先创建可恢复 profile; 素材生成数量由入口页难度派生的物品种类决定: 轻松 `3` 种、标准 `9 ` 种、进阶 `15` 种、硬核 `21` 种 。
3. 先调用 SpacetimeDB compile procedure 写入草稿。首次执行使用新 `profileId` ;重试时复用 session draft / work profile 中已有 `profileId` 。这一步不能等待 LLM、图片、音频或 OSS 成功后才执行。
4. 基于入口页题材设定文本调用文本模型生成作品生成计划 。模型固定请求 `gpt-4o` ,只返回 JSON, 其中 `gameName` 为 4 到 12 个中文字符的游戏名称,`tags` 为 3 到 6 个中文短标签;`summary` 首版必须保持空字符串,结果页 `作品描述` 默认留给用户填写。生成计划还必须包含 `backgroundMusic.title` 、`backgroundMusic.style` 、`backgroundMusic.prompt` 、`backgroundPrompt` ,以及 `items[]` 中每个物品的 `name` 与 `soundPrompt` 。`backgroundMusic.title` 是背景音乐名称,`backgroundMusic.prompt` 固定为空字符串,用于后续 Suno 纯音乐生成;`backgroundPrompt` 用于生成局内竖屏背景图,必须描述绿色纵向背景与居中浅锅/圆盘状竞技区融合为一张完整背景图,且不包含 UI、文字、按钮、倒计时或物品。 文本模型不可用时保留第 3 步的本地兜底,不阻断草稿。
5. 后端从同一份作品生成计划读取当前难度所需数量的短物品名称和音效提示词;不得再只生成物品名称而丢失后续音效生成上下文 。
6. 调用项目当前图片链路 VectorEngine `gpt-image-2-all` 生成 `1:1` 、`1024x1024` 素材图,提示词必须合入入口页选择的 `assetStylePrompt` 。历史 `nanobanana2` 图片选项当前按项目统一决策回落到 VectorEngine, 不重新接入 APIMart 图片网关。
7. 每个物品固定需要 `5` 个不同视角。单张素材图最多切成 `5*5 = 25` 格;因此单张图最多承载 `5 ` 个 物品。若草稿物品数超过 `5` ,后端按每批最多 `5 ` 个物品自动分批,多张素材图并行生成 。
8. 将每张 素材图按 `n*n` 网格切割成独立图片,并按物品顺序连续分配 `5` 张视角图。每个物品 JSON 写入 `i mageViews[]` ,同时把第一个视角兼容写入 `imageSrc/imageObjectKey ` 。
9. 将素材图和每张独立视角图片上传到 OSS。每次获得可恢复的图片资产后, 都要回写 `match3d_work_profile.generated_item_assets_json` 。成功素材状态为 `image_ready` ;失败素材保留已成功图片引用并记录 `error` 。每个素材 JSON 同步保存 `soundPrompt` ,首个素材 JSON 同步保存 `backgroundMusicTitle` 与 `backgroundMusicStyle ` , `backgroundMusicPrompt` 保存为空字符串作为兼容字段 。
10. 后端在图片素材生成后使用 `backgroundMusic.title` 提交 Suno 背景音乐任务,`prompt` 为空,`tags` 来自 `backgroundMusic.style` ,并固定走纯音乐生成。轮询完成后通过通用创作音频资产链路转存 OSS、确认 `asset_object` 、绑定到 `match3d_work/background_music` ,再写回首个素材的 `backgroundMusic` 。音乐生成失败只记录 warning, 不阻断草稿页进入, 用户可在结果页 `素材配置 > 背景音乐` 重试 。
11. 若入口页 `generateClickSound=true` ,后端在图片素材生成后继续为缺少 `clickSound` 的已生成物品并行提交 Vidu 点击音效任务,轮询完成后通过通用创作音频资产链路转存 OSS、确认 `asset_object` 、绑定实体并写回对应素材的 `clickSound` ;若开关关闭则只保存 `soundPrompt` ,不调用音频生成 。
12. 背景图生成同样由 `api-server` 调用 VectorEngine `gpt-image-2-all` ,尺寸固定为 `9:16` ,并固定传入 `public/match3d-background-references/pot-fused-reference.png` 作为参考图。参考图只表达抓大鹅绿色页面背景和锅状圆形竞技区的融合构图,不包含 HUD、物品、文字或按钮。生成后的背景图上传到 `generated-match3d-assets/{sessionId}/{profileId}/background/{taskId}/background.png` ,并作为 `backgroundAsset` 挂在首个 `generatedItemAssets[]` JSON 上; HTTP DTO 同时顶层输出 `backgroundPrompt` 、`backgroundImageSrc` 、`backgroundImageObjectKey` 与 `generatedBackgroundAsset` 。
13. 在 HTTP 返回的 draft/profile DTO 中附带本次生成的素材资产预览信息、背景音乐资产信息和背景资产信息;后续重进草稿页时从 work profile 的持久化 `generatedItemAssets` 恢复同一批素材、音乐与背景。
若文本模型不可用或返回无法解析,后端必须降级为 `{themeText}抓大鹅` 与本地标签兜底,不阻断素材生成;但描述仍保持空字符串。
草稿生成阶段会 调用 Hyper3D Rodin 并等待 GLB 下载完成; 前端 `match3d_compile_draft` action 请求超时必须覆盖该长耗时链路,当前 Match3D client 使用 20 分钟超时。Rodin 单模型状态轮询预算为 10 分钟,下载列表发布轮询预算为 5 分钟; GLB 下载和 OSS PutObject 各自设置 3 分钟 HTTP 超时,避免上游下载或转存连接长期悬挂。由于 3 个模型并行生成,总耗时按最慢模型计算,不能按模型数量线性叠加。结果页 `3D素材` Tab 直接加载已生成模型;用户点击 `重新生成` 时再复用 Rodin 安全代理,首版重新生成只更新当前页面内预览状态,后续正式资产绑定以独立技术方案为准 。
草稿生成阶段不再 调用 Hyper3D Rodin,不生成 GLB, 也不等待任何模型轮询。 前端 `match3d_compile_draft` action 的长耗时主要来自文本生成、分批 1K 生图、切图、OSS 上传、背景图和可选音频生成。批量新增物品由 `POST /api/creation/match3d/works/{profileId}/item-assets` 复用同一套 2D 素材图生成、5x5 切图、OSS 上传和可选点击音效链路,只补齐本次新增物品并把 `imageViews[]` 写回 `generatedItemAssets` 。
## 4. 图片提示词
@@ -55,27 +57,35 @@
``` text
生成一张1:1图片
生成2*2 网格素材图
生成不超过5*5 网格素材图
整体画风遵循:...
只绘制这些物品:...
不要出现文字、水印、UI、边框
```
`包含若干个物品名称` 在落地中解释为“按生成出的物品名称绘制对应主体”,不要求图片上写出物品名称。这样可以避免文字渲染污染切图和后续手动 3D 模型参考 。
`包含若干个物品名称` 在落地中解释为“按生成出的物品名称绘制对应主体”,不要求图片上写出物品名称。这样可以避免文字渲染污染切图和局内 2D 素材表现 。
入口页内置风格参考图通过同一 VectorEngine `gpt-image-2-all` 能力生成,保存路径固定为:
入口页内置 2D 风格参考图通过同一 VectorEngine `gpt-image-2-all` 能力生成,执行命令为 `npm run assets:match3d-style-references -- --live` , 保存路径固定为:
``` text
public/match3d-style-references/c lay-toy .png
public/match3d-style-references/low-poly .png
public/match3d-style-references/toy-plastic .png
public/match3d-style-references/wood-carved .png
public/match3d-style-references/voxel-block .png
public/match3d-style-references/metal-mecha .png
public/match3d-style-references/f lat-icon .png
public/match3d-style-references/cel-cartoon .png
public/match3d-style-references/pixel-retro .png
public/match3d-style-references/watercolor .png
public/match3d-style-references/sticker-outline .png
public/match3d-style-references/painterly-icon .png
```
这些图片只作为入口页风格选择的视觉参考,不进入用户草稿资产,不替代生成时的物品素材图。
局内背景生成固定参考图路径为:
``` text
public/match3d-background-references/pot-fused-reference.png
```
这张图作为 VectorEngine `image` 参考输入使用,用来锁定“绿色竖屏背景 + 居中锅状竞技区”的构图。每次草稿生成仍会根据 `backgroundPrompt` 生成新的题材化背景图;参考图本身不作为运行态最终背景。
## 5. OSS 路径
新增 generated legacy prefix:
@@ -88,47 +98,56 @@ generated-match3d-assets
``` text
generated-match3d-assets/{sessionId}/{profileId}/material-sheet/{taskId}/sheet.png
generated-match3d-assets/{sessionId}/{profileId}/items/{itemSlug}/image/image .png
generated-match3d-assets/{sessionId}/{profileId}/items/{itemSlug}/model/{taskUuid}/model.glb
generated-match3d-assets/{sessionId}/{profileId}/items/{itemSlug}/views/view-01 .png
generated-match3d-assets/{sessionId}/{profileId}/items/{itemSlug}/views/view-02.png
generated-match3d-assets/{sessionId}/{profileId}/items/{itemSlug}/views/view-03.png
generated-match3d-assets/{sessionId}/{profileId}/items/{itemSlug}/views/view-04.png
generated-match3d-assets/{sessionId}/{profileId}/items/{itemSlug}/views/view-05.png
generated-match3d-assets/{sessionId}/{profileId}/background/{taskId}/background.png
```
`itemSlug` 必须带 `itemId` 前缀,例如 `match3d-item-1-item` 。中文物品名清洗后可能都退回 `item` ,不能只用物品名做路径,否则多张切割图会写到同一个 object key, 导致草稿页预览图全部一致。
HTTP DTO 同时返回 `imageSrc` 、`imageObjectKey` 、`modelSrc` 、`modelObjectKey` 、`modelFileName` 、`taskUuid` 、`subscriptionKey ` 和 `status` 。模型 生成成功后 `status = model _ready` ; 若后续允许部分模型失败降级,失败素材必须带 `error` ,且不能伪装成可预览模型。前端模型预览必须通过 `/api/assets/read-bytes` 读取私有 GLB 字节并转成 Blob URL 后交给 Three.js, 不直接请求裸 `/generated-match3d-assets/...` 路径 。
HTTP DTO 同时返回兼容字段 `imageSrc` 、`imageObjectKey` ,以及正式 2D 字段 `imageViews[]` 、`backgroundAsset ` 和 `status` 。图片素材 生成成功后 `status = image _ready` ; 背景生成成功后首个素材的 `backgroundAsset.status = image_ready` 。前端通过 `/api/assets/read-url` 将 generated legacy path 换签后加载私有图片,不直接请求裸 `/generated-match3d-assets/...` 路径。运行态背景图同样通过 `/api/assets/read-url` 换签后作为全屏 `object-cover` 背景加载 。
## 5.1 运行态模型 消费
## 5.1 运行态 2D 素材 消费
生成模型 不仅用于结果页预览,也必须进入游戏运行态。运行态入口的传递链路为:
生成的 2D 五视角素材 不仅用于结果页预览,也必须进入游戏运行态。运行态入口的传递链路为:
``` text
Match3DWorkProfile / PlatformMatch3DGalleryCard
-> Match3DRuntimeShell(generatedItemAssets)
-> Match3DRuntimeShell(generatedItemAssets, backgroundImageSrc )
-> Match3DPhysicsBoard / Match3DTrayPreviewBoard
```
`Match3DPhysicsBoard` 与 `Match3DTrayPreviewBoard` 按运行快照中的 `itemTypeId` 稳定排序后,把生成出的模型顺序映射到对应类型。当前 MVP 固定 `clearCount = 3` ,因此 `match3d-type-01/02/03` 分别对应生成列表的第 `1/2/3` 个模型;后续恢复更多物品生成时,后端必须继续保证 `generatedItemAssets` 顺序与类型编号一致 。
运行态按运行快照中的 `itemTypeId` 稳定排序后,把 `generatedItemAssets` 顺序映射到对应类型。加载某个物品实例时,从该类型素材的 `i mageViews[]` 中按实例 id 稳定随机选择一个视角;若历史数据没有 `imageViews[]` ,则回退到 `imageSrc/imageObjectKey` 。没有生成图片或图片加载失败时,继续使用默认积木图标兜底 。
运行态背景优先读取 `backgroundImageSrc` / `generatedBackgroundAsset.imageSrc` ,为空时从 `generatedItemAssets[].backgroundAsset.imageSrc/imageObjectKey` 兜底。`Match3DRuntimeShell` 只保留顶部返回、倒计时、重开三个控件;进度、组数、版本等状态信息不得再作为顶部常驻 UI 出现,避免遮挡生成背景和锅状竞技区。
前端加载规则:
1. 优先读取 `modelSrc ` ; 为空时使用 `model ObjectKey` 。
2. 通过 `readAssetBytes` 调用 `/api/assets/read-bytes` ,由同源后端读取 OSS 私有对象字节 。
3. 使用 Three.js `GLTFLoader.parseAsync` 解析 GLB 字节,并按物品类型缓存模板 。
4. 场内每个物品和备选栏预览都从模板 clone 独立对象,点击命中继续写入 `itemInstanceId` 。
5. 物理碰撞和边界仍沿用现有 `visualKey` 的程序化几何,生成 GLB 只替换视觉模型,不承接规则真相 。
6. 模型缺失、读取失败或 WebGL 回退时,继续使用默认积木素材,不能阻断开局、点击、入槽或结算;调试模式下需要输出加载失败的 `itemTypeId` 、模型来源和错误信息, 便于区分“资产没有传入”和“GLB 字节读取或解析失败”。
1. 优先读取 `imageViews[]` 中的 `imageSrc/imageObjectKey ` , 为空时使用兼容字段 `imageSrc/image ObjectKey` 。
2. 对 generated legacy path 通过同源 `/api/assets/read-url` 换签后交给浏览器图片加载 。
3. 场内物品、点击命中和备选栏继续使用后端快照中的 `itemInstanceId/itemTypeId/x/y/radius/layer` ;生成 2D 图片只替换视觉表现,不承接规则真相 。
4. 同一物品类型的多个实例可以展示不同视角,但同一实例在本局中应稳定使用同一个视角,避免移动或入槽时闪图 。
5. 图片缺失、读取失败或解码失败时,继续使用默认积木素材,不能阻断开局、点击、入槽或结算 。
结果页点击 `试玩` 时,前端必须把当前结果页可见的 `generatedItemAssets` 带入运行态启动入参。`PUT /api/runtime/match3d/works/{profileId}` 若因为并发或旧快照返回了缺少素材的 profile, `Match3DResultView` 需要把当前 draft / profile 的素材重新合并到运行态 profile, 并在启动试玩前调用生成素材保存接口把当前可见的 `generatedItemAssets` 写回作品 profile; 不能只在内存里把素材补到 `onStartTestRun(profile)` 。发布同理必须先落库当前素材,再调用 `publish_match3d_work` ,否则公开推荐流和正式运行态只能读到旧 profile 快照,历史草稿尤其容易表现为结果页有 3D 模型、正式游戏仍是默认积木 。若历史草稿同时存在旧 `draft.generatedItemAssets` 和较新的 `profile.generatedItemAssets` ,同 `itemId` 下以 profile 中已有的 `model Src` / `model ObjectKey` 补齐 draft, 不能让旧 draft 把模型状态覆盖回 `image_ready` 。`PlatformEntryFlowShellImpl` 在渲染 `match3d-runtime` 时按 `run.profileId` 优先使用当前 `match3dProfile.generatedItemAssets` ,只有 profileId 不匹配时才读取 `selectedPublicWorkDetail.generatedItemAssets` 。推荐流内嵌正式运行态也必须走同一解析器;当推荐卡片摘要缺少素材时,启动前补读 `getMatch3DWorkDetail(profileId)` ,把详情里的生成模型 写入 `match3dProfile` 后再传给运行态。这样可以避免从公开详情页残留状态或推荐卡片旧摘要进入试玩 / 正式游戏时,把已生成草稿的 3 D 模型 覆盖成空列表。
结果页点击 `试玩` 时,前端必须把当前结果页可见的 `generatedItemAssets` 带入运行态启动入参。`PUT /api/runtime/match3d/works/{profileId}` 若因为并发或旧快照返回了缺少素材的 profile, `Match3DResultView` 需要把当前 draft / profile 的素材重新合并到运行态 profile, 并在启动试玩前调用生成素材保存接口把当前可见的 `generatedItemAssets` 写回作品 profile; 不能只在内存里把素材补到 `onStartTestRun(profile)` 。发布同理必须先落库当前素材,再调用 `publish_match3d_work` ,否则公开推荐流和正式运行态只能读到旧 profile 快照。若历史草稿同时存在旧 `draft.generatedItemAssets` 和较新的 `profile.generatedItemAssets` ,同 `itemId` 下以 profile 中已有的 `imageViews[]` 、`image Src` 或 `image ObjectKey` 补齐 draft, 不能让旧 draft 把素材覆盖成空列表 。`PlatformEntryFlowShellImpl` 在渲染 `match3d-runtime` 时按 `run.profileId` 优先使用当前 `match3dProfile.generatedItemAssets` ,只有 profileId 不匹配时才读取 `selectedPublicWorkDetail.generatedItemAssets` 。推荐流内嵌正式运行态也必须走同一解析器;当推荐卡片摘要缺少素材时,启动前补读 `getMatch3DWorkDetail(profileId)` ,把详情里的生成图片素材 写入 `match3dProfile` 后再传给运行态。这样可以避免从公开详情页残留状态或推荐卡片旧摘要进入试玩 / 正式游戏时,把已生成草稿的 2 D 素材 覆盖成空列表。
历史草稿若仍保存 `status = model_ready` 、`modelSrc` 或 `modelObjectKey` ,仅作为旧版本兼容读取,不再参与新素材生产。历史外部模型链接转存接口只用于清理旧数据,不能被新草稿生成、批量新增或结果页普通编辑入口调用。
生成完成后自动进入试玩依赖 `selectionStageRef.current === 'match3d-generating'` 的同步判断。执行 `match3d_compile_draft` 前切到生成页时,必须同时写 `selectionStageRef.current = 'match3d-generating'` 和 `setSelectionStage('match3d-generating')` ;只调用 React state 会让 action 很快返回时读到旧 stage, 表现为生成页已经 100% 但不进入试玩或结果页。拼图、大鱼吃小鱼、方洞挑战等同类生成页也遵循同一规则。
## 6. 自动保存与草稿恢复
点击 `生成抓大鹅草稿` 后,草稿存档创建与素材生成解耦:
1. 首次 compile 必须先写 `match3d_work_profile` 草稿行, 即使后续卡在文本模型、图片生成、OSS 上传、Rodin 生成或下载转存 任意阶段。
1. 首次 compile 必须先写 `match3d_work_profile` 草稿行,即使后续卡在文本模型、图片生成、音频生成或 OSS 上传任意阶段。
2. 失败态前端要重新读取 session / work detail, 并刷新草稿作品架, 保证用户离开生成页后仍能在草稿 Tab 找到这份作品。
3. 重新生成时优先使用当前 session 的 `draft.profileId` 或 `publishedProfileId` ,不得重新创建 session; 后端读取同一 profile 的 `generated_item_assets_json` 后,只补齐缺失图片或缺失模型 的阶段。
4. 已有 `status = model _ready` 且带 `modelSrc ` / `model ObjectKey` 的素材视为完成,不再重复调用 Rodin 。
3. 重新生成时优先使用当前 session 的 `draft.profileId` 或 `publishedProfileId` ,不得重新创建 session; 后端读取同一 profile 的 `generated_item_assets_json` 后,只补齐缺失图片或缺失音频 的阶段。
4. 已有 `status = image _ready` 且带 `imageViews[] ` 或 `imageSrc/image ObjectKey` 的素材视为完成,不再重复生成图片 。
抓大鹅结果页的基础信息自动保存继续调用 `PUT /api/runtime/match3d/works/{profileId}` 更新名称、题材、描述、标签、封面、消除数和难度;该保存不得清空 `generated_item_assets_json` 。结果页 `3D素材` Tab 手动点击 `重新生成` 并拿到 GLB 下载文件 后,必须把当前素材草稿 重新序列化成 `generatedItemAssets` 并写回作品 profile; 否则页面内预览会显示新模型,但 试玩、发布和重进草稿仍 会读取旧的空模型 快照。SpacetimeDB `update_match3d_work` / `publish_match3d_work` 必须保留当前行的生成素材 JSON。
抓大鹅结果页的基础信息自动保存继续调用 `PUT /api/runtime/match3d/works/{profileId}` 更新名称、题材、描述、标签、封面、消除数和难度;该保存不得清空 `generated_item_assets_json` 。结果页 `素材配置 > 物品` 只在独立面板中预览和编辑当前素材,不再提供单项重新生成入口;删除单项或批量新增成功 后,都 必须把当前素材列表 重新序列化成 `generatedItemAssets` 并写回作品 profile, 否则试玩、发布和重进草稿会读取旧素材 快照。SpacetimeDB `update_match3d_work` / `publish_match3d_work` 必须保留当前行的生成素材 JSON。
草稿架重进路径为:
@@ -136,22 +155,56 @@ Match3DWorkProfile / PlatformMatch3DGalleryCard
草稿 Tab -> getMatch3DWorkDetail(profileId) -> Match3DResultView(profile.generatedItemAssets)
```
因此 `map_match3d_work_summary_response` / `map_match3d_work_profile_response` 需要从 work profile snapshot 反序列化 `generated_item_assets_json` 并输出 `generatedItemAssets` 。前端 `Match3DResultView` 的读取顺序为:有 `draft.generatedItemAssets` 时先用 draft 保留本次生成顺序和图片;同 `itemId` 在 `profile.generatedItemAssets` 中已有模型字段 时,用 profile 模型 字段补齐 draft; 从草稿架重进没有 draft 时,用 `profile.generatedItemAssets` ;两者都没有才回退到默认 3D 素材占位。
因此 `map_match3d_work_summary_response` / `map_match3d_work_profile_response` 需要从 work profile snapshot 反序列化 `generated_item_assets_json` 并输出 `generatedItemAssets` 与顶层背景字段 。前端 `Match3DResultView` 的读取顺序为:有 `draft.generatedItemAssets` 时先用 draft 保留本次生成顺序和图片;同 `itemId` 在 `profile.generatedItemAssets` 中已有 `imageViews[]` 或 `imageSrc/imageObjectKey` 时,用 profile 图片 字段补齐 draft; 背景资产同样必须从 profile 或 draft 的首个 `backgroundAsset` 保留到保存 payload; 从草稿架重进没有 draft 时,用 `profile.generatedItemAssets` ;两者都没有才回退到默认素材占位。
结果页 `作品信息` Tab 字段命名对齐拼图草稿:
1. `作品名称` 对应 Match3D `gameName` 。
2. `作品描述` 对应 Match3D `summary` ,草稿生成默认空。
3. `作品标签` 对应 Match3D `tags` ,可由 AI 首次生成并允许用户继续编辑。
4. 封面图与作品名称不再拆成左右两个大模块;封面只作为同一 Tab 内的可选上传 入口,避免和作品基础信息割裂。
4. 封面图与作品名称不再拆成左右两个大模块;封面只作为同一 Tab 内的可选入口,避免和作品基础信息割裂。点击封面图必须弹出独立编辑面板,不允许在当前作品信息面板下方展开。封面面板布局参考拼图创作页上传卡:移动端优先、左侧/上方为方形预览,右侧/下方为提示词与操作区。面板支持三类输入:本地上传图片、上传后开启 AI 重绘、直接引用 `物品素材` 或 `UI素材` 中已有图片作为封面或 AI 重绘参考图。AI 重绘通过 `api-server` 的 Match3D 作品封面生成接口调用 VectorEngine `gpt-image-2-all` ,生成结果转存到 `generated-match3d-assets/{sessionId}/{profileId}/cover/{taskId}/cover.png` 后再写回 `coverImageSrc` ;关闭 AI 重绘时只把选中的 Data URL 或 generated legacy path 写入封面字段。
`3D素材` 详情页只保留:
结果页 `难度配置` Tab 取代旧 `玩法配置` ,不再展示旧的分散输入项。该 Tab 必须与创作入口页使用同一组难度选项,并统一把原“类型素材图片 / 局内类型”等口径归一为 `物品种类` :
1. 模型预览区:优先加载 `modelSrc` 对应 GLB, 缺失时加载 `modelObjectKey` ,支持拖动旋转;没有模型时展示空预览。
| 难度 | clearCount | difficulty | 总物品数 | 物品种类 |
| ---- | ---------: | ---------: | -------: | -------: |
| 轻松 | 8 | 2 | 24 | 3 |
| 标准 | 12 | 4 | 36 | 9 |
| 进阶 | 16 | 6 | 48 | 15 |
| 硬核 | 21 | 8 | 63 | 21 |
预览区展示 `需要消除` 、`总物品数` 、`物品种类` 和 `已生成物品种类` 。历史草稿如果保存的是旧 `clearCount/difficulty` ,前端按 `clearCount` 精确命中优先、否则按 `difficulty` 就近归一到上述选项,并把归一后的数值保存回 profile。发布校验以 `generatedItemAssets[]` 中有 `imageViews[]` 或 `imageSrc/imageObjectKey` 的 `image_ready` 素材数量为准;试玩启动时用同一数量计算 `itemTypeCountOverride` ,不足时自动降低,不修改草稿难度配置本身。
结果页 `素材配置` Tab 取代旧一级素材入口,并包含三个子 Tab:
1. `物品` :显示 2D 物品素材列表、五视角预览、素材名称、点击音效提示词和点击音效生成入口。
2. `UI` :预览生成的竖屏游戏背景图,读取顺序为 draft 顶层背景、draft `generatedBackgroundAsset` 、profile 顶层背景、profile `generatedBackgroundAsset` 、`generatedItemAssets[].backgroundAsset` 、本地参考图兜底。该页必须展示默认画面描述提示词,默认值来自草稿生成计划的 `backgroundPrompt` 或持久化 `backgroundAsset.prompt` ;用户修改后点击重新生成,后端继续固定使用 `public/match3d-background-references/pot-fused-reference.png` 作为 VectorEngine `image` 参考图,并把新的 `backgroundAsset` 写回同一份 `generated_item_assets_json` 。UI 子 Tab 还必须提供独立的运行态 UI 预览面板,直接用当前背景图模拟抓大鹅竖屏页面的顶部返回、倒计时、重开控件、锅状竞技区和底部托盘,不在 Tab 下方内联展开。
3. `背景音乐` :承载原一级音乐 Tab 的背景音乐曲名、风格、生成进度和试听控件;背景音乐始终按纯音乐生成,前端不提供提示词输入。
旧一级 `音乐` Tab 删除;抓大鹅背景音乐入口只保留在 `素材配置 > 背景音乐` 。
`素材配置 > 物品` 详情页只保留:
1. 五视角预览区:优先展示 `imageViews[]` ,缺失时展示兼容字段 `imageSrc/imageObjectKey` 。
2. 素材名称输入。
3. `重新生成` 按钮 。
3. 可编辑的点击音效提示词输入 。
4. 点击音效生成入口。
详情页不再展示参考图、用途、提示词、文生/图生切换、状态查询、下载列表、taskUuid 或 subscriptionKey。
详情页不再展示参考图、用途、模型 提示词、文生/图生切换、状态查询、下载列表、taskUuid 或 subscriptionKey。
`物品素材` 列表项点击必须弹出独立预览面板,不允许在列表右侧或列表下方内联展示。预览面板只承担查看五视角图片、编辑素材名称、编辑点击音效提示词和生成点击音效;不再展示 `重新生成` 按钮。列表项自身支持单项删除,删除后立即把剩余 `generatedItemAssets` 写回作品 profile。批量新增通过列表顶部按钮打开独立面板, 面板内每个输入框只输入一个物品名称, `新增物品名称` 按钮追加一个输入框;提交后按输入框顺序清洗、去重并调用 Match3D 作品批量生图接口。生成进度同时显示在批量新增面板和 `素材配置 > 物品` 列表顶部, 面板可关闭, 后台生成继续推进, 不阻塞封面、音频等其他生成操作。后端复用草稿生成的素材图、切图、OSS 上传和可选点击音效流程,但仅作用于本次新增名称,不重新生成已有物品,不新增 SpacetimeDB 表,最终仍写回同一份 `generated_item_assets_json` 。
## 6.1 音频生成与扣费
抓大鹅结果页音频生成复用通用创作音频路由:
1. `素材配置 > 背景音乐` 默认读取首个 `generatedItemAssets[0].backgroundMusicTitle/backgroundMusicStyle` ,用户可继续编辑曲名和风格;`backgroundMusicPrompt` 保留为空字符串兼容旧 JSON, 生成请求固定传空 `prompt` 。
2. 物品点击音效默认读取对应 `generatedItemAssets[].soundPrompt` ,用户可在 `素材配置 > 物品` 详情面板内编辑。
3. 背景音乐与物品音效生成过程必须显示进度条;提交任务、等待生成、转存资产和完成分别推进到不同进度,不再只展示旋转图标。
4. 音频生成完成后立即展示浏览器原生 audio 控件,支持试听。
5. `POST /api/creation/audio/background-music/{task_id}/asset` 和 `POST /api/creation/audio/sound-effect/{task_id}/asset` 在真正拿到音频并转存资产前,由后端按 `taskId + 资产槽位` 幂等预扣 `10` 光点; 任务仍在处理中时不扣费。资产下载、OSS 转存或资产绑定失败时后端自动退款。前端只展示生成按钮和进度,不自行计算或写入钱包。
入口页 `生成音效` Toggle 复用同一扣费与资产绑定规则。默认关闭,关闭时草稿生成阶段不产生音频任务也不扣除音频光点;开启时每个首批物品的点击音效按单独任务和单独 `match3d_click_sound` 资产槽位扣费。音效生成失败不阻断草稿结果页进入,失败素材保留 `soundPrompt` ,用户可在结果页物品详情面板手动重试。
## 7. 验收
@@ -173,4 +226,4 @@ cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-module - -manifest -path server-rs \ Cargo . toml
```
真实草稿生成需要本地私密环境配置 `VECTOR_ENGINE_API_KEY` 、`HYPER3D_API_KEY` 和 OSS 访问变量。后端改动后使用 `npm run api-server` 启动,并检查 `/healthz` 。
真实草稿生成需要本地私密环境配置 `VECTOR_ENGINE_API_KEY` 和 OSS 访问变量;开启音频生成还需要对应音频上游配置 。后端改动后使用 `npm run api-server` 启动,并检查 `/healthz` 。