dsh-crossterm 0.9.12 → 0.9.13
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/CHANGELOG.md +13 -0
- package/MAINTAINERS.md +1 -1
- package/README.md +11 -11
- package/components/session/manifest.json +1 -1
- package/lib/bus.js +28 -10
- package/lib/index.js +15 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## 0.9.13 — 2026-10-20
|
|
2
|
+
|
|
3
|
+
### 修复:链台账 fail-closed(YOCE_C 冒烟报的两处边角 + 自查的第三处)
|
|
4
|
+
|
|
5
|
+
- 空成员台账:`chains/<code>.json` 的 `members` 为空时,`leaveChainNode()` 已把文件删掉、返回 `removed:0`,但 `unlinkChain()` 只认 `removed>0`,于是把「文件已删」报成 `ok:false`。改为 `removed>0 || empty` 都算成功,返回 `{ ok:true, empty:true }`。
|
|
6
|
+
- presence 读不到不再当全员离线:`dropChainIfOrphan()` 原来用 `listNodes().catch(()=>[])`,`presence/` 目录读失败时拿到空列表,与「全员离线」无法区分,会把还在线的无主链删掉。`lib/bus.js` 拆出 `listNodesStrict()`(读失败抛错),孤儿判定改用严格读取:读不到就直接返回 `{ dropped:false, reason:'presence-unreadable' }`,`term_unlink` 回 `ok:false` 并说明原因;`listNodes()` 保留为展示用 fail-open(读不到返回空列表)。
|
|
7
|
+
- 广播会顺手修剪台账:`term_broadcast` 的 `pruneChain()` 在 `kept=0` 时删台账文件,同样吃 `listNodes()` 的 fail-open —— presence 读失败时一次广播就能把整条链删掉。改为严格读取,读不到就中止广播并返回 `ok:false`,台账不动。
|
|
8
|
+
- 测试:新增 `test/chain.test.mjs`(空台账 / 无主离线 / 有成员在线 / presence 读不到下的 unlink 与 broadcast / 严格读取抛错),并修正 `test/plugin.test.mjs` 工具清单漏掉的 `term_cloud`。
|
|
9
|
+
- 外部回归:YOCE_C 的 `regress.mjs` 在 rebase 后的本树跑 **5/5 全绿**(orphan-offline / member-online / empty-members / unlink-presence-unreadable / broadcast-presence-unreadable);同一脚本在 0.9.10 与已发布 0.9.12 上均为 2/5。
|
|
10
|
+
- 防御性补注:`pruneChain()` 上方写明「`nodes` 必须来自严格读取」,新增调用点不得传 fail-open 的空列表。
|
|
11
|
+
- 基线:0.9.12(已发布)只含「上一任」修复,不含本版三处改动;本版在其代码之上 rebase,避免版本号与内容错位。
|
|
12
|
+
- 组件版本:跨会话 `0.5.2` → `0.5.3`。
|
|
13
|
+
|
|
1
14
|
## 0.9.12 — 2026-10-20
|
|
2
15
|
|
|
3
16
|
### 修复:负责人记录的「上一任」取错人(只改代码一处)
|
package/MAINTAINERS.md
CHANGED
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
| 组件(插件页顺序) | 当前版本 | 负责人 | 范围 | 规矩 |
|
|
101
101
|
|---|---|---|---|---|
|
|
102
102
|
| ① 跨端互通(自更新) | v0.9.3 | `3195b037` | `components/ui/**`、`client/**`、`locale/**`、`lib/web.js` | 组件版本号在 `components/ui/manifest.json` |
|
|
103
|
-
| ② 跨会话 | v0.5.
|
|
103
|
+
| ② 跨会话 | v0.5.3 | `3195b037` | `components/session/**`、`lib/bus.js` 的链/邮件 | 同上 |
|
|
104
104
|
| ③ 跨工作区 | v0.1.0 | `3195b037` | `components/terminal/**`、presence / `term_send` | 同上 |
|
|
105
105
|
| ④ 跨设备 | v0.4.0 | `3195b037` | `components/device/**`、`lib/cloud/**` | 同上 |
|
|
106
106
|
| ⑤ 自动重启 | v0.3.29 | `3195b037`(暂管;原 `a3e9f80e` 已于 2026-10-20 19:33 交回) | `components/restart/**`(`core.js` / `runtime.js` / `scripts/**`) | 不单独发版;新负责人等后续交接时任命;只动它就把它的版本号加一 |
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
|
|
|
19
19
|
| 顺序 | 组件名称 | 版本 | 作用 | 工具 |
|
|
20
20
|
|---|---|---|---|---|
|
|
21
21
|
| ① | 跨端互通(入口/自更新) | v0.9.3 | 自更新开关:打开后定期检查 npm 新版本并自动安装(默认关;装完要重启 DSH 生效)。关掉它只失去卡片与自动更新,下面四个组件照常工作。裸包名行,承载插件标题/简介与配置卡片,不挂工具 | — |
|
|
22
|
-
| ② | 跨会话 | v0.5.
|
|
22
|
+
| ② | 跨会话 | v0.5.3 | 同一终端里的多个会话之间互通:特征码链、内联邮件、收件箱 | `term_link` `term_unlink` `term_broadcast` `term_inbox` `term_config` |
|
|
23
23
|
| ③ | 跨工作区 | v0.1.0 | 在本机不同工作区、不同会话之间互发邮件与文件(跨终端本轮不做,暂无条件) | `term_whoami` `term_list` `term_send` `term_send_files` |
|
|
24
24
|
| ④ | 跨设备 | v0.4.0 | 经云端中转站,在多台机器的 DSH 之间互发邮件与文件 | `term_cloud` |
|
|
25
25
|
| ⑤ | 自动重启 | v0.3.29 | 改完插件/配置后让 DSH 自己重启一次,并自动拉起离线会话(原 `dsh-restart` 插件,0.9.0 起并入) | `dsh_restart` |
|
|
@@ -30,7 +30,14 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
|
|
|
30
30
|
>
|
|
31
31
|
> 组件版本号怎么升:**只在该组件自己的文件改动时升它**,跟着插件一起发版(例:只改自动重启 → `components/restart` 的 `0.3.28` → `0.3.29`,其余四个不动)。插件总版本仍按发版技能的三分位规则。
|
|
32
32
|
|
|
33
|
-
**本版更新(0.9.
|
|
33
|
+
**本版更新(0.9.13)**
|
|
34
|
+
|
|
35
|
+
- **链台账改 fail-closed**:`presence/` 读不到(目录被删、或变成同名文件)时,`term_unlink ... all=true` 与 `term_broadcast` 以前会把「读不到在线节点」当成「全员离线」,把还在线的链台账删掉。现在改用严格读取:读不到就拒绝删除并回 `ok:false`(广播中止,台账不动)。
|
|
36
|
+
- **空成员台账不再误报失败**:链上成员为空、文件已被删时,`term_unlink` 以前回 `ok:false`;现在回 `ok:true` + `empty:true`。
|
|
37
|
+
- 无主离线台账的清理语义不变(仍回 `droppedChain:true`);新增 `test/chain.test.mjs` 覆盖四条路径。
|
|
38
|
+
- 组件版本:跨会话 0.5.2 → **0.5.3**,其余组件不动。
|
|
39
|
+
|
|
40
|
+
**上一版(0.9.12)**
|
|
34
41
|
|
|
35
42
|
- **修「上一任」取错人**:`/about` 的维护者信息与 `logs/ownership.log` 里,「上一任」以前会显示成最早的初版负责人 `6ef6da30`;现在按离任时间取最近的一位(本次交接后应为 `b6aa099b`)。
|
|
36
43
|
- 接口多返回一个 `previousAll`(完整历任列表),当前负责人与各组件版本不变,功能零变化。
|
|
@@ -50,16 +57,9 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
|
|
|
50
57
|
- 起因:0.9.9 修好了「台账有本机成员却退不掉」,但还有一种「本机根本没有成员、别的机器早解散了」的残档,之前只能人工删文件(C 盘写入要审批)。
|
|
51
58
|
- 组件版本:跨会话 0.5.1 → **0.5.2**,其余组件不动。
|
|
52
59
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
- **修「幽灵链成员」退不掉**(YOCE_C 2026-10-20 报的坑):`term_unlink ... all=true` 以前只遍历内存里的 links,遇到「链台账里还留着本终端成员、但 links 已没有这条记录」就永远退不掉(一直回「本终端没有接在特征码上」)。
|
|
56
|
-
- 现在 `all=true` 在 links 为空时也会按 **nodeId** 把本终端在这串码上的成员从台账里摘掉,返回值带 `ghosts`;成员清空时链文件自动删除。
|
|
57
|
-
- `chainSweep` 每 6 拍自愈一次:本节点在台账里有、links 里却没有的成员会被摘掉,并记一行日志「chain sweep: 摘掉幽灵成员」。
|
|
58
|
-
- 显式传 `sessionId` 的行为不变——只退那一个,不动其它会话(笔误不会误删)。
|
|
59
|
-
- 组件版本:跨会话 0.5.0 → **0.5.1**,其余组件不动。
|
|
60
|
-
|
|
61
|
-
**更早(0.9.8 / 0.9.7 / 0.9.6 / 0.9.5 / 0.9.4 / 0.9.3 / 0.9.2 / 0.9.0 / 0.8.14 / 0.8.13 / 0.8.12 / 0.8.11 / 0.8.10 / 0.8.9 / 0.8.8 / 0.8.7 / 0.8.6 / 0.8.5 / 0.8.0)**
|
|
60
|
+
**更早(0.9.9 / 0.9.8 / 0.9.7 / 0.9.6 / 0.9.5 / 0.9.4 / 0.9.3 / 0.9.2 / 0.9.0 / 0.8.14 / 0.8.13 / 0.8.12 / 0.8.11 / 0.8.10 / 0.8.9 / 0.8.8 / 0.8.7 / 0.8.6 / 0.8.5 / 0.8.0)**
|
|
62
61
|
|
|
62
|
+
- 0.9.9:修「幽灵链成员」退不掉——`all=true` 按 nodeId 把本终端在台账里的成员摘掉(返回带 `ghosts`),`chainSweep` 每 6 拍自愈;组件 0.5.0 → 0.5.1。
|
|
63
63
|
- 0.9.8:自动重启组件维护权交回、暂由插件负责人直管;只改文档。
|
|
64
64
|
- 0.9.7:组件版本按机主要求重新定起点(跨会话 v0.5.0 / 跨设备 v0.4.0 / 跨工作区 v0.1.0)。
|
|
65
65
|
- 0.9.6:去掉「跨账号」组件,插件页变成 5 个组件。
|
package/lib/bus.js
CHANGED
|
@@ -23,7 +23,7 @@ import path from 'node:path'
|
|
|
23
23
|
import crypto from 'node:crypto'
|
|
24
24
|
import { rotateIfNeeded } from './logs.js'
|
|
25
25
|
|
|
26
|
-
export const VERSION = '0.9.
|
|
26
|
+
export const VERSION = '0.9.13'
|
|
27
27
|
|
|
28
28
|
/** DSH 的家目录:与宿主其它状态(sessions / profiles)同一个根,多终端天然共享 */
|
|
29
29
|
export function dshHome() {
|
|
@@ -402,14 +402,13 @@ export class Bus {
|
|
|
402
402
|
await fsp.unlink(path.join(this.presenceDir, `${nodeId}.json`)).catch(() => {})
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
-
|
|
405
|
+
/**
|
|
406
|
+
* presence 目录的严格读取:读不到目录就抛错。
|
|
407
|
+
* 只给「要据此决定删不删东西」的调用方用 —— 读失败绝不能被当成全员离线。
|
|
408
|
+
*/
|
|
409
|
+
async listNodesStrict() {
|
|
406
410
|
const out = []
|
|
407
|
-
|
|
408
|
-
try {
|
|
409
|
-
names = await fsp.readdir(this.presenceDir)
|
|
410
|
-
} catch {
|
|
411
|
-
return out
|
|
412
|
-
}
|
|
411
|
+
const names = await fsp.readdir(this.presenceDir)
|
|
413
412
|
for (const name of names) {
|
|
414
413
|
if (!name.endsWith('.json')) continue
|
|
415
414
|
const data = await readJson(path.join(this.presenceDir, name))
|
|
@@ -420,6 +419,15 @@ export class Bus {
|
|
|
420
419
|
return out
|
|
421
420
|
}
|
|
422
421
|
|
|
422
|
+
/** 展示用:读不到就返回空列表(fail-open 只允许用在这里,不能用来做删除决策)。 */
|
|
423
|
+
async listNodes() {
|
|
424
|
+
try {
|
|
425
|
+
return await this.listNodesStrict()
|
|
426
|
+
} catch {
|
|
427
|
+
return []
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
423
431
|
async cleanupStale(now = Date.now()) {
|
|
424
432
|
const limit = this.cfg.offlineAfterMs * 3
|
|
425
433
|
const removed = []
|
|
@@ -554,7 +562,11 @@ export class Bus {
|
|
|
554
562
|
const chain = await readJson(file)
|
|
555
563
|
if (!chain || !Array.isArray(chain.members)) return { code, dropped: false, reason: 'no-chain' }
|
|
556
564
|
if (chain.members.some((m) => m.nodeId === nodeId)) return { code, dropped: false, reason: 'self-member' }
|
|
557
|
-
const nodes = await this.
|
|
565
|
+
const nodes = await this.listNodesStrict().catch(() => undefined)
|
|
566
|
+
if (!nodes) {
|
|
567
|
+
// presence 目录读不到 ≠ 全员离线:宁可留着台账,也不能把还在线的链删掉(fail-closed)
|
|
568
|
+
return { code, dropped: false, reason: 'presence-unreadable', members: chain.members.length }
|
|
569
|
+
}
|
|
558
570
|
const alive = new Map(nodes.map((n) => [n.nodeId, Number(n.updatedAt) || 0]))
|
|
559
571
|
const limit = Date.now() - this.cfg.offlineAfterMs * 3
|
|
560
572
|
const anyAlive = chain.members.some((m) => (alive.get(m.nodeId) || 0) >= limit)
|
|
@@ -563,7 +575,13 @@ export class Bus {
|
|
|
563
575
|
return { code, dropped: true, members: chain.members.length }
|
|
564
576
|
}
|
|
565
577
|
|
|
566
|
-
/**
|
|
578
|
+
/**
|
|
579
|
+
* 剔掉已经不存在的成员(进程退出 / 长时间没心跳)。
|
|
580
|
+
*
|
|
581
|
+
* 注意:`nodes` 必须是「确定读到了 presence」的列表。传空数组(例如读失败被吞成 `[]`)
|
|
582
|
+
* 会让 kept 归零、把整份台账删掉。当前唯一调用点是 `lib/index.js` 的 broadcast,
|
|
583
|
+
* 它用 `listNodesStrict()` 前置把关、读不到就中止;新增调用点也必须照做。
|
|
584
|
+
*/
|
|
567
585
|
async pruneChain(code, nodes) {
|
|
568
586
|
const chain = await readJson(this.chainFile(code))
|
|
569
587
|
if (!chain || !Array.isArray(chain.members)) return undefined
|
package/lib/index.js
CHANGED
|
@@ -1187,9 +1187,12 @@ class CrossTerm {
|
|
|
1187
1187
|
// 显式指定 sessionId 时不动 —— 那可能是笔误,不能顺手清别的会话。
|
|
1188
1188
|
if (all) {
|
|
1189
1189
|
const ghost = await this.bus.leaveChainNode(norm, this.nodeId).catch(() => undefined)
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1190
|
+
// ghost.empty 且 removed=0 = 台账里本来就没有成员、文件已被 leaveChainNode 删掉;
|
|
1191
|
+
// 只看 removed>0 会把「文件已删」报成失败(YOCE_C 2026-10-20 报的 addendum)。
|
|
1192
|
+
if (ghost && (ghost.removed > 0 || ghost.empty)) {
|
|
1193
|
+
const how = ghost.removed > 0 ? `ghost ${ghost.removed}` : 'empty chain'
|
|
1194
|
+
await this.note(`unlinked ${norm} [${how}]`)
|
|
1195
|
+
return { ok: true, code: norm, removed: ghost.removed || 0, ghosts: ghost.removed || 0, empty: !!ghost.empty, sessions: [] }
|
|
1193
1196
|
}
|
|
1194
1197
|
// 本终端在台账里也没有成员:如果这串码上的成员全部离线,就当无主台账清掉(0.9.10)
|
|
1195
1198
|
const orphan = await this.bus.dropChainIfOrphan(norm, this.nodeId).catch(() => undefined)
|
|
@@ -1197,6 +1200,9 @@ class CrossTerm {
|
|
|
1197
1200
|
await this.note(`unlinked ${norm} [orphan chain, ${orphan.members} 个离线成员]`)
|
|
1198
1201
|
return { ok: true, code: norm, removed: 0, droppedChain: true, empty: true, sessions: [] }
|
|
1199
1202
|
}
|
|
1203
|
+
if (orphan && orphan.reason === 'presence-unreadable') {
|
|
1204
|
+
return { ok: false, detail: `读不到本机 presence 目录,无法确认「${norm}」的成员是否在线,为免误删先不动它的台账;稍后再试一次` }
|
|
1205
|
+
}
|
|
1200
1206
|
}
|
|
1201
1207
|
return { ok: false, detail: `本终端${scope.length ? `的会话 ${this.labelOf(scope[0])}` : ''}没有接在特征码「${norm}」上` }
|
|
1202
1208
|
}
|
|
@@ -1221,7 +1227,12 @@ class CrossTerm {
|
|
|
1221
1227
|
const body = typeof text === 'string' ? text.trim() : ''
|
|
1222
1228
|
const list = (Array.isArray(files) ? files : (files ? [files] : [])).filter((p) => typeof p === 'string' && p.trim())
|
|
1223
1229
|
if (!body && !list.length) return { ok: false, detail: 'text 和 files 至少要给一个' }
|
|
1224
|
-
const nodes = await this.bus.
|
|
1230
|
+
const nodes = await this.bus.listNodesStrict().catch(() => undefined)
|
|
1231
|
+
if (!nodes) {
|
|
1232
|
+
// broadcast 会顺手 pruneChain(kept=0 时删台账)。presence 读失败时若按空列表修剪,
|
|
1233
|
+
// 等于把还在线的链当成全员离线删掉 —— 这里直接中止(fail-closed)。
|
|
1234
|
+
return { ok: false, detail: `读不到本机 presence 目录,无法确认「${norm}」的成员是否在线;本次不广播、也不修剪台账,稍后重试` }
|
|
1235
|
+
}
|
|
1225
1236
|
const chain = await this.bus.pruneChain(norm, nodes)
|
|
1226
1237
|
if (!chain || !chain.members?.length) {
|
|
1227
1238
|
return { ok: false, detail: `特征码「${norm}」上没有活着的会话;先用 term_link 让别人接进来` }
|