fix(#1714): 素材上传去重防重 + complete幂等传递 + 轮询收敛(前端P0) #1717

Merged
auto-approve-bot merged 2 commits from feat/1714-upload-dedup into develop 2026-09-05 16:40:06 +08:00
Owner

背景

Issue #1714:同一文件被反复走完整上传链路(prepare→OSS直传→complete),13 个素材建了 69 条记录、转码队列堆 39 个任务。根因诊断见项目文件 asset_duplicate_upload_rootcause_20260905.md

本 PR 为前端 P0 部分(后端 complete 幂等兜底 + worker 转码孤儿修复由后端 PR 处理)。

改动

1. 入队按文件指纹去重(核心)

  • 新增 src/api/assets/uploadDedup.ts 纯函数模块:
    • makeFileFingerprint:文件名+大小+lastModified 稳定指纹
    • findDuplicateInQueue:队列查重(error 态可重新激活、在途/已完成均拦截)
    • makeClientUploadId:上传幂等 token(一次逻辑上传一个,重试复用)
    • computeFileHash:SHA-256 内容哈希(≤256MB 全量;大文件抽样头尾各 8MB + 文件大小,避免 2GB 上传前卡死;不支持 crypto.subtle 时降级为空串,FileReader 回退)
  • useAssetUpload.enqueueUploads:同一文件已在 preparing/uploading/ingesting/done 时跳过并轻提示;此前 error 的同指纹项重新激活复用 clientUploadId

2. complete 携带 file_hash + 幂等 token

  • prepareDirectUpload / completeDirectUpload 请求体新增 file_hashclient_upload_id(后端 schema 已支持 file_hash,额外字段 Pydantic 忽略不报错,待后端幂等 PR 消费 token)
  • uploadAssetDirect(配音/封面/克隆等非队列链路)自动补算 hash 与 token,去重闸门对所有上传链路生效

3. 超时放宽 + complete 失败不盲目重传

  • prepare 请求超时 10s→30s,complete 10s→60s(complete 内含 OSS 检查+建库+派单)
  • complete 阶段失败(超时/网络/5xx):不重新 prepare、不重新直传,handle 留存,点重试只幂等重发 complete(文件已在 OSS,重发由 file_hash + token 保证不重复建记录);同时立即刷新素材列表,让可能已建成的「处理中」素材可见,消除用户反复重传的动因
  • prepare/transfer 失败(后端尚无记录)才全量重跑

4. 入口防重复提交

  • 直传进行中(transferActive)禁用「上传素材」按钮、拦截文件选择/拖拽并提示;重试按钮仅 error 状态可点,tooltip 区分「安全重试(只确认,不重新上传)」

5. 轮询收敛

  • 素材列表 3s 快轮询加 10 分钟上限:处理中素材创建超 10 分钟仍未就绪时停止快轮询(避免后端孤儿任务导致无限转圈),对应卡片停止转圈并显示「处理超时,可重试上传」橙色遮罩 + StatusPill 文案

测试

  • 新增 src/test/api/uploadDedup.test.ts(9 个):指纹一致性/区分度、队列查重各状态、幂等 token 唯一性、hash 内容区分与 64 位 hex 格式
  • useAssetUpload.test.tsx 新增 2 个集成测试:同文件多次选择不重复入队(1 项/1 次 prepare,完成后重复选择仍不新增,不同文件正常入队)、complete 超时后重试只重发 complete(prepare 1 次、transfer 1 次、complete 2 次)
  • 本地 tsc / eslint / prettier / build 全绿;全量 vitest 623 通过

前端职责边界

  • 未改 .gitea/workflows/ 与 Dockerfile
  • 后端 P0(complete 按 token/file_hash 幂等兜底)与 P1(worker HEVC 转码 key 不一致致孤儿 PROCESSING)由后端 PR 处理;本前端改动向后兼容(额外字段旧后端忽略)
## 背景 Issue #1714:同一文件被反复走完整上传链路(prepare→OSS直传→complete),13 个素材建了 69 条记录、转码队列堆 39 个任务。根因诊断见项目文件 `asset_duplicate_upload_rootcause_20260905.md`。 本 PR 为**前端 P0 部分**(后端 complete 幂等兜底 + worker 转码孤儿修复由后端 PR 处理)。 ## 改动 ### 1. 入队按文件指纹去重(核心) - 新增 `src/api/assets/uploadDedup.ts` 纯函数模块: - `makeFileFingerprint`:文件名+大小+lastModified 稳定指纹 - `findDuplicateInQueue`:队列查重(error 态可重新激活、在途/已完成均拦截) - `makeClientUploadId`:上传幂等 token(一次逻辑上传一个,重试复用) - `computeFileHash`:SHA-256 内容哈希(≤256MB 全量;大文件抽样头尾各 8MB + 文件大小,避免 2GB 上传前卡死;不支持 crypto.subtle 时降级为空串,FileReader 回退) - `useAssetUpload.enqueueUploads`:同一文件已在 preparing/uploading/ingesting/done 时跳过并轻提示;此前 error 的同指纹项重新激活复用 `clientUploadId` ### 2. complete 携带 file_hash + 幂等 token - `prepareDirectUpload` / `completeDirectUpload` 请求体新增 `file_hash`、`client_upload_id`(后端 schema 已支持 `file_hash`,额外字段 Pydantic 忽略不报错,待后端幂等 PR 消费 token) - `uploadAssetDirect`(配音/封面/克隆等非队列链路)自动补算 hash 与 token,去重闸门对所有上传链路生效 ### 3. 超时放宽 + complete 失败不盲目重传 - prepare 请求超时 10s→30s,complete 10s→60s(complete 内含 OSS 检查+建库+派单) - complete 阶段失败(超时/网络/5xx):**不重新 prepare、不重新直传**,handle 留存,点重试只幂等重发 complete(文件已在 OSS,重发由 file_hash + token 保证不重复建记录);同时立即刷新素材列表,让可能已建成的「处理中」素材可见,消除用户反复重传的动因 - prepare/transfer 失败(后端尚无记录)才全量重跑 ### 4. 入口防重复提交 - 直传进行中(`transferActive`)禁用「上传素材」按钮、拦截文件选择/拖拽并提示;重试按钮仅 error 状态可点,tooltip 区分「安全重试(只确认,不重新上传)」 ### 5. 轮询收敛 - 素材列表 3s 快轮询加 10 分钟上限:处理中素材创建超 10 分钟仍未就绪时停止快轮询(避免后端孤儿任务导致无限转圈),对应卡片停止转圈并显示「处理超时,可重试上传」橙色遮罩 + StatusPill 文案 ## 测试 - 新增 `src/test/api/uploadDedup.test.ts`(9 个):指纹一致性/区分度、队列查重各状态、幂等 token 唯一性、hash 内容区分与 64 位 hex 格式 - `useAssetUpload.test.tsx` 新增 2 个集成测试:**同文件多次选择不重复入队**(1 项/1 次 prepare,完成后重复选择仍不新增,不同文件正常入队)、**complete 超时后重试只重发 complete**(prepare 1 次、transfer 1 次、complete 2 次) - 本地 tsc / eslint / prettier / build 全绿;全量 vitest 623 通过 ## 前端职责边界 - 未改 `.gitea/workflows/` 与 Dockerfile - 后端 P0(complete 按 token/file_hash 幂等兜底)与 P1(worker HEVC 转码 key 不一致致孤儿 PROCESSING)由后端 PR 处理;本前端改动向后兼容(额外字段旧后端忽略)
xiaoxia added 1 commit 2026-09-05 16:19:27 +08:00
fix(#1714): 素材上传去重防重 + complete幂等传递 + 轮询收敛
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 1m25s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m40s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m28s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m40s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m54s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m0s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m17s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m42s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 1s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m57s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 6m14s
fe2fb8f293
- 入队按文件指纹(name+size+lastModified)去重:同一文件已在队列/上传中/处理中/已完成时跳过;失败项重新激活复用clientUploadId
- 上传前计算SHA-256(大文件抽样头尾8MB+大小),prepare/complete携带file_hash+client_upload_id幂等token,打开后端去重闸门;配音/封面/克隆等一次性上传链路自动补算
- complete/prepare单独放宽超时(60s/30s,全局axios仅10s);complete失败不盲目重传,复用handle只幂等重发complete;prepare/transfer失败才全量重跑
- 直传进行中禁用上传按钮并拦截拖拽/选择,防重复提交;重试按钮仅error状态可点并区分complete安全重试
- 素材列表处理中快轮询(3s)加10分钟上限,超时停止轮询并标记「处理超时」,避免孤儿任务永久转圈
- 单测:指纹/去重/幂等token/hash纯函数 + 同文件多次选择不重复入队 + complete超时重试只重发complete

🚀 预览环境已部署

项目 详情
PR号 #1717
预览链接 https://pr-1717.preview.xiaoxiajianji.com
API环境 staging

💡 预览环境使用 staging API 数据,请勿在预览环境中操作重要数据。

🔄 每次提交新代码后预览环境会自动更新。

🗑️ PR 关闭或合并后,预览环境会自动清理。

🚀 **预览环境已部署** | 项目 | 详情 | |------|------| | PR号 | #1717 | | 预览链接 | [https://pr-1717.preview.xiaoxiajianji.com](https://pr-1717.preview.xiaoxiajianji.com) | | API环境 | staging | > 💡 预览环境使用 staging API 数据,请勿在预览环境中操作重要数据。 > > 🔄 每次提交新代码后预览环境会自动更新。 > > 🗑️ PR 关闭或合并后,预览环境会自动清理。
Collaborator

【阻塞级判定】

  • 是否存在阻塞级问题:是
  • 阻塞级问题数量:1 个

📊 审查概览

  • 整体评价:需修改
  • 建议级问题数量:2 个

🔴 阻塞级问题(必须修复)

  1. [apps/web/src/api/assets/uploadDedup.ts: computeFileHash 函数内] 大文件哈希计算逻辑错误
    • 问题类型:逻辑bug / 数据损坏风险
    • 问题描述:在计算大于 256MB 文件的哈希时,代码将文件大小写入 64 位空间,但使用了 sizeView.setUint32(4, file.size >>> 0)file.size >>> 0 会将数值转换为 32 位无符号整数。对于超过 4GB (2^32 bytes) 的文件,这会导致大小被截断(例如 5GB 变成 1GB),使得哈希输入中的大小字段不正确。这增加了哈希碰撞的概率,导致去重逻辑失效。代码注释中提到“JS 安全整数远小于 2^53,高 4 字节为 0”也是错误的,因为 2^53 远大于 2^32,高 4 字节并不总是 0。
    • 修改建议:应正确处理 64 位整数。可以使用 sizeView.setBigUint64(0, BigInt(file.size), false)(如果环境支持),或者手动计算高低位:
      // 正确写入 64 位大端序
      const high = Math.floor(file.size / 0x100000000);
      const low = file.size >>> 0;
      sizeView.setUint32(0, high);
      sizeView.setUint32(4, low);
      

💡 改进建议(不阻塞合并)

  1. [apps/web/src/pages/assets/hooks/useAssetUpload.ts: runUpload & enqueueUploads] 页面刷新后幂等性丢失

    • 具体内容:当前实现依赖内存中的 handlesRefitemsRef 来实现“complete 阶段失败后的安全重试”。如果用户在上传过程中(特别是 complete 阶段超时后)刷新了页面,handlesRef 会被清空。此时用户点击重试,existingHandleundefined,代码会回退到全量重传(prepare + transfer + complete)。虽然后端有 file_hash 兜底不会产生重复数据,但会浪费 OSS 流量和时间。建议考虑将 clientUploadId 以及必要的上下文(如 assetId)持久化到 sessionStorage,以便在刷新后能恢复状态,继续只发送 complete 请求。
  2. [apps/web/src/api/assets/uploadDedup.ts: computeFileHash] 内存峰值风险

    • 具体内容:在处理大文件时,computeFileHash 会创建一个约 16MB 的 Uint8Array(8MB 头 + 8MB 尾 + 8 字节大小)。虽然单次问题不大,但如果用户批量选择多个大文件,且并发计算哈希(取决于调用方的并发控制),可能会导致内存峰值飙升。建议在调用方(如 useAssetUpload)中限制哈希计算的并发数,或者观察实际内存表现,必要时改为流式处理(虽然 Web Crypto API 流式支持较复杂)。

良好实践

  • 引入 file_hashclient_upload_id 实现端到端去重和幂等,设计思路清晰,能有效解决重复上传和超时重试导致的脏数据问题。
  • useAssetUpload hook 中对 complete 阶段失败的特殊处理(保留 handle,仅重试 complete)非常符合业务场景,避免了不必要的文件重传。
  • UI 层增加了 transferActive 状态防止重复提交,以及 stalled 状态提示长时间处理,用户体验考虑周全。

格式检查通过 | 逻辑审查需修改 | ⚠️ 建议关注性能


🤖 由 AI 代码审查机器人自动生成 | 2026-09-05 08:25:43 | 模型:

### 【阻塞级判定】 - 是否存在阻塞级问题:是 - 阻塞级问题数量:1 个 ### 📊 审查概览 - 整体评价:需修改 - 建议级问题数量:2 个 ### 🔴 阻塞级问题(必须修复) 1. **[apps/web/src/api/assets/uploadDedup.ts: computeFileHash 函数内] 大文件哈希计算逻辑错误** - 问题类型:逻辑bug / 数据损坏风险 - 问题描述:在计算大于 256MB 文件的哈希时,代码将文件大小写入 64 位空间,但使用了 `sizeView.setUint32(4, file.size >>> 0)`。`file.size >>> 0` 会将数值转换为 32 位无符号整数。对于超过 4GB (2^32 bytes) 的文件,这会导致大小被截断(例如 5GB 变成 1GB),使得哈希输入中的大小字段不正确。这增加了哈希碰撞的概率,导致去重逻辑失效。代码注释中提到“JS 安全整数远小于 2^53,高 4 字节为 0”也是错误的,因为 2^53 远大于 2^32,高 4 字节并不总是 0。 - 修改建议:应正确处理 64 位整数。可以使用 `sizeView.setBigUint64(0, BigInt(file.size), false)`(如果环境支持),或者手动计算高低位: ```typescript // 正确写入 64 位大端序 const high = Math.floor(file.size / 0x100000000); const low = file.size >>> 0; sizeView.setUint32(0, high); sizeView.setUint32(4, low); ``` ### 💡 改进建议(不阻塞合并) 1. **[apps/web/src/pages/assets/hooks/useAssetUpload.ts: runUpload & enqueueUploads] 页面刷新后幂等性丢失** - 具体内容:当前实现依赖内存中的 `handlesRef` 和 `itemsRef` 来实现“complete 阶段失败后的安全重试”。如果用户在上传过程中(特别是 complete 阶段超时后)刷新了页面,`handlesRef` 会被清空。此时用户点击重试,`existingHandle` 为 `undefined`,代码会回退到全量重传(prepare + transfer + complete)。虽然后端有 `file_hash` 兜底不会产生重复数据,但会浪费 OSS 流量和时间。建议考虑将 `clientUploadId` 以及必要的上下文(如 `assetId`)持久化到 `sessionStorage`,以便在刷新后能恢复状态,继续只发送 complete 请求。 2. **[apps/web/src/api/assets/uploadDedup.ts: computeFileHash] 内存峰值风险** - 具体内容:在处理大文件时,`computeFileHash` 会创建一个约 16MB 的 `Uint8Array`(8MB 头 + 8MB 尾 + 8 字节大小)。虽然单次问题不大,但如果用户批量选择多个大文件,且并发计算哈希(取决于调用方的并发控制),可能会导致内存峰值飙升。建议在调用方(如 `useAssetUpload`)中限制哈希计算的并发数,或者观察实际内存表现,必要时改为流式处理(虽然 Web Crypto API 流式支持较复杂)。 ### ✅ 良好实践 - 引入 `file_hash` 和 `client_upload_id` 实现端到端去重和幂等,设计思路清晰,能有效解决重复上传和超时重试导致的脏数据问题。 - `useAssetUpload` hook 中对 `complete` 阶段失败的特殊处理(保留 handle,仅重试 complete)非常符合业务场景,避免了不必要的文件重传。 - UI 层增加了 `transferActive` 状态防止重复提交,以及 `stalled` 状态提示长时间处理,用户体验考虑周全。 --- ✅ 格式检查通过 | ❌ 逻辑审查需修改 | ⚠️ 建议关注性能 --- <sub>🤖 由 AI 代码审查机器人自动生成 | 2026-09-05 08:25:43 | 模型: </sub> <!-- AI_CODE_REVIEW_AUTO_COMMENT -->
xiaoxia added 1 commit 2026-09-05 16:33:00 +08:00
fix(#1714): AI Review/CI跟进——cross-realm digest修复+大文件size 64位写入+prettier
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m38s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m52s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m56s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m58s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m6s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m4s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m28s
AI Code Review / AI Code Review (pull_request) Successful in 6m16s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 6m26s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 4m58s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 6s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 27s
5626b9654d
- computeFileHash: digest前把buffer复制到当前realm的Uint8Array,修复jsdom/Node WebCrypto跨realm WebIDL instanceof拒绝(CI单测失败根因)
- 大文件抽样hash的size字段改用setBigUint64(64位大端),修复>4GB文件size被截断(AI Review阻塞项),含BigInt不支持时的高低位兜底
- 补大文件抽样hash单测(锁定size字段差异)
- prettier 全量格式化(测试文件之前漏跑)
auto-approve-bot approved these changes 2026-09-05 16:35:06 +08:00
auto-approve-bot left a comment
Collaborator

CI全绿,自动审批通过。

CI全绿,自动审批通过。
auto-approve-bot approved these changes 2026-09-05 16:35:06 +08:00
auto-approve-bot left a comment
Collaborator

CI全绿,自动审批通过。

CI全绿,自动审批通过。
auto-approve-bot merged commit 6521be5426 into develop 2026-09-05 16:40:06 +08:00
auto-approve-bot deleted branch feat/1714-upload-dedup 2026-09-05 16:40:06 +08:00

🗑️ 预览环境已清理

PR #1717 已关闭或合并,对应的预览环境已被清理。

如有需要,可以重新打开 PR 来重新生成预览环境。

🗑️ **预览环境已清理** PR #1717 已关闭或合并,对应的预览环境已被清理。 > 如有需要,可以重新打开 PR 来重新生成预览环境。
Sign in to join this conversation.