feat: complete M6 asset OSS migration

This commit is contained in:
2026-04-22 16:35:41 +08:00
parent d5627c536d
commit 4c8ba535e4
18 changed files with 4911 additions and 40 deletions

View File

@@ -0,0 +1,142 @@
# M6 资产元数据、版本与专用表边界设计
日期:`2026-04-22`
## 1. 文档目的
这份文档用于把 `M6` 清单中剩余的以下项收口到可执行边界:
1. 内容 hash / 版本字段规范
2. `asset_job`
3. `asset_manifest`
4. `character_visual_asset`
5. `character_animation_asset`
6. `scene_image_asset`
7. `sprite_sheet_asset`
当前 `M6` 第一批已经落地的真实主链是:
1. OSS 私有对象持有二进制内容
2. `asset_object` 记录 `bucket + object_key` 和基础元数据
3. `asset_entity_binding` 记录业务实体槽位绑定
4. 角色动作发布通过 OSS `manifest.json` 表达动作集合
5. 角色主形象、角色动作、custom world 场景图/封面图都先通过通用绑定闭环
因此本阶段不继续堆新表,而是冻结“哪些内容已经由现有主链承担,哪些等真实访问模式稳定后再拆强业务表”。
## 2. 内容 hash 与版本规范
### 2.1 当前 Stage 1 规范
`asset_object` 当前字段已经包含:
1. `content_hash: Option<String>`
2. `version: u32`
本阶段规范如下:
1. `version` 固定从 `1` 起步。
2. 同一 `bucket + object_key` 被重新确认时,保留原 `created_at`,更新 `updated_at`,版本仍按当前 `INITIAL_ASSET_OBJECT_VERSION = 1` 处理。
3. `content_hash` 当前优先使用 OSS `ETag` 或调用方明确传入的 hash。
4. 不在 `api-server` 对大文件做强制全量 SHA-256 计算,避免图片/视频代理链路和服务端上传链路被额外 CPU 与内存占用放大。
5. 后续若需要强一致内容去重,再新增独立 `content_digest` 计算策略,不复用当前可空 `content_hash` 做强制约束。
### 2.2 不做强制 hash 的原因
1. OSS `ETag` 在不同上传方式下不一定等价于单纯 MD5。
2. 当前第一批主要目标是把本地 `public/` 真相迁到 OSS 与 SpacetimeDB 元数据。
3. 角色动作视频、帧序列和 custom world 图片都已经能通过 `content_length + object_key + asset_kind + binding` 完成首批追踪。
4. 强制 hash 需要统一 multipart、服务端上传、浏览器直传和迁移脚本的计算口径适合后续单独阶段。
## 3. `asset_job` 边界
当前不新增 `asset_job` 表。
理由:
1. `M4` 已引入 `module-ai::AiTaskService` 和对应 `ai_task` 设计。
2. 角色主形象与角色动作的 Stage 1 已复用 `AiTaskService` 输出旧 `jobs/:taskId` contract。
3. custom world 场景图/封面图当前仍是同步兼容接口,不需要单独资产任务态。
当前任务状态统一口径:
1. AI 生成相关:使用 `ai_task` / `AiTaskService`
2. 纯上传确认相关:使用 `asset_object``asset_entity_binding` 的返回结果。
3. 后续若出现非 AI 的长时资产处理任务,再重新评估是否拆 `asset_job`
## 4. `asset_manifest` 边界
当前不新增 SpacetimeDB `asset_manifest` 表。
Stage 1 的 manifest 口径如下:
1. manifest 是一个 OSS JSON 对象。
2. 角色动作整套 manifest 会被确认成 `asset_object`
3. `asset_entity_binding` 绑定的是整套 manifest 对象,而不是每个单帧对象。
4. 前端仍通过旧 `animationMap` contract 消费动作帧路径。
后续只有满足以下条件之一时,才新增 `asset_manifest` 表:
1. 需要在 SpacetimeDB 中按 manifest 内部动作、帧、依赖对象做查询。
2. 需要对 manifest 做版本 diff、审核、回滚。
3. 需要把 manifest 作为跨 profile、跨角色复用的结构化资产集合。
## 5. 强业务资产表边界
当前不新增以下强业务表:
1. `character_visual_asset`
2. `character_animation_asset`
3. `scene_image_asset`
4. `sprite_sheet_asset`
当前由以下组合承担业务绑定:
1. `asset_object.asset_kind`
2. `asset_entity_binding.entity_kind`
3. `asset_entity_binding.entity_id`
4. `asset_entity_binding.slot`
当前已冻结槽位:
| 业务 | `entity_kind` | `slot` | `asset_kind` |
| --- | --- | --- | --- |
| 角色主形象 | `character` | `primary_visual` | `character_visual` |
| 角色动作集 | `character` | `animation_set` | `character_animation` |
| custom world 场景图 | `custom_world_landmark` | `scene_image` | `scene_image` |
| custom world 封面 | `custom_world_profile` | `cover` | `custom_world_cover` |
后续拆强业务表的条件:
1. 需要对角色主形象候选、审核状态、模型参数做结构化查询。
2. 需要对动作集逐动作授权、复用、差分发布。
3. 需要对场景图、封面图做多版本历史、审核流或推荐流。
4. 需要对 sprite sheet 做切片、修帧、atlas 元数据查询。
## 6. `sprite_sheet_asset` 与 Qwen 边界
当前 `Qwen sprite` 独立工具链已经清理,不再作为本轮现役迁移主链。
本阶段只保留:
1. 历史 `/generated-qwen-sprites/*` 路径读取兼容。
2. `platform-oss::LegacyAssetPrefix::QwenSprites` 对象键支持。
因此 `sprite_sheet_asset` 当前只保留后续能力位,不在 `M6` Stage 1 新增表或接口。
## 7. 完成定义
当以下条件满足时,本阶段 M6 元数据与专用表边界视为完成:
1. `content_hash/version` 在文档中明确为 `asset_object` 现有可空 hash + 初始版本口径。
2. `asset_job` 明确由 `AiTaskService` 暂代,不新增重复任务表。
3. `asset_manifest` 明确由 OSS JSON manifest + `asset_object` 暂代。
4. 强业务资产表明确延后到访问模式稳定后拆分。
5. `05_M6_ASSETS_OSS_EDITOR.md` 不再把这些后续能力位误标为当前 Stage 1 未完成阻塞项。
## 8. 关联文档
1. [SPACETIMEDB_ASSET_OBJECT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_ASSET_OBJECT_TABLE_DESIGN_2026-04-21.md)
2. [ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md](./ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md)
3. [M6_CHARACTER_ANIMATION_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CHARACTER_ANIMATION_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md)
4. [M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md)

View File

@@ -0,0 +1,219 @@
# M6 角色动作资产接入 OSS 第一批设计
日期:`2026-04-22`
## 1. 文档目的
这份文档用于冻结 `M6` 第一批“角色动作生成 + 任务查询 + 正式发布”的真实落地口径。
本批只解决以下三条旧接口的 Rust 重写入口:
1. `POST /api/assets/character-animation/generate`
2. `GET /api/assets/character-animation/jobs/:taskId`
3. `POST /api/assets/character-animation/publish`
目标不是一次性接入 DashScope / Ark 视频模型,而是先把角色动作资产从旧 Node 本地 `public/generated-*` 真相切到:
1. `OSS` 草稿对象
2. `AI task` 任务态
3. `OSS` 正式动作对象
4. `asset_object`
5. `asset_entity_binding`
## 2. 当前前提
当前仓库已经具备以下能力:
1. `platform-oss::OssClient::put_object`
2. `platform-oss::OssClient::sign_get_object_url`
3. `asset_object`
4. `asset_entity_binding`
5. `module-ai` 进程内 `AiTaskService`
6. 角色主形象已完成 `generate / jobs / publish` 的第一批 OSS 主链
7. 角色动作模板、视频导入、workflow cache 已完成第一批 Rust 兼容入口
因此本批复用现有 OSS、资产对象确认、业务实体绑定和 `AiTaskService`,不新增独立 `asset_job` 表。
## 3. 本批范围
### 3.1 要完成的内容
1. 兼容角色动作草稿生成接口
2. 兼容角色动作任务查询接口
3. 兼容角色动作正式发布接口
4. `image-sequence` 草稿帧写入 OSS `generated-character-drafts/*`
5. 视频类策略草稿预览对象写入 OSS `generated-character-drafts/*`
6. 正式动作帧写入 OSS `generated-animations/*`
7. 正式动作 manifest 写入 OSS `generated-animations/*`
8. 正式动作 manifest 确认为 `asset_object`
9. 正式动作 manifest 绑定到角色实体动作槽位
10. 返回字段继续保持旧前端可消费 contract
### 3.2 本批不解决的内容
1. 不接真实 DashScope 图片序列帧模型
2. 不接真实 Ark 图生视频模型
3. 不接真实动作迁移模型
4. 不落 `character_animation_asset` 强业务表
5. 不回写 `src/data/characterOverrides.json`
6. 不迁移历史本地 `public/generated-animations`
## 4. 旧接口兼容 contract
### 4.1 `POST /api/assets/character-animation/generate`
请求结构继续保持前端当前字段:
1. `characterId`
2. `strategy`
3. `animation`
4. `promptText`
5. `characterBriefText`
6. `actionTemplateId`
7. `visualSource`
8. `referenceImageDataUrls`
9. `referenceVideoDataUrls`
10. `lastFrameImageDataUrl`
11. `frameCount`
12. `fps`
13. `durationSeconds`
14. `loop`
15. `useChromaKey`
16. `resolution`
17. `ratio`
18. `imageSequenceModel`
19. `videoModel`
20. `referenceVideoModel`
21. `motionTransferModel`
`image-sequence` 返回结构继续保持:
1. `ok`
2. `taskId`
3. `strategy`
4. `model`
5. `prompt`
6. `imageSources`
视频类策略返回结构继续保持:
1. `ok`
2. `taskId`
3. `strategy`
4. `model`
5. `prompt`
6. `previewVideoPath`
补充口径:
1. Stage 1 的 `image-sequence` 先生成 SVG 占位帧。
2. Stage 1 的视频类策略若提供 `referenceVideoDataUrls[0]`,则把该视频作为草稿预览写入 OSS。
3. Stage 1 的视频类策略若没有参考视频,则写入占位预览对象以保持接口 contract后续真实视频模型替换该产物。
### 4.2 `GET /api/assets/character-animation/jobs/:taskId`
返回结构继续保持:
1. `taskId`
2. `kind`
3. `status`
4. `characterId`
5. `animation`
6. `strategy`
7. `model`
8. `prompt`
9. `createdAt`
10. `updatedAt`
11. `result`
12. `errorMessage`
当前阶段直接复用 `AiTaskService` 内存态任务快照派生。
### 4.3 `POST /api/assets/character-animation/publish`
请求结构继续保持:
1. `characterId`
2. `visualAssetId`
3. `animations`
4. `updateCharacterOverride`
返回结构继续保持:
1. `ok`
2. `animationSetId`
3. `overrideMap`
4. `animationMap`
5. `saveMessage`
补充口径:
1. 每个动作的帧写入 `generated-animations/*`
2. 每个动作生成 `manifest.json`
3. 整套动作生成总 `manifest.json`
4. 总 manifest 确认为 `asset_object`
5. 总 manifest 绑定到角色实体槽位
6. `overrideMap` 当前返回 `{}`Rust 后端不再写本地角色覆盖文件
## 5. 业务实体与槽位约定
本批统一复用通用 `asset_entity_binding`
### 5.1 角色动作正式对象
| 字段 | 取值 |
| --- | --- |
| `entity_kind` | `character` |
| `entity_id` | `characterId` |
| `slot` | `animation_set` |
| `asset_kind` | `character_animation` |
说明:
1. 正式绑定对象是整套动作总 manifest。
2. 单帧对象不单独绑定。
3. 后续若落 `character_animation_asset` 强业务表,再把动作级索引迁到专用表。
## 6. OSS 对象键规划
### 6.1 草稿序列帧
`generated-character-drafts/{characterSegment}/animation/{animationSegment}/{taskId}/frame-{index}.svg`
### 6.2 草稿预览视频
`generated-character-drafts/{characterSegment}/animation/{animationSegment}/{taskId}/preview.{extension}`
### 6.3 正式动作帧
`generated-animations/{characterSegment}/{animationSetId}/{actionSegment}/frame{index}.{extension}`
### 6.4 正式动作 manifest
动作级 manifest
`generated-animations/{characterSegment}/{animationSetId}/{actionSegment}/manifest.json`
整套 manifest
`generated-animations/{characterSegment}/{animationSetId}/manifest.json`
## 7. 完成定义
当以下条件满足时,本批视为完成:
1. Rust 已兼容 `character-animation/generate`
2. Rust 已兼容 `character-animation/jobs/:taskId`
3. Rust 已兼容 `character-animation/publish`
4. 草稿动作产物写入 OSS
5. 正式动作产物写入 OSS
6. 正式总 manifest 形成 `asset_object`
7. 正式总 manifest 形成 `asset_entity_binding`
8. 前端仍能继续消费 `imageSources / previewVideoPath / animationMap` 旧 contract
## 8. 关联文档
1. [M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md)
2. [M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md](./M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md)
3. [M6_CHARACTER_WORKFLOW_CACHE_OSS_STAGE1_2026-04-22.md](./M6_CHARACTER_WORKFLOW_CACHE_OSS_STAGE1_2026-04-22.md)
4. [ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md](./ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md)

View File

@@ -0,0 +1,147 @@
# M6 角色动作模板与视频导入接入 OSS 第一批设计
日期:`2026-04-22`
## 1. 文档目的
这份文档用于冻结 `M6` 第一批“角色动作模板查询 + 视频导入”的真实落地口径。
本批只解决以下两条旧接口的 Rust 重写入口:
1. `GET /api/assets/character-animation/templates`
2. `POST /api/assets/character-animation/import-video`
目标不是一次性迁移角色动作生成、发布和真实视频模型,而是先把资产工坊当前可独立收口的动作模板与参考视频导入从旧 Node 本地 `public/generated-character-drafts` 写盘,切到 OSS 草稿对象。
## 2. 当前前提
当前仓库已经具备以下能力:
1. `platform-oss::OssClient::put_object`
2. `generated-character-drafts/*` 兼容对象键前缀
3. `shared-contracts::assets` 角色主形象兼容 DTO
4. `api-server` 已接入角色主形象 `generate / jobs / publish`
因此本批复用既有 OSS 服务端上传 helper不新增 SpacetimeDB 表。
## 3. 本批范围
### 3.1 要完成的内容
1. 兼容动作模板列表接口
2. 兼容参考视频导入接口
3. 导入视频对象写入 OSS `generated-character-drafts/*`
4. 返回字段继续保持旧前端可消费 contract
5. 不再把导入视频写入本地 `public/`
### 3.2 本批不解决的内容
1. 不迁移 `character-animation/generate`
2. 不迁移 `character-animation/jobs/:taskId`
3. 不迁移 `character-animation/publish`
4. 不落 `character_animation_asset` 强业务表
5. 不为导入草稿创建 `asset_object`
6. 不为导入草稿创建 `asset_entity_binding`
7. 不读取旧本地 `public/` 路径作为导入源
## 4. 旧接口兼容 contract
### 4.1 `GET /api/assets/character-animation/templates`
返回结构继续保持:
1. `ok`
2. `templates`
每个模板继续包含:
1. `id`
2. `label`
3. `animation`
4. `promptSuffix`
5. `notes`
当前模板列表固定为内置四项:
1. `idle_loop`
2. `run_side`
3. `attack_slash`
4. `die_fall`
### 4.2 `POST /api/assets/character-animation/import-video`
请求结构继续保持:
1. `characterId`
2. `animation`
3. `videoSource`
4. `sourceLabel`
返回结构继续保持:
1. `ok`
2. `importedVideoPath`
3. `draftId`
4. `saveMessage`
补充口径:
1. `videoSource` 当前阶段只接受 `data:video/*;base64,...`
2. `importedVideoPath` 继续返回旧前端习惯的 `/generated-character-drafts/*`
3. 底层对象真相在 OSS不再写本地 `public/`
4. `saveMessage` 明确说明当前是“已导入 OSS 草稿区”
## 5. OSS 对象键规划
导入视频固定写入:
`generated-character-drafts/{characterSegment}/animation/{animationSegment}/{draftId}/{sourceLabel}.{extension}`
其中:
1. `characterSegment` 来自 `characterId` 的安全路径片段
2. `animationSegment` 来自 `animation` 的安全路径片段
3. `draftId` 固定为 `animation-import-{unixMillis}`
4. `extension` 从 Data URL MIME 类型派生
## 6. 元数据规范
导入视频对象写入以下 `x-oss-meta-*` 元数据:
1. `asset_kind = character_animation_reference_video`
2. `owner_user_id = asset-tool`
3. `entity_kind = character`
4. `entity_id = characterId`
5. `slot = animation_reference_video`
6. `animation = animation`
说明:
1. 旧资产工坊接口没有显式 Bearer第一批继续使用 `asset-tool` 作为兼容归属。
2. 草稿导入视频只是后续动作生成的参考输入,不是正式发布资产,因此本批不确认 `asset_object`
## 7. 数据源边界
Rust 第一批只接受 `data:video/*;base64,...`
暂不接受旧本地 public 路径,原因是:
1. Rust 迁移目标是不再依赖本地 `public/` 作为资产真相。
2. 若为了兼容旧路径再读取本地文件,会延长旧写盘链路生命周期。
3. 前端导入入口当前可直接传视频 Data URL足以满足本批最小闭环。
## 8. 完成定义
当以下条件满足时,本批视为完成:
1. Rust 已兼容 `character-animation/templates`
2. Rust 已兼容 `character-animation/import-video`
3. 导入视频写入 OSS `generated-character-drafts/*`
4. 接口返回 `importedVideoPath / draftId` 旧 contract
5. 不再产生本地 `public/generated-character-drafts/*` 导入文件
## 9. 关联文档
1. [M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md)
2. [M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md)
3. [M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md](./M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md)

View File

@@ -0,0 +1,242 @@
# M6 角色主形象资产接入 OSS 第一批设计
日期:`2026-04-22`
## 1. 文档目的
这份文档用于冻结 `M6` 第一批“角色主形象资产链”的真实落地口径。
本批只解决以下三条旧接口的 Rust 重写入口:
1. `POST /api/assets/character-visual/generate`
2. `GET /api/assets/character-visual/jobs/:taskId`
3. `POST /api/assets/character-visual/publish`
目标不是一次性把整套资产系统迁完,而是先把“角色主形象候选生成 + 查询 + 正式发布”从旧 Node 的本地 `public/generated-*` 真相,切到:
1. `OSS`
2. `asset_object`
3. `asset_entity_binding`
4. `AI task` 任务态
形成第一批正式主链。
## 2. 当前前提
当前仓库已经具备以下能力:
1. `platform-oss::OssClient::put_object`
2. `platform-oss::OssClient::head_object`
3. `asset_object`
4. `asset_entity_binding`
5. `module-ai` 进程内 `AiTaskService`
6. `platform-llm` OpenAI 兼容文本模型网关
7. `custom world` 图片兼容入口已经完成一版 `OSS + asset_object + asset_entity_binding` 落地
因此本批不重新设计一套新资产基础设施,而是复用:
1. 既有 `OSS` 上传与确认链
2. 既有 `asset_object / asset_entity_binding`
3. 既有 `AiTaskService`
## 3. 本批范围
### 3.1 要完成的内容
1. 兼容角色主形象候选生成接口
2. 兼容角色主形象任务状态查询接口
3. 兼容角色主形象正式发布接口
4. 候选草稿对象写入 OSS `generated-character-drafts/*`
5. 正式主图对象写入 OSS `generated-characters/*`
6. 正式发布结果写入 `asset_object`
7. 正式发布结果绑定到角色实体槽位
8. 返回字段继续保持旧前端可消费 contract
### 3.2 本批不解决的内容
1. 不落 `asset_job` 正式 SpacetimeDB 表
2. 不落 `character_visual_asset` 强业务表
3. 不落 `character-workflow-cache`
4. 不落 `character-animation` 全链路
5. 不回写 `src/data/characterOverrides.json`
6. 不要求前端改成新的对象读取协议
## 4. 旧接口兼容 contract
### 4.1 `POST /api/assets/character-visual/generate`
返回结构继续保持:
1. `ok`
2. `taskId`
3. `model`
4. `prompt`
5. `drafts`
其中每个 `draft` 继续包含:
1. `id`
2. `label`
3. `imageSrc`
4. `width`
5. `height`
补充口径:
1. `imageSrc` 继续返回旧前端习惯的 `/generated-character-drafts/*`
2. 草稿对象底层不再写本地 `public/`
3. 草稿对象真相仅在 OSS
### 4.2 `GET /api/assets/character-visual/jobs/:taskId`
返回结构继续保持旧前端读取方式:
1. `taskId`
2. `kind`
3. `status`
4. `characterId`
5. `model`
6. `prompt`
7. `createdAt`
8. `updatedAt`
9. `result`
10. `errorMessage`
当前阶段直接复用 `AiTaskService` 内存态任务快照派生,不要求前端改字段名。
### 4.3 `POST /api/assets/character-visual/publish`
返回结构继续保持:
1. `ok`
2. `assetId`
3. `portraitPath`
4. `overrideMap`
5. `saveMessage`
补充口径:
1. `portraitPath` 固定返回 `/generated-characters/*`
2. 当前 `overrideMap` 先返回空对象 `{}`,只做 contract 兼容,不再在 Rust 后端写本地覆盖文件
3. `saveMessage` 明确说明当前是“已写入 OSS 并绑定业务实体”
## 5. 业务实体与槽位约定
本批统一复用通用 `asset_entity_binding`
### 5.1 角色主形象正式对象
| 字段 | 取值 |
| --- | --- |
| `entity_kind` | `character` |
| `entity_id` | `characterId` |
| `slot` | `primary_visual` |
| `asset_kind` | `character_visual` |
补充口径:
1. 同一角色重复发布时,允许覆盖到最新对象
2. 候选草稿对象不创建业务绑定
3. 业务引用真相以 `asset_entity_binding` 为准
## 6. OSS 对象键规划
### 6.1 候选草稿
候选草稿固定写入:
`generated-character-drafts/{characterSegment}/visual/{taskId}/candidate-{index}.svg`
### 6.2 正式主图
正式主图固定写入:
`generated-characters/{characterSegment}/visual/{assetId}/master.svg`
## 7. 任务状态口径
当前阶段不新增独立 `asset_job` 表,统一复用 `module-ai` 的内存态 `AiTaskService`
### 7.1 任务种类
`task_kind` 统一使用:
`custom_world_generation`
说明:
1. 这是当前 `module-ai` 已冻结的可用任务类型之一
2. 本批只把它当作“生成类资产任务”的最小任务容器
3. 后续 `asset_job` 表落地后,再把角色主形象任务迁到正式资产任务模型
### 7.2 阶段映射
当前固定使用以下阶段:
1. `prepare_prompt`
2. `request_model`
3. `normalize_result`
4. `persist_result`
其中:
1. `generate` 成功后,任务直接进入 `completed`
2. `publish` 不额外创建新任务,只消费已有候选路径
## 8. Rust 第一批生成策略
本批生成策略固定为:
1. 若已配置 `platform-llm`,则用文本模型生成一个结构化占位结果
2. 服务端把结果渲染成 SVG 占位图
3. 占位图写入 OSS 草稿路径
说明:
1. 这不是最终的 DashScope 图片模型正式链
2. 但它可以先把“接口 contract + 任务状态 + OSS 真相 + 正式发布绑定”全部打通
3. 后续替换成真实图片模型时,不需要再改动主链结构
## 9. 服务端执行顺序
### 9.1 生成
每次调用 `generate` 固定执行:
1. 创建 `AiTask`
2. 生成最终 prompt
3. 产出候选 SVG 字节
4. 每个候选对象上传 OSS
5. 回写任务结果
6. 返回 `/generated-character-drafts/*`
### 9.2 发布
每次调用 `publish` 固定执行:
1. 校验 `selectedPreviewSource`
2. 解析旧 `/generated-*` 路径为 `object_key`
3. 调 OSS `HEAD Object` 确认候选对象存在
4. 读取候选对象内容
5. 上传正式主图对象到 `generated-characters/*`
6. 对正式对象执行 `asset_object` 确认
7. 对正式对象执行 `asset_entity_binding`
8. 返回 `/generated-characters/*`
## 10. 完成定义
当以下条件满足时,本批视为完成:
1. Rust 已兼容 `character-visual generate / jobs / publish`
2. 候选草稿不再写本地 `public/generated-character-drafts`
3. 正式主图不再写本地 `public/generated-characters`
4. 发布成功后能形成 `asset_object`
5. 发布成功后能形成 `asset_entity_binding`
6. 前端仍能继续消费 `taskId / drafts / portraitPath` 旧 contract
## 11. 关联文档
1. [M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md)
2. [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md)
3. [SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md](./SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md)
4. [ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md](./ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md)

View File

@@ -0,0 +1,138 @@
# M6 角色资产工作流缓存接入 OSS 第一批设计
日期:`2026-04-22`
## 1. 文档目的
这份文档用于冻结 `M6` 第一批“角色资产工作流缓存”的真实落地口径。
本批只解决以下两条旧接口的 Rust 重写入口:
1. `GET /api/assets/character-workflow-cache/:characterId`
2. `POST /api/assets/character-workflow-cache`
目标是把旧 Node 写入本地 `public/generated-character-drafts/*/workflow-cache.json` 的临时缓存,切到 OSS JSON 草稿对象,继续保持前端当前可消费 contract。
## 2. 当前前提
当前仓库已经具备以下能力:
1. `platform-oss::OssClient::put_object`
2. `platform-oss::OssClient::sign_get_object_url`
3. `generated-character-drafts/*` 兼容对象键前缀
4. 角色主形象与动作导入已经开始把草稿对象写入 OSS
因此本批不新增数据库表,也不引入本地 JSON 文件。
## 3. 本批范围
### 3.1 要完成的内容
1. 兼容工作流缓存读取接口
2. 兼容工作流缓存保存接口
3. 缓存 JSON 写入 OSS `generated-character-drafts/*`
4. 返回字段继续保持旧前端可消费 contract
5. 不再把缓存写入本地 `public/`
### 3.2 本批不解决的内容
1. 不落 `asset_object`
2. 不落 `asset_manifest`
3. 不落 `character_visual_asset`
4. 不落 `character_animation_asset`
5. 不做跨设备强一致合并
6. 不迁移历史本地缓存文件
## 4. 旧接口兼容 contract
### 4.1 `GET /api/assets/character-workflow-cache/:characterId`
返回结构继续保持:
1. `ok`
2. `cache`
补充口径:
1. 未找到 OSS 缓存对象时返回 `cache: null`
2. 找到对象但 `characterId` 不匹配时返回 `cache: null`
3. 返回的 `cache` 字段保持前端 `CharacterAssetWorkflowCache` 结构
### 4.2 `POST /api/assets/character-workflow-cache`
请求结构继续保持前端当前字段:
1. `characterId`
2. `visualPromptText`
3. `animationPromptText`
4. `visualDrafts`
5. `selectedVisualDraftId`
6. `selectedAnimation`
7. `imageSrc`
8. `generatedVisualAssetId`
9. `generatedAnimationSetId`
10. `animationMap`
返回结构继续保持:
1. `ok`
2. `cache`
3. `saveMessage`
## 5. OSS 对象键规划
缓存 JSON 固定写入:
`generated-character-drafts/{characterSegment}/workflow-cache/workflow-cache.json`
其中:
1. `characterSegment` 来自 `characterId` 的安全路径片段
2. 文件名固定为 `workflow-cache.json`
3. content type 固定为 `application/json; charset=utf-8`
## 6. 字段归一化规则
保存接口固定执行以下归一化:
1. `characterId` 必填trim 后不能为空
2. `visualPromptText` 最长保留 280 字
3. `animationPromptText` 最长保留 280 字
4. `visualDrafts` 只保留有 `imageSrc` 的候选
5. `visualDrafts[].width` 默认 `1024`
6. `visualDrafts[].height` 默认 `1536`
7. `selectedAnimation` 默认 `idle`
8.`imageSrc / generatedVisualAssetId / generatedAnimationSetId` 不序列化
9. 非对象 `animationMap` 归一化为 `null`
10. `updatedAt` 由 Rust 服务端生成 UTC 时间
## 7. 元数据规范
缓存 JSON 对象写入以下 `x-oss-meta-*` 元数据:
1. `asset_kind = character_workflow_cache`
2. `owner_user_id = asset-tool`
3. `entity_kind = character`
4. `entity_id = characterId`
5. `slot = workflow_cache`
说明:
1. 旧资产工坊接口没有显式 Bearer第一批继续使用 `asset-tool` 作为兼容归属。
2. workflow cache 是工作流草稿状态,不是正式可发布资产,因此本批不确认 `asset_object`
## 8. 完成定义
当以下条件满足时,本批视为完成:
1. Rust 已兼容 `GET /api/assets/character-workflow-cache/:characterId`
2. Rust 已兼容 `POST /api/assets/character-workflow-cache`
3. 缓存 JSON 写入 OSS `generated-character-drafts/*`
4. 未命中时返回 `cache: null`
5. 前端仍能继续消费 `cache / saveMessage` 旧 contract
## 9. 关联文档
1. [M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md)
2. [M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md](./M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md)
3. [M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md](./M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md)

View File

@@ -0,0 +1,203 @@
# 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 真相链。
## 2. 本批范围
### 2.1 要完成的内容
1. `POST /api/custom-world/scene-image` 接入真实 DashScope 图片生成
2. `POST /api/custom-world/cover-image` 接入真实 DashScope 图片生成
3. `POST /api/custom-world/cover-upload` 接入裁剪、缩放、压缩
4. 生成后的图片仍统一写入 `OSS`
5. 每次成功写图仍统一形成 `asset_object + asset_entity_binding`
6. 路由响应继续保持旧前端字段形状
### 2.2 本批不解决的内容
1. 不引入新的 custom world 图片任务表
2. 不引入 `signedUrl` 直返业务字段
3. 不在本批补视频 Range、分片传输或前端编辑器新交互
4. 不在本批迁移更多 custom world 非图片媒体链路
## 3. 旧 Node 口径对齐
### 3.1 场景图生成
Node 旧链路区分两种模式:
1. 无参考图:走 DashScope `text2image`
2. 有参考图:走 DashScope `multimodal-generation`
本批 Rust 继续保持同口径:
1. `referenceImageSrc` 为空时:
- 模型默认 `wan2.2-t2i-flash`
- 路径:`/services/aigc/text2image/image-synthesis`
- 异步创建任务后轮询 `/tasks/{taskId}`
2. `referenceImageSrc` 非空时:
- 模型默认 `qwen-image-2.0`
- 路径:`/services/aigc/multimodal-generation/generation`
- 直接取返回中的第一张图
### 3.2 封面图生成
Node 旧链路也区分两种模式:
1. 无参考图:`wan2.2-t2i-flash`
2. 有参考图:`qwen-image-2.0`
Rust 本批保持一致,并继续沿用:
1. `profile + opening act + selected roles + landmarks` 作为 prompt 上下文
2. 最多 6 张参考图
3. 返回 `sourceType = generated`
### 3.3 封面上传
Node 旧链路对上传封面有明确处理:
1. 请求必须提供 `cropRect`
2. `cropRect` 必须保持 `16:9`
3. 输出固定缩放为 `1600x900`
4. 输出格式固定为 `webp`
5. 输出体积上限 `1.5 MB`
6. 原图体积上限 `10 MB`
Rust 本批必须保持这组兼容约束。
## 4. 请求与响应 contract
### 4.1 `POST /api/custom-world/scene-image`
`Stage 1` 字段基础上Rust 本批补齐兼容读取:
1. `negativePrompt`
2. `referenceImageSrc`
返回仍为:
1. `imageSrc`
2. `assetId`
3. `model`
4. `size`
5. `taskId`
6. `prompt`
7. `actualPrompt`
### 4.2 `POST /api/custom-world/cover-image`
继续兼容:
1. `profile`
2. `userPrompt`
3. `referenceImageSrc`
4. `characterRoleIds`
5. `size`
返回仍为:
1. `imageSrc`
2. `assetId`
3. `sourceType = generated`
4. `model`
5. `size`
6. `taskId`
7. `prompt`
8. `actualPrompt`
### 4.3 `POST /api/custom-world/cover-upload`
继续兼容:
1. `profileId`
2. `worldName`
3. `imageDataUrl`
4. `cropRect`
返回仍为:
1. `imageSrc`
2. `assetId`
3. `sourceType = uploaded`
## 5. 服务端执行顺序
### 5.1 场景图 / 封面图生成
统一执行:
1. 归一 prompt 与模型选择
2. 向 DashScope 发起生成请求
3. 下载生成结果图片二进制
4. `put_object`
5. `HEAD Object`
6. `confirm asset_object`
7. `bind asset_entity_binding`
8. 返回 `legacyPublicPath`
### 5.2 封面上传
统一执行:
1. 解析 `imageDataUrl`
2. 校验原图体积
3. 解码图片
4.`cropRect` 裁剪
5. 校验裁剪区域 `16:9`
6. 缩放到 `1600x900`
7. 编码为 `webp`
8. 若超过 `1.5 MB`,逐档降低质量重试
9. `put_object`
10. `HEAD Object`
11. `confirm asset_object`
12. `bind asset_entity_binding`
13. 返回 `legacyPublicPath`
## 6. 环境变量与模型口径
本批继续复用现有 DashScope 环境变量,不新增另一套命名:
1. `DASHSCOPE_BASE_URL`
2. `DASHSCOPE_API_KEY`
3. `DASHSCOPE_IMAGE_REQUEST_TIMEOUT_MS`
模型默认值固定为:
1. 场景图文生图:`wan2.2-t2i-flash`
2. 场景图参考图模式:`qwen-image-2.0`
3. 封面文生图:`wan2.2-t2i-flash`
4. 封面参考图模式:`qwen-image-2.0`
## 7. 完成定义
当以下条件满足时,本批视为完成:
1. `scene-image` 不再返回 Rust SVG 占位图
2. `cover-image` 不再返回 Rust SVG 占位图
3. `cover-upload` 已执行 `cropRect + 16:9 + webp + 1.5MB`
4. 三条链路仍统一落到 `OSS + asset_object + asset_entity_binding`
5. 前端无需改 contract 即可继续消费
## 8. 关联文档
1. [M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md)
2. [ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md](./ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md)
3. [M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md](./M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md)

View File

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

View File

@@ -37,6 +37,8 @@
- [SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md)`M2` 第一张身份主表 `user_account` 的职责边界、字段、唯一约束、状态迁移、旧 `users` 映射与落地约束。
- [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md):基于当前 Node 后端能力清单,设计用 `SpacetimeDB + Axum + 阿里云 OSS` 重写后端的目标架构、模块映射、数据分层、迁移顺序与验收标准。
- [M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md](./M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md):冻结 M6 剩余的 STS 与服务端上传 helper 落地口径,明确当前上传主链为服务器上传 OSSWeb 端只负责签名读下载。
- [M6_ASSET_METADATA_HASH_VERSION_AND_SPECIALIZED_TABLE_BOUNDARY_2026-04-22.md](./M6_ASSET_METADATA_HASH_VERSION_AND_SPECIALIZED_TABLE_BOUNDARY_2026-04-22.md):冻结 M6 第一批内容 hash、版本、manifest、asset job 与强业务资产表的 Stage 1 边界,明确当前使用 `asset_object + asset_entity_binding + OSS manifest + AiTaskService` 闭合,不重复新增表。
- [M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md](./M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md):冻结 M6 旧 `/generated-*` 路径到 OSS 私有读同源代理的兼容口径,保证旧前端仍能直接消费图片、视频、动作帧与 manifest。
- [AXUM_TO_SPACETIMEDB_ASSET_OBJECT_CONFIRM_CALL_DESIGN_2026-04-21.md](./AXUM_TO_SPACETIMEDB_ASSET_OBJECT_CONFIRM_CALL_DESIGN_2026-04-21.md):冻结 `POST /api/assets/objects/confirm` 从 Axum 通过 Rust SDK 调用 `SpacetimeDB procedure` 的最小落地方案,明确本地 server、数据库名、procedure/reducer 分工与 `spacetime-client` 边界。
- [M3_RUNTIME_SETTINGS_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md](./M3_RUNTIME_SETTINGS_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md):冻结 `M3` 首批 `runtime settings` 纵向切片的表字段、默认值、procedure、Axum facade、错误 contract 与测试策略。
- [SPACETIMEDB_CUSTOM_WORLD_AGENT_SESSION_STAGE6_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_AGENT_SESSION_STAGE6_DESIGN_2026-04-22.md):冻结 `M5` Agent session create / snapshot 的最小 SpacetimeDB 与 Axum facade 闭环,明确本轮不迁移 LLM、SSE、卡片更新和完整 action registry。
@@ -46,6 +48,10 @@
- [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_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。
- [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 后端的实施方案与验收口径。