dsh-composer-ux 0.15.0 → 0.15.2
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 +28 -0
- package/README.md +3 -3
- package/lib/index.js +37 -12
- package/lib/index.js.map +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [0.15.2] — 2026-09-30(重启被挡时,现在会说清是哪一条不满足)
|
|
6
|
+
|
|
7
|
+
起因(用户截图):**侧边栏与设置页两枚「重启」都报** `restart is limited to same-origin loopback requests`。
|
|
8
|
+
|
|
9
|
+
那条守卫有四道(回环 peer / 无转发头 / 有 Origin / Origin 与 Host 同源),但失败时只给一句话 —— 用户与我
|
|
10
|
+
都只能猜是哪一道。这一版**不放松任何检查**,只把判据拆成具名步骤(`explainRestartTrust`)并把原因与
|
|
11
|
+
当时看到的事实(peer / origin / host / 有没有转发头)写进 403。
|
|
12
|
+
|
|
13
|
+
这一版**不保证修好重启**:先拿到那句原因,才知道该改客户端、改守卫,还是这台机器上换了打开方式。
|
|
14
|
+
(在此之前可以先手动重启一次 DSH —— 关掉再打开即可。)
|
|
15
|
+
|
|
16
|
+
## 验证
|
|
17
|
+
|
|
18
|
+
- `npm test`:**2351 passed / 0 failed**(23 个套件;新增 9 条守卫原因断言)。
|
|
19
|
+
- 变异测试:**160/160 全部被咬住**(新增 FG 原因文案不许空着 / FH 不许把 localhost 与 127.0.0.1 当同源)。
|
|
20
|
+
|
|
21
|
+
## [0.15.1] — 2026-09-30(与 0.15.0 同样的代码:只是换个版本号重发)
|
|
22
|
+
|
|
23
|
+
**这一版没有代码改动。** 0.15.0 推到 npm 之后,registry 把它留在了「已暂存」态:版本元数据先 404,
|
|
24
|
+
约 15 分钟后变成 200,但 **tarball 一直 404**(`pnpm install` 拉不到),重发被拒
|
|
25
|
+
`409 Cannot publish over previously staged version "0.15.0"`。这是 registry 侧的问题,
|
|
26
|
+
换个新版本号重发是唯一能绕开的正当手段 —— 所以 0.15.1 与 0.15.0 的内容**逐字节相同**。
|
|
27
|
+
|
|
28
|
+
功能、测试与验证同下一条:全屏重启页 + 自动重连 + 侧边栏重启入口。
|
|
29
|
+
|
|
5
30
|
## [0.15.0] — 2026-09-30(重启体验:全屏重启页 + 自动重连 + 侧边栏入口)
|
|
6
31
|
|
|
32
|
+
> ⚠️ 这个版本号在 npm 上的 tarball 卡在暂存态、**没有落地**,请用 0.15.1。
|
|
33
|
+
|
|
34
|
+
|
|
7
35
|
起因(用户原话):**「重启功能参考这个插件,看看这个插件是怎么重启的 https://github.com/Noah0509/dsh-quick-restart」**
|
|
8
36
|
(问清后确认:学它的**重启期间体验**,宿主半不动 —— 我们那半更稳:等端口真的空出来而不是等 pid,
|
|
9
37
|
Windows 的隐藏控制台、TIME_WAIT、起不来要把诊断写进日志这些坑都修过。)
|
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`:**160 条全部咬住**(判据是 `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 个套件;当前 2351 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 # 手动跑:变异测试,证明那套护栏真的在咬人(160 条,须单独跑)
|
|
646
646
|
# 同样记录结果,供下面的文档门禁核对
|
|
647
647
|
npm run gates # 发版门禁三条一起跑:tag 指向 / 包内容 / 文档数字
|
|
648
648
|
npm run check:tag # ① tag 名里的版本 == 该 tag 所指提交里的 package.json 版本
|
package/lib/index.js
CHANGED
|
@@ -3915,19 +3915,40 @@ function servingPort(hostHeader) {
|
|
|
3915
3915
|
function isLoopbackAddress(address) {
|
|
3916
3916
|
return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
|
|
3917
3917
|
}
|
|
3918
|
-
function
|
|
3919
|
-
if (!isLoopbackAddress(request.remoteAddress)) return false;
|
|
3918
|
+
function explainRestartTrust(request) {
|
|
3920
3919
|
const headers = request.headers;
|
|
3921
|
-
|
|
3922
|
-
const
|
|
3923
|
-
const
|
|
3924
|
-
|
|
3920
|
+
const origin = firstHeader(headers.origin) ?? "";
|
|
3921
|
+
const host = firstHeader(headers.host) ?? "";
|
|
3922
|
+
const peer = request.remoteAddress ?? "";
|
|
3923
|
+
const forwarded = firstHeader(headers.forwarded) ?? firstHeader(headers["x-forwarded-for"]) ?? firstHeader(headers["x-real-ip"]) ?? "";
|
|
3924
|
+
const base = { peer, origin, host, forwarded };
|
|
3925
|
+
if (!isLoopbackAddress(request.remoteAddress)) return { ...base, reason: "peer-not-loopback" };
|
|
3926
|
+
if (forwarded !== "") return { ...base, reason: "forwarded-header" };
|
|
3927
|
+
if (origin === "") return { ...base, reason: "no-origin" };
|
|
3928
|
+
if (host === "") return { ...base, reason: "no-host" };
|
|
3925
3929
|
try {
|
|
3926
3930
|
const parsed = new URL(origin);
|
|
3927
|
-
|
|
3931
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return { ...base, reason: "origin-unparsable" };
|
|
3932
|
+
return { ...base, reason: parsed.host === host ? "ok" : "origin-host-mismatch" };
|
|
3928
3933
|
} catch {
|
|
3929
|
-
return
|
|
3930
|
-
}
|
|
3934
|
+
return { ...base, reason: "origin-unparsable" };
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
function restartTrustText(facts) {
|
|
3938
|
+
if (facts.reason === "ok") return "";
|
|
3939
|
+
const why = {
|
|
3940
|
+
"peer-not-loopback": "\u8BF7\u6C42\u4E0D\u662F\u4ECE\u672C\u673A\u56DE\u73AF\u5730\u5740\u6765\u7684",
|
|
3941
|
+
"forwarded-header": "\u8BF7\u6C42\u5E26\u7740\u8F6C\u53D1\u5934\uFF08\u4E2D\u95F4\u6709\u4EE3\u7406\uFF0C\u4E0D\u662F\u4F60\u7684\u6D4F\u89C8\u5668\u76F4\u8FDE\uFF09",
|
|
3942
|
+
"no-origin": "\u8BF7\u6C42\u6CA1\u6709 Origin \u5934\uFF08\u6D4F\u89C8\u5668\u540C\u6E90 POST \u5E94\u5F53\u4F1A\u5E26\uFF09",
|
|
3943
|
+
"no-host": "\u8BF7\u6C42\u6CA1\u6709 Host \u5934",
|
|
3944
|
+
"origin-unparsable": "Origin \u5934\u89E3\u6790\u4E0D\u51FA http/https \u6765\u6E90",
|
|
3945
|
+
"origin-host-mismatch": "Origin \u4E0E Host \u4E0D\u662F\u540C\u4E00\u4E2A\u6765\u6E90"
|
|
3946
|
+
};
|
|
3947
|
+
const peer = facts.peer === "" ? "\u7A7A" : facts.peer;
|
|
3948
|
+
const origin = facts.origin === "" ? "\u7A7A" : facts.origin;
|
|
3949
|
+
const host = facts.host === "" ? "\u7A7A" : facts.host;
|
|
3950
|
+
const fwd = facts.forwarded === "" ? "\u65E0" : facts.forwarded;
|
|
3951
|
+
return why[facts.reason] + "\uFF08peer=" + peer + " \xB7 origin=" + origin + " \xB7 host=" + host + " \xB7 \u8F6C\u53D1\u5934=" + fwd + "\uFF09";
|
|
3931
3952
|
}
|
|
3932
3953
|
function firstHeader(value) {
|
|
3933
3954
|
if (value === void 0) return void 0;
|
|
@@ -7444,11 +7465,15 @@ function apply(ctx, config) {
|
|
|
7444
7465
|
send(200, { ok: true, ...plan, running, blocked, boot: BOOT_ID });
|
|
7445
7466
|
return;
|
|
7446
7467
|
}
|
|
7447
|
-
|
|
7468
|
+
const trust = explainRestartTrust({
|
|
7448
7469
|
remoteAddress: req.socket?.remoteAddress,
|
|
7449
7470
|
headers: req.headers ?? {}
|
|
7450
|
-
})
|
|
7451
|
-
|
|
7471
|
+
});
|
|
7472
|
+
if (trust.reason !== "ok") {
|
|
7473
|
+
send(403, {
|
|
7474
|
+
ok: false,
|
|
7475
|
+
error: "restart is limited to same-origin loopback requests \u2014\u2014 " + restartTrustText(trust)
|
|
7476
|
+
});
|
|
7452
7477
|
return;
|
|
7453
7478
|
}
|
|
7454
7479
|
if (blocked !== null) {
|