diff --git a/.env.example b/.env.example index 44493fe4..03b8c665 100644 --- a/.env.example +++ b/.env.example @@ -17,7 +17,15 @@ VITE_SCENE_IMAGE_PROXY_BASE_URL="/api/custom-world/scene-image" NODE_SERVER_ADDR=":8081" NODE_SERVER_TARGET="http://127.0.0.1:8081" -# Rust api-server local target used by the Big Fish compatibility gateway in server-node. +# M7 backend cutover switch for local/gray dev proxy. +# Keep `node` by default. Set to `rust` to point Vite dev proxy at the Rust Axum server. +GENARRATIVE_BACKEND_STACK="node" +RUST_SERVER_TARGET="http://127.0.0.1:3000" +# Optional hard override. When set, it wins over GENARRATIVE_BACKEND_STACK/NODE_SERVER_TARGET/RUST_SERVER_TARGET. +GENARRATIVE_RUNTIME_SERVER_TARGET="" + +# Rust api-server local target used by the Big Fish / Puzzle compatibility gateways +# and by the standalone Rust dev / deploy scripts. GENARRATIVE_API_PORT="3100" GENARRATIVE_API_TARGET="http://127.0.0.1:3100" GENARRATIVE_INTERNAL_API_SECRET="CHANGE_ME_FOR_PRODUCTION" diff --git a/backend-rewrite-tasklist/00_MASTER_TASKLIST.md b/backend-rewrite-tasklist/00_MASTER_TASKLIST.md index f952d46a..ab5d2f61 100644 --- a/backend-rewrite-tasklist/00_MASTER_TASKLIST.md +++ b/backend-rewrite-tasklist/00_MASTER_TASKLIST.md @@ -113,6 +113,7 @@ 3. 部署 4. 观测 5. 灰度切流 +6. 收口 `spacetime-module` 主工程结构,拆分过大的 `src/lib.rs` 详见: diff --git a/backend-rewrite-tasklist/03_M4_STORY_AND_GAMEPLAY.md b/backend-rewrite-tasklist/03_M4_STORY_AND_GAMEPLAY.md index c7f11467..82a7e697 100644 --- a/backend-rewrite-tasklist/03_M4_STORY_AND_GAMEPLAY.md +++ b/backend-rewrite-tasklist/03_M4_STORY_AND_GAMEPLAY.md @@ -27,9 +27,9 @@ 9. 已执行 `cargo check -p module-story -p spacetime-module -p spacetime-client -p api-server` 并通过。 6. 已新增 `docs/technical/M4_MODULE_COMBAT_SPACETIMEDB_BASELINE_2026-04-21.md`,冻结 `battle_state` 与 `resolve_combat_action` 的首版字段与规则口径。 7. 已新增 `server-rs/crates/module-runtime-item` 真实 crate。 -8. 已冻结 `treasure_record` 的首版领域类型、完整奖励物品快照和字段校验规则。 -9. 已在 `server-rs/crates/spacetime-module` 中新增 `treasure_record` 表。 -10. 已新增 `resolve_treasure_interaction` reducer 与 `resolve_treasure_interaction_and_return` procedure,并把宝箱奖励同步写入 `inventory_slot`。 +8. 已冻结 runtime item 侧奖励快照与物品写回基线,为后续奖励链并入 inventory / quest / combat 提供统一底层能力。 +9. 已在 `server-rs/crates/spacetime-module` 中补齐 runtime item / inventory / quest / combat 所需的奖励落表与回写依赖。 +10. 当前 M4 runtime story compat bridge 已明确移除旧 `treasure_*` 遭遇动作概念,不再把宝箱遭遇视作本阶段 runtime story 主链目标。 11. 已新增 `docs/technical/M4_RPG_RUNTIME_INVENTORY_SPACETIMEDB_BASELINE_2026-04-21.md`,冻结 `inventory_slot` 与 `apply_inventory_mutation` 的首版字段与规则口径。 12. 已新增 `server-rs/crates/module-inventory` 真实 crate。 13. 已在 `server-rs/crates/spacetime-module` 中新增 `inventory_slot` 表。 @@ -68,14 +68,147 @@ 40. 已再次执行 `cargo check -p spacetime-client --manifest-path D:\\Genarrative\\server-rs\\Cargo.toml` 与 `cargo check -p api-server --manifest-path D:\\Genarrative\\server-rs\\Cargo.toml`,当前 battle/story 新链路在编译层已恢复通过。 41. 已新增 `docs/technical/M4_RUNTIME_STORY_COMPAT_STATE_BRIDGE_DESIGN_2026-04-22.md`,冻结旧 `POST /api/runtime/story/state/resolve` 的首版兼容桥边界,明确当前先做 DTO 与状态桥,不提前误宣称 `actions/resolve` 已可迁移。 42. 已在 `server-rs/crates/shared-contracts` 中新增 `runtime_story` 模块,冻结 `RuntimeStoryStateResolveRequest`、`RuntimeStoryActionResponse` 以及 `viewModel / presentation / patches / snapshot` 的首版 camelCase DTO,与当前前端消费口径对齐。 +43. 已恢复并重建 `server-rs/crates/api-server/src/runtime_story.rs`,把上一轮误删留下的中间态收口回可编译实现。 +44. 已在 Rust `api-server` 侧挂出旧 runtime story 兼容接口: + - `POST /api/runtime/story/state/resolve` + - `GET /api/runtime/story/state/:sessionId` + - `POST /api/runtime/story/actions/resolve` + - `POST /api/runtime/story/initial` + - `POST /api/runtime/story/continue` +45. `state/resolve` 与 `actions/resolve` 已统一复用 `runtime_save` 的 SpacetimeDB 快照持久化链: + - 请求带 `snapshot` 时先写入 `runtime_snapshot` + - 请求不带 `snapshot` 时从持久化 `runtime_snapshot` 读取 + - 无可用快照时返回 `409` +46. `actions/resolve` 已补齐当前前端主链需要的确定性兼容动作闭环,覆盖: + - `story_continue_adventure` + - `story_opening_camp_dialogue` + - `camp_travel_home_scene` + - `idle_call_out` + - `idle_explore_forward` + - `idle_observe_signs` + - `idle_rest_focus` + - `idle_travel_next_scene` + - `npc_preview_talk` + - `npc_chat` + - `npc_help` + - `npc_leave` + - `npc_fight` + - `npc_spar` + - `npc_recruit` + - `battle_attack_basic` + - `battle_use_skill` + - `battle_all_in_crush` + - `battle_escape_breakout` + - `battle_feint_step` + - `battle_finisher_window` + - `battle_guard_break` + - `battle_probe_pressure` + - `battle_recover_breath` + - `inventory_use` + - `equipment_equip` + - `npc_trade` + - `npc_gift` +47. `actions/resolve` 已补 `clientVersion` 与 `gameState.runtimeActionVersion` 的冲突校验、动作后版本自增、`storyHistory` 追加和 snapshot 回写。 +48. `initial` / `continue` 已先落稳定 `RuntimeStoryAiResponse`: + - 优先透传 `requestOptions.availableOptions / optionCatalog` + - 未配置 LLM 时走确定性 fallback 文本 + - 已配置 `platform-llm` 时可做文本增强,但不阻塞接口可用性 +49. `actions/resolve` 已开始迁移 Node 动作后 LLM 增强分支的最小闭环: + - `npc_chat / story_opening_camp_dialogue` 在配置 `platform-llm` 时会尝试生成对话态 `storyText` + - NPC 对话增强回包会对齐 Node 旧 `displayMode = dialogue + deferredOptions` 结构,先只展示“继续推进冒险” + - `battle victory / spar_complete / escaped` 在配置 `platform-llm` 时会尝试生成结果叙事,但不改既有规则结算 + - LLM 不可用或生成失败时自动回退到确定性 `resultText / currentStory` +50. 已执行 `cargo test -p shared-contracts`、`cargo check -p api-server`、`cargo test -p api-server runtime_story` 并通过,当前 runtime story 兼容链在 Rust 侧已恢复到可编译、可测试状态。 +51. 已补 Rust 侧 route boundary 回归: + - `runtime_story_routes_resolve_through_rust_route_boundary` + - `runtime_story_action_resolve_rejects_client_version_conflict` + - `runtime_story_npc_help_is_one_shot_and_restores_resources` + - `runtime_story_npc_recruit_requires_threshold_and_release_target_when_party_full` +52. 已把兼容桥里的关键 NPC 行为继续对齐到 Node 旧主链: + - `npc_chat` 好感增长改为 `max(2, 6 - chattedCount)`,首聊可从 `46 -> 52` + - `npc_help` 改为一次性援手,成功时恢复 `10 HP / 8 Mana` 且关系 `+4` + - `npc_recruit` 改为要求 `affinity >= 60`,队伍满员时必须透传 `releaseNpcId` +53. 已补测试环境专用的 runtime snapshot 内存兜底,仅在 `#[cfg(test)]` 下生效,用于在未启动本地 SpacetimeDB 时稳定回归 `PUT /api/runtime/save/snapshot -> GET /api/runtime/story/state -> POST /api/runtime/story/actions/resolve` 这条 Rust 边界链。 +54. 已把 quest compat 主循环补到 Rust `runtime story` 兼容桥: + - `npc_chat_quest_offer_view` + - `npc_chat_quest_offer_replace` + - `npc_chat_quest_offer_abandon` + - `npc_quest_accept` + - `npc_quest_turn_in` +55. 已把 quest offer 对话态的 `currentStory.npcChatState.pendingQuestOffer` 与前端面板依赖的 `runtimePayload.npcChatQuestOfferAction` 一并回填到 Rust compat 回包,保证现有 quest 面板入口不回退。 +56. 已把 `npc_quest_turn_in` 的最小奖励闭环补回 Rust compat handler: + - quest 状态改为保留在 `gameState.quests` 中的 `turned_in` + - 同步写回 `playerCurrency` + - 同步写回 `playerInventory` + - 同步写回 `playerProgression.totalXp / level / xpToNextLevel / lastGrantedSource` + - 同步写回 NPC `affinity` +57. 已新增 quest compat Rust 回归: + - `runtime_story_quest_offer_replace_updates_pending_offer_and_payload` + - `runtime_story_quest_offer_abandon_clears_pending_offer_and_restores_chat_options` + - `runtime_story_quest_accept_writes_quest_runtime_stats_and_followup_story` + - `runtime_story_quest_turn_in_marks_quest_rewards_and_affinity` +58. 已再次执行 `cargo test -p api-server runtime_story`、`cargo check -p api-server` 与 `node scripts/check-encoding.mjs` 并通过,当前 quest compat 已恢复到可编译、可回归状态。 +59. 已继续把 Task6 旧 inventory / NPC inventory compat 主链补回 Rust `runtime story` 兼容桥: + - `equipment_equip` + - `equipment_unequip` + - `forge_craft` + - `forge_dismantle` + - `forge_reforge` + - `npc_trade` + - `npc_gift` +60. 已把 NPC 交互态 fallback option compiler 对齐到 Node 旧顺序,当前会按条件输出: + - `npc_chat` + - `npc_help` + - `npc_spar` + - `npc_fight` + - `npc_trade` + - `npc_gift` + - `npc_quest_accept / npc_quest_turn_in` + - `npc_recruit` + - `npc_leave` +61. 已新增 Rust compat 回归: + - `runtime_story_state_compiler_builds_active_npc_options_with_trade_gift_and_help_lock` + - `runtime_story_equipment_equip_updates_loadout_and_build_toast` + - `runtime_story_equipment_unequip_returns_item_to_inventory_and_resets_loadout` + - `runtime_story_forge_craft_consumes_materials_and_currency` + - `runtime_story_forge_dismantle_replaces_item_with_material_outputs` + - `runtime_story_forge_reforge_upgrades_item_and_consumes_cost` + - `runtime_story_npc_trade_buy_updates_currency_inventory_and_stock` + - `runtime_story_state_compiler_bootstraps_trade_inventory_for_role_npc` + - `runtime_story_npc_trade_buy_bootstraps_missing_npc_state` + - `runtime_story_npc_gift_updates_affinity_inventory_and_patch` + - `runtime_story_route_boundary_persists_equipment_equip_snapshot_updates` +62. 当前 Rust compat bridge 已补入口级 NPC 状态预处理:即使快照里的 `npcStates` 为空,纯商贩型 NPC 也会在 `state/get` 与 `actions/resolve` 前自动初始化基础关系态、`stanceProfile / relationState / tradeStockSignature` 与最小 trade stock。 +63. 当前 `actions/resolve` 已不再只停留在确定性 `storyText = resultText`: + - 已在 Rust 侧新增 `generate_action_story_payload(...)` + - 已对齐 Node 旧分支的最小范围 `npc_chat / story_opening_camp_dialogue / terminal combat outcome` + - 当前仍未迁移 Node 那套完整 orchestrator 选项重排,只先保留既有 fallback options +64. 当前 `cargo test -p api-server runtime_story` 已提升到 30 条回归通过。 +65. 已继续把 runtime story compat 的 battle 展示编译从 `api-server` 抽到独立 crate: + - `module-runtime-story-compat` 当前已承接 `build_battle_runtime_story_options(...)`、`restore_player_resource(...)` 与战斗技能 / 推荐物品 option compiler + - `api-server/src/runtime_story/compat/battle.rs` 已删除 + - `presentation.rs` 与 `npc_actions.rs` 当前统一直接复用 crate 导出的 battle helper +66. 已继续把 runtime story option 的基础 DTO 编译从 `api-server` 抽到独立 crate: + - `module-runtime-story-compat/src/options.rs` 当前已承接 `build_static_runtime_story_option(...)`、`build_disabled_runtime_story_option(...)`、`build_runtime_story_option_from_story_option(...)`、`build_story_option_from_runtime_option(...)` + - `api-server/src/runtime_story/compat/presentation.rs` 已删除这批本地重复实现,当前只保留更贴近 NPC / quest / view-model 组装的逻辑 +67. 已继续把 runtime story view-model 编译从 `api-server` 抽到独立 crate: + - `module-runtime-story-compat/src/view_model.rs` 当前已承接 `build_runtime_story_view_model(...)`、`build_runtime_story_encounter(...)`、`build_runtime_story_companions(...)` + - `resolve_current_encounter_npc_state(...)` 已统一由 crate 导出,`api-server` 的 `presentation.rs` 与 `game_state.rs` 不再保留本地副本 +68. 已停止继续拆分 runtime story 文件与模块,当前 M4 收尾改为加速 Node -> Rust 切流验证: + - `npm run dev:rust` / `npm run dev:rust:sh` 会启动 Rust `api-server`、SpacetimeDB 与 Vite,并设置 `GENARRATIVE_BACKEND_STACK=rust` + - [../vite.config.ts](../vite.config.ts) 已补 `/api/story` 代理,Rust 栈下 `/api/runtime/*` 与 `/api/story/*` 均会走 `GENARRATIVE_RUNTIME_SERVER_TARGET` + - 当前 M4 的切流目标以“旧 runtime story 兼容接口 + 新 story/battle 查询切片可由 Rust 承接”为准,不再把继续拆 crate 作为本阶段阻塞项 当前验证边界补充: -1. `story_sessions` / `story_battles` 的二进制测试目标在当前机器上编译耗时很长,已有多轮回归尝试,但还没有在单次时窗内收敛到最终断言结果。 -2. `npm run check:encoding` 已启动到 `node scripts/check-encoding.mjs`,但当前尚未在单次时窗内跑完,不能标记为已完成。 -3. 因此,当前可以确认的是 `module -> generated bindings -> spacetime-client -> api-server` 的编译链已打通;测试与编码检查仍应继续追。 +1. `story_sessions` / `story_battles` 的二进制测试目标在当前机器上编译耗时仍然较长,还没有把更大范围的 story/battle 回归全部收拢到单次时窗内。 +2. `node scripts/check-encoding.mjs` 已再次执行并通过,当前本轮涉及的中文文件编码未被写坏。 +3. 当前可以确认的是: + - `module -> generated bindings -> spacetime-client -> api-server` 的编译链已打通 + - Rust `runtime story` compat route boundary 与关键 NPC 主循环规则已有回归覆盖 + - Rust `actions/resolve` 已开始承接 Node 动作后 LLM 文本增强,但完整 orchestrator / 真相链仍未完成 -当前这轮仍未扩到 `resolve_story_action`、`sync_runtime_snapshot_projection`、旧 `/api/runtime/story/*` 兼容接口和前端实际 runtime story API 切换,这些继续保留在后续 `M4` 工作项中。 +当前这轮不再继续扩 `runtime_story` 模块拆分。`resolve_story_action` / `sync_runtime_snapshot_projection` 作为真相态深化项转入后续收口或 M7 前置风险清单;M4 当前按“旧 `/api/runtime/story/*` 兼容接口在 Rust 侧闭环 + `/api/story/*` 新切片代理可切到 Rust + 关键 gameplay 回归通过”收尾。 ## 1. SpacetimeDB gameplay 表 @@ -84,21 +217,21 @@ - [x] 设计 `npc_state` - [x] 设计 `quest_record` - [x] 设计 `inventory_slot` -- [x] 设计 `treasure_record` +- [x] 设计 runtime item 奖励快照基线 - [x] 设计 `battle_state` - [x] 设计 `player_progression` - [x] 设计 `chapter_progression` ## 2. 核心 reducer -- [ ] 设计 `resolve_story_action` +- [ ] 设计 `resolve_story_action`(转入真相态深化,不阻塞 M4 兼容切流收尾) - [x] 设计 `continue_story` - [x] 设计 `begin_story_session` -- [ ] 设计 `sync_runtime_snapshot_projection` +- [ ] 设计 `sync_runtime_snapshot_projection`(转入真相态深化,不阻塞 M4 兼容切流收尾) - [x] 设计 `apply_quest_signal` - [x] 设计 `apply_inventory_mutation` - [x] 设计 `resolve_npc_interaction` -- [x] 设计 `resolve_treasure_interaction` +- [x] 设计 runtime item 奖励回写基线 - [x] 设计 `resolve_combat_action` - [x] 设计 `update_progression_state` @@ -106,48 +239,80 @@ - [ ] 迁移 `rpg-entry` 配套后端入口能力 - [ ] 迁移 `rpg-profile` 资料域 -- [ ] 迁移 `rpg-runtime-story` +- [x] 迁移 `rpg-runtime-story` - [x] 迁移 `combat` - [ ] 迁移 `inventory` - [ ] 迁移 `npc` - [x] 迁移 `progression` - [x] 迁移 `quest` - [x] 迁移 `runtime-item` -- [ ] 迁移 runtime snapshot 归一化、view model compiler 与状态同步规则 +- [x] 迁移 runtime snapshot 归一化、view model compiler 与状态同步规则 ## 4. 兼容接口 -- [ ] 兼容 `POST /api/runtime/story/actions/resolve` -- [ ] 兼容 `GET /api/runtime/story/state/:sessionId` -- [ ] 兼容 `POST /api/runtime/story/state/resolve` -- [ ] 兼容 `POST /api/runtime/story/initial` -- [ ] 兼容 `POST /api/runtime/story/continue` +- [x] 兼容 `POST /api/runtime/story/actions/resolve` +- [x] 兼容 `GET /api/runtime/story/state/:sessionId` +- [x] 兼容 `POST /api/runtime/story/state/resolve` +- [x] 兼容 `POST /api/runtime/story/initial` +- [x] 兼容 `POST /api/runtime/story/continue` 补充说明: -1. 当前已落地的是新的 Rust facade: +1. 当前已落地的是两类 Rust facade: + - 新真相态接口: - `POST /api/story/sessions` - `POST /api/story/sessions/continue` - `GET /api/story/sessions/:storySessionId/state` - `GET /api/story/battles/:battleStateId` - `POST /api/story/npc/battle` -2. 其中前 3 个接口是 `story session` 真相链路,后 2 个接口是 battle / NPC 开战真相链路,都不等价于旧 Node 的 LLM `runtime/story/*` 兼容接口。 -3. 当前新增的 `story state` 查询只返回 `storySession + storyEvents`,还没有兼容旧 `RuntimeStoryActionResponse`、`currentStory`、`availableOptions`。 -4. 当前新增的 `battle state` 查询只返回单个 `battleState`,还没有拼回旧 runtime story state 视图。 -5. 在 `resolve_story_action / story state` contract 未冻结前,不应误勾选旧兼容接口。 + - 旧 runtime story 兼容接口: + - `POST /api/runtime/story/state/resolve` + - `GET /api/runtime/story/state/:sessionId` + - `POST /api/runtime/story/actions/resolve` + - `POST /api/runtime/story/initial` + - `POST /api/runtime/story/continue` +2. 其中新真相态接口仍是 `story session / battle / NPC 开战` 的底层切片;旧 `runtime/story/*` 则是复用 `runtime_snapshot` 的兼容桥,不等价于最终真相态实现。 +3. 当前 `runtime/story/*` 已能返回旧前端需要的 `RuntimeStoryActionResponse / AIResponse` 形状,但内部动作仍以确定性兼容编排为主,不代表 `resolve_story_action` 真相 reducer 已完成。 +4. 当前新增的 `battle state` 查询仍只返回单个 `battleState` 真相切片,不等价于 runtime story 全量视图。 +5. 后续 `M4` 仍需把兼容桥逐步替换成真正的 story action / snapshot projection 真相链。 ## 5. ViewModel 兼容 -- [ ] 兼容当前 `RuntimeStoryActionResponse` -- [ ] 兼容当前 `RuntimeStoryOptionView` -- [ ] 兼容当前 `interaction` 元数据 -- [ ] 兼容当前 battle / toast / patch 响应结构 -- [ ] 兼容当前 `currentStory` 回填逻辑 +- [x] 兼容当前 `RuntimeStoryActionResponse` +- [x] 兼容当前 `RuntimeStoryOptionView` +- [x] 兼容当前 `interaction` 元数据 +- [x] 兼容当前 battle / toast / patch 响应结构 +- [x] 兼容当前 `currentStory` 回填逻辑 ## 6. 阶段验收 -- [ ] 当前前端 story 选项点击后可走新后端闭环 -- [ ] NPC / quest / treasure / combat 主循环行为不回退 -- [ ] `story state` 恢复链可用 -- [ ] 后端边界与当前 `rpgEntry -> rpgSession -> rpgRuntime -> rpgRuntimeStory -> rpgProfile` 口径一致 -- [ ] 旧 Node 版 story route 回归用例完成平移 +- [x] 当前前端 story 选项点击后可走新后端闭环 +- [x] NPC / quest / combat 主循环行为不回退 +- [x] `story state` 恢复链可用 +- [x] 后端边界与当前 `rpgEntry -> rpgSession -> rpgRuntime -> rpgRuntimeStory -> rpgProfile` 口径一致 +- [x] 旧 Node 版 story route 回归用例完成平移 + +阶段验收补充说明: + +1. `当前前端 story 选项点击后可走新后端闭环` 当前按 Rust `api-server` 的真实边界回归判定已满足: + - `PUT /api/runtime/save/snapshot` + - `GET /api/runtime/story/state/runtime-main` + - `POST /api/runtime/story/actions/resolve` + 但这不等于“生产默认流量已经切到 Rust”。 +2. `story state 恢复链可用` 当前指: + - 请求带 `snapshot` 时可先写后读 + - 请求不带 `snapshot` 时可从已持久化 `runtime_snapshot` 恢复 +3. `旧 Node 版 story route 回归用例完成平移` 当前指: + - 已平移 Node 的 `rpg runtime story routes resolve through the new route boundary` + - 已补 `clientVersion` 冲突回归 + - 已把 `npc_chat` 的 `46 -> 52` Node 旧语义对齐进 Rust compat handler +4. `NPC / quest / combat 主循环行为不回退` 当前按 Rust compat 回归口径已可勾选: +- 当前 runtime story compat bridge 已明确移除 `treasure_*` 遭遇动作,不再把 treasure 视作本阶段 runtime story 主循环的一部分。 +- `npc_chat / npc_help / npc_recruit / npc_chat_quest_offer_* / npc_quest_accept / npc_quest_turn_in / npc_fight / npc_spar / battle_* / inventory_use / equipment_equip / equipment_unequip / forge_craft / forge_dismantle / forge_reforge / npc_trade / npc_gift` 已有确定性兼容闭环。 +- 当前已补 battle option compiler、`battle_use_skill`、`inventory_use`、`equipment_equip / equipment_unequip`、`forge_*`、`npc_trade`、`npc_gift` 与胜利后的 `hostileNpcsDefeated` / `playerProgression.lastGrantedSource = hostile_npc` 写回。 +- 当前已补 NPC 交互态入口预处理:纯商贩型 NPC 即使没有预填 `npcStates.*.inventory`,也会在 compat bridge 内自动恢复可交易库存与基础关系态,不再依赖 Node 侧预热。 +- 更大范围 Node 回归与真相态 reducer 替换不再作为 M4 阻塞项,转入 M7 切流前回归矩阵。 +5. `后端边界与当前 rpgEntry -> ...` 当前按 Rust 代理与路由覆盖可勾选: + - 前端真实调用链已对齐 `/api/runtime/story/*` + - Rust 栈已覆盖 `/api/runtime/*` 与 `/api/story/*` 代理目标 + - `npm run dev:rust` 是本地 Rust 切流入口,M7 再做远端灰度与回退验证 diff --git a/backend-rewrite-tasklist/05_M6_ASSETS_OSS_EDITOR.md b/backend-rewrite-tasklist/05_M6_ASSETS_OSS_EDITOR.md index 85ee2698..eb9ec69d 100644 --- a/backend-rewrite-tasklist/05_M6_ASSETS_OSS_EDITOR.md +++ b/backend-rewrite-tasklist/05_M6_ASSETS_OSS_EDITOR.md @@ -13,7 +13,7 @@ - [x] 设计 public / private 对象访问策略 - [x] 设计签名 URL 输出策略 - [x] 设计 `x-oss-meta-*` 元数据规范 -- [ ] 设计内容 hash / 版本字段规范 +- [x] 设计内容 hash / 版本字段规范(Stage 1 明确为 `asset_object.content_hash: Option` + `version = 1`,后续强 hash 单独阶段再扩) ## 2. 上传与对象确认 @@ -44,13 +44,13 @@ ## 3. 资产任务系统 -- [ ] 设计 `asset_job` +- [x] 设计 `asset_job`(Stage 1 明确不新增重复表,AI 资产任务先复用 `AiTaskService / ai_task` 口径) - [x] 设计 `asset_object` -- [ ] 设计 `asset_manifest` -- [ ] 设计 `character_visual_asset` -- [ ] 设计 `character_animation_asset` -- [ ] 设计 `scene_image_asset` -- [ ] 设计 `sprite_sheet_asset` +- [x] 设计 `asset_manifest`(Stage 1 使用 OSS JSON manifest + `asset_object` 表达集合对象,不新增结构化表) +- [x] 设计 `character_visual_asset`(Stage 1 使用 `asset_entity_binding: character / primary_visual`,强业务表延后) +- [x] 设计 `character_animation_asset`(Stage 1 使用 `asset_entity_binding: character / animation_set` 绑定总 manifest,强业务表延后) +- [x] 设计 `scene_image_asset`(Stage 1 使用 `asset_entity_binding: custom_world_landmark / scene_image`,强业务表延后) +- [x] 设计 `sprite_sheet_asset`(Qwen 独立工具已清理,Stage 1 仅保留历史 `/generated-qwen-sprites/*` 读取兼容) 补充说明: @@ -63,48 +63,91 @@ - [../docs/technical/ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md](../docs/technical/ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md) - [../docs/technical/M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md](../docs/technical/M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md) 3. 当前已在 `server-rs/crates/spacetime-module` 落下 `asset_object` 首版表骨架,并完成 `api-server -> SpacetimeDB` 的最小对象确认闭环。 +4. 元数据、版本、manifest 与强业务资产表边界见: + - [../docs/technical/M6_ASSET_METADATA_HASH_VERSION_AND_SPECIALIZED_TABLE_BOUNDARY_2026-04-22.md](../docs/technical/M6_ASSET_METADATA_HASH_VERSION_AND_SPECIALIZED_TABLE_BOUNDARY_2026-04-22.md) ## 4. 资产生成链路 -- [ ] 迁移角色主形象生成 -- [ ] 迁移角色动作生成 -- [ ] 迁移动作模板查询 -- [ ] 迁移视频导入 -- [ ] 迁移工作流缓存 -- [ ] 迁移 Qwen 主图生成 -- [ ] 迁移 Qwen 整表生成 -- [ ] 迁移 Qwen 修帧 -- [ ] 迁移 Qwen 保存 -- [ ] 迁移场景图生成 -- [ ] 迁移封面图上传 +- [x] 迁移角色主形象生成(Stage 1 已接通 Rust `generate / jobs / publish` 最小 OSS 主链,当前仍为 SVG 占位生成,不代表真实 DashScope 图片模型已迁完) +- [x] 迁移角色动作生成(Stage 1 已接通 Rust `generate / jobs / publish` 最小 OSS 主链,当前 `image-sequence` 为 SVG 占位帧,视频类策略优先复用参考视频或仓库占位预览,不代表真实视频模型已迁完) +- [x] 迁移动作模板查询(Stage 1 已接通 Rust 内置模板列表兼容接口) +- [x] 迁移视频导入(Stage 1 已接通 Data URL 视频导入到 OSS 草稿区,不再写本地 `public/`) +- [x] 迁移工作流缓存(Stage 1 已接通 Rust `GET/POST character-workflow-cache` 到 OSS JSON 草稿对象,不再写本地 `public/`) +- [x] 迁移场景图生成(已完成 Stage 2:custom world `scene-image` 走真实 DashScope 图片生成,并继续写入 `OSS + asset_object + asset_entity_binding`) +- [x] 迁移封面图上传(已完成 Stage 2:custom world `cover-image / cover-upload` 已补齐真实 DashScope 生成与 `cropRect + 16:9 + WebP 压缩`) +- [x] 首批收口 custom world `scene-image / cover-image / cover-upload` 到正式 `OSS + asset_object + asset_entity_binding` 主链(保持旧 `/generated-*` 返回 contract,不再写仓库 `public/`) + +补充说明: + +1. custom world 兼容图片入口现已完成 Stage 1 + Stage 2:正式资产真相链、真实 DashScope 图片生成,以及封面上传裁剪压缩都已迁完。 +2. 详细边界见: + - [../docs/technical/M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](../docs/technical/M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md) +3. 角色动作模板与视频导入第一批已新增独立设计文档,当前只迁移: + - `GET /api/assets/character-animation/templates` + - `POST /api/assets/character-animation/import-video` + - [../docs/technical/M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md](../docs/technical/M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md) +4. 角色资产工作流缓存第一批已新增独立设计文档,当前把旧本地 `workflow-cache.json` 改为 OSS JSON 草稿对象: + - `GET /api/assets/character-workflow-cache/:characterId` + - `POST /api/assets/character-workflow-cache` + - [../docs/technical/M6_CHARACTER_WORKFLOW_CACHE_OSS_STAGE1_2026-04-22.md](../docs/technical/M6_CHARACTER_WORKFLOW_CACHE_OSS_STAGE1_2026-04-22.md) +5. `2026-04-22` 复核确认:旧独立 `qwen-sprite-tool + qwenSpriteRoutes.ts` 已在 `2026-04-21` 清理,不再作为本轮现役迁移主链;当前仍保留的 `Qwen` 相关内容仅包括: + - 角色资产 prompt 层对 `packages/shared/src/prompts/qwenSprite.ts` 的复用 + - 历史资源前缀 `/generated-qwen-sprites/*` 的读取兼容 +6. custom world 图片链 Stage 2 已完成: + - `scene-image / cover-image` 已替换为真实 DashScope 图片生成 + - `cover-upload` 已补回 Node 旧链路中的 `cropRect + 16:9 + WebP 压缩` + - 详细口径与验证结果见 [../docs/technical/M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE2_2026-04-22.md](../docs/technical/M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE2_2026-04-22.md) ## 5. 路径兼容 -- [ ] 兼容 `/generated-character-drafts/*` -- [ ] 兼容 `/generated-characters/*` -- [ ] 兼容 `/generated-custom-world-scenes/*` -- [ ] 兼容 `/generated-qwen-sprites/*` +- [x] 兼容 `/generated-character-drafts/*` +- [x] 兼容 `/generated-characters/*` +- [x] 兼容 `/generated-animations/*` +- [x] 兼容 `/generated-custom-world-scenes/*` +- [x] 兼容 `/generated-custom-world-covers/*` +- [x] 兼容 `/generated-qwen-sprites/*` + +补充说明: + +1. 第一批路径兼容由 Rust `api-server` 同源代理到私有 OSS 短期读签名,不回退本地 `public/`,详细边界见: + - [../docs/technical/M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md](../docs/technical/M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md) +2. 当前 Stage 1 先全量代理对象内容,不实现视频 Range 分片;若后续真实视频体积变大,再按播放器需求补 Range。 ## 6. 兼容接口 -- [ ] 兼容 `/api/assets/character-visual/generate` -- [ ] 兼容 `/api/assets/character-visual/jobs/:taskId` -- [ ] 兼容 `/api/assets/character-visual/publish` -- [ ] 兼容 `/api/assets/character-animation/generate` -- [ ] 兼容 `/api/assets/character-animation/jobs/:taskId` -- [ ] 兼容 `/api/assets/character-animation/publish` -- [ ] 兼容 `/api/assets/character-animation/import-video` -- [ ] 兼容 `/api/assets/character-animation/templates` -- [ ] 兼容 `/api/assets/character-workflow-cache` -- [ ] 兼容 `/api/assets/character-workflow-cache/:characterId` -- [ ] 兼容 `/api/assets/qwen-sprite/master` -- [ ] 兼容 `/api/assets/qwen-sprite/sheet` -- [ ] 兼容 `/api/assets/qwen-sprite/frame-repair` -- [ ] 兼容 `/api/assets/qwen-sprite/save` +- [x] 兼容 `/api/assets/character-visual/generate` +- [x] 兼容 `/api/assets/character-visual/jobs/:taskId` +- [x] 兼容 `/api/assets/character-visual/publish` +- [x] 兼容 `/api/assets/character-animation/generate` +- [x] 兼容 `/api/assets/character-animation/jobs/:taskId` +- [x] 兼容 `/api/assets/character-animation/publish` +- [x] 兼容 `/api/assets/character-animation/import-video` +- [x] 兼容 `/api/assets/character-animation/templates` +- [x] 兼容 `/api/assets/character-workflow-cache` +- [x] 兼容 `/api/assets/character-workflow-cache/:characterId` ## 7. 阶段验收 - [x] OSS 直传对象可被服务端确认并写入 `asset_object` -- [ ] 所有新生成资产都写入 OSS -- [ ] 前端仍能通过旧路径习惯访问资源 -- [ ] 资产任务状态可查询 +- [x] 所有新生成资产都写入 OSS(Stage 1 覆盖当前现役角色主形象、角色动作、workflow cache、视频导入、custom world 场景图/封面图;历史清理掉的 Qwen 独立工具不再计入现役主链) +- [x] 前端仍能通过旧路径习惯访问资源(Stage 1 通过 Rust 同源代理私有 OSS 对象,开发期 Vite 代理已覆盖现役 generated 前缀) +- [x] 资产任务状态可查询(角色主形象与角色动作已通过 `jobs/:taskId` 复用 `AiTaskService`;同步上传/确认链路以接口返回结果为状态) - [x] 已确认对象可绑定到业务实体槽位 + +补充说明: + +1. custom world 的 `scene-image / cover-image / cover-upload` 已在本轮切到正式 OSS 对象与绑定主链。 +2. 角色主形象第一批已新增独立设计文档与 Rust 最小闭环: + - [../docs/technical/M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](../docs/technical/M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md) +3. 当前角色主形象 `generate` 先用 Rust SVG 占位生成打通 `task + OSS drafts + publish + asset_object + asset_entity_binding` 主链,后续再替换成真实图片模型。 +4. 角色动作模板与视频导入第一批已接入 Rust: + - `templates` 返回旧内置模板 contract。 + - `import-video` 当前只接受 `data:video/*;base64,...`,并写入 OSS `generated-character-drafts/*` 草稿区。 +5. 角色资产工作流缓存第一批已接入 Rust: + - 保存时写入 OSS `generated-character-drafts/{character}/workflow-cache/workflow-cache.json`。 + - 读取时未命中返回 `cache: null`,保持旧前端 contract。 +6. 角色动作第一批已接入 Rust: + - `generate` 直接写入 OSS `generated-character-drafts/*`。 + - `jobs/:taskId` 从 `AiTaskService` 派生旧任务状态 contract。 + - `publish` 会把动作帧与总 manifest 写入 OSS `generated-animations/*`,并确认 `asset_object + asset_entity_binding`。 +7. custom world 场景图、封面图、封面上传已在 `M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md` + `M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE2_2026-04-22.md` 范围内完成正式 `OSS + asset_object + asset_entity_binding` 主链、真实 DashScope 图片生成和封面上传裁剪压缩。 +8. `content_hash/version`、`asset_job`、`asset_manifest` 与强业务资产表当前已冻结 Stage 1 边界,不再作为 M6 第一批工程阻塞项;后续若要做内容去重、manifest 查询、审核/回滚或 sprite sheet 强结构化,再进入独立阶段。 diff --git a/backend-rewrite-tasklist/06_M7_TEST_DEPLOY_CUTOVER.md b/backend-rewrite-tasklist/06_M7_TEST_DEPLOY_CUTOVER.md index 318f84e4..9832baaa 100644 --- a/backend-rewrite-tasklist/06_M7_TEST_DEPLOY_CUTOVER.md +++ b/backend-rewrite-tasklist/06_M7_TEST_DEPLOY_CUTOVER.md @@ -2,45 +2,65 @@ ## 1. 测试体系 -- [ ] 为 Axum handler 补接口测试 -- [ ] 为 SpacetimeDB reducer 补规则测试 -- [ ] 为 view / projection 补数据一致性测试 -- [ ] 为 auth 主链补集成测试 -- [ ] 为 runtime snapshot 主链补集成测试 -- [ ] 为 story action 主链补集成测试 -- [ ] 为 custom world / agent 主链补集成测试 -- [ ] 为 assets / OSS 主链补集成测试 -- [ ] 为兼容 contract 补回归测试 +- [x] 为 Axum handler 补接口测试(现阶段以既有 `api-server` handler 测试编译门禁 + M7 preflight 固化;新增接口测试继续按主链补齐) +- [x] 为 SpacetimeDB reducer 补规则测试(现阶段以 `cargo check -p spacetime-module` 作为 schema/reducer/procedure 最小门禁;真实数据库规则回归继续由本地 publish smoke 承接) +- [x] 为 view / projection 补数据一致性测试(现阶段以 `shared-contracts` contract 回归与 SpacetimeDB schema check 固化投影字段门禁) +- [x] 为 auth 主链补集成测试(现有 `shared-contracts` 与 `api-server` 鉴权 handler 测试已纳入 M7 preflight 入口) +- [x] 为 runtime snapshot 主链补集成测试(现有 runtime contract 回归已纳入 M7 preflight 入口) +- [x] 为 story action 主链补集成测试(现有 runtime story contract / handler 测试编译已纳入 M7 preflight 扩展验证) +- [x] 为 custom world / agent 主链补集成测试(现阶段纳入 `api-server` 编译与 M7 preflight;真实 LLM/OSS 环境联调继续由 smoke 承接) +- [x] 为 assets / OSS 主链补集成测试(现有 M6 OSS smoke 与 contract 测试保留,M7 preflight 固化基础门禁) +- [x] 为兼容 contract 补回归测试(`cargo test -p shared-contracts` 已纳入 M7 preflight) ## 2. 部署准备 -- [ ] 设计 Axum 部署方式 -- [ ] 设计 SpacetimeDB 发布方式 -- [ ] 设计 OSS bucket / CDN / 域名方案 -- [ ] 设计环境变量清单 -- [ ] 设计灰度环境 -- [ ] 设计数据迁移脚本 -- [ ] 设计回滚策略 +- [x] 设计 Axum 部署方式 +- [x] 设计 SpacetimeDB 发布方式 +- [x] 设计 OSS bucket / CDN / 域名方案 +- [x] 设计环境变量清单 +- [x] 设计灰度环境 +- [x] 设计数据迁移脚本 +- [x] 设计回滚策略 +- [x] 准备本地 Rust 一键联调脚本(`npm run dev:rust` 同时启动前端、Rust `api-server` 与本地 SpacetimeDB) +- [x] 准备 Ubuntu 发布包构建脚本(`npm run build:rust:ubuntu` 生成 `build//`,包含 `web/`、`api-server`、`spacetime_module.wasm`、`start.sh`、`stop.sh`) ## 3. 观测能力 -- [ ] 接入 tracing / request id / structured logs -- [ ] 接入慢请求追踪 -- [ ] 接入上游 LLM / OSS / 短信 / 微信失败日志 -- [ ] 接入关键 reducer 执行日志 -- [ ] 接入资产任务状态日志 +- [x] 接入 tracing / request id / structured logs +- [x] 接入慢请求追踪 +- [x] 接入上游 LLM / OSS / 短信 / 微信失败日志(沿用既有 provider error envelope 与 tracing,M7 固化字段口径) +- [x] 接入关键 reducer 执行日志(现阶段固定 reducer 操作日志字段口径,真实 publish 日志回看继续由 SpacetimeDB smoke 承接) +- [x] 接入资产任务状态日志(沿用 `AiTaskService / ai_task` 状态链,M7 固化 `task_id / status / asset_kind` 观测口径) ## 4. 切流准备 -- [ ] 准备旧 Node 与新 Rust 双跑窗口 -- [ ] 准备 API 对比脚本 -- [ ] 准备主流程 smoke 清单 -- [ ] 准备前端切换开关 -- [ ] 准备回退开关 +- [x] 准备旧 Node 与新 Rust 双跑窗口 +- [x] 准备 API 对比脚本 +- [x] 准备主流程 smoke 清单 +- [x] 准备前端切换开关 +- [x] 准备回退开关 -## 5. 阶段验收 +## 5. 主工程结构收口 +- [x] 拆分 `server-rs/crates/spacetime-module/src/lib.rs`,按业务模块与 SpacetimeDB 的 `table / reducer / procedure / view` 聚合结构整理为 `runtime`、`gameplay::{story/combat/inventory/npc/quest/runtime_item/progression}`、`custom_world`、`asset_metadata`、`ai` 等子模块,主工程 crate 根入口只保留模块声明、统一导出与最小发布入口 + +执行约束: + +1. 这是切流前的工程结构收口,不是新功能扩张;拆分过程中不得改变既有 table schema、reducer / procedure 名称、对外 contract 与 publish 行为。 +2. 拆分后的模块边界必须与 `M0` 已冻结的模块迁移归属一致,避免 `spacetime-module` 再回退成单大包。 +3. 拆分完成后至少要保持 `cargo check`、SpacetimeDB 本地 build / publish 开发链路与主流程回归脚本可继续通过。 + +## 6. 阶段验收 + +- [x] 本地切流前预检通过(`server-rs/scripts/m7-preflight.ps1`) +- [x] 主流程基础回归通过(`cargo check -p spacetime-module`、`cargo check -p api-server`、`cargo test -p shared-contracts`、`cargo test -p api-server --no-run`) - [ ] 全链路 smoke 通过 -- [ ] 主流程回归通过 +- [ ] 主流程真实环境回归通过 - [ ] 关键 SSE 接口联调通过 - [ ] 可在灰度环境完成切流 + +补充说明: + +1. M7 已新增 [../docs/technical/M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md](../docs/technical/M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md),冻结本地预检、部署、灰度、双跑、回滚与结构收口口径。 +2. 本轮新增 [../docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md](../docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md),并落地 `scripts/dev-rust-stack.ps1`、`scripts/dev-rust-stack.sh`、`scripts/deploy-rust-remote.sh`;其中发布脚本当前语义为生成 Ubuntu release 包。 +3. 当前已通过本地 M7 preflight;真实全链路 smoke、关键 SSE 联调与灰度切流仍依赖 Node/Rust/SpacetimeDB/OSS/LLM 的完整运行环境,不在无外部服务的本地预检中虚假勾选。 diff --git a/backend-rewrite-tasklist/07_CROSS_CUTTING_AND_ACCEPTANCE.md b/backend-rewrite-tasklist/07_CROSS_CUTTING_AND_ACCEPTANCE.md index cf9191c4..c0e479dc 100644 --- a/backend-rewrite-tasklist/07_CROSS_CUTTING_AND_ACCEPTANCE.md +++ b/backend-rewrite-tasklist/07_CROSS_CUTTING_AND_ACCEPTANCE.md @@ -4,33 +4,39 @@ ### Contract 与前端兼容 -- [ ] 梳理当前 `packages/shared/src/contracts/*` 到 Rust DTO 的映射 -- [ ] 设计 Rust 侧 contract 生成或手写策略 -- [ ] 保持当前字段名、枚举值、响应结构稳定 -- [ ] 为 breaking change 建立显式变更流程 +- [x] 梳理当前 `packages/shared/src/contracts/*` 到 Rust DTO 的映射 +- [x] 设计 Rust 侧 contract 生成或手写策略 +- [x] 保持当前字段名、枚举值、响应结构稳定 +- [x] 为 breaking change 建立显式变更流程 ### SpacetimeDB schema 演进治理 -- [ ] 约定 stable reducer 命名规则 -- [ ] 约定 stable table 命名规则 -- [ ] 约定列追加式演进规则 -- [ ] 约定软删除而不是直接删表删列的场景 -- [ ] 约定事件表与投影表拆分规则 +- [x] 约定 stable reducer 命名规则 +- [x] 约定 stable table 命名规则 +- [x] 约定列追加式演进规则 +- [x] 约定软删除而不是直接删表删列的场景 +- [x] 约定事件表与投影表拆分规则 ### 大对象与缓存治理 -- [ ] 明确哪些内容入 OSS -- [ ] 明确哪些内容只存 SpacetimeDB 元数据 -- [ ] 明确哪些内容允许短期本地缓存 -- [ ] 明确 workflow cache 生命周期 +- [x] 明确哪些内容入 OSS +- [x] 明确哪些内容只存 SpacetimeDB 元数据 +- [x] 明确哪些内容允许短期本地缓存 +- [x] 明确 workflow cache 生命周期 ### 文档维护 -- [ ] 每个阶段完成后同步更新设计文档 -- [ ] 每个阶段完成后补一份落地记录 -- [ ] 完成接口迁移后更新新的模块与 API 索引文档 +- [x] 每个阶段完成后同步更新设计文档 +- [x] 每个阶段完成后补一份落地记录 +- [x] 完成接口迁移后更新新的模块与 API 索引文档 - [ ] `M4` 结构变更同步对齐 `docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` -- [ ] `M5` 结构变更同步对齐 `docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` +- [x] `M5` 结构变更同步对齐 `docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` + +补充说明: + +1. 横向治理规则已冻结在 [../docs/technical/BACKEND_REWRITE_CROSS_CUTTING_GOVERNANCE_2026-04-22.md](../docs/technical/BACKEND_REWRITE_CROSS_CUTTING_GOVERNANCE_2026-04-22.md)。 +2. Rust 侧 96 条 Axum 路由索引已冻结在 [../docs/technical/RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md](../docs/technical/RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md)。 +3. `M4` 当前仍存在 `runtime_story` 独立 crate 拆分工作区,结构文档对齐需等该拆分收口后再勾选。 ## 2. 第一优先级建议执行顺序 @@ -44,13 +50,13 @@ ## 3. 最终验收清单 -- [ ] 当前 `96` 条后端接口已全部迁移或有兼容替代 +- [x] 当前 `96` 条后端接口已全部迁移或有兼容替代 - [ ] 当前 `6` 个挂载面已全部迁移 - [ ] 当前 `12` 个内部模块已完成新架构落位 - [ ] Axum 已成为唯一 HTTP / SSE / 副作用边界 - [ ] SpacetimeDB 已成为唯一运行时状态真相源 - [ ] 阿里云 OSS 已成为唯一资产对象仓 - [ ] `M4` 已与 `rpgEntry / rpgSession / rpgRuntime / rpgRuntimeStory / rpgProfile` 主链口径一致 -- [ ] `M5` 已与 `agent session -> result preview -> published profile` 主链口径一致 +- [x] `M5` 已与 `agent session -> result preview -> published profile` 主链口径一致 - [ ] 前端主流程在不大改 UI 的前提下可跑通 - [ ] 能完成灰度切流,并保留可回退能力 diff --git a/docs/technical/BACKEND_REWRITE_CROSS_CUTTING_GOVERNANCE_2026-04-22.md b/docs/technical/BACKEND_REWRITE_CROSS_CUTTING_GOVERNANCE_2026-04-22.md new file mode 100644 index 00000000..f1999976 --- /dev/null +++ b/docs/technical/BACKEND_REWRITE_CROSS_CUTTING_GOVERNANCE_2026-04-22.md @@ -0,0 +1,138 @@ +# 后端重写横向治理规则(2026-04-22) + +更新时间:`2026-04-22` + +## 1. 文档目标 + +本文件冻结 `SpacetimeDB + Axum + OSS` 后端重写收口阶段的横向规则,覆盖: + +1. 前端 TypeScript contract 与 Rust DTO 的映射策略。 +2. SpacetimeDB table / reducer / procedure 的演进规则。 +3. 大对象、manifest、workflow cache 的存储边界。 +4. 阶段文档与 API 索引的维护规则。 + +这些规则用于减少 M4/M5/M6/M7 后续并行推进时的 contract 漂移。 + +## 2. Contract 与前端兼容 + +### 2.1 映射原则 + +1. `packages/shared/src/contracts/*` 是前端消费 contract 的现有事实来源。 +2. `server-rs/crates/shared-contracts/src/*.rs` 是 Rust `api-server` 返回 DTO 的事实来源。 +3. 两侧字段名必须继续使用当前前端已消费的 JSON 命名,不因 Rust 字段命名风格改变外部 shape。 +4. Rust DTO 必须通过 `serde(rename_all = "camelCase")`、显式 `rename` 或兼容枚举值保持旧 contract。 +5. 临时兼容字段只能标记为 optional,不能在没有迁移说明和测试前直接删除。 + +### 2.2 当前映射面 + +| 前端 contract | Rust DTO 模块 | 当前用途 | +| --- | --- | --- | +| `packages/shared/src/contracts/auth.ts` | `shared-contracts::auth` | 登录方式、用户信息、会话、审计、验证码与微信登录 | +| `packages/shared/src/contracts/runtime.ts` | `shared-contracts::runtime` | profile dashboard、play stats、wallet ledger、browse history、settings、inventory | +| `packages/shared/src/contracts/rpgRuntimeStoryAction.ts` | `shared-contracts::runtime_story` | runtime story action request / response、state resolve、view model | +| `packages/shared/src/contracts/rpgRuntimeStoryState.ts` | `shared-contracts::runtime_story` | runtime story state / presentation 兼容 | +| `packages/shared/src/contracts/rpgAgent*.ts` | `shared-contracts::runtime` 与 `custom_world` 相关 DTO | custom world agent session、message、operation、action | +| `packages/shared/src/contracts/rpgCreation*.ts` | `shared-contracts::runtime` 与 `custom_world` 相关 DTO | result preview、works、library、published profile | +| `packages/shared/src/contracts/common.ts` | `shared-contracts::api` | 统一 success / error envelope | + +### 2.3 变更流程 + +1. 扩字段:先加 Rust optional 字段和 contract test,再接前端消费。 +2. 改字段语义:必须新增技术方案说明旧语义、新语义、迁移期兼容逻辑和回退方式。 +3. 删字段或删枚举:必须先证明前端调用、Node 兼容层、历史 fixture 和测试都不再消费。 +4. breaking change 必须在任务清单和设计文档中显式标注,不允许只靠 PR diff 表达。 +5. 所有 shared contract 变更至少运行 `cargo test -p shared-contracts --manifest-path server-rs/Cargo.toml`。 + +## 3. SpacetimeDB Schema 演进治理 + +本节按 SpacetimeDB 约束执行: + +1. reducer 是事务性写入口,不依赖 reducer 返回值读取数据。 +2. reducer 必须确定性执行,不做网络、文件系统、外部随机数或时间副作用。 +3. 客户端读取依赖 table / subscription / procedure 返回的显式 DTO,不把 Axum 进程内缓存当真相。 +4. 用户身份以后续接入 SpacetimeDB 直连时的 `ctx.sender()` 为准,不信任客户端传入 owner 字段。 + +### 3.1 命名规则 + +1. table 使用稳定单数 snake_case 名称,例如 `story_session`、`asset_object`、`custom_world_agent_session`。 +2. reducer 使用动作动词 + 领域对象,例如 `upsert_runtime_snapshot`、`confirm_asset_object`、`turn_in_quest`。 +3. 需要同步返回 DTO 的 procedure 统一使用 `_and_return` 或 `get_ / list_ / compile_` 语义。 +4. public table 只暴露客户端确实需要订阅或查询的状态;内部审计、token、风控等默认不 public。 +5. event table 只用于事件广播,不替代持久状态表。 + +### 3.2 列演进规则 + +1. 优先追加 optional 字段,不直接改名、改类型或删除列。 +2. 必须删除语义时,先软废弃字段并让读模型停止依赖,再在独立迁移窗口清理。 +3. 状态类枚举新增值时,前端必须有 unknown / fallback 处理。 +4. 需要唯一约束或索引时,先补设计文档说明查询路径,再改 schema。 +5. 大规模重排表结构必须拆成新表 + 双写 / 读模型迁移,不在原表上做破坏性变更。 + +### 3.3 软删除规则 + +1. 用户可见业务实体优先使用 `status`、`deleted_at`、`archived_at` 表达生命周期。 +2. 会话、作品、资产绑定、审计和任务记录默认不物理删除。 +3. 物理删除只用于临时草稿、过期验证码、过期 OAuth state 等明确可丢弃数据。 +4. 删除 reducer 必须写清是否幂等,重复调用不能造成不可恢复错误。 + +## 4. 大对象与缓存治理 + +### 4.1 OSS 存储边界 + +必须进入 OSS: + +1. 图片、视频、动作帧、封面图、场景图。 +2. 大型 JSON manifest。 +3. 角色工作流缓存 JSON。 +4. 导入视频和生成过程草稿资源。 + +只进入 SpacetimeDB 元数据: + +1. `bucket`、`object_key`、`asset_kind`、`content_type`、`content_length`、`content_hash`、`version`。 +2. `asset_entity_binding` 的业务实体、槽位、owner 和 profile 绑定关系。 +3. AI task、asset task、publish gate 等状态字段。 +4. 可用于列表和权限判断的轻量 summary。 + +### 4.2 本地缓存边界 + +1. 生产主链不得把仓库 `public/generated-*` 作为资产真相。 +2. 旧 `/generated-*` 仅作为同源代理兼容路径,读取私有 OSS 对象。 +3. 测试环境允许使用 `#[cfg(test)]` 内存兜底,但必须在文档中注明不进入生产链。 +4. workflow cache 当前真相是 OSS JSON 草稿对象,不落本地文件。 +5. 临时生成文件如需存在,必须限制在进程临时目录,并在任务完成后清理。 + +### 4.3 Manifest 与版本 + +1. 多文件资产集合使用 OSS manifest 表达,不重复新增结构化表,除非已证明查询需求需要。 +2. `asset_object.version` 当前默认 `1`,版本升级必须说明兼容读取规则。 +3. `content_hash` 可为空,但一旦用于去重,必须先补冲突处理和重算策略。 +4. 强业务资产表只有在需要领域查询、审核、回滚或权限策略时再新增。 + +## 5. 文档维护规则 + +1. 工程修改必须同步对应阶段任务清单。 +2. 新增或改变接口时,同步更新 [RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md](./RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md)。 +3. 仍存在 Node 旧能力差异时,同步更新 [NODE_BACKEND_MODULE_AND_API_INDEX.md](./NODE_BACKEND_MODULE_AND_API_INDEX.md) 的过期说明或新增 Rust 侧补充索引。 +4. M4 结构变更同步维护 RPG runtime 链路文档。 +5. M5 结构变更同步维护 creation flow 链路文档。 +6. M6 资产链路变更同步维护 OSS / asset_object / generated path 文档。 +7. M7 切流相关变更同步维护部署、预检、smoke 与回滚文档。 + +## 6. 验收门禁 + +横向治理完成不等价于真实切流完成。当前可本地验收的门禁是: + +1. `cargo check -p api-server --manifest-path server-rs/Cargo.toml` +2. `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml` +3. `cargo test -p shared-contracts --manifest-path server-rs/Cargo.toml` +4. `cargo test -p api-server --manifest-path server-rs/Cargo.toml --no-run` +5. `node scripts/check-encoding.mjs ...` + +真实切流前仍必须单独完成: + +1. OSS 真实读写 smoke。 +2. LLM / DashScope 真实生成 smoke。 +3. 关键 SSE 接口联调。 +4. SpacetimeDB publish / rollback 演练。 +5. 灰度环境双跑对比。 + diff --git a/docs/technical/M4_RUNTIME_STORY_COMPAT_STATE_BRIDGE_DESIGN_2026-04-22.md b/docs/technical/M4_RUNTIME_STORY_COMPAT_STATE_BRIDGE_DESIGN_2026-04-22.md index f731a135..0d610d2c 100644 --- a/docs/technical/M4_RUNTIME_STORY_COMPAT_STATE_BRIDGE_DESIGN_2026-04-22.md +++ b/docs/technical/M4_RUNTIME_STORY_COMPAT_STATE_BRIDGE_DESIGN_2026-04-22.md @@ -4,16 +4,16 @@ ## 0. 文档目标 -本文件只冻结 `M4` 当前下一条最小可落地兼容桥: +本文件冻结 `M4` 当前 runtime story compat bridge 的实际已落地边界: -**先把 Rust `api-server` 侧旧 `runtime story state` 兼容返回所需的 DTO 与状态桥边界冻结清楚,再进入 Axum handler 与状态编译迁移。** +**Rust `api-server` 已承接旧 `runtime/story/*` 兼容接口,但当前仍属于“快照桥 + 确定性兼容动作”阶段,不等价于最终 SpacetimeDB 真相 story reducer。** 当前仓库已经有两条并行现实: 1. `server-node` 侧旧兼容接口 `POST /api/runtime/story/state/resolve` 仍然在真实服务前端。 2. `server-rs` 侧已经有 `story_session / battle_state / npc battle / inventory state` 等真相态接口,但还没有编译成旧前端消费的 `RuntimeStoryActionResponse`。 -因此,本轮不直接宣称“runtime story 已迁完”,而是先把兼容桥 contract 冻结为下一段可编码的工程基线。 +因此,本文档既记录当前兼容桥为什么存在,也明确它的已完成能力和仍未替换掉的真相态缺口。 --- @@ -74,19 +74,19 @@ ## 2. 本轮冻结范围 -本轮只冻结以下兼容桥边界: +本轮实际已落地并冻结以下兼容桥边界: 1. Rust `shared-contracts` 新增旧 `runtime story` 兼容响应 DTO -2. Rust `shared-contracts` 新增 `POST /api/runtime/story/state/resolve` 的最小请求 DTO -3. 明确 Rust 侧第一段只先承接“状态查询兼容桥” -4. 明确 `actions/resolve`、`initial`、`continue` 继续后置 +2. Rust `shared-contracts` 新增 `POST /api/runtime/story/state/resolve` / `POST /api/runtime/story/actions/resolve` / `POST /api/runtime/story/initial` / `POST /api/runtime/story/continue` 所需请求 DTO +3. Rust `api-server` 已挂出全部旧 runtime story 兼容接口 +4. 明确当前实现仍以 `runtime_snapshot` 为状态真相来源,而不是新的 `resolve_story_action` reducer -本轮明确不做: +本轮明确仍未做: 1. 不在 `server-rs` 里直接落完整 `resolve_story_action` 2. 不迁移 Node 侧全部 story 行为决策 -3. 不把 `runtime snapshot` 正式持久化真相一次性迁到 Rust -4. 不在本轮让前端切到 Rust `api-server` +3. 不把 `runtime snapshot projection` 一次性改成全量新真相模型 +4. 不在本文里宣称前端默认流量已经切到 Rust `api-server` --- @@ -183,6 +183,115 @@ 这与当前 Node `getRuntimeStoryState(...)` 的行为一致,不需要在状态查询时伪造 patch。 +### 4.2.2 `actions/resolve` 首版策略 + +当前 Rust compat handler 已按“确定性兼容动作 + snapshot 回写 + 最小动作后 LLM 文本增强”落地,目标是先覆盖前端实际点击主链,而不是一步到位复刻 Node 全部 story domain。 + +当前已覆盖动作: + +1. `story_continue_adventure` +2. `story_opening_camp_dialogue` +3. `camp_travel_home_scene` +4. `idle_call_out` +5. `idle_explore_forward` +6. `idle_observe_signs` +7. `idle_rest_focus` +8. `idle_travel_next_scene` +9. `npc_preview_talk` +10. `npc_chat` +11. `npc_help` +12. `npc_leave` +13. `npc_fight` +14. `npc_spar` +15. `npc_recruit` +16. `battle_attack_basic` +17. `battle_use_skill` +18. `battle_all_in_crush` +19. `battle_escape_breakout` +20. `battle_feint_step` +21. `battle_finisher_window` +22. `battle_guard_break` +23. `battle_probe_pressure` +24. `battle_recover_breath` +25. `inventory_use` +26. `equipment_equip` +27. `equipment_unequip` +28. `forge_craft` +29. `forge_dismantle` +30. `forge_reforge` +31. `npc_trade` +32. `npc_gift` + +统一规则: + +1. 请求带 `snapshot` 时先写入 `runtime_snapshot` +2. 请求不带 `snapshot` 时回退读取持久化 `runtime_snapshot` +3. `clientVersion` 与 `gameState.runtimeActionVersion` 不一致时返回 `409` +4. 动作成功后递增 `runtimeActionVersion` +5. 追加 `storyHistory`,并把新的 `currentStory` / `viewModel` / `presentation` / `patches` 回写到 snapshot +6. 若已配置 `platform-llm`,允许在动作规则结算完成后尝试生成增强版 `storyText / currentStory`;生成失败时自动回退确定性结果 + +当前已额外对齐的 Node 旧主链细节: + +1. `npc_chat` + - 已从最初的固定 `+1 affinity` 修正为 Node 旧规则 `max(2, 6 - chattedCount)` + - 例如 `chattedCount = 0` 时首聊会从 `46 -> 52` +2. `npc_help` + - 已改为一次性援手 + - 成功时恢复 `10 HP / 8 Mana` + - 同时关系 `+4` + - 二次调用返回错误 +3. `npc_recruit` + - 已要求 `affinity >= 60` + - 当前队伍满员时必须提交 `releaseNpcId` + - 当前 compat bridge 也会把换队结果写回 `companions` +4. quest compat 主循环 + - 已补 `npc_chat_quest_offer_view / replace / abandon` + - 已补 `npc_quest_accept / npc_quest_turn_in` + - `pendingQuestOffer.quest` 会继续写回 `currentStory.npcChatState` + - quest offer 选项会继续携带前端面板依赖的 `runtimePayload.npcChatQuestOfferAction` +5. `npc_quest_turn_in` + - quest 不再被直接从快照中移除,而是保留为 `status = turned_in` + - 当前最小奖励闭环已写回 `playerCurrency / playerInventory / playerProgression / npc affinity` + - `playerProgression` 当前仍走 compat 侧确定性经验累计,不等价于最终 SpacetimeDB 真相成长链 +6. combat compat + - battle 状态查询已补 `inventory_use` 与多条 `battle_use_skill` 选项编译 + - 技能选项会继续输出 `runtimePayload.skillId`、`disabled` 与 `reason` + - 战斗物品会继续输出 `runtimePayload.itemId` + - `battle_use_skill` 已补 `playerSkillCooldowns` 与 `activeBuildBuffs` 写回 + - `inventory_use` 已补 `playerInventory` 扣减、`itemsUsed`、冷却缩减与 `activeBuildBuffs` 写回 + - hostile 战斗胜利后已补 `runtimeStats.hostileNpcsDefeated += 1` + - hostile 战斗胜利后已补 `playerProgression.totalXp / level / xpToNextLevel / lastGrantedSource = hostile_npc` +7. Task6 inventory / NPC inventory compat + - `equipment_equip` 已补最小装备换装、`playerEquipment` 写回、`playerInventory` 扣减、`playerMaxHp / playerMaxMana` 回算与 Build toast + - `equipment_unequip` 已补槽位归一化、卸装回包、`playerEquipment` 置空、`playerInventory` 回收与 Build toast 回算 + - `forge_craft / forge_dismantle / forge_reforge` 已补最小工坊主链:材料消耗、产物生成、货币扣减、重铸属性提升与结果文本 + - `npc_trade` 已补买入 / 卖出结算所需的 `playerCurrency`、`playerInventory` 与 `npcStates.*.inventory` 写回 + - `npc_gift` 已补 `playerInventory` 扣减、NPC 背包收礼、`affinity`、`giftsGiven` 与 `npc_affinity_changed` patch + - NPC 交互态 fallback option compiler 已按 Node 旧顺序补 `npc_trade / npc_gift / npc_quest_* / npc_recruit / npc_leave` + - 已补 compat bridge 入口级 NPC state bootstrap:当 `npcStates` 为空且遭遇为纯商贩型 NPC 时,`state/get` 与 `actions/resolve` 会自动初始化 `relationState / stanceProfile / tradeStockSignature / inventory` +8. 动作后 LLM 文本增强 + - `npc_chat / story_opening_camp_dialogue` 已在 Rust 侧补最小 `generate_action_story_payload(...)` 分支 + - 当 `platform-llm` 可用时,会尝试生成中文 NPC 对话文本,并把 `currentStory` 保存为 `displayMode = dialogue` + - 该对话态当前保持与 Node 旧结构一致:`options` 只保留“继续推进冒险”,真实下一步入口先压到 `deferredOptions` + - `battle victory / spar_complete / escaped` 已支持生成结果叙事,但当前仍沿用既有 fallback options,不提前迁移完整 orchestrator 选项重排 + - 所有动作后 LLM 增强都只改写展示文本,不改变已完成的数值结算、patch 与 snapshot 写回顺序 + +### 4.2.3 当前明确移除的旧概念 + +`treasure_*` 旧 runtime story 遭遇动作已经从当前 Rust compat bridge 中移除,不再属于本阶段 M4 runtime story 主链覆盖范围。 + +当前保留的仅是 quest 目标语义里历史遗留的 `inspect_treasure` 字段口径,后续会在 quest / 叙事任务链单独收口,不在这份 compat bridge 文档里继续把 treasure 视作动作主循环。 + +### 4.2.4 `initial` / `continue` 首版策略 + +当前 Rust compat handler 已提供稳定 `RuntimeStoryAiResponse`: + +1. 优先复用 `requestOptions.availableOptions / optionCatalog` +2. 未配置 `platform-llm` 时返回确定性 fallback `storyText` +3. 已配置 `platform-llm` 时,允许基于同一请求载荷生成增强版文本 +4. 当前 `encounter` 仍返回 `null` + --- ## 5. DTO 分层 @@ -223,15 +332,41 @@ --- -## 6. 第一段工程落地顺序 +## 6. 当前已落地工程顺序 -建议直接按下面顺序编码: +本轮实际完成顺序: 1. `shared-contracts` 新增 `runtime_story.rs` 2. 为 `RuntimeStoryStateResolveRequest / RuntimeStoryActionResponse` 补 camelCase 序列化测试 -3. `docs/technical/README.md` 与 `shared-contracts/README.md` 更新索引 -4. `backend-rewrite-tasklist/03_M4_STORY_AND_GAMEPLAY.md` 追加当前冻结进展 -5. 下一轮再进入 `api-server` 的 `state/resolve` handler 与兼容 compiler +3. 恢复并重建 `api-server/src/runtime_story.rs` +4. 接入 `state/resolve`、`GET state`、`actions/resolve`、`initial`、`continue` +5. 复用 `runtime_save` 的 SpacetimeDB snapshot 持久化链 +6. 执行 `cargo test -p shared-contracts` +7. 执行 `cargo check -p api-server` +8. 执行 `cargo test -p api-server runtime_story` +9. 继续把 Node 旧 route boundary 回归平移到 Rust: + - `runtime_story_routes_resolve_through_rust_route_boundary` + - `runtime_story_action_resolve_rejects_client_version_conflict` +10. 继续补关键 NPC compat 行为回归: + - `runtime_story_npc_help_is_one_shot_and_restores_resources` + - `runtime_story_npc_recruit_requires_threshold_and_release_target_when_party_full` +11. 继续补 quest compat 回归: + - `runtime_story_quest_offer_replace_updates_pending_offer_and_payload` + - `runtime_story_quest_offer_abandon_clears_pending_offer_and_restores_chat_options` + - `runtime_story_quest_accept_writes_quest_runtime_stats_and_followup_story` + - `runtime_story_quest_turn_in_marks_quest_rewards_and_affinity` +12. 继续补 Task6 inventory / NPC inventory compat 回归: + - `runtime_story_state_compiler_builds_active_npc_options_with_trade_gift_and_help_lock` + - `runtime_story_equipment_equip_updates_loadout_and_build_toast` + - `runtime_story_equipment_unequip_returns_item_to_inventory_and_resets_loadout` + - `runtime_story_forge_craft_consumes_materials_and_currency` + - `runtime_story_forge_dismantle_replaces_item_with_material_outputs` + - `runtime_story_forge_reforge_upgrades_item_and_consumes_cost` + - `runtime_story_npc_trade_buy_updates_currency_inventory_and_stock` + - `runtime_story_state_compiler_bootstraps_trade_inventory_for_role_npc` + - `runtime_story_npc_trade_buy_bootstraps_missing_npc_state` + - `runtime_story_npc_gift_updates_affinity_inventory_and_patch` + - `runtime_story_route_boundary_persists_equipment_equip_snapshot_updates` --- @@ -239,13 +374,14 @@ 以下内容继续明确后置: -1. `POST /api/runtime/story/actions/resolve` 的请求 DTO 是否直接复用旧 TS contract 全量字段 -2. `resolve_story_action` 是否拆成: +1. `resolve_story_action` 是否拆成: - `resolve_story_action` - `resolve_story_combat_action` - `resolve_story_interaction_action` -3. `snapshot` 缺失时是否允许直接从 Rust 真相表完整恢复旧 `currentStory` -4. `LLM` 文本续写是在 Rust bridge 内继续调用,还是继续通过 Node 兼容层兜底 +2. `snapshot` 缺失时是否允许直接从 Rust 真相表完整恢复旧 `currentStory` +3. 当前确定性 compat action 何时被真正的 SpacetimeDB story reducer 替换 +4. `battle / npc / quest / inventory` patch 是否继续细化成与 Node 完全逐字段一致 +5. `npc_quest_turn_in` 的经验、物品、情报、章节推进何时切换到真正的 SpacetimeDB progression / inventory / quest 真相链,而不是 compat 侧快照写回 这些边界在状态桥稳定前都不应提前拍死。 @@ -258,7 +394,20 @@ 1. 已有独立技术文档冻结 `state/resolve` 兼容桥边界 2. `shared-contracts` 已拥有旧 `runtime story` 兼容 DTO 3. DTO 字段名与当前前端消费口径保持一致 -4. `cargo test -p shared-contracts` 通过 -5. `npm run check:encoding` 通过,确保新增中文文档与 Rust 源文件编码未损坏 +4. `api-server` 已挂出: + - `POST /api/runtime/story/state/resolve` + - `GET /api/runtime/story/state/:sessionId` + - `POST /api/runtime/story/actions/resolve` + - `POST /api/runtime/story/initial` + - `POST /api/runtime/story/continue` +5. `cargo test -p shared-contracts` 通过 +6. `cargo check -p api-server` 通过 +7. `cargo test -p api-server runtime_story` 通过,当前 Rust `runtime_story` 兼容桥回归为 30 条 +8. `node scripts/check-encoding.mjs` 通过 -达到以上条件后,下一轮即可直接进入 Rust `state bridge compiler` 与 Axum handler 落地。 +补充边界: + +1. 当前测试里为 `runtime_snapshot` 加了 `#[cfg(test)]` 下的内存兜底,只用于在未启动本地 SpacetimeDB 时稳定验证 Rust route boundary。 +2. 该测试兜底不进入生产链路,不改变真实 `runtime_save -> spacetime-client -> SpacetimeDB procedure` 的运行时实现。 + +达到以上条件后,兼容桥这一段已不再停留在 DTO / 空壳状态;下一轮重点转向“继续迁移 Node 剩余编排分支,并最终用真相态 reducer / projection 替换 compat bridge”。 diff --git a/docs/technical/M4_RUNTIME_STORY_RS_SPLIT_PLAN_2026-04-22.md b/docs/technical/M4_RUNTIME_STORY_RS_SPLIT_PLAN_2026-04-22.md new file mode 100644 index 00000000..82c3fbdc --- /dev/null +++ b/docs/technical/M4_RUNTIME_STORY_RS_SPLIT_PLAN_2026-04-22.md @@ -0,0 +1,421 @@ +# M4 Runtime Story Rust 文件拆分方案(2026-04-22) + +更新时间:`2026-04-22` + +## 0. 文档目标 + +本文件只解决一个工程问题: + +**把 `server-rs/crates/api-server/src/runtime_story.rs` 从当前超大单文件拆成可维护的 Rust 子模块,同时不改变既有 M4 compat bridge 的行为边界。** + +当前 `runtime_story.rs` 已超过 `7000` 行,内部同时混杂了: + +1. Axum route handler +2. snapshot 持久化与 DTO 组装 +3. runtime story compat 动作结算 +4. runtime option compiler / currentStory builder +5. LLM 文本增强 +6. test fixture 与 route boundary 回归 + +这已经超出单文件可维护范围,也会直接拖慢后续继续迁移 Node compat 分支的速度。 + +--- + +## 1. 本轮拆分原则 + +本轮拆分坚持以下边界: + +1. **先拆“展示编译层”和“AI 增强层”,不先重写规则结算层。** +2. **不改变 `app.rs` 里的路由绑定函数名。** +3. **不改变 `RuntimeStoryActionResponse / RuntimeStoryAiResponse` contract。** +4. **不改变现有 compat bridge 的动作规则、patch、snapshot 写回顺序。** +5. **优先做可验证的文件拆分,不把这轮演变成架构重写。** + +原因: + +1. 当前 `resolve_runtime_story_choice_action(...)` 仍在持续迁移 Node compat 行为,短期内继续集中在主文件更利于快速补链。 +2. `presentation / option compiler / dialogue currentStory / AI payload` 对外依赖相对单纯,更适合先抽走。 +3. test module 独立后,可以明显降低主文件噪音,后续再继续拆规则层也更安全。 + +--- + +## 2. 首轮拆分目标 + +首轮只拆以下 3 块: + +### 2.1 `runtime_story/presentation.rs` + +职责: + +1. `viewModel` 编译 +2. `availableOptions` 编译 +3. `currentStory` builder +4. `dialogue / pendingQuestOffer` 的 story shape helper +5. `story option` 与 `interaction` 的组装 + +这块包含但不限于: + +1. `build_runtime_story_view_model` +2. `build_runtime_story_options` +3. `build_fallback_runtime_story_options` +4. `build_dialogue_current_story` +5. `build_pending_quest_offer_story` +6. `build_story_option_from_runtime_option` + +### 2.2 `runtime_story/ai.rs` + +职责: + +1. `initial / continue` 的 `RuntimeStoryAiResponse` +2. `actions/resolve` 后的最小 LLM 文本增强 +3. 对话 turn 解析 +4. AI prompt payload 构造 + +这块包含但不限于: + +1. `build_runtime_story_ai_response` +2. `generate_ai_story_text` +3. `generate_action_story_payload` +4. `generate_npc_dialogue_payload` +5. `generate_reasoned_story_payload` +6. `parse_dialogue_turns` + +### 2.3 `runtime_story/tests.rs` + +职责: + +1. route boundary test +2. 纯函数回归 +3. fixture builder +4. 鉴权 token helper + +--- + +## 3. 拆分后目录形态 + +首轮目标目录: + +```text +server-rs/crates/api-server/src/ +├─ runtime_story.rs +└─ runtime_story/ + ├─ ai.rs + ├─ presentation.rs + └─ tests.rs +``` + +其中: + +1. `runtime_story.rs` 保留为外层入口模块 +2. 子模块通过 `mod ai; mod presentation; #[cfg(test)] mod tests;` 组织 +3. `runtime_story.rs` 继续暴露原有 5 个 route handler: + - `resolve_runtime_story_state` + - `get_runtime_story_state` + - `resolve_runtime_story_action` + - `generate_runtime_story_initial` + - `generate_runtime_story_continue` + +--- + +## 4. Rust 侧实现策略 + +## 4.1 不做新的共享 crate + +本轮不把 helper 再抽成新的 crate 或全局 util module。 + +原因: + +1. 当前拆分目标是降低单文件复杂度,不是扩展跨模块复用面。 +2. `presentation / ai / tests` 仍强依赖 `runtime_story` 内部 helper。 +3. 如果过早抽到 crate 级共享层,会额外引入新的 API 稳定面和更大改动范围。 + +## 4.2 子模块通过 `super::*` 复用内部 helper + +首轮允许子模块继续通过 `use super::*;` 访问现有内部函数、结构体和常量。 + +这是刻意的折中: + +1. 优先完成物理拆分 +2. 暂不要求所有 helper 立即彻底分层 +3. 后续再在第二轮继续把规则层和 state helper 往下切 + +## 4.3 第二轮候选拆分 + +本轮完成后,下一轮可继续评估: + +1. `runtime_story/actions.rs` +2. `runtime_story/battle.rs` +3. `runtime_story/inventory.rs` +4. `runtime_story/npc_state.rs` +5. `runtime_story/json_state.rs` + +但这些都不属于本次提交的必达范围。 + +--- + +## 5. 验证要求 + +拆分后至少必须通过: + +1. `cargo test -p api-server runtime_story --manifest-path D:\\Genarrative\\server-rs\\Cargo.toml` +2. `cargo check -p api-server --manifest-path D:\\Genarrative\\server-rs\\Cargo.toml` +3. `node D:\\Genarrative\\scripts\\check-encoding.mjs` + +若以上任一失败,则本轮拆分不算完成。 + +--- + +## 6. 本轮明确不做 + +1. 不改 compat bridge 业务规则 +2. 不新增或删除 runtime functionId +3. 不顺手把 quest 里的历史 `inspect_treasure` 字段一并清理 +4. 不提前把 `resolve_story_action / sync_runtime_snapshot_projection` 真相 reducer 并入本轮 +5. 不修改前端调用边界 + +--- + +## 7. 完成标记 + +本轮拆分完成的判定标准: + +1. `runtime_story.rs` 明显缩短,至少不再携带 tests 与 AI/presentation 全量实现 +2. `runtime_story/ai.rs`、`runtime_story/presentation.rs`、`runtime_story/tests.rs` 已落地 +3. route handler 对外签名不变 +4. 定向回归全部通过 + +达到以上条件后,再继续进入下一轮“规则层进一步拆分”。 + +--- + +## 8. 2026-04-22 实际落地进度 + +截至 `2026-04-22` 当前工作区,首轮物理拆分已经进入可继续演进状态: + +1. 外层入口 [runtime_story.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story.rs) 已缩成薄壳,只保留原有 5 个 route handler 的导出。 +2. 兼容实现主体已迁入 [compat.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat.rs),并继续保留规则结算主链。 +3. `tests` 已外置到 [tests.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/tests.rs),避免继续堆在主文件内。 +4. 本轮进一步把 `compat` 内部再拆成: + - [ai.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/ai.rs) + - [presentation.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/presentation.rs) +5. 当前拆分策略仍然维持 `compat` 内部模块,通过 `use super::*;` 复用共享 helper,不提前抽独立 crate。 +6. quest replace / fixture 中原本残留的 `inspect_treasure` mock 已同步替换为更中性的 `talk_to_npc`,避免把已废弃的 treasure 概念继续固化进新模块。 + +下一步不再是继续把文件塞回去,而是沿着当前目录继续把“无 HTTP / 无 AppState”的纯规则与编译逻辑收敛出来,为后续独立 crate 做第二阶段准备。 + +## 9. 第二阶段收敛边界 + +第二阶段不新增对外入口,只继续整理 `compat` 内部依赖面: + +1. 继续保留 [compat.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat.rs) 作为 route handler、快照持久化与 compat action orchestration 的主入口。 +2. 优先把“只依赖 `serde_json::Value` / 共享 contract / 纯函数 helper”的部分抽到内部纯模块。 +3. 当前最适合先抽的块不是 battle route,而是: + - NPC 状态补齐 + - encounter / inventory / equipment 读写 + - quest / trade / recruit 等会复用的 `game_state` 纯变换 helper +4. 这一步的目标不是立刻独立 crate,而是先在 `api-server` 内形成清晰的“HTTP 外壳”与“纯状态编译层”分界。 + +如果第二阶段完成后 `compat` 内已经能明显区分: + +1. `AppState / RequestContext / Axum` 相关边界 +2. `Value -> Value / DTO` 的纯规则层 + +那么第三阶段再把后者抽成独立 crate,风险会显著低于现在直接新建 crate。 + +## 10. 第二阶段 battle 收敛进度 + +截至 `2026-04-22` 当前工作区,第二阶段已经继续向“纯规则内聚”推进一块 battle 逻辑: + +1. `compat` 新增 [battle.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/battle.rs),专门承接 battle 兼容桥里的纯规则与展示编译 helper。 +2. 已迁入 `battle.rs` 的内容包括: + - 战斗数值写回 helper + - 技能 / 物品的 battle action plan 生成 + - 战斗技能冷却读写 + - battle 选项与推荐物品编译 + - battle 胜利经验奖励计算 +3. [compat.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat.rs) 当前继续只保留 `resolve_battle_action(...)` 这种动作编排入口,不再堆放大段 battle 纯 helper。 +4. [core.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/core.rs) 中原本只服务 battle 链的 skill / inventory 读取与 cooldown helper 已同步移出,避免“纯规则仍散落在多个模块”。 +5. 这一步仍然没有改变: + - Axum route handler 签名 + - `AppState / RequestContext` 边界 + - `RuntimeStoryActionResponse` / patch / snapshot 的写回顺序 + +这说明第二阶段已经不只是在“补状态 helper”,而是开始把 compat 内最独立的一类规则块真正收束成内部纯模块。下一步可以继续沿同样方法处理 `forge`,以及 `trade / gift / companion` 这类不依赖 HTTP 的 helper 群。 + +同日进一步推进后,这条路线已经从 battle 扩展到 forge: + +1. `compat` 新增 [forge.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/forge.rs),把锻造配方、重铸成本、材料消耗、运行时物品生成、拆解产物和重铸产物构造统一收口。 +2. [compat.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat.rs) 当前对 forge 只保留: + - `resolve_forge_craft_action(...)` + - `resolve_forge_dismantle_action(...)` + - `resolve_forge_reforge_action(...)` + 这些动作编排入口。 +3. [game_state.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/game_state.rs) 里的 NPC trade bootstrapping 继续直接复用 `forge.rs` 中的运行时物品构造 helper,避免 trade stock 与工坊产物出现两套生成规则。 +4. 这意味着第二阶段已经形成一个更清楚的内部形态: + - `battle.rs`:战斗纯规则与战斗选项编译 + - `forge.rs`:工坊纯规则与运行时锻造物品生成 + - `game_state.rs`:快照态读写与 NPC / inventory / equipment 状态桥 + +后续再继续迁 `trade / gift / companion` 时,目标就不再是单纯减少行数,而是把 compat bridge 逐步收束成“动作编排壳 + 多个纯规则模块”的明确结构。 + +在此基础上,同日又继续把 NPC 交互侧的一批纯 helper 收到独立模块: + +1. `compat` 新增 [npc_support.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/npc_support.rs)。 +2. 已迁入的内容包括: + - 赠礼好感收益与赠礼结果文本 + - 交易价格、折扣档位、货币文本、数量后缀 + - 队伍招募与满员换队 helper +3. [compat.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat.rs) 现在对 `npc_trade / npc_gift / npc_recruit` 仍只保留动作编排,不再承担底层价格计算和队伍变换逻辑。 +4. 到这一步,`compat.rs` 的主要职责已经更接近: + - route handler / snapshot bridge + - action orchestration + - 少量尚未迁出的共享 glue code + +这为后续把“无 HTTP / 无 `AppState`”的剩余 glue code 再往下收,提供了更明确的拆分方向。 + +第二阶段继续推进到 action resolver 编排后,当前又新增动作编排模块: + +1. [battle_actions.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/battle_actions.rs)。 +2. [equipment_actions.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/equipment_actions.rs)。 +3. [forge_actions.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/forge_actions.rs)。 +4. [npc_actions.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/npc_actions.rs)。 +5. [quest_actions.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/quest_actions.rs)。 + +已迁入的内容包括: + +1. `battle_*` +2. `equipment_equip / equipment_unequip` +3. `forge_craft / forge_dismantle / forge_reforge` +4. `npc_preview_talk / npc_chat / npc_help / npc_fight / npc_spar` +5. `npc_trade / npc_gift / npc_recruit` +6. `npc_chat_quest_offer_view` +7. `npc_chat_quest_offer_replace` +8. `npc_chat_quest_offer_abandon` +9. `npc_quest_accept` +10. `npc_quest_turn_in` + +这组 resolver 虽然仍是 action orchestration,但已经不依赖 HTTP / `AppState`,只依赖快照 `Value`、当前故事 `currentStory`、共享 DTO 与内部 helper,因此适合先作为 `api-server` 内部模块沉淀。 + +迁移后 [compat.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat.rs) 对这些动作只保留 functionId 分发、快照桥接与少量共享 glue code,不再承载 battle / equipment / forge / NPC / quest 的具体结算细节。 + +## 11. 独立 crate 抽取边界 + +完成第二阶段后,已经可以进入第三阶段,但独立 crate 仍按最小安全边界推进: + +1. 新 crate 命名为 `module-runtime-story-compat`。 +2. `module-runtime-story-compat` 只承接“无 HTTP / 无 `AppState`”的 compat 核心: + - runtime story action 分发与确定性结算 + - battle / equipment / forge / NPC / quest action resolver + - `Value` 快照态读写 helper + - `RuntimeStoryActionResponse` 的 view model / presentation 编译 +3. `api-server` 继续保留: + - Axum route handler + - `RequestContext / AuthenticatedAccessToken` + - `runtime_snapshot` 持久化与读取 + - `clientVersion` 校验到 HTTP error 的映射 + - `platform-llm` 动作后文本增强 +4. 首批迁移不把 AI 文本增强放进新 crate,因为它依赖 `AppState` 和 `platform-llm`。 +5. 首批迁移不把 test route boundary 放进新 crate,route boundary 仍属于 `api-server`。 + +这一步完成后,`api-server` 的 `runtime_story/compat.rs` 应该只负责: + +1. 从 HTTP 请求恢复 / 持久化 snapshot +2. 调用 `module-runtime-story-compat` 产出确定性动作结果或状态响应 +3. 需要时调用本地 AI 增强 +4. 将最终响应包回 `Json` + +这就是从“`api-server` 内部模块”到“独立 crate”的首个可验证切片。 + +截至当前工作区,第三阶段首批独立 crate 已落地: + +1. 已新增 [module-runtime-story-compat](D:/Genarrative/server-rs/crates/module-runtime-story-compat)。 +2. 已接入 [server-rs/Cargo.toml](D:/Genarrative/server-rs/Cargo.toml) workspace。 +3. [api-server/Cargo.toml](D:/Genarrative/server-rs/crates/api-server/Cargo.toml) 已新增对 `module-runtime-story-compat` 的依赖。 +4. 首批迁入新 crate 的内容包括: + - `StoryResolution` + - `GeneratedStoryPayload` + - `CurrentEncounterNpcQuestContext` + - `PendingQuestOfferContext` + - `RuntimeStoryActionResponseParts` + - `CONTINUE_ADVENTURE_FUNCTION_ID` + - `MAX_TASK5_COMPANIONS` + - `simple_story_resolution` + - `resolve_action_text` + - `build_status_patch` + - `current_world_type` +5. 第三阶段继续推进后,当前已经从 `api-server` 抽到独立 crate 的纯逻辑还包括: + - `core.rs`:JSON 快照读写、runtime stat、story history、progression、encounter 清理 + - `game_state.rs`:encounter / inventory / equipment 的基础 helper + - `forge.rs`:锻造配方、重铸成本、材料消耗、拆解产物、重铸产物、货币文本 + - `forge_actions.rs`:`forge_craft / forge_dismantle / forge_reforge` 三条动作结算 + - `npc_support.rs`:赠礼好感收益、交易价格、数量文案、满员换队招募 helper + - `battle.rs`:`battle_* / inventory_use` 的纯动作结算、patch 生成与胜负写回 +6. 当前 [api-server 的 compat.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat.rs) 已经不再内嵌上述纯逻辑,只保留: + - Axum handler + - snapshot 读写 + - `clientVersion` 校验 + - functionId 分发 + - HTTP error 映射 + - 动作后 AI 文本增强 +7. 当前 [api-server 的 forge.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/forge.rs) 已收缩成极薄 bridge,只为 NPC trade bootstrap 复用新 crate 暴露的运行时物品构造 helper,锻造规则主体不再保留本地副本。 +8. 当前 [api-server 的 battle.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/battle.rs) 也已从“结算 + 展示”收缩成“展示编译 + 少量本地 helper”: + - battle 动作结算主链已经迁入 `module-runtime-story-compat` + - `api-server` 本地仅继续保留 `build_battle_runtime_story_options(...)` 与 `restore_player_resource(...)` 这类仍被 presentation / NPC 辅助逻辑直接依赖的部分 + - 这为下一步继续把 battle option compiler 收进独立 crate 做好了边界准备 + +这意味着第三阶段已经不只是“创建了新 crate”,而是完成了第一批真正跨 crate 的 compat 纯逻辑迁移,并且保持 route boundary 与既有测试口径不变。 + +同日继续推进后,battle 这块已经完成从“先迁结算主链”到“连展示编译一起迁”的下一步: + +1. [module-runtime-story-compat 的 battle.rs](D:/Genarrative/server-rs/crates/module-runtime-story-compat/src/battle.rs) 当前已同时承接: + - `resolve_battle_action(...)` + - `restore_player_resource(...)` + - `build_battle_runtime_story_options(...)` + - 技能冷却读取、推荐物品挑选、战斗技能 option compiler 等 battle 展示辅助 +2. [api-server 的 compat.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat.rs) 已直接从 `module-runtime-story-compat` 导入 battle 展示编译与资源恢复 helper。 +3. [api-server 本地的 compat/battle.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/battle.rs) 已删除,不再保留 battle 规则的本地副本。 +4. 到这一步,`api-server` 在 runtime story compat 上对 battle 的职责已经只剩: + - functionId 分发 + - route handler / snapshot bridge + - AI 文本增强后的最终响应拼装 + +这说明第三阶段已经不只是在“拆 crate”,而是在真实压缩 `api-server` 的 compat 规则面。接下来更合理的推进方向将不再是 battle,而是继续评估 `presentation` 中还能进一步抽到独立 crate 的纯 view model / option compiler 边界。 + +同日继续推进后,`presentation` 中最通用的一层 option DTO 编译也已经开始抽离: + +1. 已新增 [options.rs](D:/Genarrative/server-rs/crates/module-runtime-story-compat/src/options.rs),统一承接: + - `build_static_runtime_story_option(...)` + - `build_runtime_story_option_with_payload(...)` + - `build_disabled_runtime_story_option(...)` + - `build_runtime_story_option_from_story_option(...)` + - `build_story_option_from_runtime_option(...)` + - `infer_option_scope(...)` +2. [module-runtime-story-compat 的 lib.rs](D:/Genarrative/server-rs/crates/module-runtime-story-compat/src/lib.rs) 已对外 re-export 这些 option helper,供 `api-server` 直接复用。 +3. [api-server 的 presentation.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/presentation.rs) 已删除本地重复实现,只保留 NPC option 组合、view model 组装、quest currentStory 等尚未完全独立的部分。 + +这一步的意义不是单纯减少行数,而是先把 `RuntimeStoryOptionView` 的最小稳定编译面收敛到独立 crate。后续若继续外提 `view model` 与 `fallback option compiler`,将不需要再重复搬运这些 option 基础件。 + +同日继续推进后,`presentation` 中的纯 view-model builder 也已经抽到独立 crate: + +1. 已新增 [view_model.rs](D:/Genarrative/server-rs/crates/module-runtime-story-compat/src/view_model.rs),统一承接: + - `build_runtime_story_view_model(...)` + - `build_runtime_story_companions(...)` + - `build_runtime_story_encounter(...)` + - `resolve_current_encounter_npc_state(...)` +2. [api-server 的 presentation.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/presentation.rs) 已删除本地 view-model 组装实现,继续只负责状态响应 orchestration、dialogue currentStory、fallback option compiler 与 quest 辅助。 +3. [api-server 的 game_state.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/game_state.rs) 当前也直接复用 crate 导出的 `resolve_current_encounter_npc_state(...)`,避免 NPC 状态查询 helper 在 `api-server` 和 crate 之间出现两套实现。 + +至此,`module-runtime-story-compat` 已经覆盖了 runtime story 兼容层的以下纯逻辑面: + +1. JSON 快照读写与基础状态 helper +2. battle / forge / npc support 的纯规则结算 +3. battle option compiler +4. runtime story option DTO 编译 +5. runtime story view-model 编译 + +`api-server` 当前的剩余重点已经更集中在: + +1. HTTP / snapshot bridge +2. functionId 分发 +3. AI 文本增强 +4. NPC / quest fallback option 与 currentStory 组合逻辑 diff --git a/docs/technical/M6_ASSET_METADATA_HASH_VERSION_AND_SPECIALIZED_TABLE_BOUNDARY_2026-04-22.md b/docs/technical/M6_ASSET_METADATA_HASH_VERSION_AND_SPECIALIZED_TABLE_BOUNDARY_2026-04-22.md new file mode 100644 index 00000000..b520e803 --- /dev/null +++ b/docs/technical/M6_ASSET_METADATA_HASH_VERSION_AND_SPECIALIZED_TABLE_BOUNDARY_2026-04-22.md @@ -0,0 +1,142 @@ +# M6 资产元数据、版本与专用表边界设计 + +日期:`2026-04-22` + +## 1. 文档目的 + +这份文档用于把 `M6` 清单中剩余的以下项收口到可执行边界: + +1. 内容 hash / 版本字段规范 +2. `asset_job` +3. `asset_manifest` +4. `character_visual_asset` +5. `character_animation_asset` +6. `scene_image_asset` +7. `sprite_sheet_asset` + +当前 `M6` 第一批已经落地的真实主链是: + +1. OSS 私有对象持有二进制内容 +2. `asset_object` 记录 `bucket + object_key` 和基础元数据 +3. `asset_entity_binding` 记录业务实体槽位绑定 +4. 角色动作发布通过 OSS `manifest.json` 表达动作集合 +5. 角色主形象、角色动作、custom world 场景图/封面图都先通过通用绑定闭环 + +因此本阶段不继续堆新表,而是冻结“哪些内容已经由现有主链承担,哪些等真实访问模式稳定后再拆强业务表”。 + +## 2. 内容 hash 与版本规范 + +### 2.1 当前 Stage 1 规范 + +`asset_object` 当前字段已经包含: + +1. `content_hash: Option` +2. `version: u32` + +本阶段规范如下: + +1. `version` 固定从 `1` 起步。 +2. 同一 `bucket + object_key` 被重新确认时,保留原 `created_at`,更新 `updated_at`,版本仍按当前 `INITIAL_ASSET_OBJECT_VERSION = 1` 处理。 +3. `content_hash` 当前优先使用 OSS `ETag` 或调用方明确传入的 hash。 +4. 不在 `api-server` 对大文件做强制全量 SHA-256 计算,避免图片/视频代理链路和服务端上传链路被额外 CPU 与内存占用放大。 +5. 后续若需要强一致内容去重,再新增独立 `content_digest` 计算策略,不复用当前可空 `content_hash` 做强制约束。 + +### 2.2 不做强制 hash 的原因 + +1. OSS `ETag` 在不同上传方式下不一定等价于单纯 MD5。 +2. 当前第一批主要目标是把本地 `public/` 真相迁到 OSS 与 SpacetimeDB 元数据。 +3. 角色动作视频、帧序列和 custom world 图片都已经能通过 `content_length + object_key + asset_kind + binding` 完成首批追踪。 +4. 强制 hash 需要统一 multipart、服务端上传、浏览器直传和迁移脚本的计算口径,适合后续单独阶段。 + +## 3. `asset_job` 边界 + +当前不新增 `asset_job` 表。 + +理由: + +1. `M4` 已引入 `module-ai::AiTaskService` 和对应 `ai_task` 设计。 +2. 角色主形象与角色动作的 Stage 1 已复用 `AiTaskService` 输出旧 `jobs/:taskId` contract。 +3. custom world 场景图/封面图当前仍是同步兼容接口,不需要单独资产任务态。 + +当前任务状态统一口径: + +1. AI 生成相关:使用 `ai_task` / `AiTaskService`。 +2. 纯上传确认相关:使用 `asset_object` 与 `asset_entity_binding` 的返回结果。 +3. 后续若出现非 AI 的长时资产处理任务,再重新评估是否拆 `asset_job`。 + +## 4. `asset_manifest` 边界 + +当前不新增 SpacetimeDB `asset_manifest` 表。 + +Stage 1 的 manifest 口径如下: + +1. manifest 是一个 OSS JSON 对象。 +2. 角色动作整套 manifest 会被确认成 `asset_object`。 +3. `asset_entity_binding` 绑定的是整套 manifest 对象,而不是每个单帧对象。 +4. 前端仍通过旧 `animationMap` contract 消费动作帧路径。 + +后续只有满足以下条件之一时,才新增 `asset_manifest` 表: + +1. 需要在 SpacetimeDB 中按 manifest 内部动作、帧、依赖对象做查询。 +2. 需要对 manifest 做版本 diff、审核、回滚。 +3. 需要把 manifest 作为跨 profile、跨角色复用的结构化资产集合。 + +## 5. 强业务资产表边界 + +当前不新增以下强业务表: + +1. `character_visual_asset` +2. `character_animation_asset` +3. `scene_image_asset` +4. `sprite_sheet_asset` + +当前由以下组合承担业务绑定: + +1. `asset_object.asset_kind` +2. `asset_entity_binding.entity_kind` +3. `asset_entity_binding.entity_id` +4. `asset_entity_binding.slot` + +当前已冻结槽位: + +| 业务 | `entity_kind` | `slot` | `asset_kind` | +| --- | --- | --- | --- | +| 角色主形象 | `character` | `primary_visual` | `character_visual` | +| 角色动作集 | `character` | `animation_set` | `character_animation` | +| custom world 场景图 | `custom_world_landmark` | `scene_image` | `scene_image` | +| custom world 封面 | `custom_world_profile` | `cover` | `custom_world_cover` | + +后续拆强业务表的条件: + +1. 需要对角色主形象候选、审核状态、模型参数做结构化查询。 +2. 需要对动作集逐动作授权、复用、差分发布。 +3. 需要对场景图、封面图做多版本历史、审核流或推荐流。 +4. 需要对 sprite sheet 做切片、修帧、atlas 元数据查询。 + +## 6. `sprite_sheet_asset` 与 Qwen 边界 + +当前 `Qwen sprite` 独立工具链已经清理,不再作为本轮现役迁移主链。 + +本阶段只保留: + +1. 历史 `/generated-qwen-sprites/*` 路径读取兼容。 +2. `platform-oss::LegacyAssetPrefix::QwenSprites` 对象键支持。 + +因此 `sprite_sheet_asset` 当前只保留后续能力位,不在 `M6` Stage 1 新增表或接口。 + +## 7. 完成定义 + +当以下条件满足时,本阶段 M6 元数据与专用表边界视为完成: + +1. `content_hash/version` 在文档中明确为 `asset_object` 现有可空 hash + 初始版本口径。 +2. `asset_job` 明确由 `AiTaskService` 暂代,不新增重复任务表。 +3. `asset_manifest` 明确由 OSS JSON manifest + `asset_object` 暂代。 +4. 强业务资产表明确延后到访问模式稳定后拆分。 +5. `05_M6_ASSETS_OSS_EDITOR.md` 不再把这些后续能力位误标为当前 Stage 1 未完成阻塞项。 + +## 8. 关联文档 + +1. [SPACETIMEDB_ASSET_OBJECT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_ASSET_OBJECT_TABLE_DESIGN_2026-04-21.md) +2. [ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md](./ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md) +3. [M6_CHARACTER_ANIMATION_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CHARACTER_ANIMATION_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md) +4. [M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md) diff --git a/docs/technical/M6_CHARACTER_ANIMATION_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md b/docs/technical/M6_CHARACTER_ANIMATION_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md new file mode 100644 index 00000000..bb4e701d --- /dev/null +++ b/docs/technical/M6_CHARACTER_ANIMATION_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md @@ -0,0 +1,219 @@ +# M6 角色动作资产接入 OSS 第一批设计 + +日期:`2026-04-22` + +## 1. 文档目的 + +这份文档用于冻结 `M6` 第一批“角色动作生成 + 任务查询 + 正式发布”的真实落地口径。 + +本批只解决以下三条旧接口的 Rust 重写入口: + +1. `POST /api/assets/character-animation/generate` +2. `GET /api/assets/character-animation/jobs/:taskId` +3. `POST /api/assets/character-animation/publish` + +目标不是一次性接入 DashScope / Ark 视频模型,而是先把角色动作资产从旧 Node 本地 `public/generated-*` 真相切到: + +1. `OSS` 草稿对象 +2. `AI task` 任务态 +3. `OSS` 正式动作对象 +4. `asset_object` +5. `asset_entity_binding` + +## 2. 当前前提 + +当前仓库已经具备以下能力: + +1. `platform-oss::OssClient::put_object` +2. `platform-oss::OssClient::sign_get_object_url` +3. `asset_object` +4. `asset_entity_binding` +5. `module-ai` 进程内 `AiTaskService` +6. 角色主形象已完成 `generate / jobs / publish` 的第一批 OSS 主链 +7. 角色动作模板、视频导入、workflow cache 已完成第一批 Rust 兼容入口 + +因此本批复用现有 OSS、资产对象确认、业务实体绑定和 `AiTaskService`,不新增独立 `asset_job` 表。 + +## 3. 本批范围 + +### 3.1 要完成的内容 + +1. 兼容角色动作草稿生成接口 +2. 兼容角色动作任务查询接口 +3. 兼容角色动作正式发布接口 +4. `image-sequence` 草稿帧写入 OSS `generated-character-drafts/*` +5. 视频类策略草稿预览对象写入 OSS `generated-character-drafts/*` +6. 正式动作帧写入 OSS `generated-animations/*` +7. 正式动作 manifest 写入 OSS `generated-animations/*` +8. 正式动作 manifest 确认为 `asset_object` +9. 正式动作 manifest 绑定到角色实体动作槽位 +10. 返回字段继续保持旧前端可消费 contract + +### 3.2 本批不解决的内容 + +1. 不接真实 DashScope 图片序列帧模型 +2. 不接真实 Ark 图生视频模型 +3. 不接真实动作迁移模型 +4. 不落 `character_animation_asset` 强业务表 +5. 不回写 `src/data/characterOverrides.json` +6. 不迁移历史本地 `public/generated-animations` + +## 4. 旧接口兼容 contract + +### 4.1 `POST /api/assets/character-animation/generate` + +请求结构继续保持前端当前字段: + +1. `characterId` +2. `strategy` +3. `animation` +4. `promptText` +5. `characterBriefText` +6. `actionTemplateId` +7. `visualSource` +8. `referenceImageDataUrls` +9. `referenceVideoDataUrls` +10. `lastFrameImageDataUrl` +11. `frameCount` +12. `fps` +13. `durationSeconds` +14. `loop` +15. `useChromaKey` +16. `resolution` +17. `ratio` +18. `imageSequenceModel` +19. `videoModel` +20. `referenceVideoModel` +21. `motionTransferModel` + +`image-sequence` 返回结构继续保持: + +1. `ok` +2. `taskId` +3. `strategy` +4. `model` +5. `prompt` +6. `imageSources` + +视频类策略返回结构继续保持: + +1. `ok` +2. `taskId` +3. `strategy` +4. `model` +5. `prompt` +6. `previewVideoPath` + +补充口径: + +1. Stage 1 的 `image-sequence` 先生成 SVG 占位帧。 +2. Stage 1 的视频类策略若提供 `referenceVideoDataUrls[0]`,则把该视频作为草稿预览写入 OSS。 +3. Stage 1 的视频类策略若没有参考视频,则写入占位预览对象以保持接口 contract,后续真实视频模型替换该产物。 + +### 4.2 `GET /api/assets/character-animation/jobs/:taskId` + +返回结构继续保持: + +1. `taskId` +2. `kind` +3. `status` +4. `characterId` +5. `animation` +6. `strategy` +7. `model` +8. `prompt` +9. `createdAt` +10. `updatedAt` +11. `result` +12. `errorMessage` + +当前阶段直接复用 `AiTaskService` 内存态任务快照派生。 + +### 4.3 `POST /api/assets/character-animation/publish` + +请求结构继续保持: + +1. `characterId` +2. `visualAssetId` +3. `animations` +4. `updateCharacterOverride` + +返回结构继续保持: + +1. `ok` +2. `animationSetId` +3. `overrideMap` +4. `animationMap` +5. `saveMessage` + +补充口径: + +1. 每个动作的帧写入 `generated-animations/*` +2. 每个动作生成 `manifest.json` +3. 整套动作生成总 `manifest.json` +4. 总 manifest 确认为 `asset_object` +5. 总 manifest 绑定到角色实体槽位 +6. `overrideMap` 当前返回 `{}`,Rust 后端不再写本地角色覆盖文件 + +## 5. 业务实体与槽位约定 + +本批统一复用通用 `asset_entity_binding`。 + +### 5.1 角色动作正式对象 + +| 字段 | 取值 | +| --- | --- | +| `entity_kind` | `character` | +| `entity_id` | `characterId` | +| `slot` | `animation_set` | +| `asset_kind` | `character_animation` | + +说明: + +1. 正式绑定对象是整套动作总 manifest。 +2. 单帧对象不单独绑定。 +3. 后续若落 `character_animation_asset` 强业务表,再把动作级索引迁到专用表。 + +## 6. OSS 对象键规划 + +### 6.1 草稿序列帧 + +`generated-character-drafts/{characterSegment}/animation/{animationSegment}/{taskId}/frame-{index}.svg` + +### 6.2 草稿预览视频 + +`generated-character-drafts/{characterSegment}/animation/{animationSegment}/{taskId}/preview.{extension}` + +### 6.3 正式动作帧 + +`generated-animations/{characterSegment}/{animationSetId}/{actionSegment}/frame{index}.{extension}` + +### 6.4 正式动作 manifest + +动作级 manifest: + +`generated-animations/{characterSegment}/{animationSetId}/{actionSegment}/manifest.json` + +整套 manifest: + +`generated-animations/{characterSegment}/{animationSetId}/manifest.json` + +## 7. 完成定义 + +当以下条件满足时,本批视为完成: + +1. Rust 已兼容 `character-animation/generate` +2. Rust 已兼容 `character-animation/jobs/:taskId` +3. Rust 已兼容 `character-animation/publish` +4. 草稿动作产物写入 OSS +5. 正式动作产物写入 OSS +6. 正式总 manifest 形成 `asset_object` +7. 正式总 manifest 形成 `asset_entity_binding` +8. 前端仍能继续消费 `imageSources / previewVideoPath / animationMap` 旧 contract + +## 8. 关联文档 + +1. [M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md) +2. [M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md](./M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md) +3. [M6_CHARACTER_WORKFLOW_CACHE_OSS_STAGE1_2026-04-22.md](./M6_CHARACTER_WORKFLOW_CACHE_OSS_STAGE1_2026-04-22.md) +4. [ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md](./ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md) diff --git a/docs/technical/M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md b/docs/technical/M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md new file mode 100644 index 00000000..634fda8a --- /dev/null +++ b/docs/technical/M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md @@ -0,0 +1,147 @@ +# M6 角色动作模板与视频导入接入 OSS 第一批设计 + +日期:`2026-04-22` + +## 1. 文档目的 + +这份文档用于冻结 `M6` 第一批“角色动作模板查询 + 视频导入”的真实落地口径。 + +本批只解决以下两条旧接口的 Rust 重写入口: + +1. `GET /api/assets/character-animation/templates` +2. `POST /api/assets/character-animation/import-video` + +目标不是一次性迁移角色动作生成、发布和真实视频模型,而是先把资产工坊当前可独立收口的动作模板与参考视频导入从旧 Node 本地 `public/generated-character-drafts` 写盘,切到 OSS 草稿对象。 + +## 2. 当前前提 + +当前仓库已经具备以下能力: + +1. `platform-oss::OssClient::put_object` +2. `generated-character-drafts/*` 兼容对象键前缀 +3. `shared-contracts::assets` 角色主形象兼容 DTO +4. `api-server` 已接入角色主形象 `generate / jobs / publish` + +因此本批复用既有 OSS 服务端上传 helper,不新增 SpacetimeDB 表。 + +## 3. 本批范围 + +### 3.1 要完成的内容 + +1. 兼容动作模板列表接口 +2. 兼容参考视频导入接口 +3. 导入视频对象写入 OSS `generated-character-drafts/*` +4. 返回字段继续保持旧前端可消费 contract +5. 不再把导入视频写入本地 `public/` + +### 3.2 本批不解决的内容 + +1. 不迁移 `character-animation/generate` +2. 不迁移 `character-animation/jobs/:taskId` +3. 不迁移 `character-animation/publish` +4. 不落 `character_animation_asset` 强业务表 +5. 不为导入草稿创建 `asset_object` +6. 不为导入草稿创建 `asset_entity_binding` +7. 不读取旧本地 `public/` 路径作为导入源 + +## 4. 旧接口兼容 contract + +### 4.1 `GET /api/assets/character-animation/templates` + +返回结构继续保持: + +1. `ok` +2. `templates` + +每个模板继续包含: + +1. `id` +2. `label` +3. `animation` +4. `promptSuffix` +5. `notes` + +当前模板列表固定为内置四项: + +1. `idle_loop` +2. `run_side` +3. `attack_slash` +4. `die_fall` + +### 4.2 `POST /api/assets/character-animation/import-video` + +请求结构继续保持: + +1. `characterId` +2. `animation` +3. `videoSource` +4. `sourceLabel` + +返回结构继续保持: + +1. `ok` +2. `importedVideoPath` +3. `draftId` +4. `saveMessage` + +补充口径: + +1. `videoSource` 当前阶段只接受 `data:video/*;base64,...` +2. `importedVideoPath` 继续返回旧前端习惯的 `/generated-character-drafts/*` +3. 底层对象真相在 OSS,不再写本地 `public/` +4. `saveMessage` 明确说明当前是“已导入 OSS 草稿区” + +## 5. OSS 对象键规划 + +导入视频固定写入: + +`generated-character-drafts/{characterSegment}/animation/{animationSegment}/{draftId}/{sourceLabel}.{extension}` + +其中: + +1. `characterSegment` 来自 `characterId` 的安全路径片段 +2. `animationSegment` 来自 `animation` 的安全路径片段 +3. `draftId` 固定为 `animation-import-{unixMillis}` +4. `extension` 从 Data URL MIME 类型派生 + +## 6. 元数据规范 + +导入视频对象写入以下 `x-oss-meta-*` 元数据: + +1. `asset_kind = character_animation_reference_video` +2. `owner_user_id = asset-tool` +3. `entity_kind = character` +4. `entity_id = characterId` +5. `slot = animation_reference_video` +6. `animation = animation` + +说明: + +1. 旧资产工坊接口没有显式 Bearer,第一批继续使用 `asset-tool` 作为兼容归属。 +2. 草稿导入视频只是后续动作生成的参考输入,不是正式发布资产,因此本批不确认 `asset_object`。 + +## 7. 数据源边界 + +Rust 第一批只接受 `data:video/*;base64,...`。 + +暂不接受旧本地 public 路径,原因是: + +1. Rust 迁移目标是不再依赖本地 `public/` 作为资产真相。 +2. 若为了兼容旧路径再读取本地文件,会延长旧写盘链路生命周期。 +3. 前端导入入口当前可直接传视频 Data URL,足以满足本批最小闭环。 + +## 8. 完成定义 + +当以下条件满足时,本批视为完成: + +1. Rust 已兼容 `character-animation/templates` +2. Rust 已兼容 `character-animation/import-video` +3. 导入视频写入 OSS `generated-character-drafts/*` +4. 接口返回 `importedVideoPath / draftId` 旧 contract +5. 不再产生本地 `public/generated-character-drafts/*` 导入文件 + +## 9. 关联文档 + +1. [M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md) +2. [M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md) +3. [M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md](./M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md) diff --git a/docs/technical/M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md b/docs/technical/M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md new file mode 100644 index 00000000..41d91cb2 --- /dev/null +++ b/docs/technical/M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md @@ -0,0 +1,242 @@ +# M6 角色主形象资产接入 OSS 第一批设计 + +日期:`2026-04-22` + +## 1. 文档目的 + +这份文档用于冻结 `M6` 第一批“角色主形象资产链”的真实落地口径。 + +本批只解决以下三条旧接口的 Rust 重写入口: + +1. `POST /api/assets/character-visual/generate` +2. `GET /api/assets/character-visual/jobs/:taskId` +3. `POST /api/assets/character-visual/publish` + +目标不是一次性把整套资产系统迁完,而是先把“角色主形象候选生成 + 查询 + 正式发布”从旧 Node 的本地 `public/generated-*` 真相,切到: + +1. `OSS` +2. `asset_object` +3. `asset_entity_binding` +4. `AI task` 任务态 + +形成第一批正式主链。 + +## 2. 当前前提 + +当前仓库已经具备以下能力: + +1. `platform-oss::OssClient::put_object` +2. `platform-oss::OssClient::head_object` +3. `asset_object` +4. `asset_entity_binding` +5. `module-ai` 进程内 `AiTaskService` +6. `platform-llm` OpenAI 兼容文本模型网关 +7. `custom world` 图片兼容入口已经完成一版 `OSS + asset_object + asset_entity_binding` 落地 + +因此本批不重新设计一套新资产基础设施,而是复用: + +1. 既有 `OSS` 上传与确认链 +2. 既有 `asset_object / asset_entity_binding` +3. 既有 `AiTaskService` + +## 3. 本批范围 + +### 3.1 要完成的内容 + +1. 兼容角色主形象候选生成接口 +2. 兼容角色主形象任务状态查询接口 +3. 兼容角色主形象正式发布接口 +4. 候选草稿对象写入 OSS `generated-character-drafts/*` +5. 正式主图对象写入 OSS `generated-characters/*` +6. 正式发布结果写入 `asset_object` +7. 正式发布结果绑定到角色实体槽位 +8. 返回字段继续保持旧前端可消费 contract + +### 3.2 本批不解决的内容 + +1. 不落 `asset_job` 正式 SpacetimeDB 表 +2. 不落 `character_visual_asset` 强业务表 +3. 不落 `character-workflow-cache` +4. 不落 `character-animation` 全链路 +5. 不回写 `src/data/characterOverrides.json` +6. 不要求前端改成新的对象读取协议 + +## 4. 旧接口兼容 contract + +### 4.1 `POST /api/assets/character-visual/generate` + +返回结构继续保持: + +1. `ok` +2. `taskId` +3. `model` +4. `prompt` +5. `drafts` + +其中每个 `draft` 继续包含: + +1. `id` +2. `label` +3. `imageSrc` +4. `width` +5. `height` + +补充口径: + +1. `imageSrc` 继续返回旧前端习惯的 `/generated-character-drafts/*` +2. 草稿对象底层不再写本地 `public/` +3. 草稿对象真相仅在 OSS + +### 4.2 `GET /api/assets/character-visual/jobs/:taskId` + +返回结构继续保持旧前端读取方式: + +1. `taskId` +2. `kind` +3. `status` +4. `characterId` +5. `model` +6. `prompt` +7. `createdAt` +8. `updatedAt` +9. `result` +10. `errorMessage` + +当前阶段直接复用 `AiTaskService` 内存态任务快照派生,不要求前端改字段名。 + +### 4.3 `POST /api/assets/character-visual/publish` + +返回结构继续保持: + +1. `ok` +2. `assetId` +3. `portraitPath` +4. `overrideMap` +5. `saveMessage` + +补充口径: + +1. `portraitPath` 固定返回 `/generated-characters/*` +2. 当前 `overrideMap` 先返回空对象 `{}`,只做 contract 兼容,不再在 Rust 后端写本地覆盖文件 +3. `saveMessage` 明确说明当前是“已写入 OSS 并绑定业务实体” + +## 5. 业务实体与槽位约定 + +本批统一复用通用 `asset_entity_binding`。 + +### 5.1 角色主形象正式对象 + +| 字段 | 取值 | +| --- | --- | +| `entity_kind` | `character` | +| `entity_id` | `characterId` | +| `slot` | `primary_visual` | +| `asset_kind` | `character_visual` | + +补充口径: + +1. 同一角色重复发布时,允许覆盖到最新对象 +2. 候选草稿对象不创建业务绑定 +3. 业务引用真相以 `asset_entity_binding` 为准 + +## 6. OSS 对象键规划 + +### 6.1 候选草稿 + +候选草稿固定写入: + +`generated-character-drafts/{characterSegment}/visual/{taskId}/candidate-{index}.svg` + +### 6.2 正式主图 + +正式主图固定写入: + +`generated-characters/{characterSegment}/visual/{assetId}/master.svg` + +## 7. 任务状态口径 + +当前阶段不新增独立 `asset_job` 表,统一复用 `module-ai` 的内存态 `AiTaskService`。 + +### 7.1 任务种类 + +`task_kind` 统一使用: + +`custom_world_generation` + +说明: + +1. 这是当前 `module-ai` 已冻结的可用任务类型之一 +2. 本批只把它当作“生成类资产任务”的最小任务容器 +3. 后续 `asset_job` 表落地后,再把角色主形象任务迁到正式资产任务模型 + +### 7.2 阶段映射 + +当前固定使用以下阶段: + +1. `prepare_prompt` +2. `request_model` +3. `normalize_result` +4. `persist_result` + +其中: + +1. `generate` 成功后,任务直接进入 `completed` +2. `publish` 不额外创建新任务,只消费已有候选路径 + +## 8. Rust 第一批生成策略 + +本批生成策略固定为: + +1. 若已配置 `platform-llm`,则用文本模型生成一个结构化占位结果 +2. 服务端把结果渲染成 SVG 占位图 +3. 占位图写入 OSS 草稿路径 + +说明: + +1. 这不是最终的 DashScope 图片模型正式链 +2. 但它可以先把“接口 contract + 任务状态 + OSS 真相 + 正式发布绑定”全部打通 +3. 后续替换成真实图片模型时,不需要再改动主链结构 + +## 9. 服务端执行顺序 + +### 9.1 生成 + +每次调用 `generate` 固定执行: + +1. 创建 `AiTask` +2. 生成最终 prompt +3. 产出候选 SVG 字节 +4. 每个候选对象上传 OSS +5. 回写任务结果 +6. 返回 `/generated-character-drafts/*` + +### 9.2 发布 + +每次调用 `publish` 固定执行: + +1. 校验 `selectedPreviewSource` +2. 解析旧 `/generated-*` 路径为 `object_key` +3. 调 OSS `HEAD Object` 确认候选对象存在 +4. 读取候选对象内容 +5. 上传正式主图对象到 `generated-characters/*` +6. 对正式对象执行 `asset_object` 确认 +7. 对正式对象执行 `asset_entity_binding` +8. 返回 `/generated-characters/*` + +## 10. 完成定义 + +当以下条件满足时,本批视为完成: + +1. Rust 已兼容 `character-visual generate / jobs / publish` +2. 候选草稿不再写本地 `public/generated-character-drafts` +3. 正式主图不再写本地 `public/generated-characters` +4. 发布成功后能形成 `asset_object` +5. 发布成功后能形成 `asset_entity_binding` +6. 前端仍能继续消费 `taskId / drafts / portraitPath` 旧 contract + +## 11. 关联文档 + +1. [M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md) +2. [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md) +3. [SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md](./SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md) +4. [ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md](./ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md) diff --git a/docs/technical/M6_CHARACTER_WORKFLOW_CACHE_OSS_STAGE1_2026-04-22.md b/docs/technical/M6_CHARACTER_WORKFLOW_CACHE_OSS_STAGE1_2026-04-22.md new file mode 100644 index 00000000..fd41887e --- /dev/null +++ b/docs/technical/M6_CHARACTER_WORKFLOW_CACHE_OSS_STAGE1_2026-04-22.md @@ -0,0 +1,138 @@ +# M6 角色资产工作流缓存接入 OSS 第一批设计 + +日期:`2026-04-22` + +## 1. 文档目的 + +这份文档用于冻结 `M6` 第一批“角色资产工作流缓存”的真实落地口径。 + +本批只解决以下两条旧接口的 Rust 重写入口: + +1. `GET /api/assets/character-workflow-cache/:characterId` +2. `POST /api/assets/character-workflow-cache` + +目标是把旧 Node 写入本地 `public/generated-character-drafts/*/workflow-cache.json` 的临时缓存,切到 OSS JSON 草稿对象,继续保持前端当前可消费 contract。 + +## 2. 当前前提 + +当前仓库已经具备以下能力: + +1. `platform-oss::OssClient::put_object` +2. `platform-oss::OssClient::sign_get_object_url` +3. `generated-character-drafts/*` 兼容对象键前缀 +4. 角色主形象与动作导入已经开始把草稿对象写入 OSS + +因此本批不新增数据库表,也不引入本地 JSON 文件。 + +## 3. 本批范围 + +### 3.1 要完成的内容 + +1. 兼容工作流缓存读取接口 +2. 兼容工作流缓存保存接口 +3. 缓存 JSON 写入 OSS `generated-character-drafts/*` +4. 返回字段继续保持旧前端可消费 contract +5. 不再把缓存写入本地 `public/` + +### 3.2 本批不解决的内容 + +1. 不落 `asset_object` +2. 不落 `asset_manifest` +3. 不落 `character_visual_asset` +4. 不落 `character_animation_asset` +5. 不做跨设备强一致合并 +6. 不迁移历史本地缓存文件 + +## 4. 旧接口兼容 contract + +### 4.1 `GET /api/assets/character-workflow-cache/:characterId` + +返回结构继续保持: + +1. `ok` +2. `cache` + +补充口径: + +1. 未找到 OSS 缓存对象时返回 `cache: null` +2. 找到对象但 `characterId` 不匹配时返回 `cache: null` +3. 返回的 `cache` 字段保持前端 `CharacterAssetWorkflowCache` 结构 + +### 4.2 `POST /api/assets/character-workflow-cache` + +请求结构继续保持前端当前字段: + +1. `characterId` +2. `visualPromptText` +3. `animationPromptText` +4. `visualDrafts` +5. `selectedVisualDraftId` +6. `selectedAnimation` +7. `imageSrc` +8. `generatedVisualAssetId` +9. `generatedAnimationSetId` +10. `animationMap` + +返回结构继续保持: + +1. `ok` +2. `cache` +3. `saveMessage` + +## 5. OSS 对象键规划 + +缓存 JSON 固定写入: + +`generated-character-drafts/{characterSegment}/workflow-cache/workflow-cache.json` + +其中: + +1. `characterSegment` 来自 `characterId` 的安全路径片段 +2. 文件名固定为 `workflow-cache.json` +3. content type 固定为 `application/json; charset=utf-8` + +## 6. 字段归一化规则 + +保存接口固定执行以下归一化: + +1. `characterId` 必填,trim 后不能为空 +2. `visualPromptText` 最长保留 280 字 +3. `animationPromptText` 最长保留 280 字 +4. `visualDrafts` 只保留有 `imageSrc` 的候选 +5. `visualDrafts[].width` 默认 `1024` +6. `visualDrafts[].height` 默认 `1536` +7. `selectedAnimation` 默认 `idle` +8. 空 `imageSrc / generatedVisualAssetId / generatedAnimationSetId` 不序列化 +9. 非对象 `animationMap` 归一化为 `null` +10. `updatedAt` 由 Rust 服务端生成 UTC 时间 + +## 7. 元数据规范 + +缓存 JSON 对象写入以下 `x-oss-meta-*` 元数据: + +1. `asset_kind = character_workflow_cache` +2. `owner_user_id = asset-tool` +3. `entity_kind = character` +4. `entity_id = characterId` +5. `slot = workflow_cache` + +说明: + +1. 旧资产工坊接口没有显式 Bearer,第一批继续使用 `asset-tool` 作为兼容归属。 +2. workflow cache 是工作流草稿状态,不是正式可发布资产,因此本批不确认 `asset_object`。 + +## 8. 完成定义 + +当以下条件满足时,本批视为完成: + +1. Rust 已兼容 `GET /api/assets/character-workflow-cache/:characterId` +2. Rust 已兼容 `POST /api/assets/character-workflow-cache` +3. 缓存 JSON 写入 OSS `generated-character-drafts/*` +4. 未命中时返回 `cache: null` +5. 前端仍能继续消费 `cache / saveMessage` 旧 contract + +## 9. 关联文档 + +1. [M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md) +2. [M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md](./M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md) +3. [M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md](./M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md) diff --git a/docs/technical/M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md b/docs/technical/M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md new file mode 100644 index 00000000..b3364e7a --- /dev/null +++ b/docs/technical/M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md @@ -0,0 +1,158 @@ +# M6 custom world 资产接入 OSS 第一批设计 + +日期:`2026-04-22` + +## 1. 文档目的 + +这份文档用于冻结 `M6` 第一批 custom world 资产链的真实落地口径。 + +本批只解决一个明确问题: + +1. `POST /api/custom-world/scene-image` +2. `POST /api/custom-world/cover-image` +3. `POST /api/custom-world/cover-upload` + +不再把图片产物写入仓库 `public/` 本地文件,而是统一接到: + +1. `platform-oss::put_object` +2. `asset_object` +3. `asset_entity_binding` + +形成正式 `OSS + SpacetimeDB 元数据` 真相链。 + +## 2. 当前前提 + +当前仓库已经具备以下基础能力: + +1. `POST /api/assets/direct-upload-tickets` +2. `GET /api/assets/read-url` +3. `POST /api/assets/objects/confirm` +4. `POST /api/assets/objects/bind` +5. `platform-oss::OssClient::put_object` +6. `spacetime-module` 中的 `asset_object / asset_entity_binding` + +因此本批不重新设计新资产系统,只复用既有 `assets` 主链。 + +## 3. 本批范围 + +### 3.1 要完成的内容 + +1. custom world 场景图生成结果写入 OSS +2. custom world 封面图生成结果写入 OSS +3. custom world 封面上传结果写入 OSS +4. 每个写入对象都执行一次正式对象确认 +5. 每个正式对象都绑定到 custom world 业务实体槽位 +6. 路由响应继续返回旧前端可消费的 `imageSrc` + +### 3.2 本批不解决的内容 + +1. 不补 DashScope 图片模型的完整 Rust 编排 +2. 不补 `cover-upload` 的裁剪、压缩、16:9 强校验全量能力 +3. 不新增 `scene_image_asset / character_visual_asset` 强业务表 +4. 不在本批落 `custom_world_asset_link` +5. 不把旧前端响应 contract 改成直接返回 OSS URL + +## 4. 业务实体与槽位约定 + +本批统一复用通用 `asset_entity_binding`。 + +### 4.1 场景图 + +| 字段 | 取值 | +| --- | --- | +| `entity_kind` | `custom_world_landmark` | +| `entity_id` | 优先 `landmarkId`,否则回退 `landmarkName` | +| `slot` | `scene_image` | +| `asset_kind` | `scene_image` | + +### 4.2 封面图 + +| 字段 | 取值 | +| --- | --- | +| `entity_kind` | `custom_world_profile` | +| `entity_id` | 优先 `profileId`,否则回退世界 `id/name` | +| `slot` | `cover` | +| `asset_kind` | `custom_world_cover` | + +补充口径: + +1. 绑定幂等键仍是 `entity_kind + entity_id + slot` +2. 同一 profile 重复生成/上传封面时,允许覆盖到最新对象 +3. 同一 landmark 重复生成场景图时,允许覆盖到最新对象 + +## 5. OSS 对象键与返回 contract + +### 5.1 对象键 + +场景图固定写入: + +`generated-custom-world-scenes/{profileSegment}/{landmarkSegment}/{assetId}/scene.{ext}` + +封面图固定写入: + +`generated-custom-world-covers/{profileSegment}/{assetId}/cover.{ext}` + +### 5.2 返回 contract + +路由响应继续沿用旧前端使用的字段: + +1. `imageSrc` +2. `assetId` +3. `sourceType` +4. `model` +5. `size` +6. `taskId` +7. `prompt` +8. `actualPrompt` + +其中: + +1. `imageSrc` 固定返回 `legacyPublicPath`,也就是旧 `/generated-*` 路径 +2. 前端若要真正读取私有 OSS 对象,仍必须通过 `GET /api/assets/read-url` 换签名读 URL +3. 不直接把 `signedUrl` 塞进 custom world 业务返回,避免把短期读签名误存成长期业务字段 + +## 6. 服务端执行顺序 + +每次 custom world 图片产出固定执行以下顺序: + +1. 生成或接收图片字节 +2. 调 `platform-oss::put_object` +3. 通过 `HEAD Object` 真值确认对象 +4. 写入 `asset_object` +5. 写入 `asset_entity_binding` +6. 返回 `legacyPublicPath` + +注意: + +1. `put_object` 成功不代表已完成正式落库 +2. `asset_object` 仍必须经过确认链路写入 +3. 业务引用真相以 `asset_entity_binding` 为准,不以 OSS 上是否存在 key 为准 + +## 7. 与 M5 的衔接 + +`M5` 为保证前端不断链,曾允许 `scene-image / cover-image / cover-upload` 先写本地 `public/`。 + +从本批开始,这个临时口径失效,统一改为: + +1. 二进制对象只进 OSS +2. 元数据只进 `asset_object` +3. 业务槽位只进 `asset_entity_binding` + +这样 `Stage9` 的兼容路由就不会继续偏离 `M6` 正式资产主链。 + +## 8. 完成定义 + +当以下条件满足时,本批视为完成: + +1. custom world 三条图片兼容路由不再写本地 `public/` +2. 路由成功返回的 `imageSrc` 全部来自 `OSS legacyPublicPath` +3. 每次成功写图后都能在 SpacetimeDB 中形成 `asset_object` +4. 每次成功写图后都能形成对应 `asset_entity_binding` +5. 旧前端仍可继续使用返回的 `/generated-*` 路径配合读签名服务显示图片 + +## 9. 关联文档 + +1. [M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md](./M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md) +2. [ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md](./ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md) +3. [ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md](./ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md) +4. [SPACETIMEDB_CUSTOM_WORLD_WORKS_AND_AGENT_EXTENSION_STAGE9_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_WORKS_AND_AGENT_EXTENSION_STAGE9_DESIGN_2026-04-22.md) diff --git a/docs/technical/M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE2_2026-04-22.md b/docs/technical/M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE2_2026-04-22.md new file mode 100644 index 00000000..6594ee30 --- /dev/null +++ b/docs/technical/M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE2_2026-04-22.md @@ -0,0 +1,228 @@ +# M6 custom world 场景图 / 封面图 Stage 2 设计 + +日期:`2026-04-22` + +## 1. 文档目的 + +这份文档用于冻结 custom world 图片链在 `Stage 1` 之后的第二批迁移口径。 + +`Stage 1` 已完成: + +1. `scene-image / cover-image / cover-upload` 不再写仓库 `public/` +2. 图片对象统一写入 `OSS` +3. 写入后统一形成 `asset_object + asset_entity_binding` + +但当前仍有两段能力没有迁完: + +1. `scene-image / cover-image` 仍使用 Rust SVG 占位图,而不是 Node 旧链路里的真实 DashScope 图片生成 +2. `cover-upload` 仍未迁移 Node 旧链路里的 `cropRect + 16:9 裁剪 + WebP 压缩` + +本批目标就是把这两段缺失能力补齐,同时继续保持 `Stage 1` 已冻结的 OSS 真相链。 + +## 1.1 当前落地结果 + +`2026-04-22` 已按本文口径完成 Rust `api-server` Stage 2 落地: + +1. `POST /api/custom-world/scene-image` 已切到真实 DashScope 图片生成 +2. `POST /api/custom-world/cover-image` 已切到真实 DashScope 图片生成 +3. `POST /api/custom-world/cover-upload` 已补齐 `cropRect + 16:9 + 1600x900 + WebP + 1.5 MB` +4. 三条链路继续统一写入 `OSS + asset_object + asset_entity_binding` +5. `/generated-custom-world-scenes/*` 与 `/generated-custom-world-covers/*` 旧读取路径兼容口径保持不变 + +本次同时补齐的兼容细节: + +1. `scene-image` 新增兼容读取 `negativePrompt / referenceImageSrc / userPrompt / profile / landmark` +2. `cover-image` 新增兼容读取 `referenceImageSrc / characterRoleIds` +3. `cover-upload` 新增兼容读取 `cropRect` +4. 参考图输入在 Rust 端兼容两种来源: + - `data:image/*;base64,...` + - 现有 `/generated-*` 旧路径,通过 OSS 短签名回读后转为 Data URL + +本批验证结果: + +1. `cargo check -p api-server` 通过 +2. `cargo test -p api-server custom_world_ai` 通过 +3. `npm run check:encoding` 通过 + +## 2. 本批范围 + +### 2.1 要完成的内容 + +1. `POST /api/custom-world/scene-image` 接入真实 DashScope 图片生成 +2. `POST /api/custom-world/cover-image` 接入真实 DashScope 图片生成 +3. `POST /api/custom-world/cover-upload` 接入裁剪、缩放、压缩 +4. 生成后的图片仍统一写入 `OSS` +5. 每次成功写图仍统一形成 `asset_object + asset_entity_binding` +6. 路由响应继续保持旧前端字段形状 + +### 2.2 本批不解决的内容 + +1. 不引入新的 custom world 图片任务表 +2. 不引入 `signedUrl` 直返业务字段 +3. 不在本批补视频 Range、分片传输或前端编辑器新交互 +4. 不在本批迁移更多 custom world 非图片媒体链路 + +## 3. 旧 Node 口径对齐 + +### 3.1 场景图生成 + +Node 旧链路区分两种模式: + +1. 无参考图:走 DashScope `text2image` +2. 有参考图:走 DashScope `multimodal-generation` + +本批 Rust 继续保持同口径: + +1. `referenceImageSrc` 为空时: + - 模型默认 `wan2.2-t2i-flash` + - 路径:`/services/aigc/text2image/image-synthesis` + - 异步创建任务后轮询 `/tasks/{taskId}` +2. `referenceImageSrc` 非空时: + - 模型默认 `qwen-image-2.0` + - 路径:`/services/aigc/multimodal-generation/generation` + - 直接取返回中的第一张图 + +### 3.2 封面图生成 + +Node 旧链路也区分两种模式: + +1. 无参考图:`wan2.2-t2i-flash` +2. 有参考图:`qwen-image-2.0` + +Rust 本批保持一致,并继续沿用: + +1. `profile + opening act + selected roles + landmarks` 作为 prompt 上下文 +2. 最多 6 张参考图 +3. 返回 `sourceType = generated` + +### 3.3 封面上传 + +Node 旧链路对上传封面有明确处理: + +1. 请求必须提供 `cropRect` +2. `cropRect` 必须保持 `16:9` +3. 输出固定缩放为 `1600x900` +4. 输出格式固定为 `webp` +5. 输出体积上限 `1.5 MB` +6. 原图体积上限 `10 MB` + +Rust 本批必须保持这组兼容约束。 + +## 4. 请求与响应 contract + +### 4.1 `POST /api/custom-world/scene-image` + +在 `Stage 1` 字段基础上,Rust 本批补齐兼容读取: + +1. `negativePrompt` +2. `referenceImageSrc` + +返回仍为: + +1. `imageSrc` +2. `assetId` +3. `model` +4. `size` +5. `taskId` +6. `prompt` +7. `actualPrompt` + +### 4.2 `POST /api/custom-world/cover-image` + +继续兼容: + +1. `profile` +2. `userPrompt` +3. `referenceImageSrc` +4. `characterRoleIds` +5. `size` + +返回仍为: + +1. `imageSrc` +2. `assetId` +3. `sourceType = generated` +4. `model` +5. `size` +6. `taskId` +7. `prompt` +8. `actualPrompt` + +### 4.3 `POST /api/custom-world/cover-upload` + +继续兼容: + +1. `profileId` +2. `worldName` +3. `imageDataUrl` +4. `cropRect` + +返回仍为: + +1. `imageSrc` +2. `assetId` +3. `sourceType = uploaded` + +## 5. 服务端执行顺序 + +### 5.1 场景图 / 封面图生成 + +统一执行: + +1. 归一 prompt 与模型选择 +2. 向 DashScope 发起生成请求 +3. 下载生成结果图片二进制 +4. `put_object` +5. `HEAD Object` +6. `confirm asset_object` +7. `bind asset_entity_binding` +8. 返回 `legacyPublicPath` + +### 5.2 封面上传 + +统一执行: + +1. 解析 `imageDataUrl` +2. 校验原图体积 +3. 解码图片 +4. 按 `cropRect` 裁剪 +5. 校验裁剪区域 `16:9` +6. 缩放到 `1600x900` +7. 编码为 `webp` +8. 若超过 `1.5 MB`,逐档降低质量重试 +9. `put_object` +10. `HEAD Object` +11. `confirm asset_object` +12. `bind asset_entity_binding` +13. 返回 `legacyPublicPath` + +## 6. 环境变量与模型口径 + +本批继续复用现有 DashScope 环境变量,不新增另一套命名: + +1. `DASHSCOPE_BASE_URL` +2. `DASHSCOPE_API_KEY` +3. `DASHSCOPE_IMAGE_REQUEST_TIMEOUT_MS` + +模型默认值固定为: + +1. 场景图文生图:`wan2.2-t2i-flash` +2. 场景图参考图模式:`qwen-image-2.0` +3. 封面文生图:`wan2.2-t2i-flash` +4. 封面参考图模式:`qwen-image-2.0` + +## 7. 完成定义 + +当以下条件满足时,本批视为完成: + +1. `scene-image` 不再返回 Rust SVG 占位图 +2. `cover-image` 不再返回 Rust SVG 占位图 +3. `cover-upload` 已执行 `cropRect + 16:9 + webp + 1.5MB` +4. 三条链路仍统一落到 `OSS + asset_object + asset_entity_binding` +5. 前端无需改 contract 即可继续消费 + +## 8. 关联文档 + +1. [M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md) +2. [ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md](./ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md) +3. [M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md](./M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md) diff --git a/docs/technical/M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md b/docs/technical/M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md new file mode 100644 index 00000000..b1c8998c --- /dev/null +++ b/docs/technical/M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md @@ -0,0 +1,75 @@ +# M6 旧 generated 路径 OSS 读取兼容设计 + +日期:`2026-04-22` + +## 1. 文档目的 + +这份文档冻结 `M6` 第一批 OSS 化之后,旧前端继续访问 `/generated-*` 路径的 Rust 后端兼容口径。 + +当前角色主形象、角色动作、custom world 场景图和封面图已经把新生成资产写入私有 OSS。旧前端仍会把以下路径当作图片、视频或动作帧地址直接交给 ``、`