Close DDD refactor and remove generated asset proxy

This commit is contained in:
kdletters
2026-05-02 00:27:22 +08:00
parent fd08262bf0
commit 9d9913095d
605 changed files with 11811 additions and 10106 deletions

View File

@@ -2,6 +2,8 @@
更新时间:`2026-04-26` 更新时间:`2026-04-26`
> 2026-05-01 更新:正式平台入口已切到后端真相源。`startLocalPuzzleRun`、`swapLocalPuzzlePieces`、`dragLocalPuzzlePiece` 和 `advanceLocalPuzzleNextLevel` 只代表早期 V1/调试直达页背景,不再作为平台内 Puzzle 运行态主链。
## 1. 本次目标 ## 1. 本次目标
玩家每完成拼图运行时的一关后,立即弹出独立结算弹窗。弹窗需要显示: 玩家每完成拼图运行时的一关后,立即弹出独立结算弹窗。弹窗需要显示:

View File

@@ -4,13 +4,21 @@
## 文档列表 ## 文档列表
- [SERVER_RS_DDD_WP_DEL_CLEANUP_2026-05-01.md](./SERVER_RS_DDD_WP_DEL_CLEANUP_2026-05-01.md):记录 `WP-DEL 删除旧层与命名收口`,物理删除旧 runtime story HTTP DTO、前端 `Rpg*` alias、旧 `/api/custom-world/*` 非 runtime 前缀、Puzzle `local-next-level` 入口和 `/generated-*` 资产直读代理;生成资产读取统一走 OSS read-url 链路。
- [SERVER_RS_DDD_WP_API_BFF_CLOSURE_2026-05-01.md](./SERVER_RS_DDD_WP_API_BFF_CLOSURE_2026-05-01.md):记录 `WP-API api-server BFF` 收尾,补齐 `/api/llm/chat/completions``stream=true` SSE 代理,明确手机号/微信配置门控和角色动画资产占位不阻塞本次 BFF 关闭。
- [SERVER_RS_DDD_WP_AS_ASSET_CHAIN_CLOSURE_2026-05-01.md](./SERVER_RS_DDD_WP_AS_ASSET_CHAIN_CLOSURE_2026-05-01.md):记录 `WP-AS Assets` 资产主链收尾,补齐资产领域事件、`asset_event` event table、OSS 确认、API facade、Rust bindings、表目录和 migration 白名单。
- [SERVER_RS_DDD_WP_CW_FULL_CHAIN_CLOSURE_2026-05-01.md](./SERVER_RS_DDD_WP_CW_FULL_CHAIN_CLOSURE_2026-05-01.md):记录 `WP-CW Custom World` 当前主链收尾,补齐 runtime Custom World 场景图入口、RPG 创作资产 client 主路径、DashScope 缺配置测试口径和旧 `/api/custom-world/*` 兼容入口边界。
- [SERVER_RS_DDD_WP_ST_CLOSURE_2026-05-01.md](./SERVER_RS_DDD_WP_ST_CLOSURE_2026-05-01.md):记录 `WP-ST SpacetimeDB Adapter` 当前稳定范围收尾,补齐 `asset_event`、表目录、migration 白名单、Rust bindings 和 Windows bindings 生成 fallback。
- [SERVER_RS_DDD_WP_RPG_GAMEPLAY_CLOSURE_2026-05-01.md](./SERVER_RS_DDD_WP_RPG_GAMEPLAY_CLOSURE_2026-05-01.md):记录 `WP-RPG Gameplay 域` 全域收口,将战斗胜利、任务交付和宝箱奖励的跨域结算计划下沉到 `module-story`
- [SERVER_RS_DDD_WP_PZ_RUNTIME_BACKEND_TRUTH_CLOSURE_2026-05-01.md](./SERVER_RS_DDD_WP_PZ_RUNTIME_BACKEND_TRUTH_CLOSURE_2026-05-01.md):记录 `WP-PZ Puzzle` 正式平台运行态从前端本地裁决切到后端真相源,补齐 owner draft 预览 run、交换/拖动/下一关/排行榜后端调用和无 schema 变更口径。
- [SERVER_RS_DDD_WP_RS_RUNTIME_STORY_CLOSURE_2026-05-01.md](./SERVER_RS_DDD_WP_RS_RUNTIME_STORY_CLOSURE_2026-05-01.md):记录 `WP-RS Runtime Story` 写链路收尾,补齐 `/api/story/sessions/runtime``/api/story/sessions/{storySessionId}/actions/resolve`,统一返回 `StoryRuntimeMutationResponse.projection`,并保持旧 `/api/runtime/story/*` 未挂载。
- [SERVER_RS_DDD_WP_CW_ACTION_AND_DOMAIN_SPLIT_2026-04-30.md](./SERVER_RS_DDD_WP_CW_ACTION_AND_DOMAIN_SPLIT_2026-04-30.md):记录 `WP-CW Custom World` 的领域拆分与 Agent action 收口,将 `module-custom-world``lib.rs` 拆入 DDD 骨架,并移除 Custom World 运行代码中的最小兼容占位动作。 - [SERVER_RS_DDD_WP_CW_ACTION_AND_DOMAIN_SPLIT_2026-04-30.md](./SERVER_RS_DDD_WP_CW_ACTION_AND_DOMAIN_SPLIT_2026-04-30.md):记录 `WP-CW Custom World` 的领域拆分与 Agent action 收口,将 `module-custom-world``lib.rs` 拆入 DDD 骨架,并移除 Custom World 运行代码中的最小兼容占位动作。
- [SERVER_RS_DDD_WP_BF_AND_G2_DRIFT_CLEANUP_2026-04-30.md](./SERVER_RS_DDD_WP_BF_AND_G2_DRIFT_CLEANUP_2026-04-30.md):记录 `WP-BF Big Fish` 物理拆分漂移和 G2 迁移期口径清理,将 Big Fish 创作域类型、命令、应用规则和错误层拆入 DDD 文件,并清理剩余 `过渡落位` 注释。 - [SERVER_RS_DDD_WP_BF_AND_G2_DRIFT_CLEANUP_2026-04-30.md](./SERVER_RS_DDD_WP_BF_AND_G2_DRIFT_CLEANUP_2026-04-30.md):记录 `WP-BF Big Fish` 物理拆分漂移和 G2 迁移期口径清理,将 Big Fish 创作域类型、命令、应用规则和错误层拆入 DDD 文件,并清理剩余 `过渡落位` 注释。
- [SERVER_RS_DDD_TESTS_SUPPORT_CRATE_CLOSURE_2026-04-30.md](./SERVER_RS_DDD_TESTS_SUPPORT_CRATE_CLOSURE_2026-04-30.md):记录 `tests-support` 从目录占位收口为 `server-rs` workspace 共享测试支撑 crate首版提供 Maincloud healthz 与 HTTP smoke 通用断言。 - [SERVER_RS_DDD_TESTS_SUPPORT_CRATE_CLOSURE_2026-04-30.md](./SERVER_RS_DDD_TESTS_SUPPORT_CRATE_CLOSURE_2026-04-30.md):记录 `tests-support` 从目录占位收口为 `server-rs` workspace 共享测试支撑 crate首版提供 Maincloud healthz 与 HTTP smoke 通用断言。
- [SERVER_RS_DDD_WP_BF_RUNTIME_BACKEND_TRUTH_2026-04-29.md](./SERVER_RS_DDD_WP_BF_RUNTIME_BACKEND_TRUTH_2026-04-29.md):记录 `WP-BF Big Fish` 运行态从前端本地规则切到 Rust 领域真相源、SpacetimeDB run 表、API facade 和前端新接口接入的关闭口径。 - [SERVER_RS_DDD_WP_BF_RUNTIME_BACKEND_TRUTH_2026-04-29.md](./SERVER_RS_DDD_WP_BF_RUNTIME_BACKEND_TRUTH_2026-04-29.md):记录 `WP-BF Big Fish` 运行态从前端本地规则切到 Rust 领域真相源、SpacetimeDB run 表、API facade 和前端新接口接入的关闭口径。
- [SERVER_RS_DDD_WP_PF_PLATFORM_ERROR_CLASSIFICATION_2026-04-29.md](./SERVER_RS_DDD_WP_PF_PLATFORM_ERROR_CLASSIFICATION_2026-04-29.md):记录 `WP-PF platform side effects` 平台副作用收口,统一 LLM、OSS、SMS、微信平台错误分类与 API 映射,并将微信 OAuth provider 下沉到 `platform-auth` - [SERVER_RS_DDD_WP_PF_PLATFORM_ERROR_CLASSIFICATION_2026-04-29.md](./SERVER_RS_DDD_WP_PF_PLATFORM_ERROR_CLASSIFICATION_2026-04-29.md):记录 `WP-PF platform side effects` 平台副作用收口,统一 LLM、OSS、SMS、微信平台错误分类与 API 映射,并将微信 OAuth provider 下沉到 `platform-auth`
- [SERVER_RS_DDD_WP_RT_ADAPTER_API_CLOSURE_2026-04-29.md](./SERVER_RS_DDD_WP_RT_ADAPTER_API_CLOSURE_2026-04-29.md):记录 `WP-RT Runtime/Profile/Save` Adapter/API 收口,将 checkpoint、profile/save archive meta、充值/邀请/兑换/钱包等剩余纯规则迁入 `module-runtime`,移除 `/api/runtime/profile/*` 旧兼容挂载并对齐前端 `/api/profile/*` 请求路径。 - [SERVER_RS_DDD_WP_RT_ADAPTER_API_CLOSURE_2026-04-29.md](./SERVER_RS_DDD_WP_RT_ADAPTER_API_CLOSURE_2026-04-29.md):记录 `WP-RT Runtime/Profile/Save` Adapter/API 收口,将 checkpoint、profile/save archive meta、充值/邀请/兑换/钱包等剩余纯规则迁入 `module-runtime`,移除 `/api/runtime/profile/*` 旧兼容挂载并对齐前端 `/api/profile/*` 请求路径。
- [SERVER_RS_DDD_WP_SC_SPACETIME_CLIENT_REFACTOR_2026-04-29.md](./SERVER_RS_DDD_WP_SC_SPACETIME_CLIENT_REFACTOR_2026-04-29.md)冻结 `WP-SC Spacetime Client` 本次基础设施重构边界,明确只收口 `spacetime-client` 的 typed facade、错误映射row snapshot mapper,不预判尚未由 `WP-ST` 稳定的表、reducer、procedure 或 row shape - [SERVER_RS_DDD_WP_SC_SPACETIME_CLIENT_REFACTOR_2026-04-29.md](./SERVER_RS_DDD_WP_SC_SPACETIME_CLIENT_REFACTOR_2026-04-29.md)记录 `WP-SC Spacetime Client` 在当前稳定 facade 范围内的关闭口径,收口 `spacetime-client` 的 typed facade、错误映射row snapshot mapper、story runtime inventory source 接线和 README 状态;后续只随 `WP-ST` 新 facade 稳定后增量接线
- [SERVER_RS_DDD_WP_RT_APPLICATION_RECORD_REFACTOR_2026-04-29.md](./SERVER_RS_DDD_WP_RT_APPLICATION_RECORD_REFACTOR_2026-04-29.md):记录 `WP-RT Runtime/Profile/Save` 的应用记录投影拆分切片,将 settings、browse history、profile/save 等 `build_runtime_*_record` 迁入 `module-runtime/src/application.rs`,不改回包字段语义。 - [SERVER_RS_DDD_WP_RT_APPLICATION_RECORD_REFACTOR_2026-04-29.md](./SERVER_RS_DDD_WP_RT_APPLICATION_RECORD_REFACTOR_2026-04-29.md):记录 `WP-RT Runtime/Profile/Save` 的应用记录投影拆分切片,将 settings、browse history、profile/save 等 `build_runtime_*_record` 迁入 `module-runtime/src/application.rs`,不改回包字段语义。
- [SERVER_RS_DDD_WP_RT_COMMANDS_REFACTOR_2026-04-29.md](./SERVER_RS_DDD_WP_RT_COMMANDS_REFACTOR_2026-04-29.md):记录 `WP-RT Runtime/Profile/Save` 的命令构造拆分切片,将 settings、browse history、profile/save 等 `build_runtime_*_input` 和写入归一化函数迁入 `module-runtime/src/commands.rs`,不改校验语义。 - [SERVER_RS_DDD_WP_RT_COMMANDS_REFACTOR_2026-04-29.md](./SERVER_RS_DDD_WP_RT_COMMANDS_REFACTOR_2026-04-29.md):记录 `WP-RT Runtime/Profile/Save` 的命令构造拆分切片,将 settings、browse history、profile/save 等 `build_runtime_*_input` 和写入归一化函数迁入 `module-runtime/src/commands.rs`,不改校验语义。
- [SERVER_RS_DDD_WP_AI_INTERNAL_MODULE_SPLIT_2026-04-29.md](./SERVER_RS_DDD_WP_AI_INTERNAL_MODULE_SPLIT_2026-04-29.md):记录 `WP-AI AI Task``module-ai` 内部子模块拆分,将 domain、commands、application 与行为测试继续拆到职责更细的子文件,同时保持 `module_ai::*` 公开导出、SpacetimeDB schema、BFF route 和前端契约不变。 - [SERVER_RS_DDD_WP_AI_INTERNAL_MODULE_SPLIT_2026-04-29.md](./SERVER_RS_DDD_WP_AI_INTERNAL_MODULE_SPLIT_2026-04-29.md):记录 `WP-AI AI Task``module-ai` 内部子模块拆分,将 domain、commands、application 与行为测试继续拆到职责更细的子文件,同时保持 `module_ai::*` 公开导出、SpacetimeDB schema、BFF route 和前端契约不变。
@@ -24,16 +32,16 @@
- [SERVER_RS_DDD_WP_PZ_DOMAIN_ENUM_REHOME_2026-04-29.md](./SERVER_RS_DDD_WP_PZ_DOMAIN_ENUM_REHOME_2026-04-29.md):记录 `WP-PZ Puzzle` 基础领域常量与枚举归位切片,将 Puzzle Agent、发布状态、运行态状态、ID 前缀、标签数量和洗牌次数口径迁入 `module-puzzle/src/domain.rs`,不改 SpacetimeDB、API 或前端行为。 - [SERVER_RS_DDD_WP_PZ_DOMAIN_ENUM_REHOME_2026-04-29.md](./SERVER_RS_DDD_WP_PZ_DOMAIN_ENUM_REHOME_2026-04-29.md):记录 `WP-PZ Puzzle` 基础领域常量与枚举归位切片,将 Puzzle Agent、发布状态、运行态状态、ID 前缀、标签数量和洗牌次数口径迁入 `module-puzzle/src/domain.rs`,不改 SpacetimeDB、API 或前端行为。
- [SERVER_RS_DDD_WP_RPG_COMBAT_DOMAIN_ENUM_REHOME_2026-04-29.md](./SERVER_RS_DDD_WP_RPG_COMBAT_DOMAIN_ENUM_REHOME_2026-04-29.md):记录 `WP-RPG Gameplay 域` 的 combat 基础领域常量与枚举归位切片,将战斗 ID 前缀、版本、伤害、切磋保底生命、旧攻击 function 列表和基础枚举迁入 `module-combat/src/domain.rs`,不改 SpacetimeDB、API 或前端行为。 - [SERVER_RS_DDD_WP_RPG_COMBAT_DOMAIN_ENUM_REHOME_2026-04-29.md](./SERVER_RS_DDD_WP_RPG_COMBAT_DOMAIN_ENUM_REHOME_2026-04-29.md):记录 `WP-RPG Gameplay 域` 的 combat 基础领域常量与枚举归位切片,将战斗 ID 前缀、版本、伤害、切磋保底生命、旧攻击 function 列表和基础枚举迁入 `module-combat/src/domain.rs`,不改 SpacetimeDB、API 或前端行为。
- [SERVER_RS_DDD_WP_ST_AUTH_ADAPTER_SPLIT_2026-04-29.md](./SERVER_RS_DDD_WP_ST_AUTH_ADAPTER_SPLIT_2026-04-29.md):记录 `WP-ST` Auth SpacetimeDB adapter 目录化切片将认证表、procedure 和快照 JSON mapper 拆入 `auth/` 子模块,不改 schema、procedure 签名或绑定形状。 - [SERVER_RS_DDD_WP_ST_AUTH_ADAPTER_SPLIT_2026-04-29.md](./SERVER_RS_DDD_WP_ST_AUTH_ADAPTER_SPLIT_2026-04-29.md):记录 `WP-ST` Auth SpacetimeDB adapter 目录化切片将认证表、procedure 和快照 JSON mapper 拆入 `auth/` 子模块,不改 schema、procedure 签名或绑定形状。
- [SERVER_RS_DDD_WP_RS_COMPAT_RESIDUE_AUDIT_2026-04-29.md](./SERVER_RS_DDD_WP_RS_COMPAT_RESIDUE_AUDIT_2026-04-29.md):记录 `WP-RS Runtime Story 去兼容层` 的 compat 残留审计切片,清理 `module-runtime-story` 运行代码注释口径,并冻结仍需等待新写接口的前端和 contract 残留 - [SERVER_RS_DDD_WP_RS_COMPAT_RESIDUE_AUDIT_2026-04-29.md](./SERVER_RS_DDD_WP_RS_COMPAT_RESIDUE_AUDIT_2026-04-29.md):记录 `WP-RS Runtime Story 去兼容层` 的 compat 残留审计切片2026-05-01 后运行写链路已迁到 story session scoped route旧展示 DTO 和历史命名统一留给 `WP-DEL` 清理
- [SERVER_RS_DDD_WP_AS_ASSET_OBJECT_TYPE_REHOME_2026-04-29.md](./SERVER_RS_DDD_WP_AS_ASSET_OBJECT_TYPE_REHOME_2026-04-29.md):记录 `WP-AS Assets` 资产对象类型归位切片,将领域快照、命令 DTO、应用返回 DTO 和字段错误拆入 `module-assets` 的 DDD 骨架文件,不改 SpacetimeDB、API、OSS 或前端行为。 - [SERVER_RS_DDD_WP_AS_ASSET_OBJECT_TYPE_REHOME_2026-04-29.md](./SERVER_RS_DDD_WP_AS_ASSET_OBJECT_TYPE_REHOME_2026-04-29.md):记录 `WP-AS Assets` 资产对象类型归位切片,将领域快照、命令 DTO、应用返回 DTO 和字段错误拆入 `module-assets` 的 DDD 骨架文件,不改 SpacetimeDB、API、OSS 或前端行为。
- [SERVER_RS_DDD_WP_RT_ERROR_LAYER_REFACTOR_2026-04-29.md](./SERVER_RS_DDD_WP_RT_ERROR_LAYER_REFACTOR_2026-04-29.md):记录 `WP-RT Runtime/Profile/Save` 的错误层拆分切片,将 settings、browse history、profile/save 三组字段错误和中文错误文案迁入 `module-runtime/src/errors.rs`,不改校验语义。 - [SERVER_RS_DDD_WP_RT_ERROR_LAYER_REFACTOR_2026-04-29.md](./SERVER_RS_DDD_WP_RT_ERROR_LAYER_REFACTOR_2026-04-29.md):记录 `WP-RT Runtime/Profile/Save` 的错误层拆分切片,将 settings、browse history、profile/save 三组字段错误和中文错误文案迁入 `module-runtime/src/errors.rs`,不改校验语义。
- [SERVER_RS_DDD_WP_RT_DOMAIN_SNAPSHOT_RECORD_REFACTOR_2026-04-29.md](./SERVER_RS_DDD_WP_RT_DOMAIN_SNAPSHOT_RECORD_REFACTOR_2026-04-29.md):记录 `WP-RT Runtime/Profile/Save` 的 snapshot、profile、wallet、played world 与 save archive 领域快照和记录类型拆分切片,只移动纯类型和枚举方法,不改 SpacetimeDB、API 或前端接线。 - [SERVER_RS_DDD_WP_RT_DOMAIN_SNAPSHOT_RECORD_REFACTOR_2026-04-29.md](./SERVER_RS_DDD_WP_RT_DOMAIN_SNAPSHOT_RECORD_REFACTOR_2026-04-29.md):记录 `WP-RT Runtime/Profile/Save` 的 snapshot、profile、wallet、played world 与 save archive 领域快照和记录类型拆分切片,只移动纯类型和枚举方法,不改 SpacetimeDB、API 或前端接线。
- [SERVER_RS_DDD_WP_RT_RUNTIME_SETTINGS_DOMAIN_REFACTOR_2026-04-29.md](./SERVER_RS_DDD_WP_RT_RUNTIME_SETTINGS_DOMAIN_REFACTOR_2026-04-29.md):记录 `WP-RT Runtime/Profile/Save` 的 runtime settings 领域值对象拆分切片,将默认设置、平台主题和值对象迁入 `module-runtime/src/domain.rs`,不改 SpacetimeDB、API 或前端接线。 - [SERVER_RS_DDD_WP_RT_RUNTIME_SETTINGS_DOMAIN_REFACTOR_2026-04-29.md](./SERVER_RS_DDD_WP_RT_RUNTIME_SETTINGS_DOMAIN_REFACTOR_2026-04-29.md):记录 `WP-RT Runtime/Profile/Save` 的 runtime settings 领域值对象拆分切片,将默认设置、平台主题和值对象迁入 `module-runtime/src/domain.rs`,不改 SpacetimeDB、API 或前端接线。
- [SERVER_RS_DDD_WP_A_AUTH_DOMAIN_VALUE_OBJECT_REFACTOR_2026-04-29.md](./SERVER_RS_DDD_WP_A_AUTH_DOMAIN_VALUE_OBJECT_REFACTOR_2026-04-29.md):记录 `WP-A Auth` DDD 分层收口,将账号、会话、验证码、微信 state/绑定规则、命令输入、应用返回、领域错误和领域事件归位到 `module-auth` 骨架,并核查 API、platform 与 SpacetimeDB adapter 边界。 - [SERVER_RS_DDD_WP_A_AUTH_DOMAIN_VALUE_OBJECT_REFACTOR_2026-04-29.md](./SERVER_RS_DDD_WP_A_AUTH_DOMAIN_VALUE_OBJECT_REFACTOR_2026-04-29.md):记录 `WP-A Auth` DDD 分层收口,将账号、会话、验证码、微信 state/绑定规则、命令输入、应用返回、领域错误和领域事件归位到 `module-auth` 骨架,并核查 API、platform 与 SpacetimeDB adapter 边界。
- [SERVER_RS_DDD_WP_ST_CUSTOM_WORLD_ROOT_SPLIT_2026-04-29.md](./SERVER_RS_DDD_WP_ST_CUSTOM_WORLD_ROOT_SPLIT_2026-04-29.md):记录 `WP-ST` Custom World SpacetimeDB adapter 从根入口迁入 `custom_world/mod.rs` 的边界、无 schema 变更口径和验收命令。 - [SERVER_RS_DDD_WP_ST_CUSTOM_WORLD_ROOT_SPLIT_2026-04-29.md](./SERVER_RS_DDD_WP_ST_CUSTOM_WORLD_ROOT_SPLIT_2026-04-29.md):记录 `WP-ST` Custom World SpacetimeDB adapter 从根入口迁入 `custom_world/mod.rs` 的边界、无 schema 变更口径和验收命令。
- [SERVER_RS_DDD_WP_FE_S_RPG_RUNTIME_STORY_CLIENT_MIGRATION_2026-04-29.md](./SERVER_RS_DDD_WP_FE_S_RPG_RUNTIME_STORY_CLIENT_MIGRATION_2026-04-29.md):记录 `WP-FE-S` RPG runtime story client 读取侧迁到 `storySessionId` scoped runtime projection补齐 story session 新主链 `begin/continue/state/projection` API client 的边界、旧写接口暂留原因和后续依赖 - [SERVER_RS_DDD_WP_FE_S_RPG_RUNTIME_STORY_CLIENT_MIGRATION_2026-04-29.md](./SERVER_RS_DDD_WP_FE_S_RPG_RUNTIME_STORY_CLIENT_MIGRATION_2026-04-29.md):记录 `WP-FE-S` RPG runtime story client 迁到 `storySessionId` scoped runtime projection在 2026-05-01 收尾切片中关闭旧 `/api/runtime/story/*` 开局与动作写侧调用
- [SERVER_RS_DDD_WP_FE_H_RPG_RUNTIME_STORY_HOOKS_PROJECTION_2026-04-29.md](./SERVER_RS_DDD_WP_FE_H_RPG_RUNTIME_STORY_HOOKS_PROJECTION_2026-04-29.md):记录 `WP-FE-H` RPG runtime story 读取侧 hooks 接线切片,将 option catalog继续游戏刷新显式接入 `getRpgStoryRuntimeProjection`,写接口和组件层仍等待后续收口 - [SERVER_RS_DDD_WP_FE_H_RPG_RUNTIME_STORY_HOOKS_PROJECTION_2026-04-29.md](./SERVER_RS_DDD_WP_FE_H_RPG_RUNTIME_STORY_HOOKS_PROJECTION_2026-04-29.md):记录 `WP-FE-H` RPG runtime story hooks 接线切片,将 option catalog继续游戏刷新与正式动作结算统一接入 story runtime projection client
- [SERVER_RS_DDD_WP_FE_C_RPG_RUNTIME_SHELL_TEST_FIXTURE_2026-04-29.md](./SERVER_RS_DDD_WP_FE_C_RPG_RUNTIME_SHELL_TEST_FIXTURE_2026-04-29.md):记录 `WP-FE-C` RPG runtime shell 组件测试夹具接线切片,将组件测试 mock 对齐当前 hooks 暴露的 UI 对象形状,不触碰未稳定写接口 - [SERVER_RS_DDD_WP_FE_C_RPG_RUNTIME_SHELL_TEST_FIXTURE_2026-04-29.md](./SERVER_RS_DDD_WP_FE_C_RPG_RUNTIME_SHELL_TEST_FIXTURE_2026-04-29.md):记录 `WP-FE-C` RPG runtime shell 组件测试夹具接线切片,将组件测试 mock 对齐当前 hooks 暴露的 UI 对象形状,并确认组件层不拼接 runtime story API 路径
- [SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_PROGRESS_2026-04-29.md](./SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_PROGRESS_2026-04-29.md):记录 `G1 契约与路由矩阵` 已完成的本地进度、验证结果、单 owner 边界和下一批并行任务入口。 - [SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_PROGRESS_2026-04-29.md](./SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_PROGRESS_2026-04-29.md):记录 `G1 契约与路由矩阵` 已完成的本地进度、验证结果、单 owner 边界和下一批并行任务入口。
- [SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md](./SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md):冻结 `server-rs` DDD G1 契约与路由矩阵,明确新旧 HTTP 路由去留、DTO 删除/保留/重命名、页面到 query/result DTO 映射、breaking change、API 错误 envelope 和共享契约单 owner 边界。 - [SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md](./SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md):冻结 `server-rs` DDD G1 契约与路由矩阵,明确新旧 HTTP 路由去留、DTO 删除/保留/重命名、页面到 query/result DTO 映射、breaking change、API 错误 envelope 和共享契约单 owner 边界。
- [SERVER_RS_DDD_WP_API_BFF_START_2026-04-29.md](./SERVER_RS_DDD_WP_API_BFF_START_2026-04-29.md):记录 `WP-API api-server BFF` 启动切片,先收口旧 runtime story 兼容路由挂载、错误 envelope 回归和后续依赖,不越过 `spacetime-client` 接线边界。 - [SERVER_RS_DDD_WP_API_BFF_START_2026-04-29.md](./SERVER_RS_DDD_WP_API_BFF_START_2026-04-29.md):记录 `WP-API api-server BFF` 启动切片,先收口旧 runtime story 兼容路由挂载、错误 envelope 回归和后续依赖,不越过 `spacetime-client` 接线边界。
@@ -96,7 +104,7 @@
- [JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md](./JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md):冻结 Jenkins `构建 / 部署 / 构建并部署` 三条流水线的职责、版本号传递、上游触发门禁、本地目录部署脚本与 `/home/ubuntu/Genarrative-deploy/` 覆盖策略。 - [JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md](./JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md):冻结 Jenkins `构建 / 部署 / 构建并部署` 三条流水线的职责、版本号传递、上游触发门禁、本地目录部署脚本与 `/home/ubuntu/Genarrative-deploy/` 覆盖策略。
- [JENKINS_DEPLOY_ENV_BOM_FIX_2026-04-25.md](./JENKINS_DEPLOY_ENV_BOM_FIX_2026-04-25.md):记录 Jenkins 部署时 `.env.local` 首行 UTF-8 BOM 导致 `start.sh` 加载失败的根因,并冻结发布包构建、部署脚本和启动脚本的环境文件净化规则。 - [JENKINS_DEPLOY_ENV_BOM_FIX_2026-04-25.md](./JENKINS_DEPLOY_ENV_BOM_FIX_2026-04-25.md):记录 Jenkins 部署时 `.env.local` 首行 UTF-8 BOM 导致 `start.sh` 加载失败的根因,并冻结发布包构建、部署脚本和启动脚本的环境文件净化规则。
- [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、启动停止脚本、默认 scp 上传和安全清库开关。 - [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、启动停止脚本、默认 scp 上传和安全清库开关。
- [RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md](./RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md):记录当前 Rust `api-server` 已挂载的 101 条 Axum 路由,并补充管理后台入口与管理接口索引,按 auth、assets、runtime、custom world、story、generated path 等挂载面归类,用于对照 Node 能力基线与切流 smoke 清单。 - [RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md](./RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md):记录当前 Rust `api-server` 已挂载路由,并补充管理后台入口与管理接口索引,按 auth、assets、runtime、custom world、story 等挂载面归类,用于对照 Node 能力基线与切流 smoke 清单`/generated-*` 直读代理已下线
- [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 存储边界和文档维护门禁。 - [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 增量解析、错误模型与重试边界。 - [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` 的配置、状态注入与首版非流式兼容边界。 - [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` 的配置、状态注入与首版非流式兼容边界。

View File

@@ -148,12 +148,7 @@
### 3.10 Legacy Generated 路径 ### 3.10 Legacy Generated 路径
1. `GET /generated-character-drafts/{*path}` `/generated-*` 直读代理已下线。生成资产读取统一走 `GET /api/assets/read-url` 或 asset object projection`/generated-*` 字符串仅作为 `legacyPublicPath` / OSS object key 兼容标识保留。
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.11 Health ### 3.11 Health

View File

@@ -33,7 +33,7 @@ G1 单 owner 文件范围:
| 鉴权公开查询 | `GET /api/auth/login-options``GET /api/auth/public-users/by-code/{code}``GET /api/auth/public-users/by-id/{user_id}` | 保留 | `AuthLoginOptionsResponse``PublicUserSearchResponse` | WP-A | | 鉴权公开查询 | `GET /api/auth/login-options``GET /api/auth/public-users/by-code/{code}``GET /api/auth/public-users/by-id/{user_id}` | 保留 | `AuthLoginOptionsResponse``PublicUserSearchResponse` | WP-A |
| 鉴权会话 | `GET /api/auth/me``GET /api/auth/sessions``POST /api/auth/refresh``POST /api/auth/logout``POST /api/auth/logout-all` | 保留 | `AuthMeResponse``AuthSessionsResponse``RefreshSessionResponse``LogoutResponse``LogoutAllResponse` | WP-A | | 鉴权会话 | `GET /api/auth/me``GET /api/auth/sessions``POST /api/auth/refresh``POST /api/auth/logout``POST /api/auth/logout-all` | 保留 | `AuthMeResponse``AuthSessionsResponse``RefreshSessionResponse``LogoutResponse``LogoutAllResponse` | WP-A |
| 鉴权登录 | `POST /api/auth/phone/send-code``POST /api/auth/phone/login``GET /api/auth/wechat/start``GET /api/auth/wechat/callback``POST /api/auth/wechat/bind-phone``POST /api/auth/entry``POST /api/auth/password/change``POST /api/auth/password/reset` | 保留 | TS 命名统一使用 `Auth*` 前缀Rust 命名维持领域语义 | WP-A | | 鉴权登录 | `POST /api/auth/phone/send-code``POST /api/auth/phone/login``GET /api/auth/wechat/start``GET /api/auth/wechat/callback``POST /api/auth/wechat/bind-phone``POST /api/auth/entry``POST /api/auth/password/change``POST /api/auth/password/reset` | 保留 | TS 命名统一使用 `Auth*` 前缀Rust 命名维持领域语义 | WP-A |
| 旧本地生成资产代理 | `GET /generated-character-drafts/{*path}``/generated-characters/{*path}``/generated-animations/{*path}``/generated-big-fish-assets/{*path}``/generated-puzzle-assets/{*path}``/generated-custom-world-scenes/{*path}``/generated-custom-world-covers/{*path}``/generated-qwen-sprites/{*path}` | 删除 | 正式读取统一改为 `GET /api/assets/read-url` 或 asset object projection本地生成路径只允许迁移窗口内存在 | WP-AS、WP-FE、WP-DEL | | 旧本地生成资产代理 | `GET /generated-character-drafts/{*path}``/generated-characters/{*path}``/generated-animations/{*path}``/generated-big-fish-assets/{*path}``/generated-puzzle-assets/{*path}``/generated-custom-world-scenes/{*path}``/generated-custom-world-covers/{*path}``/generated-qwen-sprites/{*path}` | 删除 | 正式读取统一 `GET /api/assets/read-url` 或 asset object projection`/generated-*` 仅允许作为 legacyPublicPath/object key 标识,不再作为可裸读路由 | WP-AS、WP-FE、WP-DEL |
| LLM 代理 | `POST /api/llm/chat/completions` | 收敛 | 仅作为平台能力代理;玩法 prompt 不允许由前端直接传入 | WP-PF、WP-API | | LLM 代理 | `POST /api/llm/chat/completions` | 收敛 | 仅作为平台能力代理;玩法 prompt 不允许由前端直接传入 | WP-PF、WP-API |
| Runtime chat | `POST /api/runtime/chat/character/suggestions``/summary``/reply/stream``/npc/dialogue/stream``/npc/turn/stream``/npc/recruit/stream` | 重命名 | 收敛到 session scoped story/chat 命令;请求体不得携带前端拼装的世界真相 | WP-RS、WP-RPG、WP-FE | | Runtime chat | `POST /api/runtime/chat/character/suggestions``/summary``/reply/stream``/npc/dialogue/stream``/npc/turn/stream``/npc/recruit/stream` | 重命名 | 收敛到 session scoped story/chat 命令;请求体不得携带前端拼装的世界真相 | WP-RS、WP-RPG、WP-FE |
| 文档输入 | `POST /api/runtime/creation-agent/document-inputs/parse` | 保留 | `ParseCreationAgentDocumentInputRequest/Response` | WP-CW、WP-BF、WP-PZ | | 文档输入 | `POST /api/runtime/creation-agent/document-inputs/parse` | 保留 | `ParseCreationAgentDocumentInputRequest/Response` | WP-CW、WP-BF、WP-PZ |
@@ -44,8 +44,8 @@ G1 单 owner 文件范围:
| RPG 作品库 | `GET /api/runtime/custom-world-library``GET/PUT/DELETE /api/runtime/custom-world-library/{profile_id}``POST /publish``POST /unpublish``GET /api/runtime/custom-world-gallery``GET /api/runtime/custom-world-gallery/{owner_user_id}/{profile_id}``GET /api/runtime/custom-world-gallery/by-code/{code}` | 收敛 | 命名后续改为 RPG creation/work route family删除 `custom-world` 旧泛名歧义 | WP-CW、WP-FE | | RPG 作品库 | `GET /api/runtime/custom-world-library``GET/PUT/DELETE /api/runtime/custom-world-library/{profile_id}``POST /publish``POST /unpublish``GET /api/runtime/custom-world-gallery``GET /api/runtime/custom-world-gallery/{owner_user_id}/{profile_id}``GET /api/runtime/custom-world-gallery/by-code/{code}` | 收敛 | 命名后续改为 RPG creation/work route family删除 `custom-world` 旧泛名歧义 | WP-CW、WP-FE |
| RPG Agent | `POST /api/runtime/custom-world/agent/sessions``GET/DELETE /sessions/{session_id}``GET /result-view``GET /works``GET /cards/{card_id}``POST /messages``POST /messages/stream``POST /actions``GET /operations/{operation_id}` | 收敛 | DTO 重命名为 `RpgAgent*`Rust 当前 `CustomWorldAgent*` 后续物理重命名 | WP-CW、WP-FE、WP-DEL | | RPG Agent | `POST /api/runtime/custom-world/agent/sessions``GET/DELETE /sessions/{session_id}``GET /result-view``GET /works``GET /cards/{card_id}``POST /messages``POST /messages/stream``POST /actions``GET /operations/{operation_id}` | 收敛 | DTO 重命名为 `RpgAgent*`Rust 当前 `CustomWorldAgent*` 后续物理重命名 | WP-CW、WP-FE、WP-DEL |
| Big Fish Agent/Works/Runtime | `POST /api/runtime/big-fish/agent/sessions``GET /sessions/{session_id}``POST /messages``POST /messages/stream``POST /actions``GET /works``DELETE /works/{session_id}``GET /gallery``POST /sessions/{session_id}/play``POST /works/{session_id}/play``POST /sessions/{session_id}/runs``GET /runs/{run_id}``POST /runs/{run_id}/input` | 保留 | `BigFish*` DTO运行态正式使用 `BigFishRunResponse``SubmitBigFishInputRequest``sessions/{id}/play``works/{id}/play` 后续二选一保留 | WP-BF | | Big Fish Agent/Works/Runtime | `POST /api/runtime/big-fish/agent/sessions``GET /sessions/{session_id}``POST /messages``POST /messages/stream``POST /actions``GET /works``DELETE /works/{session_id}``GET /gallery``POST /sessions/{session_id}/play``POST /works/{session_id}/play``POST /sessions/{session_id}/runs``GET /runs/{run_id}``POST /runs/{run_id}/input` | 保留 | `BigFish*` DTO运行态正式使用 `BigFishRunResponse``SubmitBigFishInputRequest``sessions/{id}/play``works/{id}/play` 后续二选一保留 | WP-BF |
| Puzzle Agent/Works/Runtime | `POST /api/runtime/puzzle/agent/sessions``GET /sessions/{session_id}``POST /messages``POST /messages/stream``POST /actions``GET /works``GET/PUT/DELETE /works/{profile_id}``GET /gallery``GET /gallery/{profile_id}``POST /runs``POST /runs/local-next-level``GET /runs/{run_id}``POST /runs/{run_id}/swap``POST /runs/{run_id}/drag``POST /runs/{run_id}/next-level``POST /runs/{run_id}/leaderboard` | 保留 | `PuzzleAgent*``PuzzleWork*``PuzzleRun*` DTO | WP-PZ | | Puzzle Agent/Works/Runtime | `POST /api/runtime/puzzle/agent/sessions``GET /sessions/{session_id}``POST /messages``POST /messages/stream``POST /actions``GET /works``GET/PUT/DELETE /works/{profile_id}``GET /gallery``GET /gallery/{profile_id}``POST /runs``GET /runs/{run_id}``POST /runs/{run_id}/swap``POST /runs/{run_id}/drag``POST /runs/{run_id}/next-level``POST /runs/{run_id}/leaderboard`;旧 `POST /runs/local-next-level` 已取消挂载 | 保留 | `PuzzleAgent*``PuzzleWork*``PuzzleRun*` DTO;旧 `AdvanceLocalPuzzleNextLevelRequest` 已删除 | WP-PZ、WP-DEL |
| RPG profile/asset generation | `POST /api/runtime/custom-world/profile``POST /api/custom-world/entity``POST /api/runtime/custom-world/entity``POST /api/custom-world/scene-npc``POST /api/runtime/custom-world/scene-npc``POST /api/custom-world/scene-image``POST /api/custom-world/cover-image``POST /api/runtime/custom-world/cover-image``POST /api/custom-world/cover-upload``POST /api/runtime/custom-world/cover-upload` | 重命名 | 去掉非 runtime 前缀旧入口;统一到 RPG creation asset/profile route family | WP-CW、WP-AS、WP-DEL | | RPG profile/asset generation | `POST /api/runtime/custom-world/profile``POST /api/runtime/custom-world/entity``POST /api/runtime/custom-world/scene-npc``POST /api/runtime/custom-world/scene-image``POST /api/runtime/custom-world/cover-image``POST /api/runtime/custom-world/cover-upload`;旧 `/api/custom-world/*` 非 runtime 前缀已取消挂载 | 重命名 | 去掉非 runtime 前缀旧入口;统一到 RPG creation asset/profile route family | WP-CW、WP-AS、WP-DEL |
| Profile | `GET/POST/DELETE /api/profile/browse-history``GET /api/profile/dashboard``GET /api/profile/wallet-ledger``GET /api/profile/recharge-center``POST /api/profile/recharge/orders``GET /api/profile/referrals/invite-center``POST /api/profile/referrals/redeem-code``POST /api/profile/redeem-codes/redeem``GET /api/profile/play-stats``GET /api/profile/save-archives``POST /api/profile/save-archives/{world_key}`;旧 `GET/POST/DELETE /api/runtime/profile/*` 已取消挂载 | 重命名 | 保留 `/api/profile/*` 主链,删除 `/api/runtime/profile/*` 旧兼容入口 | WP-RT、WP-FE、WP-DEL | | Profile | `GET/POST/DELETE /api/profile/browse-history``GET /api/profile/dashboard``GET /api/profile/wallet-ledger``GET /api/profile/recharge-center``POST /api/profile/recharge/orders``GET /api/profile/referrals/invite-center``POST /api/profile/referrals/redeem-code``POST /api/profile/redeem-codes/redeem``GET /api/profile/play-stats``GET /api/profile/save-archives``POST /api/profile/save-archives/{world_key}`;旧 `GET/POST/DELETE /api/runtime/profile/*` 已取消挂载 | 重命名 | 保留 `/api/profile/*` 主链,删除 `/api/runtime/profile/*` 旧兼容入口 | WP-RT、WP-FE、WP-DEL |
| Runtime inventory | `GET /api/runtime/sessions/{runtime_session_id}/inventory` | 保留 | `RuntimeInventoryStateResponse` | WP-RPG、WP-RT | | Runtime inventory | `GET /api/runtime/sessions/{runtime_session_id}/inventory` | 保留 | `RuntimeInventoryStateResponse` | WP-RPG、WP-RT |
| Runtime story 旧层 | `POST /api/runtime/story/sessions``POST /api/runtime/story/state/resolve``GET /api/runtime/story/state/{session_id}``POST /api/runtime/story/actions/resolve``POST /api/runtime/story/initial``POST /api/runtime/story/continue` | 已删除 | 已从 `api-server` 取消挂载并删除 `api-server/src/runtime_story*` 兼容实现;后续前端迁移到 `GET/POST /api/story/*` 和 session scoped story/chat facade | WP-RS、WP-FE、WP-DEL | | Runtime story 旧层 | `POST /api/runtime/story/sessions``POST /api/runtime/story/state/resolve``GET /api/runtime/story/state/{session_id}``POST /api/runtime/story/actions/resolve``POST /api/runtime/story/initial``POST /api/runtime/story/continue` | 已删除 | 已从 `api-server` 取消挂载并删除 `api-server/src/runtime_story*` 兼容实现;后续前端迁移到 `GET/POST /api/story/*` 和 session scoped story/chat facade | WP-RS、WP-FE、WP-DEL |
@@ -89,10 +89,10 @@ G1 单 owner 文件范围:
| DTO/文件 | 删除条件 | 替代 | | DTO/文件 | 删除条件 | 替代 |
| --- | --- | --- | | --- | --- | --- |
| `LegacyApiErrorResponse` | 全部路由完成 envelope 归一后 | `ApiErrorEnvelope` | | `LegacyApiErrorResponse` | 全部路由完成 envelope 归一后 | `ApiErrorEnvelope` |
| Rust `RuntimeStoryStateResolveRequest` | 前端切到 `GET /api/story/sessions/{story_session_id}/state` | `StorySessionStateResponse` 加拆分投影 | | Rust `RuntimeStoryStateResolveRequest` | 已在 WP-DEL 删除 | `StorySessionStateResponse` 与 story runtime projection |
| Rust/TS `RuntimeStoryBootstrapRequest/Response` | `POST /api/runtime/story/initial` 删除 | `BeginStorySessionRequest``StorySessionMutationResponse` | | Rust/TS `RuntimeStoryBootstrapRequest/Response` | 已在 WP-DEL 删除 | `BeginStoryRuntimeSessionRequest``StoryRuntimeMutationResponse` |
| Rust/TS `RuntimeStoryAiRequest/Response` | `POST /api/runtime/story/continue` 删除后 | `ContinueStoryRequest``StorySessionMutationResponse` | | Rust/TS `RuntimeStoryAiRequest/Response` | `POST /api/runtime/story/continue` 删除后 | `ContinueStoryRequest``StorySessionMutationResponse` |
| Rust/TS `RuntimeStoryActionRequest/Response` 旧总入口形态 | `POST /api/runtime/story/actions/resolve` 删除后 | story/battle/npc/inventory 分命令 result DTO | | Rust/TS `RuntimeStoryActionResponse` 旧总入口形态 | 已在 WP-DEL 删除 | `StoryRuntimeMutationResponse.projection` story/battle/npc/inventory 分投影 |
| TS `StoryRequestPayload``PlainTextPromptRequest``PlainTextResponse` | runtime chat 不再由前端传 prompt 后 | 后端 session scoped chat/story command | | TS `StoryRequestPayload``PlainTextPromptRequest``PlainTextResponse` | runtime chat 不再由前端传 prompt 后 | 后端 session scoped chat/story command |
| TS `CreateCustomWorldSessionRequest``AnswerCustomWorldSessionQuestionRequest``CustomWorldSessionRecord` 等旧问答生成 DTO | 确认无前端运行引用后 | RPG Agent session DTO | | TS `CreateCustomWorldSessionRequest``AnswerCustomWorldSessionQuestionRequest``CustomWorldSessionRecord` 等旧问答生成 DTO | 确认无前端运行引用后 | RPG Agent session DTO |
| `/api/runtime/profile/*` 旧兼容 DTO 别名 | 前端全量迁到 `/api/profile/*` 后 | Runtime profile DTO | | `/api/runtime/profile/*` 旧兼容 DTO 别名 | 前端全量迁到 `/api/profile/*` 后 | Runtime profile DTO |
@@ -117,7 +117,7 @@ G1 单 owner 文件范围:
| Big Fish 广场/作品 | bearer token 或公开 gallery query | `RecordBigFishPlayRequest` | `BigFishWorksResponse``BigFishGalleryResponse``BigFishSessionResponse` | | Big Fish 广场/作品 | bearer token 或公开 gallery query | `RecordBigFishPlayRequest` | `BigFishWorksResponse``BigFishGalleryResponse``BigFishSessionResponse` |
| Puzzle Agent | route param `session_id` | `CreatePuzzleAgentSessionRequest``SendPuzzleAgentMessageRequest``ExecutePuzzleAgentActionRequest` | `PuzzleAgentSessionResponse``PuzzleAgentActionResponse` | | Puzzle Agent | route param `session_id` | `CreatePuzzleAgentSessionRequest``SendPuzzleAgentMessageRequest``ExecutePuzzleAgentActionRequest` | `PuzzleAgentSessionResponse``PuzzleAgentActionResponse` |
| Puzzle 作品/广场 | route param `profile_id` | `PutPuzzleWorkRequest` | `PuzzleWorksResponse``PuzzleWorkDetailResponse``PuzzleGalleryResponse``PuzzleGalleryDetailResponse` | | Puzzle 作品/广场 | route param `profile_id` | `PutPuzzleWorkRequest` | `PuzzleWorksResponse``PuzzleWorkDetailResponse``PuzzleGalleryResponse``PuzzleGalleryDetailResponse` |
| Puzzle 运行态 | route param `run_id` | `StartPuzzleRunRequest``AdvanceLocalPuzzleNextLevelRequest``SwapPuzzlePiecesRequest``DragPuzzlePieceRequest``SubmitPuzzleLeaderboardRequest` | `PuzzleRunResponse` | | Puzzle 运行态 | route param `run_id` | `StartPuzzleRunRequest``SwapPuzzlePiecesRequest``DragPuzzlePieceRequest``SubmitPuzzleLeaderboardRequest` | `PuzzleRunResponse` |
| Runtime 设置与存档 | bearer token | `PutRuntimeSettingsRequest``PutSavedGameSnapshotRequest``PutRuntimeSaveCheckpointRequest` | `RuntimeSettingsResponse``SavedGameSnapshotResponse``BasicOkResponse` | | Runtime 设置与存档 | bearer token | `PutRuntimeSettingsRequest``PutSavedGameSnapshotRequest``PutRuntimeSaveCheckpointRequest` | `RuntimeSettingsResponse``SavedGameSnapshotResponse``BasicOkResponse` |
| 个人中心 | bearer token | `CreateProfileRechargeOrderRequest``RedeemProfileReferralInviteCodeRequest``RedeemProfileRewardCodeRequest``PlatformBrowseHistoryUpsertRequest` | `ProfileDashboardSummaryResponse``ProfileWalletLedgerResponse``ProfileRechargeCenterResponse``ProfileReferralInviteCenterResponse``ProfilePlayStatsResponse``ProfileSaveArchiveListResponse``PlatformBrowseHistoryResponse` | | 个人中心 | bearer token | `CreateProfileRechargeOrderRequest``RedeemProfileReferralInviteCodeRequest``RedeemProfileRewardCodeRequest``PlatformBrowseHistoryUpsertRequest` | `ProfileDashboardSummaryResponse``ProfileWalletLedgerResponse``ProfileRechargeCenterResponse``ProfileReferralInviteCenterResponse``ProfilePlayStatsResponse``ProfileSaveArchiveListResponse``PlatformBrowseHistoryResponse` |
| RPG Story 运行态 | route param `story_session_id``battle_state_id` | `BeginStorySessionRequest``ContinueStoryRequest``CreateStoryBattleRequest``CreateStoryNpcBattleRequest``ResolveStoryBattleRequest` | `StorySessionMutationResponse``StorySessionStateResponse``StoryRuntimeProjectionResponse``StoryBattleStateResponse``ResolveStoryBattleResponse``RuntimeInventoryStateResponse` | | RPG Story 运行态 | route param `story_session_id``battle_state_id` | `BeginStorySessionRequest``ContinueStoryRequest``CreateStoryBattleRequest``CreateStoryNpcBattleRequest``ResolveStoryBattleRequest` | `StorySessionMutationResponse``StorySessionStateResponse``StoryRuntimeProjectionResponse``StoryBattleStateResponse``ResolveStoryBattleResponse``RuntimeInventoryStateResponse` |
@@ -125,12 +125,12 @@ G1 单 owner 文件范围:
## 5. Breaking change 清单 ## 5. Breaking change 清单
1. 删除兼容层是本轮默认策略。旧 `/api/runtime/story/*``/_internal/auth/*``/generated-*``/api/runtime/profile/*` 兼容入口在对应前端迁移完成后物理删除。 1. 删除兼容层是本轮默认策略。旧 `/api/runtime/story/*``/api/custom-world/*` 非 runtime 前缀、`/api/runtime/puzzle/runs/local-next-level``/api/runtime/profile/*` 兼容入口`/generated-*` 资产直读代理均在对应前端迁移完成后物理删除。
2. Runtime story/chat 不再接受前端拼装的 `worldType``character``monsters``history``context`、prompt 文本作为正式真相。正式命令必须以 `runtimeSessionId``storySessionId``battleStateId` 等后端 session id 为索引。 2. Runtime story/chat 不再接受前端拼装的 `worldType``character``monsters``history``context`、prompt 文本作为正式真相。正式命令必须以 `runtimeSessionId``storySessionId``battleStateId` 等后端 session id 为索引。
3. `CustomWorld*` 作为 RPG 主链命名将被重命名为 `RpgCreation*``RpgAgent*`。前端可同步修改,不保留旧命名适配层。 3. `CustomWorld*` 作为 RPG 主链命名将被重命名为 `RpgCreation*``RpgAgent*`。前端可同步修改,不保留旧命名适配层。
4. `/api/custom-world/*` 非 runtime 前缀旧入口删除,统一进入 RPG creation route family 或 asset route family。 4. `/api/custom-world/*` 非 runtime 前缀旧入口删除,统一进入 RPG creation route family 或 asset route family。
5. `/api/runtime/profile/*` 兼容入口删除,统一使用 `/api/profile/*` 5. `/api/runtime/profile/*` 兼容入口删除,统一使用 `/api/profile/*`
6. 资产读取不再依赖 `/generated-*` 静态代理作为正式 contract统一走 asset object、read url 或后端投影里的正式 URL 字段。 6. 资产读取不再依赖 `/generated-*` 静态代理作为正式 contract统一走 asset object、read url 或后端投影里的正式 URL 字段`/generated-*` 只保留为 legacyPublicPath/object key 兼容标识
7. LLM 代理不得作为玩法 prompt 透传入口。玩法 prompt 由 `api-server`/`platform-llm` 内部编排,前端只提交用户动作和展示态输入。 7. LLM 代理不得作为玩法 prompt 透传入口。玩法 prompt 由 `api-server`/`platform-llm` 内部编排,前端只提交用户动作和展示态输入。
8. API 错误体统一为 `ApiErrorEnvelope`。旧 `{ error, meta }` 只允许在已列入删除计划的旧接口中短期存在。 8. API 错误体统一为 `ApiErrorEnvelope`。旧 `{ error, meta }` 只允许在已列入删除计划的旧接口中短期存在。
@@ -175,4 +175,4 @@ G1 单 owner 文件范围:
2. `WP-ST` 负责 SpacetimeDB 表、reducer/procedure 和 `migration.rs`,不得由玩法领域任务直接抢改。 2. `WP-ST` 负责 SpacetimeDB 表、reducer/procedure 和 `migration.rs`,不得由玩法领域任务直接抢改。
3. `WP-API` 负责 `api-server/src/app.rs` 和 route 挂载入口,领域任务只提供应用结果和错误模型。 3. `WP-API` 负责 `api-server/src/app.rs` 和 route 挂载入口,领域任务只提供应用结果和错误模型。
4. `WP-FE` 在后端新接口稳定后删除旧前端兼容层,不新增对旧 route 的二次适配。 4. `WP-FE` 在后端新接口稳定后删除旧前端兼容层,不新增对旧 route 的二次适配。
5. `WP-DEL` 只能在搜索确认无运行引用后删除旧 DTO、旧 route 和旧静态代理。 5. `WP-DEL` 只能在搜索确认无运行引用后删除旧 DTO、旧 route 和旧静态代理`/generated-*` 直读代理已移除后,后续不得重新引入裸读转发

View File

@@ -158,23 +158,23 @@ LLM、OSS、SMS、微信等外部副作用可以独立准备不等待 `WP-SC`
| G1 契约与路由矩阵 | 已完成;本轮共享契约与路由矩阵持续巡检已关闭,已同步 profile、Big Fish run、story projection/battle 和文档输入契约索引漂移 | 未认领 | G0 后 | `shared-contracts``packages/shared/src/contracts/*`、API 路由索引 | 领域实现 | DTO 分组、breaking change 清单、前后端路由矩阵 | shared contract 测试通过 | | G1 契约与路由矩阵 | 已完成;本轮共享契约与路由矩阵持续巡检已关闭,已同步 profile、Big Fish run、story projection/battle 和文档输入契约索引漂移 | 未认领 | G0 后 | `shared-contracts``packages/shared/src/contracts/*`、API 路由索引 | 领域实现 | DTO 分组、breaking change 清单、前后端路由矩阵 | shared contract 测试通过 |
| G2 DDD 骨架与边界检查 | 已完成并持续执行;本轮 G2 骨架巡检已关闭15 个 module crate 均通过边界门禁,后续每批继续跑门禁 | 未认领 | G0 后 | `module-*` 骨架、`scripts/check-server-rs-ddd-boundaries.mjs` | 业务重写 | 所有 `module-*` 具备 `domain/commands/application/events/errors`,检查脚本覆盖禁用依赖 | `npm.cmd run check:server-rs-ddd` | | G2 DDD 骨架与边界检查 | 已完成并持续执行;本轮 G2 骨架巡检已关闭15 个 module crate 均通过边界门禁,后续每批继续跑门禁 | 未认领 | G0 后 | `module-*` 骨架、`scripts/check-server-rs-ddd-boundaries.mjs` | 业务重写 | 所有 `module-*` 具备 `domain/commands/application/events/errors`,检查脚本覆盖禁用依赖 | `npm.cmd run check:server-rs-ddd` |
| WP-A Auth | 已完成module-auth 已完成 domain/commands/application/errors/events 分层归位,账号、会话、验证码、微信 state/绑定规则由 module-auth 承接api-server/platform-auth/spacetime-module 边界已核查 | 未认领 | G1 后 | `module-auth``spacetime-module/src/auth*``api-server/src/auth*``platform-auth` | 其他玩法域 | 账号、会话、验证码、微信绑定领域化;真实短信/微信在 platform | `cargo test -p module-auth`auth API 测试 | | WP-A Auth | 已完成module-auth 已完成 domain/commands/application/errors/events 分层归位,账号、会话、验证码、微信 state/绑定规则由 module-auth 承接api-server/platform-auth/spacetime-module 边界已核查 | 未认领 | G1 后 | `module-auth``spacetime-module/src/auth*``api-server/src/auth*``platform-auth` | 其他玩法域 | 账号、会话、验证码、微信绑定领域化;真实短信/微信在 platform | `cargo test -p module-auth`auth API 测试 |
| WP-AS Assets | 进行中;资产对象类型归位、资产领域测试和 SpacetimeDB row mapper 切片已完成,资产 API/OSS/facade 尚未全链收口 | 未认领 | G1 后 | `module-assets``spacetime-module/src/asset_metadata/*`、资产 API、OSS adapter | 玩法业务规则 | 资产对象与绑定规则纯化OSS head/upload 移出领域核心 | `cargo test -p module-assets`资产 facade 测试 | | WP-AS Assets | 已完成;资产对象确认、实体槽位绑定、历史读取、OSS 对象确认、API facade、SpacetimeDB adapter`asset_event` 和 Rust bindings 已闭环 | 未认领 | 后续只随 `asset_job/asset_manifest`、专业资产生成或 `WP-DEL/WP-V` 增量维护 | `module-assets``spacetime-module/src/asset_metadata/*`、资产 API、OSS adapter`spacetime-client` assets facade | 玩法业务规则 | 资产对象与绑定规则纯化OSS head/upload 移出领域核心,事件表只承接订阅与审计事实 | `cargo test -p module-assets``cargo check -p spacetime-module``cargo check -p spacetime-client` |
| WP-AI AI Task | 已完成领域层、SpacetimeDB AI adapter/event、spacetime-client facade、BFF 路由和定向测试已闭环,`module-ai` 内部子模块拆分已完成,真实 LLM/SSE/前端消费归后续 WP-PF/WP-API/WP-FE 承接 | 未认领 | G1 后 | `module-ai``spacetime-module/src/ai/*`、AI task API | LLM prompt 业务规则 | AI task/stage/chunk/result 状态机领域化 | `cargo test -p module-ai`AI task reducer/procedure smoke | | WP-AI AI Task | 已完成领域层、SpacetimeDB AI adapter/event、spacetime-client facade、BFF 路由和定向测试已闭环,`module-ai` 内部子模块拆分已完成,真实 LLM/SSE/前端消费归后续 WP-PF/WP-API/WP-FE 承接 | 未认领 | G1 后 | `module-ai``spacetime-module/src/ai/*`、AI task API | LLM prompt 业务规则 | AI task/stage/chunk/result 状态机领域化 | `cargo test -p module-ai`AI task reducer/procedure smoke |
| WP-CW Custom World | 进行中基础领域枚举、DDD 物理拆分Agent action 最小兼容占位已关闭profile/agent/draft/gallery/publish gate 全链仍待继续接 API/前端和资产对象链 | 未认领 | G1 后 | `module-custom-world``spacetime-module/src/custom_world/*``api-server` custom world 路由、前端创作 client | Big Fish/Puzzle | profile、agent session、draft card、gallery、publish gate 领域化LLM 留在 API/platform | `cargo test -p module-custom-world`custom world 定向测试 | | WP-CW Custom World | 已完成基础领域枚举、DDD 物理拆分Agent action 确定性编排、稳定 SpacetimeDB facade、runtime API 路由和 RPG 创作资产 client 主路径已收口,旧 `/api/custom-world/*` 非 runtime 前缀已由 WP-DEL 取消挂载 | 未认领 | 后续只随 `WP-AS/WP-PF/WP-V` 增量维护 | `module-custom-world``spacetime-module/src/custom_world/*``api-server` custom world 路由、前端创作 client | Big Fish/Puzzle | profile、agent session、draft card、gallery、publish gate 领域化LLM 和图片生成留在 API/platform | `cargo test -p module-custom-world``cargo test -p api-server custom_world`,前端资产 client 测试 |
| WP-BF Big Fish | 已完成;运行态真相源和 DDD 物理拆分均已收口,并完成 SpacetimeDB run 表、spacetime-client facade、API 路由、前端 client 接入和本地运行态删除 | 未认领 | G1 后 | `module-big-fish``spacetime-module/src/big_fish/*`、Big Fish API、Big Fish 前端 client | Puzzle/RPG | 会话、草稿、素材槽、运行态纯规则;草稿校验下沉 | `cargo test -p module-big-fish`Big Fish API 测试 | | WP-BF Big Fish | 已完成;运行态真相源和 DDD 物理拆分均已收口,并完成 SpacetimeDB run 表、spacetime-client facade、API 路由、前端 client 接入和本地运行态删除 | 未认领 | G1 后 | `module-big-fish``spacetime-module/src/big_fish/*`、Big Fish API、Big Fish 前端 client | Puzzle/RPG | 会话、草稿、素材槽、运行态纯规则;草稿校验下沉 | `cargo test -p module-big-fish`Big Fish API 测试 |
| WP-PZ Puzzle | 进行中;领域类型与规则拆分切片已关闭Puzzle API/前端消费仍未全链完成 | 未认领 | G1 后 | `module-puzzle``spacetime-module/src/puzzle*`、Puzzle API、Puzzle 前端 client | Big Fish/RPG | Agent session、work profile、runtime run、排行榜规则领域化 | `cargo test -p module-puzzle`Puzzle 定向测试 | | WP-PZ Puzzle | 已完成;领域类型与规则拆分、正式平台运行态后端真相源、owner draft 预览 run、Puzzle API 和前端正式消费链路已收口,旧 `local-next-level` 兼容入口已由 WP-DEL 取消挂载 | 未认领 | 后续只随 `WP-V` 增量维护 | `module-puzzle``spacetime-module/src/puzzle*`、Puzzle API、Puzzle 前端 client | Big Fish/RPG | Agent session、work profile、runtime run、排行榜规则领域化 | `cargo test -p module-puzzle`Puzzle 定向测试 |
| WP-RT Runtime/Profile/Save | 已完成runtime settings、snapshot/profile/save archive 类型、错误层、命令构造、应用记录投影、Adapter/API/Frontend profile 路径和剩余纯规则均已收口 | 未认领 | G1 后 | `module-runtime``spacetime-module/src/runtime/*`、runtime/save/profile API | RPG story 规则 | runtime setting、snapshot、wallet、played world、save archive 领域化 | `cargo test -p module-runtime`runtime API 测试 | | WP-RT Runtime/Profile/Save | 已完成runtime settings、snapshot/profile/save archive 类型、错误层、命令构造、应用记录投影、Adapter/API/Frontend profile 路径和剩余纯规则均已收口 | 未认领 | G1 后 | `module-runtime``spacetime-module/src/runtime/*`、runtime/save/profile API | RPG story 规则 | runtime setting、snapshot、wallet、played world、save archive 领域化 | `cargo test -p module-runtime`runtime API 测试 |
| WP-RPG Gameplay 域 | 进行中combat/inventory/npc/progression/quest/runtime-item/story 的 DDD 物理拆分漂移已关闭,完整 story action 写侧与跨域组合结算仍待 `WP-RS/WP-ST/WP-SC/WP-API/WP-FE` 主链收口 | 未认领 | G1 后 | `module-combat``module-inventory``module-npc``module-progression``module-quest``module-runtime-item``module-story` | 创作域 | 战斗、背包、NPC、成长、任务、宝箱、story session 纯规则与跨域事件 | 各 module 测试;跨域应用结果测试 | | WP-RPG Gameplay 域 | 已完成combat/inventory/npc/progression/quest/runtime-item/story 的 DDD 物理拆分、跨域结算计划和 SpacetimeDB adapter 消费计划均已收口,完整 runtime story 写侧入口继续归 `WP-RS/WP-API/WP-FE` | 未认领 | 后续只随 `WP-RS/WP-DEL/WP-V` 增量维护 | `module-combat``module-inventory``module-npc``module-progression``module-quest``module-runtime-item``module-story` | 创作域 | 战斗、背包、NPC、成长、任务、宝箱、story session 纯规则与跨域事件 | `cargo test -p module-combat -p module-inventory -p module-npc -p module-progression -p module-quest -p module-runtime-item -p module-story` |
| WP-RS Runtime Story 去兼容层 | 进行中compat 残留审计和 `module-runtime-story` 顶层 DDD 物理拆分漂移已关闭,旧前端写 client 与旧 contract 残留已冻结到后续 WP-FE-S/WP-DEL | 未认领 | G1 后 | `module-runtime-story``api-server/src/runtime_story/*``src/hooks/rpg-runtime-story/*` | 非 RPG 创作域 | 先将历史 `module-runtime-story-compat` 迁为新主链 crate再删除 HTTP compat 层、接 session scoped 新接口、前端匹配新接口 | `cargo test -p module-runtime-story`runtime story/API/前端定向测试 | | WP-RS Runtime Story 去兼容层 | 已完成session scoped 开局与动作写入口已切到 `/api/story/sessions/runtime``/api/story/sessions/{storySessionId}/actions/resolve`,返回 projection 并保持旧 `/api/runtime/story/*` 未挂载 | 未认领 | 后续只随 WP-DEL/WP-V 增量维护 | `module-runtime-story``api-server/src/story_sessions.rs`、story runtime contract | 非 RPG 创作域 | 旧 HTTP compat 层不恢复运行时快照、story event 和 runtime projection 由 server-rs 主链闭合 | `cargo test -p module-runtime-story``cargo test -p api-server story_sessions`,前端 runtime story 定向测试 |
| WP-ST SpacetimeDB Adapter | 进行中;已完成 AI task event、Big Fish readiness event、Asset row mapper、Puzzle publish event、Gameplay/Custom World 根入口瘦身、Custom World Agent action 确定性编排和 Auth adapter 目录化切片,其他上下文和绑定生成仍待推进 | 未认领 | 领域任务输出稳定后 | `spacetime-module/src/**``migration.rs`、表目录 | `api-server` 业务逻辑 | table/reducer/procedure/mapper/queries 按上下文接入领域函数;必要 event/projection table`lib.rs/migration.rs/表目录` 单 owner 合流;已完成 AI task event、Big Fish readiness event、Asset row mapper、Puzzle publish event、Gameplay 根入口瘦身、Custom World 根入口瘦身、Auth adapter 目录化 | `cargo check -p spacetime-module`需要时 `spacetime build/generate` | | WP-ST SpacetimeDB Adapter | 已完成;当前稳定 adapter 范围已完成 table/reducer/procedure、event table、表目录、migration 白名单、Rust bindings 和 DDD 门禁闭环,后续仅随新增 SpacetimeDB facade 增量维护 | 未认领 | 后续只随新增 table/reducer/procedure 或 row shape 增量维护 | `spacetime-module/src/**``migration.rs`、表目录、bindings 生成脚本 | `api-server` 业务逻辑 | table/reducer/procedure/mapper/queries 按上下文接入领域函数;必要 event/projection table`lib.rs/migration.rs/表目录` 单 owner 合流;已完成 AI task event、Big Fish readiness event、Asset row mapper、Asset event、Puzzle publish event、Gameplay/Custom World 根入口瘦身、Custom World Agent action 确定性编排、Auth adapter 目录化和 bindings 生成闭环 | `cargo check -p spacetime-module``npm.cmd run spacetime:generate -- --rust-only``npm.cmd run check:server-rs-ddd` |
| WP-SC Spacetime Client | 进行中story runtime projection inventory source 接线切片已关闭,读取投影已纳入稳定 runtime inventory facade后续仅随 WP-ST 新 facade 稳定后继续 typed facade/row mapper | 未认领 | 对应 WP-ST facade 稳定后 | `spacetime-client/src/**`、绑定 mapper | 领域规则、未稳定 facade 的预判接线 | typed facade、错误映射、row snapshot mapper | `cargo check -p spacetime-client` | | WP-SC Spacetime Client | 已完成;当前稳定 facade 范围内 typed facade、错误 helper、row snapshot mapper、story runtime inventory source 接线和 README 状态已收尾,后续仅随 WP-ST 新 facade 另开增量切片 | 未认领 | 对应 WP-ST facade 稳定后增量维护 | `spacetime-client/src/**`、绑定 mapper | 领域规则、未稳定 facade 的预判接线 | typed facade、错误映射、row snapshot mapper | `cargo test -p spacetime-client``cargo check -p spacetime-client` |
| WP-PF platform side effects | 已完成LLM/OSS/SMS/微信平台副作用错误分类与 API 接线错误模型已统一,微信 OAuth provider 已下沉到 `platform-auth` | 未认领 | G1 后可独立准备;接入 API 前与 WP-API 对齐错误模型 | `platform-*``api-server` platform 接线 | 领域状态机 | LLM、OSS、SMS、微信等副作用统一 adapter | platform crate 测试或 API smoke | | WP-PF platform side effects | 已完成LLM/OSS/SMS/微信平台副作用错误分类与 API 接线错误模型已统一,微信 OAuth provider 已下沉到 `platform-auth` | 未认领 | G1 后可独立准备;接入 API 前与 WP-API 对齐错误模型 | `platform-*``api-server` platform 接线 | 领域状态机 | LLM、OSS、SMS、微信等副作用统一 adapter | platform crate 测试或 API smoke |
| WP-API api-server BFF | 进行中story/game facade 当前阶段收口切片已关闭,runtime projection、story battle DTOowner guard 已推进,更多 session scoped 写接口依赖 WP-ST/WP-SC | 未认领 | WP-SC facade 和 WP-PF 接口稳定后 | `api-server/src/**`,其中 `app.rs` 单 owner | SpacetimeDB table 定义、领域主规则、绕过 spacetime-client 的直连实现 | 路由、鉴权、SSE、请求响应映射、平台编排收口 | `cargo test -p api-server``cargo check -p api-server` | | WP-API api-server BFF | 已完成story/game facaderuntime projection、story battle DTOowner guard、Custom World runtime 资产入口、平台错误映射和 `/api/llm/chat/completions` 流式 SSE 代理均已收口,后续新 session scoped 写接口`WP-RS/WP-ST/WP-SC` 增量接入 | 未认领 | WP-SC facade 和 WP-PF 接口稳定后 | `api-server/src/**`,其中 `app.rs` 单 owner | SpacetimeDB table 定义、领域主规则、绕过 spacetime-client 的直连实现 | 路由、鉴权、SSE、请求响应映射、平台编排收口 | `cargo test -p api-server``cargo check -p api-server` |
| WP-FE-S Frontend API client 迁移 | 进行中story session API client 收口切片已关闭,已补齐 begin/continue/state/projection 新主链请求封装与定向测试;开局快照和完整动作结算仍等待后端新写接口 | 未认领 | G1 和 WP-API 契约稳定后 | `src/services/**`、必要 contract type import | hooks / components 大改 | API client、路径常量、请求体与响应解析对齐新 contract | `npm.cmd run test -- src/services` | | WP-FE-S Frontend API client 迁移 | 已完成runtime story service 写侧已切到新 story session scoped mutation response前端从 projection 水合 snapshot不再消费旧 `snapshot/viewModel/presentation/patches` 组合 | 未认领 | 后续只随 WP-DEL/WP-V 增量维护 | `src/services/rpg-runtime/**``packages/shared/src/contracts/story.ts` | hooks / components 大改 | API client、路径常量、请求体与响应解析对齐新 contract | `npm.cmd run test -- src/services/rpg-runtime/rpgRuntimeStoryClient.test.ts` |
| WP-FE-H Frontend hooks 迁移 | 进行中RPG runtime story 读取侧 hooks 接线切片已关闭,完整开局/动作写接口 hooks 迁移与组件接线仍等待后端新接口 | 未认领 | WP-FE-S 完成且后端接口可用后 | `src/hooks/**`、必要 hook 测试 | components 大面积 UI 改版 | hooks 改为调用新 client只保留 loading/error/transition 和 UI 临时态 | `npm.cmd run test -- src/hooks` | | WP-FE-H Frontend hooks 迁移 | 已完成;runtime story hooks 写侧和读取侧均通过 `storySessionId` scoped client背包/NPC/战斗表现只消费 service 返回的投影快照与可选 presentation | 未认领 | WP-FE-S 完成后已关闭 | `src/hooks/rpg-runtime-story/**`、必要 hook 测试 | components 大面积 UI 改版 | hooks 改为调用新 client只保留 loading/error/transition 和 UI 临时态 | `npm.cmd run test -- src/hooks/rpg-runtime-story/runtimeStoryCoordinator.test.ts` |
| WP-FE-C Frontend components 接线 | 进行中RPG runtime shell 组件测试夹具接线切片已关闭,组件测试 mock 已对齐当前 hooks UI 出口;完整组件迁移仍等待后端写接口和 hooks 写侧稳定 | 未认领 | WP-FE-H 完成后 | `src/components/**`、组件测试 | services / hooks contract 改动 | 组件接入新 hooks 和 DTO不新增规则说明文案 | 相关组件 vitest / 交互测试 | | WP-FE-C Frontend components 接线 | 已完成RPG runtime shell 组件继续只消费 hooks 出口,不拼接任何 runtime story API 路径,组件测试夹具对齐新 hooks 结果形状 | 未认领 | WP-FE-H 完成后已关闭 | `src/components/rpg-runtime-shell/**`、组件测试 | services / hooks contract 改动 | 组件接入新 hooks 和 DTO不新增规则说明文案 | `npm.cmd run test -- src/components/rpg-runtime-shell/RpgRuntimeShell.test.tsx` |
| WP-DEL 删除旧层与命名收口 | 暂不可执行;需等待后端新接口、前端迁移和旧引用替换完成 | 未认领 | 新接口与前端迁移后 | 旧 compat、旧 facade、旧 contract、旧测试 | 新主链 | 物理删除旧入口、旧命名、旧 fixture 中非必要样本 | 搜索无运行代码引用旧层 | | WP-DEL 删除旧层与命名收口 | 已完成;旧 runtime story HTTP DTO、前端 `Rpg*` runtime story alias、旧 `/api/custom-world/*` 非 runtime 前缀、Puzzle `local-next-level` 入口和 `/generated-*` 资产直读代理已物理删除 | 未认领 | 新接口与前端迁移后 | 旧 compat、旧 facade、旧 contract、旧测试 | 新主链 | 物理删除旧入口、旧命名、旧 fixture 中非必要样本 | 搜索无运行代码引用旧层 |
| WP-V 全链验证与发布 smoke | 暂不可执行;必须在 WP-DEL 后串行执行,当前仍处第 1/2/3 批交错推进 | 未认领 | 最后 | 文档、测试脚本、README | 新功能扩展 | 全链命令、Maincloud smoke、文档交接 | 第 8 节命令通过或记录非本轮阻塞 | | WP-V 全链验证与发布 smoke | 已完成;WP-DEL 后串行执行 SpacetimeDB bindings 生成、Rust/TS/encoding/DDD 全链验证和 Maincloud healthz smoke | 未认领 | 已关闭 | 文档、测试脚本、README | 新功能扩展 | 全链命令、Maincloud smoke、文档交接 | 命令通过Maincloud 启动恢复快照出现 503 警告但 `/healthz` 200smoke 后已清理进程和 3100 端口 |
## 4.1 未完整收口内容整合清单 ## 4.1 未完整收口内容整合清单
@@ -186,21 +186,19 @@ LLM、OSS、SMS、微信等外部副作用可以独立准备不等待 `WP-SC`
| 归属工作包 | 未完整收口项 | 当前证据 | 收口边界 | 依赖与优先级 | | 归属工作包 | 未完整收口项 | 当前证据 | 收口边界 | 依赖与优先级 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| `WP-RPG Gameplay 域` | 成长系统归属 RPG 域;`module-progression` 真实首版与 DDD 物理拆分已收口,完整 RPG 成长闭环仍需继续接跨域链路 | `server-rs/crates/module-progression/README.md` 已说明 DDD 物理拆分完成;`src/domain.rs``commands.rs``application.rs``events.rs``errors.rs` 已承载真实类型、命令、应用规则、事件和错误;`cargo test -p module-progression` 通过 | 已关闭本项“分层文件仍是过渡壳”的漂移;继续保持在 `WP-RPG` 内推进,不拆成通用 Runtime 能力章节蓝图输入、章节预算审计、quest/combat/npc 联动事件继续跟随跨域主链 | 已关闭拆分漂移;完整成长闭环仍是 P0依赖 `module-custom-world` 章节蓝图 Rust 化边界稳定,随后接 `WP-ST/WP-SC/WP-API` | | `WP-RPG Gameplay 域` | RPG 七个领域 crate 与跨域组合结算已收口 | `module-combat``module-inventory``module-npc``module-progression``module-quest``module-runtime-item``module-story` 均已完成 DDD 文件承载;`module-story` 新增 `RpgGameplaySettlementPlan`,战斗胜利、任务交付和宝箱奖励的背包、成长、章节账本计划已进入纯领域层;`spacetime-module/src/gameplay/mod.rs` 改为消费计划,`cargo test` 覆盖 RPG 七个领域 crate`cargo check -p spacetime-module` 通过 | 已关闭“完整跨域组合结算仍排队”的漂移;完整 runtime story 写侧入口、旧 contract 删除和前端写侧迁移继续归 `WP-RS/WP-FE/WP-DEL`,不再阻塞 `WP-RPG` | 已关闭;后续只随 `WP-RS/WP-DEL/WP-V` 增量维护 |
| `WP-RPG Gameplay 域` | `module-story` README 与 DDD 物理拆分漂移已关闭;后续只剩完整 story action 写侧与跨域事件继续排队 | `server-rs/crates/module-story/README.md` 已改为当前真实边界;`src/domain.rs``commands.rs``application.rs``events.rs``errors.rs` 已承载真实类型、命令、应用映射、事件和错误;`cargo test -p module-story` 通过 | 保持 `module-story` 作为 story session 纯领域薄层;不恢复旧 `/api/runtime/story/*` compat route完整动作结算继续跟随 `WP-RS/WP-ST/WP-SC/WP-API/WP-FE` | 已关闭本项漂移;剩余写侧依赖仍在 P0 主链 | | `WP-RS Runtime Story 去兼容层` | `module-runtime-story` 顶层 DDD 物理拆分和 session scoped 写入口已关闭 | `module-runtime-story/src/domain.rs``commands.rs``application.rs``events.rs``errors.rs` 已承载顶层真实类型`api-server/src/story_sessions.rs` 已提供 `/api/story/sessions/runtime``/api/story/sessions/{storySessionId}/actions/resolve`;写入口返回 `StoryRuntimeMutationResponse.projection`,旧 `/api/runtime/story/*` 仍未挂载 | 已关闭“DDD 文件仍是过渡壳”和“旧写接口未完整替代”的漂移;旧 alias、旧展示 DTO 和历史文档清理由 `WP-DEL` 统一处理 | 已关闭;后续只随 `WP-DEL/WP-V` 增量维护 |
| `WP-RPG Gameplay 域` | `module-combat``module-inventory``module-npc``module-quest``module-runtime-item` 的 DDD 物理拆分漂移已关闭;完整跨域组合结算仍排队 | 上述 crate 的 `domain/commands/application/events/errors` 已承载真实类型、命令、应用规则、事件和错误RPG 六个子域源码不再命中 `过渡落位``cargo test -p module-combat -p module-inventory -p module-npc -p module-progression -p module-quest -p module-runtime-item` 通过 | 已关闭本项“分层文件仍是过渡壳”的漂移;跨域副作用继续只输出领域事件,不在单个 crate 内互相直连;完整 story action 写侧与组合结算跟随后续主链 | 已关闭拆分漂移;完整跨域写侧仍是 P0依赖 `WP-RS/WP-ST/WP-SC/WP-API/WP-FE` | | `WP-CW Custom World` | Custom World agent 最小兼容占位、确定性动作编排、runtime API 主入口和 RPG 创作资产 client 主路径已关闭 | `spacetime-module/src/custom_world/mod.rs` 已移除 `execute_placeholder_custom_world_action``generate_characters``generate_landmarks``generate_role_assets``sync_role_assets``generate_scene_assets``sync_scene_assets``expand_long_tail` 已分别写回 draft profile、draft card、asset coverage、publish gate 或 long-tail 状态;`api-server/src/app.rs` 已补齐 `/api/runtime/custom-world/scene-image``src/services/rpg-creation/rpgCreationAssetClient.ts` 已切到 `/api/runtime/custom-world/*``rpgCreationAssetClient.test.ts` 覆盖三类资产入口 | 已关闭“最小兼容占位”和“前端继续调用旧资产入口”的漂移LLM、图片生成、OSS、资产对象确认仍只通过 `WP-API/WP-PF/WP-AS` 编排,不放进 reducer/procedure | 已关闭;后续外部副作用能力和资产对象深水区只随 `WP-AS/WP-PF/WP-V` 增量维护 |
| `WP-RS Runtime Story 去兼容层` | `module-runtime-story` 顶层 DDD 物理拆分漂移已关闭,但旧写接口仍未完整替代 | `module-runtime-story/src/domain.rs``commands.rs``application.rs``events.rs``errors.rs` 已承载顶层真实类型、命令 helper、应用 helper、事件和错误`cargo test -p module-runtime-story` 通过;旧前端写 client 与 `RuntimeStoryActionResponse` 等残留仍等待 `WP-FE-S/WP-DEL` | 已关闭本项“DDD 文件仍是过渡壳”的漂移;继续补齐 session scoped 开局、动作结算、inventory action、NPC interaction、forge/battle/quest 写接口;前端只接新 `/api/story/*` 与新 contract | 已关闭拆分漂移;旧写接口替换仍是 P0依赖 `WP-ST/WP-SC` facade完成后解锁 `WP-FE-S/WP-FE-H/WP-FE-C/WP-DEL` | | `WP-CW Custom World` | `module-custom-world` DDD 物理拆分、profile/agent/draft/gallery/publish gate 当前稳定 facade/API 主链已收口 | `module-custom-world/src/lib.rs` 已收口为模块声明、公开导出和测试;`domain/commands/application/events/errors` 已承载真实类型、命令、规则、事件和错误;`spacetime-client/src/custom_world.rs` 已具备 profile、gallery、agent session、message、action、operation、card detail、works facadeCustom World runtime route 已覆盖 profile/library/gallery/agent/works/entity/scene-npc/scene-image/cover-image/cover-upload源码不再命中 `过渡落位` | 已关闭“分层文件仍是过渡壳”和“稳定 facade 未接 API/前端主入口”的漂移;旧 `/api/custom-world/*` 非 runtime 前缀已由 WP-DEL 取消挂载,不再作为 RPG 创作主 client 路径 | 已关闭;后续随 `WP-V` 验证,不阻塞 `WP-CW` 完成 |
| `WP-CW Custom World` | Custom World agent 最小兼容占位已关闭,真实动作改为 SpacetimeDB 内确定性状态编排;完整外部副作用链仍待 `WP-AS/WP-PF/WP-API` | `spacetime-module/src/custom_world/mod.rs` 已移除 `execute_placeholder_custom_world_action``generate_characters``generate_landmarks``generate_role_assets``sync_role_assets``generate_scene_assets``sync_scene_assets``expand_long_tail` 已分别写回 draft profile、draft card、asset coverage、publish gate 或 long-tail 状态;`cargo check -p spacetime-module` 通过 | 已关闭“最小兼容占位”漂移LLM、图片生成、OSS、资产对象确认仍只通过 `WP-API/WP-PF/WP-AS` 编排,不放进 reducer/procedure | 已关闭 P0 占位项;外部副作用和资产对象全链仍随 `WP-AS/WP-PF/WP-API` 继续 |
| `WP-CW Custom World` | `module-custom-world` DDD 物理拆分已收口profile/agent/draft/gallery/publish gate 的 API/前端全链仍待后续推进 | `module-custom-world/src/lib.rs` 已收口为模块声明、公开导出和测试;`domain/commands/application/events/errors` 已承载真实类型、命令、规则、事件和错误;`module-custom-world/README.md` 已更新当前边界;源码不再命中 `过渡落位``cargo test -p module-custom-world` 通过 | 已关闭“分层文件仍是过渡壳”的漂移;继续保持 SpacetimeDB 表和 procedure 由 `WP-ST` 落地API/前端只消费稳定 facade | 已关闭拆分漂移;完整创作全链仍是 P0/P1 主链,依赖 `WP-ST/WP-SC/WP-API/WP-FE` |
| `WP-BF Big Fish` | `module-big-fish` 已完成 DDD 物理拆分和迁移期口径清理 | `module-big-fish/src/lib.rs` 已收口为模块声明、公开导出和测试;`domain/commands/application/events/errors` 已承载创作域类型、命令、应用规则、事件和错误;新增 `module-big-fish/README.md`;源码不再命中 `过渡落位``cargo test -p module-big-fish` 通过 | 已关闭“文档完成但 lib.rs 仍承载大量规则”的漂移;不改 SpacetimeDB/API/前端 shape | 已关闭;后续只随全链验证和回归维护 | | `WP-BF Big Fish` | `module-big-fish` 已完成 DDD 物理拆分和迁移期口径清理 | `module-big-fish/src/lib.rs` 已收口为模块声明、公开导出和测试;`domain/commands/application/events/errors` 已承载创作域类型、命令、应用规则、事件和错误;新增 `module-big-fish/README.md`;源码不再命中 `过渡落位``cargo test -p module-big-fish` 通过 | 已关闭“文档完成但 lib.rs 仍承载大量规则”的漂移;不改 SpacetimeDB/API/前端 shape | 已关闭;后续只随全链验证和回归维护 |
| `WP-PZ Puzzle` | Puzzle API、前端消费和运行态全链仍未完成DDD 注释漂移已清理 | 第 4 节仍标记 `WP-PZ` 进行中;`module-puzzle/README.md` 写明当前只固定 contract 与最小规则;`domain.rs``events.rs` 文件头已改为当前领域口径;源码不再命中 `过渡落位``cargo test -p module-puzzle` 通过 | 补齐 agent session、work profile、runtime run、排行榜、图片选择和结果页草稿的领域应用服务再接 `WP-ST/WP-SC/WP-API/WP-FE` | P1 `WP-CW` 并行时避免共改创作 agent 通用文档 | | `WP-PZ Puzzle` | 正式平台运行态全链已收口;旧兼容入口物理删除窗口已关闭 | `module-puzzle/README.md` 已更新后端真相源口径;新增 `SERVER_RS_DDD_WP_PZ_RUNTIME_BACKEND_TRUTH_CLOSURE_2026-05-01.md``PlatformEntryFlowShellImpl` 不再导入 `puzzleLocalRuntime`;结果页草稿预览、作品开局、交换、拖动、下一关和排行榜均调用后端 API`/puzzle` 调试直达页仍保留本地 runtime`/runs/local-next-level` 已由 WP-DEL 删除 | 已关闭正式平台 Puzzle 主链;`puzzleLocalRuntime` 仅允许作为 `/puzzle` 调试直达页开发辅助,正式 API 只保留 `/runs/{run_id}/next-level` | 已关闭;后续只随 `WP-V` 增量维护 |
| `WP-AS Assets` | 资产对象类型、领域测试和 row mapper 已完成,资产 API、OSS adapter、facade 和 event table 尚未全链收口DDD 注释漂移已清理 | 第 4 节仍标记 `WP-AS` 进行中;`module-assets/README.md` 写明尚未进入完整资产状态建模;`module-assets/src/events.rs` 文件头已改为当前领域口径;源码不再命中 `过渡落位``cargo test -p module-assets` 通过 | 将 asset object、binding、manifest、history、OSS head/upload 和确认状态统一成领域应用结果SpacetimeDB 与 API 只做 adapter | P1依赖 `WP-PF` OSS 错误模型,反向支撑 `WP-CW/WP-PZ/WP-BF` | | `WP-AS Assets` | 当前资产主链已收口;生成资产读取统一走 OSS read-url 链路 | 新增 `SERVER_RS_DDD_WP_AS_ASSET_CHAIN_CLOSURE_2026-05-01.md``module-assets` 已补齐资产领域事件;`spacetime-module` 新增 `asset_event` public event table 并在对象确认、实体绑定变更后写事件;`migration.rs`、表目录和 Rust bindings 已对齐;`module-assets/README.md` 已更新完成口径;`/generated-*` 直读代理已在 WP-DEL 移除 | 正式资产状态仍以 `asset_object``asset_entity_binding` 为准;`asset_event` 只做订阅和审计事实;`asset_job``asset_manifest`、专业资产生成表另开切片;`/generated-*` 仅作为 legacyPublicPath/object key 标识 | 已关闭;后续只随 `WP-PF/WP-DEL/WP-V` 或新增资产生成需求增量维护 |
| `WP-API api-server BFF` | LLM 流式代理、微信登录、手机号登录、角色动画资产仍有首版禁用或占位链 | `api-server/src/llm.rs` 流式请求返回 `501``wechat_auth.rs` 返回“微信登录暂未启用”;`phone_auth.rs``password_management.rs` 返回“手机号登录暂未启用”`character_animation_assets.rs` 仍写 Stage 1 序列帧视频占位链 | API 层只做 BFF 编排、鉴权、SSE 和 DTO 映射;真实副作用落 `platform-llm/platform-auth/platform-oss`,领域状态变化必须回写 SpacetimeDB facade | P1依赖 `WP-PF` 能力与 `WP-ST/WP-SC` 写入 facade | | `WP-API api-server BFF` | 当前稳定 BFF 范围已收口LLM 流式代理已从 `501` 改为真实 SSE手机号/微信属于配置门控,角色动画资产深水区不阻塞 WP-API | `api-server/src/llm.rs` 已通过 `platform-llm::stream_text` 输出 `delta/complete/[DONE]``wechat_auth.rs``phone_auth.rs``password_management.rs` 的“暂未启用”由配置决定且定向测试可开启`character_animation_assets.rs` Stage 1 序列帧/视频占位链继续归 `WP-AS/WP-PF/WP-V` 深水区 | API 层只做 BFF 编排、鉴权、SSE 和 DTO 映射;后续新增写接口必须等待 `WP-ST/WP-SC` facade 稳定,不在 API 层复制领域规则 | 已关闭;后续只随 `WP-RS/WP-ST/WP-SC/WP-AS/WP-V` 增量维护 |
| `WP-FE-S/WP-FE-H/WP-FE-C` | 前端读取侧已推进,完整开局和动作写侧仍等后端新接口;不能继续补旧 compat client | 文档记录 `beginRuntimeStorySession``resolveRuntimeStoryAction` 暂未迁移;旧 `src/services/rpg-runtime/rpgRuntimeStoryClient.ts` 写侧和测试仍等待新接口 | `WP-FE-S` 先换 API client`WP-FE-H` 再换 hooks`WP-FE-C` 最后接组件;前端只做表现和临时 UI 状态,不重建规则 | P0强依赖 `WP-RS/WP-ST/WP-SC/WP-API` | | `WP-FE-S/WP-FE-H/WP-FE-C` | 前端 runtime story service/hooks/components 已切到新 story session scoped 写读主链 | `beginRuntimeStorySession` 调用 `/api/story/sessions/runtime``resolveRuntimeStoryAction` 调用 `/api/story/sessions/{storySessionId}/actions/resolve` 并发送扁平 `ResolveStoryRuntimeActionRequest`hooks 从 projection 水合快照并通过 `inventoryView` 消费背包视图;组件层不拼 API | 已关闭前端写读迁移;前端只做表现和临时 UI 状态,不重建规则 | 已关闭;旧 alias 与旧 DTO 物理删除转入 `WP-DEL` |
| `WP-ST SpacetimeDB Adapter` | 多个上下文已根入口瘦身,但剩余 table/reducer/procedure、migration、表目录和绑定生成仍未整体闭环 | 第 4 节仍标记 `WP-ST` 进行中Custom World 真实动作、RPG 写接口、Puzzle/Assets 完整链都需要继续接 `spacetime-module` | 所有 SpacetimeDB schema 变更由 `WP-ST` 单 owner 合流,并同步 `migration.rs`、表目录和生成绑定;禁止 API 直连生成绑定绕过 `spacetime-client` | P0 `WP-SC/WP-API/WP-FE/DEL`依赖 | | `WP-ST SpacetimeDB Adapter` | 当前稳定范围已收尾;后续只随新增 SpacetimeDB facade 或 schema shape 变化增量维护 | 新增 `SERVER_RS_DDD_WP_ST_CLOSURE_2026-05-01.md``asset_event` 已对齐表目录、`migration.rs` 和 Rust bindings`npm.cmd run spacetime:generate -- --rust-only` 已可在 Windows formatter 失败时自动分批格式化DDD 门禁已检查表目录与迁移白名单漂移 | 所有后续 SpacetimeDB schema 变更`WP-ST` 单 owner 合流,并同步 `migration.rs`、表目录和生成绑定;禁止 API 直连生成绑定绕过 `spacetime-client` | 当前项关闭;后续作为 `WP-SC/WP-API/WP-FE/DEL`增量依赖 |
| `WP-SC Spacetime Client` | 已接 runtime projection inventory source但后续 typed facade、row mapper 和错误映射`WP-ST` 滚动补齐 | 第 4 节标记 `WP-SC` 进行中;文档记录“后续仅随 WP-ST 新 facade 稳定后继续 typed facade/row mapper” | 只在 reducer/procedure shape 稳定后补 typed facade不在 client crate 内发明领域规则或预判 row shape | P0跟随 `WP-ST` 分批执行 | | `WP-SC Spacetime Client` | 当前稳定 facade 范围已收尾;后续新增 typed facade、row mapper 和错误映射`WP-ST` 新 facade 另开增量切片 | 第 4 节标记 `WP-SC` 完成;`spacetime-client/README.md` 已更新完成口径;错误 helper 补齐 `validation_failed``reducer_failed``cargo test -p spacetime-client` 通过后作为关闭证据 | 只在 reducer/procedure shape 稳定后补 typed facade不在 client crate 内发明领域规则或预判 row shape | 已关闭;后续跟随 `WP-ST` 增量维护 |
| `WP-DEL 删除旧层与命名收口` | 旧 compat、旧 contract、旧 facade、旧测试仍不能删除 | 第 4 节标记暂不可执行;`RuntimeStoryActionResponse`、旧 runtime story contract、旧前端写 client 等仍等待新写接口和前端迁移完成 | 所有新接口和前端迁移完成后再统一物理删除;删除前必须搜索运行代码无旧层引用 | P0依赖 `WP-FE-S/WP-FE-H/WP-FE-C` 完成 | | `WP-DEL 删除旧层与命名收口` | 旧层物理清理已关闭 | 旧 `RuntimeStoryStateResolveRequest``RuntimeStoryBootstrapRequest/Response``RuntimeStoryActionResponse`、TS `RuntimeActionRequest/Response`、前端 `beginRpgRuntimeStorySession`/`resolveRpgRuntimeStoryAction` 等 alias、旧 `/api/custom-world/*` 非 runtime 前缀、Puzzle `/runs/local-next-level``/generated-*` 资产直读代理已删除;新增旧路由未挂载测试 | 已完成旧入口、旧命名和非必要 fixture 删除;`/generated-*` 仅保留为 read-url 入参标识story battle 等独立 presentation 语义继续保留为当前投影表现类型 | 已关闭;后续进入 `WP-V` |
| `WP-V 全链验证与发布 smoke` | 全链验证仍被旧层删除和 Maincloud smoke 阻塞 | 第 4 节标记暂不可执行;此前 `api-server:maincloud` 多次以常驻服务超时或端口占用形式结束,需要在最终阶段统一验证 | 串行执行 cargo、npm、encoding、DDD boundary、SpacetimeDB build/generate 和 Maincloud smoke只记录非本轮阻塞不新增功能 | P2必须在 `WP-DEL` | | `WP-V 全链验证与发布 smoke` | 已关闭WP-DEL 后全链验证和 Maincloud smoke 已完成 | `npm.cmd run spacetime:generate -- --rust-only` 通过Windows 长路径格式化失败由脚本短路径分批 `rustfmt` fallback 完成;`cargo fmt --all --check``cargo check -p spacetime-module``cargo test --workspace --exclude spacetime-module``npm.cmd run check:encoding``npm.cmd run check:server-rs-ddd` 均通过;`npm.cmd run api-server:maincloud` 拉起后 `/healthz` 返回 200smoke 后确认无 `api-server` 残留进程且 3100 端口已释放 | Maincloud 启动恢复认证快照阶段出现两条 `503 Service Unavailable` 警告,但不影响服务启动和 healthz不新增功能 | 已关闭 |
| `G2/跨包清理` | `过渡落位` 迁移期口径已清零;后续只保留旧 compat/API/FE 链路的真实依赖项 | 搜索 `server-rs/crates` 不再命中 `过渡落位``module-runtime``module-puzzle``module-assets` 文件头已更新Big Fish 物理拆分漂移已关闭 | 后续 `WP-DEL/WP-V` 只关注旧 compat、旧 contract、旧 facade、旧测试和全链 smoke不再把已清理的注释漂移重复认领 | 已关闭 P2 清理项;最终验收继续依赖 `WP-DEL/WP-V` | | `G2/跨包清理` | `过渡落位` 迁移期口径已清零;后续只保留旧 compat/API/FE 链路的真实依赖项 | 搜索 `server-rs/crates` 不再命中 `过渡落位``module-runtime``module-puzzle``module-assets` 文件头已更新Big Fish 物理拆分漂移已关闭 | 后续 `WP-DEL/WP-V` 只关注旧 compat、旧 contract、旧 facade、旧测试和全链 smoke不再把已清理的注释漂移重复认领 | 已关闭 P2 清理项;最终验收继续依赖 `WP-DEL/WP-V` |
| `tests-support` | 已从目录占位收口为 workspace 共享测试支撑 crate | `server-rs/Cargo.toml` 已纳入 `crates/tests-support``server-rs/crates/tests-support/src/lib.rs` 提供 Maincloud healthz 默认地址、smoke URL 归一化、HTTP 2xx 断言和 healthz 非空响应体断言;`cargo test -p tests-support` 通过 | 已关闭“只有 README 的悬空占位”;后续 contract/reducer/view/projection 夹具只在真实测试策略稳定后继续扩展 | 已关闭 P2 占位项;后续随 `WP-V` 复用和扩展 | | `tests-support` | 已从目录占位收口为 workspace 共享测试支撑 crate | `server-rs/Cargo.toml` 已纳入 `crates/tests-support``server-rs/crates/tests-support/src/lib.rs` 提供 Maincloud healthz 默认地址、smoke URL 归一化、HTTP 2xx 断言和 healthz 非空响应体断言;`cargo test -p tests-support` 通过 | 已关闭“只有 README 的悬空占位”;后续 contract/reducer/view/projection 夹具只在真实测试策略稳定后继续扩展 | 已关闭 P2 占位项;后续随 `WP-V` 复用和扩展 |
@@ -429,6 +427,41 @@ spacetime describe <database> --json
## 10. 本地进度记录 ## 10. 本地进度记录
### 2026-05-01 WP-FE-S/H/C runtime story 收尾切片
已完成:
1. `WP-FE-S``beginRuntimeStorySession` 改为调用 `POST /api/story/sessions/runtime``resolveRuntimeStoryAction` 改为调用 `POST /api/story/sessions/{storySessionId}/actions/resolve`
2. `WP-FE-S`:前端写侧响应统一消费 `StoryRuntimeMutationResponse.projection`,由 `StoryRuntimeProjectionResponse.gameState` 在 service 边界水合 `HydratedSavedGameSnapshot`,不再读取旧 `snapshot/viewModel/presentation/patches` 组合。
3. `WP-FE-H``resolveServerRuntimeChoice` 只提交 `storySessionId/functionId/actionText/payload`,背包动作改用 `response.inventoryView`battle presentation 仅在响应显式携带可选 presentation 时播放。
4. `WP-FE-C``RpgRuntimeShell` 组件层保持只消费 hooks 出口,不拼接 `/api/runtime/story/*``/api/story/*` 路径,不新增规则说明 UI 文案。
5. `server-rs`:补齐 `/api/story/sessions/runtime``/api/story/sessions/{storySessionId}/actions/resolve` 鉴权测试,保持旧 `/api/runtime/story/*` 未挂载。
6. `packages/shared/src/contracts/story.ts` 移除旧 `StoryRuntimeBootstrap*` / `StoryRuntimeAction*` HTTP 写侧类型,只保留新 `BeginStoryRuntimeSessionRequest``ResolveStoryRuntimeActionRequest``StoryRuntimeMutationResponse`
7. `src/data/functionCatalog/flow/campTravelHomeScene.ts` 的 executor 文档指向 `module-runtime-story/src/session_action.rs` 新主链。
边界说明:
1. 本次不新增或修改 SpacetimeDB 表结构,因此不改 `migration.rs`
2. 不恢复旧 `/api/runtime/story/*`,不兼容 `server-node`
3. `rpgRuntimeStoryState.ts` 中仍存在 view model / presentation 类型,作为旧展示语义与 story battle 表现的后续清理输入,物理删除归 `WP-DEL`
4. 历史设计文档中的旧路径引用不作为运行代码阻塞,统一归 `WP-DEL/WP-V` 文档清理。
验证:
```powershell
npm.cmd run test -- src/services/rpg-runtime/rpgRuntimeStoryClient.test.ts
npm.cmd run test -- src/hooks/rpg-runtime-story/runtimeStoryCoordinator.test.ts
npm.cmd run test -- src/hooks/rpg-runtime-story/storyChoiceRuntime.test.ts
npm.cmd run test -- src/components/rpg-runtime-shell/RpgRuntimeShell.test.tsx
cargo fmt --all --check --manifest-path server-rs\Cargo.toml
cargo check --workspace --manifest-path server-rs\Cargo.toml
cargo test -p shared-contracts -p module-runtime-story --manifest-path server-rs\Cargo.toml
cargo test -p api-server story_sessions --manifest-path server-rs\Cargo.toml
npm.cmd run check:server-rs-ddd
```
结果:通过。`cargo check``api-server story_sessions` 仍有既有 `prompt/rpg/runtime_chat.rs` dead code warning不影响本切片。
### 2026-04-29 WP-FE-C RPG runtime shell 组件测试夹具接线切片 ### 2026-04-29 WP-FE-C RPG runtime shell 组件测试夹具接线切片
已完成: 已完成:
@@ -1233,10 +1266,10 @@ npm.cmd run api-server:maincloud
5. 同步更新 `RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md` 和 G1 契约矩阵,将 runtime story 旧层标记为已删除。 5. 同步更新 `RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md` 和 G1 契约矩阵,将 runtime story 旧层标记为已删除。
6. 本次未修改 SpacetimeDB 表结构,未触碰 `migration.rs` 6. 本次未修改 SpacetimeDB 表结构,未触碰 `migration.rs`
当前剩余 2026-05-01 更新
1. 前端 `src/services/rpg-runtime/rpgRuntimeStoryClient.ts` 仍指向 `/api/runtime/story`,需要在 `WP-FE-S` 中迁到新 `/api/story/*`session scoped facade 1. 前端 `src/services/rpg-runtime/rpgRuntimeStoryClient.ts` 已迁到 `/api/story/sessions/runtime``/api/story/sessions/{storySessionId}/runtime-projection` `/api/story/sessions/{storySessionId}/actions/resolve`
2. `packages/shared/src/contracts/rpgRuntimeStory*``shared-contracts/src/runtime_story*` 仍保留 DTO等前端迁移完成后`WP-DEL` 统一删除。 2. `packages/shared/src/contracts/rpgRuntimeStory*``shared-contracts/src/runtime_story*` 仍保留历史展示 DTO需由 `WP-DEL` 统一评估 story battle 表现依赖后删除。
3. runtime chat 仍有 `runtimeStory` 命名和 prompt helper需另起 session scoped chat/story facade 任务继续收口。 3. runtime chat 仍有 `runtimeStory` 命名和 prompt helper需另起 session scoped chat/story facade 任务继续收口。
验证: 验证:
@@ -1329,7 +1362,7 @@ cargo test -p api-server wechat --manifest-path server-rs/Cargo.toml
cargo check -p api-server --manifest-path server-rs/Cargo.toml cargo check -p api-server --manifest-path server-rs/Cargo.toml
cargo fmt -p platform-llm -p platform-oss -p platform-auth -p api-server --manifest-path server-rs/Cargo.toml --check cargo fmt -p platform-llm -p platform-oss -p platform-auth -p api-server --manifest-path server-rs/Cargo.toml --check
npm.cmd run check:server-rs-ddd npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_PF_PLATFORM_ERROR_CLASSIFICATION_2026-04-29.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md server-rs/crates/platform-llm/src/lib.rs server-rs/crates/platform-oss/src/lib.rs server-rs/crates/platform-auth/src/lib.rs server-rs/crates/platform-auth/Cargo.toml server-rs/crates/api-server/src/platform_errors.rs server-rs/crates/api-server/src/llm.rs server-rs/crates/api-server/src/state.rs server-rs/crates/api-server/src/wechat_provider.rs server-rs/crates/api-server/src/wechat_auth.rs server-rs/crates/api-server/src/assets.rs server-rs/crates/api-server/src/big_fish.rs server-rs/crates/api-server/src/character_visual_assets.rs server-rs/crates/api-server/src/character_animation_assets.rs server-rs/crates/api-server/src/custom_world_ai.rs server-rs/crates/api-server/src/legacy_generated_assets.rs server-rs/crates/api-server/src/puzzle.rs server-rs/crates/api-server/src/phone_auth.rs server-rs/crates/api-server/src/http_error.rs server-rs/crates/api-server/src/main.rs server-rs/crates/module-auth/src/lib.rs npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_PF_PLATFORM_ERROR_CLASSIFICATION_2026-04-29.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md server-rs/crates/platform-llm/src/lib.rs server-rs/crates/platform-oss/src/lib.rs server-rs/crates/platform-auth/src/lib.rs server-rs/crates/platform-auth/Cargo.toml server-rs/crates/api-server/src/platform_errors.rs server-rs/crates/api-server/src/llm.rs server-rs/crates/api-server/src/state.rs server-rs/crates/api-server/src/wechat_provider.rs server-rs/crates/api-server/src/wechat_auth.rs server-rs/crates/api-server/src/assets.rs server-rs/crates/api-server/src/big_fish.rs server-rs/crates/api-server/src/character_visual_assets.rs server-rs/crates/api-server/src/character_animation_assets.rs server-rs/crates/api-server/src/custom_world_ai.rs server-rs/crates/api-server/src/puzzle.rs server-rs/crates/api-server/src/phone_auth.rs server-rs/crates/api-server/src/http_error.rs server-rs/crates/api-server/src/main.rs server-rs/crates/module-auth/src/lib.rs
npm.cmd run api-server:maincloud npm.cmd run api-server:maincloud
``` ```
@@ -1500,7 +1533,23 @@ npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_202
1. `spacetime-client/src/*.rs` facade 中已不再保留重复的 `.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))` 模式。 1. `spacetime-client/src/*.rs` facade 中已不再保留重复的 `.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))` 模式。
2. `spacetime-client/src/mapper.rs` 中仅保留需要领域语义表达的 `Procedure(...)` 构造。 2. `spacetime-client/src/mapper.rs` 中仅保留需要领域语义表达的 `Procedure(...)` 构造。
3. 本次未修改 `spacetime-module``migration.rs`、生成绑定、`api-server` 路由或前端。 3. 本次未修改 `spacetime-module``migration.rs`、生成绑定、`api-server` 路由或前端。
4. `WP-SC` 当前基础设施收口已完成;后续只在 `WP-ST` 新 facade 或 row shape 稳定后,按领域补 typed facade 与 row snapshot mapper。 4. `WP-SC` 当前稳定 facade 范围已完成关闭;后续只在 `WP-ST` 新 facade 或 row shape 稳定后,按领域另开增量切片补 typed facade 与 row snapshot mapper。
### 2026-05-01 WP-SC 当前稳定 facade 收尾关闭
已完成:
1. `spacetime-client/src/lib.rs` 补齐 `SpacetimeClientError::validation_failed``SpacetimeClientError::reducer_failed`,并新增错误 helper 单元测试。
2. AI reducer callback 的业务错误统一走 `reducer_failed`,保持 API 层按 `Runtime` 分类的既有语义。
3. combat、inventory、runtime、story facade 的本地命令构造与输入校验错误统一走 `validation_failed`,减少各 facade 内重复 `Runtime(error.to_string())`
4. `spacetime-client/README.md` 已从早期占位说明更新为当前完成口径,明确当前稳定 facade 范围已收尾。
5. `SERVER_RS_DDD_WP_SC_SPACETIME_CLIENT_REFACTOR_2026-04-29.md` 已补充 2026-05-01 关闭口径和最终验收命令。
边界说明:
1. 本次不修改 `spacetime-module``migration.rs`、生成绑定、HTTP route、shared contract 或前端。
2. `WP-ST` 后续若新增 table/reducer/procedure 或 row shape`WP-SC` 只按对应领域增量补 facade/mapper不再阻塞当前工作包关闭。
3. `WP-RS/WP-API/WP-FE/WP-DEL` 仍因旧 runtime story 写侧和前端迁移保持进行中。
### 2026-04-29 WP-ST AI Task 事件 Adapter 切片 ### 2026-04-29 WP-ST AI Task 事件 Adapter 切片
@@ -1640,7 +1689,7 @@ npm.cmd run api-server:maincloud
4. `getRuntimeStoryState` 复用 `getStoryRuntimeProjection`,读取侧继续保持不回退 `runtimeSessionId` 的约束。 4. `getRuntimeStoryState` 复用 `getStoryRuntimeProjection`,读取侧继续保持不回退 `runtimeSessionId` 的约束。
5. `src/services/rpg-runtime/index.ts` 导出新 story session client 函数和结果类型,供后续 `WP-FE-H` 迁移 hooks 使用。 5. `src/services/rpg-runtime/index.ts` 导出新 story session client 函数和结果类型,供后续 `WP-FE-H` 迁移 hooks 使用。
6. 补回 `src/services/customWorldAgentGenerationProgress.ts` 的“建立场景连接”进度阶段,使前端草稿生成进度重新对齐既有 13 步文档与 service 全量验收。 6. 补回 `src/services/customWorldAgentGenerationProgress.ts` 的“建立场景连接”进度阶段,使前端草稿生成进度重新对齐既有 13 步文档与 service 全量验收。
7. 更新 `SERVER_RS_DDD_WP_FE_S_RPG_RUNTIME_STORY_CLIENT_MIGRATION_2026-04-29.md``docs/technical/README.md`,明确轮只收口 service client `beginRuntimeStorySession``resolveRuntimeStoryAction` 仍等待后端完整开局快照和 session scoped 动作结算接口 7. 更新 `SERVER_RS_DDD_WP_FE_S_RPG_RUNTIME_STORY_CLIENT_MIGRATION_2026-04-29.md``docs/technical/README.md`,明确轮只收口 service client2026-05-01 收尾切片已补齐 `beginRuntimeStorySession``resolveRuntimeStoryAction` 的新写侧接入
验证: 验证:
@@ -1696,12 +1745,8 @@ cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml
2. 新增 `SERVER_RS_DDD_WP_RS_COMPAT_RESIDUE_AUDIT_2026-04-29.md`,冻结本次残留审计边界。 2. 新增 `SERVER_RS_DDD_WP_RS_COMPAT_RESIDUE_AUDIT_2026-04-29.md`,冻结本次残留审计边界。
3. 清理 `module-runtime-story` 运行代码注释里的 `compat / 兼容` 阶段性定位,将 crate 口径收束为 runtime story 主链纯规则。 3. 清理 `module-runtime-story` 运行代码注释里的 `compat / 兼容` 阶段性定位,将 crate 口径收束为 runtime story 主链纯规则。
4. 更新 `module-runtime-story/README.md`,明确后续要迁的是旧 `/api/runtime/story/*` 写侧能力,而不是继续扩展兼容桥。 4. 更新 `module-runtime-story/README.md`,明确后续要迁的是旧 `/api/runtime/story/*` 写侧能力,而不是继续扩展兼容桥。
5. 冻结本次不删除的残留: 5. 2026-05-01 更新:旧写侧能力已迁入 session scoped routeREADME 已改为“增强规则与投影,删除旧命名”的口径。
- `src/services/rpg-runtime/rpgRuntimeStoryClient.ts` 仍指向 `/api/runtime/story` 6. 剩余残留只包括历史展示 DTO、旧命名和历史文档引用统一转入 `WP-DEL`
- `src/services/rpg-runtime/rpgRuntimeStoryClient.test.ts` 仍断言旧写接口路径
- `packages/shared/src/contracts/rpgRuntimeStoryState.ts``server-rs/crates/shared-contracts/src/runtime_story.rs` 仍保留 `RuntimeStoryActionResponse`
- `src/hooks/rpg-runtime-story/**` 仍保留部分旧调用面兼容注释
6. 上述残留需等待 session scoped 新写接口稳定后,由 `WP-FE-S -> WP-FE-H -> WP-DEL` 顺序处理。
验证: 验证:
@@ -1932,6 +1977,40 @@ npm.cmd run api-server:maincloud
结果:命令在 124 秒观察窗口内超时,因为 `cargo run` 前台常驻;随后确认 `127.0.0.1:3100` 已由本仓库 `server-rs/target/debug/api-server.exe` 监听。`GET /healthz` 返回 `200``GET /api/story/sessions/storysess_001/state``GET /api/story/sessions/storysess_001/runtime-projection``GET /api/story/battles/battle_001``POST /api/story/battles/resolve` 未登录均返回 `401`,旧 `POST /api/runtime/story/actions/resolve` 返回 `404` 结果:命令在 124 秒观察窗口内超时,因为 `cargo run` 前台常驻;随后确认 `127.0.0.1:3100` 已由本仓库 `server-rs/target/debug/api-server.exe` 监听。`GET /healthz` 返回 `200``GET /api/story/sessions/storysess_001/state``GET /api/story/sessions/storysess_001/runtime-projection``GET /api/story/battles/battle_001``POST /api/story/battles/resolve` 未登录均返回 `401`,旧 `POST /api/runtime/story/actions/resolve` 返回 `404`
### 2026-05-01 WP-API BFF 收尾
已完成:
1. 新增 `SERVER_RS_DDD_WP_API_BFF_CLOSURE_2026-05-01.md`,记录本次 WP-API 当前稳定范围关闭口径。
2. `api-server/src/llm.rs` 移除 `stream=true` 返回 `501` 的首版禁用分支,改为调用 `platform-llm::LlmClient::stream_text`
3. `/api/llm/chat/completions` 流式响应固定输出 `delta``complete``[DONE]` SSE 事件;上游流式中途失败时输出 `error` SSE 事件。
4. 缺 LLM API Key 仍在响应开始前返回标准 `SERVICE_UNAVAILABLE` 错误 envelope非流式 JSON envelope 行为不变。
5. 为完成全量验证,`module-runtime-story` 补齐测试编译门槛:提高大 JSON 快照构造的宏递归上限、给 `StoryRuntimeActionResolveOutput``Debug` derive并移除未用 import。
边界说明:
1. 本次不改 SpacetimeDB 表结构、reducer、procedure、绑定 shape 或 `migration.rs`
2. `api-server` 只做鉴权、DTO 映射、SSE 转发和平台错误转换,不新增 LLM prompt 或玩法规则。
3. 手机号登录与微信登录属于配置门控;角色动画 Stage 1 占位链归资产/模型生成深水区,不阻塞 WP-API 当前稳定范围完成。
4. 后续新增 session scoped 写接口仍必须等待 `WP-RS/WP-ST/WP-SC` facade 稳定后再接入,禁止 API 层绕过 `spacetime-client`
验证:
```powershell
cargo fmt --all --check --manifest-path server-rs\Cargo.toml
cargo test -p api-server llm --manifest-path server-rs\Cargo.toml
cargo test -p api-server custom_world_ai --manifest-path server-rs\Cargo.toml
cargo test -p api-server runtime_story_legacy_routes_are_not_mounted --manifest-path server-rs\Cargo.toml
cargo test -p api-server --manifest-path server-rs\Cargo.toml
cargo test -p module-runtime-story --manifest-path server-rs\Cargo.toml
cargo check --workspace --manifest-path server-rs\Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_API_BFF_CLOSURE_2026-05-01.md docs/technical/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md server-rs/crates/api-server/src/llm.rs server-rs/crates/api-server/src/custom_world_ai.rs server-rs/crates/module-runtime-story/src/lib.rs server-rs/crates/module-runtime-story/src/bootstrap.rs server-rs/crates/module-runtime-story/src/session_action.rs
npm.cmd run api-server:maincloud
```
结果:通过。`api-server` 全量 203 个测试通过、4 个真实外部服务测试按预期 ignored`module-runtime-story` 11 个测试通过workspace 编译通过DDD 边界检查通过 15 个 module crate编码检查通过 8 个文件。`api-server:maincloud` 启动后 `GET http://127.0.0.1:3100/healthz` 返回 `200 {"ok":true,"service":"genarrative-api-server"}`,本轮 smoke 未留下常驻进程。启动日志中 Maincloud WebSocket 恢复认证快照短暂返回 `503 Service Unavailable`,未阻止 API server 就绪,属于外部 Maincloud 可用性告警。
### 2026-04-29 WP-API story runtime projection route 接线 ### 2026-04-29 WP-API story runtime projection route 接线
已完成: 已完成:
@@ -1986,11 +2065,11 @@ npm.cmd run api-server:maincloud
4. `loadRuntimeInventoryView` 改为从新投影映射背包视图。 4. `loadRuntimeInventoryView` 改为从新投影映射背包视图。
5. `rpgRuntimeStoryGateway` 的 option catalog / resume 读取侧改为使用 `storySessionId`,缺失时显式失败,不再用 `runtimeSessionId` 兜底。 5. `rpgRuntimeStoryGateway` 的 option catalog / resume 读取侧改为使用 `storySessionId`,缺失时显式失败,不再用 `runtimeSessionId` 兜底。
当前边界 2026-05-01 更新
1. `beginRuntimeStorySession` 暂未迁移,因为新 `/api/story/sessions` 当前只创建 story session不返回开局 `GameState` 快照。 1. `beginRuntimeStorySession` 已迁到 `POST /api/story/sessions/runtime`,返回 `StoryRuntimeMutationResponse.projection`,由 service 边界水合 `GameState` 快照。
2. `resolveRuntimeStoryAction` 暂未迁移,因为新主链尚未提供等价的 story session scoped 动作结算与状态更新回包 2. `resolveRuntimeStoryAction` 已迁到 `POST /api/story/sessions/{storySessionId}/actions/resolve`,请求体使用扁平 `ResolveStoryRuntimeActionRequest`
3. `WP-FE-H` hooks 大迁移继续等待后端写接口稳定;本轮只做读取侧必要网关接线 3. `WP-FE-H` hooks 写侧和读取侧均通过 service client 消费 projection不再等待后端写接口
验证: 验证:
@@ -2261,6 +2340,41 @@ npm.cmd run api-server:maincloud
结果:命令在 60 秒观察窗口内超时,但随后探测 `http://127.0.0.1:3100/healthz` 返回 `200`,本地存在 `api-server` 运行进程。本切片未触发新的 Rust 编译错误。 结果:命令在 60 秒观察窗口内超时,但随后探测 `http://127.0.0.1:3100/healthz` 返回 `200`,本地存在 `api-server` 运行进程。本切片未触发新的 Rust 编译错误。
### 2026-05-01 WP-AS Assets 资产主链收尾
已完成:
1. 新增 `SERVER_RS_DDD_WP_AS_ASSET_CHAIN_CLOSURE_2026-05-01.md`,记录本次 WP-AS 当前主链关闭口径。
2. `module-assets/src/events.rs` 补齐 `AssetDomainEvent``AssetObjectConfirmedEvent``AssetEntityBindingChangedEvent`,领域层能表达对象确认与实体绑定变更事实。
3. `spacetime-module/src/asset_metadata/objects.rs` 新增 `asset_event` public event table并在 `upsert_asset_object` 成功后写入 `ObjectConfirmed` 事件。
4. `spacetime-module/src/asset_metadata/bindings.rs``upsert_asset_entity_binding` 成功后写入 `EntityBindingChanged` 事件。
5. `migration.rs``SPACETIMEDB_TABLE_CATALOG.md` 和 Rust `module_bindings` 已补齐 `asset_event`,订阅端可通过 `asset_event().on_insert(...)` 感知资产主链变化。
6. `module-assets/README.md` 已更新完成口径资产对象、绑定、历史、OSS 确认、API facade 和 event table 已闭环。
边界说明:
1. 正式资产状态仍以 `asset_object``asset_entity_binding` 为准;`asset_event` 只承接订阅和审计所需的轻量事实。
2. 本次不新增 `asset_job``asset_manifest` 或专业资产生成表。
3.`/generated-*` 读兼容入口已由 `WP-DEL` 物理删除,生成资产读取统一走 OSS read-url 链路;历史 DTO 中的 `/generated-*` 只作为 `legacyPublicPath` 标识。
4. `spacetime-client/src/module_bindings/**` 仍是生成物;本次通过 `spacetime generate` 产出 `asset_event_*` 文件,只在 `mod.rs` 做最小接线,避免全目录格式化噪声。
验证:
```powershell
spacetime generate --no-config --lang rust --out-dir C:\g\rs --module-path server-rs\crates\spacetime-module --yes
cargo fmt -p module-assets -p spacetime-module --manifest-path server-rs\Cargo.toml --check
cargo test -p module-assets --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml --target wasm32-unknown-unknown
cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml
cargo check -p api-server --manifest-path server-rs\Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_AS_ASSET_CHAIN_CLOSURE_2026-05-01.md docs/technical/SPACETIMEDB_TABLE_CATALOG.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md docs/technical/README.md server-rs/crates/module-assets/README.md server-rs/crates/module-assets/src/events.rs server-rs/crates/module-assets/src/lib.rs server-rs/crates/spacetime-module/src/asset_metadata/objects.rs server-rs/crates/spacetime-module/src/asset_metadata/bindings.rs server-rs/crates/spacetime-module/src/migration.rs server-rs/crates/spacetime-client/src/module_bindings/mod.rs server-rs/crates/spacetime-client/src/module_bindings/asset_event_kind_type.rs server-rs/crates/spacetime-client/src/module_bindings/asset_event_type.rs server-rs/crates/spacetime-client/src/module_bindings/asset_event_table.rs
npm.cmd run api-server:maincloud
```
结果:通过。`spacetime generate --no-config --lang rust --out-dir C:\g\rs --module-path server-rs\crates\spacetime-module --yes` 生成成功;`cargo fmt -p module-assets -p spacetime-module` 通过;`module-assets` 8 个测试通过;`spacetime-module` native 与 `wasm32-unknown-unknown` 编译通过;`spacetime-client``api-server` 编译通过;`api-server assets::tests` 27 个非 ignored 测试通过、3 个 live 测试按需 ignoredDDD 边界检查 15 个 module crate 通过;编码检查 14 个文件通过;`npm.cmd run api-server:maincloud` 已启动到 `127.0.0.1:3100``GET /healthz` 返回 `200 {"ok":true,"service":"genarrative-api-server"}`,随后本次 smoke 进程已清理且 3100 端口已释放。`api-server` 仍有既有 runtime prompt 未使用 warningMaincloud WebSocket 在启动恢复认证快照时返回过 `503 Service Unavailable` warning但不影响本次本地 healthz smoke。
### 2026-04-29 WP-RS 领域投影 builder 切片 ### 2026-04-29 WP-RS 领域投影 builder 切片
已完成: 已完成:
@@ -2385,7 +2499,7 @@ cargo check -p module-combat -p module-inventory -p module-npc -p module-progres
1. 新增 `SERVER_RS_DDD_WP_RS_RUNTIME_STORY_DOMAIN_SPLIT_2026-04-30.md`,记录本次 `module-runtime-story` 顶层收口边界。 1. 新增 `SERVER_RS_DDD_WP_RS_RUNTIME_STORY_DOMAIN_SPLIT_2026-04-30.md`,记录本次 `module-runtime-story` 顶层收口边界。
2. `module-runtime-story/src/domain.rs` 收口 runtime story 顶层常量、action 结算结果、生成故事 payload、NPC 任务上下文和待接任务上下文。 2. `module-runtime-story/src/domain.rs` 收口 runtime story 顶层常量、action 结算结果、生成故事 payload、NPC 任务上下文和待接任务上下文。
3. `module-runtime-story/src/commands.rs` 收口 `resolve_action_text` 3. `module-runtime-story/src/commands.rs` 收口 `resolve_action_text`
4. `module-runtime-story/src/application.rs` 收口 `RuntimeStoryActionResponseParts``simple_story_resolution``build_status_patch``current_world_type` 4. `module-runtime-story/src/application.rs` 收口 `simple_story_resolution``build_status_patch``current_world_type`;旧 `RuntimeStoryActionResponseParts` 已在 WP-DEL 删除
5. `module-runtime-story/src/errors.rs` 补入 `RuntimeStoryRuleError` 5. `module-runtime-story/src/errors.rs` 补入 `RuntimeStoryRuleError`
6. `module-runtime-story/src/events.rs` 补入 `RuntimeStoryDomainEvent` 6. `module-runtime-story/src/events.rs` 补入 `RuntimeStoryDomainEvent`
7. `module-runtime-story/src/lib.rs` 收口为模块声明、公开导出和既有子模块 re-export保持 `module_runtime_story::*` 公开 API。 7. `module-runtime-story/src/lib.rs` 收口为模块声明、公开导出和既有子模块 re-export保持 `module_runtime_story::*` 公开 API。
@@ -2394,7 +2508,7 @@ cargo check -p module-combat -p module-inventory -p module-npc -p module-progres
1. 本次不迁移旧 `/api/runtime/story/*` 写侧接口,不新增 session scoped 写 route。 1. 本次不迁移旧 `/api/runtime/story/*` 写侧接口,不新增 session scoped 写 route。
2. 本次不改 SpacetimeDB 表、reducer、procedure、绑定 shape 或 `migration.rs` 2. 本次不改 SpacetimeDB 表、reducer、procedure、绑定 shape 或 `migration.rs`
3. `RuntimeStoryActionResponse`、旧前端写 client 和旧 contract 删除仍等待后续 `WP-FE-S/WP-FE-H/WP-FE-C/WP-DEL` 3. `RuntimeStoryActionResponse`、旧前端写 client alias 和旧 contract 已在 WP-DEL 删除,当前主链只返回 story runtime projection
验证: 验证:
@@ -2435,6 +2549,74 @@ cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml
结果:通过,`module-custom-world` 13 个单元测试通过;`spacetime-module` 编译通过Custom World 源码不再命中 `execute_placeholder_custom_world_action``最小兼容占位``过渡落位` 结果:通过,`module-custom-world` 13 个单元测试通过;`spacetime-module` 编译通过Custom World 源码不再命中 `execute_placeholder_custom_world_action``最小兼容占位``过渡落位`
### 2026-05-01 WP-CW Custom World 全链收尾
已完成:
1. 新增 `SERVER_RS_DDD_WP_CW_FULL_CHAIN_CLOSURE_2026-05-01.md`,记录本次 WP-CW 当前主链关闭口径。
2. `api-server/src/app.rs` 补齐 `POST /api/runtime/custom-world/scene-image`,与 entity、scene-npc、cover-image、cover-upload 统一进入 runtime Custom World 主链。
3. `api-server/src/custom_world_ai.rs` 在 scene-image 与 cover-image 的计费资产操作前显式检查 DashScope 配置,缺 key 时稳定返回 `SERVICE_UNAVAILABLE`,不受开发机 `.env.local` 影响。
4. `api-server/src/custom_world_ai.rs` 的场景图 prompt 测试改为复用正式 prompt compiler 输出作为断言源,避免提示词中文细节演进导致脆弱断言。
5. `src/services/rpg-creation/rpgCreationAssetClient.ts` 将 RPG 创作资产请求主路径从 `/api/custom-world/*` 切到 `/api/runtime/custom-world/*`
6. 新增 `src/services/rpg-creation/rpgCreationAssetClient.test.ts`,覆盖 scene image、landmark/entity、scene npc 三类资产 client 路由。
边界说明:
1. 本次不改 SpacetimeDB 表结构、procedure 签名、绑定 shape 或 `migration.rs`
2.`/api/custom-world/entity``/api/custom-world/scene-npc``/api/custom-world/scene-image``/api/custom-world/cover-image``/api/custom-world/cover-upload` 已在 WP-DEL 取消挂载,当前只保留 `/api/runtime/custom-world/*` 主链。
3. LLM、DashScope 图片生成、OSS 上传仍留在 `api-server + platform-*`reducer/procedure 只做确定性状态编排和持久化。
4. 后续若要删除旧入口,归 `WP-DEL/WP-V` 统一搜索、替换和 smoke不再阻塞 `WP-CW` 完成状态。
验证:
```powershell
cargo fmt --all --check --manifest-path server-rs\Cargo.toml
cargo test -p module-custom-world --manifest-path server-rs\Cargo.toml
cargo test -p api-server custom_world --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml
npm.cmd run test -- src/services/rpg-creation/rpgCreationGenerationClient.test.ts src/services/rpg-creation/rpgCreationGenerationClient.node.test.ts src/services/rpg-creation/rpgCreationAssetClient.test.ts
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md docs/technical/SERVER_RS_DDD_WP_CW_FULL_CHAIN_CLOSURE_2026-05-01.md server-rs/crates/api-server/src/custom_world_ai.rs src/services/rpg-creation/rpgCreationAssetClient.ts src/services/rpg-creation/rpgCreationAssetClient.test.ts
npm.cmd run api-server:maincloud
```
结果:`module-custom-world` 13 个单元测试通过;`api-server custom_world` 30 个定向测试通过;`spacetime-module``spacetime-client` 编译通过RPG 创作 generation/asset client 3 个测试文件 7 个用例通过DDD 边界检查 15 个 module crate 通过;编码检查 6 个文件通过;`api-server:maincloud` 超时前 `/healthz` 返回 `200 {"ok":true,"service":"genarrative-api-server"}`,本次 smoke 子进程已清理且 `3100` 端口已释放。`cargo fmt --all --check` 已确认 `api-server` 本轮文件格式通过,但当前工作区另有非本轮 `module-story` 格式漂移,需由对应工作包收口。
### 2026-05-01 WP-ST SpacetimeDB Adapter 收尾
已完成:
1. 新增 `SERVER_RS_DDD_WP_ST_CLOSURE_2026-05-01.md`,记录本次 WP-ST 当前稳定范围关闭口径。
2. `asset_event` 已作为资产事件表进入生成绑定,包含 `asset_event_table.rs``asset_event_type.rs``asset_event_kind_type.rs`
3. `SPACETIMEDB_TABLE_CATALOG.md` 补齐 `database_migration_operator`、profile 邀请/推荐/会员/充值表,以及 `asset_event` 目录项。
4. `scripts/check-server-rs-ddd-boundaries.mjs` 增加 SpacetimeDB table、`migration.rs` 白名单和表目录漂移检查。
5. `scripts/generate-spacetime-bindings.mjs` 在 SpacetimeDB CLI 已产出 Rust bindings 但生成后 formatter 失败时,自动在短临时目录分批 `rustfmt`,再同步到仓库生成目录。
6. `spacetime-client/README.md` 已同步生成物维护口径,恢复流程统一使用 `npm.cmd run spacetime:generate -- --rust-only`
边界说明:
1. 本次不新增 story action 写接口,不迁移前端写侧,不删除旧 compat 层。
2. 后续新增 SpacetimeDB 表、reducer、procedure 或 row shape 时,仍必须由 `WP-ST` 同步 `migration.rs`、表目录和 bindings。
3. `database_migration_operator` 是迁移权限表,不纳入业务迁移导出白名单。
验证:
```powershell
npm.cmd run spacetime:generate -- --rust-only
cargo fmt --all --check --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml
cargo test -p module-assets --manifest-path server-rs\Cargo.toml
cargo test -p spacetime-client --manifest-path server-rs\Cargo.toml
cargo check -p api-server --manifest-path server-rs\Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding
npm.cmd run api-server:maincloud
```
结果通过。SpacetimeDB CLI 完成 module build 和 bindings 产出后,自身 formatter 在 Windows 长参数场景失败;仓库脚本接管分批 `rustfmt` 并成功同步生成目录。`spacetime-module``spacetime-client` 编译通过,`module-assets` 8 个测试通过,`spacetime-client` 10 个测试通过DDD 边界检查通过 15 个 module crate编码检查通过 2815 个文件;`api-server:maincloud` 启动后 `/healthz` 返回 `200 {"ok":true,"service":"genarrative-api-server"}`,本次启动进程已清理并释放 `3100` 端口。当前仍存在 `module-runtime-story``api-server` 既有 unused warning非本次 WP-ST 引入。
### 2026-04-30 WP-BF 与 G2 迁移期口径清理切片 ### 2026-04-30 WP-BF 与 G2 迁移期口径清理切片
已完成: 已完成:

View File

@@ -0,0 +1,65 @@
# server-rs DDD WP-API BFF 收尾2026-05-01
## 1. 背景
`WP-API api-server BFF` 的启动切片已关闭旧 `/api/runtime/story/*` 兼容挂载,并把 `/api/story/*`、runtime projection、battle facade、平台错误模型等入口接到当前稳定的 `spacetime-client``platform-*` 能力上。
本收尾切片只处理 BFF 层还可以独立闭合的缺口:`/api/llm/chat/completions` 已具备非流式代理,但 `stream=true` 仍返回 `501``platform-llm` 已提供 OpenAI 兼容 SSE 解析和 `stream_text` 增量回调,因此 API 层可以补齐真正 SSE 输出,而不新增领域规则、不改 SpacetimeDB schema。
## 2. 收尾目标
1. `POST /api/llm/chat/completions` 保持非流式 JSON envelope 行为不变。
2. `POST /api/llm/chat/completions``stream=true` 时返回 `text/event-stream`,由 `platform-llm` 负责上游 SSE 解析API 层只转成前端可消费事件。
3. 流式事件固定为:
- `delta`:包含 `delta``content``finishReason`
- `complete`:复用 `LlmChatCompletionResponse``id/model/content/finishReason`
- 普通 data `[DONE]`:标记流结束。
4. 上游流式过程中失败时HTTP 已经开始返回 `200`,因此用 `error` SSE 事件携带 `code/message`,随后发送 `[DONE]`
5. 缺 LLM API Key 仍在开始流之前返回标准 `SERVICE_UNAVAILABLE` 错误 envelope。
## 3. 不变边界
1. 本次不新增、修改 SpacetimeDB table、reducer、procedure、绑定 shape 或 `migration.rs`
2. 本次不把 LLM prompt、剧情、玩法规则放进 `api-server`API 层只做鉴权、DTO 映射、SSE 转发和平台错误转换。
3.`/api/runtime/story/*` 继续不挂载;新 `/api/story/*` 继续只通过 `spacetime-client` facade。
4. 手机号登录与微信登录属于配置门控和 `platform-auth` 能力接入,不是 WP-API 缺失实现;角色动画 Stage 1 占位链继续归资产/模型生成深水区,不阻塞本次 BFF 收尾。
## 4. 验收
```powershell
cargo fmt -p api-server --manifest-path server-rs\Cargo.toml --check
cargo test -p api-server llm --manifest-path server-rs\Cargo.toml
cargo test -p api-server custom_world_ai --manifest-path server-rs\Cargo.toml
cargo test -p api-server runtime_story_legacy_routes_are_not_mounted --manifest-path server-rs\Cargo.toml
cargo check -p api-server --manifest-path server-rs\Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_API_BFF_CLOSURE_2026-05-01.md docs/technical/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md server-rs/crates/api-server/src/llm.rs server-rs/crates/api-server/src/custom_world_ai.rs
npm.cmd run api-server:maincloud
```
说明:`api-server:maincloud` 是常驻服务命令,验收时以启动后 `/healthz` 探测 `200` 作为成功证据。
## 5. 验证结果
本轮已执行:
```powershell
cargo fmt --all --check --manifest-path server-rs\Cargo.toml
cargo test -p api-server llm --manifest-path server-rs\Cargo.toml
cargo test -p api-server custom_world_ai --manifest-path server-rs\Cargo.toml
cargo test -p api-server runtime_story_legacy_routes_are_not_mounted --manifest-path server-rs\Cargo.toml
cargo test -p api-server --manifest-path server-rs\Cargo.toml
cargo test -p module-runtime-story --manifest-path server-rs\Cargo.toml
cargo check --workspace --manifest-path server-rs\Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_API_BFF_CLOSURE_2026-05-01.md docs/technical/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md server-rs/crates/api-server/src/llm.rs server-rs/crates/api-server/src/custom_world_ai.rs server-rs/crates/module-runtime-story/src/lib.rs server-rs/crates/module-runtime-story/src/bootstrap.rs server-rs/crates/module-runtime-story/src/session_action.rs
npm.cmd run api-server:maincloud
```
结果:通过。`api-server` 全量 203 个测试通过、4 个真实外部服务测试按预期 ignored`module-runtime-story` 11 个测试通过workspace 编译通过DDD 边界检查通过 15 个 module crate编码检查通过 8 个文件。`api-server:maincloud` 启动后 `GET http://127.0.0.1:3100/healthz` 返回 `200 {"ok":true,"service":"genarrative-api-server"}`,本轮 smoke 未留下常驻进程。启动日志中 Maincloud WebSocket 恢复认证快照短暂返回 `503 Service Unavailable`,未阻止 API server 就绪,属于外部 Maincloud 可用性告警。
本次为完成全量验证,还补齐了 `module-runtime-story` 测试编译门槛:
1. `module-runtime-story/src/lib.rs` 提高 `json!` 宏展开递归上限,避免开局快照大 JSON 构造在测试编译时触发 recursion limit。
2. `StoryRuntimeActionResolveOutput``Debug` derive满足 `expect_err` 测试约束。
3. 移除 `bootstrap.rs``session_action.rs` 的未用 import减少验证噪声。

View File

@@ -0,0 +1,40 @@
# WP-AS Assets 资产主链收尾记录2026-05-01
## 1. 收尾目标
关闭 `WP-AS Assets` 当前主链资产对象确认、实体槽位绑定、历史读取、OSS 对象确认、API facade、SpacetimeDB adapter 和订阅事件需要形成同一条后端真相链。
收尾后,`WP-AS` 不再以“资产 API/OSS/facade/event table 尚未全链收口”的口径挂起。后续 `asset_job``asset_manifest`、专业生成任务表和旧兼容入口物理删除,按新增需求或 `WP-DEL/WP-V` 另开切片。
## 2. 已完成内容
1. `module-assets` 补齐资产领域事件:`AssetDomainEvent``AssetObjectConfirmedEvent``AssetEntityBindingChangedEvent`
2. `spacetime-module` 新增 `asset_event` public event table记录 `ObjectConfirmed``EntityBindingChanged` 两类轻量事实。
3. `confirm_asset_object*` 成功 upsert `asset_object` 后写入对象确认事件;`bind_asset_object_to_entity*` 成功 upsert `asset_entity_binding` 后写入绑定变更事件。
4. `migration.rs` 已纳入 `asset_event``SPACETIMEDB_TABLE_CATALOG.md` 已补齐资产事件表结构、索引和查询模板。
5. Rust `module_bindings` 已补齐 `asset_event_type.rs``asset_event_kind_type.rs``asset_event_table.rs``mod.rs` 事件 diff 接线。
6. `module-assets/README.md` 已更新为当前状态资产对象、绑定、历史、OSS 确认、API facade 和 event table 已闭环,生成任务和 manifest 仍是后续扩展。
## 3. 边界说明
1. 正式资产状态仍以 `asset_object``asset_entity_binding` 为准;`asset_event` 只用于订阅端、BFF 和审计流程感知主链变化。
2. OSS 上传、HEAD Object、读代理和平台错误仍由 `api-server + platform-oss` 承接,领域 crate 不直接触碰网络副作用。
3. 本次不新增 `asset_job``asset_manifest` 或角色/动作/场景专用资产生成表。
4.`/generated-*` 读兼容入口暂不物理删除,后续归 `WP-DEL/WP-V` 统一处理。
## 4. 验收命令
```powershell
spacetime generate --no-config --lang rust --out-dir C:\g\rs --module-path server-rs\crates\spacetime-module --yes
cargo fmt -p module-assets -p spacetime-module --manifest-path server-rs\Cargo.toml --check
cargo test -p module-assets --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml --target wasm32-unknown-unknown
cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml
cargo check -p api-server --manifest-path server-rs\Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_AS_ASSET_CHAIN_CLOSURE_2026-05-01.md docs/technical/SPACETIMEDB_TABLE_CATALOG.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md docs/technical/README.md server-rs/crates/module-assets/README.md server-rs/crates/module-assets/src/events.rs server-rs/crates/module-assets/src/lib.rs server-rs/crates/spacetime-module/src/asset_metadata/objects.rs server-rs/crates/spacetime-module/src/asset_metadata/bindings.rs server-rs/crates/spacetime-module/src/migration.rs server-rs/crates/spacetime-client/src/module_bindings/mod.rs server-rs/crates/spacetime-client/src/module_bindings/asset_event_kind_type.rs server-rs/crates/spacetime-client/src/module_bindings/asset_event_type.rs server-rs/crates/spacetime-client/src/module_bindings/asset_event_table.rs
npm.cmd run api-server:maincloud
```
执行结果以全局任务清单的本次记录为准。

View File

@@ -0,0 +1,98 @@
# WP-CW Custom World 全链收尾
日期:`2026-05-01`
## 1. 收尾目标
本次收尾关闭 `SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md``WP-CW Custom World` 的剩余主链漂移:
1. `module-custom-world` 已完成 DDD 物理拆分,本次不再重拆领域 crate。
2. `spacetime-module/src/custom_world/*` 已完成 Agent action 确定性状态编排,本次不改表结构、不改 reducer/procedure 对外名称。
3. `spacetime-client/src/custom_world.rs` 已具备 profile、gallery、agent session、message、action、operation、card detail、works facade本次只把 API 和前端主入口对齐到这些稳定 facade。
4. `api-server` 中 Custom World 图片链路测试必须稳定,不允许因本机 `.env.local` 中真实 DashScope Key 让“缺配置”测试变成真实上游调用。
5. 前端 RPG 创作资产 client 统一使用 `/api/runtime/custom-world/*` 主链,旧 `/api/custom-world/*` 仅作为过渡入口保留给历史调用。
## 2. 主链路由
Authenticated 创作链路:
1. `POST /api/runtime/custom-world/profile`
2. `GET /api/runtime/custom-world-library`
3. `GET /api/runtime/custom-world-library/{profile_id}`
4. `PUT /api/runtime/custom-world-library/{profile_id}`
5. `DELETE /api/runtime/custom-world-library/{profile_id}`
6. `POST /api/runtime/custom-world-library/{profile_id}/publish`
7. `POST /api/runtime/custom-world-library/{profile_id}/unpublish`
8. `POST /api/runtime/custom-world/agent/sessions`
9. `GET /api/runtime/custom-world/agent/sessions/{session_id}`
10. `DELETE /api/runtime/custom-world/agent/sessions/{session_id}`
11. `GET /api/runtime/custom-world/agent/sessions/{session_id}/result-view`
12. `GET /api/runtime/custom-world/works`
13. `GET /api/runtime/custom-world/agent/sessions/{session_id}/cards/{card_id}`
14. `POST /api/runtime/custom-world/agent/sessions/{session_id}/messages`
15. `POST /api/runtime/custom-world/agent/sessions/{session_id}/messages/stream`
16. `POST /api/runtime/custom-world/agent/sessions/{session_id}/actions`
17. `GET /api/runtime/custom-world/agent/sessions/{session_id}/operations/{operation_id}`
18. `POST /api/runtime/custom-world/entity`
19. `POST /api/runtime/custom-world/scene-npc`
20. `POST /api/runtime/custom-world/scene-image`
21. `POST /api/runtime/custom-world/cover-image`
22. `POST /api/runtime/custom-world/cover-upload`
Public gallery 链路:
1. `GET /api/runtime/custom-world-gallery`
2. `GET /api/runtime/custom-world-gallery/{owner_user_id}/{profile_id}`
3. `GET /api/runtime/custom-world-gallery/by-code/{code}`
## 3. 兼容入口边界
以下旧入口本次不物理删除,避免影响历史编辑器、旧测试或外部草稿工具,但 RPG 创作主 client 不再使用它们:
1. `POST /api/custom-world/entity`
2. `POST /api/custom-world/scene-npc`
3. `POST /api/custom-world/scene-image`
4. `POST /api/custom-world/cover-image`
5. `POST /api/custom-world/cover-upload`
旧入口只转到同一批 Axum handler不拥有独立业务规则。
## 4. 资产与外部副作用边界
1. LLM、DashScope 图片生成、OSS 上传仍位于 `api-server + platform-*`,不进入 reducer/procedure。
2. 成功生成或上传后的资产对象确认、绑定通过 `spacetime-client` 调用 `spacetime-module`,不由前端自行维护真相。
3. DashScope 缺配置返回 `SERVICE_UNAVAILABLE`;上游请求已发出但失败返回 `UPSTREAM_ERROR`
4. 测试中验证“缺配置”必须显式构造无 key 配置,避免受开发机 `.env.local` 影响。
## 5. 验收
本次收尾后至少执行:
```powershell
cargo test -p module-custom-world --manifest-path server-rs\Cargo.toml
cargo test -p api-server custom_world --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml
npm.cmd run test -- src/services/rpg-creation/rpgCreationGenerationClient.test.ts src/services/rpg-creation/rpgCreationGenerationClient.node.test.ts src/services/rpg-creation/rpgCreationAssetClient.test.ts
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_CW_FULL_CHAIN_CLOSURE_2026-05-01.md
```
若本机 Maincloud 常驻服务导致 `npm.cmd run api-server:maincloud` 超时或端口占用,记录 `healthz` 探测结果即可,不改用旧后端重启命令。
## 6. 本轮验证结果
已通过:
1. `cargo test -p module-custom-world --manifest-path server-rs\Cargo.toml`13 个单元测试通过。
2. `cargo test -p api-server custom_world --manifest-path server-rs\Cargo.toml`30 个 Custom World 定向测试通过。
3. `cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml`:通过。
4. `cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml`:通过。
5. `npm.cmd run test -- src/services/rpg-creation/rpgCreationGenerationClient.test.ts src/services/rpg-creation/rpgCreationGenerationClient.node.test.ts src/services/rpg-creation/rpgCreationAssetClient.test.ts`3 个测试文件 7 个用例通过。
6. `npm.cmd run check:server-rs-ddd`15 个 module crate 边界检查通过。
7. `npm.cmd run check:encoding -- docs/technical/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md docs/technical/SERVER_RS_DDD_WP_CW_FULL_CHAIN_CLOSURE_2026-05-01.md server-rs/crates/api-server/src/custom_world_ai.rs src/services/rpg-creation/rpgCreationAssetClient.ts src/services/rpg-creation/rpgCreationAssetClient.test.ts`6 个文件通过。
8. `npm.cmd run api-server:maincloud`:按常驻服务形态在工具侧超时;超时前 `/healthz` 返回 `200``{"ok":true,"service":"genarrative-api-server"}`,随后已清理本次 smoke 启动留下的 `api-server``cargo``node` 子进程并确认 `3100` 端口释放。
已知非本轮阻塞:
1. `cargo fmt --all --check --manifest-path server-rs\Cargo.toml` 仍被当前工作区非 WP-CW 的 `module-story` 格式漂移阻塞;本轮 `api-server` 包格式检查已通过。

View File

@@ -0,0 +1,80 @@
# WP-DEL 旧层删除与命名收口
日期2026-05-01
## 目标
本轮 `WP-DEL` 只处理已被新主链完全替代的旧 HTTP contract、旧 route、旧前端 alias 和旧测试夹具,不新增业务能力,不恢复 `server-node` 兼容,不修改 SpacetimeDB 表结构。
## 已删除范围
1. Runtime Story 旧 HTTP DTO
- Rust `RuntimeStoryStateResolveRequest`
- Rust/TS `RuntimeStoryBootstrapRequest/Response`
- Rust/TS `RuntimeStoryActionResponse`
- TS 泛型 `RuntimeActionRequest/Response`
-`RuntimeStorySnapshotPayload` 只作为旧 HTTP 总入口快照结构删除;当前 story session scoped 写侧使用 `StoryRuntimeMutationResponse.projection`
2. Runtime Story 前端旧命名:
- `beginRpgRuntimeStorySession`
- `resolveRpgRuntimeStoryAction`
- `getRpgStoryRuntimeProjection`
- `getRpgRuntimeActionSnapshot`
- `rpgRuntimeStoryClient` 聚合对象
- 其他 `Rpg*RuntimeStory*` alias 统一删除,前端只导出当前主链命名。
3. Custom World 旧非 runtime 前缀路由:
- `POST /api/custom-world/entity`
- `POST /api/custom-world/scene-npc`
- `POST /api/custom-world/scene-image`
- `POST /api/custom-world/cover-image`
- `POST /api/custom-world/cover-upload`
- 当前主链固定为 `/api/runtime/custom-world/*`
4. Puzzle 旧本地下一关入口:
- `POST /api/runtime/puzzle/runs/local-next-level`
- Rust/TS `AdvanceLocalPuzzleNextLevelRequest`
- API 层本地 next-level 拼装 helper 与对应旧测试。
5. `/generated-*` 资产直读代理:
- `GET /generated-character-drafts/{*path}`
- `GET /generated-characters/{*path}`
- `GET /generated-animations/{*path}`
- `GET /generated-big-fish-assets/{*path}`
- `GET /generated-puzzle-assets/{*path}`
- `GET /generated-custom-world-scenes/{*path}`
- `GET /generated-custom-world-covers/{*path}`
- `GET /generated-qwen-sprites/{*path}`
-`api-server` 同源代理模块 `legacy_generated_assets.rs`
- Vite 与发布静态服务器中的 `/generated-*` 直读转发配置。
## 保留边界
1. `/generated-*` 字符串仍可作为历史 DTO 与测试夹具里的 `legacyPublicPath` 标识OSS object key 前缀和 `LegacyAssetPrefix` 继续保留但浏览器、Vite、本地发布静态服务器和 `api-server` 不再提供 `/generated-*` 裸读入口。
2. 正式读取契约固定为 `asset_object``GET /api/assets/read-url``ResolvedAssetImage/useResolvedAssetReadUrl` 或业务投影里的签名读 URL 字段。
3. `RuntimeStoryActionRequest` 仍保留为 `module-runtime-story` 内部动作规则输入,不作为旧 HTTP route contract。
4. `RuntimeStoryViewModel``RuntimeStoryPresentation``RuntimeStoryPatch` 和 battle presentation 仍作为当前投影/表现构件保留,不再代表旧 `/api/runtime/story/*` 总入口响应。
5. 历史设计文档中对旧 route/DTO 的引用只作为时间点记录保留,不批量改写旧档案。
## 验收门禁
计划执行:
```powershell
cargo fmt --all --manifest-path server-rs\Cargo.toml
cargo fmt --all --check --manifest-path server-rs\Cargo.toml
npm.cmd run check:server-rs-ddd
cargo check --workspace --manifest-path server-rs\Cargo.toml
cargo test --workspace --exclude spacetime-module --manifest-path server-rs\Cargo.toml
npm.cmd run test -- src/services/rpg-runtime/rpgRuntimeStoryClient.test.ts src/hooks/rpg-runtime-story/runtimeStoryCoordinator.test.ts src/services/ai.test.ts src/services/puzzle-runtime
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_DEL_CLEANUP_2026-05-01.md docs/technical/README.md docs/technical/SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md server-rs/crates/shared-contracts/README.md server-rs/crates/api-server/README.md server-rs/crates/module-puzzle/README.md
```
实际结果:
1. `cargo fmt --all --manifest-path server-rs\Cargo.toml`:通过。
2. `cargo fmt --all --check --manifest-path server-rs\Cargo.toml`:通过。
3. `npm.cmd run check:server-rs-ddd`通过15 个 module crate 边界检查通过。
4. `cargo check --workspace --manifest-path server-rs\Cargo.toml`:通过;仅保留既有 runtime chat prompt dead code warning。
5. `cargo test --workspace --exclude spacetime-module --manifest-path server-rs\Cargo.toml`:通过;`api-server` 207 个测试通过、4 个 live/外部依赖测试按既有条件忽略。
6. `npm.cmd run test -- src/services/rpg-runtime/rpgRuntimeStoryClient.test.ts src/hooks/rpg-runtime-story/runtimeStoryCoordinator.test.ts src/services/ai.test.ts src/services/puzzle-runtime`通过4 个测试文件 45 个用例通过。
7. `npm.cmd run check:encoding`:通过,全量 2816 个文件通过 UTF-8/乱码检查。
8. `npm.cmd run spacetime:generate -- --rust-only`通过SpacetimeDB CLI 在 Windows 长路径格式化阶段触发已知失败后,项目脚本使用短路径分批 `rustfmt` fallback 完成。
9. `cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml`:通过。
10. `npm.cmd run api-server:maincloud`:通过;后端拉起后 `GET http://127.0.0.1:3100/healthz` 返回 `200 {"ok":true,"service":"genarrative-api-server"}`,随后已清理本次 smoke 进程并确认 3100 端口释放。启动期 Maincloud 订阅恢复出现两条 `503 Service Unavailable` 警告,不影响 healthz smoke。

View File

@@ -13,12 +13,14 @@
- `npcUi/characterChatUi/inventoryUi/battleRewardUi/questUi/npcChatQuestOfferUi/goalUi` mock 对齐当前 hooks 暴露的稳定 UI 对象形状。 - `npcUi/characterChatUi/inventoryUi/battleRewardUi/questUi/npcChatQuestOfferUi/goalUi` mock 对齐当前 hooks 暴露的稳定 UI 对象形状。
3. 保持 `RpgRuntimeShell` 正式组件行为不变,只消除测试夹具对旧组件接线形状的依赖。 3. 保持 `RpgRuntimeShell` 正式组件行为不变,只消除测试夹具对旧组件接线形状的依赖。
## 边界 ## 2026-05-01 收尾切片
1. 本次不改 `src/services/**``src/hooks/**` 或任何后端接口。 `WP-FE-S/H` 写侧切到新 story session scoped 主链后,本切片只确认组件层夹具继续消费 hooks 出口,不在组件中补业务规则:
2. 本次不迁移 `beginRuntimeStorySession``resolveRuntimeStoryAction` 等尚未稳定的新写接口。
3. 本次不新增 UI 文案,不改组件视觉布局 1. `RpgRuntimeShell` 正式组件不拼接 `/api/runtime/story/*``/api/story/*` 路径
4. 本次不删除旧 runtime story contract 或旧 client alias这些仍等待 `WP-DEL` 串行收口 2. 组件测试 mock 继续对齐 `hydratedSnapshot/nextStory` 与当前 UI 对象形状
3. 不新增 UI 文案,不改视觉布局;服务端动作表现差异只通过 hooks 状态进入组件。
4. 旧 runtime story contract 和旧 alias 仍由 `WP-DEL` 统一删除。
## 验收 ## 验收

View File

@@ -13,13 +13,20 @@
5. 更新 `runtimeStoryCoordinator.test.ts` mock 命名,确保 hooks 测试明确断言读取侧走 projection client。 5. 更新 `runtimeStoryCoordinator.test.ts` mock 命名,确保 hooks 测试明确断言读取侧走 projection client。
6. 补齐 `src/hooks/useGameFlow.customWorld.test.tsx``beginRpgRuntimeStorySession` 测试桩,避免 hooks 全量测试在 Node 环境直接 fetch 相对路径,同时保持自定义世界开局 hooks 仍消费服务端快照。 6. 补齐 `src/hooks/useGameFlow.customWorld.test.tsx``beginRpgRuntimeStorySession` 测试桩,避免 hooks 全量测试在 Node 环境直接 fetch 相对路径,同时保持自定义世界开局 hooks 仍消费服务端快照。
## 边界 ## 2026-05-01 收尾切片
1. 本次不迁移 `beginRuntimeStorySession`,因为新开局接口尚未返回可直接进入游戏的完整 `GameState` 快照。 后端补齐 `/api/story/sessions/runtime``/api/story/sessions/{storySessionId}/actions/resolve`hooks 写侧同步收口:
2. 本次不迁移 `resolveRuntimeStoryAction`,因为完整动作结算的新 session scoped 写接口尚未稳定。
3. 本次不改 components不新增 UI 文案,不在组件层拼 API 请求 1. `resolveServerRuntimeChoice` 不再调用旧 `/api/runtime/story/actions/resolve` client
4. 本次不改 `api-server``spacetime-client``spacetime-module` 或共享契约 2. 正式动作只提交 `storySessionId/functionId/actionText/payload`,并从新 `StoryRuntimeProjectionResponse.gameState` 水合快照
5. 本次不删除旧 runtime story contract、旧 client alias 或兼容测试;这些仍等待 `WP-DEL` 串行收口 3. `resumeServerRuntimeStory` 继续以服务端投影刷新 story/runtime 版本,同时保留本地 UI 临时态职责
4. 组件层仍不拼 API不新增规则说明文案`WP-FE-C` 只消费 hooks 返回的 `hydratedSnapshot/nextStory`
边界:
1. 本次保留旧 client alias 名称,避免大面积 import churn行为已经切到新主链。
2. battle presentation 的逐帧表现只在响应仍含 presentation 时触发;新 projection 写侧先提交最终快照和故事,后续战斗表现由 story battle 专用接口继续增强。
3. 旧 runtime story contract 的物理删除仍属于 `WP-DEL`
## 验收 ## 验收

View File

@@ -32,26 +32,27 @@ GET /api/story/sessions/{storySessionId}/runtime-projection
6. `src/services/rpg-runtime/index.ts` 已导出新 client 函数与结果类型,供后续 `WP-FE-H` 迁 hook 时直接接入。 6. `src/services/rpg-runtime/index.ts` 已导出新 client 函数与结果类型,供后续 `WP-FE-H` 迁 hook 时直接接入。
7. 为满足 `WP-FE-S``src/services` 全量验收,补回 `src/services/customWorldAgentGenerationProgress.ts` 缺失的“建立场景连接”阶段,使草稿生成进度重新对齐既有 13 步文档与测试口径。 7. 为满足 `WP-FE-S``src/services` 全量验收,补回 `src/services/customWorldAgentGenerationProgress.ts` 缺失的“建立场景连接”阶段,使草稿生成进度重新对齐既有 13 步文档与测试口径。
## 未完成边界 ## 2026-05-01 收尾切片
暂未迁移 本次收尾把先前“等待后端写接口”的缺口正式关闭,执行口径如下
1. `beginRuntimeStorySession` 仍调用旧 `/api/runtime/story/sessions`,因为当前新 `/api/story/sessions` 只创建 story session不返回前端开局所需的完整 `GameState` 快照。新 `beginStorySession` 已作为稳定 client 先行提供hook 是否切换等待后端开局投影组合稳定 1. 不恢复旧 `/api/runtime/story/*` compat route前端 runtime story 开局、动作结算和读取统一走 `/api/story/sessions*`
2. `resolveRuntimeStoryAction` 仍调用旧 `/api/runtime/story/actions/resolve`,因为当前新主链尚未提供等价的 session scoped 动作结算接口与快照回包。新 `continueStorySession` 只覆盖 story event 续写,不等价于完整 runtime action settle 2. `/api/story/sessions/runtime` 作为新开局 BFF后端生成 `runtimeSessionId/storySessionId`,写入 SpacetimeDB `runtime_snapshot`,并返回可水合的 runtime projection
3. hooks/components 不在本轮大改;`WP-FE-H` 等后端写接口稳定后再迁 3. `/api/story/sessions/{storySessionId}/actions/resolve` 作为 session scoped 动作入口:前端只提交 function id、动作文案和 payload后端基于已持久化 snapshot 更新 `currentStory/storyHistory/runtimeActionVersion` 后返回 projection
4. `StoryRuntimeProjectionResponse` 补齐 `gameState`,让前端从后端投影水合 `HydratedSavedGameSnapshot`,不再消费旧 `snapshot/viewModel/presentation/patches` 组合。
5. `beginRuntimeStorySession``resolveRuntimeStoryAction` 保留前端导出名以减少调用面震荡,但实现已切到新 story session scoped 主链。
## 后续依赖 收尾后的旧层边界:
1. `WP-API/WP-RS` 需要提供新的 story session scoped 开局接口,返回可直接进入游戏的 `GameState` 或明确的前端投影组合 1. `packages/shared/src/contracts/rpgRuntimeStoryState.ts` 中的 view model / presentation / patch 类型暂不物理删除,留给 `WP-DEL` 统一清理
2. `WP-API/WP-RS` 需要提供新的 story session scoped 动作结算接口,返回新投影及必要的状态更新结果 2. 后续更完整的 battle/forge/NPC/quest 跨域结算仍由 `WP-RS/WP-ST/WP-SC/WP-API` 增量增强,但前端不再回退旧 runtime story 写路径
3. 上述接口稳定后,`WP-FE-S` 再删除旧 `/api/runtime/story/*` 写接口调用,`WP-FE-H` 迁 hooks最后由 `WP-DEL` 物理删除旧 runtime story contract 与 compat 测试。
## 验收命令 ## 验收命令
```powershell ```powershell
npm.cmd run test -- src/services/rpg-runtime/rpgRuntimeStoryClient.test.ts npm.cmd run test -- src/services/rpg-runtime/rpgRuntimeStoryClient.test.ts
npm.cmd run test -- src/hooks/rpg-runtime-story/runtimeStoryCoordinator.test.ts npm.cmd run test -- src/hooks/rpg-runtime-story/runtimeStoryCoordinator.test.ts
npm.cmd run test -- src/services/customWorldAgentGenerationProgress.test.ts npm.cmd run test -- src/hooks/rpg-runtime-story/storyChoiceRuntime.test.ts
npm.cmd run test -- src/services npm.cmd run test -- src/components/rpg-runtime-shell/RpgRuntimeShell.test.tsx
npm.cmd run check:encoding -- packages/shared/src/contracts/story.ts src/services/rpg-runtime/rpgRuntimeStoryClient.ts src/services/rpg-runtime/index.ts src/services/customWorldAgentGenerationProgress.ts src/services/customWorldAgentGenerationProgress.test.ts src/hooks/rpg-runtime-story/rpgRuntimeStoryGateway.ts src/hooks/rpg-runtime-story/inventoryActions.ts src/persistence/runtimeSnapshot.ts src/persistence/runtimeSnapshotTypes.ts src/types/game.ts docs/technical/SERVER_RS_DDD_WP_FE_S_RPG_RUNTIME_STORY_CLIENT_MIGRATION_2026-04-29.md npm.cmd run check:encoding -- packages/shared/src/contracts/story.ts src/services/rpg-runtime/rpgRuntimeStoryClient.ts src/services/rpg-runtime/index.ts src/hooks/rpg-runtime-story/rpgRuntimeStoryGateway.ts src/hooks/rpg-runtime-story/inventoryActions.ts src/hooks/rpg-runtime-story/storyChoiceRuntime.ts src/hooks/rpg-runtime-story/runtimeStoryCoordinator.test.ts docs/technical/SERVER_RS_DDD_WP_FE_S_RPG_RUNTIME_STORY_CLIENT_MIGRATION_2026-04-29.md
``` ```

View File

@@ -0,0 +1,76 @@
# WP-PZ Puzzle 运行态后端真相源收尾
日期:`2026-05-01`
## 1. 收尾目标
本轮收尾只关闭正式平台入口的 Puzzle 运行态链路,不扩大到 `/puzzle` 调试直达页和后续旧接口物理删除。
必须达成:
1. 平台内从作品详情或结果页进入拼图玩法时,开局、交换、拖动、通关排行榜和下一关全部调用 Rust API再由 `spacetime-client` 进入 SpacetimeDB procedure。
2. `PlatformEntryFlowShellImpl` 不再导入 `puzzleLocalRuntime`,不再在浏览器侧裁决棋盘、合并、拆分、通关或排行榜。
3. 结果页“试玩当前草稿”先把当前草稿轻量字段写回 `puzzle_work_profile`,再启动后端 run该预览 run 只允许草稿 owner 启动。
4. 发布作品的公开开局规则不放松:非 owner 只能启动已发布作品。
5. 本轮不新增表字段,不修改 `migration.rs`
## 2. 后端口径
`start_puzzle_run_tx` 继续以 `puzzle_work_profile` 作为入口 profile 真相源:
1. `Published` 作品维持既有公开可玩语义。
2. `Draft` 作品仅当 `owner_user_id` 与请求用户一致时可启动,用于结果页预览。
3. 草稿预览 run 不计入公开作品播放次数,也不写入 played work 记录。
4. 下一关推荐仍只从已发布 gallery 中选择候选,草稿只作为当前入口关卡,不参与公共推荐池。
5. 排行榜写入只面向已发布作品;草稿预览通关时后端返回当前 run 快照,不生成公开榜单记录。
## 3. 前端口径
正式平台入口只保留表现态:
1. `PuzzleRuntimeShell` 继续只接收后端 run snapshot 与回调。
2. 作品详情开局调用 `startPuzzleRun`
3. 结果页试玩调用 `updatePuzzleWork` 同步草稿,再调用 `startPuzzleRun` 启动 owner draft run。
4. 交换、拖动、下一关和排行榜分别调用 `swapPuzzlePieces``dragPuzzlePieceOrGroup``advancePuzzleNextLevel``submitPuzzleLeaderboard`
5. `/puzzle` 调试直达页仍可保留本地 runtime用于开发期视觉和交互调试它不再是正式平台链路。
## 4. 验收命令
本轮修改完成后至少执行:
1. `npm.cmd run check:encoding`
2. `npm.cmd run check:server-rs-ddd`
3. `cargo fmt --all --check --manifest-path server-rs\Cargo.toml`
4. `cargo test -p module-puzzle --manifest-path server-rs\Cargo.toml`
5. `cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml`
6. `cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml`
7. `cargo check -p shared-contracts --manifest-path server-rs\Cargo.toml`
8. `cargo check -p api-server --manifest-path server-rs\Cargo.toml`
9. `npm.cmd run typecheck`
10. `npm.cmd run api-server:maincloud`
若全量 workspace 测试受其他工作包既有失败影响,本轮只记录失败归属,不把 WP-PZ 已收口代码回退。
## 5. 本轮验证结果
已通过:
1. `npm.cmd run check:encoding`
2. `npm.cmd run check:server-rs-ddd`
3. `cargo test -p module-puzzle --manifest-path server-rs\Cargo.toml`
4. `cargo check -p module-puzzle --features spacetime-types --target wasm32-unknown-unknown --manifest-path server-rs\Cargo.toml`
5. `cargo check -p shared-contracts --manifest-path server-rs\Cargo.toml`
6. `cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml`
7. `cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml`
8. `cargo check -p api-server --manifest-path server-rs\Cargo.toml`
9. `npm.cmd run test -- src/components/puzzle-runtime src/services/puzzle-runtime`
10. `cargo fmt --manifest-path server-rs\Cargo.toml --package module-puzzle --check`
11. `cargo fmt --manifest-path server-rs\Cargo.toml --package spacetime-module --check`
12. `cargo fmt --manifest-path server-rs\Cargo.toml --package spacetime-client --check`
13. `cargo fmt --manifest-path server-rs\Cargo.toml --package api-server --check`
未作为本轮阻塞:
1. `cargo fmt --all --check --manifest-path server-rs\Cargo.toml` 当前会检查到非 WP-PZ 的 `module-runtime-story/src/bootstrap.rs``module-runtime-story/src/session_action.rs` 未跟踪新增文件格式差异。
2. `npm.cmd run typecheck` 当前失败在非 WP-PZ 文件:`src/data/sceneEncounterPreviews.ts``slot` 可能为 `undefined``src/services/ai.ts` 缺少 `hasMixedNarrativeLanguage``src/services/rpg-creation/rpgCreationAssetClient.test.ts` 的 Custom World scene image 请求字段不匹配。
3. `npm.cmd run api-server:maincloud` 已完成编译并尝试启动,但 Maincloud 连接出现 `503 Service Unavailable`,随后 `api-server.exe``0xffffffff` 退出;本地 `3100` 未留下可用监听,残留进程已确认清理。

View File

@@ -0,0 +1,38 @@
# WP-RPG Gameplay 全域收口记录2026-05-01
## 1. 收口目标
本次关闭 `SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md``WP-RPG Gameplay 域` 的剩余项:领域文件已完成物理拆分后,战斗胜利奖励、任务交付奖励、宝箱奖励仍由 `spacetime-module/src/gameplay/mod.rs` 直接拼装背包、成长和章节账本命令。
本次目标是把这些跨域组合规则收回 RPG 领域层,让 SpacetimeDB adapter 只负责事务内幂等检查、执行 mutation、写表和查询。
## 2. 已完成内容
1. `module-story` 新增 `RpgGameplaySettlementPlan`,作为 RPG story/gameplay 跨域结算计划。
2. `build_combat_victory_settlement_plan` 收口战斗胜利后的背包掉落、敌对经验和章节 hostile 账本计划。
3. `build_quest_turn_in_settlement_plan` 收口任务交付后的奖励物品、任务经验和章节 quest 账本计划。
4. `build_treasure_settlement_plan` 收口宝箱奖励到背包 mutation 的计划生成,并保持宝箱记录本身仍由 `module-runtime-item` 建模。
5. `spacetime-module/src/gameplay/mod.rs` 改为消费 `RpgGameplaySettlementPlan`,不再在 adapter 中重复映射战斗/任务奖励物品、稀有度、装备槽和物品来源。
6. `spacetime-module` 继续保留事务幂等检查、`inventory_slot` 写回、`player_progression` 经验发放和 `chapter_progression` 可选记账。
7. 未修改 SpacetimeDB 表结构、reducer/procedure 签名、绑定 shape 或 `migration.rs`
## 3. 边界说明
1. 本次不接 `WP-RS Runtime Story 去兼容层` 的完整动作写接口,不修改 `/api/story/*` 路由和前端 hooks。
2. 本次不新增 public/event table结算计划仍是纯领域对象由 adapter 转换为已有表写入。
3. `WP-ST/WP-SC/WP-API/WP-FE` 后续若继续做 runtime story 写侧,只能消费本次的领域计划或对应 facade不应在 BFF/前端重新拼奖励规则。
4. 章节计划不存在时仍不反向阻断战斗胜利或任务交付主链,只跳过章节账本写入。
## 4. 验证
已执行:
```powershell
cargo fmt -p module-story -p spacetime-module --manifest-path server-rs\Cargo.toml
cargo test -p module-story --manifest-path server-rs\Cargo.toml
cargo test -p module-combat -p module-inventory -p module-npc -p module-progression -p module-quest -p module-runtime-item -p module-story --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml
npm.cmd run check:server-rs-ddd
```
结果通过。RPG 七个领域 crate 共 49 个单元测试通过,`spacetime-module` 编译通过DDD 边界检查通过 15 个 module crate。

View File

@@ -2,7 +2,7 @@
## 背景 ## 背景
`WP-RS Runtime Story 去兼容层` 已完成 crate 迁名、旧 HTTP compat 路由下线、runtime projection 契约/API/读取侧迁移等多轮推进。当前仍存在一些 `compat` 或旧 `/api/runtime/story/*` 命名残留,需要区分“可以立即清理的工程语义残留”和“必须等待后端写接口稳定后再删除的运行依赖”。 `WP-RS Runtime Story 去兼容层` 已完成 crate 迁名、旧 HTTP compat 路由下线、runtime projection 契约/API/读取侧迁移等多轮推进。2026-05-01 收尾后,运行时开局和动作写侧也已迁到 story session scoped route当前仍存在一些 `compat` 或旧 `/api/runtime/story/*` 命名残留,需要区分“可以立即清理的工程语义残留”和“历史展示 DTO / 旧命名的物理删除窗口”。
本次切片只处理前者,并冻结后者的交接清单。 本次切片只处理前者,并冻结后者的交接清单。
@@ -29,22 +29,21 @@
1. 清理 `module-runtime-story` 运行代码注释中的 `compat` 定位,将 crate 口径改为 runtime story 主链纯规则收口。 1. 清理 `module-runtime-story` 运行代码注释中的 `compat` 定位,将 crate 口径改为 runtime story 主链纯规则收口。
2. 保留“历史 payload”相关说明因为这些字段仍是实际输入兼容范围不属于旧层命名。 2. 保留“历史 payload”相关说明因为这些字段仍是实际输入兼容范围不属于旧层命名。
3. 更新 `module-runtime-story/README.md`,明确后续要迁的是`/api/runtime/story/*` 写侧能力,而不是继续扩展兼容桥。 3. 更新 `module-runtime-story/README.md`,明确旧 `/api/runtime/story/*` 写侧能力已迁到 session scoped 新接口,后续不再扩展兼容桥。
## 残留清单 ## 残留清单
以下残留本次不删除: 以下残留本次不删除:
1. `src/services/rpg-runtime/rpgRuntimeStoryClient.ts` 仍指向 `/api/runtime/story`,原因是开局和动作写侧 session scoped 新接口尚未完成 1. `packages/shared/src/contracts/rpgRuntimeStoryState.ts``server-rs/crates/shared-contracts/src/runtime_story.rs` 仍保留历史 view model / presentation / patch DTO原因是 story battle 表现、历史测试和 `WP-DEL` 物理删除窗口仍需统一评估
2. `src/services/rpg-runtime/rpgRuntimeStoryClient.test.ts` 仍断言旧写接口路径,需等待新写接口和 service contract 稳定后由 `WP-FE-S` 修改 2. `src/hooks/rpg-runtime-story/**` 仍保留部分旧命名兼容注释,但运行写链路已经通过 `storySessionId` scoped client不再调用旧 `/api/runtime/story/*`
3. `packages/shared/src/contracts/rpgRuntimeStoryState.ts``server-rs/crates/shared-contracts/src/runtime_story.rs` 仍保留 `RuntimeStoryActionResponse`,原因是前端旧写侧仍消费该响应形状 3. 历史文档中仍会记录旧 compat 路由阶段,用于审计时间线;当前执行入口以 `SERVER_RS_DDD_WP_RS_RUNTIME_STORY_CLOSURE_2026-05-01.md` 为准
4. `src/hooks/rpg-runtime-story/**` 仍保留部分旧调用面兼容注释,需等 `WP-FE-H` 在 service 迁移后统一收口。
## 后续边界 ## 后续边界
1. 后端新写接口稳定前,不删除旧前端写 client 1. 不恢复旧前端写 client也不重新挂载旧 `/api/runtime/story/*`
2. `WP-FE-S` 先迁 service再由 `WP-FE-H` 迁 hooks最后 `WP-FE-C` 接组件 2. `WP-DEL` 只能在确认 story battle 等展示语义不再依赖旧 DTO 后,删除 `RuntimeStoryActionResponse` 等历史 contract
3. `WP-DEL` 只能在旧接口无运行引用后删除 `RuntimeStoryActionResponse` 等旧 contract 3. 历史文档清理只改当前状态说明,不抹掉已执行过的审计记录
## 验收 ## 验收

View File

@@ -0,0 +1,47 @@
# WP-RS Runtime Story 收尾设计
日期2026-05-01
## 目标
本轮收尾只关闭运行代码中的旧 `/api/runtime/story/*` 写链路,不恢复旧 compat route不兼容 `server-node`,也不新增 SpacetimeDB 表结构。
## 新路由
1. `POST /api/story/sessions/runtime`
- 用于前端进入运行时故事。
- 后端生成 `runtimeSessionId``storySessionId`,创建 story session写入 runtime snapshot。
- 响应返回 `StoryRuntimeMutationResponse { projection }`,其中 `projection.gameState` 是前端水合快照的唯一状态来源。
2. `POST /api/story/sessions/{story_session_id}/actions/resolve`
- 用于正式运行时选项结算。
- 路径中的 `story_session_id` 是唯一会话边界;请求体必须携带同一个 `storySessionId`,不再接受 `sessionId` 作为主键兜底。
- 后端读取 story session 与当前用户 runtime snapshot校验 snapshot 中 `runtimeSessionId` 必须匹配 story session。
- 后端调用 `module-runtime-story` 纯规则结算动作,推进 `runtimeActionVersion`,写回 runtime snapshot并用 `continue_story` 记录本轮 narrative event。
- 响应同样返回 `StoryRuntimeMutationResponse { projection }`,不返回旧 `viewModel / presentation / patches / snapshot` 组合。
## 契约收口
本轮新增 story contract 下的运行时写侧 DTO
1. `BeginStoryRuntimeSessionRequest`
2. `ResolveStoryRuntimeActionRequest`
3. `StoryRuntimeMutationResponse`
4. `StoryRuntimeProjectionResponse.gameState`
`StoryRuntimeBootstrapRequest/Response``StoryRuntimeActionRequest/Response` 已从 `packages/shared/src/contracts/story.ts``server-rs/crates/shared-contracts/src/story.rs` 移除。`runtime_story` 下的 view model、presentation、patch 类型暂作为历史展示 DTO 和 story battle 表现输入保留;它们不是旧 HTTP route。
## 不做事项
1. 不恢复 `/api/runtime/story/*`
2. 不新增或修改 SpacetimeDB table因此不改 `migration.rs`
3. 不把前端本地快照重新作为动作真相上传。
4. 不把 Node/Express/PostgreSQL 路径作为兼容目标。
## 完成判定
1. `src/services/rpg-runtime/rpgRuntimeStoryClient.ts` 不再包含 `/api/runtime/story`
2. 前端 `beginRpgRuntimeStorySession``resolveRpgRuntimeStoryAction` 只调用 `/api/story/*`
3. `server-rs/crates/api-server/src/app.rs` 仍保持旧 `/api/runtime/story/*` 未挂载测试。
4. 共享 story 写侧契约和测试不再引用旧 `StoryRuntimeBootstrapRequest/Response``StoryRuntimeActionResponse`
5. 文档进度从“旧写接口等待迁移”更新为“写链路已切到 story session scoped route”。

View File

@@ -4,7 +4,7 @@
`WP-SC Spacetime Client` 位于 `spacetime-module``api-server` 之间,只负责把 SpacetimeDB 生成绑定、procedure / reducer 调用、row snapshot 和错误语义收口成 BFF 可消费的 typed facade。 `WP-SC Spacetime Client` 位于 `spacetime-module``api-server` 之间,只负责把 SpacetimeDB 生成绑定、procedure / reducer 调用、row snapshot 和错误语义收口成 BFF 可消费的 typed facade。
当前 `spacetime-client` 已经具备连接池、生成绑定、多个领域 facade 和 mapper,但错误映射仍散落在各 facade 中,且 README 仍停留在早期占位说明。由于 `WP-ST` 尚未完成所有新 facade本次不预判新的 table、reducer、procedure 或 row shape先收口已存在调用层的基础设施 当前 `spacetime-client` 已经具备连接池、生成绑定、多个领域 facade 和 mapper。本文件用于冻结并关闭当前稳定 SpacetimeDB facade 范围内的 `WP-SC` 收口:不预判新的 table、reducer、procedure 或 row shape已存在调用层、错误 helper、mapper 与 README 状态闭环
## 2. 本次目标 ## 2. 本次目标
@@ -119,3 +119,36 @@ npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_SC_SPACETIME_CLIENT_REFACTOR_2026-04-29.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md server-rs/crates/spacetime-client/src/story_runtime.rs npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_SC_SPACETIME_CLIENT_REFACTOR_2026-04-29.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md server-rs/crates/spacetime-client/src/story_runtime.rs
npm.cmd run api-server:maincloud npm.cmd run api-server:maincloud
``` ```
## 9. 收尾关闭口径2026-05-01
当前稳定 facade 范围内,`WP-SC` 已完成:
1. `SpacetimeClientError` 统一 helper 补齐:
- `from_sdk_error`
- `validation_failed`
- `reducer_failed`
- `procedure_failed`
- `missing_snapshot`
2. reducer callback 的业务错误统一走 `reducer_failed`,保持 API 层可继续按 `Runtime` 分类映射为调用方输入或状态错误。
3. combat、inventory、runtime、story 等 facade 的本地命令构造 / 校验错误统一走 `validation_failed`,避免各文件继续散落 `Runtime(error.to_string())`
4. story runtime projection source 已接入 runtime inventory typed facade并已有定向测试覆盖 session guard、inventory guard、背包/装备覆盖和 option 解析。
5. `spacetime-client/README.md` 已从早期占位口径更新为当前完成口径,明确后续只随 `WP-ST` 新 facade 稳定后按领域增量接线。
关闭边界:
1. 本次不修改 `spacetime-module``migration.rs`、生成绑定、HTTP route、shared contract 或前端。
2. `WP-ST` 后续若新增或调整 SpacetimeDB facade需要另开对应领域切片继续补 typed facade / row mapper这不再阻塞当前 `WP-SC` 工作包关闭。
3. `WP-RS/WP-API/WP-FE/WP-DEL` 仍可因旧 runtime story 写侧和前端迁移保持进行中,但不应再把 `spacetime-client` 基础设施视为未完成。
最终验收命令:
```powershell
cargo fmt -p spacetime-client --manifest-path server-rs/Cargo.toml --check
cargo test -p spacetime-client --manifest-path server-rs/Cargo.toml
cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml
cargo check -p api-server --manifest-path server-rs/Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_SC_SPACETIME_CLIENT_REFACTOR_2026-04-29.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md docs/technical/README.md server-rs/crates/spacetime-client/README.md server-rs/crates/spacetime-client/src/lib.rs server-rs/crates/spacetime-client/src/ai.rs server-rs/crates/spacetime-client/src/combat.rs server-rs/crates/spacetime-client/src/inventory.rs server-rs/crates/spacetime-client/src/runtime.rs server-rs/crates/spacetime-client/src/story.rs
npm.cmd run api-server:maincloud
```

View File

@@ -0,0 +1,66 @@
# WP-ST SpacetimeDB Adapter 收尾记录2026-05-01
## 1. 收尾目标
本次关闭 `SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md``WP-ST SpacetimeDB Adapter` 当前稳定范围已经落地的表、reducer、procedure、event table 和上下文 adapter 必须完成 `migration.rs`、表目录、Rust bindings 与自动门禁闭环。
收尾后,`WP-ST` 不再以“多个上下文和绑定生成仍待推进”的口径挂起;后续只有新增 table / reducer / procedure 或 row shape 时,才按增量切片重新认领。
## 2. 已完成内容
1. `asset_event` 已作为 `public event` 表进入资产主链,覆盖资产对象确认和实体槽位绑定变更事件。
2. `asset_event` 已对齐 `migration.rs``SPACETIMEDB_TABLE_CATALOG.md` 和 Rust `module_bindings`
3. `SPACETIMEDB_TABLE_CATALOG.md` 补齐 `database_migration_operator`、邀请/推荐/会员/充值等 profile 表,以及 `asset_event` 的结构、索引和查询模板。
4. `scripts/check-server-rs-ddd-boundaries.mjs` 新增 SpacetimeDB 表漂移检查,自动核对 table accessor、`migration.rs` 白名单和表目录项,阻止新增表漏迁移或漏文档。
5. `scripts/generate-spacetime-bindings.mjs` 在 Windows 下继续先输出短临时目录;当 SpacetimeDB CLI 已生成文件但自身 formatter 失败时,由脚本分批 `rustfmt` 后再同步生成目录。
6. `spacetime-client/src/module_bindings` 已通过 `npm.cmd run spacetime:generate -- --rust-only` 重新生成,包含 `asset_event_table.rs``asset_event_type.rs``asset_event_kind_type.rs`
7. `spacetime-client/README.md` 已同步生成物维护口径:禁止手写 generated code格式化 fallback 只能由仓库生成脚本托管。
## 3. 边界说明
1. 本次收尾不把未稳定的新 story action 写接口、前端迁移或旧 compat 删除并入 `WP-ST`
2. `database_migration_operator` 是迁移权限表,本身不导出到业务迁移包;其他业务表必须纳入 `migration.rs`
3. `spacetime-client/src/module_bindings/**` 仍是生成物,不承载手写 facade 或领域规则。
4. 后续若新增 SpacetimeDB 表,必须同时更新表目录、`migration.rs`、绑定生成结果,并让 DDD 边界检查通过。
## 4. 验收命令
本轮收尾至少执行:
```powershell
npm.cmd run spacetime:generate -- --rust-only
cargo fmt --all --check --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding
npm.cmd run api-server:maincloud
```
执行结果以本次任务清单记录为准。
## 5. 本次执行结果
已执行并通过:
```powershell
npm.cmd run spacetime:generate -- --rust-only
cargo fmt --all --check --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml
cargo test -p module-assets --manifest-path server-rs\Cargo.toml
cargo test -p spacetime-client --manifest-path server-rs\Cargo.toml
cargo check -p api-server --manifest-path server-rs\Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding
npm.cmd run api-server:maincloud
```
结果:
1. Rust bindings 已生成并同步,`asset_event` 三个生成文件已落盘。
2. `spacetime-module``spacetime-client` 编译通过,`module-assets` 8 个测试通过,`spacetime-client` 10 个测试通过。
3. DDD 边界检查通过 15 个 module crate编码检查通过 2815 个文件。
4. `api-server:maincloud` 启动后 `/healthz` 返回 `200 {"ok":true,"service":"genarrative-api-server"}`,本次启动进程已清理并释放 `3100` 端口。
已知 warning`module-runtime-story``api-server` 仍有非本次 WP-ST 引入的未使用 helper / prompt warning后续按对应工作包收口不阻塞本次 WP-ST 关闭。

View File

@@ -22,15 +22,30 @@ spacetime sql <db> "SELECT * FROM custom_world_gallery_entry"
| 领域 | 表 | | 领域 | 表 |
| --- | --- | | --- | --- |
| 迁移权限 | `database_migration_operator` |
| 认证 | `auth_store_snapshot`, `user_account`, `auth_identity`, `refresh_session` | | 认证 | `auth_store_snapshot`, `user_account`, `auth_identity`, `refresh_session` |
| 运行时档案 | `runtime_setting`, `runtime_snapshot`, `user_browse_history`, `profile_dashboard_state`, `profile_wallet_ledger`, `profile_redeem_code`, `profile_redeem_code_usage`, `profile_played_world`, `profile_save_archive` | | 运行时档案 | `runtime_setting`, `runtime_snapshot`, `user_browse_history`, `profile_dashboard_state`, `profile_wallet_ledger`, `profile_redeem_code`, `profile_redeem_code_usage`, `profile_invite_code`, `profile_referral_relation`, `profile_played_world`, `profile_membership`, `profile_recharge_order`, `profile_save_archive` |
| RPG 运行时 | `story_session`, `story_event`, `npc_state`, `inventory_slot`, `battle_state`, `treasure_record`, `quest_record`, `quest_log`, `player_progression`, `chapter_progression` | | RPG 运行时 | `story_session`, `story_event`, `npc_state`, `inventory_slot`, `battle_state`, `treasure_record`, `quest_record`, `quest_log`, `player_progression`, `chapter_progression` |
| 世界创作 | `custom_world_profile`, `custom_world_session`, `custom_world_agent_session`, `custom_world_agent_message`, `custom_world_agent_operation`, `custom_world_draft_card`, `custom_world_gallery_entry` | | 世界创作 | `custom_world_profile`, `custom_world_session`, `custom_world_agent_session`, `custom_world_agent_message`, `custom_world_agent_operation`, `custom_world_draft_card`, `custom_world_gallery_entry` |
| 拼图 | `puzzle_agent_session`, `puzzle_agent_message`, `puzzle_work_profile`, `puzzle_event`, `puzzle_runtime_run`, `puzzle_leaderboard_entry` | | 拼图 | `puzzle_agent_session`, `puzzle_agent_message`, `puzzle_work_profile`, `puzzle_event`, `puzzle_runtime_run`, `puzzle_leaderboard_entry` |
| 大鱼吃小鱼 | `big_fish_creation_session`, `big_fish_agent_message`, `big_fish_asset_slot`, `big_fish_event`, `big_fish_runtime_run` | | 大鱼吃小鱼 | `big_fish_creation_session`, `big_fish_agent_message`, `big_fish_asset_slot`, `big_fish_event`, `big_fish_runtime_run` |
| 资产 | `asset_object`, `asset_entity_binding` | | 资产 | `asset_object`, `asset_entity_binding`, `asset_event` |
| AI 任务 | `ai_task`, `ai_task_stage`, `ai_text_chunk`, `ai_result_reference`, `ai_task_event` | | AI 任务 | `ai_task`, `ai_task_stage`, `ai_text_chunk`, `ai_result_reference`, `ai_task_event` |
## 迁移权限表
### `database_migration_operator`
- 作用:数据库迁移操作员授权表,用于限制导出、导入和增量导入等 private 表迁移能力,避免任意登录身份读取或覆盖全库真相。
- 结构:`operator_identity PK: Identity`, `created_at: Timestamp`, `created_by: Identity`, `note: String`
- 索引:主键 `operator_identity`
- 迁移说明:该表是迁移权限本身,不纳入 `migration.rs` 的业务表导出白名单。
```sql
SELECT * FROM database_migration_operator;
SELECT * FROM database_migration_operator WHERE operator_identity = '<identity>';
```
## 认证表 ## 认证表
### `auth_store_snapshot` ### `auth_store_snapshot`
@@ -154,6 +169,29 @@ SELECT * FROM profile_redeem_code_usage WHERE code = '<CODE>';
SELECT * FROM profile_redeem_code_usage WHERE user_id = '<user_id>'; SELECT * FROM profile_redeem_code_usage WHERE user_id = '<user_id>';
``` ```
### `profile_invite_code`
- 作用:用户邀请中心的邀请码主表,保存用户当前稳定邀请码。
- 结构:`user_id PK: String`, `invite_code: String`, `created_at: Timestamp`, `updated_at: Timestamp`
- 索引:主键 `user_id`,唯一索引 `invite_code`
```sql
SELECT * FROM profile_invite_code WHERE user_id = '<user_id>';
SELECT * FROM profile_invite_code WHERE invite_code = '<invite_code>';
```
### `profile_referral_relation`
- 作用:邀请关系表,记录被邀请人与邀请人之间的一次性绑定关系,以及双方奖励是否已发放。
- 结构:`invitee_user_id PK: String`, `inviter_user_id: String`, `invite_code: String`, `inviter_reward_granted: bool`, `invitee_reward_granted: bool`, `bound_at: Timestamp`
- 索引:`inviter_user_id`, `(inviter_user_id, bound_at)`
```sql
SELECT * FROM profile_referral_relation WHERE invitee_user_id = '<invitee_user_id>';
SELECT * FROM profile_referral_relation WHERE inviter_user_id = '<inviter_user_id>';
SELECT * FROM profile_referral_relation WHERE inviter_user_id = '<inviter_user_id>' ORDER BY bound_at DESC;
```
### `profile_played_world` ### `profile_played_world`
- 作用:记录用户玩过的世界及最后游玩时间,用于个人页历史和继续游戏入口。 - 作用:记录用户玩过的世界及最后游玩时间,用于个人页历史和继续游戏入口。
@@ -166,6 +204,27 @@ SELECT * FROM profile_played_world WHERE user_id = '<user_id>' AND world_key = '
SELECT * FROM profile_played_world WHERE user_id = '<user_id>' ORDER BY last_played_at DESC; SELECT * FROM profile_played_world WHERE user_id = '<user_id>' ORDER BY last_played_at DESC;
``` ```
### `profile_membership`
- 作用:用户会员状态表,保存会员状态、档位、起止时间和最近更新时间。
- 结构:`user_id PK: String`, `status: RuntimeProfileMembershipStatus`, `tier: RuntimeProfileMembershipTier`, `started_at: Timestamp`, `expires_at: Timestamp`, `updated_at: Timestamp`
- 索引:主键 `user_id`
```sql
SELECT * FROM profile_membership WHERE user_id = '<user_id>';
```
### `profile_recharge_order`
- 作用:充值订单表,记录用户购买叙世币或会员的订单、支付渠道、支付时间、积分变更和会员到期时间。
- 结构:`order_id PK: String`, `user_id: String`, `product_id: String`, `product_title: String`, `kind: RuntimeProfileRechargeProductKind`, `amount_cents: u64`, `status: RuntimeProfileRechargeOrderStatus`, `payment_channel: String`, `paid_at: Timestamp`, `created_at: Timestamp`, `points_delta: i64`, `membership_expires_at: Option<Timestamp>`
- 索引:`user_id`, `(user_id, created_at)`
```sql
SELECT * FROM profile_recharge_order WHERE order_id = '<order_id>';
SELECT * FROM profile_recharge_order WHERE user_id = '<user_id>' ORDER BY created_at DESC;
```
### `profile_save_archive` ### `profile_save_archive`
- 作用:用户存档列表,保存世界信息、封面、当前状态 JSON 和剧情 JSON。 - 作用:用户存档列表,保存世界信息、封面、当前状态 JSON 和剧情 JSON。
@@ -537,6 +596,19 @@ SELECT * FROM asset_entity_binding WHERE entity_kind = '<entity_kind>' AND entit
SELECT * FROM asset_entity_binding WHERE asset_object_id = '<asset_object_id>'; SELECT * FROM asset_entity_binding WHERE asset_object_id = '<asset_object_id>';
``` ```
### `asset_event`
- 作用资产事件表目前记录对象确认和实体槽位绑定变更事实供订阅端、BFF 或审计流程感知资产主链变化;正式资产状态仍以 `asset_object``asset_entity_binding` 为准。
- 可见性:`public event`
- 结构:`event_id PK: String`, `asset_object_id: String`, `binding_id: Option<String>`, `event_kind: AssetEventKind`, `asset_kind: String`, `owner_user_id: Option<String>`, `profile_id: Option<String>`, `entity_kind: Option<String>`, `entity_id: Option<String>`, `slot: Option<String>`, `occurred_at: Timestamp`
- 索引:`asset_object_id`, `owner_user_id`, `profile_id`
```sql
SELECT * FROM asset_event WHERE asset_object_id = '<asset_object_id>' ORDER BY occurred_at ASC;
SELECT * FROM asset_event WHERE owner_user_id = '<user_id>' ORDER BY occurred_at DESC;
SELECT * FROM asset_event WHERE profile_id = '<profile_id>' ORDER BY occurred_at DESC;
```
## AI 任务表 ## AI 任务表
### `ai_task` ### `ai_task`
@@ -599,5 +671,6 @@ SELECT * FROM ai_task_event WHERE owner_user_id = '<user_id>' ORDER BY occurred_
## 当前维护风险 ## 当前维护风险
- `story_session``story_event``npc_state``inventory_slot``battle_state``treasure_record``quest_record``quest_log``player_progression``chapter_progression``src/lib.rs``src/gameplay/mod.rs` 都能看到表定义。当前编译入口以 `src/lib.rs` 为准;后续完成拆分时,需要删除重复定义或正式挂载子模块,并同步更新本文 - `scripts/check-server-rs-ddd-boundaries.mjs` 已检查 `spacetime-module/src/**` 中的表定义、`migration.rs` 白名单和本文目录项是否一致;新增或删除表后必须先让该检查通过
- `custom_world/*` 子模块中也保留了一份表骨架;当前生成绑定与 `src/lib.rs` 对齐,包含 `public_work_code``author_public_user_code``custom_world_gallery_entry.public_work_code` 索引。维护时不要只看子模块文件。 - `database_migration_operator` 是迁移权限表,不导出到业务迁移包;除此之外的业务表都必须进入 `migration.rs`
- `spacetime-client/src/module_bindings/**` 是生成物表、reducer 或 procedure 发生 shape 变化后必须通过 `npm.cmd run spacetime:generate -- --rust-only` 或对应 SpacetimeDB CLI 流程重新生成,不要手写修改。

View File

@@ -70,11 +70,6 @@ export interface StartPuzzleRunRequest {
profileId: string; profileId: string;
} }
export interface AdvanceLocalPuzzleNextLevelRequest {
run: PuzzleRunSnapshot;
sourceSessionId?: string | null;
}
export interface PuzzleRunResponse { export interface PuzzleRunResponse {
run: PuzzleRunSnapshot; run: PuzzleRunSnapshot;
} }

View File

@@ -6,9 +6,8 @@ import {
SERVER_RUNTIME_FUNCTION_IDS, SERVER_RUNTIME_FUNCTION_IDS,
TASK5_RUNTIME_OPTION_SCOPES, TASK5_RUNTIME_OPTION_SCOPES,
TASK6_RUNTIME_FUNCTION_IDS, TASK6_RUNTIME_FUNCTION_IDS,
type RuntimeStoryActionRequest,
} from './rpgRuntimeStoryAction'; } from './rpgRuntimeStoryAction';
import type { RuntimeStoryStateRequest } from './rpgRuntimeStoryState'; import { type RuntimeStoryActionRequest } from './rpgRuntimeStoryState';
describe('RPG runtime shared contracts', () => { describe('RPG runtime shared contracts', () => {
test('拆分后的 runtime story action 契约继续导出常量与类型', () => { test('拆分后的 runtime story action 契约继续导出常量与类型', () => {
@@ -40,12 +39,7 @@ describe('RPG runtime shared contracts', () => {
targetStatus: {}, targetStatus: {},
}; };
const stateRequest: RuntimeStoryStateRequest = {
sessionId: 'runtime-session-2',
};
expect(payload.playerMessage).toBe('近况如何?'); expect(payload.playerMessage).toBe('近况如何?');
expect(stateRequest.sessionId).toBe('runtime-session-2');
expect(QUEST_NARRATIVE_TYPES).toContain('relationship'); expect(QUEST_NARRATIVE_TYPES).toContain('relationship');
}); });
}); });

View File

@@ -14,26 +14,6 @@ export type RuntimeAction<
payload?: TPayload; payload?: TPayload;
}; };
export type RuntimeActionRequest<
TAction extends RuntimeAction = RuntimeAction,
> = {
sessionId: string;
clientVersion?: number;
action: TAction;
};
export type RuntimeActionResponse<
TViewModel = JsonObject,
TPresentation = JsonObject,
TPatch = JsonObject,
> = {
sessionId: string;
serverVersion: number;
viewModel: TViewModel;
presentation: TPresentation;
patches: TPatch[];
};
export const TASK5_RUNTIME_FUNCTION_IDS = [ export const TASK5_RUNTIME_FUNCTION_IDS = [
'story_continue_adventure', 'story_continue_adventure',
'story_opening_camp_dialogue', 'story_opening_camp_dialogue',

View File

@@ -3,10 +3,7 @@
* 该文件只负责 view model、presentation、patch 与 snapshot 回包结构。 * 该文件只负责 view model、presentation、patch 与 snapshot 回包结构。
*/ */
import type { JsonObject } from './common'; import type { JsonObject } from './common';
import type { SavedGameSnapshot, SavedGameSnapshotInput } from './runtime';
import type { import type {
RuntimeActionRequest,
RuntimeActionResponse,
RuntimeStoryChoiceAction, RuntimeStoryChoiceAction,
RuntimeStoryChoicePayload, RuntimeStoryChoicePayload,
RuntimeStoryOptionInteraction, RuntimeStoryOptionInteraction,
@@ -206,8 +203,10 @@ export type RuntimeStoryPatch =
}; };
export type RuntimeStoryActionRequest = export type RuntimeStoryActionRequest =
RuntimeActionRequest<RuntimeStoryChoiceAction> & { {
snapshot?: SavedGameSnapshotInput; sessionId: string;
clientVersion?: number;
action: RuntimeStoryChoiceAction;
}; };
export type RuntimeStoryAiRequestOptions = { export type RuntimeStoryAiRequestOptions = {
@@ -224,55 +223,3 @@ export type RuntimeStoryAiRequest = {
recentActionResult?: string | null; recentActionResult?: string | null;
requestOptions?: RuntimeStoryAiRequestOptions; requestOptions?: RuntimeStoryAiRequestOptions;
}; };
export type RuntimeStoryStateRequest<
TSnapshotGameState = JsonObject,
TSnapshotCurrentStory = JsonObject,
> = {
sessionId: string;
clientVersion?: number;
snapshot?: SavedGameSnapshotInput<
TSnapshotGameState,
string,
TSnapshotCurrentStory
>;
};
export type RuntimeStoryBootstrapRequest<
TProfile = JsonObject,
TCharacter = JsonObject,
> = {
worldType: string;
customWorldProfile?: TProfile | null;
character: TCharacter;
runtimeMode?: 'play' | 'preview' | 'test';
disablePersistence?: boolean;
};
export type RuntimeStoryBootstrapResponse<
TSnapshotGameState = JsonObject,
TSnapshotCurrentStory = JsonObject,
> = {
sessionId: string;
serverVersion: number;
snapshot: SavedGameSnapshot<
TSnapshotGameState,
string,
TSnapshotCurrentStory
>;
};
export type RuntimeStoryActionResponse<
TSnapshotGameState = JsonObject,
TSnapshotCurrentStory = JsonObject,
> = RuntimeActionResponse<
RuntimeStoryViewModel,
RuntimeStoryPresentation,
RuntimeStoryPatch
> & {
snapshot: SavedGameSnapshot<
TSnapshotGameState,
string,
TSnapshotCurrentStory
>;
};

View File

@@ -1,4 +1,5 @@
import type { JsonObject } from './common'; import type { JsonObject } from './common';
import type { SavedGameSnapshotInput } from './runtime';
/** /**
* story session 主链共享契约。 * story session 主链共享契约。
@@ -12,12 +13,32 @@ export type BeginStorySessionRequest = {
openingSummary?: string | null; openingSummary?: string | null;
}; };
export type BeginStoryRuntimeSessionRequest<
TProfile = JsonObject,
TCharacter = JsonObject,
> = {
worldType: string;
customWorldProfile?: TProfile | null;
character: TCharacter;
runtimeMode?: 'play' | 'preview' | 'test';
disablePersistence?: boolean;
};
export type ContinueStoryRequest = { export type ContinueStoryRequest = {
storySessionId: string; storySessionId: string;
narrativeText: string; narrativeText: string;
choiceFunctionId?: string | null; choiceFunctionId?: string | null;
}; };
export type ResolveStoryRuntimeActionRequest = {
storySessionId: string;
clientVersion?: number;
functionId: string;
actionText: string;
targetId?: string | null;
payload?: JsonObject | null;
};
export type StorySessionPayload = { export type StorySessionPayload = {
storySessionId: string; storySessionId: string;
runtimeSessionId: string; runtimeSessionId: string;
@@ -52,6 +73,11 @@ export type StorySessionStateResponse = {
storyEvents: StoryEventPayload[]; storyEvents: StoryEventPayload[];
}; };
export type StoryRuntimeSnapshotPayload<
TGameState = JsonObject,
TCurrentStory = JsonObject,
> = SavedGameSnapshotInput<TGameState, string, TCurrentStory>;
export type StoryRuntimeProjectionRequest = { export type StoryRuntimeProjectionRequest = {
storySessionId: string; storySessionId: string;
clientVersion?: number; clientVersion?: number;
@@ -94,6 +120,7 @@ export type StoryRuntimeProjectionResponse = {
storySession: StorySessionPayload; storySession: StorySessionPayload;
storyEvents: StoryEventPayload[]; storyEvents: StoryEventPayload[];
serverVersion: number; serverVersion: number;
gameState: JsonObject;
actor: StoryRuntimeActorProjection; actor: StoryRuntimeActorProjection;
inventory: StoryRuntimeInventoryProjection; inventory: StoryRuntimeInventoryProjection;
options: StoryRuntimeOptionProjection[]; options: StoryRuntimeOptionProjection[];
@@ -102,3 +129,7 @@ export type StoryRuntimeProjectionResponse = {
actionResultText?: string | null; actionResultText?: string | null;
toast?: string | null; toast?: string | null;
}; };
export type StoryRuntimeMutationResponse = {
projection: StoryRuntimeProjectionResponse;
};

View File

@@ -3,6 +3,15 @@ import { basename, join, relative } from 'node:path';
const repoRoot = process.cwd(); const repoRoot = process.cwd();
const cratesDir = join(repoRoot, 'server-rs', 'crates'); const cratesDir = join(repoRoot, 'server-rs', 'crates');
const spacetimeModuleSrcDir = join(cratesDir, 'spacetime-module', 'src');
const spacetimeMigrationPath = join(spacetimeModuleSrcDir, 'migration.rs');
const spacetimeTableCatalogPath = join(
repoRoot,
'docs',
'technical',
'SPACETIMEDB_TABLE_CATALOG.md',
);
const migrationExcludedTables = new Set(['database_migration_operator']);
const requiredModuleFiles = [ const requiredModuleFiles = [
'domain.rs', 'domain.rs',
'commands.rs', 'commands.rs',
@@ -79,6 +88,113 @@ function listRustFiles(dir) {
return files; return files;
} }
function collectSpacetimeTables() {
if (!existsSync(spacetimeModuleSrcDir)) {
return [];
}
const tableByAccessor = new Map();
const tablePattern =
/#\[spacetimedb::table\(([\s\S]*?)\)\]\s*(?:#\[[^\]]+\]\s*)*(?:pub\s+)?struct\s+([A-Za-z0-9_]+)/gu;
for (const rustFile of listRustFiles(spacetimeModuleSrcDir)) {
const text = readText(rustFile);
let match;
while ((match = tablePattern.exec(text)) !== null) {
const accessorMatch = /accessor\s*=\s*([A-Za-z0-9_]+)/u.exec(match[1]);
if (!accessorMatch) {
continue;
}
const accessor = accessorMatch[1];
const relativePath = normalizePath(relative(repoRoot, rustFile));
const previous = tableByAccessor.get(accessor);
if (previous) {
failures.push(
`SpacetimeDB table accessor ${accessor} 重复定义于 ${previous.path}${relativePath}`,
);
continue;
}
tableByAccessor.set(accessor, {
accessor,
structName: match[2],
path: relativePath,
});
}
}
return [...tableByAccessor.values()].sort((left, right) =>
left.accessor.localeCompare(right.accessor),
);
}
function collectMigrationTables() {
if (!existsSync(spacetimeMigrationPath)) {
return new Set();
}
const migrationText = readText(spacetimeMigrationPath);
const macroMatch =
/macro_rules!\s+migration_tables\s*\{[\s\S]*?\$macro_name!\s*\{([\s\S]*?)\n\s*\}\s*\n\s*\};\s*\n\}/u.exec(
migrationText,
);
if (!macroMatch) {
failures.push('migration.rs 无法解析 migration_tables! 白名单');
return new Set();
}
return new Set(
[...macroMatch[1].matchAll(/\b([a-z][a-z0-9_]*)\b/gu)]
.map((match) => match[1])
.filter((name) => !['arg'].includes(name)),
);
}
function collectCatalogTables() {
if (!existsSync(spacetimeTableCatalogPath)) {
return new Set();
}
const catalogText = readText(spacetimeTableCatalogPath);
return new Set(
[...catalogText.matchAll(/^### `([^`]+)`/gmu)].map((match) => match[1]),
);
}
function checkSpacetimeTableCatalogAndMigration() {
const tables = collectSpacetimeTables();
const tableNames = new Set(tables.map((table) => table.accessor));
const migrationTables = collectMigrationTables();
const catalogTables = collectCatalogTables();
for (const table of tables) {
if (!migrationExcludedTables.has(table.accessor) && !migrationTables.has(table.accessor)) {
failures.push(
`${table.path}: SpacetimeDB 表 ${table.accessor} 缺少 migration.rs 白名单`,
);
}
if (!catalogTables.has(table.accessor)) {
failures.push(
`${table.path}: SpacetimeDB 表 ${table.accessor} 缺少 SPACETIMEDB_TABLE_CATALOG.md 目录项`,
);
}
}
for (const tableName of migrationTables) {
if (!tableNames.has(tableName)) {
failures.push(`migration.rs 白名单包含不存在的 SpacetimeDB 表 ${tableName}`);
}
}
for (const tableName of catalogTables) {
if (!tableNames.has(tableName)) {
failures.push(`SPACETIMEDB_TABLE_CATALOG.md 包含不存在的 SpacetimeDB 表 ${tableName}`);
}
}
}
function collectModuleCrates() { function collectModuleCrates() {
return readdirSync(cratesDir) return readdirSync(cratesDir)
.filter((name) => name.startsWith('module-')) .filter((name) => name.startsWith('module-'))
@@ -144,6 +260,8 @@ for (const crateName of moduleCrates) {
} }
} }
checkSpacetimeTableCatalogAndMigration();
if (failures.length > 0) { if (failures.length > 0) {
console.error('server-rs DDD boundary check failed:'); console.error('server-rs DDD boundary check failed:');
for (const failure of failures) { for (const failure of failures) {

View File

@@ -381,12 +381,6 @@ const indexPath = path.join(webRoot, 'index.html');
const proxyPrefixes = [ const proxyPrefixes = [
'/api/', '/api/',
'/api', '/api',
'/generated-character-drafts',
'/generated-characters',
'/generated-animations',
'/generated-custom-world-scenes',
'/generated-custom-world-covers',
'/generated-qwen-sprites',
'/healthz', '/healthz',
]; ];

View File

@@ -55,7 +55,7 @@ for (const target of selectedTargets) {
await recreateTempDir(tempOutDir); await recreateTempDir(tempOutDir);
console.log(`[spacetime:generate] 生成 ${target.name} bindings 到短路径: ${tempOutDir}`); console.log(`[spacetime:generate] 生成 ${target.name} bindings 到短路径: ${tempOutDir}`);
await run('spacetime', buildGenerateArgs(target, tempOutDir)); await generateBindings(target, tempOutDir);
const fileCount = await countFiles(tempOutDir); const fileCount = await countFiles(tempOutDir);
if (fileCount === 0) { if (fileCount === 0) {
@@ -141,7 +141,79 @@ function buildGenerateArgs(target, outDir) {
return generateArgs; return generateArgs;
} }
function run(command, commandArgs) { async function generateBindings(target, outDir) {
const result = await run('spacetime', buildGenerateArgs(target, outDir), {
allowGeneratedFormatFailure: target.lang === 'rust',
});
if (result.generatedFormatFailed) {
// Windows 下 SpacetimeDB CLI 2.1.0 会把所有 Rust 文件一次性传给 formatter
// 这里只接管“文件已生成但 CLI 格式化失败”的尾段,并仍然只同步生成目录。
console.warn(
`[spacetime:generate] ${target.name} bindings 已生成,但 SpacetimeDB CLI 自带格式化失败;改用短路径分批 rustfmt。`,
);
await formatRustBindings(outDir);
}
}
async function formatRustBindings(outDir) {
const rustFiles = await collectRustFiles(outDir);
if (rustFiles.length === 0) {
throw new Error(`Rust bindings 未生成任何 .rs 文件,无法格式化: ${outDir}`);
}
for (const chunk of chunkCommandArgs(rustFiles)) {
await run('rustfmt', ['--edition', '2024', ...chunk]);
}
}
async function collectRustFiles(dir) {
const files = [];
const entries = await readdir(dir, {withFileTypes: true});
for (const entry of entries) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await collectRustFiles(entryPath)));
continue;
}
if (entry.isFile() && entry.name.endsWith('.rs')) {
files.push(entryPath);
}
}
return files;
}
function chunkCommandArgs(argsToChunk) {
// Windows CreateProcess 受命令行长度限制;分批能避免 bindings 文件变多后再次失败。
const maxCommandLineChars = process.platform === 'win32' ? 20_000 : 100_000;
const chunks = [];
let current = [];
let currentLength = 0;
for (const arg of argsToChunk) {
const argLength = arg.length + 3;
if (current.length > 0 && currentLength + argLength > maxCommandLineChars) {
chunks.push(current);
current = [];
currentLength = 0;
}
current.push(arg);
currentLength += argLength;
}
if (current.length > 0) {
chunks.push(current);
}
return chunks;
}
function run(command, commandArgs, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const child = spawn(command, commandArgs, { const child = spawn(command, commandArgs, {
cwd: REPO_ROOT, cwd: REPO_ROOT,
@@ -171,13 +243,20 @@ function run(command, commandArgs) {
return; return;
} }
if (output.includes('Could not format generated files')) { const generatedFormatFailed = output.includes('Could not format generated files');
if (generatedFormatFailed && options.allowGeneratedFormatFailure) {
resolve({generatedFormatFailed});
return;
}
if (generatedFormatFailed) {
reject(new Error(`${command} 生成后格式化失败。`)); reject(new Error(`${command} 生成后格式化失败。`));
return; return;
} }
if (code === 0) { if (code === 0) {
resolve(); resolve({generatedFormatFailed: false});
return; return;
} }

5
server-rs/Cargo.lock generated
View File

@@ -1637,6 +1637,11 @@ dependencies = [
name = "module-story" name = "module-story"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"module-combat",
"module-inventory",
"module-progression",
"module-quest",
"module-runtime-item",
"serde", "serde",
"shared-kernel", "shared-kernel",
"spacetimedb", "spacetimedb",

View File

@@ -58,7 +58,7 @@
1. `crates/spacetime-module` 的表、reducer、view 聚合入口 1. `crates/spacetime-module` 的表、reducer、view 聚合入口
2. `module-auth` 的身份表、JWT 与 refresh cookie 主链 2. `module-auth` 的身份表、JWT 与 refresh cookie 主链
3. `platform-oss` 的浏览器直传签名、旧 `/generated-*` 前缀映射与对象 URL 解析能力 3. `platform-oss` 的浏览器直传签名、旧 `/generated-*` 前缀到 OSS object key 的映射与对象 URL 解析能力;`/generated-*` 不再作为可裸读 HTTP 路由
当前本地脚本补充说明: 当前本地脚本补充说明:

View File

@@ -46,12 +46,10 @@
22. 接入 `POST /api/assets/sts-upload-credentials` 禁用式 STS 写权限 contract 22. 接入 `POST /api/assets/sts-upload-credentials` 禁用式 STS 写权限 contract
23. 接入 `custom-world-library``custom-world-gallery` 与 agent `publish_world` 首批 Axum facade 23. 接入 `custom-world-library``custom-world-gallery` 与 agent `publish_world` 首批 Axum facade
24. 接入 custom world agent `session create / session snapshot` Axum facade 24. 接入 custom world agent `session create / session snapshot` Axum facade
25. 接入`runtime story` 兼容接口: 25.`runtime story` 兼容接口已下线并保持未挂载;运行时故事写链路改为
- `POST /api/runtime/story/state/resolve` - `POST /api/story/sessions/runtime`
- `GET /api/runtime/story/state/{session_id}` - `GET /api/story/sessions/{story_session_id}/runtime-projection`
- `POST /api/runtime/story/actions/resolve` - `POST /api/story/sessions/{story_session_id}/actions/resolve`
- `POST /api/runtime/story/initial`
- `POST /api/runtime/story/continue`
26. 接入 `POST /api/assets/character-visual/generate` 26. 接入 `POST /api/assets/character-visual/generate`
27. 接入 `GET /api/assets/character-visual/jobs/{task_id}` 27. 接入 `GET /api/assets/character-visual/jobs/{task_id}`
28. 接入 `POST /api/assets/character-visual/publish` 28. 接入 `POST /api/assets/character-visual/publish`
@@ -62,7 +60,8 @@
33. 接入 `POST /api/assets/character-animation/generate` 33. 接入 `POST /api/assets/character-animation/generate`
34. 接入 `GET /api/assets/character-animation/jobs/{task_id}` 34. 接入 `GET /api/assets/character-animation/jobs/{task_id}`
35. 接入 `POST /api/assets/character-animation/publish` 35. 接入 `POST /api/assets/character-animation/publish`
36. 接入`/generated-character-drafts/*``/generated-characters/*``/generated-animations/*``/generated-custom-world-scenes/*``/generated-custom-world-covers/*``/generated-qwen-sprites/*` 到 OSS 私有读代理 36. 下线`/generated-*` 资产直读代理;生成资产读取统一走 `/api/assets/read-url` 或 asset object projection
37. 下线旧 `/api/custom-world/*` 非 runtime 前缀和 `/api/runtime/puzzle/runs/local-next-level`,并用路由回归测试确认这些旧入口保持 `404`
后续与本 crate 直接相关的任务包括: 后续与本 crate 直接相关的任务包括:
@@ -87,12 +86,13 @@
19. [x] 接入 `/api/assets/sts-upload-credentials` 19. [x] 接入 `/api/assets/sts-upload-credentials`
20. [x] 接入 `custom world library / gallery / publish_world` 首批 facade 20. [x] 接入 `custom world library / gallery / publish_world` 首批 facade
21. [x] 接入 `custom world agent session create / snapshot` facade 21. [x] 接入 `custom world agent session create / snapshot` facade
22. [x] 接入`runtime story` compat facade 22. [x] 下线`runtime story` compat facade,并接入 story session scoped runtime story 写读入口
23. [x] 接入 `character-visual generate / jobs / publish` 第一批 OSS 主链兼容 facade 23. [x] 接入 `character-visual generate / jobs / publish` 第一批 OSS 主链兼容 facade
24. [x] 接入 `character-animation templates / import-video` 第一批 OSS 草稿兼容 facade 24. [x] 接入 `character-animation templates / import-video` 第一批 OSS 草稿兼容 facade
25. [x] 接入 `character-workflow-cache get / save` 第一批 OSS JSON 草稿兼容 facade 25. [x] 接入 `character-workflow-cache get / save` 第一批 OSS JSON 草稿兼容 facade
26. [x] 接入 `character-animation generate / jobs / publish` 第一批 OSS 主链兼容 facade 26. [x] 接入 `character-animation generate / jobs / publish` 第一批 OSS 主链兼容 facade
27. [x] 接入`/generated-*` 路径 OSS 私有读同源代理 27. [x] 下线`/generated-*` 路径 OSS 私有读同源代理
28. [x] 下线旧 `/api/custom-world/*` 非 runtime 前缀和 Puzzle `local-next-level` 兼容入口
当前 tracing 约定: 当前 tracing 约定:
@@ -161,10 +161,12 @@
12. 当前微信回调不会把第三方 token 直接透传给前端或 SpacetimeDB而是统一换成系统签发的 JWT。 12. 当前微信回调不会把第三方 token 直接透传给前端或 SpacetimeDB而是统一换成系统签发的 JWT。
13. 当前 `/api/assets/sts-upload-credentials` 按“服务器上传、Web 只下载”口径固定返回 `403`,不向浏览器下发 OSS 写权限。 13. 当前 `/api/assets/sts-upload-credentials` 按“服务器上传、Web 只下载”口径固定返回 `403`,不向浏览器下发 OSS 写权限。
14. 当前 `/api/runtime/custom-world/agent/sessions``/api/runtime/custom-world/agent/sessions/{session_id}` 只提供 deterministic session 骨架与 snapshot 读取,不承诺 message submit、operation query、card detail 的完整能力。 14. 当前 `/api/runtime/custom-world/agent/sessions``/api/runtime/custom-world/agent/sessions/{session_id}` 只提供 deterministic session 骨架与 snapshot 读取,不承诺 message submit、operation query、card detail 的完整能力。
15. 当前 `/api/runtime/story/*` 已在 Rust 侧补齐 compat handler但内部仍是 `runtime_snapshot` 驱动的兼容桥与确定性动作编排,不应误判为真正的 SpacetimeDB `resolve_story_action` 真相链已完成 15. 当前 `/api/runtime/story/*` 不再挂载runtime story 开局、读取和动作结算通过 `/api/story/sessions/runtime``/api/story/sessions/{story_session_id}/runtime-projection``/api/story/sessions/{story_session_id}/actions/resolve` 进入 BFF并由 `module-runtime-story`、story session 和 runtime snapshot 投影共同闭合
16. 当前 `/api/assets/character-visual/*` 第一批只保证旧接口 contract、OSS 草稿/正式对象、`asset_object``asset_entity_binding` 主链可用真实图片模型、workflow cache 与本地角色覆盖写回仍在后续阶段。 16. 当前 `/api/assets/character-visual/*` 第一批只保证旧接口 contract、OSS 草稿/正式对象、`asset_object``asset_entity_binding` 主链可用真实图片模型、workflow cache 与本地角色覆盖写回仍在后续阶段。
17. 当前 `/api/assets/character-animation/import-video` 第一批只接受 `data:video/*;base64,...` 并写入 OSS 草稿区,不读取旧本地 `public/` 路径,也不创建正式 `asset_object` 17. 当前 `/api/assets/character-animation/import-video` 第一批只接受 `data:video/*;base64,...` 并写入 OSS 草稿区,不读取旧本地 `public/` 路径,也不创建正式 `asset_object`
18. 当前 `/api/assets/character-workflow-cache/*` 第一批只把工作流 JSON 草稿写入 OSS不迁移历史本地缓存也不创建正式 `asset_object` 18. 当前 `/api/assets/character-workflow-cache/*` 第一批只把工作流 JSON 草稿写入 OSS不迁移历史本地缓存也不创建正式 `asset_object`
19. 当前 `/api/assets/character-animation/generate` 第一批只用 Rust 占位产物打通 `AiTaskService + OSS` 草稿链;`image-sequence` 写 SVG 帧,视频类策略优先复用参考视频或仓库内可播放占位视频,不代表真实上游视频模型已完成迁移。 19. 当前 `/api/assets/character-animation/generate` 第一批只用 Rust 占位产物打通 `AiTaskService + OSS` 草稿链;`image-sequence` 写 SVG 帧,视频类策略优先复用参考视频或仓库内可播放占位视频,不代表真实上游视频模型已完成迁移。
20. 当前 `/api/assets/character-animation/publish` 会把前端提交帧、动作级 manifest 与总 manifest 写入 OSS并只把总 manifest 确认为 `asset_object` 后绑定到 `character / animation_set` 20. 当前 `/api/assets/character-animation/publish` 会把前端提交帧、动作级 manifest 与总 manifest 写入 OSS并只把总 manifest 确认为 `asset_object` 后绑定到 `character / animation_set`
21. 当前旧 `/generated-*`兼容层只代理受支持 generated 前缀到 OSS 私有读签名,不回退仓库 `public/`Stage 1 不支持视频 Range 分片。 21. 当前旧 `/generated-*` 读兼容层已下线;`/generated-*` 只作为 `legacyPublicPath` / OSS object key 标识,读取必须通过 `/api/assets/read-url` 或业务投影中的签名读 URL。
22. 当前旧 `/api/custom-world/entity``/api/custom-world/scene-npc``/api/custom-world/scene-image``/api/custom-world/cover-image``/api/custom-world/cover-upload` 不再挂载RPG 创作资产入口统一使用 `/api/runtime/custom-world/*`
23. 当前旧 `/api/runtime/puzzle/runs/local-next-level` 不再挂载,正式 Puzzle 运行态只通过 `/api/runtime/puzzle/runs/{run_id}/next-level` 进入后端真相源。

View File

@@ -67,12 +67,6 @@ use crate::{
}, },
error_middleware::normalize_error_response, error_middleware::normalize_error_response,
health::health_check, health::health_check,
legacy_generated_assets::{
proxy_generated_animations, proxy_generated_big_fish_assets,
proxy_generated_character_drafts, proxy_generated_characters,
proxy_generated_custom_world_covers, proxy_generated_custom_world_scenes,
proxy_generated_puzzle_assets, proxy_generated_qwen_sprites,
},
llm::proxy_llm_chat_completions, llm::proxy_llm_chat_completions,
login_options::auth_login_options, login_options::auth_login_options,
logout::logout, logout::logout,
@@ -81,12 +75,11 @@ use crate::{
password_management::{change_password, reset_password}, password_management::{change_password, reset_password},
phone_auth::{phone_login, send_phone_code}, phone_auth::{phone_login, send_phone_code},
puzzle::{ puzzle::{
advance_local_puzzle_next_level, advance_puzzle_next_level, create_puzzle_agent_session, advance_puzzle_next_level, create_puzzle_agent_session, delete_puzzle_work,
delete_puzzle_work, drag_puzzle_piece_or_group, execute_puzzle_agent_action, drag_puzzle_piece_or_group, execute_puzzle_agent_action, get_puzzle_agent_session,
get_puzzle_agent_session, get_puzzle_gallery_detail, get_puzzle_run, get_puzzle_gallery_detail, get_puzzle_run, get_puzzle_work_detail, get_puzzle_works,
get_puzzle_work_detail, get_puzzle_works, list_puzzle_gallery, put_puzzle_work, list_puzzle_gallery, put_puzzle_work, start_puzzle_run, stream_puzzle_agent_message,
start_puzzle_run, stream_puzzle_agent_message, submit_puzzle_agent_message, submit_puzzle_agent_message, submit_puzzle_leaderboard, swap_puzzle_pieces,
submit_puzzle_leaderboard, swap_puzzle_pieces,
}, },
refresh_session::refresh_session, refresh_session::refresh_session,
request_context::{attach_request_context, resolve_request_id}, request_context::{attach_request_context, resolve_request_id},
@@ -117,7 +110,8 @@ use crate::{
create_story_battle, create_story_npc_battle, get_story_battle_state, resolve_story_battle, create_story_battle, create_story_npc_battle, get_story_battle_state, resolve_story_battle,
}, },
story_sessions::{ story_sessions::{
begin_story_session, continue_story, get_story_runtime_projection, get_story_session_state, begin_story_runtime_session, begin_story_session, continue_story,
get_story_runtime_projection, get_story_session_state, resolve_story_runtime_action,
}, },
wechat_auth::{bind_wechat_phone, handle_wechat_callback, start_wechat_login}, wechat_auth::{bind_wechat_phone, handle_wechat_callback, start_wechat_login},
}; };
@@ -193,38 +187,6 @@ pub fn build_router(state: AppState) -> Router {
"/api/auth/public-users/by-id/{user_id}", "/api/auth/public-users/by-id/{user_id}",
get(get_public_user_by_id), get(get_public_user_by_id),
) )
.route(
"/generated-character-drafts/{*path}",
get(proxy_generated_character_drafts),
)
.route(
"/generated-characters/{*path}",
get(proxy_generated_characters),
)
.route(
"/generated-animations/{*path}",
get(proxy_generated_animations),
)
.route(
"/generated-big-fish-assets/{*path}",
get(proxy_generated_big_fish_assets),
)
.route(
"/generated-puzzle-assets/{*path}",
get(proxy_generated_puzzle_assets),
)
.route(
"/generated-custom-world-scenes/{*path}",
get(proxy_generated_custom_world_scenes),
)
.route(
"/generated-custom-world-covers/{*path}",
get(proxy_generated_custom_world_covers),
)
.route(
"/generated-qwen-sprites/{*path}",
get(proxy_generated_qwen_sprites),
)
.route( .route(
"/api/auth/me", "/api/auth/me",
get(auth_me).route_layer(middleware::from_fn_with_state( get(auth_me).route_layer(middleware::from_fn_with_state(
@@ -738,13 +700,6 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth, require_bearer_auth,
)), )),
) )
.route(
"/api/runtime/puzzle/runs/local-next-level",
post(advance_local_puzzle_next_level).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route( .route(
"/api/runtime/puzzle/runs/{run_id}", "/api/runtime/puzzle/runs/{run_id}",
get(get_puzzle_run).route_layer(middleware::from_fn_with_state( get(get_puzzle_run).route_layer(middleware::from_fn_with_state(
@@ -787,13 +742,6 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth, require_bearer_auth,
)), )),
) )
.route(
"/api/custom-world/entity",
post(generate_custom_world_entity).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route( .route(
"/api/runtime/custom-world/entity", "/api/runtime/custom-world/entity",
post(generate_custom_world_entity).route_layer(middleware::from_fn_with_state( post(generate_custom_world_entity).route_layer(middleware::from_fn_with_state(
@@ -801,13 +749,6 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth, require_bearer_auth,
)), )),
) )
.route(
"/api/custom-world/scene-npc",
post(generate_custom_world_scene_npc).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route( .route(
"/api/runtime/custom-world/scene-npc", "/api/runtime/custom-world/scene-npc",
post(generate_custom_world_scene_npc).route_layer(middleware::from_fn_with_state( post(generate_custom_world_scene_npc).route_layer(middleware::from_fn_with_state(
@@ -816,19 +757,12 @@ pub fn build_router(state: AppState) -> Router {
)), )),
) )
.route( .route(
"/api/custom-world/scene-image", "/api/runtime/custom-world/scene-image",
post(generate_custom_world_scene_image).route_layer(middleware::from_fn_with_state( post(generate_custom_world_scene_image).route_layer(middleware::from_fn_with_state(
state.clone(), state.clone(),
require_bearer_auth, require_bearer_auth,
)), )),
) )
.route(
"/api/custom-world/cover-image",
post(generate_custom_world_cover_image).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route( .route(
"/api/runtime/custom-world/cover-image", "/api/runtime/custom-world/cover-image",
post(generate_custom_world_cover_image).route_layer(middleware::from_fn_with_state( post(generate_custom_world_cover_image).route_layer(middleware::from_fn_with_state(
@@ -836,13 +770,6 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth, require_bearer_auth,
)), )),
) )
.route(
"/api/custom-world/cover-upload",
post(upload_custom_world_cover_image).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route( .route(
"/api/runtime/custom-world/cover-upload", "/api/runtime/custom-world/cover-upload",
post(upload_custom_world_cover_image).route_layer(middleware::from_fn_with_state( post(upload_custom_world_cover_image).route_layer(middleware::from_fn_with_state(
@@ -944,6 +871,13 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth, require_bearer_auth,
)), )),
) )
.route(
"/api/story/sessions/runtime",
post(begin_story_runtime_session).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route( .route(
"/api/story/sessions/{story_session_id}/state", "/api/story/sessions/{story_session_id}/state",
get(get_story_session_state).route_layer(middleware::from_fn_with_state( get(get_story_session_state).route_layer(middleware::from_fn_with_state(
@@ -958,6 +892,13 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth, require_bearer_auth,
)), )),
) )
.route(
"/api/story/sessions/{story_session_id}/actions/resolve",
post(resolve_story_runtime_action).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route( .route(
"/api/story/sessions/continue", "/api/story/sessions/continue",
post(continue_story).route_layer(middleware::from_fn_with_state( post(continue_story).route_layer(middleware::from_fn_with_state(
@@ -1263,6 +1204,85 @@ mod tests {
} }
} }
#[tokio::test]
async fn deleted_old_routes_are_not_mounted() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
// 中文注释:旧 custom-world 非 runtime 前缀没有任何新路由可匹配,
// 因此必须稳定返回 404避免前端继续误用旧入口。
for uri in [
"/api/custom-world/entity",
"/api/custom-world/scene-npc",
"/api/custom-world/scene-image",
"/api/custom-world/cover-image",
"/api/custom-world/cover-upload",
] {
let response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri(uri)
.header("x-genarrative-response-envelope", "v1")
.body(Body::empty())
.expect("deleted old route request should build"),
)
.await
.expect("deleted old route request should be handled");
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/runtime/puzzle/runs/local-next-level")
.header("x-genarrative-response-envelope", "v1")
.body(Body::empty())
.expect("deleted old puzzle route request should build"),
)
.await
.expect("deleted old puzzle route request should be handled");
// 中文注释:该路径会被现有 GET /runs/{run_id} 的动态段识别,
// 但 POST 方法没有挂载,返回 405 代表旧 local-next-level handler 已移除。
assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
}
#[tokio::test]
async fn generated_asset_read_proxy_routes_are_not_mounted() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
// 中文注释:生成资产仍可作为 legacyPublicPath 传给 /api/assets/read-url
// 但不能再通过 /generated-* 同源路由裸读 OSS 对象。
for uri in [
"/generated-character-drafts/hero/visual/candidate.png",
"/generated-characters/hero/visual/master.png",
"/generated-animations/hero/idle/frame01.png",
"/generated-big-fish-assets/session-1/level/image.png",
"/generated-puzzle-assets/session-1/candidate/image.png",
"/generated-custom-world-scenes/world-1/camp/scene.png",
"/generated-custom-world-covers/world-1/cover.webp",
"/generated-qwen-sprites/master/candidate-01.png",
] {
let response = app
.clone()
.oneshot(
Request::builder()
.method("GET")
.uri(uri)
.header("x-genarrative-response-envelope", "v1")
.body(Body::empty())
.expect("generated asset proxy route request should build"),
)
.await
.expect("generated asset proxy route request should be handled");
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
}
#[tokio::test] #[tokio::test]
async fn internal_auth_claims_rejects_missing_bearer_token() { async fn internal_auth_claims_rejects_missing_bearer_token() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build")); let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));

View File

@@ -442,6 +442,8 @@ pub async fn generate_custom_world_scene_image(
let owner_user_id = authenticated.claims().user_id().to_string(); let owner_user_id = authenticated.claims().user_id().to_string();
let normalized = normalize_scene_image_request(payload) let normalized = normalize_scene_image_request(payload)
.map_err(|error| custom_world_ai_error_response(&request_context, error))?; .map_err(|error| custom_world_ai_error_response(&request_context, error))?;
require_dashscope_settings(&state)
.map_err(|error| custom_world_ai_error_response(&request_context, error))?;
let asset_id = format!("custom-scene-{}", current_utc_millis()); let asset_id = format!("custom-scene-{}", current_utc_millis());
let asset = execute_billable_asset_operation( let asset = execute_billable_asset_operation(
&state, &state,
@@ -703,6 +705,8 @@ pub async fn generate_custom_world_cover_image(
trim_to_option(payload.profile.name.as_deref()).unwrap_or_else(|| "world".to_string()); trim_to_option(payload.profile.name.as_deref()).unwrap_or_else(|| "world".to_string());
let entity_id = profile_id.clone().unwrap_or_else(|| world_name.clone()); let entity_id = profile_id.clone().unwrap_or_else(|| world_name.clone());
let size = trim_to_option(payload.size.as_deref()).unwrap_or_else(|| "1600*900".to_string()); let size = trim_to_option(payload.size.as_deref()).unwrap_or_else(|| "1600*900".to_string());
require_dashscope_settings(&state)
.map_err(|error| custom_world_ai_error_response(&request_context, error))?;
let asset_id = format!("custom-cover-{}", current_utc_millis()); let asset_id = format!("custom-cover-{}", current_utc_millis());
let asset = execute_billable_asset_operation( let asset = execute_billable_asset_operation(
&state, &state,
@@ -2440,10 +2444,16 @@ mod tests {
serde_json::from_slice(&body).expect("body should be valid json") serde_json::from_slice(&body).expect("body should be valid json")
} }
fn build_state_without_dashscope_key() -> AppState {
let mut config = AppConfig::default();
config.dashscope_api_key = None;
AppState::new(config).expect("state should build")
}
#[tokio::test] #[tokio::test]
async fn scene_image_returns_service_unavailable_when_dashscope_missing() { async fn scene_image_returns_service_unavailable_when_dashscope_missing() {
let state = AppState::new(AppConfig::default()).expect("state should build"); let state = build_state_without_dashscope_key();
let request_context = build_request_context("POST /api/custom-world/scene-image"); let request_context = build_request_context("POST /api/runtime/custom-world/scene-image");
let authenticated = build_authenticated(&state); let authenticated = build_authenticated(&state);
let response = generate_custom_world_scene_image( let response = generate_custom_world_scene_image(
@@ -2512,15 +2522,27 @@ mod tests {
}; };
let normalized = normalize_scene_image_request(payload).expect("payload should normalize"); let normalized = normalize_scene_image_request(payload).expect("payload should normalize");
let manual_prompt = build_custom_world_scene_image_prompt(SceneImagePromptParams {
profile: SceneImagePromptProfile {
name: "雾海群岛",
subtitle: "失落航线",
tone: "潮湿、神秘、低魔奇幻",
player_goal: "找到王冠并阻止海妖复苏",
summary: "玩家在雾海中追查沉没王冠。",
setting_text: "群岛被永恒雾潮包围。",
},
landmark: SceneImagePromptLandmark {
name: "礁石神殿",
description: "古老礁石上的半沉神殿。",
},
user_prompt: "破碎神殿矗立在蓝绿色雾潮中,潮湿石阶上有幽光贝壳。",
has_reference_image: false,
fallback_landmark_name: Some("礁石神殿"),
fallback_world_name: "雾海群岛",
});
assert!(normalized.prompt.contains("世界名:雾海群岛")); assert_eq!(normalized.prompt, manual_prompt);
assert!(normalized.prompt.contains("世界副标题:失落航线")); assert!(normalized.prompt.contains("破碎神殿矗立在蓝绿色雾潮中"));
assert!(normalized.prompt.contains("场景名称:礁石神殿"));
assert!(
normalized
.prompt
.contains("本次想要生成的画面内容:破碎神殿")
);
assert_ne!( assert_ne!(
normalized.prompt, normalized.prompt,
"破碎神殿矗立在蓝绿色雾潮中,潮湿石阶上有幽光贝壳。" "破碎神殿矗立在蓝绿色雾潮中,潮湿石阶上有幽光贝壳。"
@@ -2585,8 +2607,8 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn cover_image_returns_service_unavailable_when_dashscope_missing() { async fn cover_image_returns_service_unavailable_when_dashscope_missing() {
let state = AppState::new(AppConfig::default()).expect("state should build"); let state = build_state_without_dashscope_key();
let request_context = build_request_context("POST /api/custom-world/cover-image"); let request_context = build_request_context("POST /api/runtime/custom-world/cover-image");
let authenticated = build_authenticated(&state); let authenticated = build_authenticated(&state);
let response = generate_custom_world_cover_image( let response = generate_custom_world_cover_image(
@@ -2622,7 +2644,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn cover_upload_rejects_invalid_data_url_before_touching_oss() { async fn cover_upload_rejects_invalid_data_url_before_touching_oss() {
let state = AppState::new(AppConfig::default()).expect("state should build"); let state = AppState::new(AppConfig::default()).expect("state should build");
let request_context = build_request_context("POST /api/custom-world/cover-upload"); let request_context = build_request_context("POST /api/runtime/custom-world/cover-upload");
let authenticated = build_authenticated(&state); let authenticated = build_authenticated(&state);
let response = upload_custom_world_cover_image( let response = upload_custom_world_cover_image(

View File

@@ -1,246 +0,0 @@
use axum::{
extract::{Path, State},
http::{HeaderMap, HeaderName, HeaderValue, StatusCode, header},
response::{IntoResponse, Response},
};
use platform_oss::{LegacyAssetPrefix, OssSignedGetObjectUrlRequest};
use serde_json::json;
use crate::{http_error::AppError, platform_errors::map_oss_error, state::AppState};
const CACHE_CONTROL_VALUE: &str = "private, max-age=60";
const ASSET_OBJECT_KEY_HEADER: &str = "x-genarrative-asset-object-key";
pub async fn proxy_generated_character_drafts(
State(state): State<AppState>,
Path(path): Path<String>,
) -> Response {
proxy_legacy_generated_asset(state, LegacyAssetPrefix::CharacterDrafts, path).await
}
pub async fn proxy_generated_characters(
State(state): State<AppState>,
Path(path): Path<String>,
) -> Response {
proxy_legacy_generated_asset(state, LegacyAssetPrefix::Characters, path).await
}
pub async fn proxy_generated_animations(
State(state): State<AppState>,
Path(path): Path<String>,
) -> Response {
proxy_legacy_generated_asset(state, LegacyAssetPrefix::Animations, path).await
}
pub async fn proxy_generated_big_fish_assets(
State(state): State<AppState>,
Path(path): Path<String>,
) -> Response {
proxy_legacy_generated_asset(state, LegacyAssetPrefix::BigFishAssets, path).await
}
pub async fn proxy_generated_puzzle_assets(
State(state): State<AppState>,
Path(path): Path<String>,
) -> Response {
proxy_legacy_generated_asset(state, LegacyAssetPrefix::PuzzleAssets, path).await
}
pub async fn proxy_generated_custom_world_scenes(
State(state): State<AppState>,
Path(path): Path<String>,
) -> Response {
proxy_legacy_generated_asset(state, LegacyAssetPrefix::CustomWorldScenes, path).await
}
pub async fn proxy_generated_custom_world_covers(
State(state): State<AppState>,
Path(path): Path<String>,
) -> Response {
proxy_legacy_generated_asset(state, LegacyAssetPrefix::CustomWorldCovers, path).await
}
pub async fn proxy_generated_qwen_sprites(
State(state): State<AppState>,
Path(path): Path<String>,
) -> Response {
proxy_legacy_generated_asset(state, LegacyAssetPrefix::QwenSprites, path).await
}
async fn proxy_legacy_generated_asset(
state: AppState,
prefix: LegacyAssetPrefix,
path: String,
) -> Response {
match read_legacy_generated_asset(&state, prefix, path).await {
Ok(response) => response,
Err(error) => error.into_response(),
}
}
async fn read_legacy_generated_asset(
state: &AppState,
prefix: LegacyAssetPrefix,
path: String,
) -> Result<Response, AppError> {
let oss_client = state.oss_client().ok_or_else(|| {
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
"provider": "aliyun-oss",
"reason": "OSS 未完成环境变量配置",
}))
})?;
let object_key = build_generated_object_key(prefix, path.as_str())?;
let signed = oss_client
.sign_get_object_url(OssSignedGetObjectUrlRequest {
object_key: object_key.clone(),
expire_seconds: Some(60),
})
.map_err(map_legacy_generated_oss_error)?;
let upstream_response = reqwest::Client::new()
.get(signed.signed_url)
.send()
.await
.map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "aliyun-oss",
"message": format!("读取 OSS 旧 generated 资源失败:{error}"),
}))
})?;
if upstream_response.status() == reqwest::StatusCode::NOT_FOUND {
return Err(
AppError::from_status(StatusCode::NOT_FOUND).with_details(json!({
"provider": "aliyun-oss",
"objectKey": object_key,
})),
);
}
let status = upstream_response.status();
let content_type = upstream_response
.headers()
.get(header::CONTENT_TYPE)
.cloned();
if !status.is_success() {
return Err(map_legacy_generated_upstream_status(status, object_key));
}
let bytes = upstream_response.bytes().await.map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "aliyun-oss",
"message": format!("读取 OSS 旧 generated 资源内容失败:{error}"),
}))
})?;
// 旧 generated 路径会被 <img> / <video> 直接消费,成功分支必须返回原始二进制体。
// 这里显式组装 HeaderMap 并设置长度,避免代理层把已成功读取的 OSS 对象变成空响应。
let mut headers = HeaderMap::new();
headers.insert(
header::CACHE_CONTROL,
HeaderValue::from_static(CACHE_CONTROL_VALUE),
);
headers.insert(
HeaderName::from_static(ASSET_OBJECT_KEY_HEADER),
HeaderValue::from_str(object_key.as_str()).map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": "legacy-generated-assets",
"message": format!("构造资源响应头失败:{error}"),
}))
})?,
);
headers.insert(
header::CONTENT_LENGTH,
HeaderValue::from_str(bytes.len().to_string().as_str()).map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": "legacy-generated-assets",
"message": format!("构造资源长度响应头失败:{error}"),
}))
})?,
);
if let Some(content_type) = content_type {
headers.insert(header::CONTENT_TYPE, content_type);
}
Ok((status, headers, bytes).into_response())
}
fn build_generated_object_key(prefix: LegacyAssetPrefix, path: &str) -> Result<String, AppError> {
let path = path.trim().trim_matches('/');
if path.is_empty() || path.split('/').any(is_invalid_path_segment) {
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "legacy-generated-assets",
"message": "generated 资源路径不合法。",
})),
);
}
Ok(format!("{}/{}", prefix.as_str(), path))
}
fn is_invalid_path_segment(segment: &str) -> bool {
segment.is_empty() || segment == "." || segment == ".." || segment.contains('\\')
}
fn map_legacy_generated_oss_error(error: platform_oss::OssError) -> AppError {
map_oss_error(error, "aliyun-oss")
}
fn map_legacy_generated_upstream_status(
status: reqwest::StatusCode,
object_key: String,
) -> AppError {
let mapped_status = match status {
reqwest::StatusCode::NOT_FOUND => StatusCode::NOT_FOUND,
reqwest::StatusCode::FORBIDDEN | reqwest::StatusCode::UNAUTHORIZED => {
StatusCode::BAD_GATEWAY
}
_ => StatusCode::BAD_GATEWAY,
};
AppError::from_status(mapped_status).with_details(json!({
"provider": "aliyun-oss",
"objectKey": object_key,
"upstreamStatus": status.as_u16(),
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_generated_object_key_keeps_supported_prefix() {
let object_key = build_generated_object_key(
LegacyAssetPrefix::Animations,
"hero/animation-set-1/idle/frame01.png",
)
.expect("object key should build");
assert_eq!(
object_key,
"generated-animations/hero/animation-set-1/idle/frame01.png"
);
}
#[test]
fn build_generated_object_key_supports_big_fish_assets() {
let object_key = build_generated_object_key(
LegacyAssetPrefix::BigFishAssets,
"big-fish-session-1/level-main-image/level-1/image.png",
)
.expect("object key should build");
assert_eq!(
object_key,
"generated-big-fish-assets/big-fish-session-1/level-main-image/level-1/image.png"
);
}
#[test]
fn build_generated_object_key_rejects_parent_segment() {
assert!(
build_generated_object_key(LegacyAssetPrefix::Characters, "../secret.png").is_err()
);
}
}

View File

@@ -2,13 +2,17 @@ use axum::{
Json, Json,
extract::{Extension, State}, extract::{Extension, State},
http::StatusCode, http::StatusCode,
response::Response, response::{
IntoResponse, Response,
sse::{Event, Sse},
},
}; };
use platform_llm::{LlmMessage, LlmMessageRole, LlmTextRequest}; use platform_llm::{LlmMessage, LlmMessageRole, LlmTextRequest};
use serde_json::Value; use serde_json::{Value, json};
use shared_contracts::llm::{ use shared_contracts::llm::{
LlmChatCompletionRequest, LlmChatCompletionResponse, LlmChatMessagePayload, LlmChatMessageRole, LlmChatCompletionRequest, LlmChatCompletionResponse, LlmChatMessagePayload, LlmChatMessageRole,
}; };
use std::convert::Infallible;
use crate::{ use crate::{
api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError, api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError,
@@ -20,15 +24,7 @@ pub async fn proxy_llm_chat_completions(
Extension(request_context): Extension<RequestContext>, Extension(request_context): Extension<RequestContext>,
Extension(_authenticated): Extension<AuthenticatedAccessToken>, Extension(_authenticated): Extension<AuthenticatedAccessToken>,
Json(payload): Json<LlmChatCompletionRequest>, Json(payload): Json<LlmChatCompletionRequest>,
) -> Result<Json<Value>, Response> { ) -> Result<Response, Response> {
if payload.stream {
return Err(llm_error_response(
&request_context,
AppError::from_status(StatusCode::NOT_IMPLEMENTED)
.with_message("Rust `api-server` 首版暂不支持流式 LLM 代理"),
));
}
let llm_client = state.llm_client().ok_or_else(|| { let llm_client = state.llm_client().ok_or_else(|| {
llm_error_response( llm_error_response(
&request_context, &request_context,
@@ -48,6 +44,10 @@ pub async fn proxy_llm_chat_completions(
enable_web_search: false, enable_web_search: false,
}; };
if payload.stream {
return Ok(stream_llm_chat_completions(llm_client.clone(), request).into_response());
}
let response = llm_client let response = llm_client
.request_text(request) .request_text(request)
.await .await
@@ -61,7 +61,78 @@ pub async fn proxy_llm_chat_completions(
content: response.content, content: response.content,
finish_reason: response.finish_reason, finish_reason: response.finish_reason,
}, },
)) )
.into_response())
}
fn stream_llm_chat_completions(
llm_client: platform_llm::LlmClient,
request: LlmTextRequest,
) -> Sse<impl tokio_stream::Stream<Item = Result<Event, Infallible>>> {
let stream = async_stream::stream! {
let (delta_tx, mut delta_rx) = tokio::sync::mpsc::unbounded_channel::<Value>();
let llm_stream = llm_client.stream_text(request, move |delta| {
let _ = delta_tx.send(json!({
"delta": delta.delta_text,
"content": delta.accumulated_text,
"finishReason": delta.finish_reason,
}));
});
tokio::pin!(llm_stream);
let llm_result = loop {
// `platform-llm` 负责上游 SSE 解析;这里尽快把增量转成 API 层 SSE 事件。
tokio::select! {
result = &mut llm_stream => break result,
maybe_delta = delta_rx.recv() => {
if let Some(delta) = maybe_delta {
yield Ok::<Event, Infallible>(llm_sse_json_event_or_error("delta", delta));
}
}
}
};
while let Some(delta) = delta_rx.recv().await {
yield Ok::<Event, Infallible>(llm_sse_json_event_or_error("delta", delta));
}
match llm_result {
Ok(response) => {
yield Ok::<Event, Infallible>(llm_sse_json_event_or_error(
"complete",
json!(LlmChatCompletionResponse {
id: response.response_id,
model: response.model,
content: response.content,
finish_reason: response.finish_reason,
}),
));
}
Err(error) => {
let app_error = map_llm_error(error);
yield Ok::<Event, Infallible>(llm_sse_json_event_or_error(
"error",
json!({
"code": app_error.code(),
"message": app_error.message(),
}),
));
}
}
yield Ok::<Event, Infallible>(Event::default().data("[DONE]"));
};
Sse::new(stream)
}
fn llm_sse_json_event_or_error(event_name: &str, payload: Value) -> Event {
match serde_json::to_string(&payload) {
Ok(payload_text) => Event::default().event(event_name).data(payload_text),
Err(_) => Event::default()
.event("error")
.data("{\"code\":\"INTERNAL_SERVER_ERROR\",\"message\":\"SSE payload 序列化失败\"}"),
}
} }
fn map_chat_message(message: LlmChatMessagePayload) -> LlmMessage { fn map_chat_message(message: LlmChatMessagePayload) -> LlmMessage {
@@ -105,6 +176,7 @@ mod tests {
status_line: &'static str, status_line: &'static str,
content_type: &'static str, content_type: &'static str,
body: String, body: String,
extra_headers: Vec<(&'static str, &'static str)>,
} }
#[tokio::test] #[tokio::test]
@@ -113,6 +185,7 @@ mod tests {
status_line: "200 OK", status_line: "200 OK",
content_type: "application/json; charset=utf-8", content_type: "application/json; charset=utf-8",
body: r#"{"id":"resp_api_server_01","model":"ark-router-test","choices":[{"message":{"content":""},"finish_reason":"stop"}]}"#.to_string(), body: r#"{"id":"resp_api_server_01","model":"ark-router-test","choices":[{"message":{"content":""},"finish_reason":"stop"}]}"#.to_string(),
extra_headers: Vec::new(),
}]); }]);
let state = seed_authenticated_state(AppConfig { let state = seed_authenticated_state(AppConfig {
llm_base_url: server_url, llm_base_url: server_url,
@@ -176,8 +249,25 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn llm_chat_completions_rejects_stream_mode() { async fn llm_chat_completions_streams_sse_payload() {
let state = seed_authenticated_state(AppConfig::default()).await; let server_url = spawn_mock_server(vec![MockResponse {
status_line: "200 OK",
content_type: "text/event-stream; charset=utf-8",
body: concat!(
"data: {\"choices\":[{\"delta\":{\"content\":\"\"}}]}\n\n",
"data: {\"choices\":[{\"delta\":{\"content\":\"\"}}]}\n\n",
"data: {\"choices\":[{\"finish_reason\":\"stop\"}]}\n\n",
"data: [DONE]\n\n"
)
.to_string(),
extra_headers: vec![("x-request-id", "req_llm_stream_01")],
}]);
let state = seed_authenticated_state(AppConfig {
llm_base_url: server_url,
llm_api_key: Some("test-key".to_string()),
..AppConfig::default()
})
.await;
let token = issue_access_token(&state); let token = issue_access_token(&state);
let app = build_router(state); let app = build_router(state);
@@ -188,7 +278,6 @@ mod tests {
.uri("/api/llm/chat/completions") .uri("/api/llm/chat/completions")
.header("authorization", format!("Bearer {token}")) .header("authorization", format!("Bearer {token}"))
.header("content-type", "application/json") .header("content-type", "application/json")
.header("x-genarrative-response-envelope", "v1")
.body(Body::from( .body(Body::from(
json!({ json!({
"stream": true, "stream": true,
@@ -203,7 +292,14 @@ mod tests {
.await .await
.expect("request should succeed"); .expect("request should succeed");
assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED); assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get("content-type")
.and_then(|value| value.to_str().ok()),
Some("text/event-stream")
);
let body = response let body = response
.into_body() .into_body()
@@ -211,14 +307,15 @@ mod tests {
.await .await
.expect("body should collect") .expect("body should collect")
.to_bytes(); .to_bytes();
let payload: Value = let body_text = String::from_utf8(body.to_vec()).expect("body should be utf8");
serde_json::from_slice(&body).expect("response body should be valid json");
assert_eq!(payload["ok"], Value::Bool(false)); assert!(body_text.contains("event: delta"));
assert_eq!( assert!(body_text.contains(r#""delta":"你""#));
payload["error"]["code"], assert!(body_text.contains(r#""content":"你好""#));
Value::String("NOT_IMPLEMENTED".to_string()) assert!(body_text.contains("event: complete"));
); assert!(body_text.contains(r#""id":"req_llm_stream_01""#));
assert!(body_text.contains(r#""finishReason":"stop""#));
assert!(body_text.contains("data: [DONE]"));
} }
async fn seed_authenticated_state(config: AppConfig) -> AppState { async fn seed_authenticated_state(config: AppConfig) -> AppState {
@@ -306,13 +403,17 @@ mod tests {
fn write_response(stream: &mut std::net::TcpStream, response: MockResponse) { fn write_response(stream: &mut std::net::TcpStream, response: MockResponse) {
let body = response.body; let body = response.body;
let raw_response = format!( let mut raw_response = format!(
"HTTP/1.1 {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", "HTTP/1.1 {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n",
response.status_line, response.status_line,
response.content_type, response.content_type,
body.len(), body.len()
body
); );
for (name, value) in response.extra_headers {
raw_response.push_str(format!("{name}: {value}\r\n").as_str());
}
raw_response.push_str("\r\n");
raw_response.push_str(body.as_str());
stream stream
.write_all(raw_response.as_bytes()) .write_all(raw_response.as_bytes())

View File

@@ -34,7 +34,6 @@ mod custom_world_rpg_draft_prompts;
mod error_middleware; mod error_middleware;
mod health; mod health;
mod http_error; mod http_error;
mod legacy_generated_assets;
mod llm; mod llm;
mod login_options; mod login_options;
mod logout; mod logout;

View File

@@ -17,7 +17,7 @@ use module_assets::{
AssetObjectAccessPolicy, AssetObjectFieldError, build_asset_entity_binding_input, AssetObjectAccessPolicy, AssetObjectFieldError, build_asset_entity_binding_input,
build_asset_object_upsert_input, generate_asset_binding_id, generate_asset_object_id, build_asset_object_upsert_input, generate_asset_binding_id, generate_asset_object_id,
}; };
use module_puzzle::{PuzzleBoardSnapshot, PuzzleGeneratedImageCandidate}; use module_puzzle::PuzzleGeneratedImageCandidate;
use platform_oss::{ use platform_oss::{
LegacyAssetPrefix, OssHeadObjectRequest, OssObjectAccess, OssPutObjectRequest, LegacyAssetPrefix, OssHeadObjectRequest, OssObjectAccess, OssPutObjectRequest,
OssSignedGetObjectUrlRequest, OssSignedGetObjectUrlRequest,
@@ -36,11 +36,10 @@ use shared_contracts::{
}, },
puzzle_gallery::{PuzzleGalleryDetailResponse, PuzzleGalleryResponse}, puzzle_gallery::{PuzzleGalleryDetailResponse, PuzzleGalleryResponse},
puzzle_runtime::{ puzzle_runtime::{
AdvanceLocalPuzzleNextLevelRequest, DragPuzzlePieceRequest, PuzzleBoardSnapshotResponse, DragPuzzlePieceRequest, PuzzleBoardSnapshotResponse, PuzzleCellPositionResponse,
PuzzleCellPositionResponse, PuzzleLeaderboardEntryResponse, PuzzleMergedGroupStateResponse, PuzzleLeaderboardEntryResponse, PuzzleMergedGroupStateResponse, PuzzlePieceStateResponse,
PuzzlePieceStateResponse, PuzzleRunResponse, PuzzleRunSnapshotResponse, PuzzleRunResponse, PuzzleRunSnapshotResponse, PuzzleRuntimeLevelSnapshotResponse,
PuzzleRuntimeLevelSnapshotResponse, StartPuzzleRunRequest, SubmitPuzzleLeaderboardRequest, StartPuzzleRunRequest, SubmitPuzzleLeaderboardRequest, SwapPuzzlePiecesRequest,
SwapPuzzlePiecesRequest,
}, },
puzzle_works::{ puzzle_works::{
PutPuzzleWorkRequest, PuzzleWorkDetailResponse, PuzzleWorkMutationResponse, PutPuzzleWorkRequest, PuzzleWorkDetailResponse, PuzzleWorkMutationResponse,
@@ -52,14 +51,13 @@ use spacetime_client::{
PuzzleAgentMessageRecord, PuzzleAgentMessageSubmitRecordInput, PuzzleAgentMessageRecord, PuzzleAgentMessageSubmitRecordInput,
PuzzleAgentSessionCreateRecordInput, PuzzleAgentSessionRecord, PuzzleAgentSessionCreateRecordInput, PuzzleAgentSessionRecord,
PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord, PuzzleAnchorPackRecord, PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord, PuzzleAnchorPackRecord,
PuzzleBoardRecord, PuzzleCellPositionRecord, PuzzleCreatorIntentRecord, PuzzleCreatorIntentRecord, PuzzleGeneratedImageCandidateRecord,
PuzzleGeneratedImageCandidateRecord, PuzzleGeneratedImagesSaveRecordInput, PuzzleGeneratedImagesSaveRecordInput, PuzzleLeaderboardEntryRecord,
PuzzleLeaderboardEntryRecord, PuzzleLeaderboardSubmitRecordInput, PuzzleMergedGroupRecord, PuzzleLeaderboardSubmitRecordInput, PuzzlePublishRecordInput, PuzzleResultDraftRecord,
PuzzlePieceStateRecord, PuzzlePublishRecordInput, PuzzleResultDraftRecord,
PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord, PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord,
PuzzleRunDragRecordInput, PuzzleRunRecord, PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, PuzzleRunDragRecordInput, PuzzleRunRecord, PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput,
PuzzleRuntimeLevelRecord, PuzzleSelectCoverImageRecordInput, PuzzleWorkProfileRecord, PuzzleSelectCoverImageRecordInput, PuzzleWorkProfileRecord, PuzzleWorkUpsertRecordInput,
PuzzleWorkUpsertRecordInput, SpacetimeClientError, SpacetimeClientError,
}; };
use std::convert::Infallible; use std::convert::Infallible;
use tokio::time::sleep; use tokio::time::sleep;
@@ -1100,36 +1098,6 @@ pub async fn advance_puzzle_next_level(
)) ))
} }
pub async fn advance_local_puzzle_next_level(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
payload: Result<Json<AdvanceLocalPuzzleNextLevelRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
let Json(payload) = payload.map_err(|error| {
puzzle_error_response(
&request_context,
PUZZLE_RUNTIME_PROVIDER,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": PUZZLE_RUNTIME_PROVIDER,
"message": error.body_text(),
})),
)
})?;
let owner_user_id = authenticated.claims().user_id().to_string();
let run = build_local_next_puzzle_run(&state, payload, owner_user_id.as_str())
.await
.map_err(|error| puzzle_error_response(&request_context, PUZZLE_RUNTIME_PROVIDER, error))?;
Ok(json_success_body(
Some(&request_context),
PuzzleRunResponse {
run: map_puzzle_run_response(run),
},
))
}
pub async fn submit_puzzle_leaderboard( pub async fn submit_puzzle_leaderboard(
State(state): State<AppState>, State(state): State<AppState>,
AxumPath(run_id): AxumPath<String>, AxumPath(run_id): AxumPath<String>,
@@ -1385,98 +1353,6 @@ fn map_puzzle_run_response(run: PuzzleRunRecord) -> PuzzleRunSnapshotResponse {
} }
} }
fn map_puzzle_run_request_record(run: PuzzleRunSnapshotResponse) -> PuzzleRunRecord {
PuzzleRunRecord {
run_id: run.run_id,
entry_profile_id: run.entry_profile_id,
cleared_level_count: run.cleared_level_count,
current_level_index: run.current_level_index,
current_grid_size: run.current_grid_size,
played_profile_ids: run.played_profile_ids,
previous_level_tags: run.previous_level_tags,
current_level: run.current_level.map(map_puzzle_level_request_record),
recommended_next_profile_id: run.recommended_next_profile_id,
leaderboard_entries: run
.leaderboard_entries
.into_iter()
.map(map_puzzle_leaderboard_request_record)
.collect(),
}
}
fn map_puzzle_level_request_record(
level: PuzzleRuntimeLevelSnapshotResponse,
) -> PuzzleRuntimeLevelRecord {
PuzzleRuntimeLevelRecord {
run_id: level.run_id,
level_index: level.level_index,
grid_size: level.grid_size,
profile_id: level.profile_id,
level_name: level.level_name,
author_display_name: level.author_display_name,
theme_tags: level.theme_tags,
cover_image_src: level.cover_image_src,
board: map_puzzle_board_request_record(level.board),
status: level.status,
started_at_ms: level.started_at_ms,
cleared_at_ms: level.cleared_at_ms,
elapsed_ms: level.elapsed_ms,
leaderboard_entries: level
.leaderboard_entries
.into_iter()
.map(map_puzzle_leaderboard_request_record)
.collect(),
}
}
fn map_puzzle_leaderboard_request_record(
entry: PuzzleLeaderboardEntryResponse,
) -> PuzzleLeaderboardEntryRecord {
PuzzleLeaderboardEntryRecord {
rank: entry.rank,
nickname: entry.nickname,
elapsed_ms: entry.elapsed_ms,
is_current_player: entry.is_current_player,
}
}
fn map_puzzle_board_request_record(board: PuzzleBoardSnapshotResponse) -> PuzzleBoardRecord {
PuzzleBoardRecord {
rows: board.rows,
cols: board.cols,
pieces: board
.pieces
.into_iter()
.map(|piece| PuzzlePieceStateRecord {
piece_id: piece.piece_id,
correct_row: piece.correct_row,
correct_col: piece.correct_col,
current_row: piece.current_row,
current_col: piece.current_col,
merged_group_id: piece.merged_group_id,
})
.collect(),
merged_groups: board
.merged_groups
.into_iter()
.map(|group| PuzzleMergedGroupRecord {
group_id: group.group_id,
piece_ids: group.piece_ids,
occupied_cells: group
.occupied_cells
.into_iter()
.map(|cell| PuzzleCellPositionRecord {
row: cell.row,
col: cell.col,
})
.collect(),
})
.collect(),
selected_piece_id: board.selected_piece_id,
all_tiles_resolved: board.all_tiles_resolved,
}
}
fn map_puzzle_runtime_level_response( fn map_puzzle_runtime_level_response(
level: spacetime_client::PuzzleRuntimeLevelRecord, level: spacetime_client::PuzzleRuntimeLevelRecord,
) -> PuzzleRuntimeLevelSnapshotResponse { ) -> PuzzleRuntimeLevelSnapshotResponse {
@@ -1842,343 +1718,6 @@ async fn generate_puzzle_image_candidates(
.collect()) .collect())
} }
async fn build_local_next_puzzle_run(
state: &AppState,
payload: AdvanceLocalPuzzleNextLevelRequest,
owner_user_id: &str,
) -> Result<PuzzleRunRecord, AppError> {
let run = map_puzzle_run_request_record(payload.run);
let current_level = run.current_level.clone().ok_or_else(|| {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": PUZZLE_RUNTIME_PROVIDER,
"message": "currentLevel is required",
}))
})?;
if current_level.status != "cleared" {
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": PUZZLE_RUNTIME_PROVIDER,
"message": "current level is not cleared",
})),
);
}
if let Some(gallery_item) = resolve_gallery_next_puzzle_work(state, &run).await? {
return Ok(build_next_run_from_puzzle_work(run, gallery_item));
}
let source_session_id = payload.source_session_id.unwrap_or_default();
if source_session_id.trim().is_empty() {
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": PUZZLE_RUNTIME_PROVIDER,
"message": "sourceSessionId is required when gallery has no next puzzle work",
})),
);
}
let session = state
.spacetime_client()
.get_puzzle_agent_session(source_session_id, owner_user_id.to_string())
.await
.map_err(map_puzzle_client_error)?;
if let Some(candidate) = session
.draft
.as_ref()
.and_then(|draft| pick_unused_puzzle_candidate(&draft.candidates, &run.played_profile_ids))
{
return Ok(build_next_run_from_candidate(run, &session, candidate));
}
let draft = session.draft.clone().ok_or_else(|| {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": PUZZLE_RUNTIME_PROVIDER,
"message": "puzzle draft is required when gallery has no next puzzle work",
}))
})?;
let candidates = generate_puzzle_image_candidates(
state,
owner_user_id,
&session.session_id,
&draft.level_name,
&draft.summary,
None,
1,
draft.candidates.len(),
)
.await
.map_err(|message| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": PUZZLE_RUNTIME_PROVIDER,
"message": message,
}))
})?;
let candidates_json = serde_json::to_string(
&candidates
.iter()
.map(to_puzzle_generated_image_candidate)
.collect::<Vec<_>>(),
)
.map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": PUZZLE_RUNTIME_PROVIDER,
"message": format!("拼图候选图序列化失败:{error}"),
}))
})?;
let updated_session = state
.spacetime_client()
.save_puzzle_generated_images(PuzzleGeneratedImagesSaveRecordInput {
session_id: session.session_id,
owner_user_id: owner_user_id.to_string(),
candidates_json,
saved_at_micros: current_utc_micros(),
})
.await
.map_err(map_puzzle_client_error)?;
let candidate = updated_session
.draft
.as_ref()
.and_then(|draft| {
draft
.candidates
.iter()
.find(|candidate| !candidate.image_src.is_empty())
})
.ok_or_else(|| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": PUZZLE_RUNTIME_PROVIDER,
"message": "现场生成后没有可用候选图",
}))
})?;
Ok(build_next_run_from_candidate(
run,
&updated_session,
candidate,
))
}
async fn resolve_gallery_next_puzzle_work(
state: &AppState,
run: &PuzzleRunRecord,
) -> Result<Option<PuzzleWorkProfileRecord>, AppError> {
let items = state
.spacetime_client()
.list_puzzle_gallery()
.await
.map_err(map_puzzle_client_error)?;
Ok(items.into_iter().find(|item| {
item.publication_status == "published"
&& item
.cover_image_src
.as_ref()
.is_some_and(|value| !value.is_empty())
&& !run.played_profile_ids.contains(&item.profile_id)
}))
}
fn pick_unused_puzzle_candidate<'a>(
candidates: &'a [PuzzleGeneratedImageCandidateRecord],
played_profile_ids: &[String],
) -> Option<&'a PuzzleGeneratedImageCandidateRecord> {
candidates.iter().find(|candidate| {
!candidate.image_src.is_empty()
&& !played_profile_ids
.iter()
.any(|profile_id| profile_id.contains(&candidate.candidate_id))
})
}
fn build_next_run_from_puzzle_work(
run: PuzzleRunRecord,
item: PuzzleWorkProfileRecord,
) -> PuzzleRunRecord {
build_next_run_from_parts(
run,
item.profile_id,
item.level_name,
item.author_display_name,
item.theme_tags,
item.cover_image_src,
)
}
fn build_next_run_from_candidate(
run: PuzzleRunRecord,
session: &PuzzleAgentSessionRecord,
candidate: &PuzzleGeneratedImageCandidateRecord,
) -> PuzzleRunRecord {
let draft = session.draft.as_ref();
let level_index = run.current_level_index + 1;
build_next_run_from_parts(
run,
format!(
"{}-{}-level-{}",
session.session_id, candidate.candidate_id, level_index
),
draft
.map(|draft| format!("{} · 候选 {}", draft.level_name, level_index))
.unwrap_or_else(|| format!("候选拼图 {level_index}")),
"当前草稿".to_string(),
draft
.map(|draft| draft.theme_tags.clone())
.unwrap_or_default(),
Some(candidate.image_src.clone()),
)
}
fn build_next_run_from_parts(
run: PuzzleRunRecord,
profile_id: String,
level_name: String,
author_display_name: String,
theme_tags: Vec<String>,
cover_image_src: Option<String>,
) -> PuzzleRunRecord {
let next_level_index = run.current_level_index + 1;
let grid_size = if run.cleared_level_count >= 3 { 4 } else { 3 };
let mut played_profile_ids = run.played_profile_ids.clone();
if !played_profile_ids.contains(&profile_id) {
played_profile_ids.push(profile_id.clone());
}
let board = build_local_puzzle_board(grid_size, &run.run_id, &profile_id, next_level_index);
PuzzleRunRecord {
run_id: run.run_id.clone(),
entry_profile_id: run.entry_profile_id,
cleared_level_count: run.cleared_level_count,
current_level_index: next_level_index,
current_grid_size: grid_size,
played_profile_ids,
previous_level_tags: theme_tags.clone(),
current_level: Some(PuzzleRuntimeLevelRecord {
run_id: run.run_id,
level_index: next_level_index,
grid_size,
profile_id,
level_name,
author_display_name,
theme_tags,
cover_image_src,
board,
status: "playing".to_string(),
started_at_ms: (current_utc_micros().max(0) as u64) / 1_000,
cleared_at_ms: None,
elapsed_ms: None,
leaderboard_entries: Vec::new(),
}),
recommended_next_profile_id: None,
leaderboard_entries: Vec::new(),
}
}
fn build_local_puzzle_board(
grid_size: u32,
run_id: &str,
profile_id: &str,
level_index: u32,
) -> PuzzleBoardRecord {
let board = module_puzzle::build_initial_board_with_seed(
grid_size,
build_local_puzzle_shuffle_seed(run_id, profile_id, level_index, grid_size),
)
.unwrap_or_else(|_| {
module_puzzle::build_initial_board_with_seed(3, 1)
.expect("fallback puzzle board should use supported grid size")
});
map_puzzle_board_snapshot_record(board)
}
fn build_local_puzzle_shuffle_seed(
run_id: &str,
profile_id: &str,
level_index: u32,
grid_size: u32,
) -> u64 {
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
for byte in run_id
.bytes()
.chain(profile_id.bytes())
.chain(level_index.to_le_bytes())
.chain(grid_size.to_le_bytes())
{
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
fn map_puzzle_board_snapshot_record(board: PuzzleBoardSnapshot) -> PuzzleBoardRecord {
PuzzleBoardRecord {
rows: board.rows,
cols: board.cols,
pieces: board
.pieces
.into_iter()
.map(|piece| PuzzlePieceStateRecord {
piece_id: piece.piece_id,
correct_row: piece.correct_row,
correct_col: piece.correct_col,
current_row: piece.current_row,
current_col: piece.current_col,
merged_group_id: piece.merged_group_id,
})
.collect(),
merged_groups: board
.merged_groups
.into_iter()
.map(|group| PuzzleMergedGroupRecord {
group_id: group.group_id,
piece_ids: group.piece_ids,
occupied_cells: group
.occupied_cells
.into_iter()
.map(|cell| PuzzleCellPositionRecord {
row: cell.row,
col: cell.col,
})
.collect(),
})
.collect(),
selected_piece_id: board.selected_piece_id,
all_tiles_resolved: board.all_tiles_resolved,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn board_positions(board: &PuzzleBoardRecord) -> Vec<(u32, u32)> {
board
.pieces
.iter()
.map(|piece| (piece.current_row, piece.current_col))
.collect()
}
fn has_original_neighbor_pair(board: &PuzzleBoardRecord) -> bool {
board.pieces.iter().any(|piece| {
board.pieces.iter().any(|candidate| {
piece.piece_id != candidate.piece_id
&& piece.current_row.abs_diff(candidate.current_row)
+ piece.current_col.abs_diff(candidate.current_col)
== 1
&& piece.correct_row.abs_diff(candidate.correct_row)
+ piece.correct_col.abs_diff(candidate.correct_col)
== 1
})
})
}
#[test]
fn local_next_level_board_shuffle_changes_by_level() {
let second = build_local_puzzle_board(3, "run-a", "profile-level-2", 2);
let third = build_local_puzzle_board(3, "run-a", "profile-level-3", 3);
assert_ne!(board_positions(&second), board_positions(&third));
assert!(!has_original_neighbor_pair(&second));
assert!(!has_original_neighbor_pair(&third));
}
}
struct PuzzleDashScopeSettings { struct PuzzleDashScopeSettings {
base_url: String, base_url: String,
api_key: String, api_key: String,

View File

@@ -4,12 +4,17 @@ use axum::{
http::StatusCode, http::StatusCode,
response::Response, response::Response,
}; };
use module_runtime::RuntimeSnapshotRecord;
use serde_json::{Value, json}; use serde_json::{Value, json};
use shared_contracts::story::{ use shared_contracts::story::{
BeginStorySessionRequest, ContinueStoryRequest, StoryEventPayload, BeginStoryRuntimeSessionRequest, BeginStorySessionRequest, ContinueStoryRequest,
StorySessionMutationResponse, StorySessionPayload, StorySessionStateResponse, ResolveStoryRuntimeActionRequest, StoryEventPayload, StoryRuntimeMutationResponse,
StoryRuntimeSnapshotPayload, StorySessionMutationResponse, StorySessionPayload,
StorySessionStateResponse,
}; };
use shared_kernel::{offset_datetime_to_unix_micros, parse_rfc3339};
use spacetime_client::SpacetimeClientError; use spacetime_client::SpacetimeClientError;
use time::OffsetDateTime;
use crate::{ use crate::{
api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError, api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError,
@@ -43,28 +48,75 @@ pub async fn begin_story_session(
Ok(json_success_body( Ok(json_success_body(
Some(&request_context), Some(&request_context),
StorySessionMutationResponse { StorySessionMutationResponse {
story_session: StorySessionPayload { story_session: story_session_payload_from_record(result.session),
story_session_id: result.session.story_session_id, story_event: story_event_payload_from_record(result.event),
runtime_session_id: result.session.runtime_session_id, },
actor_user_id: result.session.actor_user_id, ))
world_profile_id: result.session.world_profile_id, }
initial_prompt: result.session.initial_prompt,
opening_summary: result.session.opening_summary, pub async fn begin_story_runtime_session(
latest_narrative_text: result.session.latest_narrative_text, State(state): State<AppState>,
latest_choice_function_id: result.session.latest_choice_function_id, Extension(request_context): Extension<RequestContext>,
status: result.session.status, Extension(authenticated): Extension<AuthenticatedAccessToken>,
version: result.session.version, Json(payload): Json<BeginStoryRuntimeSessionRequest>,
created_at: result.session.created_at, ) -> Result<Json<Value>, Response> {
updated_at: result.session.updated_at, let now_micros = current_utc_micros();
}, let actor_user_id = authenticated.claims().user_id().to_string();
story_event: StoryEventPayload { let runtime_session_id = module_runtime_story::generate_runtime_session_id(
event_id: result.event.event_id, actor_user_id.as_str(),
story_session_id: result.event.story_session_id, payload.custom_world_profile.as_ref(),
event_kind: result.event.event_kind, &payload.character,
narrative_text: result.event.narrative_text, now_micros,
choice_function_id: result.event.choice_function_id, );
created_at: result.event.created_at, let story_session_id = module_story::generate_story_session_id(now_micros);
}, let built = module_runtime_story::build_runtime_story_bootstrap(
&payload,
module_runtime_story::RuntimeStoryBootstrapSeed {
runtime_session_id,
story_session_id,
actor_user_id: actor_user_id.clone(),
now_micros,
},
)
.map_err(|message| {
story_sessions_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "story-runtime",
"message": message,
})),
)
})?;
let story_result = state
.spacetime_client()
.begin_story_session(
built.story_session_id.clone(),
built.runtime_session_id.clone(),
actor_user_id.clone(),
built.world_profile_id,
built.initial_prompt,
built.opening_summary,
now_micros,
)
.await
.map_err(|error| {
story_sessions_error_response(&request_context, map_story_session_client_error(error))
})?;
let persisted =
persist_story_runtime_snapshot(&state, &request_context, actor_user_id, built.snapshot)
.await?;
Ok(json_success_body(
Some(&request_context),
StoryRuntimeMutationResponse {
projection: build_story_runtime_projection_from_persisted(
story_session_payload_from_record(story_result.session),
vec![story_event_payload_from_record(story_result.event)],
&persisted,
persisted.version,
),
}, },
)) ))
} }
@@ -108,28 +160,105 @@ pub async fn continue_story(
Ok(json_success_body( Ok(json_success_body(
Some(&request_context), Some(&request_context),
StorySessionMutationResponse { StorySessionMutationResponse {
story_session: StorySessionPayload { story_session: story_session_payload_from_record(result.session),
story_session_id: result.session.story_session_id, story_event: story_event_payload_from_record(result.event),
runtime_session_id: result.session.runtime_session_id, },
actor_user_id: result.session.actor_user_id, ))
world_profile_id: result.session.world_profile_id, }
initial_prompt: result.session.initial_prompt,
opening_summary: result.session.opening_summary, pub async fn resolve_story_runtime_action(
latest_narrative_text: result.session.latest_narrative_text, State(state): State<AppState>,
latest_choice_function_id: result.session.latest_choice_function_id, Path(story_session_id): Path<String>,
status: result.session.status, Extension(request_context): Extension<RequestContext>,
version: result.session.version, Extension(authenticated): Extension<AuthenticatedAccessToken>,
created_at: result.session.created_at, Json(payload): Json<ResolveStoryRuntimeActionRequest>,
updated_at: result.session.updated_at, ) -> Result<Json<Value>, Response> {
}, let now_micros = current_utc_micros();
story_event: StoryEventPayload { let actor_user_id = authenticated.claims().user_id().to_string();
event_id: result.event.event_id, let story_session_id = validate_story_runtime_action_path(
story_session_id: result.event.story_session_id, &request_context,
event_kind: result.event.event_kind, story_session_id,
narrative_text: result.event.narrative_text, payload.story_session_id.as_str(),
choice_function_id: result.event.choice_function_id, )?;
created_at: result.event.created_at, let story_state = state
}, .spacetime_client()
.get_story_session_state(story_session_id.clone())
.await
.map_err(|error| {
story_sessions_error_response(&request_context, map_story_session_client_error(error))
})?;
require_story_session_owner(
&request_context,
&story_state.session.actor_user_id,
&actor_user_id,
)?;
let snapshot_record = state
.get_runtime_snapshot_record(actor_user_id.clone())
.await
.map_err(|error| {
story_sessions_error_response(&request_context, map_story_session_client_error(error))
})?
.ok_or_else(|| {
story_sessions_error_response(
&request_context,
AppError::from_status(StatusCode::CONFLICT).with_details(json!({
"provider": "story-runtime",
"message": "当前用户缺少 runtime snapshot",
})),
)
})?;
let snapshot = story_runtime_snapshot_payload_from_record(&snapshot_record);
validate_story_runtime_client_version(
&request_context,
payload.client_version,
&snapshot.game_state,
)?;
let resolved = module_runtime_story::resolve_story_runtime_action(
module_runtime_story::StoryRuntimeActionResolveInput {
story_session_id: story_state.session.story_session_id.clone(),
runtime_session_id: story_state.session.runtime_session_id.clone(),
snapshot,
request: payload,
},
)
.map_err(|message| {
story_sessions_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "story-runtime",
"message": message,
})),
)
})?;
let story_result = state
.spacetime_client()
.continue_story(
story_state.session.story_session_id,
module_story::generate_story_event_id(now_micros),
resolved.narrative_text,
resolved.choice_function_id,
now_micros,
)
.await
.map_err(|error| {
story_sessions_error_response(&request_context, map_story_session_client_error(error))
})?;
let persisted =
persist_story_runtime_snapshot(&state, &request_context, actor_user_id, resolved.snapshot)
.await?;
Ok(json_success_body(
Some(&request_context),
StoryRuntimeMutationResponse {
projection: build_story_runtime_projection_from_persisted(
story_session_payload_from_record(story_result.session),
vec![story_event_payload_from_record(story_result.event)],
&persisted,
resolved.server_version.max(persisted.version),
),
}, },
)) ))
} }
@@ -157,36 +286,257 @@ pub async fn get_story_session_state(
Ok(json_success_body( Ok(json_success_body(
Some(&request_context), Some(&request_context),
StorySessionStateResponse { StorySessionStateResponse {
story_session: StorySessionPayload { story_session: story_session_payload_from_record(result.session),
story_session_id: result.session.story_session_id,
runtime_session_id: result.session.runtime_session_id,
actor_user_id: result.session.actor_user_id,
world_profile_id: result.session.world_profile_id,
initial_prompt: result.session.initial_prompt,
opening_summary: result.session.opening_summary,
latest_narrative_text: result.session.latest_narrative_text,
latest_choice_function_id: result.session.latest_choice_function_id,
status: result.session.status,
version: result.session.version,
created_at: result.session.created_at,
updated_at: result.session.updated_at,
},
story_events: result story_events: result
.events .events
.into_iter() .into_iter()
.map(|event| StoryEventPayload { .map(story_event_payload_from_record)
event_id: event.event_id,
story_session_id: event.story_session_id,
event_kind: event.event_kind,
narrative_text: event.narrative_text,
choice_function_id: event.choice_function_id,
created_at: event.created_at,
})
.collect(), .collect(),
}, },
)) ))
} }
async fn persist_story_runtime_snapshot(
state: &AppState,
request_context: &RequestContext,
user_id: String,
snapshot: StoryRuntimeSnapshotPayload,
) -> Result<RuntimeSnapshotRecord, Response> {
validate_story_runtime_snapshot_payload(&snapshot).map_err(|message| {
story_sessions_error_response(
request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "story-runtime",
"message": message,
})),
)
})?;
let now = OffsetDateTime::now_utc();
let saved_at = snapshot
.saved_at
.as_deref()
.and_then(module_runtime_story::normalize_required_string)
.map(|value| parse_rfc3339(value.as_str()))
.transpose()
.map_err(|error| {
story_sessions_error_response(
request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "story-runtime",
"field": "snapshot.savedAt",
"message": format!("savedAt 非法: {error}"),
})),
)
})?
.unwrap_or(now);
let saved_at_micros = offset_datetime_to_unix_micros(saved_at);
let updated_at_micros = offset_datetime_to_unix_micros(now);
let game_state = canonicalize_story_runtime_game_state(snapshot.game_state);
state
.put_runtime_snapshot_record(
user_id,
saved_at_micros,
snapshot.bottom_tab,
game_state,
snapshot.current_story,
updated_at_micros,
)
.await
.map_err(|error| {
story_sessions_error_response(request_context, map_story_session_client_error(error))
})
}
fn canonicalize_story_runtime_game_state(mut game_state: Value) -> Value {
if let Some(root) = game_state.as_object_mut() {
// 中文注释NPC 交易 / 赠礼 view 是展示层投影,持久快照只保存可复算真相。
root.remove("runtimeNpcInteraction");
}
game_state
}
fn validate_story_runtime_snapshot_payload(
snapshot: &StoryRuntimeSnapshotPayload,
) -> Result<(), String> {
if module_runtime_story::normalize_required_string(snapshot.bottom_tab.as_str()).is_none() {
return Err("snapshot.bottomTab 不能为空".to_string());
}
if !snapshot.game_state.is_object() {
return Err("snapshot.gameState 必须是 JSON object".to_string());
}
if snapshot
.current_story
.as_ref()
.is_some_and(|current_story| !current_story.is_object())
{
return Err("snapshot.currentStory 必须是 JSON object 或 null".to_string());
}
Ok(())
}
fn story_runtime_snapshot_payload_from_record(
record: &RuntimeSnapshotRecord,
) -> StoryRuntimeSnapshotPayload {
let mut game_state = record.game_state.clone();
module_runtime_story::write_runtime_npc_interaction_view(&mut game_state);
StoryRuntimeSnapshotPayload {
saved_at: Some(record.saved_at.clone()),
bottom_tab: record.bottom_tab.clone(),
game_state,
current_story: record.current_story.clone(),
}
}
fn build_story_runtime_projection_from_persisted(
story_session: StorySessionPayload,
story_events: Vec<StoryEventPayload>,
record: &RuntimeSnapshotRecord,
server_version: u32,
) -> shared_contracts::story::StoryRuntimeProjectionResponse {
let snapshot = story_runtime_snapshot_payload_from_record(record);
let current_story = snapshot.current_story.as_ref();
let options =
module_runtime_story::build_runtime_story_options(current_story, &snapshot.game_state);
let current_narrative_text = read_story_runtime_current_text(current_story)
.or_else(|| Some(story_session.latest_narrative_text.clone()));
let action_result_text = read_story_runtime_current_field(current_story, "resultText");
let toast = read_story_runtime_current_field(current_story, "toast");
module_runtime_story::build_story_runtime_projection(
module_runtime_story::StoryRuntimeProjectionSource {
story_session,
story_events,
game_state: snapshot.game_state,
options,
server_version,
current_narrative_text,
action_result_text,
toast,
},
)
}
fn read_story_runtime_current_text(current_story: Option<&Value>) -> Option<String> {
read_story_runtime_current_field(current_story, "text")
.or_else(|| read_story_runtime_current_field(current_story, "storyText"))
}
fn read_story_runtime_current_field(current_story: Option<&Value>, field: &str) -> Option<String> {
current_story?
.as_object()?
.get(field)?
.as_str()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
fn validate_story_runtime_action_path(
request_context: &RequestContext,
path_story_session_id: String,
body_story_session_id: &str,
) -> Result<String, Response> {
let path_story_session_id =
module_runtime_story::normalize_required_string(path_story_session_id.as_str())
.ok_or_else(|| {
story_sessions_error_response(
request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "story-runtime",
"message": "storySessionId 不能为空",
})),
)
})?;
let body_story_session_id = module_runtime_story::normalize_required_string(
body_story_session_id,
)
.ok_or_else(|| {
story_sessions_error_response(
request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "story-runtime",
"message": "request.storySessionId 不能为空",
})),
)
})?;
if path_story_session_id != body_story_session_id {
return Err(story_sessions_error_response(
request_context,
AppError::from_status(StatusCode::CONFLICT).with_details(json!({
"provider": "story-runtime",
"message": "path storySessionId 与 request.storySessionId 不一致",
"pathStorySessionId": path_story_session_id,
"requestStorySessionId": body_story_session_id,
})),
));
}
Ok(path_story_session_id)
}
fn validate_story_runtime_client_version(
request_context: &RequestContext,
client_version: Option<u32>,
game_state: &Value,
) -> Result<(), Response> {
let Some(client_version) = client_version else {
return Ok(());
};
let Some(server_version) =
module_runtime_story::read_u32_field(game_state, "runtimeActionVersion")
else {
return Ok(());
};
if client_version == server_version {
return Ok(());
}
Err(story_sessions_error_response(
request_context,
AppError::from_status(StatusCode::CONFLICT).with_details(json!({
"provider": "story-runtime",
"message": "运行时版本已变化,请先同步最新快照后再提交动作",
"clientVersion": client_version,
"serverVersion": server_version,
})),
))
}
fn story_session_payload_from_record(
record: module_story::StorySessionRecord,
) -> StorySessionPayload {
StorySessionPayload {
story_session_id: record.story_session_id,
runtime_session_id: record.runtime_session_id,
actor_user_id: record.actor_user_id,
world_profile_id: record.world_profile_id,
initial_prompt: record.initial_prompt,
opening_summary: record.opening_summary,
latest_narrative_text: record.latest_narrative_text,
latest_choice_function_id: record.latest_choice_function_id,
status: record.status,
version: record.version,
created_at: record.created_at,
updated_at: record.updated_at,
}
}
fn story_event_payload_from_record(record: module_story::StoryEventRecord) -> StoryEventPayload {
StoryEventPayload {
event_id: record.event_id,
story_session_id: record.story_session_id,
event_kind: record.event_kind,
narrative_text: record.narrative_text,
choice_function_id: record.choice_function_id,
created_at: record.created_at,
}
}
pub async fn get_story_runtime_projection( pub async fn get_story_runtime_projection(
State(state): State<AppState>, State(state): State<AppState>,
Path(story_session_id): Path<String>, Path(story_session_id): Path<String>,
@@ -373,6 +723,156 @@ mod tests {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED); assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
} }
#[tokio::test]
async fn begin_story_runtime_session_requires_authentication() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/story/sessions/runtime")
.header("content-type", "application/json")
.body(Body::from(
json!({
"worldType": "CUSTOM",
"customWorldProfile": { "id": "profile_001" },
"character": { "id": "hero_001", "name": "沈砺" },
"runtimeMode": "play",
"disablePersistence": false
})
.to_string(),
))
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn begin_story_runtime_session_returns_bad_gateway_when_spacetime_not_published() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let app = build_router(state);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/story/sessions/runtime")
.header("authorization", format!("Bearer {token}"))
.header("content-type", "application/json")
.header("x-genarrative-response-envelope", "v1")
.body(Body::from(
json!({
"worldType": "CUSTOM",
"customWorldProfile": { "id": "profile_001" },
"character": { "id": "hero_001", "name": "沈砺" },
"runtimeMode": "play",
"disablePersistence": false
})
.to_string(),
))
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
let body = response
.into_body()
.collect()
.await
.expect("body should collect")
.to_bytes();
let payload: Value =
serde_json::from_slice(&body).expect("response body should be valid json");
assert_eq!(payload["ok"], Value::Bool(false));
assert_eq!(
payload["error"]["details"]["provider"],
Value::String("spacetimedb".to_string())
);
}
#[tokio::test]
async fn resolve_story_runtime_action_requires_authentication() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/story/sessions/storysess_001/actions/resolve")
.header("content-type", "application/json")
.body(Body::from(
json!({
"storySessionId": "storysess_001",
"clientVersion": 1,
"functionId": "idle_observe_signs",
"actionText": "观察周围迹象",
"payload": { "optionText": "观察周围迹象" }
})
.to_string(),
))
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn resolve_story_runtime_action_returns_bad_gateway_when_spacetime_not_published() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let app = build_router(state);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/story/sessions/storysess_001/actions/resolve")
.header("authorization", format!("Bearer {token}"))
.header("content-type", "application/json")
.header("x-genarrative-response-envelope", "v1")
.body(Body::from(
json!({
"storySessionId": "storysess_001",
"clientVersion": 1,
"functionId": "idle_observe_signs",
"actionText": "观察周围迹象",
"payload": { "optionText": "观察周围迹象" }
})
.to_string(),
))
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
let body = response
.into_body()
.collect()
.await
.expect("body should collect")
.to_bytes();
let payload: Value =
serde_json::from_slice(&body).expect("response body should be valid json");
assert_eq!(payload["ok"], Value::Bool(false));
assert_eq!(
payload["error"]["details"]["provider"],
Value::String("spacetimedb".to_string())
);
}
#[tokio::test] #[tokio::test]
async fn continue_story_returns_bad_gateway_when_spacetime_not_published() { async fn continue_story_returns_bad_gateway_when_spacetime_not_published() {
let state = seed_authenticated_state().await; let state = seed_authenticated_state().await;

View File

@@ -14,7 +14,7 @@
## 2. 当前阶段说明 ## 2. 当前阶段说明
当前提交尚未进入完整资产状态建模,但已完成与本模块直接相关的前置基础设施与首版 schema 骨架 当前资产对象主链已完成后端收口资产对象确认、实体槽位绑定、历史读取、OSS 对象确认、API facade、SpacetimeDB adapter 和资产事件表已经形成同一条后端真相链。与本模块直接相关的基础设施包括
1. `api-server` 已具备 `POST /api/assets/direct-upload-tickets` 1. `api-server` 已具备 `POST /api/assets/direct-upload-tickets`
2. `platform-oss` 已具备旧 `/generated-*` 前缀兼容的 `PostObject` 签名能力 2. `platform-oss` 已具备旧 `/generated-*` 前缀兼容的 `PostObject` 签名能力
@@ -25,7 +25,8 @@
- `assetobj_` ID 前缀与初始版本常量 - `assetobj_` ID 前缀与初始版本常量
- `asset_entity_binding` 输入、快照、返回记录与字段校验 helper - `asset_entity_binding` 输入、快照、返回记录与字段校验 helper
- `assetbind_` ID 前缀 - `assetbind_` ID 前缀
5. `WP-AS Assets` 资产对象类型归位已完成,领域快照、命令 DTO、应用返回 DTO 和字段错误已分别落到 DDD 骨架文件中。 5. `WP-AS Assets` 资产对象类型归位已完成,领域快照、命令 DTO、应用返回 DTO、领域事件和字段错误已分别落到 DDD 骨架文件中。
6. `asset_event` public event table 已承接对象确认与实体绑定变更事实,订阅端和审计流程可以感知资产主链变化。
当前 `asset_object` 表的字段、索引与可编码约束见: 当前 `asset_object` 表的字段、索引与可编码约束见:
@@ -33,19 +34,22 @@
2. [../../../docs/technical/ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md](../../../docs/technical/ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md) 2. [../../../docs/technical/ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md](../../../docs/technical/ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md)
3. [../../../docs/technical/ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md](../../../docs/technical/ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md) 3. [../../../docs/technical/ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md](../../../docs/technical/ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md)
4. [../../../docs/technical/SERVER_RS_DDD_WP_AS_ASSET_OBJECT_TYPE_REHOME_2026-04-29.md](../../../docs/technical/SERVER_RS_DDD_WP_AS_ASSET_OBJECT_TYPE_REHOME_2026-04-29.md) 4. [../../../docs/technical/SERVER_RS_DDD_WP_AS_ASSET_OBJECT_TYPE_REHOME_2026-04-29.md](../../../docs/technical/SERVER_RS_DDD_WP_AS_ASSET_OBJECT_TYPE_REHOME_2026-04-29.md)
5. [../../../docs/technical/SERVER_RS_DDD_WP_AS_ASSET_CHAIN_CLOSURE_2026-05-01.md](../../../docs/technical/SERVER_RS_DDD_WP_AS_ASSET_CHAIN_CLOSURE_2026-05-01.md)
当前还已补齐: 当前还已补齐:
1. `AssetObjectService` 1. `AssetObjectService`
2. 私有 bucket `HEAD Object` 后的对象确认写入 2. 私有 bucket `HEAD Object` 后的对象确认写入
3. 当前阶段的进程内 `asset_object` 去重存储 3. 当前阶段的进程内 `asset_object` 去重存储
4. SpacetimeDB `asset_object` / `asset_entity_binding` / `asset_event` adapter 写入
5. Rust `spacetime-client` 资产对象确认、绑定和历史 facade
后续与本 package 直接相关的任务包括: 后续与本 package 直接相关的任务包括:
1. 设计 `asset_job``asset_object``asset_manifest` 1. 设计 `asset_job``asset_manifest`
2. 设计角色、动作、场景、精灵表相关资产表 2. 设计角色、动作、场景、精灵表相关资产表
3. 对齐资产生成、发布、对象确认与兼容接口链路 3. 对齐资产生成、发布和专业资产任务编排
4. 接入 OSS 对象写入与绑定编排 4. 新增资产生成表或专业资产任务时继续复用 OSS read-url 读取链路
## 3. 边界约束 ## 3. 边界约束

View File

@@ -1,4 +1,43 @@
//! 资产领域事件。 //! 资产领域事件。
//! //!
//! 用于表达资产已确认、绑定已变更和资产历史投影待刷新等事实。 //! 用于表达资产已确认、绑定已变更和资产历史投影待刷新等事实。
//! 当前阶段暂不新增事件类型,避免在 SpacetimeDB 表未补 event table 前扩散未消费 API。
use serde::{Deserialize, Serialize};
#[cfg(feature = "spacetime-types")]
use spacetimedb::SpacetimeType;
/// 资产领域事件。
///
/// 事件只描述已经发生的轻量事实,正式资产状态仍以 `asset_object`
/// 和 `asset_entity_binding` 为准。
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum AssetDomainEvent {
ObjectConfirmed(AssetObjectConfirmedEvent),
EntityBindingChanged(AssetEntityBindingChangedEvent),
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AssetObjectConfirmedEvent {
pub asset_object_id: String,
pub asset_kind: String,
pub owner_user_id: Option<String>,
pub profile_id: Option<String>,
pub entity_id: Option<String>,
pub occurred_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AssetEntityBindingChangedEvent {
pub binding_id: String,
pub asset_object_id: String,
pub entity_kind: String,
pub entity_id: String,
pub slot: String,
pub asset_kind: String,
pub owner_user_id: Option<String>,
pub profile_id: Option<String>,
pub occurred_at_micros: i64,
}

View File

@@ -26,6 +26,7 @@ pub use domain::{
INITIAL_ASSET_OBJECT_VERSION, INITIAL_ASSET_OBJECT_VERSION,
}; };
pub use errors::AssetObjectFieldError; pub use errors::AssetObjectFieldError;
pub use events::{AssetDomainEvent, AssetEntityBindingChangedEvent, AssetObjectConfirmedEvent};
pub use asset_object_core::{ pub use asset_object_core::{
build_asset_entity_binding_record, build_asset_history_entry_record, build_asset_object_record, build_asset_entity_binding_record, build_asset_history_entry_record, build_asset_object_record,

View File

@@ -23,6 +23,8 @@
4. `src/errors.rs` 承接拼图字段错误与中文错误文案。 4. `src/errors.rs` 承接拼图字段错误与中文错误文案。
5. `src/events.rs` 承接草稿变化、作品发布和运行态推进的最小领域事件。 5. `src/events.rs` 承接草稿变化、作品发布和运行态推进的最小领域事件。
6. `spacetime-types` feature 下可供 SpacetimeDB 绑定复用的类型派生。 6. `spacetime-types` feature 下可供 SpacetimeDB 绑定复用的类型派生。
7. 正式平台 Puzzle 运行态已经接入后端真相源:结果页草稿预览、作品开局、交换、拖动、下一关和排行榜都通过 Rust API / SpacetimeDB procedure 裁决。
8.`/api/runtime/puzzle/runs/local-next-level` 已由 `WP-DEL` 物理删除;正式 API 只保留 run scoped 的 `/api/runtime/puzzle/runs/{run_id}/next-level`
当前 crate 仍然只承接: 当前 crate 仍然只承接:
@@ -40,8 +42,10 @@
1. [../../../docs/technical/SERVER_RS_DDD_WP_PZ_DOMAIN_ENUM_REHOME_2026-04-29.md](../../../docs/technical/SERVER_RS_DDD_WP_PZ_DOMAIN_ENUM_REHOME_2026-04-29.md) 1. [../../../docs/technical/SERVER_RS_DDD_WP_PZ_DOMAIN_ENUM_REHOME_2026-04-29.md](../../../docs/technical/SERVER_RS_DDD_WP_PZ_DOMAIN_ENUM_REHOME_2026-04-29.md)
2. [../../../docs/technical/SERVER_RS_DDD_WP_PZ_DOMAIN_SPLIT_2026-04-29.md](../../../docs/technical/SERVER_RS_DDD_WP_PZ_DOMAIN_SPLIT_2026-04-29.md) 2. [../../../docs/technical/SERVER_RS_DDD_WP_PZ_DOMAIN_SPLIT_2026-04-29.md](../../../docs/technical/SERVER_RS_DDD_WP_PZ_DOMAIN_SPLIT_2026-04-29.md)
3. [../../../docs/technical/PUZZLE_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md](../../../docs/technical/PUZZLE_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md) 3. [../../../docs/technical/SERVER_RS_DDD_WP_PZ_RUNTIME_BACKEND_TRUTH_CLOSURE_2026-05-01.md](../../../docs/technical/SERVER_RS_DDD_WP_PZ_RUNTIME_BACKEND_TRUTH_CLOSURE_2026-05-01.md)
4. [../../../docs/technical/PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.md](../../../docs/technical/PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.md) 4. [../../../docs/technical/SERVER_RS_DDD_WP_DEL_CLEANUP_2026-05-01.md](../../../docs/technical/SERVER_RS_DDD_WP_DEL_CLEANUP_2026-05-01.md)
5. [../../../docs/technical/PUZZLE_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md](../../../docs/technical/PUZZLE_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md)
6. [../../../docs/technical/PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.md](../../../docs/technical/PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.md)
## 3. 边界约束 ## 3. 边界约束

View File

@@ -11,6 +11,8 @@
5. `src/errors.rs` 承载 runtime story 纯规则错误。 5. `src/errors.rs` 承载 runtime story 纯规则错误。
6. `src/lib.rs` 只保留模块声明、公开导出和子模块 re-export。 6. `src/lib.rs` 只保留模块声明、公开导出和子模块 re-export。
后续 WP-RS 继续按 battle / forge / NPC / quest / presentation 的顺序,把旧 `/api/runtime/story/*` 写侧能力迁到 session scoped 新接口,并删除运行代码中的旧入口命名 `bootstrap.rs` 需要组装较深的运行时初始 `game_state` JSON 模板crate 级 `recursion_limit = "512"` 仅用于支撑 `serde_json::json!` 宏展开,不承载额外领域规则
当前 WP-RS 写链路已经通过 `POST /api/story/sessions/runtime``POST /api/story/sessions/{storySessionId}/actions/resolve` 收口到 session scoped 新接口。后续只按 battle / forge / NPC / quest / presentation 的顺序增强领域规则和投影,并在 `WP-DEL` 中删除运行代码不再需要的旧入口命名。
配套记录见 [../../../docs/technical/SERVER_RS_DDD_WP_RS_RUNTIME_STORY_DOMAIN_SPLIT_2026-04-30.md](../../../docs/technical/SERVER_RS_DDD_WP_RS_RUNTIME_STORY_DOMAIN_SPLIT_2026-04-30.md)。 配套记录见 [../../../docs/technical/SERVER_RS_DDD_WP_RS_RUNTIME_STORY_DOMAIN_SPLIT_2026-04-30.md](../../../docs/technical/SERVER_RS_DDD_WP_RS_RUNTIME_STORY_DOMAIN_SPLIT_2026-04-30.md)。

View File

@@ -3,26 +3,10 @@
//! 这里组合纯领域规则并返回后端投影真实保存、SSE 和模型调用由外层完成。 //! 这里组合纯领域规则并返回后端投影真实保存、SSE 和模型调用由外层完成。
use serde_json::Value; use serde_json::Value;
use shared_contracts::runtime_story::{ use shared_contracts::runtime_story::RuntimeStoryPatch;
RuntimeBattlePresentation, RuntimeStoryOptionView, RuntimeStoryPatch,
RuntimeStorySnapshotPayload,
};
use crate::{StoryResolution, read_bool_field, read_optional_string_field}; use crate::{StoryResolution, read_bool_field, read_optional_string_field};
pub struct RuntimeStoryActionResponseParts {
pub requested_session_id: String,
pub server_version: u32,
pub snapshot: RuntimeStorySnapshotPayload,
pub action_text: String,
pub result_text: String,
pub story_text: String,
pub options: Vec<RuntimeStoryOptionView>,
pub patches: Vec<RuntimeStoryPatch>,
pub toast: Option<String>,
pub battle: Option<RuntimeBattlePresentation>,
}
pub fn simple_story_resolution( pub fn simple_story_resolution(
game_state: &Value, game_state: &Value,
action_text: String, action_text: String,

View File

@@ -58,7 +58,6 @@ fn build_request(function_id: &str, option_text: &str) -> RuntimeStoryActionRequ
"optionText": option_text "optionText": option_text
})), })),
}, },
snapshot: None,
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,12 @@
#![recursion_limit = "512"]
mod application; mod application;
mod bootstrap;
mod commands; mod commands;
mod domain; mod domain;
mod errors; mod errors;
mod events; mod events;
mod session_action;
pub mod battle; pub mod battle;
#[cfg(test)] #[cfg(test)]
@@ -24,6 +28,11 @@ pub use battle::{
build_battle_runtime_story_options, inventory_item_has_usable_effect, resolve_battle_action, build_battle_runtime_story_options, inventory_item_has_usable_effect, resolve_battle_action,
restore_player_resource, restore_player_resource,
}; };
pub use bootstrap::{
RuntimeStoryBootstrapBuild, RuntimeStoryBootstrapSeed, build_custom_scene_preset,
build_encounter_from_scene_npc, build_runtime_story_bootstrap, generate_runtime_session_id,
resolve_custom_runtime_scene_id,
};
pub use commands::*; pub use commands::*;
pub use core::{ pub use core::{
MAX_PLAYER_LEVEL, add_player_currency, add_player_inventory_items, append_active_build_buffs, MAX_PLAYER_LEVEL, add_player_currency, add_player_inventory_items, append_active_build_buffs,
@@ -71,6 +80,10 @@ pub use post_battle::{
}; };
pub use projection::{StoryRuntimeProjectionSource, build_story_runtime_projection}; pub use projection::{StoryRuntimeProjectionSource, build_story_runtime_projection};
pub use prompt_context::{RuntimeStoryPromptContextExtras, build_runtime_story_prompt_context}; pub use prompt_context::{RuntimeStoryPromptContextExtras, build_runtime_story_prompt_context};
pub use session_action::{
StoryRuntimeActionResolveInput, StoryRuntimeActionResolveOutput, build_runtime_story_options,
build_runtime_story_state_response_parts, resolve_story_runtime_action,
};
pub use story_engine::project_story_engine_after_action; pub use story_engine::project_story_engine_after_action;
pub use view_model::{ pub use view_model::{
build_runtime_story_companions, build_runtime_story_encounter, build_runtime_story_view_model, build_runtime_story_companions, build_runtime_story_encounter, build_runtime_story_view_model,

View File

@@ -35,6 +35,7 @@ pub fn build_story_runtime_projection(
story_session: source.story_session, story_session: source.story_session,
story_events: source.story_events, story_events: source.story_events,
server_version: source.server_version, server_version: source.server_version,
game_state: source.game_state.clone(),
actor: StoryRuntimeActorProjection { actor: StoryRuntimeActorProjection {
hp: read_i32_field(&source.game_state, "playerHp").unwrap_or(0), hp: read_i32_field(&source.game_state, "playerHp").unwrap_or(0),
max_hp: read_i32_field(&source.game_state, "playerMaxHp").unwrap_or(1), max_hp: read_i32_field(&source.game_state, "playerMaxHp").unwrap_or(1),
@@ -174,6 +175,7 @@ mod tests {
}); });
assert_eq!(projection.story_session.story_session_id, "storysess_1"); assert_eq!(projection.story_session.story_session_id, "storysess_1");
assert_eq!(projection.game_state["worldType"], json!("WUXIA"));
assert_eq!(projection.actor.hp, 28); assert_eq!(projection.actor.hp, 28);
assert_eq!(projection.actor.currency_text, "80 铜钱"); assert_eq!(projection.actor.currency_text, "80 铜钱");
assert_eq!(projection.inventory.backpack_items.len(), 1); assert_eq!(projection.inventory.backpack_items.len(), 1);

File diff suppressed because it is too large Load Diff

View File

@@ -6,9 +6,21 @@ license.workspace = true
[features] [features]
default = [] default = []
spacetime-types = ["dep:spacetimedb"] spacetime-types = [
"dep:spacetimedb",
"module-combat/spacetime-types",
"module-inventory/spacetime-types",
"module-progression/spacetime-types",
"module-quest/spacetime-types",
"module-runtime-item/spacetime-types",
]
[dependencies] [dependencies]
module-combat = { path = "../module-combat", default-features = false }
module-inventory = { path = "../module-inventory", default-features = false }
module-progression = { path = "../module-progression", default-features = false }
module-quest = { path = "../module-quest", default-features = false }
module-runtime-item = { path = "../module-runtime-item", default-features = false }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
shared-kernel = { path = "../shared-kernel" } shared-kernel = { path = "../shared-kernel" }
spacetimedb = { workspace = true, optional = true } spacetimedb = { workspace = true, optional = true }

View File

@@ -9,7 +9,8 @@
1. `StorySessionSnapshot``StoryEventSnapshot` 等故事会话与事件快照。 1. `StorySessionSnapshot``StoryEventSnapshot` 等故事会话与事件快照。
2. `begin / continue / state query` 相关输入命令的字段归一化与基础校验。 2. `begin / continue / state query` 相关输入命令的字段归一化与基础校验。
3. 剧情会话开局、续写事件追加、版本推进和只读记录投影。 3. 剧情会话开局、续写事件追加、版本推进和只读记录投影。
4. `spacetime-module``spacetime-client``api-server` 提供可复用的纯 Rust 规则 4. RPG gameplay 跨域结算计划:战斗胜利、任务交付和宝箱奖励先生成纯领域计划,再由 adapter 事务执行
5.`spacetime-module``spacetime-client``api-server` 提供可复用的纯 Rust 规则。
## 2. 当前阶段说明 ## 2. 当前阶段说明
@@ -18,11 +19,11 @@
1. `src/domain.rs`:会话领域快照、状态和值对象。 1. `src/domain.rs`:会话领域快照、状态和值对象。
2. `src/commands.rs`story session scoped 输入命令与校验。 2. `src/commands.rs`story session scoped 输入命令与校验。
3. `src/events.rs`:剧情事件类型、事件快照和事件 ID 生成。 3. `src/events.rs`:剧情事件类型、事件快照和事件 ID 生成。
4. `src/application.rs`:快照构造、续写应用服务读模型记录映射。 4. `src/application.rs`:快照构造、续写应用服务读模型记录映射和 RPG gameplay 结算计划
5. `src/errors.rs`:剧情字段错误与中文错误文案。 5. `src/errors.rs`:剧情字段错误与中文错误文案。
6. `src/lib.rs`:只保留模块公开导出,保持 `module_story::*` 对外 API 稳定。 6. `src/lib.rs`:只保留模块公开导出,保持 `module_story::*` 对外 API 稳定。
当前仍未扩到完整运行态动作结算。`inventory action`、NPC interaction、forge、battle、quest 等写侧闭环继续归入 `WP-RS Runtime Story 去兼容层``WP-RPG Gameplay 域` 后续切片 当前 `WP-RPG Gameplay 域` 已完成领域侧收口:战斗胜利、任务交付和宝箱奖励的背包、成长、章节账本组合规则不再由 `spacetime-module` 临时拼装。完整 runtime story 动作入口、前端写侧迁移和旧 contract 删除仍归 `WP-RS / WP-FE / WP-DEL`
## 3. 边界约束 ## 3. 边界约束

View File

@@ -7,6 +7,22 @@ use crate::commands::{StoryContinueInput, StorySessionInput, normalize_optional_
use crate::domain::{INITIAL_STORY_SESSION_VERSION, StorySessionSnapshot, StorySessionStatus}; use crate::domain::{INITIAL_STORY_SESSION_VERSION, StorySessionSnapshot, StorySessionStatus};
use crate::errors::StorySessionFieldError; use crate::errors::StorySessionFieldError;
use crate::events::{StoryEventKind, StoryEventSnapshot}; use crate::events::{StoryEventKind, StoryEventSnapshot};
use module_combat::{BattleStateSnapshot, CombatOutcome};
use module_inventory::{
GrantInventoryItemInput, InventoryEquipmentSlot, InventoryItemRarity, InventoryItemSnapshot,
InventoryItemSourceKind, InventoryMutation, InventoryMutationInput,
generate_inventory_mutation_id, generate_inventory_slot_id,
};
use module_progression::{
ChapterProgressionLedgerInput, PlayerProgressionGrantInput, PlayerProgressionGrantSource,
};
use module_quest::{
QuestRecordSnapshot, QuestRewardEquipmentSlot, QuestRewardItem, QuestRewardItemRarity,
};
use module_runtime_item::{
RuntimeItemEquipmentSlot, RuntimeItemRewardItemRarity, RuntimeItemRewardItemSnapshot,
TreasureRecordSnapshot, build_inventory_item_snapshot_from_reward_item,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use shared_kernel::format_timestamp_micros; use shared_kernel::format_timestamp_micros;
#[cfg(feature = "spacetime-types")] #[cfg(feature = "spacetime-types")]
@@ -68,6 +84,17 @@ pub struct StorySessionStateRecord {
pub events: Vec<StoryEventRecord>, pub events: Vec<StoryEventRecord>,
} }
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RpgGameplaySettlementPlan {
/// 背包写入计划。adapter 只负责读取当前槽位并执行 mutation不再拼奖励物品字段。
pub inventory_mutations: Vec<InventoryMutationInput>,
/// 玩家经验入账计划。章节账本需要依赖执行后的等级结果,所以单独保留。
pub progression_grant: Option<PlayerProgressionGrantInput>,
/// 章节实际收益账本计划。若章节预算尚未初始化adapter 会跳过而不阻断主链。
pub chapter_ledger: Option<ChapterProgressionLedgerInput>,
}
pub fn build_story_session_snapshot(input: StorySessionInput) -> StorySessionSnapshot { pub fn build_story_session_snapshot(input: StorySessionInput) -> StorySessionSnapshot {
StorySessionSnapshot { StorySessionSnapshot {
story_session_id: input.story_session_id, story_session_id: input.story_session_id,
@@ -165,3 +192,272 @@ pub fn build_story_session_state_record(
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
} }
} }
pub fn build_combat_victory_settlement_plan(
snapshot: &BattleStateSnapshot,
) -> RpgGameplaySettlementPlan {
// 非胜利结果不触发战利品、敌对经验或章节 hostile 账本,避免逃脱/切磋混入击杀收益。
if snapshot.last_outcome != CombatOutcome::Victory {
return empty_settlement_plan();
}
let inventory_mutations = snapshot
.reward_items
.iter()
.cloned()
.enumerate()
.map(|(index, reward_item)| {
let seed = build_reward_seed(snapshot.updated_at_micros, index);
InventoryMutationInput {
mutation_id: generate_inventory_mutation_id(seed),
runtime_session_id: snapshot.runtime_session_id.clone(),
story_session_id: Some(snapshot.story_session_id.clone()),
actor_user_id: snapshot.actor_user_id.clone(),
mutation: InventoryMutation::GrantItem(GrantInventoryItemInput {
slot_id: generate_inventory_slot_id(seed),
item: build_inventory_item_snapshot_from_battle_reward_item(
&snapshot.battle_state_id,
reward_item,
),
}),
updated_at_micros: snapshot.updated_at_micros,
}
})
.collect::<Vec<_>>();
let progression_grant = (snapshot.experience_reward > 0).then(|| PlayerProgressionGrantInput {
user_id: snapshot.actor_user_id.clone(),
amount: snapshot.experience_reward,
source: PlayerProgressionGrantSource::HostileNpc,
updated_at_micros: snapshot.updated_at_micros,
});
let chapter_ledger =
progression_grant
.as_ref()
.and_then(|_| match snapshot.chapter_id.as_deref() {
Some(chapter_id) if !chapter_id.trim().is_empty() => {
Some(ChapterProgressionLedgerInput {
user_id: snapshot.actor_user_id.clone(),
chapter_id: chapter_id.trim().to_string(),
granted_quest_xp: 0,
granted_hostile_xp: snapshot.experience_reward,
hostile_defeat_increment: 1,
level_at_exit: None,
updated_at_micros: snapshot.updated_at_micros,
})
}
_ => None,
});
RpgGameplaySettlementPlan {
inventory_mutations,
progression_grant,
chapter_ledger,
}
}
/// 任务交付后只生成结算计划,不在领域层直接写背包、成长或章节表。
pub fn build_quest_turn_in_settlement_plan(
snapshot: &QuestRecordSnapshot,
) -> RpgGameplaySettlementPlan {
let inventory_mutations = snapshot
.reward
.items
.iter()
.cloned()
.enumerate()
.map(|(index, reward_item)| {
let seed = build_reward_seed(snapshot.updated_at_micros, index);
InventoryMutationInput {
mutation_id: generate_inventory_mutation_id(seed),
runtime_session_id: snapshot.runtime_session_id.clone(),
story_session_id: snapshot.story_session_id.clone(),
actor_user_id: snapshot.actor_user_id.clone(),
mutation: InventoryMutation::GrantItem(GrantInventoryItemInput {
slot_id: generate_inventory_slot_id(seed),
item: build_inventory_item_snapshot_from_quest_reward_item(
&snapshot.quest_id,
reward_item,
),
}),
updated_at_micros: snapshot.updated_at_micros,
}
})
.collect::<Vec<_>>();
let reward_experience = snapshot.reward.experience.unwrap_or(0);
let progression_grant = (reward_experience > 0).then(|| PlayerProgressionGrantInput {
user_id: snapshot.actor_user_id.clone(),
amount: reward_experience,
source: PlayerProgressionGrantSource::Quest,
updated_at_micros: snapshot.updated_at_micros,
});
let chapter_ledger =
progression_grant
.as_ref()
.and_then(|_| match snapshot.chapter_id.as_deref() {
Some(chapter_id) if !chapter_id.trim().is_empty() => {
Some(ChapterProgressionLedgerInput {
user_id: snapshot.actor_user_id.clone(),
chapter_id: chapter_id.trim().to_string(),
granted_quest_xp: reward_experience,
granted_hostile_xp: 0,
hostile_defeat_increment: 0,
level_at_exit: None,
updated_at_micros: snapshot.updated_at_micros,
})
}
_ => None,
});
RpgGameplaySettlementPlan {
inventory_mutations,
progression_grant,
chapter_ledger,
}
}
/// 宝箱记录由 `module-runtime-item` 建模,这里只把奖励转成 story gameplay 的背包写入计划。
pub fn build_treasure_settlement_plan(
snapshot: &TreasureRecordSnapshot,
) -> Result<RpgGameplaySettlementPlan, StorySessionFieldError> {
let inventory_mutations = snapshot
.reward_items
.iter()
.cloned()
.enumerate()
.map(
|(index, reward_item)| -> Result<InventoryMutationInput, StorySessionFieldError> {
Ok(InventoryMutationInput {
mutation_id: build_treasure_inventory_mutation_id(
&snapshot.treasure_record_id,
index,
),
runtime_session_id: snapshot.runtime_session_id.clone(),
story_session_id: Some(snapshot.story_session_id.clone()),
actor_user_id: snapshot.actor_user_id.clone(),
mutation: InventoryMutation::GrantItem(GrantInventoryItemInput {
slot_id: build_treasure_inventory_slot_id(
&snapshot.treasure_record_id,
index,
),
item: build_inventory_item_snapshot_from_reward_item(
&snapshot.treasure_record_id,
reward_item,
)
.map_err(|_| StorySessionFieldError::InvalidGameplayReward)?,
}),
updated_at_micros: snapshot.updated_at_micros,
})
},
)
.collect::<Result<Vec<_>, _>>()?;
Ok(RpgGameplaySettlementPlan {
inventory_mutations,
progression_grant: None,
chapter_ledger: None,
})
}
fn empty_settlement_plan() -> RpgGameplaySettlementPlan {
RpgGameplaySettlementPlan {
inventory_mutations: Vec::new(),
progression_grant: None,
chapter_ledger: None,
}
}
fn build_inventory_item_snapshot_from_battle_reward_item(
battle_state_id: &str,
reward_item: RuntimeItemRewardItemSnapshot,
) -> InventoryItemSnapshot {
InventoryItemSnapshot {
item_id: reward_item.item_id,
category: reward_item.category,
name: reward_item.item_name,
description: reward_item.description,
quantity: reward_item.quantity,
rarity: map_runtime_reward_item_rarity(reward_item.rarity),
tags: reward_item.tags,
stackable: reward_item.stackable,
stack_key: reward_item.stack_key,
equipment_slot_id: reward_item
.equipment_slot_id
.map(map_runtime_reward_equipment_slot),
source_kind: InventoryItemSourceKind::CombatDrop,
source_reference_id: Some(battle_state_id.to_string()),
}
}
fn build_inventory_item_snapshot_from_quest_reward_item(
quest_id: &str,
reward_item: QuestRewardItem,
) -> InventoryItemSnapshot {
InventoryItemSnapshot {
item_id: reward_item.item_id,
category: reward_item.category,
name: reward_item.name,
description: reward_item.description,
quantity: reward_item.quantity,
rarity: map_quest_reward_item_rarity(reward_item.rarity),
tags: reward_item.tags,
stackable: reward_item.stackable,
stack_key: reward_item.stack_key,
equipment_slot_id: reward_item
.equipment_slot_id
.map(map_quest_reward_equipment_slot),
source_kind: InventoryItemSourceKind::QuestReward,
source_reference_id: Some(quest_id.to_string()),
}
}
fn map_quest_reward_item_rarity(rarity: QuestRewardItemRarity) -> InventoryItemRarity {
match rarity {
QuestRewardItemRarity::Common => InventoryItemRarity::Common,
QuestRewardItemRarity::Uncommon => InventoryItemRarity::Uncommon,
QuestRewardItemRarity::Rare => InventoryItemRarity::Rare,
QuestRewardItemRarity::Epic => InventoryItemRarity::Epic,
QuestRewardItemRarity::Legendary => InventoryItemRarity::Legendary,
}
}
fn map_runtime_reward_item_rarity(rarity: RuntimeItemRewardItemRarity) -> InventoryItemRarity {
match rarity {
RuntimeItemRewardItemRarity::Common => InventoryItemRarity::Common,
RuntimeItemRewardItemRarity::Uncommon => InventoryItemRarity::Uncommon,
RuntimeItemRewardItemRarity::Rare => InventoryItemRarity::Rare,
RuntimeItemRewardItemRarity::Epic => InventoryItemRarity::Epic,
RuntimeItemRewardItemRarity::Legendary => InventoryItemRarity::Legendary,
}
}
fn map_quest_reward_equipment_slot(slot: QuestRewardEquipmentSlot) -> InventoryEquipmentSlot {
match slot {
QuestRewardEquipmentSlot::Weapon => InventoryEquipmentSlot::Weapon,
QuestRewardEquipmentSlot::Armor => InventoryEquipmentSlot::Armor,
QuestRewardEquipmentSlot::Relic => InventoryEquipmentSlot::Relic,
}
}
fn map_runtime_reward_equipment_slot(slot: RuntimeItemEquipmentSlot) -> InventoryEquipmentSlot {
match slot {
RuntimeItemEquipmentSlot::Weapon => InventoryEquipmentSlot::Weapon,
RuntimeItemEquipmentSlot::Armor => InventoryEquipmentSlot::Armor,
RuntimeItemEquipmentSlot::Relic => InventoryEquipmentSlot::Relic,
}
}
fn build_reward_seed(updated_at_micros: i64, index: usize) -> i64 {
updated_at_micros.saturating_add(index as i64 + 1)
}
fn build_treasure_inventory_slot_id(treasure_record_id: &str, reward_index: usize) -> String {
format!("invslot_{}_{}", treasure_record_id, reward_index)
}
fn build_treasure_inventory_mutation_id(treasure_record_id: &str, reward_index: usize) -> String {
format!("invmut_{}_{}", treasure_record_id, reward_index)
}

View File

@@ -14,6 +14,7 @@ pub enum StorySessionFieldError {
MissingNarrativeText, MissingNarrativeText,
MissingEventId, MissingEventId,
InvalidVersion, InvalidVersion,
InvalidGameplayReward,
} }
impl fmt::Display for StorySessionFieldError { impl fmt::Display for StorySessionFieldError {
@@ -29,6 +30,7 @@ impl fmt::Display for StorySessionFieldError {
Self::MissingNarrativeText => f.write_str("story_event.narrative_text 不能为空"), Self::MissingNarrativeText => f.write_str("story_event.narrative_text 不能为空"),
Self::MissingEventId => f.write_str("story_event.event_id 不能为空"), Self::MissingEventId => f.write_str("story_event.event_id 不能为空"),
Self::InvalidVersion => f.write_str("story_session.version 必须大于 0"), Self::InvalidVersion => f.write_str("story_session.version 必须大于 0"),
Self::InvalidGameplayReward => f.write_str("RPG 结算奖励字段非法"),
} }
} }
} }

View File

@@ -13,6 +13,11 @@ pub use events::*;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use module_quest::{
QuestNarrativeBindingSnapshot, QuestNarrativeOrigin, QuestNarrativeType,
QuestObjectiveKind, QuestObjectiveSnapshot, QuestRecordSnapshot, QuestRewardEquipmentSlot,
QuestRewardItem, QuestRewardItemRarity, QuestRewardSnapshot, QuestStatus,
};
#[test] #[test]
fn validate_story_session_input_accepts_minimal_contract() { fn validate_story_session_input_accepts_minimal_contract() {
@@ -200,4 +205,172 @@ mod tests {
assert_eq!(next.version, INITIAL_STORY_SESSION_VERSION + 1); assert_eq!(next.version, INITIAL_STORY_SESSION_VERSION + 1);
assert_eq!(event.event_kind, StoryEventKind::StoryContinued); assert_eq!(event.event_kind, StoryEventKind::StoryContinued);
} }
#[test]
fn combat_victory_settlement_plan_grants_items_xp_and_chapter_ledger() {
let plan = build_combat_victory_settlement_plan(&module_combat::BattleStateSnapshot {
battle_state_id: "battle_001".to_string(),
story_session_id: "storysess_001".to_string(),
runtime_session_id: "runtime_001".to_string(),
actor_user_id: "user_001".to_string(),
chapter_id: Some("chapter_01".to_string()),
target_npc_id: "npc_bandit".to_string(),
target_name: "山匪".to_string(),
battle_mode: module_combat::BattleMode::Fight,
status: module_combat::BattleStatus::Resolved,
player_hp: 20,
player_max_hp: 30,
player_mana: 6,
player_max_mana: 10,
target_hp: 0,
target_max_hp: 18,
experience_reward: 35,
reward_items: vec![module_runtime_item::RuntimeItemRewardItemSnapshot {
item_id: "iron_token".to_string(),
category: "material".to_string(),
item_name: "铁制信物".to_string(),
description: Some("山匪随身携带的旧信物。".to_string()),
quantity: 1,
rarity: module_runtime_item::RuntimeItemRewardItemRarity::Uncommon,
tags: vec!["quest".to_string()],
stackable: true,
stack_key: "iron_token".to_string(),
equipment_slot_id: None,
}],
turn_index: 2,
last_action_function_id: Some("battle_use_skill".to_string()),
last_action_text: Some("破阵一击".to_string()),
last_result_text: Some("战斗结束。".to_string()),
last_damage_dealt: 18,
last_damage_taken: 0,
last_outcome: module_combat::CombatOutcome::Victory,
version: 3,
created_at_micros: 100,
updated_at_micros: 200,
});
assert_eq!(plan.inventory_mutations.len(), 1);
assert_eq!(
plan.progression_grant.as_ref().map(|input| input.amount),
Some(35)
);
assert_eq!(
plan.chapter_ledger
.as_ref()
.map(|input| input.granted_hostile_xp),
Some(35)
);
let module_inventory::InventoryMutation::GrantItem(input) =
&plan.inventory_mutations[0].mutation
else {
panic!("combat victory should grant inventory item");
};
assert_eq!(
input.item.source_reference_id.as_deref(),
Some("battle_001")
);
assert_eq!(
input.item.source_kind,
module_inventory::InventoryItemSourceKind::CombatDrop
);
}
#[test]
fn quest_turn_in_settlement_plan_grants_reward_items_and_quest_xp() {
let plan = build_quest_turn_in_settlement_plan(&QuestRecordSnapshot {
quest_id: "quest_001".to_string(),
runtime_session_id: "runtime_001".to_string(),
story_session_id: Some("storysess_001".to_string()),
actor_user_id: "user_001".to_string(),
issuer_npc_id: "npc_scout".to_string(),
issuer_npc_name: "斥候".to_string(),
scene_id: None,
chapter_id: Some("chapter_01".to_string()),
act_id: None,
thread_id: None,
contract_id: None,
title: "寻找信物".to_string(),
description: "找回遗失的信物。".to_string(),
summary: "信物已找到。".to_string(),
objective: QuestObjectiveSnapshot {
kind: QuestObjectiveKind::InspectTreasure,
target_hostile_npc_id: None,
target_npc_id: None,
target_scene_id: Some("scene_ruin".to_string()),
target_item_id: None,
required_count: 1,
},
progress: 1,
status: QuestStatus::TurnedIn,
completion_notified: true,
reward: QuestRewardSnapshot {
affinity_bonus: 0,
currency: 0,
experience: Some(40),
items: vec![QuestRewardItem {
item_id: "scout_badge".to_string(),
category: "trinket".to_string(),
name: "斥候徽记".to_string(),
description: None,
quantity: 1,
rarity: QuestRewardItemRarity::Rare,
tags: vec!["badge".to_string()],
stackable: false,
stack_key: "scout_badge".to_string(),
equipment_slot_id: Some(QuestRewardEquipmentSlot::Relic),
}],
intel: None,
story_hint: None,
},
reward_text: "获得斥候徽记。".to_string(),
narrative_binding: QuestNarrativeBindingSnapshot {
origin: QuestNarrativeOrigin::FallbackBuilder,
narrative_type: QuestNarrativeType::Retrieval,
dramatic_need: String::new(),
issuer_goal: String::new(),
player_hook: String::new(),
world_reason: String::new(),
followup_hooks: Vec::new(),
},
steps: Vec::new(),
active_step_id: None,
visible_stage: 1,
hidden_flags: Vec::new(),
discovered_fact_ids: Vec::new(),
related_carrier_ids: Vec::new(),
consequence_ids: Vec::new(),
created_at_micros: 100,
updated_at_micros: 300,
completed_at_micros: Some(200),
turned_in_at_micros: Some(300),
});
assert_eq!(plan.inventory_mutations.len(), 1);
assert_eq!(
plan.progression_grant.as_ref().map(|input| input.amount),
Some(40)
);
assert_eq!(
plan.chapter_ledger
.as_ref()
.map(|input| input.granted_quest_xp),
Some(40)
);
let module_inventory::InventoryMutation::GrantItem(input) =
&plan.inventory_mutations[0].mutation
else {
panic!("quest turn-in should grant inventory item");
};
assert_eq!(input.item.source_reference_id.as_deref(), Some("quest_001"));
assert_eq!(
input.item.equipment_slot_id,
Some(module_inventory::InventoryEquipmentSlot::Relic)
);
assert_eq!(
input.item.source_kind,
module_inventory::InventoryItemSourceKind::QuestReward
);
}
} }

View File

@@ -73,13 +73,15 @@
2. `assets/character-animation/jobs/:taskId` 2. `assets/character-animation/jobs/:taskId`
3. `assets/character-animation/publish` 3. `assets/character-animation/publish`
当前阶段新增 Stage5 `runtime story` 兼容桥 DTO 基线 当前阶段新增 Stage5 story session scoped runtime story 写读 DTO
1. `runtime/story/state/resolve` 请求 DTO 1. `BeginStoryRuntimeSessionRequest`
2. `runtime/story/actions/resolve``runtime/story/initial``runtime/story/continue` 请求 DTO 2. `ResolveStoryRuntimeActionRequest`
2. `RuntimeStoryActionResponse` 兼容响应 DTO 3. `StoryRuntimeProjectionResponse`
3. `RuntimeStoryViewModel / presentation / patches / snapshot` 显式结构 4. `StoryRuntimeMutationResponse`
4. `RuntimeStoryAiResponse` 兼容响应 DTO 5. `StoryRuntimeSnapshotPayload` 仅用于 story session scoped 写侧持久化边界,不复用旧 `/api/runtime/story/*` 总入口 contract。
`WP-DEL` 已删除旧 runtime story HTTP DTO`RuntimeStoryStateResolveRequest``RuntimeStoryBootstrapRequest/Response``RuntimeStoryActionResponse` 和旧 `RuntimeStorySnapshotPayload``runtime_story` 模块中仍保留的 `RuntimeStoryViewModel``RuntimeStoryPresentation``RuntimeStoryPatch` 与 battle presentation 是当前投影/表现构件,不作为旧 HTTP 写入口。
当前仍刻意未做: 当前仍刻意未做:

View File

@@ -6,14 +6,6 @@ pub struct StartPuzzleRunRequest {
pub profile_id: String, pub profile_id: String,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdvanceLocalPuzzleNextLevelRequest {
pub run: PuzzleRunSnapshotResponse,
#[serde(default)]
pub source_session_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SwapPuzzlePiecesRequest { pub struct SwapPuzzlePiecesRequest {

View File

@@ -1,48 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStorySnapshotPayload {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub saved_at: Option<String>,
pub bottom_tab: String,
pub game_state: Value,
#[serde(default)]
pub current_story: Option<Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStoryStateResolveRequest {
pub session_id: String,
#[serde(default)]
pub client_version: Option<u32>,
#[serde(default)]
pub snapshot: Option<RuntimeStorySnapshotPayload>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStoryBootstrapRequest {
pub world_type: String,
#[serde(default)]
pub custom_world_profile: Option<Value>,
pub character: Value,
#[serde(default)]
pub runtime_mode: Option<String>,
#[serde(default)]
pub disable_persistence: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStoryBootstrapResponse {
pub session_id: String,
pub server_version: u32,
pub snapshot: RuntimeStorySnapshotPayload,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct RuntimeStoryChoiceAction { pub struct RuntimeStoryChoiceAction {
@@ -62,8 +20,6 @@ pub struct RuntimeStoryActionRequest {
#[serde(default)] #[serde(default)]
pub client_version: Option<u32>, pub client_version: Option<u32>,
pub action: RuntimeStoryChoiceAction, pub action: RuntimeStoryChoiceAction,
#[serde(default)]
pub snapshot: Option<RuntimeStorySnapshotPayload>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -404,43 +360,11 @@ pub enum RuntimeStoryPatch {
}, },
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStoryActionResponse {
pub session_id: String,
pub server_version: u32,
pub view_model: RuntimeStoryViewModel,
pub presentation: RuntimeStoryPresentation,
pub patches: Vec<RuntimeStoryPatch>,
pub snapshot: RuntimeStorySnapshotPayload,
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use serde_json::json; use serde_json::json;
#[test]
fn runtime_story_state_resolve_request_accepts_missing_saved_at() {
let payload: RuntimeStoryStateResolveRequest = serde_json::from_value(json!({
"sessionId": "runtime-main",
"clientVersion": 7,
"snapshot": {
"bottomTab": "adventure",
"gameState": { "runtimeSessionId": "runtime-main" },
"currentStory": { "text": "营地里的火光还没有熄灭。" }
}
}))
.expect("payload should deserialize");
assert_eq!(payload.session_id, "runtime-main");
assert_eq!(payload.client_version, Some(7));
assert_eq!(
payload.snapshot.expect("snapshot should exist").saved_at,
None
);
}
#[test] #[test]
fn runtime_story_action_request_uses_camel_case_fields() { fn runtime_story_action_request_uses_camel_case_fields() {
let payload = serde_json::to_value(RuntimeStoryActionRequest { let payload = serde_json::to_value(RuntimeStoryActionRequest {
@@ -452,12 +376,6 @@ mod tests {
target_id: Some("npc_camp_firekeeper".to_string()), target_id: Some("npc_camp_firekeeper".to_string()),
payload: Some(json!({ "optionText": "继续交谈" })), payload: Some(json!({ "optionText": "继续交谈" })),
}, },
snapshot: Some(RuntimeStorySnapshotPayload {
saved_at: Some("2026-04-22T12:00:00.000Z".to_string()),
bottom_tab: "adventure".to_string(),
game_state: json!({ "runtimeSessionId": "runtime-main" }),
current_story: None,
}),
}) })
.expect("payload should serialize"); .expect("payload should serialize");
@@ -466,27 +384,6 @@ mod tests {
assert_eq!(payload["action"]["type"], json!("story_choice")); assert_eq!(payload["action"]["type"], json!("story_choice"));
assert_eq!(payload["action"]["functionId"], json!("npc_chat")); assert_eq!(payload["action"]["functionId"], json!("npc_chat"));
assert_eq!(payload["action"]["targetId"], json!("npc_camp_firekeeper")); assert_eq!(payload["action"]["targetId"], json!("npc_camp_firekeeper"));
assert_eq!(
payload["snapshot"]["savedAt"],
json!("2026-04-22T12:00:00.000Z")
);
}
#[test]
fn runtime_story_bootstrap_request_uses_camel_case_fields() {
let payload = serde_json::to_value(RuntimeStoryBootstrapRequest {
world_type: "CUSTOM".to_string(),
custom_world_profile: Some(json!({ "id": "profile-1" })),
character: json!({ "id": "role-1", "name": "沈砺" }),
runtime_mode: Some("play".to_string()),
disable_persistence: Some(false),
})
.expect("payload should serialize");
assert_eq!(payload["worldType"], json!("CUSTOM"));
assert_eq!(payload["customWorldProfile"]["id"], json!("profile-1"));
assert_eq!(payload["runtimeMode"], json!("play"));
assert_eq!(payload["disablePersistence"], json!(false));
} }
#[test] #[test]
@@ -532,37 +429,157 @@ mod tests {
} }
#[test] #[test]
fn runtime_story_action_response_uses_camel_case_fields() { fn runtime_story_presentation_uses_camel_case_fields() {
let payload = serde_json::to_value(RuntimeStoryActionResponse { let view_model = RuntimeStoryViewModel {
session_id: "runtime-main".to_string(), player: RuntimeStoryPlayerViewModel {
server_version: 8, hp: 32,
view_model: RuntimeStoryViewModel { max_hp: 40,
player: RuntimeStoryPlayerViewModel { mana: 18,
hp: 32, max_mana: 20,
max_hp: 40, },
mana: 18, encounter: Some(RuntimeStoryEncounterViewModel {
max_mana: 20, id: "npc_camp_firekeeper".to_string(),
}, kind: "npc".to_string(),
encounter: Some(RuntimeStoryEncounterViewModel { npc_name: "守火人".to_string(),
id: "npc_camp_firekeeper".to_string(), hostile: false,
kind: "npc".to_string(), affinity: Some(12),
npc_name: "守火人".to_string(), recruited: Some(false),
hostile: false, interaction_active: true,
affinity: Some(12), battle_mode: None,
recruited: Some(false), }),
interaction_active: true, companions: vec![RuntimeStoryCompanionViewModel {
battle_mode: None, npc_id: "npc_companion_001".to_string(),
}), character_id: Some("char_companion_001".to_string()),
companions: vec![RuntimeStoryCompanionViewModel { joined_at_affinity: 64,
npc_id: "npc_companion_001".to_string(), }],
character_id: Some("char_companion_001".to_string()), inventory: RuntimeStoryInventoryViewModel {
joined_at_affinity: 64, player_currency: 80,
currency_text: "80 铜钱".to_string(),
in_battle: false,
backpack_items: vec![RuntimeStoryInventoryItemView {
item: json!({
"id": "potion-1",
"name": "疗伤药",
"category": "消耗品",
"quantity": 2,
"rarity": "common",
"tags": ["healing"]
}),
actions: RuntimeStoryInventoryItemActionsView {
use_item: RuntimeStoryInventoryActionView {
function_id: "inventory_use".to_string(),
action_text: "使用疗伤药".to_string(),
payload: Some(json!({ "itemId": "potion-1" })),
enabled: true,
reason: None,
},
equip: RuntimeStoryInventoryActionView {
function_id: "equipment_equip".to_string(),
action_text: "装备疗伤药".to_string(),
payload: Some(json!({ "itemId": "potion-1" })),
enabled: false,
reason: Some("该物品不能装备。".to_string()),
},
dismantle: RuntimeStoryInventoryActionView {
function_id: "forge_dismantle".to_string(),
action_text: "拆解疗伤药".to_string(),
payload: Some(json!({ "itemId": "potion-1" })),
enabled: false,
reason: Some("该物品不能拆解。".to_string()),
},
reforge: RuntimeStoryInventoryActionView {
function_id: "forge_reforge".to_string(),
action_text: "重铸疗伤药".to_string(),
payload: Some(json!({ "itemId": "potion-1" })),
enabled: false,
reason: Some("该物品不能重铸。".to_string()),
},
},
}], }],
inventory: RuntimeStoryInventoryViewModel { equipment_slots: vec![RuntimeStoryEquipmentSlotView {
player_currency: 80, slot_id: "weapon".to_string(),
currency_text: "80 铜钱".to_string(), label: "武器".to_string(),
in_battle: false, item: None,
backpack_items: vec![RuntimeStoryInventoryItemView { unequip: RuntimeStoryInventoryActionView {
function_id: "equipment_unequip".to_string(),
action_text: "卸下武器".to_string(),
payload: Some(json!({ "slotId": "weapon" })),
enabled: false,
reason: Some("武器位当前没有装备。".to_string()),
},
}],
forge_recipes: vec![RuntimeStoryForgeRecipeView {
id: "synthesis-refined-ingot".to_string(),
name: "压炼锭材".to_string(),
kind: "synthesis".to_string(),
description: "把零散残片和基础材料压成稳定可用的金属锭材。".to_string(),
result_label: "精炼锭材".to_string(),
currency_cost: 18,
currency_text: "18 铜钱".to_string(),
requirements: vec![RuntimeStoryForgeRequirementView {
id: "material:any".to_string(),
label: "任意材料".to_string(),
quantity: 3,
owned: 0,
}],
can_craft: false,
disabled_reason: Some("材料不足。".to_string()),
action: RuntimeStoryInventoryActionView {
function_id: "forge_craft".to_string(),
action_text: "制作精炼锭材".to_string(),
payload: Some(json!({ "recipeId": "synthesis-refined-ingot" })),
enabled: false,
reason: Some("材料不足。".to_string()),
},
}],
},
available_options: vec![RuntimeStoryOptionView {
function_id: "npc_chat".to_string(),
action_text: "继续交谈".to_string(),
detail_text: Some("围绕当前话题继续推进关系判断。".to_string()),
scope: "npc".to_string(),
interaction: Some(RuntimeStoryOptionInteraction::Npc {
npc_id: "npc_camp_firekeeper".to_string(),
action: "chat".to_string(),
quest_id: None,
}),
payload: Some(json!({ "note": "server-runtime-test" })),
disabled: None,
reason: None,
}],
status: RuntimeStoryStatusViewModel {
in_battle: false,
npc_interaction_active: true,
current_npc_battle_mode: None,
current_npc_battle_outcome: None,
},
npc_interaction: Some(RuntimeNpcInteractionView {
npc_id: "npc_camp_firekeeper".to_string(),
npc_name: "守火人".to_string(),
player_currency: 80,
currency_name: "铜钱".to_string(),
trade: RuntimeNpcTradeView {
buy_items: vec![RuntimeNpcTradeItemView {
item_id: "npc-potion".to_string(),
item: json!({
"id": "npc-potion",
"name": "疗伤药",
"category": "消耗品",
"quantity": 2,
"rarity": "common",
"tags": ["healing"]
}),
mode: "buy".to_string(),
unit_price: 20,
max_quantity: 2,
can_submit: true,
reason: None,
}],
sell_items: Vec::new(),
},
gift: RuntimeNpcGiftView {
items: vec![RuntimeNpcGiftItemView {
item_id: "potion-1".to_string(),
item: json!({ item: json!({
"id": "potion-1", "id": "potion-1",
"name": "疗伤药", "name": "疗伤药",
@@ -571,176 +588,47 @@ mod tests {
"rarity": "common", "rarity": "common",
"tags": ["healing"] "tags": ["healing"]
}), }),
actions: RuntimeStoryInventoryItemActionsView { affinity_gain: 10,
use_item: RuntimeStoryInventoryActionView { can_submit: true,
function_id: "inventory_use".to_string(), reason: None,
action_text: "使用疗伤药".to_string(),
payload: Some(json!({ "itemId": "potion-1" })),
enabled: true,
reason: None,
},
equip: RuntimeStoryInventoryActionView {
function_id: "equipment_equip".to_string(),
action_text: "装备疗伤药".to_string(),
payload: Some(json!({ "itemId": "potion-1" })),
enabled: false,
reason: Some("该物品不能装备。".to_string()),
},
dismantle: RuntimeStoryInventoryActionView {
function_id: "forge_dismantle".to_string(),
action_text: "拆解疗伤药".to_string(),
payload: Some(json!({ "itemId": "potion-1" })),
enabled: false,
reason: Some("该物品不能拆解。".to_string()),
},
reforge: RuntimeStoryInventoryActionView {
function_id: "forge_reforge".to_string(),
action_text: "重铸疗伤药".to_string(),
payload: Some(json!({ "itemId": "potion-1" })),
enabled: false,
reason: Some("该物品不能重铸。".to_string()),
},
},
}],
equipment_slots: vec![RuntimeStoryEquipmentSlotView {
slot_id: "weapon".to_string(),
label: "武器".to_string(),
item: None,
unequip: RuntimeStoryInventoryActionView {
function_id: "equipment_unequip".to_string(),
action_text: "卸下武器".to_string(),
payload: Some(json!({ "slotId": "weapon" })),
enabled: false,
reason: Some("武器位当前没有装备。".to_string()),
},
}],
forge_recipes: vec![RuntimeStoryForgeRecipeView {
id: "synthesis-refined-ingot".to_string(),
name: "压炼锭材".to_string(),
kind: "synthesis".to_string(),
description: "把零散残片和基础材料压成稳定可用的金属锭材。".to_string(),
result_label: "精炼锭材".to_string(),
currency_cost: 18,
currency_text: "18 铜钱".to_string(),
requirements: vec![RuntimeStoryForgeRequirementView {
id: "material:any".to_string(),
label: "任意材料".to_string(),
quantity: 3,
owned: 0,
}],
can_craft: false,
disabled_reason: Some("材料不足。".to_string()),
action: RuntimeStoryInventoryActionView {
function_id: "forge_craft".to_string(),
action_text: "制作精炼锭材".to_string(),
payload: Some(json!({ "recipeId": "synthesis-refined-ingot" })),
enabled: false,
reason: Some("材料不足。".to_string()),
},
}], }],
}, },
available_options: vec![RuntimeStoryOptionView { }),
function_id: "npc_chat".to_string(), };
action_text: "继续交谈".to_string(), let presentation = RuntimeStoryPresentation {
detail_text: Some("围绕当前话题继续推进关系判断。".to_string()), action_text: "".to_string(),
scope: "npc".to_string(), result_text: "".to_string(),
interaction: Some(RuntimeStoryOptionInteraction::Npc { story_text: "守火人抬眼看了你一瞬,示意你把想问的话继续说完。".to_string(),
npc_id: "npc_camp_firekeeper".to_string(), options: vec![RuntimeStoryOptionView {
action: "chat".to_string(), function_id: "npc_chat".to_string(),
quest_id: None, action_text: "继续交谈".to_string(),
}), detail_text: Some("围绕当前话题继续推进关系判断。".to_string()),
payload: Some(json!({ "note": "server-runtime-test" })), scope: "npc".to_string(),
disabled: None, interaction: Some(RuntimeStoryOptionInteraction::Npc {
reason: None,
}],
status: RuntimeStoryStatusViewModel {
in_battle: false,
npc_interaction_active: true,
current_npc_battle_mode: None,
current_npc_battle_outcome: None,
},
npc_interaction: Some(RuntimeNpcInteractionView {
npc_id: "npc_camp_firekeeper".to_string(), npc_id: "npc_camp_firekeeper".to_string(),
npc_name: "守火人".to_string(), action: "chat".to_string(),
player_currency: 80, quest_id: None,
currency_name: "铜钱".to_string(),
trade: RuntimeNpcTradeView {
buy_items: vec![RuntimeNpcTradeItemView {
item_id: "npc-potion".to_string(),
item: json!({
"id": "npc-potion",
"name": "疗伤药",
"category": "消耗品",
"quantity": 2,
"rarity": "common",
"tags": ["healing"]
}),
mode: "buy".to_string(),
unit_price: 20,
max_quantity: 2,
can_submit: true,
reason: None,
}],
sell_items: Vec::new(),
},
gift: RuntimeNpcGiftView {
items: vec![RuntimeNpcGiftItemView {
item_id: "potion-1".to_string(),
item: json!({
"id": "potion-1",
"name": "疗伤药",
"category": "消耗品",
"quantity": 2,
"rarity": "common",
"tags": ["healing"]
}),
affinity_gain: 10,
can_submit: true,
reason: None,
}],
},
}), }),
}, payload: Some(json!({ "note": "server-runtime-test" })),
presentation: RuntimeStoryPresentation { disabled: None,
action_text: "".to_string(), reason: None,
result_text: "".to_string(),
story_text: "守火人抬眼看了你一瞬,示意你把想问的话继续说完。".to_string(),
options: vec![RuntimeStoryOptionView {
function_id: "npc_chat".to_string(),
action_text: "继续交谈".to_string(),
detail_text: Some("围绕当前话题继续推进关系判断。".to_string()),
scope: "npc".to_string(),
interaction: Some(RuntimeStoryOptionInteraction::Npc {
npc_id: "npc_camp_firekeeper".to_string(),
action: "chat".to_string(),
quest_id: None,
}),
payload: Some(json!({ "note": "server-runtime-test" })),
disabled: None,
reason: None,
}],
toast: None,
battle: None,
},
patches: vec![RuntimeStoryPatch::StatusChanged {
in_battle: false,
npc_interaction_active: true,
current_npc_battle_mode: None,
current_npc_battle_outcome: None,
}], }],
snapshot: RuntimeStorySnapshotPayload { toast: None,
saved_at: Some("2026-04-22T12:00:00.000Z".to_string()), battle: None,
bottom_tab: "adventure".to_string(), };
game_state: json!({ "runtimeSessionId": "runtime-main" }), let patches = vec![RuntimeStoryPatch::StatusChanged {
current_story: Some(json!({ in_battle: false,
"text": "守火人抬眼看了你一瞬,示意你把想问的话继续说完。" npc_interaction_active: true,
})), current_npc_battle_mode: None,
}, current_npc_battle_outcome: None,
}) }];
let payload = serde_json::to_value(json!({
"viewModel": view_model,
"presentation": presentation,
"patches": patches
}))
.expect("payload should serialize"); .expect("payload should serialize");
assert_eq!(payload["sessionId"], json!("runtime-main"));
assert_eq!(payload["serverVersion"], json!(8));
assert_eq!(payload["viewModel"]["player"]["maxHp"], json!(40)); assert_eq!(payload["viewModel"]["player"]["maxHp"], json!(40));
assert_eq!( assert_eq!(
payload["viewModel"]["availableOptions"][0]["interaction"]["npcId"], payload["viewModel"]["availableOptions"][0]["interaction"]["npcId"],
@@ -759,6 +647,5 @@ mod tests {
json!("守火人抬眼看了你一瞬,示意你把想问的话继续说完。") json!("守火人抬眼看了你一瞬,示意你把想问的话继续说完。")
); );
assert_eq!(payload["patches"][0]["type"], json!("status_changed")); assert_eq!(payload["patches"][0]["type"], json!("status_changed"));
assert_eq!(payload["snapshot"]["bottomTab"], json!("adventure"));
} }
} }

View File

@@ -11,6 +11,19 @@ pub struct BeginStorySessionRequest {
pub opening_summary: Option<String>, pub opening_summary: Option<String>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BeginStoryRuntimeSessionRequest {
pub world_type: String,
#[serde(default)]
pub custom_world_profile: Option<Value>,
pub character: Value,
#[serde(default)]
pub runtime_mode: Option<String>,
#[serde(default)]
pub disable_persistence: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ContinueStoryRequest { pub struct ContinueStoryRequest {
@@ -20,6 +33,20 @@ pub struct ContinueStoryRequest {
pub choice_function_id: Option<String>, pub choice_function_id: Option<String>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ResolveStoryRuntimeActionRequest {
pub story_session_id: String,
#[serde(default)]
pub client_version: Option<u32>,
pub function_id: String,
pub action_text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub payload: Option<Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct StorySessionPayload { pub struct StorySessionPayload {
@@ -65,6 +92,17 @@ pub struct StorySessionStateResponse {
pub story_events: Vec<StoryEventPayload>, pub story_events: Vec<StoryEventPayload>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct StoryRuntimeSnapshotPayload {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub saved_at: Option<String>,
pub bottom_tab: String,
pub game_state: Value,
#[serde(default)]
pub current_story: Option<Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct StoryRuntimeProjectionRequest { pub struct StoryRuntimeProjectionRequest {
@@ -126,6 +164,7 @@ pub struct StoryRuntimeProjectionResponse {
pub story_session: StorySessionPayload, pub story_session: StorySessionPayload,
pub story_events: Vec<StoryEventPayload>, pub story_events: Vec<StoryEventPayload>,
pub server_version: u32, pub server_version: u32,
pub game_state: Value,
pub actor: StoryRuntimeActorProjection, pub actor: StoryRuntimeActorProjection,
pub inventory: StoryRuntimeInventoryProjection, pub inventory: StoryRuntimeInventoryProjection,
pub options: Vec<StoryRuntimeOptionProjection>, pub options: Vec<StoryRuntimeOptionProjection>,
@@ -138,6 +177,12 @@ pub struct StoryRuntimeProjectionResponse {
pub toast: Option<String>, pub toast: Option<String>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct StoryRuntimeMutationResponse {
pub projection: StoryRuntimeProjectionResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct StoryBattleRewardItemRequest { pub struct StoryBattleRewardItemRequest {
@@ -476,6 +521,11 @@ mod tests {
created_at: "2.000000Z".to_string(), created_at: "2.000000Z".to_string(),
}], }],
server_version: 2, server_version: 2,
game_state: json!({
"runtimeSessionId": "runtime_1",
"storySessionId": "storysess_1",
"currentScene": "Story"
}),
actor: StoryRuntimeActorProjection { actor: StoryRuntimeActorProjection {
hp: 32, hp: 32,
max_hp: 40, max_hp: 40,
@@ -516,6 +566,7 @@ mod tests {
json!("storysess_1") json!("storysess_1")
); );
assert_eq!(payload["serverVersion"], json!(2)); assert_eq!(payload["serverVersion"], json!(2));
assert_eq!(payload["gameState"]["storySessionId"], json!("storysess_1"));
assert_eq!(payload["actor"]["maxHp"], json!(40)); assert_eq!(payload["actor"]["maxHp"], json!(40));
assert_eq!( assert_eq!(
payload["inventory"]["backpackItems"][0]["name"], payload["inventory"]["backpackItems"][0]["name"],
@@ -527,6 +578,69 @@ mod tests {
assert!(payload.get("presentation").is_none()); assert!(payload.get("presentation").is_none());
} }
#[test]
fn story_runtime_mutation_response_wraps_projection_only() {
let payload = serde_json::to_value(StoryRuntimeMutationResponse {
projection: StoryRuntimeProjectionResponse {
story_session: StorySessionPayload {
story_session_id: "storysess_1".to_string(),
runtime_session_id: "runtime_1".to_string(),
actor_user_id: "user_1".to_string(),
world_profile_id: "profile_1".to_string(),
initial_prompt: "进入营地".to_string(),
opening_summary: Some("营地开场".to_string()),
latest_narrative_text: "营火还亮着。".to_string(),
latest_choice_function_id: None,
status: "active".to_string(),
version: 1,
created_at: "1.000000Z".to_string(),
updated_at: "1.000000Z".to_string(),
},
story_events: Vec::new(),
server_version: 1,
game_state: json!({
"runtimeSessionId": "runtime_1",
"storySessionId": "storysess_1",
"currentScene": "Story"
}),
actor: StoryRuntimeActorProjection {
hp: 40,
max_hp: 40,
mana: 20,
max_mana: 20,
currency: 0,
currency_text: "0 铜钱".to_string(),
},
inventory: StoryRuntimeInventoryProjection {
backpack_items: Vec::new(),
equipment_slots: Vec::new(),
forge_recipes: Vec::new(),
},
options: Vec::new(),
status: StoryRuntimeStatusProjection {
in_battle: false,
npc_interaction_active: false,
current_encounter_id: None,
current_npc_battle_mode: None,
current_npc_battle_outcome: None,
},
current_narrative_text: Some("营火还亮着。".to_string()),
action_result_text: None,
toast: None,
},
})
.expect("payload should serialize");
assert_eq!(
payload["projection"]["storySession"]["storySessionId"],
json!("storysess_1")
);
assert_eq!(payload["projection"]["serverVersion"], json!(1));
assert!(payload.get("snapshot").is_none());
assert!(payload.get("viewModel").is_none());
assert!(payload.get("presentation").is_none());
}
#[test] #[test]
fn story_battle_responses_use_story_contract_shape() { fn story_battle_responses_use_story_contract_shape() {
let battle_state = StoryBattleStatePayload { let battle_state = StoryBattleStatePayload {

View File

@@ -1,6 +1,6 @@
# spacetime-client 共享 package 说明 # spacetime-client 共享 package 说明
日期:`2026-04-20` 日期:`2026-05-01`
## 1. package 职责 ## 1. package 职责
@@ -19,24 +19,26 @@
本轮方案见 [`SERVER_RS_DDD_WP_SC_SPACETIME_CLIENT_REFACTOR_2026-04-29.md`](../../../docs/technical/SERVER_RS_DDD_WP_SC_SPACETIME_CLIENT_REFACTOR_2026-04-29.md)。 本轮方案见 [`SERVER_RS_DDD_WP_SC_SPACETIME_CLIENT_REFACTOR_2026-04-29.md`](../../../docs/technical/SERVER_RS_DDD_WP_SC_SPACETIME_CLIENT_REFACTOR_2026-04-29.md)。
## 2. 当前阶段说明 ## 2. 当前完成口径
当前目录已不再只是占位,当前阶段已经落下 当前目录已不再只是占位`WP-SC Spacetime Client` 在当前稳定 SpacetimeDB facade 范围内已经完成收尾
1. 通过 `npm run spacetime:generate -- --rust-only` 生成公开 Rust bindings 1. 通过 `npm run spacetime:generate -- --rust-only` 生成并纳管公开 Rust bindings
2. `DbConnection` 连接封装 2. `DbConnection` 连接池、握手等待、超时和断线清理已封装在 `SpacetimeClient` 内部。
3. `confirm_asset_object_and_return` procedure 的最小调用适配 3. 已稳定的 assets、auth、AI task、Big Fish、Custom World、Puzzle、Runtime/Profile/Save、Story session、combat、inventory、NPC facade 均通过 typed 方法对外暴露。
4. `bind_asset_object_to_entity_and_return` procedure 的最小调用适配 4. 生成绑定到 BFF record / module record 的 row snapshot mapper 已集中在 `mapper.rs`
5. `api-server` 所需的 `asset_object` 确认与 `asset_entity_binding` 绑定返回值转换 5. SDK 调用错误、reducer 业务错误、procedure 业务错误、缺快照错误和本地输入校验错误已统一收口到 `SpacetimeClientError` helper。
6. Story runtime projection source 已复用 runtime inventory typed facade读取投影不再只依赖 runtime snapshot 中的历史背包 JSON 副本。
`confirm_asset_object_and_return``bind_asset_object_to_entity_and_return` 的调用必须等到 SDK `on_connect` 回调后再发起。`DbConnection::build()` 只代表 WebSocket 已经初始化,不代表 SpacetimeDB 身份握手完成;如果过早调用 procedure本地联调会表现为连接建立但请求长期没有回调最终等到 idle timeout。 `confirm_asset_object_and_return``bind_asset_object_to_entity_and_return` 的调用必须等到 SDK `on_connect` 回调后再发起。`DbConnection::build()` 只代表 WebSocket 已经初始化,不代表 SpacetimeDB 身份握手完成;如果过早调用 procedure本地联调会表现为连接建立但请求长期没有回调最终等到 idle timeout。
后续与本 package 直接相关的任务包括 后续新增工作只随 `WP-ST` 新 table / reducer / procedure 或 row shape 稳定后按领域增量接入,不再把整个 `WP-SC` 包保持为进行中状态。新增 facade 时必须继续满足
1. 固化 bindings 生成与更新脚本 1. 不手写 `module_bindings` 生成物。
2. 设计 reducer、procedure、view、订阅的统一调用接口 2. 不在 `spacetime-client` 内新增领域规则。
3. 设计身份透传与连接配置策略 3. procedure / reducer shape 稳定后再接 typed facade。
4. 设计 Axum / worker / 测试环境下的客户端复用方式 4. 错误映射继续使用 `SpacetimeClientError` helper。
5. mapper 测试或 facade 定向测试随新增场景补齐。
## 2.1 `module_bindings` 生成物约束 ## 2.1 `module_bindings` 生成物约束
@@ -44,19 +46,19 @@
1. 只允许通过仓库根目录 `npm run spacetime:generate -- --rust-only` 刷新,不允许手工修改。 1. 只允许通过仓库根目录 `npm run spacetime:generate -- --rust-only` 刷新,不允许手工修改。
2. 不生成私有表绑定,不追加 `--include-private`;如后端需要读取私有表,应先在 `api-server` 或模块层补明确 contract而不是让客户端 crate 直接依赖私有表结构。 2. 不生成私有表绑定,不追加 `--include-private`;如后端需要读取私有表,应先在 `api-server` 或模块层补明确 contract而不是让客户端 crate 直接依赖私有表结构。
3. 不允许对该目录额外执行 `rustfmt`,生成物格式只接受 SpacetimeDB CLI 生成阶段自身输出 3. 不允许手工对该目录执行散装 `rustfmt`;若 SpacetimeDB CLI 生成文件但自身 formatter 在 Windows 下失败,只能由 `scripts/generate-spacetime-bindings.mjs` 在短临时目录中分批 `rustfmt` 后同步
4. `src/lib.rs` 已通过 `#[rustfmt::skip] pub mod module_bindings;` 显式阻止 workspace 级 `cargo fmt` 继续递归格式化该目录。 4. `src/lib.rs` 已通过 `#[rustfmt::skip] pub mod module_bindings;` 显式阻止 workspace 级 `cargo fmt` 继续递归格式化该目录。
5. Windows 下直接把 Rust bindings 输出到本目录时,SpacetimeDB CLI `2.1.0` 的生成后 formatter 可能因为路径参数总长触发 `文件名或扩展名太长`;仓库脚本会先输出到短临时目录,再同步回本目录。 5. Windows 下 SpacetimeDB CLI `2.1.0` 的生成后 formatter 可能因为一次性传入过多 Rust 文件路径而失败;仓库脚本会先输出到短临时目录,必要时接管分批格式化,再同步回本目录。
### 2.1.1 绑定缺文件恢复流程 ### 2.1.1 绑定缺文件恢复流程
`mod.rs` 已声明 `*_table` 模块,但目录内缺少对应 `*_table.rs` 文件,说明 Rust bindings 刷新不完整。不要手工补 generated code统一在仓库根目录执行 `mod.rs` 已声明 `*_table` 模块,但目录内缺少对应 `*_table.rs` 文件,说明 Rust bindings 刷新不完整。不要手工补 generated code统一在仓库根目录执行
```powershell ```powershell
spacetime generate --no-config --lang rust --include-private --out-dir .\server-rs\crates\spacetime-client\src\module_bindings --module-path .\server-rs\crates\spacetime-module --yes npm.cmd run spacetime:generate -- --rust-only
``` ```
这里必须带 `--no-config`:仓库根目录的 `spacetime.json` 同时配置了 TypeScript 与 Rust 两个生成目标,直接追加 `--lang` / `--out-dir` 会触发 SpacetimeDB CLI 的多目标参数冲突。 脚本内部会使用 `--no-config`:仓库根目录的 `spacetime.json` 同时配置了 TypeScript 与 Rust 两个生成目标,直接追加 `--lang` / `--out-dir` 会触发 SpacetimeDB CLI 的多目标参数冲突。
生成后用以下命令确认 `mod.rs` 声明的模块都有落盘文件: 生成后用以下命令确认 `mod.rs` 声明的模块都有落盘文件:

View File

@@ -36,7 +36,7 @@ impl SpacetimeClient {
.start_ai_task_then(reducer_input, move |_, result| { .start_ai_task_then(reducer_input, move |_, result| {
let mapped = result let mapped = result
.map_err(SpacetimeClientError::from_sdk_error) .map_err(SpacetimeClientError::from_sdk_error)
.and_then(|inner| inner.map_err(SpacetimeClientError::Runtime)); .and_then(|inner| inner.map_err(SpacetimeClientError::reducer_failed));
send_reducer_once(&callback_sender, mapped); send_reducer_once(&callback_sender, mapped);
}) })
{ {
@@ -60,7 +60,7 @@ impl SpacetimeClient {
.start_ai_task_stage_then(reducer_input, move |_, result| { .start_ai_task_stage_then(reducer_input, move |_, result| {
let mapped = result let mapped = result
.map_err(SpacetimeClientError::from_sdk_error) .map_err(SpacetimeClientError::from_sdk_error)
.and_then(|inner| inner.map_err(SpacetimeClientError::Runtime)); .and_then(|inner| inner.map_err(SpacetimeClientError::reducer_failed));
send_reducer_once(&callback_sender, mapped); send_reducer_once(&callback_sender, mapped);
}) })
{ {

View File

@@ -6,8 +6,7 @@ impl SpacetimeClient {
&self, &self,
input: DomainBattleStateInput, input: DomainBattleStateInput,
) -> Result<BattleStateRecord, SpacetimeClientError> { ) -> Result<BattleStateRecord, SpacetimeClientError> {
validate_battle_state_input(&input) validate_battle_state_input(&input).map_err(SpacetimeClientError::validation_failed)?;
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?;
let procedure_input = input.into(); let procedure_input = input.into();
self.call_after_connect(move |connection, sender| { self.call_after_connect(move |connection, sender| {
@@ -29,7 +28,7 @@ impl SpacetimeClient {
battle_state_id: String, battle_state_id: String,
) -> Result<BattleStateRecord, SpacetimeClientError> { ) -> Result<BattleStateRecord, SpacetimeClientError> {
let procedure_input = build_battle_state_query_input(battle_state_id) let procedure_input = build_battle_state_query_input(battle_state_id)
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))? .map_err(SpacetimeClientError::validation_failed)?
.into(); .into();
self.call_after_connect(move |connection, sender| { self.call_after_connect(move |connection, sender| {
@@ -50,7 +49,7 @@ impl SpacetimeClient {
input: DomainResolveCombatActionInput, input: DomainResolveCombatActionInput,
) -> Result<ResolveCombatActionRecord, SpacetimeClientError> { ) -> Result<ResolveCombatActionRecord, SpacetimeClientError> {
validate_resolve_combat_action_input(&input) validate_resolve_combat_action_input(&input)
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?; .map_err(SpacetimeClientError::validation_failed)?;
let procedure_input = input.into(); let procedure_input = input.into();
self.call_after_connect(move |connection, sender| { self.call_after_connect(move |connection, sender| {

View File

@@ -8,7 +8,7 @@ impl SpacetimeClient {
) -> Result<RuntimeInventoryStateRecord, SpacetimeClientError> { ) -> Result<RuntimeInventoryStateRecord, SpacetimeClientError> {
let procedure_input = let procedure_input =
build_runtime_inventory_state_query_input(runtime_session_id, actor_user_id) build_runtime_inventory_state_query_input(runtime_session_id, actor_user_id)
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))? .map_err(SpacetimeClientError::validation_failed)?
.into(); .into();
self.call_after_connect(move |connection, sender| { self.call_after_connect(move |connection, sender| {

View File

@@ -432,6 +432,14 @@ impl SpacetimeClientError {
Self::Procedure(error.to_string()) Self::Procedure(error.to_string())
} }
pub(crate) fn validation_failed(error: impl fmt::Display) -> Self {
Self::Runtime(error.to_string())
}
pub(crate) fn reducer_failed(message: String) -> Self {
Self::Runtime(message)
}
pub(crate) fn procedure_failed(message: Option<String>) -> Self { pub(crate) fn procedure_failed(message: Option<String>) -> Self {
Self::Procedure(message.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string())) Self::Procedure(message.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()))
} }
@@ -515,3 +523,40 @@ impl fmt::Display for SpacetimeClientError {
} }
impl Error for SpacetimeClientError {} impl Error for SpacetimeClientError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn procedure_failed_keeps_server_message_or_default() {
assert_eq!(
SpacetimeClientError::procedure_failed(Some("领域错误".to_string())).to_string(),
"领域错误"
);
assert_eq!(
SpacetimeClientError::procedure_failed(None).to_string(),
"SpacetimeDB procedure 返回未知错误"
);
}
#[test]
fn missing_snapshot_names_adapter_boundary() {
assert_eq!(
SpacetimeClientError::missing_snapshot("story session 快照").to_string(),
"SpacetimeDB procedure 未返回story session 快照"
);
}
#[test]
fn validation_and_reducer_failures_stay_runtime_classified() {
assert!(matches!(
SpacetimeClientError::validation_failed("字段缺失"),
SpacetimeClientError::Runtime(_)
));
assert!(matches!(
SpacetimeClientError::reducer_failed("状态非法".to_string()),
SpacetimeClientError::Runtime(_)
));
}
}

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::quest_record_input_type::QuestRecordInput; use super::quest_record_input_type::QuestRecordInput;
@@ -19,10 +14,8 @@ pub(super) struct AcceptQuestArgs {
impl From<AcceptQuestArgs> for super::Reducer { impl From<AcceptQuestArgs> for super::Reducer {
fn from(args: AcceptQuestArgs) -> Self { fn from(args: AcceptQuestArgs) -> Self {
Self::AcceptQuest { Self::AcceptQuest { input: args.input }
input: args.input, }
}
}
} }
impl __sdk::InModule for AcceptQuestArgs { impl __sdk::InModule for AcceptQuestArgs {
@@ -40,9 +33,8 @@ pub trait accept_quest {
/// The reducer will run asynchronously in the future, /// The reducer will run asynchronously in the future,
/// and this method provides no way to listen for its completion status. /// and this method provides no way to listen for its completion status.
/// /// Use [`accept_quest:accept_quest_then`] to run a callback after the reducer completes. /// /// Use [`accept_quest:accept_quest_then`] to run a callback after the reducer completes.
fn accept_quest(&self, input: QuestRecordInput, fn accept_quest(&self, input: QuestRecordInput) -> __sdk::Result<()> {
) -> __sdk::Result<()> { self.accept_quest_then(input, |_, _| {})
self.accept_quest_then(input, |_, _| {})
} }
/// Request that the remote module invoke the reducer `accept_quest` to run as soon as possible, /// Request that the remote module invoke the reducer `accept_quest` to run as soon as possible,
@@ -55,9 +47,11 @@ pub trait accept_quest {
&self, &self,
input: QuestRecordInput, input: QuestRecordInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>) callback: impl FnOnce(
+ Send &super::ReducerEventContext,
+ 'static, Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()>; ) -> __sdk::Result<()>;
} }
@@ -66,11 +60,13 @@ impl accept_quest for super::RemoteReducers {
&self, &self,
input: QuestRecordInput, input: QuestRecordInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>) callback: impl FnOnce(
+ Send &super::ReducerEventContext,
+ 'static, Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()> { ) -> __sdk::Result<()> {
self.imp.invoke_reducer_with_callback(AcceptQuestArgs { input, }, callback) self.imp
.invoke_reducer_with_callback(AcceptQuestArgs { input }, callback)
} }
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::quest_completion_ack_input_type::QuestCompletionAckInput; use super::quest_completion_ack_input_type::QuestCompletionAckInput;
@@ -19,10 +14,8 @@ pub(super) struct AcknowledgeQuestCompletionArgs {
impl From<AcknowledgeQuestCompletionArgs> for super::Reducer { impl From<AcknowledgeQuestCompletionArgs> for super::Reducer {
fn from(args: AcknowledgeQuestCompletionArgs) -> Self { fn from(args: AcknowledgeQuestCompletionArgs) -> Self {
Self::AcknowledgeQuestCompletion { Self::AcknowledgeQuestCompletion { input: args.input }
input: args.input, }
}
}
} }
impl __sdk::InModule for AcknowledgeQuestCompletionArgs { impl __sdk::InModule for AcknowledgeQuestCompletionArgs {
@@ -40,9 +33,8 @@ pub trait acknowledge_quest_completion {
/// The reducer will run asynchronously in the future, /// The reducer will run asynchronously in the future,
/// and this method provides no way to listen for its completion status. /// and this method provides no way to listen for its completion status.
/// /// Use [`acknowledge_quest_completion:acknowledge_quest_completion_then`] to run a callback after the reducer completes. /// /// Use [`acknowledge_quest_completion:acknowledge_quest_completion_then`] to run a callback after the reducer completes.
fn acknowledge_quest_completion(&self, input: QuestCompletionAckInput, fn acknowledge_quest_completion(&self, input: QuestCompletionAckInput) -> __sdk::Result<()> {
) -> __sdk::Result<()> { self.acknowledge_quest_completion_then(input, |_, _| {})
self.acknowledge_quest_completion_then(input, |_, _| {})
} }
/// Request that the remote module invoke the reducer `acknowledge_quest_completion` to run as soon as possible, /// Request that the remote module invoke the reducer `acknowledge_quest_completion` to run as soon as possible,
@@ -55,9 +47,11 @@ pub trait acknowledge_quest_completion {
&self, &self,
input: QuestCompletionAckInput, input: QuestCompletionAckInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>) callback: impl FnOnce(
+ Send &super::ReducerEventContext,
+ 'static, Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()>; ) -> __sdk::Result<()>;
} }
@@ -66,11 +60,13 @@ impl acknowledge_quest_completion for super::RemoteReducers {
&self, &self,
input: QuestCompletionAckInput, input: QuestCompletionAckInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>) callback: impl FnOnce(
+ Send &super::ReducerEventContext,
+ 'static, Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()> { ) -> __sdk::Result<()> {
self.imp.invoke_reducer_with_callback(AcknowledgeQuestCompletionArgs { input, }, callback) self.imp
.invoke_reducer_with_callback(AcknowledgeQuestCompletionArgs { input }, callback)
} }
} }

View File

@@ -2,23 +2,17 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::runtime_profile_redeem_code_admin_disable_input_type::RuntimeProfileRedeemCodeAdminDisableInput; use super::runtime_profile_redeem_code_admin_disable_input_type::RuntimeProfileRedeemCodeAdminDisableInput;
use super::runtime_profile_redeem_code_admin_procedure_result_type::RuntimeProfileRedeemCodeAdminProcedureResult; use super::runtime_profile_redeem_code_admin_procedure_result_type::RuntimeProfileRedeemCodeAdminProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)] #[sats(crate = __lib)]
struct AdminDisableProfileRedeemCodeArgs { struct AdminDisableProfileRedeemCodeArgs {
pub input: RuntimeProfileRedeemCodeAdminDisableInput, pub input: RuntimeProfileRedeemCodeAdminDisableInput,
} }
impl __sdk::InModule for AdminDisableProfileRedeemCodeArgs { impl __sdk::InModule for AdminDisableProfileRedeemCodeArgs {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }
@@ -28,16 +22,19 @@ impl __sdk::InModule for AdminDisableProfileRedeemCodeArgs {
/// ///
/// Implemented for [`super::RemoteProcedures`]. /// Implemented for [`super::RemoteProcedures`].
pub trait admin_disable_profile_redeem_code { pub trait admin_disable_profile_redeem_code {
fn admin_disable_profile_redeem_code(&self, input: RuntimeProfileRedeemCodeAdminDisableInput, fn admin_disable_profile_redeem_code(&self, input: RuntimeProfileRedeemCodeAdminDisableInput) {
) { self.admin_disable_profile_redeem_code_then(input, |_, _| {});
self.admin_disable_profile_redeem_code_then(input, |_, _| {});
} }
fn admin_disable_profile_redeem_code_then( fn admin_disable_profile_redeem_code_then(
&self, &self,
input: RuntimeProfileRedeemCodeAdminDisableInput, input: RuntimeProfileRedeemCodeAdminDisableInput,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>) + Send + 'static, __callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
); );
} }
@@ -46,13 +43,17 @@ impl admin_disable_profile_redeem_code for super::RemoteProcedures {
&self, &self,
input: RuntimeProfileRedeemCodeAdminDisableInput, input: RuntimeProfileRedeemCodeAdminDisableInput,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>) + Send + 'static, __callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) { ) {
self.imp.invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminProcedureResult>( self.imp
"admin_disable_profile_redeem_code", .invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminProcedureResult>(
AdminDisableProfileRedeemCodeArgs { input, }, "admin_disable_profile_redeem_code",
__callback, AdminDisableProfileRedeemCodeArgs { input },
); __callback,
);
} }
} }

View File

@@ -2,23 +2,17 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::runtime_profile_redeem_code_admin_procedure_result_type::RuntimeProfileRedeemCodeAdminProcedureResult; use super::runtime_profile_redeem_code_admin_procedure_result_type::RuntimeProfileRedeemCodeAdminProcedureResult;
use super::runtime_profile_redeem_code_admin_upsert_input_type::RuntimeProfileRedeemCodeAdminUpsertInput; use super::runtime_profile_redeem_code_admin_upsert_input_type::RuntimeProfileRedeemCodeAdminUpsertInput;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)] #[sats(crate = __lib)]
struct AdminUpsertProfileRedeemCodeArgs { struct AdminUpsertProfileRedeemCodeArgs {
pub input: RuntimeProfileRedeemCodeAdminUpsertInput, pub input: RuntimeProfileRedeemCodeAdminUpsertInput,
} }
impl __sdk::InModule for AdminUpsertProfileRedeemCodeArgs { impl __sdk::InModule for AdminUpsertProfileRedeemCodeArgs {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }
@@ -28,16 +22,19 @@ impl __sdk::InModule for AdminUpsertProfileRedeemCodeArgs {
/// ///
/// Implemented for [`super::RemoteProcedures`]. /// Implemented for [`super::RemoteProcedures`].
pub trait admin_upsert_profile_redeem_code { pub trait admin_upsert_profile_redeem_code {
fn admin_upsert_profile_redeem_code(&self, input: RuntimeProfileRedeemCodeAdminUpsertInput, fn admin_upsert_profile_redeem_code(&self, input: RuntimeProfileRedeemCodeAdminUpsertInput) {
) { self.admin_upsert_profile_redeem_code_then(input, |_, _| {});
self.admin_upsert_profile_redeem_code_then(input, |_, _| {});
} }
fn admin_upsert_profile_redeem_code_then( fn admin_upsert_profile_redeem_code_then(
&self, &self,
input: RuntimeProfileRedeemCodeAdminUpsertInput, input: RuntimeProfileRedeemCodeAdminUpsertInput,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>) + Send + 'static, __callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
); );
} }
@@ -46,13 +43,17 @@ impl admin_upsert_profile_redeem_code for super::RemoteProcedures {
&self, &self,
input: RuntimeProfileRedeemCodeAdminUpsertInput, input: RuntimeProfileRedeemCodeAdminUpsertInput,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>) + Send + 'static, __callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) { ) {
self.imp.invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminProcedureResult>( self.imp
"admin_upsert_profile_redeem_code", .invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminProcedureResult>(
AdminUpsertProfileRedeemCodeArgs { input, }, "admin_upsert_profile_redeem_code",
__callback, AdminUpsertProfileRedeemCodeArgs { input },
); __callback,
);
} }
} }

View File

@@ -2,23 +2,17 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::puzzle_run_next_level_input_type::PuzzleRunNextLevelInput; use super::puzzle_run_next_level_input_type::PuzzleRunNextLevelInput;
use super::puzzle_run_procedure_result_type::PuzzleRunProcedureResult; use super::puzzle_run_procedure_result_type::PuzzleRunProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)] #[sats(crate = __lib)]
struct AdvancePuzzleNextLevelArgs { struct AdvancePuzzleNextLevelArgs {
pub input: PuzzleRunNextLevelInput, pub input: PuzzleRunNextLevelInput,
} }
impl __sdk::InModule for AdvancePuzzleNextLevelArgs { impl __sdk::InModule for AdvancePuzzleNextLevelArgs {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }
@@ -28,16 +22,19 @@ impl __sdk::InModule for AdvancePuzzleNextLevelArgs {
/// ///
/// Implemented for [`super::RemoteProcedures`]. /// Implemented for [`super::RemoteProcedures`].
pub trait advance_puzzle_next_level { pub trait advance_puzzle_next_level {
fn advance_puzzle_next_level(&self, input: PuzzleRunNextLevelInput, fn advance_puzzle_next_level(&self, input: PuzzleRunNextLevelInput) {
) { self.advance_puzzle_next_level_then(input, |_, _| {});
self.advance_puzzle_next_level_then(input, |_, _| {});
} }
fn advance_puzzle_next_level_then( fn advance_puzzle_next_level_then(
&self, &self,
input: PuzzleRunNextLevelInput, input: PuzzleRunNextLevelInput,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<PuzzleRunProcedureResult, __sdk::InternalError>) + Send + 'static, __callback: impl FnOnce(
&super::ProcedureEventContext,
Result<PuzzleRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
); );
} }
@@ -46,13 +43,17 @@ impl advance_puzzle_next_level for super::RemoteProcedures {
&self, &self,
input: PuzzleRunNextLevelInput, input: PuzzleRunNextLevelInput,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<PuzzleRunProcedureResult, __sdk::InternalError>) + Send + 'static, __callback: impl FnOnce(
&super::ProcedureEventContext,
Result<PuzzleRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) { ) {
self.imp.invoke_procedure_with_callback::<_, PuzzleRunProcedureResult>( self.imp
"advance_puzzle_next_level", .invoke_procedure_with_callback::<_, PuzzleRunProcedureResult>(
AdvancePuzzleNextLevelArgs { input, }, "advance_puzzle_next_level",
__callback, AdvancePuzzleNextLevelArgs { input },
); __callback,
);
} }
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_result_reference_kind_type::AiResultReferenceKind; use super::ai_result_reference_kind_type::AiResultReferenceKind;
@@ -17,12 +12,10 @@ pub struct AiResultReferenceInput {
pub task_id: String, pub task_id: String,
pub reference_kind: AiResultReferenceKind, pub reference_kind: AiResultReferenceKind,
pub reference_id: String, pub reference_id: String,
pub label: Option::<String>, pub label: Option<String>,
pub created_at_micros: i64, pub created_at_micros: i64,
} }
impl __sdk::InModule for AiResultReferenceInput { impl __sdk::InModule for AiResultReferenceInput {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)] #[sats(crate = __lib)]
@@ -24,12 +19,8 @@ pub enum AiResultReferenceKind {
RuntimeItemRecord, RuntimeItemRecord,
AssetObject, AssetObject,
} }
impl __sdk::InModule for AiResultReferenceKind { impl __sdk::InModule for AiResultReferenceKind {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_result_reference_kind_type::AiResultReferenceKind; use super::ai_result_reference_kind_type::AiResultReferenceKind;
@@ -18,12 +13,10 @@ pub struct AiResultReferenceSnapshot {
pub task_id: String, pub task_id: String,
pub reference_kind: AiResultReferenceKind, pub reference_kind: AiResultReferenceKind,
pub reference_id: String, pub reference_id: String,
pub label: Option::<String>, pub label: Option<String>,
pub created_at_micros: i64, pub created_at_micros: i64,
} }
impl __sdk::InModule for AiResultReferenceSnapshot { impl __sdk::InModule for AiResultReferenceSnapshot {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_result_reference_kind_type::AiResultReferenceKind; use super::ai_result_reference_kind_type::AiResultReferenceKind;
@@ -19,16 +14,14 @@ pub struct AiResultReference {
pub task_id: String, pub task_id: String,
pub reference_kind: AiResultReferenceKind, pub reference_kind: AiResultReferenceKind,
pub reference_id: String, pub reference_id: String,
pub label: Option::<String>, pub label: Option<String>,
pub created_at: __sdk::Timestamp, pub created_at: __sdk::Timestamp,
} }
impl __sdk::InModule for AiResultReference { impl __sdk::InModule for AiResultReference {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }
/// Column accessor struct for the table `AiResultReference`. /// Column accessor struct for the table `AiResultReference`.
/// ///
/// Provides typed access to columns for query building. /// Provides typed access to columns for query building.
@@ -38,7 +31,7 @@ pub struct AiResultReferenceCols {
pub task_id: __sdk::__query_builder::Col<AiResultReference, String>, pub task_id: __sdk::__query_builder::Col<AiResultReference, String>,
pub reference_kind: __sdk::__query_builder::Col<AiResultReference, AiResultReferenceKind>, pub reference_kind: __sdk::__query_builder::Col<AiResultReference, AiResultReferenceKind>,
pub reference_id: __sdk::__query_builder::Col<AiResultReference, String>, pub reference_id: __sdk::__query_builder::Col<AiResultReference, String>,
pub label: __sdk::__query_builder::Col<AiResultReference, Option::<String>>, pub label: __sdk::__query_builder::Col<AiResultReference, Option<String>>,
pub created_at: __sdk::__query_builder::Col<AiResultReference, __sdk::Timestamp>, pub created_at: __sdk::__query_builder::Col<AiResultReference, __sdk::Timestamp>,
} }
@@ -46,14 +39,16 @@ impl __sdk::__query_builder::HasCols for AiResultReference {
type Cols = AiResultReferenceCols; type Cols = AiResultReferenceCols;
fn cols(table_name: &'static str) -> Self::Cols { fn cols(table_name: &'static str) -> Self::Cols {
AiResultReferenceCols { AiResultReferenceCols {
result_reference_row_id: __sdk::__query_builder::Col::new(table_name, "result_reference_row_id"), result_reference_row_id: __sdk::__query_builder::Col::new(
table_name,
"result_reference_row_id",
),
result_ref_id: __sdk::__query_builder::Col::new(table_name, "result_ref_id"), result_ref_id: __sdk::__query_builder::Col::new(table_name, "result_ref_id"),
task_id: __sdk::__query_builder::Col::new(table_name, "task_id"), task_id: __sdk::__query_builder::Col::new(table_name, "task_id"),
reference_kind: __sdk::__query_builder::Col::new(table_name, "reference_kind"), reference_kind: __sdk::__query_builder::Col::new(table_name, "reference_kind"),
reference_id: __sdk::__query_builder::Col::new(table_name, "reference_id"), reference_id: __sdk::__query_builder::Col::new(table_name, "reference_id"),
label: __sdk::__query_builder::Col::new(table_name, "label"), label: __sdk::__query_builder::Col::new(table_name, "label"),
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
} }
} }
} }
@@ -70,12 +65,13 @@ impl __sdk::__query_builder::HasIxCols for AiResultReference {
type IxCols = AiResultReferenceIxCols; type IxCols = AiResultReferenceIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols { fn ix_cols(table_name: &'static str) -> Self::IxCols {
AiResultReferenceIxCols { AiResultReferenceIxCols {
result_reference_row_id: __sdk::__query_builder::IxCol::new(table_name, "result_reference_row_id"), result_reference_row_id: __sdk::__query_builder::IxCol::new(
table_name,
"result_reference_row_id",
),
task_id: __sdk::__query_builder::IxCol::new(table_name, "task_id"), task_id: __sdk::__query_builder::IxCol::new(table_name, "task_id"),
} }
} }
} }
impl __sdk::__query_builder::CanBeLookupTable for AiResultReference {} impl __sdk::__query_builder::CanBeLookupTable for AiResultReference {}

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_stage_kind_type::AiTaskStageKind; use super::ai_task_stage_kind_type::AiTaskStageKind;
@@ -16,14 +11,12 @@ use super::ai_task_stage_kind_type::AiTaskStageKind;
pub struct AiStageCompletionInput { pub struct AiStageCompletionInput {
pub task_id: String, pub task_id: String,
pub stage_kind: AiTaskStageKind, pub stage_kind: AiTaskStageKind,
pub text_output: Option::<String>, pub text_output: Option<String>,
pub structured_payload_json: Option::<String>, pub structured_payload_json: Option<String>,
pub warning_messages: Vec::<String>, pub warning_messages: Vec<String>,
pub completed_at_micros: i64, pub completed_at_micros: i64,
} }
impl __sdk::InModule for AiStageCompletionInput { impl __sdk::InModule for AiStageCompletionInput {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,13 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)] #[sats(crate = __lib)]
@@ -17,8 +11,6 @@ pub struct AiTaskCancelInput {
pub completed_at_micros: i64, pub completed_at_micros: i64,
} }
impl __sdk::InModule for AiTaskCancelInput { impl __sdk::InModule for AiTaskCancelInput {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_kind_type::AiTaskKind; use super::ai_task_kind_type::AiTaskKind;
use super::ai_task_stage_blueprint_type::AiTaskStageBlueprint; use super::ai_task_stage_blueprint_type::AiTaskStageBlueprint;
@@ -20,14 +15,12 @@ pub struct AiTaskCreateInput {
pub owner_user_id: String, pub owner_user_id: String,
pub request_label: String, pub request_label: String,
pub source_module: String, pub source_module: String,
pub source_entity_id: Option::<String>, pub source_entity_id: Option<String>,
pub request_payload_json: Option::<String>, pub request_payload_json: Option<String>,
pub stages: Vec::<AiTaskStageBlueprint>, pub stages: Vec<AiTaskStageBlueprint>,
pub created_at_micros: i64, pub created_at_micros: i64,
} }
impl __sdk::InModule for AiTaskCreateInput { impl __sdk::InModule for AiTaskCreateInput {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)] #[sats(crate = __lib)]
@@ -24,12 +19,8 @@ pub enum AiTaskEventKind {
TextChunkAppended, TextChunkAppended,
ResultReferenceAttached, ResultReferenceAttached,
} }
impl __sdk::InModule for AiTaskEventKind { impl __sdk::InModule for AiTaskEventKind {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,16 +2,11 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_event_type::AiTaskEvent;
use super::ai_task_status_type::AiTaskStatus;
use super::ai_task_event_kind_type::AiTaskEventKind; use super::ai_task_event_kind_type::AiTaskEventKind;
use super::ai_task_event_type::AiTaskEvent;
use super::ai_task_stage_kind_type::AiTaskStageKind; use super::ai_task_stage_kind_type::AiTaskStageKind;
use super::ai_task_status_type::AiTaskStatus;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `ai_task_event`. /// Table handle for the table `ai_task_event`.
/// ///
@@ -51,8 +46,12 @@ impl<'ctx> __sdk::EventTable for AiTaskEventTableHandle<'ctx> {
type Row = AiTaskEvent; type Row = AiTaskEvent;
type EventContext = super::EventContext; type EventContext = super::EventContext;
fn count(&self) -> u64 { self.imp.count() } fn count(&self) -> u64 {
fn iter(&self) -> impl Iterator<Item = AiTaskEvent> + '_ { self.imp.iter() } self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = AiTaskEvent> + '_ {
self.imp.iter()
}
type InsertCallbackId = AiTaskEventInsertCallbackId; type InsertCallbackId = AiTaskEventInsertCallbackId;
@@ -70,8 +69,7 @@ impl<'ctx> __sdk::EventTable for AiTaskEventTableHandle<'ctx> {
#[doc(hidden)] #[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) { pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table = client_cache.get_or_make_table::<AiTaskEvent>("ai_task_event");
let _table = client_cache.get_or_make_table::<AiTaskEvent>("ai_task_event");
_table.add_unique_constraint::<String>("event_id", |row| &row.event_id); _table.add_unique_constraint::<String>("event_id", |row| &row.event_id);
} }
@@ -80,26 +78,24 @@ pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate, raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<AiTaskEvent>> { ) -> __sdk::Result<__sdk::TableUpdate<AiTaskEvent>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse( __sdk::InternalError::failed_parse("TableUpdate<AiTaskEvent>", "TableUpdate")
"TableUpdate<AiTaskEvent>", .with_cause(e)
"TableUpdate", .into()
).with_cause(e).into()
}) })
} }
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `AiTaskEvent`. /// Extension trait for query builder access to the table `AiTaskEvent`.
/// ///
/// Implemented for [`__sdk::QueryTableAccessor`]. /// Implemented for [`__sdk::QueryTableAccessor`].
pub trait ai_task_eventQueryTableAccess { pub trait ai_task_eventQueryTableAccess {
#[allow(non_snake_case)] #[allow(non_snake_case)]
/// Get a query builder for the table `AiTaskEvent`. /// Get a query builder for the table `AiTaskEvent`.
fn ai_task_event(&self) -> __sdk::__query_builder::Table<AiTaskEvent>; fn ai_task_event(&self) -> __sdk::__query_builder::Table<AiTaskEvent>;
} }
impl ai_task_eventQueryTableAccess for __sdk::QueryTableAccessor {
fn ai_task_event(&self) -> __sdk::__query_builder::Table<AiTaskEvent> {
__sdk::__query_builder::Table::new("ai_task_event")
}
}
impl ai_task_eventQueryTableAccess for __sdk::QueryTableAccessor {
fn ai_task_event(&self) -> __sdk::__query_builder::Table<AiTaskEvent> {
__sdk::__query_builder::Table::new("ai_task_event")
}
}

View File

@@ -2,16 +2,11 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_status_type::AiTaskStatus;
use super::ai_task_event_kind_type::AiTaskEventKind; use super::ai_task_event_kind_type::AiTaskEventKind;
use super::ai_task_stage_kind_type::AiTaskStageKind; use super::ai_task_stage_kind_type::AiTaskStageKind;
use super::ai_task_status_type::AiTaskStatus;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)] #[sats(crate = __lib)]
@@ -20,19 +15,17 @@ pub struct AiTaskEvent {
pub task_id: String, pub task_id: String,
pub owner_user_id: String, pub owner_user_id: String,
pub event_kind: AiTaskEventKind, pub event_kind: AiTaskEventKind,
pub task_status: Option::<AiTaskStatus>, pub task_status: Option<AiTaskStatus>,
pub stage_kind: Option::<AiTaskStageKind>, pub stage_kind: Option<AiTaskStageKind>,
pub text_chunk_row_id: Option::<String>, pub text_chunk_row_id: Option<String>,
pub result_reference_row_id: Option::<String>, pub result_reference_row_id: Option<String>,
pub occurred_at: __sdk::Timestamp, pub occurred_at: __sdk::Timestamp,
} }
impl __sdk::InModule for AiTaskEvent { impl __sdk::InModule for AiTaskEvent {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }
/// Column accessor struct for the table `AiTaskEvent`. /// Column accessor struct for the table `AiTaskEvent`.
/// ///
/// Provides typed access to columns for query building. /// Provides typed access to columns for query building.
@@ -41,10 +34,10 @@ pub struct AiTaskEventCols {
pub task_id: __sdk::__query_builder::Col<AiTaskEvent, String>, pub task_id: __sdk::__query_builder::Col<AiTaskEvent, String>,
pub owner_user_id: __sdk::__query_builder::Col<AiTaskEvent, String>, pub owner_user_id: __sdk::__query_builder::Col<AiTaskEvent, String>,
pub event_kind: __sdk::__query_builder::Col<AiTaskEvent, AiTaskEventKind>, pub event_kind: __sdk::__query_builder::Col<AiTaskEvent, AiTaskEventKind>,
pub task_status: __sdk::__query_builder::Col<AiTaskEvent, Option::<AiTaskStatus>>, pub task_status: __sdk::__query_builder::Col<AiTaskEvent, Option<AiTaskStatus>>,
pub stage_kind: __sdk::__query_builder::Col<AiTaskEvent, Option::<AiTaskStageKind>>, pub stage_kind: __sdk::__query_builder::Col<AiTaskEvent, Option<AiTaskStageKind>>,
pub text_chunk_row_id: __sdk::__query_builder::Col<AiTaskEvent, Option::<String>>, pub text_chunk_row_id: __sdk::__query_builder::Col<AiTaskEvent, Option<String>>,
pub result_reference_row_id: __sdk::__query_builder::Col<AiTaskEvent, Option::<String>>, pub result_reference_row_id: __sdk::__query_builder::Col<AiTaskEvent, Option<String>>,
pub occurred_at: __sdk::__query_builder::Col<AiTaskEvent, __sdk::Timestamp>, pub occurred_at: __sdk::__query_builder::Col<AiTaskEvent, __sdk::Timestamp>,
} }
@@ -59,9 +52,11 @@ impl __sdk::__query_builder::HasCols for AiTaskEvent {
task_status: __sdk::__query_builder::Col::new(table_name, "task_status"), task_status: __sdk::__query_builder::Col::new(table_name, "task_status"),
stage_kind: __sdk::__query_builder::Col::new(table_name, "stage_kind"), stage_kind: __sdk::__query_builder::Col::new(table_name, "stage_kind"),
text_chunk_row_id: __sdk::__query_builder::Col::new(table_name, "text_chunk_row_id"), text_chunk_row_id: __sdk::__query_builder::Col::new(table_name, "text_chunk_row_id"),
result_reference_row_id: __sdk::__query_builder::Col::new(table_name, "result_reference_row_id"), result_reference_row_id: __sdk::__query_builder::Col::new(
table_name,
"result_reference_row_id",
),
occurred_at: __sdk::__query_builder::Col::new(table_name, "occurred_at"), occurred_at: __sdk::__query_builder::Col::new(table_name, "occurred_at"),
} }
} }
} }
@@ -82,8 +77,6 @@ impl __sdk::__query_builder::HasIxCols for AiTaskEvent {
event_id: __sdk::__query_builder::IxCol::new(table_name, "event_id"), event_id: __sdk::__query_builder::IxCol::new(table_name, "event_id"),
owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"),
task_id: __sdk::__query_builder::IxCol::new(table_name, "task_id"), task_id: __sdk::__query_builder::IxCol::new(table_name, "task_id"),
} }
} }
} }

View File

@@ -2,13 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)] #[sats(crate = __lib)]
@@ -18,8 +12,6 @@ pub struct AiTaskFailureInput {
pub completed_at_micros: i64, pub completed_at_micros: i64,
} }
impl __sdk::InModule for AiTaskFailureInput { impl __sdk::InModule for AiTaskFailureInput {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,13 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)] #[sats(crate = __lib)]
@@ -17,8 +11,6 @@ pub struct AiTaskFinishInput {
pub completed_at_micros: i64, pub completed_at_micros: i64,
} }
impl __sdk::InModule for AiTaskFinishInput { impl __sdk::InModule for AiTaskFinishInput {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)] #[sats(crate = __lib)]
@@ -24,12 +19,8 @@ pub enum AiTaskKind {
QuestIntent, QuestIntent,
RuntimeItemIntent, RuntimeItemIntent,
} }
impl __sdk::InModule for AiTaskKind { impl __sdk::InModule for AiTaskKind {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_snapshot_type::AiTaskSnapshot; use super::ai_task_snapshot_type::AiTaskSnapshot;
use super::ai_text_chunk_snapshot_type::AiTextChunkSnapshot; use super::ai_text_chunk_snapshot_type::AiTextChunkSnapshot;
@@ -16,13 +11,11 @@ use super::ai_text_chunk_snapshot_type::AiTextChunkSnapshot;
#[sats(crate = __lib)] #[sats(crate = __lib)]
pub struct AiTaskProcedureResult { pub struct AiTaskProcedureResult {
pub ok: bool, pub ok: bool,
pub task: Option::<AiTaskSnapshot>, pub task: Option<AiTaskSnapshot>,
pub text_chunk: Option::<AiTextChunkSnapshot>, pub text_chunk: Option<AiTextChunkSnapshot>,
pub error_message: Option::<String>, pub error_message: Option<String>,
} }
impl __sdk::InModule for AiTaskProcedureResult { impl __sdk::InModule for AiTaskProcedureResult {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,17 +2,12 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_kind_type::AiTaskKind;
use super::ai_task_status_type::AiTaskStatus;
use super::ai_task_stage_snapshot_type::AiTaskStageSnapshot;
use super::ai_result_reference_snapshot_type::AiResultReferenceSnapshot; use super::ai_result_reference_snapshot_type::AiResultReferenceSnapshot;
use super::ai_task_kind_type::AiTaskKind;
use super::ai_task_stage_snapshot_type::AiTaskStageSnapshot;
use super::ai_task_status_type::AiTaskStatus;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)] #[sats(crate = __lib)]
@@ -22,23 +17,21 @@ pub struct AiTaskSnapshot {
pub owner_user_id: String, pub owner_user_id: String,
pub request_label: String, pub request_label: String,
pub source_module: String, pub source_module: String,
pub source_entity_id: Option::<String>, pub source_entity_id: Option<String>,
pub request_payload_json: Option::<String>, pub request_payload_json: Option<String>,
pub status: AiTaskStatus, pub status: AiTaskStatus,
pub failure_message: Option::<String>, pub failure_message: Option<String>,
pub stages: Vec::<AiTaskStageSnapshot>, pub stages: Vec<AiTaskStageSnapshot>,
pub result_references: Vec::<AiResultReferenceSnapshot>, pub result_references: Vec<AiResultReferenceSnapshot>,
pub latest_text_output: Option::<String>, pub latest_text_output: Option<String>,
pub latest_structured_payload_json: Option::<String>, pub latest_structured_payload_json: Option<String>,
pub version: u32, pub version: u32,
pub created_at_micros: i64, pub created_at_micros: i64,
pub started_at_micros: Option::<i64>, pub started_at_micros: Option<i64>,
pub completed_at_micros: Option::<i64>, pub completed_at_micros: Option<i64>,
pub updated_at_micros: i64, pub updated_at_micros: i64,
} }
impl __sdk::InModule for AiTaskSnapshot { impl __sdk::InModule for AiTaskSnapshot {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_stage_kind_type::AiTaskStageKind; use super::ai_task_stage_kind_type::AiTaskStageKind;
@@ -20,8 +15,6 @@ pub struct AiTaskStageBlueprint {
pub order: u32, pub order: u32,
} }
impl __sdk::InModule for AiTaskStageBlueprint { impl __sdk::InModule for AiTaskStageBlueprint {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)] #[sats(crate = __lib)]
@@ -22,12 +17,8 @@ pub enum AiTaskStageKind {
NormalizeResult, NormalizeResult,
PersistResult, PersistResult,
} }
impl __sdk::InModule for AiTaskStageKind { impl __sdk::InModule for AiTaskStageKind {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_stage_kind_type::AiTaskStageKind; use super::ai_task_stage_kind_type::AiTaskStageKind;
use super::ai_task_stage_status_type::AiTaskStageStatus; use super::ai_task_stage_status_type::AiTaskStageStatus;
@@ -20,15 +15,13 @@ pub struct AiTaskStageSnapshot {
pub detail: String, pub detail: String,
pub order: u32, pub order: u32,
pub status: AiTaskStageStatus, pub status: AiTaskStageStatus,
pub text_output: Option::<String>, pub text_output: Option<String>,
pub structured_payload_json: Option::<String>, pub structured_payload_json: Option<String>,
pub warning_messages: Vec::<String>, pub warning_messages: Vec<String>,
pub started_at_micros: Option::<i64>, pub started_at_micros: Option<i64>,
pub completed_at_micros: Option::<i64>, pub completed_at_micros: Option<i64>,
} }
impl __sdk::InModule for AiTaskStageSnapshot { impl __sdk::InModule for AiTaskStageSnapshot {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_stage_kind_type::AiTaskStageKind; use super::ai_task_stage_kind_type::AiTaskStageKind;
@@ -19,8 +14,6 @@ pub struct AiTaskStageStartInput {
pub started_at_micros: i64, pub started_at_micros: i64,
} }
impl __sdk::InModule for AiTaskStageStartInput { impl __sdk::InModule for AiTaskStageStartInput {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)] #[sats(crate = __lib)]
@@ -20,12 +15,8 @@ pub enum AiTaskStageStatus {
Completed, Completed,
Skipped, Skipped,
} }
impl __sdk::InModule for AiTaskStageStatus { impl __sdk::InModule for AiTaskStageStatus {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_stage_kind_type::AiTaskStageKind; use super::ai_task_stage_kind_type::AiTaskStageKind;
use super::ai_task_stage_status_type::AiTaskStageStatus; use super::ai_task_stage_status_type::AiTaskStageStatus;
@@ -22,19 +17,17 @@ pub struct AiTaskStage {
pub detail: String, pub detail: String,
pub stage_order: u32, pub stage_order: u32,
pub status: AiTaskStageStatus, pub status: AiTaskStageStatus,
pub text_output: Option::<String>, pub text_output: Option<String>,
pub structured_payload_json: Option::<String>, pub structured_payload_json: Option<String>,
pub warning_messages: Vec::<String>, pub warning_messages: Vec<String>,
pub started_at: Option::<__sdk::Timestamp>, pub started_at: Option<__sdk::Timestamp>,
pub completed_at: Option::<__sdk::Timestamp>, pub completed_at: Option<__sdk::Timestamp>,
} }
impl __sdk::InModule for AiTaskStage { impl __sdk::InModule for AiTaskStage {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }
/// Column accessor struct for the table `AiTaskStage`. /// Column accessor struct for the table `AiTaskStage`.
/// ///
/// Provides typed access to columns for query building. /// Provides typed access to columns for query building.
@@ -46,11 +39,11 @@ pub struct AiTaskStageCols {
pub detail: __sdk::__query_builder::Col<AiTaskStage, String>, pub detail: __sdk::__query_builder::Col<AiTaskStage, String>,
pub stage_order: __sdk::__query_builder::Col<AiTaskStage, u32>, pub stage_order: __sdk::__query_builder::Col<AiTaskStage, u32>,
pub status: __sdk::__query_builder::Col<AiTaskStage, AiTaskStageStatus>, pub status: __sdk::__query_builder::Col<AiTaskStage, AiTaskStageStatus>,
pub text_output: __sdk::__query_builder::Col<AiTaskStage, Option::<String>>, pub text_output: __sdk::__query_builder::Col<AiTaskStage, Option<String>>,
pub structured_payload_json: __sdk::__query_builder::Col<AiTaskStage, Option::<String>>, pub structured_payload_json: __sdk::__query_builder::Col<AiTaskStage, Option<String>>,
pub warning_messages: __sdk::__query_builder::Col<AiTaskStage, Vec::<String>>, pub warning_messages: __sdk::__query_builder::Col<AiTaskStage, Vec<String>>,
pub started_at: __sdk::__query_builder::Col<AiTaskStage, Option::<__sdk::Timestamp>>, pub started_at: __sdk::__query_builder::Col<AiTaskStage, Option<__sdk::Timestamp>>,
pub completed_at: __sdk::__query_builder::Col<AiTaskStage, Option::<__sdk::Timestamp>>, pub completed_at: __sdk::__query_builder::Col<AiTaskStage, Option<__sdk::Timestamp>>,
} }
impl __sdk::__query_builder::HasCols for AiTaskStage { impl __sdk::__query_builder::HasCols for AiTaskStage {
@@ -65,11 +58,13 @@ impl __sdk::__query_builder::HasCols for AiTaskStage {
stage_order: __sdk::__query_builder::Col::new(table_name, "stage_order"), stage_order: __sdk::__query_builder::Col::new(table_name, "stage_order"),
status: __sdk::__query_builder::Col::new(table_name, "status"), status: __sdk::__query_builder::Col::new(table_name, "status"),
text_output: __sdk::__query_builder::Col::new(table_name, "text_output"), text_output: __sdk::__query_builder::Col::new(table_name, "text_output"),
structured_payload_json: __sdk::__query_builder::Col::new(table_name, "structured_payload_json"), structured_payload_json: __sdk::__query_builder::Col::new(
table_name,
"structured_payload_json",
),
warning_messages: __sdk::__query_builder::Col::new(table_name, "warning_messages"), warning_messages: __sdk::__query_builder::Col::new(table_name, "warning_messages"),
started_at: __sdk::__query_builder::Col::new(table_name, "started_at"), started_at: __sdk::__query_builder::Col::new(table_name, "started_at"),
completed_at: __sdk::__query_builder::Col::new(table_name, "completed_at"), completed_at: __sdk::__query_builder::Col::new(table_name, "completed_at"),
} }
} }
} }
@@ -88,10 +83,8 @@ impl __sdk::__query_builder::HasIxCols for AiTaskStage {
AiTaskStageIxCols { AiTaskStageIxCols {
task_id: __sdk::__query_builder::IxCol::new(table_name, "task_id"), task_id: __sdk::__query_builder::IxCol::new(table_name, "task_id"),
task_stage_id: __sdk::__query_builder::IxCol::new(table_name, "task_stage_id"), task_stage_id: __sdk::__query_builder::IxCol::new(table_name, "task_stage_id"),
} }
} }
} }
impl __sdk::__query_builder::CanBeLookupTable for AiTaskStage {} impl __sdk::__query_builder::CanBeLookupTable for AiTaskStage {}

View File

@@ -2,13 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)] #[sats(crate = __lib)]
@@ -17,8 +11,6 @@ pub struct AiTaskStartInput {
pub started_at_micros: i64, pub started_at_micros: i64,
} }
impl __sdk::InModule for AiTaskStartInput { impl __sdk::InModule for AiTaskStartInput {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)] #[sats(crate = __lib)]
@@ -22,12 +17,8 @@ pub enum AiTaskStatus {
Failed, Failed,
Cancelled, Cancelled,
} }
impl __sdk::InModule for AiTaskStatus { impl __sdk::InModule for AiTaskStatus {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_kind_type::AiTaskKind; use super::ai_task_kind_type::AiTaskKind;
use super::ai_task_status_type::AiTaskStatus; use super::ai_task_status_type::AiTaskStatus;
@@ -20,25 +15,23 @@ pub struct AiTask {
pub owner_user_id: String, pub owner_user_id: String,
pub request_label: String, pub request_label: String,
pub source_module: String, pub source_module: String,
pub source_entity_id: Option::<String>, pub source_entity_id: Option<String>,
pub request_payload_json: Option::<String>, pub request_payload_json: Option<String>,
pub status: AiTaskStatus, pub status: AiTaskStatus,
pub failure_message: Option::<String>, pub failure_message: Option<String>,
pub latest_text_output: Option::<String>, pub latest_text_output: Option<String>,
pub latest_structured_payload_json: Option::<String>, pub latest_structured_payload_json: Option<String>,
pub version: u32, pub version: u32,
pub created_at: __sdk::Timestamp, pub created_at: __sdk::Timestamp,
pub started_at: Option::<__sdk::Timestamp>, pub started_at: Option<__sdk::Timestamp>,
pub completed_at: Option::<__sdk::Timestamp>, pub completed_at: Option<__sdk::Timestamp>,
pub updated_at: __sdk::Timestamp, pub updated_at: __sdk::Timestamp,
} }
impl __sdk::InModule for AiTask { impl __sdk::InModule for AiTask {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }
/// Column accessor struct for the table `AiTask`. /// Column accessor struct for the table `AiTask`.
/// ///
/// Provides typed access to columns for query building. /// Provides typed access to columns for query building.
@@ -48,16 +41,16 @@ pub struct AiTaskCols {
pub owner_user_id: __sdk::__query_builder::Col<AiTask, String>, pub owner_user_id: __sdk::__query_builder::Col<AiTask, String>,
pub request_label: __sdk::__query_builder::Col<AiTask, String>, pub request_label: __sdk::__query_builder::Col<AiTask, String>,
pub source_module: __sdk::__query_builder::Col<AiTask, String>, pub source_module: __sdk::__query_builder::Col<AiTask, String>,
pub source_entity_id: __sdk::__query_builder::Col<AiTask, Option::<String>>, pub source_entity_id: __sdk::__query_builder::Col<AiTask, Option<String>>,
pub request_payload_json: __sdk::__query_builder::Col<AiTask, Option::<String>>, pub request_payload_json: __sdk::__query_builder::Col<AiTask, Option<String>>,
pub status: __sdk::__query_builder::Col<AiTask, AiTaskStatus>, pub status: __sdk::__query_builder::Col<AiTask, AiTaskStatus>,
pub failure_message: __sdk::__query_builder::Col<AiTask, Option::<String>>, pub failure_message: __sdk::__query_builder::Col<AiTask, Option<String>>,
pub latest_text_output: __sdk::__query_builder::Col<AiTask, Option::<String>>, pub latest_text_output: __sdk::__query_builder::Col<AiTask, Option<String>>,
pub latest_structured_payload_json: __sdk::__query_builder::Col<AiTask, Option::<String>>, pub latest_structured_payload_json: __sdk::__query_builder::Col<AiTask, Option<String>>,
pub version: __sdk::__query_builder::Col<AiTask, u32>, pub version: __sdk::__query_builder::Col<AiTask, u32>,
pub created_at: __sdk::__query_builder::Col<AiTask, __sdk::Timestamp>, pub created_at: __sdk::__query_builder::Col<AiTask, __sdk::Timestamp>,
pub started_at: __sdk::__query_builder::Col<AiTask, Option::<__sdk::Timestamp>>, pub started_at: __sdk::__query_builder::Col<AiTask, Option<__sdk::Timestamp>>,
pub completed_at: __sdk::__query_builder::Col<AiTask, Option::<__sdk::Timestamp>>, pub completed_at: __sdk::__query_builder::Col<AiTask, Option<__sdk::Timestamp>>,
pub updated_at: __sdk::__query_builder::Col<AiTask, __sdk::Timestamp>, pub updated_at: __sdk::__query_builder::Col<AiTask, __sdk::Timestamp>,
} }
@@ -71,17 +64,22 @@ impl __sdk::__query_builder::HasCols for AiTask {
request_label: __sdk::__query_builder::Col::new(table_name, "request_label"), request_label: __sdk::__query_builder::Col::new(table_name, "request_label"),
source_module: __sdk::__query_builder::Col::new(table_name, "source_module"), source_module: __sdk::__query_builder::Col::new(table_name, "source_module"),
source_entity_id: __sdk::__query_builder::Col::new(table_name, "source_entity_id"), source_entity_id: __sdk::__query_builder::Col::new(table_name, "source_entity_id"),
request_payload_json: __sdk::__query_builder::Col::new(table_name, "request_payload_json"), request_payload_json: __sdk::__query_builder::Col::new(
table_name,
"request_payload_json",
),
status: __sdk::__query_builder::Col::new(table_name, "status"), status: __sdk::__query_builder::Col::new(table_name, "status"),
failure_message: __sdk::__query_builder::Col::new(table_name, "failure_message"), failure_message: __sdk::__query_builder::Col::new(table_name, "failure_message"),
latest_text_output: __sdk::__query_builder::Col::new(table_name, "latest_text_output"), latest_text_output: __sdk::__query_builder::Col::new(table_name, "latest_text_output"),
latest_structured_payload_json: __sdk::__query_builder::Col::new(table_name, "latest_structured_payload_json"), latest_structured_payload_json: __sdk::__query_builder::Col::new(
table_name,
"latest_structured_payload_json",
),
version: __sdk::__query_builder::Col::new(table_name, "version"), version: __sdk::__query_builder::Col::new(table_name, "version"),
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
started_at: __sdk::__query_builder::Col::new(table_name, "started_at"), started_at: __sdk::__query_builder::Col::new(table_name, "started_at"),
completed_at: __sdk::__query_builder::Col::new(table_name, "completed_at"), completed_at: __sdk::__query_builder::Col::new(table_name, "completed_at"),
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
} }
} }
} }
@@ -104,10 +102,8 @@ impl __sdk::__query_builder::HasIxCols for AiTask {
status: __sdk::__query_builder::IxCol::new(table_name, "status"), status: __sdk::__query_builder::IxCol::new(table_name, "status"),
task_id: __sdk::__query_builder::IxCol::new(table_name, "task_id"), task_id: __sdk::__query_builder::IxCol::new(table_name, "task_id"),
task_kind: __sdk::__query_builder::IxCol::new(table_name, "task_kind"), task_kind: __sdk::__query_builder::IxCol::new(table_name, "task_kind"),
} }
} }
} }
impl __sdk::__query_builder::CanBeLookupTable for AiTask {} impl __sdk::__query_builder::CanBeLookupTable for AiTask {}

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_stage_kind_type::AiTaskStageKind; use super::ai_task_stage_kind_type::AiTaskStageKind;
@@ -21,8 +16,6 @@ pub struct AiTextChunkAppendInput {
pub created_at_micros: i64, pub created_at_micros: i64,
} }
impl __sdk::InModule for AiTextChunkAppendInput { impl __sdk::InModule for AiTextChunkAppendInput {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_stage_kind_type::AiTaskStageKind; use super::ai_task_stage_kind_type::AiTaskStageKind;
@@ -22,8 +17,6 @@ pub struct AiTextChunkSnapshot {
pub created_at_micros: i64, pub created_at_micros: i64,
} }
impl __sdk::InModule for AiTextChunkSnapshot { impl __sdk::InModule for AiTextChunkSnapshot {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_stage_kind_type::AiTaskStageKind; use super::ai_task_stage_kind_type::AiTaskStageKind;
@@ -23,12 +18,10 @@ pub struct AiTextChunk {
pub created_at: __sdk::Timestamp, pub created_at: __sdk::Timestamp,
} }
impl __sdk::InModule for AiTextChunk { impl __sdk::InModule for AiTextChunk {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }
/// Column accessor struct for the table `AiTextChunk`. /// Column accessor struct for the table `AiTextChunk`.
/// ///
/// Provides typed access to columns for query building. /// Provides typed access to columns for query building.
@@ -53,7 +46,6 @@ impl __sdk::__query_builder::HasCols for AiTextChunk {
sequence: __sdk::__query_builder::Col::new(table_name, "sequence"), sequence: __sdk::__query_builder::Col::new(table_name, "sequence"),
delta_text: __sdk::__query_builder::Col::new(table_name, "delta_text"), delta_text: __sdk::__query_builder::Col::new(table_name, "delta_text"),
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
} }
} }
} }
@@ -72,10 +64,8 @@ impl __sdk::__query_builder::HasIxCols for AiTextChunk {
AiTextChunkIxCols { AiTextChunkIxCols {
task_id: __sdk::__query_builder::IxCol::new(table_name, "task_id"), task_id: __sdk::__query_builder::IxCol::new(table_name, "task_id"),
text_chunk_row_id: __sdk::__query_builder::IxCol::new(table_name, "text_chunk_row_id"), text_chunk_row_id: __sdk::__query_builder::IxCol::new(table_name, "text_chunk_row_id"),
} }
} }
} }
impl __sdk::__query_builder::CanBeLookupTable for AiTextChunk {} impl __sdk::__query_builder::CanBeLookupTable for AiTextChunk {}

View File

@@ -2,23 +2,17 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_text_chunk_append_input_type::AiTextChunkAppendInput;
use super::ai_task_procedure_result_type::AiTaskProcedureResult; use super::ai_task_procedure_result_type::AiTaskProcedureResult;
use super::ai_text_chunk_append_input_type::AiTextChunkAppendInput;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)] #[sats(crate = __lib)]
struct AppendAiTextChunkAndReturnArgs { struct AppendAiTextChunkAndReturnArgs {
pub input: AiTextChunkAppendInput, pub input: AiTextChunkAppendInput,
} }
impl __sdk::InModule for AppendAiTextChunkAndReturnArgs { impl __sdk::InModule for AppendAiTextChunkAndReturnArgs {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }
@@ -28,16 +22,19 @@ impl __sdk::InModule for AppendAiTextChunkAndReturnArgs {
/// ///
/// Implemented for [`super::RemoteProcedures`]. /// Implemented for [`super::RemoteProcedures`].
pub trait append_ai_text_chunk_and_return { pub trait append_ai_text_chunk_and_return {
fn append_ai_text_chunk_and_return(&self, input: AiTextChunkAppendInput, fn append_ai_text_chunk_and_return(&self, input: AiTextChunkAppendInput) {
) { self.append_ai_text_chunk_and_return_then(input, |_, _| {});
self.append_ai_text_chunk_and_return_then(input, |_, _| {});
} }
fn append_ai_text_chunk_and_return_then( fn append_ai_text_chunk_and_return_then(
&self, &self,
input: AiTextChunkAppendInput, input: AiTextChunkAppendInput,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<AiTaskProcedureResult, __sdk::InternalError>) + Send + 'static, __callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
); );
} }
@@ -46,13 +43,17 @@ impl append_ai_text_chunk_and_return for super::RemoteProcedures {
&self, &self,
input: AiTextChunkAppendInput, input: AiTextChunkAppendInput,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<AiTaskProcedureResult, __sdk::InternalError>) + Send + 'static, __callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) { ) {
self.imp.invoke_procedure_with_callback::<_, AiTaskProcedureResult>( self.imp
"append_ai_text_chunk_and_return", .invoke_procedure_with_callback::<_, AiTaskProcedureResult>(
AppendAiTextChunkAndReturnArgs { input, }, "append_ai_text_chunk_and_return",
__callback, AppendAiTextChunkAndReturnArgs { input },
); __callback,
);
} }
} }

View File

@@ -2,23 +2,17 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::chapter_progression_ledger_input_type::ChapterProgressionLedgerInput; use super::chapter_progression_ledger_input_type::ChapterProgressionLedgerInput;
use super::chapter_progression_procedure_result_type::ChapterProgressionProcedureResult; use super::chapter_progression_procedure_result_type::ChapterProgressionProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)] #[sats(crate = __lib)]
struct ApplyChapterProgressionLedgerEntryAndReturnArgs { struct ApplyChapterProgressionLedgerEntryAndReturnArgs {
pub input: ChapterProgressionLedgerInput, pub input: ChapterProgressionLedgerInput,
} }
impl __sdk::InModule for ApplyChapterProgressionLedgerEntryAndReturnArgs { impl __sdk::InModule for ApplyChapterProgressionLedgerEntryAndReturnArgs {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }
@@ -28,16 +22,22 @@ impl __sdk::InModule for ApplyChapterProgressionLedgerEntryAndReturnArgs {
/// ///
/// Implemented for [`super::RemoteProcedures`]. /// Implemented for [`super::RemoteProcedures`].
pub trait apply_chapter_progression_ledger_entry_and_return { pub trait apply_chapter_progression_ledger_entry_and_return {
fn apply_chapter_progression_ledger_entry_and_return(&self, input: ChapterProgressionLedgerInput, fn apply_chapter_progression_ledger_entry_and_return(
) { &self,
self.apply_chapter_progression_ledger_entry_and_return_then(input, |_, _| {}); input: ChapterProgressionLedgerInput,
) {
self.apply_chapter_progression_ledger_entry_and_return_then(input, |_, _| {});
} }
fn apply_chapter_progression_ledger_entry_and_return_then( fn apply_chapter_progression_ledger_entry_and_return_then(
&self, &self,
input: ChapterProgressionLedgerInput, input: ChapterProgressionLedgerInput,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<ChapterProgressionProcedureResult, __sdk::InternalError>) + Send + 'static, __callback: impl FnOnce(
&super::ProcedureEventContext,
Result<ChapterProgressionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
); );
} }
@@ -46,13 +46,17 @@ impl apply_chapter_progression_ledger_entry_and_return for super::RemoteProcedur
&self, &self,
input: ChapterProgressionLedgerInput, input: ChapterProgressionLedgerInput,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<ChapterProgressionProcedureResult, __sdk::InternalError>) + Send + 'static, __callback: impl FnOnce(
&super::ProcedureEventContext,
Result<ChapterProgressionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) { ) {
self.imp.invoke_procedure_with_callback::<_, ChapterProgressionProcedureResult>( self.imp
"apply_chapter_progression_ledger_entry_and_return", .invoke_procedure_with_callback::<_, ChapterProgressionProcedureResult>(
ApplyChapterProgressionLedgerEntryAndReturnArgs { input, }, "apply_chapter_progression_ledger_entry_and_return",
__callback, ApplyChapterProgressionLedgerEntryAndReturnArgs { input },
); __callback,
);
} }
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::chapter_progression_ledger_input_type::ChapterProgressionLedgerInput; use super::chapter_progression_ledger_input_type::ChapterProgressionLedgerInput;
@@ -19,10 +14,8 @@ pub(super) struct ApplyChapterProgressionLedgerEntryArgs {
impl From<ApplyChapterProgressionLedgerEntryArgs> for super::Reducer { impl From<ApplyChapterProgressionLedgerEntryArgs> for super::Reducer {
fn from(args: ApplyChapterProgressionLedgerEntryArgs) -> Self { fn from(args: ApplyChapterProgressionLedgerEntryArgs) -> Self {
Self::ApplyChapterProgressionLedgerEntry { Self::ApplyChapterProgressionLedgerEntry { input: args.input }
input: args.input, }
}
}
} }
impl __sdk::InModule for ApplyChapterProgressionLedgerEntryArgs { impl __sdk::InModule for ApplyChapterProgressionLedgerEntryArgs {
@@ -40,9 +33,11 @@ pub trait apply_chapter_progression_ledger_entry {
/// The reducer will run asynchronously in the future, /// The reducer will run asynchronously in the future,
/// and this method provides no way to listen for its completion status. /// and this method provides no way to listen for its completion status.
/// /// Use [`apply_chapter_progression_ledger_entry:apply_chapter_progression_ledger_entry_then`] to run a callback after the reducer completes. /// /// Use [`apply_chapter_progression_ledger_entry:apply_chapter_progression_ledger_entry_then`] to run a callback after the reducer completes.
fn apply_chapter_progression_ledger_entry(&self, input: ChapterProgressionLedgerInput, fn apply_chapter_progression_ledger_entry(
) -> __sdk::Result<()> { &self,
self.apply_chapter_progression_ledger_entry_then(input, |_, _| {}) input: ChapterProgressionLedgerInput,
) -> __sdk::Result<()> {
self.apply_chapter_progression_ledger_entry_then(input, |_, _| {})
} }
/// Request that the remote module invoke the reducer `apply_chapter_progression_ledger_entry` to run as soon as possible, /// Request that the remote module invoke the reducer `apply_chapter_progression_ledger_entry` to run as soon as possible,
@@ -55,9 +50,11 @@ pub trait apply_chapter_progression_ledger_entry {
&self, &self,
input: ChapterProgressionLedgerInput, input: ChapterProgressionLedgerInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>) callback: impl FnOnce(
+ Send &super::ReducerEventContext,
+ 'static, Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()>; ) -> __sdk::Result<()>;
} }
@@ -66,11 +63,15 @@ impl apply_chapter_progression_ledger_entry for super::RemoteReducers {
&self, &self,
input: ChapterProgressionLedgerInput, input: ChapterProgressionLedgerInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>) callback: impl FnOnce(
+ Send &super::ReducerEventContext,
+ 'static, Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()> { ) -> __sdk::Result<()> {
self.imp.invoke_reducer_with_callback(ApplyChapterProgressionLedgerEntryArgs { input, }, callback) self.imp.invoke_reducer_with_callback(
ApplyChapterProgressionLedgerEntryArgs { input },
callback,
)
} }
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::inventory_mutation_input_type::InventoryMutationInput; use super::inventory_mutation_input_type::InventoryMutationInput;
@@ -19,10 +14,8 @@ pub(super) struct ApplyInventoryMutationArgs {
impl From<ApplyInventoryMutationArgs> for super::Reducer { impl From<ApplyInventoryMutationArgs> for super::Reducer {
fn from(args: ApplyInventoryMutationArgs) -> Self { fn from(args: ApplyInventoryMutationArgs) -> Self {
Self::ApplyInventoryMutation { Self::ApplyInventoryMutation { input: args.input }
input: args.input, }
}
}
} }
impl __sdk::InModule for ApplyInventoryMutationArgs { impl __sdk::InModule for ApplyInventoryMutationArgs {
@@ -40,9 +33,8 @@ pub trait apply_inventory_mutation {
/// The reducer will run asynchronously in the future, /// The reducer will run asynchronously in the future,
/// and this method provides no way to listen for its completion status. /// and this method provides no way to listen for its completion status.
/// /// Use [`apply_inventory_mutation:apply_inventory_mutation_then`] to run a callback after the reducer completes. /// /// Use [`apply_inventory_mutation:apply_inventory_mutation_then`] to run a callback after the reducer completes.
fn apply_inventory_mutation(&self, input: InventoryMutationInput, fn apply_inventory_mutation(&self, input: InventoryMutationInput) -> __sdk::Result<()> {
) -> __sdk::Result<()> { self.apply_inventory_mutation_then(input, |_, _| {})
self.apply_inventory_mutation_then(input, |_, _| {})
} }
/// Request that the remote module invoke the reducer `apply_inventory_mutation` to run as soon as possible, /// Request that the remote module invoke the reducer `apply_inventory_mutation` to run as soon as possible,
@@ -55,9 +47,11 @@ pub trait apply_inventory_mutation {
&self, &self,
input: InventoryMutationInput, input: InventoryMutationInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>) callback: impl FnOnce(
+ Send &super::ReducerEventContext,
+ 'static, Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()>; ) -> __sdk::Result<()>;
} }
@@ -66,11 +60,13 @@ impl apply_inventory_mutation for super::RemoteReducers {
&self, &self,
input: InventoryMutationInput, input: InventoryMutationInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>) callback: impl FnOnce(
+ Send &super::ReducerEventContext,
+ 'static, Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()> { ) -> __sdk::Result<()> {
self.imp.invoke_reducer_with_callback(ApplyInventoryMutationArgs { input, }, callback) self.imp
.invoke_reducer_with_callback(ApplyInventoryMutationArgs { input }, callback)
} }
} }

View File

@@ -2,12 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
use super::quest_signal_apply_input_type::QuestSignalApplyInput; use super::quest_signal_apply_input_type::QuestSignalApplyInput;
@@ -19,10 +14,8 @@ pub(super) struct ApplyQuestSignalArgs {
impl From<ApplyQuestSignalArgs> for super::Reducer { impl From<ApplyQuestSignalArgs> for super::Reducer {
fn from(args: ApplyQuestSignalArgs) -> Self { fn from(args: ApplyQuestSignalArgs) -> Self {
Self::ApplyQuestSignal { Self::ApplyQuestSignal { input: args.input }
input: args.input, }
}
}
} }
impl __sdk::InModule for ApplyQuestSignalArgs { impl __sdk::InModule for ApplyQuestSignalArgs {
@@ -40,9 +33,8 @@ pub trait apply_quest_signal {
/// The reducer will run asynchronously in the future, /// The reducer will run asynchronously in the future,
/// and this method provides no way to listen for its completion status. /// and this method provides no way to listen for its completion status.
/// /// Use [`apply_quest_signal:apply_quest_signal_then`] to run a callback after the reducer completes. /// /// Use [`apply_quest_signal:apply_quest_signal_then`] to run a callback after the reducer completes.
fn apply_quest_signal(&self, input: QuestSignalApplyInput, fn apply_quest_signal(&self, input: QuestSignalApplyInput) -> __sdk::Result<()> {
) -> __sdk::Result<()> { self.apply_quest_signal_then(input, |_, _| {})
self.apply_quest_signal_then(input, |_, _| {})
} }
/// Request that the remote module invoke the reducer `apply_quest_signal` to run as soon as possible, /// Request that the remote module invoke the reducer `apply_quest_signal` to run as soon as possible,
@@ -55,9 +47,11 @@ pub trait apply_quest_signal {
&self, &self,
input: QuestSignalApplyInput, input: QuestSignalApplyInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>) callback: impl FnOnce(
+ Send &super::ReducerEventContext,
+ 'static, Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()>; ) -> __sdk::Result<()>;
} }
@@ -66,11 +60,13 @@ impl apply_quest_signal for super::RemoteReducers {
&self, &self,
input: QuestSignalApplyInput, input: QuestSignalApplyInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>) callback: impl FnOnce(
+ Send &super::ReducerEventContext,
+ 'static, Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()> { ) -> __sdk::Result<()> {
self.imp.invoke_reducer_with_callback(ApplyQuestSignalArgs { input, }, callback) self.imp
.invoke_reducer_with_callback(ApplyQuestSignalArgs { input }, callback)
} }
} }

View File

@@ -2,13 +2,7 @@
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)] #[sats(crate = __lib)]
@@ -19,13 +13,11 @@ pub struct AssetEntityBindingInput {
pub entity_id: String, pub entity_id: String,
pub slot: String, pub slot: String,
pub asset_kind: String, pub asset_kind: String,
pub owner_user_id: Option::<String>, pub owner_user_id: Option<String>,
pub profile_id: Option::<String>, pub profile_id: Option<String>,
pub updated_at_micros: i64, pub updated_at_micros: i64,
} }
impl __sdk::InModule for AssetEntityBindingInput { impl __sdk::InModule for AssetEntityBindingInput {
type Module = super::RemoteModule; type Module = super::RemoteModule;
} }

Some files were not shown because too many files have changed in this diff Show More