Skip to content

feat(update): dashboard 展示二进制下载进度并挂接进行中的更新 - #1825

Merged
deepcoldy merged 2 commits into
deepcoldy:masterfrom
47seek:fix/update-download-progress-20261012
Oct 12, 2026
Merged

deepcoldy merged 2 commits into
deepcoldy:masterfrom
47seek:fix/update-download-progress-20261012

Conversation

@47seek

@47seek 47seek commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator

背景

Web Dashboard 独立二进制更新(install.sh 形态)从 GitHub Releases 同步下载约 174MB 资产,POST /api/update/run 长时间阻塞:前端无进度反馈,用户重复点击撞上 409 update_in_flight 后直接渲染「更新失败」。

错误分类(锁前/锁异常误报 409)由并行 PR 修复,本 PR 只覆盖下载体验。

改动

  • src/core/binary-self-update.ts:BinarySelfUpdateDeps 新增 onProgress。下载 pipeline 中插入计数 Transform,按 Content-Length 报告字节总量与已接收字节(chunked/测试流无 Content-Length 时 bytesTotal=null);checksum/probe/swap 阶段持续回报,100% 下载后 UI 仍显示进行中。
  • src/dashboard.ts:模块级 binaryUpdateProgress;/api/update/status 增加本进程 updateInFlight 与 binaryUpdate(phase/version/bytesReceived/bytesTotal)。升级与回滚两条二进制下载路径都接入,finally 清理。
  • 前端(顶部胶囊 app.tsx + 设置页 settings-page.tsx + update-action.ts):
    • POST 等待期间每秒轮询 status,真实下载百分比映射进进度环安装段(download 5–45%,checksum/probe/swap 停在 46/47/48%,重启段 50%+ 不变);npm/local-dev 无字节,保留原有拟真进度。
    • POST 收到 409 update_in_flight 不再报失败:挂接另一个标签页或 CLI/定时任务中的更新——有字节显示 received/total (percent%),仅本进程标志位为真显示「另一个更新进行中」,跨进程持锁(标志位不可见)显示外部等待文案。
    • 完成判定只依据版本状态(behind === false / current 变化),不依据 updateInFlight === false;完成后自动 reload,10 分钟超时提示手动确认并重启。
    • maintenance.ts 与 CLI 调用不传 onProgress,行为不变。

验证

  • tsc --noEmit、tsconfig.scripts.json、tsconfig.test-mocks.json 均通过;node scripts/build-dashboard.mjs 打包通过。
  • 新增测试:binary-self-update onProgress 事件序列/字节单调性/无 Content-Length(2 项);update-action 错误码携带、formatBytes、进度环映射、文案、classifyOtherUpdate 四态、pollUpdateStatus 停止语义(9 项)。
  • test/binary-self-update.test.ts + test/dashboard-update-action.test.ts:Vitest 100 项通过,bun test 100 项通过。
  • dashboard/update/binary 相关 140 个测试文件 2757 项通过;全量 unit 中 25 个环境/集成文件失败(pm2 socket 被本机会话占用、seccomp/cgroup 内核特性、dist 编译产物缺失、EMFILE),已在未改动基线工作树复现同类失败,与本改动无关。

与并行错误分类 PR 的交界

两个 PR 都在 /api/update/run 二进制分支改动,hunk 相邻(本 PR 改 onProgress 接线与 finally,对方改 catch 的 FileLockTimeoutError 判定),合并时可能有一处轻量冲突,后合者 rebase 即可。

🤖 Generated with Claude Code

独立二进制更新从 GitHub Releases 下载约 174MB 资产,同步 POST 期间前端
只能干等,重复点击撞上 409 update_in_flight 时直接渲染更新失败。

- binary-self-update 新增 onProgress:下载流上统计字节数(Content-Length
  可得时给出总量),并在 checksum/probe/swap 阶段继续回报进行中状态
- /api/update/status 暴露本进程 updateInFlight 与 binaryUpdate 实时进度
- 顶部胶囊与设置页在 POST 等待期间轮询 status,真实百分比映射进安装段
  进度环,checksum/probe/swap 阶段保持进行中显示
- POST 收到 409 update_in_flight 时不再报失败:按 status 挂接另一个标签页
  或 CLI/定时任务中的更新,有字节显示字节,无字节显示等待;完成仅依据
  版本状态判定(跨进程持锁不翻转 dashboard 内存标志),10 分钟超时提示
  手动确认

Co-Authored-By: Claude Code <noreply@anthropic.com>
review 两项 P2:

1. 持续 503、网络异常或请求挂起时,超时判断位于 status 成功回调内,
   10 分钟超时永远无法触发,顶部即使进入超时分支也不会停轮询。
   pollUpdateStatus 的 timeoutMs/onTimeout 改由独立定时器驱动,触发时
   先自动停止轮询再回调;两处观察入口(顶部胶囊、设置页)接入。

2. 观察到版本状态完成(behind=false/current 变化)只代表磁盘上安装
   完成,dashboard 实例并未变化:胜方可能仍停在设置页重启确认上,CLI
   更新则根本不重启。顶部胶囊不再直接 reload,改转 restarting 阶段,
   复用 /__selfcheck 实例变化判据(90s 无变化提示手动刷新);设置页
   观察完成只刷新状态并提示重启后生效,不打断既有重启确认。完成判据
   抽为 observedInstallCompleted 纯函数,两处复用。

新增 8 个测试:超时在持续失败/持续 503 下仍触发且自动停止、stop 后
不触发;observedInstallCompleted 更新/回退各场景。共 108 项 Vitest
与 bun test 通过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
@deepcoldy

Copy link
Copy Markdown
Owner

感谢这个 PR!下面是自动评审的初步意见,最终以维护者审阅为准。

总体结论

改动质量很好,没有阻断性问题:下载计数管线、四阶段(download/checksum/probe/swap)事件顺序、409 update_in_flight 改为挂接观察而非报错、完成判定只信版本状态(不依赖本进程 flag)、reload 只信 dashboard 实例变化、独立超时计时器等关键路径都处理正确;现有测试对阶段顺序、字节单调不越界、无 Content-Length、前端四态分类与超时停止语义的覆盖也比较完整。

一个非阻断的测试强度建议

onProgress 的字节计数目前只用单 chunk 流(Readable.from([payload]))验证。单 chunk 时,即使实现把累加误写成重置,用例也测不出来:

bytesReceived += chunk.length;  // 若误写成 = ,单 chunk 流结果相同

建议补一条多 chunk 用例,把「逐 chunk 累加」和「pipeline 开始前先发一帧 0」一起钉住(复用现有 fakeAsset(byte, size) 与 probeOk):

  it('accumulates bytesReceived across multiple chunks', async () => {
    const dir = tmp();
    const target = join(dir, 'botmux');
    writeFileSync(target, 'OLD BINARY', { mode: 0o755 });
    const chunkA = fakeAsset(0x41, 600_000);
    const chunkB = fakeAsset(0x42, 1_200_000);
    const events: BinaryUpdateProgress[] = [];
    const stream = Object.assign(Readable.from([chunkA, chunkB]), {
      headers: { 'content-length': String(chunkA.length + chunkB.length) },
    });
    await replaceStandaloneBinary('3.99.0', target, {
      fetchStream: async () => stream,
      fetchChecksum: async () => null,
      probeBinary: probeOk,
      onProgress: event => events.push({ ...event }),
    });
    const received = events.filter(e => e.phase === 'download').map(e => e.bytesReceived);
    // First frame is the pre-pipeline 0; then one per chunk, cumulative.
    expect(received).toEqual([0, 600_000, 1_800_000]);
  });

该用例在当前实现下通过;若计数误写成 =,第二个 download 帧会变成 1_200_000 而非 1_800_000,精确失败。

另一个可选小项

OtherUpdateState 包含 'completed',但顶部胶囊文案只显式分支了 busy / waiting;回滚观察的「已替换待重启」窗口会落到 rollbackInstalling 兜底文案,行为是正确的,补一行注释说明即可,不强制。

CI 说明

本分支基底较旧,CI 里唯一的红腿 bun-test(test/summary-command-window.test.ts 报 effectiveBotDisplayName 导出缺失)是主干最近已修复的基线问题,与本改动无关;rebase 到最新 master 后该测试即转绿。

再次感谢,进度环与挂接观察的体验改进很实用。

@deepcoldy
deepcoldy merged commit 4c72b79 into deepcoldy:master Oct 12, 2026
10 of 11 checks passed
@github-actions

Copy link
Copy Markdown

🚀 Released in v3.43.0

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.

2 participants