dsh-crossterm 0.8.7 → 0.8.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 +40 -0
- package/README.md +27 -20
- package/client/client.js +49 -1
- package/cordis.patch.yml +12 -11
- package/lib/bus.js +15 -1
- package/lib/index.js +62 -19
- package/lib/web.js +94 -3
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# dsh-crossterm 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.8.9 — 2026-10-19
|
|
4
|
+
|
|
5
|
+
### 第一个组件从「总开关」改成「自更新开关」
|
|
6
|
+
|
|
7
|
+
来源:User said (m10846):**"把第一个组件总开关改为自更新开关,"**;追问后用户选「自动更新开关,四个业务组件不再受它约束」+「默认关」。
|
|
8
|
+
|
|
9
|
+
- **语义变更**:入口行(裸包名 `dsh-crossterm`,`config.part: ui`)不再 provide `crossterm` 服务,
|
|
10
|
+
也不再约束另外四个组件。以前关掉它 → 服务消失 → 跨会话/跨终端/跨设备/跨账号的 `inject` 回调根本不执行
|
|
11
|
+
(工具、心跳、云同步、中转站、会话备份一起停);0.8.9 起五行一律 `root.inject(['agents'], …)`,各挂各的,
|
|
12
|
+
关掉入口行只失去「配置卡片 + 自动更新」。
|
|
13
|
+
- **自动更新**:`lib/web.js` 新增导出 `selfUpdate(term)` —— `profileInfo()` 定位 profile、
|
|
14
|
+
`installMode() === 'link'` 直接跳过(源码版不自动覆盖)、`latestVersion()` 读不到返回失败、
|
|
15
|
+
`compareVersion(latest, VERSION) <= 0` 视为已最新,否则走既有 `updatePlugin()`(dsh → cli → pnpm 三级兜底)。
|
|
16
|
+
装完**不自动重启**,只回 `needRestart: true`,由卡片/日志提示「重启 DSH 后生效」。
|
|
17
|
+
- **定时器**:`lib/index.js` 新增 `autoUpdateStatus()` / `enableAutoUpdate({ immediate })` / `disableAutoUpdate()` /
|
|
18
|
+
`autoUpdateOnce()`;`start()` 在 `cfg.autoUpdate` 为真时启动(首次 30 s 后查一次,之后按周期),`stop()` 清定时器。
|
|
19
|
+
结果记 `this.autoLast` 并写 `logs/update.log`。
|
|
20
|
+
- **配置**:`bus.js` DEFAULTS 新增 `autoUpdate: false`、`autoUpdateIntervalMs: 21600000`(6 小时);
|
|
21
|
+
`readConfig()` 把 `autoUpdate` 归一为布尔、`autoUpdateIntervalMs` 夹到 1 分钟–7 天。
|
|
22
|
+
- **接口**:`status` 新增 `autoUpdate: {enabled, running, intervalMs, last}`;
|
|
23
|
+
新增 `POST /crossterm/api/auto-update/start|stop`(要 `x-crossterm-token`,写 `config.json` 并即时生效)。
|
|
24
|
+
- **界面**:卡片新增「自动更新」一行 + 复用云同步样式的开关;开启时显示周期与上次结果
|
|
25
|
+
(已装 / 已是最新 / 失败原因),关闭时显示「已关闭(打开后自动检查并安装新版本)」。
|
|
26
|
+
- **文案**:`locale/zh.json` 标题改「跨端互通(自更新)」、`en.json` 改 `CrossTerm (self-update)`;
|
|
27
|
+
`cordis.patch.yml` 顶部说明与 ui 行注释同步改写。
|
|
28
|
+
|
|
29
|
+
## 0.8.8 — 2026-10-19
|
|
30
|
+
|
|
31
|
+
### 「维护者」一栏只给维护端看
|
|
32
|
+
|
|
33
|
+
- **问题**:0.8.5 加的「维护者」行任何设备装上插件都会显示负责人是谁,而这是维护端信息,
|
|
34
|
+
别的机器不需要看到。
|
|
35
|
+
- **判断规则**:新增导出纯函数 `maintainerVisible(term, doc)` —— 本机 `nodeId` 必须命中
|
|
36
|
+
`MAINTAINERS.md` 机器可读注释里的 `current` 或 `assistant` 条目(负责人与助手同机时都算
|
|
37
|
+
「本机授权端」);配置 `maintainerDevices`(`bus.js` DEFAULTS 新增,默认 `[]`)可额外放行设备。
|
|
38
|
+
- **接口**:`GET /crossterm/api/about` 的 `maintainers` 增加 `visible`(布尔)与 `me`(本机 nodeId),
|
|
39
|
+
客户端据此决定渲不渲染该行;负责人换机器只改 `MAINTAINERS.md` 的 `current.nodeId`,不用改代码。
|
|
40
|
+
- **测试**:`F:\dsh\001X\crossterm-work\probe-maintainer-visible.mjs` 15 项全过
|
|
41
|
+
(维护端可见、其他设备不可见、助手同机可见、显式授权可见、换机后本机隐藏、无文档/无 nodeId/cfg 为 null 等边界)。
|
|
42
|
+
|
|
3
43
|
## 0.8.7 — 2026-10-19
|
|
4
44
|
|
|
5
45
|
### 修 0.8.6 的回归:云端设备列表整个消失
|
package/README.md
CHANGED
|
@@ -12,46 +12,53 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
|
|
|
12
12
|
|
|
13
13
|
## 零、简介与本版更新(每次发版重写这一节)
|
|
14
14
|
|
|
15
|
-
**插件名称**:`dsh-crossterm`(插件页显示
|
|
15
|
+
**插件名称**:`dsh-crossterm`(插件页显示 **跨端互通(自更新)**)| **插件简介**:同一 DSH 账号下的会话、终端、设备之间互通 —— 特征码链、内联邮件、云端中转站与自动备份。
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
**组件列表(插件页第一个是入口/自更新,后面四个是业务组件,名称与简介来自各自的 manifest)**
|
|
18
18
|
|
|
19
19
|
| 顺序 | 组件名称 | 作用 | 工具 |
|
|
20
20
|
|---|---|---|---|
|
|
21
|
-
| ① |
|
|
21
|
+
| ① | 跨端互通(自更新) | 自更新开关:打开后定期检查 npm 新版本并自动安装(默认关;装完要重启 DSH 生效)。关掉它只失去卡片与自动更新,下面四个组件照常工作。裸包名行,承载插件标题/简介与配置卡片,不挂工具 | — |
|
|
22
22
|
| ② | 跨会话 | 同一终端里的多个会话之间互通:特征码链、内联邮件、收件箱 | `term_link` `term_unlink` `term_broadcast` `term_inbox` `term_config` |
|
|
23
23
|
| ③ | 跨终端 | 同一台机器上的多个 DSH 终端/进程之间互发邮件与文件 | `term_whoami` `term_list` `term_send` `term_send_files` |
|
|
24
24
|
| ④ | 跨设备 | 经云端总线,在多台机器的 DSH 之间互发邮件与文件 | `term_cloud` |
|
|
25
25
|
| ⑤ | 跨账号 | 预留位:后续用于跨账号互通,当前不挂工具 | — |
|
|
26
26
|
|
|
27
|
-
>
|
|
27
|
+
> 入口行为什么必须用裸包名:浏览器 client 半端只对裸包名行生效(`dsh-client-modules` 的
|
|
28
28
|
> `locatePkgJson()` 对子路径行返回 undefined),而插件页顶部标题与这一行共用同一份 `locale/zh.json` 的 meta,
|
|
29
|
-
>
|
|
29
|
+
> 所以它单独做入口(配置卡片 + 自动更新开关),标题带「(自更新)」;卡片头用 `shortTitle` 显示短名「跨端互通」。
|
|
30
30
|
|
|
31
|
-
**本版更新(0.8.
|
|
31
|
+
**本版更新(0.8.9)**
|
|
32
32
|
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
-
|
|
33
|
+
- **第一个组件从「总开关」改成「自更新开关」**:它只管「有没有新版本、要不要自动装」,
|
|
34
|
+
不再管另外四个组件。以前关掉它会连坐停掉工具、心跳、云同步、中转站与备份;现在关掉它只失去卡片与自动更新,
|
|
35
|
+
跨会话 / 跨终端 / 跨设备 / 跨账号 各自独立开关、照常工作。
|
|
36
|
+
- **自动更新默认关**:卡片新增「自动更新」一行与开关,打开后按 `autoUpdateIntervalMs`(默认 6 小时)检查
|
|
37
|
+
npm 最新版并自动安装,结果写进 `logs/update.log`。装完**不会自己重启**,卡片提示「重启 DSH 后生效」;
|
|
38
|
+
关掉后仍可手动点「检查更新 / 更新」。
|
|
39
|
+
- **服务注入拆掉**:0.7.x–0.8.8 是 `ui` 行 `root.provide('crossterm')`、其余四行 `inject` 它;
|
|
40
|
+
0.8.9 起五行都只 `inject(['agents'])`,各挂各的。
|
|
41
|
+
- 新增配置:`autoUpdate`(默认 `false`)、`autoUpdateIntervalMs`(默认 `21600000`,即 6 小时,范围 1 分钟–7 天)。
|
|
42
42
|
|
|
43
|
-
**上一版(0.8.
|
|
43
|
+
**上一版(0.8.8)**
|
|
44
44
|
|
|
45
|
+
- **「维护者」一栏只给维护端看**:以前任何设备装上插件都能看到负责人是谁;现在只在维护端显示 ——
|
|
46
|
+
本机 `nodeId` 必须出现在 `MAINTAINERS.md` 的负责人/助手条目里(两者在同一台机器上时都算),
|
|
47
|
+
或用配置 `maintainerDevices` 显式放行。其他设备上这行直接不渲染。
|
|
48
|
+
- **换机器只改一个字段**:负责人换机器时,把 `MAINTAINERS.md` 里 `current.nodeId` 改成新机器,
|
|
49
|
+
旧机器下次刷新卡片自动隐藏,不用改代码。
|
|
50
|
+
|
|
51
|
+
**更早(0.8.7 / 0.8.6 / 0.8.5 / 0.8.2 / 0.8.0)**
|
|
52
|
+
|
|
53
|
+
- 0.8.7:修 0.8.6 的回归(卡片「云端设备」整块消失,接口报 `layout is not defined`);
|
|
54
|
+
合并逻辑变成可测纯函数 `mergeDevices({ old, fresh, selfId, ownerDevice })`,配套 20 项单测;
|
|
55
|
+
离线时间按 `at → lastSeen → updatedAt` 先归一毫秒再排。
|
|
45
56
|
- **云端设备只列「别人」**:卡片里那行不再显示本机(自己已经在「当前终端」一栏),
|
|
46
57
|
只列同一账号下的其他设备。
|
|
47
58
|
- **每台带权限和状态**:逐台显示 `名称 · 管理员/成员 · 在线/离线(x 分钟前)· v版本号`;
|
|
48
59
|
管理员就是云端 `_account.json` 里 `ownerDevice` 那台,其余都是成员。
|
|
49
|
-
- **离线时间不再显示「未知」**:以前只认 `updatedAt`,新布局心跳写的是 `at`,
|
|
50
|
-
离线设备一律「未知」;现在按 `updatedAt → at → lastSeen` 依次取。
|
|
51
60
|
- `term_list` 的云端行也带角色。
|
|
52
61
|
|
|
53
|
-
**更早(0.8.5 / 0.8.2 / 0.8.0)**
|
|
54
|
-
|
|
55
62
|
- 0.8.5:负责人留痕 `MAINTAINERS.md`、运行日志可轮转(5 MB 压 `logs/archive/` 留 20 份)、
|
|
56
63
|
卡片「维护者」行与「运行日志」按钮、`GET /crossterm/api/logs`。
|
|
57
64
|
- 0.8.2:死信按 20s/1m/5m/15m/1h 退避,8 次后进 `dead/`;收件人写设备名会回退「最近活跃会话」;
|
package/client/client.js
CHANGED
|
@@ -356,6 +356,21 @@ window.__ModuleLoader__.load({
|
|
|
356
356
|
}
|
|
357
357
|
};
|
|
358
358
|
|
|
359
|
+
const onToggleAutoUpdate = async () => {
|
|
360
|
+
const enabled = Boolean(status && status.autoUpdate && status.autoUpdate.enabled);
|
|
361
|
+
setBusy("auto");
|
|
362
|
+
setMsg(enabled ? "正在关闭自动更新…" : "正在开启自动更新…");
|
|
363
|
+
try {
|
|
364
|
+
const data = await call(enabled ? "/crossterm/api/auto-update/stop" : "/crossterm/api/auto-update/start", "POST");
|
|
365
|
+
setMsg((data && data.message) || (data && data.ok ? "已切换" : "未能切换"));
|
|
366
|
+
if (data && data.autoUpdate && status) setStatus({ ...status, autoUpdate: data.autoUpdate });
|
|
367
|
+
} catch (error) {
|
|
368
|
+
setMsg("切换失败:" + String((error && error.message) || error));
|
|
369
|
+
} finally {
|
|
370
|
+
setBusy("");
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
359
374
|
const onSetupCloud = async () => {
|
|
360
375
|
setBusy("setup");
|
|
361
376
|
setMsg("正在打开记事本…把 AppKey / AppSecret 填进引号里,保存并关闭记事本");
|
|
@@ -428,6 +443,7 @@ window.__ModuleLoader__.load({
|
|
|
428
443
|
const version = status ? "v" + status.version : "";
|
|
429
444
|
const mode = status && status.install ? status.install.mode : "";
|
|
430
445
|
const cloud = (status && status.cloud) || {};
|
|
446
|
+
const autoUpdate = (status && status.autoUpdate) || {};
|
|
431
447
|
const home = (status && status.home) || null;
|
|
432
448
|
const subtitle = status
|
|
433
449
|
? `${status.node || "本机"} · ${status.chains && status.chains.length ? status.chains.join(" / ") : "未入链"}`
|
|
@@ -482,7 +498,7 @@ window.__ModuleLoader__.load({
|
|
|
482
498
|
label: "当前终端",
|
|
483
499
|
value: status ? `${status.node}(${status.nodeId})· ${status.hostname}` : "",
|
|
484
500
|
}),
|
|
485
|
-
about && about.maintainers
|
|
501
|
+
about && about.maintainers && about.maintainers.visible === true
|
|
486
502
|
? h(FriendlyRow, {
|
|
487
503
|
label: "维护者",
|
|
488
504
|
value: maintainersText(about),
|
|
@@ -557,6 +573,38 @@ window.__ModuleLoader__.load({
|
|
|
557
573
|
home && home.backup
|
|
558
574
|
? h(FriendlyRow, { label: "会话备份", value: backupText(home.backup) })
|
|
559
575
|
: null,
|
|
576
|
+
h(
|
|
577
|
+
"div",
|
|
578
|
+
{ style: S.grid },
|
|
579
|
+
h("div", { style: S.key }, "自动更新"),
|
|
580
|
+
h(
|
|
581
|
+
"div",
|
|
582
|
+
{ style: S.val },
|
|
583
|
+
h(
|
|
584
|
+
"span",
|
|
585
|
+
null,
|
|
586
|
+
autoUpdate.enabled
|
|
587
|
+
? `已开启 · 每 ${Math.max(1, Math.round((autoUpdate.intervalMs || 0) / 3600000))} 小时检查一次${autoUpdate.last
|
|
588
|
+
? `;上次:${autoUpdate.last.updated ? `已装 ${autoUpdate.last.installed || ""}` : autoUpdate.last.ok ? "已是最新" : `失败(${autoUpdate.last.message || "未知"})`}`
|
|
589
|
+
: ""}`
|
|
590
|
+
: "已关闭(打开后自动检查并安装新版本)",
|
|
591
|
+
),
|
|
592
|
+
h(
|
|
593
|
+
"button",
|
|
594
|
+
{
|
|
595
|
+
style: autoUpdate.enabled ? S.switchOn : S.switchOff,
|
|
596
|
+
onClick: (event) => {
|
|
597
|
+
event.stopPropagation();
|
|
598
|
+
void onToggleAutoUpdate();
|
|
599
|
+
},
|
|
600
|
+
disabled: busy !== "",
|
|
601
|
+
title: autoUpdate.enabled ? "点击关闭自动更新" : "点击开启自动更新",
|
|
602
|
+
"aria-label": autoUpdate.enabled ? "自动更新开启中" : "自动更新已关闭",
|
|
603
|
+
},
|
|
604
|
+
h("span", { style: autoUpdate.enabled ? S.knobOn : S.knobOff }),
|
|
605
|
+
),
|
|
606
|
+
),
|
|
607
|
+
),
|
|
560
608
|
h(
|
|
561
609
|
"div",
|
|
562
610
|
{ style: S.actions },
|
package/cordis.patch.yml
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
# 每个 insert 行在 DSH「插件 → 跨端互通 → 包含的组件」里是一行组件,可单独开关。
|
|
11
11
|
# 五行指向同一个包,只是走不同子路径 / 不同 config.part:
|
|
12
12
|
#
|
|
13
|
-
# ① dsh-crossterm
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
13
|
+
# ① dsh-crossterm 自更新(入口):插件页标题/简介/配置卡片,不挂任何工具
|
|
14
|
+
# (config.part: ui)。0.8.9 起它不再是总开关 —— 关掉它只失去
|
|
15
|
+
# 卡片与「自动更新」,下面四个组件照常跑。
|
|
16
|
+
# 自动更新默认关,开关在卡片里,值落 config.json 的 autoUpdate。
|
|
17
17
|
# ② dsh-crossterm/parts/session 跨会话:term_link / term_unlink / term_broadcast / term_inbox / term_config
|
|
18
18
|
# ③ dsh-crossterm/parts/terminal 跨终端:term_whoami / term_list / term_send / term_send_files
|
|
19
19
|
# ④ dsh-crossterm/parts/device 跨设备:term_cloud(云端总线同步)
|
|
@@ -23,13 +23,14 @@
|
|
|
23
23
|
# "./parts/session/package.json" -> "./parts/session.manifest.json"
|
|
24
24
|
# 每个组件显示自己的中文名,实际加载的仍是 lib/index.js,靠 config.part 决定挂哪组工具。
|
|
25
25
|
#
|
|
26
|
-
#
|
|
26
|
+
# 为什么入口行必须用「裸包名」:浏览器的 client 半端只对裸包名行生效
|
|
27
27
|
# (dsh-client-modules 的 locatePkgJson 对子路径行直接返回 undefined),而插件页顶部标题
|
|
28
|
-
# 与裸包名行共用同一份 locale/zh.json 的 meta ——
|
|
29
|
-
#
|
|
28
|
+
# 与裸包名行共用同一份 locale/zh.json 的 meta —— 所以裸包名行单独做入口(卡片 + 自动更新),
|
|
29
|
+
# 标题写「跨端互通(自更新)」,四个业务组件排在它后面、名字仍是跨会话/跨终端/跨设备/跨账号。
|
|
30
30
|
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
31
|
+
# 0.8.9 起五个组件互不依赖:谁都不用 provide/inject 别人,各挂各的工具与开销。
|
|
32
|
+
# 关掉入口行只失去「配置卡片 + 自动更新」,四个业务组件照常工作、各自仍可单独关。
|
|
33
|
+
# 之前(0.7.x–0.8.8)是 ui 行 provide('crossterm')、其余四行 inject 它,关入口会连坐——已废弃。
|
|
33
34
|
#
|
|
34
35
|
# 五个组件在同一 DSH 进程里共用一份总线实例:心跳只起一次,云同步只在 device 组件启用
|
|
35
36
|
# 且 config.json 里 cloudEnabled=true 时才跑。安装/改动后需重启对应的 DSH 进程。
|
|
@@ -37,8 +38,8 @@
|
|
|
37
38
|
|
|
38
39
|
- insert:
|
|
39
40
|
- id: crossterm-ui
|
|
40
|
-
#
|
|
41
|
-
#
|
|
41
|
+
# 入口(自更新):裸包名,浏览器半端的配置卡片 + 插件页标题/简介 + 「自动更新」开关。
|
|
42
|
+
# 关掉这一行只失去卡片与自动更新,四个业务组件不受影响;它自己不挂工具。
|
|
42
43
|
name: dsh-crossterm
|
|
43
44
|
config:
|
|
44
45
|
part: ui
|
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.9'
|
|
27
27
|
|
|
28
28
|
/** DSH 的家目录:与宿主其它状态(sessions / profiles)同一个根,多终端天然共享 */
|
|
29
29
|
export function dshHome() {
|
|
@@ -79,6 +79,13 @@ const DEFAULTS = {
|
|
|
79
79
|
backupSessionsPerRun: 20,
|
|
80
80
|
backupAttachmentsPerRun: 20,
|
|
81
81
|
attachmentBackupEnabled: true,
|
|
82
|
+
// 卡片「维护者」一栏只在维护端显示:默认按 MAINTAINERS.md 里的 nodeId 判断,
|
|
83
|
+
// 这里可以额外放行别的设备(例如用户授权过的本机另装环境)。
|
|
84
|
+
maintainerDevices: [],
|
|
85
|
+
// 自动更新(0.8.9):入口组件不再当总开关,只负责这件事 —— 打开后每 autoUpdateIntervalMs
|
|
86
|
+
// 检查一次 npm 上的新版本,有更新就装;装完要重启 DSH 才生效,绝不自动重启。
|
|
87
|
+
autoUpdate: false,
|
|
88
|
+
autoUpdateIntervalMs: 6 * 60 * 60 * 1000,
|
|
82
89
|
baiduAppFile: '',
|
|
83
90
|
baiduAuthFile: '',
|
|
84
91
|
cloudStateFile: '',
|
|
@@ -139,6 +146,13 @@ export function readConfig() {
|
|
|
139
146
|
cfg.maxFileBytes = clampNum(cfg.maxFileBytes, 1024, 2 * 1024 * 1024 * 1024, DEFAULTS.maxFileBytes)
|
|
140
147
|
cfg.maxFiles = clampNum(cfg.maxFiles, 1, 50, DEFAULTS.maxFiles)
|
|
141
148
|
cfg.deliverAttempts = clampNum(cfg.deliverAttempts, 1, 20, DEFAULTS.deliverAttempts)
|
|
149
|
+
cfg.autoUpdate = cfg.autoUpdate === true
|
|
150
|
+
cfg.autoUpdateIntervalMs = clampNum(
|
|
151
|
+
cfg.autoUpdateIntervalMs,
|
|
152
|
+
60000,
|
|
153
|
+
7 * 24 * 60 * 60 * 1000,
|
|
154
|
+
DEFAULTS.autoUpdateIntervalMs,
|
|
155
|
+
)
|
|
142
156
|
return cfg
|
|
143
157
|
}
|
|
144
158
|
|
package/lib/index.js
CHANGED
|
@@ -40,7 +40,7 @@ import { createCloud } from './cloud/sync.js'
|
|
|
40
40
|
import { createCloudHome } from './cloud/home.js'
|
|
41
41
|
import { runSetup } from './cloud/setup.js'
|
|
42
42
|
import { deviceRow, matchDevice } from './cloud/match.js'
|
|
43
|
-
import { registerWeb } from './web.js'
|
|
43
|
+
import { registerWeb, selfUpdate } from './web.js'
|
|
44
44
|
import { recordOwnership } from './logs.js'
|
|
45
45
|
|
|
46
46
|
const PLUGIN = 'dsh-crossterm'
|
|
@@ -1396,6 +1396,59 @@ class CrossTerm {
|
|
|
1396
1396
|
return true
|
|
1397
1397
|
}
|
|
1398
1398
|
|
|
1399
|
+
// ── 自动更新(0.8.9)──────────────────────────────────────────────────
|
|
1400
|
+
// 入口组件不再是总开关,它只负责「有没有新版本」。开关落在 cfg.autoUpdate,
|
|
1401
|
+
// 卡片上的开关走 /crossterm/api/auto-update/start|stop 写进 config.json。
|
|
1402
|
+
autoUpdateStatus() {
|
|
1403
|
+
const cfg = this.cfg || {}
|
|
1404
|
+
return {
|
|
1405
|
+
enabled: cfg.autoUpdate === true,
|
|
1406
|
+
running: Boolean(this.autoTimer),
|
|
1407
|
+
intervalMs: Number(cfg.autoUpdateIntervalMs) || 0,
|
|
1408
|
+
last: this.autoLast || null,
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
enableAutoUpdate({ immediate = false } = {}) {
|
|
1413
|
+
if (this.autoTimer) return false
|
|
1414
|
+
const every = Math.max(60000, Number(this.cfg && this.cfg.autoUpdateIntervalMs) || 6 * 60 * 60 * 1000)
|
|
1415
|
+
const tick = () => { void this.autoUpdateOnce() }
|
|
1416
|
+
this.autoTimer = setInterval(tick, every)
|
|
1417
|
+
if (typeof this.autoTimer.unref === 'function') this.autoTimer.unref()
|
|
1418
|
+
// 首次不用等满一个周期:30 秒后先查一次,之后按周期走
|
|
1419
|
+
const first = setTimeout(tick, immediate ? 5000 : 30000)
|
|
1420
|
+
if (typeof first.unref === 'function') first.unref()
|
|
1421
|
+
void this.note(`auto-update: 已开启,每 ${Math.max(1, Math.round(every / 3600000))} 小时检查一次`)
|
|
1422
|
+
return true
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
disableAutoUpdate() {
|
|
1426
|
+
if (!this.autoTimer) return false
|
|
1427
|
+
clearInterval(this.autoTimer)
|
|
1428
|
+
this.autoTimer = undefined
|
|
1429
|
+
void this.note('auto-update: 已关闭')
|
|
1430
|
+
return true
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
async autoUpdateOnce() {
|
|
1434
|
+
let result
|
|
1435
|
+
try {
|
|
1436
|
+
result = await selfUpdate(this)
|
|
1437
|
+
} catch (e) {
|
|
1438
|
+
result = { ok: false, message: (e && e.message) || String(e) }
|
|
1439
|
+
}
|
|
1440
|
+
this.autoLast = { at: Date.now(), ...result }
|
|
1441
|
+
try {
|
|
1442
|
+
const line = result.updated
|
|
1443
|
+
? `已装 ${result.installed || ''}(重启后生效)`
|
|
1444
|
+
: result.ok
|
|
1445
|
+
? `${result.skipped ? '跳过' : '已是最新'} ${result.latest || VERSION}`
|
|
1446
|
+
: `失败 ${result.message || ''}`
|
|
1447
|
+
await this.note(`auto-update: ${line}`)
|
|
1448
|
+
} catch { /* ignore */ }
|
|
1449
|
+
return result
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1399
1452
|
async start() {
|
|
1400
1453
|
if (this.started) return
|
|
1401
1454
|
this.started = true
|
|
@@ -1409,6 +1462,9 @@ class CrossTerm {
|
|
|
1409
1462
|
if (this.cfg.cloudEnabled && wantsCloud) {
|
|
1410
1463
|
try { this.enableCloud() } catch (e) { void this.warn('cloud sync 启动失败:' + ((e && e.message) || String(e))) }
|
|
1411
1464
|
}
|
|
1465
|
+
if (this.cfg.autoUpdate) {
|
|
1466
|
+
try { this.enableAutoUpdate() } catch (e) { void this.warn('auto-update 启动失败:' + ((e && e.message) || String(e))) }
|
|
1467
|
+
}
|
|
1412
1468
|
await this.beat()
|
|
1413
1469
|
await this.note(`started node=${this.nodeId} name=${this.cfg.name} bus=${this.bus.root}`)
|
|
1414
1470
|
this.timer = setInterval(() => { void this.tick() }, this.cfg.pollMs)
|
|
@@ -1422,6 +1478,7 @@ class CrossTerm {
|
|
|
1422
1478
|
if (this.timer) clearInterval(this.timer)
|
|
1423
1479
|
this.timer = undefined
|
|
1424
1480
|
if (this.cloud) { this.cloud.stop(); this.cloud = null }
|
|
1481
|
+
if (this.autoTimer) { clearInterval(this.autoTimer); this.autoTimer = undefined }
|
|
1425
1482
|
if (this.home) { this.home.stop(); this.home = null }
|
|
1426
1483
|
await this.bus.leave(this.nodeId).catch(() => {})
|
|
1427
1484
|
await this.note('stopped')
|
|
@@ -2017,24 +2074,10 @@ export default {
|
|
|
2017
2074
|
name: PLUGIN,
|
|
2018
2075
|
apply(root, config) {
|
|
2019
2076
|
const part = normalizePart(config?.part)
|
|
2020
|
-
//
|
|
2021
|
-
//
|
|
2022
|
-
//
|
|
2023
|
-
|
|
2024
|
-
const isMaster = part === 'ui'
|
|
2025
|
-
if (isMaster) {
|
|
2026
|
-
root.provide('crossterm', { master: true, part, since: Date.now() })
|
|
2027
|
-
} else {
|
|
2028
|
-
const timer = setTimeout(() => {
|
|
2029
|
-
let alive = true
|
|
2030
|
-
try {
|
|
2031
|
-
alive = Boolean(root.get?.('crossterm'))
|
|
2032
|
-
} catch { alive = true }
|
|
2033
|
-
if (!alive) console.warn(`[dsh-crossterm] 总开关(入口组件)未启用,组件「${PART_LABEL[part]}」已跳过挂载`)
|
|
2034
|
-
}, 3000)
|
|
2035
|
-
if (timer && typeof timer.unref === 'function') timer.unref()
|
|
2036
|
-
}
|
|
2037
|
-
root.inject(isMaster ? ['agents'] : ['agents', 'crossterm'], (ctx) => {
|
|
2077
|
+
// 0.8.9 起「入口」组件不再当总开关:它只负责插件卡片与「自动更新」,
|
|
2078
|
+
// 四个业务组件(跨会话/跨终端/跨设备/跨账号)各挂各的 —— 关掉入口行不会连坐。
|
|
2079
|
+
// 自动更新默认关(cfg.autoUpdate=false),开关在卡片里,改了落 config.json。
|
|
2080
|
+
root.inject(['agents'], (ctx) => {
|
|
2038
2081
|
// 四个组件跑在同一个进程里,共用一份总线实例:各自只挂自己那组工具,
|
|
2039
2082
|
// 心跳/云同步只在第一个组件加载时启动一次,最后一个组件卸载时才停。
|
|
2040
2083
|
const term = acquireTerm(ctx)
|
package/lib/web.js
CHANGED
|
@@ -429,6 +429,26 @@ async function updatePlugin(term, info, latest) {
|
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
+
/**
|
|
433
|
+
* 自动更新(0.8.9):给定时器用的一步式「检查 + 安装」。
|
|
434
|
+
* 只在 registry 安装模式下有意义;link: 源码版直接跳过(代码就在工作区里)。
|
|
435
|
+
* 装完不重启,由调用方提示「重启 DSH 后生效」。
|
|
436
|
+
*/
|
|
437
|
+
export async function selfUpdate(term) {
|
|
438
|
+
const info = profileInfo(term.cfg && term.cfg.busDir)
|
|
439
|
+
if (!info) return { ok: false, updated: false, current: VERSION, latest: '', message: '找不到安装本插件的 profile' }
|
|
440
|
+
if (installMode(info.spec) === 'link') {
|
|
441
|
+
return { ok: true, updated: false, skipped: 'link', current: VERSION, latest: '' }
|
|
442
|
+
}
|
|
443
|
+
const latest = await latestVersion().catch(() => '')
|
|
444
|
+
if (!latest) return { ok: false, updated: false, current: VERSION, latest: '', message: '读不到 npm 最新版本' }
|
|
445
|
+
if (compareVersion(latest, VERSION) <= 0) {
|
|
446
|
+
return { ok: true, updated: false, current: VERSION, latest }
|
|
447
|
+
}
|
|
448
|
+
const result = await updatePlugin(term, info, latest)
|
|
449
|
+
return { ...result, current: VERSION, latest, needRestart: Boolean(result.ok && result.updated) }
|
|
450
|
+
}
|
|
451
|
+
|
|
432
452
|
function statusOf(term) {
|
|
433
453
|
const cfg = term.cfg || {}
|
|
434
454
|
const info = profileInfo(cfg.busDir)
|
|
@@ -468,6 +488,11 @@ function statusOf(term) {
|
|
|
468
488
|
provider,
|
|
469
489
|
},
|
|
470
490
|
chains,
|
|
491
|
+
// 自动更新(0.8.9):入口组件不再是总开关,开关与状态都走这里
|
|
492
|
+
autoUpdate:
|
|
493
|
+
typeof term.autoUpdateStatus === 'function'
|
|
494
|
+
? term.autoUpdateStatus()
|
|
495
|
+
: { enabled: cfg.autoUpdate === true, running: false, intervalMs: 0, last: null },
|
|
471
496
|
// 云端目录(0.3.39):账号/角色/设备目录/中转站/会话备份/设备心跳
|
|
472
497
|
home: homeStatus,
|
|
473
498
|
}
|
|
@@ -506,7 +531,28 @@ function readMaintainers() {
|
|
|
506
531
|
return readMaintainersDoc(ROOT)
|
|
507
532
|
}
|
|
508
533
|
|
|
509
|
-
|
|
534
|
+
/**
|
|
535
|
+
* 「维护者」只给维护端看:本机 nodeId 必须出现在 MAINTAINERS.md 的
|
|
536
|
+
* 负责人/助手条目里,或用配置 `maintainerDevices` 额外授权;其他设备一律不给这行。
|
|
537
|
+
*/
|
|
538
|
+
export function maintainerVisible(term, doc) {
|
|
539
|
+
if (!doc) return false
|
|
540
|
+
const self = String((term && term.nodeId) || '')
|
|
541
|
+
if (!self) return false
|
|
542
|
+
const allow = new Set()
|
|
543
|
+
const add = (item) => {
|
|
544
|
+
if (item && item.nodeId) allow.add(String(item.nodeId))
|
|
545
|
+
}
|
|
546
|
+
add(doc.current)
|
|
547
|
+
add(doc.assistant)
|
|
548
|
+
try {
|
|
549
|
+
const extra = term && term.cfg && Array.isArray(term.cfg.maintainerDevices) ? term.cfg.maintainerDevices : []
|
|
550
|
+
for (const item of extra) allow.add(String(item))
|
|
551
|
+
} catch { /* ignore */ }
|
|
552
|
+
return allow.has(self)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function aboutOf(term) {
|
|
510
556
|
if (aboutCache && aboutCache.version === VERSION) return aboutCache
|
|
511
557
|
let meta = {}
|
|
512
558
|
try {
|
|
@@ -524,6 +570,7 @@ function aboutOf() {
|
|
|
524
570
|
description: doc.description || '',
|
|
525
571
|
}
|
|
526
572
|
})
|
|
573
|
+
const maintainers = readMaintainers()
|
|
527
574
|
aboutCache = {
|
|
528
575
|
name: PKG,
|
|
529
576
|
version: VERSION,
|
|
@@ -532,7 +579,9 @@ function aboutOf() {
|
|
|
532
579
|
shortTitle: meta.shortTitle || meta.title || PKG,
|
|
533
580
|
description: meta.description || '',
|
|
534
581
|
components,
|
|
535
|
-
maintainers:
|
|
582
|
+
maintainers: maintainers
|
|
583
|
+
? { ...maintainers, visible: maintainerVisible(term, maintainers), me: String((term && term.nodeId) || '') }
|
|
584
|
+
: null,
|
|
536
585
|
changelog: readChangelog(),
|
|
537
586
|
}
|
|
538
587
|
return aboutCache
|
|
@@ -578,7 +627,7 @@ export function registerWeb(ctx, term, log) {
|
|
|
578
627
|
path: '/crossterm/api/about',
|
|
579
628
|
handler: (req, res) => {
|
|
580
629
|
try {
|
|
581
|
-
json(res, 200, { ok: true, ...aboutOf() })
|
|
630
|
+
json(res, 200, { ok: true, ...aboutOf(term) })
|
|
582
631
|
} catch (error) {
|
|
583
632
|
json(res, 200, { ok: false, message: String((error && error.message) || error) })
|
|
584
633
|
}
|
|
@@ -797,6 +846,48 @@ export function registerWeb(ctx, term, log) {
|
|
|
797
846
|
},
|
|
798
847
|
}),
|
|
799
848
|
)
|
|
849
|
+
disposers.push(
|
|
850
|
+
webCtx.webServer.register({
|
|
851
|
+
kind: 'exact',
|
|
852
|
+
path: '/crossterm/api/auto-update/start',
|
|
853
|
+
handler: async (req, res) => {
|
|
854
|
+
if (!checkHttp(req, res, term.cfg)) return
|
|
855
|
+
try {
|
|
856
|
+
await writeConfig({ autoUpdate: true })
|
|
857
|
+
term.cfg = readConfig()
|
|
858
|
+
term.enableAutoUpdate()
|
|
859
|
+
json(res, 200, {
|
|
860
|
+
ok: true,
|
|
861
|
+
message: '自动更新已开启:发现新版本会自动安装,装完重启 DSH 生效',
|
|
862
|
+
autoUpdate: statusOf(term).autoUpdate,
|
|
863
|
+
})
|
|
864
|
+
} catch (error) {
|
|
865
|
+
json(res, 200, { ok: false, message: `开启失败:${String((error && error.message) || error)}` })
|
|
866
|
+
}
|
|
867
|
+
},
|
|
868
|
+
}),
|
|
869
|
+
)
|
|
870
|
+
disposers.push(
|
|
871
|
+
webCtx.webServer.register({
|
|
872
|
+
kind: 'exact',
|
|
873
|
+
path: '/crossterm/api/auto-update/stop',
|
|
874
|
+
handler: async (req, res) => {
|
|
875
|
+
if (!checkHttp(req, res, term.cfg)) return
|
|
876
|
+
try {
|
|
877
|
+
await writeConfig({ autoUpdate: false })
|
|
878
|
+
term.cfg = readConfig()
|
|
879
|
+
term.disableAutoUpdate()
|
|
880
|
+
json(res, 200, {
|
|
881
|
+
ok: true,
|
|
882
|
+
message: '自动更新已关闭(仍可手动点「检查更新 / 更新」)',
|
|
883
|
+
autoUpdate: statusOf(term).autoUpdate,
|
|
884
|
+
})
|
|
885
|
+
} catch (error) {
|
|
886
|
+
json(res, 200, { ok: false, message: `关闭失败:${String((error && error.message) || error)}` })
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
}),
|
|
890
|
+
)
|
|
800
891
|
disposers.push(
|
|
801
892
|
webCtx.webServer.register({
|
|
802
893
|
kind: 'exact',
|
package/locale/en.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
|
-
"title": "CrossTerm (
|
|
3
|
+
"title": "CrossTerm (self-update)",
|
|
4
4
|
"shortTitle": "CrossTerm",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Self-update switch: when on, the plugin periodically checks npm for a newer version and installs it (off by default; restart DSH afterwards to take effect — it never restarts on its own). Turning it off does not affect the four components below — cross-session / cross-terminal / cross-device / cross-account each have their own switch. About: synchronize sessions, terminals and devices under one DSH account, with feature-code chains, inline mail, a cloud relay and automatic session backups."
|
|
6
6
|
}
|
|
7
7
|
}
|
package/locale/zh.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
|
-
"title": "
|
|
3
|
+
"title": "跨端互通(自更新)",
|
|
4
4
|
"shortTitle": "跨端互通",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "自更新开关:打开后插件会定期检查 npm 上的新版本并自动安装(默认关;装完要重启 DSH 才生效,不会自己重启)。关掉它不影响下面四个组件 —— 跨会话 / 跨终端 / 跨设备 / 跨账号 各自独立开关。插件简介:同一 DSH 账号下的会话、终端、设备之间互通,支持特征码链、内联邮件、云端中转站与会话自动备份。"
|
|
6
6
|
}
|
|
7
7
|
}
|