Files
cmbot/docs/10-lan-update.md
T
adminandClaude Opus 4.8 51cefab2db docs: pivot update design from LAN/UNC to HTTP transport
- docs/10: HTTP source + Basic Auth; manifest url/sha256/size; download zip +
  verify SHA-256 + extract; HTTP security model; stage notes marked pending
- docs/02/05: add update_user/update_pass; update_source is now an HTTP(S) URL
- tasks 17.18: HTTP pivot task (docs done; code TODO)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 08:54:16 +08:00

17 KiB
Raw Blame History

在线更新设计(HTTP)

本文档原为「局域网更新」设计,现统一改为基于 HTTP(S) 的在线更新:更新源是一个 HTTP 文件服务(如 gohttpserver + nginx),客户端通过 HTTP 下载清单与版本包。文件名沿用 10-lan-update.md 以避免引用断裂;「版本并排 + 指针切换 + 回滚」等与传输无关的设计保持不变,仅「更新源 / 下载 / 安全」改为 HTTP。

1. 文档定位

本文档定义工具的**在线自动更新(HTTP)**设计。它是 docs/09-packaging-release.md 第 13 节列出的「暂不做」能力的后续扩展,按该文档第 262 行要求单独成文,不混入第一阶段打包规则。

适用前提:

  • 多台 Windows 电脑使用同一工具(见 docs/01-product-vision.md、docs/02-prd.md)。
  • 更新源为一个 HTTP(S) 文件服务(当前部署:gohttpserver,前置 nginx,HTTP Basic Auth),托管 manifest.json 与各版本 zip 包。可部署在内网或公网。
  • 主程序仍本地可运行:更新源不可达时降级启动本地版本,不阻断使用。
  • 打包采用 onedir 目录模式(见 docs/09 第 3 节)。目录模式是版本并排的前提;不使用 onefile。

本文档定义设计,不约束最终实现语言;启动器可用 Python、PowerShell 或编译型语言实现。

2. 目标与非目标

2.1 目标

  • 客户端启动时自动检查 HTTP 更新源是否有新版本。
  • 有新版本时自动下载并安装,用户无需手动拷贝。
  • 安装过程不破坏正在运行的程序,不丢失用户数据。
  • 安装失败或更新源不可达时,能降级到本地当前版本继续使用。
  • 支持回滚到上一可用版本。
  • 支持「强制更新」:关键修复发布后阻止旧版本继续启动。

2.2 非目标

  • 不做静默后台下载(更新只在启动时进行,理由见第 6 节)。
  • 不做增量二进制差分(patch)。本阶段以「整版 zip 下载 + 版本并排」为准。
  • 不做安装程序(installer)、注册表写入、开机自启动。
  • 不做按电脑名/用户的差异化版本策略(可作为后续扩展,见第 13 节)。

3. 总体架构

核心约束:运行中的 .exe 无法被覆盖。 因此「替换」必须发生在主程序未运行时,由一个独立的**启动器(Launcher)**在主程序启动前完成。

采用「版本并排目录 + 启动器 + 当前版本指针」方案:新版本整份安装到旧版本旁边,校验完整后再切换指针,最后启动当前版本。绝不覆盖正在使用的版本目录。

优点:

  • 永不与文件锁冲突——从不写入正在运行的目录。
  • 切换原子——只改写一个指针文件,状态非新即旧,不存在「装一半」。
  • 回滚廉价——把指针改回旧版本目录即可。
  • 与 onedir 天然契合——每个版本是一份完整 onedir 产物(打成 zip 下载、解压并排)。

4. 安装目录结构

安装到当前用户始终可写的位置(见第 9 节):%LOCALAPPDATA%\CMBot (即 C:\Users\<用户>\AppData\Local\CMBot)。不安装到 C:\ 盘根或 C:\Program Files—— 两者标准用户默认不可写,会导致自动更新与数据写入失败(见第 9 节)。

%LOCALAPPDATA%\CMBot\        # C:\Users\<用户>\AppData\Local\CMBot
  Launcher.exe              # 用户双击入口,逻辑稳定、极少变更
  current.txt               # 当前版本指针,内容为单行版本号,如 1.1.0
  versions\
    1.0.0\                  # 历史版本,保留以便回滚
      CMBot.exe
      _internal\
      resources\
    1.1.0\                  # 当前版本
      CMBot.exe
      _internal\
      resources\
  data\                     # 用户数据,独立于版本,更新时不动
    config\
      app_config.json       # 用户偏好(输出格式、最近文件夹、最近模板等)
      templates.json        # 用户自定义模板
    logs\
    output\
  staging\                  # 下载临时区,安装成功后清理

说明:

  • Launcher.exe:版本检查、下载、校验、切换、启动主程序。逻辑稳定,本身不参与自更新(见第 11 节)。
  • current.txt:纯文本,仅存当前应启动的版本号。
  • versions\<x.y.z>\:每个版本一份完整 onedir 产物。
  • data\:所有用户可写数据集中存放,不随版本切换变动。
  • staging\:下载的 zip 与解压临时目录,校验通过后再并入 versions\。
  • 整个安装根(含 versions\、data\、Launcher.exe)都必须免提权可写:自动更新需要写入 versions\ 与 current.txt,故安装根不能落在 C:\ 盘根或 C:\Program Files。

5. 数据目录分离(实现前置改造)

当前实现中,配置、模板、日志、输出都位于程序目录下(get_app_dir()/config、/logs、/output)。版本并排方案要求程序目录与数据目录分离,否则每次切换版本都会丢失用户自定义模板与偏好。

这是落地任何自动更新方案的共同地基,已实现:

  • 引入「程序根目录」与「数据根目录」两个概念:
    • 程序根(get_app_dir()):当前运行版本目录 versions\<current>\(只读,更新时整体替换)。
    • 数据根(get_data_dir()):data\(可写,跨版本稳定)。
  • 路径函数划分:
    • get_resource_path() 指向程序根下的 resources/。
    • get_config_path()、get_log_dir()、get_output_dir() 指向数据根下的对应目录。
  • 数据根解析规则(get_data_dir()):
    1. 环境变量 CMBOT_DATA_DIR 非空时使用它。启动器在版本并排布局中将其指向安装根的 data\ 文件夹。
    2. 否则回退到 get_app_dir()——即开发环境(项目根)与当前扁平 onedir 发布(数据与程序同级)的现状布局。
    • 该设计使开发、当前发布、未来版本并排三种布局共用同一套路径规则(呼应 docs/09 第 7 节),且对现状零行为变更:未设环境变量时,config/logs/output 仍在程序目录旁。
  • 兼容旧布局:写入配置/模板/日志/输出前按需创建多级目录(parents=True);数据根不存在时按 docs/09 第 6 节规则首次创建并写入默认配置/模板。
  • 用户数据文件均位于数据根 config/ 下:config/app_config.json、config/templates.json。

数据目录分离后续需同步更新 docs/05-project-architecture.md 与 docs/09 第 5、6 节的目录说明。

6. 更新时机

仅在启动时由启动器检查并安装,不做运行期后台更新。

理由:

  • 桌面工具天然会重启,启动时更新覆盖绝大多数场景。
  • 避免运行期替换带来的文件锁与状态一致性问题。
  • 主程序无需内置更新逻辑,职责更清晰(呼应 docs/04 单一职责)。

更新检查必须非阻塞要害路径:更新源不可达时跳过更新,直接启动本地当前版本。

7. 更新源与版本清单

更新源为一个 HTTP(S) 文件服务,托管清单与各版本 zip。相关配置项在 app_config.json(见 docs/02-prd.md):

  • update_source:清单 manifest.json 的基础 URL 或完整 URL(为空时不做更新检查)。
  • update_user / update_pass:HTTP Basic Auth 凭据(更新源开启鉴权时使用,为空表示匿名)。

当前部署示例(gohttpserver + nginx,HTTP Basic Auth):

http://cm.xiapi.com/
  manifest.json             # 最新版本清单
  CMBot-1.1.0.zip           # 整版 onedir 产物打包
  CMBot-1.0.0.zip

manifest.json 字段:

{
  "version": "1.1.0",
  "url": "http://cm.xiapi.com/CMBot-1.1.0.zip",
  "sha256": "<CMBot-1.1.0.zip 的 SHA-256 十六进制>",
  "size": 57033820,
  "mandatory": false,
  "min_supported": "1.0.0",
  "notes": "修复批量导出格式问题"
}
  • version:最新版本号,遵循 docs/09 第 9 节版本规则,与 src/version.py 的 APP_VERSION 一致。
  • url:该版本 zip 包的下载地址(可为绝对 URL;若为相对路径,相对 update_source 解析)。
  • sha256:zip 包的 SHA-256,下载后校验,防止下载损坏或被篡改(见第 8、14 节)。
  • size:zip 字节数,用于显示进度与粗校验(可选)。
  • mandatory:是否强制更新(见第 10 节)。
  • min_supported:低于此版本必须更新后才能启动。
  • notes:更新说明,可在提示中展示。

版本比较按语义化版本(major.minor.patch)数值比较,不做字符串比较。

8. 更新流程

启动器每次启动执行:

  1. 读取本地版本:读 current.txt;不存在则视为无本地版本。
  2. 读取远端清单:HTTP GET manifest.json(带 Basic Auth)。
    • 失败(网络不可达、4xx/5xx、JSON 格式错误)→ 记录日志,跳过更新,直接进入第 7 步启动本地当前版本。
  3. 版本比较:远端 version ≤ 本地版本 → 无需更新,进入第 7 步。
  4. 下载 zip 到 staging:HTTP GET manifest.url 下载到 staging\<version>.zip(带 Basic Auth)。
    • 支持断点续传(更新源返回 Accept-Ranges: bytes)、失败重试。
    • 下载期间展示简易进度/启动画面(见第 12 节)。
  5. 校验并解压:
    • 校验下载文件的 SHA-256 与 manifest.sha256 一致(不一致 = 下载损坏或被篡改)。
    • 解压到 staging\<version>.tmp\。
    • 任一步失败 → 删除临时文件/目录,记录日志,降级启动本地当前版本(若有)。
  6. 原子切换:
    1. 将 staging\<version>.tmp\ 重命名为 versions\<version>\。
    2. 将 current.txt 写为该版本号(先写临时文件再替换,保证写入原子)。
  7. 启动主程序:启动 versions\<current.txt>\CMBot.exe。
  8. 清理:成功切换后清理 staging\(zip 与临时目录);按保留策略清理过旧版本(见第 13 节)。

任一步失败都不得使系统进入「无可启动版本」状态:只要本地存在一个可用版本,就以它启动。

9. 安装位置与权限

自动更新要求整个安装根免提权可写:不仅写用户数据,还要写 versions\(新版本)与 current.txt(切换指针)。据此选择安装位置。

  • 默认安装到 %LOCALAPPDATA%\CMBot(C:\Users\<用户>\AppData\Local\CMBot)。
    • 当前用户对该目录始终可写,无需管理员权限、不弹 UAC,是自更新桌面应用(如 Chrome、VS Code、Slack)的标准做法。
    • 这是每用户安装:每个登录用户各一份程序与数据,互不影响。
  • 禁止安装到以下位置:
    • C:\Program Files / C:\Program Files (x86):写入需管理员权限。
    • C:\ 盘根(如 C:\CMBot):在盘根新建目录默认需提权;即便由管理员预建,其继承 ACL 通常只给标准用户读取权限,自动更新与数据写入会失败;域环境组策略常直接锁死盘根。
  • 不采用机器级共享安装(如 C:\ProgramData\CMBot):默认 ACL 下多用户互写、改他人文件易失败,且共享安装目录的更新通常又需管理员权限,与「标准用户自动更新」相悖。如确有全机共享需求,作为后续扩展单独设计。
  • 启动器与更新过程不应要求管理员权限。
  • 若检测到安装根不可写,提示用户改装到可写目录,不静默失败(呼应 docs/09 第 6 节)。
  • CMBOT_DATA_DIR(见第 5 节)由启动器指向安装根下的 data\;未设置时主程序回退到程序目录,保持开发与当前扁平发布的现状行为。

10. 强制更新

  • manifest.mandatory = true 或本地版本低于 min_supported 时,启动器必须更新成功后才启动主程序;更新失败则提示用户并终止,不启动旧版本。
  • 普通更新(mandatory = false)失败时降级启动旧版本,不阻断使用。

11. 启动器自身的更新

启动器逻辑稳定、极少变更,不参与版本并排自更新,以避免「更新器更新自己」的文件锁问题。

  • 启动器版本与主程序版本解耦,单独维护。
  • 确需升级启动器时,作为一次性手动分发处理(替换 Launcher.exe),并在发布说明中标注。

12. 用户体验

  • 启动器在下载/安装期间显示轻量启动画面与进度,避免「双击无反应」。
  • 更新完成后正常进入主程序;可选地在主程序内展示一次本次更新说明(manifest.notes)。
  • 更新源不可达时静默降级启动,不打断用户;仅记录日志。
  • 文案遵循界面语气:说明发生了什么、下一步做什么,不做无谓道歉(呼应 docs/07 文案规范)。

13. 失败处理与回滚

  • 回滚:将 current.txt 改回上一个 versions\<x.y.z>\ 即可,无需重新下载。
  • 版本保留策略:默认保留最近 N 个版本(建议 N=2~3),其余在成功切换后清理,兼顾回滚能力与磁盘占用。
  • 下载中断:残留的 zip / 临时目录不影响现有版本;下次启动续传、重新下载或清理。
  • 校验失败:丢弃 zip 与临时目录,使用本地当前版本。

14. 安全考量

HTTP 在线更新比内网共享面临更高风险,按以下层次防护:

  • 传输用 HTTPS:Basic Auth 凭据为 base64≈明文,纯 HTTP 下凭据与下载内容均可被监听/篡改。生产环境必须让更新源走 HTTPS(gohttpserver 配证书,或前置 nginx/Caddy 终止 TLS)。
  • 访问控制:更新源开启 Basic Auth;客户端使用只读账号(仅能下载,不能上传/删除),上传发布用单独的管理账号。
  • SHA-256 完整性校验(第 8 节):下载后比对 manifest.sha256,既防「下载损坏」也防「文件被替换」(前提是 manifest 本身可信)。
  • (可选)防篡改签名:若担心更新源被攻破后投毒,可扩展为 Ed25519 签名 manifest(公钥内嵌启动器,验签通过才采信 manifest 与其中的 sha256)。本阶段先不做,列入第 17 节。
  • 凭据 update_user / update_pass 存于 app_config.json(明文)。因其为只读账号、风险可控;如需更强保护可后续改为系统凭据库或环境变量。

15. 版本号规则

沿用 docs/09 第 9 节:

  • 版本号从 src/version.py 统一读取,标题栏、打包产物、发布目录、manifest.json 四处一致。
  • 发布目录与 versions\ 子目录均以版本号命名(如 CMBot-1.1.0 / versions\1.1.0)。

16. 实现阶段建议

分阶段落地,每阶段可独立验证:

  1. 地基:数据目录分离(第 5 节)。任何更新方案的前提。✅ 已实现。
  2. 只读通知:启动时读取 manifest.json 比对版本,有新版仅提示。🚧 已实现本地文件版——services/update_service.py(check_for_update / 版本比较,纯逻辑可测)+ 主窗口顶部通知横幅,后台线程检查(不可达不阻塞启动),更新源由 update_source 配置。待补:check_for_update 支持 http(s):// 源 + Basic Auth(当前只会 open() 本地路径,填 URL 会静默返回 None)。
  3. 自动安装:启动器完整流程(下载 → 校验 → 原子切换 → 启动 → 回滚)。🚧 scripts/update.ps1 已实现并以假版本目录验证全流程(含降级路径);当前为 UNC/robocopy 版,待改为 HTTP:用带凭据的 HTTP 下载 zip + SHA-256 校验 + 解压(版本切换/回滚逻辑不变)。尚未接入真实安装结构(build.ps1 仍产出扁平 onedir,无生成版本并排布局的安装步骤)。
  4. 强制更新与保留策略:补全 mandatory / min_supported 与版本清理(保留最近 N 个)。⛔ 未做。

启动器(scripts/update.ps1,HTTP 版目标)要点:

  • 入口参数 -InstallRoot(默认脚本所在目录)、-NoLaunch(测试用,只更新不启动)。
  • 从 <InstallRoot>\data\config\app_config.json 读 update_source / update_user / update_pass,与 stage ② 同源。
  • HTTP GET manifest.json → 比较版本 → 下载 manifest.url 到 staging\<ver>.zip(带 Basic Auth,支持续传)→ 校验 SHA-256 → 解压到 staging\<ver>.tmp → 重命名进 versions\<ver> → 原子写 current.txt(ascii 无 BOM)。
  • 启动 versions\<current>\CMBot.exe 并设 CMBOT_DATA_DIR=<InstallRoot>\data(接 stage ① 数据目录分离)。
  • 任何失败(网络不可达 / 4xx-5xx / SHA-256 不符 / 解压失败 / 坏 manifest)都降级启动本地现版本,绝不进入「无可启动版本」。
  • 旧版本目录保留,回滚 = 手改 current.txt 回旧版本号。

17. 暂不做

  • 增量二进制差分更新。
  • 运行期后台静默更新。
  • 按电脑名/用户的差异化版本下发。
  • 更新源防篡改签名(Ed25519 签名 manifest,见第 14 节,作为后续加固项)。

如需上述能力,再行扩展本文档,不直接混入本阶段设计。

18. 验收要点

  • 启动器能在有新版本时自动完成安装并启动新版本。
  • 安装后用户自定义模板与偏好(data\)完整保留。
  • 更新源不可达时仍能启动本地当前版本。
  • 强制更新失败时不启动旧版本并给出提示。
  • 可通过改写 current.txt 回滚到上一版本。
  • 安装目录在非管理员权限下可正常更新。