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.
- package/dsh-mobile-credentials/lib/credentials.js +8 -8
- 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 -8
- 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/approvals.js +18 -44
- package/lib/mobile/archive-clean.js +10 -19
- package/lib/mobile/attachments.js +11 -36
- 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/git-repos.js +6 -15
- package/lib/mobile/git.js +11 -19
- package/lib/mobile/history-derive.js +23 -41
- 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 +11 -17
- package/lib/mobile/monitor.js +8 -17
- package/lib/mobile/pairing.js +7 -19
- package/lib/mobile/plugin-self.js +5 -22
- 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 +6 -27
- 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-file.js +8 -21
- 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 +10 -24
- package/lib/mobile/stats-ledger.cjs +13 -20
- package/lib/mobile/stream.js +12 -23
- package/lib/mobile/subagents.js +5 -18
- package/lib/mobile/uploads.js +8 -17
- package/lib/mobile/workspaces.js +20 -8
- package/lib/panel/index.html +7 -7
- package/lib/read-receipt.cjs +10 -21
- package/lib/relay/lan-network.cjs +14 -28
- package/lib/relay/lan-proxy.js +14 -21
- package/lib/relay/pairing-policy.js +6 -9
- package/lib/relay/preview.js +8 -25
- package/lib/relay/protocol.js +6 -13
- package/lib/relay/status.js +5 -8
- package/lib/relay/tunnel.js +9 -14
- package/lib/routes/control-plane.js +28 -45
- package/lib/routes/mobile-api.js +19 -32
- package/lib/session-cleanup.cjs +16 -27
- package/lib/shared/credentials-entry.js +4 -11
- 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 +13 -26
- package/lib/sidecar-facade-worker.cjs +20 -33
- package/lib/sidecar-facade.cjs +7 -7
- package/lib/sidecar-lifecycle.cjs +7 -9
- package/lib/update-install.cjs +14 -59
- package/lib/update-registry.cjs +6 -18
- package/package.json +1 -1
package/lib/session-cleanup.cjs
CHANGED
|
@@ -1,30 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 会话数据清理:按 sessionId 删除 dshHome 下的 sessions/** 目录与 project cache。
|
|
3
3
|
*
|
|
4
|
-
* 两个入口共用同一套落地代码:DSH agent 工具 registerSessionCleanupTool
|
|
5
|
-
*
|
|
4
|
+
* 两个入口共用同一套落地代码:DSH agent 工具 `registerSessionCleanupTool`(action=delete 必须
|
|
5
|
+
* confirm 且只针对已归档)与移动 API `/workspace/clear-archived`。
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* mobile-archived-clear.json
|
|
9
|
-
*
|
|
10
|
-
* 随时可能被读或写,文件不在就是一次未捕获的 ENOENT —— 宿主对未捕获异常是零容忍的,
|
|
11
|
-
* 整个进程退出,桌面端跟着消失(2026-10-20 就是这么闪退的)。
|
|
7
|
+
* 两个入口都不在进程里删磁盘:只把 id 排进待删队列(dshHome 下的
|
|
8
|
+
* `mobile-archived-clear.json`),真正的删除放在宿主下次启动时做。宿主没有「删除会话」接口,
|
|
9
|
+
* 进程活着时那些会话随时可能被读或写,文件不在就是一次未捕获的 ENOENT,整个进程退出。
|
|
12
10
|
*
|
|
13
|
-
* 认哪份文件、怎么读 id
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* 命令),不再 shell 出去:没装 zstd 命令的机器上它一缺就是 readHeader 静默返回
|
|
20
|
-
* undefined,整条删除路径空转。id 仍然取自日志第一行那条 header。
|
|
21
|
-
* - **进程活着的时候不删日志、也不擦宿主的归档集合**;两件事都放到宿主启动时
|
|
22
|
-
* (pruneOrphanedArchivedSessionsOnce)。擦集合的理由写在那个函数上:桌面端的会话
|
|
23
|
-
* 列表是连上宿主时拉的一份快照 + 之后的事件增量,进程里擦集合等于把已经删掉、却还
|
|
24
|
-
* 留在快照里的行放回正常列表。删日志的理由见上面那段:进程里删 = 宿主随时可能踩空。
|
|
25
|
-
* - **Windows 上目录删不掉 ≠ 会话没删掉**:宿主对它打开过的会话一直握着 `session.lock`
|
|
26
|
-
* 的写句柄,日志 unlink 成功后那个名字要等句柄关闭才消失(delete-pending),rmdir
|
|
27
|
-
* 于是报 ENOTEMPTY。判据改成「目录里还有没有规范名日志」:没有就是删成了。
|
|
11
|
+
* 认哪份文件、怎么读 id 以宿主规则为准,不按目录名猜:
|
|
12
|
+
* - 文件名是 `session[.vN].jsonl[.zstd]`(0 代不带 vN,压缩时才带 .zstd);
|
|
13
|
+
* - 解压用 Node 自带的 zstd,不 shell 出去,id 取自日志第一行的 header;
|
|
14
|
+
* - 进程活着时不删日志、也不擦宿主的归档集合,两件事都放到宿主启动时;
|
|
15
|
+
* - Windows 上目录删不掉不等于会话没删掉:宿主握着 `session.lock` 时名字要等句柄关闭才
|
|
16
|
+
* 消失,所以判据是「目录里还有没有规范名日志」。
|
|
28
17
|
*/
|
|
29
18
|
'use strict'
|
|
30
19
|
|
|
@@ -198,9 +187,9 @@ async function archivedIdsWithoutLogs(archivedSessionIds) {
|
|
|
198
187
|
/**
|
|
199
188
|
* 盘上已经不剩任何会话数据的 id:目录没了,或者目录里只剩宿主锁着的 `session.lock`。
|
|
200
189
|
*
|
|
201
|
-
* 只给移动端的「清理所有已归档」补账用:更早一次删除如果删到一半抛错(
|
|
202
|
-
*
|
|
203
|
-
*
|
|
190
|
+
* 只给移动端的「清理所有已归档」补账用:更早一次删除如果删到一半抛错(Windows 上目录非空
|
|
191
|
+
* 时就是这样),已经删掉的那批 id 没能记进本进程的删除集,而宿主的进程内清单还挂着它们 ——
|
|
192
|
+
* 再点清理只会回 `deleted: 0`,手机上永远清不掉。
|
|
204
193
|
*
|
|
205
194
|
* 判据比 archivedIdsWithoutLogs 更宽(那个只认规范名日志),但**不许宽到把迁移中途的
|
|
206
195
|
* 临时文件当成没有数据**:目录里只要还有任何非锁文件(含子目录)就算「还在」,读不出来
|
|
@@ -234,7 +223,7 @@ function sessionIdsWithoutAnyData(sessionIds) {
|
|
|
234
223
|
* deleteQueuedArchivedSessions。原因是宿主没有「删除会话」这个接口:进程活着的时候,
|
|
235
224
|
* 它内存里那些会话随时可能被读或写(列一遍会话、续写一条事件、开一条还没落盘的空白会话),
|
|
236
225
|
* 文件不在就是一次没人接的 ENOENT —— 宿主对未捕获异常零容忍,进程直接退出,桌面端窗口
|
|
237
|
-
*
|
|
226
|
+
* 跟着消失(清理删到一条 0 轮次的空白归档会话就会这样)。
|
|
238
227
|
*
|
|
239
228
|
* 队列是**并集**语义:同一批 id 点两次、两次清理之间换了批次,都只是往同一张表里加。
|
|
240
229
|
*/
|
|
@@ -345,7 +334,7 @@ async function pruneOrphanedArchivedSessions(ctx) {
|
|
|
345
334
|
*
|
|
346
335
|
* 这两件事都只能在启动这一刻做:
|
|
347
336
|
* - **删日志**:宿主没有「删除会话」接口,进程活着时它随时可能去开那些文件;文件不在
|
|
348
|
-
* 就是一次未捕获的 ENOENT
|
|
337
|
+
* 就是一次未捕获的 ENOENT,宿主直接退出。
|
|
349
338
|
* - **擦归档集合**:桌面端的会话列表是「连上宿主时拉的一份快照 + 之后的事件增量」,
|
|
350
339
|
* 进程活着的时候擦集合,宿主会把「归档集合变了」广播给桌面端,桌面端就把它快照里
|
|
351
340
|
* 那些其实已经没有日志、本来被归档藏着的行放回正常列表 —— 用户报的正是「清理后
|
|
@@ -3,18 +3,11 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 凭据包 `dsh-mobile-credentials` 的入口。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* 它在盘上有两代布局:新布局嵌在插件包内(一代自包含,回滚时凭据跟着一起回滚),旧布局与
|
|
7
|
+
* 插件包同级。往哪里落由外壳的资产重映射决定,而外壳在进程启动时就被 Node 缓存住、换不掉;
|
|
8
|
+
* 迁移期新旧外壳可能各按一种布局放置,只认一条路就会「装完激活才炸 Cannot find module」。
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
* (一代自包含,回滚时凭据版本跟着一起回滚);
|
|
10
|
-
* - 旧布局:与插件包同级 —— `node_modules/dsh-mobile-credentials/…`,
|
|
11
|
-
* 代际目录下则是 `<代>/dsh-mobile-credentials/…`。
|
|
12
|
-
*
|
|
13
|
-
* 往哪里落由**外壳**(`lib/shell/ota.js` 的资产重映射)决定,而外壳在进程启动时就被 Node
|
|
14
|
-
* 缓存住、换不掉。布局迁移那一次,旧外壳把凭据放进同级目录、新实现却按嵌套路径 require,
|
|
15
|
-
* 结果是「装完激活才炸 Cannot find module」——热更半装成功,留下的一代还是残缺的。
|
|
16
|
-
*
|
|
17
|
-
* 所以这里两条路都试,谁在就用谁:旧外壳配新实现也能跑起来,布局迁移不再是地雷。
|
|
10
|
+
* 所以这里两条路都试,谁在就用谁。
|
|
18
11
|
*/
|
|
19
12
|
|
|
20
13
|
/** 新布局:`<插件包>/dsh-mobile-credentials/`(本文件在 `<插件包>/lib/shared/`)。 */
|
package/lib/shared/dshtoken.js
CHANGED
|
@@ -24,10 +24,10 @@ function hmacPayload(secret, payload) {
|
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* 签发 LAN/mobile token:<instanceId>.<exp>.<sig>。
|
|
27
|
-
* 缺省 ttlSec=30d
|
|
28
|
-
*
|
|
29
|
-
* @param {string} secret lanSecret
|
|
30
|
-
* @param {string} instanceId 实例 id
|
|
27
|
+
* 缺省 ttlSec=30d,但消费方必须显式传原值(LAN 主进程 3600、30d 处 30*24*3600),本函数不
|
|
28
|
+
* 隐藏调用点的默认值差异。
|
|
29
|
+
* @param {string} secret lanSecret(relay 配置里的密钥)
|
|
30
|
+
* @param {string} instanceId 实例 id(relay 配置)
|
|
31
31
|
* @param {number} ttlSec 有效期(秒),缺省 30*24*3600
|
|
32
32
|
* @param {number} [nowSec] 秒级「当前时间」覆盖(测试/主进程签名用);缺省 Date.now()/1000
|
|
33
33
|
* @returns {string}
|
|
@@ -291,7 +291,7 @@ const MAX_SEALED_BODY = Math.ceil(11 * 1024 * 1024 * 4 / 3);
|
|
|
291
291
|
function readRawBody(req, maxBytes = MAX_SEALED_BODY) {
|
|
292
292
|
// **没有请求体的请求不能等 `end`**:SSE 客户端(XHR)发出来的 GET 有时不会像普通
|
|
293
293
|
// fetch 那样把请求流收尾,而守卫在读体这一步就把处理器挡住了 —— 表现是「手机一直在
|
|
294
|
-
//
|
|
294
|
+
// 重连、宿主侧连开流日志都没有」。GET/HEAD 且既没有 content-length
|
|
295
295
|
// 也没有 transfer-encoding,就是没有体,直接回空。
|
|
296
296
|
const method = String((req && req.method) || 'GET').toUpperCase();
|
|
297
297
|
const headers = (req && req.headers) || {};
|
package/lib/shared/e2ee.js
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* 链路加密(协议 v2)的字节级原语。App 侧有一份逐字节等价实现(`services/e2ee.ts
|
|
5
|
-
*
|
|
4
|
+
* 链路加密(协议 v2)的字节级原语。App 侧有一份逐字节等价实现(`services/e2ee.ts`),两边的
|
|
5
|
+
* 正确性靠同一份测试向量保证(`test/e2ee-vectors.json`),不靠互相阅读代码。
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* 这一层只做「算字节」,不认识路由与业务:信封放在哪里、什么时候加解密由路由层负责。
|
|
8
|
+
* 算法只用三样、两端现成:AES-256-GCM、HKDF-SHA256、HMAC-SHA256。
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `chacha20-poly1305`(实测 `createCipheriv` 直接 `Unknown cipher`,`openssl 0.0.0`)。
|
|
15
|
-
* 换过来之前,每一条协议 v2 的请求在宿主侧都会在解封那一步抛错 —— 表现成扫码配对
|
|
16
|
-
* 一律「配对码错误」、已配对设备全线 401,而两端的离线测试(用真实 node:crypto)
|
|
17
|
-
* 全绿,因为测试进程的 Node 是带 chacha 的那一种。
|
|
18
|
-
* 不引 HPKE、不引新依赖 —— 理由(Node 不暴露 `crypto.hpke`,App 侧也没有
|
|
19
|
-
* 现成的 X25519,而二维码里那张一次性票据已经解决了「怎么秘密交付密钥」)。
|
|
10
|
+
* AEAD 必须用 AES-256-GCM 而不是 ChaCha20-Poly1305:桌面宿主跑在 Electron 里,那份 Node
|
|
11
|
+
* 链接 BoringSSL,`crypto.getCiphers()` 里没有 chacha20-poly1305,解封会直接抛错,表现成
|
|
12
|
+
* 扫码配对一律「配对码错误」。不引 HPKE 也不引新依赖:Node 不暴露 `crypto.hpke`,App 侧也没有
|
|
13
|
+
* 现成的 X25519,而二维码里那张一次性票据已经解决了「怎么秘密交付密钥」。
|
|
20
14
|
*/
|
|
21
15
|
|
|
22
16
|
const crypto = require('node:crypto');
|
|
@@ -1,29 +1,13 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
// 必须反映出这次写入,不能被任何「代读副本」顶掉。
|
|
3
|
+
// 「读己所写」标记:写操作之后紧接着的那次列表刷新必须反映这次写入,不能被任何代读副本顶掉。
|
|
5
4
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// /workspaces 回给手机,根本不连宿主插件;桌面侧 500ms 推一次,所以正常最多
|
|
9
|
-
// 旧 500ms;
|
|
10
|
-
// 2. 宿主插件自己的 10s 会话列表缓存(lib/mobile/sessions.js 的
|
|
11
|
-
// SESSION_LIST_CACHE_MS)。
|
|
12
|
-
// 两层对「每 15s 一次的轮询」都无所谓,但写操作之后的第一次刷新撞上任何一层,就会把
|
|
13
|
-
// 用户刚做完的事显示成没生效。
|
|
5
|
+
// 读请求会经过两层代读副本:sidecar 的镜像快照(桌面侧 500ms 推一次)与宿主插件自己的 10s
|
|
6
|
+
// 会话列表缓存。轮询撞上它们无所谓,写后第一次刷新撞上就会把刚做完的事显示成没生效。
|
|
14
7
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// +17233ms SNAPSHOT /api/mobile/v1/sessions fresh ← 直到 15s 轮询才拿到新列表
|
|
19
|
-
//
|
|
20
|
-
// 于是约定 `?fresh=1`:带这个标记的读跳过所有代读副本,直接回源宿主插件。
|
|
21
|
-
// 轮询、首屏、下拉刷新不带,快路径照旧。
|
|
22
|
-
//
|
|
23
|
-
// 权威实现在本文件;`dsh-mobile-server/lib/fresh-request.js` 是它的手工同步副本,
|
|
24
|
-
// 两侧由 `test/shared-helpers-parity.test.cjs` 钉住。**改一边必须改另一边。**
|
|
25
|
-
// 中转侧同样必须认这个标记:中转自己也持有镜像与响应缓存,它若把带 fresh 的读
|
|
26
|
-
// 当普通读缓存掉,App 的下拉刷新就失去了唯一的自救手段。
|
|
8
|
+
// 所以约定 `?fresh=1`:带这个标记的读跳过所有代读副本、直接回源;轮询 / 首屏 / 下拉刷新不带。
|
|
9
|
+
// 权威实现在本文件,`dsh-mobile-server/lib/fresh-request.js` 是手工同步副本,两边必须一起改;
|
|
10
|
+
// 中转也必须认这个标记,否则 App 就失去了唯一的自救手段。
|
|
27
11
|
|
|
28
12
|
const FRESH_QUERY_RE = /(?:^|[?&])fresh=1(?:&|$)/
|
|
29
13
|
|
package/lib/shared/http-util.js
CHANGED
|
@@ -100,11 +100,10 @@ function sendHtml(res, status, html) {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
|
-
* 读取请求体并 JSON.parse
|
|
103
|
+
* 读取请求体并 JSON.parse;超限 / 非法 JSON / IO 错误 → null。
|
|
104
104
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* JSON」。上限按**明文**判(与线上字节的 4/3 放大分开算,见 §8.3(9))。
|
|
105
|
+
* 协议 v2 的请求体已经被守卫解开了(`req.body`),这里直接返回它:那条请求的字节流早被读走
|
|
106
|
+
* 拿去验签与解封,再读只会拿到空串。上限按明文判(线上字节按 4/3 放大,两边分开算)。
|
|
108
107
|
*
|
|
109
108
|
* @param {object} req node:http IncomingMessage
|
|
110
109
|
* @param {number} [maxBytes] 请求体上限(字节,按明文),缺省 344KB
|
|
@@ -3,21 +3,12 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 清单内容指纹(sessions / workspaces 共用)。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* 两处要用:列表响应带 `revision`,App 连同 `generatedAt` 存进快照就能说清数据是什么时候的;
|
|
7
|
+
* 镜像推送每 500ms 完整构建一次清单,内容没变时比对 revision 就能安全跳过。
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* sessions + workspaces,内容没变时推送一份字节完全相同的 payload 纯属浪费,
|
|
12
|
-
* 比对 revision 就能安全跳过。
|
|
13
|
-
*
|
|
14
|
-
* 为什么用内容哈希而不是自增序号:插件随桌面 harness 频繁重启,内存里的序号每次
|
|
15
|
-
* 都从 0 开始,重启后第一份快照会因为「序号变了」而被当成新内容重推一遍;内容哈希
|
|
16
|
-
* 跨重启稳定(同样的清单 → 同样的 revision),这才是判重想要的性质。
|
|
17
|
-
*
|
|
18
|
-
* 为什么用 sha1 而不是更省的 32 位哈希:这个值会被用来**跳过推送**,一次碰撞就等于
|
|
19
|
-
* 一份该推的新数据被静默丢掉。sha1 足够便宜(清单量级下每 tick 亚毫秒),不值得为
|
|
20
|
-
* 几个字节冒这个风险。截断到 16 个十六进制字符只是为了让日志短一点。
|
|
9
|
+
* 用内容哈希而不是自增序号:插件随桌面 harness 频繁重启,序号每次都从 0 开始,重启后第一份
|
|
10
|
+
* 快照会被当成新内容重推。用 sha1 而不是 32 位哈希:这个值会被用来跳过推送,一次碰撞等于
|
|
11
|
+
* 一份该推的新数据被静默丢掉。截断到 16 个十六进制字符只是为了让日志短一点。
|
|
21
12
|
*/
|
|
22
13
|
|
|
23
14
|
const crypto = require('node:crypto');
|
|
@@ -3,24 +3,15 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 设备级「某个会话的正文水位涨了」信号总线。
|
|
5
5
|
*
|
|
6
|
-
* 与 list-signal
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 列表」时,运行中的助手回复只能等下一次点开该会话才被拉下来。
|
|
6
|
+
* 与 list-signal(清单变了)形状相同,但管的是另一种场景:手机上没打开某个会话时,那条会话
|
|
7
|
+
* 的正文没人订阅,运行中的回复只能等下次点开才拉下来。插件本来就把正文推进了中转镜像,缺的
|
|
8
|
+
* 只是「这个会话的正文又长了,可以来取」这句通知。
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* 「这个会话的正文又长了,你(手机)可以来取」这一句通知,本模块就是那个出口。
|
|
10
|
+
* 清单信号是全局的(一个 revision),这条是按会话的(sessionId → 已广播的消息水位 seq)。
|
|
11
|
+
* 水位取快照里真正装上的消息的最大 seq,与 App 侧缓存水位同一个 seq 空间。
|
|
14
12
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* (见 pusher 的 messageWatermarkOf),也就是 App 侧会话缓存的水位口径(同一个 seq 空间),
|
|
18
|
-
* 手机拿它和自己的缓存水位一比就知道该不该增量拉一次。
|
|
19
|
-
*
|
|
20
|
-
* 为什么是模块级单例:生产端(mirror/pusher.cjs,CJS)与消费端(mobile/events.js,CJS)
|
|
21
|
-
* 都 require 到同一个实例,两边不需要认识对方;测试可以 createSessionSignal() 造独立实例。
|
|
22
|
-
* 为什么订阅者抛错要吞掉:bus 的调用方是镜像推送链,一条写坏的 SSE 连接不能把推送拖垮
|
|
23
|
-
* (真故障会从连接自身的 close 反映出来)。
|
|
13
|
+
* 做成模块级单例:生产端与消费端 require 到同一个实例,互不认识;测试可以自己造实例。
|
|
14
|
+
* 订阅者抛错要吞掉——bus 的调用方是镜像推送链,一条写坏的 SSE 连接不能把推送拖垮。
|
|
24
15
|
*/
|
|
25
16
|
|
|
26
17
|
const NOOP_RELEASE = () => {};
|
package/lib/shared/sse-write.js
CHANGED
|
@@ -1,26 +1,20 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* SSE 写帧的唯一实现:单会话流(lib/mobile/stream.js)与设备级流(lib/mobile/events.js
|
|
5
|
-
* 共用这一份。
|
|
4
|
+
* SSE 写帧的唯一实现:单会话流(lib/mobile/stream.js)与设备级流(lib/mobile/events.js)共用。
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 现在「这条连接上到底积压了多少字节」「软/硬水位怎么判」「SSE 记录长什么样」「稳定
|
|
11
|
-
* 事件 id 怎么算」「写不动时怎么等 drain」都只在这里,两条流只在调用点上声明自己的策略。
|
|
6
|
+
* 合并是因为两份实现会让同一个坑修两遍:宿主 OOM 时先修了会话流那份,隔一轮才发现设备流那份
|
|
7
|
+
* 还漏着。现在积压字节数、软/硬水位、SSE 记录形状、稳定事件 id、等 drain 都只在这里,两条流
|
|
8
|
+
* 只在调用点声明自己的策略。
|
|
12
9
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* 所以「断开重连」是它的正常收敛路径。
|
|
22
|
-
* - 填充:会话流每条记录补 2048 字节 SSE 注释(Android RN 的 XHR 会攒小 chunk,
|
|
23
|
-
* 不补就迟迟不触发 onprogress);设备级流的帧很少、单帧也小,不补。
|
|
10
|
+
* 两条流刻意保留的差异(改之前先看理由):
|
|
11
|
+
* - 丢帧口径:会话流的软水位只丢明确可丢的增量(`assistant/chunk` 草稿,收尾的
|
|
12
|
+
* `assistant/message` 带完整正文);`session/queue`、`session/jobs`、`resync-required` 这些
|
|
13
|
+
* 控制帧没有 seq 但绝不能丢。设备级流的帧全是幂等信号,丢掉后由调用方断流重连并补发。
|
|
14
|
+
* - 写不动时:会话流 await 一次 drain(20s 超时收尾);设备级流直接收尾让客户端重连,重连
|
|
15
|
+
* 时会补发当前状态,所以断开是它的正常收敛路径。
|
|
16
|
+
* - 填充:会话流每条补 2048 字节注释(Android RN 的 XHR 会攒小 chunk,不补迟迟不触发
|
|
17
|
+
* onprogress);设备级流帧少且小,不补。
|
|
24
18
|
*/
|
|
25
19
|
|
|
26
20
|
const crypto = require('crypto');
|
|
@@ -66,8 +60,8 @@ function ssePadding(bytes) {
|
|
|
66
60
|
* 这条连接上真正「还没发出去」的字节数。
|
|
67
61
|
*
|
|
68
62
|
* 只看 `res.writableLength` 是错的:它只算响应对象自己那一层,真正的积压在底下的套接字
|
|
69
|
-
* 写队列里(堆快照里那些 `{chunk, encoding, callback}
|
|
70
|
-
*
|
|
63
|
+
* 写队列里(堆快照里那些 `{chunk, encoding, callback}`)。实测只看前者时闸门从不触发,
|
|
64
|
+
* 内存照涨。所以顺着 socket 一起看,取较大值。
|
|
71
65
|
*/
|
|
72
66
|
function bufferedBytesOf(res) {
|
|
73
67
|
const own = Number(res.writableLength) || 0;
|
|
@@ -2,23 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
// 空清单守卫(工作区 / 会话列表共用)。
|
|
4
4
|
//
|
|
5
|
-
// 「没取到」与「真的什么都没有」在单份快照上长得一模一样:都是
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// `host-session-list-degraded`),守卫则负责兜住「宿主确实回了合法空列表」这种情况
|
|
9
|
-
// ——它和真实删除无法区分,所以按时间兜底。
|
|
5
|
+
// 「没取到」与「真的什么都没有」在单份快照上长得一模一样:都是 `{ ok: true, items: [] }`。
|
|
6
|
+
// 读取侧必须如实报错(见 workspaces.js / sessions.js 的 `*-list-degraded`),守卫负责兜住
|
|
7
|
+
// 「宿主确实回了合法空列表」——单看一份快照区分不了它和真实删除,所以按时间兜底。
|
|
10
8
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// 设备仍显示「在线」(那是另一条独立的 /status 探测线),点不开,且全程零错误提示。
|
|
9
|
+
// 一份空快照若被当成权威清单推给中转,下一次 15s 轮询就会把好数据覆盖成空:侧边栏工作区
|
|
10
|
+
// 整行消失、会话塌进「未分组」,而且全程零错误提示。所以距上一次确认过的非空清单不到
|
|
11
|
+
// graceMs 时拒绝空清单、保留上一份镜像;超过 graceMs 则放行,让真实删除也能收敛。
|
|
15
12
|
//
|
|
16
|
-
//
|
|
17
|
-
// 距上一次确认过的非空清单还不到 graceMs 时拒绝空清单、保留上一份镜像;超过 graceMs
|
|
18
|
-
// 则放行,让真实删除也能收敛,不会把侧边栏永久钉死在旧数据上。
|
|
19
|
-
//
|
|
20
|
-
// 抽成独立模块是为了能被真正单测(早期版本把这段逻辑内联在 lib/index.js 的闭包里,
|
|
21
|
-
// 只能靠 grep 源码做契约测试,行为本身一次都跑不到)。
|
|
13
|
+
// 抽成独立模块是为了能被真正单测。
|
|
22
14
|
|
|
23
15
|
const fs = require('node:fs')
|
|
24
16
|
|
package/lib/shell/device-ota.js
CHANGED
|
@@ -7,12 +7,10 @@ import { movePath, readManifest, sha256Hex, unpackTarGz, verifyManifest } from '
|
|
|
7
7
|
/**
|
|
8
8
|
* OTA 包落在哪。
|
|
9
9
|
*
|
|
10
|
-
* 默认 `<DSH_HOME>/plugin-ota
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* 第一句 `require('@deepseek-ai/dsh-tools')` 就 MODULE_NOT_FOUND(2026-10-17 真机:
|
|
15
|
-
* 0.1.80 装载失败)。落在 profile 里之后,OTA 代与装进来的那一代走**同一条**解析链。
|
|
10
|
+
* 默认 `<DSH_HOME>/plugin-ota`,外壳知道 profile 在哪之后会把它挪进 profile 里面。原因是
|
|
11
|
+
* DSH 宿主对 `profiles/` 树外的模块走原生解析,而宿主提供的 peer 不在 profile 的
|
|
12
|
+
* node_modules 里,OTA 代第一句 require 就 MODULE_NOT_FOUND。落在 profile 里之后,OTA 代与
|
|
13
|
+
* 装进来的那一代走同一条解析链。
|
|
16
14
|
*/
|
|
17
15
|
let storageRoot = ''
|
|
18
16
|
export function configureOtaRoot(dir) {
|
package/lib/shell/manager.js
CHANGED
|
@@ -1,31 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 代际装载器:把「插件实现」当成可以整代替换的东西。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 拉起来的,重启一条命令的事;桌面这边用户得自己退出再打开,而他不想每次都做这件事。
|
|
4
|
+
* 宿主启动时就把 profile 的插件树定死了,改一次插件代码就得完整重启 DSH(Node 缓存
|
|
5
|
+
* realpath);桌面用户不想每次都做这件事,所以要能在进程里换代。
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* 正式版本由 DSH 插件市场装进 profile,紧急 OTA 把完整包落在 profile 里,两者都由这里切换。
|
|
8
|
+
* pnpm 装新版会把 `node_modules/dsh-mobile-plugin` 指向一个全新的 store 目录 ——「新目录 =
|
|
9
|
+
* 新 realpath = 新 URL」,天然绕开 ESM 缓存;所以只要认出 profile 里当前装的是哪一份,把它
|
|
10
|
+
* 当成一代接上去就够了,不需要拷贝或校验。
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* <profile>/node_modules/dsh-mobile-plugin -> .pnpm/dsh-mobile-plugin@0.1.58/node_modules/dsh-mobile-plugin
|
|
14
|
-
*
|
|
15
|
-
* 「新目录 = 新 realpath = 新 URL」,正是当年必须靠 `.live/<releaseId>` 才能拿到的东西
|
|
16
|
-
* (见 `importGeneration` 里 `?v=` 那一段:同一路径换代码是骗不过 ESM 缓存的)。所以这里
|
|
17
|
-
* 只要**认出 profile 里当前装的是哪一份**,把它当成一代接上去就够了 —— 不需要拷贝、
|
|
18
|
-
* 不需要下载,也不需要 sha256 校验(那是市场的活儿)。
|
|
19
|
-
*
|
|
20
|
-
* 一代 = 一个完整包目录,`package.json` 与 `lib/impl.js` 缺一不可:`lib/*.js` 内部有
|
|
21
|
-
* 80 多处相对引用(`../shared/http-util` 之类),必须保持 `lib/` 这一层的相对结构;
|
|
22
|
-
* 实现里还有 `require('../dsh-mobile-credentials/lib/credentials.js')` 这种跨包相对引用,
|
|
23
|
-
* 而凭据包就嵌在插件包目录内部,所以一代是**自包含**的:回滚到旧代时连凭据版本一起
|
|
24
|
-
* 回滚,不会出现「旧插件配新凭据」。
|
|
25
|
-
*
|
|
26
|
-
* 裸包名(`ws`、`@deepseek-ai/...`)依旧沿祖先目录解析:从 `<代>/lib` 往上会经过
|
|
27
|
-
* `<代>/node_modules`、profile 的 `node_modules`,最终落在同一份插件自身依赖与 peer
|
|
28
|
-
* 依赖上,与基础代解析到同一份 real path,不会出现两份实例。
|
|
12
|
+
* 一代 = 一个完整包目录,`package.json` 与 `lib/impl.js` 缺一不可:`lib/*.js` 内部有大量
|
|
13
|
+
* 相对引用,凭据包也嵌在插件包目录内部,所以一代是自包含的——回滚时连凭据版本一起回滚。
|
|
14
|
+
* 裸包名沿祖先目录解析,与基础代落到同一份依赖上。
|
|
29
15
|
*/
|
|
30
16
|
|
|
31
17
|
import fs from 'node:fs'
|
|
@@ -249,8 +235,8 @@ function evictCommonJsTree(roots) {
|
|
|
249
235
|
* 宿主对模块解析有一层拦截:**`profiles/` 树内**的模块,裸包名会先按 profile 的
|
|
250
236
|
* 本地包解析、再回落到宿主自己那份;树外的模块走原生解析。而宿主提供的那批 peer
|
|
251
237
|
* (`@deepseek-ai/dsh-tools` 等)并不在 profile 的 `node_modules` 里 —— 所以 OTA 包
|
|
252
|
-
* 一旦落在树外,那一代 `require('@deepseek-ai/dsh-tools')` 就是 MODULE_NOT_FOUND
|
|
253
|
-
*
|
|
238
|
+
* 一旦落在树外,那一代 `require('@deepseek-ai/dsh-tools')` 就是 MODULE_NOT_FOUND。
|
|
239
|
+
* 位置挪进 profile 后,OTA 代与装进来的那一代
|
|
254
240
|
* 走同一条解析链,不必再靠调用点逐个打补丁。
|
|
255
241
|
*
|
|
256
242
|
* 老位置残留的包顺手搬过来:换代失败时它还是磁盘上的回退包,不能在升级路径上丢。
|
|
@@ -282,8 +268,7 @@ function moveOtaRootIntoProfile() {
|
|
|
282
268
|
* Node 只沿**自己的祖先目录**找 `node_modules`,而 OTA 落在
|
|
283
269
|
* `<DSH_HOME>/plugin-ota/<version>/` —— 这条路径上没有任何一份 `node_modules`。于是
|
|
284
270
|
* `lib/impl.js` 第一句 `import { z } from '@deepseek-ai/schemastery'` 就报
|
|
285
|
-
* `Cannot find package
|
|
286
|
-
* 外壳按规矩回滚到旧代,机器上什么都没变)。
|
|
271
|
+
* `Cannot find package`,一代整个装不起来。外壳按规矩回滚到旧代,机器上什么都没变。
|
|
287
272
|
*
|
|
288
273
|
* 修法是补上基础代本来就有、而 OTA 路径上没有的那两层软链:
|
|
289
274
|
* `<version>/node_modules` → profile 的 `node_modules`
|
package/lib/shell/ota-package.js
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OTA 包(npm tarball)的读取、校验与落盘。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 长路径就直接抛「不支持的 tar 项」;反过来,包里少几个文件它又看不出来,于是装上去之后
|
|
7
|
-
* 才在某一代运行时报「找不到某个 JS 文件」。这里把两件事一起解决:
|
|
4
|
+
* 这一层只为「换一台机器也照样能装」:手写解包只认普通文件和目录,遇到 PAX 扩展头、GNU 长名
|
|
5
|
+
* 或长路径就抛错,而包里少几个文件又看不出来。这里把两件事一起解决:
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* 每个文件的路径和字节数),解包后逐项核对。缺文件、大小不符都在**激活之前**失败,
|
|
14
|
-
* 错误里直接点名是哪个文件。
|
|
7
|
+
* 1. 解包要认得真实世界里的 tar:ustar 的 prefix、PAX、GNU 长名都要能读;软链接、硬链接、
|
|
8
|
+
* 设备文件明确拒绝并说清原因,不静默跳过。
|
|
9
|
+
* 2. 装完要能证明完整:包里带一份发布时生成的清单(版本 + 每个文件的路径与字节数),解包后
|
|
10
|
+
* 逐项核对,缺文件或大小不符都在激活之前失败并点名。
|
|
15
11
|
*
|
|
16
|
-
*
|
|
17
|
-
* DSH 的机器上被解出来,不能要求它先装点别的),而 ustar 的头部格式是固定的 512 字节。
|
|
12
|
+
* 手写而不是引依赖:这是发布链路上要能在一台刚装好 DSH 的机器上跑的自包含代码。
|
|
18
13
|
*/
|
|
19
14
|
|
|
20
15
|
import crypto from 'node:crypto'
|
package/lib/shell/runtime.js
CHANGED
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 代际运行时:把「外壳」和「某一代实现」缝在一起,并负责换代那一刻的秩序。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 照常 `ctx.effect(...)` / `ctx.webServer.register(...)` / `ctx.reflect.provide(...)`。
|
|
4
|
+
* 外壳只做两件事:启动时激活版本最高的一代,以及把换代请求转给这里。实现侧拿到的是一个普通
|
|
5
|
+
* 的 cordis `Context`,不知道自己是被外壳装进来的。
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* 换代必须串行、且先停后起:一代在 `apply` 里会注册同一条路径的路由(撞路径直接抛错)、绑
|
|
8
|
+
* sidecar、起镜像推送,只能先 dispose 旧代(每个 `ctx.effect` 反向清理),再装新代。
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* 卸旧的」——只能先把旧代的 fiber dispose 掉(它的每个 `ctx.effect` 会反向执行清理:路由
|
|
13
|
-
* 摘掉、子进程收掉),再装新的。这个文件里的 `swapping` 锁就是保证同一时刻只有一次换代。
|
|
14
|
-
*
|
|
15
|
-
* ## 让路
|
|
16
|
-
*
|
|
17
|
-
* 手机那边随时可能有一个请求在读事件流。直接 dispose 会把 `ctx.effect` 的清理跑在别人的
|
|
18
|
-
* 请求中间。所以换代前先等 `inFlight` 归零,最多等 `DRAIN_TIMEOUT_MS`(等不到也照换——
|
|
19
|
-
* 卡住的请求不该拖住一次修复性更新)。
|
|
10
|
+
* 换代前先等 `inFlight` 归零,最多等 `DRAIN_TIMEOUT_MS`——把清理跑在别人的请求中间会出事,
|
|
11
|
+
* 但卡住的请求也不该拖住一次修复性更新。
|
|
20
12
|
*/
|
|
21
13
|
|
|
22
14
|
import { importGeneration, invalidateGeneration, resolveGeneration, resolveGenerations } from './manager.js'
|
|
@@ -199,7 +191,7 @@ export function createRuntime() {
|
|
|
199
191
|
* 旧代先停、新代后起;新代起不来就回滚把旧代装回去,并把错误留在 `lastError` 里——
|
|
200
192
|
* 「更新把自己弄挂了」必须能被看见,而不是静默少个插件。
|
|
201
193
|
*
|
|
202
|
-
* `options.reread`
|
|
194
|
+
* `options.reread` 是给**开发机**的那条路:版本号与目录都没变,只是盘上
|
|
203
195
|
* 那几个文件被改了。默认会走上面那句「身份一致就直接返回」,于是改完代码再触发也没有
|
|
204
196
|
* 任何变化 —— 开发时看到的「改了不生效」就是这么来的。带上它就先把这一代从
|
|
205
197
|
* `imported` 里作废、再让 `importGeneration` 清掉 CJS 子树,强制重新读盘。
|
|
@@ -288,7 +280,7 @@ export function createRuntime() {
|
|
|
288
280
|
/**
|
|
289
281
|
* 第一次激活;最高代装载失败时按优先级尝试保留的回退代;全失败则按固定间隔重试有限次。
|
|
290
282
|
*
|
|
291
|
-
*
|
|
283
|
+
* 为什么要重试:一次失败 = **整张控制面路由表都不在**,而设置页
|
|
292
284
|
* 的「移动端」照样画得出来(客户端半边按包扫描,不看宿主 fiber 活没活),于是现场只表现成
|
|
293
285
|
* 「读取宿主状态失败: HTTP 404」。失败最常见的原因恰好是**可能自己好**的:注入的服务
|
|
294
286
|
* (typertGateway / apiProxy)比本插件晚就位、撞上 20s 装载上限,或者第一次装载撞上
|
|
@@ -1,30 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 换代之后的自检:新版装起来之后,手机要打的那批接口真的能用吗?不能就退回上一代。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 这条不能交给手机:装坏的后果是 App 连不上这台设备,那一刻手机上的回滚按钮发不出去。
|
|
5
|
+
* 必须由还活着的宿主做完,手机只在事后读到「上次装失败了,已自动退回 X」。
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* 判据是打本机的 `127.0.0.1:<宿主端口>`:200 且 `service: 'dsh-mobile-plugin'` 算活;401 也
|
|
8
|
+
* 算活(守卫先核 token,说明实现与守卫都在);404 / 别的 service / 超时算死。连不上
|
|
9
|
+
* (ECONNREFUSED)不算数——那是宿主 webServer 自己没起来,与新代无关,退回去也救不了,
|
|
10
|
+
* 如实报 `skip`。
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* 不看日志、不猜堆栈,直接打本机的 `127.0.0.1:<宿主端口>`:
|
|
13
|
-
*
|
|
14
|
-
* · 回 200 + `service: 'dsh-mobile-plugin'` → 活,路由挂上了;
|
|
15
|
-
* · 回 401 → 也活:移动端守卫先核 token 再谈路由,没带 token 回 401 说明实现与守卫都在;
|
|
16
|
-
* · 回 404 / 别的 service → **死了**:端口在、但插件那批路由没挂上,手机打过去就是这个 404;
|
|
17
|
-
* · 超时 → **死了**:连上了但不回包,多半是某个 handler 卡在等一个永远不来的依赖;
|
|
18
|
-
* · 连不上(ECONNREFUSED)→ **不算数**:那是宿主 webServer 自己没起来,与新代无关,
|
|
19
|
-
* 退回去也救不了。如实报 `skip`。
|
|
20
|
-
*
|
|
21
|
-
* 最后那一条是这次刻意分开的:把「宿主没在监听」当成「新版坏了」会退掉一次其实成功的
|
|
22
|
-
* 更新(开发机上跑测试时就是这么一种现场:假 webServer 有个端口号,但没有任何东西在听)。
|
|
23
|
-
*
|
|
24
|
-
* ## 端口从哪儿来
|
|
25
|
-
*
|
|
26
|
-
* 用宿主的 `ctx.webServer.port`(运行中那一份),不是配置里的默认值 —— 端口被改过之后,
|
|
27
|
-
* 配置与实绑可能不同,按配置去问会问到一个没人监听的地方,于是「自检失败」全是假的。
|
|
12
|
+
* 端口用 `ctx.webServer.port`(运行中那份),不是配置里的默认值:端口被改过之后按配置去问
|
|
13
|
+
* 会问到一个没人监听的地方。
|
|
28
14
|
*/
|
|
29
15
|
|
|
30
16
|
/** 自检的超时上限:本机环回,正常几十毫秒。给 3 秒已经很宽。 */
|