feat: complete M5 custom world and agent chain

This commit is contained in:
2026-04-22 14:15:27 +08:00
parent 209e924403
commit 0773a0d0ca
27 changed files with 3359 additions and 159 deletions

View File

@@ -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 compilerStage 9 按冻结口径落最小 preview compiler不再搬 Node 全量 compiler
- [x] 迁移 published profile compileStage 3 已落地) - [x] 迁移 published profile compileStage 3 已落地)
- [ ] 迁移 works 聚合读模型 - [x] 迁移 works 聚合读模型Stage 9 Rust procedure + Axum facade 已接通)
- [x] 迁移 library 存储与删除Stage 2 设计已冻结,待继续接 Axum 兼容) - [x] 迁移 library 存储与删除Stage 2 设计已冻结,待继续接 Axum 兼容)
- [x] 迁移 publish / unpublishStage 2 设计已冻结,待继续接 Agent publish gate - [x] 迁移 publish / unpublishStage 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 gatesession 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 submitStage 7 deterministic message / operation 最小闭环) - [x] 迁移 message submitStage 7 deterministic message / operation 最小闭环)
- [x] 迁移 message streamStage 8 SSE facade 已落地) - [x] 迁移 message streamStage 8 SSE facade 已落地)
- [x] 迁移 operation queryStage 7 deterministic message / operation 最小闭环) - [x] 迁移 operation queryStage 7 deterministic message / operation 最小闭环)
- [ ] 迁移 card detail - [x] 迁移 card detailStage 9 Rust procedure + Axum facade 已接通)
- [ ] 迁移 card update - [x] 迁移 card update(统一走 `/actions``update_draft_card`
- [ ] 迁移 action registry / supportedActions - [x] 迁移 action registry / supportedActionssession 真相态 `supportedActions` 已接通)
- [ ] 迁移 draft foundation - [x] 迁移 draft foundation(统一走 `/actions``draft_foundation`
- [ ] 迁移 result preview 生成 - [x] 迁移 result preview 生成session 最小 `resultPreview` 已接通)
- [ ] 迁移 entity generation - [x] 迁移 entity generationAxum 兼容 `/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 ...` 编码检查通过

View File

@@ -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 后端的实施方案与验收口径。

View File

@@ -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
View File

@@ -2517,6 +2517,7 @@ dependencies = [
"module-runtime-item", "module-runtime-item",
"module-story", "module-story",
"serde_json", "serde_json",
"shared-kernel",
"spacetimedb", "spacetimedb",
] ]

View File

@@ -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(

View File

@@ -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

View 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('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
}
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>,
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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>,

View File

@@ -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"),

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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,
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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;

View File

@@ -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"] }

View File

@@ -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