From f6046ef6589bc9f66538c667cd6633c01ae2ccd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E7=89=A9?= <253518756@qq.com> Date: Thu, 23 Apr 2026 06:01:00 +0800 Subject: [PATCH] 1 --- ...SSET_PLACEHOLDER_PREVIEW_FIX_2026-04-23.md | 134 + ...NTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md | 6 +- ...FISH_FORMAL_IMAGE_GENERATION_2026-04-23.md | 168 + docs/technical/README.md | 2 + server-rs/crates/api-server/src/app.rs | 7 +- server-rs/crates/api-server/src/big_fish.rs | 794 +++- .../api-server/src/legacy_generated_assets.rs | 21 + server-rs/crates/module-big-fish/src/lib.rs | 9 +- server-rs/crates/platform-oss/src/lib.rs | 6 +- server-rs/crates/spacetime-client/src/lib.rs | 2 + .../big_fish_asset_generate_input_type.rs | 1 + .../src/module_bindings/mod.rs | 425 +- server-rs/crates/spacetime-module/src/lib.rs | 17 +- .../BigFishResultView.test.tsx | 149 + .../big-fish-result/BigFishResultView.tsx | 68 +- .../generated/accept_quest_reducer.ts | 21 + .../acknowledge_quest_completion_reducer.ts | 21 + .../advance_puzzle_next_level_procedure.ts | 23 + ...pend_ai_text_chunk_and_return_procedure.ts | 23 + ...ssion_ledger_entry_and_return_procedure.ts | 23 + ...hapter_progression_ledger_entry_reducer.ts | 21 + .../apply_inventory_mutation_reducer.ts | 21 + .../generated/apply_quest_signal_reducer.ts | 21 + ...i_result_reference_and_return_procedure.ts | 23 + ...egin_story_session_and_return_procedure.ts | 23 + .../generated/begin_story_session_reducer.ts | 21 + .../cancel_ai_task_and_return_procedure.ts | 23 + ...orm_browse_history_and_return_procedure.ts | 23 + .../compile_big_fish_draft_procedure.ts | 23 + ...ustom_world_published_profile_procedure.ts | 23 + .../compile_puzzle_agent_draft_procedure.ts | 23 + .../complete_ai_stage_and_return_procedure.ts | 23 + .../complete_ai_task_and_return_procedure.ts | 23 + .../continue_story_and_return_procedure.ts | 23 + .../generated/continue_story_reducer.ts | 21 + .../create_ai_task_and_return_procedure.ts | 23 + .../generated/create_ai_task_reducer.ts | 21 + ...reate_battle_state_and_return_procedure.ts | 23 + .../generated/create_battle_state_reducer.ts | 21 + .../create_big_fish_session_procedure.ts | 23 + ...te_custom_world_agent_session_procedure.ts | 23 + .../create_puzzle_agent_session_procedure.ts | 23 + .../custom_world_gallery_entry_table.ts | 32 + ...stom_world_profile_and_return_procedure.ts | 23 + ...e_runtime_snapshot_and_return_procedure.ts | 23 + .../drag_puzzle_piece_or_group_procedure.ts | 23 + ...ute_custom_world_agent_action_procedure.ts | 23 + .../fail_ai_task_and_return_procedure.ts | 23 + ...stom_world_agent_message_turn_procedure.ts | 23 + .../generate_big_fish_asset_procedure.ts | 23 + .../generated/get_battle_state_procedure.ts | 23 + .../generated/get_big_fish_run_procedure.ts | 23 + .../get_big_fish_session_procedure.ts | 23 + .../get_chapter_progression_procedure.ts | 23 + ...ustom_world_agent_card_detail_procedure.ts | 23 + ..._custom_world_agent_operation_procedure.ts | 23 + ...et_custom_world_agent_session_procedure.ts | 23 + ...t_custom_world_gallery_detail_procedure.ts | 23 + ...t_custom_world_library_detail_procedure.ts | 23 + ...player_progression_or_default_procedure.ts | 23 + .../get_profile_dashboard_procedure.ts | 23 + .../get_profile_play_stats_procedure.ts | 23 + .../get_puzzle_agent_session_procedure.ts | 23 + .../get_puzzle_gallery_detail_procedure.ts | 23 + .../generated/get_puzzle_run_procedure.ts | 23 + .../get_puzzle_work_detail_procedure.ts | 23 + .../get_runtime_inventory_state_procedure.ts | 23 + ...et_runtime_setting_or_default_procedure.ts | 23 + .../get_runtime_snapshot_procedure.ts | 23 + .../get_story_session_state_procedure.ts | 23 + ...ression_experience_and_return_procedure.ts | 23 + ...t_player_progression_experience_reducer.ts | 21 + src/spacetime/generated/index.ts | 224 ++ ..._custom_world_gallery_entries_procedure.ts | 19 + .../list_custom_world_profiles_procedure.ts | 23 + .../list_custom_world_works_procedure.ts | 23 + .../list_platform_browse_history_procedure.ts | 23 + .../list_profile_save_archives_procedure.ts | 23 + .../list_profile_wallet_ledger_procedure.ts | 23 + .../list_puzzle_gallery_procedure.ts | 19 + .../generated/list_puzzle_works_procedure.ts | 23 + .../publish_big_fish_game_procedure.ts | 23 + ...stom_world_profile_and_return_procedure.ts | 23 + .../publish_custom_world_profile_reducer.ts | 21 + .../publish_custom_world_world_procedure.ts | 23 + .../publish_puzzle_work_procedure.ts | 23 + ...olve_combat_action_and_return_procedure.ts | 23 + .../resolve_combat_action_reducer.ts | 21 + ...battle_interaction_and_return_procedure.ts | 23 + ...ve_npc_interaction_and_return_procedure.ts | 23 + .../resolve_npc_interaction_reducer.ts | 21 + ..._npc_social_action_and_return_procedure.ts | 23 + .../resolve_npc_social_action_reducer.ts | 21 + ...easure_interaction_and_return_procedure.ts | 23 + .../resolve_treasure_interaction_reducer.ts | 21 + ...ofile_save_archive_and_return_procedure.ts | 23 + .../save_puzzle_generated_images_procedure.ts | 23 + .../select_puzzle_cover_image_procedure.ts | 23 + .../generated/start_ai_task_reducer.ts | 21 + .../generated/start_ai_task_stage_reducer.ts | 21 + .../generated/start_big_fish_run_procedure.ts | 23 + .../generated/start_puzzle_run_procedure.ts | 23 + .../submit_big_fish_input_procedure.ts | 23 + .../submit_big_fish_message_procedure.ts | 23 + ...it_custom_world_agent_message_procedure.ts | 23 + .../submit_puzzle_agent_message_procedure.ts | 23 + .../generated/swap_puzzle_pieces_procedure.ts | 23 + .../generated/turn_in_quest_reducer.ts | 21 + src/spacetime/generated/types.ts | 3519 +++++++++++++++++ src/spacetime/generated/types/procedures.ts | 243 ++ src/spacetime/generated/types/reducers.ts | 44 + ...stom_world_profile_and_return_procedure.ts | 23 + .../unpublish_custom_world_profile_reducer.ts | 21 + .../generated/update_puzzle_work_procedure.ts | 23 + ...hapter_progression_and_return_procedure.ts | 23 + .../upsert_chapter_progression_reducer.ts | 21 + ...stom_world_profile_and_return_procedure.ts | 23 + .../upsert_custom_world_profile_reducer.ts | 21 + .../upsert_npc_state_and_return_procedure.ts | 23 + .../generated/upsert_npc_state_reducer.ts | 21 + ...orm_browse_history_and_return_procedure.ts | 23 + ...rt_runtime_setting_and_return_procedure.ts | 23 + ...t_runtime_snapshot_and_return_procedure.ts | 23 + 123 files changed, 7752 insertions(+), 436 deletions(-) create mode 100644 docs/technical/BIG_FISH_ASSET_PLACEHOLDER_PREVIEW_FIX_2026-04-23.md create mode 100644 docs/technical/BIG_FISH_FORMAL_IMAGE_GENERATION_2026-04-23.md create mode 100644 src/components/big-fish-result/BigFishResultView.test.tsx create mode 100644 src/spacetime/generated/accept_quest_reducer.ts create mode 100644 src/spacetime/generated/acknowledge_quest_completion_reducer.ts create mode 100644 src/spacetime/generated/advance_puzzle_next_level_procedure.ts create mode 100644 src/spacetime/generated/append_ai_text_chunk_and_return_procedure.ts create mode 100644 src/spacetime/generated/apply_chapter_progression_ledger_entry_and_return_procedure.ts create mode 100644 src/spacetime/generated/apply_chapter_progression_ledger_entry_reducer.ts create mode 100644 src/spacetime/generated/apply_inventory_mutation_reducer.ts create mode 100644 src/spacetime/generated/apply_quest_signal_reducer.ts create mode 100644 src/spacetime/generated/attach_ai_result_reference_and_return_procedure.ts create mode 100644 src/spacetime/generated/begin_story_session_and_return_procedure.ts create mode 100644 src/spacetime/generated/begin_story_session_reducer.ts create mode 100644 src/spacetime/generated/cancel_ai_task_and_return_procedure.ts create mode 100644 src/spacetime/generated/clear_platform_browse_history_and_return_procedure.ts create mode 100644 src/spacetime/generated/compile_big_fish_draft_procedure.ts create mode 100644 src/spacetime/generated/compile_custom_world_published_profile_procedure.ts create mode 100644 src/spacetime/generated/compile_puzzle_agent_draft_procedure.ts create mode 100644 src/spacetime/generated/complete_ai_stage_and_return_procedure.ts create mode 100644 src/spacetime/generated/complete_ai_task_and_return_procedure.ts create mode 100644 src/spacetime/generated/continue_story_and_return_procedure.ts create mode 100644 src/spacetime/generated/continue_story_reducer.ts create mode 100644 src/spacetime/generated/create_ai_task_and_return_procedure.ts create mode 100644 src/spacetime/generated/create_ai_task_reducer.ts create mode 100644 src/spacetime/generated/create_battle_state_and_return_procedure.ts create mode 100644 src/spacetime/generated/create_battle_state_reducer.ts create mode 100644 src/spacetime/generated/create_big_fish_session_procedure.ts create mode 100644 src/spacetime/generated/create_custom_world_agent_session_procedure.ts create mode 100644 src/spacetime/generated/create_puzzle_agent_session_procedure.ts create mode 100644 src/spacetime/generated/custom_world_gallery_entry_table.ts create mode 100644 src/spacetime/generated/delete_custom_world_profile_and_return_procedure.ts create mode 100644 src/spacetime/generated/delete_runtime_snapshot_and_return_procedure.ts create mode 100644 src/spacetime/generated/drag_puzzle_piece_or_group_procedure.ts create mode 100644 src/spacetime/generated/execute_custom_world_agent_action_procedure.ts create mode 100644 src/spacetime/generated/fail_ai_task_and_return_procedure.ts create mode 100644 src/spacetime/generated/finalize_custom_world_agent_message_turn_procedure.ts create mode 100644 src/spacetime/generated/generate_big_fish_asset_procedure.ts create mode 100644 src/spacetime/generated/get_battle_state_procedure.ts create mode 100644 src/spacetime/generated/get_big_fish_run_procedure.ts create mode 100644 src/spacetime/generated/get_big_fish_session_procedure.ts create mode 100644 src/spacetime/generated/get_chapter_progression_procedure.ts create mode 100644 src/spacetime/generated/get_custom_world_agent_card_detail_procedure.ts create mode 100644 src/spacetime/generated/get_custom_world_agent_operation_procedure.ts create mode 100644 src/spacetime/generated/get_custom_world_agent_session_procedure.ts create mode 100644 src/spacetime/generated/get_custom_world_gallery_detail_procedure.ts create mode 100644 src/spacetime/generated/get_custom_world_library_detail_procedure.ts create mode 100644 src/spacetime/generated/get_player_progression_or_default_procedure.ts create mode 100644 src/spacetime/generated/get_profile_dashboard_procedure.ts create mode 100644 src/spacetime/generated/get_profile_play_stats_procedure.ts create mode 100644 src/spacetime/generated/get_puzzle_agent_session_procedure.ts create mode 100644 src/spacetime/generated/get_puzzle_gallery_detail_procedure.ts create mode 100644 src/spacetime/generated/get_puzzle_run_procedure.ts create mode 100644 src/spacetime/generated/get_puzzle_work_detail_procedure.ts create mode 100644 src/spacetime/generated/get_runtime_inventory_state_procedure.ts create mode 100644 src/spacetime/generated/get_runtime_setting_or_default_procedure.ts create mode 100644 src/spacetime/generated/get_runtime_snapshot_procedure.ts create mode 100644 src/spacetime/generated/get_story_session_state_procedure.ts create mode 100644 src/spacetime/generated/grant_player_progression_experience_and_return_procedure.ts create mode 100644 src/spacetime/generated/grant_player_progression_experience_reducer.ts create mode 100644 src/spacetime/generated/list_custom_world_gallery_entries_procedure.ts create mode 100644 src/spacetime/generated/list_custom_world_profiles_procedure.ts create mode 100644 src/spacetime/generated/list_custom_world_works_procedure.ts create mode 100644 src/spacetime/generated/list_platform_browse_history_procedure.ts create mode 100644 src/spacetime/generated/list_profile_save_archives_procedure.ts create mode 100644 src/spacetime/generated/list_profile_wallet_ledger_procedure.ts create mode 100644 src/spacetime/generated/list_puzzle_gallery_procedure.ts create mode 100644 src/spacetime/generated/list_puzzle_works_procedure.ts create mode 100644 src/spacetime/generated/publish_big_fish_game_procedure.ts create mode 100644 src/spacetime/generated/publish_custom_world_profile_and_return_procedure.ts create mode 100644 src/spacetime/generated/publish_custom_world_profile_reducer.ts create mode 100644 src/spacetime/generated/publish_custom_world_world_procedure.ts create mode 100644 src/spacetime/generated/publish_puzzle_work_procedure.ts create mode 100644 src/spacetime/generated/resolve_combat_action_and_return_procedure.ts create mode 100644 src/spacetime/generated/resolve_combat_action_reducer.ts create mode 100644 src/spacetime/generated/resolve_npc_battle_interaction_and_return_procedure.ts create mode 100644 src/spacetime/generated/resolve_npc_interaction_and_return_procedure.ts create mode 100644 src/spacetime/generated/resolve_npc_interaction_reducer.ts create mode 100644 src/spacetime/generated/resolve_npc_social_action_and_return_procedure.ts create mode 100644 src/spacetime/generated/resolve_npc_social_action_reducer.ts create mode 100644 src/spacetime/generated/resolve_treasure_interaction_and_return_procedure.ts create mode 100644 src/spacetime/generated/resolve_treasure_interaction_reducer.ts create mode 100644 src/spacetime/generated/resume_profile_save_archive_and_return_procedure.ts create mode 100644 src/spacetime/generated/save_puzzle_generated_images_procedure.ts create mode 100644 src/spacetime/generated/select_puzzle_cover_image_procedure.ts create mode 100644 src/spacetime/generated/start_ai_task_reducer.ts create mode 100644 src/spacetime/generated/start_ai_task_stage_reducer.ts create mode 100644 src/spacetime/generated/start_big_fish_run_procedure.ts create mode 100644 src/spacetime/generated/start_puzzle_run_procedure.ts create mode 100644 src/spacetime/generated/submit_big_fish_input_procedure.ts create mode 100644 src/spacetime/generated/submit_big_fish_message_procedure.ts create mode 100644 src/spacetime/generated/submit_custom_world_agent_message_procedure.ts create mode 100644 src/spacetime/generated/submit_puzzle_agent_message_procedure.ts create mode 100644 src/spacetime/generated/swap_puzzle_pieces_procedure.ts create mode 100644 src/spacetime/generated/turn_in_quest_reducer.ts create mode 100644 src/spacetime/generated/unpublish_custom_world_profile_and_return_procedure.ts create mode 100644 src/spacetime/generated/unpublish_custom_world_profile_reducer.ts create mode 100644 src/spacetime/generated/update_puzzle_work_procedure.ts create mode 100644 src/spacetime/generated/upsert_chapter_progression_and_return_procedure.ts create mode 100644 src/spacetime/generated/upsert_chapter_progression_reducer.ts create mode 100644 src/spacetime/generated/upsert_custom_world_profile_and_return_procedure.ts create mode 100644 src/spacetime/generated/upsert_custom_world_profile_reducer.ts create mode 100644 src/spacetime/generated/upsert_npc_state_and_return_procedure.ts create mode 100644 src/spacetime/generated/upsert_npc_state_reducer.ts create mode 100644 src/spacetime/generated/upsert_platform_browse_history_and_return_procedure.ts create mode 100644 src/spacetime/generated/upsert_runtime_setting_and_return_procedure.ts create mode 100644 src/spacetime/generated/upsert_runtime_snapshot_and_return_procedure.ts diff --git a/docs/technical/BIG_FISH_ASSET_PLACEHOLDER_PREVIEW_FIX_2026-04-23.md b/docs/technical/BIG_FISH_ASSET_PLACEHOLDER_PREVIEW_FIX_2026-04-23.md new file mode 100644 index 00000000..3f026830 --- /dev/null +++ b/docs/technical/BIG_FISH_ASSET_PLACEHOLDER_PREVIEW_FIX_2026-04-23.md @@ -0,0 +1,134 @@ +# 大鱼吃小鱼结果页主图占位预览修复说明 2026-04-23 + +日期:`2026-04-23` + +## 1. 问题现象 + +在“深海谜境 / 大鱼吃小鱼”结果页中,等级卡片会显示: + +1. `主图 已生成` +2. 操作后会出现“已应用主图”感知 + +但实际结果页看不到角色主图,卡片里只有一张蓝色底图。 + +## 2. 排查结论 + +本次沿着“结果页展示 -> API action -> SpacetimeDB procedure -> 资产路径”全链检查后确认: + +1. 前端确实成功触发了 `big_fish_generate_level_main_image` +2. SpacetimeDB 侧确实把资产槽位状态写成了 `ready` +3. 但这条链路没有接真实图像模型,也没有接 OSS 真实资产对象 +4. 旧实现只回写一个 `/generated-big-fish/...png` 占位 URL +5. 同时仓库里没有实际把这张占位图写到 `public/generated-big-fish/...` +6. 因此前端读到的是一个“看起来像图片地址、实际上没有真实文件”的路径 +7. `` 加载失败后,卡片底层蓝色渐变背景暴露出来,于是用户只能看到蓝色图块 + +## 3. 根因拆解 + +### 3.1 状态成功过早 + +`generate_big_fish_asset` 当前最小实现只负责: + +1. 写 `asset slot` +2. 写 `prompt snapshot` +3. 标记 `status = ready` + +它并不代表真实主图已经生成完成。 + +### 3.2 预览资源未真正落盘 + +旧实现会构造: + +`/generated-big-fish/{asset_kind}/{level_part}/{seed}.png` + +但没有同步在 `public/generated-big-fish/...` 写出对应文件。 + +### 3.3 结果页直接吃裸 `assetUrl` + +Big Fish 结果页主图卡之前直接: + +1. 取 `slot.assetUrl` +2. 塞进 `` + +一旦文件不存在,就只剩下卡片自己的蓝色渐变背景。 + +### 3.4 UI 文案误导 + +旧文案把当前阶段写成: + +1. `已生成` +2. `已生成并设为正式资产` + +这会让用户自然理解为“真实主图已经出来了”,与当前最小实现不一致。 + +## 4. 本次修复策略 + +本轮不直接接真实模型生成,而是先把最小可见闭环补完整。 + +### 4.1 API action 写出可预览占位图 + +在 Rust `api-server` 的 Big Fish action 处理中: + +1. 复用拼图玩法“写本地可预览占位图”的方式 +2. 在调用 `generate_big_fish_asset` 之前,先把 Big Fish 占位图真正写到: + +`public/generated-big-fish/...` + +3. 保证 SpacetimeDB 里写入的占位 URL 至少对应一个真实可访问文件 + +### 4.2 结果页改用统一图片渲染组件 + +Big Fish 结果页主图和背景预览改为: + +1. 使用 `ResolvedAssetImage` +2. 统一走现有图片渲染链路 +3. 避免后续接 OSS / 旧 generated 路径兼容时再重复返工 + +### 4.3 状态文案改准 + +当前还是占位资产阶段,因此把结果页状态文案改为: + +1. `占位已生成` +2. `生成并应用占位图` + +避免继续把“槽位 ready”误说成“真实主图已完成”。 + +### 4.4 结果页露出背景预览 + +除了等级主图卡,本轮顺手把场地背景卡也接上真实预览图渲染,避免出现: + +1. 状态显示完成 +2. 右侧仍只是一块纯渐变底图 + +## 5. 修复后的链路语义 + +修复后 Big Fish 当前资产链路语义明确为: + +1. 点击生成 +2. `api-server` 先写出本地可访问占位图 +3. `spacetime-module` 写正式资产槽位和提示词快照 +4. 前端读取 `assetUrl` 并真实渲染预览 + +也就是说: + +1. 当前可以保证“用户看得见” +2. 但仍然不是“真实模型图像生成” +3. 后续真实模型 / OSS worker 接入后,再把占位图链替换成正式资产真相链 + +## 6. 验收标准 + +本次修复后需要满足: + +1. 结果页等级卡在主图生成后能直接看到真实可加载图片,不再只剩蓝色底图 +2. 场地背景生成后右侧卡片能看到真实可加载图片 +3. Big Fish 结果页图片渲染统一走现有图片组件,不再直接裸 `` 吃易失路径 +4. 当前 UI 文案不再把占位图误称为真实主图 +5. 若本地占位图写盘失败,则 action 不能继续回到“ready 成功”状态 + +## 7. 后续建议 + +下一阶段继续补 Big Fish 真实资产链时,建议按下面顺序推进: + +1. 先引入 Big Fish `asset_object + asset_entity_binding` 正式槽位设计 +2. 再接真实图片 / 动作生成 worker +3. 最后把当前 `/generated-big-fish/...` 本地占位路径迁为兼容层,而不是继续作为真相路径 diff --git a/docs/technical/BIG_FISH_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md b/docs/technical/BIG_FISH_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md index 51f95f8f..9ad29494 100644 --- a/docs/technical/BIG_FISH_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md +++ b/docs/technical/BIG_FISH_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md @@ -11,14 +11,14 @@ 1. 平台创作入口选择大鱼吃小鱼玩法 2. Agent 会话创建、消息提交和 SSE 兼容返回 3. 基于 4 个高杠杆锚点编译玩法草稿 -4. 结果页生成等级主图、等级动作、场地背景的正式资产槽位 +4. 结果页生成等级主图、等级动作、场地背景的正式资产槽位,并同步提供可预览资源 5. 发布校验 6. 启动测试运行态 7. 后端推进摇杆输入、刷怪、吞噬收编、三合一、屏外清理和胜负裁决 ## 2. 本轮明确不做 -1. 不接入真实图片 / 动作模型调用,只生成可预览的占位资产引用与冻结提示词快照。 +1. 不在本文件内展开正式图片模型链、OSS 真相链和占位兼容层的细节;相关正式出图方案以 `BIG_FISH_FORMAL_IMAGE_GENERATION_2026-04-23.md` 为准。 2. 不新增 WebSocket 依赖;首版运行态使用 `POST input + GET snapshot` 的有限 HTTP 辅助接口,后续再升级长连接。 3. 不把 Big Fish 写回 `custom_world`、`rpgCreation` 或 RPG runtime 旧语义。 4. 不新增作品市场、排行榜、复盘、局外成长、PvP。 @@ -141,7 +141,7 @@ 说明: 1. `submit_big_fish_message` 只做 deterministic 锚点补全,不调用 LLM。 -2. `generate_big_fish_asset` 只写正式资产槽位和提示词快照,真实模型生成后续由 AI/OSS worker 替换。 +2. `generate_big_fish_asset` 的槽位写入语义允许 `api-server` 传入正式 `asset_url`;若未传则回退为占位路径,保证最小链与正式链共存。 3. `submit_big_fish_input` 每次至少推进 1 个后端 tick,前端不能本地裁决。 4. 运行态所有“持续时间”语义按真实秒数累计,前端即使摇杆静止也要持续以当前输入心跳驱动后端推进,避免刷怪与屏外 `3` 秒清理依赖手速或提交频率。 diff --git a/docs/technical/BIG_FISH_FORMAL_IMAGE_GENERATION_2026-04-23.md b/docs/technical/BIG_FISH_FORMAL_IMAGE_GENERATION_2026-04-23.md new file mode 100644 index 00000000..2c27feff --- /dev/null +++ b/docs/technical/BIG_FISH_FORMAL_IMAGE_GENERATION_2026-04-23.md @@ -0,0 +1,168 @@ +# 大鱼吃小鱼正式图片生成接入方案 2026-04-23 + +日期:`2026-04-23` + +## 1. 文档目的 + +在 `2026-04-23` 早些时候,我们已经修复了 Big Fish 结果页“显示已生成但只能看到蓝色底图”的问题,先让占位图真正可见。 + +这份文档继续冻结下一步方案:把 Big Fish 结果页从“占位可见”升级为“模型正式出图”,并且复用仓库现有的 Rust 图片生成与 OSS 真相链,不再为 Big Fish 单独发明一套新资产系统。 + +## 2. 当前问题复盘 + +上一阶段虽然解决了“看不见图”的问题,但本质仍是占位链: + +1. `api-server` 先写本地 `public/generated-big-fish/*` 占位 PNG。 +2. `spacetime-module` 把 `big_fish_asset_slot.status` 写成 `ready`。 +3. `asset_url` 写的是 `/generated-big-fish/...`。 +4. 结果页能看到图片,但那只是占位预览,不是真实模型图。 + +这意味着: + +1. 用户在结果页看到的“主图 / 动作 / 背景”仍不是正式资产。 +2. `ready` 语义对 Big Fish 来说仍然偏弱,只表示“槽位上已有可预览资源”,不等同于“模型正式资产已落 OSS 真相链”。 + +## 3. 本次目标 + +本次把以下三类 Big Fish 资产切到正式图片生成链: + +1. `level_main_image` +2. `level_motion` +3. `stage_background` + +当前只接“正式静态图片生成”,不在这一轮扩视频、逐帧序列或动作 manifest。 + +原因是: + +1. Big Fish 结果页当前只消费单个 `assetUrl`。 +2. 运行态和结果页目前都按静态图预览设计。 +3. 先把正式主图链闭合,比提前引入一套未被消费的视频协议更稳。 + +## 4. 统一真相链 + +Big Fish 正式图片生成统一复用现有 Rust 主链: + +1. `api-server` 根据 Big Fish 草稿 prompt 调用 DashScope 文生图。 +2. Rust 下载远端图片二进制。 +3. Rust 上传到私有 OSS。 +4. Rust 调用 `confirm_asset_object` 确认正式对象。 +5. Rust 调用 `bind_asset_object_to_entity` 绑定 Big Fish 业务槽位。 +6. Rust 再调用 Big Fish procedure,把 `big_fish_asset_slot.asset_url` 写成正式兼容路径。 +7. 前端继续通过 `ResolvedAssetImage` 和 `/api/assets/read-url` 消费图片。 + +## 5. 路径策略 + +### 5.1 占位路径继续保留 + +占位图路径继续保持: + +`/generated-big-fish/*` + +它只代表: + +1. 本地开发态占位可见资源。 +2. 旧的最小预览兼容层。 + +### 5.2 正式图片使用新前缀 + +正式 Big Fish 图片统一写到新的 OSS legacy 兼容前缀: + +`/generated-big-fish-assets/*` + +这样可以同时满足: + +1. 与仓库现有 `/generated-*` 兼容代理体系一致。 +2. 不会被前端继续误判成占位图。 +3. 后续可继续通过 `LegacyAssetPrefix`、`/api/assets/read-url`、`ResolvedAssetImage` 复用现有链路。 + +## 6. SpacetimeDB 语义调整 + +### 6.1 Big Fish 资产生成输入补充 `asset_url` + +当前 `BigFishAssetGenerateInput` 只有: + +1. `asset_kind` +2. `level` +3. `motion_key` +4. `generated_at_micros` + +这会导致 procedure 无法知道 API 层是否已经拿到了正式 OSS 兼容路径。 + +因此本次补充: + +1. `asset_url: Option` + +### 6.2 槽位写入规则 + +`build_generated_asset_slot(...)` 改为: + +1. 若输入提供 `asset_url`,则直接写正式路径。 +2. 若输入未提供 `asset_url`,才回退为 `/generated-big-fish/...` 占位路径。 + +这样做的原因是: + +1. 允许同一个 Big Fish procedure 兼容“占位生成”和“正式生成”两种调用方式。 +2. 不需要为了正式图片再新增一条平行 procedure。 + +## 7. Big Fish 与 `asset_object` 的绑定语义 + +Big Fish 不新增专门资产表,继续复用: + +1. `asset_object` +2. `asset_entity_binding` + +绑定原则: + +1. `entity_kind` 使用 Big Fish 会话实体语义。 +2. `entity_id` 使用 `session_id`。 +3. `slot` 使用稳定可重建的槽位名。 + +推荐槽位命名: + +1. `level_main_image:level-{n}` +2. `level_motion:level-{n}:{motion_key}` +3. `stage_background` + +这样做可以: + +1. 与 `big_fish_asset_slot` 一一对应。 +2. 让后续真正做“重新生成覆盖旧资产”时有稳定槽位。 + +## 8. 前端识别语义 + +当前 `BigFishResultView` 仍用路径前缀判断是否为占位图: + +1. 包含 `/generated-big-fish/` -> `占位已生成` +2. 否则 -> `已生成` + +本轮先保留这个最小判定方式,原因是: + +1. 正式图片会改走 `/generated-big-fish-assets/`。 +2. 前端无需立即扩 contract 字段也能正确显示状态。 + +长期建议仍然是给 Big Fish 资产槽位补显式来源字段,但这不阻塞本轮正式出图。 + +## 9. 本轮验收标准 + +完成后需要满足: + +1. `big_fish_generate_level_main_image` 会实际触发模型生成,并返回正式 Big Fish 图片。 +2. `big_fish_generate_level_motion` 会实际触发模型生成,并返回静态动作预览图。 +3. `big_fish_generate_stage_background` 会实际触发模型生成,并返回正式背景图。 +4. SpacetimeDB 中对应 `big_fish_asset_slot.asset_url` 不再是 `/generated-big-fish/*`,而是 `/generated-big-fish-assets/*`。 +5. 结果页状态从“占位已生成”切到“已生成”。 +6. `/generated-big-fish-assets/*` 能通过 Rust 同源代理正确读取 OSS 私有对象。 +7. `cargo check -p api-server` +8. `cargo check -p module-big-fish` +9. `cargo check -p spacetime-module` +10. `spacetime generate` +11. `cargo check -p spacetime-client` +12. `npm run check:encoding` + +## 10. 本轮明确暂不做 + +1. 不做视频动作生成。 +2. 不做序列帧 manifest。 +3. 不新增 Big Fish 专属资产数据库表。 +4. 不把 Big Fish 结果页改成复杂工作流编辑器。 +5. 不修改现有占位图路径的兼容职责。 diff --git a/docs/technical/README.md b/docs/technical/README.md index 8384d590..f4bac4f9 100644 --- a/docs/technical/README.md +++ b/docs/technical/README.md @@ -56,6 +56,8 @@ - [SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STAGE7_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STAGE7_DESIGN_2026-04-22.md):冻结 `M5` Agent `message submit / operation query` 的 deterministic 最小闭环,明确同步写入 user/assistant 消息、`process_message` operation 与 session 进度推进规则。 - [SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STREAM_STAGE8_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STREAM_STAGE8_DESIGN_2026-04-22.md):冻结 `M5` Agent `/messages/stream` 的最小兼容 SSE facade,明确复用 Stage 7 的同步写表逻辑,只输出当前前端真实消费的 `reply_delta / session / done / error` 事件。 - [BIG_FISH_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md](./BIG_FISH_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md):冻结大鱼吃小鱼玩法本轮最小完整落地方案,明确 `module-big-fish`、SpacetimeDB 表 / procedure、Axum facade、前端接入和运行态规则边界。 +- [BIG_FISH_ASSET_PLACEHOLDER_PREVIEW_FIX_2026-04-23.md](./BIG_FISH_ASSET_PLACEHOLDER_PREVIEW_FIX_2026-04-23.md):记录大鱼吃小鱼结果页“状态成功但只看到蓝色底图”的根因,冻结占位图真实写盘、结果页预览渲染与文案收口方案。 +- [BIG_FISH_FORMAL_IMAGE_GENERATION_2026-04-23.md](./BIG_FISH_FORMAL_IMAGE_GENERATION_2026-04-23.md):冻结 Big Fish 从“占位可见”升级到“模型正式出图”的 Rust 落地方案,明确复用 DashScope + OSS + `asset_object` 真相链、新的 `/generated-big-fish-assets/*` 兼容路径,以及 Big Fish 槽位写入语义调整。 - [PUZZLE_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md](./PUZZLE_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md):冻结拼图玩法本轮最小完整落地方案,明确 `module-puzzle`、SpacetimeDB 表 / procedure、Axum facade、前端接入,以及交换 / 合并 / 拖动 / 拆分 / 下一关推荐边界。 - [UNIFIED_CREATION_AGENT_CHAT_FRAMEWORK_2026-04-22.md](./UNIFIED_CREATION_AGENT_CHAT_FRAMEWORK_2026-04-22.md):冻结所有创作品类 Agent 聊天 UI 与对话进度管理统一框架,明确品类差异只保留锚点映射、提示词/话术和 action。 - [RUST_API_SERVER_SSE_INFRASTRUCTURE_DESIGN_2026-04-22.md](./RUST_API_SERVER_SSE_INFRASTRUCTURE_DESIGN_2026-04-22.md):冻结 `server-rs/crates/api-server` 的 SSE 使用口径,明确统一使用 Axum 内建 `Sse`,不再保留自定义 `sse.rs` 模块。 diff --git a/server-rs/crates/api-server/src/app.rs b/server-rs/crates/api-server/src/app.rs index 011bc83c..4ec48b83 100644 --- a/server-rs/crates/api-server/src/app.rs +++ b/server-rs/crates/api-server/src/app.rs @@ -57,7 +57,8 @@ use crate::{ error_middleware::normalize_error_response, health::health_check, legacy_generated_assets::{ - proxy_generated_animations, proxy_generated_character_drafts, proxy_generated_characters, + proxy_generated_animations, proxy_generated_big_fish_assets, + proxy_generated_character_drafts, proxy_generated_characters, proxy_generated_custom_world_covers, proxy_generated_custom_world_scenes, proxy_generated_qwen_sprites, }, @@ -137,6 +138,10 @@ pub fn build_router(state: AppState) -> Router { "/generated-animations/{*path}", get(proxy_generated_animations), ) + .route( + "/generated-big-fish-assets/{*path}", + get(proxy_generated_big_fish_assets), + ) .route( "/generated-custom-world-scenes/{*path}", get(proxy_generated_custom_world_scenes), diff --git a/server-rs/crates/api-server/src/big_fish.rs b/server-rs/crates/api-server/src/big_fish.rs index cdab1d9f..a1ef305a 100644 --- a/server-rs/crates/api-server/src/big_fish.rs +++ b/server-rs/crates/api-server/src/big_fish.rs @@ -1,10 +1,22 @@ +use std::{ + collections::BTreeMap, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, +}; + use axum::{ Json, extract::{Extension, Path, State, rejection::JsonRejection}, http::{HeaderName, StatusCode, header}, response::{IntoResponse, Response}, }; -use serde_json::{Value, json}; +use module_assets::{ + AssetObjectAccessPolicy, AssetObjectFieldError, build_asset_entity_binding_input, + build_asset_object_upsert_input, generate_asset_binding_id, generate_asset_object_id, +}; +use platform_oss::{ + LegacyAssetPrefix, OssHeadObjectRequest, OssObjectAccess, OssPutObjectRequest, +}; +use serde_json::{Map, Value, json}; use shared_contracts::big_fish::{ BigFishActionResponse, BigFishAgentMessageResponse, BigFishAnchorItemResponse, BigFishAnchorPackResponse, BigFishAssetCoverageResponse, BigFishAssetSlotResponse, @@ -24,6 +36,7 @@ use spacetime_client::{ BigFishSessionCreateRecordInput, BigFishSessionRecord, BigFishVector2Record, SpacetimeClientError, }; +use tokio::time::sleep; use crate::{ api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError, @@ -233,6 +246,17 @@ pub async fn execute_big_fish_action( .await } "big_fish_generate_level_main_image" => { + let asset_url = generate_big_fish_formal_asset( + &state, + &owner_user_id, + &session_id, + "level_main_image", + payload.level, + None, + now, + ) + .await + .map_err(|error| big_fish_error_response(&request_context, error))?; state .spacetime_client() .generate_big_fish_asset(BigFishAssetGenerateRecordInput { @@ -241,11 +265,23 @@ pub async fn execute_big_fish_action( asset_kind: "level_main_image".to_string(), level: payload.level, motion_key: None, + asset_url: Some(asset_url), generated_at_micros: now, }) .await } "big_fish_generate_level_motion" => { + let asset_url = generate_big_fish_formal_asset( + &state, + &owner_user_id, + &session_id, + "level_motion", + payload.level, + payload.motion_key.as_deref(), + now, + ) + .await + .map_err(|error| big_fish_error_response(&request_context, error))?; state .spacetime_client() .generate_big_fish_asset(BigFishAssetGenerateRecordInput { @@ -254,11 +290,23 @@ pub async fn execute_big_fish_action( asset_kind: "level_motion".to_string(), level: payload.level, motion_key: payload.motion_key, + asset_url: Some(asset_url), generated_at_micros: now, }) .await } "big_fish_generate_stage_background" => { + let asset_url = generate_big_fish_formal_asset( + &state, + &owner_user_id, + &session_id, + "stage_background", + None, + None, + now, + ) + .await + .map_err(|error| big_fish_error_response(&request_context, error))?; state .spacetime_client() .generate_big_fish_asset(BigFishAssetGenerateRecordInput { @@ -267,6 +315,7 @@ pub async fn execute_big_fish_action( asset_kind: "stage_background".to_string(), level: None, motion_key: None, + asset_url: Some(asset_url), generated_at_micros: now, }) .await @@ -588,6 +637,747 @@ fn build_big_fish_welcome_text(seed_text: &str) -> String { "我已经收到你的玩法起点,会先把它整理成锚点并准备结果页草稿。".to_string() } +struct BigFishDashScopeSettings { + base_url: String, + api_key: String, + request_timeout_ms: u64, +} + +struct BigFishGeneratedImage { + image_url: String, + task_id: String, +} + +struct BigFishDownloadedImage { + mime_type: String, + extension: String, + bytes: Vec, +} + +struct BigFishFormalAssetContext { + entity_id: String, + prompt: String, + negative_prompt: String, + size: String, + asset_object_kind: String, + binding_slot: String, + path_segments: Vec, +} + +const BIG_FISH_TEXT_TO_IMAGE_MODEL: &str = "wan2.2-t2i-flash"; +const BIG_FISH_ENTITY_KIND: &str = "big_fish_session"; +const BIG_FISH_DEFAULT_NEGATIVE_PROMPT: &str = + "文字,水印,logo,UI界面,对话框,边框,多余肢体,畸形鱼体,低清晰度,模糊,压缩噪点,现代摄影棚,写实照片背景"; + +async fn generate_big_fish_formal_asset( + state: &AppState, + owner_user_id: &str, + session_id: &str, + asset_kind: &str, + level: Option, + motion_key: Option<&str>, + generated_at_micros: i64, +) -> Result { + let session = state + .spacetime_client() + .get_big_fish_session(session_id.to_string(), owner_user_id.to_string()) + .await + .map_err(map_big_fish_client_error)?; + let draft = session.draft.as_ref().ok_or_else(|| { + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "big-fish", + "message": "玩法草稿尚未编译,不能生成正式图片。", + })) + })?; + let context = build_big_fish_formal_asset_context( + &session, + draft, + asset_kind, + level, + motion_key, + generated_at_micros, + )?; + let settings = require_big_fish_dashscope_settings(state)?; + let http_client = build_big_fish_dashscope_http_client(&settings)?; + let generated = create_big_fish_text_to_image_generation( + &http_client, + &settings, + context.prompt.as_str(), + context.negative_prompt.as_str(), + context.size.as_str(), + ) + .await?; + let downloaded = download_big_fish_remote_image( + &http_client, + generated.image_url.as_str(), + "下载 Big Fish 正式图片失败", + ) + .await?; + + persist_big_fish_formal_asset( + state, + owner_user_id, + &context, + generated, + downloaded, + generated_at_micros, + ) + .await +} + +fn build_big_fish_formal_asset_context( + session: &BigFishSessionRecord, + draft: &BigFishGameDraftRecord, + asset_kind: &str, + level: Option, + motion_key: Option<&str>, + generated_at_micros: i64, +) -> Result { + let asset_id = format!("asset-{generated_at_micros}"); + match asset_kind { + "level_main_image" => { + let level = find_big_fish_level_blueprint(draft, level)?; + let level_part = build_big_fish_level_part(Some(level.level)); + Ok(BigFishFormalAssetContext { + entity_id: session.session_id.clone(), + prompt: build_big_fish_level_main_image_prompt(draft, level), + negative_prompt: BIG_FISH_DEFAULT_NEGATIVE_PROMPT.to_string(), + size: "1024*1024".to_string(), + asset_object_kind: "big_fish_level_main_image".to_string(), + binding_slot: format!("level_main_image:{level_part}"), + path_segments: vec![ + sanitize_big_fish_path_segment(session.session_id.as_str(), "session"), + "level-main-image".to_string(), + level_part, + asset_id, + ], + }) + } + "level_motion" => { + let level = find_big_fish_level_blueprint(draft, level)?; + let motion_key = motion_key + .map(str::trim) + .filter(|value| matches!(*value, "idle_float" | "move_swim")) + .ok_or_else(|| { + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "big-fish", + "message": "motionKey 必须是 idle_float 或 move_swim。", + })) + })?; + let level_part = build_big_fish_level_part(Some(level.level)); + Ok(BigFishFormalAssetContext { + entity_id: session.session_id.clone(), + prompt: build_big_fish_level_motion_prompt(draft, level, motion_key), + negative_prompt: BIG_FISH_DEFAULT_NEGATIVE_PROMPT.to_string(), + size: "1024*1024".to_string(), + asset_object_kind: "big_fish_level_motion".to_string(), + binding_slot: format!("level_motion:{level_part}:{motion_key}"), + path_segments: vec![ + sanitize_big_fish_path_segment(session.session_id.as_str(), "session"), + "level-motion".to_string(), + level_part, + sanitize_big_fish_path_segment(motion_key, "motion"), + asset_id, + ], + }) + } + "stage_background" => Ok(BigFishFormalAssetContext { + entity_id: session.session_id.clone(), + prompt: build_big_fish_stage_background_prompt(draft), + negative_prompt: BIG_FISH_DEFAULT_NEGATIVE_PROMPT.to_string(), + size: "720*1280".to_string(), + asset_object_kind: "big_fish_stage_background".to_string(), + binding_slot: "stage_background".to_string(), + path_segments: vec![ + sanitize_big_fish_path_segment(session.session_id.as_str(), "session"), + "stage-background".to_string(), + asset_id, + ], + }), + _ => Err(AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "big-fish", + "message": format!("assetKind `{asset_kind}` 不支持正式图片生成。"), + }))), + } +} + +fn find_big_fish_level_blueprint( + draft: &BigFishGameDraftRecord, + level: Option, +) -> Result<&BigFishLevelBlueprintRecord, AppError> { + let level = level.ok_or_else(|| { + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "big-fish", + "message": "level 是等级资产生成的必填项。", + })) + })?; + draft + .levels + .iter() + .find(|blueprint| blueprint.level == level) + .ok_or_else(|| { + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "big-fish", + "message": format!("level `{level}` 不存在于当前 Big Fish 草稿。"), + })) + }) +} + +fn build_big_fish_level_main_image_prompt( + draft: &BigFishGameDraftRecord, + level: &BigFishLevelBlueprintRecord, +) -> String { + vec![ + format!( + "为竖屏移动游戏《{}》生成一张等级生物主图。", + draft.title + ), + format!( + "生态主题:{}。核心乐趣:{}。", + draft.ecology_theme, draft.core_fun + ), + format!( + "等级:Lv.{},名称:{},幻想描述:{}。", + level.level, level.name, level.one_line_fantasy + ), + format!("轮廓方向:{}。", level.silhouette_direction), + format!("视觉提示词种子:{}。", level.visual_prompt_seed), + "画面要求:单体游戏生物完整入镜,轮廓清晰,适合作为大鱼吃小鱼等级角色主图,2D 高完成度游戏插画,深海发光质感,中央构图。".to_string(), + "不要出现 UI、文字、logo、水印、对话框或边框;背景保持干净的深海渐变或透明感,不要出现多只主体。".to_string(), + ] + .join("") +} + +fn build_big_fish_level_motion_prompt( + draft: &BigFishGameDraftRecord, + level: &BigFishLevelBlueprintRecord, + motion_key: &str, +) -> String { + let motion_text = match motion_key { + "move_swim" => "向右游动的关键帧预览,身体与尾鳍有清晰推进姿态,带轻微水流拖尾。", + _ => "待机漂浮的关键帧预览,身体轻微摆动,姿态稳定,适合作为 idle 状态。", + }; + vec![ + format!( + "为竖屏移动游戏《{}》生成一张等级生物动作关键帧静态预览图。", + draft.title + ), + format!("生态主题:{}。", draft.ecology_theme), + format!( + "等级:Lv.{},名称:{},幻想描述:{}。", + level.level, level.name, level.one_line_fantasy + ), + format!("动作提示词种子:{}。", level.motion_prompt_seed), + format!("动作要求:{motion_text}"), + "画面要求:单体生物完整入镜,轮廓清晰,动作方向明确,2D 高完成度游戏插画,适合作为 Big Fish 动作槽位的静态 keyframe。".to_string(), + "不要出现 UI、文字、logo、水印、对话框或边框;不要生成序列帧拼图,不要出现多只主体。".to_string(), + ] + .join("") +} + +fn build_big_fish_stage_background_prompt(draft: &BigFishGameDraftRecord) -> String { + let background = &draft.background; + vec![ + format!( + "为竖屏移动游戏《{}》生成一张 9:16 全屏活动区域背景。", + draft.title + ), + format!("生态主题:{}。", draft.ecology_theme), + format!("背景主题:{}。色彩氛围:{}。", background.theme, background.color_mood), + format!("前景提示:{}。", background.foreground_hints), + format!("中景构图:{}。", background.midground_composition), + format!("背景纵深:{}。", background.background_depth), + format!("安全操作区:{}。", background.safe_play_area_hint), + format!("出生边缘:{}。", background.spawn_edge_hint), + format!("背景提示词种子:{}。", background.background_prompt_seed), + "画面要求:竖屏 9:16,中央 70% 保持清爽可读,边缘有深海生态层次和微弱生物光,适合作为大鱼吃小鱼运行态背景。".to_string(), + "不要出现 UI、文字、logo、水印、对话框、边框或巨大主体遮挡;不要把中央操作区画得过暗或过复杂。".to_string(), + ] + .join("") +} + +fn require_big_fish_dashscope_settings( + state: &AppState, +) -> Result { + let base_url = state.config.dashscope_base_url.trim().trim_end_matches('/'); + if base_url.is_empty() { + return Err( + AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({ + "provider": "dashscope", + "reason": "DASHSCOPE_BASE_URL 未配置", + })), + ); + } + + let api_key = state + .config + .dashscope_api_key + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .ok_or_else(|| { + AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({ + "provider": "dashscope", + "reason": "DASHSCOPE_API_KEY 未配置", + })) + })?; + + Ok(BigFishDashScopeSettings { + base_url: base_url.to_string(), + api_key: api_key.to_string(), + request_timeout_ms: state.config.dashscope_image_request_timeout_ms.max(1), + }) +} + +fn build_big_fish_dashscope_http_client( + settings: &BigFishDashScopeSettings, +) -> Result { + reqwest::Client::builder() + .timeout(Duration::from_millis(settings.request_timeout_ms)) + .build() + .map_err(|error| { + AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({ + "provider": "dashscope", + "message": format!("构造 DashScope HTTP 客户端失败:{error}"), + })) + }) +} + +async fn create_big_fish_text_to_image_generation( + http_client: &reqwest::Client, + settings: &BigFishDashScopeSettings, + prompt: &str, + negative_prompt: &str, + size: &str, +) -> Result { + let mut parameters = Map::from_iter([ + ("n".to_string(), json!(1)), + ("size".to_string(), Value::String(size.to_string())), + ("prompt_extend".to_string(), Value::Bool(true)), + ("watermark".to_string(), Value::Bool(false)), + ]); + if !negative_prompt.trim().is_empty() { + parameters.insert( + "negative_prompt".to_string(), + Value::String(negative_prompt.trim().to_string()), + ); + } + + let response = http_client + .post(format!( + "{}/services/aigc/text2image/image-synthesis", + settings.base_url + )) + .header( + reqwest::header::AUTHORIZATION, + format!("Bearer {}", settings.api_key), + ) + .header(reqwest::header::CONTENT_TYPE, "application/json") + .header("X-DashScope-Async", "enable") + .json(&json!({ + "model": BIG_FISH_TEXT_TO_IMAGE_MODEL, + "input": { + "prompt": prompt, + }, + "parameters": parameters, + })) + .send() + .await + .map_err(|error| map_big_fish_dashscope_request_error(format!( + "创建 Big Fish 图片生成任务失败:{error}" + )))?; + let status = response.status(); + let response_text = response.text().await.map_err(|error| { + map_big_fish_dashscope_request_error(format!("读取 Big Fish 图片生成响应失败:{error}")) + })?; + if !status.is_success() { + return Err(map_big_fish_dashscope_upstream_error( + response_text.as_str(), + "创建 Big Fish 图片生成任务失败", + )); + } + let payload = parse_big_fish_json_payload(response_text.as_str(), "解析 Big Fish 图片生成响应失败")?; + let task_id = extract_big_fish_task_id(&payload).ok_or_else(|| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "dashscope", + "message": "Big Fish 图片生成任务未返回 task_id", + })) + })?; + let deadline = Instant::now() + Duration::from_millis(settings.request_timeout_ms); + + while Instant::now() < deadline { + let poll_response = http_client + .get(format!("{}/tasks/{}", settings.base_url, task_id)) + .header( + reqwest::header::AUTHORIZATION, + format!("Bearer {}", settings.api_key), + ) + .send() + .await + .map_err(|error| map_big_fish_dashscope_request_error(format!( + "查询 Big Fish 图片生成任务失败:{error}" + )))?; + let poll_status = poll_response.status(); + let poll_text = poll_response.text().await.map_err(|error| { + map_big_fish_dashscope_request_error(format!( + "读取 Big Fish 图片生成任务响应失败:{error}" + )) + })?; + if !poll_status.is_success() { + return Err(map_big_fish_dashscope_upstream_error( + poll_text.as_str(), + "查询 Big Fish 图片生成任务失败", + )); + } + let poll_payload = + parse_big_fish_json_payload(poll_text.as_str(), "解析 Big Fish 图片生成任务响应失败")?; + let task_status = find_first_big_fish_string_by_key(&poll_payload, "task_status") + .unwrap_or_default() + .trim() + .to_string(); + if task_status == "SUCCEEDED" { + let image_url = extract_big_fish_image_urls(&poll_payload) + .into_iter() + .next() + .ok_or_else(|| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "dashscope", + "message": "Big Fish 图片生成成功但未返回图片地址", + })) + })?; + return Ok(BigFishGeneratedImage { image_url, task_id }); + } + if matches!(task_status.as_str(), "FAILED" | "UNKNOWN") { + return Err(map_big_fish_dashscope_upstream_error( + poll_text.as_str(), + "Big Fish 图片生成任务失败", + )); + } + + sleep(Duration::from_secs(2)).await; + } + + Err( + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "dashscope", + "message": "Big Fish 图片生成超时或未返回图片地址", + })), + ) +} + +async fn download_big_fish_remote_image( + http_client: &reqwest::Client, + image_url: &str, + fallback_message: &str, +) -> Result { + let response = http_client + .get(image_url) + .send() + .await + .map_err(|error| map_big_fish_dashscope_request_error(format!("{fallback_message}:{error}")))?; + let status = response.status(); + let content_type = response + .headers() + .get(reqwest::header::CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .unwrap_or("image/jpeg") + .to_string(); + let bytes = response + .bytes() + .await + .map_err(|error| map_big_fish_dashscope_request_error(format!("{fallback_message}:{error}")))?; + if !status.is_success() { + return Err( + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "dashscope", + "message": fallback_message, + "status": status.as_u16(), + })), + ); + } + + let mime_type = normalize_big_fish_downloaded_image_mime_type(content_type.as_str()); + Ok(BigFishDownloadedImage { + extension: big_fish_mime_to_extension(mime_type.as_str()).to_string(), + mime_type, + bytes: bytes.to_vec(), + }) +} + +async fn persist_big_fish_formal_asset( + state: &AppState, + owner_user_id: &str, + context: &BigFishFormalAssetContext, + generated: BigFishGeneratedImage, + downloaded: BigFishDownloadedImage, + generated_at_micros: i64, +) -> Result { + let oss_client = state.oss_client().ok_or_else(|| { + AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({ + "provider": "aliyun-oss", + "reason": "OSS 未完成环境变量配置", + })) + })?; + let http_client = reqwest::Client::new(); + let put_result = oss_client + .put_object( + &http_client, + OssPutObjectRequest { + prefix: LegacyAssetPrefix::BigFishAssets, + path_segments: context.path_segments.clone(), + file_name: format!("image.{}", downloaded.extension), + content_type: Some(downloaded.mime_type.clone()), + access: OssObjectAccess::Private, + metadata: build_big_fish_asset_metadata( + context.asset_object_kind.as_str(), + owner_user_id, + BIG_FISH_ENTITY_KIND, + context.entity_id.as_str(), + context.binding_slot.as_str(), + ), + body: downloaded.bytes, + }, + ) + .await + .map_err(map_big_fish_asset_oss_error)?; + let head = oss_client + .head_object( + &http_client, + OssHeadObjectRequest { + object_key: put_result.object_key.clone(), + }, + ) + .await + .map_err(map_big_fish_asset_oss_error)?; + let asset_object = state + .spacetime_client() + .confirm_asset_object( + build_asset_object_upsert_input( + generate_asset_object_id(generated_at_micros), + head.bucket, + head.object_key, + AssetObjectAccessPolicy::Private, + head.content_type.or(Some(downloaded.mime_type)), + head.content_length, + head.etag, + context.asset_object_kind.clone(), + Some(generated.task_id), + Some(owner_user_id.to_string()), + None, + Some(context.entity_id.clone()), + generated_at_micros, + ) + .map_err(map_big_fish_asset_object_prepare_error)?, + ) + .await + .map_err(map_big_fish_asset_spacetime_error)?; + state + .spacetime_client() + .bind_asset_object_to_entity( + build_asset_entity_binding_input( + generate_asset_binding_id(generated_at_micros), + asset_object.asset_object_id, + BIG_FISH_ENTITY_KIND.to_string(), + context.entity_id.clone(), + context.binding_slot.clone(), + context.asset_object_kind.clone(), + Some(owner_user_id.to_string()), + None, + generated_at_micros, + ) + .map_err(map_big_fish_asset_binding_prepare_error)?, + ) + .await + .map_err(map_big_fish_asset_spacetime_error)?; + + Ok(put_result.legacy_public_path) +} + +fn build_big_fish_asset_metadata( + asset_kind: &str, + owner_user_id: &str, + entity_kind: &str, + entity_id: &str, + slot: &str, +) -> BTreeMap { + BTreeMap::from([ + ("asset_kind".to_string(), asset_kind.to_string()), + ("owner_user_id".to_string(), owner_user_id.to_string()), + ("entity_kind".to_string(), entity_kind.to_string()), + ("entity_id".to_string(), entity_id.to_string()), + ("slot".to_string(), slot.to_string()), + ]) +} + +fn parse_big_fish_json_payload(raw_text: &str, fallback_message: &str) -> Result { + serde_json::from_str::(raw_text).map_err(|error| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "dashscope", + "message": format!("{fallback_message}:{error}"), + })) + }) +} + +fn extract_big_fish_task_id(payload: &Value) -> Option { + find_first_big_fish_string_by_key(payload, "task_id") +} + +fn extract_big_fish_image_urls(payload: &Value) -> Vec { + let mut urls = Vec::new(); + collect_big_fish_strings_by_key(payload, "image", &mut urls); + collect_big_fish_strings_by_key(payload, "url", &mut urls); + let mut deduped = Vec::new(); + for url in urls { + if !deduped.contains(&url) { + deduped.push(url); + } + } + deduped +} + +fn find_first_big_fish_string_by_key(payload: &Value, target_key: &str) -> Option { + let mut results = Vec::new(); + collect_big_fish_strings_by_key(payload, target_key, &mut results); + results.into_iter().next() +} + +fn collect_big_fish_strings_by_key(payload: &Value, target_key: &str, results: &mut Vec) { + match payload { + Value::Array(entries) => { + for entry in entries { + collect_big_fish_strings_by_key(entry, target_key, results); + } + } + Value::Object(object) => { + for (key, value) in object { + if key == target_key + && let Some(text) = value.as_str() + { + results.push(text.to_string()); + } + collect_big_fish_strings_by_key(value, target_key, results); + } + } + _ => {} + } +} + +fn normalize_big_fish_downloaded_image_mime_type(content_type: &str) -> String { + let mime_type = content_type + .split(';') + .next() + .map(str::trim) + .unwrap_or("image/jpeg"); + match mime_type { + "image/png" | "image/webp" | "image/jpeg" | "image/jpg" | "image/gif" => { + mime_type.to_string() + } + _ => "image/jpeg".to_string(), + } +} + +fn big_fish_mime_to_extension(mime_type: &str) -> &str { + match mime_type { + "image/png" => "png", + "image/webp" => "webp", + "image/gif" => "gif", + _ => "jpg", + } +} + +fn map_big_fish_dashscope_request_error(message: String) -> AppError { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "dashscope", + "message": message, + })) +} + +fn map_big_fish_dashscope_upstream_error(raw_text: &str, fallback_message: &str) -> AppError { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "dashscope", + "message": parse_big_fish_api_error_message(raw_text, fallback_message), + })) +} + +fn parse_big_fish_api_error_message(raw_text: &str, fallback_message: &str) -> String { + let trimmed = raw_text.trim(); + if trimmed.is_empty() { + return fallback_message.to_string(); + } + if let Ok(payload) = serde_json::from_str::(trimmed) + && let Some(message) = find_first_big_fish_string_by_key(&payload, "message") + .or_else(|| find_first_big_fish_string_by_key(&payload, "code")) + { + return message; + } + let excerpt = trimmed.chars().take(240).collect::(); + format!("{fallback_message}:{excerpt}") +} + +fn map_big_fish_asset_object_prepare_error(error: AssetObjectFieldError) -> AppError { + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "asset-object", + "message": error.to_string(), + })) +} + +fn map_big_fish_asset_binding_prepare_error(error: AssetObjectFieldError) -> AppError { + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "asset-entity-binding", + "message": error.to_string(), + })) +} + +fn map_big_fish_asset_spacetime_error(error: SpacetimeClientError) -> AppError { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "spacetimedb", + "message": error.to_string(), + })) +} + +fn map_big_fish_asset_oss_error(error: platform_oss::OssError) -> AppError { + let status = match error { + platform_oss::OssError::InvalidConfig(_) | platform_oss::OssError::InvalidRequest(_) => { + StatusCode::BAD_REQUEST + } + platform_oss::OssError::ObjectNotFound(_) => StatusCode::NOT_FOUND, + platform_oss::OssError::Request(_) + | platform_oss::OssError::SerializePolicy(_) + | platform_oss::OssError::Sign(_) => StatusCode::BAD_GATEWAY, + }; + AppError::from_status(status).with_details(json!({ + "provider": "aliyun-oss", + "message": error.to_string(), + })) +} + +fn build_big_fish_level_part(level: Option) -> String { + level + .map(|value| format!("level-{value}")) + .unwrap_or_else(|| "stage".to_string()) +} + +fn sanitize_big_fish_path_segment(value: &str, fallback: &str) -> String { + let sanitized = value + .trim() + .chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' { + ch + } else { + '-' + } + }) + .collect::() + .trim_matches('-') + .to_string(); + if sanitized.is_empty() { + fallback.to_string() + } else { + sanitized + } +} + fn ensure_non_empty( request_context: &RequestContext, value: &str, @@ -680,8 +1470,6 @@ fn big_fish_error_response(request_context: &RequestContext, error: AppError) -> } fn current_utc_micros() -> i64 { - use std::time::{SystemTime, UNIX_EPOCH}; - let duration = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("system clock should be after unix epoch"); diff --git a/server-rs/crates/api-server/src/legacy_generated_assets.rs b/server-rs/crates/api-server/src/legacy_generated_assets.rs index 65a0785f..1f910183 100644 --- a/server-rs/crates/api-server/src/legacy_generated_assets.rs +++ b/server-rs/crates/api-server/src/legacy_generated_assets.rs @@ -33,6 +33,13 @@ pub async fn proxy_generated_animations( proxy_legacy_generated_asset(state, LegacyAssetPrefix::Animations, path).await } +pub async fn proxy_generated_big_fish_assets( + State(state): State, + Path(path): Path, +) -> Response { + proxy_legacy_generated_asset(state, LegacyAssetPrefix::BigFishAssets, path).await +} + pub async fn proxy_generated_custom_world_scenes( State(state): State, Path(path): Path, @@ -200,6 +207,20 @@ mod tests { ); } + #[test] + fn build_generated_object_key_supports_big_fish_assets() { + let object_key = build_generated_object_key( + LegacyAssetPrefix::BigFishAssets, + "big-fish-session-1/level-main-image/level-1/image.png", + ) + .expect("object key should build"); + + assert_eq!( + object_key, + "generated-big-fish-assets/big-fish-session-1/level-main-image/level-1/image.png" + ); + } + #[test] fn build_generated_object_key_rejects_parent_segment() { assert!( diff --git a/server-rs/crates/module-big-fish/src/lib.rs b/server-rs/crates/module-big-fish/src/lib.rs index 42c3562d..61082bdb 100644 --- a/server-rs/crates/module-big-fish/src/lib.rs +++ b/server-rs/crates/module-big-fish/src/lib.rs @@ -305,6 +305,7 @@ pub struct BigFishAssetGenerateInput { pub asset_kind: BigFishAssetKind, pub level: Option, pub motion_key: Option, + pub asset_url: Option, pub generated_at_micros: i64, } @@ -593,12 +594,16 @@ pub fn build_generated_asset_slot( asset_kind: BigFishAssetKind, level: Option, motion_key: Option, + asset_url: Option, updated_at_micros: i64, ) -> Result { let session_id = normalize_required_string(session_id).ok_or(BigFishFieldError::MissingSessionId)?; - let prompt_snapshot = build_asset_prompt_snapshot(draft, asset_kind, level, motion_key.as_deref())?; + let prompt_snapshot = + build_asset_prompt_snapshot(draft, asset_kind, level, motion_key.as_deref())?; let slot_id = build_asset_slot_id(&session_id, asset_kind, level, motion_key.as_deref()); + let resolved_asset_url = normalize_required_string(asset_url.as_deref().unwrap_or_default()) + .unwrap_or_else(|| build_placeholder_asset_url(asset_kind, level, updated_at_micros)); Ok(BigFishAssetSlotSnapshot { slot_id, @@ -607,7 +612,7 @@ pub fn build_generated_asset_slot( level, motion_key, status: BigFishAssetStatus::Ready, - asset_url: Some(build_placeholder_asset_url(asset_kind, level, updated_at_micros)), + asset_url: Some(resolved_asset_url), prompt_snapshot, updated_at_micros, }) diff --git a/server-rs/crates/platform-oss/src/lib.rs b/server-rs/crates/platform-oss/src/lib.rs index a9ac3f53..ce2dceb0 100644 --- a/server-rs/crates/platform-oss/src/lib.rs +++ b/server-rs/crates/platform-oss/src/lib.rs @@ -17,10 +17,11 @@ pub const DEFAULT_POST_MAX_SIZE_BYTES: u64 = 20 * 1024 * 1024; pub const DEFAULT_SUCCESS_ACTION_STATUS: u16 = 200; pub const DEFAULT_METADATA_TOTAL_BYTES_LIMIT: usize = 8 * 1024; -pub const LEGACY_PUBLIC_PREFIXES: [&str; 6] = [ +pub const LEGACY_PUBLIC_PREFIXES: [&str; 7] = [ "generated-character-drafts", "generated-characters", "generated-animations", + "generated-big-fish-assets", "generated-custom-world-scenes", "generated-custom-world-covers", "generated-qwen-sprites", @@ -38,6 +39,7 @@ pub enum LegacyAssetPrefix { CharacterDrafts, Characters, Animations, + BigFishAssets, CustomWorldScenes, CustomWorldCovers, QwenSprites, @@ -206,6 +208,7 @@ impl LegacyAssetPrefix { "generated-character-drafts" => Some(Self::CharacterDrafts), "generated-characters" => Some(Self::Characters), "generated-animations" => Some(Self::Animations), + "generated-big-fish-assets" => Some(Self::BigFishAssets), "generated-custom-world-scenes" => Some(Self::CustomWorldScenes), "generated-custom-world-covers" => Some(Self::CustomWorldCovers), "generated-qwen-sprites" => Some(Self::QwenSprites), @@ -218,6 +221,7 @@ impl LegacyAssetPrefix { Self::CharacterDrafts => "generated-character-drafts", Self::Characters => "generated-characters", Self::Animations => "generated-animations", + Self::BigFishAssets => "generated-big-fish-assets", Self::CustomWorldScenes => "generated-custom-world-scenes", Self::CustomWorldCovers => "generated-custom-world-covers", Self::QwenSprites => "generated-qwen-sprites", diff --git a/server-rs/crates/spacetime-client/src/lib.rs b/server-rs/crates/spacetime-client/src/lib.rs index d719d3bc..5b31af43 100644 --- a/server-rs/crates/spacetime-client/src/lib.rs +++ b/server-rs/crates/spacetime-client/src/lib.rs @@ -1487,6 +1487,7 @@ impl SpacetimeClient { asset_kind: map_big_fish_asset_kind_input(input.asset_kind.as_str())?, level: input.level, motion_key: input.motion_key, + asset_url: input.asset_url, generated_at_micros: input.generated_at_micros, }; @@ -6137,6 +6138,7 @@ pub struct BigFishAssetGenerateRecordInput { pub asset_kind: String, pub level: Option, pub motion_key: Option, + pub asset_url: Option, pub generated_at_micros: i64, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_generate_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_generate_input_type.rs index e57057d1..ba67939a 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_generate_input_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_generate_input_type.rs @@ -19,6 +19,7 @@ pub struct BigFishAssetGenerateInput { pub asset_kind: BigFishAssetKind, pub level: Option::, pub motion_key: Option::, + pub asset_url: Option::, pub generated_at_micros: i64, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/mod.rs b/server-rs/crates/spacetime-client/src/module_bindings/mod.rs index 3af1a265..7558f9b7 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/mod.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/mod.rs @@ -328,44 +328,7 @@ pub mod unpublish_custom_world_profile_reducer; pub mod upsert_chapter_progression_reducer; pub mod upsert_custom_world_profile_reducer; pub mod upsert_npc_state_reducer; -pub mod ai_result_reference_table; -pub mod ai_task_table; -pub mod ai_task_stage_table; -pub mod ai_text_chunk_table; -pub mod asset_entity_binding_table; -pub mod asset_object_table; -pub mod battle_state_table; -pub mod big_fish_agent_message_table; -pub mod big_fish_asset_slot_table; -pub mod big_fish_creation_session_table; -pub mod big_fish_runtime_run_table; -pub mod chapter_progression_table; -pub mod custom_world_agent_message_table; -pub mod custom_world_agent_operation_table; -pub mod custom_world_agent_session_table; -pub mod custom_world_draft_card_table; pub mod custom_world_gallery_entry_table; -pub mod custom_world_profile_table; -pub mod custom_world_session_table; -pub mod inventory_slot_table; -pub mod npc_state_table; -pub mod player_progression_table; -pub mod profile_dashboard_state_table; -pub mod profile_played_world_table; -pub mod profile_save_archive_table; -pub mod profile_wallet_ledger_table; -pub mod puzzle_agent_message_table; -pub mod puzzle_agent_session_table; -pub mod puzzle_runtime_run_table; -pub mod puzzle_work_profile_table; -pub mod quest_log_table; -pub mod quest_record_table; -pub mod runtime_setting_table; -pub mod runtime_snapshot_table; -pub mod story_event_table; -pub mod story_session_table; -pub mod treasure_record_table; -pub mod user_browse_history_table; pub mod advance_puzzle_next_level_procedure; pub mod append_ai_text_chunk_and_return_procedure; pub mod apply_chapter_progression_ledger_entry_and_return_procedure; @@ -743,44 +706,7 @@ pub use treasure_record_snapshot_type::TreasureRecordSnapshot; pub use treasure_resolve_input_type::TreasureResolveInput; pub use unequip_inventory_item_input_type::UnequipInventoryItemInput; pub use user_browse_history_type::UserBrowseHistory; -pub use ai_result_reference_table::*; -pub use ai_task_table::*; -pub use ai_task_stage_table::*; -pub use ai_text_chunk_table::*; -pub use asset_entity_binding_table::*; -pub use asset_object_table::*; -pub use battle_state_table::*; -pub use big_fish_agent_message_table::*; -pub use big_fish_asset_slot_table::*; -pub use big_fish_creation_session_table::*; -pub use big_fish_runtime_run_table::*; -pub use chapter_progression_table::*; -pub use custom_world_agent_message_table::*; -pub use custom_world_agent_operation_table::*; -pub use custom_world_agent_session_table::*; -pub use custom_world_draft_card_table::*; pub use custom_world_gallery_entry_table::*; -pub use custom_world_profile_table::*; -pub use custom_world_session_table::*; -pub use inventory_slot_table::*; -pub use npc_state_table::*; -pub use player_progression_table::*; -pub use profile_dashboard_state_table::*; -pub use profile_played_world_table::*; -pub use profile_save_archive_table::*; -pub use profile_wallet_ledger_table::*; -pub use puzzle_agent_message_table::*; -pub use puzzle_agent_session_table::*; -pub use puzzle_runtime_run_table::*; -pub use puzzle_work_profile_table::*; -pub use quest_log_table::*; -pub use quest_record_table::*; -pub use runtime_setting_table::*; -pub use runtime_snapshot_table::*; -pub use story_event_table::*; -pub use story_session_table::*; -pub use treasure_record_table::*; -pub use user_browse_history_table::*; pub use accept_quest_reducer::accept_quest; pub use acknowledge_quest_completion_reducer::acknowledge_quest_completion; pub use apply_chapter_progression_ledger_entry_reducer::apply_chapter_progression_ledger_entry; @@ -1138,44 +1064,7 @@ fn args_bsatn(&self) -> Result, __sats::bsatn::EncodeError> { #[allow(non_snake_case)] #[doc(hidden)] pub struct DbUpdate { - ai_result_reference: __sdk::TableUpdate, - ai_task: __sdk::TableUpdate, - ai_task_stage: __sdk::TableUpdate, - ai_text_chunk: __sdk::TableUpdate, - asset_entity_binding: __sdk::TableUpdate, - asset_object: __sdk::TableUpdate, - battle_state: __sdk::TableUpdate, - big_fish_agent_message: __sdk::TableUpdate, - big_fish_asset_slot: __sdk::TableUpdate, - big_fish_creation_session: __sdk::TableUpdate, - big_fish_runtime_run: __sdk::TableUpdate, - chapter_progression: __sdk::TableUpdate, - custom_world_agent_message: __sdk::TableUpdate, - custom_world_agent_operation: __sdk::TableUpdate, - custom_world_agent_session: __sdk::TableUpdate, - custom_world_draft_card: __sdk::TableUpdate, - custom_world_gallery_entry: __sdk::TableUpdate, - custom_world_profile: __sdk::TableUpdate, - custom_world_session: __sdk::TableUpdate, - inventory_slot: __sdk::TableUpdate, - npc_state: __sdk::TableUpdate, - player_progression: __sdk::TableUpdate, - profile_dashboard_state: __sdk::TableUpdate, - profile_played_world: __sdk::TableUpdate, - profile_save_archive: __sdk::TableUpdate, - profile_wallet_ledger: __sdk::TableUpdate, - puzzle_agent_message: __sdk::TableUpdate, - puzzle_agent_session: __sdk::TableUpdate, - puzzle_runtime_run: __sdk::TableUpdate, - puzzle_work_profile: __sdk::TableUpdate, - quest_log: __sdk::TableUpdate, - quest_record: __sdk::TableUpdate, - runtime_setting: __sdk::TableUpdate, - runtime_snapshot: __sdk::TableUpdate, - story_event: __sdk::TableUpdate, - story_session: __sdk::TableUpdate, - treasure_record: __sdk::TableUpdate, - user_browse_history: __sdk::TableUpdate, + custom_world_gallery_entry: __sdk::TableUpdate, } @@ -1186,44 +1075,7 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { for table_update in __sdk::transaction_update_iter_table_updates(raw) { match &table_update.table_name[..] { - "ai_result_reference" => db_update.ai_result_reference.append(ai_result_reference_table::parse_table_update(table_update)?), - "ai_task" => db_update.ai_task.append(ai_task_table::parse_table_update(table_update)?), - "ai_task_stage" => db_update.ai_task_stage.append(ai_task_stage_table::parse_table_update(table_update)?), - "ai_text_chunk" => db_update.ai_text_chunk.append(ai_text_chunk_table::parse_table_update(table_update)?), - "asset_entity_binding" => db_update.asset_entity_binding.append(asset_entity_binding_table::parse_table_update(table_update)?), - "asset_object" => db_update.asset_object.append(asset_object_table::parse_table_update(table_update)?), - "battle_state" => db_update.battle_state.append(battle_state_table::parse_table_update(table_update)?), - "big_fish_agent_message" => db_update.big_fish_agent_message.append(big_fish_agent_message_table::parse_table_update(table_update)?), - "big_fish_asset_slot" => db_update.big_fish_asset_slot.append(big_fish_asset_slot_table::parse_table_update(table_update)?), - "big_fish_creation_session" => db_update.big_fish_creation_session.append(big_fish_creation_session_table::parse_table_update(table_update)?), - "big_fish_runtime_run" => db_update.big_fish_runtime_run.append(big_fish_runtime_run_table::parse_table_update(table_update)?), - "chapter_progression" => db_update.chapter_progression.append(chapter_progression_table::parse_table_update(table_update)?), - "custom_world_agent_message" => db_update.custom_world_agent_message.append(custom_world_agent_message_table::parse_table_update(table_update)?), - "custom_world_agent_operation" => db_update.custom_world_agent_operation.append(custom_world_agent_operation_table::parse_table_update(table_update)?), - "custom_world_agent_session" => db_update.custom_world_agent_session.append(custom_world_agent_session_table::parse_table_update(table_update)?), - "custom_world_draft_card" => db_update.custom_world_draft_card.append(custom_world_draft_card_table::parse_table_update(table_update)?), - "custom_world_gallery_entry" => db_update.custom_world_gallery_entry.append(custom_world_gallery_entry_table::parse_table_update(table_update)?), - "custom_world_profile" => db_update.custom_world_profile.append(custom_world_profile_table::parse_table_update(table_update)?), - "custom_world_session" => db_update.custom_world_session.append(custom_world_session_table::parse_table_update(table_update)?), - "inventory_slot" => db_update.inventory_slot.append(inventory_slot_table::parse_table_update(table_update)?), - "npc_state" => db_update.npc_state.append(npc_state_table::parse_table_update(table_update)?), - "player_progression" => db_update.player_progression.append(player_progression_table::parse_table_update(table_update)?), - "profile_dashboard_state" => db_update.profile_dashboard_state.append(profile_dashboard_state_table::parse_table_update(table_update)?), - "profile_played_world" => db_update.profile_played_world.append(profile_played_world_table::parse_table_update(table_update)?), - "profile_save_archive" => db_update.profile_save_archive.append(profile_save_archive_table::parse_table_update(table_update)?), - "profile_wallet_ledger" => db_update.profile_wallet_ledger.append(profile_wallet_ledger_table::parse_table_update(table_update)?), - "puzzle_agent_message" => db_update.puzzle_agent_message.append(puzzle_agent_message_table::parse_table_update(table_update)?), - "puzzle_agent_session" => db_update.puzzle_agent_session.append(puzzle_agent_session_table::parse_table_update(table_update)?), - "puzzle_runtime_run" => db_update.puzzle_runtime_run.append(puzzle_runtime_run_table::parse_table_update(table_update)?), - "puzzle_work_profile" => db_update.puzzle_work_profile.append(puzzle_work_profile_table::parse_table_update(table_update)?), - "quest_log" => db_update.quest_log.append(quest_log_table::parse_table_update(table_update)?), - "quest_record" => db_update.quest_record.append(quest_record_table::parse_table_update(table_update)?), - "runtime_setting" => db_update.runtime_setting.append(runtime_setting_table::parse_table_update(table_update)?), - "runtime_snapshot" => db_update.runtime_snapshot.append(runtime_snapshot_table::parse_table_update(table_update)?), - "story_event" => db_update.story_event.append(story_event_table::parse_table_update(table_update)?), - "story_session" => db_update.story_session.append(story_session_table::parse_table_update(table_update)?), - "treasure_record" => db_update.treasure_record.append(treasure_record_table::parse_table_update(table_update)?), - "user_browse_history" => db_update.user_browse_history.append(user_browse_history_table::parse_table_update(table_update)?), + "custom_world_gallery_entry" => db_update.custom_world_gallery_entry.append(custom_world_gallery_entry_table::parse_table_update(table_update)?), unknown => { return Err(__sdk::InternalError::unknown_name( @@ -1246,44 +1098,7 @@ impl __sdk::DbUpdate for DbUpdate { fn apply_to_client_cache(&self, cache: &mut __sdk::ClientCache) -> AppliedDiff<'_> { let mut diff = AppliedDiff::default(); - diff.ai_result_reference = cache.apply_diff_to_table::("ai_result_reference", &self.ai_result_reference).with_updates_by_pk(|row| &row.result_reference_row_id); - diff.ai_task = cache.apply_diff_to_table::("ai_task", &self.ai_task).with_updates_by_pk(|row| &row.task_id); - diff.ai_task_stage = cache.apply_diff_to_table::("ai_task_stage", &self.ai_task_stage).with_updates_by_pk(|row| &row.task_stage_id); - diff.ai_text_chunk = cache.apply_diff_to_table::("ai_text_chunk", &self.ai_text_chunk).with_updates_by_pk(|row| &row.text_chunk_row_id); - diff.asset_entity_binding = cache.apply_diff_to_table::("asset_entity_binding", &self.asset_entity_binding).with_updates_by_pk(|row| &row.binding_id); - diff.asset_object = cache.apply_diff_to_table::("asset_object", &self.asset_object).with_updates_by_pk(|row| &row.asset_object_id); - diff.battle_state = cache.apply_diff_to_table::("battle_state", &self.battle_state).with_updates_by_pk(|row| &row.battle_state_id); - diff.big_fish_agent_message = cache.apply_diff_to_table::("big_fish_agent_message", &self.big_fish_agent_message).with_updates_by_pk(|row| &row.message_id); - diff.big_fish_asset_slot = cache.apply_diff_to_table::("big_fish_asset_slot", &self.big_fish_asset_slot).with_updates_by_pk(|row| &row.slot_id); - diff.big_fish_creation_session = cache.apply_diff_to_table::("big_fish_creation_session", &self.big_fish_creation_session).with_updates_by_pk(|row| &row.session_id); - diff.big_fish_runtime_run = cache.apply_diff_to_table::("big_fish_runtime_run", &self.big_fish_runtime_run).with_updates_by_pk(|row| &row.run_id); - diff.chapter_progression = cache.apply_diff_to_table::("chapter_progression", &self.chapter_progression).with_updates_by_pk(|row| &row.chapter_progression_id); - diff.custom_world_agent_message = cache.apply_diff_to_table::("custom_world_agent_message", &self.custom_world_agent_message).with_updates_by_pk(|row| &row.message_id); - diff.custom_world_agent_operation = cache.apply_diff_to_table::("custom_world_agent_operation", &self.custom_world_agent_operation).with_updates_by_pk(|row| &row.operation_id); - diff.custom_world_agent_session = cache.apply_diff_to_table::("custom_world_agent_session", &self.custom_world_agent_session).with_updates_by_pk(|row| &row.session_id); - diff.custom_world_draft_card = cache.apply_diff_to_table::("custom_world_draft_card", &self.custom_world_draft_card).with_updates_by_pk(|row| &row.card_id); - diff.custom_world_gallery_entry = cache.apply_diff_to_table::("custom_world_gallery_entry", &self.custom_world_gallery_entry).with_updates_by_pk(|row| &row.profile_id); - diff.custom_world_profile = cache.apply_diff_to_table::("custom_world_profile", &self.custom_world_profile).with_updates_by_pk(|row| &row.profile_id); - diff.custom_world_session = cache.apply_diff_to_table::("custom_world_session", &self.custom_world_session).with_updates_by_pk(|row| &row.session_id); - diff.inventory_slot = cache.apply_diff_to_table::("inventory_slot", &self.inventory_slot).with_updates_by_pk(|row| &row.slot_id); - diff.npc_state = cache.apply_diff_to_table::("npc_state", &self.npc_state).with_updates_by_pk(|row| &row.npc_state_id); - diff.player_progression = cache.apply_diff_to_table::("player_progression", &self.player_progression).with_updates_by_pk(|row| &row.user_id); - diff.profile_dashboard_state = cache.apply_diff_to_table::("profile_dashboard_state", &self.profile_dashboard_state).with_updates_by_pk(|row| &row.user_id); - diff.profile_played_world = cache.apply_diff_to_table::("profile_played_world", &self.profile_played_world).with_updates_by_pk(|row| &row.played_world_id); - diff.profile_save_archive = cache.apply_diff_to_table::("profile_save_archive", &self.profile_save_archive).with_updates_by_pk(|row| &row.archive_id); - diff.profile_wallet_ledger = cache.apply_diff_to_table::("profile_wallet_ledger", &self.profile_wallet_ledger).with_updates_by_pk(|row| &row.wallet_ledger_id); - diff.puzzle_agent_message = cache.apply_diff_to_table::("puzzle_agent_message", &self.puzzle_agent_message).with_updates_by_pk(|row| &row.message_id); - diff.puzzle_agent_session = cache.apply_diff_to_table::("puzzle_agent_session", &self.puzzle_agent_session).with_updates_by_pk(|row| &row.session_id); - diff.puzzle_runtime_run = cache.apply_diff_to_table::("puzzle_runtime_run", &self.puzzle_runtime_run).with_updates_by_pk(|row| &row.run_id); - diff.puzzle_work_profile = cache.apply_diff_to_table::("puzzle_work_profile", &self.puzzle_work_profile).with_updates_by_pk(|row| &row.profile_id); - diff.quest_log = cache.apply_diff_to_table::("quest_log", &self.quest_log).with_updates_by_pk(|row| &row.log_id); - diff.quest_record = cache.apply_diff_to_table::("quest_record", &self.quest_record).with_updates_by_pk(|row| &row.quest_id); - diff.runtime_setting = cache.apply_diff_to_table::("runtime_setting", &self.runtime_setting).with_updates_by_pk(|row| &row.user_id); - diff.runtime_snapshot = cache.apply_diff_to_table::("runtime_snapshot", &self.runtime_snapshot).with_updates_by_pk(|row| &row.user_id); - diff.story_event = cache.apply_diff_to_table::("story_event", &self.story_event).with_updates_by_pk(|row| &row.event_id); - diff.story_session = cache.apply_diff_to_table::("story_session", &self.story_session).with_updates_by_pk(|row| &row.story_session_id); - diff.treasure_record = cache.apply_diff_to_table::("treasure_record", &self.treasure_record).with_updates_by_pk(|row| &row.treasure_record_id); - diff.user_browse_history = cache.apply_diff_to_table::("user_browse_history", &self.user_browse_history).with_updates_by_pk(|row| &row.browse_history_id); + diff.custom_world_gallery_entry = cache.apply_diff_to_table::("custom_world_gallery_entry", &self.custom_world_gallery_entry).with_updates_by_pk(|row| &row.profile_id); diff } @@ -1291,44 +1106,7 @@ fn parse_initial_rows(raw: __ws::v2::QueryRows) -> __sdk::Result { let mut db_update = DbUpdate::default(); for table_rows in raw.tables { match &table_rows.table[..] { - "ai_result_reference" => db_update.ai_result_reference.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "ai_task" => db_update.ai_task.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "ai_task_stage" => db_update.ai_task_stage.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "ai_text_chunk" => db_update.ai_text_chunk.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "asset_entity_binding" => db_update.asset_entity_binding.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "asset_object" => db_update.asset_object.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "battle_state" => db_update.battle_state.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "big_fish_agent_message" => db_update.big_fish_agent_message.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "big_fish_asset_slot" => db_update.big_fish_asset_slot.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "big_fish_creation_session" => db_update.big_fish_creation_session.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "big_fish_runtime_run" => db_update.big_fish_runtime_run.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "chapter_progression" => db_update.chapter_progression.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "custom_world_agent_message" => db_update.custom_world_agent_message.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "custom_world_agent_operation" => db_update.custom_world_agent_operation.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "custom_world_agent_session" => db_update.custom_world_agent_session.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "custom_world_draft_card" => db_update.custom_world_draft_card.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "custom_world_gallery_entry" => db_update.custom_world_gallery_entry.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "custom_world_profile" => db_update.custom_world_profile.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "custom_world_session" => db_update.custom_world_session.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "inventory_slot" => db_update.inventory_slot.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "npc_state" => db_update.npc_state.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "player_progression" => db_update.player_progression.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "profile_dashboard_state" => db_update.profile_dashboard_state.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "profile_played_world" => db_update.profile_played_world.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "profile_save_archive" => db_update.profile_save_archive.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "profile_wallet_ledger" => db_update.profile_wallet_ledger.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "puzzle_agent_message" => db_update.puzzle_agent_message.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "puzzle_agent_session" => db_update.puzzle_agent_session.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "puzzle_runtime_run" => db_update.puzzle_runtime_run.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "puzzle_work_profile" => db_update.puzzle_work_profile.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "quest_log" => db_update.quest_log.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "quest_record" => db_update.quest_record.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "runtime_setting" => db_update.runtime_setting.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "runtime_snapshot" => db_update.runtime_snapshot.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "story_event" => db_update.story_event.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "story_session" => db_update.story_session.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "treasure_record" => db_update.treasure_record.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), - "user_browse_history" => db_update.user_browse_history.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "custom_world_gallery_entry" => db_update.custom_world_gallery_entry.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), unknown => { return Err(__sdk::InternalError::unknown_name("table", unknown, "QueryRows").into()); } }} Ok(db_update) } @@ -1336,44 +1114,7 @@ fn parse_unsubscribe_rows(raw: __ws::v2::QueryRows) -> __sdk::Result { let mut db_update = DbUpdate::default(); for table_rows in raw.tables { match &table_rows.table[..] { - "ai_result_reference" => db_update.ai_result_reference.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "ai_task" => db_update.ai_task.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "ai_task_stage" => db_update.ai_task_stage.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "ai_text_chunk" => db_update.ai_text_chunk.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "asset_entity_binding" => db_update.asset_entity_binding.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "asset_object" => db_update.asset_object.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "battle_state" => db_update.battle_state.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "big_fish_agent_message" => db_update.big_fish_agent_message.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "big_fish_asset_slot" => db_update.big_fish_asset_slot.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "big_fish_creation_session" => db_update.big_fish_creation_session.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "big_fish_runtime_run" => db_update.big_fish_runtime_run.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "chapter_progression" => db_update.chapter_progression.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "custom_world_agent_message" => db_update.custom_world_agent_message.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "custom_world_agent_operation" => db_update.custom_world_agent_operation.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "custom_world_agent_session" => db_update.custom_world_agent_session.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "custom_world_draft_card" => db_update.custom_world_draft_card.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "custom_world_gallery_entry" => db_update.custom_world_gallery_entry.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "custom_world_profile" => db_update.custom_world_profile.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "custom_world_session" => db_update.custom_world_session.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "inventory_slot" => db_update.inventory_slot.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "npc_state" => db_update.npc_state.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "player_progression" => db_update.player_progression.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "profile_dashboard_state" => db_update.profile_dashboard_state.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "profile_played_world" => db_update.profile_played_world.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "profile_save_archive" => db_update.profile_save_archive.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "profile_wallet_ledger" => db_update.profile_wallet_ledger.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "puzzle_agent_message" => db_update.puzzle_agent_message.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "puzzle_agent_session" => db_update.puzzle_agent_session.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "puzzle_runtime_run" => db_update.puzzle_runtime_run.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "puzzle_work_profile" => db_update.puzzle_work_profile.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "quest_log" => db_update.quest_log.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "quest_record" => db_update.quest_record.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "runtime_setting" => db_update.runtime_setting.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "runtime_snapshot" => db_update.runtime_snapshot.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "story_event" => db_update.story_event.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "story_session" => db_update.story_session.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "treasure_record" => db_update.treasure_record.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), - "user_browse_history" => db_update.user_browse_history.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "custom_world_gallery_entry" => db_update.custom_world_gallery_entry.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), unknown => { return Err(__sdk::InternalError::unknown_name("table", unknown, "QueryRows").into()); } }} Ok(db_update) } @@ -1383,44 +1124,7 @@ for table_rows in raw.tables { #[allow(non_snake_case)] #[doc(hidden)] pub struct AppliedDiff<'r> { - ai_result_reference: __sdk::TableAppliedDiff<'r, AiResultReference>, - ai_task: __sdk::TableAppliedDiff<'r, AiTask>, - ai_task_stage: __sdk::TableAppliedDiff<'r, AiTaskStage>, - ai_text_chunk: __sdk::TableAppliedDiff<'r, AiTextChunk>, - asset_entity_binding: __sdk::TableAppliedDiff<'r, AssetEntityBinding>, - asset_object: __sdk::TableAppliedDiff<'r, AssetObject>, - battle_state: __sdk::TableAppliedDiff<'r, BattleState>, - big_fish_agent_message: __sdk::TableAppliedDiff<'r, BigFishAgentMessage>, - big_fish_asset_slot: __sdk::TableAppliedDiff<'r, BigFishAssetSlot>, - big_fish_creation_session: __sdk::TableAppliedDiff<'r, BigFishCreationSession>, - big_fish_runtime_run: __sdk::TableAppliedDiff<'r, BigFishRuntimeRun>, - chapter_progression: __sdk::TableAppliedDiff<'r, ChapterProgression>, - custom_world_agent_message: __sdk::TableAppliedDiff<'r, CustomWorldAgentMessage>, - custom_world_agent_operation: __sdk::TableAppliedDiff<'r, CustomWorldAgentOperation>, - custom_world_agent_session: __sdk::TableAppliedDiff<'r, CustomWorldAgentSession>, - custom_world_draft_card: __sdk::TableAppliedDiff<'r, CustomWorldDraftCard>, - custom_world_gallery_entry: __sdk::TableAppliedDiff<'r, CustomWorldGalleryEntry>, - custom_world_profile: __sdk::TableAppliedDiff<'r, CustomWorldProfile>, - custom_world_session: __sdk::TableAppliedDiff<'r, CustomWorldSession>, - inventory_slot: __sdk::TableAppliedDiff<'r, InventorySlot>, - npc_state: __sdk::TableAppliedDiff<'r, NpcState>, - player_progression: __sdk::TableAppliedDiff<'r, PlayerProgression>, - profile_dashboard_state: __sdk::TableAppliedDiff<'r, ProfileDashboardState>, - profile_played_world: __sdk::TableAppliedDiff<'r, ProfilePlayedWorld>, - profile_save_archive: __sdk::TableAppliedDiff<'r, ProfileSaveArchive>, - profile_wallet_ledger: __sdk::TableAppliedDiff<'r, ProfileWalletLedger>, - puzzle_agent_message: __sdk::TableAppliedDiff<'r, PuzzleAgentMessageRow>, - puzzle_agent_session: __sdk::TableAppliedDiff<'r, PuzzleAgentSessionRow>, - puzzle_runtime_run: __sdk::TableAppliedDiff<'r, PuzzleRuntimeRunRow>, - puzzle_work_profile: __sdk::TableAppliedDiff<'r, PuzzleWorkProfileRow>, - quest_log: __sdk::TableAppliedDiff<'r, QuestLog>, - quest_record: __sdk::TableAppliedDiff<'r, QuestRecord>, - runtime_setting: __sdk::TableAppliedDiff<'r, RuntimeSetting>, - runtime_snapshot: __sdk::TableAppliedDiff<'r, RuntimeSnapshotRow>, - story_event: __sdk::TableAppliedDiff<'r, StoryEvent>, - story_session: __sdk::TableAppliedDiff<'r, StorySession>, - treasure_record: __sdk::TableAppliedDiff<'r, TreasureRecord>, - user_browse_history: __sdk::TableAppliedDiff<'r, UserBrowseHistory>, + custom_world_gallery_entry: __sdk::TableAppliedDiff<'r, CustomWorldGalleryEntry>, __unused: std::marker::PhantomData<&'r ()>, } @@ -1431,44 +1135,7 @@ impl __sdk::InModule for AppliedDiff<'_> { impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { fn invoke_row_callbacks(&self, event: &EventContext, callbacks: &mut __sdk::DbCallbacks) { - callbacks.invoke_table_row_callbacks::("ai_result_reference", &self.ai_result_reference, event); - callbacks.invoke_table_row_callbacks::("ai_task", &self.ai_task, event); - callbacks.invoke_table_row_callbacks::("ai_task_stage", &self.ai_task_stage, event); - callbacks.invoke_table_row_callbacks::("ai_text_chunk", &self.ai_text_chunk, event); - callbacks.invoke_table_row_callbacks::("asset_entity_binding", &self.asset_entity_binding, event); - callbacks.invoke_table_row_callbacks::("asset_object", &self.asset_object, event); - callbacks.invoke_table_row_callbacks::("battle_state", &self.battle_state, event); - callbacks.invoke_table_row_callbacks::("big_fish_agent_message", &self.big_fish_agent_message, event); - callbacks.invoke_table_row_callbacks::("big_fish_asset_slot", &self.big_fish_asset_slot, event); - callbacks.invoke_table_row_callbacks::("big_fish_creation_session", &self.big_fish_creation_session, event); - callbacks.invoke_table_row_callbacks::("big_fish_runtime_run", &self.big_fish_runtime_run, event); - callbacks.invoke_table_row_callbacks::("chapter_progression", &self.chapter_progression, event); - callbacks.invoke_table_row_callbacks::("custom_world_agent_message", &self.custom_world_agent_message, event); - callbacks.invoke_table_row_callbacks::("custom_world_agent_operation", &self.custom_world_agent_operation, event); - callbacks.invoke_table_row_callbacks::("custom_world_agent_session", &self.custom_world_agent_session, event); - callbacks.invoke_table_row_callbacks::("custom_world_draft_card", &self.custom_world_draft_card, event); - callbacks.invoke_table_row_callbacks::("custom_world_gallery_entry", &self.custom_world_gallery_entry, event); - callbacks.invoke_table_row_callbacks::("custom_world_profile", &self.custom_world_profile, event); - callbacks.invoke_table_row_callbacks::("custom_world_session", &self.custom_world_session, event); - callbacks.invoke_table_row_callbacks::("inventory_slot", &self.inventory_slot, event); - callbacks.invoke_table_row_callbacks::("npc_state", &self.npc_state, event); - callbacks.invoke_table_row_callbacks::("player_progression", &self.player_progression, event); - callbacks.invoke_table_row_callbacks::("profile_dashboard_state", &self.profile_dashboard_state, event); - callbacks.invoke_table_row_callbacks::("profile_played_world", &self.profile_played_world, event); - callbacks.invoke_table_row_callbacks::("profile_save_archive", &self.profile_save_archive, event); - callbacks.invoke_table_row_callbacks::("profile_wallet_ledger", &self.profile_wallet_ledger, event); - callbacks.invoke_table_row_callbacks::("puzzle_agent_message", &self.puzzle_agent_message, event); - callbacks.invoke_table_row_callbacks::("puzzle_agent_session", &self.puzzle_agent_session, event); - callbacks.invoke_table_row_callbacks::("puzzle_runtime_run", &self.puzzle_runtime_run, event); - callbacks.invoke_table_row_callbacks::("puzzle_work_profile", &self.puzzle_work_profile, event); - callbacks.invoke_table_row_callbacks::("quest_log", &self.quest_log, event); - callbacks.invoke_table_row_callbacks::("quest_record", &self.quest_record, event); - callbacks.invoke_table_row_callbacks::("runtime_setting", &self.runtime_setting, event); - callbacks.invoke_table_row_callbacks::("runtime_snapshot", &self.runtime_snapshot, event); - callbacks.invoke_table_row_callbacks::("story_event", &self.story_event, event); - callbacks.invoke_table_row_callbacks::("story_session", &self.story_session, event); - callbacks.invoke_table_row_callbacks::("treasure_record", &self.treasure_record, event); - callbacks.invoke_table_row_callbacks::("user_browse_history", &self.user_browse_history, event); + callbacks.invoke_table_row_callbacks::("custom_world_gallery_entry", &self.custom_world_gallery_entry, event); } } @@ -2120,83 +1787,9 @@ impl __sdk::SpacetimeModule for RemoteModule { type QueryBuilder = __sdk::QueryBuilder; fn register_tables(client_cache: &mut __sdk::ClientCache) { - ai_result_reference_table::register_table(client_cache); - ai_task_table::register_table(client_cache); - ai_task_stage_table::register_table(client_cache); - ai_text_chunk_table::register_table(client_cache); - asset_entity_binding_table::register_table(client_cache); - asset_object_table::register_table(client_cache); - battle_state_table::register_table(client_cache); - big_fish_agent_message_table::register_table(client_cache); - big_fish_asset_slot_table::register_table(client_cache); - big_fish_creation_session_table::register_table(client_cache); - big_fish_runtime_run_table::register_table(client_cache); - chapter_progression_table::register_table(client_cache); - custom_world_agent_message_table::register_table(client_cache); - custom_world_agent_operation_table::register_table(client_cache); - custom_world_agent_session_table::register_table(client_cache); - custom_world_draft_card_table::register_table(client_cache); - custom_world_gallery_entry_table::register_table(client_cache); - custom_world_profile_table::register_table(client_cache); - custom_world_session_table::register_table(client_cache); - inventory_slot_table::register_table(client_cache); - npc_state_table::register_table(client_cache); - player_progression_table::register_table(client_cache); - profile_dashboard_state_table::register_table(client_cache); - profile_played_world_table::register_table(client_cache); - profile_save_archive_table::register_table(client_cache); - profile_wallet_ledger_table::register_table(client_cache); - puzzle_agent_message_table::register_table(client_cache); - puzzle_agent_session_table::register_table(client_cache); - puzzle_runtime_run_table::register_table(client_cache); - puzzle_work_profile_table::register_table(client_cache); - quest_log_table::register_table(client_cache); - quest_record_table::register_table(client_cache); - runtime_setting_table::register_table(client_cache); - runtime_snapshot_table::register_table(client_cache); - story_event_table::register_table(client_cache); - story_session_table::register_table(client_cache); - treasure_record_table::register_table(client_cache); - user_browse_history_table::register_table(client_cache); + custom_world_gallery_entry_table::register_table(client_cache); } const ALL_TABLE_NAMES: &'static [&'static str] = &[ - "ai_result_reference", - "ai_task", - "ai_task_stage", - "ai_text_chunk", - "asset_entity_binding", - "asset_object", - "battle_state", - "big_fish_agent_message", - "big_fish_asset_slot", - "big_fish_creation_session", - "big_fish_runtime_run", - "chapter_progression", - "custom_world_agent_message", - "custom_world_agent_operation", - "custom_world_agent_session", - "custom_world_draft_card", - "custom_world_gallery_entry", - "custom_world_profile", - "custom_world_session", - "inventory_slot", - "npc_state", - "player_progression", - "profile_dashboard_state", - "profile_played_world", - "profile_save_archive", - "profile_wallet_ledger", - "puzzle_agent_message", - "puzzle_agent_session", - "puzzle_runtime_run", - "puzzle_work_profile", - "quest_log", - "quest_record", - "runtime_setting", - "runtime_snapshot", - "story_event", - "story_session", - "treasure_record", - "user_browse_history", + "custom_world_gallery_entry", ]; } diff --git a/server-rs/crates/spacetime-module/src/lib.rs b/server-rs/crates/spacetime-module/src/lib.rs index 2224236d..ab619154 100644 --- a/server-rs/crates/spacetime-module/src/lib.rs +++ b/server-rs/crates/spacetime-module/src/lib.rs @@ -2178,6 +2178,7 @@ fn generate_big_fish_asset_tx( input.asset_kind, input.level, input.motion_key.clone(), + input.asset_url.clone(), input.generated_at_micros, ) .map_err(|error| error.to_string())?; @@ -2186,10 +2187,18 @@ fn generate_big_fish_asset_tx( let asset_slots = list_big_fish_asset_slots(ctx, &session.session_id); let coverage = build_asset_coverage(Some(&draft), &asset_slots); let updated_at = Timestamp::from_micros_since_unix_epoch(input.generated_at_micros); - let reply = match input.asset_kind { - BigFishAssetKind::LevelMainImage => "本级主图已生成并设为正式资产。", - BigFishAssetKind::LevelMotion => "本级动作已生成并设为正式资产。", - BigFishAssetKind::StageBackground => "活动区域背景已生成并设为正式资产。", + let uses_placeholder = input + .asset_url + .as_deref() + .map(str::trim) + .is_none_or(str::is_empty); + let reply = match (input.asset_kind, uses_placeholder) { + (BigFishAssetKind::LevelMainImage, true) => "本级主图占位图已生成,可在结果页继续预览。", + (BigFishAssetKind::LevelMainImage, false) => "本级主图已正式生成,可在结果页继续预览。", + (BigFishAssetKind::LevelMotion, true) => "本级动作占位图已生成,可在结果页继续预览。", + (BigFishAssetKind::LevelMotion, false) => "本级动作图已正式生成,可在结果页继续预览。", + (BigFishAssetKind::StageBackground, true) => "活动区域背景占位图已生成,可在结果页继续预览。", + (BigFishAssetKind::StageBackground, false) => "活动区域背景已正式生成,可在结果页继续预览。", } .to_string(); let next_stage = if coverage.publish_ready { diff --git a/src/components/big-fish-result/BigFishResultView.test.tsx b/src/components/big-fish-result/BigFishResultView.test.tsx new file mode 100644 index 00000000..2f8c883d --- /dev/null +++ b/src/components/big-fish-result/BigFishResultView.test.tsx @@ -0,0 +1,149 @@ +// @vitest-environment jsdom + +import { render, screen } from '@testing-library/react'; +import { describe, expect, test, vi } from 'vitest'; + +import type { BigFishSessionSnapshotResponse } from '../../../packages/shared/src/contracts/bigFish'; +import { BigFishResultView } from './BigFishResultView'; + +vi.mock('../ResolvedAssetImage', () => ({ + ResolvedAssetImage: ({ + src, + alt, + className, + }: { + src?: string | null; + alt?: string; + className?: string; + }) => (src ? {alt} : null), +})); + +function createSession(): BigFishSessionSnapshotResponse { + return { + sessionId: 'big-fish-session-1', + currentTurn: 2, + progressPercent: 88, + stage: 'asset_refining', + anchorPack: { + gameplayPromise: { + key: 'gameplayPromise', + label: '玩法承诺', + value: '弱小逆袭', + status: 'confirmed', + }, + ecologyVisualTheme: { + key: 'ecologyVisualTheme', + label: '生态与视觉母题', + value: '深海谜境', + status: 'confirmed', + }, + growthLadder: { + key: 'growthLadder', + label: '成长阶梯', + value: '8 级进化', + status: 'confirmed', + }, + riskTempo: { + key: 'riskTempo', + label: '风险节奏', + value: '平衡', + status: 'confirmed', + }, + }, + draft: { + title: '深海谜境', + subtitle: '逐级吞噬成长', + coreFun: '弱小逆袭', + ecologyTheme: '深海谜境', + levels: [ + { + level: 1, + name: '荧潮幼体', + oneLineFantasy: '在深海荧光裂谷中寻找第一个同伴。', + silhouetteDirection: '圆润鱼苗', + sizeRatio: 1, + visualPromptSeed: '深海荧光幼体', + motionPromptSeed: '轻微摆尾', + mergeSourceLevel: null, + preyWindow: [1], + threatWindow: [2], + isFinalLevel: false, + }, + ], + background: { + theme: '深海谜境', + colorMood: '深蓝与青绿', + foregroundHints: '漂浮微粒', + midgroundComposition: '中央留白', + backgroundDepth: '深海纵深', + safePlayAreaHint: '中央 70%', + spawnEdgeHint: '四周边缘', + backgroundPromptSeed: '深海谜境背景', + }, + runtimeParams: { + levelCount: 1, + mergeCountPerUpgrade: 3, + spawnTargetCount: 12, + leaderMoveSpeed: 160, + followerCatchUpSpeed: 120, + offscreenCullSeconds: 3, + preySpawnDeltaLevels: [1], + threatSpawnDeltaLevels: [1], + winLevel: 1, + }, + }, + assetSlots: [ + { + slotId: 'big-fish-asset-level-main', + assetKind: 'level_main_image', + level: 1, + motionKey: null, + status: 'ready', + assetUrl: + '/generated-big-fish-assets/big-fish-session-1/level-main-image/level-1/image.png', + promptSnapshot: '深海荧光幼体', + updatedAt: '2026-04-23T10:00:00.000Z', + }, + { + slotId: 'big-fish-asset-background', + assetKind: 'stage_background', + level: null, + motionKey: null, + status: 'ready', + assetUrl: + '/generated-big-fish-assets/big-fish-session-1/stage-background/image.png', + promptSnapshot: '深海谜境背景', + updatedAt: '2026-04-23T10:00:00.000Z', + }, + ], + assetCoverage: { + levelMainImageReadyCount: 1, + levelMotionReadyCount: 0, + backgroundReady: true, + requiredLevelCount: 1, + publishReady: false, + blockers: ['还缺少 2 个基础动作'], + }, + messages: [], + lastAssistantReply: '主图占位图已生成。', + publishReady: false, + updatedAt: '2026-04-23T10:00:00.000Z', + }; +} + +describe('BigFishResultView', () => { + test('renders generated formal previews with accurate status copy', () => { + render( + {}} + onExecuteAction={() => {}} + onStartTestRun={() => {}} + />, + ); + + expect(screen.getByText('主图 已生成')).toBeTruthy(); + expect(screen.getByAltText('荧潮幼体')).toBeTruthy(); + expect(screen.getByAltText('深海谜境 场地背景')).toBeTruthy(); + }); +}); diff --git a/src/components/big-fish-result/BigFishResultView.tsx b/src/components/big-fish-result/BigFishResultView.tsx index da41b7af..693b5db7 100644 --- a/src/components/big-fish-result/BigFishResultView.tsx +++ b/src/components/big-fish-result/BigFishResultView.tsx @@ -16,6 +16,7 @@ import type { BigFishSessionSnapshotResponse, ExecuteBigFishActionRequest, } from '../../../packages/shared/src/contracts/bigFish'; +import { ResolvedAssetImage } from '../ResolvedAssetImage'; type BigFishAssetStudioTarget = | { @@ -61,7 +62,10 @@ function findAssetSlot( } function assetReadyLabel(slot: BigFishAssetSlotResponse | undefined) { - return slot?.status === 'ready' ? '已生成' : '待生成'; + if (slot?.status !== 'ready') { + return '待生成'; + } + return isBigFishPlaceholderAsset(slot) ? '占位已生成' : '已生成'; } function buildLevelAssetPreview(slot: BigFishAssetSlotResponse | undefined) { @@ -71,15 +75,43 @@ function buildLevelAssetPreview(slot: BigFishAssetSlotResponse | undefined) { return null; } +function isBigFishPlaceholderAsset(slot: BigFishAssetSlotResponse | undefined) { + return Boolean(slot?.assetUrl?.includes('/generated-big-fish/')); +} + +function buildStudioAssetPreview( + slots: BigFishAssetSlotResponse[], + target: BigFishAssetStudioTarget, +) { + if (target.kind === 'stage_background') { + return buildLevelAssetPreview(findAssetSlot(slots, 'stage_background')); + } + if (target.kind === 'level_main_image') { + return buildLevelAssetPreview( + findAssetSlot(slots, 'level_main_image', target.level.level), + ); + } + return buildLevelAssetPreview( + findAssetSlot( + slots, + 'level_motion', + target.level.level, + target.motionKey, + ), + ); +} + function BigFishAssetStudioModal({ draft, target, + previewUrl, isBusy, onClose, onExecuteAction, }: { draft: BigFishGameDraftResponse; target: BigFishAssetStudioTarget; + previewUrl?: string | null; isBusy: boolean; onClose: () => void; onExecuteAction: (payload: ExecuteBigFishActionRequest) => void; @@ -140,8 +172,16 @@ function BigFishAssetStudioModal({ {prompt} -
- AI 资产候选预览 +
+ {previewUrl ? ( + + ) : ( + 'AI 资产候选预览' + )}
@@ -160,7 +200,7 @@ function BigFishAssetStudioModal({ className="inline-flex items-center gap-2 rounded-full bg-cyan-600 px-4 py-2 text-sm font-bold text-white disabled:opacity-45" > {isBusy ? : null} - 生成并设为正式资产 + 生成并应用正式图
@@ -203,7 +243,7 @@ function BigFishLevelCard({
{previewUrl ? ( - {level.name}(null); const draft = session.draft; const backgroundSlot = findAssetSlot(session.assetSlots, 'stage_background'); + const backgroundPreviewUrl = buildLevelAssetPreview(backgroundSlot); const blockers = useMemo( () => session.assetCoverage.blockers.filter(Boolean), [session.assetCoverage.blockers], ); + const studioPreviewUrl = useMemo(() => { + if (!studioTarget) { + return null; + } + return buildStudioAssetPreview(session.assetSlots, studioTarget); + }, [session.assetSlots, studioTarget]); if (!draft) { return ( @@ -404,7 +451,15 @@ export function BigFishResultView({
-
+
+ {backgroundPreviewUrl ? ( + + ) : null} +