From 985955205c44a38f28f9cb651e53ecbb779f975e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 23 Sep 2026 17:50:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=8A=20checkpoint=20=E5=88=86=E6=94=AF?= =?UTF-8?q?=E5=86=99=E5=AE=9E=E4=B8=BA=20at-most-once=20=E7=A1=AC=E7=BA=A6?= =?UTF-8?q?=E6=9D=9F=E8=80=8C=E9=9D=9E=E7=BB=AD=E8=B7=91=E8=83=BD=E5=8A=9B?= =?UTF-8?q?=20-=20worker.rs=20=E6=A8=A1=E5=9D=97=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E6=98=8E=E7=A1=AE=20Tripo=20job=20=E5=9B=BA=E5=AE=9A=20max=5Fa?= =?UTF-8?q?ttempts=20=3D=201=EF=BC=8C=E5=B4=A9=E6=BA=83=E5=90=8E=E7=A7=9F?= =?UTF-8?q?=E7=BA=A6=E8=80=97=E5=B0=BD=E7=9B=B4=E6=8E=A5=E7=BB=88=E6=80=81?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E3=80=81=E4=B8=8D=E4=BC=9A=E8=A2=AB=E9=87=8D?= =?UTF-8?q?=E6=96=B0=20claim=EF=BC=8C=E5=9B=A0=E6=AD=A4=E4=B8=8D=E5=AD=98?= =?UTF-8?q?=E5=9C=A8=E7=BB=AD=E8=B7=91=E8=B7=AF=E5=BE=84=20-=20worker.rs?= =?UTF-8?q?=20=E8=AF=B4=E6=98=8E=20existing=5Fcheckpoint=20=E6=98=AF?= =?UTF-8?q?=E9=98=B2=E5=BE=A1=E6=80=A7=E7=A1=AC=E7=BA=A6=E6=9D=9F=EF=BC=8C?= =?UTF-8?q?checkpoint=20=E7=9A=84=E6=97=A5=E5=B8=B8=E7=94=A8=E9=80=94?= =?UTF-8?q?=E6=98=AF=E4=BA=BA=E5=B7=A5=E5=AF=B9=E8=B4=A6=20-=20=E6=8A=80?= =?UTF-8?q?=E6=9C=AF=E6=96=B9=E6=A1=88=E4=B8=8E=20ADR=20=E6=8A=8A=E3=80=8C?= =?UTF-8?q?=E5=B4=A9=E6=BA=83=E5=90=8E=E9=87=8D=E6=96=B0=20claim=20?= =?UTF-8?q?=E5=8F=AA=E6=9F=A5=E8=AF=A2=E3=80=8D=E6=94=B9=E5=86=99=E4=B8=BA?= =?UTF-8?q?=E3=80=8C=E4=B8=8D=E9=87=8D=E6=96=B0=20claim=EF=BC=8C=E5=B8=A6?= =?UTF-8?q?=20checkpoint=20=E7=9A=84=E8=B7=AF=E5=BE=84=E7=BB=9D=E4=B8=8D?= =?UTF-8?q?=E4=BA=8C=E6=AC=A1=20submit=E3=80=8D=20-=20=E5=AE=9E=E6=96=BD?= =?UTF-8?q?=E8=AE=A1=E5=88=92=E4=B8=8E=E9=87=8C=E7=A8=8B=E7=A2=91=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=20checkpoint=20=E8=AF=AD=E4=B9=89=EF=BC=8C=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E5=AE=A3=E7=A7=B0=E7=BB=AD=E8=B7=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...02-Tripo生成API集成边界与提交时定价-2026-09-21.md | 2 +- ...划】Tripo生成Worker执行链路与API路由-2026-09-21.md | 4 ++-- ...碑】Tripo生成Worker执行链路与API路由-2026-09-21.md | 2 +- .../【技术方案】Tripo 3D生成API集成-2026-09-21.md | 6 +++--- server-rs/crates/api-server/src/tripo3d/worker.rs | 14 +++++++++++--- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/docs/adr/【ADR】0002-Tripo生成API集成边界与提交时定价-2026-09-21.md b/docs/adr/【ADR】0002-Tripo生成API集成边界与提交时定价-2026-09-21.md index 664341735..55a432ab5 100644 --- a/docs/adr/【ADR】0002-Tripo生成API集成边界与提交时定价-2026-09-21.md +++ b/docs/adr/【ADR】0002-Tripo生成API集成边界与提交时定价-2026-09-21.md @@ -6,7 +6,7 @@ Tripo 3D 生成以**全新 API** 接入 api-server,不复用 `platform-hyper3d 选择这个边界是因为第三方 SDK 的任务与错误模型会随 provider 变化,一旦穿透到 API 与前端就会把手第三方约束固化成产品契约;同时 Hyper3D 与 Tripo 的参数、状态与计费形态都不同,把它们塞进同一组路径会让“路径名”与“实际 provider”不一致,调用方无法分辨。 -**at-most-once submit**:worker 的崩溃恢复模型是“租约过期后重新 claim,handler 从头重跑”,这对可重跑的 provider 安全,但 Tripo 重跑会二次 submit、二次消耗额度。因此 `external_generation_job` 追加 provider checkpoint 字段并新增受租约栅栏保护的写入 procedure:没有 checkpoint 才允许 submit,submit 成功后必须先落 checkpoint 再轮询,已有 checkpoint 只允许查询、下载与落库。submit 成功但 checkpoint 落库失败的 attempt 只能终态失败,不得退回可重试队列。checkpoint 不写入 `request_payload_json`,因为该字段是请求真相并参与请求指纹与压缩逻辑。 +**at-most-once submit**:其它生成任务的崩溃恢复模型是“租约过期后重新 claim,handler 从头重跑”,这对可重跑的 provider 安全,但 Tripo 重跑会二次 submit、二次消耗额度。Tripo job 固定 `max_attempts = 1`,崩溃后租约耗尽直接终态失败、不重新 claim,因此没有续跑路径。因此 `external_generation_job` 追加 provider checkpoint 字段并新增受租约栅栏保护的写入 procedure:没有 checkpoint 才允许 submit,submit 成功后必须先落 checkpoint 再轮询,已有 checkpoint 只允许查询、下载与落库(防御性硬约束)。submit 成功但 checkpoint 落库失败的 attempt 只能终态失败,不得退回可重试队列。checkpoint 不写入 `request_payload_json`,因为该字段是请求真相并参与请求指纹与压缩逻辑。 **提交时定价与真实扣费**:Tripo 计费形态是“底价 + 可叠加 add-on”,现有“模型 → 档位 → 单价”查表表达不了,因此新增 `model3d` 定价段,全部以泥点计价,底价按 `endpoint × modelVersion × 是否有贴图` 拆分,add-on 按请求参数判定叠加;配置加载即校验全部底价键存在,运行期缺键直接拒绝提交,不复用现有 `unwrap_or(0)` 兜底。扣费依据固定为提交时定价,而不是 provider 返回的实际消耗:用户在提交前即可得到确定价格,失败退款就是 attempt 级单次冲正,不需要按实际用量退差额。provider 的实际消耗只写日志用于成本对账,不进入契约、API 响应或资源行。 diff --git a/docs/project-memory/plans/【实施计划】Tripo生成Worker执行链路与API路由-2026-09-21.md b/docs/project-memory/plans/【实施计划】Tripo生成Worker执行链路与API路由-2026-09-21.md index 6ae7b7da3..5005962a3 100644 --- a/docs/project-memory/plans/【实施计划】Tripo生成Worker执行链路与API路由-2026-09-21.md +++ b/docs/project-memory/plans/【实施计划】Tripo生成Worker执行链路与API路由-2026-09-21.md @@ -59,11 +59,11 @@ Milestone Spec: `docs/project-memory/plans/【里程碑】Tripo生成Worker执 - 执行分支:无 checkpoint 才 submit,submit 成功后先落 checkpoint 再轮询;已有 checkpoint 只查询、下载与落库;checkpoint 写入失败按失败收口。 - 轮询在单次 attempt 内进行并按现有心跳续租,超时按现有 worker 预算语义处理。 - provider 成功但落库失败按失败收口并冲正扣费,checkpoint 保留供对账。 - - 验收:崩溃重放不产生第二次 submit;落库失败不退化成“已交付”。 + - 验收:任何带 checkpoint 的执行路径都不产生第二次 submit;`max_attempts = 1` 使崩溃后不重新 claim(不存在续跑);落库失败不退化成“已交付”。 7. **接线与文档** - `app.rs` merge 新路由;`external_generation_worker.rs` 增加两个分支并纳入长任务超时;`tripo3d/mod.rs` 去掉里程碑一的 `dead_code` 豁免。 - - 主规范把 `max_attempts = 1` 与 checkpoint 的实际语义写实,里程碑状态同步。 + - 主规范把 `max_attempts = 1` 与 checkpoint 的实际语义写实(at-most-once 硬约束 + 人工对账,不是续跑能力),里程碑状态同步。 - 验收:`node scripts/check-doc-index.mjs`、`npm run check:encoding`、`git diff --check` 通过。 ## 进展 diff --git a/docs/project-memory/plans/【里程碑】Tripo生成Worker执行链路与API路由-2026-09-21.md b/docs/project-memory/plans/【里程碑】Tripo生成Worker执行链路与API路由-2026-09-21.md index 1cddbd27f..039daea3b 100644 --- a/docs/project-memory/plans/【里程碑】Tripo生成Worker执行链路与API路由-2026-09-21.md +++ b/docs/project-memory/plans/【里程碑】Tripo生成Worker执行链路与API路由-2026-09-21.md @@ -14,7 +14,7 @@ Implementation Plan: `docs/project-memory/plans/【实施计划】Tripo生成Wor - 两个 Bearer 路由:`/api/assets/tripo/text-to-model`、`/api/assets/tripo/image-to-model`,含 `Idempotency-Key` 校验与错误映射。 - 两个 Tripo job kind 的 worker 分支:submit、单次 `get_task` 轮询、下载、OSS 写入、资源登记、严格结果序列化。 -- checkpoint 规则:无 checkpoint 才 submit,submit 成功后先落 checkpoint 再轮询,已有 checkpoint 只续跑;submit 成功但 checkpoint 落库失败的 attempt 终态失败。 +- checkpoint 规则:无 checkpoint 才 submit,submit 成功后先落 checkpoint 再轮询,已有 checkpoint 只查询(防御性硬约束,当前 `max_attempts = 1` 下不存在续跑路径);submit 成功但 checkpoint 落库失败的 attempt 终态失败。 - 图片输入的归属校验与 OSS 对象解析;结果落点的两个分支。 - 提交时定价扣费、attempt 级退款、幂等重放不重复扣费。 - 真实 Provider smoke 与端到端验收证据。 diff --git a/docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md b/docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md index d62fd65f5..e94ce81fc 100644 --- a/docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md +++ b/docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md @@ -75,14 +75,14 @@ canvasCompletion? 画布占位框回填,只在项目资源落点下生效 本能力复用现有 `external_generation_job` 队列、worker、租约与 owner 过滤,不新建平行队列、不新增状态枚举、不新增查询接口。状态对外仍是 `queued` / `running` / `completed` / `failed`,阶段提示沿用现有 `phase` 与 `phaseLabel` / `phaseDetail`。 -**at-most-once submit** 是本能力与其它生成任务的关键差异。现有 worker 的崩溃恢复模型是“租约过期后由别的 worker 重新 claim,handler 从头重跑”,这对可重跑的 provider 安全,但 Tripo 重跑会二次 submit、二次消耗额度。因此: +**at-most-once submit** 是本能力与其它生成任务的关键差异。其它生成任务的崩溃恢复模型是“租约过期后由别的 worker 重新 claim,handler 从头重跑”,这对可重跑的 provider 安全,但 Tripo 重跑会二次 submit、二次消耗额度。Tripo job 固定 `max_attempts = 1`,任何 attempt 失败都是终态失败,崩溃后租约耗尽同样直接终态失败、不会被重新 claim,因此不存在“重新 claim 后继续查询”的续跑路径 —— checkpoint 是 at-most-once 的硬约束与人工对账凭据,不是续跑能力。因此: 1. 没有 `providerTaskId` 的 job 才允许 submit; 2. submit 成功后必须先把 `providerTaskId` 写回 checkpoint,再进入轮询; 3. 已有 `providerTaskId` 的 job 只允许 `get_task`、下载与落库,绝不允许再次 submit; 4. submit 成功但 checkpoint 写入失败的 attempt 只能终态失败,不得退回可重试队列,否则重试会二次 submit;该 job 保留脱敏对账信息供人工处理。 -失败重试沿用现有语义:`fail` 先按 attempt 冲正扣费,`attempt < maxAttempts` 时回到 `pending` 并延迟重试,否则终态 `failed`。 +失败重试沿用现有语义:`fail` 先按 attempt 冲正扣费,`attempt < maxAttempts` 时回到 `pending` 并延迟重试,否则终态 `failed`。Tripo job 固定 `max_attempts = 1`,这条路径退化为“第一次失败即终态”,永不进入延迟重试。 **provider 成功但落库失败按失败处理**:job 失败、该 attempt 退款,客户端看到 failed。没有正式资源引用就不算交付成功;checkpoint 保留,人工对账可证明这次生成确实发生过。由此产生的“provider 已消耗、用户已退款”净亏损是已知并接受的成本。 @@ -196,7 +196,7 @@ width/height = 预览图像素尺寸 3. 定价配置缺任一底价键时服务启动失败;请求命中 add-on 后扣费等于底价加全部命中 add-on;`fast` 与 `standard` 不加价。 4. 组合校验前置于 provider 副作用:`texture=false` + `textureQuality`、`generateParts=true` 与 `texture=true` 的同现请求被拒绝且不扣费。 5. image-to-model 的 `source` 元数据预检前置于扣费与入队:跨 owner、未登记、已删除与 `kind` 不符的引用返回 400,不产生 operation、不扣费,也不把图片正文读进内存或调用 provider。 -6. worker 崩溃后重新 claim 不会产生第二次 submit:已有 checkpoint 的 job 只轮询、下载与落库。 +6. Tripo job 固定 `max_attempts = 1`:崩溃后租约耗尽即终态失败、不重新 claim,不存在续跑路径;同时任何带 checkpoint 的执行路径都只轮询、下载与落库,绝不二次 submit(防御性硬约束,不依赖配置保证)。 7. submit 成功但 checkpoint 写入失败的 attempt 终态失败且不自动重提。 8. provider 成功但落库失败时 job 失败、该 attempt 退款,checkpoint 保留供对账。 9. worker 落库时构造的完成结果按端点严格类型化,只含模型与预览的正式资源引用,不含 provider task ID 与带签名的临时 URL;标准消费者的队列结果不透传它(与其它画布生成任务一致,结果靠画布 / 素材读回)。 diff --git a/server-rs/crates/api-server/src/tripo3d/worker.rs b/server-rs/crates/api-server/src/tripo3d/worker.rs index 1949eba01..7a27d698a 100644 --- a/server-rs/crates/api-server/src/tripo3d/worker.rs +++ b/server-rs/crates/api-server/src/tripo3d/worker.rs @@ -2,8 +2,12 @@ //! //! 与其它生成任务的关键差异是 **at-most-once submit**:provider 不接受幂等重放, //! 所以只有“没有 checkpoint 的 job”才 submit,submit 成功后必须先落 checkpoint 再轮询; -//! 单次尝试(`max_attempts = 1`)失败即终态收口,不存在第二次 submit 的窗口, -//! checkpoint 只用于继续查询与人工对账。 +//! 单次尝试(`max_attempts = 1`)失败即终态收口。 +//! +//! 注意这不是「崩溃后接着查询」的续跑能力:3D job 的 `max_attempts = 1`,租约耗尽的 job 会被 +//! 直接判终态失败、不会被重新 claim,所以不存在「重新 claim 后继续查询」的真实路径。 +//! [`existing_checkpoint`] 的分支是一条**防御性硬约束**(任何带 checkpoint 的执行都绝不会 +//! 再 submit 一次),checkpoint 的日常用途是人工对账。 use std::time::{Duration, Instant}; @@ -131,7 +135,11 @@ async fn run_model3d_job( .await } -/// checkpoint 是 provider 侧任务的唯一凭据;有值就只能续跑查询。 +/// checkpoint 是 provider 侧任务的唯一凭据,也是 at-most-once 的开关:有值就只能查询, +/// 绝不允许再次 submit。 +/// +/// 当前 `max_attempts = 1` 下 job 不会被重新 claim(租约耗尽直接终态失败),所以这里守住的 +/// 是一条防御性路径;它存在的意义是把「不许二次 submit」写成代码里的硬约束,而不是靠配置保证。 fn existing_checkpoint(job: &ExternalGenerationJobRecord) -> Option { job.provider_task_id .as_deref()