dsh-crossterm 0.8.5 → 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 +34 -0
- package/README.md +27 -26
- package/client/client.js +6 -4
- package/lib/bus.js +1 -1
- package/lib/cloud/match.js +1 -0
- package/lib/index.js +3 -2
- package/lib/web.js +43 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
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
|
+
|
|
21
|
+
## 0.8.6 — 2026-10-19
|
|
22
|
+
|
|
23
|
+
### 云端设备列表:只列别人,带权限和状态
|
|
24
|
+
|
|
25
|
+
- **不显示本机**:`/crossterm/api/devices` 与卡片「云端设备」行都过滤掉本机 ——
|
|
26
|
+
本机已经在「当前终端」一栏,列在设备里等于把自己当远端。
|
|
27
|
+
- **每台标角色**:`annotateDevices(list, { selfId, ownerDevice })`(`lib/web.js` 导出,纯函数可测);
|
|
28
|
+
角色取云端 `_account.json` 的 `ownerDevice`:是它 → **管理员**,其余 → **成员**。
|
|
29
|
+
- **离线时间口径修正**:以前只读 `updatedAt`,新布局心跳写的是 `at`,离线设备全显示「未知」;
|
|
30
|
+
现在按 `updatedAt → at → lastSeen` 依次取,显示「x 分钟前」。
|
|
31
|
+
- **`term_list` 云端行带角色**:`lib/cloud/match.js` 的 `deviceRow()` 增 `role`;
|
|
32
|
+
`Term.cloudDevices()` 过滤本机,跨设备匹配更干净。
|
|
33
|
+
- 探针 `probe-devices-086.mjs` **11 PASS**(排除本机、剔除无 nodeId、管理员/成员、字段保留、
|
|
34
|
+
at 三种来源、已有 role 不覆盖、空列表)。
|
|
35
|
+
- **要不要跟着动手:不用**(纯展示改动,装上即可)。
|
|
36
|
+
|
|
3
37
|
## 0.8.5 — 2026-10-19
|
|
4
38
|
|
|
5
39
|
### 负责人留痕 + 运行日志可查(日志轮转)
|
package/README.md
CHANGED
|
@@ -28,33 +28,34 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
|
|
|
28
28
|
> `locatePkgJson()` 对子路径行返回 undefined),而插件页顶部标题与这一行共用同一份 `locale/zh.json` 的 meta,
|
|
29
29
|
> 所以它单独做总开关,标题带「(总开关)」;卡片头用 `shortTitle` 显示短名「跨端互通」。
|
|
30
30
|
|
|
31
|
-
**本版更新(0.8.
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
-
|
|
42
|
-
|
|
43
|
-
**上一版(0.8.
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
31
|
+
**本版更新(0.8.7)**
|
|
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
|
+
- 纯展示与拼装修复,装上重启即可,不用改配置。
|
|
42
|
+
|
|
43
|
+
**上一版(0.8.6)**
|
|
44
|
+
|
|
45
|
+
- **云端设备只列「别人」**:卡片里那行不再显示本机(自己已经在「当前终端」一栏),
|
|
46
|
+
只列同一账号下的其他设备。
|
|
47
|
+
- **每台带权限和状态**:逐台显示 `名称 · 管理员/成员 · 在线/离线(x 分钟前)· v版本号`;
|
|
48
|
+
管理员就是云端 `_account.json` 里 `ownerDevice` 那台,其余都是成员。
|
|
49
|
+
- **离线时间不再显示「未知」**:以前只认 `updatedAt`,新布局心跳写的是 `at`,
|
|
50
|
+
离线设备一律「未知」;现在按 `updatedAt → at → lastSeen` 依次取。
|
|
51
|
+
- `term_list` 的云端行也带角色。
|
|
52
|
+
|
|
53
|
+
**更早(0.8.5 / 0.8.2 / 0.8.0)**
|
|
54
|
+
|
|
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/`;收件人写设备名会回退「最近活跃会话」;
|
|
53
58
|
同一个信封不会投两次;`/crossterm/api/*` 要求本机回环 `Host`。
|
|
54
|
-
|
|
55
|
-
**更早(0.8.1 / 0.8.0)**
|
|
56
|
-
|
|
57
|
-
- 0.8.1:旧布局缓存缺签名密钥时会重算并补写 `relaySecret`。
|
|
58
59
|
- 0.8.0 安全加固:附件名净化 + 越界校验、写接口要 `x-crossterm-token`、信封 HMAC 签名、入站限额、按 id 去重。
|
|
59
60
|
|
|
60
61
|
## 一、安装与卸载
|
package/client/client.js
CHANGED
|
@@ -175,13 +175,15 @@ window.__ModuleLoader__.load({
|
|
|
175
175
|
if (!(status && status.cloud && status.cloud.configured)) return "未连接云盘";
|
|
176
176
|
if (!devices || devices.loading) return "读取中…";
|
|
177
177
|
if (!devices.ok) return devices.message || "读取失败";
|
|
178
|
-
const list = devices.devices || [];
|
|
178
|
+
const list = (devices.devices || []).filter((d) => d && (!devices.me || d.nodeId !== devices.me));
|
|
179
179
|
if (!list.length) return "云端还没有其他设备";
|
|
180
180
|
return list
|
|
181
181
|
.map((d) => {
|
|
182
|
-
const label =
|
|
183
|
-
const
|
|
184
|
-
|
|
182
|
+
const label = d.name || d.nodeId || "未知设备";
|
|
183
|
+
const role = d.role || "成员";
|
|
184
|
+
const at = d.updatedAt || d.at || (d.lastSeen ? Date.parse(d.lastSeen) : 0);
|
|
185
|
+
const state = d.online ? "在线" : agoText(at);
|
|
186
|
+
return label + " · " + role + " · " + state + (d.version ? " · v" + d.version : "");
|
|
185
187
|
})
|
|
186
188
|
.join(";");
|
|
187
189
|
}
|
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/cloud/match.js
CHANGED
package/lib/index.js
CHANGED
|
@@ -348,12 +348,13 @@ class CrossTerm {
|
|
|
348
348
|
|
|
349
349
|
// ── 云端设备(跨设备投递,0.7.3 起)─────────────────────────────────
|
|
350
350
|
|
|
351
|
-
/**
|
|
351
|
+
/** 云端设备列表:没连云端、没布局、或还读不到时统一回空数组(0.8.6 起不含本机) */
|
|
352
352
|
async cloudDevices() {
|
|
353
353
|
if (!this.home) return []
|
|
354
354
|
try {
|
|
355
355
|
const r = await this.home.devices()
|
|
356
|
-
|
|
356
|
+
const list = r && r.ok && Array.isArray(r.devices) ? r.devices : []
|
|
357
|
+
return list.filter((d) => d && d.nodeId && d.nodeId !== this.nodeId)
|
|
357
358
|
} catch (e) {
|
|
358
359
|
this.warn(`cloudDevices: ${(e && e.message) || String(e)}`)
|
|
359
360
|
return []
|
package/lib/web.js
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
8
8
|
import { spawn } from 'node:child_process'
|
|
9
9
|
import { VERSION, readConfig, writeConfig } from './bus.js'
|
|
10
10
|
import { createPan } from './cloud/pan.js'
|
|
11
|
-
import { listCloudDevices
|
|
11
|
+
import { listCloudDevices } from './cloud/devices.js'
|
|
12
12
|
import { listPresences } from './cloud/presence.js'
|
|
13
13
|
import { runSetup } from './cloud/setup.js'
|
|
14
14
|
import { listLogs, tailLogs, readMaintainersDoc, listOwnership } from './logs.js'
|
|
@@ -158,6 +158,40 @@ function cloudProvider(cfg) {
|
|
|
158
158
|
return { id: 'none', name: '未连接', configured: false }
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/**
|
|
162
|
+
* 云端设备列表的展示口径(0.8.6 起):
|
|
163
|
+
* - 不显示本机(自己已经在「当前终端」那一行);
|
|
164
|
+
* - 每台标上账号里的角色:`_account.json` 的 `ownerDevice` 是管理员,其余是成员;
|
|
165
|
+
* - 保底字段:`role` / `online` / `at`(离线时卡片用它算「x 分钟前」)。
|
|
166
|
+
*/
|
|
167
|
+
export function annotateDevices(list, { selfId = '', ownerDevice = '' } = {}) {
|
|
168
|
+
return (Array.isArray(list) ? list : [])
|
|
169
|
+
.filter((d) => d && d.nodeId && d.nodeId !== selfId)
|
|
170
|
+
.map((d) => ({
|
|
171
|
+
...d,
|
|
172
|
+
role: d.role || (d.nodeId === ownerDevice ? '管理员' : '成员'),
|
|
173
|
+
at: Number(d.at || 0) || (d.lastSeen ? Date.parse(d.lastSeen) || 0 : 0) || (d.updatedAt ? Number(d.updatedAt) || 0 : 0),
|
|
174
|
+
}))
|
|
175
|
+
}
|
|
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
|
+
|
|
161
195
|
/** 语义化版本比较:只比 主.次.补(忽略预发布尾巴),a>b 返回正数 */
|
|
162
196
|
function compareVersion(a, b) {
|
|
163
197
|
const num = (v) => String(v || '').split('-')[0].split('.').map((x) => parseInt(x, 10) || 0)
|
|
@@ -614,21 +648,21 @@ export function registerWeb(ctx, term, log) {
|
|
|
614
648
|
})
|
|
615
649
|
const result = await listCloudDevices(pan, cfg.cloudRoot)
|
|
616
650
|
let fresh = { devices: [], files: 0 }
|
|
651
|
+
let layout = null
|
|
617
652
|
try {
|
|
618
|
-
|
|
653
|
+
layout = term.home ? await term.home.getLayout(false) : null
|
|
619
654
|
if (layout) fresh = await listPresences({ pan, layout, log: (m) => log('devices: ' + m) })
|
|
620
655
|
} catch (error) {
|
|
621
656
|
log('devices: 新布局读取失败 ' + String((error && error.message) || error))
|
|
622
657
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
result.devices =
|
|
658
|
+
// 0.8.7:合并/过滤/角色/排序都走纯函数,路由里不再直接碰 layout 的块级作用域
|
|
659
|
+
const ownerDevice = (layout && layout.accountInfo && layout.accountInfo.ownerDevice) || (layout && layout.deviceId) || ''
|
|
660
|
+
const selfId = info.nodeId || ''
|
|
661
|
+
result.devices = mergeDevices({ old: result.devices, fresh: fresh.devices, selfId, ownerDevice })
|
|
627
662
|
delete result.tmpFiles
|
|
628
663
|
result.files = (result.files || 0) + (fresh.files || 0)
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
json(res, 200, { ...result, configured: true, me: info.nodeId || '' })
|
|
664
|
+
result.ownerDevice = ownerDevice
|
|
665
|
+
json(res, 200, { ...result, configured: true, me: selfId })
|
|
632
666
|
} catch (error) {
|
|
633
667
|
json(res, 200, {
|
|
634
668
|
ok: false,
|