dsh-crossterm 0.9.3 → 0.9.5

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 CHANGED
@@ -1,3 +1,26 @@
1
+ ## 0.9.5 — 2026-10-20
2
+
3
+ **组件改名:跨终端 → 跨工作区**(User said (m13047):「将跨终端组件名改为跨工作区,目前不做跨终端,暂无条件」)
4
+
5
+ - 起因:跨终端(同一台机器上多个 DSH 进程互发)本轮不做了 —— 没有第二台终端/第二套环境可测,先把这个位置改成实际在用的场景:**同一台 DSH 上不同工作区、不同会话之间互通**。
6
+ - 组件本体:`components/terminal/manifest.json` 的 `name` 改「跨工作区」、`description` 改「在本机不同工作区、不同会话之间互发邮件与文件(跨终端本轮不做,暂无条件)」、`version` 0.9.2 → **0.9.5**(只有这个组件升,其余五个不动)。
7
+ - 显示名同步改:`lib/index.js` 的 `PART_LABEL.terminal`、`lib/web.js` 的 `PART_ORDER` fallback、`locale/zh.json` 与 `locale/en.json` 的组件列表与简介、插件页组件表、`MAINTAINERS.md` 的角色与表格、`cordis.patch.yml` 的两处注释。
8
+ - 顺带把三处工具描述里泛指的「跨终端」改成「跨端互通 / 跨端」(`term_whoami` / `term_inbox` / `term_config`),挂载标签改「跨工作区工具」。
9
+ - **内部标识不动**:part 名仍是 `terminal`、`TOOL_PART` 映射不变、`cordis.patch.yml` 第 3 行仍是 `dsh-crossterm/components/terminal` —— 已装机器不用改配置,重启即见新名字。
10
+ - 邮件四档标签里的「跨终端邮件」保留原义(确实来自另一个 DSH 进程时才会出现),只是这个能力本轮不做、不做宣传。
11
+
12
+ ## 0.9.4 — 2026-10-20
13
+
14
+ 自动重启组件 **0.3.28 → 0.3.29**(只动 `components/restart/**`;由该组件负责人 `a3e9f80e` 提交、主维护会话发版)。插件内其余五个组件不变。
15
+
16
+ - **忙时判定补第三路信号**:`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`。
17
+ - 背景:长工具调用期间 transcript 不写盘,市场 `runningAgents` 也会掉出列表 —— 旧的两路判据会把「真在跑」判成空闲,于是在别人干活时重启。
18
+ - **守卫终检**:helper 的等待下限 3000 → **250 ms**;守卫通过后 400 ms 再探一次,有人开工就带剩余预算重新等(最多 3 次,日志 `idle guard recheck#N`)。
19
+ - **倒计时与正计时对齐**:`scripts/countdown.html` 归零后先停在 `00:00` + 「倒计时结束,正在准备重启…」,只有 helper 的 ui-state 进 `killing`/`starting` 才切 `+mm:ss`。
20
+ - 背景:helper 起得晚时窗口已归零而 helper 还在等,被读成「没读完就变正计时」,实际 kill 还在 `scheduledAt` 之后 1~5 秒。
21
+
22
+ > 本机是 `link:` 源码版:重启后 `ensureRestartRuntime()` 因版本变化(`.version` 0.9.3 → 0.9.4)把 8 个脚本整组重拷到 `<总线目录>\restart-module`。
23
+
1
24
  ## 0.9.3 — 2026-10-20
2
25
 
3
26
  修 0.9.2 的两处显示缺陷(代码逻辑没变,只动 `GET /crossterm/api/about` 与 ui 组件清单):
package/MAINTAINERS.md CHANGED
@@ -44,7 +44,7 @@
44
44
  "terminal": {
45
45
  "session": "session-b6aa099b-cfa6-42b7-961c-10f720236219",
46
46
  "short": "b6aa099b",
47
- "role": "跨终端组件负责人(= 插件负责人)",
47
+ "role": "跨工作区组件负责人(= 插件负责人)",
48
48
  "since": "2026-10-20",
49
49
  "scope": "components/terminal/**、本机 presence / term_send / term_send_files"
50
50
  },
@@ -96,7 +96,7 @@
96
96
  |---|---|---|---|---|
97
97
  | ① 跨端互通(自更新) | v0.9.2 | `b6aa099b` | `components/ui/**`、`client/**`、`locale/**`、`lib/web.js` | 组件版本号在 `components/ui/manifest.json` |
98
98
  | ② 跨会话 | v0.9.2 | `b6aa099b` | `components/session/**`、`lib/bus.js` 的链/邮件 | 同上 |
99
- | ③ 跨终端 | v0.9.2 | `b6aa099b` | `components/terminal/**`、presence / `term_send` | 同上 |
99
+ | ③ 跨工作区 | v0.9.5 | `b6aa099b` | `components/terminal/**`、presence / `term_send` | 同上 |
100
100
  | ④ 跨设备 | v0.9.2 | `b6aa099b` | `components/device/**`、`lib/cloud/**` | 同上 |
101
101
  | ⑤ 跨账号 | v0.9.2 | `b6aa099b` | `components/account/**`(预留) | 同上 |
102
102
  | ⑥ 自动重启 | v0.3.28 | `a3e9f80e`(会话名「自动重启」) | `components/restart/**`(`core.js` / `runtime.js` / `scripts/**`) | 不单独发版、不自己发版;改完汇报 `b6aa099b`,由插件负责人并入 `dsh-crossterm` 发版;只动它就把它的版本号加一 |
package/README.md CHANGED
@@ -20,10 +20,10 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
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
- | ③ | 跨终端 | v0.9.2 | 同一台机器上的多个 DSH 终端/进程之间互发邮件与文件 | `term_whoami` `term_list` `term_send` `term_send_files` |
23
+ | ③ | 跨工作区 | v0.9.5 | 在本机不同工作区、不同会话之间互发邮件与文件(跨终端本轮不做,暂无条件) | `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.28 | 改完插件/配置后让 DSH 自己重启一次,并自动拉起离线会话(原 `dsh-restart` 插件,0.9.0 起并入) | `dsh_restart` |
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,21 +31,21 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
31
31
  >
32
32
  > 组件版本号怎么升:**只在该组件自己的文件改动时升它**,跟着插件一起发版(例:只改自动重启 → `components/restart` 的 `0.3.28` → `0.3.29`,其余五个不动)。插件总版本仍按发版技能的三分位规则。
33
33
 
34
- **本版更新(0.9.3)**
34
+ **本版更新(0.9.5)**
35
35
 
36
- - **组件表补全**:`GET /crossterm/api/about` 的 `components` 现在按插件页顺序给六个组件 —— 入口、跨会话、跨终端、跨设备、跨账号、自动重启,每个都带 `version`;卡片「包含的组件」不会再漏掉自动重启。
37
- - **入口组件的名字**:`components/ui/manifest.json` 的 `name` 从 `ui` 改成「跨端互通(入口)」,并补了简介。
38
- - **组件版本号各升各的**(新规矩第一次落地):这次只改 ui 组件,所以只有它 0.9.2 → 0.9.3;其余五个仍是 0.9.2,自动重启仍是 0.3.28。
36
+ - **组件改名:跨终端 → 跨工作区**(跨终端本轮不做,暂无条件)。这个位置改成实际在用的场景:同一台 DSH 上不同工作区、不同会话之间互发邮件与文件。
37
+ - **只管本次变动**:插件总版本 0.9.4 → 0.9.5,组件里只有**跨工作区 0.9.2 → 0.9.5**,其余五个(入口 0.9.3 / 跨会话 0.9.2 / 跨设备 0.9.2 / 跨账号 0.9.2 / 自动重启 0.3.29)不动。
38
+ - **已装的机器不用改配置**:part 标识仍是 `terminal`、`cordis.patch.yml` 第 3 行没动,改的是显示名(插件页组件表、`/about`、locale 中英文、`MAINTAINERS.md`、注释),重启即见。
39
39
 
40
- **上一版(0.9.2)**
40
+ **上一版(0.9.4)**
41
41
 
42
- - **组件分目录存放**:`components/<part>/` 各放各的 `manifest.json` 与代码,`parts/` 删除;原 `lib/restart/` 整体搬进 `components/restart/`。
43
- - **每个组件有独立版本号**:插件页与更新说明都显示「名称 vX.Y.Z」,方便看清这次改的是哪个组件、由谁维护。
44
- - **组件维护分出去**:`MAINTAINERS.md` 的 `modules` 段记录每个组件的负责人;自动重启 = `a3e9f80e`,其余由主维护会话 `b6aa099b` 负责。
45
- - 兼容:`cordis.patch.yml` 的 `name` 改为 `dsh-crossterm/components/<part>`,`exports` 同步;升级后重启 DSH 生效。
42
+ - **只管本次变动**:插件总版本 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)不动。
43
+ - **重启前先确认没人干活(第三路信号)**:插件每 2 秒把「哪些会话正在回合中」写进 `<总线目录>\restart-module\logs\busy-agents.json`,重启守卫读它 —— 长工具调用期间 transcript 不写盘、市场运行列表也会掉,过去靠这两路会误判空闲。
44
+ - **守卫终检 + 倒计时对齐**:守卫放行后 400 ms 再确认一次;倒计时归零先停 `00:00`(「正在准备重启…」),等 helper 真开始杀进程才转正计时。
46
45
 
47
- **更早(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)**
46
+ **更早(0.9.3 / 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)**
48
47
 
48
+ - 0.9.3:组件表补全六个组件,入口名从 `ui` 改成「跨端互通(入口)」,组件版本号各升各的第一次落地。
49
49
  - 0.9.0:自动重启并进来当第 6 个组件(`dsh_restart` + 8 个 helper 脚本),由原 `dsh-restart` 会话继续维护、不再单独发版。
50
50
  - 0.8.14:装完未重启时卡片显示「待重启」、更新按钮变灰「已装待重启」,`/check` 回 `needRestart=true`。
51
51
  - 0.8.13:已经是最新就不装;没写收件人的信优先投给 `MAINTAINERS.md` 里的维护者会话。
@@ -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')
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "自动重启",
3
3
  "description": "改完插件/配置后让 DSH 自己重启一次,并自动拉起离线会话(原 dsh-restart 插件,0.9.0 起并入)",
4
- "version": "0.3.28"
4
+ "version": "0.3.29"
5
5
  }
@@ -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
- el("clock").textContent = "+" + mmss((Date.now() - zeroAt) / 1000);
387
- if (el("fill").style.display !== "none") {
388
- el("fill").style.display = "none";
389
- var bar = el("bar");
390
- if (bar) bar.className = "bar off";
391
- var sp = el("spin");
392
- if (sp) sp.className = "spin on";
393
- dbg("spinner on");
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
- var u = readJson(UI);
398
- if (u && u.phase === "killing") el("hint").textContent = "正在结束 DSH 进程…";
399
- else if (u && u.phase === "starting") el("hint").textContent = "正在启动 DSH…";
400
- else if (u && u.phase === "waiting") el("hint").textContent = "等待其他会话空闲后重启…";
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
- if (waitMs < 3000) waitMs = 3000;
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 quiet = await waitQuiet({
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": "同一台机器上的多个 DSH 终端/进程之间互发邮件与文件",
4
- "version": "0.9.2"
2
+ "name": "跨工作区",
3
+ "description": "在本机不同工作区、不同会话之间互发邮件与文件(跨终端本轮不做,暂无条件)",
4
+ "version": "0.9.5"
5
5
  }
package/cordis.patch.yml CHANGED
@@ -15,7 +15,7 @@
15
15
  # 卡片与「自动更新」,下面四个组件照常跑。
16
16
  # 自动更新默认关,开关在卡片里,值落 config.json 的 autoUpdate。
17
17
  # ② dsh-crossterm/components/session 跨会话:term_link / term_unlink / term_broadcast / term_inbox / term_config
18
- # ③ dsh-crossterm/components/terminal 跨终端:term_whoami / term_list / term_send / term_send_files
18
+ # ③ dsh-crossterm/components/terminal 跨工作区:term_whoami / term_list / term_send / term_send_files
19
19
  # ④ dsh-crossterm/components/device 跨设备:term_cloud(云端总线同步)
20
20
  # ⑤ dsh-crossterm/components/account 跨账号:预留占位,暂不挂工具
21
21
  # ⑥ dsh-crossterm/components/restart 自动重启:dsh_restart(原 dsh-restart 插件,0.9.0 起并入)
@@ -29,7 +29,7 @@
29
29
  # 为什么入口行必须用「裸包名」:浏览器的 client 半端只对裸包名行生效
30
30
  # (dsh-client-modules 的 locatePkgJson 对子路径行直接返回 undefined),而插件页顶部标题
31
31
  # 与裸包名行共用同一份 locale/zh.json 的 meta —— 所以裸包名行单独做入口(卡片 + 自动更新),
32
- # 标题写「跨端互通(自更新)」,四个业务组件排在它后面、名字仍是跨会话/跨终端/跨设备/跨账号。
32
+ # 标题写「跨端互通(自更新)」,四个业务组件排在它后面、名字仍是跨会话/跨工作区/跨设备/跨账号。
33
33
  #
34
34
  # 0.8.9 起五个组件互不依赖:谁都不用 provide/inject 别人,各挂各的工具与开销。
35
35
  # 关掉入口行只失去「配置卡片 + 自动更新」,四个业务组件照常工作、各自仍可单独关。
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.3'
26
+ export const VERSION = '0.9.5'
27
27
 
28
28
  /** DSH 的家目录:与宿主其它状态(sessions / profiles)同一个根,多终端天然共享 */
29
29
  export function dshHome() {
package/lib/index.js CHANGED
@@ -1518,7 +1518,7 @@ const PART_LABEL = {
1518
1518
  all: '全部组件',
1519
1519
  ui: '入口(插件卡片)',
1520
1520
  session: '跨会话',
1521
- terminal: '跨终端',
1521
+ terminal: '跨工作区',
1522
1522
  device: '跨设备',
1523
1523
  account: '跨账号(预留)',
1524
1524
  restart: '自动重启',
@@ -1534,7 +1534,7 @@ function buildTools(term, agent, part = 'all') {
1534
1534
 
1535
1535
  const whoami = defineTool({
1536
1536
  name: 'term_whoami',
1537
- description: '显示当前 DSH 终端在本插件里的身份(nodeId / 显示名 / 总线目录 / 本进程活着的会话),排查跨终端问题先用它。',
1537
+ description: '显示当前 DSH 终端在本插件里的身份(nodeId / 显示名 / 总线目录 / 本进程活着的会话),排查跨端互通问题先用它。',
1538
1538
  parameters: {},
1539
1539
  output: {
1540
1540
  schema: { type: 'json' },
@@ -1851,7 +1851,7 @@ function buildTools(term, agent, part = 'all') {
1851
1851
 
1852
1852
  const inbox = defineTool({
1853
1853
  name: 'term_inbox',
1854
- description: '查看本终端收到的跨终端消息(没自动投递进来的、或静默投递的),并可以把它们投进当前会话或丢弃。',
1854
+ description: '查看本终端收到的跨端消息(没自动投递进来的、或静默投递的),并可以把它们投进当前会话或丢弃。',
1855
1855
  parameters: {
1856
1856
  action: { type: 'string', enum: ['list', 'deliver', 'drop'], description: 'list=查看(默认),deliver=投进当前会话,drop=丢弃' },
1857
1857
  ids: { type: 'array', items: { type: 'string' }, description: '要处理的消息 id;deliver/drop 时留空表示全部' },
@@ -1890,7 +1890,7 @@ function buildTools(term, agent, part = 'all') {
1890
1890
 
1891
1891
  const config = defineTool({
1892
1892
  name: 'term_config',
1893
- description: '查看或修改本终端在跨终端插件里的配置(显示名 name、是否自动投递 autoDeliver、收件文件落地目录 inboxDir)。改完立即生效,不用重启。',
1893
+ description: '查看或修改本终端在跨端互通插件里的配置(显示名 name、是否自动投递 autoDeliver、收件文件落地目录 inboxDir)。改完立即生效,不用重启。',
1894
1894
  parameters: {
1895
1895
  name: { type: 'string', description: '终端显示名,方便别的终端认出你' },
1896
1896
  autoDeliver: { type: 'boolean', description: '收到消息是否自动投进目标会话' },
@@ -2160,7 +2160,7 @@ export default {
2160
2160
  try { fn() } catch { /* ignore */ }
2161
2161
  }
2162
2162
  }
2163
- }, 'dsh-crossterm: 跨终端工具')
2163
+ }, 'dsh-crossterm: 跨工作区工具')
2164
2164
  mounted.set(agent, dispose)
2165
2165
  term.activeSessionId = String(agent.id || term.activeSessionId)
2166
2166
  void term.note(`mounted tools -> ${shortId(agent.id)}`)
package/lib/web.js CHANGED
@@ -561,7 +561,7 @@ function statusOf(term) {
561
561
  const PART_ORDER = [
562
562
  { part: 'ui', fallback: '跨端互通(入口)' },
563
563
  { part: 'session', fallback: '跨会话' },
564
- { part: 'terminal', fallback: '跨终端' },
564
+ { part: 'terminal', fallback: '跨工作区' },
565
565
  { part: 'device', fallback: '跨设备' },
566
566
  { part: 'account', fallback: '跨账号' },
567
567
  { part: 'restart', fallback: '自动重启' },
package/locale/en.json CHANGED
@@ -2,6 +2,6 @@
2
2
  "meta": {
3
3
  "title": "CrossTerm (self-update)",
4
4
  "shortTitle": "CrossTerm",
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 five components below — session / terminal / device / account / auto-restart — each has its own switch and version. About: synchronize sessions, terminals and devices under one DSH account, with feature-code chains, inline mail, a cloud relay and automatic session backups."
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 five components below — session / workspace / device / account / auto-restart — each has its own switch and version. About: synchronize sessions, workspaces 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
@@ -2,6 +2,6 @@
2
2
  "meta": {
3
3
  "title": "跨端互通(自更新)",
4
4
  "shortTitle": "跨端互通",
5
- "description": "自更新开关:打开后插件会定期检查 npm 上的新版本并自动安装(默认关;装完要重启 DSH 才生效,不会自己重启)。关掉它不影响下面五个组件 —— 跨会话 / 跨终端 / 跨设备 / 跨账号 / 自动重启,各自独立开关、各有版本号。插件简介:同一 DSH 账号下的会话、终端、设备之间互通,支持特征码链、内联邮件、云端中转站与会话自动备份。"
5
+ "description": "自更新开关:打开后插件会定期检查 npm 上的新版本并自动安装(默认关;装完要重启 DSH 才生效,不会自己重启)。关掉它不影响下面五个组件 —— 跨会话 / 跨工作区 / 跨设备 / 跨账号 / 自动重启,各自独立开关、各有版本号。插件简介:同一 DSH 账号下的会话、终端、设备之间互通,支持特征码链、内联邮件、云端中转站与会话自动备份。"
6
6
  }
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-crossterm",
3
- "version": "0.9.3",
4
- "description": "跨端互通:同一 DSH 账号下的会话、终端、设备之间互通 —— 特征码链、内联邮件、云端中转站与自动备份",
3
+ "version": "0.9.5",
4
+ "description": "跨端互通:同一 DSH 账号下的会话、工作区、设备之间互通 —— 特征码链、内联邮件、云端中转站与自动备份",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "exports": {