dsh-mobile-plugin 0.4.0 → 0.4.1

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.
Files changed (95) hide show
  1. package/dsh-mobile-credentials/lib/credentials.js +8 -8
  2. package/host.js +22 -36
  3. package/lib/client.js +29 -82
  4. package/lib/desktop-view.cjs +5 -10
  5. package/lib/git/read.js +6 -8
  6. package/lib/host-compat.cjs +13 -25
  7. package/lib/host-guard.cjs +9 -39
  8. package/lib/host-patches.cjs +5 -16
  9. package/lib/http/routing.js +7 -27
  10. package/lib/impl.js +6 -8
  11. package/lib/mirror/pusher.cjs +33 -85
  12. package/lib/mirror-snapshot.cjs +6 -24
  13. package/lib/mobile/agent-presets.js +6 -26
  14. package/lib/mobile/approvals.js +18 -44
  15. package/lib/mobile/archive-clean.js +10 -19
  16. package/lib/mobile/attachments.js +11 -36
  17. package/lib/mobile/channels.js +15 -41
  18. package/lib/mobile/credential-upgrade.js +7 -16
  19. package/lib/mobile/credentials.js +5 -21
  20. package/lib/mobile/directory-browse.js +8 -27
  21. package/lib/mobile/directory-listing.js +10 -23
  22. package/lib/mobile/drive-roots.js +59 -0
  23. package/lib/mobile/envelope.js +4 -15
  24. package/lib/mobile/events.js +35 -65
  25. package/lib/mobile/extensions.js +5 -8
  26. package/lib/mobile/git-repos.js +6 -15
  27. package/lib/mobile/git.js +11 -19
  28. package/lib/mobile/history-derive.js +23 -41
  29. package/lib/mobile/host-bridge/endpoints.js +4 -6
  30. package/lib/mobile/host-bridge/event-synthesis.js +21 -35
  31. package/lib/mobile/host-bridge/shims.js +12 -22
  32. package/lib/mobile/host-bridge.js +8 -20
  33. package/lib/mobile/host-settings.js +9 -19
  34. package/lib/mobile/inbox-clean.js +7 -24
  35. package/lib/mobile/local-images.js +11 -17
  36. package/lib/mobile/monitor.js +8 -17
  37. package/lib/mobile/pairing.js +7 -19
  38. package/lib/mobile/plugin-self.js +5 -22
  39. package/lib/mobile/plugin-update.js +8 -27
  40. package/lib/mobile/plugins.js +6 -24
  41. package/lib/mobile/questions.js +15 -40
  42. package/lib/mobile/references.js +11 -31
  43. package/lib/mobile/relay-control.js +6 -27
  44. package/lib/mobile/rpc.js +5 -13
  45. package/lib/mobile/schedule.js +8 -33
  46. package/lib/mobile/self-check.cjs +9 -36
  47. package/lib/mobile/session-cache.js +13 -19
  48. package/lib/mobile/session-file.js +8 -21
  49. package/lib/mobile/session-flags.js +7 -13
  50. package/lib/mobile/session-mirror.js +8 -15
  51. package/lib/mobile/session-models.js +3 -3
  52. package/lib/mobile/session-schedule-mark.js +8 -14
  53. package/lib/mobile/session-send.js +13 -21
  54. package/lib/mobile/session-snapshot.js +4 -6
  55. package/lib/mobile/sessions.js +7 -23
  56. package/lib/mobile/stats-collect.cjs +10 -24
  57. package/lib/mobile/stats-ledger.cjs +13 -20
  58. package/lib/mobile/stream.js +12 -23
  59. package/lib/mobile/subagents.js +5 -18
  60. package/lib/mobile/uploads.js +8 -17
  61. package/lib/mobile/workspaces.js +20 -8
  62. package/lib/panel/index.html +7 -7
  63. package/lib/read-receipt.cjs +10 -21
  64. package/lib/relay/lan-network.cjs +14 -28
  65. package/lib/relay/lan-proxy.js +14 -21
  66. package/lib/relay/pairing-policy.js +6 -9
  67. package/lib/relay/preview.js +8 -25
  68. package/lib/relay/protocol.js +6 -13
  69. package/lib/relay/status.js +5 -8
  70. package/lib/relay/tunnel.js +9 -14
  71. package/lib/routes/control-plane.js +28 -45
  72. package/lib/routes/mobile-api.js +19 -32
  73. package/lib/session-cleanup.cjs +16 -27
  74. package/lib/shared/credentials-entry.js +4 -11
  75. package/lib/shared/dshtoken.js +4 -4
  76. package/lib/shared/e2ee-request.js +1 -1
  77. package/lib/shared/e2ee.js +8 -14
  78. package/lib/shared/fresh-request.js +6 -22
  79. package/lib/shared/http-util.js +3 -4
  80. package/lib/shared/list-revision.js +5 -14
  81. package/lib/shared/session-signal.js +7 -16
  82. package/lib/shared/sse-write.js +14 -20
  83. package/lib/shared/workspace-guard.js +7 -15
  84. package/lib/shell/device-ota.js +4 -6
  85. package/lib/shell/manager.js +12 -27
  86. package/lib/shell/ota-package.js +7 -12
  87. package/lib/shell/runtime.js +8 -16
  88. package/lib/shell/self-check-after-swap.js +9 -23
  89. package/lib/shell/updater.js +13 -26
  90. package/lib/sidecar-facade-worker.cjs +20 -33
  91. package/lib/sidecar-facade.cjs +7 -7
  92. package/lib/sidecar-lifecycle.cjs +7 -9
  93. package/lib/update-install.cjs +14 -59
  94. package/lib/update-registry.cjs +6 -18
  95. package/package.json +1 -1
@@ -1,29 +1,16 @@
1
1
  /**
2
- * 更新编排:**查与装都归实现自己**,装完**就地换代**(不重启 DSH)。
2
+ * 更新编排:查与装都归实现自己,装完就地换代(不重启 DSH)。
3
3
  *
4
- * ## 为什么这件事归外壳做,而不是归面板(浏览器)
4
+ * 面板那颗「更新」按钮只覆盖「用户正开着页面点一下」;自动更新的定义是没人看着也要发生,
5
+ * 所以轮询与「装完切换」都必须在宿主进程里做。
5
6
  *
6
- * 面板里那颗「更新」按钮只能覆盖「用户正开着页面点一下」这一种情形;自动更新的定义是
7
- * **没人看着也要发生**。所以轮询和「装完切换」都必须在宿主进程里做,面板只是给它一个开关。
7
+ * 查问 npm 注册表、装在 profile 里跑包管理器(`update-registry.cjs` / `update-install.cjs`)。
8
+ * 外壳只从盘上那一代把自装模块取进来跑,所以换装法不用等重启。下面那套市场实现是兜底,只在
9
+ * 盘上那一代还没有自装模块时走到;市场装完会报 `restartRequired`,但本插件有外壳可以自己
10
+ * 换代,所以那条路里故意不调它的重启。
8
11
  *
9
- * ## 装法住哪儿(2026-10-11 改)
10
- *
11
- * 从前「查」和「装」都是请 DSH 插件市场代劳的(打它的 HTTP 接口)。现在两件都归实现自己:
12
- * 查问 npm 注册表、装在 profile 里跑它自己的包管理器(`lib/update-registry.cjs` /
13
- * `lib/update-install.cjs`)。外壳**只从盘上那一代**把自装模块取进来跑(`installedInstaller`),
14
- * 所以换装法、换判定都不用等重启 —— 盘上换了,下一次自动更新就走新的。
15
- *
16
- * 下面那套市场实现是**兜底**,只在「盘上那一代还没有自装模块」时才走到(老一代、刚回滚到
17
- * 旧代)。自动更新宁可暂时还走老路,也不能直接坏掉;市场装完会报 `restartRequired: true`,
18
- * 而本插件有外壳可以自己换代,所以那条路里**故意不调它的重启**。
19
- *
20
- * 装完新一代是**新目录**(包管理器换链接),外壳照着新 realpath 重新 import 一代即可,见
21
- * `lib/shell/manager.js` 与 `lib/shell/runtime.js`。
22
- *
23
- * ## 兜底那条路不做的事
24
- *
25
- * 不下载、不解包、不校验、不写 profile —— 全是市场的职责。它只回答四个问题:有没有新版、
26
- * 请它装、装完了没、盘上现在是什么版本。
12
+ * 兜底那条路不下载、不解包、不校验、不写 profile,只回答「有没有新版、装完了没、盘上是什么
13
+ * 版本」。
27
14
  */
28
15
 
29
16
  import fs from 'node:fs'
@@ -40,7 +27,7 @@ const { dshHome } = require_('../shared/paths.js')
40
27
  /**
41
28
  * 盘上那一代的「自己装」模块(`lib/update-install.cjs`)。
42
29
  *
43
- * 「装」这件事 2026-10-11 起归**实现**自己:在 profile 里跑它自己的包管理器,不再借道 DSH
30
+ * 「装」这件事归实现自己:在 profile 里跑它自己的包管理器,不再借道 DSH
44
31
  * 插件市场。外壳只负责把盘上那一代的模块取进来跑 —— 从**盘上**(不是外壳自己的目录)取,
45
32
  * 是为了「装法改了不用等重启」:市场以外的方式换掉那一代之后,下一次自动更新就走新装法。
46
33
  *
@@ -229,12 +216,12 @@ const TERMINAL = new Set(['succeeded', 'failed', 'cancelled', 'rolled-back'])
229
216
  */
230
217
  export async function installIfNeeded(base, { name = PACKAGE_NAME, force = false, auto = false, onEvent = () => {}, packageManager = () => null, agents = () => null, beforeInstall = null, now = Date.now } = {}) {
231
218
  const before = installedVersion()
232
- // 装归实现自己(2026-10-11 起):盘上那一代里有 `update-install.cjs` 就走它。判定(有没有
219
+ // 装归实现自己:盘上那一代里有 `update-install.cjs` 就走它。判定(有没有
233
220
  // 新版、`link:` 来源不碰、有 agent 在跑就先别换文件)也一并交给它,别在这里抄第二份 ——
234
221
  // 外壳的自动更新与面板那颗按钮必须是同一套判定,否则会出现「手动说有新版、自动说没有」。
235
222
  //
236
- // `auto` 也透下去:自动那一轮跳过「有对话在跑就先别装」那道闸(2026-10-25 用户拍板的取舍,
237
- // 见 `update-install.cjs` 文件头),但没有新版时它照样什么都不做。
223
+ // `auto` 也透下去:自动那一轮跳过「有对话在跑就先别装」那道闸(取舍见
224
+ // `update-install.cjs` 文件头),但没有新版时它照样什么都不做。
238
225
  const selfInstall = await installedInstaller()
239
226
  if (selfInstall) {
240
227
  const outcome = await selfInstall({
@@ -3,19 +3,16 @@
3
3
  /**
4
4
  * sidecar worker:真正持有「中转隧道 + 局域网代理」的那个子进程。
5
5
  *
6
- * 宿主不自己开 WebSocket、也不自己监听局域网端口,而是 fork 出这个 worker,用环回
7
- * HTTP + Bearer 令牌对话(见 lib/sidecar-facade.cjs)。这样隧道崩了、原生模块加载
8
- * 失败都只死一个子进程,宿主仍能如实把状态报给 /api/relay/status。
6
+ * 宿主不自己开 WebSocket、也不自己监听局域网端口,而是 fork 出这个 worker,用环回 HTTP +
7
+ * Bearer 令牌对话。隧道崩了、原生模块加载失败都只死一个子进程,宿主仍能如实回报状态。
9
8
  *
10
9
  * 三条不能忘的语义:
11
- * 1. 状态既要在 `/status` 上按需可查,也要在**变化的那一刻**主动 `process.send` 给宿主
12
- * (publishState):宿主的健康轮询是 10s 一次,只靠轮询的话隧道真断后它最多 10s
13
- * 仍以为连着、继续往一条死隧道推镜像;而「断又连」整段落在两次轮询之间时,宿主
14
- * 连一次「刚连上」都观测不到,那次全量补推就永远不来。
15
- * 2. `/tunnel/send` 先把镜像快照留在本地(localSnapshot)再尝试真发:局域网直连的手机
16
- * 走 snapshotRead 读这份快照,隧道断了也不至于读到一份过期镜像。
17
- * 3. 请求体一律先量后读(readBody + 显式上限):`/configure` 与镜像帧都有字节上限,
18
- * 超限立刻 413,别让一个畸形请求把内存吃光。
10
+ * 1. 状态既要在 `/status` 上可查,也要在变化那一刻主动 `process.send` 给宿主:健康轮询是
11
+ * 10s 一次,只靠轮询的话隧道真断后宿主最多 10s 仍以为连着;「断又连」落在两次轮询之间时
12
+ * 那次全量补推也会漏掉。
13
+ * 2. `/tunnel/send` 先把快照留在本地再尝试真发:局域网直连的手机读本地这份,隧道断了也不
14
+ * 至于读到过期镜像。
15
+ * 3. 请求体一律先量后读:`/configure` 与镜像帧都有字节上限,超限立刻 413。
19
16
  */
20
17
 
21
18
  const http = require('node:http')
@@ -98,15 +95,12 @@ function authorized(req) { return typeof token === 'string' && token.length >= 6
98
95
  class BodyTooLarge extends Error {}
99
96
  /** /tunnel/send 的请求体上限。
100
97
  *
101
- * 这个接口收的是桌面推给移动端的镜像:整份会话列表 + 工作区 + 当前会话的完整消息、
102
- * 统计与模型。实测桌面一次推送就超过 1MB,而中转清洗后真正留下的只有 ~114KB——
103
- * 旧的 1MB 上限是在把有用的东西挡在门外。
98
+ * 这个接口收桌面推给移动端的镜像:整份会话列表、工作区、当前会话的完整消息与统计。桌面一次
99
+ * 推送就超过 1MB,旧的 1MB 上限把有用的东西挡在门外。
104
100
  *
105
- * 上限的依据是**中转那侧的帧上限**,不是「估个够大的数」:
106
- * dsh-mobile-server/lib/config.js 的 MAX_FRAME_BYTES 默认 8MB,一帧超过它,
107
- * 中转会以 1009 'frame too large' 直接关掉隧道(lib/protocol.js → onError)。
108
- * 丢一次镜像可以恢复,隧道被关掉要等重连、期间手机端全瞎——所以宁可拒也不能赌。
109
- * 留 64KB 余量给帧头。推得再大也只是这一次 413,隧道安然无恙。
101
+ * 依据是中转那侧的帧上限(默认 8MB):一帧超过它,中转会以 1009 'frame too large' 直接关掉
102
+ * 隧道。丢一次镜像可以恢复,隧道被关掉要等重连、期间手机端全瞎。所以留 64KB 余量给帧头,
103
+ * 推得再大也只是这一次 413。
110
104
  */
111
105
  const TUNNEL_SEND_MAX_BODY = 8 * 1024 * 1024 - 64 * 1024
112
106
 
@@ -464,22 +458,15 @@ process.once('SIGTERM', shutdown); process.once('SIGINT', shutdown)
464
458
  /**
465
459
  * 父进程(宿主)没了就自己退出:否则这里会变成一具握着隧道的尸体。
466
460
  *
467
- * ~~Node 的 fork 子进程不会随父进程退出~~。宿主被 Cmd+Q / 被系统内存压力清掉 / 被
468
- * SIGKILL 时,没有任何人会给这里发 SIGTERM,IPC 通道也只是**静默 EOF**(Node 不会
469
- * 替我们发 disconnect)。于是 worker 继续活着、继续握着那条 `register` 成功过的中转
470
- * 隧道、继续如实回答 `/api/mobile/v1/status`(它知道的宿主身份还是那一份),中转的
471
- * `instances` 里那条记录就永不删除 —— 手机侧栏的设备「在线」从此再也灭不掉,正文
472
- * 镜像也停在最后一帧。2026-10-10 真机故障,根因就在这里。
461
+ * 宿主被 Cmd+Q、被系统清掉或被 SIGKILL 时,没有人会给这里发 SIGTERM,IPC 也只是静默 EOF。
462
+ * worker 继续活着、继续握着那条注册成功过的隧道,中转的 instances 里那条记录就永不删除,
463
+ * 手机侧栏的设备「在线」再也灭不掉。
473
464
  *
474
- * 两道判据都留着,互相兜底:
475
- * - `disconnect`:宿主正常退出 / 主动销毁 IPC 时立刻触发,退出最快;
476
- * - ppid 巡检:宿主被 SIGKILL 时**不发** disconnect(实测只看到 ppid 变成 1),
477
- * 只能靠定时对账。启动时记下 ppid,之后对不上就退出;间隔与宿主那边的健康轮询
478
- * 同量级(10s),2s 只是让「关掉桌面 → 手机显示离线」快一点。
465
+ * 两道判据互相兜底:`disconnect` 在宿主正常退出时立刻触发(最快);ppid 巡检兜住 SIGKILL
466
+ * (实测只看到 ppid 变成 1,不发 disconnect),启动时记下 ppid,之后对不上就退出。
479
467
  *
480
- * 为什么是整进程退出而不是只 `stopServices()`:隧道一关,中转最多 60s(心跳 pong
481
- * 超时)才删掉实例,这段时间手机仍显示在线;而孤儿进程本来就没有存在的理由,退出是
482
- * 最快的收敛路径(也让 `ps` 里不再留下一行认不出归属的 node 进程)。
468
+ * 整进程退出而不是只 `stopServices()`:隧道一关,中转要等心跳超时才删实例,这期间手机仍
469
+ * 显示在线;孤儿进程没有存在的理由,退出是最快的收敛路径。
483
470
  */
484
471
  const PARENT_WATCH_INTERVAL_MS = 2000
485
472
  const parentPidAtStart = process.ppid
@@ -3,14 +3,14 @@
3
3
  /**
4
4
  * SidecarFacade:宿主 ⇄ sidecar worker 的环回 HTTP 客户端 + 生命周期看守。
5
5
  *
6
- * fork worker、等 ready、把它的 stdout/stderr 转发到宿主 stderr(诊断只有这一条通道),
7
- * 之后按需 request('/status' | '/configure' | '/tunnel/send' | '/revoke-all')。
8
- * 两条上报通道都是「宿主必须立刻知道」的事:onDown(worker 意外退出,否则宿主的隧道/
9
- * 局域网快照会永远冻在临死前那一帧,比状态难看危险得多)与 onState(worker 在状态
10
- * 变化那一刻主动推的一帧,比 10s 轮询早得多)。
6
+ * fork worker、等 ready、把它的 stdout/stderr 转发到宿主 stderr(诊断只有这一条通道),之后
7
+ * 按需 request('/status' | '/configure' | '/tunnel/send' | '/revoke-all')。
11
8
  *
12
- * 刻意不做重试:request 超时就是超时,由调用方(sidecar-lifecycle)决定是否重启;
13
- * 在这里偷偷重试会把「隧道已死」拖成「偶尔能通」,反而更难诊断。
9
+ * 两条上报通道都是「宿主必须立刻知道」的事:onDown(worker 意外退出,否则隧道/局域网快照会
10
+ * 永远冻在临死前那一帧)、onState(worker 在状态变化那一刻主动推的一帧)。
11
+ *
12
+ * 刻意不做重试:超时就是超时,由调用方决定是否重启;偷偷重试会把「隧道已死」拖成
13
+ * 「偶尔能通」,更难诊断。
14
14
  */
15
15
 
16
16
  const http = require('node:http')
@@ -3,15 +3,13 @@
3
3
  /**
4
4
  * sidecar 子进程的完整生命周期:起停、配置下发、自重启、健康轮询。
5
5
  *
6
- * 这些代码原本散在 apply() 里三段(状态变量、配置下发的串行化、自重启),中间还隔着
7
- * 一个 200 行的镜像推送调度器。它们共享同一批可变量(sidecarState / sidecarSnapshot /
8
- * restartInFlight / syncInFlight),拆到一处才看得清「谁改了什么」。
6
+ * 状态变量、配置下发的串行化、自重启共享同一批可变状态,拆到一处才看得清「谁改了什么」。
9
7
  *
10
- * 注意扩展名是 .cjs:包根声明 type=module,lib/ 下没有就近的 package.json,所以
11
- * lib/*.js 全是 ESM,只有 .cjs 是 CommonJS(同 sidecar-facade.cjs)。
8
+ * 扩展名是 .cjs:包根声明 type=module,lib/ 下没有就近的 package.json,所以 lib/*.js 全是
9
+ * ESM,只有 .cjs 是 CommonJS。
12
10
  *
13
- * ⚠️ `sidecar` 是**会被整体替换**的 let(自重启换掉 facade 实例),所以对外一律给
14
- * 取值器或 accessor,绝不能把实例本身传出去——传出去就是一份永远指向旧实例的快照。
11
+ * `sidecar` 是会被整体替换的 let(自重启换掉 facade 实例),所以对外一律给取值器,绝不能把
12
+ * 实例本身传出去。
15
13
  */
16
14
 
17
15
  const path = require('node:path');
@@ -250,8 +248,8 @@ function createSidecarLifecycle(deps) {
250
248
  * `configure` 完的那个窗口里快照还是上一个号(或还没绑完),此刻去试绑刚下发的号必然
251
249
  * 失败——那是自己占着自己,不是被别人占了。只认快照的话,紧接着的第二次同步就会把这个
252
250
  * 号判成「被占」、换个内核随手给的端口写回 relay.json,监听也跟着挪走(`/api/relay/config`
253
- * 改端口那条路由**一次会同步两遍**,第二遍正好落在这个窗口里;2026-10-11 真机实测,
254
- * 落盘端口 18787→22779→33091→17079→28223 一路漂,手机里存的地址就是这么废的)。
251
+ * 改端口那条路由**一次会同步两遍**,第二遍正好落在这个窗口里,落盘端口一路漂,
252
+ * 手机里存的地址就是这么废的)。
255
253
  *
256
254
  * 代价照实说:真的被别的程序占着时,这里也认这个号。那没关系——worker 自己 bind 失败会
257
255
  * 退到内核给的空闲端口并按**实绑值**上报,对外报的地址(getAdvertisedLanPort)跟着它走。
@@ -1,68 +1,23 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * 自己装新版:在 profile 里跑一次它**自己的**包管理器,把本插件换到指定版本。
4
+ * 自己装新版:在 profile 里跑一次它自己的包管理器,把本插件换到指定版本。
5
5
  *
6
- * ## 为什么不借道 DSH 插件市场
6
+ * 不借道 DSH 插件市场:更新分「查」(update-registry)和「装」,装该由插件自己完成,不该因为
7
+ * 另一个插件没装、太老或坏了就整个失效。
7
8
  *
8
- * 插件的更新分两步:**查**(线上最新是几,见 `update-registry.cjs`)和**装**(把新版本真的
9
- * 落到这台机器的 profile 里)。2026-10-11 之前「装」是请市场代劳的(打它的
10
- * `/dsh-market/api/v1/updates`,等它跑完 pnpm 再问结果)。用户的判断是:这件事插件应该
11
- * 自己完成,不该因为「另一个插件没装 / 太老 / 坏了」就整个失效。这里就是那一步。
9
+ * 不自己实现包管理,只驱动它:profile 的 pnpm-workspace.yaml 已经写好 node-linker、peer 等
10
+ * 关键设置,这里只「cd 到 profile 跑一句 pnpm add <包名>@<版本>」,不传布局类开关。三处必要
11
+ * 的开关:auto-install-peers=false(peer 是运行时注入的,npm 上没有)、CI=1(无终端),以及
12
+ * 撞上 pnpm「新发布先晾一会儿」时带 minimum-release-age=0 重试一次(那道策略查整份 lockfile,
13
+ * 别的插件刚发也会连累我们)。
12
14
  *
13
- * ## 关键:不自己实现包管理,只**驱动**它
15
+ * 包管理器优先用宿主发布的那个(profile 是宿主自带 pnpm 装的,PATH 上版本不同会
16
+ * ERR_PNPM_UNEXPECTED_STORE),找不到再退回 PATH。装之前先看有没有 agent 在跑;force 跳过
17
+ * 这道闸并跳过发布晾置期,auto 只跳过这道闸。顺序上先答「有没有新版」,没东西可装时不该拿
18
+ * 「有对话在跑」挡在前面。
14
19
  *
15
- * profile 里装插件用的是 pnpm,而且 profile 自己的 `pnpm-workspace.yaml` 已经写好了所有
16
- * 关键设置(`node-linker=hoisted`、`auto-install-peers=false`、`minimum-release-age-exclude`
17
- * 那些按包放行的条目)。所以这里是「cd 到 profile,跑一句 `pnpm add <包名>@<版本>`」——
18
- * **不传布局类开关**,让它按 profile 的既有配置来,免得把别人的布局改写掉。市场那套
19
- * (`pnpm-compat.js` + `install.js`)几千行都在处理 pnpm 各版本的旗标拼写、hoist 恢复、
20
- * 镜像/超时/Windows shim 这些边角;这里刻意**只做我们自己这条路上必须的那两三个开关**,
21
- * 剩下的交给 profile 自己的配置。
22
- *
23
- * ## 用哪个包管理器:**宿主给的那个优先**
24
- *
25
- * 2026-10-11 真机踩到:这台机器 PATH 上的 pnpm 是 10.x(store v10),而 profile 是**宿主
26
- * 自带的** pnpm 11.x(store v11)装的,拿 PATH 那个去装直接 `ERR_PNPM_UNEXPECTED_STORE`。
27
- * 所以顺序是「宿主发布的调用方式 → PATH 上的 pnpm → npm」,见 `normalizeInvocation`。
28
- *
29
- * ## 三个开关,每个都有理由
30
- *
31
- * · `--config.auto-install-peers=false`:本插件的 peer 里有一串 `@deepseek-ai/*`
32
- * (`cordis`、`dsh-tools`、`dsh-client-runtime`…),它们是 **DSH 运行时注入的、npm 上
33
- * 根本没有**。pnpm 8 起 `auto-install-peers` 默认开,会去 npm 上拉这些 peer 然后 404。
34
- * profile 里通常已经关掉了,但那是市场帮它写的 —— 换台机器不一定有,所以这里显式带上。
35
- * · `--config.minimum-release-age=0`:**撞上 pnpm 的「新发布先晾一会儿」策略时才带**。
36
- * 强制更新(`force`)一开始就带;平时不带,但**失败信息确实是这类策略时会带开关重试一次**
37
- * —— 那道策略查的是**整份 lockfile**,别的插件刚发不久也会把整份锁文件判成「含未过期
38
- * 条目」,于是我们自己的更新整整一天都做不了,而原因跟我们这个包毫无关系(2026-10-11
39
- * 真机撞上的就是 `billion-context@0.1.192`)。市场那边对这种拒绝同样是带开关重试一次。
40
- * · `CI=1`:没有终端可交互。包管理器在无人值守的子进程里问问题是没法回答的。
41
- *
42
- * ## 装之前先看有没有 agent 在跑(但「有没有新版」排在它前面)
43
- *
44
- * 装是**就地替换插件文件**,正在干活的对话可能读到半个包。市场那条路有一道这样的闸
45
- * (`AGENTS_RUNNING`),这里也补上了 —— 判到有人跑就**不动盘**,如实报成「可再试」。
46
- *
47
- * 两档能跳过它:`force`(面板上的「强制更新」,用户明说「我知道,装吧」)与 `auto`(宿主
48
- * 每 10 分钟那一轮自动更新)。后者是有意的取舍:手机那边几乎一直有对话在跑,按着这道闸等,
49
- * 自动更新实际上永远轮不到 —— 而它的定义就是「没人看着也会发生」。
50
- *
51
- * `auto` **只**跳过这道闸:照样要有新版才装(不跳过下面那条判断),也不一开始就带发布时长
52
- * 开关(撞上再重试一次就够了)。把 `force` 那一档借给自动会在每 10 分钟把同一个版本重装
53
- * 一遍,那不是自动更新。
54
- *
55
- * 顺序上**先答「有没有新版」**:没东西可装时拿一句「有对话在跑」挡在前面是误导,用户会以为
56
- * 是那个原因,而真正的答案是「已经是最新」。
57
- *
58
- * ## 边界(照实说)
59
- *
60
- * · **只动 profile 的依赖树**(`pnpm add` 会顺手把 profile 的 `package.json` 里的依赖声明
61
- * 也更新掉——那正是「装上了」的凭据)。不碰仓库、不碰 `~/.dsh` 里别的东西。
62
- * · **找不到包管理器就如实失败**,不猜、不回退到「假装装好了」。桌面端由 GUI 拉起时 PATH
63
- * 往往很干净(LaunchServices 只给 `/usr/bin:/bin` 之类),所以这里照市场的做法补一组
64
- * 常见 bin 目录再找,见 `toolSearchDirs`。
65
- * · 装完**以盘上的版本号为准**:`installedVersion` 没变就是没装上,哪怕命令退出码是 0。
20
+ * 只动 profile 的依赖树;找不到包管理器就如实失败;装完以盘上版本号为准,退出码 0 不算装上。
66
21
  */
67
22
 
68
23
  const fs = require('node:fs');
@@ -491,7 +446,7 @@ async function installLatestIfNeeded(options = {}) {
491
446
  }
492
447
  const check = await checkForUpdate({ installedVersion: beforeVersion, source });
493
448
  // 「有没有新版」排在「有没有人在跑」**前面**:没东西可装的时候,拿一句「有对话在跑」挡住
494
- // 真正的答案(已经是最新)是误导 —— 2026-10-11 真机上就是这么被绊了一下。
449
+ // 真正的答案(已经是最新)是误导。
495
450
  if (!force && !check.updateAvailable) {
496
451
  return {
497
452
  installed: false,
@@ -3,25 +3,13 @@
3
3
  /**
4
4
  * 「有没有新版」由插件自己回答:直接问 npm 注册表(`<registry>/<name>/latest`)。
5
5
  *
6
- * ## 为什么不再借道 DSH 插件市场
6
+ * 更新分「查」和「装」:查不需要市场(公开包一个 GET 就够),装由 `update-install.cjs` 在
7
+ * profile 里自己跑包管理器。绕开市场的理由很具体:市场没装、太老或坏了,「检查更新」就整个
8
+ * 答不出来。
7
9
  *
8
- * 更新其实分两件事:**查**(线上最新是几、这台机器上是几)和**装**(在 profile 里跑依赖
9
- * 安装、再把运行中的实现换掉)。装只能交给市场 —— 它握着 profile 的依赖树、版本核验与
10
- * 回滚点,插件自己再 shell 出去装一遍等于把那些抄一遍,而抄漏一处就是「装了个不是最新
11
- * 的版本」。但**查**不需要市场:这是个公开的 npm 包,一个 GET 就够了。
12
- *
13
- * 绕市场的代价很具体:市场(另一个插件)没装、太老或者坏了,「检查更新」就整个答不出来
14
- * —— 一个插件得另一个插件在场,才能告诉你自己有没有新版。所以这一格拿回来自己做。
15
- *
16
- * ## 边界(别把这条读成「更新彻底不依赖市场了」)
17
- *
18
- * · 这里**只查不装**:从头到尾不碰 profile、不碰市场、不写盘。装仍然是
19
- * `/api/relay/update/apply` 那条路(外壳问市场 → 市场装 → 外壳就地换代)。
20
- * · `source`(npm / 本地 link / `file:` tgz)只有市场知道 —— 它读的是 profile 里的依赖
21
- * 声明。所以那一格传进来的是外壳**上次**问市场的结果;市场不在时如实为空,本模块
22
- * **不会**为了这一格去敲市场。
23
- * · 判据是「线上版本严格大于盘上版本」。线上比盘上旧(回滚、本地构建跑在发布前面)一律
24
- * 报「没有新版」——「这次更新会降级」必须由版本比较判定,不能由「两个号不相等」判定。
10
+ * 边界:这里只查不装,不碰 profile、不写盘;`source`(npm / link / file:)只有市场知道,传
11
+ * 进来的是上次问到的结果,本模块不会为它去敲市场。判据是「线上版本严格大于盘上版本」,线上
12
+ * 更旧一律报没有新版——「这次更新会降级」必须由版本比较判定。
25
13
  */
26
14
 
27
15
  /** 默认注册表:官方源。私有镜像 / 测试用 `DSHM_NPM_REGISTRY` 覆盖。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mobile-plugin",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Mobile API (/api/mobile/v1) and relay client for the DSH mobile app — desktop profile build (dsh 0.2.x, peers resolved by the host)",
5
5
  "license": "MIT",
6
6
  "engines": {