dsh-composer-ux 0.15.5 → 0.16.0
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 +30 -0
- package/README.md +3 -3
- package/lib/index.js +42 -3
- package/lib/index.js.map +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [0.16.0] — 2026-09-30(桌面重启改成「连壳一起换掉」—— 不再弹那个恢复框)
|
|
6
|
+
|
|
7
|
+
前置事实(0.15.5 修好命令行之后发现的):桌面壳把宿主子进程的 close **一律**判成失败
|
|
8
|
+
(连退出码 0 也照样 `fail()`),唯一的恢复式重启是它弹框里那个按钮。所以 0.15.5 之后,
|
|
9
|
+
我们越能成功重启,那个框越会弹 —— 而且它成了陷阱(点里面的「重启」会再拉一整个应用抢端口)。
|
|
10
|
+
证据与推演见仓库根的 `重启机制改造计划.md`。
|
|
11
|
+
|
|
12
|
+
**改法**(照 `HHHEEEWWW/dsh-quick-restart` 的核心思路,但规避它 README 自陈的两个致命坑):
|
|
13
|
+
|
|
14
|
+
- 新增 `desktopKindOf()`:`ELECTRON_RUN_AS_NODE=1` 且 `execPath` 是 `.exe` ⇒ 桌面形态;
|
|
15
|
+
- 桌面形态的助手:`taskkill /F /PID <壳>`(**只用 /PID,绝不用 /T** —— /T 会把负责重启的助手自己
|
|
16
|
+
一起带走)→ 轮询等旧宿主真的没了 → 用应用 exe 重建,且**必须删掉 `ELECTRON_RUN_AS_NODE`**
|
|
17
|
+
(留着它拉起来的会是"又一个 node 进程",不是应用)→ **不按映像名扫描**(`/IM` 会误杀自己;
|
|
18
|
+
渲染进程随 Electron 主进程自然退出);
|
|
19
|
+
- `dsh web` 形态一行不改:等端口 → 重拉 → 校验端口;
|
|
20
|
+
- `RestartIo`/`planRestart` 补 `ppid`,宿主传入 `process.ppid`。
|
|
21
|
+
|
|
22
|
+
## 验证
|
|
23
|
+
|
|
24
|
+
- `npm test`:**2368 passed / 0 failed**(23 个套件;新增 8 条:形态判定 3 条 + 助手源码约束 5 条)。
|
|
25
|
+
- 变异:**164/164 全部咬住**(新增 FN 忘了删环境变量 / FO 带上 /T;并修好三条**假测试** ——
|
|
26
|
+
FN、FO、以及老变异 D 的断言原来都在"查文本",注释里或别处的同类代码就能骗过它们,
|
|
27
|
+
现在一律改成效果导向)。
|
|
28
|
+
|
|
29
|
+
## 升级说明
|
|
30
|
+
|
|
31
|
+
- 桌面版:装完**仍需手动重启一次**(`C:\Users\fangwen\.dsh\launcher\restart-webui.bat`)让这一版生效;
|
|
32
|
+
之后界面上的重启按钮才走新机制。
|
|
33
|
+
- 已知代价:杀掉壳会让**窗口短暂整个消失**再回来(参考插件也接受这个代价)。
|
|
34
|
+
|
|
5
35
|
## [0.15.5] — 2026-09-30(**真凶**:重启时重放的启动命令是坏的 —— 桌面版一直没成功过)
|
|
6
36
|
|
|
7
37
|
前一轮查壳的判定时顺手翻了自己的助手日志,发现一个比弹框严重得多的问题。
|
package/README.md
CHANGED
|
@@ -506,7 +506,7 @@ Windows 上 DSH 给模型的终端工具是 **PowerShell**(工具名 `pwsh`)
|
|
|
506
506
|
| 工具循环的消息形状 / 轮次封顶 / 异常必须降级 | 同上第 6 节(假 llm 驱动整条循环,逐条检查 `role:'tool'` 与 `toolCallId`) |
|
|
507
507
|
| 工具路径:默认关、开了才派、**查完吐散文必须回落**、拿不到工作目录就不派 | `test/quick-commands.mjs` 5g 节(13 条,真路由 + 假 llm) |
|
|
508
508
|
| 工具读到的内容不进台账 | 同 5g 节:断言台账里只有工具名与次数、没有读到的文件内容 |
|
|
509
|
-
| 变异守卫真的会咬人,且**必须是指定的那条测试变红** | `npm run test:mutations`:**
|
|
509
|
+
| 变异守卫真的会咬人,且**必须是指定的那条测试变红** | `npm run test:mutations`:**164 条全部咬住**(判据是 `red.some(line => line.includes(item.expect))`,不是"有红就行") |
|
|
510
510
|
| 发布门禁三条判据 | `npm run gates` 全过;`check:tag` 实测 `v0.12.0` 的 tag 名与所指提交里的版本一致 |
|
|
511
511
|
| 三平台 CI | GitHub Actions:ubuntu / windows / macos × Node 20(`npm ci → typecheck → build → test → check:pack → check:docs`) |
|
|
512
512
|
|
|
@@ -640,9 +640,9 @@ node build.mjs # 产出 lib/index.js + lib/client.js
|
|
|
640
640
|
# ⚠️ 宿主半会**内联** schemastery / cosmokit:
|
|
641
641
|
# 优先用 DSH 检出里的 vendor 副本,检出不在时退到 node_modules
|
|
642
642
|
# 里同版本的 npm 包(两者逐字节相同)——CI 上走的就是退路
|
|
643
|
-
npm test # 23 个套件;当前
|
|
643
|
+
npm test # 23 个套件;当前 2368 passed, 0 failed(2026-09-29 实测;CI 三平台同样全绿)
|
|
644
644
|
# 走 scripts/run-tests.mjs:顺带把"多少套件/多少条"记进 test/.last-run.json
|
|
645
|
-
npm run test:mutations # 手动跑:变异测试,证明那套护栏真的在咬人(
|
|
645
|
+
npm run test:mutations # 手动跑:变异测试,证明那套护栏真的在咬人(164 条,须单独跑)
|
|
646
646
|
# 同样记录结果,供下面的文档门禁核对
|
|
647
647
|
npm run gates # 发版门禁三条一起跑:tag 指向 / 包内容 / 文档数字
|
|
648
648
|
npm run check:tag # ① tag 名里的版本 == 该 tag 所指提交里的 package.json 版本
|
package/lib/index.js
CHANGED
|
@@ -3982,8 +3982,12 @@ function detectedSupervisor(input) {
|
|
|
3982
3982
|
if (input.ppid === 1 || input.parentComm(input.ppid) === "systemd") return "systemd";
|
|
3983
3983
|
return null;
|
|
3984
3984
|
}
|
|
3985
|
+
function desktopKindOf(input) {
|
|
3986
|
+
return input.env.ELECTRON_RUN_AS_NODE === "1" && /\.exe$/iu.test(input.execPath);
|
|
3987
|
+
}
|
|
3985
3988
|
function restartHelperSource(input) {
|
|
3986
|
-
const { spawned, cwd, logs, port } = input;
|
|
3989
|
+
const { spawned, cwd, logs, port, desktop } = input;
|
|
3990
|
+
const desktopFacts = desktop === null || desktop === void 0 ? "null" : `{ shellPid: ${String(desktop.shellPid)}, hostPid: ${String(desktop.hostPid)}, appExe: ${JSON.stringify(desktop.appExe)} }`;
|
|
3987
3991
|
return [
|
|
3988
3992
|
"const { spawn } = require('node:child_process')",
|
|
3989
3993
|
"const fs = require('node:fs')",
|
|
@@ -3996,6 +4000,7 @@ function restartHelperSource(input) {
|
|
|
3996
4000
|
`const logOut = ${JSON.stringify(logs.out)}`,
|
|
3997
4001
|
`const logErr = ${JSON.stringify(logs.err)}`,
|
|
3998
4002
|
`const port = ${JSON.stringify(port)}`,
|
|
4003
|
+
`const desktop = ${desktopFacts}`,
|
|
3999
4004
|
`const pollMs = ${String(RESTART_POLL_MS)}`,
|
|
4000
4005
|
`const portWaitMs = ${String(RESTART_PORT_WAIT_MS)}`,
|
|
4001
4006
|
`const settleMs = ${String(RESTART_PORT_SETTLE_MS)}`,
|
|
@@ -4046,7 +4051,39 @@ function restartHelperSource(input) {
|
|
|
4046
4051
|
" while (Date.now() < upBy && !(await listening())) await sleep(500)",
|
|
4047
4052
|
' if (!(await listening())) note("the replacement did not bind port " + port + " within " + replacementWaitMs + "ms \u2014 see the output log beside this one")',
|
|
4048
4053
|
"}",
|
|
4049
|
-
"
|
|
4054
|
+
"const alive = (pid) => new Promise((resolve) => {",
|
|
4055
|
+
' let out = ""',
|
|
4056
|
+
' const probe = spawn("tasklist", ["/FI", "PID eq " + pid, "/FO", "CSV", "/NH"], { windowsHide: true })',
|
|
4057
|
+
' probe.stdout.on("data", (chunk) => { out += String(chunk) })',
|
|
4058
|
+
' probe.on("close", () => resolve(out.includes("\\"" + pid + "\\"")))',
|
|
4059
|
+
' probe.on("error", () => resolve(false))',
|
|
4060
|
+
"})",
|
|
4061
|
+
"// \u684C\u9762\u5F62\u6001\uFF1A\u8FDE\u58F3\u4E00\u8D77\u6362\u6389 \u2014\u2014 \u58F3\u770B\u4E0D\u5230\u81EA\u5DF1\u5B50\u8FDB\u7A0B\u7684\u9000\u51FA\uFF0C\u5C31\u4E0D\u4F1A\u5F39\u6062\u590D\u6846\u3002",
|
|
4062
|
+
"const desktopMain = async () => {",
|
|
4063
|
+
' const out = fs.openSync(logOut, "a")',
|
|
4064
|
+
' const err = fs.openSync(logErr, "a")',
|
|
4065
|
+
' // \u53EA /PID\uFF1A/T \u4F1A\u628A"\u8D1F\u8D23\u91CD\u542F\u7684\u52A9\u624B\u81EA\u5DF1"\u4E00\u8D77\u5E26\u8D70\uFF08\u53C2\u8003\u63D2\u4EF6\u8E29\u8FC7\u7684\u5751\uFF09\u3002',
|
|
4066
|
+
" try {",
|
|
4067
|
+
' const killer = spawn("taskkill", ["/F", "/PID", String(desktop.shellPid)], { windowsHide: true, stdio: ["ignore", out, err] })',
|
|
4068
|
+
' killer.on("error", (error) => note("could not kill the shell: " + (error && error.message ? error.message : String(error))))',
|
|
4069
|
+
" killer.unref()",
|
|
4070
|
+
' } catch (error) { note("could not kill the shell: " + String(error)) }',
|
|
4071
|
+
" const until = Date.now() + portWaitMs",
|
|
4072
|
+
" while (Date.now() < until && await alive(desktop.hostPid)) await sleep(pollMs)",
|
|
4073
|
+
' if (await alive(desktop.hostPid)) note("the old host was still alive after " + portWaitMs + "ms; relaunching anyway")',
|
|
4074
|
+
" await sleep(settleMs)",
|
|
4075
|
+
' // \u5FC5\u987B\u5220\u6389 ELECTRON_RUN_AS_NODE\uFF1A\u7559\u7740\u5B83\u62C9\u8D77\u6765\u7684\u4F1A\u662F"\u53C8\u4E00\u4E2A node \u8FDB\u7A0B"\uFF0C\u4E0D\u662F\u5E94\u7528\u3002',
|
|
4076
|
+
" const env = { ...process.env }",
|
|
4077
|
+
" delete env.ELECTRON_RUN_AS_NODE",
|
|
4078
|
+
" try {",
|
|
4079
|
+
' const child = spawn(desktop.appExe, [], { detached: true, stdio: ["ignore", out, err], env, windowsHide: true })',
|
|
4080
|
+
' child.on("error", (error) => note("could not relaunch the app: " + (error && error.message ? error.message : String(error))))',
|
|
4081
|
+
" child.unref()",
|
|
4082
|
+
' } catch (error) { note("could not relaunch the app: " + String(error)) }',
|
|
4083
|
+
" await sleep(lingerMs)",
|
|
4084
|
+
"}",
|
|
4085
|
+
"if (desktop === null) main()",
|
|
4086
|
+
"else desktopMain()"
|
|
4050
4087
|
].join("\n");
|
|
4051
4088
|
}
|
|
4052
4089
|
function planRestart(io, port) {
|
|
@@ -4061,6 +4098,7 @@ function planRestart(io, port) {
|
|
|
4061
4098
|
resolve: io.resolve,
|
|
4062
4099
|
dirname: io.dirname
|
|
4063
4100
|
});
|
|
4101
|
+
const desktop = desktopKindOf({ env: io.env, execPath: io.execPath }) ? { shellPid: io.ppid, hostPid: io.pid, appExe: io.execPath } : null;
|
|
4064
4102
|
const respawn = respawnCommand(launch, io.platform);
|
|
4065
4103
|
const logOut = io.join(io.tmpdir, `${RESTART_LOG_PREFIX}${io.stamp}.out.log`);
|
|
4066
4104
|
const logErr = io.join(io.tmpdir, `${RESTART_LOG_PREFIX}${io.stamp}.err.log`);
|
|
@@ -4068,7 +4106,7 @@ function planRestart(io, port) {
|
|
|
4068
4106
|
node,
|
|
4069
4107
|
launch,
|
|
4070
4108
|
respawn,
|
|
4071
|
-
helper: restartHelperSource({ spawned: respawn, cwd: launch.cwd, logs: { out: logOut, err: logErr }, port }),
|
|
4109
|
+
helper: restartHelperSource({ spawned: respawn, cwd: launch.cwd, logs: { out: logOut, err: logErr }, port, desktop }),
|
|
4072
4110
|
logOut,
|
|
4073
4111
|
logErr,
|
|
4074
4112
|
port
|
|
@@ -7394,6 +7432,7 @@ function apply(ctx, config) {
|
|
|
7394
7432
|
const buildIo = () => ({
|
|
7395
7433
|
platform: process.platform,
|
|
7396
7434
|
pid: process.pid,
|
|
7435
|
+
ppid: process.ppid,
|
|
7397
7436
|
argv0: process.argv0,
|
|
7398
7437
|
execPath: process.execPath,
|
|
7399
7438
|
argv1: process.argv[1],
|