dsh-crossterm 0.8.6 → 0.8.7
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 +18 -0
- package/README.md +22 -27
- package/lib/bus.js +1 -1
- package/lib/web.js +22 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# dsh-crossterm 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.8.7 — 2026-10-19
|
|
4
|
+
|
|
5
|
+
### 修 0.8.6 的回归:云端设备列表整个消失
|
|
6
|
+
|
|
7
|
+
- **回归原因**:0.8.6 在 `/crossterm/api/devices` 路由里把 `const layout = …` 写在内部
|
|
8
|
+
`try {}` 块里,块外的 `const ownerDevice = (layout && …)` 直接抛
|
|
9
|
+
`ReferenceError: layout is not defined`,于是接口返回
|
|
10
|
+
`{"ok":false,"configured":true,"devices":[],"message":"layout is not defined"}`,
|
|
11
|
+
卡片「云端设备」一行整块消失。这类错误 0.8.6 的探针测不到 —— 它只测纯函数,没测路由。
|
|
12
|
+
- **修法**:`let layout = null` 提到 `try` 外;更重要的是把「老总线镜像 + 新布局心跳」的合并逻辑
|
|
13
|
+
提成导出的纯函数 `mergeDevices({ old, fresh, selfId, ownerDevice })`(同一 nodeId 新布局覆盖老镜像、
|
|
14
|
+
`source:'layout'`、过滤本机、补角色、按心跳 `at` 倒序),路由只做 IO 和拼装。
|
|
15
|
+
- **排序口径**:之前先把 `lastSeen`(ISO 字符串)和 `at`(毫秒)混在一起比,单位不一致导致顺序错乱;
|
|
16
|
+
现在 `annotateDevices()` 统一把 `at → lastSeen → updatedAt` 归一成毫秒,`mergeDevices()` 只按 `at` 倒序。
|
|
17
|
+
- 探针 `probe-devices-086.mjs` 扩到 **20 项全 PASS**:新增 `mergeDevices` 九项
|
|
18
|
+
(排除本机、同 nodeId 合并、新布局字段覆盖、来源标记、补进新设备、补角色、心跳倒序、空输入、null 容忍)。
|
|
19
|
+
- **要不要跟着动手:不用**(补丁只改云端设备列表的拼装与排序,装上重启即生效)。
|
|
20
|
+
|
|
3
21
|
## 0.8.6 — 2026-10-19
|
|
4
22
|
|
|
5
23
|
### 云端设备列表:只列别人,带权限和状态
|
package/README.md
CHANGED
|
@@ -28,39 +28,34 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
|
|
|
28
28
|
> `locatePkgJson()` 对子路径行返回 undefined),而插件页顶部标题与这一行共用同一份 `locale/zh.json` 的 meta,
|
|
29
29
|
> 所以它单独做总开关,标题带「(总开关)」;卡片头用 `shortTitle` 显示短名「跨端互通」。
|
|
30
30
|
|
|
31
|
-
**本版更新(0.8.
|
|
32
|
-
|
|
33
|
-
- **云端设备只列「别人」**:卡片里那行不再显示本机(自己已经在「当前终端」一栏),
|
|
34
|
-
只列同一账号下的其他设备,避免把「本机」也当远端看。
|
|
35
|
-
- **每台带权限和状态**:逐台显示 `名称 · 管理员/成员 · 在线/离线(x 分钟前)· v版本号`。
|
|
36
|
-
管理员就是云端 `_account.json` 里 `ownerDevice` 那台;其余都是成员。
|
|
37
|
-
- **离线时间不再显示「未知」**:以前只认 `updatedAt`,新布局心跳写的是 `at`,
|
|
38
|
-
于是离线设备一律「未知」;现在按 `updatedAt → at → lastSeen` 依次取,显示「x 分钟前」。
|
|
39
|
-
- **`term_list` 的云端行也带角色**:agent 侧看到的云端设备行同样标管理员/成员。
|
|
40
|
-
- 纯展示改动,装上即可,无需改动配置。
|
|
41
|
-
|
|
42
|
-
**上一版(0.8.5)**
|
|
31
|
+
**本版更新(0.8.7)**
|
|
43
32
|
|
|
33
|
+
- **修 0.8.6 的回归**:0.8.6 装上后卡片「云端设备」整块消失,接口报
|
|
34
|
+
`layout is not defined`(路由里把 `layout` 声明在 `try` 块内、块外又用了它)。
|
|
35
|
+
0.8.7 已修;本来就还没升 0.8.6 的设备可以直接装 0.8.7。
|
|
36
|
+
- **合并逻辑变成可测的纯函数**:`mergeDevices({ old, fresh, selfId, ownerDevice })` 负责
|
|
37
|
+
「老总线镜像 + 新布局心跳」的合并、排除本机、补管理员/成员角色、按心跳倒序,
|
|
38
|
+
路由不再自己拼装,配套 20 项单测。
|
|
39
|
+
- **离线时间排序口径统一**:`annotateDevices()` 把 `at → lastSeen → updatedAt` 归一成毫秒,
|
|
40
|
+
列表按它倒序,不会再把 ISO 字符串和毫秒时间戳混着比。
|
|
41
|
+
- 纯展示与拼装修复,装上重启即可,不用改配置。
|
|
44
42
|
|
|
43
|
+
**上一版(0.8.6)**
|
|
45
44
|
|
|
46
|
-
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
`
|
|
52
|
-
-
|
|
53
|
-
接口是 `GET /crossterm/api/logs?lines=120`(需接口令牌),同时返回归档清单与最近 10 条负责人变更。
|
|
54
|
-
- 卡片新增「维护者」一栏,直接显示当前负责人(含上一任与交卸时间)。
|
|
45
|
+
- **云端设备只列「别人」**:卡片里那行不再显示本机(自己已经在「当前终端」一栏),
|
|
46
|
+
只列同一账号下的其他设备。
|
|
47
|
+
- **每台带权限和状态**:逐台显示 `名称 · 管理员/成员 · 在线/离线(x 分钟前)· v版本号`;
|
|
48
|
+
管理员就是云端 `_account.json` 里 `ownerDevice` 那台,其余都是成员。
|
|
49
|
+
- **离线时间不再显示「未知」**:以前只认 `updatedAt`,新布局心跳写的是 `at`,
|
|
50
|
+
离线设备一律「未知」;现在按 `updatedAt → at → lastSeen` 依次取。
|
|
51
|
+
- `term_list` 的云端行也带角色。
|
|
55
52
|
|
|
56
|
-
|
|
53
|
+
**更早(0.8.5 / 0.8.2 / 0.8.0)**
|
|
57
54
|
|
|
58
|
-
-
|
|
55
|
+
- 0.8.5:负责人留痕 `MAINTAINERS.md`、运行日志可轮转(5 MB 压 `logs/archive/` 留 20 份)、
|
|
56
|
+
卡片「维护者」行与「运行日志」按钮、`GET /crossterm/api/logs`。
|
|
57
|
+
- 0.8.2:死信按 20s/1m/5m/15m/1h 退避,8 次后进 `dead/`;收件人写设备名会回退「最近活跃会话」;
|
|
59
58
|
同一个信封不会投两次;`/crossterm/api/*` 要求本机回环 `Host`。
|
|
60
|
-
|
|
61
|
-
**更早(0.8.1 / 0.8.0)**
|
|
62
|
-
|
|
63
|
-
- 0.8.1:旧布局缓存缺签名密钥时会重算并补写 `relaySecret`。
|
|
64
59
|
- 0.8.0 安全加固:附件名净化 + 越界校验、写接口要 `x-crossterm-token`、信封 HMAC 签名、入站限额、按 id 去重。
|
|
65
60
|
|
|
66
61
|
## 一、安装与卸载
|
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.
|
|
26
|
+
export const VERSION = '0.8.7'
|
|
27
27
|
|
|
28
28
|
/** DSH 的家目录:与宿主其它状态(sessions / profiles)同一个根,多终端天然共享 */
|
|
29
29
|
export function dshHome() {
|
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)
|
|
@@ -630,21 +648,17 @@ export function registerWeb(ctx, term, log) {
|
|
|
630
648
|
})
|
|
631
649
|
const result = await listCloudDevices(pan, cfg.cloudRoot)
|
|
632
650
|
let fresh = { devices: [], files: 0 }
|
|
651
|
+
let layout = null
|
|
633
652
|
try {
|
|
634
|
-
|
|
653
|
+
layout = term.home ? await term.home.getLayout(false) : null
|
|
635
654
|
if (layout) fresh = await listPresences({ pan, layout, log: (m) => log('devices: ' + m) })
|
|
636
655
|
} catch (error) {
|
|
637
656
|
log('devices: 新布局读取失败 ' + String((error && error.message) || error))
|
|
638
657
|
}
|
|
639
|
-
|
|
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)
|
|
658
|
+
// 0.8.7:合并/过滤/角色/排序都走纯函数,路由里不再直接碰 layout 的块级作用域
|
|
643
659
|
const ownerDevice = (layout && layout.accountInfo && layout.accountInfo.ownerDevice) || (layout && layout.deviceId) || ''
|
|
644
660
|
const selfId = info.nodeId || ''
|
|
645
|
-
result.devices =
|
|
646
|
-
(a, b) => Number(b.lastSeen ? Date.parse(b.lastSeen) : b.at || 0) - Number(a.lastSeen ? Date.parse(a.lastSeen) : a.at || 0),
|
|
647
|
-
)
|
|
661
|
+
result.devices = mergeDevices({ old: result.devices, fresh: fresh.devices, selfId, ownerDevice })
|
|
648
662
|
delete result.tmpFiles
|
|
649
663
|
result.files = (result.files || 0) + (fresh.files || 0)
|
|
650
664
|
result.ownerDevice = ownerDevice
|