refactor: split large modules and normalize rust layout

This commit is contained in:
kdletters
2026-05-18 19:40:14 +08:00
parent 472a47eae7
commit 269f35cecf
51 changed files with 17492 additions and 17169 deletions
+20 -1
View File
@@ -16,10 +16,29 @@
---
## 2026-05-18 Rust 手写模块入口统一不用 mod.rs
- 背景:Rust 目录模块同时存在 `mod.rs` 与同名 `.rs` 两种入口形式,前次拆分已让 `spacetime-client/src/mapper.rs` 采用同名入口;继续新增 `mod.rs` 会让文件定位和评审口径不一致。
- 决策:手写 Rust 模块统一使用同名入口文件,例如 `puzzle.rs``match3d.rs``gameplay.rs`,子模块继续放在同名目录下;不要再为手写模块新增 `mod.rs`。SpacetimeDB CLI 生成的 bindings 也由生成脚本同步为 `module_bindings.rs``module_bindings/` 子目录,避免仓库里继续出现 `mod.rs`
- 边界:本决策只规范文件布局,不改变 module path、HTTP route、DTO、SpacetimeDB schema、生成绑定内容或运行时行为。
- 影响范围:`server-rs/crates/api-server/src/``server-rs/crates/spacetime-module/src/``server-rs/crates/spacetime-client/src/module_bindings.rs``scripts/generate-spacetime-bindings.mjs`
- 验证方式:执行 `Get-ChildItem server-rs -Recurse -Filter mod.rs` 应无结果;再执行对应 `cargo check` / 定向测试 / 编码检查。
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`
## 2026-05-18 大文件拆分继续按聚合入口加领域子模块推进
- 背景:完成拼图 `api-server` 拆分后,`match3d.rs``spacetime-client/src/mapper.rs``PlatformEntryFlowShellImpl.tsx` 仍是后续迭代和评审的高噪音大文件。
- 决策:抓大鹅 Match3D 的 `api-server` 单文件改为同名入口 `src/match3d.rs``src/match3d/` 子模块目录,`handlers.rs``draft.rs``works.rs``item_assets.rs``runtime.rs``vector_engine_gemini.rs``mappers.rs``tags.rs``tests.rs` 分担原实现;`spacetime-client/src/mapper.rs` 改为聚合入口,具体 mapper 按领域落到 `src/mapper/*.rs`;平台入口继续以 `PlatformEntryFlowShellImpl.tsx` 为编排壳,独立 UI 片段优先拆到 `PlatformEntryFlowShellImpl/` 子目录,本次已抽出 `PuzzleOnboardingView.tsx`
- 边界:这些拆分只改变文件组织,不改变 HTTP route、DTO、error envelope、SpacetimeDB schema、生成绑定、procedure result、入口配置事实源、前端行为、VectorEngine / OSS 副作用或计费语义。后续要下沉领域规则时另行讨论并更新设计。
- 影响范围:`server-rs/crates/api-server/src/match3d/``server-rs/crates/spacetime-client/src/mapper/``src/components/platform-entry/PlatformEntryFlowShellImpl/`、后端架构文档和玩法链路文档。
- 验证方式:执行 `cargo check -p api-server --manifest-path server-rs\Cargo.toml``cargo test -p api-server match3d --manifest-path server-rs\Cargo.toml --no-run``cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml`、前端 typecheck 或定向 tsc、`git diff --check``npm run check:encoding`
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md``docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
## 2026-05-18 api-server 拼图能力按 HTTP/BFF 子模块拆分
- 背景:`server-rs/crates/api-server/src/puzzle.rs` 已膨胀为数千行大文件,混合 Axum handler、草稿编译、图片生成、VectorEngine / OSS 持久化、DTO mapper、标签生成和测试;继续在单文件内迭代会降低定位和评审效率。
- 决策:删除单文件 `puzzle.rs`改为 `server-rs/crates/api-server/src/puzzle/` 目录模块。`mod.rs` 只保留聚合入口和 handler re-export`handlers.rs` 放 HTTP handler`draft.rs` 放表单草稿 / 编译 / snapshot helper`generation.rs` 放图片与 UI 背景生成编排;`vector_engine.rs` 放 VectorEngine、下载、OSS、asset object / binding 和错误归一;`mappers.rs` / `tags.rs` 保留映射和标签 / 错误 helper;`tests.rs` 承接原 puzzle 单测。
- 决策:原超大 `puzzle.rs` 改为同名入口 `server-rs/crates/api-server/src/puzzle.rs` `server-rs/crates/api-server/src/puzzle/` 子模块目录。`puzzle.rs` 只保留聚合入口和 handler re-export`handlers.rs` 放 HTTP handler`draft.rs` 放表单草稿 / 编译 / snapshot helper`generation.rs` 放图片与 UI 背景生成编排;`vector_engine.rs` 放 VectorEngine、下载、OSS、asset object / binding 和错误归一;`mappers.rs` / `tags.rs` 保留映射和标签 / 错误 helper;`tests.rs` 承接原 puzzle 单测。
- 边界:本次只改变 `api-server` 内部文件组织,不改变 `/api/runtime/puzzle/*` 路由、DTO、error envelope、SpacetimeDB schema、公开 gallery cache 语义或计费语义。领域规则后续仍应逐步沉到 `module-puzzle`SpacetimeDB 表、reducer、procedure 和 row shape 仍留在 `spacetime-module`
- 影响范围:`server-rs/crates/api-server/src/puzzle/``server-rs/crates/api-server/src/modules/puzzle.rs` 的 handler 引用、后端架构文档。
- 验证方式:执行 `cargo check -p api-server --manifest-path server-rs\Cargo.toml`;后续若改动 puzzle API 行为,再按对应路由补充定向测试和 `npm run dev:api-server` `/healthz` smoke。
+3 -3
View File
@@ -44,7 +44,7 @@
- 原因:封面生成属于定向图片槽位更新;若后端复用草稿编译写回,可能按 session config 重算作品行。即使后端已修正,前端若直接把封面接口返回的整份 `item` 当成最新 profile,也可能用旧回包里的空 `generatedItemAssets` 覆盖当前页面素材。
- 处理:`POST /api/creation/match3d/works/{profileId}/cover-image` 只保存 `coverImageSrc` / `coverAssetId` 等封面字段,保留当前 `generated_item_assets_json`、难度、消除次数、题材和描述;前端收到回包后只合并 `coverImageSrc`,继续保留当前可见 `generatedItemAssets``clearCount``difficulty`
- 验证:`npm run test -- src\components\match3d-result\Match3DResultView.test.tsx` 覆盖旧回包不覆盖物品素材和配置;`cargo test -p api-server match3d_cover --manifest-path server-rs\Cargo.toml` 覆盖封面提示词与参考图链路。
- 关联:`src/components/match3d-result/Match3DResultView.tsx``server-rs/crates/api-server/src/match3d.rs``server-rs/crates/spacetime-module/src/match3d/mod.rs``docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`
- 关联:`src/components/match3d-result/Match3DResultView.tsx``server-rs/crates/api-server/src/match3d.rs``server-rs/crates/spacetime-module/src/match3d.rs``docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`
## OSS V4 签名时间和 bucket/object_key 兼容
@@ -113,7 +113,7 @@
- 原因:个人作品列表和公开广场列表复用了同一套 procedure 输入,导致公开列表为了通过 owner 校验传固定占位 owner,并把可长期同步的公开读模型当成请求期查询。
- 处理:每个公开广场新增或复用专用 public view / public read model`match_3_d_gallery_view``square_hole_gallery_view``visual_novel_gallery_view``big_fish_gallery_view``spacetime-client` 建连接后订阅这些 view 和对应 `public_work_play_daily_stat` source_type 桶,HTTP gallery 只读本地 cache。个人作品列表、详情、发布、点赞、游玩记录和 Remix 仍走原有 procedure / reducer。
- 验证:搜索 `server-rs/crates/spacetime-client/src/{match3d,square_hole,visual_novel,big_fish}.rs`,公开 gallery 主路径应读取 `connection.db().*_gallery_view()`,不应调用 `list_*_works_with_input`;执行 `npm run spacetime:generate``cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml``cargo check -p api-server --manifest-path server-rs/Cargo.toml``npm run check:spacetime-schema`
- 关联:`server-rs/crates/spacetime-module/src/match3d/mod.rs``server-rs/crates/spacetime-module/src/square_hole/mod.rs``server-rs/crates/spacetime-module/src/visual_novel.rs``server-rs/crates/spacetime-module/src/big_fish/session.rs``docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`
- 关联:`server-rs/crates/spacetime-module/src/match3d.rs``server-rs/crates/spacetime-module/src/square_hole.rs``server-rs/crates/spacetime-module/src/visual_novel.rs``server-rs/crates/spacetime-module/src/big_fish/session.rs``docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`
## 自定义世界广场和创作入口配置不要每次 HTTP 请求调用只读 procedure
@@ -846,7 +846,7 @@
- 原因:旧运行态把消除次数和类型数量绑在一起,结果页文案又同时展示“素材图片 / 局内类型”,导致前端、发布校验和 run start 口径不一致。
- 处理:统一使用 `物品种类` 口径:轻松 3、标准 9、进阶 15、硬核 21;历史 `clearCount=20` 且难度为硬核的运行态按新硬核升为 21 组三消,避免 20 组却要求 21 种素材。发布前按 `image_ready` 且有 `imageViews[]``imageSrc/imageObjectKey` 的生成素材数量阻断不足难度;试玩不阻断,但通过 `itemTypeCountOverride` 自动降到已生成 2D 素材数量。重启从已有 run 快照反推实际物品种类,保持同一局重开不变。
- 验证:`npm run test -- src\components\match3d-result\Match3DResultView.test.tsx``cargo test -p module-match3d --manifest-path server-rs\Cargo.toml`,涉及发布 reducer 时补跑 `cargo test -p spacetime-module match3d --manifest-path server-rs\Cargo.toml`
- 关联:`src/components/match3d-result/Match3DResultView.tsx``src/services/match3d-runtime/match3dRuntimeClient.ts``server-rs/crates/module-match3d/src/application.rs``server-rs/crates/spacetime-module/src/match3d/mod.rs``docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`
- 关联:`src/components/match3d-result/Match3DResultView.tsx``src/services/match3d-runtime/match3dRuntimeClient.ts``server-rs/crates/module-match3d/src/application.rs``server-rs/crates/spacetime-module/src/match3d.rs``docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`
## 抓大鹅标签清洗不要把 `3D素材` 当编号剥掉
@@ -40,6 +40,12 @@ server-rs + Axum + SpacetimeDB
npm run check:server-rs-ddd
```
## `spacetime-client` mapper 组织
`server-rs/crates/spacetime-client/src/mapper.rs` 只作为聚合入口,负责声明 `src/mapper/` 下的领域子模块并 re-export 原有 record / mapper 能力;不要在该文件继续堆叠大段映射实现。
当前子模块按调用领域拆分:`assets.rs``auth.rs``runtime.rs``runtime_profile.rs``custom_world.rs``puzzle.rs``match3d.rs``square_hole.rs``visual_novel.rs``big_fish.rs``story.rs``ai.rs``bark_battle.rs``combat.rs``inventory.rs``npc.rs`,跨领域轻量 helper 和共享 record 统一放在 `common.rs`。该拆分只改变 `spacetime-client` 文件组织,不改变 SpacetimeDB schema、生成绑定、procedure result 契约或外部 DTO;后续新增 mapper 时优先落到对应领域子模块,不得重新引入跨层 JSON 字符串兼容结构。
## API 路由分组
路由树由 `server-rs/crates/api-server/src/app.rs` 统一构造。当前主要分组:
@@ -73,11 +79,12 @@ npm run check:server-rs-ddd
2. `app.rs` 只保留全局 middleware、TraceLayer、request context、tracking middleware、入口开关和少量顶层 glue。
3. 路由迁移和业务重构分阶段处理;先移动路由装配,再拆 handler 内部实现。
4. 大 handler 拆分时优先按 `router.rs``handlers.rs``application.rs``assets.rs``mapper.rs``errors.rs` 分层。`handlers.rs` 只做 Axum extract、鉴权和 request/response,业务规则继续下沉到 `module-*`
5. 手写 Rust 模块入口统一使用同名 `.rs` 文件,例如 `puzzle.rs` + `puzzle/*.rs``match3d.rs` + `match3d/*.rs`;不要再新增 `mod.rs` 入口。生成的 SpacetimeDB Rust bindings 也由生成脚本同步为 `module_bindings.rs` + `module_bindings/*.rs` 布局。
拼图 `api-server` 内部拆分:
- `server-rs/crates/api-server/src/modules/puzzle.rs` 只负责路由装配、鉴权层和参考图 body limit;对外继续引用同一批 handler 名称。
- `server-rs/crates/api-server/src/puzzle/mod.rs` 只作为聚合入口,保留共享 import / 常量、内部模块声明和 handler re-export,不继续承载大段实现。
- `server-rs/crates/api-server/src/puzzle.rs` 只作为聚合入口,保留共享 import / 常量、内部模块声明和 handler re-export,不继续承载大段实现。
- `server-rs/crates/api-server/src/puzzle/handlers.rs` 承接 Axum handler,负责 extract、鉴权上下文、调用 SpacetimeDB facade / 编排 helper,并返回 HTTP/SSE 响应。
- `server-rs/crates/api-server/src/puzzle/draft.rs` 承接表单草稿保存、草稿编译、首关命名、UI 背景 prompt、降级 snapshot 和初始资产就绪校验。
- `server-rs/crates/api-server/src/puzzle/generation.rs` 承接拼图图片与 UI 背景的生成编排、计费包裹和 reference image 路径选择。
@@ -87,6 +94,19 @@ npm run check:server-rs-ddd
该拆分只改变 `api-server` 文件组织,不改变 `/api/runtime/puzzle/*` route、DTO、error envelope、SpacetimeDB schema、公开 gallery cache 语义或计费语义;后续继续细分时也必须先保持行为不变,再单独讨论领域规则下沉。
抓大鹅 Match3D `api-server` 内部拆分:
- `server-rs/crates/api-server/src/modules/match3d.rs` 继续负责路由装配和 body limit;对外 handler 名称保持不变。
- `server-rs/crates/api-server/src/match3d.rs` 只作为聚合入口,保留共享 import / 常量 / 内部类型、模块声明和 handler re-export。
- `server-rs/crates/api-server/src/match3d/handlers.rs` 承接 Axum handler,负责 extract、鉴权上下文、调用 SpacetimeDB facade / 编排 helper,并返回 HTTP 响应。
- `server-rs/crates/api-server/src/match3d/draft.rs` 承接 Agent session、草稿编译、题材 / 难度 / 物品计划和草稿持久化编排。
- `server-rs/crates/api-server/src/match3d/works.rs` 承接作品 CRUD、封面 / 背景 / 容器资产生成入口、发布 / Remix / 点赞 / 游玩记录和作品级 helper。
- `server-rs/crates/api-server/src/match3d/item_assets.rs` 承接物品 sheet 生成、绿幕 / 近白底透明化、切图、append / replace / delete / sort / merge 和素材持久化。
- `server-rs/crates/api-server/src/match3d/vector_engine_gemini.rs` 承接 VectorEngine Gemini 请求体、响应解析、base64 图片下载和上游错误归一。
- `server-rs/crates/api-server/src/match3d/runtime.rs` 保留运行态轻量归一 helper`mappers.rs` / `tags.rs` / `tests.rs` 分别承接 DTO 映射、标签 / 通用错误 helper 和原有单测。
该拆分只改变 `api-server` 文件组织,不改变 `/api/creation/match3d/*``/api/runtime/match3d/*` route、DTO、error envelope、SpacetimeDB schema、公开 gallery cache 语义、VectorEngine / OSS 副作用边界或计费语义;后续继续细分时也必须先保持行为不变,再单独讨论领域规则下沉到 `module-match3d`
生成资产 Adapter 规则:
1. 稳定单图链路可收敛到 `api-server` 内部生成资产 Adapterprovider 生成、下载/base64 解码、MIME/extension 归一、OSS private upload、HEAD、asset object confirm、entity binding。
@@ -240,7 +260,7 @@ npm run check:server-rs-ddd
### `battle_state`
- Rust 结构体:`BattleState`
- 源码:`server-rs/crates/spacetime-module/src/gameplay/mod.rs`
- 源码:`server-rs/crates/spacetime-module/src/gameplay.rs`
### `big_fish_agent_message`
@@ -278,7 +298,7 @@ npm run check:server-rs-ddd
### `chapter_progression`
- Rust 结构体:`ChapterProgression`
- 源码:`server-rs/crates/spacetime-module/src/gameplay/mod.rs`
- 源码:`server-rs/crates/spacetime-module/src/gameplay.rs`
### `creation_entry_config`
@@ -293,38 +313,38 @@ npm run check:server-rs-ddd
### `custom_world_agent_message`
- Rust 结构体:`CustomWorldAgentMessage`
- 源码:`server-rs/crates/spacetime-module/src/custom_world/mod.rs`
- 源码:`server-rs/crates/spacetime-module/src/custom_world.rs`
### `custom_world_agent_operation`
- Rust 结构体:`CustomWorldAgentOperation`
- 源码:`server-rs/crates/spacetime-module/src/custom_world/mod.rs`
- 源码:`server-rs/crates/spacetime-module/src/custom_world.rs`
### `custom_world_agent_session`
- Rust 结构体:`CustomWorldAgentSession`
- 源码:`server-rs/crates/spacetime-module/src/custom_world/mod.rs`
- 源码:`server-rs/crates/spacetime-module/src/custom_world.rs`
### `custom_world_draft_card`
- Rust 结构体:`CustomWorldDraftCard`
- 源码:`server-rs/crates/spacetime-module/src/custom_world/mod.rs`
- 源码:`server-rs/crates/spacetime-module/src/custom_world.rs`
### `custom_world_gallery_entry`
- Rust 结构体:`CustomWorldGalleryEntry`
- 源码:`server-rs/crates/spacetime-module/src/custom_world/mod.rs`
- 源码:`server-rs/crates/spacetime-module/src/custom_world.rs`
- 作用:自定义世界公开作品列表读模型。`api-server``spacetime-client` 长期订阅 `SELECT * FROM custom_world_gallery_entry``SELECT * FROM public_work_play_daily_stat WHERE source_type = 'custom-world'``/api/runtime/custom-world-gallery` 从本地 cache 排序并聚合 `recentPlayCount7d`,不再每个 HTTP 请求调用 `list_custom_world_gallery_entries` procedure。旧 procedure 只用于兼容旧库缺少 gallery 读模型行时的一次性同步兜底。
### `custom_world_profile`
- Rust 结构体:`CustomWorldProfile`
- 源码:`server-rs/crates/spacetime-module/src/custom_world/mod.rs`
- 源码:`server-rs/crates/spacetime-module/src/custom_world.rs`
### `custom_world_session`
- Rust 结构体:`CustomWorldSession`
- 源码:`server-rs/crates/spacetime-module/src/custom_world/mod.rs`
- 源码:`server-rs/crates/spacetime-module/src/custom_world.rs`
### `database_migration_import_chunk`
@@ -339,7 +359,7 @@ npm run check:server-rs-ddd
### `inventory_slot`
- Rust 结构体:`InventorySlot`
- 源码:`server-rs/crates/spacetime-module/src/gameplay/mod.rs`
- 源码:`server-rs/crates/spacetime-module/src/gameplay.rs`
### `match3d_agent_message`
@@ -365,18 +385,18 @@ npm run check:server-rs-ddd
- Rust view`match3d_gallery_view`
- 返回类型:`Vec<Match3DGalleryViewRow>`
- 源码:`server-rs/crates/spacetime-module/src/match3d/mod.rs`
- 源码:`server-rs/crates/spacetime-module/src/match3d.rs`
- 说明:抓大鹅公开广场列表投影,只暴露 `publication_status = published` 的作品卡片字段;`api-server``spacetime-client` 长期订阅 `SELECT * FROM match_3_d_gallery_view``SELECT * FROM public_work_play_daily_stat WHERE source_type = 'match3d'` 后,从本地 cache 构造 `/api/runtime/match3d/gallery` 响应。公开列表不再每个 HTTP 请求调用 `list_match3d_works` procedure;个人作品列表、详情、发布、点赞、游玩记录和 Remix 仍按原有 procedure / reducer 路径处理。
### `npc_state`
- Rust 结构体:`NpcState`
- 源码:`server-rs/crates/spacetime-module/src/gameplay/mod.rs`
- 源码:`server-rs/crates/spacetime-module/src/gameplay.rs`
### `player_progression`
- Rust 结构体:`PlayerProgression`
- 源码:`server-rs/crates/spacetime-module/src/gameplay/mod.rs`
- 源码:`server-rs/crates/spacetime-module/src/gameplay.rs`
### `profile_dashboard_state`
@@ -546,12 +566,12 @@ npm run check:server-rs-ddd
### `quest_log`
- Rust 结构体:`QuestLog`
- 源码:`server-rs/crates/spacetime-module/src/gameplay/mod.rs`
- 源码:`server-rs/crates/spacetime-module/src/gameplay.rs`
### `quest_record`
- Rust 结构体:`QuestRecord`
- 源码:`server-rs/crates/spacetime-module/src/gameplay/mod.rs`
- 源码:`server-rs/crates/spacetime-module/src/gameplay.rs`
### `refresh_session`
@@ -592,18 +612,18 @@ npm run check:server-rs-ddd
- Rust view`square_hole_gallery_view`
- 返回类型:`Vec<SquareHoleGalleryViewRow>`
- 源码:`server-rs/crates/spacetime-module/src/square_hole/mod.rs`
- 源码:`server-rs/crates/spacetime-module/src/square_hole.rs`
- 说明:方洞挑战公开广场列表投影,只暴露 `publication_status = published` 的作品卡片字段;`api-server``spacetime-client` 长期订阅 `SELECT * FROM square_hole_gallery_view``SELECT * FROM public_work_play_daily_stat WHERE source_type = 'square-hole'` 后,从本地 cache 构造 `/api/runtime/square-hole/gallery` 响应。公开列表不再每个 HTTP 请求调用 `list_square_hole_works` procedure;个人作品列表、详情、发布、点赞、游玩记录和 Remix 仍按原有 procedure / reducer 路径处理。
### `story_event`
- Rust 结构体:`StoryEvent`
- 源码:`server-rs/crates/spacetime-module/src/gameplay/mod.rs`
- 源码:`server-rs/crates/spacetime-module/src/gameplay.rs`
### `story_session`
- Rust 结构体:`StorySession`
- 源码:`server-rs/crates/spacetime-module/src/gameplay/mod.rs`
- 源码:`server-rs/crates/spacetime-module/src/gameplay.rs`
### `tracking_daily_stat`
@@ -618,7 +638,7 @@ npm run check:server-rs-ddd
### `treasure_record`
- Rust 结构体:`TreasureRecord`
- 源码:`server-rs/crates/spacetime-module/src/gameplay/mod.rs`
- 源码:`server-rs/crates/spacetime-module/src/gameplay.rs`
### `user_account`
@@ -8,6 +8,8 @@
当前创作 Tab 固定为智能创作首页与模板入口,草稿 Tab 承接作品架。点击独立入口后应切换到对应内嵌创作表单或生成页;不要额外做一张平行配置页,除非玩法本身需要完整独立工作台。
`PlatformEntryFlowShellImpl.tsx` 仍是平台入口编排壳,后续维护时应优先把独立 UI 片段、公开作品映射、草稿生成 notice 和运行态状态 helper 拆到 `src/components/platform-entry/PlatformEntryFlowShellImpl/` 或同目录紧邻 helper 文件。拆分只允许改变文件组织,不改变入口配置事实源、默认导出、props、页面阶段、UI 文案或现有交互;其中拼图首访 onboarding 已拆为 `PlatformEntryFlowShellImpl/PuzzleOnboardingView.tsx`
## 草稿与作品架
1. 草稿页作品卡对齐发现页列表卡风格:左侧信息,右侧封面图,移动端单列,桌面两到三列。
+3 -3
View File
@@ -29,7 +29,7 @@ const forbiddenSnippets = [
reason: 'puzzle_leaderboard_entry 已有 by_puzzle_leaderboard_profile_grid 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/match3d/mod.rs',
file: 'server-rs/crates/spacetime-module/src/match3d.rs',
snippet: '.match3d_work_profile()\n .iter()\n .filter(|row| {',
reason: 'match3d_work_profile 已有 owner/status 索引,列表不应整表过滤',
},
@@ -99,12 +99,12 @@ const forbiddenSnippets = [
reason: 'tracking_daily_stat 已有 by_tracking_daily_stat_scope_day / event_day 索引,analytics 查询不应整表过滤',
},
{
file: 'server-rs/crates/spacetime-module/src/custom_world/mod.rs',
file: 'server-rs/crates/spacetime-module/src/custom_world.rs',
snippet: '.custom_world_profile()\n .iter()\n .find(|row| {',
reason: 'custom_world_profile owner 维度已有 by_custom_world_profile_owner_user_id 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/custom_world/mod.rs',
file: 'server-rs/crates/spacetime-module/src/custom_world.rs',
snippet: '.custom_world_profile()\n .iter()\n .filter(|profile| {',
reason: 'custom_world_profile Published 同步已有 by_custom_world_profile_publication_status 索引',
},
+26
View File
@@ -21,6 +21,14 @@ const TARGETS = [
'src',
'module_bindings',
),
entryFile: path.join(
REPO_ROOT,
'server-rs',
'crates',
'spacetime-client',
'src',
'module_bindings.rs',
),
},
];
@@ -64,6 +72,7 @@ for (const target of selectedTargets) {
console.log(`[spacetime:generate] 同步 ${fileCount} 个文件到 ${target.outDir}`);
await replaceGeneratedDir(tempOutDir, target.outDir);
await moveGeneratedEntryFile(target);
}
await rm(tempRoot, {recursive: true, force: true});
@@ -111,6 +120,23 @@ async function replaceGeneratedDir(fromDir, toDir) {
}
}
async function moveGeneratedEntryFile(target) {
if (!target.entryFile) {
return;
}
assertInside(target.entryFile, REPO_ROOT, '生成入口文件');
const generatedModFile = path.join(target.outDir, 'mod.rs');
if (!existsSync(generatedModFile)) {
throw new Error(`${target.name} bindings 缺少入口文件: ${generatedModFile}`);
}
await rm(target.entryFile, {force: true});
await cp(generatedModFile, target.entryFile, {force: true});
await rm(generatedModFile, {force: true});
}
function assertInside(candidate, parent, label) {
const relative = path.relative(path.resolve(parent), path.resolve(candidate));
if (relative === '' || relative.startsWith('..') || path.isAbsolute(relative)) {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,37 @@
pub(super) fn normalize_match3d_run_status(value: &str) -> &str {
match value {
"Running" => "running",
"Won" => "won",
"Failed" => "failed",
"Stopped" => "stopped",
_ => value,
}
}
pub(super) fn normalize_match3d_item_state(value: &str) -> &str {
match value {
"InBoard" => "in_board",
"InTray" => "in_tray",
"Cleared" => "cleared",
_ => value,
}
}
pub(super) fn normalize_match3d_failure_reason(value: &str) -> &str {
match value {
"TimeUp" => "time_up",
"TrayFull" => "tray_full",
_ => value,
}
}
pub(super) fn normalize_match3d_click_reject_reason(value: &str) -> &str {
match value {
"RejectedNotClickable" => "item_not_clickable",
"RejectedAlreadyMoved" => "item_not_in_board",
"RejectedTrayFull" => "tray_full",
"VersionConflict" => "snapshot_version_mismatch",
"RunFinished" => "run_not_active",
_ => value,
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,483 @@
use super::*;
pub(super) async fn generate_match3d_material_sheet(
state: &AppState,
config: &Match3DConfigJson,
item_names: &[String],
) -> Result<Match3DMaterialSheet, AppError> {
let settings = require_match3d_vector_engine_gemini_image_settings(state)?;
let http_client = build_match3d_vector_engine_gemini_image_http_client(&settings)?;
let prompt = build_match3d_material_sheet_prompt(config, item_names);
let negative_prompt = build_match3d_material_sheet_negative_prompt(config);
let generated = create_match3d_vector_engine_gemini_image_generation(
&http_client,
&settings,
prompt.as_str(),
negative_prompt.as_str(),
"抓大鹅素材图生成失败",
)
.await?;
let image = generated.images.into_iter().next().ok_or_else(|| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine-gemini",
"message": "抓大鹅素材图生成失败:未返回图片",
}))
})?;
Ok(Match3DMaterialSheet {
task_id: generated.task_id,
image,
})
}
fn require_match3d_vector_engine_gemini_image_settings(
state: &AppState,
) -> Result<Match3DVectorEngineGeminiImageSettings, AppError> {
let base_url = state
.config
.vector_engine_base_url
.trim()
.trim_end_matches('/');
if base_url.is_empty() {
return Err(
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
"provider": "vector-engine-gemini",
"reason": "VECTOR_ENGINE_BASE_URL 未配置",
})),
);
}
let api_key = state
.config
.vector_engine_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": "vector-engine-gemini",
"reason": "VECTOR_ENGINE_API_KEY 未配置",
}))
})?;
Ok(Match3DVectorEngineGeminiImageSettings {
base_url: base_url.to_string(),
api_key: api_key.to_string(),
request_timeout_ms: state.config.vector_engine_image_request_timeout_ms.max(1),
})
}
fn build_match3d_vector_engine_gemini_image_http_client(
settings: &Match3DVectorEngineGeminiImageSettings,
) -> Result<reqwest::Client, AppError> {
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": "vector-engine-gemini",
"message": format!("构造抓大鹅 VectorEngine Gemini 图片生成 HTTP 客户端失败:{error}"),
}))
})
}
async fn create_match3d_vector_engine_gemini_image_generation(
http_client: &reqwest::Client,
settings: &Match3DVectorEngineGeminiImageSettings,
prompt: &str,
negative_prompt: &str,
failure_context: &str,
) -> Result<OpenAiGeneratedImages, AppError> {
let request_body = build_match3d_vector_engine_gemini_image_request_body(
prompt,
negative_prompt,
MATCH3D_MATERIAL_VECTOR_ENGINE_GEMINI_ASPECT_RATIO,
);
let response = http_client
.post(build_match3d_vector_engine_gemini_generate_content_url(
settings,
))
.query(&[("key", settings.api_key.as_str())])
.header(header::ACCEPT, "application/json")
.header(header::CONTENT_TYPE, "application/json")
.json(&request_body)
.send()
.await
.map_err(|error| {
map_match3d_vector_engine_gemini_image_request_error(format!(
"{failure_context}:调用 VectorEngine Gemini 图片生成失败:{error}"
))
})?;
let status = response.status();
let response_text = response.text().await.map_err(|error| {
map_match3d_vector_engine_gemini_image_request_error(format!(
"{failure_context}:读取 VectorEngine Gemini 图片生成响应失败:{error}"
))
})?;
if !status.is_success() {
return Err(map_match3d_vector_engine_gemini_image_upstream_error(
status,
response_text.as_str(),
failure_context,
));
}
let payload = parse_match3d_json_payload(
response_text.as_str(),
"解析抓大鹅 VectorEngine Gemini 图片生成响应失败",
"vector-engine-gemini",
)?;
let image_urls = extract_match3d_image_urls(&payload);
if !image_urls.is_empty() {
return download_match3d_images_from_urls(
http_client,
format!("vector-engine-gemini-{}", current_utc_micros()),
image_urls,
1,
"vector-engine-gemini",
)
.await;
}
let b64_images = extract_match3d_b64_images(&payload);
if !b64_images.is_empty() {
return Ok(match3d_images_from_base64(
format!("vector-engine-gemini-{}", current_utc_micros()),
b64_images,
1,
));
}
Err(
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine-gemini",
"message": "抓大鹅 VectorEngine Gemini 图片生成未返回图片",
"rawExcerpt": trim_match3d_upstream_excerpt(response_text.as_str(), 800),
})),
)
}
pub(super) fn build_match3d_vector_engine_gemini_image_request_body(
prompt: &str,
negative_prompt: &str,
aspect_ratio: &str,
) -> Value {
json!({
"contents": [{
"role": "user",
"parts": [{
"text": build_match3d_vector_engine_gemini_prompt(prompt, negative_prompt),
}],
}],
"generationConfig": {
"responseModalities": ["TEXT", "IMAGE"],
"imageConfig": {
"aspectRatio": aspect_ratio,
},
},
})
}
pub(super) fn build_match3d_vector_engine_gemini_generate_content_url(
settings: &Match3DVectorEngineGeminiImageSettings,
) -> String {
let base_url = settings.base_url.trim_end_matches("/v1");
format!(
"{}/v1beta/models/{}:generateContent",
base_url, MATCH3D_MATERIAL_VECTOR_ENGINE_GEMINI_MODEL
)
}
fn build_match3d_vector_engine_gemini_prompt(prompt: &str, negative_prompt: &str) -> String {
let prompt = prompt.trim();
let negative_prompt = negative_prompt.trim();
if negative_prompt.is_empty() {
return prompt.to_string();
}
format!("{prompt}\n避免:{negative_prompt}")
}
async fn download_match3d_images_from_urls(
http_client: &reqwest::Client,
task_id: String,
image_urls: Vec<String>,
candidate_count: u32,
provider: &str,
) -> Result<OpenAiGeneratedImages, AppError> {
let mut images = Vec::with_capacity(candidate_count.clamp(1, 4) as usize);
for image_url in image_urls
.into_iter()
.take(candidate_count.clamp(1, 4) as usize)
{
images
.push(download_match3d_remote_image(http_client, image_url.as_str(), provider).await?);
}
Ok(OpenAiGeneratedImages {
task_id,
actual_prompt: None,
images,
})
}
async fn download_match3d_remote_image(
http_client: &reqwest::Client,
image_url: &str,
provider: &str,
) -> Result<DownloadedOpenAiImage, AppError> {
let response = http_client.get(image_url).send().await.map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": provider,
"message": format!("下载抓大鹅生成图片失败:{error}"),
}))
})?;
let status = response.status();
let content_type = response
.headers()
.get(header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.unwrap_or("image/png")
.to_string();
let body = response.bytes().await.map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": provider,
"message": format!("读取抓大鹅生成图片内容失败:{error}"),
}))
})?;
if !status.is_success() {
return Err(
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": provider,
"message": "下载抓大鹅生成图片失败",
"status": status.as_u16(),
})),
);
}
let mime_type = normalize_match3d_downloaded_image_mime_type(content_type.as_str());
Ok(DownloadedOpenAiImage {
extension: match3d_mime_to_extension(mime_type.as_str()).to_string(),
mime_type,
bytes: body.to_vec(),
})
}
fn match3d_images_from_base64(
task_id: String,
b64_images: Vec<String>,
candidate_count: u32,
) -> OpenAiGeneratedImages {
let images = b64_images
.into_iter()
.take(candidate_count.clamp(1, 4) as usize)
.filter_map(|raw| decode_match3d_base64_image(raw.as_str()))
.collect();
OpenAiGeneratedImages {
task_id,
actual_prompt: None,
images,
}
}
fn decode_match3d_base64_image(raw: &str) -> Option<DownloadedOpenAiImage> {
let bytes = BASE64_STANDARD.decode(raw.trim()).ok()?;
let mime_type = infer_match3d_image_mime_type(bytes.as_slice()).to_string();
Some(DownloadedOpenAiImage {
extension: match3d_mime_to_extension(mime_type.as_str()).to_string(),
mime_type,
bytes,
})
}
fn parse_match3d_json_payload(
raw_text: &str,
failure_context: &str,
provider: &str,
) -> Result<Value, AppError> {
serde_json::from_str::<Value>(raw_text).map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": provider,
"message": format!("{failure_context}{error}"),
"rawExcerpt": trim_match3d_upstream_excerpt(raw_text, 800),
}))
})
}
fn extract_match3d_image_urls(payload: &Value) -> Vec<String> {
let mut urls = Vec::new();
collect_match3d_strings_by_key(payload, "url", &mut urls);
collect_match3d_strings_by_key(payload, "image", &mut urls);
collect_match3d_strings_by_key(payload, "image_url", &mut urls);
let mut deduped = Vec::new();
for url in urls {
if (url.starts_with("http://") || url.starts_with("https://")) && !deduped.contains(&url) {
deduped.push(url);
}
}
deduped
}
pub(super) fn extract_match3d_b64_images(payload: &Value) -> Vec<String> {
let mut values = Vec::new();
collect_match3d_strings_by_key(payload, "b64_json", &mut values);
collect_match3d_inline_image_data(payload, &mut values);
values
}
fn collect_match3d_inline_image_data(payload: &Value, results: &mut Vec<String>) {
match payload {
Value::Array(entries) => {
for entry in entries {
collect_match3d_inline_image_data(entry, results);
}
}
Value::Object(object) => {
for key in ["inlineData", "inline_data"] {
if let Some(Value::Object(inline_data)) = object.get(key) {
let mime_type = inline_data
.get("mimeType")
.or_else(|| inline_data.get("mime_type"))
.and_then(Value::as_str)
.map(str::trim)
.unwrap_or("image/png")
.to_ascii_lowercase();
if !mime_type.is_empty() && !mime_type.starts_with("image/") {
continue;
}
if let Some(data) = inline_data
.get("data")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
{
results.push(data.to_string());
}
}
}
for nested_value in object.values() {
collect_match3d_inline_image_data(nested_value, results);
}
}
_ => {}
}
}
fn find_first_match3d_string_by_key(payload: &Value, target_key: &str) -> Option<String> {
let mut results = Vec::new();
collect_match3d_strings_by_key(payload, target_key, &mut results);
results.into_iter().next()
}
fn collect_match3d_strings_by_key(payload: &Value, target_key: &str, results: &mut Vec<String>) {
match payload {
Value::Array(entries) => {
for entry in entries {
collect_match3d_strings_by_key(entry, target_key, results);
}
}
Value::Object(object) => {
for (key, nested_value) in object {
if key == target_key {
match nested_value {
Value::String(text) => {
let text = text.trim();
if !text.is_empty() {
results.push(text.to_string());
}
}
Value::Array(entries) => {
for entry in entries {
if let Some(text) = entry
.as_str()
.map(str::trim)
.filter(|value| !value.is_empty())
{
results.push(text.to_string());
}
}
}
_ => {}
}
}
collect_match3d_strings_by_key(nested_value, target_key, results);
}
}
_ => {}
}
}
fn map_match3d_vector_engine_gemini_image_request_error(message: String) -> AppError {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine-gemini",
"message": message,
}))
}
fn map_match3d_vector_engine_gemini_image_upstream_error(
upstream_status: reqwest::StatusCode,
raw_text: &str,
fallback_message: &str,
) -> AppError {
let message = parse_match3d_api_error_message(raw_text, fallback_message);
let raw_excerpt = trim_match3d_upstream_excerpt(raw_text, 800);
tracing::warn!(
provider = "vector-engine-gemini",
upstream_status = upstream_status.as_u16(),
message = %message,
raw_excerpt = %raw_excerpt,
"抓大鹅 VectorEngine Gemini 图片生成上游请求失败"
);
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine-gemini",
"upstreamStatus": upstream_status.as_u16(),
"message": message,
"rawExcerpt": raw_excerpt,
}))
}
fn parse_match3d_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::<Value>(trimmed) {
for key in ["message", "code"] {
if let Some(value) = find_first_match3d_string_by_key(&payload, key) {
return if key == "message" {
value
} else {
format!("{fallback_message}{value}")
};
}
}
}
trimmed.to_string()
}
fn trim_match3d_upstream_excerpt(raw_text: &str, max_chars: usize) -> String {
raw_text.chars().take(max_chars).collect()
}
fn normalize_match3d_downloaded_image_mime_type(content_type: &str) -> String {
let mime_type = content_type
.split(';')
.next()
.map(str::trim)
.unwrap_or("image/png");
match mime_type {
"image/png" | "image/webp" | "image/jpeg" | "image/jpg" | "image/gif" => {
mime_type.to_string()
}
_ => "image/png".to_string(),
}
}
pub(super) fn match3d_mime_to_extension(mime_type: &str) -> &str {
match mime_type {
"image/png" => "png",
"image/webp" => "webp",
"image/gif" => "gif",
"image/jpeg" | "image/jpg" => "jpg",
_ => "png",
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,306 @@
use super::*;
use crate::mapper::{
custom_world::{format_ai_result_reference_kind, format_ai_task_kind},
inventory::map_ai_result_reference_kind,
npc::map_ai_task_kind,
};
impl From<DomainAiTaskCreateInput> for AiTaskCreateInput {
fn from(input: DomainAiTaskCreateInput) -> Self {
Self {
task_id: input.task_id,
task_kind: map_ai_task_kind(input.task_kind),
owner_user_id: input.owner_user_id,
request_label: input.request_label,
source_module: input.source_module,
source_entity_id: input.source_entity_id,
request_payload_json: input.request_payload_json,
stages: input.stages.into_iter().map(Into::into).collect(),
created_at_micros: input.created_at_micros,
}
}
}
impl From<DomainAiTaskStartInput> for AiTaskStartInput {
fn from(input: DomainAiTaskStartInput) -> Self {
Self {
task_id: input.task_id,
started_at_micros: input.started_at_micros,
}
}
}
impl From<DomainAiTaskStageStartInput> for AiTaskStageStartInput {
fn from(input: DomainAiTaskStageStartInput) -> Self {
Self {
task_id: input.task_id,
stage_kind: map_ai_task_stage_kind(input.stage_kind),
started_at_micros: input.started_at_micros,
}
}
}
impl From<DomainAiTextChunkAppendInput> for AiTextChunkAppendInput {
fn from(input: DomainAiTextChunkAppendInput) -> Self {
Self {
task_id: input.task_id,
stage_kind: map_ai_task_stage_kind(input.stage_kind),
sequence: input.sequence,
delta_text: input.delta_text,
created_at_micros: input.created_at_micros,
}
}
}
impl From<DomainAiStageCompletionInput> for AiStageCompletionInput {
fn from(input: DomainAiStageCompletionInput) -> Self {
Self {
task_id: input.task_id,
stage_kind: map_ai_task_stage_kind(input.stage_kind),
text_output: input.text_output,
structured_payload_json: input.structured_payload_json,
warning_messages: input.warning_messages,
completed_at_micros: input.completed_at_micros,
}
}
}
impl From<DomainAiResultReferenceInput> for AiResultReferenceInput {
fn from(input: DomainAiResultReferenceInput) -> Self {
Self {
task_id: input.task_id,
reference_kind: map_ai_result_reference_kind(input.reference_kind),
reference_id: input.reference_id,
label: input.label,
created_at_micros: input.created_at_micros,
}
}
}
impl From<DomainAiTaskFinishInput> for AiTaskFinishInput {
fn from(input: DomainAiTaskFinishInput) -> Self {
Self {
task_id: input.task_id,
completed_at_micros: input.completed_at_micros,
}
}
}
impl From<DomainAiTaskFailureInput> for AiTaskFailureInput {
fn from(input: DomainAiTaskFailureInput) -> Self {
Self {
task_id: input.task_id,
failure_message: input.failure_message,
completed_at_micros: input.completed_at_micros,
}
}
}
impl From<DomainAiTaskCancelInput> for AiTaskCancelInput {
fn from(input: DomainAiTaskCancelInput) -> Self {
Self {
task_id: input.task_id,
completed_at_micros: input.completed_at_micros,
}
}
}
impl From<DomainAiTaskStageBlueprint> for AiTaskStageBlueprint {
fn from(blueprint: DomainAiTaskStageBlueprint) -> Self {
Self {
stage_kind: map_ai_task_stage_kind(blueprint.stage_kind),
label: blueprint.label,
detail: blueprint.detail,
order: blueprint.order,
}
}
}
pub(crate) fn map_ai_task_procedure_result(
result: AiTaskProcedureResult,
) -> Result<AiTaskMutationRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let task = result
.task
.ok_or_else(|| SpacetimeClientError::missing_snapshot("ai_task 快照"))?;
Ok(AiTaskMutationRecord {
task: map_ai_task_snapshot(task),
text_chunk: result.text_chunk.map(map_ai_text_chunk_snapshot),
})
}
pub(crate) fn map_ai_task_snapshot(snapshot: AiTaskSnapshot) -> AiTaskRecord {
AiTaskRecord {
task_id: snapshot.task_id,
task_kind: format_ai_task_kind(snapshot.task_kind).to_string(),
owner_user_id: snapshot.owner_user_id,
request_label: snapshot.request_label,
source_module: snapshot.source_module,
source_entity_id: snapshot.source_entity_id,
request_payload_json: snapshot.request_payload_json,
status: format_ai_task_status(snapshot.status).to_string(),
failure_message: snapshot.failure_message,
stages: snapshot
.stages
.into_iter()
.map(map_ai_task_stage_snapshot)
.collect(),
result_references: snapshot
.result_references
.into_iter()
.map(map_ai_result_reference_snapshot)
.collect(),
latest_text_output: snapshot.latest_text_output,
latest_structured_payload_json: snapshot.latest_structured_payload_json,
version: snapshot.version,
created_at: format_timestamp_micros(snapshot.created_at_micros),
started_at: snapshot.started_at_micros.map(format_timestamp_micros),
completed_at: snapshot.completed_at_micros.map(format_timestamp_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
pub(crate) fn map_ai_task_stage_snapshot(snapshot: AiTaskStageSnapshot) -> AiTaskStageRecord {
AiTaskStageRecord {
stage_kind: format_ai_task_stage_kind(snapshot.stage_kind).to_string(),
label: snapshot.label,
detail: snapshot.detail,
order: snapshot.order,
status: format_ai_task_stage_status(snapshot.status).to_string(),
text_output: snapshot.text_output,
structured_payload_json: snapshot.structured_payload_json,
warning_messages: snapshot.warning_messages,
started_at: snapshot.started_at_micros.map(format_timestamp_micros),
completed_at: snapshot.completed_at_micros.map(format_timestamp_micros),
}
}
pub(crate) fn map_ai_text_chunk_snapshot(snapshot: AiTextChunkSnapshot) -> AiTextChunkRecord {
AiTextChunkRecord {
chunk_id: snapshot.chunk_id,
task_id: snapshot.task_id,
stage_kind: format_ai_task_stage_kind(snapshot.stage_kind).to_string(),
sequence: snapshot.sequence,
delta_text: snapshot.delta_text,
created_at: format_timestamp_micros(snapshot.created_at_micros),
}
}
pub(crate) fn map_ai_result_reference_snapshot(
snapshot: AiResultReferenceSnapshot,
) -> AiResultReferenceRecord {
AiResultReferenceRecord {
result_ref_id: snapshot.result_ref_id,
task_id: snapshot.task_id,
reference_kind: format_ai_result_reference_kind(snapshot.reference_kind).to_string(),
reference_id: snapshot.reference_id,
label: snapshot.label,
created_at: format_timestamp_micros(snapshot.created_at_micros),
}
}
pub(crate) fn map_ai_task_stage_kind(value: DomainAiTaskStageKind) -> AiTaskStageKind {
match value {
DomainAiTaskStageKind::PreparePrompt => AiTaskStageKind::PreparePrompt,
DomainAiTaskStageKind::RequestModel => AiTaskStageKind::RequestModel,
DomainAiTaskStageKind::RepairResponse => AiTaskStageKind::RepairResponse,
DomainAiTaskStageKind::NormalizeResult => AiTaskStageKind::NormalizeResult,
DomainAiTaskStageKind::PersistResult => AiTaskStageKind::PersistResult,
}
}
pub(crate) fn format_ai_task_status(value: AiTaskStatus) -> &'static str {
match value {
AiTaskStatus::Pending => "pending",
AiTaskStatus::Running => "running",
AiTaskStatus::Completed => "completed",
AiTaskStatus::Failed => "failed",
AiTaskStatus::Cancelled => "cancelled",
}
}
pub(crate) fn format_ai_task_stage_kind(value: AiTaskStageKind) -> &'static str {
match value {
AiTaskStageKind::PreparePrompt => "prepare_prompt",
AiTaskStageKind::RequestModel => "request_model",
AiTaskStageKind::RepairResponse => "repair_response",
AiTaskStageKind::NormalizeResult => "normalize_result",
AiTaskStageKind::PersistResult => "persist_result",
}
}
pub(crate) fn format_ai_task_stage_status(value: AiTaskStageStatus) -> &'static str {
match value {
AiTaskStageStatus::Pending => "pending",
AiTaskStageStatus::Running => "running",
AiTaskStageStatus::Completed => "completed",
AiTaskStageStatus::Skipped => "skipped",
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AiTaskStageRecord {
pub stage_kind: String,
pub label: String,
pub detail: String,
pub order: u32,
pub status: String,
pub text_output: Option<String>,
pub structured_payload_json: Option<String>,
pub warning_messages: Vec<String>,
pub started_at: Option<String>,
pub completed_at: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AiResultReferenceRecord {
pub result_ref_id: String,
pub task_id: String,
pub reference_kind: String,
pub reference_id: String,
pub label: Option<String>,
pub created_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AiTextChunkRecord {
pub chunk_id: String,
pub task_id: String,
pub stage_kind: String,
pub sequence: u32,
pub delta_text: String,
pub created_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AiTaskRecord {
pub task_id: String,
pub task_kind: String,
pub owner_user_id: String,
pub request_label: String,
pub source_module: String,
pub source_entity_id: Option<String>,
pub request_payload_json: Option<String>,
pub status: String,
pub failure_message: Option<String>,
pub stages: Vec<AiTaskStageRecord>,
pub result_references: Vec<AiResultReferenceRecord>,
pub latest_text_output: Option<String>,
pub latest_structured_payload_json: Option<String>,
pub version: u32,
pub created_at: String,
pub started_at: Option<String>,
pub completed_at: Option<String>,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AiTaskMutationRecord {
pub task: AiTaskRecord,
pub text_chunk: Option<AiTextChunkRecord>,
}
@@ -0,0 +1,382 @@
use super::*;
impl From<module_assets::AssetEntityBindingInput> for AssetEntityBindingInput {
fn from(input: module_assets::AssetEntityBindingInput) -> Self {
Self {
binding_id: input.binding_id,
asset_object_id: input.asset_object_id,
entity_kind: input.entity_kind,
entity_id: input.entity_id,
slot: input.slot,
asset_kind: input.asset_kind,
owner_user_id: input.owner_user_id,
profile_id: input.profile_id,
updated_at_micros: input.updated_at_micros,
}
}
}
impl From<module_assets::AssetObjectUpsertInput> for AssetObjectUpsertInput {
fn from(input: module_assets::AssetObjectUpsertInput) -> Self {
Self {
asset_object_id: input.asset_object_id,
bucket: input.bucket,
object_key: input.object_key,
access_policy: map_access_policy(input.access_policy),
content_type: input.content_type,
content_length: input.content_length,
content_hash: input.content_hash,
version: input.version,
source_job_id: input.source_job_id,
owner_user_id: input.owner_user_id,
profile_id: input.profile_id,
entity_id: input.entity_id,
asset_kind: input.asset_kind,
updated_at_micros: input.updated_at_micros,
}
}
}
impl From<module_assets::AssetHistoryListInput> for AssetHistoryListInput {
fn from(input: module_assets::AssetHistoryListInput) -> Self {
Self {
asset_kind: input.asset_kind,
limit: input.limit,
}
}
}
pub(crate) fn map_procedure_result(
result: AssetObjectProcedureResult,
) -> Result<AssetObjectRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let snapshot = result
.record
.ok_or_else(|| SpacetimeClientError::missing_snapshot("对象快照"))?;
Ok(build_asset_object_record(map_snapshot(snapshot)))
}
pub(crate) fn map_entity_binding_procedure_result(
result: AssetEntityBindingProcedureResult,
) -> Result<AssetEntityBindingRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let snapshot = result
.record
.ok_or_else(|| SpacetimeClientError::missing_snapshot("绑定快照"))?;
Ok(build_asset_entity_binding_record(
map_entity_binding_snapshot(snapshot),
))
}
pub(crate) fn map_entity_binding_snapshot(
snapshot: AssetEntityBindingSnapshot,
) -> module_assets::AssetEntityBindingSnapshot {
module_assets::AssetEntityBindingSnapshot {
binding_id: snapshot.binding_id,
asset_object_id: snapshot.asset_object_id,
entity_kind: snapshot.entity_kind,
entity_id: snapshot.entity_id,
slot: snapshot.slot,
asset_kind: snapshot.asset_kind,
owner_user_id: snapshot.owner_user_id,
profile_id: snapshot.profile_id,
created_at_micros: snapshot.created_at_micros,
updated_at_micros: snapshot.updated_at_micros,
}
}
pub(crate) fn map_snapshot(
snapshot: AssetObjectUpsertSnapshot,
) -> module_assets::AssetObjectUpsertSnapshot {
module_assets::AssetObjectUpsertSnapshot {
asset_object_id: snapshot.asset_object_id,
bucket: snapshot.bucket,
object_key: snapshot.object_key,
access_policy: map_access_policy_back(snapshot.access_policy),
content_type: snapshot.content_type,
content_length: snapshot.content_length,
content_hash: snapshot.content_hash,
version: snapshot.version,
source_job_id: snapshot.source_job_id,
owner_user_id: snapshot.owner_user_id,
profile_id: snapshot.profile_id,
entity_id: snapshot.entity_id,
asset_kind: snapshot.asset_kind,
created_at_micros: snapshot.created_at_micros,
updated_at_micros: snapshot.updated_at_micros,
}
}
pub(crate) fn map_access_policy(
value: AssetObjectAccessPolicy,
) -> crate::module_bindings::AssetObjectAccessPolicy {
match value {
AssetObjectAccessPolicy::Private => {
crate::module_bindings::AssetObjectAccessPolicy::Private
}
AssetObjectAccessPolicy::PublicRead => {
crate::module_bindings::AssetObjectAccessPolicy::PublicRead
}
}
}
pub(crate) fn map_access_policy_back(
value: crate::module_bindings::AssetObjectAccessPolicy,
) -> AssetObjectAccessPolicy {
match value {
crate::module_bindings::AssetObjectAccessPolicy::Private => {
AssetObjectAccessPolicy::Private
}
crate::module_bindings::AssetObjectAccessPolicy::PublicRead => {
AssetObjectAccessPolicy::PublicRead
}
}
}
impl TryFrom<&str> for BigFishAssetKind {
type Error = SpacetimeClientError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value.trim() {
"level_main_image" => Ok(Self::LevelMainImage),
"level_motion" => Ok(Self::LevelMotion),
"stage_background" => Ok(Self::StageBackground),
other => Err(SpacetimeClientError::Runtime(format!(
"big fish asset kind `{other}` 当前尚未支持"
))),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CustomWorldDraftCardRecord {
pub card_id: String,
pub kind: String,
pub title: String,
pub subtitle: String,
pub summary: String,
pub status: String,
pub linked_ids: Vec<String>,
pub warning_count: u32,
pub asset_status: Option<String>,
pub asset_status_label: Option<String>,
pub detail_payload: Option<serde_json::Value>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldDraftCardDetailRecord {
pub card_id: String,
pub kind: String,
pub title: String,
pub sections: Vec<CustomWorldDraftCardDetailSectionRecord>,
pub linked_ids: Vec<String>,
pub locked: bool,
pub editable: bool,
pub editable_section_ids: Vec<String>,
pub warning_messages: Vec<String>,
pub asset_status: Option<String>,
pub asset_status_label: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CustomWorldAgentSessionRecord {
pub session_id: String,
pub seed_text: String,
pub current_turn: u32,
pub anchor_content: serde_json::Value,
pub progress_percent: u32,
pub last_assistant_reply: Option<String>,
pub stage: String,
pub focus_card_id: Option<String>,
pub creator_intent: serde_json::Value,
pub creator_intent_readiness: serde_json::Value,
pub anchor_pack: serde_json::Value,
pub lock_state: serde_json::Value,
pub draft_profile: serde_json::Value,
pub messages: Vec<CustomWorldAgentMessageRecord>,
pub draft_cards: Vec<CustomWorldDraftCardRecord>,
pub pending_clarifications: Vec<serde_json::Value>,
pub suggested_actions: Vec<serde_json::Value>,
pub recommended_replies: Vec<String>,
pub quality_findings: Vec<serde_json::Value>,
pub asset_coverage: serde_json::Value,
pub checkpoints: Vec<CustomWorldCheckpointRecord>,
pub supported_actions: Vec<CustomWorldSupportedActionRecord>,
pub publish_gate: Option<CustomWorldPublishGateRecord>,
pub result_preview: Option<serde_json::Value>,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldAgentSessionCreateRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub seed_text: String,
pub welcome_message_id: String,
pub welcome_message_text: String,
pub anchor_content_json: String,
pub creator_intent_json: Option<String>,
pub creator_intent_readiness_json: String,
pub anchor_pack_json: Option<String>,
pub lock_state_json: Option<String>,
pub draft_profile_json: Option<String>,
pub pending_clarifications_json: String,
pub suggested_actions_json: String,
pub recommended_replies_json: String,
pub quality_findings_json: String,
pub asset_coverage_json: String,
pub checkpoints_json: String,
pub created_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldAgentMessageFinalizeRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub operation_id: String,
pub assistant_message_id: Option<String>,
pub assistant_reply_text: Option<String>,
pub phase_label: String,
pub phase_detail: String,
pub operation_status: String,
pub operation_progress: u32,
pub stage: String,
pub progress_percent: u32,
pub focus_card_id: Option<String>,
pub anchor_content_json: String,
pub creator_intent_json: Option<String>,
pub creator_intent_readiness_json: String,
pub anchor_pack_json: Option<String>,
pub draft_profile_json: Option<String>,
pub pending_clarifications_json: String,
pub suggested_actions_json: String,
pub recommended_replies_json: String,
pub quality_findings_json: String,
pub asset_coverage_json: String,
pub error_message: Option<String>,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VisualNovelAgentSessionCreateRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub source_mode: String,
pub seed_text: String,
pub source_asset_ids_json: String,
pub welcome_message_id: String,
pub welcome_message_text: String,
pub draft_json: Option<String>,
pub created_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VisualNovelWorkUpdateRecordInput {
pub profile_id: String,
pub owner_user_id: String,
pub work_title: String,
pub work_description: String,
pub tags_json: String,
pub cover_image_src: Option<String>,
pub source_asset_ids_json: String,
pub draft_json: String,
pub publish_ready: bool,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct VisualNovelAgentSessionRecord {
pub session_id: String,
pub owner_user_id: String,
pub source_mode: String,
pub status: String,
pub seed_text: String,
pub source_asset_ids: Vec<String>,
pub current_turn: u32,
pub progress_percent: u32,
pub messages: Vec<VisualNovelAgentMessageRecord>,
pub draft: Option<serde_json::Value>,
pub pending_action: Option<serde_json::Value>,
pub last_assistant_reply: Option<String>,
pub published_profile_id: Option<String>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct VisualNovelWorkProfileRecord {
pub work_id: String,
pub profile_id: String,
pub owner_user_id: String,
pub source_session_id: Option<String>,
pub author_display_name: String,
pub work_title: String,
pub work_description: String,
pub tags: Vec<String>,
pub cover_image_src: Option<String>,
pub source_asset_ids: Vec<String>,
pub draft: serde_json::Value,
pub publication_status: String,
pub publish_ready: bool,
pub play_count: u32,
pub created_at: String,
pub updated_at: String,
pub published_at: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishAssetGenerateRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub asset_kind: String,
pub level: Option<u32>,
pub motion_key: Option<String>,
pub asset_url: Option<String>,
pub generated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishAssetSlotRecord {
pub slot_id: String,
pub asset_kind: String,
pub level: Option<u32>,
pub motion_key: Option<String>,
pub status: String,
pub asset_url: Option<String>,
pub prompt_snapshot: String,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishAssetCoverageRecord {
pub level_main_image_ready_count: u32,
pub level_motion_ready_count: u32,
pub background_ready: bool,
pub required_level_count: u32,
pub publish_ready: bool,
pub blockers: Vec<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct BigFishSessionRecord {
pub session_id: String,
pub current_turn: u32,
pub progress_percent: u32,
pub stage: String,
pub anchor_pack: BigFishAnchorPackRecord,
pub draft: Option<BigFishGameDraftRecord>,
pub asset_slots: Vec<BigFishAssetSlotRecord>,
pub asset_coverage: BigFishAssetCoverageRecord,
pub messages: Vec<BigFishAgentMessageRecord>,
pub last_assistant_reply: Option<String>,
pub publish_ready: bool,
pub updated_at: String,
}
@@ -0,0 +1,42 @@
use super::*;
pub(crate) fn map_auth_store_snapshot_procedure_result(
result: AuthStoreSnapshotProcedureResult,
) -> Result<AuthStoreSnapshotRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let record = result
.record
.ok_or_else(|| SpacetimeClientError::missing_snapshot("认证快照"))?;
Ok(map_auth_store_snapshot_record(record))
}
pub(crate) fn map_auth_store_snapshot_record(
record: crate::module_bindings::AuthStoreSnapshotRecord,
) -> crate::AuthStoreSnapshotRecord {
crate::AuthStoreSnapshotRecord {
snapshot_json: record.snapshot_json,
updated_at_micros: record.updated_at_micros,
}
}
pub(crate) fn map_auth_store_snapshot_import_procedure_result(
result: AuthStoreSnapshotImportProcedureResult,
) -> Result<AuthStoreSnapshotImportRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let record = result
.record
.ok_or_else(|| SpacetimeClientError::missing_snapshot("认证快照导入结果"))?;
Ok(AuthStoreSnapshotImportRecord {
imported_user_count: record.imported_user_count,
imported_identity_count: record.imported_identity_count,
imported_refresh_session_count: record.imported_refresh_session_count,
})
}
@@ -0,0 +1,94 @@
use super::*;
pub(crate) fn map_bark_battle_draft_config_procedure_result(
result: BarkBattleProcedureResult,
) -> Result<BarkBattleDraftConfigRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.draft_config
.ok_or_else(|| SpacetimeClientError::missing_snapshot("Bark Battle draft config"))
.map(bark_battle_draft_config_to_value)
}
pub(crate) fn map_bark_battle_runtime_config_procedure_result(
result: BarkBattleProcedureResult,
) -> Result<BarkBattleRuntimeConfigRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.runtime_config
.ok_or_else(|| SpacetimeClientError::missing_snapshot("Bark Battle runtime config"))
.map(bark_battle_runtime_config_to_value)
}
pub(crate) fn map_bark_battle_run_procedure_result(
result: BarkBattleProcedureResult,
) -> Result<BarkBattleRunRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.run
.ok_or_else(|| SpacetimeClientError::missing_snapshot("Bark Battle run"))
.map(bark_battle_run_to_value)
}
fn bark_battle_draft_config_to_value(snapshot: BarkBattleDraftConfigSnapshot) -> serde_json::Value {
serde_json::json!({
"draftId": snapshot.draft_id,
"ownerUserId": snapshot.owner_user_id,
"workId": snapshot.work_id,
"configVersion": snapshot.config_version,
"rulesetVersion": snapshot.ruleset_version,
"difficultyPreset": snapshot.difficulty_preset,
"leaderboardEnabled": snapshot.leaderboard_enabled,
"configJson": snapshot.config_json,
"editorStateJson": snapshot.editor_state_json,
"createdAtMicros": snapshot.created_at_micros,
"updatedAtMicros": snapshot.updated_at_micros,
})
}
fn bark_battle_runtime_config_to_value(
snapshot: BarkBattleRuntimeConfigSnapshot,
) -> serde_json::Value {
serde_json::json!({
"workId": snapshot.work_id,
"ownerUserId": snapshot.owner_user_id,
"sourceDraftId": snapshot.source_draft_id,
"configVersion": snapshot.config_version,
"rulesetVersion": snapshot.ruleset_version,
"difficultyPreset": snapshot.difficulty_preset,
"leaderboardEnabled": snapshot.leaderboard_enabled,
"configJson": snapshot.config_json,
"publishedSnapshotJson": snapshot.published_snapshot_json,
"publishedAtMicros": snapshot.published_at_micros,
"updatedAtMicros": snapshot.updated_at_micros,
})
}
fn bark_battle_run_to_value(snapshot: BarkBattleRunSnapshot) -> serde_json::Value {
serde_json::json!({
"runId": snapshot.run_id,
"ownerUserId": snapshot.owner_user_id,
"workId": snapshot.work_id,
"configVersion": snapshot.config_version,
"rulesetVersion": snapshot.ruleset_version,
"difficultyPreset": snapshot.difficulty_preset,
"leaderboardEnabled": snapshot.leaderboard_enabled,
"status": snapshot.status,
"clientStartedAtMicros": snapshot.client_started_at_micros,
"serverStartedAtMicros": snapshot.server_started_at_micros,
"clientFinishedAtMicros": snapshot.client_finished_at_micros,
"serverFinishedAtMicros": snapshot.server_finished_at_micros,
"metricsJson": snapshot.metrics_json,
"serverResult": snapshot.server_result,
"validationStatus": snapshot.validation_status,
"antiCheatFlagsJson": snapshot.anti_cheat_flags_json,
"leaderboardScore": snapshot.leaderboard_score,
"scoreId": snapshot.score_id,
})
}

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