refactor: split large modules and normalize rust layout
This commit is contained in:
@@ -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。
|
||||
|
||||
@@ -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` 内部生成资产 Adapter:provider 生成、下载/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. 草稿页作品卡对齐发现页列表卡风格:左侧信息,右侧封面图,移动端单列,桌面两到三列。
|
||||
|
||||
@@ -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 索引',
|
||||
},
|
||||
|
||||
@@ -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
881
server-rs/crates/api-server/src/match3d/draft.rs
Normal file
881
server-rs/crates/api-server/src/match3d/draft.rs
Normal file
@@ -0,0 +1,881 @@
|
||||
use super::*;
|
||||
|
||||
pub(super) async fn submit_and_finalize_match3d_message(
|
||||
state: &AppState,
|
||||
request_context: &RequestContext,
|
||||
owner_user_id: &str,
|
||||
session_id: String,
|
||||
payload: SendMatch3DAgentMessageRequest,
|
||||
) -> Result<Match3DAgentSessionRecord, Response> {
|
||||
ensure_non_empty(
|
||||
request_context,
|
||||
MATCH3D_AGENT_PROVIDER,
|
||||
&session_id,
|
||||
"sessionId",
|
||||
)?;
|
||||
ensure_non_empty(
|
||||
request_context,
|
||||
MATCH3D_AGENT_PROVIDER,
|
||||
&payload.client_message_id,
|
||||
"clientMessageId",
|
||||
)?;
|
||||
ensure_non_empty(
|
||||
request_context,
|
||||
MATCH3D_AGENT_PROVIDER,
|
||||
&payload.text,
|
||||
"text",
|
||||
)?;
|
||||
|
||||
let submitted = state
|
||||
.spacetime_client()
|
||||
.submit_match3d_agent_message(Match3DAgentMessageSubmitRecordInput {
|
||||
session_id: session_id.clone(),
|
||||
owner_user_id: owner_user_id.to_string(),
|
||||
user_message_id: payload.client_message_id.clone(),
|
||||
user_message_text: payload.text.clone(),
|
||||
submitted_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(|error| {
|
||||
match3d_error_response(
|
||||
request_context,
|
||||
MATCH3D_AGENT_PROVIDER,
|
||||
map_match3d_client_error(error),
|
||||
)
|
||||
})?;
|
||||
let next_turn = submitted.current_turn.saturating_add(1);
|
||||
let next_config = build_config_from_message(&submitted, &payload);
|
||||
let assistant_reply = build_match3d_assistant_reply_for_turn(&next_config, next_turn);
|
||||
let progress_percent = resolve_progress_percent_for_turn(next_turn);
|
||||
let stage = if progress_percent >= 100 {
|
||||
"ReadyToCompile"
|
||||
} else {
|
||||
"Collecting"
|
||||
}
|
||||
.to_string();
|
||||
|
||||
state
|
||||
.spacetime_client()
|
||||
.finalize_match3d_agent_message(Match3DAgentMessageFinalizeRecordInput {
|
||||
session_id,
|
||||
owner_user_id: owner_user_id.to_string(),
|
||||
assistant_message_id: Some(build_prefixed_uuid_id(MATCH3D_MESSAGE_ID_PREFIX)),
|
||||
assistant_reply_text: Some(assistant_reply),
|
||||
config_json: serialize_match3d_config(&next_config),
|
||||
progress_percent,
|
||||
stage,
|
||||
updated_at_micros: current_utc_micros(),
|
||||
error_message: None,
|
||||
})
|
||||
.await
|
||||
.map_err(|error| {
|
||||
match3d_error_response(
|
||||
request_context,
|
||||
MATCH3D_AGENT_PROVIDER,
|
||||
map_match3d_client_error(error),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn load_match3d_agent_session_response_with_persisted_assets(
|
||||
state: &AppState,
|
||||
owner_user_id: &str,
|
||||
session: Match3DAgentSessionRecord,
|
||||
) -> Match3DAgentSessionSnapshotResponse {
|
||||
let Some(profile_id) = resolve_match3d_session_existing_profile_id(&session) else {
|
||||
return map_match3d_agent_session_response(session);
|
||||
};
|
||||
let assets =
|
||||
get_match3d_existing_generated_item_assets(state, owner_user_id, profile_id.as_str()).await;
|
||||
map_match3d_agent_session_response_with_assets(session, &assets)
|
||||
}
|
||||
|
||||
fn resolve_match3d_session_existing_profile_id(
|
||||
session: &Match3DAgentSessionRecord,
|
||||
) -> Option<String> {
|
||||
session
|
||||
.draft
|
||||
.as_ref()
|
||||
.map(|draft| draft.profile_id.trim())
|
||||
.filter(|profile_id| !profile_id.is_empty())
|
||||
.or_else(|| {
|
||||
session
|
||||
.published_profile_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|profile_id| !profile_id.is_empty())
|
||||
})
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
pub(super) async fn compile_match3d_draft_for_session(
|
||||
state: &AppState,
|
||||
request_context: &RequestContext,
|
||||
authenticated: &AuthenticatedAccessToken,
|
||||
session_id: String,
|
||||
game_name: Option<String>,
|
||||
summary: Option<String>,
|
||||
tags: Option<Vec<String>>,
|
||||
cover_image_src: Option<String>,
|
||||
generate_click_sound: Option<bool>,
|
||||
) -> Result<(Match3DAgentSessionRecord, Vec<Match3DGeneratedItemAsset>), Response> {
|
||||
let owner_user_id = authenticated.claims().user_id().to_string();
|
||||
let initial_session = state
|
||||
.spacetime_client()
|
||||
.get_match3d_agent_session(session_id.clone(), owner_user_id.clone())
|
||||
.await
|
||||
.map_err(|error| {
|
||||
match3d_error_response(
|
||||
request_context,
|
||||
MATCH3D_AGENT_PROVIDER,
|
||||
map_match3d_client_error(error),
|
||||
)
|
||||
})?;
|
||||
let mut config = resolve_config_or_default(initial_session.config.as_ref());
|
||||
if let Some(generate_click_sound) = generate_click_sound {
|
||||
config.generate_click_sound = generate_click_sound;
|
||||
}
|
||||
// 中文注释:抓大鹅入口已支持表单直创;完整表单创建的 session
|
||||
// 不需要再伪造三轮聊天,只要配置字段完整即可进入草稿编译。
|
||||
let has_complete_form_config = !config.theme_text.trim().is_empty()
|
||||
&& config.clear_count > 0
|
||||
&& (1..=10).contains(&config.difficulty);
|
||||
if !has_complete_form_config
|
||||
&& (initial_session.current_turn < 3 || initial_session.progress_percent < 100)
|
||||
{
|
||||
return Err(match3d_bad_request(
|
||||
request_context,
|
||||
MATCH3D_AGENT_PROVIDER,
|
||||
"match3d 创作配置尚未确认完成",
|
||||
));
|
||||
}
|
||||
|
||||
let requested_game_name = normalize_optional_match3d_text(game_name);
|
||||
let requested_summary = normalize_optional_match3d_text(summary);
|
||||
let requested_tags = tags.map(normalize_tags).filter(|items| !items.is_empty());
|
||||
let requested_cover_image_src = normalize_optional_match3d_text(cover_image_src);
|
||||
let fallback_work_metadata = fallback_match3d_work_metadata(config.theme_text.as_str());
|
||||
let profile_id = resolve_match3d_draft_profile_id(&initial_session);
|
||||
let initial_game_name = requested_game_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| fallback_work_metadata.game_name.clone());
|
||||
let initial_tags = requested_tags
|
||||
.clone()
|
||||
.unwrap_or_else(|| fallback_work_metadata.tags.clone());
|
||||
let billing_asset_id = format!("{}:{}:{}", session_id, profile_id, current_utc_micros());
|
||||
execute_billable_match3d_draft_generation(
|
||||
state,
|
||||
request_context,
|
||||
owner_user_id.as_str(),
|
||||
billing_asset_id.as_str(),
|
||||
async {
|
||||
let mut session = upsert_match3d_draft_snapshot(
|
||||
state,
|
||||
request_context,
|
||||
authenticated,
|
||||
session_id.clone(),
|
||||
owner_user_id.clone(),
|
||||
profile_id.clone(),
|
||||
Some(initial_game_name),
|
||||
requested_summary.clone().or_else(|| Some(String::new())),
|
||||
Some(serde_json::to_string(&initial_tags).unwrap_or_default()),
|
||||
requested_cover_image_src.clone(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if session.draft.is_none() {
|
||||
return Err(match3d_error_response(
|
||||
request_context,
|
||||
MATCH3D_AGENT_PROVIDER,
|
||||
match3d_bad_gateway("抓大鹅草稿创建失败,请稍后重试"),
|
||||
));
|
||||
}
|
||||
|
||||
let mut generated_work_metadata = generate_match3d_draft_plan(state, &config).await;
|
||||
let resolved_game_name = requested_game_name
|
||||
.unwrap_or_else(|| generated_work_metadata.metadata.game_name.clone());
|
||||
let resolved_summary = requested_summary
|
||||
.clone()
|
||||
.unwrap_or_else(|| generated_work_metadata.metadata.summary.clone());
|
||||
let resolved_tags = match requested_tags {
|
||||
Some(tags) => tags,
|
||||
None => {
|
||||
generate_match3d_work_tags_for_plan(
|
||||
state,
|
||||
resolved_game_name.as_str(),
|
||||
config.theme_text.as_str(),
|
||||
resolved_summary.as_str(),
|
||||
&generated_work_metadata.metadata.tags,
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
generated_work_metadata.metadata.tags = resolved_tags.clone();
|
||||
session = upsert_match3d_draft_snapshot(
|
||||
state,
|
||||
request_context,
|
||||
authenticated,
|
||||
session_id,
|
||||
owner_user_id.clone(),
|
||||
profile_id.clone(),
|
||||
Some(resolved_game_name),
|
||||
Some(resolved_summary),
|
||||
Some(serde_json::to_string(&resolved_tags).unwrap_or_default()),
|
||||
requested_cover_image_src.clone(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let existing_assets = get_match3d_existing_generated_item_assets(
|
||||
state,
|
||||
owner_user_id.as_str(),
|
||||
profile_id.as_str(),
|
||||
)
|
||||
.await;
|
||||
let generated_item_assets = generate_match3d_item_assets(
|
||||
state,
|
||||
request_context,
|
||||
authenticated,
|
||||
owner_user_id.as_str(),
|
||||
session.session_id.as_str(),
|
||||
profile_id.as_str(),
|
||||
&config,
|
||||
generated_work_metadata.items,
|
||||
existing_assets,
|
||||
)
|
||||
.await?;
|
||||
let generated_item_assets = ensure_match3d_background_asset(
|
||||
state,
|
||||
request_context,
|
||||
authenticated,
|
||||
owner_user_id.as_str(),
|
||||
session.session_id.as_str(),
|
||||
profile_id.as_str(),
|
||||
&config,
|
||||
generated_work_metadata.background_prompt.as_str(),
|
||||
generated_item_assets,
|
||||
)
|
||||
.await?;
|
||||
let existing_cover_image_src = get_match3d_existing_cover_image_src(
|
||||
state,
|
||||
owner_user_id.as_str(),
|
||||
profile_id.as_str(),
|
||||
)
|
||||
.await;
|
||||
let default_cover_image_src = requested_cover_image_src
|
||||
.clone()
|
||||
.or(existing_cover_image_src)
|
||||
.or_else(|| resolve_match3d_default_cover_image_src(&generated_item_assets));
|
||||
let next_session = upsert_match3d_draft_snapshot(
|
||||
state,
|
||||
request_context,
|
||||
authenticated,
|
||||
session.session_id.clone(),
|
||||
owner_user_id.clone(),
|
||||
profile_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
default_cover_image_src,
|
||||
None,
|
||||
serialize_match3d_generated_item_assets(&generated_item_assets),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((next_session, generated_item_assets))
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 中文注释:抓大鹅草稿生成是一次完整外部生成动作,按 session/profile 幂等预扣 10 泥点。
|
||||
async fn execute_billable_match3d_draft_generation<T, Fut>(
|
||||
state: &AppState,
|
||||
request_context: &RequestContext,
|
||||
owner_user_id: &str,
|
||||
billing_asset_id: &str,
|
||||
operation: Fut,
|
||||
) -> Result<T, Response>
|
||||
where
|
||||
Fut: Future<Output = Result<T, Response>>,
|
||||
{
|
||||
let points_consumed = consume_match3d_draft_generation_points(
|
||||
state,
|
||||
request_context,
|
||||
owner_user_id,
|
||||
billing_asset_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match operation.await {
|
||||
Ok(value) => Ok(value),
|
||||
Err(response) => {
|
||||
if points_consumed {
|
||||
refund_match3d_draft_generation_points(state, owner_user_id, billing_asset_id)
|
||||
.await;
|
||||
}
|
||||
Err(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn consume_match3d_draft_generation_points(
|
||||
state: &AppState,
|
||||
request_context: &RequestContext,
|
||||
owner_user_id: &str,
|
||||
billing_asset_id: &str,
|
||||
) -> Result<bool, Response> {
|
||||
let ledger_id = format!(
|
||||
"asset_operation_consume:{}:match3d_draft_generation:{}",
|
||||
owner_user_id, billing_asset_id
|
||||
);
|
||||
match state
|
||||
.spacetime_client()
|
||||
.consume_profile_wallet_points(
|
||||
owner_user_id.to_string(),
|
||||
MATCH3D_DRAFT_GENERATION_POINTS_COST,
|
||||
ledger_id,
|
||||
current_utc_micros(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(true),
|
||||
Err(error) if should_skip_asset_operation_billing_for_connectivity(&error) => {
|
||||
tracing::warn!(
|
||||
owner_user_id,
|
||||
billing_asset_id,
|
||||
error = %error,
|
||||
"抓大鹅草稿泥点预扣因 SpacetimeDB 连接不可用而降级跳过"
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
Err(error) => Err(match3d_error_response(
|
||||
request_context,
|
||||
MATCH3D_AGENT_PROVIDER,
|
||||
map_asset_operation_wallet_error(error),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn refund_match3d_draft_generation_points(
|
||||
state: &AppState,
|
||||
owner_user_id: &str,
|
||||
billing_asset_id: &str,
|
||||
) {
|
||||
let ledger_id = format!(
|
||||
"asset_operation_refund:{}:match3d_draft_generation:{}",
|
||||
owner_user_id, billing_asset_id
|
||||
);
|
||||
if let Err(error) = state
|
||||
.spacetime_client()
|
||||
.refund_profile_wallet_points(
|
||||
owner_user_id.to_string(),
|
||||
MATCH3D_DRAFT_GENERATION_POINTS_COST,
|
||||
ledger_id,
|
||||
current_utc_micros(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
owner_user_id,
|
||||
billing_asset_id,
|
||||
error = %error,
|
||||
"抓大鹅草稿生成失败后的泥点退款失败"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_match3d_draft_profile_id(session: &Match3DAgentSessionRecord) -> String {
|
||||
session
|
||||
.draft
|
||||
.as_ref()
|
||||
.map(|draft| draft.profile_id.trim())
|
||||
.filter(|profile_id| !profile_id.is_empty())
|
||||
.or_else(|| {
|
||||
session
|
||||
.published_profile_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|profile_id| !profile_id.is_empty())
|
||||
})
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| build_prefixed_uuid_id(MATCH3D_PROFILE_ID_PREFIX))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) async fn upsert_match3d_draft_snapshot(
|
||||
state: &AppState,
|
||||
request_context: &RequestContext,
|
||||
authenticated: &AuthenticatedAccessToken,
|
||||
session_id: String,
|
||||
owner_user_id: String,
|
||||
profile_id: String,
|
||||
game_name: Option<String>,
|
||||
summary_text: Option<String>,
|
||||
tags_json: Option<String>,
|
||||
cover_image_src: Option<String>,
|
||||
cover_asset_id: Option<String>,
|
||||
generated_item_assets_json: Option<String>,
|
||||
) -> Result<Match3DAgentSessionRecord, Response> {
|
||||
state
|
||||
.spacetime_client()
|
||||
.compile_match3d_draft(Match3DCompileDraftRecordInput {
|
||||
session_id,
|
||||
owner_user_id,
|
||||
profile_id,
|
||||
author_display_name: resolve_author_display_name(state, authenticated),
|
||||
game_name,
|
||||
summary_text,
|
||||
tags_json,
|
||||
cover_image_src,
|
||||
cover_asset_id,
|
||||
compiled_at_micros: current_utc_micros(),
|
||||
generated_item_assets_json,
|
||||
})
|
||||
.await
|
||||
.map_err(|error| {
|
||||
match3d_error_response(
|
||||
request_context,
|
||||
MATCH3D_AGENT_PROVIDER,
|
||||
map_match3d_client_error(error),
|
||||
)
|
||||
})
|
||||
}
|
||||
pub(super) fn build_config_from_create_request(
|
||||
payload: &CreateMatch3DAgentSessionRequest,
|
||||
) -> Match3DConfigJson {
|
||||
Match3DConfigJson {
|
||||
theme_text: payload
|
||||
.theme_text
|
||||
.as_deref()
|
||||
.or(payload.seed_text.as_deref())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(MATCH3D_DEFAULT_THEME)
|
||||
.to_string(),
|
||||
reference_image_src: payload.reference_image_src.clone(),
|
||||
clear_count: payload.clear_count.unwrap_or(MATCH3D_DEFAULT_CLEAR_COUNT),
|
||||
difficulty: payload
|
||||
.difficulty
|
||||
.unwrap_or(MATCH3D_DEFAULT_DIFFICULTY)
|
||||
.clamp(1, 10),
|
||||
asset_style_id: normalize_optional_text(payload.asset_style_id.as_deref()),
|
||||
asset_style_label: normalize_optional_text(payload.asset_style_label.as_deref()),
|
||||
asset_style_prompt: normalize_optional_text(payload.asset_style_prompt.as_deref()),
|
||||
generate_click_sound: payload.generate_click_sound.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_config_from_message(
|
||||
session: &Match3DAgentSessionRecord,
|
||||
payload: &SendMatch3DAgentMessageRequest,
|
||||
) -> Match3DConfigJson {
|
||||
let current = resolve_config_or_default(session.config.as_ref());
|
||||
let text = payload.text.trim();
|
||||
let reference_image_src = payload
|
||||
.reference_image_src
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
.or(current.reference_image_src);
|
||||
let quick_fill_requested =
|
||||
payload.quick_fill_requested.unwrap_or(false) || text.contains("自动配置");
|
||||
|
||||
let mut theme_text = current.theme_text;
|
||||
let mut clear_count = current.clear_count.max(1);
|
||||
let mut difficulty = current.difficulty.clamp(1, 10);
|
||||
let asset_style_id = current.asset_style_id;
|
||||
let asset_style_label = current.asset_style_label;
|
||||
let asset_style_prompt = current.asset_style_prompt;
|
||||
let generate_click_sound = current.generate_click_sound;
|
||||
|
||||
match session.current_turn {
|
||||
0 => {
|
||||
theme_text = if quick_fill_requested {
|
||||
MATCH3D_DEFAULT_THEME.to_string()
|
||||
} else {
|
||||
parse_theme_answer(text).unwrap_or(theme_text)
|
||||
};
|
||||
}
|
||||
1 => {
|
||||
clear_count = if quick_fill_requested {
|
||||
clear_count
|
||||
} else {
|
||||
parse_number_after_keywords(text, &["消除", "次数", "clearCount"])
|
||||
.unwrap_or(clear_count)
|
||||
}
|
||||
.max(1);
|
||||
}
|
||||
_ => {
|
||||
difficulty = if quick_fill_requested {
|
||||
difficulty
|
||||
} else {
|
||||
parse_number_after_keywords(text, &["难度", "difficulty"]).unwrap_or(difficulty)
|
||||
}
|
||||
.clamp(1, 10);
|
||||
}
|
||||
}
|
||||
|
||||
Match3DConfigJson {
|
||||
theme_text,
|
||||
reference_image_src,
|
||||
clear_count,
|
||||
difficulty,
|
||||
asset_style_id,
|
||||
asset_style_label,
|
||||
asset_style_prompt,
|
||||
generate_click_sound,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn resolve_config_or_default(config: Option<&Match3DCreatorConfigRecord>) -> Match3DConfigJson {
|
||||
config
|
||||
.map(|config| Match3DConfigJson {
|
||||
theme_text: config.theme_text.clone(),
|
||||
reference_image_src: config.reference_image_src.clone(),
|
||||
clear_count: config.clear_count.max(1),
|
||||
difficulty: config.difficulty.clamp(1, 10),
|
||||
asset_style_id: config.asset_style_id.clone(),
|
||||
asset_style_label: config.asset_style_label.clone(),
|
||||
asset_style_prompt: config.asset_style_prompt.clone(),
|
||||
generate_click_sound: config.generate_click_sound,
|
||||
})
|
||||
.unwrap_or_else(|| Match3DConfigJson {
|
||||
theme_text: MATCH3D_DEFAULT_THEME.to_string(),
|
||||
reference_image_src: None,
|
||||
clear_count: MATCH3D_DEFAULT_CLEAR_COUNT,
|
||||
difficulty: MATCH3D_DEFAULT_DIFFICULTY,
|
||||
asset_style_id: None,
|
||||
asset_style_label: None,
|
||||
asset_style_prompt: None,
|
||||
generate_click_sound: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn normalize_optional_text(value: Option<&str>) -> Option<String> {
|
||||
value
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
pub(super) fn serialize_match3d_config(config: &Match3DConfigJson) -> Option<String> {
|
||||
serde_json::to_string(config).ok()
|
||||
}
|
||||
|
||||
pub(super) fn build_seed_text(
|
||||
payload: &CreateMatch3DAgentSessionRequest,
|
||||
config: &Match3DConfigJson,
|
||||
) -> String {
|
||||
payload
|
||||
.seed_text
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| {
|
||||
format!(
|
||||
"{}题材,消除{}次,难度{}",
|
||||
config.theme_text, config.clear_count, config.difficulty
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn build_match3d_assistant_reply(config: &Match3DConfigJson) -> String {
|
||||
format!(
|
||||
"已确认:{}题材,需要消除 {} 次,共 {} 件物品,难度 {}。",
|
||||
config.theme_text,
|
||||
config.clear_count,
|
||||
config.clear_count.saturating_mul(3),
|
||||
config.difficulty
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn build_match3d_assistant_reply_for_turn(config: &Match3DConfigJson, current_turn: u32) -> String {
|
||||
match current_turn {
|
||||
0 => MATCH3D_QUESTION_THEME.to_string(),
|
||||
1 => MATCH3D_QUESTION_CLEAR_COUNT.to_string(),
|
||||
2 => MATCH3D_QUESTION_DIFFICULTY.to_string(),
|
||||
_ => build_match3d_assistant_reply(config),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn resolve_progress_percent_for_turn(current_turn: u32) -> u32 {
|
||||
match current_turn {
|
||||
0 => 0,
|
||||
1 => 33,
|
||||
2 => 66,
|
||||
_ => 100,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_theme_answer(text: &str) -> Option<String> {
|
||||
for marker in ["题材", "主题"] {
|
||||
if let Some((_, value)) = text.split_once(marker) {
|
||||
let normalized = value
|
||||
.trim_matches(|ch: char| ch == ':' || ch == ':' || ch.is_whitespace())
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.trim_matches(['。', ',', ',', ';', ';'])
|
||||
.to_string();
|
||||
if !normalized.is_empty() {
|
||||
return Some(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
let trimmed = text.trim();
|
||||
if (2..=24).contains(&trimmed.chars().count()) && !trimmed.chars().any(|ch| ch.is_ascii_digit())
|
||||
{
|
||||
return Some(trimmed.to_string());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_number_after_keywords(text: &str, keywords: &[&str]) -> Option<u32> {
|
||||
for keyword in keywords {
|
||||
if let Some(index) = text.find(keyword) {
|
||||
let suffix = &text[index + keyword.len()..];
|
||||
if let Some(value) = first_positive_integer(suffix) {
|
||||
return Some(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
first_positive_integer(text)
|
||||
}
|
||||
|
||||
fn first_positive_integer(text: &str) -> Option<u32> {
|
||||
let mut digits = String::new();
|
||||
for ch in text.chars() {
|
||||
if ch.is_ascii_digit() {
|
||||
digits.push(ch);
|
||||
} else if !digits.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
digits.parse::<u32>().ok().filter(|value| *value > 0)
|
||||
}
|
||||
|
||||
pub(super) fn normalize_tags(tags: Vec<String>) -> Vec<String> {
|
||||
let mut result: Vec<String> = Vec::new();
|
||||
for tag in tags {
|
||||
let trimmed = normalize_match3d_tag(tag.as_str());
|
||||
if !trimmed.is_empty() && !result.iter().any(|value| value == &trimmed) {
|
||||
result.push(trimmed);
|
||||
}
|
||||
if result.len() >= 6 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn normalize_optional_match3d_text(value: Option<String>) -> Option<String> {
|
||||
value
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
async fn generate_match3d_draft_plan(
|
||||
state: &AppState,
|
||||
config: &Match3DConfigJson,
|
||||
) -> Match3DGeneratedDraftPlan {
|
||||
let Some(llm_client) = state
|
||||
.creative_agent_gpt5_client()
|
||||
.or_else(|| state.llm_client())
|
||||
else {
|
||||
return fallback_match3d_draft_plan(config);
|
||||
};
|
||||
let system_prompt = "你是抓大鹅游戏的草稿生成编辑,只返回 JSON。";
|
||||
let gameplay_item_count = resolve_match3d_gameplay_item_count(config);
|
||||
let generated_item_count = resolve_match3d_generated_item_count(config);
|
||||
let user_prompt = format!(
|
||||
"题材设定:{}\n请生成抓大鹅游戏草稿生成计划。要求:只返回 JSON 对象,字段为 gameName、summary、tags、backgroundPrompt、items。gameName 为 4 到 12 个中文字符,不要包含“作品”“游戏”;summary 为 18 到 48 个中文字符的作品描述,说明题材氛围和核心体验,不要写规则说明;tags 为 3 到 6 个中文短标签,每个 2 到 6 个汉字,后续会用同一作品信息再次生成作品标签;backgroundPrompt 是用于生成局内纯背景图的中文提示词,只描述竖屏移动端抓大鹅题材氛围、色彩和环境,不得描述锅、圆盘、托盘、拼图槽、物品槽、HUD、UI、文字、按钮、倒计时、分数或物品;当前玩法需要 {} 种物品,但素材图固定每 5 个物品一批,因此 items 必须向上补齐并正好返回 {} 项,每项包含 name、itemSize 和 soundPrompt。name 为 2 到 6 个汉字;itemSize 只能是“大”“中”“小”之一,按物品真实相对尺寸判断,例如西瓜/大箱子偏大,苹果/杯子偏中,糖果/钥匙偏小;soundPrompt 只作为历史字段保留,可返回空字符串。",
|
||||
config.theme_text, gameplay_item_count, generated_item_count
|
||||
);
|
||||
let response = llm_client
|
||||
.request_text(
|
||||
LlmTextRequest::new(vec![
|
||||
LlmMessage::system(system_prompt),
|
||||
LlmMessage::user(user_prompt),
|
||||
])
|
||||
.with_model(MATCH3D_WORK_METADATA_LLM_MODEL)
|
||||
.with_responses_api(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match response {
|
||||
Ok(response) => parse_match3d_draft_plan(response.content.as_str(), config)
|
||||
.unwrap_or_else(|| fallback_match3d_draft_plan(config)),
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
provider = MATCH3D_AGENT_PROVIDER,
|
||||
theme_text = config.theme_text.as_str(),
|
||||
error = %error,
|
||||
"抓大鹅草稿生成计划失败,降级使用本地生成计划"
|
||||
);
|
||||
fallback_match3d_draft_plan(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn parse_match3d_draft_plan(
|
||||
raw: &str,
|
||||
config: &Match3DConfigJson,
|
||||
) -> Option<Match3DGeneratedDraftPlan> {
|
||||
let raw = raw.trim();
|
||||
let json_text = if let Some(start) = raw.find('{')
|
||||
&& let Some(end) = raw.rfind('}')
|
||||
&& end > start
|
||||
{
|
||||
&raw[start..=end]
|
||||
} else {
|
||||
raw
|
||||
};
|
||||
let value = serde_json::from_str::<Value>(json_text).ok()?;
|
||||
let game_name = normalize_match3d_game_name(value.get("gameName")?.as_str()?);
|
||||
if game_name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let tags = value
|
||||
.get("tags")
|
||||
.and_then(Value::as_array)
|
||||
.map(|items| normalize_match3d_tag_candidates(items.iter().filter_map(Value::as_str)))
|
||||
.unwrap_or_default();
|
||||
let fallback = fallback_match3d_draft_plan(config);
|
||||
let summary = value
|
||||
.get("summary")
|
||||
.or_else(|| value.get("description"))
|
||||
.or_else(|| value.get("workSummary"))
|
||||
.or_else(|| value.get("work_summary"))
|
||||
.and_then(Value::as_str)
|
||||
.map(normalize_match3d_work_summary)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(fallback.metadata.summary);
|
||||
let items = value
|
||||
.get("items")
|
||||
.and_then(Value::as_array)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let name =
|
||||
normalize_match3d_item_name(item.get("name").and_then(Value::as_str)?);
|
||||
if name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let item_size = item
|
||||
.get("itemSize")
|
||||
.or_else(|| item.get("item_size"))
|
||||
.or_else(|| item.get("size"))
|
||||
.and_then(Value::as_str)
|
||||
.map(normalize_match3d_item_size)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| infer_match3d_item_size(&name));
|
||||
let sound_prompt = item
|
||||
.get("soundPrompt")
|
||||
.or_else(|| item.get("sound_prompt"))
|
||||
.and_then(Value::as_str)
|
||||
.map(normalize_match3d_audio_prompt)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| build_fallback_match3d_item_sound_prompt(config, &name));
|
||||
Some(Match3DGeneratedItemPlan {
|
||||
name,
|
||||
item_size,
|
||||
sound_prompt,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let background_prompt = value
|
||||
.get("backgroundPrompt")
|
||||
.or_else(|| value.get("background_prompt"))
|
||||
.and_then(Value::as_str)
|
||||
.map(normalize_match3d_background_prompt)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(fallback.background_prompt);
|
||||
|
||||
Some(Match3DGeneratedDraftPlan {
|
||||
metadata: Match3DGeneratedWorkMetadata {
|
||||
game_name,
|
||||
summary,
|
||||
tags: normalize_match3d_tag_candidates(tags),
|
||||
},
|
||||
items: normalize_match3d_item_plan(config, items),
|
||||
background_prompt,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn parse_match3d_work_metadata(raw: &str) -> Option<Match3DGeneratedWorkMetadata> {
|
||||
let config = Match3DConfigJson {
|
||||
theme_text: MATCH3D_DEFAULT_THEME.to_string(),
|
||||
reference_image_src: None,
|
||||
clear_count: MATCH3D_DEFAULT_CLEAR_COUNT,
|
||||
difficulty: MATCH3D_DEFAULT_DIFFICULTY,
|
||||
asset_style_id: None,
|
||||
asset_style_label: None,
|
||||
asset_style_prompt: None,
|
||||
generate_click_sound: false,
|
||||
};
|
||||
parse_match3d_draft_plan(raw, &config).map(|plan| plan.metadata)
|
||||
}
|
||||
|
||||
fn normalize_match3d_game_name(raw: &str) -> String {
|
||||
raw.trim()
|
||||
.trim_matches(['"', '\'', '“', '”', '。', ',', ',', '、'])
|
||||
.chars()
|
||||
.filter(|character| !character.is_control())
|
||||
.take(16)
|
||||
.collect::<String>()
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn normalize_match3d_work_summary(raw: &str) -> String {
|
||||
raw.trim()
|
||||
.trim_matches(['"', '\'', '“', '”'])
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
.chars()
|
||||
.filter(|character| !character.is_control())
|
||||
.take(80)
|
||||
.collect::<String>()
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub(super) fn fallback_match3d_work_metadata(theme_text: &str) -> Match3DGeneratedWorkMetadata {
|
||||
let theme = theme_text.trim();
|
||||
let normalized_theme = if theme.is_empty() { "主题" } else { theme };
|
||||
Match3DGeneratedWorkMetadata {
|
||||
game_name: format!("{normalized_theme}抓大鹅"),
|
||||
summary: normalize_match3d_work_summary(
|
||||
format!("{normalized_theme}主题的轻量抓取消除作品,适合快速体验。").as_str(),
|
||||
),
|
||||
tags: normalize_match3d_tag_candidates([normalized_theme, "抓大鹅", "经典消除", "2D素材"]),
|
||||
}
|
||||
}
|
||||
|
||||
fn fallback_match3d_draft_plan(config: &Match3DConfigJson) -> Match3DGeneratedDraftPlan {
|
||||
let metadata = fallback_match3d_work_metadata(config.theme_text.as_str());
|
||||
let items = fallback_match3d_item_names(config.theme_text.as_str())
|
||||
.into_iter()
|
||||
.take(resolve_match3d_generated_item_count(config))
|
||||
.map(|name| Match3DGeneratedItemPlan {
|
||||
item_size: infer_match3d_item_size(&name),
|
||||
sound_prompt: build_fallback_match3d_item_sound_prompt(config, &name),
|
||||
name,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Match3DGeneratedDraftPlan {
|
||||
background_prompt: build_fallback_match3d_background_prompt(config),
|
||||
metadata,
|
||||
items,
|
||||
}
|
||||
}
|
||||
1406
server-rs/crates/api-server/src/match3d/handlers.rs
Normal file
1406
server-rs/crates/api-server/src/match3d/handlers.rs
Normal file
File diff suppressed because it is too large
Load Diff
2631
server-rs/crates/api-server/src/match3d/item_assets.rs
Normal file
2631
server-rs/crates/api-server/src/match3d/item_assets.rs
Normal file
File diff suppressed because it is too large
Load Diff
37
server-rs/crates/api-server/src/match3d/runtime.rs
Normal file
37
server-rs/crates/api-server/src/match3d/runtime.rs
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
1875
server-rs/crates/api-server/src/match3d/tests.rs
Normal file
1875
server-rs/crates/api-server/src/match3d/tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
483
server-rs/crates/api-server/src/match3d/vector_engine_gemini.rs
Normal file
483
server-rs/crates/api-server/src/match3d/vector_engine_gemini.rs
Normal file
@@ -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",
|
||||
}
|
||||
}
|
||||
1254
server-rs/crates/api-server/src/match3d/works.rs
Normal file
1254
server-rs/crates/api-server/src/match3d/works.rs
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
306
server-rs/crates/spacetime-client/src/mapper/ai.rs
Normal file
306
server-rs/crates/spacetime-client/src/mapper/ai.rs
Normal file
@@ -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>,
|
||||
}
|
||||
382
server-rs/crates/spacetime-client/src/mapper/assets.rs
Normal file
382
server-rs/crates/spacetime-client/src/mapper/assets.rs
Normal file
@@ -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,
|
||||
}
|
||||
42
server-rs/crates/spacetime-client/src/mapper/auth.rs
Normal file
42
server-rs/crates/spacetime-client/src/mapper/auth.rs
Normal file
@@ -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,
|
||||
})
|
||||
}
|
||||
94
server-rs/crates/spacetime-client/src/mapper/bark_battle.rs
Normal file
94
server-rs/crates/spacetime-client/src/mapper/bark_battle.rs
Normal file
@@ -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,
|
||||
})
|
||||
}
|
||||
616
server-rs/crates/spacetime-client/src/mapper/big_fish.rs
Normal file
616
server-rs/crates/spacetime-client/src/mapper/big_fish.rs
Normal file
@@ -0,0 +1,616 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn map_big_fish_session_procedure_result(
|
||||
result: BigFishSessionProcedureResult,
|
||||
) -> Result<BigFishSessionRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let session = result
|
||||
.session
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("big fish session 快照"))?;
|
||||
|
||||
Ok(map_big_fish_session_snapshot(session))
|
||||
}
|
||||
|
||||
pub(crate) fn map_big_fish_works_procedure_result(
|
||||
result: BigFishWorksProcedureResult,
|
||||
_fallback_owner_user_id: Option<&str>,
|
||||
) -> Result<Vec<BigFishWorkSummaryRecord>, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
Ok(result
|
||||
.items
|
||||
.into_iter()
|
||||
.map(map_big_fish_work_summary_snapshot)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(crate) fn map_big_fish_run_procedure_result(
|
||||
result: BigFishRunProcedureResult,
|
||||
) -> Result<BigFishRuntimeRunRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let run = result
|
||||
.run
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("big fish run 快照"))?;
|
||||
Ok(map_big_fish_runtime_snapshot(run))
|
||||
}
|
||||
|
||||
pub(crate) fn map_big_fish_session_snapshot(
|
||||
snapshot: BigFishSessionSnapshot,
|
||||
) -> BigFishSessionRecord {
|
||||
BigFishSessionRecord {
|
||||
session_id: snapshot.session_id,
|
||||
current_turn: snapshot.current_turn,
|
||||
progress_percent: snapshot.progress_percent,
|
||||
stage: format_big_fish_creation_stage(snapshot.stage).to_string(),
|
||||
anchor_pack: map_big_fish_anchor_pack(snapshot.anchor_pack),
|
||||
draft: snapshot.draft.map(map_big_fish_game_draft),
|
||||
asset_slots: snapshot
|
||||
.asset_slots
|
||||
.into_iter()
|
||||
.map(map_big_fish_asset_slot_snapshot)
|
||||
.collect(),
|
||||
asset_coverage: map_big_fish_asset_coverage(snapshot.asset_coverage),
|
||||
messages: snapshot
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(map_big_fish_agent_message_snapshot)
|
||||
.collect(),
|
||||
last_assistant_reply: snapshot.last_assistant_reply,
|
||||
publish_ready: snapshot.publish_ready,
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_big_fish_anchor_pack(snapshot: BigFishAnchorPack) -> BigFishAnchorPackRecord {
|
||||
BigFishAnchorPackRecord {
|
||||
gameplay_promise: map_big_fish_anchor_item(snapshot.gameplay_promise),
|
||||
ecology_visual_theme: map_big_fish_anchor_item(snapshot.ecology_visual_theme),
|
||||
growth_ladder: map_big_fish_anchor_item(snapshot.growth_ladder),
|
||||
risk_tempo: map_big_fish_anchor_item(snapshot.risk_tempo),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_big_fish_anchor_item(snapshot: BigFishAnchorItem) -> BigFishAnchorItemRecord {
|
||||
BigFishAnchorItemRecord {
|
||||
key: snapshot.key,
|
||||
label: snapshot.label,
|
||||
value: snapshot.value,
|
||||
status: format_big_fish_anchor_status(snapshot.status).to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_big_fish_game_draft(snapshot: BigFishGameDraft) -> BigFishGameDraftRecord {
|
||||
BigFishGameDraftRecord {
|
||||
title: snapshot.title,
|
||||
subtitle: snapshot.subtitle,
|
||||
core_fun: snapshot.core_fun,
|
||||
ecology_theme: snapshot.ecology_theme,
|
||||
levels: snapshot
|
||||
.levels
|
||||
.into_iter()
|
||||
.map(map_big_fish_level_blueprint)
|
||||
.collect(),
|
||||
background: map_big_fish_background_blueprint(snapshot.background),
|
||||
runtime_params: map_big_fish_runtime_params(snapshot.runtime_params),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_big_fish_level_blueprint(
|
||||
snapshot: BigFishLevelBlueprint,
|
||||
) -> BigFishLevelBlueprintRecord {
|
||||
BigFishLevelBlueprintRecord {
|
||||
level: snapshot.level,
|
||||
name: snapshot.name,
|
||||
one_line_fantasy: snapshot.one_line_fantasy,
|
||||
text_description: snapshot.text_description,
|
||||
silhouette_direction: snapshot.silhouette_direction,
|
||||
size_ratio: snapshot.size_ratio,
|
||||
visual_description: snapshot.visual_description,
|
||||
visual_prompt_seed: snapshot.visual_prompt_seed,
|
||||
idle_motion_description: snapshot.idle_motion_description,
|
||||
move_motion_description: snapshot.move_motion_description,
|
||||
motion_prompt_seed: snapshot.motion_prompt_seed,
|
||||
merge_source_level: snapshot.merge_source_level,
|
||||
prey_window: snapshot.prey_window,
|
||||
threat_window: snapshot.threat_window,
|
||||
is_final_level: snapshot.is_final_level,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_big_fish_background_blueprint(
|
||||
snapshot: BigFishBackgroundBlueprint,
|
||||
) -> BigFishBackgroundBlueprintRecord {
|
||||
BigFishBackgroundBlueprintRecord {
|
||||
theme: snapshot.theme,
|
||||
color_mood: snapshot.color_mood,
|
||||
foreground_hints: snapshot.foreground_hints,
|
||||
midground_composition: snapshot.midground_composition,
|
||||
background_depth: snapshot.background_depth,
|
||||
safe_play_area_hint: snapshot.safe_play_area_hint,
|
||||
spawn_edge_hint: snapshot.spawn_edge_hint,
|
||||
background_prompt_seed: snapshot.background_prompt_seed,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_big_fish_runtime_params(
|
||||
snapshot: BigFishRuntimeParams,
|
||||
) -> BigFishRuntimeParamsRecord {
|
||||
BigFishRuntimeParamsRecord {
|
||||
level_count: snapshot.level_count,
|
||||
merge_count_per_upgrade: snapshot.merge_count_per_upgrade,
|
||||
spawn_target_count: snapshot.spawn_target_count,
|
||||
leader_move_speed: snapshot.leader_move_speed,
|
||||
follower_catch_up_speed: snapshot.follower_catch_up_speed,
|
||||
offscreen_cull_seconds: snapshot.offscreen_cull_seconds,
|
||||
prey_spawn_delta_levels: snapshot.prey_spawn_delta_levels,
|
||||
threat_spawn_delta_levels: snapshot.threat_spawn_delta_levels,
|
||||
win_level: snapshot.win_level,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_big_fish_asset_slot_snapshot(
|
||||
snapshot: BigFishAssetSlotSnapshot,
|
||||
) -> BigFishAssetSlotRecord {
|
||||
BigFishAssetSlotRecord {
|
||||
slot_id: snapshot.slot_id,
|
||||
asset_kind: format_big_fish_asset_kind(snapshot.asset_kind).to_string(),
|
||||
level: snapshot.level,
|
||||
motion_key: snapshot.motion_key,
|
||||
status: format_big_fish_asset_status(snapshot.status).to_string(),
|
||||
asset_url: snapshot.asset_url,
|
||||
prompt_snapshot: snapshot.prompt_snapshot,
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_big_fish_asset_coverage(
|
||||
snapshot: BigFishAssetCoverage,
|
||||
) -> BigFishAssetCoverageRecord {
|
||||
BigFishAssetCoverageRecord {
|
||||
level_main_image_ready_count: snapshot.level_main_image_ready_count,
|
||||
level_motion_ready_count: snapshot.level_motion_ready_count,
|
||||
background_ready: snapshot.background_ready,
|
||||
required_level_count: snapshot.required_level_count,
|
||||
publish_ready: snapshot.publish_ready,
|
||||
blockers: snapshot.blockers,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_big_fish_agent_message_snapshot(
|
||||
snapshot: BigFishAgentMessageSnapshot,
|
||||
) -> BigFishAgentMessageRecord {
|
||||
BigFishAgentMessageRecord {
|
||||
message_id: snapshot.message_id,
|
||||
role: format_big_fish_agent_message_role(snapshot.role).to_string(),
|
||||
kind: format_big_fish_agent_message_kind(snapshot.kind).to_string(),
|
||||
text: snapshot.text,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_big_fish_work_summary_snapshot(
|
||||
snapshot: BigFishWorkSummarySnapshot,
|
||||
) -> BigFishWorkSummaryRecord {
|
||||
BigFishWorkSummaryRecord {
|
||||
work_id: snapshot.work_id,
|
||||
source_session_id: snapshot.source_session_id,
|
||||
owner_user_id: snapshot.owner_user_id,
|
||||
title: snapshot.title,
|
||||
subtitle: snapshot.subtitle,
|
||||
summary: snapshot.summary,
|
||||
cover_image_src: snapshot.cover_image_src,
|
||||
status: snapshot.status,
|
||||
updated_at_micros: snapshot.updated_at_micros,
|
||||
published_at_micros: snapshot.published_at_micros,
|
||||
publish_ready: snapshot.publish_ready,
|
||||
level_count: snapshot.level_count,
|
||||
level_main_image_ready_count: snapshot.level_main_image_ready_count,
|
||||
level_motion_ready_count: snapshot.level_motion_ready_count,
|
||||
background_ready: snapshot.background_ready,
|
||||
play_count: snapshot.play_count,
|
||||
remix_count: snapshot.remix_count,
|
||||
like_count: snapshot.like_count,
|
||||
recent_play_count_7d: snapshot.recent_play_count_7_d,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_big_fish_gallery_view_row(
|
||||
row: BigFishWorkSummarySnapshot,
|
||||
recent_play_count_7d: u32,
|
||||
) -> BigFishWorkSummaryRecord {
|
||||
let mut record = map_big_fish_work_summary_snapshot(row);
|
||||
record.recent_play_count_7d = recent_play_count_7d;
|
||||
record
|
||||
}
|
||||
|
||||
pub(crate) fn map_big_fish_runtime_snapshot(
|
||||
snapshot: BigFishRuntimeSnapshot,
|
||||
) -> BigFishRuntimeRunRecord {
|
||||
BigFishRuntimeRunRecord {
|
||||
run_id: snapshot.run_id,
|
||||
session_id: snapshot.session_id,
|
||||
status: format_big_fish_run_status(snapshot.status).to_string(),
|
||||
tick: snapshot.tick,
|
||||
player_level: snapshot.player_level,
|
||||
win_level: snapshot.win_level,
|
||||
leader_entity_id: snapshot.leader_entity_id,
|
||||
owned_entities: snapshot
|
||||
.owned_entities
|
||||
.into_iter()
|
||||
.map(map_big_fish_runtime_entity_snapshot)
|
||||
.collect(),
|
||||
wild_entities: snapshot
|
||||
.wild_entities
|
||||
.into_iter()
|
||||
.map(map_big_fish_runtime_entity_snapshot)
|
||||
.collect(),
|
||||
camera_center: map_big_fish_vector2(snapshot.camera_center),
|
||||
last_input: map_big_fish_vector2(snapshot.last_input),
|
||||
event_log: snapshot.event_log,
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_big_fish_runtime_entity_snapshot(
|
||||
snapshot: BigFishRuntimeEntitySnapshot,
|
||||
) -> BigFishRuntimeEntityRecord {
|
||||
BigFishRuntimeEntityRecord {
|
||||
entity_id: snapshot.entity_id,
|
||||
level: snapshot.level,
|
||||
position: map_big_fish_vector2(snapshot.position),
|
||||
radius: snapshot.radius,
|
||||
offscreen_seconds: snapshot.offscreen_seconds,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_big_fish_vector2(snapshot: BigFishVector2) -> BigFishVector2Record {
|
||||
BigFishVector2Record {
|
||||
x: snapshot.x,
|
||||
y: snapshot.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_big_fish_creation_stage(
|
||||
value: &str,
|
||||
) -> Result<BigFishCreationStage, SpacetimeClientError> {
|
||||
match value.trim() {
|
||||
"collecting_anchors" => Ok(BigFishCreationStage::CollectingAnchors),
|
||||
"draft_ready" => Ok(BigFishCreationStage::DraftReady),
|
||||
"asset_refining" => Ok(BigFishCreationStage::AssetRefining),
|
||||
"ready_to_publish" => Ok(BigFishCreationStage::ReadyToPublish),
|
||||
"published" => Ok(BigFishCreationStage::Published),
|
||||
other => Err(SpacetimeClientError::Runtime(format!(
|
||||
"big fish creation stage `{other}` 当前尚未支持"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_big_fish_creation_stage(value: BigFishCreationStage) -> &'static str {
|
||||
match value {
|
||||
BigFishCreationStage::CollectingAnchors => "collecting_anchors",
|
||||
BigFishCreationStage::DraftReady => "draft_ready",
|
||||
BigFishCreationStage::AssetRefining => "asset_refining",
|
||||
BigFishCreationStage::ReadyToPublish => "ready_to_publish",
|
||||
BigFishCreationStage::Published => "published",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_big_fish_anchor_status(value: BigFishAnchorStatus) -> &'static str {
|
||||
match value {
|
||||
BigFishAnchorStatus::Confirmed => "confirmed",
|
||||
BigFishAnchorStatus::Inferred => "inferred",
|
||||
BigFishAnchorStatus::Missing => "missing",
|
||||
BigFishAnchorStatus::Locked => "locked",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_big_fish_agent_message_role(value: BigFishAgentMessageRole) -> &'static str {
|
||||
match value {
|
||||
BigFishAgentMessageRole::User => "user",
|
||||
BigFishAgentMessageRole::Assistant => "assistant",
|
||||
BigFishAgentMessageRole::System => "system",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_big_fish_agent_message_kind(value: BigFishAgentMessageKind) -> &'static str {
|
||||
match value {
|
||||
BigFishAgentMessageKind::Chat => "chat",
|
||||
BigFishAgentMessageKind::Summary => "summary",
|
||||
BigFishAgentMessageKind::ActionResult => "action_result",
|
||||
BigFishAgentMessageKind::Warning => "warning",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_big_fish_asset_kind(value: BigFishAssetKind) -> &'static str {
|
||||
match value {
|
||||
BigFishAssetKind::LevelMainImage => "level_main_image",
|
||||
BigFishAssetKind::LevelMotion => "level_motion",
|
||||
BigFishAssetKind::StageBackground => "stage_background",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_big_fish_asset_status(value: BigFishAssetStatus) -> &'static str {
|
||||
match value {
|
||||
BigFishAssetStatus::Missing => "missing",
|
||||
BigFishAssetStatus::Ready => "ready",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_big_fish_run_status(value: BigFishRunStatus) -> &'static str {
|
||||
match value {
|
||||
BigFishRunStatus::Running => "running",
|
||||
BigFishRunStatus::Won => "won",
|
||||
BigFishRunStatus::Failed => "failed",
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct BigFishWorkSummaryRecord {
|
||||
pub work_id: String,
|
||||
pub source_session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub summary: String,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub status: String,
|
||||
pub updated_at_micros: i64,
|
||||
pub published_at_micros: Option<i64>,
|
||||
pub publish_ready: bool,
|
||||
pub level_count: u32,
|
||||
pub level_main_image_ready_count: u32,
|
||||
pub level_motion_ready_count: u32,
|
||||
pub background_ready: bool,
|
||||
pub play_count: u32,
|
||||
pub remix_count: u32,
|
||||
pub like_count: u32,
|
||||
pub recent_play_count_7d: u32,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn puzzle_works_mapper_keeps_typed_public_stat_fields() {
|
||||
let result = PuzzleWorksProcedureResult {
|
||||
ok: true,
|
||||
items: vec![PuzzleWorkProfile {
|
||||
work_id: "puzzle-work-1".to_string(),
|
||||
profile_id: "puzzle-profile-1".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
source_session_id: None,
|
||||
author_display_name: "测试作者".to_string(),
|
||||
work_title: "雨夜拼图作品".to_string(),
|
||||
work_description: "拼图作品说明".to_string(),
|
||||
level_name: "雨夜拼图".to_string(),
|
||||
summary: "公开作品摘要".to_string(),
|
||||
theme_tags: vec!["雨夜".to_string(), "猫咪".to_string(), "神庙".to_string()],
|
||||
cover_image_src: None,
|
||||
cover_asset_id: None,
|
||||
levels: Vec::new(),
|
||||
publication_status: PuzzlePublicationStatus::Published,
|
||||
updated_at_micros: 123000000,
|
||||
published_at_micros: Some(123000000),
|
||||
play_count: 11,
|
||||
remix_count: 7,
|
||||
like_count: 5,
|
||||
recent_play_count_7_d: 3,
|
||||
point_incentive_total_half_points: 4,
|
||||
point_incentive_claimed_points: 2,
|
||||
publish_ready: true,
|
||||
anchor_pack: test_puzzle_anchor_pack(),
|
||||
}],
|
||||
error_message: None,
|
||||
};
|
||||
|
||||
let items = map_puzzle_works_procedure_result(result)
|
||||
.expect("typed puzzle works result 应能映射统计字段");
|
||||
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0].play_count, 11);
|
||||
assert_eq!(items[0].remix_count, 7);
|
||||
assert_eq!(items[0].like_count, 5);
|
||||
assert_eq!(items[0].recent_play_count_7d, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_run_mapper_maps_typed_timer_fields() {
|
||||
let result = PuzzleRunProcedureResult {
|
||||
ok: true,
|
||||
run: Some(PuzzleRunSnapshot {
|
||||
run_id: "puzzle-run-1".to_string(),
|
||||
entry_profile_id: "puzzle-profile-1".to_string(),
|
||||
cleared_level_count: 0,
|
||||
current_level_index: 1,
|
||||
current_grid_size: 3,
|
||||
played_profile_ids: vec!["puzzle-profile-1".to_string()],
|
||||
previous_level_tags: vec![
|
||||
"雨夜".to_string(),
|
||||
"猫咪".to_string(),
|
||||
"神庙".to_string(),
|
||||
],
|
||||
current_level: Some(PuzzleRuntimeLevelSnapshot {
|
||||
run_id: "puzzle-run-1".to_string(),
|
||||
level_index: 1,
|
||||
level_id: None,
|
||||
grid_size: 3,
|
||||
profile_id: "puzzle-profile-1".to_string(),
|
||||
level_name: "雨夜拼图".to_string(),
|
||||
author_display_name: "测试作者".to_string(),
|
||||
theme_tags: vec!["雨夜".to_string(), "猫咪".to_string(), "神庙".to_string()],
|
||||
cover_image_src: None,
|
||||
ui_background_image_src: None,
|
||||
ui_background_image_object_key: None,
|
||||
background_music: None,
|
||||
board: PuzzleBoardSnapshot {
|
||||
rows: 3,
|
||||
cols: 3,
|
||||
pieces: vec![PuzzlePieceState {
|
||||
piece_id: "piece-1".to_string(),
|
||||
correct_row: 0,
|
||||
correct_col: 0,
|
||||
current_row: 0,
|
||||
current_col: 0,
|
||||
merged_group_id: None,
|
||||
}],
|
||||
merged_groups: Vec::new(),
|
||||
selected_piece_id: None,
|
||||
all_tiles_resolved: false,
|
||||
},
|
||||
status: PuzzleRuntimeLevelStatus::Playing,
|
||||
started_at_ms: 0,
|
||||
cleared_at_ms: None,
|
||||
elapsed_ms: None,
|
||||
time_limit_ms: 0,
|
||||
remaining_ms: 0,
|
||||
paused_accumulated_ms: 0,
|
||||
pause_started_at_ms: None,
|
||||
freeze_accumulated_ms: 0,
|
||||
freeze_started_at_ms: None,
|
||||
freeze_until_ms: None,
|
||||
leaderboard_entries: Vec::new(),
|
||||
}),
|
||||
recommended_next_profile_id: None,
|
||||
next_level_mode: "none".to_string(),
|
||||
next_level_profile_id: None,
|
||||
next_level_id: None,
|
||||
recommended_next_works: Vec::new(),
|
||||
leaderboard_entries: Vec::new(),
|
||||
}),
|
||||
error_message: None,
|
||||
};
|
||||
|
||||
let run = map_puzzle_run_procedure_result(result)
|
||||
.expect("typed puzzle run result 应能映射计时字段");
|
||||
let level = run.current_level.expect("兼容后仍应保留当前关卡");
|
||||
|
||||
assert_eq!(run.run_id, "puzzle-run-1");
|
||||
assert!(level.started_at_ms > 0);
|
||||
assert_eq!(level.time_limit_ms, 0);
|
||||
assert_eq!(level.remaining_ms, 0);
|
||||
assert!(level.leaderboard_entries.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn big_fish_works_mapper_uses_typed_owner_and_public_stats() {
|
||||
let result = BigFishWorksProcedureResult {
|
||||
ok: true,
|
||||
items: vec![BigFishWorkSummarySnapshot {
|
||||
work_id: "big-fish-work-session-1".to_string(),
|
||||
source_session_id: "session-1".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
title: "深海草稿".to_string(),
|
||||
subtitle: "副标题".to_string(),
|
||||
summary: "摘要".to_string(),
|
||||
cover_image_src: None,
|
||||
status: "draft".to_string(),
|
||||
updated_at_micros: 123,
|
||||
publish_ready: false,
|
||||
level_count: 8,
|
||||
level_main_image_ready_count: 0,
|
||||
level_motion_ready_count: 0,
|
||||
background_ready: false,
|
||||
play_count: 9,
|
||||
remix_count: 4,
|
||||
like_count: 2,
|
||||
recent_play_count_7_d: 6,
|
||||
published_at_micros: None,
|
||||
}],
|
||||
error_message: None,
|
||||
};
|
||||
|
||||
let items = map_big_fish_works_procedure_result(result, Some("user-1"))
|
||||
.expect("typed big fish works result 应能映射 owner 和统计字段");
|
||||
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0].owner_user_id, "user-1");
|
||||
assert_eq!(items[0].published_at_micros, None);
|
||||
assert_eq!(items[0].play_count, 9);
|
||||
assert_eq!(items[0].remix_count, 4);
|
||||
assert_eq!(items[0].like_count, 2);
|
||||
assert_eq!(items[0].recent_play_count_7d, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match3d_work_mapper_keeps_generated_item_assets_json() {
|
||||
let result = Match3DWorkProcedureResult {
|
||||
ok: true,
|
||||
work: Some(Match3DWorkSnapshot {
|
||||
profile_id: "match3d-profile-1".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
source_session_id: "match3d-session-1".to_string(),
|
||||
author_display_name: "测试作者".to_string(),
|
||||
game_name: "水果抓大鹅".to_string(),
|
||||
theme_text: "水果".to_string(),
|
||||
summary_text: "水果主题".to_string(),
|
||||
tags: vec!["水果".to_string()],
|
||||
cover_image_src: String::new(),
|
||||
cover_asset_id: String::new(),
|
||||
clear_count: 3,
|
||||
difficulty: 3,
|
||||
config: Match3DCreatorConfigSnapshot {
|
||||
theme_text: "水果".to_string(),
|
||||
reference_image_src: None,
|
||||
clear_count: 3,
|
||||
difficulty: 3,
|
||||
asset_style_id: None,
|
||||
asset_style_label: None,
|
||||
asset_style_prompt: None,
|
||||
generate_click_sound: false,
|
||||
},
|
||||
publication_status: "Draft".to_string(),
|
||||
publish_ready: false,
|
||||
play_count: 0,
|
||||
updated_at_micros: 123000000,
|
||||
published_at_micros: None,
|
||||
generated_item_assets_json: Some(
|
||||
r#"[{"itemId":"match3d-item-1","itemName":"草莓","imageSrc":"/generated-match3d-assets/session/profile/items/item/image.png","status":"image_ready"}]"#
|
||||
.to_string(),
|
||||
),
|
||||
}),
|
||||
error_message: None,
|
||||
};
|
||||
|
||||
let item = map_match3d_work_procedure_result(result)
|
||||
.expect("typed match3d work result 应保留生成素材 JSON");
|
||||
|
||||
assert_eq!(
|
||||
item.generated_item_assets_json.as_deref(),
|
||||
Some(
|
||||
r#"[{"itemId":"match3d-item-1","itemName":"草莓","imageSrc":"/generated-match3d-assets/session/profile/items/item/image.png","status":"image_ready"}]"#
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
fn test_puzzle_anchor_pack() -> PuzzleAnchorPack {
|
||||
PuzzleAnchorPack {
|
||||
theme_promise: test_puzzle_anchor_item("themePromise", "题材承诺", "雨夜冒险"),
|
||||
visual_subject: test_puzzle_anchor_item("visualSubject", "画面主体", "猫咪神庙"),
|
||||
visual_mood: test_puzzle_anchor_item("visualMood", "视觉气质", "温暖"),
|
||||
composition_hooks: test_puzzle_anchor_item("compositionHooks", "拼图记忆点", "灯光"),
|
||||
tags_and_forbidden: test_puzzle_anchor_item(
|
||||
"tagsAndForbidden",
|
||||
"标签与禁忌",
|
||||
"雨夜, 猫咪, 神庙",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn test_puzzle_anchor_item(key: &str, label: &str, value: &str) -> PuzzleAnchorItem {
|
||||
PuzzleAnchorItem {
|
||||
key: key.to_string(),
|
||||
label: label.to_string(),
|
||||
value: value.to_string(),
|
||||
status: PuzzleAnchorStatus::Inferred,
|
||||
}
|
||||
}
|
||||
}
|
||||
124
server-rs/crates/spacetime-client/src/mapper/combat.rs
Normal file
124
server-rs/crates/spacetime-client/src/mapper/combat.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use super::*;
|
||||
|
||||
impl From<DomainBattleStateQueryInput> for BattleStateQueryInput {
|
||||
fn from(input: DomainBattleStateQueryInput) -> Self {
|
||||
Self {
|
||||
battle_state_id: input.battle_state_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomainResolveCombatActionInput> for ResolveCombatActionInput {
|
||||
fn from(input: DomainResolveCombatActionInput) -> Self {
|
||||
Self {
|
||||
battle_state_id: input.battle_state_id,
|
||||
function_id: input.function_id,
|
||||
action_text: input.action_text,
|
||||
base_damage: input.base_damage,
|
||||
mana_cost: input.mana_cost,
|
||||
heal: input.heal,
|
||||
mana_restore: input.mana_restore,
|
||||
counter_multiplier_basis_points: input.counter_multiplier_basis_points,
|
||||
updated_at_micros: input.updated_at_micros,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type BarkBattleDraftConfigRecord = serde_json::Value;
|
||||
|
||||
pub type BarkBattleRuntimeConfigRecord = serde_json::Value;
|
||||
|
||||
pub type BarkBattleRunRecord = serde_json::Value;
|
||||
|
||||
pub(crate) fn map_battle_state_procedure_result(
|
||||
result: BattleStateProcedureResult,
|
||||
) -> Result<BattleStateRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let snapshot = result
|
||||
.snapshot
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("battle_state 快照"))?;
|
||||
|
||||
Ok(build_battle_state_record(map_battle_state_snapshot(
|
||||
snapshot,
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) fn map_resolve_combat_action_procedure_result(
|
||||
result: ResolveCombatActionProcedureResult,
|
||||
) -> Result<ResolveCombatActionRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let action_result = result
|
||||
.result
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("战斗结算结果"))?;
|
||||
|
||||
Ok(build_resolve_combat_action_record(
|
||||
map_resolve_combat_action_result(action_result),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn map_resolve_combat_action_result(
|
||||
result: ResolveCombatActionResult,
|
||||
) -> DomainResolveCombatActionResult {
|
||||
DomainResolveCombatActionResult {
|
||||
snapshot: map_battle_state_snapshot(result.snapshot),
|
||||
damage_dealt: result.damage_dealt,
|
||||
damage_taken: result.damage_taken,
|
||||
outcome: map_combat_outcome(result.outcome),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_battle_mode(value: DomainBattleMode) -> BattleMode {
|
||||
match value {
|
||||
DomainBattleMode::Fight => BattleMode::Fight,
|
||||
DomainBattleMode::Spar => BattleMode::Spar,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_battle_mode_back(value: BattleMode) -> DomainBattleMode {
|
||||
match value {
|
||||
BattleMode::Fight => DomainBattleMode::Fight,
|
||||
BattleMode::Spar => DomainBattleMode::Spar,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_battle_status(value: BattleStatus) -> DomainBattleStatus {
|
||||
match value {
|
||||
BattleStatus::Ongoing => DomainBattleStatus::Ongoing,
|
||||
BattleStatus::Resolved => DomainBattleStatus::Resolved,
|
||||
BattleStatus::Aborted => DomainBattleStatus::Aborted,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_combat_outcome(value: CombatOutcome) -> DomainCombatOutcome {
|
||||
match value {
|
||||
CombatOutcome::Ongoing => DomainCombatOutcome::Ongoing,
|
||||
CombatOutcome::Victory => DomainCombatOutcome::Victory,
|
||||
CombatOutcome::SparComplete => DomainCombatOutcome::SparComplete,
|
||||
CombatOutcome::Escaped => DomainCombatOutcome::Escaped,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ResolveCombatActionRecord {
|
||||
pub battle_state: BattleStateRecord,
|
||||
pub damage_dealt: i32,
|
||||
pub damage_taken: i32,
|
||||
pub outcome: String,
|
||||
}
|
||||
|
||||
pub(crate) fn build_resolve_combat_action_record(
|
||||
result: DomainResolveCombatActionResult,
|
||||
) -> ResolveCombatActionRecord {
|
||||
ResolveCombatActionRecord {
|
||||
battle_state: build_battle_state_record(result.snapshot),
|
||||
damage_dealt: result.damage_dealt,
|
||||
damage_taken: result.damage_taken,
|
||||
outcome: result.outcome.as_str().to_string(),
|
||||
}
|
||||
}
|
||||
706
server-rs/crates/spacetime-client/src/mapper/common.rs
Normal file
706
server-rs/crates/spacetime-client/src/mapper/common.rs
Normal file
@@ -0,0 +1,706 @@
|
||||
use super::*;
|
||||
|
||||
impl From<CustomWorldPublishWorldRecordInput> for CustomWorldPublishWorldInput {
|
||||
fn from(input: CustomWorldPublishWorldRecordInput) -> Self {
|
||||
Self {
|
||||
session_id: input.session_id,
|
||||
profile_id: input.profile_id,
|
||||
owner_user_id: input.owner_user_id,
|
||||
public_work_code: input.public_work_code,
|
||||
author_public_user_code: input.author_public_user_code,
|
||||
draft_profile_json: input.draft_profile_json,
|
||||
legacy_result_profile_json: input.legacy_result_profile_json,
|
||||
setting_text: input.setting_text,
|
||||
author_display_name: input.author_display_name,
|
||||
published_at_micros: input.published_at_micros,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn empty_string_to_none(value: String) -> Option<String> {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn i64_to_u64_ms(value: i64) -> u64 {
|
||||
value.max(0) as u64
|
||||
}
|
||||
|
||||
pub(crate) fn parse_optional_json_value(
|
||||
value: Option<&str>,
|
||||
fallback: serde_json::Value,
|
||||
label: &str,
|
||||
) -> Result<serde_json::Value, SpacetimeClientError> {
|
||||
match value.map(str::trim).filter(|value| !value.is_empty()) {
|
||||
Some(value) => parse_json_value(value, label),
|
||||
None => Ok(fallback),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CustomWorldLibraryMutationRecord {
|
||||
pub entry: CustomWorldLibraryEntryRecord,
|
||||
pub gallery_entry: Option<CustomWorldGalleryEntryRecord>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CustomWorldPublishWorldRecord {
|
||||
pub compiled_record: CustomWorldPublishedProfileCompileRecord,
|
||||
pub entry: CustomWorldLibraryEntryRecord,
|
||||
pub gallery_entry: Option<CustomWorldGalleryEntryRecord>,
|
||||
pub session_stage: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CustomWorldAgentMessageRecord {
|
||||
pub message_id: String,
|
||||
pub role: String,
|
||||
pub kind: String,
|
||||
pub text: String,
|
||||
pub created_at: String,
|
||||
pub related_operation_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CustomWorldAgentOperationRecord {
|
||||
pub operation_id: String,
|
||||
pub operation_type: String,
|
||||
pub status: String,
|
||||
pub phase_label: String,
|
||||
pub phase_detail: String,
|
||||
pub progress: u32,
|
||||
pub error_message: Option<String>,
|
||||
pub started_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CustomWorldAgentOperationProgressRecordInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub operation_id: String,
|
||||
// SpacetimeDB 模块侧使用枚举存储操作类型,这里保留字符串给 API 层做轻量传参。
|
||||
pub operation_type: String,
|
||||
pub operation_status: String,
|
||||
pub phase_label: String,
|
||||
pub phase_detail: String,
|
||||
pub operation_progress: u32,
|
||||
pub error_message: Option<String>,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CustomWorldSupportedActionRecord {
|
||||
pub action: String,
|
||||
pub enabled: bool,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CustomWorldCheckpointRecord {
|
||||
pub checkpoint_id: String,
|
||||
pub created_at: String,
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
// 兼容并行 custom world facade 中仍在使用的旧命名,避免本轮 module-npc 收口被无关改动阻塞。
|
||||
|
||||
pub type CustomWorldAgentCheckpointRecord = CustomWorldCheckpointRecord;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CustomWorldResultPreviewBlockerRecord {
|
||||
pub id: String,
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CustomWorldPublishGateRecord {
|
||||
pub profile_id: String,
|
||||
pub blockers: Vec<CustomWorldResultPreviewBlockerRecord>,
|
||||
pub blocker_count: u32,
|
||||
pub publish_ready: bool,
|
||||
pub can_enter_world: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CustomWorldDraftCardDetailSectionRecord {
|
||||
pub section_id: String,
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CustomWorldProfileRemixRecordInput {
|
||||
pub source_owner_user_id: String,
|
||||
pub source_profile_id: String,
|
||||
pub target_owner_user_id: String,
|
||||
pub target_profile_id: String,
|
||||
pub author_display_name: String,
|
||||
pub remixed_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CustomWorldProfilePlayReportRecordInput {
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub played_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CustomWorldProfileLikeReportRecordInput {
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub user_id: String,
|
||||
pub liked_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CustomWorldPublishWorldRecordInput {
|
||||
pub session_id: String,
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub public_work_code: Option<String>,
|
||||
pub author_public_user_code: String,
|
||||
pub draft_profile_json: String,
|
||||
pub legacy_result_profile_json: Option<String>,
|
||||
pub setting_text: String,
|
||||
pub author_display_name: String,
|
||||
pub published_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CustomWorldAgentMessageSubmitRecordInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub user_message_id: String,
|
||||
pub user_message_text: String,
|
||||
pub operation_id: String,
|
||||
pub submitted_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CustomWorldAgentActionExecuteRecordInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub operation_id: String,
|
||||
pub action: String,
|
||||
pub payload_json: Option<String>,
|
||||
pub submitted_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CustomWorldAgentActionExecuteRecord {
|
||||
pub operation: CustomWorldAgentOperationRecord,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BigFishPlayReportRecordInput {
|
||||
pub session_id: String,
|
||||
pub user_id: String,
|
||||
pub elapsed_ms: u64,
|
||||
pub reported_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BigFishRunStartRecordInput {
|
||||
pub run_id: String,
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub started_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BigFishInputSubmitRecordInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub submitted_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BigFishLikeReportRecordInput {
|
||||
pub session_id: String,
|
||||
pub user_id: String,
|
||||
pub liked_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BigFishWorkRemixRecordInput {
|
||||
pub source_session_id: String,
|
||||
pub target_session_id: String,
|
||||
pub target_owner_user_id: String,
|
||||
pub welcome_message_id: String,
|
||||
pub remixed_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleAgentSessionCreateRecordInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub seed_text: String,
|
||||
pub welcome_message_id: String,
|
||||
pub welcome_message_text: String,
|
||||
pub config_json: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleAgentMessageSubmitRecordInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub user_message_id: String,
|
||||
pub user_message_text: String,
|
||||
pub submitted_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleAgentMessageFinalizeRecordInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub assistant_message_id: Option<String>,
|
||||
pub assistant_reply_text: Option<String>,
|
||||
pub config_json: Option<String>,
|
||||
pub progress_percent: u32,
|
||||
pub stage: String,
|
||||
pub updated_at_micros: i64,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleCompileDraftRecordInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub author_display_name: String,
|
||||
pub game_name: Option<String>,
|
||||
pub summary_text: Option<String>,
|
||||
pub tags_json: Option<String>,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub compiled_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleWorkUpdateRecordInput {
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub game_name: String,
|
||||
pub theme_text: String,
|
||||
pub twist_rule: String,
|
||||
pub summary_text: String,
|
||||
pub tags_json: String,
|
||||
pub cover_image_src: String,
|
||||
pub background_prompt: String,
|
||||
pub background_image_src: String,
|
||||
pub shape_options_json: String,
|
||||
pub hole_options_json: String,
|
||||
pub shape_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleRunStartRecordInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub started_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleRunDropRecordInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub hole_id: String,
|
||||
pub client_snapshot_version: u64,
|
||||
pub client_event_id: String,
|
||||
pub dropped_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleRunStopRecordInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub stopped_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleRunRestartRecordInput {
|
||||
pub source_run_id: String,
|
||||
pub next_run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub restarted_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleRunTimeUpRecordInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub finished_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct VisualNovelAgentMessageSubmitRecordInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub user_message_id: String,
|
||||
pub user_message_text: String,
|
||||
pub submitted_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct VisualNovelAgentMessageFinalizeRecordInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub assistant_message_id: Option<String>,
|
||||
pub assistant_reply_text: Option<String>,
|
||||
pub draft_json: Option<String>,
|
||||
pub pending_action_json: Option<String>,
|
||||
pub status: String,
|
||||
pub progress_percent: u32,
|
||||
pub updated_at_micros: i64,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct VisualNovelWorkCompileRecordInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub work_id: Option<String>,
|
||||
pub author_display_name: String,
|
||||
pub work_title: Option<String>,
|
||||
pub work_description: Option<String>,
|
||||
pub tags_json: Option<String>,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub compiled_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct VisualNovelRunStartRecordInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub mode: String,
|
||||
pub snapshot_json: Option<String>,
|
||||
pub started_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct VisualNovelRunSnapshotRecordInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub status: String,
|
||||
pub current_scene_id: Option<String>,
|
||||
pub current_phase_id: Option<String>,
|
||||
pub visible_character_ids_json: String,
|
||||
pub flags_json: String,
|
||||
pub metrics_json: String,
|
||||
pub available_choices_json: String,
|
||||
pub text_mode_enabled: bool,
|
||||
pub snapshot_json: Option<String>,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct VisualNovelHistoryEntryRecordInput {
|
||||
pub entry_id: String,
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub turn_index: u32,
|
||||
pub source: String,
|
||||
pub action_text: Option<String>,
|
||||
pub steps_json: String,
|
||||
pub snapshot_before_hash: Option<String>,
|
||||
pub snapshot_after_hash: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct VisualNovelAgentMessageRecord {
|
||||
pub message_id: String,
|
||||
pub session_id: String,
|
||||
pub role: String,
|
||||
pub kind: String,
|
||||
pub text: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct VisualNovelHistoryEntryRecord {
|
||||
pub entry_id: String,
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub turn_index: u32,
|
||||
pub source: String,
|
||||
pub action_text: Option<String>,
|
||||
pub steps: serde_json::Value,
|
||||
pub snapshot_before_hash: Option<String>,
|
||||
pub snapshot_after_hash: Option<String>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct VisualNovelRunRecord {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub mode: String,
|
||||
pub status: String,
|
||||
pub current_scene_id: Option<String>,
|
||||
pub current_phase_id: Option<String>,
|
||||
pub visible_character_ids: Vec<String>,
|
||||
pub flags: serde_json::Value,
|
||||
pub metrics: serde_json::Value,
|
||||
pub history: Vec<VisualNovelHistoryEntryRecord>,
|
||||
pub available_choices: serde_json::Value,
|
||||
pub text_mode_enabled: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleAnchorItemRecord {
|
||||
pub key: String,
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleAnchorPackRecord {
|
||||
pub theme: SquareHoleAnchorItemRecord,
|
||||
pub twist_rule: SquareHoleAnchorItemRecord,
|
||||
pub shape_count: SquareHoleAnchorItemRecord,
|
||||
pub difficulty: SquareHoleAnchorItemRecord,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleCreatorConfigRecord {
|
||||
pub theme_text: String,
|
||||
pub twist_rule: String,
|
||||
pub shape_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub shape_options: Vec<SquareHoleShapeOptionRecord>,
|
||||
pub hole_options: Vec<SquareHoleHoleOptionRecord>,
|
||||
pub background_prompt: String,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub background_image_src: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleShapeOptionRecord {
|
||||
pub option_id: String,
|
||||
pub shape_kind: String,
|
||||
pub label: String,
|
||||
pub target_hole_id: String,
|
||||
pub image_prompt: String,
|
||||
pub image_src: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleHoleOptionRecord {
|
||||
pub hole_id: String,
|
||||
pub hole_kind: String,
|
||||
pub label: String,
|
||||
pub image_prompt: String,
|
||||
pub image_src: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleResultDraftRecord {
|
||||
pub profile_id: String,
|
||||
pub game_name: String,
|
||||
pub theme_text: String,
|
||||
pub twist_rule: String,
|
||||
pub summary: String,
|
||||
pub tags: Vec<String>,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub background_prompt: String,
|
||||
pub background_image_src: Option<String>,
|
||||
pub shape_options: Vec<SquareHoleShapeOptionRecord>,
|
||||
pub hole_options: Vec<SquareHoleHoleOptionRecord>,
|
||||
pub shape_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub publish_ready: bool,
|
||||
pub blockers: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleAgentMessageRecord {
|
||||
pub id: String,
|
||||
pub role: String,
|
||||
pub kind: String,
|
||||
pub text: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleAgentSessionRecord {
|
||||
pub session_id: String,
|
||||
pub current_turn: u32,
|
||||
pub progress_percent: u32,
|
||||
pub stage: String,
|
||||
pub anchor_pack: SquareHoleAnchorPackRecord,
|
||||
pub config: SquareHoleCreatorConfigRecord,
|
||||
pub draft: Option<SquareHoleResultDraftRecord>,
|
||||
pub messages: Vec<SquareHoleAgentMessageRecord>,
|
||||
pub last_assistant_reply: Option<String>,
|
||||
pub published_profile_id: Option<String>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleWorkProfileRecord {
|
||||
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 game_name: String,
|
||||
pub theme_text: String,
|
||||
pub twist_rule: String,
|
||||
pub summary: String,
|
||||
pub tags: Vec<String>,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub background_prompt: String,
|
||||
pub background_image_src: Option<String>,
|
||||
pub shape_options: Vec<SquareHoleShapeOptionRecord>,
|
||||
pub hole_options: Vec<SquareHoleHoleOptionRecord>,
|
||||
pub shape_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub publication_status: String,
|
||||
pub play_count: u32,
|
||||
pub updated_at: String,
|
||||
pub published_at: Option<String>,
|
||||
pub publish_ready: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SquareHoleShapeSnapshotRecord {
|
||||
pub shape_id: String,
|
||||
pub shape_kind: String,
|
||||
pub label: String,
|
||||
pub target_hole_id: String,
|
||||
pub color: String,
|
||||
pub image_src: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SquareHoleHoleSnapshotRecord {
|
||||
pub hole_id: String,
|
||||
pub hole_kind: String,
|
||||
pub label: String,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub image_src: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BigFishSessionCreateRecordInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub seed_text: String,
|
||||
pub welcome_message_id: String,
|
||||
pub welcome_message_text: String,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BigFishMessageSubmitRecordInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub user_message_id: String,
|
||||
pub user_message_text: String,
|
||||
pub assistant_message_id: String,
|
||||
pub submitted_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BigFishMessageFinalizeRecordInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub assistant_message_id: Option<String>,
|
||||
pub assistant_reply_text: Option<String>,
|
||||
pub stage: String,
|
||||
pub progress_percent: u32,
|
||||
pub anchor_pack_json: String,
|
||||
pub error_message: Option<String>,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BigFishDraftCompileRecordInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub draft_json: Option<String>,
|
||||
pub compiled_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BigFishAnchorItemRecord {
|
||||
pub key: String,
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BigFishAnchorPackRecord {
|
||||
pub gameplay_promise: BigFishAnchorItemRecord,
|
||||
pub ecology_visual_theme: BigFishAnchorItemRecord,
|
||||
pub growth_ladder: BigFishAnchorItemRecord,
|
||||
pub risk_tempo: BigFishAnchorItemRecord,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BigFishLevelBlueprintRecord {
|
||||
pub level: u32,
|
||||
pub name: String,
|
||||
pub one_line_fantasy: String,
|
||||
pub text_description: String,
|
||||
pub silhouette_direction: String,
|
||||
pub size_ratio: f32,
|
||||
pub visual_description: String,
|
||||
pub visual_prompt_seed: String,
|
||||
pub idle_motion_description: String,
|
||||
pub move_motion_description: String,
|
||||
pub motion_prompt_seed: String,
|
||||
pub merge_source_level: Option<u32>,
|
||||
pub prey_window: Vec<u32>,
|
||||
pub threat_window: Vec<u32>,
|
||||
pub is_final_level: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BigFishBackgroundBlueprintRecord {
|
||||
pub theme: String,
|
||||
pub color_mood: String,
|
||||
pub foreground_hints: String,
|
||||
pub midground_composition: String,
|
||||
pub background_depth: String,
|
||||
pub safe_play_area_hint: String,
|
||||
pub spawn_edge_hint: String,
|
||||
pub background_prompt_seed: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BigFishAgentMessageRecord {
|
||||
pub message_id: String,
|
||||
pub role: String,
|
||||
pub kind: String,
|
||||
pub text: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BigFishVector2Record {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
957
server-rs/crates/spacetime-client/src/mapper/custom_world.rs
Normal file
957
server-rs/crates/spacetime-client/src/mapper/custom_world.rs
Normal file
@@ -0,0 +1,957 @@
|
||||
use super::*;
|
||||
|
||||
impl From<CustomWorldProfileUpsertRecordInput> for CustomWorldProfileUpsertInput {
|
||||
fn from(input: CustomWorldProfileUpsertRecordInput) -> Self {
|
||||
Self {
|
||||
profile_id: input.profile_id,
|
||||
owner_user_id: input.owner_user_id,
|
||||
public_work_code: input.public_work_code,
|
||||
author_public_user_code: input.author_public_user_code,
|
||||
source_agent_session_id: input.source_agent_session_id,
|
||||
world_name: input.world_name,
|
||||
subtitle: input.subtitle,
|
||||
summary_text: input.summary_text,
|
||||
theme_mode: map_custom_world_theme_mode(input.theme_mode),
|
||||
cover_image_src: input.cover_image_src,
|
||||
profile_payload_json: input.profile_payload_json,
|
||||
playable_npc_count: input.playable_npc_count,
|
||||
landmark_count: input.landmark_count,
|
||||
author_display_name: input.author_display_name,
|
||||
updated_at_micros: input.updated_at_micros,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_profile_list_result(
|
||||
result: CustomWorldProfileListResult,
|
||||
) -> Result<Vec<CustomWorldLibraryEntryRecord>, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
result
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(map_custom_world_library_entry_from_profile_snapshot)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_library_detail_result(
|
||||
result: CustomWorldLibraryMutationResult,
|
||||
) -> Result<CustomWorldLibraryMutationRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let entry = result
|
||||
.entry
|
||||
.ok_or_else(|| SpacetimeClientError::Procedure("custom_world_profile 不存在".to_string()))
|
||||
.and_then(map_custom_world_library_entry_from_profile_snapshot)?;
|
||||
let gallery_entry = result
|
||||
.gallery_entry
|
||||
.map(map_custom_world_gallery_entry_snapshot)
|
||||
.transpose()?;
|
||||
|
||||
Ok(CustomWorldLibraryMutationRecord {
|
||||
entry,
|
||||
gallery_entry,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_gallery_list_result(
|
||||
result: CustomWorldGalleryListResult,
|
||||
) -> Result<Vec<CustomWorldGalleryEntryRecord>, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
Ok(result
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(map_custom_world_gallery_entry_snapshot)
|
||||
.collect::<Result<Vec<_>, _>>()?)
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_library_mutation_result(
|
||||
result: CustomWorldLibraryMutationResult,
|
||||
) -> Result<CustomWorldLibraryMutationRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let entry = result
|
||||
.entry
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("custom world entry"))
|
||||
.and_then(map_custom_world_library_entry_from_profile_snapshot)?;
|
||||
let gallery_entry = result
|
||||
.gallery_entry
|
||||
.map(map_custom_world_gallery_entry_snapshot)
|
||||
.transpose()?;
|
||||
|
||||
Ok(CustomWorldLibraryMutationRecord {
|
||||
entry,
|
||||
gallery_entry,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_publish_world_result(
|
||||
result: CustomWorldPublishWorldResult,
|
||||
) -> Result<CustomWorldPublishWorldRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let compiled_record = result
|
||||
.compiled_record
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("published profile compile 快照"))
|
||||
.and_then(map_custom_world_published_profile_compile_snapshot)?;
|
||||
let entry = result
|
||||
.entry
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("custom world entry"))
|
||||
.and_then(map_custom_world_library_entry_from_profile_snapshot)?;
|
||||
let gallery_entry = result
|
||||
.gallery_entry
|
||||
.map(map_custom_world_gallery_entry_snapshot)
|
||||
.transpose()?;
|
||||
let session_stage = result
|
||||
.session_stage
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("session stage"))
|
||||
.map(map_rpg_agent_stage)?;
|
||||
|
||||
Ok(CustomWorldPublishWorldRecord {
|
||||
compiled_record,
|
||||
entry,
|
||||
gallery_entry,
|
||||
session_stage,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_agent_session_procedure_result(
|
||||
result: CustomWorldAgentSessionProcedureResult,
|
||||
) -> Result<CustomWorldAgentSessionRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let session = result
|
||||
.session
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("custom world agent session 快照"))?;
|
||||
|
||||
map_custom_world_agent_session_snapshot(session)
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_agent_operation_procedure_result(
|
||||
result: CustomWorldAgentOperationProcedureResult,
|
||||
) -> Result<CustomWorldAgentOperationRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let operation = result.operation.ok_or_else(|| {
|
||||
SpacetimeClientError::missing_snapshot("custom world agent operation 快照")
|
||||
})?;
|
||||
|
||||
Ok(map_custom_world_agent_operation_snapshot(operation))
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_works_list_result(
|
||||
result: CustomWorldWorksListResult,
|
||||
) -> Result<Vec<CustomWorldWorkSummaryRecord>, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
result
|
||||
.items
|
||||
.into_iter()
|
||||
.map(map_custom_world_work_summary_snapshot)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_draft_card_detail_result(
|
||||
result: CustomWorldDraftCardDetailResult,
|
||||
) -> Result<CustomWorldDraftCardDetailRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let card = result
|
||||
.card
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("custom world card detail 快照"))?;
|
||||
|
||||
map_custom_world_draft_card_detail_snapshot(card)
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_agent_action_execute_result(
|
||||
result: CustomWorldAgentActionExecuteResult,
|
||||
) -> Result<CustomWorldAgentActionExecuteRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let operation = result.operation.ok_or_else(|| {
|
||||
SpacetimeClientError::missing_snapshot("custom world action operation 快照")
|
||||
})?;
|
||||
|
||||
Ok(CustomWorldAgentActionExecuteRecord {
|
||||
operation: map_custom_world_agent_operation_snapshot(operation),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_library_entry_from_profile_snapshot(
|
||||
snapshot: CustomWorldProfileSnapshot,
|
||||
) -> Result<CustomWorldLibraryEntryRecord, SpacetimeClientError> {
|
||||
let profile = serde_json::from_str::<serde_json::Value>(&snapshot.profile_payload_json)
|
||||
.map_err(|error| {
|
||||
SpacetimeClientError::Runtime(format!(
|
||||
"custom world profile payload JSON 非法: {error}"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(CustomWorldLibraryEntryRecord {
|
||||
owner_user_id: snapshot.owner_user_id,
|
||||
profile_id: snapshot.profile_id,
|
||||
public_work_code: snapshot.public_work_code,
|
||||
author_public_user_code: snapshot.author_public_user_code,
|
||||
profile,
|
||||
visibility: map_custom_world_publication_status(snapshot.publication_status).to_string(),
|
||||
published_at: snapshot.published_at_micros.map(format_timestamp_micros),
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
author_display_name: snapshot.author_display_name,
|
||||
world_name: snapshot.world_name,
|
||||
subtitle: snapshot.subtitle,
|
||||
summary_text: snapshot.summary_text,
|
||||
cover_image_src: snapshot.cover_image_src,
|
||||
theme_mode: format_custom_world_theme_mode(map_custom_world_theme_mode_back(
|
||||
snapshot.theme_mode,
|
||||
))
|
||||
.to_string(),
|
||||
playable_npc_count: snapshot.playable_npc_count,
|
||||
landmark_count: snapshot.landmark_count,
|
||||
play_count: snapshot.play_count,
|
||||
remix_count: snapshot.remix_count,
|
||||
like_count: snapshot.like_count,
|
||||
recent_play_count_7d: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_gallery_entry_snapshot(
|
||||
snapshot: CustomWorldGalleryEntrySnapshot,
|
||||
) -> Result<CustomWorldGalleryEntryRecord, SpacetimeClientError> {
|
||||
Ok(CustomWorldGalleryEntryRecord {
|
||||
owner_user_id: snapshot.owner_user_id,
|
||||
profile_id: snapshot.profile_id,
|
||||
public_work_code: snapshot.public_work_code,
|
||||
author_public_user_code: snapshot.author_public_user_code,
|
||||
visibility: "published".to_string(),
|
||||
published_at: Some(format_timestamp_micros(snapshot.published_at_micros)),
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
author_display_name: snapshot.author_display_name,
|
||||
world_name: snapshot.world_name,
|
||||
subtitle: snapshot.subtitle,
|
||||
summary_text: snapshot.summary_text,
|
||||
cover_image_src: snapshot.cover_image_src,
|
||||
theme_mode: format_custom_world_theme_mode(map_custom_world_theme_mode_back(
|
||||
snapshot.theme_mode,
|
||||
))
|
||||
.to_string(),
|
||||
playable_npc_count: snapshot.playable_npc_count,
|
||||
landmark_count: snapshot.landmark_count,
|
||||
play_count: snapshot.play_count,
|
||||
remix_count: snapshot.remix_count,
|
||||
like_count: snapshot.like_count,
|
||||
recent_play_count_7d: snapshot.recent_play_count_7_d,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_gallery_entry_row(
|
||||
row: CustomWorldGalleryEntry,
|
||||
recent_play_count_7d: u32,
|
||||
) -> CustomWorldGalleryEntryRecord {
|
||||
CustomWorldGalleryEntryRecord {
|
||||
owner_user_id: row.owner_user_id,
|
||||
profile_id: row.profile_id,
|
||||
public_work_code: row.public_work_code,
|
||||
author_public_user_code: row.author_public_user_code,
|
||||
visibility: "published".to_string(),
|
||||
published_at: Some(format_timestamp_micros(
|
||||
row.published_at.to_micros_since_unix_epoch(),
|
||||
)),
|
||||
updated_at: format_timestamp_micros(row.updated_at.to_micros_since_unix_epoch()),
|
||||
author_display_name: row.author_display_name,
|
||||
world_name: row.world_name,
|
||||
subtitle: row.subtitle,
|
||||
summary_text: row.summary_text,
|
||||
cover_image_src: row.cover_image_src,
|
||||
theme_mode: format_custom_world_theme_mode(map_custom_world_theme_mode_back(
|
||||
row.theme_mode,
|
||||
))
|
||||
.to_string(),
|
||||
playable_npc_count: row.playable_npc_count,
|
||||
landmark_count: row.landmark_count,
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
recent_play_count_7d,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_published_profile_compile_snapshot(
|
||||
snapshot: CustomWorldPublishedProfileCompileSnapshot,
|
||||
) -> Result<CustomWorldPublishedProfileCompileRecord, SpacetimeClientError> {
|
||||
let compiled_profile =
|
||||
serde_json::from_str::<serde_json::Value>(&snapshot.compiled_profile_payload_json)
|
||||
.map_err(|error| {
|
||||
SpacetimeClientError::Runtime(format!(
|
||||
"published profile compile JSON 非法: {error}"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(CustomWorldPublishedProfileCompileRecord {
|
||||
profile_id: snapshot.profile_id,
|
||||
owner_user_id: snapshot.owner_user_id,
|
||||
world_name: snapshot.world_name,
|
||||
subtitle: snapshot.subtitle,
|
||||
summary_text: snapshot.summary_text,
|
||||
theme_mode: format_custom_world_theme_mode(map_custom_world_theme_mode_back(
|
||||
snapshot.theme_mode,
|
||||
))
|
||||
.to_string(),
|
||||
cover_image_src: snapshot.cover_image_src,
|
||||
playable_npc_count: snapshot.playable_npc_count,
|
||||
landmark_count: snapshot.landmark_count,
|
||||
author_display_name: snapshot.author_display_name,
|
||||
compiled_profile: compiled_profile,
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_work_summary_snapshot(
|
||||
snapshot: CustomWorldWorkSummarySnapshot,
|
||||
) -> Result<CustomWorldWorkSummaryRecord, SpacetimeClientError> {
|
||||
Ok(CustomWorldWorkSummaryRecord {
|
||||
work_id: snapshot.work_id,
|
||||
source_type: snapshot.source_type,
|
||||
status: snapshot.status,
|
||||
title: snapshot.title,
|
||||
subtitle: snapshot.subtitle,
|
||||
summary: snapshot.summary,
|
||||
cover_image_src: snapshot.cover_image_src,
|
||||
cover_render_mode: snapshot.cover_render_mode,
|
||||
cover_character_image_srcs: parse_json_string_array(
|
||||
&snapshot.cover_character_image_srcs_json,
|
||||
"custom world work cover_character_image_srcs_json",
|
||||
)?,
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
published_at: snapshot.published_at_micros.map(format_timestamp_micros),
|
||||
stage: snapshot.stage.map(map_rpg_agent_stage),
|
||||
stage_label: snapshot.stage_label,
|
||||
playable_npc_count: snapshot.playable_npc_count,
|
||||
landmark_count: snapshot.landmark_count,
|
||||
role_visual_ready_count: snapshot.role_visual_ready_count,
|
||||
role_animation_ready_count: snapshot.role_animation_ready_count,
|
||||
role_asset_summary_label: snapshot.role_asset_summary_label,
|
||||
session_id: snapshot.session_id,
|
||||
profile_id: snapshot.profile_id,
|
||||
can_resume: snapshot.can_resume,
|
||||
can_enter_world: snapshot.can_enter_world,
|
||||
blocker_count: snapshot.blocker_count,
|
||||
publish_ready: snapshot.publish_ready,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_agent_session_snapshot(
|
||||
snapshot: CustomWorldAgentSessionSnapshot,
|
||||
) -> Result<CustomWorldAgentSessionRecord, SpacetimeClientError> {
|
||||
let anchor_content = parse_json_value(
|
||||
&snapshot.anchor_content_json,
|
||||
"custom world agent anchor_content_json",
|
||||
)?;
|
||||
let creator_intent = parse_optional_json_value(
|
||||
snapshot.creator_intent_json.as_deref(),
|
||||
serde_json::json!({}),
|
||||
"custom world agent creator_intent_json",
|
||||
)?;
|
||||
let creator_intent_readiness = parse_json_value(
|
||||
&snapshot.creator_intent_readiness_json,
|
||||
"custom world agent creator_intent_readiness_json",
|
||||
)?;
|
||||
let anchor_pack = parse_optional_json_value(
|
||||
snapshot.anchor_pack_json.as_deref(),
|
||||
serde_json::json!({}),
|
||||
"custom world agent anchor_pack_json",
|
||||
)?;
|
||||
let lock_state = parse_optional_json_value(
|
||||
snapshot.lock_state_json.as_deref(),
|
||||
serde_json::json!({}),
|
||||
"custom world agent lock_state_json",
|
||||
)?;
|
||||
let draft_profile = parse_optional_json_value(
|
||||
snapshot.draft_profile_json.as_deref(),
|
||||
serde_json::json!({}),
|
||||
"custom world agent draft_profile_json",
|
||||
)?;
|
||||
let pending_clarifications = parse_json_array(
|
||||
&snapshot.pending_clarifications_json,
|
||||
"custom world agent pending_clarifications_json",
|
||||
)?;
|
||||
let suggested_actions = parse_json_array(
|
||||
&snapshot.suggested_actions_json,
|
||||
"custom world agent suggested_actions_json",
|
||||
)?;
|
||||
let recommended_replies = parse_json_string_array(
|
||||
&snapshot.recommended_replies_json,
|
||||
"custom world agent recommended_replies_json",
|
||||
)?;
|
||||
let quality_findings = parse_json_array(
|
||||
&snapshot.quality_findings_json,
|
||||
"custom world agent quality_findings_json",
|
||||
)?;
|
||||
let asset_coverage = parse_json_value(
|
||||
&snapshot.asset_coverage_json,
|
||||
"custom world agent asset_coverage_json",
|
||||
)?;
|
||||
let checkpoints_json = parse_json_array(
|
||||
&snapshot.checkpoints_json,
|
||||
"custom world agent checkpoints_json",
|
||||
)?;
|
||||
let checkpoints = checkpoints_json
|
||||
.into_iter()
|
||||
.map(map_custom_world_checkpoint_record)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let supported_actions = parse_supported_actions_json(&snapshot.supported_actions_json)?;
|
||||
let publish_gate = snapshot
|
||||
.publish_gate_json
|
||||
.as_deref()
|
||||
.map(parse_custom_world_publish_gate_record)
|
||||
.transpose()?;
|
||||
|
||||
Ok(CustomWorldAgentSessionRecord {
|
||||
session_id: snapshot.session_id,
|
||||
seed_text: snapshot.seed_text,
|
||||
current_turn: snapshot.current_turn,
|
||||
anchor_content,
|
||||
progress_percent: snapshot.progress_percent,
|
||||
last_assistant_reply: snapshot.last_assistant_reply,
|
||||
stage: map_rpg_agent_stage(snapshot.stage),
|
||||
focus_card_id: snapshot.focus_card_id,
|
||||
creator_intent,
|
||||
creator_intent_readiness,
|
||||
anchor_pack,
|
||||
lock_state,
|
||||
draft_profile,
|
||||
messages: snapshot
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(map_custom_world_agent_message_snapshot)
|
||||
.collect(),
|
||||
draft_cards: snapshot
|
||||
.draft_cards
|
||||
.into_iter()
|
||||
.map(map_custom_world_draft_card_snapshot)
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
pending_clarifications,
|
||||
suggested_actions,
|
||||
recommended_replies,
|
||||
quality_findings,
|
||||
asset_coverage,
|
||||
checkpoints,
|
||||
supported_actions,
|
||||
publish_gate,
|
||||
result_preview: snapshot
|
||||
.result_preview_json
|
||||
.as_deref()
|
||||
.map(|value| parse_json_value(value, "custom world agent result_preview_json"))
|
||||
.transpose()?,
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_agent_message_snapshot(
|
||||
snapshot: CustomWorldAgentMessageSnapshot,
|
||||
) -> CustomWorldAgentMessageRecord {
|
||||
CustomWorldAgentMessageRecord {
|
||||
message_id: snapshot.message_id,
|
||||
role: format_rpg_agent_message_role(snapshot.role).to_string(),
|
||||
kind: format_rpg_agent_message_kind(snapshot.kind).to_string(),
|
||||
text: snapshot.text,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
related_operation_id: snapshot.related_operation_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_agent_operation_snapshot(
|
||||
snapshot: CustomWorldAgentOperationSnapshot,
|
||||
) -> CustomWorldAgentOperationRecord {
|
||||
CustomWorldAgentOperationRecord {
|
||||
operation_id: snapshot.operation_id,
|
||||
operation_type: format_rpg_agent_operation_type(snapshot.operation_type).to_string(),
|
||||
status: format_rpg_agent_operation_status(snapshot.status).to_string(),
|
||||
phase_label: snapshot.phase_label,
|
||||
phase_detail: snapshot.phase_detail,
|
||||
progress: snapshot.progress,
|
||||
error_message: snapshot.error_message,
|
||||
started_at_micros: snapshot.created_at_micros,
|
||||
updated_at_micros: snapshot.updated_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_draft_card_snapshot(
|
||||
snapshot: CustomWorldDraftCardSnapshot,
|
||||
) -> Result<CustomWorldDraftCardRecord, SpacetimeClientError> {
|
||||
Ok(CustomWorldDraftCardRecord {
|
||||
card_id: snapshot.card_id,
|
||||
kind: format_rpg_agent_draft_card_kind(snapshot.kind).to_string(),
|
||||
title: snapshot.title,
|
||||
subtitle: snapshot.subtitle,
|
||||
summary: snapshot.summary,
|
||||
status: format_rpg_agent_draft_card_status(snapshot.status).to_string(),
|
||||
linked_ids: parse_json_string_array(
|
||||
&snapshot.linked_ids_json,
|
||||
"custom world draft_card linked_ids_json",
|
||||
)?,
|
||||
warning_count: snapshot.warning_count,
|
||||
asset_status: snapshot
|
||||
.asset_status
|
||||
.map(format_custom_world_role_asset_status_back),
|
||||
asset_status_label: snapshot.asset_status_label,
|
||||
detail_payload: snapshot
|
||||
.detail_payload_json
|
||||
.as_deref()
|
||||
.map(|value| parse_json_value(value, "custom world draft_card detail_payload_json"))
|
||||
.transpose()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_draft_card_detail_snapshot(
|
||||
snapshot: CustomWorldDraftCardDetailSnapshot,
|
||||
) -> Result<CustomWorldDraftCardDetailRecord, SpacetimeClientError> {
|
||||
Ok(CustomWorldDraftCardDetailRecord {
|
||||
card_id: snapshot.card_id,
|
||||
kind: format_rpg_agent_draft_card_kind(snapshot.kind).to_string(),
|
||||
title: snapshot.title,
|
||||
sections: snapshot
|
||||
.sections
|
||||
.into_iter()
|
||||
.map(map_custom_world_draft_card_detail_section_snapshot)
|
||||
.collect(),
|
||||
linked_ids: parse_json_string_array(
|
||||
&snapshot.linked_ids_json,
|
||||
"custom world card detail linked_ids_json",
|
||||
)?,
|
||||
locked: snapshot.locked,
|
||||
editable: snapshot.editable,
|
||||
editable_section_ids: parse_json_string_array(
|
||||
&snapshot.editable_section_ids_json,
|
||||
"custom world card detail editable_section_ids_json",
|
||||
)?,
|
||||
warning_messages: parse_json_string_array(
|
||||
&snapshot.warning_messages_json,
|
||||
"custom world card detail warning_messages_json",
|
||||
)?,
|
||||
asset_status: snapshot
|
||||
.asset_status
|
||||
.map(format_custom_world_role_asset_status_back),
|
||||
asset_status_label: snapshot.asset_status_label,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_draft_card_detail_section_snapshot(
|
||||
snapshot: CustomWorldDraftCardDetailSectionSnapshot,
|
||||
) -> CustomWorldDraftCardDetailSectionRecord {
|
||||
CustomWorldDraftCardDetailSectionRecord {
|
||||
section_id: snapshot.section_id,
|
||||
label: snapshot.label,
|
||||
value: snapshot.value,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_theme_mode(
|
||||
value: DomainCustomWorldThemeMode,
|
||||
) -> CustomWorldThemeMode {
|
||||
match value {
|
||||
DomainCustomWorldThemeMode::Martial => CustomWorldThemeMode::Martial,
|
||||
DomainCustomWorldThemeMode::Arcane => CustomWorldThemeMode::Arcane,
|
||||
DomainCustomWorldThemeMode::Machina => CustomWorldThemeMode::Machina,
|
||||
DomainCustomWorldThemeMode::Tide => CustomWorldThemeMode::Tide,
|
||||
DomainCustomWorldThemeMode::Rift => CustomWorldThemeMode::Rift,
|
||||
DomainCustomWorldThemeMode::Mythic => CustomWorldThemeMode::Mythic,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_theme_mode_back(
|
||||
value: CustomWorldThemeMode,
|
||||
) -> DomainCustomWorldThemeMode {
|
||||
match value {
|
||||
CustomWorldThemeMode::Martial => DomainCustomWorldThemeMode::Martial,
|
||||
CustomWorldThemeMode::Arcane => DomainCustomWorldThemeMode::Arcane,
|
||||
CustomWorldThemeMode::Machina => DomainCustomWorldThemeMode::Machina,
|
||||
CustomWorldThemeMode::Tide => DomainCustomWorldThemeMode::Tide,
|
||||
CustomWorldThemeMode::Rift => DomainCustomWorldThemeMode::Rift,
|
||||
CustomWorldThemeMode::Mythic => DomainCustomWorldThemeMode::Mythic,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_publication_status(
|
||||
value: CustomWorldPublicationStatus,
|
||||
) -> &'static str {
|
||||
match value {
|
||||
CustomWorldPublicationStatus::Draft => "draft",
|
||||
CustomWorldPublicationStatus::Published => "published",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_rpg_agent_stage(value: crate::module_bindings::RpgAgentStage) -> String {
|
||||
match value {
|
||||
crate::module_bindings::RpgAgentStage::CollectingIntent => "collecting_intent",
|
||||
crate::module_bindings::RpgAgentStage::Clarifying => "clarifying",
|
||||
crate::module_bindings::RpgAgentStage::FoundationReview => "foundation_review",
|
||||
crate::module_bindings::RpgAgentStage::ObjectRefining => "object_refining",
|
||||
crate::module_bindings::RpgAgentStage::VisualRefining => "visual_refining",
|
||||
crate::module_bindings::RpgAgentStage::LongTailReview => "long_tail_review",
|
||||
crate::module_bindings::RpgAgentStage::ReadyToPublish => "ready_to_publish",
|
||||
crate::module_bindings::RpgAgentStage::Published => "published",
|
||||
crate::module_bindings::RpgAgentStage::Error => "error",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_rpg_agent_stage_record(
|
||||
value: &str,
|
||||
) -> Result<crate::module_bindings::RpgAgentStage, SpacetimeClientError> {
|
||||
match value.trim() {
|
||||
"collecting_intent" => Ok(crate::module_bindings::RpgAgentStage::CollectingIntent),
|
||||
"clarifying" => Ok(crate::module_bindings::RpgAgentStage::Clarifying),
|
||||
"foundation_review" => Ok(crate::module_bindings::RpgAgentStage::FoundationReview),
|
||||
"object_refining" => Ok(crate::module_bindings::RpgAgentStage::ObjectRefining),
|
||||
"visual_refining" => Ok(crate::module_bindings::RpgAgentStage::VisualRefining),
|
||||
"long_tail_review" => Ok(crate::module_bindings::RpgAgentStage::LongTailReview),
|
||||
"ready_to_publish" => Ok(crate::module_bindings::RpgAgentStage::ReadyToPublish),
|
||||
"published" => Ok(crate::module_bindings::RpgAgentStage::Published),
|
||||
"error" => Ok(crate::module_bindings::RpgAgentStage::Error),
|
||||
other => Err(SpacetimeClientError::Runtime(format!(
|
||||
"未知 rpg agent stage: {other}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_rpg_agent_message_role(
|
||||
value: crate::module_bindings::RpgAgentMessageRole,
|
||||
) -> &'static str {
|
||||
match value {
|
||||
crate::module_bindings::RpgAgentMessageRole::User => "user",
|
||||
crate::module_bindings::RpgAgentMessageRole::Assistant => "assistant",
|
||||
crate::module_bindings::RpgAgentMessageRole::System => "system",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_rpg_agent_message_kind(
|
||||
value: crate::module_bindings::RpgAgentMessageKind,
|
||||
) -> &'static str {
|
||||
match value {
|
||||
crate::module_bindings::RpgAgentMessageKind::Chat => "chat",
|
||||
crate::module_bindings::RpgAgentMessageKind::Clarification => "clarification",
|
||||
crate::module_bindings::RpgAgentMessageKind::Summary => "summary",
|
||||
crate::module_bindings::RpgAgentMessageKind::Checkpoint => "checkpoint",
|
||||
crate::module_bindings::RpgAgentMessageKind::Warning => "warning",
|
||||
crate::module_bindings::RpgAgentMessageKind::ActionResult => "action_result",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_rpg_agent_operation_type(
|
||||
value: crate::module_bindings::RpgAgentOperationType,
|
||||
) -> &'static str {
|
||||
match value {
|
||||
crate::module_bindings::RpgAgentOperationType::ProcessMessage => "process_message",
|
||||
crate::module_bindings::RpgAgentOperationType::DraftFoundation => "draft_foundation",
|
||||
crate::module_bindings::RpgAgentOperationType::UpdateDraftCard => "update_draft_card",
|
||||
crate::module_bindings::RpgAgentOperationType::SyncResultProfile => "sync_result_profile",
|
||||
crate::module_bindings::RpgAgentOperationType::GenerateCharacters => "generate_characters",
|
||||
crate::module_bindings::RpgAgentOperationType::GenerateLandmarks => "generate_landmarks",
|
||||
crate::module_bindings::RpgAgentOperationType::GenerateRoleAssets => "generate_role_assets",
|
||||
crate::module_bindings::RpgAgentOperationType::SyncRoleAssets => "sync_role_assets",
|
||||
crate::module_bindings::RpgAgentOperationType::GenerateSceneAssets => {
|
||||
"generate_scene_assets"
|
||||
}
|
||||
crate::module_bindings::RpgAgentOperationType::SyncSceneAssets => "sync_scene_assets",
|
||||
crate::module_bindings::RpgAgentOperationType::ExpandLongTail => "expand_long_tail",
|
||||
crate::module_bindings::RpgAgentOperationType::PublishWorld => "publish_world",
|
||||
crate::module_bindings::RpgAgentOperationType::RevertCheckpoint => "revert_checkpoint",
|
||||
crate::module_bindings::RpgAgentOperationType::DeleteCharacters => "delete_characters",
|
||||
crate::module_bindings::RpgAgentOperationType::DeleteLandmarks => "delete_landmarks",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_rpg_agent_operation_type_record(
|
||||
value: &str,
|
||||
) -> Result<crate::module_bindings::RpgAgentOperationType, SpacetimeClientError> {
|
||||
match value.trim() {
|
||||
"process_message" => Ok(crate::module_bindings::RpgAgentOperationType::ProcessMessage),
|
||||
"draft_foundation" => Ok(crate::module_bindings::RpgAgentOperationType::DraftFoundation),
|
||||
"update_draft_card" => Ok(crate::module_bindings::RpgAgentOperationType::UpdateDraftCard),
|
||||
"sync_result_profile" => {
|
||||
Ok(crate::module_bindings::RpgAgentOperationType::SyncResultProfile)
|
||||
}
|
||||
"generate_characters" => {
|
||||
Ok(crate::module_bindings::RpgAgentOperationType::GenerateCharacters)
|
||||
}
|
||||
"generate_landmarks" => {
|
||||
Ok(crate::module_bindings::RpgAgentOperationType::GenerateLandmarks)
|
||||
}
|
||||
"generate_role_assets" => {
|
||||
Ok(crate::module_bindings::RpgAgentOperationType::GenerateRoleAssets)
|
||||
}
|
||||
"sync_role_assets" => Ok(crate::module_bindings::RpgAgentOperationType::SyncRoleAssets),
|
||||
"generate_scene_assets" => {
|
||||
Ok(crate::module_bindings::RpgAgentOperationType::GenerateSceneAssets)
|
||||
}
|
||||
"sync_scene_assets" => Ok(crate::module_bindings::RpgAgentOperationType::SyncSceneAssets),
|
||||
"expand_long_tail" => Ok(crate::module_bindings::RpgAgentOperationType::ExpandLongTail),
|
||||
"publish_world" => Ok(crate::module_bindings::RpgAgentOperationType::PublishWorld),
|
||||
"revert_checkpoint" => Ok(crate::module_bindings::RpgAgentOperationType::RevertCheckpoint),
|
||||
"delete_characters" => Ok(crate::module_bindings::RpgAgentOperationType::DeleteCharacters),
|
||||
"delete_landmarks" => Ok(crate::module_bindings::RpgAgentOperationType::DeleteLandmarks),
|
||||
other => Err(SpacetimeClientError::Runtime(format!(
|
||||
"未知 rpg agent operation type: {other}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_rpg_agent_operation_status(
|
||||
value: crate::module_bindings::RpgAgentOperationStatus,
|
||||
) -> &'static str {
|
||||
match value {
|
||||
crate::module_bindings::RpgAgentOperationStatus::Queued => "queued",
|
||||
crate::module_bindings::RpgAgentOperationStatus::Running => "running",
|
||||
crate::module_bindings::RpgAgentOperationStatus::Completed => "completed",
|
||||
crate::module_bindings::RpgAgentOperationStatus::Failed => "failed",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_rpg_agent_operation_status_record(
|
||||
value: &str,
|
||||
) -> Result<crate::module_bindings::RpgAgentOperationStatus, SpacetimeClientError> {
|
||||
match value.trim() {
|
||||
"queued" => Ok(crate::module_bindings::RpgAgentOperationStatus::Queued),
|
||||
"running" => Ok(crate::module_bindings::RpgAgentOperationStatus::Running),
|
||||
"completed" => Ok(crate::module_bindings::RpgAgentOperationStatus::Completed),
|
||||
"failed" => Ok(crate::module_bindings::RpgAgentOperationStatus::Failed),
|
||||
other => Err(SpacetimeClientError::Runtime(format!(
|
||||
"未知 rpg agent operation status: {other}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_rpg_agent_draft_card_kind(
|
||||
value: crate::module_bindings::RpgAgentDraftCardKind,
|
||||
) -> &'static str {
|
||||
match value {
|
||||
crate::module_bindings::RpgAgentDraftCardKind::World => "world",
|
||||
crate::module_bindings::RpgAgentDraftCardKind::Camp => "camp",
|
||||
crate::module_bindings::RpgAgentDraftCardKind::Faction => "faction",
|
||||
crate::module_bindings::RpgAgentDraftCardKind::Character => "character",
|
||||
crate::module_bindings::RpgAgentDraftCardKind::Landmark => "landmark",
|
||||
crate::module_bindings::RpgAgentDraftCardKind::Thread => "thread",
|
||||
crate::module_bindings::RpgAgentDraftCardKind::Chapter => "chapter",
|
||||
crate::module_bindings::RpgAgentDraftCardKind::SceneChapter => "scene_chapter",
|
||||
crate::module_bindings::RpgAgentDraftCardKind::Carrier => "carrier",
|
||||
crate::module_bindings::RpgAgentDraftCardKind::SidequestSeed => "sidequest_seed",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_rpg_agent_draft_card_status(
|
||||
value: crate::module_bindings::RpgAgentDraftCardStatus,
|
||||
) -> &'static str {
|
||||
match value {
|
||||
crate::module_bindings::RpgAgentDraftCardStatus::Suggested => "suggested",
|
||||
crate::module_bindings::RpgAgentDraftCardStatus::Confirmed => "confirmed",
|
||||
crate::module_bindings::RpgAgentDraftCardStatus::Locked => "locked",
|
||||
crate::module_bindings::RpgAgentDraftCardStatus::Warning => "warning",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_custom_world_role_asset_status_back(
|
||||
value: crate::module_bindings::CustomWorldRoleAssetStatus,
|
||||
) -> String {
|
||||
match value {
|
||||
crate::module_bindings::CustomWorldRoleAssetStatus::Missing => "missing",
|
||||
crate::module_bindings::CustomWorldRoleAssetStatus::VisualReady => "visual_ready",
|
||||
crate::module_bindings::CustomWorldRoleAssetStatus::AnimationsReady => "animations_ready",
|
||||
crate::module_bindings::CustomWorldRoleAssetStatus::Complete => "complete",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub(crate) fn format_custom_world_theme_mode(value: DomainCustomWorldThemeMode) -> &'static str {
|
||||
match value {
|
||||
DomainCustomWorldThemeMode::Martial => "martial",
|
||||
DomainCustomWorldThemeMode::Arcane => "arcane",
|
||||
DomainCustomWorldThemeMode::Machina => "machina",
|
||||
DomainCustomWorldThemeMode::Tide => "tide",
|
||||
DomainCustomWorldThemeMode::Rift => "rift",
|
||||
DomainCustomWorldThemeMode::Mythic => "mythic",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_ai_task_kind(value: AiTaskKind) -> &'static str {
|
||||
match value {
|
||||
AiTaskKind::StoryGeneration => "story_generation",
|
||||
AiTaskKind::CharacterChat => "character_chat",
|
||||
AiTaskKind::NpcChat => "npc_chat",
|
||||
AiTaskKind::CustomWorldGeneration => "custom_world_generation",
|
||||
AiTaskKind::QuestIntent => "quest_intent",
|
||||
AiTaskKind::RuntimeItemIntent => "runtime_item_intent",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_ai_result_reference_kind(value: AiResultReferenceKind) -> &'static str {
|
||||
match value {
|
||||
AiResultReferenceKind::StorySession => "story_session",
|
||||
AiResultReferenceKind::StoryEvent => "story_event",
|
||||
AiResultReferenceKind::CustomWorldProfile => "custom_world_profile",
|
||||
AiResultReferenceKind::QuestRecord => "quest_record",
|
||||
AiResultReferenceKind::RuntimeItemRecord => "runtime_item_record",
|
||||
AiResultReferenceKind::AssetObject => "asset_object",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_custom_world_checkpoint_record(
|
||||
value: serde_json::Value,
|
||||
) -> Result<CustomWorldCheckpointRecord, SpacetimeClientError> {
|
||||
let object = value.as_object().ok_or_else(|| {
|
||||
SpacetimeClientError::Runtime("custom world checkpoint 必须是 JSON object".to_string())
|
||||
})?;
|
||||
let checkpoint_id = object
|
||||
.get("checkpointId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
SpacetimeClientError::Runtime("custom world checkpoint.checkpointId 缺失".to_string())
|
||||
})?;
|
||||
let created_at = object
|
||||
.get("createdAt")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
SpacetimeClientError::Runtime("custom world checkpoint.createdAt 缺失".to_string())
|
||||
})?;
|
||||
let label = object
|
||||
.get("label")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
SpacetimeClientError::Runtime("custom world checkpoint.label 缺失".to_string())
|
||||
})?;
|
||||
|
||||
Ok(CustomWorldCheckpointRecord {
|
||||
checkpoint_id: checkpoint_id.to_string(),
|
||||
created_at: created_at.to_string(),
|
||||
label: label.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn parse_custom_world_publish_gate_record(
|
||||
value: &str,
|
||||
) -> Result<CustomWorldPublishGateRecord, SpacetimeClientError> {
|
||||
let object = parse_json_value(value, "custom world publish_gate_json")?
|
||||
.as_object()
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
SpacetimeClientError::Runtime(
|
||||
"custom world publish_gate_json 必须是 JSON object".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let profile_id = object
|
||||
.get("profileId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
SpacetimeClientError::Runtime("custom world publish_gate.profileId 缺失".to_string())
|
||||
})?;
|
||||
let blockers = object
|
||||
.get("blockers")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.ok_or_else(|| {
|
||||
SpacetimeClientError::Runtime("custom world publish_gate.blockers 缺失".to_string())
|
||||
})?
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|entry| {
|
||||
let object = entry.as_object().ok_or_else(|| {
|
||||
SpacetimeClientError::Runtime(
|
||||
"custom world publish gate blocker 必须是 JSON object".to_string(),
|
||||
)
|
||||
})?;
|
||||
let id = object
|
||||
.get("id")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
SpacetimeClientError::Runtime(
|
||||
"custom world publish gate blocker.id 缺失".to_string(),
|
||||
)
|
||||
})?;
|
||||
let code = object
|
||||
.get("code")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
SpacetimeClientError::Runtime(
|
||||
"custom world publish gate blocker.code 缺失".to_string(),
|
||||
)
|
||||
})?;
|
||||
let message = object
|
||||
.get("message")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
SpacetimeClientError::Runtime(
|
||||
"custom world publish gate blocker.message 缺失".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(CustomWorldResultPreviewBlockerRecord {
|
||||
id: id.to_string(),
|
||||
code: code.to_string(),
|
||||
message: message.to_string(),
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let blocker_count = object
|
||||
.get("blockerCount")
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.and_then(|value| u32::try_from(value).ok())
|
||||
.ok_or_else(|| {
|
||||
SpacetimeClientError::Runtime("custom world publish_gate.blockerCount 缺失".to_string())
|
||||
})?;
|
||||
let publish_ready = object
|
||||
.get("publishReady")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.ok_or_else(|| {
|
||||
SpacetimeClientError::Runtime("custom world publish_gate.publishReady 缺失".to_string())
|
||||
})?;
|
||||
let can_enter_world = object
|
||||
.get("canEnterWorld")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.ok_or_else(|| {
|
||||
SpacetimeClientError::Runtime(
|
||||
"custom world publish_gate.canEnterWorld 缺失".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(CustomWorldPublishGateRecord {
|
||||
profile_id: profile_id.to_string(),
|
||||
blockers,
|
||||
blocker_count,
|
||||
publish_ready,
|
||||
can_enter_world,
|
||||
})
|
||||
}
|
||||
200
server-rs/crates/spacetime-client/src/mapper/inventory.rs
Normal file
200
server-rs/crates/spacetime-client/src/mapper/inventory.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
use super::*;
|
||||
|
||||
impl From<DomainRuntimeInventoryStateQueryInput> for RuntimeInventoryStateQueryInput {
|
||||
fn from(input: DomainRuntimeInventoryStateQueryInput) -> Self {
|
||||
Self {
|
||||
runtime_session_id: input.runtime_session_id,
|
||||
actor_user_id: input.actor_user_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_inventory_state_procedure_result(
|
||||
result: RuntimeInventoryStateProcedureResult,
|
||||
) -> Result<RuntimeInventoryStateRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let snapshot = result
|
||||
.snapshot
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("runtime inventory state 快照"))?;
|
||||
|
||||
Ok(build_runtime_inventory_state_record(
|
||||
map_runtime_inventory_state_snapshot(snapshot),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_inventory_state_snapshot(
|
||||
snapshot: RuntimeInventoryStateSnapshot,
|
||||
) -> DomainRuntimeInventoryStateSnapshot {
|
||||
DomainRuntimeInventoryStateSnapshot {
|
||||
runtime_session_id: snapshot.runtime_session_id,
|
||||
actor_user_id: snapshot.actor_user_id,
|
||||
backpack_items: snapshot
|
||||
.backpack_items
|
||||
.into_iter()
|
||||
.map(map_inventory_slot_snapshot)
|
||||
.collect(),
|
||||
equipment_items: snapshot
|
||||
.equipment_items
|
||||
.into_iter()
|
||||
.map(map_inventory_slot_snapshot)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_inventory_slot_snapshot(
|
||||
snapshot: InventorySlotSnapshot,
|
||||
) -> module_inventory::InventorySlotSnapshot {
|
||||
module_inventory::InventorySlotSnapshot {
|
||||
slot_id: snapshot.slot_id,
|
||||
runtime_session_id: snapshot.runtime_session_id,
|
||||
story_session_id: snapshot.story_session_id,
|
||||
actor_user_id: snapshot.actor_user_id,
|
||||
container_kind: map_inventory_container_kind(snapshot.container_kind),
|
||||
slot_key: snapshot.slot_key,
|
||||
item_id: snapshot.item_id,
|
||||
category: snapshot.category,
|
||||
name: snapshot.name,
|
||||
description: snapshot.description,
|
||||
quantity: snapshot.quantity,
|
||||
rarity: map_inventory_item_rarity(snapshot.rarity),
|
||||
tags: snapshot.tags,
|
||||
stackable: snapshot.stackable,
|
||||
stack_key: snapshot.stack_key,
|
||||
equipment_slot_id: snapshot.equipment_slot_id.map(map_inventory_equipment_slot),
|
||||
source_kind: map_inventory_item_source_kind(snapshot.source_kind),
|
||||
source_reference_id: snapshot.source_reference_id,
|
||||
created_at_micros: snapshot.created_at_micros,
|
||||
updated_at_micros: snapshot.updated_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_item_reward_item_rarity(
|
||||
value: DomainRuntimeItemRewardItemRarity,
|
||||
) -> RuntimeItemRewardItemRarity {
|
||||
match value {
|
||||
DomainRuntimeItemRewardItemRarity::Common => RuntimeItemRewardItemRarity::Common,
|
||||
DomainRuntimeItemRewardItemRarity::Uncommon => RuntimeItemRewardItemRarity::Uncommon,
|
||||
DomainRuntimeItemRewardItemRarity::Rare => RuntimeItemRewardItemRarity::Rare,
|
||||
DomainRuntimeItemRewardItemRarity::Epic => RuntimeItemRewardItemRarity::Epic,
|
||||
DomainRuntimeItemRewardItemRarity::Legendary => RuntimeItemRewardItemRarity::Legendary,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_item_equipment_slot(
|
||||
value: DomainRuntimeItemEquipmentSlot,
|
||||
) -> RuntimeItemEquipmentSlot {
|
||||
match value {
|
||||
DomainRuntimeItemEquipmentSlot::Weapon => RuntimeItemEquipmentSlot::Weapon,
|
||||
DomainRuntimeItemEquipmentSlot::Armor => RuntimeItemEquipmentSlot::Armor,
|
||||
DomainRuntimeItemEquipmentSlot::Relic => RuntimeItemEquipmentSlot::Relic,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_item_reward_item_rarity_back(
|
||||
value: RuntimeItemRewardItemRarity,
|
||||
) -> DomainRuntimeItemRewardItemRarity {
|
||||
match value {
|
||||
RuntimeItemRewardItemRarity::Common => DomainRuntimeItemRewardItemRarity::Common,
|
||||
RuntimeItemRewardItemRarity::Uncommon => DomainRuntimeItemRewardItemRarity::Uncommon,
|
||||
RuntimeItemRewardItemRarity::Rare => DomainRuntimeItemRewardItemRarity::Rare,
|
||||
RuntimeItemRewardItemRarity::Epic => DomainRuntimeItemRewardItemRarity::Epic,
|
||||
RuntimeItemRewardItemRarity::Legendary => DomainRuntimeItemRewardItemRarity::Legendary,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_item_equipment_slot_back(
|
||||
value: RuntimeItemEquipmentSlot,
|
||||
) -> DomainRuntimeItemEquipmentSlot {
|
||||
match value {
|
||||
RuntimeItemEquipmentSlot::Weapon => DomainRuntimeItemEquipmentSlot::Weapon,
|
||||
RuntimeItemEquipmentSlot::Armor => DomainRuntimeItemEquipmentSlot::Armor,
|
||||
RuntimeItemEquipmentSlot::Relic => DomainRuntimeItemEquipmentSlot::Relic,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_ai_result_reference_kind(
|
||||
value: DomainAiResultReferenceKind,
|
||||
) -> AiResultReferenceKind {
|
||||
match value {
|
||||
DomainAiResultReferenceKind::StorySession => AiResultReferenceKind::StorySession,
|
||||
DomainAiResultReferenceKind::StoryEvent => AiResultReferenceKind::StoryEvent,
|
||||
DomainAiResultReferenceKind::CustomWorldProfile => {
|
||||
AiResultReferenceKind::CustomWorldProfile
|
||||
}
|
||||
DomainAiResultReferenceKind::QuestRecord => AiResultReferenceKind::QuestRecord,
|
||||
DomainAiResultReferenceKind::RuntimeItemRecord => AiResultReferenceKind::RuntimeItemRecord,
|
||||
DomainAiResultReferenceKind::AssetObject => AiResultReferenceKind::AssetObject,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_item_reward_item_snapshot(
|
||||
snapshot: DomainRuntimeItemRewardItemSnapshot,
|
||||
) -> RuntimeItemRewardItemSnapshot {
|
||||
RuntimeItemRewardItemSnapshot {
|
||||
item_id: snapshot.item_id,
|
||||
category: snapshot.category,
|
||||
item_name: snapshot.item_name,
|
||||
description: snapshot.description,
|
||||
quantity: snapshot.quantity,
|
||||
rarity: map_runtime_item_reward_item_rarity(snapshot.rarity),
|
||||
tags: snapshot.tags,
|
||||
stackable: snapshot.stackable,
|
||||
stack_key: snapshot.stack_key,
|
||||
equipment_slot_id: snapshot
|
||||
.equipment_slot_id
|
||||
.map(map_runtime_item_equipment_slot),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_item_reward_item_snapshot_back(
|
||||
snapshot: RuntimeItemRewardItemSnapshot,
|
||||
) -> DomainRuntimeItemRewardItemSnapshot {
|
||||
DomainRuntimeItemRewardItemSnapshot {
|
||||
item_id: snapshot.item_id,
|
||||
category: snapshot.category,
|
||||
item_name: snapshot.item_name,
|
||||
description: snapshot.description,
|
||||
quantity: snapshot.quantity,
|
||||
rarity: map_runtime_item_reward_item_rarity_back(snapshot.rarity),
|
||||
tags: snapshot.tags,
|
||||
stackable: snapshot.stackable,
|
||||
stack_key: snapshot.stack_key,
|
||||
equipment_slot_id: snapshot
|
||||
.equipment_slot_id
|
||||
.map(map_runtime_item_equipment_slot_back),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_inventory_container_kind(
|
||||
value: InventoryContainerKind,
|
||||
) -> module_inventory::InventoryContainerKind {
|
||||
match value {
|
||||
InventoryContainerKind::Backpack => module_inventory::InventoryContainerKind::Backpack,
|
||||
InventoryContainerKind::Equipment => module_inventory::InventoryContainerKind::Equipment,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_inventory_item_rarity(
|
||||
value: InventoryItemRarity,
|
||||
) -> module_inventory::InventoryItemRarity {
|
||||
match value {
|
||||
InventoryItemRarity::Common => module_inventory::InventoryItemRarity::Common,
|
||||
InventoryItemRarity::Uncommon => module_inventory::InventoryItemRarity::Uncommon,
|
||||
InventoryItemRarity::Rare => module_inventory::InventoryItemRarity::Rare,
|
||||
InventoryItemRarity::Epic => module_inventory::InventoryItemRarity::Epic,
|
||||
InventoryItemRarity::Legendary => module_inventory::InventoryItemRarity::Legendary,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_inventory_equipment_slot(
|
||||
value: InventoryEquipmentSlot,
|
||||
) -> module_inventory::InventoryEquipmentSlot {
|
||||
match value {
|
||||
InventoryEquipmentSlot::Weapon => module_inventory::InventoryEquipmentSlot::Weapon,
|
||||
InventoryEquipmentSlot::Armor => module_inventory::InventoryEquipmentSlot::Armor,
|
||||
InventoryEquipmentSlot::Relic => module_inventory::InventoryEquipmentSlot::Relic,
|
||||
}
|
||||
}
|
||||
606
server-rs/crates/spacetime-client/src/mapper/match3d.rs
Normal file
606
server-rs/crates/spacetime-client/src/mapper/match3d.rs
Normal file
@@ -0,0 +1,606 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn map_match3d_agent_session_procedure_result(
|
||||
result: Match3DAgentSessionProcedureResult,
|
||||
) -> Result<Match3DAgentSessionRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::Procedure(
|
||||
result
|
||||
.error_message
|
||||
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
let session = result.session.ok_or_else(|| {
|
||||
SpacetimeClientError::Procedure(
|
||||
"SpacetimeDB procedure 未返回 match3d agent session 快照".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(map_match3d_agent_session_snapshot(session))
|
||||
}
|
||||
|
||||
pub(crate) fn map_match3d_work_procedure_result(
|
||||
result: Match3DWorkProcedureResult,
|
||||
) -> Result<Match3DWorkProfileRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::Procedure(
|
||||
result
|
||||
.error_message
|
||||
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
let work = result.work.ok_or_else(|| {
|
||||
SpacetimeClientError::Procedure(
|
||||
"SpacetimeDB procedure 未返回 match3d work 快照".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(map_match3d_work_snapshot(work))
|
||||
}
|
||||
|
||||
pub(crate) fn map_match3d_works_procedure_result(
|
||||
result: Match3DWorksProcedureResult,
|
||||
) -> Result<Vec<Match3DWorkProfileRecord>, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::Procedure(
|
||||
result
|
||||
.error_message
|
||||
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(result
|
||||
.items
|
||||
.into_iter()
|
||||
.map(map_match3d_work_snapshot)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(crate) fn map_match3d_run_procedure_result(
|
||||
result: Match3DRunProcedureResult,
|
||||
) -> Result<Match3DRunRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::Procedure(
|
||||
result
|
||||
.error_message
|
||||
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
let run = result.run.ok_or_else(|| {
|
||||
SpacetimeClientError::Procedure("SpacetimeDB procedure 未返回 match3d run 快照".to_string())
|
||||
})?;
|
||||
Ok(map_match3d_run_snapshot(run))
|
||||
}
|
||||
|
||||
pub(crate) fn map_match3d_click_item_procedure_result(
|
||||
result: Match3DClickItemProcedureResult,
|
||||
) -> Result<Match3DClickConfirmationRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::Procedure(
|
||||
result
|
||||
.error_message
|
||||
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
let run = result.run.ok_or_else(|| {
|
||||
SpacetimeClientError::Procedure(
|
||||
"SpacetimeDB procedure 未返回 match3d click run 快照".to_string(),
|
||||
)
|
||||
})?;
|
||||
let run = map_match3d_run_snapshot(run);
|
||||
let accepted = result.status == "Accepted";
|
||||
let accepted_item_instance_id = result.accepted_item_instance_id.clone();
|
||||
let entered_slot_index = accepted_item_instance_id.as_deref().and_then(|item_id| {
|
||||
run.items
|
||||
.iter()
|
||||
.find(|item| item.item_instance_id == item_id)
|
||||
.and_then(|item| item.tray_slot_index)
|
||||
});
|
||||
|
||||
Ok(Match3DClickConfirmationRecord {
|
||||
status: result.status.clone(),
|
||||
accepted,
|
||||
reject_reason: if accepted { None } else { Some(result.status) },
|
||||
accepted_item_instance_id,
|
||||
entered_slot_index,
|
||||
cleared_item_instance_ids: result.cleared_item_instance_ids,
|
||||
failure_reason: result.failure_reason,
|
||||
run,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_match3d_agent_session_snapshot(
|
||||
snapshot: Match3DAgentSessionSnapshot,
|
||||
) -> Match3DAgentSessionRecord {
|
||||
let config = map_match3d_creator_config(snapshot.config);
|
||||
Match3DAgentSessionRecord {
|
||||
session_id: snapshot.session_id,
|
||||
current_turn: snapshot.current_turn,
|
||||
progress_percent: snapshot.progress_percent,
|
||||
stage: normalize_match3d_stage(&snapshot.stage).to_string(),
|
||||
anchor_pack: build_match3d_anchor_pack(&config),
|
||||
draft: snapshot
|
||||
.draft
|
||||
.map(|draft| map_match3d_result_draft(draft, config.reference_image_src.clone())),
|
||||
config: Some(config),
|
||||
messages: snapshot
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(map_match3d_agent_message_snapshot)
|
||||
.collect(),
|
||||
last_assistant_reply: empty_string_to_none(snapshot.last_assistant_reply),
|
||||
published_profile_id: snapshot.published_profile_id,
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_match3d_creator_config(
|
||||
snapshot: Match3DCreatorConfigSnapshot,
|
||||
) -> Match3DCreatorConfigRecord {
|
||||
Match3DCreatorConfigRecord {
|
||||
theme_text: snapshot.theme_text,
|
||||
reference_image_src: snapshot.reference_image_src,
|
||||
clear_count: snapshot.clear_count,
|
||||
difficulty: snapshot.difficulty,
|
||||
asset_style_id: snapshot.asset_style_id,
|
||||
asset_style_label: snapshot.asset_style_label,
|
||||
asset_style_prompt: snapshot.asset_style_prompt,
|
||||
generate_click_sound: snapshot.generate_click_sound,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_match3d_result_draft(
|
||||
snapshot: Match3DDraftSnapshot,
|
||||
reference_image_src: Option<String>,
|
||||
) -> Match3DResultDraftRecord {
|
||||
Match3DResultDraftRecord {
|
||||
profile_id: snapshot.profile_id,
|
||||
game_name: snapshot.game_name,
|
||||
theme_text: snapshot.theme_text,
|
||||
summary_text: snapshot.summary_text,
|
||||
tags: snapshot.tags,
|
||||
cover_image_src: None,
|
||||
reference_image_src,
|
||||
clear_count: snapshot.clear_count,
|
||||
difficulty: snapshot.difficulty,
|
||||
generated_item_assets_json: snapshot.generated_item_assets_json,
|
||||
total_item_count: snapshot.clear_count.saturating_mul(3),
|
||||
publish_ready: false,
|
||||
blockers: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_match3d_agent_message_snapshot(
|
||||
snapshot: Match3DAgentMessageSnapshot,
|
||||
) -> Match3DAgentMessageRecord {
|
||||
Match3DAgentMessageRecord {
|
||||
message_id: snapshot.message_id,
|
||||
role: snapshot.role,
|
||||
kind: normalize_match3d_message_kind(&snapshot.kind).to_string(),
|
||||
text: snapshot.text,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_match3d_work_snapshot(snapshot: Match3DWorkSnapshot) -> Match3DWorkProfileRecord {
|
||||
let config = map_match3d_creator_config(snapshot.config);
|
||||
Match3DWorkProfileRecord {
|
||||
work_id: snapshot.profile_id.clone(),
|
||||
profile_id: snapshot.profile_id,
|
||||
owner_user_id: snapshot.owner_user_id,
|
||||
source_session_id: empty_string_to_none(snapshot.source_session_id),
|
||||
author_display_name: snapshot.author_display_name,
|
||||
game_name: snapshot.game_name,
|
||||
theme_text: snapshot.theme_text,
|
||||
summary: snapshot.summary_text,
|
||||
tags: snapshot.tags,
|
||||
cover_image_src: empty_string_to_none(snapshot.cover_image_src),
|
||||
cover_asset_id: empty_string_to_none(snapshot.cover_asset_id),
|
||||
reference_image_src: config.reference_image_src,
|
||||
clear_count: snapshot.clear_count,
|
||||
difficulty: snapshot.difficulty,
|
||||
publication_status: normalize_match3d_publication_status(&snapshot.publication_status)
|
||||
.to_string(),
|
||||
play_count: snapshot.play_count,
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
published_at: snapshot.published_at_micros.map(format_timestamp_micros),
|
||||
publish_ready: snapshot.publish_ready,
|
||||
generated_item_assets_json: snapshot.generated_item_assets_json,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_match3d_gallery_view_row(row: Match3DGalleryViewRow) -> Match3DWorkProfileRecord {
|
||||
Match3DWorkProfileRecord {
|
||||
work_id: row.profile_id.clone(),
|
||||
profile_id: row.profile_id,
|
||||
owner_user_id: row.owner_user_id,
|
||||
source_session_id: empty_string_to_none(row.source_session_id),
|
||||
author_display_name: row.author_display_name,
|
||||
game_name: row.game_name,
|
||||
theme_text: row.theme_text,
|
||||
summary: row.summary_text,
|
||||
tags: row.tags,
|
||||
cover_image_src: empty_string_to_none(row.cover_image_src),
|
||||
cover_asset_id: empty_string_to_none(row.cover_asset_id),
|
||||
reference_image_src: row.reference_image_src,
|
||||
clear_count: row.clear_count,
|
||||
difficulty: row.difficulty,
|
||||
publication_status: normalize_match3d_publication_status(&row.publication_status)
|
||||
.to_string(),
|
||||
play_count: row.play_count,
|
||||
updated_at: format_timestamp_micros(row.updated_at_micros),
|
||||
published_at: row.published_at_micros.map(format_timestamp_micros),
|
||||
publish_ready: row.publish_ready,
|
||||
generated_item_assets_json: row.generated_item_assets_json,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_match3d_run_snapshot(snapshot: Match3DRunSnapshot) -> Match3DRunRecord {
|
||||
let tray_slots = snapshot
|
||||
.tray_slots
|
||||
.into_iter()
|
||||
.map(map_match3d_tray_slot_snapshot)
|
||||
.collect::<Vec<_>>();
|
||||
let items = snapshot
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let tray_slot_index = tray_slots
|
||||
.iter()
|
||||
.find(|slot| {
|
||||
slot.item_instance_id.as_deref() == Some(item.item_instance_id.as_str())
|
||||
})
|
||||
.map(|slot| slot.slot_index);
|
||||
map_match3d_item_snapshot(item, tray_slot_index)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Match3DRunRecord {
|
||||
run_id: snapshot.run_id,
|
||||
profile_id: snapshot.profile_id,
|
||||
owner_user_id: String::new(),
|
||||
status: snapshot.status,
|
||||
snapshot_version: u64::from(snapshot.snapshot_version),
|
||||
started_at_ms: i64_to_u64_ms(snapshot.started_at_ms),
|
||||
duration_limit_ms: i64_to_u64_ms(snapshot.duration_limit_ms),
|
||||
server_now_ms: Some(i64_to_u64_ms(snapshot.server_now_ms)),
|
||||
remaining_ms: i64_to_u64_ms(snapshot.remaining_ms),
|
||||
clear_count: snapshot.clear_count,
|
||||
total_item_count: snapshot.total_item_count,
|
||||
cleared_item_count: snapshot.cleared_item_count,
|
||||
items,
|
||||
tray_slots,
|
||||
failure_reason: snapshot.failure_reason,
|
||||
last_confirmed_action_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_match3d_item_snapshot(
|
||||
snapshot: Match3DItemSnapshot,
|
||||
tray_slot_index: Option<u32>,
|
||||
) -> Match3DItemSnapshotRecord {
|
||||
Match3DItemSnapshotRecord {
|
||||
item_instance_id: snapshot.item_instance_id,
|
||||
item_type_id: snapshot.item_type_id,
|
||||
visual_key: snapshot.visual_key,
|
||||
x: snapshot.x,
|
||||
y: snapshot.y,
|
||||
radius: snapshot.radius,
|
||||
layer: snapshot.layer,
|
||||
state: snapshot.state,
|
||||
clickable: snapshot.clickable,
|
||||
tray_slot_index,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_match3d_tray_slot_snapshot(snapshot: Match3DTraySlotSnapshot) -> Match3DTraySlotRecord {
|
||||
Match3DTraySlotRecord {
|
||||
slot_index: snapshot.slot_index,
|
||||
item_instance_id: snapshot.item_instance_id,
|
||||
item_type_id: snapshot.item_type_id,
|
||||
visual_key: snapshot.visual_key,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_match3d_anchor_pack(config: &Match3DCreatorConfigRecord) -> Match3DAnchorPackRecord {
|
||||
let clear_count = config.clear_count.to_string();
|
||||
let difficulty = config.difficulty.to_string();
|
||||
Match3DAnchorPackRecord {
|
||||
theme: build_match3d_anchor_item("theme", "题材主题", config.theme_text.as_str()),
|
||||
clear_count: build_match3d_anchor_item("clearCount", "需要消除次数", clear_count.as_str()),
|
||||
difficulty: build_match3d_anchor_item("difficulty", "难度", difficulty.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_match3d_anchor_item(key: &str, label: &str, value: &str) -> Match3DAnchorItemRecord {
|
||||
Match3DAnchorItemRecord {
|
||||
key: key.to_string(),
|
||||
label: label.to_string(),
|
||||
value: value.to_string(),
|
||||
status: if value.trim().is_empty() {
|
||||
"missing"
|
||||
} else {
|
||||
"confirmed"
|
||||
}
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_match3d_stage(value: &str) -> &str {
|
||||
match value {
|
||||
"Collecting" | "collecting" | "collecting_config" => "collecting_config",
|
||||
"ReadyToCompile" | "ready_to_compile" => "ready_to_compile",
|
||||
"DraftCompiled" | "draft_compiled" | "draft_ready" => "draft_ready",
|
||||
"Published" | "published" => "published",
|
||||
_ => value,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_match3d_publication_status(value: &str) -> &str {
|
||||
match value {
|
||||
"Draft" | "draft" => "draft",
|
||||
"Published" | "published" => "published",
|
||||
_ => value,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_match3d_message_kind(value: &str) -> &str {
|
||||
match value {
|
||||
"text" => "chat",
|
||||
_ => value,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Match3DAgentSessionCreateRecordInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub seed_text: String,
|
||||
pub welcome_message_id: String,
|
||||
pub welcome_message_text: String,
|
||||
pub config_json: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Match3DAgentMessageSubmitRecordInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub user_message_id: String,
|
||||
pub user_message_text: String,
|
||||
pub submitted_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Match3DAgentMessageFinalizeRecordInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub assistant_message_id: Option<String>,
|
||||
pub assistant_reply_text: Option<String>,
|
||||
pub config_json: Option<String>,
|
||||
pub progress_percent: u32,
|
||||
pub stage: String,
|
||||
pub updated_at_micros: i64,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Match3DCompileDraftRecordInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub author_display_name: String,
|
||||
pub game_name: Option<String>,
|
||||
pub summary_text: Option<String>,
|
||||
pub tags_json: Option<String>,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub cover_asset_id: Option<String>,
|
||||
pub compiled_at_micros: i64,
|
||||
pub generated_item_assets_json: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Match3DWorkUpdateRecordInput {
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub game_name: String,
|
||||
pub theme_text: String,
|
||||
pub summary_text: String,
|
||||
pub tags_json: String,
|
||||
pub cover_image_src: String,
|
||||
pub cover_asset_id: String,
|
||||
pub clear_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Match3DRunStartRecordInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub started_at_ms: i64,
|
||||
pub item_type_count_override: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Match3DRunClickRecordInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub item_instance_id: String,
|
||||
pub client_snapshot_version: u32,
|
||||
pub client_event_id: String,
|
||||
pub clicked_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Match3DRunStopRecordInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub stopped_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Match3DRunRestartRecordInput {
|
||||
pub source_run_id: String,
|
||||
pub next_run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub restarted_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Match3DRunTimeUpRecordInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub finished_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Match3DAnchorItemRecord {
|
||||
pub key: String,
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Match3DAnchorPackRecord {
|
||||
pub theme: Match3DAnchorItemRecord,
|
||||
pub clear_count: Match3DAnchorItemRecord,
|
||||
pub difficulty: Match3DAnchorItemRecord,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Match3DCreatorConfigRecord {
|
||||
pub theme_text: String,
|
||||
pub reference_image_src: Option<String>,
|
||||
pub clear_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub asset_style_id: Option<String>,
|
||||
pub asset_style_label: Option<String>,
|
||||
pub asset_style_prompt: Option<String>,
|
||||
pub generate_click_sound: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Match3DResultDraftRecord {
|
||||
pub profile_id: String,
|
||||
pub game_name: String,
|
||||
pub theme_text: String,
|
||||
pub summary_text: String,
|
||||
pub tags: Vec<String>,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub reference_image_src: Option<String>,
|
||||
pub clear_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub generated_item_assets_json: Option<String>,
|
||||
pub total_item_count: u32,
|
||||
pub publish_ready: bool,
|
||||
pub blockers: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Match3DAgentMessageRecord {
|
||||
pub message_id: String,
|
||||
pub role: String,
|
||||
pub kind: String,
|
||||
pub text: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Match3DAgentSessionRecord {
|
||||
pub session_id: String,
|
||||
pub current_turn: u32,
|
||||
pub progress_percent: u32,
|
||||
pub stage: String,
|
||||
pub anchor_pack: Match3DAnchorPackRecord,
|
||||
pub config: Option<Match3DCreatorConfigRecord>,
|
||||
pub draft: Option<Match3DResultDraftRecord>,
|
||||
pub messages: Vec<Match3DAgentMessageRecord>,
|
||||
pub last_assistant_reply: Option<String>,
|
||||
pub published_profile_id: Option<String>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Match3DWorkProfileRecord {
|
||||
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 game_name: String,
|
||||
pub theme_text: String,
|
||||
pub summary: String,
|
||||
pub tags: Vec<String>,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub cover_asset_id: Option<String>,
|
||||
pub reference_image_src: Option<String>,
|
||||
pub clear_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub publication_status: String,
|
||||
pub play_count: u32,
|
||||
pub updated_at: String,
|
||||
pub published_at: Option<String>,
|
||||
pub publish_ready: bool,
|
||||
pub generated_item_assets_json: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Match3DItemSnapshotRecord {
|
||||
pub item_instance_id: String,
|
||||
pub item_type_id: String,
|
||||
pub visual_key: String,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub radius: f32,
|
||||
pub layer: u32,
|
||||
pub state: String,
|
||||
pub clickable: bool,
|
||||
pub tray_slot_index: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Match3DTraySlotRecord {
|
||||
pub slot_index: u32,
|
||||
pub item_instance_id: Option<String>,
|
||||
pub item_type_id: Option<String>,
|
||||
pub visual_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Match3DRunRecord {
|
||||
pub run_id: String,
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub status: String,
|
||||
pub snapshot_version: u64,
|
||||
pub started_at_ms: u64,
|
||||
pub duration_limit_ms: u64,
|
||||
pub server_now_ms: Option<u64>,
|
||||
pub remaining_ms: u64,
|
||||
pub clear_count: u32,
|
||||
pub total_item_count: u32,
|
||||
pub cleared_item_count: u32,
|
||||
pub items: Vec<Match3DItemSnapshotRecord>,
|
||||
pub tray_slots: Vec<Match3DTraySlotRecord>,
|
||||
pub failure_reason: Option<String>,
|
||||
pub last_confirmed_action_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Match3DClickConfirmationRecord {
|
||||
pub status: String,
|
||||
pub accepted: bool,
|
||||
pub reject_reason: Option<String>,
|
||||
pub accepted_item_instance_id: Option<String>,
|
||||
pub entered_slot_index: Option<u32>,
|
||||
pub cleared_item_instance_ids: Vec<String>,
|
||||
pub failure_reason: Option<String>,
|
||||
pub run: Match3DRunRecord,
|
||||
}
|
||||
624
server-rs/crates/spacetime-client/src/mapper/npc.rs
Normal file
624
server-rs/crates/spacetime-client/src/mapper/npc.rs
Normal file
@@ -0,0 +1,624 @@
|
||||
use super::*;
|
||||
|
||||
impl From<DomainBattleStateInput> for BattleStateInput {
|
||||
fn from(input: DomainBattleStateInput) -> Self {
|
||||
Self {
|
||||
battle_state_id: input.battle_state_id,
|
||||
story_session_id: input.story_session_id,
|
||||
runtime_session_id: input.runtime_session_id,
|
||||
actor_user_id: input.actor_user_id,
|
||||
chapter_id: input.chapter_id,
|
||||
target_npc_id: input.target_npc_id,
|
||||
target_name: input.target_name,
|
||||
battle_mode: map_battle_mode(input.battle_mode),
|
||||
player_hp: input.player_hp,
|
||||
player_max_hp: input.player_max_hp,
|
||||
player_mana: input.player_mana,
|
||||
player_max_mana: input.player_max_mana,
|
||||
target_hp: input.target_hp,
|
||||
target_max_hp: input.target_max_hp,
|
||||
experience_reward: input.experience_reward,
|
||||
reward_items: input
|
||||
.reward_items
|
||||
.into_iter()
|
||||
.map(map_runtime_item_reward_item_snapshot)
|
||||
.collect(),
|
||||
created_at_micros: input.created_at_micros,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_npc_battle_interaction_procedure_result(
|
||||
result: NpcBattleInteractionProcedureResult,
|
||||
) -> Result<NpcBattleInteractionRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let interaction_result = result
|
||||
.result
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("NPC 开战结果"))?;
|
||||
|
||||
Ok(build_npc_battle_interaction_record(
|
||||
map_npc_battle_interaction_result(interaction_result),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn map_battle_state_snapshot(
|
||||
snapshot: BattleStateSnapshot,
|
||||
) -> DomainBattleStateSnapshot {
|
||||
DomainBattleStateSnapshot {
|
||||
battle_state_id: snapshot.battle_state_id,
|
||||
story_session_id: snapshot.story_session_id,
|
||||
runtime_session_id: snapshot.runtime_session_id,
|
||||
actor_user_id: snapshot.actor_user_id,
|
||||
chapter_id: snapshot.chapter_id,
|
||||
target_npc_id: snapshot.target_npc_id,
|
||||
target_name: snapshot.target_name,
|
||||
battle_mode: map_battle_mode_back(snapshot.battle_mode),
|
||||
status: map_battle_status(snapshot.status),
|
||||
player_hp: snapshot.player_hp,
|
||||
player_max_hp: snapshot.player_max_hp,
|
||||
player_mana: snapshot.player_mana,
|
||||
player_max_mana: snapshot.player_max_mana,
|
||||
target_hp: snapshot.target_hp,
|
||||
target_max_hp: snapshot.target_max_hp,
|
||||
experience_reward: snapshot.experience_reward,
|
||||
reward_items: snapshot
|
||||
.reward_items
|
||||
.into_iter()
|
||||
.map(map_runtime_item_reward_item_snapshot_back)
|
||||
.collect(),
|
||||
turn_index: snapshot.turn_index,
|
||||
last_action_function_id: snapshot.last_action_function_id,
|
||||
last_action_text: snapshot.last_action_text,
|
||||
last_result_text: snapshot.last_result_text,
|
||||
last_damage_dealt: snapshot.last_damage_dealt,
|
||||
last_damage_taken: snapshot.last_damage_taken,
|
||||
last_outcome: map_combat_outcome(snapshot.last_outcome),
|
||||
version: snapshot.version,
|
||||
created_at_micros: snapshot.created_at_micros,
|
||||
updated_at_micros: snapshot.updated_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_npc_battle_interaction_result(
|
||||
result: NpcBattleInteractionResult,
|
||||
) -> NpcBattleInteractionSnapshot {
|
||||
NpcBattleInteractionSnapshot {
|
||||
interaction: map_npc_interaction_result(result.interaction),
|
||||
battle_state: map_battle_state_snapshot(result.battle_state),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_npc_interaction_result(
|
||||
result: NpcInteractionResult,
|
||||
) -> DomainNpcInteractionResult {
|
||||
DomainNpcInteractionResult {
|
||||
npc_state: map_npc_state_snapshot(result.npc_state),
|
||||
interaction_status: map_npc_interaction_status(result.interaction_status),
|
||||
action_text: result.action_text,
|
||||
result_text: result.result_text,
|
||||
story_text: result.story_text,
|
||||
battle_mode: result.battle_mode.map(map_npc_interaction_battle_mode),
|
||||
encounter_closed: result.encounter_closed,
|
||||
affinity_changed: result.affinity_changed,
|
||||
previous_affinity: result.previous_affinity,
|
||||
next_affinity: result.next_affinity,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_npc_state_snapshot(snapshot: NpcStateSnapshot) -> DomainNpcStateSnapshot {
|
||||
DomainNpcStateSnapshot {
|
||||
npc_state_id: snapshot.npc_state_id,
|
||||
runtime_session_id: snapshot.runtime_session_id,
|
||||
npc_id: snapshot.npc_id,
|
||||
npc_name: snapshot.npc_name,
|
||||
affinity: snapshot.affinity,
|
||||
relation_state: map_npc_relation_state(snapshot.relation_state),
|
||||
help_used: snapshot.help_used,
|
||||
chatted_count: snapshot.chatted_count,
|
||||
gifts_given: snapshot.gifts_given,
|
||||
recruited: snapshot.recruited,
|
||||
trade_stock_signature: snapshot.trade_stock_signature,
|
||||
revealed_facts: snapshot.revealed_facts,
|
||||
known_attribute_rumors: snapshot.known_attribute_rumors,
|
||||
first_meaningful_contact_resolved: snapshot.first_meaningful_contact_resolved,
|
||||
seen_backstory_chapter_ids: snapshot.seen_backstory_chapter_ids,
|
||||
stance_profile: map_npc_stance_profile(snapshot.stance_profile),
|
||||
created_at_micros: snapshot.created_at_micros,
|
||||
updated_at_micros: snapshot.updated_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_npc_relation_state(value: NpcRelationState) -> DomainNpcRelationState {
|
||||
DomainNpcRelationState {
|
||||
affinity: value.affinity,
|
||||
stance: map_npc_relation_stance(value.stance),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_npc_stance_profile(value: NpcStanceProfile) -> DomainNpcStanceProfile {
|
||||
DomainNpcStanceProfile {
|
||||
trust: value.trust,
|
||||
warmth: value.warmth,
|
||||
ideological_fit: value.ideological_fit,
|
||||
fear_or_guard: value.fear_or_guard,
|
||||
loyalty: value.loyalty,
|
||||
current_conflict_tag: value.current_conflict_tag,
|
||||
recent_approvals: value.recent_approvals,
|
||||
recent_disapprovals: value.recent_disapprovals,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_npc_interaction_status(
|
||||
value: NpcInteractionStatus,
|
||||
) -> DomainNpcInteractionStatus {
|
||||
match value {
|
||||
NpcInteractionStatus::Previewed => DomainNpcInteractionStatus::Previewed,
|
||||
NpcInteractionStatus::Dialogue => DomainNpcInteractionStatus::Dialogue,
|
||||
NpcInteractionStatus::Resolved => DomainNpcInteractionStatus::Resolved,
|
||||
NpcInteractionStatus::Recruited => DomainNpcInteractionStatus::Recruited,
|
||||
NpcInteractionStatus::BattlePending => DomainNpcInteractionStatus::BattlePending,
|
||||
NpcInteractionStatus::Left => DomainNpcInteractionStatus::Left,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_npc_interaction_battle_mode(
|
||||
value: NpcInteractionBattleMode,
|
||||
) -> DomainNpcInteractionBattleMode {
|
||||
match value {
|
||||
NpcInteractionBattleMode::Fight => DomainNpcInteractionBattleMode::Fight,
|
||||
NpcInteractionBattleMode::Spar => DomainNpcInteractionBattleMode::Spar,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_npc_relation_stance(value: NpcRelationStance) -> DomainNpcRelationStance {
|
||||
match value {
|
||||
NpcRelationStance::Hostile => DomainNpcRelationStance::Hostile,
|
||||
NpcRelationStance::Guarded => DomainNpcRelationStance::Guarded,
|
||||
NpcRelationStance::Neutral => DomainNpcRelationStance::Neutral,
|
||||
NpcRelationStance::Cooperative => DomainNpcRelationStance::Cooperative,
|
||||
NpcRelationStance::Bonded => DomainNpcRelationStance::Bonded,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_ai_task_kind(value: DomainAiTaskKind) -> AiTaskKind {
|
||||
match value {
|
||||
DomainAiTaskKind::StoryGeneration => AiTaskKind::StoryGeneration,
|
||||
DomainAiTaskKind::CharacterChat => AiTaskKind::CharacterChat,
|
||||
DomainAiTaskKind::NpcChat => AiTaskKind::NpcChat,
|
||||
DomainAiTaskKind::CustomWorldGeneration => AiTaskKind::CustomWorldGeneration,
|
||||
DomainAiTaskKind::QuestIntent => AiTaskKind::QuestIntent,
|
||||
DomainAiTaskKind::RuntimeItemIntent => AiTaskKind::RuntimeItemIntent,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BattleStateRecord {
|
||||
pub battle_state_id: String,
|
||||
pub story_session_id: String,
|
||||
pub runtime_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub chapter_id: Option<String>,
|
||||
pub target_npc_id: String,
|
||||
pub target_name: String,
|
||||
pub battle_mode: String,
|
||||
pub status: String,
|
||||
pub player_hp: i32,
|
||||
pub player_max_hp: i32,
|
||||
pub player_mana: i32,
|
||||
pub player_max_mana: i32,
|
||||
pub target_hp: i32,
|
||||
pub target_max_hp: i32,
|
||||
pub experience_reward: u32,
|
||||
pub reward_items: Vec<DomainRuntimeItemRewardItemSnapshot>,
|
||||
pub turn_index: u32,
|
||||
pub last_action_function_id: Option<String>,
|
||||
pub last_action_text: Option<String>,
|
||||
pub last_result_text: Option<String>,
|
||||
pub last_damage_dealt: i32,
|
||||
pub last_damage_taken: i32,
|
||||
pub last_outcome: String,
|
||||
pub version: u32,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CustomWorldLibraryEntryRecord {
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub public_work_code: Option<String>,
|
||||
pub author_public_user_code: Option<String>,
|
||||
pub profile: serde_json::Value,
|
||||
pub visibility: String,
|
||||
pub published_at: Option<String>,
|
||||
pub updated_at: String,
|
||||
pub author_display_name: String,
|
||||
pub world_name: String,
|
||||
pub subtitle: String,
|
||||
pub summary_text: String,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub theme_mode: String,
|
||||
pub playable_npc_count: u32,
|
||||
pub landmark_count: u32,
|
||||
pub play_count: u32,
|
||||
pub remix_count: u32,
|
||||
pub like_count: u32,
|
||||
pub recent_play_count_7d: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CustomWorldGalleryEntryRecord {
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub public_work_code: String,
|
||||
pub author_public_user_code: String,
|
||||
pub visibility: String,
|
||||
pub published_at: Option<String>,
|
||||
pub updated_at: String,
|
||||
pub author_display_name: String,
|
||||
pub world_name: String,
|
||||
pub subtitle: String,
|
||||
pub summary_text: String,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub theme_mode: String,
|
||||
pub playable_npc_count: u32,
|
||||
pub landmark_count: u32,
|
||||
pub play_count: u32,
|
||||
pub remix_count: u32,
|
||||
pub like_count: u32,
|
||||
pub recent_play_count_7d: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CustomWorldPublishedProfileCompileRecord {
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub world_name: String,
|
||||
pub subtitle: String,
|
||||
pub summary_text: String,
|
||||
pub theme_mode: String,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub playable_npc_count: u32,
|
||||
pub landmark_count: u32,
|
||||
pub author_display_name: String,
|
||||
pub compiled_profile: serde_json::Value,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CustomWorldWorkSummaryRecord {
|
||||
pub work_id: String,
|
||||
pub source_type: String,
|
||||
pub status: String,
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub summary: String,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub cover_render_mode: Option<String>,
|
||||
pub cover_character_image_srcs: Vec<String>,
|
||||
pub updated_at: String,
|
||||
pub published_at: Option<String>,
|
||||
pub stage: Option<String>,
|
||||
pub stage_label: Option<String>,
|
||||
pub playable_npc_count: u32,
|
||||
pub landmark_count: u32,
|
||||
pub role_visual_ready_count: Option<u32>,
|
||||
pub role_animation_ready_count: Option<u32>,
|
||||
pub role_asset_summary_label: Option<String>,
|
||||
pub session_id: Option<String>,
|
||||
pub profile_id: Option<String>,
|
||||
pub can_resume: bool,
|
||||
pub can_enter_world: bool,
|
||||
pub blocker_count: u32,
|
||||
pub publish_ready: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CustomWorldProfileUpsertRecordInput {
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub public_work_code: Option<String>,
|
||||
pub author_public_user_code: Option<String>,
|
||||
pub source_agent_session_id: Option<String>,
|
||||
pub world_name: String,
|
||||
pub subtitle: String,
|
||||
pub summary_text: String,
|
||||
pub theme_mode: DomainCustomWorldThemeMode,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub profile_payload_json: String,
|
||||
pub playable_npc_count: u32,
|
||||
pub landmark_count: u32,
|
||||
pub author_display_name: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ResolveNpcBattleInteractionInput {
|
||||
pub npc_interaction: DomainResolveNpcInteractionInput,
|
||||
pub story_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub battle_state_id: Option<String>,
|
||||
pub player_hp: i32,
|
||||
pub player_max_hp: i32,
|
||||
pub player_mana: i32,
|
||||
pub player_max_mana: i32,
|
||||
pub target_hp: i32,
|
||||
pub target_max_hp: i32,
|
||||
pub experience_reward: u32,
|
||||
pub reward_items: Vec<DomainRuntimeItemRewardItemSnapshot>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct NpcStateRecord {
|
||||
pub npc_state_id: String,
|
||||
pub runtime_session_id: String,
|
||||
pub npc_id: String,
|
||||
pub npc_name: String,
|
||||
pub affinity: i32,
|
||||
pub relation_stance: String,
|
||||
pub help_used: bool,
|
||||
pub chatted_count: u32,
|
||||
pub gifts_given: u32,
|
||||
pub recruited: bool,
|
||||
pub trade_stock_signature: Option<String>,
|
||||
pub revealed_facts: Vec<String>,
|
||||
pub known_attribute_rumors: Vec<String>,
|
||||
pub first_meaningful_contact_resolved: bool,
|
||||
pub seen_backstory_chapter_ids: Vec<String>,
|
||||
pub trust: u8,
|
||||
pub warmth: u8,
|
||||
pub ideological_fit: u8,
|
||||
pub fear_or_guard: u8,
|
||||
pub loyalty: u8,
|
||||
pub current_conflict_tag: Option<String>,
|
||||
pub recent_approvals: Vec<String>,
|
||||
pub recent_disapprovals: Vec<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct NpcInteractionRecord {
|
||||
pub npc_state: NpcStateRecord,
|
||||
pub interaction_status: String,
|
||||
pub action_text: String,
|
||||
pub result_text: String,
|
||||
pub story_text: Option<String>,
|
||||
pub battle_mode: Option<String>,
|
||||
pub encounter_closed: bool,
|
||||
pub affinity_changed: bool,
|
||||
pub previous_affinity: i32,
|
||||
pub next_affinity: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct NpcBattleInteractionRecord {
|
||||
pub npc_interaction: NpcInteractionRecord,
|
||||
pub battle_state: BattleStateRecord,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct NpcBattleInteractionSnapshot {
|
||||
interaction: DomainNpcInteractionResult,
|
||||
battle_state: DomainBattleStateSnapshot,
|
||||
}
|
||||
|
||||
pub(crate) fn build_battle_state_record(snapshot: DomainBattleStateSnapshot) -> BattleStateRecord {
|
||||
BattleStateRecord {
|
||||
battle_state_id: snapshot.battle_state_id,
|
||||
story_session_id: snapshot.story_session_id,
|
||||
runtime_session_id: snapshot.runtime_session_id,
|
||||
actor_user_id: snapshot.actor_user_id,
|
||||
chapter_id: snapshot.chapter_id,
|
||||
target_npc_id: snapshot.target_npc_id,
|
||||
target_name: snapshot.target_name,
|
||||
battle_mode: snapshot.battle_mode.as_str().to_string(),
|
||||
status: snapshot.status.as_str().to_string(),
|
||||
player_hp: snapshot.player_hp,
|
||||
player_max_hp: snapshot.player_max_hp,
|
||||
player_mana: snapshot.player_mana,
|
||||
player_max_mana: snapshot.player_max_mana,
|
||||
target_hp: snapshot.target_hp,
|
||||
target_max_hp: snapshot.target_max_hp,
|
||||
experience_reward: snapshot.experience_reward,
|
||||
reward_items: snapshot.reward_items,
|
||||
turn_index: snapshot.turn_index,
|
||||
last_action_function_id: snapshot.last_action_function_id,
|
||||
last_action_text: snapshot.last_action_text,
|
||||
last_result_text: snapshot.last_result_text,
|
||||
last_damage_dealt: snapshot.last_damage_dealt,
|
||||
last_damage_taken: snapshot.last_damage_taken,
|
||||
last_outcome: snapshot.last_outcome.as_str().to_string(),
|
||||
version: snapshot.version,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResolveNpcBattleInteractionInput>
|
||||
for crate::module_bindings::ResolveNpcBattleInteractionInput
|
||||
{
|
||||
fn from(input: ResolveNpcBattleInteractionInput) -> Self {
|
||||
Self {
|
||||
npc_interaction: crate::module_bindings::ResolveNpcInteractionInput {
|
||||
runtime_session_id: input.npc_interaction.runtime_session_id,
|
||||
npc_id: input.npc_interaction.npc_id,
|
||||
npc_name: input.npc_interaction.npc_name,
|
||||
interaction_function_id: input.npc_interaction.interaction_function_id,
|
||||
release_npc_id: input.npc_interaction.release_npc_id,
|
||||
updated_at_micros: input.npc_interaction.updated_at_micros,
|
||||
},
|
||||
story_session_id: input.story_session_id,
|
||||
actor_user_id: input.actor_user_id,
|
||||
battle_state_id: input.battle_state_id,
|
||||
player_hp: input.player_hp,
|
||||
player_max_hp: input.player_max_hp,
|
||||
player_mana: input.player_mana,
|
||||
player_max_mana: input.player_max_mana,
|
||||
target_hp: input.target_hp,
|
||||
target_max_hp: input.target_max_hp,
|
||||
experience_reward: input.experience_reward,
|
||||
reward_items: input
|
||||
.reward_items
|
||||
.into_iter()
|
||||
.map(map_runtime_item_reward_item_snapshot)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn validate_npc_battle_interaction_input(
|
||||
input: &ResolveNpcBattleInteractionInput,
|
||||
) -> Result<(), SpacetimeClientError> {
|
||||
let battle_state_input = DomainBattleStateInput {
|
||||
battle_state_id: input
|
||||
.battle_state_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "battle_preview".to_string()),
|
||||
story_session_id: input.story_session_id.clone(),
|
||||
runtime_session_id: input.npc_interaction.runtime_session_id.clone(),
|
||||
actor_user_id: input.actor_user_id.clone(),
|
||||
chapter_id: None,
|
||||
target_npc_id: input.npc_interaction.npc_id.clone(),
|
||||
target_name: input.npc_interaction.npc_name.clone(),
|
||||
battle_mode: DomainBattleMode::Fight,
|
||||
player_hp: input.player_hp,
|
||||
player_max_hp: input.player_max_hp,
|
||||
player_mana: input.player_mana,
|
||||
player_max_mana: input.player_max_mana,
|
||||
target_hp: input.target_hp,
|
||||
target_max_hp: input.target_max_hp,
|
||||
experience_reward: input.experience_reward,
|
||||
reward_items: input.reward_items.clone(),
|
||||
created_at_micros: input.npc_interaction.updated_at_micros,
|
||||
};
|
||||
validate_battle_state_input(&battle_state_input)
|
||||
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?;
|
||||
for reward_item in input.reward_items.iter().cloned() {
|
||||
normalize_reward_item_snapshot(reward_item)
|
||||
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn build_npc_state_record(snapshot: DomainNpcStateSnapshot) -> NpcStateRecord {
|
||||
NpcStateRecord {
|
||||
npc_state_id: snapshot.npc_state_id,
|
||||
runtime_session_id: snapshot.runtime_session_id,
|
||||
npc_id: snapshot.npc_id,
|
||||
npc_name: snapshot.npc_name,
|
||||
affinity: snapshot.affinity,
|
||||
relation_stance: format_npc_relation_stance(snapshot.relation_state.stance).to_string(),
|
||||
help_used: snapshot.help_used,
|
||||
chatted_count: snapshot.chatted_count,
|
||||
gifts_given: snapshot.gifts_given,
|
||||
recruited: snapshot.recruited,
|
||||
trade_stock_signature: snapshot.trade_stock_signature,
|
||||
revealed_facts: snapshot.revealed_facts,
|
||||
known_attribute_rumors: snapshot.known_attribute_rumors,
|
||||
first_meaningful_contact_resolved: snapshot.first_meaningful_contact_resolved,
|
||||
seen_backstory_chapter_ids: snapshot.seen_backstory_chapter_ids,
|
||||
trust: snapshot.stance_profile.trust,
|
||||
warmth: snapshot.stance_profile.warmth,
|
||||
ideological_fit: snapshot.stance_profile.ideological_fit,
|
||||
fear_or_guard: snapshot.stance_profile.fear_or_guard,
|
||||
loyalty: snapshot.stance_profile.loyalty,
|
||||
current_conflict_tag: snapshot.stance_profile.current_conflict_tag,
|
||||
recent_approvals: snapshot.stance_profile.recent_approvals,
|
||||
recent_disapprovals: snapshot.stance_profile.recent_disapprovals,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_npc_interaction_record(
|
||||
result: DomainNpcInteractionResult,
|
||||
) -> NpcInteractionRecord {
|
||||
NpcInteractionRecord {
|
||||
npc_state: build_npc_state_record(result.npc_state),
|
||||
interaction_status: format_npc_interaction_status(result.interaction_status).to_string(),
|
||||
action_text: result.action_text,
|
||||
result_text: result.result_text,
|
||||
story_text: result.story_text,
|
||||
battle_mode: result
|
||||
.battle_mode
|
||||
.map(|mode| format_npc_interaction_battle_mode(mode).to_string()),
|
||||
encounter_closed: result.encounter_closed,
|
||||
affinity_changed: result.affinity_changed,
|
||||
previous_affinity: result.previous_affinity,
|
||||
next_affinity: result.next_affinity,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_npc_battle_interaction_record(
|
||||
result: NpcBattleInteractionSnapshot,
|
||||
) -> NpcBattleInteractionRecord {
|
||||
NpcBattleInteractionRecord {
|
||||
npc_interaction: build_npc_interaction_record(result.interaction),
|
||||
battle_state: build_battle_state_record(result.battle_state),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_npc_relation_stance(value: DomainNpcRelationStance) -> &'static str {
|
||||
match value {
|
||||
DomainNpcRelationStance::Hostile => "hostile",
|
||||
DomainNpcRelationStance::Guarded => "guarded",
|
||||
DomainNpcRelationStance::Neutral => "neutral",
|
||||
DomainNpcRelationStance::Cooperative => "cooperative",
|
||||
DomainNpcRelationStance::Bonded => "bonded",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_npc_interaction_status(value: DomainNpcInteractionStatus) -> &'static str {
|
||||
match value {
|
||||
DomainNpcInteractionStatus::Previewed => "previewed",
|
||||
DomainNpcInteractionStatus::Dialogue => "dialogue",
|
||||
DomainNpcInteractionStatus::Resolved => "resolved",
|
||||
DomainNpcInteractionStatus::Recruited => "recruited",
|
||||
DomainNpcInteractionStatus::BattlePending => "battle_pending",
|
||||
DomainNpcInteractionStatus::Left => "left",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_npc_interaction_battle_mode(
|
||||
value: DomainNpcInteractionBattleMode,
|
||||
) -> &'static str {
|
||||
match value {
|
||||
DomainNpcInteractionBattleMode::Fight => "fight",
|
||||
DomainNpcInteractionBattleMode::Spar => "spar",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_inventory_item_source_kind(
|
||||
value: InventoryItemSourceKind,
|
||||
) -> module_inventory::InventoryItemSourceKind {
|
||||
match value {
|
||||
InventoryItemSourceKind::StoryReward => {
|
||||
module_inventory::InventoryItemSourceKind::StoryReward
|
||||
}
|
||||
InventoryItemSourceKind::QuestReward => {
|
||||
module_inventory::InventoryItemSourceKind::QuestReward
|
||||
}
|
||||
InventoryItemSourceKind::TreasureReward => {
|
||||
module_inventory::InventoryItemSourceKind::TreasureReward
|
||||
}
|
||||
InventoryItemSourceKind::NpcGift => module_inventory::InventoryItemSourceKind::NpcGift,
|
||||
InventoryItemSourceKind::NpcTrade => module_inventory::InventoryItemSourceKind::NpcTrade,
|
||||
InventoryItemSourceKind::CombatDrop => {
|
||||
module_inventory::InventoryItemSourceKind::CombatDrop
|
||||
}
|
||||
InventoryItemSourceKind::ForgeCraft => {
|
||||
module_inventory::InventoryItemSourceKind::ForgeCraft
|
||||
}
|
||||
InventoryItemSourceKind::ForgeReforge => {
|
||||
module_inventory::InventoryItemSourceKind::ForgeReforge
|
||||
}
|
||||
InventoryItemSourceKind::ManualPatch => {
|
||||
module_inventory::InventoryItemSourceKind::ManualPatch
|
||||
}
|
||||
}
|
||||
}
|
||||
1084
server-rs/crates/spacetime-client/src/mapper/puzzle.rs
Normal file
1084
server-rs/crates/spacetime-client/src/mapper/puzzle.rs
Normal file
File diff suppressed because it is too large
Load Diff
440
server-rs/crates/spacetime-client/src/mapper/runtime.rs
Normal file
440
server-rs/crates/spacetime-client/src/mapper/runtime.rs
Normal file
@@ -0,0 +1,440 @@
|
||||
use super::*;
|
||||
|
||||
impl From<module_runtime::CreationEntryTypeAdminUpsertInput> for CreationEntryTypeAdminUpsertInput {
|
||||
fn from(input: module_runtime::CreationEntryTypeAdminUpsertInput) -> Self {
|
||||
Self {
|
||||
id: input.id,
|
||||
title: input.title,
|
||||
subtitle: input.subtitle,
|
||||
badge: input.badge,
|
||||
image_src: input.image_src,
|
||||
visible: input.visible,
|
||||
open: input.open,
|
||||
sort_order: input.sort_order,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::RuntimeSettingGetInput> for RuntimeSettingGetInput {
|
||||
fn from(input: module_runtime::RuntimeSettingGetInput) -> Self {
|
||||
Self {
|
||||
user_id: input.user_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::RuntimeSettingUpsertInput> for RuntimeSettingUpsertInput {
|
||||
fn from(input: module_runtime::RuntimeSettingUpsertInput) -> Self {
|
||||
Self {
|
||||
user_id: input.user_id,
|
||||
music_volume: input.music_volume,
|
||||
platform_theme: map_runtime_platform_theme(input.platform_theme),
|
||||
updated_at_micros: input.updated_at_micros,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::RuntimeBrowseHistoryListInput> for RuntimeBrowseHistoryListInput {
|
||||
fn from(input: module_runtime::RuntimeBrowseHistoryListInput) -> Self {
|
||||
Self {
|
||||
user_id: input.user_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::RuntimeBrowseHistoryClearInput> for RuntimeBrowseHistoryClearInput {
|
||||
fn from(input: module_runtime::RuntimeBrowseHistoryClearInput) -> Self {
|
||||
Self {
|
||||
user_id: input.user_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::RuntimeBrowseHistorySyncInput> for RuntimeBrowseHistorySyncInput {
|
||||
fn from(input: module_runtime::RuntimeBrowseHistorySyncInput) -> Self {
|
||||
Self {
|
||||
user_id: input.user_id,
|
||||
entries: input.entries.into_iter().map(Into::into).collect(),
|
||||
updated_at_micros: input.updated_at_micros,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::RuntimeBrowseHistoryWriteInput> for RuntimeBrowseHistoryWriteInput {
|
||||
fn from(input: module_runtime::RuntimeBrowseHistoryWriteInput) -> Self {
|
||||
Self {
|
||||
owner_user_id: input.owner_user_id,
|
||||
profile_id: input.profile_id,
|
||||
world_name: input.world_name,
|
||||
subtitle: input.subtitle,
|
||||
summary_text: input.summary_text,
|
||||
cover_image_src: input.cover_image_src,
|
||||
theme_mode: input.theme_mode,
|
||||
author_display_name: input.author_display_name,
|
||||
visited_at: input.visited_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::RuntimeSnapshotGetInput> for RuntimeSnapshotGetInput {
|
||||
fn from(input: module_runtime::RuntimeSnapshotGetInput) -> Self {
|
||||
Self {
|
||||
user_id: input.user_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::RuntimeSnapshotDeleteInput> for RuntimeSnapshotDeleteInput {
|
||||
fn from(input: module_runtime::RuntimeSnapshotDeleteInput) -> Self {
|
||||
Self {
|
||||
user_id: input.user_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type CreationEntryConfigRecord =
|
||||
shared_contracts::creation_entry_config::CreationEntryConfigResponse;
|
||||
|
||||
pub(crate) fn map_creation_entry_config_procedure_result(
|
||||
result: CreationEntryConfigProcedureResult,
|
||||
) -> Result<CreationEntryConfigRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let snapshot = result
|
||||
.record
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("创作入口配置快照"))?;
|
||||
|
||||
Ok(module_runtime::build_creation_entry_config_response(
|
||||
map_creation_entry_config_snapshot(snapshot),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn build_creation_entry_config_record_from_rows(
|
||||
header: CreationEntryConfig,
|
||||
mut creation_types: Vec<CreationEntryTypeConfig>,
|
||||
) -> CreationEntryConfigRecord {
|
||||
creation_types.sort_by(|left, right| {
|
||||
left.sort_order
|
||||
.cmp(&right.sort_order)
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
|
||||
module_runtime::build_creation_entry_config_response(
|
||||
module_runtime::CreationEntryConfigSnapshot {
|
||||
config_id: header.config_id,
|
||||
start_card: module_runtime::CreationEntryStartCardSnapshot {
|
||||
title: header.start_title,
|
||||
description: header.start_description,
|
||||
idle_badge: header.start_idle_badge,
|
||||
busy_badge: header.start_busy_badge,
|
||||
},
|
||||
type_modal: module_runtime::CreationEntryTypeModalSnapshot {
|
||||
title: header.modal_title,
|
||||
description: header.modal_description,
|
||||
},
|
||||
creation_types: creation_types
|
||||
.into_iter()
|
||||
.map(|item| module_runtime::CreationEntryTypeSnapshot {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
subtitle: item.subtitle,
|
||||
badge: item.badge,
|
||||
image_src: item.image_src,
|
||||
visible: item.visible,
|
||||
open: item.open,
|
||||
sort_order: item.sort_order,
|
||||
updated_at_micros: item.updated_at.to_micros_since_unix_epoch(),
|
||||
})
|
||||
.collect(),
|
||||
updated_at_micros: header.updated_at.to_micros_since_unix_epoch(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn map_creation_entry_config_snapshot(
|
||||
snapshot: CreationEntryConfigSnapshot,
|
||||
) -> module_runtime::CreationEntryConfigSnapshot {
|
||||
module_runtime::CreationEntryConfigSnapshot {
|
||||
config_id: snapshot.config_id,
|
||||
start_card: module_runtime::CreationEntryStartCardSnapshot {
|
||||
title: snapshot.start_card.title,
|
||||
description: snapshot.start_card.description,
|
||||
idle_badge: snapshot.start_card.idle_badge,
|
||||
busy_badge: snapshot.start_card.busy_badge,
|
||||
},
|
||||
type_modal: module_runtime::CreationEntryTypeModalSnapshot {
|
||||
title: snapshot.type_modal.title,
|
||||
description: snapshot.type_modal.description,
|
||||
},
|
||||
creation_types: snapshot
|
||||
.creation_types
|
||||
.into_iter()
|
||||
.map(|item| module_runtime::CreationEntryTypeSnapshot {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
subtitle: item.subtitle,
|
||||
badge: item.badge,
|
||||
image_src: item.image_src,
|
||||
visible: item.visible,
|
||||
open: item.open,
|
||||
sort_order: item.sort_order,
|
||||
updated_at_micros: item.updated_at_micros,
|
||||
})
|
||||
.collect(),
|
||||
updated_at_micros: snapshot.updated_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_setting_procedure_result(
|
||||
result: RuntimeSettingProcedureResult,
|
||||
) -> Result<RuntimeSettingsRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let snapshot = result
|
||||
.record
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("runtime settings 快照"))?;
|
||||
|
||||
Ok(build_runtime_setting_record(map_runtime_setting_snapshot(
|
||||
snapshot,
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_tracking_event_procedure_result(
|
||||
result: RuntimeTrackingEventProcedureResult,
|
||||
) -> Result<(), SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_snapshot_procedure_result(
|
||||
result: RuntimeSnapshotProcedureResult,
|
||||
) -> Result<Option<RuntimeSnapshotRecord>, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
result
|
||||
.record
|
||||
.map(|snapshot| {
|
||||
build_runtime_snapshot_record(map_runtime_snapshot_snapshot(snapshot))
|
||||
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_snapshot_required_procedure_result(
|
||||
result: RuntimeSnapshotProcedureResult,
|
||||
) -> Result<RuntimeSnapshotRecord, SpacetimeClientError> {
|
||||
map_runtime_snapshot_procedure_result(result)?
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("runtime snapshot 快照"))
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_snapshot_delete_procedure_result(
|
||||
result: RuntimeSnapshotProcedureResult,
|
||||
) -> Result<bool, SpacetimeClientError> {
|
||||
map_runtime_snapshot_procedure_result(result).map(|record| record.is_some())
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_setting_snapshot(
|
||||
snapshot: RuntimeSettingSnapshot,
|
||||
) -> module_runtime::RuntimeSettingSnapshot {
|
||||
module_runtime::RuntimeSettingSnapshot {
|
||||
user_id: snapshot.user_id,
|
||||
music_volume: snapshot.music_volume,
|
||||
platform_theme: map_runtime_platform_theme_back(snapshot.platform_theme),
|
||||
created_at_micros: snapshot.created_at_micros,
|
||||
updated_at_micros: snapshot.updated_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_platform_theme(
|
||||
value: DomainRuntimePlatformTheme,
|
||||
) -> crate::module_bindings::RuntimePlatformTheme {
|
||||
match value {
|
||||
DomainRuntimePlatformTheme::Light => crate::module_bindings::RuntimePlatformTheme::Light,
|
||||
DomainRuntimePlatformTheme::Dark => crate::module_bindings::RuntimePlatformTheme::Dark,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_platform_theme_back(
|
||||
value: crate::module_bindings::RuntimePlatformTheme,
|
||||
) -> DomainRuntimePlatformTheme {
|
||||
match value {
|
||||
crate::module_bindings::RuntimePlatformTheme::Light => DomainRuntimePlatformTheme::Light,
|
||||
crate::module_bindings::RuntimePlatformTheme::Dark => DomainRuntimePlatformTheme::Dark,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_tracking_scope_kind(
|
||||
value: DomainRuntimeTrackingScopeKind,
|
||||
) -> crate::module_bindings::RuntimeTrackingScopeKind {
|
||||
match value {
|
||||
DomainRuntimeTrackingScopeKind::Site => {
|
||||
crate::module_bindings::RuntimeTrackingScopeKind::Site
|
||||
}
|
||||
DomainRuntimeTrackingScopeKind::Work => {
|
||||
crate::module_bindings::RuntimeTrackingScopeKind::Work
|
||||
}
|
||||
DomainRuntimeTrackingScopeKind::Module => {
|
||||
crate::module_bindings::RuntimeTrackingScopeKind::Module
|
||||
}
|
||||
DomainRuntimeTrackingScopeKind::User => {
|
||||
crate::module_bindings::RuntimeTrackingScopeKind::User
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_tracking_scope_kind_back(
|
||||
value: crate::module_bindings::RuntimeTrackingScopeKind,
|
||||
) -> DomainRuntimeTrackingScopeKind {
|
||||
match value {
|
||||
crate::module_bindings::RuntimeTrackingScopeKind::Site => {
|
||||
DomainRuntimeTrackingScopeKind::Site
|
||||
}
|
||||
crate::module_bindings::RuntimeTrackingScopeKind::Work => {
|
||||
DomainRuntimeTrackingScopeKind::Work
|
||||
}
|
||||
crate::module_bindings::RuntimeTrackingScopeKind::Module => {
|
||||
DomainRuntimeTrackingScopeKind::Module
|
||||
}
|
||||
crate::module_bindings::RuntimeTrackingScopeKind::User => {
|
||||
DomainRuntimeTrackingScopeKind::User
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_json_value(
|
||||
value: &str,
|
||||
label: &str,
|
||||
) -> Result<serde_json::Value, SpacetimeClientError> {
|
||||
serde_json::from_str::<serde_json::Value>(value)
|
||||
.map_err(|error| SpacetimeClientError::Runtime(format!("{label} 非法: {error}")))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_json_array(
|
||||
value: &str,
|
||||
label: &str,
|
||||
) -> Result<Vec<serde_json::Value>, SpacetimeClientError> {
|
||||
match parse_json_value(value, label)? {
|
||||
serde_json::Value::Array(entries) => Ok(entries),
|
||||
_ => Err(SpacetimeClientError::Runtime(format!(
|
||||
"{label} 必须是 JSON array"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_json_string_array(
|
||||
value: &str,
|
||||
label: &str,
|
||||
) -> Result<Vec<String>, SpacetimeClientError> {
|
||||
parse_json_array(value, label)?
|
||||
.into_iter()
|
||||
.map(|entry| match entry {
|
||||
serde_json::Value::String(value) => Ok(value),
|
||||
_ => Err(SpacetimeClientError::Runtime(format!(
|
||||
"{label} 必须是 string array"
|
||||
))),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_supported_actions_json(
|
||||
value: &str,
|
||||
) -> Result<Vec<CustomWorldSupportedActionRecord>, SpacetimeClientError> {
|
||||
parse_json_array(value, "custom world agent supported_actions_json")?
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
let object = entry.as_object().ok_or_else(|| {
|
||||
SpacetimeClientError::Runtime(
|
||||
"custom world supported action 必须是 JSON object".to_string(),
|
||||
)
|
||||
})?;
|
||||
let action = object
|
||||
.get("action")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
SpacetimeClientError::Runtime(
|
||||
"custom world supported action.action 缺失".to_string(),
|
||||
)
|
||||
})?;
|
||||
let enabled = object
|
||||
.get("enabled")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.ok_or_else(|| {
|
||||
SpacetimeClientError::Runtime(
|
||||
"custom world supported action.enabled 缺失".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(CustomWorldSupportedActionRecord {
|
||||
action: action.to_string(),
|
||||
enabled,
|
||||
reason: object
|
||||
.get("reason")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BigFishRuntimeParamsRecord {
|
||||
pub level_count: u32,
|
||||
pub merge_count_per_upgrade: u32,
|
||||
pub spawn_target_count: u32,
|
||||
pub leader_move_speed: f32,
|
||||
pub follower_catch_up_speed: f32,
|
||||
pub offscreen_cull_seconds: f32,
|
||||
pub prey_spawn_delta_levels: Vec<u32>,
|
||||
pub threat_spawn_delta_levels: Vec<u32>,
|
||||
pub win_level: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BigFishGameDraftRecord {
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub core_fun: String,
|
||||
pub ecology_theme: String,
|
||||
pub levels: Vec<BigFishLevelBlueprintRecord>,
|
||||
pub background: BigFishBackgroundBlueprintRecord,
|
||||
pub runtime_params: BigFishRuntimeParamsRecord,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BigFishRuntimeEntityRecord {
|
||||
pub entity_id: String,
|
||||
pub level: u32,
|
||||
pub position: BigFishVector2Record,
|
||||
pub radius: f32,
|
||||
pub offscreen_seconds: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BigFishRuntimeRunRecord {
|
||||
pub run_id: String,
|
||||
pub session_id: String,
|
||||
pub status: String,
|
||||
pub tick: u64,
|
||||
pub player_level: u32,
|
||||
pub win_level: u32,
|
||||
pub leader_entity_id: Option<String>,
|
||||
pub owned_entities: Vec<BigFishRuntimeEntityRecord>,
|
||||
pub wild_entities: Vec<BigFishRuntimeEntityRecord>,
|
||||
pub camera_center: BigFishVector2Record,
|
||||
pub last_input: BigFishVector2Record,
|
||||
pub event_log: Vec<String>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
1326
server-rs/crates/spacetime-client/src/mapper/runtime_profile.rs
Normal file
1326
server-rs/crates/spacetime-client/src/mapper/runtime_profile.rs
Normal file
File diff suppressed because it is too large
Load Diff
417
server-rs/crates/spacetime-client/src/mapper/square_hole.rs
Normal file
417
server-rs/crates/spacetime-client/src/mapper/square_hole.rs
Normal file
@@ -0,0 +1,417 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn map_square_hole_agent_session_procedure_result(
|
||||
result: SquareHoleAgentSessionProcedureResult,
|
||||
) -> Result<SquareHoleAgentSessionRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let session = result
|
||||
.session
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole agent session 快照"))?;
|
||||
|
||||
Ok(map_square_hole_agent_session_snapshot(session))
|
||||
}
|
||||
|
||||
pub(crate) fn map_square_hole_work_procedure_result(
|
||||
result: SquareHoleWorkProcedureResult,
|
||||
) -> Result<SquareHoleWorkProfileRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let work = result
|
||||
.work
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole work 快照"))?;
|
||||
|
||||
Ok(map_square_hole_work_snapshot(work))
|
||||
}
|
||||
|
||||
pub(crate) fn map_square_hole_works_procedure_result(
|
||||
result: SquareHoleWorksProcedureResult,
|
||||
) -> Result<Vec<SquareHoleWorkProfileRecord>, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
Ok(result
|
||||
.items
|
||||
.into_iter()
|
||||
.map(map_square_hole_work_snapshot)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(crate) fn map_square_hole_run_procedure_result(
|
||||
result: SquareHoleRunProcedureResult,
|
||||
) -> Result<SquareHoleRunRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let run = result
|
||||
.run
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole run 快照"))?;
|
||||
Ok(map_square_hole_run_snapshot(run))
|
||||
}
|
||||
|
||||
pub(crate) fn map_square_hole_drop_shape_procedure_result(
|
||||
result: SquareHoleDropShapeProcedureResult,
|
||||
) -> Result<SquareHoleDropConfirmationRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let run = result
|
||||
.run
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole drop run 快照"))?;
|
||||
let feedback = result
|
||||
.feedback
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole drop feedback 快照"))?;
|
||||
let run = map_square_hole_run_snapshot(run);
|
||||
|
||||
Ok(SquareHoleDropConfirmationRecord {
|
||||
status: result.status,
|
||||
accepted: feedback.accepted,
|
||||
reject_reason: feedback.reject_reason.clone(),
|
||||
failure_reason: result.failure_reason,
|
||||
feedback: map_square_hole_feedback_snapshot(feedback),
|
||||
run,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_square_hole_agent_session_snapshot(
|
||||
snapshot: SquareHoleAgentSessionSnapshot,
|
||||
) -> SquareHoleAgentSessionRecord {
|
||||
let config = map_square_hole_creator_config(snapshot.config);
|
||||
SquareHoleAgentSessionRecord {
|
||||
session_id: snapshot.session_id,
|
||||
current_turn: snapshot.current_turn,
|
||||
progress_percent: snapshot.progress_percent,
|
||||
stage: normalize_square_hole_stage(&snapshot.stage).to_string(),
|
||||
anchor_pack: build_square_hole_anchor_pack(&config),
|
||||
config,
|
||||
draft: snapshot.draft.map(map_square_hole_result_draft),
|
||||
messages: snapshot
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(map_square_hole_agent_message_snapshot)
|
||||
.collect(),
|
||||
last_assistant_reply: empty_string_to_none(snapshot.last_assistant_reply),
|
||||
published_profile_id: snapshot.published_profile_id,
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_square_hole_creator_config(
|
||||
snapshot: SquareHoleCreatorConfigSnapshot,
|
||||
) -> SquareHoleCreatorConfigRecord {
|
||||
SquareHoleCreatorConfigRecord {
|
||||
theme_text: snapshot.theme_text,
|
||||
twist_rule: snapshot.twist_rule,
|
||||
shape_count: snapshot.shape_count,
|
||||
difficulty: snapshot.difficulty,
|
||||
shape_options: snapshot
|
||||
.shape_options
|
||||
.into_iter()
|
||||
.map(map_square_hole_shape_option)
|
||||
.collect(),
|
||||
hole_options: snapshot
|
||||
.hole_options
|
||||
.into_iter()
|
||||
.map(map_square_hole_hole_option)
|
||||
.collect(),
|
||||
background_prompt: snapshot.background_prompt,
|
||||
cover_image_src: empty_string_to_none(snapshot.cover_image_src),
|
||||
background_image_src: empty_string_to_none(snapshot.background_image_src),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_square_hole_result_draft(snapshot: SquareHoleDraftSnapshot) -> SquareHoleResultDraftRecord {
|
||||
SquareHoleResultDraftRecord {
|
||||
profile_id: snapshot.profile_id,
|
||||
game_name: snapshot.game_name,
|
||||
theme_text: snapshot.theme_text,
|
||||
twist_rule: snapshot.twist_rule,
|
||||
summary: snapshot.summary_text,
|
||||
tags: snapshot.tags,
|
||||
cover_image_src: empty_string_to_none(snapshot.cover_image_src),
|
||||
background_prompt: snapshot.background_prompt,
|
||||
background_image_src: empty_string_to_none(snapshot.background_image_src),
|
||||
shape_options: snapshot
|
||||
.shape_options
|
||||
.into_iter()
|
||||
.map(map_square_hole_shape_option)
|
||||
.collect(),
|
||||
hole_options: snapshot
|
||||
.hole_options
|
||||
.into_iter()
|
||||
.map(map_square_hole_hole_option)
|
||||
.collect(),
|
||||
shape_count: snapshot.shape_count,
|
||||
difficulty: snapshot.difficulty,
|
||||
publish_ready: false,
|
||||
blockers: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_square_hole_agent_message_snapshot(
|
||||
snapshot: SquareHoleAgentMessageSnapshot,
|
||||
) -> SquareHoleAgentMessageRecord {
|
||||
SquareHoleAgentMessageRecord {
|
||||
id: snapshot.message_id,
|
||||
role: snapshot.role,
|
||||
kind: normalize_square_hole_message_kind(&snapshot.kind).to_string(),
|
||||
text: snapshot.text,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_square_hole_work_snapshot(snapshot: SquareHoleWorkSnapshot) -> SquareHoleWorkProfileRecord {
|
||||
SquareHoleWorkProfileRecord {
|
||||
work_id: snapshot.work_id,
|
||||
profile_id: snapshot.profile_id,
|
||||
owner_user_id: snapshot.owner_user_id,
|
||||
source_session_id: empty_string_to_none(snapshot.source_session_id),
|
||||
author_display_name: snapshot.author_display_name,
|
||||
game_name: snapshot.game_name,
|
||||
theme_text: snapshot.theme_text,
|
||||
twist_rule: snapshot.twist_rule,
|
||||
summary: snapshot.summary_text,
|
||||
tags: snapshot.tags,
|
||||
cover_image_src: empty_string_to_none(snapshot.cover_image_src),
|
||||
background_prompt: snapshot.background_prompt,
|
||||
background_image_src: empty_string_to_none(snapshot.background_image_src),
|
||||
shape_options: snapshot
|
||||
.shape_options
|
||||
.into_iter()
|
||||
.map(map_square_hole_shape_option)
|
||||
.collect(),
|
||||
hole_options: snapshot
|
||||
.hole_options
|
||||
.into_iter()
|
||||
.map(map_square_hole_hole_option)
|
||||
.collect(),
|
||||
shape_count: snapshot.shape_count,
|
||||
difficulty: snapshot.difficulty,
|
||||
publication_status: normalize_square_hole_publication_status(&snapshot.publication_status)
|
||||
.to_string(),
|
||||
play_count: snapshot.play_count,
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
published_at: snapshot.published_at_micros.map(format_timestamp_micros),
|
||||
publish_ready: snapshot.publish_ready,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_square_hole_gallery_view_row(
|
||||
row: SquareHoleGalleryViewRow,
|
||||
) -> SquareHoleWorkProfileRecord {
|
||||
SquareHoleWorkProfileRecord {
|
||||
work_id: row.work_id,
|
||||
profile_id: row.profile_id,
|
||||
owner_user_id: row.owner_user_id,
|
||||
source_session_id: empty_string_to_none(row.source_session_id),
|
||||
author_display_name: row.author_display_name,
|
||||
game_name: row.game_name,
|
||||
theme_text: row.theme_text,
|
||||
twist_rule: row.twist_rule,
|
||||
summary: row.summary_text,
|
||||
tags: row.tags,
|
||||
cover_image_src: empty_string_to_none(row.cover_image_src),
|
||||
background_prompt: row.background_prompt,
|
||||
background_image_src: empty_string_to_none(row.background_image_src),
|
||||
shape_options: row
|
||||
.shape_options
|
||||
.into_iter()
|
||||
.map(map_square_hole_shape_option)
|
||||
.collect(),
|
||||
hole_options: row
|
||||
.hole_options
|
||||
.into_iter()
|
||||
.map(map_square_hole_hole_option)
|
||||
.collect(),
|
||||
shape_count: row.shape_count,
|
||||
difficulty: row.difficulty,
|
||||
publication_status: normalize_square_hole_publication_status(&row.publication_status)
|
||||
.to_string(),
|
||||
play_count: row.play_count,
|
||||
updated_at: format_timestamp_micros(row.updated_at_micros),
|
||||
published_at: row.published_at_micros.map(format_timestamp_micros),
|
||||
publish_ready: row.publish_ready,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_square_hole_run_snapshot(snapshot: SquareHoleRunSnapshot) -> SquareHoleRunRecord {
|
||||
SquareHoleRunRecord {
|
||||
run_id: snapshot.run_id,
|
||||
profile_id: snapshot.profile_id,
|
||||
owner_user_id: snapshot.owner_user_id,
|
||||
status: normalize_square_hole_run_status(&snapshot.status).to_string(),
|
||||
snapshot_version: snapshot.snapshot_version,
|
||||
started_at_ms: i64_to_u64_ms(snapshot.started_at_ms),
|
||||
duration_limit_ms: i64_to_u64_ms(snapshot.duration_limit_ms),
|
||||
server_now_ms: Some(i64_to_u64_ms(snapshot.server_now_ms)),
|
||||
remaining_ms: i64_to_u64_ms(snapshot.remaining_ms),
|
||||
total_shape_count: snapshot.total_shape_count,
|
||||
completed_shape_count: snapshot.completed_shape_count,
|
||||
combo: snapshot.combo,
|
||||
best_combo: snapshot.best_combo,
|
||||
score: snapshot.score,
|
||||
rule_label: snapshot.rule_label,
|
||||
background_image_src: empty_string_to_none(snapshot.background_image_src),
|
||||
current_shape: snapshot.current_shape.map(map_square_hole_shape_snapshot),
|
||||
holes: snapshot
|
||||
.holes
|
||||
.into_iter()
|
||||
.map(map_square_hole_hole_snapshot)
|
||||
.collect(),
|
||||
last_feedback: snapshot
|
||||
.last_feedback
|
||||
.map(map_square_hole_feedback_snapshot),
|
||||
last_confirmed_action_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_square_hole_shape_snapshot(
|
||||
snapshot: SquareHoleShapeSnapshot,
|
||||
) -> SquareHoleShapeSnapshotRecord {
|
||||
SquareHoleShapeSnapshotRecord {
|
||||
shape_id: snapshot.shape_id,
|
||||
shape_kind: snapshot.shape_kind,
|
||||
label: snapshot.label,
|
||||
target_hole_id: snapshot.target_hole_id,
|
||||
color: snapshot.color,
|
||||
image_src: empty_string_to_none(snapshot.image_src),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_square_hole_hole_snapshot(snapshot: SquareHoleHoleSnapshot) -> SquareHoleHoleSnapshotRecord {
|
||||
SquareHoleHoleSnapshotRecord {
|
||||
hole_id: snapshot.hole_id,
|
||||
hole_kind: snapshot.hole_kind,
|
||||
label: snapshot.label,
|
||||
x: snapshot.x,
|
||||
y: snapshot.y,
|
||||
image_src: empty_string_to_none(snapshot.image_src),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_square_hole_shape_option(
|
||||
snapshot: SquareHoleShapeOptionSnapshot,
|
||||
) -> SquareHoleShapeOptionRecord {
|
||||
SquareHoleShapeOptionRecord {
|
||||
option_id: snapshot.option_id,
|
||||
shape_kind: snapshot.shape_kind,
|
||||
label: snapshot.label,
|
||||
target_hole_id: snapshot.target_hole_id,
|
||||
image_prompt: snapshot.image_prompt,
|
||||
image_src: empty_string_to_none(snapshot.image_src),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_square_hole_hole_option(
|
||||
snapshot: SquareHoleHoleOptionSnapshot,
|
||||
) -> SquareHoleHoleOptionRecord {
|
||||
SquareHoleHoleOptionRecord {
|
||||
hole_id: snapshot.hole_id,
|
||||
hole_kind: snapshot.hole_kind,
|
||||
label: snapshot.label,
|
||||
image_prompt: snapshot.image_prompt,
|
||||
image_src: empty_string_to_none(snapshot.image_src),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_square_hole_feedback_snapshot(
|
||||
snapshot: SquareHoleDropFeedbackSnapshot,
|
||||
) -> SquareHoleDropFeedbackRecord {
|
||||
SquareHoleDropFeedbackRecord {
|
||||
accepted: snapshot.accepted,
|
||||
reject_reason: snapshot
|
||||
.reject_reason
|
||||
.map(|value| normalize_square_hole_reject_reason(&value).to_string()),
|
||||
message: snapshot.message,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_square_hole_anchor_pack(
|
||||
config: &SquareHoleCreatorConfigRecord,
|
||||
) -> SquareHoleAnchorPackRecord {
|
||||
let shape_count = config.shape_count.to_string();
|
||||
let difficulty = config.difficulty.to_string();
|
||||
SquareHoleAnchorPackRecord {
|
||||
theme: build_square_hole_anchor_item("theme", "题材主题", config.theme_text.as_str()),
|
||||
twist_rule: build_square_hole_anchor_item(
|
||||
"twistRule",
|
||||
"反差规则",
|
||||
config.twist_rule.as_str(),
|
||||
),
|
||||
shape_count: build_square_hole_anchor_item("shapeCount", "形状数量", shape_count.as_str()),
|
||||
difficulty: build_square_hole_anchor_item("difficulty", "难度", difficulty.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_square_hole_anchor_item(
|
||||
key: &str,
|
||||
label: &str,
|
||||
value: &str,
|
||||
) -> SquareHoleAnchorItemRecord {
|
||||
SquareHoleAnchorItemRecord {
|
||||
key: key.to_string(),
|
||||
label: label.to_string(),
|
||||
value: value.to_string(),
|
||||
status: if value.trim().is_empty() {
|
||||
"missing"
|
||||
} else {
|
||||
"confirmed"
|
||||
}
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_square_hole_stage(value: &str) -> &str {
|
||||
match value {
|
||||
"Collecting" | "CollectingConfig" | "collecting" | "collecting_config" => {
|
||||
"collecting_config"
|
||||
}
|
||||
"ReadyToCompile" | "ready_to_compile" => "ready_to_compile",
|
||||
"DraftCompiled" | "DraftReady" | "draft_compiled" | "draft_ready" => "draft_ready",
|
||||
"Published" | "published" => "published",
|
||||
_ => value,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_square_hole_publication_status(value: &str) -> &str {
|
||||
match value {
|
||||
"Draft" | "draft" => "draft",
|
||||
"Published" | "published" => "published",
|
||||
_ => value,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_square_hole_run_status(value: &str) -> &str {
|
||||
match value {
|
||||
"Running" | "running" => "running",
|
||||
"Won" | "won" => "won",
|
||||
"Failed" | "failed" => "failed",
|
||||
"Stopped" | "stopped" => "stopped",
|
||||
_ => value,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_square_hole_message_kind(value: &str) -> &str {
|
||||
match value {
|
||||
"text" => "chat",
|
||||
_ => value,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_square_hole_reject_reason(value: &str) -> &str {
|
||||
match value {
|
||||
"RunNotActive" | "run_not_active" => "run_not_active",
|
||||
"SnapshotVersionMismatch" | "snapshot_version_mismatch" => "snapshot_version_mismatch",
|
||||
"HoleNotFound" | "hole_not_found" => "hole_not_found",
|
||||
"Incompatible" | "incompatible" => "incompatible",
|
||||
"TimeUp" | "time_up" => "time_up",
|
||||
_ => value,
|
||||
}
|
||||
}
|
||||
291
server-rs/crates/spacetime-client/src/mapper/story.rs
Normal file
291
server-rs/crates/spacetime-client/src/mapper/story.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
use super::*;
|
||||
|
||||
impl From<module_runtime::RuntimeSnapshotUpsertInput> for RuntimeSnapshotUpsertInput {
|
||||
fn from(input: module_runtime::RuntimeSnapshotUpsertInput) -> Self {
|
||||
Self {
|
||||
user_id: input.user_id,
|
||||
saved_at_micros: input.saved_at_micros,
|
||||
bottom_tab: input.bottom_tab,
|
||||
game_state_json: input.game_state_json,
|
||||
current_story_json: input.current_story_json,
|
||||
updated_at_micros: input.updated_at_micros,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomainStorySessionInput> for StorySessionInput {
|
||||
fn from(input: DomainStorySessionInput) -> Self {
|
||||
Self {
|
||||
story_session_id: input.story_session_id,
|
||||
runtime_session_id: input.runtime_session_id,
|
||||
actor_user_id: input.actor_user_id,
|
||||
world_profile_id: input.world_profile_id,
|
||||
initial_prompt: input.initial_prompt,
|
||||
opening_summary: input.opening_summary,
|
||||
created_at_micros: input.created_at_micros,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomainStoryContinueInput> for StoryContinueInput {
|
||||
fn from(input: DomainStoryContinueInput) -> Self {
|
||||
Self {
|
||||
story_session_id: input.story_session_id,
|
||||
event_id: input.event_id,
|
||||
narrative_text: input.narrative_text,
|
||||
choice_function_id: input.choice_function_id,
|
||||
updated_at_micros: input.updated_at_micros,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomainStorySessionStateInput> for StorySessionStateInput {
|
||||
fn from(input: DomainStorySessionStateInput) -> Self {
|
||||
Self {
|
||||
story_session_id: input.story_session_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_asset_history_list_result(
|
||||
result: AssetHistoryListResult,
|
||||
) -> Result<Vec<AssetHistoryEntryRecord>, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
Ok(result
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(map_asset_history_entry_snapshot)
|
||||
.map(build_asset_history_entry_record)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_browse_history_procedure_result(
|
||||
result: RuntimeBrowseHistoryProcedureResult,
|
||||
) -> Result<Vec<RuntimeBrowseHistoryRecord>, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
Ok(result
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(|snapshot| {
|
||||
build_runtime_browse_history_record(map_runtime_browse_history_snapshot(snapshot))
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(crate) fn map_story_session_procedure_result(
|
||||
result: StorySessionProcedureResult,
|
||||
) -> Result<StorySessionResultRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let session = result
|
||||
.session
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("story session 快照"))?;
|
||||
let event = result
|
||||
.event
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("story event 快照"))?;
|
||||
|
||||
Ok(StorySessionResultRecord {
|
||||
session: map_story_session_snapshot(session),
|
||||
event: map_story_event_snapshot(event),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_story_session_state_procedure_result(
|
||||
result: StorySessionStateProcedureResult,
|
||||
) -> Result<StorySessionStateRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let session = result
|
||||
.session
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("story session state 快照"))?;
|
||||
|
||||
Ok(StorySessionStateRecord {
|
||||
session: map_story_session_snapshot(session),
|
||||
events: result
|
||||
.events
|
||||
.into_iter()
|
||||
.map(map_story_event_snapshot)
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_asset_history_entry_snapshot(
|
||||
snapshot: AssetHistoryEntrySnapshot,
|
||||
) -> module_assets::AssetHistoryEntrySnapshot {
|
||||
module_assets::AssetHistoryEntrySnapshot {
|
||||
asset_object_id: snapshot.asset_object_id,
|
||||
asset_kind: snapshot.asset_kind,
|
||||
image_src: snapshot.image_src,
|
||||
owner_user_id: snapshot.owner_user_id,
|
||||
profile_id: snapshot.profile_id,
|
||||
entity_id: snapshot.entity_id,
|
||||
created_at_micros: snapshot.created_at_micros,
|
||||
updated_at_micros: snapshot.updated_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_browse_history_snapshot(
|
||||
snapshot: RuntimeBrowseHistorySnapshot,
|
||||
) -> module_runtime::RuntimeBrowseHistorySnapshot {
|
||||
module_runtime::RuntimeBrowseHistorySnapshot {
|
||||
browse_history_id: snapshot.browse_history_id,
|
||||
user_id: snapshot.user_id,
|
||||
owner_user_id: snapshot.owner_user_id,
|
||||
profile_id: snapshot.profile_id,
|
||||
world_name: snapshot.world_name,
|
||||
subtitle: snapshot.subtitle,
|
||||
summary_text: snapshot.summary_text,
|
||||
cover_image_src: snapshot.cover_image_src,
|
||||
theme_mode: map_runtime_browse_history_theme_mode_back(snapshot.theme_mode),
|
||||
author_display_name: snapshot.author_display_name,
|
||||
visited_at_micros: snapshot.visited_at_micros,
|
||||
created_at_micros: snapshot.created_at_micros,
|
||||
updated_at_micros: snapshot.updated_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_snapshot_snapshot(
|
||||
snapshot: RuntimeSnapshot,
|
||||
) -> module_runtime::RuntimeSnapshot {
|
||||
module_runtime::RuntimeSnapshot {
|
||||
user_id: snapshot.user_id,
|
||||
version: snapshot.version,
|
||||
saved_at_micros: snapshot.saved_at_micros,
|
||||
bottom_tab: snapshot.bottom_tab,
|
||||
game_state_json: snapshot.game_state_json,
|
||||
current_story_json: snapshot.current_story_json,
|
||||
created_at_micros: snapshot.created_at_micros,
|
||||
updated_at_micros: snapshot.updated_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_profile_save_archive_snapshot(
|
||||
snapshot: RuntimeProfileSaveArchiveSnapshot,
|
||||
) -> module_runtime::RuntimeProfileSaveArchiveSnapshot {
|
||||
module_runtime::RuntimeProfileSaveArchiveSnapshot {
|
||||
archive_id: snapshot.archive_id,
|
||||
user_id: snapshot.user_id,
|
||||
world_key: snapshot.world_key,
|
||||
owner_user_id: snapshot.owner_user_id,
|
||||
profile_id: snapshot.profile_id,
|
||||
world_type: snapshot.world_type,
|
||||
world_name: snapshot.world_name,
|
||||
subtitle: snapshot.subtitle,
|
||||
summary_text: snapshot.summary_text,
|
||||
cover_image_src: snapshot.cover_image_src,
|
||||
saved_at_micros: snapshot.saved_at_micros,
|
||||
bottom_tab: snapshot.bottom_tab,
|
||||
game_state_json: snapshot.game_state_json,
|
||||
current_story_json: snapshot.current_story_json,
|
||||
created_at_micros: snapshot.created_at_micros,
|
||||
updated_at_micros: snapshot.updated_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_story_session_snapshot(snapshot: StorySessionSnapshot) -> StorySessionRecord {
|
||||
StorySessionRecord {
|
||||
story_session_id: snapshot.story_session_id,
|
||||
runtime_session_id: snapshot.runtime_session_id,
|
||||
actor_user_id: snapshot.actor_user_id,
|
||||
world_profile_id: snapshot.world_profile_id,
|
||||
initial_prompt: snapshot.initial_prompt,
|
||||
opening_summary: snapshot.opening_summary,
|
||||
latest_narrative_text: snapshot.latest_narrative_text,
|
||||
latest_choice_function_id: snapshot.latest_choice_function_id,
|
||||
status: map_story_session_status(snapshot.status)
|
||||
.as_str()
|
||||
.to_string(),
|
||||
version: snapshot.version,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_story_event_snapshot(snapshot: StoryEventSnapshot) -> StoryEventRecord {
|
||||
StoryEventRecord {
|
||||
event_id: snapshot.event_id,
|
||||
story_session_id: snapshot.story_session_id,
|
||||
event_kind: map_story_event_kind(snapshot.event_kind)
|
||||
.as_str()
|
||||
.to_string(),
|
||||
narrative_text: snapshot.narrative_text,
|
||||
choice_function_id: snapshot.choice_function_id,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_browse_history_theme_mode_back(
|
||||
value: crate::module_bindings::RuntimeBrowseHistoryThemeMode,
|
||||
) -> module_runtime::RuntimeBrowseHistoryThemeMode {
|
||||
match value {
|
||||
crate::module_bindings::RuntimeBrowseHistoryThemeMode::Martial => {
|
||||
module_runtime::RuntimeBrowseHistoryThemeMode::Martial
|
||||
}
|
||||
crate::module_bindings::RuntimeBrowseHistoryThemeMode::Arcane => {
|
||||
module_runtime::RuntimeBrowseHistoryThemeMode::Arcane
|
||||
}
|
||||
crate::module_bindings::RuntimeBrowseHistoryThemeMode::Machina => {
|
||||
module_runtime::RuntimeBrowseHistoryThemeMode::Machina
|
||||
}
|
||||
crate::module_bindings::RuntimeBrowseHistoryThemeMode::Tide => {
|
||||
module_runtime::RuntimeBrowseHistoryThemeMode::Tide
|
||||
}
|
||||
crate::module_bindings::RuntimeBrowseHistoryThemeMode::Rift => {
|
||||
module_runtime::RuntimeBrowseHistoryThemeMode::Rift
|
||||
}
|
||||
crate::module_bindings::RuntimeBrowseHistoryThemeMode::Mythic => {
|
||||
module_runtime::RuntimeBrowseHistoryThemeMode::Mythic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_story_session_status(value: StorySessionStatus) -> DomainStorySessionStatus {
|
||||
match value {
|
||||
StorySessionStatus::Active => DomainStorySessionStatus::Active,
|
||||
StorySessionStatus::Completed => DomainStorySessionStatus::Completed,
|
||||
StorySessionStatus::Archived => DomainStorySessionStatus::Archived,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_story_event_kind(value: StoryEventKind) -> DomainStoryEventKind {
|
||||
match value {
|
||||
StoryEventKind::SessionStarted => DomainStoryEventKind::SessionStarted,
|
||||
StoryEventKind::StoryContinued => DomainStoryEventKind::StoryContinued,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct VisualNovelRuntimeEventRecordInput {
|
||||
pub event_id: String,
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: Option<String>,
|
||||
pub event_kind: String,
|
||||
pub client_event_id: Option<String>,
|
||||
pub history_entry_id: Option<String>,
|
||||
pub payload_json: String,
|
||||
pub occurred_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct VisualNovelRuntimeEventRecord {
|
||||
pub event_id: String,
|
||||
pub run_id: Option<String>,
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: Option<String>,
|
||||
pub event_kind: String,
|
||||
pub client_event_id: Option<String>,
|
||||
pub history_entry_id: Option<String>,
|
||||
pub payload: serde_json::Value,
|
||||
pub occurred_at: String,
|
||||
}
|
||||
252
server-rs/crates/spacetime-client/src/mapper/visual_novel.rs
Normal file
252
server-rs/crates/spacetime-client/src/mapper/visual_novel.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn map_visual_novel_agent_session_procedure_result(
|
||||
result: VisualNovelAgentSessionProcedureResult,
|
||||
) -> Result<VisualNovelAgentSessionRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let session = result
|
||||
.session
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel agent session 快照"))?;
|
||||
|
||||
Ok(map_visual_novel_agent_session_snapshot(session))
|
||||
}
|
||||
|
||||
pub(crate) fn map_visual_novel_work_procedure_result(
|
||||
result: VisualNovelWorkProcedureResult,
|
||||
) -> Result<VisualNovelWorkProfileRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let work = result
|
||||
.work
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel work 快照"))?;
|
||||
|
||||
Ok(map_visual_novel_work_snapshot(work))
|
||||
}
|
||||
|
||||
pub(crate) fn map_visual_novel_works_procedure_result(
|
||||
result: VisualNovelWorksProcedureResult,
|
||||
) -> Result<Vec<VisualNovelWorkProfileRecord>, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
Ok(result
|
||||
.items
|
||||
.into_iter()
|
||||
.map(map_visual_novel_work_snapshot)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(crate) fn map_visual_novel_run_procedure_result(
|
||||
result: VisualNovelRunProcedureResult,
|
||||
) -> Result<VisualNovelRunRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let run = result
|
||||
.run
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel run 快照"))?;
|
||||
|
||||
Ok(map_visual_novel_run_snapshot(run))
|
||||
}
|
||||
|
||||
pub(crate) fn map_visual_novel_history_procedure_result(
|
||||
result: VisualNovelHistoryProcedureResult,
|
||||
) -> Result<Vec<VisualNovelHistoryEntryRecord>, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
Ok(result
|
||||
.items
|
||||
.into_iter()
|
||||
.map(map_visual_novel_history_entry)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(crate) fn map_visual_novel_runtime_event_procedure_result(
|
||||
result: VisualNovelRuntimeEventProcedureResult,
|
||||
) -> Result<VisualNovelRuntimeEventRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let event = result
|
||||
.event
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel runtime event 快照"))?;
|
||||
|
||||
Ok(map_visual_novel_runtime_event(event))
|
||||
}
|
||||
|
||||
fn map_visual_novel_agent_session_snapshot(
|
||||
snapshot: VisualNovelAgentSessionSnapshot,
|
||||
) -> VisualNovelAgentSessionRecord {
|
||||
VisualNovelAgentSessionRecord {
|
||||
session_id: snapshot.session_id,
|
||||
owner_user_id: snapshot.owner_user_id,
|
||||
source_mode: snapshot.source_mode,
|
||||
status: snapshot.status,
|
||||
seed_text: snapshot.seed_text,
|
||||
source_asset_ids: snapshot.source_asset_ids,
|
||||
current_turn: snapshot.current_turn,
|
||||
progress_percent: snapshot.progress_percent,
|
||||
messages: snapshot
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(map_visual_novel_agent_message)
|
||||
.collect(),
|
||||
draft: snapshot.draft.map(visual_novel_json_to_value),
|
||||
pending_action: snapshot.pending_action.map(visual_novel_json_to_value),
|
||||
last_assistant_reply: snapshot.last_assistant_reply,
|
||||
published_profile_id: snapshot.published_profile_id,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_visual_novel_agent_message(
|
||||
snapshot: VisualNovelAgentMessageSnapshot,
|
||||
) -> VisualNovelAgentMessageRecord {
|
||||
VisualNovelAgentMessageRecord {
|
||||
message_id: snapshot.message_id,
|
||||
session_id: snapshot.session_id,
|
||||
role: snapshot.role,
|
||||
kind: snapshot.kind,
|
||||
text: snapshot.text,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_visual_novel_work_snapshot(
|
||||
snapshot: VisualNovelWorkSnapshot,
|
||||
) -> VisualNovelWorkProfileRecord {
|
||||
VisualNovelWorkProfileRecord {
|
||||
work_id: snapshot.work_id,
|
||||
profile_id: snapshot.profile_id,
|
||||
owner_user_id: snapshot.owner_user_id,
|
||||
source_session_id: snapshot.source_session_id,
|
||||
author_display_name: snapshot.author_display_name,
|
||||
work_title: snapshot.work_title,
|
||||
work_description: snapshot.work_description,
|
||||
tags: snapshot.tags,
|
||||
cover_image_src: snapshot.cover_image_src,
|
||||
source_asset_ids: snapshot.source_asset_ids,
|
||||
draft: visual_novel_json_to_value(snapshot.draft),
|
||||
publication_status: snapshot.publication_status,
|
||||
publish_ready: snapshot.publish_ready,
|
||||
play_count: snapshot.play_count,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
published_at: snapshot.published_at_micros.map(format_timestamp_micros),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_visual_novel_gallery_view_row(
|
||||
row: VisualNovelGalleryViewRow,
|
||||
) -> VisualNovelWorkProfileRecord {
|
||||
VisualNovelWorkProfileRecord {
|
||||
work_id: row.work_id,
|
||||
profile_id: row.profile_id,
|
||||
owner_user_id: row.owner_user_id,
|
||||
source_session_id: row.source_session_id,
|
||||
author_display_name: row.author_display_name,
|
||||
work_title: row.work_title,
|
||||
work_description: row.work_description,
|
||||
tags: row.tags,
|
||||
cover_image_src: row.cover_image_src,
|
||||
source_asset_ids: row.source_asset_ids,
|
||||
// 中文注释:公开列表 view 不暴露完整 draft,详情页仍通过 detail procedure 读取。
|
||||
draft: serde_json::Value::Null,
|
||||
publication_status: row.publication_status,
|
||||
publish_ready: row.publish_ready,
|
||||
play_count: row.play_count,
|
||||
created_at: format_timestamp_micros(row.created_at_micros),
|
||||
updated_at: format_timestamp_micros(row.updated_at_micros),
|
||||
published_at: row.published_at_micros.map(format_timestamp_micros),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_visual_novel_run_snapshot(snapshot: VisualNovelRunSnapshot) -> VisualNovelRunRecord {
|
||||
VisualNovelRunRecord {
|
||||
run_id: snapshot.run_id,
|
||||
owner_user_id: snapshot.owner_user_id,
|
||||
profile_id: snapshot.profile_id,
|
||||
mode: snapshot.mode,
|
||||
status: snapshot.status,
|
||||
current_scene_id: snapshot.current_scene_id,
|
||||
current_phase_id: snapshot.current_phase_id,
|
||||
visible_character_ids: snapshot.visible_character_ids,
|
||||
flags: visual_novel_json_to_value(snapshot.flags),
|
||||
metrics: visual_novel_json_to_value(snapshot.metrics),
|
||||
history: snapshot
|
||||
.history
|
||||
.into_iter()
|
||||
.map(map_visual_novel_history_entry)
|
||||
.collect(),
|
||||
available_choices: visual_novel_json_to_value(snapshot.available_choices),
|
||||
text_mode_enabled: snapshot.text_mode_enabled,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_visual_novel_history_entry(
|
||||
snapshot: VisualNovelRuntimeHistoryEntrySnapshot,
|
||||
) -> VisualNovelHistoryEntryRecord {
|
||||
VisualNovelHistoryEntryRecord {
|
||||
entry_id: snapshot.entry_id,
|
||||
run_id: snapshot.run_id,
|
||||
owner_user_id: snapshot.owner_user_id,
|
||||
profile_id: snapshot.profile_id,
|
||||
turn_index: snapshot.turn_index,
|
||||
source: snapshot.source,
|
||||
action_text: snapshot.action_text,
|
||||
steps: visual_novel_json_to_value(snapshot.steps),
|
||||
snapshot_before_hash: snapshot.snapshot_before_hash,
|
||||
snapshot_after_hash: snapshot.snapshot_after_hash,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_visual_novel_runtime_event(
|
||||
snapshot: VisualNovelRuntimeEventSnapshot,
|
||||
) -> VisualNovelRuntimeEventRecord {
|
||||
VisualNovelRuntimeEventRecord {
|
||||
event_id: snapshot.event_id,
|
||||
run_id: snapshot.run_id,
|
||||
owner_user_id: snapshot.owner_user_id,
|
||||
profile_id: snapshot.profile_id,
|
||||
event_kind: snapshot.event_kind,
|
||||
client_event_id: snapshot.client_event_id,
|
||||
history_entry_id: snapshot.history_entry_id,
|
||||
payload: visual_novel_json_to_value(snapshot.payload),
|
||||
occurred_at: format_timestamp_micros(snapshot.occurred_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
fn visual_novel_json_to_value(value: VisualNovelJsonValue) -> serde_json::Value {
|
||||
match value {
|
||||
VisualNovelJsonValue::Null => serde_json::Value::Null,
|
||||
VisualNovelJsonValue::Bool(value) => serde_json::Value::Bool(value),
|
||||
VisualNovelJsonValue::Number(value) => serde_json::Number::from_f64(value)
|
||||
.map(serde_json::Value::Number)
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
VisualNovelJsonValue::String(value) => serde_json::Value::String(value),
|
||||
VisualNovelJsonValue::Array(items) => {
|
||||
serde_json::Value::Array(items.into_iter().map(visual_novel_json_to_value).collect())
|
||||
}
|
||||
VisualNovelJsonValue::Object(fields) => {
|
||||
let object = fields
|
||||
.into_iter()
|
||||
.map(|field| (field.key, visual_novel_json_to_value(field.value)))
|
||||
.collect();
|
||||
serde_json::Value::Object(object)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ArrowRight, Loader2, Sparkles } from 'lucide-react';
|
||||
import { ArrowRight, Loader2 } from 'lucide-react';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import {
|
||||
type Dispatch,
|
||||
@@ -332,6 +332,11 @@ import {
|
||||
filterGeneralPublicWorks,
|
||||
} from './platformEdutainmentVisibility';
|
||||
import { PlatformEntryCreationTypeModal } from './PlatformEntryCreationTypeModal';
|
||||
import {
|
||||
PuzzleOnboardingLoginOverlay,
|
||||
type PuzzleOnboardingPhase,
|
||||
PuzzleOnboardingView,
|
||||
} from './PlatformEntryFlowShellImpl/PuzzleOnboardingView';
|
||||
import type { PlatformCreationTypeId } from './platformEntryCreationTypes';
|
||||
import {
|
||||
derivePlatformCreationTypes,
|
||||
@@ -402,8 +407,6 @@ type PuzzleRuntimeReturnStage =
|
||||
| 'platform';
|
||||
type PuzzleRuntimeAuthMode = 'default' | 'isolated';
|
||||
|
||||
type PuzzleOnboardingPhase = 'input' | 'generating' | 'generated';
|
||||
|
||||
type PuzzleOnboardingDraft = {
|
||||
promptText: string;
|
||||
item: PuzzleWorkSummary;
|
||||
@@ -1340,134 +1343,6 @@ function markPuzzleOnboardingSeen() {
|
||||
}
|
||||
}
|
||||
|
||||
function PuzzleOnboardingView({
|
||||
prompt,
|
||||
phase,
|
||||
error,
|
||||
onPromptChange,
|
||||
onSubmit,
|
||||
onSkip,
|
||||
}: {
|
||||
prompt: string;
|
||||
phase: PuzzleOnboardingPhase;
|
||||
error: string | null;
|
||||
onPromptChange: (value: string) => void;
|
||||
onSubmit: () => void;
|
||||
onSkip: () => void;
|
||||
}) {
|
||||
const isGenerating = phase === 'generating';
|
||||
const isGenerated = phase === 'generated';
|
||||
const canSubmit = Boolean(prompt.trim()) && !isGenerating && !isGenerated;
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-[radial-gradient(circle_at_30%_15%,rgba(251,191,36,0.22),transparent_30%),linear-gradient(135deg,#0f172a,#111827_46%,#1e1b4b)] px-4 py-8 text-white">
|
||||
<div className="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.045)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[length:38px_38px] opacity-30" />
|
||||
<button
|
||||
type="button"
|
||||
disabled={isGenerating}
|
||||
onClick={onSkip}
|
||||
className="absolute right-4 top-4 z-10 inline-flex min-h-10 items-center justify-center rounded-full border border-white/14 bg-black/24 px-4 text-sm font-black text-white/86 shadow-[0_12px_28px_rgba(0,0,0,0.22)] backdrop-blur transition hover:border-amber-200/45 hover:text-amber-100 disabled:cursor-not-allowed disabled:opacity-45 sm:right-6 sm:top-6"
|
||||
>
|
||||
跳过
|
||||
</button>
|
||||
<section className="relative flex w-full max-w-[34rem] flex-col items-center gap-5 text-center">
|
||||
<div className="grid h-14 w-14 place-items-center rounded-[1.2rem] border border-amber-200/32 bg-amber-200/14 text-amber-100 shadow-[0_18px_48px_rgba(251,191,36,0.18)]">
|
||||
{isGenerating ? (
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="h-6 w-6" />
|
||||
)}
|
||||
</div>
|
||||
<h1 className="text-[2rem] font-black leading-tight sm:text-[2.85rem]">
|
||||
{PUZZLE_ONBOARDING_COPY}
|
||||
</h1>
|
||||
<form
|
||||
className="flex w-full flex-col gap-3"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
onSubmit();
|
||||
}}
|
||||
>
|
||||
<textarea
|
||||
value={prompt}
|
||||
disabled={isGenerating || isGenerated}
|
||||
onChange={(event) => onPromptChange(event.target.value)}
|
||||
placeholder="把你的梦讲给我听吧"
|
||||
rows={4}
|
||||
className="min-h-32 w-full resize-none rounded-[1.25rem] border border-white/14 bg-black/28 px-4 py-4 text-base font-semibold leading-7 text-white shadow-[0_18px_50px_rgba(0,0,0,0.24)] outline-none backdrop-blur placeholder:text-white/42 focus:border-amber-200/70 focus:ring-2 focus:ring-amber-200/20 disabled:opacity-70"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!canSubmit}
|
||||
className="inline-flex min-h-12 items-center justify-center gap-2 rounded-[1rem] bg-amber-200 px-5 text-sm font-black text-slate-950 transition hover:bg-amber-100 disabled:cursor-not-allowed disabled:opacity-45"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
生成
|
||||
</>
|
||||
) : (
|
||||
'生成'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
{error ? (
|
||||
<div className="w-full rounded-[1rem] border border-red-300/30 bg-red-500/14 px-4 py-3 text-sm font-semibold text-red-50">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PuzzleOnboardingLoginOverlay({
|
||||
isSaving,
|
||||
error,
|
||||
onLogin,
|
||||
}: {
|
||||
isSaving: boolean;
|
||||
error: string | null;
|
||||
onLogin: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-[110] flex items-center justify-center bg-slate-950/72 px-4 py-6 text-white backdrop-blur-md">
|
||||
<section className="flex w-full max-w-[24rem] flex-col items-center gap-5 rounded-[1.35rem] border border-white/14 bg-slate-950/94 px-5 py-6 text-center shadow-[0_28px_90px_rgba(0,0,0,0.5)]">
|
||||
<div className="grid h-12 w-12 place-items-center rounded-[1rem] bg-amber-200 text-slate-950">
|
||||
{isSaving ? (
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="h-5 w-5" />
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-2xl font-black leading-tight">
|
||||
{PUZZLE_ONBOARDING_CLEAR_COPY}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isSaving}
|
||||
onClick={onLogin}
|
||||
className="inline-flex min-h-12 w-full items-center justify-center gap-2 rounded-[1rem] bg-amber-200 px-5 text-sm font-black text-slate-950 transition hover:bg-amber-100 disabled:cursor-not-allowed disabled:opacity-45"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
注册账号 / 登录
|
||||
</>
|
||||
) : (
|
||||
'注册账号 / 登录'
|
||||
)}
|
||||
</button>
|
||||
{error ? (
|
||||
<div className="w-full rounded-[1rem] border border-red-300/30 bg-red-500/14 px-4 py-3 text-sm font-semibold text-red-50">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function mergeBigFishWorkSummary(
|
||||
current: BigFishWorkSummary,
|
||||
updated: BigFishWorkSummary,
|
||||
@@ -12435,6 +12310,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
prompt={puzzleOnboardingPrompt}
|
||||
phase={puzzleOnboardingPhase}
|
||||
error={puzzleOnboardingError}
|
||||
copy={PUZZLE_ONBOARDING_COPY}
|
||||
onPromptChange={setPuzzleOnboardingPrompt}
|
||||
onSubmit={() => {
|
||||
void submitPuzzleOnboardingPrompt();
|
||||
@@ -12808,6 +12684,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
<PuzzleOnboardingLoginOverlay
|
||||
isSaving={isPuzzleOnboardingSaving}
|
||||
error={puzzleOnboardingError}
|
||||
copy={PUZZLE_ONBOARDING_CLEAR_COPY}
|
||||
onLogin={requestPuzzleOnboardingLogin}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { Loader2, Sparkles } from 'lucide-react';
|
||||
|
||||
export type PuzzleOnboardingPhase = 'input' | 'generating' | 'generated';
|
||||
|
||||
type PuzzleOnboardingViewProps = {
|
||||
prompt: string;
|
||||
phase: PuzzleOnboardingPhase;
|
||||
error: string | null;
|
||||
copy: string;
|
||||
onPromptChange: (value: string) => void;
|
||||
onSubmit: () => void;
|
||||
onSkip: () => void;
|
||||
};
|
||||
|
||||
export function PuzzleOnboardingView({
|
||||
prompt,
|
||||
phase,
|
||||
error,
|
||||
copy,
|
||||
onPromptChange,
|
||||
onSubmit,
|
||||
onSkip,
|
||||
}: PuzzleOnboardingViewProps) {
|
||||
const isGenerating = phase === 'generating';
|
||||
const isGenerated = phase === 'generated';
|
||||
const canSubmit = Boolean(prompt.trim()) && !isGenerating && !isGenerated;
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-[radial-gradient(circle_at_30%_15%,rgba(251,191,36,0.22),transparent_30%),linear-gradient(135deg,#0f172a,#111827_46%,#1e1b4b)] px-4 py-8 text-white">
|
||||
<div className="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.045)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[length:38px_38px] opacity-30" />
|
||||
<button
|
||||
type="button"
|
||||
disabled={isGenerating}
|
||||
onClick={onSkip}
|
||||
className="absolute right-4 top-4 z-10 inline-flex min-h-10 items-center justify-center rounded-full border border-white/14 bg-black/24 px-4 text-sm font-black text-white/86 shadow-[0_12px_28px_rgba(0,0,0,0.22)] backdrop-blur transition hover:border-amber-200/45 hover:text-amber-100 disabled:cursor-not-allowed disabled:opacity-45 sm:right-6 sm:top-6"
|
||||
>
|
||||
跳过
|
||||
</button>
|
||||
<section className="relative flex w-full max-w-[34rem] flex-col items-center gap-5 text-center">
|
||||
<div className="grid h-14 w-14 place-items-center rounded-[1.2rem] border border-amber-200/32 bg-amber-200/14 text-amber-100 shadow-[0_18px_48px_rgba(251,191,36,0.18)]">
|
||||
{isGenerating ? (
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="h-6 w-6" />
|
||||
)}
|
||||
</div>
|
||||
<h1 className="text-[2rem] font-black leading-tight sm:text-[2.85rem]">
|
||||
{copy}
|
||||
</h1>
|
||||
<form
|
||||
className="flex w-full flex-col gap-3"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
onSubmit();
|
||||
}}
|
||||
>
|
||||
<textarea
|
||||
value={prompt}
|
||||
disabled={isGenerating || isGenerated}
|
||||
onChange={(event) => onPromptChange(event.target.value)}
|
||||
placeholder="把你的梦讲给我听吧"
|
||||
rows={4}
|
||||
className="min-h-32 w-full resize-none rounded-[1.25rem] border border-white/14 bg-black/28 px-4 py-4 text-base font-semibold leading-7 text-white shadow-[0_18px_50px_rgba(0,0,0,0.24)] outline-none backdrop-blur placeholder:text-white/42 focus:border-amber-200/70 focus:ring-2 focus:ring-amber-200/20 disabled:opacity-70"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!canSubmit}
|
||||
className="inline-flex min-h-12 items-center justify-center gap-2 rounded-[1rem] bg-amber-200 px-5 text-sm font-black text-slate-950 transition hover:bg-amber-100 disabled:cursor-not-allowed disabled:opacity-45"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
生成
|
||||
</>
|
||||
) : (
|
||||
'生成'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
{error ? (
|
||||
<div className="w-full rounded-[1rem] border border-red-300/30 bg-red-500/14 px-4 py-3 text-sm font-semibold text-red-50">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type PuzzleOnboardingLoginOverlayProps = {
|
||||
isSaving: boolean;
|
||||
error: string | null;
|
||||
copy: string;
|
||||
onLogin: () => void;
|
||||
};
|
||||
|
||||
export function PuzzleOnboardingLoginOverlay({
|
||||
isSaving,
|
||||
error,
|
||||
copy,
|
||||
onLogin,
|
||||
}: PuzzleOnboardingLoginOverlayProps) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-[110] flex items-center justify-center bg-slate-950/72 px-4 py-6 text-white backdrop-blur-md">
|
||||
<section className="flex w-full max-w-[24rem] flex-col items-center gap-5 rounded-[1.35rem] border border-white/14 bg-slate-950/94 px-5 py-6 text-center shadow-[0_28px_90px_rgba(0,0,0,0.5)]">
|
||||
<div className="grid h-12 w-12 place-items-center rounded-[1rem] bg-amber-200 text-slate-950">
|
||||
{isSaving ? (
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="h-5 w-5" />
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-2xl font-black leading-tight">{copy}</h2>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isSaving}
|
||||
onClick={onLogin}
|
||||
className="inline-flex min-h-12 w-full items-center justify-center gap-2 rounded-[1rem] bg-amber-200 px-5 text-sm font-black text-slate-950 transition hover:bg-amber-100 disabled:cursor-not-allowed disabled:opacity-45"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
注册账号 / 登录
|
||||
</>
|
||||
) : (
|
||||
'注册账号 / 登录'
|
||||
)}
|
||||
</button>
|
||||
{error ? (
|
||||
<div className="w-full rounded-[1rem] border border-red-300/30 bg-red-500/14 px-4 py-3 text-sm font-semibold text-red-50">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user