fix(update): accept cmhub release metadata contract
This commit is contained in:
@@ -75,9 +75,9 @@ def metadata_from_update_info(info):
|
||||
download_url=str(getattr(info, "download_url", "") or "").strip(),
|
||||
sha256=str(getattr(info, "sha256", "") or "").strip().lower(),
|
||||
size_bytes=int(getattr(info, "size_bytes", 0) or 0),
|
||||
package_format=str(getattr(info, "package_format", "") or "").strip(),
|
||||
updater_protocol=int(getattr(info, "updater_protocol", 0) or 0),
|
||||
min_updater_protocol=int(getattr(info, "min_updater_protocol", 0) or 1),
|
||||
package_format=PACKAGE_FORMAT,
|
||||
updater_protocol=UPDATER_PROTOCOL,
|
||||
min_updater_protocol=UPDATER_PROTOCOL,
|
||||
)
|
||||
validate_metadata(metadata)
|
||||
return metadata
|
||||
|
||||
@@ -535,6 +535,7 @@ cmshopee/
|
||||
- T-617 的独立 `cmshopee-updater.exe` 必须先复制到系统临时目录运行,并等待主程序退出。替换粒度是manifest允许的程序根项目,旧根先整体移动到同盘backup,新根再整体移入;事务锁防止并发更新,journal记录每次移动,任一步失败逆序恢复。`data/`、更新管理目录与未知安装根项目永不进入替换清单。
|
||||
- T-618 在创建 `MainWindow` 前显示强制升级 `QDialog`;下载与校验只能在 `QObject + QThread` worker中执行,线程结束前保留引用,取消时等待part清理。独立更新器进程成功创建后才退出旧主程序;强制版本后续失败保持阻断,只有版本接口本身不可达/非法继续失败放行。
|
||||
- T-619 要求新版按事务写 `process_started`、`main_window_ready`、`environment_blocked` 健康标记。更新器只在主窗口就绪后认定升级成功并清理成功备份;无标记/早期退出/超时则事务回滚并按目标版本+包hash熔断。环境阻断保留新版,不把本地数据目录、配置或Chrome问题误判成坏发布包。
|
||||
- T-624 以 cmhub 实际响应作为版本接口边界:服务端只需提供版本、下载地址、zip SHA-256、准确大小、强制策略、发布说明和发布时间;包格式与更新器协议由客户端内置,并在下载解压后通过包内manifest验证,不要求版本接口重复返回。
|
||||
|
||||
- CDP 交互事实变化同步第七节。
|
||||
- 正式代码只放 `app/` 包;根目录只保留 `main.py`、配置/数据目录、文档和原型目录,不新增正式业务模块。
|
||||
|
||||
@@ -186,6 +186,8 @@ T-619 增加启动健康确认与失败熔断。新版带事务参数启动,
|
||||
|
||||
T-623 为外部对象存储/CDN下载链接增加临时 HTTP 兼容。版本检查接口仍固定使用受信任 HTTPS;安装包 `download_url` 不限制域名,可以使用任意公网 HTTP/HTTPS 域名或公网 IP,并允许跨域、跨协议重定向。初始地址、每次跳转和最终响应都会拒绝本机、内网、账号信息和非 HTTP(S) 协议。HTTP 不会降低安装校验标准:声明大小、整包 SHA-256、安全解压、包内manifest、事务替换和健康回滚全部照常执行;强制升级窗口会显示临时 HTTP 通道提示。服务端外链支持 HTTPS 后应删除 HTTP 分支。
|
||||
|
||||
T-624 将 cmhub 版本接口收敛为实际字段:`version`、`download_url`、`sha256`、`release_notes`、`force_update`、`size_bytes`、`published_at`。服务端不需要返回 `package_format` 或更新器协议;客户端固定使用当前 `cmshopee-portable-v1` 和更新器协议构造安装元数据,并在解压后以包内manifest复核真实格式和协议。因此接口字段减少不等于跳过包格式或协议安全检查。
|
||||
|
||||
## 四、绝不打包的本地数据
|
||||
|
||||
发布包里不能包含以下本地数据、密钥、业务数据或登录态:
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
id: T-624
|
||||
title: 自动升级适配 cmhub 最小发布接口字段
|
||||
phase: 8
|
||||
deps: [T-623]
|
||||
status: DONE
|
||||
created: 2026-07-13
|
||||
---
|
||||
|
||||
## 问题 / 背景
|
||||
|
||||
cmhub `GET /api/v1/client/releases/latest?platform=windows` 的正式响应只提供 `platform` 和 `release` 内的 `version`、`download_url`、`sha256`、`release_notes`、`force_update`、`size_bytes`、`published_at`。客户端当前还把 `package_format`、`updater_protocol`、`min_updater_protocol` 当作服务端自动安装必填字段,导致线上已经提供 hash 和大小后仍被阻断。
|
||||
|
||||
## 方案
|
||||
|
||||
1. 版本接口只读取并依赖 cmhub 正式响应中已有的发布字段,不要求服务端增加包格式或更新器协议字段。
|
||||
2. `app/update_installer.py` 从接口结果构造安装元数据时,`package_format` 固定使用客户端 `PACKAGE_FORMAT`,`updater_protocol` 固定使用客户端 `UPDATER_PROTOCOL`,最低协议使用当前兼容默认值。
|
||||
3. 不降低安装包验证:下载后仍要求包内 `package-manifest.json` 的格式、协议、版本、文件大小和 SHA-256 与客户端支持范围一致;不兼容的包仍在替换程序前拒绝。
|
||||
4. 同步版本接口、打包和架构文档,明确服务端字段与包内 manifest 字段的职责边界。
|
||||
|
||||
## 验收要点
|
||||
|
||||
- 与线上示例完全同形、缺少 `package_format` 和协议字段的强制更新响应可以进入自动下载流程。
|
||||
- 缺少或错误的 `version`、`download_url`、`sha256`、`size_bytes` 仍被阻断;`force_update` 继续决定是否强制。
|
||||
- 包内 manifest 的包格式或更新器协议不兼容时仍拒绝安装。
|
||||
- HTTP 外链继续沿用 T-623 的公网地址与完整性校验。
|
||||
|
||||
## 测试要求
|
||||
|
||||
- 增加 cmhub 正式 JSON 结构的解析、强制门禁和内部安装元数据默认值测试。
|
||||
- 运行 `python -m ruff check app tests main.py`、`python -m compileall app main.py`、`python -m unittest discover -s tests` 和 `git diff --check`。
|
||||
|
||||
## 边界(不改什么)
|
||||
|
||||
- 不修改 cmhub 服务端,不要求新增接口字段。
|
||||
- 不移除大小、整包 SHA-256、安全解压、manifest、事务替换、健康确认或回滚。
|
||||
- 不修改业务 Tab、CDP、AI、Excel、SQLite 业务数据或蝦皮更新流程。
|
||||
|
||||
## 执行记录
|
||||
|
||||
- 2026-07-13:`metadata_from_update_info()` 不再要求版本接口提供包格式和更新器协议,统一使用客户端内置 `PACKAGE_FORMAT` / `UPDATER_PROTOCOL`;包内manifest格式、协议、版本和逐文件hash校验保持不变。
|
||||
- 2026-07-13:新增与 cmhub 线上响应完全同形的解析、强制门禁、内部安装元数据和 GUI HTTP 提示回归测试,并同步版本接口、打包和架构文档。
|
||||
- 2026-07-13:使用线上接口实测通过:旧版本 `0.1.5` 能识别 `0.1.6` 强制更新,读取 `size_bytes=53318076`,并补齐内部包格式 `cmshopee-portable-v1`、协议 `1`。
|
||||
- 2026-07-13:为隔离工作区既有提示词/版本号改动,在独立干净 worktree 验证通过:Ruff、Python 3.10 compileall、完整 unittest 438 项及 `git diff --check` 全绿;未执行完整安装包替换和重启。
|
||||
@@ -25,10 +25,10 @@
|
||||
| `download_url` | string(URL) | 安装包外链;优先 HTTPS,T-623 临时兼容任意公网 HTTP/HTTPS 地址 |
|
||||
| `sha256` | string | zip 的 SHA-256;自动安装时必须是 64 位十六进制且不可为空 |
|
||||
| `size_bytes` | integer | zip 的准确字节数;自动安装时必须大于 0 |
|
||||
| `package_format` | string | 自动安装固定为 `cmshopee-portable-v1` |
|
||||
| `updater_protocol` | integer | 发布包要求的更新器协议版本,当前为 `1` |
|
||||
| `min_updater_protocol` | integer | 可安装此包的最低更新器协议,当前为 `1` |
|
||||
| `release_notes` / `message` | string | 升级说明,显示在强制升级弹窗里。客户端取 `message` 或 `release_notes` |
|
||||
| `published_at` | string | 服务端发布时间;客户端当前不参与强制判定 |
|
||||
|
||||
`package_format`、`updater_protocol`、`min_updater_protocol` 不是 cmhub 版本接口字段。客户端使用内置 `cmshopee-portable-v1` 和当前更新器协议构造安装元数据,下载后再以包内 `package-manifest.json` 验证真实格式和协议;不兼容的包仍会在替换程序前拒绝。
|
||||
|
||||
版本号比较用**语义化数字段比较**(`compare_versions`,逐段比数字),不是字符串字典序:`0.1.10 > 0.1.9`。
|
||||
|
||||
@@ -55,7 +55,7 @@ T-617 已提供独立无控制台更新器和事务回滚能力:更新器从
|
||||
|
||||
## 五、发版约定(服务端据此控制)
|
||||
|
||||
自动安装使用的全部字段必须放在同一个 `release` 对象内,不得把版本取自一个对象、hash 取自另一个对象。构建脚本生成的 `release/release-metadata.json` 是服务端录入模板;发布人员补公网 HTTP/HTTPS `download_url`、强制策略和中文发布说明,不得手工改写 hash、大小、包格式或协议版本。正式发布仍应优先使用 HTTPS;HTTP 只用于外链暂时无法提供 HTTPS 的兼容期。
|
||||
自动安装使用的全部字段必须放在同一个 `release` 对象内,不得把版本取自一个对象、hash 取自另一个对象。构建脚本生成的 `release/release-metadata.json` 提供版本、zip SHA-256 和准确大小;发布人员录入公网 HTTP/HTTPS `download_url`、强制策略和中文发布说明,不得手工改写 hash 或大小。正式发布仍应优先使用 HTTPS;HTTP 只用于外链暂时无法提供 HTTPS 的兼容期。
|
||||
|
||||
| 想要的效果 | 服务端返回 |
|
||||
| --- | --- |
|
||||
@@ -75,9 +75,6 @@ T-617 已提供独立无控制台更新器和事务回滚能力:更新器从
|
||||
"download_url": "https://cm.833729.com/media/downloads/%E8%9D%A6%E7%9A%AE%E5%9C%88%E5%84%AA%E5%8C%96%E5%8A%A9%E6%89%8B0.1.1.zip",
|
||||
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
"size_bytes": 123456789,
|
||||
"package_format": "cmshopee-portable-v1",
|
||||
"updater_protocol": 1,
|
||||
"min_updater_protocol": 1,
|
||||
"release_notes": "优化了ai模块的生图的功能",
|
||||
"published_at": "2026-07-08T14:30:26.935549+08:00",
|
||||
"force_update": true
|
||||
@@ -121,11 +118,11 @@ T-617 已提供独立无控制台更新器和事务回滚能力:更新器从
|
||||
- `download_url` 里的中文可用 URL 编码(如 `%E8%9D%A6...`),系统浏览器能正确打开。
|
||||
- 版本检查接口 `APP_UPDATE_CHECK_URL` 必须保持受信任 HTTPS;只有它返回的 `download_url` 可以在 T-623 兼容期使用公网 HTTP 外链。客户端不提供手工输入下载地址或关闭校验的入口。
|
||||
- 若将来要"非强制也温和提示(可跳过、不阻断)",需在客户端加一个非强制分支,另立任务。
|
||||
- 空 `sha256` 只兼容 T-544 的人工下载引导,绝不能进入自动安装。自动安装无论使用 HTTP 还是 HTTPS,都必须同时校验 `size_bytes`、整包 SHA-256、包格式、更新器协议和包内manifest。
|
||||
- 空 `sha256` 只兼容 T-544 的人工下载引导,绝不能进入自动安装。自动安装无论使用 HTTP 还是 HTTPS,都必须同时校验接口 `size_bytes`、整包 SHA-256,以及客户端内置格式/协议对应的包内manifest。
|
||||
- `manifest_signature` 与 `signature_algorithm` 是预留字段;当前未启用数字签名,不能将 SHA-256 描述为发布者身份认证。
|
||||
|
||||
## 八、引导版本与灰度发布
|
||||
|
||||
仍只有T-544“浏览器下载”能力的旧客户端无法凭空获得独立更新器,必须先人工覆盖一个同时包含T-615至T-619代码和 `cmshopee-updater.exe` 的引导版本。后续自动发布先以非强制方式灰度确认接口字段、下载和manifest,再开启 `force_update` 或提高 `min_supported_version`。发布包的 `updater_protocol` / `min_updater_protocol` 必须与引导版本兼容。
|
||||
仍只有T-544“浏览器下载”能力的旧客户端无法凭空获得独立更新器,必须先人工覆盖一个同时包含T-615至T-619代码和 `cmshopee-updater.exe` 的引导版本。后续自动发布先以非强制方式灰度确认接口字段、下载和manifest,再开启 `force_update` 或提高 `min_supported_version`。包内manifest的 `updater_protocol` / `min_updater_protocol` 必须与引导版本兼容,cmhub 版本接口无需重复返回。
|
||||
|
||||
更新器启动新版后等待健康标记:`process_started` 表示基础导入、Qt和版本核对完成;`main_window_ready` 表示数据目录、必要初始化和主窗口显示完成,此时才清理成功备份;`environment_blocked` 表示数据目录、配置或Chrome等本地环境需用户处理,保留新版且不误回滚。无已知标记、版本不符、早期退出或健康等待超时会恢复旧版并写失败版本熔断记录。
|
||||
|
||||
@@ -1747,9 +1747,6 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
"latest_version": "1.2.0",
|
||||
"sha256": "a" * 64,
|
||||
"size_bytes": 100,
|
||||
"package_format": "cmshopee-portable-v1",
|
||||
"updater_protocol": 1,
|
||||
"min_updater_protocol": 1,
|
||||
}
|
||||
http_dialog = ForcedUpdateDialog(
|
||||
update_check.UpdateCheckResult(
|
||||
|
||||
@@ -92,6 +92,34 @@ class UpdateCheckTests(unittest.TestCase):
|
||||
self.assertEqual("abc", result.sha256)
|
||||
self.assertEqual("新版说明", result.message)
|
||||
|
||||
def test_check_for_update_accepts_cmhub_release_contract(self):
|
||||
def fetcher(_url, _timeout):
|
||||
return {
|
||||
"platform": "windows",
|
||||
"release": {
|
||||
"version": "0.1.6",
|
||||
"download_url": "http://185.216.248.75:24521/down/release.zip",
|
||||
"sha256": "a" * 64,
|
||||
"release_notes": "启动时联网获取更新信息",
|
||||
"force_update": True,
|
||||
"size_bytes": 53318076,
|
||||
"published_at": "2026-07-13T17:10:59.660653+08:00",
|
||||
},
|
||||
}
|
||||
|
||||
result = update_check.check_for_update(
|
||||
current_version="0.1.5",
|
||||
url="https://cm.example.test/api/v1/client/releases/latest?platform=windows",
|
||||
fetcher=fetcher,
|
||||
)
|
||||
|
||||
self.assertTrue(result.forced)
|
||||
self.assertFalse(result.can_enter)
|
||||
self.assertEqual("0.1.6", result.latest_version)
|
||||
self.assertEqual(53318076, result.size_bytes)
|
||||
self.assertEqual("", result.package_format)
|
||||
self.assertEqual(0, result.updater_protocol)
|
||||
|
||||
def test_network_failure_allows_entry(self):
|
||||
def fetcher(_url, _timeout):
|
||||
raise socket.timeout("timeout")
|
||||
|
||||
@@ -6,6 +6,7 @@ import unittest
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app import release_manifest, update_installer
|
||||
|
||||
@@ -71,6 +72,20 @@ class UpdateInstallerTests(unittest.TestCase):
|
||||
values.update(changes)
|
||||
return update_installer.UpdatePackageMetadata(**values)
|
||||
|
||||
def test_online_release_uses_internal_package_contract_defaults(self):
|
||||
info = SimpleNamespace(
|
||||
latest_version="0.1.6",
|
||||
download_url="http://downloads.external.test/cmshopee.zip",
|
||||
sha256="a" * 64,
|
||||
size_bytes=53318076,
|
||||
)
|
||||
|
||||
metadata = update_installer.metadata_from_update_info(info)
|
||||
|
||||
self.assertEqual(release_manifest.PACKAGE_FORMAT, metadata.package_format)
|
||||
self.assertEqual(release_manifest.UPDATER_PROTOCOL, metadata.updater_protocol)
|
||||
self.assertEqual(release_manifest.UPDATER_PROTOCOL, metadata.min_updater_protocol)
|
||||
|
||||
def test_prepare_update_verifies_and_reuses_pending(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
payload = self.make_zip(temp_dir)
|
||||
|
||||
Reference in New Issue
Block a user