Merge branch 'codex/backend-rewrite-spacetimedb' of http://82.157.175.59:3000/GenarrativeAI/Genarrative into codex/backend-rewrite-spacetimedb

This commit is contained in:
2026-04-22 20:37:56 +08:00
82 changed files with 26950 additions and 1312 deletions

View File

@@ -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. 灰度环境双跑对比。

View File

@@ -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”。

View File

@@ -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 放进新 crateroute boundary 仍属于 `api-server`
这一步完成后,`api-server``runtime_story/compat.rs` 应该只负责:
1. 从 HTTP 请求恢复 / 持久化 snapshot
2. 调用 `module-runtime-story-compat` 产出确定性动作结果或状态响应
3. 需要时调用本地 AI 增强
4. 将最终响应包回 `Json<Value>`
这就是从“`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 组合逻辑

View File

@@ -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<String>`
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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -0,0 +1,75 @@
# M6 旧 generated 路径 OSS 读取兼容设计
日期:`2026-04-22`
## 1. 文档目的
这份文档冻结 `M6` 第一批 OSS 化之后,旧前端继续访问 `/generated-*` 路径的 Rust 后端兼容口径。
当前角色主形象、角色动作、custom world 场景图和封面图已经把新生成资产写入私有 OSS。旧前端仍会把以下路径当作图片、视频或动作帧地址直接交给 `<img>``<video>`、canvas 抽帧或 `CharacterAnimator`
1. `/generated-character-drafts/*`
2. `/generated-characters/*`
3. `/generated-animations/*`
4. `/generated-custom-world-scenes/*`
5. `/generated-custom-world-covers/*`
6. `/generated-qwen-sprites/*`
如果只提供 `/api/assets/read-url`,旧 UI 中直接消费资源路径的位置会继续失败。因此本批补一个同源读取兼容层。
## 2. 本批范围
### 2.1 要完成的内容
1. Rust `api-server` 挂接上述六类 `GET /generated-*/*` 路由。
2. 路由把 legacy path 转成 OSS `object_key`
3. 路由使用服务端 OSS 主凭证生成短期私有读签名。
4. 路由由服务端拉取 OSS 对象并同源返回二进制内容。
5. 返回保留 OSS 的 `content-type`,补充 `cache-control`让图片、视频、SVG、JSON manifest 都能被旧前端直接消费。
6. Vite 本地开发代理补齐 `/generated-animations``/generated-custom-world-covers`,避免新 OSS 路径在开发期落回本地 `public/`
### 2.2 本批不解决的内容
1. 不把私有 OSS 对象改成公开读。
2. 不引入 CDN。
3. 不把对象缓存到本地 `public/`
4. 不迁移历史本地文件。
5. 不实现 Range 分片视频流Stage 1 先全量代理对象,后续如视频体积变大再补 Range。
## 3. 路由契约
每条旧路径均返回原始资源内容:
1. 成功:`200`body 为 OSS 对象二进制内容。
2. OSS 对象不存在:`404`
3. OSS 配置缺失:`503`
4. object key 不在受支持 `generated-*` 前缀:`400`
5. OSS 请求失败:`502`
响应头:
1. `content-type`:优先使用 OSS 响应头。
2. `cache-control``private, max-age=60`
3. `x-genarrative-asset-object-key`:回写解析后的 OSS object key方便调试。
## 4. 对象键约定
旧路径去掉开头 `/` 后就是 OSS `object_key`
示例:
`/generated-animations/hero/animation-set-1/idle/frame01.png`
对应:
`generated-animations/hero/animation-set-1/idle/frame01.png`
## 5. 完成定义
当以下条件满足时,本批路径兼容视为完成:
1. Rust 已挂接六类 `/generated-*` 路由。
2. 路由能通过 OSS 私有读签名同源代理对象内容。
3. `cargo check -p api-server` 通过。
4. `scripts/check-encoding.mjs` 覆盖本轮新增文档和相关代码。
5. `05_M6_ASSETS_OSS_EDITOR.md` 中路径兼容项完成勾选。

View File

@@ -0,0 +1,133 @@
# M7 联调、回归、部署与切流执行方案
日期:`2026-04-22`
## 1. 文档目标
这份文档把 `M7联调、回归、部署与切流任务清单` 从高层勾选项细化为可直接执行的工程方案。
M7 的目标不是新增玩法功能,而是在 `M0 ~ M6` 已迁移的 Rust 后端基础上完成切流前收口:
1. 固定本地、灰度、切流前的检查命令。
2. 固定 `Axum + SpacetimeDB + OSS` 的部署与回滚口径。
3. 固定观测字段、慢请求、上游失败日志与资产任务日志。
4. 固定旧 `server-node` 与新 `server-rs` 的双跑和 API 对比方式。
5. 等价拆分 `server-rs/crates/spacetime-module/src/lib.rs`,避免 SpacetimeDB 主工程继续退化为单大文件。
## 2. 执行约束
1. 不改变现有 HTTP contract、SSE contract、SpacetimeDB 表名、reducer 名、procedure 名和对象键前缀。
2. 不把 LLM、OSS、短信、微信等外部副作用移入 SpacetimeDB reducer。
3. `spacetime-module` 拆分只做物理结构收口,不做 schema 重命名、字段删除、字段重排或 reducer/procedure 改名。
4. 迁移期保留 `server-node` 作为回退锚点M7 不删除旧后端。
5. 前端切换默认仍指向 Node只有显式设置 `GENARRATIVE_BACKEND_STACK=rust``GENARRATIVE_RUNTIME_SERVER_TARGET` 时才切到 Rust。
## 3. 测试体系
M7 固定四层测试入口:
1. Rust crate 级别:`cargo check/test` 覆盖 `api-server``spacetime-module``shared-contracts` 与模块 crate。
2. Axum handler 级别:继续复用 `api-server` 内已有 `build_router + tower::ServiceExt` 测试,重点覆盖 `healthz/auth/runtime/assets/custom-world/story` 的兼容响应。
3. SpacetimeDB 模块级别:`cargo check -p spacetime-module` 作为 schema/reducer/procedure 的最低门禁;需要真实数据库行为时使用 `spacetime publish --server local --yes` 后再跑 smoke。
4. 端到端主流程:`server-rs/scripts/smoke.ps1``server-rs/scripts/oss-smoke.ps1` 分别覆盖基础 HTTP contract 与真实 OSS 链路。
推荐本地顺序:
```powershell
.\server-rs\scripts\m7-preflight.ps1
.\server-rs\scripts\smoke.ps1
node scripts\run-tsx.cjs scripts\m7-api-compare.ts
```
## 4. 部署准备
Axum 部署方式:
1. `cargo build -p api-server --release` 生成发布二进制。
2. 进程环境显式配置 `GENARRATIVE_API_HOST``GENARRATIVE_API_PORT``GENARRATIVE_API_LOG`
3. 反向代理继续保留 `Host``X-Forwarded-For``X-Forwarded-Proto``X-Request-Id`
4. SSE 路由必须禁用代理缓冲。
SpacetimeDB 发布方式:
1. 本地开发先执行 `server-rs/scripts/spacetime-dev.ps1` 启动 standalone。
2. 发布模块使用 `spacetime publish genarrative-dev --server local --yes --module-path server-rs/crates/spacetime-module`
3. 若需要重置开发库,必须显式加 `--clear-database --yes`,不得默认清库。
4. 生成绑定时使用仓库根目录 `spacetime.json` 中的 `typescript``rust` 输出目录。
OSS / CDN / 域名方案:
1. 正式对象真相仍为 `bucket + object_key`
2. bucket 默认私有读写,浏览器不直接匿名读取。
3. `/generated-*` 旧路径由 Axum 同源代理或 CDN 边缘回源到 Rust API。
4. CDN 只缓存可公开缓存的派生读结果,不把私有签名 URL 写入业务表。
环境变量最小清单:
1. `GENARRATIVE_API_HOST``GENARRATIVE_API_PORT``GENARRATIVE_API_LOG`
2. `GENARRATIVE_JWT_ISSUER``GENARRATIVE_JWT_SECRET`
3. `GENARRATIVE_SPACETIME_SERVER_URL``GENARRATIVE_SPACETIME_DATABASE``GENARRATIVE_SPACETIME_TOKEN`
4. `ALIYUN_OSS_BUCKET``ALIYUN_OSS_ENDPOINT``ALIYUN_OSS_ACCESS_KEY_ID``ALIYUN_OSS_ACCESS_KEY_SECRET`
5. `GENARRATIVE_LLM_PROVIDER``GENARRATIVE_LLM_BASE_URL``GENARRATIVE_LLM_API_KEY`
6. `DASHSCOPE_BASE_URL``DASHSCOPE_API_KEY`
7. `SMS_AUTH_ENABLED` 与短信供应商变量
8. `WECHAT_AUTH_ENABLED` 与微信 OAuth 变量
9. `GENARRATIVE_BACKEND_STACK``NODE_SERVER_TARGET``RUST_SERVER_TARGET``GENARRATIVE_RUNTIME_SERVER_TARGET`
## 5. 灰度与切流
灰度环境固定为三段:
1. `shadow`Node 继续承接用户流量Rust 只由脚本和内部账号请求。
2. `dual-run`:同一组 smoke/API compare 同时打 Node 与 Rust差异必须登记。
3. `rust-primary`:反向代理或 Vite dev proxy 指向 RustNode 进程保留但不作为主入口。
前端切换方式:
1. 默认 `GENARRATIVE_BACKEND_STACK=node`
2. 本地或灰度切 Rust 设置 `GENARRATIVE_BACKEND_STACK=rust`,并配置 `RUST_SERVER_TARGET`
3. 紧急回退设置 `GENARRATIVE_BACKEND_STACK=node` 或直接覆盖 `GENARRATIVE_RUNTIME_SERVER_TARGET` 指回 Node。
## 6. API 对比
`scripts/m7-api-compare.ts` 负责对比 Node 与 Rust 的基础 contract
1. 默认对比 `/healthz``/api/auth/login-options`
2. 可通过 `M7_COMPARE_PATHS` 扩展只读路径清单。
3. 对比时会固定传入 `x-request-id`,并归一化 `requestId / timestamp / latencyMs` 等波动字段。
4. 默认严格模式下发现差异直接返回非零退出码。
该脚本只承担“无状态 GET contract”对比带登录、写入、OSS 或 SSE 的主流程仍由专门 smoke 脚本负责。
## 7. 观测能力
M7 观测字段固定为:
1. HTTP 访问日志:`method``uri``status``latency_ms``slow_request``request_id`
2. 错误日志:`request_id``status``error_code`
3. 上游失败:`provider``operation``request_id``status/code``message`
4. 关键 reducer操作名、主实体 ID、结果状态
5. 资产任务:`task_id``character_id/entity_id``asset_kind``status`
慢请求阈值默认 `1000ms`,可通过 `GENARRATIVE_SLOW_REQUEST_THRESHOLD_MS` 覆盖。
## 8. 数据迁移与回滚
当前 M7 不做一次性“Node PostgreSQL 全量导入 SpacetimeDB”的危险迁移采用双跑验证与按主链确认的渐进策略
1. 已迁移主链以 SpacetimeDB 为真相源。
2. 未迁移或灰度失败主链继续回退到 Node。
3. 资产二进制以 OSS 为真相,不回滚到本地 `public/generated-*` 写盘。
4. 若 SpacetimeDB schema 需要清库重发,只允许在开发库或明确灰度库执行 `--clear-database`
5. 生产回滚优先切反向代理目标,不优先改代码。
## 9. 验收定义
M7 完成时必须满足:
1. M7 文档、脚本、任务清单均同步。
2. `api-server``spacetime-module` 至少通过 `cargo check`
3. 基础 smoke 脚本可执行,并覆盖 `healthz + envelope + request id`
4. Node/Rust API 对比脚本可执行。
5. Vite dev proxy 已具备 Node/Rust 切换与回退开关。
6. `spacetime-module` 已从单 `lib.rs` 拆为按 `runtime / gameplay / custom_world / asset_metadata / ai` 组织的文件结构。

View File

@@ -3,6 +3,8 @@
> 该文档由 `server-node/src/manifest/backendCapabilityManifest.ts` 自动生成。
> 生成命令:`npm run server-node:manifest:backend`
> 生成时间:`2026-04-20T14:26:38.663Z`
>
> 过期说明:该索引生成于 `2026-04-20`,其中 `createQwenSpriteRoutes` 与 `/api/assets/qwen-sprite/*` 相关描述已在 `2026-04-21` 后失效。当前 Node 现役资产挂载面仅保留 `createCharacterAssetRoutes``Qwen` 仅剩 prompt 模板复用与 `/generated-qwen-sprites/*` 历史路径兼容,不再存在独立路由主链。
## 总览

View File

@@ -4,6 +4,9 @@
## 文档列表
- [RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md](./RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md):冻结 Rust 本地一键联调脚本与 Ubuntu 发布包构建脚本的执行口径,覆盖 `npm run dev:rust``npm run build:rust:ubuntu`、Vite release、Linux `api-server`、SpacetimeDB wasm、启动停止脚本和安全清库开关。
- [RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md](./RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md):记录当前 Rust `api-server` 已挂载的 96 条 Axum 路由,按 auth、assets、runtime、custom world、story、generated path 等挂载面归类,用于对照 Node 能力基线与切流 smoke 清单。
- [BACKEND_REWRITE_CROSS_CUTTING_GOVERNANCE_2026-04-22.md](./BACKEND_REWRITE_CROSS_CUTTING_GOVERNANCE_2026-04-22.md):冻结后端重写收口阶段的横向治理规则,覆盖 TypeScript contract 到 Rust DTO 映射、SpacetimeDB schema 演进、大对象 / workflow cache 存储边界和文档维护门禁。
- [PLATFORM_LLM_TEXT_GATEWAY_DESIGN_2026-04-21.md](./PLATFORM_LLM_TEXT_GATEWAY_DESIGN_2026-04-21.md)`platform-llm` 文本模型网关首版设计,冻结 OpenAI 兼容 `/chat/completions`、SSE 增量解析、错误模型与重试边界。
- [API_SERVER_PLATFORM_LLM_PROXY_DESIGN_2026-04-21.md](./API_SERVER_PLATFORM_LLM_PROXY_DESIGN_2026-04-21.md)`api-server` 接入 `platform-llm` 的最小代理设计,冻结 `/api/llm/chat/completions` 的配置、状态注入与首版非流式兼容边界。
- [PHONE_SMS_LOGIN_STAGE_A_IMPLEMENTATION_2026-04-21.md](./PHONE_SMS_LOGIN_STAGE_A_IMPLEMENTATION_2026-04-21.md):冻结手机号验证码登录第一阶段的真实落地边界,明确游客兜底默认关闭、公开请求不污染登录态,以及 smoke 必须覆盖短信登录主链。
@@ -37,6 +40,8 @@
- [SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md)`M2` 第一张身份主表 `user_account` 的职责边界、字段、唯一约束、状态迁移、旧 `users` 映射与落地约束。
- [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md):基于当前 Node 后端能力清单,设计用 `SpacetimeDB + Axum + 阿里云 OSS` 重写后端的目标架构、模块映射、数据分层、迁移顺序与验收标准。
- [M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md](./M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md):冻结 M6 剩余的 STS 与服务端上传 helper 落地口径,明确当前上传主链为服务器上传 OSSWeb 端只负责签名读下载。
- [M6_ASSET_METADATA_HASH_VERSION_AND_SPECIALIZED_TABLE_BOUNDARY_2026-04-22.md](./M6_ASSET_METADATA_HASH_VERSION_AND_SPECIALIZED_TABLE_BOUNDARY_2026-04-22.md):冻结 M6 第一批内容 hash、版本、manifest、asset job 与强业务资产表的 Stage 1 边界,明确当前使用 `asset_object + asset_entity_binding + OSS manifest + AiTaskService` 闭合,不重复新增表。
- [M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md](./M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md):冻结 M6 旧 `/generated-*` 路径到 OSS 私有读同源代理的兼容口径,保证旧前端仍能直接消费图片、视频、动作帧与 manifest。
- [AXUM_TO_SPACETIMEDB_ASSET_OBJECT_CONFIRM_CALL_DESIGN_2026-04-21.md](./AXUM_TO_SPACETIMEDB_ASSET_OBJECT_CONFIRM_CALL_DESIGN_2026-04-21.md):冻结 `POST /api/assets/objects/confirm` 从 Axum 通过 Rust SDK 调用 `SpacetimeDB procedure` 的最小落地方案,明确本地 server、数据库名、procedure/reducer 分工与 `spacetime-client` 边界。
- [M3_RUNTIME_SETTINGS_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md](./M3_RUNTIME_SETTINGS_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md):冻结 `M3` 首批 `runtime settings` 纵向切片的表字段、默认值、procedure、Axum facade、错误 contract 与测试策略。
- [SPACETIMEDB_CUSTOM_WORLD_AGENT_SESSION_STAGE6_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_AGENT_SESSION_STAGE6_DESIGN_2026-04-22.md):冻结 `M5` Agent session create / snapshot 的最小 SpacetimeDB 与 Axum facade 闭环,明确本轮不迁移 LLM、SSE、卡片更新和完整 action registry。
@@ -45,8 +50,16 @@
- [BIG_FISH_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md](./BIG_FISH_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md):冻结大鱼吃小鱼玩法本轮最小完整落地方案,明确 `module-big-fish`、SpacetimeDB 表 / procedure、Axum facade、前端接入和运行态规则边界。
- [PUZZLE_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md](./PUZZLE_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md):冻结拼图玩法本轮最小完整落地方案,明确 `module-puzzle`、SpacetimeDB 表 / procedure、Axum facade、前端接入以及交换 / 合并 / 拖动 / 拆分 / 下一关推荐边界。
- [UNIFIED_CREATION_AGENT_CHAT_FRAMEWORK_2026-04-22.md](./UNIFIED_CREATION_AGENT_CHAT_FRAMEWORK_2026-04-22.md):冻结所有创作品类 Agent 聊天 UI 与对话进度管理统一框架,明确品类差异只保留锚点映射、提示词/话术和 action。
- [RUST_API_SERVER_SSE_INFRASTRUCTURE_DESIGN_2026-04-22.md](./RUST_API_SERVER_SSE_INFRASTRUCTURE_DESIGN_2026-04-22.md):冻结 `server-rs/crates/api-server` 的 SSE 使用口径,明确统一使用 Axum 内建 `Sse<Event>`,不再保留自定义 `sse.rs` 模块。
- [SPACETIMEDB_CUSTOM_WORLD_LIBRARY_DETAIL_STAGE5_EXTENSION_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_LIBRARY_DETAIL_STAGE5_EXTENSION_DESIGN_2026-04-22.md):补齐 `M5` Stage 5 遗漏的 owner-only `GET /api/runtime/custom-world-library/:profileId` 设计,冻结单条 profile detail 的 SpacetimeDB procedure、client facade、404 语义与 Axum 路由扩展方式。
- [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):冻结 `M5` 剩余主链的 works、card detail、publish gate、supportedActions、action registry 与 AI/OSS 兼容路由边界,作为 Stage 9 到收口阶段的统一落地依据。
- [M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md):冻结 `M6` 第一批 custom world 场景图、封面图、封面上传从本地 `public/` 临时落地切到 `OSS + asset_object + asset_entity_binding` 正式真相链的边界与槽位约定。
- [M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE2_2026-04-22.md](./M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE2_2026-04-22.md):冻结 `M6` 第二批 custom world 图片链迁移口径,明确把 `scene-image / cover-image` 从 Rust SVG 占位切到真实 DashScope 图片生成,并补回 `cover-upload``cropRect + 16:9 + WebP 压缩`
- [M6_CHARACTER_ANIMATION_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CHARACTER_ANIMATION_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md):冻结 `M6` 第一批角色动作 `generate / jobs / publish` 接口从旧本地 `public/generated-*` 真相切到 `OSS + asset_object + asset_entity_binding + AI task` 的最小闭环与兼容 contract。
- [M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md](./M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md):冻结 `M6` 第一批角色动作模板查询与参考视频导入从旧 Node 本地草稿写盘切到 Rust `OSS` 草稿对象的接口 contract、对象键规划与暂不确认 `asset_object` 的边界。
- [M6_CHARACTER_WORKFLOW_CACHE_OSS_STAGE1_2026-04-22.md](./M6_CHARACTER_WORKFLOW_CACHE_OSS_STAGE1_2026-04-22.md):冻结 `M6` 第一批角色资产工作流缓存从旧 Node 本地 `workflow-cache.json` 切到 Rust `OSS` JSON 草稿对象的读写 contract、字段归一化与暂不落正式资产表的边界。
- [M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md):冻结 `M6` 第一批角色主形象 `generate / jobs / publish` 接口从旧本地 `public/generated-*` 真相切到 `OSS + asset_object + asset_entity_binding + AI task` 的最小闭环与兼容 contract。
- [M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md](./M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md):冻结 `M7` 联调、回归、部署、观测、双跑对比、灰度切流、回滚和 `spacetime-module` 结构收口的可执行方案。
- [M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md](./M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md):冻结 `M3` 第二批 `browse history` 纵向切片的 `user_browse_history` 表、双路径 facade、宽松归一化、去重排序规则与测试策略。
- [ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md](./ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md):冻结已确认 `asset_object` 绑定到业务实体槽位的首版 reducer/procedure、通用 `asset_entity_binding` 表与 Axum facade。
- [FRONTEND_TO_BACKEND_MIGRATION_EXECUTION_PLAN_2026-04-21.md](./FRONTEND_TO_BACKEND_MIGRATION_EXECUTION_PLAN_2026-04-21.md)把鉴权、浏览历史、runtime story 快照、NPC 待接委托与正式生成编排继续后移到 Express 后端的实施方案与验收口径。
@@ -74,6 +87,7 @@
- [M4_RPG_RUNTIME_STORY_SPACETIMEDB_BASELINE_2026-04-21.md](./M4_RPG_RUNTIME_STORY_SPACETIMEDB_BASELINE_2026-04-21.md):记录 `server-rs``M4` 首轮已落地的 `story_session / story_event` SpacetimeDB 基座、`begin_story_session / continue_story` reducer、同步返回快照的 story procedure、`spacetime-client` facade 与新的 `/api/story/sessions*` Axum 接口,以及当前尚未兼容旧 `runtime story` 路由的边界。
- [M4_RPG_RUNTIME_STORY_SESSION_STATE_QUERY_DESIGN_2026-04-22.md](./M4_RPG_RUNTIME_STORY_SESSION_STATE_QUERY_DESIGN_2026-04-22.md):冻结 `GET /api/story/sessions/:storySessionId/state` 这条最小 story state 查询切片,明确当前只返回 `storySession + storyEvents`,不等价于旧 `runtime story state` 兼容完成。
- [M4_RUNTIME_STORY_COMPAT_STATE_BRIDGE_DESIGN_2026-04-22.md](./M4_RUNTIME_STORY_COMPAT_STATE_BRIDGE_DESIGN_2026-04-22.md):冻结旧 `POST /api/runtime/story/state/resolve` 兼容桥的首版边界,明确先补 `RuntimeStoryActionResponse` DTO 与状态桥,再继续进入 Rust `actions/resolve` 与正式 snapshot projection。
- [M4_RUNTIME_STORY_RS_SPLIT_PLAN_2026-04-22.md](./M4_RUNTIME_STORY_RS_SPLIT_PLAN_2026-04-22.md):冻结 `runtime_story.rs` 从超大单文件拆到 `compat/ai/presentation/tests/battle/core/game_state/forge/npc_support/*_actions` 子模块的收口策略、验证要求与下一阶段纯规则下沉边界。
- [M4_MODULE_AI_BASELINE_DESIGN_2026-04-21.md](./M4_MODULE_AI_BASELINE_DESIGN_2026-04-21.md):冻结 `module-ai` 首版的任务/阶段/流式片段/结果引用领域模型、最小内存服务与后续 `platform-llm` / `api-server` / `spacetime-module` 的边界。
- [M4_MODULE_AI_SPACETIMEDB_BASELINE_2026-04-21.md](./M4_MODULE_AI_SPACETIMEDB_BASELINE_2026-04-21.md):记录 `module-ai``spacetime-module` 中首轮已落地的 `ai_task / ai_task_stage / ai_text_chunk / ai_result_reference` 真相表、最小 reducer/procedure 与当前仍未扩到真实模型调用和 Axum facade 的边界。
- [M4_MODULE_AI_AXUM_FACADE_DESIGN_2026-04-22.md](./M4_MODULE_AI_AXUM_FACADE_DESIGN_2026-04-22.md):冻结 `module-ai``shared-contracts``spacetime-client``api-server` 的最小 AI task mutation facade明确 `start` 路由当前只返回 `202 Accepted`

View File

@@ -0,0 +1,159 @@
# Rust API Server 路由索引2026-04-22
更新时间:`2026-04-22`
## 1. 文档目标
本文件记录当前 `server-rs/crates/api-server/src/app.rs` 中已挂载的 Rust Axum 路由面,用于对照 Node 后端 `96` 条路由能力基线。
本文件只做路由索引,不替代单个阶段的设计文档;接口字段、权限、错误模型仍以各阶段技术方案和 `shared-contracts` 为准。
## 2. 当前统计
当前 Rust `api-server``app.rs` 可抽取到 `96` 条路由:
1. 内部鉴权调试接口:`2` 条。
2. AI task 接口:`9` 条。
3. assets / OSS 接口:`15` 条。
4. auth 接口:`12` 条。
5. custom world / agent 接口:`23` 条。
6. llm proxy 接口:`1` 条。
7. profile / runtime profile 接口:`12` 条。
8. runtime story / story gameplay 接口:`15` 条。
9. legacy generated 静态路径兼容:`6` 条。
10. health check`1` 条。
## 3. 路由清单
### 3.1 内部鉴权调试
1. `GET /_internal/auth/claims`
2. `GET /_internal/auth/refresh-cookie`
### 3.2 AI Task
1. `POST /api/ai/tasks`
2. `POST /api/ai/tasks/{task_id}/start`
3. `POST /api/ai/tasks/{task_id}/cancel`
4. `POST /api/ai/tasks/{task_id}/complete`
5. `POST /api/ai/tasks/{task_id}/fail`
6. `POST /api/ai/tasks/{task_id}/chunks`
7. `POST /api/ai/tasks/{task_id}/references`
8. `POST /api/ai/tasks/{task_id}/stages/{stage_kind}/start`
9. `POST /api/ai/tasks/{task_id}/stages/{stage_kind}/complete`
### 3.3 Assets / OSS
1. `POST /api/assets/direct-upload-tickets`
2. `POST /api/assets/sts-upload-credentials`
3. `POST /api/assets/objects/confirm`
4. `POST /api/assets/objects/bind`
5. `GET /api/assets/read-url`
6. `POST /api/assets/character-visual/generate`
7. `GET /api/assets/character-visual/jobs/{task_id}`
8. `POST /api/assets/character-visual/publish`
9. `POST /api/assets/character-animation/generate`
10. `GET /api/assets/character-animation/jobs/{task_id}`
11. `POST /api/assets/character-animation/publish`
12. `POST /api/assets/character-animation/import-video`
13. `GET /api/assets/character-animation/templates`
14. `GET /api/assets/character-workflow-cache/{character_id}`
15. `GET / POST /api/assets/character-workflow-cache`
### 3.4 Auth
1. `GET /api/auth/login-options`
2. `GET /api/auth/me`
3. `POST /api/auth/logout`
4. `POST /api/auth/logout-all`
5. `GET /api/auth/sessions`
6. `POST /api/auth/refresh`
7. `POST /api/auth/phone/send-code`
8. `POST /api/auth/phone/login`
9. `GET /api/auth/wechat/start`
10. `GET /api/auth/wechat/callback`
11. `POST /api/auth/wechat/bind-phone`
12. `POST /api/auth/entry`
### 3.5 Custom World / Agent
1. `GET /api/runtime/custom-world-library`
2. `GET /api/runtime/custom-world-library/{profile_id}`
3. `POST /api/runtime/custom-world-library/{profile_id}/publish`
4. `POST /api/runtime/custom-world-library/{profile_id}/unpublish`
5. `GET /api/runtime/custom-world-gallery`
6. `GET /api/runtime/custom-world-gallery/{owner_user_id}/{profile_id}`
7. `GET /api/runtime/custom-world/works`
8. `POST /api/runtime/custom-world/agent/sessions`
9. `GET /api/runtime/custom-world/agent/sessions/{session_id}`
10. `POST /api/runtime/custom-world/agent/sessions/{session_id}/messages`
11. `GET /api/runtime/custom-world/agent/sessions/{session_id}/messages/stream`
12. `GET /api/runtime/custom-world/agent/sessions/{session_id}/operations/{operation_id}`
13. `GET /api/runtime/custom-world/agent/sessions/{session_id}/cards/{card_id}`
14. `POST /api/runtime/custom-world/agent/sessions/{session_id}/actions`
15. `POST /api/custom-world/entity`
16. `POST /api/runtime/custom-world/entity`
17. `POST /api/custom-world/scene-npc`
18. `POST /api/runtime/custom-world/scene-npc`
19. `POST /api/custom-world/scene-image`
20. `POST /api/custom-world/cover-image`
21. `POST /api/custom-world/cover-upload`
22. `POST /api/runtime/custom-world/cover-image`
23. `POST /api/runtime/custom-world/cover-upload`
### 3.6 LLM Proxy
1. `POST /api/llm/chat/completions`
### 3.7 Profile / Runtime Profile
1. `GET /api/profile/dashboard`
2. `GET /api/runtime/profile/dashboard`
3. `GET /api/profile/play-stats`
4. `GET /api/runtime/profile/play-stats`
5. `GET /api/profile/wallet-ledger`
6. `GET /api/runtime/profile/wallet-ledger`
7. `GET /api/profile/browse-history`
8. `GET /api/runtime/profile/browse-history`
9. `GET /api/profile/save-archives`
10. `GET /api/runtime/profile/save-archives`
11. `POST /api/profile/save-archives/{world_key}`
12. `POST /api/runtime/profile/save-archives/{world_key}`
### 3.8 Runtime Story / Gameplay
1. `POST /api/runtime/save/snapshot`
2. `GET /api/runtime/settings`
3. `GET /api/runtime/story/state/{session_id}`
4. `POST /api/runtime/story/state/resolve`
5. `POST /api/runtime/story/actions/resolve`
6. `POST /api/runtime/story/initial`
7. `POST /api/runtime/story/continue`
8. `POST /api/story/sessions`
9. `POST /api/story/sessions/continue`
10. `GET /api/story/sessions/{story_session_id}/state`
11. `POST /api/story/battles`
12. `POST /api/story/battles/resolve`
13. `GET /api/story/battles/{battle_state_id}`
14. `POST /api/story/npc/battle`
15. `GET /api/runtime/sessions/{runtime_session_id}/inventory`
### 3.9 Legacy Generated 路径
1. `GET /generated-character-drafts/{*path}`
2. `GET /generated-characters/{*path}`
3. `GET /generated-animations/{*path}`
4. `GET /generated-custom-world-scenes/{*path}`
5. `GET /generated-custom-world-covers/{*path}`
6. `GET /generated-qwen-sprites/{*path}`
### 3.10 Health
1. `GET /healthz`
## 4. 维护规则
1. 新增、删除或改名 Rust 路由时,必须同步更新本索引。
2. 如果 Node 后端 `NODE_BACKEND_MODULE_AND_API_INDEX.md` 的现役能力面发生变化,必须同时更新本索引与对应阶段任务清单。
3. 任何 breaking route change 都必须先更新阶段设计文档,再改代码。
4. 真实切流前,必须用本索引对照代理层、前端调用面和 smoke 清单,避免只完成编译而遗漏外部可访问路径。

View File

@@ -0,0 +1,105 @@
# Rust `api-server` SSE 使用口径2026-04-22
日期:`2026-04-22`
## 1. 文档目的
这份文档用于冻结 `server-rs/crates/api-server` 的 SSE 实现口径。
本轮结论调整为Rust `api-server` 不再维护自定义 `sse.rs` 基础设施,统一使用 Axum 内建的 `axum::response::sse::{Event, Sse}` 能力。
本轮目标只有三个:
1. 删除 `server-rs/crates/api-server/src/sse.rs` 自定义模块。
2. 把现有 custom world message stream 切到 Axum 官方 SSE 类型。
3. 保持现有业务事件协议与“一次性返回完整事件序列”的兼容语义不变。
本轮不做:
1. 不改前端消费协议。
2. 不把 custom world message stream 当场改成真实逐段 token streaming。
3. 不引入跨 crate 的共享 SSE runtime helper。
4. 不抽象 `reply_delta / session / done / error` 等业务事件名。
## 2. 当前问题
上一轮曾在 `server-rs/crates/api-server/src/sse.rs` 中抽出自定义 SSE helper用于统一响应头、事件编码、缓冲式输出和实时 writer。
继续保留这套自定义模块的问题是:
1. Axum 已经提供 `Sse<Event>``Event::json_data(...)` 和标准 SSE body 编码。
2. 自定义文本编码需要自行维护换行、JSON 序列化、响应头等细节。
3. 后续真流式接口如果继续沿用自定义 writer会和 Axum 官方生态产生重复抽象。
4. 当前项目已经以 Axum 作为 Rust HTTP 框架,优先使用框架内建能力更简单。
## 3. 统一实现口径
Rust `api-server` 的 SSE 路由统一使用:
```rust
use axum::response::sse::{Event, Sse};
```
有限事件序列使用:
```rust
let stream = tokio_stream::iter(events);
Sse::new(stream).into_response()
```
实时流式接口后续直接使用:
```rust
Sse::new(event_stream)
```
如需保持长连接,可在真实长流接口中追加:
```rust
.keep_alive(axum::response::sse::KeepAlive::default())
```
## 4. custom world message stream 边界
`POST /api/runtime/custom-world/agent/sessions/:sessionId/messages/stream` 当前继续保持 Stage 8 文档冻结的最小语义:
1. 业务层先完成 deterministic 写表。
2. 读取最新 session snapshot。
3. 组装 `reply_delta`
4. 组装 `session`
5. 组装 `done`
6. 通过 Axum `Sse` 返回完整事件序列。
本轮只替换传输层实现,不改变事件顺序、事件名和 payload 结构。
## 5. 响应头说明
Axum `Sse` 默认写入:
1. `Content-Type: text/event-stream`
2. `Cache-Control: no-cache`
当前不再额外写入自定义 `X-Accel-Buffering: no` helper。
原因:
1. 本轮目标是移除项目自定义 SSE 模块,避免继续维护传输层封装。
2. 当前 custom world stream 仍是短生命周期的兼容事件序列,不是长时间 token streaming。
3. 如果未来某条真实长流接口需要反向代理禁用缓冲,应在该路由或统一 HTTP 中间件层显式评估,而不是恢复自定义 SSE 编码器。
## 6. 验收标准
当以下条件满足时,本轮视为完成:
1. `api-server/src/sse.rs` 已删除。
2. `api-server/src/main.rs` 不再声明 `mod sse;`
3. `custom_world.rs` 不再依赖 `crate::sse::SseEventBuffer`
4. custom world message stream 使用 Axum `Sse<Event>` 构造响应。
5. 为旧自定义 writer 引入的 `bytes``tokio::sync` feature 等依赖已清理。
6. `cargo fmt -p api-server` 通过。
7. `cargo check -p api-server` 通过。
8. `npm run check:encoding` 通过。
## 7. 一句话结论
Rust `api-server` 的 SSE 能力以 Axum 内建 `Sse<Event>` 为唯一实现入口,不再保留项目自定义 `sse.rs` 模块;当前 custom world stream 只替换传输层,不改变业务协议。

View File

@@ -0,0 +1,145 @@
# Rust 本地联调与远端发布脚本方案
日期:`2026-04-22`
## 1. 目标
本方案补齐 `server-rs` 在 M7 切流前需要的两类工程脚本:
1. 本地一键联调脚本:同时启动本地 SpacetimeDB、Rust `api-server` 与 Web 前端,并通过现有 Vite 代理开关把运行时 API 指向 Rust。
2. Ubuntu 发布包构建脚本:在仓库根目录生成 `build/<当前时间>/` 发布目录,内含前端 release、Linux `api-server`、SpacetimeDB wasm、启动脚本与停止脚本。
脚本只做部署与联调编排,不改变 HTTP contract、SpacetimeDB schema 命名、对象存储键规划和前端默认 Node 开发入口。
## 2. 本地脚本
入口:
```powershell
npm run dev:rust
```
跨平台 Bash 入口:
```bash
npm run dev:rust:sh
```
Windows 下 `dev:rust:sh``deploy:rust:remote``build:rust:ubuntu` 会通过 `scripts/run-bash-script.mjs` 优先查找 Git Bash如安装路径不标准可用 `GENARRATIVE_BASH` 指定 `bash` 可执行文件。
默认端口:
1. Web 前端:`http://127.0.0.1:3000`
2. Rust `api-server``http://127.0.0.1:8082`
3. SpacetimeDB standalone`http://127.0.0.1:3101`
4. SpacetimeDB database`genarrative-dev`
默认流程:
1. 检查 `cargo``node``spacetime` CLI。
2. 启动 `spacetime --root-dir server-rs/.spacetimedb/local start --edition standalone --listen-addr 127.0.0.1:3101`
3. 等待 `spacetime server ping http://127.0.0.1:3101` 可用。
4. 执行 `spacetime publish genarrative-dev --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module --yes`
5. 注入 `GENARRATIVE_API_*``GENARRATIVE_SPACETIME_*` 后启动 `cargo run -p api-server`
6. 注入 `GENARRATIVE_BACKEND_STACK=rust``RUST_SERVER_TARGET``GENARRATIVE_RUNTIME_SERVER_TARGET` 后启动 Vite。
7. 任一子进程退出时,脚本回收其余子进程。
Vite 代理覆盖范围:
1. `/api/runtime/*` 会在 Rust 栈下代理到 Rust `api-server`,覆盖旧 runtime story 兼容接口。
2. `/api/story/*` 会在 Rust 栈下代理到 Rust `api-server`,覆盖新 story session、battle 查询与 NPC battle 切片接口。
3. 其他 `/api/auth``/api/assets``/api/custom-world``/api/llm` 等路径仍由同一个 `GENARRATIVE_RUNTIME_SERVER_TARGET` 控制,便于 M7 按服务能力逐项做对比 smoke。
安全边界:
1. 默认不执行 `--clear-database`
2. 只有显式传入 `-ClearDatabase``--clear-database` 才允许清库重发。
3. 如需要复用已经启动的 SpacetimeDB可传 `-SkipSpacetime` / `--skip-spacetime`
4. 如只想启动进程不发布模块,可传 `-SkipPublish` / `--skip-publish`
常用示例:
```powershell
.\scripts\dev-rust-stack.ps1 -ApiPort 8090 -SpacetimePort 3110 -Database genarrative-dev
.\scripts\dev-rust-stack.ps1 -SkipSpacetime -SkipPublish
.\scripts\dev-rust-stack.ps1 -ClearDatabase
```
```bash
./scripts/dev-rust-stack.sh --api-port 8090 --spacetime-port 3110 --database genarrative-dev
./scripts/dev-rust-stack.sh --skip-spacetime --skip-publish
./scripts/dev-rust-stack.sh --clear-database
```
## 3. Ubuntu 发布包脚本
入口:
```bash
npm run build:rust:ubuntu
```
兼容入口:
```bash
npm run deploy:rust:remote
```
保留 `deploy:rust:remote` 是为了不打断既有命令习惯;当前语义已调整为“生成 Ubuntu 发布包”,不再通过 SSH 进入服务器执行部署。
默认流程:
1. 在仓库根目录创建 `build/`
2.`build/` 下创建当前时间命名的目标目录,例如 `build/20260422-153000/`
3. 使用 Vite 构建前端 release 到目标目录的 `web/`
4. 执行 `cargo build -p api-server --release --target x86_64-unknown-linux-gnu --manifest-path server-rs/Cargo.toml`,并把 `api-server` 复制到目标目录。
5. 执行 `cargo build -p spacetime-module --release --target wasm32-unknown-unknown --manifest-path server-rs/Cargo.toml`,并把 `spacetime_module.wasm` 复制到目标目录。
6. 在目标目录写入 `web-server.mjs`,用于托管 `web/` 并把 `/api/*``/generated-*``/healthz` 反代到本包内的 `api-server`
7. 在目标目录写入 `start.sh``stop.sh`
发布包结构:
```text
build/<timestamp>/
├─ web/
├─ api-server
├─ spacetime_module.wasm
├─ web-server.mjs
├─ start.sh
├─ stop.sh
└─ README.md
```
常用示例:
```bash
npm run build:rust:ubuntu -- --name 20260422-153000
npm run build:rust:ubuntu -- --database genarrative-dev --web-port 3000 --api-port 8082 --spacetime-port 3101
```
目标服务器启动:
```bash
cd build/<timestamp>
./start.sh
./stop.sh
```
安全边界:
1. 构建脚本不读取、不传输、不打印生产密钥。
2. 目标服务器 `.env``.env.local` 或进程环境仍由服务器本身维护。
3. `start.sh` 默认不清空 SpacetimeDB只有显式执行 `./start.sh --clear-database` 才允许清库重发。
4. `start.sh` 使用 `spacetime publish --bin-path spacetime_module.wasm --yes` 发布当前包内 wasm。
5. 当前脚本是单目录进程启动方案,不替代生产 systemd、Nginx、TLS、日志轮转与守护进程配置。
目标服务器最小要求:
1. Ubuntu x86_64。
2. 已安装 `node`,用于运行发布包内的 `web-server.mjs`
3. 已安装 `spacetime` CLI`start.sh` 会启动本地 SpacetimeDB 并发布 wasm。
4. 业务密钥通过目标服务器环境变量或发布包同目录 `.env.local` 提供。
## 4. 与 M7 的关系
这套脚本补齐 M7 的部署执行入口但不等价于完成灰度切流。M7 后续仍需要在真实 OSS、LLM、短信、微信、SpacetimeDB 数据库和反向代理环境下完成全链路 smoke、关键 SSE 联调和灰度切流验收。

View File

@@ -832,6 +832,21 @@ workflow-cache/{workflow_type}/{workflow_id}.json
1. `editor` 已于 `2026-04-21` 被确认为遗留无用模块,退出本轮 Rust 后端重写范围。
2. Phase 5 只覆盖资产与 OSS 主链,不再包含 editor 迁移。
## Phase 6联调、回归、部署与切流收口
交付:
1. 联调与回归测试体系
2. 灰度环境、切流开关、回退方案
3. tracing / request id / 关键链路观测
4. 拆分 `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 开发链路与主流程回归脚本可继续通过。
## 14. 验收标准
重写完成至少要满足:

View File

@@ -331,15 +331,24 @@ session snapshot 中的 `resultPreview` 固定输出:
#### `scene-image / cover-image`
1. 当前不直接生成真实图片
2. 返回明确 `NOT_IMPLEMENTED` 或最小占位错误会导致前端主链中断
3. 因此前端兼容需要的最小可用策略是:创建上传票据或返回可继续上传的对象位置信息
1. `M5` 验收时允许先用本地占位产物保证前端主链不断
2. `2026-04-22``M6` 第一批开始,正式口径改为:
- `platform-oss::put_object`
- `asset_object`
- `asset_entity_binding`
3. 兼容响应仍返回旧 `/generated-*` 路径,不直接返回裸 OSS URL
4. 详细边界见:
- [M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md)
#### `cover-upload`
1. 复用 `/api/assets/direct-upload-tickets`
2. 生成 OSS 上传票据
3. 返回兼容旧前端所需的上传字段
1. `M5` 阶段允许先走最小本地上传兼容
2. `2026-04-22``M6` 第一批开始,正式口径与 `cover-image` 一致:
- 服务器接收 Data URL
- 服务器上传 OSS
- 确认 `asset_object`
- 绑定 `asset_entity_binding`
3. 返回值仍保持旧前端所需的 `imageSrc / assetId / sourceType`
## 8. crate 级改动范围