Files
zyt/app/research/client_update_diagnosis.md
T
2026-08-28 18:24:37 +08:00

14 KiB
Raw Blame History

Windows x64 客户端更新 can_install=False 诊断

结论

“已识别最新版本,但按钮显示「暂不可安装」并提示后台尚未配置安装包”并不等价于只有一种后台配置错误。当前链路把多种拒绝原因压缩为同一个 UpdateOffer.can_install=False,而对话框的兜底文案统一归因为“后台未配置”。

对标准 Windows x64 客户端,最值得按以下顺序检查:

  1. 服务端没有在 packages.windows_x64 取到同时非空的 urlsha256 最新版本是全局字段,安装包是按平台另行选择,因此完全可能 has_update=Truecan_install=False
  2. Windows Inno Setup 包使用了 HTTP(非 localhost)地址,或本机关闭了 HTTPS 证书校验。 前者会被解析器拒绝;后者会在 UI session 中把一个原本可安装的 offer 二次降级为不可安装。
  3. 服务端返回了非 64 位十六进制 SHA-256。 当前 PHP evaluate 只检查 SHA 是否非空,Python 客户端则做严格格式校验,两端判定可能不一致。
  4. package 结构或 package.type 不符合客户端契约。 客户端只接受对象形式的 package,类型只接受 archive / inno_setup;不过当前第一方 PHP 后端会把未知类型归一为 archive,当前管理端也只提供这两个选项,所以这通常只发生在旧服务、手工响应或绕过当前保存链路的配置中。

如果更新对话框确实已经出现,则单纯的版本、enabled、响应平台/架构不匹配通常可以排除:AppUpdateSession._on_offer()offer.has_update=False 时直接返回,不会展示更新对话框(app/src/doctor_workstation/ui/dialogs/app_update.py:360-368)。

端到端链路与证据

1. 客户端发送的身份

  • Windows 被映射为 windowsapp/src/doctor_workstation/services/app_update.py:78-83)。
  • AMD64x86_64x64 都被映射为 x64app/src/doctor_workstation/services/app_update.py:86-92)。
  • 检查请求固定发往 setting.desktop_workstation/check,携带 current_versionplatformarchapp/src/doctor_workstation/services/app_update.py:218-243)。
  • ApiClient 会解开 {code, data} 信封,code == 1 时把 data 直接交给更新解析器(app/src/doctor_workstation/services/api_client.py:515-542)。

因此标准 64 位 Windows 的请求应为:

GET /adminapi/setting.desktop_workstation/check
    ?current_version=<当前版本>&platform=windows&arch=x64

2. 服务端先决定是否有对应平台安装包

  • 服务端只声明三个包槽位:windows_x64macos_arm64macos_x64server/app/adminapi/logic/setting/DesktopWorkstationLogic.php:28-32)。
  • windows/win32/win64 会归一为 windowsx64/amd64/x86_64 会归一为 x64,然后拼成 windows_x64(同文件 :125-153)。
  • evaluate 从 config.packages[windows_x64] 取包;服务端 canInstall 只要求 url !== '' && sha256 !== ''(同文件 :75-86)。
  • hasUpdate 独立由启用状态和版本比较决定(:86-88),响应中只有 canInstall 为真才返回 package,最终 can_install = hasUpdate && canInstall:90-103)。

这直接解释了核心现象:latest_version 配置正确会让客户端看到新版本,但 packages.windows_x64.urlpackages.windows_x64.sha256 任一为空,响应仍会是 has_update: truepackage: nullcan_install: false

管理端保存的真实字段是嵌套结构 packages.windows_x64.{url,sha256,size,filename,type}admin/src/api/setting/desktop_workstation.ts:5-24admin/src/views/setting/desktop_workstation/index.vue:330-344),而不是把 Windows 包放在 macOS 槽位或任意自定义键下。管理页默认 Windows 类型为 inno_setupVue 文件 :198-218),上传 .exe 也会设置为 inno_setup 并在浏览器计算 SHA-256:292-315)。

当前服务端校验允许整行安装包为空:空值/空行会继续通过(server/app/adminapi/validate/setting/DesktopWorkstationValidate.php:83-110),所以“自动检测已启用、最新版本有效、Windows 包未完整配置”是被允许保存的状态。外部 URL 缺 SHA 会被拒绝,但站内相对 URL 对应文件不存在且 SHA 为空的情形仍可能保存;服务端只会在本地文件确实存在时自动补 SHA、大小和文件名(DesktopWorkstationLogic.php:309-331)。

3. Python 客户端会再做一轮更严格的判定

parse_update_offer() 的规则位于 app/src/doctor_workstation/services/app_update.py:140-215

  • package 必须是字典;url 必须非空(:152-167)。
  • type 缺省为 archive,只接受 archive / inno_setupinno_setup 只允许 Windows:158-167)。
  • 响应平台、架构必须与请求时的期望值完全一致;同时必须满足服务端 has_updateenabled、合法且更高的版本(:175-184)。
  • SHA-256 必须恰好 64 个十六进制字符(:185-189)。
  • inno_setup URL 必须是 HTTPS,唯一例外是 HTTP localhost/loopback:190-194,具体 URL 规则在 :371-376)。
  • 最终 can_install 是服务端 can_install、有效 package、有效 SHA、安全安装器传输、has_update 五者的合取(:195-201)。判失败后返回对象会清除 package,并把 force 一并降为 false:202-215)。

因此若原始 API 返回 can_install: true,客户端仍可能因以下字段得到 false:

字段/状态 拒绝条件 Windows x64 症状是否吻合
package null、数组、字符串等非对象
package.url 空字符串
package.sha256 空、长度不是 64、包含非十六进制字符
package.type archive / inno_setup 是,但当前第一方后端通常会归一为 archive
package.type=inno_setup + URL 非 localhost 的 http:// 或相对 URL
package.filename 空或扩展名不匹配 不会在 offer 阶段令 can_install=False;可能在下载/应用阶段失败
package.size 空、0、不可转整数 不会在 offer 阶段令 can_install=False;解析为 0
缺少 package.type 默认 archive 不会单独导致 falseEXE 被误当 archive 会在稍后解压失败

一个重要的不一致是:PHP evaluate 目前只检查 SHA 非空(DesktopWorkstationLogic.php:85),Python 检查完整格式(app_update.py:185-189)。管理端正常保存会校验 64 位十六进制(DesktopWorkstationValidate.php:147-151),但旧数据、直接写配置或绕过校验的导入仍可能造成“后端说可安装、客户端说不可安装”。

4. UI session 还会因本机 TLS 设置二次降级

即使 fetch_update_offer() 返回的 Inno Setup offer 已经 can_install=TrueAppUpdateSession._on_offer() 仍会以本机 config.verify_ssl 调用安装器下载策略;失败时把 force=Falsepackage=Nonecan_install=Falseapp/src/doctor_workstation/ui/dialogs/app_update.py:371-392)。

本机配置默认 verify_ssl=Trueapp/src/doctor_workstation/config.py:88-97:124-130),但登录页勾选“信任自签名证书(仅内网调试)”会把它反转为 false(app/src/doctor_workstation/ui/login.py:831-844:937-944:1041-1053)。validate_installer_download_policy() 明确拒绝 verify_ssl=False,也拒绝非安全的 Inno Setup URLapp/src/doctor_workstation/services/app_update.py:379-385)。

这是最容易被误判为“后台没包”的非后台原因。诊断时应比较两个时点:

  1. fetch_update_offer() 刚返回时是否 can_install=True
  2. _on_offer() 传给 _present() 时是否已经变成 false。

若只有第 2 个时点为 false,按当前代码唯一的正常降级入口就是 Inno Setup 下载策略,优先检查 verify_ssl

审阅时工作树中已存在一项并非本文创建的未提交改善:UpdateOffer 增加 install_unavailable_reason,TLS 策略降级时生成具体原因,对话框优先展示该原因(app_update.py service :53-67UI :200-206:379-391)。兜底文案仍用于服务端/解析阶段没有原因的 can_install=False,所以根因判别和补测仍有必要。

5. 为什么平台或版本通常不是这个弹窗的根因

  • 客户端要求响应 platform/arch 与请求期望值精确相等,错配会让 has_update=Falseapp_update.py:175-184)。
  • session 对 has_update=False 直接显示“当前已是最新版本”或静默返回,不创建更新对话框(UI :360-368)。
  • 当前第一方后端会把 Windows/x64 常见别名归一为响应中的 windows/x64DesktopWorkstationLogic.php:125-153)。

所以对于已经出现该对话框的标准 Windows x64 客户端,优先查 packages.windows_x64,而不是先怀疑 AMD64x64 名称差异。例外是非标准/旧后端没有按当前契约归一,或实际机器是 Windows ARM64;服务端没有 windows_arm64 包槽位,后者会天然没有对应包。

同理,enabled=false、最新版本无效、当前版本不低于最新版本都会使 has_update=False,与“更新弹窗出现但不可安装”不吻合。源码运行也不是该兜底文案的成因:源码模式只会取消强制属性,点击安装后才显示“当前为源码运行”(UI :393-394:401-410)。

最短现场排查路径

  1. 用发生问题的当前版本请求实际 API,并保留解包后的 data

    /adminapi/setting.desktop_workstation/check?current_version=<version>&platform=windows&arch=x64
    
  2. 若响应已经是 can_install:falsepackage:null,读取管理端配置并核对 packages.windows_x64.url.sha256 是否同时非空;确认包没有误填到 macos_x64,也没有只保存最新版本而未保存包。

  3. 若响应是 can_install:true,核对 package 是否为对象、SHA 是否 64 位十六进制、type 是否精确为 archiveinno_setup。若为 Inno SetupURL 应为 HTTPS。

  4. 若解析后 offer 为 true、弹窗前变成 false,检查客户端 preferences.json 中的 verify_ssl,以及登录页“信任自签名证书”是否被勾选。

  5. 对 Windows 安装程序,期望响应至少应类似:

    {
      "has_update": true,
      "enabled": true,
      "platform": "windows",
      "arch": "x64",
      "can_install": true,
      "package": {
        "url": "https://cdn.example.com/DoctorWorkstation-Setup-Windows-x64-0.2.0.exe",
        "sha256": "<64 lowercase hex chars>",
        "size": 123456789,
        "filename": "DoctorWorkstation-Setup-Windows-x64-0.2.0.exe",
        "type": "inno_setup"
      }
    }
    

现有测试覆盖与缺口

已有客户端测试覆盖:

  • 缺 SHA 会拒绝安装(app/tests/test_app_update.py:43-63)。
  • 合法 HTTPS Inno Setup 会接受(:66-89)。
  • HTTP Inno Setup 会拒绝(:92-115)。
  • 未知类型会拒绝(:118-139)。
  • 旧版本/错误平台响应不会成为 update(:142-163)。
  • 检查接口会发送 platform=windowsarch=x64:166-208)。
  • UI 的可选/强制升级基本行为,以及“给定 policy reason 时展示该 reason”(app/tests/test_app_update_ui.py:49-96)。

已有 PHP 契约测试覆盖 win32 + amd64 -> windows_x64,以及完整 Windows Inno 包可安装(server/tests/DesktopWorkstationUpdateContractTest.php:17-62);缺包测试只覆盖 macOS 槽位(:74-77)。

建议新增以下测试:

  1. Windows x64 服务端缺字段矩阵(最高优先级):分别让 packages.windows_x64.url 为空、sha256 为空、整个键缺失;断言 has_update=truepackage=nullcan_install=false。这会直接固化本次症状。
  2. 服务端/客户端 SHA 契约一致性:给 evaluate 一个“非空但不是 64 位十六进制”的 SHA。期望服务端也返回不可安装,或至少用共享 fixture 明确当前由客户端拒绝;避免两端一个 true、一个 false。
  3. AppUpdateSession TLS 二次降级:构造合法 HTTPS inno_setup offer,分别设置 verify_ssl=True/False,截获 _present();true 时保持可安装,false 时断言 can_install=False 且原因明确指向证书策略而非后台缺包。
  4. UI 兜底分支:构造 can_install=False 且无 reason 的 offer,断言按钮禁用并展示后台/平台包缺失文案;与已有“注入 policy reason”的测试形成两条独立路径。
  5. 解析字段矩阵:补充 package=null、非对象、空 URL、63 位 SHA、含非 hex SHA、缺少 type 默认 archive、Windows archive 使用 HTTP 仍可解析等边界测试。现有测试覆盖了部分,但没有把每个判定条件与原因一一锁定。
  6. 跨层契约 fixture:把 PHP check 的 Windows x64 JSON 响应作为 Python parse_update_offer() 输入,验证 canonical windows/x64、包类型、SHA 和 can_install 不发生语义漂移。
  7. 管理端 payload 测试:确认保存时始终发送 packages.windows_x64 嵌套对象,上传 .exetype=inno_setup 且 URL、SHA、文件名、大小均落在同一槽位。

长期看,最稳妥的可观测性是让 can_install=False 同时带结构化原因(例如 missing_packageinvalid_digestunsupported_typeinsecure_installer_urltls_verification_disabled),并在客户端保留原因而不是立即清除所有包信息。这样 UI 不必用一个“后台未配置”文案覆盖所有安全门禁。

验证记录

  • 根目录 .trellis/ 不存在;本次按根 AGENTS.md 执行,只读检查生产代码,仅新增本文档。
  • app/.venv/Scripts/python.exe -m pytest tests/test_app_update.py tests/test_app_update_ui.py -q:通过。
  • 收集结果:test_app_update.py 21 项、test_app_update_ui.py 3 项,共 24 项。
  • php tests/DesktopWorkstationUpdateContractTest.php(工作目录 server/):Desktop workstation update contract: OK