This commit is contained in:
2026-04-30 17:49:07 +08:00
parent 805d6f8cae
commit 9d684cb7b3
615 changed files with 15368 additions and 6172 deletions

View File

@@ -6,7 +6,7 @@
1. 桌面端首页布局保持现有顶部栏、侧边导航、Hero、趋势区与下方网格结构不调整桌面端区块顺序。
2. 移动端首页改为参考图式信息流:顶部搜索框、横向频道 Tab、纵向作品列表、底部主导航。
3. 双端公开作品卡统一结构:上方 `16:9` 封面图,下方作品名称、作品描述、标签、点赞数
3. 双端公开作品卡统一结构:上方 `16:9` 封面图,下方作品名称、作者信息、作品描述与玩法类型
4. 点赞数必须来自作品读模型字段,前端只负责展示,不把游玩数或评分临时改名成点赞。
## 2. 数据契约
@@ -16,7 +16,7 @@
公开作品卡和创作中心复用的作品摘要都增加:
```ts
likeCount: number
likeCount: number;
```
当前阶段只做只读展示,不新增点赞按钮和点击 reducer。后端对尚未接入真实点赞表的作品返回 `0`,保证接口 shape 稳定,后续可无 UI 结构迁移地接入真实互动计数。
@@ -62,12 +62,15 @@ RpgEntryHomeView
每张公开作品卡固定为:
1. 封面区域:`aspect-ratio: 16 / 9`,图片 `object-cover`;无封面时使用轻量主题底。
2. 信息区域:
- 第一行:作品名称,右侧点赞数
- 第二行:作品描述,两行截断。
-行:最多三个标签。
3. 点赞数展示在参考图评分位置,使用心形图标 + 紧凑数字,例如 `128``1.2万`
4. 不展示作品号;作品号仍只在详情页或分享路径中使用
2. 封面左上角不展示“推荐”标签,也不展示作者昵称标签,避免遮挡作品主视觉。
3. 封面右下角展示三项轻量指标:游玩、改造、点赞;统一为图标 + 紧凑数字,例如 `128``1.2万`,不写额外说明长文案
4. 信息区域:
-行:作品名称右侧展示玩法类型。拼图玩法展示“拼图”大鱼玩法展示“大鱼”RPG 作品展示题材短标签。
- 第二行:原副标题位置展示作者头像和昵称。当前公开列表只返回作者昵称时,头像使用昵称首字生成的轻量头像;后续接入作者头像 URL 后复用同一位置
- 第三行:作品描述,两行截断
- 第四行:最多三个标签。
5. 点赞数仍必须来自作品读模型字段,只是展示位置从信息区右侧迁移到封面右下角。
6. 不展示作品号;作品号仍只在详情页或分享路径中使用。
## 5. 验收

View File

@@ -5,7 +5,7 @@
## 1. 本次目标
1. 平台首页、公开广场、分类列表中的每个公开作品点击后,统一先进入作品详情页,不再直接启动玩法。
2. 作品详情页结构参考 TapTap 详情页:顶部封面图、作品基础信息、右侧“作品改造”按钮、四项统计、简介内容、底部启动按钮
2. 作品详情页结构参考 TapTap 详情页:顶部封面图、作品基础信息、右侧“点赞”按钮、四项统计、简介内容、底部“作品改造 + 启动”同行动作
3. 删除参考图顶部 Tab不接入评价和论坛功能不展示“开发者的话”模块。
4. 统计数据必须从数据库读模型贯穿到前端展示,禁止在前端用假字段、游玩数冒充点赞数或固定文案代替真实字段。
5. Remix 按钮必须由后端事务复制公开作品为当前用户草稿,并同步增加原作品改造次数,成功后前端进入新草稿详情/结果页。
@@ -19,15 +19,15 @@
3. 基础信息区:
- 左侧作品图标使用作品封面或首图。
- 中间展示作品名、作者头像、作者名、玩法类型;作者头像读取公开用户资料 `avatarUrl`,缺失时使用作者昵称首字占位。
- 右侧原 TapTap 评分位置替换为 `作品改造` 按钮。
- 右侧原 TapTap 评分位置替换为 `点赞` 按钮;点击后调用后端点赞接口,由后端记录当前登录用户对该公开作品的点赞关系并返回更新后的真实 `likeCount` 读模型,前端不伪造点赞增长
4. 统计区固定四项:
- 改造:`remixCount`,显示为“数字 + 次”,单位放在数字后方。
- 游玩:`playCount`,显示为“数字 + 次”,单位放在数字后方。
- 改造:`remixCount`,显示为“数字 + 次”,单位放在数字后方。
- 点赞:`likeCount`,显示为“数字 + 赞”,单位放在数字后方。
- 最近更新:优先展示 `updatedAt`,缺失时回退 `publishedAt`;前端只负责格式化显示,必须兼容后端当前 `seconds.microsZ` 与 ISO 字符串两种真实时间文本,显示为完整 `YYYY-MM-DD`,使用更小字号并保持单行不换行。
- 日期:优先展示 `updatedAt`,缺失时回退 `publishedAt`;前端只负责格式化显示,必须兼容后端当前 `seconds.microsZ` 与 ISO 字符串两种真实时间文本,显示为完整 `YYYY-MM-DD`,使用更小字号并保持单行不换行。
- 四项统计需要使用浅色图标底强化识别,但不得追加规则说明类文案。
5. 简介区:展示玩法标签和作品简介;不追加说明类文案。
6. 底部动作:主按钮为“启动”,点击后进入对应玩法运行态并记录游玩次数。
6. 底部动作:左侧按钮为“作品改造”,右侧主按钮为“启动”;两个按钮必须位于同一行,点击“启动”后进入对应玩法运行态并记录游玩次数。
7. 页面配色必须跟随平台明暗主题变量;亮色主题使用平台浅色底、深色文字和主按钮渐变,暗色主题使用平台暗色底、亮色文字和对应主按钮渐变,不在详情页写死独立黑色皮肤。
8. 字号规范跟随平台页面既有节奏:标题/主按钮使用 `1rem` 级别,作品名使用卡片标题同级 `1rem`,辅助信息与简介使用 `0.8125rem` / `0.875rem`,标签与统计标签使用 `0.75rem`,避免在详情页使用随视口放大的独立大字号。
@@ -38,7 +38,8 @@
1. `custom_world_profile` 增加 `play_count``remix_count``like_count`
2. `custom_world_gallery_entry` 同步这三项统计,作为公开详情和首页卡片读模型。
3. `record_custom_world_profile_play` 负责在公开作品启动前递增 `play_count`,只更新已发布且未删除作品。
4. `remix_custom_world_profile` 在同一事务内:
4. `record_custom_world_profile_like` 负责记录当前用户对公开作品的点赞;同一用户对同一公开作品只计入一次,首次点赞时递增 `like_count` 并同步刷新 `custom_world_gallery_entry`
5. `remix_custom_world_profile` 在同一事务内:
- 校验源作品已发布、未删除。
- 递增源作品 `remix_count` 并刷新源作品 gallery。
- 复制源 profile payload 为当前用户草稿,清空公开编号、发布时间与统计。
@@ -48,7 +49,8 @@
1. `puzzle_work_profile` 保留既有 `play_count`,新增 `remix_count``like_count`
2. `start_puzzle_run` 继续作为游玩次数递增入口。
3. `remix_puzzle_work` 在同一事务内:
3. `record_puzzle_work_like` 负责记录当前用户对公开拼图作品的点赞;同一用户对同一公开作品只计入一次,首次点赞时递增 `like_count`
4. `remix_puzzle_work` 在同一事务内:
- 校验源 profile 为已发布作品。
- 递增源作品 `remix_count`
- 新建当前用户拼图 Agent session并把源作品锚点、封面、简介复制为草稿。
@@ -60,7 +62,8 @@
1. `big_fish_creation_session` 现有 `play_count` 继续作为游玩统计,新增 `remix_count``like_count``published_at`
2. `publish_big_fish_game` 写入 `published_at``updated_at`,公开列表和详情优先用 `updated_at` 展示最近更新。
3. `record_big_fish_play` 继续作为游玩次数递增入口。
4. `remix_big_fish_work` 在同一事务内:
4. `record_big_fish_like` 负责记录当前用户对公开大鱼作品的点赞;同一用户对同一公开作品只计入一次,首次点赞时递增 `like_count`
5. `remix_big_fish_work` 在同一事务内:
- 校验源 session 为已发布作品。
- 递增源作品 `remix_count`
- 新建当前用户创作 session复制锚点、草稿和资源槽阶段回到可编辑草稿态。
@@ -74,14 +77,19 @@
- RPG`POST /api/runtime/custom-world-gallery/{owner_user_id}/{profile_id}/remix`
- 拼图:`POST /api/runtime/puzzle/gallery/{profile_id}/remix`
- 大鱼:`POST /api/runtime/big-fish/gallery/{session_id}/remix`
3. 前端统一详情页只消费读模型字段,不自行派生统计。
4. 首页卡片点击只设置统一详情状态;启动与 Remix 只能在详情页触发。
5. Remix 成功后的跳转:
3. 点赞 API
- RPG`POST /api/runtime/custom-world-gallery/{owner_user_id}/{profile_id}/like`
- 拼图:`POST /api/runtime/puzzle/gallery/{profile_id}/like`
- 大鱼:`POST /api/runtime/big-fish/gallery/{session_id}/like`
- 三个接口都必须走登录态鉴权,后端使用当前登录用户身份写入点赞关系;重复点击返回当前最新读模型,不重复增加 `likeCount`
4. 前端统一详情页只消费读模型字段,不自行派生统计。
5. 首页卡片点击只设置统一详情状态;启动、点赞与 Remix 只能在详情页触发。
6. Remix 成功后的跳转:
- RPG进入复制出的草稿详情。
- 拼图:进入复制出的拼图结果页草稿。
- 大鱼:进入复制出的大鱼结果页草稿。
6. 拼图作品详情页启动时复用当前详情页已经展示的公开作品读模型,直接调用 `POST /api/runtime/puzzle/runs` 记录游玩并进入运行态;不得在启动前额外依赖 `GET /api/runtime/puzzle/gallery/{profile_id}`,避免开发代理或详情读取短断点阻塞启动链路。
7. 本地开发时 `localhost:3000` 是 Vite 前端端口,`/api/**` 默认代理到 Rust `api-server:3100`;若 3100 未监听,点击启动或作品改造会在浏览器显示 `/api/... 500`,此时真实断点是 Rust 后端未启动,不允许用前端假数据替代后端事务。
7. 拼图作品详情页启动时复用当前详情页已经展示的公开作品读模型,直接调用 `POST /api/runtime/puzzle/runs` 记录游玩并进入运行态;不得在启动前额外依赖 `GET /api/runtime/puzzle/gallery/{profile_id}`,避免开发代理或详情读取短断点阻塞启动链路。
8. 本地开发时 `localhost:3000` 是 Vite 前端端口,`/api/**` 默认代理到 Rust `api-server:3100`;若 3100 未监听,点击启动、点赞或作品改造会在浏览器显示 `/api/... 500`,此时真实断点是 Rust 后端未启动,不允许用前端假数据替代后端事务。
## 5. 验收点
@@ -89,5 +97,6 @@
2. 详情页无评价、论坛 Tab无开发者的话模块。
3. 四项统计在前端、共享契约、API facade、SpacetimeDB 表之间字段一致。
4. Remix 后原作品改造次数增加,新草稿归当前用户所有,且不会继承源作品统计。
5. 启动公开作品会走对应后端记录入口,刷新仍能看到递增后的游玩次数
6. 修改后运行编码检查、SpacetimeDB 绑定生成、Rust 检查和必要前端测试。
5. 点赞公开作品会走对应后端记录入口,首次点赞后刷新仍能看到递增后的点赞次数,重复点赞不会继续增加
6. 启动公开作品会走对应后端记录入口,刷新后仍能看到递增后的游玩次数。
7. 修改后运行编码检查、SpacetimeDB 绑定生成、Rust 检查和必要前端测试。

View File

@@ -504,7 +504,8 @@ tagSimilarityScore =
1. 拼图舞台占满可用全屏区域
2. 真正可操作的拼图棋盘按正方形比例填满安全区域,并在移动端贴近屏幕两侧边缘
3. 棋盘外延空间用同图模糊背景或纯净氛围底承接
4. 不默认堆玩法说明文字
4. 基础单块和合并块都使用圆角,基础单块图片需要被圆角容器裁剪
5. 不默认堆玩法说明文字
## 9.2 HUD 必显信息
@@ -1207,8 +1208,9 @@ interface PuzzleRunSnapshot {
10. 合并块可以整体拖动。
11. 单块拖到合并块位置时可以拆分合并块。
12. 游戏画面能显示作者信息和关卡名。
13. 拼图玩法没有继续错误复用 `customWorld``rpgWorld` 域命名
14. 新增脚本命名符合平台现有规范
13. 基础单块和合并块都使用圆角,基础单块图片不会露出直角
14. 拼图玩法没有继续错误复用 `customWorld``rpgWorld` 域命名
15. 新增脚本命名符合平台现有规范。
---

View File

@@ -47,7 +47,7 @@ server-rs/crates/api-server/src/prompt/big_fish.rs
同时把 `prompt/mod.rs` 补齐为正式导出入口,和现有:
1. `puzzle_image.rs`
1. `puzzle/image.rs`
2. `character_visual.rs`
3. `character_animation.rs`
4. `scene_background.rs`

View File

@@ -0,0 +1,79 @@
# RPG 剧情与模板创作模型路由调整2026-04-30
## 1. 背景
当前 `server-rs` 的文本模型主链统一通过 `platform-llm` 走 Ark OpenAI 兼容 `/chat/completions`。本轮模型切换有两个不同边界:
1. RPG 运行时剧情推理继续使用 Ark `/chat/completions`,但模型固定为 `doubao-seed-character-251128`
2. 模板创作流程的大模型推理统一使用 Ark `/responses`,模型固定为 `deepseek-v3-2-251201`,并按 Responses API 的 `tools: [{ type: "web_search", max_keyword: 3 }]` 方式启用联网搜索。
因此本次不能只替换 `GENARRATIVE_LLM_MODEL` 默认值。默认值仍可能被通用代理或其他兼容调用使用RPG 剧情与模板创作需要在业务请求上显式覆盖模型和协议,避免两条主链互相污染。
## 2. 落地范围
### 2.1 RPG 剧情推理
以下运行时 RPG 推理请求必须显式使用:
- model: `doubao-seed-character-251128`
- protocol: `/chat/completions`
覆盖入口:
1. `runtime_story/compat/ai.rs`
- 首段剧情与继续剧情。
- NPC 对话剧情文本。
- 预留的动作结果叙事生成。
2. `runtime_chat.rs`
- NPC 单轮聊天回复。
- NPC 单轮后续建议。
3. `runtime_chat_plain.rs`
- 角色私聊回复、建议、摘要。
- NPC 对话、招募对话等纯文本流。
### 2.2 模板创作流程
以下创作链路必须显式使用:
- model: `deepseek-v3-2-251201`
- protocol: `/responses`
- web_search: 开启时映射为 `tools: [{ "type": "web_search", "max_keyword": 3 }]`
覆盖入口:
1. `creation_agent_llm_turn.rs`
- RPG/自定义世界 Agent 单轮 JSON turn。
- 大鱼吃小鱼 Agent 单轮 JSON turn。
- 拼图 Agent 单轮 JSON turn。
- 动态状态判断等非流式 JSON turn。
2. `custom_world_foundation_draft.rs`
- 世界框架、角色、场景、角色详情等分阶段底稿生成。
- JSON 修复阶段。
3. `custom_world_agent_entities.rs`
- 结果页新增角色/地点生成。
4. `custom_world_ai.rs`
- 结果页兜底补齐实体生成。
5. `big_fish_draft_compiler.rs`
- 大鱼吃小鱼草稿结构化编译与 JSON 修复。
图片、视频、OSS、SpacetimeDB reducer 不属于本次模型切换范围。
## 3. 平台层改造
`platform-llm` 保留原 `/chat/completions` 能力,并新增 Responses 协议:
1. `LlmTextProtocol::ChatCompletions`
2. `LlmTextProtocol::Responses`
3. `LlmTextRequest::with_responses_api()`
4. `LlmConfig::responses_url()`
Responses 非流式解析优先读取 `output_text`,再兼容 `output[].content[].text`。Responses 流式解析只把 `response.output_text.delta``delta` 推给上层,避免把 reasoning summary、工具事件或完成事件误拼进玩家可见文本。
## 4. 验收标准
1. RPG 运行时 LLM 请求在代码层显式带 `doubao-seed-character-251128`
2. 创作模板 LLM 请求在代码层显式带 `deepseek-v3-2-251201` 与 Responses 协议。
3. `platform-llm` 单测覆盖 Responses 非流式、Responses SSE、Responses web_search tools 请求体。
4. `cargo test -p platform-llm --manifest-path server-rs/Cargo.toml` 通过。
5. `cargo test -p api-server creation_agent_llm_turn --manifest-path server-rs/Cargo.toml` 通过。
6. 修改后按项目约束使用 `npm run api-server:maincloud` 重新启动后端,并执行相应自动测试。

View File

@@ -23,11 +23,12 @@
1. 点击昵称右侧编辑按钮打开独立弹窗。
2. 弹窗内只提供昵称输入、取消、保存。
3. 前端先做长度与字符校验:
3. 弹窗面板使用平台标准不透明面板底,不复用透明轻量面板。
4. 前端先做长度与字符校验:
- `2-20` 个字符。
- 允许中文、英文、数字、下划线。
- 不允许纯空白。
4. 保存调用 `PATCH /api/profile/me`,成功后即时回写 `AuthUiContext.user`
5. 保存调用 `PATCH /api/profile/me`,成功后即时回写 `AuthUiContext.user`
### 2.3 头像上传与裁剪
@@ -36,9 +37,10 @@
- MIME 类型仅允许 `image/jpeg``image/png``image/webp`
- 单文件不超过 `5MB`
3. 校验通过后读取为图片,打开裁剪弹窗。
4. 裁剪工具使用正方形裁剪框,支持拖动裁剪区域与缩放图片
5. 保存时前端输出 `256x256` 的 PNG data URL调用 `PATCH /api/profile/me` 保存为账号头像
6. 成功后资料卡头像立即展示新图
4. 裁剪弹窗面板使用平台标准不透明面板底,避免底层资料卡内容透出
5. 裁剪工具使用正方形裁剪框,支持拖动裁剪区域与缩放图片
6. 保存时前端输出 `256x256` 的 PNG data URL调用 `PATCH /api/profile/me` 保存为账号头像
7. 成功后资料卡头像立即展示新图。
## 3. 后端契约
@@ -85,5 +87,6 @@ SpacetimeDB 正式表 `user_account` 需要增加 `avatar_url: Option<String>`
2. “我的”页陶泥号复制按钮点击后显示 `已复制`
3. “我的”页不展示 `手机号``正常` 标签。
4. 昵称编辑成功后,资料卡与顶部账号入口同步新昵称。
5. 非法头像文件不会进入裁剪流程
6. 裁剪保存成功后,资料卡头像展示裁剪后的图片。
5. 昵称与头像裁剪弹窗面板不透明,不能露出底层页面内容
6. 非法头像文件不会进入裁剪流程。
7. 裁剪保存成功后,资料卡头像展示裁剪后的图片。

View File

@@ -2,25 +2,38 @@
## 背景
拼图创作入口不再使用 Agent 对话收集题材锚点。新流程让玩家填写两个字段:拼图标题、画面描述。画面描述支持上传参考图。玩家确认后直接进入草稿生成进度页,后续草稿生成、首图生成、正式图选择、结果页编辑和发布沿用现有后端编排。
拼图创作入口不再使用 Agent 对话收集题材锚点。新流程让玩家填写作品名称、作品描述、画面描述三类信息,其中画面描述只服务首关画面生成与关卡画面语义,不再作为作品详情页的作品描述。画面描述支持上传参考图。玩家确认后直接进入草稿生成进度页,后续草稿生成、首图生成、正式图选择、结果页编辑和发布沿用现有后端编排。
## 入口表单
1. 拼图标题为必填字段,保存到 `seedText`,同时作为 `levelName` 的优先来源。
2. 画面描述为必填字段,保存到 `pictureDescription`,同时作为 `summary` 和首图生成 prompt 的优先来源;支持多行文本,后端解析不得截断首行之后的内容。
3. 参考图为可选字段,保存到 `referenceImageSrc`。表单支持本地图片上传为 Data URL草稿首图生成时直接传入现有拼图图生图接口
4. 表单确认后前端先创建拼图 session再立即执行 `compile_puzzle_draft`,并传入 `promptText = pictureDescription``referenceImageSrc`
5. 表单提交 payload 需要在前端创作流程中暂存,生成进度页失败重试时必须继续携带同一份画面描述与参考图
6. 入口不再展示拼图 Agent 聊天气泡、快捷补齐或多锚点卡片;新建拼图时必须清空旧 session只有从当前生成进度页返回表单时保留本轮内容
### 2026-04-30 初始表单草稿保存补充
1. 玩家在创作页点击“拼图”入口时,前端必须立即创建一个新的拼图 Agent session并同步生成一条 `publicationStatus = draft` 的拼图作品卡;此时不触发 `compile_puzzle_draft`,不生成图片,不进入生成进度页
2. 新 session 的 `seedText` 允许为空SpacetimeDB 侧用空锚点和空表单草稿初始化,不得把默认题材文案写入玩家草稿字段
3. 初始表单输入自动保存到 session 的 `draft_json``puzzle_work_profile` 投影。保存字段只包含 `workTitle``workDescription``pictureDescription`、可推断标签和一个 `generationStatus = idle` 的默认关卡;参考图只保存在当前前端会话内,不落入 SpacetimeDB
4. 玩家在生成草稿前退出,再次从创作中心点击这条拼图草稿时,必须恢复到填表页,并回填之前自动保存的作品名称、作品描述和画面描述;只有执行 `compile_puzzle_draft` 且生成结果页草稿后,草稿入口才进入结果页
5. 表单自动保存走 `save_puzzle_form_draft` action不消耗陶泥币不生成图片不改变 `stage = collecting_anchors`;生成草稿按钮仍单独触发 `compile_puzzle_draft` 并进入进度页。
6. 点击拼图入口始终创建新草稿,不复用上一次未完成 session恢复旧草稿只通过“我的创作”中的草稿卡进入。
1. 作品名称为必填字段,保存到 `workTitle`,兼容写入旧 `seedText`,同时作为作品级 `workTitle` 的真相源。
2. 作品描述为必填字段,保存到 `workDescription`,作为作品详情页、作品列表和发布资料中的 `summary` 真相源。
3. 画面描述为必填字段,保存到 `pictureDescription`,只作为首关画面生成 prompt、首关 `pictureDescription` 和关卡命名输入,不再覆盖作品描述。
4. 参考图为可选字段,保存到 `referenceImageSrc`。表单支持本地图片上传为 Data URL草稿首图生成时直接传入现有拼图图生图接口。
5. 表单确认后前端先创建拼图 session再立即执行 `compile_puzzle_draft`,并传入 `promptText = pictureDescription``referenceImageSrc`
6. 表单提交 payload 需要在前端创作流程中暂存,生成进度页失败重试时必须继续携带同一份作品名称、作品描述、画面描述与参考图。
7. 生成进度页的“当前拼图信息”必须优先读取这份表单 payload而不是读取 session 中旧 Agent 锚点或编译后的关卡名,避免用户确认后看到的标题、作品描述、画面描述发生漂移。
8. `compile_puzzle_draft` action 必须显式携带 `workTitle``workDescription``pictureDescription``promptText``referenceImageSrc``seedText` 只作为 SpacetimeDB 旧表结构兼容载体,不能成为前端生成页展示和失败重试的唯一来源。
9. 入口不再展示拼图 Agent 聊天气泡、快捷补齐或多锚点卡片;新建拼图时必须清空旧 session只有从当前生成进度页返回表单时保留本轮内容。
## 锚点映射
拼图模式锚点收口为个玩家输入源:
拼图模式锚点收口为个玩家输入源:
| 新字段 | 落地字段 | 说明 |
| --- | --- | --- |
| 拼图标题 | `themePromise.value``levelName``creatorIntent.themePromise` | 作为题材承诺与关卡名称的真相源 |
| 画面描述 | `visualSubject.value``summary`、首图 `promptText` | 作为画面主体与生图 prompt 的真相源 |
| 作品名称 | `themePromise.value``workTitle`、旧 `levelName` 兼容字段`creatorIntent.themePromise` | 作为作品名称与题材承诺真相源 |
| 作品描述 | `workDescription``summary` 兼容字段 | 作为作品详情页描述、列表描述和发布描述真相源 |
| 画面描述 | `visualSubject.value``levels[0].pictureDescription`、首图 `promptText` | 作为首关画面主体、首图生成 prompt 和首关关卡命名输入 |
兼容旧结构时仍保留 `visualMood``compositionHooks``tagsAndForbidden` 字段,但它们不再由 Agent 问答收集:
@@ -28,31 +41,49 @@
2. `compositionHooks` 固定标记为系统推断,值为“主体轮廓、色块分区、局部细节”。
3. `tagsAndForbidden` 根据拼图标题和画面描述生成 3 到 6 个题材标签;禁忌只保留通用图像约束,不写入 UI。
生成进度页的“当前拼图信息”只展示玩家输入锚点:拼图标题、画面描述。题材标签仅作为草稿结果页内容展示,不在进度页混入旧五锚点结构。
生成进度页的“当前拼图信息”只展示玩家输入锚点:作品名称、作品描述、画面描述。题材标签仅作为草稿结果页内容展示,不在进度页混入旧五锚点结构。
## 草稿数据结构
拼图草稿从单关卡字段升级为作品级信息与关卡列表并存:
1. `PuzzleResultDraft.workTitle`:作品名称,旧 `levelName` 只作为兼容字段同步为当前主关卡名称或作品名称。
2. `PuzzleResultDraft.workDescription`:作品描述,旧 `summary` 只作为兼容字段同步为作品描述。
3. `PuzzleResultDraft.themeTags`:作品标签,仍限制 3 到 6 个。
4. `PuzzleResultDraft.levels[]`:关卡列表。每个关卡包含 `levelId``levelName``pictureDescription``candidates``selectedCandidateId``coverImageSrc``coverAssetId``generationStatus`
5. 首次草稿生成时必须创建一个默认关卡,`levelId = puzzle-level-1``pictureDescription = 表单画面描述`,首图生成后直接写入该关卡。
6. 关卡名称由后端基于画面描述和图片语义输入生成;无可用语义时按题材标签与序号兜底,禁止继续直接使用作品名称作为关卡名称。
7. 旧草稿或旧作品缺少 `levels` 时,读取层必须由旧 `levelName``summary``coverImageSrc``candidates` 补出一个兼容关卡,避免历史草稿无法打开。
## 后端编译
1. `CreatePuzzleAgentSessionRequest` 新增 `pictureDescription``referenceImageSrc`,但不改 SpacetimeDB 表结构。
2. api-server 创建 session 时把标题和画面描述合成 `seedText` 传入 SpacetimeDBSpacetimeDB reducer 只做确定性锚点生成,不接触图片或外部服务。
3. `compile_puzzle_draft_with_initial_cover` 新增首图 prompt 和参考图参数。若前端传入画面描述,则首图生成直接使用这段文本;若传入参考图,则走现有 DashScope 图生图链路。
4. 图片生成仍在 api-server 内完成,遵守 SpacetimeDB reducer 不做网络 I/O 的约束
1. `CreatePuzzleAgentSessionRequest` 新增 `workTitle``workDescription``pictureDescription``referenceImageSrc`,但不改 SpacetimeDB 表结构。
2. api-server 创建 session 时把作品名称、作品描述和画面描述合成 `seedText` 传入 SpacetimeDBSpacetimeDB reducer 只做确定性锚点生成,不接触图片或外部服务。
3. `compile_puzzle_draft_with_initial_cover` 新增首图 prompt 和参考图参数。若前端传入画面描述,则首图生成直接使用这段文本;若传入参考图,则走现有 DashScope 图生图链路;生成结果写入默认第一关
4. 首图文生图 prompt 由 api-server 拼接固定拼图约束后统一压缩到 `500` 字符以内,避免玩家长画面描述触发 DashScope 参数非法;进度页和结果页仍展示玩家原始画面描述,不展示压缩后的内部 prompt
5. 图片生成仍在 api-server 内完成,遵守 SpacetimeDB reducer 不做网络 I/O 的约束。
6. 参考图以 Data URL 进入 `POST /api/runtime/puzzle/agent/sessions``POST /api/runtime/puzzle/agent/sessions/{sessionId}/actions`,这两条路由必须单独放宽 JSON body 上限;不要放大全局默认 body limit。
7. 前端仍应优先压缩参考图;后端 body 上限只用于容纳合理尺寸的单张参考图,超大原图不应直接落入 SpacetimeDB 或作为作品字段持久化。
8. 作品更新接口 `PUT /api/runtime/puzzle/works/{profileId}` 必须支持作品信息和关卡列表一起写入,前端自动保存不得只写旧单关字段。
9. `StartPuzzleRunRequest` 新增可选 `levelId`。详情页或草稿结果页单独体验某关时传入目标关卡,后端从作品/草稿的 `levels` 中选取该关卡生成运行态。
10. `ExecutePuzzleAgentActionRequest` 必须保留 `pictureDescription` 字段。表单直达生成时,`compile_puzzle_draft` 优先用 `pictureDescription` 作为首图 prompt再回退到旧 `promptText`;避免生成页展示的是玩家画面描述,但后端实际用作品名称或旧摘要出图。
11. `compile_puzzle_draft` 中的图片上游失败不得映射成 `400 BAD_REQUEST`。DashScope 返回 `InvalidParameter` 或任务失败时api-server 统一按 `502 UPSTREAM_ERROR` 暴露,并在 `details.message` 中保留“拼图图片生成失败:...”的业务原因,避免生成页只显示“请求参数不合法”。
12. `compile_puzzle_draft` 前置陶泥币预扣失败不得映射成 `400 BAD_REQUEST`。余额不足返回 `409 CONFLICT`SpacetimeDB procedure 不可用、绑定不匹配、钱包服务异常等统一按 `502 UPSTREAM_ERROR` 暴露,并在 `details.message` 中保留真实钱包错误。
## 结果页
拼图草稿结果页不再区分 Tab合并为一个可滚动列表页内容顺序固定为
拼图草稿结果页分为两个 Tab
1. 关卡名称
2. 画面预览
3. 画面描述。
4. 重新生成画面按钮。
5. 题材标签。
1. 拼图关卡列表:默认展示草稿生成出的第一关。列表项参考 RPG 草稿卡片样式,显示画面图、关卡名称和轻量状态。支持新增关卡、删除关卡。点击列表项进入独立关卡详情页,不在列表项下方展开。关卡详情页可编辑关卡名称、画面描述、重新生成画面,并支持单独体验该关卡。
2. 作品信息:展示并编辑作品名称、作品描述、作品标签
画面描述区域不再展示候选图实际 prompt 或“请生成一张适合……”之类内部提示词模块。参考图入口保留在画面描述编辑区域内,便于重新生成时继续带入。结果页编辑画面描述时必须同步更新 `summary`,确保自动保存、作品测试、发布和重新生成画面使用同一份描述。
画面描述区域不再展示候选图实际 prompt 或“请生成一张适合……”之类内部提示词模块。参考图入口保留在画面描述编辑区域内,便于重新生成时继续带入。结果页编辑关卡画面描述时只同步该关卡 `pictureDescription`;作品描述只在作品信息 Tab 编辑,作品详情页不得再回退使用画面描述。
## 验收
1. 从拼图创作入口只能看到标题、画面描述和参考图上传,不出现 Agent 聊天输入、补齐设定、锚点问答。
1. 从拼图创作入口只能看到作品名称、作品描述、画面描述和参考图上传,不出现 Agent 聊天输入、补齐设定、锚点问答。
2. 点击确认后进入拼图草稿生成进度页,并自动完成草稿编译、首图生成、正式图选择。
3. 首图生成请求使用玩家画面描述作为 prompt上传参考图时走图生图。
4. 结果页为单列表,顺序符合上文要求,不展示 Tab 和内部实际 prompt
5. 发布、作品测试、自动保存标题、画面描述和标签仍可用。
3. 首图生成请求使用玩家画面描述作为 prompt上传参考图时走图生图;作品详情页展示玩家作品描述
4. 结果页包含“拼图关卡”和“作品信息”两个 Tab关卡列表默认至少一关支持新增、删除和进入关卡详情
5. 关卡详情页支持重新生成画面和单独体验该关卡。
6. 发布、作品测试、自动保存作品名称、作品描述、作品标签和关卡列表仍可用。

View File

@@ -17,7 +17,11 @@
1. 拼图生成图固定使用 `1024*1024`
2. 文生图和参考图生图共用同一个尺寸常量,禁止一条链路仍生成竖屏或横版图。
3. 拼图图片提示词明确写入 `1:1 正方形画布`,继续保留 `3x3 或 4x4 拼图切块`、主体清晰、层次明确、无文字水印等约束。
4. 图片生成仍由 `api-server` 执行。SpacetimeDB reducer 不做网络 I/O
4. 文生图正向 prompt 必须由后端压缩到 `500` 字符以内,优先保留玩家画面描述开头与固定拼图约束,避免 DashScope 旧 text2image 协议把超长 prompt 判为“请求参数不合法”
5. DashScope 上游失败时api-server 必须在错误 details 中保留业务 message、`upstreamStatus` 和截断后的 `rawExcerpt`,日志也要记录同样的摘要,避免生成进度页只能看到通用 HTTP 文案。
6. 图片生成仍由 `api-server` 执行。SpacetimeDB reducer 不做网络 I/O。
7. 拼图文生图请求体按 DashScope Wan text2image 协议收口:`input``prompt` 与非空 `negative_prompt``parameters``n``size``prompt_extend``watermark`。不要在 `input``parameters` 里重复写入反向提示词,否则上游容易返回参数非法。
8. 陶泥币预扣失败属于钱包或 SpacetimeDB 服务链路错误,不得映射成 `400 BAD_REQUEST`。除余额不足返回 `409 CONFLICT` 外,其余预扣异常统一按上游/服务错误暴露,避免生成页误提示“请求参数不合法”。
### 2. 前端规则裁决
@@ -33,10 +37,19 @@
3. 单格不设置固定最小高度,避免移动端被单格撑破。
4. 顶部 HUD 与底部道具仍保留安全区,不能遮挡棋盘可操作区域。
### 4. 拼块视觉圆角
1. 基础单块和合并块都必须使用圆角,不能只让合并后的外轮廓有圆角。
2. 基础单块的图片层必须跟随单块容器裁剪,避免图片直角从圆角边框里露出。
3. 合并块继续按实际拼块外轮廓描边,内部相邻边不额外显示边框。
## 验收
1. 点击拼图草稿生成或重新生成画面时,后端请求 DashScope 的 `size``1024*1024`
2. 图片提示词包含 `1:1 正方形拼图关卡`
3. 正式拼图 run 中拖动拼块后,前端立即更新棋盘、合并块和通关状态,不再等待 `/drag`
4. 移动端运行时棋盘为正方形,并尽量贴近屏幕两侧边缘
5. 下一关、道具、排行榜仍走现有后端链路,不把外部 I/O 或扣费逻辑塞回前端。
3. 图片提示词长度不超过 `500` 字符,超长画面描述会被截断,但 `3x3 或 4x4``避免文字、水印、边框和 UI 元素` 等玩法约束不能丢
4. DashScope 返回参数错误、任务失败或非 2xx 时,前端错误优先展示后端 details.message后端日志能看到 `upstreamStatus``rawExcerpt`
5. 正式拼图 run 中拖动拼块后,前端立即更新棋盘、合并块和通关状态,不再等待 `/drag`
6. 移动端运行时棋盘为正方形,并尽量贴近屏幕两侧边缘。
7. 基础单块和合并块都能看到圆角,且基础单块图片不会溢出圆角裁剪。
8. 下一关、道具、排行榜仍走现有后端链路,不把外部 I/O 或扣费逻辑塞回前端。

View File

@@ -6,7 +6,7 @@
## 本轮落地边界
1. 拼图图片提示词统一放到 `server-rs/crates/api-server/src/prompt/puzzle_image.rs`
1. 拼图图片提示词统一放到 `server-rs/crates/api-server/src/prompt/puzzle/image.rs`
2. `puzzle.rs` 只负责读取提示词构建结果,并继续处理 DashScope、OSS、SpacetimeDB 写回。
3. 提示词模块只暴露:
- `build_puzzle_image_prompt(level_name, prompt)`
@@ -18,7 +18,7 @@
1. 不把图片生成逻辑下沉到 SpacetimeDB reducer外部 I/O 必须留在 `api-server`
2. 不改候选图 JSON 持久化结构,仍使用 `module-puzzle::PuzzleGeneratedImageCandidate` 对应的 snake_case 字段。
3. 不改前端 UI 文案和交互;本轮只拆后端提示词脚本。
4. 后续若调整拼图图片风格、尺寸、禁止元素或切块可读性要求,优先修改 `prompt/puzzle_image.rs`,再按需补测试。
4. 后续若调整拼图图片风格、尺寸、禁止元素或切块可读性要求,优先修改 `prompt/puzzle/image.rs`,再按需补测试。
## 验收

View File

@@ -0,0 +1,37 @@
# 拼图排行榜前端关卡提交与 RPG 敬请期待 2026-04-30
## 背景
拼图运行态的交换、拖动、合并、拆分与本关通关判定已经交给前端即时计算。第二关通过后端 `local-next-level` 兼容接口生成下一关时,前端当前关卡已经推进到新作品,但 SpacetimeDB 中的旧 run 快照可能仍停留在上一关。
因此第二关通关后提交排行榜,如果后端继续要求 `run.currentLevel.profileId == payload.profileId`,会误报:
```text
提交成绩的拼图作品与当前关卡不匹配
```
本轮同时把 RPG 创作入口设置为“敬请期待”,只调整平台入口展示与分流防线,不删除 RPG 既有代码、历史作品详情或运行时兼容能力。
## 落地口径
### 1. 拼图排行榜
1. 排行榜提交仍必须校验 run 归属,避免跨用户提交。
2. 排行榜写入以本次提交的 `profileId``gridSize``elapsedMs` 为准。
3. 当 SpacetimeDB 旧 run 的当前关卡与提交关卡一致时,后端可以把真实榜单合并回服务端关卡快照。
4. 当旧 run 当前关卡与提交关卡不一致时,不再报错;后端只写入真实榜单,并把榜单放到 run 顶层 `leaderboardEntries` 返回给前端。
5. 前端继续用当前本地 run 合并后端返回的 `leaderboardEntries`,不能用服务端旧棋盘覆盖本地第二关棋盘。
### 2. RPG 敬请期待
1. 平台创作类型元数据中,`rpg` 改为 `locked: true`
2. RPG 创作卡片 badge 与副标题统一显示 `敬请期待`
3. 创作类型弹窗与创作首页卡带复用同一份元数据,因此入口自动禁用。
4. 分流函数中继续防御 `rpg` 类型直达触发,避免旧测试或隐藏入口绕过禁用态。
## 验收
1. 第二关拼图通关后提交排行榜,不再出现“提交成绩的拼图作品与当前关卡不匹配”。
2. 排行榜返回后,前端仍保留当前第二关棋盘与通关状态。
3. 创作页 RPG 卡片显示 `敬请期待` 且不可点击。
4. 拼图、大鱼和其它已锁定玩法的显示状态不被本轮改动影响。

View File

@@ -5,6 +5,7 @@
## 文档列表
- [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md):冻结 SpacetimeDB 表结构变更约束、自动迁移可接受范围、冲突后的系统行为,以及保留旧数据的增量迁移流程;凡涉及 `spacetime publish`、表字段调整或 `migration.rs` 对齐时优先参考。
- [LLM_MODEL_ROUTING_RPG_AND_CREATION_2026-04-30.md](./LLM_MODEL_ROUTING_RPG_AND_CREATION_2026-04-30.md):冻结 RPG 运行时剧情推理使用 `doubao-seed-character-251128``/chat/completions`,以及所有模板创作大模型推理使用 `deepseek-v3-2-251201``/responses`
- [RPG_PROMPT_FRONTEND_REMOVAL_AND_SERVER_RS_MIGRATION_2026-04-28.md](./RPG_PROMPT_FRONTEND_REMOVAL_AND_SERVER_RS_MIGRATION_2026-04-28.md):冻结 RPG 提示词禁止存在前端的边界,明确前端只保留 API client角色私聊/NPC 对话/剧情续写等 prompt 统一收口到 `server-rs`
- [RPG_CREATION_RESULT_VIEW_BACKEND_TRUTH_MIGRATION_2026-04-28.md](./RPG_CREATION_RESULT_VIEW_BACKEND_TRUTH_MIGRATION_2026-04-28.md):冻结 RPG 创作结果页保存、Agent session/result preview 真相优先级和结果页入口裁决迁移到后端 result-view 的落地边界。
- [RPG_CREATION_PROFILE_GENERATION_BACKEND_MIGRATION_2026-04-28.md](./RPG_CREATION_PROFILE_GENERATION_BACKEND_MIGRATION_2026-04-28.md):记录 RPG 创作 profile 生成移除非浏览器 legacy AI 回退,统一通过 `server-rs``/api/runtime/custom-world/profile` 生成世界底稿。
@@ -13,7 +14,11 @@
- [BIG_FISH_PROMPT_MODULE_EXTRACTION_2026-04-28.md](./BIG_FISH_PROMPT_MODULE_EXTRACTION_2026-04-28.md):记录大鱼吃小鱼草稿生成、生图、动作三类提示词从业务脚本中抽离到独立 `prompt/big_fish.rs` 模块的边界与职责划分。
- [BIG_FISH_MAIN_IMAGE_TRANSPARENT_BACKGROUND_ALIGNMENT_2026-04-28.md](./BIG_FISH_MAIN_IMAGE_TRANSPARENT_BACKGROUND_ALIGNMENT_2026-04-28.md):记录大鱼吃小鱼等级主图与动作关键帧正式图在 Rust 后端复用 RPG 角色主图透明背景 alpha 后处理的对齐口径,并明确场地背景不走该处理。
- [PUZZLE_IMAGE_AND_FRONTEND_RULES_ALIGNMENT_2026-04-29.md](./PUZZLE_IMAGE_AND_FRONTEND_RULES_ALIGNMENT_2026-04-29.md):记录拼图生成图片回到 1:1运行时拖动、交换、合并与拆分由前端即时裁决以及移动端棋盘贴近屏幕边缘的落地边界。
- [PUZZLE_FORM_CREATION_FLOW_2026-04-29.md](./PUZZLE_FORM_CREATION_FLOW_2026-04-29.md):冻结拼图填表式创作入口、初始表单自动保存草稿、生成前退出后的表单恢复,以及草稿编译/首图生成的前后端边界。
- [PUZZLE_LEADERBOARD_FRONTEND_LEVEL_AND_RPG_COMING_SOON_2026-04-30.md](./PUZZLE_LEADERBOARD_FRONTEND_LEVEL_AND_RPG_COMING_SOON_2026-04-30.md):记录拼图第二关排行榜提交以前端当前关卡为准、不被 SpacetimeDB 旧 run 快照误杀,以及 RPG 创作入口改为敬请期待的落地边界。
- [RPG_SCENE_ACT_PREVIEW_BOOTSTRAP_FIX_2026-04-30.md](./RPG_SCENE_ACT_PREVIEW_BOOTSTRAP_FIX_2026-04-30.md):记录编辑器幕预览卡在“正在载入这一幕”时的启动态根因,收口预览本地运行态装配与禁持久化首段 story 注入。
- [PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.md](./PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.md):记录拼图结果页名称与标签编辑自动保存、发布门槛统一到 `3~6` 标签,以及前端发布校验不再被旧 session blocker 卡死的修复口径。
- [WORK_AUTHOR_ID_RESOLUTION_2026-04-30.md](./WORK_AUTHOR_ID_RESOLUTION_2026-04-30.md):记录作品作者以 `owner_user_id` 为真相源API 按用户 ID 解析最新昵称与公开用户码,历史 `author_display_name` 仅作为兼容回退。
- [SPACETIMEDB_START_SH_EARLY_EXIT_DIAGNOSTICS_2026-04-27.md](./SPACETIMEDB_START_SH_EARLY_EXIT_DIAGNOSTICS_2026-04-27.md):记录发布包 `start.sh` 只输出“SpacetimeDB 进程在就绪前退出”时的诊断补强,启动失败或超时时自动回显 `logs/spacetimedb.log``server ping`、端口监听和 root-dir 相关进程。
- [RPG_AND_AGENT_CHAT_TRUE_SSE_STREAMING_2026-04-26.md](./RPG_AND_AGENT_CHAT_TRUE_SSE_STREAMING_2026-04-26.md):记录 RPG 运行时 NPC 聊天、RPG/自定义世界 Agent 与大鱼 Agent 从“拼完整 SSE 字符串后一次性返回”改为 `mpsc + Sse<Event>` 真流式输出的后端落地口径。
- [SPACETIMEDB_START_SH_ROOT_OWNER_FALSE_POSITIVE_FIX_2026-04-27.md](./SPACETIMEDB_START_SH_ROOT_OWNER_FALSE_POSITIVE_FIX_2026-04-27.md):记录发布包 `start.sh` root-dir 占用检测把 `grep -F .../.spacetimedb` 误判为 SpacetimeDB 实例的根因、脚本修复和现场处理方式。

View File

@@ -51,7 +51,10 @@ server-rs/crates/api-server/src/prompt/
├─ big_fish.rs
├─ character_animation.rs
├─ character_visual.rs
├─ puzzle_image.rs
├─ puzzle/
│ ├─ agent_chat.rs
│ ├─ image.rs
│ └─ mod.rs
├─ scene_background.rs
├─ mod.rs
└─ rpg/

View File

@@ -0,0 +1,67 @@
# RPG 开局首幕 NPC 流程收口方案2026-04-30
## 目标
本轮只收口“进入游戏开局场景后遇到第一幕第一批人”的运行时流程:
1. 对方主角色好感度 `>= 0` 时,聊天过程中允许出现 `npc_chat`、任务、送礼、交易、获得帮助等 NPC 功能选项;聊天结束后界面只保留一个 `story_continue_adventure`,点击后直接推进到下一幕。
2. 对方主角色好感度 `< 0` 时,聊天过程中只允许 `npc_chat`;聊天可以由模型中断,也可以由玩家主动中断。中断后只允许 `npc_fight``battle_escape_breakout`
3. 删除这条主流程里的干扰分支:正好感聊天结束后不再展开旧 NPC 目录或相邻场景旅行;负好感聊天中不再混入交易、送礼、求助、任务、招募、切磋、离开等 function。
## 工程落点
1. `src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts`
- `buildNpcChatFunctionOptionCatalog(...)` 按当前 NPC 好感过滤功能候选。
- 负好感聊天候选只保留 `npc_chat`
- 正好感聊天结束后的 `story_continue_adventure` 只揭开下一幕入口;若当前场景没有下一幕,才退回相邻场景入口。
2. `src/hooks/rpg-runtime-story/choiceActions.ts`
- 应用 `deferredRuntimeState.storyEngineMemory`,保证点击继续后真正切到下一幕的 `currentSceneActState`
3. `server-rs/crates/api-server/src/runtime_story/compat/presentation.rs`
- 服务端 active NPC option catalog 与前端同规则对齐。
- 负好感 active NPC 只返回 `npc_chat`
- 非负好感 active NPC 返回聊天、帮助、交易、送礼、任务、招募等功能,不再返回战斗、切磋、离开。
## 验收
1. 正好感 NPC 主动退出聊天后,只显示 `story_continue_adventure`
2. 点击 `story_continue_adventure` 后,`storyEngineMemory.currentSceneActState.currentActId` 推进到下一幕。
3. 负好感 NPC 聊天请求中的 `functionOptions` 为空,聊天 UI 不出现非聊天 function。
4. 负好感聊天中断后只出现“战斗”和逃跑选项。
5. 服务端 state catalog 对负好感 active NPC 不返回交易、送礼、帮助、任务、招募、切磋、离开等干扰入口。
## 2026-04-30 补充:负好感主动中止恢复
### 问题
敌对聊天的模型主动中止依赖后端建议 JSON 中的 `shouldEndChat` 字段,但部分入口没有把负好感 NPC 标记为 `terminationMode: hostile_model`,导致后端即使收到 `shouldEndChat: true` 也会按非敌对聊天忽略。另一个缺口是 NPC 主动开场第一轮只展示后续候选,没有处理 `chatDirective.forceExit`,因此第一轮开场也无法被模型主动中止。
### 落地
1. `src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts`
- 构造 `NpcChatDirective` 时直接读取当前 NPC 好感度。
- 只要 `affinity < 0`,统一写入 `limitReason: negative_affinity``terminationMode: hostile_model``isHostileChat: true`
- NPC 主动开场收到 `chatTurn.chatDirective.forceExit === true` 时,立即收起 `npcChatState`,展示战斗与逃跑选项。
### 补充验收
1. 任意负好感 NPC 聊天轮都必须向后端传 `terminationMode: hostile_model`,不能只依赖第一幕主 NPC 场景幕状态。
2. 负好感 NPC 主动开场第一轮若返回 `forceExit: true`,聊天输入立即关闭,只显示战斗与逃跑。
## 2026-04-30 补充:聊天首句统一由模型 NPC 发起
### 问题
NPC 主动开场链路本身已经存在,并会以空玩家消息调用模型,同时传入 `npcInitiatesConversation: true`。但运行时入口曾把这条链路限制在 `firstMeaningfulContactResolved !== true`,导致角色完成首次有效接触后,再次从 NPC 入口或交互选项进入聊天时,会退回旧的 `enterNpcChat(...)` 本地入口:界面先展示玩家可点的话题,没有模型生成的 NPC 首句。负好感且非限定场景幕时,还存在一条本地敌对宣言分支,会直接给战斗/逃跑,绕过“先聊天再中断”的主流程。
### 落地
1. `enterNpcInteraction(...)` 不再用 `firstMeaningfulContactResolved` 决定是否走 NPC 主动开场;只要是从 NPC 入口新开聊天,都调用 `startNpcInitiatedOpening(...)`
2. `handleNpcInteraction(...)``chat` 分支保留“当前已经在同一段 `npcChatState` 内时,点击 `npc_chat` 作为玩家回复”的行为;若不在已有聊天内,统一调用 `startNpcInitiatedOpening(...)`
3. 删除负好感入口的本地敌对宣言分支。负好感只通过 `NpcChatDirective` 影响模型语气、功能选项和 `forceExit` 后的战斗/逃跑收束,不再跳过模型首句。
4. `enterNpcChat(...)` 仅保留为缺少角色/世界类型或模型开场失败时的兜底入口,不作为正常聊天开场路径。
### 补充验收
1. 不论好感度正负,也不论 `firstMeaningfulContactResolved` 是否为 `true`,新开聊天首轮都必须调用 `streamNpcChatTurn(..., '', { npcInitiatesConversation: true })`
2. 新开聊天最终展示的第一条 `dialogue` 必须是模型返回的 NPC 文本,`npcChatState.openingSource` 必须是 `npc_initiated`
3. 已经处于同一段 `npcChatState` 中时,点击 `npc_chat` 仍作为玩家本轮回复进入 `handleNpcChatTurn(...)`,不能重新开一段 NPC 首句。
4. 负好感入口不能直接显示本地战斗/逃跑;只有模型或玩家中断聊天后,才显示 `npc_fight``battle_escape_breakout`

View File

@@ -0,0 +1,66 @@
# RPG 幕预览启动卡载入修复2026-04-30
## 背景
编辑器内点击“幕预览”后,独立预览层会一直停在“正在载入这一幕的游戏流程...”,无法进入真实 RPG 运行壳。
## 根因
`SceneActPreviewRuntime` 先调用 `handleCustomWorldSelect(profile)`,紧接着调用 `handleCharacterSelect(previewCharacter)`
`handleCharacterSelect()` 读取的是当前 render 闭包中的旧 `gameState`。此时 `handleCustomWorldSelect()` 写入的 `worldType` 还没有完成 React 状态提交,所以选角入口看到 `worldType` 为空后直接返回。随后幕预览虽然又手动写入了 `currentScene / currentScenePreset / currentEncounter`,却没有写入 `playerCharacter`,导致 `isPreviewReady` 永远不成立。
另一个隐患是:有 `currentEncounter` 时 story controller 不会主动生成普通首段剧情,而是交给 NPC 交互流接管;若预览没有显式注入一个可展示的 `currentStory`,运行面板也可能无法稳定挂载。
## 本轮继续修复
继续复测时发现,`SceneActPreviewRuntime` 虽然已经不再调用 `handleCharacterSelect()`,但仍会调用 `handleCustomWorldSelect(profile)` 来同步 runtime 静态资料。
这个入口是正式运行态的“选择世界”入口,会排队写入“已选择世界、尚未选角”的中间 `GameState`。在幕预览本地 `setGameState()` 写入玩家、场景与故事后,这个中间态仍可能覆盖回来,导致 `currentScenePreset``playerCharacter` 被清掉,预览层重新停在“正在载入这一幕的游戏流程...”。
本轮调整后:
1. 幕预览不再调用 `handleCustomWorldSelect(profile)`
2. 幕预览直接调用 `setRuntimeCustomWorldProfile(profile)``setRuntimeCharacterOverrides(buildCustomWorldRuntimeCharacters(profile))` 同步静态资料层。
3. `isPreviewReady` 同时校验:
- `currentScene === "Story"`
- `runtimeSessionId === "runtime-scene-act-preview"`
- 当前玩家就是本次预览角色
- 当前场景就是本次预览场景
- 当前 story 已经完成注入
4. 这样 preview ready 只依赖本次预览自己的完整启动结果,不再被正式选世界中间态影响。
## 修复口径
1. 幕预览不再调用 `handleCharacterSelect()` 触发后端开局副作用。
2. 幕预览不调用正式 `handleCustomWorldSelect(profile)`,而是直接同步 runtime 静态资料层。
3. 随后在同一个 `setGameState` 中一次性写入:
- `playerCharacter`
- `runtimeMode: "play"`
- `runtimePersistenceDisabled: true`
- `currentScene / currentScenePreset / currentEncounter`
- 玩家血蓝、技能冷却、装备、统计、进度、队伍、任务等运行态基础字段
- 当前幕 `currentSceneActState`
4. 幕预览使用固定临时 `runtimeSessionId: "runtime-scene-act-preview"`,并通过禁持久化快照保持不写正式存档。
5. 启动时同步 `hydrateStoryState()`,注入当前幕 NPC 的本地开场 story`RpgRuntimeShell` 立即满足挂载条件。
## 约束
1. 幕预览继续复用正式 `play` 运行链,不恢复旧 `preview/test` 行为分支。
2. 幕预览只允许前端做临时运行态装配;正式游戏开局仍由 `server-rs` 裁决。
3. 后续如把幕预览也迁到后端 bootstrap应新增专门的禁持久化 bootstrap 入口,而不是再次依赖 `handleCharacterSelect()` 的异步状态顺序。
## 验证
新增回归覆盖:
```bash
npm test -- --run src/components/CustomWorldEntityEditorModal.test.tsx
```
断言幕预览打开后:
1. 不再显示“正在载入这一幕的游戏流程...”。
2. `RpgRuntimeShell` 已收到预览玩家角色。
3. 运行态为 `play` 且禁用持久化。
4. 当前 story 已注入为当前幕 NPC 开场内容。

View File

@@ -57,3 +57,14 @@
- LLM 不可用时的聊天 reply、普通 choice、function choice 兜底生成。
3. `server-rs/crates/api-server/src/runtime_chat.rs` 只保留 Axum SSE、LLM 调用、解析、好感变化、结束聊天判断等流程编排,不再直接承载提示词正文或 choice 文案兜底。
4. 后续调整聊天 choice 语气、候选数量、`functionOptions` 描述方式、敌对聊天收束策略时,优先修改 `prompt/runtime_chat.rs`
## 7. 拼图 Prompt 独立目录收口
2026-04-30 追加收口:
1. 拼图提示词参考 RPG 的目录组织,统一迁入 `server-rs/crates/api-server/src/prompt/puzzle/`
2. `prompt/puzzle/agent_chat.rs` 承接拼图共创 Agent 的 system prompt、单轮 JSON 输出契约、用户提示词与 anchor pack / 聊天记录提示词组装。
3. `prompt/puzzle/image.rs` 承接拼图图片生成正式提示词与默认反向提示词。
4. `puzzle_agent_turn.rs` 只保留 LLM 调用、结果解析、阶段判断和 SpacetimeDB 写回输入构造,不再内联拼图聊天提示词正文。
5. `puzzle.rs` 只保留拼图路由、计费、DashScope、OSS、候选图持久化和运行态编排不再内联拼图图片提示词正文。
6. 后续调整拼图共创问法、输出契约、图片画面约束或反向提示词时,优先修改 `prompt/puzzle/`,不要在 `puzzle.rs``puzzle_agent_turn.rs` 中新增提示词正文。

View File

@@ -390,6 +390,7 @@ SELECT * FROM custom_world_gallery_entry WHERE public_work_code = '<public_work_
- 作用:拼图创作 Agent 会话表,保存种子、阶段、锚点包、草稿和已发布 profile。
- 结构:`session_id PK: String`, `owner_user_id: String`, `seed_text: String`, `current_turn: u32`, `progress_percent: u32`, `stage: PuzzleAgentStage`, `anchor_pack_json: String`, `draft_json: Option<String>`, `last_assistant_reply: Option<String>`, `published_profile_id: Option<String>`, `created_at: Timestamp`, `updated_at: Timestamp`
- 说明:填表式拼图入口会在点击“拼图”时立即创建空 session生成草稿前的表单自动保存复用 `seed_text``draft_json`,不新增表字段,`stage` 保持 `CollectingAnchors`
- 索引:`owner_user_id`
```sql
@@ -409,8 +410,10 @@ SELECT * FROM puzzle_agent_message WHERE session_id = '<session_id>' ORDER BY cr
### `puzzle_work_profile`
- 作用:拼图作品主表,保存作品信息、封面、发布状态、游玩次数和锚点包。
- 结构:`profile_id PK: String`, `work_id: String`, `owner_user_id: String`, `source_session_id: Option<String>`, `author_display_name: String`, `level_name: String`, `summary: String`, `theme_tags_json: String`, `cover_image_src: Option<String>`, `cover_asset_id: Option<String>`, `publication_status: PuzzlePublicationStatus`, `play_count: u32`, `anchor_pack_json: String`, `publish_ready: bool`, `created_at: Timestamp`, `updated_at: Timestamp`, `published_at: Option<Timestamp>`
- 作用:拼图作品主表,保存作品信息、多关卡草稿、封面、发布状态、游玩次数和锚点包。
- 结构:`profile_id PK: String`, `work_id: String`, `owner_user_id: String`, `source_session_id: Option<String>`, `author_display_name: String`, `work_title: String`, `work_description: String`, `level_name: String`, `summary: String`, `theme_tags_json: String`, `cover_image_src: Option<String>`, `cover_asset_id: Option<String>`, `levels_json: String`, `publication_status: PuzzlePublicationStatus`, `play_count: u32`, `anchor_pack_json: String`, `publish_ready: bool`, `created_at: Timestamp`, `updated_at: Timestamp`, `published_at: Option<Timestamp>`
- 说明:`work_title`/`work_description` 是作品详情页展示来源;`levels_json` 保存拼图关卡列表,`level_name`/`summary` 继续作为首关兼容字段和旧数据回退来源。
- 说明:拼图初始表单草稿也写入本表作为创作中心卡片投影;未生成图片前 `cover_image_src = None``publish_ready = false`,再次打开草稿时通过 `source_session_id` 恢复表单。
- 索引:`owner_user_id`, `publication_status`
```sql

View File

@@ -0,0 +1,42 @@
# 作品作者按用户 ID 解析设计 2026-04-30
## 背景
作品列表、公开广场和详情页需要展示作者信息。旧链路里部分作品表会同时写入 `author_display_name`,如果用户后续修改昵称,旧作品仍会显示发布时的昵称快照,造成作者信息不一致。
## 目标
1. 作品作者的真相源统一使用 `owner_user_id`
2. API 返回作品读模型时,通过 `owner_user_id` 读取账号公开资料,并使用最新 `display_name` 作为 `authorDisplayName`
3. `author_display_name` 暂时保留为历史兼容字段,只在用户资料不存在或读取失败时作为回退值。
4. 前端详情页优先展示按 `ownerUserId` 读取到的公开用户资料;作品字段里的作者名只作为兜底展示。
## 落地规则
### SpacetimeDB 存储
1. `custom_world_profile.owner_user_id` / `custom_world_gallery_entry.owner_user_id` 是 RPG 作品作者 ID。
2. `puzzle_work_profile.owner_user_id` 是拼图作品作者 ID。
3. `big_fish_creation_session.owner_user_id` 是大鱼吃小鱼作品作者 ID。
4. 现有 `author_display_name` 不再作为作者真相源,不新增依赖它做权限、同作者推荐或作者资料展示的逻辑。
5. 本次不删除 `author_display_name`,避免破坏历史迁移包、生成绑定和旧客户端兼容;后续若要删除,必须单独做 schema 迁移和绑定刷新。
### API facade
1. 输出 `authorDisplayName` 时先用 `owner_user_id` 查询认证用户表。
2. 查询成功时使用用户最新 `display_name`,并同步补齐 `public_user_code`
3. 查询失败或用户缺失时才回退作品表旧 `author_display_name`
4. 大鱼吃小鱼公开作品不再由前端硬编码作者名API 根据 `owner_user_id` 输出作者显示名。
### 前端
1. 统一作品详情页已按 `ownerUserId` 读取公开用户摘要,用于头像和作者名。
2. 详情页展示作者名时优先使用公开用户摘要的 `displayName`,缺失时回退作品读模型的 `authorDisplayName`
3. 新增作品类型接入平台详情页时,不允许只在前端写固定作者昵称。
## 验收点
1. 用户修改昵称后RPG / 拼图 / 大鱼公开作品列表与详情页能展示新昵称。
2. 旧作品缺少可读取用户资料时,仍能用历史 `author_display_name` 或“玩家”兜底。
3. 作品权限和“同作者”判断继续使用 `owner_user_id`
4. 本次不改变 SpacetimeDB 表结构,因此不需要调整 `migration.rs` 白名单或导入补字段逻辑。

View File

@@ -4,6 +4,7 @@ export interface BigFishWorkSummary {
workId: string;
sourceSessionId: string;
ownerUserId: string;
authorDisplayName: string;
title: string;
subtitle: string;
summary: string;

View File

@@ -13,6 +13,7 @@ export interface PuzzleAgentSuggestedAction {
}
export type PuzzleAgentActionType =
| 'save_puzzle_form_draft'
| 'compile_puzzle_draft'
| 'generate_puzzle_images'
| 'select_puzzle_image'
@@ -39,27 +40,42 @@ export interface PuzzleAgentOperationRecord {
}
export type PuzzleAgentActionRequest =
| {
action: 'save_puzzle_form_draft';
promptText?: string | null;
workTitle?: string;
workDescription?: string;
pictureDescription?: string;
}
| {
action: 'compile_puzzle_draft';
promptText?: string | null;
workTitle?: string;
workDescription?: string;
pictureDescription?: string;
referenceImageSrc?: string | null;
candidateCount?: number;
}
| {
action: 'generate_puzzle_images';
levelId?: string | null;
promptText?: string | null;
referenceImageSrc?: string | null;
candidateCount?: number;
}
| {
action: 'select_puzzle_image';
levelId?: string | null;
candidateId: string;
}
| {
action: 'publish_puzzle_work';
workTitle?: string;
workDescription?: string;
levelName?: string;
summary?: string;
themeTags?: string[];
levelsJson?: string;
};
/**

View File

@@ -42,7 +42,20 @@ export interface PuzzleGeneratedImageCandidate {
selected: boolean;
}
export interface PuzzleDraftLevel {
levelId: string;
levelName: string;
pictureDescription: string;
candidates: PuzzleGeneratedImageCandidate[];
selectedCandidateId: string | null;
coverImageSrc: string | null;
coverAssetId: string | null;
generationStatus: 'idle' | 'generating' | 'ready';
}
export interface PuzzleResultDraft {
workTitle?: string;
workDescription?: string;
levelName: string;
summary: string;
themeTags: string[];
@@ -54,5 +67,11 @@ export interface PuzzleResultDraft {
coverImageSrc: string | null;
coverAssetId: string | null;
generationStatus: 'idle' | 'generating' | 'ready';
levels?: PuzzleDraftLevel[];
formDraft?: {
workTitle?: string;
workDescription?: string;
pictureDescription?: string;
} | null;
metadata?: JsonObject | null;
}

View File

@@ -27,6 +27,7 @@ export interface PuzzleAgentMessage {
export interface PuzzleAgentSessionSnapshot {
sessionId: string;
seedText?: string;
currentTurn: number;
progressPercent: number;
stage: PuzzleAgentStage;
@@ -42,6 +43,8 @@ export interface PuzzleAgentSessionSnapshot {
export interface CreatePuzzleAgentSessionRequest {
seedText?: string;
workTitle?: string;
workDescription?: string;
pictureDescription?: string;
referenceImageSrc?: string | null;
}

View File

@@ -79,6 +79,7 @@ export interface PuzzleRunSnapshot {
export interface StartPuzzleRunRequest {
profileId: string;
levelId?: string | null;
}
export interface AdvanceLocalPuzzleNextLevelRequest {

View File

@@ -1,5 +1,5 @@
import type { JsonObject } from './common';
import type { PuzzleAnchorPack } from './puzzleAgentDraft';
import type { PuzzleAnchorPack, PuzzleDraftLevel } from './puzzleAgentDraft';
export type PuzzleWorkPublicationStatus = 'draft' | 'published';
@@ -9,6 +9,8 @@ export interface PuzzleWorkSummary {
ownerUserId: string;
sourceSessionId?: string | null;
authorDisplayName: string;
workTitle?: string;
workDescription?: string;
levelName: string;
summary: string;
themeTags: string[];
@@ -26,6 +28,7 @@ export interface PuzzleWorkSummary {
export interface PuzzleWorkProfile extends PuzzleWorkSummary {
anchorPack: PuzzleAnchorPack;
levels?: PuzzleDraftLevel[];
metadata?: JsonObject | null;
}

View File

@@ -100,8 +100,15 @@ async function recreateTempDir(dir) {
async function replaceGeneratedDir(fromDir, toDir) {
assertInside(toDir, REPO_ROOT, '仓库生成目录');
await rm(toDir, {recursive: true, force: true});
await mkdir(path.dirname(toDir), {recursive: true});
await cp(fromDir, toDir, {recursive: true});
await mkdir(toDir, {recursive: true});
const entries = await readdir(fromDir, {withFileTypes: true});
for (const entry of entries) {
await cp(path.join(fromDir, entry.name), path.join(toDir, entry.name), {
recursive: true,
force: true,
});
}
}
function assertInside(candidate, parent, label) {
@@ -171,12 +178,12 @@ function run(command, commandArgs) {
return;
}
if (output.includes('Could not format generated files')) {
reject(new Error(`${command} 生成后格式化失败。`));
return;
}
if (code === 0) {
if (output.includes('Could not format generated files')) {
console.warn(
`[spacetime:generate] ${command} 已生成 bindings但 formatter 受限未执行;继续同步生成文件。`,
);
}
resolve();
return;
}

View File

@@ -1,7 +1,7 @@
use axum::{
Router,
body::Body,
extract::Extension,
extract::{DefaultBodyLimit, Extension},
http::Request,
middleware,
routing::{delete, get, post},
@@ -34,8 +34,9 @@ use crate::{
auth_sessions::auth_sessions,
big_fish::{
create_big_fish_session, delete_big_fish_work, execute_big_fish_action,
get_big_fish_session, get_big_fish_works, list_big_fish_gallery, record_big_fish_play,
remix_big_fish_gallery_work, stream_big_fish_message, submit_big_fish_message,
get_big_fish_session, get_big_fish_works, list_big_fish_gallery,
record_big_fish_gallery_like, record_big_fish_play, remix_big_fish_gallery_work,
stream_big_fish_message, submit_big_fish_message,
},
character_animation_assets::{
generate_character_animation, get_character_animation_job, get_character_workflow_cache,
@@ -56,9 +57,9 @@ use crate::{
get_custom_world_gallery_detail_by_code, get_custom_world_library,
get_custom_world_library_detail, get_custom_world_works, list_custom_world_gallery,
publish_custom_world_library_profile, put_custom_world_library_profile,
record_custom_world_gallery_play, remix_custom_world_gallery_profile,
stream_custom_world_agent_message, submit_custom_world_agent_message,
unpublish_custom_world_library_profile,
record_custom_world_gallery_like, record_custom_world_gallery_play,
remix_custom_world_gallery_profile, stream_custom_world_agent_message,
submit_custom_world_agent_message, unpublish_custom_world_library_profile,
},
custom_world_ai::{
generate_custom_world_cover_image, generate_custom_world_entity,
@@ -85,9 +86,10 @@ use crate::{
advance_local_puzzle_next_level, advance_puzzle_next_level, create_puzzle_agent_session,
delete_puzzle_work, execute_puzzle_agent_action, get_puzzle_agent_session,
get_puzzle_gallery_detail, get_puzzle_run, get_puzzle_work_detail, get_puzzle_works,
list_puzzle_gallery, put_puzzle_work, remix_puzzle_gallery_work, start_puzzle_run,
stream_puzzle_agent_message, submit_puzzle_agent_message, submit_puzzle_leaderboard,
swap_puzzle_pieces, update_puzzle_run_pause, use_puzzle_runtime_prop,
list_puzzle_gallery, put_puzzle_work, record_puzzle_gallery_like,
remix_puzzle_gallery_work, start_puzzle_run, stream_puzzle_agent_message,
submit_puzzle_agent_message, submit_puzzle_leaderboard, swap_puzzle_pieces,
update_puzzle_run_pause, use_puzzle_runtime_prop,
},
refresh_session::refresh_session,
request_context::{attach_request_context, resolve_request_id},
@@ -126,6 +128,8 @@ use crate::{
wechat_auth::{bind_wechat_phone, handle_wechat_callback, start_wechat_login},
};
const PUZZLE_REFERENCE_IMAGE_BODY_LIMIT_BYTES: usize = 12 * 1024 * 1024;
// 统一由这里构造 Axum 路由树,后续再逐项挂接中间件与业务路由。
pub fn build_router(state: AppState) -> Router {
let slow_request_threshold_ms = state.config.slow_request_threshold_ms;
@@ -544,6 +548,13 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/custom-world-gallery/{owner_user_id}/{profile_id}/like",
post(record_custom_world_gallery_like).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/custom-world-gallery/by-code/{code}",
get(get_custom_world_gallery_detail_by_code),
@@ -663,6 +674,13 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/big-fish/gallery/{session_id}/like",
post(record_big_fish_gallery_like).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/big-fish/works/{session_id}",
delete(delete_big_fish_work).route_layer(middleware::from_fn_with_state(
@@ -686,10 +704,15 @@ pub fn build_router(state: AppState) -> Router {
)
.route(
"/api/runtime/puzzle/agent/sessions",
post(create_puzzle_agent_session).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
post(create_puzzle_agent_session)
// 中文注释:拼图表单会携带单张参考图 Data URL需只给该写入入口放宽 body 上限。
.layer(DefaultBodyLimit::max(
PUZZLE_REFERENCE_IMAGE_BODY_LIMIT_BYTES,
))
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/puzzle/agent/sessions/{session_id}",
@@ -714,10 +737,15 @@ pub fn build_router(state: AppState) -> Router {
)
.route(
"/api/runtime/puzzle/agent/sessions/{session_id}/actions",
post(execute_puzzle_agent_action).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
post(execute_puzzle_agent_action)
// 中文注释:生成草稿/重新出图会复用 referenceImageSrc避免默认 2MB JSON limit 拦截。
.layer(DefaultBodyLimit::max(
PUZZLE_REFERENCE_IMAGE_BODY_LIMIT_BYTES,
))
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/puzzle/works",
@@ -748,6 +776,13 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/puzzle/gallery/{profile_id}/like",
post(record_puzzle_gallery_like).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/puzzle/runs",
post(start_puzzle_run).route_layer(middleware::from_fn_with_state(
@@ -1239,6 +1274,30 @@ mod tests {
.await
}
fn sign_test_user_token(
state: &AppState,
user: &module_auth::AuthUser,
session_id: &str,
) -> String {
let claims = AccessTokenClaims::from_input(
AccessTokenClaimsInput {
user_id: user.id.clone(),
session_id: session_id.to_string(),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: user.token_version,
phone_verified: false,
binding_status: BindingStatus::Active,
display_name: Some(user.display_name.clone()),
},
state.auth_jwt_config(),
OffsetDateTime::now_utc(),
)
.expect("claims should build");
sign_access_token(&claims, state.auth_jwt_config()).expect("token should sign")
}
async fn password_login_request(
app: Router,
phone_number: &str,
@@ -1496,6 +1555,88 @@ mod tests {
);
}
#[tokio::test]
async fn puzzle_agent_actions_accept_reference_image_body_above_default_limit() {
let state = AppState::new(AppConfig::default()).expect("state should build");
let seed_user = seed_phone_user_with_password(&state, "13800138024", TEST_PASSWORD).await;
let token = sign_test_user_token(&state, &seed_user, "sess_puzzle_reference_body");
let app = build_router(state);
let reference_image_src = format!("data:image/png;base64,{}", "A".repeat(3 * 1024 * 1024));
let request_body = serde_json::json!({
"action": "unsupported_large_reference_test",
"referenceImageSrc": reference_image_src,
})
.to_string();
assert!(request_body.len() > 2 * 1024 * 1024);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/runtime/puzzle/agent/sessions/puzzle-session-large/actions")
.header("authorization", format!("Bearer {token}"))
.header("content-type", "application/json")
.body(Body::from(request_body))
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let body = response
.into_body()
.collect()
.await
.expect("response body should collect")
.to_bytes();
let body_text = String::from_utf8_lossy(&body);
assert!(
body_text.contains("unsupported_large_reference_test"),
"handler should parse the oversized reference payload before rejecting the action: {body_text}"
);
assert!(!body_text.contains("length limit exceeded"));
}
#[tokio::test]
async fn puzzle_agent_session_creation_accepts_reference_image_body_above_default_limit() {
let state = AppState::new(AppConfig::default()).expect("state should build");
let seed_user = seed_phone_user_with_password(&state, "13800138025", TEST_PASSWORD).await;
let token = sign_test_user_token(&state, &seed_user, "sess_puzzle_form_reference_body");
let app = build_router(state);
let request_body = format!(
"{{\"seedText\":\"大参考图拼图\",\"pictureDescription\":\"一张用于验证 body limit 的参考图。\",\"referenceImageSrc\":\"data:image/png;base64,{}\"",
"A".repeat(3 * 1024 * 1024)
);
assert!(request_body.len() > 2 * 1024 * 1024);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/runtime/puzzle/agent/sessions")
.header("authorization", format!("Bearer {token}"))
.header("content-type", "application/json")
.body(Body::from(request_body))
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let body = response
.into_body()
.collect()
.await
.expect("response body should collect")
.to_bytes();
let body_text = String::from_utf8_lossy(&body);
assert!(
body_text.contains("EOF") || body_text.contains("expected"),
"handler should parse the oversized form payload before rejecting malformed JSON: {body_text}"
);
assert!(!body_text.contains("length limit exceeded"));
}
#[tokio::test]
async fn password_entry_rejects_unknown_phone_without_registration() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));

View File

@@ -85,8 +85,13 @@ async fn refund_asset_operation_points(
}
pub(crate) fn map_asset_operation_wallet_error(error: SpacetimeClientError) -> AppError {
let message = error.to_string();
tracing::warn!(
provider = "profile-wallet",
error = %message,
"资产操作陶泥币预扣失败"
);
let status = match &error {
SpacetimeClientError::Runtime(_) => StatusCode::BAD_REQUEST,
SpacetimeClientError::Procedure(message) if message.contains("陶泥币余额不足") => {
StatusCode::CONFLICT
}
@@ -95,7 +100,7 @@ pub(crate) fn map_asset_operation_wallet_error(error: SpacetimeClientError) -> A
AppError::from_status(status).with_details(json!({
"provider": "profile-wallet",
"message": error.to_string(),
"message": message,
}))
}

View File

@@ -33,9 +33,10 @@ use spacetime_client::{
BigFishAgentMessageRecord, BigFishAnchorItemRecord, BigFishAnchorPackRecord,
BigFishAssetCoverageRecord, BigFishAssetGenerateRecordInput, BigFishAssetSlotRecord,
BigFishBackgroundBlueprintRecord, BigFishDraftCompileRecordInput, BigFishGameDraftRecord,
BigFishLevelBlueprintRecord, BigFishMessageSubmitRecordInput, BigFishPlayReportRecordInput,
BigFishRuntimeParamsRecord, BigFishSessionCreateRecordInput, BigFishSessionRecord,
BigFishWorkRemixRecordInput, BigFishWorkSummaryRecord, SpacetimeClientError,
BigFishLevelBlueprintRecord, BigFishLikeReportRecordInput, BigFishMessageSubmitRecordInput,
BigFishPlayReportRecordInput, BigFishRuntimeParamsRecord, BigFishSessionCreateRecordInput,
BigFishSessionRecord, BigFishWorkRemixRecordInput, BigFishWorkSummaryRecord,
SpacetimeClientError,
};
use tokio::time::sleep;
@@ -60,6 +61,7 @@ use crate::{
http_error::AppError,
request_context::RequestContext,
state::AppState,
work_author::resolve_work_author_by_user_id,
};
pub async fn create_big_fish_session(
@@ -144,7 +146,7 @@ pub async fn get_big_fish_works(
BigFishWorksResponse {
items: items
.into_iter()
.map(map_big_fish_work_summary_response)
.map(|item| map_big_fish_work_summary_response(&state, item))
.collect(),
},
))
@@ -176,7 +178,7 @@ pub async fn list_big_fish_gallery(
BigFishWorksResponse {
items: items
.into_iter()
.map(map_big_fish_work_summary_response)
.map(|item| map_big_fish_work_summary_response(&state, item))
.collect(),
},
))
@@ -203,7 +205,7 @@ pub async fn delete_big_fish_work(
BigFishWorksResponse {
items: items
.into_iter()
.map(map_big_fish_work_summary_response)
.map(|item| map_big_fish_work_summary_response(&state, item))
.collect(),
},
))
@@ -245,7 +247,38 @@ pub async fn record_big_fish_play(
BigFishWorksResponse {
items: items
.into_iter()
.map(map_big_fish_work_summary_response)
.map(|item| map_big_fish_work_summary_response(&state, item))
.collect(),
},
))
}
pub async fn record_big_fish_gallery_like(
State(state): State<AppState>,
Path(session_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
) -> Result<Json<Value>, Response> {
ensure_non_empty(&request_context, &session_id, "sessionId")?;
let items = state
.spacetime_client()
.record_big_fish_like(BigFishLikeReportRecordInput {
session_id,
user_id: authenticated.claims().user_id().to_string(),
liked_at_micros: current_utc_micros(),
})
.await
.map_err(|error| {
big_fish_error_response(&request_context, map_big_fish_client_error(error))
})?;
Ok(json_success_body(
Some(&request_context),
BigFishWorksResponse {
items: items
.into_iter()
.map(|item| map_big_fish_work_summary_response(&state, item))
.collect(),
},
))
@@ -924,12 +957,15 @@ fn map_big_fish_agent_message_response(
}
fn map_big_fish_work_summary_response(
state: &AppState,
item: BigFishWorkSummaryRecord,
) -> BigFishWorkSummaryResponse {
let author = resolve_work_author_by_user_id(state, &item.owner_user_id, None, None);
BigFishWorkSummaryResponse {
work_id: item.work_id,
source_session_id: item.source_session_id,
owner_user_id: item.owner_user_id,
author_display_name: author.display_name,
title: item.title,
subtitle: item.subtitle,
summary: item.summary,

View File

@@ -7,6 +7,7 @@ use serde::Deserialize;
use serde_json::Value as JsonValue;
use crate::creation_agent_llm_turn::parse_json_response_text;
use crate::llm_model_routing::CREATION_TEMPLATE_LLM_MODEL;
const BIG_FISH_DRAFT_JSON_ONLY_SYSTEM_PROMPT: &str = r#"你是一个负责把“大鱼吃小鱼”玩法锚点编译成首版可实现草稿的中文玩法策划。
@@ -108,10 +109,15 @@ async fn request_big_fish_json_stage(
empty_response_message: &str,
) -> Result<JsonValue, BigFishDraftCompileError> {
let response = llm_client
.request_text(LlmTextRequest::new(vec![
LlmMessage::system(BIG_FISH_DRAFT_JSON_ONLY_SYSTEM_PROMPT),
LlmMessage::user(user_prompt),
]))
.request_text(
LlmTextRequest::new(vec![
LlmMessage::system(BIG_FISH_DRAFT_JSON_ONLY_SYSTEM_PROMPT),
LlmMessage::user(user_prompt),
])
.with_model(CREATION_TEMPLATE_LLM_MODEL)
.with_responses_api()
.with_web_search(true),
)
.await
.map_err(|error| {
BigFishDraftCompileError::new(format!("{debug_label} LLM 请求失败:{error}"))
@@ -124,12 +130,16 @@ async fn request_big_fish_json_stage(
Ok(value) => Ok(value),
Err(_) => {
let repaired = llm_client
.request_text(LlmTextRequest::new(vec![
LlmMessage::system(BIG_FISH_DRAFT_JSON_REPAIR_SYSTEM_PROMPT),
LlmMessage::user(format!(
"请把下面这段文本修复成单个合法 JSON 对象,不要补充额外解释:\n\n{text}"
)),
]))
.request_text(
LlmTextRequest::new(vec![
LlmMessage::system(BIG_FISH_DRAFT_JSON_REPAIR_SYSTEM_PROMPT),
LlmMessage::user(format!(
"请把下面这段文本修复成单个合法 JSON 对象,不要补充额外解释:\n\n{text}"
)),
])
.with_model(CREATION_TEMPLATE_LLM_MODEL)
.with_responses_api(),
)
.await
.map_err(|error| {
BigFishDraftCompileError::new(format!(

View File

@@ -1,6 +1,8 @@
use platform_llm::{LlmClient, LlmMessage, LlmStreamDelta, LlmTextRequest};
use serde_json::Value as JsonValue;
use crate::llm_model_routing::CREATION_TEMPLATE_LLM_MODEL;
#[derive(Clone, Copy, Debug)]
pub(crate) struct CreationAgentLlmTurnErrorMessages<'a> {
pub model_unavailable: &'a str,
@@ -69,6 +71,8 @@ fn build_creation_agent_llm_request(
LlmMessage::system(system_prompt),
LlmMessage::user(user_prompt),
])
.with_model(CREATION_TEMPLATE_LLM_MODEL)
.with_responses_api()
.with_web_search(enable_web_search)
}
@@ -79,10 +83,14 @@ pub(crate) async fn request_creation_agent_json_turn<E>(
build_error: impl Fn(String) -> E,
) -> Result<JsonValue, E> {
let response = llm_client
.request_text(LlmTextRequest::new(vec![
LlmMessage::system(system_prompt),
LlmMessage::user(user_prompt),
]))
.request_text(
LlmTextRequest::new(vec![
LlmMessage::system(system_prompt),
LlmMessage::user(user_prompt),
])
.with_model(CREATION_TEMPLATE_LLM_MODEL)
.with_responses_api(),
)
.await
.map_err(|error| build_error(error.to_string()))?;
parse_json_response_text(response.content.as_str())
@@ -160,6 +168,8 @@ fn read_reply_text(parsed: &JsonValue) -> Option<String> {
#[cfg(test)]
mod tests {
use crate::llm_model_routing::CREATION_TEMPLATE_LLM_MODEL;
use super::{
build_creation_agent_llm_request, extract_reply_text_from_partial_json,
parse_json_response_text,
@@ -188,6 +198,8 @@ mod tests {
build_creation_agent_llm_request("系统提示".to_string(), "用户提示".to_string(), true);
assert!(request.enable_web_search);
assert_eq!(request.model.as_deref(), Some(CREATION_TEMPLATE_LLM_MODEL));
assert_eq!(request.protocol, platform_llm::LlmTextProtocol::Responses);
assert_eq!(request.messages.len(), 2);
}
}

View File

@@ -38,10 +38,10 @@ use spacetime_client::{
CustomWorldAgentSessionRecord, CustomWorldDraftCardDetailRecord,
CustomWorldDraftCardDetailSectionRecord, CustomWorldDraftCardRecord,
CustomWorldGalleryEntryRecord, CustomWorldLibraryEntryRecord,
CustomWorldProfilePlayReportRecordInput, CustomWorldProfileRemixRecordInput,
CustomWorldProfileUpsertRecordInput, CustomWorldPublishGateRecord,
CustomWorldResultPreviewBlockerRecord, CustomWorldSupportedActionRecord,
CustomWorldWorkSummaryRecord, SpacetimeClientError,
CustomWorldProfileLikeReportRecordInput, CustomWorldProfilePlayReportRecordInput,
CustomWorldProfileRemixRecordInput, CustomWorldProfileUpsertRecordInput,
CustomWorldPublishGateRecord, CustomWorldResultPreviewBlockerRecord,
CustomWorldSupportedActionRecord, CustomWorldWorkSummaryRecord, SpacetimeClientError,
};
use std::{collections::BTreeSet, convert::Infallible, sync::Arc, time::Instant};
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
@@ -73,6 +73,7 @@ use crate::{
},
request_context::RequestContext,
state::AppState,
work_author::resolve_work_author_by_user_id,
};
const DRAFT_ASSET_GENERATION_MAX_ATTEMPTS: u32 = 3;
@@ -414,7 +415,6 @@ pub async fn get_custom_world_library(
Extension(authenticated): Extension<AuthenticatedAccessToken>,
) -> Result<Json<Value>, Response> {
let owner_user_id = authenticated.claims().user_id().to_string();
let author_display_name = resolve_author_display_name(&state, &authenticated);
let entries = state
.spacetime_client()
.list_custom_world_works(owner_user_id.clone())
@@ -430,9 +430,9 @@ pub async fn get_custom_world_library(
.into_iter()
.filter_map(|item| {
map_custom_world_library_entry_response_from_work_summary(
&state,
item,
&owner_user_id,
&author_display_name,
)
})
.collect(),
@@ -467,7 +467,7 @@ pub async fn get_custom_world_library_detail(
Ok(json_success_body(
Some(&request_context),
CustomWorldGalleryDetailResponse {
entry: map_custom_world_library_entry_response(detail.entry),
entry: map_custom_world_library_entry_response(&state, detail.entry),
},
))
}
@@ -548,8 +548,11 @@ pub async fn put_custom_world_library_profile(
Ok(json_success_body(
Some(&request_context),
CustomWorldLibraryMutationResponse {
entry: map_custom_world_library_entry_response(mutation.entry.clone()),
entries: vec![map_custom_world_library_entry_response(mutation.entry)],
entry: map_custom_world_library_entry_response(&state, mutation.entry.clone()),
entries: vec![map_custom_world_library_entry_response(
&state,
mutation.entry,
)],
},
))
}
@@ -584,7 +587,7 @@ pub async fn delete_custom_world_library_profile(
CustomWorldLibraryResponse {
entries: entries
.into_iter()
.map(map_custom_world_library_entry_response)
.map(|entry| map_custom_world_library_entry_response(&state, entry))
.collect(),
},
))
@@ -636,8 +639,11 @@ pub async fn publish_custom_world_library_profile(
Ok(json_success_body(
Some(&request_context),
CustomWorldLibraryMutationResponse {
entry: map_custom_world_library_entry_response(mutation.entry.clone()),
entries: vec![map_custom_world_library_entry_response(mutation.entry)],
entry: map_custom_world_library_entry_response(&state, mutation.entry.clone()),
entries: vec![map_custom_world_library_entry_response(
&state,
mutation.entry,
)],
},
))
}
@@ -675,8 +681,11 @@ pub async fn unpublish_custom_world_library_profile(
Ok(json_success_body(
Some(&request_context),
CustomWorldLibraryMutationResponse {
entry: map_custom_world_library_entry_response(mutation.entry.clone()),
entries: vec![map_custom_world_library_entry_response(mutation.entry)],
entry: map_custom_world_library_entry_response(&state, mutation.entry.clone()),
entries: vec![map_custom_world_library_entry_response(
&state,
mutation.entry,
)],
},
))
}
@@ -698,7 +707,7 @@ pub async fn list_custom_world_gallery(
CustomWorldGalleryResponse {
entries: entries
.into_iter()
.map(map_custom_world_gallery_card_response)
.map(|entry| map_custom_world_gallery_card_response(&state, entry))
.collect(),
},
))
@@ -730,7 +739,7 @@ pub async fn get_custom_world_gallery_detail(
Ok(json_success_body(
Some(&request_context),
CustomWorldGalleryDetailResponse {
entry: map_custom_world_library_entry_response(detail.entry),
entry: map_custom_world_library_entry_response(&state, detail.entry),
},
))
}
@@ -761,7 +770,7 @@ pub async fn get_custom_world_gallery_detail_by_code(
Ok(json_success_body(
Some(&request_context),
CustomWorldGalleryDetailResponse {
entry: map_custom_world_library_entry_response(detail.entry),
entry: map_custom_world_library_entry_response(&state, detail.entry),
},
))
}
@@ -800,8 +809,11 @@ pub async fn remix_custom_world_gallery_profile(
Ok(json_success_body(
Some(&request_context),
CustomWorldLibraryMutationResponse {
entry: map_custom_world_library_entry_response(mutation.entry.clone()),
entries: vec![map_custom_world_library_entry_response(mutation.entry)],
entry: map_custom_world_library_entry_response(&state, mutation.entry.clone()),
entries: vec![map_custom_world_library_entry_response(
&state,
mutation.entry,
)],
},
))
}
@@ -837,7 +849,44 @@ pub async fn record_custom_world_gallery_play(
Ok(json_success_body(
Some(&request_context),
CustomWorldGalleryDetailResponse {
entry: map_custom_world_library_entry_response(mutation.entry),
entry: map_custom_world_library_entry_response(&state, mutation.entry),
},
))
}
pub async fn record_custom_world_gallery_like(
State(state): State<AppState>,
Path((owner_user_id, profile_id)): Path<(String, String)>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
) -> Result<Json<Value>, Response> {
if owner_user_id.trim().is_empty() || profile_id.trim().is_empty() {
return Err(custom_world_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "custom-world-gallery",
"message": "ownerUserId and profileId are required",
})),
));
}
let mutation = state
.spacetime_client()
.record_custom_world_profile_like(CustomWorldProfileLikeReportRecordInput {
owner_user_id,
profile_id,
user_id: authenticated.claims().user_id().to_string(),
liked_at_micros: current_utc_micros(),
})
.await
.map_err(|error| {
custom_world_error_response(&request_context, map_custom_world_client_error(error))
})?;
Ok(json_success_body(
Some(&request_context),
CustomWorldGalleryDetailResponse {
entry: map_custom_world_library_entry_response(&state, mutation.entry),
},
))
}
@@ -2697,18 +2746,25 @@ async fn upsert_custom_world_draft_foundation_progress(
}
fn map_custom_world_library_entry_response(
state: &AppState,
entry: CustomWorldLibraryEntryRecord,
) -> CustomWorldLibraryEntryResponse {
let author = resolve_work_author_by_user_id(
state,
&entry.owner_user_id,
Some(&entry.author_display_name),
entry.author_public_user_code.as_deref(),
);
CustomWorldLibraryEntryResponse {
owner_user_id: entry.owner_user_id,
profile_id: entry.profile_id,
public_work_code: entry.public_work_code,
author_public_user_code: entry.author_public_user_code,
author_public_user_code: author.public_user_code.or(entry.author_public_user_code),
profile: entry.profile,
visibility: entry.visibility,
published_at: entry.published_at,
updated_at: entry.updated_at,
author_display_name: entry.author_display_name,
author_display_name: author.display_name,
world_name: entry.world_name,
subtitle: entry.subtitle,
summary_text: entry.summary_text,
@@ -2724,23 +2780,24 @@ fn map_custom_world_library_entry_response(
}
fn map_custom_world_library_entry_response_from_work_summary(
state: &AppState,
item: CustomWorldWorkSummaryRecord,
owner_user_id: &str,
author_display_name: &str,
) -> Option<CustomWorldLibraryEntryResponse> {
let profile_id = item.profile_id.as_ref()?.clone();
let profile = build_custom_world_library_list_profile_payload(&item, &profile_id);
let author = resolve_work_author_by_user_id(state, owner_user_id, None, None);
Some(CustomWorldLibraryEntryResponse {
owner_user_id: owner_user_id.to_string(),
public_work_code: (item.status == "published")
.then(|| build_public_work_code_from_profile_id(&profile_id)),
profile_id,
author_public_user_code: None,
author_public_user_code: author.public_user_code,
profile,
visibility: item.status,
published_at: item.published_at,
updated_at: item.updated_at,
author_display_name: author_display_name.to_string(),
author_display_name: author.display_name,
world_name: item.title,
subtitle: item.subtitle,
summary_text: item.summary,
@@ -2803,17 +2860,26 @@ fn build_custom_world_library_list_profile_payload(
}
fn map_custom_world_gallery_card_response(
state: &AppState,
entry: CustomWorldGalleryEntryRecord,
) -> CustomWorldGalleryCardResponse {
let author = resolve_work_author_by_user_id(
state,
&entry.owner_user_id,
Some(&entry.author_display_name),
Some(&entry.author_public_user_code),
);
CustomWorldGalleryCardResponse {
owner_user_id: entry.owner_user_id,
profile_id: entry.profile_id,
public_work_code: entry.public_work_code,
author_public_user_code: entry.author_public_user_code,
author_public_user_code: author
.public_user_code
.unwrap_or(entry.author_public_user_code),
visibility: entry.visibility,
published_at: entry.published_at,
updated_at: entry.updated_at,
author_display_name: entry.author_display_name,
author_display_name: author.display_name,
world_name: entry.world_name,
subtitle: entry.subtitle,
summary_text: entry.summary_text,

View File

@@ -1,6 +1,8 @@
use platform_llm::{LlmClient, LlmMessage, LlmTextRequest};
use serde_json::{Map as JsonMap, Value as JsonValue};
use shared_contracts::runtime::ExecuteCustomWorldAgentActionRequest;
use crate::llm_model_routing::CREATION_TEMPLATE_LLM_MODEL;
use spacetime_client::CustomWorldAgentSessionRecord;
const CUSTOM_WORLD_AGENT_CHARACTER_EXPANSION_SYSTEM_PROMPT: &str =
@@ -92,10 +94,15 @@ pub async fn generate_custom_world_agent_entities(
};
let response = llm_client
.request_text(LlmTextRequest::new(vec![
LlmMessage::system(system_prompt),
LlmMessage::user(user_prompt),
]))
.request_text(
LlmTextRequest::new(vec![
LlmMessage::system(system_prompt),
LlmMessage::user(user_prompt),
])
.with_model(CREATION_TEMPLATE_LLM_MODEL)
.with_responses_api()
.with_web_search(true),
)
.await
.map_err(|error| format!("{action} LLM 请求失败:{error}"))?;
let generated_entities = parse_json_array_response(response.content.as_str())

View File

@@ -35,6 +35,7 @@ use crate::{
build_result_scene_npc_system_prompt, build_result_scene_npc_user_prompt,
},
http_error::AppError,
llm_model_routing::CREATION_TEMPLATE_LLM_MODEL,
prompt::scene_background::{
DEFAULT_CUSTOM_WORLD_SCENE_IMAGE_NEGATIVE_PROMPT, SceneImagePromptLandmark,
SceneImagePromptParams, SceneImagePromptProfile, build_custom_world_scene_image_prompt,
@@ -1039,7 +1040,10 @@ async fn generate_entity_with_fallback(state: &AppState, profile: &Value, kind:
let request = LlmTextRequest::new(vec![
LlmMessage::system(build_result_entity_system_prompt()),
LlmMessage::user(build_result_entity_user_prompt(profile, kind, &fallback)),
]);
])
.with_model(CREATION_TEMPLATE_LLM_MODEL)
.with_responses_api()
.with_web_search(true);
llm_client
.request_text(request)
@@ -1065,7 +1069,10 @@ async fn generate_scene_npc_with_fallback(
landmark_id,
&fallback,
)),
]);
])
.with_model(CREATION_TEMPLATE_LLM_MODEL)
.with_responses_api()
.with_web_search(true);
llm_client
.request_text(request)

View File

@@ -11,6 +11,8 @@ use serde_json::{Map as JsonMap, Value as JsonValue, json};
use shared_contracts::runtime::ExecuteCustomWorldAgentActionRequest;
use spacetime_client::CustomWorldAgentSessionRecord;
use crate::llm_model_routing::CREATION_TEMPLATE_LLM_MODEL;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldFoundationDraftResult {
pub draft_profile_json: String,
@@ -174,10 +176,15 @@ where
F: Fn(&str) -> String,
{
let response = llm_client
.request_text(LlmTextRequest::new(vec![
LlmMessage::system(FOUNDATION_JSON_ONLY_SYSTEM_PROMPT),
LlmMessage::user(user_prompt),
]))
.request_text(
LlmTextRequest::new(vec![
LlmMessage::system(FOUNDATION_JSON_ONLY_SYSTEM_PROMPT),
LlmMessage::user(user_prompt),
])
.with_model(CREATION_TEMPLATE_LLM_MODEL)
.with_responses_api()
.with_web_search(true),
)
.await
.map_err(|error| format!("{debug_label} LLM 请求失败:{error}"))?;
let text = response.content.trim();
@@ -188,10 +195,14 @@ where
Ok(value) => Ok(value),
Err(_) => {
let repaired = llm_client
.request_text(LlmTextRequest::new(vec![
LlmMessage::system(FOUNDATION_JSON_REPAIR_SYSTEM_PROMPT),
LlmMessage::user(repair_prompt_builder(text)),
]))
.request_text(
LlmTextRequest::new(vec![
LlmMessage::system(FOUNDATION_JSON_REPAIR_SYSTEM_PROMPT),
LlmMessage::user(repair_prompt_builder(text)),
])
.with_model(CREATION_TEMPLATE_LLM_MODEL)
.with_responses_api(),
)
.await
.map_err(|error| format!("{repair_debug_label} LLM 请求失败:{error}"))?;
parse_json_response_text(repaired.content.as_str())

View File

@@ -4,7 +4,7 @@ use axum::{
http::StatusCode,
response::Response,
};
use platform_llm::{LlmError, LlmMessage, LlmMessageRole, LlmTextRequest};
use platform_llm::{LlmError, LlmMessage, LlmMessageRole, LlmTextProtocol, LlmTextRequest};
use serde_json::Value;
use shared_contracts::llm::{
LlmChatCompletionRequest, LlmChatCompletionResponse, LlmChatMessagePayload, LlmChatMessageRole,
@@ -39,6 +39,7 @@ pub async fn proxy_llm_chat_completions(
let request = LlmTextRequest {
model: payload.model,
protocol: LlmTextProtocol::ChatCompletions,
messages: payload
.messages
.into_iter()

View File

@@ -0,0 +1,2 @@
pub(crate) const RPG_STORY_LLM_MODEL: &str = "doubao-seed-character-251128";
pub(crate) const CREATION_TEMPLATE_LLM_MODEL: &str = "deepseek-v3-2-251201";

View File

@@ -36,6 +36,7 @@ mod health;
mod http_error;
mod legacy_generated_assets;
mod llm;
mod llm_model_routing;
mod login_options;
mod logout;
mod logout_all;
@@ -63,6 +64,7 @@ mod story_battles;
mod story_sessions;
mod wechat_auth;
mod wechat_provider;
mod work_author;
use shared_logging::init_tracing;
use tokio::net::TcpListener;

View File

@@ -1,7 +1,7 @@
pub(crate) mod big_fish;
pub(crate) mod character_animation;
pub(crate) mod character_visual;
pub(crate) mod puzzle_image;
pub(crate) mod puzzle;
pub(crate) mod rpg;
pub(crate) mod scene_background;

View File

@@ -0,0 +1,212 @@
use module_puzzle::{PuzzleAnchorPack, PuzzleAnchorStatus, empty_anchor_pack};
use serde_json::{Value as JsonValue, json};
use spacetime_client::{
PuzzleAgentMessageRecord, PuzzleAgentSessionRecord, PuzzleAnchorPackRecord,
};
use crate::creation_agent_anchor_templates::{
get_creation_agent_anchor_template, render_anchor_question_block,
};
use crate::creation_agent_chat::render_quick_fill_extra_rules;
/// 拼图共创 Agent 的系统提示词。
///
/// 这里作为拼图聊天提示词主源,业务文件只负责调用 LLM、解析结果和写回状态。
pub(crate) const PUZZLE_AGENT_SYSTEM_PROMPT: &str = r#"你是一个负责和陶泥主共创拼图画面的中文创意策划。
你要帮助用户把一句灵感逐步收束成可以发布成拼图关卡的视觉方案。
你必须同时输出:
1. 一段直接发给用户的中文回复 replyText
2. 当前进度 progressPercent
3. 下一轮完整可用的 nextAnchorPack
硬约束:
1. 只能输出 JSON不能输出代码块或解释
2. nextAnchorPack 必须是完整对象,不能只输出 patch
3. replyText 必须是自然中文不能提“字段”“锚点”“结构”“JSON”等内部词
4. replyText 一次最多推进一个最关键问题
5. 如果用户已经给出明确方向,就优先吸收和收束,不要机械反问
6. progressPercent 范围只能是 0 到 100
7. status 只能使用 missing / inferred / confirmed / locked
"#;
/// 拼图共创 Agent 单轮 JSON 输出契约。
const PUZZLE_AGENT_OUTPUT_CONTRACT: &str = r#"请严格按以下 JSON 输出,不要输出其他文字:
{
"replyText": "",
"progressPercent": 0,
"nextAnchorPack": {
"themePromise": {
"key": "themePromise",
"label": "题材承诺",
"value": "",
"status": "missing"
},
"visualSubject": {
"key": "visualSubject",
"label": "画面主体",
"value": "",
"status": "missing"
},
"visualMood": {
"key": "visualMood",
"label": "视觉气质",
"value": "",
"status": "missing"
},
"compositionHooks": {
"key": "compositionHooks",
"label": "拼图记忆点",
"value": "",
"status": "missing"
},
"tagsAndForbidden": {
"key": "tagsAndForbidden",
"label": "标签与禁忌",
"value": "",
"status": "missing"
}
}
}"#;
/// 拼图共创 Agent 的用户提示词,用于触发模型按系统约定返回单轮 JSON。
pub(crate) const PUZZLE_AGENT_JSON_TURN_USER_PROMPT: &str = "请按约定输出这一轮的 JSON。";
/// 拼图草稿生成对话提示词脚本。
pub(crate) fn build_puzzle_agent_prompt(
session: &PuzzleAgentSessionRecord,
quick_fill_requested: bool,
) -> String {
let anchor_question_block = get_creation_agent_anchor_template("puzzle")
.map(render_anchor_question_block)
.unwrap_or_else(|| "模板目标:收束成可以发布为拼图关卡的视觉方案。".to_string());
let quick_fill_rules = if quick_fill_requested {
format!(
"\n\n{}",
render_quick_fill_extra_rules(
"当前题材方向里的拼图关键词",
"不要要求用户再提供素材、风格或禁忌",
"输出完整 nextAnchorPack直接补齐 value 为空或 status 为 missing 的项",
"生成结果页",
)
)
} else {
String::new()
};
format!(
"{anchor_question_block}{quick_fill_rules}\n\n当前是第 {turn} 轮,当前进度 {progress}% 。\n\n是否要求自动补充剩余关键字:{quick_fill_requested_text}\n\n当前 anchor pack\n{anchor_pack}\n\n最近聊天记录:\n{chat_history}\n\n{contract}",
anchor_question_block = anchor_question_block,
quick_fill_rules = quick_fill_rules,
turn = session.current_turn.saturating_add(1),
progress = session.progress_percent,
quick_fill_requested_text = if quick_fill_requested { "" } else { "" },
anchor_pack = serialize_puzzle_record_anchor_pack(&session.anchor_pack),
chat_history =
serde_json::to_string_pretty(&build_chat_history(session.messages.as_slice()))
.unwrap_or_else(|_| "[]".to_string()),
contract = PUZZLE_AGENT_OUTPUT_CONTRACT,
)
}
/// 将 SpacetimeDB 记录态锚点序列化成提示词可读 JSON。
pub(crate) fn serialize_puzzle_record_anchor_pack(record: &PuzzleAnchorPackRecord) -> String {
serde_json::to_string_pretty(&map_puzzle_record_anchor_pack(record)).unwrap_or_else(|_| {
serde_json::to_string_pretty(&empty_anchor_pack()).unwrap_or_else(|_| "{}".to_string())
})
}
fn build_chat_history(messages: &[PuzzleAgentMessageRecord]) -> Vec<JsonValue> {
messages
.iter()
.map(|message| {
json!({
"role": message.role,
"kind": message.kind,
"content": message.text,
})
})
.collect()
}
fn map_puzzle_record_anchor_pack(record: &PuzzleAnchorPackRecord) -> PuzzleAnchorPack {
PuzzleAnchorPack {
theme_promise: map_puzzle_record_anchor_item(&record.theme_promise),
visual_subject: map_puzzle_record_anchor_item(&record.visual_subject),
visual_mood: map_puzzle_record_anchor_item(&record.visual_mood),
composition_hooks: map_puzzle_record_anchor_item(&record.composition_hooks),
tags_and_forbidden: map_puzzle_record_anchor_item(&record.tags_and_forbidden),
}
}
fn map_puzzle_record_anchor_item(
record: &spacetime_client::PuzzleAnchorItemRecord,
) -> module_puzzle::PuzzleAnchorItem {
module_puzzle::PuzzleAnchorItem {
key: record.key.clone(),
label: record.label.clone(),
value: record.value.clone(),
status: parse_puzzle_anchor_status(record.status.as_str()),
}
}
fn parse_puzzle_anchor_status(value: &str) -> PuzzleAnchorStatus {
match value {
"confirmed" => PuzzleAnchorStatus::Confirmed,
"locked" => PuzzleAnchorStatus::Locked,
"inferred" => PuzzleAnchorStatus::Inferred,
_ => PuzzleAnchorStatus::Missing,
}
}
#[cfg(test)]
mod tests {
use super::build_puzzle_agent_prompt;
fn anchor_item(
key: &str,
label: &str,
value: &str,
status: &str,
) -> spacetime_client::PuzzleAnchorItemRecord {
spacetime_client::PuzzleAnchorItemRecord {
key: key.to_string(),
label: label.to_string(),
value: value.to_string(),
status: status.to_string(),
}
}
fn empty_session_record() -> spacetime_client::PuzzleAgentSessionRecord {
spacetime_client::PuzzleAgentSessionRecord {
session_id: "puzzle-session-test".to_string(),
seed_text: "雨夜猫咪遗迹".to_string(),
current_turn: 2,
progress_percent: 60,
stage: "collecting_anchors".to_string(),
anchor_pack: spacetime_client::PuzzleAnchorPackRecord {
theme_promise: anchor_item("themePromise", "题材承诺", "雨夜猫咪遗迹", "confirmed"),
visual_subject: anchor_item("visualSubject", "画面主体", "", "missing"),
visual_mood: anchor_item("visualMood", "视觉气质", "", "missing"),
composition_hooks: anchor_item("compositionHooks", "拼图记忆点", "", "missing"),
tags_and_forbidden: anchor_item("tagsAndForbidden", "标签与禁忌", "", "missing"),
},
draft: None,
messages: Vec::new(),
last_assistant_reply: None,
published_profile_id: None,
suggested_actions: Vec::new(),
result_preview: None,
updated_at: "2026-04-24T10:00:00.000Z".to_string(),
}
}
#[test]
fn quick_fill_prompt_forbids_follow_up_questions() {
let prompt = build_puzzle_agent_prompt(&empty_session_record(), true);
assert!(prompt.contains("用户刚刚主动要求你自动补充剩余关键字"));
assert!(prompt.contains("不要再继续提问"));
assert!(prompt.contains("progressPercent 直接输出为 100"));
}
}

View File

@@ -0,0 +1,106 @@
/// 拼图图片生成的默认反向提示词。
///
/// 这里单独收口拼图图片提示词,避免图片生成链路、候选图持久化和 DashScope 请求编排
/// 混在同一个脚本里,后续调画风或资产约束时只需要改这一处。
pub(crate) const PUZZLE_DEFAULT_NEGATIVE_PROMPT: &str =
"低清晰度,低质量,文字水印,畸形构图,过度模糊,重复肢体,画面脏污";
/// wan2.2 / wan2.1 文生图旧协议的正向 prompt 上限。
///
/// 中文注释DashScope 旧 text2image 接口会把超长 prompt 判成请求参数不合法,
/// 所以这里先在拼图提示词模块内压缩,保证固定玩法约束不会被用户长描述挤掉。
pub(crate) const PUZZLE_TEXT_TO_IMAGE_PROMPT_MAX_CHARS: usize = 500;
const PUZZLE_IMAGE_LEVEL_NAME_MAX_CHARS: usize = 40;
const PUZZLE_IMAGE_PROMPT_FALLBACK: &str = "清晰、有辨识度的拼图画面";
/// 根据拼图关卡名和陶泥主输入构造最终发给图片模型的提示词。
pub(crate) fn build_puzzle_image_prompt(level_name: &str, prompt: &str) -> String {
let level_name =
truncate_puzzle_prompt_segment(level_name.trim(), PUZZLE_IMAGE_LEVEL_NAME_MAX_CHARS);
let prompt = prompt.trim();
let prompt = if prompt.is_empty() {
PUZZLE_IMAGE_PROMPT_FALLBACK
} else {
prompt
};
let template_chars = build_puzzle_image_prompt_text(level_name.as_str(), "")
.chars()
.count();
let prompt_max_chars = PUZZLE_TEXT_TO_IMAGE_PROMPT_MAX_CHARS.saturating_sub(template_chars);
let prompt = truncate_puzzle_prompt_segment(prompt, prompt_max_chars);
let image_prompt = build_puzzle_image_prompt_text(level_name.as_str(), prompt.as_str());
debug_assert!(
image_prompt.chars().count() <= PUZZLE_TEXT_TO_IMAGE_PROMPT_MAX_CHARS,
"puzzle image prompt should fit DashScope wan2.2 limit"
);
image_prompt
}
fn build_puzzle_image_prompt_text(level_name: &str, prompt: &str) -> String {
format!(
concat!(
"请生成一张高清插画。",
"关卡名:{level_name}。",
"画面主体:{prompt}。",
"画面要求1:1 正方形拼图关卡,适配 3x3 或 4x4 拼图切块,",
"主体要清晰集中,前中后景层次明确,局部细节丰富但不要杂乱,",
"避免文字、水印、边框和 UI 元素。"
),
level_name = level_name,
prompt = prompt,
)
}
fn truncate_puzzle_prompt_segment(value: &str, max_chars: usize) -> String {
if value.chars().count() <= max_chars {
return value.to_string();
}
const MARKER: &str = "...";
if max_chars <= MARKER.chars().count() {
return value.chars().take(max_chars).collect();
}
let keep_chars = max_chars - MARKER.chars().count();
format!(
"{}{MARKER}",
value.chars().take(keep_chars).collect::<String>()
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_puzzle_image_prompt_keeps_puzzle_asset_constraints() {
let prompt = build_puzzle_image_prompt("雨夜神庙", "猫咪在发光遗迹前寻找线索");
assert!(prompt.contains("雨夜神庙"));
assert!(prompt.contains("猫咪在发光遗迹前寻找线索"));
assert!(prompt.contains("1:1 正方形拼图关卡"));
assert!(prompt.contains("3x3 或 4x4"));
assert!(prompt.contains("避免文字、水印、边框和 UI 元素"));
}
#[test]
fn build_puzzle_image_prompt_trims_long_user_description_for_wan22() {
let long_level_name = "雨夜神庙".repeat(20);
let long_description =
"发光遗迹、猫咪、漂浮碎片、雨水反光、远处灯塔、适合拼图切块。".repeat(50);
let prompt = build_puzzle_image_prompt(long_level_name.as_str(), long_description.as_str());
assert!(prompt.chars().count() <= PUZZLE_TEXT_TO_IMAGE_PROMPT_MAX_CHARS);
assert!(prompt.contains("1:1 正方形拼图关卡"));
assert!(prompt.contains("3x3 或 4x4"));
assert!(prompt.contains("避免文字、水印、边框和 UI 元素"));
}
#[test]
fn default_negative_prompt_blocks_text_and_low_quality_assets() {
assert!(PUZZLE_DEFAULT_NEGATIVE_PROMPT.contains("低清晰度"));
assert!(PUZZLE_DEFAULT_NEGATIVE_PROMPT.contains("文字水印"));
}
}

View File

@@ -0,0 +1,2 @@
pub(crate) mod agent_chat;
pub(crate) mod image;

View File

@@ -1,44 +0,0 @@
/// 拼图图片生成的默认反向提示词。
///
/// 这里单独收口拼图图片提示词,避免图片生成链路、候选图持久化和 DashScope 请求编排
/// 混在同一个脚本里,后续调画风或资产约束时只需要改这一处。
pub(crate) const PUZZLE_DEFAULT_NEGATIVE_PROMPT: &str =
"低清晰度,低质量,文字水印,畸形构图,过度模糊,重复肢体,画面脏污";
/// 根据拼图关卡名和陶泥主输入构造最终发给图片模型的提示词。
pub(crate) fn build_puzzle_image_prompt(level_name: &str, prompt: &str) -> String {
format!(
concat!(
"请生成一张适合 1:1 正方形拼图关卡的高清插画。",
"关卡名:{level_name}。",
"画面主体:{prompt}。",
"画面要求1:1 正方形画布,适配 3x3 或 4x4 拼图切块,",
"主体要清晰集中,前中后景层次明确,局部细节丰富但不要杂乱,",
"避免文字、水印、边框和 UI 元素。"
),
level_name = level_name,
prompt = prompt,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_puzzle_image_prompt_keeps_puzzle_asset_constraints() {
let prompt = build_puzzle_image_prompt("雨夜神庙", "猫咪在发光遗迹前寻找线索");
assert!(prompt.contains("雨夜神庙"));
assert!(prompt.contains("猫咪在发光遗迹前寻找线索"));
assert!(prompt.contains("1:1 正方形拼图关卡"));
assert!(prompt.contains("3x3 或 4x4"));
assert!(prompt.contains("避免文字、水印、边框和 UI 元素"));
}
#[test]
fn default_negative_prompt_blocks_text_and_low_quality_assets() {
assert!(PUZZLE_DEFAULT_NEGATIVE_PROMPT.contains("低清晰度"));
assert!(PUZZLE_DEFAULT_NEGATIVE_PROMPT.contains("文字水印"));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,16 @@
use module_puzzle::{PuzzleAgentStage, PuzzleAnchorPack, PuzzleAnchorStatus, empty_anchor_pack};
use platform_llm::LlmClient;
use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json};
use spacetime_client::{
PuzzleAgentMessageFinalizeRecordInput, PuzzleAgentMessageRecord, PuzzleAgentSessionRecord,
};
use serde_json::Value as JsonValue;
use spacetime_client::{PuzzleAgentMessageFinalizeRecordInput, PuzzleAgentSessionRecord};
use crate::creation_agent_anchor_templates::{
get_creation_agent_anchor_template, render_anchor_question_block,
};
use crate::creation_agent_chat::render_quick_fill_extra_rules;
use crate::creation_agent_llm_turn::{
CreationAgentLlmTurnErrorMessages, stream_creation_agent_json_turn,
};
use crate::prompt::puzzle::agent_chat::{
PUZZLE_AGENT_JSON_TURN_USER_PROMPT, PUZZLE_AGENT_SYSTEM_PROMPT, build_puzzle_agent_prompt,
serialize_puzzle_record_anchor_pack,
};
#[derive(Clone, Debug)]
pub(crate) struct PuzzleAgentTurnRequest<'a> {
@@ -60,63 +58,6 @@ struct PuzzleAgentModelOutput {
next_anchor_pack: PuzzleAnchorPack,
}
const PUZZLE_AGENT_SYSTEM_PROMPT: &str = r#"你是一个负责和陶泥主共创拼图画面的中文创意策划。
你要帮助用户把一句灵感逐步收束成可以发布成拼图关卡的视觉方案。
你必须同时输出:
1. 一段直接发给用户的中文回复 replyText
2. 当前进度 progressPercent
3. 下一轮完整可用的 nextAnchorPack
硬约束:
1. 只能输出 JSON不能输出代码块或解释
2. nextAnchorPack 必须是完整对象,不能只输出 patch
3. replyText 必须是自然中文不能提“字段”“锚点”“结构”“JSON”等内部词
4. replyText 一次最多推进一个最关键问题
5. 如果用户已经给出明确方向,就优先吸收和收束,不要机械反问
6. progressPercent 范围只能是 0 到 100
7. status 只能使用 missing / inferred / confirmed / locked
"#;
const PUZZLE_AGENT_OUTPUT_CONTRACT: &str = r#"请严格按以下 JSON 输出,不要输出其他文字:
{
"replyText": "",
"progressPercent": 0,
"nextAnchorPack": {
"themePromise": {
"key": "themePromise",
"label": "题材承诺",
"value": "",
"status": "missing"
},
"visualSubject": {
"key": "visualSubject",
"label": "画面主体",
"value": "",
"status": "missing"
},
"visualMood": {
"key": "visualMood",
"label": "视觉气质",
"value": "",
"status": "missing"
},
"compositionHooks": {
"key": "compositionHooks",
"label": "拼图记忆点",
"value": "",
"status": "missing"
},
"tagsAndForbidden": {
"key": "tagsAndForbidden",
"label": "标签与禁忌",
"value": "",
"status": "missing"
}
}
}"#;
pub(crate) async fn run_puzzle_agent_turn<F>(
request: PuzzleAgentTurnRequest<'_>,
on_reply_update: F,
@@ -128,7 +69,7 @@ where
let turn_output = stream_creation_agent_json_turn(
request.llm_client,
format!("{PUZZLE_AGENT_SYSTEM_PROMPT}\n\n{prompt}"),
"请按约定输出这一轮的 JSON。",
PUZZLE_AGENT_JSON_TURN_USER_PROMPT,
request.enable_web_search,
CreationAgentLlmTurnErrorMessages {
model_unavailable: "当前模型不可用,请稍后重试。",
@@ -185,10 +126,6 @@ pub(crate) fn build_failed_finalize_record_input(
error_message: String,
updated_at_micros: i64,
) -> PuzzleAgentMessageFinalizeRecordInput {
let anchor_pack_json = serde_json::to_string(&map_record_anchor_pack(&session.anchor_pack))
.unwrap_or_else(|_| {
serde_json::to_string(&empty_anchor_pack()).unwrap_or_else(|_| "{}".to_string())
});
PuzzleAgentMessageFinalizeRecordInput {
session_id,
owner_user_id,
@@ -196,61 +133,12 @@ pub(crate) fn build_failed_finalize_record_input(
assistant_reply_text: None,
stage: session.stage.clone(),
progress_percent: session.progress_percent,
anchor_pack_json,
anchor_pack_json: serialize_puzzle_record_anchor_pack(&session.anchor_pack),
error_message: Some(error_message),
updated_at_micros,
}
}
fn build_puzzle_agent_prompt(
session: &PuzzleAgentSessionRecord,
quick_fill_requested: bool,
) -> String {
let anchor_question_block = get_creation_agent_anchor_template("puzzle")
.map(render_anchor_question_block)
.unwrap_or_else(|| "模板目标:收束成可以发布为拼图关卡的视觉方案。".to_string());
let quick_fill_rules = if quick_fill_requested {
format!(
"\n\n{}",
render_quick_fill_extra_rules(
"当前题材方向里的拼图关键词",
"不要要求用户再提供素材、风格或禁忌",
"输出完整 nextAnchorPack直接补齐 value 为空或 status 为 missing 的项",
"生成结果页",
)
)
} else {
String::new()
};
format!(
"{anchor_question_block}{quick_fill_rules}\n\n当前是第 {turn} 轮,当前进度 {progress}% 。\n\n是否要求自动补充剩余关键字:{quick_fill_requested_text}\n\n当前 anchor pack\n{anchor_pack}\n\n最近聊天记录:\n{chat_history}\n\n{contract}",
anchor_question_block = anchor_question_block,
quick_fill_rules = quick_fill_rules,
turn = session.current_turn.saturating_add(1),
progress = session.progress_percent,
quick_fill_requested_text = if quick_fill_requested { "" } else { "" },
anchor_pack = serde_json::to_string_pretty(&map_record_anchor_pack(&session.anchor_pack))
.unwrap_or_else(|_| "{}".to_string()),
chat_history =
serde_json::to_string_pretty(&build_chat_history(session.messages.as_slice()))
.unwrap_or_else(|_| "[]".to_string()),
contract = PUZZLE_AGENT_OUTPUT_CONTRACT,
)
}
fn build_chat_history(messages: &[PuzzleAgentMessageRecord]) -> Vec<JsonValue> {
messages
.iter()
.map(|message| {
json!({
"role": message.role,
"kind": message.kind,
"content": message.text,
})
})
.collect()
}
fn parse_model_output(parsed: &JsonValue) -> Result<PuzzleAgentModelOutput, PuzzleAgentTurnError> {
let reply_text = parsed
.get("replyText")
@@ -348,27 +236,6 @@ fn resolve_puzzle_agent_stage(progress_percent: u32) -> PuzzleAgentStage {
}
}
fn map_record_anchor_pack(record: &spacetime_client::PuzzleAnchorPackRecord) -> PuzzleAnchorPack {
PuzzleAnchorPack {
theme_promise: map_record_anchor_item(&record.theme_promise),
visual_subject: map_record_anchor_item(&record.visual_subject),
visual_mood: map_record_anchor_item(&record.visual_mood),
composition_hooks: map_record_anchor_item(&record.composition_hooks),
tags_and_forbidden: map_record_anchor_item(&record.tags_and_forbidden),
}
}
fn map_record_anchor_item(
record: &spacetime_client::PuzzleAnchorItemRecord,
) -> module_puzzle::PuzzleAnchorItem {
module_puzzle::PuzzleAnchorItem {
key: record.key.clone(),
label: record.label.clone(),
value: record.value.clone(),
status: parse_anchor_status(record.status.as_str()),
}
}
fn parse_anchor_status(value: &str) -> PuzzleAnchorStatus {
match value {
"confirmed" => PuzzleAnchorStatus::Confirmed,
@@ -383,57 +250,9 @@ mod tests {
use module_puzzle::PuzzleAnchorStatus;
use serde_json::json;
use super::{build_puzzle_agent_prompt, parse_model_output};
use super::parse_model_output;
use crate::creation_agent_llm_turn::extract_reply_text_from_partial_json;
fn empty_session_record() -> spacetime_client::PuzzleAgentSessionRecord {
spacetime_client::PuzzleAgentSessionRecord {
session_id: "puzzle-session-test".to_string(),
current_turn: 2,
progress_percent: 60,
stage: "collecting_anchors".to_string(),
anchor_pack: spacetime_client::PuzzleAnchorPackRecord {
theme_promise: spacetime_client::PuzzleAnchorItemRecord {
key: "themePromise".to_string(),
label: "题材承诺".to_string(),
value: "雨夜猫咪遗迹".to_string(),
status: "confirmed".to_string(),
},
visual_subject: spacetime_client::PuzzleAnchorItemRecord {
key: "visualSubject".to_string(),
label: "画面主体".to_string(),
value: String::new(),
status: "missing".to_string(),
},
visual_mood: spacetime_client::PuzzleAnchorItemRecord {
key: "visualMood".to_string(),
label: "视觉气质".to_string(),
value: String::new(),
status: "missing".to_string(),
},
composition_hooks: spacetime_client::PuzzleAnchorItemRecord {
key: "compositionHooks".to_string(),
label: "拼图记忆点".to_string(),
value: String::new(),
status: "missing".to_string(),
},
tags_and_forbidden: spacetime_client::PuzzleAnchorItemRecord {
key: "tagsAndForbidden".to_string(),
label: "标签与禁忌".to_string(),
value: String::new(),
status: "missing".to_string(),
},
},
draft: None,
messages: Vec::new(),
last_assistant_reply: None,
published_profile_id: None,
suggested_actions: Vec::new(),
result_preview: None,
updated_at: "2026-04-24T10:00:00.000Z".to_string(),
}
}
#[test]
fn extract_reply_text_from_partial_json_preserves_chinese_characters() {
let partial_json = r#"{"replyText":"夜雨猫咪遗迹","progressPercent":42"#;
@@ -498,13 +317,4 @@ mod tests {
"雨夜、猫咪、神庙遗迹;禁止文字水印"
);
}
#[test]
fn quick_fill_prompt_forbids_follow_up_questions() {
let prompt = build_puzzle_agent_prompt(&empty_session_record(), true);
assert!(prompt.contains("用户刚刚主动要求你自动补充剩余关键字"));
assert!(prompt.contains("不要再继续提问"));
assert!(prompt.contains("progressPercent 直接输出为 100"));
}
}

View File

@@ -22,6 +22,7 @@ use module_runtime_story_compat::{
use crate::{
auth::AuthenticatedAccessToken,
http_error::AppError,
llm_model_routing::RPG_STORY_LLM_MODEL,
prompt::runtime_chat::{
NPC_CHAT_TURN_REPLY_SYSTEM_PROMPT, NPC_CHAT_TURN_SUGGESTION_SYSTEM_PROMPT,
NpcChatTurnPromptInput, build_deterministic_chat_suggestions,
@@ -227,6 +228,7 @@ where
]);
reply_request.max_tokens = Some(700);
reply_request.enable_web_search = state.config.rpg_llm_web_search_enabled;
reply_request.model = Some(RPG_STORY_LLM_MODEL.to_string());
let reply_response = llm_client
.stream_text(reply_request, |delta| {
@@ -254,6 +256,7 @@ where
]);
suggestion_request.max_tokens = Some(200);
suggestion_request.enable_web_search = state.config.rpg_llm_web_search_enabled;
suggestion_request.model = Some(RPG_STORY_LLM_MODEL.to_string());
let suggestion_text = llm_client
.request_text(suggestion_request)
.await

View File

@@ -15,7 +15,8 @@ use std::convert::Infallible;
use crate::{
api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError,
prompt::runtime_chat::*, request_context::RequestContext, state::AppState,
llm_model_routing::RPG_STORY_LLM_MODEL, prompt::runtime_chat::*,
request_context::RequestContext, state::AppState,
};
use module_runtime_story_compat::{
RuntimeStoryPromptContextExtras, build_runtime_story_prompt_context, current_world_type,
@@ -587,6 +588,7 @@ async fn request_runtime_plain_text(
]);
request.max_tokens = Some(400);
request.enable_web_search = state.config.rpg_llm_web_search_enabled;
request.model = Some(RPG_STORY_LLM_MODEL.to_string());
llm_client
.request_text(request)
@@ -618,6 +620,7 @@ fn stream_plain_text_response<'a>(
]);
request.max_tokens = Some(700);
request.enable_web_search = enable_web_search;
request.model = Some(RPG_STORY_LLM_MODEL.to_string());
let response = llm_client
.stream_text(request, |_| {})

View File

@@ -1,4 +1,5 @@
use super::*;
use crate::llm_model_routing::RPG_STORY_LLM_MODEL;
use crate::prompt::runtime_chat::{
RuntimeNpcDialoguePromptParams, RuntimeReasonedStoryPromptParams, RuntimeStoryTextPromptParams,
build_runtime_npc_dialogue_user_prompt, build_runtime_reasoned_story_user_prompt,
@@ -100,6 +101,7 @@ pub(super) async fn generate_action_story_payload(
fn apply_rpg_web_search(state: &AppState, request: &mut LlmTextRequest) {
request.enable_web_search = state.config.rpg_llm_web_search_enabled;
request.model = Some(RPG_STORY_LLM_MODEL.to_string());
}
pub(super) async fn generate_npc_dialogue_payload(
@@ -144,6 +146,7 @@ pub(super) async fn generate_npc_dialogue_payload(
]);
llm_request.max_tokens = Some(700);
llm_request.enable_web_search = enable_web_search;
llm_request.model = Some(RPG_STORY_LLM_MODEL.to_string());
let dialogue_text = llm_client
.request_text(llm_request)
@@ -195,6 +198,7 @@ pub(super) async fn generate_reasoned_story_payload(
]);
llm_request.max_tokens = Some(700);
llm_request.enable_web_search = enable_web_search;
llm_request.model = Some(RPG_STORY_LLM_MODEL.to_string());
let story_text = llm_client
.request_text(llm_request)

View File

@@ -271,11 +271,18 @@ pub(super) fn build_active_npc_runtime_story_options(
game_state: &Value,
npc_id: &str,
) -> Vec<RuntimeStoryOptionView> {
if read_current_npc_affinity(game_state) < 0 {
return vec![build_npc_runtime_story_option(
"npc_chat",
"继续交谈",
npc_id,
"chat",
)];
}
let mut options = vec![
build_npc_runtime_story_option("npc_chat", "继续交谈", npc_id, "chat"),
build_npc_help_runtime_story_option(game_state, npc_id),
build_npc_runtime_story_option("npc_spar", "点到为止切磋", npc_id, "spar"),
build_npc_runtime_story_option("npc_fight", "与对方战斗", npc_id, "fight"),
];
if current_npc_inventory_items(game_state)
@@ -332,12 +339,6 @@ pub(super) fn build_active_npc_runtime_story_options(
));
}
options.push(build_npc_runtime_story_option(
"npc_leave",
"离开当前角色",
npc_id,
"leave",
));
options
}

View File

@@ -1112,12 +1112,9 @@ fn runtime_story_state_compiler_builds_active_npc_options_with_trade_gift_and_he
vec![
"npc_chat",
"npc_help",
"npc_spar",
"npc_fight",
"npc_trade",
"npc_gift",
"npc_quest_accept",
"npc_leave"
"npc_quest_accept"
]
);
assert_eq!(
@@ -1129,11 +1126,11 @@ fn runtime_story_state_compiler_builds_active_npc_options_with_trade_gift_and_he
Some("当前 NPC 的一次性援手已经用完了。")
);
assert!(matches!(
response.view_model.available_options[4].interaction,
response.view_model.available_options[2].interaction,
Some(RuntimeStoryOptionInteraction::Npc { ref action, .. }) if action == "trade"
));
assert!(matches!(
response.view_model.available_options[5].interaction,
response.view_model.available_options[3].interaction,
Some(RuntimeStoryOptionInteraction::Npc { ref action, .. }) if action == "gift"
));
let npc_interaction = response
@@ -1154,6 +1151,35 @@ fn runtime_story_state_compiler_builds_active_npc_options_with_trade_gift_and_he
);
}
#[test]
fn runtime_story_state_compiler_limits_negative_affinity_active_npc_to_chat() {
let mut game_state = build_runtime_story_boundary_game_state_fixture();
write_bool_field(&mut game_state, "npcInteractionActive", true);
write_current_npc_state_i32_field(&mut game_state, "affinity", -8);
let response = build_runtime_story_state_response(
"runtime-main",
Some(0),
RuntimeStorySnapshotPayload {
saved_at: None,
bottom_tab: "adventure".to_string(),
game_state,
current_story: None,
},
);
let function_ids = response
.view_model
.available_options
.iter()
.map(|option| option.function_id.as_str())
.collect::<Vec<_>>();
assert_eq!(function_ids, vec!["npc_chat"]);
assert!(matches!(
response.view_model.available_options[0].interaction,
Some(RuntimeStoryOptionInteraction::Npc { ref action, .. }) if action == "chat"
));
}
#[test]
fn runtime_story_equipment_equip_updates_loadout_and_build_toast() {
let request = RuntimeStoryActionRequest {

View File

@@ -0,0 +1,54 @@
use module_auth::AuthUser;
use crate::state::AppState;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WorkAuthorSummary {
pub display_name: String,
pub public_user_code: Option<String>,
}
/// 中文注释:作品作者的真相源是 owner_user_id历史昵称字段只作为账号资料不可读时的兼容回退。
pub fn resolve_work_author_by_user_id(
state: &AppState,
owner_user_id: &str,
fallback_display_name: Option<&str>,
fallback_public_user_code: Option<&str>,
) -> WorkAuthorSummary {
let fallback_display_name =
normalize_optional_text(fallback_display_name).unwrap_or_else(|| "玩家".to_string());
let fallback_public_user_code = normalize_optional_text(fallback_public_user_code);
let Some(owner_user_id) = normalize_optional_text(Some(owner_user_id)) else {
return WorkAuthorSummary {
display_name: fallback_display_name,
public_user_code: fallback_public_user_code,
};
};
match state.auth_user_service().get_user_by_id(&owner_user_id) {
Ok(Some(user)) => map_auth_user_to_work_author_summary(user, fallback_display_name),
Ok(None) | Err(_) => WorkAuthorSummary {
display_name: fallback_display_name,
public_user_code: fallback_public_user_code,
},
}
}
fn map_auth_user_to_work_author_summary(
user: AuthUser,
fallback_display_name: String,
) -> WorkAuthorSummary {
WorkAuthorSummary {
display_name: normalize_optional_text(Some(user.display_name.as_str()))
.unwrap_or(fallback_display_name),
public_user_code: normalize_optional_text(Some(user.public_user_code.as_str())),
}
}
fn normalize_optional_text(value: Option<&str>) -> Option<String> {
value
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}

View File

@@ -347,6 +347,14 @@ pub struct BigFishPlayRecordInput {
pub played_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BigFishWorkLikeRecordInput {
pub session_id: String,
pub user_id: String,
pub liked_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BigFishFieldError {
MissingSessionId,

View File

@@ -510,6 +510,15 @@ pub struct CustomWorldProfilePlayRecordInput {
pub played_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CustomWorldProfileLikeRecordInput {
pub owner_user_id: String,
pub profile_id: String,
pub user_id: String,
pub liked_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CustomWorldAgentSessionCreateInput {

View File

@@ -113,9 +113,26 @@ pub struct PuzzleGeneratedImageCandidate {
pub selected: bool,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PuzzleDraftLevel {
pub level_id: String,
pub level_name: String,
pub picture_description: String,
pub candidates: Vec<PuzzleGeneratedImageCandidate>,
pub selected_candidate_id: Option<String>,
pub cover_image_src: Option<String>,
pub cover_asset_id: Option<String>,
pub generation_status: String,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PuzzleResultDraft {
#[serde(default)]
pub work_title: String,
#[serde(default)]
pub work_description: String,
pub level_name: String,
pub summary: String,
pub theme_tags: Vec<String>,
@@ -127,6 +144,18 @@ pub struct PuzzleResultDraft {
pub cover_image_src: Option<String>,
pub cover_asset_id: Option<String>,
pub generation_status: String,
#[serde(default)]
pub levels: Vec<PuzzleDraftLevel>,
#[serde(default)]
pub form_draft: Option<PuzzleFormDraft>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PuzzleFormDraft {
pub work_title: Option<String>,
pub work_description: Option<String>,
pub picture_description: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
@@ -202,11 +231,17 @@ pub struct PuzzleWorkProfile {
pub owner_user_id: String,
pub source_session_id: Option<String>,
pub author_display_name: String,
#[serde(default)]
pub work_title: String,
#[serde(default)]
pub work_description: String,
pub level_name: String,
pub summary: String,
pub theme_tags: Vec<String>,
pub cover_image_src: Option<String>,
pub cover_asset_id: Option<String>,
#[serde(default)]
pub levels: Vec<PuzzleDraftLevel>,
pub publication_status: PuzzlePublicationStatus,
pub updated_at_micros: i64,
pub published_at_micros: Option<i64>,
@@ -334,6 +369,15 @@ pub struct PuzzleAgentSessionCreateInput {
pub created_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PuzzleFormDraftSaveInput {
pub session_id: String,
pub owner_user_id: String,
pub seed_text: String,
pub saved_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PuzzleAgentSessionGetInput {
@@ -378,6 +422,7 @@ pub struct PuzzleDraftCompileInput {
pub struct PuzzleGeneratedImagesSaveInput {
pub session_id: String,
pub owner_user_id: String,
pub level_id: Option<String>,
pub candidates_json: String,
pub saved_at_micros: i64,
}
@@ -387,6 +432,7 @@ pub struct PuzzleGeneratedImagesSaveInput {
pub struct PuzzleSelectCoverImageInput {
pub session_id: String,
pub owner_user_id: String,
pub level_id: Option<String>,
pub candidate_id: String,
pub selected_at_micros: i64,
}
@@ -399,9 +445,12 @@ pub struct PuzzlePublishInput {
pub work_id: String,
pub profile_id: String,
pub author_display_name: String,
pub work_title: Option<String>,
pub work_description: Option<String>,
pub level_name: Option<String>,
pub summary: Option<String>,
pub theme_tags: Option<Vec<String>>,
pub levels_json: Option<String>,
pub published_at_micros: i64,
}
@@ -429,11 +478,14 @@ pub struct PuzzleWorkDeleteInput {
pub struct PuzzleWorkUpsertInput {
pub profile_id: String,
pub owner_user_id: String,
pub work_title: String,
pub work_description: String,
pub level_name: String,
pub summary: String,
pub theme_tags: Vec<String>,
pub cover_image_src: Option<String>,
pub cover_asset_id: Option<String>,
pub levels_json: Option<String>,
pub updated_at_micros: i64,
}
@@ -450,12 +502,21 @@ pub struct PuzzleWorkRemixInput {
pub remixed_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PuzzleWorkLikeRecordInput {
pub profile_id: String,
pub user_id: String,
pub liked_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PuzzleRunStartInput {
pub run_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub level_id: Option<String>,
pub started_at_micros: i64,
}
@@ -690,10 +751,17 @@ pub fn empty_anchor_pack() -> PuzzleAnchorPack {
pub fn infer_anchor_pack(seed_text: &str, latest_message: Option<&str>) -> PuzzleAnchorPack {
let source = normalize_required_string(latest_message.unwrap_or(seed_text))
.or_else(|| normalize_required_string(seed_text))
.unwrap_or_else(|| "童话森林里的发光猫咪遗迹".to_string());
if let Some((title, picture_description)) = parse_form_seed_text(&source) {
return build_form_anchor_pack(title.as_str(), picture_description.as_str());
.or_else(|| normalize_required_string(seed_text));
let Some(source) = source else {
return empty_anchor_pack();
};
if let Some(form_seed) = parse_form_seed_text(&source) {
if form_seed.has_any_value() {
return build_form_anchor_pack(
form_seed.work_title.as_deref().unwrap_or(""),
form_seed.picture_description.as_deref().unwrap_or(""),
);
}
}
let mut pack = empty_anchor_pack();
@@ -711,22 +779,26 @@ pub fn infer_anchor_pack(seed_text: &str, latest_message: Option<&str>) -> Puzzl
}
pub fn build_form_anchor_pack(title: &str, picture_description: &str) -> PuzzleAnchorPack {
let normalized_title =
normalize_required_string(title).unwrap_or_else(|| "奇景拼图".to_string());
let normalized_description =
normalize_required_string(picture_description).unwrap_or_else(|| normalized_title.clone());
let normalized_title = normalize_required_string(title);
let normalized_description = normalize_required_string(picture_description);
let mut pack = empty_anchor_pack();
pack.theme_promise.value = normalized_title.clone();
pack.theme_promise.status = PuzzleAnchorStatus::Locked;
pack.visual_subject.value = normalized_description.clone();
pack.visual_subject.status = PuzzleAnchorStatus::Locked;
if let Some(title) = normalized_title.as_ref() {
pack.theme_promise.value = title.clone();
pack.theme_promise.status = PuzzleAnchorStatus::Locked;
}
if let Some(description) = normalized_description.as_ref() {
pack.visual_subject.value = description.clone();
pack.visual_subject.status = PuzzleAnchorStatus::Locked;
}
pack.visual_mood.value = "清晰、适合拼图切块".to_string();
pack.visual_mood.status = PuzzleAnchorStatus::Inferred;
pack.composition_hooks.value = "主体轮廓、色块分区、局部细节".to_string();
pack.composition_hooks.status = PuzzleAnchorStatus::Inferred;
pack.tags_and_forbidden.value =
build_form_tags_and_forbidden(normalized_title.as_str(), normalized_description.as_str());
pack.tags_and_forbidden.value = build_form_tags_and_forbidden(
normalized_title.as_deref().unwrap_or(""),
normalized_description.as_deref().unwrap_or(""),
);
pack.tags_and_forbidden.status = PuzzleAnchorStatus::Inferred;
pack
@@ -766,13 +838,37 @@ pub fn build_creator_intent(
pub fn compile_result_draft(
anchor_pack: &PuzzleAnchorPack,
messages: &[PuzzleAgentMessageSnapshot],
) -> PuzzleResultDraft {
compile_result_draft_from_seed(anchor_pack, messages, None)
}
pub fn compile_result_draft_from_seed(
anchor_pack: &PuzzleAnchorPack,
messages: &[PuzzleAgentMessageSnapshot],
seed_text: Option<&str>,
) -> PuzzleResultDraft {
let creator_intent = build_creator_intent(anchor_pack, messages);
let normalized_tags = normalize_theme_tags(creator_intent.theme_tags.clone());
let level_name = build_level_name(anchor_pack, &normalized_tags);
let work_title = build_work_title(anchor_pack);
let work_description = resolve_work_description(seed_text, anchor_pack);
let picture_description = fallback_text(&anchor_pack.visual_subject.value, "画面主体");
let level_name =
build_level_name_from_picture(picture_description.as_str(), &normalized_tags, 1);
let level = PuzzleDraftLevel {
level_id: "puzzle-level-1".to_string(),
level_name: level_name.clone(),
picture_description,
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: None,
cover_asset_id: None,
generation_status: "idle".to_string(),
};
PuzzleResultDraft {
work_title,
work_description: work_description.clone(),
level_name,
summary: build_result_summary(anchor_pack),
summary: work_description,
theme_tags: normalized_tags,
forbidden_directives: creator_intent.forbidden_directives.clone(),
creator_intent: Some(creator_intent),
@@ -782,6 +878,79 @@ pub fn compile_result_draft(
cover_image_src: None,
cover_asset_id: None,
generation_status: "idle".to_string(),
levels: vec![level],
form_draft: None,
}
}
pub fn build_form_draft_from_seed(
anchor_pack: &PuzzleAnchorPack,
seed_text: Option<&str>,
) -> PuzzleResultDraft {
let form_seed = seed_text.and_then(parse_form_seed_text);
build_form_draft_from_parts(
anchor_pack,
form_seed.as_ref().and_then(|seed| seed.work_title.clone()),
form_seed
.as_ref()
.and_then(|seed| seed.work_description.clone()),
form_seed.and_then(|seed| seed.picture_description),
)
}
pub fn build_form_draft_from_parts(
anchor_pack: &PuzzleAnchorPack,
work_title: Option<String>,
work_description: Option<String>,
picture_description: Option<String>,
) -> PuzzleResultDraft {
let work_title = work_title.and_then(|value| normalize_required_string(&value));
let work_description = work_description.and_then(|value| normalize_required_string(&value));
let picture_description = picture_description.and_then(|value| normalize_required_string(&value));
let title_for_tags = work_title.as_deref().unwrap_or("");
let picture_for_tags = picture_description.as_deref().unwrap_or("");
let mut tags = normalize_theme_tags(derive_form_theme_tags(title_for_tags, picture_for_tags));
if tags.is_empty() {
tags = vec!["拼图".to_string(), "插画".to_string(), "清晰构图".to_string()];
}
let level_name = picture_description
.as_deref()
.map(|value| build_level_name_from_picture(value, &tags, 1))
.or_else(|| work_title.clone())
.unwrap_or_else(|| "未命名拼图".to_string());
let summary = work_description.clone().unwrap_or_default();
let level = PuzzleDraftLevel {
level_id: "puzzle-level-1".to_string(),
level_name: level_name.clone(),
picture_description: picture_description.clone().unwrap_or_default(),
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: None,
cover_asset_id: None,
generation_status: "idle".to_string(),
};
// 中文注释:这是生成前的表单草稿,只用于创作中心恢复和表单回填,不进入发布就绪判断。
PuzzleResultDraft {
work_title: work_title.clone().unwrap_or_default(),
work_description: summary.clone(),
level_name,
summary,
theme_tags: tags,
forbidden_directives: Vec::new(),
creator_intent: None,
anchor_pack: anchor_pack.clone(),
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: None,
cover_asset_id: None,
generation_status: "idle".to_string(),
levels: vec![level],
form_draft: Some(PuzzleFormDraft {
work_title,
work_description,
picture_description,
}),
}
}
@@ -849,13 +1018,155 @@ pub fn apply_selected_candidate(
Ok(draft)
}
pub fn normalize_puzzle_draft(mut draft: PuzzleResultDraft) -> PuzzleResultDraft {
if draft.work_title.trim().is_empty() {
draft.work_title = fallback_text(&draft.anchor_pack.theme_promise.value, &draft.level_name);
}
if draft.work_description.trim().is_empty() {
draft.work_description = draft.summary.clone();
}
if draft.levels.is_empty() {
draft.levels = vec![PuzzleDraftLevel {
level_id: "puzzle-level-1".to_string(),
level_name: draft.level_name.clone(),
picture_description: fallback_text(
&draft.anchor_pack.visual_subject.value,
&draft.summary,
),
candidates: draft.candidates.clone(),
selected_candidate_id: draft.selected_candidate_id.clone(),
cover_image_src: draft.cover_image_src.clone(),
cover_asset_id: draft.cover_asset_id.clone(),
generation_status: draft.generation_status.clone(),
}];
}
sync_primary_level_fields(&mut draft);
draft
}
pub fn sync_primary_level_fields(draft: &mut PuzzleResultDraft) {
if let Some(primary_level) = draft.levels.first() {
draft.level_name = primary_level.level_name.clone();
draft.candidates = primary_level.candidates.clone();
draft.selected_candidate_id = primary_level.selected_candidate_id.clone();
draft.cover_image_src = primary_level.cover_image_src.clone();
draft.cover_asset_id = primary_level.cover_asset_id.clone();
draft.generation_status = primary_level.generation_status.clone();
}
if draft.work_description.trim().is_empty() {
draft.work_description = draft.summary.clone();
}
draft.summary = draft.work_description.clone();
if draft.form_draft.is_some() {
draft.form_draft = Some(PuzzleFormDraft {
work_title: normalize_required_string(&draft.work_title),
work_description: normalize_required_string(&draft.work_description),
picture_description: draft
.levels
.first()
.and_then(|level| normalize_required_string(&level.picture_description)),
});
}
}
pub fn selected_puzzle_level(
draft: &PuzzleResultDraft,
level_id: Option<&str>,
) -> Option<PuzzleDraftLevel> {
let normalized = normalize_puzzle_draft(draft.clone());
let requested_level_id = level_id.and_then(normalize_required_string);
requested_level_id
.as_deref()
.and_then(|target_id| {
normalized
.levels
.iter()
.find(|level| level.level_id == target_id)
.cloned()
})
.or_else(|| normalized.levels.first().cloned())
}
pub fn replace_puzzle_level(
draft: &PuzzleResultDraft,
level: PuzzleDraftLevel,
) -> Result<PuzzleResultDraft, PuzzleFieldError> {
let mut next_draft = normalize_puzzle_draft(draft.clone());
let Some(index) = next_draft
.levels
.iter()
.position(|entry| entry.level_id == level.level_id)
else {
return Err(PuzzleFieldError::InvalidOperation);
};
next_draft.levels[index] = level;
sync_primary_level_fields(&mut next_draft);
Ok(next_draft)
}
pub fn append_blank_puzzle_level(draft: &PuzzleResultDraft) -> PuzzleResultDraft {
let mut next_draft = normalize_puzzle_draft(draft.clone());
let next_index = next_draft.levels.len() + 1;
let picture_description = next_draft
.levels
.first()
.map(|level| level.picture_description.clone())
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| fallback_text(&next_draft.anchor_pack.visual_subject.value, "画面主体"));
next_draft.levels.push(PuzzleDraftLevel {
level_id: format!("puzzle-level-{next_index}"),
level_name: build_level_name_from_picture(
picture_description.as_str(),
&next_draft.theme_tags,
next_index,
),
picture_description,
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: None,
cover_asset_id: None,
generation_status: "idle".to_string(),
});
sync_primary_level_fields(&mut next_draft);
next_draft
}
pub fn remove_puzzle_level(
draft: &PuzzleResultDraft,
level_id: &str,
) -> Result<PuzzleResultDraft, PuzzleFieldError> {
let mut next_draft = normalize_puzzle_draft(draft.clone());
if next_draft.levels.len() <= 1 {
return Err(PuzzleFieldError::InvalidOperation);
}
let normalized_level_id =
normalize_required_string(level_id).ok_or(PuzzleFieldError::InvalidOperation)?;
next_draft
.levels
.retain(|level| level.level_id != normalized_level_id);
if next_draft.levels.is_empty() {
return Err(PuzzleFieldError::InvalidOperation);
}
sync_primary_level_fields(&mut next_draft);
Ok(next_draft)
}
pub fn build_result_preview(
draft: &PuzzleResultDraft,
author_display_name: Option<&str>,
) -> PuzzleResultPreviewEnvelope {
let blockers = validate_publish_requirements(draft, author_display_name);
let normalized_draft = normalize_puzzle_draft(draft.clone());
if normalized_draft.form_draft.is_some() {
return PuzzleResultPreviewEnvelope {
draft: normalized_draft,
blockers: Vec::new(),
quality_findings: Vec::new(),
publish_ready: false,
};
}
let blockers = validate_publish_requirements(&normalized_draft, author_display_name);
PuzzleResultPreviewEnvelope {
draft: draft.clone(),
draft: normalized_draft,
blockers,
quality_findings: Vec::new(),
publish_ready: validate_publish_requirements(draft, author_display_name).is_empty(),
@@ -866,27 +1177,44 @@ pub fn validate_publish_requirements(
draft: &PuzzleResultDraft,
author_display_name: Option<&str>,
) -> Vec<PuzzleResultPreviewBlocker> {
let draft = normalize_puzzle_draft(draft.clone());
let mut blockers = Vec::new();
if normalize_required_string(&draft.level_name).is_none() {
if normalize_required_string(&draft.work_title).is_none() {
blockers.push(PuzzleResultPreviewBlocker {
id: "missing-level-name".to_string(),
code: "MISSING_LEVEL_NAME".to_string(),
message: "关卡名不能为空".to_string(),
id: "missing-work-title".to_string(),
code: "MISSING_WORK_TITLE".to_string(),
message: "作品名称不能为空".to_string(),
});
}
if draft
.cover_image_src
.as_deref()
.map(str::trim)
.unwrap_or("")
.is_empty()
{
if normalize_required_string(&draft.work_description).is_none() {
blockers.push(PuzzleResultPreviewBlocker {
id: "missing-cover-image".to_string(),
code: "MISSING_COVER_IMAGE".to_string(),
message: "正式拼图图片尚未确定".to_string(),
id: "missing-work-description".to_string(),
code: "MISSING_WORK_DESCRIPTION".to_string(),
message: "作品描述不能为空".to_string(),
});
}
for level in &draft.levels {
if normalize_required_string(&level.level_name).is_none() {
blockers.push(PuzzleResultPreviewBlocker {
id: format!("missing-level-name-{}", level.level_id),
code: "MISSING_LEVEL_NAME".to_string(),
message: "关卡名不能为空".to_string(),
});
}
if level
.cover_image_src
.as_deref()
.map(str::trim)
.unwrap_or("")
.is_empty()
{
blockers.push(PuzzleResultPreviewBlocker {
id: format!("missing-cover-image-{}", level.level_id),
code: "MISSING_COVER_IMAGE".to_string(),
message: "正式拼图图片尚未确定".to_string(),
});
}
}
if draft.theme_tags.len() < PUZZLE_MIN_TAG_COUNT
|| draft.theme_tags.len() > PUZZLE_MAX_TAG_COUNT
{
@@ -917,18 +1245,22 @@ pub fn create_work_profile(
) -> Result<PuzzleWorkProfile, PuzzleFieldError> {
let author_display_name = normalize_required_string(author_display_name)
.ok_or(PuzzleFieldError::MissingAuthorDisplayName)?;
let preview = build_result_preview(draft, Some(&author_display_name));
let draft = normalize_puzzle_draft(draft.clone());
let preview = build_result_preview(&draft, Some(&author_display_name));
Ok(PuzzleWorkProfile {
work_id,
profile_id,
owner_user_id,
source_session_id,
author_display_name,
work_title: draft.work_title.clone(),
work_description: draft.work_description.clone(),
level_name: draft.level_name.clone(),
summary: draft.summary.clone(),
theme_tags: normalize_theme_tags(draft.theme_tags.clone()),
cover_image_src: draft.cover_image_src.clone(),
cover_asset_id: draft.cover_asset_id.clone(),
levels: draft.levels.clone(),
publication_status: PuzzlePublicationStatus::Draft,
updated_at_micros,
published_at_micros: None,
@@ -946,14 +1278,18 @@ pub fn publish_work_profile(
draft: &PuzzleResultDraft,
published_at_micros: i64,
) -> Result<PuzzleWorkProfile, PuzzleFieldError> {
if !validate_publish_requirements(draft, Some(&profile.author_display_name)).is_empty() {
let draft = normalize_puzzle_draft(draft.clone());
if !validate_publish_requirements(&draft, Some(&profile.author_display_name)).is_empty() {
return Err(PuzzleFieldError::InvalidOperation);
}
profile.work_title = draft.work_title.clone();
profile.work_description = draft.work_description.clone();
profile.level_name = draft.level_name.clone();
profile.summary = draft.summary.clone();
profile.theme_tags = normalize_theme_tags(draft.theme_tags.clone());
profile.cover_image_src = draft.cover_image_src.clone();
profile.cover_asset_id = draft.cover_asset_id.clone();
profile.levels = draft.levels.clone();
profile.publication_status = PuzzlePublicationStatus::Published;
profile.publish_ready = true;
profile.updated_at_micros = published_at_micros;
@@ -965,22 +1301,39 @@ pub fn publish_work_profile(
/// 这里只允许覆盖 PRD 明确要求的关卡名、摘要与标签,不额外扩到更多结果页元数据。
pub fn apply_publish_overrides_to_draft(
draft: &PuzzleResultDraft,
work_title: Option<String>,
work_description: Option<String>,
level_name: Option<String>,
summary: Option<String>,
theme_tags: Option<Vec<String>>,
levels: Option<Vec<PuzzleDraftLevel>>,
) -> Result<PuzzleResultDraft, PuzzleFieldError> {
let mut next_draft = draft.clone();
let mut next_draft = normalize_puzzle_draft(draft.clone());
if let Some(next_work_title) = work_title
&& let Some(normalized_work_title) = normalize_required_string(&next_work_title)
{
next_draft.work_title = normalized_work_title;
}
if let Some(next_work_description) = work_description
&& let Some(normalized_work_description) = normalize_required_string(&next_work_description)
{
next_draft.work_description = normalized_work_description;
}
if let Some(next_level_name) = level_name
&& let Some(normalized_level_name) = normalize_required_string(&next_level_name)
{
next_draft.level_name = normalized_level_name;
if let Some(primary_level) = next_draft.levels.first_mut() {
primary_level.level_name = normalized_level_name;
}
}
if let Some(next_summary) = summary
&& let Some(normalized_summary) = normalize_required_string(&next_summary)
{
next_draft.summary = normalized_summary;
next_draft.work_description = normalized_summary;
}
if let Some(next_theme_tags) = theme_tags {
@@ -993,9 +1346,41 @@ pub fn apply_publish_overrides_to_draft(
next_draft.theme_tags = normalized_theme_tags;
}
if let Some(next_levels) = levels {
let normalized_levels = normalize_puzzle_levels(next_levels, &next_draft.theme_tags)?;
next_draft.levels = normalized_levels;
}
sync_primary_level_fields(&mut next_draft);
Ok(next_draft)
}
pub fn normalize_puzzle_levels(
levels: Vec<PuzzleDraftLevel>,
theme_tags: &[String],
) -> Result<Vec<PuzzleDraftLevel>, PuzzleFieldError> {
let mut normalized_levels = Vec::new();
for (index, mut level) in levels.into_iter().enumerate() {
let level_id = normalize_required_string(&level.level_id)
.unwrap_or_else(|| format!("puzzle-level-{}", index + 1));
let picture_description = normalize_required_string(&level.picture_description)
.unwrap_or_else(|| format!("{}关画面", index + 1));
let level_name = normalize_required_string(&level.level_name).unwrap_or_else(|| {
build_level_name_from_picture(picture_description.as_str(), theme_tags, index + 1)
});
level.level_id = level_id;
level.level_name = level_name;
level.picture_description = picture_description;
level.generation_status = normalize_required_string(&level.generation_status)
.unwrap_or_else(|| "idle".to_string());
normalized_levels.push(level);
}
if normalized_levels.is_empty() {
return Err(PuzzleFieldError::InvalidOperation);
}
Ok(normalized_levels)
}
pub fn resolve_puzzle_grid_size(cleared_level_count: u32) -> u32 {
if cleared_level_count >= 3 { 4 } else { 3 }
}
@@ -1683,25 +2068,54 @@ fn infer_tags_and_forbidden(source: &str) -> String {
}
}
fn parse_form_seed_text(source: &str) -> Option<(String, String)> {
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct PuzzleFormSeedParts {
work_title: Option<String>,
work_description: Option<String>,
picture_description: Option<String>,
}
impl PuzzleFormSeedParts {
fn has_any_value(&self) -> bool {
self.work_title.is_some()
|| self.work_description.is_some()
|| self.picture_description.is_some()
}
}
fn parse_form_seed_text(source: &str) -> Option<PuzzleFormSeedParts> {
let normalized_source = source.trim();
let title_marker = "拼图标题:";
let description_marker = "画面描述:";
let title_start = normalized_source.find(title_marker)? + title_marker.len();
let description_start = normalized_source.find(description_marker)?;
if description_start <= title_start {
if normalized_source.is_empty() {
return None;
}
let title = normalize_required_string(&normalized_source[title_start..description_start]);
let picture_description = normalize_required_string(
&normalized_source[description_start + description_marker.len()..],
);
let title_marker = if normalized_source.contains("作品名称:") {
"作品名称:"
} else {
"拼图标题:"
};
let parts = PuzzleFormSeedParts {
work_title: extract_form_seed_value(normalized_source, title_marker),
work_description: extract_form_seed_value(normalized_source, "作品描述:"),
picture_description: extract_form_seed_value(normalized_source, "画面描述:"),
};
match (title, picture_description) {
(Some(title), Some(picture_description)) => Some((title, picture_description)),
_ => None,
}
parts.has_any_value().then_some(parts)
}
fn extract_form_seed_value(source: &str, marker: &str) -> Option<String> {
let value_start = source.find(marker)? + marker.len();
let value_end = ["作品名称:", "拼图标题:", "作品描述:", "画面描述:"]
.into_iter()
.filter(|next_marker| *next_marker != marker)
.filter_map(|next_marker| {
source[value_start..]
.find(next_marker)
.map(|index| value_start + index)
})
.min()
.unwrap_or(source.len());
normalize_required_string(&source[value_start..value_end])
}
fn build_form_tags_and_forbidden(title: &str, picture_description: &str) -> String {
@@ -1777,6 +2191,22 @@ fn build_result_summary(anchor_pack: &PuzzleAnchorPack) -> String {
)
}
fn resolve_work_description(seed_text: Option<&str>, anchor_pack: &PuzzleAnchorPack) -> String {
seed_text
.and_then(parse_form_seed_text)
.and_then(|parts| {
parts
.work_description
.or(parts.picture_description)
.or(parts.work_title)
})
.unwrap_or_else(|| build_result_summary(anchor_pack))
}
fn build_work_title(anchor_pack: &PuzzleAnchorPack) -> String {
fallback_text(&anchor_pack.theme_promise.value, "奇景拼图")
}
fn extract_forbidden_directive(source: &str) -> String {
if let Some((_, tail)) = source.split_once('') {
return normalize_required_string(tail).unwrap_or_else(|| "禁止标题字".to_string());
@@ -1784,20 +2214,24 @@ fn extract_forbidden_directive(source: &str) -> String {
"禁止标题字".to_string()
}
fn build_level_name(anchor_pack: &PuzzleAnchorPack, normalized_tags: &[String]) -> String {
if is_form_anchor_pack(anchor_pack)
&& let Some(title) = normalize_required_string(&anchor_pack.theme_promise.value)
{
return title;
fn build_level_name_from_picture(
picture_description: &str,
normalized_tags: &[String],
level_index: usize,
) -> String {
let source = normalize_required_string(picture_description).unwrap_or_default();
for keyword in [
"", "", "神庙", "遗迹", "森林", "雨夜", "城市", "机械", "", "", "", "", "",
"",
] {
if source.contains(keyword) {
return format!("{keyword}画面");
}
}
if let Some(tag) = normalized_tags.first() {
return format!("{tag}拼图");
return format!("{tag}{level_index}");
}
if let Some(subject) = normalize_required_string(&anchor_pack.visual_subject.value) {
return subject.chars().take(8).collect::<String>();
}
"奇景拼图".to_string()
format!("{level_index}")
}
fn fallback_text(value: &str, fallback: &str) -> String {
@@ -2432,15 +2866,28 @@ mod tests {
owner_user_id: owner_user_id.to_string(),
source_session_id: None,
author_display_name: "作者".to_string(),
work_title: format!("{profile_id} 作品"),
work_description: "summary".to_string(),
level_name: format!("{profile_id} 关"),
summary: "summary".to_string(),
theme_tags: tags.into_iter().map(|value| value.to_string()).collect(),
cover_image_src: Some("/cover.png".to_string()),
cover_asset_id: Some("asset-1".to_string()),
levels: vec![PuzzleDraftLevel {
level_id: "puzzle-level-1".to_string(),
level_name: format!("{profile_id} 关"),
picture_description: "summary".to_string(),
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: Some("/cover.png".to_string()),
cover_asset_id: Some("asset-1".to_string()),
generation_status: "ready".to_string(),
}],
publication_status: PuzzlePublicationStatus::Published,
updated_at_micros: 100,
published_at_micros: Some(100),
play_count: 0,
recent_play_count_7d: 0,
remix_count: 0,
like_count: 0,
publish_ready: true,
@@ -2488,10 +2935,16 @@ mod tests {
#[test]
fn form_seed_locks_title_and_picture_description_as_primary_anchors() {
let anchor_pack = infer_anchor_pack(
"拼图标题:暖灯猫街\n画面描述:一只猫在雨夜灯牌下回头。",
"作品名称:暖灯猫街\n作品描述:一套雨夜猫街主题拼图。\n画面描述:一只猫在雨夜灯牌下回头。",
None,
);
let draft = compile_result_draft(&anchor_pack, &[]);
let draft = compile_result_draft_from_seed(
&anchor_pack,
&[],
Some(
"作品名称:暖灯猫街\n作品描述:一套雨夜猫街主题拼图。\n画面描述:一只猫在雨夜灯牌下回头。",
),
);
assert_eq!(anchor_pack.theme_promise.value, "暖灯猫街");
assert_eq!(anchor_pack.theme_promise.status, PuzzleAnchorStatus::Locked);
@@ -2500,8 +2953,14 @@ mod tests {
anchor_pack.visual_subject.status,
PuzzleAnchorStatus::Locked
);
assert_eq!(draft.level_name, "暖灯猫街");
assert_eq!(draft.summary, "只猫在雨夜灯牌下回头");
assert_eq!(draft.work_title, "暖灯猫街");
assert_eq!(draft.work_description, "套雨夜猫街主题拼图");
assert_eq!(draft.summary, "一套雨夜猫街主题拼图。");
assert_eq!(draft.level_name, "猫画面");
assert_eq!(
draft.levels[0].picture_description,
"一只猫在雨夜灯牌下回头。"
);
assert_eq!(
draft
.creator_intent
@@ -2525,9 +2984,10 @@ mod tests {
"一只猫在雨夜灯牌下回头。\n远处有暖色霓虹和玻璃雨滴。"
);
assert_eq!(
draft.summary,
draft.levels[0].picture_description,
"一只猫在雨夜灯牌下回头。\n远处有暖色霓虹和玻璃雨滴。"
);
assert_eq!(draft.summary, draft.work_description);
assert!(draft.theme_tags.iter().any(|tag| tag == "猫咪"));
assert!(draft.theme_tags.iter().any(|tag| tag == "雨夜"));
}
@@ -2909,6 +3369,8 @@ mod tests {
let updated = apply_publish_overrides_to_draft(
&draft,
Some("雨夜猫塔作品".to_string()),
Some("作品描述。".to_string()),
Some("雨夜猫塔".to_string()),
Some("一张更聚焦猫咪塔楼的夜景拼图。".to_string()),
Some(vec![
@@ -2916,10 +3378,12 @@ mod tests {
"猫咪".to_string(),
"遗迹".to_string(),
]),
None,
)
.expect("publish overrides should succeed");
assert_eq!(updated.level_name, "雨夜猫塔");
assert_eq!(updated.work_title, "雨夜猫塔作品");
assert_eq!(updated.summary, "一张更聚焦猫咪塔楼的夜景拼图。");
assert_eq!(
updated.theme_tags,
@@ -2935,9 +3399,16 @@ mod tests {
fn apply_publish_overrides_rejects_invalid_tag_count() {
let anchor_pack = infer_anchor_pack("蒸汽城市", Some("蒸汽城市"));
let draft = compile_result_draft(&anchor_pack, &[]);
let error =
apply_publish_overrides_to_draft(&draft, None, None, Some(vec!["蒸汽".to_string()]))
.expect_err("invalid tag count should fail");
let error = apply_publish_overrides_to_draft(
&draft,
None,
None,
None,
None,
Some(vec!["蒸汽".to_string()]),
None,
)
.expect_err("invalid tag count should fail");
assert_eq!(error, PuzzleFieldError::InvalidTagCount);
}

View File

@@ -18,6 +18,7 @@ pub const DEFAULT_REQUEST_TIMEOUT_MS: u64 = 30_000;
pub const DEFAULT_MAX_RETRIES: u32 = 1;
pub const DEFAULT_RETRY_BACKOFF_MS: u64 = 500;
pub const CHAT_COMPLETIONS_PATH: &str = "/chat/completions";
pub const RESPONSES_PATH: &str = "/responses";
const DEFAULT_LLM_RAW_LOG_DIR: &str = "logs/llm-raw";
static LLM_RAW_LOG_SEQUENCE: AtomicU64 = AtomicU64::new(1);
@@ -66,6 +67,14 @@ pub struct LlmTextRequest {
pub messages: Vec<LlmMessage>,
pub max_tokens: Option<u32>,
pub enable_web_search: bool,
pub protocol: LlmTextProtocol,
}
// 文本协议必须由业务请求显式选择,避免全局默认模型把不同场景混到同一上游形态。
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LlmTextProtocol {
ChatCompletions,
Responses,
}
// 上层在流式消费时拿到的是“累计文本 + 当前增量”,避免每层重新自己拼接。
@@ -117,9 +126,16 @@ pub struct LlmClient {
}
#[derive(Serialize)]
struct ChatCompletionsRequestBody<'a> {
model: &'a str,
messages: &'a [LlmMessage],
#[serde(untagged)]
enum LlmRequestBody {
ChatCompletions(ChatCompletionsRequestBody),
Responses(ResponsesRequestBody),
}
#[derive(Serialize)]
struct ChatCompletionsRequestBody {
model: String,
messages: Vec<LlmMessage>,
stream: bool,
#[serde(skip_serializing_if = "Option::is_none")]
max_tokens: Option<u32>,
@@ -130,10 +146,42 @@ struct ChatCompletionsRequestBody<'a> {
#[derive(Serialize)]
struct ChatCompletionsWebSearchOptions {}
#[derive(Serialize)]
struct ResponsesRequestBody {
model: String,
stream: bool,
input: Vec<ResponsesInputMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
max_output_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
tools: Option<Vec<ResponsesWebSearchTool>>,
}
#[derive(Serialize)]
struct ResponsesInputMessage {
role: &'static str,
content: Vec<ResponsesInputContentPart>,
}
#[derive(Serialize)]
struct ResponsesInputContentPart {
#[serde(rename = "type")]
part_type: &'static str,
text: String,
}
#[derive(Serialize)]
struct ResponsesWebSearchTool {
#[serde(rename = "type")]
tool_type: &'static str,
max_keyword: u8,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct LlmRawFailureInputLog<'a> {
provider: &'static str,
protocol: &'static str,
model: &'a str,
stream: bool,
attempt: u32,
@@ -181,10 +229,48 @@ struct ChatCompletionsContentPart {
text: Option<String>,
}
#[derive(Default)]
#[derive(Deserialize)]
struct ResponsesResponseEnvelope {
id: Option<String>,
model: Option<String>,
#[serde(default)]
output_text: Option<String>,
#[serde(default)]
output: Vec<ResponsesOutputItem>,
#[serde(default)]
status: Option<String>,
usage: Option<ResponsesUsage>,
}
#[derive(Deserialize)]
struct ResponsesOutputItem {
#[serde(default)]
content: Vec<ResponsesOutputContentPart>,
}
#[derive(Deserialize)]
struct ResponsesOutputContentPart {
#[serde(rename = "type")]
#[allow(dead_code)]
part_type: Option<String>,
#[serde(default)]
text: Option<String>,
}
#[derive(Deserialize)]
struct ResponsesUsage {
#[serde(default)]
input_tokens: u64,
#[serde(default)]
output_tokens: u64,
#[serde(default)]
total_tokens: u64,
}
struct OpenAiCompatibleSseParser {
buffer: String,
raw_text: String,
protocol: LlmTextProtocol,
}
#[derive(Debug)]
@@ -282,6 +368,14 @@ impl LlmConfig {
CHAT_COMPLETIONS_PATH.trim_start_matches('/')
)
}
pub fn responses_url(&self) -> String {
format!(
"{}/{}",
self.base_url.trim_end_matches('/'),
RESPONSES_PATH.trim_start_matches('/')
)
}
}
impl LlmMessage {
@@ -312,6 +406,7 @@ impl LlmTextRequest {
messages,
max_tokens: None,
enable_web_search: false,
protocol: LlmTextProtocol::ChatCompletions,
}
}
@@ -337,6 +432,11 @@ impl LlmTextRequest {
self
}
pub fn with_responses_api(mut self) -> Self {
self.protocol = LlmTextProtocol::Responses;
self
}
fn validate(&self) -> Result<(), LlmError> {
if self.messages.is_empty() {
return Err(LlmError::InvalidRequest(
@@ -372,6 +472,15 @@ impl LlmTextRequest {
}
}
impl LlmTextProtocol {
fn as_str(self) -> &'static str {
match self {
Self::ChatCompletions => "chat_completions",
Self::Responses => "responses",
}
}
}
impl fmt::Display for LlmError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
@@ -430,18 +539,23 @@ impl LlmClient {
llm_error
})?;
parse_chat_completions_response(self.config.provider(), &resolved_model, raw_text.as_str())
.map_err(|error| {
log_llm_raw_failure(
&self.config,
&request,
false,
1,
"parse_response_failed",
raw_text.as_str(),
);
error
})
parse_text_response(
request.protocol,
self.config.provider(),
&resolved_model,
raw_text.as_str(),
)
.map_err(|error| {
log_llm_raw_failure(
&self.config,
&request,
false,
1,
"parse_response_failed",
raw_text.as_str(),
);
error
})
}
pub async fn request_single_message_text(
@@ -470,7 +584,7 @@ impl LlmClient {
.and_then(|value| value.to_str().ok())
.map(str::to_string);
let mut parser = OpenAiCompatibleSseParser::default();
let mut parser = OpenAiCompatibleSseParser::new(request.protocol);
let mut accumulated_text = String::new();
let mut finish_reason = None;
let mut undecoded_chunk_bytes = Vec::new();
@@ -658,29 +772,27 @@ impl LlmClient {
request: &LlmTextRequest,
stream: bool,
) -> Result<reqwest::Response, LlmError> {
let request_body = ChatCompletionsRequestBody {
model: request.resolved_model(self.config.model()),
messages: request.messages.as_slice(),
stream,
max_tokens: request.max_tokens,
web_search_options: request
.enable_web_search
.then_some(ChatCompletionsWebSearchOptions {}),
let request_body = build_request_body(request, self.config.model(), stream);
let model = request.resolved_model(self.config.model());
let url = match request.protocol {
LlmTextProtocol::ChatCompletions => self.config.chat_completions_url(),
LlmTextProtocol::Responses => self.config.responses_url(),
};
let max_attempts = self.config.max_retries().saturating_add(1);
for attempt in 1..=max_attempts {
debug!(
"platform-llm request started: provider={}, stream={}, attempt={}, model={}",
"platform-llm request started: provider={}, protocol={}, stream={}, attempt={}, model={}",
self.config.provider().as_str(),
request.protocol.as_str(),
stream,
attempt,
request_body.model
model
);
let send_result = self
.http_client
.post(self.config.chat_completions_url())
.post(url.as_str())
.bearer_auth(self.config.api_key())
.json(&request_body)
.timeout(Duration::from_millis(self.config.request_timeout_ms()))
@@ -690,8 +802,9 @@ impl LlmClient {
match send_result {
Ok(response) if response.status().is_success() => {
debug!(
"platform-llm request succeeded: provider={}, stream={}, attempt={}, status={}",
"platform-llm request succeeded: provider={}, protocol={}, stream={}, attempt={}, status={}",
self.config.provider().as_str(),
request.protocol.as_str(),
stream,
attempt,
response.status().as_u16()
@@ -705,8 +818,9 @@ impl LlmClient {
if should_retry_status(status) && attempt < max_attempts {
warn!(
"platform-llm request retrying after upstream status: provider={}, attempt={}, status={}, message={}",
"platform-llm request retrying after upstream status: provider={}, protocol={}, attempt={}, status={}, message={}",
self.config.provider().as_str(),
request.protocol.as_str(),
attempt,
status.as_u16(),
message
@@ -731,8 +845,9 @@ impl LlmClient {
Err(error) if error.is_timeout() => {
if attempt < max_attempts {
warn!(
"platform-llm request retrying after timeout: provider={}, attempt={}",
"platform-llm request retrying after timeout: provider={}, protocol={}, attempt={}",
self.config.provider().as_str(),
request.protocol.as_str(),
attempt
);
self.sleep_before_retry(attempt).await;
@@ -754,8 +869,9 @@ impl LlmClient {
let message = error.to_string();
if attempt < max_attempts {
warn!(
"platform-llm request retrying after connectivity failure: provider={}, attempt={}, error={}",
"platform-llm request retrying after connectivity failure: provider={}, protocol={}, attempt={}, error={}",
self.config.provider().as_str(),
request.protocol.as_str(),
attempt,
message
);
@@ -810,6 +926,14 @@ impl LlmClient {
}
impl OpenAiCompatibleSseParser {
fn new(protocol: LlmTextProtocol) -> Self {
Self {
buffer: String::new(),
raw_text: String::new(),
protocol,
}
}
fn push_chunk(&mut self, chunk: &str) -> Result<Vec<ParsedStreamEvent>, LlmError> {
self.raw_text.push_str(chunk);
self.buffer.push_str(chunk);
@@ -837,7 +961,7 @@ impl OpenAiCompatibleSseParser {
let block = self.buffer[..boundary].to_string();
self.buffer = self.buffer[(boundary + 2)..].to_string();
if let Some(event) = parse_sse_event_block(block.as_str())? {
if let Some(event) = parse_sse_event_block(self.protocol, block.as_str())? {
events.push(event);
}
}
@@ -855,6 +979,55 @@ fn normalize_non_empty(value: String, error_message: &str) -> Result<String, Llm
Ok(trimmed)
}
fn build_request_body(
request: &LlmTextRequest,
fallback_model: &str,
stream: bool,
) -> LlmRequestBody {
match request.protocol {
LlmTextProtocol::ChatCompletions => {
LlmRequestBody::ChatCompletions(ChatCompletionsRequestBody {
model: request.resolved_model(fallback_model).to_string(),
messages: request.messages.clone(),
stream,
max_tokens: request.max_tokens,
web_search_options: request
.enable_web_search
.then_some(ChatCompletionsWebSearchOptions {}),
})
}
LlmTextProtocol::Responses => LlmRequestBody::Responses(ResponsesRequestBody {
model: request.resolved_model(fallback_model).to_string(),
stream,
input: map_responses_input_messages(request.messages.as_slice()),
max_output_tokens: request.max_tokens,
tools: request.enable_web_search.then(|| {
vec![ResponsesWebSearchTool {
tool_type: "web_search",
max_keyword: 3,
}]
}),
}),
}
}
fn map_responses_input_messages(messages: &[LlmMessage]) -> Vec<ResponsesInputMessage> {
messages
.iter()
.map(|message| ResponsesInputMessage {
role: match message.role {
LlmMessageRole::System => "system",
LlmMessageRole::User => "user",
LlmMessageRole::Assistant => "assistant",
},
content: vec![ResponsesInputContentPart {
part_type: "input_text",
text: message.content.clone(),
}],
})
.collect()
}
fn log_llm_raw_failure(
config: &LlmConfig,
request: &LlmTextRequest,
@@ -890,6 +1063,7 @@ fn write_llm_raw_failure(
let model = request.resolved_model(config.model());
let input_log = LlmRawFailureInputLog {
provider: config.provider().as_str(),
protocol: request.protocol.as_str(),
model,
stream,
attempt,
@@ -936,6 +1110,20 @@ fn sanitize_log_file_segment(value: &str) -> String {
}
}
fn parse_text_response(
protocol: LlmTextProtocol,
provider: LlmProvider,
fallback_model: &str,
raw_text: &str,
) -> Result<LlmTextResponse, LlmError> {
match protocol {
LlmTextProtocol::ChatCompletions => {
parse_chat_completions_response(provider, fallback_model, raw_text)
}
LlmTextProtocol::Responses => parse_responses_response(provider, fallback_model, raw_text),
}
}
fn parse_chat_completions_response(
provider: LlmProvider,
fallback_model: &str,
@@ -967,6 +1155,56 @@ fn parse_chat_completions_response(
})
}
fn parse_responses_response(
provider: LlmProvider,
fallback_model: &str,
raw_text: &str,
) -> Result<LlmTextResponse, LlmError> {
let parsed: ResponsesResponseEnvelope = serde_json::from_str(raw_text).map_err(|error| {
LlmError::Deserialize(format!("解析 LLM Responses JSON 响应失败:{error}"))
})?;
let content = extract_responses_text(&parsed)
.ok_or(LlmError::EmptyResponse)?
.trim()
.to_string();
if content.is_empty() {
return Err(LlmError::EmptyResponse);
}
Ok(LlmTextResponse {
provider,
model: parsed.model.unwrap_or_else(|| fallback_model.to_string()),
content,
finish_reason: parsed.status,
response_id: parsed.id,
usage: parsed.usage.map(|usage| LlmTokenUsage {
prompt_tokens: usage.input_tokens,
completion_tokens: usage.output_tokens,
total_tokens: usage.total_tokens,
}),
})
}
fn extract_responses_text(parsed: &ResponsesResponseEnvelope) -> Option<String> {
parsed
.output_text
.as_deref()
.map(str::to_string)
.filter(|text| !text.is_empty())
.or_else(|| {
let text = parsed
.output
.iter()
.flat_map(|item| item.content.iter())
.filter_map(|part| part.text.as_deref())
.collect::<Vec<_>>()
.join("");
if text.is_empty() { None } else { Some(text) }
})
}
fn extract_message_text(choice: &ChatCompletionsChoice) -> Option<String> {
choice
.message
@@ -1016,7 +1254,10 @@ fn decode_utf8_stream_chunk(bytes: &[u8]) -> Result<(String, Vec<u8>), LlmError>
}
}
fn parse_sse_event_block(block: &str) -> Result<Option<ParsedStreamEvent>, LlmError> {
fn parse_sse_event_block(
protocol: LlmTextProtocol,
block: &str,
) -> Result<Option<ParsedStreamEvent>, LlmError> {
let data_lines = block
.lines()
.filter_map(|line| line.trim().strip_prefix("data:"))
@@ -1032,6 +1273,10 @@ fn parse_sse_event_block(block: &str) -> Result<Option<ParsedStreamEvent>, LlmEr
return Ok(None);
}
if protocol == LlmTextProtocol::Responses {
return parse_responses_sse_event(data.as_str());
}
let parsed: ChatCompletionsResponseEnvelope = serde_json::from_str(data.as_str())
.map_err(|error| LlmError::Deserialize(format!("解析 LLM SSE 事件失败:{error}")))?;
let first_choice = parsed
@@ -1045,6 +1290,44 @@ fn parse_sse_event_block(block: &str) -> Result<Option<ParsedStreamEvent>, LlmEr
}))
}
fn parse_responses_sse_event(data: &str) -> Result<Option<ParsedStreamEvent>, LlmError> {
let parsed: serde_json::Value = serde_json::from_str(data).map_err(|error| {
LlmError::Deserialize(format!("解析 LLM Responses SSE 事件失败:{error}"))
})?;
let event_type = parsed
.get("type")
.and_then(serde_json::Value::as_str)
.unwrap_or_default();
match event_type {
"response.output_text.delta" => Ok(Some(ParsedStreamEvent {
delta_text: parsed
.get("delta")
.and_then(serde_json::Value::as_str)
.map(str::to_string),
finish_reason: None,
})),
"response.completed" => Ok(Some(ParsedStreamEvent {
delta_text: None,
finish_reason: Some("completed".to_string()),
})),
"response.failed" | "error" => {
let message = parsed
.get("error")
.and_then(|error| error.get("message"))
.and_then(serde_json::Value::as_str)
.or_else(|| parsed.get("message").and_then(serde_json::Value::as_str))
.unwrap_or("LLM Responses SSE 返回失败事件")
.to_string();
Err(LlmError::Upstream {
status_code: 502,
message,
})
}
_ => Ok(None),
}
}
fn should_retry_status(status: StatusCode) -> bool {
status == StatusCode::REQUEST_TIMEOUT
|| status == StatusCode::TOO_MANY_REQUESTS
@@ -1151,11 +1434,12 @@ mod tests {
config.chat_completions_url(),
"https://example.com/base/chat/completions"
);
assert_eq!(config.responses_url(), "https://example.com/base/responses");
}
#[test]
fn sse_parser_handles_split_chunks_and_done_marker() {
let mut parser = OpenAiCompatibleSseParser::default();
let mut parser = OpenAiCompatibleSseParser::new(LlmTextProtocol::ChatCompletions);
let events_a = parser
.push_chunk("data: {\"choices\":[{\"delta\":{\"content\":\"\"}}]}\r\n\r\n")
.expect("first chunk should parse");
@@ -1170,6 +1454,24 @@ mod tests {
assert_eq!(events_b[0].finish_reason.as_deref(), Some("stop"));
}
#[test]
fn responses_sse_parser_only_emits_output_text_delta() {
let mut parser = OpenAiCompatibleSseParser::new(LlmTextProtocol::Responses);
let events = parser
.push_chunk(concat!(
"data: {\"type\":\"response.created\"}\n\n",
"data: {\"type\":\"response.output_text.delta\",\"delta\":\"\"}\n\n",
"data: {\"type\":\"response.output_text.delta\",\"delta\":\"\"}\n\n",
"data: {\"type\":\"response.completed\"}\n\n",
))
.expect("responses stream should parse");
assert_eq!(events.len(), 3);
assert_eq!(events[0].delta_text.as_deref(), Some(""));
assert_eq!(events[1].delta_text.as_deref(), Some(""));
assert_eq!(events[2].finish_reason.as_deref(), Some("completed"));
}
#[test]
fn decode_utf8_stream_chunk_preserves_incomplete_multibyte_suffix() {
let full_bytes = "你好".as_bytes();
@@ -1284,6 +1586,72 @@ mod tests {
assert_eq!(request_json["web_search_options"], serde_json::json!({}));
}
#[tokio::test]
async fn request_text_sends_responses_body_with_web_search_tool() {
let listener = TcpListener::bind("127.0.0.1:0").expect("listener should bind");
let address = listener.local_addr().expect("listener should have addr");
let server_handle = thread::spawn(move || {
let (mut stream, _) = listener.accept().expect("request should connect");
let request_text = read_request(&mut stream);
write_response(
&mut stream,
MockResponse {
status_line: "200 OK",
content_type: "application/json; charset=utf-8",
body: r#"{"id":"resp_responses","model":"deepseek-v3-2-251201","output_text":"Responses ","status":"completed","usage":{"input_tokens":9,"output_tokens":4,"total_tokens":13}}"#.to_string(),
extra_headers: Vec::new(),
},
);
request_text
});
let client = build_test_client(format!("http://{address}"), 0);
let response = client
.request_text(
LlmTextRequest::single_turn("系统", "用户")
.with_model("deepseek-v3-2-251201")
.with_responses_api()
.with_web_search(true)
.with_max_tokens(128),
)
.await
.expect("responses request_text should succeed");
let request_text = server_handle.join().expect("server thread should join");
let request_line = request_text.lines().next().unwrap_or_default();
let request_body = request_text
.split("\r\n\r\n")
.nth(1)
.expect("request body should exist");
let request_json: serde_json::Value =
serde_json::from_str(request_body).expect("request body should be json");
assert!(request_line.contains("POST /responses HTTP/1.1"));
assert_eq!(response.content, "Responses 成功");
assert_eq!(response.model, "deepseek-v3-2-251201");
assert_eq!(
response.usage,
Some(LlmTokenUsage {
prompt_tokens: 9,
completion_tokens: 4,
total_tokens: 13,
})
);
assert_eq!(
request_json["model"],
serde_json::json!("deepseek-v3-2-251201")
);
assert_eq!(request_json["stream"], serde_json::json!(false));
assert_eq!(
request_json["tools"],
serde_json::json!([{ "type": "web_search", "max_keyword": 3 }])
);
assert_eq!(
request_json["input"][0]["content"][0],
serde_json::json!({ "type": "input_text", "text": "系统" })
);
}
#[tokio::test]
async fn stream_text_accumulates_sse_response() {
let server_url = spawn_mock_server(vec![MockResponse {
@@ -1314,6 +1682,41 @@ mod tests {
assert_eq!(response.response_id.as_deref(), Some("req_stream_01"));
}
#[tokio::test]
async fn stream_text_accumulates_responses_sse_response() {
let server_url = spawn_mock_server(vec![MockResponse {
status_line: "200 OK",
content_type: "text/event-stream; charset=utf-8",
body: concat!(
"data: {\"type\":\"response.output_text.delta\",\"delta\":\"\"}\n\n",
"data: {\"type\":\"response.output_text.delta\",\"delta\":\"\"}\n\n",
"data: {\"type\":\"response.completed\"}\n\n"
)
.to_string(),
extra_headers: vec![("x-request-id", "req_responses_stream_01")],
}]);
let client = build_test_client(server_url, 0);
let mut updates = Vec::new();
let response = client
.stream_text(
LlmTextRequest::single_turn("系统", "用户").with_responses_api(),
|delta| {
updates.push(delta.accumulated_text.clone());
},
)
.await
.expect("responses stream_text should succeed");
assert_eq!(updates, vec!["".to_string(), "你好".to_string()]);
assert_eq!(response.content, "你好");
assert_eq!(response.finish_reason.as_deref(), Some("completed"));
assert_eq!(
response.response_id.as_deref(),
Some("req_responses_stream_01")
);
}
#[tokio::test]
async fn request_text_writes_raw_failure_logs_after_parse_error() {
let log_dir = std::env::temp_dir().join(format!(

View File

@@ -6,6 +6,7 @@ pub struct BigFishWorkSummaryResponse {
pub work_id: String,
pub source_session_id: String,
pub owner_user_id: String,
pub author_display_name: String,
pub title: String,
pub subtitle: String,
pub summary: String,

View File

@@ -6,6 +6,10 @@ pub struct CreatePuzzleAgentSessionRequest {
#[serde(default)]
pub seed_text: Option<String>,
#[serde(default)]
pub work_title: Option<String>,
#[serde(default)]
pub work_description: Option<String>,
#[serde(default)]
pub picture_description: Option<String>,
#[serde(default)]
pub reference_image_src: Option<String>,
@@ -33,11 +37,32 @@ pub struct ExecutePuzzleAgentActionRequest {
#[serde(default)]
pub candidate_id: Option<String>,
#[serde(default)]
pub level_id: Option<String>,
#[serde(default)]
pub work_title: Option<String>,
#[serde(default)]
pub work_description: Option<String>,
#[serde(default)]
pub picture_description: Option<String>,
#[serde(default)]
pub level_name: Option<String>,
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub theme_tags: Option<Vec<String>>,
#[serde(default)]
pub levels_json: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleFormDraftResponse {
#[serde(default)]
pub work_title: Option<String>,
#[serde(default)]
pub work_description: Option<String>,
#[serde(default)]
pub picture_description: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@@ -88,6 +113,8 @@ pub struct PuzzleCreatorIntentResponse {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleResultDraftResponse {
pub work_title: String,
pub work_description: String,
pub level_name: String,
pub summary: String,
pub theme_tags: Vec<String>,
@@ -103,6 +130,25 @@ pub struct PuzzleResultDraftResponse {
#[serde(default)]
pub cover_asset_id: Option<String>,
pub generation_status: String,
pub levels: Vec<PuzzleDraftLevelResponse>,
#[serde(default)]
pub form_draft: Option<PuzzleFormDraftResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleDraftLevelResponse {
pub level_id: String,
pub level_name: String,
pub picture_description: String,
pub candidates: Vec<PuzzleGeneratedImageCandidateResponse>,
#[serde(default)]
pub selected_candidate_id: Option<String>,
#[serde(default)]
pub cover_image_src: Option<String>,
#[serde(default)]
pub cover_asset_id: Option<String>,
pub generation_status: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@@ -153,6 +199,7 @@ pub struct PuzzleResultPreviewEnvelopeResponse {
#[serde(rename_all = "camelCase")]
pub struct PuzzleAgentSessionSnapshotResponse {
pub session_id: String,
pub seed_text: String,
pub current_turn: u32,
pub progress_percent: u32,
pub stage: String,

View File

@@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
#[serde(rename_all = "camelCase")]
pub struct StartPuzzleRunRequest {
pub profile_id: String,
#[serde(default)]
pub level_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]

View File

@@ -1,10 +1,12 @@
use serde::{Deserialize, Serialize};
use crate::puzzle_agent::PuzzleAnchorPackResponse;
use crate::puzzle_agent::{PuzzleAnchorPackResponse, PuzzleDraftLevelResponse};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PutPuzzleWorkRequest {
pub work_title: String,
pub work_description: String,
pub level_name: String,
pub summary: String,
pub theme_tags: Vec<String>,
@@ -12,6 +14,8 @@ pub struct PutPuzzleWorkRequest {
pub cover_image_src: Option<String>,
#[serde(default)]
pub cover_asset_id: Option<String>,
#[serde(default)]
pub levels: Vec<PuzzleDraftLevelResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -23,6 +27,8 @@ pub struct PuzzleWorkSummaryResponse {
#[serde(default)]
pub source_session_id: Option<String>,
pub author_display_name: String,
pub work_title: String,
pub work_description: String,
pub level_name: String,
pub summary: String,
pub theme_tags: Vec<String>,
@@ -50,6 +56,7 @@ pub struct PuzzleWorkProfileResponse {
#[serde(flatten)]
pub summary: PuzzleWorkSummaryResponse,
pub anchor_pack: PuzzleAnchorPackResponse,
pub levels: Vec<PuzzleDraftLevelResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]

View File

@@ -1,6 +1,7 @@
use super::*;
use crate::mapper::*;
use crate::module_bindings::delete_big_fish_work_procedure::delete_big_fish_work;
use crate::module_bindings::record_big_fish_like_procedure::record_big_fish_like;
use crate::module_bindings::record_big_fish_play_procedure::record_big_fish_play;
use crate::module_bindings::remix_big_fish_work_procedure::remix_big_fish_work;
use module_big_fish::PUBLIC_BIG_FISH_GALLERY_OWNER_USER_ID;
@@ -294,6 +295,29 @@ impl SpacetimeClient {
.await
}
pub async fn record_big_fish_like(
&self,
input: BigFishLikeReportRecordInput,
) -> Result<Vec<BigFishWorkSummaryRecord>, SpacetimeClientError> {
let procedure_input = BigFishWorkLikeRecordInput {
session_id: input.session_id,
user_id: input.user_id,
liked_at_micros: input.liked_at_micros,
};
self.call_after_connect(move |connection, sender| {
connection
.procedures()
.record_big_fish_like_then(procedure_input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.and_then(|result| map_big_fish_works_procedure_result(result, None));
send_once(&sender, mapped);
});
})
.await
}
pub async fn remix_big_fish_work(
&self,
input: BigFishWorkRemixRecordInput,

View File

@@ -1,6 +1,7 @@
use super::*;
use crate::mapper::*;
use crate::module_bindings::delete_custom_world_agent_session_procedure::delete_custom_world_agent_session;
use crate::module_bindings::record_custom_world_profile_like_procedure::record_custom_world_profile_like;
use crate::module_bindings::record_custom_world_profile_play_procedure::record_custom_world_profile_play;
use crate::module_bindings::remix_custom_world_profile_procedure::remix_custom_world_profile;
@@ -261,6 +262,30 @@ impl SpacetimeClient {
.await
}
pub async fn record_custom_world_profile_like(
&self,
input: CustomWorldProfileLikeReportRecordInput,
) -> Result<CustomWorldLibraryMutationRecord, SpacetimeClientError> {
let procedure_input = CustomWorldProfileLikeRecordInput {
owner_user_id: input.owner_user_id,
profile_id: input.profile_id,
user_id: input.user_id,
liked_at_micros: input.liked_at_micros,
};
self.call_after_connect(move |connection, sender| {
connection
.procedures()
.record_custom_world_profile_like_then(procedure_input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.and_then(map_custom_world_library_mutation_result);
send_once(&sender, mapped);
});
})
.await
}
pub async fn publish_custom_world_world(
&self,
input: CustomWorldPublishWorldRecordInput,

View File

@@ -3,42 +3,44 @@
pub mod module_bindings;
mod mapper;
pub(crate) use mapper::*;
use mapper::*;
pub use mapper::{
AiResultReferenceRecord, AiTaskMutationRecord, AiTaskRecord, AiTaskStageRecord,
AiTextChunkRecord, BattleStateRecord, BigFishAgentMessageRecord, BigFishAnchorItemRecord,
BigFishAnchorPackRecord, BigFishAssetCoverageRecord, BigFishAssetGenerateRecordInput,
BigFishAssetSlotRecord, BigFishBackgroundBlueprintRecord, BigFishDraftCompileRecordInput,
BigFishGameDraftRecord, BigFishLevelBlueprintRecord, BigFishMessageFinalizeRecordInput,
BigFishMessageSubmitRecordInput, BigFishPlayReportRecordInput, BigFishRuntimeParamsRecord,
BigFishSessionCreateRecordInput, BigFishSessionRecord, BigFishWorkRemixRecordInput,
BigFishWorkSummaryRecord, CustomWorldAgentActionExecuteRecord,
CustomWorldAgentActionExecuteRecordInput, CustomWorldAgentCheckpointRecord,
CustomWorldAgentMessageFinalizeRecordInput, CustomWorldAgentMessageRecord,
CustomWorldAgentMessageSubmitRecordInput, CustomWorldAgentOperationProgressRecordInput,
CustomWorldAgentOperationRecord, CustomWorldAgentSessionCreateRecordInput,
CustomWorldAgentSessionRecord, CustomWorldCheckpointRecord, CustomWorldDraftCardDetailRecord,
BigFishGameDraftRecord, BigFishLevelBlueprintRecord, BigFishLikeReportRecordInput,
BigFishMessageFinalizeRecordInput, BigFishMessageSubmitRecordInput,
BigFishPlayReportRecordInput, BigFishRuntimeParamsRecord, BigFishSessionCreateRecordInput,
BigFishSessionRecord, BigFishWorkRemixRecordInput, BigFishWorkSummaryRecord,
CustomWorldAgentActionExecuteRecord, CustomWorldAgentActionExecuteRecordInput,
CustomWorldAgentCheckpointRecord, CustomWorldAgentMessageFinalizeRecordInput,
CustomWorldAgentMessageRecord, CustomWorldAgentMessageSubmitRecordInput,
CustomWorldAgentOperationProgressRecordInput, CustomWorldAgentOperationRecord,
CustomWorldAgentSessionCreateRecordInput, CustomWorldAgentSessionRecord,
CustomWorldCheckpointRecord, CustomWorldDraftCardDetailRecord,
CustomWorldDraftCardDetailSectionRecord, CustomWorldDraftCardRecord,
CustomWorldGalleryEntryRecord, CustomWorldLibraryEntryRecord, CustomWorldLibraryMutationRecord,
CustomWorldProfilePlayReportRecordInput, CustomWorldProfileRemixRecordInput,
CustomWorldProfileUpsertRecordInput, CustomWorldPublishGateRecord,
CustomWorldPublishWorldRecord, CustomWorldPublishWorldRecordInput,
CustomWorldPublishedProfileCompileRecord, CustomWorldResultPreviewBlockerRecord,
CustomWorldSupportedActionRecord, CustomWorldWorkSummaryRecord, NpcBattleInteractionRecord,
NpcInteractionRecord, NpcStateRecord, PuzzleAgentMessageFinalizeRecordInput,
PuzzleAgentMessageRecord, PuzzleAgentMessageSubmitRecordInput,
PuzzleAgentSessionCreateRecordInput, PuzzleAgentSessionRecord,
PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord, PuzzleAnchorPackRecord,
PuzzleBoardRecord, PuzzleCellPositionRecord, PuzzleCreatorIntentRecord,
CustomWorldProfileLikeReportRecordInput, CustomWorldProfilePlayReportRecordInput,
CustomWorldProfileRemixRecordInput, CustomWorldProfileUpsertRecordInput,
CustomWorldPublishGateRecord, CustomWorldPublishWorldRecord,
CustomWorldPublishWorldRecordInput, CustomWorldPublishedProfileCompileRecord,
CustomWorldResultPreviewBlockerRecord, CustomWorldSupportedActionRecord,
CustomWorldWorkSummaryRecord, NpcBattleInteractionRecord, NpcInteractionRecord, NpcStateRecord,
PuzzleAgentMessageFinalizeRecordInput, PuzzleAgentMessageRecord,
PuzzleAgentMessageSubmitRecordInput, PuzzleAgentSessionCreateRecordInput,
PuzzleAgentSessionRecord, PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord,
PuzzleAnchorPackRecord, PuzzleBoardRecord, PuzzleCellPositionRecord, PuzzleCreatorIntentRecord,
PuzzleDraftLevelRecord, PuzzleFormDraftRecord, PuzzleFormDraftSaveRecordInput,
PuzzleGeneratedImageCandidateRecord, PuzzleGeneratedImagesSaveRecordInput,
PuzzleLeaderboardEntryRecord, PuzzleLeaderboardSubmitRecordInput, PuzzleMergedGroupRecord,
PuzzlePieceStateRecord, PuzzlePublishRecordInput, PuzzleResultDraftRecord,
PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord,
PuzzleRunDragRecordInput, PuzzleRunNextLevelRecordInput, PuzzleRunPauseRecordInput,
PuzzleRunPropRecordInput, PuzzleRunRecord, PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput,
PuzzleRuntimeLevelRecord, PuzzleSelectCoverImageRecordInput, PuzzleWorkProfileRecord,
PuzzleWorkRemixRecordInput, PuzzleWorkUpsertRecordInput, ResolveCombatActionRecord,
ResolveNpcBattleInteractionInput,
PuzzleRuntimeLevelRecord, PuzzleSelectCoverImageRecordInput, PuzzleWorkLikeReportRecordInput,
PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput, PuzzleWorkUpsertRecordInput,
ResolveCombatActionRecord, ResolveNpcBattleInteractionInput,
};
pub mod ai;
@@ -108,7 +110,7 @@ use module_puzzle::{
PuzzleAnchorItem as DomainPuzzleAnchorItem, PuzzleAnchorPack as DomainPuzzleAnchorPack,
PuzzleBoardSnapshot as DomainPuzzleBoardSnapshot,
PuzzleCellPosition as DomainPuzzleCellPosition,
PuzzleCreatorIntent as DomainPuzzleCreatorIntent,
PuzzleCreatorIntent as DomainPuzzleCreatorIntent, PuzzleDraftLevel as DomainPuzzleDraftLevel,
PuzzleGeneratedImageCandidate as DomainPuzzleGeneratedImageCandidate,
PuzzleMergedGroupState as DomainPuzzleMergedGroupState,
PuzzlePieceState as DomainPuzzlePieceState, PuzzleResultDraft as DomainPuzzleResultDraft,

View File

@@ -1917,7 +1917,7 @@ pub(crate) fn map_custom_world_gallery_entry_snapshot(
play_count: snapshot.play_count,
remix_count: snapshot.remix_count,
like_count: snapshot.like_count,
recent_play_count_7d: snapshot.recent_play_count_7d,
recent_play_count_7d: snapshot.recent_play_count_7_d,
})
}
@@ -2223,6 +2223,7 @@ pub(crate) fn map_puzzle_agent_session_snapshot(
) -> PuzzleAgentSessionRecord {
PuzzleAgentSessionRecord {
session_id: snapshot.session_id,
seed_text: snapshot.seed_text,
current_turn: snapshot.current_turn,
progress_percent: snapshot.progress_percent,
stage: snapshot.stage.as_str().to_string(),
@@ -2268,6 +2269,8 @@ pub(crate) fn map_puzzle_result_draft(
snapshot: DomainPuzzleResultDraft,
) -> PuzzleResultDraftRecord {
PuzzleResultDraftRecord {
work_title: snapshot.work_title,
work_description: snapshot.work_description,
level_name: snapshot.level_name,
summary: snapshot.summary,
theme_tags: snapshot.theme_tags,
@@ -2283,6 +2286,39 @@ pub(crate) fn map_puzzle_result_draft(
cover_image_src: snapshot.cover_image_src,
cover_asset_id: snapshot.cover_asset_id,
generation_status: snapshot.generation_status,
levels: snapshot
.levels
.into_iter()
.map(map_puzzle_draft_level)
.collect(),
form_draft: snapshot.form_draft.map(map_puzzle_form_draft),
}
}
pub(crate) fn map_puzzle_form_draft(
snapshot: module_puzzle::PuzzleFormDraft,
) -> PuzzleFormDraftRecord {
PuzzleFormDraftRecord {
work_title: snapshot.work_title,
work_description: snapshot.work_description,
picture_description: snapshot.picture_description,
}
}
pub(crate) fn map_puzzle_draft_level(snapshot: DomainPuzzleDraftLevel) -> PuzzleDraftLevelRecord {
PuzzleDraftLevelRecord {
level_id: snapshot.level_id,
level_name: snapshot.level_name,
picture_description: snapshot.picture_description,
candidates: snapshot
.candidates
.into_iter()
.map(map_puzzle_generated_image_candidate)
.collect(),
selected_candidate_id: snapshot.selected_candidate_id,
cover_image_src: snapshot.cover_image_src,
cover_asset_id: snapshot.cover_asset_id,
generation_status: snapshot.generation_status,
}
}
@@ -2386,6 +2422,8 @@ pub(crate) fn map_puzzle_work_profile(
owner_user_id: snapshot.owner_user_id,
source_session_id: snapshot.source_session_id,
author_display_name: snapshot.author_display_name,
work_title: snapshot.work_title,
work_description: snapshot.work_description,
level_name: snapshot.level_name,
summary: snapshot.summary,
theme_tags: snapshot.theme_tags,
@@ -2400,6 +2438,11 @@ pub(crate) fn map_puzzle_work_profile(
recent_play_count_7d: snapshot.recent_play_count_7d,
publish_ready: snapshot.publish_ready,
anchor_pack: map_puzzle_anchor_pack(snapshot.anchor_pack),
levels: snapshot
.levels
.into_iter()
.map(map_puzzle_draft_level)
.collect(),
}
}
@@ -4215,6 +4258,14 @@ pub struct CustomWorldProfilePlayReportRecordInput {
pub played_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldProfileLikeReportRecordInput {
pub owner_user_id: String,
pub profile_id: String,
pub user_id: String,
pub liked_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldPublishWorldRecordInput {
pub session_id: String,
@@ -4314,6 +4365,14 @@ pub struct PuzzleAgentSessionCreateRecordInput {
pub created_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PuzzleFormDraftSaveRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub seed_text: String,
pub saved_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PuzzleAgentMessageSubmitRecordInput {
pub session_id: String,
@@ -4340,6 +4399,7 @@ pub struct PuzzleAgentMessageFinalizeRecordInput {
pub struct PuzzleGeneratedImagesSaveRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub level_id: Option<String>,
pub candidates_json: String,
pub saved_at_micros: i64,
}
@@ -4348,6 +4408,7 @@ pub struct PuzzleGeneratedImagesSaveRecordInput {
pub struct PuzzleSelectCoverImageRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub level_id: Option<String>,
pub candidate_id: String,
pub selected_at_micros: i64,
}
@@ -4359,9 +4420,12 @@ pub struct PuzzlePublishRecordInput {
pub work_id: String,
pub profile_id: String,
pub author_display_name: String,
pub work_title: Option<String>,
pub work_description: Option<String>,
pub level_name: Option<String>,
pub summary: Option<String>,
pub theme_tags: Option<Vec<String>>,
pub levels_json: Option<String>,
pub published_at_micros: i64,
}
@@ -4369,11 +4433,14 @@ pub struct PuzzlePublishRecordInput {
pub struct PuzzleWorkUpsertRecordInput {
pub profile_id: String,
pub owner_user_id: String,
pub work_title: String,
pub work_description: String,
pub level_name: String,
pub summary: String,
pub theme_tags: Vec<String>,
pub cover_image_src: Option<String>,
pub cover_asset_id: Option<String>,
pub levels_json: Option<String>,
pub updated_at_micros: i64,
}
@@ -4389,11 +4456,19 @@ pub struct PuzzleWorkRemixRecordInput {
pub remixed_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PuzzleWorkLikeReportRecordInput {
pub profile_id: String,
pub user_id: String,
pub liked_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PuzzleRunStartRecordInput {
pub run_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub level_id: Option<String>,
pub started_at_micros: i64,
}
@@ -4447,6 +4522,13 @@ pub struct BigFishPlayReportRecordInput {
pub reported_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishLikeReportRecordInput {
pub session_id: String,
pub user_id: String,
pub liked_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishWorkRemixRecordInput {
pub source_session_id: String,
@@ -4498,6 +4580,8 @@ pub struct PuzzleGeneratedImageCandidateRecord {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PuzzleResultDraftRecord {
pub work_title: String,
pub work_description: String,
pub level_name: String,
pub summary: String,
pub theme_tags: Vec<String>,
@@ -4509,6 +4593,27 @@ pub struct PuzzleResultDraftRecord {
pub cover_image_src: Option<String>,
pub cover_asset_id: Option<String>,
pub generation_status: String,
pub levels: Vec<PuzzleDraftLevelRecord>,
pub form_draft: Option<PuzzleFormDraftRecord>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PuzzleFormDraftRecord {
pub work_title: Option<String>,
pub work_description: Option<String>,
pub picture_description: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PuzzleDraftLevelRecord {
pub level_id: String,
pub level_name: String,
pub picture_description: String,
pub candidates: Vec<PuzzleGeneratedImageCandidateRecord>,
pub selected_candidate_id: Option<String>,
pub cover_image_src: Option<String>,
pub cover_asset_id: Option<String>,
pub generation_status: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -4553,6 +4658,7 @@ pub struct PuzzleResultPreviewRecord {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PuzzleAgentSessionRecord {
pub session_id: String,
pub seed_text: String,
pub current_turn: u32,
pub progress_percent: u32,
pub stage: String,
@@ -4573,6 +4679,8 @@ pub struct PuzzleWorkProfileRecord {
pub owner_user_id: String,
pub source_session_id: Option<String>,
pub author_display_name: String,
pub work_title: String,
pub work_description: String,
pub level_name: String,
pub summary: String,
pub theme_tags: Vec<String>,
@@ -4587,6 +4695,7 @@ pub struct PuzzleWorkProfileRecord {
pub recent_play_count_7d: u32,
pub publish_ready: bool,
pub anchor_pack: PuzzleAnchorPackRecord,
pub levels: Vec<PuzzleDraftLevelRecord>,
}
#[derive(Clone, Debug, PartialEq, Eq)]

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::quest_record_input_type::QuestRecordInput;
@@ -14,8 +19,10 @@ pub(super) struct AcceptQuestArgs {
impl From<AcceptQuestArgs> for super::Reducer {
fn from(args: AcceptQuestArgs) -> Self {
Self::AcceptQuest { input: args.input }
}
Self::AcceptQuest {
input: args.input,
}
}
}
impl __sdk::InModule for AcceptQuestArgs {
@@ -33,8 +40,9 @@ pub trait accept_quest {
/// The reducer will run asynchronously in the future,
/// and this method provides no way to listen for its completion status.
/// /// Use [`accept_quest:accept_quest_then`] to run a callback after the reducer completes.
fn accept_quest(&self, input: QuestRecordInput) -> __sdk::Result<()> {
self.accept_quest_then(input, |_, _| {})
fn accept_quest(&self, input: QuestRecordInput,
) -> __sdk::Result<()> {
self.accept_quest_then(input, |_, _| {})
}
/// Request that the remote module invoke the reducer `accept_quest` to run as soon as possible,
@@ -62,7 +70,7 @@ impl accept_quest for super::RemoteReducers {
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp
.invoke_reducer_with_callback(AcceptQuestArgs { input }, callback)
self.imp.invoke_reducer_with_callback(AcceptQuestArgs { input, }, callback)
}
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::quest_completion_ack_input_type::QuestCompletionAckInput;
@@ -14,8 +19,10 @@ pub(super) struct AcknowledgeQuestCompletionArgs {
impl From<AcknowledgeQuestCompletionArgs> for super::Reducer {
fn from(args: AcknowledgeQuestCompletionArgs) -> Self {
Self::AcknowledgeQuestCompletion { input: args.input }
}
Self::AcknowledgeQuestCompletion {
input: args.input,
}
}
}
impl __sdk::InModule for AcknowledgeQuestCompletionArgs {
@@ -33,8 +40,9 @@ pub trait acknowledge_quest_completion {
/// The reducer will run asynchronously in the future,
/// and this method provides no way to listen for its completion status.
/// /// Use [`acknowledge_quest_completion:acknowledge_quest_completion_then`] to run a callback after the reducer completes.
fn acknowledge_quest_completion(&self, input: QuestCompletionAckInput) -> __sdk::Result<()> {
self.acknowledge_quest_completion_then(input, |_, _| {})
fn acknowledge_quest_completion(&self, input: QuestCompletionAckInput,
) -> __sdk::Result<()> {
self.acknowledge_quest_completion_then(input, |_, _| {})
}
/// Request that the remote module invoke the reducer `acknowledge_quest_completion` to run as soon as possible,
@@ -62,7 +70,7 @@ impl acknowledge_quest_completion for super::RemoteReducers {
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp
.invoke_reducer_with_callback(AcknowledgeQuestCompletionArgs { input }, callback)
self.imp.invoke_reducer_with_callback(AcknowledgeQuestCompletionArgs { input, }, callback)
}
}

View File

@@ -2,17 +2,23 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::runtime_profile_redeem_code_admin_disable_input_type::RuntimeProfileRedeemCodeAdminDisableInput;
use super::runtime_profile_redeem_code_admin_procedure_result_type::RuntimeProfileRedeemCodeAdminProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct AdminDisableProfileRedeemCodeArgs {
struct AdminDisableProfileRedeemCodeArgs {
pub input: RuntimeProfileRedeemCodeAdminDisableInput,
}
impl __sdk::InModule for AdminDisableProfileRedeemCodeArgs {
type Module = super::RemoteModule;
}
@@ -22,19 +28,16 @@ impl __sdk::InModule for AdminDisableProfileRedeemCodeArgs {
///
/// Implemented for [`super::RemoteProcedures`].
pub trait admin_disable_profile_redeem_code {
fn admin_disable_profile_redeem_code(&self, input: RuntimeProfileRedeemCodeAdminDisableInput) {
self.admin_disable_profile_redeem_code_then(input, |_, _| {});
fn admin_disable_profile_redeem_code(&self, input: RuntimeProfileRedeemCodeAdminDisableInput,
) {
self.admin_disable_profile_redeem_code_then(input, |_, _| {});
}
fn admin_disable_profile_redeem_code_then(
&self,
input: RuntimeProfileRedeemCodeAdminDisableInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>) + Send + 'static,
);
}
@@ -43,17 +46,13 @@ impl admin_disable_profile_redeem_code for super::RemoteProcedures {
&self,
input: RuntimeProfileRedeemCodeAdminDisableInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>) + Send + 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminProcedureResult>(
"admin_disable_profile_redeem_code",
AdminDisableProfileRedeemCodeArgs { input },
__callback,
);
self.imp.invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminProcedureResult>(
"admin_disable_profile_redeem_code",
AdminDisableProfileRedeemCodeArgs { input, },
__callback,
);
}
}

View File

@@ -2,17 +2,23 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::runtime_profile_redeem_code_admin_procedure_result_type::RuntimeProfileRedeemCodeAdminProcedureResult;
use super::runtime_profile_redeem_code_admin_upsert_input_type::RuntimeProfileRedeemCodeAdminUpsertInput;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct AdminUpsertProfileRedeemCodeArgs {
struct AdminUpsertProfileRedeemCodeArgs {
pub input: RuntimeProfileRedeemCodeAdminUpsertInput,
}
impl __sdk::InModule for AdminUpsertProfileRedeemCodeArgs {
type Module = super::RemoteModule;
}
@@ -22,19 +28,16 @@ impl __sdk::InModule for AdminUpsertProfileRedeemCodeArgs {
///
/// Implemented for [`super::RemoteProcedures`].
pub trait admin_upsert_profile_redeem_code {
fn admin_upsert_profile_redeem_code(&self, input: RuntimeProfileRedeemCodeAdminUpsertInput) {
self.admin_upsert_profile_redeem_code_then(input, |_, _| {});
fn admin_upsert_profile_redeem_code(&self, input: RuntimeProfileRedeemCodeAdminUpsertInput,
) {
self.admin_upsert_profile_redeem_code_then(input, |_, _| {});
}
fn admin_upsert_profile_redeem_code_then(
&self,
input: RuntimeProfileRedeemCodeAdminUpsertInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>) + Send + 'static,
);
}
@@ -43,17 +46,13 @@ impl admin_upsert_profile_redeem_code for super::RemoteProcedures {
&self,
input: RuntimeProfileRedeemCodeAdminUpsertInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>) + Send + 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminProcedureResult>(
"admin_upsert_profile_redeem_code",
AdminUpsertProfileRedeemCodeArgs { input },
__callback,
);
self.imp.invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminProcedureResult>(
"admin_upsert_profile_redeem_code",
AdminUpsertProfileRedeemCodeArgs { input, },
__callback,
);
}
}

View File

@@ -2,17 +2,23 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::puzzle_run_next_level_input_type::PuzzleRunNextLevelInput;
use super::puzzle_run_procedure_result_type::PuzzleRunProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct AdvancePuzzleNextLevelArgs {
struct AdvancePuzzleNextLevelArgs {
pub input: PuzzleRunNextLevelInput,
}
impl __sdk::InModule for AdvancePuzzleNextLevelArgs {
type Module = super::RemoteModule;
}
@@ -22,19 +28,16 @@ impl __sdk::InModule for AdvancePuzzleNextLevelArgs {
///
/// Implemented for [`super::RemoteProcedures`].
pub trait advance_puzzle_next_level {
fn advance_puzzle_next_level(&self, input: PuzzleRunNextLevelInput) {
self.advance_puzzle_next_level_then(input, |_, _| {});
fn advance_puzzle_next_level(&self, input: PuzzleRunNextLevelInput,
) {
self.advance_puzzle_next_level_then(input, |_, _| {});
}
fn advance_puzzle_next_level_then(
&self,
input: PuzzleRunNextLevelInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<PuzzleRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<PuzzleRunProcedureResult, __sdk::InternalError>) + Send + 'static,
);
}
@@ -43,17 +46,13 @@ impl advance_puzzle_next_level for super::RemoteProcedures {
&self,
input: PuzzleRunNextLevelInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<PuzzleRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<PuzzleRunProcedureResult, __sdk::InternalError>) + Send + 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, PuzzleRunProcedureResult>(
"advance_puzzle_next_level",
AdvancePuzzleNextLevelArgs { input },
__callback,
);
self.imp.invoke_procedure_with_callback::<_, PuzzleRunProcedureResult>(
"advance_puzzle_next_level",
AdvancePuzzleNextLevelArgs { input, },
__callback,
);
}
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_result_reference_kind_type::AiResultReferenceKind;
@@ -12,10 +17,12 @@ pub struct AiResultReferenceInput {
pub task_id: String,
pub reference_kind: AiResultReferenceKind,
pub reference_id: String,
pub label: Option<String>,
pub label: Option::<String>,
pub created_at_micros: i64,
}
impl __sdk::InModule for AiResultReferenceInput {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
@@ -19,8 +24,12 @@ pub enum AiResultReferenceKind {
RuntimeItemRecord,
AssetObject,
}
impl __sdk::InModule for AiResultReferenceKind {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_result_reference_kind_type::AiResultReferenceKind;
@@ -13,10 +18,12 @@ pub struct AiResultReferenceSnapshot {
pub task_id: String,
pub reference_kind: AiResultReferenceKind,
pub reference_id: String,
pub label: Option<String>,
pub label: Option::<String>,
pub created_at_micros: i64,
}
impl __sdk::InModule for AiResultReferenceSnapshot {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_result_reference_kind_type::AiResultReferenceKind;
@@ -14,14 +19,16 @@ pub struct AiResultReference {
pub task_id: String,
pub reference_kind: AiResultReferenceKind,
pub reference_id: String,
pub label: Option<String>,
pub label: Option::<String>,
pub created_at: __sdk::Timestamp,
}
impl __sdk::InModule for AiResultReference {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `AiResultReference`.
///
/// Provides typed access to columns for query building.
@@ -31,7 +38,7 @@ pub struct AiResultReferenceCols {
pub task_id: __sdk::__query_builder::Col<AiResultReference, String>,
pub reference_kind: __sdk::__query_builder::Col<AiResultReference, AiResultReferenceKind>,
pub reference_id: __sdk::__query_builder::Col<AiResultReference, String>,
pub label: __sdk::__query_builder::Col<AiResultReference, Option<String>>,
pub label: __sdk::__query_builder::Col<AiResultReference, Option::<String>>,
pub created_at: __sdk::__query_builder::Col<AiResultReference, __sdk::Timestamp>,
}
@@ -39,16 +46,14 @@ impl __sdk::__query_builder::HasCols for AiResultReference {
type Cols = AiResultReferenceCols;
fn cols(table_name: &'static str) -> Self::Cols {
AiResultReferenceCols {
result_reference_row_id: __sdk::__query_builder::Col::new(
table_name,
"result_reference_row_id",
),
result_reference_row_id: __sdk::__query_builder::Col::new(table_name, "result_reference_row_id"),
result_ref_id: __sdk::__query_builder::Col::new(table_name, "result_ref_id"),
task_id: __sdk::__query_builder::Col::new(table_name, "task_id"),
reference_kind: __sdk::__query_builder::Col::new(table_name, "reference_kind"),
reference_id: __sdk::__query_builder::Col::new(table_name, "reference_id"),
label: __sdk::__query_builder::Col::new(table_name, "label"),
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
}
}
}
@@ -65,13 +70,12 @@ impl __sdk::__query_builder::HasIxCols for AiResultReference {
type IxCols = AiResultReferenceIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
AiResultReferenceIxCols {
result_reference_row_id: __sdk::__query_builder::IxCol::new(
table_name,
"result_reference_row_id",
),
result_reference_row_id: __sdk::__query_builder::IxCol::new(table_name, "result_reference_row_id"),
task_id: __sdk::__query_builder::IxCol::new(table_name, "task_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for AiResultReference {}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_stage_kind_type::AiTaskStageKind;
@@ -11,12 +16,14 @@ use super::ai_task_stage_kind_type::AiTaskStageKind;
pub struct AiStageCompletionInput {
pub task_id: String,
pub stage_kind: AiTaskStageKind,
pub text_output: Option<String>,
pub structured_payload_json: Option<String>,
pub warning_messages: Vec<String>,
pub text_output: Option::<String>,
pub structured_payload_json: Option::<String>,
pub warning_messages: Vec::<String>,
pub completed_at_micros: i64,
}
impl __sdk::InModule for AiStageCompletionInput {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,13 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
@@ -11,6 +17,8 @@ pub struct AiTaskCancelInput {
pub completed_at_micros: i64,
}
impl __sdk::InModule for AiTaskCancelInput {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_kind_type::AiTaskKind;
use super::ai_task_stage_blueprint_type::AiTaskStageBlueprint;
@@ -15,12 +20,14 @@ pub struct AiTaskCreateInput {
pub owner_user_id: String,
pub request_label: String,
pub source_module: String,
pub source_entity_id: Option<String>,
pub request_payload_json: Option<String>,
pub stages: Vec<AiTaskStageBlueprint>,
pub source_entity_id: Option::<String>,
pub request_payload_json: Option::<String>,
pub stages: Vec::<AiTaskStageBlueprint>,
pub created_at_micros: i64,
}
impl __sdk::InModule for AiTaskCreateInput {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,13 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
@@ -12,6 +18,8 @@ pub struct AiTaskFailureInput {
pub completed_at_micros: i64,
}
impl __sdk::InModule for AiTaskFailureInput {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,13 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
@@ -11,6 +17,8 @@ pub struct AiTaskFinishInput {
pub completed_at_micros: i64,
}
impl __sdk::InModule for AiTaskFinishInput {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
@@ -19,8 +24,12 @@ pub enum AiTaskKind {
QuestIntent,
RuntimeItemIntent,
}
impl __sdk::InModule for AiTaskKind {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_snapshot_type::AiTaskSnapshot;
use super::ai_text_chunk_snapshot_type::AiTextChunkSnapshot;
@@ -11,11 +16,13 @@ use super::ai_text_chunk_snapshot_type::AiTextChunkSnapshot;
#[sats(crate = __lib)]
pub struct AiTaskProcedureResult {
pub ok: bool,
pub task: Option<AiTaskSnapshot>,
pub text_chunk: Option<AiTextChunkSnapshot>,
pub error_message: Option<String>,
pub task: Option::<AiTaskSnapshot>,
pub text_chunk: Option::<AiTextChunkSnapshot>,
pub error_message: Option::<String>,
}
impl __sdk::InModule for AiTaskProcedureResult {
type Module = super::RemoteModule;
}

View File

@@ -2,12 +2,17 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_result_reference_snapshot_type::AiResultReferenceSnapshot;
use super::ai_task_kind_type::AiTaskKind;
use super::ai_task_stage_snapshot_type::AiTaskStageSnapshot;
use super::ai_task_status_type::AiTaskStatus;
use super::ai_task_stage_snapshot_type::AiTaskStageSnapshot;
use super::ai_result_reference_snapshot_type::AiResultReferenceSnapshot;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
@@ -17,21 +22,23 @@ pub struct AiTaskSnapshot {
pub owner_user_id: String,
pub request_label: String,
pub source_module: String,
pub source_entity_id: Option<String>,
pub request_payload_json: Option<String>,
pub source_entity_id: Option::<String>,
pub request_payload_json: Option::<String>,
pub status: AiTaskStatus,
pub failure_message: Option<String>,
pub stages: Vec<AiTaskStageSnapshot>,
pub result_references: Vec<AiResultReferenceSnapshot>,
pub latest_text_output: Option<String>,
pub latest_structured_payload_json: Option<String>,
pub failure_message: Option::<String>,
pub stages: Vec::<AiTaskStageSnapshot>,
pub result_references: Vec::<AiResultReferenceSnapshot>,
pub latest_text_output: Option::<String>,
pub latest_structured_payload_json: Option::<String>,
pub version: u32,
pub created_at_micros: i64,
pub started_at_micros: Option<i64>,
pub completed_at_micros: Option<i64>,
pub started_at_micros: Option::<i64>,
pub completed_at_micros: Option::<i64>,
pub updated_at_micros: i64,
}
impl __sdk::InModule for AiTaskSnapshot {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_stage_kind_type::AiTaskStageKind;
@@ -15,6 +20,8 @@ pub struct AiTaskStageBlueprint {
pub order: u32,
}
impl __sdk::InModule for AiTaskStageBlueprint {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
@@ -17,8 +22,12 @@ pub enum AiTaskStageKind {
NormalizeResult,
PersistResult,
}
impl __sdk::InModule for AiTaskStageKind {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_stage_kind_type::AiTaskStageKind;
use super::ai_task_stage_status_type::AiTaskStageStatus;
@@ -15,13 +20,15 @@ pub struct AiTaskStageSnapshot {
pub detail: String,
pub order: u32,
pub status: AiTaskStageStatus,
pub text_output: Option<String>,
pub structured_payload_json: Option<String>,
pub warning_messages: Vec<String>,
pub started_at_micros: Option<i64>,
pub completed_at_micros: Option<i64>,
pub text_output: Option::<String>,
pub structured_payload_json: Option::<String>,
pub warning_messages: Vec::<String>,
pub started_at_micros: Option::<i64>,
pub completed_at_micros: Option::<i64>,
}
impl __sdk::InModule for AiTaskStageSnapshot {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_stage_kind_type::AiTaskStageKind;
@@ -14,6 +19,8 @@ pub struct AiTaskStageStartInput {
pub started_at_micros: i64,
}
impl __sdk::InModule for AiTaskStageStartInput {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
@@ -15,8 +20,12 @@ pub enum AiTaskStageStatus {
Completed,
Skipped,
}
impl __sdk::InModule for AiTaskStageStatus {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_stage_kind_type::AiTaskStageKind;
use super::ai_task_stage_status_type::AiTaskStageStatus;
@@ -17,17 +22,19 @@ pub struct AiTaskStage {
pub detail: String,
pub stage_order: u32,
pub status: AiTaskStageStatus,
pub text_output: Option<String>,
pub structured_payload_json: Option<String>,
pub warning_messages: Vec<String>,
pub started_at: Option<__sdk::Timestamp>,
pub completed_at: Option<__sdk::Timestamp>,
pub text_output: Option::<String>,
pub structured_payload_json: Option::<String>,
pub warning_messages: Vec::<String>,
pub started_at: Option::<__sdk::Timestamp>,
pub completed_at: Option::<__sdk::Timestamp>,
}
impl __sdk::InModule for AiTaskStage {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `AiTaskStage`.
///
/// Provides typed access to columns for query building.
@@ -39,11 +46,11 @@ pub struct AiTaskStageCols {
pub detail: __sdk::__query_builder::Col<AiTaskStage, String>,
pub stage_order: __sdk::__query_builder::Col<AiTaskStage, u32>,
pub status: __sdk::__query_builder::Col<AiTaskStage, AiTaskStageStatus>,
pub text_output: __sdk::__query_builder::Col<AiTaskStage, Option<String>>,
pub structured_payload_json: __sdk::__query_builder::Col<AiTaskStage, Option<String>>,
pub warning_messages: __sdk::__query_builder::Col<AiTaskStage, Vec<String>>,
pub started_at: __sdk::__query_builder::Col<AiTaskStage, Option<__sdk::Timestamp>>,
pub completed_at: __sdk::__query_builder::Col<AiTaskStage, Option<__sdk::Timestamp>>,
pub text_output: __sdk::__query_builder::Col<AiTaskStage, Option::<String>>,
pub structured_payload_json: __sdk::__query_builder::Col<AiTaskStage, Option::<String>>,
pub warning_messages: __sdk::__query_builder::Col<AiTaskStage, Vec::<String>>,
pub started_at: __sdk::__query_builder::Col<AiTaskStage, Option::<__sdk::Timestamp>>,
pub completed_at: __sdk::__query_builder::Col<AiTaskStage, Option::<__sdk::Timestamp>>,
}
impl __sdk::__query_builder::HasCols for AiTaskStage {
@@ -58,13 +65,11 @@ impl __sdk::__query_builder::HasCols for AiTaskStage {
stage_order: __sdk::__query_builder::Col::new(table_name, "stage_order"),
status: __sdk::__query_builder::Col::new(table_name, "status"),
text_output: __sdk::__query_builder::Col::new(table_name, "text_output"),
structured_payload_json: __sdk::__query_builder::Col::new(
table_name,
"structured_payload_json",
),
structured_payload_json: __sdk::__query_builder::Col::new(table_name, "structured_payload_json"),
warning_messages: __sdk::__query_builder::Col::new(table_name, "warning_messages"),
started_at: __sdk::__query_builder::Col::new(table_name, "started_at"),
completed_at: __sdk::__query_builder::Col::new(table_name, "completed_at"),
}
}
}
@@ -83,8 +88,10 @@ impl __sdk::__query_builder::HasIxCols for AiTaskStage {
AiTaskStageIxCols {
task_id: __sdk::__query_builder::IxCol::new(table_name, "task_id"),
task_stage_id: __sdk::__query_builder::IxCol::new(table_name, "task_stage_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for AiTaskStage {}

View File

@@ -2,7 +2,13 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
@@ -11,6 +17,8 @@ pub struct AiTaskStartInput {
pub started_at_micros: i64,
}
impl __sdk::InModule for AiTaskStartInput {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
@@ -17,8 +22,12 @@ pub enum AiTaskStatus {
Failed,
Cancelled,
}
impl __sdk::InModule for AiTaskStatus {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_kind_type::AiTaskKind;
use super::ai_task_status_type::AiTaskStatus;
@@ -15,23 +20,25 @@ pub struct AiTask {
pub owner_user_id: String,
pub request_label: String,
pub source_module: String,
pub source_entity_id: Option<String>,
pub request_payload_json: Option<String>,
pub source_entity_id: Option::<String>,
pub request_payload_json: Option::<String>,
pub status: AiTaskStatus,
pub failure_message: Option<String>,
pub latest_text_output: Option<String>,
pub latest_structured_payload_json: Option<String>,
pub failure_message: Option::<String>,
pub latest_text_output: Option::<String>,
pub latest_structured_payload_json: Option::<String>,
pub version: u32,
pub created_at: __sdk::Timestamp,
pub started_at: Option<__sdk::Timestamp>,
pub completed_at: Option<__sdk::Timestamp>,
pub started_at: Option::<__sdk::Timestamp>,
pub completed_at: Option::<__sdk::Timestamp>,
pub updated_at: __sdk::Timestamp,
}
impl __sdk::InModule for AiTask {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `AiTask`.
///
/// Provides typed access to columns for query building.
@@ -41,16 +48,16 @@ pub struct AiTaskCols {
pub owner_user_id: __sdk::__query_builder::Col<AiTask, String>,
pub request_label: __sdk::__query_builder::Col<AiTask, String>,
pub source_module: __sdk::__query_builder::Col<AiTask, String>,
pub source_entity_id: __sdk::__query_builder::Col<AiTask, Option<String>>,
pub request_payload_json: __sdk::__query_builder::Col<AiTask, Option<String>>,
pub source_entity_id: __sdk::__query_builder::Col<AiTask, Option::<String>>,
pub request_payload_json: __sdk::__query_builder::Col<AiTask, Option::<String>>,
pub status: __sdk::__query_builder::Col<AiTask, AiTaskStatus>,
pub failure_message: __sdk::__query_builder::Col<AiTask, Option<String>>,
pub latest_text_output: __sdk::__query_builder::Col<AiTask, Option<String>>,
pub latest_structured_payload_json: __sdk::__query_builder::Col<AiTask, Option<String>>,
pub failure_message: __sdk::__query_builder::Col<AiTask, Option::<String>>,
pub latest_text_output: __sdk::__query_builder::Col<AiTask, Option::<String>>,
pub latest_structured_payload_json: __sdk::__query_builder::Col<AiTask, Option::<String>>,
pub version: __sdk::__query_builder::Col<AiTask, u32>,
pub created_at: __sdk::__query_builder::Col<AiTask, __sdk::Timestamp>,
pub started_at: __sdk::__query_builder::Col<AiTask, Option<__sdk::Timestamp>>,
pub completed_at: __sdk::__query_builder::Col<AiTask, Option<__sdk::Timestamp>>,
pub started_at: __sdk::__query_builder::Col<AiTask, Option::<__sdk::Timestamp>>,
pub completed_at: __sdk::__query_builder::Col<AiTask, Option::<__sdk::Timestamp>>,
pub updated_at: __sdk::__query_builder::Col<AiTask, __sdk::Timestamp>,
}
@@ -64,22 +71,17 @@ impl __sdk::__query_builder::HasCols for AiTask {
request_label: __sdk::__query_builder::Col::new(table_name, "request_label"),
source_module: __sdk::__query_builder::Col::new(table_name, "source_module"),
source_entity_id: __sdk::__query_builder::Col::new(table_name, "source_entity_id"),
request_payload_json: __sdk::__query_builder::Col::new(
table_name,
"request_payload_json",
),
request_payload_json: __sdk::__query_builder::Col::new(table_name, "request_payload_json"),
status: __sdk::__query_builder::Col::new(table_name, "status"),
failure_message: __sdk::__query_builder::Col::new(table_name, "failure_message"),
latest_text_output: __sdk::__query_builder::Col::new(table_name, "latest_text_output"),
latest_structured_payload_json: __sdk::__query_builder::Col::new(
table_name,
"latest_structured_payload_json",
),
latest_structured_payload_json: __sdk::__query_builder::Col::new(table_name, "latest_structured_payload_json"),
version: __sdk::__query_builder::Col::new(table_name, "version"),
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
started_at: __sdk::__query_builder::Col::new(table_name, "started_at"),
completed_at: __sdk::__query_builder::Col::new(table_name, "completed_at"),
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
}
}
}
@@ -102,8 +104,10 @@ impl __sdk::__query_builder::HasIxCols for AiTask {
status: __sdk::__query_builder::IxCol::new(table_name, "status"),
task_id: __sdk::__query_builder::IxCol::new(table_name, "task_id"),
task_kind: __sdk::__query_builder::IxCol::new(table_name, "task_kind"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for AiTask {}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_stage_kind_type::AiTaskStageKind;
@@ -16,6 +21,8 @@ pub struct AiTextChunkAppendInput {
pub created_at_micros: i64,
}
impl __sdk::InModule for AiTextChunkAppendInput {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_stage_kind_type::AiTaskStageKind;
@@ -17,6 +22,8 @@ pub struct AiTextChunkSnapshot {
pub created_at_micros: i64,
}
impl __sdk::InModule for AiTextChunkSnapshot {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_stage_kind_type::AiTaskStageKind;
@@ -18,10 +23,12 @@ pub struct AiTextChunk {
pub created_at: __sdk::Timestamp,
}
impl __sdk::InModule for AiTextChunk {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `AiTextChunk`.
///
/// Provides typed access to columns for query building.
@@ -46,6 +53,7 @@ impl __sdk::__query_builder::HasCols for AiTextChunk {
sequence: __sdk::__query_builder::Col::new(table_name, "sequence"),
delta_text: __sdk::__query_builder::Col::new(table_name, "delta_text"),
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
}
}
}
@@ -64,8 +72,10 @@ impl __sdk::__query_builder::HasIxCols for AiTextChunk {
AiTextChunkIxCols {
task_id: __sdk::__query_builder::IxCol::new(table_name, "task_id"),
text_chunk_row_id: __sdk::__query_builder::IxCol::new(table_name, "text_chunk_row_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for AiTextChunk {}

View File

@@ -2,17 +2,23 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::ai_task_procedure_result_type::AiTaskProcedureResult;
use super::ai_text_chunk_append_input_type::AiTextChunkAppendInput;
use super::ai_task_procedure_result_type::AiTaskProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct AppendAiTextChunkAndReturnArgs {
struct AppendAiTextChunkAndReturnArgs {
pub input: AiTextChunkAppendInput,
}
impl __sdk::InModule for AppendAiTextChunkAndReturnArgs {
type Module = super::RemoteModule;
}
@@ -22,19 +28,16 @@ impl __sdk::InModule for AppendAiTextChunkAndReturnArgs {
///
/// Implemented for [`super::RemoteProcedures`].
pub trait append_ai_text_chunk_and_return {
fn append_ai_text_chunk_and_return(&self, input: AiTextChunkAppendInput) {
self.append_ai_text_chunk_and_return_then(input, |_, _| {});
fn append_ai_text_chunk_and_return(&self, input: AiTextChunkAppendInput,
) {
self.append_ai_text_chunk_and_return_then(input, |_, _| {});
}
fn append_ai_text_chunk_and_return_then(
&self,
input: AiTextChunkAppendInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<AiTaskProcedureResult, __sdk::InternalError>) + Send + 'static,
);
}
@@ -43,17 +46,13 @@ impl append_ai_text_chunk_and_return for super::RemoteProcedures {
&self,
input: AiTextChunkAppendInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AiTaskProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<AiTaskProcedureResult, __sdk::InternalError>) + Send + 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, AiTaskProcedureResult>(
"append_ai_text_chunk_and_return",
AppendAiTextChunkAndReturnArgs { input },
__callback,
);
self.imp.invoke_procedure_with_callback::<_, AiTaskProcedureResult>(
"append_ai_text_chunk_and_return",
AppendAiTextChunkAndReturnArgs { input, },
__callback,
);
}
}

View File

@@ -2,17 +2,23 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::chapter_progression_ledger_input_type::ChapterProgressionLedgerInput;
use super::chapter_progression_procedure_result_type::ChapterProgressionProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct ApplyChapterProgressionLedgerEntryAndReturnArgs {
struct ApplyChapterProgressionLedgerEntryAndReturnArgs {
pub input: ChapterProgressionLedgerInput,
}
impl __sdk::InModule for ApplyChapterProgressionLedgerEntryAndReturnArgs {
type Module = super::RemoteModule;
}
@@ -22,22 +28,16 @@ impl __sdk::InModule for ApplyChapterProgressionLedgerEntryAndReturnArgs {
///
/// Implemented for [`super::RemoteProcedures`].
pub trait apply_chapter_progression_ledger_entry_and_return {
fn apply_chapter_progression_ledger_entry_and_return(
&self,
input: ChapterProgressionLedgerInput,
) {
self.apply_chapter_progression_ledger_entry_and_return_then(input, |_, _| {});
fn apply_chapter_progression_ledger_entry_and_return(&self, input: ChapterProgressionLedgerInput,
) {
self.apply_chapter_progression_ledger_entry_and_return_then(input, |_, _| {});
}
fn apply_chapter_progression_ledger_entry_and_return_then(
&self,
input: ChapterProgressionLedgerInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<ChapterProgressionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<ChapterProgressionProcedureResult, __sdk::InternalError>) + Send + 'static,
);
}
@@ -46,17 +46,13 @@ impl apply_chapter_progression_ledger_entry_and_return for super::RemoteProcedur
&self,
input: ChapterProgressionLedgerInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<ChapterProgressionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<ChapterProgressionProcedureResult, __sdk::InternalError>) + Send + 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, ChapterProgressionProcedureResult>(
"apply_chapter_progression_ledger_entry_and_return",
ApplyChapterProgressionLedgerEntryAndReturnArgs { input },
__callback,
);
self.imp.invoke_procedure_with_callback::<_, ChapterProgressionProcedureResult>(
"apply_chapter_progression_ledger_entry_and_return",
ApplyChapterProgressionLedgerEntryAndReturnArgs { input, },
__callback,
);
}
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::chapter_progression_ledger_input_type::ChapterProgressionLedgerInput;
@@ -14,8 +19,10 @@ pub(super) struct ApplyChapterProgressionLedgerEntryArgs {
impl From<ApplyChapterProgressionLedgerEntryArgs> for super::Reducer {
fn from(args: ApplyChapterProgressionLedgerEntryArgs) -> Self {
Self::ApplyChapterProgressionLedgerEntry { input: args.input }
}
Self::ApplyChapterProgressionLedgerEntry {
input: args.input,
}
}
}
impl __sdk::InModule for ApplyChapterProgressionLedgerEntryArgs {
@@ -33,11 +40,9 @@ pub trait apply_chapter_progression_ledger_entry {
/// The reducer will run asynchronously in the future,
/// and this method provides no way to listen for its completion status.
/// /// Use [`apply_chapter_progression_ledger_entry:apply_chapter_progression_ledger_entry_then`] to run a callback after the reducer completes.
fn apply_chapter_progression_ledger_entry(
&self,
input: ChapterProgressionLedgerInput,
) -> __sdk::Result<()> {
self.apply_chapter_progression_ledger_entry_then(input, |_, _| {})
fn apply_chapter_progression_ledger_entry(&self, input: ChapterProgressionLedgerInput,
) -> __sdk::Result<()> {
self.apply_chapter_progression_ledger_entry_then(input, |_, _| {})
}
/// Request that the remote module invoke the reducer `apply_chapter_progression_ledger_entry` to run as soon as possible,
@@ -65,9 +70,7 @@ impl apply_chapter_progression_ledger_entry for super::RemoteReducers {
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp.invoke_reducer_with_callback(
ApplyChapterProgressionLedgerEntryArgs { input },
callback,
)
self.imp.invoke_reducer_with_callback(ApplyChapterProgressionLedgerEntryArgs { input, }, callback)
}
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::inventory_mutation_input_type::InventoryMutationInput;
@@ -14,8 +19,10 @@ pub(super) struct ApplyInventoryMutationArgs {
impl From<ApplyInventoryMutationArgs> for super::Reducer {
fn from(args: ApplyInventoryMutationArgs) -> Self {
Self::ApplyInventoryMutation { input: args.input }
}
Self::ApplyInventoryMutation {
input: args.input,
}
}
}
impl __sdk::InModule for ApplyInventoryMutationArgs {
@@ -33,8 +40,9 @@ pub trait apply_inventory_mutation {
/// The reducer will run asynchronously in the future,
/// and this method provides no way to listen for its completion status.
/// /// Use [`apply_inventory_mutation:apply_inventory_mutation_then`] to run a callback after the reducer completes.
fn apply_inventory_mutation(&self, input: InventoryMutationInput) -> __sdk::Result<()> {
self.apply_inventory_mutation_then(input, |_, _| {})
fn apply_inventory_mutation(&self, input: InventoryMutationInput,
) -> __sdk::Result<()> {
self.apply_inventory_mutation_then(input, |_, _| {})
}
/// Request that the remote module invoke the reducer `apply_inventory_mutation` to run as soon as possible,
@@ -62,7 +70,7 @@ impl apply_inventory_mutation for super::RemoteReducers {
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp
.invoke_reducer_with_callback(ApplyInventoryMutationArgs { input }, callback)
self.imp.invoke_reducer_with_callback(ApplyInventoryMutationArgs { input, }, callback)
}
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::quest_signal_apply_input_type::QuestSignalApplyInput;
@@ -14,8 +19,10 @@ pub(super) struct ApplyQuestSignalArgs {
impl From<ApplyQuestSignalArgs> for super::Reducer {
fn from(args: ApplyQuestSignalArgs) -> Self {
Self::ApplyQuestSignal { input: args.input }
}
Self::ApplyQuestSignal {
input: args.input,
}
}
}
impl __sdk::InModule for ApplyQuestSignalArgs {
@@ -33,8 +40,9 @@ pub trait apply_quest_signal {
/// The reducer will run asynchronously in the future,
/// and this method provides no way to listen for its completion status.
/// /// Use [`apply_quest_signal:apply_quest_signal_then`] to run a callback after the reducer completes.
fn apply_quest_signal(&self, input: QuestSignalApplyInput) -> __sdk::Result<()> {
self.apply_quest_signal_then(input, |_, _| {})
fn apply_quest_signal(&self, input: QuestSignalApplyInput,
) -> __sdk::Result<()> {
self.apply_quest_signal_then(input, |_, _| {})
}
/// Request that the remote module invoke the reducer `apply_quest_signal` to run as soon as possible,
@@ -62,7 +70,7 @@ impl apply_quest_signal for super::RemoteReducers {
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp
.invoke_reducer_with_callback(ApplyQuestSignalArgs { input }, callback)
self.imp.invoke_reducer_with_callback(ApplyQuestSignalArgs { input, }, callback)
}
}

View File

@@ -2,7 +2,13 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
@@ -13,11 +19,13 @@ pub struct AssetEntityBindingInput {
pub entity_id: String,
pub slot: String,
pub asset_kind: String,
pub owner_user_id: Option<String>,
pub profile_id: Option<String>,
pub owner_user_id: Option::<String>,
pub profile_id: Option::<String>,
pub updated_at_micros: i64,
}
impl __sdk::InModule for AssetEntityBindingInput {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,12 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
use super::asset_entity_binding_snapshot_type::AssetEntityBindingSnapshot;
@@ -10,10 +15,12 @@ use super::asset_entity_binding_snapshot_type::AssetEntityBindingSnapshot;
#[sats(crate = __lib)]
pub struct AssetEntityBindingProcedureResult {
pub ok: bool,
pub record: Option<AssetEntityBindingSnapshot>,
pub error_message: Option<String>,
pub record: Option::<AssetEntityBindingSnapshot>,
pub error_message: Option::<String>,
}
impl __sdk::InModule for AssetEntityBindingProcedureResult {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,13 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
@@ -13,12 +19,14 @@ pub struct AssetEntityBindingSnapshot {
pub entity_id: String,
pub slot: String,
pub asset_kind: String,
pub owner_user_id: Option<String>,
pub profile_id: Option<String>,
pub owner_user_id: Option::<String>,
pub profile_id: Option::<String>,
pub created_at_micros: i64,
pub updated_at_micros: i64,
}
impl __sdk::InModule for AssetEntityBindingSnapshot {
type Module = super::RemoteModule;
}

View File

@@ -2,7 +2,13 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
@@ -13,16 +19,18 @@ pub struct AssetEntityBinding {
pub entity_id: String,
pub slot: String,
pub asset_kind: String,
pub owner_user_id: Option<String>,
pub profile_id: Option<String>,
pub owner_user_id: Option::<String>,
pub profile_id: Option::<String>,
pub created_at: __sdk::Timestamp,
pub updated_at: __sdk::Timestamp,
}
impl __sdk::InModule for AssetEntityBinding {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `AssetEntityBinding`.
///
/// Provides typed access to columns for query building.
@@ -33,8 +41,8 @@ pub struct AssetEntityBindingCols {
pub entity_id: __sdk::__query_builder::Col<AssetEntityBinding, String>,
pub slot: __sdk::__query_builder::Col<AssetEntityBinding, String>,
pub asset_kind: __sdk::__query_builder::Col<AssetEntityBinding, String>,
pub owner_user_id: __sdk::__query_builder::Col<AssetEntityBinding, Option<String>>,
pub profile_id: __sdk::__query_builder::Col<AssetEntityBinding, Option<String>>,
pub owner_user_id: __sdk::__query_builder::Col<AssetEntityBinding, Option::<String>>,
pub profile_id: __sdk::__query_builder::Col<AssetEntityBinding, Option::<String>>,
pub created_at: __sdk::__query_builder::Col<AssetEntityBinding, __sdk::Timestamp>,
pub updated_at: __sdk::__query_builder::Col<AssetEntityBinding, __sdk::Timestamp>,
}
@@ -53,6 +61,7 @@ impl __sdk::__query_builder::HasCols for AssetEntityBinding {
profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"),
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
}
}
}
@@ -71,8 +80,10 @@ impl __sdk::__query_builder::HasIxCols for AssetEntityBinding {
AssetEntityBindingIxCols {
asset_object_id: __sdk::__query_builder::IxCol::new(table_name, "asset_object_id"),
binding_id: __sdk::__query_builder::IxCol::new(table_name, "binding_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for AssetEntityBinding {}

View File

@@ -2,7 +2,13 @@
// 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 spacetimedb_sdk::__codegen::{
self as __sdk,
__lib,
__sats,
__ws,
};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
@@ -10,13 +16,15 @@ pub struct AssetHistoryEntrySnapshot {
pub asset_object_id: String,
pub asset_kind: String,
pub image_src: String,
pub owner_user_id: Option<String>,
pub profile_id: Option<String>,
pub entity_id: Option<String>,
pub owner_user_id: Option::<String>,
pub profile_id: Option::<String>,
pub entity_id: Option::<String>,
pub created_at_micros: i64,
pub updated_at_micros: i64,
}
impl __sdk::InModule for AssetHistoryEntrySnapshot {
type Module = super::RemoteModule;
}

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