dsh-mobile-plugin 0.4.0 → 0.4.4
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 +71 -74
- package/dsh-mobile-credentials/lib/credentials.js +47 -24
- package/host.js +22 -36
- package/lib/client.js +29 -82
- package/lib/desktop-view.cjs +5 -10
- package/lib/git/read.js +6 -8
- package/lib/host-compat.cjs +13 -25
- package/lib/host-guard.cjs +9 -39
- package/lib/host-patches.cjs +5 -16
- package/lib/http/routing.js +7 -27
- package/lib/impl.js +6 -12
- package/lib/index.js +1 -1
- package/lib/mirror/pusher.cjs +33 -85
- package/lib/mirror-snapshot.cjs +6 -24
- package/lib/mobile/agent-presets.js +6 -26
- package/lib/mobile/allowed-zone.js +100 -0
- package/lib/mobile/approvals.js +18 -44
- package/lib/mobile/archive-clean.js +13 -21
- package/lib/mobile/attachments.js +16 -37
- package/lib/mobile/channels.js +15 -41
- package/lib/mobile/credential-upgrade.js +7 -16
- package/lib/mobile/credentials.js +5 -21
- package/lib/mobile/directory-browse.js +8 -27
- package/lib/mobile/directory-listing.js +10 -23
- package/lib/mobile/drive-roots.js +59 -0
- package/lib/mobile/envelope.js +4 -15
- package/lib/mobile/events.js +35 -65
- package/lib/mobile/extensions.js +5 -8
- package/lib/mobile/file-read.js +168 -0
- package/lib/mobile/git-repos.js +6 -15
- package/lib/mobile/git.js +11 -19
- package/lib/mobile/history-derive.js +85 -134
- package/lib/mobile/host-bridge/endpoints.js +4 -6
- package/lib/mobile/host-bridge/event-synthesis.js +21 -35
- package/lib/mobile/host-bridge/shims.js +12 -22
- package/lib/mobile/host-bridge.js +8 -20
- package/lib/mobile/host-settings.js +9 -19
- package/lib/mobile/inbox-clean.js +7 -24
- package/lib/mobile/local-images.js +48 -23
- package/lib/mobile/model-vendors.js +105 -0
- package/lib/mobile/monitor.js +8 -17
- package/lib/mobile/pairing.js +7 -19
- package/lib/mobile/plugin-market.js +475 -0
- package/lib/mobile/plugin-self.js +6 -23
- package/lib/mobile/plugin-update.js +8 -27
- package/lib/mobile/plugins.js +6 -24
- package/lib/mobile/questions.js +15 -40
- package/lib/mobile/references.js +11 -31
- package/lib/mobile/relay-control.js +136 -33
- package/lib/mobile/rpc.js +5 -13
- package/lib/mobile/schedule.js +8 -33
- package/lib/mobile/self-check.cjs +9 -36
- package/lib/mobile/session-cache.js +13 -19
- package/lib/mobile/session-cwd.js +28 -0
- package/lib/mobile/session-file.js +9 -33
- package/lib/mobile/session-flags.js +7 -13
- package/lib/mobile/session-mirror.js +8 -15
- package/lib/mobile/session-models.js +3 -3
- package/lib/mobile/session-schedule-mark.js +8 -14
- package/lib/mobile/session-send.js +13 -21
- package/lib/mobile/session-snapshot.js +4 -6
- package/lib/mobile/sessions.js +7 -23
- package/lib/mobile/stats-collect.cjs +154 -37
- package/lib/mobile/stats-ledger.cjs +258 -75
- package/lib/mobile/stats-scheduler.cjs +41 -0
- package/lib/mobile/stream.js +39 -130
- package/lib/mobile/subagents.js +5 -18
- package/lib/mobile/uploads.js +75 -21
- package/lib/mobile/vendor-logos.cjs +30 -0
- package/lib/mobile/wire-cut.js +212 -0
- package/lib/mobile/workspaces.js +20 -8
- package/lib/panel/index.html +7 -7
- package/lib/read-receipt.cjs +10 -21
- package/lib/relay/apply-ledger.cjs +52 -0
- package/lib/relay/core.js +14 -3
- package/lib/relay/lan-network.cjs +14 -28
- package/lib/relay/lan-proxy.js +14 -21
- package/lib/relay/migrate.js +90 -0
- package/lib/relay/pairing-policy.js +6 -9
- package/lib/relay/preview.js +8 -25
- package/lib/relay/protocol.js +9 -12
- package/lib/relay/status.js +5 -8
- package/lib/relay/tunnel.js +9 -14
- package/lib/relay/verify.js +52 -0
- package/lib/routes/control-plane.js +39 -47
- package/lib/routes/mobile-api.js +47 -34
- package/lib/session-cleanup.cjs +26 -27
- package/lib/shared/credentials-entry.js +4 -11
- package/lib/shared/device-headers.js +12 -4
- package/lib/shared/dshtoken.js +4 -4
- package/lib/shared/e2ee-request.js +1 -1
- package/lib/shared/e2ee.js +8 -14
- package/lib/shared/fresh-request.js +6 -22
- package/lib/shared/http-util.js +3 -4
- package/lib/shared/list-revision.js +5 -14
- package/lib/shared/session-signal.js +7 -16
- package/lib/shared/sse-write.js +14 -20
- package/lib/shared/workspace-guard.js +7 -15
- package/lib/shell/device-ota.js +4 -6
- package/lib/shell/manager.js +12 -27
- package/lib/shell/ota-package.js +7 -12
- package/lib/shell/runtime.js +8 -16
- package/lib/shell/self-check-after-swap.js +9 -23
- package/lib/shell/updater.js +15 -28
- package/lib/sidecar-facade-worker.cjs +75 -41
- package/lib/sidecar-facade.cjs +18 -8
- package/lib/sidecar-lifecycle.cjs +21 -10
- package/lib/update-install.cjs +24 -62
- package/lib/update-registry.cjs +6 -18
- package/package.json +1 -1
- package/lib/auto-archive.cjs +0 -98
package/lib/sidecar-facade.cjs
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* SidecarFacade:宿主 ⇄ sidecar worker 的环回 HTTP 客户端 + 生命周期看守。
|
|
5
5
|
*
|
|
6
|
-
* fork worker、等 ready、把它的 stdout/stderr 转发到宿主 stderr
|
|
7
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* 两条上报通道都是「宿主必须立刻知道」的事:onDown(worker 意外退出,否则隧道/局域网快照会
|
|
10
|
+
* 永远冻在临死前那一帧)、onState(worker 在状态变化那一刻主动推的一帧)。
|
|
11
|
+
*
|
|
12
|
+
* 刻意不做重试:超时就是超时,由调用方决定是否重启;偷偷重试会把「隧道已死」拖成
|
|
13
|
+
* 「偶尔能通」,更难诊断。
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
const http = require('node:http')
|
|
@@ -19,6 +19,15 @@ const { join } = require('node:path')
|
|
|
19
19
|
|
|
20
20
|
const worker = join(__dirname, 'sidecar-facade-worker.cjs')
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* `/configure` 的等待上限。
|
|
24
|
+
*
|
|
25
|
+
* 换中转地址时 worker 会**等新隧道真的注册上**再回包(顺序见 lib/relay/migrate.js):注册
|
|
26
|
+
* 失败要撤销迁移、还要把错误一路回给手机上那一发请求。隧道自己的注册超时是 8s,所以这里
|
|
27
|
+
* 比它宽一截;超时了宿主拿到的是「facade 超时」而不是真正的原因,那句话在手机上没法用。
|
|
28
|
+
*/
|
|
29
|
+
const CONFIGURE_TIMEOUT_MS = 12_000
|
|
30
|
+
|
|
22
31
|
class SidecarFacade {
|
|
23
32
|
constructor(options) {
|
|
24
33
|
this.parentPort = options.parentPort
|
|
@@ -120,7 +129,8 @@ class SidecarFacade {
|
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
async configure(config) {
|
|
123
|
-
|
|
132
|
+
// 换地址那一发要等新隧道注册,比默认 5s 久(见 CONFIGURE_TIMEOUT_MS)。
|
|
133
|
+
return this.request('/configure', 'POST', config, CONFIGURE_TIMEOUT_MS)
|
|
124
134
|
}
|
|
125
135
|
|
|
126
136
|
async stop() {
|
|
@@ -3,21 +3,23 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* sidecar 子进程的完整生命周期:起停、配置下发、自重启、健康轮询。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* 一个 200 行的镜像推送调度器。它们共享同一批可变量(sidecarState / sidecarSnapshot /
|
|
8
|
-
* restartInFlight / syncInFlight),拆到一处才看得清「谁改了什么」。
|
|
6
|
+
* 状态变量、配置下发的串行化、自重启共享同一批可变状态,拆到一处才看得清「谁改了什么」。
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* 扩展名是 .cjs:包根声明 type=module,lib/ 下没有就近的 package.json,所以 lib/*.js 全是
|
|
9
|
+
* ESM,只有 .cjs 是 CommonJS。
|
|
12
10
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* `sidecar` 是会被整体替换的 let(自重启换掉 facade 实例),所以对外一律给取值器,绝不能把
|
|
12
|
+
* 实例本身传出去。
|
|
13
|
+
*
|
|
14
|
+
* 每一次配置下发(`applySidecarConfig`)的成功失败都记进 `relay/apply-ledger.cjs` 的台账:
|
|
15
|
+
* 换中转地址失败时,手机端「运行状态」读的就是那一份(错误、时间与当时要切过去的地址)。
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
const path = require('node:path');
|
|
18
19
|
const { randomBytes } = require('node:crypto');
|
|
19
20
|
|
|
20
21
|
const { SidecarFacade } = require('./sidecar-facade.cjs');
|
|
22
|
+
const applyLedger = require('./relay/apply-ledger.cjs');
|
|
21
23
|
|
|
22
24
|
const HEALTH_POLL_INTERVAL_MS = 10000;
|
|
23
25
|
|
|
@@ -106,7 +108,16 @@ function createSidecarLifecycle(deps) {
|
|
|
106
108
|
await resolveLanPort()
|
|
107
109
|
const config = buildConfig(lanPortInUse())
|
|
108
110
|
configuredLanPort = Number.isInteger(config.lanPort) ? config.lanPort : 0
|
|
109
|
-
|
|
111
|
+
// 失败必须落账(`/api/mobile/v1/relay/diagnostics` 的 `apply` 那一格):所有下发都经过
|
|
112
|
+
// 这里 —— 手机改地址、电脑面板改地址、启动时那次 —— 而调用方几乎都 `.catch(() => …)`
|
|
113
|
+
// 掉。换地址失败的现场是「保存成功、然后永久离线」,台账是唯一能说清原因的地方。
|
|
114
|
+
try {
|
|
115
|
+
sidecarSnapshot = await sidecar.configure(config)
|
|
116
|
+
} catch (error) {
|
|
117
|
+
applyLedger.recordFailure(error, config.relayUrl)
|
|
118
|
+
throw error
|
|
119
|
+
}
|
|
120
|
+
applyLedger.recordSuccess()
|
|
110
121
|
sidecarState = 'healthy'
|
|
111
122
|
return sidecarSnapshot
|
|
112
123
|
}
|
|
@@ -250,8 +261,8 @@ function createSidecarLifecycle(deps) {
|
|
|
250
261
|
* `configure` 完的那个窗口里快照还是上一个号(或还没绑完),此刻去试绑刚下发的号必然
|
|
251
262
|
* 失败——那是自己占着自己,不是被别人占了。只认快照的话,紧接着的第二次同步就会把这个
|
|
252
263
|
* 号判成「被占」、换个内核随手给的端口写回 relay.json,监听也跟着挪走(`/api/relay/config`
|
|
253
|
-
*
|
|
254
|
-
*
|
|
264
|
+
* 改端口那条路由**一次会同步两遍**,第二遍正好落在这个窗口里,落盘端口一路漂,
|
|
265
|
+
* 手机里存的地址就是这么废的)。
|
|
255
266
|
*
|
|
256
267
|
* 代价照实说:真的被别的程序占着时,这里也认这个号。那没关系——worker 自己 bind 失败会
|
|
257
268
|
* 退到内核给的空闲端口并按**实绑值**上报,对外报的地址(getAdvertisedLanPort)跟着它走。
|
package/lib/update-install.cjs
CHANGED
|
@@ -1,68 +1,24 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* 自己装新版:在 profile
|
|
4
|
+
* 自己装新版:在 profile 里跑一次它自己的包管理器,把本插件换到指定版本。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* 不借道 DSH 插件市场:更新分「查」(update-registry)和「装」,装该由插件自己完成,不该因为
|
|
7
|
+
* 另一个插件没装、太老或坏了就整个失效。
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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 与 auto
|
|
17
|
+
* 都跳过这道闸、也都不等发布晾置期(2026-10-26 用户要求自动更新一检测到新版就立刻装,与面板
|
|
18
|
+
* 上那颗「强制更新」同一个口径),两者的区别只剩「没新版时装不装」。顺序上先答「有没有新版」,
|
|
19
|
+
* 没东西可装时不该拿「有对话在跑」挡在前面。
|
|
14
20
|
*
|
|
15
|
-
*
|
|
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。
|
|
21
|
+
* 只动 profile 的依赖树;找不到包管理器就如实失败;装完以盘上版本号为准,退出码 0 不算装上。
|
|
66
22
|
*/
|
|
67
23
|
|
|
68
24
|
const fs = require('node:fs');
|
|
@@ -288,6 +244,7 @@ function run(command, args, { cwd, env, timeoutMs, spawnImpl = spawn }) {
|
|
|
288
244
|
* @param {string} options.profileNodeModules profile 的 `node_modules` 目录
|
|
289
245
|
* @param {string} options.version 要装的版本(注册表给的)
|
|
290
246
|
* @param {boolean} [options.force] true = 跳过「新发布先晾一会儿」的门槛
|
|
247
|
+
* @param {boolean} [options.auto] 自动更新那一轮:同样跳过发布晾置期(见文件头)
|
|
291
248
|
* @param {() => any} [options.beforeInstall] 真正跑包管理器**之前**调一次(外壳用它把正在跑的
|
|
292
249
|
* 那一代复制进回退目录)。包管理器一跑就把旧的 store 目录清掉了,晚一步没得复制。
|
|
293
250
|
* @returns {Promise<{installed, skipped, beforeVersion, installedVersion, source, command, output, failure}>}
|
|
@@ -297,6 +254,7 @@ async function installLatest({
|
|
|
297
254
|
version,
|
|
298
255
|
name = DEFAULT_NAME,
|
|
299
256
|
force = false,
|
|
257
|
+
auto = false,
|
|
300
258
|
timeoutMs = INSTALL_TIMEOUT_MS,
|
|
301
259
|
env = process.env,
|
|
302
260
|
platform = process.platform,
|
|
@@ -376,8 +334,11 @@ async function installLatest({
|
|
|
376
334
|
|
|
377
335
|
const spec = `${name}@${version}`;
|
|
378
336
|
const releaseAgeFlag = '--config.minimum-release-age=0';
|
|
337
|
+
// `auto` 与 `force` 一样一开始就带上开关:自动更新要「一检测到就装」,不该先被那道策略挡回来
|
|
338
|
+
// 再重试一遍(多等一轮、日志里多一条失败)。
|
|
339
|
+
const skipReleaseAge = force || auto;
|
|
379
340
|
const args = picked.manager === 'pnpm'
|
|
380
|
-
? ['add', spec, '--config.auto-install-peers=false', ...(
|
|
341
|
+
? ['add', spec, '--config.auto-install-peers=false', ...(skipReleaseAge ? [releaseAgeFlag] : [])]
|
|
381
342
|
: ['install', spec, '--no-audit', '--no-fund'];
|
|
382
343
|
let command = [picked.manager, ...args].join(' ');
|
|
383
344
|
onEvent({ phase: 'install-started', command, manager: picked.kind });
|
|
@@ -401,7 +362,7 @@ async function installLatest({
|
|
|
401
362
|
* 重试一次只会把同一条错误再说一遍,还多花一次等待。
|
|
402
363
|
*/
|
|
403
364
|
const releaseAgeLike = /MINIMUM_RELEASE_AGE|minimumReleaseAge|minimum-release-age|release age/i;
|
|
404
|
-
const retried = !result.ok && picked.manager === 'pnpm' && !
|
|
365
|
+
const retried = !result.ok && picked.manager === 'pnpm' && !skipReleaseAge && releaseAgeLike.test(output);
|
|
405
366
|
if (retried) {
|
|
406
367
|
onEvent({ phase: 'release-age-retry' });
|
|
407
368
|
result = await spawnAdd([releaseAgeFlag]);
|
|
@@ -467,7 +428,8 @@ async function installLatest({
|
|
|
467
428
|
*
|
|
468
429
|
* `force` 是**用户明说「我知道,装吧」**的那一挡(面板上那颗「强制更新」):它同时跳过
|
|
469
430
|
* 这道闸和「新发布先晾一会儿」的门槛,而且不管有没有新版都装一遍。`auto`(宿主那一轮自动
|
|
470
|
-
*
|
|
431
|
+
* 更新)跳过同样的两道闸,只是仍然「没新版就不动盘」—— 2026-10-26 用户要求自动更新一检测到
|
|
432
|
+
* 新版就立刻装,所以它不再等发布晾置期,见文件头。
|
|
471
433
|
*/
|
|
472
434
|
async function installLatestIfNeeded(options = {}) {
|
|
473
435
|
const { profileNodeModules, force = false, auto = false, name = DEFAULT_NAME, agents = null } = options;
|
|
@@ -491,7 +453,7 @@ async function installLatestIfNeeded(options = {}) {
|
|
|
491
453
|
}
|
|
492
454
|
const check = await checkForUpdate({ installedVersion: beforeVersion, source });
|
|
493
455
|
// 「有没有新版」排在「有没有人在跑」**前面**:没东西可装的时候,拿一句「有对话在跑」挡住
|
|
494
|
-
//
|
|
456
|
+
// 真正的答案(已经是最新)是误导。
|
|
495
457
|
if (!force && !check.updateAvailable) {
|
|
496
458
|
return {
|
|
497
459
|
installed: false,
|
package/lib/update-registry.cjs
CHANGED
|
@@ -3,25 +3,13 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 「有没有新版」由插件自己回答:直接问 npm 注册表(`<registry>/<name>/latest`)。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* 更新分「查」和「装」:查不需要市场(公开包一个 GET 就够),装由 `update-install.cjs` 在
|
|
7
|
+
* profile 里自己跑包管理器。绕开市场的理由很具体:市场没装、太老或坏了,「检查更新」就整个
|
|
8
|
+
* 答不出来。
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
package/lib/auto-archive.cjs
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { unwrap } = require('./mobile/envelope.js')
|
|
4
|
-
|
|
5
|
-
const ARCHIVE_AFTER_MS = 24 * 60 * 60 * 1000
|
|
6
|
-
const SCAN_INTERVAL_MS = 12 * 60 * 60 * 1000
|
|
7
|
-
|
|
8
|
-
function projectCounts(items) {
|
|
9
|
-
const counts = new Map()
|
|
10
|
-
for (const item of items) {
|
|
11
|
-
const cwd = typeof item?.cwd === 'string' ? item.cwd.trim() : ''
|
|
12
|
-
if (cwd) counts.set(cwd, (counts.get(cwd) || 0) + 1)
|
|
13
|
-
}
|
|
14
|
-
return counts
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function archiveCandidates(items, now = Date.now()) {
|
|
18
|
-
const counts = projectCounts(items)
|
|
19
|
-
const cutoff = now - ARCHIVE_AFTER_MS
|
|
20
|
-
return items.filter(item => {
|
|
21
|
-
const cwd = typeof item?.cwd === 'string' ? item.cwd.trim() : ''
|
|
22
|
-
if (!cwd || (counts.get(cwd) || 0) <= 10) return false
|
|
23
|
-
if (item?.blank || item?.running || item?.sessionId === undefined) return false
|
|
24
|
-
return Number.isFinite(item.updatedAt) && item.updatedAt < cutoff
|
|
25
|
-
})
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function loggerOf(ctx) {
|
|
29
|
-
return ctx?.logger && typeof ctx.logger === 'object' ? ctx.logger : undefined
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function logFailure(ctx, sessionId, error) {
|
|
33
|
-
const logger = loggerOf(ctx)
|
|
34
|
-
const message = `automatic session archive failed for ${sessionId}: ${String(error?.message || error)}`
|
|
35
|
-
if (typeof logger?.warn === 'function') logger.warn(message)
|
|
36
|
-
else if (typeof logger?.error === 'function') logger.error(message)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function scanAndArchive(ctx, now = Date.now()) {
|
|
40
|
-
const api = ctx?.apiProxy
|
|
41
|
-
const list = api?.sessions?.list
|
|
42
|
-
const archive = api?.workspace?.archiveSession
|
|
43
|
-
if (typeof list !== 'function' || typeof archive !== 'function') return { scanned: 0, archived: 0, failed: 0 }
|
|
44
|
-
|
|
45
|
-
const response = await list({ rpcId: 'dsh-mobile-plugin-auto-archive', method: 'session.list', payload: {} })
|
|
46
|
-
// 回执是 { result: { ok, value } } 信封:早先这里直接读 response.value,
|
|
47
|
-
// 而 list() 的回执根本不在那一层——这个定时任务从来没真正扫到过会话。
|
|
48
|
-
const listed = unwrap(response)
|
|
49
|
-
const items = (listed.value && listed.value.items) || []
|
|
50
|
-
const candidates = archiveCandidates(items, now)
|
|
51
|
-
let archived = 0
|
|
52
|
-
let failed = 0
|
|
53
|
-
for (const item of candidates) {
|
|
54
|
-
try {
|
|
55
|
-
const result = await archive({
|
|
56
|
-
rpcId: 'dsh-mobile-plugin-auto-archive',
|
|
57
|
-
method: 'workspace.archiveSession',
|
|
58
|
-
payload: { sessionId: item.sessionId },
|
|
59
|
-
})
|
|
60
|
-
if (result?.ok === false) throw new Error(result.error?.message || 'archive rejected')
|
|
61
|
-
archived += 1
|
|
62
|
-
} catch (error) {
|
|
63
|
-
failed += 1
|
|
64
|
-
logFailure(ctx, item.sessionId, error)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return { scanned: items.length, archived, failed }
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function installAutoArchive(ctx) {
|
|
71
|
-
let disposed = false
|
|
72
|
-
let inFlight
|
|
73
|
-
const scan = () => {
|
|
74
|
-
if (disposed || inFlight) return inFlight
|
|
75
|
-
inFlight = Promise.resolve().then(() => scanAndArchive(ctx)).catch(error => {
|
|
76
|
-
logFailure(ctx, 'scan', error)
|
|
77
|
-
return { scanned: 0, archived: 0, failed: 1 }
|
|
78
|
-
}).finally(() => { inFlight = undefined })
|
|
79
|
-
return inFlight
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
void scan()
|
|
83
|
-
const timer = setInterval(() => { void scan() }, SCAN_INTERVAL_MS)
|
|
84
|
-
return () => {
|
|
85
|
-
if (disposed) return
|
|
86
|
-
disposed = true
|
|
87
|
-
clearInterval(timer)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
module.exports = {
|
|
92
|
-
ARCHIVE_AFTER_MS,
|
|
93
|
-
SCAN_INTERVAL_MS,
|
|
94
|
-
archiveCandidates,
|
|
95
|
-
installAutoArchive,
|
|
96
|
-
projectCounts,
|
|
97
|
-
scanAndArchive,
|
|
98
|
-
}
|