Read manifest.json from the configured update_source and show a dismissable banner when a newer version is available. Notify-only — no install yet. - services/update_service.py: version compare + check_for_update (pure, tested) - config: add update_source key (empty = no check) - main_window: top banner, background-thread check; open folder via os.startfile (QDesktopServices.openUrl mishandles file:// folder URLs — ShellExecute err 2) - tests: +15 covering version compare and check_for_update branches - docs 02/05/10 + tasks 17.16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
14 KiB
局域网更新设计
1. 文档定位
本文档定义工具在局域网内自动更新的设计。它是 docs/09-packaging-release.md 第 13 节列出的「暂不做」能力的后续扩展,按该文档第 262 行要求单独成文,不混入第一阶段打包规则。
适用前提:
- 多台 Windows 电脑在同一局域网内使用同一工具(见
docs/01-product-vision.md、docs/02-prd.md)。 - 主程序保持本地可运行,不依赖公网服务,更新源也在内网。
- 打包采用
onedir目录模式(见docs/09第 3 节)。目录模式是本方案增量替换、版本并排的前提;不使用onefile。
本文档定义设计,不约束最终实现语言;启动器可用 Python、PowerShell 或编译型语言实现。
2. 目标与非目标
2.1 目标
- 客户端启动时自动检查内网更新源是否有新版本。
- 有新版本时自动下载并安装,用户无需手动拷贝。
- 安装过程不破坏正在运行的程序,不丢失用户数据。
- 安装失败或更新源不可达时,能降级到本地当前版本继续使用。
- 支持回滚到上一可用版本。
- 支持「强制更新」:关键修复发布后阻止旧版本继续启动。
2.2 非目标
- 不做公网更新、不依赖外部更新服务。
- 不做静默后台下载(更新只在启动时进行,理由见第 6 节)。
- 不做增量二进制差分(patch)。本阶段以「整版并排 + 文件级增量拷贝」为准。
- 不做安装程序(installer)、注册表写入、开机自启动。
- 不做按电脑名/用户的差异化版本策略(可作为后续扩展,见第 13 节)。
3. 总体架构
核心约束:运行中的 .exe 无法被覆盖。 因此「替换」必须发生在主程序未运行时,由一个独立的**启动器(Launcher)**在主程序启动前完成。
采用「版本并排目录 + 启动器 + 当前版本指针」方案:新版本整份安装到旧版本旁边,校验完整后再切换指针,最后启动当前版本。绝不覆盖正在使用的版本目录。
优点:
- 永不与文件锁冲突——从不写入正在运行的目录。
- 切换原子——只改写一个指针文件,状态非新即旧,不存在「装一半」。
- 回滚廉价——把指针改回旧版本目录即可。
- 与
onedir天然契合,支持文件级增量拷贝。
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\:下载与校验的临时目录,校验通过后再并入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()):- 环境变量
CMBOT_DATA_DIR非空时使用它。启动器在版本并排布局中将其指向安装根的data\文件夹。 - 否则回退到
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. 更新源与版本清单
更新源路径由 app_config.json 的 update_source 配置(见 docs/02-prd.md);为空时不做更新检查。更新源为内网共享目录(UNC 路径)或内网文件服务,例如:
\\nas\cmbot\releases\
manifest.json # 最新版本清单
CMBot-1.1.0\ # 与 release 目录结构一致的完整版本
CMBot-1.0.0\
manifest.json 字段:
{
"version": "1.1.0",
"source": "\\\\nas\\cmbot\\releases\\CMBot-1.1.0",
"mandatory": false,
"min_supported": "1.0.0",
"notes": "修复批量导出格式问题",
"files": 142,
"marker": "CMBot.exe"
}
version:最新版本号,遵循docs/09第 9 节版本规则,与src/version.py的APP_VERSION一致。source:该版本完整产物所在的内网路径。mandatory:是否强制更新(见第 10 节)。min_supported:低于此版本必须更新后才能启动。notes:更新说明,可在提示中展示。files/marker:完整性校验依据(见第 8 节)。
版本比较按语义化版本(major.minor.patch)数值比较,不做字符串比较。
8. 更新流程
启动器每次启动执行:
- 读取本地版本:读
current.txt;不存在则视为无本地版本。 - 读取远端清单:读更新源
manifest.json。- 读取失败(路径不可达、文件缺失、格式错误)→ 记录日志,跳过更新,直接进入第 7 步启动本地当前版本。
- 版本比较:远端
version≤ 本地版本 → 无需更新,进入第 7 步。 - 下载到 staging:将远端
source目录完整拷贝到本地staging\<version>.tmp\。- 优先文件级增量(仅拷变动文件),减少内网带宽与耗时。
- 拷贝期间展示简易进度/启动画面(见第 12 节)。
- 完整性校验:校验
staging\<version>.tmp\是否完整——至少校验marker文件存在、文件数与files一致(后续可升级为清单哈希校验)。- 校验失败 → 删除该临时目录,记录日志,降级启动本地当前版本(若有)。
- 原子切换:
- 将
staging\<version>.tmp\重命名为versions\<version>\。 - 将
current.txt写为该版本号(先写临时文件再替换,保证写入原子)。
- 将
- 启动主程序:启动
versions\<current.txt>\CMBot.exe。 - 清理:成功切换后清理
staging\;按保留策略清理过旧版本(见第 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),其余在成功切换后清理,兼顾回滚能力与磁盘占用。
- 下载中断:临时目录残留不影响现有版本;下次启动重新下载或清理。
- 校验失败:丢弃临时目录,使用本地当前版本。
14. 安全考量
- 更新源处于受信任内网;本方案不引入额外签名机制,但应对更新源做访问控制(共享目录权限、只读发布账号)。
- 完整性校验(第 8 节)用于防止「装一半」的损坏,不用于防篡改;若后续需要防篡改,可扩展为对清单签名 + 文件哈希校验。
- 不从公网拉取任何内容。
15. 版本号规则
沿用 docs/09 第 9 节:
- 版本号从
src/version.py统一读取,标题栏、打包产物、发布目录、manifest.json四处一致。 - 发布目录与
versions\子目录均以版本号命名(如CMBot-1.1.0/versions\1.1.0)。
16. 实现阶段建议
分阶段落地,每阶段可独立验证:
- 地基:数据目录分离(第 5 节)。任何更新方案的前提。✅ 已实现。
- 只读通知:启动时读取
manifest.json比对版本,有新版仅提示并打开更新源目录(不自动安装)。✅ 已实现——services/update_service.py(check_for_update/ 版本比较,纯逻辑可测)+ 主窗口顶部通知横幅,检查在后台线程进行(更新源不可达不阻塞启动),更新源路径由update_source配置。 - 自动安装:实现启动器完整流程(下载 → 校验 → 原子切换 → 启动 → 回滚)。⛔ 未做。
- 强制更新与保留策略:补全
mandatory/min_supported与版本清理。⛔ 未做。
17. 暂不做
- 增量二进制差分更新。
- 运行期后台静默更新。
- 公网更新通道。
- 按电脑名/用户的差异化版本下发。
- 更新源防篡改签名。
如需上述能力,再行扩展本文档,不直接混入本阶段设计。
18. 验收要点
- 启动器能在有新版本时自动完成安装并启动新版本。
- 安装后用户自定义模板与偏好(
data\)完整保留。 - 更新源不可达时仍能启动本地当前版本。
- 强制更新失败时不启动旧版本并给出提示。
- 可通过改写
current.txt回滚到上一版本。 - 安装目录在非管理员权限下可正常更新。