diff --git a/docs/02-prd.md b/docs/02-prd.md index 53fde7d..2b85ad8 100644 --- a/docs/02-prd.md +++ b/docs/02-prd.md @@ -331,7 +331,9 @@ CMBot/ "last_print_dir": "", "last_template": "正方形模板", "last_batch_mode": "full_combo", - "update_source": "" + "update_source": "", + "update_user": "", + "update_pass": "" } ``` @@ -340,7 +342,8 @@ CMBot/ - `last_garment_dir` / `last_print_dir`:上次打开的衣服 / 印花文件夹路径,用于让文件夹对话框定位到上次位置。 - `last_template`:上次选中的模板名称,用于启动恢复。 - `last_batch_mode`:上次选择的批量模式(取 `BatchMode` 枚举值,如 `full_combo` / `many_garments` / `many_prints` / `one_to_one`)。 -- `update_source`:局域网更新源目录(含 `manifest.json`)。为空时不做更新检查。详见 `docs/10-lan-update.md`。 +- `update_source`:在线更新源 `manifest.json` 的 HTTP(S) 地址。为空时不做更新检查。详见 `docs/10-lan-update.md`。 +- `update_user` / `update_pass`:更新源 HTTP Basic Auth 凭据,更新源开启鉴权时使用,为空表示匿名。 - 偏好的读取、分发与「改一次存一次」由主窗口集中处理,UI 控件不直接读写配置文件。 ## 10. 验收标准 diff --git a/docs/05-project-architecture.md b/docs/05-project-architecture.md index f645fbd..c7380ec 100644 --- a/docs/05-project-architecture.md +++ b/docs/05-project-architecture.md @@ -240,7 +240,7 @@ BatchResult - 读取和保存应用配置。 - 管理默认配置。 - 在配置损坏或缺失时提供安全默认值。 -- 持久化用户偏好:输出设置、上次的衣服/印花文件夹(`last_garment_dir`/`last_print_dir`)、上次选择的模板(`last_template`)、上次选择的批量模式(`last_batch_mode`)、局域网更新源(`update_source`)等。 +- 持久化用户偏好:输出设置、上次的衣服/印花文件夹(`last_garment_dir`/`last_print_dir`)、上次选择的模板(`last_template`)、上次选择的批量模式(`last_batch_mode`)、在线更新源(`update_source` 及凭据 `update_user`/`update_pass`)等。 由主窗口集中使用:启动时加载一次并把初值分发给各面板,面板选择变化时「改一次存一次」回写。各 UI 控件不直接读写配置文件,避免分散解析。 diff --git a/docs/10-lan-update.md b/docs/10-lan-update.md index d43affd..7cd2547 100644 --- a/docs/10-lan-update.md +++ b/docs/10-lan-update.md @@ -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\\`:每个版本一份完整 `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": "", + "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\.tmp\`。 - - 优先文件级增量(仅拷变动文件),减少内网带宽与耗时。 - - 拷贝期间展示简易进度/启动画面(见第 12 节)。 -5. **完整性校验**:校验 `staging\.tmp\` 是否完整——至少校验 `marker` 文件存在、文件数与 `files` 一致(后续可升级为清单哈希校验)。 - - 校验失败 → 删除该临时目录,记录日志,降级启动本地当前版本(若有)。 +4. **下载 zip 到 staging**:HTTP GET `manifest.url` 下载到 `staging\.zip`(带 Basic Auth)。 + - 支持断点续传(更新源返回 `Accept-Ranges: bytes`)、失败重试。 + - 下载期间展示简易进度/启动画面(见第 12 节)。 +5. **校验并解压**: + - 校验下载文件的 SHA-256 与 `manifest.sha256` 一致(不一致 = 下载损坏或被篡改)。 + - 解压到 `staging\.tmp\`。 + - 任一步失败 → 删除临时文件/目录,记录日志,降级启动本地当前版本(若有)。 6. **原子切换**: 1. 将 `staging\.tmp\` 重命名为 `versions\\`。 2. 将 `current.txt` 写为该版本号(先写临时文件再替换,保证写入原子)。 7. **启动主程序**:启动 `versions\\CMBot.exe`。 -8. **清理**:成功切换后清理 `staging\`;按保留策略清理过旧版本(见第 13 节)。 +8. **清理**:成功切换后清理 `staging\`(zip 与临时目录);按保留策略清理过旧版本(见第 13 节)。 任一步失败都不得使系统进入「无可启动版本」状态:只要本地存在一个可用版本,就以它启动。 @@ -207,14 +217,18 @@ - **回滚**:将 `current.txt` 改回上一个 `versions\\` 即可,无需重新下载。 - **版本保留策略**:默认保留最近 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\\` 版本并排布局的安装步骤)。 +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`(测试用,只更新不启动)。 -- 从 `\data\config\app_config.json` 读 `update_source`,与 stage ② 同源。 -- 下载到 `staging\.tmp` → 校验 `manifest.marker` 存在 → 重命名进 `versions\` → 原子写 `current.txt`(ascii 无 BOM)。 +- 从 `\data\config\app_config.json` 读 `update_source` / `update_user` / `update_pass`,与 stage ② 同源。 +- HTTP GET `manifest.json` → 比较版本 → 下载 `manifest.url` 到 `staging\.zip`(带 Basic Auth,支持续传)→ 校验 SHA-256 → 解压到 `staging\.tmp` → 重命名进 `versions\` → 原子写 `current.txt`(ascii 无 BOM)。 - 启动 `versions\\CMBot.exe` 并设 `CMBOT_DATA_DIR=\data`(接 stage ① 数据目录分离)。 -- 任何失败(源不可达 / robocopy 失败 / marker 缺失 / 坏 manifest)都降级启动本地现版本,绝不进入「无可启动版本」。 +- 任何失败(网络不可达 / 4xx-5xx / SHA-256 不符 / 解压失败 / 坏 manifest)都降级启动本地现版本,绝不进入「无可启动版本」。 - 旧版本目录保留,回滚 = 手改 `current.txt` 回旧版本号。 ## 17. 暂不做 - 增量二进制差分更新。 - 运行期后台静默更新。 -- 公网更新通道。 - 按电脑名/用户的差异化版本下发。 -- 更新源防篡改签名。 +- 更新源防篡改签名(Ed25519 签名 manifest,见第 14 节,作为后续加固项)。 如需上述能力,再行扩展本文档,不直接混入本阶段设计。 diff --git a/tasks.md b/tasks.md index 9ed4a72..a55086e 100644 --- a/tasks.md +++ b/tasks.md @@ -891,7 +891,29 @@ - [x] 降级:源不可达 / robocopy 失败 / marker 缺失 / 坏 manifest 一律启动本地现版本 - [x] 假版本目录验证 6 个用例(更新、幂等、源不可达、未配置源、下载损坏、坏 manifest)全过 - [ ] 接入真实安装结构:`build.ps1` 或新增安装步骤产出 `%LOCALAPPDATA%\CMBot\versions\\` 版本并排布局 -- [ ] 真实内网双机实测 +- [ ] 真实双机实测 + +### 17.18 更新传输统一改为 HTTP + +前置阅读: + +- `docs/10-lan-update.md`(已整体改为 HTTP:§7 源、§8 流程、§14 安全、§16 阶段) +- `docs/02-prd.md`(`update_source` / `update_user` / `update_pass`) + +背景: + +- 更新源改为 HTTP(S) 文件服务(已部署 gohttpserver + nginx,HTTP Basic Auth,示例 `http://cm.xiapi.com/`)。§17.16/§17.17 的本地文件版与 UNC/robocopy 版作为历史保留,本任务把传输统一到 HTTP。 + +任务: + +- [x] 文档:`docs/10` 由 UNC/robocopy 全面改为 HTTP(源/清单/流程/安全/阶段);`docs/02`、`docs/05` 同步配置项 +- [ ] 配置项 `update_user` / `update_pass` 写入 `DEFAULT_CONFIG` +- [ ] `update_service.check_for_update` 支持 `http(s)://` 源 + Basic Auth(当前仅 `open()` 本地文件,填 URL 静默返回 None),补单测 +- [ ] 启动器 `update.ps1` 改 HTTP:带凭据下载 zip → 校验 SHA-256 → 解压到 staging(切换/回滚逻辑不变) +- [ ] manifest 字段由 `source`/`files`/`marker` 改为 `url`/`sha256`/`size` +- [ ] 发布流程脚本:build → 打 zip → 算 SHA-256 → 写 manifest → 上传 +- [ ] 生产前将更新源切到 HTTPS、客户端改用只读账号 +- [ ] 真实环境端到端实测 ## 18. 后续暂缓任务