Skip to content

harmonyos: resumable model download (pause/resume/cancel + cross-rest… - #36

Open
Leeson-Wong wants to merge 1 commit into
OpenBMB:mainfrom
Leeson-Wong:feature/download_resumable
Open

harmonyos: resumable model download (pause/resume/cancel + cross-rest…#36
Leeson-Wong wants to merge 1 commit into
OpenBMB:mainfrom
Leeson-Wong:feature/download_resumable

Conversation

@Leeson-Wong

Copy link
Copy Markdown
Contributor

HarmonyOS:可恢复的模型下载(暂停 / 继续 / 取消 + 跨重启 reattach)

简介 | Summary

为 HarmonyOS 客户端增加可恢复的、以文件系统为对账依据的模型下载子系统:支持暂停 / 继续 / 取消并保留 .tmp 断点;App 重启后能 reattach 仍在存活的系统下载任务;多源竞速;UI 通过观察者模式与传输层彻底解耦。仅 HarmonyOS、仅 ArkTS。

Adds a resumable, filesystem-authoritative model-download subsystem to the HarmonyOS client: pause / resume / cancel with partial-file preservation, cross-restart reattachment of surviving system download tasks, multi-source racing, and a UI fully decoupled from the transfer via an observer pattern. HarmonyOS only, ArkTS only — no native code touched.

关联 issue | Linked issues

背景 | Motivation

原来的 HarmonyOS 下载器没有暂停 / 取消,也无法跨 App 重启恢复:杀进程会留下写了一半的 .tmp,用户只能从头再来。模型文件动辄数 GB,在移动网络波动或误杀 App 时丢进度是主要痛点。

The previous downloader had no pause/cancel and no recovery across app restarts: killing the app mid-download left a half-written .tmp and the user had to start over. Model files are multi-GB; losing progress on a flaky connection or an accidental app kill was the main pain point.

设计 | Design

设计方法论来自 DVA——领域建模(Domain Modeling)的个人实践总结,也可简单理解为领域建模。核心判据:变化速率对齐的归在一起,不对齐的拆开。 文件系统是唯一权威(扛任意 kill),UI 退成纯观察者。

The design methodology follows DVA — a personal synthesis of domain modeling; it can be read simply as domain modeling. Core criterion: group what changes together, separate what changes at different rates. The filesystem is the single source of truth (survives any kill); the UI is a pure observer.

时序图 | Sequence — 暂停 → 杀进程 → 重开 → 继续

静态结构见下方「实体 / 关系」表;本图展示动态行为——跨重启如何不腐败地恢复(本 PR 最关键、也最难一眼看懂的部分)。

Static structure is in the 实体 / 关系 tables below; this diagram shows the dynamic behavior — how a download recovers across a restart without corruption (the PR's most important, hardest-to-grasp part).

sequenceDiagram
    autonumber
    participant U as 用户
    participant MM as ModelManager
    participant Svc as Service
    participant DM as DownloadManager
    participant T as request.agent.Task
    participant FS as .tmp(文件系统)

    U->>MM: 暂停
    MM->>Svc: pause()
    Svc->>DM: pauseDownloadForFile
    DM->>T: task.pause()
    Svc-->>MM: 发 paused(保留 .tmp)
    Note over U,FS: ── 用户杀掉 App ──
    Note over FS: .tmp 留在盘上 = 对账依据
    Note over U,FS: ── 重新拉起 App ──
    MM->>Svc: attach()
    Svc->>DM: reattachSystemTasks(search / show)
    DM->>T: getTask + 重挂 progress/completed/failed
    Svc->>FS: reconcileFromFs(statSync .tmp)
    Svc-->>MM: 发 paused(按 fs 字节,非 running)
    U->>MM: 继续
    MM->>Svc: resume()
    Svc->>DM: resumeDownloadForFile
    DM->>T: task.resume()
    T->>FS: 续写字节
    Svc-->>MM: running → completed
Loading

实体 | Entities

实体 Entity 职责 变化速率
ModelManager / Index 展示与用户操作 高(交互 / 导航)
DownloadSnapshot(AppStorage) 单一来源的状态快照 高(每次进度)
ModelDownloadService 下载生命周期状态机
DownloadManager 传输机制(race / single / reattach) 低(平台 API)
request.agent.Task 字节传输 低(系统服务)
.tmp 文件 断点字节(对账依据) 低(磁盘,kill 后仍在)
LlamaEngine / TtsEngine 推理引擎 + loadedModelId

关系 | Relationships(关键的"拆开"决策)

关系 不对齐点 处理
下载流 → UI UI 变化(导航)≠ 传输 外化:Observer(AppStorage 广播)
字节数来源 内存回调(快 / 会撒谎)≠ 磁盘(慢 / 权威) 外化:fs 为准,回调仅 500ms 乐观覆盖
选中模型 vs 已加载模型 选择(中频)≠ 加载(低频) 拆开:loadedModelId 独立追踪

实现 | What ships

  • 暂停 / 继续 / 取消,保留 .tmp。暂停保留断点,取消才丢弃。
    Pause / Resume / Cancel with .tmp preservation. Pause keeps the partial file; Cancel discards it.
  • 跨重启 reattach。App 重启时通过 request.agent.search 找回存活的系统任务并重新挂回调,不会对同一 .tmp 起第二个任务导致冲突;杀进程前处于暂停的任务,重启后正确显示为「继续 / 取消」(而非「暂停 / 取消」)。
    On relaunch, surviving request.agent tasks are discovered and re-attached, so no duplicate task ever writes the same .tmp. A task paused before the kill is surfaced as Resume/Cancel (not Pause/Cancel).
  • 文件系统是唯一权威。已下载字节数永远来自 fs.statSync(<file>.tmp),进度回调只是乐观覆盖。App 被任意时刻杀掉后,都能从盘上重推出一致状态。
    Bytes-on-disk always comes from fs.statSync(<file>.tmp); progress callbacks are an optimistic overlay only. Coherent state is re-derived from disk after an arbitrary kill.
  • UI 单一数据源ModelDownloadService 把结构化 DownloadSnapshot 发到 AppStorage,ModelManager / Index 是纯观察者(@StorageLink + @Watch),与传输层完全解耦。
    ModelDownloadService publishes a structured DownloadSnapshot to AppStorage; pages observe via @StorageLink + @Watch, fully decoupled from the transfer.
  • HTTP Range 续传:用 request.agentConfig.begins 追加到已有 .tmp;续传前把竞速残留的 .tmp 合并成最大的那个。
    Range resume via Config.begins; race-leftover .tmp consolidated to the largest before resuming.

已知局限 | Known limitation

以下行为已知,本 PR 未改动,根植于现有下载基础能力。

The following behaviors are known, not changed by this PR, and rooted in the existing download infrastructure.

实际影响:

  • 字节传输由系统下载服务执行,所以划掉 / 杀掉 App 并不会停止下载。停止下载的唯一可靠方式是 App 内的「取消」按钮。
    The transfer runs in the OS download service; swipe/kill does not stop it. Use the in-app Cancel button.
  • 没有系统下载通知
    No system download notification.
  • 多文件冷启动续传:reattach 后只经存活任务续传当前那一个文件,后续文件不会自动排队(下载循环不跨进程存活);重新开始下载时通过 Range 从 .tmp 续上。
    Multi-file cold-start resume: only the in-flight file resumes via the surviving task; the queue is not auto-continued. Re-starting resumes from .tmp via Range.

验证 | Verification

Device: HUAWEI Pura X Max 典藏版 (HOP-AL10)
        HUAWEI Pura 90 Pro Max
OS:     HarmonyOS NEXT 6.1.1(24)
ABI:    arm64-v8a

实测通过:

  • 加载模型 → 重进模型管理 → 状态栏正确显示「已就绪」(此前因 LLM / TTS 引擎监听器"订阅即发射",TTS 的 Uninitialized 覆盖了 LLM 的 ModelReady,误显示「未初始化」)。
  • 切到别的(未加载)模型 → 中性状态、「加载」按钮(不再残留「已就绪」);选中模型不再自动加载,改为显式点「加载」。
  • 暂停 → 杀进程 → 重进 → 显示「继续 / 取消」(而非「暂停 / 取消」)。
  • 快速点暂停可重试,不会进入按钮失效的死状态。

Verified on both devices above: status bar shows "Ready" correctly after re-entering Model Manager (previously showed "Uninitialized" due to a subscribe-time state clobber between LLM/TTS listeners); selecting a non-loaded model shows neutral status + "Load" (no stale "Ready") and no longer auto-loads; pause → kill → relaunch shows Resume/Cancel; rapid Pause is retryable with no dead-button state. Other HarmonyOS hardware classes not covered — happy if the team can verify on additional devices (Mate 60 / X series / tablets) where available.

改动文件 | Files

仅 HarmonyOS。无 iOS / Android / native 改动。
HarmonyOS only. No iOS / Android / native changes.

区域 Area 文件 Files
新增下载子系统 entry/src/main/ets/download/{DownloadState,ModelDownloadService,Reconciler}.ets
传输层 entry/src/main/ets/utils/DownloadManager.ets
页面 entry/src/main/ets/pages/{ModelManager,Index}.ets
引擎 entry/src/main/ets/engine/{LlamaEngine,TtsEngine,ModelInfo}.ets
文案 entry/src/main/resources/{base,en_US}/element/string.json

…art reattach) with cold-start paused-task fix
@tc-mb

tc-mb commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

我在 1f547bc 上做了真机测试:TYR-AL00、OpenHarmony 6.0.2.130,clean build、签名、安装和首次启动下载均正常。

目前有一个可稳定复现的阻断问题:

  1. 首次下载会创建 2 个多源 race task。
  2. force-stop 后重启,reattach 能找到这 2 个 task,但页面随后恢复成“可下载”状态。
  3. 再点一次下载并重启,request.agent.search 返回 4 个 task。

也就是说,同一模型会被重复创建下载任务,可能重复消耗数 GB 流量,并并发操作同一模型文件。原因看起来是 reattach 发布的 paused 状态又被后续文件扫描覆盖;在 tmp 尚未产生时,页面回到了 idle。

另外请一起检查:

  • race task 实际写入 .rN.tmp,reattach completed 回调却固定重命名 .tmp
  • 多文件模型 reattach 后不会继续后续文件;
  • 只有 tmp、没有存活 task 时,Resume 会进入无实际任务的 running 状态。

这些下载生命周期问题修复并补充对应真机用例后,再考虑合入会更稳妥。

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