dsh-crossterm 1.0.0 → 1.0.1
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 +11 -0
- package/README.md +8 -2
- package/components/restart/manifest.json +1 -1
- package/components/restart/scripts/busy-check.mjs +19 -1
- package/components/restart/scripts/dsh-restart-helper.mjs +65 -13
- package/components/restart/scripts/trigger-restart.mjs +7 -0
- package/lib/bus.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## 1.0.1 — 2026-10-20
|
|
2
|
+
|
|
3
|
+
### 修复:换台机器就跑不了的三处写死(第二设备验收 F1/F2/F3)
|
|
4
|
+
|
|
5
|
+
- 背景:1.0.0 发布后,第二设备(YOCE_C)按 npm 1.0.0 做本机验收,报三条阻断,全部成立 —— 根因是代码里带着 TS-01 的环境常量。报告:`D:\dsh\dsh-crossterm\acceptance\v1.0.0\验收报告-第二设备.md`。
|
|
6
|
+
- **F1 端口写死**:`busy-check.mjs` 的 `DEFAULT_PORT = Number(process.env.DSH_RESTART_PORT || 19387)`。第二设备 19387 关闭,市场接口在 `DSH_WEB_URL`(`http://127.0.0.1:63875`)→ 探测恒 unknown → 排程被前置守卫误拦。改法:新增 `resolveWebPort()`,顺序 `DSH_RESTART_PORT` → `DSH_WEB_URL`(缺协议头也认,无端口按协议默认)→ `19387`;`DEFAULT_PORT` 与 helper 的 `PORT` 都用它。
|
|
7
|
+
- **F2 App 路径写死**:helper 里 `APP_EXE = 'F:\dsh\DeepSeek Harness.exe'`。第二设备真 exe 在 `D:\dsh`;helper 先按镜像名全杀、再 `fs.existsSync(APP_EXE)` 失败即放弃 → 真实重启会「杀而不起」。改法:`resolveAppExe()` 依次试 `DSH_RESTART_APP_EXE` → 用 PowerShell 查正在跑的宿主进程 ImagePath → 各盘 `\dsh\DeepSeek Harness.exe` → `%LOCALAPPDATA%\Programs\DeepSeek Harness`;**非 dryRun 且解析不到 exe 时直接拒绝 kill**(`reason:'app-exe-not-found'`,exit 1),并写进 result.json。
|
|
8
|
+
- **F3 守卫一刀切**:`trigger-restart.mjs` 里 `if (!opts.force && !probe.ok)` 把「读不到权威信号」与「真有人忙」报成同一句「其他会话正在跑任务:[]」。改法:unknown 单独分支,打印 `sources` 与端口提示,退出码 4(busy 仍 exit 7)。
|
|
9
|
+
- 自测:新增 `test-portability.mjs`(端口七种取值、exe 环境变量优先、import 不产生落盘副作用)全部 PASS;TS-01 上模拟第二设备(`DSH_RESTART_PORT=19999` + 心跳文件不存在)→ 守卫 `exit 4` 且提示「读不到权威信号(unknown)」;默认环境全链 dry-run → `app exe: F:\dsh\DeepSeek Harness.exe dir=F:\dsh port=19387`、`DRY-RUN result ok:true`、`exit code=0`。
|
|
10
|
+
- 组件版本:自动重启 `0.3.40` → `0.3.41`。
|
|
11
|
+
|
|
1
12
|
## 1.0.0 — 2026-10-20
|
|
2
13
|
|
|
3
14
|
### 1.0.0 概览:自动重启组件从 0.3.30 走到 0.3.40
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
|
|
|
22
22
|
| ② | 跨会话 | v0.5.3 | 同一终端里的多个会话之间互通:特征码链、内联邮件、收件箱 | `term_link` `term_unlink` `term_broadcast` `term_inbox` `term_config` |
|
|
23
23
|
| ③ | 跨工作区 | v0.1.0 | 在本机不同工作区、不同会话之间互发邮件与文件(跨终端本轮不做,暂无条件) | `term_whoami` `term_list` `term_send` `term_send_files` |
|
|
24
24
|
| ④ | 跨设备 | v0.4.0 | 经云端中转站,在多台机器的 DSH 之间互发邮件与文件 | `term_cloud` |
|
|
25
|
-
| ⑤ | 自动重启 | v0.3.
|
|
25
|
+
| ⑤ | 自动重启 | v0.3.41 | 改完插件/配置后让 DSH 自己重启一次,并自动拉起离线会话(原 `dsh-restart` 插件,0.9.0 起并入) | `dsh_restart` |
|
|
26
26
|
|
|
27
27
|
> 入口行为什么必须用裸包名:浏览器 client 半端只对裸包名行生效(`dsh-client-modules` 的
|
|
28
28
|
> `locatePkgJson()` 对子路径行返回 undefined),而插件页顶部标题与这一行共用同一份 `locale/zh.json` 的 meta,
|
|
@@ -30,7 +30,13 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
|
|
|
30
30
|
>
|
|
31
31
|
> 组件版本号怎么升:**只在该组件自己的文件改动时升它**,跟着插件一起发版(例:只改自动重启 → `components/restart` 的 `0.3.28` → `0.3.29`,其余四个不动)。插件总版本仍按发版技能的三分位规则。
|
|
32
32
|
|
|
33
|
-
**本版更新(1.0.
|
|
33
|
+
**本版更新(1.0.1)**
|
|
34
|
+
|
|
35
|
+
- 可移植性修复(第二设备验收报的三条阻断,全部在本机复现后修掉):**端口不再写死 19387** —— `DSH_RESTART_PORT` → `DSH_WEB_URL` → `19387` 三级回退,`dsh web` 或换端口的机器不再恒判 unknown;**桌面 App 路径不再写死 `F:\dsh`** —— 依次试 `DSH_RESTART_APP_EXE` → 正在跑的宿主进程 ImagePath → 各盘 `\dsh\DeepSeek Harness.exe` → `%LOCALAPPDATA%\Programs`,一个都找不到就**拒绝执行 kill**(「杀而不起」比不重启更糟);**守卫区分 unknown 与 busy** —— 读不到权威信号时退出码 4 并明说「市场接口与心跳都没读到」,不再误报「其他会话正在跑任务:[]」。
|
|
36
|
+
- 组件版本:自动重启 `0.3.40` → `0.3.41`;其余组件不动。新增 `test-portability.mjs` 覆盖端口回退与 exe 解析。
|
|
37
|
+
- 本版只动 `components/restart/`,`lib/*.js` 除版本号外未改。
|
|
38
|
+
|
|
39
|
+
**上一版(1.0.0)**
|
|
34
40
|
|
|
35
41
|
- **版本号正式跨过 1.0**:这一版把「自动重启」组件从 0.3.30 推到 0.3.40,十次连续加固全部来自真实事故与本机复现 —— wmic 在新系统失效后改走 PowerShell CIM;安全软件拦 `mshta` 时用 `conhost --headless` 无界面兜底;helper 加单实例锁(mtime 心跳判活、休眠后二次确认、跨主机让路);node 运行时不再依赖旧家目录;`launch:` / `exit code=` 两行让每次重启事后可审。
|
|
36
42
|
- **空闲守卫重做**:心跳从「读会话快照」改成订阅实时 `session/event`,生产端写 `producer` / `eventsSeen` / `src`;消费端把「市场残影」「权威的零」「读不到权威信号」分开判,退出码 `busy=3` / `unknown=4`,不再把「读不到」当成「没人忙」。
|
|
@@ -12,7 +12,25 @@ import path from 'node:path'
|
|
|
12
12
|
import { fileURLToPath } from 'node:url'
|
|
13
13
|
|
|
14
14
|
export const QUIET_MS = 3000
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
// 宿主 web 端口解析(第二设备验收 F1):不能写死 19387。
|
|
17
|
+
// 顺序:DSH_RESTART_PORT(显式覆盖)→ DSH_WEB_URL(宿主自己给的地址,最可靠)→ 19387(单机默认)。
|
|
18
|
+
// TS-01 上 19387 确实开着,但换个 profile / 换台机器端口就未必是这个数;
|
|
19
|
+
// 拼错或读不到都只是回退,不抛错。
|
|
20
|
+
export function resolveWebPort(env = process.env) {
|
|
21
|
+
const direct = Number(env.DSH_RESTART_PORT)
|
|
22
|
+
if (Number.isInteger(direct) && direct > 0 && direct < 65536) return direct
|
|
23
|
+
const raw = String(env.DSH_WEB_URL || '').trim()
|
|
24
|
+
if (raw) {
|
|
25
|
+
try {
|
|
26
|
+
const u = new URL(raw.includes('://') ? raw : `http://${raw}`)
|
|
27
|
+
const p = Number(u.port || (u.protocol === 'https:' ? 443 : 80))
|
|
28
|
+
if (Number.isInteger(p) && p > 0 && p < 65536) return p
|
|
29
|
+
} catch { /* 解析不了就继续退到默认值 */ }
|
|
30
|
+
}
|
|
31
|
+
return 19387
|
|
32
|
+
}
|
|
33
|
+
export const DEFAULT_PORT = resolveWebPort()
|
|
16
34
|
|
|
17
35
|
// 心跳文件:插件(宿主进程内)每 2 秒写一次「谁还在回合中」,比市场 runningAgents
|
|
18
36
|
// (工具调用期间会掉出列表)和 transcript mtime(长工具调用期间不写盘)都准。
|
|
@@ -8,7 +8,7 @@ import net from 'node:net';
|
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
-
import { waitQuiet } from './busy-check.mjs';
|
|
11
|
+
import { waitQuiet, resolveWebPort } from './busy-check.mjs';
|
|
12
12
|
|
|
13
13
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
const LOGS_DIR = path.join(MODULE_DIR, 'logs');
|
|
@@ -16,15 +16,47 @@ const REQUEST_FILE = path.join(MODULE_DIR, 'request.json');
|
|
|
16
16
|
// 单实例锁:mshta 迟到(被安全软件延迟放行)与 CIM 直拉 helper 兜底同时发生时,
|
|
17
17
|
// 两个 helper 都去 taskkill + 拉起,会把刚起来的 DSH 再杀一次。锁放总线目录,跨进程/会话可见。
|
|
18
18
|
const LOCK_FILE = path.join(MODULE_DIR, 'helper.lock');
|
|
19
|
-
const APP_EXE = 'F:\\dsh\\DeepSeek Harness.exe';
|
|
20
|
-
const APP_DIR = 'F:\\dsh';
|
|
21
19
|
const APP_IMAGE = 'DeepSeek Harness.exe';
|
|
22
|
-
|
|
20
|
+
// 可移植性(第二设备验收 F2):exe 路径与端口都不能写死成 TS-01 的名字。
|
|
21
|
+
// 解析顺序:DSH_RESTART_APP_EXE → 正在跑的宿主进程 ImagePath → 各盘 \dsh\DeepSeek Harness.exe
|
|
22
|
+
// → %LOCALAPPDATA%\Programs\…。一个都找不到就返回 '',main() 会拒绝执行 kill(「杀而不起」比不重启更糟)。
|
|
23
|
+
function detectRunningAppExe() {
|
|
24
|
+
try {
|
|
25
|
+
const out = execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command',
|
|
26
|
+
`(Get-Process -Name 'DeepSeek Harness' -ErrorAction SilentlyContinue | Where-Object { $_.Path } | Select-Object -First 1).Path`,
|
|
27
|
+
], { encoding: 'utf8', timeout: 8000, windowsHide: true }).trim();
|
|
28
|
+
if (out && fs.existsSync(out)) return out;
|
|
29
|
+
} catch { /* 拿不到就继续退 */ }
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveAppExe() {
|
|
34
|
+
const list = [];
|
|
35
|
+
if (process.env.DSH_RESTART_APP_EXE) list.push(process.env.DSH_RESTART_APP_EXE);
|
|
36
|
+
const detected = detectRunningAppExe();
|
|
37
|
+
if (detected) list.push(detected);
|
|
38
|
+
for (const drive of 'CDEFGHIJKLMNOPQRSTUVWXYZ') list.push(`${drive}:\\dsh\\${APP_IMAGE}`);
|
|
39
|
+
list.push(path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'DeepSeek Harness', APP_IMAGE));
|
|
40
|
+
for (const c of list) {
|
|
41
|
+
try { if (c && fs.existsSync(c)) return path.resolve(c); } catch { /* 忽略坏路径 */ }
|
|
42
|
+
}
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const APP_EXE = resolveAppExe();
|
|
47
|
+
const APP_DIR = APP_EXE ? path.dirname(APP_EXE) : '';
|
|
48
|
+
const PORT = resolveWebPort();
|
|
23
49
|
const WAIT_GONE_MS = 40000;
|
|
24
50
|
const WAIT_UP_MS = 120000;
|
|
25
51
|
const RETRY_UP_MS = 60000;
|
|
26
52
|
|
|
27
|
-
|
|
53
|
+
// 只有被直接执行时才做落盘副作用;被 import(单测)时保持只读,免得测试在仓库里留 logs/helper.pid。
|
|
54
|
+
const invokedDirectly = (() => {
|
|
55
|
+
try {
|
|
56
|
+
return path.resolve(fileURLToPath(import.meta.url)) === path.resolve(process.argv[1] || '');
|
|
57
|
+
} catch { return true; }
|
|
58
|
+
})();
|
|
59
|
+
if (invokedDirectly) fs.mkdirSync(LOGS_DIR, { recursive: true });
|
|
28
60
|
|
|
29
61
|
function stampName(d = new Date()) {
|
|
30
62
|
const p = (n) => String(n).padStart(2, '0');
|
|
@@ -32,7 +64,9 @@ function stampName(d = new Date()) {
|
|
|
32
64
|
}
|
|
33
65
|
|
|
34
66
|
const logFile = path.join(LOGS_DIR, `helper-${stampName()}.log`);
|
|
35
|
-
|
|
67
|
+
if (invokedDirectly) {
|
|
68
|
+
try { fs.writeFileSync(path.join(LOGS_DIR, 'helper.pid'), String(process.pid), 'utf8'); } catch {}
|
|
69
|
+
}
|
|
36
70
|
function log(msg) {
|
|
37
71
|
const line = `[${new Date().toISOString()}] ${msg}`;
|
|
38
72
|
try { fs.appendFileSync(logFile, line + '\n', 'utf8'); } catch {}
|
|
@@ -60,8 +94,11 @@ const LAUNCH_MODE = ARG_MODE || {
|
|
|
60
94
|
'WmiPrvSE.exe': 'cim-direct',
|
|
61
95
|
'node.exe': 'node-direct',
|
|
62
96
|
}[PARENT_IMAGE] || (PARENT_IMAGE ? `parent:${PARENT_IMAGE}` : 'unknown');
|
|
63
|
-
|
|
64
|
-
|
|
97
|
+
// 这两行只在「被直接执行」时打:被 import(单测)时不该往仓库 logs 里写 launch/exit 痕迹。
|
|
98
|
+
if (invokedDirectly) {
|
|
99
|
+
log(`launch: mode=${LAUNCH_MODE} parent=${PARENT_IMAGE || '?'} pid=${process.pid} ppid=${process.ppid}`);
|
|
100
|
+
process.on('exit', (code) => log(`exit code=${code}`));
|
|
101
|
+
}
|
|
65
102
|
|
|
66
103
|
// result.json 是「最近一次」的状态;再留一份带时间戳的进 logs\ 供排障翻查(trigger 清理 request.json 后尤其有用)。
|
|
67
104
|
const RESULT_FILE = path.join(LOGS_DIR, 'result.json');
|
|
@@ -337,6 +374,16 @@ async function main() {
|
|
|
337
374
|
process.on('SIGTERM', () => { log('signal SIGTERM received; releasing lock and exiting (143)'); releaseLock(); process.exit(143); });
|
|
338
375
|
startLockHeartbeat(); // 从抢到锁到进程退出为止,每 10s 刷一次 mtime
|
|
339
376
|
|
|
377
|
+
// 找不到要拉起的 exe 就绝不杀:「杀而不起」等于把人机器关了。
|
|
378
|
+
// dry-run 不需要 exe(只验证链路),所以只在非 dryRun 时硬拦。
|
|
379
|
+
if (!APP_EXE && !dryRun) {
|
|
380
|
+
log('APP_EXE unresolved: tried DSH_RESTART_APP_EXE / running-process path / <drive>:\\dsh\\DeepSeek Harness.exe / %LOCALAPPDATA%; refusing to kill DSH');
|
|
381
|
+
writeResult({ ok: false, dryRun, reason: 'app-exe-not-found', at: new Date().toISOString(), logFile, note: '找不到要拉起的桌面 App 路径,为免「杀而不起」本次不执行' });
|
|
382
|
+
clearRequest(req);
|
|
383
|
+
return 1;
|
|
384
|
+
}
|
|
385
|
+
log(`app exe: ${APP_EXE || '(unresolved, dry-run only)'} dir=${APP_DIR} port=${PORT}`);
|
|
386
|
+
|
|
340
387
|
const before = appProcesses();
|
|
341
388
|
log(`pre-kill app processes: ${JSON.stringify(before)}`);
|
|
342
389
|
log(`pre-kill port ${PORT}: ${(await portOpen()) ? 'open' : 'closed'}`);
|
|
@@ -540,8 +587,13 @@ async function main() {
|
|
|
540
587
|
return up.ok ? 0 : 1;
|
|
541
588
|
}
|
|
542
589
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
});
|
|
590
|
+
// 只有被直接执行时才跑 main();被 import(单测/自检)时只导出纯函数,不碰进程。
|
|
591
|
+
if (invokedDirectly) {
|
|
592
|
+
main().then((code) => process.exit(code)).catch((err) => {
|
|
593
|
+
log(`FATAL: ${err && err.stack ? err.stack : err}`);
|
|
594
|
+
try { writeResult({ ok: false, error: String(err), logFile }); } catch {}
|
|
595
|
+
process.exit(2);
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export { resolveAppExe, detectRunningAppExe, PORT, APP_EXE, APP_DIR, MODULE_DIR, LOGS_DIR };
|
|
@@ -117,6 +117,13 @@ if (running) { console.error(`helper already running (pid=${running}); refuse`);
|
|
|
117
117
|
const selfSession = opts.selfSession || process.env.DSH_SESSION_ID || '';
|
|
118
118
|
const probe = await waitQuiet({ selfSessionId: selfSession, maxWaitMs: 0 });
|
|
119
119
|
if (!opts.force && !probe.ok) {
|
|
120
|
+
// F3:unknown(读不到权威信号)与 busy(真有人在跑)必须分开说,否则用户看到「其他会话正在跑任务:[]」无从下手。
|
|
121
|
+
if (probe.unknown === true) {
|
|
122
|
+
console.error(`读不到权威信号(unknown),拒绝重启:sources=${JSON.stringify(probe.sources || [])}`);
|
|
123
|
+
console.error('市场接口与插件心跳都没读到(常见原因:宿主 web 端口不是 19387、或者这是 dsh web 而不是桌面端)。');
|
|
124
|
+
console.error('可设 DSH_RESTART_PORT / 让 DSH_WEB_URL 指向宿主地址后重试;或由用户同意后用 --force。');
|
|
125
|
+
process.exit(4);
|
|
126
|
+
}
|
|
120
127
|
console.error(`其他会话正在跑任务,拒绝重启:${JSON.stringify(probe.busy)}`);
|
|
121
128
|
console.error('要么等其他会话任务结束(到点前会等 3 秒静默),要么由用户同意后用 --force 重试。');
|
|
122
129
|
process.exit(7);
|
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 = '1.0.
|
|
26
|
+
export const VERSION = '1.0.1'
|
|
27
27
|
|
|
28
28
|
/** DSH 的家目录:与宿主其它状态(sessions / profiles)同一个根,多终端天然共享 */
|
|
29
29
|
export function dshHome() {
|