dsh-crossterm 0.9.8 → 0.9.9

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 CHANGED
@@ -1,3 +1,15 @@
1
+ ## 0.9.9 — 2026-10-20
2
+
3
+ ### 修复:幽灵链成员退不掉(YOCE_C 报告)
4
+
5
+ - 现象:`term_unlink code="X" all=true` 回「本终端没有接在特征码「X」上」,但链台账里确实还留着本终端成员;只能先 `term_link` 补回 links 再 `term_unlink` 绕开。
6
+ - 根因:`unlinkChain()` 只遍历内存 `this.links`(`lib/index.js`),台账里的成员它看不见;`leaveChain()` 只能按 `nodeId::sessionId` 精确删。
7
+ - 修法:
8
+ - `lib/bus.js` 新增 `leaveChainNode(code, nodeId)`:按 nodeId 摘掉该节点在链上的全部成员,空了删链文件。
9
+ - `lib/index.js` 的 `unlinkChain()`:`all=true` 且 links 无记录时改走 `leaveChainNode`,成功则返回 `{ ok:true, ghosts, removed }`。
10
+ - `chainSweep` 加自愈:`linksLoaded` 之后,本节点在台账里有、links 里没有的成员会被摘掉并记日志。
11
+ - 组件版本:跨会话 `0.5.0` → `0.5.1`。
12
+
1
13
  ## 0.9.8 — 2026-10-20
2
14
 
3
15
  - 自动重启组件维护权交回:原负责人会话「自动重启」(`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.0 | 同一终端里的多个会话之间互通:特征码链、内联邮件、收件箱 | `term_link` `term_unlink` `term_broadcast` `term_inbox` `term_config` |
22
+ | ② | 跨会话 | v0.5.1 | 同一终端里的多个会话之间互通:特征码链、内联邮件、收件箱 | `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,23 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
30
30
  >
31
31
  > 组件版本号怎么升:**只在该组件自己的文件改动时升它**,跟着插件一起发版(例:只改自动重启 → `components/restart` 的 `0.3.28` → `0.3.29`,其余四个不动)。插件总版本仍按发版技能的三分位规则。
32
32
 
33
- **本版更新(0.9.8)**
33
+ **本版更新(0.9.9)**
34
34
 
35
- - **自动重启组件交回维护权**:原负责人会话「自动重启」按机主指示即日起停止改动,`components/restart/**` 暂由插件负责人 `b6aa099b` 直管;新负责人等后续交接时再任命(`MAINTAINERS.md` 的 `modules.restart` 已改,旧负责人记入任期)。
36
- - 它交回时留了两条观察,已记进 `MAINTAINERS.md` 的「待定事项」,下次动这个组件前先定口径。
37
- - 只改 `MAINTAINERS.md` / `CHANGELOG.md` / README,代码与组件版本都不动(入口 0.9.3 / 跨会话 0.5.0 / 跨工作区 0.1.0 / 跨设备 0.4.0 / 自动重启 0.3.29)。
35
+ - **修「幽灵链成员」退不掉**(YOCE_C 2026-10-20 报的坑):`term_unlink ... all=true` 以前只遍历内存里的 links,遇到「链台账里还留着本终端成员、但 links 已没有这条记录」就永远退不掉(一直回「本终端没有接在特征码上」)。
36
+ - 现在 `all=true` 在 links 为空时也会按 **nodeId** 把本终端在这串码上的成员从台账里摘掉,返回值带 `ghosts`;成员清空时链文件自动删除。
37
+ - `chainSweep` 每 6 拍自愈一次:本节点在台账里有、links 里却没有的成员会被摘掉,并记一行日志「chain sweep: 摘掉幽灵成员」。
38
+ - 显式传 `sessionId` 的行为不变——只退那一个,不动其它会话(笔误不会误删)。
39
+ - 组件版本:跨会话 0.5.0 → **0.5.1**,其余组件不动。
38
40
 
39
- **上一版(0.9.7)**
41
+ **上一版(0.9.8)**
40
42
 
41
- - **组件版本按机主要求重新定起点**:跨会话 → v0.5.0、跨设备 → v0.4.0、跨工作区 → v0.1.0;入口仍 v0.9.3、自动重启仍 v0.3.29。数字变小是故意的,之后哪个组件改了才升哪个。
43
+ - **自动重启组件交回维护权**:原负责人会话「自动重启」按机主指示即日起停止改动,`components/restart/**` 暂由插件负责人 `b6aa099b` 直管;新负责人等后续交接时再任命(`MAINTAINERS.md` 的 `modules.restart` 已改,旧负责人记入任期)。
44
+ - 它交回时留了两条观察,已记进 `MAINTAINERS.md` 的「待定事项」,下次动这个组件前先定口径。
45
+ - 只改 `MAINTAINERS.md` / `CHANGELOG.md` / README,代码与组件版本都不动。
42
46
 
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)**
47
+ **更早(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
48
 
49
+ - 0.9.7:组件版本按机主要求重新定起点(跨会话 v0.5.0 / 跨设备 v0.4.0 / 跨工作区 v0.1.0)。
45
50
  - 0.9.6:去掉「跨账号」组件,插件页变成 5 个组件。
46
51
  - 0.9.5:组件「跨终端」改名「跨工作区」(跨终端本轮不做)。
47
52
  - 0.9.3:组件表补全六个组件,入口名从 `ui` 改成「跨端互通(入口)」,组件版本号各升各的第一次落地。
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "跨会话",
3
3
  "description": "同一终端里的多个会话之间互通:特征码链、内联邮件、收件箱",
4
- "version": "0.5.0"
4
+ "version": "0.5.1"
5
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.8'
26
+ export const VERSION = '0.9.9'
27
27
 
28
28
  /** DSH 的家目录:与宿主其它状态(sessions / profiles)同一个根,多终端天然共享 */
29
29
  export function dshHome() {
@@ -524,6 +524,26 @@ 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
+
527
547
  /** 剔掉已经不存在的成员(进程退出 / 长时间没心跳) */
528
548
  async pruneChain(code, nodes) {
529
549
  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,16 @@ 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
+ }
1175
1195
  return { ok: false, detail: `本终端${scope.length ? `的会话 ${this.labelOf(scope[0])}` : ''}没有接在特征码「${norm}」上` }
1176
1196
  }
1177
1197
  let removed = 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-crossterm",
3
- "version": "0.9.8",
3
+ "version": "0.9.9",
4
4
  "description": "跨端互通:同一 DSH 账号下的会话、工作区、设备之间互通 —— 特征码链、内联邮件、云端中转站与自动备份",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",