3D 产物下载的重试改为覆盖整个响应体

- download_model / download_rendered_image 把「取响应头 + 读完 body」作为一次可重试事务,
  每次尝试都重新取响应头并整体重下,不做断点续传
- body 传输失败与长度不一致归一成可重试的传输错误(不再带 HTTP 200),HTTP 状态只留在文案里
- 新增 TripoArtifactBytes(读完的产物)并公开 download_model / download_rendered_image 的 max_bytes 上限;
  超限按 OutputSchema 失败且不重试,流式句柄 TripoDownloadedArtifact 收回 crate 内部
- api-server 的 read_artifact 改成对完整产物做落库前校验(只判空),体积上限转由调用方传入
- smoke 示例改为一次写入完整字节;新增「body 中途断开后整体重下成功」与「超限不重试」两个用例
- 同步技术方案与实施计划:把「只重试响应头」的描述改成「重试包住整个 body」
This commit is contained in:
2026-09-23 17:01:40 +08:00
parent 347d066fc5
commit b33a8a141c
9 changed files with 254 additions and 101 deletions
@@ -44,7 +44,7 @@ TODO:图生输入当前由 api-server 自己读站内对象的字节再上传
`get_task` 按 `TripoTaskHandle` 做单次查询,返回通用 `TripoTaskSnapshot`,由 `task_type` 决定 `output` 的具体 variant;adapter 不做轮询、不阻塞等待。
`download_model` 接受 `TripoTaskSnapshot`,先确认快照确实处于完成态,再从严格 endpoint 结果中取得已校验的 `model_url`,由 provider 自己的无鉴权 reqwest client 打开签名 URL 并返回 `TripoDownloadedArtifact` 流包装。包装只公开 `url`、`content_type`、`content_length`、`filename(name)` 和 `next_chunk()`;不提供完整 `Vec<u8>`,调用方必须逐块消费。`filename(name)` 的扩展名来自远端地址,只接受短的 ASCII 字母数字,其余退回 `glb`,避免远端地址里的 `%2F` 解码后拼出跨目录路径。provider 在流结束时校验实际接收字节数与 `Content-Length`,响应体中断或长度不一致按结构化请求错误失败。产物下载不设总超时:几十 MB 的流只要还在出数据就不该被判失败、更不该重试后从零重传,因此下载链路只按「无数据推进」判超时,预算取 `TripoSettings::request_timeout`(连接用 `connect_timeout`,响应体用 `read_timeout`,响应头之前由显式的 `tokio::time::timeout` 兜住),超过预算没有数据推进即按传输失败返回;SDK 保持第三方原样,不承担产物下载。smoke example 使用异步文件写入逐块落盘;未来接入 OSS 时应把同一数据流直接送入 OSS 分片上传,不经过完整内存缓冲。
`download_model`(及预览图的 `download_rendered_image`)接受 `TripoTaskSnapshot` 与调用方给的体积上限 `max_bytes`,先确认快照确实处于完成态,再从严格 endpoint 结果中取得已校验的产物地址,由 provider 自己的无鉴权 reqwest client 打开签名 URL,读完整个 body 后返回 `TripoArtifactBytes`(`url`、`content_type`、`content_length`、`bytes`、`filename(name)`)。流式句柄 `TripoDownloadedArtifact`(`next_chunk()`)保留为内部实现:**重试必须包住整个 body** —— 只重试「拿到响应头」这一步的时候,几十 MB 的字节其实是在之后才传输的,一次 CDN 抖动就会毁掉一次已经扣费、provider 任务也跑完的生成。因此每次尝试都重新取响应头并整体重下(不做断点续传,签名地址会过期),body 读取失败与长度不一致都归一成可重试的传输错误,重试上限与退避沿用 `TripoSettings::retries`。`filename(name)` 的扩展名来自远端地址,只接受短的 ASCII 字母数字,其余退回 `glb`,避免远端地址里的 `%2F` 解码后拼出跨目录路径。每次读取都校验实际接收字节数与 `Content-Length`,读到 `max_bytes` 之上直接按输出违约失败(不重试):宁可失败退款也不要把 api-server 内存打满。产物下载不设总超时:几十 MB 的流只要还在出数据就不该被判失败,重下只发生在传输真的断了的时候,因此下载链路只按「无数据推进」判超时,预算取 `TripoSettings::request_timeout`(连接用 `connect_timeout`,响应体用 `read_timeout`,响应头之前由显式的 `tokio::time::timeout` 兜住),超过预算没有数据推进即按传输失败返回;SDK 保持第三方原样,不承担产物下载。smoke example 一次写入完整字节;未来接入 OSS 时应把同一数据流直接送入 OSS 分片上传,不经过完整内存缓冲。
TODO:等待上游 `tripo-rust-sdk` 提供原生 artifact stream API 后,删除 provider-side reqwest 下载器,改由 SDK stream 直接承接。
@@ -55,5 +55,5 @@ TODO:等待上游 `tripo-rust-sdk` 提供原生 artifact stream API 后,删
- 三个入口的输入校验可拒绝空白 `prompt` / `input`、视图不足两张和空白 `taskId`;组合校验在调用 SDK 前完成。
- provider 错误统一为 adapter 错误类型。
- shared contracts 的 ts-rs binding 无 feature 开关且始终可生成。
- 产物下载只按「无数据推进」判超时:连接、响应头与每个数据块都必须有进展,慢速但持续的下载可完整收完,中途断流按请求错误失败。
- 产物下载只按「无数据推进」判超时:连接、响应头与每个数据块都必须有进展,慢速但持续的下载可完整收完,中途断流按可重试的传输错误失败并由下载入口整体重下。
- 真实 Provider smoke 已覆盖三个入口;example 在任务未完成时跳过下载并继续跑后续入口,只打印脱敏后的结果结构和下载产物信息。