ygc-dsh-plugin-autoupdate 0.9.1 → 0.9.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/README.md +186 -19
- package/client.js +134 -61
- package/index.js +131 -6
- package/lib/api.js +56 -15
- package/lib/core.js +28 -0
- package/lib/restart-helper.js +155 -80
- package/lib/restart.js +178 -179
- package/lib/share.js +6 -4
- package/package.json +2 -3
- package/lib/restart-boot.js +0 -45
package/README.md
CHANGED
|
@@ -45,18 +45,63 @@
|
|
|
45
45
|
`ctx.get('desktopActions')`),插件直接调它。桌面 shell 自己 `app.relaunch()` 并无确认退出,
|
|
46
46
|
把这次退出当作**预期内**的退出,因此**不会弹「启动失败」恢复框、不写 crash report**。
|
|
47
47
|
|
|
48
|
-
###
|
|
48
|
+
### 通道二:进程树之外的助手(实际生效的那条)
|
|
49
49
|
|
|
50
50
|
拿不到官方通道时才走这条。插件不自杀——它把重启助手**送到调用树之外**,由助手负责:
|
|
51
51
|
|
|
52
52
|
1. 等 `restartDelaySeconds`(默认 0 秒)
|
|
53
|
-
2.
|
|
54
|
-
3.
|
|
55
|
-
仍不领情才 `/T /F`
|
|
53
|
+
2. **按可执行文件路径**枚举所有同程序进程(main / host / renderer / gpu 都在内),排除助手自己
|
|
54
|
+
3. 给 250ms 让正在写盘的日志收尾,然后 **`taskkill /PID <n…> /F` 一次性批量结束**
|
|
56
55
|
4. 用 `process.execPath` 重新拉起,Windows 上带 `--updated`(窗口会自己置前台)
|
|
57
56
|
5. 删掉跨进程锁,下一次重启才能正常排期
|
|
58
57
|
|
|
59
|
-
全程写 `~/.dsh/storages/plugin-autoupdate/restart.log
|
|
58
|
+
全程写 `~/.dsh/storages/plugin-autoupdate/restart.log`。**端到端实测 ≤ 5 秒**
|
|
59
|
+
(`test/restart-speed.e2e.mjs`,三份同程序进程一次清掉)。
|
|
60
|
+
|
|
61
|
+
### 0.9.2 修的「点重启完全没反应」(最直接的那条)
|
|
62
|
+
|
|
63
|
+
用户原话:**「purge 是弹出来重启客户端,我点一下就立刻重启了;你这边我点了完全没反应。」**
|
|
64
|
+
|
|
65
|
+
对照 dsh-purge 的实现找到了两个原因:
|
|
66
|
+
|
|
67
|
+
**原因一:助手顶着应用的名字,被别人的映像名清场打死。**
|
|
68
|
+
|
|
69
|
+
`process.execPath` 在宿主里是 `C:\DeepSeekHarness\DeepSeek Harness.exe`,
|
|
70
|
+
所以 0.9.x 的助手**也叫 `DeepSeek Harness.exe`**。dsh-purge 的 helper 正是按
|
|
71
|
+
`DeepSeek Harness.exe` 枚举后逐个结束的(只排除它自己)——我的助手在那张名单里。
|
|
72
|
+
12:03 那次重启的现场就是证据:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
我的日志: [helper] start; ... self=21860
|
|
76
|
+
← 之后再无任何一行(没有「等待期内未退出」,没有 relaunched)
|
|
77
|
+
purge 日志: relaunched via execPath ← 34 秒后,DSH 是 purge 拉回来的
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
现在助手由安装包自带的
|
|
81
|
+
`resources\runtime\primary-runtime\dependencies\node\bin\node.exe` 跑,
|
|
82
|
+
映像是 `node.exe`:任何按 `DeepSeek Harness.exe` 的清场都碰不到它,
|
|
83
|
+
也不再需要 `ELECTRON_RUN_AS_NODE` 那个开关(只在拿不到 node.exe 的退路上用)。
|
|
84
|
+
|
|
85
|
+
**原因二:流程里有最长 31 秒的沉默,其中一步还会弹模态框。**
|
|
86
|
+
|
|
87
|
+
| 步骤 | 旧行为 | 为什么致命 |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| 等旧 main 退出 | 15 秒 | 用户点完什么都看不到 |
|
|
90
|
+
| 「礼貌退出」 | 8 秒 | `taskkill` 不带 `/F` = 发 WM_CLOSE → 桌面 `before-quit` 会 `preventDefault()` 并弹原生「退出 DeepSeek Harness?当前正在运行的任务将会中断」,**不点就永远不退** |
|
|
91
|
+
| 强杀 | 8 秒 | 到这一步用户早已认定「没反应」 |
|
|
92
|
+
|
|
93
|
+
purge 的助手是直接 terminate,所以「点一下立刻重启」。0.9.2 对齐它:
|
|
94
|
+
不做 WM_CLOSE、批量 `/F`、固定等待合计 ≤ 500ms。
|
|
95
|
+
|
|
96
|
+
**顺带修的两个隐患:**
|
|
97
|
+
|
|
98
|
+
- **按路径枚举必须加闸门。** 纯 node 环境里 `process.execPath` 是 `node.exe`,
|
|
99
|
+
若照旧按路径扫会把机器上所有 node 进程杀光(实测一次带走 4 个,包括测试进程和
|
|
100
|
+
bash 作业运行器)。`canEnumerateByPath()` 只允许「应用自己的 exe」走路径模式,
|
|
101
|
+
node 一律退回显式 pid 名单。
|
|
102
|
+
- **新实例的 stderr 必须落盘。** 原来 `stdio: 'ignore'` 让 `Cannot find module …`
|
|
103
|
+
这类错误彻底消失 —— 日志里只有 `relaunched`,实际上新实例根本没起来。
|
|
104
|
+
现在写到同目录的 `relaunch-stderr.log`。
|
|
60
105
|
|
|
61
106
|
### 0.9.0 修的两条「重启之后 DSH 就没了」(实测复现 + 回归测试)
|
|
62
107
|
|
|
@@ -67,12 +112,12 @@
|
|
|
67
112
|
日志停在「兜底结束旧宿主 pid=…」,`relaunched` 永远不出现。
|
|
68
113
|
实测确认:`taskkill /T` 连 detached 子进程一起杀。
|
|
69
114
|
|
|
70
|
-
|
|
115
|
+
现在把助手送走:
|
|
71
116
|
|
|
72
117
|
| 通道 | 父进程 | 说明 |
|
|
73
118
|
|---|---|---|
|
|
74
119
|
| **WMI** `Win32_Process.Create`(首选) | `WmiPrvSE.exe` | 压根不在调用树里,`/T` 走不到 |
|
|
75
|
-
|
|
|
120
|
+
| **detached spawn**(WMI 不可用时) | 调用方 | 兜底;助手映像是 `node.exe`,不会与 `DeepSeek Harness.exe` 混淆 |
|
|
76
121
|
|
|
77
122
|
**故障二:结束的是宿主(Host),不是 Electron main。**
|
|
78
123
|
|
|
@@ -115,10 +160,13 @@ Electron 的 main / gpu-process / renderer / utility,以及以 `ELECTRON_RUN_A
|
|
|
115
160
|
- 把自己的 renderer 一起打死 → `Desktop renderer exited: launch-failed` 崩溃日志
|
|
116
161
|
- 用户看到的是 **DSH 无预警消失、随后自动重开**,完全不像「更新完成后的正常重启」
|
|
117
162
|
|
|
118
|
-
0.8.0
|
|
119
|
-
|
|
163
|
+
0.8.0 起到现在,助手从不按映像名做事:它按**可执行文件路径**枚举、
|
|
164
|
+
排除自己、一次批量 `/F`。这条边界由 `test/restart-safety.test.mjs`
|
|
120
165
|
以源码契约钉死(`tasklist` 不得出现 `IMAGENAME`、`taskkill` 必须带 `/PID` 且不得带 `/IM`)。
|
|
121
166
|
|
|
167
|
+
0.9.2 额外把「按路径枚举」本身也加了闸门:只有目标是**应用自己的 exe** 时才允许,
|
|
168
|
+
`node.exe` 一律退回显式 pid 名单 —— 否则在纯 node 环境里会把机器上所有 node 进程扫掉。
|
|
169
|
+
|
|
122
170
|
「只在没有会话正在跑的时候重启」默认开着:判定依据是最近 90 秒内有没有会话事件。
|
|
123
171
|
有会话时重启会被推迟,等空闲了再执行。**手动卸载与手动重启不受这条约束。**
|
|
124
172
|
|
|
@@ -149,12 +197,16 @@ Electron 的 main / gpu-process / renderer / utility,以及以 `ELECTRON_RUN_A
|
|
|
149
197
|
|
|
150
198
|
`profile/package.json` 里 `dsh.profile.bundles` 列出的就是插件全集,再按 `dependencies` 的来源分三类:
|
|
151
199
|
|
|
152
|
-
| 来源写法 | 类别 | 是否参与更新 |
|
|
153
|
-
|
|
154
|
-
| `1.2.3` / `^1.2.3` | `registry` | 是 |
|
|
155
|
-
| `file:` / `link:` / `workspace:` | `local` | 否(没有「发布」概念) |
|
|
156
|
-
| `git+` / `github:`
|
|
157
|
-
|
|
|
200
|
+
| 来源写法 | 类别 | 是否参与更新 | 面板 |
|
|
201
|
+
|---|---|---|---|
|
|
202
|
+
| `1.2.3` / `^1.2.3` | `registry` | 是 | 显示 |
|
|
203
|
+
| `file:` / `link:` / `workspace:` | `local` | 否(没有「发布」概念) | 显示 |
|
|
204
|
+
| `git+` / `github:` | `git` | 是(比 commit) | 显示 |
|
|
205
|
+
| `http(s)://…tgz` 直链 | `local` | 否(没有版本号可比) | 显示 |
|
|
206
|
+
| 不在 `dependencies` 里(随 DSH 安装提供) | `installation` | 否(版本跟随宿主) | **不显示** |
|
|
207
|
+
| 包名在 `@deepseek-ai/` 下 | — | 否 | **不显示** |
|
|
208
|
+
|
|
209
|
+
> 后两行是 0.9.5 的行为:DSH 本体插件整体隐藏,判据见下面「DSH 本体插件」一节。
|
|
158
210
|
|
|
159
211
|
## HTTP API
|
|
160
212
|
|
|
@@ -345,7 +397,7 @@ $node = "C:\DeepSeekHarness\resources\runtime\primary-runtime\dependencies\node\
|
|
|
345
397
|
& $node "C:\DSH-Work\plugin-autoupdate\pkg\test\all.mjs"
|
|
346
398
|
```
|
|
347
399
|
|
|
348
|
-
|
|
400
|
+
二十二个套件(含 6 个 e2e):
|
|
349
401
|
|
|
350
402
|
| 套件 | 覆盖 |
|
|
351
403
|
|---|---|
|
|
@@ -358,12 +410,16 @@ $node = "C:\DeepSeekHarness\resources\runtime\primary-runtime\dependencies\node\
|
|
|
358
410
|
| `machine.test.mjs` | 本机标识:继承会话同步的 machineId、固定后不漂移、改名不换特征值、卸载会话同步后旧值仍有效、写边界(别人的格 / `../` 逃逸 / 前缀相近) |
|
|
359
411
|
| `share.test.mjs` | 特征文件夹:清单分类(registry 只写名 / 本地打副本 / 宿主自带不写)、tgz 结构、同步幂等与副本复用、他人清单合并去重、安装路径选择、特征名防上跳 |
|
|
360
412
|
| `schedule.test.mjs` | 定时排期:更新与共享同步各自独立开关 |
|
|
361
|
-
| `
|
|
413
|
+
| `system-plugins.test.mjs` | **本体插件隐藏契约**(源码级):`isBuiltinPlugin` 判据只有一处定义、listPlugins 出口过滤并缓存隐藏名单、runCheck 不查、snapshot 再兜一层并透出 `builtin.count`、策略/更新接口对本体插件一律 400、共享清单同一判据、面板整组隐藏只留一行说明 |
|
|
414
|
+
| `system-plugins.behavior.test.mjs` | **本体插件隐藏行为**(真执行 + 假宿主 + 假 profile):`@deepseek-ai/*` 一行都不出现、第三方插件照常可写策略、`builtin.count` 数对、`pluginManager` 缺失 / 抛错 / 脏数据三种降级都不让 `/state` 失败 |
|
|
415
|
+
| `restart-safety.test.mjs` | **重启安全契约**(源码级):不得按映像名杀、必须排除自身、枚举→批量结束→拉起的顺序固定、助手须由 WMI 送出进程树、直接 `/F` 不做 WM_CLOSE、有冷却与锁、助手解释器须优先用自带 node.exe |
|
|
416
|
+
| `restart-interpreter.test.mjs` | **助手解释器与安全闸门**:`canEnumerateByPath()` 拒绝 node.exe/node、payload 带闸门结果与显式 pid、固定等待 ≤ 1 秒、不再每轮重新枚举、批量 taskkill |
|
|
362
417
|
| `pidtree.test.mjs` | **进程树解析**(合成进程表):Host 爬到 main、renderer 爬到同一个 main、起点缺失/父进程缺失/链过深时的边界、只在 Electron 里才爬 |
|
|
363
|
-
| `restart-helper-behavior.test.mjs` |
|
|
418
|
+
| `restart-helper-behavior.test.mjs` | **助手行为**(真子进程):by-pid 只结束登记的那个、空名单零动作且 <4 秒、by-path 一次收掉多份同程序进程、源码无软退出 |
|
|
364
419
|
| `restart-relaunch.e2e.mjs` | **重启链路**(真进程树):助手父进程不是调用方、整树 `taskkill /T /F` 之后助手仍活着并完成 `relaunched` |
|
|
420
|
+
| `restart-speed.e2e.mjs` | **即时重启**(独立 exe 副本):三份同程序进程一次清掉 → 拉起新实例 → 全流程 ≤ 12 秒(实测约 4~5 秒),并断言助手不在自己的击杀名单里 |
|
|
365
421
|
| `restart-env.e2e.mjs` | **环境传递**:`DSH_PROFILE` / `DSH_HOME` / `PATH` 传到新实例、白名单外的变量不落盘、新实例不带 `ELECTRON_RUN_AS_NODE` |
|
|
366
|
-
| `restart-electron-env.e2e.mjs` |
|
|
422
|
+
| `restart-electron-env.e2e.mjs` | **退路通道**(真 Electron exe):拿不到 node.exe 时,Electron + `ELECTRON_RUN_AS_NODE` 仍能跑助手 |
|
|
367
423
|
| `restart-electron-env.negative.mjs` | **负向对照**:故意不带 `ELECTRON_RUN_AS_NODE` 时脚本不执行,证明上一条断言不是空的 |
|
|
368
424
|
| `uninstall.test.mjs` | **卸载契约**:未确认不出手、`installation`/不可移除的拒绝、拒绝卸自己与管理器、走 `removeBundle`、失败写 `uninstall-failed`、成功后请求重启;外加前端按钮 / 二次确认 / `canUninstall` 契约 |
|
|
369
425
|
| `client-smoke.test.mjs` | 在 Node 里用假 `document` / React 求值 `client.js`,验证插槽注册、locale 注册、面板首渲染 |
|
|
@@ -376,10 +432,121 @@ $env:ELECTRON_RUN_AS_NODE = '1'
|
|
|
376
432
|
& "C:\DeepSeekHarness\DeepSeek Harness.exe" "C:\DSH-Work\plugin-autoupdate\pkg\test\_live-pidtree.mjs"
|
|
377
433
|
```
|
|
378
434
|
|
|
435
|
+
## DSH 本体插件:不显示、不管理(0.9.5)
|
|
436
|
+
|
|
437
|
+
`@deepseek-ai/*` 这一批(`dsh-base`、`dsh-web-app`、`dsh-experimental-*`、`dsh-subagent-*` 等)
|
|
438
|
+
是**随桌面端一起发布的官方 bundle**。它们不在 `profile/node_modules` 里,而在安装目录
|
|
439
|
+
(`resources/app/dsh/node_modules`),版本跟着桌面端走,装/升/卸都由桌面端负责。
|
|
440
|
+
|
|
441
|
+
**0.9.5 起它们彻底不进这个面板**:不显示、不检查、不更新、不卸载。
|
|
442
|
+
|
|
443
|
+
> 为什么改:本机实测 40 行里 24 行是本体插件(可见行 16),真正的第三方插件被淹掉;
|
|
444
|
+
> 而这些行的策略下拉、「立即更新」、「刷新」本来就是假的(点了必然报错或无意义),
|
|
445
|
+
> 列出来只增加误点。用户要的是「我装的那些插件」,不是「宿主里所有的 bundle」。
|
|
446
|
+
|
|
447
|
+
### 判据只有一处:`isBuiltinPlugin()`
|
|
448
|
+
|
|
449
|
+
`lib/core.js` 导出 `BUILTIN_SCOPE = '@deepseek-ai/'` 与 `isBuiltinPlugin(plugin)`,命中任意一条即算本体:
|
|
450
|
+
|
|
451
|
+
| 判据 | 说明 |
|
|
452
|
+
|---|---|
|
|
453
|
+
| 包名以 `@deepseek-ai/` 开头 | 官方 scope |
|
|
454
|
+
| `availability === 'installation'` | 宿主说「随 DSH 安装提供」 |
|
|
455
|
+
| `kind === 'installation'` | 不在 profile `dependencies` 里,没有可查的版本约束 |
|
|
456
|
+
|
|
457
|
+
面板、检查、共享、卸载四处引用同一个函数 —— 只有一处定义,才不会出现
|
|
458
|
+
「面板藏了、共享清单里还在」这种自相矛盾。
|
|
459
|
+
|
|
460
|
+
### 四处生效点
|
|
461
|
+
|
|
462
|
+
| 位置 | 行为 |
|
|
463
|
+
|---|---|
|
|
464
|
+
| `listPlugins()`(`index.js`) | 出口过滤;**被剔除的名字记进 `builtinNamesCache`** |
|
|
465
|
+
| `runCheck()`(`index.js`) | 双重排除 `availability !== 'installation'` 且 `!isBuiltinPlugin`,不发网络请求 |
|
|
466
|
+
| `snapshot()`(`lib/api.js`) | 再兜一层(`deps.listPlugins` 可注入,未必带过滤);透出 `builtin.count` / `builtin.names` |
|
|
467
|
+
| `shareablePlugins()`(`lib/share.js`) | 不进他人的共享清单 |
|
|
468
|
+
| `client.js` | 按 scope / availability / kind 再筛一次,整组不渲染 |
|
|
469
|
+
|
|
470
|
+
### 面板上还能看到什么
|
|
471
|
+
|
|
472
|
+
只有一行说明,不给任何控件:
|
|
473
|
+
|
|
474
|
+
```
|
|
475
|
+
已隐藏 24 个 DSH 本体插件(@deepseek-ai/ 下的官方包)· 随桌面端更新,不由本插件检查、更新或卸载
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
条数取 `snapshot.builtin.count`,它由两个来源并集去重得到:
|
|
479
|
+
`listPlugins()` 记录的名字 + `snapshot()` 自己筛掉的条数。
|
|
480
|
+
注入的 `listPlugins` 不带过滤时也不会报 0(否则面板会说「什么都没隐藏」)。
|
|
481
|
+
|
|
482
|
+
### 写接口对本体插件一律拒绝
|
|
483
|
+
|
|
484
|
+
即使面板不显示,直接打接口也不能得手 —— 改它们的策略或版本只会让宿主状态不一致:
|
|
485
|
+
|
|
486
|
+
```json
|
|
487
|
+
POST /api/local/plugin-autoupdate/plugin-policy {"name":"@deepseek-ai/dsh-base","policy":"immediate"}
|
|
488
|
+
→ 400 {"ok":false,"error":"@deepseek-ai/dsh-base 是 DSH 本体插件,不由本插件管理"}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
`POST /update` 同形。
|
|
492
|
+
|
|
493
|
+
### 历史(别把这两处修复改回去)
|
|
494
|
+
|
|
495
|
+
> **0.9.3** 修过「Dsh 系统插件不在自动更新插件里显示」:它们其实一直在列表里,
|
|
496
|
+
> 只是 `version` 读成了 `null`(只查 profile 的 `node_modules`),渲染成「未装」。
|
|
497
|
+
> 修法是 `listPlugins(host)` 把宿主 `pluginManager.listBundles()` 当第二来源合并,
|
|
498
|
+
> 版本回落顺序「profile 里实际装的 → 宿主报的 → null」,名字集取并集。
|
|
499
|
+
> **这段逻辑仍然在**(`system-plugins.test.mjs` 有断言),只是筛出来的行现在被隐藏了。
|
|
500
|
+
>
|
|
501
|
+
> **0.9.4** 修过两处归类错误:`availability=missing` 的官方包被划进「已装」组,
|
|
502
|
+
> 显示成「随安装 + 未装」的矛盾行;以及 `snapshot()` 读了一个从未赋值的
|
|
503
|
+
> `_removableCache`,导致所有插件都不可卸。
|
|
504
|
+
>
|
|
505
|
+
> **0.9.5** 按用户要求改为整体隐藏。`missing` 但**非官方 scope** 的第三方缺失插件
|
|
506
|
+
> 仍然单列「可选、本机未安装(N)」——那是另一回事,用户可能真想装。
|
|
507
|
+
|
|
508
|
+
宿主清单按 **10 秒 TTL** 缓存在 `hostBundlesCache`,由 `GET /state` 等入口惰性刷新。
|
|
509
|
+
不在 `apply` 阶段拉:那时别的插件可能还没注册完,`pluginManager` 未必在。
|
|
510
|
+
`pluginManager` 缺失、`listBundles()` 抛错、或返回脏数据(`null` / 数字 name)都只降级,
|
|
511
|
+
不会让 `/state` 失败 —— 这条由 `system-plugins.behavior.test.mjs` 逐个场景验证。
|
|
512
|
+
|
|
513
|
+
### 五种来源,各自怎么显示
|
|
514
|
+
|
|
515
|
+
宿主清单的 `availability` 决定分组,`spec` 决定来源类型。两者**都**要看,
|
|
516
|
+
只按其中一个分就会混组:
|
|
517
|
+
|
|
518
|
+
| 来源 | 判据 | 面板位置 | 状态文案 | 能自动更新 | 能卸载 |
|
|
519
|
+
|---|---|---|---|---|---|
|
|
520
|
+
| **DSH 本体** | 官方 scope / `availability=installation` / `kind=installation` | **不显示**(仅一行「已隐藏 N 个」) | — | 否 | 否 |
|
|
521
|
+
| 第三方但宿主知道、机器上没装 | `availability=missing` 且非官方 scope | 「可选、本机未安装(N)」 | **未安装** | 否 | 否(无需) |
|
|
522
|
+
| npm registry | `kind=registry` | 「已装插件(N)」 | 可更新 / 已是最新 | 是 | 宿主说 removable 才行 |
|
|
523
|
+
| git 来源 | `kind=git` | 同上 | 比 commit | 是 | 同上 |
|
|
524
|
+
| 直链 tar 包 | `kind=local` + `spec` 是 `https://` | 「本地路径 / 直链来源」 | **直链·手动更新** | 否 | 同上 |
|
|
525
|
+
|
|
526
|
+
> **直链 release tar 为什么单独说**:`spec` 是
|
|
527
|
+
> `https://github.com/.../releases/latest/download/xxx.tgz` 这类,没有版本号可比、
|
|
528
|
+
> 也没有 ref 可追,说「已是最新」是假的。
|
|
529
|
+
|
|
530
|
+
### 可卸载性:不要再自己算一遍
|
|
531
|
+
|
|
532
|
+
`listPlugins()` 直接把宿主的 `removable` / `readOnlyReason` 带在每行上
|
|
533
|
+
(`canUninstall` / `uninstallReadOnlyReason`),`snapshot()` 只做归一,不另起炉灶。
|
|
534
|
+
|
|
535
|
+
> **0.9.4 修的连带缺陷**:`snapshot()` 原来读 `deps._removableCache`,
|
|
536
|
+
> 而那个字段**从来没有被赋值**(全仓库只有一处读、零处写),
|
|
537
|
+
> 于是每个插件都显示「宿主未提供插件清单,无法卸载」——
|
|
538
|
+
> 连宿主明说 `removable: true` 的 `dsh-purge-community` 都点不动卸载。
|
|
539
|
+
> 另外 `refreshHostBundles()` 里字段名写成了 `removability`,与宿主的 `removable`
|
|
540
|
+
> 对不上,等于又丢了一层。两处一起修,`system-plugins.behavior.test.mjs`
|
|
541
|
+
> 用「宿主 removable=true 必须 canUninstall=true」把它钉死。
|
|
542
|
+
|
|
543
|
+
|
|
379
544
|
## 边界
|
|
380
545
|
|
|
381
546
|
- 只**更新**已装插件,不安装新插件;**卸载仅限 profile 自装且宿主判定 removable 的包**(见「卸载插件」)
|
|
382
547
|
- 只在 `registry` 来源上工作;`local` / `git` / `installation` 来源只显示、不处理
|
|
548
|
+
- **DSH 本体插件(`@deepseek-ai/*` / `availability=installation` / `kind=installation`)不显示、不检查、不更新、不卸载**:
|
|
549
|
+
版本跟桌面端走,`registry` 上的同名包与它无关;策略/更新接口对它们一律 400
|
|
383
550
|
- 更新失败只记历史,不自动回滚(避免把用户自己手改的清单吞掉);要回滚用 `/restore`
|
|
384
551
|
- DSH 关闭期间不检查(进程内定时器的固有限制);重开后会立刻按上游发布时间重新判定
|
|
385
552
|
- 共享只**同步清单与副本**,不自动安装别人的插件;安装必须人工点选
|
package/client.js
CHANGED
|
@@ -179,13 +179,27 @@ function makePanel(React) {
|
|
|
179
179
|
);
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
// DSH 本体插件(@deepseek-ai/* 与随安装提供的)不显示、不管理:
|
|
183
|
+
// 版本跟着桌面端走,装/升/卸都由桌面端负责。宿主侧已经把它们剔掉了,
|
|
184
|
+
// 这里再筛一次,防止老宿主或注入的实现把整张名单原样送过来。
|
|
185
|
+
const isBuiltinRow = (p) =>
|
|
186
|
+
Boolean(p) &&
|
|
187
|
+
(p.availability === 'installation' ||
|
|
188
|
+
p.kind === 'installation' ||
|
|
189
|
+
(typeof p.name === 'string' && p.name.startsWith('@deepseek-ai/')));
|
|
190
|
+
const allRows = Array.isArray(snap.plugins) ? snap.plugins : [];
|
|
191
|
+
const plugins = allRows.filter((p) => !isBuiltinRow(p));
|
|
192
|
+
// 「宿主清单里有、但本机没装」仍然单列:它不是本体插件,用户可能真想装。
|
|
193
|
+
const missingPlugins = plugins.filter((p) => p.availability === 'missing');
|
|
194
|
+
const managed = plugins.filter((p) => p.availability !== 'missing');
|
|
195
|
+
const registryPlugins = managed.filter((p) => p.kind === 'registry');
|
|
184
196
|
// git 与 registry 一样参与自动更新,只是比的是 commit 而不是版本号
|
|
185
|
-
const gitPlugins =
|
|
186
|
-
const otherPlugins =
|
|
197
|
+
const gitPlugins = managed.filter((p) => p.kind === 'git');
|
|
198
|
+
const otherPlugins = managed.filter((p) => p.kind !== 'registry' && p.kind !== 'git');
|
|
187
199
|
const available = plugins.filter((p) => p.updateAvailable);
|
|
188
200
|
const ready = available.filter((p) => !p.waitMs);
|
|
201
|
+
// 被隐藏的本体插件数量:宿主报了就用宿主的,没报就按本地筛掉的差值兜底。
|
|
202
|
+
const builtinHidden = (snap.builtin && Number(snap.builtin.count)) || allRows.length - plugins.length;
|
|
189
203
|
|
|
190
204
|
const header = h(
|
|
191
205
|
'div',
|
|
@@ -326,18 +340,31 @@ function makePanel(React) {
|
|
|
326
340
|
|
|
327
341
|
const pluginRow = (plugin) => {
|
|
328
342
|
const isGit = plugin.kind === 'git';
|
|
343
|
+
// 三种特殊行,各自的状态文案必须诚实:
|
|
344
|
+
// system —— 随 DSH 安装(本体插件已整组隐藏,这里只作兜底)
|
|
345
|
+
// missing —— 宿主知道但没装,不是「已是最新」
|
|
346
|
+
// link —— 直链 tar 包(如 GitHub release),没有版本号可比
|
|
347
|
+
const isSystem = plugin.availability === 'installation';
|
|
348
|
+
const isMissing = plugin.availability === 'missing';
|
|
349
|
+
const isLink = plugin.kind === 'local' && typeof plugin.spec === 'string' && /^https?:\/\//i.test(plugin.spec);
|
|
329
350
|
const tone = plugin.updateAvailable
|
|
330
351
|
? plugin.waitMs
|
|
331
352
|
? 'pau-tag-warn'
|
|
332
353
|
: 'pau-tag-ok'
|
|
333
354
|
: 'pau-tag';
|
|
334
|
-
const stateText =
|
|
335
|
-
?
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
355
|
+
const stateText = isMissing
|
|
356
|
+
? '未安装'
|
|
357
|
+
: isSystem
|
|
358
|
+
? '随安装'
|
|
359
|
+
: isLink
|
|
360
|
+
? '直链·手动更新'
|
|
361
|
+
: plugin.updateAvailable
|
|
362
|
+
? plugin.waitMs
|
|
363
|
+
? `待等 ${plugin.waitText}`
|
|
364
|
+
: '可更新'
|
|
365
|
+
: plugin.latestError
|
|
366
|
+
? '查询失败'
|
|
367
|
+
: '已是最新';
|
|
341
368
|
// git 来源没有版本号可比:当前版本只认 commit,「最新」也只写短 sha
|
|
342
369
|
const currentCommit = plugin.git && plugin.git.commit ? String(plugin.git.commit).slice(0, 10) : '';
|
|
343
370
|
const showLatest = isGit
|
|
@@ -379,22 +406,31 @@ function makePanel(React) {
|
|
|
379
406
|
? h('div', { className: 'pau-row-d' }, plugin.description.slice(0, 160))
|
|
380
407
|
: null,
|
|
381
408
|
),
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
409
|
+
// 系统插件 / 未安装 / 直链来源的策略与「立即更新」都是假的:
|
|
410
|
+
// 一个版本跟桌面端走,一个根本没装,一个没有版本号可比。
|
|
411
|
+
// 与其让用户点了报错,不如把控件换成一句实话。
|
|
412
|
+
isMissing
|
|
413
|
+
? h('span', { className: 'pau-row-d' }, '宿主清单里有、但本机没装')
|
|
414
|
+
: isSystem
|
|
415
|
+
? h('span', { className: 'pau-row-d' }, '随桌面端更新')
|
|
416
|
+
: isLink
|
|
417
|
+
? h('span', { className: 'pau-row-d' }, '直链包,无法按版本号自动更新')
|
|
418
|
+
: h(
|
|
419
|
+
'select',
|
|
420
|
+
{
|
|
421
|
+
className: 'pau-select',
|
|
422
|
+
value: plugin.hasOverride ? plugin.policy : 'inherit',
|
|
423
|
+
onChange: (e) => {
|
|
424
|
+
const value = e.target.value;
|
|
425
|
+
if (value === 'inherit') setPolicy(plugin.name, 'inherit');
|
|
426
|
+
else setPolicy(plugin.name, value, plugin.delayHours);
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
h('option', { value: 'inherit' }, '跟随默认'),
|
|
430
|
+
h('option', { value: 'immediate' }, '立即更新'),
|
|
431
|
+
h('option', { value: 'delayed' }, '延迟更新'),
|
|
432
|
+
),
|
|
433
|
+
!isSystem && !isMissing && !isLink && plugin.policy === 'delayed'
|
|
398
434
|
? h('input', {
|
|
399
435
|
className: 'pau-input pau-input-num',
|
|
400
436
|
type: 'number',
|
|
@@ -406,27 +442,29 @@ function makePanel(React) {
|
|
|
406
442
|
setPolicy(plugin.name, 'delayed', Number(e.target.value)),
|
|
407
443
|
})
|
|
408
444
|
: null,
|
|
409
|
-
plugin.policy === 'delayed' ? h('span', { className: 'pau-row-d' }, '小时') : null,
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
445
|
+
!isSystem && !isMissing && !isLink && plugin.policy === 'delayed' ? h('span', { className: 'pau-row-d' }, '小时') : null,
|
|
446
|
+
isSystem || isMissing || isLink
|
|
447
|
+
? null
|
|
448
|
+
: h(
|
|
449
|
+
'button',
|
|
450
|
+
{
|
|
451
|
+
className: 'pau-btn pau-btn-sm',
|
|
452
|
+
disabled: !!busy || !plugin.updateAvailable,
|
|
453
|
+
title: isGit ? '安装远端最新提交' : '安装最新版本',
|
|
454
|
+
onClick: () =>
|
|
455
|
+
run(`update:${plugin.name}`, async () => {
|
|
456
|
+
const body = isGit
|
|
457
|
+
? { name: plugin.name, commit: plugin.latestCommit || '' }
|
|
458
|
+
: { name: plugin.name, version: plugin.latest };
|
|
459
|
+
const next = await call('/update', body);
|
|
460
|
+
if (next.restart) setNote(next.restart.message || '已安排重启');
|
|
461
|
+
else if (next.result && next.result.ok === false) setNote(next.result.message || '未执行');
|
|
462
|
+
else setNote(`${plugin.name} 已更新`);
|
|
463
|
+
return next;
|
|
464
|
+
}),
|
|
465
|
+
},
|
|
466
|
+
busy === `update:${plugin.name}` ? '更新中…' : '立即更新',
|
|
467
|
+
),
|
|
430
468
|
canMigrate
|
|
431
469
|
? h(
|
|
432
470
|
'button',
|
|
@@ -448,16 +486,18 @@ function makePanel(React) {
|
|
|
448
486
|
busy === `migrate:${plugin.name}` ? '切换中…' : '切换到 npm',
|
|
449
487
|
)
|
|
450
488
|
: null,
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
489
|
+
isSystem || isMissing || isLink
|
|
490
|
+
? null
|
|
491
|
+
: h(
|
|
492
|
+
'button',
|
|
493
|
+
{
|
|
494
|
+
className: 'pau-btn pau-btn-sm',
|
|
495
|
+
disabled: !!busy,
|
|
496
|
+
title: '只查这一个插件',
|
|
497
|
+
onClick: () =>
|
|
498
|
+
run(`query:${plugin.name}`, async () => {
|
|
499
|
+
const one = await call('/query', { name: plugin.name });
|
|
500
|
+
setNote(
|
|
461
501
|
one.error
|
|
462
502
|
? `查询失败:${one.error}`
|
|
463
503
|
: one.updateAvailable
|
|
@@ -468,7 +508,7 @@ function makePanel(React) {
|
|
|
468
508
|
}),
|
|
469
509
|
},
|
|
470
510
|
'刷新',
|
|
471
|
-
|
|
511
|
+
),
|
|
472
512
|
h(
|
|
473
513
|
'button',
|
|
474
514
|
{
|
|
@@ -517,7 +557,13 @@ function makePanel(React) {
|
|
|
517
557
|
h(
|
|
518
558
|
'div',
|
|
519
559
|
{ className: 'pau-card-h' },
|
|
520
|
-
h(
|
|
560
|
+
h(
|
|
561
|
+
'h3',
|
|
562
|
+
{ className: 'pau-card-t' },
|
|
563
|
+
// 只数「真正参与自动更新」的(registry + git)。local / 直链 / 系统 / 未装
|
|
564
|
+
// 各自单列,混进这个数字会让「已装插件(N)」与实际能自动更新的数量不符。
|
|
565
|
+
`已装插件(${registryPlugins.length + gitPlugins.length})`,
|
|
566
|
+
),
|
|
521
567
|
h('span', { className: 'pau-row-d' }, available.length ? `${available.length} 个可更新` : '全部最新'),
|
|
522
568
|
),
|
|
523
569
|
registryPlugins.length + gitPlugins.length
|
|
@@ -527,12 +573,39 @@ function makePanel(React) {
|
|
|
527
573
|
? h(
|
|
528
574
|
'div',
|
|
529
575
|
{ style: { marginTop: 12 } },
|
|
530
|
-
h('div', { className: 'pau-row-d' }, '本地路径 /
|
|
576
|
+
h('div', { className: 'pau-row-d' }, '本地路径 / 直链来源(不参与版本检查)'),
|
|
531
577
|
otherPlugins.map((plugin) =>
|
|
532
578
|
pluginRow(plugin),
|
|
533
579
|
),
|
|
534
580
|
)
|
|
535
581
|
: null,
|
|
582
|
+
// DSH 本体插件整组隐藏,只留一行说明:用户问过「我明明装了怎么这里没有」,
|
|
583
|
+
// 所以要能看出「有几个被隐了、为什么隐」,但不占用列表、不给任何控件。
|
|
584
|
+
builtinHidden > 0
|
|
585
|
+
? h(
|
|
586
|
+
'div',
|
|
587
|
+
{ style: { marginTop: 12 } },
|
|
588
|
+
h(
|
|
589
|
+
'div',
|
|
590
|
+
{ className: 'pau-row-d' },
|
|
591
|
+
`已隐藏 ${builtinHidden} 个 DSH 本体插件(@deepseek-ai/ 下的官方包)· 随桌面端更新,不由本插件检查、更新或卸载`,
|
|
592
|
+
),
|
|
593
|
+
)
|
|
594
|
+
: null,
|
|
595
|
+
// 「宿主清单里有、本机没装」:既不是系统插件也不是已装插件。
|
|
596
|
+
// 混进任何一组都会显示成矛盾状态(随安装 + 未装),所以单列。
|
|
597
|
+
missingPlugins.length
|
|
598
|
+
? h(
|
|
599
|
+
'div',
|
|
600
|
+
{ style: { marginTop: 12 } },
|
|
601
|
+
h(
|
|
602
|
+
'div',
|
|
603
|
+
{ className: 'pau-row-d' },
|
|
604
|
+
`可选、本机未安装(${missingPlugins.length})· 宿主提供了入口但包不在磁盘上`,
|
|
605
|
+
),
|
|
606
|
+
missingPlugins.map(pluginRow),
|
|
607
|
+
)
|
|
608
|
+
: null,
|
|
536
609
|
);
|
|
537
610
|
|
|
538
611
|
const restartCard = h(
|