dsh-crossterm 0.9.2 → 0.9.4
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 +21 -0
- package/MAINTAINERS.md +48 -5
- package/README.md +10 -11
- package/components/restart/core.js +49 -0
- package/components/restart/manifest.json +1 -1
- package/components/restart/scripts/busy-check.mjs +39 -1
- package/components/restart/scripts/countdown.html +24 -13
- package/components/restart/scripts/dsh-restart-helper.mjs +29 -2
- package/components/ui/manifest.json +3 -3
- package/lib/bus.js +1 -1
- package/lib/index.js +2 -2
- package/lib/web.js +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
## 0.9.4 — 2026-10-20
|
|
2
|
+
|
|
3
|
+
自动重启组件 **0.3.28 → 0.3.29**(只动 `components/restart/**`;由该组件负责人 `a3e9f80e` 提交、主维护会话发版)。插件内其余五个组件不变。
|
|
4
|
+
|
|
5
|
+
- **忙时判定补第三路信号**:`components/restart/core.js` 每 2 秒把所有 agent 的 `{sessionId,inTurn}` 写 `logs/busy-agents.json`;`scripts/busy-check.mjs` 新增 `heartbeatBusy()`(文件 8 秒内有效,`inTurn` 且不是自己即算忙),`othersBusy()` 合并「市场运行列表 + transcript 写入时间 + 回合心跳」三路,`sources` 记 `heartbeat:busy-agents` / `heartbeat:unavailable`。
|
|
6
|
+
- 背景:长工具调用期间 transcript 不写盘,市场 `runningAgents` 也会掉出列表 —— 旧的两路判据会把「真在跑」判成空闲,于是在别人干活时重启。
|
|
7
|
+
- **守卫终检**:helper 的等待下限 3000 → **250 ms**;守卫通过后 400 ms 再探一次,有人开工就带剩余预算重新等(最多 3 次,日志 `idle guard recheck#N`)。
|
|
8
|
+
- **倒计时与正计时对齐**:`scripts/countdown.html` 归零后先停在 `00:00` + 「倒计时结束,正在准备重启…」,只有 helper 的 ui-state 进 `killing`/`starting` 才切 `+mm:ss`。
|
|
9
|
+
- 背景:helper 起得晚时窗口已归零而 helper 还在等,被读成「没读完就变正计时」,实际 kill 还在 `scheduledAt` 之后 1~5 秒。
|
|
10
|
+
|
|
11
|
+
> 本机是 `link:` 源码版:重启后 `ensureRestartRuntime()` 因版本变化(`.version` 0.9.3 → 0.9.4)把 8 个脚本整组重拷到 `<总线目录>\restart-module`。
|
|
12
|
+
|
|
13
|
+
## 0.9.3 — 2026-10-20
|
|
14
|
+
|
|
15
|
+
修 0.9.2 的两处显示缺陷(代码逻辑没变,只动 `GET /crossterm/api/about` 与 ui 组件清单):
|
|
16
|
+
|
|
17
|
+
- **组件表漏了第 ⑥ 个**:`lib/web.js` 的 `PART_ORDER` 还停在四个业务组件,自动重启没进 `/about.components`,卡片「包含的组件」里看不到它。补 `restart`。
|
|
18
|
+
- **入口组件名字是 "ui"**:`components/ui/manifest.json` 的 `name` 没写人读文案。改成「跨端互通(入口)」,并补一句简介。
|
|
19
|
+
- `PART_ORDER` 现在按插件页顺序给六个组件:入口、跨会话、跨终端、跨设备、跨账号、自动重启;每个都带 `version`。
|
|
20
|
+
- **组件版本号按新规矩各升各的**:这次只动 ui 组件 → `components/ui/manifest.json` 0.9.2 → 0.9.3;其余五个仍是 0.9.2,自动重启仍是 0.3.28。
|
|
21
|
+
|
|
1
22
|
## 0.9.2 — 2026-10-20
|
|
2
23
|
|
|
3
24
|
**组件拆成目录 + 每个组件有自己的版本号**(User said:组件文件分开存放、插件页每个组件显示版本、改到哪个组件就升哪个)。
|
package/MAINTAINERS.md
CHANGED
|
@@ -27,14 +27,49 @@
|
|
|
27
27
|
"scope": "按负责人派活改 lib/*.js;不得改版本号、总线状态、重启、C 盘写入、AGENTS.md"
|
|
28
28
|
},
|
|
29
29
|
"modules": {
|
|
30
|
+
"ui": {
|
|
31
|
+
"session": "session-b6aa099b-cfa6-42b7-961c-10f720236219",
|
|
32
|
+
"short": "b6aa099b",
|
|
33
|
+
"role": "入口/自更新组件负责人(= 插件负责人)",
|
|
34
|
+
"since": "2026-10-20",
|
|
35
|
+
"scope": "components/ui/**、client/**、lib/web.js 的卡片与自动更新、locale/**"
|
|
36
|
+
},
|
|
37
|
+
"session": {
|
|
38
|
+
"session": "session-b6aa099b-cfa6-42b7-961c-10f720236219",
|
|
39
|
+
"short": "b6aa099b",
|
|
40
|
+
"role": "跨会话组件负责人(= 插件负责人)",
|
|
41
|
+
"since": "2026-10-20",
|
|
42
|
+
"scope": "components/session/**、lib/bus.js 的链/邮件/收件箱"
|
|
43
|
+
},
|
|
44
|
+
"terminal": {
|
|
45
|
+
"session": "session-b6aa099b-cfa6-42b7-961c-10f720236219",
|
|
46
|
+
"short": "b6aa099b",
|
|
47
|
+
"role": "跨终端组件负责人(= 插件负责人)",
|
|
48
|
+
"since": "2026-10-20",
|
|
49
|
+
"scope": "components/terminal/**、本机 presence / term_send / term_send_files"
|
|
50
|
+
},
|
|
51
|
+
"device": {
|
|
52
|
+
"session": "session-b6aa099b-cfa6-42b7-961c-10f720236219",
|
|
53
|
+
"short": "b6aa099b",
|
|
54
|
+
"role": "跨设备组件负责人(= 插件负责人)",
|
|
55
|
+
"since": "2026-10-20",
|
|
56
|
+
"scope": "components/device/**、lib/cloud/**(中转站、备份、layout)"
|
|
57
|
+
},
|
|
58
|
+
"account": {
|
|
59
|
+
"session": "session-b6aa099b-cfa6-42b7-961c-10f720236219",
|
|
60
|
+
"short": "b6aa099b",
|
|
61
|
+
"role": "跨账号组件负责人(= 插件负责人,预留位)",
|
|
62
|
+
"since": "2026-10-20",
|
|
63
|
+
"scope": "components/account/**(目前无代码)"
|
|
64
|
+
},
|
|
30
65
|
"restart": {
|
|
31
66
|
"session": "session-a3e9f80e-6f28-425c-9807-0b9d8b872b5e",
|
|
32
67
|
"short": "a3e9f80e",
|
|
33
68
|
"name": "自动重启",
|
|
34
69
|
"role": "自动重启模块负责人(原 dsh-restart 插件作者会话)",
|
|
35
70
|
"since": "2026-10-20",
|
|
36
|
-
"scope": "
|
|
37
|
-
"rule": "不单独发版、不自己发版;改完向主维护会话(b6aa099b)汇报,由主维护并入 dsh-crossterm
|
|
71
|
+
"scope": "components/restart/ 下的 core.js、runtime.js、scripts/**",
|
|
72
|
+
"rule": "不单独发版、不自己发版;改完向主维护会话(b6aa099b)汇报,由主维护并入 dsh-crossterm 发版;只改 components/restart/** 时把它的组件版本号(现 0.3.28)加一"
|
|
38
73
|
}
|
|
39
74
|
},
|
|
40
75
|
"previous": [
|
|
@@ -57,9 +92,17 @@
|
|
|
57
92
|
|
|
58
93
|
一份插件里可以拆给多人:模块负责人只管自己那几支文件,发版仍由插件负责人统一做。
|
|
59
94
|
|
|
60
|
-
|
|
|
61
|
-
|
|
62
|
-
|
|
|
95
|
+
| 组件(插件页顺序) | 当前版本 | 负责人 | 范围 | 规矩 |
|
|
96
|
+
|---|---|---|---|---|
|
|
97
|
+
| ① 跨端互通(自更新) | v0.9.2 | `b6aa099b` | `components/ui/**`、`client/**`、`locale/**`、`lib/web.js` | 组件版本号在 `components/ui/manifest.json` |
|
|
98
|
+
| ② 跨会话 | v0.9.2 | `b6aa099b` | `components/session/**`、`lib/bus.js` 的链/邮件 | 同上 |
|
|
99
|
+
| ③ 跨终端 | v0.9.2 | `b6aa099b` | `components/terminal/**`、presence / `term_send` | 同上 |
|
|
100
|
+
| ④ 跨设备 | v0.9.2 | `b6aa099b` | `components/device/**`、`lib/cloud/**` | 同上 |
|
|
101
|
+
| ⑤ 跨账号 | v0.9.2 | `b6aa099b` | `components/account/**`(预留) | 同上 |
|
|
102
|
+
| ⑥ 自动重启 | v0.3.28 | `a3e9f80e`(会话名「自动重启」) | `components/restart/**`(`core.js` / `runtime.js` / `scripts/**`) | 不单独发版、不自己发版;改完汇报 `b6aa099b`,由插件负责人并入 `dsh-crossterm` 发版;只动它就把它的版本号加一 |
|
|
103
|
+
|
|
104
|
+
**组件版本号怎么升**:只在该组件自己的文件改动时升它(在 `components/<part>/manifest.json` 的 `version`),
|
|
105
|
+
跟着插件一起发版;插件总版本仍按发版技能的三分位规则。插件页与 `GET /crossterm/api/about` 的 `components[].version` 读的就是这里。
|
|
63
106
|
## 任期表
|
|
64
107
|
|
|
65
108
|
| 起(本地时间) | 止(本地时间) | 会话 | 终端 / nodeId | 角色 | 交接依据 |
|
package/README.md
CHANGED
|
@@ -18,12 +18,12 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
|
|
|
18
18
|
|
|
19
19
|
| 顺序 | 组件名称 | 版本 | 作用 | 工具 |
|
|
20
20
|
|---|---|---|---|---|
|
|
21
|
-
| ① |
|
|
21
|
+
| ① | 跨端互通(入口/自更新) | v0.9.3 | 自更新开关:打开后定期检查 npm 新版本并自动安装(默认关;装完要重启 DSH 生效)。关掉它只失去卡片与自动更新,下面五个组件照常工作。裸包名行,承载插件标题/简介与配置卡片,不挂工具 | — |
|
|
22
22
|
| ② | 跨会话 | v0.9.2 | 同一终端里的多个会话之间互通:特征码链、内联邮件、收件箱 | `term_link` `term_unlink` `term_broadcast` `term_inbox` `term_config` |
|
|
23
23
|
| ③ | 跨终端 | v0.9.2 | 同一台机器上的多个 DSH 终端/进程之间互发邮件与文件 | `term_whoami` `term_list` `term_send` `term_send_files` |
|
|
24
24
|
| ④ | 跨设备 | v0.9.2 | 经云端中转站,在多台机器的 DSH 之间互发邮件与文件 | `term_cloud` |
|
|
25
25
|
| ⑤ | 跨账号 | v0.9.2 | 预留位:后续用于跨账号互通,当前不挂工具 | — |
|
|
26
|
-
| ⑥ | 自动重启 | v0.3.
|
|
26
|
+
| ⑥ | 自动重启 | v0.3.29 | 改完插件/配置后让 DSH 自己重启一次,并自动拉起离线会话(原 `dsh-restart` 插件,0.9.0 起并入) | `dsh_restart` |
|
|
27
27
|
|
|
28
28
|
> 入口行为什么必须用裸包名:浏览器 client 半端只对裸包名行生效(`dsh-client-modules` 的
|
|
29
29
|
> `locatePkgJson()` 对子路径行返回 undefined),而插件页顶部标题与这一行共用同一份 `locale/zh.json` 的 meta,
|
|
@@ -31,19 +31,18 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
|
|
|
31
31
|
>
|
|
32
32
|
> 组件版本号怎么升:**只在该组件自己的文件改动时升它**,跟着插件一起发版(例:只改自动重启 → `components/restart` 的 `0.3.28` → `0.3.29`,其余五个不动)。插件总版本仍按发版技能的三分位规则。
|
|
33
33
|
|
|
34
|
-
**本版更新(0.9.
|
|
34
|
+
**本版更新(0.9.4)**
|
|
35
35
|
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
- 兼容:`cordis.patch.yml` 的 `name` 改为 `dsh-crossterm/components/<part>`,`exports` 同步;升级后重启 DSH 生效。
|
|
36
|
+
- **只管本次变动**:插件总版本 0.9.3 → 0.9.4,组件里只有**自动重启 0.3.28 → 0.3.29**,其余五个(入口 0.9.3 / 跨会话 0.9.2 / 跨终端 0.9.2 / 跨设备 0.9.2 / 跨账号 0.9.2)不动。
|
|
37
|
+
- **重启前先确认没人干活(第三路信号)**:插件每 2 秒把「哪些会话正在回合中」写进 `<总线目录>\restart-module\logs\busy-agents.json`,重启守卫读它 —— 长工具调用期间 transcript 不写盘、市场运行列表也会掉,过去靠这两路会误判空闲。
|
|
38
|
+
- **守卫终检 + 倒计时对齐**:守卫放行后 400 ms 再确认一次;倒计时归零先停 `00:00`(「正在准备重启…」),等 helper 真开始杀进程才转正计时。
|
|
40
39
|
|
|
41
|
-
**上一版(0.9.
|
|
40
|
+
**上一版(0.9.3)**
|
|
42
41
|
|
|
43
|
-
-
|
|
44
|
-
-
|
|
42
|
+
- 组件表补全:`/crossterm/api/about` 的 `components` 现在六个组件都在(之前漏了自动重启),入口名从 `ui` 改成「跨端互通(入口)」。
|
|
43
|
+
- 组件版本号各升各的第一次落地:那次只改 ui,所以只有它 0.9.2 → 0.9.3。
|
|
45
44
|
|
|
46
|
-
**更早(0.9.0 / 0.8.14 / 0.8.13 / 0.8.12 / 0.8.11 / 0.8.10 / 0.8.9 / 0.8.8 / 0.8.7 / 0.8.6 / 0.8.5 / 0.8.0)**
|
|
45
|
+
**更早(0.9.2 / 0.9.0 / 0.8.14 / 0.8.13 / 0.8.12 / 0.8.11 / 0.8.10 / 0.8.9 / 0.8.8 / 0.8.7 / 0.8.6 / 0.8.5 / 0.8.0)**
|
|
47
46
|
|
|
48
47
|
- 0.9.0:自动重启并进来当第 6 个组件(`dsh_restart` + 8 个 helper 脚本),由原 `dsh-restart` 会话继续维护、不再单独发版。
|
|
49
48
|
- 0.8.14:装完未重启时卡片显示「待重启」、更新按钮变灰「已装待重启」,`/check` 回 `needRestart=true`。
|
|
@@ -465,6 +465,48 @@ function scheduleRevive(root) {
|
|
|
465
465
|
mark(`revive: scheduled in ${REVIVE_DELAY_MS}ms`)
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
+
// ── 忙时心跳 ────────────────────────────────────────────────────────────────
|
|
469
|
+
// 判定「谁还在回合里」最可靠的信号在宿主进程内:agent.session.events 的最后一条是
|
|
470
|
+
// turn/start 还是 turn/end。市场 /dsh-market/status 的 runningAgents 在工具调用期间
|
|
471
|
+
// 会掉出去(实测),transcript mtime 在长工具调用期间也不写盘 —— 两个都会误判空闲。
|
|
472
|
+
// 所以插件每 2 秒把「所有 agent 的 id + 是否在回合中」写成 logs\busy-agents.json,
|
|
473
|
+
// helper 侧的 busy-check.mjs 拿它当第三路信号。
|
|
474
|
+
const BUSY_HEARTBEAT = path.join(LOG_DIR, 'busy-agents.json')
|
|
475
|
+
|
|
476
|
+
function inTurn(agent) {
|
|
477
|
+
try {
|
|
478
|
+
const events = agent && agent.session && agent.session.events
|
|
479
|
+
if (!events || !events.length) return false
|
|
480
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
481
|
+
const type = String((events[i] && events[i].type) || '')
|
|
482
|
+
if (type === 'turn/end') return false
|
|
483
|
+
if (type === 'turn/start') return true
|
|
484
|
+
}
|
|
485
|
+
return false
|
|
486
|
+
} catch { return false }
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function writeBusyHeartbeat(ctx) {
|
|
490
|
+
try {
|
|
491
|
+
const seen = new Map()
|
|
492
|
+
const add = (agent) => {
|
|
493
|
+
const id = String((agent && agent.id) || '')
|
|
494
|
+
if (!id || seen.has(id)) return
|
|
495
|
+
seen.set(id, { sessionId: id, inTurn: inTurn(agent) })
|
|
496
|
+
}
|
|
497
|
+
try { for (const agent of ctx.agents.roots()) add(agent) } catch { /* ignore */ }
|
|
498
|
+
try {
|
|
499
|
+
for (const s of (ctx.sessions?.list?.() || [])) {
|
|
500
|
+
try { add(ctx.agents?.get?.(s.id)) } catch { /* ignore */ }
|
|
501
|
+
}
|
|
502
|
+
} catch { /* ignore */ }
|
|
503
|
+
fs.mkdirSync(LOG_DIR, { recursive: true })
|
|
504
|
+
fs.writeFileSync(BUSY_HEARTBEAT, JSON.stringify({ at: new Date().toISOString(), agents: [...seen.values()] }), 'utf8')
|
|
505
|
+
} catch (e) {
|
|
506
|
+
mark(`busy heartbeat failed: ${(e && e.message) || String(e)}`)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
468
510
|
export default {
|
|
469
511
|
name: 'crossterm-restart',
|
|
470
512
|
apply(root) {
|
|
@@ -515,6 +557,13 @@ export default {
|
|
|
515
557
|
return () => clearInterval(timer)
|
|
516
558
|
}, 'dsh-restart: 会话工具补挂')
|
|
517
559
|
|
|
560
|
+
writeBusyHeartbeat(ctx)
|
|
561
|
+
ctx.effect(() => {
|
|
562
|
+
const timer = setInterval(() => writeBusyHeartbeat(ctx), 2000)
|
|
563
|
+
if (timer && typeof timer.unref === 'function') timer.unref()
|
|
564
|
+
return () => clearInterval(timer)
|
|
565
|
+
}, 'dsh-restart: 忙时心跳')
|
|
566
|
+
|
|
518
567
|
ctx.on('agent/created', ({ agent }) => mount(agent))
|
|
519
568
|
ctx.on('agent/disposed', ({ agent }) => { mounted.delete(agent) })
|
|
520
569
|
mark('ready')
|
|
@@ -9,10 +9,17 @@
|
|
|
9
9
|
import http from 'node:http'
|
|
10
10
|
import fs from 'node:fs'
|
|
11
11
|
import path from 'node:path'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
12
13
|
|
|
13
14
|
export const QUIET_MS = 3000
|
|
14
15
|
export const DEFAULT_PORT = Number(process.env.DSH_RESTART_PORT || 19387)
|
|
15
16
|
|
|
17
|
+
// 心跳文件:插件(宿主进程内)每 2 秒写一次「谁还在回合中」,比市场 runningAgents
|
|
18
|
+
// (工具调用期间会掉出列表)和 transcript mtime(长工具调用期间不写盘)都准。
|
|
19
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
20
|
+
const HEARTBEAT_FILE = process.env.DSH_BUSY_HEARTBEAT || path.join(HERE, 'logs', 'busy-agents.json')
|
|
21
|
+
const HEARTBEAT_FRESH_MS = Number(process.env.DSH_BUSY_HEARTBEAT_MS || 8000)
|
|
22
|
+
|
|
16
23
|
const norm = (id) => String(id ?? '').replace(/^session-/, '')
|
|
17
24
|
|
|
18
25
|
function httpGetJson(port, pathName, timeoutMs) {
|
|
@@ -110,16 +117,47 @@ export async function othersBusy({ selfSessionId, port = DEFAULT_PORT, recentMs
|
|
|
110
117
|
const recent = recentSessionWriter({ selfSessionId: self, recentMs, dshHome })
|
|
111
118
|
if (recent) {
|
|
112
119
|
sources.push(`transcript-mtime(${recentMs}ms)`)
|
|
113
|
-
if (!busy.some((b) => b.sessionId === recent.sessionId)) {
|
|
120
|
+
if (!busy.some((b) => norm(b.sessionId) === norm(recent.sessionId))) {
|
|
114
121
|
busy.push({ sessionId: recent.sessionId, why: 'transcript-write' })
|
|
115
122
|
}
|
|
116
123
|
} else {
|
|
117
124
|
sources.push('transcript-mtime:quiet')
|
|
118
125
|
}
|
|
119
126
|
|
|
127
|
+
const hb = heartbeatBusy({ selfSessionId: self })
|
|
128
|
+
if (hb.stale) {
|
|
129
|
+
sources.push('heartbeat:unavailable')
|
|
130
|
+
} else {
|
|
131
|
+
sources.push('heartbeat:busy-agents')
|
|
132
|
+
for (const item of hb.busy) {
|
|
133
|
+
if (!busy.some((b) => norm(b.sessionId) === norm(item.sessionId))) busy.push(item)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
120
137
|
return { busy, sources, checkedAt: new Date().toISOString() }
|
|
121
138
|
}
|
|
122
139
|
|
|
140
|
+
/** 读插件写的 busy-agents.json:文件够新才采信,其中 inTurn 的会话都算忙。 */
|
|
141
|
+
export function heartbeatBusy({ selfSessionId, heartbeatFile = HEARTBEAT_FILE, freshMs = HEARTBEAT_FRESH_MS } = {}) {
|
|
142
|
+
try {
|
|
143
|
+
const stat = fs.statSync(heartbeatFile)
|
|
144
|
+
if (Date.now() - stat.mtimeMs > freshMs) return { stale: true, busy: [] }
|
|
145
|
+
const raw = fs.readFileSync(heartbeatFile, 'utf8').replace(/^\uFEFF/, '')
|
|
146
|
+
const data = JSON.parse(raw)
|
|
147
|
+
const agents = Array.isArray(data && data.agents) ? data.agents : []
|
|
148
|
+
const self = norm(selfSessionId)
|
|
149
|
+
const busy = []
|
|
150
|
+
for (const agent of agents) {
|
|
151
|
+
const id = String((agent && agent.sessionId) || '')
|
|
152
|
+
if (!id || norm(id) === self) continue
|
|
153
|
+
if (agent && agent.inTurn === true) busy.push({ sessionId: id, why: 'agent-in-turn' })
|
|
154
|
+
}
|
|
155
|
+
return { stale: false, busy }
|
|
156
|
+
} catch {
|
|
157
|
+
return { stale: true, busy: [] }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
123
161
|
/**
|
|
124
162
|
* 等到「连续 quietMs 内没有其他会话活动」。
|
|
125
163
|
* maxWaitMs=0 表示只探一次(用于排程时的快速拒绝)。
|
|
@@ -381,23 +381,34 @@ function fitWindow() {
|
|
|
381
381
|
}
|
|
382
382
|
if (!spawned) { el("clock").textContent = "00:00"; el("fill").style.width = "0%"; zeroAt = Date.now(); spawnHelper(); }
|
|
383
383
|
|
|
384
|
-
//
|
|
384
|
+
// 归零后先停在 00:00 等 helper 真动手(守卫可能还在等空闲),
|
|
385
|
+
// 只有 helper 进入 killing/starting 才切正计时 + 转圈,避免“倒计时没读完就开始 +”。
|
|
386
|
+
var u = readJson(UI);
|
|
387
|
+
var phase = u && u.phase ? String(u.phase) : "";
|
|
388
|
+
var killing = phase === "killing" || phase === "starting";
|
|
385
389
|
if (zeroAt && !done) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
el("fill").style.display
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
390
|
+
if (killing) {
|
|
391
|
+
el("clock").textContent = "+" + mmss((Date.now() - zeroAt) / 1000);
|
|
392
|
+
if (el("fill").style.display !== "none") {
|
|
393
|
+
el("fill").style.display = "none";
|
|
394
|
+
var bar = el("bar");
|
|
395
|
+
if (bar) bar.className = "bar off";
|
|
396
|
+
var sp = el("spin");
|
|
397
|
+
if (sp) sp.className = "spin on";
|
|
398
|
+
dbg("spinner on");
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
el("clock").textContent = "00:00";
|
|
402
|
+
if (el("fill").style.display !== "none") {
|
|
403
|
+
el("fill").style.width = "0%";
|
|
404
|
+
}
|
|
394
405
|
}
|
|
395
406
|
}
|
|
396
407
|
|
|
397
|
-
|
|
398
|
-
if (
|
|
399
|
-
else if (
|
|
400
|
-
else if (
|
|
408
|
+
if (phase === "killing") el("hint").textContent = "正在结束 DSH 进程…";
|
|
409
|
+
else if (phase === "starting") el("hint").textContent = "正在启动 DSH…";
|
|
410
|
+
else if (phase === "waiting") el("hint").textContent = "等待其他会话空闲后重启…";
|
|
411
|
+
else if (zeroAt && !killing) el("hint").textContent = "倒计时结束,正在准备重启…";
|
|
401
412
|
|
|
402
413
|
var r = readJson(RES);
|
|
403
414
|
var when = r ? Date.parse(r.finishedAt || r.at || "") : NaN;
|
|
@@ -157,7 +157,9 @@ async function main() {
|
|
|
157
157
|
log(`pre-kill port ${PORT}: ${(await portOpen()) ? 'open' : 'closed'}`);
|
|
158
158
|
|
|
159
159
|
let waitMs = targetMs - Date.now();
|
|
160
|
-
|
|
160
|
+
// 不再强加 3 秒下限:以前 helper 起得比窗口晚时会被推到 scheduledAt 之后,
|
|
161
|
+
// 窗口那边倒计时已经归零、这边还没动手,于是“没读完就变正计时”。现在只留 250ms 防呆。
|
|
162
|
+
if (waitMs < 250) waitMs = 250;
|
|
161
163
|
log(`waiting ${waitMs}ms before kill`);
|
|
162
164
|
ui('waiting', `还剩 ${Math.round(waitMs / 1000)} 秒`);
|
|
163
165
|
// 等待期间每 500ms 复查请求是否被 cancel / 被新排程替换:撤销了就立刻作废,绝不杀进程。
|
|
@@ -199,13 +201,38 @@ async function main() {
|
|
|
199
201
|
const quietWaitSec = Math.max(0, Number(req.quietWaitSec ?? 180));
|
|
200
202
|
const selfSessionId = req.selfSessionId || req.sessionId || '';
|
|
201
203
|
log(`idle guard: self=${selfSessionId || '(unknown)'} quietWait=${quietWaitSec}s`);
|
|
202
|
-
const
|
|
204
|
+
const guardStart = Date.now();
|
|
205
|
+
const guardDeadline = guardStart + quietWaitSec * 1000;
|
|
206
|
+
let quiet = await waitQuiet({
|
|
203
207
|
selfSessionId,
|
|
204
208
|
maxWaitMs: quietWaitSec * 1000,
|
|
205
209
|
onPoll: ({ elapsedMs, busy }) => {
|
|
206
210
|
if (busy.length > 0) log(`idle guard waiting ${Math.round(elapsedMs / 1000)}s: busy=${JSON.stringify(busy)}`);
|
|
207
211
|
},
|
|
208
212
|
});
|
|
213
|
+
// 终检:静默窗刚满足时,可能有会话正好在这几秒里开了新回合。
|
|
214
|
+
// 连续两次「立刻探测」都空闲才真放行;中间有人开工就重新进等待(共用同一预算)。
|
|
215
|
+
if (quiet.ok) {
|
|
216
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
217
|
+
await sleep(400);
|
|
218
|
+
const probe = await waitQuiet({ selfSessionId, maxWaitMs: 0 });
|
|
219
|
+
if (!probe.ok) {
|
|
220
|
+
log(`idle guard recheck#${attempt}: 有人在这几秒里开工 -> 重新等待:${JSON.stringify(probe.busy)}`);
|
|
221
|
+
const remaining = Math.max(0, guardDeadline - Date.now());
|
|
222
|
+
quiet = await waitQuiet({
|
|
223
|
+
selfSessionId,
|
|
224
|
+
maxWaitMs: remaining,
|
|
225
|
+
onPoll: ({ elapsedMs, busy }) => {
|
|
226
|
+
if (busy.length > 0) log(`idle guard re-wait ${Math.round(elapsedMs / 1000)}s: busy=${JSON.stringify(busy)}`);
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
if (!quiet.ok) break;
|
|
230
|
+
} else {
|
|
231
|
+
log(`idle guard recheck#${attempt}: still quiet`);
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
209
236
|
log(`idle guard result: ${JSON.stringify(quiet)}`);
|
|
210
237
|
if (!quiet.ok) {
|
|
211
238
|
const result = { ok: false, dryRun: false, reason: 'others-busy', busy: quiet.busy, waitedMs: quiet.waitedMs, at: new Date().toISOString(), logFile };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "0.9.
|
|
2
|
+
"name": "跨端互通(入口)",
|
|
3
|
+
"description": "插件页的入口行:配置卡片、插件简介、自动更新开关与运行日志都在这里。它自己不挂工具,关掉只失去卡片与自动更新,下面五个业务组件照常工作。",
|
|
4
|
+
"version": "0.9.3"
|
|
5
5
|
}
|
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.9.
|
|
26
|
+
export const VERSION = '0.9.4'
|
|
27
27
|
|
|
28
28
|
/** DSH 的家目录:与宿主其它状态(sessions / profiles)同一个根,多终端天然共享 */
|
|
29
29
|
export function dshHome() {
|
package/lib/index.js
CHANGED
|
@@ -2087,7 +2087,7 @@ const RESTART_SRC = path.join(ROOT, 'components', 'restart', 'scripts')
|
|
|
2087
2087
|
* 「自动重启」组件(原 dsh-restart 插件,0.9.0 起并入)。
|
|
2088
2088
|
*
|
|
2089
2089
|
* 它不参与总线:只把 dsh_restart 工具挂到每个会话上,并把 helper 脚本复制到
|
|
2090
|
-
* 可写目录后交给
|
|
2090
|
+
* 可写目录后交给 ../components/restart/core.js。脚本必须在 import core.js **之前**落到
|
|
2091
2091
|
* DSH_RESTART_MODULE_DIR,因为 core.js 在模块顶层读这个环境变量。
|
|
2092
2092
|
*/
|
|
2093
2093
|
function mountRestart(root, config) {
|
|
@@ -2110,7 +2110,7 @@ function mountRestart(root, config) {
|
|
|
2110
2110
|
if (info.skipped.length) mark(`脚本缺失:${info.skipped.join(', ')}`)
|
|
2111
2111
|
if (!process.env.DSH_RESTART_MODULE_DIR) process.env.DSH_RESTART_MODULE_DIR = destDir
|
|
2112
2112
|
mark(`运行目录就绪:${destDir}(本次复制 ${info.copied.length} 个脚本)`)
|
|
2113
|
-
void import('
|
|
2113
|
+
void import('../components/restart/core.js')
|
|
2114
2114
|
.then((mod) => {
|
|
2115
2115
|
mod.default.apply(root, { ...config, part: 'restart' })
|
|
2116
2116
|
mark('已挂载 dsh_restart 工具')
|
package/lib/web.js
CHANGED
|
@@ -554,15 +554,17 @@ function statusOf(term) {
|
|
|
554
554
|
}
|
|
555
555
|
|
|
556
556
|
/**
|
|
557
|
-
* 插件自述(0.6.2):插件标题/简介 +
|
|
557
|
+
* 插件自述(0.6.2):插件标题/简介 + 六个组件(入口 + 五个业务组件)的名称/简介/版本 + 最近几版更新日志。
|
|
558
558
|
* 卡片(浏览器半端)与插件页顶部共用 locale/zh.json 的 meta,这里读同一份文件,
|
|
559
559
|
* 保证「插件页怎么显示」和「卡片里怎么显示」永远一致。
|
|
560
560
|
*/
|
|
561
561
|
const PART_ORDER = [
|
|
562
|
+
{ part: 'ui', fallback: '跨端互通(入口)' },
|
|
562
563
|
{ part: 'session', fallback: '跨会话' },
|
|
563
564
|
{ part: 'terminal', fallback: '跨终端' },
|
|
564
565
|
{ part: 'device', fallback: '跨设备' },
|
|
565
566
|
{ part: 'account', fallback: '跨账号' },
|
|
567
|
+
{ part: 'restart', fallback: '自动重启' },
|
|
566
568
|
]
|
|
567
569
|
|
|
568
570
|
let aboutCache = null
|