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:
@@ -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. 路由索引与技术文档已同步更新。
|
||||
|
||||
@@ -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 步。验证以实际命令输出为准。
|
||||
+1
-1
@@ -45,7 +45,7 @@
|
||||
修复:
|
||||
|
||||
1. 在 `map_password_entry_error(...)` 中补充 `InvalidPublicUserCode`
|
||||
2. 返回中文错误文案 `叙世号格式不正确`
|
||||
2. 返回中文错误文案 `百梦号格式不正确`
|
||||
|
||||
### 3.3 `module-custom-world` 的 `Display` 分支未覆盖新字段错误
|
||||
|
||||
|
||||
@@ -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 失败,资产操作服务自动发起同额退款。
|
||||
|
||||
@@ -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` 等真实登录请求无法完成。
|
||||
|
||||
@@ -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 邀请码预填与提交。
|
||||
@@ -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 恢复后重新完成一次成功同步。
|
||||
@@ -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>` | 脱敏手机号。 |
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
- 当前场景的核心任务描述。
|
||||
- 文本会作为游戏中首次进入某个场景生成章节任务的关键上下文。
|
||||
- 必须结合场景描述、场景入口钩子、出场角色与 3 幕事件,说明玩家首次进入该场景时要完成什么。
|
||||
- 世界档案的场景详情页必须直接展示该字段,便于创作者确认每个场景的默认章节任务。
|
||||
- 世界档案的场景详情页必须直接展示该字段,便于百梦主确认每个场景的默认章节任务。
|
||||
|
||||
### Landmark 生成源字段
|
||||
|
||||
|
||||
@@ -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 主运行态 |
|
||||
|
||||
## 落地边界
|
||||
|
||||
|
||||
@@ -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 配置,例如:
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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` 重新启动后端,并执行相应自动测试。
|
||||
@@ -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 必跑
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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. 相关测试、类型检查和编码检查通过。
|
||||
@@ -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。
|
||||
@@ -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 client;B5 接入后由后端 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` 通过。
|
||||
@@ -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 运行态试玩入口保留清晰边界。
|
||||
@@ -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 表已纳入 migration,procedure 已接 `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 联调链路单独排查。
|
||||
@@ -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。
|
||||
@@ -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. 移动端弹窗单列可滚动,桌面端接近参考图卡片网格。
|
||||
|
||||
@@ -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`,有已保存头像时展示头像图片,缺失时才回退到首字头像。
|
||||
@@ -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 群短标签。
|
||||
- 弹窗文案只保留必要标签和短提示;本次邀请奖励规则属于必要交易说明,固定展示在邀请码下方。
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user