## 现象 动作生成任务在**视频已经生成完毕之后**失败,整单丢弃,费用照付。 2026-08-05 实测,同一角色连续两单都死在同一处,各烧掉一次生成费用: ``` peer closed connection without sending complete message body (received 720450 bytes, expected 929531) ``` 第三单一次成功——**两次复现对一次成功**,说明这不是稳定失败,是概率性的连接中断。 ## 根因 取视频成品那一步是单次读取、无重试、不校验长度: ```python return client.get(url).raise_for_status().content ``` 代码位置:`backend/packages/framework/src/windup_framework/providers/sufy.py` (`SufyVideoProvider.i2v` 的最后一行;轮询到 `completed`、拿到 `task_result.videos[0].url` 之后)。 这一步的位置很特殊:**它在"钱已经花完"之后**。前面的提交任务、轮询、等待都成功了, 视频在供应商那边已经生成好,只差把 bytes 取回来。此时读 body 断一次,整单就废, 用户看到的是任务 FAILED,而账单上那次生成照常计费。 两个具体缺陷: 1. **不重试。** 这是对成品 URL 的 GET,幂等、不再计费。重试的代价是一次重下(几百 KB), 不重试的代价是一次重新生成。代价不对等。 2. **不校验长度。** 截断**不一定抛异常**——服务端提前关流而客户端已收到部分 body 时, `.content` 可能直接返回短 bytes。那样坏视频会一路流到出帧环节才暴露, 在那里表现为"解码失败",很难回溯到下载这一步。 ## 修复 `_download(client, url, tries=3)`:指数退避重试 3 次;有 `Content-Length` 时校验实收字节数, 不符按失败重试;分块传输无该头时跳过校验;三次都失败抛 `RuntimeError` 并带出最后一次的真实原因。 回归测试用 `httpx.MockTransport`,不联网。**已用旧实现做控制样本验证**: 三条断言(重试成功 / 拒绝截断 / 包装最终原因)在修复前确实失败,两条护栏断言两边都过。 ## 影响面与不在本次范围内的同类点 同一份代码里还有两处 `resp.raise_for_status(); return resp.content` (`server/orchestrator/executor.py` 的 `_download_master` 与 `_download`), 但它们取的是**母版 / 参考图,即输入**,失败发生在付费生成之前,损失只有一次任务而没有账单。 严重性不同一档,故不并进本 PR;如需一并加固可另开。 `SufyImageProvider.gen_image` 走 `.json()` 取内联 base64 图,截断会抛 JSONDecodeError 而不是静默短读,同样在付费之后,但目前没有实测复现,暂不动。 ## 验收 - 首次读 body 断连、第二次成功时,任务应完成而不是 FAILED; - 服务端声明长度与实收不符时,不得把短 bytes 交给下游; - 三次都失败时报错信息要带出最后一次的网络层原因,而不是笼统的"生成失败"。
Activity
johnnyzhang-eng commentedon Aug 6, 2026
修复已提: xiaocheny214#33 (base
feat/backend-dev,与 #30 / #31 同一条路)。本仓
main目前没有这个文件——framework/providers/下只有chat.py/image.py/video.py三个 OpenAI SDK 客户端工厂,出问题的sufy.py只存在于后端开发线。所以修复先落那边。但 #126 正在把这行带进本仓:它新增的
framework/providers/sufy.py里是同一行return client.get(url).raise_for_status().content。已在该 PR 的评审里点出,建议合并前先带上本修复。Merge pull request #33 from johnnyzhang-eng/fix/video-download-retry
xiaocheny214 commentedon Aug 6, 2026
已在 PR #75 中修复:
88880d9fix(providers): retry the finished-video download and verify its length