dsh-crossterm 0.9.8 → 0.9.10
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 +21 -0
- package/README.md +15 -8
- package/components/session/manifest.json +1 -1
- package/lib/bus.js +40 -1
- package/lib/index.js +26 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
## 0.9.10 — 2026-10-20
|
|
2
|
+
|
|
3
|
+
### 修复:无主链台账收口(清除所有特征码的最后一步)
|
|
4
|
+
|
|
5
|
+
- 现象:本机 `links` 里没有某串码的记录、`chains/<code>.json` 里只剩别的机器早已下线的成员,`term_unlink all=true` 只能回「本终端没有接在特征码上」,台账留在磁盘上没人清。
|
|
6
|
+
- 修法:`lib/bus.js` 新增 `dropChainIfOrphan(code, nodeId)` —— 本节点不在链上、且链上所有成员节点都离线(超过 `offlineAfterMs * 3`)时删掉该链文件;`unlinkChain()` 的 ghost 分支在上一步没摘到成员时调用它,成功返回 `{ ok:true, droppedChain:true }`。
|
|
7
|
+
- 安全性:本节点是成员、或有任一成员在线时一律不删;探针 `probe-0910.mjs` 10 项覆盖四种分支。
|
|
8
|
+
- 组件版本:跨会话 `0.5.1` → `0.5.2`。
|
|
9
|
+
|
|
10
|
+
## 0.9.9 — 2026-10-20
|
|
11
|
+
|
|
12
|
+
### 修复:幽灵链成员退不掉(YOCE_C 报告)
|
|
13
|
+
|
|
14
|
+
- 现象:`term_unlink code="X" all=true` 回「本终端没有接在特征码「X」上」,但链台账里确实还留着本终端成员;只能先 `term_link` 补回 links 再 `term_unlink` 绕开。
|
|
15
|
+
- 根因:`unlinkChain()` 只遍历内存 `this.links`(`lib/index.js`),台账里的成员它看不见;`leaveChain()` 只能按 `nodeId::sessionId` 精确删。
|
|
16
|
+
- 修法:
|
|
17
|
+
- `lib/bus.js` 新增 `leaveChainNode(code, nodeId)`:按 nodeId 摘掉该节点在链上的全部成员,空了删链文件。
|
|
18
|
+
- `lib/index.js` 的 `unlinkChain()`:`all=true` 且 links 无记录时改走 `leaveChainNode`,成功则返回 `{ ok:true, ghosts, removed }`。
|
|
19
|
+
- `chainSweep` 加自愈:`linksLoaded` 之后,本节点在台账里有、links 里没有的成员会被摘掉并记日志。
|
|
20
|
+
- 组件版本:跨会话 `0.5.0` → `0.5.1`。
|
|
21
|
+
|
|
1
22
|
## 0.9.8 — 2026-10-20
|
|
2
23
|
|
|
3
24
|
- 自动重启组件维护权交回:原负责人会话「自动重启」(`a3e9f80e`)按机主指示即日起停止改动 `components/restart/**`;`MAINTAINERS.md` 的 `modules.restart` 改为插件负责人 `b6aa099b` 暂管,新负责人等后续交接时再任命。
|
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.2 | 同一终端里的多个会话之间互通:特征码链、内联邮件、收件箱 | `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,18 +30,25 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
|
|
|
30
30
|
>
|
|
31
31
|
> 组件版本号怎么升:**只在该组件自己的文件改动时升它**,跟着插件一起发版(例:只改自动重启 → `components/restart` 的 `0.3.28` → `0.3.29`,其余四个不动)。插件总版本仍按发版技能的三分位规则。
|
|
32
32
|
|
|
33
|
-
**本版更新(0.9.
|
|
33
|
+
**本版更新(0.9.10)**
|
|
34
34
|
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
35
|
+
- **「清除所有特征码」最后一步收口**:本终端已经不在某串码上、links 里也没有记录、而这串码上的成员全部离线时,`term_unlink ... all=true` 现在会把这份无主台账一并删掉(返回 `droppedChain:true`),不用再人工去删 `chains/<code>.json`。
|
|
36
|
+
- 判定很保守:本节点还在链上、或有任何一个成员节点在线(含刚心跳过),都**不会**删;只清「谁都不在线」的残档。
|
|
37
|
+
- 起因:0.9.9 修好了「台账有本机成员却退不掉」,但还有一种「本机根本没有成员、别的机器早解散了」的残档,之前只能人工删文件(C 盘写入要审批)。
|
|
38
|
+
- 组件版本:跨会话 0.5.1 → **0.5.2**,其余组件不动。
|
|
38
39
|
|
|
39
|
-
**上一版(0.9.
|
|
40
|
+
**上一版(0.9.9)**
|
|
40
41
|
|
|
41
|
-
-
|
|
42
|
+
- **修「幽灵链成员」退不掉**(YOCE_C 2026-10-20 报的坑):`term_unlink ... all=true` 以前只遍历内存里的 links,遇到「链台账里还留着本终端成员、但 links 已没有这条记录」就永远退不掉(一直回「本终端没有接在特征码上」)。
|
|
43
|
+
- 现在 `all=true` 在 links 为空时也会按 **nodeId** 把本终端在这串码上的成员从台账里摘掉,返回值带 `ghosts`;成员清空时链文件自动删除。
|
|
44
|
+
- `chainSweep` 每 6 拍自愈一次:本节点在台账里有、links 里却没有的成员会被摘掉,并记一行日志「chain sweep: 摘掉幽灵成员」。
|
|
45
|
+
- 显式传 `sessionId` 的行为不变——只退那一个,不动其它会话(笔误不会误删)。
|
|
46
|
+
- 组件版本:跨会话 0.5.0 → **0.5.1**,其余组件不动。
|
|
42
47
|
|
|
43
|
-
**更早(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)**
|
|
48
|
+
**更早(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)**
|
|
44
49
|
|
|
50
|
+
- 0.9.8:自动重启组件维护权交回、暂由插件负责人直管;只改文档。
|
|
51
|
+
- 0.9.7:组件版本按机主要求重新定起点(跨会话 v0.5.0 / 跨设备 v0.4.0 / 跨工作区 v0.1.0)。
|
|
45
52
|
- 0.9.6:去掉「跨账号」组件,插件页变成 5 个组件。
|
|
46
53
|
- 0.9.5:组件「跨终端」改名「跨工作区」(跨终端本轮不做)。
|
|
47
54
|
- 0.9.3:组件表补全六个组件,入口名从 `ui` 改成「跨端互通(入口)」,组件版本号各升各的第一次落地。
|
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.10'
|
|
27
27
|
|
|
28
28
|
/** DSH 的家目录:与宿主其它状态(sessions / profiles)同一个根,多终端天然共享 */
|
|
29
29
|
export function dshHome() {
|
|
@@ -524,6 +524,45 @@ export class Bus {
|
|
|
524
524
|
return { code, removed }
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
+
/**
|
|
528
|
+
* 按代码 + 节点退出:把这个节点在链上的**全部**成员一次性摘掉。
|
|
529
|
+
* 用途是清「链台账里还有成员、但本机 links 已经没有记录」的幽灵成员 ——
|
|
530
|
+
* 0.9.9 之前 unlinkChain 只遍历内存 this.links,这种残留永远退不掉(YOCE_C 2026-10-20 报的坑)。
|
|
531
|
+
*/
|
|
532
|
+
async leaveChainNode(code, nodeId) {
|
|
533
|
+
const file = this.chainFile(code)
|
|
534
|
+
const chain = await readJson(file)
|
|
535
|
+
if (!chain || !Array.isArray(chain.members)) return { code, removed: 0, found: false }
|
|
536
|
+
const before = chain.members.length
|
|
537
|
+
chain.members = chain.members.filter((m) => m.nodeId !== nodeId)
|
|
538
|
+
const removed = before - chain.members.length
|
|
539
|
+
if (!chain.members.length) {
|
|
540
|
+
await fsp.unlink(file).catch(() => {})
|
|
541
|
+
return { code, removed, empty: true, found: true }
|
|
542
|
+
}
|
|
543
|
+
if (removed) await writeJsonAtomic(file, { ...chain, updatedAt: Date.now() })
|
|
544
|
+
return { code, removed, found: true }
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* 无主台账清理:本节点不在链上、且链上所有成员所属节点都离线 / 过期时,把这串码的本地台账删掉。
|
|
549
|
+
* 场景:别的机器早就解散了链,本机 links 里也没有它,但 chains/<code>.json 还留着离线成员记录 ——
|
|
550
|
+
* 以前只能人工删文件(C 盘写入要审批),0.9.10 让 term_unlink all=true 顺手收口。
|
|
551
|
+
*/
|
|
552
|
+
async dropChainIfOrphan(code, nodeId) {
|
|
553
|
+
const file = this.chainFile(code)
|
|
554
|
+
const chain = await readJson(file)
|
|
555
|
+
if (!chain || !Array.isArray(chain.members)) return { code, dropped: false, reason: 'no-chain' }
|
|
556
|
+
if (chain.members.some((m) => m.nodeId === nodeId)) return { code, dropped: false, reason: 'self-member' }
|
|
557
|
+
const nodes = await this.listNodes().catch(() => [])
|
|
558
|
+
const alive = new Map(nodes.map((n) => [n.nodeId, Number(n.updatedAt) || 0]))
|
|
559
|
+
const limit = Date.now() - this.cfg.offlineAfterMs * 3
|
|
560
|
+
const anyAlive = chain.members.some((m) => (alive.get(m.nodeId) || 0) >= limit)
|
|
561
|
+
if (anyAlive) return { code, dropped: false, reason: 'member-online', members: chain.members.length }
|
|
562
|
+
await fsp.unlink(file).catch(() => {})
|
|
563
|
+
return { code, dropped: true, members: chain.members.length }
|
|
564
|
+
}
|
|
565
|
+
|
|
527
566
|
/** 剔掉已经不存在的成员(进程退出 / 长时间没心跳) */
|
|
528
567
|
async pruneChain(code, nodes) {
|
|
529
568
|
const chain = await readJson(this.chainFile(code))
|
package/lib/index.js
CHANGED
|
@@ -1024,6 +1024,16 @@ class CrossTerm {
|
|
|
1024
1024
|
for (const chain of await this.bus.listChains()) {
|
|
1025
1025
|
for (const m of chain.members || []) {
|
|
1026
1026
|
if (m.nodeId !== this.nodeId || !m.sessionId) continue
|
|
1027
|
+
// 自愈:本节点在台账里有成员,但 links 里已经没有这条记录 → 摘掉(0.9.9)。
|
|
1028
|
+
// 只在 links 已从磁盘载入之后做,避免启动早期把合法成员误删。
|
|
1029
|
+
if (this.linksLoaded) {
|
|
1030
|
+
const stillLinked = [...this.links.values()].some((l) => l.code === chain.code && String(l.sessionId || '') === String(m.sessionId))
|
|
1031
|
+
if (!stillLinked) {
|
|
1032
|
+
await this.bus.leaveChain(chain.code, this.nodeId, m.sessionId).catch(() => {})
|
|
1033
|
+
await this.note(`chain sweep: 摘掉幽灵成员 ${shortId(m.sessionId)} @ ${chain.code}`)
|
|
1034
|
+
continue
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1027
1037
|
const { known } = this.sessionPresence(m.sessionId)
|
|
1028
1038
|
if (known) this.chainMisses.delete(`${chain.code}::${m.sessionId}`)
|
|
1029
1039
|
if (Number(m.pid) !== process.pid || m.version !== VERSION) {
|
|
@@ -1172,6 +1182,22 @@ class CrossTerm {
|
|
|
1172
1182
|
if (!sid && !all && !caller) return { ok: false, detail: '不知道该退哪个会话:请传 sessionId,或显式 all=true 退掉本终端在这串码上的全部会话' }
|
|
1173
1183
|
const victims = [...this.links.entries()].filter(([, l]) => l.code === norm && (!scope.length || scope.includes(l.sessionId)))
|
|
1174
1184
|
if (!victims.length) {
|
|
1185
|
+
// 幽灵成员:links 里没有本终端的记录,但链台账里还留着本节点的人(0.9.9 修)。
|
|
1186
|
+
// all=true 视为「把本终端在这串码上的痕迹全清」,连台账一起摘;
|
|
1187
|
+
// 显式指定 sessionId 时不动 —— 那可能是笔误,不能顺手清别的会话。
|
|
1188
|
+
if (all) {
|
|
1189
|
+
const ghost = await this.bus.leaveChainNode(norm, this.nodeId).catch(() => undefined)
|
|
1190
|
+
if (ghost && ghost.removed > 0) {
|
|
1191
|
+
await this.note(`unlinked ${norm} [ghost ${ghost.removed}]`)
|
|
1192
|
+
return { ok: true, code: norm, removed: ghost.removed, ghosts: ghost.removed, empty: !!ghost.empty, sessions: [] }
|
|
1193
|
+
}
|
|
1194
|
+
// 本终端在台账里也没有成员:如果这串码上的成员全部离线,就当无主台账清掉(0.9.10)
|
|
1195
|
+
const orphan = await this.bus.dropChainIfOrphan(norm, this.nodeId).catch(() => undefined)
|
|
1196
|
+
if (orphan && orphan.dropped) {
|
|
1197
|
+
await this.note(`unlinked ${norm} [orphan chain, ${orphan.members} 个离线成员]`)
|
|
1198
|
+
return { ok: true, code: norm, removed: 0, droppedChain: true, empty: true, sessions: [] }
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1175
1201
|
return { ok: false, detail: `本终端${scope.length ? `的会话 ${this.labelOf(scope[0])}` : ''}没有接在特征码「${norm}」上` }
|
|
1176
1202
|
}
|
|
1177
1203
|
let removed = 0
|