ygc-dsh-plugin-autoupdate 0.9.4 → 0.9.6

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 CHANGED
@@ -197,12 +197,16 @@ Electron 的 main / gpu-process / renderer / utility,以及以 `ELECTRON_RUN_A
197
197
 
198
198
  `profile/package.json` 里 `dsh.profile.bundles` 列出的就是插件全集,再按 `dependencies` 的来源分三类:
199
199
 
200
- | 来源写法 | 类别 | 是否参与更新 |
201
- |---|---|---|
202
- | `1.2.3` / `^1.2.3` | `registry` | 是 |
203
- | `file:` / `link:` / `workspace:` | `local` | 否(没有「发布」概念) |
204
- | `git+` / `github:` / `http(s)://` | `git` | 否 |
205
- | 不在 `dependencies` 里(随 DSH 安装提供) | `installation` | 否(版本跟随宿主) |
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 本体插件」一节。
206
210
 
207
211
  ## HTTP API
208
212
 
@@ -274,17 +278,19 @@ Electron 的 main / gpu-process / renderer / utility,以及以 `ELECTRON_RUN_A
274
278
  ```
275
279
  D:\Ai工作空间\插件库\ ← 共享文件夹,两台机器都能读写
276
280
  ├─ <本机标识>\ ← 我的格子,例 DSH-7e6b1bb7e052
277
- │ ├─ PLUGINS.json 我的插件清单(含副本指纹表)
281
+ │ ├─ PLUGINS.json 我的插件清单(**只有清单,没有任何插件文件**)
278
282
  │ ├─ FEATURE.json 我的身份说明
279
283
  │ ├─ OTHERS-PLUGINS.json 我的汇总(本机自用)
280
- │ ├─ OTHERS-PLUGINS.md 同一份的人读版
281
- │ └─ local\ 本地 / Git 来源插件的 .tgz 副本
284
+ │ └─ OTHERS-PLUGINS.md 同一份的人读版
282
285
  └─ <别人的标识>\ ← 别人写,我只读
283
286
  ├─ PLUGINS.json
284
- ├─ FEATURE.json
285
- └─ local\
287
+ └─ FEATURE.json
286
288
  ```
287
289
 
290
+ > **0.9.6 起:特征文件夹只存清单,不存插件副本。** 以前这里还有 `local/` 放 .tgz,
291
+ > 现在没有了,而且会自动清掉老版本留下的 `local/` 与散落 tgz。
292
+ > 安装一律按清单里的「安装坐标」去 registry 联网拉取,不存在本地副本安装这条路。
293
+
288
294
  **没有公共分区。** 早先版本在插件库下分出「特征 / 汇集」两级子目录,那是错的:
289
295
  插件库是多人共享的盘,公共目录等于公共广场,所有人的东西往一处堆,谁的产出分不清,
290
296
  而且「汇集」会成为多方争写的热点。现在改成:
@@ -319,31 +325,45 @@ D:\Ai工作空间\插件库\ ← 共享文件夹,两台机器都能
319
325
  `POST /settings { shareResetHash: true }`——这是唯一的重算入口,其它情况一律沿用旧值。
320
326
 
321
327
 
322
- ### 清单里写什么
328
+ ### 清单里写什么(只有清单,没有文件)
323
329
 
324
- | 插件来源 | 清单里写什么 | 要不要打副本 |
330
+ 每一项只有五个字段:`name` / `version` / `kind` / `installSpec` / `installable`。
331
+
332
+ | 插件来源 | 安装坐标 `installSpec` | 能装吗 |
325
333
  |---|---|---|
326
- | `registry`(能联网更新) | 名称 + 版本 | **不打**,对方自己从 registry 拉 |
327
- | `local`(`file:` 本地路径) | 名称 + 版本 + 来源说明 | **打**,`local/<name>-<version>.tgz` |
328
- | `git`(GitHub 等) | 名称 + 版本 | **打**,同上 |
329
- | `installation`(随 DSH 安装提供) | **不写** | 别人本来就有,也没版本号可装 |
334
+ | `registry`(能联网更新) | `<name>@<version>` | 能,registry 拉 |
335
+ | `git`(GitHub 等) | 原 git 写法(`github:o/r#ref`) | 能,联网拉 |
336
+ | `local`(`file:` 本地路径) | 空 | **不能**:未发布的东西没地方拉 |
337
+ | `installation`(随 DSH 安装提供) | — | **不写进清单**:别人本来就有 |
338
+
339
+ `PLUGINS.json` 里的 `policy` 段把这条规矩写死,别人一看就知道这份清单不含文件:
340
+
341
+ ```json
342
+ {
343
+ "schemaVersion": 2,
344
+ "policy": {
345
+ "manifestOnly": true,
346
+ "storesBundles": false,
347
+ "installVia": "registry",
348
+ "note": "特征文件夹只存清单;安装一律按 installSpec 联网拉取,不使用本地副本。"
349
+ },
350
+ "plugins": [
351
+ { "name": "dsh-tavern", "version": "2.8.1", "kind": "registry",
352
+ "installSpec": "dsh-tavern@2.8.1", "installable": true }
353
+ ]
354
+ }
355
+ ```
356
+
357
+ 历史遗留的 `local/` 与根上散落的 `*.tgz` 会在每轮同步时被删掉(删除记录写进
358
+ `PLUGINS.json` 的 `legacyPurged`,便于排查「我的 local/ 怎么没了」)。
330
359
 
331
- 副本是 npm 标准 tgz(`package/` 前缀),排除 `node_modules`、`.git`、`__pycache__`、`*.bak-*`。
332
- 单包上限 96 MB,单个特征的副本总量上限 512 MB,同名插件只留最近 2 个版本。
333
- `link:` 装的包在 `node_modules` 下是符号链接,打包时按 `realpath` 追到真实目录。
334
360
 
335
- ### 什么时候跑
336
361
 
337
362
  **每次巡检跑一次**——巡检 = 自动更新的那一轮定时检查(间隔见「检查频率」)。
338
363
  顺序是先查更新、再同步共享;更新失败不影响共享同步。面板上也有「立即同步一次」。
339
364
 
340
365
  每次同步都幂等:
341
366
 
342
- - 清单内容(除 `generatedAt` / `lastSyncAt` 这类时间字段)没变 → 不重写文件
343
- - 源目录指纹(文件数 + 最大 mtime + 总字节)没变 → 不重压 `.tgz`,直接复用
344
- - 本机标识第一次同步时固定进 state,之后每轮原样传回,不做任何重算
345
-
346
- 所以几百 MB 的本地插件不会把每轮巡检拖慢。
347
367
 
348
368
  ### 拉别人的清单
349
369
 
@@ -357,28 +377,31 @@ D:\Ai工作空间\插件库\ ← 共享文件夹,两台机器都能
357
377
 
358
378
  合并去重规则:
359
379
 
360
- - 去重键是包名;多本特征都有同一插件时,版本取**更高**的那份(按语义版本比,不看文件时间)
361
- - 副本跟着最高版本走;`features` 里记下它出现在哪几本
362
- - 「本机有没有」按 profile 的 `dsh.profile.bundles` 判断,不看 `node_modules`(那里塞满传递依赖)
380
+ - 去重键是包名;多本特征都有同一插件时,版本与安装坐标都取**更高**的那份(按语义版本比,不看文件时间)
381
+ - `features` 里记下它出现在哪几本;老清单里的 `bundles` 数组一律忽略
363
382
  - 没 `PLUGINS.json` 的目录只记一条提示,不报错
364
383
 
365
- ### 装的时候走哪条路
384
+ ### 装的时候走哪条路(只有一条:联网)
385
+
386
+ | 清单里的 `installSpec` | 用的 spec | 结果 |
387
+ |---|---|---|
388
+ | `<name>@<version>` | 原样 | 从 registry 拉 |
389
+ | `github:o/r#ref` 这类 git 写法 | 原样 | 联网拉 |
390
+ | 空(`local` 来源) | — | **拒绝**,说明「对方未发布到 registry」 |
391
+ | `file:` / `link:` 开头 | — | **拒绝**:只走 registry 联网拉取 |
392
+
393
+ 就算对方是老版本写的清单、里面还带着 `bundles` 数组,也一律忽略:
394
+ 拿别人打包过的旧代码装上,版本号写着新的、行为是老的那份,比装不上更难查。
366
395
 
367
- | 对方特征里有 | 用的 spec |
368
- |---|---|
369
- | `.tgz` 副本 | `file:<对方副本的绝对路径>`(正斜杠) |
370
- | 只有名称(registry 来源) | `<name>@<version>` |
371
- | 既没副本又不是 registry | 拒绝,说明原因 |
372
396
 
373
- 装完自动把包名补进 profile 的 `dsh.profile.bundles`——不补的话插件装下来但不会加载。
374
397
  装之前同样会快照 profile 三个清单文件,出问题用 `/restore` 回滚。**装完必须完全退出 DSH 再重开。**
375
398
 
376
399
  ### 共享相关接口
377
400
 
378
401
  | 方法 | 路径 | 作用 |
379
402
  |---|---|---|
380
- | GET | `/share` | 我的特征现状 + 他人清单(含每项是否已装、有没有副本) |
381
- | POST | `/share/sync` | 立即同步一次(我的清单 + 副本 + 他人汇集) |
403
+ | GET | `/share` | 我的特征现状 + 他人清单(含每项是否已装、能不能装、安装坐标) |
404
+ | POST | `/share/sync` | 立即同步一次(我的清单 + 他人汇集;不打包任何文件) |
382
405
  | POST | `/share/install` | 安装他人清单里的一个插件(body: `{ name }`) |
383
406
 
384
407
  设置项(走 `/settings`):`shareEnabled`、`shareLibraryRoot`、`shareFeatureName`、`shareResetHash`。
@@ -404,10 +427,10 @@ $node = "C:\DeepSeekHarness\resources\runtime\primary-runtime\dependencies\node\
404
427
  | `update-guard.test.mjs` | 只向上更新、来源判定、发布年龄门槛 |
405
428
  | `git-source.test.mjs` | Git 来源的 commit 比较与安装 spec |
406
429
  | `machine.test.mjs` | 本机标识:继承会话同步的 machineId、固定后不漂移、改名不换特征值、卸载会话同步后旧值仍有效、写边界(别人的格 / `../` 逃逸 / 前缀相近) |
407
- | `share.test.mjs` | 特征文件夹:清单分类(registry 只写名 / 本地打副本 / 宿主自带不写)、tgz 结构、同步幂等与副本复用、他人清单合并去重、安装路径选择、特征名防上跳 |
430
+ | `share.test.mjs` | 特征文件夹:清单只写元数据、**目录里不允许出现任何插件文件**、历史 `local/` 被清理、同步幂等、他人清单合并去重(忽略老清单 bundles)、安装只走 registry(有副本也拒绝、`file:` 坐标也拒绝)、特征名防上跳 |
408
431
  | `schedule.test.mjs` | 定时排期:更新与共享同步各自独立开关 |
409
- | `system-plugins.test.mjs` | **系统插件显示契约**(源码级):宿主清单是版本第二来源、版本回落顺序、宿主多出的名字也补进来、`installation` 不进检查、面板单独成组且不给策略/更新/刷新 |
410
- | `system-plugins.behavior.test.mjs` | **系统插件显示行为**(真执行 + 假宿主):版本真的从宿主补出来、缺失 `pluginManager` / 抛错 / 脏数据三种降级都不让 `/state` 失败 |
432
+ | `system-plugins.test.mjs` | **本体插件隐藏契约**(源码级):`isBuiltinPlugin` 判据只有一处定义、listPlugins 出口过滤并缓存隐藏名单、runCheck 不查、snapshot 再兜一层并透出 `builtin.count`、策略/更新接口对本体插件一律 400、共享清单同一判据、面板整组隐藏只留一行说明 |
433
+ | `system-plugins.behavior.test.mjs` | **本体插件隐藏行为**(真执行 + 假宿主 + 假 profile):`@deepseek-ai/*` 一行都不出现、第三方插件照常可写策略、`builtin.count` 数对、`pluginManager` 缺失 / 抛错 / 脏数据三种降级都不让 `/state` 失败 |
411
434
  | `restart-safety.test.mjs` | **重启安全契约**(源码级):不得按映像名杀、必须排除自身、枚举→批量结束→拉起的顺序固定、助手须由 WMI 送出进程树、直接 `/F` 不做 WM_CLOSE、有冷却与锁、助手解释器须优先用自带 node.exe |
412
435
  | `restart-interpreter.test.mjs` | **助手解释器与安全闸门**:`canEnumerateByPath()` 拒绝 node.exe/node、payload 带闸门结果与显式 pid、固定等待 ≤ 1 秒、不再每轮重新枚举、批量 taskkill |
413
436
  | `pidtree.test.mjs` | **进程树解析**(合成进程表):Host 爬到 main、renderer 爬到同一个 main、起点缺失/父进程缺失/链过深时的边界、只在 Electron 里才爬 |
@@ -428,56 +451,100 @@ $env:ELECTRON_RUN_AS_NODE = '1'
428
451
  & "C:\DeepSeekHarness\DeepSeek Harness.exe" "C:\DSH-Work\plugin-autoupdate\pkg\test\_live-pidtree.mjs"
429
452
  ```
430
453
 
431
- ## 系统插件(随 DSH 安装提供的那些)
454
+ ## DSH 本体插件:不显示、不管理(0.9.5)
455
+
456
+ `@deepseek-ai/*` 这一批(`dsh-base`、`dsh-web-app`、`dsh-experimental-*`、`dsh-subagent-*` 等)
457
+ 是**随桌面端一起发布的官方 bundle**。它们不在 `profile/node_modules` 里,而在安装目录
458
+ (`resources/app/dsh/node_modules`),版本跟着桌面端走,装/升/卸都由桌面端负责。
459
+
460
+ **0.9.5 起它们彻底不进这个面板**:不显示、不检查、不更新、不卸载。
461
+
462
+ > 为什么改:本机实测 40 行里 24 行是本体插件(可见行 16),真正的第三方插件被淹掉;
463
+ > 而这些行的策略下拉、「立即更新」、「刷新」本来就是假的(点了必然报错或无意义),
464
+ > 列出来只增加误点。用户要的是「我装的那些插件」,不是「宿主里所有的 bundle」。
465
+
466
+ ### 判据只有一处:`isBuiltinPlugin()`
432
467
 
433
- 面板里的「随 DSH 安装提供(N)」那一组,包括 `@deepseek-ai/dsh-base`、`dsh-web-app`、
434
- `dsh-experimental-*` 等。它们的包在**安装目录**(`resources/app/dsh/node_modules`),
435
- `profile/node_modules` 里没有 —— 所以:
468
+ `lib/core.js` 导出 `BUILTIN_SCOPE = '@deepseek-ai/'` 与 `isBuiltinPlugin(plugin)`,命中任意一条即算本体:
436
469
 
437
- | 数据 | 从哪来 |
470
+ | 判据 | 说明 |
438
471
  |---|---|
439
- | 名字 | profile 的 `dsh.profile.bundles`(`profileBundleNames()`) |
440
- | **版本** | **宿主** `pluginManager.listBundles()` 的 `version` |
441
- | 归属 | 同一接口的 `availability === 'installation'` |
442
- | 可卸载性 | 同一接口的 `removable`(这些恒为 false) |
443
-
444
- > **0.9.3 修的「Dsh 系统插件不在自动更新插件里显示」**:它们其实**一直**在列表里,
445
- > 只是 `version` 读成了 `null`(只查 profile 的 node_modules),面板渲染成「未装」,
446
- > 看着就像没列出来。现在 `listPlugins(host)` 把宿主清单当第二来源合并,
447
- > 版本回落顺序是「profile 里实际装的 → 宿主报的 → null」。
472
+ | 包名以 `@deepseek-ai/` 开头 | 官方 scope |
473
+ | `availability === 'installation'` | 宿主说「随 DSH 安装提供」 |
474
+ | `kind === 'installation'` | 不在 profile `dependencies` 里,没有可查的版本约束 |
475
+
476
+ 面板、检查、共享、卸载四处引用同一个函数 —— 只有一处定义,才不会出现
477
+ 「面板藏了、共享清单里还在」这种自相矛盾。
478
+
479
+ ### 四处生效点
480
+
481
+ | 位置 | 行为 |
482
+ |---|---|
483
+ | `listPlugins()`(`index.js`) | 出口过滤;**被剔除的名字记进 `builtinNamesCache`** |
484
+ | `runCheck()`(`index.js`) | 双重排除 `availability !== 'installation'` 且 `!isBuiltinPlugin`,不发网络请求 |
485
+ | `snapshot()`(`lib/api.js`) | 再兜一层(`deps.listPlugins` 可注入,未必带过滤);透出 `builtin.count` / `builtin.names` |
486
+ | `shareablePlugins()`(`lib/share.js`) | 不进他人的共享清单 |
487
+ | `client.js` | 按 scope / availability / kind 再筛一次,整组不渲染 |
488
+
489
+ ### 面板上还能看到什么
490
+
491
+ 只有一行说明,不给任何控件:
492
+
493
+ ```
494
+ 已隐藏 24 个 DSH 本体插件(@deepseek-ai/ 下的官方包)· 随桌面端更新,不由本插件检查、更新或卸载
495
+ ```
496
+
497
+ 条数取 `snapshot.builtin.count`,它由两个来源并集去重得到:
498
+ `listPlugins()` 记录的名字 + `snapshot()` 自己筛掉的条数。
499
+ 注入的 `listPlugins` 不带过滤时也不会报 0(否则面板会说「什么都没隐藏」)。
500
+
501
+ ### 写接口对本体插件一律拒绝
502
+
503
+ 即使面板不显示,直接打接口也不能得手 —— 改它们的策略或版本只会让宿主状态不一致:
504
+
505
+ ```json
506
+ POST /api/local/plugin-autoupdate/plugin-policy {"name":"@deepseek-ai/dsh-base","policy":"immediate"}
507
+ → 400 {"ok":false,"error":"@deepseek-ai/dsh-base 是 DSH 本体插件,不由本插件管理"}
508
+ ```
509
+
510
+ `POST /update` 同形。
511
+
512
+ ### 历史(别把这两处修复改回去)
513
+
514
+ > **0.9.3** 修过「Dsh 系统插件不在自动更新插件里显示」:它们其实一直在列表里,
515
+ > 只是 `version` 读成了 `null`(只查 profile 的 `node_modules`),渲染成「未装」。
516
+ > 修法是 `listPlugins(host)` 把宿主 `pluginManager.listBundles()` 当第二来源合并,
517
+ > 版本回落顺序「profile 里实际装的 → 宿主报的 → null」,名字集取并集。
518
+ > **这段逻辑仍然在**(`system-plugins.test.mjs` 有断言),只是筛出来的行现在被隐藏了。
519
+ >
520
+ > **0.9.4** 修过两处归类错误:`availability=missing` 的官方包被划进「已装」组,
521
+ > 显示成「随安装 + 未装」的矛盾行;以及 `snapshot()` 读了一个从未赋值的
522
+ > `_removableCache`,导致所有插件都不可卸。
448
523
  >
449
- > 顺带对齐了这些插件的语义:状态写「随安装」而不是「已是最新」(没联网比过就别说这话);
450
- > 不给策略下拉、不给「立即更新」、不给「刷新」—— 它们的版本跟桌面端走,
451
- > 查 registry 只会白打网络。检查阶段也显式跳过 `availability=installation`。
524
+ > **0.9.5** 按用户要求改为整体隐藏。`missing` 但**非官方 scope** 的第三方缺失插件
525
+ > 仍然单列「可选、本机未安装(N)」——那是另一回事,用户可能真想装。
452
526
 
453
527
  宿主清单按 **10 秒 TTL** 缓存在 `hostBundlesCache`,由 `GET /state` 等入口惰性刷新。
454
528
  不在 `apply` 阶段拉:那时别的插件可能还没注册完,`pluginManager` 未必在。
455
529
  `pluginManager` 缺失、`listBundles()` 抛错、或返回脏数据(`null` / 数字 name)都只降级,
456
530
  不会让 `/state` 失败 —— 这条由 `system-plugins.behavior.test.mjs` 逐个场景验证。
457
531
 
458
- ### 五种来源,各自怎么显示(0.9.4 明确了分类)
532
+ ### 五种来源,各自怎么显示
459
533
 
460
534
  宿主清单的 `availability` 决定分组,`spec` 决定来源类型。两者**都**要看,
461
535
  只按其中一个分就会混组:
462
536
 
463
537
  | 来源 | 判据 | 面板位置 | 状态文案 | 能自动更新 | 能卸载 |
464
538
  |---|---|---|---|---|---|
465
- | 随 DSH 安装 | `availability=installation` | 「随 DSH 安装提供(N)」 | 随安装 | 否 | 否 |
466
- | **宿主知道但没装** | `availability=missing` | 「可选、本机未安装(N)」 | **未安装** | 否 | 否(无需) |
539
+ | **DSH 本体** | 官方 scope / `availability=installation` / `kind=installation` | **不显示**(仅一行「已隐藏 N 个」) | — | 否 | 否 |
540
+ | 第三方但宿主知道、机器上没装 | `availability=missing` 且非官方 scope | 「可选、本机未安装(N)」 | **未安装** | 否 | 否(无需) |
467
541
  | npm registry | `kind=registry` | 「已装插件(N)」 | 可更新 / 已是最新 | 是 | 宿主说 removable 才行 |
468
542
  | git 来源 | `kind=git` | 同上 | 比 commit | 是 | 同上 |
469
- | **直链 tar 包** | `kind=local` + `spec` 是 `https://` | 「本地路径 / 直链来源」 | **直链·手动更新** | 否 | 同上 |
470
-
471
- > **0.9.4 修的两处归类错误**(用户报「有两个插件应该是 dsh 系统插件,没有正确归类」):
472
- > `@deepseek-ai/dsh-subagent-claude-code` 与 `-codex` 在宿主清单里是
473
- > `availability: 'missing'`(宿主提供入口、包不在磁盘上),但旧代码
474
- > `managed = availability !== 'installation'` 把它们划进了「已装」组,
475
- > 同时 `kind` 又被算成 `installation`,于是显示成「随安装 + 未装」的矛盾行。
476
- > 现在 `missing` 单列一组,状态写「未安装」,并且 `installed: false` 一并透出。
477
- >
478
- > 顺带把 `dsh-purge-community` 这类**直链 release tar** 标成「直链·手动更新」:
479
- > 它的 `spec` 是 `https://github.com/.../releases/latest/download/xxx.tgz`,
480
- > 没有版本号可比、也没有 ref 可追,说「已是最新」是假的。
543
+ | 直链 tar 包 | `kind=local` + `spec` 是 `https://` | 「本地路径 / 直链来源」 | **直链·手动更新** | 否 | 同上 |
544
+
545
+ > **直链 release tar 为什么单独说**:`spec` 是
546
+ > `https://github.com/.../releases/latest/download/xxx.tgz` 这类,没有版本号可比、
547
+ > 也没有 ref 可追,说「已是最新」是假的。
481
548
 
482
549
  ### 可卸载性:不要再自己算一遍
483
550
 
@@ -497,11 +564,11 @@ $env:ELECTRON_RUN_AS_NODE = '1'
497
564
 
498
565
  - 只**更新**已装插件,不安装新插件;**卸载仅限 profile 自装且宿主判定 removable 的包**(见「卸载插件」)
499
566
  - 只在 `registry` 来源上工作;`local` / `git` / `installation` 来源只显示、不处理
500
- - **系统插件(`availability=installation`)只显示、不检查、不更新、不卸载**:
501
- 版本跟桌面端走,`registry` 上的同名包与它无关
567
+ - **DSH 本体插件(`@deepseek-ai/*` / `availability=installation` / `kind=installation`)不显示、不检查、不更新、不卸载**:
568
+ 版本跟桌面端走,`registry` 上的同名包与它无关;策略/更新接口对它们一律 400
502
569
  - 更新失败只记历史,不自动回滚(避免把用户自己手改的清单吞掉);要回滚用 `/restore`
503
570
  - DSH 关闭期间不检查(进程内定时器的固有限制);重开后会立刻按上游发布时间重新判定
504
- - 共享只**同步清单与副本**,不自动安装别人的插件;安装必须人工点选
571
+ - 共享只**同步清单**(不存任何插件文件),不自动安装别人的插件;安装必须人工点选,且只走 registry 联网拉取
505
572
  - 只写 `插件库\<本机标识>\` 那一格,别人的格子只读(写入口有守卫)
506
573
  - 不在插件库根下留任何公共文件或公共目录
507
574
  - **重启只认显式 pid**:任何「按映像名清场」的写法都是回归,`restart-safety.test.mjs` 会红
package/client.js CHANGED
@@ -179,20 +179,27 @@ function makePanel(React) {
179
179
  );
180
180
  }
181
181
 
182
- const plugins = Array.isArray(snap.plugins) ? snap.plugins : [];
183
- // 三类要分清,不能混:
184
- // 1. 随 DSH 安装提供(availability=installation)—— 版本跟桌面端走
185
- // 2. 知道但没装(availability=missing)—— 宿主清单里有条目,包并不在机器上
186
- // 3. 本 profile 装的 —— 真正参与自动更新的那一批
187
- const systemPlugins = plugins.filter((p) => p.availability === 'installation');
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
+ // 「宿主清单里有、但本机没装」仍然单列:它不是本体插件,用户可能真想装。
188
193
  const missingPlugins = plugins.filter((p) => p.availability === 'missing');
189
- const managed = plugins.filter((p) => p.availability !== 'installation' && p.availability !== 'missing');
194
+ const managed = plugins.filter((p) => p.availability !== 'missing');
190
195
  const registryPlugins = managed.filter((p) => p.kind === 'registry');
191
196
  // git 与 registry 一样参与自动更新,只是比的是 commit 而不是版本号
192
197
  const gitPlugins = managed.filter((p) => p.kind === 'git');
193
198
  const otherPlugins = managed.filter((p) => p.kind !== 'registry' && p.kind !== 'git');
194
199
  const available = plugins.filter((p) => p.updateAvailable);
195
200
  const ready = available.filter((p) => !p.waitMs);
201
+ // 被隐藏的本体插件数量:宿主报了就用宿主的,没报就按本地筛掉的差值兜底。
202
+ const builtinHidden = (snap.builtin && Number(snap.builtin.count)) || allRows.length - plugins.length;
196
203
 
197
204
  const header = h(
198
205
  'div',
@@ -334,7 +341,7 @@ function makePanel(React) {
334
341
  const pluginRow = (plugin) => {
335
342
  const isGit = plugin.kind === 'git';
336
343
  // 三种特殊行,各自的状态文案必须诚实:
337
- // system —— 随 DSH 安装,版本跟桌面端走
344
+ // system —— 随 DSH 安装(本体插件已整组隐藏,这里只作兜底)
338
345
  // missing —— 宿主知道但没装,不是「已是最新」
339
346
  // link —— 直链 tar 包(如 GitHub release),没有版本号可比
340
347
  const isSystem = plugin.availability === 'installation';
@@ -572,18 +579,17 @@ function makePanel(React) {
572
579
  ),
573
580
  )
574
581
  : null,
575
- // 系统插件:必须列出来(用户要能看到自己装了哪些),但要明确标注
576
- // 「随 DSH 安装」——它们的版本跟桌面端走,查 registry 没有意义。
577
- systemPlugins.length
582
+ // DSH 本体插件整组隐藏,只留一行说明:用户问过「我明明装了怎么这里没有」,
583
+ // 所以要能看出「有几个被隐了、为什么隐」,但不占用列表、不给任何控件。
584
+ builtinHidden > 0
578
585
  ? h(
579
586
  'div',
580
587
  { style: { marginTop: 12 } },
581
588
  h(
582
589
  'div',
583
590
  { className: 'pau-row-d' },
584
- `随 DSH 安装提供(${systemPlugins.length})· 版本跟随桌面端,不参与联网检查与卸载`,
591
+ `已隐藏 ${builtinHidden} 个 DSH 本体插件(@deepseek-ai/ 下的官方包)· 随桌面端更新,不由本插件检查、更新或卸载`,
585
592
  ),
586
- systemPlugins.map(pluginRow),
587
593
  )
588
594
  : null,
589
595
  // 「宿主清单里有、本机没装」:既不是系统插件也不是已装插件。
@@ -686,7 +692,7 @@ function makePanel(React) {
686
692
  h(
687
693
  'div',
688
694
  { className: 'pau-card-h' },
689
- h('h3', { className: 'pau-card-t' }, '插件共享(特征文件夹)'),
695
+ h('h3', { className: 'pau-card-t' }, '插件共享(特征文件夹 · 只存清单)'),
690
696
  h(
691
697
  'span',
692
698
  { className: share.enabled ? 'pau-tag pau-tag-ok' : 'pau-tag' },
@@ -696,7 +702,7 @@ function makePanel(React) {
696
702
  h(
697
703
  'p',
698
704
  { className: 'pau-sub' },
699
- '插件库是共享文件夹,一个人一格:我的清单、本地插件副本、我自己的汇总全都写在「插件库\\<我的特征>」这一格里面。能联网更新的插件只写名称,本地/Git 来源的打成 .tgz 副本一起放过去。别人的格子只读,汇总也是各机扫一遍自己算,不放在公共位置。',
705
+ '插件库是共享文件夹,一个人一格:我的插件清单、我自己的汇总都写在「插件库\\<我的特征>」这一格里面,**只存清单,不存任何插件文件**。别人要装,按清单里的安装坐标去 registry 联网拉取,不使用本地副本。别人的格子只读,汇总也是各机扫一遍自己算,不放在公共位置。',
700
706
  ),
701
707
  h(
702
708
  'label',
@@ -760,7 +766,7 @@ function makePanel(React) {
760
766
  { className: 'pau-row-d', style: { marginTop: 6 } },
761
767
  `上次同步:${fmtAgo(share.lastSyncAt)}${
762
768
  share.lastResult
763
- ? ` · 本机 ${share.lastResult.total} 个插件(副本 ${share.lastResult.bundled},${share.lastResult.totalMb} MB) · 他人 ${share.lastResult.others} 个,可装 ${share.lastResult.installable}`
769
+ ? ` · 本机 ${share.lastResult.total} 个插件(可联网装 ${share.lastResult.localInstallable ?? '-'},装不了 ${share.lastResult.localUninstallable ?? '-'}) · 他人 ${share.lastResult.others} 个,可装 ${share.lastResult.installable}`
764
770
  : ''
765
771
  }`,
766
772
  ),
@@ -785,7 +791,7 @@ function makePanel(React) {
785
791
  const counts = (result.mine && result.mine.counts) || {};
786
792
  const others2 = (result.aggregate && result.aggregate.counts) || {};
787
793
  setNote(
788
- `同步完成:本机 ${counts.total || 0} 个插件(副本 ${counts.bundled || 0}),他人 ${others2.othersTotal || 0} 个,可装 ${others2.installable || 0} 个`,
794
+ `同步完成:本机 ${counts.total || 0} 个插件(可联网装 ${counts.installable || 0},装不了 ${counts.uninstallable || 0}),他人 ${others2.othersTotal || 0} 个,可装 ${others2.installable || 0} 个`,
789
795
  );
790
796
  }
791
797
  if (next.share && next.share.detail) setShareDetail(next.share.detail);
@@ -837,16 +843,13 @@ function makePanel(React) {
837
843
  h(
838
844
  'span',
839
845
  {
840
- className:
841
- plugin.bundle || plugin.kind === 'registry'
842
- ? 'pau-tag pau-tag-ok'
843
- : 'pau-tag pau-tag-warn',
846
+ className: plugin.installable ? 'pau-tag pau-tag-ok' : 'pau-tag pau-tag-warn',
844
847
  },
845
- plugin.bundle
846
- ? '含副本,可离线装'
847
- : plugin.kind === 'registry'
848
- ? '需联网拉取'
849
- : '无副本,装不了',
848
+ plugin.installable
849
+ ? plugin.kind === 'git'
850
+ ? 'Git 联网拉取'
851
+ : '联网拉取'
852
+ : '无安装坐标,装不了',
850
853
  ),
851
854
  h('span', { className: 'pau-tag pau-tag-mine' }, plugin.features.join(' / ')),
852
855
  ),
@@ -858,8 +861,10 @@ function makePanel(React) {
858
861
  'button',
859
862
  {
860
863
  className: 'pau-btn pau-btn-sm pau-btn-primary',
861
- disabled: !!busy || (!plugin.bundle && plugin.kind !== 'registry'),
862
- title: plugin.bundle ? `安装对方特征里的副本` : '从 registry 安装该版本',
864
+ disabled: !!busy || !plugin.installable,
865
+ title: plugin.installable
866
+ ? `从 registry 联网安装 ${plugin.installSpec || plugin.name}`
867
+ : '这个插件没有可联网的安装坐标(对方未发布到 registry)',
863
868
  onClick: () =>
864
869
  run(`share-install:${plugin.name}`, async () => {
865
870
  const next = await call('/share/install', { name: plugin.name });
package/index.js CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  installedMeta,
20
20
  installedVersion,
21
21
  installedGitHead,
22
+ isBuiltinPlugin,
22
23
  isNewerVersion,
23
24
  isUpdatableKind,
24
25
  loadState,
@@ -98,6 +99,13 @@ export function apply(ctx, config = {}) {
98
99
  * `profile/node_modules` 里没有它们 —— 只有宿主知道它们的版本。
99
100
  */
100
101
  let hostBundlesCache = Object.freeze({ bundles: [], at: 0, error: '' });
102
+
103
+ /**
104
+ * 被隐藏的 DSH 本体插件名单(同步缓存,给面板显示「已隐藏 N 个」用)。
105
+ * listPlugins() 每次调用都会刷新它。
106
+ */
107
+ let builtinNamesCache = [];
108
+ const builtinNames = () => builtinNamesCache;
101
109
  const HOST_BUNDLES_TTL = 10 * 1000;
102
110
 
103
111
  const readHostManager = () => {
@@ -183,7 +191,7 @@ export function apply(ctx, config = {}) {
183
191
  // 宿主里有、profile bundles 里没有的(例如其他 app 入口),也补进来:
184
192
  // 用户要的是「我装了什么」,不是「profile 声明了什么」。
185
193
  const allNames = [...new Set([...names, ...hostMap.keys()])];
186
- return allNames
194
+ const rows = allNames
187
195
  .map((name) => {
188
196
  const meta = installedMeta(name);
189
197
  const spec = typeof specs[name] === 'string' ? specs[name] : null;
@@ -214,6 +222,18 @@ export function apply(ctx, config = {}) {
214
222
  homepage: meta.homepage || meta.repository || '',
215
223
  };
216
224
  });
225
+
226
+ /**
227
+ * DSH 本体插件(@deepseek-ai/* 与随安装提供的)从这里就剔掉:
228
+ * 不显示、不检查、不更新、不卸载——版本跟着桌面端走,动它们等于拆宿主。
229
+ *
230
+ * 但被隐藏的名字要留一份给面板,否则用户会问「我明明装了怎么这里没有」。
231
+ */
232
+ builtinNamesCache = rows
233
+ .filter((row) => isBuiltinPlugin(row))
234
+ .map((row) => row.name)
235
+ .sort();
236
+ return rows.filter((row) => !isBuiltinPlugin(row));
217
237
  };
218
238
 
219
239
  const registries = () => {
@@ -305,9 +325,14 @@ export function apply(ctx, config = {}) {
305
325
  feature: mine.feature,
306
326
  root: mine.root,
307
327
  total: mine.counts.total,
308
- bundled: mine.counts.bundled,
309
- skipped: mine.counts.skipped,
310
- totalMb: mine.totalMb,
328
+ // 只存清单:bundled / totalMb 恒为 0,保留字段是为了旧面板与旧历史记录不炸
329
+ bundled: 0,
330
+ skipped: 0,
331
+ totalMb: 0,
332
+ // 新口径:本机清单里「能联网装」与「装不了」各多少
333
+ localInstallable: mine.counts.installable,
334
+ localUninstallable: mine.counts.uninstallable,
335
+ legacyPurged: mine.legacyPurged?.removed?.length || 0,
311
336
  others: aggregate.doc.counts.othersTotal,
312
337
  installable: aggregate.doc.counts.installable,
313
338
  sources: aggregate.doc.counts.sources,
@@ -317,7 +342,10 @@ export function apply(ctx, config = {}) {
317
342
  pushHistory(next, {
318
343
  kind: 'share',
319
344
  reason: reason || '定时',
320
- text: `共享同步:本机 ${mine.counts.total} 个插件 / 副本 ${mine.counts.bundled};他人 ${aggregate.doc.counts.othersTotal} 个,可装 ${aggregate.doc.counts.installable}`,
345
+ text:
346
+ `共享同步:本机 ${mine.counts.total} 个插件(可联网装 ${mine.counts.installable},装不了 ${mine.counts.uninstallable});` +
347
+ `他人 ${aggregate.doc.counts.othersTotal} 个,可装 ${aggregate.doc.counts.installable}` +
348
+ (mine.legacyPurged?.removed?.length ? `;清理历史副本 ${mine.legacyPurged.removed.length} 个` : ''),
321
349
  });
322
350
  saveState(next);
323
351
  return { ok: true, mine, aggregate: { counts: aggregate.doc.counts }, root: libraryRoot(config) };
@@ -379,7 +407,9 @@ export function apply(ctx, config = {}) {
379
407
  // 随 DSH 安装提供的(availability=installation)不查:它们的版本跟着桌面端走,
380
408
  // 查 registry 只会得到无关结论,还白白打网络。
381
409
  const plugins = listPlugins(hostBundles()).filter(
382
- (p) => isUpdatableKind(p.kind) && p.version && p.availability !== 'installation',
410
+ // 本体插件已在 listPlugins 里剔掉,这里再挡一次 isBuiltinPlugin:
411
+ // 版本跟着桌面端走,查 registry 只会得到无关结论,还白白打网络。
412
+ (p) => isUpdatableKind(p.kind) && p.version && p.availability !== 'installation' && !isBuiltinPlugin(p),
383
413
  );
384
414
  const { registry, fallbackRegistries } = registries();
385
415
 
@@ -846,6 +876,8 @@ export function apply(ctx, config = {}) {
846
876
  ensureHostBundles: () => refreshHostBundles(false),
847
877
  // 同步读取当前缓存(快照是同步函数)
848
878
  hostBundles,
879
+ // 被隐藏的本体插件名单:面板据此说明「为什么我这里只看到 N 个」
880
+ builtinNames,
849
881
  runCheck: checkAndRecord,
850
882
  applyUpdate,
851
883
  migrateToNpm,
package/lib/api.js CHANGED
@@ -18,10 +18,11 @@ import {
18
18
  saveState,
19
19
  updateKind,
20
20
  isUpdatableKind,
21
+ isBuiltinPlugin,
21
22
  } from './core.js';
22
23
  import { normalizeRegistry, queryLatest } from './registry.js';
23
24
  import { restoreBackup, listBackups } from './backup.js';
24
- import { libraryRoot, myFeatureName } from './share.js';
25
+ import { SHARE_POLICY, libraryRoot, myFeatureName } from './share.js';
25
26
 
26
27
  export const API_PREFIX = '/api/local/plugin-autoupdate';
27
28
 
@@ -52,7 +53,12 @@ function snapshot(deps) {
52
53
  // 同步预取一次(快照本来就是 async 调用方在用),拿不到就整体置灰。
53
54
  const removable = deps._removableCache || null;
54
55
  const host = typeof deps.hostBundles === 'function' ? deps.hostBundles() : undefined;
55
- const plugins = deps.listPlugins(host).map((plugin) => {
56
+ // DSH 本体插件(@deepseek-ai/* 与随安装提供的)不显示、不管理:
57
+ // listPlugins 已经剔过一遍,这里再挡一层——deps.listPlugins 是可注入的,
58
+ // 注入方(测试、其他宿主)未必带过滤,漏一层就会在面板上冒出来。
59
+ const listed = deps.listPlugins(host);
60
+ const rawPlugins = listed.filter((plugin) => !isBuiltinPlugin(plugin));
61
+ const plugins = rawPlugins.map((plugin) => {
56
62
  const policy = policyFor(state, plugin.name);
57
63
  const pending = state.pending[plugin.name];
58
64
  const seen = cached[plugin.name];
@@ -127,6 +133,21 @@ function snapshot(deps) {
127
133
  busy: deps.isBusy(),
128
134
  pendingRestart: needsRestart,
129
135
  plugins,
136
+ // 被隐藏的 DSH 本体插件:面板只显示条数 + 可展开的名字,
137
+ // 既回答「我明明装了怎么这里没有」,又不把它们混进可管理列表。
138
+ //
139
+ // 条数取两个来源的并集:
140
+ // - deps.builtinNames():listPlugins 自己剔除时记下的名字(正常路径);
141
+ // - listed.length - rawPlugins.length:注入的 listPlugins 没剔除时,
142
+ // 在本函数里被筛掉的条数(也不能报 0,否则面板说「什么都没隐藏」)。
143
+ builtin: (() => {
144
+ const recorded = typeof deps.builtinNames === 'function' ? deps.builtinNames() : [];
145
+ const names = [...new Set([
146
+ ...(Array.isArray(recorded) ? recorded : []),
147
+ ...listed.filter((plugin) => isBuiltinPlugin(plugin)).map((plugin) => plugin.name),
148
+ ].filter((n) => typeof n === 'string' && n))].sort();
149
+ return { hidden: true, count: names.length, names: names.slice(0, 200) };
150
+ })(),
130
151
  history: (state.history || []).slice(0, 40),
131
152
  share: {
132
153
  enabled: state.shareEnabled,
@@ -144,6 +165,8 @@ function snapshot(deps) {
144
165
  featureName: state.shareFeatureName,
145
166
  machineHash: state.shareMachineHash,
146
167
  }),
168
+ // 口径声明:特征文件夹只存清单、安装走 registry。面板据此显示,不再提「副本」
169
+ policy: SHARE_POLICY,
147
170
  },
148
171
  limits: {
149
172
  minIntervalMinutes: MIN_INTERVAL_MINUTES,
@@ -210,6 +233,12 @@ export function createApi(deps) {
210
233
  json(res, 400, { ok: false, error: '缺少 name' });
211
234
  return;
212
235
  }
236
+ // 本体插件(@deepseek-ai/* 等)不受理任何写入:面板已不显示它们,
237
+ // 这里挡的是直接打接口的调用方。
238
+ if (isBuiltinPlugin({ name })) {
239
+ json(res, 400, { ok: false, error: `${name} 是 DSH 本体插件,不由本插件管理` });
240
+ return;
241
+ }
213
242
  const state = loadState();
214
243
  if (body.policy === 'inherit') {
215
244
  delete state.perPlugin[name];
@@ -292,6 +321,10 @@ export function createApi(deps) {
292
321
  json(res, 400, { ok: false, error: '缺少 name,或 version / commit' });
293
322
  return;
294
323
  }
324
+ if (isBuiltinPlugin({ name })) {
325
+ json(res, 400, { ok: false, error: `${name} 是 DSH 本体插件,不由本插件管理` });
326
+ return;
327
+ }
295
328
  const state = loadState();
296
329
  const kind = updateKind(name, { publishedNames: state.publishedNames });
297
330
  if (!isUpdatableKind(kind)) {