dsh-app-updater 1.3.0 → 1.3.2
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 +75 -0
- package/CONTRACT.md +20 -9
- package/README.en.md +37 -12
- package/README.md +36 -12
- package/lib/client.js +68 -4
- package/lib/engine.js +61 -65
- package/lib/index.js +122 -26
- package/lib/macos.js +409 -0
- package/lib/platform.js +7 -5
- package/lib/state.js +2 -0
- package/package.json +2 -2
- package/test/deny.mjs +149 -0
- package/test/harness.mjs +3 -0
- package/test/render.mjs +75 -1
- package/test/unit.mjs +212 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,81 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
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
|
+
|
|
5
80
|
## [1.3.0] — 2026-10-31
|
|
6
81
|
|
|
7
82
|
### 修复
|
package/CONTRACT.md
CHANGED
|
@@ -41,10 +41,14 @@ dsh-app-updater/
|
|
|
41
41
|
|
|
42
42
|
全部 `kind: "exact"`,前缀 `/dsh-app-updater/`。每个请求先过 **connection 准入**:
|
|
43
43
|
`ctx.connection.admit(req)`(DSH 的 Host/Origin 信任校验 + 浏览器会话 / API 认证,
|
|
44
|
-
宿主自己的 `/api` 路由用的同一套),被拒即返回 `401`(未认证)/ `403
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
宿主自己的 `/api` 路由用的同一套),被拒即返回 `401`(未认证)/ `403`(不可信来源)/
|
|
45
|
+
`409`(连接被占用),响应头带 `X-DSH-Admission: denied`;写操作再要求 `confirm: true`。
|
|
46
|
+
|
|
47
|
+
**默认拒绝**(1.3.1 起):宿主没有 `connection` 服务时不再「仅凭回环地址放行」,而是 `403`。
|
|
48
|
+
只有用户显式开启兼容模式(插件配置 `allowLegacyLoopback: true` 或环境变量
|
|
49
|
+
`DSH_APP_UPDATER_ALLOW_LEGACY_LOOPBACK=1`)时,回环请求才放行,并带
|
|
50
|
+
`X-DSH-Admission: loopback-legacy-optin` 与 `X-DSH-Admission-Warning`(见 `lib/index.js` 的
|
|
51
|
+
`admissionGate` / `resolveLegacyLoopbackOptIn`)。
|
|
48
52
|
|
|
49
53
|
### GET /dsh-app-updater/state.json
|
|
50
54
|
|
|
@@ -62,7 +66,9 @@ dsh-app-updater/
|
|
|
62
66
|
"update": { "running": false, "total": 0, "done": 0, "current": null, "error": null },
|
|
63
67
|
"apps": { "scannedAt": 0, "stats": { "total": 0, "upgradable": 0, "unsupported": 0 }, "items": [], "engine": "winget" },
|
|
64
68
|
"log": [ { "at": 0, "level": "info|warn|error|success", "text": "..." } ],
|
|
65
|
-
"settings": { "includeStoreApps": true, "includeRegistryOnly": true, "downloadDir": "", "keepInstallers": true, "hashFallbackUpgrade": true, "dryRun": false, "matchSystemArch": true, "autoElevate": true }
|
|
69
|
+
"settings": { "includeStoreApps": true, "includeRegistryOnly": true, "downloadDir": "", "keepInstallers": true, "hashFallbackUpgrade": true, "dryRun": false, "matchSystemArch": true, "autoElevate": true },
|
|
70
|
+
// 准入现状,供界面与复核确认当前走哪条路径(默认 "deny" 不会出现在能读到响应的地方)
|
|
71
|
+
"admission": { "mode": "connection|connection+loopback-legacy|loopback-legacy-optin|deny", "service": true, "allowLegacyLoopback": false, "warning": null }
|
|
66
72
|
}
|
|
67
73
|
```
|
|
68
74
|
|
|
@@ -70,20 +76,25 @@ dsh-app-updater/
|
|
|
70
76
|
|
|
71
77
|
```jsonc
|
|
72
78
|
{
|
|
73
|
-
"id": "Google.Chrome", // winget id;注册表项 "reg:<key>";macOS "brew:<cask token>"
|
|
79
|
+
"id": "Google.Chrome", // winget id;注册表项 "reg:<key>";macOS 由 Homebrew 管理的是 "brew:<cask token>",其余 "macapp:<bundle id>"
|
|
74
80
|
"name": "Google Chrome",
|
|
75
81
|
"version": "155.0.8059.85", // 已安装版本,未知时 ""
|
|
76
82
|
"available": "156.0.8078.48", // 可升级到的版本,无更新时 ""
|
|
77
83
|
"publisher": "Google LLC",
|
|
78
|
-
"source": "winget", // winget | msstore | registry | homebrew
|
|
84
|
+
"source": "winget", // winget | msstore | registry | homebrew | appstore | app
|
|
79
85
|
"installLocation": "C:\\Program Files\\Google\\Chrome\\Application",
|
|
80
86
|
"hasUpdate": true,
|
|
81
87
|
"updateSupported": true, // 本插件能否驱动更新
|
|
82
|
-
"reason": null, // null | "not-in-catalog" | "unknown-version" | "pinned" | "store-managed" | "system-component"
|
|
83
|
-
"kind": "exe" // exe | msi | msix | inno | cask | null(未知)
|
|
88
|
+
"reason": null, // null | "not-in-catalog" | "not-managed" | "unknown-version" | "pinned" | "store-managed" | "system-component"
|
|
89
|
+
"kind": "exe" // exe | msi | msix | inno | cask | app | null(未知)
|
|
84
90
|
}
|
|
85
91
|
```
|
|
86
92
|
|
|
93
|
+
`reason` 里的 `not-managed` 是 macOS 专用:应用在标准目录里枚举到了,但不是由 Homebrew 管理
|
|
94
|
+
(拖 DMG / 手动安装),所以能列出、不能一键更新。`source` 的 `app` / `appstore` 同理只出现在
|
|
95
|
+
macOS。macOS 的扫描范围是 `/Applications`、`/Applications/Utilities`、`/System/Applications`
|
|
96
|
+
(及其 `Utilities`)与 `~/Applications`;装在别处的应用不在范围内。
|
|
97
|
+
|
|
87
98
|
### POST /dsh-app-updater/scan
|
|
88
99
|
|
|
89
100
|
body `{ "confirm": true, "force": true }` → `{ "ok": true, "started": true }`(异步;结果看 state.json / apps.json)
|
package/README.en.md
CHANGED
|
@@ -88,8 +88,9 @@ State file: `%USERPROFILE%\.dsh\dsh-app-updater\state.json`
|
|
|
88
88
|
|
|
89
89
|
| Key | Default | Meaning |
|
|
90
90
|
|---|---|---|
|
|
91
|
-
| `includeStoreApps` | `true` | List
|
|
92
|
-
| `includeRegistryOnly` | `true` |
|
|
91
|
+
| `includeStoreApps` | `true` | List store apps too (MSIX on Windows, App Store on macOS; display only). |
|
|
92
|
+
| `includeRegistryOnly` | `true` | Windows: fill in apps missing from the winget catalog using the registry (display only). |
|
|
93
|
+
| `includeUnmanagedApps` | `true` | macOS: also list apps that Homebrew does not manage (drag-to-Applications installs; display only). |
|
|
93
94
|
| `downloadDir` | empty | Custom download folder; empty = `%USERPROFILE%\Downloads\DSH App Updater`. Must be absolute. |
|
|
94
95
|
| `keepInstallers` | `true` | Keep the installer after a successful update. |
|
|
95
96
|
| `hashFallbackUpgrade` | `true` | Fall back to `winget upgrade` when the download fails. |
|
|
@@ -110,8 +111,25 @@ curl -X POST -H "Content-Type: application/json" \
|
|
|
110
111
|
|
|
111
112
|
## Safety boundaries
|
|
112
113
|
|
|
113
|
-
- **Every
|
|
114
|
-
|
|
114
|
+
- **Every route is deny-by-default**: each request first goes through `ctx.connection.admit(request)`
|
|
115
|
+
— DSH's Host/Origin fence plus browser-session / API authentication, the same gate the host uses for
|
|
116
|
+
its own `/api` routes. Rejected requests get `401` / `403` / `409` with `X-DSH-Admission: denied`.
|
|
117
|
+
- **Legacy loopback mode is off by default** (since 1.3.1): when the host has no `connection`
|
|
118
|
+
service, requests are no longer trusted just because they come from loopback — they get `403`.
|
|
119
|
+
If you really need it on an old host, opt in explicitly, either via plugin config:
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
- id: dsh-app-updater
|
|
123
|
+
config:
|
|
124
|
+
allowLegacyLoopback: true
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
or by exporting `DSH_APP_UPDATER_ALLOW_LEGACY_LOOPBACK=1` before starting DSH. Once enabled, allowed
|
|
128
|
+
requests carry `X-DSH-Admission: loopback-legacy-optin` and `X-DSH-Admission-Warning`, the
|
|
129
|
+
`admission` field in `state.json` / `health.json` reports `mode: "loopback-legacy-optin"`, the
|
|
130
|
+
settings page shows a red banner, and a `warn` line is written to the log.
|
|
131
|
+
- **Every write still requires explicit confirmation**: `scan` / `update` / `cancel` /
|
|
132
|
+
`settings` / `open-downloads` require `confirm:true`.
|
|
115
133
|
- **No guessed silent switches**: the publisher's declared `Silent` switches are preferred;
|
|
116
134
|
generic defaults are used only when a manifest is unavailable.
|
|
117
135
|
- **Elevation only for installs, and on demand**: install commands that need administrator rights
|
|
@@ -130,8 +148,10 @@ curl -X POST -H "Content-Type: application/json" \
|
|
|
130
148
|
|
|
131
149
|
- **Windows**: `winget.exe` (Microsoft App Installer). Without winget the plugin can still
|
|
132
150
|
list registry apps, but cannot tell whether they are up to date.
|
|
133
|
-
- **macOS**:
|
|
134
|
-
|
|
151
|
+
- **macOS**: no extra dependency is needed to **list** apps (the plugin reads `.app` bundles from
|
|
152
|
+
`/Applications` and friends); judging and applying updates needs `brew` ([Homebrew](https://brew.sh)).
|
|
153
|
+
Without Homebrew the apps are still listed, with a note that installing Homebrew enables one-click
|
|
154
|
+
cask upgrades.
|
|
135
155
|
- No third-party npm dependencies: the host half uses Node built-ins only, the browser half
|
|
136
156
|
only the host-provided `react`.
|
|
137
157
|
|
|
@@ -142,7 +162,7 @@ The plugin detects its own environment at startup and picks the installer that m
|
|
|
142
162
|
| System | Architecture | Engine | Notes |
|
|
143
163
|
|---|---|---|---|
|
|
144
164
|
| Windows 10 / 11 | x64 / ARM64 / x86 | **winget** | the detected architecture is passed as `winget --architecture`; if the manifest has no installer for it, the plugin falls back to automatic matching and says so in the log |
|
|
145
|
-
| macOS 11+ | Apple Silicon / Intel | **Homebrew casks** | Homebrew
|
|
165
|
+
| macOS 11+ | Apple Silicon / Intel | **Homebrew casks** | apps are enumerated from `/Applications` and friends, then matched against the cask catalog; Homebrew resolves the right cask for the machine. Without Homebrew the apps are still listed, they just cannot be upgraded in one click |
|
|
146
166
|
| anything else | — | none | the scan/update routes answer 400 with the reason |
|
|
147
167
|
|
|
148
168
|
The result is shown in the status line and in the settings panel's “运行环境 / Environment” row, e.g.
|
|
@@ -152,9 +172,13 @@ The result is shown in the status line and in the settings panel's “运行环
|
|
|
152
172
|
The “按本机架构安装包 / match system architecture” switch (on by default) can be turned off to hand
|
|
153
173
|
the choice back to winget; Homebrew always resolves for the local architecture.
|
|
154
174
|
|
|
155
|
-
> The macOS
|
|
156
|
-
>
|
|
157
|
-
>
|
|
175
|
+
> The macOS app enumeration (`/Applications` + `Info.plist`) and cask matching are covered by unit
|
|
176
|
+
> tests and were validated against the real `formulae.brew.sh` catalog (7,800+ casks), but the
|
|
177
|
+
> macOS **install/update execution** has not been verified on a real Mac — this plugin is developed
|
|
178
|
+
> on Windows. Please open an issue if something misbehaves there.
|
|
179
|
+
>
|
|
180
|
+
> Only the standard application directories are enumerated. Apps installed elsewhere (a custom
|
|
181
|
+
> folder, the Desktop, an external volume) are outside the scan scope by design.
|
|
158
182
|
|
|
159
183
|
## HTTP API (host half)
|
|
160
184
|
|
|
@@ -191,8 +215,9 @@ Then add `dsh-app-updater` to `dsh.profile.bundles` in the profile's `package.js
|
|
|
191
215
|
## Tests
|
|
192
216
|
|
|
193
217
|
```bash
|
|
194
|
-
node test/unit.mjs #
|
|
195
|
-
node test/render.mjs #
|
|
218
|
+
node test/unit.mjs # 152 checks: table parsing, version compare, manifest & switches, OS/arch detection, Homebrew parsing, macOS app enumeration & cask matching
|
|
219
|
+
node test/render.mjs # 44 checks: settings panel (per platform), rehearsal banner, column drag, request bodies
|
|
220
|
+
node test/deny.mjs # 12 checks: deny-by-default per route when the host has no connection service
|
|
196
221
|
node test/harness.mjs --scan # boots the host half against a fake cordis ctx + real HTTP
|
|
197
222
|
```
|
|
198
223
|
|
package/README.md
CHANGED
|
@@ -93,9 +93,10 @@ dsh plugin --profile web add https://github.com/19952730389/dsh-app-updater/rele
|
|
|
93
93
|
|
|
94
94
|
| 键 | 默认 | 说明 |
|
|
95
95
|
|---|---|---|
|
|
96
|
-
| `includeStoreApps` | `true` |
|
|
97
|
-
| `includeRegistryOnly` | `true` |
|
|
98
|
-
| `
|
|
96
|
+
| `includeStoreApps` | `true` | 是否把商店应用列进来(Windows 是 MSIX、macOS 是 App Store 应用;只展示,不会被更新)。 |
|
|
97
|
+
| `includeRegistryOnly` | `true` | Windows:是否用注册表补齐 winget 未收录的应用(只展示,不能更新)。 |
|
|
98
|
+
| `includeUnmanagedApps` | `true` | macOS:是否把非 Homebrew 管理(拖 DMG / 手动装)的应用也列出来(只展示,不能更新)。 |
|
|
99
|
+
| `downloadDir` | 空 | 自定义安装包下载目录;空 = `%USERPROFILE%\Downloads\DSH App Updater`(macOS 为 `~/Downloads/DSH App Updater`)。 |
|
|
99
100
|
| `keepInstallers` | `true` | 更新后保留安装包;`false` 则在安装完成后删掉该应用刚下载的包。 |
|
|
100
101
|
| `hashFallbackUpgrade` | `true` | 下载失败时退回 `winget upgrade` 兜底。 |
|
|
101
102
|
| `dryRun` | `false` | **演练模式**:只下载官网全量安装包、不执行安装。用来先看会装什么。 |
|
|
@@ -113,14 +114,32 @@ curl -X POST -H "Content-Type: application/json" \
|
|
|
113
114
|
`downloadDir` 会被校验:必须留空(用默认目录)或是绝对路径(`D:\Apps`)/UNC 路径(`\\server\share`),
|
|
114
115
|
否则返回 `400` 并在面板里显示红字提示,不会把安装包写到意料之外的位置。
|
|
115
116
|
|
|
117
|
+
> 这些路由**默认拒绝**:请求必须通过宿主的 `connection` 准入(浏览器里就是带上 DSH 页面的会话
|
|
118
|
+
> Cookie),或在**没有 `connection` 服务的旧宿主**上由你显式开启兼容模式(见「安全边界」)。
|
|
119
|
+
> 仅凭「来自 127.0.0.1」不再放行 —— 1.3.1 已把这条路径改成默认拒绝。
|
|
120
|
+
|
|
116
121
|
---
|
|
117
122
|
|
|
118
123
|
## 安全边界
|
|
119
124
|
|
|
120
|
-
-
|
|
125
|
+
- **路由默认拒绝**:每个请求先过 `ctx.connection.admit(request)` —— DSH 用来做
|
|
121
126
|
Host/Origin 信任校验与浏览器会话 / API 认证的那套边界(宿主自己的 `/api` 路由用的就是它),
|
|
122
|
-
未通过的请求直接得到 `401
|
|
123
|
-
|
|
127
|
+
未通过的请求直接得到 `401` / `403` / `409`,响应头带 `X-DSH-Admission: denied`。
|
|
128
|
+
- **兼容模式默认关闭**(1.3.1 起):宿主没有 `connection` 服务时**不再**「仅凭回环地址放行」,
|
|
129
|
+
一律 `403`。旧宿主上确实需要时,必须由你**显式**开启,二选一:
|
|
130
|
+
- 插件配置(`cordis.patch.yml` 里覆盖同名条目):
|
|
131
|
+
|
|
132
|
+
```yaml
|
|
133
|
+
- id: dsh-app-updater
|
|
134
|
+
config:
|
|
135
|
+
allowLegacyLoopback: true
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- 或启动 DSH 前设置环境变量 `DSH_APP_UPDATER_ALLOW_LEGACY_LOOPBACK=1`。
|
|
139
|
+
|
|
140
|
+
开启后:放行的回环请求带 `X-DSH-Admission: loopback-legacy-optin` 与
|
|
141
|
+
`X-DSH-Admission-Warning`,`state.json` / `health.json` 的 `admission` 字段会标明
|
|
142
|
+
`mode: "loopback-legacy-optin"`,设置页顶部也会出现红色提示条;日志里记一条 `warn`。
|
|
124
143
|
- **写操作还要显式确认**:`scan` / `update` / `cancel` / `settings` / `open-downloads` 必须带 `confirm:true`。
|
|
125
144
|
- **不猜测静默参数**:优先用发布方在清单里声明的 `Silent` 参数;清单缺失时才按安装包类型用保守的默认开关。
|
|
126
145
|
- **需要管理员时自动提权**:机器级安装包(MSI、写入 `Program Files`)交给
|
|
@@ -136,7 +155,7 @@ curl -X POST -H "Content-Type: application/json" \
|
|
|
136
155
|
## 依赖
|
|
137
156
|
|
|
138
157
|
- **Windows**:`winget.exe`(Microsoft「应用安装程序 / App Installer」)。没装 winget 时插件仍能列出注册表里的应用,但无法判断是否有新版本。
|
|
139
|
-
- **macOS
|
|
158
|
+
- **macOS**:无需额外依赖即可**列出**应用(直接读 `/Applications` 等目录里的 `.app`);要判断并执行更新需要 `brew`([Homebrew](https://brew.sh))。没装 Homebrew 时会照常列出应用,并说明「安装 Homebrew 后可一键升级 cask 应用」。
|
|
140
159
|
- 不需要任何第三方 npm 依赖;宿主半身只用 Node 内置模块,浏览器半身只用宿主提供的 `react`。
|
|
141
160
|
|
|
142
161
|
## 接口(宿主半身)
|
|
@@ -171,8 +190,9 @@ dsh plugin --profile <profile> add link:/path/to/dsh-app-updater
|
|
|
171
190
|
## 测试
|
|
172
191
|
|
|
173
192
|
```bash
|
|
174
|
-
node test/unit.mjs #
|
|
175
|
-
node test/render.mjs #
|
|
193
|
+
node test/unit.mjs # 152 项:定宽表格解析、版本比较、清单与静默参数、系统/架构识别、Homebrew 解析、macOS 应用枚举与 cask 匹配
|
|
194
|
+
node test/render.mjs # 44 项:设置面板(按平台)、演练提示、列宽拖拽、请求体
|
|
195
|
+
node test/deny.mjs # 12 项:无 connection 服务时逐路由验证默认拒绝
|
|
176
196
|
node test/harness.mjs --scan # 用假 cordis ctx + 真 HTTP 把宿主半身跑起来
|
|
177
197
|
```
|
|
178
198
|
|
|
@@ -183,7 +203,7 @@ node test/harness.mjs --scan # 用假 cordis ctx + 真 HTTP 把宿主半身
|
|
|
183
203
|
| 系统 | 架构 | 更新引擎 | 说明 |
|
|
184
204
|
|---|---|---|---|
|
|
185
205
|
| Windows 10 / 11 | x64 / ARM64 / x86 | **winget** | 把识别到的架构传给 `winget --architecture`;清单没有该架构的包时自动退回自动匹配并记录日志 |
|
|
186
|
-
| macOS 11+ | Apple Silicon / Intel | **Homebrew cask** | Homebrew 自己按本机架构取 cask
|
|
206
|
+
| macOS 11+ | Apple Silicon / Intel | **Homebrew cask** | 应用从 `/Applications` 等目录枚举,再按 cask 目录匹配;Homebrew 自己按本机架构取 cask。没装 Homebrew 时仍能列出应用,只是不能一键更新 |
|
|
187
207
|
| 其它 | — | 无 | 扫描/更新接口返回 400 并说明原因 |
|
|
188
208
|
|
|
189
209
|
识别结果会显示在状态行与设置面板的「运行环境」里,例如
|
|
@@ -201,8 +221,12 @@ UAC 为「始终通知」时每次安装会弹一次确认;把 DSH 本身以
|
|
|
201
221
|
Edge 走它自带的更新服务:winget 目录里的 `Microsoft.Edge` 是企业版 MSI,与本机消费版的
|
|
202
222
|
安装技术不匹配(`0x8A15008E`),winget 会直接拒绝。
|
|
203
223
|
|
|
204
|
-
> macOS
|
|
205
|
-
>
|
|
224
|
+
> macOS 的应用枚举(`/Applications` 等目录 + `Info.plist`)与 cask 匹配逻辑有单测覆盖,
|
|
225
|
+
> 并用真实的 `formulae.brew.sh` 目录(7800+ cask)验证过匹配与去重;
|
|
226
|
+
> 但本插件主要在 Windows 上开发,macOS 上的**安装/更新执行**没有真机验证过,遇到问题请开 Issue。
|
|
227
|
+
>
|
|
228
|
+
> macOS 上只枚举标准应用目录。应用如果装在别处(例如 `~/Desktop` 或自定义目录),
|
|
229
|
+
> 本插件看不到它 —— 这不是漏扫,是扫描范围之外。
|
|
206
230
|
|
|
207
231
|
## 卸载
|
|
208
232
|
|
package/lib/client.js
CHANGED
|
@@ -34,7 +34,9 @@ window.__ModuleLoader__.load({
|
|
|
34
34
|
const MIN_COL_PX = 48;
|
|
35
35
|
const MAX_COL_PX = 640;
|
|
36
36
|
|
|
37
|
-
/* 设置项 →
|
|
37
|
+
/* 设置项 → 中文标签与说明。
|
|
38
|
+
`only` 限定平台:跨平台的开关不写,平台专属的开关只有对应系统才显示,
|
|
39
|
+
避免 macOS 上出现「自动提权安装」这种 Windows 概念。 */
|
|
38
40
|
const TOGGLES = [
|
|
39
41
|
{
|
|
40
42
|
key: "keepInstallers",
|
|
@@ -56,26 +58,49 @@ window.__ModuleLoader__.load({
|
|
|
56
58
|
key: "autoElevate",
|
|
57
59
|
label: "自动提权安装",
|
|
58
60
|
hint: "需要管理员的安装包(MSI、写入 Program Files)改由管理员上下文执行;UAC 开启时每次安装会弹一次确认",
|
|
61
|
+
only: "windows",
|
|
59
62
|
},
|
|
60
63
|
{
|
|
61
64
|
key: "hashFallbackUpgrade",
|
|
62
65
|
label: "下载失败时回退 winget",
|
|
63
66
|
hint: "官网包哈希与目录记录不一致时,改用 winget 安装同源全量包",
|
|
67
|
+
only: "windows",
|
|
64
68
|
},
|
|
65
69
|
{
|
|
66
70
|
key: "includeStoreApps",
|
|
67
71
|
label: "包含应用商店应用",
|
|
68
72
|
hint: "商店 / MSIX 应用只列出,更新仍由 Microsoft Store 负责",
|
|
73
|
+
only: "windows",
|
|
69
74
|
},
|
|
70
75
|
{
|
|
71
76
|
key: "includeRegistryOnly",
|
|
72
77
|
label: "包含目录未收录应用",
|
|
73
78
|
hint: "用注册表补齐 winget 目录未收录的软件(只列出,无法自动更新)",
|
|
79
|
+
only: "windows",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
key: "includeStoreApps",
|
|
83
|
+
label: "包含 App Store 应用",
|
|
84
|
+
hint: "App Store 应用只列出,升级仍由 App Store 负责",
|
|
85
|
+
only: "macos",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
key: "includeUnmanagedApps",
|
|
89
|
+
label: "包含非 Homebrew 应用",
|
|
90
|
+
hint: "把拖 DMG / 手动安装的应用也列出来(只列出,无法自动更新)",
|
|
91
|
+
only: "macos",
|
|
74
92
|
},
|
|
75
93
|
];
|
|
76
94
|
|
|
95
|
+
/** 当前平台该显示的开关(未知平台按 Windows 显示,保持兼容)。 */
|
|
96
|
+
function togglesFor(family) {
|
|
97
|
+
const fam = family === "macos" ? "macos" : "windows";
|
|
98
|
+
return TOGGLES.filter((t) => !t.only || t.only === fam);
|
|
99
|
+
}
|
|
100
|
+
|
|
77
101
|
const REASON_TEXT = {
|
|
78
102
|
"not-in-catalog": "软件目录未收录",
|
|
103
|
+
"not-managed": "非 Homebrew 管理",
|
|
79
104
|
"unknown-version": "版本未知",
|
|
80
105
|
"system-component": "系统组件",
|
|
81
106
|
pinned: "版本被固定",
|
|
@@ -87,6 +112,8 @@ window.__ModuleLoader__.load({
|
|
|
87
112
|
msstore: "msstore",
|
|
88
113
|
registry: "registry",
|
|
89
114
|
homebrew: "homebrew",
|
|
115
|
+
appstore: "App Store",
|
|
116
|
+
app: "应用",
|
|
90
117
|
};
|
|
91
118
|
|
|
92
119
|
/** 引擎显示名 */
|
|
@@ -170,6 +197,8 @@ window.__ModuleLoader__.load({
|
|
|
170
197
|
".appupd-badge-msstore{border-color:rgba(52,211,153,.55);color:#34d399;}",
|
|
171
198
|
".appupd-badge-registry{border-color:rgba(148,163,184,.5);}",
|
|
172
199
|
".appupd-badge-homebrew{border-color:rgba(251,191,36,.55);color:#fbbf24;}",
|
|
200
|
+
".appupd-badge-appstore{border-color:rgba(52,211,153,.55);color:#34d399;}",
|
|
201
|
+
".appupd-badge-app{border-color:rgba(148,163,184,.5);}",
|
|
173
202
|
".appupd-envline{font-size:12px;color:var(--dsw-alias-label-primary,#f3f4f6);word-break:break-word;}",
|
|
174
203
|
".appupd-state-new{font-weight:600;color:var(--dsw-alias-brand-primary,#4f8cff);white-space:nowrap;}",
|
|
175
204
|
".appupd-state-ok{color:#34d399;white-space:nowrap;}",
|
|
@@ -200,6 +229,8 @@ window.__ModuleLoader__.load({
|
|
|
200
229
|
|
|
201
230
|
/* 演练模式警示条 */
|
|
202
231
|
".appupd-banner{display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:8px;border:1px solid rgba(251,191,36,.5);background:rgba(251,191,36,.12);color:#fbbf24;font-size:12px;}",
|
|
232
|
+
/* 兼容模式(仅回环放行)警示条:默认关闭,开启时用红色提醒 */
|
|
233
|
+
".appupd-banner-warn{border-color:rgba(239,68,68,.55);background:rgba(239,68,68,.12);color:#f87171;}",
|
|
203
234
|
|
|
204
235
|
/* 插件设置面板 */
|
|
205
236
|
".appupd-panel{border:1px solid var(--dsw-alias-border-l1,#4b5563);border-radius:8px;overflow:hidden;}",
|
|
@@ -632,6 +663,8 @@ window.__ModuleLoader__.load({
|
|
|
632
663
|
(state && state.envSummary) ||
|
|
633
664
|
(env ? [env.osName || env.platform, env.archLabel].filter(Boolean).join(" · ") : "环境未知");
|
|
634
665
|
const engineText = !state ? "" : engineTextOf(engine);
|
|
666
|
+
/* 平台族:设置面板与空状态文案都要用,必须在下面的组件定义之前算出来 */
|
|
667
|
+
const family = (env && env.family) || (platform === "darwin" ? "macos" : "windows");
|
|
635
668
|
|
|
636
669
|
const filtered = react.useMemo(() => {
|
|
637
670
|
if (!items) return [];
|
|
@@ -907,7 +940,17 @@ window.__ModuleLoader__.load({
|
|
|
907
940
|
"还没有检测过,点右上角『刷新检测』开始"
|
|
908
941
|
);
|
|
909
942
|
}
|
|
910
|
-
if (!items.length)
|
|
943
|
+
if (!items.length) {
|
|
944
|
+
// 分平台给出可执行的下一步,而不是一句「未检测到任何已安装应用」
|
|
945
|
+
return h(
|
|
946
|
+
"div",
|
|
947
|
+
{ className: "appupd-empty" },
|
|
948
|
+
family === "macos"
|
|
949
|
+
? "未在应用目录里找到应用。若确认本机有应用,请看看下面的运行日志里「枚举应用目录」的记录;" +
|
|
950
|
+
"应用装在非标准位置时本插件看不到它。"
|
|
951
|
+
: "未检测到任何已安装应用"
|
|
952
|
+
);
|
|
953
|
+
}
|
|
911
954
|
return h("div", { className: "appupd-empty" }, "没有匹配的应用");
|
|
912
955
|
};
|
|
913
956
|
|
|
@@ -917,7 +960,10 @@ window.__ModuleLoader__.load({
|
|
|
917
960
|
|
|
918
961
|
const settings = (state && state.settings) || {};
|
|
919
962
|
const dryRunOn = Boolean(settings.dryRun);
|
|
920
|
-
const
|
|
963
|
+
const visibleToggles = togglesFor(family);
|
|
964
|
+
const effectiveDir =
|
|
965
|
+
(state && state.downloadDir) ||
|
|
966
|
+
(family === "macos" ? "~/Downloads/DSH App Updater" : "%USERPROFILE%\\Downloads\\DSH App Updater");
|
|
921
967
|
|
|
922
968
|
const dryRunBanner = dryRunOn
|
|
923
969
|
? h(
|
|
@@ -927,6 +973,21 @@ window.__ModuleLoader__.load({
|
|
|
927
973
|
)
|
|
928
974
|
: null;
|
|
929
975
|
|
|
976
|
+
/* 兼容模式:只有用户显式开启(插件配置 / 环境变量)才会出现,默认不显示 */
|
|
977
|
+
const admission = (state && state.admission) || null;
|
|
978
|
+
const legacyOn = Boolean(admission && admission.allowLegacyLoopback);
|
|
979
|
+
const legacyBanner = legacyOn
|
|
980
|
+
? h(
|
|
981
|
+
"div",
|
|
982
|
+
{ className: "appupd-banner appupd-banner-warn", role: "alert" },
|
|
983
|
+
h(
|
|
984
|
+
"span",
|
|
985
|
+
null,
|
|
986
|
+
"兼容模式已开启:没有连接凭据的本机回环请求会被放行(旧宿主没有 connection 准入时才会用到)。非必要请关闭,见 README 的「安全」一节。"
|
|
987
|
+
)
|
|
988
|
+
)
|
|
989
|
+
: null;
|
|
990
|
+
|
|
930
991
|
const switchEl = (key) => {
|
|
931
992
|
const on = Boolean(settings[key]);
|
|
932
993
|
return h("button", {
|
|
@@ -1026,7 +1087,7 @@ window.__ModuleLoader__.load({
|
|
|
1026
1087
|
)
|
|
1027
1088
|
)
|
|
1028
1089
|
),
|
|
1029
|
-
...
|
|
1090
|
+
...visibleToggles.map((t) =>
|
|
1030
1091
|
h(
|
|
1031
1092
|
"div",
|
|
1032
1093
|
{ className: "appupd-field", key: t.key },
|
|
@@ -1195,6 +1256,9 @@ window.__ModuleLoader__.load({
|
|
|
1195
1256
|
/* 3. 演练模式提示 */
|
|
1196
1257
|
dryRunBanner,
|
|
1197
1258
|
|
|
1259
|
+
/* 3b. 兼容模式(仅回环放行)提示:默认不出现 */
|
|
1260
|
+
legacyBanner,
|
|
1261
|
+
|
|
1198
1262
|
/* 4. 进度 */
|
|
1199
1263
|
progress,
|
|
1200
1264
|
|