dsh-crossterm 0.8.12 → 0.8.14
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 +26 -0
- package/README.md +24 -27
- package/client/client.js +20 -2
- package/lib/bus.js +1 -1
- package/lib/index.js +12 -3
- package/lib/web.js +42 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# dsh-crossterm 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.8.14 — 2026-10-20
|
|
4
|
+
|
|
5
|
+
### 修「更新按钮看起来没用」(YOCE_C 实测反馈)
|
|
6
|
+
|
|
7
|
+
上一版起更新链路已经能装成功,但宿主进程里还是旧版本,卡片继续显示「有更新」,
|
|
8
|
+
再点一次却回「已经是最新」—— 主观上就是点了没用。
|
|
9
|
+
|
|
10
|
+
- `/crossterm/api/status` 新增 `installed`(磁盘上 node_modules 里的真实版本)与
|
|
11
|
+
`needRestart`(磁盘版本 > 进程版本)。
|
|
12
|
+
- `/crossterm/api/check`:磁盘版本已达最新但进程未重启时返回
|
|
13
|
+
`updateAvailable=false, needRestart=true`,不再报「有更新」。
|
|
14
|
+
- `/crossterm/api/update`:已装未重启时提示「已装 X,重启 DSH 后生效(当前进程还在跑 Y)」。
|
|
15
|
+
- 卡片:多一行「待重启」,更新按钮变成不可点的「已装待重启」,检查更新也提示重启后生效。
|
|
16
|
+
|
|
17
|
+
## 0.8.13 — 2026-10-20
|
|
18
|
+
|
|
19
|
+
### 优化:已经是最新就不再重装;没写收件人的信优先投给维护者
|
|
20
|
+
|
|
21
|
+
来源:跨设备邮件(YOCE_C 的 0.8.12 验收报告,经 QQ 会话转交,2026-10-20T07:55:35Z)的两条观察。
|
|
22
|
+
|
|
23
|
+
- **已是最新就短路**:`updatePlugin()` 在 `latest && before && compareVersion(latest, before) <= 0` 时直接返回
|
|
24
|
+
`{ok:true, updated:false, skipped:'latest'}`,不再跑安装 —— 实测等于最新时仍会花 15.7s 重写 node_modules。
|
|
25
|
+
- **无收件人的信优先给维护者**:`relayDeliver()` 的兜底顺序改为「MAINTAINERS.md 主维护会话 → 最近活跃会话 → 第一个会话」,
|
|
26
|
+
回执文案区分「按维护者 MAINTAINERS.md」与「按最近活跃会话」。修 YOCE_C 的验收报告被投进 `im:qq_39` 的问题。
|
|
27
|
+
- **不用跟着动手**:直接升级即可,配置与云端数据格式都没变。
|
|
28
|
+
|
|
3
29
|
## 0.8.12 — 2026-10-20
|
|
4
30
|
|
|
5
31
|
### 修复:更新链路在 pnpm 11 上必定失败;latest 被镜像滞后带偏
|
package/README.md
CHANGED
|
@@ -28,38 +28,35 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
|
|
|
28
28
|
> `locatePkgJson()` 对子路径行返回 undefined),而插件页顶部标题与这一行共用同一份 `locale/zh.json` 的 meta,
|
|
29
29
|
> 所以它单独做入口(配置卡片 + 自动更新开关),标题带「(自更新)」;卡片头用 `shortTitle` 显示短名「跨端互通」。
|
|
30
30
|
|
|
31
|
-
**本版更新(0.8.
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
自动用 `--config.minimumReleaseAge=0` 重试一次,并在 `logs/update.log` 记一笔「临时放宽」。
|
|
38
|
-
- **npm 最新版读得准了**:原来「第一个能返回的镜像就用」(npmmirror 优先),镜像滞后时会把 0.8.12 读成旧号,
|
|
39
|
-
钉版本也钉错。现在两个源都问、取最大版本。
|
|
40
|
-
- 本机是 `link:` 源码版,点更新仍提示「改源码后重启」;以上对用 npm 安装的其他设备生效。
|
|
31
|
+
**本版更新(0.8.14)**
|
|
32
|
+
|
|
33
|
+
- **「更新」按钮不再看起来没用**:装完新版但还没重启时,卡片多一行「待重启」、
|
|
34
|
+
更新按钮变成不可点的「已装待重启」,接口 `/check` 改回 `needRestart=true` 而不是继续报「有更新」。
|
|
35
|
+
- 根因是状态表达:宿主进程里还是旧版本号,而磁盘上已经装了新版 —— 现在这两个版本分开显示。
|
|
36
|
+
- 本机是 `link:` 源码版(磁盘版本永远等于源码版本),所以这行只会在 npm 安装的设备上出现。
|
|
41
37
|
|
|
42
|
-
**上一版(0.8.
|
|
38
|
+
**上一版(0.8.13)**
|
|
39
|
+
|
|
40
|
+
- **已经是最新就不装**:点「更新」时若 npm 最新版 ≤ 当前版本,直接回「已经是最新(0.8.12),不用更新」,
|
|
41
|
+
不再跑一次 15 秒的安装、也不再重写 `node_modules`。
|
|
42
|
+
- **没写收件人的信优先投给维护者**:跨设备信封没写收件人(或写的名字本机认不出)时,原来按「最近活跃会话」投,
|
|
43
|
+
可能落进 QQ 或无关会话 —— YOCE_C 的 0.8.12 验收报告就投到了 `im:qq_39`。现在先找 `MAINTAINERS.md` 里的
|
|
44
|
+
主维护会话,命中就用它,否则仍按最近活跃投。
|
|
45
|
+
- 本机是 `link:` 源码版,点更新仍提示「改源码后重启」;以上对用 npm 安装的其他设备生效。
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
实测把 0.8.2 装成 0.6.1 还判成功);装完必须**等于**最新版才算成功,装低了自动回滚并报错。
|
|
46
|
-
- **同 id 邮件副本不再各自重投**:一份投递失败时,同 id 的其他副本直接归档。
|
|
47
|
+
**上一版(0.8.12)**
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
- **修「点更新必定失败」**:0.8.11 给三级兜底都加了 `--prefer-online`,而这是 pnpm 11 的非法选项;
|
|
50
|
+
现在只给 pnpm 那步加它认识的 `--config.prefer-offline=false`。
|
|
51
|
+
- **新版本发布不到 24h 也能装**:撞 `minimumReleaseAge` 策略时自动用 `--config.minimumReleaseAge=0` 重试一次。
|
|
52
|
+
- **npm 最新版读得准了**:两个源都问、取最大版本(原来谁先返回用谁,镜像滞后会把版本号读旧)。
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
合并逻辑变成可测纯函数 `mergeDevices({ old, fresh, selfId, ownerDevice })`,配套 20 项单测;
|
|
52
|
-
离线时间按 `at → lastSeen → updatedAt` 先归一毫秒再排。
|
|
53
|
-
- **云端设备只列「别人」**:卡片里那行不再显示本机(自己已经在「当前终端」一栏),
|
|
54
|
-
只列同一账号下的其他设备。
|
|
55
|
-
- **每台带权限和状态**:逐台显示 `名称 · 管理员/成员 · 在线/离线(x 分钟前)· v版本号`;
|
|
56
|
-
管理员就是云端 `_account.json` 里 `ownerDevice` 那台,其余都是成员。
|
|
57
|
-
- `term_list` 的云端行也带角色。
|
|
54
|
+
**更早(0.8.11 / 0.8.10 / 0.8.9 / 0.8.8 / 0.8.7 / 0.8.6 / 0.8.5 / 0.8.0)**
|
|
58
55
|
|
|
59
|
-
- 0.8.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
- 0.8.11:钉死版本号、装低自动回滚;同 id 邮件副本不再各自重投。
|
|
57
|
+
- 0.8.10 / 0.8.9 / 0.8.8 / 0.8.7:云端设备只列别人、带权限与状态、版本仅在差异时显示;插件页标题改「跨端互通(自更新)」;
|
|
58
|
+
入口组件从「总开关」改为「自更新开关」,四个业务组件互不依赖。
|
|
59
|
+
- 0.8.5:负责人留痕 `MAINTAINERS.md`、运行日志可轮转、卡片「维护者」行与「运行日志」按钮。
|
|
63
60
|
- 0.8.0 安全加固:附件名净化 + 越界校验、写接口要 `x-crossterm-token`、信封 HMAC 签名、入站限额、按 id 去重。
|
|
64
61
|
|
|
65
62
|
## 一、安装与卸载
|
package/client/client.js
CHANGED
|
@@ -310,6 +310,7 @@ window.__ModuleLoader__.load({
|
|
|
310
310
|
try {
|
|
311
311
|
const data = await call("/crossterm/api/check", "POST");
|
|
312
312
|
if (!data || !data.ok) setMsg("检查失败:" + ((data && data.message) || "未知错误"));
|
|
313
|
+
else if (data.needRestart) setMsg(`已装 ${data.installed},重启 DSH 后生效(当前进程还在跑 ${data.current})`);
|
|
313
314
|
else if (data.updateAvailable) setMsg(`发现新版本 ${data.latest}(当前 ${data.current})`);
|
|
314
315
|
else if (data.ahead) setMsg(`本地 ${data.current} 领先已发布版 ${data.latest}`);
|
|
315
316
|
else setMsg(`已是最新版本 ${data.current}`);
|
|
@@ -446,7 +447,9 @@ window.__ModuleLoader__.load({
|
|
|
446
447
|
|
|
447
448
|
const version = status ? "v" + status.version : "";
|
|
448
449
|
const mode = status && status.install ? status.install.mode : "";
|
|
449
|
-
|
|
450
|
+
// 0.8.14:磁盘版本 > 进程版本 = 已装好但还没重启,卡片要明说,别再提示「有更新」。
|
|
451
|
+
const installedVer = (status && status.installed) || "";
|
|
452
|
+
const needRestart = Boolean(status && status.needRestart); const cloud = (status && status.cloud) || {};
|
|
450
453
|
const autoUpdate = (status && status.autoUpdate) || {};
|
|
451
454
|
const home = (status && status.home) || null;
|
|
452
455
|
const subtitle = status
|
|
@@ -498,6 +501,12 @@ window.__ModuleLoader__.load({
|
|
|
498
501
|
)
|
|
499
502
|
: null,
|
|
500
503
|
h(FriendlyRow, { label: "当前版本", value: status ? `v${status.version}${mode === "link" ? "(源码 link:)" : ""}` : "" }),
|
|
504
|
+
needRestart
|
|
505
|
+
? h(FriendlyRow, {
|
|
506
|
+
label: "待重启",
|
|
507
|
+
value: `已装 v${installedVer},重启 DSH 后生效(当前进程还在跑 v${status && status.version})`,
|
|
508
|
+
})
|
|
509
|
+
: null,
|
|
501
510
|
h(FriendlyRow, {
|
|
502
511
|
label: "当前终端",
|
|
503
512
|
value: status ? `${status.node}(${status.nodeId})· ${status.hostname}` : "",
|
|
@@ -613,7 +622,16 @@ window.__ModuleLoader__.load({
|
|
|
613
622
|
"div",
|
|
614
623
|
{ style: S.actions },
|
|
615
624
|
h("button", { style: S.btn, onClick: onCheck, disabled: busy !== "" }, busy === "check" ? "检查中…" : "检查更新"),
|
|
616
|
-
h(
|
|
625
|
+
h(
|
|
626
|
+
"button",
|
|
627
|
+
{
|
|
628
|
+
style: needRestart ? S.btn : S.btnPrimary,
|
|
629
|
+
onClick: needRestart ? undefined : onUpdate,
|
|
630
|
+
disabled: busy !== "" || needRestart,
|
|
631
|
+
title: needRestart ? `已装 v${installedVer},重启 DSH 后生效` : "检查并安装最新版本",
|
|
632
|
+
},
|
|
633
|
+
busy === "update" ? "更新中…" : needRestart ? "已装待重启" : "更新",
|
|
634
|
+
),
|
|
617
635
|
h("button", { style: S.btn, onClick: () => void loadDevices(), disabled: Boolean(devices && devices.loading) }, devices && devices.loading ? "读取中…" : "刷新设备"),
|
|
618
636
|
h("button", { style: S.btn, onClick: onRelayCloud, disabled: busy !== "" }, busy === "relay" ? "收信中…" : "收信"),
|
|
619
637
|
h("button", { style: S.btn, onClick: onBackupNow, disabled: busy !== "" }, busy === "backup" ? "备份中…" : "备份会话"),
|
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.14'
|
|
27
27
|
|
|
28
28
|
/** DSH 的家目录:与宿主其它状态(sessions / profiles)同一个根,多终端天然共享 */
|
|
29
29
|
export function dshHome() {
|
package/lib/index.js
CHANGED
|
@@ -41,7 +41,7 @@ import { createCloudHome } from './cloud/home.js'
|
|
|
41
41
|
import { runSetup } from './cloud/setup.js'
|
|
42
42
|
import { deviceRow, matchDevice } from './cloud/match.js'
|
|
43
43
|
import { registerWeb, selfUpdate } from './web.js'
|
|
44
|
-
import { recordOwnership } from './logs.js'
|
|
44
|
+
import { recordOwnership, readMaintainersDoc } from './logs.js'
|
|
45
45
|
|
|
46
46
|
const PLUGIN = 'dsh-crossterm'
|
|
47
47
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
@@ -852,13 +852,22 @@ class CrossTerm {
|
|
|
852
852
|
let fallbackNote = ''
|
|
853
853
|
if (!targets.length) {
|
|
854
854
|
const list = this.sessions()
|
|
855
|
-
|
|
855
|
+
// 0.8.13:信封没写收件人(或写的名字本机认不出)时,优先投给 MAINTAINERS.md 里的主维护会话,
|
|
856
|
+
// 别让跨设备的维护信落进 QQ / 无关会话 —— YOCE_C 的 0.8.12 验收报告就这样投到了 im:qq_39。
|
|
857
|
+
const doc = readMaintainersDoc(ROOT)
|
|
858
|
+
const owner = doc && doc.current
|
|
859
|
+
const ownerIds = owner ? [owner.short, owner.session, owner.id].map((v) => String(v || '').trim()).filter(Boolean) : []
|
|
860
|
+
const owned = ownerIds.length
|
|
861
|
+
? list.find((s) => s && ownerIds.some((id) => s.id === id || s.short === id || String(s.id || '').startsWith(id)))
|
|
862
|
+
: null
|
|
863
|
+
const latest = owned || list.find((s) => s && s.active) || list[0]
|
|
856
864
|
if (!latest) return { ok: false, reason: `收件人「${rawTargets.join('、') || '空'}」在本机找不到会话,本机也没有可投递的会话` }
|
|
857
865
|
targets = [latest.name || latest.short]
|
|
858
866
|
const what = rawTargets.length ? `收件人「${rawTargets.join('、')}」不是本机会话名` : '信封没写收件人'
|
|
859
867
|
const selfNames = [this.nodeId, this.name, os.hostname()].map((v) => String(v || '').trim().toLowerCase())
|
|
860
868
|
const deviceHint = rawTargets.some((t) => selfNames.includes(String(t).trim().toLowerCase())) ? '(写的是本机设备名)' : ''
|
|
861
|
-
|
|
869
|
+
const how = owned ? '按维护者 MAINTAINERS.md' : '按「最近活跃会话」'
|
|
870
|
+
fallbackNote = `(${what}${deviceHint},${how}投给「${targets[0]}」)`
|
|
862
871
|
} else if (unknown.length) {
|
|
863
872
|
fallbackNote = `(收件人「${unknown.join('、')}」没匹配到会话,已投给能匹配的 ${targets.length} 个)`
|
|
864
873
|
}
|
package/lib/web.js
CHANGED
|
@@ -383,6 +383,25 @@ async function updatePlugin(term, info, latest) {
|
|
|
383
383
|
const pin = latest ? `${PKG}@${latest}` : ''
|
|
384
384
|
const target = pin || `${PKG}@latest`
|
|
385
385
|
const before = installedVersion(info)
|
|
386
|
+
// 0.8.13:已经是最新就直接返回,不再白跑一次安装(YOCE_C 实测:等于最新时仍跑了 15.7s
|
|
387
|
+
// 重写 node_modules)。only-when-newer 也避免 @latest 把机器降级。
|
|
388
|
+
if (latest && before && compareVersion(latest, before) <= 0) {
|
|
389
|
+
const out = {
|
|
390
|
+
profile: info.name,
|
|
391
|
+
ok: true,
|
|
392
|
+
updated: false,
|
|
393
|
+
skipped: 'latest',
|
|
394
|
+
via: 'none',
|
|
395
|
+
before,
|
|
396
|
+
installed: before,
|
|
397
|
+
latest,
|
|
398
|
+
downgraded: false,
|
|
399
|
+
rollback: null,
|
|
400
|
+
steps: [],
|
|
401
|
+
}
|
|
402
|
+
appendUpdateLog(term.cfg, out)
|
|
403
|
+
return out
|
|
404
|
+
}
|
|
386
405
|
const steps = []
|
|
387
406
|
const record = async (label, cmd, args, opts) => {
|
|
388
407
|
const started = Date.now()
|
|
@@ -483,6 +502,10 @@ function statusOf(term) {
|
|
|
483
502
|
const cfg = term.cfg || {}
|
|
484
503
|
const info = profileInfo(cfg.busDir)
|
|
485
504
|
const mode = installMode(info && info.spec)
|
|
505
|
+
// 0.8.14:磁盘上装着的版本可能比进程内存里的 VERSION 新(更新完还没重启),
|
|
506
|
+
// 这个状态必须能表达出来,否则卡片一直显示「有更新」、再点又说「已是最新」。
|
|
507
|
+
const onDisk = installedVersion(info) || ''
|
|
508
|
+
const pendingRestart = Boolean(onDisk && compareVersion(onDisk, VERSION) > 0)
|
|
486
509
|
const chains = []
|
|
487
510
|
try {
|
|
488
511
|
for (const link of term.links.values()) if (!chains.includes(link.code)) chains.push(link.code)
|
|
@@ -497,6 +520,8 @@ function statusOf(term) {
|
|
|
497
520
|
return {
|
|
498
521
|
ok: true,
|
|
499
522
|
version: VERSION,
|
|
523
|
+
installed: onDisk,
|
|
524
|
+
needRestart: pendingRestart,
|
|
500
525
|
node: cfg.name || '',
|
|
501
526
|
nodeId: term.nodeId || '',
|
|
502
527
|
hostname: os.hostname(),
|
|
@@ -828,9 +853,15 @@ export function registerWeb(ctx, term, log) {
|
|
|
828
853
|
const latest = await latestVersion()
|
|
829
854
|
// 只有 npm 上的版本「更新」才算有更新:本地源码版常领先于已发布版,
|
|
830
855
|
// 用 !== 会把 0.3.32 说成「发现新版本 0.3.31」。
|
|
831
|
-
const
|
|
832
|
-
const
|
|
833
|
-
|
|
856
|
+
const info = profileInfo(term.cfg && term.cfg.busDir)
|
|
857
|
+
const installed = installedVersion(info) || ''
|
|
858
|
+
// 磁盘上已经装上新版、只是还没重启时:不再报「有更新」,改报「待重启」,
|
|
859
|
+
// 否则用户会以为「点了更新没用」。
|
|
860
|
+
const needRestart = Boolean(installed && compareVersion(installed, latest) >= 0 && compareVersion(installed, VERSION) > 0)
|
|
861
|
+
const base = installed && compareVersion(installed, VERSION) > 0 ? installed : VERSION
|
|
862
|
+
const updateAvailable = !needRestart && compareVersion(latest, base) > 0
|
|
863
|
+
const ahead = !needRestart && compareVersion(VERSION, latest) > 0
|
|
864
|
+
json(res, 200, { ok: true, current: VERSION, installed, latest, updateAvailable, ahead, needRestart })
|
|
834
865
|
} catch (error) {
|
|
835
866
|
json(res, 200, { ok: false, message: String((error && error.message) || error) })
|
|
836
867
|
}
|
|
@@ -980,7 +1011,14 @@ export function registerWeb(ctx, term, log) {
|
|
|
980
1011
|
const steps = (result.steps || []).map((s) => ({ label: s.label, code: s.code, ms: s.ms, out: s.out }))
|
|
981
1012
|
let message
|
|
982
1013
|
if (result.ok && result.updated) message = `已更新到 ${installed},重启 DSH 后生效`
|
|
983
|
-
else if (result.ok)
|
|
1014
|
+
else if (result.ok) {
|
|
1015
|
+
// 已经装上了新版、只是进程还是旧的:这时候要说清「重启后生效」,
|
|
1016
|
+
// 不能只说「已经是最新」,否则用户会觉得点了没反应。
|
|
1017
|
+
const pending = Boolean(installed && compareVersion(installed, VERSION) > 0)
|
|
1018
|
+
message = pending
|
|
1019
|
+
? `已装 ${installed},重启 DSH 后生效(当前进程还在跑 ${VERSION})`
|
|
1020
|
+
: `已经是最新(${installed || latest}),不用更新`
|
|
1021
|
+
}
|
|
984
1022
|
else if (result.downgraded) {
|
|
985
1023
|
message = result.rollback && result.rollback.ok
|
|
986
1024
|
? `更新把版本装低了(原 ${result.before},装成更旧版本),已恢复到 ${installed};请重试`
|