dsh-app-updater 1.3.2 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,192 +1,366 @@
1
- # 更新日志
2
-
3
- 本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
-
5
- ## [1.3.2] — 2026-11-01
6
-
7
- ### 修复
8
-
9
- - **macOS 上「未检测到任何已安装应用」**(用户实测报告):1.3.0/1.3.1 的 macOS 扫描只执行
10
- `brew list --cask --versions`,也就是**只有通过 Homebrew 装过的应用才会出现在列表里**。
11
- 一台正常使用的 Mac 上绝大多数应用是拖 DMG 装的,于是该命令返回空 → 界面显示「未检测到
12
- 任何已安装应用」,日志里是「Homebrew 报告 0 个已安装 cask 应用」。这不是「没有应用」,
13
- 而是**根本没有枚举应用**。现在(`lib/macos.js`):
14
- - 枚举标准应用目录里的 `.app`:`/Applications`、`/Applications/Utilities`、
15
- `/System/Applications`(及其 `Utilities`)、`~/Applications`,用 `plutil` 读每个
16
- bundle 的 `Info.plist` 取显示名与版本(读不到时退回 `defaults read`);
17
- - 抓取 Homebrew 的 cask 目录(`formulae.brew.sh/api/cask.json`,本地缓存 24 小时)把
18
- `.app` 反查到 cask token,从而知道**官网最新版本**;
19
- - 只有**确实由 Homebrew 管理**的应用才标记为可一键更新;其余应用如实列出并标注原因
20
- (`not-managed` 非 Homebrew 管理 / `store-managed` 由 App Store 管理 /
21
- `system-component` 系统组件),绝不假装能更新。
22
- - **同一个 `.app` 被多个 cask 声明时匹配错乱**:真实目录里 `iterm2` 与 `iterm2@nightly`
23
- 都声明 `iTerm.app`,旧写法「后写覆盖」会让匹配结果随目录顺序漂移(实测会把同一个应用
24
- 列出两次)。现在保留全部候选,由 `pickCask` 按「已装 > 非弃用/停用 > 主 token >
25
- token 与应用名相等」挑选。
26
- - **没有 Homebrew 也能扫描**:`capabilitiesFor` 不再把「macOS 无 Homebrew」判成不可扫描 ——
27
- 应用照样列出来,只是无法判断/驱动更新,提示改为「安装 Homebrew 后可一键升级 cask 应用」。
28
- - **设置面板按平台显示**:macOS 上不再出现「自动提权安装」「下载失败时回退 winget」
29
- 「包含应用商店应用」(Windows 概念),改为 macOS 专属的「包含 App Store 应用」与
30
- 「包含非 Homebrew 应用」(新增设置项 `includeUnmanagedApps`,默认开启)。
31
- - **空状态文案分平台**:macOS 下列表为空时提示去看运行日志里的「枚举应用目录」记录,
32
- 而不是干巴巴一句「未检测到任何已安装应用」。
33
-
34
- ### 新增
35
-
36
- - **`lib/macos.js`**:应用目录枚举、`Info.plist` 解析、cask 目录缓存与解析、cask 匹配、
37
- 列表合并(纯函数 `buildMacosItems` 便于单测)。
38
- - 日志更细:分别记录「枚举到多少个 .app」「Homebrew 报告多少个 cask」「读取 cask 目录结果」,
39
- macOS 上再出现异常时一眼能看出断在哪一步。
40
-
41
- ### 测试
42
-
43
- - `test/unit.mjs` 152 项(新增 macOS 应用枚举 20 项、同名 cask 挑选 6 项、cask 目录解析 6 项);
44
- 用**真实** `formulae.brew.sh/api/cask.json`(7824 个 cask)验证过匹配与去重。
45
- - `test/render.mjs` 44 项(新增 macOS 设置面板 6 项、macOS 空状态文案 2 项)。
46
- - `test/deny.mjs` 12 项不变。
47
-
48
- ## [1.3.1] — 2026-11-01
49
-
50
- ### 安全
51
-
52
- - **「仅凭回环地址放行」改为默认拒绝**(插件市场 dshopen.org 对 1.3.0 的复核意见):
53
- 1.3.0 接入 `connection` 准入后仍留了一条退回路径 —— 宿主没有 `connection` 服务,或请求
54
- 不带任何连接凭据且来自回环地址时,直接按「本机信任」放行。在没有准入服务的旧宿主上,
55
- 任何本机进程(以及能向回环发起请求的页面)都能借此调用 `/update`、`/settings` 等安装接口,
56
- 等于绕过认证。1.3.1 起这条路径**默认关闭**:
57
- - 宿主有 `connection`:一律以 `admit()` 的结果为准,`401` / `403` / `409` 原样返回
58
- (响应头 `X-DSH-Admission: denied`)。只有「401 + 回环 + 无凭据 + 用户显式开启兼容模式」
59
- 这一种例外,且必须在下面第 2 条的前提下才成立。
60
- - 宿主没有 `connection`:**默认 403**,不再自动相信回环地址。
61
- - **兼容模式必须由用户显式开启**(保留给确实跑在旧宿主上的用户):插件配置
62
- `allowLegacyLoopback: true`(`cordis.patch.yml` 覆盖同名条目)或环境变量
63
- `DSH_APP_UPDATER_ALLOW_LEGACY_LOOPBACK=1`,两者默认都没有。开启后:
64
- - 放行的请求带 `X-DSH-Admission: loopback-legacy-optin` 与 `X-DSH-Admission-Warning`;
65
- - `state.json` / `health.json` 新增 `admission` 字段(`mode` / `service` /
66
- `allowLegacyLoopback` / `warning`),复核时不必读代码就能确认当前路径;
67
- - 设置页顶部显示红色警示条,日志写一条 `warn`。
68
- - **准入拒绝统一返回 JSON**:`401`/`403`/`409` 带 `{ ok:false, error, admission:"denied" }`,
69
- 不再用纯文本,排障时能直接看出是被准入挡掉的。
70
-
71
- ### 测试
72
-
73
- - `test/unit.mjs`:准入用例从 6 项扩到 25 项,覆盖兼容模式开关(配置 / 环境变量 / 缺省关闭)、
74
- 无 `connection` 服务默认拒绝、显式开启后放行、非回环仍拒绝、`409`、带凭据未认证不放行等。
75
- - 新增 `test/deny.mjs`:起真实 HTTP 服务,验证无 `connection` 且未开兼容模式时**每一条路由**
76
- 都是 `403 + X-DSH-Admission: denied`;再验证显式开启后放行并标注 opt-in 响应头。
77
- - `test/render.mjs`:新增兼容模式警示条 4 项(开启显示、样式、文案、默认不显示)。
78
- - 自测命令更新为 `node test/unit.mjs && node test/render.mjs && node test/deny.mjs`。
79
-
80
- ## [1.3.0] — 2026-10-31
81
-
82
- ### 修复
83
-
84
- - **机器级安装全部失败的问题**:进程没有管理员令牌时,MSI 直接失败(msiexec 1603,
85
- MSI 日志里是 `Error 1925: You do not have sufficient privileges…`),而部分安装器是
86
- **静默退出 0 却什么都没装**(实测 Logitech G HUB)。两个现象同源,都是权限。
87
- - **安装后复核**:退出码 0 不再等于成功。安装完成后用「安装前版本 / 安装后版本 / 目标版本」
88
- 判断是否真的生效,没生效就如实报失败 —— 原来只看退出码,会把 G HUB 那种
89
- 「退出码 0、版本没变」的情况误报成「更新完成」。
90
- - **架构过滤回退**:`winget download/upgrade --architecture x64` 在清单里没有该架构时现在能
91
- 正确识别(中文报错是「找不到适用的安装程序」,旧正则只匹配了「没有适用」)并退回不限架构
92
- 重试;`winget upgrade` 兜底同时补上 `--include-unknown`(虎牙直播的已装版本被读成 Unknown,
93
- 不带它直接「目录里没有适用的更新」)。
94
- - **Edge**:winget 目录里的 `Microsoft.Edge` 是企业版 MSI,与本机消费版 Edge 的安装技术不匹配,
95
- winget 直接拒绝(`0x8A15008E`「安装技术与当前安装的版本不同,请卸载包并安装较新版本」),
96
- 现在先走 Edge 自带的更新服务 `MicrosoftEdgeUpdate.exe /ua`;若它没生效(实测该机的
97
- EdgeUpdate 引擎目录是 `1.3.311.5` 而服务主程序还是 `1.3.275.13`,服务起不来,`/ua` 直接
98
- `0x80070002`),自动退回安装 winget 目录里的官网全量包(企业版 MSI,提权执行)——
99
- 实测这一步能把 Edge 正常装到目标版本,winget 拒绝执行只是因为它的策略。
100
- - **读不到版本的应用**:虎牙直播的卸载信息里没有版本号,winget 永远显示 `Unknown`。
101
- 复核阶段不再把它误判为失败,改为「已安装、未复核」写进结果与日志。
102
- - **「安装包不生效」的软件给出可操作提示**:实测 Logitech G HUB 的更新包早已下载到
103
- `%ProgramData%\LGHUB\next.json` 指向的 depots(build 880806 / 2026.7.992920),但安装器
104
- `--silent` 与 `winget upgrade` 都会在 3 秒内退出 0 且什么都不装,只有**启动 G HUB 本体**
105
- 之后 `current.json` 才跳到新版本。插件承诺不启动用户的软件,所以这类情况现在会在失败
106
- 原因里直接给出「手动打开一次该应用即可完成升级」的提示(`updateHintFor`)。
107
- - **MSI 日志**:MSI 安装现在带 `/l*v <下载目录>\<包名>.msi.log`,失败时把日志路径写进错误信息
108
- (「Error 1925」就是这么被找到的)。
109
- - **残留清单**:关闭「保留安装包」时,现在连 winget 落盘的 `.yaml` 清单一并删除。
110
-
111
- ### 新增
112
-
113
- - **自动提权安装**(`lib/elevate.js`):需要管理员的安装类命令交给 `Start-Process -Verb RunAs`
114
- 的提权副本执行,退出码与输出写进临时文件后读回。新增开关「自动提权安装」(默认开启)。
115
- UAC 为「始终通知」时每次安装会弹一次确认;把 `ConsentPromptBehaviorAdmin` 设为 0
116
- (管理员静默提权)时完全无感。
117
- - 说明:提权副本是独立进程,取消更新只会停掉启动器,已经开始的那次安装会自己跑完。
118
-
119
- ### 安全
120
-
121
- - **接入宿主 connection 准入**(插件市场 dshopen.org 对 1.2.0 的复核意见):所有
122
- `/dsh-app-updater/*` 路由先过 `ctx.connection.admit(request)` —— DSH 用来做 Host/Origin
123
- 信任校验与浏览器会话 / API 认证的那套边界,宿主自己的 `/api` 路由用的就是它。
124
- 未通过准入的请求直接得到 `401`/`403`;带凭据的请求一律以准入结果为准。
125
- 只有在宿主没有 `connection` 服务、或者请求不带任何连接凭据且来自回环地址时,才退回旧的
126
- 「回环」规则,并在响应头标注 `X-DSH-Admission: loopback-legacy`(宿主自己的 Electron
127
- 载体与本机脚本都不带 Cookie,需要一个不误伤它们的路径)。
128
- - 因此 README 里「用 curl 直接改设置」的例子现在需要带上 DSH 的会话 Cookie,或在
129
- `connection` 不可用的宿主上从回环地址调用。
130
-
131
- ### 说明
132
-
133
- - 读类命令(`winget list` / `upgrade --include-unknown` / `--version`)不提权。
134
- - `update.results[]` 新增 `installOk`(安装器自身是否报成功)与 `manifest`(落盘的清单位置)。
135
-
136
- ## [1.2.0] — 2026-10-31
137
-
138
- ### 新增
139
-
140
- - **运行环境识别**(`lib/platform.js`):识别 Windows / macOS / 其它系统、具体版本与名称
141
- (`Windows 11(build 26300)`、`macOS 15(Sequoia)`)、CPU 架构(ARM64 / x64 / Intel / x86),
142
- 并记录 CPU 型号与线程数。Windows on ARM 上跑 x64 的 Node 时,靠 `PROCESSOR_ARCHITEW6432`
143
- 仍能还原出真实架构,而不是被骗成 x64。
144
- - **按架构挑选安装包**:Windows 侧把识别到的架构传给 winget 的 `--architecture`,让「官网全量包」
145
- 与当前系统/芯片匹配;清单里没有该架构的安装包时自动退回自动匹配,并在日志里说明。
146
- macOS 侧由 Homebrew 按本机架构取对应 cask。新增开关「按本机架构安装包」(默认开启)。
147
- - **macOS 更新引擎(Homebrew cask)**:用 `brew list --cask --versions` 拿已装版本、
148
- `brew outdated --json=v2 --cask --greedy` 拿可升级版本,`brew upgrade --cask <token>` 更新;
149
- 演练模式改用 `brew fetch --cask <token>`,只下载安装包不安装。没有 Homebrew 时明确报告
150
- 「不可用」,而不是假装能更新。
151
- - **界面**:状态行与设置面板新增「运行环境」(系统 · 架构 · 引擎);引擎不可用时红字给出原因;
152
- 应用来源新增 `homebrew` 徽章。
153
- - `/health.json` 增加 `version` / `env` / `envSummary` / `engine` / `capabilities`;
154
- `state.json` 同步带出这些字段。
155
-
156
- ### 说明
157
-
158
- - macOS 路径无法在开发机(Windows)上真机验证,因此识别与 Homebrew 输出解析均以注入式单测覆盖
159
- (`test/unit.mjs` 共 66 项),安装/更新命令按 Homebrew 官方 CLI 行为编写。
160
- - 扫描/更新接口按平台放行:Windows 与 macOS 通过,其它系统返回 400 并说明原因。
161
-
162
- ## [1.0.0] — 2026-10-31
163
-
164
- 首个可用版本。
165
-
166
- ### 检测
167
-
168
- - 用 `winget list` 列出已安装应用(含 winget 目录收录的软件与商店 / MSIX 包),并用注册表三处
169
- `Uninstall` 键补齐 winget 目录未收录的软件。全程只读注册表与文件,**不启动任何被检测的应用**。
170
- - 用 `winget upgrade --include-unknown` 取「已装版本 → 可升级版本」,按包 Id 与清单合并。
171
- - 版本比较支持 Windows 风格的 `1.2.3.4`、日期风格 `2026.7.992920`、带后缀 `0.9.2+2026103013`
172
- 以及 `1.2.3-beta` 这类预发布标记。
173
- - 定宽表格解析按**显示宽度**切列(CJK 占 2 列),并剔除 `15 upgrades available.` 之类的尾部提示行。
174
-
175
- ### 更新
176
-
177
- - 一键更新链路:`winget download` 从**发布方官网**拉取**完整安装包**(下载时核对清单里的 SHA-256)→
178
- 解析随包落盘的 winget 清单,取发布方自己声明的 `InstallerSwitches.Silent` →
179
- 静默安装 → 用 `winget list --id` 复核安装后版本。
180
- - 官网包哈希与目录记录不一致(发布方重签了安装包)或清单禁止直接下载时,自动退回
181
- `winget upgrade --id … --exact --silent`(同源全量包,下载与安装交给 winget)。
182
- - 安装包与清单留档在下载目录,便于核对来源;可关闭「保留安装包」在安装后自动清理。
183
- - 需要管理员权限的安装包会正常弹出 UAC,插件不做静默提权。
184
-
185
- ### 界面
186
-
187
- - 设置 → 应用更新:工具条(刷新检测 / 全部更新 / 更新所选 / 取消 / 打开下载目录)、
188
- 状态行、进度条、筛选行、应用表格、可折叠日志。
189
- - 表格列宽默认给足以完整显示应用名,并且**每列可拖拽调宽、双击恢复默认**,偏好记在浏览器本地。
190
- - 插件设置面板:安装包下载目录(含绝对路径校验)、保留安装包、演练模式、下载失败回退 winget、
191
- 包含商店应用、包含目录未收录应用。演练模式开启时页面顶部有醒目提示。
1
+ # 更新日志
2
+
3
+ 本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
+
5
+ ## [1.3.5] — 2026-11-02
6
+
7
+ 两轮对抗式复查的合并收口(1.3.4 之后),修掉 9 个真实缺陷、补上 1 处文档与实现不一致的承诺。
8
+ 这一版没有新功能 —— 全是「判断错误 / 静默失败 / 条件触发的安全面」类问题。
9
+
10
+ ### 修复
11
+
12
+ - **尾缀字典序把 build 号判反**(`lib/version.js`)。数字段一致后,尾缀(`-build10`、
13
+ `-r100`、`-beta2`、Java 的 `8u202` 这类)原来是**整串字典序**比较:`-build9` > `-build10`、
14
+ `8u99` > `8u202` —— 位数一变顺序就翻转。对插件来说这是静默漏报:已装 `1.2.3-build9`、
15
+ 有 `1.2.3-build10`,`isNewer()` 返回 false → 用户看到「已是最新」;复核路径还会把成功
16
+ 安装误判「没有变化」。现在尾缀按**自然序**比较(数字块按数值、字母块按字典序,
17
+ `-beta2 < -beta10`),保留原有语义:正式版 > 任何预发布尾缀。
18
+ - **Windows 扫描不检查取消/超时,半截输出伪装成「0 个可更新」**(`lib/engine.js scanApps`)。
19
+ 取消或 240s 超时杀掉 `winget list` 后,半截 stdout 被解析成残缺清单;
20
+ `if (upRes.ok || upRes.stdout)` 又把超时残留字符串真值当成功 → `hasUpdate` 全 false →
21
+ 整表覆盖上一次完整结果。macOS 路径 1.3.4 修过同款,这次补上 Windows 侧:
22
+ 取消一律向上抛(不算故障、不写红横幅),非零退出/超时不再解析,降级注册表源并如实
23
+ 警告「本次清单可能不完整」。
24
+ - **注册表读取会被一条无关的 PowerShell 警告整体误伤**(`lib/registry.js`)。stderr 原来与
25
+ stdout 共用同一缓冲数组:任何一条执行策略提示或 .NET 横幅插进 JSON 中部 → 整体解析失败
26
+ → 注册表源全量丢弃,用户以为这台机器读不到已装应用。winget 侧同病(`lib/winget.js`):
27
+ stderr 混进定宽表还可能伪造数据行。现在两个进程都分桶收集,`stderr` 字段如实传给错误分类。
28
+ - **哈希校验:文档承诺 ≠ 实际行为 → 现在真的做了**(`lib/engine.js`)。README 写「下载时按清单
29
+ 核对 SHA-256」——winget 自己确实会核对(我们从不传 `--skip-hash-check`),但**执行前的那个文件**
30
+ 此前没有任何本地校验。而这个文件紧接着会以**管理员**身份静默执行。1.3.5 起:
31
+ - `parseInstallerManifest` 按 Installer 块配对收集 `Architecture → InstallerSha256`
32
+ (兼容 `- Architecture:` 列表首项写法);
33
+ - `verifyInstallerHash` 流式(`createReadStream`,不整读几百 MB 的包)复核实际哈希;
34
+ - 配对策略:先按本机架构键,再用文件名别名(`64-bit`/`x64`/`arm64`/`aarch64`…),
35
+ 单哈希清单直接用;**多架构配对不出时宁可跳过并记 info**,绝不拿别架构哈希把正确文件判成篡改;
36
+ - 不一致 → **拒绝执行**,报「期望/实际」两个哈希,照旧走 `winget upgrade` 兜底;
37
+ - 更新结果新增 `hash: verified|skipped|mismatch`。
38
+ - **兼容模式(legacy loopback)可被任意网页 CSRF 驱动提权安装**(`lib/index.js`)。开启
39
+ `allowLegacyLoopback` 后,第三方网页的 `fetch("http://127.0.0.1:…/update",{mode:"no-cors"})`
40
+ 天然不带 Cookie → 过「无凭据才豁免」那条规则 → 无认证触发「下载任意包 + UAC 提权静默安装」。
41
+ 现在豁免路径加来源围栏:`Origin`/`Referer` 存在且与 `Host` 不同源、或
42
+ `Sec-Fetch-Site: cross-site`,一律 403(浏览器禁止脚本伪造这些头,判定可靠;本机脚本与
43
+ IPC 桥不带这些头,不受影响)。
44
+ - **取消/超时杀不掉提权安装树**(`lib/elevate.js`)。旧实现 Node 侧只能杀**启动器**;
45
+ 提权副本与安装器在独立进程树里继续跑,而队列已排到下一个应用(并发双装 + 幽灵安装)。
46
+ 现在助手脚本(本身就是提权进程)自己盯 `cancel.flag`(500ms 轮询),看到就
47
+ `taskkill /T /F` 杀整棵安装器树再报告退出码;读不到退出码时按失败处理,绝不伪造 0。
48
+ - **「取消后立刻重试」的竞态**(`lib/index.js`)。旧任务 finally 无条件清 `control` 槽位,
49
+ 会把新任务句柄清成 null → `/cancel` 声称成功但新任务不可取消,旧半截结果还会覆盖新结果。
50
+ 现在 finally 做所有权检查(`control.scanAbort === abort` 才清),被接管的任务结果作废。
51
+ - **macOS 更新复核失败仍报「更新完成」**(`lib/engine.js runMacosUpdates`)。
52
+ `brew list` 读空时此前直接 `log success` 且 `entry.ok=true` 无标注;现在与 Windows 准则
53
+ 一致:读不到版本 → note 标「未复核:brew list 读不到更新后的版本」+ warn 日志;
54
+ 取消/超时优先判错,不给误导性原因。
55
+ - **注册表 PowerShell 枚举没有取消通道**(`lib/registry.js`)。扫描取消后仍跑满 60s;
56
+ 现在 `spawn({ signal })` 直通,取消立刻杀。
57
+ - **`normalizeName` 的 `\b版本\b` 是死分支**(`lib/registry.js`)。JS `\b` 只认 ASCII 边界,
58
+ 「版」「本」都是非词字符 → 永不命中 → 「XXX 版本 1.2.3」与 winget 的「XXX」认不成同一条,
59
+ 同一软件重复列出。改为字面匹配 `版本`。
60
+ - **中文 winget 的落盘行匹配不到**(`lib/engine.js downloadInstaller`)。只认英文
61
+ `Installer downloaded:`,中文系统的「安装程序已下载:」落到不可靠的目录 diff,失手时报
62
+ 「没找到安装包」→ 触发兜底**把同一个包再下载一遍**。正则改为中英双语(与
63
+ `classifyWingetError` 一致)。
64
+
65
+ ### 文档
66
+
67
+ - README / README.en 的执行流程表补充「执行前本地复核哈希(1.3.5 起)」,使承诺与实现一致。
68
+
69
+ ### 测试
70
+
71
+ - `node test/unit.mjs` **184/184**(+31:自然序 8、清单哈希配对 4、哈希端到端 3、
72
+ 来源围栏 9、归一化 2、双语匹配 3、取消检查 2)、`node test/render.mjs` **51/51**、
73
+ `node test/deny.mjs` **12/12**。
74
+ - 哈希端到端用 `mkdtemp` 临时目录真算真比:一致 → 通过;不一致 → 拒绝且错误里带两个哈希;
75
+ 清单缺哈希 → `skipped` 而不是失败。
76
+
77
+ ## [1.3.4] — 2026-11-01
78
+
79
+ 1.3.3 发布后对着同样几台真实机器做了一轮对抗式复查,修掉 5 个「1.3.3 自己引入或没修干净」的问题。
80
+ 其中两个会**直接让用户看到错误的结论**(阿里云旧快照导致静默漏报、红色告警横幅永久常驻),
81
+ 所以单独发一个补丁版本,而不是悄悄混进下一个功能版本。
82
+
83
+ ### 修复
84
+
85
+ - **阿里云镜像是一份 17 个月前的冻结快照,会把「有更新」静默说成「已是最新」**。
86
+ 1.3.3 把 `mirrors.aliyun.com/homebrew/homebrew-bottles/api/cask.json` 放进了并发竞速,
87
+ 但实测(`curl -sSI` + 全量比对):
88
+
89
+ | 源 | `Last-Modified` | 大小 | 与官方版本不一致 |
90
+ |---|---|---|---|
91
+ | `formulae.brew.sh`(官方) | 2026-11-01 | 19.3 MB | — |
92
+ | 清华 TUNA | 2026-11-01 | 19.3 MB | — |
93
+ | 中科大 USTC | 2026-11-01 | 19.3 MB | — |
94
+ | 阿里云 | **2025-05-22** | **13.7 MB** | **2908 个 cask 版本落后,1257 个缺失** |
95
+
96
+ 例如 `1password` 阿里云是 `8.10.76`、官方是 `8.12.42`。它的 JSON 结构同构、不会报错,
97
+ 而 `buildMacosItems` 只在 `isNewer(latest, app.version)` 为真时才标记可更新 —— 于是旧数据
98
+ 不会误报,而是**静默漏报**,用户看到「0 个可更新」:正是 1.3.3 承诺要消灭的那类沉默失败。
99
+ 更糟的是阿里云是国内最快的源,竞速里几乎总是它先返回,**越快越容易中招**。
100
+ 现在(`lib/macos.js`)把它从 `CATALOG_URLS` 移除,只保留官方 + 清华 + 中科大三个同步源。
101
+
102
+ - **红色告警横幅会永久常驻**。1.3.3 新增的横幅渲染的是 `state.scan.warnings`,但
103
+ `warnings` 里混着两条**按设计的日常说明**,而 `includeStoreApps` 默认 `true`:
104
+ - `App Store 应用只列出、不由本插件更新`(`lib/engine.js`)
105
+ - `「包含非 Homebrew 应用」已关闭`(`lib/macos.js`)
106
+
107
+ 结果每次**完全正常**的扫描也会挂一条 `role="alert"` 红条,把真正的故障(brew 失败 /
108
+ 目录抓不到)稀释成噪音。现在这两条改走 `info` 日志,`warnings` 只保留「本次判断依据失效」。
109
+
110
+ - **扫描整体失败时界面什么也不显示**。`state.scan.error` 在 1.3.2/1.3.3 的界面上**完全没有
111
+ 渲染**,扫描抛异常时用户只看到一行「0 个应用」。现在同一条横幅也覆盖它(「检测失败:<原因>」),
112
+ 并且 `warnings` 里混入非字符串/空值时不会渲染出错。
113
+
114
+ - **用户按「取消」会被报成故障**。`runProcess` 在 abort 后返回 `{ok:false, code:null}`,
115
+ 1.3.3 的判断会把它写成「Homebrew 更新检查失败(exit null):本次可能漏报可更新应用」——
116
+ 用户主动取消,却收到一条错误。现在 `signal.aborted` 时只记一条 `info`。
117
+
118
+ - **`brew outdated` 的失败仍可能被吞掉**。1.3.3 用的 `if (outRes.ok || outRes.stdout)` 是
119
+ 字符串真值判断:超时被杀但已经吐出半截 JSON 时条件为真 → 解析出 0 条 → **不发警告**,
120
+ 与同文件 `listRes` 的 `if (listRes.ok)` 自相矛盾。现在只有 `ok` 才解析,其余一律告警。
121
+
122
+ ### 测试
123
+
124
+ - **`findBrew` 的断言不再依赖本机环境**:1.3.3 只把返回值糊进一个「null 或两个默认前缀」的
125
+ 白名单,而 `findBrew` 的候选里还有 `/home/linuxbrew/.linuxbrew/bin/brew` 且会扫 `$PATH` ——
126
+ 在装了 Linuxbrew 的机器上照样红。现在用临时目录构造「有 brew / 没有 brew」两种确定场景,
127
+ 验证真正的不变量(**只返回真实存在的文件**),跨平台可跑。
128
+ - 新增界面回归测试:警告横幅的出现条件(无警告不出、扫描中不出、整体失败要出、非字符串不崩)。
129
+ - 全量:`node test/unit.mjs` **153/153**、`node test/render.mjs` **51/51**、
130
+ `node test/deny.mjs` **12/12**。
131
+
132
+ ## [1.3.3] — 2026-11-01
133
+
134
+ 针对 macOS 真实故障现场(macOS 27.2 / Apple M5 / Homebrew 7.0.11 / Node 24.21.0)的修复。
135
+ 1.3.2 之后扫描确实会枚举应用了,但当 **Homebrew 自身故障**或**cask 目录抓不下来**时,
136
+ 界面给出的仍然是「0 个可更新」这种「看起来一切正常」的结论 —— 本次把这两条沉默失败的路径
137
+ 改成明说,并把「白等两分钟」变成「一秒出结果」。
138
+
139
+ ### 修复
140
+
141
+ - **Homebrew 命令失败被伪装成「0 个已安装应用」**(实测触发:brew 卡在 Portable Ruby 4.0.9
142
+ 的 ghcr.io 下载并持有 `vendor-install-ruby` 锁,此后每条 `brew` 命令都立刻以
143
+ `lockf: 200: already locked` 失败)。旧代码不检查 `ok`,直接解析空 stdout,于是日志写
144
+ 「Homebrew 报告 0 个已安装 cask」,真正的错误躺在 stderr 里被丢弃 —— 用户看到的是
145
+ 「本机没装任何应用」。现在(`lib/engine.js`):
146
+ - `brew list --cask --versions` / `brew outdated` 失败时写入扫描警告 + 警告日志,并给出可执行
147
+ 的排查提示(「先在终端执行 `brew list --cask` 检查 Homebrew 是否正常」);
148
+ - 这两条命令的超时放宽到 5 / 10 分钟(慢网络下 brew 首次运行可能真的很慢),
149
+ `brew --version` 放宽到 60 秒。
150
+ - **cask 目录在国内网络下必然抓不到**(`formulae.brew.sh/api/cask.json` 有 19 MB,实测
151
+ 25 KB/s,120 秒只下到 0.39 MB)。旧实现单源单次 120 秒,于是每次扫描白等两分钟后
152
+ `catalog = null`,所有应用都算不出「可更新」。现在(`lib/macos.js`):
153
+ - 官方地址 + 清华 TUNA / 中科大 USTC / 阿里云三个镜像 **并发竞速**(`Promise.any`),
154
+ 谁先返回用谁,其余立刻 `abort()` 取消;官方仍参与竞速,网络正常时优先用官方数据;
155
+ - 单地址限时 60 秒(原 120 秒);
156
+ - 全部失败时回退到 **30 天内的过期缓存**兜底(`readCatalogCache` 新增 `allowStale`),
157
+ 宁可略旧,也好过整屏「无法判断是否有新版本」;顺带拒绝空数组缓存。
158
+ - 实测:冷启动(无缓存)62 秒 → 42 秒;有缓存时 0.7 秒。
159
+ - **扫描警告在界面上不可见**:`state.scan.warnings` 之前只写进 state 与运行日志。目录抓不到时
160
+ 用户只看到「0 个可更新」,完全不知道判断依据已经失效。现在(`lib/client.js`)检测完成后若有
161
+ 警告,在状态行下方显示红色横幅(复用 `.appupd-banner-warn`)取第一条,多条可展开「查看全部」。
162
+
163
+ ### 测试
164
+
165
+ - **修掉一条环境相关的既有失败断言**:`findBrew` 在 `HOMEBREW_PREFIX` 之外还会探测
166
+ `/opt/homebrew`、`/usr/local`、`/home/linuxbrew` 等默认位置,因此「装了 Homebrew 的机器」
167
+ 上旧断言 `=== null` 必然失败 —— 1.3.2 发布状态下在真实 Mac 上 `npm test` 是 **151/152**。
168
+ 现在改成跨平台的存在性断言(覆盖 Apple Silicon / Intel / Linuxbrew 三处默认前缀)。
169
+ - 全量:`node test/unit.mjs` **152/152**、`node test/render.mjs` **44/44**、
170
+ `node test/deny.mjs` **12/12**。修完在真实机器上全量扫描:110 个应用、33 个对上 cask、
171
+ 0 条警告、耗时 0.7 秒。
172
+
173
+ ### 说明
174
+
175
+ - 「0 个可更新」在部分机器上仍然**是正确的**:插件只对 Homebrew 管理的应用提供一键更新,
176
+ 拖 DMG / 手动装的应用只列出并标 `not-managed`。想让某个应用变成可一键更新,可以用
177
+ `brew install --cask --adopt <token>` 收编(接管现有 `.app`,不重装)。
178
+
179
+ ## [1.3.2] — 2026-11-01
180
+
181
+ ### 修复
182
+
183
+ - **macOS 上「未检测到任何已安装应用」**(用户实测报告):1.3.0/1.3.1 的 macOS 扫描只执行
184
+ `brew list --cask --versions`,也就是**只有通过 Homebrew 装过的应用才会出现在列表里**。
185
+ 一台正常使用的 Mac 上绝大多数应用是拖 DMG 装的,于是该命令返回空 → 界面显示「未检测到
186
+ 任何已安装应用」,日志里是「Homebrew 报告 0 个已安装 cask 应用」。这不是「没有应用」,
187
+ 而是**根本没有枚举应用**。现在(`lib/macos.js`):
188
+ - 枚举标准应用目录里的 `.app`:`/Applications`、`/Applications/Utilities`、
189
+ `/System/Applications`(及其 `Utilities`)、`~/Applications`,用 `plutil` 读每个
190
+ bundle 的 `Info.plist` 取显示名与版本(读不到时退回 `defaults read`);
191
+ - 抓取 Homebrew 的 cask 目录(`formulae.brew.sh/api/cask.json`,本地缓存 24 小时)把
192
+ `.app` 反查到 cask token,从而知道**官网最新版本**;
193
+ - 只有**确实由 Homebrew 管理**的应用才标记为可一键更新;其余应用如实列出并标注原因
194
+ (`not-managed` 非 Homebrew 管理 / `store-managed` 由 App Store 管理 /
195
+ `system-component` 系统组件),绝不假装能更新。
196
+ - **同一个 `.app` 被多个 cask 声明时匹配错乱**:真实目录里 `iterm2` 与 `iterm2@nightly`
197
+ 都声明 `iTerm.app`,旧写法「后写覆盖」会让匹配结果随目录顺序漂移(实测会把同一个应用
198
+ 列出两次)。现在保留全部候选,由 `pickCask` 按「已装 > 非弃用/停用 > 主 token >
199
+ token 与应用名相等」挑选。
200
+ - **没有 Homebrew 也能扫描**:`capabilitiesFor` 不再把「macOS 无 Homebrew」判成不可扫描 ——
201
+ 应用照样列出来,只是无法判断/驱动更新,提示改为「安装 Homebrew 后可一键升级 cask 应用」。
202
+ - **设置面板按平台显示**:macOS 上不再出现「自动提权安装」「下载失败时回退 winget」
203
+ 「包含应用商店应用」(Windows 概念),改为 macOS 专属的「包含 App Store 应用」与
204
+ 「包含非 Homebrew 应用」(新增设置项 `includeUnmanagedApps`,默认开启)。
205
+ - **空状态文案分平台**:macOS 下列表为空时提示去看运行日志里的「枚举应用目录」记录,
206
+ 而不是干巴巴一句「未检测到任何已安装应用」。
207
+
208
+ ### 新增
209
+
210
+ - **`lib/macos.js`**:应用目录枚举、`Info.plist` 解析、cask 目录缓存与解析、cask 匹配、
211
+ 列表合并(纯函数 `buildMacosItems` 便于单测)。
212
+ - 日志更细:分别记录「枚举到多少个 .app」「Homebrew 报告多少个 cask」「读取 cask 目录结果」,
213
+ macOS 上再出现异常时一眼能看出断在哪一步。
214
+
215
+ ### 测试
216
+
217
+ - `test/unit.mjs` 152 项(新增 macOS 应用枚举 20 项、同名 cask 挑选 6 项、cask 目录解析 6 项);
218
+ 用**真实** `formulae.brew.sh/api/cask.json`(7824 个 cask)验证过匹配与去重。
219
+ - `test/render.mjs` 44 项(新增 macOS 设置面板 6 项、macOS 空状态文案 2 项)。
220
+ - `test/deny.mjs` 12 项不变。
221
+
222
+ ## [1.3.1] — 2026-11-01
223
+
224
+ ### 安全
225
+
226
+ - **「仅凭回环地址放行」改为默认拒绝**(插件市场 dshopen.org 对 1.3.0 的复核意见):
227
+ 1.3.0 接入 `connection` 准入后仍留了一条退回路径 —— 宿主没有 `connection` 服务,或请求
228
+ 不带任何连接凭据且来自回环地址时,直接按「本机信任」放行。在没有准入服务的旧宿主上,
229
+ 任何本机进程(以及能向回环发起请求的页面)都能借此调用 `/update`、`/settings` 等安装接口,
230
+ 等于绕过认证。1.3.1 起这条路径**默认关闭**:
231
+ - 宿主有 `connection`:一律以 `admit()` 的结果为准,`401` / `403` / `409` 原样返回
232
+ (响应头 `X-DSH-Admission: denied`)。只有「401 + 回环 + 无凭据 + 用户显式开启兼容模式」
233
+ 这一种例外,且必须在下面第 2 条的前提下才成立。
234
+ - 宿主没有 `connection`:**默认 403**,不再自动相信回环地址。
235
+ - **兼容模式必须由用户显式开启**(保留给确实跑在旧宿主上的用户):插件配置
236
+ `allowLegacyLoopback: true`(`cordis.patch.yml` 覆盖同名条目)或环境变量
237
+ `DSH_APP_UPDATER_ALLOW_LEGACY_LOOPBACK=1`,两者默认都没有。开启后:
238
+ - 放行的请求带 `X-DSH-Admission: loopback-legacy-optin` 与 `X-DSH-Admission-Warning`;
239
+ - `state.json` / `health.json` 新增 `admission` 字段(`mode` / `service` /
240
+ `allowLegacyLoopback` / `warning`),复核时不必读代码就能确认当前路径;
241
+ - 设置页顶部显示红色警示条,日志写一条 `warn`。
242
+ - **准入拒绝统一返回 JSON**:`401`/`403`/`409` 带 `{ ok:false, error, admission:"denied" }`,
243
+ 不再用纯文本,排障时能直接看出是被准入挡掉的。
244
+
245
+ ### 测试
246
+
247
+ - `test/unit.mjs`:准入用例从 6 项扩到 25 项,覆盖兼容模式开关(配置 / 环境变量 / 缺省关闭)、
248
+ 无 `connection` 服务默认拒绝、显式开启后放行、非回环仍拒绝、`409`、带凭据未认证不放行等。
249
+ - 新增 `test/deny.mjs`:起真实 HTTP 服务,验证无 `connection` 且未开兼容模式时**每一条路由**
250
+ 都是 `403 + X-DSH-Admission: denied`;再验证显式开启后放行并标注 opt-in 响应头。
251
+ - `test/render.mjs`:新增兼容模式警示条 4 项(开启显示、样式、文案、默认不显示)。
252
+ - 自测命令更新为 `node test/unit.mjs && node test/render.mjs && node test/deny.mjs`。
253
+
254
+ ## [1.3.0] — 2026-10-31
255
+
256
+ ### 修复
257
+
258
+ - **机器级安装全部失败的问题**:进程没有管理员令牌时,MSI 直接失败(msiexec 1603,
259
+ MSI 日志里是 `Error 1925: You do not have sufficient privileges…`),而部分安装器是
260
+ **静默退出 0 却什么都没装**(实测 Logitech G HUB)。两个现象同源,都是权限。
261
+ - **安装后复核**:退出码 0 不再等于成功。安装完成后用「安装前版本 / 安装后版本 / 目标版本」
262
+ 判断是否真的生效,没生效就如实报失败 —— 原来只看退出码,会把 G HUB 那种
263
+ 「退出码 0、版本没变」的情况误报成「更新完成」。
264
+ - **架构过滤回退**:`winget download/upgrade --architecture x64` 在清单里没有该架构时现在能
265
+ 正确识别(中文报错是「找不到适用的安装程序」,旧正则只匹配了「没有适用」)并退回不限架构
266
+ 重试;`winget upgrade` 兜底同时补上 `--include-unknown`(虎牙直播的已装版本被读成 Unknown,
267
+ 不带它直接「目录里没有适用的更新」)。
268
+ - **Edge**:winget 目录里的 `Microsoft.Edge` 是企业版 MSI,与本机消费版 Edge 的安装技术不匹配,
269
+ winget 直接拒绝(`0x8A15008E`「安装技术与当前安装的版本不同,请卸载包并安装较新版本」),
270
+ 现在先走 Edge 自带的更新服务 `MicrosoftEdgeUpdate.exe /ua`;若它没生效(实测该机的
271
+ EdgeUpdate 引擎目录是 `1.3.311.5` 而服务主程序还是 `1.3.275.13`,服务起不来,`/ua` 直接
272
+ `0x80070002`),自动退回安装 winget 目录里的官网全量包(企业版 MSI,提权执行)——
273
+ 实测这一步能把 Edge 正常装到目标版本,winget 拒绝执行只是因为它的策略。
274
+ - **读不到版本的应用**:虎牙直播的卸载信息里没有版本号,winget 永远显示 `Unknown`。
275
+ 复核阶段不再把它误判为失败,改为「已安装、未复核」写进结果与日志。
276
+ - **「安装包不生效」的软件给出可操作提示**:实测 Logitech G HUB 的更新包早已下载到
277
+ `%ProgramData%\LGHUB\next.json` 指向的 depots(build 880806 / 2026.7.992920),但安装器
278
+ `--silent` 与 `winget upgrade` 都会在 3 秒内退出 0 且什么都不装,只有**启动 G HUB 本体**
279
+ 之后 `current.json` 才跳到新版本。插件承诺不启动用户的软件,所以这类情况现在会在失败
280
+ 原因里直接给出「手动打开一次该应用即可完成升级」的提示(`updateHintFor`)。
281
+ - **MSI 日志**:MSI 安装现在带 `/l*v <下载目录>\<包名>.msi.log`,失败时把日志路径写进错误信息
282
+ (「Error 1925」就是这么被找到的)。
283
+ - **残留清单**:关闭「保留安装包」时,现在连 winget 落盘的 `.yaml` 清单一并删除。
284
+
285
+ ### 新增
286
+
287
+ - **自动提权安装**(`lib/elevate.js`):需要管理员的安装类命令交给 `Start-Process -Verb RunAs`
288
+ 的提权副本执行,退出码与输出写进临时文件后读回。新增开关「自动提权安装」(默认开启)。
289
+ UAC 为「始终通知」时每次安装会弹一次确认;把 `ConsentPromptBehaviorAdmin` 设为 0
290
+ (管理员静默提权)时完全无感。
291
+ - 说明:提权副本是独立进程,取消更新只会停掉启动器,已经开始的那次安装会自己跑完。
292
+
293
+ ### 安全
294
+
295
+ - **接入宿主 connection 准入**(插件市场 dshopen.org 对 1.2.0 的复核意见):所有
296
+ `/dsh-app-updater/*` 路由先过 `ctx.connection.admit(request)` —— DSH 用来做 Host/Origin
297
+ 信任校验与浏览器会话 / API 认证的那套边界,宿主自己的 `/api` 路由用的就是它。
298
+ 未通过准入的请求直接得到 `401`/`403`;带凭据的请求一律以准入结果为准。
299
+ 只有在宿主没有 `connection` 服务、或者请求不带任何连接凭据且来自回环地址时,才退回旧的
300
+ 「回环」规则,并在响应头标注 `X-DSH-Admission: loopback-legacy`(宿主自己的 Electron
301
+ 载体与本机脚本都不带 Cookie,需要一个不误伤它们的路径)。
302
+ - 因此 README 里「用 curl 直接改设置」的例子现在需要带上 DSH 的会话 Cookie,或在
303
+ `connection` 不可用的宿主上从回环地址调用。
304
+
305
+ ### 说明
306
+
307
+ - 读类命令(`winget list` / `upgrade --include-unknown` / `--version`)不提权。
308
+ - `update.results[]` 新增 `installOk`(安装器自身是否报成功)与 `manifest`(落盘的清单位置)。
309
+
310
+ ## [1.2.0] — 2026-10-31
311
+
312
+ ### 新增
313
+
314
+ - **运行环境识别**(`lib/platform.js`):识别 Windows / macOS / 其它系统、具体版本与名称
315
+ (`Windows 11(build 26300)`、`macOS 15(Sequoia)`)、CPU 架构(ARM64 / x64 / Intel / x86),
316
+ 并记录 CPU 型号与线程数。Windows on ARM 上跑 x64 的 Node 时,靠 `PROCESSOR_ARCHITEW6432`
317
+ 仍能还原出真实架构,而不是被骗成 x64。
318
+ - **按架构挑选安装包**:Windows 侧把识别到的架构传给 winget 的 `--architecture`,让「官网全量包」
319
+ 与当前系统/芯片匹配;清单里没有该架构的安装包时自动退回自动匹配,并在日志里说明。
320
+ macOS 侧由 Homebrew 按本机架构取对应 cask。新增开关「按本机架构安装包」(默认开启)。
321
+ - **macOS 更新引擎(Homebrew cask)**:用 `brew list --cask --versions` 拿已装版本、
322
+ `brew outdated --json=v2 --cask --greedy` 拿可升级版本,`brew upgrade --cask <token>` 更新;
323
+ 演练模式改用 `brew fetch --cask <token>`,只下载安装包不安装。没有 Homebrew 时明确报告
324
+ 「不可用」,而不是假装能更新。
325
+ - **界面**:状态行与设置面板新增「运行环境」(系统 · 架构 · 引擎);引擎不可用时红字给出原因;
326
+ 应用来源新增 `homebrew` 徽章。
327
+ - `/health.json` 增加 `version` / `env` / `envSummary` / `engine` / `capabilities`;
328
+ `state.json` 同步带出这些字段。
329
+
330
+ ### 说明
331
+
332
+ - macOS 路径无法在开发机(Windows)上真机验证,因此识别与 Homebrew 输出解析均以注入式单测覆盖
333
+ (`test/unit.mjs` 共 66 项),安装/更新命令按 Homebrew 官方 CLI 行为编写。
334
+ - 扫描/更新接口按平台放行:Windows 与 macOS 通过,其它系统返回 400 并说明原因。
335
+
336
+ ## [1.0.0] — 2026-10-31
337
+
338
+ 首个可用版本。
339
+
340
+ ### 检测
341
+
342
+ - 用 `winget list` 列出已安装应用(含 winget 目录收录的软件与商店 / MSIX 包),并用注册表三处
343
+ `Uninstall` 键补齐 winget 目录未收录的软件。全程只读注册表与文件,**不启动任何被检测的应用**。
344
+ - 用 `winget upgrade --include-unknown` 取「已装版本 → 可升级版本」,按包 Id 与清单合并。
345
+ - 版本比较支持 Windows 风格的 `1.2.3.4`、日期风格 `2026.7.992920`、带后缀 `0.9.2+2026103013`
346
+ 以及 `1.2.3-beta` 这类预发布标记。
347
+ - 定宽表格解析按**显示宽度**切列(CJK 占 2 列),并剔除 `15 upgrades available.` 之类的尾部提示行。
348
+
349
+ ### 更新
350
+
351
+ - 一键更新链路:`winget download` 从**发布方官网**拉取**完整安装包**(下载时核对清单里的 SHA-256)→
352
+ 解析随包落盘的 winget 清单,取发布方自己声明的 `InstallerSwitches.Silent` →
353
+ 静默安装 → 用 `winget list --id` 复核安装后版本。
354
+ - 官网包哈希与目录记录不一致(发布方重签了安装包)或清单禁止直接下载时,自动退回
355
+ `winget upgrade --id … --exact --silent`(同源全量包,下载与安装交给 winget)。
356
+ - 安装包与清单留档在下载目录,便于核对来源;可关闭「保留安装包」在安装后自动清理。
357
+ - 需要管理员权限的安装包会正常弹出 UAC,插件不做静默提权。
358
+
359
+ ### 界面
360
+
361
+ - 设置 → 应用更新:工具条(刷新检测 / 全部更新 / 更新所选 / 取消 / 打开下载目录)、
362
+ 状态行、进度条、筛选行、应用表格、可折叠日志。
363
+ - 表格列宽默认给足以完整显示应用名,并且**每列可拖拽调宽、双击恢复默认**,偏好记在浏览器本地。
364
+ - 插件设置面板:安装包下载目录(含绝对路径校验)、保留安装包、演练模式、下载失败回退 winget、
365
+ 包含商店应用、包含目录未收录应用。演练模式开启时页面顶部有醒目提示。
192
366
  - 全部写操作要求本机(loopback)来源并显式带 `confirm:true`。