feat: restore generation draft persistence
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
# AI 生成过程草稿持久化设计(2026-04-24)
|
||||
|
||||
## 1. 背景
|
||||
|
||||
当前创作类模板已经具备 session / message / operation 级别的最终态落库能力,但部分流式生成只把模型增量推给前端。若 HTTP/SSE 连接、浏览器页面或 LLM 请求在最终解析前中断,用户只能看到短暂流式文本,服务端缺少可恢复的生成中间态。
|
||||
|
||||
本设计补齐“生成过程中已经生成的内容必须持续持久化”的机制,并要求该机制对所有创作模板统一生效。
|
||||
|
||||
## 2. 目标
|
||||
|
||||
1. 每次模板生成开始前创建或绑定一个 `ai_task`。
|
||||
2. 模型每次产出可见文本增量时,写入 `ai_text_chunk`,并同步更新 `ai_task.latest_text_output` 与对应 stage 的 `text_output`。
|
||||
3. 生成失败或连接中断时,不丢弃已经落库的 chunk;后续可用 `ai_task.latest_text_output` 作为续写上下文。
|
||||
4. 成功解析并 finalize 后,将最终结构化结果继续写回各模板原有 session 表,保持现有业务快照不变。
|
||||
|
||||
## 3. 统一落库边界
|
||||
|
||||
### 3.1 真相表
|
||||
|
||||
- `ai_task`:记录一次模板生成任务的业务来源、状态、最新聚合文本、结构化结果。
|
||||
- `ai_task_stage`:记录模板生成阶段状态;当前创作对话统一使用 `DraftGeneration`。
|
||||
- `ai_text_chunk`:按 `sequence` 追加保存模型增量文本,是断点恢复的最小粒度。
|
||||
|
||||
### 3.2 适用模板
|
||||
|
||||
- 自定义世界创作 Agent。
|
||||
- 解谜游戏创作 Agent。
|
||||
- 大鱼吃小鱼创作 Agent。
|
||||
- 后续新增模板必须复用同一生成草稿持久化工具,不允许只在 UI 内存保存流式文本。
|
||||
|
||||
## 4. 续写策略
|
||||
|
||||
1. 发起生成时,后端根据 `template_key + session_id + operation_id` 创建稳定 `task_id`。
|
||||
2. LLM 流式回调收到 `replyText` 的最新可见文本后,计算相对上一次文本的增量;只有非空增量写入 `ai_text_chunk`。
|
||||
3. 写入失败不应阻断当前生成主流程,但必须记录 warn 日志,避免因持久化瞬时失败导致用户生成直接失败。
|
||||
4. 若最终解析失败,`ai_task` 保持 `Running` 或显式 `Failed`,已写入的 `latest_text_output` 仍可作为下一轮 prompt 的“已生成草稿”。
|
||||
5. 下一轮续写 prompt 应优先带上最近未完成任务的 `latest_text_output`;本次先落地服务端 chunk 持久化能力,后续模板 prompt 可逐步消费该草稿。
|
||||
|
||||
## 5. 编码要求
|
||||
|
||||
1. 持久化逻辑放在 `server-rs/crates/api-server` 的通用工具中,由各模板路由接入。
|
||||
2. 不引入 `server-node` 兼容分支。
|
||||
3. SpacetimeDB 写入必须通过 `spacetime-client` 已生成绑定,不在 reducer 中访问网络或文件系统。
|
||||
4. 所有新增 Rust 代码保留中文注释,且只做局部修改,避免重写包含中文的大文件。
|
||||
|
||||
## 6. 失败排查原文日志
|
||||
|
||||
1. RPG 草稿生成链路的模型输入与模型输出原文日志统一收口在 `platform-llm` 网关层,避免每个模板调用点重复实现。
|
||||
2. 只有发生请求失败、上游非 2xx、响应读取失败、JSON/SSE 解析失败或空响应时,才将本次模型输入与已拿到的模型输出原文分别写入文件;正常成功生成不默认落盘原文,避免日志体积不可控。
|
||||
3. 日志目录默认使用仓库运行目录下的 `logs/llm-raw`,可通过 `LLM_RAW_LOG_DIR` 覆盖;每次失败写成同一 trace 前缀下的 `*.input.json` 与 `*.output.txt` 两个 UTF-8 文件。
|
||||
4. `*.input.json` 记录 provider、model、stream、attempt、maxTokens 与完整 messages;`*.output.txt` 记录上游 HTTP 原文、非流式响应原文、SSE 原始事件文本,或请求尚未到达上游时的错误摘要。
|
||||
5. 文件名只使用时间戳、进程号、递增序号与安全化错误阶段,不包含用户输入、sessionId 或 API key;输入 JSON 不写入 API key。
|
||||
6. 文件日志失败只写 warn,不影响草稿生成主错误返回;该日志仅用于本地开发与排障,不作为 SpacetimeDB 真相态。
|
||||
@@ -0,0 +1,35 @@
|
||||
# 创作 Agent 发布门槛结果页归一化回写修正
|
||||
|
||||
日期:`2026-04-24`
|
||||
|
||||
## 1. 问题现象
|
||||
|
||||
`custom_world.publish_gate` 诊断日志显示:
|
||||
|
||||
1. `has_draft_profile=true`
|
||||
2. `has_result_preview=true`
|
||||
3. `has_world_hook=true`
|
||||
4. `has_core_conflicts=true`
|
||||
5. 但仍存在 `publish_missing_player_premise / publish_missing_main_chapter / publish_missing_first_act`
|
||||
|
||||
这说明接口可正常读取 session,问题不在 `GET /api/runtime/custom-world/agent/sessions/:sessionId` 本身,而在结果页 profile 回写到 session 时,发布门槛需要的部分结构字段没有稳定保留下来。
|
||||
|
||||
## 2. 根因
|
||||
|
||||
前端结果页通过 `normalizeCustomWorldProfileRecord` 把 `resultPreview.preview` 转成 `CustomWorldProfile`。该归一化模型原本主要服务作品库与运行时展示,只保留了 `settingText / summary / playerGoal / creatorIntent / anchorContent / sceneChapterBlueprints` 等字段,没有把后端发布门槛直接读取的顶层 `worldHook / playerPremise` 纳入 `CustomWorldProfile` 稳定字段。
|
||||
|
||||
当自动保存或发布前执行 `sync_result_profile` 时,前端会把归一化后的 profile 传回 SpacetimeDB。若这份 profile 中缺少顶层 `playerPremise`,且 `creatorIntent / anchorContent` 又未包含可读玩家切入字段,后端最终 publish gate 会继续报 `publish_missing_player_premise`。
|
||||
|
||||
## 3. 修复口径
|
||||
|
||||
1. `CustomWorldProfile` 显式声明 `worldHook / playerPremise` 为 Agent 发布快照兼容字段。
|
||||
2. `normalizeCustomWorldProfileRecord` 保留顶层 `worldHook / playerPremise`,并在缺失时从 `creatorIntent.worldHook / creatorIntent.playerPremise / summary / playerGoal` 做最小回填。
|
||||
3. 不在 UI 新增规则说明文案;这两个字段只作为后端发布门槛与 session 回写的稳定数据槽位。
|
||||
4. 后端 publish gate 继续以 SpacetimeDB 中的 `draft_profile_json` 为最终真相源,前端只负责把结果页当前 profile 完整同步回去。
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
1. 从 `resultPreview.preview` 构建结果页 profile 后,`worldHook / playerPremise` 不会被前端归一化丢弃。
|
||||
2. 自动保存或点击发布前执行 `sync_result_profile` 时,传回后端的 profile 保留发布门槛所需顶层字段。
|
||||
3. 若当前草稿确实包含玩家切入与 `sceneChapterBlueprints[*].acts`,后端诊断日志不应再出现对应结构 blocker。
|
||||
4. 若草稿真实缺失章节或第一幕,`publish_missing_main_chapter / publish_missing_first_act` 仍应保留,不做前端假放行。
|
||||
@@ -52,3 +52,14 @@
|
||||
- 不再把 `server-node/src/prompts/characterAssetPrompts.ts` 作为主链修改目标。
|
||||
- 默认描述字段必须由世界草稿生成阶段写入,前端只负责把字段填入输入框并允许用户编辑。
|
||||
- UI 不默认展示规则解释文案,正式约束只进入后端 prompt。
|
||||
|
||||
## 5. 自动草稿素材回写约束
|
||||
|
||||
- 世界草稿自动素材生成与草稿页手动生成使用同一套 `server-rs/crates/api-server/src/custom_world_ai.rs` 场景图接口和 OSS/SpacetimeDB 资产持久化链路。
|
||||
- 自动批量生成幕背景时,后端必须把已成功生成的 `backgroundImageSrc/backgroundAssetId/generatedScenePrompt/generatedSceneModel` 写回 `sceneChapterBlueprints[*].acts[*]`,不能因为同批某一幕失败而丢弃已成功图片。
|
||||
- 某一幕连续重试仍失败时,只允许在该幕写入 `backgroundGenerationError` 作为诊断字段;只要至少一幕成功,草稿仍应完成并让前端展示成功图片。
|
||||
- 只有全部幕背景均失败时,才把“生成幕背景图失败”作为草稿素材阶段失败原因保存。
|
||||
- Rust 服务实际生图模型读取 `DASHSCOPE_SCENE_IMAGE_MODEL` / `DASHSCOPE_COVER_IMAGE_MODEL` / `DASHSCOPE_REFERENCE_IMAGE_MODEL`;兼容旧 `DASHSCOPE_IMAGE_MODEL`,避免 `.env.example` 中配置了模型但服务端仍使用硬编码模型。
|
||||
- 自动草稿幕背景不能把 `backgroundPromptText` 直接作为最终 `prompt` 传给 DashScope;它必须像草稿页手动生成一样,把幕级描述作为 `userPrompt`,并用同一个地点对象的 `name/description/dangerLevel` 作为场景上下文,再由 `build_custom_world_scene_image_prompt` 统一拼入世界名、世界摘要、风格、玩家目标、场景名、场景描述和负面词。用户不修改默认描述直接点生成时,手动生成与自动草稿生成的正式生图上下文必须一致。
|
||||
- 自动草稿幕背景的默认尺寸必须与草稿页手动生成默认尺寸一致,当前统一为 `1280*720`;不能在自动链路中单独改成 `1600*900`,否则同一 prompt 在同一模型下也可能因供应商尺寸支持或耗时不同而表现不一致。
|
||||
- 批量自动生图失败日志必须保留 `AppError.details.message` 中的供应商真实原因,不能只记录 `AppError.message()` 的 HTTP 泛化文案,否则排查时只能看到“上游服务请求失败”,无法确认是尺寸、模型、限流、超时还是内容审核失败。
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# 世界草稿图片生成与预览补齐说明
|
||||
|
||||
更新时间:`2026-04-24`
|
||||
|
||||
## 1. 检查结论
|
||||
|
||||
当前 server-rs 的世界底稿生成链路已经在 `draft_foundation` 后台任务中补齐两类图片:
|
||||
|
||||
1. `playableNpcs` 与 `storyNpcs` 中的每个角色都会调用角色主形象生成链路,并把 `imageSrc`、`generatedVisualAssetId` 写回底稿。
|
||||
2. `sceneChapterBlueprints[].acts[]` 中的每一幕都会调用场景图生成链路,并把 `backgroundImageSrc`、`backgroundAssetId`、生成提示词与模型信息写回底稿。
|
||||
|
||||
图片生成后不落本地真值,而是通过 OSS `put_object -> head_object -> confirm_asset_object -> bind_asset_object_to_entity` 确认对象,并用兼容的 `/generated-*` 路径供前端读取。
|
||||
|
||||
## 2. 前端缺口
|
||||
|
||||
结果页的场景列表此前只把每个场景的第一张幕背景图作为场景卡封面。这样虽然后端已经生成了每一幕图片,但用户只能看到第一幕,无法在结果页确认同一场景下其他幕的图片是否存在。
|
||||
|
||||
## 3. 本次落地
|
||||
|
||||
1. 在 `CustomWorldEntityCatalog` 中增加每幕图片缩略条,来源为当前场景匹配到的 `sceneChapterBlueprints[].acts[].backgroundImageSrc`。
|
||||
2. 保留原来的场景卡封面策略:第一幕背景图仍作为主封面,旧的场景图字段继续作为兜底。
|
||||
3. 缩略条只展示已生成图片的幕,不额外暴露章节结构文本,避免结果页变成规则说明面板。
|
||||
4. 增加结果页测试,覆盖同一场景下两幕背景图都能在前端以图片形式预览。
|
||||
|
||||
## 4. 验收点
|
||||
|
||||
1. 生成世界草稿完成后,角色页签中所有可扮演角色和场景角色能展示 `imageSrc`。
|
||||
2. 场景页签中,每个场景卡片仍展示主封面。
|
||||
3. 场景卡片下方能横向预览该场景所有已生成幕背景图。
|
||||
4. OSS 未配置或上传失败时,后端任务应失败并把错误写入 operation,而不是生成伪本地路径。
|
||||
|
||||
## 5. 上游图片服务失败降级
|
||||
|
||||
`draft_foundation` 的底稿文本结构是进入结果页和继续编辑的主产物,角色主图、幕背景图属于可后补资产。若 DashScope 或 OSS 上游临时不可用,后台任务不应把整份底稿标记为失败。
|
||||
|
||||
本次补充后:
|
||||
|
||||
1. 角色主图分支失败时,operation 记录错误信息并继续使用未带角色图的底稿。
|
||||
2. 幕背景图分支失败时,operation 记录错误信息并继续使用未带幕图的底稿。
|
||||
3. 已成功的并行资产分支仍会合并回底稿,不会被失败分支覆盖。
|
||||
4. 后续可通过资产工坊或单项生成动作补齐缺失图片。
|
||||
@@ -7,6 +7,8 @@
|
||||
- [BIG_FISH_DIRECTION_TOUCH_CONTROL_2026-04-24.md](./BIG_FISH_DIRECTION_TOUCH_CONTROL_2026-04-24.md):记录大鱼吃小鱼从固定摇杆改为屏幕首触点方向控制,并要求本地直达局在未操作时保持对象运动。
|
||||
- [BIG_FISH_DIRECT_ROUTE_PLAYGROUND_2026-04-24.md](./BIG_FISH_DIRECT_ROUTE_PLAYGROUND_2026-04-24.md):记录 `/big-fish` 大鱼吃小鱼玩法直达入口,明确复用现有 `BigFishRuntimeShell` 和本地占位运行态的调试边界。
|
||||
- [PUZZLE_DIRECT_ROUTE_PLAYGROUND_2026-04-24.md](./PUZZLE_DIRECT_ROUTE_PLAYGROUND_2026-04-24.md):记录 `/puzzle` 拼图玩法直达入口,明确复用现有 `PuzzleRuntimeShell` 和本地占位图运行态的调试边界。
|
||||
- [CREATION_AGENT_PUBLISH_GATE_NORMALIZE_WRITEBACK_FIX_2026-04-24.md](./CREATION_AGENT_PUBLISH_GATE_NORMALIZE_WRITEBACK_FIX_2026-04-24.md):记录结果页 profile 归一化回写丢失顶层 `worldHook / playerPremise` 导致 publish gate 继续误报结构 blocker 的根因,并冻结前端归一化保留发布字段的修复口径。
|
||||
|
||||
- [CUSTOM_WORLD_RESULT_ENTITY_GENERATION_FIX_2026-04-24.md](./CUSTOM_WORLD_RESULT_ENTITY_GENERATION_FIX_2026-04-24.md):记录世界结果页在 Agent 草稿模式下新增场景、新增 NPC 生成成功但结果页字段不可用的根因,并冻结 `api-server` 生成归一化层补齐 profile 字段的修复口径。
|
||||
- [ADMIN_CONSOLE_SERVICE_DESIGN_2026-04-23.md](./ADMIN_CONSOLE_SERVICE_DESIGN_2026-04-23.md):冻结 Rust `api-server` 内后台管理服务首版方案,明确管理员用户名密码登录、管理员 JWT 鉴权、数据库概览、受控 API 调试台与同源管理页面的落地边界。
|
||||
- [SPACETIME_MODULE_LIB_RS_SPLIT_EXECUTION_2026-04-23.md](./SPACETIME_MODULE_LIB_RS_SPLIT_EXECUTION_2026-04-23.md):冻结 `server-rs/crates/spacetime-module/src/lib.rs` 的模块地图、二级落位点与迁移顺序,要求后续 SpacetimeDB 主工程改动按对应模块落位,不再继续堆回单大文件。
|
||||
@@ -164,4 +166,3 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# RPG 生成流程刷新恢复与即时持久化设计(2026-04-24)
|
||||
|
||||
## 背景
|
||||
- RPG 共创从 Agent 聊天页触发 `draft_foundation` 后进入生成过程页。
|
||||
- 旧实现只持久化 `activeSessionId` 与 `activeOperationId`,刷新时恢复入口会无条件回到 Agent 聊天页。
|
||||
- operation 失败后继续创作也会因为 operation 指针被清空而缺失生成页上下文。
|
||||
|
||||
## 目标
|
||||
1. 生成中刷新网页后仍停留在生成过程页。
|
||||
2. 生成完成后结果页内容第一时间落入作品持久化链路。
|
||||
3. 生成失败后从创作入口继续处理该草稿时,优先回到生成过程页展示失败状态,而不是 Agent 聊天页。
|
||||
|
||||
## 落地规则
|
||||
- 前端只保存恢复指针,不在 UI 持久层复制世界数据。
|
||||
- `sessionStorage` 与 URL query 中增加生成页来源字段 `customWorldGenerationSource`,当前仅支持 `agent-draft-foundation`。
|
||||
- 初始恢复时:
|
||||
- 若存在 `activeOperationId` 且来源为 `agent-draft-foundation`,先进入 `custom-world-generating`。
|
||||
- 否则若 session 已经可构建结果预览,进入 `custom-world-result`。
|
||||
- 其他情况进入 `agent-workspace`。
|
||||
- operation 进入 `completed` 或 `failed` 后仍保留 `activeOperationId`,直到用户离开、重新发起操作或清理工作区,保证刷新和继续创作能恢复完成/失败状态。
|
||||
- 生成完成后由 `useRpgCreationResultAutosave` 在结果页立即保存。生成页跳结果页前必须先同步最新 session 并写入 `generatedCustomWorldProfile`,确保自动保存消费的是最新快照。
|
||||
|
||||
## 验收点
|
||||
- 生成中刷新:URL/sessionStorage 可恢复 `custom-world-generating`,页面显示“世界草稿生成进度”。
|
||||
- 生成失败刷新或继续创作:页面仍显示生成过程页和失败信息,不展示 Agent 聊天页。
|
||||
- 生成完成:跳到结果页后触发 `upsertRpgWorldProfile`,保存请求带 `sourceAgentSessionId`。
|
||||
@@ -37,8 +37,9 @@ npm run dev:rust
|
||||
4. 等待 `spacetime --root-dir=server-rs/.spacetimedb/local server ping http://127.0.0.1:3101` 可用。
|
||||
5. 执行 `spacetime --root-dir=server-rs/.spacetimedb/local publish <本地数据库名> --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module -c=on-conflict --yes`,确保 publish 的签名身份与 standalone 的本地控制库一致,并在当前开发阶段允许新版模块表结构变化且发生 schema 冲突时清除旧模块数据。
|
||||
6. 注入 `GENARRATIVE_API_*` 与 `GENARRATIVE_SPACETIME_*` 后启动 `cargo run -p api-server`;直接运行 `api-server` 时,如未显式设置 `GENARRATIVE_SPACETIME_DATABASE`,服务端也会向上查找 `spacetime.local.json` 作为本地默认库名。
|
||||
7. 注入 `GENARRATIVE_BACKEND_STACK=rust`、`RUST_SERVER_TARGET`、`GENARRATIVE_RUNTIME_SERVER_TARGET` 后启动 Vite。
|
||||
8. 任一子进程退出时,脚本回收其余子进程。
|
||||
7. 等待 `http://127.0.0.1:<api-port>/healthz` 返回 HTTP 响应后再启动 Vite,避免前端初始化请求早于 Rust `api-server` 监听完成并在终端刷出 `ECONNREFUSED 127.0.0.1:<api-port>`。
|
||||
8. 注入 `GENARRATIVE_BACKEND_STACK=rust`、`RUST_SERVER_TARGET`、`GENARRATIVE_RUNTIME_SERVER_TARGET` 后启动 Vite。
|
||||
9. 任一子进程退出时,脚本回收其余子进程。
|
||||
|
||||
Vite 代理覆盖范围:
|
||||
|
||||
@@ -84,6 +85,13 @@ npm run dev:rust:logs -- --follow
|
||||
1. 如果首页公开广场出现 `上游服务请求失败`,优先检查 `api-server` 错误详情里的 `ws://.../v1/database/<database>/subscribe` 是否指向了未发布的库。
|
||||
2. `spacetime --root-dir=server-rs/.spacetimedb/local list --server http://127.0.0.1:3101` 应能看到 `spacetime.local.json` 中的库名;若没有,执行 `spacetime --root-dir=server-rs/.spacetimedb/local publish <本地数据库名> --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module -c=on-conflict --yes`。
|
||||
3. 发布库名与 `GENARRATIVE_SPACETIME_DATABASE` 不一致时,`/api/runtime/custom-world-gallery` 会从 Rust `api-server` 返回 `502`,前端首页只能展示空态或错误提示,无法自行修复。
|
||||
4. 如果 Vite 输出 `/api/auth/refresh`、`/api/auth/login-options` 或 `/api/runtime/custom-world-gallery` 的 `ECONNREFUSED`,先确认当前脚本是否已经打印 `等待 api-server 就绪` 并通过;正常情况下 Vite 只会在 `/healthz` 可访问后启动,不应再因为 Rust 监听未完成而代理失败。
|
||||
|
||||
编译警告治理:
|
||||
|
||||
1. Rust 本地栈启动日志应保持可行动,运行态未使用函数不应长期保留为普通编译警告。
|
||||
2. 仅供测试断言使用的辅助函数使用 `#[cfg(test)]` 限定,避免进入 `cargo run -p api-server` 的普通二进制编译。
|
||||
3. 已无调用入口且无迁移价值的映射函数直接删除;如果后续新增同类 SpacetimeDB 记录映射,再按实际调用路径补回,避免提前保留死代码。
|
||||
|
||||
## 3. Ubuntu 发布包脚本
|
||||
|
||||
|
||||
Reference in New Issue
Block a user