From d4a3df4df0b1f8f10e965a4fa32c71c89ba28746 Mon Sep 17 00:00:00 2001 From: chengma Date: Mon, 13 Jul 2026 11:52:43 +0800 Subject: [PATCH] docs(update): split automatic upgrade implementation tasks --- docs/tasks/T-615.md | 46 ++++++++++++++++++++++++++++++++++++++++ docs/tasks/T-616.md | 47 +++++++++++++++++++++++++++++++++++++++++ docs/tasks/T-617.md | 48 ++++++++++++++++++++++++++++++++++++++++++ docs/tasks/T-618.md | 48 ++++++++++++++++++++++++++++++++++++++++++ docs/tasks/T-619.md | 51 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 240 insertions(+) create mode 100644 docs/tasks/T-615.md create mode 100644 docs/tasks/T-616.md create mode 100644 docs/tasks/T-617.md create mode 100644 docs/tasks/T-618.md create mode 100644 docs/tasks/T-619.md diff --git a/docs/tasks/T-615.md b/docs/tasks/T-615.md new file mode 100644 index 0000000..59e0844 --- /dev/null +++ b/docs/tasks/T-615.md @@ -0,0 +1,46 @@ +--- +id: T-615 +title: 自动升级发布契约与可验证发布清单 +phase: 8 +deps: [T-540, T-544] +status: TODO +created: 2026-07-13 +--- + +## 问题 / 背景 + +当前 T-544 版本接口只够支持“打开浏览器下载”:客户端虽然读取 `sha256`,但不校验,服务端也可能返回空值;发布包没有机器可验证的文件清单、包大小和更新器协议。自动下载并替换程序前,必须先让服务端元数据、zip内容和本地版本源形成一致且可自动验证的发布契约。 + +本任务是自动升级的发布基础,不下载、不安装、不替换用户程序。 + +## 方案 + +1. 扩展 `docs/update-check.md` 的服务端契约。自动安装版本必须提供:`version`、`force_update`/`min_supported_version`、HTTPS `download_url`、64位十六进制 `sha256`、`size_bytes`、`package_format=cmshopee-portable-v1`、`updater_protocol` 和中文发布说明。保留顶层与 `release` 子对象兼容读取,但自动安装字段必须来自同一 release。 +2. 新增发布清单生成逻辑,构建 `package-manifest.json`,至少包含包格式、应用版本、入口文件、更新器协议、允许替换的根项目和程序文件清单。每个文件记录规范化相对路径、字节数和 SHA-256;清单自身不递归列入自身。 +3. 修改 `scripts/build_exe.ps1`:组装 release 目录后生成清单,再压缩 zip;压缩完成后计算整个 zip 的 SHA-256 和准确字节数,生成与服务端录入字段一致的 `release-metadata.json`。发布人员不得手工估算 hash 或大小。 +4. 清单只允许程序根项目,例如 `cmshopee.exe`、`_internal/`、`version.txt`、`README.txt`、`package-manifest.json` 和后续的 `cmshopee-updater.exe`;明确排除 `data/` 与 `.cmshopee-update/`。 +5. 预留 `manifest_signature`、`signature_algorithm`、`min_updater_protocol` 字段,第一阶段不因签名缺失阻断构建;数字签名与Authenticode单独迭代,不把未实现的签名宣称为已验证安全能力。 +6. 同步 `docs/packaging.md` 和 `docs/04-architecture.md`,明确当前自动升级引导版本、服务端发布步骤和发布包边界。 + +## 验收要点 + +- 同一 `APP_VERSION` 同时出现在GUI版本源、`version.txt`、manifest、release目录和zip元数据中。 +- `release-metadata.json` 中的zip大小与磁盘实际大小一致,SHA-256重新计算一致。 +- manifest覆盖发布包内所有程序文件,不包含绝对路径、反斜线漂移、重复路径或任何 `data/` 内容。 +- 构建缺少入口、`_internal/`、版本文件或发现用户数据时失败,不生成可发布元数据。 +- `docs/update-check.md` 能直接交给服务端实现,不再把空 `sha256` 视为可自动安装。 + +## 测试要求 + +- 更新 `tests/test_packaging.py`,覆盖manifest字段、文件清单、hash/大小、排除用户数据和版本一致性。 +- 运行ruff、compileall、完整unittest、构建脚本静态/可控测试和 `git diff --check`。 + +## 边界(不改什么) + +- 不实现客户端下载、解压、替换、重启或更新进度窗口。 +- 不修改CDP、AI、Excel、SQLite业务表或蝦皮流程。 +- 不把真实下载密钥、API Key、Cookie或用户数据写入清单和发布元数据。 + +## 执行记录 + +(完成后记录实现、验证命令与结果。) diff --git a/docs/tasks/T-616.md b/docs/tasks/T-616.md new file mode 100644 index 0000000..9ce2375 --- /dev/null +++ b/docs/tasks/T-616.md @@ -0,0 +1,47 @@ +--- +id: T-616 +title: 自动升级安全下载校验与同盘暂存 +phase: 8 +deps: [T-615] +status: TODO +created: 2026-07-13 +--- + +## 问题 / 背景 + +自动升级不能把远程zip直接交给更新器。客户端必须先完成可信地址限制、流式下载、大小/hash校验、防zip路径穿越、安全解压、包内版本与manifest逐文件校验;任何一步失败都不能碰当前程序文件。 + +更新临时文件不能混进用户业务数据 `data/`。本任务使用安装目录同级管理区 `<安装目录>/.cmshopee-update/`,既与程序在同一磁盘便于后续目录切换,也与账号、SQLite、图片和密钥完全隔离。 + +## 方案 + +1. 新增 `app/update_installer.py` 纯逻辑模块,定义自动安装元数据、下载结果、暂存结果和中文错误类型;不得依赖Qt控件。 +2. 只接受HTTPS下载地址。下载主机采用客户端受信任域名白名单;重定向只能落在同一白名单内,禁止降级HTTP、`file://`、内网地址和任意第三方跳转。 +3. 流式下载到 `.cmshopee-update/downloads/.zip.part`,持续校验累计字节数,完成后要求与 `size_bytes` 完全一致、SHA-256严格一致,再原子改名为 `.zip`。包大小硬上限建议512 MiB;超限、磁盘不足、中断或取消时保留当前版本,清理不完整part文件。 +4. 安全解压到 `.cmshopee-update/staging/-<随机值>/`。解压前逐项拒绝绝对路径、盘符/UNC、`..`、符号链接、Windows保留名、NTFS ADS冒号、尾随点/空格、大小写不敏感重复目标;限制文件数不超过20000、解压总量不超过2 GiB,并限制异常压缩比。 +5. 解压后严格验证 `version.txt`、`package-manifest.json`、`cmshopee.exe` 和 `_internal/`;版本、包格式、更新器协议必须与服务端元数据一致。按manifest逐文件复算大小和SHA-256,拒绝manifest未声明的程序文件和缺失文件。 +6. 本地写入 `pending.json` 只记录版本、安装根、暂存根、hash和阶段,不记录URL查询参数、密钥或用户数据路径。下一次启动可识别已经完整校验的暂存包,避免重复下载;无效/不完整pending必须清理后重新下载。 +7. 失败消息对用户保持中文概述,远程URL、内部路径、堆栈写入脱敏诊断日志,不直接展示。 + +## 验收要点 + +- 正常zip能下载、校验并暂存,当前 `cmshopee.exe`、`_internal/` 和 `data/` 完全不变。 +- hash为空、格式错误、不匹配,大小不匹配,HTTP/非白名单地址和越权重定向均拒绝安装。 +- zip slip、符号链接、大小写重复、保留名、ADS、zip bomb和manifest缺失/篡改均被自动化测试阻断。 +- 下载中断后不会把 `.part` 当完整包;已验证pending可在重启后复用,不重复下载。 +- 更新目录始终位于 `.cmshopee-update/`,不创建 `data/updates`,不读取用户业务数据。 + +## 测试要求 + +- 新增 `tests/test_update_installer.py`,使用临时目录和本地可控响应覆盖成功、取消、网络失败、所有安全拒绝路径及pending恢复。 +- 运行ruff、compileall、完整unittest和 `git diff --check`;不依赖真实线上下载地址。 + +## 边界(不改什么) + +- 不启动独立更新器,不替换程序,不自动重启。 +- 不修改当前T-544强制升级GUI;GUI接入由T-618完成。 +- 不读取、移动、覆盖或清理 `data/`。 + +## 执行记录 + +(完成后记录实现、验证命令与结果。) diff --git a/docs/tasks/T-617.md b/docs/tasks/T-617.md new file mode 100644 index 0000000..9821add --- /dev/null +++ b/docs/tasks/T-617.md @@ -0,0 +1,48 @@ +--- +id: T-617 +title: 独立更新器事务替换与失败恢复 +phase: 8 +deps: [T-615] +status: TODO +created: 2026-07-13 +--- + +## 问题 / 背景 + +Windows正在运行的 `cmshopee.exe` 和PyInstaller `_internal/` 可能被锁定,主程序不能安全覆盖自身。必须使用运行在安装目录之外的独立更新器,在旧主程序退出后执行完整目录切换;替换中任何失败都要恢复旧程序,不能留下半套版本。 + +## 方案 + +1. 新增最小化 `app/updater_entry.py` 与独立PyInstaller配置,构建无控制台、尽量仅依赖标准库的 `cmshopee-updater.exe`。它作为发布包程序文件存在,但执行前复制到 `%TEMP%/cmshopee-update-<随机值>/`,避免锁住安装目录或暂存目录。 +2. 主程序以后通过结构化 `update-plan.json` 启动更新器,传入父进程PID、安装根、已验证暂存根、目标版本、事务ID和日志路径。更新器必须重新验证所有路径:绝对路径、安装根可写、暂存根属于 `.cmshopee-update/staging`、版本和manifest匹配;不得信任任意命令行目标。 +3. 更新器有上限地等待主程序退出,并使用互斥锁/事务锁避免两个更新器或另一个cmshopee实例同时操作同一安装目录。启动更新器使用Windows隐藏进程标志,不弹黑色控制台窗口。 +4. 替换采用根项目事务,不逐文件覆盖 `_internal/`:先将旧 `cmshopee.exe`、整个 `_internal/`、版本/说明/manifest/更新器移动到 `.cmshopee-update/backup/<旧版本>-<事务ID>/`,再把暂存中的对应根项目移动到安装根。这样新版删除的旧依赖不会残留。 +5. `data/`、`.cmshopee-update/` 和安装根未知文件永不进入替换清单;更新器不得扫描后删除未知目录。包内清单若声明这些排除项,立即拒绝。 +6. 每一步写事务journal。移动、权限、文件锁或启动新版失败时,按journal逆序移走不完整新版并恢复旧根项目;恢复失败要保留备份和中文日志,不得继续反复覆盖。 +7. 替换成功后启动新 `cmshopee.exe` 并传入事务/目标版本标识。T-617只确认新进程已成功创建;启动健康确认和早期崩溃回滚由T-619完成。 +8. 修改构建脚本和发布包,将 `cmshopee-updater.exe` 纳入manifest和zip,并声明当前更新器协议版本。 + +## 验收要点 + +- 更新器运行于安装目录外,等待旧主程序退出后才替换,不出现黑色命令窗口。 +- `_internal/` 整目录切换,新版已删除的旧依赖不会残留。 +- 注入任意移动失败、只读目录、文件锁、父进程不退出和新版启动失败时,旧程序根项目能恢复。 +- `data/` 前后目录树与文件hash完全一致;安装根未知文件保持不变。 +- 非法plan、越界路径、版本/manifest不一致、并发更新器均被阻断。 +- release包包含可独立运行的更新器,且不包含用户数据。 + +## 测试要求 + +- 新增更新器纯逻辑/子进程集成测试:临时安装树成功替换、故障注入回滚、超时、并发锁、路径越界、`data/`不变。 +- 更新打包测试,断言独立更新器产物、无控制台配置和manifest协议一致。 +- 在Windows 10/11打包环境人工验证一次真实文件锁与自动重启;未执行时必须如实记录。 + +## 边界(不改什么) + +- 不实现版本检查下载窗口;T-618负责GUI编排。 +- 不做新版主窗口健康标记和失败版本熔断;T-619负责。 +- 不删除用户备份,不修改CDP、AI、Excel、SQLite业务数据或蝦皮流程。 + +## 执行记录 + +(完成后记录实现、验证命令与结果。) diff --git a/docs/tasks/T-618.md b/docs/tasks/T-618.md new file mode 100644 index 0000000..5d1f58e --- /dev/null +++ b/docs/tasks/T-618.md @@ -0,0 +1,48 @@ +--- +id: T-618 +title: 强制升级进度窗口与自动下载重启编排 +phase: 8 +deps: [T-616, T-617] +status: TODO +created: 2026-07-13 +--- + +## 问题 / 背景 + +T-544当前“下载新版”只打开浏览器并退出。完成安全下载暂存和独立更新器后,需要把它们接回启动门禁:用户点击「立即升级」后在软件内看到持续进度,下载和准备完成后旧程序退出,由更新器替换并自动启动新版。 + +## 方案 + +1. 扩展 `UpdateInfo/UpdateCheckResult`,解析并校验 `size_bytes`、`package_format`、`updater_protocol`、可选签名字段;旧接口仍可用于手动下载提示,但只有完整自动安装元数据才能启用「立即升级」。 +2. 用专用模态 `QDialog` 替代当前一次性 `QMessageBox`。状态包括:发现必须升级、下载、校验、暂存、准备重启、失败;显示中文阶段、进度百分比、已下载/总大小和发布说明,不显示URL、内部路径、堆栈或英文异常。 +3. 下载、hash、解压和manifest校验全部由 `QObject + QThread` worker执行;主线程只更新UI。线程引用保留到 `thread.finished`,退出/取消使用协作式停止并等待安全清理,禁止出现 `QThread: Destroyed while thread is still running`。 +4. 强制响应元数据完整时按钮为「立即升级」「退出程序」;运行中允许退出并协作式取消;失败后为「重试」「退出程序」。没有成功启动更新器前,用户不能进入主窗口。 +5. 区分失败边界:版本接口完全不可达/非法时保持T-544失败放行;一旦服务端已明确返回强制升级,后续元数据缺失、下载、校验、暂存或启动更新器失败都保持阻断,只允许重试或退出。 +6. 启动时先检查本地已验证 `pending.json`;有效则直接进入“准备重启”,不重复下载。无效pending清理后再请求/下载。 +7. 暂存成功后生成plan,把更新器复制到临时目录并以无窗口方式启动;确认更新器进程创建成功后,主程序退出事件循环。更新器完成替换后自动启动新版。 +8. 同步 `app/gui/__init__.py`、`docs/update-check.md`、`docs/packaging.md` 和 `docs/routes.md`。普通非强制更新仍不新增提示。 + +## 验收要点 + +- 命中强制更新后不能创建或进入 `MainWindow`;点击「立即升级」不调用浏览器。 +- 下载、校验、暂存阶段GUI持续响应且中文进度正确;失败可重试,退出不会遗留运行线程或把part当完整包。 +- 服务端明确强制但hash/大小/格式不完整时阻断自动安装并给中文原因,不降级放行旧版。 +- 有效pending重启后直接继续安装,不重复下载或重复解压。 +- 更新器启动失败时旧程序文件完全不变;启动成功后旧主程序退出,新程序由更新器拉起。 +- 版本接口断网仍按现有策略允许进入,且记录脱敏诊断日志。 + +## 测试要求 + +- 更新 `tests/test_update_check.py`、`tests/test_gui.py`,新增worker/对话框测试,覆盖状态转换、进度、重试、取消、失败放行与强制后阻断。 +- 使用mock更新器验证主程序只在更新器成功创建后退出;不得在自动化测试访问真实线上。 +- 运行ruff、compileall、完整unittest和 `git diff --check`。 + +## 边界(不改什么) + +- 不改变非强制更新产品策略。 +- 不实现早期崩溃健康回滚和失败版本熔断;T-619负责。 +- 不修改五个业务Tab、CDP、AI、Excel、SQLite业务表或蝦皮流程。 + +## 执行记录 + +(完成后记录实现、验证命令与结果。) diff --git a/docs/tasks/T-619.md b/docs/tasks/T-619.md new file mode 100644 index 0000000..d374c8b --- /dev/null +++ b/docs/tasks/T-619.md @@ -0,0 +1,51 @@ +--- +id: T-619 +title: 自动升级启动健康确认熔断与发布验收 +phase: 8 +deps: [T-618] +status: TODO +created: 2026-07-13 +--- + +## 问题 / 背景 + +完成文件替换并创建新进程不等于升级成功。新版可能因缺失依赖、打包错误或早期异常无法显示主窗口;直接删除备份会让用户失去可运行版本,直接恢复旧版又可能被同一强制版本反复触发,形成无限更新循环。 + +本任务完成自动升级的健康确认、失败版本熔断、备份清理、引导版本发布和Windows实机验收。 + +## 方案 + +1. 新版通过更新器启动时携带事务ID/目标版本,在 `.cmshopee-update/transactions//health.json` 写分阶段标记:`process_started`、`main_window_ready` 或 `environment_blocked`。标记使用原子写入,不包含用户密钥或业务数据。 +2. `process_started` 在新程序完成基础导入、Qt可用和自身版本核对后写入;`main_window_ready` 在 `prepare_data_dir()`、必要DB初始化和 `MainWindow.show()` 成功后由事件循环回调写入。数据迁移冲突、数据目录不可写、配置/Chrome等已知用户环境错误写 `environment_blocked`,避免把环境问题误判为坏发布包。 +3. 更新器等待健康标记有明确上限。新进程无法创建、基础导入崩溃、版本不符或未出现任何已知标记即退出时,恢复旧程序;出现 `environment_blocked` 时保留新版并显示原有中文环境提示,不自动回滚。 +4. 回滚时记录 `failed_version`、包hash、时间和简短原因。同一失败版本不得自动再次下载/安装;旧版启动门禁看到该记录时仍保持强制阻断,显示“该版本自动升级失败,请重试管理员修复后的新版或手动安装”,避免无限循环和偷偷绕过强制升级。 +5. `main_window_ready` 后更新器才清理pending、下载包和临时目录;备份至少保留到健康确认完成。默认仅保留最近一次失败备份或设置合理容量上限,不扫描、不清理 `data/`。 +6. 完成端到端集成矩阵:正常升级、断网、hash错误、磁盘满、zip非法、安装目录只读、文件锁、替换中断、新版缺依赖、已知环境阻断、回滚和失败版本熔断。 +7. 完成引导版本策略:当前只支持浏览器下载的旧版必须先人工覆盖一个包含自动升级代码和更新器的引导版本;此后协议兼容版本才可自动升级。服务端发布前先灰度非强制检查,再启用强制标记。 +8. 更新 `docs/update-check.md`、`docs/packaging.md`、`docs/04-architecture.md` 和用户排障说明,记录真实Windows 10/11无Python环境证据。 + +## 验收要点 + +- 正常升级后自动显示新版主窗口,窗口版本与服务端目标一致,旧版备份在健康确认后按策略清理。 +- 缺失依赖或早期崩溃时自动恢复旧程序,并留下中文更新日志;`data/`前后hash和目录结构不变。 +- 已知数据目录、配置或Chrome环境错误不触发程序版本回滚。 +- 同一失败版本不会无限自动重试,也不会绕过服务端强制升级进入旧版业务界面。 +- 五个正式Tab、账号Chrome资料、SQLite、图片、提示词、cmhub Key和日志在升级前后保持一致。 +- 发布zip、更新器、manifest和诊断日志均不包含真实账号、Cookie、API Key、运营Excel或业务图片。 +- 无Python的Windows 10与Windows 11完成“旧引导版→新版”真实升级验收。 + +## 测试要求 + +- 新增健康标记、回滚判定、失败版本熔断和备份清理单元/集成测试。 +- 在临时安装树做故障注入;打包后在Windows 10/11虚拟机人工验证,记录版本、结果和日志位置,不提交真实data。 +- 运行ruff、compileall、完整unittest、build脚本、release内容检查和 `git diff --check`。 + +## 边界(不改什么) + +- 不把自动升级失败当作允许进入被淘汰旧版的理由。 +- 不自动上传诊断日志,不读取用户业务数据内容,不删除 `data/`。 +- 不引入增量补丁、多发布渠道或静默后台升级;第一版仍是用户在强制窗口点击「立即升级」。 + +## 执行记录 + +(完成后记录实现、验证命令与结果。)