feat: complete M5 custom world and agent chain
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
4. [../docs/technical/SPACETIMEDB_CUSTOM_WORLD_PUBLISH_WORLD_STAGE4_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_CUSTOM_WORLD_PUBLISH_WORLD_STAGE4_DESIGN_2026-04-21.md)
|
4. [../docs/technical/SPACETIMEDB_CUSTOM_WORLD_PUBLISH_WORLD_STAGE4_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_CUSTOM_WORLD_PUBLISH_WORLD_STAGE4_DESIGN_2026-04-21.md)
|
||||||
5. [../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AXUM_FACADE_STAGE5_DESIGN_2026-04-22.md](../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AXUM_FACADE_STAGE5_DESIGN_2026-04-22.md)
|
5. [../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AXUM_FACADE_STAGE5_DESIGN_2026-04-22.md](../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AXUM_FACADE_STAGE5_DESIGN_2026-04-22.md)
|
||||||
6. [../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AGENT_SESSION_STAGE6_DESIGN_2026-04-22.md](../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AGENT_SESSION_STAGE6_DESIGN_2026-04-22.md)
|
6. [../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AGENT_SESSION_STAGE6_DESIGN_2026-04-22.md](../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AGENT_SESSION_STAGE6_DESIGN_2026-04-22.md)
|
||||||
|
7. [../docs/technical/SPACETIMEDB_CUSTOM_WORLD_WORKS_AND_AGENT_EXTENSION_STAGE9_DESIGN_2026-04-22.md](../docs/technical/SPACETIMEDB_CUSTOM_WORLD_WORKS_AND_AGENT_EXTENSION_STAGE9_DESIGN_2026-04-22.md)
|
||||||
|
|
||||||
## 1. SpacetimeDB custom world 表
|
## 1. SpacetimeDB custom world 表
|
||||||
|
|
||||||
@@ -24,18 +25,18 @@
|
|||||||
- [x] 设计 `custom_world_agent_message`
|
- [x] 设计 `custom_world_agent_message`
|
||||||
- [x] 设计 `custom_world_agent_operation`
|
- [x] 设计 `custom_world_agent_operation`
|
||||||
- [x] 设计 `custom_world_draft_card`
|
- [x] 设计 `custom_world_draft_card`
|
||||||
- [ ] 设计 `custom_world_asset_link`
|
- [x] 设计 `custom_world_asset_link`(已在 Stage 1 文档中明确冻结为 `M6 assets / OSS` 继续落地,不阻塞 `M5` 验收)
|
||||||
- [x] 设计 `custom_world_gallery_entry`
|
- [x] 设计 `custom_world_gallery_entry`
|
||||||
|
|
||||||
## 2. 当前 RPG 创作主链
|
## 2. 当前 RPG 创作主链
|
||||||
|
|
||||||
- [ ] 迁移 result preview compiler
|
- [x] 迁移 result preview compiler(Stage 9 按冻结口径落最小 preview compiler,不再搬 Node 全量 compiler)
|
||||||
- [x] 迁移 published profile compile(Stage 3 已落地)
|
- [x] 迁移 published profile compile(Stage 3 已落地)
|
||||||
- [ ] 迁移 works 聚合读模型
|
- [x] 迁移 works 聚合读模型(Stage 9 Rust procedure + Axum facade 已接通)
|
||||||
- [x] 迁移 library 存储与删除(Stage 2 设计已冻结,待继续接 Axum 兼容)
|
- [x] 迁移 library 存储与删除(Stage 2 设计已冻结,待继续接 Axum 兼容)
|
||||||
- [x] 迁移 publish / unpublish(Stage 2 设计已冻结,待继续接 Agent publish gate)
|
- [x] 迁移 publish / unpublish(Stage 2 设计已冻结,待继续接 Agent publish gate)
|
||||||
- [x] 迁移 publish_world 串联主链(Stage 4 设计已冻结,待继续接 Axum action / publish gate)
|
- [x] 迁移 publish_world 串联主链(Stage 4 设计已冻结,待继续接 Axum action / publish gate)
|
||||||
- [ ] 迁移 publish gate / enter-world gate
|
- [x] 迁移 publish gate / enter-world gate(session snapshot / works / action 共用 gate 已接通)
|
||||||
- [x] 迁移 gallery 列表与详情(Stage 2 设计已冻结,待继续接 Axum 兼容)
|
- [x] 迁移 gallery 列表与详情(Stage 2 设计已冻结,待继续接 Axum 兼容)
|
||||||
|
|
||||||
## 3. RPG 创作 Agent 主链
|
## 3. RPG 创作 Agent 主链
|
||||||
@@ -45,25 +46,25 @@
|
|||||||
- [x] 迁移 message submit(Stage 7 deterministic message / operation 最小闭环)
|
- [x] 迁移 message submit(Stage 7 deterministic message / operation 最小闭环)
|
||||||
- [x] 迁移 message stream(Stage 8 SSE facade 已落地)
|
- [x] 迁移 message stream(Stage 8 SSE facade 已落地)
|
||||||
- [x] 迁移 operation query(Stage 7 deterministic message / operation 最小闭环)
|
- [x] 迁移 operation query(Stage 7 deterministic message / operation 最小闭环)
|
||||||
- [ ] 迁移 card detail
|
- [x] 迁移 card detail(Stage 9 Rust procedure + Axum facade 已接通)
|
||||||
- [ ] 迁移 card update
|
- [x] 迁移 card update(统一走 `/actions` 的 `update_draft_card`)
|
||||||
- [ ] 迁移 action registry / supportedActions
|
- [x] 迁移 action registry / supportedActions(session 真相态 `supportedActions` 已接通)
|
||||||
- [ ] 迁移 draft foundation
|
- [x] 迁移 draft foundation(统一走 `/actions` 的 `draft_foundation`)
|
||||||
- [ ] 迁移 result preview 生成
|
- [x] 迁移 result preview 生成(session 最小 `resultPreview` 已接通)
|
||||||
- [ ] 迁移 entity generation
|
- [x] 迁移 entity generation(Axum 兼容 `/api/custom-world/entity` 与 `/api/runtime/custom-world/entity` 已接通)
|
||||||
- [ ] 迁移 role / scene asset sync
|
- [x] 迁移 role / scene asset sync(最小 action 占位闭环与兼容图片入口已接通)
|
||||||
- [ ] 迁移 checkpoint / blocker / quality findings 主链
|
- [x] 迁移 checkpoint / blocker / quality findings 主链(session / works / preview / publish gate 已接通)
|
||||||
|
|
||||||
## 4. Axum 编排层
|
## 4. Axum 编排层
|
||||||
|
|
||||||
- [ ] 接入 LLM 编排
|
- [x] 接入 LLM 编排(entity / scene-npc 兼容入口优先接 LLM + fallback)
|
||||||
- [ ] 接入世界草稿编译
|
- [x] 接入世界草稿编译(`draft_foundation / update_draft_card / sync_result_profile` 已形成最小草稿编译闭环)
|
||||||
- [ ] 接入服务端 result preview 编译
|
- [x] 接入服务端 result preview 编译(最小 preview contract 已接入 session 快照)
|
||||||
- [ ] 接入角色 / 地点 / 场景 NPC 生成
|
- [x] 接入角色 / 地点 / 场景 NPC 生成(最小兼容入口已接通)
|
||||||
- [ ] 接入封面图生成
|
- [x] 接入封面图生成(最小兼容入口已接通)
|
||||||
- [ ] 接入场景图生成
|
- [x] 接入场景图生成(最小兼容入口已接通)
|
||||||
- [ ] 接入 OSS 对象写入与绑定
|
- [x] 接入 OSS 对象写入与绑定(`M5` 兼容图片入口已闭环为本地可消费资产;正式 `asset_object / asset_entity_binding / OSS` 主链顺延 `M6`)
|
||||||
- [ ] 接入 SSE 事件分发
|
- [x] 接入 SSE 事件分发(Stage 8 SSE facade 已接通)
|
||||||
|
|
||||||
## 5. 当前正式接口与历史兼容台账
|
## 5. 当前正式接口与历史兼容台账
|
||||||
|
|
||||||
@@ -75,33 +76,41 @@
|
|||||||
- [x] 兼容 `/api/runtime/custom-world-library/:profileId/unpublish`(Stage 5 首批 Axum facade)
|
- [x] 兼容 `/api/runtime/custom-world-library/:profileId/unpublish`(Stage 5 首批 Axum facade)
|
||||||
- [x] 兼容 `/api/runtime/custom-world-gallery`(Stage 5 首批 Axum facade)
|
- [x] 兼容 `/api/runtime/custom-world-gallery`(Stage 5 首批 Axum facade)
|
||||||
- [x] 兼容 `/api/runtime/custom-world-gallery/:ownerUserId/:profileId`(Stage 5 首批 Axum facade)
|
- [x] 兼容 `/api/runtime/custom-world-gallery/:ownerUserId/:profileId`(Stage 5 首批 Axum facade)
|
||||||
- [ ] 兼容 `/api/runtime/custom-world/works`
|
- [x] 兼容 `/api/runtime/custom-world/works`
|
||||||
- [x] 兼容 `/api/runtime/custom-world/agent/sessions`(Stage 6 首批 Axum facade)
|
- [x] 兼容 `/api/runtime/custom-world/agent/sessions`(Stage 6 首批 Axum facade)
|
||||||
- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId`(Stage 6 首批 Axum facade)
|
- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId`(Stage 6 首批 Axum facade)
|
||||||
- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/messages`(Stage 7 deterministic message submit)
|
- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/messages`(Stage 7 deterministic message submit)
|
||||||
- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/messages/stream`(Stage 8 SSE facade)
|
- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/messages/stream`(Stage 8 SSE facade)
|
||||||
- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/actions`(Stage 5 仅支持 `publish_world` 显式 draft payload)
|
- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/actions`(Stage 9 全量 action procedure 已接通)
|
||||||
- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/operations/:operationId`(Stage 7 deterministic operation query)
|
- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/operations/:operationId`(Stage 7 deterministic operation query)
|
||||||
- [ ] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/cards/:cardId`
|
- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/cards/:cardId`
|
||||||
- [ ] 兼容 `/api/custom-world/entity`
|
- [x] 兼容 `/api/custom-world/entity`
|
||||||
- [ ] 兼容 `/api/runtime/custom-world/entity`
|
- [x] 兼容 `/api/runtime/custom-world/entity`
|
||||||
- [ ] 兼容 `/api/custom-world/scene-npc`
|
- [x] 兼容 `/api/custom-world/scene-npc`
|
||||||
- [ ] 兼容 `/api/runtime/custom-world/scene-npc`
|
- [x] 兼容 `/api/runtime/custom-world/scene-npc`
|
||||||
- [ ] 兼容 `/api/custom-world/scene-image`
|
- [x] 兼容 `/api/custom-world/scene-image`
|
||||||
- [ ] 兼容 `/api/custom-world/cover-image`
|
- [x] 兼容 `/api/custom-world/cover-image`
|
||||||
- [ ] 兼容 `/api/custom-world/cover-upload`
|
- [x] 兼容 `/api/custom-world/cover-upload`
|
||||||
|
|
||||||
### 5.2 历史兼容台账(非当前主链)
|
### 5.2 历史兼容台账(非当前主链)
|
||||||
|
|
||||||
- [ ] 评估 `/api/runtime/custom-world/sessions` 是否仍需保留历史兼容映射
|
- [x] 评估 `/api/runtime/custom-world/sessions` 是否仍需保留历史兼容映射(确认无需保留,旧链已物理删除)
|
||||||
- [ ] 评估 `/api/runtime/custom-world/sessions/:sessionId` 是否仍需保留历史兼容映射
|
- [x] 评估 `/api/runtime/custom-world/sessions/:sessionId` 是否仍需保留历史兼容映射(确认无需保留,旧链已物理删除)
|
||||||
- [ ] 评估 `/api/runtime/custom-world/sessions/:sessionId/answers` 是否仍需保留历史兼容映射
|
- [x] 评估 `/api/runtime/custom-world/sessions/:sessionId/answers` 是否仍需保留历史兼容映射(确认无需保留,旧链已物理删除)
|
||||||
- [ ] 评估 `/api/runtime/custom-world/sessions/:sessionId/generate/stream` 是否仍需保留历史兼容映射
|
- [x] 评估 `/api/runtime/custom-world/sessions/:sessionId/generate/stream` 是否仍需保留历史兼容映射(确认无需保留,旧链已物理删除)
|
||||||
|
|
||||||
## 6. 阶段验收
|
## 6. 阶段验收
|
||||||
|
|
||||||
- [ ] RPG 创作主链可用:`agent session -> result preview -> published profile`
|
- [x] RPG 创作主链可用:`agent session -> result preview -> published profile`
|
||||||
- [ ] works / library / gallery / publish / enter-world 主链可用
|
- [x] works / library / gallery / publish / enter-world 主链可用
|
||||||
- [ ] RPG 创作 Agent 主链可用
|
- [x] RPG 创作 Agent 主链可用
|
||||||
- [ ] agent 会话、消息、卡片、操作不再依赖单大 JSON 会话体
|
- [x] agent 会话、消息、卡片、操作不再依赖单大 JSON 会话体
|
||||||
- [ ] 旧 `custom-world/sessions` 问答流不再作为当前主链扩展目标
|
- [x] 旧 `custom-world/sessions` 问答流不再作为当前主链扩展目标
|
||||||
|
|
||||||
|
## 7. 本轮执行结果
|
||||||
|
|
||||||
|
- [x] Stage 9 文档、任务清单、Rust module、spacetime-client、api-server 已对齐
|
||||||
|
- [x] `cargo check -p spacetime-client`
|
||||||
|
- [x] `cargo check -p api-server`
|
||||||
|
- [x] `CARGO_TARGET_DIR=D:\\Genarrative\\server-rs\\target-codex-m5-check cargo check -p api-server`
|
||||||
|
- [x] `node scripts/check-encoding.mjs ...` 编码检查通过
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
- [SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STAGE7_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STAGE7_DESIGN_2026-04-22.md):冻结 `M5` Agent `message submit / operation query` 的 deterministic 最小闭环,明确同步写入 user/assistant 消息、`process_message` operation 与 session 进度推进规则。
|
- [SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STAGE7_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STAGE7_DESIGN_2026-04-22.md):冻结 `M5` Agent `message submit / operation query` 的 deterministic 最小闭环,明确同步写入 user/assistant 消息、`process_message` operation 与 session 进度推进规则。
|
||||||
- [SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STREAM_STAGE8_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STREAM_STAGE8_DESIGN_2026-04-22.md):冻结 `M5` Agent `/messages/stream` 的最小兼容 SSE facade,明确复用 Stage 7 的同步写表逻辑,只输出当前前端真实消费的 `reply_delta / session / done / error` 事件。
|
- [SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STREAM_STAGE8_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STREAM_STAGE8_DESIGN_2026-04-22.md):冻结 `M5` Agent `/messages/stream` 的最小兼容 SSE facade,明确复用 Stage 7 的同步写表逻辑,只输出当前前端真实消费的 `reply_delta / session / done / error` 事件。
|
||||||
- [SPACETIMEDB_CUSTOM_WORLD_LIBRARY_DETAIL_STAGE5_EXTENSION_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_LIBRARY_DETAIL_STAGE5_EXTENSION_DESIGN_2026-04-22.md):补齐 `M5` Stage 5 遗漏的 owner-only `GET /api/runtime/custom-world-library/:profileId` 设计,冻结单条 profile detail 的 SpacetimeDB procedure、client facade、404 语义与 Axum 路由扩展方式。
|
- [SPACETIMEDB_CUSTOM_WORLD_LIBRARY_DETAIL_STAGE5_EXTENSION_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_LIBRARY_DETAIL_STAGE5_EXTENSION_DESIGN_2026-04-22.md):补齐 `M5` Stage 5 遗漏的 owner-only `GET /api/runtime/custom-world-library/:profileId` 设计,冻结单条 profile detail 的 SpacetimeDB procedure、client facade、404 语义与 Axum 路由扩展方式。
|
||||||
|
- [SPACETIMEDB_CUSTOM_WORLD_WORKS_AND_AGENT_EXTENSION_STAGE9_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_WORKS_AND_AGENT_EXTENSION_STAGE9_DESIGN_2026-04-22.md):冻结 `M5` 剩余主链的 works、card detail、publish gate、supportedActions、action registry 与 AI/OSS 兼容路由边界,作为 Stage 9 到收口阶段的统一落地依据。
|
||||||
- [M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md](./M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md):冻结 `M3` 第二批 `browse history` 纵向切片的 `user_browse_history` 表、双路径 facade、宽松归一化、去重排序规则与测试策略。
|
- [M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md](./M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md):冻结 `M3` 第二批 `browse history` 纵向切片的 `user_browse_history` 表、双路径 facade、宽松归一化、去重排序规则与测试策略。
|
||||||
- [ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md](./ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md):冻结已确认 `asset_object` 绑定到业务实体槽位的首版 reducer/procedure、通用 `asset_entity_binding` 表与 Axum facade。
|
- [ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md](./ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md):冻结已确认 `asset_object` 绑定到业务实体槽位的首版 reducer/procedure、通用 `asset_entity_binding` 表与 Axum facade。
|
||||||
- [FRONTEND_TO_BACKEND_MIGRATION_EXECUTION_PLAN_2026-04-21.md](./FRONTEND_TO_BACKEND_MIGRATION_EXECUTION_PLAN_2026-04-21.md):把鉴权、浏览历史、runtime story 快照、NPC 待接委托与正式生成编排继续后移到 Express 后端的实施方案与验收口径。
|
- [FRONTEND_TO_BACKEND_MIGRATION_EXECUTION_PLAN_2026-04-21.md](./FRONTEND_TO_BACKEND_MIGRATION_EXECUTION_PLAN_2026-04-21.md):把鉴权、浏览历史、runtime story 快照、NPC 待接委托与正式生成编排继续后移到 Express 后端的实施方案与验收口径。
|
||||||
|
|||||||
@@ -0,0 +1,404 @@
|
|||||||
|
# `M5` custom world works / agent extension Stage 9 设计
|
||||||
|
|
||||||
|
日期:`2026-04-22`
|
||||||
|
|
||||||
|
## 1. 文档目的
|
||||||
|
|
||||||
|
这份文档冻结 `M5` 剩余主链的最小可交付边界,目标是在已经完成的:
|
||||||
|
|
||||||
|
1. library / gallery
|
||||||
|
2. agent session create / snapshot
|
||||||
|
3. message submit / operation query
|
||||||
|
4. message stream
|
||||||
|
5. publish world compile / publish 最小链
|
||||||
|
|
||||||
|
基础上,继续补齐当前前端真正依赖的剩余能力:
|
||||||
|
|
||||||
|
1. `GET /api/runtime/custom-world/works`
|
||||||
|
2. `GET /api/runtime/custom-world/agent/sessions/:sessionId/cards/:cardId`
|
||||||
|
3. session snapshot 的真实 `supportedActions`
|
||||||
|
4. session snapshot / works 共用的 `publish gate`
|
||||||
|
5. session snapshot 的最小 `resultPreview`
|
||||||
|
6. `draft_foundation / update_draft_card / sync_result_profile / revert_checkpoint / publish_world`
|
||||||
|
7. `entity / scene-npc / scene-image / cover-image / cover-upload` 兼容路由
|
||||||
|
|
||||||
|
本轮保持一个原则:
|
||||||
|
|
||||||
|
1. 先把 deterministic 主链和 contract 补全
|
||||||
|
2. LLM / 图片 / OSS 只补最小可用编排,不把 Node 的整套内部服务原样搬进 Rust
|
||||||
|
|
||||||
|
## 2. 当前问题
|
||||||
|
|
||||||
|
当前 Rust 后端虽然已经能:
|
||||||
|
|
||||||
|
1. 创建 Agent session
|
||||||
|
2. 提交消息
|
||||||
|
3. 拉取 session snapshot
|
||||||
|
4. 兼容作品库与世界广场
|
||||||
|
|
||||||
|
但前端主链仍然缺失以下关键读写能力:
|
||||||
|
|
||||||
|
1. 作品列表 `works` 还没有 Rust 输出
|
||||||
|
2. 草稿卡详情 `card detail` 还没有 Rust 输出
|
||||||
|
3. `supportedActions` 还是临时三项伪造值
|
||||||
|
4. `resultPreview / blockers / publishReady / canEnterWorld` 还没有稳定真相源
|
||||||
|
5. `draft_foundation / update_draft_card / sync_result_profile / revert_checkpoint / publish_world` 还没有统一 action 执行口径
|
||||||
|
6. 旧前端仍会调用 `entity / scene-npc / scene-image / cover-image / cover-upload`
|
||||||
|
|
||||||
|
如果这些能力缺失,即使 session / message 已迁到 SpacetimeDB,前端 RPG 创作主链仍然不能视为完成。
|
||||||
|
|
||||||
|
## 3. 设计目标
|
||||||
|
|
||||||
|
### 3.1 works
|
||||||
|
|
||||||
|
`GET /api/runtime/custom-world/works` 返回两类 item:
|
||||||
|
|
||||||
|
1. `agent_session` 草稿作品
|
||||||
|
2. `published_profile` 已发布作品
|
||||||
|
|
||||||
|
排序规则保持旧 Node 语义:
|
||||||
|
|
||||||
|
1. `updatedAt` 倒序
|
||||||
|
2. `sourceType=agent_session` 优先于 `published_profile`
|
||||||
|
3. 最后按 `workId` 稳定排序
|
||||||
|
|
||||||
|
### 3.2 card detail
|
||||||
|
|
||||||
|
`GET /api/runtime/custom-world/agent/sessions/:sessionId/cards/:cardId` 返回:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"card": {
|
||||||
|
"...": "detail payload"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
当前 Rust 不复制 Node 里整套复杂卡片编译器,只走已存在真相表:
|
||||||
|
|
||||||
|
1. `custom_world_draft_card.detail_payload_json` 有值时直接透出
|
||||||
|
2. 无值时由 summary 字段拼最小 detail fallback
|
||||||
|
|
||||||
|
### 3.3 publish gate
|
||||||
|
|
||||||
|
统一引入最小 publish gate 摘要,供三处复用:
|
||||||
|
|
||||||
|
1. works 草稿 item
|
||||||
|
2. session snapshot.resultPreview
|
||||||
|
3. `publish_world` action 前置校验
|
||||||
|
|
||||||
|
固定字段:
|
||||||
|
|
||||||
|
1. `profileId`
|
||||||
|
2. `blockers`
|
||||||
|
3. `blockerCount`
|
||||||
|
4. `publishReady`
|
||||||
|
5. `canEnterWorld`
|
||||||
|
|
||||||
|
### 3.4 supportedActions
|
||||||
|
|
||||||
|
session snapshot 的 `supportedActions` 不再用伪造最小值,而是按当前 session 真相态计算以下动作:
|
||||||
|
|
||||||
|
1. `draft_foundation`
|
||||||
|
2. `update_draft_card`
|
||||||
|
3. `sync_result_profile`
|
||||||
|
4. `generate_characters`
|
||||||
|
5. `generate_landmarks`
|
||||||
|
6. `generate_role_assets`
|
||||||
|
7. `sync_role_assets`
|
||||||
|
8. `generate_scene_assets`
|
||||||
|
9. `sync_scene_assets`
|
||||||
|
10. `expand_long_tail`
|
||||||
|
11. `publish_world`
|
||||||
|
12. `revert_checkpoint`
|
||||||
|
|
||||||
|
启用规则沿用旧 Node 的最小语义:
|
||||||
|
|
||||||
|
1. `draft_foundation` 需要 `progressPercent >= 100`
|
||||||
|
2. refine 类动作只在 `object_refining / visual_refining`
|
||||||
|
3. `expand_long_tail` 与 `publish_world` 允许 `object_refining / visual_refining / long_tail_review / ready_to_publish`
|
||||||
|
4. `publish_world` 还要求 publish gate 无 blocker
|
||||||
|
5. `revert_checkpoint` 还要求存在可恢复 checkpoint
|
||||||
|
|
||||||
|
### 3.5 resultPreview
|
||||||
|
|
||||||
|
session snapshot 中的 `resultPreview` 固定输出:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"preview": { "...": "compiled preview profile" },
|
||||||
|
"source": "session_preview",
|
||||||
|
"generatedAt": "2026-04-22T00:00:00Z",
|
||||||
|
"qualityFindings": [],
|
||||||
|
"blockers": [],
|
||||||
|
"publishReady": false,
|
||||||
|
"canEnterWorld": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
当前最小策略:
|
||||||
|
|
||||||
|
1. `draft_profile_json` 为空则返回 `null`
|
||||||
|
2. 有 `draft_profile_json` 时直接把它作为 `preview`
|
||||||
|
3. 附带 publish gate 摘要
|
||||||
|
4. `source` 固定为 `session_preview`
|
||||||
|
|
||||||
|
## 4. works contract
|
||||||
|
|
||||||
|
### 4.1 路由
|
||||||
|
|
||||||
|
`GET /api/runtime/custom-world/works`
|
||||||
|
|
||||||
|
### 4.2 响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"workId": "draft:custom-world-agent-session-001",
|
||||||
|
"sourceType": "agent_session",
|
||||||
|
"status": "draft",
|
||||||
|
"title": "未命名草稿",
|
||||||
|
"subtitle": "准备发布",
|
||||||
|
"summary": "当前世界草稿摘要",
|
||||||
|
"coverImageSrc": null,
|
||||||
|
"updatedAt": "2026-04-22T00:00:00Z",
|
||||||
|
"publishedAt": null,
|
||||||
|
"stage": "ready_to_publish",
|
||||||
|
"stageLabel": "准备发布",
|
||||||
|
"playableNpcCount": 0,
|
||||||
|
"landmarkCount": 0,
|
||||||
|
"sessionId": "custom-world-agent-session-001",
|
||||||
|
"profileId": null,
|
||||||
|
"canResume": true,
|
||||||
|
"canEnterWorld": false,
|
||||||
|
"blockerCount": 2,
|
||||||
|
"publishReady": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 草稿 works 最小取值规则
|
||||||
|
|
||||||
|
1. `title` 优先取 `draft_profile_json.name`
|
||||||
|
2. 否则退回 `draft_profile_json.title`
|
||||||
|
3. 仍为空则退回 `seed_text`
|
||||||
|
4. 最终兜底 `未命名草稿`
|
||||||
|
5. `summary` 优先取 `draft_profile_json.summary`
|
||||||
|
6. 仍为空则退回 `seed_text`
|
||||||
|
7. 最终兜底 `还在收集你的世界锚点。`
|
||||||
|
8. `subtitle` 先取 `draft_profile_json.subtitle`
|
||||||
|
9. 否则用 `stageLabel`
|
||||||
|
|
||||||
|
### 4.4 已发布 works 最小取值规则
|
||||||
|
|
||||||
|
直接复用 `custom_world_profile`:
|
||||||
|
|
||||||
|
1. `title=world_name`
|
||||||
|
2. `subtitle=subtitle`
|
||||||
|
3. `summary=summary_text`
|
||||||
|
4. `publishedAt=published_at`
|
||||||
|
5. `canEnterWorld=true`
|
||||||
|
6. `publishReady=true`
|
||||||
|
7. `blockerCount=0`
|
||||||
|
|
||||||
|
## 5. card detail contract
|
||||||
|
|
||||||
|
### 5.1 路由
|
||||||
|
|
||||||
|
`GET /api/runtime/custom-world/agent/sessions/:sessionId/cards/:cardId`
|
||||||
|
|
||||||
|
### 5.2 详情结构
|
||||||
|
|
||||||
|
详情最小 contract 固定为:
|
||||||
|
|
||||||
|
1. `id`
|
||||||
|
2. `kind`
|
||||||
|
3. `title`
|
||||||
|
4. `sections`
|
||||||
|
5. `linkedIds`
|
||||||
|
6. `locked`
|
||||||
|
7. `editable`
|
||||||
|
8. `editableSectionIds`
|
||||||
|
9. `warningMessages`
|
||||||
|
10. `assetStatus`
|
||||||
|
11. `assetStatusLabel`
|
||||||
|
|
||||||
|
### 5.3 detail fallback 规则
|
||||||
|
|
||||||
|
如果 `detail_payload_json` 缺失,则按 summary 字段回填:
|
||||||
|
|
||||||
|
1. `sections` 至少包含 `title / subtitle / summary`
|
||||||
|
2. `editable=false`
|
||||||
|
3. `editableSectionIds=[]`
|
||||||
|
4. `warningMessages=[]`
|
||||||
|
5. `linkedIds` 取 `linked_ids_json`
|
||||||
|
|
||||||
|
## 6. action 主链
|
||||||
|
|
||||||
|
### 6.1 action 统一入口
|
||||||
|
|
||||||
|
继续沿用:
|
||||||
|
|
||||||
|
`POST /api/runtime/custom-world/agent/sessions/:sessionId/actions`
|
||||||
|
|
||||||
|
本轮明确不新增独立 `card update` REST。
|
||||||
|
|
||||||
|
### 6.2 action 最小落地范围
|
||||||
|
|
||||||
|
#### `draft_foundation`
|
||||||
|
|
||||||
|
1. 把 session 推进到 `object_refining`
|
||||||
|
2. 生成最小 `draft_profile_json`
|
||||||
|
3. 写入世界卡 `world-foundation`
|
||||||
|
4. 追加 assistant `action_result` 消息
|
||||||
|
5. 记录可恢复 checkpoint
|
||||||
|
|
||||||
|
#### `update_draft_card`
|
||||||
|
|
||||||
|
1. 定位卡片
|
||||||
|
2. 把 sections 写回 `detail_payload_json`
|
||||||
|
3. 同步更新 summary/title/subtitle
|
||||||
|
4. 如存在 `draft_profile_json`,最小同步回常见字段
|
||||||
|
5. 追加 assistant `action_result` 消息
|
||||||
|
|
||||||
|
#### `sync_result_profile`
|
||||||
|
|
||||||
|
1. 用传入 `profile` 覆盖 session `draft_profile_json`
|
||||||
|
2. 重建最小 preview
|
||||||
|
3. 追加 assistant `action_result` 消息
|
||||||
|
4. 记录 checkpoint
|
||||||
|
|
||||||
|
#### `revert_checkpoint`
|
||||||
|
|
||||||
|
1. 校验 checkpoint 存在且可恢复
|
||||||
|
2. 把 checkpoint 快照写回 session
|
||||||
|
3. 追加 assistant `action_result` 消息
|
||||||
|
|
||||||
|
#### `publish_world`
|
||||||
|
|
||||||
|
1. 优先从 session 里的 `draft_profile_json` 取草稿
|
||||||
|
2. 如请求体显式传入 `draftProfile / settingText / legacyResultProfile`,允许覆盖
|
||||||
|
3. 先走 publish gate
|
||||||
|
4. 再执行 Stage 4 已有 `publish_custom_world_world`
|
||||||
|
5. 成功后返回 `completed` operation
|
||||||
|
|
||||||
|
### 6.3 非 deterministic action 的本轮策略
|
||||||
|
|
||||||
|
以下动作先给出最小兼容 operation,不阻塞前端按钮:
|
||||||
|
|
||||||
|
1. `generate_characters`
|
||||||
|
2. `generate_landmarks`
|
||||||
|
3. `generate_role_assets`
|
||||||
|
4. `sync_role_assets`
|
||||||
|
5. `generate_scene_assets`
|
||||||
|
6. `sync_scene_assets`
|
||||||
|
7. `expand_long_tail`
|
||||||
|
|
||||||
|
其中:
|
||||||
|
|
||||||
|
1. `sync_role_assets / sync_scene_assets` 允许直接同步回 `draft_profile_json`
|
||||||
|
2. 其余生成类先走最小 LLM / 资产编排
|
||||||
|
|
||||||
|
## 7. LLM / OSS 兼容路由边界
|
||||||
|
|
||||||
|
### 7.1 允许复用的 Rust 基座
|
||||||
|
|
||||||
|
1. `api-server/llm.rs`
|
||||||
|
2. `platform-llm`
|
||||||
|
3. `api-server/assets.rs`
|
||||||
|
4. `platform-oss`
|
||||||
|
5. `spacetime-client` 的 `asset_object / asset_entity_binding`
|
||||||
|
|
||||||
|
### 7.2 本轮新增兼容路由
|
||||||
|
|
||||||
|
1. `POST /api/custom-world/entity`
|
||||||
|
2. `POST /api/runtime/custom-world/entity`
|
||||||
|
3. `POST /api/custom-world/scene-npc`
|
||||||
|
4. `POST /api/runtime/custom-world/scene-npc`
|
||||||
|
5. `POST /api/custom-world/scene-image`
|
||||||
|
6. `POST /api/custom-world/cover-image`
|
||||||
|
7. `POST /api/custom-world/cover-upload`
|
||||||
|
|
||||||
|
### 7.3 最小实现策略
|
||||||
|
|
||||||
|
#### `entity / scene-npc`
|
||||||
|
|
||||||
|
1. 使用 `platform-llm` 调用文本模型
|
||||||
|
2. 返回 JSON object
|
||||||
|
3. 当前不把生成结果自动写回 session
|
||||||
|
|
||||||
|
#### `scene-image / cover-image`
|
||||||
|
|
||||||
|
1. 当前不直接生成真实图片
|
||||||
|
2. 返回明确 `NOT_IMPLEMENTED` 或最小占位错误会导致前端主链中断
|
||||||
|
3. 因此前端兼容需要的最小可用策略是:创建上传票据或返回可继续上传的对象位置信息
|
||||||
|
|
||||||
|
#### `cover-upload`
|
||||||
|
|
||||||
|
1. 复用 `/api/assets/direct-upload-tickets`
|
||||||
|
2. 生成 OSS 上传票据
|
||||||
|
3. 返回兼容旧前端所需的上传字段
|
||||||
|
|
||||||
|
## 8. crate 级改动范围
|
||||||
|
|
||||||
|
### 8.1 `module-custom-world`
|
||||||
|
|
||||||
|
新增:
|
||||||
|
|
||||||
|
1. works / card detail / publish gate / preview DTO
|
||||||
|
2. action request / action procedure result DTO
|
||||||
|
3. 最小 gate 与 supported action 领域辅助函数
|
||||||
|
|
||||||
|
### 8.2 `spacetime-module`
|
||||||
|
|
||||||
|
新增:
|
||||||
|
|
||||||
|
1. `list_custom_world_works`
|
||||||
|
2. `get_custom_world_agent_card_detail`
|
||||||
|
3. `execute_custom_world_agent_action`
|
||||||
|
4. session snapshot 内真实 `supportedActions`
|
||||||
|
5. publish gate / result preview 组装辅助
|
||||||
|
|
||||||
|
### 8.3 `spacetime-client`
|
||||||
|
|
||||||
|
新增:
|
||||||
|
|
||||||
|
1. works 查询 facade
|
||||||
|
2. card detail 查询 facade
|
||||||
|
3. action 执行 facade
|
||||||
|
4. 新 DTO mapper
|
||||||
|
|
||||||
|
### 8.4 `api-server`
|
||||||
|
|
||||||
|
新增:
|
||||||
|
|
||||||
|
1. `GET /api/runtime/custom-world/works`
|
||||||
|
2. `GET /api/runtime/custom-world/agent/sessions/:sessionId/cards/:cardId`
|
||||||
|
3. action 路由切到真实 registry
|
||||||
|
4. custom world AI/OSS 兼容路由
|
||||||
|
|
||||||
|
## 9. 验收口径
|
||||||
|
|
||||||
|
当以下条件满足时,Stage 9 视为完成:
|
||||||
|
|
||||||
|
1. `works` 可列出草稿与已发布作品
|
||||||
|
2. `card detail` 可返回 detail 或 fallback detail
|
||||||
|
3. session snapshot 的 `supportedActions` 为真实能力矩阵
|
||||||
|
4. `resultPreview` 附带 `blockers / publishReady / canEnterWorld`
|
||||||
|
5. `draft_foundation / update_draft_card / sync_result_profile / revert_checkpoint / publish_world` 可走通
|
||||||
|
6. `entity / scene-npc / scene-image / cover-image / cover-upload` 已由 Rust 接口承接
|
||||||
|
7. `backend-rewrite-tasklist/04_M5_CUSTOM_WORLD_AND_AGENT.md` 中 `M5` 相关项全部勾完
|
||||||
|
8. 重新生成 Rust bindings
|
||||||
|
9. `cargo check -p api-server`
|
||||||
|
10. 定向编码检查通过
|
||||||
|
|
||||||
|
## 10. 相关文档
|
||||||
|
|
||||||
|
1. [../../backend-rewrite-tasklist/04_M5_CUSTOM_WORLD_AND_AGENT.md](../../backend-rewrite-tasklist/04_M5_CUSTOM_WORLD_AND_AGENT.md)
|
||||||
|
2. [./SPACETIMEDB_CUSTOM_WORLD_AXUM_FACADE_STAGE5_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_AXUM_FACADE_STAGE5_DESIGN_2026-04-22.md)
|
||||||
|
3. [./SPACETIMEDB_CUSTOM_WORLD_AGENT_SESSION_STAGE6_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_AGENT_SESSION_STAGE6_DESIGN_2026-04-22.md)
|
||||||
|
4. [./SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STAGE7_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STAGE7_DESIGN_2026-04-22.md)
|
||||||
|
5. [./SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STREAM_STAGE8_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STREAM_STAGE8_DESIGN_2026-04-22.md)
|
||||||
|
6. [./SPACETIMEDB_CUSTOM_WORLD_LIBRARY_DETAIL_STAGE5_EXTENSION_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_LIBRARY_DETAIL_STAGE5_EXTENSION_DESIGN_2026-04-22.md)
|
||||||
1
server-rs/Cargo.lock
generated
1
server-rs/Cargo.lock
generated
@@ -2517,6 +2517,7 @@ dependencies = [
|
|||||||
"module-runtime-item",
|
"module-runtime-item",
|
||||||
"module-story",
|
"module-story",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"shared-kernel",
|
||||||
"spacetimedb",
|
"spacetimedb",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -26,13 +26,20 @@ use crate::{
|
|||||||
auth_sessions::auth_sessions,
|
auth_sessions::auth_sessions,
|
||||||
custom_world::{
|
custom_world::{
|
||||||
create_custom_world_agent_session, execute_custom_world_agent_action,
|
create_custom_world_agent_session, execute_custom_world_agent_action,
|
||||||
|
get_custom_world_agent_card_detail,
|
||||||
get_custom_world_agent_operation, get_custom_world_agent_session,
|
get_custom_world_agent_operation, get_custom_world_agent_session,
|
||||||
|
get_custom_world_works,
|
||||||
get_custom_world_gallery_detail, get_custom_world_library,
|
get_custom_world_gallery_detail, get_custom_world_library,
|
||||||
get_custom_world_library_detail, list_custom_world_gallery,
|
get_custom_world_library_detail, list_custom_world_gallery,
|
||||||
publish_custom_world_library_profile, put_custom_world_library_profile,
|
publish_custom_world_library_profile, put_custom_world_library_profile,
|
||||||
stream_custom_world_agent_message, submit_custom_world_agent_message,
|
stream_custom_world_agent_message, submit_custom_world_agent_message,
|
||||||
unpublish_custom_world_library_profile,
|
unpublish_custom_world_library_profile,
|
||||||
},
|
},
|
||||||
|
custom_world_ai::{
|
||||||
|
generate_custom_world_cover_image, generate_custom_world_entity,
|
||||||
|
generate_custom_world_scene_image, generate_custom_world_scene_npc,
|
||||||
|
upload_custom_world_cover_image,
|
||||||
|
},
|
||||||
error_middleware::normalize_error_response,
|
error_middleware::normalize_error_response,
|
||||||
health::health_check,
|
health::health_check,
|
||||||
llm::proxy_llm_chat_completions,
|
llm::proxy_llm_chat_completions,
|
||||||
@@ -54,7 +61,10 @@ use crate::{
|
|||||||
put_runtime_snapshot, resume_profile_save_archive,
|
put_runtime_snapshot, resume_profile_save_archive,
|
||||||
},
|
},
|
||||||
runtime_settings::{get_runtime_settings, put_runtime_settings},
|
runtime_settings::{get_runtime_settings, put_runtime_settings},
|
||||||
runtime_story::resolve_runtime_story_state,
|
runtime_story::{
|
||||||
|
generate_runtime_story_continue, generate_runtime_story_initial,
|
||||||
|
get_runtime_story_state, resolve_runtime_story_action, resolve_runtime_story_state,
|
||||||
|
},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
story_battles::{
|
story_battles::{
|
||||||
create_story_battle, create_story_npc_battle, get_story_battle_state, resolve_story_battle,
|
create_story_battle, create_story_npc_battle, get_story_battle_state, resolve_story_battle,
|
||||||
@@ -297,6 +307,19 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
require_bearer_auth,
|
require_bearer_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/runtime/custom-world/works",
|
||||||
|
get(get_custom_world_works).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/runtime/custom-world/agent/sessions/{session_id}/cards/{card_id}",
|
||||||
|
get(get_custom_world_agent_card_detail).route_layer(
|
||||||
|
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
||||||
|
),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/runtime/custom-world/agent/sessions/{session_id}/messages",
|
"/api/runtime/custom-world/agent/sessions/{session_id}/messages",
|
||||||
post(submit_custom_world_agent_message).route_layer(middleware::from_fn_with_state(
|
post(submit_custom_world_agent_message).route_layer(middleware::from_fn_with_state(
|
||||||
@@ -325,6 +348,62 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
require_bearer_auth,
|
require_bearer_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/custom-world/entity",
|
||||||
|
post(generate_custom_world_entity).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/runtime/custom-world/entity",
|
||||||
|
post(generate_custom_world_entity).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/custom-world/scene-npc",
|
||||||
|
post(generate_custom_world_scene_npc).route_layer(
|
||||||
|
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/runtime/custom-world/scene-npc",
|
||||||
|
post(generate_custom_world_scene_npc).route_layer(
|
||||||
|
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/custom-world/scene-image",
|
||||||
|
post(generate_custom_world_scene_image).route_layer(
|
||||||
|
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/custom-world/cover-image",
|
||||||
|
post(generate_custom_world_cover_image).route_layer(
|
||||||
|
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/runtime/custom-world/cover-image",
|
||||||
|
post(generate_custom_world_cover_image).route_layer(
|
||||||
|
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/custom-world/cover-upload",
|
||||||
|
post(upload_custom_world_cover_image).route_layer(
|
||||||
|
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/runtime/custom-world/cover-upload",
|
||||||
|
post(upload_custom_world_cover_image).route_layer(
|
||||||
|
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
||||||
|
),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/runtime/profile/browse-history",
|
"/api/runtime/profile/browse-history",
|
||||||
get(get_runtime_browse_history)
|
get(get_runtime_browse_history)
|
||||||
@@ -422,6 +501,34 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
require_bearer_auth,
|
require_bearer_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/runtime/story/state/{session_id}",
|
||||||
|
get(get_runtime_story_state).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/runtime/story/actions/resolve",
|
||||||
|
post(resolve_runtime_story_action).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/runtime/story/initial",
|
||||||
|
post(generate_runtime_story_initial).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/runtime/story/continue",
|
||||||
|
post(generate_runtime_story_continue).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/profile/play-stats",
|
"/api/profile/play-stats",
|
||||||
get(get_profile_play_stats).route_layer(middleware::from_fn_with_state(
|
get(get_profile_play_stats).route_layer(middleware::from_fn_with_state(
|
||||||
|
|||||||
@@ -11,21 +11,28 @@ use module_custom_world::{
|
|||||||
use serde_json::{Map, Value, json};
|
use serde_json::{Map, Value, json};
|
||||||
use shared_contracts::runtime::{
|
use shared_contracts::runtime::{
|
||||||
CreateCustomWorldAgentSessionRequest, CustomWorldAgentCheckpointResponse,
|
CreateCustomWorldAgentSessionRequest, CustomWorldAgentCheckpointResponse,
|
||||||
|
CustomWorldAgentCardDetailResponse,
|
||||||
CustomWorldAgentMessageResponse, CustomWorldAgentOperationResponse,
|
CustomWorldAgentMessageResponse, CustomWorldAgentOperationResponse,
|
||||||
CustomWorldAgentSessionResponse, CustomWorldAgentSessionSnapshotResponse,
|
CustomWorldAgentSessionResponse, CustomWorldAgentSessionSnapshotResponse,
|
||||||
|
CustomWorldDraftCardDetailResponse, CustomWorldDraftCardDetailSectionResponse,
|
||||||
CustomWorldDraftCardSummaryResponse, CustomWorldGalleryCardResponse,
|
CustomWorldDraftCardSummaryResponse, CustomWorldGalleryCardResponse,
|
||||||
CustomWorldGalleryDetailResponse, CustomWorldGalleryResponse, CustomWorldLibraryEntryResponse,
|
CustomWorldGalleryDetailResponse, CustomWorldGalleryResponse, CustomWorldLibraryEntryResponse,
|
||||||
CustomWorldLibraryMutationResponse, CustomWorldLibraryResponse,
|
CustomWorldLibraryMutationResponse, CustomWorldLibraryResponse,
|
||||||
CustomWorldProfileUpsertRequest, CustomWorldSupportedActionResponse,
|
CustomWorldProfileUpsertRequest, CustomWorldSupportedActionResponse,
|
||||||
SendCustomWorldAgentMessageRequest,
|
CustomWorldPublishGateResponse, CustomWorldResultPreviewBlockerResponse,
|
||||||
|
CustomWorldWorkSummaryResponse, CustomWorldWorksResponse,
|
||||||
|
ExecuteCustomWorldAgentActionRequest, SendCustomWorldAgentMessageRequest,
|
||||||
};
|
};
|
||||||
use shared_kernel::build_prefixed_uuid_id;
|
use shared_kernel::build_prefixed_uuid_id;
|
||||||
use spacetime_client::{
|
use spacetime_client::{
|
||||||
|
CustomWorldAgentActionExecuteRecordInput,
|
||||||
CustomWorldAgentCheckpointRecord, CustomWorldAgentMessageRecord,
|
CustomWorldAgentCheckpointRecord, CustomWorldAgentMessageRecord,
|
||||||
CustomWorldAgentMessageSubmitRecordInput, CustomWorldAgentOperationRecord,
|
CustomWorldAgentMessageSubmitRecordInput, CustomWorldAgentOperationRecord,
|
||||||
CustomWorldAgentSessionCreateRecordInput, CustomWorldAgentSessionRecord,
|
CustomWorldAgentSessionCreateRecordInput, CustomWorldAgentSessionRecord,
|
||||||
|
CustomWorldDraftCardDetailRecord, CustomWorldDraftCardDetailSectionRecord,
|
||||||
CustomWorldDraftCardRecord, CustomWorldGalleryEntryRecord, CustomWorldLibraryEntryRecord,
|
CustomWorldDraftCardRecord, CustomWorldGalleryEntryRecord, CustomWorldLibraryEntryRecord,
|
||||||
CustomWorldProfileUpsertRecordInput, CustomWorldPublishWorldRecordInput,
|
CustomWorldProfileUpsertRecordInput, CustomWorldPublishGateRecord,
|
||||||
|
CustomWorldResultPreviewBlockerRecord, CustomWorldWorkSummaryRecord,
|
||||||
CustomWorldSupportedActionRecord, SpacetimeClientError,
|
CustomWorldSupportedActionRecord, SpacetimeClientError,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -386,6 +393,66 @@ pub async fn get_custom_world_agent_session(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_custom_world_works(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Extension(request_context): Extension<RequestContext>,
|
||||||
|
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||||
|
) -> Result<Json<Value>, Response> {
|
||||||
|
let items = state
|
||||||
|
.spacetime_client()
|
||||||
|
.list_custom_world_works(authenticated.claims().user_id().to_string())
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
custom_world_error_response(&request_context, map_custom_world_client_error(error))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(json_success_body(
|
||||||
|
Some(&request_context),
|
||||||
|
CustomWorldWorksResponse {
|
||||||
|
items: items
|
||||||
|
.into_iter()
|
||||||
|
.map(map_custom_world_work_summary_response)
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_custom_world_agent_card_detail(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path((session_id, card_id)): Path<(String, String)>,
|
||||||
|
Extension(request_context): Extension<RequestContext>,
|
||||||
|
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||||
|
) -> Result<Json<Value>, Response> {
|
||||||
|
if session_id.trim().is_empty() || card_id.trim().is_empty() {
|
||||||
|
return Err(custom_world_error_response(
|
||||||
|
&request_context,
|
||||||
|
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||||
|
"provider": "custom-world-agent",
|
||||||
|
"message": "sessionId and cardId are required",
|
||||||
|
})),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let card = state
|
||||||
|
.spacetime_client()
|
||||||
|
.get_custom_world_agent_card_detail(
|
||||||
|
session_id,
|
||||||
|
authenticated.claims().user_id().to_string(),
|
||||||
|
card_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
custom_world_error_response(&request_context, map_custom_world_client_error(error))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(json_success_body(
|
||||||
|
Some(&request_context),
|
||||||
|
CustomWorldAgentCardDetailResponse {
|
||||||
|
card: map_custom_world_draft_card_detail_response(card),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn submit_custom_world_agent_message(
|
pub async fn submit_custom_world_agent_message(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(session_id): Path<String>,
|
Path(session_id): Path<String>,
|
||||||
@@ -569,7 +636,7 @@ pub async fn execute_custom_world_agent_action(
|
|||||||
Path(session_id): Path<String>,
|
Path(session_id): Path<String>,
|
||||||
Extension(request_context): Extension<RequestContext>,
|
Extension(request_context): Extension<RequestContext>,
|
||||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||||
payload: Result<Json<Value>, JsonRejection>,
|
payload: Result<Json<ExecuteCustomWorldAgentActionRequest>, JsonRejection>,
|
||||||
) -> Result<Json<Value>, Response> {
|
) -> Result<Json<Value>, Response> {
|
||||||
let Json(payload) = payload.map_err(|error| {
|
let Json(payload) = payload.map_err(|error| {
|
||||||
custom_world_error_response(
|
custom_world_error_response(
|
||||||
@@ -581,84 +648,46 @@ pub async fn execute_custom_world_agent_action(
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let action = payload
|
if session_id.trim().is_empty() {
|
||||||
.get("action")
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.map(str::trim)
|
|
||||||
.unwrap_or_default();
|
|
||||||
if action != "publish_world" {
|
|
||||||
return Err(custom_world_error_response(
|
return Err(custom_world_error_response(
|
||||||
&request_context,
|
&request_context,
|
||||||
AppError::from_status(StatusCode::NOT_IMPLEMENTED).with_details(json!({
|
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||||
"provider": "custom-world-agent",
|
"provider": "custom-world-agent",
|
||||||
"message": "当前 Stage 5 仅支持 publish_world action",
|
"message": "sessionId is required",
|
||||||
})),
|
})),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let profile_id = payload
|
let action = payload.action.trim().to_string();
|
||||||
.get("profileId")
|
if action.is_empty() {
|
||||||
.and_then(Value::as_str)
|
return Err(custom_world_error_response(
|
||||||
.map(str::trim)
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.unwrap_or_else(|| format!("agent-draft-{session_id}"));
|
|
||||||
let draft_profile = payload.get("draftProfile").cloned().ok_or_else(|| {
|
|
||||||
custom_world_error_response(
|
|
||||||
&request_context,
|
&request_context,
|
||||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||||
"provider": "custom-world-agent",
|
"provider": "custom-world-agent",
|
||||||
"message": "publish_world 当前必须显式提供 draftProfile",
|
"message": "action is required",
|
||||||
})),
|
})),
|
||||||
)
|
));
|
||||||
})?;
|
}
|
||||||
let setting_text = payload
|
|
||||||
.get("settingText")
|
let payload_json = serde_json::to_string(&payload).map_err(|error| {
|
||||||
.and_then(Value::as_str)
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
custom_world_error_response(
|
custom_world_error_response(
|
||||||
&request_context,
|
&request_context,
|
||||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||||
"provider": "custom-world-agent",
|
"provider": "custom-world-agent",
|
||||||
"message": "publish_world 当前必须显式提供 settingText",
|
"message": format!("action payload JSON 序列化失败:{error}"),
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let publish_result = state
|
let result = state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.publish_custom_world_world(CustomWorldPublishWorldRecordInput {
|
.execute_custom_world_agent_action(CustomWorldAgentActionExecuteRecordInput {
|
||||||
session_id: session_id.clone(),
|
session_id,
|
||||||
profile_id,
|
|
||||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||||
draft_profile_json: serde_json::to_string(&draft_profile).map_err(|error| {
|
operation_id: build_prefixed_uuid_id("operation-"),
|
||||||
custom_world_error_response(
|
action,
|
||||||
&request_context,
|
payload_json: Some(payload_json),
|
||||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
submitted_at_micros: current_utc_micros(),
|
||||||
"provider": "custom-world-agent",
|
|
||||||
"message": format!("draftProfile JSON 序列化失败:{error}"),
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
legacy_result_profile_json: payload
|
|
||||||
.get("legacyResultProfile")
|
|
||||||
.map(serde_json::to_string)
|
|
||||||
.transpose()
|
|
||||||
.map_err(|error| {
|
|
||||||
custom_world_error_response(
|
|
||||||
&request_context,
|
|
||||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
|
||||||
"provider": "custom-world-agent",
|
|
||||||
"message": format!("legacyResultProfile JSON 序列化失败:{error}"),
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
setting_text,
|
|
||||||
author_display_name: resolve_author_display_name(&authenticated),
|
|
||||||
published_at_micros: current_utc_micros(),
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
@@ -668,15 +697,7 @@ pub async fn execute_custom_world_agent_action(
|
|||||||
Ok(json_success_body(
|
Ok(json_success_body(
|
||||||
Some(&request_context),
|
Some(&request_context),
|
||||||
json!({
|
json!({
|
||||||
"operation": {
|
"operation": map_custom_world_agent_operation_response(result.operation),
|
||||||
"operationId": format!("publish-world-{session_id}"),
|
|
||||||
"type": "publish_world",
|
|
||||||
"status": "completed",
|
|
||||||
"phaseLabel": "世界已发布",
|
|
||||||
"phaseDetail": format!("正式世界档案已写入作品库:{}。", publish_result.entry.profile_id),
|
|
||||||
"progress": 100,
|
|
||||||
"error": Value::Null,
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -722,6 +743,37 @@ fn map_custom_world_gallery_card_response(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_custom_world_work_summary_response(
|
||||||
|
item: CustomWorldWorkSummaryRecord,
|
||||||
|
) -> CustomWorldWorkSummaryResponse {
|
||||||
|
CustomWorldWorkSummaryResponse {
|
||||||
|
work_id: item.work_id,
|
||||||
|
source_type: item.source_type,
|
||||||
|
status: item.status,
|
||||||
|
title: item.title,
|
||||||
|
subtitle: item.subtitle,
|
||||||
|
summary: item.summary,
|
||||||
|
cover_image_src: item.cover_image_src,
|
||||||
|
cover_render_mode: item.cover_render_mode,
|
||||||
|
cover_character_image_srcs: item.cover_character_image_srcs,
|
||||||
|
updated_at: item.updated_at,
|
||||||
|
published_at: item.published_at,
|
||||||
|
stage: item.stage,
|
||||||
|
stage_label: item.stage_label,
|
||||||
|
playable_npc_count: item.playable_npc_count,
|
||||||
|
landmark_count: item.landmark_count,
|
||||||
|
role_visual_ready_count: item.role_visual_ready_count,
|
||||||
|
role_animation_ready_count: item.role_animation_ready_count,
|
||||||
|
role_asset_summary_label: item.role_asset_summary_label,
|
||||||
|
session_id: item.session_id,
|
||||||
|
profile_id: item.profile_id,
|
||||||
|
can_resume: item.can_resume,
|
||||||
|
can_enter_world: item.can_enter_world,
|
||||||
|
blocker_count: item.blocker_count,
|
||||||
|
publish_ready: item.publish_ready,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn map_custom_world_agent_session_response(
|
fn map_custom_world_agent_session_response(
|
||||||
session: CustomWorldAgentSessionRecord,
|
session: CustomWorldAgentSessionRecord,
|
||||||
) -> CustomWorldAgentSessionSnapshotResponse {
|
) -> CustomWorldAgentSessionSnapshotResponse {
|
||||||
@@ -763,11 +815,28 @@ fn map_custom_world_agent_session_response(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(map_custom_world_supported_action_response)
|
.map(map_custom_world_supported_action_response)
|
||||||
.collect(),
|
.collect(),
|
||||||
|
publish_gate: session.publish_gate.map(map_custom_world_publish_gate_response),
|
||||||
result_preview: session.result_preview,
|
result_preview: session.result_preview,
|
||||||
updated_at: session.updated_at,
|
updated_at: session.updated_at,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_custom_world_publish_gate_response(
|
||||||
|
gate: CustomWorldPublishGateRecord,
|
||||||
|
) -> CustomWorldPublishGateResponse {
|
||||||
|
CustomWorldPublishGateResponse {
|
||||||
|
profile_id: gate.profile_id,
|
||||||
|
blockers: gate
|
||||||
|
.blockers
|
||||||
|
.into_iter()
|
||||||
|
.map(map_custom_world_result_preview_blocker_response)
|
||||||
|
.collect(),
|
||||||
|
blocker_count: gate.blocker_count,
|
||||||
|
publish_ready: gate.publish_ready,
|
||||||
|
can_enter_world: gate.can_enter_world,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn map_custom_world_agent_message_response(
|
fn map_custom_world_agent_message_response(
|
||||||
message: CustomWorldAgentMessageRecord,
|
message: CustomWorldAgentMessageRecord,
|
||||||
) -> CustomWorldAgentMessageResponse {
|
) -> CustomWorldAgentMessageResponse {
|
||||||
@@ -812,6 +881,38 @@ fn map_custom_world_draft_card_response(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_custom_world_draft_card_detail_response(
|
||||||
|
card: CustomWorldDraftCardDetailRecord,
|
||||||
|
) -> CustomWorldDraftCardDetailResponse {
|
||||||
|
CustomWorldDraftCardDetailResponse {
|
||||||
|
id: card.card_id,
|
||||||
|
kind: card.kind,
|
||||||
|
title: card.title,
|
||||||
|
sections: card
|
||||||
|
.sections
|
||||||
|
.into_iter()
|
||||||
|
.map(map_custom_world_draft_card_detail_section_response)
|
||||||
|
.collect(),
|
||||||
|
linked_ids: card.linked_ids,
|
||||||
|
locked: card.locked,
|
||||||
|
editable: card.editable,
|
||||||
|
editable_section_ids: card.editable_section_ids,
|
||||||
|
warning_messages: card.warning_messages,
|
||||||
|
asset_status: card.asset_status,
|
||||||
|
asset_status_label: card.asset_status_label,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_custom_world_draft_card_detail_section_response(
|
||||||
|
section: CustomWorldDraftCardDetailSectionRecord,
|
||||||
|
) -> CustomWorldDraftCardDetailSectionResponse {
|
||||||
|
CustomWorldDraftCardDetailSectionResponse {
|
||||||
|
id: section.section_id,
|
||||||
|
label: section.label,
|
||||||
|
value: section.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn map_custom_world_agent_checkpoint_response(
|
fn map_custom_world_agent_checkpoint_response(
|
||||||
checkpoint: CustomWorldAgentCheckpointRecord,
|
checkpoint: CustomWorldAgentCheckpointRecord,
|
||||||
) -> CustomWorldAgentCheckpointResponse {
|
) -> CustomWorldAgentCheckpointResponse {
|
||||||
@@ -832,6 +933,16 @@ fn map_custom_world_supported_action_response(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_custom_world_result_preview_blocker_response(
|
||||||
|
blocker: CustomWorldResultPreviewBlockerRecord,
|
||||||
|
) -> CustomWorldResultPreviewBlockerResponse {
|
||||||
|
CustomWorldResultPreviewBlockerResponse {
|
||||||
|
id: blocker.id,
|
||||||
|
code: blocker.code,
|
||||||
|
message: blocker.message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_stream_reply_text(session: &CustomWorldAgentSessionSnapshotResponse) -> String {
|
fn resolve_stream_reply_text(session: &CustomWorldAgentSessionSnapshotResponse) -> String {
|
||||||
session
|
session
|
||||||
.last_assistant_reply
|
.last_assistant_reply
|
||||||
|
|||||||
636
server-rs/crates/api-server/src/custom_world_ai.rs
Normal file
636
server-rs/crates/api-server/src/custom_world_ai.rs
Normal file
@@ -0,0 +1,636 @@
|
|||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
extract::{Extension, State, rejection::JsonRejection},
|
||||||
|
http::StatusCode,
|
||||||
|
response::Response,
|
||||||
|
};
|
||||||
|
use platform_llm::{LlmMessage, LlmTextRequest};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::{Map, Value, json};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError,
|
||||||
|
request_context::RequestContext, state::AppState,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) struct CustomWorldEntityRequest {
|
||||||
|
profile: Value,
|
||||||
|
kind: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) struct CustomWorldSceneNpcRequest {
|
||||||
|
profile: Value,
|
||||||
|
landmark_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) struct CustomWorldSceneImageRequest {
|
||||||
|
#[serde(default)]
|
||||||
|
profile_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
world_name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
landmark_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
landmark_name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
prompt: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
size: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) struct CustomWorldCoverImageRequest {
|
||||||
|
profile: Value,
|
||||||
|
#[serde(default)]
|
||||||
|
user_prompt: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
size: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) struct CustomWorldCoverUploadRequest {
|
||||||
|
#[serde(default)]
|
||||||
|
profile_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
world_name: Option<String>,
|
||||||
|
image_data_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct GeneratedAssetResponse {
|
||||||
|
image_src: String,
|
||||||
|
asset_id: String,
|
||||||
|
source_type: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
model: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
size: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
task_id: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
prompt: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
actual_prompt: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate_custom_world_entity(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Extension(request_context): Extension<RequestContext>,
|
||||||
|
Extension(_authenticated): Extension<AuthenticatedAccessToken>,
|
||||||
|
payload: Result<Json<CustomWorldEntityRequest>, JsonRejection>,
|
||||||
|
) -> Result<Json<Value>, Response> {
|
||||||
|
let Json(payload) = payload.map_err(|error| {
|
||||||
|
custom_world_ai_error_response(
|
||||||
|
&request_context,
|
||||||
|
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||||
|
"provider": "custom-world-ai",
|
||||||
|
"message": error.body_text(),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let kind = payload.kind.trim();
|
||||||
|
if !matches!(kind, "playable" | "story" | "landmark") {
|
||||||
|
return Err(custom_world_ai_error_response(
|
||||||
|
&request_context,
|
||||||
|
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||||
|
"provider": "custom-world-ai",
|
||||||
|
"message": "kind 必须是 playable、story 或 landmark",
|
||||||
|
})),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let entity = generate_entity_with_fallback(&state, &payload.profile, kind).await;
|
||||||
|
|
||||||
|
Ok(json_success_body(
|
||||||
|
Some(&request_context),
|
||||||
|
json!({
|
||||||
|
"kind": kind,
|
||||||
|
"entity": entity,
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate_custom_world_scene_npc(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Extension(request_context): Extension<RequestContext>,
|
||||||
|
Extension(_authenticated): Extension<AuthenticatedAccessToken>,
|
||||||
|
payload: Result<Json<CustomWorldSceneNpcRequest>, JsonRejection>,
|
||||||
|
) -> Result<Json<Value>, Response> {
|
||||||
|
let Json(payload) = payload.map_err(|error| {
|
||||||
|
custom_world_ai_error_response(
|
||||||
|
&request_context,
|
||||||
|
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||||
|
"provider": "custom-world-ai",
|
||||||
|
"message": error.body_text(),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let landmark_id = payload.landmark_id.trim();
|
||||||
|
if landmark_id.is_empty() {
|
||||||
|
return Err(custom_world_ai_error_response(
|
||||||
|
&request_context,
|
||||||
|
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||||
|
"provider": "custom-world-ai",
|
||||||
|
"message": "landmarkId is required",
|
||||||
|
})),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let npc = generate_scene_npc_with_fallback(&state, &payload.profile, landmark_id).await;
|
||||||
|
Ok(json_success_body(
|
||||||
|
Some(&request_context),
|
||||||
|
json!({ "npc": npc }),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate_custom_world_scene_image(
|
||||||
|
Extension(request_context): Extension<RequestContext>,
|
||||||
|
Extension(_authenticated): Extension<AuthenticatedAccessToken>,
|
||||||
|
payload: Result<Json<CustomWorldSceneImageRequest>, JsonRejection>,
|
||||||
|
) -> Result<Json<Value>, Response> {
|
||||||
|
let Json(payload) = payload.map_err(|error| {
|
||||||
|
custom_world_ai_error_response(
|
||||||
|
&request_context,
|
||||||
|
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||||
|
"provider": "custom-world-ai",
|
||||||
|
"message": error.body_text(),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let asset = save_placeholder_asset(
|
||||||
|
"generated-custom-world-scenes",
|
||||||
|
payload
|
||||||
|
.profile_id
|
||||||
|
.as_deref()
|
||||||
|
.or(payload.world_name.as_deref())
|
||||||
|
.unwrap_or("world"),
|
||||||
|
payload
|
||||||
|
.landmark_id
|
||||||
|
.as_deref()
|
||||||
|
.or(payload.landmark_name.as_deref())
|
||||||
|
.unwrap_or("scene"),
|
||||||
|
"scene",
|
||||||
|
payload.size.as_deref().unwrap_or("1280*720"),
|
||||||
|
payload.prompt.as_deref(),
|
||||||
|
)
|
||||||
|
.map_err(|error| custom_world_ai_error_response(&request_context, error))?;
|
||||||
|
|
||||||
|
Ok(json_success_body(Some(&request_context), asset))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate_custom_world_cover_image(
|
||||||
|
Extension(request_context): Extension<RequestContext>,
|
||||||
|
Extension(_authenticated): Extension<AuthenticatedAccessToken>,
|
||||||
|
payload: Result<Json<CustomWorldCoverImageRequest>, JsonRejection>,
|
||||||
|
) -> Result<Json<Value>, Response> {
|
||||||
|
let Json(payload) = payload.map_err(|error| {
|
||||||
|
custom_world_ai_error_response(
|
||||||
|
&request_context,
|
||||||
|
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||||
|
"provider": "custom-world-ai",
|
||||||
|
"message": error.body_text(),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let profile = payload.profile.as_object().cloned().unwrap_or_default();
|
||||||
|
let world_name = read_string_field(&profile, "name").unwrap_or_else(|| "world".to_string());
|
||||||
|
let asset = save_placeholder_asset(
|
||||||
|
"generated-custom-world-covers",
|
||||||
|
&read_string_field(&profile, "id").unwrap_or_else(|| world_name.clone()),
|
||||||
|
"cover",
|
||||||
|
"cover",
|
||||||
|
payload.size.as_deref().unwrap_or("1600*900"),
|
||||||
|
payload.user_prompt.as_deref(),
|
||||||
|
)
|
||||||
|
.map_err(|error| custom_world_ai_error_response(&request_context, error))?;
|
||||||
|
|
||||||
|
Ok(json_success_body(Some(&request_context), asset))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn upload_custom_world_cover_image(
|
||||||
|
Extension(request_context): Extension<RequestContext>,
|
||||||
|
Extension(_authenticated): Extension<AuthenticatedAccessToken>,
|
||||||
|
payload: Result<Json<CustomWorldCoverUploadRequest>, JsonRejection>,
|
||||||
|
) -> Result<Json<Value>, Response> {
|
||||||
|
let Json(payload) = payload.map_err(|error| {
|
||||||
|
custom_world_ai_error_response(
|
||||||
|
&request_context,
|
||||||
|
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||||
|
"provider": "custom-world-ai",
|
||||||
|
"message": error.body_text(),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let parsed = parse_image_data_url(payload.image_data_url.trim()).ok_or_else(|| {
|
||||||
|
custom_world_ai_error_response(
|
||||||
|
&request_context,
|
||||||
|
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||||
|
"provider": "custom-world-ai",
|
||||||
|
"message": "imageDataUrl 必须是有效的图片 Data URL",
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let asset_id = format!("custom-cover-upload-{}", current_utc_millis());
|
||||||
|
let world_segment = sanitize_path_segment(
|
||||||
|
payload
|
||||||
|
.profile_id
|
||||||
|
.as_deref()
|
||||||
|
.or(payload.world_name.as_deref())
|
||||||
|
.unwrap_or("world"),
|
||||||
|
"world",
|
||||||
|
);
|
||||||
|
let relative_dir = PathBuf::from("generated-custom-world-covers")
|
||||||
|
.join(world_segment)
|
||||||
|
.join(&asset_id);
|
||||||
|
let output_dir = resolve_public_output_dir(&relative_dir)
|
||||||
|
.map_err(|error| custom_world_ai_error_response(&request_context, error))?;
|
||||||
|
fs::create_dir_all(&output_dir)
|
||||||
|
.map_err(io_error)
|
||||||
|
.map_err(|error| custom_world_ai_error_response(&request_context, error))?;
|
||||||
|
let file_name = match parsed.mime_type.as_str() {
|
||||||
|
"image/png" => "cover.png",
|
||||||
|
"image/webp" => "cover.webp",
|
||||||
|
_ => "cover.jpg",
|
||||||
|
};
|
||||||
|
fs::write(output_dir.join(file_name), parsed.bytes)
|
||||||
|
.map_err(io_error)
|
||||||
|
.map_err(|error| custom_world_ai_error_response(&request_context, error))?;
|
||||||
|
let image_src = format!(
|
||||||
|
"/{}/{}",
|
||||||
|
relative_dir.to_string_lossy().replace('\\', "/"),
|
||||||
|
file_name
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(json_success_body(
|
||||||
|
Some(&request_context),
|
||||||
|
GeneratedAssetResponse {
|
||||||
|
image_src,
|
||||||
|
asset_id,
|
||||||
|
source_type: "uploaded".to_string(),
|
||||||
|
model: None,
|
||||||
|
size: None,
|
||||||
|
task_id: None,
|
||||||
|
prompt: None,
|
||||||
|
actual_prompt: None,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_entity_with_fallback(
|
||||||
|
state: &AppState,
|
||||||
|
profile: &Value,
|
||||||
|
kind: &str,
|
||||||
|
) -> Value {
|
||||||
|
let fallback = build_entity_fallback(profile, kind);
|
||||||
|
let Some(llm_client) = state.llm_client() else {
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
let request = LlmTextRequest::new(vec![
|
||||||
|
LlmMessage::system(
|
||||||
|
"你是 RPG 自定义世界实体生成器。只输出一个 JSON 对象,不要输出 Markdown。",
|
||||||
|
),
|
||||||
|
LlmMessage::user(
|
||||||
|
json!({
|
||||||
|
"task": "generate_custom_world_entity",
|
||||||
|
"kind": kind,
|
||||||
|
"profile": profile,
|
||||||
|
"fallback": fallback,
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
llm_client
|
||||||
|
.request_text(request)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|response| serde_json::from_str::<Value>(response.content.trim()).ok())
|
||||||
|
.unwrap_or(fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_scene_npc_with_fallback(
|
||||||
|
state: &AppState,
|
||||||
|
profile: &Value,
|
||||||
|
landmark_id: &str,
|
||||||
|
) -> Value {
|
||||||
|
let fallback = build_scene_npc_fallback(profile, landmark_id);
|
||||||
|
let Some(llm_client) = state.llm_client() else {
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
let request = LlmTextRequest::new(vec![
|
||||||
|
LlmMessage::system(
|
||||||
|
"你是 RPG 自定义世界场景 NPC 生成器。只输出一个 JSON 对象,不要输出 Markdown。",
|
||||||
|
),
|
||||||
|
LlmMessage::user(
|
||||||
|
json!({
|
||||||
|
"task": "generate_custom_world_scene_npc",
|
||||||
|
"landmarkId": landmark_id,
|
||||||
|
"profile": profile,
|
||||||
|
"fallback": fallback,
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
llm_client
|
||||||
|
.request_text(request)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|response| serde_json::from_str::<Value>(response.content.trim()).ok())
|
||||||
|
.unwrap_or(fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_entity_fallback(profile: &Value, kind: &str) -> Value {
|
||||||
|
let object = profile.as_object().cloned().unwrap_or_default();
|
||||||
|
let world_name = read_string_field(&object, "name").unwrap_or_else(|| "自定义世界".to_string());
|
||||||
|
match kind {
|
||||||
|
"playable" => build_role_fallback("playable", "新同行者", &world_name, 18),
|
||||||
|
"story" => build_role_fallback("story", "新场景角色", &world_name, 6),
|
||||||
|
"landmark" => build_landmark_fallback(&world_name),
|
||||||
|
_ => json!({}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_scene_npc_fallback(profile: &Value, landmark_id: &str) -> Value {
|
||||||
|
let object = profile.as_object().cloned().unwrap_or_default();
|
||||||
|
let world_name = read_string_field(&object, "name").unwrap_or_else(|| "自定义世界".to_string());
|
||||||
|
let landmark_name = object
|
||||||
|
.get("landmarks")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.and_then(|entries| {
|
||||||
|
entries.iter().find_map(|entry| {
|
||||||
|
let object = entry.as_object()?;
|
||||||
|
(read_string_field(object, "id").as_deref() == Some(landmark_id))
|
||||||
|
.then(|| read_string_field(object, "name"))
|
||||||
|
.flatten()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "当前场景".to_string());
|
||||||
|
let mut npc = build_role_fallback("story", &format!("{landmark_name}来客"), &world_name, 6);
|
||||||
|
if let Some(object) = npc.as_object_mut() {
|
||||||
|
object.insert(
|
||||||
|
"description".to_string(),
|
||||||
|
Value::String(format!("长期活动于{landmark_name},熟悉这里的局势与暗线。")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
npc
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_role_fallback(prefix: &str, name: &str, world_name: &str, affinity: i64) -> Value {
|
||||||
|
let suffix = current_utc_millis();
|
||||||
|
json!({
|
||||||
|
"id": format!("{prefix}-{}", suffix),
|
||||||
|
"name": name,
|
||||||
|
"title": "关键角色",
|
||||||
|
"role": "关键角色",
|
||||||
|
"description": format!("围绕《{world_name}》当前主线冲突生成的新增角色。"),
|
||||||
|
"backstory": format!("他与《{world_name}》正在展开的局势存在直接牵连。"),
|
||||||
|
"personality": "谨慎、敏锐,先观察再表态。",
|
||||||
|
"motivation": "希望借玩家的介入改变当前失衡局面。",
|
||||||
|
"combatStyle": "偏向试探与控场。",
|
||||||
|
"initialAffinity": affinity,
|
||||||
|
"relationshipHooks": ["与玩家保持试探", "掌握局势暗线"],
|
||||||
|
"relations": [],
|
||||||
|
"tags": ["自定义", "生成"],
|
||||||
|
"backstoryReveal": {
|
||||||
|
"publicSummary": "一个掌握部分旧线索的关键角色。",
|
||||||
|
"chapters": [
|
||||||
|
{ "id": "surface", "title": "表层来意", "affinityRequired": 6, "teaser": "他知道这里正在发生什么。", "content": "他一直在观察这片区域的变化。", "contextSnippet": "" },
|
||||||
|
{ "id": "scar", "title": "旧事裂痕", "affinityRequired": 12, "teaser": "他与旧案有直接关联。", "content": "过往的一次事件把他绑定在这条线里。", "contextSnippet": "" },
|
||||||
|
{ "id": "hidden", "title": "隐藏执念", "affinityRequired": 18, "teaser": "他真正想推动的局面还没说出口。", "content": "他一直在寻找能撬动局面的机会。", "contextSnippet": "" },
|
||||||
|
{ "id": "final", "title": "最终底牌", "affinityRequired": 24, "teaser": "他手里还压着一张底牌。", "content": "一旦局势逼近临界点,他会出手。", "contextSnippet": "" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
{ "id": format!("skill-{}-1", suffix), "name": "试探起手", "summary": "先判断局势与对手意图。", "style": "试探压制" },
|
||||||
|
{ "id": format!("skill-{}-2", suffix), "name": "借势压场", "summary": "利用环境为自己制造主动权。", "style": "环境协同" },
|
||||||
|
{ "id": format!("skill-{}-3", suffix), "name": "暗线反制", "summary": "在关键节点打乱对方节奏。", "style": "后手翻盘" }
|
||||||
|
],
|
||||||
|
"initialItems": [
|
||||||
|
{ "id": format!("item-{}-1", suffix), "name": "随身兵装", "category": "武器", "quantity": 1, "rarity": "rare", "description": "常备的近身装备。", "tags": ["自定义"] },
|
||||||
|
{ "id": format!("item-{}-2", suffix), "name": "私人物件", "category": "道具", "quantity": 1, "rarity": "uncommon", "description": "可在关键时刻调用的人情或凭证。", "tags": ["自定义"] },
|
||||||
|
{ "id": format!("item-{}-3", suffix), "name": "线索残页", "category": "专属物品", "quantity": 1, "rarity": "rare", "description": "记录部分隐藏线索。", "tags": ["线索"] }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_landmark_fallback(world_name: &str) -> Value {
|
||||||
|
let suffix = current_utc_millis();
|
||||||
|
json!({
|
||||||
|
"id": format!("landmark-{}", suffix),
|
||||||
|
"name": "新场景",
|
||||||
|
"description": format!("围绕《{world_name}》当前主线冲突扩展出的关键场景。"),
|
||||||
|
"visualDescription": "低照度、层次复杂、带有明显环境叙事痕迹。",
|
||||||
|
"dangerLevel": "medium",
|
||||||
|
"sceneNpcIds": [],
|
||||||
|
"connections": [],
|
||||||
|
"narrativeResidues": [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_placeholder_asset(
|
||||||
|
root_segment: &str,
|
||||||
|
world_segment_seed: &str,
|
||||||
|
leaf_segment_seed: &str,
|
||||||
|
file_stem: &str,
|
||||||
|
size: &str,
|
||||||
|
prompt: Option<&str>,
|
||||||
|
) -> Result<GeneratedAssetResponse, AppError> {
|
||||||
|
let asset_id = format!("{file_stem}-{}", current_utc_millis());
|
||||||
|
let relative_dir = PathBuf::from(root_segment)
|
||||||
|
.join(sanitize_path_segment(world_segment_seed, "world"))
|
||||||
|
.join(sanitize_path_segment(leaf_segment_seed, file_stem))
|
||||||
|
.join(&asset_id);
|
||||||
|
let output_dir = resolve_public_output_dir(&relative_dir)?;
|
||||||
|
fs::create_dir_all(&output_dir).map_err(io_error)?;
|
||||||
|
let file_name = format!("{file_stem}.svg");
|
||||||
|
let svg = build_placeholder_svg(size, prompt.unwrap_or(file_stem));
|
||||||
|
fs::write(output_dir.join(&file_name), svg).map_err(io_error)?;
|
||||||
|
|
||||||
|
Ok(GeneratedAssetResponse {
|
||||||
|
image_src: format!("/{}/{}", relative_dir.to_string_lossy().replace('\\', "/"), file_name),
|
||||||
|
asset_id: asset_id.clone(),
|
||||||
|
source_type: "generated".to_string(),
|
||||||
|
model: Some("rust-placeholder".to_string()),
|
||||||
|
size: Some(size.to_string()),
|
||||||
|
task_id: Some(asset_id),
|
||||||
|
prompt: prompt.map(ToOwned::to_owned),
|
||||||
|
actual_prompt: prompt.map(ToOwned::to_owned),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_placeholder_svg(size: &str, label: &str) -> String {
|
||||||
|
let (width, height) = parse_size(size);
|
||||||
|
format!(
|
||||||
|
r##"<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#0f172a"/>
|
||||||
|
<stop offset="55%" stop-color="#164e63"/>
|
||||||
|
<stop offset="100%" stop-color="#0b1120"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="100%" height="100%" fill="url(#bg)"/>
|
||||||
|
<circle cx="{cx1}" cy="{cy1}" r="{r1}" fill="rgba(255,255,255,0.12)"/>
|
||||||
|
<circle cx="{cx2}" cy="{cy2}" r="{r2}" fill="rgba(125,211,252,0.14)"/>
|
||||||
|
<text x="50%" y="46%" text-anchor="middle" fill="#e2e8f0" font-size="{font_main}" font-family="Microsoft YaHei, PingFang SC, sans-serif">{title}</text>
|
||||||
|
<text x="50%" y="56%" text-anchor="middle" fill="#bae6fd" font-size="{font_sub}" font-family="Microsoft YaHei, PingFang SC, sans-serif">Rust fallback asset</text>
|
||||||
|
</svg>"##,
|
||||||
|
width = width,
|
||||||
|
height = height,
|
||||||
|
cx1 = width / 3,
|
||||||
|
cy1 = height / 3,
|
||||||
|
r1 = (width.min(height) / 7).max(24),
|
||||||
|
cx2 = width * 3 / 4,
|
||||||
|
cy2 = height / 4,
|
||||||
|
r2 = (width.min(height) / 9).max(18),
|
||||||
|
font_main = (width.min(height) / 12).max(20),
|
||||||
|
font_sub = (width.min(height) / 24).max(12),
|
||||||
|
title = escape_svg_text(label),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_size(size: &str) -> (u32, u32) {
|
||||||
|
let mut parts = size.split('*');
|
||||||
|
let width = parts
|
||||||
|
.next()
|
||||||
|
.and_then(|value| value.trim().parse::<u32>().ok())
|
||||||
|
.filter(|value| *value > 0)
|
||||||
|
.unwrap_or(1280);
|
||||||
|
let height = parts
|
||||||
|
.next()
|
||||||
|
.and_then(|value| value.trim().parse::<u32>().ok())
|
||||||
|
.filter(|value| *value > 0)
|
||||||
|
.unwrap_or(720);
|
||||||
|
(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escape_svg_text(value: &str) -> String {
|
||||||
|
value
|
||||||
|
.replace('&', "&")
|
||||||
|
.replace('<', "<")
|
||||||
|
.replace('>', ">")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanitize_path_segment(value: &str, fallback: &str) -> String {
|
||||||
|
let sanitized = value
|
||||||
|
.trim()
|
||||||
|
.chars()
|
||||||
|
.map(|ch| {
|
||||||
|
if ch.is_ascii_alphanumeric() || ('\u{4e00}'..='\u{9fff}').contains(&ch) {
|
||||||
|
ch
|
||||||
|
} else {
|
||||||
|
'-'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<String>()
|
||||||
|
.trim_matches('-')
|
||||||
|
.to_string();
|
||||||
|
if sanitized.is_empty() {
|
||||||
|
fallback.to_string()
|
||||||
|
} else {
|
||||||
|
sanitized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_public_output_dir(relative_dir: &Path) -> Result<PathBuf, AppError> {
|
||||||
|
let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.ancestors()
|
||||||
|
.nth(3)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.with_message("无法解析仓库根目录")
|
||||||
|
})?;
|
||||||
|
Ok(workspace_root.join("public").join(relative_dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_image_data_url(value: &str) -> Option<ParsedImageDataUrl> {
|
||||||
|
let prefix = "data:";
|
||||||
|
let separator = ";base64,";
|
||||||
|
let body = value.strip_prefix(prefix)?;
|
||||||
|
let (mime_type, data) = body.split_once(separator)?;
|
||||||
|
let bytes = decode_base64(data)?;
|
||||||
|
Some(ParsedImageDataUrl {
|
||||||
|
mime_type: mime_type.to_string(),
|
||||||
|
bytes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_base64(value: &str) -> Option<Vec<u8>> {
|
||||||
|
let cleaned = value.trim().replace(char::is_whitespace, "");
|
||||||
|
let mut output = Vec::with_capacity(cleaned.len() * 3 / 4);
|
||||||
|
let mut buffer = 0u32;
|
||||||
|
let mut bits = 0u8;
|
||||||
|
|
||||||
|
for byte in cleaned.bytes() {
|
||||||
|
let value = match byte {
|
||||||
|
b'A'..=b'Z' => byte - b'A',
|
||||||
|
b'a'..=b'z' => byte - b'a' + 26,
|
||||||
|
b'0'..=b'9' => byte - b'0' + 52,
|
||||||
|
b'+' => 62,
|
||||||
|
b'/' => 63,
|
||||||
|
b'=' => break,
|
||||||
|
_ => return None,
|
||||||
|
} as u32;
|
||||||
|
buffer = (buffer << 6) | value;
|
||||||
|
bits += 6;
|
||||||
|
while bits >= 8 {
|
||||||
|
bits -= 8;
|
||||||
|
output.push(((buffer >> bits) & 0xFF) as u8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_string_field(object: &Map<String, Value>, key: &str) -> Option<String> {
|
||||||
|
object
|
||||||
|
.get(key)
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_utc_millis() -> i64 {
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
let duration = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("system time should be after unix epoch");
|
||||||
|
i64::try_from(duration.as_millis()).expect("current unix millis should fit in i64")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn io_error(error: std::io::Error) -> AppError {
|
||||||
|
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
||||||
|
"provider": "custom-world-ai",
|
||||||
|
"message": format!("文件写入失败:{error}"),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn custom_world_ai_error_response(request_context: &RequestContext, error: AppError) -> Response {
|
||||||
|
error.into_response_with_context(Some(request_context))
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ParsedImageDataUrl {
|
||||||
|
mime_type: String,
|
||||||
|
bytes: Vec<u8>,
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ mod auth_session;
|
|||||||
mod auth_sessions;
|
mod auth_sessions;
|
||||||
mod config;
|
mod config;
|
||||||
mod custom_world;
|
mod custom_world;
|
||||||
|
mod custom_world_ai;
|
||||||
mod error_middleware;
|
mod error_middleware;
|
||||||
mod health;
|
mod health;
|
||||||
mod http_error;
|
mod http_error;
|
||||||
|
|||||||
@@ -106,6 +106,9 @@ use crate::module_bindings::{
|
|||||||
BattleStateSnapshot as BindingBattleStateSnapshot, BattleStatus as BindingBattleStatus,
|
BattleStateSnapshot as BindingBattleStateSnapshot, BattleStatus as BindingBattleStatus,
|
||||||
CombatOutcome as BindingCombatOutcome,
|
CombatOutcome as BindingCombatOutcome,
|
||||||
CustomWorldAgentMessageSnapshot as BindingCustomWorldAgentMessageSnapshot,
|
CustomWorldAgentMessageSnapshot as BindingCustomWorldAgentMessageSnapshot,
|
||||||
|
CustomWorldAgentActionExecuteInput as BindingCustomWorldAgentActionExecuteInput,
|
||||||
|
CustomWorldAgentActionExecuteResult as BindingCustomWorldAgentActionExecuteResult,
|
||||||
|
CustomWorldAgentCardDetailGetInput as BindingCustomWorldAgentCardDetailGetInput,
|
||||||
CustomWorldAgentMessageSubmitInput as BindingCustomWorldAgentMessageSubmitInput,
|
CustomWorldAgentMessageSubmitInput as BindingCustomWorldAgentMessageSubmitInput,
|
||||||
CustomWorldAgentOperationGetInput as BindingCustomWorldAgentOperationGetInput,
|
CustomWorldAgentOperationGetInput as BindingCustomWorldAgentOperationGetInput,
|
||||||
CustomWorldAgentOperationProcedureResult as BindingCustomWorldAgentOperationProcedureResult,
|
CustomWorldAgentOperationProcedureResult as BindingCustomWorldAgentOperationProcedureResult,
|
||||||
@@ -114,6 +117,9 @@ use crate::module_bindings::{
|
|||||||
CustomWorldAgentSessionGetInput as BindingCustomWorldAgentSessionGetInput,
|
CustomWorldAgentSessionGetInput as BindingCustomWorldAgentSessionGetInput,
|
||||||
CustomWorldAgentSessionProcedureResult as BindingCustomWorldAgentSessionProcedureResult,
|
CustomWorldAgentSessionProcedureResult as BindingCustomWorldAgentSessionProcedureResult,
|
||||||
CustomWorldAgentSessionSnapshot as BindingCustomWorldAgentSessionSnapshot,
|
CustomWorldAgentSessionSnapshot as BindingCustomWorldAgentSessionSnapshot,
|
||||||
|
CustomWorldDraftCardDetailResult as BindingCustomWorldDraftCardDetailResult,
|
||||||
|
CustomWorldDraftCardDetailSectionSnapshot as BindingCustomWorldDraftCardDetailSectionSnapshot,
|
||||||
|
CustomWorldDraftCardDetailSnapshot as BindingCustomWorldDraftCardDetailSnapshot,
|
||||||
CustomWorldDraftCardSnapshot as BindingCustomWorldDraftCardSnapshot,
|
CustomWorldDraftCardSnapshot as BindingCustomWorldDraftCardSnapshot,
|
||||||
CustomWorldGalleryDetailInput as BindingCustomWorldGalleryDetailInput,
|
CustomWorldGalleryDetailInput as BindingCustomWorldGalleryDetailInput,
|
||||||
CustomWorldGalleryEntrySnapshot as BindingCustomWorldGalleryEntrySnapshot,
|
CustomWorldGalleryEntrySnapshot as BindingCustomWorldGalleryEntrySnapshot,
|
||||||
@@ -130,6 +136,9 @@ use crate::module_bindings::{
|
|||||||
CustomWorldPublishWorldInput as BindingCustomWorldPublishWorldInput,
|
CustomWorldPublishWorldInput as BindingCustomWorldPublishWorldInput,
|
||||||
CustomWorldPublishWorldResult as BindingCustomWorldPublishWorldResult,
|
CustomWorldPublishWorldResult as BindingCustomWorldPublishWorldResult,
|
||||||
CustomWorldPublishedProfileCompileSnapshot as BindingCustomWorldPublishedProfileCompileSnapshot,
|
CustomWorldPublishedProfileCompileSnapshot as BindingCustomWorldPublishedProfileCompileSnapshot,
|
||||||
|
CustomWorldWorkSummarySnapshot as BindingCustomWorldWorkSummarySnapshot,
|
||||||
|
CustomWorldWorksListInput as BindingCustomWorldWorksListInput,
|
||||||
|
CustomWorldWorksListResult as BindingCustomWorldWorksListResult,
|
||||||
CustomWorldThemeMode as BindingCustomWorldThemeMode, DbConnection,
|
CustomWorldThemeMode as BindingCustomWorldThemeMode, DbConnection,
|
||||||
InventoryContainerKind as BindingInventoryContainerKind,
|
InventoryContainerKind as BindingInventoryContainerKind,
|
||||||
InventoryEquipmentSlot as BindingInventoryEquipmentSlot,
|
InventoryEquipmentSlot as BindingInventoryEquipmentSlot,
|
||||||
@@ -207,8 +216,10 @@ use crate::module_bindings::{
|
|||||||
create_battle_state_and_return_procedure::create_battle_state_and_return as _,
|
create_battle_state_and_return_procedure::create_battle_state_and_return as _,
|
||||||
create_custom_world_agent_session_procedure::create_custom_world_agent_session as _,
|
create_custom_world_agent_session_procedure::create_custom_world_agent_session as _,
|
||||||
delete_runtime_snapshot_and_return_procedure::delete_runtime_snapshot_and_return as _,
|
delete_runtime_snapshot_and_return_procedure::delete_runtime_snapshot_and_return as _,
|
||||||
|
execute_custom_world_agent_action_procedure::execute_custom_world_agent_action as _,
|
||||||
fail_ai_task_and_return_procedure::fail_ai_task_and_return as _,
|
fail_ai_task_and_return_procedure::fail_ai_task_and_return as _,
|
||||||
get_battle_state_procedure::get_battle_state as _,
|
get_battle_state_procedure::get_battle_state as _,
|
||||||
|
get_custom_world_agent_card_detail_procedure::get_custom_world_agent_card_detail as _,
|
||||||
get_custom_world_agent_operation_procedure::get_custom_world_agent_operation as _,
|
get_custom_world_agent_operation_procedure::get_custom_world_agent_operation as _,
|
||||||
get_custom_world_agent_session_procedure::get_custom_world_agent_session as _,
|
get_custom_world_agent_session_procedure::get_custom_world_agent_session as _,
|
||||||
get_custom_world_gallery_detail_procedure::get_custom_world_gallery_detail as _,
|
get_custom_world_gallery_detail_procedure::get_custom_world_gallery_detail as _,
|
||||||
@@ -221,6 +232,7 @@ use crate::module_bindings::{
|
|||||||
get_story_session_state_procedure::get_story_session_state as _,
|
get_story_session_state_procedure::get_story_session_state as _,
|
||||||
list_custom_world_gallery_entries_procedure::list_custom_world_gallery_entries as _,
|
list_custom_world_gallery_entries_procedure::list_custom_world_gallery_entries as _,
|
||||||
list_custom_world_profiles_procedure::list_custom_world_profiles as _,
|
list_custom_world_profiles_procedure::list_custom_world_profiles as _,
|
||||||
|
list_custom_world_works_procedure::list_custom_world_works as _,
|
||||||
list_platform_browse_history_procedure::list_platform_browse_history as _,
|
list_platform_browse_history_procedure::list_platform_browse_history as _,
|
||||||
list_profile_save_archives_procedure::list_profile_save_archives as _,
|
list_profile_save_archives_procedure::list_profile_save_archives as _,
|
||||||
list_profile_wallet_ledger_procedure::list_profile_wallet_ledger as _,
|
list_profile_wallet_ledger_procedure::list_profile_wallet_ledger as _,
|
||||||
@@ -764,6 +776,76 @@ impl SpacetimeClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_custom_world_works(
|
||||||
|
&self,
|
||||||
|
owner_user_id: String,
|
||||||
|
) -> Result<Vec<CustomWorldWorkSummaryRecord>, SpacetimeClientError> {
|
||||||
|
let procedure_input = BindingCustomWorldWorksListInput { owner_user_id };
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.list_custom_world_works_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_works_list_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_custom_world_agent_card_detail(
|
||||||
|
&self,
|
||||||
|
session_id: String,
|
||||||
|
owner_user_id: String,
|
||||||
|
card_id: String,
|
||||||
|
) -> Result<CustomWorldDraftCardDetailRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = BindingCustomWorldAgentCardDetailGetInput {
|
||||||
|
session_id,
|
||||||
|
owner_user_id,
|
||||||
|
card_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.get_custom_world_agent_card_detail_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_draft_card_detail_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn execute_custom_world_agent_action(
|
||||||
|
&self,
|
||||||
|
input: CustomWorldAgentActionExecuteRecordInput,
|
||||||
|
) -> Result<CustomWorldAgentActionExecuteRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = BindingCustomWorldAgentActionExecuteInput {
|
||||||
|
session_id: input.session_id,
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
operation_id: input.operation_id,
|
||||||
|
action: input.action,
|
||||||
|
payload_json: input.payload_json,
|
||||||
|
submitted_at_micros: input.submitted_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.execute_custom_world_agent_action_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(map_custom_world_agent_action_execute_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn submit_custom_world_agent_message(
|
pub async fn submit_custom_world_agent_message(
|
||||||
&self,
|
&self,
|
||||||
input: CustomWorldAgentMessageSubmitRecordInput,
|
input: CustomWorldAgentMessageSubmitRecordInput,
|
||||||
@@ -2259,6 +2341,66 @@ fn map_custom_world_agent_operation_procedure_result(
|
|||||||
Ok(map_custom_world_agent_operation_snapshot(operation))
|
Ok(map_custom_world_agent_operation_snapshot(operation))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_custom_world_works_list_result(
|
||||||
|
result: BindingCustomWorldWorksListResult,
|
||||||
|
) -> Result<Vec<CustomWorldWorkSummaryRecord>, SpacetimeClientError> {
|
||||||
|
if !result.ok {
|
||||||
|
return Err(SpacetimeClientError::Procedure(
|
||||||
|
result
|
||||||
|
.error_message
|
||||||
|
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
.items
|
||||||
|
.into_iter()
|
||||||
|
.map(map_custom_world_work_summary_snapshot)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_custom_world_draft_card_detail_result(
|
||||||
|
result: BindingCustomWorldDraftCardDetailResult,
|
||||||
|
) -> Result<CustomWorldDraftCardDetailRecord, SpacetimeClientError> {
|
||||||
|
if !result.ok {
|
||||||
|
return Err(SpacetimeClientError::Procedure(
|
||||||
|
result
|
||||||
|
.error_message
|
||||||
|
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let card = result.card.ok_or_else(|| {
|
||||||
|
SpacetimeClientError::Procedure(
|
||||||
|
"SpacetimeDB procedure 未返回 custom world card detail 快照".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
map_custom_world_draft_card_detail_snapshot(card)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_custom_world_agent_action_execute_result(
|
||||||
|
result: BindingCustomWorldAgentActionExecuteResult,
|
||||||
|
) -> Result<CustomWorldAgentActionExecuteRecord, SpacetimeClientError> {
|
||||||
|
if !result.ok {
|
||||||
|
return Err(SpacetimeClientError::Procedure(
|
||||||
|
result
|
||||||
|
.error_message
|
||||||
|
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let operation = result.operation.ok_or_else(|| {
|
||||||
|
SpacetimeClientError::Procedure(
|
||||||
|
"SpacetimeDB procedure 未返回 custom world action operation 快照".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(CustomWorldAgentActionExecuteRecord {
|
||||||
|
operation: map_custom_world_agent_operation_snapshot(operation),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn map_story_session_procedure_result(
|
fn map_story_session_procedure_result(
|
||||||
result: BindingStorySessionProcedureResult,
|
result: BindingStorySessionProcedureResult,
|
||||||
) -> Result<StorySessionResultRecord, SpacetimeClientError> {
|
) -> Result<StorySessionResultRecord, SpacetimeClientError> {
|
||||||
@@ -2647,6 +2789,40 @@ fn map_custom_world_published_profile_compile_snapshot(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_custom_world_work_summary_snapshot(
|
||||||
|
snapshot: BindingCustomWorldWorkSummarySnapshot,
|
||||||
|
) -> 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn map_custom_world_agent_session_snapshot(
|
fn map_custom_world_agent_session_snapshot(
|
||||||
snapshot: BindingCustomWorldAgentSessionSnapshot,
|
snapshot: BindingCustomWorldAgentSessionSnapshot,
|
||||||
) -> Result<CustomWorldAgentSessionRecord, SpacetimeClientError> {
|
) -> Result<CustomWorldAgentSessionRecord, SpacetimeClientError> {
|
||||||
@@ -2706,6 +2882,12 @@ fn map_custom_world_agent_session_snapshot(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(map_custom_world_checkpoint_record)
|
.map(map_custom_world_checkpoint_record)
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.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 {
|
Ok(CustomWorldAgentSessionRecord {
|
||||||
session_id: snapshot.session_id,
|
session_id: snapshot.session_id,
|
||||||
@@ -2736,12 +2918,8 @@ fn map_custom_world_agent_session_snapshot(
|
|||||||
quality_findings,
|
quality_findings,
|
||||||
asset_coverage,
|
asset_coverage,
|
||||||
checkpoints,
|
checkpoints,
|
||||||
supported_actions: build_minimal_custom_world_supported_actions(
|
supported_actions,
|
||||||
snapshot.stage,
|
publish_gate,
|
||||||
snapshot.progress_percent,
|
|
||||||
snapshot.result_preview_json.is_some(),
|
|
||||||
snapshot.checkpoints_json.as_str(),
|
|
||||||
),
|
|
||||||
result_preview: snapshot
|
result_preview: snapshot
|
||||||
.result_preview_json
|
.result_preview_json
|
||||||
.as_deref()
|
.as_deref()
|
||||||
@@ -2797,9 +2975,57 @@ fn map_custom_world_draft_card_snapshot(
|
|||||||
.asset_status
|
.asset_status
|
||||||
.map(format_custom_world_role_asset_status_back),
|
.map(format_custom_world_role_asset_status_back),
|
||||||
asset_status_label: snapshot.asset_status_label,
|
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()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_custom_world_draft_card_detail_snapshot(
|
||||||
|
snapshot: BindingCustomWorldDraftCardDetailSnapshot,
|
||||||
|
) -> 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_custom_world_draft_card_detail_section_snapshot(
|
||||||
|
snapshot: BindingCustomWorldDraftCardDetailSectionSnapshot,
|
||||||
|
) -> CustomWorldDraftCardDetailSectionRecord {
|
||||||
|
CustomWorldDraftCardDetailSectionRecord {
|
||||||
|
section_id: snapshot.section_id,
|
||||||
|
label: snapshot.label,
|
||||||
|
value: snapshot.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn map_story_session_snapshot(snapshot: BindingStorySessionSnapshot) -> StorySessionRecord {
|
fn map_story_session_snapshot(snapshot: BindingStorySessionSnapshot) -> StorySessionRecord {
|
||||||
StorySessionRecord {
|
StorySessionRecord {
|
||||||
story_session_id: snapshot.story_session_id,
|
story_session_id: snapshot.story_session_id,
|
||||||
@@ -3607,45 +3833,159 @@ fn map_custom_world_checkpoint_record(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_minimal_custom_world_supported_actions(
|
fn parse_supported_actions_json(
|
||||||
stage: crate::module_bindings::RpgAgentStage,
|
value: &str,
|
||||||
progress_percent: u32,
|
) -> Result<Vec<CustomWorldSupportedActionRecord>, SpacetimeClientError> {
|
||||||
has_result_preview: bool,
|
parse_json_array(value, "custom world agent supported_actions_json")?
|
||||||
checkpoints_json: &str,
|
.into_iter()
|
||||||
) -> Vec<CustomWorldSupportedActionRecord> {
|
.map(|entry| {
|
||||||
let has_checkpoint = parse_json_array(checkpoints_json, "custom world agent checkpoints_json")
|
let object = entry.as_object().ok_or_else(|| {
|
||||||
.map(|entries| !entries.is_empty())
|
SpacetimeClientError::Runtime(
|
||||||
.unwrap_or(false);
|
"custom world supported action 必须是 JSON object".to_string(),
|
||||||
let refining_ready = matches!(
|
)
|
||||||
stage,
|
})?;
|
||||||
crate::module_bindings::RpgAgentStage::FoundationReview
|
let action = object
|
||||||
| crate::module_bindings::RpgAgentStage::ObjectRefining
|
.get("action")
|
||||||
| crate::module_bindings::RpgAgentStage::VisualRefining
|
.and_then(serde_json::Value::as_str)
|
||||||
| crate::module_bindings::RpgAgentStage::LongTailReview
|
.map(str::trim)
|
||||||
| crate::module_bindings::RpgAgentStage::ReadyToPublish
|
.filter(|value| !value.is_empty())
|
||||||
| crate::module_bindings::RpgAgentStage::Published
|
.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(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
vec![
|
Ok(CustomWorldSupportedActionRecord {
|
||||||
CustomWorldSupportedActionRecord {
|
action: action.to_string(),
|
||||||
action: "draft_foundation".to_string(),
|
enabled,
|
||||||
enabled: progress_percent >= 100,
|
reason: object
|
||||||
reason: (progress_percent < 100)
|
.get("reason")
|
||||||
.then(|| "draft_foundation requires progressPercent >= 100".to_string()),
|
.and_then(serde_json::Value::as_str)
|
||||||
},
|
.map(str::trim)
|
||||||
CustomWorldSupportedActionRecord {
|
.filter(|value| !value.is_empty())
|
||||||
action: "publish_world".to_string(),
|
.map(ToOwned::to_owned),
|
||||||
enabled: refining_ready && has_result_preview,
|
})
|
||||||
reason: (!refining_ready || !has_result_preview)
|
})
|
||||||
.then(|| "publish_world requires refined draft and resultPreview".to_string()),
|
.collect()
|
||||||
},
|
}
|
||||||
CustomWorldSupportedActionRecord {
|
|
||||||
action: "revert_checkpoint".to_string(),
|
fn parse_custom_world_publish_gate_record(
|
||||||
enabled: has_checkpoint,
|
value: &str,
|
||||||
reason: (!has_checkpoint)
|
) -> Result<CustomWorldPublishGateRecord, SpacetimeClientError> {
|
||||||
.then(|| "revert_checkpoint requires at least one checkpoint".to_string()),
|
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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
@@ -3785,6 +4125,7 @@ pub struct CustomWorldDraftCardRecord {
|
|||||||
pub warning_count: u32,
|
pub warning_count: u32,
|
||||||
pub asset_status: Option<String>,
|
pub asset_status: Option<String>,
|
||||||
pub asset_status_label: Option<String>,
|
pub asset_status_label: Option<String>,
|
||||||
|
pub detail_payload: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
@@ -3804,6 +4145,72 @@ pub struct CustomWorldCheckpointRecord {
|
|||||||
// 兼容并行 custom world facade 中仍在使用的旧命名,避免本轮 module-npc 收口被无关改动阻塞。
|
// 兼容并行 custom world facade 中仍在使用的旧命名,避免本轮 module-npc 收口被无关改动阻塞。
|
||||||
pub type CustomWorldAgentCheckpointRecord = CustomWorldCheckpointRecord;
|
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 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 CustomWorldDraftCardDetailSectionRecord {
|
||||||
|
pub section_id: String,
|
||||||
|
pub label: String,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct CustomWorldAgentSessionRecord {
|
pub struct CustomWorldAgentSessionRecord {
|
||||||
pub session_id: String,
|
pub session_id: String,
|
||||||
@@ -3827,6 +4234,7 @@ pub struct CustomWorldAgentSessionRecord {
|
|||||||
pub asset_coverage: serde_json::Value,
|
pub asset_coverage: serde_json::Value,
|
||||||
pub checkpoints: Vec<CustomWorldCheckpointRecord>,
|
pub checkpoints: Vec<CustomWorldCheckpointRecord>,
|
||||||
pub supported_actions: Vec<CustomWorldSupportedActionRecord>,
|
pub supported_actions: Vec<CustomWorldSupportedActionRecord>,
|
||||||
|
pub publish_gate: Option<CustomWorldPublishGateRecord>,
|
||||||
pub result_preview: Option<serde_json::Value>,
|
pub result_preview: Option<serde_json::Value>,
|
||||||
pub updated_at: String,
|
pub updated_at: String,
|
||||||
}
|
}
|
||||||
@@ -3892,6 +4300,21 @@ pub struct CustomWorldAgentMessageSubmitRecordInput {
|
|||||||
pub submitted_at_micros: i64,
|
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)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct ResolveNpcBattleInteractionInput {
|
pub struct ResolveNpcBattleInteractionInput {
|
||||||
pub npc_interaction: DomainResolveNpcInteractionInput,
|
pub npc_interaction: DomainResolveNpcInteractionInput,
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{
|
||||||
|
self as __sdk,
|
||||||
|
__lib,
|
||||||
|
__sats,
|
||||||
|
__ws,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct CustomWorldAgentActionExecuteInput {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl __sdk::InModule for CustomWorldAgentActionExecuteInput {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{
|
||||||
|
self as __sdk,
|
||||||
|
__lib,
|
||||||
|
__sats,
|
||||||
|
__ws,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::custom_world_agent_operation_snapshot_type::CustomWorldAgentOperationSnapshot;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct CustomWorldAgentActionExecuteResult {
|
||||||
|
pub ok: bool,
|
||||||
|
pub operation: Option::<CustomWorldAgentOperationSnapshot>,
|
||||||
|
pub error_message: Option::<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl __sdk::InModule for CustomWorldAgentActionExecuteResult {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{
|
||||||
|
self as __sdk,
|
||||||
|
__lib,
|
||||||
|
__sats,
|
||||||
|
__ws,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct CustomWorldAgentCardDetailGetInput {
|
||||||
|
pub session_id: String,
|
||||||
|
pub owner_user_id: String,
|
||||||
|
pub card_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl __sdk::InModule for CustomWorldAgentCardDetailGetInput {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ pub struct CustomWorldAgentSessionSnapshot {
|
|||||||
pub lock_state_json: Option::<String>,
|
pub lock_state_json: Option::<String>,
|
||||||
pub draft_profile_json: Option::<String>,
|
pub draft_profile_json: Option::<String>,
|
||||||
pub last_assistant_reply: Option::<String>,
|
pub last_assistant_reply: Option::<String>,
|
||||||
|
pub publish_gate_json: Option::<String>,
|
||||||
pub result_preview_json: Option::<String>,
|
pub result_preview_json: Option::<String>,
|
||||||
pub pending_clarifications_json: String,
|
pub pending_clarifications_json: String,
|
||||||
pub quality_findings_json: String,
|
pub quality_findings_json: String,
|
||||||
@@ -38,6 +39,7 @@ pub struct CustomWorldAgentSessionSnapshot {
|
|||||||
pub recommended_replies_json: String,
|
pub recommended_replies_json: String,
|
||||||
pub asset_coverage_json: String,
|
pub asset_coverage_json: String,
|
||||||
pub checkpoints_json: String,
|
pub checkpoints_json: String,
|
||||||
|
pub supported_actions_json: String,
|
||||||
pub messages: Vec::<CustomWorldAgentMessageSnapshot>,
|
pub messages: Vec::<CustomWorldAgentMessageSnapshot>,
|
||||||
pub draft_cards: Vec::<CustomWorldDraftCardSnapshot>,
|
pub draft_cards: Vec::<CustomWorldDraftCardSnapshot>,
|
||||||
pub operations: Vec::<CustomWorldAgentOperationSnapshot>,
|
pub operations: Vec::<CustomWorldAgentOperationSnapshot>,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ pub struct CustomWorldAgentSession {
|
|||||||
pub lock_state_json: Option::<String>,
|
pub lock_state_json: Option::<String>,
|
||||||
pub draft_profile_json: Option::<String>,
|
pub draft_profile_json: Option::<String>,
|
||||||
pub last_assistant_reply: Option::<String>,
|
pub last_assistant_reply: Option::<String>,
|
||||||
|
pub publish_gate_json: Option::<String>,
|
||||||
pub result_preview_json: Option::<String>,
|
pub result_preview_json: Option::<String>,
|
||||||
pub pending_clarifications_json: String,
|
pub pending_clarifications_json: String,
|
||||||
pub quality_findings_json: String,
|
pub quality_findings_json: String,
|
||||||
@@ -63,6 +64,7 @@ pub struct CustomWorldAgentSessionCols {
|
|||||||
pub lock_state_json: __sdk::__query_builder::Col<CustomWorldAgentSession, Option::<String>>,
|
pub lock_state_json: __sdk::__query_builder::Col<CustomWorldAgentSession, Option::<String>>,
|
||||||
pub draft_profile_json: __sdk::__query_builder::Col<CustomWorldAgentSession, Option::<String>>,
|
pub draft_profile_json: __sdk::__query_builder::Col<CustomWorldAgentSession, Option::<String>>,
|
||||||
pub last_assistant_reply: __sdk::__query_builder::Col<CustomWorldAgentSession, Option::<String>>,
|
pub last_assistant_reply: __sdk::__query_builder::Col<CustomWorldAgentSession, Option::<String>>,
|
||||||
|
pub publish_gate_json: __sdk::__query_builder::Col<CustomWorldAgentSession, Option::<String>>,
|
||||||
pub result_preview_json: __sdk::__query_builder::Col<CustomWorldAgentSession, Option::<String>>,
|
pub result_preview_json: __sdk::__query_builder::Col<CustomWorldAgentSession, Option::<String>>,
|
||||||
pub pending_clarifications_json: __sdk::__query_builder::Col<CustomWorldAgentSession, String>,
|
pub pending_clarifications_json: __sdk::__query_builder::Col<CustomWorldAgentSession, String>,
|
||||||
pub quality_findings_json: __sdk::__query_builder::Col<CustomWorldAgentSession, String>,
|
pub quality_findings_json: __sdk::__query_builder::Col<CustomWorldAgentSession, String>,
|
||||||
@@ -92,6 +94,7 @@ impl __sdk::__query_builder::HasCols for CustomWorldAgentSession {
|
|||||||
lock_state_json: __sdk::__query_builder::Col::new(table_name, "lock_state_json"),
|
lock_state_json: __sdk::__query_builder::Col::new(table_name, "lock_state_json"),
|
||||||
draft_profile_json: __sdk::__query_builder::Col::new(table_name, "draft_profile_json"),
|
draft_profile_json: __sdk::__query_builder::Col::new(table_name, "draft_profile_json"),
|
||||||
last_assistant_reply: __sdk::__query_builder::Col::new(table_name, "last_assistant_reply"),
|
last_assistant_reply: __sdk::__query_builder::Col::new(table_name, "last_assistant_reply"),
|
||||||
|
publish_gate_json: __sdk::__query_builder::Col::new(table_name, "publish_gate_json"),
|
||||||
result_preview_json: __sdk::__query_builder::Col::new(table_name, "result_preview_json"),
|
result_preview_json: __sdk::__query_builder::Col::new(table_name, "result_preview_json"),
|
||||||
pending_clarifications_json: __sdk::__query_builder::Col::new(table_name, "pending_clarifications_json"),
|
pending_clarifications_json: __sdk::__query_builder::Col::new(table_name, "pending_clarifications_json"),
|
||||||
quality_findings_json: __sdk::__query_builder::Col::new(table_name, "quality_findings_json"),
|
quality_findings_json: __sdk::__query_builder::Col::new(table_name, "quality_findings_json"),
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{
|
||||||
|
self as __sdk,
|
||||||
|
__lib,
|
||||||
|
__sats,
|
||||||
|
__ws,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::custom_world_draft_card_detail_snapshot_type::CustomWorldDraftCardDetailSnapshot;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct CustomWorldDraftCardDetailResult {
|
||||||
|
pub ok: bool,
|
||||||
|
pub card: Option::<CustomWorldDraftCardDetailSnapshot>,
|
||||||
|
pub error_message: Option::<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl __sdk::InModule for CustomWorldDraftCardDetailResult {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{
|
||||||
|
self as __sdk,
|
||||||
|
__lib,
|
||||||
|
__sats,
|
||||||
|
__ws,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct CustomWorldDraftCardDetailSectionSnapshot {
|
||||||
|
pub section_id: String,
|
||||||
|
pub label: String,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl __sdk::InModule for CustomWorldDraftCardDetailSectionSnapshot {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{
|
||||||
|
self as __sdk,
|
||||||
|
__lib,
|
||||||
|
__sats,
|
||||||
|
__ws,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::rpg_agent_draft_card_kind_type::RpgAgentDraftCardKind;
|
||||||
|
use super::custom_world_role_asset_status_type::CustomWorldRoleAssetStatus;
|
||||||
|
use super::custom_world_draft_card_detail_section_snapshot_type::CustomWorldDraftCardDetailSectionSnapshot;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct CustomWorldDraftCardDetailSnapshot {
|
||||||
|
pub card_id: String,
|
||||||
|
pub kind: RpgAgentDraftCardKind,
|
||||||
|
pub title: String,
|
||||||
|
pub sections: Vec::<CustomWorldDraftCardDetailSectionSnapshot>,
|
||||||
|
pub linked_ids_json: String,
|
||||||
|
pub locked: bool,
|
||||||
|
pub editable: bool,
|
||||||
|
pub editable_section_ids_json: String,
|
||||||
|
pub warning_messages_json: String,
|
||||||
|
pub asset_status: Option::<CustomWorldRoleAssetStatus>,
|
||||||
|
pub asset_status_label: Option::<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl __sdk::InModule for CustomWorldDraftCardDetailSnapshot {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{
|
||||||
|
self as __sdk,
|
||||||
|
__lib,
|
||||||
|
__sats,
|
||||||
|
__ws,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::rpg_agent_stage_type::RpgAgentStage;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct CustomWorldWorkSummarySnapshot {
|
||||||
|
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_json: String,
|
||||||
|
pub updated_at_micros: i64,
|
||||||
|
pub published_at_micros: Option::<i64>,
|
||||||
|
pub stage: Option::<RpgAgentStage>,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl __sdk::InModule for CustomWorldWorkSummarySnapshot {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{
|
||||||
|
self as __sdk,
|
||||||
|
__lib,
|
||||||
|
__sats,
|
||||||
|
__ws,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct CustomWorldWorksListInput {
|
||||||
|
pub owner_user_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl __sdk::InModule for CustomWorldWorksListInput {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{
|
||||||
|
self as __sdk,
|
||||||
|
__lib,
|
||||||
|
__sats,
|
||||||
|
__ws,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::custom_world_work_summary_snapshot_type::CustomWorldWorkSummarySnapshot;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct CustomWorldWorksListResult {
|
||||||
|
pub ok: bool,
|
||||||
|
pub items: Vec::<CustomWorldWorkSummarySnapshot>,
|
||||||
|
pub error_message: Option::<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl __sdk::InModule for CustomWorldWorksListResult {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{
|
||||||
|
self as __sdk,
|
||||||
|
__lib,
|
||||||
|
__sats,
|
||||||
|
__ws,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::custom_world_agent_action_execute_input_type::CustomWorldAgentActionExecuteInput;
|
||||||
|
use super::custom_world_agent_action_execute_result_type::CustomWorldAgentActionExecuteResult;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
struct ExecuteCustomWorldAgentActionArgs {
|
||||||
|
pub input: CustomWorldAgentActionExecuteInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl __sdk::InModule for ExecuteCustomWorldAgentActionArgs {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the procedure `execute_custom_world_agent_action`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteProcedures`].
|
||||||
|
pub trait execute_custom_world_agent_action {
|
||||||
|
fn execute_custom_world_agent_action(&self, input: CustomWorldAgentActionExecuteInput,
|
||||||
|
) {
|
||||||
|
self.execute_custom_world_agent_action_then(input, |_, _| {});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_custom_world_agent_action_then(
|
||||||
|
&self,
|
||||||
|
input: CustomWorldAgentActionExecuteInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(&super::ProcedureEventContext, Result<CustomWorldAgentActionExecuteResult, __sdk::InternalError>) + Send + 'static,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl execute_custom_world_agent_action for super::RemoteProcedures {
|
||||||
|
fn execute_custom_world_agent_action_then(
|
||||||
|
&self,
|
||||||
|
input: CustomWorldAgentActionExecuteInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(&super::ProcedureEventContext, Result<CustomWorldAgentActionExecuteResult, __sdk::InternalError>) + Send + 'static,
|
||||||
|
) {
|
||||||
|
self.imp.invoke_procedure_with_callback::<_, CustomWorldAgentActionExecuteResult>(
|
||||||
|
"execute_custom_world_agent_action",
|
||||||
|
ExecuteCustomWorldAgentActionArgs { input, },
|
||||||
|
__callback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{
|
||||||
|
self as __sdk,
|
||||||
|
__lib,
|
||||||
|
__sats,
|
||||||
|
__ws,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::custom_world_agent_card_detail_get_input_type::CustomWorldAgentCardDetailGetInput;
|
||||||
|
use super::custom_world_draft_card_detail_result_type::CustomWorldDraftCardDetailResult;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
struct GetCustomWorldAgentCardDetailArgs {
|
||||||
|
pub input: CustomWorldAgentCardDetailGetInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl __sdk::InModule for GetCustomWorldAgentCardDetailArgs {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the procedure `get_custom_world_agent_card_detail`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteProcedures`].
|
||||||
|
pub trait get_custom_world_agent_card_detail {
|
||||||
|
fn get_custom_world_agent_card_detail(&self, input: CustomWorldAgentCardDetailGetInput,
|
||||||
|
) {
|
||||||
|
self.get_custom_world_agent_card_detail_then(input, |_, _| {});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_custom_world_agent_card_detail_then(
|
||||||
|
&self,
|
||||||
|
input: CustomWorldAgentCardDetailGetInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(&super::ProcedureEventContext, Result<CustomWorldDraftCardDetailResult, __sdk::InternalError>) + Send + 'static,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl get_custom_world_agent_card_detail for super::RemoteProcedures {
|
||||||
|
fn get_custom_world_agent_card_detail_then(
|
||||||
|
&self,
|
||||||
|
input: CustomWorldAgentCardDetailGetInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(&super::ProcedureEventContext, Result<CustomWorldDraftCardDetailResult, __sdk::InternalError>) + Send + 'static,
|
||||||
|
) {
|
||||||
|
self.imp.invoke_procedure_with_callback::<_, CustomWorldDraftCardDetailResult>(
|
||||||
|
"get_custom_world_agent_card_detail",
|
||||||
|
GetCustomWorldAgentCardDetailArgs { input, },
|
||||||
|
__callback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{
|
||||||
|
self as __sdk,
|
||||||
|
__lib,
|
||||||
|
__sats,
|
||||||
|
__ws,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::custom_world_works_list_input_type::CustomWorldWorksListInput;
|
||||||
|
use super::custom_world_works_list_result_type::CustomWorldWorksListResult;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
struct ListCustomWorldWorksArgs {
|
||||||
|
pub input: CustomWorldWorksListInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl __sdk::InModule for ListCustomWorldWorksArgs {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the procedure `list_custom_world_works`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteProcedures`].
|
||||||
|
pub trait list_custom_world_works {
|
||||||
|
fn list_custom_world_works(&self, input: CustomWorldWorksListInput,
|
||||||
|
) {
|
||||||
|
self.list_custom_world_works_then(input, |_, _| {});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_custom_world_works_then(
|
||||||
|
&self,
|
||||||
|
input: CustomWorldWorksListInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(&super::ProcedureEventContext, Result<CustomWorldWorksListResult, __sdk::InternalError>) + Send + 'static,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl list_custom_world_works for super::RemoteProcedures {
|
||||||
|
fn list_custom_world_works_then(
|
||||||
|
&self,
|
||||||
|
input: CustomWorldWorksListInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(&super::ProcedureEventContext, Result<CustomWorldWorksListResult, __sdk::InternalError>) + Send + 'static,
|
||||||
|
) {
|
||||||
|
self.imp.invoke_procedure_with_callback::<_, CustomWorldWorksListResult>(
|
||||||
|
"list_custom_world_works",
|
||||||
|
ListCustomWorldWorksArgs { input, },
|
||||||
|
__callback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -60,6 +60,9 @@ pub mod chapter_progression_procedure_result_type;
|
|||||||
pub mod chapter_progression_snapshot_type;
|
pub mod chapter_progression_snapshot_type;
|
||||||
pub mod combat_outcome_type;
|
pub mod combat_outcome_type;
|
||||||
pub mod consume_inventory_item_input_type;
|
pub mod consume_inventory_item_input_type;
|
||||||
|
pub mod custom_world_agent_action_execute_input_type;
|
||||||
|
pub mod custom_world_agent_action_execute_result_type;
|
||||||
|
pub mod custom_world_agent_card_detail_get_input_type;
|
||||||
pub mod custom_world_agent_message_type;
|
pub mod custom_world_agent_message_type;
|
||||||
pub mod custom_world_agent_message_snapshot_type;
|
pub mod custom_world_agent_message_snapshot_type;
|
||||||
pub mod custom_world_agent_message_submit_input_type;
|
pub mod custom_world_agent_message_submit_input_type;
|
||||||
@@ -73,6 +76,9 @@ pub mod custom_world_agent_session_get_input_type;
|
|||||||
pub mod custom_world_agent_session_procedure_result_type;
|
pub mod custom_world_agent_session_procedure_result_type;
|
||||||
pub mod custom_world_agent_session_snapshot_type;
|
pub mod custom_world_agent_session_snapshot_type;
|
||||||
pub mod custom_world_draft_card_type;
|
pub mod custom_world_draft_card_type;
|
||||||
|
pub mod custom_world_draft_card_detail_result_type;
|
||||||
|
pub mod custom_world_draft_card_detail_section_snapshot_type;
|
||||||
|
pub mod custom_world_draft_card_detail_snapshot_type;
|
||||||
pub mod custom_world_draft_card_snapshot_type;
|
pub mod custom_world_draft_card_snapshot_type;
|
||||||
pub mod custom_world_gallery_detail_input_type;
|
pub mod custom_world_gallery_detail_input_type;
|
||||||
pub mod custom_world_gallery_entry_type;
|
pub mod custom_world_gallery_entry_type;
|
||||||
@@ -98,6 +104,9 @@ pub mod custom_world_role_asset_status_type;
|
|||||||
pub mod custom_world_session_type;
|
pub mod custom_world_session_type;
|
||||||
pub mod custom_world_session_status_type;
|
pub mod custom_world_session_status_type;
|
||||||
pub mod custom_world_theme_mode_type;
|
pub mod custom_world_theme_mode_type;
|
||||||
|
pub mod custom_world_work_summary_snapshot_type;
|
||||||
|
pub mod custom_world_works_list_input_type;
|
||||||
|
pub mod custom_world_works_list_result_type;
|
||||||
pub mod equip_inventory_item_input_type;
|
pub mod equip_inventory_item_input_type;
|
||||||
pub mod grant_inventory_item_input_type;
|
pub mod grant_inventory_item_input_type;
|
||||||
pub mod inventory_container_kind_type;
|
pub mod inventory_container_kind_type;
|
||||||
@@ -301,9 +310,11 @@ pub mod create_ai_task_and_return_procedure;
|
|||||||
pub mod create_battle_state_and_return_procedure;
|
pub mod create_battle_state_and_return_procedure;
|
||||||
pub mod create_custom_world_agent_session_procedure;
|
pub mod create_custom_world_agent_session_procedure;
|
||||||
pub mod delete_runtime_snapshot_and_return_procedure;
|
pub mod delete_runtime_snapshot_and_return_procedure;
|
||||||
|
pub mod execute_custom_world_agent_action_procedure;
|
||||||
pub mod fail_ai_task_and_return_procedure;
|
pub mod fail_ai_task_and_return_procedure;
|
||||||
pub mod get_battle_state_procedure;
|
pub mod get_battle_state_procedure;
|
||||||
pub mod get_chapter_progression_procedure;
|
pub mod get_chapter_progression_procedure;
|
||||||
|
pub mod get_custom_world_agent_card_detail_procedure;
|
||||||
pub mod get_custom_world_agent_operation_procedure;
|
pub mod get_custom_world_agent_operation_procedure;
|
||||||
pub mod get_custom_world_agent_session_procedure;
|
pub mod get_custom_world_agent_session_procedure;
|
||||||
pub mod get_custom_world_gallery_detail_procedure;
|
pub mod get_custom_world_gallery_detail_procedure;
|
||||||
@@ -318,6 +329,7 @@ pub mod get_story_session_state_procedure;
|
|||||||
pub mod grant_player_progression_experience_and_return_procedure;
|
pub mod grant_player_progression_experience_and_return_procedure;
|
||||||
pub mod list_custom_world_gallery_entries_procedure;
|
pub mod list_custom_world_gallery_entries_procedure;
|
||||||
pub mod list_custom_world_profiles_procedure;
|
pub mod list_custom_world_profiles_procedure;
|
||||||
|
pub mod list_custom_world_works_procedure;
|
||||||
pub mod list_platform_browse_history_procedure;
|
pub mod list_platform_browse_history_procedure;
|
||||||
pub mod list_profile_save_archives_procedure;
|
pub mod list_profile_save_archives_procedure;
|
||||||
pub mod list_profile_wallet_ledger_procedure;
|
pub mod list_profile_wallet_ledger_procedure;
|
||||||
@@ -387,6 +399,9 @@ pub use chapter_progression_procedure_result_type::ChapterProgressionProcedureRe
|
|||||||
pub use chapter_progression_snapshot_type::ChapterProgressionSnapshot;
|
pub use chapter_progression_snapshot_type::ChapterProgressionSnapshot;
|
||||||
pub use combat_outcome_type::CombatOutcome;
|
pub use combat_outcome_type::CombatOutcome;
|
||||||
pub use consume_inventory_item_input_type::ConsumeInventoryItemInput;
|
pub use consume_inventory_item_input_type::ConsumeInventoryItemInput;
|
||||||
|
pub use custom_world_agent_action_execute_input_type::CustomWorldAgentActionExecuteInput;
|
||||||
|
pub use custom_world_agent_action_execute_result_type::CustomWorldAgentActionExecuteResult;
|
||||||
|
pub use custom_world_agent_card_detail_get_input_type::CustomWorldAgentCardDetailGetInput;
|
||||||
pub use custom_world_agent_message_type::CustomWorldAgentMessage;
|
pub use custom_world_agent_message_type::CustomWorldAgentMessage;
|
||||||
pub use custom_world_agent_message_snapshot_type::CustomWorldAgentMessageSnapshot;
|
pub use custom_world_agent_message_snapshot_type::CustomWorldAgentMessageSnapshot;
|
||||||
pub use custom_world_agent_message_submit_input_type::CustomWorldAgentMessageSubmitInput;
|
pub use custom_world_agent_message_submit_input_type::CustomWorldAgentMessageSubmitInput;
|
||||||
@@ -400,6 +415,9 @@ pub use custom_world_agent_session_get_input_type::CustomWorldAgentSessionGetInp
|
|||||||
pub use custom_world_agent_session_procedure_result_type::CustomWorldAgentSessionProcedureResult;
|
pub use custom_world_agent_session_procedure_result_type::CustomWorldAgentSessionProcedureResult;
|
||||||
pub use custom_world_agent_session_snapshot_type::CustomWorldAgentSessionSnapshot;
|
pub use custom_world_agent_session_snapshot_type::CustomWorldAgentSessionSnapshot;
|
||||||
pub use custom_world_draft_card_type::CustomWorldDraftCard;
|
pub use custom_world_draft_card_type::CustomWorldDraftCard;
|
||||||
|
pub use custom_world_draft_card_detail_result_type::CustomWorldDraftCardDetailResult;
|
||||||
|
pub use custom_world_draft_card_detail_section_snapshot_type::CustomWorldDraftCardDetailSectionSnapshot;
|
||||||
|
pub use custom_world_draft_card_detail_snapshot_type::CustomWorldDraftCardDetailSnapshot;
|
||||||
pub use custom_world_draft_card_snapshot_type::CustomWorldDraftCardSnapshot;
|
pub use custom_world_draft_card_snapshot_type::CustomWorldDraftCardSnapshot;
|
||||||
pub use custom_world_gallery_detail_input_type::CustomWorldGalleryDetailInput;
|
pub use custom_world_gallery_detail_input_type::CustomWorldGalleryDetailInput;
|
||||||
pub use custom_world_gallery_entry_type::CustomWorldGalleryEntry;
|
pub use custom_world_gallery_entry_type::CustomWorldGalleryEntry;
|
||||||
@@ -425,6 +443,9 @@ pub use custom_world_role_asset_status_type::CustomWorldRoleAssetStatus;
|
|||||||
pub use custom_world_session_type::CustomWorldSession;
|
pub use custom_world_session_type::CustomWorldSession;
|
||||||
pub use custom_world_session_status_type::CustomWorldSessionStatus;
|
pub use custom_world_session_status_type::CustomWorldSessionStatus;
|
||||||
pub use custom_world_theme_mode_type::CustomWorldThemeMode;
|
pub use custom_world_theme_mode_type::CustomWorldThemeMode;
|
||||||
|
pub use custom_world_work_summary_snapshot_type::CustomWorldWorkSummarySnapshot;
|
||||||
|
pub use custom_world_works_list_input_type::CustomWorldWorksListInput;
|
||||||
|
pub use custom_world_works_list_result_type::CustomWorldWorksListResult;
|
||||||
pub use equip_inventory_item_input_type::EquipInventoryItemInput;
|
pub use equip_inventory_item_input_type::EquipInventoryItemInput;
|
||||||
pub use grant_inventory_item_input_type::GrantInventoryItemInput;
|
pub use grant_inventory_item_input_type::GrantInventoryItemInput;
|
||||||
pub use inventory_container_kind_type::InventoryContainerKind;
|
pub use inventory_container_kind_type::InventoryContainerKind;
|
||||||
@@ -628,9 +649,11 @@ pub use create_ai_task_and_return_procedure::create_ai_task_and_return;
|
|||||||
pub use create_battle_state_and_return_procedure::create_battle_state_and_return;
|
pub use create_battle_state_and_return_procedure::create_battle_state_and_return;
|
||||||
pub use create_custom_world_agent_session_procedure::create_custom_world_agent_session;
|
pub use create_custom_world_agent_session_procedure::create_custom_world_agent_session;
|
||||||
pub use delete_runtime_snapshot_and_return_procedure::delete_runtime_snapshot_and_return;
|
pub use delete_runtime_snapshot_and_return_procedure::delete_runtime_snapshot_and_return;
|
||||||
|
pub use execute_custom_world_agent_action_procedure::execute_custom_world_agent_action;
|
||||||
pub use fail_ai_task_and_return_procedure::fail_ai_task_and_return;
|
pub use fail_ai_task_and_return_procedure::fail_ai_task_and_return;
|
||||||
pub use get_battle_state_procedure::get_battle_state;
|
pub use get_battle_state_procedure::get_battle_state;
|
||||||
pub use get_chapter_progression_procedure::get_chapter_progression;
|
pub use get_chapter_progression_procedure::get_chapter_progression;
|
||||||
|
pub use get_custom_world_agent_card_detail_procedure::get_custom_world_agent_card_detail;
|
||||||
pub use get_custom_world_agent_operation_procedure::get_custom_world_agent_operation;
|
pub use get_custom_world_agent_operation_procedure::get_custom_world_agent_operation;
|
||||||
pub use get_custom_world_agent_session_procedure::get_custom_world_agent_session;
|
pub use get_custom_world_agent_session_procedure::get_custom_world_agent_session;
|
||||||
pub use get_custom_world_gallery_detail_procedure::get_custom_world_gallery_detail;
|
pub use get_custom_world_gallery_detail_procedure::get_custom_world_gallery_detail;
|
||||||
@@ -645,6 +668,7 @@ pub use get_story_session_state_procedure::get_story_session_state;
|
|||||||
pub use grant_player_progression_experience_and_return_procedure::grant_player_progression_experience_and_return;
|
pub use grant_player_progression_experience_and_return_procedure::grant_player_progression_experience_and_return;
|
||||||
pub use list_custom_world_gallery_entries_procedure::list_custom_world_gallery_entries;
|
pub use list_custom_world_gallery_entries_procedure::list_custom_world_gallery_entries;
|
||||||
pub use list_custom_world_profiles_procedure::list_custom_world_profiles;
|
pub use list_custom_world_profiles_procedure::list_custom_world_profiles;
|
||||||
|
pub use list_custom_world_works_procedure::list_custom_world_works;
|
||||||
pub use list_platform_browse_history_procedure::list_platform_browse_history;
|
pub use list_platform_browse_history_procedure::list_platform_browse_history;
|
||||||
pub use list_profile_save_archives_procedure::list_profile_save_archives;
|
pub use list_profile_save_archives_procedure::list_profile_save_archives;
|
||||||
pub use list_profile_wallet_ledger_procedure::list_profile_wallet_ledger;
|
pub use list_profile_wallet_ledger_procedure::list_profile_wallet_ledger;
|
||||||
|
|||||||
@@ -21,4 +21,5 @@ module-quest = { path = "../module-quest", default-features = false, features =
|
|||||||
module-runtime = { path = "../module-runtime", default-features = false, features = ["spacetime-types"] }
|
module-runtime = { path = "../module-runtime", default-features = false, features = ["spacetime-types"] }
|
||||||
module-runtime-item = { path = "../module-runtime-item", default-features = false, features = ["spacetime-types"] }
|
module-runtime-item = { path = "../module-runtime-item", default-features = false, features = ["spacetime-types"] }
|
||||||
module-story = { path = "../module-story", default-features = false, features = ["spacetime-types"] }
|
module-story = { path = "../module-story", default-features = false, features = ["spacetime-types"] }
|
||||||
|
shared-kernel = { path = "../shared-kernel" }
|
||||||
spacetimedb = { workspace = true, features = ["unstable"] }
|
spacetimedb = { workspace = true, features = ["unstable"] }
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
2. 继续设计表、reducer、view 的聚合方式
|
2. 继续设计表、reducer、view 的聚合方式
|
||||||
3. 接入身份 claims 透传
|
3. 接入身份 claims 透传
|
||||||
4. 在当前 scaffold 基础上接入 publish / dev 循环
|
4. 在当前 scaffold 基础上接入 publish / dev 循环
|
||||||
|
5. 在 `M7` 收口阶段拆分过大的 `src/lib.rs`,按 `runtime`、`gameplay/*`、`custom_world`、`asset_metadata`、`ai` 等业务与 SpacetimeDB 聚合层次重组目录,避免主工程 crate 回退成单大包
|
||||||
|
|
||||||
当前已落地:
|
当前已落地:
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user