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
This commit is contained in:
kdletters
2026-05-02 03:35:59 +08:00
513 changed files with 52813 additions and 6013 deletions

View File

@@ -2,6 +2,10 @@
日期:`2026-04-23`
更新:`2026-04-30`
> 状态说明:本文件中的管理员鉴权、`/admin/api/*` 管理接口、数据库概览与受控 API 调试设计继续有效;同源内嵌 HTML/CSS/JS 后台页面已废弃。后续后台 UI 迁移到独立前端工程,当前 `api-server` 不再挂载 `GET /admin` 页面入口。独立后台前端的产品边界见 [`../prd/ADMIN_WEB_CONSOLE_PRD_2026-04-30.md`](../prd/ADMIN_WEB_CONSOLE_PRD_2026-04-30.md),技术方案见 [`ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md`](./ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md)。
## 1. 目标
为当前 Rust `api-server` 增加一套同源后台管理服务,满足以下首版目标:
@@ -10,7 +14,7 @@
2. 支持独立的管理员鉴权,不允许普通玩家 JWT 越权访问。
3. 支持在后台查看当前服务与数据库概览信息。
4. 支持在后台测试当前 `api-server` 已挂载接口。
5. 保持首版工程足够轻量,不新建额外独立服务进程,不引入第二套前端工程。
5. 保持管理能力继续收口在 `server-rs`,管理 UI 由独立后台前端工程承接
## 2. 背景与约束
@@ -24,19 +28,20 @@
1. 后端统一落在 `server-rs`,不回退到 `server-node`
2. 不额外新起独立管理服务进程。
3. 首版以“一个受保护管理域 + 一个同源后台页面”为落地形态
3. 管理 API 继续作为受保护管理域挂载在 `api-server`
4. 数据库信息必须尽量读取真实数据库侧信息,不能只展示硬编码假数据。
## 3. 首版范围
### 3.1 包含
1. `GET /admin`:后台管理页面入口
2. `POST /admin/api/login`:管理员用户名密码登录
3. `GET /admin/api/me`:当前管理员会话信息
4. `GET /admin/api/overview`:服务与数据库概览
5. `POST /admin/api/debug/http`:受控 HTTP 接口调试
6. 基于 Bearer JWT 的管理员鉴权中间件
1. `POST /admin/api/login`:管理员用户名密码登录
2. `GET /admin/api/me`当前管理员会话信息
3. `GET /admin/api/overview`:服务与数据库概览
4. `POST /admin/api/debug/http`:受控 HTTP 接口调试
5. `POST /admin/api/profile/redeem-codes`:兑换码创建/更新
6. `POST /admin/api/profile/redeem-codes/disable`:兑换码停用
7. 基于 Bearer JWT 的管理员鉴权中间件。
### 3.2 不包含
@@ -44,7 +49,7 @@
2. 管理员 refresh cookie / 多端会话管理。
3. 后台直接写库、删库、执行 reducer。
4. 任意 SQL 执行器。
5. 新建独立 React/Vite 管理端工程
5. `api-server` 内嵌 HTML/CSS/JS 后台页面
## 4. 总体方案
@@ -60,13 +65,13 @@
### 4.2 页面形态
后台管理页面采用 `api-server` 直接返回一份内嵌 HTML/CSS/JS 的管理页
后台管理页面不再由 `api-server` 直接返回内嵌 HTML/CSS/JS`api-server` 仅保留管理 API页面由独立后台前端工程调用这些接口
原因:
1. 首版目标是“可用的后台能力”,不是新建一套复杂前端基建
2. 管理页面交互相对简单,直接内嵌更易随服务端一起部署
3. 可以避免新增构建链和静态资源发布路径
1. 管理 UI 需要独立演进,不应继续堆在 Rust 源码字符串中
2. `server-rs` 继续负责鉴权、聚合和写操作,符合前端只做表现的工程约束
3. 删除 `GET /admin` 后,当前服务访问该路径应返回 `404`
### 4.3 数据库信息来源
@@ -129,7 +134,7 @@ claims 设计:
## 6. 后台页面设计
首版页面包含三个主区域:
本节已由独立后台前端工程方案接管。历史同源页面包含三个主区域:
1. 登录卡片。
2. 数据库概览面板。
@@ -149,47 +154,11 @@ claims 设计:
2. 当前 `SpacetimeDB server/database` 配置。
3. `SpacetimeDB` 数据库基础信息。
4. 当前 schema 表清单。
5. 首批关键表的行数统计。
5. schema 表清单对应的逐表行数统计。
首批关键表固定覆盖:
表统计必须以 SpacetimeDB schema 返回的表名为唯一来源,`schemaTableNames` 的数量必须与 `tableStats` 的行数一致。后台服务只对 schema 中符合安全 SQL 标识符格式的表名发起 `SELECT COUNT(*)`,不提供任意 SQL 输入能力。
1. `runtime_setting`
2. `runtime_snapshot`
3. `user_browse_history`
4. `profile_dashboard_state`
5. `profile_wallet_ledger`
6. `profile_played_world`
7. `profile_save_archive`
8. `story_session`
9. `story_event`
10. `battle_state`
11. `inventory_slot`
12. `quest_record`
13. `quest_log`
14. `treasure_record`
15. `npc_state`
16. `custom_world_profile`
17. `custom_world_gallery_entry`
18. `custom_world_agent_session`
19. `custom_world_agent_message`
20. `custom_world_agent_operation`
21. `custom_world_draft_card`
22. `big_fish_creation_session`
23. `big_fish_agent_message`
24. `big_fish_asset_slot`
25. `big_fish_runtime_run`
26. `puzzle_work_profile`
27. `puzzle_agent_session`
28. `puzzle_agent_message`
29. `puzzle_runtime_run`
30. `ai_task`
31. `ai_task_stage`
32. `ai_text_chunk`
33. `ai_result_reference`
34. `asset_object`
35. `asset_entity_binding`
返回中的计数失败项必须带错误信息,不能静默吞掉。
返回中的计数失败项必须带错误信息不能静默吞掉。SpacetimeDB private 表或当前身份不可见的表可能在 `/sql` 下返回 `no such table` / `marked private`这类项统一展示为“不可统计private 或当前身份不可见)”,不作为整页读取失败处理。
## 8. API 调试设计
@@ -223,7 +192,7 @@ claims 设计:
默认策略:
1. 若未配置用户名或密码,则后台登录接口返回 `503`后台页面显示“后台未启用
1. 若未配置用户名或密码,则后台登录接口返回 `503`独立后台前端自行展示未启用状态
2. 默认管理员 token TTL 为 `4` 小时。
## 10. 测试要求
@@ -240,21 +209,27 @@ claims 设计:
## 11. 路由清单
首版新增路由:
当前保留的管理 API 路由:
1. `GET /admin`
2. `POST /admin/api/login`
3. `GET /admin/api/me`
4. `GET /admin/api/overview`
5. `POST /admin/api/debug/http`
1. `POST /admin/api/login`
2. `GET /admin/api/me`
3. `GET /admin/api/overview`
4. `POST /admin/api/debug/http`
5. `POST /admin/api/profile/redeem-codes`
6. `POST /admin/api/profile/redeem-codes/disable`
`GET /admin` 已取消挂载,后续由独立后台前端工程承接页面入口。
## 12. 完成定义
满足以下条件时,本任务视为完成:
当前管理 API 保留与内嵌页面移除满足以下条件时,本任务视为完成:
1. `api-server` 内存在受保护后台管理域。
2. 管理员用户名密码可登录。
3. 普通用户 token 无法访问后台接口。
4. 后台能看到服务和数据库真实概览。
5. 后台能调试当前服务 HTTP 接口。
6. 路由索引与技术文档已同步更新。
6. 兑换码管理 API 可由管理员 token 调用。
7. `GET /admin` 不再挂载,访问返回 `404`
8. 独立后台前端 PRD 与技术方案已补齐。
9. 路由索引与技术文档已同步更新。

View File

@@ -0,0 +1,454 @@
# 后台管理独立前端工程技术方案
日期:`2026-04-30`
对应 PRD[后台管理独立前端工程 PRD](../prd/ADMIN_WEB_CONSOLE_PRD_2026-04-30.md)
落地状态:`2026-04-30` 已创建 `apps/admin-web` 独立前端工程包含登录、总览、API 调试、兑换码管理和注册邀请码管理首版页面;根工程已补 `admin-web:*` 转发脚本。`2026-05-01` 起,根构建与 Ubuntu 发布包会同步构建后台前端,并在发布包 Web 网关中以同域 `/admin/` 暴露。
## 1. 结论
后台管理端采用独立前端工程,路径固定为 `apps/admin-web`。它只负责 UI 表现、输入采集、请求发起和结果渲染所有鉴权、聚合、写操作、SpacetimeDB 访问和业务校验继续收口在 `server-rs/crates/api-server`
本方案接管旧 `api-server` 内嵌 HTML/CSS/JS 页面Rust `api-server` 直连时旧 `GET /admin` 不再挂载。部署态后台入口由发布包内 `web-server.mjs` 承接:`/admin/` 返回独立前端静态产物,`/admin/api/*` 继续反代到 `api-server`
## 2. 工程结构
建议首版结构:
```text
apps/
└─ admin-web/
├─ index.html
├─ package.json
├─ tsconfig.json
├─ vite.config.ts
└─ src/
├─ main.tsx
├─ app/
│ ├─ AdminApp.tsx
│ ├─ AdminShell.tsx
│ └─ adminRoutes.ts
├─ api/
│ ├─ adminApiClient.ts
│ └─ adminApiTypes.ts
├─ auth/
│ └─ adminAuthStore.ts
├─ pages/
│ ├─ AdminLoginPage.tsx
│ ├─ AdminOverviewPage.tsx
│ ├─ AdminDebugHttpPage.tsx
│ ├─ AdminRedeemCodePage.tsx
│ └─ AdminInviteCodePage.tsx
└─ styles/
└─ admin.css
```
首版可使用独立 `package.json`,不要求立刻把根工程改成 npm workspace。后续如果根工程统一 workspace再把 `apps/admin-web` 纳入统一脚本。
## 3. 技术栈
1. React + TypeScript + Vite。
2. 图标使用 `lucide-react`
3. 样式首版使用普通 CSS 或 CSS Modules不引入新的大型 UI 组件库。
4. 请求使用浏览器 `fetch` 封装,不新增状态管理库。
5. 不引入 SpacetimeDB TypeScript SDK管理端不直连 SpacetimeDB。
## 4. API 边界
### 4.1 基础约定
所有管理端请求使用同一个 `adminApiClient`
1. base URL 由 `VITE_ADMIN_API_BASE_URL` 配置。
2. 未配置时默认同源空前缀。
3. 有 token 时附加 `Authorization: Bearer <token>`
4. 后端统一响应 envelope 时,前端读取 `data`;错误优先读取 `error.details.message`,再读 `error.message`,最后回退到 HTTP 状态。
前端统一按以下响应形状解析,不在页面组件里重复拆 envelope
```ts
export interface ApiSuccessEnvelope<T> {
data: T;
meta?: unknown;
}
export interface ApiErrorEnvelope {
error?: {
code?: string;
message?: string;
details?: {
message?: string;
[key: string]: unknown;
} | null;
};
meta?: unknown;
}
```
`adminApiClient` 暴露 `request<T>()``get<T>()``post<T>()` 三层即可。页面只拿到成功数据或抛出的中文错误消息,不直接处理 `Response`
### 4.2 已有管理接口
| 功能 | 方法与路径 | 鉴权 |
| --- | --- | --- |
| 管理员登录 | `POST /admin/api/login` | 无 |
| 当前管理员 | `GET /admin/api/me` | 管理员 Bearer |
| 服务与数据库概览 | `GET /admin/api/overview` | 管理员 Bearer |
| 受控 HTTP 调试 | `POST /admin/api/debug/http` | 管理员 Bearer |
| 创建/更新兑换码 | `POST /admin/api/profile/redeem-codes` | 管理员 Bearer |
| 停用兑换码 | `POST /admin/api/profile/redeem-codes/disable` | 管理员 Bearer |
| 创建/更新注册邀请码 | `POST /admin/api/profile/invite-codes` | 管理员 Bearer |
### 4.3 前端类型命名
后台前端首版不引入自动生成 contract。为了避免字段漂移`apps/admin-web/src/api/adminApiTypes.ts` 必须按 `shared-contracts` 的 camelCase JSON 字段命名:
```ts
export interface AdminSessionPayload {
subject: string;
username: string;
displayName: string;
roles: string[];
issuedAt: string;
expiresAt: string;
}
export interface AdminLoginResponse {
token: string;
admin: AdminSessionPayload;
}
export interface AdminOverviewResponse {
service: AdminServiceOverviewPayload;
database: AdminDatabaseOverviewPayload;
}
export interface AdminServiceOverviewPayload {
bindHost: string;
bindPort: number;
jwtIssuer: string;
adminEnabled: boolean;
spacetimeServerUrl: string;
spacetimeDatabase: string;
}
export interface AdminDatabaseOverviewPayload {
databaseIdentity: string | null;
ownerIdentity: string | null;
hostType: string | null;
schemaTableNames: string[];
tableStats: AdminDatabaseTableStatPayload[];
fetchErrors: string[];
}
export interface AdminDatabaseTableStatPayload {
tableName: string;
rowCount: number | null;
errorMessage: string | null;
}
export interface AdminDebugHeaderInput {
name: string;
value: string;
}
export interface AdminDebugHttpRequest {
method: string;
path: string;
headers?: AdminDebugHeaderInput[];
body?: string;
}
export interface AdminDebugHttpResponse {
status: number;
statusText: string;
headers: AdminDebugHeaderInput[];
bodyText: string;
bodyJson: unknown | null;
}
```
兑换码类型同样保持 camelCase
```ts
export type ProfileRedeemCodeMode = 'public' | 'unique' | 'private';
export interface AdminUpsertProfileRedeemCodeRequest {
code: string;
mode: ProfileRedeemCodeMode;
rewardPoints: number;
maxUses: number;
enabled: boolean;
allowedUserIds: string[];
allowedPublicUserCodes: string[];
}
export interface AdminDisableProfileRedeemCodeRequest {
code: string;
}
export interface AdminUpsertProfileInviteCodeRequest {
inviteCode: string;
metadata?: Record<string, unknown>;
}
export interface ProfileRedeemCodeAdminResponse {
code: string;
mode: ProfileRedeemCodeMode;
rewardPoints: number;
maxUses: number;
globalUsedCount: number;
enabled: boolean;
allowedUserIds: string[];
createdBy: string;
createdAt: string;
updatedAt: string;
}
export interface ProfileInviteCodeAdminResponse {
userId: string;
inviteCode: string;
metadata: Record<string, unknown>;
createdAt: string;
updatedAt: string;
}
```
### 4.4 登录 contract
请求:
```json
{
"username": "root",
"password": "secret123"
}
```
成功数据:
```json
{
"token": "<admin bearer token>",
"admin": {
"subject": "admin:root",
"username": "root",
"displayName": "root",
"roles": ["admin"],
"issuedAt": "2026-04-30T00:00:00Z",
"expiresAt": "2026-04-30T04:00:00Z"
}
}
```
`503` 表示后台未启用;`401` 表示用户名或密码错误。
### 4.5 总览 contract
`GET /admin/api/overview` 返回:
1. `service``bindHost``bindPort``jwtIssuer``adminEnabled``spacetimeServerUrl``spacetimeDatabase`
2. `database``databaseIdentity``ownerIdentity``hostType``schemaTableNames``tableStats``fetchErrors`
后端读取 SpacetimeDB schema 时必须请求 `/v1/database/{database}/schema?version=9`。SpacetimeDB 2.x schema HTTP API 缺少 `version` query 会返回 `400 missing field version`,后台页面只能展示读取异常,不能拿到真实表名。
`schemaTableNames``tableStats` 必须采用同一份 schema 表清单生成不能再用硬编码关键表白名单补齐统计项。后台右上角显示的表数量必须等于统计表格实际行数schema 读取失败时两者均为空,并通过 `fetchErrors` 暴露读取失败原因。
后端读取表行数时必须按 SpacetimeDB 2.x `/sql` 响应解析:接口返回 statement result 数组,单条结果内的 `schema.elements` 描述列名,`rows` 是按列顺序排列的数组行,例如 `rows: [[0]]`。后台服务不能再假设响应是 `{ rows: [{ row_count: 0 }] }` 的对象行形状;为了兼容小版本差异,可保留对象行兜底解析。
`tableStats` 中单表失败必须展示 `errorMessage`不能让整页变成空白。SpacetimeDB private 表或当前身份不可见的表在 `/sql` 下可能返回 `no such table` / `marked private`后台服务必须将这类错误归一为“不可统计private 或当前身份不可见)”,避免把预期的访问边界展示成原始 HTTP 400 故障。
线上如果大量表都显示“不可统计private 或当前身份不可见)”,优先检查 `api-server` 启动环境中的 `GENARRATIVE_SPACETIME_TOKEN` / `GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN` 是否存在且属于目标库 owner。Jenkins 覆盖发布包时必须保留部署目录已有运行 token只带迁移 token 不能让后台概览读取 private 表。
### 4.6 API 调试 contract
请求:
```json
{
"method": "GET",
"path": "/healthz",
"headers": [],
"body": ""
}
```
限制由后端执行:
1. `path` 只允许同源相对路径。
2. 禁止绝对 URL。
3. 禁止调试 `/admin/api/login`
4. 禁止覆盖危险请求头。
5. 请求体大小和超时由后端收口。
### 4.7 兑换码管理 contract
创建/更新请求:
```json
{
"code": "WELCOME2026",
"mode": "public",
"rewardPoints": 100,
"maxUses": 1,
"enabled": true,
"allowedUserIds": [],
"allowedPublicUserCodes": []
}
```
停用请求:
兑换码管理页的最近一次接口返回记录由 `AdminApp` 维护为管理端会话态,并传入 `AdminRedeemCodePage` 渲染。页面页签通过 hash 切换时子页面会卸载,不能把最近记录只放在兑换码页面内部 `useState` 中,否则切换到其他页签再返回会展示“暂无记录”。该会话态只用于保留当前操作结果,不作为兑换码历史列表;退出登录或重新登录时清空。
```json
{
"code": "WELCOME2026"
}
```
成功返回兑换码记录:
```json
{
"code": "WELCOME2026",
"mode": "public",
"rewardPoints": 100,
"maxUses": 1,
"globalUsedCount": 0,
"enabled": true,
"allowedUserIds": [],
"createdBy": "admin:root",
"createdAt": "2026-04-30T00:00:00Z",
"updatedAt": "2026-04-30T00:00:00Z"
}
```
前端只做基础输入约束,最终标准化、私有码用户解析、次数和奖励合法性以 `server-rs` 为准。
### 4.8 邀请码管理 contract
创建/更新请求:
```json
{
"inviteCode": "SPRING2026",
"metadata": {
"batch": "spring"
}
}
```
成功返回邀请码记录:
```json
{
"userId": "admin",
"inviteCode": "SPRING2026",
"metadata": {
"batch": "spring"
},
"createdAt": "2026-04-30T00:00:00Z",
"updatedAt": "2026-04-30T00:00:00Z"
}
```
邀请码页的 metadata 输入必须先在前端解析为 JSON 对象;空字符串按 `{}` 处理,数组、字符串、数字等非对象值直接提示错误。最终标准化、长度限制和邀请码合法性以 `server-rs` 为准。
## 5. 鉴权与会话
1. token key 固定为 `genarrative_admin_token`
2. token 首版存 localStorage。
3. 应用启动时如果存在 token先调用 `GET /admin/api/me`
4. `401` 时清空 token 并回到登录页。
5. `403` 时展示无权限状态,不自动重试。
6. 退出登录只清理本地 token首版没有后台 refresh session 和服务端会话吊销。
## 6. 页面实现要点
1. `AdminShell` 承载导航、当前管理员、退出按钮和页面容器。
2. 登录页不进入 `AdminShell`,避免未登录时展示后台导航。
3. 总览页加载失败时展示后端错误,不吞掉 `fetchErrors`
4. API 调试页的 headers 使用键值行编辑,提交前转为 `[{ name, value }]`
5. 兑换码页的 `mode=private` 时展示允许用户输入区;其他模式提交空数组。
6. 邀请码页只提交 `inviteCode` 与 JSON 对象 metadata不在前端复制后端邀请码规则。
7. 所有按钮的 loading 状态必须锁定重复提交。
8. 移动端优先:表单单列,导航紧凑,结果面板可横向/纵向滚动。
## 7. 部署与联调
### 7.1 本地联调
1. 启动后端:`npm run api-server:maincloud`
2. 启动后台前端:在 `apps/admin-web` 执行 `npm run dev`
3. 后台 dev server 通过 Vite proxy 转发 `/admin/api``ADMIN_API_TARGET`;未配置时默认 `http://127.0.0.1:3100`
4. 若使用非 3100 端口,在仓库根目录 `.env.local` 设置 `ADMIN_API_TARGET=http://127.0.0.1:<api-server-port>`,并重启后台前端 dev server。
5. `GENARRATIVE_API_PORT` 控制 Rust `api-server` 监听端口;`ADMIN_API_TARGET` 只控制后台前端 dev proxy 目标,二者需要指向同一个端口。
### 7.2 构建部署
当前发布形态固定为同域 `/admin/`
1. 本地单独执行 `npm run admin-web:build` 时,后台构建产物默认输出到 `apps/admin-web/dist`
2. 根工程执行 `npm run build` 时,会先构建主前端,再构建后台前端;任一构建失败或输出 warning 都会让构建门禁失败。
3. Ubuntu 发布包执行 `npm run deploy:rust:remote` 时,后台前端以 Vite `--base /admin/` 构建到发布包 `web/admin/`
4. 发布包 `web-server.mjs``/admin` 返回 301 到 `/admin/`,对 `/admin/``/admin/*` 提供后台 SPA fallback`/admin/api/*` 优先反代到 `api-server`
该形态不新增后台静态端口和后台专用后端。`server-rs` 仍然是唯一管理 API 后端,后台前端不直连 SpacetimeDB。
### 7.3 后台工程脚本
`apps/admin-web/package.json` 首版至少提供以下脚本:
```json
{
"scripts": {
"dev": "vite --host 127.0.0.1",
"build": "node ../../scripts/admin-web-build.mjs build",
"typecheck": "node ../../scripts/admin-web-build.mjs typecheck",
"preview": "vite preview --host 127.0.0.1"
}
}
```
如果后续接入根 npm workspace再在根 `package.json` 增加转发脚本;本轮不要为了后台工程强行重排现有前端脚本。
当前工程没有启用 npm workspace因此后台构建脚本必须从仓库根目录调用 root toolchain。`scripts/admin-web-build.mjs` 统一执行 `tsc --noEmit -p apps/admin-web/tsconfig.json` 与 Vite 构建,避免 `npm --prefix apps/admin-web` 在子目录找不到 `tsc`
当前根工程同步提供以下转发脚本:
1. `npm run admin-web:dev`
2. `npm run admin-web:typecheck`
3. `npm run admin-web:build`
4. `npm run admin-web:preview`
## 8. 测试计划
1. `apps/admin-web`
- 登录成功和失败。
- token 恢复、过期清理、退出登录。
- 总览页正常数据、部分表统计失败、整体请求失败。
- API 调试成功访问 `/healthz`,绝对 URL 被后端拒绝。
- 兑换码 public/unique/private 表单提交和停用。
- 邀请码表单提交、metadata JSON 对象校验和结果展示。
2. 根工程:
- `npm run check:encoding`
- 后续接入根 workspace 后,补充后台工程 build/typecheck 脚本。
3. 后端:
- 继续保留 `cargo test -p api-server --manifest-path server-rs/Cargo.toml admin`
- 修改后端管理 API 后必须运行 `npm run api-server:maincloud` 并手动验证 `/admin` 为 404、`/admin/api/login` 可用。
## 9. 后续扩展边界
后续新增用户管理、作品审核、资产审核、订单/充值管理时,必须先补对应 PRD 和技术方案,并在 `server-rs` 增加受保护管理 API。不要让 `apps/admin-web` 直接读取 SpacetimeDB 或复制业务规则。
## 10. 实施顺序
1. 先创建 `apps/admin-web` 工程骨架,确保空应用可 `dev/build`
2. 再实现 `adminApiTypes``adminApiClient`,用 `/admin/api/login` 做第一条真实链路。
3. 接入 `adminAuthStore` 和启动恢复逻辑,确认 `401` 会清理本地 token。
4. 完成 `AdminShell` 与四页路由再分别接入总览、API 调试、兑换码和邀请码接口。
5. 最后补测试、运行 `npm run check:encoding`,并确认 `GET /admin` 仍由 `api-server` 返回 `404`
当前实现已完成第 1 至第 4 步。验证以实际命令输出为准。

View File

@@ -45,7 +45,7 @@
修复:
1.`map_password_entry_error(...)` 中补充 `InvalidPublicUserCode`
2. 返回中文错误文案 `叙世号格式不正确`
2. 返回中文错误文案 `百梦号格式不正确`
### 3.3 `module-custom-world` 的 `Display` 分支未覆盖新字段错误

View File

@@ -1,8 +1,8 @@
# 资产操作叙世币消耗接入方案
# 资产操作光点消耗接入方案
## 背景
当前叙世币钱包余额、充值流水与邀请奖励已经收口到 `server-rs/crates/spacetime-module/src/runtime/profile.rs`。资产图片生成和作品发布由 Axum API 调用外部模型或写入业务状态SpacetimeDB reducer/procedure 不能直接执行外部网络生成,因此计费需要拆成两层:
当前光点钱包余额、充值流水与邀请奖励已经收口到 `server-rs/crates/spacetime-module/src/runtime/profile.rs`。资产图片生成和作品发布由 Axum API 调用外部模型或写入业务状态SpacetimeDB reducer/procedure 不能直接执行外部网络生成,因此计费需要拆成两层:
- SpacetimeDB 负责钱包余额和流水的原子变更。
- Axum 资产操作服务负责在执行业务资产操作前扣费,并在生成、持久化或发布失败时补偿退款。
@@ -24,13 +24,13 @@
暂不接入以下入口:
- 旧资产工坊角色主形象/动作生成接口:当前仍使用 `asset-tool` 作为兼容归属,无法确认真实用户。
- 手动上传封面:不调用外部生成模型,不消耗叙世币
- 手动上传封面:不调用外部生成模型,不消耗光点
- 自定义世界草稿自动补图链路:属于后台补全流程,避免一次用户操作触发多笔不可预期扣费。
- 文本实体、NPC 生成:本次需求聚焦图片资产和发布资产操作,首期只覆盖可明确归属的入口。
## 计费规则
- 每次可计费资产操作消耗 `1`叙世币
- 每次可计费资产操作消耗 `1`光点
- 图片生成和作品发布都按资产操作计费;余额不足时禁止继续执行。
- 在调用外部图片生成或发布 mutation 前预扣,余额不足时直接返回业务错误,不继续调用后续资产操作。
- 如果图片生成、远程下载、OSS 写入、资产记录确认或发布 mutation 失败,资产操作服务自动发起同额退款。

View File

@@ -106,3 +106,12 @@
2. 响应字段命名与前端约定一致
3. 配置开关可稳定映射到返回数组
4. 文档、任务清单与测试已同步更新
## 8. 2026-05-01 前端降级修复记录
本地联调时若 `api-server` 未启动或 Vite 代理暂时返回 `500``GET /api/auth/login-options` 会失败。前端必须继续遵循第 5.3 节约束:
1. `AuthGate``login-options` 读取失败时设置 `availableLoginMethods = ["password"]`
2. 该失败只代表登录方式配置探测失败,不代表登录功能不可用,因此不把 `读取登录方式失败` 写入登录弹窗错误条。
3. 登录弹窗仍展示密码登录表单,玩家可继续登录后进入创作链路。
4. 本地仍需要启动 `api-server`,否则后续 `POST /api/auth/entry` 等真实登录请求无法完成。

View File

@@ -0,0 +1,63 @@
# 新账号短信登录后置邀请码弹窗设计
日期:`2026-05-01`
## 1. 目标
账号入口不再展示独立注册入口。用户统一从短信登录进入,后端通过 `POST /api/auth/phone/login` 返回的 `created` 字段判断本次是否创建了新账号。
`created=true` 时,前端在登录成功后额外弹出独立邀请码面板:
1. 标题固定为 `请填写邀请码`
2. 标题下方展示邀请码输入框。
3. 输入为空时主按钮显示 `跳过`,点击后关闭面板。
4. 输入非空时主按钮显示 `提交`,点击后提交邀请码。
5. 面板右上角提供取消按钮,点击后关闭面板。
## 2. 入口调整
登录弹窗只保留可用登录方式:
1. 短信登录。
2. 密码登录。
3. 微信登录。
不得再展示 `注册` 页签、注册按钮或注册表单。邀请码不再出现在短信验证码表单中,避免用户把登录和注册理解成两套入口。
## 3. 邀请码提交
后置弹窗提交邀请码时调用已登录接口:
```text
POST /api/profile/referrals/redeem-code
```
请求体:
```json
{
"inviteCode": "SPRING2026"
}
```
后端继续使用 SpacetimeDB 的 `redeem_profile_referral_invite_code` procedure 作为唯一真相源。该 procedure 已负责校验:
1. 每个用户最多只能填写一个邀请码。
2. 邀请码必须存在。
3. 用户不能填写自己的邀请码。
4. 双方奖励与钱包流水在同一事务内落地。
## 4. URL 邀请码
若地址中存在 `inviteCode``invite_code`,前端只将其作为新账号后置弹窗的默认输入值。它不会触发注册页签,也不会在短信登录请求中提前提交。
若用户登录的是已有账号,则不会弹出新账号邀请码面板。
## 5. 完成定义
1. 登录弹窗内不可见注册入口。
2. 短信登录创建新账号后弹出邀请码面板。
3. 邀请码为空时按钮为 `跳过`,非空时按钮为 `提交`
4. 取消按钮可关闭面板。
5. 已登录邀请码接口允许提交,并继续由 SpacetimeDB procedure 兜底业务校验。
6. 前端测试覆盖注册入口删除、新账号弹窗、URL 邀请码预填与提交。

View File

@@ -0,0 +1,105 @@
# 认证快照同步与抓大鹅本地联调修复记录
日期:`2026-05-01`
## 1. 现场问题
本地访问 `http://127.0.0.1:3000` 时出现两类失败:
1. 验证码登录成功后,接口返回 `同步认证快照失败`
2. 抓大鹅创作页请求报 `Failed to initiate WebSocket connection ... HTTP error: 503 Service Unavailable`,或同源创作接口直接 `404`
## 2. 根因
### 2.1 Maincloud 目标库挂起
CLI 直接查询 `xushi-p4wfr` 返回:
```text
Error: database is suspended
HTTP status server error (503 Service Unavailable)
```
这说明 `maincloud.spacetimedb.com` 入口在线,但具体数据库 `xushi-p4wfr` 当前不可订阅、不可查 schema、不可执行 SQL。所有依赖该库的 procedure 都会失败。
### 2.2 认证快照同步被当成硬失败
手机号、密码、刷新、退出等认证流程会先更新本地 `auth_store`,然后调用 SpacetimeDB 同步认证快照。旧逻辑把同步失败直接转为 HTTP 500导致本地会话已经创建成功响应却被远端快照同步失败阻断。
### 2.3 Vite 未代理 `/api/creation`
抓大鹅创作接口挂在:
```text
/api/creation/match3d/*
```
但 Vite 代理只覆盖了 `/api/auth``/api/runtime` 等路径,未覆盖 `/api/creation`,因此浏览器同源请求会被 Vite 返回 `404`,没有进入 Rust `api-server`
## 3. 修复
### 3.1 认证快照同步改为非阻断
`AppState::sync_auth_store_snapshot_to_spacetime` 保持导出本地快照、写入 SpacetimeDB、导入正式表的顺序但当远端写入或导入失败时只写 warn 日志并返回 `Ok(())`
设计边界:
1. 当前认证请求的即时真相源是本地 `auth_store`
2. SpacetimeDB 认证快照用于跨进程恢复和正式表投影。
3. 远端库挂起或网络异常只降级远端恢复能力,不回滚已经成功的登录、刷新、退出和资料更新。
### 3.2 Vite 补齐创作接口代理
`vite.config.ts` 新增:
```ts
'/api/creation': {
target: runtimeServerTarget,
changeOrigin: true,
secure: false,
},
```
前端仍只请求同源 `/api/creation/match3d/*`,不直连 Rust 端口。
## 4. 本地可跑链路
Maincloud `xushi-p4wfr` 挂起期间,抓大鹅本地体验应使用本地 SpacetimeDB
```powershell
spacetime --root-dir=server-rs/.spacetimedb/local start --edition standalone --listen-addr 127.0.0.1:3101
$env:GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET="codex-local-bootstrap-secret-20260501"
spacetime --root-dir=server-rs/.spacetimedb/local publish xushi-p4wfr --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module -c=on-conflict --yes
```
再让 Rust API 指向本地库:
```powershell
$env:GENARRATIVE_SPACETIME_MAINCLOUD_SERVER_URL="http://127.0.0.1:3101"
$env:GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE="xushi-p4wfr"
$env:GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN=""
npm run api-server:maincloud
```
最后重启前端:
```powershell
$env:RUST_SERVER_TARGET="http://127.0.0.1:3100"
$env:GENARRATIVE_RUNTIME_SERVER_TARGET="http://127.0.0.1:3100"
npm run dev:web
```
## 5. 验证结果
已验证:
1. `GET http://127.0.0.1:3000/api/auth/login-options` 返回 `["phone","password"]`
2. `GET http://127.0.0.1:3000/api/runtime/match3d/gallery` 返回 `{"items":[]}`,不再返回 SpacetimeDB 503。
3. 未登录请求 `POST http://127.0.0.1:3000/api/creation/match3d/sessions` 返回 `401`,说明同源请求已进入 Rust 鉴权层,不再被 Vite `404`
4. 隔离端口指向挂起的 Maincloud 并使用 mock 短信时,手机号验证码登录返回 `200` 和 token日志只记录“认证快照写入 SpacetimeDB 失败,当前认证流程继续”。
## 6. 后续
1. Maincloud `xushi-p4wfr` 仍需恢复数据库挂起状态,否则正式云端玩法 procedure 仍不可用。
2. 本地开发如只为体验抓大鹅,可继续使用本地 SpacetimeDB 链路。
3. 认证快照同步失败会影响进程重启后的云端恢复完整性,需要在 Maincloud 恢复后重新完成一次成功同步。

View File

@@ -34,7 +34,7 @@ Stage 1 已把 Rust 鉴权快照同步到 SpacetimeDB 的 `auth_store_snapshot`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `user_id` | `String` | 主键。 |
| `public_user_code` | `String` | 公开叙世号。 |
| `public_user_code` | `String` | 公开百梦号。 |
| `username` | `String` | 当前账号用户名。 |
| `display_name` | `String` | 展示名。 |
| `phone_number_masked` | `Option<String>` | 脱敏手机号。 |

View File

@@ -47,7 +47,7 @@ server-rs/crates/api-server/src/prompt/big_fish.rs
同时把 `prompt/mod.rs` 补齐为正式导出入口,和现有:
1. `puzzle_image.rs`
1. `puzzle/image.rs`
2. `character_visual.rs`
3. `character_animation.rs`
4. `scene_background.rs`

View File

@@ -37,7 +37,7 @@
- 当前场景的核心任务描述。
- 文本会作为游戏中首次进入某个场景生成章节任务的关键上下文。
- 必须结合场景描述、场景入口钩子、出场角色与 3 幕事件,说明玩家首次进入该场景时要完成什么。
- 世界档案的场景详情页必须直接展示该字段,便于创作者确认每个场景的默认章节任务。
- 世界档案的场景详情页必须直接展示该字段,便于百梦主确认每个场景的默认章节任务。
### Landmark 生成源字段

View File

@@ -16,22 +16,23 @@
## 页面路径表
| 页面阶段 | 路径 | 说明 |
| --- | --- | --- |
| `platform` | `/` | 平台首页、广场、我的、创作中心等主入口 |
| `detail` | `/worlds/detail` | RPG 世界详情页,依赖当前已选作品 |
| `agent-workspace` | `/creation/rpg/agent` | RPG Agent 共创工作区 |
| `custom-world-generating` | `/creation/rpg/generating` | RPG 世界草稿生成进度页 |
| `custom-world-result` | `/creation/rpg/result` | RPG 世界结果页与编辑页 |
| `big-fish-agent-workspace` | `/creation/big-fish/agent` | 大鱼吃小鱼 Agent 共创工作区 |
| `big-fish-result` | `/creation/big-fish/result` | 大鱼吃小鱼草稿结果页 |
| `big-fish-runtime` | `/runtime/big-fish` | 正式链路中的大鱼吃小鱼运行页 |
| `puzzle-agent-workspace` | `/creation/puzzle/agent` | 拼图 Agent 共创工作区 |
| `puzzle-result` | `/creation/puzzle/result` | 拼图草稿结果页 |
| `puzzle-gallery-detail` | `/gallery/puzzle/detail` | 拼图作品详情页,依赖当前已选作品 |
| `puzzle-runtime` | `/runtime/puzzle` | 正式链路中的拼图运行页 |
| RPG 选角页 | `/runtime/rpg/characters` | 进入世界后、确认角色前的选角阶段 |
| RPG 冒险页 | `/runtime/rpg/adventure` | 已确认角色后的 RPG 主运行态 |
| 页面阶段 | 路径 | 说明 |
| -------------------------- | --------------------------- | ------------------------------------------------------ |
| `platform` | `/` | 平台首页、广场、我的、创作中心等主入口 |
| `work-detail` | `/works/detail` | 统一公开作品详情页,承接 RPG、拼图、大鱼吃小鱼公开作品 |
| `detail` | `/worlds/detail` | RPG 世界详情页,依赖当前已选作品 |
| `agent-workspace` | `/creation/rpg/agent` | RPG Agent 共创工作区 |
| `custom-world-generating` | `/creation/rpg/generating` | RPG 世界草稿生成进度页 |
| `custom-world-result` | `/creation/rpg/result` | RPG 世界结果页与编辑页 |
| `big-fish-agent-workspace` | `/creation/big-fish/agent` | 大鱼吃小鱼 Agent 共创工作区 |
| `big-fish-result` | `/creation/big-fish/result` | 大鱼吃小鱼草稿结果页 |
| `big-fish-runtime` | `/runtime/big-fish` | 正式链路中的大鱼吃小鱼运行页 |
| `puzzle-agent-workspace` | `/creation/puzzle/agent` | 拼图 Agent 共创工作区 |
| `puzzle-result` | `/creation/puzzle/result` | 拼图草稿结果页 |
| `puzzle-gallery-detail` | `/gallery/puzzle/detail` | 拼图作品详情页,依赖当前已选作品 |
| `puzzle-runtime` | `/runtime/puzzle` | 正式链路中的拼图运行页 |
| RPG 选角页 | `/runtime/rpg/characters` | 进入世界后、确认角色前的选角阶段 |
| RPG 冒险页 | `/runtime/rpg/adventure` | 已确认角色后的 RPG 主运行态 |
## 落地边界

View File

@@ -16,15 +16,19 @@
1. 构建产物目录统一使用 `build/<版本号>/`
2. 默认使用 Jenkins `BUILD_NUMBER` 作为版本号,避免依赖时间戳;如有需要也允许显式传 `BUILD_VERSION`
3. `构建``构建并部署``checkout scm` 后、实际构建前必须执行 `git reset --hard HEAD``git clean -fd`,避免固定源码目录内的 Git 变更和未跟踪文件影响发布包;不使用 `-x`,避免删除 `node_modules/` 等忽略目录后与 `RUN_NPM_CI=false` 冲突。
4. `部署` 流水线允许人工启动;没有上游触发 cause 时按人工部署处理,不再直接失败
5. `部署` 流水线仅在存在上游触发 cause 时校验上游作业名与传入的 `EXPECTED_UPSTREAM_JOB` 一致;如配置了环境变量 `GENARRATIVE_ALLOWED_UPSTREAM_JOB`,还必须与该值一致
6. `构建并部署` 在触发 `部署` 前先释放自己的构建节点,避免单执行器节点出现死锁
7. `部署` 不重新构建,不重新上传,不从 Jenkins 插件仓库复制产物,直接使用上游构建节点的本地 `build/<版本号>/` 目录
8. `部署` 流水线读取触发原因时必须使用 `currentBuild.getBuildCauses(...)` 这类白名单方法,不能直接访问 `currentBuild.rawBuild`,否则会被 Jenkins Script Security 拦截
9. 由于 Jenkins Pipeline 的 `build` 步骤触发下游时,原因类型通常是 `org.jenkinsci.plugins.workflow.support.steps.build.BuildUpstreamCause`,实现上需要同时兼容它和经典的 `hudson.model.Cause$UpstreamCause`,否则会把真实的上游触发误判成人工执行
10. 如果线上进程的启停必须经过 `sudo`,只允许 `start.sh` / `stop.sh` 这两个 hook 使用 `sudo -n` 执行,部署目录清空与文件覆盖仍保持普通权限
11. `WEB_PORT` 必须在 `构建并部署``部署` 两条流水线之间使用同名参数传递;部署脚本会把最终端口写入固定部署目录 `.env.local``GENARRATIVE_WEB_PORT`,避免 `sudo` 启动 hook 时环境变量被清理导致端口回退
3. 所有使用仓库源码的 Jenkins 流水线在实际执行脚本前必须执行 `git reset --hard HEAD`,避免固定源码目录内的 Git 变更影响本次构建、部署或迁移操作;其中 `构建``构建并部署` 在实际构建前还必须执行 `git clean -fd` 清理未跟踪文件,不使用 `-x`,避免删除 `node_modules/` 等忽略目录后与 `RUN_NPM_CI=false` 冲突。
4. `构建并部署` 可选填写 `COMMIT_HASH`。留空时使用 Jenkins SCM 当前检出的提交;填写时只能是 7 到 40 位十六进制 commit hash流水线会先按 SCM checkout 得到仓库,再尽量拉取 `origin` 全部分支引用、解析该 hash 并 detached checkout 到对应 commit 后构建
5. `部署` 流水线允许人工启动;没有上游触发 cause 时按人工部署处理,不再直接失败
6. `部署` 流水线仅在存在上游触发 cause 时校验上游作业名与传入的 `EXPECTED_UPSTREAM_JOB` 一致;如配置了环境变量 `GENARRATIVE_ALLOWED_UPSTREAM_JOB`,还必须与该值一致
7. `构建并部署` 在触发 `部署` 前先释放自己的构建节点,避免单执行器节点出现死锁
8. `部署` 不重新构建,不重新上传,不从 Jenkins 插件仓库复制产物,直接使用上游构建节点的本地 `build/<版本号>/` 目录
9. `部署` 流水线读取触发原因时必须使用 `currentBuild.getBuildCauses(...)` 这类白名单方法,不能直接访问 `currentBuild.rawBuild`,否则会被 Jenkins Script Security 拦截
10. 由于 Jenkins Pipeline 的 `build` 步骤触发下游时,原因类型通常是 `org.jenkinsci.plugins.workflow.support.steps.build.BuildUpstreamCause`,实现上需要同时兼容它和经典的 `hudson.model.Cause$UpstreamCause`,否则会把真实的上游触发误判成人工执行
11. 如果线上进程的启停必须经过 `sudo`,只允许 `start.sh` / `stop.sh` 这两个 hook 使用 `sudo -n` 执行,部署目录清空与文件覆盖仍保持普通权限
12. `WEB_PORT` 必须在 `构建并部署``部署` 两条流水线之间使用同名参数传递;部署脚本会把最终端口写入固定部署目录 `.env.local``GENARRATIVE_WEB_PORT`,避免 `sudo` 启动 hook 时环境变量被清理导致端口回退。
13. `DATABASE` 必须匹配 SpacetimeDB CLI 数据库名规则 `^[a-z0-9]+(-[a-z0-9]+)*$`:只能使用小写字母、数字,并用单个短横线分隔;大写字母、点号、下划线、首尾短横线和连续短横线都会被拒绝,否则 `spacetime publish` 会报 `invalid characters in database name`
14. Jenkins 日志必须能看到构建参数中的 SpacetimeDB 发布数据库,以及 `start.sh` 最终加载环境文件后的运行时数据库、server 和 root-dir避免 `.env.local` 覆盖默认值后无法判断实际发布目标。
15. `构建并部署` 流水线开头通过 `GENARRATIVE_TOOLS_PATH` 固定声明 Jenkins 用户下的 Node、Cargo、SpacetimeDB 常用安装目录:`/var/lib/jenkins/.nvm/versions/node/v22.22.2/bin:/var/lib/jenkins/.cargo/bin:/var/lib/jenkins/.local/bin:/var/lib/jenkins/bin`,并显式保留 `/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`,避免覆盖系统路径导致 `sh` 步骤无法启动。
## 3. 节点与工作区要求
@@ -80,7 +84,8 @@ jenkins/Jenkinsfile.deploy
1. 读取触发原因;人工启动时跳过上游门禁,上游触发时同时兼容 `BuildUpstreamCause` 与经典 `UpstreamCause` 并继续校验上游作业名。
2. 校验 `BUILD_VERSION``SOURCE_WORKSPACE_ROOT``DEPLOY_DIRECTORY` 非空。
3. 执行:
3. `SOURCE_WORKSPACE_ROOT` 内执行 `git reset --hard HEAD`,确保部署脚本和构建产物选择不受本地改动影响。
4. 执行:
```bash
scripts/jenkins-deploy-release.sh \
@@ -88,20 +93,24 @@ scripts/jenkins-deploy-release.sh \
--deploy-dir /var/lib/jenkins/deploy/Genarrative \
--web-port <WEB_PORT> \
[--clear-database] \
[--no-migrate-on-conflict] \
[--migration-dir <MIGRATION_DIRECTORY>] \
--hook-with-sudo
```
脚本语义:
1. 若部署目录已有旧版本且存在 `stop.sh`,先执行旧版本 `stop.sh`
2. 只删除发布产物白名单中的旧文件,例如 `web/``api-server``spacetime_module.wasm``.env*``start.sh``stop.sh``web-server.mjs``README.md`
3. 将指定版本目录中的同名发布产物复制到部署目录;文件产物使用普通复制,`web/` 等目录产物必须递归复制
4. 如果 `CLEAR_DATABASE=true`,部署脚本会以 `./start.sh --clear-database` 启动新版本;这样发布阶段的 `spacetime publish` 会追加 `-c=on-conflict`
5. 执行新版本 `start.sh`
2. 覆盖前如果旧部署目录存在 `migration-bootstrap-secret.txt`,先复制到 `deploy-state/migration-bootstrap-secret.previous.txt`,供新版本 `start.sh` 在 schema 冲突自动迁移时授权导出旧库。该文件属于 Jenkins 部署状态,不放入 `run/`,避免 `sudo` 启停脚本生成的 root 私有运行目录阻断后续部署写入;如果后续部署失败,部署脚本必须把该快照复制回部署目录根下的 `migration-bootstrap-secret.txt`,避免当前仍在运行的数据库丢失对应迁移引导密钥
3. 只删除发布产物白名单中的旧文件,例如 `web/``api-server``spacetime_module.wasm``migration-bootstrap-secret.txt``scripts/``.env*``start.sh``stop.sh``web-server.mjs``README.md`
4. 将指定版本目录中的同名发布产物复制到部署目录;文件产物使用普通复制,`web/``scripts/` 等目录产物必须递归复制
5. `WEB_PORT``MIGRATE_ON_CONFLICT``MIGRATION_DIRECTORY` 写入部署目录 `.env.local`,确保通过 sudo 执行 `start.sh` 时仍能读取 Jenkins 参数;启动前读取 `.env``.env.local` 中最终的 `GENARRATIVE_SPACETIME_DATABASE`,打印并校验其符合 SpacetimeDB 数据库名规则。Jenkins 参数 `MIGRATION_EXPORT_TOKEN` / `MIGRATION_IMPORT_TOKEN` 会分别写入 `GENARRATIVE_SPACETIME_MIGRATION_EXPORT_TOKEN` / `GENARRATIVE_SPACETIME_MIGRATION_IMPORT_TOKEN`;如果参数为空,部署目录已有同名变量时会尽量保留
6. 如果 `CLEAR_DATABASE=true`,部署脚本会以 `./start.sh --clear-database` 启动新版本;这样发布阶段的 `spacetime publish` 会追加 `-c=on-conflict`,代表人工确认清库,不进入自动导出和回灌。
7. 执行新版本 `start.sh`;普通发布遇到 schema 冲突时,默认由发布包内迁移脚本自动导出旧库、清库发布新 wasm、导入回灌。
如果 `RUN_DEPLOY_HOOKS_WITH_SUDO=true`第 1 步和第 4 步会改为 `sudo -n` 调用;这要求 Jenkins 运行用户提前配置免密 sudo否则部署会直接失败不会进入交互式密码提示。
如果 `RUN_DEPLOY_HOOKS_WITH_SUDO=true`旧版本 `stop.sh` 和新版本 `start.sh` 会改为 `sudo -n` 调用;这要求 Jenkins 运行用户提前配置免密 sudo否则部署会直接失败不会进入交互式密码提示。
这样可以满足“发布文件直接覆盖”的要求,同时保留部署目录里像 `.spacetimedb/``logs/``run/` 这类运行态目录,不会因为部署被整体删除。发布白名单内的 `.env``.env.local` 会先以构建产物中的文件为准;部署脚本会在启动 hook 前移除这些环境文件中的 UTF-8 BOM 与 CRLF并把 Jenkins 部署参数 `WEB_PORT` 写入 `.env.local``GENARRATIVE_WEB_PORT`,避免 `start.sh` 在 Bash 下把首行变量名误解析成命令,也避免端口配置只停留在上游构建阶段。`start.sh` 会先执行 Ubuntu 专用 `sync_ubuntu_spacetime_install`,优先从 `/usr/.local/share/spacetime/bin/<version>/spacetimedb-cli``$HOME/.local/share/spacetime/bin/<version>/spacetimedb-cli` 同步到部署目录 `.spacetimedb/bin/current/spacetimedb-cli`,后续启动、探活和 root-dir 占用判定都使用部署目录内 `.spacetimedb/`,且不再额外设置 `--data-dir`,避免 Jenkins 机器全局 `spacetime login` 变化影响本地库更新;如遇 `403 Forbidden`,按 `SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md` 排查数据库所有者与 CLI 身份。
这样可以满足“发布文件直接覆盖”的要求,同时保留部署目录里像 `.spacetimedb/``logs/``run/``deploy-state/``database-migrations/` 这类运行态目录,不会因为部署被整体删除。`run/` 只承载 pid 等启停运行状态;`deploy-state/` 承载 Jenkins 覆盖部署前保存的旧迁移引导密钥,必须由 Jenkins 用户保持可写,并在部署失败时作为恢复源写回根目录 `migration-bootstrap-secret.txt`。发布白名单内的 `.env``.env.local` 会先以构建产物中的文件为准;部署脚本会在启动 hook 前移除这些环境文件中的 UTF-8 BOM 与 CRLF并把 Jenkins 部署参数 `WEB_PORT` 写入 `.env.local``GENARRATIVE_WEB_PORT`,把 `MIGRATE_ON_CONFLICT` 写入 `GENARRATIVE_SPACETIME_MIGRATE_ON_CONFLICT`,把 `MIGRATION_DIRECTORY` 写入 `GENARRATIVE_SPACETIME_MIGRATION_DIR`,并在启动前输出最终 `GENARRATIVE_SPACETIME_DATABASE`,避免 `start.sh` 在 Bash 下把首行变量名误解析成命令,也避免端口、数据库名和迁移配置只停留在上游构建阶段。`start.sh` 会先执行 Ubuntu 专用 `sync_ubuntu_spacetime_install`,优先从 `/usr/.local/share/spacetime/bin/<version>/spacetimedb-cli``$HOME/.local/share/spacetime/bin/<version>/spacetimedb-cli` 同步到部署目录 `.spacetimedb/bin/current/spacetimedb-cli`,后续启动、探活和 root-dir 占用判定都使用部署目录内 `.spacetimedb/`,且不再额外设置 `--data-dir`,避免 Jenkins 机器全局 `spacetime login` 变化影响本地库更新;如遇 `403 Forbidden`,按 `SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md` 排查数据库所有者与 CLI 身份。
### 4.3 构建并部署
@@ -113,18 +122,22 @@ jenkins/Jenkinsfile.build-and-deploy
核心流程:
1. `checkout scm`执行 `git reset --hard HEAD``git clean -fd` 清理工作区
2. 复用与 `构建` 相同的构建命令生成 `build/<BUILD_VERSION>/`
3. 归档 `build/<BUILD_VERSION>/**`
4. 记录当前 `NODE_NAME`、源码根目录、版本号
5. 构建时额外透传 `--web-port <WEB_PORT>`,默认生成监听 `25001` 的发布包
6. 触发 `部署` 流水线,并传递:
1. `checkout scm`,如果 `COMMIT_HASH` 非空,则先拉取远端分支和 tag解析该 hash 指向的 commit并 detached checkout 到该提交
2. 执行 `git reset --hard HEAD``git clean -fd` 清理工作区
3. 复用与 `构建` 相同的构建命令生成 `build/<BUILD_VERSION>/`
4. 归档 `build/<BUILD_VERSION>/**`
5. 记录当前 `NODE_NAME`、源码根目录、版本号与实际构建 commit
6. 构建时额外透传 `--web-port <WEB_PORT>`,默认生成监听 `25001` 的发布包。
7. 构建日志会输出 `SpacetimeDB 发布数据库: <DATABASE>``构建 commit: <COMMIT>`,发布包启动日志会输出最终 `database/server/root-dir`
8. 触发 `部署` 流水线,并传递:
- `BUILD_VERSION`
- `SOURCE_WORKSPACE_ROOT`
- `SOURCE_NODE_NAME`
- `DEPLOY_DIRECTORY`
- `WEB_PORT`
- `CLEAR_DATABASE`
- `MIGRATE_ON_CONFLICT`
- `MIGRATION_DIRECTORY`
- `EXPECTED_UPSTREAM_JOB`
## 5. Jenkins 参数建议
@@ -137,8 +150,13 @@ jenkins/Jenkinsfile.build-and-deploy
4. `RUN_NPM_CI`:是否在构建前执行 `npm ci`
5. `WEB_PORT`:静态网站监听端口;`构建并部署` 默认值为 `25001`,并通过下游 `部署` 同名参数作为最终启动端口。
6. `CLEAR_DATABASE`:部署阶段是否清空 SpacetimeDB 数据后再发布 wasm默认 `false`
7. `MIGRATE_ON_CONFLICT`:普通部署遇到 SpacetimeDB schema 冲突时是否自动导出、清库发布、导入回灌;默认 `true`
8. `MIGRATION_DIRECTORY`:自动迁移 JSON 输出目录;留空时使用部署目录内 `database-migrations/<database>`
9. `MIGRATION_EXPORT_TOKEN`:可选,旧库已授权迁移操作员 token只在 schema 冲突导出旧库时使用。
10. `MIGRATION_IMPORT_TOKEN`:可选,新库已授权迁移操作员 token只在清库发布新 wasm 后导入回灌时使用。
如果当前 Jenkins 没有额外配置独立 Agent而是直接在控制器自身执行任务`AGENT_LABEL` 应填写 `built-in`
如果 `node``cargo``spacetime` 安装在 Jenkins 用户目录下,`构建并部署` 已默认把 `/var/lib/jenkins/.nvm/versions/node/v22.22.2/bin``/var/lib/jenkins/.cargo/bin``/var/lib/jenkins/.local/bin``/var/lib/jenkins/bin` 写入流水线 `PATH` 前缀;仍应确保这些目录和其中二进制文件对 Jenkins 运行用户可读可执行。
如果 Jenkins 进程以默认 `jenkins` 用户运行,部署目录建议直接放在 `/var/lib/jenkins/deploy/Genarrative` 这类 Jenkins 自有目录下,避免再依赖 `/home/ubuntu/*` 的额外写权限。
如果目标 Ubuntu 的 Jenkins `sh` 默认实际落到 `/bin/sh -> dash`,而流水线脚本又使用了 `set -euo pipefail`,则必须显式通过 `bash -lc` 执行命令,不能直接依赖 Jenkins 默认 `sh` 解释器。
@@ -148,9 +166,13 @@ jenkins/Jenkinsfile.build-and-deploy
2. `SOURCE_NODE_NAME`
3. `DEPLOY_DIRECTORY`
4. `CLEAR_DATABASE`
5. `RUN_DEPLOY_HOOKS_WITH_SUDO`
6. `EXPECTED_UPSTREAM_JOB`
7. `WEB_PORT`
5. `MIGRATE_ON_CONFLICT`
6. `MIGRATION_DIRECTORY`
7. `RUN_DEPLOY_HOOKS_WITH_SUDO`
8. `EXPECTED_UPSTREAM_JOB`
9. `WEB_PORT`
10. `MIGRATION_EXPORT_TOKEN`
11. `MIGRATION_IMPORT_TOKEN`
其中仅 `构建并部署` 流水线还需要:
@@ -158,6 +180,12 @@ jenkins/Jenkinsfile.build-and-deploy
2. `RUN_DEPLOY_HOOKS_WITH_SUDO`
3. `WEB_PORT`
4. `CLEAR_DATABASE`
5. `MIGRATE_ON_CONFLICT`
6. `MIGRATION_DIRECTORY`
7. `MIGRATION_EXPORT_TOKEN`
8. `MIGRATION_IMPORT_TOKEN`
9. `DATABASE`:发布包默认数据库名,默认 `genarrative-pipeline-local-test`,必须匹配 `^[a-z0-9]+(-[a-z0-9]+)*$`
10. `COMMIT_HASH`:可选指定构建提交;如果目标 commit 不在 Jenkins 当前浅克隆历史中,流水线会尝试 unshallow仍找不到时构建失败。
如果你选择启用 `RUN_DEPLOY_HOOKS_WITH_SUDO=true`,推荐提前在服务器上增加一份最小 sudoers 配置,例如:

View File

@@ -0,0 +1,118 @@
# Jenkins SpacetimeDB 数据库导入导出流水线方案
日期:`2026-04-29`
## 1. 目标
为 Jenkins 增加两条人工触发的数据库迁移流水线:
1. `Genarrative-Database-Export`:调用仓库内 `scripts/spacetime-export-migration-json.mjs`,通过 SpacetimeDB 迁移导出 procedure 生成迁移 JSON并归档为 Jenkins 产物。
2. `Genarrative-Database-Import`:调用仓库内 `scripts/spacetime-import-migration-json.mjs`,通过 SpacetimeDB 迁移导入 procedure 导入迁移 JSON默认只执行 `dry-run`
本方案只编排已有迁移脚本,不在 Jenkinsfile 中重新实现表结构枚举、JSON 解析或 SQL 拼接逻辑。
## 2. 执行依据
1. SpacetimeDB CLI 调用按仓库技能 `spacetimedb-cli` 执行,数据库调用通过 `spacetime call` 或 HTTP procedure API 完成。
2. SpacetimeDB 读写语义按 `spacetimedb-concepts` 执行:导入导出能力由模块内 procedure/reducer 负责校验和事务处理Jenkins 不直接改表。
3. 迁移脚本复用当前仓库的参数解析与错误处理:
- `scripts/spacetime-export-migration-json.mjs`
- `scripts/spacetime-import-migration-json.mjs`
- `scripts/spacetime-migration-common.mjs`
## 3. Jenkins 作业
### 3.1 数据库导出
脚本路径:
```text
jenkins/Jenkinsfile.database-export
```
推荐作业名:
```text
Genarrative-Database-Export
```
关键参数:
1. `DATABASE`:目标 SpacetimeDB 数据库名;留空时读取仓库环境变量。
2. `SERVER`SpacetimeDB server 别名,默认 `maincloud`
3. `SERVER_URL`:显式服务地址;填写后优先于 `SERVER`
4. `DEPLOY_DIRECTORY`:固定部署目录,默认 `/var/lib/jenkins/deploy/Genarrative`
5. `ROOT_DIR`:可选,透传给 `spacetime --root-dir`;为空时使用 `<DEPLOY_DIRECTORY>/.spacetimedb`
6. `INCLUDE_TABLES`:可选,逗号分隔的表名白名单。
7. `OUTPUT_DIRECTORY`:导出文件目录,默认 `database-exports`
8. `EXPORT_NAME`:导出文件名;留空时使用 `spacetime-migration-<BUILD_NUMBER>.json`
导出成功后Jenkins 归档:
```text
<OUTPUT_DIRECTORY>/<EXPORT_NAME>
```
### 3.2 数据库导入
脚本路径:
```text
jenkins/Jenkinsfile.database-import
```
推荐作业名:
```text
Genarrative-Database-Import
```
关键参数:
1. `INPUT_FILE`:必填,迁移 JSON 文件路径。
2. `DATABASE``SERVER``SERVER_URL``DEPLOY_DIRECTORY``ROOT_DIR`:与导出流水线一致。
3. `INCLUDE_TABLES`:可选,只导入指定表。
4. `CHUNK_SIZE`:迁移 JSON 分片大小,默认 `524288` bytes。导入脚本会在文件超过该大小或直接导入触发 HTTP 413 时自动分片上传。
5. `DRY_RUN`:默认 `true`,只校验不写入。
6. `INCREMENTAL`:默认 `true`,跳过已存在或冲突的行。
7. `REPLACE_EXISTING`:默认 `false`,只覆盖本次迁移文件中涉及的表;不可与 `INCREMENTAL` 同时启用。
8. `BOOTSTRAP_SECRET`:可选,用于授权临时 Web API identity。
9. `TOKEN`可选SpacetimeDB 客户端连接 token留空时脚本会自动创建临时 identity 并在结束后撤销。
10. `NOTE`:迁移授权备注。
## 4. 安全边界
1. 导入流水线默认 `DRY_RUN=true`,需要人工明确关闭才会写入数据。
2. `INCREMENTAL``REPLACE_EXISTING` 互斥Jenkinsfile 会在执行前阻止同时启用。
3. Jenkinsfile 不打印 token生产环境应通过 Jenkins 凭据或目标机器环境变量传入敏感值。
4. 如果不传 `TOKEN`,导入脚本会创建临时 Web API identity并调用迁移授权/撤销 procedure 收敛权限窗口。
5. 导入导出流水线在调用仓库内迁移脚本前都会执行 `git reset --hard HEAD`,确保固定源码目录中的本地改动不会影响本次迁移操作。
6. 如果日志出现 `SpacetimeDB HTTP 413: Failed to buffer the request body: length limit exceeded`,优先把 `CHUNK_SIZE` 调低到 `262144` 或更小后重跑。该参数只降低单次 HTTP body不改变导入表范围。
## 5. 本地部署测试参数
`Genarrative-Build-And-Deploy` 增加以下本地发布包参数,便于在 Jenkins 中测试本地 SpacetimeDB不依赖 Maincloud
1. `DATABASE`:发布包默认数据库名,默认 `genarrative-pipeline-local-test`。SpacetimeDB CLI 当前要求数据库名匹配 `^[a-z0-9]+(-[a-z0-9]+)*$`,只能使用小写字母、数字,并用单个短横线分隔;不要使用大写字母、点号、下划线、首尾短横线或连续短横线。
2. `API_PORT`:发布包内 api-server 端口,默认 `8082`
3. `WEB_PORT`:发布包内静态网站端口,默认 `25001`
4. `SPACETIME_PORT`:发布包内本地 SpacetimeDB 端口,默认 `3101`
5. `DEPLOY_DIRECTORY`:固定部署目录,继续透传给 `Genarrative-Deploy`
数据库导入导出流水线在本地测试时应显式填写:
```text
DATABASE=genarrative-pipeline-local-test
SERVER_URL=http://127.0.0.1:3101
DEPLOY_DIRECTORY=/var/lib/jenkins/deploy/Genarrative
```
这样脚本会自动使用 `/var/lib/jenkins/deploy/Genarrative/.spacetimedb` 作为 `spacetime --root-dir`,避免回退到 Jenkins 用户全局 CLI 登录态,也避免误连 Maincloud。
## 6. 文件清单
```text
jenkins/Jenkinsfile.database-export
jenkins/Jenkinsfile.database-import
docs/technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md
```

View File

@@ -0,0 +1,79 @@
# RPG 剧情与模板创作模型路由调整2026-04-30
## 1. 背景
当前 `server-rs` 的文本模型主链统一通过 `platform-llm` 走 Ark OpenAI 兼容 `/chat/completions`。本轮模型切换有两个不同边界:
1. RPG 运行时剧情推理继续使用 Ark `/chat/completions`,但模型固定为 `doubao-seed-character-251128`
2. 模板创作流程的大模型推理统一使用 Ark `/responses`,模型固定为 `deepseek-v3-2-251201`,并按 Responses API 的 `tools: [{ type: "web_search", max_keyword: 3 }]` 方式启用联网搜索。
因此本次不能只替换 `GENARRATIVE_LLM_MODEL` 默认值。默认值仍可能被通用代理或其他兼容调用使用RPG 剧情与模板创作需要在业务请求上显式覆盖模型和协议,避免两条主链互相污染。
## 2. 落地范围
### 2.1 RPG 剧情推理
以下运行时 RPG 推理请求必须显式使用:
- model: `doubao-seed-character-251128`
- protocol: `/chat/completions`
覆盖入口:
1. `runtime_story/compat/ai.rs`
- 首段剧情与继续剧情。
- NPC 对话剧情文本。
- 预留的动作结果叙事生成。
2. `runtime_chat.rs`
- NPC 单轮聊天回复。
- NPC 单轮后续建议。
3. `runtime_chat_plain.rs`
- 角色私聊回复、建议、摘要。
- NPC 对话、招募对话等纯文本流。
### 2.2 模板创作流程
以下创作链路必须显式使用:
- model: `deepseek-v3-2-251201`
- protocol: `/responses`
- web_search: 开启时映射为 `tools: [{ "type": "web_search", "max_keyword": 3 }]`
覆盖入口:
1. `creation_agent_llm_turn.rs`
- RPG/自定义世界 Agent 单轮 JSON turn。
- 大鱼吃小鱼 Agent 单轮 JSON turn。
- 拼图 Agent 单轮 JSON turn。
- 动态状态判断等非流式 JSON turn。
2. `custom_world_foundation_draft.rs`
- 世界框架、角色、场景、角色详情等分阶段底稿生成。
- JSON 修复阶段。
3. `custom_world_agent_entities.rs`
- 结果页新增角色/地点生成。
4. `custom_world_ai.rs`
- 结果页兜底补齐实体生成。
5. `big_fish_draft_compiler.rs`
- 大鱼吃小鱼草稿结构化编译与 JSON 修复。
图片、视频、OSS、SpacetimeDB reducer 不属于本次模型切换范围。
## 3. 平台层改造
`platform-llm` 保留原 `/chat/completions` 能力,并新增 Responses 协议:
1. `LlmTextProtocol::ChatCompletions`
2. `LlmTextProtocol::Responses`
3. `LlmTextRequest::with_responses_api()`
4. `LlmConfig::responses_url()`
Responses 非流式解析优先读取 `output_text`,再兼容 `output[].content[].text`。Responses 流式解析只把 `response.output_text.delta``delta` 推给上层,避免把 reasoning summary、工具事件或完成事件误拼进玩家可见文本。
## 4. 验收标准
1. RPG 运行时 LLM 请求在代码层显式带 `doubao-seed-character-251128`
2. 创作模板 LLM 请求在代码层显式带 `deepseek-v3-2-251201` 与 Responses 协议。
3. `platform-llm` 单测覆盖 Responses 非流式、Responses SSE、Responses web_search tools 请求体。
4. `cargo test -p platform-llm --manifest-path server-rs/Cargo.toml` 通过。
5. `cargo test -p api-server creation_agent_llm_turn --manifest-path server-rs/Cargo.toml` 通过。
6. 修改后按项目约束使用 `npm run api-server:maincloud` 重新启动后端,并执行相应自动测试。

View File

@@ -406,6 +406,12 @@ Node 侧入口位于:
5. `profile_dashboard_state.total_play_time_ms` 通过同一用户同一世界的 `runtimeStats.playTimeMs - last_observed_play_time_ms` 增量累积,后端使用 `saturating_sub` 防止旧快照回退导致负增量。
6. 作品卡上的公开热度计数如果需要覆盖 RPG 作品,应另立公开作品统计方案;不能把个人 `profile_played_world` 误当成全站作品 `playCount`
## 10.2 2026-05-01 新用户注册赠送修正
新注册用户默认获得 `10` 个光点,注册链路通过 SpacetimeDB procedure 写入 `profile_dashboard_state.wallet_balance``profile_wallet_ledger`。流水来源为 `new_user_registration_reward`,流水 ID 固定为 `new-user-registration:{user_id}`,重复调用不重复发放。
注册赠送、邀请码奖励、充值、兑换码、资产扣费等都属于真实平台钱包流水。用户只要已经存在非 `snapshot_sync` 钱包流水,后续 `runtime_snapshot.game_state.playerCurrency` 不再覆盖 `wallet_balance`,只继续刷新游玩时长和玩过世界,避免首次保存旧运行态货币字段时把注册赠送覆盖成 `0`
## 11. 测试策略
### 11.1 必跑

View File

@@ -0,0 +1,842 @@
# 抓大鹅 Match3D 创作与运行态最小落地技术方案 2026-04-30
## 1. 文档目的
本文件承接 PRD《AI 原生抓大鹅 Match3D 玩法创作工具与玩法系统 PRD》冻结首版 demo 的最小可开发方案。
本轮目标不是先做一个纯前端临时小游戏,而是在当前平台内新增独立 `match3d` 玩法域,跑通下面这条最小主链:
1. 平台创作入口选择“抓大鹅”。
2. Agent 对话确认题材、需要消除次数和难度。
3. 编译 Match3D 草稿。
4. 进入结果页编辑游戏名称、标签和封面图。
5. 发布前试玩,可随时停止并返回修改。
6. 发布作品。
7. 玩家进入单局运行态。
8. 前端即时呈现点击、飞入、入槽、三消、腾格、胜负过渡。
9. 后端权威确认点击、入槽、消除、失败、胜利和成绩。
本文是后续并行开发的工程合同。若实现过程中发现字段、路由、表结构或前后端职责需要变化,必须先更新本文,再进入对应编码分支。
---
## 2. 本轮明确不做
1. 不做多关卡链。
2. 不做排行榜展示。
3. 不做道具逻辑,只预留字段和扩展点。
4. 不做真实 3D 模型和真实 3D 物理遮挡。
5. 不做洗牌、重置、旋转、放大等局内操作。
6. 不做必须试玩通关才能发布。
7. 不做前端本地最终规则真相。
8. 不接入 `server-node` 或 PostgreSQL。
9. 不把 Match3D 挂到 RPG、拼图或大鱼吃小鱼旧命名下。
---
## 3. 分层边界
## 3.1 前端
前端继续使用当前 `React + TypeScript + Vite` 平台壳层,负责所有即时呈现的局内反馈:
1. 创作入口、Agent 工作区、结果页、试玩和运行态 UI。
2. 参考图片上传入口。
3. 运行态圆形空间、2D 物品、倒计时和 `7` 格备选栏展示。
4. 基于后端快照做 2D 命中检测、悬停、按压、选中反馈。
5. 在等待后端确认期间,先行播放飞入、入槽、三消、腾格、胜利和失败过渡。
6. 收到后端确认后,以权威快照校正本地表现。
7. 后端拒绝或版本冲突时,回滚本次即时反馈。
前端禁止:
1. 把本地即时反馈作为最终规则真相。
2. 本地生成可提交成绩。
3. 本地伪造胜利、失败、消除计数或运行态持久化结果。
4. 在 UI 中默认展示长篇玩法规则说明。
## 3.2 api-server
`server-rs/crates/api-server` 负责 Match3D 对外 HTTP facade
1. 鉴权、请求上下文、错误 envelope。
2. 创作 Agent 的 LLM turn 编排。
3. 参考图片上传复用现有资产/OSS 能力。
4. 调用 `spacetime-client` 读写 Match3D 会话、作品和运行态。
5. 对前端返回稳定 HTTP DTO不泄露 SpacetimeDB 内部表结构。
## 3.3 SpacetimeDB
`server-rs/crates/spacetime-module` 负责 Match3D 真相态:
1. 存储 Agent session / message。
2. 存储作品 profile。
3. 存储运行态 run snapshot。
4. 通过 procedure 同步返回会话、作品和运行态快照。
5. 在 reducer/procedure 内保持确定性,不做网络、文件系统或外部模型调用。
## 3.4 纯领域 crate
新增:
```text
server-rs/crates/module-match3d
```
职责:
1. 创作配置校验。
2. 物品类型规划。
3. 初始布局生成输入/输出模型。
4. 2D 遮挡与可点击快照计算。
5. 点击确认。
6. 入槽与三消确认。
7. 胜负确认。
8. 成绩基础数据计算。
`module-match3d` 不直接依赖 Axum、不访问 OSS、不调用 LLM、不读写 SpacetimeDB 表。
---
## 4. 共享契约
## 4.1 TypeScript shared contracts
新增:
```text
packages/shared/src/contracts/match3dAgent.ts
packages/shared/src/contracts/match3dWorks.ts
packages/shared/src/contracts/match3dRuntime.ts
```
### `match3dAgent.ts`
承载:
1. `Match3DAgentSession`
2. `Match3DAgentMessage`
3. `Match3DCreatorConfig`
4. `Match3DCompileDraftRequest`
5. `Match3DCompileDraftResult`
### `match3dWorks.ts`
承载:
1. `Match3DWorkProfile`
2. `Match3DWorkSummary`
3. `Match3DWorkUpdateRequest`
4. `Match3DPublishRequest`
5. `Match3DPublishResult`
### `match3dRuntime.ts`
承载:
1. `Match3DRunSnapshot`
2. `Match3DItemSnapshot`
3. `Match3DTraySlot`
4. `Match3DStartRunRequest`
5. `Match3DClickItemRequest`
6. `Match3DClickItemResult`
7. `Match3DStopRunRequest`
8. `Match3DRestartRunRequest`
## 4.2 Rust shared contracts
新增:
```text
server-rs/crates/shared-contracts/src/match3d_agent.rs
server-rs/crates/shared-contracts/src/match3d_works.rs
server-rs/crates/shared-contracts/src/match3d_runtime.rs
```
并在 `server-rs/crates/shared-contracts/src/lib.rs` 导出。
Rust DTO 只承载 HTTP contract 和跨 crate 稳定模型,不直接暴露 `module-match3d` 内部结构。
## 4.3 命名约束
1. 对外展示:抓大鹅。
2. 工程域:`match3d`
3. TypeScript 类型前缀:`Match3D`
4. Rust 类型前缀:`Match3D`
5. HTTP path`/api/creation/match3d/*``/api/runtime/match3d/*`
6. SpacetimeDB 表与 procedure 前缀:`match3d_`
---
## 5. SpacetimeDB 表
首版保持最小闭环,复杂结构统一使用结构化字段 + `snapshot_json` / `draft_json`,避免过早拆出多张高耦合子表。
新增表属于安全 schema 演进;后续如果改字段,必须遵守 `SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`,不能直接删除、重排或改名已有列。表结构变更后必须同步对齐 `migration.rs`
## 5.1 `match3d_agent_session`
作用:保存 Match3D 创作 Agent 会话、配置草稿和发布指针。
字段:
1. `session_id: String`,主键。
2. `owner_user_id: String`,索引。
3. `seed_text: String`,用户初始输入或自动配置摘要。
4. `current_turn: u32`
5. `progress_percent: u32`
6. `stage: String`,建议值:`Collecting``ReadyToCompile``DraftCompiled``Published`
7. `config_json: String`,序列化 `Match3DCreatorConfig`
8. `draft_json: String`,序列化草稿结果。
9. `last_assistant_reply: String`
10. `published_profile_id: String`,未发布为空字符串。
11. `created_at: Timestamp`
12. `updated_at: Timestamp`
## 5.2 `match3d_agent_message`
作用:保存 Match3D 创作 Agent 消息流水。
字段:
1. `message_id: String`,主键。
2. `session_id: String`,索引。
3. `role: String`,建议值:`user``assistant``system`
4. `kind: String`,建议值:`text``action``error`
5. `text: String`
6. `created_at: Timestamp`
## 5.3 `match3d_work_profile`
作用:保存 Match3D 作品主表和发布状态。
字段:
1. `profile_id: String`,主键。
2. `owner_user_id: String`,索引。
3. `source_session_id: String`
4. `author_display_name: String`
5. `game_name: String`
6. `theme_text: String`
7. `summary_text: String`
8. `tags_json: String`
9. `cover_image_src: String`
10. `cover_asset_id: String`
11. `clear_count: u32`
12. `difficulty: u32`
13. `config_json: String`
14. `publication_status: String`,建议值:`Draft``Published`
15. `play_count: u32`
16. `updated_at: Timestamp`
17. `published_at: Option<Timestamp>`,未发布为 `None`
## 5.4 `match3d_runtime_run`
作用:保存 Match3D 单局运行态快照和成绩基础数据。
字段:
1. `run_id: String`,主键。
2. `owner_user_id: String`,索引。
3. `profile_id: String`,索引。
4. `status: String`,建议值:`Running``Won``Failed``Stopped`
5. `snapshot_version: u32`
6. `started_at_ms: i64`
7. `duration_limit_ms: i64`,首版固定 `600000`
8. `finished_at_ms: i64`,未结束为 `0`
9. `elapsed_ms: i64`
10. `clear_count: u32`
11. `total_item_count: u32`
12. `cleared_item_count: u32`
13. `failure_reason: String`,建议值为空、`TimeUp``TrayFull`
14. `snapshot_json: String`,序列化 `Match3DRunSnapshot`
15. `created_at: Timestamp`
16. `updated_at: Timestamp`
## 5.5 `match3d_play_record`
首版可选,若本轮不做排行榜,可先不建表,只在 `match3d_runtime_run` 保留成绩字段。
若实现成绩历史,字段建议:
1. `record_id: String`,主键。
2. `profile_id: String`,索引。
3. `owner_user_id: String`,索引。
4. `run_id: String`
5. `status: String`
6. `elapsed_ms: i64`
7. `cleared_item_count: u32`
8. `total_item_count: u32`
9. `created_at: i64`
---
## 6. SpacetimeDB procedure
本轮全部使用 procedure 同步返回快照,避免 `api-server` 在写入后再读 private table。
## 6.1 创作链
1. `create_match3d_agent_session(input)`
创建会话,写入初始配置或空配置,返回 session snapshot。
2. `get_match3d_agent_session(input)`
获取会话、消息和当前 draft。
3. `submit_match3d_agent_message(input)`
只写 user message不调用 LLM不生成 assistant 回复。
4. `finalize_match3d_agent_message_turn(input)`
`api-server` LLM turn 完成后写入 assistant message、配置状态、进度和 `last_assistant_reply`
5. `compile_match3d_draft(input)`
校验题材、需要消除次数、难度,生成草稿和作品 draft profile。
## 6.2 作品链
1. `update_match3d_work(input)`
更新游戏名称、标签、封面、题材、需要消除次数和难度。
2. `publish_match3d_work(input)`
校验基础信息完整后发布作品,不要求试玩通关。
3. `list_match3d_works(input)`
查询当前用户作品。
4. `get_match3d_work_detail(input)`
查询作品详情,支持结果页恢复和作品详情页。
5. `delete_match3d_work(input)`
可后置;若接入创作中心删除,需要与其他玩法卡片删除语义一致。
## 6.3 运行态链
1. `start_match3d_run(input)`
基于作品配置生成单局快照,返回 `Match3DRunSnapshot`
2. `get_match3d_run(input)`
返回当前权威运行态快照。
3. `click_match3d_item(input)`
根据 `run_id / item_instance_id / client_snapshot_version` 权威确认点击、入槽、三消、失败或胜利,返回新快照和确认结果。
4. `stop_match3d_run(input)`
把运行态标记为 `Stopped`,供试玩中止和返回结果页使用。
5. `restart_match3d_run(input)`
复用同一作品配置创建新 run返回新快照。
6. `finish_match3d_time_up(input)`
可选。若倒计时由前端触发,前端在倒计时归零时调用该 procedure后端确认 `TimeUp`。也可以由 `click_match3d_item``get_match3d_run` 懒确认超时。
## 6.4 procedure 输入输出约束
1. 所有 mutation 输入必须带 `owner_user_id` 或由 `api-server` 注入用户上下文SpacetimeDB 内部仍需以可信身份或 owner 字段校验归属。
2. 运行态 mutation 必须携带 `client_snapshot_version`
3. 若版本不匹配,返回 `VersionConflict`,并携带最新快照。
4. procedure 返回字符串化 JSON 时,`spacetime-client` 必须负责反序列化和错误归一化。
---
## 7. 运行态确认协议
Match3D 首版采用“前端即时反馈 + 后端权威确认”。
## 7.1 点击流程
```text
玩家点击物品
-> 前端基于最新快照做 2D 命中检测
-> 前端立即播放按压/选中/飞入表现
-> 前端调用 click_match3d_item
-> 后端确认点击是否合法
-> 后端返回新快照与确认结果
-> 前端按确认结果固化或回滚表现
```
## 7.2 点击请求
```ts
interface Match3DClickItemRequest {
runId: string;
itemInstanceId: string;
clientSnapshotVersion: number;
clientEventId: string;
clickedAtMs: number;
}
```
字段说明:
1. `clientSnapshotVersion` 用于发现前端基于旧快照操作。
2. `clientEventId` 用于前端去重和日志定位。
3. `clickedAtMs` 只用于观测,不作为成绩可信时间源。
## 7.3 点击结果
```ts
type Match3DClickConfirmStatus =
| 'Accepted'
| 'RejectedNotClickable'
| 'RejectedAlreadyMoved'
| 'RejectedTrayFull'
| 'VersionConflict'
| 'RunFinished';
interface Match3DClickItemResult {
status: Match3DClickConfirmStatus;
run: Match3DRunSnapshot;
acceptedItemInstanceId?: string;
clearedItemInstanceIds: string[];
failureReason?: 'TimeUp' | 'TrayFull';
}
```
## 7.4 前端回滚规则
1. `Accepted`:固化飞入、入槽、消除或胜负表现。
2. `RejectedNotClickable`:被点物品回到原位,备选栏恢复。
3. `RejectedAlreadyMoved`:直接应用后端最新快照。
4. `RejectedTrayFull`:应用后端失败快照。
5. `VersionConflict`:取消当前局部动画,应用最新快照,允许用户继续操作。
6. `RunFinished`:应用后端胜负快照,进入结算。
## 7.5 快照版本
1. 每次后端接受会改变运行态的操作,`snapshot_version` 必须递增。
2. 前端所有即时反馈都基于某个明确版本。
3. 前端同时只能有一个未确认的点击操作;首版不做多点击并发队列。
4. 如果动画期间用户再次点击,前端应忽略或排队到当前确认完成后再处理;首版建议忽略。
---
## 8. 运行态快照
## 8.1 `Match3DRunSnapshot`
```ts
interface Match3DRunSnapshot {
runId: string;
profileId: string;
status: 'Running' | 'Won' | 'Failed' | 'Stopped';
snapshotVersion: number;
startedAtMs: number;
durationLimitMs: number;
serverNowMs: number;
remainingMs: number;
clearCount: number;
totalItemCount: number;
clearedItemCount: number;
traySlots: Match3DTraySlot[];
items: Match3DItemSnapshot[];
failureReason?: 'TimeUp' | 'TrayFull';
}
```
说明:
1. `serverNowMs` 用于前端校准倒计时。
2. `remainingMs` 由后端按 `durationLimitMs` 和服务端时间计算。
3. 前端可以本地递减倒计时,但归零失败必须调用后端确认或等待下一次后端确认。
## 8.2 `Match3DItemSnapshot`
```ts
interface Match3DItemSnapshot {
itemInstanceId: string;
itemTypeId: string;
visualKey: string;
x: number;
y: number;
radius: number;
layer: number;
state: 'InBoard' | 'Flying' | 'InTray' | 'Cleared';
clickable: boolean;
}
```
说明:
1. `Flying` 可以作为前端表现态使用,不要求后端逐帧落库。
2. 后端主要确认 `InBoard -> InTray -> Cleared` 的权威状态变化。
3. `clickable` 是后端计算给前端的可点击快照,前端命中检测必须尊重它。
4. `x / y / radius` 统一使用 `0~1` 归一化舞台坐标。圆心为 `(0.5, 0.5)`,圆形可用半径为 `0.5`
5. 后端生成物品时必须保证 `distance((x, y), (0.5, 0.5)) + radius <= 0.5 - safeMargin`。首版 `safeMargin` 用于覆盖圆形边框和阴影,避免物品被边界压住或裁切。
6. 前端渲染收到旧快照或异常坐标时,可以只做显示层兜底收束,但不得把兜底后的表现坐标写回为规则真相。
## 8.3 `Match3DTraySlot`
```ts
interface Match3DTraySlot {
slotIndex: number;
itemInstanceId?: string;
itemTypeId?: string;
visualKey?: string;
}
```
## 8.4 2D 遮挡口径
首版不做真实物理遮挡。
建议后端按以下输入计算 `clickable`
1. 物品圆形或近似圆形碰撞范围。
2. `layer` 越大越靠上。
3. 被更高层物品覆盖到低于可点击阈值时,标记为不可点击。
4. 阈值首版作为领域常量,后续体验后再参数化。
前端基于 `clickable` 和自身命中检测呈现即时反馈;后端仍在点击确认时再次校验。
---
## 9. 领域规则冻结
## 9.1 创作配置
```ts
interface Match3DCreatorConfig {
themeText: string;
referenceImageSrc?: string;
clearCount: number;
difficulty: number;
}
```
规则:
1. `themeText` 必填。
2. `clearCount` 必须为正整数。
3. `difficulty` 范围 `1~10`
4. `referenceImageSrc` 首版只支持图片,不支持视频。
## 9.2 物品数量
```text
totalItemCount = clearCount * 3
```
每种 `itemTypeId` 的数量必须是 `3` 的倍数。
## 9.3 demo 视觉素材
首版使用内置视觉键和前端内置几何图形资产,不接真实图片生成。
1. 水果题材必须使用 `watermelon-green / apple-red / banana-yellow / grape-purple / melon-green / berry-blue / peach-pink / plum-indigo / lime-lime / orange-orange` 这组内置水果视觉键;前端首版将其映射为纯色几何体,不渲染水果写实图,也不能显示为带文字或透明气泡的小球。
2. 非水果题材暂使用 `red_circle / yellow_triangle / purple_diamond / green_square / blue_star / orange_hexagon / cyan_capsule / pink_heart / lime_leaf / white_moon` 这组兜底颜色形状视觉键。
3. `visualKey` 不允许在前端统一兜底为同一个素材;未知 key 至少要有稳定的颜色差异,避免多个不同 `itemTypeId` 被玩家误认为同一种物品。
4. 运行态图案必须使用实心、高饱和、无文字的几何 SVG至少覆盖圆形、三角形、菱形、方形、五角星、六边形、胶囊、心形、梯形、平行四边形等多种轮廓外层命中按钮不得再显示半透明气泡底。
5. 水果题材的相对尺寸由后端权威半径决定,首版要求西瓜明显大于苹果,苹果、橙子、桃子等中型水果大于葡萄、李子、青柠等小型水果;前端不得自行改写规则半径,只负责按快照表现。
6. 后续接入真实题材图片素材前,必须另补资产生成方案。
## 9.4 难度
首版 `difficulty` 只作为布局和生成算法参数。
后端需要保留参数入口,但难度公式先保持简洁:
1. 难度越高,物品尺寸可整体略小。
2. 难度越高,堆叠层级可略深。
3. 难度越高,首屏可直接三消的可见组合可略少。
4. 同一局内允许有轻微尺寸差异,但每个物品仍必须完整落在圆形空间内。
具体数值不在 A0 冻死,由 B1 领域 crate 分支给出首版常量并通过测试覆盖。
---
## 10. api-server HTTP facade
## 10.1 创作链
```text
POST /api/creation/match3d/sessions
GET /api/creation/match3d/sessions/:sessionId
POST /api/creation/match3d/sessions/:sessionId/messages
POST /api/creation/match3d/sessions/:sessionId/messages/stream
POST /api/creation/match3d/sessions/:sessionId/compile
```
说明:
1. 同步消息接口用于普通提交。
2. 流式接口复用现有 Agent SSE 基建。
3. `messages` 只写 user messageLLM 推理由 `api-server` 完成后 finalize 到 SpacetimeDB。
4. `compile` 不生成额外素材,只生成 Match3D 草稿和作品 draft。
## 10.2 作品链
```text
PATCH /api/creation/match3d/works/:profileId
POST /api/creation/match3d/works/:profileId/publish
GET /api/creation/match3d/works
GET /api/creation/match3d/works/:profileId
```
首版发布不要求试玩通关。
## 10.3 运行态链
```text
POST /api/runtime/match3d/works/:profileId/runs
GET /api/runtime/match3d/runs/:runId
POST /api/runtime/match3d/runs/:runId/click
POST /api/runtime/match3d/runs/:runId/stop
POST /api/runtime/match3d/runs/:runId/restart
POST /api/runtime/match3d/runs/:runId/time-up
```
`time-up` 可后置;若不单独实现,`get` 或下一次 `click` 必须能懒确认超时失败。
## 10.4 错误语义
HTTP 层使用现有 API envelope。
建议错误码:
1. `MATCH3D_SESSION_NOT_FOUND`
2. `MATCH3D_WORK_NOT_FOUND`
3. `MATCH3D_RUN_NOT_FOUND`
4. `MATCH3D_INVALID_CONFIG`
5. `MATCH3D_PUBLISH_BLOCKED`
6. `MATCH3D_RUN_VERSION_CONFLICT`
7. `MATCH3D_RUN_ALREADY_FINISHED`
---
## 11. 前端落点
## 11.1 contracts 与 service
新增:
```text
src/services/match3d-creation/
src/services/match3d-works/
src/services/match3d-runtime/
```
分别负责 Agent/草稿、作品/发布、运行态请求。
## 11.2 组件
新增:
```text
src/components/match3d-creation/
src/components/match3d-result/
src/components/match3d-runtime/
```
## 11.3 平台入口
需要接入:
1. `src/components/platform-entry/platformEntryCreationTypes.ts`
2. `src/components/platform-entry/PlatformEntryCreationTypeModal.tsx`
3. `src/components/platform-entry/usePlatformCreationAgentFlowController.ts`
入口展示:
1. 名称:`抓大鹅`
2. 子标题:`经典消除玩法`
## 11.4 运行态 UI
首版运行态必须移动端优先:
1. 圆形空间占据主要区域。
2. 备选栏固定 `7` 格。
3. 倒计时清晰但不遮挡物品。
4. 物品点击区域稳定,不因动画造成布局跳动。
5. 胜利/失败结算使用独立面板,不在当前面板下方展开。
## 11.5 本地 mock 口径
F3 运行态即时反馈分支可以先用本地 mock snapshot 开发,但必须满足:
1. mock 类型来自 `packages/shared/src/contracts/match3dRuntime.ts`
2. mock 字段不得脱离 A0 文档。
3. 接入真实 API 时删除或降级为测试 fixture。
---
## 12. 并行开发包
## 12.1 第二波并行
### B1 + B2领域 crate 与 shared contracts
写入范围:
1. `server-rs/crates/module-match3d/`
2. `server-rs/Cargo.toml`
3. `server-rs/crates/shared-contracts/src/match3d_*.rs`
4. `packages/shared/src/contracts/match3d*.ts`
交付:
1. 领域规则单测。
2. DTO 编译通过。
3. 不接 SpacetimeDB。
### B3SpacetimeDB 表与 procedure
写入范围:
1. `server-rs/crates/spacetime-module/src/match3d/`
2. `server-rs/crates/spacetime-module/src/lib.rs`
3. `server-rs/crates/spacetime-module/src/migration.rs`
4. 生成后的 bindings 由后续 B4 处理。
交付:
1. 表和 procedure 定义。
2.`module-match3d` 规则接线。
3. `spacetime build` 或仓库现有等价脚本通过。
B3 当前落地状态:
1. `server-rs/crates/spacetime-module/src/match3d/` 已承载 Match3D 的表、procedure 输入输出类型和 procedure 实现,并由 `server-rs/crates/spacetime-module/src/lib.rs` 挂载导出。
2. `migration.rs` 已纳入 `match3d_agent_session``match3d_agent_message``match3d_work_profile``match3d_runtime_run` 四张表,后续字段变更继续按 `SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md` 追加兼容字段。
3. 运行态 `start_match3d_run``click_match3d_item``stop_match3d_run``finish_match3d_time_up` 通过适配层调用 `module-match3d` 的领域规则SpacetimeDB 层只负责归属校验、事务写入、权威快照持久化和 procedure JSON 返回。
4. B3 对外仍返回当前首版快照字段 `snapshotVersion / clientSnapshotVersion` 对应语义;`module-match3d` 内部的 `board_version` 只在适配层中转换,避免影响并行中的 B4/F3 接入。
5. SpacetimeDB module 的有效验收命令是 `spacetime build --module-path crates/spacetime-module`;不要用普通 native `cargo test -p spacetime-module` 作为验收口径,因为该 crate 会链接 SpacetimeDB 宿主符号。
### F1创作入口与 Agent UI
写入范围:
1. `src/components/platform-entry/`
2. `src/components/match3d-creation/`
3. `src/services/match3d-creation/`
交付:
1. 平台入口可见。
2. Agent 工作区能收集题材、需要消除次数和难度。
3. 可用 mock client等待 B5 接口。
### F3运行态即时反馈 UI
写入范围:
1. `src/components/match3d-runtime/`
2. `src/services/match3d-runtime/`
交付:
1. 圆形空间、2D 物品、`7` 格备选栏。
2. 点击命中、飞入、入槽、三消、腾格、胜负过渡。
3. 后端确认失败时的回滚和快照校正逻辑。
4. 先用 mock snapshot。
## 12.2 第三波并行
### B4 + B5spacetime-client 与 api-server facade
写入范围:
1. `server-rs/crates/spacetime-client/src/match3d.rs`
2. `server-rs/crates/spacetime-client/src/lib.rs`
3. `server-rs/crates/api-server/src/match3d.rs`
4. `server-rs/crates/api-server/src/app.rs`
5. `server-rs/crates/api-server/src/main.rs` 如需注册模块
交付:
1. HTTP facade 可调用 SpacetimeDB procedure。
2. 创作、作品、运行态接口返回 shared-contract DTO。
3. 后端定向测试通过。
### F2结果页与发布
写入范围:
1. `src/components/match3d-result/`
2. `src/services/match3d-works/`
3. 创作中心作品恢复相关最小接线。
交付:
1. 编辑游戏名称、标签、封面图。
2. 试玩入口。
3. 发布入口。
### F4平台分发最小接入
写入范围:
1. 创作中心作品货架。
2. 首页/分类/广场卡片映射。
3. 作品详情启动运行态入口。
交付:
1. 已发布 Match3D 作品可进入平台列表。
2. 卡片可进入详情或运行态。
## 12.3 最后集成
### Q1集成验收
交付:
1. 创作到发布到试玩主链通过。
2. 运行态点击、入槽、三消、失败、胜利通过。
3. 移动端视口检查通过。
4. `npm run api-server:maincloud` 通过。
5. 对应测试与 `npm run check:encoding` 通过。
---
## 13. 合并顺序
建议合并顺序:
1. A0本文档。
2. B1 + B2领域 crate 与 shared contracts。
3. B3SpacetimeDB 表和 procedure。
4. B4 + B5spacetime-client 与 api-server facade。
5. F1 / F2 / F3前端创作、结果页、运行态。
6. F4平台分发。
7. Q1集成收口。
如果 F1/F3 先完成,应只以 mock client 保持可编译,不直接修改后端合同。
---
## 14. 验收命令
后续编码分支按改动范围执行。
文档分支:
```powershell
npm run check:encoding -- docs/technical/MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md docs/technical/README.md
```
后端分支:
```powershell
cargo test -p module-match3d
cargo test -p shared-contracts
npm run api-server:maincloud
npm run check:encoding
```
SpacetimeDB 分支按仓库现有发布脚本执行,并在需要生成绑定时使用 `spacetime generate` 或仓库封装脚本。不得手写生成文件。
前端分支:
```powershell
npm run check:encoding
npm run typecheck
```
若新增定向测试,应补跑对应 `vitest`
---
## 15. 一句话结论
Match3D 首版按独立玩法域落地:前端负责所有局内即时反馈以保证手感,后端通过 SpacetimeDB procedure 权威确认规则和成绩api-server 只暴露稳定 HTTP facade后续并行分支必须围绕本文冻结的 DTO、表、procedure 和路由推进。

View File

@@ -0,0 +1,34 @@
# 抓大鹅创作入口开放与错误隔离 2026-05-01
## 1. 背景
抓大鹅 Match3D 玩法域已完成当前 demo 主链接入,本轮恢复创作页入口,使玩家可以从创作中心直接进入抓大鹅共创工作台。同时,平台首页会并行读取 RPG、拼图、抓大鹅等公开广场数据公开广场接口未就绪、空表或临时失败不应污染创作入口错误态也不应表现成登录异常。
## 2. 落地边界
本轮只调整平台创作入口展示、点击分流与公开广场错误隔离:
1. `PLATFORM_CREATION_TYPES``match3d` 保持展示,标题仍为 `抓大鹅`
2. `match3d` 的副标题显示 `经典消除玩法`badge 显示 `可创建`
3. `match3d.locked` 设为 `false`,创作页首屏卡片和创作类型弹层均可点击。
4. 首屏卡片的 `handleCreationHubCreateType('match3d')` 必须走登录保护后调用 `openMatch3DAgentWorkspace()`
5. 创作类型弹层的 `onSelectMatch3D` 必须走同一条登录保护与工作台打开链路。
6. 公开抓大鹅广场读取失败只清空抓大鹅公开列表,不写入 `match3dError`,避免把公开数据失败展示为创作工作台错误。
7. RPG 公开作品广场读取失败只降级为空列表,不提升为整个平台错误;私有作品库、创作作品列表等受保护请求失败仍保留错误提示。
## 3. 非目标
1. 不删除 `src/components/match3d-creation/``src/services/match3d-creation/` 或已完成的 Match3D 玩法域代码。
2. 不修改 SpacetimeDB 表、procedure、bindings 或 `migration.rs`
3. 不改变已发布抓大鹅作品的详情、运行态和后续恢复入口能力。
4. 不在本轮补做公开广场接口的后端业务兜底;前端只对公开读取失败做非阻塞降级。
## 4. 验收点
1. 创作页能看到 `抓大鹅` 卡片。
2. 该卡片显示 `经典消除玩法`,且按钮可点击。
3. 登录态点击创作页首屏 `抓大鹅` 卡片后进入抓大鹅共创工作区。
4. 未登录点击 `抓大鹅` 入口时弹出登录面板,不静默吞掉点击。
5. 抓大鹅公开广场读取失败时,创作页不显示 `读取抓大鹅广场失败`,抓大鹅入口仍可进入。
6. RPG 公开作品广场读取失败时,首页不显示阻塞性的 `读取作品广场失败`,创作页仍可正常打开。
7. 相关测试、类型检查和编码检查通过。

View File

@@ -0,0 +1,113 @@
# 抓大鹅 Match3D 领域规则与共享契约 Stage1 方案
日期:`2026-04-30`
## 1. 文档目的
本文件承接 [MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md](./MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md),只冻结 B1 + B2 开发范围:
1. 新增 `module-match3d` 纯领域 crate。
2. 新增 Rust shared contracts。
3. 新增 TypeScript shared contracts。
本阶段不实现 SpacetimeDB 表、procedure、`spacetime-client` 调用封装、`api-server` facade 和前端页面。
## 2. Stage1 边界
## 2.1 本阶段做
1. 领域层定义创作配置、作品草稿、作品 profile、运行态快照、物品、托盘、点击确认结果。
2. 领域层提供纯函数:
- 校验创作配置
- 编译默认草稿
- 校验发布字段
- 按确定性 seed 生成初始运行态
- 刷新 2D 可点击快照
- 确认点击、入槽、三消、胜利、托盘满失败
- 确认倒计时失败
3. Rust / TypeScript shared contracts 提供前后端对齐的请求与响应 DTO。
4. 运行态采用“前端即时反馈 + 后端权威确认”契约:
- 前端可先播放点击、飞入、入槽、三消、腾格和胜负过渡。
- 后端确认后返回权威快照。
- 后端拒绝或快照版本不一致时,前端按权威快照回滚或校正。
## 2.2 本阶段不做
1. 不新增 SpacetimeDB 表。
2. 不新增 SpacetimeDB procedure。
3. 不生成新的 SpacetimeDB bindings。
4. 不新增 `api-server` 路由。
5. 不接入平台入口、结果页或运行态 UI。
6. 不接入真实图片生成。
7. 不做排行榜与后续关卡推荐。
## 3. 领域 crate 设计
新增:
```text
server-rs/crates/module-match3d
```
该 crate 是纯领域层,不读写数据库,不访问网络,不依赖浏览器或文件系统。
本阶段虽然不落 SpacetimeDB 表和 procedure但领域模型已经为后续 SpacetimeDB 接入预留 `spacetime-types` feature。后续在 `spacetime-module` 内使用这些类型时,仍必须遵守 reducer 确定性、`ctx.sender()` 鉴权和表结构迁移约束。
核心类型:
1. `Match3DCreatorConfig`
2. `Match3DResultDraft`
3. `Match3DWorkProfile`
4. `Match3DRunSnapshot`
5. `Match3DItemSnapshot`
6. `Match3DTraySlot`
7. `Match3DClickConfirmation`
核心函数:
1. `build_creator_config`
2. `compile_result_draft`
3. `validate_publish_requirements`
4. `create_work_profile`
5. `publish_work_profile`
6. `start_run_with_seed_at`
7. `confirm_click_at`
8. `resolve_run_timer_at`
## 4. 即时反馈与权威确认
本阶段将点击处理明确拆成两层:
1. 前端即时反馈层
- 读取后端快照中的 `boardVersion`、物品位置、层级、半径和 `clickable`
- 本地做命中检测和动画。
- 立即表现飞入、入槽、三消和胜负过渡。
2. 后端权威确认层
- 校验 `runId``itemInstanceId`、运行态状态和物品是否仍可点击。
- 重新计算入槽、三消、托盘满失败和胜利。
- 返回最新 `Match3DRunSnapshot`
-`boardVersion` 帮前端识别是否需要校正。
`Flying` 只作为前端表现态,不要求后端逐帧落库。后端只确认物品是否已从 `InBoard` 进入 `InTray``Cleared`
运行态领域内部使用 `board_version` 表示权威快照版本HTTP 与 TypeScript shared contracts 对外使用 `snapshotVersion` / `clientSnapshotVersion`,由后续 `api-server` facade 做字段映射。
## 5. 生成规则 Stage1 口径
1. `clearCount` 必须是正整数。
2. `totalItemCount = clearCount * 3`
3. 难度范围为 `1~10`
4. 首版内置水果题材视觉 key 和颜色形状兜底视觉 key。
5. 当题材包含水果语义时,使用水果视觉 key其他题材使用颜色形状兜底 key。
6.`clearCount > 10` 时,复用视觉 key并保证每种物品数量仍为 `3` 的倍数。
7. 初始布局使用确定性 seed 生成圆形空间内的 2D 坐标。
8. 坐标使用 `0~1` 归一化舞台坐标,圆心为 `(0.5, 0.5)`;生成时必须保证 `distance((x, y), (0.5, 0.5)) + radius <= 0.5 - safeMargin`,避免物品被圆形边界压住或裁切。
9. 可点击判定只做 2D 近似:若物品被更高层物品完全覆盖,则不可点击;否则可点击。
## 6. 验收
1. `cargo test -p module-match3d` 通过。
2. `cargo test -p shared-contracts match3d` 通过。
3. `npm run check:encoding` 覆盖新增中文文档和新增源码。
4. 本阶段不要求运行 `npm run api-server:maincloud`因为未修改后端运行服务入口、SpacetimeDB 表或 `api-server` facade。

View File

@@ -0,0 +1,91 @@
# 抓大鹅 Match3D F1 创作入口与 Agent UI 落地记录 2026-04-30
## 1. 阶段边界
本文件承接《MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md》的 F1 包。
F1 只处理前端创作入口、Agent 工作区和等待后端 B5 facade 前的 mock client。它不实现运行态规则不修改 SpacetimeDB 表,不接 `api-server` 路由。
## 2. 本阶段写入范围
1. `src/components/platform-entry/`
2. `src/components/match3d-creation/`
3. `src/services/match3d-creation/`
4. `packages/shared/src/contracts/match3dAgent.ts`
其中 `packages/shared/src/contracts/match3dAgent.ts` 作为 F1 与后续 B5 的 DTO 对齐点F1 mock client 不自建脱离共享契约的临时类型。
## 3. 入口接入
平台入口新增可见创作类型:
```text
id: match3d
title: 抓大鹅
subtitle: 经典消除玩法
badge: 可创建
```
入口来源统一走 `getVisiblePlatformCreationTypes()`,因此创作首页首屏卡带与“选择创作类型”弹层会同时出现抓大鹅。
## 4. Agent 工作区
新增 `Match3DAgentWorkspace`,复用通用 `CreationAgentWorkspace`
Agent 只收集三类锚点:
1. 题材主题。
2. 需要消除次数。
3. 难度。
工作区支持参考图片上传入口。图片在 F1 中先以 Data URL 形式随消息 payload 带给 mock clientB5 接入后由后端 facade 替换为正式资产上传与引用。
UI 中不默认展示玩法规则长文,只展示进度、锚点、聊天内容和必要按钮。
## 5. mock client
新增 `src/services/match3d-creation/match3dCreationClient.ts`
mock client 提供:
1. `createMatch3DCreationSession`
2. `getMatch3DCreationSession`
3. `streamMatch3DCreationMessage`
4. `executeMatch3DCreationAction`
mock 行为:
1. 创建本地会话。
2. 从中文输入中提取题材、消除次数和难度。
3. 支持“自动配置”。
4. 当三项配置完整时允许执行 `match3d_compile_draft`
5. 编译后返回 `draft_ready` 会话和草稿。
## 6. 结果承接
F1 新增 `Match3DDraftReadyView` 作为草稿生成后的临时承接页,只展示草稿基础信息并允许返回 Agent 修改。
正式结果页的基础信息编辑、封面图、试玩、发布由 F2 接入F1 不在这里模拟发布。
## 7. 后续替换点
B5 完成后,只需要把 `match3dCreationClient` 的本地 Map mock 替换为 HTTP/SSE facade
```text
POST /api/creation/match3d/sessions
GET /api/creation/match3d/sessions/:sessionId
POST /api/creation/match3d/sessions/:sessionId/messages/stream
POST /api/creation/match3d/sessions/:sessionId/compile
```
`PlatformEntryFlowShellImpl``Match3DAgentWorkspace` 不应再改一轮业务字段。
## 8. 验收口径
1. 创作首页能看到“抓大鹅 / 经典消除玩法”。
2. 弹层选择“抓大鹅”能进入 Agent 工作区。
3. 输入题材、消除次数、难度后进度到 `100%`
4. 点击“生成结果页”进入草稿承接页。
5. 可从草稿承接页返回 Agent 修改。
6. `npm run check:encoding` 通过。
7. `npm run typecheck` 通过。

View File

@@ -0,0 +1,394 @@
# 抓大鹅 Match3D F2 结果页与发布技术方案
日期:`2026-04-30`
## 1. 文档目的
本文件承接 [MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md](./MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md),只冻结 F2 开发范围:
1. Match3D 待发布结果页。
2. 作品基础信息编辑。
3. 发布前试玩入口。
4. 发布入口。
5. 已发布作品二次编辑恢复口径。
本阶段不实现运行态即时反馈 UI不实现 SpacetimeDB 表与 procedure不实现 `api-server` facade。F2 可以先基于 shared contracts 与 mock client 开发,等待 B4+B5 接入真实 HTTP。
---
## 2. 前置依赖
F2 依赖以下已冻结文档:
1. PRD[AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md](../prd/AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md)
2. A0[MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md](./MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md)
3. B1+B2[MATCH3D_DOMAIN_AND_CONTRACTS_STAGE1_2026-04-30.md](./MATCH3D_DOMAIN_AND_CONTRACTS_STAGE1_2026-04-30.md)
F2 可在 B4+B5 之前并行开发,但必须遵守 B2 的 TypeScript contract不得在前端私自扩字段。
---
## 3. 本阶段做
1. 新增 Match3D 结果页组件目录。
2. 新增 Match3D works service 目录。
3. 展示草稿配置摘要:
- 题材主题
- 需要消除次数
- 难度
- 参考图片预览
4. 支持编辑发布基础信息:
- 游戏名称
- 标签
- 封面图
5. 支持发布前试玩入口。
6. 支持试玩中止后回到结果页继续编辑。
7. 支持发布入口。
8. 支持已发布作品二次编辑的前端恢复路径。
---
## 4. 本阶段不做
1. 不生成题材物品素材。
2. 不生成额外封面图;封面图只接收已有图片、上传图片或后端已有占位结果。
3. 不要求试玩通关后才能发布。
4. 不实现运行态点击、飞入、三消等即时反馈。
5. 不实现首页、分类页和广场投影。
6. 不实现排行榜。
7. 不在 UI 中默认展示玩法规则说明长文。
8. 不把发布校验只写在前端;前端只做即时提示,后端 publish gate 是最终门槛。
---
## 5. 文件落点
## 5.1 前端组件
新增:
```text
src/components/match3d-result/
```
建议文件:
```text
src/components/match3d-result/Match3DResultView.tsx
src/components/match3d-result/Match3DResultView.test.tsx
src/components/match3d-result/index.ts
```
如组件变大,可后续拆分:
```text
Match3DResultHeader.tsx
Match3DResultBasicsForm.tsx
Match3DResultConfigPreview.tsx
Match3DResultPublishPanel.tsx
```
首版不要过早拆太多文件,优先保持可读和低冲突。
## 5.2 前端 service
新增:
```text
src/services/match3d-works/
```
建议文件:
```text
src/services/match3d-works/match3dWorksClient.ts
src/services/match3d-works/index.ts
```
F2 只负责 works 维度:
1. 读取作品详情。
2. 更新作品基础信息。
3. 发布作品。
4. 删除作品可后置,若 F4 需要再补。
运行态启动接口归 `src/services/match3d-runtime/`F2 只调用上层传入的 `onStartTestRun`
---
## 6. shared contracts 使用
F2 只消费 B2 已冻结的 TypeScript contract
```text
packages/shared/src/contracts/match3dWorks.ts
packages/shared/src/contracts/match3dAgent.ts
packages/shared/src/contracts/match3dRuntime.ts
```
必要类型:
1. `Match3DWorkProfile`
2. `Match3DWorkSummary`
3. `Match3DWorkUpdateRequest`
4. `Match3DPublishRequest`
5. `Match3DPublishResult`
6. `Match3DCompileDraftResult`
7. `Match3DCreatorConfig`
F2 不新增独立的前端私有数据结构来表达作品真相;只允许使用局部表单状态承载未保存输入。
---
## 7. 结果页 props contract
建议 `Match3DResultView` props
```ts
type Match3DResultViewProps = {
profile: Match3DWorkProfile;
draft?: Match3DCompileDraftResult | null;
isBusy?: boolean;
error?: string | null;
onBack: () => void;
onStartTestRun: (profile: Match3DWorkProfile) => void;
onPublish: (payload: Match3DPublishRequest) => void;
onSaved?: (profile: Match3DWorkProfile) => void;
};
```
说明:
1. `profile` 是结果页当前作品真相源。
2. `draft` 只用于展示草稿生成附加信息;不能覆盖 `profile` 的发布字段。
3. `onStartTestRun` 进入 F3/B5 运行态链路。
4. `onPublish` 可以先由 mock client 实现B5 完成后替换为真实 HTTP。
5. `onSaved` 用于把自动保存后的 profile 回写给上层流程控制器。
---
## 8. 页面内容顺序
结果页保持单列表,不做多 Tab。
固定顺序:
1. 顶部返回与保存状态。
2. 封面图。
3. 游戏名称。
4. 标签。
5. 题材主题。
6. 需要消除次数。
7. 难度。
8. 参考图片预览。
9. 试玩按钮。
10. 发布按钮。
UI 只呈现必要信息,不在页面中展示玩法规则说明长文。
---
## 9. 字段编辑规则
## 9.1 游戏名称
1. 必填。
2. 首版建议前端限制 `1~30` 个中文字符等价长度。
3. 默认值来自 Agent 确认题材或系统生成草稿。
## 9.2 标签
1. 必填。
2. 首版建议 `3~6` 个标签,与拼图发布门槛保持一致。
3. 输入支持中文逗号、英文逗号、顿号、换行拆分。
4. 前端需要去重和去空格。
## 9.3 封面图
1. 必填。
2. F2 可先复用参考图片、占位封面或用户上传图。
3. 图片真实存储由现有资产链或后续 B5 facade 处理。
4. 前端不得把本地临时 blob URL 当作已发布封面真相。
## 9.4 题材主题、需要消除次数、难度
首版结果页允许展示并可编辑这些配置。
修改后必须同步保存到作品 profile
1. `themeText`
2. `clearCount`
3. `difficulty`
注意:
1. `clearCount` 必须为正整数。
2. `difficulty` 必须在 `1~10`
3. 修改配置后,下一次试玩必须基于最新保存配置启动。
---
## 10. 自动保存
F2 建议实现自动保存,口径参考拼图结果页:
1. 输入变更后 `600ms` debounce。
2. 只保存结果页可编辑字段。
3. 保存中展示轻量状态。
4. 保存失败展示轻量错误,不弹长说明。
5. 发布前必须等待最后一次保存完成,或发布 payload 直接携带当前表单字段。
建议状态:
```ts
type Match3DAutoSaveState = 'idle' | 'saving' | 'saved' | 'error';
```
---
## 11. 发布门槛
前端即时 blocker
1. 游戏名称为空。
2. 标签数量不在 `3~6`
3. 封面图为空。
4. `clearCount` 不是正整数。
5. `difficulty` 不在 `1~10`
后端 publish gate 是最终门槛,前端不得绕过。
发布不要求试玩通关。
---
## 12. 试玩入口
结果页提供“试玩”入口。
行为:
1. 点击试玩前先保存当前表单。
2. 保存成功后调用 `onStartTestRun(profile)`
3. 上层进入 Match3D 运行态。
4. 运行态停止或返回后,回到同一个结果页继续编辑。
F2 不实现运行态本身;只冻结结果页如何发起试玩。
---
## 13. 发布接口
F2 service 建议接口:
```ts
const MATCH3D_WORKS_API_BASE = '/api/creation/match3d/works';
export async function getMatch3DWorkDetail(profileId: string): Promise<Match3DWorkDetailResponse>;
export async function updateMatch3DWork(
profileId: string,
payload: Match3DWorkUpdateRequest,
): Promise<Match3DWorkMutationResponse>;
export async function publishMatch3DWork(
profileId: string,
payload: Match3DPublishRequest,
): Promise<Match3DPublishResult>;
```
后续 B5 必须提供同名 HTTP facade 或在 service 层做最小适配。
---
## 14. Mock client 口径
F2 可以在真实 B5 接口完成前使用 mock client。
要求:
1. mock 数据必须来自 shared contracts。
2. mock profile 字段必须覆盖发布必填项。
3. mock publish 只能返回“可发布成功”的本地结果,不得伪造平台广场投影。
4. B5 接入后mock 只能保留为测试 fixture。
---
## 15. 已发布作品二次编辑
进入自己已发布 Match3D 作品时,结果页应支持二次编辑。
规则:
1. 优先通过 `sourceSessionId` 恢复原创作 session。
2. 如果没有 session则通过 `profileId` 读取作品详情进入结果页。
3. 二次发布不得创建新作品,必须覆盖同一 `profileId`
4. 不清零 `playCount`
5. 不改变作品归属。
---
## 16. 与其它分支的接口边界
## 16.1 依赖 F1
F1 负责创建会话和 Agent UI。F2 接收 F1 编译出的 `profile / draft`,不重复实现 Agent 对话。
## 16.2 依赖 F3
F3 负责运行态 UI。F2 只提供 `onStartTestRun` 入口。
## 16.3 依赖 B5
B5 负责真实 HTTP facade。F2 的 service path 和 DTO 必须按本文冻结,避免后续替换 mock 时改组件结构。
## 16.4 依赖 F4
F4 负责首页、分类页和广场分发。F2 发布成功后只需要把返回 profile 交给上层;不直接刷新广场列表。
---
## 17. 测试要求
建议新增:
```text
src/components/match3d-result/Match3DResultView.test.tsx
```
覆盖:
1. 展示游戏名称、标签、封面图、题材、需要消除次数和难度。
2. 游戏名称为空时发布按钮阻断。
3. 标签数量不足时发布按钮阻断。
4. `clearCount` 非正整数时发布按钮阻断。
5. `difficulty` 超出 `1~10` 时发布按钮阻断。
6. 点击试玩前触发保存。
7. 发布不要求试玩通关。
service 测试可在 B5 接入后补齐。
---
## 18. 验收命令
F2 文档分支:
```powershell
npm run check:encoding -- docs/technical/MATCH3D_F2_RESULT_AND_PUBLISH_2026-04-30.md docs/technical/README.md
```
F2 前端实现分支:
```powershell
npm run check:encoding
npm run typecheck
```
如新增组件测试,补跑对应 `vitest`
---
## 19. 一句话结论
F2 只负责把 Match3D 草稿变成可编辑、可试玩、可发布的作品工作台;它必须复用平台结果页和发布体验,发布不要求试玩通关,并为 B5 真实后端接口与 F3 运行态试玩入口保留清晰边界。

View File

@@ -0,0 +1,146 @@
# 抓大鹅 Match3D Q1 集成验收与收口记录 2026-05-01
## 1. 本轮目标
Q1 不新增玩法规则,只把第一至第三波已经形成的 Match3D 独立玩法域接成可跑主链:
1. 创作 Agent 前端从本地 mock 切到 `api-server` HTTP/SSE facade。
2. 结果页从临时草稿承接页升级为可编辑、可保存、可试玩、可发布的作品工作台。
3. 试玩运行态从结果页启动真实 `/api/runtime/match3d/*` run并继续保持“前端即时反馈 + 后端权威确认”。
4. 创作中心至少能读取当前用户 Match3D 作品列表,并支持打开草稿继续编辑。
本轮结论:已按合并顺序完成 Q1 主链集成。第一至第三波的主体能力均已落到工程Q1 已把它们串成“创作 Agent -> 结果页保存/发布/试玩 -> 公开详情/作品号搜索 -> 运行态”的最小可跑链路。
## 2. 第一至第三波验收口径
### 第一波 A0
文档已存在:
```text
docs/technical/MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md
```
结论:已完成。该文档冻结了独立玩法域、表与 procedure、HTTP facade、前端即时反馈协议和合并顺序。
### 第二波 B1 + B2
已落点:
```text
server-rs/crates/module-match3d/
server-rs/crates/shared-contracts/src/match3d_*.rs
packages/shared/src/contracts/match3d*.ts
```
结论:已完成。领域 crate、Rust DTO、TypeScript DTO 已存在,并已通过 Q1 定向复跑。
### 第二波 B3
已落点:
```text
server-rs/crates/spacetime-module/src/match3d/
server-rs/crates/spacetime-module/src/migration.rs
```
结论:已完成。四张 Match3D 表已纳入 migrationprocedure 已接 `module-match3d` 领域规则。本轮不改表结构,不需要新增 migration。
### 第二波 F1
已落点:
```text
src/components/match3d-creation/
src/services/match3d-creation/
src/components/platform-entry/
```
结论:已完成并已接入 Q1。入口与 Agent UI 已存在,`match3dCreationClient` 已从本地 mock 切到 `api-server` HTTP/SSE facade本地 mock 只保留在测试夹具和 `/match3d` playground 运行调试链路中。
### 第二波 F3
已落点:
```text
src/components/match3d-runtime/
src/services/match3d-runtime/match3dLocalRuntime.ts
src/Match3DPlaygroundApp.tsx
```
结论:已完成并已接入 Q1。圆形空间、7 格备选栏、乐观点击、三消反馈、结算面板和回滚校正语义已存在Q1 已补真实 runtime client 与平台入口接线。
### 第三波 B4 + B5
已落点:
```text
server-rs/crates/spacetime-client/src/match3d.rs
server-rs/crates/api-server/src/match3d.rs
server-rs/crates/api-server/src/app.rs
```
结论已完成。HTTP facade 路由已注册Q1 前端已按这些稳定路由接入。
### 第三波 F2
目标落点:
```text
src/components/match3d-result/
src/services/match3d-works/
```
结论:已完成并已接入 Q1。新增 `Match3DResultView``match3d-works` service支持基础信息编辑、保存、发布、试玩入口发布仍要求封面和标签门槛试玩只要求基础配置可保存。
### 第三波 F4
结论:已完成 Q1 最小平台分发。创作中心作品货架、公开卡片映射、统一作品详情、`M3-xxxxxxxx` 作品号搜索和详情页启动运行态已接入;排行榜、点赞、改造统计和更复杂推荐策略仍留到后续优化。
## 3. Q1 本轮代码落点
本轮实际落点:
1. `src/services/match3d-creation/`:替换本地 mock 为 HTTP/SSE facade。
2. `src/services/match3d-works/`:新增作品读取、保存、发布 service。
3. `src/services/match3d-runtime/`:新增真实运行态 service保留本地 playground mock。
4. `src/components/match3d-result/`:新增结果页组件。
5. `src/components/platform-entry/`:串起结果页、试玩 run、作品列表刷新。
6. `src/components/custom-world-home/` 与展示映射:扩展 Match3D 作品货架、公开卡片、统一详情页。
7. `src/services/publicWorkCode.ts``src/routing/appPageRoutes.ts`:新增 `M3-xxxxxxxx` 作品号与公开详情路由识别。
8. `src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`:补齐 Match3D 作品号搜索启动运行态回归,并同步统一详情页后的 RPG/Big Fish 旧测试语义。
## 4. 验收命令
本轮已通过:
```powershell
npm test -- src/components/match3d-result/Match3DResultView.test.tsx src/components/match3d-runtime/Match3DRuntimeShell.test.tsx src/routing/appPageRoutes.test.ts src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx --reporter=verbose --silent
```
结果:`6 passed``65 passed`
```powershell
cargo test -p module-match3d
cargo test -p shared-contracts
cargo check -p api-server
cargo check -p spacetime-client
npm run check:encoding
```
结果:
1. `module-match3d``7 passed`
2. `shared-contracts``47 passed`
3. `api-server``cargo check` 通过。
4. `spacetime-client``cargo check` 通过。
5. 编码检查:`2804 file(s)` 通过。
## 5. 本轮不做与遗留风险
1. 不改 Match3D 表结构。
2. 不扩展排行榜、点赞、二次创作统计。
3. 不把 Match3D 公开广场并入更复杂的推荐、排行和运营榜单策略。
4. 不删除 `/match3d` 本地 playground它作为开发调试入口继续保留。
5. 全量 `npm run typecheck` 曾存在非 Match3D 既有阻塞,本轮以 Q1 定向测试和后端定向检查作为集成验收口径。
6. Maincloud 运行态仍依赖当前 SpacetimeDB 环境稳定性;如 `npm run api-server:maincloud` 现场遇到订阅 HTTP 500应按 Maincloud/SpacetimeDB 联调链路单独排查。

View File

@@ -0,0 +1,131 @@
# 抓大鹅 Match3D B4+B5 spacetime-client 与 api-server facade 落地记录
日期:`2026-04-30`
## 1. 本阶段目标
本文件记录 B4+B5 的技术落地范围:把 B3 已生成的 Match3D SpacetimeDB procedure 接到 `spacetime-client`,再通过 `api-server` 暴露给前端使用的 HTTP facade。
本阶段不改 SpacetimeDB 表结构,不新增 migration不接入真实题材素材生成也不改前端即时反馈实现。
## 2. 已落地范围
### 2.1 SpacetimeDB bindings
使用仓库封装脚本重新生成 bindings
```powershell
npm run spacetime:generate
```
Windows 下 SpacetimeDB CLI 可能在 Rust bindings 已生成后输出 `Could not format generated files: 文件名或扩展名太长。 (os error 206)`。脚本已调整为:当 CLI 退出码为 `0` 且只是格式化警告时继续同步生成文件。
生成文件仍视为机器产物,禁止手写修改。
### 2.2 spacetime-client facade
新增:
```text
server-rs/crates/spacetime-client/src/match3d.rs
```
并在 `spacetime-client/src/lib.rs` 导出 `match3d` 模块与 Match3D Record 类型。
已覆盖:
1. 创作会话create / get / submit message / finalize / compile draft。
2. 作品list / get detail / update / publish / delete / public gallery list。
3. 运行态start / get / click / stop / restart / time-up。
`mapper.rs` 负责把 procedure 返回的 JSON 字符串解析为稳定 Record不把 generated bindings 泄露到 `api-server`
### 2.3 api-server HTTP facade
新增:
```text
server-rs/crates/api-server/src/match3d.rs
```
并在 `main.rs` 注册模块,在 `app.rs` 挂载路由。
已挂载路由:
```text
POST /api/creation/match3d/sessions
GET /api/creation/match3d/sessions/{session_id}
POST /api/creation/match3d/sessions/{session_id}/messages
POST /api/creation/match3d/sessions/{session_id}/messages/stream
POST /api/creation/match3d/sessions/{session_id}/actions
POST /api/creation/match3d/sessions/{session_id}/compile
GET /api/creation/match3d/works
GET /api/creation/match3d/works/{profile_id}
PATCH /api/creation/match3d/works/{profile_id}
PUT /api/creation/match3d/works/{profile_id}
DELETE /api/creation/match3d/works/{profile_id}
POST /api/creation/match3d/works/{profile_id}/publish
GET /api/runtime/match3d/gallery
POST /api/runtime/match3d/works/{profile_id}/runs
GET /api/runtime/match3d/runs/{run_id}
POST /api/runtime/match3d/runs/{run_id}/click
POST /api/runtime/match3d/runs/{run_id}/stop
POST /api/runtime/match3d/runs/{run_id}/restart
POST /api/runtime/match3d/runs/{run_id}/time-up
```
`api-server` 返回 `shared-contracts` 中的 Match3D DTO前端不需要感知 SpacetimeDB 内部 JSON 快照结构。
## 3. 创作 Agent 当前口径
B5 首版先采用确定性配置抽取,不在本阶段新增真实 LLM prompt。
2026-05-01 起,抓大鹅创作入口必须按三轮 Agent 问答收集配置,不能在用户未回答前用默认值生成“已确认”回复:
1. `POST /api/creation/match3d/sessions` 创建会话后,首条 assistant 消息固定为“你想创作什么题材”。
2. 用户第一轮回复只写入题材assistant 继续问“需要消除多少次才能通关”。
3. 用户第二轮回复只写入需要消除次数assistant 继续问“如果难度是从1-10你要创作的关卡是难度几”。
4. 用户第三轮回复写入难度后assistant 才返回“已确认:...”,并把进度推进到 `100`、stage 推进到 `ReadyToCompile`
5. SpacetimeDB 当前配置快照仍要求合法数值,因此 `api-server` facade 可以在 `config_json` 内保留兜底合法值,但回复、进度和是否允许生成结果页必须以三轮问答进度为准。
6. `match3d_compile_draft` 动作只能在三项收集完成后调用 SpacetimeDB `compile_match3d_draft`,生成 draft work profile。
后续若要接真实 LLM turn应复用现有创作 Agent 公共编排,并保持 submit/finalize 两阶段职责不变。
## 4. 运行态确认协议
B5 保持 PRD 调整后的边界:
1. 前端负责点击、飞入、入槽、三消、腾格、胜负等即时表现。
2. 后端通过 `click_match3d_item``finish_match3d_time_up` 等 procedure 做权威确认。
3. HTTP response 会把 SpacetimeDB `Accepted / VersionConflict / RunFinished` 等状态归一到前端 shared contract 可消费的 `accepted / rejectReason / run` 结构。
4. `snapshotVersion` 继续作为前端即时反馈和后端确认之间的版本校验字段。
## 5. shared contract 对齐
本阶段补齐 Rust shared contract 与 TypeScript contract 的已知差异:
1. `Match3DAgentSessionSnapshotResponse` 增加 `anchorPack`
2. `Match3DResultDraftResponse` 增加 `profileId / summaryText / totalItemCount`,同时保留 `summary` 兼容结果页读取。
3. `PutMatch3DWorkRequest` 增加可选 `themeText`,结果页可编辑题材;旧请求缺省时 API 会沿用已有作品题材。
## 6. 验收命令
本阶段至少执行:
```powershell
cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml
cargo check -p api-server --manifest-path server-rs\Cargo.toml
cargo test -p shared-contracts match3d --manifest-path server-rs\Cargo.toml
npm run check:encoding
npm run api-server:maincloud
```
`api-server:maincloud` 是修改后端后的必跑项;如果本地缺少 Maincloud 环境或 SpacetimeDB 发布态不一致,需要在最终结果里明确说明。
## 7. 后续接入点
1. F1/F2/F3 可把 mock client 替换到上述 HTTP facade。
2. F4 平台分发可先读取 `/api/runtime/match3d/gallery` 的已发布作品列表。
3. 若后续要记录排行榜或作品播放统计,需要补 Match3D 成绩表或 play record procedure并同步更新 migration。

View File

@@ -6,33 +6,33 @@
本轮在“我的”页面的“会员充值”入口落地账户充值弹窗,包含两个页签:
1. `叙世币充值`
1. `光点充值`
2. `会员卡充值`
前端只负责展示与发起购买,套餐、价格、赠送规则、会员权益、生效时间、钱包余额与交易流水统一由 `server-rs` 后端返回。当前没有真实支付网关,本轮采用服务端模拟支付成功:创建订单后立即写入余额或会员状态,并返回最新账户中心快照。后续接入真实支付时,只替换订单支付状态推进,不改前端套餐与账户快照 contract。
## 2. 产品规则
### 2.1 叙世币充值套餐
### 2.1 光点充值套餐
| productId | 叙世币 | 金额分 | 徽标 | 说明 |
| productId | 光点 | 金额分 | 徽标 | 说明 |
| --- | ---: | ---: | --- | --- |
| `points_60` | 60 | 600 | 首充双倍 | 首充送60叙世币 |
| `points_180` | 180 | 1800 | 首充双倍 | 首充送180叙世币 |
| `points_300` | 300 | 3000 | 首充双倍 | 首充送300叙世币 |
| `points_680` | 680 | 6800 | 首充双倍 | 首充送680叙世币 |
| `points_1280` | 1280 | 12800 | 首充双倍 | 首充送1280叙世币 |
| `points_3280` | 3280 | 32800 | 首充双倍 | 首充送3280叙世币 |
| `points_60` | 60 | 600 | 首充双倍 | 首充送60光点 |
| `points_180` | 180 | 1800 | 首充双倍 | 首充送180光点 |
| `points_300` | 300 | 3000 | 首充双倍 | 首充送300光点 |
| `points_680` | 680 | 6800 | 首充双倍 | 首充送680光点 |
| `points_1280` | 1280 | 12800 | 首充双倍 | 首充送1280光点 |
| `points_3280` | 3280 | 32800 | 首充双倍 | 首充送3280光点 |
叙世币充值固定为 `¥6 / ¥18 / ¥30 / ¥68 / ¥128 / ¥328` 六个档位。全部档位参与首充双倍:用户历史上没有 `points_recharge` 流水时,本次购买到账叙世币为基础叙世币与等额赠送叙世币之和;已有充值流水后只到账基础叙世币。实际到账叙世币写入交易流水,余额以 SpacetimeDB projection 为准。
光点充值固定为 `¥6 / ¥18 / ¥30 / ¥68 / ¥128 / ¥328` 六个档位。全部档位参与首充双倍:用户历史上没有 `points_recharge` 流水时,本次购买到账光点为基础光点与等额赠送光点之和;已有充值流水后只到账基础光点。实际到账光点写入交易流水,余额以 SpacetimeDB projection 为准。
### 2.2 会员卡套餐
| productId | 类型 | 天数 | 金额分 | 权益 |
| --- | --- | ---: | ---: | --- |
| `member_month` | 月卡 | 30 | 2800 | 免叙世币回合数100每日签到加成0% |
| `member_season` | 季卡 | 90 | 7800 | 免叙世币回合数100每日签到加成100% |
| `member_year` | 年卡 | 365 | 24800 | 免叙世币回合数100每日签到加成210% |
| `member_month` | 月卡 | 30 | 2800 | 免光点回合数100每日签到加成0% |
| `member_season` | 季卡 | 90 | 7800 | 免光点回合数100每日签到加成100% |
| `member_year` | 年卡 | 365 | 24800 | 免光点回合数100每日签到加成210% |
购买会员时,如果当前会员仍有效,则从当前到期时间顺延;如果已过期或从未购买,则从当前服务端时间开始计算。状态只区分 `普通` 与已生效会员,前端不自行推断。
@@ -42,8 +42,8 @@
需要 Bearer JWT。返回
1. 当前叙世币余额、会员状态、到期时间
2. 叙世币套餐与会员套餐
1. 当前光点余额、会员状态、到期时间
2. 光点套餐与会员套餐
3. 会员权益表
4. 最近订单摘要
@@ -64,7 +64,7 @@
1. 校验 `productId`
2. 后端创建已支付订单
3. 叙世币套餐写入钱包余额与流水
3. 光点套餐写入钱包余额与流水
4. 会员套餐写入会员状态
5. 返回最新账户中心快照与订单摘要
@@ -74,15 +74,15 @@
1. “我的”页会员充值按钮打开独立弹窗,不在当前面板下方展开。
2. 弹窗顶部标题为 `账户充值`,右上角关闭。
3. 默认打开 `叙世币充值`,可切换到 `会员卡充值`
3. 默认打开 `光点充值`,可切换到 `会员卡充值`
4. 点击套餐后调用下单接口,按钮进入处理中状态,成功后刷新 `profileDashboard`
5. 弹窗内不写大段说明文案,只保留必要金额、叙世币、会员权益和状态反馈。
5. 弹窗内不写大段说明文案,只保留必要金额、光点、会员权益和状态反馈。
6. 会员卡充值区以套餐卡片优先展示周期、价格和处理状态;移动端单列,桌面端三列,权益表允许横向滚动,避免小屏挤压。
## 5. 验收
1. 普通用户打开弹窗能看到叙世币与会员套餐。
2. 叙世币购买后余额增加,流水来源为 `points_recharge`
3. 首充赠送只在首次叙世币充值时生效。
1. 普通用户打开弹窗能看到光点与会员套餐。
2. 光点购买后余额增加,流水来源为 `points_recharge`
3. 首充赠送只在首次光点充值时生效。
4. 会员购买后会员状态与到期时间立即更新。
5. 移动端弹窗单列可滚动,桌面端接近参考图卡片网格。

View File

@@ -0,0 +1,93 @@
# “我的”资料卡昵称与头像编辑落地说明
日期:`2026-04-29`
## 1. 背景
本次迭代基于 `docs/prd/MY_TAB_PROFILE_IDENTITY_CARD_PRD_2026-04-16.md` 落地,但交互口径有两处收敛:
1. 昵称编辑不进入账号安全弹窗,点击昵称后的编辑按钮直接打开独立轻弹窗。
2. 头像编辑不进入通用资料抽屉,点击头像先选择本地图片,校验通过后进入头像裁剪弹窗。
资料卡仍保持清爽,不展示规则说明型长文案。
## 2. 前端交互
### 2.1 百梦号复制
1. 点击“我的”页百梦号后的复制按钮后,按钮文案临时切换为 `已复制`
2. 复制失败时临时切换为 `复制失败`
3. 状态自动恢复为 `复制`
### 2.2 昵称修改
1. 点击昵称右侧编辑按钮打开独立弹窗。
2. 弹窗内只提供昵称输入、取消、保存。
3. 弹窗面板使用平台标准不透明面板底,不复用透明轻量面板。
4. 前端先做长度与字符校验:
- `2-20` 个字符。
- 允许中文、英文、数字、下划线。
- 不允许纯空白。
5. 保存调用 `PATCH /api/profile/me`,成功后即时回写 `AuthUiContext.user`
### 2.3 头像上传与裁剪
1. 点击头像触发文件选择。
2. 前端先审核文件:
- MIME 类型仅允许 `image/jpeg``image/png``image/webp`
- 单文件不超过 `5MB`
3. 校验通过后读取为图片,打开裁剪弹窗。
4. 裁剪弹窗面板使用平台标准不透明面板底,避免底层资料卡内容透出。
5. 裁剪工具使用正方形裁剪框,支持拖动裁剪区域与缩放图片。
6. 保存时前端输出 `256x256` 的 PNG data URL调用 `PATCH /api/profile/me` 保存为账号头像。
7. 成功后资料卡头像立即展示新图。
## 3. 后端契约
### `PATCH /api/profile/me`
请求:
```json
{
"displayName": "新昵称",
"avatarDataUrl": "data:image/png;base64,..."
}
```
两个字段均可选,但至少提供一个有效字段。
响应:
```json
{
"user": {
"id": "user_00000001",
"publicUserCode": "SY-00000001",
"username": "phone_xxx",
"displayName": "新昵称",
"avatarUrl": "data:image/png;base64,...",
"phoneNumberMasked": "138****8000",
"loginMethod": "phone",
"bindingStatus": "active",
"wechatBound": false
}
}
```
## 4. 存储边界
当前头像先作为裁剪后的 `256x256` data URL 写入认证快照,保证账号资料可立即持久化和恢复。后续若接入 OSS 头像对象,应保持前端裁剪输出不变,只把后端 `avatarUrl` 从 data URL 替换为私有读代理 URL。
SpacetimeDB 正式表 `user_account` 需要增加 `avatar_url: Option<String>`,并在认证快照导入/导出、迁移导入兼容中对齐。
## 5. 验收
1. 创作页已发布作品分享按钮点击后显示 `已复制`
2. “我的”页百梦号复制按钮点击后显示 `已复制`
3. “我的”页不展示 `手机号``正常` 标签。
4. 昵称编辑成功后,资料卡与顶部账号入口同步新昵称。
5. 昵称与头像裁剪弹窗面板不透明,不能露出底层页面内容。
6. 非法头像文件不会进入裁剪流程。
7. 裁剪保存成功后,资料卡头像展示裁剪后的图片。
8. 桌面右上角账号入口与“我的”资料卡共用 `avatarUrl`,有已保存头像时展示头像图片,缺失时才回退到首字头像。

View File

@@ -1,20 +1,20 @@
# 我的 Tab 邀请与玩家社区首期落地方案
更新时间:`2026-04-25`
更新时间:`2026-05-01`
## 目标
在现有“我的”Tab 常用功能落地三个轻量入口:
在现有“我的”Tab 功能入口区(常用功能落地三个轻量入口,入口顺序固定为 `邀请好友``填邀请码``玩家社区`
1. `邀请好友`:弹出面板展示当前账号绑定的邀请码。
2. `填邀请码`:弹出面板填写邀请码,成功后邀请者与被邀请者各获得 `30` 叙世币
3. `玩家社区`:弹出面板展示微信群与 QQ 群二维码占位图,后续替换为正式图片。
1. `邀请好友`:弹出面板展示当前账号绑定的邀请码、邀请奖励规则和成功邀请用户列表
2. `填邀请码`:弹出面板填写邀请码,成功后邀请者与被邀请者各获得 `30` 光点
3. `玩家社区`:弹出面板展示微信群与 QQ 群正式二维码图片。
## 后端边界
- 邀请码、邀请关系与奖励发放全部存入 `server-rs/crates/spacetime-module`
- Axum 只做鉴权、参数转发与响应映射,不在 API 层自行计算奖励。
- 前端只读取后端状态与调用提交接口,不做本地加叙世币
- 前端只读取后端状态与调用提交接口,不做本地加光点
- 钱包余额继续复用 `profile_dashboard_state.wallet_balance`
- 奖励流水继续复用 `profile_wallet_ledger`,新增来源类型:
- `invite_inviter_reward`
@@ -43,7 +43,7 @@
- 每个用户拥有一个稳定邀请码,首次进入邀请中心时自动生成。
- 用户不能填写自己的邀请码。
- 用户最多填写一个邀请码,成功后不可修改。
- 被邀请者绑定成功后获得 `30` 叙世币
- 被邀请者绑定成功后获得 `30` 光点
- 邀请者每天最多获得 `10` 次邀请奖励,超过后关系仍可绑定,被邀请者仍获得奖励,邀请者当次不再加分。
- 每次奖励都写入钱包流水,钱包余额以后端返回为准。
@@ -51,7 +51,26 @@
### `GET /api/runtime/profile/referrals/invite-center`
返回当前用户的邀请码、邀请链接、今日奖励次数、剩余奖励次数、已绑定状态奖励参数。
返回当前用户的邀请码、邀请链接、今日奖励次数、剩余奖励次数、已绑定状态奖励参数与成功邀请用户列表
成功邀请用户列表字段:
```json
{
"invitedUsers": [
{
"userId": "user_001",
"displayName": "百梦玩家",
"avatarUrl": null,
"boundAt": "2026-05-01T08:00:00Z"
}
]
}
```
- `invitedUsers` 只包含当前账号作为邀请人的关系。
- 列表按 `boundAt` 倒序返回,最多展示最近 `20` 位成功邀请用户。
- 昵称与头像从 `user_account` 读取;缺失昵称时前端回退展示 `玩家`
### `POST /api/runtime/profile/referrals/redeem-code`
@@ -69,13 +88,18 @@
- `server-rs/crates/spacetime-module` 已新增邀请码与邀请关系表,邀请中心读取和填码绑定均通过 SpacetimeDB procedure 执行。
- `server-rs/crates/api-server` 已挂接 `/api/runtime/profile/referrals/*``/api/profile/referrals/*` 两组路由。
- 前端“我的”Tab 三个快捷入口均打开独立弹窗,玩家社区使用空白二维码占位
- 复制邀请会复制邀请码和邀请链接;填码成功后刷新个人看板叙世币
- 前端“我的”Tab 三个功能入口均打开独立弹窗,玩家社区使用 `media/social-media-group/wechat.png``media/social-media-group/qq.png` 两张正式二维码图片
- 复制邀请会复制邀请码和邀请链接;填码成功后刷新个人看板光点
- 邀请好友弹窗展示 `邀请一个用户注册,双方都可获得 30 光点。每日最多获得十次邀请奖励。`,不再展示“邀请 / 已奖 / 今日”三项统计。
- 邀请好友弹窗底部展示成功邀请用户头像和昵称列表;没有成功邀请时展示短空状态。
- “我的”页 `邀请好友` 按钮副标题展示 `双方得30光点icon``玩家社区` 按钮副标题展示 `每日领福利`
- “我的”页功能入口区不展示 `常用功能` 标题和 `快捷入口` 副标题,避免首屏重复说明类文案。
## 前端交互
- 三个入口继续放在“我的”Tab 常用功能,不新增页面。
- `邀请好友` 弹窗展示邀请码、复制按钮、邀请链接
- `填邀请码` 弹窗在未绑定时展示输入框;已绑定时展示短状态
- `玩家社区` 弹窗展示两个紧凑二维码占位区
- 弹窗文案只保留必要标签和短提示,不放长规则说明。
- 三个入口继续放在“我的”Tab 功能入口区(常用功能,不新增页面。
- `邀请好友` 弹窗展示邀请码、复制按钮、邀请奖励规则和成功邀请用户头像昵称列表
- `填邀请码` 入口只在账号注册后 `24` 小时内且尚未填写过邀请码时展示;若 `auth.user.createdAt` 缺失或解析失败,前端按已超时处理并隐藏入口
- `填邀请码` 弹窗在未绑定时展示输入框;成功绑定后刷新邀请中心与个人看板,并隐藏常用功能里的入口
- `玩家社区` 弹窗展示两个紧凑二维码图片区,保留微信群与 QQ 群短标签。
- 弹窗文案只保留必要标签和短提示;本次邀请奖励规则属于必要交易说明,固定展示在邀请码下方。

View File

@@ -1,6 +1,6 @@
# 密码登录入口历史落地设计
> 2026-04-25 更新:当前产品策略已调整为“不开放密码注册”。新用户必须通过手机号验证码注册/登录,密码登录只面向已经登录后设置过密码的手机号账号。`POST /api/auth/entry` 只接受 `phone + password`,不支持邮箱、用户名或叙世号登录,也不承担自动建号能力。本文原有“密码自动建号”内容仅作为历史背景保留,当前落地以本更新和 [PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md](./PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md) 为准。
> 2026-04-25 更新:当前产品策略已调整为“不开放密码注册”。新用户必须通过手机号验证码注册/登录,密码登录只面向已经登录后设置过密码的手机号账号。`POST /api/auth/entry` 只接受 `phone + password`,不支持邮箱、用户名或百梦号登录,也不承担自动建号能力。本文原有“密码自动建号”内容仅作为历史背景保留,当前落地以本更新和 [PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md](./PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md) 为准。
>
> 2026-04-28 更新:为开发期本地/测试服联调新增服务端环境变量 `GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED`,默认 `false`。仅当该变量显式为 `true` 时,`POST /api/auth/entry` 可对未知手机号用本次密码直接创建账号并登录;默认关闭时仍严格保持未知手机号返回 `401` 的生产语义。该开关不得用于生产环境,也不新增任何前端规则说明文案。
@@ -17,7 +17,7 @@
1. `api-server` 对外只暴露 `phone + password` 的最小接口。
2. `module-auth` 只负责已存在手机号账号的密码校验。
3. 密码入口不创建账号,不接收邮箱、用户名或叙世号。
3. 密码入口不创建账号,不接收邮箱、用户名或百梦号。
4. 登录成功后与 JWT、refresh cookie 的衔接方式。
## 1.1 当前冻结结论
@@ -239,7 +239,7 @@
1. 未知手机号密码登录返回 `401`,且不创建账号。
2. 已登录手机号账号设置密码后可用 `phone + password` 登录。
3. 同手机号错误密码返回 `401`
4. 邮箱、用户名或叙世号作为密码登录标识返回 `400`
4. 邮箱、用户名或百梦号作为密码登录标识返回 `400`
5. 登录成功时返回 access token。
6. 登录成功时写回 refresh cookie。
7. `GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED` 默认关闭时行为不变。

View File

@@ -19,7 +19,7 @@
沿用现有 `POST /api/auth/entry`
1. 请求字段固定为 `phone``password`,前端只提交手机号。
2. 后端只按标准手机号归一化后查找账号,不兼容邮箱、用户名、叙世号或历史开发游客标识。
2. 后端只按标准手机号归一化后查找账号,不兼容邮箱、用户名、百梦号或历史开发游客标识。
3. 手机号不存在时返回 `401`,不创建账号。
4. 手机号存在但未设置密码时返回 `401`
5. 校验成功后签发 access token并写入 refresh cookie。

View File

@@ -0,0 +1,22 @@
# 网页端首页模块内容同步移动端首页 2026-04-30
## 背景
平台首页移动端已经收口为 `推荐 / 今日游戏 / 游戏分类` 三个频道。网页端首页保留宽屏布局,但模块文案和数据语义仍残留 `趋势关注``最新发布``作品广场` 等旧入口口径,导致双端首页内容不一致。
## 落地规则
1. 网页端首页只调整模块内容和文案,不改变现有宽屏栅格、面板数量与卡片布局。
2. `推荐` 使用移动端推荐频道同源数据:精选作品优先,并与最新公开作品去重合并。
3. `趋势关注` 改为 `今日游戏`,数据只取今天首次发布的公开作品,不把今天更新的旧作品计入今日游戏。
4. `最新发布` 改为 `作品分类`,数据使用当前分类组内按综合指标排序后的作品。
5. 首页首屏和快捷区域不再展示 `作品广场` 文案。
6. 删除首页中的 `公开作品` 兜底模块;快捷区域只在存在最近作品或最近浏览时显示,不再用空模块占位。
## 验收标准
1. 网页端首页仍保持原有 hero、右侧列表、中部双栏与底部网格布局。
2. 网页端可见模块包含推荐、今日游戏、作品分类。
3. 网页端首页不再出现 `趋势关注``最新发布``作品广场`
4. 无最近作品和最近浏览时,网页端首页不再展示 `公开作品` 快捷模块。
5. 今日游戏与移动端 `今日游戏` 频道使用同一发布时间过滤规则。

View File

@@ -0,0 +1,26 @@
# 平台首页作品模糊搜索 2026-05-01
## 背景
首页顶部搜索框原本主要承担公开编号直达能力,适合输入 `SY / CW / BF / M3 / PZ` 编号后打开用户或作品详情。用户在浏览首页时也会按作品名称、作者昵称或作品描述回忆作品,现有搜索口径没有覆盖这些常见路径。
## 落地规则
1. 首页搜索框先在当前公开作品聚合列表中做本地模糊匹配。
2. 匹配字段包含:
- 作品 ID`publicWorkCode``profileId``workId`
- 作品名称:`worldName`
- 作者昵称:`authorDisplayName`
- 作品描述:`summaryText``subtitle`
3. 匹配忽略大小写,允许用户输入去掉空格、连字符或下划线后的连续片段,例如 `PZEPUBLIC1` 命中 `PZ-EPUBLIC1`
4. 输入完整公开作品号并本地命中时,保留既有作品号直达行为。
5. 输入模糊片段命中公开作品时,在首页直接展示搜索结果列表,点击结果打开对应作品详情。
6. 当前公开作品列表无命中时,保留既有公开编号直达兜底,继续支持远端按作品号或百梦号查找。
## 验收标准
1. 输入作品号片段可命中对应公开作品,输入完整公开作品号仍可直达。
2. 输入作品名称片段可命中对应公开作品。
3. 输入作者昵称片段可命中对应公开作品。
4. 输入作品描述片段可命中对应公开作品。
5. 未命中本地公开作品时,原有编号搜索行为不退化。

View File

@@ -0,0 +1,18 @@
# 平台首页移动端底部 Dock 可见视口修复
## 背景
手机浏览器会把顶部地址栏纳入传统 `100vh` 的计算,导致平台首页根容器高于真实可见区域。底部 dock 虽然在 flex 布局末尾,但会被推到浏览器可见区域之外,用户需要滚动或收起地址栏后才能看到。
## 落地口径
- 平台入口根壳统一使用 `.platform-viewport-shell`,优先按 `100dvh` 约束高度,旧浏览器回退到 `100vh`
- 移动端首页底部 dock 使用 `.platform-mobile-bottom-dock` 固定在可见视口底部,并叠加 `safe-area-inset-bottom`
- 移动端首页内容壳通过 `--platform-bottom-dock-outer-height` 预留底部空间,避免滚动内容被固定 dock 遮挡。
- 不新增 UI 说明文案,不改变底部导航业务语义。
## 验收
- 手机竖屏打开平台首页时,底部 dock 始终贴住浏览器可见区域底部。
- 浏览器地址栏展开时dock 不应被挤到屏幕外。
- 主页、分类、创作、存档、我的五个 tab 均保持原有点击行为。

View File

@@ -0,0 +1,24 @@
# 百梦产品命名规范落地说明
## 背景
平台对外中文命名统一使用以下称谓:
- 产品中文展示名:`百梦`
- 平台内消费单位:`光点`
- 公开账号标识:`百梦号`
- 创作侧面向创作者称谓:`百梦主`
## 落地边界
1. 前端页面、弹窗、测试断言和后端返回给用户的中文错误文案统一使用新称谓。
2. SpacetimeDB 表字段、Rust/TypeScript contract 字段、流水来源枚举、`points_*` 商品 ID、`public_user_code` 字段名继续保持不变,避免引入数据库迁移和历史数据兼容风险。
3. 公开编号现有 `SY-XXXXXXXX` 格式本轮不迁移,只调整用户可见标签为“百梦号”;编号格式如需改为新前缀,应另起迁移方案并同步老用户兼容策略。
4. 历史日志、构建产物、第三方依赖和生成绑定不参与本轮文本替换。
## 验收点
1. 首页、登录绑定页、我的页和搜索结果不再展示旧产品名。
2. 钱包、充值、邀请、兑换码、资产计费和拼图道具确认文案统一展示“光点”。
3. 账号公开标识相关错误和搜索空状态统一展示“百梦号”。
4. 创作相关可见默认称谓使用“百梦主”。

View File

@@ -0,0 +1,24 @@
# 百梦产品命名替换落地说明
## 1. 本轮目标
本轮统一平台对外中文命名,当前生效称谓如下:
- 产品中文展示名:`百梦`
- 平台内消费单位:`光点`
- 公开账号标识:`百梦号`
- 创作侧面向创作者称谓:`百梦主`
## 2. 落地范围
1. 前端网页、管理后台、HTML 标题、metadata 与品牌标识统一展示“百梦”。
2. 钱包、充值、邀请、兑换码、资产计费、拼图道具与作者激励统一展示“光点”。
3. 公开账号标识、搜索兜底、登录限制与错误信息统一展示“百梦号”。
4. 创作侧面向创作者的称谓统一展示“百梦主”。
5. 后端错误信息、默认商品文案、测试断言与文档说明同步更新。
## 3. 非目标
1. 本轮只调整对外文本不修改数据库字段名、API 字段名、流水 source、商品 productId 或现有 `SY-XXXXXXXX` 公开编号格式。
2. 不修改 SpacetimeDB 表结构,因此不需要新增 migration。
3. 不引入新的前端页面或后端系统。

View File

@@ -0,0 +1,55 @@
# 注册环节邀请码与管理员邀请码方案
更新时间:`2026-04-30`
## 背景
旧版“我的 Tab 填邀请码”设计把邀请码绑定放在登录后的个人面板中,容易让老账号重复发现入口,也不利于承接带邀请码的分享链接。本方案将邀请码填写收口到注册链路:未登录用户打开带 `inviteCode``invite_code` 查询参数的链接时,前端自动打开注册弹窗并预填邀请码。
## 落地边界
1. 注册入口复用当前手机号验证码登录自动建号能力,不新增独立注册系统。
2. 已登录用户不自动弹注册弹窗;登录后的“我的 Tab”只保留“邀请好友”不再提供“填邀请码”入口。
3. 邀请码只在本次手机号验证码登录创建新账号时尝试绑定。老账号登录时即使请求体带邀请码,也不会绑定。
4. 链接邀请码无效或不可用时不阻断注册,登录响应返回短错误提示,由前端展示;不写邀请关系、不发邀请奖励。
5. 普通登录态下的 `/api/profile/referrals/redeem-code` 不再允许手动填码,统一返回“邀请码仅注册时填写”。
## 数据与接口
`profile_invite_code` 增加 `metadata_json` 字段,默认 `{}`,用于保存渠道、活动、批次等元数据。旧迁移导入数据缺失该字段时由 `migration.rs``{}`
新增管理员接口:
- `POST /admin/api/profile/invite-codes`
请求:
```json
{
"inviteCode": "SPRING2026",
"metadata": {
"campaign": "spring"
}
}
```
管理员邀请码写入 SpacetimeDB 时使用虚拟主体:
```text
admin:{管理员用户名}:{邀请码}
```
管理员码只做归因和被邀请人奖励,不给虚拟主体写邀请人钱包流水。
手机号登录响应新增:
- `created`:本次登录是否创建新账号。
- `referral`:注册邀请码绑定结果;仅当本次提交了邀请码时返回。
## 验收标准
1. 未登录用户访问 `/?inviteCode=ABC123` 自动打开注册弹窗并预填 `ABC123`
2. 有效邀请码注册成功后,被邀请人获得光点奖励,邀请关系落库。
3. 无效邀请码注册成功但不绑定,并返回短提示。
4. 管理员可添加邀请码并写入 metadata重复提交同管理员同码更新 metadata。
5. 管理员邀请码被使用时不产生 `admin:*` 虚拟主体的钱包流水。

View File

@@ -0,0 +1,33 @@
# 新用户注册默认光点落地说明
## 目标
每个新注册用户默认获得 `10` 个光点。赠送必须由后端统一落账,前端只展示余额和流水,不在本地补发或推算。
## 落账规则
1. 默认赠送数量固定为 `10`,由 `module-runtime` 暴露常量,避免不同 crate 散落数字。
2. 钱包余额继续使用 `profile_dashboard_state.wallet_balance`,流水继续使用 `profile_wallet_ledger`
3. 新增流水来源 `new_user_registration_reward`,用于区分注册赠送、邀请奖励、充值和兑换码。
4. 注册赠送的流水 ID 固定为 `new-user-registration:{user_id}`SpacetimeDB procedure 内做幂等保护,重复调用不重复加钱。
5. 手机号注册、开发密码自动注册、微信新用户绑定手机号后激活,都必须调用同一个注册赠送入口。
6. 微信 callback 阶段只会生成待绑定手机号的临时账号,不在该阶段赠送;只有绑定新手机号并激活该微信账号时才赠送。若绑定到已有手机号账号,则不补发注册赠送。
7. 注册赠送和邀请码奖励可以叠加。先发注册赠送,再处理邀请码奖励时,新用户最终余额为 `10 + 邀请奖励`
## 存档同步约束
`runtime_snapshot.game_state.playerCurrency` 是运行态内的旧货币字段,不允许覆盖已经存在真实钱包业务流水的账户余额。只要用户已有非 `snapshot_sync` 的钱包流水,后续存档同步只能累计游玩时长和玩过世界,不再用 `playerCurrency` 回写 `wallet_balance`
这样可以避免新用户注册赠送的 `10` 个光点,在首次保存 `playerCurrency = 0` 的运行态快照时被覆盖成 `0`
## 影响文件
1. `server-rs/crates/module-runtime/src/lib.rs`
2. `server-rs/crates/spacetime-module/src/runtime/profile.rs`
3. `server-rs/crates/spacetime-client/src/runtime.rs`
4. `server-rs/crates/api-server/src/phone_auth.rs`
5. `server-rs/crates/api-server/src/password_entry.rs`
6. `server-rs/crates/api-server/src/wechat_auth.rs`
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,9 +2,9 @@
## 1. 目标
本轮在现有“我的”资料与钱包 projection 上新增兑换码能力。用户兑换成功后直接增加叙世币余额,写入 `profile_wallet_ledger`,并同步刷新 `profile_dashboard_state.wallet_balance`
本轮在现有“我的”资料与钱包 projection 上新增兑换码能力。用户兑换成功后直接增加光点余额,写入 `profile_wallet_ledger`,并同步刷新 `profile_dashboard_state.wallet_balance`
管理侧本轮只提供后端 API不新增管理后台页面。私有兑换码创建时支持内部 `userId` 与公开叙世号两类输入,后端创建阶段统一解析成内部 `userId` 存储。
管理侧本轮只提供后端 API不新增管理后台页面。私有兑换码创建时支持内部 `userId` 与公开百梦号两类输入,后端创建阶段统一解析成内部 `userId` 存储。
## 2. 兑换码类型
@@ -26,7 +26,7 @@
| --- | --- | --- |
| `code` | `String` | 主键,标准化后的兑换码。 |
| `mode` | `RuntimeProfileRedeemCodeMode` | 兑换码模式。 |
| `reward_points` | `u64` | 单次到账叙世币。 |
| `reward_points` | `u64` | 单次到账光点。 |
| `max_uses` | `u32` | 公共码为单用户上限,唯一码/私有码为全局上限。 |
| `global_used_count` | `u32` | 全局已使用次数。公共码也记录总使用次数,但不参与公共码上限判断。 |
| `enabled` | `bool` | 是否启用。 |
@@ -42,7 +42,7 @@
| `usage_id` | `String` | 主键,格式 `redeem:{code}:{user_id}:{micros}:{sequence}`。 |
| `code` | `String` | 兑换码。 |
| `user_id` | `String` | 兑换用户。 |
| `amount_granted` | `u64` | 到账叙世币。 |
| `amount_granted` | `u64` | 到账光点。 |
| `created_at` | `Timestamp` | 兑换时间。 |
索引:`code``user_id``(code, user_id)`
@@ -121,7 +121,7 @@
“我的”页头像右侧入口由 `会员充值` 改为 `兑换码`。点击打开独立模态窗口,窗口内只保留输入框、兑换按钮和后端返回提示,不展示兑换规则说明。
成功后展示 `已到账 X 叙世币`,并刷新 profile dashboard。失败后直接展示后端 `message`
成功后展示 `已到账 X 光点`,并刷新 profile dashboard。失败后直接展示后端 `message`
## 8. 测试矩阵

View File

@@ -2,7 +2,7 @@
## 1. 背景
当前前端展示的“叙世号”由前端基于 `AuthUser.id` 临时拼装:
当前前端展示的“百梦号”由前端基于 `AuthUser.id` 临时拼装:
- 前缀固定为 `SY-`
-`user.id``username` 去除非字母数字字符后的末 8 位
@@ -174,7 +174,7 @@
1. `id` 返回内部 ID 仅供当前工程内部跳转与资源读取使用,不在 UI 上直接暴露为文案
2. 不返回手机号、登录方式、绑定状态、tokenVersion 等敏感字段
3. 未命中返回 `404`
4. `by-id` 仅接受内部 `user_XXXXXXXX` 这类用户 ID用于工程内跳转、运营排查或已有资源引用不替代公开叙世号主搜索语义
4. `by-id` 仅接受内部 `user_XXXXXXXX` 这类用户 ID用于工程内跳转、运营排查或已有资源引用不替代公开百梦号主搜索语义
## 5.2 广场作品公开编号搜索
@@ -251,7 +251,7 @@
## 7.1 账号展示
当前首页资料卡和桌面顶部都展示前端拼装叙世号,改为:
当前首页资料卡和桌面顶部都展示前端拼装百梦号,改为:
1. 直接展示 `authUi.user.publicUserCode`
2. 复制按钮复制后端返回值
@@ -262,7 +262,7 @@
广场作品卡和详情页增加:
1. 作品号 `CW-XXXXXXXX`
2. 作者叙世`SY-XXXXXXXX`
2. 作者百梦`SY-XXXXXXXX`
展示要求:
@@ -284,7 +284,7 @@
用户搜索命中后的最小行为:
1. 打开独立用户搜索结果面板或对话框
2. 展示头像字母、显示名、叙世
2. 展示头像字母、显示名、百梦
3. 提供“查看该作者作品”入口
作品搜索命中后的行为:
@@ -325,7 +325,7 @@
## 11. 当前落地说明
1. 首页叙世号展示已优先读取后端 `publicUserCode`,原本基于 `AuthUser.id/username` 的前端拼装仅保留为兼容兜底,避免老会话未刷新时界面直接空白。
1. 首页百梦号展示已优先读取后端 `publicUserCode`,原本基于 `AuthUser.id/username` 的前端拼装仅保留为兼容兜底,避免老会话未刷新时界面直接空白。
2. 用户公开搜索与广场作品公开搜索均已改为调用后端匿名接口,前端只负责输入、展示与跳转,不再自行决定最终编号格式。
3. 自定义世界发布链路已改为从认证服务读取真实 `public_user_code` 写入作品真相与广场读模型,不再从内部 `user_id` 临时反推 `SY-XXXXXXXX`
4. 当前作品号 `public_work_code` 仍采用基于 `profile_id` 的稳定 fallback 方案生成 `CW-XXXXXXXX`;若后续补独立计数表,需要在不改变读写接口的前提下替换生成来源。

View File

@@ -16,7 +16,7 @@
## 作品分享路由补充
1. 公开作品入口路由统一使用当前作品页面路径加 `work=作品号`RPG 为 `/worlds/detail?work=CW-00000001`,拼图为 `/gallery/puzzle/detail?work=PZ-00000001`,大鱼玩法为 `/runtime/big-fish?work=BF-00000001`
1. 公开作品入口路由统一使用 `/works/detail?work=作品号`,三类作品在该页先展示统一公开详情,再由“启动”进入对应运行态;旧 `/worlds/detail``/gallery/puzzle/detail` 只保留给既有创作/编辑链路
2. 从公开广场、最近浏览、创作中心打开已发布作品详情或玩法时,若当前作品有公开作品号,地址栏必须同步追加 `work=作品号`;没有作品号的草稿详情仍保持无查询参数路径。
3. 首次进入主应用时若 URL 带 `work` 查询参数,平台入口自动复用现有公开编号搜索逻辑打开对应作品详情,不新增独立详情系统。
4. 详情页必须保留“复制作品号”和“分享作品”两个独立动作:
@@ -34,5 +34,5 @@
5. 桌面右侧趋势列表只显示排序和作品类型,不再显示 `1777110165.990127Z` 这类原始时间字符串,也不直接显示作品号。
6. 在内嵌浏览器 Clipboard API 拒绝写入时,详情页与创作中心作品号复制仍能通过降级路径完成,并显示 `已复制``复制失败`
7. 打开拼图详情后点击返回,不再固定跳到创作中心,而是回到打开详情前的平台 Tab。
8. 打开 `/?work=CW-00000001``/worlds/detail?work=CW-00000001``/gallery/puzzle/detail?work=PZ-00000001``/runtime/big-fish?work=BF-00000001` 后能自动进入对应公开作品详情或玩法。
8. 打开 `/?work=CW-00000001``/works/detail?work=CW-00000001` 后能自动进入对应公开作品详情;拼图、大鱼吃小鱼作品号同样进入统一公开详情后再启动玩法。
9. 点击详情页“分享作品”后,剪切板内容包含邀请文本、作品号和当前站点下带 `work=作品号` 的完整网址。

View File

@@ -201,7 +201,7 @@ Rust DTO 只承载对前端公开的 HTTP contract不直接泄露 `module-puz
1. 每次生成 2 张候选图。
2. 候选图通过 `api-server` 写入 OSS兼容展示路径统一为 `/generated-puzzle-assets/...`,禁止再落到仓库 `public/` 目录。
3. Axum 把候选图 URL、assetId、prompt snapshot 回写到 Spacetime session draft。
4. 创作者在结果页选择其中 1 张作为正式图。
4. 百梦主在结果页选择其中 1 张作为正式图。
这样可以保证:
@@ -211,7 +211,7 @@ Rust DTO 只承载对前端公开的 HTTP contract不直接泄露 `module-puz
### 6.1 发布前编辑真相补充
结果页允许创作者在发布前直接编辑:
结果页允许百梦主在发布前直接编辑:
1. `关卡名`
2. `摘要`
@@ -449,6 +449,8 @@ finalScore = tagSimilarityScore * 0.7 + sameAuthorScore * 0.3;
后续反馈要求合并块边界也要圆角后,外轮廓描边补充按四个角判断:只有相邻两条外露边同时存在的真实外轮廓角才应用圆角,内部拼接角保持直角且不显示分界线。
2026-05-01 追加修正:合并块的圆角不能继续依赖逐格 `border-radius` 叠加。运行时应根据合并组真实占据格提取一条整体 SVG 轮廓路径,外凸角和内凹角都通过同一条路径生成二次贝塞尔圆角;合并块图片层也必须用这条整体路径裁剪,避免 L 形、阶梯形凹口处仍露出直角图片像素。
### 12.4 第二关后打乱规则旁路修正
用户反馈“从第二关开始打乱规则像是完全相同”后,检查发现 `api-server` 的本地下一关 fallback 仍使用旧版 `build_local_puzzle_board` 固定左移一格,没有复用 `module-puzzle` 的种子化初始化规则。该路径会在图库/正式推荐不可用、由 API 临时构造下一关时触发。

View File

@@ -0,0 +1,98 @@
# 拼图失败续时与存档投影设计 2026-05-01
## 背景
拼图运行时已经有倒计时失败态、道具确认扣费、下一关推荐和个人存档页,但失败后的玩家选择与拼图作品存档投影还没有闭环:
1. 倒计时结束后只能返回,不能重新开始或付费继续。
2. 进入拼图作品后,存档页没有稳定出现一条可恢复的拼图游戏存档。
3. 每通过一关后,存档应该更新到下一关入口,而不是停留在旧关卡。
本轮只补齐拼图运行态与存档投影,不迁移旧 `server-node`,不新增平行存档页。
## 目标
1. 限定时间内未完成时弹出失败面板。
2. 失败面板提供两个选择:
- `重新开始`:重新开启当前拼图关卡,不扣光点。
- `继续1分钟`:先弹出确认窗口,确认后消耗 `1` 光点,并把当前失败关卡恢复为 `playing`,剩余时间固定为 `60000ms`
3. 进入拼图作品后立即写入 `profile_save_archive`,存档页显示拼图存档。
4. 每次进入下一关后更新同一条拼图存档,使存档恢复时指向最新可继续的关卡。
## 运行态规则
### 失败续时
`PuzzleRuntimePropKind` 增加 `extendTime`,沿用现有道具确认与扣费接口:
1. 前端只在 `runtimeStatus = failed` 时开放 `继续1分钟`
2. 点击后打开独立确认弹窗,文案只显示短标题和 `消耗 1 光点`
3. 正式 run 继续走 `POST /api/runtime/puzzle/runs/:runId/props`
4. `api-server``extendTime` 映射为账单 `asset_kind = puzzle_prop_extend_time`
5. SpacetimeDB 侧只允许失败关卡续时;续时成功后:
- `status = playing`
- `remaining_ms = 60000`
- `elapsed_ms = None`
- `cleared_at_ms = None`
- 清空暂停与冻结生效点
- 调整 `paused_accumulated_ms`,保证从确认成功那一刻开始完整倒计时 `60`
本地调试 run 没有真实钱包,沿用本地道具兜底:仍弹确认窗,但不扣真实光点。
### 重新开始
重新开始不复用旧失败棋盘,而是重新创建当前关卡的 run
1. 前端从当前 `currentLevel.profileId``currentLevel.levelId` 调用 `startPuzzleRun`
2. 新 run 的棋盘重新打乱、倒计时重置。
3. 如果当前关卡来自作品内部第 N 关,必须携带 `levelId`,避免重开误回作品第 1 关。
4. 旧失败 run 保留为历史运行记录,不在前端继续使用。
为支持第 3 点,`PuzzleRuntimeLevelSnapshot` 增加 `levelId: string | null`
## 存档投影规则
复用现有 `profile_save_archive` 表,不新增拼图专属存档表。拼图存档固定规则:
1. `world_key = puzzle:{entry_profile_id}`
2. `world_type = PUZZLE`
3. `profile_id = entry_profile_id`,保证同一个作品链只覆盖一条存档。
4. `world_name` 使用当前可恢复关卡名。
5. `subtitle` 使用 `第 N 关`
6. `summary_text` 使用可恢复关卡状态:
- playing`拼图进行中`
- failed`关卡失败`
- cleared`关卡已完成`
7. `cover_image_src` 使用可恢复关卡正式图。
8. `game_state_json` 保存最小拼图恢复载荷:
- `runtimeKind = "puzzle"`
- `runId`
- `entryProfileId`
- `currentProfileId`
- `currentLevelIndex`
- `currentLevelId`
- `status`
通关存档投影有一个额外规则:如果当前关卡已通关,并且 `refresh_next_level_handoff` 已经确认同作品存在下一关,则存档立即投影到同作品下一关入口,`status` 写为 `playing``subtitle / world_name / cover_image_src / currentLevelId` 都使用下一关。若当前作品没有下一关、只存在相似作品候选,存档保持当前已通关关卡,等待玩家在结算弹窗里选择相似作品,不能提前替玩家切换到某个候选作品。
## 写入时机
SpacetimeDB 拼图运行态每次持久化 run 时同步刷新存档:
1. `start_puzzle_run`:创建 run 后立即写入拼图存档。
2. `advance_puzzle_next_level`:进入下一关后更新同一条存档。
3. `use_puzzle_runtime_prop(extendTime)`:续时成功后更新状态。
4. `get_puzzle_run` 导致失败态落库时,也同步更新为失败存档。
5. `submit_puzzle_leaderboard_entry`:正式 run 提交成绩并把当前关标记为已通关时,先刷新下一关 handoff再按上面的通关投影规则同步存档。
前端在 `startPuzzleRun / usePuzzleProp / submitPuzzleLeaderboard / advancePuzzleLevel / getPuzzleRun` 成功后主动刷新存档列表,避免存档页停留在进入作品前或上一关的旧投影。
## 验收
1. 倒计时归零后失败弹窗有 `重新开始``继续1分钟`
2. 点击 `继续1分钟` 后先出现扣费确认,确认成功后失败弹窗关闭并恢复 `60` 秒倒计时。
3. 光点余额不足时确认弹窗保留,并展示错误。
4. 点击 `重新开始` 后当前关卡重新打乱并重置倒计时。
5. 进入拼图作品后,存档页出现 `worldType = PUZZLE` 的拼图存档。
6. 通过一关后,只要后端确认同作品下一关存在,同一条存档立即更新到新关卡;没有同作品下一关时保留已完成关卡,等待玩家选择相似作品。
7. 定向前端测试、Rust 拼图模块测试与编码检查通过。

View File

@@ -0,0 +1,109 @@
# 拼图填表式创作流程改造 2026-04-29
## 背景
拼图创作入口不再使用 Agent 对话收集题材锚点。新流程让玩家填写作品名称、作品描述、画面描述三类信息,其中画面描述只服务首关画面生成与关卡画面语义,不再作为作品详情页的作品描述。画面描述支持上传参考图。玩家确认后直接进入草稿生成进度页,后续草稿生成、首图生成、正式图选择、结果页编辑和发布沿用现有后端编排。
## 入口表单
### 2026-04-30 初始表单草稿保存补充
1. 玩家在创作页点击“拼图”入口时,前端必须立即创建一个新的拼图 Agent session并同步生成一条 `publicationStatus = draft` 的拼图作品卡;此时不触发 `compile_puzzle_draft`,不生成图片,不进入生成进度页。
2. 新 session 的 `seedText` 允许为空SpacetimeDB 侧用空锚点和空表单草稿初始化,不得把默认题材文案写入玩家草稿字段。
3. 初始表单输入自动保存到 session 的 `draft_json``puzzle_work_profile` 投影。保存字段只包含 `workTitle``workDescription``pictureDescription`、可推断标签和一个 `generationStatus = idle` 的默认关卡草稿设置阶段默认关卡名称必须为空不得写入“第一关”“第1关”或作品名称作为默认值。参考图只保存在当前前端会话内不落入 SpacetimeDB。
4. 玩家在生成草稿前退出,再次从创作中心点击这条拼图草稿时,必须恢复到填表页,并回填之前自动保存的作品名称、作品描述和画面描述;只有执行 `compile_puzzle_draft` 且生成结果页草稿后,草稿入口才进入结果页。
5. 表单自动保存走 `save_puzzle_form_draft` action不消耗光点不生成图片不改变 `stage = collecting_anchors`;生成草稿按钮仍单独触发 `compile_puzzle_draft` 并进入进度页。
6. 点击拼图入口始终创建新草稿,不复用上一次未完成 session恢复旧草稿只通过“我的创作”中的草稿卡进入。
7. 若 Maincloud 仍运行旧 wasm缺少 `save_puzzle_form_draft` procedure前端提交生成或生成失败页重试时不得继续复用空 `seedText` 的表单 session必须用当前表单 payload 新建带真实 seed 的 session 再执行 `compile_puzzle_draft`
8. api-server 也要兼容旧 wasm`save_puzzle_form_draft` 缺失时,自动保存 action 降级返回当前 session`compile_puzzle_draft` 前置保存缺失且当前 session 为空 seed 时,创建一条带表单 seed 的替代 session 后继续编译,避免再次暴露 `No such procedure`
9. 正式修复仍是发布最新 SpacetimeDB wasm。当前 Maincloud `xushi-p4wfr` 的迁移操作员表为空,但旧库引导密钥来自旧 wasm本次临时生成的新引导密钥无法授权导出迁移需使用已有迁移操作员 token 或数据库 owner 重新授权后发布;禁止为绕过冲突直接清库,除非明确接受数据丢失。
1. 作品名称为必填字段,保存到 `workTitle`,兼容写入旧 `seedText`,同时作为作品级 `workTitle` 的真相源。
2. 作品描述为必填字段,保存到 `workDescription`,作为作品详情页、作品列表和发布资料中的 `summary` 真相源。
3. 画面描述为必填字段,保存到 `pictureDescription`,只作为首关画面生成 prompt、首关 `pictureDescription` 和关卡命名输入,不再覆盖作品描述。
4. 参考图为可选字段,保存到 `referenceImageSrc`。表单支持本地图片上传为 Data URL草稿首图生成时直接传入现有拼图图生图接口。
5. 表单确认后前端先创建拼图 session再立即执行 `compile_puzzle_draft`,并传入 `promptText = pictureDescription``referenceImageSrc`
6. 表单提交 payload 需要在前端创作流程中暂存,生成进度页失败重试时必须继续携带同一份作品名称、作品描述、画面描述与参考图。
7. 生成进度页的“当前拼图信息”必须优先读取这份表单 payload而不是读取 session 中旧 Agent 锚点或编译后的关卡名,避免用户确认后看到的标题、作品描述、画面描述发生漂移。
8. `compile_puzzle_draft` action 必须显式携带 `workTitle``workDescription``pictureDescription``promptText``referenceImageSrc``seedText` 只作为 SpacetimeDB 旧表结构兼容载体,不能成为前端生成页展示和失败重试的唯一来源。
9. 入口不再展示拼图 Agent 聊天气泡、快捷补齐或多锚点卡片;新建拼图时必须清空旧 session只有从当前生成进度页返回表单时保留本轮内容。
## 锚点映射
拼图模式锚点收口为三个玩家输入源:
| 新字段 | 落地字段 | 说明 |
| --- | --- | --- |
| 作品名称 | `themePromise.value``workTitle`、旧 `levelName` 兼容字段、`creatorIntent.themePromise` | 作为作品名称与题材承诺真相源 |
| 作品描述 | `workDescription`、旧 `summary` 兼容字段 | 作为作品详情页描述、列表描述和发布描述真相源 |
| 画面描述 | `visualSubject.value``levels[0].pictureDescription`、首图 `promptText` | 作为首关画面主体、首图生成 prompt 和首关关卡命名输入 |
兼容旧结构时仍保留 `visualMood``compositionHooks``tagsAndForbidden` 字段,但它们不再由 Agent 问答收集:
1. `visualMood` 固定标记为系统推断,值为“清晰、适合拼图切块”。
2. `compositionHooks` 固定标记为系统推断,值为“主体轮廓、色块分区、局部细节”。
3. `tagsAndForbidden` 根据拼图标题和画面描述生成 3 到 6 个题材标签;禁忌只保留通用图像约束,不写入 UI。
生成进度页的“当前拼图信息”只展示玩家输入锚点:作品名称、作品描述、画面描述。题材标签仅作为草稿结果页内容展示,不在进度页混入旧五锚点结构。
## 草稿数据结构
拼图草稿从单关卡字段升级为作品级信息与关卡列表并存:
1. `PuzzleResultDraft.workTitle`:作品名称,旧 `levelName` 只作为兼容字段同步为当前主关卡名称或作品名称。
2. `PuzzleResultDraft.workDescription`:作品描述,旧 `summary` 只作为兼容字段同步为作品描述。
3. `PuzzleResultDraft.themeTags`:作品标签,仍限制 3 到 6 个。
4. `PuzzleResultDraft.levels[]`:关卡列表。每个关卡包含 `levelId``levelName``pictureDescription``candidates``selectedCandidateId``coverImageSrc``coverAssetId``generationStatus`
5. 首次草稿生成时必须创建一个默认关卡,`levelId = puzzle-level-1``pictureDescription = 表单画面描述`,草稿设置阶段 `levelName` 为空;首图生成后可由后端根据画面描述和图片语义生成关卡名称并写入该关卡。
6. 关卡名称由后端基于画面描述和图片语义输入生成;无可用语义时按题材标签与序号兜底,禁止继续直接使用作品名称作为关卡名称。
7. 旧草稿或旧作品缺少 `levels` 时,读取层必须由旧 `levelName``summary``coverImageSrc``candidates` 补出一个兼容关卡,避免历史草稿无法打开。
## 后端编译
1. `CreatePuzzleAgentSessionRequest` 新增 `workTitle``workDescription``pictureDescription``referenceImageSrc`,但不改 SpacetimeDB 表结构。
2. api-server 创建 session 时把作品名称、作品描述和画面描述合成 `seedText` 传入 SpacetimeDBSpacetimeDB reducer 只做确定性锚点生成,不接触图片或外部服务。
3. `compile_puzzle_draft_with_initial_cover` 新增首图 prompt 和参考图参数。若前端传入画面描述,则首图生成直接使用这段文本;若传入参考图,则走现有 DashScope 图生图链路;生成结果写入默认第一关。
4. 首图文生图 prompt 由 api-server 拼接固定拼图约束后统一压缩到 `500` 字符以内,避免玩家长画面描述触发 DashScope 参数非法;进度页和结果页仍展示玩家原始画面描述,不展示压缩后的内部 prompt。
5. 图片生成仍在 api-server 内完成,遵守 SpacetimeDB reducer 不做网络 I/O 的约束。
6. 参考图以 Data URL 进入 `POST /api/runtime/puzzle/agent/sessions``POST /api/runtime/puzzle/agent/sessions/{sessionId}/actions`,这两条路由必须单独放宽 JSON body 上限;不要放大全局默认 body limit。
7. 前端仍应优先压缩参考图;后端 body 上限只用于容纳合理尺寸的单张参考图,超大原图不应直接落入 SpacetimeDB 或作为作品字段持久化。
8. 作品更新接口 `PUT /api/runtime/puzzle/works/{profileId}` 必须支持作品信息和关卡列表一起写入,前端自动保存不得只写旧单关字段。
9. `StartPuzzleRunRequest` 新增可选 `levelId`。详情页或草稿结果页单独体验某关时传入目标关卡,后端从作品/草稿的 `levels` 中选取该关卡生成运行态。
10. `ExecutePuzzleAgentActionRequest` 必须保留 `pictureDescription` 字段。表单直达生成时,`compile_puzzle_draft` 优先用 `pictureDescription` 作为首图 prompt再回退到旧 `promptText`;避免生成页展示的是玩家画面描述,但后端实际用作品名称或旧摘要出图。
11. `compile_puzzle_draft` 中的图片上游失败不得映射成 `400 BAD_REQUEST`。DashScope 返回 `InvalidParameter` 或任务失败时api-server 统一按 `502 UPSTREAM_ERROR` 暴露,并在 `details.message` 中保留“拼图图片生成失败:...”的业务原因,避免生成页只显示“请求参数不合法”。
12. `compile_puzzle_draft` 前置光点预扣失败不得映射成 `400 BAD_REQUEST`。余额不足返回 `409 CONFLICT`SpacetimeDB procedure 不可用、绑定不匹配、钱包服务异常等统一按 `502 UPSTREAM_ERROR` 暴露,并在 `details.message` 中保留真实钱包错误。
13. 生成拼图作品草稿动作涉及的表单 seed prompt 与首图 prompt 来源选择统一收口在 `server-rs/crates/api-server/src/prompt/puzzle/draft.rs``puzzle.rs` 只负责调用 SpacetimeDB、计费、图片服务和持久化不再直接拼草稿 prompt 文本。
## 结果页
拼图草稿结果页分为两个 Tab
1. 拼图关卡列表:默认展示草稿生成出的第一关。列表项参考 RPG 草稿卡片样式,显示画面图、关卡名称和轻量状态。支持新增关卡、删除关卡。点击列表项进入独立关卡详情页,不在列表项下方展开。关卡详情页可编辑关卡名称、画面描述、生成或重新生成画面,并在已有正式图后支持关卡测试。
2. 作品信息:展示并编辑作品名称、作品描述、作品标签。
### 2026-04-30 关卡列表卡片交互补充
1. 关卡列表卡片的删除按钮与关卡名称放在同一信息行,按钮固定在卡片右下角;不得再单独占用一整条底部分隔栏。
2. 关卡图片、序号与名称区域仍作为打开关卡详情的主点击区;删除按钮只触发删除,不进入详情。
### 2026-04-30 关卡详情面板交互补充
1. 关卡详情面板内容区按移动端优先的单列顺序展示:`关卡名称 -> 画面图 -> 画面描述`。其中画面图只在该关卡已有正式图时出现;新建关卡或画面为空的关卡不展示空图占位模块。
2. 画面生成主按钮固定吸底,始终位于关卡详情面板底部操作区。若当前关卡还没有正式图,按钮文案为“生成画面”;已有正式图后,按钮文案为“重新生成画面”。
3. 关卡已有正式图后,底部操作区在生成按钮上方新增单独的关卡测试入口,原“体验该关”文案收口为“关卡测试”。无正式图时不展示该入口。
4. 底部吸底操作区只承载动作按钮,不默认写玩法说明或规则解释,避免压缩移动端编辑空间。
5. 关卡详情面板内触发生成画面时,前端必须把当前编辑态完整 `levelsJson``generate_puzzle_images` action 一起提交。这样新建关卡在自动保存完成前立即生成,也能由后端写回目标关卡。
6. api-server 处理 `generate_puzzle_images` 时,若 action 带有 `levelsJson`,必须用这份关卡快照覆盖本次生成的草稿关卡视图后再定位 `levelId`。若请求明确传入 `levelId` 但关卡列表中不存在该关卡,必须返回错误,不得静默回退第一关。
7. 历史拼图素材入口只在已有正式图的 `画面图` 区域右下角展示,不再放在 `画面描述` 输入区;本地上传参考图入口仍保留在画面描述输入区右下角。
8. 历史拼图素材列表必须由服务端按当前登录账号过滤,只返回 `asset_kind = puzzle_cover_image``owner_user_id = 当前账号` 的资产;不得依赖前端过滤,也不得展示其他账号素材。
画面描述区域不再展示候选图实际 prompt 或“请生成一张适合……”之类内部提示词模块。参考图入口保留在画面描述编辑区域内,便于重新生成时继续带入。结果页编辑关卡画面描述时只同步该关卡 `pictureDescription`;作品描述只在作品信息 Tab 编辑,作品详情页不得再回退使用画面描述。
## 验收
1. 从拼图创作入口只能看到作品名称、作品描述、画面描述和参考图上传,不出现 Agent 聊天输入、补齐设定、锚点问答。
2. 点击确认后进入拼图草稿生成进度页,并自动完成草稿编译、首图生成、正式图选择。
3. 首图生成请求使用玩家画面描述作为 prompt上传参考图时走图生图作品详情页展示玩家作品描述。
4. 结果页包含“拼图关卡”和“作品信息”两个 Tab关卡列表默认至少一关支持新增、删除和进入关卡详情。
5. 关卡详情页支持生成或重新生成画面;已有正式图后显示吸底“关卡测试”入口。
6. 发布、作品测试、自动保存作品名称、作品描述、作品标签和关卡列表仍可用。

View File

@@ -0,0 +1,57 @@
# 拼图图片与前端规则裁决对齐 2026-04-29
## 背景
本轮明确调整拼图运行态边界:
1. 拼图生成图片重新回到 `1:1` 正方形。
2. 拼图中的拖动、交换、合并、拆分与通关判定由前端即时计算。
3. 移动端运行时棋盘需要贴近屏幕两侧边缘,减少无效留白。
此前误按 `9:16` 竖屏统一图片和棋盘,会让拼图块在移动端可操作面积不足,也和拼图素材的切块体验不匹配。本轮回到正方形棋盘与正方形生图。
## 落地结论
### 1. 图片生成
1. 拼图生成图固定使用 `1024*1024`
2. 文生图和参考图生图共用同一个尺寸常量,禁止一条链路仍生成竖屏或横版图。
3. 拼图图片提示词明确写入 `1:1 正方形画布`,继续保留适配 `3x3 / 4x4 / 5x5 / 6x6 / 7x7` 拼图切块、主体清晰、层次明确、无文字水印等约束。
4. 文生图正向 prompt 必须由后端压缩到 `500` 字符以内,优先保留玩家画面描述开头与固定拼图约束,避免 DashScope 旧 text2image 协议把超长 prompt 判为“请求参数不合法”。
5. DashScope 上游失败时api-server 必须在错误 details 中保留业务 message、`upstreamStatus` 和截断后的 `rawExcerpt`,日志也要记录同样的摘要,避免生成进度页只能看到通用 HTTP 文案。
6. 图片生成仍由 `api-server` 执行。SpacetimeDB reducer 不做网络 I/O。
7. 拼图文生图请求体按 DashScope Wan text2image 协议收口:`input``prompt` 与非空 `negative_prompt``parameters``n``size``prompt_extend``watermark`。不要在 `input``parameters` 里重复写入反向提示词,否则上游容易返回参数非法。
8. 光点预扣失败属于钱包或 SpacetimeDB 服务链路错误,不得映射成 `400 BAD_REQUEST`。除余额不足返回 `409 CONFLICT` 外,其余预扣异常统一按上游/服务错误暴露,避免生成页误提示“请求参数不合法”。
### 2. 前端规则裁决
1. 运行态的交换、拖动、合并、拆分、通关判定由前端基于 `PuzzleRunSnapshot` 即时计算。
2. 正式 run 与本地测试 run 复用同一套前端规则函数,避免正式链路和测试链路玩法漂移。
3. 后端仍负责开始 run、进入下一关、道具扣费、暂停计时同步、排行榜提交、作品与下一关候选读取。
4. 正式 run 的 `/drag` 后端 HTTP 接口已撤出 Rust API拖动不再有后端入口。`/swap` 暂作点击交换兼容入口,拖动中的交换由前端本地规则完成。
### 3. 移动端棋盘布局
1. 运行时棋盘根容器恢复 `aspect-square`
2. 移动端横向 padding 收紧到 `0.25rem`,棋盘宽度使用 `min(99vw, 可用高度)`,尽量贴近屏幕两侧边缘。
3. 单格不设置固定最小高度,避免移动端被单格撑破。
4. 顶部 HUD 与底部道具仍保留安全区,不能遮挡棋盘可操作区域。
### 4. 拼块视觉圆角
1. 基础单块和合并块都必须使用圆角,不能只让合并后的外轮廓有圆角。
2. 基础单块的图片层必须跟随单块容器裁剪,避免图片直角从圆角边框里露出。
3. 合并块继续按实际拼块外轮廓描边,内部相邻边不额外显示边框。
4. 合并块轮廓必须按真实占据格生成整体路径,再统一处理外凸角与内凹角圆角;不能只依赖单个格子的 `border-radius` 拼接,否则 L 形、阶梯形合并块的凹入角会露出直角。
5. 合并块图片层必须使用同一条整体轮廓做裁剪,确保凹入角的图片像素也跟随圆角被裁掉。
## 验收
1. 点击拼图草稿生成或重新生成画面时,后端请求 DashScope 的 `size``1024*1024`
2. 图片提示词包含 `1:1 正方形拼图关卡`
3. 图片提示词长度不超过 `500` 字符,超长画面描述会被截断,但适配 `3x3 / 4x4 / 5x5 / 6x6 / 7x7` 拼图切块、`避免文字、水印、边框和 UI 元素` 等玩法约束不能丢。
4. DashScope 返回参数错误、任务失败或非 2xx 时,前端错误优先展示后端 details.message后端日志能看到 `upstreamStatus``rawExcerpt`
5. 正式拼图 run 中拖动拼块后,前端立即更新棋盘、合并块和通关状态,不再等待 `/drag`
6. 移动端运行时棋盘为正方形,并尽量贴近屏幕两侧边缘。
7. 基础单块和合并块都能看到圆角,合并块的外凸角与内凹角都不是直角,且图片不会溢出圆角裁剪。
8. 下一关、道具、排行榜仍走现有后端链路,不把外部 I/O 或扣费逻辑塞回前端。

View File

@@ -6,7 +6,7 @@
## 本轮落地边界
1. 拼图图片提示词统一放到 `server-rs/crates/api-server/src/prompt/puzzle_image.rs`
1. 拼图图片提示词统一放到 `server-rs/crates/api-server/src/prompt/puzzle/image.rs`
2. `puzzle.rs` 只负责读取提示词构建结果,并继续处理 DashScope、OSS、SpacetimeDB 写回。
3. 提示词模块只暴露:
- `build_puzzle_image_prompt(level_name, prompt)`
@@ -18,7 +18,7 @@
1. 不把图片生成逻辑下沉到 SpacetimeDB reducer外部 I/O 必须留在 `api-server`
2. 不改候选图 JSON 持久化结构,仍使用 `module-puzzle::PuzzleGeneratedImageCandidate` 对应的 snake_case 字段。
3. 不改前端 UI 文案和交互;本轮只拆后端提示词脚本。
4. 后续若调整拼图图片风格、尺寸、禁止元素或切块可读性要求,优先修改 `prompt/puzzle_image.rs`,再按需补测试。
4. 后续若调整拼图图片风格、尺寸、禁止元素或切块可读性要求,优先修改 `prompt/puzzle/image.rs`,再按需补测试。
## 验收

View File

@@ -0,0 +1,37 @@
# 拼图排行榜前端关卡提交与 RPG 敬请期待 2026-04-30
## 背景
拼图运行态的交换、拖动、合并、拆分与本关通关判定已经交给前端即时计算。第二关通过后端 `local-next-level` 兼容接口生成下一关时,前端当前关卡已经推进到新作品,但 SpacetimeDB 中的旧 run 快照可能仍停留在上一关。
因此第二关通关后提交排行榜,如果后端继续要求 `run.currentLevel.profileId == payload.profileId`,会误报:
```text
提交成绩的拼图作品与当前关卡不匹配
```
本轮同时把 RPG 创作入口设置为“敬请期待”,只调整平台入口展示与分流防线,不删除 RPG 既有代码、历史作品详情或运行时兼容能力。
## 落地口径
### 1. 拼图排行榜
1. 排行榜提交仍必须校验 run 归属,避免跨用户提交。
2. 排行榜写入以本次提交的 `profileId``gridSize``elapsedMs` 为准。
3. 当 SpacetimeDB 旧 run 的当前关卡与提交关卡一致时,后端可以把真实榜单合并回服务端关卡快照。
4. 当旧 run 当前关卡与提交关卡不一致时,不再报错;后端只写入真实榜单,并把榜单放到 run 顶层 `leaderboardEntries` 返回给前端。
5. 前端继续用当前本地 run 合并后端返回的 `leaderboardEntries`,不能用服务端旧棋盘覆盖本地第二关棋盘。
### 2. RPG 敬请期待
1. 平台创作类型元数据中,`rpg` 改为 `locked: true`
2. RPG 创作卡片 badge 与副标题统一显示 `敬请期待`
3. 创作类型弹窗与创作首页卡带复用同一份元数据,因此入口自动禁用。
4. 分流函数中继续防御 `rpg` 类型直达触发,避免旧测试或隐藏入口绕过禁用态。
## 验收
1. 第二关拼图通关后提交排行榜,不再出现“提交成绩的拼图作品与当前关卡不匹配”。
2. 排行榜返回后,前端仍保留当前第二关棋盘与通关状态。
3. 创作页 RPG 卡片显示 `敬请期待` 且不可点击。
4. 拼图、大鱼和其它已锁定玩法的显示状态不被本轮改动影响。

View File

@@ -0,0 +1,75 @@
# 拼图下一关与相似作品接续设计 2026-04-30
## 背景
拼图通关结算弹窗已有“下一关”按钮,但当前按钮依赖 `recommendedNextProfileId`。这会带来两个问题:
1. 当前作品还有未玩的内部关卡时,按钮可能因为没有跨作品推荐而被禁用。
2. 当前作品全部关卡玩完后,只返回单个推荐作品,无法满足“三个相似作品由用户选择”的交互。
本轮只修复拼图运行态接续链路,不迁移旧 `server-node`,不在前端计算正式相似度。
## 目标
1. 通关后默认点击“下一关”,优先加载当前拼图作品的下一关。
2. 当前作品没有下一关时,后端按标签语义相似度选出相似度最高的三个已发布作品。
3. 用户在通关弹窗里点击候选作品后,进入该作品并从第 `1` 关重新开始。
4. 移动端优先,候选卡片要紧凑,不写玩法说明类文案。
## 数据契约
`PuzzleRunSnapshot` 增加:
1. `nextLevelMode: "sameWork" | "similarWorks" | "none"`
2. `nextLevelProfileId: string | null`:同作品下一关或跨作品推荐的默认目标。
3. `nextLevelId: string | null`:同作品下一关的 `levelId`;跨作品时为 `null`
4. `recommendedNextWorks: PuzzleRecommendedNextWork[]`:跨作品候选,最多 3 个。
`PuzzleRecommendedNextWork` 字段:
1. `profileId`
2. `levelName`
3. `authorDisplayName`
4. `themeTags`
5. `coverImageSrc`
6. `similarityScore`
保留 `recommendedNextProfileId` 作为旧字段兼容,但前端新逻辑不再只依赖它。
## 后端规则
1. SpacetimeDB 侧在 `start / get / swap / drag / leaderboard / advance` 后刷新下一关状态。
2. 当前作品存在未玩的下一张关卡图时:
- `nextLevelMode = "sameWork"`
- `nextLevelProfileId = 当前作品 profileId`
- `nextLevelId = 下一关 levelId`
- `recommendedNextWorks = []`
3. 当前作品没有内部下一关时:
- 使用拼图现有 `recommendation_score = tagSimilarity * 0.7 + sameAuthor * 0.3`
- `tagSimilarity` 优先复用 RPG/build 标签语义亲和度模型;两侧标签未命中该语义模型时,回退到规范化标签 Jaccard
- 排除当前 run 已玩过的作品;若池子为空,允许回收但不连续重复上一关作品
- 返回最高的 3 个候选
4. `advance_puzzle_next_level`
- `nextLevelMode = sameWork` 时加载当前作品的下一关,并继续当前 run。
- `nextLevelMode = similarWorks` 时默认加载候选第一项,并把 `entryProfileId / clearedLevelCount / currentLevelIndex` 重置到目标作品第 `1` 关。
5. `local-next-level` 兼容接口同样优先找同作品下一关;没有时返回 `similarWorks` 候选并保持当前通关 run只有候选池为空时才进入旧草稿兜底。
## 前端规则
1. 结算弹窗:
- `sameWork`:主按钮显示“下一关”,直接触发默认推进。
- `similarWorks`:展示最多 3 个作品候选卡;用户点击卡片进入候选作品。
- `none`:禁用下一关入口。
2. 底部通关后入口:
- `sameWork` 保留“下一关”。
- `similarWorks` 显示“换个作品”,点击后打开结算弹窗供选择。
3. 所有正式相似度计算只信任后端返回,不在 UI 里重新算。
4. 本地/草稿 run 通关提交本地排行榜后,会异步调用 `local-next-level` 刷新 handoff若拿到 `similarWorks`,只合并候选字段,不把已通关弹窗改成新的 playing 关卡。
## 验收
1. 当前作品有下一关时,点击“下一关”进入当前作品下一关。
2. 当前作品没有下一关时,通关弹窗显示最多 3 个相似作品。
3. 点击相似作品后进入该作品第 `1`HUD 关卡序号、切割规格和倒计时都按第 `1` 关显示。
4.`recommendedNextProfileId` 为空时,只要 `nextLevelMode = sameWork`,按钮仍可用。
5. 拼图 runtime 单测、Rust 拼图模块测试和编码检查通过。

View File

@@ -4,7 +4,7 @@
拼图结果页此前存在两个串联问题:
1. 创作者在结果页修改 `关卡名`、新增标签、删除标签,只会改前端本地 `editState`,不会立即写回拼图作品 profile。
1. 百梦主在结果页修改 `关卡名`、新增标签、删除标签,只会改前端本地 `editState`,不会立即写回拼图作品 profile。
2. 发布弹窗同时混用了旧 session 内的 `publishReady` 与前端本地编辑态,导致标签已经在界面里补够,但发布校验仍然盯着旧草稿里的标签数量,用户无法通过发布检验。
这会直接破坏拼图创作主链的可用性:用户明明已经在结果页补齐正式标签,却因为没有自动保存、也没有按当前编辑态重算门槛而卡在发布前。

View File

@@ -41,6 +41,15 @@
2. 单块交换、拖到合并块后拆分、合并块整体重排,继续沿用当前本地运行态规则。
3. 不新增前端本地裁决,不把玩法真相从既有运行态实现中分叉出去。
### 3.4 点击触觉反馈
移动端用户每次按下可交互拼图片时,需要触发一次短促手机震动:
1. 震动触发点放在 `pointerdown`,让点击选中、按住准备拖动与拖起都有一致手感。
2. 同一次按下会话只触发一次震动,后续连续移动不重复震动。
3. 使用浏览器标准 `navigator.vibrate([12])`,不支持震动能力的设备静默跳过。
4. 该反馈只属于前端表现层,不影响拖拽落点、交换、合并、拆分与通关判定。
## 4. 验收标准
1. 单块拖动时拼块视觉位置应紧跟手指或鼠标,不再出现明显缓动拖尾。
@@ -48,3 +57,4 @@
3. 点击选中与拖动阈值判定仍保持原语义,不因为优化误触发交换。
4. 运行时现有结算弹窗、排行榜和下一关入口不受影响。
5. 定向测试覆盖拖动提交坐标的行为,并运行编码检查确保中文文档未被写坏。
6. 移动端点击拼图片时立即触发一次短震,同一次按下后的连续移动不重复触发。

View File

@@ -103,6 +103,15 @@
3. 结算弹窗显示时,如果真实榜单尚未回写完成,可以显示加载态;但不能回退到假数据。
4. 下一关开始后,当前关卡榜单状态清空。
## 7.1 2026-04-29 与前端拖动裁决的对齐
当前拼图拖动、合并、拆分与通关判定完全由前端运行态负责,后端排行榜接口只负责真实成绩表与榜单聚合:
1. 排行榜提交不得依赖 SpacetimeDB 里的旧棋盘快照已经通过后端拖动接口进入 `cleared`
2. 后端仍校验 `profileId``gridSize`、昵称和成绩,并把当前提交写入真实成绩表。
3. 后端响应里的 `leaderboardEntries` 是唯一需要合并回前端当前 run 的数据。
4. 前端不能用排行榜响应里的旧棋盘快照覆盖本地拖动后的棋盘,否则会把刚刚通关的前端状态回滚。
## 8. 测试要求
至少覆盖:

View File

@@ -0,0 +1,45 @@
# 拼图运行态 `run_json` 计时字段兼容修复 2026-04-29
## 背景
作品详情页点击“启动”时Rust API 通过 SpacetimeDB `start_puzzle_run` procedure 拿到字符串化的 `run_json`,再在 `spacetime-client` 映射层反序列化为拼图运行态快照。
本次线上报错为:
```text
puzzle run run_json 非法: missing field `started_at_ms`
```
说明主云 procedure 已成功返回快照,但返回的 JSON 仍可能是旧字段集,没有带上后续限时与排行榜迭代新增的计时字段。
## 根因
`PuzzleRuntimeLevelSnapshot` 在早期 PRD 中只包含关卡基础信息、棋盘和状态。后续版本新增:
1. `started_at_ms`
2. `cleared_at_ms`
3. `elapsed_ms`
4. `time_limit_ms`
5. `remaining_ms`
6. `paused_accumulated_ms`
7. `pause_started_at_ms`
8. `freeze_accumulated_ms`
9. `freeze_started_at_ms`
10. `freeze_until_ms`
11. `leaderboard_entries`
其中部分字段已经有 `serde(default)`,但 `started_at_ms``cleared_at_ms``elapsed_ms``leaderboard_entries` 仍按必填字段解析。只要主云旧模块或历史快照缺少这些字段API facade 就会在映射层失败,导致详情页启动中断。
## 修复口径
本次只做后端兼容,不改表结构,不改前端表现:
1. `module-puzzle` 的运行态快照新增字段统一允许缺省。
2. 旧 JSON 缺 `started_at_ms` 时,用当前毫秒时间作为兼容起点,保证前端倒计时不会从 `0` 时间戳开始。
3. 旧棋盘缺 `all_tiles_resolved` 时按 `false` 处理。
4. 旧 run / level 缺 `leaderboard_entries` 时按空榜单处理。
5. `spacetime-client` 增加回归测试,确保 `run_json` 缺新增计时字段仍能启动。
## 经验结论
`procedure -> run_json/items_json -> client record` 这类链路只要返回字符串化聚合快照,新增字段就必须默认具备向后兼容能力。平台入口级操作不应因为单个新增字段缺失直接 500能安全补默认值的字段应在服务端契约层统一兜底。

View File

@@ -0,0 +1,127 @@
# 拼图运行时限时与道具系统设计 2026-04-29
## 背景
拼图运行时从纯粹的无压解谜升级为限时关卡,需要同时补齐三类体验:
1. 不同难度有明确倒计时,超时即失败。
2. 底部固定 3 个轻量道具:提示、查看原图、冻结时间。
3. 道具使用必须经过确认弹窗并消耗 `1` 光点,确认弹窗期间暂停关卡计时。
本设计只处理拼图运行时,不改拼图创作链、发布链和广场推荐链。
## 运行态字段
`PuzzleRuntimeLevelSnapshot` 增加以下字段:
1. `timeLimitMs`:当前关卡限时。
2. `remainingMs`:后端或本地运行态计算出的剩余时间。
3. `pausedAccumulatedMs`:已累计暂停时长。
4. `pauseStartedAtMs`:当前是否处于暂停中;有值表示暂停开始时间。
5. `freezeUntilMs`:冻结时间道具生效截止时间;冻结期间倒计时不减少。
`status` 增加 `failed`。当 `remainingMs <= 0` 且关卡尚未通关时,状态进入 `failed`,后续交换、拖动、排行榜提交都拒绝。
## 难度限时
拼图关卡切割规格和倒计时由统一关卡配置函数解析,不再按网格规模单独推导时间:
| 关卡 | 切割规格 | 限时 |
| -------- | -------- | ---------- |
| 第 1 关 | `3x3` | `300000ms` |
| 第 2 关 | `4x4` | `300000ms` |
| 第 3 关 | `5x5` | `300000ms` |
| 第 4 关 | `5x5` | `210000ms` |
| 第 5 关 | `5x5` | `210000ms` |
| 第 6 关 | `6x6` | `240000ms` |
| 第 7 关 | `5x5` | `210000ms` |
| 第 8 关 | `7x7` | `270000ms` |
| 第 9 关 | `5x5` | `240000ms` |
| 第 10 关 | `7x7` | `270000ms` |
第 11 关开始,每 6 关循环复用第 5 关到第 10 关的配置,即 `5x5/210000ms``6x6/240000ms``5x5/210000ms``7x7/270000ms``5x5/240000ms``7x7/270000ms`
同作品下一关必须使用同一个运行时关卡序号继续推进。跨作品相似推荐代表进入新作品,必须从目标作品第 `1` 关重新开始。
失败状态点击“重新开始”时,不进入作品第 `1` 关,而是重开当前失败关卡:前端需要传当前关 `levelId`,服务端按该 `levelId` 在作品内的位置恢复 `currentLevelIndex`、切割规格和倒计时。
后续若扩展更多难度,只能通过同一个关卡配置解析函数扩展,不允许在 UI 里写死另一套时间。
## 计时规则
有效消耗时间计算:
```text
effectiveElapsedMs = nowMs - startedAtMs - pausedAccumulatedMs - activeFreezeElapsedMs
remainingMs = max(0, timeLimitMs - effectiveElapsedMs)
```
其中:
1. 弹窗打开、设置面板打开、查看原图覆盖打开时,运行态需要暂停。
2. 冻结时间生效时,画面播放冻结特效,并展示冻结剩余时长。
3. 通关时 `elapsedMs` 使用有效消耗时间,不把确认弹窗、查看原图和冻结时间计入成绩。
4. 失败后保留棋盘,不弹通关结算。
5. 正式后端 run 的前端倒计时归零时,需要主动刷新一次 `getPuzzleRun`,让 SpacetimeDB 侧把 `failed` 状态写回快照,避免只停留在本地视觉失败。
## 道具规则
### 提示
提示道具只演示,不替玩家移动。
演示对象选择:
1. 优先选当前棋盘中拼块数量最多、且尚未完全处于正确位置的合并块。
2. 若没有合并块,选择一个不在正确格子的单块。
3. 演示从当前所在格移动到该块锚点的正确格,结束后回到原位。
### 查看原图
查看原图是开关按钮:
1. 打开后把原图以半透明方式覆盖在拼图棋盘上。
2. 覆盖期间暂停倒计时;确认弹窗关闭到覆盖层显示之间不得恢复计时,正式后端 run 也需要保持 `pauseStartedAtMs`
3. 再次点击关闭覆盖并恢复计时。
### 冻结时间
冻结时间确认后:
1. 播放冻结视觉特效。
2. 显示冻结剩余时长。
3. 第一版冻结 `10000ms`
4. 若玩家打开冻结确认弹窗前视觉上仍是 `playing`,但确认扣费期间正式 run 已被服务端计时结算为 `failed`,服务端不得返回“操作不合法”。本次调用视为一次边界同步,已预扣费用必须退款,只返回失败态快照并刷新存档;前端关闭道具确认窗,展示失败面板,不播放冻结特效。
## 计费规则
每次确认使用道具消耗 `1` 光点。
正式后端运行态复用现有资产操作钱包预扣链路,新增道具 `asset_kind`
1. `puzzle_prop_hint`
2. `puzzle_prop_preview`
3. `puzzle_prop_freeze_time`
本地调试 run 没有真实用户钱包,不伪造扣费,只保留同样的确认交互与运行态效果。
若扣费或道具过程失败,确认弹窗保持打开并继续暂停倒计时,在弹窗内展示失败原因;只有成功确认后才关闭弹窗并播放对应反馈。
补充规则:冻结时间的边界同步不属于道具使用成功。服务端若发现 run 已超时失败,应退回本次预扣、把失败态落库并返回最新快照,避免玩家在确认窗内看到“操作不合法”。
## UI 规则
1. 底部只放 3 个道具按钮,不写规则说明文案。
2. 点击道具弹出独立确认窗口,不在底栏下方展开。
3. 确认窗口打开期间暂停计时。
4. 按钮使用图标和短标签;不可用时降低透明度。
5. 失败状态使用简洁弹窗展示,可返回或重新开始,不与通关结算混用。
## 画布表现修正
本轮同步修正合并块视觉:
1. 合并块之间不再使用额外 `p-1` 缝隙,拼图块需要贴合。
2. 单块和大块使用同一套边界描边宽度与颜色。
3. 外轮廓和凹入转角都需要圆角化。
4. 新合并产生时,在新大块中心播放一次简洁闪光,不显示文字提示。

View File

@@ -65,6 +65,16 @@
7. 每次进入下一关都会重置棋盘、推进关卡序号,并按已通关数量切换 `3x3 / 4x4`
8. 当前不依赖后端 `start/swap/drag/next-level` 接口保存过程状态
## 6. 2026-04-29 拖动责任边界修正
拼图运行态的拖动逻辑完全交给前端:
1. `pointerup` 解析出的目标格只调用前端 `dragLocalPuzzlePiece`
2. 单块拖动、合并块整体平移、被覆盖块交换、拆分、重新合并、通关判定,都以前端当前 `PuzzleRunSnapshot` 为准。
3. Rust API 不再暴露 `/api/runtime/puzzle/runs/{runId}/drag` 给前端调用;后端旧 procedure 仅作为历史兼容实现,不作为当前运行态入口。
4. 真实排行榜仍由后端成绩表负责;提交成功后,前端只把后端返回的 `leaderboardEntries` 合并回当前本地棋盘快照,不能用后端旧棋盘覆盖前端拖动后的状态。
5. 下一关仍通过 `advanceLocalPuzzleNextLevel` 把前端当前 run 交给 Rust API 生成候选关卡,后端只裁决图片来源与新关卡初始化,不保存上一关拖动过程。
## 5. 当前实现判断标准
当下面结果成立时,视为这一轮目标达成:

View File

@@ -0,0 +1,60 @@
# 拼图作品积分激励链路设计
更新时间:`2026-05-01`
## 1. 目标
1. 拼图草稿页“新增关卡”按钮下方显示一行小字:“获得更多积分激励”。
2. 创作页的已发布拼图作品卡展示当前作品的积分激励总数、待领取积分数和领取按钮。
3. 用户在他人已发布拼图作品中消耗光点时,作品作者获得消耗光点数量的一半作为积分激励。
4. 作者领取时只能领取整数个光点,待领取值向下取整;未满 1 个光点的半数余额继续保留。
## 2. 数据模型
拼图作品激励归属到 `puzzle_work_profile`
1. `point_incentive_total_half_points: u64`
- 记录该作品累计获得的激励,单位为“半个光点”。
- 每消耗 `N` 个光点,增加 `N` 个 half points当前拼图道具每次消耗 1 个光点,因此每次为作者增加 0.5。
2. `point_incentive_claimed_points: u64`
- 记录作者已领取的整数光点数量。
3. 前端展示:
- 激励总数 = `pointIncentiveTotalHalfPoints / 2`,允许展示一位小数。
- 待领取积分 = `floor(pointIncentiveTotalHalfPoints / 2) - pointIncentiveClaimedPoints`
- 领取按钮仅在待领取积分大于 0 时可用。
## 3. 后端事务
1. 拼图运行道具扣费成功、道具效果成功落库后,后端根据 run 的当前作品 `profile_id` 查找作者。
2. 若使用者不是作品作者,则给该作品累积 `consumed_points` 个 half points。
3. 若使用者是作者本人,视为作者自测,不产生积分激励。
4. 若后续业务操作失败并触发扣费退款,不写入激励。
5. 领取接口:
- 只允许作品作者领取。
- 计算可领取整数 `claimable = total_half_points / 2 - claimed_points`
- `claimable <= 0` 时拒绝领取。
- 同一事务内更新作品 `claimed_points += claimable`,并向作者钱包增加 `claimable` 光点,钱包流水来源使用 `puzzle_author_incentive_claim`
## 4. API 与前端
1. `PuzzleWorkSummary` / `PuzzleWorkProfile` 增加:
- `pointIncentiveTotalHalfPoints`
- `pointIncentiveClaimedPoints`
- `pointIncentiveTotalPoints`
- `pointIncentiveClaimablePoints`
2. 新增领取接口:
- `POST /api/runtime/puzzle/works/{profile_id}/point-incentive/claim`
- 返回更新后的 `PuzzleWorkProfile`
3. 创作页仅对已发布拼图作品显示积分激励块RPG、大鱼和草稿卡不显示。
4. 领取成功后刷新对应拼图作品列表状态,按钮立即禁用或显示新的待领取数,并同步刷新个人钱包看板。
5. `spacetime-client` 映射层继续兼容历史拼图运行快照:旧 `run_json` 若缺少 `started_at_ms`API 记录回填为非 0 值,避免前端计时器拿到无效开始时间。
## 5. 验收点
1. 拼图草稿页新增关卡按钮下方显示“获得更多积分激励”。
2. 已发布拼图作品卡展示“积分激励总数”和“待领取”两个数值。
3. 待领取积分为 0 时领取按钮禁用。
4. 非作者游玩他人拼图并使用付费道具后,该作品累计 half points 增加。
5. 作者领取后钱包增加向下取整后的整数光点,作品待领取数归零或保留不足 1 的小数余额。
6. 领取成功后顶部/我的页钱包余额随个人看板刷新。
7. 修改后运行编码检查、SpacetimeDB 绑定生成、Rust 检查和必要前端测试。

View File

@@ -48,6 +48,22 @@
- [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_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):记录 Maincloud `xushi-p4wfr` 挂起导致认证快照同步和抓大鹅创作失败的根因、认证同步非阻断修复、`/api/creation` Vite 代理补齐和本地 SpacetimeDB 可跑链路。
- [LLM_MODEL_ROUTING_RPG_AND_CREATION_2026-04-30.md](./LLM_MODEL_ROUTING_RPG_AND_CREATION_2026-04-30.md):冻结 RPG 运行时剧情推理使用 `doubao-seed-character-251128``/chat/completions`,以及所有模板创作大模型推理使用 `deepseek-v3-2-251201``/responses`
- [PROFILE_INVITE_CODE_REGISTRATION_AND_ADMIN_2026-04-30.md](./PROFILE_INVITE_CODE_REGISTRATION_AND_ADMIN_2026-04-30.md):冻结邀请码从“我的 Tab 填写”迁到注册环节的前后端边界、`profile_invite_code.metadata_json` 表结构扩展、管理员邀请码虚拟主体和奖励规则。
- [MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md](./MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md):冻结抓大鹅 Match3D 首版 demo 的独立玩法域、表与 procedure、HTTP facade、前端即时反馈/后端权威确认协议,以及可并行开发包。
- [MATCH3D_DOMAIN_AND_CONTRACTS_STAGE1_2026-04-30.md](./MATCH3D_DOMAIN_AND_CONTRACTS_STAGE1_2026-04-30.md):冻结抓大鹅 Match3D B1+B2 的纯领域规则 crate、Rust/TypeScript shared contracts以及 Stage1 不触碰 SpacetimeDB 表和 api-server 的边界。
- [MATCH3D_F1_CREATION_ENTRY_AND_AGENT_UI_2026-04-30.md](./MATCH3D_F1_CREATION_ENTRY_AND_AGENT_UI_2026-04-30.md):记录抓大鹅 F1 创作入口、Agent 工作区、参考图入口、本地 mock client 与后续 B5 HTTP facade 替换点。
- [MATCH3D_F2_RESULT_AND_PUBLISH_2026-04-30.md](./MATCH3D_F2_RESULT_AND_PUBLISH_2026-04-30.md):冻结抓大鹅 F2 结果页、基础信息编辑、发布前试玩入口、发布门槛、自动保存和已发布作品二次编辑恢复口径。
- [MATCH3D_SPACETIME_CLIENT_AND_API_FACADE_2026-04-30.md](./MATCH3D_SPACETIME_CLIENT_AND_API_FACADE_2026-04-30.md):记录抓大鹅 B4+B5 已落地的 SpacetimeDB bindings、`spacetime-client` facade、`api-server` HTTP 路由、shared contract 对齐和验收命令。
- [MATCH3D_CREATION_ENTRY_COMING_SOON_2026-05-01.md](./MATCH3D_CREATION_ENTRY_COMING_SOON_2026-05-01.md):记录抓大鹅创作页入口重新开放、首屏与弹层分流一致,以及公开广场失败不污染创作错误态的边界。
- [MATCH3D_Q1_INTEGRATION_ACCEPTANCE_2026-05-01.md](./MATCH3D_Q1_INTEGRATION_ACCEPTANCE_2026-05-01.md):记录抓大鹅 Match3D 第一至第三波完成度复核、Q1 主链集成落点、定向验收命令和遗留风险。
- [PLATFORM_MOBILE_BOTTOM_DOCK_VIEWPORT_FIX_2026-04-30.md](./PLATFORM_MOBILE_BOTTOM_DOCK_VIEWPORT_FIX_2026-04-30.md):记录平台首页底部 dock 在手机浏览器地址栏展开时脱离可见区域的根因,以及 `100dvh`、固定底部锚点和安全区占位的修复口径。
- [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md):记录 SpacetimeDB private 表迁移 JSON 导出/导入 procedure、迁移操作员授权、HTTP 413 分片导入、Jenkins 自动迁移回灌和导入脚本参数。
- [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md):记录 `Genarrative-Database-Export` / `Genarrative-Database-Import` 两条 SCM-backed 数据库迁移流水线参数、默认 dry-run、token 边界和 `CHUNK_SIZE` 413 规避参数。
- [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md):冻结后台管理独立前端工程 `apps/admin-web` 的技术方案,明确管理端只做表现、全部数据和写操作走 `server-rs``/admin/api/*`,并接管旧 `GET /admin` 内嵌页面的 UI 职责。
- [RPG_PROMPT_FRONTEND_REMOVAL_AND_SERVER_RS_MIGRATION_2026-04-28.md](./RPG_PROMPT_FRONTEND_REMOVAL_AND_SERVER_RS_MIGRATION_2026-04-28.md):冻结 RPG 提示词禁止存在前端的边界,明确前端只保留 API client角色私聊/NPC 对话/剧情续写等 prompt 统一收口到 `server-rs`
- [RPG_CREATION_RESULT_VIEW_BACKEND_TRUTH_MIGRATION_2026-04-28.md](./RPG_CREATION_RESULT_VIEW_BACKEND_TRUTH_MIGRATION_2026-04-28.md):冻结 RPG 创作结果页保存、Agent session/result preview 真相优先级和结果页入口裁决迁移到后端 result-view 的落地边界。
- [RPG_CREATION_PROFILE_GENERATION_BACKEND_MIGRATION_2026-04-28.md](./RPG_CREATION_PROFILE_GENERATION_BACKEND_MIGRATION_2026-04-28.md):记录 RPG 创作 profile 生成移除非浏览器 legacy AI 回退,统一通过 `server-rs``/api/runtime/custom-world/profile` 生成世界底稿。
@@ -55,7 +71,15 @@
- [BIG_FISH_DRAFT_PROGRESS_AND_SESSION_TIMEOUT_GUARD_FIX_2026-04-28.md](./BIG_FISH_DRAFT_PROGRESS_AND_SESSION_TIMEOUT_GUARD_FIX_2026-04-28.md):记录大鱼吃小鱼草稿进度页从单步 compile 改为多阶段感知展示,以及大鱼会话读取在 Maincloud 抖动时增加短重试与超时语义收口的修复口径。
- [BIG_FISH_PROMPT_MODULE_EXTRACTION_2026-04-28.md](./BIG_FISH_PROMPT_MODULE_EXTRACTION_2026-04-28.md):记录大鱼吃小鱼草稿生成、生图、动作三类提示词从业务脚本中抽离到独立 `prompt/big_fish.rs` 模块的边界与职责划分。
- [BIG_FISH_MAIN_IMAGE_TRANSPARENT_BACKGROUND_ALIGNMENT_2026-04-28.md](./BIG_FISH_MAIN_IMAGE_TRANSPARENT_BACKGROUND_ALIGNMENT_2026-04-28.md):记录大鱼吃小鱼等级主图与动作关键帧正式图在 Rust 后端复用 RPG 角色主图透明背景 alpha 后处理的对齐口径,并明确场地背景不走该处理。
- [PUZZLE_IMAGE_AND_FRONTEND_RULES_ALIGNMENT_2026-04-29.md](./PUZZLE_IMAGE_AND_FRONTEND_RULES_ALIGNMENT_2026-04-29.md):记录拼图生成图片回到 1:1运行时拖动、交换、合并与拆分由前端即时裁决以及移动端棋盘贴近屏幕边缘的落地边界。
- [PUZZLE_FORM_CREATION_FLOW_2026-04-29.md](./PUZZLE_FORM_CREATION_FLOW_2026-04-29.md):冻结拼图填表式创作入口、初始表单自动保存草稿、生成前退出后的表单恢复,以及草稿编译/首图生成的前后端边界。
- [PUZZLE_LEADERBOARD_FRONTEND_LEVEL_AND_RPG_COMING_SOON_2026-04-30.md](./PUZZLE_LEADERBOARD_FRONTEND_LEVEL_AND_RPG_COMING_SOON_2026-04-30.md):记录拼图第二关排行榜提交以前端当前关卡为准、不被 SpacetimeDB 旧 run 快照误杀,以及 RPG 创作入口改为敬请期待的落地边界。
- [PUZZLE_NEXT_LEVEL_AND_SIMILAR_WORK_HANDOFF_2026-04-30.md](./PUZZLE_NEXT_LEVEL_AND_SIMILAR_WORK_HANDOFF_2026-04-30.md):记录拼图通关后优先同作品下一关、无下一关时按 RPG/build 标签语义相似度返回三个候选作品,并在跨作品时只切换到候选作品第 1 张图、运行时关卡序号继续累进的落地规则。
- [PUZZLE_FAILURE_EXTENSION_AND_SAVE_ARCHIVE_2026-05-01.md](./PUZZLE_FAILURE_EXTENSION_AND_SAVE_ARCHIVE_2026-05-01.md):记录拼图失败后重新开始/付费续时,以及进入作品与过关后同步存档页投影的落地规则。
- [PUZZLE_RUNTIME_TIMER_AND_PROPS_2026-04-29.md](./PUZZLE_RUNTIME_TIMER_AND_PROPS_2026-04-29.md)记录拼图关卡切割、倒计时、失败态和三个运行时道具的统一规则2026-05-01 起关卡切割与限时按第 1-10 关配置,并从第 11 关按第 5-10 关六关循环。
- [RPG_SCENE_ACT_PREVIEW_BOOTSTRAP_FIX_2026-04-30.md](./RPG_SCENE_ACT_PREVIEW_BOOTSTRAP_FIX_2026-04-30.md):记录编辑器幕预览卡在“正在载入这一幕”时的启动态根因,收口预览本地运行态装配与禁持久化首段 story 注入。
- [PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.md](./PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.md):记录拼图结果页名称与标签编辑自动保存、发布门槛统一到 `3~6` 标签,以及前端发布校验不再被旧 session blocker 卡死的修复口径。
- [WORK_AUTHOR_ID_RESOLUTION_2026-04-30.md](./WORK_AUTHOR_ID_RESOLUTION_2026-04-30.md):记录作品作者以 `owner_user_id` 为真相源API 按用户 ID 解析最新昵称与公开用户码,历史 `author_display_name` 仅作为兼容回退。
- [SPACETIMEDB_START_SH_EARLY_EXIT_DIAGNOSTICS_2026-04-27.md](./SPACETIMEDB_START_SH_EARLY_EXIT_DIAGNOSTICS_2026-04-27.md):记录发布包 `start.sh` 只输出“SpacetimeDB 进程在就绪前退出”时的诊断补强,启动失败或超时时自动回显 `logs/spacetimedb.log``server ping`、端口监听和 root-dir 相关进程。
- [RPG_AND_AGENT_CHAT_TRUE_SSE_STREAMING_2026-04-26.md](./RPG_AND_AGENT_CHAT_TRUE_SSE_STREAMING_2026-04-26.md):记录 RPG 运行时 NPC 聊天、RPG/自定义世界 Agent 与大鱼 Agent 从“拼完整 SSE 字符串后一次性返回”改为 `mpsc + Sse<Event>` 真流式输出的后端落地口径。
- [SPACETIMEDB_START_SH_ROOT_OWNER_FALSE_POSITIVE_FIX_2026-04-27.md](./SPACETIMEDB_START_SH_ROOT_OWNER_FALSE_POSITIVE_FIX_2026-04-27.md):记录发布包 `start.sh` root-dir 占用检测把 `grep -F .../.spacetimedb` 误判为 SpacetimeDB 实例的根因、脚本修复和现场处理方式。
@@ -87,7 +111,7 @@
- [CREATION_AGENT_PUBLISH_GATE_NORMALIZE_WRITEBACK_FIX_2026-04-24.md](./CREATION_AGENT_PUBLISH_GATE_NORMALIZE_WRITEBACK_FIX_2026-04-24.md):记录结果页 profile 归一化回写丢失顶层 `worldHook / playerPremise` 导致 publish gate 继续误报结构 blocker 的根因,并冻结前端归一化保留发布字段的修复口径。
- [CUSTOM_WORLD_RESULT_ENTITY_GENERATION_FIX_2026-04-24.md](./CUSTOM_WORLD_RESULT_ENTITY_GENERATION_FIX_2026-04-24.md):记录世界结果页在 Agent 草稿模式下新增场景、新增 NPC 生成成功但结果页字段不可用的根因,并冻结 `api-server` 生成归一化层补齐 profile 字段的修复口径。
- [ADMIN_CONSOLE_SERVICE_DESIGN_2026-04-23.md](./ADMIN_CONSOLE_SERVICE_DESIGN_2026-04-23.md):冻结 Rust `api-server` 内后台管理服务首版方案,明确管理员用户名密码登录、管理员 JWT 鉴权、数据库概览、受控 API 调试台与同源管理页面的落地边界
- [ADMIN_CONSOLE_SERVICE_DESIGN_2026-04-23.md](./ADMIN_CONSOLE_SERVICE_DESIGN_2026-04-23.md):冻结 Rust `api-server` 内后台管理 API 首版方案;同源内嵌页面已取消,管理员登录、管理员 JWT 鉴权、数据库概览、受控 API 调试台和兑换码管理 API 保留给独立后台前端工程复用
- [SPACETIME_MODULE_LIB_RS_SPLIT_EXECUTION_2026-04-23.md](./SPACETIME_MODULE_LIB_RS_SPLIT_EXECUTION_2026-04-23.md):冻结 `server-rs/crates/spacetime-module/src/lib.rs` 的模块地图、二级落位点与迁移顺序,要求后续 SpacetimeDB 主工程改动按对应模块落位,不再继续堆回单大文件。
- [CUSTOM_WORLD_DRAFT_FOUNDATION_API_SERVER_LLM_MIGRATION_2026-04-23.md](./CUSTOM_WORLD_DRAFT_FOUNDATION_API_SERVER_LLM_MIGRATION_2026-04-23.md):冻结 `draft_foundation` 从 SpacetimeDB 内部规则编译迁到 `api-server + platform-llm` 的边界,明确草稿必须由 `api-server` 真实调 LLM 生成SpacetimeDB 只负责落库。
- [CUSTOM_WORLD_AGENT_LLM_REPLY_RESTORE_2026-04-22.md](./CUSTOM_WORLD_AGENT_LLM_REPLY_RESTORE_2026-04-22.md):恢复 Custom World Agent 聊天必须走大模型推理的 Rust 落地方案,冻结 submit/finalize 两阶段职责、旧 Node 提示词原样搬运、SSE 流式回复与 session 回写边界。
@@ -101,10 +125,10 @@
- [CREATION_AGENT_PUBLISH_GATE_SCHEMA_ALIGNMENT_FIX_2026-04-23.md](./CREATION_AGENT_PUBLISH_GATE_SCHEMA_ALIGNMENT_FIX_2026-04-23.md):记录发布阻断项仍按旧 `worldHook / playerPremise / sceneChapters` schema 校验的问题,以及将 Rust `publish gate` 对齐到 `anchorContent / creatorIntent / sceneChapterBlueprints` 当前主链结构的修复口径。
- [CREATION_HUB_CARD_ACTIONS_2026-04-22.md](./CREATION_HUB_CARD_ACTIONS_2026-04-22.md):冻结创作中心作品卡“体验 / 删除”入口的最小落地语义,明确 RPG 已发布作品软删除、卡片直达运行时,以及暂不扩草稿 / 拼图删除契约。
- [CREATION_CATEGORY_OPENING_TIMEOUT_GUARD_FIX_2026-04-22.md](./CREATION_CATEGORY_OPENING_TIMEOUT_GUARD_FIX_2026-04-22.md):记录创作中心点击类别后长时间停留在“正在开启”的根因与修复口径,收口前端创建会话启动超时、中文错误提示以及 Big Fish / 拼图代理上游超时兜底。
- [JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md](./JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md):冻结 Jenkins `构建 / 部署 / 构建并部署` 三条流水线的职责、版本号传递、上游触发门禁、本地目录部署脚本`/home/ubuntu/Genarrative-deploy/` 覆盖策略
- [JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md](./JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md):冻结 Jenkins `构建 / 部署 / 构建并部署` 三条流水线的职责、版本号传递、上游触发门禁、本地目录部署脚本、发布包覆盖策略,以及部署阶段 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 上传安全清库开关。
- [RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md](./RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md):记录当前 Rust `api-server` 已挂载路由,并补充管理后台入口与管理接口索引,按 auth、assets、runtime、custom world、story 等挂载面归类,用于对照 Node 能力基线与切流 smoke 清单;`/generated-*` 直读代理已下线。
- [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 等挂载面归类,用于对照 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

@@ -15,11 +15,13 @@
## 体验规则
- 等待态继续复用 `RouteLoadingScreen`,只显示简短加载文案,不在 UI 中追加规则说明。
- `RouteLoadingScreen` 必须读取 `tavernrealms.settings.v1` 中的 `platformTheme`,并复用 `platform-theme--light / platform-theme--dark``platform-body-fill / platform-text-*` token不能硬编码独立深色背景。
- 页面主体隐藏时使用 `visibility: hidden`,不能用 `display: none`,否则浏览器可能不触发布局与图片加载。
- 图片加载失败不直接改写业务 UI后续仍由原页面的兜底图、占位图或错误态处理。
## 涉及文件
- `src/routing/RouteImageReadyGate.tsx`
- `src/routing/RouteLoadingScreen.tsx`
- `src/routing/RouteImageReadyGate.test.ts`
- `src/main.tsx`

View File

@@ -51,7 +51,10 @@ server-rs/crates/api-server/src/prompt/
├─ big_fish.rs
├─ character_animation.rs
├─ character_visual.rs
├─ puzzle_image.rs
├─ puzzle/
│ ├─ agent_chat.rs
│ ├─ image.rs
│ └─ mod.rs
├─ scene_background.rs
├─ mod.rs
└─ rpg/

View File

@@ -0,0 +1,50 @@
# RPG 首页自定义世界库超时修复
日期:`2026-04-29`
## 1. 问题
首页进入时会并发读取:
1. `GET /api/runtime/custom-world-library`
2. `GET /api/runtime/custom-world-gallery`
3. 个人看板、浏览历史、存档等私有数据
其中 `custom-world-library` 通过 `api-server -> spacetime-client -> list_custom_world_profiles procedure` 读取当前用户作品。旧实现把每个作品的完整 `profile_payload_json` 一并返回给首页列表,而首页卡片只需要标题、摘要、封面、状态和计数字段。用户作品较多或 Maincloud 连接抖动时,这个 procedure 容易超过 `spacetime-client` 固定 `10s` 等待窗口,最终由 Axum 映射成 `502 Bad Gateway`,前端控制台显示 `SpacetimeDB procedure 调用超时`
## 2. 修复口径
本轮不改表结构,不新增前端展示规则,只收窄首屏读模型负载:
1. `list_custom_world_profiles` 仍保持旧 procedure 名称和返回 envelope避免本轮重新生成 bindings。
2. 列表返回的 `profile_payload_json` 改为轻量摘要 JSON只包含首页卡片和标签兜底需要的少量字段。
3. 单条详情、发布、下架、编辑继续使用完整 profile snapshot确保进入详情或结果页时仍有完整世界数据。
4. `spacetime-client` 的 procedure 等待窗口从硬编码 `10s` 改为可配置Maincloud 默认使用更宽的窗口吸收连接冷启动与短时抖动。
5. Axum 的 `GET /api/runtime/custom-world-library` 首屏接口改走已有 `custom-world/works` 轻量读模型,并在用户点击详情/编辑时再调用 owner-only detail 接口取完整 profile避免 Maincloud wasm 尚未发布轻量 profile procedure 时首页继续命中重 procedure。
## 3. 轻量 profile JSON 字段
列表轻量 profile 只保留:
1. `id`
2. `name`
3. `subtitle`
4. `summary`
5. `themeMode`
6. `cover.imageSrc`
7. `majorFactions`
8. `coreConflicts`
9. `playableNpcs`
10. `storyNpcs`
11. `landmarks`
这些字段足够支撑首页卡片的封面、标签、数量和基本文案。服务端列表兜底允许把 `majorFactions``coreConflicts``playableNpcs``storyNpcs``landmarks` 返回为空数组,并依赖 entry 顶层的计数字段、封面和主题兜底展示。需要完整 profile 的操作必须走 detail 或 mutation 回包,不能依赖列表接口搬大 JSON。
## 4. 验收
1. 首页进入不再因为 `custom-world-library` 首屏列表超时直接报 502。
2. `cargo check -p spacetime-module` 通过。
3. `cargo check -p spacetime-client` 通过。
4. `cargo check -p api-server` 通过。
5. `npm run check:encoding` 通过。
6. 修改后按项目约束使用 `npm run api-server:maincloud` 重启后端。

View File

@@ -0,0 +1,26 @@
# RPG NPC 战斗入口任务与目标显示修复记录2026-04-29
## 背景
运行态从 NPC 交互进入战斗后,出现两个连带问题:
1. 玩家没有确认领取任务,但界面表现为突然多了一个任务。
2. 对面的 NPC 进入战斗后从画布上消失。
## 根因
1. `project_story_engine_after_action` 会在动作后自动补齐当前场景章节任务。这个规则适合“进入/探索场景”的开章节点,但不适合 `npc_fight / npc_spar` 战斗入口;否则玩家点击战斗也会像被系统强行塞入任务。
2. `resolve_npc_battle_entry_action` 进入战斗时会清空 `currentEncounter`,并改由 `sceneHostileNpcs` 承接敌方渲染。若进入战斗前已有 `sceneHostileNpcs` 但条目缺少 `encounter`,画布层会因为没有 NPC 形象上下文而跳过渲染。
## 落地边界
1. `npc_fight / npc_spar` 只负责进入战斗,不创建章节任务,不接取 NPC pending quest。
2. 场景章节任务仍保留在真正的场景进入、观察、推进节点自动创建。
3. 战斗入口必须保证每一个 `sceneHostileNpcs` 条目都带有可渲染的 `encounter`;若旧数据缺失,使用进入战斗前的当前 NPC encounter 兜底。
4. 前端画布也要兜底渲染缺少 `encounter` 的战斗目标,避免服务端旧快照或迁移数据导致目标直接不可见。
## 验证点
1. `npc_fight` 带 pending quest story 时,不写入 `quests`,不增加 `runtimeStats.questsAccepted`
2. `npc_fight` 时若已有敌方列表缺少 `encounter`,服务端会给战斗目标补齐进入战斗前的 NPC encounter。
3. 画布层在 `sceneCombatants[].encounter` 缺失时仍显示敌方名称和血条。

View File

@@ -0,0 +1,49 @@
# RPG NPC 聊天禁存快照会话修复2026-04-28
## 背景
复测 NPC 对话时,前端请求:
```text
POST /api/runtime/chat/npc/turn/stream
```
返回 `409 Conflict`,错误为:
```text
请求的运行时会话与服务端快照不一致,请重新进入游戏
```
当前 story action 主链已经支持 `runtimePersistenceDisabled = true` 的运行态:这类请求会携带临时 `snapshot`,后端只构造本次响应,不写入 `runtime_snapshot`。但 NPC 聊天接口在带 `sessionId` 时只从正式 `runtime_snapshot` 读取上下文。若当前运行态是作品测试、幕预览或其他禁存 run服务端正式快照仍可能是上一局正式存档导致 `sessionId``snapshotSessionId` 不一致。
## 修复原则
1. 正式 `play` 且允许持久化的运行态,聊天请求继续只提交 `sessionId` 与聊天输入,保持后端快照为唯一来源。
2. `runtimePersistenceDisabled = true` 或历史 `runtimeMode = preview/test` 的运行态,聊天请求必须携带临时 `snapshot`
3. 后端聊天接口收到临时 `snapshot` 时,只用它投影本轮 prompt context、NPC state、history、encounter不写入 `runtime_snapshot`
4. 临时 `snapshot.gameState.runtimeSessionId` 必须与请求 `sessionId` 一致;不一致仍返回 `409`
5. 前端不得把完整快照默认用于正式游玩聊天,避免重新扩大浏览器对正式真相链的写入权。
## 工程落点
1. `packages/shared/src/contracts/rpgRuntimeChat.ts`
- 给角色聊天、NPC 对话、NPC 单轮、招募对话请求补可选 `snapshot` 字段。
2. `src/services/aiService.ts`
- 增加禁存运行态 snapshot 构造。
- 仅在 `context.runtimeSnapshot` 存在时,随聊天请求提交 `snapshot`
3. `src/services/aiTypes.ts`
- `StoryGenerationContext` 增加可选 `runtimeSnapshot`
4. `src/hooks/rpg-runtime-story/storyContextBuilder.ts`
- 禁存运行态把当前 `GameState` 作为临时 snapshot 注入 context。
5. `server-rs/crates/api-server/src/runtime_chat.rs`
- `NpcChatTurnRequest` 接收 `snapshot`
- 优先使用请求 snapshot 校验并投影上下文。
6. `server-rs/crates/api-server/src/runtime_chat_plain.rs`
- 角色聊天、NPC 开场、招募同样支持临时 snapshot。
## 验收
1. 禁存运行态 NPC 聊天请求体携带 `snapshot`,正式运行态不携带。
2. 后端 NPC 聊天在请求 snapshot 与正式存档 session 不一致时,仍按请求 snapshot 成功投影,不读取旧正式存档。
3. snapshot 内部 `runtimeSessionId` 与请求 `sessionId` 不一致时返回 `409`
4. 相关前端单测、后端 `runtime_chat` 单测、编码检查通过。

View File

@@ -0,0 +1,67 @@
# RPG 开局首幕 NPC 流程收口方案2026-04-30
## 目标
本轮只收口“进入游戏开局场景后遇到第一幕第一批人”的运行时流程:
1. 对方主角色好感度 `>= 0` 时,聊天过程中允许出现 `npc_chat`、任务、送礼、交易、获得帮助等 NPC 功能选项;聊天结束后界面只保留一个 `story_continue_adventure`,点击后直接推进到下一幕。
2. 对方主角色好感度 `< 0` 时,聊天过程中只允许 `npc_chat`;聊天可以由模型中断,也可以由玩家主动中断。中断后只允许 `npc_fight``battle_escape_breakout`
3. 删除这条主流程里的干扰分支:正好感聊天结束后不再展开旧 NPC 目录或相邻场景旅行;负好感聊天中不再混入交易、送礼、求助、任务、招募、切磋、离开等 function。
## 工程落点
1. `src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts`
- `buildNpcChatFunctionOptionCatalog(...)` 按当前 NPC 好感过滤功能候选。
- 负好感聊天候选只保留 `npc_chat`
- 正好感聊天结束后的 `story_continue_adventure` 只揭开下一幕入口;若当前场景没有下一幕,才退回相邻场景入口。
2. `src/hooks/rpg-runtime-story/choiceActions.ts`
- 应用 `deferredRuntimeState.storyEngineMemory`,保证点击继续后真正切到下一幕的 `currentSceneActState`
3. `server-rs/crates/api-server/src/runtime_story/compat/presentation.rs`
- 服务端 active NPC option catalog 与前端同规则对齐。
- 负好感 active NPC 只返回 `npc_chat`
- 非负好感 active NPC 返回聊天、帮助、交易、送礼、任务、招募等功能,不再返回战斗、切磋、离开。
## 验收
1. 正好感 NPC 主动退出聊天后,只显示 `story_continue_adventure`
2. 点击 `story_continue_adventure` 后,`storyEngineMemory.currentSceneActState.currentActId` 推进到下一幕。
3. 负好感 NPC 聊天请求中的 `functionOptions` 为空,聊天 UI 不出现非聊天 function。
4. 负好感聊天中断后只出现“战斗”和逃跑选项。
5. 服务端 state catalog 对负好感 active NPC 不返回交易、送礼、帮助、任务、招募、切磋、离开等干扰入口。
## 2026-04-30 补充:负好感主动中止恢复
### 问题
敌对聊天的模型主动中止依赖后端建议 JSON 中的 `shouldEndChat` 字段,但部分入口没有把负好感 NPC 标记为 `terminationMode: hostile_model`,导致后端即使收到 `shouldEndChat: true` 也会按非敌对聊天忽略。另一个缺口是 NPC 主动开场第一轮只展示后续候选,没有处理 `chatDirective.forceExit`,因此第一轮开场也无法被模型主动中止。
### 落地
1. `src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts`
- 构造 `NpcChatDirective` 时直接读取当前 NPC 好感度。
- 只要 `affinity < 0`,统一写入 `limitReason: negative_affinity``terminationMode: hostile_model``isHostileChat: true`
- NPC 主动开场收到 `chatTurn.chatDirective.forceExit === true` 时,立即收起 `npcChatState`,展示战斗与逃跑选项。
### 补充验收
1. 任意负好感 NPC 聊天轮都必须向后端传 `terminationMode: hostile_model`,不能只依赖第一幕主 NPC 场景幕状态。
2. 负好感 NPC 主动开场第一轮若返回 `forceExit: true`,聊天输入立即关闭,只显示战斗与逃跑。
## 2026-04-30 补充:聊天首句统一由模型 NPC 发起
### 问题
NPC 主动开场链路本身已经存在,并会以空玩家消息调用模型,同时传入 `npcInitiatesConversation: true`。但运行时入口曾把这条链路限制在 `firstMeaningfulContactResolved !== true`,导致角色完成首次有效接触后,再次从 NPC 入口或交互选项进入聊天时,会退回旧的 `enterNpcChat(...)` 本地入口:界面先展示玩家可点的话题,没有模型生成的 NPC 首句。负好感且非限定场景幕时,还存在一条本地敌对宣言分支,会直接给战斗/逃跑,绕过“先聊天再中断”的主流程。
### 落地
1. `enterNpcInteraction(...)` 不再用 `firstMeaningfulContactResolved` 决定是否走 NPC 主动开场;只要是从 NPC 入口新开聊天,都调用 `startNpcInitiatedOpening(...)`
2. `handleNpcInteraction(...)``chat` 分支保留“当前已经在同一段 `npcChatState` 内时,点击 `npc_chat` 作为玩家回复”的行为;若不在已有聊天内,统一调用 `startNpcInitiatedOpening(...)`
3. 删除负好感入口的本地敌对宣言分支。负好感只通过 `NpcChatDirective` 影响模型语气、功能选项和 `forceExit` 后的战斗/逃跑收束,不再跳过模型首句。
4. `enterNpcChat(...)` 仅保留为缺少角色/世界类型或模型开场失败时的兜底入口,不作为正常聊天开场路径。
### 补充验收
1. 不论好感度正负,也不论 `firstMeaningfulContactResolved` 是否为 `true`,新开聊天首轮都必须调用 `streamNpcChatTurn(..., '', { npcInitiatesConversation: true })`
2. 新开聊天最终展示的第一条 `dialogue` 必须是模型返回的 NPC 文本,`npcChatState.openingSource` 必须是 `npc_initiated`
3. 已经处于同一段 `npcChatState` 中时,点击 `npc_chat` 仍作为玩家本轮回复进入 `handleNpcChatTurn(...)`,不能重新开一段 NPC 首句。
4. 负好感入口不能直接显示本地战斗/逃跑;只有模型或玩家中断聊天后,才显示 `npc_fight``battle_escape_breakout`

View File

@@ -0,0 +1,26 @@
# RPG 运行态面板右上关闭按钮修复2026-04-29
## 背景
RPG 运行态里仍有一批历史手写弹窗,没有统一迁入 `UnifiedModal`。这些弹窗的右上关闭按钮分别散落在角色详情、队伍、背包、地图、NPC 交易、任务日志和奖励面板里,按钮尺寸、层级、点击事件传播和无障碍标识不一致。
用户反馈多个 RPG 模板游戏内面板右上角关闭按钮点击无效。排查后,本次先按最小风险方式修复关闭交互边界,不重构业务面板结构。
## 落地方案
1. 新增 `PixelCloseButton` 作为 RPG 像素风面板右上关闭按钮的统一组件。
2. 组件内部统一处理:
- `event.preventDefault()`
- `event.stopPropagation()`
- 稳定 `z-index`
- 固定移动端友好的点击面积;
- `aria-label``title`
3. RPG 游戏内旧弹窗的右上关闭按钮统一替换为 `PixelCloseButton`
4. 保留各面板原本的关闭回调和业务状态清理逻辑,不改变任务、奖励、交易、地图、角色详情等业务行为。
## 验收
1. 点击游戏内面板右上关闭按钮时,只触发该按钮的关闭回调,不被父层遮罩或面板点击处理吞掉。
2. 队伍、背包、地图、角色详情、角色聊天、NPC 交易 / 赠礼 / 招募、任务日志、任务详情、奖励详情等面板的右上关闭按钮可稳定关闭。
3. 关闭按钮具备可检索的无障碍名称,后续可用自动化测试直接定位。
4. 编码检查、定向测试和类型检查通过。

View File

@@ -0,0 +1,19 @@
# RPG 运行态队伍 / 背包面板信息精简2026-04-29
## 背景
运行态队伍与背包都属于冒险过程中的辅助弹出面板,移动端优先要求是快速查看成员状态、物品格子与工坊操作。当前队伍面板在成员列表上方额外展示活跃任务,背包面板在格子上方额外展示旅程回顾,会把首屏焦点从“队伍成员 / 背包物品”推开。
## 落地边界
1. 队伍面板删除成员列表上方的任务信息模块。
2. 背包面板删除物品格子上方的旅程回顾模块。
3. 不删除任务系统、旅程回顾数据或冒险页里的任务提示,只调整这两个辅助面板的展示入口。
4. 父级不再向这两个面板传入已经不展示的字段,避免保留无效 UI 契约。
## 验收
1. 打开队伍面板后,顶部直接进入“队伍成员”列表。
2. 打开背包面板后,顶部直接进入物品格子。
3. 任务状态仍由冒险主面板和任务弹层承担。
4. `continueGameDigest` 数据仍保留在运行态状态中,后续可在更合适的独立入口展示。

View File

@@ -0,0 +1,66 @@
# RPG 幕预览启动卡载入修复2026-04-30
## 背景
编辑器内点击“幕预览”后,独立预览层会一直停在“正在载入这一幕的游戏流程...”,无法进入真实 RPG 运行壳。
## 根因
`SceneActPreviewRuntime` 先调用 `handleCustomWorldSelect(profile)`,紧接着调用 `handleCharacterSelect(previewCharacter)`
`handleCharacterSelect()` 读取的是当前 render 闭包中的旧 `gameState`。此时 `handleCustomWorldSelect()` 写入的 `worldType` 还没有完成 React 状态提交,所以选角入口看到 `worldType` 为空后直接返回。随后幕预览虽然又手动写入了 `currentScene / currentScenePreset / currentEncounter`,却没有写入 `playerCharacter`,导致 `isPreviewReady` 永远不成立。
另一个隐患是:有 `currentEncounter` 时 story controller 不会主动生成普通首段剧情,而是交给 NPC 交互流接管;若预览没有显式注入一个可展示的 `currentStory`,运行面板也可能无法稳定挂载。
## 本轮继续修复
继续复测时发现,`SceneActPreviewRuntime` 虽然已经不再调用 `handleCharacterSelect()`,但仍会调用 `handleCustomWorldSelect(profile)` 来同步 runtime 静态资料。
这个入口是正式运行态的“选择世界”入口,会排队写入“已选择世界、尚未选角”的中间 `GameState`。在幕预览本地 `setGameState()` 写入玩家、场景与故事后,这个中间态仍可能覆盖回来,导致 `currentScenePreset``playerCharacter` 被清掉,预览层重新停在“正在载入这一幕的游戏流程...”。
本轮调整后:
1. 幕预览不再调用 `handleCustomWorldSelect(profile)`
2. 幕预览直接调用 `setRuntimeCustomWorldProfile(profile)``setRuntimeCharacterOverrides(buildCustomWorldRuntimeCharacters(profile))` 同步静态资料层。
3. `isPreviewReady` 同时校验:
- `currentScene === "Story"`
- `runtimeSessionId === "runtime-scene-act-preview"`
- 当前玩家就是本次预览角色
- 当前场景就是本次预览场景
- 当前 story 已经完成注入
4. 这样 preview ready 只依赖本次预览自己的完整启动结果,不再被正式选世界中间态影响。
## 修复口径
1. 幕预览不再调用 `handleCharacterSelect()` 触发后端开局副作用。
2. 幕预览不调用正式 `handleCustomWorldSelect(profile)`,而是直接同步 runtime 静态资料层。
3. 随后在同一个 `setGameState` 中一次性写入:
- `playerCharacter`
- `runtimeMode: "play"`
- `runtimePersistenceDisabled: true`
- `currentScene / currentScenePreset / currentEncounter`
- 玩家血蓝、技能冷却、装备、统计、进度、队伍、任务等运行态基础字段
- 当前幕 `currentSceneActState`
4. 幕预览使用固定临时 `runtimeSessionId: "runtime-scene-act-preview"`,并通过禁持久化快照保持不写正式存档。
5. 启动时同步 `hydrateStoryState()`,注入当前幕 NPC 的本地开场 story`RpgRuntimeShell` 立即满足挂载条件。
## 约束
1. 幕预览继续复用正式 `play` 运行链,不恢复旧 `preview/test` 行为分支。
2. 幕预览只允许前端做临时运行态装配;正式游戏开局仍由 `server-rs` 裁决。
3. 后续如把幕预览也迁到后端 bootstrap应新增专门的禁持久化 bootstrap 入口,而不是再次依赖 `handleCharacterSelect()` 的异步状态顺序。
## 验证
新增回归覆盖:
```bash
npm test -- --run src/components/CustomWorldEntityEditorModal.test.tsx
```
断言幕预览打开后:
1. 不再显示“正在载入这一幕的游戏流程...”。
2. `RpgRuntimeShell` 已收到预览玩家角色。
3. 运行态为 `play` 且禁用持久化。
4. 当前 story 已注入为当前幕 NPC 开场内容。

View File

@@ -2,7 +2,7 @@
## 背景
世界创作结果页已经提供“作品测试”入口,但测试运行时此前缺少与“幕预览”一致的显式退出按钮。创作者进入测试后只能依赖浏览器返回、刷新或其他间接链路离开,不符合独立运行时面板的交互语义。
世界创作结果页已经提供“作品测试”入口,但测试运行时此前缺少与“幕预览”一致的显式退出按钮。百梦主进入测试后只能依赖浏览器返回、刷新或其他间接链路离开,不符合独立运行时面板的交互语义。
## 本次约束

View File

@@ -2,7 +2,7 @@
## 背景
幕预览和测试作品用于创作者检查玩法表现,不能被当作玩家正式游玩记录。若这类运行时复用正式 RPG 壳、story action 或 snapshot 接口,必须在进入个人存档页、游玩统计、作品游玩历史前被过滤。
幕预览和测试作品用于百梦主检查玩法表现,不能被当作玩家正式游玩记录。若这类运行时复用正式 RPG 壳、story action 或 snapshot 接口,必须在进入个人存档页、游玩统计、作品游玩历史前被过滤。
## 落地约束

View File

@@ -1,6 +1,6 @@
# Rust API Server 路由索引2026-04-23
更新时间:`2026-04-23`
更新时间:`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 和兼容路由删除计划。
>
@@ -14,29 +14,33 @@
## 2. 当前统计
当前 Rust `api-server``app.rs` 可抽取到 `101` 条路由:
当前 Rust `api-server``app.rs` 可抽取到 `116` 条路由:
1. 管理后台接口`5` 条。
1. 管理后台 API`6` 条。
2. 内部鉴权调试接口:`2` 条。
3. AI task 接口:`9` 条。
4. assets / OSS 接口:`15` 条。
5. auth 接口:`12` 条。
6. custom world / agent 接口:`23` 条。
7. llm proxy 接口:`1` 条。
8. profile / runtime profile 接口:`12` 条。
9. story gameplay / runtime inventory 接口:`10` 条。
10. legacy generated 静态路径兼容`6` 条。
11. health check`1`
7. match3d creation / runtime 接口:`14` 条。
8. llm proxy 接口:`1` 条。
9. profile / runtime profile 接口:`12` 条。
10. runtime story / story gameplay 接口`15` 条。
11. legacy generated 静态标识`6` 类历史路径字符串,直读代理已下线
12. health check`1` 条。
## 3. 路由清单
### 3.1 管理后台
### 3.1 管理后台 API
1. `GET /admin`
2. `POST /admin/api/login`
3. `GET /admin/api/me`
4. `GET /admin/api/overview`
5. `POST /admin/api/debug/http`
1. `POST /admin/api/login`
2. `GET /admin/api/me`
3. `GET /admin/api/overview`
4. `POST /admin/api/debug/http`
5. `POST /admin/api/profile/redeem-codes`
6. `POST /admin/api/profile/redeem-codes/disable`
`GET /admin` 同源内嵌页面入口已取消挂载,后台 UI 后续由独立前端工程承接。
### 3.2 内部鉴权调试
@@ -118,7 +122,24 @@
1. `POST /api/llm/chat/completions`
### 3.8 Profile / Runtime Profile
### 3.8 Match3D Creation / Runtime
1. `POST /api/creation/match3d/sessions`
2. `GET /api/creation/match3d/sessions/{session_id}`
3. `POST /api/creation/match3d/sessions/{session_id}/messages`
4. `POST /api/creation/match3d/sessions/{session_id}/messages/stream`
5. `POST /api/creation/match3d/sessions/{session_id}/actions`
6. `POST /api/creation/match3d/sessions/{session_id}/compile`
7. `GET /api/creation/match3d/works`
8. `GET / PATCH / PUT / DELETE /api/creation/match3d/works/{profile_id}`
9. `POST /api/creation/match3d/works/{profile_id}/publish`
10. `GET /api/runtime/match3d/gallery`
11. `POST /api/runtime/match3d/works/{profile_id}/runs`
12. `GET /api/runtime/match3d/runs/{run_id}`
13. `POST /api/runtime/match3d/runs/{run_id}/click`
14. `POST /api/runtime/match3d/runs/{run_id}/stop / restart / time-up`
### 3.9 Profile / Runtime Profile
1. `GET /api/profile/dashboard`
2. `GET /api/runtime/profile/dashboard`
@@ -133,7 +154,7 @@
11. `POST /api/profile/save-archives/{world_key}`
12. `POST /api/runtime/profile/save-archives/{world_key}`
### 3.9 Story Gameplay / Runtime Inventory
### 3.10 Runtime Story / Gameplay
1. `POST /api/runtime/save/snapshot`
2. `GET /api/runtime/settings`
@@ -146,11 +167,11 @@
9. `POST /api/story/npc/battle`
10. `GET /api/runtime/sessions/{runtime_session_id}/inventory`
### 3.10 Legacy Generated 路径
### 3.11 Legacy Generated 路径
`/generated-*` 直读代理已下线。生成资产读取统一走 `GET /api/assets/read-url` 或 asset object projection`/generated-*` 字符串仅作为 `legacyPublicPath` / OSS object key 兼容标识保留。
### 3.11 Health
### 3.12 Health
1. `GET /healthz`

View File

@@ -34,7 +34,7 @@ npm run dev:rust
1. 检查 `cargo``node``spacetime` CLI。
2. Windows Git Bash 下如 `server-rs/.spacetimedb/local/bin/current/spacetimedb-cli.exe` 不存在,先把本机 `spacetime` 所在安装目录的 `bin/``spacetime.exe` 同步到 `server-rs/.spacetimedb/local/`
3. 启动 `spacetime --root-dir=server-rs/.spacetimedb/local start --edition standalone --listen-addr 127.0.0.1:3101`,确保本地数据库与 SpacetimeDB 内部日志不会落到开发者全局目录。
4. 等待 `spacetime --root-dir=server-rs/.spacetimedb/local server ping http://127.0.0.1:3101` 可用;判定标准必须包含输出中的 `Server is online:`,不能只依赖 CLI 退出码,因为 SpacetimeDB CLI `2.1.0``502 Bad Gateway` 时也可能返回退出码 `0`
4. 等待 SpacetimeDB 就绪:优先接受 `spacetime --root-dir=server-rs/.spacetimedb/local server ping http://127.0.0.1:3101` 输出中的 `Server is online:`;如果 Windows 下 SpacetimeDB CLI `2.1.0` 对已经监听的 standalone 仍打印 `502 Bad Gateway`,脚本会兜底请求 `http://127.0.0.1:3101/v1/ping`,只有该健康端点返回 `2xx` 时才放行。不能只依赖 CLI 退出码,因为 CLI`502 Bad Gateway` 时也可能返回退出码 `0`
5. 执行 `spacetime --root-dir=server-rs/.spacetimedb/local publish <本地数据库名> --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module -c=on-conflict --yes`,确保 publish 的签名身份与 standalone 的本地控制库一致,并在当前开发阶段允许新版模块表结构变化且发生 schema 冲突时清除旧模块数据。
6. 注入 `GENARRATIVE_API_*``GENARRATIVE_SPACETIME_*` 后启动 `cargo run -p api-server`;直接运行 `api-server` 时,如未显式设置 `GENARRATIVE_SPACETIME_DATABASE`,服务端也会向上查找 `spacetime.local.json` 作为本地默认库名。
7. 等待 `http://127.0.0.1:<api-port>/healthz` 返回 HTTP 响应后再启动 Vite避免前端初始化请求早于 Rust `api-server` 监听完成并在终端刷出 `ECONNREFUSED 127.0.0.1:<api-port>`
@@ -45,7 +45,8 @@ Vite 代理覆盖范围:
1. `/api/runtime/*` 会在 Rust 栈下代理到 Rust `api-server`,覆盖旧 runtime story 兼容接口。
2. `/api/story/*` 会在 Rust 栈下代理到 Rust `api-server`,覆盖新 story session、battle 查询与 NPC battle 切片接口。
3. 其他 `/api/auth``/api/assets``/api/custom-world``/api/llm` 等路径仍由同一个 `GENARRATIVE_RUNTIME_SERVER_TARGET` 控制,便于 M7 按服务能力逐项做对比 smoke
3. `/api/creation/*` 会在 Rust 栈下代理到 Rust `api-server`,覆盖 Match3D 等创作域接口,避免开发态请求落回 Vite SPA HTML
4. 其他 `/api/auth``/api/assets``/api/custom-world``/api/llm` 等路径仍由同一个 `GENARRATIVE_RUNTIME_SERVER_TARGET` 控制,便于 M7 按服务能力逐项做对比 smoke。
安全边界:
@@ -103,7 +104,7 @@ npm run dev:rust:logs -- --follow
2. `spacetime --root-dir=server-rs/.spacetimedb/local list --server http://127.0.0.1:3101` 应能看到 `spacetime.local.json` 中的库名;若没有,执行 `spacetime --root-dir=server-rs/.spacetimedb/local publish <本地数据库名> --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module -c=on-conflict --yes`
3. 发布库名与 `GENARRATIVE_SPACETIME_DATABASE` 不一致时,`/api/runtime/custom-world-gallery` 会从 Rust `api-server` 返回 `502`,前端首页只能展示空态或错误提示,无法自行修复。
4. 如果 Vite 输出 `/api/auth/refresh``/api/auth/login-options``/api/runtime/custom-world-gallery``ECONNREFUSED`,先确认当前脚本是否已经打印 `等待 api-server 就绪` 并通过;正常情况下 Vite 只会在 `/healthz` 可访问后启动,不应再因为 Rust 监听未完成而代理失败。
5. 如果 `spacetime server ping` 打印 `Server could not be reached (502 Bad Gateway)`,即使命令退出码为 `0`必须视为就绪;脚本会继续等待新启动实例,或在 root-dir 已被其他实例占用时输出占用进程。
5. 如果 `spacetime server ping` 打印 `Server could not be reached (502 Bad Gateway)`,即使命令退出码为 `0`不能直接视为就绪;本地脚本会继续探测 `/v1/ping`。若 `/v1/ping` 返回 `200`,说明 standalone 已经可用,可以继续发布模块;若 `/v1/ping` 也失败,脚本会继续等待新启动实例,或在 root-dir 已被其他实例占用时输出占用进程。
编译警告治理:
@@ -137,14 +138,18 @@ npm run deploy:rust:remote
1. 在仓库根目录创建 `build/`
2.`build/` 下创建当前时间命名的目标目录,例如 `build/20260422-153000/`
3. 使用 Vite 构建前端 release 到目标目录的 `web/`
3. 使用 Vite 构建前端 release 到目标目录的 `web/`,并构建后台管理前端 release 到 `web/admin/`;后台构建固定使用 `/admin/` 作为 Vite base
4. 执行 `cargo build -p api-server --release --target x86_64-unknown-linux-gnu --manifest-path server-rs/Cargo.toml`,并把 `api-server` 复制到目标目录。
5. 执行 `cargo build -p spacetime-module --release --target wasm32-unknown-unknown --manifest-path server-rs/Cargo.toml`,并把 `spacetime_module.wasm` 复制到目标目录。
6. 把仓库根目录的 `.env``.env.local` 分别复制到目标目录根部和目标目录的 `web/` 下;复制后统一移除 UTF-8 BOM 与 CRLF避免目标服务器 Bash 加载环境文件失败
7. 在目标目录写入 `web-server.mjs`,用于托管 `web/` 并把 `/api/*``/generated-*``/healthz` 反代到本包内的 `api-server`
8. 在目标目录写入 `start.sh``stop.sh``start.sh` 会先按 `KEY=value` 子集加载发布目录根部的 `.env``.env.local`,兼容 UTF-8 BOM 与 CRLF再回退到构建时通过 `--database``--api-port``--web-port``--spacetime-host``--spacetime-port` 写入的默认值,并默认导出 `NO_COLOR=1``CARGO_TERM_COLOR=never`,避免 ANSI 控制码写入日志文件;同时按 Ubuntu 发布环境使用发布目录内 `.spacetimedb/` 作为 root-dir不再额外设置 `--data-dir`,启动前先执行 `sync_ubuntu_spacetime_install`,优先从 `/usr/.local/share/spacetime/bin/<version>/spacetimedb-cli``$HOME/.local/share/spacetime/bin/<version>/spacetimedb-cli` 同步到 `.spacetimedb/bin/current/spacetimedb-cli`,当前线上 `spacetime` 入口为 `/usr/local/bin/spacetime`;启动参数为 `spacetime --root-dir ./.spacetimedb start --edition standalone --listen-addr <host>:<port>`,探活必须确认 `server ping` 输出包含 `Server is online:`;如果以 `--clear-database` 启动,则内部 `spacetime publish` 会追加 `-c=on-conflict`仅在 schema 冲突时删除旧模块数据
6. 把仓库根目录的 `.env``.env.local` 分别复制到目标目录根部和目标目录的 `web/` 下;复制后统一移除 UTF-8 BOM 与 CRLF并把 `GENARRATIVE_SPACETIME_DATABASE` 覆盖为本次 `--database` 参数,避免 Jenkins 工作区里残留的旧 `.env.local` 覆盖发布包目标库
7. 在目标目录写入 `web-server.mjs`,用于托管 `web/` `web/admin/`;其中 `/admin` 跳转到 `/admin/``/admin/` 提供后台 SPA`/admin/api/*``/api/*``/generated-*``/healthz` 反代到本包内的 `api-server`
8. 在目标目录写入 `start.sh``stop.sh``start.sh` 会先按 `KEY=value` 子集加载发布目录根部的 `.env``.env.local`,兼容 UTF-8 BOM 与 CRLF再回退到构建时通过 `--database``--api-port``--web-host``--web-port``--spacetime-host``--spacetime-port` 写入的默认值,其中 Web 默认只监听 `127.0.0.1`并默认导出 `NO_COLOR=1``CARGO_TERM_COLOR=never`,避免 ANSI 控制码写入日志文件;同时按 Ubuntu 发布环境使用发布目录内 `.spacetimedb/` 作为 root-dir不再额外设置 `--data-dir`,启动前先执行 `sync_ubuntu_spacetime_install`,优先从 `/usr/.local/share/spacetime/bin/<version>/spacetimedb-cli``$HOME/.local/share/spacetime/bin/<version>/spacetimedb-cli` 同步到 `.spacetimedb/bin/current/spacetimedb-cli`,当前线上 `spacetime` 入口为 `/usr/local/bin/spacetime`;启动参数为 `spacetime --root-dir ./.spacetimedb start --edition standalone --listen-addr <host>:<port>`,探活必须确认 `server ping` 输出包含 `Server is online:`普通启动先无清库发布,若 publish 输出可判定为 schema 冲突,则自动导出旧库、清库发布新 wasm、导入回灌如果以 `--clear-database` 启动,则内部 `spacetime publish` 会追加 `-c=on-conflict`代表人工确认清库,不触发自动回灌
9. 默认执行 `scp -r -i ~\.ssh\dsk.pem build/<timestamp> ubuntu@82.157.175.59:/home/ubuntu/genarrative/` 上传发布包。
SpacetimeDB database 名称必须匹配 `^[a-z0-9]+(-[a-z0-9]+)*$`:只能使用小写字母、数字,并用单个短横线分隔;大写字母、点号、下划线、首尾短横线和连续短横线都会触发 `spacetime publish``invalid characters in database name`。发布包构建脚本和 `start.sh` 都会提前拦截这类非法名称。
发布包构建日志会输出 `SpacetimeDB 发布数据库: <database>`;目标服务器执行 `start.sh` 时会在发布前输出最终加载后的 `database/server/root-dir`,用于确认 `.env.local` 或 Jenkins 参数覆盖后的实际发布目标。
发布包结构:
```text
@@ -153,9 +158,14 @@ build/<timestamp>/
├─ .env.local
├─ web/
│ ├─ .env
─ .env.local
─ .env.local
│ └─ admin/
│ └─ index.html
├─ api-server
├─ spacetime_module.wasm
├─ migration-bootstrap-secret.txt
├─ scripts/
│ └─ spacetime-*.mjs
├─ web-server.mjs
├─ start.sh
├─ stop.sh
@@ -167,9 +177,12 @@ build/<timestamp>/
```bash
npm run build:rust:ubuntu -- --name 20260422-153000
npm run build:rust:ubuntu -- --database genarrative-dev --web-port 3000 --api-port 8082 --spacetime-port 3101
npm run build:rust:ubuntu -- --database genarrative-dev --web-host 127.0.0.1 --web-port 3000
npm run build:rust:ubuntu -- --skip-upload
```
`--skip-web-build` 会同时跳过主前端和后台管理前端构建,仅用于调试已有发布包内容;正式发布不应使用该参数。
目标服务器启动:
```bash
@@ -178,26 +191,28 @@ cd build/<timestamp>
./stop.sh
```
如果后续通过 Jenkins 的部署脚本把发布包覆盖到固定部署目录,部署阶段默认只替换 `web/``api-server``spacetime_module.wasm``.env*``start.sh``stop.sh``web-server.mjs``README.md` 等发布产物;文件产物使用普通复制,`web/` 等目录产物递归复制,不会删除部署目录中的 `.spacetimedb/``logs/``run/` 这类运行态目录
如果后续通过 Jenkins 的部署脚本把发布包覆盖到固定部署目录,部署阶段默认只替换 `web/``api-server``spacetime_module.wasm``migration-bootstrap-secret.txt``scripts/``.env*``start.sh``stop.sh``web-server.mjs``README.md` 等发布产物;后台管理前端位于 `web/admin/`,随 `web/` 一并覆盖。文件产物使用普通复制,`web/``scripts/` 等目录产物递归复制,不会删除部署目录中的 `.spacetimedb/``logs/``run/``deploy-state/``database-migrations/` 这类运行态目录。Jenkins 覆盖 `.env.local` 时会保留目标部署目录已有的 `GENARRATIVE_SPACETIME_TOKEN` / `GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN`,避免后台表统计在部署后失去读取 private 表所需的 owner 身份
安全边界:
1. 构建脚本会把仓库根目录已有的 `.env``.env.local` 一并复制进发布包,因此运行前必须确认这些文件内容适合被带入目标环境。
2. 如果仓库根目录不存在 `.env``.env.local`,脚本会打印跳过日志,但不会因此失败;此时 `start.sh` 仅使用构建时写入的默认值与运行时显式传入的环境变量。
3. `start.sh` 只解析合法 `KEY=value` 环境行,支持不加引号、双引号和单引号;不执行复杂 shell 表达式,避免把环境文件变成脚本入口。
4. `start.sh` 默认不追加清理参数;只有显式执行 `./start.sh --clear-database` 才追加 `-c=on-conflict`,在 schema 冲突时清理旧模块数据后重发
5. `start.sh` 使用 `spacetime publish --bin-path spacetime_module.wasm --yes` 发布当前包内 wasm清库模式下会追加 `-c=on-conflict`,仅在 schema 冲突时删除旧模块数据
6. `start.sh` 会先复用已经按目标地址就绪的 SpacetimeDB如果同一个 `.spacetimedb/` root-dir 已被其他未就绪实例占用,则只输出命令名为 `spacetime``spacetimedb-cli` 且命令行包含当前 root-dir 的真实占用进程并失败,避免把排查用的 `grep` / `awk` 误判为 SpacetimeDB 实例
7. 如果 `spacetime publish``403 Forbidden`,优先确认 `spacetime --root-dir ./.spacetimedb login show` 输出的身份是否有权更新目标库;`--clear-database` 不能绕过身份授权
8. 当前脚本是单目录进程启动方案,不替代生产 systemd、Nginx、TLS、日志轮转与守护进程配置
9.只需要本地生成发布包,可传 `--skip-upload` 跳过默认 scp 上传
4. `start.sh` 默认不追加清理参数;普通发布如果遇到 SpacetimeDB schema 冲突,会调用发布包内的 `scripts/spacetime-export-migration-json.mjs` 导出旧库,再清库发布新 wasm并调用 `scripts/spacetime-import-migration-json.mjs --replace-existing` 回灌。可通过 `GENARRATIVE_SPACETIME_MIGRATE_ON_CONFLICT=false` 禁用该行为
5. 自动迁移导出旧库时优先读取 `deploy-state/migration-bootstrap-secret.previous.txt`,导入新库时读取当前发布包 `migration-bootstrap-secret.txt`Jenkins 部署脚本会在覆盖发布包前保存旧密钥。该快照属于部署状态,不放入 `run/`,避免启停 hook 通过 `sudo` 运行后把部署阶段要写的文件变成 root 私有。手工覆盖发布包时,也应在覆盖前保留旧模块的引导密钥,否则旧库导出可能无法授权
6. 自动迁移 JSON 默认写入发布目录下 `database-migrations/<database>/`;可通过 `GENARRATIVE_SPACETIME_MIGRATION_DIR` 改写。该目录属于运行态,不应被 Jenkins 覆盖部署删除
7. 只有显式执行 `./start.sh --clear-database` 才追加 `-c=on-conflict`,该模式代表人工确认清库,不执行导出和回灌
8. `start.sh` 会先复用已经按目标地址就绪的 SpacetimeDB如果同一个 `.spacetimedb/` root-dir 已被其他未就绪实例占用,则只输出命令名为 `spacetime``spacetimedb-cli` 且命令行包含当前 root-dir 的真实占用进程并失败,避免把排查用的 `grep` / `awk` 误判为 SpacetimeDB 实例
9.`spacetime publish``403 Forbidden`,优先确认 `spacetime --root-dir ./.spacetimedb login show` 输出的身份是否有权更新目标库;`--clear-database` 不能绕过身份授权
10. 当前脚本是单目录进程启动方案,不替代生产 systemd、Nginx、TLS、日志轮转与守护进程配置。
11. 如只需要本地生成发布包,可传 `--skip-upload` 跳过默认 scp 上传。
目标服务器最小要求:
1. Ubuntu x86_64。
2. 已安装 `node`,用于运行发布包内的 `web-server.mjs`
3. 已安装 `spacetime` CLI`start.sh` 会启动本地 SpacetimeDB 并发布 wasm。
4. 业务密钥通过目标服务器环境变量或发布包同目录 `.env.local` 提供。
4. 业务密钥通过目标服务器环境变量或发布包同目录 `.env.local` 提供;后台概览如果需要统计 private 表,`GENARRATIVE_SPACETIME_TOKEN` 必须是目标库 owner 或具备等效读取权限的 token
## 4. 与 M7 的关系

View File

@@ -10,7 +10,7 @@
1. 草稿层可以承载 `scene chapter / scene act`
2. 后端可以把 `scene_chapter` 编译成正式蓝图
3. 创作者可以在现有场景编辑弹层里看到并编辑多幕配置
3. 百梦主可以在现有场景编辑弹层里看到并编辑多幕配置
4. 编辑后的幕信息可以正确写回 `sceneChapterBlueprints`
5. 运行时共享层先具备读取幕背景、主角色、相遇 NPC 池的基础能力
6. 当前幕主角色的负好感 `5` 轮聊天限制先形成首个可运行闭环
@@ -60,7 +60,7 @@
前端已完成第一批接入:
1. `scene_chapter` 不再作为独立 Tab / 独立卡片暴露给创作者
1. `scene_chapter` 不再作为独立 Tab / 独立卡片暴露给百梦主
2. 多幕配置已内嵌到 `CustomWorldEntityEditorModal.tsx``LandmarkEditor`
3. 单幕编辑已从文本表单切成“背景大图预览 + 3 个角色槽位”的轻量交互
4. “幕标题 / 幕摘要 / 幕目标 / 过渡钩子”已从场景手工编辑区移除,继续留在草稿生成与编译层
@@ -88,7 +88,7 @@
7. 幕预览运行时已补 custom world NPC 的视觉兜底链路,优先使用 `visual / imageSrc` 渲染,避免角色形象或动画空白
8. 当前幕小预览已调整为左侧玩家、右侧敌对/相遇角色的构图NPC 站位采用一前两后
前排主角色与玩家角色保持同一 y 轴后排两个角色改为同一列、x 轴对齐并上下分布,且后排整体 y 轴中点与前排主角色一致
9. 新增幕默认只带 1 个主角色,后续槽位由创作者按需补充
9. 新增幕默认只带 1 个主角色,后续槽位由百梦主按需补充
10. 小预览里的名字已移动到角色头顶,角色渲染不再带方形底板,避免遮挡场景背景
11. 幕预览复用真实游戏壳时隐藏左上角角色等级徽标,退出入口固定在上方画面区域底部居中,并使用“结束预览”作为操作文案
12. 创作侧场景列表封面、多幕配置卡片、配置背景弹层统一读取同一张场景显示图;在任一幕保存背景时同步回全部幕背景字段和场景兼容图,避免同一场景在不同层级出现不同预览图

View File

@@ -409,7 +409,7 @@ Access-Control-Allow-Credentials: true
职责:
- 面向创作者、运营、内部编辑器
- 面向百梦主、运营、内部编辑器
- 必须鉴权
- 必须审计
- 不建议对公网完全开放
@@ -469,7 +469,7 @@ flowchart TD
当出现这些需求时,再进入下一阶段:
- 多人同时在线
-创作者协作
-百梦主协作
- 图片/视频生成任务变多
- 需要账号体系、存档、云同步
- 需要审计和版本回滚

View File

@@ -57,3 +57,15 @@
- LLM 不可用时的聊天 reply、普通 choice、function choice 兜底生成。
3. `server-rs/crates/api-server/src/runtime_chat.rs` 只保留 Axum SSE、LLM 调用、解析、好感变化、结束聊天判断等流程编排,不再直接承载提示词正文或 choice 文案兜底。
4. 后续调整聊天 choice 语气、候选数量、`functionOptions` 描述方式、敌对聊天收束策略时,优先修改 `prompt/runtime_chat.rs`
## 7. 拼图 Prompt 独立目录收口
2026-04-30 追加收口:
1. 拼图提示词参考 RPG 的目录组织,统一迁入 `server-rs/crates/api-server/src/prompt/puzzle/`
2. `prompt/puzzle/agent_chat.rs` 承接拼图共创 Agent 的 system prompt、单轮 JSON 输出契约、用户提示词与 anchor pack / 聊天记录提示词组装。
3. `prompt/puzzle/draft.rs` 承接生成拼图作品草稿动作里的表单 seed prompt、草稿首图 prompt 来源选择、单关图片再生成 prompt 来源选择。
4. `prompt/puzzle/image.rs` 承接拼图图片生成正式提示词与默认反向提示词。
5. `puzzle_agent_turn.rs` 只保留 LLM 调用、结果解析、阶段判断和 SpacetimeDB 写回输入构造,不再内联拼图聊天提示词正文。
6. `puzzle.rs` 只保留拼图路由、计费、DashScope、OSS、候选图持久化和运行态编排不再内联拼图草稿或图片提示词正文。
7. 后续调整拼图共创问法、输出契约、生成草稿 prompt 来源、图片画面约束或反向提示词时,优先修改 `prompt/puzzle/`,不要在 `puzzle.rs``puzzle_agent_turn.rs` 中新增提示词正文。

View File

@@ -206,7 +206,7 @@
1. 密码登录仍由 `user_account.password_hash` 承担
2. 本轮不引入 `password` provider identity
3. 密码登录只接受已绑定手机号的账号,不支持邮箱、用户名或叙世号作为登录身份
3. 密码登录只接受已绑定手机号的账号,不支持邮箱、用户名或百梦号作为登录身份
4. 密码登录不创建账号,新账号只由手机号验证码登录创建
### 9.2 `POST /api/auth/phone/login`

View File

@@ -8,11 +8,13 @@ SpacetimeDB reducer 必须保持确定性,不能访问文件系统和网络。
1. `spacetime-module` 内的导出 procedure 读取迁移白名单表,并直接返回迁移 JSON 字符串。
2. Node 运维脚本默认通过 `spacetime call` 调用导出 procedure把返回的 JSON 字符串写入本地文件。
3. Node 运维脚本读取本地 JSON 文件内容。导入时默认先通过 `POST /v1/identity` 创建临时 Web API identity/token再用当前 CLI 登录态把该 identity 授权为迁移操作员,最后通过 HTTP request body 把 JSON 字符串传给导入 procedure。
3. Node 运维脚本读取本地 JSON 文件内容。导入时默认先通过 `POST /v1/identity` 创建临时 Web API identity/token再用当前 CLI 登录态把该 identity 授权为迁移操作员;小文件直接通过 HTTP request body 传给导入 procedure,大文件自动切成分片上传后再提交
4. 导入 procedure 校验 JSON 与表白名单后,在事务中写入目标数据库。
procedure 不再访问 HTTP 文件桥,也不接收部署机本地文件路径。这样可以避开 SpacetimeDB 对 private/special-purpose 地址的 HTTP 访问限制,并避免把 private 表内容通过临时 HTTP 服务转发。
SpacetimeDB Wasm 运行环境不支持 `std::time::SystemTime::now()`procedure 或 reducer 内需要当前时间时必须使用 `ctx.timestamp`。如果共享 crate 同时服务前端/本地纯逻辑与 SpacetimeDB 模块,应提供 `*_at(now_ms)` 或显式时间参数版本SpacetimeDB 模块只调用注入时间的函数,避免发布后在 maincloud 触发 `time not implemented on this platform` panic。
`spacetime login show --token` 输出的是 CLI 登录 token不是 HTTP `/v1/database/.../call` 所需的数据库连接 token。导入脚本如果没有显式传 `--token`,会自动调用 `POST /v1/identity` 获取 Web API token迁移时不要把 CLI token 传给 `--token`
## 接口
@@ -28,6 +30,8 @@ procedure 不再访问 HTTP 文件桥,也不接收部署机本地文件路径
`database_migration_operator` 只控制迁移 procedure 调用权限,不会被导出或导入,避免把源库的运维权限复制到目标库。
大文件分片导入额外使用私有临时表 `database_migration_import_chunk` 暂存上传片段。这张表只保存当前导入过程的中间数据,提交成功后自动删除,失败时由脚本尽量调用清理 procedure它不在迁移白名单内也不会被导出到业务迁移 JSON。
首次授权时,操作员表为空,必须通过编译进模块的 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET` 引导密钥授权第一位操作员。发布脚本会在构建或发布 SpacetimeDB 模块时自动生成一份强随机引导密钥、注入 wasm 编译环境,并在控制台显示;运维人员必须记录对应数据库本次发布输出的密钥。表内已经存在操作员后,后续授权与撤销只能由已有操作员发起;此时不再接受引导密钥越权扩权。
新增 procedure
@@ -97,6 +101,14 @@ node scripts/spacetime-revoke-migration-operator.mjs \
`import_database_migration_incremental_from_file(ctx, input)`
`put_database_migration_import_chunk(ctx, input)`
`import_database_migration_from_chunks(ctx, input)`
`import_database_migration_incremental_from_chunks(ctx, input)`
`clear_database_migration_import_chunks(ctx, input)`
输入字段:
- `migration_json`: 导出 procedure 生成的完整迁移 JSON 字符串。
@@ -104,6 +116,15 @@ node scripts/spacetime-revoke-migration-operator.mjs \
- `replace_existing`: 是否先清空本次迁移文件内实际导入的目标表。不会清空迁移文件未包含的表;分批迁移时只覆盖当前批次。
- `dry_run`: 只解析和统计,不写表。
分片导入字段:
- `upload_id`: 本次分片上传的唯一 ID只允许 ASCII 字母、数字、短横线或下划线。
- `chunk_index`: 当前分片序号,从 `0` 开始。
- `chunk_count`: 本次上传总分片数。
- `chunk`: 当前迁移 JSON 片段,单片最多 `1048576` bytes。
Node 导入脚本默认在文件超过 `524288` bytes 时使用分片导入;如果小文件直接导入仍遇到 `SpacetimeDB HTTP 413: Failed to buffer the request body: length limit exceeded`,也会自动退回分片流程。可通过 `--chunk-size <bytes>` 或环境变量 `GENARRATIVE_SPACETIME_MIGRATION_CHUNK_SIZE` 调小单片大小。
导入模式:
- 默认严格追加:不清空目标表,逐行插入;遇到主键或唯一约束冲突时失败并回滚,适合确认目标库没有同表旧数据时使用。
@@ -122,6 +143,76 @@ node scripts/spacetime-revoke-migration-operator.mjs \
## Node 脚本
### 发布冲突自动迁移
`npm run spacetime:publish:maincloud` 默认采用冲突感知发布:
1. 先不清库发布新 wasm。
2. 如果发布成功,流程结束。
3. 如果发布失败且输出可判定为 schema 冲突,脚本自动导出旧库迁移 JSON 到 `tmp/spacetime-migrations/maincloud/<database>/<timestamp>.json`
4. 导出成功后执行清库发布新 wasm。
5. 新 wasm 发布成功后,把第 3 步导出的 JSON 自动导入回灌。
SpacetimeDB 2.1 对 schema 冲突的报错文案可能不再包含 `schema conflict`,而是直接提示 `manual migration``default value annotation``--delete-data`。发布脚本必须把这些文案同样识别为可迁移冲突,否则会停在原始失败而不进入导出回灌流程。
新增字段优先采用低风险热升级策略:旧字段顺序保持不变,新字段追加到表尾,并用 `#[default(...)]` 提供旧行默认值。只有仍无法通过发布器检查时,才执行清库发布与 JSON 回灌。
任一阶段失败都会中止流程,并保留已经导出的迁移 JSON。非 schema 冲突的发布失败不会进入迁移流程。
```bash
npm run spacetime:publish:maincloud -- --database xushi-p4wfr
```
可选参数:
- `--no-migrate-on-conflict`:禁用冲突自动迁移,只保留原始发布失败。
- `--migration-dir <dir>`:指定迁移 JSON 输出目录。
- `--clear-database`:显式清库发布;该模式代表人工确认清库,不触发自动迁移。
冲突自动迁移需要发布脚本本次生成的 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET`。因此不要和 `--no-migration-bootstrap-secret` 同时使用。
### 部署流水线自动迁移
Ubuntu 发布包的 `start.sh` 与 Jenkins `Genarrative-Deploy` 也采用同一套迁移 procedure但迁移触发点在部署目录内
1. Jenkins 覆盖部署前,如果旧部署目录存在 `migration-bootstrap-secret.txt`,先保存到 `deploy-state/migration-bootstrap-secret.previous.txt`。旧密钥快照属于部署状态,不再写入 `run/`,避免 `sudo` 启停脚本生成的 root 私有运行目录阻断后续覆盖部署。
2. Jenkins 复制新发布包,包含新 wasm、新 `migration-bootstrap-secret.txt``scripts/spacetime-*.mjs` 迁移脚本。
3.`start.sh` 先不清库发布当前包内 `spacetime_module.wasm`
4. 如果发布成功,流程结束。
5. 如果发布失败且输出可判定为 schema 冲突,`start.sh` 用旧密钥授权导出旧库 JSON。
6. 导出成功后,`start.sh` 清库发布新 wasm。
7. 新 wasm 发布成功后,`start.sh` 用新密钥授权导入,并以 `--replace-existing` 回灌迁移 JSON。
自动迁移 JSON 默认保存到部署目录的 `database-migrations/<database>/<timestamp>.json`。可通过 Jenkins 参数 `MIGRATION_DIRECTORY` 或环境变量 `GENARRATIVE_SPACETIME_MIGRATION_DIR` 覆盖。该目录属于运行态数据,部署脚本不会删除。
Jenkins 参数 `MIGRATE_ON_CONFLICT` 默认 `true`。如果设为 `false`,普通发布遇到 schema 冲突时会保留原始失败,不执行导出、清库发布和导入回灌。
Jenkins 参数 `CLEAR_DATABASE=true` 或手工执行 `./start.sh --clear-database` 时,语义是人工确认清库发布;此时 `spacetime publish` 追加 `-c=on-conflict`,不执行自动导出和导入回灌。
自动迁移依赖两个引导密钥:
- 导出旧库:优先使用 `deploy-state/migration-bootstrap-secret.previous.txt`,也就是旧模块编译时注入的密钥。
- 导入新库:使用当前发布包 `migration-bootstrap-secret.txt`,也就是新模块编译时注入的密钥。
如果旧库或新库的 `database_migration_operator` 表已经不为空bootstrap secret 不能再越权授权新的操作员;此时必须由已有迁移操作员发起授权,或在部署目录 `.env.local` 中配置已授权操作员的连接 token
```text
GENARRATIVE_SPACETIME_MIGRATION_EXPORT_TOKEN=<旧库迁移操作员 token>
GENARRATIVE_SPACETIME_MIGRATION_IMPORT_TOKEN=<新库迁移操作员 token>
```
`GENARRATIVE_SPACETIME_MIGRATION_EXPORT_TOKEN` 只用于 schema 冲突时导出旧库;`GENARRATIVE_SPACETIME_MIGRATION_IMPORT_TOKEN` 只用于清库发布新 wasm 后导入回灌。Jenkins 覆盖部署会尽量保留部署目录现有 `.env.local` 中的这两个 token除非新发布包已经显式提供同名变量。
如果不是通过 Jenkins 部署脚本覆盖发布包,而是手工替换文件,必须在覆盖前保留旧 `migration-bootstrap-secret.txt`;否则旧库迁移 procedure 可能无法授权导出。
### 删除表和删除字段
迁移文件来自旧模块时,可能包含新模块已经删除的表或字段。导入阶段按以下规则处理:
- 迁移文件包含新模块已删除或不在白名单内的表时,不中断迁移;该表全部行计入 `skipped_row_count`,并在导入结束后统一展示 `dropped_table` 告警。
- 迁移行包含新模块已删除的旧字段时,导入 procedure 会尝试丢弃旧字段后继续反序列化;恢复成功则导入该行,并在导入结束后统一展示 `dropped_field` 告警。
- 新模块新增必填字段、字段类型变化、枚举不兼容等无法通过“丢弃旧字段”恢复的情况仍会失败并回滚,避免写入不完整数据。
本机导出时,先确保本机 SpacetimeDB 服务和源数据库可访问,然后授权本机调用身份:
```bash
@@ -180,17 +271,22 @@ node scripts/spacetime-import-migration-json.mjs \
1. `POST /v1/identity` 创建临时 Web API identity/token。
2. 使用当前机器 `spacetime` CLI 登录态调用 `authorize_database_migration_operator`,授权这个临时 identity。
3. 使用 `Authorization: Bearer <临时 token>` 调用 `import_database_migration_from_file`,把完整迁移 JSON 放在 HTTP body 中
4. 导入请求结束后,脚本会用同一个临时 Web API token 调用 `revoke_database_migration_operator`,撤销该临时 identity
3. 使用 `Authorization: Bearer <临时 token>` 导入迁移 JSON。文件不超过 `--chunk-size` 时直接调用 `import_database_migration_from_file`;超过阈值或直接导入触发 HTTP 413 时,先逐片调用 `put_database_migration_import_chunk`,再调用 `import_database_migration_from_chunks``import_database_migration_incremental_from_chunks`
4. 分片上传或提交失败时,脚本会尽量调用 `clear_database_migration_import_chunks` 清理临时分片
5. 导入请求结束后,脚本会用同一个临时 Web API token 调用 `revoke_database_migration_operator`,撤销该临时 identity。
所有直接访问 SpacetimeDB Web API 的 POST 请求必须显式发送 `Content-Type: application/json`。部分 SpacetimeDB 版本不会接受省略 content type 或附带非预期 media type 的请求,即使 body 本身是合法 JSON也会返回 `HTTP 415`
如果你已经有可用的数据库连接 token也可以显式传 `--token <web-api-token>`。这种情况下脚本不会自动授权;该 token 对应的 identity 必须已经是迁移操作员。
如果 `authorize_database_migration_operator` 返回 `当前 identity 未被授权执行数据库迁移`,说明当前机器 `spacetime` CLI 登录身份不是既有迁移操作员。表内已经存在操作员时,即使提供了正确 bootstrap secret也不会允许非操作员继续扩权需要先让既有操作员授权当前部署机 identity或直接使用既有操作员 token 执行导出/导入。
正式导入前建议先加 `--dry-run`,确认 JSON 可解析、版本匹配、表名都在迁移白名单内。
`--dry-run` 不会模拟目标库主键或唯一约束冲突,因此增量模式的 `skipped_row_count` 只有真实导入时才准确。
如果 Jenkins 或 SpacetimeDB 返回 `HTTP 413`,优先降低导入流水线的 `CHUNK_SIZE`,例如 `262144`。该参数只影响上传到 procedure 的单片 request body不改变迁移 JSON 的表范围和导入语义。
不要在只想追加数据时使用 `--replace-existing`。该参数会先删除覆盖范围内的目标表旧数据,再插入迁移文件中的数据;如果源文件不是完整快照,会造成目标表数据丢失。
如需分批迁移,可用逗号分隔表名:
@@ -221,12 +317,14 @@ node scripts/spacetime-export-migration-json.mjs \
- 自定义世界:`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`
- 资产索引:`asset_object``asset_entity_binding`
- 拼图:`puzzle_agent_session``puzzle_agent_message``puzzle_work_profile``puzzle_runtime_run`
- 大鱼:`big_fish_creation_session``big_fish_agent_message``big_fish_asset_slot``big_fish_runtime_run`
- 大鱼:`big_fish_creation_session``big_fish_agent_message``big_fish_asset_slot`
`big_fish_runtime_run` 当前运行态已由前端本地运行服务承接,不再加入迁移白名单;但 maincloud 旧库仍可能存在该表。为避免热升级被 “Removing the table big_fish_runtime_run requires a manual migration” 阻断,模块发布期可以保留兼容空壳表,后续确认旧数据可丢弃后再走正式删除表迁移。
后续新增 SpacetimeDB 表时,必须同步把表加入迁移白名单与本文档。
## 风险与限制
迁移 JSON 作为 procedure 返回值和 HTTP request body 传递,会受 SpacetimeDB 调用响应体、请求体以及中间代理大小限制。数据量较大时,先按 `include_tables` 分批迁移;若单表本身过大,再补充分片 procedure而不是恢复 HTTP 文件桥
迁移 JSON 作为 procedure 返回值和 HTTP request body 传递,会受 SpacetimeDB 调用响应体、请求体以及中间代理大小限制。导入端已经内置分片上传来规避 `HTTP 413` 请求体限制;如果导出响应本身过大,仍需先按 `include_tables` 分批导出
`spacetime call` 在 PowerShell 中手写 JSON 容易被剥掉双引号。导入大文件时也不能把完整 JSON 放进命令行参数,否则 Linux 会在启动子进程时返回 `spawn E2BIG`。推荐使用仓库里的 Node 脚本,由脚本直接走 Web API request body避免 shell 二次处理和命令行长度限制。

View File

@@ -0,0 +1,88 @@
# SpacetimeDB 本地 replica identity 不一致处理方案
日期:`2026-04-30`
## 1. 问题
本地启动 SpacetimeDB standalone 时出现:
```text
error starting database: failed to init replica 1 for <new-database-identity>: mismatched database identity: <old-database-identity> != <new-database-identity>
```
本次现场日志中,`server-rs/.spacetimedb/local/data/logs/spacetime-standalone.log` 显示:
1. `2026-04-30T12:17:26Z` 开始按 `c2006f3d846a8259512006a556b1bc3f751a9aef6608fc0ee75788deea6d9331` 启动数据库。
2. `replica 1` 的持久化数据仍带有旧库 `c20037fcfaac4e5c4b1f492f026a4f6119a98f56319b77f21ef021ededf8b7ae`
3. SpacetimeDB 因同一个副本目录中 identity 不一致而拒绝继续启动。
这不是 Rust 编译错误,也不是 `api-server:maincloud` 的 token 错误。只要错误来自 `server-rs/.spacetimedb/local/.../spacetime-standalone.log`,优先按本地 root-dir 数据目录污染处理。
## 2. 根因
`spacetime start --edition standalone` 会在同一个 `--root-dir` 下保存控制库、程序字节、WAL 与 replica 数据。当前仓库默认本地 root-dir 是:
```text
server-rs/.spacetimedb/local
```
当这个目录曾经启动并发布过旧 database identity之后又用同一个 root-dir 初始化或发布到另一个 database identity 时,可能出现:
1. `control-db` 记录的是新库。
2. `data/replicas/1` 里仍残留旧库 WAL 或快照。
3. 启动时 SpacetimeDB 尝试把旧 replica 当作新库加载,触发 `mismatched database identity`
## 3. 处理原则
1. 不在脚本里默认删除 `.spacetimedb` 数据,避免误删本地开发数据。
2. 如果只是本地开发库且数据可丢弃,优先备份后重建 `data` 目录。
3. 如果数据必须保留,不要清理目录;应改回创建旧库时使用的 database/root-dir或先导出迁移数据。
4. Maincloud 发布与本地 standalone root-dir 是两条链路;不要通过切回 `server-node` 或 PostgreSQL 绕过。
## 4. 本地可丢弃数据时的修复
PowerShell
```powershell
$root = "C:\Genarrative\server-rs\.spacetimedb\local"
Get-CimInstance Win32_Process |
Where-Object { $_.Name -match "spacetime" -and $_.CommandLine -and $_.CommandLine.Replace("/", "\") -like "*$($root.Replace("/", "\"))*" } |
Select-Object ProcessId, Name, CommandLine
```
确认占用进程后停止:
```powershell
Stop-Process -Id <pid> -Force
```
备份运行态数据目录:
```powershell
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
Move-Item -LiteralPath "C:\Genarrative\server-rs\.spacetimedb\local\data" -Destination "C:\Genarrative\server-rs\.spacetimedb\local\data.identity-mismatch-backup.$stamp"
```
重新启动本地链路:
```powershell
npm run dev:rust
```
`npm run dev:rust` 会重新启动 standalone、发布 `spacetime-module`,并生成新的本地数据库运行态。
## 5. 需要保留数据时的处理
不要移动或删除 `server-rs/.spacetimedb/local/data`。先确认旧库 identity 对应的数据库名、root-dir 与发布命令,然后选择:
1. 用旧库对应的 database/root-dir 重新启动。
2. 使用迁移导出脚本导出旧数据,再清理本地 root-dir 并导入到新库。
3. 如目标其实是 Maincloud改用 `npm run api-server:maincloud` 连接云端,避免误启动本地 standalone。
## 6. 脚本诊断
`scripts/dev-rust-stack.sh` 已补充本地启动失败诊断:
1. SpacetimeDB 进程在就绪前退出时,会打印 `spacetime-standalone.log` 尾部。
2. 若日志包含 `mismatched database identity`,会提示本地 `data/replicas/1` 与当前 control-db identity 不一致。
3. 诊断只输出建议,不自动清理数据。

View File

@@ -32,7 +32,8 @@ npm run spacetime:publish:maincloud
1. 使用 `cargo build -p spacetime-module --target wasm32-unknown-unknown --release` 构建 wasm。
2. 使用 `spacetime publish <database> --server maincloud --bin-path <wasm> --yes` 发布到 Maincloud。
3. 输出 `api-server` 需要的 Maincloud 环境变量,便于部署进程复用
3. 发布前输出目标数据库名和 server便于在 Jenkins 或手工日志中确认实际发布目标
4. 输出 `api-server` 需要的 Maincloud 环境变量,便于部署进程复用。
如需 schema 冲突时清库发布:
@@ -56,6 +57,7 @@ npm run api-server:maincloud
## 设计约束
- Maincloud 数据库名必须显式配置,不能默认读取本地 `spacetime.local.json`
- Maincloud 数据库名必须匹配 `^[a-z0-9]+(-[a-z0-9]+)*$`,只能使用小写字母、数字,并用单个短横线分隔;否则 `spacetime publish` 会报 `invalid characters in database name`
- 发布脚本只处理 SpacetimeDB 模块发布,不启动本地 SpacetimeDB。
- `api-server` 继续通过 `SpacetimeClientConfig``server_url / database / token` 连接数据库,不在前端增加逻辑。
- Windows 进程清理只能匹配本仓库 `server-rs/target/debug/api-server.exe` 的完整路径,不能按进程名泛化清理,避免影响其他 Rust 服务。

View File

@@ -22,17 +22,18 @@ spacetime sql <db> "SELECT * FROM custom_world_gallery_entry"
| 领域 | 表 |
| --- | --- |
| 迁移权限 | `database_migration_operator` |
| 运维迁移 | `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`
@@ -46,6 +47,13 @@ SELECT * FROM database_migration_operator;
SELECT * FROM database_migration_operator WHERE operator_identity = '<identity>';
```
### `database_migration_import_chunk`
- 作用:大迁移 JSON 分片导入的私有临时表,用于规避单次 HTTP request body 过大导致的 `HTTP 413`;提交成功后由导入 procedure 自动清理,失败时由脚本尽量清理。
- 结构:`chunk_key PK: String`, `upload_id: String`, `chunk_index: u32`, `chunk_count: u32`, `operator_identity: Identity`, `created_at: Timestamp`, `chunk: String`
- 索引:主键 `chunk_key``upload_id`
- 迁移边界:不加入迁移白名单,不导出到业务迁移 JSON。
## 认证表
### `auth_store_snapshot`
@@ -61,8 +69,8 @@ SELECT * FROM auth_store_snapshot WHERE snapshot_id = 'default';
### `user_account`
- 作用:用户账号主表,保存用户名、公开叙世号、手机号掩码、登录方式、密码登录开关和 token 版本。
- 结构:`user_id PK: String`, `public_user_code: String`, `username: String`, `display_name: String`, `phone_number_masked: Option<String>`, `phone_number_e164: Option<String>`, `login_method: String`, `binding_status: String`, `wechat_bound: bool`, `password_hash: Option<String>`, `password_login_enabled: bool`, `token_version: u64`
- 作用:用户账号主表,保存用户名、公开百梦号、手机号掩码、登录方式、密码登录开关和 token 版本。
- 结构:`user_id PK: String`, `public_user_code: String`, `username: String`, `display_name: String`, `avatar_url: Option<String>`, `phone_number_masked: Option<String>`, `phone_number_e164: Option<String>`, `login_method: String`, `binding_status: String`, `wechat_bound: bool`, `password_hash: Option<String>`, `password_login_enabled: bool`, `token_version: u64`
- 索引:`username`, `public_user_code`
```sql
@@ -132,6 +140,7 @@ SELECT * FROM user_browse_history WHERE user_id = '<user_id>' AND owner_user_id
- 作用:个人主页聚合状态,保存钱包余额和总游玩时长。
- 结构:`user_id PK: String`, `wallet_balance: u64`, `total_play_time_ms: u64`, `created_at: Timestamp`, `updated_at: Timestamp`
- 索引:主键 `user_id`
- 新用户注册成功后默认写入 `10` 个光点,余额仍以后端钱包 projection 为准。
```sql
SELECT * FROM profile_dashboard_state WHERE user_id = '<user_id>';
@@ -142,6 +151,7 @@ SELECT * FROM profile_dashboard_state WHERE user_id = '<user_id>';
- 作用:钱包流水账,记录金币/货币变更来源与变更后的余额。
- 结构:`wallet_ledger_id PK: String`, `user_id: String`, `amount_delta: i64`, `balance_after: u64`, `source_type: RuntimeProfileWalletLedgerSourceType`, `created_at: Timestamp`
- 索引:`user_id`, `(user_id, created_at)`
- 注册赠送流水来源为 `new_user_registration_reward`,流水 ID 固定为 `new-user-registration:{user_id}`,用于保证重复调用不重复发放。
```sql
SELECT * FROM profile_wallet_ledger WHERE user_id = '<user_id>';
@@ -150,7 +160,7 @@ SELECT * FROM profile_wallet_ledger WHERE user_id = '<user_id>' ORDER BY created
### `profile_redeem_code`
- 作用:运营发放的叙世币兑换码,支持公共码、唯一码和私有码。
- 作用:运营发放的光点兑换码,支持公共码、唯一码和私有码。
- 结构:`code PK: String`, `mode: RuntimeProfileRedeemCodeMode`, `reward_points: u64`, `max_uses: u32`, `global_used_count: u32`, `enabled: bool`, `allowed_user_ids: Vec<String>`, `created_by: String`, `created_at: Timestamp`, `updated_at: Timestamp`
- 索引:主键 `code`
@@ -204,6 +214,29 @@ 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`
- 作用:用户会员状态表,保存会员状态、档位、起止时间和最近更新时间。
@@ -449,6 +482,7 @@ SELECT * FROM custom_world_gallery_entry WHERE public_work_code = '<public_work_
- 作用:拼图创作 Agent 会话表,保存种子、阶段、锚点包、草稿和已发布 profile。
- 结构:`session_id PK: String`, `owner_user_id: String`, `seed_text: String`, `current_turn: u32`, `progress_percent: u32`, `stage: PuzzleAgentStage`, `anchor_pack_json: String`, `draft_json: Option<String>`, `last_assistant_reply: Option<String>`, `published_profile_id: Option<String>`, `created_at: Timestamp`, `updated_at: Timestamp`
- 说明:填表式拼图入口会在点击“拼图”时立即创建空 session生成草稿前的表单自动保存复用 `seed_text``draft_json`,不新增表字段,`stage` 保持 `CollectingAnchors`
- 索引:`owner_user_id`
```sql
@@ -468,8 +502,10 @@ SELECT * FROM puzzle_agent_message WHERE session_id = '<session_id>' ORDER BY cr
### `puzzle_work_profile`
- 作用:拼图作品主表,保存作品信息、封面、发布状态、游玩次数和锚点包。
- 结构:`profile_id PK: String`, `work_id: String`, `owner_user_id: String`, `source_session_id: Option<String>`, `author_display_name: String`, `level_name: String`, `summary: String`, `theme_tags_json: String`, `cover_image_src: Option<String>`, `cover_asset_id: Option<String>`, `publication_status: PuzzlePublicationStatus`, `play_count: u32`, `anchor_pack_json: String`, `publish_ready: bool`, `created_at: Timestamp`, `updated_at: Timestamp`, `published_at: Option<Timestamp>`
- 作用:拼图作品主表,保存作品信息、多关卡草稿、封面、发布状态、游玩次数和锚点包。
- 结构:`profile_id PK: String`, `work_id: String`, `owner_user_id: String`, `source_session_id: Option<String>`, `author_display_name: String`, `work_title: String`, `work_description: String`, `level_name: String`, `summary: String`, `theme_tags_json: String`, `cover_image_src: Option<String>`, `cover_asset_id: Option<String>`, `levels_json: String`, `publication_status: PuzzlePublicationStatus`, `play_count: u32`, `anchor_pack_json: String`, `publish_ready: bool`, `created_at: Timestamp`, `updated_at: Timestamp`, `published_at: Option<Timestamp>`
- 说明:`work_title`/`work_description` 是作品详情页展示来源;`levels_json` 保存拼图关卡列表,`level_name`/`summary` 继续作为首关兼容字段和旧数据回退来源。
- 说明:拼图初始表单草稿也写入本表作为创作中心卡片投影;未生成图片前 `cover_image_src = None``publish_ready = false`,再次打开草稿时通过 `source_session_id` 恢复表单。
- 索引:`owner_user_id`, `publication_status`
```sql
@@ -512,6 +548,53 @@ SELECT * FROM puzzle_leaderboard_entry WHERE profile_id = '<profile_id>' AND gri
SELECT * FROM puzzle_leaderboard_entry WHERE user_id = '<user_id>' AND profile_id = '<profile_id>' AND grid_size = 4;
```
## 抓大鹅 Match3D 表
### `match3d_agent_session`
- 作用:抓大鹅 Match3D 创作 Agent 会话表,保存种子、配置 JSON、草稿 JSON 和发布 profile 指针。
- 结构:`session_id PK: String`, `owner_user_id: String`, `seed_text: String`, `current_turn: u32`, `progress_percent: u32`, `stage: String`, `config_json: String`, `draft_json: String`, `last_assistant_reply: String`, `published_profile_id: String`, `created_at: Timestamp`, `updated_at: Timestamp`
- 索引:`owner_user_id`
```sql
SELECT * FROM match3d_agent_session WHERE session_id = '<session_id>';
SELECT * FROM match3d_agent_session WHERE owner_user_id = '<user_id>' ORDER BY updated_at DESC;
```
### `match3d_agent_message`
- 作用:抓大鹅 Match3D 创作 Agent 消息流水。
- 结构:`message_id PK: String`, `session_id: String`, `role: String`, `kind: String`, `text: String`, `created_at: Timestamp`
- 索引:`session_id`
```sql
SELECT * FROM match3d_agent_message WHERE session_id = '<session_id>' ORDER BY created_at ASC;
```
### `match3d_work_profile`
- 作用:抓大鹅 Match3D 作品主表,保存作品基础信息、配置、发布状态和游玩次数。
- 结构:`profile_id PK: String`, `owner_user_id: String`, `source_session_id: String`, `author_display_name: String`, `game_name: String`, `theme_text: String`, `summary_text: String`, `tags_json: String`, `cover_image_src: String`, `cover_asset_id: String`, `clear_count: u32`, `difficulty: u32`, `config_json: String`, `publication_status: String`, `play_count: u32`, `updated_at: Timestamp`, `published_at: Option<Timestamp>`
- 索引:`owner_user_id`, `publication_status`
```sql
SELECT * FROM match3d_work_profile WHERE profile_id = '<profile_id>';
SELECT * FROM match3d_work_profile WHERE owner_user_id = '<user_id>' ORDER BY updated_at DESC;
SELECT * FROM match3d_work_profile WHERE publication_status = 'Published';
```
### `match3d_runtime_run`
- 作用:抓大鹅 Match3D 单局运行态表,保存权威快照、快照版本、胜负状态和成绩基础字段。
- 结构:`run_id PK: String`, `owner_user_id: String`, `profile_id: String`, `status: String`, `snapshot_version: u32`, `started_at_ms: i64`, `duration_limit_ms: i64`, `finished_at_ms: i64`, `elapsed_ms: i64`, `clear_count: u32`, `total_item_count: u32`, `cleared_item_count: u32`, `failure_reason: String`, `snapshot_json: String`, `created_at: Timestamp`, `updated_at: Timestamp`
- 索引:`owner_user_id`, `profile_id`
```sql
SELECT * FROM match3d_runtime_run WHERE run_id = '<run_id>';
SELECT * FROM match3d_runtime_run WHERE owner_user_id = '<user_id>' ORDER BY updated_at DESC;
SELECT * FROM match3d_runtime_run WHERE profile_id = '<profile_id>';
```
## 大鱼吃小鱼表
### `big_fish_creation_session`

View File

@@ -39,7 +39,7 @@
- 发布
3. 完整复制外部 TXT 模式的运行机制:
- 玩家游玩会话
- 创作者测试/读档会话
- 百梦主测试/读档会话
- 流式动作执行
- 文本模式显示
- 历史记录
@@ -99,7 +99,7 @@
- 属性面板
5. 双会话机制:
- 玩家游玩会话
- 创作者测试/读档会话
- 百梦主测试/读档会话
6. 流式动作接口与事件协议:
- `start`
- `raw_text`
@@ -551,7 +551,7 @@ TXT 模式后续必须完整落地双会话机制:
1. 玩家游玩会话
- 对应外部 `POST /api/optical/games/session/create`
- 用于正式游玩
2. 创作者测试/读档会话
2. 百梦主测试/读档会话
- 对应外部 `POST /api/visual/session/create`
- 用于测试体验与加载指定存档

View File

@@ -0,0 +1,42 @@
# 作品作者按用户 ID 解析设计 2026-04-30
## 背景
作品列表、公开广场和详情页需要展示作者信息。旧链路里部分作品表会同时写入 `author_display_name`,如果用户后续修改昵称,旧作品仍会显示发布时的昵称快照,造成作者信息不一致。
## 目标
1. 作品作者的真相源统一使用 `owner_user_id`
2. API 返回作品读模型时,通过 `owner_user_id` 读取账号公开资料,并使用最新 `display_name` 作为 `authorDisplayName`
3. `author_display_name` 暂时保留为历史兼容字段,只在用户资料不存在或读取失败时作为回退值。
4. 前端详情页优先展示按 `ownerUserId` 读取到的公开用户资料;作品字段里的作者名只作为兜底展示。
## 落地规则
### SpacetimeDB 存储
1. `custom_world_profile.owner_user_id` / `custom_world_gallery_entry.owner_user_id` 是 RPG 作品作者 ID。
2. `puzzle_work_profile.owner_user_id` 是拼图作品作者 ID。
3. `big_fish_creation_session.owner_user_id` 是大鱼吃小鱼作品作者 ID。
4. 现有 `author_display_name` 不再作为作者真相源,不新增依赖它做权限、同作者推荐或作者资料展示的逻辑。
5. 本次不删除 `author_display_name`,避免破坏历史迁移包、生成绑定和旧客户端兼容;后续若要删除,必须单独做 schema 迁移和绑定刷新。
### API facade
1. 输出 `authorDisplayName` 时先用 `owner_user_id` 查询认证用户表。
2. 查询成功时使用用户最新 `display_name`,并同步补齐 `public_user_code`
3. 查询失败或用户缺失时才回退作品表旧 `author_display_name`
4. 大鱼吃小鱼公开作品不再由前端硬编码作者名API 根据 `owner_user_id` 输出作者显示名。
### 前端
1. 统一作品详情页已按 `ownerUserId` 读取公开用户摘要,用于头像和作者名。
2. 详情页展示作者名时优先使用公开用户摘要的 `displayName`,缺失时回退作品读模型的 `authorDisplayName`
3. 新增作品类型接入平台详情页时,不允许只在前端写固定作者昵称。
## 验收点
1. 用户修改昵称后RPG / 拼图 / 大鱼公开作品列表与详情页能展示新昵称。
2. 旧作品缺少可读取用户资料时,仍能用历史 `author_display_name` 或“玩家”兜底。
3. 作品权限和“同作者”判断继续使用 `owner_user_id`
4. 本次不改变 SpacetimeDB 表结构,因此不需要调整 `migration.rs` 白名单或导入补字段逻辑。