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:
@@ -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. 灰度环境双跑对比。
|
||||
|
||||
@@ -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”。
|
||||
|
||||
421
docs/technical/M4_RUNTIME_STORY_RS_SPLIT_PLAN_2026-04-22.md
Normal file
421
docs/technical/M4_RUNTIME_STORY_RS_SPLIT_PLAN_2026-04-22.md
Normal 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 放进新 crate,route 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 组合逻辑
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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` 中路径兼容项完成勾选。
|
||||
@@ -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 指向 Rust,Node 进程保留但不作为主入口。
|
||||
|
||||
前端切换方式:
|
||||
|
||||
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` 组织的文件结构。
|
||||
@@ -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/*` 历史路径兼容,不再存在独立路由主链。
|
||||
|
||||
## 总览
|
||||
|
||||
|
||||
@@ -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 落地口径,明确当前上传主链为服务器上传 OSS,Web 端只负责签名读下载。
|
||||
- [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`。
|
||||
|
||||
159
docs/technical/RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md
Normal file
159
docs/technical/RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md
Normal 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 清单,避免只完成编译而遗漏外部可访问路径。
|
||||
@@ -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 只替换传输层,不改变业务协议。
|
||||
@@ -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 联调和灰度切流验收。
|
||||
@@ -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. 验收标准
|
||||
|
||||
重写完成至少要满足:
|
||||
|
||||
@@ -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 级改动范围
|
||||
|
||||
|
||||
Reference in New Issue
Block a user