14 Commits

Author SHA1 Message Date
5831703156 1
Some checks failed
CI / verify (push) Has been cancelled
2026-05-02 20:43:41 +08:00
543ccf2509 Merge branch 'master' into codex/ddd 2026-05-02 20:43:35 +08:00
4716d9b582 Refine AGENTS backend constraints with DDD docs 2026-05-02 15:30:51 +08:00
kdletters
9b5aa25fe9 fix: repair api server merge fallout 2026-05-02 14:18:12 +08:00
kdletters
8f4ca9abfa Merge remote-tracking branch 'origin/master' into codex/ddd
# Conflicts:
#	docs/technical/README.md
#	docs/technical/RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md
#	docs/technical/SPACETIMEDB_TABLE_CATALOG.md
#	scripts/generate-spacetime-bindings.mjs
#	server-rs/crates/api-server/src/app.rs
#	server-rs/crates/api-server/src/assets.rs
#	server-rs/crates/api-server/src/big_fish.rs
#	server-rs/crates/api-server/src/custom_world_ai.rs
#	server-rs/crates/api-server/src/llm.rs
#	server-rs/crates/api-server/src/main.rs
#	server-rs/crates/api-server/src/puzzle.rs
#	server-rs/crates/api-server/src/runtime_profile.rs
#	server-rs/crates/api-server/src/runtime_story/compat/ai.rs
#	server-rs/crates/api-server/src/runtime_story/compat/npc_actions.rs
#	server-rs/crates/api-server/src/runtime_story/compat/presentation.rs
#	server-rs/crates/api-server/src/runtime_story/compat/tests.rs
#	server-rs/crates/api-server/src/state.rs
#	server-rs/crates/module-auth/src/lib.rs
#	server-rs/crates/module-big-fish/src/lib.rs
#	server-rs/crates/module-custom-world/src/lib.rs
#	server-rs/crates/module-puzzle/src/lib.rs
#	server-rs/crates/module-runtime/src/lib.rs
#	server-rs/crates/spacetime-client/src/big_fish.rs
#	server-rs/crates/spacetime-client/src/lib.rs
#	server-rs/crates/spacetime-client/src/mapper.rs
#	server-rs/crates/spacetime-client/src/module_bindings/admin_disable_profile_redeem_code_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_redeem_code_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/advance_puzzle_next_level_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/append_ai_text_chunk_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/apply_chapter_progression_ledger_entry_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/attach_ai_result_reference_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/authorize_database_migration_operator_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/begin_story_session_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_run_type.rs
#	server-rs/crates/spacetime-client/src/module_bindings/bind_asset_object_to_entity_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/cancel_ai_task_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/clear_platform_browse_history_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/compile_big_fish_draft_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/compile_custom_world_published_profile_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/compile_puzzle_agent_draft_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/complete_ai_stage_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/complete_ai_task_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/confirm_asset_object_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/consume_profile_wallet_points_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/continue_story_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/create_ai_task_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/create_battle_state_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/create_big_fish_session_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/create_custom_world_agent_session_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/create_profile_recharge_order_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/create_puzzle_agent_session_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/delete_big_fish_work_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/delete_custom_world_agent_session_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/delete_custom_world_profile_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/delete_puzzle_work_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/delete_runtime_snapshot_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/drag_puzzle_piece_or_group_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/execute_custom_world_agent_action_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/export_auth_store_snapshot_from_tables_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/export_database_migration_to_file_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/fail_ai_task_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/finalize_big_fish_agent_message_turn_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/finalize_custom_world_agent_message_turn_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/finalize_puzzle_agent_message_turn_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/generate_big_fish_asset_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_auth_store_snapshot_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_battle_state_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_big_fish_session_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_chapter_progression_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_agent_card_detail_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_agent_operation_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_agent_session_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_gallery_detail_by_code_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_gallery_detail_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_library_detail_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_player_progression_or_default_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_profile_dashboard_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_profile_play_stats_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_profile_recharge_center_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_profile_referral_invite_center_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_agent_session_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_gallery_detail_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_run_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_work_detail_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_runtime_inventory_state_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_runtime_setting_or_default_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_runtime_snapshot_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_story_session_state_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/grant_player_progression_experience_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/import_auth_store_snapshot_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/import_database_migration_from_file_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/import_database_migration_incremental_from_file_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_asset_history_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_big_fish_works_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_custom_world_gallery_entries_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_custom_world_profiles_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_custom_world_works_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_platform_browse_history_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_profile_save_archives_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_profile_wallet_ledger_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_puzzle_gallery_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_puzzle_works_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/mod.rs
#	server-rs/crates/spacetime-client/src/module_bindings/publish_big_fish_game_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/publish_custom_world_profile_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/publish_custom_world_world_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/publish_puzzle_work_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/record_big_fish_play_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/redeem_profile_referral_invite_code_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/redeem_profile_reward_code_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/refund_profile_wallet_points_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/resolve_combat_action_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/resolve_npc_battle_interaction_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/resolve_npc_interaction_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/resolve_npc_social_action_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/resolve_treasure_interaction_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/resume_profile_save_archive_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/revoke_database_migration_operator_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/save_puzzle_generated_images_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/select_puzzle_cover_image_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/start_puzzle_run_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/submit_big_fish_message_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/submit_custom_world_agent_message_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/submit_puzzle_agent_message_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/submit_puzzle_leaderboard_entry_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/swap_puzzle_pieces_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/unpublish_custom_world_profile_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/update_puzzle_work_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/upsert_auth_store_snapshot_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/upsert_chapter_progression_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/upsert_custom_world_agent_operation_progress_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/upsert_custom_world_profile_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/upsert_npc_state_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/upsert_platform_browse_history_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/upsert_runtime_setting_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/upsert_runtime_snapshot_and_return_procedure.rs
#	server-rs/crates/spacetime-module/src/auth/procedures.rs
#	server-rs/crates/spacetime-module/src/custom_world/mod.rs
#	server-rs/crates/spacetime-module/src/lib.rs
#	server-rs/crates/spacetime-module/src/migration.rs
#	server-rs/crates/spacetime-module/src/puzzle.rs
#	server-rs/crates/spacetime-module/src/runtime/profile.rs
#	src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
#	src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx
#	src/services/aiService.ts
#	src/services/puzzle-runtime/puzzleRuntimeClient.ts
2026-05-02 03:35:59 +08:00
kdletters
9d9913095d Close DDD refactor and remove generated asset proxy 2026-05-02 00:27:22 +08:00
fd08262bf0 Close DDD cleanup and tests-support closure 2026-04-30 16:15:05 +08:00
7ab0933f6d Integrate unfinished server-rs refactor worklists 2026-04-30 13:39:06 +08:00
62934b0809 推进 SpacetimeDB adapter 与 client 收口 2026-04-29 16:53:54 +08:00
Codex
f82775b852 推进 server-rs DDD 分层与新接口接线 2026-04-29 15:46:34 +08:00
Codex
9d3fcfae77 docs: add spacetimedb schema change constraints 2026-04-29 15:36:46 +08:00
Codex
aa2e9b36d7 完善 server-rs DDD 重构计划与骨架 2026-04-29 11:51:30 +08:00
Codex
39200ea9cc Merge branch 'master' into codex/ddd 2026-04-29 11:05:05 +08:00
90a19aeb0d 添加迁移计划文档 2026-04-28 17:53:54 +08:00
941 changed files with 46780 additions and 47851 deletions

View File

@@ -1,7 +1,6 @@
# AGENTS.md
## 项目约束
- 在修改server-rs的内容时不要去兼容server-node中的任何内容只允许参考以及把server-node中未迁移到server-rs的内容迁移过来
- 代码需要有完善的中文注释
- 在落地工程修改前检查是否有详细指导本次落地的文档,若没有文档或文档的完善程度仍有落地过程中编码级别的歧义优先优化文档后落地工程迭代。
- 对工程的修改不仅要落地到代码更面还要更改对应文档若没有生成新的文档文档统一存在doc目录中
@@ -14,13 +13,20 @@
- UI设计需要兼顾网页端、移动端双端的使用体验确保在不同设备上都能正常显示和操作移动端优先考虑。
- 不要在gitignore中添加.env.local文件。
- 严格遵循简洁的代码风格
- 前端只负责做表现所有的逻辑、数据都放到后端工程后端使用server-rs中用Rust+spacetimeDB的方案实现禁止继续使用server-nodeExpress和postgreSQL
- 后端采用多crate设计
- 请默认保持系统的简洁性,能复用、修改、扩展现有系统、页面就不新建新系统新页面。
- 禁止将功能说明描述类的文本默认写入UI界面中。
- prd文档中每个模块的描述要落地设计到可以精准编码到位不能出现需求落地漂移。
- 点击按钮弹出独立的面板的设计不要实现成在当前面板下面显示内容。
- 每个阶段任务完成后自动压缩上下文,确保后续阶段在清晰、低噪音的上下文基础上继续推进。
## 后端技术约束
- 后端最新技术约束以 [`docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md`](docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md) 为总纲;执行和收口状态以 [`docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`](docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md) 为准。
- 契约、路由、DTO 去留和 breaking change 以 [`docs/technical/SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md`](docs/technical/SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md) 为准;不得在前端、`api-server` 或临时兼容层中重新发明旧接口。
- SpacetimeDB 表结构、自动迁移限制和冲突处理以 [`docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`](docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md) 为准;涉及 table、reducer、procedure、row shape 或绑定变化时,必须同步 `migration.rs`、表目录和生成绑定。
- 后端路线固定为 `server-rs + Axum + SpacetimeDB`。旧 `server-node`、Express、PostgreSQL 不再作为兼容目标;历史实现只能作为迁移参考,若旧文档与 DDD 约束冲突,先修正文档和方案再编码。
- DDD 分层边界按总纲执行:领域规则沉到 `module-*`SpacetimeDB 表和事务编排留在 `spacetime-module`,后端访问 SpacetimeDB 统一经 `spacetime-client` facadeHTTP/SSE/BFF 留在 `api-server`,外部副作用留在 `platform-*`,前后端 DTO 留在 `shared-contracts`
- 前端只做表现、交互和临时 UI 状态,不承接正式业务真相,不绕过后端投影或后端 API 直接实现业务规则。
- 修改后端代码后,按对应 DDD 文档中的验收命令执行测试;涉及 API smoke 时使用 `npm run api-server` 重新拉起后端并执行相应自动测试,同时确认 `/healthz`
- 凡是涉及 SpacetimeDB 的设计、实现、脚本、调试、前端绑定接入,统一显式使用以下 skill 作为执行依据:
- [$spacetimedb-cli](.codex\\skills\\spacetimedb-cli\\SKILL.md)
- [$spacetimedb-rust](.codex\\skills\\spacetimedb-rust\\SKILL.md)

77
PLAN.md Normal file
View File

@@ -0,0 +1,77 @@
# server-rs DDD 一次性重构方案
## Summary
当前仓库已不再存在 `server-node`,本次只针对现有 `server-rs` 做一次性 DDD 化。
目标是把 Rust + SpacetimeDB 后端统一成清晰边界:领域规则在 `module-*`,事务和持久化在 `spacetime-module`HTTP/BFF 在 `api-server`,外部能力在 `platform-*`,共享值处理和 DTO 分别在 `shared-kernel` / `shared-contracts`
全局并行执行任务清单见 `docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`。runtime story 去兼容层属于该清单中的 `WP-RS` 工作包,不再单独维护专项清单。
## Target Architecture
- `module-*`:领域模型、值对象、聚合方法、领域服务、命令、领域错误、领域事件、纯应用编排结果;禁止直接依赖 Axum、reqwest、OSS、LLM、文件系统、SpacetimeDB table 操作。
- `spacetime-module`SpacetimeDB adapter只保留 table、reducer、procedure、row/snapshot mapper、事务内查询写回、event table核心规则必须调用 `module-*`
- `api-server`HTTP/BFF adapter只保留路由、鉴权上下文、请求响应映射、SSE、SpacetimeDB client 调用、平台服务调用。
- `platform-*`JWT/SMS/微信/OSS/LLM/HTTP client 等外部能力实现。
- `shared-kernel`:跨领域纯值处理;`shared-contracts`HTTP/前端契约 DTO。
## Required Refactor
- 为所有业务上下文统一目录:
- `domain.rs``domain/*`:聚合、值对象、领域方法。
- `commands.rs`:写入用例输入。
- `application.rs`:用例处理函数。
- `events.rs`:领域事件与跨上下文事件。
- `errors.rs`:领域错误。
- `mapper.rs` 仅允许出现在 adapter crate。
- 一次性处理混合边界:
- `module-auth` 拆出认证、会话、验证码、微信绑定领域;内存 store / 文件持久化移出领域核心。
- `module-assets` 拆出资产对象确认规则OSS head、reqwest、fallback store 移出领域核心。
- `spacetime-module` 全量拆分 table、reducer/procedure、mapper、跨上下文事务编排。
- `api-server` 中 handler 只保留 transport 逻辑,业务分支迁移到领域或应用层。
- `runtime_story``custom_world``puzzle``big_fish``inventory``quest``npc``combat``progression` 全部对齐同一结构。
- 去兼容层任务边界:
- `module-runtime-story-compat` 不作为目标架构保留,迁移为无 `compat` 命名的 `module-runtime-story` 或拆入对应领域模块。
- `api-server/src/runtime_story/compat*` 只允许作为待删除历史入口,不再新增兼容分支。
- 前端 runtime story / chat client 统一改到 `POST /api/runtime/story/sessions/:sessionId/...` 新接口族。
- 旧请求体里的 `worldType / character / monsters / history / context` 不再作为正式主链输入。
- 表结构硬约束:
- 默认保持现有 SpacetimeDB 主表兼容。
- 表结构变更采用最小必要原则。
- 只有为修正聚合边界、读写分离、事件化、查询索引或生命周期独立性不可避免时,才新增或调整表。
- 优先新增 optional 字段、投影表、事件表,不做破坏性 rename/delete/type change。
- 任何 table 变更必须同步 `migration.rs`、SpacetimeDB 表目录、相关 reducer/procedure 测试。
- 统一跨上下文协作:
- 单聚合内部变化由聚合方法完成。
- 跨聚合流程由应用服务或 SpacetimeDB 事务 adapter 编排。
- 战斗奖励、任务奖励、成长记账、画廊投影、agent 操作进度等副作用必须显式表达为事件或应用结果。
- 统一查询策略:
- 写模型不复用给复杂查询。
- 每个前端场景有独立 query/result DTO。
- SpacetimeDB private table 默认不暴露public table 只服务明确订阅读模型。
## Documentation
- 新增 `docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md`,写清 DDD 规则、依赖方向、crate 职责矩阵、每个上下文的聚合/命令/事件/读模型、SpacetimeDB adapter 映射、表结构变更约束。
- 更新现有后端基线、SpacetimeDB 表目录、API 路由索引、相关模块技术文档。
- 表结构或 reducer/procedure 变化同步 `migration.rs`
## Acceptance Criteria
- `server-rs` 所有业务模块通过统一 DDD 目录和依赖边界检查。
- `spacetime-module/src/lib.rs` 不再承载大段业务流程,拆到上下文 adapter。
- 默认不破坏现有 SpacetimeDB 主表确需改表时有文档、migration 和测试。
- 所有领域规则都有纯 Rust 单元测试。
- 所有 reducer/procedure 有事务适配测试或最小 smoke。
- HTTP contract shape 不发生未记录 breaking change。
- 执行并通过:
- `cargo test --workspace --manifest-path server-rs/Cargo.toml`
- `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`
- `npm run api-server:maincloud`
- 仓库编码检查

View File

@@ -15,6 +15,8 @@
SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md)private 表迁移 JSON 导入导出、HTTP 413 分片导入和 Jenkins 数据库迁移流水线见 [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md) 与 [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md);后台管理独立前端工程技术方案见 [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./technical/ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md)。
SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md)。
## 推荐阅读顺序
1. 先看 [经验沉淀](./experience/README.md),快速建立这个项目的开发共识。

View File

@@ -0,0 +1,66 @@
# api-server 合并后编译修复记录
日期:`2026-05-02`
## 背景
`codex/ddd` 合入 `master` 后,`api-server` 编译失败。问题集中在合并后的跨 crate 契约缺口:`api-server` 已引用新接口或新字段,但对应的领域 crate 与 HTTP 转接层没有同步补齐。
## 修复范围
1. `module-auth` 补齐个人资料更新契约:
- 新增 `UpdateProfileInput``UpdateProfileResult`
- `AuthUser` 增加 `avatar_url``created_at`,并通过 `serde(default)` 兼容旧认证快照。
- `PasswordEntryService::update_profile` 统一校验昵称与头像 data URL并写回认证快照。
2. 微信绑定手机号结果补齐 `activated_new_user`
- 待绑定微信账号绑定新手机号时返回 `true`,用于注册奖励发放。
- 待绑定微信账号合并到已有手机号账号时返回 `false`
3. 拼图运行态补齐 HTTP 转接:
- `POST /api/runtime/puzzle/runs/{run_id}/drag` 读取 `DragPuzzlePieceRequest`
- 转发到 SpacetimeDB client 的 `drag_puzzle_piece_or_group` procedure 包装。
4. runtime story 聊天接口改用当前 shared contract
-`runtime_story::RuntimeStorySnapshotPayload` 已删除。
- `api-server` 侧临时别名到 `story::StoryRuntimeSnapshotPayload`,保持现有请求结构不漂移。
5. `api-server` 全量测试修复:
- `custom_world_foundation_draft` 的 mock LLM 响应仍是旧 Chat Completions 结构。
- 当前 `LlmClient` 默认走 Responses API测试 mock 已改为 `output[].content[].text` 结构。
6. 前端全量测试期望补齐:
- 自定义世界结果页的第二幕场景预览断言改为校验第二幕生成图。
- 拼图下一关交互测试保留后端下一关调用断言,并明确只调用一次。
- 拼图正式 run 客户端补回 `/drag` 调用包装,测试 mock 同步走正式 run 的 `swap/drag` 服务路径。
7. 前端门禁合并缺口修复:
- 拼图测试运行前更新作品时同步提交 `levels`,对齐当前 `updatePuzzleWork` 契约。
- 大鱼和 Match3D 测试 mock 对齐当前共享契约,避免 typecheck 阻塞。
- 移除 Vite dev proxy 中重复的 `/api/creation` key避免 build gate 将 warning 视为失败。
## 验证
本次修复应至少通过:
```powershell
cargo check -p module-auth --manifest-path server-rs\Cargo.toml
cargo check -p api-server --manifest-path server-rs\Cargo.toml
cargo test -p module-auth --manifest-path server-rs\Cargo.toml
cargo test -p api-server --manifest-path server-rs\Cargo.toml
npm run check:encoding
npm test
npm run typecheck
npm run build
npm run check:content
```
后端代码变更后,按项目约束还需要用 `npm run api-server:maincloud` 做一次启动验证。
本轮最终结果:
- `cargo test -p module-auth --manifest-path server-rs\Cargo.toml` 已通过,结果为 `17 passed; 0 failed`
- `cargo test -p api-server --manifest-path server-rs\Cargo.toml` 已通过,结果为 `237 passed; 0 failed; 4 ignored`
- `cargo test --manifest-path server-rs\Cargo.toml` 已通过,结果同 `api-server` 默认测试。
- `npm test` 已通过,结果为 `160 passed` 个测试文件、`704 passed` 个用例。
- `npm run typecheck``npm run build``npm run check:content``npm run check:encoding``git diff --check` 已通过。
- `npm run api-server:maincloud` 已完成启动烟测,`/healthz` 返回 `200`;期间 Maincloud 订阅恢复出现 `503` warning但未阻止服务启动。
仍需单独处理的非本轮阻塞:
- `cargo test --workspace --manifest-path server-rs\Cargo.toml` 在 Windows 原生测试链接 SpacetimeDB module crate 时失败,缺失 `bytes_sink_write``console_log``table_id_from_name``identity``datastore_table_scan_bsatn` 等 SpacetimeDB 宿主符号;这是 module crate 原生 Windows test 链接环境问题。
- `npm run check` 当前仍会停在全仓 `lint:eslint`,涉及大量既有 import 排序、未使用符号和 hook dependency lint debt本轮触碰文件已清掉 lint error`PlatformEntryFlowShellImpl.tsx` 保留既有 hook dependency warnings。

View File

@@ -31,3 +31,12 @@
- 本地与远端部署:`RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md``JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md`
如果旧文档与本基线冲突,以本基线和更新日期更近的 Rust / SpacetimeDB 文档为准。
## 4. DDD 重构总纲补充
`2026-04-28` 起,`server-rs` 后续后端改动还必须同时遵循 [SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md](./SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md)
- `module-*` 只承载领域模型、命令、应用编排结果、领域事件和领域错误。
- `spacetime-module` 只承载 SpacetimeDB 表、reducer、procedure、事务 adapter 与 mapper。
- `api-server` 只承载 HTTP / SSE / BFF adapter 和外部平台服务编排。
- 任何表结构变化仍必须同步 `migration.rs` 与 [SPACETIMEDB_TABLE_CATALOG.md](./SPACETIMEDB_TABLE_CATALOG.md)。

View File

@@ -277,56 +277,56 @@ Rust DTO 只承载 HTTP contract 和跨 crate 稳定模型,不直接暴露 `mo
## 6.1 创作链
1. `create_match3d_agent_session(input)`
1. `create_match3d_agent_session(input)`
创建会话,写入初始配置或空配置,返回 session snapshot。
2. `get_match3d_agent_session(input)`
2. `get_match3d_agent_session(input)`
获取会话、消息和当前 draft。
3. `submit_match3d_agent_message(input)`
3. `submit_match3d_agent_message(input)`
只写 user message不调用 LLM不生成 assistant 回复。
4. `finalize_match3d_agent_message_turn(input)`
4. `finalize_match3d_agent_message_turn(input)`
`api-server` LLM turn 完成后写入 assistant message、配置状态、进度和 `last_assistant_reply`
5. `compile_match3d_draft(input)`
5. `compile_match3d_draft(input)`
校验题材、需要消除次数、难度,生成草稿和作品 draft profile。
## 6.2 作品链
1. `update_match3d_work(input)`
1. `update_match3d_work(input)`
更新游戏名称、标签、封面、题材、需要消除次数和难度。
2. `publish_match3d_work(input)`
2. `publish_match3d_work(input)`
校验基础信息完整后发布作品,不要求试玩通关。
3. `list_match3d_works(input)`
3. `list_match3d_works(input)`
查询当前用户作品。
4. `get_match3d_work_detail(input)`
4. `get_match3d_work_detail(input)`
查询作品详情,支持结果页恢复和作品详情页。
5. `delete_match3d_work(input)`
5. `delete_match3d_work(input)`
可后置;若接入创作中心删除,需要与其他玩法卡片删除语义一致。
## 6.3 运行态链
1. `start_match3d_run(input)`
1. `start_match3d_run(input)`
基于作品配置生成单局快照,返回 `Match3DRunSnapshot`
2. `get_match3d_run(input)`
2. `get_match3d_run(input)`
返回当前权威运行态快照。
3. `click_match3d_item(input)`
3. `click_match3d_item(input)`
根据 `run_id / item_instance_id / client_snapshot_version` 权威确认点击、入槽、三消、失败或胜利,返回新快照和确认结果。
4. `stop_match3d_run(input)`
4. `stop_match3d_run(input)`
把运行态标记为 `Stopped`,供试玩中止和返回结果页使用。
5. `restart_match3d_run(input)`
5. `restart_match3d_run(input)`
复用同一作品配置创建新 run返回新快照。
6. `finish_match3d_time_up(input)`
6. `finish_match3d_time_up(input)`
可选。若倒计时由前端触发,前端在倒计时归零时调用该 procedure后端确认 `TimeUp`。也可以由 `click_match3d_item``get_match3d_run` 懒确认超时。
## 6.4 procedure 输入输出约束

View File

@@ -0,0 +1,37 @@
# Profile 主链 Vite 代理修复
## 1. 问题
“我的”和“存档”页面在本地开发环境报:
```text
Unexpected token '<', "<!doctype "... is not valid JSON
```
这不是后端返回了坏 JSON而是前端请求 `/api/profile/*` 时没有命中 Vite 代理Vite 将请求按 SPA fallback 返回了 `index.html``requestJson` 随后对 HTML 执行 `JSON.parse`,首字符 `<` 触发该错误。
## 2. 现有约束
DDD 路由矩阵已冻结 profile 主链:
1. “我的”与存档读取统一走 `/api/profile/*`
2.`/api/runtime/profile/*` 已取消挂载,不允许前端回退到旧路径。
3. 后端 `api-server` 已挂载 `/api/profile/dashboard``/api/profile/save-archives` 等路由,问题只在本地 Vite 代理层。
## 3. 修复
`vite.config.ts` 在现有 `/api/auth``/api/runtime` 等代理旁补齐:
```ts
'/api/profile': {
target: runtimeServerTarget,
changeOrigin: true,
secure: false,
},
```
这样 profile 主链请求在 `npm run dev:web` 下会直接转发到 Rust API server不再落到前端入口页。
## 4. 回归
新增 `src/config/viteProxyConfig.test.ts`,断言 Vite server proxy 必须包含 `/api/profile`。后续若再调整 profile route 或代理配置,先更新本文和测试,再改工程实现。

View File

@@ -31,4 +31,3 @@
7. `server-rs/crates/shared-contracts/src/runtime.rs`
8. `packages/shared/src/contracts/runtime.ts`
9. `src/components/rpg-entry/RpgEntryHomeView.tsx`

View File

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

View File

@@ -13,7 +13,7 @@
1. 通关后默认点击“下一关”,优先加载当前拼图作品的下一关。
2. 当前作品没有下一关时,后端按标签语义相似度选出相似度最高的三个已发布作品。
3. 用户在通关弹窗里点击候选作品后,进入该作品并从第 `1` 关重新开始
3. 用户在通关弹窗里点击候选作品后,切换到候选作品的第一张图,但运行时关卡序号、切割规格和倒计时继续按当前 run 累进
4. 移动端优先,候选卡片要紧凑,不写玩法说明类文案。
## 数据契约
@@ -51,7 +51,8 @@
- 返回最高的 3 个候选
4. `advance_puzzle_next_level`
- `nextLevelMode = sameWork` 时加载当前作品的下一关,并继续当前 run。
- `nextLevelMode = similarWorks` 时默认加载候选第一项,并把 `entryProfileId / clearedLevelCount / currentLevelIndex` 重置到目标作品第 `1`
- `nextLevelMode = similarWorks` 时默认加载候选第一项的第一张图;正式 UI 点击具体候选作品时通过 `targetProfileId` 指定候选
- 任何跨作品进入都只切换图片来源,不重置 `entryProfileId / clearedLevelCount / currentLevelIndex`,并按当前 run 的下一关配置切割规格和倒计时。
5. `local-next-level` 兼容接口同样优先找同作品下一关;没有时返回 `similarWorks` 候选并保持当前通关 run只有候选池为空时才进入旧草稿兜底。
## 前端规则
@@ -70,6 +71,6 @@
1. 当前作品有下一关时,点击“下一关”进入当前作品下一关。
2. 当前作品没有下一关时,通关弹窗显示最多 3 个相似作品。
3. 点击相似作品后进入该作品第 `1`HUD 关卡序号、切割规格和倒计时都按第 `1` 关显示。
3. 点击相似作品后进入该作品第一张图HUD 关卡序号、切割规格和倒计时继续按运行时下一关显示。
4.`recommendedNextProfileId` 为空时,只要 `nextLevelMode = sameWork`,按钮仍可用。
5. 拼图 runtime 单测、Rust 拼图模块测试和编码检查通过。

View File

@@ -20,6 +20,14 @@
5. 面板次按钮为 `保存并退出`,点击后关闭面板并执行原返回逻辑。
6. 非首次点击返回不再弹出面板,直接执行原返回逻辑。
## UI 布局
1. 面板保持居中独立弹层,移动端宽度不超过屏幕安全边距,桌面端保持紧凑。
2. 面板只展示标题与两个行动按钮,不增加说明性文案。
3. 标题使用两行居中排版,顶部可以放无文字图标强化游戏感。
4. `作品改造` 为主按钮,视觉权重高于 `保存并退出`
5. 两个按钮纵向排列,固定触控高度,确保移动端易点击。
## 首次状态
首次曝光是浏览器侧 UI 引导状态,不是业务真相态:

View File

@@ -0,0 +1,46 @@
# 拼图运行态低延迟交互前端化修正 2026-05-02
## 背景
本次检查发现正式平台入口的拼图运行态存在后端裁决回流:
1. 作品详情、公开作品卡和结果页试玩会启动后端 run。
2. `PuzzleRuntimeShell` 的交换和拖动回调在非本地 run 时会调用 `swapPuzzlePieces``dragPuzzlePieceOrGroup`
3. 服务端 run snapshot 如果直接覆盖前端当前棋盘,会让移动、交换、合并和通关反馈出现延迟或回退。
这与 PRD 中“前端以本地计算得到的 `allTilesResolved = true``status = cleared` 作为本关通关真相;后端不再参与拼块布局裁决”的规则冲突。
## 修正口径
正式平台入口采用混合运行态:
1. 正式平台开局仍调用后端 `startPuzzleRun`,保留真实 `runId`、游玩记录、排行榜和下一关存储锚点。
2. 点击交换只调用 `swapLocalPuzzlePieces`
3. 拖动单块或合并块只调用 `dragLocalPuzzlePiece`
4. 自动合并、拆分、合并块整体平移、被覆盖块交换和通关判定都以前端当前 `PuzzleRunSnapshot` 为准。
5. 通关后调用后端 `submitPuzzleLeaderboard` 持久化成绩并读取真实排行榜;前端只合并排行榜与下一关 handoff不用后端棋盘覆盖当前棋盘。
6. 点击同作品下一关调用后端 `advancePuzzleNextLevel`,由 SpacetimeDB 返回新的运行态快照。
7. 当前作品没有下一关时,通关弹窗展示后端 handoff 返回的相似作品;用户点击具体候选作品时直接 `startPuzzleRun(profileId, null)`,从目标作品第 `1` 关重新开始。
8. 失败状态点击“重新开始”时,正式 run 使用当前关 `levelId` 重新 `startPuzzleRun`,草稿/本地 run 使用本地重建,二者都保留当前失败关卡。
9. 结果页草稿试玩没有正式后端 run 时,继续使用本地 run、local leaderboard 和本地下一关兜底。
## 工程落点
1. `src/services/puzzle-runtime/puzzleLocalRuntime.ts`
- `startLocalPuzzleRun` 支持按 `levelId` 启动。
- `advanceLocalPuzzleLevel` 仅作为草稿试玩和无后端 run 的兜底。
- 正式平台的移动、交换、合并、拆分和通关裁决仍复用本地函数,避免交互延迟。
2. `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
- 平台拼图开局、恢复存档、排行榜、下一关接回后端。
- 正式平台入口不再调用 `/api/runtime/puzzle/runs/{runId}/swap``/drag`
- 后端排行榜返回的 run 只合并排行榜和 `nextLevelMode / nextLevelProfileId / nextLevelId / recommendedNextWorks`,不覆盖当前棋盘。
- 相似作品候选卡点击启动目标作品新 run失败重开按当前关 `levelId` 启动新 run。
3. `src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`
- 公开拼图玩法交互测试断言前端本地交换函数被调用。
- 同时断言后端 `swap / drag` 不参与棋盘交互,后端 `leaderboard / next-level` 继续参与非即时链路。
## 边界
本次只收回拼图玩法内的移动、交换、合并、拆分和通关裁决。创作 Agent、作品保存、发布、公开广场读取、作品详情读取、作品改造、排行榜、同作品下一关、相似候选生成、失败重开和游玩记录仍走现有后端链路。
`SERVER_RS_DDD_WP_PZ_RUNTIME_BACKEND_TRUTH_CLOSURE_2026-05-01.md` 作为历史收尾记录保留;若与本文冲突,以本文的“低延迟棋盘前端裁决,非即时链路后端持久化”口径为准。

View File

@@ -41,7 +41,7 @@
第 11 关开始,每 6 关循环复用第 5 关到第 10 关的配置,即 `5x5/210000ms``6x6/240000ms``5x5/210000ms``7x7/270000ms``5x5/240000ms``7x7/270000ms`
同作品下一关必须使用同一个运行时关卡序号继续推进。跨作品相似推荐代表进入新作品,必须从目标作品第 `1` 关重新开始
同作品下一关必须使用同一个运行时关卡序号继续推进。跨作品相似推荐只切换到候选作品的第一张图,运行时关卡序号、切割规格和倒计时继续按当前 run 累进,不重置难度循环
失败状态点击“重新开始”时,不进入作品第 `1` 关,而是重开当前失败关卡:前端需要传当前关 `levelId`,服务端按该 `levelId` 在作品内的位置恢复 `currentLevelIndex`、切割规格和倒计时。

View File

@@ -4,8 +4,53 @@
## 文档列表
- [PRODUCT_NAMING_BAIMENG_RENAME_2026-05-01.md](./PRODUCT_NAMING_BAIMENG_RENAME_2026-05-01.md)冻结当前对外中文命名,产品展示名统一为“百梦”,消费单位为“光点”,公开账号标识为“百梦号”,创作侧称谓为“百梦主”
- [PUZZLE_RUNTIME_FRONTEND_LOGIC_REHOME_2026-05-02.md](./PUZZLE_RUNTIME_FRONTEND_LOGIC_REHOME_2026-05-02.md)记录拼图正式平台入口移动、交换、合并、拆分和通关裁决收回前端即时运行态,排行榜、下一关和游玩记录继续由后端持久化处理
- [PROFILE_MAIN_ROUTE_VITE_PROXY_FIX_2026-05-02.md](./PROFILE_MAIN_ROUTE_VITE_PROXY_FIX_2026-05-02.md):记录“我的”和“存档”页面在本地把 `/api/profile/*` 请求落到 Vite SPA fallback、导致 HTML 被当 JSON 解析的根因,以及 `/api/profile` 代理补齐与回归测试。
- [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_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_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_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` 在当前稳定 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_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_TASK_BFF_CLOSURE_2026-04-29.md](./SERVER_RS_DDD_WP_AI_TASK_BFF_CLOSURE_2026-04-29.md):记录 `WP-AI AI Task` BFF 收口与关闭口径,补齐 AI task mutation route 鉴权和 SpacetimeDB 未发布错误 envelope 的定向验证不改表结构、LLM provider、SSE 或前端消费。
- [SERVER_RS_DDD_WP_CW_DOMAIN_ENUM_REHOME_2026-04-29.md](./SERVER_RS_DDD_WP_CW_DOMAIN_ENUM_REHOME_2026-04-29.md):记录 `WP-CW Custom World` 基础领域枚举归位切片,将 Custom World / RPG Agent 基础枚举、进度常量和字符串口径迁入 `module-custom-world/src/domain.rs`,不改 SpacetimeDB、API 或前端行为。
- [SERVER_RS_DDD_WP_RPG_GAMEPLAY_DOMAIN_SPLIT_2026-04-30.md](./SERVER_RS_DDD_WP_RPG_GAMEPLAY_DOMAIN_SPLIT_2026-04-30.md):记录 `WP-RPG Gameplay 域` 的 combat、inventory、NPC、quest、runtime-item 领域拆分收口,将真实规则从 `lib.rs` 拆入 DDD 骨架文件并保留原公开 API。
- [SERVER_RS_DDD_WP_RPG_PROGRESSION_DOMAIN_SPLIT_2026-04-30.md](./SERVER_RS_DDD_WP_RPG_PROGRESSION_DOMAIN_SPLIT_2026-04-30.md):记录 `WP-RPG Gameplay 域``module-progression` 领域拆分收口,将玩家成长、章节预算、章节账本、自动定级、领域事件和错误层从 `lib.rs` 拆入 DDD 骨架文件。
- [SERVER_RS_DDD_WP_RS_RUNTIME_STORY_DOMAIN_SPLIT_2026-04-30.md](./SERVER_RS_DDD_WP_RS_RUNTIME_STORY_DOMAIN_SPLIT_2026-04-30.md):记录 `WP-RS Runtime Story 去兼容层``module-runtime-story` 顶层领域拆分收口,将 action 结果、状态 patch、响应组装参数、领域事件和错误从 `lib.rs` 拆入 DDD 骨架文件。
- [SERVER_RS_DDD_WP_RPG_STORY_DOMAIN_SPLIT_2026-04-29.md](./SERVER_RS_DDD_WP_RPG_STORY_DOMAIN_SPLIT_2026-04-29.md):记录 `WP-RPG Gameplay 域``module-story` 领域拆分收口,将 story session 领域模型、命令、事件、应用映射和错误层从 `lib.rs` 拆入 DDD 骨架文件,并修正 README 不再指向旧 `/api/runtime/story/*` 兼容链路。
- [SERVER_RS_DDD_WP_PZ_DOMAIN_SPLIT_2026-04-29.md](./SERVER_RS_DDD_WP_PZ_DOMAIN_SPLIT_2026-04-29.md):记录 `WP-PZ Puzzle` 领域类型与规则拆分切片,将 Agent/作品/运行态领域类型、写入命令、应用规则、字段错误和最小领域事件归位到 `module-puzzle` 的 DDD 骨架文件,不改 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_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 残留审计切片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_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_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_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并在 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、继续游戏刷新与正式动作结算统一接入 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 对象形状,并确认组件层不拼接 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_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_PARALLEL_TASKLIST_2026-04-29.md](./SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md):把 `server-rs` DDD 一次性重构拆成全局可并行工作包,覆盖 `module-*``spacetime-module``spacetime-client``api-server``platform-*`、共享契约和前端接入的依赖、边界与验收命令。
- [SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md](./SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md):冻结 `server-rs` 一次性 DDD 重构总纲,明确 crate 依赖方向、模块目录、上下文聚合/命令/事件/读模型、SpacetimeDB adapter 映射和表结构变更约束。
- [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md):冻结 SpacetimeDB 表结构变更约束、自动迁移可接受范围、冲突后的系统行为,以及保留旧数据的增量迁移流程;凡涉及 `spacetime publish`、表字段调整或 `migration.rs` 对齐时优先参考。
- [PRODUCT_NAMING_BAIMENG_RENAME_2026-05-01.md](./PRODUCT_NAMING_BAIMENG_RENAME_2026-05-01.md):冻结当前对外中文命名,产品展示名统一为“百梦”,消费单位为“光点”,公开账号标识为“百梦号”,创作侧称谓为“百梦主”。
- [SPACETIMEDB_CLOUD_CONFIG_REMOVAL_2026-05-02.md](./SPACETIMEDB_CLOUD_CONFIG_REMOVAL_2026-05-02.md):记录旧云端 SpacetimeDB 配置、发布脚本和默认文档口径的移除结果,冻结后续仅使用本地或显式 `SERVER_URL` 的运维规则。
- [SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md](./SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md):记录本地 standalone 启动时报 `mismatched database identity` 的 root-dir/replica 数据残留根因、备份重建步骤和脚本诊断口径。
- [AUTH_SNAPSHOT_AND_MATCH3D_LOCAL_DEV_FIX_2026-05-01.md](./AUTH_SNAPSHOT_AND_MATCH3D_LOCAL_DEV_FIX_2026-05-01.md):记录远端库挂起导致认证快照同步和抓大鹅创作失败的根因、认证同步非阻断修复、`/api/creation` Vite 代理补齐和本地 SpacetimeDB 可跑链路。
@@ -86,7 +131,7 @@
- [JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md](./JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md):冻结 Jenkins `构建 / 部署 / 构建并部署` 三条流水线的职责、版本号传递、上游触发门禁、本地目录部署脚本、发布包覆盖策略,以及部署阶段 SpacetimeDB schema 冲突自动导出、清库发布、导入回灌能力。
- [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 上传、安全清库开关,以及发布包内 schema 冲突自动迁移脚本。
- [RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md](./RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md):记录当前 Rust `api-server` 已挂载的 Axum 路由,并补充管理 API 索引,按 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` 已挂载的 Axum 路由,并补充管理 API 索引,按 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 存储边界和文档维护门禁。
- [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` 的配置、状态注入与首版非流式兼容边界。

View File

@@ -2,6 +2,10 @@
更新时间:`2026-05-01`
> 2026-04-29 补充本文件保留为迁移期路由快照。DDD G1 后续并行工作的契约冻结口径以 [`SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md`](./SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md) 为准,尤其是新增的 Big Fish、Puzzle、profile、runtime chat、story facade 和兼容路由删除计划。
>
> 2026-04-29 WP-RS 进度:旧 `/api/runtime/story/*` HTTP compat 路由已从 `api-server/src/app.rs` 取消挂载,并删除 `api-server/src/runtime_story*` 兼容实现。当前 Rust `api-server` 对外 story 主链只保留 `/api/story/*`、`/api/runtime/sessions/{runtime_session_id}/inventory` 与 runtime chat 相关路由。
## 1. 文档目标
本文件记录当前 `server-rs/crates/api-server/src/app.rs` 中已挂载的 Rust Axum 路由面,用于对照 Node 后端 `96` 条路由能力基线。
@@ -22,7 +26,7 @@
8. llm proxy 接口:`1` 条。
9. profile / runtime profile 接口:`12` 条。
10. runtime story / story gameplay 接口:`15` 条。
11. legacy generated 静态路径兼容`6`
11. legacy generated 静态标识`6` 类历史路径字符串,直读代理已下线
12. health check`1` 条。
## 3. 路由清单
@@ -154,28 +158,18 @@
1. `POST /api/runtime/save/snapshot`
2. `GET /api/runtime/settings`
3. `GET /api/runtime/story/state/{session_id}`
4. `POST /api/runtime/story/state/resolve`
5. `POST /api/runtime/story/actions/resolve`
6. `POST /api/runtime/story/initial`
7. `POST /api/runtime/story/continue`
8. `POST /api/story/sessions`
9. `POST /api/story/sessions/continue`
10. `GET /api/story/sessions/{story_session_id}/state`
11. `POST /api/story/battles`
12. `POST /api/story/battles/resolve`
13. `GET /api/story/battles/{battle_state_id}`
14. `POST /api/story/npc/battle`
15. `GET /api/runtime/sessions/{runtime_session_id}/inventory`
3. `POST /api/story/sessions`
4. `POST /api/story/sessions/continue`
5. `GET /api/story/sessions/{story_session_id}/state`
6. `POST /api/story/battles`
7. `POST /api/story/battles/resolve`
8. `GET /api/story/battles/{battle_state_id}`
9. `POST /api/story/npc/battle`
10. `GET /api/runtime/sessions/{runtime_session_id}/inventory`
### 3.11 Legacy Generated 路径
1. `GET /generated-character-drafts/{*path}`
2. `GET /generated-characters/{*path}`
3. `GET /generated-animations/{*path}`
4. `GET /generated-custom-world-scenes/{*path}`
5. `GET /generated-custom-world-covers/{*path}`
6. `GET /generated-qwen-sprites/{*path}`
`/generated-*` 直读代理已下线。生成资产读取统一走 `GET /api/assets/read-url` 或 asset object projection`/generated-*` 字符串仅作为 `legacyPublicPath` / OSS object key 兼容标识保留。
### 3.12 Health

View File

@@ -0,0 +1,393 @@
# server-rs DDD 一次性重构落地方案
日期:`2026-04-28`
## 1. 背景与当前结论
当前仓库已经进入 `server-rs + Axum + SpacetimeDB` 单一后端路线,旧 `server-node` 已不存在,后续不再围绕 Express / PostgreSQL 做兼容设计。本轮重构目标不是新增玩法,而是把已有 Rust 后端统一成可长期演进的 DDD 边界:
1. 领域规则沉到 `module-*`
2. SpacetimeDB 事务、表、reducer、procedure 留在 `spacetime-module`
3. HTTP / SSE / BFF 留在 `api-server`
4. 外部副作用留在 `platform-*``api-server` 应用编排层。
5. 前后端 DTO 留在 `shared-contracts`,跨领域纯值处理留在 `shared-kernel`
本文件是后续编码前的总纲。若局部历史文档与本文冲突,以本文和对应 SpacetimeDB skill 为准;若本文仍无法精准指导某一处编码,必须先补本文或新增更细技术文档,再继续改工程。
## 2. 依赖方向
允许方向:
```text
api-server
-> shared-contracts
-> module-*
-> spacetime-client
-> platform-*
spacetime-module
-> module-*
-> shared-kernel
module-*
-> shared-kernel
-> 其他 module-* 的纯领域类型(仅在确有跨域规则复用时)
platform-*
-> shared-kernel
shared-contracts
-> shared-kernel
```
禁止方向:
1. `module-*` 直接依赖 Axum、HTTP client、OSS、LLM、文件系统、SpacetimeDB table/reducer/procedure API。
2. `module-*` 新增 `mapper.rs`,映射只能落在 adapter crate。
3. `spacetime-module` 反向依赖 `api-server``spacetime-client``platform-*`
4. `shared-kernel` / `shared-contracts` 依赖业务 crate。
5. 前端绕过 Axum 直接承接后端业务规则或数据真相。
阶段性例外必须满足三个条件:
1. 在本文“阶段性债务表”登记。
2. 有明确迁出目标 crate。
3. 不允许继续扩大例外范围。
## 3. crate 职责矩阵
| crate | 职责 | 禁止内容 |
| --- | --- | --- |
| `module-ai` | AI 任务、阶段、流式片段、结果引用的纯领域模型和状态机 | 真实 LLM 调用、HTTP、SSE |
| `module-assets` | 资产对象、资产绑定、历史查询输入输出和纯校验规则 | OSS head、reqwest、进程内 fallback store 扩散到领域核心 |
| `module-auth` | 用户、会话、验证码、微信绑定、密码规则、领域错误 | 文件持久化、真实短信副作用、HTTP cookie 写入 |
| `module-big-fish` | 大鱼创作会话、素材槽、运行态规则 | 图片生成、OSS 上传、HTTP handler |
| `module-combat` | 战斗聚合、行动结算、奖励结果 | 直接写背包表、直接发放成长账本 |
| `module-custom-world` | 世界 profile、Agent 会话、草稿卡、发布门禁规则 | LLM 推理、OSS、Axum response shape |
| `module-inventory` | 背包、装备槽、堆叠与消耗规则 | SpacetimeDB table 操作 |
| `module-npc` | NPC 关系、好感、互动、招募规则 | 战斗表初始化事务 |
| `module-progression` | 玩家等级、章节预算、经验记账规则 | 查询前端视图拼装 |
| `module-puzzle` | 拼图创作与运行态纯规则 | 图片生成、排行榜 HTTP shape |
| `module-quest` | 任务领取、推进、完成、交付规则 | 奖励跨域副作用直接写表 |
| `module-runtime` | 运行时设置、快照、个人页状态、存档领域模型 | 直接读写 SpacetimeDB |
| `module-runtime-item` | 宝箱、奖励物品、运行时物品快照 | 背包持久化事务 |
| `module-runtime-story` | RPG runtime story 新接口下的纯应用编排、剧情投影、场景旅行、战后收束、prompt context 投影 | Axum、LLM、SpacetimeDB |
| `module-story` | story session、story event 与推进输入输出 | 直接调用 LLM 或 HTTP |
| `spacetime-module` | 表、reducer、procedure、事务内查询写回、row/snapshot mapper、event table | 领域规则大段堆叠 |
| `spacetime-client` | api-server 调用 SpacetimeDB 的客户端 facade 与绑定 mapper | 领域规则 |
| `api-server` | 路由、鉴权上下文、请求响应映射、SSE、平台服务编排 | 表结构定义、领域规则主逻辑 |
| `platform-*` | JWT/SMS/微信/OSS/LLM/HTTP client 等外部能力 | 玩法领域规则 |
| `shared-kernel` | 字符串、时间、ID、纯值归一化 | 业务流程 |
| `shared-contracts` | HTTP/前端 DTO、兼容 response shape | 领域状态机 |
## 4. 统一目录结构
所有 `module-*` 必须具备以下文件。第一阶段允许旧实现仍在 `lib.rs` 或历史子模块中,但新增和迁移代码必须进入对应落点。
```text
src/
├─ lib.rs
├─ domain.rs 或 domain/
├─ commands.rs
├─ application.rs
├─ events.rs
└─ errors.rs
```
落位规则:
1. `domain.rs` / `domain/*`:聚合、值对象、领域方法、纯校验、状态迁移。
2. `commands.rs`:写入用例输入,只表达意图和必要参数,不含 adapter 类型。
3. `application.rs`:纯应用编排函数,输出领域事件或应用结果,不执行外部副作用。
4. `events.rs`:领域事件和跨上下文事件,例如奖励待入账、画廊投影待刷新。
5. `errors.rs`:领域错误,优先可测试、可映射,不直接绑定 HTTP status。
6. `mapper.rs`:只允许在 `api-server``spacetime-module``spacetime-client` 等 adapter crate 中出现。
## 5. 上下文设计清单
### 5.1 认证 `module-auth`
聚合:
1. `AuthUser`账号、公开百梦号、登录方式、绑定状态、token version。
2. `RefreshSession`refresh token hash、客户端信息、过期、吊销、last seen。
3. `SmsVerification`:手机号、场景、验证码状态、冷却、失败次数。
4. `WechatBinding`:微信 provider 身份、union id、绑定状态。
命令:
1. `PasswordEntryInput`
2. `SendPhoneCodeInput`
3. `PhoneLoginInput`
4. `CreateRefreshSessionInput`
5. `RevokeRefreshSessionInput`
6. `ResolveWechatLoginInput`
事件:
1. `AuthUserCreated`
2. `RefreshSessionIssued`
3. `RefreshSessionRevoked`
4. `PhoneCodeAccepted`
5. `WechatIdentityLinked`
读模型:
1. `AuthMeResult`
2. `PublicUserSearchResult`
3. `AuthSessionListResult`
迁移要求:文件持久化和内存 store 从领域核心剥离到 adapter 或临时测试支撑;短信真实发送继续在 `platform-auth`
### 5.2 资产 `module-assets`
聚合:
1. `AssetObject`bucket、object key、访问策略、hash、版本、业务归属。
2. `AssetEntityBinding`:实体类型、实体 id、slot、资产对象 id。
命令:
1. `ConfirmAssetObjectInput`
2. `AssetObjectUpsertInput`
3. `AssetEntityBindingInput`
4. `AssetHistoryListInput`
事件:
1. `AssetObjectConfirmed`
2. `AssetEntityBindingChanged`
读模型:
1. `AssetObjectUpsertSnapshot`
2. `AssetEntityBindingSnapshot`
3. `AssetHistoryEntrySnapshot`
迁移要求OSS `head_object`、reqwest client 和 fallback store 不再放入领域核心SpacetimeDB 持久化由 `spacetime-module` 完成。
### 5.3 RPG 运行时
覆盖 `module-story``module-combat``module-inventory``module-npc``module-progression``module-quest``module-runtime-item`
聚合:
1. `StorySession``StoryEvent`
2. `BattleState`
3. `InventorySlot`
4. `NpcState`
5. `PlayerProgression``ChapterProgression`
6. `QuestRecord``QuestLog`
7. `TreasureRecord`
跨上下文事件:
1. `CombatVictoryResolved`
2. `QuestTurnedIn`
3. `InventoryItemsGranted`
4. `ProgressionXpGranted`
5. `NpcRelationChanged`
6. `RuntimeStoryProjectionChanged`
事务边界:
1. 单聚合变更在对应 `module-*` 纯函数中完成。
2. 战斗奖励、任务奖励、背包写入、成长记账由 `spacetime-module` 或应用服务显式编排。
3. reducer/procedure 不允许复制领域规则,只负责取 row、调用领域函数、写回 row 和事件表。
### 5.4 世界创作 `module-custom-world`
聚合:
1. `CustomWorldProfile`
2. `CustomWorldSession`
3. `CustomWorldAgentSession`
4. `CustomWorldAgentMessage`
5. `CustomWorldAgentOperation`
6. `CustomWorldDraftCard`
7. `CustomWorldGalleryEntry`
命令:
1. 创建/恢复 Agent 会话。
2. 写入用户消息。
3. 写入 LLM 最终回复。
4. 更新草稿卡。
5. 发布/下架 profile。
事件:
1. `CustomWorldDraftChanged`
2. `CustomWorldProfilePublished`
3. `CustomWorldGalleryProjectionChanged`
4. `CustomWorldAgentOperationProgressed`
迁移要求LLM 提示词和推理在 `api-server + platform-llm`SpacetimeDB 只落真相表和投影。
### 5.5 拼图 `module-puzzle`
聚合:
1. `PuzzleAgentSession`
2. `PuzzleAgentMessage`
3. `PuzzleWorkProfile`
4. `PuzzleRuntimeRun`
事件:
1. `PuzzleDraftChanged`
2. `PuzzleWorkPublished`
3. `PuzzleRunAdvanced`
读模型:
1. 作品卡片。
2. 运行态快照。
3. 排行榜结果。
### 5.6 大鱼吃小鱼 `module-big-fish`
聚合:
1. `BigFishCreationSession`
2. `BigFishAgentMessage`
3. `BigFishAssetSlot`
4. `BigFishRuntimeRun`
事件:
1. `BigFishDraftChanged`
2. `BigFishAssetSlotChanged`
3. `BigFishRunTicked`
### 5.7 AI `module-ai`
聚合:
1. `AiTask`
2. `AiTaskStage`
3. `AiTextChunk`
4. `AiResultReference`
事件:
1. `AiTaskStarted`
2. `AiTaskStageCompleted`
3. `AiTaskFailed`
4. `AiResultAttached`
边界:真实模型调用只在 `platform-llm``module-ai` 只表达状态机。
## 6. SpacetimeDB adapter 映射
`spacetime-module` 中每个上下文遵循:
```text
src/<context>/
├─ mod.rs
├─ tables.rs # table 和 public/event 标记
├─ reducers.rs # 客户端可调用写入口
├─ procedures.rs # 需要同步返回或外部 procedure 语义的入口
├─ mapper.rs # row <-> module-* snapshot/input
└─ queries.rs # 事务内查询辅助,只返回 adapter DTO
```
当前已有历史拆分与本文不同名时,先按现有文档继续维护;新增或迁移时逐步对齐上面结构。
Reducer / procedure 规则:
1. reducer 使用 `&ReducerContext`,返回 `Result<(), String>` 处理预期错误。
2. reducer 内授权必须基于 `ctx.sender()`,禁止信任参数中的身份。
3. reducer 禁止网络、文件系统、外部随机数和全局状态。
4. procedure 使用 SpacetimeDB 2.0 正确 API涉及事务必须显式 `with_tx` / `try_with_tx`
5. 表访问必须使用 `ctx.db.table()`,更新只通过主键。
6. 复杂查询不复用写模型;需要前端订阅时新增明确读模型或 public 投影表。
## 7. 表结构约束
默认不改现有 SpacetimeDB 主表。确需改表时按以下优先级:
1. 新增 optional 字段。
2. 新增投影表或 event table。
3. 新增索引。
4. 最后才考虑 rename/delete/type change且必须单独写迁移方案。
任何 table 变更必须同步:
1. `server-rs/crates/spacetime-module/src/migration.rs`
2. `docs/technical/SPACETIMEDB_TABLE_CATALOG.md`
3. 对应 reducer/procedure 测试或最小 smoke
4. 绑定生成和前端/`spacetime-client` 映射
本阶段只做目录、文档和边界检查,不变更表结构,因此不需要改 `migration.rs`
## 8. 查询策略
1. 写模型不直接服务复杂前端页面。
2. 每个前端场景必须有独立 query/result DTO。
3. private table 默认不暴露给前端。
4. public table 只服务明确订阅场景。
5. event table 用于 reducer 后广播一次性事件,客户端必须显式订阅。
## 9. 第一阶段落地范围
第一阶段只做低风险基础设施:
1. 新增本文作为 DDD 总纲。
2. 所有 `module-*` 补齐 `domain / commands / application / events / errors` 过渡落位文件。
3. 新增 `scripts/check-server-rs-ddd-boundaries.mjs`,检查 DDD 骨架、禁用 mapper 落位和 SpacetimeDB/Axum 绝对边界。
4. 更新文档索引和后端 README。
5. 不改表、不改 reducer/procedure 名、不改 HTTP contract。
### 9.1 首个样板切片
`module-assets` 先作为 DDD 分层导出样板:
1. `domain.rs` 对外导出资产对象、实体绑定、访问策略、快照和纯校验函数。
2. `commands.rs` 对外导出确认资产、绑定实体、资产历史查询等输入。
3. `application.rs` 对外导出应用结果和纯构建函数。
4. `errors.rs` 对外导出资产领域错误。
5. `asset_object_core.rs` 暂作为内部历史实现文件保留,不再由 `lib.rs` 直接对外导出;后续触碰资产规则时继续把实现逐段迁到 DDD 文件。
## 10. 阶段性债务表
| 债务 | 当前位置 | 迁出目标 | 约束 |
| --- | --- | --- | --- |
| 资产 OSS head 与 reqwest 服务仍在 `module-assets` server-service feature | `module-assets/src/asset_object_service.rs` | `api-server` 应用服务或独立 adapter crate | 不得被 `domain.rs` 引用,不得新增更多 OSS 领域规则 |
| 认证内存 store / 文件持久化仍在 `module-auth` | `module-auth/src/lib.rs` | adapter / repository 目录或 `api-server` 启动恢复层 | 新业务规则不得继续依赖文件路径 |
| `spacetime-module/src/lib.rs` 仍有大量 gameplay 入口 | `spacetime-module/src/lib.rs` | `src/gameplay/*``src/custom_world/*``src/puzzle/*` | 新增实现禁止继续堆回根入口 |
| 部分 `module-*` 仍是单文件领域实现 | 多个 `module-*``src/lib.rs` | 对应 DDD 文件 | 每次触碰模块时顺手迁移相关片段 |
| runtime story 兼容层仍存在 | `module-runtime-story-compat``api-server/src/runtime_story/compat*`、前端旧 runtime story client | `module-runtime-story`、session scoped 新接口、前端新 client | 本轮允许 breaking change不再为旧 HTTP shape 保留双接口 |
## 11. 全局并行执行清单
`2026-04-29` 起,`server-rs` DDD 全局重构按 `SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md` 执行。该清单覆盖:
1. 文档、契约、DDD 骨架和边界检查。
2. `module-auth``module-assets``module-ai``module-custom-world``module-big-fish``module-puzzle``module-runtime`、RPG gameplay 域和 runtime story 域。
3. `spacetime-module``spacetime-client``api-server``platform-*` 和前端接入。
4. 旧层删除、命名收口、全链验证和 Maincloud smoke。
## 12. 去兼容层任务
`2026-04-29`runtime story / chat 改造按 `SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md` 中的 `WP-RS Runtime Story 去兼容层` 工作包执行。该工作包明确:
1. 当前 `compat` 层可以物理删除。
2. 新接口采用 `POST /api/runtime/story/sessions/:sessionId/...` 的 session scoped 口径。
3. 前端允许同步修改以匹配新 contract。
4. `api-server` 不再为旧 `worldType / character / monsters / history / context` 请求体保留正式主链分支。
## 13. 验收命令
阶段验收至少执行:
```powershell
node scripts/check-server-rs-ddd-boundaries.mjs
cargo fmt --all --check --manifest-path server-rs/Cargo.toml
cargo test --workspace --manifest-path server-rs/Cargo.toml
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml
npm run api-server:maincloud
npm run check:encoding
```
`npm run api-server:maincloud` 因本机未配置 Maincloud 数据库或令牌失败,必须记录具体错误;不能改用旧后端重启命令。

View File

@@ -0,0 +1,178 @@
# server-rs DDD G1 契约与路由矩阵冻结2026-04-29
## 1. 冻结范围
本文是 `SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md``G1 契约与路由矩阵` 的串行冻结结果。G1 只冻结契约、路由和后续并行任务边界,不实现业务逻辑,不迁移 reducer不改前端页面。
G1 单 owner 文件范围:
1. `server-rs/crates/shared-contracts/src/**`
2. `packages/shared/src/contracts/**`
3. `packages/shared/src/index.ts`
4. `docs/technical/RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md`
5. 本文档
后续并行任务只能消费本文冻结的 route、DTO 和 error envelope。确实需要新增或调整 DTO shape 时,先在对应工作包交接中写清变更原因,再回到 G1 owner 文件集中改动,避免多个并行线同时抢改契约。
## 2. HTTP 路由矩阵
状态含义:
1. `保留`:作为 DDD 改造后的主链路由继续存在。
2. `重命名`:后续改成新的 route family旧路径删除不做兼容双主链。
3. `删除`:兼容层或临时调试入口,前端迁移后物理删除。
4. `收敛`:保留功能,但 route、DTO 或返回 envelope 需要归一到新的主链。
| 分组 | 当前路由 | G1 决议 | 新主链目标 | 所属后续任务 |
| --- | --- | --- | --- | --- |
| 健康检查 | `GET /healthz` | 保留 | 不变,统一 envelope 可例外保留轻量 health payload | WP-API |
| 管理后台页面 | `GET /admin` | 保留 | 不变 | WP-API |
| 管理后台 API | `POST /admin/api/login``GET /admin/api/me``GET /admin/api/overview``POST /admin/api/debug/http` | 保留 | `Admin*` DTO 继续由 `admin.rs` 管理 | WP-A、WP-API |
| 管理兑换码 | `POST /admin/api/profile/redeem-codes``POST /admin/api/profile/redeem-codes/disable` | 收敛 | 继续走 admin 路由DTO 归入 profile/runtime 管理命令组 | WP-RT、WP-API |
| 内部鉴权调试 | `GET /_internal/auth/claims``GET /_internal/auth/refresh-cookie` | 删除 | 只允许本地诊断脚本或 admin debug 能力使用,不作为前端契约 | WP-DEL |
| 鉴权公开查询 | `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 |
| 鉴权登录 | `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`/generated-*` 仅允许作为 legacyPublicPath/object key 标识,不再作为可裸读路由 | WP-AS、WP-FE、WP-DEL |
| 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 |
| 文档输入 | `POST /api/runtime/creation-agent/document-inputs/parse` | 保留 | `ParseCreationAgentDocumentInputRequest/Response` | WP-CW、WP-BF、WP-PZ |
| AI task | `POST /api/ai/tasks``/{task_id}/start``/{task_id}/stages/{stage_kind}/start``/{task_id}/chunks``/{task_id}/stages/{stage_kind}/complete``/{task_id}/references``/{task_id}/complete``/{task_id}/fail``/{task_id}/cancel` | 保留 | `AiTask*` 命令/result DTO后续接 module-ai 状态机 | WP-AI |
| Assets object | `POST /api/assets/direct-upload-tickets``POST /api/assets/sts-upload-credentials``POST /api/assets/objects/confirm``POST /api/assets/objects/bind``GET /api/assets/read-url``GET /api/assets/history` | 保留 | `CreateDirectUploadTicket*``ConfirmAssetObject*``BindAssetObject*``GetReadUrlQuery/Response``AssetHistory*` | WP-AS |
| 角色资产工作流 | `POST /api/assets/character-visual/generate``GET /api/assets/character-visual/jobs/{task_id}``POST /api/assets/character-visual/publish``POST /api/assets/character-animation/generate``GET /api/assets/character-animation/jobs/{task_id}``POST /api/assets/character-animation/publish``POST /api/assets/character-animation/import-video``GET /api/assets/character-animation/templates``POST /api/assets/character-workflow-cache``GET /api/assets/character-workflow-cache/{character_id}``POST/PUT /api/runtime/custom-world/asset-studio/role/{character_id}/workflow` | 收敛 | Asset object、AI task、role workflow 三组 DTO 拆清workflow 不再把业务真相藏在 cache body | WP-AS、WP-CW、WP-API |
| Runtime settings/save | `GET/PUT /api/runtime/settings``GET/PUT/DELETE /api/runtime/save/snapshot` | 保留 | `RuntimeSettingsResponse``PutRuntimeSettingsRequest``SavedGameSnapshotResponse``PutSavedGameSnapshotRequest` | WP-RT |
| 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 |
| 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``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/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 |
| 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 |
| Story/Game facade | `POST /api/story/sessions``GET /api/story/sessions/{story_session_id}/state``GET /api/story/sessions/{story_session_id}/runtime-projection``POST /api/story/sessions/continue``POST /api/story/battles``GET /api/story/battles/{battle_state_id}``POST /api/story/npc/battle``POST /api/story/battles/resolve` | 保留 | `BeginStorySession*``ContinueStory*``StoryRuntimeProjection*``CreateStoryBattle*``StoryBattleState*``ResolveStoryBattle*` DTO 已补齐到 `shared-contracts` | WP-RPG、WP-RS |
## 3. DTO 冻结清单
### 3.1 保留
| 契约文件 | 保留 DTO |
| --- | --- |
| `shared-contracts/src/api.rs` | `ApiResponseMeta``ApiErrorPayload``ApiSuccessEnvelope<T>``ApiErrorEnvelope` |
| `shared-contracts/src/admin.rs` | `AdminLoginRequest/Response``AdminSessionPayload``AdminMeResponse``AdminOverviewResponse``AdminDebugHttpRequest/Response` |
| `shared-contracts/src/auth.rs` | `AuthLoginOptionsResponse``AuthUserPayload``PublicUserSummaryPayload``PublicUserSearchResponse``PasswordEntry*``PasswordChange*``PasswordReset*``AuthMeResponse``AuthSessionsResponse``RefreshSessionResponse``Logout*``Phone*``Wechat*` |
| `shared-contracts/src/ai.rs` | `CreateAiTaskRequest``AppendAiTextChunkRequest``CompleteAiStageRequest``AttachAiResultReferenceRequest``FailAiTaskRequest``AiTask*Payload``AiTaskMutationResponse``AiTaskAcceptedResponse` |
| `shared-contracts/src/assets.rs` | Direct upload、read url、asset object、asset binding、asset history、character visual/animation、workflow cache、role asset workflow 相关 DTO |
| `shared-contracts/src/creation_agent_document_input.rs` | `ParseCreationAgentDocumentInputRequest/Response``CreationAgentDocumentInputPayload` |
| `shared-contracts/src/big_fish*.rs` | `CreateBigFishSessionRequest``SendBigFishMessageRequest``ExecuteBigFishActionRequest``RecordBigFishPlayRequest``SubmitBigFishInputRequest``BigFish*Response``BigFishRunResponse``BigFishWorksResponse` |
| `shared-contracts/src/puzzle_*.rs` | `CreatePuzzleAgentSessionRequest``SendPuzzleAgentMessageRequest``ExecutePuzzleAgentActionRequest``PuzzleAgent*Response``PuzzleWork*``PuzzleRun*``PuzzleGallery*` |
| `shared-contracts/src/runtime.rs` | runtime settings/save/profile/browse history/custom world library/agent/result view/inventory 现有 DTO 在迁移窗口保留 |
| `shared-contracts/src/story.rs` | `BeginStorySessionRequest``ContinueStoryRequest``StorySessionPayload``StoryEventPayload``StorySessionMutationResponse``StorySessionStateResponse``StoryRuntimeProjectionRequest/Response``CreateStoryBattleRequest``CreateStoryNpcBattleRequest``StoryBattleStateResponse``ResolveStoryBattleRequest/Response` |
| `packages/shared/src/contracts/runtime.ts` | `RuntimeSettings``SavedGameSnapshot*`、profile、browse history、library/gallery DTO迁移窗口继续作为前端消费主入口 |
| `packages/shared/src/contracts/creationAgentDocumentInput.ts` | `ParseCreationAgentDocumentInputRequest/Response``CreationAgentDocumentInputPayload` |
| `packages/shared/src/contracts/rpgAgent*.ts` | RPG Agent、draft、anchors、result view、work summary DTO |
| `packages/shared/src/contracts/bigFish*.ts` | Big Fish Agent、backend runtime run/input、本地作品列表 DTO |
| `packages/shared/src/contracts/puzzle*.ts` | Puzzle Agent、work、gallery、runtime DTO |
### 3.2 重命名
| 当前 DTO | 新命名 | 说明 |
| --- | --- | --- |
| Rust `CustomWorldAgent*` | `RpgAgent*` | Rust 与 TS 命名对齐,`custom world` 只作为历史目录语义,不再作为 RPG 主链契约名。 |
| Rust `CustomWorldLibrary*` / `CustomWorldWorks*` | `RpgCreationWork*` / `RpgCreationLibrary*` | 前端已有 `RpgCreationWorkSummary`,后端后续对齐 RPG 创作域命名。 |
| Rust `GenerateCustomWorldProfile*` | `GenerateRpgCreationProfile*` | 去掉泛化 custom world 命名,明确 RPG 创作 profile。 |
| TS `AuthEntry*` | `PasswordEntry*` 或统一后端 `AuthPasswordEntry*` | 需要在 WP-A 中二选一收口,避免 entry 与 phone/wechat 登录语义混杂。 |
| `RuntimeStory*` view model | `RpgRuntimeStory*` 或拆到 `Story*``Battle*``Inventory*` | 旧聚合大 DTO 后续拆分为 story session、battle、inventory、npc interaction 投影。 |
| Profile 旧兼容入口 DTO | `RuntimeProfile*` | `/api/runtime/profile/*` 旧兼容入口已删除,契约命名可继续保留 RuntimeProfile 领域语义HTTP 主链固定为 `/api/profile/*`。 |
### 3.3 删除
| DTO/文件 | 删除条件 | 替代 |
| --- | --- | --- |
| `LegacyApiErrorResponse` | 全部路由完成 envelope 归一后 | `ApiErrorEnvelope` |
| Rust `RuntimeStoryStateResolveRequest` | 已在 WP-DEL 删除 | `StorySessionStateResponse` 与 story runtime projection |
| Rust/TS `RuntimeStoryBootstrapRequest/Response` | 已在 WP-DEL 删除 | `BeginStoryRuntimeSessionRequest``StoryRuntimeMutationResponse` |
| Rust/TS `RuntimeStoryAiRequest/Response` | `POST /api/runtime/story/continue` 删除后 | `ContinueStoryRequest``StorySessionMutationResponse` |
| Rust/TS `RuntimeStoryActionResponse` 旧总入口形态 | 已在 WP-DEL 删除 | `StoryRuntimeMutationResponse.projection` 与 story/battle/npc/inventory 分投影 |
| TS `StoryRequestPayload``PlainTextPromptRequest``PlainTextResponse` | runtime chat 不再由前端传 prompt 后 | 后端 session scoped chat/story command |
| TS `CreateCustomWorldSessionRequest``AnswerCustomWorldSessionQuestionRequest``CustomWorldSessionRecord` 等旧问答生成 DTO | 确认无前端运行引用后 | RPG Agent session DTO |
| `/api/runtime/profile/*` 旧兼容 DTO 别名 | 前端全量迁到 `/api/profile/*` 后 | Runtime profile DTO |
## 4. 页面/功能到 query/result DTO 映射
| 页面/功能 | Query DTO | Command DTO | Result DTO |
| --- | --- | --- | --- |
| 管理后台登录 | 无 | `AdminLoginRequest` | `AdminLoginResponse` |
| 管理后台概览 | 无 | 无 | `AdminMeResponse``AdminOverviewResponse` |
| 管理后台 API 调试 | 无 | `AdminDebugHttpRequest` | `AdminDebugHttpResponse` |
| 登录方式页 | 无 | 无 | `AuthLoginOptionsResponse` |
| 手机号登录 | 无 | `PhoneSendCodeRequest``PhoneLoginRequest` | `PhoneSendCodeResponse``PhoneLoginResponse` |
| 密码登录/改密/重置 | 无 | `PasswordEntryRequest``PasswordChangeRequest``PasswordResetRequest` | `PasswordEntryResponse``PasswordChangeResponse``PasswordResetResponse` |
| 会话中心 | refresh cookie / bearer token | `logout``logout-all` 无 body | `AuthMeResponse``AuthSessionsResponse``RefreshSessionResponse``LogoutResponse``LogoutAllResponse` |
| 公开用户卡片 | route param `code``user_id` | 无 | `PublicUserSearchResponse` |
| 创作中心 RPG 作品货架 | bearer token | 无 | `CustomWorldWorksResponse`,后续重命名 `RpgCreationWorksResponse` |
| RPG Agent 工作区 | route param `session_id` / `operation_id` | `CreateCustomWorldAgentSessionRequest``SendCustomWorldAgentMessageRequest``ExecuteCustomWorldAgentActionRequest` | `CustomWorldAgentSessionResponse``CustomWorldAgentOperationResponse``CustomWorldCreationResultViewResponse`,后续重命名 `RpgAgent*` |
| RPG 结果页 | route param `session_id` | section patch/action request | `CustomWorldCreationResultViewResponse``CustomWorldAgentCardDetailResponse` |
| RPG 资产工坊 | route param `character_id``GetReadUrlQuery` | `CharacterVisualGenerateRequest``CharacterAnimationGenerateRequest``CharacterWorkflowCacheSaveRequest``CharacterRoleAssetWorkflowResolveRequest` | `CharacterVisualGenerateResponse``CharacterAnimationGenerateResponse``CharacterWorkflowCacheGetResponse``CharacterRoleAssetWorkflowResponse` |
| Big Fish Agent/Runtime | route param `session_id` / `run_id` | `CreateBigFishSessionRequest``SendBigFishMessageRequest``ExecuteBigFishActionRequest``SubmitBigFishInputRequest` | `BigFishSessionResponse``BigFishActionResponse``BigFishRunResponse` |
| Big Fish 广场/作品 | bearer token 或公开 gallery query | `RecordBigFishPlayRequest` | `BigFishWorksResponse``BigFishGalleryResponse``BigFishSessionResponse` |
| Puzzle Agent | route param `session_id` | `CreatePuzzleAgentSessionRequest``SendPuzzleAgentMessageRequest``ExecutePuzzleAgentActionRequest` | `PuzzleAgentSessionResponse``PuzzleAgentActionResponse` |
| Puzzle 作品/广场 | route param `profile_id` | `PutPuzzleWorkRequest` | `PuzzleWorksResponse``PuzzleWorkDetailResponse``PuzzleGalleryResponse``PuzzleGalleryDetailResponse` |
| Puzzle 运行态 | route param `run_id` | `StartPuzzleRunRequest``SwapPuzzlePiecesRequest``DragPuzzlePieceRequest``SubmitPuzzleLeaderboardRequest` | `PuzzleRunResponse` |
| Runtime 设置与存档 | bearer token | `PutRuntimeSettingsRequest``PutSavedGameSnapshotRequest``PutRuntimeSaveCheckpointRequest` | `RuntimeSettingsResponse``SavedGameSnapshotResponse``BasicOkResponse` |
| 个人中心 | 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` |
| Runtime chat/NPC 私聊 | route param `runtime_session_id``story_session_id` | 后续新增 session scoped chat command | 后续新增 chat turn result`rpgRuntimeChat.ts` DTO 只作为迁移参考 |
## 5. Breaking change 清单
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 为索引。
3. `CustomWorld*` 作为 RPG 主链命名将被重命名为 `RpgCreation*``RpgAgent*`。前端可同步修改,不保留旧命名适配层。
4. `/api/custom-world/*` 非 runtime 前缀旧入口删除,统一进入 RPG creation route family 或 asset route family。
5. `/api/runtime/profile/*` 兼容入口删除,统一使用 `/api/profile/*`
6. 资产读取不再依赖 `/generated-*` 静态代理作为正式 contract统一走 asset object、read url 或后端投影里的正式 URL 字段;`/generated-*` 只保留为 legacyPublicPath/object key 兼容标识。
7. LLM 代理不得作为玩法 prompt 透传入口。玩法 prompt 由 `api-server`/`platform-llm` 内部编排,前端只提交用户动作和展示态输入。
8. API 错误体统一为 `ApiErrorEnvelope`。旧 `{ error, meta }` 只允许在已列入删除计划的旧接口中短期存在。
## 6. API 错误 envelope
所有主链 HTTP JSON 响应统一使用:
```json
{
"ok": false,
"data": null,
"error": {
"code": "BAD_REQUEST",
"message": "请求参数不合法",
"details": {
"message": "具体中文错误说明"
}
},
"meta": {
"apiVersion": "2026-04-08",
"requestId": "req-xxx",
"routeVersion": "2026-04-08",
"operation": "POST /api/example",
"latencyMs": 12,
"timestamp": "2026-04-29T00:00:00Z"
}
}
```
冻结规则:
1. 成功响应使用 `ApiSuccessEnvelope<T>``ok=true``data` 为 result DTO、`error=null``meta` 必填。
2. 失败响应使用 `ApiErrorEnvelope``ok=false``data=null``error` 必填、`meta` 必填。
3. `error.message` 是稳定的分类中文文案,`error.details.message` 是可展示给用户的具体中文错误。
4. 前端展示业务错误时优先读取 `error.details.message`,再退回 `error.message`
5. `code` 使用稳定英文枚举值,禁止把中文错误全文塞进 `code`
6. `LegacyApiErrorResponse` 只服务迁移窗口,不能用于新增主链 route。
## 7. 后续并行任务交接
1. 第 1 批领域任务不得改 `shared-contracts``packages/shared/src/contracts/**`。需要新字段时先写入任务交接,等 G1 owner 合流。
2. `WP-ST` 负责 SpacetimeDB 表、reducer/procedure 和 `migration.rs`,不得由玩法领域任务直接抢改。
3. `WP-API` 负责 `api-server/src/app.rs` 和 route 挂载入口,领域任务只提供应用结果和错误模型。
4. `WP-FE` 在后端新接口稳定后删除旧前端兼容层,不新增对旧 route 的二次适配。
5. `WP-DEL` 只能在搜索确认无运行引用后删除旧 DTO、旧 route 和旧静态代理;`/generated-*` 直读代理已移除后,后续不得重新引入裸读转发。

View File

@@ -0,0 +1,67 @@
# server-rs DDD G1 契约与路由矩阵进度记录2026-04-29
## 1. 当前状态
`G1 契约与路由矩阵` 已完成串行冻结,当前可作为第 1 批领域规则并行任务的契约输入。
本次只落地文档与索引,不修改 Rust / TypeScript 契约源码,不改业务实现,不启动前端迁移。
## 2. 已完成内容
1. 新增 G1 冻结文档:[`SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md`](./SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md)。
2. 冻结新旧 HTTP 路由清单,按 `保留``重命名``删除``收敛` 标记后续处理。
3. 冻结 DTO 保留、删除、重命名清单。
4. 冻结页面/功能到 query、command、result DTO 的映射。
5. 冻结 breaking change 清单,明确本轮不保留旧兼容层作为约束。
6. 冻结 API 错误 envelope主链统一使用 `ApiSuccessEnvelope<T>` / `ApiErrorEnvelope`
7. 在全局并行任务清单中补充 G1 冻结文档入口和单 owner 文件边界。
8. 在旧 Rust API route index 中补充 2026-04-29 提示,避免继续把 2026-04-23 快照当作最新契约。
9. 在技术 README 中补充 G1 冻结文档入口。
## 3. 单 owner 边界
G1 后续合流文件:
1. `server-rs/crates/shared-contracts/src/**`
2. `packages/shared/src/contracts/**`
3. `packages/shared/src/index.ts`
4. `docs/technical/RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md`
5. `docs/technical/SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md`
第 1 批并行领域任务不得直接改这些文件。确实需要新增字段或调整 DTO shape 时,先在任务交接里记录变更原因,再由 G1 owner 文件集中合流。
## 4. 验证结果
已执行:
```powershell
npm.cmd run check:encoding -- 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 docs/technical/README.md docs/technical/RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md
```
结果通过4 个文档文件 UTF-8 编码检查正常。
## 4.1 持续巡检记录
2026-04-29 本轮持续巡检已补齐迁移后漂移:
1. `Profile` 路由矩阵从旧 `/api/runtime/profile/*` 主链修正为当前 `/api/profile/*` 主链,并明确 `/api/runtime/profile/*` 已作为旧兼容入口取消挂载。
2. `Big Fish Agent/Works` 路由矩阵补齐后端真相源运行态接口:`POST /api/runtime/big-fish/sessions/{session_id}/runs``GET /api/runtime/big-fish/runs/{run_id}``POST /api/runtime/big-fish/runs/{run_id}/input`
3. `Story/Game facade` 补齐 `GET /api/story/sessions/{story_session_id}/runtime-projection`,并将 story runtime projection、story battle command/result DTO 标记为已进入 `shared-contracts/src/story.rs`
4. DTO 保留清单补齐 `shared-contracts/src/creation_agent_document_input.rs``packages/shared/src/contracts/creationAgentDocumentInput.ts`
5. `packages/shared/src/index.ts` 已导出 `creationAgentDocumentInput` 类型,避免前端继续绕过共享契约包入口深路径引用。
## 5. 后续入口
下一步可以按全局清单进入第 1 批领域规则并行任务:
1. `WP-A Auth`
2. `WP-AS Assets`
3. `WP-AI AI Task`
4. `WP-CW Custom World`
5. `WP-BF Big Fish`
6. `WP-PZ Puzzle`
7. `WP-RT Runtime/Profile/Save`
8. `WP-RPG Gameplay 域`
9. `WP-RS Runtime Story 去兼容层`
进入下一批前,先以 G1 冻结文档确认 route、DTO、error envelope 和 breaking change避免并行任务各自定义接口。

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
# server-rs DDD tests-support 共享测试支撑 crate 收口记录
日期:`2026-04-30`
## 1. 收口目标
本次关闭 `4.1 未完整收口内容整合清单``tests-support` 仍只是目录占位的问题,将其补成 `server-rs` workspace 中的真实共享测试支撑 crate。
## 2. 已完成
1. `server-rs/Cargo.toml` 已把 `crates/tests-support` 纳入 workspace member。
2. 新增 `server-rs/crates/tests-support/Cargo.toml`
3. 新增 `server-rs/crates/tests-support/src/lib.rs`,提供 Maincloud healthz 默认地址、smoke URL 归一化、HTTP 2xx 断言和 healthz 非空响应体断言。
4. 更新 `server-rs/crates/tests-support/README.md`,明确当前首版边界和后续可扩展方向。
5. 更新 `SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`,将 `tests-support` 从目录占位改为已收口的真实测试支撑 crate。
## 3. 边界说明
1. 本次不引入业务规则、不创建伪领域 fixture。
2. 本次不修改 SpacetimeDB 表、reducer、procedure、绑定或 `migration.rs`
3. Contract DTO、reducer/view/projection 共享夹具仍等待对应接口与最终 smoke 策略稳定后继续扩展。
## 4. 验证
```powershell
cargo test -p tests-support --manifest-path server-rs\Cargo.toml
cargo check -p tests-support --manifest-path server-rs\Cargo.toml
```
结果:通过,`tests-support` 4 个单元测试通过crate 编译通过。

View File

@@ -0,0 +1,94 @@
# server-rs DDD WP-AI 内部子模块拆分记录2026-04-29
## 1. 背景
`WP-AI AI Task` 已完成领域层、SpacetimeDB adapter、spacetime-client facade 与 BFF route 闭环。当前继续推进 DDD 收口时,`module-ai` 虽然已经从首轮 `lib.rs` 大文件拆成 `domain / commands / application / events / errors`,但 `domain.rs``commands.rs``application.rs` 仍承载多类职责,后续继续演进阶段规则、任务结果聚合或 store 实现时容易重新堆成大文件。
本次只做 `module-ai` crate 内部子模块拆分,保持 `module_ai::*` 对外公开导出不变,不改变 SpacetimeDB table / reducer / procedure / event table不改变 HTTP DTO、route 或前端调用。
## 2. 本次拆分范围
允许修改:
1. `server-rs/crates/module-ai/src/domain.rs`
2. `server-rs/crates/module-ai/src/domain/*`
3. `server-rs/crates/module-ai/src/commands.rs`
4. `server-rs/crates/module-ai/src/commands/*`
5. `server-rs/crates/module-ai/src/application.rs`
6. `server-rs/crates/module-ai/src/application/*`
7. `server-rs/crates/module-ai/src/lib.rs`
8. `server-rs/crates/module-ai/src/tests.rs`
9. `server-rs/crates/module-ai/README.md`
10. 本文档、`docs/technical/README.md` 与全局任务清单进度记录
禁止修改:
1. `server-rs/crates/spacetime-module/src/**`
2. `server-rs/crates/spacetime-client/src/**`
3. `server-rs/crates/api-server/src/**`
4. `server-rs/crates/shared-contracts/src/**`
5. `packages/shared/src/contracts/**`
6. `server-rs/crates/spacetime-module/src/migration.rs`
## 3. 拆分落点
```text
server-rs/crates/module-ai/src/
├─ application.rs
├─ application/
│ ├─ result.rs
│ ├─ service.rs
│ └─ store.rs
├─ commands.rs
├─ commands/
│ ├─ inputs.rs
│ └─ validation.rs
├─ domain.rs
├─ domain/
│ ├─ ids.rs
│ ├─ stages.rs
│ └─ types.rs
├─ errors.rs
├─ events.rs
├─ lib.rs
└─ tests.rs
```
职责说明:
1. `domain/types.rs` 只放 AI task、stage、chunk、result reference 的领域类型与快照。
2. `domain/stages.rs` 只放默认阶段蓝图、阶段 slug、阶段中文标签与终态判断。
3. `domain/ids.rs` 只放 ID 前缀、ID helper 和共享字符串归一 helper re-export。
4. `commands/inputs.rs` 只放写入输入结构。
5. `commands/validation.rs` 只放创建任务输入校验。
6. `application/result.rs` 只放面向 SpacetimeDB procedure 的轻量结果结构。
7. `application/service.rs` 只放 AI task 状态机应用服务。
8. `application/store.rs` 只放当前内存 store 与流式文本片段聚合。
9. `tests.rs` 承接原 `lib.rs` 行为测试,`lib.rs` 只保留模块声明与公开导出。
## 4. 行为不变口径
本次必须保持:
1. `module_ai::*` 公开导出兼容。
2. 默认阶段蓝图顺序不变。
3. 任务状态迁移不变。
4. 终态任务不允许继续写入阶段、文本片段、结果引用或完成状态。
5. 流式文本片段继续按 `sequence` 聚合到阶段输出和 `latest_text_output`
6. 中文错误文案不改写为英文。
7. 不新增真实 LLM provider、prompt 组装、SSE 协议或前端消费逻辑。
## 5. 验收
必须执行:
```powershell
cargo fmt -p module-ai --manifest-path server-rs/Cargo.toml --check
cargo test -p module-ai --manifest-path server-rs/Cargo.toml
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_AI_INTERNAL_MODULE_SPLIT_2026-04-29.md docs/technical/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md server-rs/crates/module-ai/README.md server-rs/crates/module-ai/src/lib.rs server-rs/crates/module-ai/src/domain.rs server-rs/crates/module-ai/src/domain/ids.rs server-rs/crates/module-ai/src/domain/stages.rs server-rs/crates/module-ai/src/domain/types.rs server-rs/crates/module-ai/src/commands.rs server-rs/crates/module-ai/src/commands/inputs.rs server-rs/crates/module-ai/src/commands/validation.rs server-rs/crates/module-ai/src/application.rs server-rs/crates/module-ai/src/application/result.rs server-rs/crates/module-ai/src/application/service.rs server-rs/crates/module-ai/src/application/store.rs server-rs/crates/module-ai/src/tests.rs
npm.cmd run api-server:maincloud
```
`api-server:maincloud` 是常驻后端启动命令,验收时以命令启动和 `GET http://127.0.0.1:3100/healthz` 探测结果记录为准。

View File

@@ -0,0 +1,73 @@
# server-rs DDD WP-AI AI Task BFF 收口记录2026-04-29
## 1. 背景
`WP-AI AI Task` 的领域层已完成 DDD 拆分,`spacetime-module/src/ai/*` 已具备 AI task 真相表、阶段表、文本片段、结果引用、事件表和最小 reducer / procedure`spacetime-client` 已提供 typed facade`api-server` 已挂载 AI task mutation route。
本次认领的目标不是新增模型供应商能力,也不是改表,而是把现有 AI task BFF 链路做闭环验证,确认前端或后续业务模块调用 AI task 写接口时不会绕过鉴权、不会在 SpacetimeDB 未发布时返回错误形态不一致的响应。
## 2. 本次完成范围
1. 补齐 `api-server/src/ai_tasks.rs` 的 AI task mutation route 定向测试。
2. 覆盖以下写接口的未登录拦截:
- `POST /api/ai/tasks/{task_id}/stages/{stage_kind}/start`
- `POST /api/ai/tasks/{task_id}/chunks`
- `POST /api/ai/tasks/{task_id}/stages/{stage_kind}/complete`
- `POST /api/ai/tasks/{task_id}/references`
- `POST /api/ai/tasks/{task_id}/complete`
- `POST /api/ai/tasks/{task_id}/fail`
- `POST /api/ai/tasks/{task_id}/cancel`
3. 覆盖上述写接口在 token 有效但 SpacetimeDB 未发布时统一返回 `502 BAD_GATEWAY`,并保持错误详情 `provider = spacetimedb`
4. 复核 `module-ai``spacetime-client``spacetime-module` 与 BFF 定向测试,确认本次不需要修改 `migration.rs`
## 3. 边界
本次未进入:
1. 真实 LLM provider 调用、prompt 组装和供应商降级策略。
2. SSE 流式输出协议。
3. AI task 订阅 projection、清理调度或前端消费 UI。
4. SpacetimeDB table、reducer、procedure、event table 的结构变更。
5. 共享契约字段调整或前端 API client 改造。
这些能力继续归入 `WP-PF platform side effects``WP-API api-server BFF``WP-FE` 和后续对应业务域工作包,不在 `module-ai` 领域核心中混入。
## 4. 验收
已执行:
```powershell
cargo fmt -p api-server --manifest-path server-rs\Cargo.toml --check
cargo test -p api-server ai_task --manifest-path server-rs\Cargo.toml
cargo test -p module-ai --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-module --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_PARALLEL_TASKLIST_2026-04-29.md docs/technical/SERVER_RS_DDD_WP_AI_TASK_BFF_CLOSURE_2026-04-29.md docs/technical/README.md server-rs/crates/api-server/src/ai_tasks.rs server-rs/crates/api-server/src/wechat_auth.rs
npm.cmd run api-server:maincloud
```
结果:
1. `api-server ai_task` 定向测试通过7 个测试全部通过。
2. `module-ai` 测试通过9 个测试全部通过。
3. `spacetime-client` 编译通过。
4. `spacetime-module` 编译通过。
5. `api-server` 编译通过,仅保留既有未使用代码 warning。
6. `npm.cmd run check:server-rs-ddd` 通过。
7. 编码检查通过5 个文件均为 UTF-8。
8. `npm.cmd run api-server:maincloud` 已启动本仓库 `server-rs/target/debug/api-server.exe``GET http://127.0.0.1:3100/healthz` 返回 `200`
9. 未执行 SpacetimeDB 发布、绑定生成或 migration 更新,原因是本次未改变 SpacetimeDB schema、reducer/procedure 签名或绑定形状。
## 5. 关闭口径
`WP-AI AI Task` 当前已完成:
1. 领域层 DDD 拆分。
2. SpacetimeDB AI task 真相表、阶段表、文本片段、结果引用和事件表 adapter。
3. `spacetime-client` typed facade。
4. `api-server` AI task mutation route 与错误 envelope。
5. BFF 鉴权和 SpacetimeDB 未发布错误形态的定向回归测试。
因此本次将工作包认领状态从 `已认领` 更新为 `已关闭`。后续真实模型调用、SSE 和前端消费不再阻塞 `WP-AI` 关闭分别由平台副作用、API 编排和前端迁移工作包继续承接。

View File

@@ -0,0 +1,64 @@
# server-rs DDD WP-AI AI Task 领域层重构方案2026-04-29
## 1. 背景
`G1 契约与路由矩阵` 已冻结,`WP-AI AI Task` 进入第 1 批领域规则并行泳道。当前 `module-ai` 已有 AI task 状态机、输入类型、错误和内存服务,但主要实现集中在 `src/lib.rs`,与全局 DDD 清单要求的 `domain / commands / application / events / errors` 分层不一致。
本次只整理 `module-ai` 纯领域层,不改 HTTP route不改 SpacetimeDB table / reducer / procedure不改前端。
## 2. 目标
1. 保持 `module_ai::*` 公开 API 兼容,让 `spacetime-module` 现有引用不需要跟随修改。
2. 将 AI task 领域模型、命令、应用服务、事件、错误拆入对应文件。
3. 保持 AI task 状态迁移规则不变:
- `Pending -> Running`
- `Running -> Completed / Failed / Cancelled`
- 终态不允许继续写入阶段、文本片段、结果引用或任务结束状态
4. 保持流式文本片段按 `sequence` 聚合到阶段输出和任务 `latest_text_output`
5. 保持中文错误信息,便于 HTTP adapter 与 SpacetimeDB adapter 显式映射。
## 3. 文件边界
本次允许修改:
1. `server-rs/crates/module-ai/src/lib.rs`
2. `server-rs/crates/module-ai/src/domain.rs`
3. `server-rs/crates/module-ai/src/commands.rs`
4. `server-rs/crates/module-ai/src/application.rs`
5. `server-rs/crates/module-ai/src/events.rs`
6. `server-rs/crates/module-ai/src/errors.rs`
7. `server-rs/crates/module-ai/README.md`
8. 本文档和全局任务清单进度记录
本次禁止修改:
1. `server-rs/crates/spacetime-module/src/**`
2. `server-rs/crates/spacetime-client/src/**`
3. `server-rs/crates/api-server/src/**`
4. `server-rs/crates/shared-contracts/src/**`
5. `packages/shared/src/contracts/**`
## 4. 分层落点
| 文件 | 职责 |
| --- | --- |
| `domain.rs` | AI task kind/status、stage kind/status、snapshot、ID helper、文本归一 helper |
| `commands.rs` | create/start/stage/chunk/result/fail/cancel 等写入输入,以及创建命令校验 |
| `application.rs` | `AiTaskService``InMemoryAiTaskStore` 和纯内存状态迁移 |
| `events.rs` | AI task 领域事件枚举,供后续 adapter / event table 映射 |
| `errors.rs` | `AiTaskFieldError``AiTaskServiceError` 与中文 Display |
| `lib.rs` | 模块声明、公开 re-export、既有行为测试 |
## 5. 验收
必须执行:
```powershell
cargo test -p module-ai --manifest-path server-rs/Cargo.toml
cargo fmt --all --check --manifest-path server-rs/Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_AI_TASK_DOMAIN_REFACTOR_2026-04-29.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md server-rs/crates/module-ai/README.md server-rs/crates/module-ai/src/lib.rs server-rs/crates/module-ai/src/domain.rs server-rs/crates/module-ai/src/commands.rs server-rs/crates/module-ai/src/application.rs server-rs/crates/module-ai/src/events.rs server-rs/crates/module-ai/src/errors.rs
npm.cmd run api-server:maincloud
```
说明:本次不改 `api-server` route、SpacetimeDB table/reducer/procedure 或前端接线,但按仓库约束,后端 Rust 代码变更后仍执行 `npm.cmd run api-server:maincloud` 重新启动后端。

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,98 @@
# server-rs DDD WP-API BFF 启动切片2026-04-29
## 1. 背景
`G1 契约与路由矩阵` 已冻结,`WP-RS` 已把旧 `module-runtime-story-compat` 迁为 `module-runtime-story`,当前可以启动 `WP-API api-server BFF` 的第一段边界收口。
`WP-ST SpacetimeDB Adapter``WP-SC Spacetime Client``WP-RS` 已提供 runtime projection 所需 facade 与领域投影 builder因此本切片继续完成 BFF 层接线:路由挂载收口、错误 envelope 门禁、健康检查、runtime projection 新主链 route 接入,不新增领域规则,不改 SpacetimeDB table/reducer/procedure不绕过 `spacetime-client`
## 2. 本切片目标
1. 确认旧 `/api/runtime/story/*` 兼容路由不再挂载到 `api-server/src/app.rs`
2. 保留新主链 `/api/story/*` route family后续只通过 `spacetime-client` facade 访问 SpacetimeDB。
3. 对旧 runtime story 路由补充 404 + `ApiErrorEnvelope` 回归测试,避免后续重新接回兼容入口。
4. 保持 `/healthz` 轻量健康检查可用,并在请求方声明 envelope 时返回标准成功 envelope。
5. 接入 story runtime projection 主链路由,固定鉴权、错误 envelope 与 `StoryRuntimeProjectionResponse` 输出。
6. 记录 WP-API 后续接线边界与前端迁移前置条件。
## 3. 文件边界
本切片允许修改:
1. `server-rs/crates/api-server/src/app.rs`
2. `server-rs/crates/api-server/src/story_sessions.rs`
3. `docs/technical/SERVER_RS_DDD_WP_API_BFF_START_2026-04-29.md`
4. `docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`
5. `docs/technical/README.md`
本切片不修改:
1. `server-rs/crates/spacetime-module/src/**`
2. `server-rs/crates/spacetime-client/src/**`
3. `server-rs/crates/shared-contracts/src/**`
4. `packages/shared/src/contracts/**`
5. `src/services/**``src/hooks/**``src/components/**`
## 4. 当前 API 边界
### 4.1 已收口
旧 runtime story 兼容入口不再挂载:
1. `POST /api/runtime/story/sessions`
2. `POST /api/runtime/story/state/resolve`
3. `GET /api/runtime/story/state/{session_id}`
4. `POST /api/runtime/story/actions/resolve`
5. `POST /api/runtime/story/initial`
6. `POST /api/runtime/story/continue`
这些路径请求 `x-genarrative-response-envelope: v1` 时应返回:
1. HTTP status`404`
2. `ok=false`
3. `error.code=NOT_FOUND`
4. `error.message=资源不存在`
### 4.2 保留主链
当前保留的新主链入口:
1. `POST /api/story/sessions`
2. `GET /api/story/sessions/{story_session_id}/state`
3. `GET /api/story/sessions/{story_session_id}/runtime-projection`
4. `POST /api/story/sessions/continue`
5. `POST /api/story/battles`
6. `GET /api/story/battles/{battle_state_id}`
7. `POST /api/story/npc/battle`
8. `POST /api/story/battles/resolve`
这些 route 只能做鉴权、请求响应 DTO 映射、错误 envelope 和 `spacetime-client` 调用,不在 `api-server` 中复制 RPG 领域规则。
`runtime-projection` 已接入新主链鉴权通过后route 只读取当前用户身份,调用 `spacetime-client::get_story_runtime_projection_source`,再交给 `module-runtime-story::build_story_runtime_projection` 输出 `StoryRuntimeProjectionResponse`。API 层不得重新挂回旧 `/api/runtime/story/*` compat 总入口,也不得复制 actor、inventory、option、status 等领域投影规则。
## 5. 后续依赖
`WP-API` 后续继续接线前必须保持:
1. runtime projection 只通过 `spacetime-client` facade 读取 SpacetimeDB。
2. 投影规则只由 `module-runtime-story` 输出,`api-server` 只做 BFF 编排。
3. `WP-PF` 稳定 LLM、OSS、SMS、微信等平台副作用错误模型。
4. `G1` owner 合流必要 DTO shape 变更。
`runtime-projection` route/DTO 已可作为 `WP-FE-S` 迁移输入;前端仍需按 `services -> hooks -> components` 顺序推进,不在 hooks/components 中重建正式业务规则。
## 6. 验收
本切片验证命令:
```powershell
cargo test -p api-server runtime_story_legacy_routes_are_not_mounted --manifest-path server-rs/Cargo.toml
cargo test -p api-server healthz_returns_standard_envelope_when_requested --manifest-path server-rs/Cargo.toml
cargo test -p api-server get_story_runtime_projection --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_START_2026-04-29.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md docs/technical/README.md server-rs/crates/api-server/src/app.rs server-rs/crates/api-server/src/story_sessions.rs
npm.cmd run api-server:maincloud
```
说明:本切片未修改 SpacetimeDB 表结构、reducer 或 procedure因此不需要更新 `migration.rs`,也不执行绑定生成。

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,58 @@
# WP-AS Assets 资产对象类型归位落地说明
## 背景
`module-assets` 已具备 `domain / commands / application / errors / events` DDD 骨架,但 `asset_object_core.rs` 仍同时承载领域模型、命令 DTO、应用返回 DTO、字段错误和纯构建函数。继续推进资产对象、实体绑定、OSS adapter 与 SpacetimeDB row mapper 时,容易把纯领域事实和外层编排混在同一个文件里。
本次作为 `WP-AS Assets` 的小切片,只在 `module-assets` crate 内做类型归位,不改变现有公开 API、SpacetimeDB 表结构、reducer/procedure、OSS 行为、api-server 路由或前端行为。
## 本次范围
允许修改:
1. `server-rs/crates/module-assets/src/domain.rs`
2. `server-rs/crates/module-assets/src/commands.rs`
3. `server-rs/crates/module-assets/src/application.rs`
4. `server-rs/crates/module-assets/src/errors.rs`
5. `server-rs/crates/module-assets/src/asset_object_core.rs`
6. `server-rs/crates/module-assets/src/lib.rs`
7. `server-rs/crates/module-assets/README.md`
8. 本文档与全局 DDD 任务清单
禁止修改:
1. `server-rs/crates/spacetime-module/src/**`
2. `server-rs/crates/spacetime-client/src/**`
3. `server-rs/crates/api-server/src/**`
4. `server-rs/crates/platform-oss/src/**`
5. 前端 services/hooks/components
## 设计
本次按 DDD 骨架进行类型归位:
1. `domain.rs` 承接资产对象、资产历史、实体绑定的纯快照、记录、访问策略、ID 前缀和版本常量。
2. `commands.rs` 承接确认资产对象、资产历史查询、对象 upsert、实体绑定等输入 DTO。
3. `application.rs` 承接 procedure result 和确认资产对象结果等应用返回 DTO。
4. `errors.rs` 承接 `AssetObjectFieldError` 及其中文错误文案。
5. `asset_object_core.rs` 保留字段校验、输入构建、记录构建、ID 生成和可复用归一化函数。
`lib.rs` 继续按原名称导出,避免影响 `spacetime-module``api-server` 或既有测试。
## 边界说明
1. 本次不移动 `AssetObjectService`,因为它仍依赖 `platform-oss` 的对象探测和进程内仓储。
2. 本次不新增资产任务、manifest 或专业资产表。
3. 本次不修改 `migration.rs`,因为没有表结构变更。
4. 本次不改变 `bucket + object_key` 双列真相字段。
## 验收
```powershell
cargo fmt -p module-assets --manifest-path server-rs/Cargo.toml --check
cargo test -p module-assets --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_OBJECT_TYPE_REHOME_2026-04-29.md server-rs/crates/module-assets/src/domain.rs server-rs/crates/module-assets/src/commands.rs server-rs/crates/module-assets/src/application.rs server-rs/crates/module-assets/src/errors.rs server-rs/crates/module-assets/src/asset_object_core.rs server-rs/crates/module-assets/src/lib.rs server-rs/crates/module-assets/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md docs/technical/README.md
```
后端启动按项目约束执行 `npm.cmd run api-server:maincloud`,若命令以前台服务常驻,则以 `/healthz` 结果记录。

View File

@@ -0,0 +1,121 @@
# WP-A Auth DDD 分层收口说明
## 背景
`module-auth` 当前已经具备 `domain / commands / application / events / errors` 文件骨架,但真实认证类型、服务、内存仓、文件持久化、短信 provider、密码哈希和微信状态逻辑仍主要集中在 `src/lib.rs`。这会让后续继续迁移 Auth 领域规则时难以区分纯领域模型和外层 adapter 能力。
本次从原先的领域值对象启动切片继续推进到 `WP-A Auth` 收口:把认证上下文的纯领域事实、命令输入、应用返回、领域错误和领域事件落回 DDD 骨架文件,同时核查 `api-server / platform-auth / spacetime-module` 的职责边界。
## 本次范围
允许修改:
1. `server-rs/crates/module-auth/src/domain.rs`
2. `server-rs/crates/module-auth/src/commands.rs`
3. `server-rs/crates/module-auth/src/application.rs`
4. `server-rs/crates/module-auth/src/events.rs`
5. `server-rs/crates/module-auth/src/errors.rs`
6. `server-rs/crates/module-auth/src/lib.rs`
7. `server-rs/crates/spacetime-module/src/auth.rs`
8. `server-rs/crates/module-auth/README.md`
9. 本文档
10. 全局 DDD 任务清单进度记录
本次只核查但不改动:
1. `server-rs/crates/api-server/src/auth*.rs`
2. `server-rs/crates/api-server/src/password_entry.rs`
3. `server-rs/crates/api-server/src/phone_auth.rs`
4. `server-rs/crates/api-server/src/refresh_session.rs`
5. `server-rs/crates/api-server/src/wechat_auth.rs`
6. `server-rs/crates/platform-auth/src/**`
禁止修改:
1. 其他玩法域。
2. 前端 services / hooks / components。
3. `server-node` 或旧 PostgreSQL 实现。
4. SpacetimeDB 表结构、reducer/procedure 签名和 `migration.rs`
## 设计
本次将以下纯领域事实和规则落入 `domain.rs`
1. `AuthLoginMethod`
2. `AuthBindingStatus`
3. `AuthUser`
4. `PhoneNumberSnapshot`
5. `PhoneAuthScene`
6. `WechatIdentityProfile`
7. `WechatAuthScene`
8. `WechatAuthStateRecord`
9. `RefreshSessionClientInfo`
10. `RefreshSessionRecord`
11. `AuthStoreSnapshotRecord`
12. 密码长度、短信验证码长度、验证码 TTL、冷却、失败次数等领域常量。
13. 手机号规范化、手机号脱敏、公开百梦号规范化、验证码 key 构造等纯函数。
本次将以下写入输入落入 `commands.rs`
1. `PasswordEntryInput`
2. `ChangePasswordInput`
3. `ResetPasswordInput`
4. `SendPhoneCodeInput`
5. `PhoneLoginInput`
6. `ResolveWechatLoginInput`
7. `CreateWechatAuthStateInput`
8. `BindWechatPhoneInput`
9. `CreateRefreshSessionInput`
10. `RotateRefreshSessionInput`
11. `LogoutCurrentSessionInput`
12. `LogoutAllSessionsInput`
13. `AuthStoreSnapshotUpsertInput`
本次将以下应用返回落入 `application.rs`
1. 登录、换密、重置密码、验证码发送、手机号登录、微信登录、微信绑定、会话签发/轮换/查询/登出等 result。
2. `AuthStoreSnapshotProcedureResult`
本次将以下错误落入 `errors.rs`
1. `PasswordEntryError`
2. `PhoneAuthError`
3. `WechatAuthError`
4. `RefreshSessionError`
5. `LogoutError`
6. 错误展示文案和模块内错误映射辅助函数。
本次将领域事件落入 `events.rs`
1. `UserCreated`
2. `RefreshSessionIssued`
3. `RefreshSessionRevoked`
4. `PhoneVerified`
5. `WechatIdentityBound`
`lib.rs` 保留当前服务、进程内仓储和文件持久化实现,但不再继续拥有上述命令、结果、错误、事件和领域值对象定义;公开 API 继续通过 `pub use application::* / commands::* / domain::* / errors::* / events::*` 导出,避免影响现有 BFF 调用。
## 边界说明
1. `module-auth` 承接账号、refresh session、短信验证码状态、微信 state 和微信绑定规则,不依赖 Axum、SpacetimeDB table API、HTTP status、真实短信 SDK、微信 HTTP 或 JWT/cookie 细节。
2. `platform-auth` 继续承接短信 provider、微信 OAuth provider、密码哈希、refresh token、JWT 和 cookie 等平台副作用。
3. `api-server` 只做请求解析、鉴权、DTO 映射、session cookie/JWT 编排和平台/领域服务装配;本次核查未发现 Auth route 需要复制领域状态机。
4. `spacetime-module/src/auth.rs` 当前仍是认证快照与正式表之间的 SpacetimeDB adapter不调用短信、微信、JWT、HTTP 或文件系统;本次只修正历史乱码中文注释和错误文案,不改变表结构或 procedure 签名。
5. `InMemoryAuthStore` 和文件持久化仍保留在 `lib.rs`,作为当前 Auth 运行支撑;总纲中“仓储剥离到 adapter 或测试支撑”的更彻底物理拆分可作为后续非 WP-A 阻塞项继续推进,但本次 WP-A 已完成 DDD 骨架归位和边界核查。
## 验收
```powershell
cargo fmt -p module-auth --manifest-path server-rs/Cargo.toml --check
cargo test -p module-auth --manifest-path server-rs/Cargo.toml
cargo check -p module-auth --manifest-path server-rs/Cargo.toml --all-features
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml
cargo test -p api-server auth --manifest-path server-rs/Cargo.toml
cargo test -p api-server wechat --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_A_AUTH_DOMAIN_VALUE_OBJECT_REFACTOR_2026-04-29.md server-rs/crates/module-auth/src/domain.rs server-rs/crates/module-auth/src/commands.rs server-rs/crates/module-auth/src/application.rs server-rs/crates/module-auth/src/errors.rs server-rs/crates/module-auth/src/events.rs server-rs/crates/module-auth/src/lib.rs server-rs/crates/module-auth/README.md server-rs/crates/spacetime-module/src/auth.rs docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md docs/technical/README.md
npm.cmd run api-server:maincloud
```
本次不发布 SpacetimeDB、不生成绑定、不修改 `migration.rs`原因是没有表结构、reducer、procedure 或绑定形状变更。

View File

@@ -0,0 +1,34 @@
# WP-BF 与 G2 迁移期口径清理
日期:`2026-04-30`
## 1. 本次目标
本次收口 `4.1 未完整收口内容整合清单``G2/跨包清理``过渡落位` 的残留命中,重点处理:
1. `module-big-fish` 已标记 `WP-BF` 完成,但 `lib.rs` 仍承载大量创作域类型、命令、校验和序列化规则。
2. `module-runtime``module-puzzle``module-assets` 只剩文件头迁移期口径,与当前真实边界不一致。
## 2. 已完成
1. `module-big-fish/src/domain.rs` 承接创作阶段、锚点、资产槽、草稿、会话、作品摘要、发布门禁和运行态领域类型。
2. `module-big-fish/src/commands.rs` 承接会话、消息、草稿、资产、发布、游玩记录和运行态输入 DTO。
3. `module-big-fish/src/application.rs` 承接锚点推断、默认草稿编译、资产覆盖、资产槽构造、字段校验、序列化与运行态真相源规则。
4. `module-big-fish/src/errors.rs` 承接应用错误、字段错误和中文错误文案。
5. `module-big-fish/src/lib.rs` 收口为模块声明、公开导出和测试,继续保持 `module_big_fish::*` 公开 API。
6. `module-runtime``module-puzzle``module-assets` 的文件头已从迁移期“过渡落位”口径改为当前领域边界口径。
## 3. 边界说明
1. 本次不改 Big Fish SpacetimeDB 表、procedure、API route、前端 client 或绑定 shape。
2. 本次不为 Assets 新增未消费 event API`module-assets/src/events.rs` 只修正口径。
3. 本次不把 Puzzle/API/前端未完成链路误标为完成,只清理 DDD 物理拆分和注释漂移。
## 4. 验证
```powershell
cargo test -p module-runtime -p module-puzzle -p module-assets -p module-big-fish --manifest-path server-rs\Cargo.toml
cargo fmt -p module-runtime -p module-puzzle -p module-assets -p module-big-fish --manifest-path server-rs\Cargo.toml --check
```
结果:通过;上述源码不再命中 `过渡落位`

View File

@@ -0,0 +1,84 @@
# server-rs DDD WP-BF Big Fish 运行态后端真相源关闭记录2026-04-29
## 1. 背景
`WP-BF Big Fish` 已完成发布门禁领域化,但运行态仍由前端本地规则推进,导致前端同时承担输入、实体移动、碰撞、合成、胜负结算和快照更新。按照本轮 DDD 重构边界,前端只负责表现和提交输入,正式运行态规则必须落到 `server-rs`
本次关闭目标是把 Big Fish 从“前端本地运行态”切到“Rust 领域规则 + SpacetimeDB 运行表 + API facade + 前端 client 接入”的单主链,同时移除 Big Fish works mapper 中的旧形状兼容解析。
## 2. 本次完成范围
1. `module-big-fish` 新增运行态领域快照:
- `BigFishRunStatus`
- `BigFishVector2`
- `BigFishRuntimeEntitySnapshot`
- `BigFishRuntimeSnapshot`
2. `module-big-fish` 新增运行态应用服务:
- `start_big_fish_run`
- `submit_big_fish_input`
- `serialize_runtime_snapshot`
- `deserialize_runtime_snapshot`
3. 运行态规则已由领域层统一负责:
- 初始己方和野生实体生成。
- 输入向量归一化。
- 领队、跟随者和野生实体移动。
- 同级收编、强弱碰撞、三合一升级。
- 离屏野生实体裁剪和补刷。
- 胜利、失败和结算事件。
4. `spacetime-module/src/big_fish/runtime.rs` 新增 Big Fish 运行态 procedure
- `start_big_fish_run`
- `get_big_fish_run`
- `submit_big_fish_input`
5. 新增 `big_fish_runtime_run` 表,保存 run 快照、最后输入方向、tick、归属用户和会话来源并已同步 `migration.rs` 与表目录。
6. `spacetime-client` 新增 Big Fish runtime typed facade 和 record mapper并重新生成 Rust 绑定。
7. `api-server` 新增鉴权路由:
- `POST /api/runtime/big-fish/sessions/{session_id}/runs`
- `GET /api/runtime/big-fish/runs/{run_id}`
- `POST /api/runtime/big-fish/runs/{run_id}/input`
8. `shared-contracts` 与前端 shared contract 新增 `BigFishRunResponse`、运行态实体/坐标 DTO 和 `SubmitBigFishInputRequest`
9. 前端 Big Fish 运行态 client 改为调用后端 run/input 接口,`PlatformEntryFlowShellImpl` 不再推进本地规则。
10. 删除 `src/services/big-fish-runtime/bigFishLocalRuntime.ts``/big-fish` 调试直达页降级回平台入口,避免继续暴露本地运行态主链。
## 3. 边界说明
1. 本次不接入 `server-node`、Express 或 PostgreSQL也不为旧 Node 兼容层保留双主链。
2. `spacetime-module` 只负责表读写、授权、事务编排和调用领域服务,不在 adapter 内复制 Big Fish 玩法规则。
3. `api-server` 只负责鉴权、请求校验、DTO 映射和 `spacetime-client` facade 调用,不直接访问 SpacetimeDB table。
4. 前端只提交方向输入并渲染后端返回快照,正式实体状态、胜负和事件日志都以后端响应为准。
5. Big Fish works mapper 已移除旧 JSON 兼容解析,后续要求 SpacetimeDB procedure 返回新的 `owner_user_id``play_count` 等稳定字段。
6. `big_fish_runtime_run` 是运行态快照表,不替代创作会话、资产槽或发布门禁真相;删除作品时同步清理来源会话下的 run。
## 4. 后续边界
1. Big Fish 运行态资源展示仍复用现有 session / work 的资产槽数据;若要让公开作品 run 响应直接携带资产包,应由后续契约任务单独扩展 DTO。
2. 真实 Maincloud 发布和订阅侧 smoke 仍归 `WP-V 全链验证与发布 smoke`
3. 若后续把运行态拆为更细表,而不是整份 `snapshot_json`,必须重新同步 `migration.rs`、绑定和 `SPACETIMEDB_TABLE_CATALOG.md`
## 5. 验收
关闭前必须执行:
```powershell
cargo test -p module-big-fish --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 check -p api-server --manifest-path server-rs/Cargo.toml
npm.cmd run test -- src/components/big-fish-runtime/BigFishRuntimeShell.test.tsx src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_BF_RUNTIME_BACKEND_TRUTH_2026-04-29.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md docs/technical/README.md docs/technical/SPACETIMEDB_TABLE_CATALOG.md server-rs/crates/module-big-fish/src/application.rs server-rs/crates/module-big-fish/src/domain.rs server-rs/crates/module-big-fish/src/commands.rs server-rs/crates/module-big-fish/src/errors.rs server-rs/crates/module-big-fish/src/events.rs server-rs/crates/module-big-fish/src/lib.rs server-rs/crates/spacetime-module/src/big_fish/runtime.rs server-rs/crates/spacetime-module/src/big_fish/tables.rs server-rs/crates/spacetime-module/src/big_fish/session.rs server-rs/crates/spacetime-module/src/migration.rs server-rs/crates/spacetime-client/src/big_fish.rs server-rs/crates/spacetime-client/src/mapper.rs server-rs/crates/spacetime-client/src/lib.rs server-rs/crates/shared-contracts/src/big_fish.rs server-rs/crates/api-server/src/big_fish.rs server-rs/crates/api-server/src/app.rs packages/shared/src/contracts/bigFish.ts src/services/big-fish-runtime/bigFishRuntimeClient.ts src/services/big-fish-runtime/index.ts src/components/platform-entry/PlatformEntryFlowShellImpl.tsx src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx src/BigFishPlaygroundApp.tsx
npm.cmd run api-server:maincloud
```
最终执行结果:
1. `cargo test -p module-big-fish --manifest-path server-rs/Cargo.toml` 通过6 个测试通过。
2. `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml` 通过。
3. `cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml` 通过。
4. `cargo check -p api-server --manifest-path server-rs/Cargo.toml` 通过,仅保留既有 `api-server/src/prompt/rpg/runtime_chat.rs` 未使用 helper warning。
5. `npm.cmd run check:server-rs-ddd` 通过。
6. `npm.cmd run test -- src/components/big-fish-runtime/BigFishRuntimeShell.test.tsx` 通过4 个测试通过;测试已改为真实定时等待,不再使用全局 fake timers。
7. `npm.cmd run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "public code search opens a published big fish work by BF code"` 通过,确认 BF 编号搜索进入后端 run 主链。
8. `npm.cmd run check:encoding -- ...` 对本次触达文件通过,收尾补充检查对 3 个文件通过。
9. `npm.cmd run api-server:maincloud` 已按常驻服务启动;`GET http://127.0.0.1:3100/healthz` 返回 `200`,验收后已停止本次启动进程。
补充说明:完整 `src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx` 当前单独运行仍有既有长流程用例失败,失败集中在 RPG/拼图/创作结果页路径,例如 `create tab opens compiled agent draft in result refinement page``published puzzle detail returns to the source platform tab``agent draft result test button enters current draft without publish gate`。这些失败在不并跑 Big Fish runtime 测试时也存在,未指向本次 WP-BF 运行态迁移文件,后续应由对应 RPG/Puzzle/Custom World 前端接线包继续收口。

View File

@@ -0,0 +1,43 @@
# WP-CW Custom World 动作与领域拆分收口
日期:`2026-04-30`
## 1. 本次目标
本次收口 `4.1 未完整收口内容整合清单``WP-CW Custom World` 的两个漂移点:
1. `module-custom-world` 仍由 `lib.rs` 承载主要领域类型、命令、错误和应用规则。
2. `spacetime-module/src/custom_world/mod.rs` 中多个 Agent action 仍走最小兼容占位。
## 2. 已完成
1. `module-custom-world/src/lib.rs` 已收口为模块声明、公开导出和测试,继续保持 `module_custom_world::*` 公开 API。
2. `src/domain.rs` 承接 Custom World / RPG Agent 枚举、进度常量、profile/session/card/gallery/publish gate 快照与结果类型。
3. `src/commands.rs` 承接 profile、library/gallery、Agent session/message/operation/action、published profile compile 和 publish world 输入 DTO。
4. `src/application.rs` 承接字段校验、默认 JSON、profile canonicalize、published profile compile 和 publish gate 相关纯规则。
5. `src/errors.rs` 承接 `CustomWorldFieldError` 与中文错误文案。
6. `src/events.rs` 承接 Custom World 领域事件与 payload struct避免 `spacetime-types` feature 下使用结构体式 enum 变体。
7. `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`
## 3. 边界说明
1. 本次不引入 LLM、图片生成、OSS 上传或外部网络副作用。
2. SpacetimeDB procedure/reducer 内只组合当前会话状态、payload、draft card、asset coverage 和 publish gate保持确定性。
3. 本次不改表结构、绑定 shape 或 `migration.rs`
4. 完整 profile/agent/draft/gallery/publish gate 全链仍按 `WP-CW/WP-ST/WP-SC/WP-API/WP-FE` 后续推进。
## 4. 验证
```powershell
cargo test -p module-custom-world --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml
```
结果:通过,`module-custom-world` 13 个单元测试通过,`spacetime-module` 编译通过Custom World 源码不再命中 `execute_placeholder_custom_world_action``最小兼容占位``过渡落位`

View File

@@ -0,0 +1,41 @@
# WP-CW Custom World 基础领域枚举归位切片
## 背景
`module-custom-world/src/lib.rs` 仍直接承载 Custom World 和 RPG Agent 的基础枚举、字符串口径与进度常量。随着 `custom_world` SpacetimeDB adapter 已完成根入口瘦身,领域 crate 也需要继续把纯领域对象迁入 `domain.rs`,避免后续 profile、agent session、draft card、gallery 与 publish gate 规则继续堆回根文件。
## 本次范围
1. 认领 `WP-CW Custom World` 的基础领域枚举归位切片。
2.`MAX_PROGRESS_PERCENT` 迁入 `module-custom-world/src/domain.rs`
3. 将 Custom World / RPG Agent 基础枚举迁入 `domain.rs`
- `CustomWorldPublicationStatus`
- `CustomWorldThemeMode`
- `CustomWorldGenerationMode`
- `CustomWorldSessionStatus`
- `RpgAgentStage`
- `RpgAgentMessageRole`
- `RpgAgentMessageKind`
- `RpgAgentOperationType`
- `RpgAgentOperationStatus`
- `RpgAgentDraftCardKind`
- `RpgAgentDraftCardStatus`
- `CustomWorldRoleAssetStatus`
4. 将这些枚举的 `as_str``CustomWorldThemeMode::from_client_str` 一并迁入 `domain.rs`
5. `lib.rs` 通过 `pub use domain::*` 保持既有 `module_custom_world::*` 公开 API。
## 边界
1. 本次不改 SpacetimeDB table、reducer、procedure、row mapper 或 `migration.rs`
2. 本次不改 `api-server``spacetime-client`、platform adapter 或前端。
3. 本次不移动 profile、agent session、draft card、publish gate 的结构体和校验函数,避免把大包拆分与本切片混在一起。
4. 本次不改变任何序列化字段、枚举字符串值或中文错误文案。
## 验收
```powershell
cargo fmt -p module-custom-world --manifest-path server-rs/Cargo.toml --check
cargo test -p module-custom-world --manifest-path server-rs/Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_CW_DOMAIN_ENUM_REHOME_2026-04-29.md server-rs/crates/module-custom-world/src/domain.rs server-rs/crates/module-custom-world/src/lib.rs docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md
```

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

@@ -0,0 +1,34 @@
# WP-FE-C RPG runtime shell 组件测试夹具接线切片
## 背景
`WP-FE-S``WP-FE-H` 已经把 RPG runtime story 读取侧收口到 `storySessionId` scoped projection client 与 hooks 网关。组件层仍不能迁移完整开局、动作结算等写接口,但 `RpgRuntimeShell` 组件测试夹具还保留旧 hook UI 形状,导致 `typecheck` 在组件测试文件中失败。
## 本次范围
1. 认领 `WP-FE-C Frontend components 接线` 中可并行的 RPG runtime shell 组件测试夹具接线切片。
2. 修正 `src/components/rpg-runtime-shell/RpgRuntimeShell.test.tsx`
- `StoryMoment` mock 使用当前 `text/options` 字段。
- `Character` mock 对齐当前角色类型,不再写入旧 `motivation/combatStyle/role/imageSrc/initialItems` 等字段。
- `npcUi/characterChatUi/inventoryUi/battleRewardUi/questUi/npcChatQuestOfferUi/goalUi` mock 对齐当前 hooks 暴露的稳定 UI 对象形状。
3. 保持 `RpgRuntimeShell` 正式组件行为不变,只消除测试夹具对旧组件接线形状的依赖。
## 2026-05-01 收尾切片
`WP-FE-S/H` 写侧切到新 story session scoped 主链后,本切片只确认组件层夹具继续消费 hooks 出口,不在组件中补业务规则:
1. `RpgRuntimeShell` 正式组件不拼接 `/api/runtime/story/*``/api/story/*` 路径。
2. 组件测试 mock 继续对齐 `hydratedSnapshot/nextStory` 与当前 UI 对象形状。
3. 不新增 UI 文案,不改视觉布局;服务端动作表现差异只通过 hooks 状态进入组件。
4. 旧 runtime story contract 和旧 alias 仍由 `WP-DEL` 统一删除。
## 验收
```powershell
npm.cmd run typecheck -- --pretty false
npm.cmd run test -- src/components/rpg-runtime-shell/RpgRuntimeShell.test.tsx
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_FE_C_RPG_RUNTIME_SHELL_TEST_FIXTURE_2026-04-29.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md src/components/rpg-runtime-shell/RpgRuntimeShell.test.tsx
git diff --check -- docs/technical/SERVER_RS_DDD_WP_FE_C_RPG_RUNTIME_SHELL_TEST_FIXTURE_2026-04-29.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md src/components/rpg-runtime-shell/RpgRuntimeShell.test.tsx
```
结果:`RpgRuntimeShell.test.tsx` 定向测试通过;编码检查和空白检查通过。`typecheck` 未全量通过,但不再报本切片文件,剩余阻塞来自既有非本切片文件 `src/data/sceneEncounterPreviews.ts``src/services/ai.ts`

View File

@@ -0,0 +1,39 @@
# WP-FE-H RPG runtime story 读取侧 hooks 接线切片
## 背景
`WP-FE-S` 已经补齐 story session 新主链 client并提供 `getRpgStoryRuntimeProjection` 读取 `/api/story/sessions/{storySessionId}/runtime-projection``WP-FE-H` 的完整 hooks 迁移仍依赖后端 session scoped 开局和动作结算写接口;但读取侧 option catalog 与继续游戏刷新已经具备稳定 projection contract可以先把 hooks 网关的读取语义收口到 projection client。
## 本次范围
1. 认领 `WP-FE-H Frontend hooks 迁移` 中可并行的 RPG runtime story 读取侧 hooks 接线切片。
2.`src/hooks/rpg-runtime-story/rpgRuntimeStoryGateway.ts` 的读取侧从历史别名 `getRpgRuntimeStoryState` 改为显式调用 `getRpgStoryRuntimeProjection`
3. `loadServerRuntimeOptionCatalog` 继续只读取服务端 option catalog不上传本地 `GameState` 参与动作合法性解析。
4. `resumeServerRuntimeStory` 继续保持本地已水合快照主体,只同步服务端 `runtimeSessionId / storySessionId / runtimeActionVersion` 和投影故事。
5. 更新 `runtimeStoryCoordinator.test.ts` mock 命名,确保 hooks 测试明确断言读取侧走 projection client。
6. 补齐 `src/hooks/useGameFlow.customWorld.test.tsx``beginRpgRuntimeStorySession` 测试桩,避免 hooks 全量测试在 Node 环境直接 fetch 相对路径,同时保持自定义世界开局 hooks 仍消费服务端快照。
## 2026-05-01 收尾切片
后端补齐 `/api/story/sessions/runtime``/api/story/sessions/{storySessionId}/actions/resolve`hooks 写侧同步收口:
1. `resolveServerRuntimeChoice` 不再调用旧 `/api/runtime/story/actions/resolve` client。
2. 正式动作只提交 `storySessionId/functionId/actionText/payload`,并从新 `StoryRuntimeProjectionResponse.gameState` 水合快照。
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`
## 验收
```powershell
npm.cmd run test -- src/hooks/rpg-runtime-story/runtimeStoryCoordinator.test.ts
npm.cmd run test -- src/hooks/useGameFlow.customWorld.test.tsx
npm.cmd run test -- src/hooks
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_FE_H_RPG_RUNTIME_STORY_HOOKS_PROJECTION_2026-04-29.md src/hooks/rpg-runtime-story/rpgRuntimeStoryGateway.ts src/hooks/rpg-runtime-story/inventoryActions.ts src/hooks/rpg-runtime-story/runtimeStoryCoordinator.test.ts src/hooks/useGameFlow.customWorld.test.tsx docs/technical/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md
git diff --check -- docs/technical/SERVER_RS_DDD_WP_FE_H_RPG_RUNTIME_STORY_HOOKS_PROJECTION_2026-04-29.md src/hooks/rpg-runtime-story/rpgRuntimeStoryGateway.ts src/hooks/rpg-runtime-story/inventoryActions.ts src/hooks/rpg-runtime-story/runtimeStoryCoordinator.test.ts src/hooks/useGameFlow.customWorld.test.tsx docs/technical/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md
```

View File

@@ -0,0 +1,58 @@
# WP-FE-S RPG runtime story client 迁移记录
## 背景
`WP-API` 已提供新主链读取接口:
```text
GET /api/story/sessions/{storySessionId}/runtime-projection
```
该接口返回 `StoryRuntimeProjectionResponse`,不再返回旧 runtime story 的 `snapshot / viewModel / presentation / patches` 组合。前端读取侧必须改用 `storySessionId`,不能继续把 `runtimeSessionId` 当成 story 会话主键。
## 本轮落地边界
已落地:
1.`packages/shared/src/contracts/story.ts` 补齐前端 story session / runtime projection 契约,字段对齐 Rust `shared-contracts/src/story.rs` 的 camelCase 回包。
2.`GameState`、快照水合类型与水合逻辑中新增 `storySessionId?: string | null`
3. `src/services/rpg-runtime/rpgRuntimeStoryClient.ts` 的读取侧改为:
- `getRuntimeStoryState({ storySessionId })` 请求 `/api/story/sessions/{storySessionId}/runtime-projection`
- `loadRuntimeInventoryView``StoryRuntimeProjectionResponse` 映射背包视图。
- 缺失 `storySessionId` 时直接抛出中文错误,不回退到 `runtimeSessionId`
4. `src/hooks/rpg-runtime-story/rpgRuntimeStoryGateway.ts` 的读取侧改为消费新投影:
- 选项目录来自 `projection.options`
- 继续游戏时保留本地已水合快照主体,只同步 `runtimeSessionId / storySessionId / runtimeActionVersion` 和展示故事。
5. `src/services/rpg-runtime/rpgRuntimeStoryClient.ts` 新增 story session 新主链 API client
- `beginStorySession` 请求 `POST /api/story/sessions`
- `continueStorySession` 请求 `POST /api/story/sessions/continue`
- `getStorySessionState` 请求 `GET /api/story/sessions/{storySessionId}/state`
- `getStoryRuntimeProjection` 请求 `GET /api/story/sessions/{storySessionId}/runtime-projection`
-`storySessionId` 做统一 trim 与空值中文错误,避免后续 hooks 继续散落路径常量和空 ID 分支。
6. `src/services/rpg-runtime/index.ts` 已导出新 client 函数与结果类型,供后续 `WP-FE-H` 迁 hook 时直接接入。
7. 为满足 `WP-FE-S``src/services` 全量验收,补回 `src/services/customWorldAgentGenerationProgress.ts` 缺失的“建立场景连接”阶段,使草稿生成进度重新对齐既有 13 步文档与测试口径。
## 2026-05-01 收尾切片
本次收尾把先前“等待后端写接口”的缺口正式关闭,执行口径如下:
1. 不恢复旧 `/api/runtime/story/*` compat route前端 runtime story 开局、动作结算和读取统一走 `/api/story/sessions*`
2. `/api/story/sessions/runtime` 作为新开局 BFF后端生成 `runtimeSessionId/storySessionId`,写入 SpacetimeDB `runtime_snapshot`,并返回可水合的 runtime projection。
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. `packages/shared/src/contracts/rpgRuntimeStoryState.ts` 中的 view model / presentation / patch 类型暂不物理删除,留给 `WP-DEL` 统一清理。
2. 后续更完整的 battle/forge/NPC/quest 跨域结算仍由 `WP-RS/WP-ST/WP-SC/WP-API` 增量增强,但前端不再回退旧 runtime story 写路径。
## 验收命令
```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
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,69 @@
# WP-PF 平台副作用错误分类收口记录2026-04-29
## 1. 背景
`WP-PF platform side effects` 负责承载 LLM、OSS、SMS、微信等外部副作用实现。当前 `platform-llm``platform-oss``platform-auth` 已经各自有错误枚举,但 `api-server` 后续接入时仍容易重复用字符串或具体枚举分支判断 HTTP 错误类别。
第一段切片已补平台错误分类基础设施。继续收口时,需要把 `api-server` 中已接入的平台副作用统一改为消费这些稳定分类,并把微信 OAuth HTTP provider 下沉到 `platform-auth`,让 `api-server` 只保留 BFF 编排、会话签发、redirect 与错误 envelope 映射。
## 2. 范围
允许修改:
1. `server-rs/crates/platform-llm/src/lib.rs`
2. `server-rs/crates/platform-oss/src/lib.rs`
3. `server-rs/crates/platform-auth/src/lib.rs`
4. `server-rs/crates/api-server/src/platform_errors.rs`
5. `server-rs/crates/api-server/src/llm.rs`
6. `server-rs/crates/api-server/src/assets.rs`
7. 已经直接映射 OSS 错误的资产相关 BFF 模块
8. `server-rs/crates/api-server/src/state.rs`
9. `server-rs/crates/api-server/src/wechat_auth.rs`
10. `server-rs/crates/api-server/src/wechat_provider.rs`
11. 本文档
12. 全局任务清单进度记录
禁止修改:
1. `spacetime-module/src/**`
2. `spacetime-client/src/**`
3. `module-*`
4. 前端 services/hooks/components
5. SpacetimeDB table / reducer / procedure / migration
6. 玩法状态机和领域规则
## 3. 设计
每个 platform crate 暴露自己的错误分类枚举:
1. `LlmErrorKind`
2. `OssErrorKind`
3. `AuthPlatformErrorKind`
并在既有错误枚举上增加 `kind()` 方法。分类只表达 adapter 可消费的稳定错误类别,不承载业务状态机,也不直接绑定 HTTP status。
`api-server` 增加内部 `platform_errors` 模块,负责把平台错误分类映射到 HTTP status、统一 provider details 与中文 envelope。映射原则
1. `InvalidRequest` / `InvalidConfig` 等本地配置或请求错误不再散落在业务 route 中重复 match。
2. `ObjectNotFound` 稳定映射为 `404`
3. LLM 上游 `429` 保留 `429`,其他上游、网络、序列化、签名类错误映射为 `502`
4. SMS/微信 provider 错误统一走 `platform-auth` 错误分类,领域态错误仍由 `module-auth` 自己的应用错误映射。
5. `platform-*` 不绑定 HTTP status不生成 Axum response不持有玩法领域状态。
微信 OAuth provider 的 HTTP 请求、授权 URL 拼接、mock provider 和回调资料解析归入 `platform-auth`。由于 `platform-auth` 不能依赖 `module-auth`,微信 provider 输出独立的 `WechatIdentityProfile``api-server` 在 BFF 边界把它转换为 `module_auth::WechatIdentityProfile` 后再调用领域服务。
图像/视频模型直连接口仍分布在角色形象、角色动画、拼图和自定义世界资产模块中,当前属于视觉资产生成专项遗留,不在本次 WP-PF LLM/OSS/SMS/微信统一 adapter 收口内;后续若新建 image/video platform crate需要另立工作包迁移。
## 4. 验收
```powershell
cargo test -p platform-llm -p platform-oss -p platform-auth --manifest-path server-rs/Cargo.toml
cargo fmt -p platform-llm -p platform-oss -p platform-auth --manifest-path server-rs/Cargo.toml --check
cargo test -p api-server platform_errors --manifest-path server-rs/Cargo.toml
cargo test -p api-server llm --manifest-path server-rs/Cargo.toml
cargo test -p api-server wechat --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_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/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
npm.cmd run api-server:maincloud
```

View File

@@ -0,0 +1,43 @@
# WP-PZ Puzzle 基础领域常量与枚举归位切片
## 背景
`module-puzzle/src/lib.rs` 当前仍承载 Puzzle Agent、作品发布和运行态拼图的基础枚举、ID 前缀、标签数量规则与洗牌尝试次数。随着 DDD 骨架已经具备 `domain.rs``commands.rs``application.rs``events.rs``errors.rs`,本切片先把纯领域常量和基础枚举迁入 `domain.rs`,避免后续 Agent session、work profile、runtime run 与排行榜规则继续堆回根文件。
## 本次范围
1. 认领 `WP-PZ Puzzle` 的基础领域常量与枚举归位切片。
2. 将以下常量迁入 `module-puzzle/src/domain.rs`
- `PUZZLE_AGENT_SESSION_ID_PREFIX`
- `PUZZLE_AGENT_MESSAGE_ID_PREFIX`
- `PUZZLE_PROFILE_ID_PREFIX`
- `PUZZLE_RUN_ID_PREFIX`
- `PUZZLE_MIN_TAG_COUNT`
- `PUZZLE_MAX_TAG_COUNT`
- `PUZZLE_INITIAL_SHUFFLE_ATTEMPTS`
3. 将以下基础枚举迁入 `domain.rs`
- `PuzzleAgentStage`
- `PuzzleAnchorStatus`
- `PuzzleAgentMessageRole`
- `PuzzleAgentMessageKind`
- `PuzzlePublicationStatus`
- `PuzzleRuntimeLevelStatus`
4. 将这些枚举的 `as_str` 方法一并迁入 `domain.rs`
5. `lib.rs` 通过 `pub use domain::*` 保持既有 `module_puzzle::*` 公开 API。
## 边界
1. 本次不改 SpacetimeDB table、reducer、procedure、row mapper 或 `migration.rs`
2. 本次不改 `api-server``spacetime-client`、platform adapter 或前端。
3. 本次不移动 Agent session、work profile、runtime run、leaderboard 的结构体和校验函数,避免把大包拆分与本切片混在一起。
4. 本次不改变任何序列化字段、枚举字符串值、标签数量规则、洗牌规则或中文错误文案。
## 验收
```powershell
cargo fmt -p module-puzzle --manifest-path server-rs/Cargo.toml --check
cargo test -p module-puzzle --manifest-path server-rs/Cargo.toml
cargo check -p module-puzzle --features spacetime-types --target wasm32-unknown-unknown --manifest-path server-rs/Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_PZ_DOMAIN_ENUM_REHOME_2026-04-29.md server-rs/crates/module-puzzle/src/domain.rs server-rs/crates/module-puzzle/src/lib.rs server-rs/crates/module-puzzle/README.md docs/technical/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md
```

View File

@@ -0,0 +1,33 @@
# WP-PZ Puzzle 领域类型与规则拆分切片
## 背景
上一轮 `WP-PZ Puzzle` 已经把基础常量与枚举迁入 `module-puzzle/src/domain.rs`,但 `module-puzzle/src/lib.rs` 仍然承载 Agent 会话快照、作品 profile、运行态棋盘、procedure 输入、procedure 返回、字段错误与全部纯规则函数。
按照 `SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md``module-puzzle` 应继续把纯领域内容落入 `domain.rs``commands.rs``application.rs``events.rs``errors.rs`,让根入口只保留模块声明和公开导出。
## 本次范围
1. 将 Puzzle Agent、锚点包、草稿、作品 profile、运行态棋盘、排行榜等纯领域类型迁入 `module-puzzle/src/domain.rs`
2. 将 SpacetimeDB procedure/reducer 写入输入迁入 `module-puzzle/src/commands.rs`
3. 将 procedure 返回包装和拼图纯规则函数迁入 `module-puzzle/src/application.rs`
4.`PuzzleFieldError` 和中文错误文案迁入 `module-puzzle/src/errors.rs`
5.`module-puzzle/src/events.rs` 增加最小 `PuzzleDomainEvent`,先表达草稿变化、作品发布和运行态推进事实。
6. `module-puzzle/src/lib.rs` 只保留 DDD 子模块声明和 `pub use`,保持既有 `module_puzzle::*` 外部 API。
## 边界
1. 本次不改 SpacetimeDB table、reducer、procedure、row mapper 或 `migration.rs`
2. 本次不改 `api-server``spacetime-client`、platform adapter 或前端。
3. 本次不改变序列化字段、procedure 输入结构、procedure 返回 JSON 包装、标签数量规则、洗牌规则、移动/合并/拆分语义或中文错误文案。
4. 本次新增的 `PuzzleDomainEvent` 只作为领域事件落位,不接入 `spacetime-module` event table。
## 验收
```powershell
cargo fmt -p module-puzzle --manifest-path server-rs/Cargo.toml --check
cargo test -p module-puzzle --manifest-path server-rs/Cargo.toml
cargo check -p module-puzzle --features spacetime-types --target wasm32-unknown-unknown --manifest-path server-rs/Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_PZ_DOMAIN_SPLIT_2026-04-29.md server-rs/crates/module-puzzle/src/lib.rs server-rs/crates/module-puzzle/src/domain.rs server-rs/crates/module-puzzle/src/commands.rs server-rs/crates/module-puzzle/src/application.rs server-rs/crates/module-puzzle/src/events.rs server-rs/crates/module-puzzle/src/errors.rs server-rs/crates/module-puzzle/README.md docs/technical/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_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,41 @@
# WP-RPG Combat 基础领域常量与枚举归位切片
## 背景
`WP-RPG Gameplay 域` 覆盖 combat、inventory、npc、progression、quest、runtime-item、story 等多个玩法 crate。本次选择其中最小且可并行的 `module-combat` 切片:`module-combat/src/lib.rs` 仍直接承载战斗 ID 前缀、版本/伤害常量、旧攻击 function 列表以及基础枚举。随着 DDD 骨架已经具备 `domain.rs`,这些纯领域对象应先归位到 `domain.rs`,为后续拆分命令、错误、应用结果和跨域事件留出边界。
## 本次范围
1. 认领 `WP-RPG Gameplay 域` 的 combat 基础领域常量与枚举归位切片。
2. 将以下常量迁入 `module-combat/src/domain.rs`
- `BATTLE_STATE_ID_PREFIX`
- `INITIAL_BATTLE_VERSION`
- `BASIC_FIGHT_COUNTER_RATIO`
- `MIN_FIGHT_COUNTER_DAMAGE`
- `SPAR_MIN_HP`
- `LEGACY_ATTACK_FUNCTION_IDS`
3. 将以下基础枚举迁入 `domain.rs`
- `BattleMode`
- `BattleStatus`
- `CombatOutcome`
4. 将这些枚举的 `as_str` 方法一并迁入 `domain.rs`
5. `lib.rs` 通过 `pub use domain::*` 保持既有 `module_combat::*` 公开 API。
6. `module-combat``spacetime-types` feature 同步启用 `module-runtime-item/spacetime-types`,确保战斗快照里引用的 `RuntimeItemRewardItemSnapshot` 在 wasm 目标下具备 SpacetimeDB 类型派生。
## 边界
1. 本次不改 SpacetimeDB table、reducer、procedure、row mapper 或 `migration.rs`
2. 本次不改 `api-server``spacetime-client`、platform adapter 或前端。
3. 本次不移动 `BattleStateInput``BattleStateSnapshot``ResolveCombatActionInput``CombatFieldError` 和战斗结算函数,避免把常量枚举归位与大包拆分混在一起。
4. 本次不改变任何战斗数值、支持的 function id、枚举字符串值或中文错误文案。
5. Cargo feature 传播仅用于修复 `spacetime-types` 组合编译,不引入新依赖路径或运行时行为。
## 验收
```powershell
cargo fmt -p module-combat --manifest-path server-rs/Cargo.toml --check
cargo test -p module-combat --manifest-path server-rs/Cargo.toml
cargo check -p module-combat --features spacetime-types --target wasm32-unknown-unknown --manifest-path server-rs/Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_RPG_COMBAT_DOMAIN_ENUM_REHOME_2026-04-29.md server-rs/crates/module-combat/Cargo.toml server-rs/crates/module-combat/src/domain.rs server-rs/crates/module-combat/src/lib.rs server-rs/crates/module-combat/README.md docs/technical/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md
```

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

@@ -0,0 +1,44 @@
# WP-RPG Gameplay 子域领域拆分收口2026-04-30
## 1. 收口目标
本切片关闭 `SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md` 中 RPG 子域的 DDD 物理拆分漂移:
1. `module-combat``module-inventory``module-npc``module-quest``module-runtime-item` 的真实规则仍主要集中在 `lib.rs`
2. 上述 crate 的 `domain / commands / application / events / errors` 文件仍停留在“过渡落位”口径。
3. `module-progression` 已在前一切片完成物理拆分,本切片把 RPG 子域剩余同类壳层一起收口。
本次只做纯领域文件拆分、最小领域事件补位和文档对齐,不修改 SpacetimeDB 表结构、reducer/procedure 签名、绑定 shape、Axum route 或前端契约。
## 2. 已完成内容
1. `module-combat` 将战斗输入、战斗快照、行动结算、错误和战斗领域事件拆入对应 DDD 文件,`lib.rs` 只保留公开导出和测试。
2. `module-inventory` 将背包槽、物品快照、背包 mutation 输入、状态投影、应用规则、错误和背包领域事件拆入对应 DDD 文件。
3. `module-npc` 将 NPC 状态、关系、立场、互动输入、互动结算、错误和 NPC 领域事件拆入对应 DDD 文件。
4. `module-quest` 将任务模型、任务命令、任务状态流转、错误和任务领域事件拆入对应 DDD 文件。
5. `module-runtime-item` 将宝箱奖励模型、宝箱结算输入、奖励到背包映射、错误和运行时物品领域事件拆入对应 DDD 文件。
6. 五个 crate 的 `lib.rs` 均收口为 `mod` 声明、`pub use` 和原有测试,继续保持现有 `module_*::*` 公开 API。
7. RPG 六个子域源码已不再命中 `过渡落位`
## 3. 边界
1. 本切片不新增 `inventory_use`、完整掉落、好感、任务信号、story AI 续写、多目标战斗或完整 build/cooldown 真相建模。
2. 本切片不把任务货币、好感、情报统一发放提前塞进 `module-quest`,也不把背包落库塞进 `module-runtime-item`
3. 跨域副作用仍由 `spacetime-module` 事务 adapter、`spacetime-client` facade、`api-server` BFF 和前端主链分批接入。
4. 完整 story action 写侧、inventory action、NPC interaction、forge/battle/quest 组合结算继续跟随 `WP-RS/WP-ST/WP-SC/WP-API/WP-FE`
## 4. 验收
已执行:
```powershell
cargo fmt -p module-combat --manifest-path server-rs\Cargo.toml --check
cargo fmt -p module-inventory --manifest-path server-rs\Cargo.toml --check
cargo fmt -p module-npc --manifest-path server-rs\Cargo.toml --check
cargo fmt -p module-quest --manifest-path server-rs\Cargo.toml --check
cargo fmt -p module-runtime-item --manifest-path server-rs\Cargo.toml --check
cargo test -p module-combat -p module-inventory -p module-npc -p module-progression -p module-quest -p module-runtime-item --manifest-path server-rs\Cargo.toml
cargo check -p module-combat -p module-inventory -p module-npc -p module-progression -p module-quest -p module-runtime-item --manifest-path server-rs\Cargo.toml
```
结果通过。RPG 六个子域共 39 个单元测试通过。

View File

@@ -0,0 +1,39 @@
# WP-RPG module-progression 领域拆分收口2026-04-30
## 1. 收口目标
本切片关闭 `SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md``module-progression` 的 DDD 物理拆分漂移:
1. `domain / commands / application / events / errors` 文件仍停留在“过渡落位”口径。
2. 玩家等级、章节预算、章节账本、章节自动定级和敌对奖励规则集中在 `lib.rs`
本次只做纯领域分层和文档对齐,不修改 SpacetimeDB 表结构,不触碰 `migration.rs`,不新增 HTTP、LLM、OSS 或前端接线。
## 2. 已完成内容
1. `src/domain.rs` 收口等级常量、玩家成长快照、章节成长快照、章节节奏、实体定级角色和定级来源。
2. `src/commands.rs` 收口玩家成长查询/授予经验、章节预算、章节账本和章节自动定级输入。
3. `src/application.rs` 收口经验曲线、等级解析、玩家成长快照构造、章节预算、章节账本、章节自动定级、敌对生命值和经验奖励规则。
4. `src/events.rs` 收口玩家经验授予、章节账本应用和章节自动定级解析领域事件。
5. `src/errors.rs` 收口 `ProgressionFieldError` 与中文错误文案。
6. `src/lib.rs` 收口为模块声明和公开导出,继续保持 `module_progression::*` 公开 API。
7. `module-progression/README.md` 更新为当前真实边界,明确 DDD 物理拆分已经收口。
## 3. 边界
1. 本切片不改变 `player_progression``chapter_progression` 的 SpacetimeDB row shape、reducer/procedure 签名或绑定生成结果。
2. 本切片不把 `custom-world` 章节蓝图编译、`repeatPenalty`、超预算衰减和完整章节偏差审计提前迁入。
3. 任务、战斗、NPC 和章节成长联动继续通过领域事件与 `spacetime-module` adapter 编排,不让单个 RPG 子域互相直连。
4. 后续完整成长闭环仍随 `WP-CW/WP-RPG/WP-ST/WP-SC/WP-API` 分批推进。
## 4. 验收
已执行:
```powershell
cargo fmt -p module-progression --manifest-path server-rs\Cargo.toml --check
cargo test -p module-progression --manifest-path server-rs\Cargo.toml
cargo check -p module-progression --manifest-path server-rs\Cargo.toml
```
结果:通过,`module-progression` 7 个单元测试通过。

View File

@@ -0,0 +1,38 @@
# WP-RPG module-story 领域拆分收口2026-04-29
## 1. 收口目标
本切片关闭 `SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md``module-story` 的两类漂移:
1. README 仍写“目录占位”和旧 `/api/runtime/story/*` 兼容链路。
2. `domain / commands / application / events / errors` 文件仍停在“过渡落位”注释,真实规则集中在 `lib.rs`
本次只做物理分层和文档对齐,不新增 story action 写接口,不修改 SpacetimeDB 表结构,不触碰 `migration.rs`
## 2. 已完成内容
1. `src/domain.rs` 收口剧情会话领域快照、状态、ID 前缀和 `generate_story_session_id`
2. `src/commands.rs` 收口 `StorySessionInput``StoryContinueInput``StorySessionStateInput`、输入构造和字段校验。
3. `src/events.rs` 收口 `StoryEventKind``StoryEventSnapshot`、开局事件和事件 ID 生成。
4. `src/application.rs` 收口会话快照构造、续写应用服务、procedure result 类型和只读 record mapper。
5. `src/errors.rs` 收口 `StorySessionFieldError` 与中文错误文案。
6. `src/lib.rs` 只保留模块声明和 `pub use`,继续保持 `module_story::*` 公开 API。
7. `module-story/README.md` 改为当前真实边界,明确不恢复旧 `/api/runtime/story/*` 兼容路由。
## 3. 边界
1. 本切片不改变 `story_session``story_event` 的 SpacetimeDB row shape、reducer/procedure 签名或前端 DTO。
2. 本切片不接入 LLM、SSE、HTTP route 或前端 hooks。
3. 后续完整动作结算仍等待 `WP-RS/WP-ST/WP-SC/WP-API/WP-FE` 继续推进。
4. `module-story` 只承载纯 story session 规则;运行态投影、背包/NPC/战斗/任务联动继续由相邻领域模块和 adapter 编排。
## 4. 验收
已执行:
```powershell
cargo test -p module-story --manifest-path server-rs\Cargo.toml
cargo check -p module-story --manifest-path server-rs\Cargo.toml
```
结果通过8 个单元测试通过。

View File

@@ -0,0 +1,55 @@
# WP-RS Runtime Story Compat 残留审计切片
## 背景
`WP-RS Runtime Story 去兼容层` 已完成 crate 迁名、旧 HTTP compat 路由下线、runtime projection 契约/API/读取侧迁移等多轮推进。2026-05-01 收尾后,运行时开局和动作写侧也已迁到 story session scoped route当前仍存在一些 `compat` 或旧 `/api/runtime/story/*` 命名残留,需要区分“可以立即清理的工程语义残留”和“历史展示 DTO / 旧命名的物理删除窗口”。
本次切片只处理前者,并冻结后者的交接清单。
## 本次范围
允许修改:
1. `server-rs/crates/module-runtime-story/src/**` 中只描述历史兼容阶段的注释。
2. `server-rs/crates/module-runtime-story/README.md`
3. 本文档
4. `docs/technical/README.md`
5. `docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`
禁止修改:
1. `src/services/rpg-runtime/rpgRuntimeStoryClient.ts`
2. `src/hooks/rpg-runtime-story/**`
3. `packages/shared/src/contracts/rpgRuntimeStoryState.ts`
4. `server-rs/crates/shared-contracts/src/runtime_story.rs`
5. `api-server` route 挂载和 BFF 行为
6. SpacetimeDB 表、procedure、绑定和 `migration.rs`
## 本次处理
1. 清理 `module-runtime-story` 运行代码注释中的 `compat` 定位,将 crate 口径改为 runtime story 主链纯规则收口。
2. 保留“历史 payload”相关说明因为这些字段仍是实际输入兼容范围不属于旧层命名。
3. 更新 `module-runtime-story/README.md`,明确旧 `/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/hooks/rpg-runtime-story/**` 仍保留部分旧命名兼容注释,但运行写链路已经通过 `storySessionId` scoped client不再调用旧 `/api/runtime/story/*`
3. 历史文档中仍会记录旧 compat 路由阶段,用于审计时间线;当前执行入口以 `SERVER_RS_DDD_WP_RS_RUNTIME_STORY_CLOSURE_2026-05-01.md` 为准。
## 后续边界
1. 不恢复旧前端写 client也不重新挂载旧 `/api/runtime/story/*`
2. `WP-DEL` 只能在确认 story battle 等展示语义不再依赖旧 DTO 后,删除 `RuntimeStoryActionResponse` 等历史 contract。
3. 历史文档清理只改当前状态说明,不抹掉已执行过的审计记录。
## 验收
```powershell
cargo fmt -p module-runtime-story --manifest-path server-rs/Cargo.toml --check
cargo test -p module-runtime-story --manifest-path server-rs/Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_RS_COMPAT_RESIDUE_AUDIT_2026-04-29.md docs/technical/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md server-rs/crates/module-runtime-story/README.md server-rs/crates/module-runtime-story/src/domain.rs server-rs/crates/module-runtime-story/src/commands.rs server-rs/crates/module-runtime-story/src/errors.rs server-rs/crates/module-runtime-story/src/events.rs server-rs/crates/module-runtime-story/src/core.rs server-rs/crates/module-runtime-story/src/game_state.rs server-rs/crates/module-runtime-story/src/battle.rs server-rs/crates/module-runtime-story/src/forge.rs server-rs/crates/module-runtime-story/src/forge_actions.rs server-rs/crates/module-runtime-story/src/npc_support.rs
```

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

@@ -0,0 +1,38 @@
# WP-RS module-runtime-story 领域拆分收口2026-04-30
## 1. 收口目标
本切片关闭 `module-runtime-story` 顶层 DDD 文件仍停留在“过渡落位”的漂移:
1. 顶层 `StoryResolution``RuntimeStoryActionResponseParts`、NPC 任务上下文、常量和 helper 仍集中在 `lib.rs`
2. `domain / commands / application / events / errors` 文件没有承载真实类型或规则。
本次只做顶层纯规则拆分和文档对齐,不迁移旧 `/api/runtime/story/*` 写侧接口,不修改 SpacetimeDB 表结构、reducer/procedure、BFF route 或前端 client。
## 2. 已完成内容
1. `src/domain.rs` 收口 runtime story 顶层常量、`StoryResolution`、生成故事 payload、当前 NPC 任务上下文和待接任务上下文。
2. `src/commands.rs` 收口 `resolve_action_text`,固定从 action payload 读取展示文本的写入命令口径。
3. `src/application.rs` 收口 `RuntimeStoryActionResponseParts``simple_story_resolution``build_status_patch``current_world_type`
4. `src/errors.rs` 补入 `RuntimeStoryRuleError`,用于表达运行时剧情纯规则错误。
5. `src/events.rs` 补入 `RuntimeStoryDomainEvent`,用于表达快照变化、战斗表现变化和跨域同步待处理事实。
6. `src/lib.rs` 收口为模块声明、公开导出和既有子模块 re-export继续保持现有 `module_runtime_story::*` 公开 API。
## 3. 边界
1. 本切片不改 battle、forge、NPC、quest、presentation 等大模块内部逻辑。
2. 本切片不恢复旧 `/api/runtime/story/*`,也不新增 session scoped 写 route。
3. `RuntimeStoryActionResponse`、旧前端写 client 和旧 contract 删除仍等待后续 `WP-FE-S/WP-FE-H/WP-FE-C/WP-DEL`
4. 完整 story action 写侧、inventory action、NPC interaction、forge/battle/quest 组合结算仍需跟随 `WP-ST/WP-SC/WP-API` 接入。
## 4. 验收
已执行:
```powershell
cargo fmt -p module-runtime-story --manifest-path server-rs\Cargo.toml --check
cargo test -p module-runtime-story --manifest-path server-rs\Cargo.toml
cargo check -p module-runtime-story --manifest-path server-rs\Cargo.toml
```
结果:通过,`module-runtime-story` 8 个单元测试通过。

View File

@@ -0,0 +1,130 @@
# WP-RT Adapter/API 收口落地说明
## 背景
`WP-RT Runtime/Profile/Save` 已完成 runtime settings、snapshot/profile/save archive 类型、错误层、命令构造和应用记录投影拆分。剩余风险集中在 Adapter/API 层仍保留部分纯规则,以及 profile 旧兼容路径继续挂载,容易让后续前端或 BFF 再走 `/api/runtime/profile/*` 旧入口。
本次收口目标是继续遵循 DDD 边界:`module-runtime` 承载 runtime/profile/save 的纯规则和字段错误,`spacetime-module` 只保留 SpacetimeDB table、事务读写和 row mapper`api-server` 只负责 HTTP/BFF 映射,前端请求层改用新的 profile API。
## 本次范围
允许修改:
1. `server-rs/crates/module-runtime/src/domain.rs`
2. `server-rs/crates/module-runtime/src/errors.rs`
3. `server-rs/crates/module-runtime/src/commands.rs`
4. `server-rs/crates/module-runtime/src/application.rs`
5. `server-rs/crates/module-runtime/src/lib.rs`
6. `server-rs/crates/spacetime-module/src/runtime/profile.rs`
7. `server-rs/crates/api-server/src/app.rs`
8. `server-rs/crates/api-server/src/runtime_save.rs`
9. `server-rs/crates/api-server/src/runtime_profile.rs`
10. `server-rs/crates/api-server/src/runtime_browse_history.rs`
11. `src/services/rpg-runtime/rpgRuntimeRequest.ts`
12. `src/services/rpg-entry/rpgProfileClient.test.ts`
13. `src/services/rpg-entry/rpgEntryClients.routing.test.ts`
14. 相关 README、技术文档和全局任务清单
禁止修改:
1. SpacetimeDB 表结构和 `migration.rs`
2. RPG story / runtime story 玩法规则
3.`server-node` / PostgreSQL 兼容逻辑
4. 非 WP-RT 并行包文件
## 设计
### 1. checkpoint 规则下沉
`api-server/src/runtime_save.rs` 不再本地维护 checkpoint 的 sessionId 校验、预览/测试态拒绝和 runtimeStats 时间水位刷新规则。
新增或迁入 `module-runtime`
1. `RuntimeSaveCheckpointInput`
2. `RuntimeSaveCheckpointSnapshotUpdate`
3. `build_runtime_save_checkpoint_input`
4. `build_runtime_save_checkpoint_update`
5. `refresh_runtime_snapshot_play_time`
6. `is_non_persistent_runtime_snapshot`
7. checkpoint 相关 `RuntimeProfileFieldError`
`api-server` 只把 HTTP payload 转为领域输入,并把领域错误映射为 runtime-save 的 API envelope。
### 2. profile/save archive 投影 meta 规则下沉
`spacetime-module/src/runtime/profile.rs` 不再本地维护 save archive/world meta 的 JSON 解析规则。
新增或迁入 `module-runtime`
1. `RuntimeProfileWorldSnapshotMeta`
2. `RuntimeProfileSaveArchiveMeta`
3. `resolve_runtime_profile_world_snapshot_meta`
4. `resolve_runtime_profile_save_archive_meta`
5. `read_runtime_json_non_negative_u64`
6. `read_runtime_json_string_field`
7. `build_runtime_builtin_world_title`
`spacetime-module` 继续负责读取 snapshot、写入 dashboard / played world / save archive 表,但 meta 判断和默认标题、摘要兜底由领域模块统一维护。
### 3. profile 剩余纯规则下沉
本次继续把留在 SpacetimeDB adapter 中的纯规则收回 `module-runtime`
1. played world、snapshot wallet ledger、save archive、recharge order、recharge wallet ledger、redeem usage、redeem ledger 等 ID 生成规则。
2. 首充光点奖励计算。
3. 会员购买续期时间计算。
4. 邀请码 deterministic 生成、邀请链接、每日奖励窗口和邀请人奖励上限判断。
5. 兑换码 public / unique / private 模式使用资格校验。
6. 钱包正负 delta 转换、余额溢出和余额不足校验。
`spacetime-module` 中仍保留必须依赖表状态的逻辑:查找已有邀请码、统计当天奖励次数、读取/更新 wallet dashboard、写入 wallet ledger、membership、recharge order、redeem usage 和 referral relation。
### 4. profile 旧兼容路径移除
`api-server/src/app.rs` 移除 `/api/runtime/profile/*` 旧兼容挂载,只保留 `/api/profile/*` 新主路径。
保留的新路径:
1. `/api/profile/browse-history`
2. `/api/profile/dashboard`
3. `/api/profile/wallet-ledger`
4. `/api/profile/recharge-center`
5. `/api/profile/recharge/orders`
6. `/api/profile/referrals/invite-center`
7. `/api/profile/referrals/redeem-code`
8. `/api/profile/redeem-codes/redeem`
9. `/api/profile/save-archives`
10. `/api/profile/save-archives/{world_key}`
新增 `runtime_profile_legacy_routes_are_not_mounted` 测试,确认 `/api/runtime/profile/*` 旧路径返回 `404`
### 5. 前端 profile 请求路径对齐
`src/services/rpg-runtime/rpgRuntimeRequest.ts` 对以 `/profile/` 开头的 runtime request 直接发送到 `/api/profile/*`,其他 runtime 路径继续发送到 `/api/runtime/*`
`rpgProfileClient` 与 entry routing 测试同步改为断言 `/api/profile/*`
## 边界说明
1. 本次没有新增、删除或调整 SpacetimeDB 表字段,因此不修改 `migration.rs`
2. 本次没有改 reducer/procedure 对外签名,也没有改 generated binding。
3. 本次删除的是 HTTP 兼容挂载,不保留 `/api/runtime/profile/*` fallback。
4. 本次不把 SpacetimeDB 表查询搬进 `module-runtime`;领域模块只接收纯输入并返回纯结果。
5. 本次不处理 runtime story / RPG story 规则;相关内容仍归 `WP-RS``WP-RPG`
## 验收
```powershell
cargo fmt -p module-runtime -p spacetime-module -p api-server --manifest-path server-rs/Cargo.toml --check
cargo test -p module-runtime --manifest-path server-rs/Cargo.toml
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml
cargo test -p api-server runtime_profile --manifest-path server-rs/Cargo.toml
cargo test -p api-server runtime_browse_history --manifest-path server-rs/Cargo.toml
cargo test -p api-server runtime_snapshot --manifest-path server-rs/Cargo.toml
cargo test -p api-server profile_save_archives --manifest-path server-rs/Cargo.toml
npm.cmd run test -- src/services/rpg-entry/rpgProfileClient.test.ts src/services/rpg-entry/rpgEntryClients.routing.test.ts
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_RT_ADAPTER_API_CLOSURE_2026-04-29.md server-rs/crates/module-runtime/src/domain.rs server-rs/crates/module-runtime/src/errors.rs server-rs/crates/module-runtime/src/commands.rs server-rs/crates/module-runtime/src/application.rs server-rs/crates/module-runtime/src/lib.rs server-rs/crates/module-runtime/README.md server-rs/crates/spacetime-module/src/runtime/profile.rs server-rs/crates/api-server/src/app.rs server-rs/crates/api-server/src/runtime_save.rs server-rs/crates/api-server/src/runtime_profile.rs server-rs/crates/api-server/src/runtime_browse_history.rs src/services/rpg-runtime/rpgRuntimeRequest.ts src/services/rpg-entry/rpgProfileClient.test.ts src/services/rpg-entry/rpgEntryClients.routing.test.ts docs/technical/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md
git diff --check -- docs/technical/SERVER_RS_DDD_WP_RT_ADAPTER_API_CLOSURE_2026-04-29.md server-rs/crates/module-runtime/src/domain.rs server-rs/crates/module-runtime/src/errors.rs server-rs/crates/module-runtime/src/commands.rs server-rs/crates/module-runtime/src/application.rs server-rs/crates/module-runtime/src/lib.rs server-rs/crates/module-runtime/README.md server-rs/crates/spacetime-module/src/runtime/profile.rs server-rs/crates/api-server/src/app.rs server-rs/crates/api-server/src/runtime_save.rs server-rs/crates/api-server/src/runtime_profile.rs server-rs/crates/api-server/src/runtime_browse_history.rs src/services/rpg-runtime/rpgRuntimeRequest.ts src/services/rpg-entry/rpgProfileClient.test.ts src/services/rpg-entry/rpgEntryClients.routing.test.ts docs/technical/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md
npm.cmd run api-server:maincloud
```

View File

@@ -0,0 +1,47 @@
# WP-RT 应用记录投影拆分落地说明
## 背景
`module-runtime` 已完成领域类型、错误类型和命令构造拆分。根入口 `lib.rs` 仍承载大量 `build_runtime_*_record`,负责把 SpacetimeDB/procedure 快照转换为 BFF 或上层 facade 使用的记录投影。该职责更接近应用层读模型映射,应迁入 `application.rs`
## 本次范围
允许修改:
1. `server-rs/crates/module-runtime/src/application.rs`
2. `server-rs/crates/module-runtime/src/lib.rs`
3. `server-rs/crates/module-runtime/README.md`
4. `docs/technical/README.md`
5. 全局 DDD 任务清单进度记录
禁止修改:
1. `server-rs/crates/spacetime-module/src/**`
2. `server-rs/crates/spacetime-client/src/**`
3. `server-rs/crates/api-server/src/**`
4. 前端 services/hooks/components
5. `server-rs/crates/spacetime-module/src/migration.rs`
## 设计
本次将以下记录投影函数迁入 `application.rs`
1. settings、browse history、profile dashboard、wallet ledger、recharge center、membership、referral、reward code、redeem code、played world、play stats、runtime snapshot、save archive 的 `build_runtime_*_record`
2. 记录投影专用 JSON helper`parse_optional_json_value`
`format_utc_micros` 暂留 `lib.rs`,作为跨 commands/application 复用的时间格式化工具。充值商品目录函数也暂留 `lib.rs`,因为命令构造仍需要用它校验充值商品 ID。
## 边界说明
1. 本次不改变任何记录投影字段、时间格式、JSON 解析或错误语义。
2. 本次不迁移充值商品目录和商品查找函数。
3. 本次不改 SpacetimeDB 表结构、reducer、procedure 或 API route。
## 验收
```powershell
cargo fmt -p module-runtime --manifest-path server-rs/Cargo.toml --check
cargo test -p module-runtime --manifest-path server-rs/Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_RT_APPLICATION_RECORD_REFACTOR_2026-04-29.md server-rs/crates/module-runtime/src/application.rs server-rs/crates/module-runtime/src/lib.rs server-rs/crates/module-runtime/README.md docs/technical/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md
```

View File

@@ -0,0 +1,50 @@
# WP-RT 命令构造拆分落地说明
## 背景
`module-runtime` 已将领域类型迁入 `domain.rs`,错误类型迁入 `errors.rs`。根入口 `lib.rs` 仍承载大量 `build_runtime_*_input`、浏览历史写入准备、snapshot upsert JSON 归一化、邀请码/兑换码归一化等命令构造函数。为了继续推进 DDD 分层,本次将写入命令构造与字段归一化迁入 `commands.rs`
## 本次范围
允许修改:
1. `server-rs/crates/module-runtime/src/commands.rs`
2. `server-rs/crates/module-runtime/src/lib.rs`
3. `server-rs/crates/module-runtime/README.md`
4. `docs/technical/README.md`
5. 全局 DDD 任务清单进度记录
禁止修改:
1. `server-rs/crates/spacetime-module/src/**`
2. `server-rs/crates/spacetime-client/src/**`
3. `server-rs/crates/api-server/src/**`
4. 前端 services/hooks/components
5. `server-rs/crates/spacetime-module/src/migration.rs`
## 设计
本次将以下函数迁入 `commands.rs`
1. settings、browse history、profile dashboard、wallet、recharge、referral、reward code、redeem code、play stats、runtime snapshot、save archive 的 `build_runtime_*_input`
2. `build_runtime_browse_history_sync_input``prepare_runtime_browse_history_entries``build_runtime_browse_history_id`
3. `normalize_invite_code``normalize_redeem_code`
4. 仅服务命令构造的私有 helper`normalize_runtime_*_user_id``parse_utc_rfc3339_to_micros``normalize_bottom_tab``normalize_current_story_json`
`lib.rs` 继续通过 `pub use commands::*` 暴露原公开函数名。记录投影 builder、充值商品目录、通用时间格式化和 JSON 读取 helper 暂留 `lib.rs`,后续再拆 `application.rs`
## 边界说明
1. 本次不改变任何输入构造的校验语义。
2. 本次不移动记录投影 builder避免命令层和应用读模型混写。
3. 本次不改 SpacetimeDB 表结构、reducer、procedure 或 API route。
4. 本次不触发 `migration.rs` 更新。
## 验收
```powershell
cargo fmt -p module-runtime --manifest-path server-rs/Cargo.toml --check
cargo test -p module-runtime --manifest-path server-rs/Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_RT_COMMANDS_REFACTOR_2026-04-29.md server-rs/crates/module-runtime/src/commands.rs server-rs/crates/module-runtime/src/lib.rs server-rs/crates/module-runtime/README.md docs/technical/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md
```

View File

@@ -0,0 +1,52 @@
# WP-RT Snapshot/Profile/Save Archive 领域快照与记录类型拆分落地说明
## 背景
`module-runtime/src/lib.rs` 仍集中承载 runtime snapshot、browse history、profile dashboard、wallet ledger、recharge、referral、played world、play stats 和 save archive 的大量快照、输入、过程结果与 BFF 记录类型。上一切片已将 runtime settings 值对象迁入 `domain.rs`,本次继续把这些纯数据事实迁入领域模型文件,降低根入口体积,并为后续 `commands.rs``application.rs` 和 SpacetimeDB adapter 接线拆分留出边界。
本次只移动纯类型和类型自带的字符串格式化方法不修改构造函数、归一化函数、测试、SpacetimeDB 表结构、API route 或前端。
## 本次范围
允许修改:
1. `server-rs/crates/module-runtime/src/domain.rs`
2. `server-rs/crates/module-runtime/src/lib.rs`
3. `server-rs/crates/module-runtime/README.md`
4. `docs/technical/README.md`
5. 全局 DDD 任务清单进度记录
禁止修改:
1. `server-rs/crates/spacetime-module/src/**`
2. `server-rs/crates/spacetime-client/src/**`
3. `server-rs/crates/api-server/src/**`
4. 前端 services/hooks/components
5. `server-rs/crates/spacetime-module/src/migration.rs`
## 设计
本次将以下类型迁入 `domain.rs`
1. runtime snapshot、runtime setting、browse history、profile dashboard、wallet ledger、recharge、reward code、redeem code、referral、played world、play stats、save archive 的 snapshot/input/procedure result。
2. `RuntimeProfileWalletLedgerSourceType``RuntimeProfileRedeemCodeMode``RuntimeProfileRechargeProductKind``RuntimeProfileMembershipStatus``RuntimeProfileMembershipTier``RuntimeProfileRechargeOrderStatus``RuntimeBrowseHistoryThemeMode` 等领域枚举及其 `as_str` / `from_client_str` 方法。
3. `RuntimeSettingsRecord``RuntimeProfileDashboardRecord``RuntimeProfileWalletLedgerEntryRecord``RuntimeProfilePlayedWorldRecord``RuntimeProfilePlayStatsRecord``RuntimeSnapshotRecord``RuntimeProfileSaveArchiveRecord` 等回包投影记录类型。
4. 与这些类型强绑定、但不携带构造逻辑的默认常量:`DEFAULT_BROWSE_HISTORY_AUTHOR_DISPLAY_NAME``MAX_BROWSE_HISTORY_BATCH_SIZE``PROFILE_WALLET_LEDGER_LIST_LIMIT``PROFILE_REFERRAL_REWARD_POINTS``PROFILE_REFERRAL_DAILY_INVITER_REWARD_LIMIT``SAVE_SNAPSHOT_VERSION``DEFAULT_SAVE_ARCHIVE_SUMMARY_TEXT``PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK`
`lib.rs` 继续通过 `pub use domain::*` 保持原公开 API。构造函数、归一化函数、充值商品目录函数、错误枚举和测试暂留 `lib.rs`,避免在同一切片里混入命令层与错误层重排。
## 边界说明
1. 这些类型包含条件 `SpacetimeType` 派生,但不是 SpacetimeDB table本次只移动自定义类型位置不修改 table/reducer/procedure。
2. 本次不移动 `RuntimeSettingsFieldError``RuntimeBrowseHistoryFieldError``RuntimeProfileFieldError`,后续可单独迁入 `errors.rs`
3. 本次不移动 `build_runtime_*` 构造函数,后续可按 settings、browse history、profile/save 三组拆入 `commands.rs` / `application.rs`
4. 本次不触发 `migration.rs` 更新。
## 验收
```powershell
cargo fmt -p module-runtime --manifest-path server-rs/Cargo.toml --check
cargo test -p module-runtime --manifest-path server-rs/Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_RT_DOMAIN_SNAPSHOT_RECORD_REFACTOR_2026-04-29.md server-rs/crates/module-runtime/src/domain.rs server-rs/crates/module-runtime/src/lib.rs server-rs/crates/module-runtime/README.md docs/technical/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md
```

View File

@@ -0,0 +1,48 @@
# WP-RT 错误层拆分落地说明
## 背景
`module-runtime` 已将 runtime settings 值对象,以及 snapshot/profile/save archive 的快照、输入、过程结果和记录投影类型迁入 `domain.rs`。根入口 `lib.rs` 仍承载 `RuntimeSettingsFieldError``RuntimeBrowseHistoryFieldError``RuntimeProfileFieldError` 及其中文错误文案,后续继续拆 `commands.rs``application.rs` 前,需要先把错误语义归位到 `errors.rs`
## 本次范围
允许修改:
1. `server-rs/crates/module-runtime/src/errors.rs`
2. `server-rs/crates/module-runtime/src/lib.rs`
3. `server-rs/crates/module-runtime/README.md`
4. `docs/technical/README.md`
5. 全局 DDD 任务清单进度记录
禁止修改:
1. `server-rs/crates/spacetime-module/src/**`
2. `server-rs/crates/spacetime-client/src/**`
3. `server-rs/crates/api-server/src/**`
4. 前端 services/hooks/components
5. `server-rs/crates/spacetime-module/src/migration.rs`
## 设计
本次将以下错误类型迁入 `errors.rs`
1. `RuntimeSettingsFieldError`
2. `RuntimeBrowseHistoryFieldError`
3. `RuntimeProfileFieldError`
同步迁移三组 `Display` 实现,保持中文错误文案和 `MAX_BROWSE_HISTORY_BATCH_SIZE` 上限提示不变。`lib.rs` 继续通过 `pub use errors::*` 暴露原公开 API调用点无需修改。
## 边界说明
1. 本次不调整错误枚举变体,不改任何业务校验语义。
2. 本次不移动 `build_runtime_*` 构造函数;这些函数仍在 `lib.rs` 使用错误类型。
3. 本次不改 SpacetimeDB 表结构、reducer、procedure 或 API route。
## 验收
```powershell
cargo fmt -p module-runtime --manifest-path server-rs/Cargo.toml --check
cargo test -p module-runtime --manifest-path server-rs/Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_RT_ERROR_LAYER_REFACTOR_2026-04-29.md server-rs/crates/module-runtime/src/errors.rs server-rs/crates/module-runtime/src/lib.rs server-rs/crates/module-runtime/README.md docs/technical/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md
```

View File

@@ -0,0 +1,52 @@
# WP-RT Runtime Settings 领域值对象拆分落地说明
## 背景
`module-runtime` 已承接运行时设置、快照、浏览历史、资料页、钱包、充值、邀请、兑换码、游玩记录和存档等纯规则,但当前大量类型仍集中在 `src/lib.rs``WP-RT Runtime/Profile/Save` 需要逐步把纯领域事实和写入命令拆入 DDD 分层文件,避免后续 `spacetime-module``api-server` 接线时继续依赖巨型根文件。
本次只启动 `runtime settings` 这一条最小切片,不改 SpacetimeDB 表结构、不改 HTTP route、不改前端。
## 本次范围
允许修改:
1. `server-rs/crates/module-runtime/src/domain.rs`
2. `server-rs/crates/module-runtime/src/lib.rs`
3. `server-rs/crates/module-runtime/README.md`
4. 本文档
5. 全局 DDD 任务清单进度记录
禁止修改:
1. `server-rs/crates/spacetime-module/src/**`
2. `server-rs/crates/spacetime-client/src/**`
3. `server-rs/crates/api-server/src/**`
4. 前端 services/hooks/components
## 设计
本次将以下运行时设置领域对象迁入 `domain.rs`
1. `DEFAULT_MUSIC_VOLUME`
2. `DEFAULT_PLATFORM_THEME`
3. `RuntimePlatformTheme`
4. `RuntimeSettings`
`RuntimePlatformTheme::as_str``RuntimePlatformTheme::from_client_str``RuntimeSettings::defaults``RuntimeSettings::normalized` 同步迁入 `domain.rs``lib.rs` 继续通过 `pub use domain::*` 暴露原有 API保证 `spacetime-module``spacetime-client` 和既有测试无需改调用点。
## 边界说明
1. 本次不移动 `RuntimeSettingSnapshot``RuntimeSettingGetInput``RuntimeSettingUpsertInput` 和 procedure result因为它们仍与 SpacetimeDB procedure DTO 强绑定,后续可作为 `commands.rs` / Adapter mapper 切片单独拆分。
2. 本次不移动 `RuntimeSettingsFieldError`,避免把错误 Display 与多个 profile 错误枚举混在同一切片里改动。
3. 本次不移动浏览历史、钱包、充值、邀请、兑换码、游玩记录和存档类型。
## 验收
```powershell
cargo test -p module-runtime --manifest-path server-rs/Cargo.toml
cargo fmt -p module-runtime --manifest-path server-rs/Cargo.toml --check
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_RT_RUNTIME_SETTINGS_DOMAIN_REFACTOR_2026-04-29.md server-rs/crates/module-runtime/src/domain.rs server-rs/crates/module-runtime/src/lib.rs server-rs/crates/module-runtime/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md
```
本次不改后端运行接线、不改 SpacetimeDB table/reducer/procedure因此不触发 `migration.rs` 更新。

View File

@@ -0,0 +1,154 @@
# server-rs DDD WP-SC Spacetime Client 重构方案2026-04-29
## 1. 背景
`WP-SC Spacetime Client` 位于 `spacetime-module``api-server` 之间,只负责把 SpacetimeDB 生成绑定、procedure / reducer 调用、row snapshot 和错误语义收口成 BFF 可消费的 typed facade。
当前 `spacetime-client` 已经具备连接池、生成绑定、多个领域 facade 和 mapper。本文件用于冻结并关闭当前稳定 SpacetimeDB facade 范围内的 `WP-SC` 收口:不预判新的 table、reducer、procedure 或 row shape只把已存在调用层、错误 helper、mapper 与 README 状态闭环。
## 2. 本次目标
1. 明确 `spacetime-client` 的 DDD 边界和后续接入顺序。
2. 新增统一的 SDK 调用错误、业务 procedure 错误、缺失快照错误 helper。
3. 用 AI task 与 Big Fish 现有 facade 作为第一批示范,减少重复的 `SpacetimeClientError::Procedure(error.to_string())`
4. 保持现有公开 facade 方法和返回 record 不变,不改 `api-server` 调用方。
5. 不修改 `spacetime-module``shared-contracts``api-server` 路由挂载或前端。
## 3. 文件边界
本次允许修改:
1. `server-rs/crates/spacetime-client/src/lib.rs`
2. `server-rs/crates/spacetime-client/src/ai.rs`
3. `server-rs/crates/spacetime-client/src/big_fish.rs`
4. `server-rs/crates/spacetime-client/src/mapper.rs`
5. `server-rs/crates/spacetime-client/README.md`
6. 本文档
7. `docs/technical/README.md`
8. `docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md` 的进度记录
本次禁止修改:
1. `server-rs/crates/spacetime-module/src/**`
2. `server-rs/crates/shared-contracts/src/**`
3. `server-rs/crates/api-server/src/app.rs`
4. `server-rs/crates/api-server/src/**` 路由行为
5. `src/services/**``src/hooks/**``src/components/**`
6. `server-rs/crates/spacetime-client/src/module_bindings/**` 生成绑定
## 4. 分层落点
| 层 | 职责 | 本次落点 |
| --- | --- | --- |
| 连接层 | 连接池、握手、超时、断线处理 | 保持现状,不改连接策略 |
| 调用层 | procedure / reducer then 回调、SDK 错误映射 | 新增统一错误 helper并先接 AI / Big Fish |
| mapper 层 | 绑定类型到 BFF record / DTO 的转换 | 新增通用 procedure 失败与缺快照 helper后续逐步替换重复代码 |
| facade 层 | 面向 `api-server` 的 typed 方法 | 方法签名保持不变 |
## 5. 后续依赖
1. `WP-ST` 每稳定一个 SpacetimeDB facade 后,再由 `WP-SC` 接对应 mapper / facade。
2. `WP-API` 只能通过 `spacetime-client` 调用 SpacetimeDB不直接拼接生成绑定。
3. 前端迁移必须等待 `WP-API` route 和 DTO 稳定后,再按 `services -> hooks -> components` 接入。
4. 若后续改变 table / reducer / procedure必须由 `WP-ST` 同步表目录和必要的绑定生成记录。
## 6. 本次完成范围
1. `SpacetimeClientError` 已新增并接入统一 helper
- `from_sdk_error`
- `procedure_failed`
- `missing_snapshot`
2. 已覆盖现有稳定 facade 的 SDK 错误映射,包含 AI、Big Fish、assets、auth、story、combat、inventory、npc、runtime、puzzle、custom world。
3. `mapper.rs` 中稳定 procedure result 的重复 `ok=false` 默认错误和缺快照错误已收口到统一 helper。
4. auth store snapshot import mapper 中历史乱码错误文案已恢复为中文语义。
5. 领域语义错误继续保留原样,例如 `custom_world_profile 不存在`,避免把业务不存在误归类为缺快照。
## 7. 验收
必须执行:
```powershell
cargo fmt --all --check --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/big_fish.rs server-rs/crates/spacetime-client/src/mapper.rs
npm.cmd run api-server:maincloud
```
说明:本次不改 SpacetimeDB 表、reducer、procedure不刷新生成绑定不同步 `migration.rs`
## 8. story runtime inventory source 接线切片
### 8.1 目标
本轮继续在 `WP-SC` 内认领一个可并行切片:在 `SpacetimeClient::get_story_runtime_projection_source` 中复用已稳定的 `get_runtime_inventory_state` typed facade将 SpacetimeDB 背包/装备快照折回投影所需的 `game_state.playerInventory``game_state.playerEquipment`
目标是让 `/api/story/sessions/{story_session_id}/runtime-projection` 的读取投影优先消费新的 inventory adapter 结果,而不是只依赖 runtime snapshot 中的历史 JSON 背包副本。
### 8.2 边界
本轮允许修改:
1. `server-rs/crates/spacetime-client/src/story_runtime.rs`
2. 本文档
3. `docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`
本轮禁止修改:
1. `server-rs/crates/spacetime-module/src/**`
2. `server-rs/crates/spacetime-client/src/module_bindings/**`
3. `server-rs/crates/api-server/src/app.rs`
4. HTTP DTO、前端 services/hooks/components
5. `migration.rs`
### 8.3 实现约束
1. 只在 `spacetime-client` 中组合已存在 facade不新增 table、reducer、procedure。
2. `StoryRuntimeProjectionSource` 的输出结构保持不变,投影规则继续由 `module-runtime-story::build_story_runtime_projection` 承接。
3. inventory slot 到 runtime story JSON 的转换只做字段映射,不新增玩法规则。
4. `get_runtime_inventory_state` 若返回作用域不匹配,必须中止投影,避免把其他会话的背包装进当前 story session。
### 8.4 验收
```powershell
cargo test -p spacetime-client story_runtime --manifest-path server-rs/Cargo.toml
cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml
cargo fmt -p spacetime-client --manifest-path server-rs/Cargo.toml --check
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 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,78 @@
# server-rs DDD WP-ST AI Task 事件 Adapter 落地记录2026-04-29
## 1. 背景
`WP-AI AI Task` 已完成领域层拆分,`WP-ST SpacetimeDB Adapter` 可以开始把稳定领域状态变化接入 SpacetimeDB。当前 AI 任务已有真相表:
1. `ai_task`
2. `ai_task_stage`
3. `ai_text_chunk`
4. `ai_result_reference`
本次不改变这些真相表的字段,不改 HTTP/BFF不改前端只补齐 AI 任务状态变化的 SpacetimeDB 事件流。
## 2. 本次范围
允许修改:
1. `server-rs/crates/spacetime-module/src/ai/**`
2. `server-rs/crates/spacetime-module/src/migration.rs`
3. `docs/technical/SPACETIMEDB_TABLE_CATALOG.md`
4. 本文档
禁止修改:
1. `server-rs/crates/api-server/src/**`
2. `server-rs/crates/spacetime-client/src/**`
3. `src/services/**`
4. `src/hooks/**`
5. `src/components/**`
## 3. 设计
新增 `ai_task_event``public event` 表,供订阅端和后续 BFF 增量消费 AI 任务变化。
事件类型:
1. `TaskCreated`
2. `TaskStatusChanged`
3. `StageStarted`
4. `StageCompleted`
5. `TextChunkAppended`
6. `ResultReferenceAttached`
事件字段只保存用于路由和定位的轻量信息:
1. `task_id`
2. `owner_user_id`
3. `event_kind`
4. `task_status`
5. `stage_kind`
6. `text_chunk_row_id`
7. `result_reference_row_id`
8. `occurred_at`
## 4. 边界说明
1. `ai_task_event` 不是业务真相表,不能替代 `ai_task` / `ai_task_stage` / `ai_text_chunk` / `ai_result_reference`
2. reducer 和 procedure 仍只在事务成功后写入事件。
3. reducer 继续返回 `Result<(), String>`procedure 继续返回现有 `AiTaskProcedureResult`
4. 本次没有引入网络、文件、外部随机数或全局可变状态。
5. 本次新增表已同步 `migration.rs` 迁移白名单。
## 5. 验收命令
```powershell
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml
cargo test -p module-ai --manifest-path server-rs/Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_ST_AI_TASK_EVENT_ADAPTER_2026-04-29.md docs/technical/SPACETIMEDB_TABLE_CATALOG.md server-rs/crates/spacetime-module/src/ai/events.rs server-rs/crates/spacetime-module/src/ai/mod.rs server-rs/crates/spacetime-module/src/ai/snapshots.rs server-rs/crates/spacetime-module/src/ai/stages.rs server-rs/crates/spacetime-module/src/ai/tasks.rs server-rs/crates/spacetime-module/src/migration.rs
```
若后续生成前端绑定或发布数据库,需要继续执行:
```powershell
spacetime build
spacetime generate --lang typescript --out-dir <前端绑定目录> --module-path server-rs/crates/spacetime-module
spacetime describe <database> --json
```

View File

@@ -0,0 +1,51 @@
# server-rs DDD WP-ST Asset Row Mapper Adapter 落地记录2026-04-29
## 1. 背景
`module-assets` 已提供资产对象和实体绑定的领域 snapshot / record 构建能力。`spacetime-module` 的资产 Adapter 之前在 upsert 过程中直接重复拼装 SpacetimeDB row 与返回 snapshot字段规则分散在 Adapter 内。
本次不改变资产表结构,不改变 reducer / procedure 签名,只把 row 构造收口到更明确的 snapshot -> row mapper。
## 2. 本次范围
允许修改:
1. `server-rs/crates/spacetime-module/src/asset_metadata/objects.rs`
2. `server-rs/crates/spacetime-module/src/asset_metadata/bindings.rs`
3. 本文档
4. 全局任务清单进度记录
禁止修改:
1. `server-rs/crates/spacetime-module/src/migration.rs`
2. `docs/technical/SPACETIMEDB_TABLE_CATALOG.md`
3. `server-rs/crates/api-server/src/**`
4. `server-rs/crates/spacetime-client/src/**`
5. 前端 services/hooks/components
## 3. 设计
本次新增两个 Adapter 内部 mapper
1. `build_asset_object_row(&AssetObjectUpsertSnapshot) -> AssetObject`
2. `build_asset_entity_binding_row(&AssetEntityBindingSnapshot) -> AssetEntityBinding`
`upsert_asset_object``upsert_asset_entity_binding` 先构造领域 snapshot再由 mapper 落 SpacetimeDB row。这样后续继续迁移到 `module-assets` 应用服务时Adapter 的职责会更清楚:只做 row 查询、幂等定位、snapshot 持久化和 procedure result 返回。
## 4. 边界说明
1. 本次不新增、删除或修改 SpacetimeDB table 字段。
2. 本次不改 `migration.rs`
3. 本次不改 `spacetime-client` 绑定和 facade。
4. 本次不改 HTTP/BFF 和前端。
## 5. 验收命令
```powershell
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml
cargo test -p module-assets --manifest-path server-rs/Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_ST_ASSET_ROW_MAPPER_ADAPTER_2026-04-29.md server-rs/crates/spacetime-module/src/asset_metadata/objects.rs server-rs/crates/spacetime-module/src/asset_metadata/bindings.rs
```
结果:通过。

View File

@@ -0,0 +1,56 @@
# WP-ST Auth Adapter 目录化切片说明
## 背景
`spacetime-module/src/auth.rs` 同时承接认证表定义、procedure 入口、事务内导入导出逻辑和 module-auth 快照 JSON mapper。随着 `WP-A Auth` 已完成 DDD 骨架归位SpacetimeDB 侧也需要把 Auth adapter 从单文件拆到上下文目录,避免后续继续堆回根文件。
本次属于 `WP-ST SpacetimeDB Adapter` 的可并行切片,只做 Auth adapter 目录化,不改变 SpacetimeDB schema、procedure 名称、procedure 入参/出参或绑定形状。
## 本次范围
允许修改:
1. `server-rs/crates/spacetime-module/src/auth.rs`
2. `server-rs/crates/spacetime-module/src/auth/mod.rs`
3. `server-rs/crates/spacetime-module/src/auth/tables.rs`
4. `server-rs/crates/spacetime-module/src/auth/procedures.rs`
5. `server-rs/crates/spacetime-module/src/auth/mapper.rs`
6. 本文档
7. `docs/technical/README.md`
8. `docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`
禁止修改:
1. `spacetime-module/src/lib.rs`
2. `spacetime-module/src/migration.rs`
3. `docs/technical/SPACETIMEDB_TABLE_CATALOG.md`
4. `spacetime-client/src/module_bindings/**`
5. `api-server/src/**`
6. 前端 services / hooks / components
## 设计
本次将原 `auth.rs` 拆成:
1. `auth/mod.rs`Auth adapter 子模块入口,继续 `pub use procedures::*``pub use tables::*`,保持根模块 `pub use auth::*` 的对外导出不变。
2. `auth/tables.rs`:保留 `AuthStoreSnapshot``UserAccount``AuthIdentity``RefreshSession` 四张表定义和索引不改字段、可见性、accessor 或索引名。
3. `auth/procedures.rs`:保留 `get_auth_store_snapshot``upsert_auth_store_snapshot``import_auth_store_snapshot``export_auth_store_snapshot_from_tables` 四个 procedure 入口,以及对应事务内读写逻辑。
4. `auth/mapper.rs`:收口 module-auth 持久化快照 JSON 结构、refresh session client info JSON 结构和 identity id 组件清理函数,仅供 Auth procedure 内部使用。
## 边界说明
1. 本次未新增 reducer现有 Auth 同步仍通过 procedure 完成。
2. 本次未改变表结构,不需要修改 `migration.rs`
3. 本次未生成 SpacetimeDB 绑定,原因是导出的表、类型和 procedure 名称未改变。
4. Auth adapter 仍只负责认证快照与正式表的导入导出,不承接短信、微信 OAuth、JWT、cookie、HTTP 或文件持久化。
## 验收
```powershell
cargo fmt -p spacetime-module --manifest-path server-rs/Cargo.toml --check
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_ST_AUTH_ADAPTER_SPLIT_2026-04-29.md docs/technical/README.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md server-rs/crates/spacetime-module/src/auth/mod.rs server-rs/crates/spacetime-module/src/auth/tables.rs server-rs/crates/spacetime-module/src/auth/procedures.rs server-rs/crates/spacetime-module/src/auth/mapper.rs
```
`cargo check -p spacetime-module` 被非 WP-ST 依赖 crate 当前并行改动阻断,应记录具体阻断 crate 和错误位置;本切片不越界修改其他已认领工作包。

View File

@@ -0,0 +1,69 @@
# server-rs DDD WP-ST Big Fish 发布门禁 Adapter 落地记录2026-04-29
## 1. 背景
`WP-BF Big Fish` 已在领域层新增 `evaluate_publish_readiness`,用于评估草稿和资产槽是否满足发布条件。`spacetime-module` 之前在 Big Fish adapter 内直接调用 `build_asset_coverage` 判断发布就绪,容易让门禁规则继续散落在 Adapter。
本次将 SpacetimeDB Adapter 的发布门禁收口到 `module-big-fish` 应用服务,并新增轻量事件表记录成功事务中的门禁评估事实。
## 2. 本次范围
允许修改:
1. `server-rs/crates/spacetime-module/src/big_fish/**`
2. `server-rs/crates/spacetime-module/src/migration.rs`
3. `docs/technical/SPACETIMEDB_TABLE_CATALOG.md`
4. 本文档
禁止修改:
1. `server-rs/crates/api-server/src/**`
2. `server-rs/crates/spacetime-client/src/**`
3. `src/services/**`
4. `src/hooks/**`
5. `src/components/**`
## 3. 设计
新增 `big_fish_event``public event` 表,当前只承接 `PublishReadinessEvaluated`
事件字段:
1. `session_id`
2. `owner_user_id`
3. `event_kind`
4. `publish_ready`
5. `blockers_json`
6. `occurred_at`
接入点:
1. `compile_big_fish_draft_tx`
2. `generate_big_fish_asset_tx`
3. `publish_big_fish_game_tx`
这些接入点先从 SpacetimeDB row 读取草稿和资产槽,再调用 `module_big_fish::evaluate_publish_readiness`,最后把 readiness 回写到 `big_fish_creation_session.publish_ready``asset_coverage_json`
## 4. 边界说明
1. `big_fish_event` 不是作品真相表,不能替代 `big_fish_creation_session``big_fish_asset_slot`
2. 发布门禁规则由 `module-big-fish` 领域应用服务决定SpacetimeDB Adapter 只负责 row 映射、持久化和事件落表。
3. 由于 SpacetimeDB 事务在 `Err` 时回滚,发布失败路径中的事件不会持久化;事件表只记录成功事务内完成的门禁评估事实。
4. 本次没有引入 HTTP、OSS、图片生成、文件系统或外部随机数。
5. 本次新增表已同步 `migration.rs` 迁移白名单。
## 5. 验收命令
```powershell
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml
cargo test -p module-big-fish --manifest-path server-rs/Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_WP_ST_BIG_FISH_READINESS_ADAPTER_2026-04-29.md docs/technical/SPACETIMEDB_TABLE_CATALOG.md server-rs/crates/spacetime-module/src/big_fish/events.rs server-rs/crates/spacetime-module/src/big_fish/mod.rs server-rs/crates/spacetime-module/src/big_fish/assets.rs server-rs/crates/spacetime-module/src/big_fish/session.rs server-rs/crates/spacetime-module/src/migration.rs
```
若后续具备 CLI 环境,需要继续执行:
```powershell
spacetime build --project-path server-rs/crates/spacetime-module
spacetime generate --lang rust --out-dir server-rs/crates/spacetime-client/src/module_bindings --module-path server-rs/crates/spacetime-module
```

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

@@ -0,0 +1,30 @@
# WP-ST Custom World 根入口瘦身落地说明
## 背景
`spacetime-module/src/lib.rs` 已完成 gameplay 根入口瘦身,但仍直接承载 Custom World 的 SpacetimeDB 表、reducer、procedure 与私有事务 helper。根入口继续膨胀会让后续 `module-custom-world` 领域化、绑定生成检查和并行任务边界都难以维护。
本次迁移按 WP-ST Adapter 边界执行:根入口只负责声明模块与 re-exportCustom World 的 SpacetimeDB adapter 真正落到 `spacetime-module/src/custom_world/mod.rs`
## 落地范围
1. `lib.rs` 新增 `mod custom_world;``pub use custom_world::*;`,保留根模块对绑定生成需要的公开导出。
2. 将当前 `lib.rs` 已公开的 Custom World 表、procedure、reducer、事务 helper 与单测整体迁移到 `custom_world/mod.rs`
3. `custom_world/mod.rs``use crate::*;` 复用根模块已经公开的领域类型、SpacetimeDB 类型和 JSON helper避免复制跨模块前置导入。
4. 移除 `lib.rs` 中 Custom World 的重复实现,让根入口只承担组合职责。
## 边界
1. 不新增、删除或重命名 Custom World 表。
2. 不新增、删除或重命名当前已公开的 reducer / procedure。
3. 不修改 `CustomWorldProfile``CustomWorldGalleryEntry` 等表字段,本次不触发 `migration.rs` 更新。
4. 不启用 `custom_world` 子目录中尚未成为根入口正式导出的新过程,避免把后续行为变更混入本次根入口迁移。
5. 不修改前端、BFF、`server-node` 或 PostgreSQL 兼容逻辑。
## 验收
1. `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`
2. `cargo test -p module-custom-world --manifest-path server-rs/Cargo.toml`
3. `npm.cmd run check:server-rs-ddd`
4. `npm.cmd run check:encoding -- docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md docs/technical/SERVER_RS_DDD_WP_ST_CUSTOM_WORLD_ROOT_SPLIT_2026-04-29.md server-rs/crates/spacetime-module/src/lib.rs server-rs/crates/spacetime-module/src/custom_world/mod.rs`
5. 修改后端 Rust 代码后,按项目约束运行 `npm.cmd run api-server:maincloud` 并探测 `/healthz`

View File

@@ -0,0 +1,31 @@
# WP-ST Gameplay 根入口瘦身落地说明
## 背景
`spacetime-module/src/gameplay/mod.rs` 已经承接 RPG gameplay 的 SpacetimeDB table、reducer、procedure、row mapper 和事务 helper但根入口 `spacetime-module/src/lib.rs` 仍保留同一批 gameplay 代码,导致根文件继续承担 Custom World 与 RPG gameplay 两条大链路。
本次切片属于 `WP-ST SpacetimeDB Adapter`目标是让根入口只做模块声明、re-export 和仍未拆出的 Custom World adapter不改变 SpacetimeDB schema、reducer/procedure 对外名称或业务规则。
## 落地范围
1.`spacetime-module/src/lib.rs` 接入 `mod gameplay; pub use gameplay::*;`
2. 将根入口中已迁入 `gameplay/mod.rs` 的 RPG gameplay table、reducer、procedure、row mapper 和事务 helper 删除。
3.`gameplay/mod.rs` 补齐模块内依赖导入。
4. 补齐 story session 继续推进与读取所需的内部 helper
- `continue_story_tx`
- `get_story_session_state_tx`
5. 保留根入口中的 Custom World table、procedure、reducer 和测试,本次不拆 Custom World。
## 边界
1. 本次不修改 `migration.rs`因为表名、字段、reducer/procedure 名称没有变化。
2. 本次不修改 `SPACETIMEDB_TABLE_CATALOG.md`,因为表目录没有变化。
3. 本次不修改 `api-server``spacetime-client`、前端 services/hooks/components。
4. 本次只移动 adapter 归属,不把业务规则从 `module-*` 拉回 SpacetimeDB adapter。
## 验收
1. `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`
2. `npm.cmd run check:server-rs-ddd`
3. `npm.cmd run check:encoding -- server-rs/crates/spacetime-module/src/lib.rs server-rs/crates/spacetime-module/src/gameplay/mod.rs docs/technical/SERVER_RS_DDD_WP_ST_GAMEPLAY_ROOT_SPLIT_2026-04-29.md docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`
4. 如需联调发布,再执行当前 CLI 支持的 `spacetime build -p server-rs/crates/spacetime-module`

View File

@@ -0,0 +1,30 @@
# WP-ST 拼图发布事件 Adapter 落地说明
## 背景
`module-puzzle` 已经承载拼图作品创建、发布、运行和成绩规则,`spacetime-module/src/puzzle.rs` 负责把领域对象映射为 SpacetimeDB table/procedure。当前发布成功事实只体现在 `puzzle_work_profile``puzzle_agent_session` 的最终行状态中,订阅端若要捕获一次性发布动作,需要轮询或自行对比快照。
本次切片属于 `WP-ST SpacetimeDB Adapter`,只新增 SpacetimeDB 事件投影,不新增业务规则,也不兼容旧 Node/PostgreSQL 路径。
## 落地范围
1. 新增 `puzzle_event` public event table。
2. 新增 `PuzzleEventKind::WorkPublished`,用于表示作品发布成功。
3.`publish_puzzle_work_tx` 完成 `puzzle_work_profile` upsert 和 `puzzle_agent_session` 更新后写入事件。
4.`puzzle_event` 纳入 `migration.rs` 迁移白名单。
5. 顺手补齐已有 `puzzle_leaderboard_entry` 的迁移白名单和表目录记录,避免 schema 账本遗漏。
## 边界
1. `puzzle_event` 只承接跨层订阅和审计所需的轻量事实。
2. 正式作品真相仍以 `puzzle_work_profile``puzzle_agent_session` 为准。
3. 排行榜真相仍以 `puzzle_leaderboard_entry` 为准,本次不改成绩规则。
4. 前端和 `spacetime-client` 后续如需消费发布事件,应订阅 `puzzle_event`,不要把事件表当作作品列表来源。
## 验收
1. `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`
2. `cargo test -p module-puzzle --manifest-path server-rs/Cargo.toml`
3. `npm.cmd run check:server-rs-ddd`
4. `npm.cmd run check:encoding -- <本次修改文件>`
5. 若本机安装 `spacetime` CLI再执行 `spacetime build --project-path server-rs/crates/spacetime-module` 和绑定生成。

View File

@@ -0,0 +1,40 @@
# server-rs DDD WP-ST SpacetimeDB 根入口 warning 收口记录2026-04-29
## 1. 背景
`spacetime-module/src/lib.rs` 需要继续从各 `module-*` re-export 领域类型,供 SpacetimeDB table、reducer、procedure 和绑定生成使用。当前多个领域包仍暴露同名 normalize helper导致 `cargo check -p spacetime-module` 持续出现 `ambiguous glob re-exports` warning。
这些同名 helper 只影响 value namespace 的公开导出,不影响 SpacetimeDB 表、输入、输出和枚举类型。
## 2. 本次范围
允许修改:
1. `server-rs/crates/spacetime-module/src/lib.rs`
2. 本文档
3. 全局任务清单进度记录
禁止修改:
1. `module-*` 领域公开 API
2. `spacetime-client/src/module_bindings/**`
3. `api-server/src/**`
4. 前端 services/hooks/components
## 3. 处理方式
`spacetime-module/src/lib.rs` 顶部增加中文注释,并局部允许 `ambiguous_glob_reexports`
1. 保留根模块 re-export避免影响 SpacetimeDB 绑定生成和既有 adapter 引用。
2. 明确 warning 来源是领域 helper 同名,不是 schema 或 reducer 冲突。
3. 后续等各领域包进一步缩小公开 API 时,再移除该 lint 允许。
## 4. 验收
```powershell
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml
npm.cmd run check:server-rs-ddd
npm.cmd run check:encoding -- server-rs/crates/spacetime-module/src/lib.rs
```
结果:通过,`spacetime-module` 编译不再输出 `ambiguous glob re-exports` warning。

View File

@@ -20,26 +20,32 @@ spacetime sql <db> "SELECT * FROM custom_world_gallery_entry"
## 总览
| 领域 | 表 |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 认证 | `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` |
| 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` |
| 拼图 | `puzzle_agent_session`, `puzzle_agent_message`, `puzzle_work_profile`, `puzzle_runtime_run` |
| 抓大鹅 Match3D | `match3d_agent_session`, `match3d_agent_message`, `match3d_work_profile`, `match3d_runtime_run` |
| 大鱼吃小鱼 | `big_fish_creation_session`, `big_fish_agent_message`, `big_fish_asset_slot`, `big_fish_runtime_run` |
| 资产 | `asset_object`, `asset_entity_binding` |
| AI 任务 | `ai_task`, `ai_task_stage`, `ai_text_chunk`, `ai_result_reference` |
| 运维迁移 | `database_migration_operator`, `database_migration_import_chunk` |
| 领域 | 表 |
| --- | --- |
| 运维迁移 | `database_migration_operator`, `database_migration_import_chunk` |
| 认证 | `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_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` |
| 世界创作 | `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` |
| 抓大鹅 Match3D | `match3d_agent_session`, `match3d_agent_message`, `match3d_work_profile`, `match3d_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_event` |
| AI 任务 | `ai_task`, `ai_task_stage`, `ai_text_chunk`, `ai_result_reference`, `ai_task_event` |
## 运维迁移表
### `database_migration_operator`
- 作用:迁移操作员白名单,控制导出、导入、授权和撤销迁移 procedure 的调用权限
- 作用:数据库迁移操作员授权表,用于限制导出、导入和增量导入等 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>';
```
### `database_migration_import_chunk`
@@ -173,6 +179,29 @@ SELECT * FROM profile_redeem_code_usage WHERE code = '<CODE>';
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`
- 作用:记录用户玩过的世界及最后游玩时间,用于个人页历史和继续游戏入口。
@@ -185,6 +214,50 @@ 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;
```
### `public_work_play_daily_stat`
- 作用:跨玩法公开作品的每日游玩聚合表,用于 gallery 近 7 日热度和排序,不把运行统计散落在各玩法私有表。
- 结构:`stat_id PK: String`, `source_type: String`, `owner_user_id: String`, `profile_id: String`, `played_day: i64`, `play_count: u32`, `updated_at: Timestamp`
- 索引:主键 `stat_id``(source_type, profile_id, played_day)`
```sql
SELECT * FROM public_work_play_daily_stat WHERE stat_id = '<stat_id>';
SELECT * FROM public_work_play_daily_stat WHERE source_type = '<source_type>' AND profile_id = '<profile_id>';
```
### `public_work_like`
- 作用:跨玩法公开作品的点赞去重表,按用户维度保证同一用户对同一作品只计一次点赞。
- 结构:`like_id PK: String`, `source_type: String`, `owner_user_id: String`, `profile_id: String`, `user_id: String`, `liked_at: Timestamp`
- 索引:主键 `like_id``(source_type, profile_id)``user_id`
```sql
SELECT * FROM public_work_like WHERE like_id = '<like_id>';
SELECT * FROM public_work_like WHERE source_type = '<source_type>' AND profile_id = '<profile_id>';
SELECT * FROM public_work_like WHERE user_id = '<user_id>';
```
### `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`
- 作用:用户存档列表,保存世界信息、封面、当前状态 JSON 和剧情 JSON。
@@ -441,6 +514,18 @@ SELECT * FROM puzzle_work_profile WHERE owner_user_id = '<user_id>' ORDER BY upd
SELECT * FROM puzzle_work_profile WHERE publication_status = 'Published';
```
### `puzzle_event`
- 作用拼图创作事件表目前记录作品发布成功事实供订阅端、BFF 或审计流程感知创作流已产出公开作品;正式作品状态仍以 `puzzle_work_profile``puzzle_agent_session` 为准。
- 可见性:`public event`
- 结构:`event_id PK: String`, `profile_id: String`, `work_id: String`, `session_id: Option<String>`, `owner_user_id: String`, `event_kind: PuzzleEventKind`, `occurred_at: Timestamp`
- 索引:`profile_id`, `owner_user_id`
```sql
SELECT * FROM puzzle_event WHERE profile_id = '<profile_id>' ORDER BY occurred_at ASC;
SELECT * FROM puzzle_event WHERE owner_user_id = '<user_id>' ORDER BY occurred_at DESC;
```
### `puzzle_runtime_run`
- 作用:拼图游玩运行态,保存当前关卡、网格、已玩 profile 列表、标签和运行快照。
@@ -452,6 +537,17 @@ SELECT * FROM puzzle_runtime_run WHERE run_id = '<run_id>';
SELECT * FROM puzzle_runtime_run WHERE owner_user_id = '<user_id>' ORDER BY updated_at DESC;
```
### `puzzle_leaderboard_entry`
- 作用:拼图关卡真实成绩表,按用户、作品和网格规格保留最佳成绩,用于结算弹窗排行榜。
- 结构:`entry_id PK: String`, `profile_id: String`, `grid_size: u32`, `user_id: String`, `nickname: String`, `best_elapsed_ms: u64`, `last_run_id: String`, `updated_at: Timestamp`
- 索引:`profile_id + grid_size`, `user_id + profile_id + grid_size`
```sql
SELECT * FROM puzzle_leaderboard_entry WHERE profile_id = '<profile_id>' AND grid_size = 4 ORDER BY best_elapsed_ms ASC;
SELECT * FROM puzzle_leaderboard_entry WHERE user_id = '<user_id>' AND profile_id = '<profile_id>' AND grid_size = 4;
```
## 抓大鹅 Match3D 表
### `match3d_agent_session`
@@ -504,7 +600,7 @@ SELECT * FROM match3d_runtime_run WHERE profile_id = '<profile_id>';
### `big_fish_creation_session`
- 作用:大鱼吃小鱼创作会话表,保存种子、阶段、锚点包、草稿、资产覆盖和发布就绪状态。
- 结构:`session_id PK: String`, `owner_user_id: String`, `seed_text: String`, `current_turn: u32`, `progress_percent: u32`, `stage: BigFishCreationStage`, `anchor_pack_json: String`, `draft_json: Option<String>`, `asset_coverage_json: String`, `last_assistant_reply: Option<String>`, `publish_ready: bool`, `created_at: Timestamp`, `updated_at: Timestamp`
- 结构:`session_id PK: String`, `owner_user_id: String`, `seed_text: String`, `current_turn: u32`, `progress_percent: u32`, `stage: BigFishCreationStage`, `anchor_pack_json: String`, `draft_json: Option<String>`, `asset_coverage_json: String`, `last_assistant_reply: Option<String>`, `publish_ready: bool`, `play_count: u32`, `created_at: Timestamp`, `updated_at: Timestamp`
- 索引:`owner_user_id`
```sql
@@ -533,6 +629,18 @@ SELECT * FROM big_fish_asset_slot WHERE slot_id = '<slot_id>';
SELECT * FROM big_fish_asset_slot WHERE session_id = '<session_id>';
```
### `big_fish_event`
- 作用大鱼吃小鱼创作事件表目前记录发布门禁评估结果供订阅端、BFF 或审计流程感知草稿是否达到发布条件;正式作品状态仍以 `big_fish_creation_session``big_fish_asset_slot` 为准。
- 可见性:`public event`
- 结构:`event_id PK: String`, `session_id: String`, `owner_user_id: String`, `event_kind: BigFishEventKind`, `publish_ready: bool`, `blockers_json: String`, `occurred_at: Timestamp`
- 索引:`session_id`, `owner_user_id`
```sql
SELECT * FROM big_fish_event WHERE session_id = '<session_id>' ORDER BY occurred_at ASC;
SELECT * FROM big_fish_event WHERE owner_user_id = '<user_id>' ORDER BY occurred_at DESC;
```
### `big_fish_runtime_run`
- 作用:大鱼吃小鱼运行态表,保存当前 run 的快照、最后输入方向和 tick。
@@ -571,6 +679,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>';
```
### `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_task`
@@ -619,7 +740,20 @@ SELECT * FROM ai_result_reference WHERE result_reference_row_id = '<row_id>';
SELECT * FROM ai_result_reference WHERE task_id = '<task_id>' ORDER BY created_at ASC;
```
### `ai_task_event`
- 作用AI 任务事件表,用于把任务创建、状态变化、阶段变化、流式文本和结果引用挂接广播给订阅端;任务真相仍以 `ai_task``ai_task_stage``ai_text_chunk``ai_result_reference` 为准。
- 可见性:`public event`
- 结构:`event_id PK: String`, `task_id: String`, `owner_user_id: String`, `event_kind: AiTaskEventKind`, `task_status: Option<AiTaskStatus>`, `stage_kind: Option<AiTaskStageKind>`, `text_chunk_row_id: Option<String>`, `result_reference_row_id: Option<String>`, `occurred_at: Timestamp`
- 索引:`task_id`, `owner_user_id`
```sql
SELECT * FROM ai_task_event WHERE task_id = '<task_id>' ORDER BY occurred_at ASC;
SELECT * FROM ai_task_event WHERE owner_user_id = '<user_id>' ORDER BY occurred_at DESC;
```
## 当前维护风险
- `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` 为准;后续完成拆分时,需要删除重复定义或正式挂载子模块,并同步更新本文
- `custom_world/*` 子模块中也保留了一份表骨架;当前生成绑定与 `src/lib.rs` 对齐,包含 `public_work_code``author_public_user_code``custom_world_gallery_entry.public_work_code` 索引。维护时不要只看子模块文件。
- `scripts/check-server-rs-ddd-boundaries.mjs` 已检查 `spacetime-module/src/**` 中的表定义、`migration.rs` 白名单和本文目录项是否一致;新增或删除表后必须先让该检查通过
- `database_migration_operator` 是迁移权限表,不导出到业务迁移包;除此之外的业务表都必须进入 `migration.rs`
- `spacetime-client/src/module_bindings/**` 是生成物表、reducer 或 procedure 发生 shape 变化后必须通过 `npm.cmd run spacetime:generate -- --rust-only` 或对应 SpacetimeDB CLI 流程重新生成,不要手写修改。

View File

@@ -12,7 +12,7 @@
2. 标题下显示可复制的分享文本。
3. 分享文本下方显示主按钮“分享”,点击后复制完整分享文本。
4. 页面底部显示三个分享渠道 icon微信、QQ、抖音。
5. 移动端使用底部弹层,桌面端居中展示,复用 `UnifiedModal` 的平台弹窗外壳。
5. 移动端与桌面端都使用居中独立面板,复用 `UnifiedModal` 的平台弹窗外壳。
## 分享文本
@@ -39,6 +39,10 @@
仓库现有 `media/social-media-group/wechat.png``qq.png` 是社群二维码,不作为本面板渠道 icon 使用。渠道 icon 采用轻量圆形文字标识,避免误导用户进入社群。
## 面板样式约束
分享面板通过 `UnifiedModal` portal 挂载到页面根部时,需要在遮罩层补齐当前平台主题类,避免主题变量脱离页面容器后丢失。面板外壳继续使用 `platform-modal-shell``--platform-modal-fill` 背景,并在移动端覆盖平台弹窗默认底部抽屉布局,保持居中显示。
## 接入范围
- `RpgCreationResultActionBar`RPG 发布成功后由父层回传分享数据并打开面板。

View File

@@ -21,6 +21,7 @@
"preview": "node scripts/vite-cli.mjs preview",
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
"check:encoding": "node scripts/check-encoding.mjs",
"check:server-rs-ddd": "node scripts/check-server-rs-ddd-boundaries.mjs",
"lint:eslint": "eslint . --ext .ts,.tsx,.js,.mjs,.cjs --max-warnings 0",
"lint:guardrails": "npm run lint:eslint",
"typecheck": "tsc -p tsconfig.typecheck-guardrails.json --noEmit",

View File

@@ -193,3 +193,7 @@ export type BigFishRuntimeSnapshotResponse = {
eventLog: string[];
updatedAt: string;
};
export type BigFishRunResponse = {
run: BigFishRuntimeSnapshotResponse;
};

View File

@@ -102,11 +102,6 @@ export interface StartPuzzleRunRequest {
levelId?: string | null;
}
export interface AdvanceLocalPuzzleNextLevelRequest {
run: PuzzleRunSnapshot;
sourceSessionId?: string | null;
}
export interface PuzzleRunResponse {
run: PuzzleRunSnapshot;
}
@@ -129,6 +124,10 @@ export interface DragPuzzlePieceRequest {
targetCol: number;
}
export interface AdvancePuzzleNextLevelRequest {
targetProfileId?: string | null;
}
export interface UsePuzzleRuntimePropRequest {
propKind: PuzzleRuntimePropKind;
}

View File

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

View File

@@ -14,26 +14,6 @@ export type RuntimeAction<
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 = [
'story_continue_adventure',
'story_opening_camp_dialogue',

View File

@@ -3,10 +3,7 @@
* 该文件只负责 view model、presentation、patch 与 snapshot 回包结构。
*/
import type { JsonObject } from './common';
import type { SavedGameSnapshot, SavedGameSnapshotInput } from './runtime';
import type {
RuntimeActionRequest,
RuntimeActionResponse,
RuntimeStoryChoiceAction,
RuntimeStoryChoicePayload,
RuntimeStoryOptionInteraction,
@@ -206,8 +203,10 @@ export type RuntimeStoryPatch =
};
export type RuntimeStoryActionRequest =
RuntimeActionRequest<RuntimeStoryChoiceAction> & {
snapshot?: SavedGameSnapshotInput;
{
sessionId: string;
clientVersion?: number;
action: RuntimeStoryChoiceAction;
};
export type RuntimeStoryAiRequestOptions = {
@@ -224,55 +223,3 @@ export type RuntimeStoryAiRequest = {
recentActionResult?: string | null;
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

@@ -0,0 +1,135 @@
import type { JsonObject } from './common';
import type { SavedGameSnapshotInput } from './runtime';
/**
* story session 主链共享契约。
* 字段命名与 server-rs/shared-contracts/src/story.rs 的 camelCase 回包保持一致。
*/
export type BeginStorySessionRequest = {
runtimeSessionId: string;
worldProfileId: string;
initialPrompt: string;
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 = {
storySessionId: string;
narrativeText: string;
choiceFunctionId?: string | null;
};
export type ResolveStoryRuntimeActionRequest = {
storySessionId: string;
clientVersion?: number;
functionId: string;
actionText: string;
targetId?: string | null;
payload?: JsonObject | null;
};
export type StorySessionPayload = {
storySessionId: string;
runtimeSessionId: string;
actorUserId: string;
worldProfileId: string;
initialPrompt: string;
openingSummary?: string | null;
latestNarrativeText: string;
latestChoiceFunctionId?: string | null;
status: string;
version: number;
createdAt: string;
updatedAt: string;
};
export type StoryEventPayload = {
eventId: string;
storySessionId: string;
eventKind: string;
narrativeText: string;
choiceFunctionId?: string | null;
createdAt: string;
};
export type StorySessionMutationResponse = {
storySession: StorySessionPayload;
storyEvent: StoryEventPayload;
};
export type StorySessionStateResponse = {
storySession: StorySessionPayload;
storyEvents: StoryEventPayload[];
};
export type StoryRuntimeSnapshotPayload<
TGameState = JsonObject,
TCurrentStory = JsonObject,
> = SavedGameSnapshotInput<TGameState, string, TCurrentStory>;
export type StoryRuntimeProjectionRequest = {
storySessionId: string;
clientVersion?: number;
};
export type StoryRuntimeActorProjection = {
hp: number;
maxHp: number;
mana: number;
maxMana: number;
currency: number;
currencyText: string;
};
export type StoryRuntimeInventoryProjection = {
backpackItems: JsonObject[];
equipmentSlots: JsonObject[];
forgeRecipes: JsonObject[];
};
export type StoryRuntimeOptionProjection = {
functionId: string;
actionText: string;
detailText?: string | null;
scope: string;
payload?: JsonObject | null;
enabled: boolean;
reason?: string | null;
};
export type StoryRuntimeStatusProjection = {
inBattle: boolean;
npcInteractionActive: boolean;
currentEncounterId?: string | null;
currentNpcBattleMode?: string | null;
currentNpcBattleOutcome?: string | null;
};
export type StoryRuntimeProjectionResponse = {
storySession: StorySessionPayload;
storyEvents: StoryEventPayload[];
serverVersion: number;
gameState: JsonObject;
actor: StoryRuntimeActorProjection;
inventory: StoryRuntimeInventoryProjection;
options: StoryRuntimeOptionProjection[];
status: StoryRuntimeStatusProjection;
currentNarrativeText?: string | null;
actionResultText?: string | null;
toast?: string | null;
};
export type StoryRuntimeMutationResponse = {
projection: StoryRuntimeProjectionResponse;
};

View File

@@ -2,6 +2,7 @@ export * from './assets/qwenSprite';
export * from './contracts/auth';
export type * from './contracts/bigFish';
export * from './contracts/common';
export type * from './contracts/creationAgentDocumentInput';
export type * from './contracts/customWorldAgent';
export * from './contracts/match3dAgent';
export * from './contracts/match3dRuntime';
@@ -25,6 +26,7 @@ export * from './contracts/rpgRuntimeQuestAssist';
export * from './contracts/rpgRuntimeStoryAction';
export * from './contracts/rpgRuntimeStoryState';
export * from './contracts/runtime';
export type * from './contracts/story';
export * from './http';
export * from './llm/narrativeLanguage';
export * from './llm/parsers';

View File

@@ -0,0 +1,276 @@
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { basename, join, relative } from 'node:path';
const repoRoot = process.cwd();
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',
'database_migration_import_chunk',
]);
const requiredModuleFiles = [
'domain.rs',
'commands.rs',
'application.rs',
'events.rs',
'errors.rs',
];
const requiredLibModules = ['domain', 'commands', 'application', 'events', 'errors'];
const forbiddenModuleWidePatterns = [
{
pattern: /\baxum::/u,
message: 'module-* 不允许直接依赖 Axum',
},
{
pattern: /\bspacetimedb::(?:table|reducer|procedure|ReducerContext|ProcedureContext|Table)\b/u,
message: 'module-* 不允许声明 SpacetimeDB table/reducer/procedure 或直接操作表',
},
];
const forbiddenCorePatterns = [
{
pattern: /\breqwest::/u,
message: 'DDD 核心文件不允许直接依赖 reqwest',
},
{
pattern: /\bplatform_oss::/u,
message: 'DDD 核心文件不允许直接依赖 OSS adapter',
},
{
pattern: /\bplatform_llm::/u,
message: 'DDD 核心文件不允许直接依赖 LLM adapter',
},
{
pattern: /\bspacetime_client::/u,
message: 'DDD 核心文件不允许直接依赖 SpacetimeDB client adapter',
},
{
pattern: /\bstd::fs\b/u,
message: 'DDD 核心文件不允许直接访问文件系统',
},
{
pattern: /\btokio::/u,
message: 'DDD 核心文件不允许绑定异步运行时',
},
];
function normalizePath(path) {
return path.replace(/\\/gu, '/');
}
function readText(path) {
return readFileSync(path, 'utf8');
}
function listRustFiles(dir) {
const files = [];
function walk(currentDir) {
for (const name of readdirSync(currentDir)) {
const fullPath = join(currentDir, name);
const stat = statSync(fullPath);
if (stat.isDirectory()) {
walk(fullPath);
continue;
}
if (name.endsWith('.rs')) {
files.push(fullPath);
}
}
}
walk(dir);
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() {
return readdirSync(cratesDir)
.filter((name) => name.startsWith('module-'))
.filter((name) => existsSync(join(cratesDir, name, 'Cargo.toml')))
.sort();
}
const failures = [];
const moduleCrates = collectModuleCrates();
for (const crateName of moduleCrates) {
const crateDir = join(cratesDir, crateName);
const srcDir = join(crateDir, 'src');
const libPath = join(srcDir, 'lib.rs');
for (const fileName of requiredModuleFiles) {
const filePath = join(srcDir, fileName);
if (!existsSync(filePath)) {
failures.push(`${crateName} 缺少 DDD 落位文件 src/${fileName}`);
}
}
if (existsSync(libPath)) {
const libText = readText(libPath);
for (const moduleName of requiredLibModules) {
const moduleDeclaration = new RegExp(
`(?:^|\\n)\\s*(?:pub(?:\\([^)]*\\))?\\s+)?mod\\s+${moduleName}\\s*;`,
'u',
);
if (!moduleDeclaration.test(libText)) {
failures.push(`${crateName} 的 lib.rs 缺少模块声明 mod ${moduleName};`);
}
}
}
for (const rustFile of listRustFiles(srcDir)) {
const relativePath = normalizePath(relative(repoRoot, rustFile));
const fileName = basename(rustFile);
const text = readText(rustFile);
if (fileName === 'mapper.rs') {
failures.push(`${relativePath} 不能位于 module-*mapper 只能放在 adapter crate`);
}
for (const rule of forbiddenModuleWidePatterns) {
if (rule.pattern.test(text)) {
failures.push(`${relativePath}: ${rule.message}`);
}
}
const isDddCoreFile = requiredModuleFiles.some((name) =>
relativePath.endsWith(`/src/${name}`),
);
if (!isDddCoreFile) {
continue;
}
for (const rule of forbiddenCorePatterns) {
if (rule.pattern.test(text)) {
failures.push(`${relativePath}: ${rule.message}`);
}
}
}
}
checkSpacetimeTableCatalogAndMigration();
if (failures.length > 0) {
console.error('server-rs DDD boundary check failed:');
for (const failure of failures) {
console.error(`- ${failure}`);
}
process.exit(1);
}
console.log(`server-rs DDD boundary check passed for ${moduleCrates.length} module crate(s).`);

View File

@@ -438,12 +438,6 @@ const proxyPrefixes = [
'/admin/api',
'/api/',
'/api',
'/generated-character-drafts',
'/generated-characters',
'/generated-animations',
'/generated-custom-world-scenes',
'/generated-custom-world-covers',
'/generated-qwen-sprites',
'/healthz',
];

View File

@@ -1,4 +1,4 @@
import {spawn} from 'node:child_process';
import {spawn} from 'node:child_process';
import {existsSync} from 'node:fs';
import {cp, mkdir, readdir, rm, stat} from 'node:fs/promises';
import path from 'node:path';
@@ -55,7 +55,7 @@ for (const target of selectedTargets) {
await recreateTempDir(tempOutDir);
console.log(`[spacetime:generate] 生成 ${target.name} bindings 到短路径: ${tempOutDir}`);
await run('spacetime', buildGenerateArgs(target, tempOutDir));
await generateBindings(target, tempOutDir);
const fileCount = await countFiles(tempOutDir);
if (fileCount === 0) {
@@ -148,7 +148,79 @@ function buildGenerateArgs(target, outDir) {
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) => {
const child = spawn(command, commandArgs, {
cwd: REPO_ROOT,
@@ -178,13 +250,21 @@ function run(command, commandArgs) {
return;
}
const generatedFormatFailed = output.includes('Could not format generated files');
if (generatedFormatFailed && options.allowGeneratedFormatFailure) {
console.warn(`[spacetime:generate] ${command} generated files but formatting failed; continuing with validation.`);
resolve({generatedFormatFailed});
return;
}
if (generatedFormatFailed) {
reject(new Error(`${command} generated files but formatting failed.`));
return;
}
if (code === 0) {
if (output.includes('Could not format generated files')) {
// 中文注释Windows 下 Rust 绑定文件很多时SpacetimeDB CLI 可能已生成成功但 rustfmt 启动失败。
// 这里保留后续文件数量校验,避免把格式化警告误判成绑定生成失败。
console.warn(`[spacetime:generate] ${command} 生成后格式化失败,继续校验并同步生成文件。`);
}
resolve();
resolve({generatedFormatFailed: false});
return;
}

16
server-rs/Cargo.lock generated
View File

@@ -89,7 +89,7 @@ dependencies = [
"module-puzzle",
"module-runtime",
"module-runtime-item",
"module-runtime-story-compat",
"module-runtime-story",
"module-story",
"platform-auth",
"platform-llm",
@@ -1630,7 +1630,7 @@ dependencies = [
]
[[package]]
name = "module-runtime-story-compat"
name = "module-runtime-story"
version = "0.1.0"
dependencies = [
"serde_json",
@@ -1643,6 +1643,11 @@ dependencies = [
name = "module-story"
version = "0.1.0"
dependencies = [
"module-combat",
"module-inventory",
"module-progression",
"module-quest",
"module-runtime-item",
"serde",
"shared-kernel",
"spacetimedb",
@@ -1870,6 +1875,7 @@ dependencies = [
"time",
"tokio",
"tracing",
"url",
"urlencoding",
]
@@ -2674,9 +2680,11 @@ dependencies = [
"module-puzzle",
"module-runtime",
"module-runtime-item",
"module-runtime-story",
"module-story",
"serde",
"serde_json",
"shared-contracts",
"shared-kernel",
"spacetimedb-sdk",
"tokio",
@@ -3070,6 +3078,10 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "tests-support"
version = "0.1.0"
[[package]]
name = "thiserror"
version = "1.0.69"

View File

@@ -21,7 +21,7 @@ members = [
"crates/module-progression",
"crates/module-quest",
"crates/module-runtime",
"crates/module-runtime-story-compat",
"crates/module-runtime-story",
"crates/module-runtime-item",
"crates/module-story",
"crates/platform-oss",
@@ -32,6 +32,7 @@ members = [
"crates/shared-logging",
"crates/spacetime-client",
"crates/spacetime-module",
"crates/tests-support",
]
[workspace.package]

View File

@@ -40,7 +40,7 @@
22. 创建 `crates/platform-oss/` 目录占位,固定 OSS 平台适配 crate 落位。
23. 创建 `crates/platform-llm/` 目录占位,固定大模型平台适配 crate 落位。
24. 创建 `crates/spacetime-client/` 目录占位,固定 SpacetimeDB 客户端适配 crate 落位。
25. 创建 `crates/tests-support/` 目录占位,固定测试支撑共享 crate 落位。
25. 创建 `crates/tests-support/` 共享测试支撑 crate,固定 smoke/contract 测试辅助能力落位。
26. 创建 `scripts/dev.ps1`,固定 Windows 本地开发入口。
27. 创建 `scripts/dev.sh`,固定 Unix-like 本地开发入口。
28. 创建 `scripts/test.ps1`,固定 Windows 本地测试入口。
@@ -58,7 +58,7 @@
1. `crates/spacetime-module` 的表、reducer、view 聚合入口
2. `module-auth` 的身份表、JWT 与 refresh cookie 主链
3. `platform-oss` 的浏览器直传签名、旧 `/generated-*` 前缀映射与对象 URL 解析能力
3. `platform-oss` 的浏览器直传签名、旧 `/generated-*` 前缀到 OSS object key 的映射与对象 URL 解析能力;`/generated-*` 不再作为可裸读 HTTP 路由
当前本地脚本补充说明:
@@ -97,6 +97,18 @@
3. 前端或 Node 侧的 SpacetimeDB TypeScript SDK、订阅、绑定使用按 `spacetimedb-typescript``spacetimedb-concepts` 执行。
4. 若仓库内旧实现或旧文档与这些 skill 冲突,先修正文档和方案,再继续编码。
## 6. DDD 目录与边界
`2026-04-28` 起,`server-rs` 进入 DDD 边界收口阶段,完整规则见 [../docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md](../docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md)。
新增或迁移业务代码时必须遵守:
1. `module-*` 统一维护 `domain.rs``commands.rs``application.rs``events.rs``errors.rs`
2. `module-*` 不新增 Axum、reqwest、OSS、LLM、文件系统、SpacetimeDB table/reducer/procedure 依赖。
3. `mapper.rs` 只允许出现在 `api-server``spacetime-module``spacetime-client` 等 adapter crate。
4. `spacetime-module` 新增业务入口前先确认是否已有对应上下文目录,禁止继续把大段业务流程堆回 `src/lib.rs`
5. 根目录可执行 `npm run check:server-rs-ddd` 检查第一阶段 DDD 骨架与绝对边界。
## 5. 关联文档
1. [../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md)

View File

@@ -23,7 +23,7 @@ module-match3d = { path = "../module-match3d" }
module-npc = { path = "../module-npc" }
module-puzzle = { path = "../module-puzzle" }
module-runtime = { path = "../module-runtime" }
module-runtime-story-compat = { path = "../module-runtime-story-compat" }
module-runtime-story = { path = "../module-runtime-story" }
module-runtime-item = { path = "../module-runtime-item" }
module-story = { path = "../module-story" }
platform-auth = { path = "../platform-auth" }

View File

@@ -46,12 +46,10 @@
22. 接入 `POST /api/assets/sts-upload-credentials` 禁用式 STS 写权限 contract
23. 接入 `custom-world-library``custom-world-gallery` 与 agent `publish_world` 首批 Axum facade
24. 接入 custom world agent `session create / session snapshot` Axum facade
25. 接入`runtime story` 兼容接口:
- `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`
25.`runtime story` 兼容接口已下线并保持未挂载;运行时故事写链路改为
- `POST /api/story/sessions/runtime`
- `GET /api/story/sessions/{story_session_id}/runtime-projection`
- `POST /api/story/sessions/{story_session_id}/actions/resolve`
26. 接入 `POST /api/assets/character-visual/generate`
27. 接入 `GET /api/assets/character-visual/jobs/{task_id}`
28. 接入 `POST /api/assets/character-visual/publish`
@@ -62,7 +60,8 @@
33. 接入 `POST /api/assets/character-animation/generate`
34. 接入 `GET /api/assets/character-animation/jobs/{task_id}`
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 直接相关的任务包括:
@@ -87,12 +86,13 @@
19. [x] 接入 `/api/assets/sts-upload-credentials`
20. [x] 接入 `custom world library / gallery / publish_world` 首批 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
24. [x] 接入 `character-animation templates / import-video` 第一批 OSS 草稿兼容 facade
25. [x] 接入 `character-workflow-cache get / save` 第一批 OSS JSON 草稿兼容 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 约定:
@@ -161,10 +161,12 @@
12. 当前微信回调不会把第三方 token 直接透传给前端或 SpacetimeDB而是统一换成系统签发的 JWT。
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 的完整能力。
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 与本地角色覆盖写回仍在后续阶段。
17. 当前 `/api/assets/character-animation/import-video` 第一批只接受 `data:video/*;base64,...` 并写入 OSS 草稿区,不读取旧本地 `public/` 路径,也不创建正式 `asset_object`
18. 当前 `/api/assets/character-workflow-cache/*` 第一批只把工作流 JSON 草稿写入 OSS不迁移历史本地缓存也不创建正式 `asset_object`
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`
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

@@ -500,6 +500,7 @@ fn current_utc_micros() -> i64 {
#[cfg(test)]
mod tests {
use axum::{
Router,
body::Body,
http::{Request, StatusCode},
};
@@ -639,6 +640,129 @@ mod tests {
);
}
#[tokio::test]
async fn ai_task_mutation_routes_require_authentication() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
for route in ai_task_mutation_route_cases() {
let (status, _) = post_ai_task_route(app.clone(), route.uri, None, route.body).await;
assert_eq!(status, StatusCode::UNAUTHORIZED, "{}", route.uri);
}
}
#[tokio::test]
async fn ai_task_mutation_routes_return_bad_gateway_when_spacetime_not_published() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let app = build_router(state);
for route in ai_task_mutation_route_cases() {
let (status, payload) =
post_ai_task_route(app.clone(), route.uri, Some(&token), route.body).await;
assert_eq!(status, StatusCode::BAD_GATEWAY, "{}", route.uri);
assert_eq!(
payload["error"]["details"]["provider"],
Value::String("spacetimedb".to_string()),
"{}",
route.uri
);
}
}
struct AiTaskRouteCase {
uri: &'static str,
body: Option<Value>,
}
fn ai_task_mutation_route_cases() -> Vec<AiTaskRouteCase> {
vec![
AiTaskRouteCase {
uri: "/api/ai/tasks/aitask_001/stages/request_model/start",
body: None,
},
AiTaskRouteCase {
uri: "/api/ai/tasks/aitask_001/chunks",
body: Some(json!({
"stageKind": "request_model",
"sequence": 1,
"deltaText": "你听见远处的铃声。"
})),
},
AiTaskRouteCase {
uri: "/api/ai/tasks/aitask_001/stages/request_model/complete",
body: Some(json!({
"textOutput": "你听见远处的铃声。",
"structuredPayloadJson": "{\"scene\":\"camp\"}",
"warningMessages": []
})),
},
AiTaskRouteCase {
uri: "/api/ai/tasks/aitask_001/references",
body: Some(json!({
"referenceKind": "story_event",
"referenceId": "storyevt_001",
"label": "营地开场"
})),
},
AiTaskRouteCase {
uri: "/api/ai/tasks/aitask_001/complete",
body: None,
},
AiTaskRouteCase {
uri: "/api/ai/tasks/aitask_001/fail",
body: Some(json!({
"failureMessage": "模型返回内容为空"
})),
},
AiTaskRouteCase {
uri: "/api/ai/tasks/aitask_001/cancel",
body: None,
},
]
}
async fn post_ai_task_route(
app: Router,
uri: &str,
bearer_token: Option<&str>,
body: Option<Value>,
) -> (StatusCode, Value) {
let mut request = Request::builder()
.method("POST")
.uri(uri)
.header("x-genarrative-response-envelope", "v1");
if let Some(token) = bearer_token {
request = request.header("authorization", format!("Bearer {token}"));
}
let body = if let Some(payload) = body {
request = request.header("content-type", "application/json");
Body::from(payload.to_string())
} else {
Body::empty()
};
let response = app
.oneshot(request.body(body).expect("request should build"))
.await
.expect("request should succeed");
let status = response.status();
let body = response
.into_body()
.collect()
.await
.expect("body should collect")
.to_bytes();
let payload = if body.is_empty() {
Value::Null
} else {
serde_json::from_slice(&body).expect("response body should be valid json")
};
(status, payload)
}
async fn seed_authenticated_state() -> AppState {
let state = AppState::new(AppConfig::default()).expect("state should build");
state

View File

@@ -30,10 +30,11 @@ use crate::{
auth_public_user::{get_public_user_by_code, get_public_user_by_id},
auth_sessions::auth_sessions,
big_fish::{
create_big_fish_session, delete_big_fish_work, execute_big_fish_action,
create_big_fish_session, delete_big_fish_work, execute_big_fish_action, get_big_fish_run,
get_big_fish_session, get_big_fish_works, list_big_fish_gallery,
record_big_fish_gallery_like, record_big_fish_play, remix_big_fish_gallery_work,
stream_big_fish_message, submit_big_fish_message,
start_big_fish_run, stream_big_fish_message, submit_big_fish_input,
submit_big_fish_message,
},
character_animation_assets::{
generate_character_animation, get_character_animation_job, get_character_workflow_cache,
@@ -65,12 +66,6 @@ use crate::{
},
error_middleware::normalize_error_response,
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,
login_options::auth_login_options,
logout::logout,
@@ -88,11 +83,11 @@ use crate::{
phone_auth::{phone_login, send_phone_code},
profile_identity::update_profile_identity,
puzzle::{
advance_local_puzzle_next_level, advance_puzzle_next_level,
claim_puzzle_work_point_incentive, create_puzzle_agent_session, delete_puzzle_work,
execute_puzzle_agent_action, get_puzzle_agent_session, get_puzzle_gallery_detail,
get_puzzle_run, get_puzzle_work_detail, get_puzzle_works, list_puzzle_gallery,
put_puzzle_work, record_puzzle_gallery_like, remix_puzzle_gallery_work, start_puzzle_run,
advance_puzzle_next_level, claim_puzzle_work_point_incentive, create_puzzle_agent_session,
delete_puzzle_work, drag_puzzle_piece_or_group, execute_puzzle_agent_action,
get_puzzle_agent_session, get_puzzle_gallery_detail, get_puzzle_run,
get_puzzle_work_detail, get_puzzle_works, list_puzzle_gallery, put_puzzle_work,
record_puzzle_gallery_like, remix_puzzle_gallery_work, start_puzzle_run,
stream_puzzle_agent_message, submit_puzzle_agent_message, submit_puzzle_leaderboard,
swap_puzzle_pieces, update_puzzle_run_pause, use_puzzle_runtime_prop,
},
@@ -120,16 +115,14 @@ use crate::{
put_runtime_snapshot, resume_profile_save_archive,
},
runtime_settings::{get_runtime_settings, put_runtime_settings},
runtime_story::{
begin_runtime_story_session, generate_runtime_story_continue,
generate_runtime_story_initial, get_runtime_story_state, resolve_runtime_story_action,
resolve_runtime_story_state,
},
state::AppState,
story_battles::{
create_story_battle, create_story_npc_battle, get_story_battle_state, resolve_story_battle,
},
story_sessions::{begin_story_session, continue_story, get_story_session_state},
story_sessions::{
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},
};
@@ -212,38 +205,6 @@ pub fn build_router(state: AppState) -> Router {
"/api/auth/public-users/by-id/{user_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(
"/api/auth/me",
get(auth_me).route_layer(middleware::from_fn_with_state(
@@ -713,6 +674,27 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/big-fish/sessions/{session_id}/runs",
post(start_big_fish_run).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/big-fish/runs/{run_id}",
get(get_big_fish_run).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/big-fish/runs/{run_id}/input",
post(submit_big_fish_input).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/match3d/sessions",
post(create_match3d_agent_session).route_layer(middleware::from_fn_with_state(
@@ -918,13 +900,6 @@ pub fn build_router(state: AppState) -> Router {
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(
"/api/runtime/puzzle/runs/{run_id}",
get(get_puzzle_run).route_layer(middleware::from_fn_with_state(
@@ -939,6 +914,13 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/puzzle/runs/{run_id}/drag",
post(drag_puzzle_piece_or_group).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/puzzle/runs/{run_id}/next-level",
post(advance_puzzle_next_level).route_layer(middleware::from_fn_with_state(
@@ -974,13 +956,6 @@ pub fn build_router(state: AppState) -> Router {
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(
"/api/runtime/custom-world/entity",
post(generate_custom_world_entity).route_layer(middleware::from_fn_with_state(
@@ -988,13 +963,6 @@ pub fn build_router(state: AppState) -> Router {
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(
"/api/runtime/custom-world/scene-npc",
post(generate_custom_world_scene_npc).route_layer(middleware::from_fn_with_state(
@@ -1003,19 +971,12 @@ pub fn build_router(state: AppState) -> Router {
)),
)
.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(
state.clone(),
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(
"/api/runtime/custom-world/cover-image",
post(generate_custom_world_cover_image).route_layer(middleware::from_fn_with_state(
@@ -1023,13 +984,6 @@ pub fn build_router(state: AppState) -> Router {
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(
"/api/runtime/custom-world/cover-upload",
post(upload_custom_world_cover_image).route_layer(middleware::from_fn_with_state(
@@ -1037,16 +991,6 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/profile/browse-history",
get(get_runtime_browse_history)
.post(post_runtime_browse_history)
.delete(delete_runtime_browse_history)
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/profile/browse-history",
get(get_runtime_browse_history)
@@ -1057,13 +1001,6 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/profile/dashboard",
get(get_profile_dashboard).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/profile/dashboard",
get(get_profile_dashboard).route_layer(middleware::from_fn_with_state(
@@ -1071,13 +1008,6 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/profile/wallet-ledger",
get(get_profile_wallet_ledger).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/profile/wallet-ledger",
get(get_profile_wallet_ledger).route_layer(middleware::from_fn_with_state(
@@ -1085,13 +1015,6 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/profile/recharge-center",
get(get_profile_recharge_center).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/profile/recharge-center",
get(get_profile_recharge_center).route_layer(middleware::from_fn_with_state(
@@ -1099,13 +1022,6 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/profile/recharge/orders",
post(create_profile_recharge_order).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/profile/recharge/orders",
post(create_profile_recharge_order).route_layer(middleware::from_fn_with_state(
@@ -1113,13 +1029,6 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/profile/referrals/invite-center",
get(get_profile_referral_invite_center).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/profile/referrals/invite-center",
get(get_profile_referral_invite_center).route_layer(middleware::from_fn_with_state(
@@ -1127,13 +1036,6 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/profile/referrals/redeem-code",
post(redeem_profile_referral_invite_code).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/profile/referrals/redeem-code",
post(redeem_profile_referral_invite_code).route_layer(middleware::from_fn_with_state(
@@ -1141,13 +1043,6 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/profile/redeem-codes/redeem",
post(redeem_profile_reward_code).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/profile/redeem-codes/redeem",
post(redeem_profile_reward_code).route_layer(middleware::from_fn_with_state(
@@ -1155,20 +1050,6 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/profile/play-stats",
get(get_profile_play_stats).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/profile/save-archives",
get(list_profile_save_archives).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/profile/save-archives",
get(list_profile_save_archives).route_layer(middleware::from_fn_with_state(
@@ -1176,13 +1057,6 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/profile/save-archives/{world_key}",
post(resume_profile_save_archive).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/profile/save-archives/{world_key}",
post(resume_profile_save_archive).route_layer(middleware::from_fn_with_state(
@@ -1197,48 +1071,6 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/story/sessions",
post(begin_runtime_story_session).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/story/state/resolve",
post(resolve_runtime_story_state).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/story/state/{session_id}",
get(get_runtime_story_state).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/story/actions/resolve",
post(resolve_runtime_story_action).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/story/initial",
post(generate_runtime_story_initial).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/story/continue",
post(generate_runtime_story_continue).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/profile/play-stats",
get(get_profile_play_stats).route_layer(middleware::from_fn_with_state(
@@ -1253,6 +1085,13 @@ pub fn build_router(state: AppState) -> Router {
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(
"/api/story/sessions/{story_session_id}/state",
get(get_story_session_state).route_layer(middleware::from_fn_with_state(
@@ -1260,6 +1099,20 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/story/sessions/{story_session_id}/runtime-projection",
get(get_story_runtime_projection).route_layer(middleware::from_fn_with_state(
state.clone(),
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(
"/api/story/sessions/continue",
post(continue_story).route_layer(middleware::from_fn_with_state(
@@ -1542,6 +1395,132 @@ mod tests {
);
}
#[tokio::test]
async fn runtime_story_legacy_routes_are_not_mounted() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
for (method, uri) in [
("POST", "/api/runtime/story/sessions"),
("POST", "/api/runtime/story/state/resolve"),
("GET", "/api/runtime/story/state/runtime-main"),
("POST", "/api/runtime/story/actions/resolve"),
("POST", "/api/runtime/story/initial"),
("POST", "/api/runtime/story/continue"),
] {
let response = app
.clone()
.oneshot(
Request::builder()
.method(method)
.uri(uri)
.header("x-genarrative-response-envelope", "v1")
.body(Body::empty())
.expect("legacy runtime story request should build"),
)
.await
.expect("legacy runtime story request should be handled");
assert_eq!(response.status(), StatusCode::NOT_FOUND);
let body = response
.into_body()
.collect()
.await
.expect("legacy runtime story body should collect")
.to_bytes();
let payload: Value =
serde_json::from_slice(&body).expect("legacy runtime story body should be json");
assert_eq!(payload["ok"], Value::Bool(false));
assert_eq!(
payload["error"]["code"],
Value::String("NOT_FOUND".to_string())
);
assert_eq!(
payload["error"]["message"],
Value::String("资源不存在".to_string())
);
}
}
#[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]
async fn internal_auth_claims_rejects_missing_bearer_token() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));

View File

@@ -24,7 +24,7 @@ use spacetime_client::SpacetimeClientError;
use crate::{
api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError,
request_context::RequestContext, state::AppState,
platform_errors::map_oss_error, request_context::RequestContext, state::AppState,
};
// 历史素材类型需要与 SpacetimeDB 侧白名单保持同一口径,避免新增素材类型时 HTTP 门面漏同步。
@@ -402,17 +402,7 @@ fn map_confirm_asset_object_prepare_error(error: ConfirmAssetObjectPrepareError)
"message": error.to_string(),
}))
}
ConfirmAssetObjectPrepareError::Oss(platform_oss::OssError::ObjectNotFound(_)) => {
AppError::from_status(StatusCode::NOT_FOUND).with_details(json!({
"provider": "aliyun-oss",
"message": error.to_string(),
}))
}
ConfirmAssetObjectPrepareError::Oss(_) => AppError::from_status(StatusCode::BAD_GATEWAY)
.with_details(json!({
"provider": "aliyun-oss",
"message": error.to_string(),
})),
ConfirmAssetObjectPrepareError::Oss(error) => map_oss_error(error, "aliyun-oss"),
}
}

View File

@@ -23,9 +23,10 @@ use shared_contracts::big_fish::{
BigFishActionResponse, BigFishAgentMessageResponse, BigFishAnchorItemResponse,
BigFishAnchorPackResponse, BigFishAssetCoverageResponse, BigFishAssetSlotResponse,
BigFishBackgroundBlueprintResponse, BigFishGameDraftResponse, BigFishLevelBlueprintResponse,
BigFishRuntimeParamsResponse, BigFishSessionResponse, BigFishSessionSnapshotResponse,
CreateBigFishSessionRequest, ExecuteBigFishActionRequest, RecordBigFishPlayRequest,
SendBigFishMessageRequest,
BigFishRunResponse, BigFishRuntimeEntityResponse, BigFishRuntimeParamsResponse,
BigFishRuntimeSnapshotResponse, BigFishSessionResponse, BigFishSessionSnapshotResponse,
BigFishVector2Response, CreateBigFishSessionRequest, ExecuteBigFishActionRequest,
RecordBigFishPlayRequest, SendBigFishMessageRequest, SubmitBigFishInputRequest,
};
use shared_contracts::big_fish_works::{BigFishWorkSummaryResponse, BigFishWorksResponse};
use shared_kernel::{build_prefixed_uuid_id, format_timestamp_micros};
@@ -33,10 +34,11 @@ use spacetime_client::{
BigFishAgentMessageRecord, BigFishAnchorItemRecord, BigFishAnchorPackRecord,
BigFishAssetCoverageRecord, BigFishAssetGenerateRecordInput, BigFishAssetSlotRecord,
BigFishBackgroundBlueprintRecord, BigFishDraftCompileRecordInput, BigFishGameDraftRecord,
BigFishLevelBlueprintRecord, BigFishLikeReportRecordInput, BigFishMessageSubmitRecordInput,
BigFishPlayReportRecordInput, BigFishRuntimeParamsRecord, BigFishSessionCreateRecordInput,
BigFishSessionRecord, BigFishWorkRemixRecordInput, BigFishWorkSummaryRecord,
SpacetimeClientError,
BigFishInputSubmitRecordInput, BigFishLevelBlueprintRecord, BigFishLikeReportRecordInput,
BigFishMessageSubmitRecordInput, BigFishPlayReportRecordInput, BigFishRunStartRecordInput,
BigFishRuntimeEntityRecord, BigFishRuntimeParamsRecord, BigFishRuntimeRunRecord,
BigFishSessionCreateRecordInput, BigFishSessionRecord, BigFishVector2Record,
BigFishWorkRemixRecordInput, BigFishWorkSummaryRecord, SpacetimeClientError,
};
use tokio::time::sleep;
@@ -59,6 +61,7 @@ use crate::{
auth::AuthenticatedAccessToken,
character_visual_assets::try_apply_background_alpha_to_png,
http_error::AppError,
platform_errors::map_oss_error,
request_context::RequestContext,
state::AppState,
work_author::resolve_work_author_by_user_id,
@@ -253,6 +256,35 @@ pub async fn record_big_fish_play(
))
}
pub async fn start_big_fish_run(
State(state): State<AppState>,
Path(session_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
) -> Result<Json<Value>, Response> {
ensure_non_empty(&request_context, &session_id, "sessionId")?;
let run = state
.spacetime_client()
.start_big_fish_run(BigFishRunStartRecordInput {
run_id: build_prefixed_uuid_id("big-fish-run-"),
session_id,
owner_user_id: authenticated.claims().user_id().to_string(),
started_at_micros: current_utc_micros(),
})
.await
.map_err(|error| {
big_fish_error_response(&request_context, map_big_fish_client_error(error))
})?;
Ok(json_success_body(
Some(&request_context),
BigFishRunResponse {
run: map_big_fish_run_response(run),
},
))
}
pub async fn record_big_fish_gallery_like(
State(state): State<AppState>,
Path(session_id): Path<String>,
@@ -284,6 +316,73 @@ pub async fn record_big_fish_gallery_like(
))
}
pub async fn get_big_fish_run(
State(state): State<AppState>,
Path(run_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
) -> Result<Json<Value>, Response> {
ensure_non_empty(&request_context, &run_id, "runId")?;
let run = state
.spacetime_client()
.get_big_fish_run(run_id, authenticated.claims().user_id().to_string())
.await
.map_err(|error| {
big_fish_error_response(&request_context, map_big_fish_client_error(error))
})?;
Ok(json_success_body(
Some(&request_context),
BigFishRunResponse {
run: map_big_fish_run_response(run),
},
))
}
pub async fn submit_big_fish_input(
State(state): State<AppState>,
Path(run_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
payload: Result<Json<SubmitBigFishInputRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
let Json(payload) = payload.map_err(|error| {
big_fish_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "big-fish",
"message": error.body_text(),
})),
)
})?;
ensure_non_empty(&request_context, &run_id, "runId")?;
if !payload.x.is_finite() || !payload.y.is_finite() {
return Err(big_fish_bad_request(&request_context, "input is invalid"));
}
let run = state
.spacetime_client()
.submit_big_fish_input(BigFishInputSubmitRecordInput {
run_id,
owner_user_id: authenticated.claims().user_id().to_string(),
x: payload.x,
y: payload.y,
submitted_at_micros: current_utc_micros(),
})
.await
.map_err(|error| {
big_fish_error_response(&request_context, map_big_fish_client_error(error))
})?;
Ok(json_success_body(
Some(&request_context),
BigFishRunResponse {
run: map_big_fish_run_response(run),
},
))
}
pub async fn remix_big_fish_gallery_work(
State(state): State<AppState>,
Path(session_id): Path<String>,
@@ -862,6 +961,51 @@ fn map_big_fish_asset_coverage_response(
}
}
fn map_big_fish_run_response(run: BigFishRuntimeRunRecord) -> BigFishRuntimeSnapshotResponse {
BigFishRuntimeSnapshotResponse {
run_id: run.run_id,
session_id: run.session_id,
status: run.status,
tick: run.tick,
player_level: run.player_level,
win_level: run.win_level,
leader_entity_id: run.leader_entity_id,
owned_entities: run
.owned_entities
.into_iter()
.map(map_big_fish_runtime_entity_response)
.collect(),
wild_entities: run
.wild_entities
.into_iter()
.map(map_big_fish_runtime_entity_response)
.collect(),
camera_center: map_big_fish_vector2_response(run.camera_center),
last_input: map_big_fish_vector2_response(run.last_input),
event_log: run.event_log,
updated_at: run.updated_at,
}
}
fn map_big_fish_runtime_entity_response(
entity: BigFishRuntimeEntityRecord,
) -> BigFishRuntimeEntityResponse {
BigFishRuntimeEntityResponse {
entity_id: entity.entity_id,
level: entity.level,
position: map_big_fish_vector2_response(entity.position),
radius: entity.radius,
offscreen_seconds: entity.offscreen_seconds,
}
}
fn map_big_fish_vector2_response(vector: BigFishVector2Record) -> BigFishVector2Response {
BigFishVector2Response {
x: vector.x,
y: vector.y,
}
}
async fn compile_big_fish_draft_only(
state: &AppState,
session_id: String,
@@ -1642,19 +1786,7 @@ fn map_big_fish_asset_spacetime_error(error: SpacetimeClientError) -> AppError {
}
fn map_big_fish_asset_oss_error(error: platform_oss::OssError) -> AppError {
let status = match error {
platform_oss::OssError::InvalidConfig(_) | platform_oss::OssError::InvalidRequest(_) => {
StatusCode::BAD_REQUEST
}
platform_oss::OssError::ObjectNotFound(_) => StatusCode::NOT_FOUND,
platform_oss::OssError::Request(_)
| platform_oss::OssError::SerializePolicy(_)
| platform_oss::OssError::Sign(_) => StatusCode::BAD_GATEWAY,
};
AppError::from_status(status).with_details(json!({
"provider": "aliyun-oss",
"message": error.to_string(),
}))
map_oss_error(error, "aliyun-oss")
}
fn build_big_fish_level_part(level: Option<u32>) -> String {
@@ -1731,6 +1863,14 @@ fn map_big_fish_client_error(error: SpacetimeClientError) -> AppError {
{
StatusCode::NOT_FOUND
}
SpacetimeClientError::Procedure(message)
if message.contains("big_fish_runtime_run 不存在") =>
{
StatusCode::NOT_FOUND
}
SpacetimeClientError::Procedure(message) if message.contains("无权访问") => {
StatusCode::FORBIDDEN
}
SpacetimeClientError::Procedure(message)
if message.contains("不能为空")
|| message.contains("尚未编译")

View File

@@ -50,6 +50,7 @@ use crate::{
build_character_animation_prompt, build_fallback_moderation_safe_animation_prompt,
},
http_error::AppError,
platform_errors::map_oss_error,
prompt::role_asset_studio::{
build_role_asset_workflow, normalize_animation_prompt_text_by_key,
},
@@ -1639,7 +1640,9 @@ async fn load_workflow_cache(
expire_seconds: Some(60),
}) {
Ok(signed) => signed,
Err(platform_oss::OssError::ObjectNotFound(_)) => return Ok(None),
Err(error) if error.kind() == platform_oss::OssErrorKind::ObjectNotFound => {
return Ok(None);
}
Err(error) => return Err(map_character_animation_oss_error(error)),
};
let response = reqwest::Client::new()
@@ -3303,19 +3306,7 @@ fn map_character_animation_spacetime_error(error: SpacetimeClientError) -> AppEr
}
fn map_character_animation_oss_error(error: platform_oss::OssError) -> AppError {
let status = match error {
platform_oss::OssError::InvalidConfig(_) | platform_oss::OssError::InvalidRequest(_) => {
StatusCode::BAD_REQUEST
}
platform_oss::OssError::ObjectNotFound(_) => StatusCode::NOT_FOUND,
platform_oss::OssError::Request(_)
| platform_oss::OssError::SerializePolicy(_)
| platform_oss::OssError::Sign(_) => StatusCode::BAD_GATEWAY,
};
AppError::from_status(status).with_details(json!({
"provider": "aliyun-oss",
"message": error.to_string(),
}))
map_oss_error(error, "aliyun-oss")
}
fn character_animation_error_response(

View File

@@ -38,6 +38,7 @@ use crate::{
build_fallback_moderation_safe_character_visual_prompt,
},
http_error::AppError,
platform_errors::map_oss_error,
request_context::RequestContext,
state::AppState,
};
@@ -1335,19 +1336,7 @@ fn map_character_visual_spacetime_error(error: SpacetimeClientError) -> AppError
}
fn map_character_visual_oss_error(error: platform_oss::OssError) -> AppError {
let status = match error {
platform_oss::OssError::InvalidConfig(_) | platform_oss::OssError::InvalidRequest(_) => {
StatusCode::BAD_REQUEST
}
platform_oss::OssError::ObjectNotFound(_) => StatusCode::NOT_FOUND,
platform_oss::OssError::Request(_)
| platform_oss::OssError::SerializePolicy(_)
| platform_oss::OssError::Sign(_) => StatusCode::BAD_GATEWAY,
};
AppError::from_status(status).with_details(json!({
"provider": "aliyun-oss",
"message": error.to_string(),
}))
map_oss_error(error, "aliyun-oss")
}
fn parse_json_payload(

View File

@@ -36,6 +36,7 @@ use crate::{
},
http_error::AppError,
llm_model_routing::CREATION_TEMPLATE_LLM_MODEL,
platform_errors::map_oss_error,
prompt::scene_background::{
DEFAULT_CUSTOM_WORLD_SCENE_IMAGE_NEGATIVE_PROMPT, SceneImagePromptLandmark,
SceneImagePromptParams, SceneImagePromptProfile, build_custom_world_scene_image_prompt,
@@ -442,6 +443,8 @@ pub async fn generate_custom_world_scene_image(
let owner_user_id = authenticated.claims().user_id().to_string();
let normalized = normalize_scene_image_request(payload)
.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 = execute_billable_asset_operation(
&state,
@@ -703,6 +706,8 @@ pub async fn generate_custom_world_cover_image(
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 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 = execute_billable_asset_operation(
&state,
@@ -1017,19 +1022,7 @@ fn map_custom_world_asset_spacetime_error(error: SpacetimeClientError) -> AppErr
}
fn map_custom_world_asset_oss_error(error: platform_oss::OssError) -> AppError {
let status = match error {
platform_oss::OssError::InvalidConfig(_) | platform_oss::OssError::InvalidRequest(_) => {
StatusCode::BAD_REQUEST
}
platform_oss::OssError::ObjectNotFound(_) => StatusCode::NOT_FOUND,
platform_oss::OssError::Request(_)
| platform_oss::OssError::SerializePolicy(_)
| platform_oss::OssError::Sign(_) => StatusCode::BAD_GATEWAY,
};
AppError::from_status(status).with_details(json!({
"provider": "aliyun-oss",
"message": error.to_string(),
}))
map_oss_error(error, "aliyun-oss")
}
async fn generate_entity_with_fallback(state: &AppState, profile: &Value, kind: &str) -> Value {
@@ -2458,10 +2451,16 @@ mod tests {
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]
async fn scene_image_returns_service_unavailable_when_dashscope_missing() {
let state = AppState::new(AppConfig::default()).expect("state should build");
let request_context = build_request_context("POST /api/custom-world/scene-image");
let state = build_state_without_dashscope_key();
let request_context = build_request_context("POST /api/runtime/custom-world/scene-image");
let authenticated = build_authenticated(&state);
let response = generate_custom_world_scene_image(
@@ -2530,15 +2529,27 @@ mod tests {
};
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!(normalized.prompt.contains("世界副标题:失落航线"));
assert!(normalized.prompt.contains("场景名称:礁石神殿"));
assert!(
normalized
.prompt
.contains("本次想要生成的画面内容:破碎神殿")
);
assert_eq!(normalized.prompt, manual_prompt);
assert!(normalized.prompt.contains("破碎神殿矗立在蓝绿色雾潮中"));
assert_ne!(
normalized.prompt,
"破碎神殿矗立在蓝绿色雾潮中,潮湿石阶上有幽光贝壳。"
@@ -2603,8 +2614,8 @@ mod tests {
#[tokio::test]
async fn cover_image_returns_service_unavailable_when_dashscope_missing() {
let state = AppState::new(AppConfig::default()).expect("state should build");
let request_context = build_request_context("POST /api/custom-world/cover-image");
let state = build_state_without_dashscope_key();
let request_context = build_request_context("POST /api/runtime/custom-world/cover-image");
let authenticated = build_authenticated(&state);
let response = generate_custom_world_cover_image(
@@ -2640,7 +2651,7 @@ mod tests {
#[tokio::test]
async fn cover_upload_rejects_invalid_data_url_before_touching_oss() {
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 response = upload_custom_world_cover_image(

View File

@@ -1,258 +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, 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 {
let status = match error {
platform_oss::OssError::InvalidConfig(_) | platform_oss::OssError::InvalidRequest(_) => {
StatusCode::BAD_REQUEST
}
platform_oss::OssError::ObjectNotFound(_) => StatusCode::NOT_FOUND,
platform_oss::OssError::Request(_)
| platform_oss::OssError::SerializePolicy(_)
| platform_oss::OssError::Sign(_) => StatusCode::BAD_GATEWAY,
};
AppError::from_status(status).with_details(json!({
"provider": "aliyun-oss",
"message": error.to_string(),
}))
}
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,17 +2,21 @@ use axum::{
Json,
extract::{Extension, State},
http::StatusCode,
response::Response,
response::{
IntoResponse, Response,
sse::{Event, Sse},
},
};
use platform_llm::{LlmError, LlmMessage, LlmMessageRole, LlmTextProtocol, LlmTextRequest};
use serde_json::Value;
use platform_llm::{LlmMessage, LlmMessageRole, LlmTextProtocol, LlmTextRequest};
use serde_json::{Value, json};
use shared_contracts::llm::{
LlmChatCompletionRequest, LlmChatCompletionResponse, LlmChatMessagePayload, LlmChatMessageRole,
};
use std::convert::Infallible;
use crate::{
api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError,
request_context::RequestContext, state::AppState,
platform_errors::map_llm_error, request_context::RequestContext, state::AppState,
};
pub async fn proxy_llm_chat_completions(
@@ -20,15 +24,7 @@ pub async fn proxy_llm_chat_completions(
Extension(request_context): Extension<RequestContext>,
Extension(_authenticated): Extension<AuthenticatedAccessToken>,
Json(payload): Json<LlmChatCompletionRequest>,
) -> Result<Json<Value>, Response> {
if payload.stream {
return Err(llm_error_response(
&request_context,
AppError::from_status(StatusCode::NOT_IMPLEMENTED)
.with_message("Rust `api-server` 首版暂不支持流式 LLM 代理"),
));
}
) -> Result<Response, Response> {
let llm_client = state.llm_client().ok_or_else(|| {
llm_error_response(
&request_context,
@@ -49,6 +45,10 @@ pub async fn proxy_llm_chat_completions(
enable_web_search: false,
};
if payload.stream {
return Ok(stream_llm_chat_completions(llm_client.clone(), request).into_response());
}
let response = llm_client
.request_text(request)
.await
@@ -62,7 +62,78 @@ pub async fn proxy_llm_chat_completions(
content: response.content,
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 {
@@ -75,39 +146,6 @@ fn map_chat_message(message: LlmChatMessagePayload) -> LlmMessage {
LlmMessage::new(role, message.content)
}
fn map_llm_error(error: LlmError) -> AppError {
match error {
LlmError::InvalidRequest(message) => {
AppError::from_status(StatusCode::BAD_REQUEST).with_message(message)
}
LlmError::InvalidConfig(message) => {
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_message(message)
}
LlmError::Upstream {
status_code: 429,
message,
} => AppError::from_status(StatusCode::TOO_MANY_REQUESTS).with_message(message),
LlmError::Upstream { message, .. } => {
AppError::from_status(StatusCode::BAD_GATEWAY).with_message(message)
}
LlmError::Timeout { attempts } => AppError::from_status(StatusCode::BAD_GATEWAY)
.with_message(format!("LLM 请求超时,累计尝试 {attempts}")),
LlmError::Connectivity { attempts, message } => {
AppError::from_status(StatusCode::BAD_GATEWAY)
.with_message(format!("LLM 连接失败,累计尝试 {attempts} 次:{message}"))
}
LlmError::StreamUnavailable => {
AppError::from_status(StatusCode::BAD_GATEWAY).with_message("LLM 流式响应体不可用")
}
LlmError::EmptyResponse => {
AppError::from_status(StatusCode::BAD_GATEWAY).with_message("LLM 返回内容为空")
}
LlmError::Transport(message) | LlmError::Deserialize(message) => {
AppError::from_status(StatusCode::BAD_GATEWAY).with_message(message)
}
}
}
fn llm_error_response(request_context: &RequestContext, error: AppError) -> Response {
error.into_response_with_context(Some(request_context))
}
@@ -139,6 +177,7 @@ mod tests {
status_line: &'static str,
content_type: &'static str,
body: String,
extra_headers: Vec<(&'static str, &'static str)>,
}
#[tokio::test]
@@ -147,6 +186,7 @@ mod tests {
status_line: "200 OK",
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(),
extra_headers: Vec::new(),
}]);
let state = seed_authenticated_state(AppConfig {
llm_base_url: server_url,
@@ -210,8 +250,25 @@ mod tests {
}
#[tokio::test]
async fn llm_chat_completions_rejects_stream_mode() {
let state = seed_authenticated_state(AppConfig::default()).await;
async fn llm_chat_completions_streams_sse_payload() {
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 app = build_router(state);
@@ -222,7 +279,6 @@ mod tests {
.uri("/api/llm/chat/completions")
.header("authorization", format!("Bearer {token}"))
.header("content-type", "application/json")
.header("x-genarrative-response-envelope", "v1")
.body(Body::from(
json!({
"stream": true,
@@ -237,7 +293,14 @@ mod tests {
.await
.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
.into_body()
@@ -245,14 +308,15 @@ mod tests {
.await
.expect("body should collect")
.to_bytes();
let payload: Value =
serde_json::from_slice(&body).expect("response body should be valid json");
let body_text = String::from_utf8(body.to_vec()).expect("body should be utf8");
assert_eq!(payload["ok"], Value::Bool(false));
assert_eq!(
payload["error"]["code"],
Value::String("NOT_IMPLEMENTED".to_string())
);
assert!(body_text.contains("event: delta"));
assert!(body_text.contains(r#""delta":"你""#));
assert!(body_text.contains(r#""content":"你好""#));
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 {
@@ -340,13 +404,17 @@ mod tests {
fn write_response(stream: &mut std::net::TcpStream, response: MockResponse) {
let body = response.body;
let raw_response = format!(
"HTTP/1.1 {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
let mut raw_response = format!(
"HTTP/1.1 {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n",
response.status_line,
response.content_type,
body.len(),
body
body.len()
);
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
.write_all(raw_response.as_bytes())

View File

@@ -34,7 +34,6 @@ mod custom_world_rpg_draft_prompts;
mod error_middleware;
mod health;
mod http_error;
mod legacy_generated_assets;
mod llm;
mod llm_model_routing;
mod login_options;
@@ -44,6 +43,7 @@ mod match3d;
mod password_entry;
mod password_management;
mod phone_auth;
mod platform_errors;
mod profile_identity;
mod prompt;
mod puzzle;
@@ -59,7 +59,6 @@ mod runtime_inventory;
mod runtime_profile;
mod runtime_save;
mod runtime_settings;
mod runtime_story;
mod session_client;
mod state;
mod story_battles;

View File

@@ -1,7 +1,7 @@
use axum::{
Json,
extract::{Extension, State},
http::{HeaderMap, HeaderValue, StatusCode},
http::{HeaderMap, StatusCode},
response::IntoResponse,
};
use module_auth::{
@@ -22,6 +22,7 @@ use crate::{
attach_set_cookie_header, build_refresh_session_cookie_header, create_auth_session,
},
http_error::AppError,
platform_errors::{attach_retry_after, map_phone_auth_platform_store_error},
request_context::RequestContext,
session_client::resolve_session_client_context,
state::AppState,
@@ -303,10 +304,7 @@ pub fn map_phone_auth_error(error: PhoneAuthError) -> AppError {
let app_error = AppError::from_status(StatusCode::TOO_MANY_REQUESTS)
.with_message(error.to_string())
.with_details(json!({ "retryAfterSeconds": retry_after_seconds }));
match HeaderValue::from_str(&retry_after_seconds.to_string()) {
Ok(value) => app_error.with_header("retry-after", value),
Err(_) => app_error,
}
attach_retry_after(app_error, retry_after_seconds)
}
PhoneAuthError::VerifyAttemptsExceeded => {
AppError::from_status(StatusCode::TOO_MANY_REQUESTS).with_message(error.to_string())
@@ -315,7 +313,7 @@ pub fn map_phone_auth_error(error: PhoneAuthError) -> AppError {
AppError::from_status(StatusCode::UNAUTHORIZED).with_message(error.to_string())
}
PhoneAuthError::Store(_) | PhoneAuthError::PasswordHash(_) => {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string())
map_phone_auth_platform_store_error(error.to_string())
}
}
}

View File

@@ -0,0 +1,133 @@
use axum::http::{HeaderValue, StatusCode};
use platform_auth::{AuthPlatformErrorKind, WechatProviderError};
use platform_llm::{LlmError, LlmErrorKind};
use platform_oss::{OssError, OssErrorKind};
use serde_json::json;
use crate::http_error::AppError;
// API 层统一消费 platform 的稳定错误分类,避免各 route 重复 match 具体 provider 分支。
pub fn map_llm_error(error: LlmError) -> AppError {
let message = llm_error_message(&error);
let status = match error.kind() {
LlmErrorKind::InvalidRequest => StatusCode::BAD_REQUEST,
LlmErrorKind::InvalidConfig => StatusCode::SERVICE_UNAVAILABLE,
LlmErrorKind::Upstream
if matches!(
error,
LlmError::Upstream {
status_code: 429,
..
}
) =>
{
StatusCode::TOO_MANY_REQUESTS
}
LlmErrorKind::Timeout
| LlmErrorKind::Connectivity
| LlmErrorKind::Upstream
| LlmErrorKind::StreamUnavailable
| LlmErrorKind::EmptyResponse
| LlmErrorKind::Transport
| LlmErrorKind::Deserialize => StatusCode::BAD_GATEWAY,
};
AppError::from_status(status).with_message(message)
}
pub fn map_oss_error(error: OssError, provider: &'static str) -> AppError {
let status = oss_error_status(error.kind());
AppError::from_status(status).with_details(json!({
"provider": provider,
"message": error.to_string(),
}))
}
pub fn map_phone_auth_platform_store_error(message: String) -> AppError {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(message)
}
pub fn map_wechat_provider_error(error: WechatProviderError) -> AppError {
let status = match error.kind() {
AuthPlatformErrorKind::Disabled
| AuthPlatformErrorKind::MissingCode
| AuthPlatformErrorKind::InvalidCallback => StatusCode::BAD_REQUEST,
AuthPlatformErrorKind::InvalidConfig => StatusCode::SERVICE_UNAVAILABLE,
AuthPlatformErrorKind::RequestFailed
| AuthPlatformErrorKind::DeserializeFailed
| AuthPlatformErrorKind::MissingProfile
| AuthPlatformErrorKind::Upstream => StatusCode::BAD_GATEWAY,
AuthPlatformErrorKind::InvalidClaims
| AuthPlatformErrorKind::SignFailed
| AuthPlatformErrorKind::VerifyFailed
| AuthPlatformErrorKind::CookieConfig
| AuthPlatformErrorKind::HashFailed
| AuthPlatformErrorKind::InvalidVerifyCode => StatusCode::INTERNAL_SERVER_ERROR,
};
AppError::from_status(status).with_message(error.to_string())
}
pub fn attach_retry_after(error: AppError, retry_after_seconds: u64) -> AppError {
match HeaderValue::from_str(&retry_after_seconds.to_string()) {
Ok(value) => error.with_header("retry-after", value),
Err(_) => error,
}
}
fn oss_error_status(kind: OssErrorKind) -> StatusCode {
match kind {
OssErrorKind::InvalidConfig | OssErrorKind::InvalidRequest => StatusCode::BAD_REQUEST,
OssErrorKind::ObjectNotFound => StatusCode::NOT_FOUND,
OssErrorKind::Request | OssErrorKind::SerializePolicy | OssErrorKind::Sign => {
StatusCode::BAD_GATEWAY
}
}
}
fn llm_error_message(error: &LlmError) -> String {
match error {
LlmError::InvalidConfig(message)
| LlmError::InvalidRequest(message)
| LlmError::Transport(message)
| LlmError::Deserialize(message) => message.clone(),
LlmError::Timeout { .. }
| LlmError::Connectivity { .. }
| LlmError::Upstream { .. }
| LlmError::StreamUnavailable
| LlmError::EmptyResponse => error.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn map_oss_error_uses_stable_kind_for_not_found() {
let error = map_oss_error(
OssError::ObjectNotFound("missing object".to_string()),
"oss",
);
assert_eq!(error.status_code(), StatusCode::NOT_FOUND);
}
#[test]
fn map_llm_error_preserves_upstream_rate_limit() {
let error = map_llm_error(LlmError::Upstream {
status_code: 429,
message: "too many requests".to_string(),
});
assert_eq!(error.status_code(), StatusCode::TOO_MANY_REQUESTS);
}
#[test]
fn map_wechat_provider_error_keeps_provider_boundary() {
let error = map_wechat_provider_error(WechatProviderError::MissingCode);
assert_eq!(error.status_code(), StatusCode::BAD_REQUEST);
assert_eq!(error.message(), "缺少微信授权 code");
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -258,7 +258,7 @@ mod tests {
.oneshot(
Request::builder()
.method("GET")
.uri("/api/runtime/profile/browse-history")
.uri("/api/profile/browse-history")
.body(Body::empty())
.expect("request should build"),
)
@@ -278,7 +278,7 @@ mod tests {
.oneshot(
Request::builder()
.method("POST")
.uri("/api/runtime/profile/browse-history")
.uri("/api/profile/browse-history")
.header("authorization", format!("Bearer {token}"))
.header("content-type", "application/json")
.header("x-genarrative-response-envelope", "v1")
@@ -324,7 +324,7 @@ mod tests {
.oneshot(
Request::builder()
.method("POST")
.uri("/api/runtime/profile/browse-history")
.uri("/api/profile/browse-history")
.header("authorization", format!("Bearer {token}"))
.header("content-type", "application/json")
.header("x-genarrative-response-envelope", "v1")
@@ -361,64 +361,6 @@ mod tests {
);
}
#[tokio::test]
async fn runtime_browse_history_compat_route_matches_main_route_error_shape() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let app = build_router(state);
let main_response = app
.clone()
.oneshot(
Request::builder()
.method("GET")
.uri("/api/runtime/profile/browse-history")
.header("authorization", format!("Bearer {token}"))
.header("x-genarrative-response-envelope", "v1")
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
let compat_response = app
.oneshot(
Request::builder()
.method("GET")
.uri("/api/profile/browse-history")
.header("authorization", format!("Bearer {token}"))
.header("x-genarrative-response-envelope", "v1")
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(main_response.status(), compat_response.status());
let main_body = main_response
.into_body()
.collect()
.await
.expect("body should collect")
.to_bytes();
let compat_body = compat_response
.into_body()
.collect()
.await
.expect("body should collect")
.to_bytes();
let main_payload: Value =
serde_json::from_slice(&main_body).expect("response body should be valid json");
let compat_payload: Value =
serde_json::from_slice(&compat_body).expect("response body should be valid json");
assert_eq!(
main_payload["error"]["details"]["provider"],
compat_payload["error"]["details"]["provider"]
);
}
async fn seed_authenticated_state() -> AppState {
let state = AppState::new(AppConfig::default()).expect("state should build");
state

View File

@@ -10,10 +10,10 @@ use axum::{
use platform_llm::{LlmMessage, LlmTextRequest};
use serde::Deserialize;
use serde_json::{Value, json};
use shared_contracts::runtime_story::RuntimeStorySnapshotPayload;
use shared_contracts::story::StoryRuntimeSnapshotPayload as RuntimeStorySnapshotPayload;
use std::convert::Infallible;
use module_runtime_story_compat::{
use module_runtime_story::{
RuntimeStoryPromptContextExtras, build_runtime_story_prompt_context, current_world_type,
normalize_required_string, read_array_field, read_field, read_i32_field, read_object_field,
read_optional_string_field, read_runtime_session_id,

View File

@@ -10,7 +10,7 @@ use axum::{
use platform_llm::{LlmMessage, LlmTextRequest};
use serde::Deserialize;
use serde_json::{Value, json};
use shared_contracts::runtime_story::RuntimeStorySnapshotPayload;
use shared_contracts::story::StoryRuntimeSnapshotPayload as RuntimeStorySnapshotPayload;
use std::convert::Infallible;
use crate::{
@@ -18,7 +18,7 @@ use crate::{
llm_model_routing::RPG_STORY_LLM_MODEL, prompt::runtime_chat::*,
request_context::RequestContext, state::AppState,
};
use module_runtime_story_compat::{
use module_runtime_story::{
RuntimeStoryPromptContextExtras, build_runtime_story_prompt_context, current_world_type,
normalize_required_string, read_array_field, read_field, read_runtime_session_id,
};

View File

@@ -685,7 +685,7 @@ mod tests {
.oneshot(
Request::builder()
.method("GET")
.uri("/api/runtime/profile/dashboard")
.uri("/api/profile/dashboard")
.body(Body::empty())
.expect("request should build"),
)
@@ -703,7 +703,7 @@ mod tests {
.oneshot(
Request::builder()
.method("GET")
.uri("/api/runtime/profile/wallet-ledger")
.uri("/api/profile/wallet-ledger")
.body(Body::empty())
.expect("request should build"),
)
@@ -721,7 +721,7 @@ mod tests {
.oneshot(
Request::builder()
.method("GET")
.uri("/api/runtime/profile/play-stats")
.uri("/api/profile/play-stats")
.body(Body::empty())
.expect("request should build"),
)
@@ -860,90 +860,36 @@ mod tests {
}
#[tokio::test]
async fn profile_dashboard_compat_route_matches_main_route_error_shape() {
assert_compat_route_matches_main_route_error_shape(
async fn runtime_profile_legacy_routes_are_not_mounted() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
for uri in [
"/api/runtime/profile/dashboard",
"/api/profile/dashboard",
)
.await;
}
#[tokio::test]
async fn profile_wallet_ledger_compat_route_matches_main_route_error_shape() {
assert_compat_route_matches_main_route_error_shape(
"/api/runtime/profile/wallet-ledger",
"/api/profile/wallet-ledger",
)
.await;
}
#[tokio::test]
async fn profile_play_stats_compat_route_matches_main_route_error_shape() {
assert_compat_route_matches_main_route_error_shape(
"/api/runtime/profile/recharge-center",
"/api/runtime/profile/recharge/orders",
"/api/runtime/profile/referrals/invite-center",
"/api/runtime/profile/referrals/redeem-code",
"/api/runtime/profile/redeem-codes/redeem",
"/api/runtime/profile/play-stats",
"/api/profile/play-stats",
)
.await;
}
"/api/runtime/profile/save-archives",
"/api/runtime/profile/save-archives/world-1",
"/api/runtime/profile/browse-history",
] {
let response = app
.clone()
.oneshot(
Request::builder()
.method("GET")
.uri(uri)
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
async fn assert_compat_route_matches_main_route_error_shape(
main_route: &str,
compat_route: &str,
) {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let app = build_router(state);
let main_response = app
.clone()
.oneshot(
Request::builder()
.method("GET")
.uri(main_route)
.header("authorization", format!("Bearer {token}"))
.header("x-genarrative-response-envelope", "v1")
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
let compat_response = app
.oneshot(
Request::builder()
.method("GET")
.uri(compat_route)
.header("authorization", format!("Bearer {token}"))
.header("x-genarrative-response-envelope", "v1")
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(main_response.status(), compat_response.status());
let main_body = main_response
.into_body()
.collect()
.await
.expect("body should collect")
.to_bytes();
let compat_body = compat_response
.into_body()
.collect()
.await
.expect("body should collect")
.to_bytes();
let main_payload: Value =
serde_json::from_slice(&main_body).expect("response body should be valid json");
let compat_payload: Value =
serde_json::from_slice(&compat_body).expect("response body should be valid json");
assert_eq!(
main_payload["error"]["details"]["provider"],
compat_payload["error"]["details"]["provider"]
);
assert_eq!(response.status(), StatusCode::NOT_FOUND, "{uri}");
}
}
async fn seed_authenticated_state() -> AppState {

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