--- id: T-301 title: 可恢复下载队列 phase: 3 deps: [T-202] status: DOING created: 2026-07-16 issue: null context_ref: a52acbf926041550ebc30049ff87f08faaa5ba0c claim_branch: null work_branch: agent/codex/T-301 write_paths: - docs/tasks/T-301.md - core/downloader/ - core/application/ - core/storage/ - schemas/download-task.schema.json - testdata/download/ - testdata/README.md - docs/api.md - docs/04-architecture.md - docs/current-state.md --- ## 问题 / 背景 Phase 2 已能识别软件状态并展示列表/详情,但点击下载后还没有并发调度、暂停/取消/重试、HTTP Range 续传或重启恢复。若下载状态只保存在 goroutine 内存中,盒子退出后任务丢失;若 Range 响应、临时文件和元数据不一致,可能把拼接错误的字节交给 T-302 校验/安装链路。 T-301 是 T-302/T-303 的前置安全边界。它只负责把远端 ZIP 稳定下载为隔离的 `.part`/完成文件并发布进度事件,不声称包已可信可执行。 ## 方案 1. 在 `core/downloader` 定义任务模型、队列命令与状态机;默认最多 2 个 active transfer,其余保持 queued。 2. 支持 enqueue、pause、resume、cancel、retry;每个命令按稳定 `request_id` / `app_id` 定位,重复命令返回确定结果而非产生第二份任务。 3. HTTP transport 使用注入接口和标准库实现: - 已有 `.part` 时发送 `Range: bytes=-`。 - `206` 必须校验 Content-Range 起点;不匹配则拒绝。 - 服务端返回 `200` 时安全截断并从 0 重下,不得追加到旧字节。 - 网络中断保留已落盘 `.part`;取消按策略删除任务临时文件;完成前 flush/sync/close。 4. 在 `core/storage` 持久化下载任务元数据,采用同目录临时文件 + backup 原子替换;Schema 落到 `schemas/download-task.schema.json`。 5. 启动恢复时: - queued / paused / failed 保持可恢复语义。 - 进程退出时遗留的 downloading 降级为 queued,不假装仍在下载。 - 元数据与 `.part` 实际长度不一致时以磁盘为准或返回稳定损坏错误,不得静默跳过字节。 6. application 事件沿用 `DownloadStarted` / `DownloadProgress` / `DownloadPaused` / `DownloadCompleted` / `DownloadFailed`;负载使用具体类型,UI 只消费事件,不轮询文件。 7. 进度包含 done/total/speed;total 未知时明确表示 unknown,不伪造百分比。事件节流与 clock 注入保证测试确定性。 ## 验收要点 - 并发默认值为 2;表驱动/阻塞 transport 测试证明任意时刻 active transfer 不超过 2,完成/暂停后下一个 queued 自动开始。 - pause 保留 `.part` 和元数据;resume 使用正确 Range;cancel 不再发布进度且按协议清理;failed 可 retry。 - httptest 覆盖:正常 200、正确 206、错误 Content-Range、服务器忽略 Range 返回 200、连接中断后续传、非成功状态。 - 任务元数据严格读写;未知字段、非法状态/ID/URL/路径/计数被拒绝;主文件缺失时可读取中断遗留 backup。 - 新队列实例能从磁盘恢复;遗留 downloading → queued,paused 保持 paused,已完成任务不重复下载。 - `.part` 长度、done 与下一次 Range offset 一致;不把未完成文件暴露为完成文件。 - application 具体事件负载与 request_id/app_id 匹配;进度 done 单调不回退,暂停/取消后不出现迟到 progress。 - `cd core && go vet ./... && go test -count=1 ./...`、完整双目标闸门与治理校验通过。 ## 边界(不改什么) - 不做 ZIP SHA-256 / package 签名、app.json 比对、解压、staging 切换或健康检查(T-302)。 - 不做磁盘空间预检查、程序占用、hash_mismatch/zip_corrupt 等完整失败矩阵(T-303)。 - 不实现 Gio DownloadPanel;本任务只交付 core 队列、持久化与事件,后续 UI 通过事件接入。 - 不实现多源下载、P2P、带宽限速、代理设置或动态并发 UI。 - 不引入第三方下载器、数据库或 Go 1.21+ API。 ## 协作约束 - 唯一写入 Agent 独占本任务全部 `write_paths`,负责实现但不创建额外任务提交。 - 测试设计 Agent 只读输出并发、Range、崩溃恢复和事件时序测试矩阵。 - 安全/架构 Agent 只读审查路径隔离、响应拼接、持久化原子性、迟到事件和 T-302 信任边界。 - coordinator 汇总意见、要求必要修正、执行最终验证、更新本执行记录并创建唯一 T-301 Git 提交。 - T-302/T-303 在本任务 DONE 前不得领取;若后续决定并行编码,先另行落成并提交带后缀子任务及共享接口冻结任务。 ## 执行记录 - 2026-07-16:正式落成多 Agent 协作方式;采用一个写入 Agent + 测试设计/安全架构两个只读 Agent,保持 T-301 → T-302 → T-303 依赖串行。