Merge remote-tracking branch 'origin/master' into hermes/hermes-1e775b03
Some checks failed
CI / verify (pull_request) Has been cancelled
Some checks failed
CI / verify (pull_request) Has been cancelled
# Conflicts: # docs/technical/README.md # src/components/custom-world-home/CustomWorldCreationHub.tsx # src/components/custom-world-home/creationWorkShelf.ts # src/components/platform-entry/PlatformEntryFlowShellImpl.tsx # src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx
This commit is contained in:
@@ -65,7 +65,7 @@ Admin Web
|
||||
-> spacetime-module creation_entry_type_config 表
|
||||
```
|
||||
|
||||
`visible=false` 会让创作中心不展示对应入口;`open=false` 会让前端展示锁定态;api-server 的运行态熔断继续以 `visible && open` 判断路由是否可用。
|
||||
`visible=false` 会让创作中心不展示对应入口;`open=false` 会让前端展示锁定态,并让 api-server 熔断对应玩法创作 / 运行态 API。隐藏入口但仍保留既有作品号、广场详情或试玩链路时,应只关闭 `visible`,不要关闭 `open`。
|
||||
|
||||
## 注意
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ SpacetimeDB 新增两张表:
|
||||
其中:
|
||||
|
||||
- `visible=false`:前端隐藏入口。
|
||||
- `open=false`:前端展示为锁定/暂不可创建,api-server 也可据此熔断运行时入口。
|
||||
- `open=false`:前端展示为锁定/暂不可创建,api-server 据此熔断对应玩法 API;只隐藏创作页入口但保留既有作品链路时不要关闭 `open`。
|
||||
- `sort_order`:数据库排序字段,前端只做可见/锁定分组派生。
|
||||
|
||||
## API
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
- `https://developer.hyper3d.ai/api-specification/rodin-generation-gen2`
|
||||
- `https://developer.hyper3d.ai/api-specification/check-status`
|
||||
- `https://developer.hyper3d.ai/api-specification/download-results`
|
||||
- `https://developer.hyper3d.ai/api-specification/check-status_reset_v`
|
||||
- `https://developer.hyper3d.ai/api-specification/download-results_reset_v`
|
||||
|
||||
上游接口:
|
||||
|
||||
@@ -24,6 +26,11 @@ POST https://api.hyper3d.com/api/v2/download
|
||||
|
||||
Rodin Gen-2 提交接口必须使用 `multipart/form-data`。文本生成时提交 `prompt`;图片生成时提交一个或多个 `images` 文件,可选 `prompt` 作为辅助描述。两种模式均固定提交 `tier=Gen-2`。
|
||||
|
||||
官方 `*_reset_v` 文档对状态和下载有两个关键约束:
|
||||
|
||||
1. 生成接口返回的顶层 `uuid` 是后续下载接口的 `task_uuid`,不要使用 `jobs.uuids` 中的子任务 uuid 作为下载参数。
|
||||
2. 状态接口使用 `subscription_key` 查询,并返回 `jobs[]`;只有所有 job 的 `status` 都为 `Done` 才能进入下载,任一 job `Failed` 都应视为任务失败。
|
||||
|
||||
## 3. 环境变量
|
||||
|
||||
```text
|
||||
@@ -111,7 +118,7 @@ RODIN_MODEL_REQUEST_TIMEOUT_MS
|
||||
}
|
||||
```
|
||||
|
||||
状态查询会把上游 `Waiting / Generating / Done / Failed` 归一化为 `waiting / generating / done / failed / unknown`。下载接口只返回上游 `list.name` 与 `list.url`,不在后端转存文件。
|
||||
状态查询会把上游 `Waiting / Generating / Done / Failed` 归一化为 `waiting / generating / done / failed / unknown`,整体状态必须以 `jobs[]` 聚合结果为准。下载接口只返回上游 `list.name` 与 `list.url`,不在 Hyper3D 代理路由中转存文件;具体玩法若需要持久化模型,应在玩法编排层等待 `Done` 后再下载并转存。
|
||||
|
||||
## 7. 验收
|
||||
|
||||
|
||||
@@ -11,19 +11,22 @@
|
||||
入口仍复用 `Match3DAgentWorkspace` 表单。点击 `生成抓大鹅草稿` 后:
|
||||
|
||||
1. 创建 Match3D session。
|
||||
2. 进入 `match3d-generating` 生成过程页。
|
||||
3. 过程页复用拼图生成页的 `CustomWorldGenerationView` 结构。
|
||||
4. 生成成功后自动进入 `match3d-result`。
|
||||
5. 生成失败时停留在生成过程页,允许重新生成或返回创作中心。
|
||||
2. 后端先用当前题材和本地兜底元信息创建同一个 Match3D 草稿 profile,草稿 Tab 必须立即能看到这份存档。
|
||||
3. 进入 `match3d-generating` 生成过程页。
|
||||
4. 过程页复用拼图生成页的 `CustomWorldGenerationView` 结构。
|
||||
5. 生成成功后自动进入 `match3d-result`。
|
||||
6. 生成失败时停留在生成过程页,允许重新生成或返回创作中心;重新生成必须复用同一个 session / profile,并从缺失的素材阶段继续,不新建第二份草稿。
|
||||
|
||||
生成页步骤固定为:
|
||||
|
||||
```text
|
||||
生成游戏名称 -> 生成物品名称 -> 生成素材图 -> 切割独立图片 -> 上传图片资产 -> 写入草稿页
|
||||
生成游戏名称 -> 生成物品名称 -> 生成素材图 -> 切割独立图片 -> 上传图片资产 -> 生成3D模型 -> 写入草稿页
|
||||
```
|
||||
|
||||
生成页只展示题材和物品数量,不展示玩法规则说明。
|
||||
|
||||
当前 `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 完成`。
|
||||
|
||||
## 3. 后端编排边界
|
||||
|
||||
外部模型和 OSS 上传全部由 `api-server` 编排,不进入 SpacetimeDB reducer。SpacetimeDB 继续只负责 Match3D 会话、草稿和作品 profile 的确定性写入。
|
||||
@@ -32,17 +35,19 @@
|
||||
|
||||
1. 读取 session config。
|
||||
2. 将本次 MVP 的 `clearCount` 固定为 `3`,并同步用于草稿编译。
|
||||
3. 基于入口页题材设定文本调用文本模型生成作品元信息。模型固定请求 `gpt-4o`,只返回 JSON,其中 `gameName` 为 4 到 12 个中文字符的游戏名称,`tags` 为 3 到 6 个中文短标签;`summary` 首版必须保持空字符串,结果页 `作品描述` 默认留给用户填写。
|
||||
4. 调用文本模型生成 `3` 个题材下的短物品名称。
|
||||
5. 调用项目当前图片链路 VectorEngine `gpt-image-2-all` 生成一张 `1:1` 素材图,提示词必须合入入口页选择的 `assetStylePrompt`。历史 `nanobanana2` 图片选项当前按项目统一决策回落到 VectorEngine,不重新接入 APIMart 图片网关。
|
||||
6. 将素材图按 `n*n` 网格切割成独立图片。当前 `3` 件物品使用 `2*2` 网格,取前 `3` 格。
|
||||
7. 将素材图和每张独立图片上传到 OSS,其中独立图片作为草稿页素材预览和后续 Rodin 图生模型参考图。
|
||||
8. 调用现有 SpacetimeDB compile procedure 写入草稿,并把本次生成的独立物品图片列表序列化写入 `match3d_work_profile.generated_item_assets_json`。这一步对标拼图的 `save_puzzle_generated_images`:生成资产不能只挂在本次 HTTP response 上,否则退出结果页后从草稿架读取 `getMatch3DWorkDetail` 会丢失素材列表。
|
||||
9. 在 HTTP 返回的 draft/profile DTO 中附带本次生成的素材资产预览信息,独立图片状态为 `image_ready`,模型字段保持为空;后续重进草稿页时从 work profile 的持久化 `generatedItemAssets` 恢复同一批素材。
|
||||
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` 恢复同一批素材。
|
||||
|
||||
若文本模型不可用或返回无法解析,后端必须降级为 `{themeText}抓大鹅` 与本地标签兜底,不阻断素材生成;但描述仍保持空字符串。
|
||||
|
||||
草稿生成阶段不调用 Hyper3D Rodin,不等待 `subscriptionKey`,也不下载模型文件;Rodin 生成只在结果页 `3D素材` Tab 由用户手动触发。手动生成得到的上游下载 URL 仍不得直接写入 Match3D profile,后续正式资产绑定以独立技术方案为准。
|
||||
草稿生成阶段会调用 Hyper3D Rodin 并等待 GLB 下载完成;前端 `match3d_compile_draft` action 请求超时必须覆盖该长耗时链路,当前 Match3D client 使用 20 分钟超时。Rodin 单模型状态轮询预算为 10 分钟,下载列表发布轮询预算为 5 分钟;GLB 下载和 OSS PutObject 各自设置 3 分钟 HTTP 超时,避免上游下载或转存连接长期悬挂。由于 3 个模型并行生成,总耗时按最慢模型计算,不能按模型数量线性叠加。结果页 `3D素材` Tab 直接加载已生成模型;用户点击 `重新生成` 时再复用 Rodin 安全代理,首版重新生成只更新当前页面内预览状态,后续正式资产绑定以独立技术方案为准。
|
||||
|
||||
## 4. 图片提示词
|
||||
|
||||
@@ -83,16 +88,47 @@ generated-match3d-assets
|
||||
|
||||
```text
|
||||
generated-match3d-assets/{sessionId}/{profileId}/material-sheet/{taskId}/sheet.png
|
||||
generated-match3d-assets/{sessionId}/{profileId}/items/{itemSlug}/image.png
|
||||
generated-match3d-assets/{sessionId}/{profileId}/items/{itemSlug}/image/image.png
|
||||
generated-match3d-assets/{sessionId}/{profileId}/items/{itemSlug}/model/{taskUuid}/model.glb
|
||||
```
|
||||
|
||||
`itemSlug` 必须带 `itemId` 前缀,例如 `match3d-item-1-item`。中文物品名清洗后可能都退回 `item`,不能只用物品名做路径,否则多张切割图会写到同一个 object key,导致草稿页预览图全部一致。
|
||||
|
||||
HTTP DTO 同时返回 `imageSrc`、`imageObjectKey`、空的 `modelSrc`、空的 `modelObjectKey` 和 `status = image_ready`。前端预览图片继续走 `ResolvedAssetImage` 换签;后续手动生成的模型文件也必须通过 `useResolvedAssetReadUrl` / `/api/assets/read-url` 换签后打开,不直接请求裸 `/generated-match3d-assets/...` 路径。
|
||||
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/...` 路径。
|
||||
|
||||
## 5.1 运行态模型消费
|
||||
|
||||
生成模型不仅用于结果页预览,也必须进入游戏运行态。运行态入口的传递链路为:
|
||||
|
||||
```text
|
||||
Match3DWorkProfile / PlatformMatch3DGalleryCard
|
||||
-> Match3DRuntimeShell(generatedItemAssets)
|
||||
-> Match3DPhysicsBoard / Match3DTrayPreviewBoard
|
||||
```
|
||||
|
||||
`Match3DPhysicsBoard` 与 `Match3DTrayPreviewBoard` 按运行快照中的 `itemTypeId` 稳定排序后,把生成出的模型顺序映射到对应类型。当前 MVP 固定 `clearCount = 3`,因此 `match3d-type-01/02/03` 分别对应生成列表的第 `1/2/3` 个模型;后续恢复更多物品生成时,后端必须继续保证 `generatedItemAssets` 顺序与类型编号一致。
|
||||
|
||||
前端加载规则:
|
||||
|
||||
1. 优先读取 `modelSrc`;为空时使用 `modelObjectKey`。
|
||||
2. 通过 `readAssetBytes` 调用 `/api/assets/read-bytes`,由同源后端读取 OSS 私有对象字节。
|
||||
3. 使用 Three.js `GLTFLoader.parseAsync` 解析 GLB 字节,并按物品类型缓存模板。
|
||||
4. 场内每个物品和备选栏预览都从模板 clone 独立对象,点击命中继续写入 `itemInstanceId`。
|
||||
5. 物理碰撞和边界仍沿用现有 `visualKey` 的程序化几何,生成 GLB 只替换视觉模型,不承接规则真相。
|
||||
6. 模型缺失、读取失败或 WebGL 回退时,继续使用默认积木素材,不能阻断开局、点击、入槽或结算;调试模式下需要输出加载失败的 `itemTypeId`、模型来源和错误信息,便于区分“资产没有传入”和“GLB 字节读取或解析失败”。
|
||||
|
||||
结果页点击 `试玩` 时,前端必须把当前结果页可见的 `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 中已有的 `modelSrc` / `modelObjectKey` 补齐 draft,不能让旧 draft 把模型状态覆盖回 `image_ready`。`PlatformEntryFlowShellImpl` 在渲染 `match3d-runtime` 时按 `run.profileId` 优先使用当前 `match3dProfile.generatedItemAssets`,只有 profileId 不匹配时才读取 `selectedPublicWorkDetail.generatedItemAssets`。推荐流内嵌正式运行态也必须走同一解析器;当推荐卡片摘要缺少素材时,启动前补读 `getMatch3DWorkDetail(profileId)`,把详情里的生成模型写入 `match3dProfile` 后再传给运行态。这样可以避免从公开详情页残留状态或推荐卡片旧摘要进入试玩 / 正式游戏时,把已生成草稿的 3D 模型覆盖成空列表。
|
||||
|
||||
## 6. 自动保存与草稿恢复
|
||||
|
||||
抓大鹅结果页的基础信息自动保存继续调用 `PUT /api/runtime/match3d/works/{profileId}` 更新名称、题材、描述、标签、封面、消除数和难度;该保存不得清空 `generated_item_assets_json`。SpacetimeDB `update_match3d_work` / `publish_match3d_work` 必须保留当前行的生成素材 JSON。
|
||||
点击 `生成抓大鹅草稿` 后,草稿存档创建与素材生成解耦:
|
||||
|
||||
1. 首次 compile 必须先写 `match3d_work_profile` 草稿行,即使后续卡在文本模型、图片生成、OSS 上传、Rodin 生成或下载转存任意阶段。
|
||||
2. 失败态前端要重新读取 session / work detail,并刷新草稿作品架,保证用户离开生成页后仍能在草稿 Tab 找到这份作品。
|
||||
3. 重新生成时优先使用当前 session 的 `draft.profileId` 或 `publishedProfileId`,不得重新创建 session;后端读取同一 profile 的 `generated_item_assets_json` 后,只补齐缺失图片或缺失模型的阶段。
|
||||
4. 已有 `status = model_ready` 且带 `modelSrc` / `modelObjectKey` 的素材视为完成,不再重复调用 Rodin。
|
||||
|
||||
抓大鹅结果页的基础信息自动保存继续调用 `PUT /api/runtime/match3d/works/{profileId}` 更新名称、题材、描述、标签、封面、消除数和难度;该保存不得清空 `generated_item_assets_json`。结果页 `3D素材` Tab 手动点击 `重新生成` 并拿到 GLB 下载文件后,必须把当前素材草稿重新序列化成 `generatedItemAssets` 并写回作品 profile;否则页面内预览会显示新模型,但试玩、发布和重进草稿仍会读取旧的空模型快照。SpacetimeDB `update_match3d_work` / `publish_match3d_work` 必须保留当前行的生成素材 JSON。
|
||||
|
||||
草稿架重进路径为:
|
||||
|
||||
@@ -100,7 +136,7 @@ HTTP DTO 同时返回 `imageSrc`、`imageObjectKey`、空的 `modelSrc`、空的
|
||||
草稿 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;从草稿架重进没有 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` 中已有模型字段时,用 profile 模型字段补齐 draft;从草稿架重进没有 draft 时,用 `profile.generatedItemAssets`;两者都没有才回退到默认 3D 素材占位。
|
||||
|
||||
结果页 `作品信息` Tab 字段命名对齐拼图草稿:
|
||||
|
||||
@@ -111,7 +147,7 @@ HTTP DTO 同时返回 `imageSrc`、`imageObjectKey`、空的 `modelSrc`、空的
|
||||
|
||||
`3D素材` 详情页只保留:
|
||||
|
||||
1. 模型预览区:优先加载 `modelSrc` 对应 GLB,支持拖动旋转;没有模型时展示空预览。
|
||||
1. 模型预览区:优先加载 `modelSrc` 对应 GLB,缺失时加载 `modelObjectKey`,支持拖动旋转;没有模型时展示空预览。
|
||||
2. 素材名称输入。
|
||||
3. `重新生成` 按钮。
|
||||
|
||||
@@ -125,6 +161,8 @@ HTTP DTO 同时返回 `imageSrc`、`imageObjectKey`、空的 `modelSrc`、空的
|
||||
npm run check:encoding
|
||||
npm run test -- src\services\miniGameDraftGenerationProgress.test.ts
|
||||
npm run test -- src\components\match3d-result\Match3DResultView.test.tsx
|
||||
npm run test -- src\components\match3d-runtime\Match3DRuntimeShell.test.tsx
|
||||
npm run test -- src\components\rpg-entry\RpgEntryFlowShell.agent.interaction.test.tsx
|
||||
npm run typecheck
|
||||
cargo test -p shared-contracts match3d --manifest-path server-rs\Cargo.toml
|
||||
cargo test -p spacetime-client match3d --manifest-path server-rs\Cargo.toml
|
||||
@@ -135,4 +173,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` 和 OSS 访问变量;`HYPER3D_API_KEY` 只在结果页手动生成 3D 模型时需要。后端改动后使用 `npm run api-server` 启动,并检查 `/healthz`。
|
||||
真实草稿生成需要本地私密环境配置 `VECTOR_ENGINE_API_KEY`、`HYPER3D_API_KEY` 和 OSS 访问变量。后端改动后使用 `npm run api-server` 启动,并检查 `/healthz`。
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
|
||||
## 3. Rodin 任务边界
|
||||
|
||||
前端只维护当前页面内的临时重新生成任务状态:
|
||||
前端只维护当前页面内的临时重新生成任务状态;草稿生成得到的正式模型资产从 `generatedItemAssets.modelSrc` 恢复:
|
||||
|
||||
1. 素材槽位名称。
|
||||
2. 模型预览。草稿生成的 `/generated-match3d-assets/...` GLB 必须通过同源 `/api/assets/read-bytes` 由后端换签并读取字节,前端再转成 Blob URL 后交给 Three.js GLTFLoader,避免浏览器直接 `fetch` OSS 签名 URL 时被 CORS 拦截。
|
||||
3. 图生模型参考图只作为重新生成的隐藏输入来源,不在详情页展示。上传图片在前端直接读成 Data URL;草稿生成的 `/generated-match3d-assets/...` 图片必须通过 `/api/assets/read-bytes` 转成 Data URL 后提交给 Hyper3D。
|
||||
4. Hyper3D `taskUuid` 与 `subscriptionKey`。
|
||||
5. 查询到的状态、进度与下载文件列表。
|
||||
4. Hyper3D `taskUuid` 与 `subscriptionKey` 仅用于重新生成过程,不在详情页展示。
|
||||
5. 查询到的状态、进度与下载文件列表仅作为内部状态,不在详情页展示。
|
||||
|
||||
正式资产链后续再接:
|
||||
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
|
||||
## 背景
|
||||
|
||||
创作中心的模板 Tab、平台创作类型弹层和旧“新建作品”卡片配置都依赖同一组玩法模板。此前入口开放状态、隐藏状态和中文文案集中写在 `src/components/platform-entry/platformEntryCreationTypes.ts` 与入口组件中,后续切换玩法开放节奏时容易出现多个入口不一致。
|
||||
创作中心的模板 Tab、平台创作类型弹层和旧“新建作品”卡片配置都依赖同一组玩法模板。此前入口开放状态、隐藏状态和中文文案集中写在前端配置与入口组件中,后续切换玩法开放节奏时容易出现多个入口不一致。当前入口配置事实源已经迁移到 SpacetimeDB,由 `api-server` 通过 `GET /api/creation-entry/config` 下发。
|
||||
|
||||
## 落地规则
|
||||
|
||||
1. 新建作品入口配置统一放在 `src/config/newWorkEntryConfig.ts`。
|
||||
1. 新建作品入口配置统一存放在 SpacetimeDB 的 `creation_entry_config` / `creation_entry_type_config` 表;默认种子位于 `server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs`。
|
||||
2. `visible` 控制玩法是否展示在创作 Tab 模板入口、新建作品入口和创作类型弹层中。
|
||||
3. `open` 控制玩法是否允许点击创建;`open: false` 时入口保持展示但禁用。
|
||||
3. `open` 控制玩法是否允许点击创建以及对应创作 / runtime API 路由是否放行;`open: false` 时入口保持展示但禁用,并由 `api-server` 熔断对应玩法 API。
|
||||
4. `title`、`subtitle`、`badge` 控制玩法卡片文案。
|
||||
5. `startCard` 控制旧创作中心顶部新建作品模块的标题、辅助文案和移动端角标文案;当前创作 Tab 首屏标题固定在 `PlatformEntryFlowShellImpl.tsx`,不再由 `startCard` 控制。
|
||||
6. `typeModal` 控制平台创作类型弹层标题和描述。
|
||||
7. 入口排序仍遵循“可创建玩法在前,未开放玩法在后”;同组内部沿用配置顺序。
|
||||
8. `creative-agent` 可以继续保留运行链路,但默认 `visible: false`,不出现在创作 Tab 模板入口。
|
||||
9. 前端 `src/components/platform-entry/platformEntryCreationTypes.ts` 只做展示派生,不再承载默认入口配置。
|
||||
|
||||
## 当前状态
|
||||
|
||||
@@ -23,17 +24,17 @@
|
||||
| 大鱼吃小鱼 | 否 | 是 | 功能仍保留,不在新建作品入口展示 |
|
||||
| 拼图 | 是 | 是 | 创作 Tab 默认选中并内嵌展示拼图创作表单,提交后进入拼图草稿生成 |
|
||||
| 抓大鹅 | 是 | 是 | 点击后进入抓大鹅 Match3D Agent 共创工作台 |
|
||||
| 方洞挑战 | 是 | 是 | 点击后进入方洞挑战 Agent 共创工作台,支持草稿、结果页、发布、试玩、作品架与广场 |
|
||||
| 方洞挑战 | 否 | 是 | 创作页入口暂时完全隐藏,既有草稿、结果页、发布、试玩、作品架与广场链路保留 |
|
||||
| AIRP | 是 | 否 | 保留入口,显示敬请期待 |
|
||||
| 视觉小说 | 是 | 是 | 点击后进入视觉小说创作工作台 |
|
||||
| 视觉小说 | 是 | 否 | 保留入口,显示敬请期待,暂不允许创建视觉小说草稿 |
|
||||
| 智能创作 | 否 | 是 | 入口隐藏,既有 `creative-agent` 链路保留 |
|
||||
|
||||
## 验收
|
||||
|
||||
1. 修改 `src/config/newWorkEntryConfig.ts` 后,创作 Tab 模板入口、创作中心顶部卡带和平台创作类型弹层应同步变化。
|
||||
1. 修改 SpacetimeDB 入口配置后,创作 Tab 模板入口、创作中心顶部卡带和平台创作类型弹层应同步变化。
|
||||
2. 隐藏玩法不触发入口预加载,也不出现在新建作品入口中。
|
||||
3. 未开放玩法点击态保持禁用,不应进入鉴权或创建会话链路。
|
||||
4. 已开放玩法点击后必须进入对应创建链路;若用户未登录,先走登录保护。
|
||||
5. 创作 Tab 首屏应显示“10分钟创作一个精品互动玩法”,并默认展示拼图创作表单。
|
||||
6. 智能创作入口隐藏后,不应出现“Hi, 朋友”“问一问百梦”或“一句话生成闪应用”等旧首页入口。
|
||||
7. 方洞挑战作品发布后应生成 `SH-` 作品号,并能从作品架、广场详情和试玩 runtime 回到同一作品详情。
|
||||
7. 方洞挑战入口隐藏后,不应出现在创作 Tab 模板入口、创作中心顶部卡带、平台创作类型弹层和创作页作品架中;既有 `SH-` 作品号、广场详情和试玩 runtime 链路不因此删除。
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# 公开作品详情失效回首页修复
|
||||
|
||||
日期:`2026-05-11`
|
||||
|
||||
## 背景
|
||||
|
||||
直接访问 `/works/detail?work=<公开作品号>` 时,如果作品已经删除、下架或当前公开列表无法命中该作品,统一作品详情会先进入 `work-detail` 阶段。此前该阶段在没有 `selectedPublicWorkDetail` 时不会渲染任何内容;用户关闭“作品不存在或已下架”的提示后,页面可能只剩空白区域。
|
||||
|
||||
## 修复
|
||||
|
||||
1. `resolveWorkNotFoundRecoveryAction(...)` 覆盖 `/works/detail`、拼图公开详情和视觉小说公开详情,并复用运行态深链失效的回首页策略。
|
||||
2. 拼图公开详情、拼图运行态启动和拼图详情页读取的 `404/NOT_FOUND` 分支改为统一走公开作品失效恢复逻辑。
|
||||
3. 直接打开 `/works/detail?work=...` 的搜索失败分支会清理详情态、运行态临时数据,切回首页并清掉 URL query。
|
||||
4. `work-detail` 阶段在详情数据为空时渲染轻量读取态,避免异步间隙或异常分支出现纯白屏。
|
||||
|
||||
## 验证
|
||||
|
||||
- `npm run test -- src/routing/runtimeNotFoundRecovery.test.ts`
|
||||
- `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct missing public work detail alert returns to platform home"`
|
||||
- `npm run typecheck`
|
||||
- `npm run check:encoding -- src/routing/runtimeNotFoundRecovery.ts src/routing/runtimeNotFoundRecovery.test.ts src/components/platform-entry/PlatformEntryFlowShellImpl.tsx src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx docs/technical/PUBLIC_WORK_DETAIL_NOT_FOUND_RECOVERY_2026-05-11.md`
|
||||
|
||||
## 关联文件
|
||||
|
||||
1. `src/routing/runtimeNotFoundRecovery.ts`
|
||||
2. `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
3. `src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`
|
||||
89
docs/technical/PUZZLE_MATCH3D_RESULT_AUDIO_TAB_2026-05-11.md
Normal file
89
docs/technical/PUZZLE_MATCH3D_RESULT_AUDIO_TAB_2026-05-11.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# 拼图与抓大鹅结果页音乐 Tab 2026-05-11
|
||||
|
||||
## 1. 范围
|
||||
|
||||
本方案把 VectorEngine 音频生成能力从视觉小说结果页扩展到拼图与抓大鹅结果页:
|
||||
|
||||
1. 拼图结果页新增 `音乐` Tab,支持通过 Suno 生成作品背景音乐。
|
||||
2. 抓大鹅结果页新增 `音乐` Tab,支持通过 Suno 生成作品背景音乐。
|
||||
3. 抓大鹅 `3D素材` Tab 支持为每个生成物体通过 Vidu 生成点击音效。
|
||||
|
||||
本轮不新增 SpacetimeDB 表,不修改表字段,不把供应商密钥下发到前端。
|
||||
|
||||
## 2. 通用音频接口
|
||||
|
||||
后端在既有视觉小说音频路由外新增通用创作音频路由:
|
||||
|
||||
| 方法 | 路由 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `POST` | `/api/creation/audio/background-music` | 提交 Suno 背景音乐任务 |
|
||||
| `POST` | `/api/creation/audio/background-music/{task_id}/asset` | 查询并转存 Suno 音频资产 |
|
||||
| `POST` | `/api/creation/audio/sound-effect` | 提交 Vidu 音效任务 |
|
||||
| `POST` | `/api/creation/audio/sound-effect/{task_id}/asset` | 查询并转存 Vidu 音效资产 |
|
||||
|
||||
通用转存请求由前端传入 `entityKind`、`entityId`、`slot`、`assetKind`、`profileId`。后端仍负责:
|
||||
|
||||
1. 校验 VectorEngine 与 OSS 环境变量。
|
||||
2. 轮询供应商任务结果。
|
||||
3. 下载音频字节。
|
||||
4. 写入 OSS 私有对象。
|
||||
5. 确认 `asset_object` 并绑定 `asset_entity_binding`。
|
||||
|
||||
视觉小说原路由保持兼容,内部继续复用同一套提交、轮询、转存逻辑。
|
||||
|
||||
## 3. 数据落点
|
||||
|
||||
### 3.1 拼图
|
||||
|
||||
拼图作品没有独立作品级 metadata 字段。背景音乐随 `levels_json` 保存到首个 `PuzzleDraftLevel.backgroundMusic`:
|
||||
|
||||
```json
|
||||
{
|
||||
"levelId": "puzzle-level-1",
|
||||
"backgroundMusic": {
|
||||
"taskId": "suno-task",
|
||||
"provider": "vector-engine-suno",
|
||||
"assetObjectId": "assetobj_1",
|
||||
"assetKind": "puzzle_background_music",
|
||||
"audioSrc": "/generated-puzzle-assets/..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
运行态后续可从当前关卡快照或作品详情读取该字段作为背景音乐源;若字段为空,继续使用现有程序化背景音乐兜底。
|
||||
|
||||
### 3.2 抓大鹅
|
||||
|
||||
抓大鹅作品级音频与物体点击音效复用 `generated_item_assets_json` 数组保存,不新增表字段:
|
||||
|
||||
1. 作品背景音乐暂存到第一个 `Match3DGeneratedItemAsset.backgroundMusic`,表示当前 work profile 的作品级背景音乐。
|
||||
2. 单个物体点击音效保存到对应 `Match3DGeneratedItemAsset.clickSound`。
|
||||
|
||||
这是一个兼容性折中:当前 Match3D work profile 没有 work-level metadata 字段,而 `generated_item_assets_json` 已经随作品详情、草稿架、运行态入口稳定传递。后续若新增正式作品 metadata 表达,应迁移 `backgroundMusic` 到作品级字段。
|
||||
|
||||
## 4. 前端交互
|
||||
|
||||
结果页 UI 保持轻量:
|
||||
|
||||
1. `音乐` Tab 只展示必要输入、生成按钮、状态与音频预览,不展示供应商规则说明。
|
||||
2. 生成完成后立即写回本地草稿状态,并触发既有保存链路或专用保存接口。
|
||||
3. 抓大鹅每个物体音效生成入口放在对应素材详情面板内,不在列表下方展开大段配置。
|
||||
|
||||
## 5. 验收
|
||||
|
||||
建议执行:
|
||||
|
||||
```powershell
|
||||
npm run check:encoding
|
||||
npm run test -- src\components\puzzle-result\PuzzleResultView.test.tsx
|
||||
npm run test -- src\components\match3d-result\Match3DResultView.test.tsx
|
||||
npm run typecheck
|
||||
cargo test -p shared-contracts creation_audio --manifest-path server-rs\Cargo.toml
|
||||
cargo test -p shared-contracts puzzle --manifest-path server-rs\Cargo.toml
|
||||
cargo test -p shared-contracts match3d --manifest-path server-rs\Cargo.toml
|
||||
cargo test -p api-server vector_engine_audio_generation --manifest-path server-rs\Cargo.toml
|
||||
cargo test -p api-server match3d --manifest-path server-rs\Cargo.toml
|
||||
cargo check -p api-server --manifest-path server-rs\Cargo.toml
|
||||
```
|
||||
|
||||
真实生成 smoke 需要本地私密环境配置 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY` 与 OSS 变量。后端改动后使用 `npm run api-server` 启动,并确认 `/healthz`。
|
||||
@@ -6,9 +6,10 @@
|
||||
|
||||
- [BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md](./BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md):冻结“汪汪声浪大作战 / bark-battle”后端 DDD 技术方案,明确 `server-rs + Axum + SpacetimeDB` 分层边界、shared contracts、作品配置、runtime run、派生成绩、排行榜、`work_play_start` 埋点、migration/绑定生成策略,以及不保存原始麦克风音频的隐私与反作弊约束。
|
||||
- [BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md](./BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md):冻结“汪汪声浪大作战 / bark-battle”2D 浏览器 runtime 技术方案,明确 Phaser + TypeScript + Vite 选型、纯 TS simulation 与 Phaser renderer/DOM HUD 边界、Web Audio 输入适配、移动端权限降级和后续测试验证命令。
|
||||
- [PUBLIC_WORK_DETAIL_NOT_FOUND_RECOVERY_2026-05-11.md](./PUBLIC_WORK_DETAIL_NOT_FOUND_RECOVERY_2026-05-11.md):记录直接访问公开作品详情深链时作品不存在或已下架的回首页修复,避免关闭提示后停在 `work-detail` 空状态白屏。
|
||||
- [CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md](./CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md):冻结儿童动作识别互动玩法 Demo 固定热身关的开发落地规格,覆盖横屏展示、摄像头背景虚化、角色剪影、绿色圆环 2 秒保持、动作教学、当前会话内空间边界记录和后续关卡安全暂停规则。
|
||||
- [RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md](./RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md):记录运行态输入设备抽象层,明确鼠标、触控、mocap 等设备统一归一为通用拖拽语义,玩法组件只负责解释目标和落点。
|
||||
- [RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md](./RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md):记录 `server-rs` Cargo 依赖集中配置口径,第三方版本和 workspace 内部 crate path 统一维护在根 `server-rs/Cargo.toml`,成员 crate 只保留 feature/optional 差异。
|
||||
- [RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md](./RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md):记录 `server-rs` Cargo 依赖集中配置口径,第三方版本和 workspace 内部 crate path 统一维护在根 `server-rs/Cargo.toml`,成员 crate 只保留 feature/optional 差异;同时冻结 `shared-contracts` 不得反向依赖 `platform-*`,避免 SpacetimeDB 模块发布时拉入 `wasm-bindgen`。
|
||||
- [DEV_RUST_STACK_PORT_CONFLICT_PRECHECK_2026-05-09.md](./DEV_RUST_STACK_PORT_CONFLICT_PRECHECK_2026-05-09.md):记录本地完整 Rust 栈启动时 `api-server`、主站 Vite 和后台 Vite 端口占用的误判根因、脚本预检策略和 Windows 排障命令。
|
||||
- [VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md](./VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md):记录 GPT-image-2 图片生成从 APIMart 迁移到 VectorEngine `gpt-image-2-all` 的接口、环境变量、尺寸映射、错误口径和验收命令。
|
||||
- [SPACETIMEDB_PUBLISH_SCCACHE_FALLBACK_2026-05-09.md](./SPACETIMEDB_PUBLISH_SCCACHE_FALLBACK_2026-05-09.md):记录本地 `spacetime publish` 被 sccache wrapper 通信异常阻断时的根因、debug 构建参数口径和手动排障命令。
|
||||
|
||||
@@ -26,6 +26,12 @@
|
||||
- mocap 光标按 60Hz 插值更新 UI 位置,并在拖拽中用插值后的当前点持续驱动输入层,避免输入包帧率低或抖动时出现明显跳变。
|
||||
- 合并大块由拼图运行态把手部坐标命中到任一成员拼块;本地拼图运行时再按 `mergedGroupId` 执行整组平移。
|
||||
|
||||
## 调试模式
|
||||
|
||||
前端全局调试模式统一通过 `src/config/debugMode.ts` 判断。默认跟随 Vite 开发态:`import.meta.env.DEV` 为真时开启,生产构建默认关闭;如需显式覆盖,可设置 `VITE_DEBUG_MODE=true` 或 `VITE_DEBUG_MODE=false`。
|
||||
|
||||
拼图运行态的 mocap 调试面板只在全局调试模式下渲染。面板默认折叠,只保留一行连接状态,展开后才显示动作、手势、解析告警和原始包预览,避免开发诊断信息遮挡拼图棋盘和底部操作。
|
||||
|
||||
## 接入规则
|
||||
|
||||
新玩法或新设备接入时遵循以下边界:
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
4. 成员 crate 只保留自身需要表达的差异,例如 `features`、`optional = true` 或 target-specific dependency。
|
||||
5. 需要关闭 default features 的依赖,应优先在 workspace 根依赖中声明;成员 crate 不再重复覆盖同一项。
|
||||
6. `module-assets` 这类有默认服务端 feature 的领域 crate,在 workspace 根内按 `default-features = false` 维护;需要服务端 OSS/HTTP 能力的 adapter crate 显式启用 `features = ["server-service"]`。
|
||||
7. 面向 SpacetimeDB WASM 的依赖链不得隐式启用原生 HTTP / OSS / Web 平台依赖;例如 `shared-contracts` 的 `assets` 模块通过 `oss-contracts` feature 暴露给 `api-server`,`spacetime-module` 路径只消费关闭默认 feature 后的纯 DTO 子集。
|
||||
7. `shared-contracts` 只能承载前后端公开 DTO 和轻量枚举,禁止直接依赖 `platform-*` 服务实现 crate;需要把平台实现响应转换为公开 DTO 时,转换函数放在 `api-server` 等 adapter 层。
|
||||
8. 面向 SpacetimeDB WASM 的依赖链不得隐式启用原生 HTTP / OSS / Web 平台依赖;例如 `shared-contracts` 的 `assets` 模块通过不依赖 `platform-oss` 的 `oss-contracts` feature 暴露给 `api-server`,`spacetime-module` 路径只消费关闭默认 feature 后的纯 DTO 子集。
|
||||
9. `spacetime-module` 的传递依赖不能包含 `reqwest`、`web-sys`、`js-sys`、`wasm-bindgen` 等 Web/HTTP 客户端链路;发布前可用 `cargo tree -i wasm-bindgen --manifest-path server-rs/Cargo.toml -p spacetime-module --target wasm32-unknown-unknown` 排查。
|
||||
|
||||
## 3. 本次收敛范围
|
||||
|
||||
@@ -57,14 +59,33 @@ npm.cmd run check:encoding -- docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDA
|
||||
|
||||
## 6. SpacetimeDB WASM 依赖边界
|
||||
|
||||
2026-05-11 本地重置 SpacetimeDB 并重新发布 `xushi-p4wfr` 时,`spacetime publish` 在 Rust 编译成功后报 `wasm-bindgen detected`。排查命令显示链路为:
|
||||
|
||||
```text
|
||||
spacetime-module -> module-runtime -> shared-contracts -> platform-oss -> reqwest -> wasm-bindgen
|
||||
```
|
||||
|
||||
根因是 `shared-contracts` 为了复用 OSS 直传/读签名返回类型,直接依赖了 `platform-oss`。这违反 DDD 分层边界:契约 crate 不能依赖平台副作用实现,否则所有引用契约的纯领域和 SpacetimeDB 模块都会被迫拉入 HTTP client。
|
||||
|
||||
`spacetime publish` 会构建 `spacetime-module` 的 `wasm32-unknown-unknown` 目标。这个目标不能包含 `wasm-bindgen`,也不应通过 DTO crate 间接拉入 `reqwest`、`web-sys` 或浏览器 WebAssembly 平台依赖。
|
||||
|
||||
修正口径:
|
||||
|
||||
1. `shared-contracts::assets` 定义独立的公开 DTO 和 `DirectUploadObjectAccess` 轻量枚举。
|
||||
2. `platform-oss` 保持 OSS 签名、读写请求和错误分类实现,不被契约层引用。
|
||||
3. `api-server::assets` 负责把 `platform_oss::OssPostObjectResponse` / `OssSignedGetObjectUrlResponse` 转成 `shared-contracts` DTO。
|
||||
4. 后续新增外部平台能力时,重复使用这个边界:平台 crate 不得被 `shared-contracts`、`module-*` 或 `spacetime-module` 反向依赖。
|
||||
|
||||
已验证的排查命令:
|
||||
|
||||
```powershell
|
||||
cargo tree -i wasm-bindgen --manifest-path server-rs\Cargo.toml -p spacetime-module --target wasm32-unknown-unknown
|
||||
cargo tree -i wasm-bindgen --manifest-path server-rs\crates\spacetime-module\Cargo.toml --target wasm32-unknown-unknown
|
||||
cargo tree --manifest-path server-rs\crates\spacetime-module\Cargo.toml --target wasm32-unknown-unknown | Select-String -Pattern 'wasm-bindgen|platform-oss|reqwest'
|
||||
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml --target wasm32-unknown-unknown
|
||||
cargo check -p shared-contracts --manifest-path server-rs\Cargo.toml
|
||||
cargo check -p api-server --manifest-path server-rs\Cargo.toml
|
||||
spacetime publish xushi-p4wfr --server local --module-path server-rs\crates\spacetime-module --build-options="--debug" -c=on-conflict --yes
|
||||
```
|
||||
|
||||
若反向树显示 `reqwest -> platform-oss -> shared-contracts -> module-* -> spacetime-module`,优先检查新增的 `shared-contracts` 或领域 crate 依赖是否忘记关闭默认 feature。原生 `api-server` 需要资产上传契约时,应在自身 `Cargo.toml` 显式启用 `shared-contracts` 的 `oss-contracts` feature,而不是让 workspace 根依赖默认启用。
|
||||
若反向树显示 `reqwest -> platform-oss -> shared-contracts -> module-* -> spacetime-module`,优先检查新增的 `shared-contracts` 或领域 crate 依赖是否忘记关闭默认 feature,或 `shared-contracts` feature 是否错误依赖了平台实现 crate。原生 `api-server` 需要资产上传契约时,应在自身 `Cargo.toml` 显式启用 `shared-contracts` 的 `oss-contracts` feature,而不是让 workspace 根依赖默认启用。
|
||||
|
||||
@@ -7,6 +7,7 @@ Windows 本地执行 `npm run dev:rust` 或 `spacetime publish` 时,`spacetime
|
||||
当本机 sccache server 状态损坏、client/server 通信异常或版本残留不一致时,可能出现:
|
||||
|
||||
```text
|
||||
sccache: error: Timed out waiting for server startup. Maybe the remote service is unreachable?
|
||||
sccache: error: failed to execute compile
|
||||
sccache: caused by: Failed to send data to or receive data from server
|
||||
sccache: caused by: Failed to read response header
|
||||
@@ -15,9 +16,24 @@ sccache: caused by: failed to fill whole buffer
|
||||
|
||||
这类错误发生在 rustc wrapper 层,不能说明 SpacetimeDB module 代码本身编译失败。
|
||||
|
||||
## 2026-05-11 本机根因定位
|
||||
|
||||
本机 `cargo check -p api-server` 失败时,Cargo 还没有进入业务 crate 编译,而是在读取 `server-rs/.cargo/config.toml` 后执行 `sccache rustc -vV` 探测编译器版本。失败的 stderr 会被写入 `server-rs/target/.rustc_info.json`,内容为 `Timed out waiting for server startup`。
|
||||
|
||||
当前 PowerShell 环境设置了 `SCCACHE_OSS_BUCKET=genarrative-sccache`、`SCCACHE_OSS_ENDPOINT=https://oss-rg-china-mainland.aliyuncs.com` 和 `SCCACHE_OSS_KEY_PREFIX=genarrative`,且没有设置本地 `SCCACHE_DIR`。因此 sccache daemon 冷启动时会先初始化 OSS 远端缓存,并执行 `.sccache_check` 的读写检查;日志中可见 `Init oss cache ...`、`proxy(http://127.0.0.1:7897/) intercepts ...`,随后才出现 `server started, listening on 127.0.0.1:4226`。
|
||||
|
||||
本次排查的结论是:冷启动失败主要发生在 sccache client 等待 daemon 启动的握手窗口内,而 daemon 启动又依赖 OSS/本机代理链路先完成缓存可读写检查。代理或 OSS 链路稍慢时,Cargo 调用的 `sccache rustc -vV` 会先超时;daemon 预热后直接执行同一条 `sccache rustc -vV` 又可能成功,所以这是冷启动/通道状态问题,不是 `api-server` 或 Rust 代码错误。
|
||||
|
||||
辅助证据:
|
||||
|
||||
1. `rustc -vV` 可直接输出版本,说明 Rust 工具链本身可用。
|
||||
2. `tasklist` 曾只看到 `sccache --show-stats` 客户端进程,`netstat` 只出现到 `127.0.0.1:4226` 的 `SYN_SENT`,没有真正的 `LISTEN`,说明当时 client 正在等一个尚未成功监听的 daemon。
|
||||
3. 在子进程中临时清掉 `SCCACHE_OSS_*` 并设置本地 `SCCACHE_DIR` 后,sccache 退回本地磁盘缓存,日志显示 `Init disk cache ...`,`rustc -vV` 和 `sccache --show-stats` 均能完成。
|
||||
4. `C:\Users\DSK\AppData\Roaming\Mozilla\sccache\config\config` 缺失只是非致命 warning,本机实际配置来自环境变量。
|
||||
|
||||
## 本地开发处理
|
||||
|
||||
`scripts/dev-rust-stack.sh` 的 publish 阶段继续由 SpacetimeDB CLI 内部调用 Cargo,并通过 `--build-options="--debug"` 使用 debug 构建参数。遇到 sccache 通信或 wrapper 失败时,本地排障仍优先绕过 wrapper 验证 rustc 本身可用。
|
||||
`scripts/dev-rust-stack.sh` 的 publish 阶段继续由 SpacetimeDB CLI 内部调用 Cargo,并通过 `--build-options="--debug"` 使用 debug 构建参数。遇到 sccache 冷启动超时时,优先保留 `sccache` wrapper,并修复 sccache daemon 的启动等待时间;只有在排除 sccache 本身问题时,才临时绕过 wrapper 验证 rustc 本身可用。
|
||||
|
||||
该处理不修改 `server-rs/.cargo/config.toml`,也不删除本地 target 缓存。
|
||||
|
||||
@@ -29,13 +45,56 @@ sccache: caused by: failed to fill whole buffer
|
||||
rustc -vV
|
||||
```
|
||||
|
||||
如果只想绕过本次 Cargo 构建的 sccache wrapper,可在 Git Bash 中执行:
|
||||
如果要保留 sccache 并修复冷启动等待时间,在 PowerShell 中创建或更新 sccache 默认配置:
|
||||
|
||||
```powershell
|
||||
$configDir = Join-Path $env:APPDATA "Mozilla\sccache\config"
|
||||
New-Item -ItemType Directory -Force -Path $configDir | Out-Null
|
||||
@(
|
||||
"# Windows 本机 sccache 冷启动需要先完成 OSS 缓存读写检查。"
|
||||
"# 拉长 client 等待 daemon 启动的时间,避免 Cargo 在 rustc -vV 阶段误判超时。"
|
||||
"server_startup_timeout_ms = 60000"
|
||||
) | Set-Content -Encoding UTF8 -Path (Join-Path $configDir "config")
|
||||
```
|
||||
|
||||
随后清掉 Cargo 曾缓存的失败探测结果,并从冷启动验证:
|
||||
|
||||
```powershell
|
||||
cd C:\proj\Genarrative\server-rs
|
||||
sccache --stop-server
|
||||
Remove-Item -Force target\.rustc_info.json -ErrorAction SilentlyContinue
|
||||
cargo check -p api-server
|
||||
```
|
||||
|
||||
注意:不要在另一个 `cargo` / `rustc` 仍在编译时执行 `taskkill /F /IM sccache.exe /T`。sccache 对 proc-macro crate 会显示 `Server sent UnhandledCompile` 并把请求转交给真实 rustc;如果此时强杀 sccache client/server,可能让 `serde_derive`、`spacetimedb-bindings-macro` 等 proc-macro 编译直接以 `sccache ... exit code: 1` 失败,而 stderr 里看不到真正的 Rust 诊断。这是排障动作打断编译,不是 `spacetime-module` 源码错误。
|
||||
|
||||
如果只想临时绕过本次 Cargo 构建的 sccache wrapper,可在 Git Bash 中执行:
|
||||
|
||||
```bash
|
||||
cd server-rs/crates/spacetime-module
|
||||
RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= cargo check --target=wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
PowerShell 原生 Cargo 的一次性 wrapper 绕过命令是:
|
||||
|
||||
```powershell
|
||||
cd C:\proj\Genarrative\server-rs
|
||||
cargo check -p api-server --config "build.rustc-wrapper=''"
|
||||
```
|
||||
|
||||
如果需要验证是否为 OSS/代理冷启动问题,可只在当前 PowerShell 进程中切到本地缓存做对照:
|
||||
|
||||
```powershell
|
||||
$env:SCCACHE_LOG = "debug"
|
||||
$env:SCCACHE_ERROR_LOG = "C:\proj\Genarrative\logs\sccache-local-start-error.log"
|
||||
$env:SCCACHE_DIR = Join-Path $env:TEMP "genarrative-sccache-local-test"
|
||||
Remove-Item Env:SCCACHE_OSS_BUCKET -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:SCCACHE_OSS_ENDPOINT -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:SCCACHE_OSS_KEY_PREFIX -ErrorAction SilentlyContinue
|
||||
sccache "C:\Users\DSK\.rustup\toolchains\stable-x86_64-pc-windows-msvc\bin\rustc.exe" -vV
|
||||
sccache --show-stats
|
||||
```
|
||||
|
||||
如果需要排查 sccache server 状态:
|
||||
|
||||
```bash
|
||||
@@ -44,10 +103,12 @@ sccache --stop-server
|
||||
sccache --start-server
|
||||
```
|
||||
|
||||
`sccache --stop-server` 本身也可能因为 server 通道已损坏而失败;此时不应阻断本地开发 publish,先使用 wrapper 降级完成验证。
|
||||
`sccache --stop-server` 本身也可能因为 server 通道已损坏而失败;只有确认当前没有 `cargo`、`rustc`、`link` 进程后,才用 `taskkill /F /IM sccache.exe /T` 清理残留进程。此时不应阻断本地开发 publish,先使用 wrapper 降级完成验证。
|
||||
|
||||
## 验证
|
||||
|
||||
1. `bash -n scripts/dev-rust-stack.sh`
|
||||
2. `RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= cargo check --target=wasm32-unknown-unknown`
|
||||
3. 重新运行 `npm run dev:rust`,确认 publish 命令带有 `--build-options="--debug"`。
|
||||
2. 冷启动后直接执行 `cargo check -p api-server`,确认不再出现 `Timed out waiting for server startup`。
|
||||
3. 执行 `cargo check -p spacetime-module`,确认 proc-macro 依赖和 SpacetimeDB module 都能在 sccache wrapper 下通过。
|
||||
4. `sccache --show-stats` 显示 `Cache location oss, name: genarrative-sccache`,确认仍在使用 sccache/OSS 缓存。
|
||||
5. 重新运行 `npm run dev:rust`,确认 publish 命令带有 `--build-options="--debug"`。
|
||||
|
||||
@@ -72,8 +72,8 @@ SELECT * FROM auth_store_snapshot WHERE snapshot_id = 'default';
|
||||
### `user_account`
|
||||
|
||||
- 作用:用户账号主表,保存用户名、公开百梦号、手机号掩码、登录方式、密码登录开关、token 版本和默认不前端展示的运营标签。
|
||||
- 结构:`user_id PK: String`, `public_user_code: String`, `username: String`, `display_name: String`, `avatar_url: Option<String>`, `phone_number_masked: Option<String>`, `phone_number_e164: Option<String>`, `login_method: String`, `binding_status: String`, `wechat_bound: bool`, `password_hash: String`, `password_login_enabled: bool`, `token_version: u64`, `user_tags: Vec<String>`。
|
||||
- 说明:`user_tags` 默认空数组,只允许后端白名单投影到特定业务接口;不得在登录态、个人资料等通用前端响应中直接暴露。
|
||||
- 结构:`user_id PK: String`, `public_user_code: String`, `username: String`, `display_name: String`, `avatar_url: Option<String>`, `phone_number_masked: Option<String>`, `phone_number_e164: Option<String>`, `login_method: String`, `binding_status: String`, `wechat_bound: bool`, `password_hash: String`, `password_login_enabled: bool`, `token_version: u64`, `user_tags: Option<Vec<String>>`。
|
||||
- 说明:`user_tags` 数据库默认 `None`,业务读取时按空数组归一化;只允许后端白名单投影到特定业务接口,不得在登录态、个人资料等通用前端响应中直接暴露。
|
||||
- 索引:`username`, `public_user_code`。
|
||||
|
||||
```sql
|
||||
@@ -256,11 +256,11 @@ SELECT * FROM profile_redeem_code_usage WHERE user_id = '<user_id>';
|
||||
|
||||
### `profile_invite_code`
|
||||
|
||||
- 作用:用户邀请中心的邀请码主表,也承载后台运营预置邀请码和使用后授予账号的运营标签。
|
||||
- 结构:`user_id PK: String`, `invite_code: String`, `metadata_json: String`, `created_at: Timestamp`, `updated_at: Timestamp`, `starts_at: Option<Timestamp>`, `expires_at: Option<Timestamp>`, `granted_user_tags: Vec<String>`。
|
||||
- 作用:用户邀请中心的邀请码主表,也承载后台运营预置邀请码和使用后授予账号的运营标签配置。
|
||||
- 结构:`user_id PK: String`, `invite_code: String`, `metadata_json: String`, `created_at: Timestamp`, `updated_at: Timestamp`, `starts_at: Option<Timestamp>`, `expires_at: Option<Timestamp>`。
|
||||
- 索引:主键 `user_id`,唯一索引 `invite_code`。
|
||||
- 后台读取:`GET /admin/api/profile/invite-codes` 只返回 `user_id` 以 `admin:` 开头的后台预置码;普通用户自己的邀请码不得进入后台运营列表。
|
||||
- 说明:`granted_user_tags` 默认空数组;用户注册填写该邀请码后合并进 `user_account.user_tags`,不回改历史用户。
|
||||
- 说明:使用该邀请码后授予的标签存放在 `metadata_json.userTags`,服务端兼容读取 `metadata_json.user_tags`;用户注册填写该邀请码后合并进 `user_account.user_tags`,不回改历史用户。
|
||||
|
||||
```sql
|
||||
SELECT * FROM profile_invite_code WHERE user_id = '<user_id>';
|
||||
@@ -665,7 +665,7 @@ SELECT * FROM match3d_agent_message WHERE session_id = '<session_id>' ORDER BY c
|
||||
|
||||
- 作用:抓大鹅 Match3D 作品主表,保存作品基础信息、配置、发布状态、游玩次数和草稿生成出的独立物品素材引用。
|
||||
- 结构:`profile_id PK: String`, `owner_user_id: String`, `source_session_id: String`, `author_display_name: String`, `game_name: String`, `theme_text: String`, `summary_text: String`, `tags_json: String`, `cover_image_src: String`, `cover_asset_id: String`, `clear_count: u32`, `difficulty: u32`, `config_json: String`, `publication_status: String`, `play_count: u32`, `updated_at: Timestamp`, `published_at: Option<Timestamp>`, `generated_item_assets_json: Option<String>`。
|
||||
- 说明:`generated_item_assets_json` 保存 `Match3DGeneratedItemAsset` 数组 JSON,用于草稿页退出后从作品架重进时恢复 `3D素材` Tab 中的切割图片预览;基础信息自动保存和发布必须保留该字段。
|
||||
- 说明:`generated_item_assets_json` 保存 `Match3DGeneratedItemAsset` 数组 JSON,用于草稿页退出后从作品架重进时恢复 `3D素材` Tab 中的切割图片和 GLB 模型预览;运行态也通过该字段拿到 `modelSrc` / `modelObjectKey` 并优先渲染生成模型。基础信息自动保存和发布必须保留该字段。
|
||||
- 索引:`owner_user_id`, `publication_status`。
|
||||
|
||||
```sql
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# 用户标签、邀请码授予与拼图榜单展示方案
|
||||
|
||||
更新时间:`2026-05-10`
|
||||
更新时间:`2026-05-11`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本次新增用户标签系统的最小闭环:
|
||||
|
||||
1. `user_account` 增加账号标签字段,默认空。
|
||||
2. 后台预置邀请码可配置授予标签。
|
||||
1. `user_account` 增加账号标签字段,数据库默认空,业务读取时按空数组处理。
|
||||
2. 后台预置邀请码可通过原有 `metadata` 字段配置授予标签。
|
||||
3. 用户填写带标签的邀请码后,把标签合并到自己的账号。
|
||||
4. 标签默认不在前端资料页、邀请中心或通用接口展示。
|
||||
5. 拼图排行榜仅对白名单标签做展示,首版只展示 `北科`。
|
||||
@@ -16,19 +16,21 @@
|
||||
|
||||
### `user_account.user_tags`
|
||||
|
||||
- 类型:`Vec<String>`。
|
||||
- 默认:空数组。
|
||||
- 类型:`Option<Vec<String>>`。
|
||||
- 默认:`None`,业务层读取时统一按空数组处理。
|
||||
- 语义:账号级运营标签,属于后台与服务端投影数据,不作为普通前端个人资料字段。
|
||||
- 写入:首版只由邀请码兑换链路合并写入。
|
||||
- 迁移:旧迁移包和旧数据库按空数组兼容。
|
||||
- 迁移:旧迁移包和旧数据库按 `null` 兼容,再由业务层归一化为空数组。
|
||||
|
||||
### `profile_invite_code.granted_user_tags`
|
||||
### `profile_invite_code.metadata_json.userTags`
|
||||
|
||||
- 类型:`Vec<String>`。
|
||||
- 默认:空数组。
|
||||
- 类型:`metadata_json` 对象里的 `userTags: string[]`。
|
||||
- 默认:字段缺失或空数组时不授予标签。
|
||||
- 语义:使用该邀请码后授予被邀请账号的标签列表。
|
||||
- 范围:后台运营预置码和普通用户个人邀请码都可存字段,但后台表单首版只允许管理员配置预置码。
|
||||
- 迁移:旧邀请码按空数组兼容。
|
||||
- 存储:不再新增或使用独立的邀请码标签列;后台保存时把用户标签写回 `metadata.userTags`。
|
||||
- 解析:服务端优先读取 `metadata_json.userTags`,并兼容解析 `metadata_json.user_tags`。
|
||||
- 迁移:旧邀请码缺少 `metadata_json` 时按 `{}` 兼容;旧迁移包里已废弃的独立字段会在导入时丢弃。
|
||||
|
||||
## 3. 标签归一化
|
||||
|
||||
@@ -45,35 +47,38 @@
|
||||
|
||||
1. 写入 `profile_referral_relation`。
|
||||
2. 发放原有双方奖励。
|
||||
3. 读取 `profile_invite_code.granted_user_tags`。
|
||||
3. 从 `profile_invite_code.metadata_json` 解析 `userTags` / `user_tags`。
|
||||
4. 将这些标签合并进 `user_account.user_tags`。
|
||||
|
||||
管理员更新邀请码时,`grantedUserTags` 代表覆盖该邀请码之后授予的标签集合;空数组代表不授予标签。更新邀请码不会回溯修改已经使用过该邀请码的账号。
|
||||
管理员更新邀请码时,后台表单里的用户标签会覆盖写入 `metadata.userTags`;空数组代表不授予标签。更新邀请码不会回溯修改已经使用过该邀请码的账号。
|
||||
|
||||
## 5. API 契约
|
||||
|
||||
后台邀请码 upsert 请求增加:
|
||||
后台邀请码 upsert 请求继续只提交 `metadata`,标签写在 `metadata.userTags` 中:
|
||||
|
||||
```json
|
||||
{
|
||||
"inviteCode": "BEIKE2026",
|
||||
"grantedUserTags": ["北科"],
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"userTags": ["北科"]
|
||||
},
|
||||
"startsAt": null,
|
||||
"expiresAt": null
|
||||
}
|
||||
```
|
||||
|
||||
后台邀请码列表和 upsert 返回增加同名字段:
|
||||
后台邀请码列表和 upsert 返回继续回传 `metadata`:
|
||||
|
||||
```json
|
||||
{
|
||||
"inviteCode": "BEIKE2026",
|
||||
"grantedUserTags": ["北科"]
|
||||
"metadata": {
|
||||
"userTags": ["北科"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
用户侧邀请中心、账号资料、登录返回和普通 profile 接口不返回 `userTags`。
|
||||
后台表单展示时从 `metadata.userTags` 回显用户标签;用户侧邀请中心、账号资料、登录返回和普通 profile 接口不返回 `userTags`。
|
||||
|
||||
## 6. 拼图排行榜展示
|
||||
|
||||
@@ -94,9 +99,10 @@
|
||||
|
||||
## 7. 验收
|
||||
|
||||
1. 新账号 `user_account.user_tags` 默认为空。
|
||||
2. 后台创建邀请码时可填写 `北科`,列表和结果面板可回显。
|
||||
1. 新账号 `user_account.user_tags` 数据库默认为 `None`,业务读取为空数组。
|
||||
2. 后台创建邀请码时可填写 `北科`,请求和返回的 `metadata.userTags` 可回显。
|
||||
3. 用户填写该邀请码后,账号表 `user_tags` 包含 `北科`。
|
||||
4. 不带标签的邀请码不改变账号标签。
|
||||
5. 拼图排行榜中带 `北科` 的用户昵称下方显示 `北科`,其它标签不显示。
|
||||
6. 执行 `npm run check:encoding`、`cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`,并按影响范围执行后台 typecheck / 拼图前端测试。
|
||||
6. 生成 SpacetimeDB bindings 后,不再出现独立的邀请码标签字段。
|
||||
7. 执行 `npm run check:encoding`、`cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`,并按影响范围执行后台 typecheck / 拼图前端测试。
|
||||
|
||||
Reference in New Issue
Block a user