Skip to content

fix(downloads): 让卡住/已取消的下载能重下,已完成的行不再常驻 - #90

Open
std-external wants to merge 1 commit into
std-microblock:masterfrom
std-external:fix/download-stall-watchdog
Open

std-external wants to merge 1 commit into
std-microblock:masterfrom
std-external:fix/download-stall-watchdog

Conversation

@std-external

@std-external std-external commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

问题

群里(Celeste Coders)有用户报告:下载的 Mod 会卡住进度,必须重进 Mod 页面才能重新下载;另外下载好的 Mod 在「下载任务」页里进度条停在 100% 不消失,而左下角侧边栏的徽章数字是另一回事。用户是在网吧玩的,每次都是全新安装、所有 Mod 都是首次下载。

原因(都指到文件行)

  1. src/celemod-ui/src/stores/download.ts:395(原 :335):downloadMod() 发现同名任务已经是 pending 就直接 return existingTask,既不发起新请求,也不触发传入的 onFinished/onFailed。于是 await 它的调用方永远等不到结果:
    • src/components/ModList.tsx 的下载按钮 —— 点了没反应;
    • src/routes/Manage.tsx:1606 downloadMissing()(「安装缺失依赖」按钮)—— 按钮永久停在「下载中…」且 disabled。
      组件不重挂(= 重进页面)就一直卡着。
  2. src/celemod-ui/src/stores/download.ts:114:取消之后任务仍是 state: "pending"(另带 canceled 标记),ModList.tsx:217 的 if (downloadTask) { ... return; } 只认 failed,于是取消过/卡住过的 Mod 再也点不动;DownloadList.tsx 的重试按钮同样只有 failed 才有 —— 卡住时唯一的出路是取消,取消后就没法重下,只能重进页面或换个入口(RecommendMods.tsx 恰好允许 canceled 重试,所以「换个页面又能下」)。
  3. src/celemod-ui/src/components/DownloadList.tsx:311(DownloadListPage):页面渲染所有 requested 任务,而 store 里没有任何地方删除任务(download.ts 只有 tasks: {} 初值)。已完成的任务因此永远停在 100%;同时 visibleCount 只数非 finished,出现「100% 的任务行 + 计数 0 项」的自相矛盾,直到重启 App。
  4. 后端超时是好的但很容易被改回去:src-tauri/src/ureq.rs:39(download_agent,PR fix(download): 下载链路走 ureq 自带的环境代理 + 补上读写超时 #84 加的 timeout_connect/read/write = 30s)是「连上但不发数据」唯一的兜底,没有测试锁住。

修复

  • 新增 src/celemod-ui/src/downloadTasks.ts:把「这次该开新尝试还是挂到已有尝试上」和「已完成的行什么时候退场」抽成纯函数,便于测试。
  • downloadMod():仍在下行时把新调用方的 onFinished/onFailed 接到这次尝试上(不会再吞掉请求);已取消/已失败的任务正常开新尝试。
  • Mod 列表、下载页的按钮:canceled 也可重试(下载页的重试按钮从「只有 failed」放宽到「failed 或 canceled」)。
  • 下载页:已完成的行显示 4 秒后自动消失,计数与实际列出的行一致。
  • ureq.rs:新增 download_agent_bounds_every_network_wait,断言 download agent 带 30s 的 read/write/connect 超时。

改动范围仅前端 + 一个测试,不动下载协议、不动镜像选择逻辑。

验证

  • cargo test --lib → 114 passed(含新测试)
  • cargo fmt --check 干净
  • pnpm typecheck(tsc --noEmit)干净,prettier 干净
  • node --import tsx --test src/downloadTasks.test.ts → 5 passed;仓库现有前端测试(catalog-download / crash-trace / deep-link / mod-updates)全绿
  • 新 pnpm test:download-tasks 已加进 package.json

顺带查到、但不在本 PR 里的两件事(供参考)

  • 镜像未准备的文件现在直接 500:GET celeste.weg.fan/api/v2/download/gamebanana-files/1848970 → 302 → /api/v2/download/files/590035773449893743 → HTTP 500 {"code":10000,"message":"制作镜像失败"}(57 B,0.33 s,实测;/api/v2/mirror/status/590035773449893743 一直是 0/0,说明没有排队任务)。CeleMod 把它当普通网络错误重试 3+3 次后失败,报错只有 status code 500。对「首次下载」的用户来说,这条路径体验就是「点了下不动/失败」。
  • 镜像有现成的「制作进度」接口,流程是「先触发制作 → 轮询进度 → 进度满了再下载」(参考实现:群主的 Everest 镜像插件 ChinaMirror.cs 的 PrepareFile(),只在镜像选 weg.fan 时走):
    • HEAD 下载 URL 且不跟随重定向,从 Location 取 /api/v2/download/files/(\d+) 当 fileId;
    • POST /api/v1/mirror/start-download?type=&fileName={fileId} 开始制作(本机只验到 GET 会回 code:400);
    • 每 200ms GET /api/v1/mirror/status?type=…&fileName={fileId}(v2 等价路径 /api/v2/mirror/status/{fileId},v1 会 302 过去),字段 downloadProgress/uploadProgress 的 current/total;total == 0 30 s、有进度但 10 s 不动算超时。
    • 把这条接进 CeleMod 是另一条任务在做,本 PR 不动这块,只做「下载卡住 / 进度条不消失」。

来源

Celeste Coders 群用户报告(下载卡住 / 需重进页面才能重下;已完成的行不消失),以及群主关于「镜像要先从香蕉网回源,首下会慢」的线索。

下载页/Mod 列表在几种情况下会永久卡住,只能重进页面:

- downloadMod() 在「同名任务还是 pending」时直接返回,不触发任何回调,
  于是 await 它的调用方(Mod 列表按钮、管理页「安装缺失依赖」)的 promise
  永远不 settle,按钮停在「下载中…」且被 disabled。
- 取消后任务仍是 pending(另带 canceled 标记),Mod 列表点下载直接 return,
  下载页只有 failed 才有重试按钮 → 取消过就再也重下不了。
- Downloads 页渲染所有 requested 任务且没有任何地方清理任务,已完成的行会
  一直停在 100%,而侧边栏徽章只数 pending,两处长期不一致。

改动:
- 新增 downloadTasks.ts:attempt 决策(start/attach)与已完成行的可见性规则。
- downloadMod():仍在下行时把新调用方的回调接到该次尝试上;已取消/已失败
  则正常开新尝试。
- ModList / DownloadList 的下载按钮:canceled 也可重试。
- DownloadListPage:已完成的行保留 4 秒后自动消失,计数随之一致。
- ureq.rs 增加回归测试,锁住 PR std-microblock#84 的读/写/连接超时(没有它就再也无法从
  「连上但不发数据」的卡死里恢复)。
@std-external

Copy link
Copy Markdown
Contributor Author

补一组 1.2.2 的实机证据(同一现象,用户新反馈)

用户在 CeleMod 1.2.2 上复现(本 PR 尚未合并,所以现象依旧),下载页里 4 行标着「已完成」的 Mod 一直挂在列表里:

1.2.2 已完成的行停在下载页

(截图仅裁出 CeleMod 窗口,去掉桌面/其它窗口。)

1) 「已完成的行不消失」是 bug,不是「保留历史」的设计

  • 同一个页面的头部计数 visibleCount 本来就排除 finished(DownloadList.tsx:319-321,改动前),侧边栏徽章也只数 pending(App.tsx:78-83)——页面自己都不认为完成的任务该继续列出来,只是渲染时漏了过滤。
  • 全仓库没有任何删除/清理任务的地方(stores/download.ts 只有初值 tasks: {},没有任何 clear*/remove* 动作),也没有「清除已完成」这类文案(locales/zh-CN.json 里查不到)→ 连手动点掉的办法都没有,只能重启 App。这也是用户问「为啥不会直接消失」的直接原因。
  • 翻历史:这个下载页布局由 a47a9ee feat(downloads): enhance UI for Downloads page(2026-09-04)引入,从那以后就没有过自动清理 —— 属于从未实现,不是被删掉的回归。

2) 「100% 125 KB/125 KB 但 0 B/s」不是另一条卡死路径

它是已完成任务的正常外观,不是「速度上报停了但完成事件没发」:

  • 后端收到 worker 的 Finished 时显式把速度清零:backend.rs:2748 tasks[index].speed_bytes_per_sec = 0.0;(同时把 data 设成 "100")。
  • 前端进度优先用字节计数器(DownloadList.tsx:27-38 getTaskProgress)→ 字节相等就是 100%。
  • 该行同时标着「已完成」(stateLabel),说明完成事件已经发到前端;它只是没有被退场(就是上面第 1 条)。

录屏里唯一"像卡住"的是依赖行 CollabUtils2 显示 0 B/0 B——那段时间它还没拿到首个字节(HEAD/probe 阶段,前端只有 Waiting 或还没有任何进度事件时就是 0/0)。同一段录屏后面这个依赖出现在 3.7 MiB / 5.2 MiB (71%), 29.3 KiB/s,说明它在动,只是慢。

3) 顺带确认的一条「下载中很慢」的成因(不在本 PR 内)

录屏里同时有十几个任务在跑,每个只有 5–30 KiB/s(StrawberryJam2021Assets 16.7 KiB/s、...AudioA 5.5 KiB/s、JackalHelper 11.7 KiB/s …)。这和后端没有并发上限一致:start_waiting_mod_downloads(backend.rs:2613-2671)把所有 Waiting 任务一次全部启动,每个任务再多线程分段(ureq.rs:20 NUM_THREADS = 8)。所以一个「批量下载/安装缺失依赖」会一口气开几十路连接,在镜像限速 + 网吧带宽下每个都很慢——看起来就像卡住。这条要不要加并发上限/队列显示,建议单独决定,本 PR 不动。

本 PR 现在的效果(对照)

  • 已完成的行显示 4 秒后自动退场(不再需要重启),失败/已取消的行保留重试按钮;
  • 重复请求不再被静默丢弃、已取消/已失败都能重下(原来按钮会永久卡住)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant