# 基于 SpacetimeDB + Axum + 阿里云 OSS 的后端重写设计文档 日期:`2026-04-20` ## 1. 文档定位 这份文档不是继续扩写当前 `server-node/` 的实现细节,而是基于**当前仓库已经落地的后端能力**,为下一版 Rust 后端提供一份可以直接落地编码的重写设计。 目标很明确: 1. 保留当前项目已经具备的后端能力面,不做需求缩水。 2. 把后端实现从 `Express + PostgreSQL + 本地 public/generated-* 文件` 重写为: - `Axum`:唯一 HTTP 边界层与流式接口层 - `SpacetimeDB`:唯一运行时状态与实时订阅真相源 - `阿里云 OSS`:唯一大文件与二进制资产存储 3. 让前端在第一阶段尽量少改,优先兼容当前 `/api/*`、`/healthz`、SSE 与资源路径习惯。 ## 2. 当前工程必须继承的能力基线 以下能力清单来自 `2026-04-20` 迁移设计时对旧 Node 后端的快照整理,只作为迁移参考,不再作为新功能扩展依据。后续实现方向以 `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md` 为准。 - 对外挂载面:`6` 个 - 已登记路由:`96` 条 - 内部模块目录:`12` 个 - 公开接口:`10` 条 - JWT 接口:`69` 条 - 环境开关接口:`17` 条 - 流式接口:`6` 条 当前 Node 后端的历史基线仍然是这 `6` 个挂载面,但自 `2026-04-21` 起,本轮 Rust 后端重写的 active rewrite target 固定为其中 `5` 个: 1. `health` 2. `auth` 3. `assets` 4. `runtime-main` 5. `runtime-story-action` 补充说明: 1. `editor` 挂载面在历史系统中真实存在,但已被确认为遗留无用能力。 2. `editor` 仅保留为历史基线对照,不纳入本轮 `server-rs` 重写验收。 3. 当前执行顺序允许在 `M3 / M4 / M5` 前,先前置 `assets / OSS` 的基础设施接入,以便后续 runtime、custom world、agent 统一复用同一资产入口。 当前后端内部模块也不能“凭感觉重设计”,而要按现有职责做映射: 1. `ai` 2. `assets` 3. `combat` 4. `custom-world` 5. `editor` 6. `inventory` 7. `npc` 8. `progression` 9. `quest` 10. `runtime` 11. `runtime-item` 12. `story` 其中: 1. 上述 `12` 个模块是历史基线总量。 2. 本轮 active rewrite modules 固定为 `11` 个。 3. `editor` 仅保留历史事实,不进入 `server-rs` 主线 crate 与阶段验收。 ## 3. 技术选型后的硬边界 ### 3.1 SpacetimeDB 的平台约束与本项目边界 根据 SpacetimeDB 官方 Rust crate 文档,`reducer` 不能直接使用 `std::net` 或 `std::fs` 进行外部 IO;而 2.0 官方文档又提供了 `procedure + ctx.http` 的受控 HTTP 能力。 也就是说,**平台层面并不是完全不能做外部调用**,但对于本项目这次重写,我们仍然主动把下面这些副作用统一放到 Axum,而不是塞进 SpacetimeDB: 1. 阿里云 OSS 上传、下载、签名 2. DashScope / Ark / 其他 LLM 请求 3. 微信 OAuth 4. 短信验证码 5. 本地文件系统读写 这样设计不是因为“SpacetimeDB 绝对做不到”,而是因为这类能力都要求: 1. 更强的重试、超时、日志和熔断能力 2. 更自由的 SDK / multipart / 签名上传实现 3. 与 HTTP 头、cookie、回调、对象存储策略深度耦合 4. 与游戏状态 schema 解耦,避免把第三方供应商能力直接绑进数据库模块发布周期 结论: - `SpacetimeDB` 负责状态、规则、订阅、命令执行与读模型。 - `Axum` 负责所有外部副作用与 HTTP 协议。 - 在本次重写中,不为 SpacetimeDB module 增加任何外部副作用例外通道。 ### 3.2 Axum 的边界 根据 Axum 官方文档,`Router` 通过 `.with_state(...)` 注入共享状态后才成为可真正 `serve()` 的路由树;因此 Axum 适合作为: 1. 统一 HTTP 入口 2. Bearer / Cookie 鉴权入口 3. SSE 输出入口 4. OSS 上传凭证签发入口 5. 与 SpacetimeDB 的应用层编排入口 结论: - Axum 是唯一 BFF / API Gateway。 - 前端第一阶段仍然只认识 Axum,不直接依赖 SpacetimeDB 原生接口。 - 在 `M0 ~ M6` 迁移期内,不新增前端直连 SpacetimeDB 的主链方案。 ### 3.3 OSS 的边界 根据阿里云 OSS 官方文档: 1. `PostObject` 适合浏览器表单直传,服务端可以下发 `policy`、`signature` 与上传条件。 2. `STS` 临时访问凭证适合把上传权限以有限时、有限范围的方式下发给客户端。 3. `PutObject` / `PostObject` 都支持 `x-oss-meta-*` 元数据与标签。 结论: - 所有图片、动画、精灵表、场景图、封面图、视频参考素材都存 OSS。 - SpacetimeDB 只存 `bucket`、对象键、版本、尺寸、状态、逻辑元数据,不存二进制。 - Axum 负责下发直传凭证、校验上传结果、补写元数据。 ## 4. 目标总体架构 ```text Web / Mobile Frontend ├─ 继续访问 /api/*、/healthz、/generated-* └─ 第一阶段不直接依赖 SpacetimeDB 原生协议 Axum API Server ├─ auth:登录、refresh cookie、JWT/OIDC 签发 ├─ runtime facade:兼容当前 REST / SSE contract ├─ asset gateway:OSS 直传签名、对象确认、媒体任务编排 ├─ ai gateway:DashScope / Ark / 其他模型调用 ├─ background workers:异步作业执行与回写 └─ SpacetimeDB client:调用 reducer、查询 view / public table、订阅任务状态 SpacetimeDB Module ├─ auth tables:用户、身份、session、风控、审计 ├─ runtime tables:存档、设置、浏览历史、个人面板 ├─ gameplay tables:story / npc / quest / inventory / combat / progression ├─ custom-world tables:问答会话、agent 会话、草稿卡、操作记录 ├─ asset metadata tables:生成任务、对象清单、引用关系 ├─ reducers:唯一状态写入口 └─ views / public tables:唯一读模型与订阅面 Aliyun OSS ├─ generated-character-drafts ├─ generated-characters ├─ generated-animations ├─ generated-custom-world-scenes ├─ generated-custom-world-covers ├─ generated-qwen-sprites └─ temp-uploads / workflow-cache ``` ## 5. 重写后的核心原则 ### 5.1 先兼容当前 API 面,再逐步让前端吃到实时能力 第一阶段不要强推前端直接改成 SpacetimeDB 客户端模式,而是: 1. Axum 保持当前 `/api/*` 路由空间。 2. Axum 保持当前 `x-request-id / x-api-version / x-route-version` 头和响应 envelope。 3. Axum 保持当前 story / custom-world-agent 的 SSE 体验。 4. SpacetimeDB 先做后端内部真相源。 补充执行口径: 1. 虽然总体里程碑仍保留 `M6` 编号,但 `OSS` 的平台适配、浏览器直传票据与旧 `/generated-*` 路径兼容能力允许提前于 `M3 / M4 / M5` 落地。 2. 提前落地的目标是先收口统一资产入口,不是提前把全部资产业务状态迁完。 第二阶段再按模块把只读页改成直接订阅 SpacetimeDB。 ### 5.2 命令与读模型分离 SpacetimeDB 官方文档明确说明: 1. reducer 是唯一可修改表的入口。 2. reducer 不直接返回业务数据给客户端。 3. view 可被查询与订阅,并会随底层表变化自动更新。 因此本项目必须采用: - `Reducer = 命令入口` - `View / Public Table = 读模型入口` - `Axum = HTTP 兼容层与聚合层` ### 5.3 大对象不进数据库 当前 Node 后端把生成结果落在 `public/generated-*`。重写后统一改成: - `OSS` 存二进制 - `SpacetimeDB` 存引用和状态 - `Axum` 输出 URL、签名 URL 或 CDN URL ### 5.4 Schema 必须按 SpacetimeDB 的迁移约束设计 SpacetimeDB 官方文档对自动迁移的限制很强: 1. 删表、改列类型、改列名、调整列顺序都不是安全日常操作。 2. 新列只能追加到表末尾,且要提供默认值。 3. reducer 改名或删除会直接影响客户端调用。 因此本项目的数据模型必须尽量满足: 1. 主表稳定、字段追加式演进 2. 高频变化数据优先事件表化 3. 聚合结果优先投影表 / view,而不是频繁重塑旧表结构 ### 5.5 主工程必须按多 crate 方式组织模块 从当前版本开始,Rust 后端固定采用“主工程 crate + 独立模块 crate”的方式组织。 这里再明确一层: 1. `crates/` 只是工作区下统一承载 Rust crate 的目录名。 2. 目录里的每个独立单元在 Rust 语义上都按 workspace crate 对待。 组织规则固定为: 1. `crates/api-server` 作为 Axum 主工程 crate,只负责协议装配与模块组合。 2. `crates/spacetime-module` 作为 SpacetimeDB 主工程 crate,只负责聚合各模块 crate 的表、reducer、view。 3. 每个独立业务模块必须优先拥有自己的 workspace crate,再由主工程 crate 引用。 4. 只有共享 contract、共享领域内核、平台适配、SpacetimeDB client 这类跨模块能力,才允许使用共享 crate。 这样做的目的,是避免把当前 `12` 个既有模块边界重新压缩回单个“大 application crate”或“大 domain crate”中,确保后续重写能继续按模块独立演进。 ### 5.6 SpacetimeDB 相关修改的执行约束 从当前版本开始,凡是涉及 `SpacetimeDB` 的设计、实现、脚本、调试与前端接入,统一要求显式使用以下 skill 作为执行依据: 1. [$spacetimedb-cli](../../.codex/skills/spacetimedb-cli/SKILL.md) 2. [$spacetimedb-rust](../../.codex/skills/spacetimedb-rust/SKILL.md) 3. [$spacetimedb-concepts](../../.codex/skills/spacetimedb-concepts/SKILL.md) 4. [$spacetimedb-typescript](../../.codex/skills/spacetimedb-typescript/SKILL.md) 执行要求: 1. 涉及 `spacetime` CLI、发布、绑定生成、本地联调时,优先按 `spacetimedb-cli` 约束执行。 2. 涉及 `crates/spacetime-module` 的表、reducer、view、Rust API 使用时,优先按 `spacetimedb-rust` 与 `spacetimedb-concepts` 约束执行。 3. 涉及前端或 Node 侧的 SpacetimeDB 绑定、订阅、TypeScript SDK 接入时,优先按 `spacetimedb-typescript` 与 `spacetimedb-concepts` 约束执行。 4. 若 skill 约束与仓库内已有旧实现存在冲突,必须先以 skill 约束校正设计文档与实现方案,再继续编码,避免沿用已过时或幻觉式 API。 ## 6. 推荐工程结构 本次重写固定在仓库根目录新增 Rust 工作区 `server-rs/`,并与 `server-node/` 同级: ```text server-rs/ ├─ Cargo.toml ├─ crates/ │ ├─ api-server/ # Axum 主工程 crate,负责装配路由、中间件、SSE 与模块引用 │ ├─ spacetime-module/ # SpacetimeDB 主工程 crate,负责聚合表、reducer、view 并发布 wasm │ ├─ module-auth/ # 鉴权与会话模块 crate │ ├─ module-runtime/ # runtime snapshot / settings / profile 模块 crate │ ├─ module-story/ # story 主循环模块 crate │ ├─ module-combat/ # 战斗规则模块 crate │ ├─ module-inventory/ # 背包与奖励模块 crate │ ├─ module-npc/ # NPC 状态与对话模块 crate │ ├─ module-progression/ # 成长与章节推进模块 crate │ ├─ module-quest/ # 任务运行时模块 crate │ ├─ module-runtime-item/ # 运行时物品模块 crate │ ├─ module-custom-world/ # 自定义世界与 agent 模块 crate │ ├─ module-assets/ # 资产任务与对象绑定模块 crate │ ├─ module-ai/ # AI 编排模块 crate │ ├─ shared-contracts/ # HTTP DTO / SSE event / 前后端兼容 contract │ ├─ shared-kernel/ # 跨模块共享领域类型、ID、枚举、值对象 │ ├─ shared-logging/ # 工作区统一日志初始化与 tracing subscriber 基础设施 │ ├─ platform-auth/ # JWT、cookie、provider adapter │ ├─ platform-oss/ # OSS 直传、签名、对象管理 │ ├─ platform-llm/ # DashScope / Ark / 其他模型适配 │ ├─ spacetime-client/ # 生成 bindings 后的 DB client adapter │ └─ tests-support/ # 集成测试、contract 测试、smoke 支撑 └─ scripts/ ├─ dev.sh / dev.ps1 ├─ check.sh / check.ps1 ├─ spacetime-dev.sh / spacetime-dev.ps1 └─ smoke.sh / smoke.ps1 ``` 目录职责约束: 1. `crates/api-server/` 只做协议装配、鉴权、中间件、handler 与模块组合,不把业务模块重新堆回单包。 2. `crates/spacetime-module/` 只负责聚合各模块 crate 的状态模型,不直接承接外部副作用。 3. `crates/module-*` 保持与当前业务模块边界一一对应,已明确退出本轮的 `editor` 遗留模块除外;必要时可在 crate 内部再拆 `application`、`domain`、`spacetime` 子层次。 4. `crates/shared-contracts/` 负责与当前前端兼容的 JSON / SSE 协议。 5. `crates/shared-kernel/` 只放跨模块复用的数据结构和规则,不碰框架。 6. `crates/shared-logging/` 负责统一日志初始化、过滤器解析与 subscriber 基础设施,不承接 HTTP 业务语义。 7. `crates/platform-*` 统一承接三方供应商与平台适配。 命名补充说明: 1. 本文后续若出现 `auth-service`、`oss-service`、`llm-service`、`application::...` 等历史逻辑名,统一视为职责标签,而不是强制要求继续存在同名顶层目录。 2. 在新的多 crate 版本中,这些职责会落到 `crates/module-*` 内部子层次,或落到 `crates/platform-*`、`crates/shared-*` 等共享 crate 中。 ## 7. 目标模块映射 | 当前模块 | 重写后主归属 | 次归属 | 说明 | | --- | --- | --- | --- | | `auth` | Axum `auth-service` + SpacetimeDB private tables | 无 | 登录入口、refresh cookie、JWT/OIDC、审计与风控拆为“Axum 处理副作用 + SpacetimeDB 落状态”。 | | `runtime` | SpacetimeDB module | Axum facade | 存档、设置、浏览历史、profile dashboard 统一进入 SpacetimeDB。 | | `story` | SpacetimeDB module | Axum SSE facade | story action、状态推进、可选项构造、恢复态读取以后端 reducer/view 为准。 | | `combat` | SpacetimeDB module | 无 | 纯规则计算,天然适合 reducer。 | | `inventory` | SpacetimeDB module | 无 | 背包、赠礼、交易、物品副作用全部 reducer 化。 | | `npc` | SpacetimeDB module | Axum for LLM dialogue | 关系、招募、状态机在 SpacetimeDB;LLM 台词生成留在 Axum。 | | `progression` | SpacetimeDB module | 无 | 关卡、等级、敌对 scaling、章节推进都做领域表和 reducer。 | | `quest` | SpacetimeDB module | Axum for AI quest drafting | 任务主状态在 SpacetimeDB;生成型内容由 Axum 生产后回写。 | | `runtime-item` | SpacetimeDB module | Axum for AI intent | 物品奖励和解析归 reducer;AI 意图生成由 Axum 负责。 | | `custom-world` | SpacetimeDB module + Axum orchestration | OSS | 会话、草稿、agent 状态放 SpacetimeDB;世界编译、资产生成、发布编排在 Axum。 | | `ai` | Axum `llm-service` | SpacetimeDB task tables | 外部模型调用全部放 Axum。 | | `assets` | Axum `oss-service` | SpacetimeDB asset metadata | 二进制进 OSS,元数据进 SpacetimeDB。 | 补充说明: 1. 历史 `editor` 模块不纳入 `server-rs` 本轮重写。 2. 相关 `/api/editor/*` 与 `server-node/src/modules/editor` 仅保留为旧系统对照事实,后续若要清理再单独立项。 ## 8. 数据建模方案 ## 8.1 表的分层 建议在 SpacetimeDB 中至少拆成 5 组表: ### A. 身份与会话表 - `user_account` - `auth_identity` - `refresh_session` - `auth_audit_log` - `auth_risk_block` - `sms_auth_event` - `wechat_auth_state` 说明: 1. 这些表默认都应为 private。 2. 密码哈希、短信验证码校验、微信 code 换 token 的动作在 Axum 完成。 3. Axum 再调用 reducer 写入最终结果。 `user_account` 的详细字段、状态迁移与旧 `users` 映射规则,见: - [SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md) `auth_identity` 的 provider 约束、唯一键与手机号/微信身份写入规则,见: - [SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md) `refresh_session` 的 cookie/hash 边界、轮换与吊销语义,见: - [SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md) `auth_audit_log` 的事件范围、追加写规则与 DTO 派生约束,见: - [SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md) `auth_risk_block` 的作用域、活跃态与解除规则,见: - [SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md) `sms_auth_event` 的事件范围、发送/校验写入规则、统计口径与和风控/审计表的边界,见: - [SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md) `wechat_auth_state` 的字段、过期时间、授权场景、callback 单次消费与清理策略,见: - [SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md) ### B. 运行时主状态表 - `runtime_snapshot` - `runtime_setting` - `profile_dashboard_state` - `profile_wallet_ledger` - `profile_played_world` - `profile_save_archive` - `user_browse_history` 说明: 1. `runtime_snapshot` 继续作为“恢复游戏”的主聚合表。 2. `profile_*` 作为只读页投影表,避免每次从大快照现算。 3. `browse_history`、`save_archive` 单独建表,不要藏进 snapshot blob。 ### C. Gameplay 领域表 - `story_session` - `story_event` - `npc_state` - `quest_record` - `inventory_slot` - `treasure_record` - `battle_state` - `player_progression` - `chapter_progression` 说明: 1. `story_action` 不直接改大 JSON,而是 reducer 驱动多个领域表。 2. 是否继续保留兼容快照,可由投影 reducer 汇总生成。 3. 旧前端仍需要 `gameState + currentStory` 时,由 Axum 聚合成兼容 DTO。 ### D. Custom World 表 - `custom_world_profile` - `custom_world_session` - `custom_world_agent_session` - `custom_world_agent_message` - `custom_world_agent_operation` - `custom_world_draft_card` - `custom_world_asset_link` - `custom_world_gallery_entry` 说明: 1. 传统问答流与 Agent 流不再混一个 payload。 2. 卡片、操作、消息必须拆表,不能再都塞进一个大 JSON 会话体。 3. 公开画廊作为独立投影,避免从 profile 运行时拼装。 ### E. 资产与对象元数据表 - `asset_job` - `asset_object` - `asset_manifest` - `character_visual_asset` - `character_animation_asset` - `scene_image_asset` - `sprite_sheet_asset` 说明: 1. 任务状态在 SpacetimeDB。 2. 二进制对象在 OSS。 3. `asset_object` 的正式真相字段固定为 `bucket + object_key`。 4. 所有 URL 都只作为派生读模型,不作为对象主键存储。 5. `asset_object` 的首版字段、访问级别与索引设计见: - [SPACETIMEDB_ASSET_OBJECT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_ASSET_OBJECT_TABLE_DESIGN_2026-04-21.md) ## 8.2 public / private 原则 依据 SpacetimeDB 官方访问权限文档: 1. private table 默认只给 reducer / view / owner 看。 2. public table 才适合直接查询和订阅。 本项目建议: - `auth_*`、`refresh_session`、风控、验证码全部 private - `runtime_snapshot` private - `story_session` private - `custom_world_agent_*` 绝大多数 private - `gallery`、公共角色卡、公共作品索引可 public - 如需“用户自己的读模型”,优先用 `ViewContext` view 暴露 ## 8.3 view 设计原则 依据 SpacetimeDB 官方 view 文档: 1. view 可以被查询和订阅,底层表变化时会自动更新。 2. `AnonymousViewContext` 可被所有用户共享物化结果,性能明显优于按用户单独计算。 3. view 不适合全表 `.iter()` 扫描,应该通过索引查找或返回可分析的 query。 所以本项目的 view 设计规则必须是: 1. 画廊、排行榜、商店、公共世界列表,优先匿名 view。 2. “我的背包 / 我的档案 / 我的当前会话”这类按用户隔离的读模型,才用带身份上下文的 view。 3. 首页、资料库、历史记录、存档列表都必须先有索引,再写 view。 ## 9. 鉴权设计 ## 9.1 总体方案 建议保留当前“密码 / 手机验证码 / 微信”三类登录能力,但把实现改成: 1. Axum 负责登录副作用: - 密码校验 - 短信发送与校验 - 微信 OAuth code 交换 - refresh cookie 签发与轮换 2. Axum 负责签发**OIDC 兼容 JWT** 3. 同一张 JWT 同时用于: - Axum 自身 Bearer 鉴权 - SpacetimeDB reducer / view 的身份透传 这是可行的,因为 SpacetimeDB 官方文档说明其可以从 OIDC 兼容 JWT 中提取身份声明,并在模块上下文中使用这些 claims。 ## 9.2 Token 设计 建议: - `iss`:固定为 Axum 网关身份发行者,例如 `https://api.genarrative.example/auth` - `sub`:稳定用户 ID - 扩展 claims: - `sid`:session id - `provider`:password / phone / wechat - `roles` - `phone_verified` - `display_name` `iss/sub/sid/provider/roles/ver/phone_verified/binding_status` 的字段定义、哪些字段禁止进入 JWT、以及 Axum 与 `SpacetimeDB` 的使用边界,见: - [OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md](./OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md) ## 9.3 Refresh Session 建议保留当前模式: 1. 浏览器短期 Bearer access token 2. HttpOnly refresh cookie 3. refresh session 服务端可吊销 但新的会话 ledger 写入 SpacetimeDB private 表即可。 ## 10. Axum 侧设计 ## 10.1 进程职责 Axum 进程建议拆成以下子系统: 1. `http::middleware` - request id - tracing - envelope / 错误标准化 - auth extractor 2. `http::routes` - `/healthz` - `/api/auth/*` - `/api/runtime/*` - `/api/runtime/story/*` - `/api/assets/*` 3. `application::services` - story facade - runtime snapshot facade - custom world facade - asset facade 4. `infra` - SpacetimeDB client - OSS adapter - DashScope / Ark adapter - SMS / WeChat adapter ## 10.2 流式接口 当前项目已经有 6 条流式接口,重写时不建议一上来全部改成 WebSocket。 第一阶段建议: 1. 继续使用 Axum SSE 输出流式文本与阶段事件。 2. Axum 在处理流式过程中,持续把阶段状态写回 SpacetimeDB。 3. 前端仍按当前 SSE 协议消费。 适合继续保留为 Axum SSE 的场景: 1. `story/initial` 2. `story/continue` 3. `chat/character/*` 4. `chat/npc/*` 5. `custom-world/generate/stream` 6. `custom-world/agent/messages/stream` ## 10.3 兼容 contract Axum 第一阶段需要兼容当前项目的这些约定: 1. 路径空间不变 2. `x-request-id` 3. `x-api-version` 4. `x-route-version` 5. `x-response-time-ms` 6. 可选 envelope:`x-genarrative-response-envelope` 这样前端可以在不大改 `src/services/*` 的前提下切换后端实现。 ## 11. OSS 设计 ## 11.1 对象键规划 建议统一对象键前缀,保持与当前前端路径习惯接近: ```text generated-character-drafts/{character_id}/{job_id}/{file} generated-characters/{character_id}/visual/{asset_id}/{file} generated-animations/{character_id}/{animation_set_id}/{action}/{file} generated-custom-world-scenes/{profile_id}/{landmark_id}/{asset_id}/{file} generated-qwen-sprites/{role_id}/{sheet_id}/{file} generated-custom-world-covers/{profile_id}/{asset_id}/{file} workflow-cache/{workflow_type}/{workflow_id}.json ``` ## 11.2 上传模式 建议: 1. 前端直传图片、封面、小文件:`PostObject` 2. 大文件或需要细粒度控制时:`STS + PutObject / Multipart` 3. 生成型资产:Axum worker 直接上传 OSS 其中: - `PostObject` 用于浏览器上传时,Axum 负责生成 policy 与 signature。 - policy 中必须明确: - key 前缀 - content-type 白名单 - content-length-range - success_action_status 当前已落地的最小实现补充: 1. `server-rs/crates/platform-oss` 已提供 `PostObject` 直传签名能力。 2. `server-rs/crates/api-server` 已暴露 `POST /api/assets/direct-upload-tickets`。 3. `server-rs/crates/platform-oss` 已提供私有对象 `GET` 短期签名 URL 能力。 4. `server-rs/crates/api-server` 已暴露 `GET /api/assets/read-url`。 5. 上传接口当前输出: - `bucket` - `objectKey` - `legacyPublicPath` - `formFields` - `expiresAt` 6. 读取接口当前支持: - `objectKey` - `legacyPublicPath` - `expireSeconds` 7. 当前 bucket 已明确为私有读写,因此 `publicUrl` 不再作为正式对象真相输出。 8. 当前签名链路优先兼容旧公开前缀: - `/generated-character-drafts/*` - `/generated-characters/*` - `/generated-animations/*` - `/generated-custom-world-scenes/*` - `/generated-custom-world-covers/*` - `/generated-qwen-sprites/*` 9. 当前 `POST /api/assets/sts-upload-credentials` 已按“服务器上传、Web 只下载”的需求固定为禁用式 contract,不向浏览器下发 OSS 写权限。 10. 当前 `platform-oss` 已提供服务端 `PutObject` 上传 helper,供后续 AI worker 上传生成资源后继续走对象确认链路。 ## 11.3 元数据与标签 建议所有业务对象写入统一元数据: - `x-oss-meta-owner-user-id` - `x-oss-meta-profile-id` - `x-oss-meta-entity-id` - `x-oss-meta-asset-kind` - `x-oss-meta-source-job-id` - `x-oss-meta-content-hash` - `x-oss-meta-origin` 注意: 1. OSS 官方文档要求自定义元数据使用 `x-oss-meta-*` 前缀。 2. 所有元数据总大小不能超过 `8 KB`。 ## 11.4 URL 策略 建议: 1. 业务表里统一存 `bucket + object_key` 2. 对外输出 `cdn_url` 或签名 URL 3. 私有对象默认输出短期签名 URL,而不是假设匿名公开读 为了兼容当前前端相对路径使用习惯,第一阶段可以让 Axum 或 CDN 兼容以下历史前缀: 1. `/generated-character-drafts/*` 2. `/generated-characters/*` 3. `/generated-animations/*` 4. `/generated-custom-world-scenes/*` 5. `/generated-custom-world-covers/*` 6. `/generated-qwen-sprites/*` 补充约束: 1. 当前 `xushi-dev` bucket 已明确为私有读写,因此这些旧前缀在第一阶段只代表兼容路径习惯,不代表对象可匿名读取。 2. Web 端若拿到的是历史 `/generated-*` 路径,必须先调用 `GET /api/assets/read-url` 换取 `signedUrl`,不能直接把该路径当成正式可读 URL。 3. 前端工程内凡是图片、背景图、封面图、角色图、场景图等展示入口,只要可能接收到 `/generated-*`,都必须统一走资源解析层: - 列表/卡片/普通 `` 优先复用 `src/services/assetReadUrlService.ts` - 组件内优先复用 `src/hooks/useResolvedAssetReadUrl.ts` - 通用图片标签优先复用 `src/components/ResolvedAssetImage.tsx` - 当前已完成的高优先级入口包括:`CharacterAnimator`、`CharacterPanel`、`CompanionCampModal`、`CharacterSelectionFlow`、`MapModal`、`GameCanvasSceneLayer`、`GameCanvasShared`、`GameCanvasEntityLayer`、`CustomWorldResultView`、`CustomWorldEntityEditorModal`、`CustomWorldRoleAssetStudioModal`、`CustomWorldAgentDraftDetailPanel`、`PlatformHomeView`、`PlatformWorldDetailView`、`QwenSpriteSheetTool` 4. 对私有 OSS 资源,前端在签名地址返回前不能先回退渲染原始 `/generated-*` 路径,否则浏览器会先发起一次无签名请求并触发 `403`。 2. 具体对象引用设计见: - [SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md](./SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md) ## 12. 关键业务流设计 ## 12.1 Story Action 目标: 1. `storyActionService` 当前承担的跨模块结算必须迁到 SpacetimeDB reducer。 2. Axum 只做 request parse、auth、调用 reducer、读取 view、拼回当前前端响应。 推荐流程: 1. 前端 `POST /api/runtime/story/actions/resolve` 2. Axum 校验请求并附带 JWT 3. Axum 调用 SpacetimeDB `resolve_story_action` reducer 4. reducer 内部联动: - `story` - `combat` - `inventory` - `npc` - `quest` - `runtime-item` - `progression` 5. reducer 写回领域表 6. Axum 再读取 `current_story_view`、`runtime_snapshot_view` 7. Axum 返回兼容当前前端的 `RuntimeStoryActionResponse` ## 12.2 存档与恢复 目标: 1. 仍保留当前“完整恢复游戏”的能力。 2. 但底层不再由 PostgreSQL 单大 JSON 承担全部职责。 建议: 1. `runtime_snapshot` 继续保留兼容聚合快照,满足现有恢复链路。 2. gameplay 真相由领域表维护。 3. 每次重要 reducer 提交后,异步或同步刷新 snapshot projection。 这样可以兼顾: 1. 旧前端兼容 2. 新后端可审计 3. SpacetimeDB 实时订阅能力 ## 12.3 Custom World Agent 当前 Node 后端中,`customWorldAgentOrchestrator + SessionStore + Operation` 已经是一条清晰主链。 重写后建议进一步正规化: 1. SpacetimeDB: - 存会话 - 存消息 - 存卡片 - 存操作状态 - 存草稿 profile 2. Axum: - 调 LLM - 调图片生成 - 调 OSS - 发 SSE - 把阶段结果回写 reducer 不再允许“一整个 agent 会话对象 JSON 一把写回”作为长期形态。 ## 12.4 资产生成 资产链路拆成四步: 1. Axum 创建 `asset_job` 2. Axum worker 调外部模型 3. 产物上传 OSS,写 `asset_object` 4. Axum 调 reducer 将对象绑定到: - 角色 - 地点 - 世界草稿 - Qwen sprite sheet 所有“对象是否已经被业务引用”的事实,以 SpacetimeDB 绑定表为准,而不是以 OSS 是否存在某个 key 为准。 ## 13. 迁移阶段建议 迁移期仓库边界补充约束: 1. 旧 `server-node/` 在 `M0 ~ M6` 期间继续保留,作为协议对照、回归基线与回退锚点。 2. 只有在 `M7` 切流、回归、回退方案都稳定后,才评估是否清理旧 Node 后端。 ## Phase 0:冻结能力清单 交付: 1. 固定当前 `96` 条接口为重写验收基线 2. 固定当前 `12` 个模块为迁移映射基线 3. 固定当前前端 contract 与 SSE 协议 ## Phase 1:先搭 Axum 外壳与鉴权 交付: 1. `/healthz` 2. `/api/auth/*` 3. response envelope 4. request id / tracing 5. Axum -> SpacetimeDB 基础 client 6. OIDC-compatible JWT 签发 阶段执行补充: 1. 微信登录链路在当前阶段暂缓,不进入连续执行顺序。 2. 当前优先顺序固定为:JWT / refresh cookie / 密码登录 / 手机验证码登录。 ## Phase 2:迁移 runtime snapshot / settings / profile 交付: 1. 存档 2. 设置 3. 浏览历史 4. profile dashboard 5. save archives 这是最容易先跑通的闭环。 ## Phase 3:迁移 story action 主循环 交付: 1. story reducer 2. combat / inventory / npc / quest / runtime-item / progression 联动 3. `/api/runtime/story/*` 这一阶段完成后,运行时真相就真正从 Node 版切出去了。 ## Phase 4:迁移 custom world 与 agent 交付: 1. legacy custom world session 2. custom world library / gallery 3. custom world agent 会话、卡片、操作 4. scene npc / entity generation ## Phase 5:迁移 assets / OSS 交付: 1. OSS 直传 2. 生成任务 3. 对象元数据 4. 旧 `/generated-*` 路径兼容 补充说明: 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. 验收标准 重写完成至少要满足: 1. 当前 `96` 条已登记路由全部有对应实现或明确兼容替代。 2. 当前历史 `6` 个挂载面的迁移去向全部明确,且本轮 active rewrite target 的 `5` 个挂载面全部落地。 3. 浏览器无需直接知道 SpacetimeDB 原生接口即可跑通主流程。 4. story action、存档、custom world、agent、assets 都以后端为唯一真相。 5. 所有生成图片、动画、精灵表都不再依赖本地 `public/generated-*` 持久化。 6. Axum 和 SpacetimeDB 的职责边界稳定,不把外部网络 IO 偷放进 module。 ## 15. 关键风险 ### 15.1 不能把 SpacetimeDB 当 PostgreSQL 替身直接套 SpacetimeDB 更像“带强实时订阅能力的状态机数据库”,不是传统 SQL 仓储替身。 如果仍沿用“单大 JSON + 巨型路由文件 + 过程式 handler”思路,重写后仍然会很快回到旧热点。 ### 15.2 schema 演进成本高于 PostgreSQL SpacetimeDB 自动迁移更适合“增量追加”,不适合高频改列结构。 所以必须提前定好: 1. 稳定主键 2. 稳定 reducer 命名 3. 事件表 / 投影表边界 ### 15.3 view 滥用会造成性能问题 如果把资料库、画廊、排行榜写成按用户逐个计算、又缺索引的 view,性能会很差。 因此 read model 必须先建索引,再决定用匿名 view 还是按用户 view。 ### 15.4 资产路径兼容是迁移成败关键 当前前端大量相对路径、角色图、场景图、动画图都是围绕 `/generated-*` 组织的。 如果不先做路径兼容层,存档恢复和世界资料库会大面积失效。 ## 16. 内部实现依据 这份设计稿对当前工程的判断,主要依据以下仓库现状: 1. `server-node/src/server.ts` 2. `server-node/src/app.ts` 3. `server-node/src/routes/authRoutes.ts` 4. `server-node/src/routes/runtimeRoutes.ts` 5. `server-node/src/modules/story/storyActionRoutes.ts` 6. `server-node/src/repositories/runtimeRepository.ts` 7. `server-node/src/services/customWorldAgentOrchestrator.ts` 8. 本文第 2 节保留的旧 Node 能力快照 9. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md` ## 17. 外部技术依据 以下外部依据用于确定本次新架构的技术边界,均来自官方文档或官方 crate 文档: 1. [SpacetimeDB Tables](https://spacetimedb.com/docs/tables/) 2. [SpacetimeDB Table Access Permissions](https://spacetimedb.com/docs/tables/access-permissions/) 3. [SpacetimeDB Reducers Overview](https://spacetimedb.com/docs/functions/reducers/) 4. [SpacetimeDB Views](https://spacetimedb.com/docs/functions/views/) 5. [SpacetimeDB Authorization](https://spacetimedb.com/docs/http/authorization/) 6. [SpacetimeDB Authentication](https://spacetimedb.com/docs/core-concepts/authentication/) 7. [SpacetimeDB Using Auth Claims](https://spacetimedb.com/docs/core-concepts/authentication/usage/) 8. [SpacetimeDB Rust crate docs](https://docs.rs/spacetimedb/latest/spacetimedb/) 9. [SpacetimeDB Rust Client SDK](https://spacetimedb.com/docs/sdks/rust/) 10. [Axum crate docs](https://docs.rs/axum/latest/axum/) 11. [Axum SSE](https://docs.rs/axum/latest/axum/response/sse/) 12. [阿里云 OSS 服务端签名表单上传](https://help.aliyun.com/zh/oss/user-guide/form-upload) 13. [阿里云 OSS STS 临时授权访问](https://help.aliyun.com/zh/oss/developer-reference/authorized-access-1) 14. [阿里云 OSS PutObject](https://help.aliyun.com/zh/oss/developer-reference/putobject) 15. [阿里云 OSS PostObject](https://help.aliyun.com/zh/oss/developer-reference/postobject) ## 18. 一句话结论 这次重写最正确的落点不是“把 Express 改成 Axum”,而是: **让 Axum 成为唯一外部副作用和 HTTP 边界,让 SpacetimeDB 成为唯一状态机真相源,让 OSS 成为唯一资产对象仓,从而在不丢当前 96 条能力面的前提下,把项目升级成真正可持续扩展的 Rust 后端。**