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>
This commit is contained in:
2026-06-18 08:54:16 +08:00
co-authored by Claude Opus 4.8
parent 275ce852c3
commit 51cefab2db
4 changed files with 83 additions and 45 deletions
+54 -41
View File
@@ -1,14 +1,17 @@
# 局域网更新设计
# 在线更新设计(HTTP)
> 本文档原为「局域网更新」设计,现统一改为基于 **HTTP(S)** 的在线更新:更新源是一个 HTTP 文件服务(如 gohttpserver + nginx),客户端通过 HTTP 下载清单与版本包。文件名沿用 `10-lan-update.md` 以避免引用断裂;「版本并排 + 指针切换 + 回滚」等与传输无关的设计保持不变,仅「更新源 / 下载 / 安全」改为 HTTP。
## 1. 文档定位
本文档定义工具在**局域网内自动更新**的设计。它是 `docs/09-packaging-release.md` 第 13 节列出的「暂不做」能力的后续扩展,按该文档第 262 行要求单独成文,不混入第一阶段打包规则。
本文档定义工具的**在线自动更新(HTTP)**设计。它是 `docs/09-packaging-release.md` 第 13 节列出的「暂不做」能力的后续扩展,按该文档第 262 行要求单独成文,不混入第一阶段打包规则。
适用前提:
- 多台 Windows 电脑在同一局域网内使用同一工具(见 `docs/01-product-vision.md`、`docs/02-prd.md`)。
- 主程序保持本地可运行,**不依赖公网服务**,更新源也在内网。
- 打包采用 `onedir` 目录模式(见 `docs/09` 第 3 节)。目录模式是本方案增量替换、版本并排的前提;不使用 `onefile`。
- 多台 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 或编译型语言实现。
@@ -16,7 +19,7 @@
### 2.1 目标
- 客户端启动时自动检查内网更新源是否有新版本。
- 客户端启动时自动检查 HTTP 更新源是否有新版本。
- 有新版本时自动下载并安装,用户无需手动拷贝。
- 安装过程不破坏正在运行的程序,不丢失用户数据。
- 安装失败或更新源不可达时,能降级到本地当前版本继续使用。
@@ -25,9 +28,8 @@
### 2.2 非目标
- 不做公网更新、不依赖外部更新服务。
- 不做静默后台下载(更新只在启动时进行,理由见第 6 节)。
- 不做增量二进制差分(patch)。本阶段以「整版并排 + 文件级增量拷贝」为准。
- 不做增量二进制差分(patch)。本阶段以「整版 zip 下载 + 版本并排」为准。
- 不做安装程序(installer)、注册表写入、开机自启动。
- 不做按电脑名/用户的差异化版本策略(可作为后续扩展,见第 13 节)。
@@ -42,7 +44,7 @@
- 永不与文件锁冲突——从不写入正在运行的目录。
- 切换原子——只改写一个指针文件,状态非新即旧,不存在「装一半」。
- 回滚廉价——把指针改回旧版本目录即可。
- 与 `onedir` 天然契合,支持文件级增量拷贝。
- 与 `onedir` 天然契合——每个版本是一份完整 `onedir` 产物(打成 zip 下载、解压并排)。
## 4. 安装目录结构
@@ -78,7 +80,7 @@
- `current.txt`:纯文本,仅存当前应启动的版本号。
- `versions\<x.y.z>\`:每个版本一份完整 `onedir` 产物。
- `data\`:所有用户可写数据集中存放,**不随版本切换变动**。
- `staging\`:下载与校验的临时目录,校验通过后再并入 `versions\`。
- `staging\`:下载的 zip 与解压临时目录,校验通过后再并入 `versions\`。
- 整个安装根(含 `versions\`、`data\`、`Launcher.exe`)都必须免提权可写:自动更新需要写入 `versions\` 与 `current.txt`,故安装根不能落在 `C:\` 盘根或 `C:\Program Files`。
## 5. 数据目录分离(实现前置改造)
@@ -116,13 +118,18 @@
## 7. 更新源与版本清单
更新源路径由 `app_config.json` 的 `update_source` 配置(见 `docs/02-prd.md`);为空时不做更新检查。更新源为内网共享目录(UNC 路径)或内网文件服务,例如:
更新源为一个 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):
```text
\\nas\cmbot\releases\
http://cm.xiapi.com/
manifest.json # 最新版本清单
CMBot-1.1.0\ # 与 release 目录结构一致的完整版本
CMBot-1.0.0\
CMBot-1.1.0.zip # 整版 onedir 产物打包
CMBot-1.0.0.zip
```
`manifest.json` 字段:
@@ -130,21 +137,22 @@
```json
{
"version": "1.1.0",
"source": "\\\\nas\\cmbot\\releases\\CMBot-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": "修复批量导出格式问题",
"files": 142,
"marker": "CMBot.exe"
"notes": "修复批量导出格式问题"
}
```
- `version`:最新版本号,遵循 `docs/09` 第 9 节版本规则,与 `src/version.py` 的 `APP_VERSION` 一致。
- `source`:该版本完整产物所在的内网路径。
- `url`:该版本 zip 包的下载地址(可为绝对 URL;若为相对路径,相对 `update_source` 解析)。
- `sha256`:zip 包的 SHA-256,下载后校验,防止下载损坏或被篡改(见第 8、14 节)。
- `size`:zip 字节数,用于显示进度与粗校验(可选)。
- `mandatory`:是否强制更新(见第 10 节)。
- `min_supported`:低于此版本必须更新后才能启动。
- `notes`:更新说明,可在提示中展示。
- `files` / `marker`:完整性校验依据(见第 8 节)。
版本比较按语义化版本(major.minor.patch)数值比较,不做字符串比较。
@@ -153,19 +161,21 @@
启动器每次启动执行:
1. **读取本地版本**:读 `current.txt`;不存在则视为无本地版本。
2. **读取远端清单**:读更新源 `manifest.json`。
- 读取失败(路径不可达、文件缺失、格式错误)→ 记录日志,**跳过更新**,直接进入第 7 步启动本地当前版本。
2. **读取远端清单**:HTTP GET `manifest.json`(带 Basic Auth)。
- 失败(网络不可达、4xx/5xx、JSON 格式错误)→ 记录日志,**跳过更新**,直接进入第 7 步启动本地当前版本。
3. **版本比较**:远端 `version` ≤ 本地版本 → 无需更新,进入第 7 步。
4. **下载到 staging**:将远端 `source` 目录完整拷贝到本地 `staging\<version>.tmp\`。
- 优先文件级增量(仅拷变动文件),减少内网带宽与耗时。
- 拷贝期间展示简易进度/启动画面(见第 12 节)。
5. **完整性校验**:校验 `staging\<version>.tmp\` 是否完整——至少校验 `marker` 文件存在、文件数与 `files` 一致(后续可升级为清单哈希校验)。
- 校验失败 → 删除该临时目录,记录日志,降级启动本地当前版本(若有)。
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\`;按保留策略清理过旧版本(见第 13 节)。
8. **清理**:成功切换后清理 `staging\`(zip 与临时目录);按保留策略清理过旧版本(见第 13 节)。
任一步失败都不得使系统进入「无可启动版本」状态:只要本地存在一个可用版本,就以它启动。
@@ -207,14 +217,18 @@
- **回滚**:将 `current.txt` 改回上一个 `versions\<x.y.z>\` 即可,无需重新下载。
- **版本保留策略**:默认保留最近 N 个版本(建议 N=2~3),其余在成功切换后清理,兼顾回滚能力与磁盘占用。
- **下载中断**:临时目录残留不影响现有版本;下次启动重新下载或清理。
- **校验失败**:丢弃临时目录,使用本地当前版本。
- **下载中断**:残留的 zip / 临时目录不影响现有版本;下次启动续传、重新下载或清理。
- **校验失败**:丢弃 zip 与临时目录,使用本地当前版本。
## 14. 安全考量
- 更新源处于受信任内网;本方案不引入额外签名机制,但**应对更新源做访问控制**(共享目录权限、只读发布账号)。
- 完整性校验(第 8 节)用于防止「装一半」的损坏,不用于防篡改;若后续需要防篡改,可扩展为对清单签名 + 文件哈希校验。
- 不从公网拉取任何内容。
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. 版本号规则
@@ -228,26 +242,25 @@
分阶段落地,每阶段可独立验证:
1. **地基**:数据目录分离(第 5 节)。任何更新方案的前提。✅ 已实现。
2. **只读通知**:启动时读取 `manifest.json` 比对版本,有新版仅提示并打开更新源目录(不自动安装)。✅ 已实现——`services/update_service.py`(`check_for_update` / 版本比较,纯逻辑可测)+ 主窗口顶部通知横幅,检查在后台线程进行(更新源不可达不阻塞启动),更新源路径由 `update_source` 配置。
3. **自动安装**:实现启动器完整流程(下载 → 校验 → 原子切换 → 启动 → 回滚)。🚧 启动器脚本 `scripts/update.ps1` 已实现并以假版本目录验证全流程(含源不可达、下载损坏、坏 manifest 等降级路径);尚未接入真实安装结构(`build.ps1` 仍产出扁平 onedir,无生成 `%LOCALAPPDATA%\CMBot\versions\<ver>\` 版本并排布局的安装步骤)。
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`)要点:
启动器(`scripts/update.ps1`,HTTP 版目标)要点:
- 入口参数 `-InstallRoot`(默认脚本所在目录)、`-NoLaunch`(测试用,只更新不启动)。
- 从 `<InstallRoot>\data\config\app_config.json` 读 `update_source`,与 stage ② 同源。
- 下载到 `staging\<ver>.tmp` → 校验 `manifest.marker` 存在 → 重命名进 `versions\<ver>` → 原子写 `current.txt`(ascii 无 BOM)。
- 从 `<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 ① 数据目录分离)。
- 任何失败(源不可达 / robocopy 失败 / marker 缺失 / 坏 manifest)都降级启动本地现版本,绝不进入「无可启动版本」。
- 任何失败(网络不可达 / 4xx-5xx / SHA-256 不符 / 解压失败 / 坏 manifest)都降级启动本地现版本,绝不进入「无可启动版本」。
- 旧版本目录保留,回滚 = 手改 `current.txt` 回旧版本号。
## 17. 暂不做
- 增量二进制差分更新。
- 运行期后台静默更新。
- 公网更新通道。
- 按电脑名/用户的差异化版本下发。
- 更新源防篡改签名。
- 更新源防篡改签名(Ed25519 签名 manifest,见第 14 节,作为后续加固项)。
如需上述能力,再行扩展本文档,不直接混入本阶段设计。