首页
/ Project NOMAD 更新机制全指南:软件核心、Apps 与离线内容的三线更新实战

Project NOMAD 更新机制全指南:软件核心、Apps 与离线内容的三线更新实战

2026-09-08 18:44:01作者:温玫谨Lighthearted

Project NOMAD 是一台离线优先(offline-first)的知识与教育服务器,它的核心设计哲学是:在有网时把软件与内容更新到最新,到无网环境后一切立即可用。本文以项目官方文档 admin/docs/updates.md 为主体,结合仓库中自动更新服务、定时任务、更新调度页面等源码,完整讲解三类可更新对象(软件核心 / Supply Depot 应用 / Wikipedia·ZIM·地图内容)的手动更新步骤与自动更新配置,并逐层拆解“时间窗口 + 冷却期 + 主版本不放行 + 安全预检 + 失败自禁用”这套自动更新决策管线的实现细节,帮助自托管者在掌握行为边界的前提下把它调教到“省心且不失控”。

一、先建立心智模型:三类更新,各自独立控制

在 Project NOMAD 中,“更新”不是单一动作,而是三个相互独立、可分别控制的通道。只有先分清它们,才能正确使用后面的所有配置:

类别 指向对象 触发入口(UI) 典型体量
软件核心(Software / Core) Command Center、新功能、Bug 修复与安全改进 Settings → Check for Updates / Updates 镜像级更新,通常 2–5 分钟
应用(Apps) Supply Depot 中可安装的应用(Kiwix、AI Assistant 等) Supply Depot 各应用卡片 Manage → Update 容器镜像更新
内容(Content) 已安装的 Wikipedia 等 Kiwix 库、下载的地图区域 Settings 中的内容管理/内容浏览页 ZIM 常达数 GB

三者都可以“手动触发”,也可以各自“设为自动更新”。把“有网时保持最新”作为习惯,才能让离线体验始终处于“最后同步到的最新版本”。

前端设置入口统一收敛在 admin/inertia/pages/settings/update.tsx(对应 UI 导航 Settings → Update),供应链/应用商店入口见 admin/inertia/pages/supply-depot.tsx

二、手动更新:按需检查与安装

1. 软件核心更新与失败回滚

手动升级核心的路径在 Settings → Check for Updates:若检测到新版本,点击安装即可,NOMAD 会下载更新并重启容器(通常 2–5 分钟)。该页面的前端实现把整个流程建模成一段可轮询的阶段状态机,状态标签见 admin/inertia/pages/settings/update.tsx

idle → starting → pulling → pulled → recreating → complete
                                          ↘ error
  • pulling / pulled:拉取所有核心容器的新 Docker 镜像;
  • recreating:安全地停止并重建核心容器;
  • complete:前端检测完成后自动重新加载页面,并用 checkLatestVersion 清除 KVStore 中陈旧的“有可用更新”标记,避免出现 “current → current” 的假横幅。

页面实现中有两个值得注意的工程细节:

  1. 轮询容错:更新期间 admin 容器会重启导致连接中断,前端轮询捕获失败后并不报错,而是显示“连接暂时中断(符合预期)”的提示并继续轮询,直到容器恢复。
  2. 阶段竞态处理:sidecar 停留在 complete 阶段只有约 5 秒(见 install/sidecar-updater/update-watcher.sh),SPA 在 admin 容器重启时可能错过该窗口;因此前端用 seenAdvancedStageRef 记录是否见过高级阶段,一旦从 recreating 等阶段回到 idle,就视为“错过窗口的完成事件”而非失败。

页面同时提供 “View Update Logs”,拉取真实更新日志(后端接口在 admin/app/controllers/system_controller.ts,对应路由注册见 admin/start/routes.ts)。官方给出的三点执行预期也写在 UI 上:先拉镜像 → 再重建容器 → 完成后页面自动刷新,并提示用户“更新前建议备份关键数据”“更新期间服务会短暂不可用”。

关键的安全承诺是优雅降级:即使软件或应用更新失败,之前正常工作的版本会继续运行,服务器不会因此停机。

2. Apps 手动更新

应用在 Supply Depot 各自的应用卡片上通过 Manage → Update 更新。与“核心由 sidecar 代为重建”不同,兄弟应用由 admin 容器直接操作 Docker:从源码看,DockerService.updateContainer 走的是进程内 pull → 重命名替换 → 健康检查 → 必要时回滚的更新链路(参见 admin/app/services/docker_service.ts),应用级更新不需要 sidecar。

3. 内容手动更新(ZIM 与地图)

内容更新由 Settings 下的内容管理与内容浏览界面承载,可对已安装的 Kiwix 库与地图下载更新版本。在 Settings → Update 页面内,同时内置了一个 Manual Content Updates 面板(组件见 admin/inertia/components/updates/ContentUpdatesSection.tsx),其工作流为:

  1. 点击 Check for Content Updates,调用 GET /api/content-updates 检查通道(由 admin/app/controllers/collection_updates_controller.ts 提供),对照上游 Kiwix / 地图目录逐一比对已安装资源版本;
  2. 结果以表格呈现:标题、类型(ZIM/Map)、新版本体积、installed_version → latest_version,用户可对单个资源点 Update,也可一键 Update All
  3. 更新被投递为可断点续传的下载任务(组件会主动使下载列表失效,让过小的更新也能立即出现在 Active Downloads 中,而不是等待空闲轮询)。

三、自动更新的共同安全边界

NOMAD 的自动更新默认全部关闭、必须显式开启(opt-in)——在你打开之前,任何东西都不会自行更新。三种自动更新共享如下设计原则,全部集中在 Settings → Updates 管理:

  • 时间窗口自选:自动更新只在设定的时段内执行,避免打断使用(窗口取容器本地时间,通过 TZ 环境变量指定);
  • 大版本绝不自动:只有 minor / patch 版本才会自动生效,主版本跳跃始终留给你手动执行,这是有意的设计;
  • 安全预检先行:应用任何更新前,NOMAD 都会确认磁盘空间充足、且当前没有其他更新/下载/安装正在执行;
  • 离线无害:若无法联网检查,就跳过本轮,稍后重试,绝不报错中断。

这三条边界不只是在文档层面承诺,而是在代码里被严格实现为一条**“决策管线”。核心服务 admin/app/services/auto_update_service.ts 的注释将其概括为:服务本身“不重建容器”,只负责决策**——是否启用、是否在窗口内、是否存在已过冷却期的 minor/patch 版本、预检是否通过——决策通过后带着校验过的镜像 tag 驱动既有更新通道,实际的容器重建由 sidecar 完成。通用判定产出是一个无副作用的 AutoUpdateDecision

disabled | outside-window | eligibility-error | no-eligible | blocked | ready

evaluate() 是无副作用的决策核心,attempt()dryRun() 都建立在它之上,因此干跑(dry-run)能忠实地反映真实运行会做什么。三个 dry-run 命令入口见 admin/commands/auto_update/dry_run.tsadmin/commands/app_auto_update/dry_run.tsadmin/commands/content_auto_update/dry_run.ts。由于决策器支持注入“当前版本、release 列表、固定时钟、强制窗口、假预检”等输入,自托管者可以在实施自动更新前精确模拟任意场景。

底层判定依赖的通用工具包括:

  • 窗口判断 admin/app/utils/update_window.ts:支持 HH:MM 24 小时制解析与跨午夜窗口(如 22:00–02:00,即 start > end 时取 current >= start || current < end);起止相同视为零长度窗口返回 false。
  • 版本比较与主版本解析 admin/app/utils/version.ts:主版本相同性判断是“大版本不自动”的直接依据。
  • 镜像磁盘预检 admin/app/utils/image_disk_preflight.ts:把所有阻断因素分成 skip(瞬时,下一窗口自动重试、不计惩罚)failure(真故障,计入退避,最终导致自动禁用) 两类。

四、自动软件(核心)更新

Settings → Updates 打开开关后,NOMAD 会在你设定的窗口内、经过可配置的**冷却期(cool-off)**之后,把核心升级到同主版本内的新版本。同一页面集中展示:开关、窗口、冷却期与实时状态。UI 实现见 admin/inertia/components/updates/CoreAutoUpdateSection.tsx,对应 KV 配置键如下:

配置键(KVStore) 含义 默认值
autoUpdate.enabled 总开关 false
autoUpdate.windowStart / windowEnd 服务器本地时区下的执行窗口 02:00 / 05:00
autoUpdate.cooloffHours 新版本发布后延迟生效的小时数 72
autoUpdate.lastResult / lastError / consecutiveFailures / autoDisabledReason 运行状态记录与失败退避

冷却期在 UI 上提供四档:24h(1 天)、48h(2 天)、72h(3 天)、168h(7 天)。设置存储在 KVStore 中,读写实现见 admin/app/models/kv_store.ts。注意代码中的一个细节:未设置时 Number(null) === 0,所以实现会区分“显式填 0”与“未设置”,未设置回落到默认 72 小时,避免静默出现“零冷却”的激进更新。

资格判定(eligibility) 是核心算法的灵魂,逻辑位于 admin/app/services/auto_update_service.tsselectEligibleTarget

  1. 过滤掉 draft 与 prerelease——自动更新从不搭乘 Early Access
  2. tag 必须是严格 semver(正则 ^\d+\.\d+\.\d+$),作为纵深防御:该 tag 会被 sidecar 拼接到宿主侧 sed 中(见 install/sidecar-updater/update-watcher.sh),畸形 tag 必须无法触达;
  3. 与当前运行版本同主版本、严格更新;
  4. 发布时间不早于 now - cooloffHours(冷却期从发布时刻起算);
  5. 开发构建(dev / 0.0.0)直接返回无可更新目标。

版本来源是 GitHub Releases 接口,进程内做了两级缓存:成功结果缓存 15 分钟(避免每次打开状态页都命中未认证 API 的限流),失败结果负缓存 60 秒(离线时反复调用不会每次都阻塞在超时上)。

安全预检 五项(源码 runPreflight):

  • sidecar 必须可用(否则 core 更新无人执行,属 failure);
  • 无系统更新正在执行(skip);
  • 无内容/模型下载在进行(skip);
  • 无应用安装/更新在进行(skip);
  • 磁盘空间足够容纳新镜像(依据 Docker 守护进程架构映射到 OCI 命名 amd64/arm64/arm 后查询镜像大小)。

失败退避与自禁用:只有真实的更新请求失败(ready 分支后的失败)才计入连续失败计数;发布源查询失败这类瞬时错误被当作“离线正常态”直接跳过,触发退避。连续失败达到 3 次(MAX_CONSECUTIVE_FAILURES = 3)后,服务会把 autoUpdate.enabled 置回 false、写入 autoDisabledReason 并在 UI 顶部以警告条展示原因,而不是无限重试。

调度方式:状态页之外,真正驱动它的是一个每小时执行一次的 BullMQ 定时任务 admin/app/jobs/auto_update_job.ts,cron 0 * * * *。设计上窗口可能跨多个小时,因此每小时都评估一次、由 attempt() 自行判断“是否落在窗口内”——窗口只做门禁,不负责叫醒。

五、自动应用更新:双开关与逐应用自禁用

App 自动更新是两级 opt-in:总开关在 Settings → Updates,同时还需在 Supply Depot 每个应用卡片上单独打开 per-app 开关,两者都开启该应用才会自更新。实现见 admin/app/services/app_auto_update_service.ts 与前端 admin/inertia/components/updates/AppAutoUpdateSection.tsx

关键行为:

  • 共享窗口与冷却期appAutoUpdate.enabled 是独立开关,但窗口/冷却期复用 core 的 autoUpdate.windowStart / windowEnd / cooloffHours,避免维护多套时间表;
  • 仍只升 minor/patch:上游 admin/app/services/container_registry_service.ts 的更新发现本就带“同主版本”过滤,服务内再叠加一道主版本校验作为纵深防御;此外 pinned 到 :latest 的应用无法做版本比对,会被标记为不可自动更新;
  • 应用级预检:全局预检确保没有下载在跑;逐应用预检确保该应用当前无操作进行(installation_status === 'idle')且磁盘充足;
  • 逐应用失败退避:单个应用连续失败 3 次后仅自身被禁用(写入该应用的 auto_update_disabled_reason,见 admin/app/models/service.ts 相关字段),其他应用不受影响继续运行——这正对应文档所说的“back off automatically for any individual app that keeps failing”。

执行时通过 DockerService.updateContainer(name, targetVersion) 完成进程内 pull → 替换 → 健康检查 → 回滚的容器更新,调度任务 admin/app/jobs/app_auto_update_job.ts 同样每小时评估一次。

六、自动内容更新:独立窗口、带宽上限与预算调度

已安装的 Wikipedia/ZIM 库与地图区域也可以自动刷新。因为内容下载体积大(常达数 GB),内容更新运行在它自己专属的“夜间”窗口,并配有带宽上限,与软件/应用的时间表完全分离;NOMAD 直接检查上游 Kiwix 与地图目录;当某 Wikipedia 库被替换成新版本时,AI 知识库(Knowledge Base)会自动保持同步。

实现位于 admin/app/services/content_auto_update_service.ts,前端面板见 admin/inertia/components/updates/ContentAutoUpdateSection.tsx。配置键与 UI 表单一一对应:

配置键(KVStore) 含义 默认值 / 建议
contentAutoUpdate.enabled 内容自动更新总开关 false
contentAutoUpdate.windowStart / windowEnd 内容专用窗口(本地时间) 02:00 / 05:00
contentAutoUpdate.cooloffHours 新版本出现后的冷却期 72
contentAutoUpdate.maxBytesPerWindow 每个窗口新增下载字节上限,0 = 不限 0(官方建议至少 0.5 GB/窗口)

带宽上限(Data Cap) 是内容更新区别于其他两类更新的核心机制:UI 以 GB 为单位编辑(0 = 不限),存储时换算为字节。选包算法 selectUnderCap 是一个贪心预算调度

  • 排序策略:最久安装的优先(越陈旧的内容优先级越高),同时间再按体积小者优先,保证可预测;
  • 体积未知(0)→ 推迟(无法安全预算);
  • 体积超过整个窗口上限 → 归入 skippedOversize永不自动启动、只能手动更新,并在 UI 中明确显示 “Skipped — exceeds data cap, update manually”;
  • 体积在剩余预算内 → 选中下载;
  • 否则 → 推迟到下一窗口重试。

每次进入窗口,KVStore 中的窗口预算(windowBytesUsed)会基于 windowResetAt 边界重置一次(cron 每小时触发,但窗口可能横跨数小时,因此只重置一次)。一个细节值得留意:下载真正完成才清零该资源的失败计数RunDownloadJob.onComplete),而投递失败在 ContentAutoUpdateService.attempt 中直接计一次,终态失败由队列 worker 的 failed 处理器记账——这样避免“每个窗口都重新投递成功而永远触发不了自禁用”。资源级退避共享逻辑在 admin/app/utils/content_auto_update_backoff.ts,同样以 3 次为阈值;此外还有功能级退避(如整个目录不可达),连续失败 3 次后关闭整个内容自动更新并写入 autoDisabledReason

内容更新的冷却期以“首次检测到该更新”为计时起点(记录在 InstalledResource 的 available_update_first_seen_at),版本号采用 YYYY-MM 时间戳词法比较(admin/app/models/installed_resource.ts,字段由迁移 admin/database/migrations/1776200000001_add_content_auto_update_fields_to_installed_resources.ts 建立),天然按时间顺序排序。

内容更新绝不“同步安装”:它只投递可断点续传的下载任务,随后交给既有任务完成链路推进已安装版本并重建 Kiwix 库。同一每小时任务还会顺带执行 FDA 药品数据集的鲜度检查(attemptDrugDataset,走药品下载/摄取链路而非目录版本模型),但它与 ZIM/地图共享同一总开关与窗口,且被隔离封装——药品侧的失败不会拖垮目录运行。任务入口见 admin/app/jobs/content_auto_update_job.ts

七、Early Access Channel:抢先体验 RC

在 Settings → Check for Updates 页面启用 Early Access Channel 即可接收候选发布(RC)构建,提前体验正式版之前的新功能。RC 版本可能包含粗糙边缘,页面会以显著文案提醒“RC 版本可能包含 Bug,不建议在稳定性与数据完整性要求高的环境使用”。你可以随时切回稳定版。

实现上这是一个名为 system.earlyAccess 的布尔设置(前端见 admin/inertia/pages/settings/update.tsx 的 Early Access 区与 admin/inertia/hooks/useSystemSetting.ts)。两个值得注意的行为:

  1. 切换后页面会立刻重新检查版本,而不是等用户手动点 “Check Again”——因为开关改变的是“哪些版本有资格”这一判定前提;
  2. 自动更新与 Early Access 是严格隔离的:决策器在选取目标时显式排除 prerelease,因此“开了 Early Access 的服务器会自己装 RC”是错误预期——RC 只会出现在手动检查结果里,自动通道永远只认 stable 的同主版本 minor/patch。

八、下线之前:把“有网更新”变成肌肉记忆

无论选择手动还是自动,最重要的习惯只有一条:在仍有网络时完成更新。手动可以,交给自动更新也可以——关键是确保软件与内容在下一次离线前处于最新状态。离线期间,你手上就是所有内容的最后一次同步版本。

相关速查:

总结:一条可控、可观测、可回滚的更新管线

把文档与源码对照后可以看到,Project NOMAD 的更新体系并非“轮询然后升级”的简单脚本,而是一条贯穿 UI 状态机、BullMQ 每小时任务、无副作用决策器、双级退避与 sidecar 执行器的完整管线。三类更新共享四条纪律——opt-in 默认关闭、窗口内执行、同主版本限定、预检先行;再分别叠加各自的特殊策略:核心更新靠严格 semver 与冷却期保护,App 更新靠双开关与逐应用自禁用,内容更新靠独立窗口、数据上限与“下载完成才算成功”的记账规则。理解了这些边界,你就能放心地在有网环境下把它调到自动挡,让每次离线都从一个“最新状态”开始。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391