feat: add inline external generation mode

This commit is contained in:
2026-06-07 00:56:53 +08:00
parent 853d1db618
commit 4bb6d0bd1e
20 changed files with 393 additions and 114 deletions

View File

@@ -1,6 +1,6 @@
# 外部生成 Worker 化方案
更新时间:`2026-06-03`
更新时间:`2026-06-07`
## 背景
@@ -8,11 +8,12 @@
## 目标
- `api-server` 的 HTTP 角色只负责鉴权、入参校验、扣费前置/状态初始化、任务入队和返回 `queued` 操作结果。
- 默认 `queue` 模式下,`api-server` 的 HTTP 角色只负责鉴权、入参校验、扣费前置/状态初始化、任务入队和返回 `queued` 操作结果。
- 外部生成副作用由独立 `external-generation-worker` 角色执行。
- 多个 worker 进程通过 SpacetimeDB 任务表抢占任务,依赖 lease 超时恢复,支持按进程数和单进程并发动态缩扩容。
- 本地或小流量同步排查可显式启用 `inline` 模式,由 HTTP handler 复用同一 worker executor 同步执行并返回 `completed`;该模式不创建队列任务,也不具备 worker 横向扩容能力。
- SpacetimeDB reducer / procedure 只做任务状态流转,不做网络、文件系统或外部 provider I/O。
- 已接入拼图 `compile_puzzle_draft`结果页 `generate_puzzle_images`;后续玩法继续复用同一队列 Module不再为每个玩法发明独立队列。
- 已接入拼图 `compile_puzzle_draft`结果页 `generate_puzzle_images` 与结果页 `generate_puzzle_ui_background`;后续玩法继续复用同一队列 Module不再为每个玩法发明独立队列。
## Module 与 Interface
@@ -68,9 +69,14 @@ pending/running -> cancelled (预留)
`claim` 只领取 `pending``available_at <= now` 的任务,或 `running``lease_expires_at <= now` 的任务。领取时递增 `attempt`、写入 `worker_id``started_at`、新的 `lease_expires_at``lease_token`。SpacetimeDB procedure 使用 `ctx.timestamp` 作为状态流转时间,只从 worker 入参读取“时长差值”,不信任 worker 本机绝对时间。worker 每次执行只处理自己 claim 到的任务;续租、完成或失败时必须带同一个 `worker_id + lease_token`,且当前 lease 尚未过期,防止过期 worker 覆盖新 lease。
玩法业务写回也必须在 SpacetimeDB 同一事务里校验 lease fencing。拼图的 `compile_puzzle_agent_draft` worker 调用、`save_puzzle_generated_images``save_puzzle_ui_background``mark_puzzle_draft_generation_failed``mark_puzzle_level_generation_failed` 会带 `external_generation_job_id / worker_id / lease_token`,并校验 job 仍为 `running`、token 未过期、`job_kind``owner_user_id``source_module``source_entity_id` 均匹配后才写 session / work profile。worker 路径的核心业务写回失败不能返回内存快照并把 job 标为 `completed`;失败态业务回写成功后才允许把队列 job 标为 `failed`,失败态仍未写回时保留当前租约并等待后续 lease 过期重领,避免队列状态和真实 session 脱节。api-server 的资产扣费包装遇到这类 stale worker lease guard 错误时不执行补偿退款,避免旧 worker 冲掉后续合法 worker 的同一账本扣费。
玩法业务写回也必须在 SpacetimeDB 同一事务里校验 lease fencing。拼图的 `compile_puzzle_agent_draft` worker 调用、`save_puzzle_generated_images``save_puzzle_ui_background``mark_puzzle_draft_generation_failed``mark_puzzle_level_generation_failed` `queue` 模式下会带 `external_generation_job_id / worker_id / lease_token`,并校验 job 仍为 `running`、token 未过期、`job_kind``owner_user_id``source_module``source_entity_id` 均匹配后才写 session / work profile。`inline` 模式不创建 `external_generation_job`,因此这三个 guard 字段必须同时为空transaction 只把三项全空识别为 api-server 受控同步写回,三项半空仍按非法请求拒绝。worker 路径的核心业务写回失败不能返回内存快照并把 job 标为 `completed`;失败态业务回写成功后才允许把队列 job 标为 `failed`,失败态仍未写回时保留当前租约并等待后续 lease 过期重领,避免队列状态和真实 session 脱节。api-server 的资产扣费包装遇到这类 stale worker lease guard 错误时不执行补偿退款,避免旧 worker 冲掉后续合法 worker 的同一账本扣费。
## 进程角色
## 执行模式与进程角色
外部生成执行模式由 `GENARRATIVE_EXTERNAL_GENERATION_MODE` 控制:
- `queue`默认值HTTP handler 入队 `external_generation_job`,由 `external-generation-worker` 角色 claim lease 后执行;生产、预发和压测默认使用该模式。
- `inline`HTTP handler 直接调用同一个 worker executor同步等待 provider、OSS 和 SpacetimeDB 写回完成后返回 `operation.status = completed`只用于本地或低并发排查不提供队列持久化、lease 重领和 worker 横向扩容。
同一个 Rust binary 通过 `GENARRATIVE_PROCESS_ROLE` 切换:
@@ -91,8 +97,8 @@ worker 配置:
`compile_puzzle_draft`
1. HTTP handler 保存拼图表单草稿;`queued/running` 的持久事实源是 `external_generation_job`,不把 HTTP 进程变成外部生成执行者。
2. HTTP handler 入队 `puzzle_compile_draft`,返回 `operation.status = queued` 和当前 session。拼图 dedupe key 包含本次 `extgen-` job id只保证同一任务行唯一不把同一 session 后续重新生成吞掉。
1. HTTP handler 保存拼图表单草稿;`queue` 模式下 `queued/running` 的持久事实源是 `external_generation_job`,不把 HTTP 进程变成外部生成执行者。
2. `queue` 模式下 HTTP handler 入队 `puzzle_compile_draft`,返回 `operation.status = queued` 和当前 session。拼图 dedupe key 包含本次 `extgen-` job id只保证同一任务行唯一不把同一 session 后续重新生成吞掉。`inline` 模式下 HTTP handler 复用同一 executor 同步执行,成功后直接返回 `completed` 和最新 session。
3. 前端保持 `puzzle-generating`,继续轮询 `getPuzzleAgentSession`;首期不把 `queued/running` 写回 `puzzle_agent_session`,因此刷新或跨设备恢复生成中状态仍是后续 read model 工作。
4. worker claim 后执行原有 `compile_puzzle_draft_with_initial_cover``compile_puzzle_draft_with_uploaded_cover`;前置 `compile_puzzle_agent_draft` 也必须携带本次 `job_id / worker_id / lease_token`,防止过期 worker 先把草稿卡和 session 写到 ready。
5. 成功后沿原有 SpacetimeDB 拼图会话/作品写回,前端轮询看到 `progressPercent >= 94/96/100` 和 ready 草稿。
@@ -100,14 +106,14 @@ worker 配置:
`generate_puzzle_images`
1. HTTP handler 校验本次 `levelsJson` 快照入队 `puzzle_generate_images`返回 `operation.status = queued/running/completed/failed`
1. HTTP handler 校验本次 `levelsJson` 快照`queue` 模式下入队 `puzzle_generate_images`返回 `operation.status = queued/running/completed/failed``inline` 模式下同步执行原 worker executor 并在成功后返回 `completed`
2. worker 执行原结果页关卡图链路自动命名、VectorEngine / 上传图直用、关卡场景图、UI spritesheet、关卡背景资产包、OSS 持久化和 SpacetimeDB 回写。
3. 成功后 `save_puzzle_generated_images` 写回目标关卡和草稿卡;失败后 `mark_puzzle_level_generation_failed` 只标记目标关卡 `failed`,不污染已 ready 的其它关卡。队列 job 只有在目标关卡失败态写回成功后才进入 failed。
4. 前端结果页对 `queued/running` 操作继续轮询 `getPuzzleAgentSession`,目标关卡变为 ready 或 failed 后收敛。
`generate_puzzle_ui_background`
1. HTTP handler 校验本次 `levelsJson` 快照入队 `puzzle_generate_ui_background`返回 `operation.status = queued/running/completed/failed`
1. HTTP handler 校验本次 `levelsJson` 快照`queue` 模式下入队 `puzzle_generate_ui_background`返回 `operation.status = queued/running/completed/failed``inline` 模式下同步执行原 worker executor 并在成功后返回 `completed`
2. worker 执行原结果页 UI 背景链路归一化提示词、VectorEngine 生成、OSS 持久化和 `save_puzzle_ui_background` 写回。
3. 成功后目标关卡写入 `uiBackgroundPrompt/uiBackgroundImageSrc/uiBackgroundImageObjectKey`;失败后复用 `mark_puzzle_level_generation_failed` 标记目标关卡 `failed`,并在失败态写回成功后才终结队列 job让前端轮询能收敛。
@@ -141,7 +147,7 @@ GENARRATIVE_PROCESS_ROLE=all npm run dev
curl -f http://127.0.0.1:<api-port>/healthz
```
生产 smoke 需要至少启动一个 `api` 角色和一个 `external-generation-worker` 角色;发布脚本会在默认 worker pattern 下自动启用并启动 `genarrative-external-generation-worker@1.service`,并等待 worker active。若 worker 数量归零,生成任务会保持 `queued/running`,不会由 HTTP 进程偷偷执行。
本地同步排查可显式使用 `GENARRATIVE_EXTERNAL_GENERATION_MODE=inline npm run dev:api-server`,用于确认 provider、OSS 和 SpacetimeDB 写回链路本身是否可行;该模式不覆盖 worker 队列 smoke。生产 smoke 需要保持 `GENARRATIVE_EXTERNAL_GENERATION_MODE=queue`,并至少启动一个 `api` 角色和一个 `external-generation-worker` 角色;发布脚本会在默认 worker pattern 下自动启用并启动 `genarrative-external-generation-worker@1.service`,并等待 worker active。若 worker 数量归零,生成任务会保持 `queued/running`,不会由 HTTP 进程偷偷执行。
systemd 生产扩缩容示例: