dsh-crossterm 0.8.6 → 0.8.8

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,5 +1,37 @@
1
1
  # dsh-crossterm 变更记录
2
2
 
3
+ ## 0.8.8 — 2026-10-19
4
+
5
+ ### 「维护者」一栏只给维护端看
6
+
7
+ - **问题**:0.8.5 加的「维护者」行任何设备装上插件都会显示负责人是谁,而这是维护端信息,
8
+ 别的机器不需要看到。
9
+ - **判断规则**:新增导出纯函数 `maintainerVisible(term, doc)` —— 本机 `nodeId` 必须命中
10
+ `MAINTAINERS.md` 机器可读注释里的 `current` 或 `assistant` 条目(负责人与助手同机时都算
11
+ 「本机授权端」);配置 `maintainerDevices`(`bus.js` DEFAULTS 新增,默认 `[]`)可额外放行设备。
12
+ - **接口**:`GET /crossterm/api/about` 的 `maintainers` 增加 `visible`(布尔)与 `me`(本机 nodeId),
13
+ 客户端据此决定渲不渲染该行;负责人换机器只改 `MAINTAINERS.md` 的 `current.nodeId`,不用改代码。
14
+ - **测试**:`F:\dsh\001X\crossterm-work\probe-maintainer-visible.mjs` 15 项全过
15
+ (维护端可见、其他设备不可见、助手同机可见、显式授权可见、换机后本机隐藏、无文档/无 nodeId/cfg 为 null 等边界)。
16
+
17
+ ## 0.8.7 — 2026-10-19
18
+
19
+ ### 修 0.8.6 的回归:云端设备列表整个消失
20
+
21
+ - **回归原因**:0.8.6 在 `/crossterm/api/devices` 路由里把 `const layout = …` 写在内部
22
+ `try {}` 块里,块外的 `const ownerDevice = (layout && …)` 直接抛
23
+ `ReferenceError: layout is not defined`,于是接口返回
24
+ `{"ok":false,"configured":true,"devices":[],"message":"layout is not defined"}`,
25
+ 卡片「云端设备」一行整块消失。这类错误 0.8.6 的探针测不到 —— 它只测纯函数,没测路由。
26
+ - **修法**:`let layout = null` 提到 `try` 外;更重要的是把「老总线镜像 + 新布局心跳」的合并逻辑
27
+ 提成导出的纯函数 `mergeDevices({ old, fresh, selfId, ownerDevice })`(同一 nodeId 新布局覆盖老镜像、
28
+ `source:'layout'`、过滤本机、补角色、按心跳 `at` 倒序),路由只做 IO 和拼装。
29
+ - **排序口径**:之前先把 `lastSeen`(ISO 字符串)和 `at`(毫秒)混在一起比,单位不一致导致顺序错乱;
30
+ 现在 `annotateDevices()` 统一把 `at → lastSeen → updatedAt` 归一成毫秒,`mergeDevices()` 只按 `at` 倒序。
31
+ - 探针 `probe-devices-086.mjs` 扩到 **20 项全 PASS**:新增 `mergeDevices` 九项
32
+ (排除本机、同 nodeId 合并、新布局字段覆盖、来源标记、补进新设备、补角色、心跳倒序、空输入、null 容忍)。
33
+ - **要不要跟着动手:不用**(补丁只改云端设备列表的拼装与排序,装上重启即生效)。
34
+
3
35
  ## 0.8.6 — 2026-10-19
4
36
 
5
37
  ### 云端设备列表:只列别人,带权限和状态
package/README.md CHANGED
@@ -28,39 +28,36 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
28
28
  > `locatePkgJson()` 对子路径行返回 undefined),而插件页顶部标题与这一行共用同一份 `locale/zh.json` 的 meta,
29
29
  > 所以它单独做总开关,标题带「(总开关)」;卡片头用 `shortTitle` 显示短名「跨端互通」。
30
30
 
31
- **本版更新(0.8.6)**
31
+ **本版更新(0.8.8)**
32
32
 
33
- - **云端设备只列「别人」**:卡片里那行不再显示本机(自己已经在「当前终端」一栏),
34
- 只列同一账号下的其他设备,避免把「本机」也当远端看。
35
- - **每台带权限和状态**:逐台显示 `名称 · 管理员/成员 · 在线/离线(x 分钟前)· v版本号`。
36
- 管理员就是云端 `_account.json` 里 `ownerDevice` 那台;其余都是成员。
37
- - **离线时间不再显示「未知」**:以前只认 `updatedAt`,新布局心跳写的是 `at`,
38
- 于是离线设备一律「未知」;现在按 `updatedAt → at → lastSeen` 依次取,显示「x 分钟前」。
39
- - **`term_list` 的云端行也带角色**:agent 侧看到的云端设备行同样标管理员/成员。
40
- - 纯展示改动,装上即可,无需改动配置。
33
+ - **「维护者」一栏只给维护端看**:以前任何设备装上插件都能看到负责人是谁;现在只在维护端显示 ——
34
+ 本机 `nodeId` 必须出现在 `MAINTAINERS.md` 的负责人/助手条目里(两者在同一台机器上时都算),
35
+ 或用配置 `maintainerDevices` 显式放行。其他设备上这行直接不渲染。
36
+ - **换机器只改一个字段**:负责人换机器时,把 `MAINTAINERS.md` 里 `current.nodeId` 改成新机器,
37
+ 旧机器下次刷新卡片自动隐藏,不用改代码。
38
+ - 纯展示改动,装上重启即可,不用改配置。
41
39
 
42
- **上一版(0.8.5)**
40
+ **上一版(0.8.7)**
43
41
 
42
+ - **修 0.8.6 的回归**:0.8.6 装上后卡片「云端设备」整块消失(接口报 `layout is not defined`);
43
+ 0.8.7 已修,没升 0.8.6 的设备可直接装 0.8.7。
44
+ - **合并逻辑变成可测的纯函数**:`mergeDevices({ old, fresh, selfId, ownerDevice })`,配套 20 项单测。
45
+ - **离线时间排序口径统一**:`at → lastSeen → updatedAt` 先归一毫秒再排。
44
46
 
47
+ **更早(0.8.6 / 0.8.5 / 0.8.2 / 0.8.0)**
45
48
 
46
- - **负责人写清楚、换人及时更正**:新增 `MAINTAINERS.md`(任期表 + 换人步骤 + 机器可读注释)。
47
- 当前负责人 `session-b6aa099b…`(TS-01),助手 `session-ccf34a5a…`(同端多会话2),
48
- 上一任是 `session-6ef6da30…`(2026-10-18 01:09 因会话故障交卸)。
49
- 负责人一变,插件启动时自动往 `<busDir>\logs\ownership.log` 记一行。
50
- - **运行日志存档、可轮转**:`logs\<nodeId>.jsonl` 超过 5 MB 自动压成
51
- `logs\archive\<nodeId>-<时间戳>.jsonl.gz`,只留最近 20 份;不再担心日志把盘写满。
52
- - **方便调阅**:卡片底部新增「运行日志」按钮(看最近 120 行,当前文件不够时接上前一个归档);
53
- 接口是 `GET /crossterm/api/logs?lines=120`(需接口令牌),同时返回归档清单与最近 10 条负责人变更。
54
- - 卡片新增「维护者」一栏,直接显示当前负责人(含上一任与交卸时间)。
55
-
56
- **上一版(0.8.2)**
49
+ - **云端设备只列「别人」**:卡片里那行不再显示本机(自己已经在「当前终端」一栏),
50
+ 只列同一账号下的其他设备。
51
+ - **每台带权限和状态**:逐台显示 `名称 · 管理员/成员 · 在线/离线(x 分钟前)· v版本号`;
52
+ 管理员就是云端 `_account.json` 里 `ownerDevice` 那台,其余都是成员。
53
+ - **离线时间不再显示「未知」**:以前只认 `updatedAt`,新布局心跳写的是 `at`,
54
+ 离线设备一律「未知」;现在按 `updatedAt → at → lastSeen` 依次取。
55
+ - `term_list` 的云端行也带角色。
57
56
 
58
- - 死信按 20s / 1m / 5m / 15m / 1h 退避,8 次后进 `dead/`;收件人写设备名会回退「最近活跃会话」;
57
+ - 0.8.5:负责人留痕 `MAINTAINERS.md`、运行日志可轮转(5 MB 压 `logs/archive/` 留 20 份)、
58
+ 卡片「维护者」行与「运行日志」按钮、`GET /crossterm/api/logs`。
59
+ - 0.8.2:死信按 20s/1m/5m/15m/1h 退避,8 次后进 `dead/`;收件人写设备名会回退「最近活跃会话」;
59
60
  同一个信封不会投两次;`/crossterm/api/*` 要求本机回环 `Host`。
60
-
61
- **更早(0.8.1 / 0.8.0)**
62
-
63
- - 0.8.1:旧布局缓存缺签名密钥时会重算并补写 `relaySecret`。
64
61
  - 0.8.0 安全加固:附件名净化 + 越界校验、写接口要 `x-crossterm-token`、信封 HMAC 签名、入站限额、按 id 去重。
65
62
 
66
63
  ## 一、安装与卸载
package/client/client.js CHANGED
@@ -482,7 +482,7 @@ window.__ModuleLoader__.load({
482
482
  label: "当前终端",
483
483
  value: status ? `${status.node}(${status.nodeId})· ${status.hostname}` : "",
484
484
  }),
485
- about && about.maintainers
485
+ about && about.maintainers && about.maintainers.visible === true
486
486
  ? h(FriendlyRow, {
487
487
  label: "维护者",
488
488
  value: maintainersText(about),
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.8.6'
26
+ export const VERSION = '0.8.8'
27
27
 
28
28
  /** DSH 的家目录:与宿主其它状态(sessions / profiles)同一个根,多终端天然共享 */
29
29
  export function dshHome() {
@@ -79,6 +79,9 @@ const DEFAULTS = {
79
79
  backupSessionsPerRun: 20,
80
80
  backupAttachmentsPerRun: 20,
81
81
  attachmentBackupEnabled: true,
82
+ // 卡片「维护者」一栏只在维护端显示:默认按 MAINTAINERS.md 里的 nodeId 判断,
83
+ // 这里可以额外放行别的设备(例如用户授权过的本机另装环境)。
84
+ maintainerDevices: [],
82
85
  baiduAppFile: '',
83
86
  baiduAuthFile: '',
84
87
  cloudStateFile: '',
package/lib/web.js CHANGED
@@ -174,6 +174,24 @@ export function annotateDevices(list, { selfId = '', ownerDevice = '' } = {}) {
174
174
  }))
175
175
  }
176
176
 
177
+ /**
178
+ * 合并两路设备来源(老总线镜像 `listCloudDevices` + 新布局心跳 `listPresences`),
179
+ * 同一个 nodeId 以新布局为准,最后过滤本机、补角色、按心跳时间倒序。
180
+ * 0.8.7:把这段纯逻辑从路由里提出来 —— 0.8.6 曾在路由里把 `layout` 写成块级变量,
181
+ * 导致 `layout is not defined`、设备列表整个返回 ok:false,单测能挡住这类回归。
182
+ */
183
+ export function mergeDevices({ old = [], fresh = [], selfId = '', ownerDevice = '' } = {}) {
184
+ const byId = new Map()
185
+ for (const d of Array.isArray(old) ? old : []) if (d && d.nodeId) byId.set(d.nodeId, d)
186
+ for (const d of Array.isArray(fresh) ? fresh : []) {
187
+ if (d && d.nodeId) byId.set(d.nodeId, { ...byId.get(d.nodeId), ...d, source: 'layout' })
188
+ }
189
+ return annotateDevices([...byId.values()], { selfId, ownerDevice }).sort(
190
+ // annotateDevices 已把 at 归一成毫秒时间戳(含 lastSeen / updatedAt 兜底),这里只按它倒序
191
+ (a, b) => Number(b.at || 0) - Number(a.at || 0),
192
+ )
193
+ }
194
+
177
195
  /** 语义化版本比较:只比 主.次.补(忽略预发布尾巴),a>b 返回正数 */
178
196
  function compareVersion(a, b) {
179
197
  const num = (v) => String(v || '').split('-')[0].split('.').map((x) => parseInt(x, 10) || 0)
@@ -488,7 +506,28 @@ function readMaintainers() {
488
506
  return readMaintainersDoc(ROOT)
489
507
  }
490
508
 
491
- function aboutOf() {
509
+ /**
510
+ * 「维护者」只给维护端看:本机 nodeId 必须出现在 MAINTAINERS.md 的
511
+ * 负责人/助手条目里,或用配置 `maintainerDevices` 额外授权;其他设备一律不给这行。
512
+ */
513
+ export function maintainerVisible(term, doc) {
514
+ if (!doc) return false
515
+ const self = String((term && term.nodeId) || '')
516
+ if (!self) return false
517
+ const allow = new Set()
518
+ const add = (item) => {
519
+ if (item && item.nodeId) allow.add(String(item.nodeId))
520
+ }
521
+ add(doc.current)
522
+ add(doc.assistant)
523
+ try {
524
+ const extra = term && term.cfg && Array.isArray(term.cfg.maintainerDevices) ? term.cfg.maintainerDevices : []
525
+ for (const item of extra) allow.add(String(item))
526
+ } catch { /* ignore */ }
527
+ return allow.has(self)
528
+ }
529
+
530
+ function aboutOf(term) {
492
531
  if (aboutCache && aboutCache.version === VERSION) return aboutCache
493
532
  let meta = {}
494
533
  try {
@@ -506,6 +545,7 @@ function aboutOf() {
506
545
  description: doc.description || '',
507
546
  }
508
547
  })
548
+ const maintainers = readMaintainers()
509
549
  aboutCache = {
510
550
  name: PKG,
511
551
  version: VERSION,
@@ -514,7 +554,9 @@ function aboutOf() {
514
554
  shortTitle: meta.shortTitle || meta.title || PKG,
515
555
  description: meta.description || '',
516
556
  components,
517
- maintainers: readMaintainers(),
557
+ maintainers: maintainers
558
+ ? { ...maintainers, visible: maintainerVisible(term, maintainers), me: String((term && term.nodeId) || '') }
559
+ : null,
518
560
  changelog: readChangelog(),
519
561
  }
520
562
  return aboutCache
@@ -560,7 +602,7 @@ export function registerWeb(ctx, term, log) {
560
602
  path: '/crossterm/api/about',
561
603
  handler: (req, res) => {
562
604
  try {
563
- json(res, 200, { ok: true, ...aboutOf() })
605
+ json(res, 200, { ok: true, ...aboutOf(term) })
564
606
  } catch (error) {
565
607
  json(res, 200, { ok: false, message: String((error && error.message) || error) })
566
608
  }
@@ -630,21 +672,17 @@ export function registerWeb(ctx, term, log) {
630
672
  })
631
673
  const result = await listCloudDevices(pan, cfg.cloudRoot)
632
674
  let fresh = { devices: [], files: 0 }
675
+ let layout = null
633
676
  try {
634
- const layout = term.home ? await term.home.getLayout(false) : null
677
+ layout = term.home ? await term.home.getLayout(false) : null
635
678
  if (layout) fresh = await listPresences({ pan, layout, log: (m) => log('devices: ' + m) })
636
679
  } catch (error) {
637
680
  log('devices: 新布局读取失败 ' + String((error && error.message) || error))
638
681
  }
639
- const byId = new Map()
640
- for (const d of result.devices || []) byId.set(d.nodeId, d)
641
- for (const d of fresh.devices || []) byId.set(d.nodeId, { ...byId.get(d.nodeId), ...d, source: 'layout' })
642
- // 0.8.6:列表里不显示本机,只给「其他设备」;每台标上账号里的角色(管理员 = _account.json 的 ownerDevice)
682
+ // 0.8.7:合并/过滤/角色/排序都走纯函数,路由里不再直接碰 layout 的块级作用域
643
683
  const ownerDevice = (layout && layout.accountInfo && layout.accountInfo.ownerDevice) || (layout && layout.deviceId) || ''
644
684
  const selfId = info.nodeId || ''
645
- result.devices = annotateDevices([...byId.values()], { selfId, ownerDevice }).sort(
646
- (a, b) => Number(b.lastSeen ? Date.parse(b.lastSeen) : b.at || 0) - Number(a.lastSeen ? Date.parse(a.lastSeen) : a.at || 0),
647
- )
685
+ result.devices = mergeDevices({ old: result.devices, fresh: fresh.devices, selfId, ownerDevice })
648
686
  delete result.tmpFiles
649
687
  result.files = (result.files || 0) + (fresh.files || 0)
650
688
  result.ownerDevice = ownerDevice
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-crossterm",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
4
4
  "description": "跨端互通:同一 DSH 账号下的会话、终端、设备之间互通 —— 特征码链、内联邮件、云端中转站与自动备份",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",