dsh-remote-agent 0.9.1 → 0.9.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 CHANGED
@@ -9,6 +9,108 @@
9
9
 
10
10
  ---
11
11
 
12
+ ## [0.9.2] - 2026-10-18
13
+
14
+ > **紧急修复:0.9.0/0.9.1 会让新版 DSH 启动崩溃。**
15
+ > 如果你装了 0.9.x 并升级过 DSH(0.2.1-rc.2 及以后),**先升级到本版**。
16
+
17
+ ### 🔴 严重事故:包内 patch 绑 `host: '0.0.0.0'` 导致 DSH 启动崩溃
18
+
19
+ **这是我的错。** 0.9.0 为了让"副机连得上",在包内 patch 里加了:
20
+
21
+ ```yaml
22
+ - id: webserver
23
+ config:
24
+ host: '0.0.0.0'
25
+ port: 19387
26
+ ```
27
+
28
+ 新版 `@deepseek-ai/dsh-host-webserver` 在 **schema 校验阶段**就硬拒绝通配地址:
29
+
30
+ ```
31
+ webserver: host "0.0.0.0" is an unspecified (wildcard) address, which is not
32
+ supported: binding every interface would expose remote code execution to the
33
+ network; bind one concrete IPv4 or IPv6 address of a local interface instead
34
+ ```
35
+
36
+ **后果链**:
37
+
38
+ ```
39
+ webserver(required)激活失败
40
+ → webServer 服务缺失
41
+ → 至少 6 个插件排队等它
42
+ (web-runtime / open-in-app / directory-picker / client-hmr / remote-agent …)
43
+ → host 启动失败
44
+ ```
45
+
46
+ 实测在用户机器上产生 **10 份 crash 日志**(`crash-*.log`)。
47
+
48
+ **这比它想修的"副机连不上"严重得多:连 DSH 都起不来了。**
49
+
50
+ ### 修复
51
+
52
+ **包内 patch 不再碰 `webserver` 的任何字段。** 理由:
53
+
54
+ 1. `host` 必须是**具体某个本机接口地址**(Tailscale IP / 局域网 IP),
55
+ 而这个值因人而异 —— 包里写死任何具体 IP 都是错的。
56
+ 2. 插件在 `apply()` 里**也不能**自己读网卡再拼配置:loader 的 patch 是
57
+ 启动前静态合并的,改不了自己所在那一行的 config。
58
+ 3. 实际做法:**由用户在 profile 里配一次**,插件负责**检测并说清该怎么配**。
59
+
60
+ 配套修正(四处面向用户的文案,它们此前都在建议 `0.0.0.0`):
61
+
62
+ - `lib/env-check.js` 的 reachability 修法 → 改为"具体的本机 IP",并明确警告
63
+ 不能用 0.0.0.0,教会用户 `tailscale ip -4` / `ipconfig` 查地址
64
+ - `lib/client.js` 的红色横幅 → 同上
65
+ - `lib/hub/index.js` 的可达性自检 hint 与 log fix → 同上
66
+ - `README.md` / `README.en.md` 的安装段 → 重写,把"必须绑具体 IP"讲清楚
67
+
68
+ ### 断言方向反转(79 / 新增 79b)
69
+
70
+ **原来的断言 79 是错的** —— 它要求包内 patch **绑** `0.0.0.0`。现在反过来:
71
+
72
+ - **79**:包内 patch 的**生效行**不得出现 `- id: webserver` 或
73
+ `host: '0.0.0.0'`,且必须把事故写进注释留档
74
+ - **79b**:所有面向用户的修法文案都不得建议 `host: 0.0.0.0`,
75
+ 且必须包含"不能用 0.0.0.0"的明确警告
76
+
77
+ 79 只扫**非注释行** —— 注释里必须保留事故原文(含 `0.0.0.0` 示例),
78
+ 那是给后来者的警示,不该被判违规。
79
+
80
+ ### 现在"副机连不上"怎么办
81
+
82
+ 包内 patch 不再代你配 `webserver`,所以**新装用户要自己填一次 host**。
83
+ 但这件事不再是静默失败:
84
+
85
+ - 面板加载时**自动体检**,绑 loopback 会显示红色横幅
86
+ - 「环境体检」里的 reachability 项会告诉你**当前绑在哪**、**该改成什么**
87
+ - 提示里直接教怎么查自己的 IP
88
+
89
+ **填什么**:
90
+
91
+ ```yaml
92
+ - id: webserver
93
+ config:
94
+ host: '100.x.x.x' # ← 换成你自己的地址(推荐 Tailscale IP)
95
+ port: 19387
96
+ ```
97
+
98
+ 查地址:`tailscale ip -4`(跨网,固定不变)或 `ipconfig`(同一局域网)。
99
+
100
+ ### 其他
101
+
102
+ - 新增 `scripts/diag-worker.ps1` —— 副机诊断脚本(**只读**,不改任何文件),
103
+ 输出 profile 列表、`remote-agent` 配置、插件版本、面板端点探测、
104
+ 配置备份时间线。用于排查"每次重启都回到引导页"这类问题。
105
+
106
+ ```
107
+ packaging 4 项
108
+ doc-drift 121/121
109
+ diag:inject 11 项
110
+ tests 949(948 pass / 0 fail / 1 skip)
111
+ lint 干净
112
+ ```
113
+
12
114
  ## [0.9.1] - 2026-10-17
13
115
 
14
116
  > **0.9.0 发布约 40 分钟后补发**。两个 High 都是真实功能失效
package/README.en.md CHANGED
@@ -44,9 +44,16 @@ memory badges, task dispatch, task states, result summaries, workspace changes,
44
44
  change diffs (unified diffs of text-file edits), final artifacts, log tail,
45
45
  **environment health-check**, **workspace sharing (SMB)**, model dropdown.
46
46
 
47
- > **Works out of the box**: restart DSH after install and it just works. The
48
- > bundled patch defaults `role: auto` (no `hubUrl` ⇒ hub) and binds `webserver`
49
- > to `0.0.0.0:19387` (otherwise workers can never connect). See *Install* below.
47
+ > **Works out of the box**: restart DSH after install and the bundled patch
48
+ > defaults `role: auto` (no `hubUrl` ⇒ hub), which takes effect automatically.
49
+ >
50
+ > **For workers to connect, the hub's `webserver` must bind one concrete local
51
+ > IP** (DSH binds `127.0.0.1` by default, which workers can never reach). You
52
+ > fill this in once — the panel's *environment health-check* tells you exactly
53
+ > what to put there.
54
+ >
55
+ > ⚠ **Never use `0.0.0.0`**: newer DSH rejects wildcard addresses and will
56
+ > **fail to start**. See *Install* below.
50
57
 
51
58
  <p align="center">
52
59
  <img src="docs/screenshots/panel-main.png" width="720" alt="Web panel: nodes, dispatch form, task list">
package/README.md CHANGED
@@ -42,7 +42,13 @@
42
42
  **工作区共享(SMB)**、模型下拉。
43
43
 
44
44
  > **开箱即用**:装完重启 DSH 就能用。包内 patch 默认 `role: auto`(没配
45
- > `hubUrl` 就是主机),并把 `webserver` 绑到 `0.0.0.0:19387`(否则副机连不上)。
45
+ > `hubUrl` 就是主机),自动生效。
46
+ >
47
+ > **副机要能连上,主机的 `webserver` 必须绑一个具体的本机 IP**(DSH 默认只绑
48
+ > `127.0.0.1`,那样副机永远连不上)。这一步需要你填一次 —— 打开面板时
49
+ > 「环境体检」会检查并告诉你该填什么。
50
+ >
51
+ > ⚠ **不能用 `0.0.0.0`**:新版 DSH 会硬拒绝通配地址,导致 DSH **启动崩溃**。
46
52
  > 详见下方「安装」。
47
53
 
48
54
  <p align="center">
@@ -210,20 +216,41 @@ npm install -g dsh-remote-agent
210
216
 
211
217
  或在 DSH 的插件对话框里填 `github:zbzbhs/dsh-remote-agent`。
212
218
 
213
- **装完重启 DSH 就能用** —— 不需要改任何配置:
219
+ **装完重启 DSH 就能用**:
214
220
 
215
221
  - 包内 patch 默认 `role: auto`。**没配 `hubUrl` 就是主机**,自动生效。
216
- - 包内 patch 把 `webserver` 绑到 `0.0.0.0:19387`,**副机才能连上**
217
- (DSH 默认只绑 `127.0.0.1`,那样副机永远连不上且主机侧毫无异常)。
218
222
  - 打开 Web 面板「远程同事」,点「副机接入」生成配对码,发给副机。
219
223
 
220
- > **安全披露**:绑 `0.0.0.0` 意味着 **DSH 的整个 Web GUI 也能被局域网访问**
221
- > (不只是本插件的节点端点)。DSH 自带 Host/Origin 围栏 + 浏览器 cookie 鉴权,
222
- > 但暴露面确实变大了。如果你不需要跨机访问,可以在 profile 的
223
- > `cordis.patch.yml` 里覆盖回 `127.0.0.1` —— 但那样副机就连不上,
224
- > 请改用 [中继模式](profiles/README.md)。
224
+ **唯一需要你填一次的是 `webserver` 的 host** —— 副机要能连上,主机必须绑一个
225
+ **具体的本机 IP**(DSH 默认只绑 `127.0.0.1`,那样副机永远连不上,而且主机侧
226
+ 毫无异常)。在 profile 的 `cordis.patch.yml` 里:
227
+
228
+ ```yaml
229
+ - id: webserver
230
+ config:
231
+ host: '100.x.x.x' # ← 换成你自己的地址
232
+ port: 19387
233
+ ```
234
+
235
+ **怎么查自己的地址**:
236
+
237
+ - 跨网(推荐):`tailscale ip -4` —— Tailscale IP 固定不变、跨网可达
238
+ - 同一局域网:`ipconfig` 看 WLAN/以太网那块网卡的 IPv4
239
+
240
+ > ⚠ **绝对不能写 `0.0.0.0`**。新版 `@deepseek-ai/dsh-host-webserver` 在 schema
241
+ > 校验阶段就会硬拒绝通配地址:
242
+ >
243
+ > ```
244
+ > host "0.0.0.0" is an unspecified (wildcard) address, which is not supported:
245
+ > binding every interface would expose remote code execution to the network;
246
+ > bind one concrete IPv4 or IPv6 address of a local interface instead
247
+ > ```
248
+ >
249
+ > 后果是 **DSH 启动崩溃**(`webserver` 是 required 插件,它挂了 `webServer`
250
+ > 服务就缺失,至少 6 个插件排队等它,包括本插件)。
225
251
  >
226
- > 面板的「体检」按钮会明确告诉你当前绑在哪(面板加载时也会自动体检一次)。
252
+ > 面板的「环境体检」会自动检测这一项,绑错了会明确告诉你当前绑在哪、
253
+ > 该改成什么 —— 面板加载时也会自动体检一次。
227
254
 
228
255
  ### 副机(worker)—— 贴个配对码就行
229
256
 
package/cordis.patch.yml CHANGED
@@ -99,36 +99,43 @@
99
99
  role: auto
100
100
 
101
101
  # ═══════════════════════════════════════════════════════════════════════════
102
- # 让 hub 真的能被外部访问(2026-10-17,**这是"开箱即用"的关键一条**)
102
+ # ⚠⚠ 绝对不要覆盖 webserver 的 host(2026-10-18 事故:我犯过这个错)
103
103
  # ═══════════════════════════════════════════════════════════════════════════
104
104
  #
105
- # ── 为什么必须改 webserver ──────────────────────────────────────────────
105
+ # ── 事故经过 ────────────────────────────────────────────────────────────
106
106
  #
107
- # 本插件的副机要主动连主机:`ws://<主机IP>:19387/api/remote-agent/node`。
108
- # 而那个 WebSocket upgrade 是注册在 DSH 的 `ctx.webServer` 上的
109
- # (`ctx.webServer.registerUpgrade(...)`)—— **没有独立端口选项**。
107
+ # v0.9.0 为了让"副机连得上",我在包内 patch 里加了:
110
108
  #
111
- # 而 DSH 的 `@deepseek-ai/dsh-host-webserver` 的 `host` 是必填项、无默认值,
112
- # 且它面向的默认场景是"本机浏览器访问自己",所以多数 profile 绑 127.0.0.1。
109
+ # - id: webserver
110
+ # config:
111
+ # host: '0.0.0.0'
112
+ # port: 19387
113
+ #
114
+ # **结果:DSH 直接启动崩溃。** 新版 `@deepseek-ai/dsh-host-webserver` 在
115
+ # schema 校验阶段就硬拒绝通配地址:
116
+ #
117
+ # webserver: host "0.0.0.0" is an unspecified (wildcard) address, which is
118
+ # not supported: binding every interface would expose remote code execution
119
+ # to the network; bind one concrete IPv4 or IPv6 address of a local
120
+ # interface instead
121
+ #
122
+ # 后果链:webserver(required)激活失败 → `webServer` 服务缺失 → 至少 6 个
123
+ # 插件排队等它(web-runtime / open-in-app / directory-picker / client-hmr /
124
+ # **remote-agent** …)→ host 启动失败。实测产生了 10 份 crash 日志。
113
125
  #
114
- # 结果:不覆盖这一行,hub 只监听 loopback → **副机永远连不上**,而主机侧
115
- # 毫无异常(healthz 200、面板能开、工具都在)。用户只能猜"地址错了?
116
- # token 错了?防火墙?" —— 2026-10-17 实测踩到,代价是好几轮排查。
126
+ # 这个错误比它想修的"副机连不上"严重得多:**连 DSH 都起不来了**。
117
127
  #
118
- # ── 安全披露(必须让用户知道)──────────────────────────────────────────
128
+ # ── 所以现在的做法 ──────────────────────────────────────────────────────
119
129
  #
120
- # 绑 0.0.0.0 意味着 **DSH 的整个 Web GUI 也能被局域网访问**(不只是我们的
121
- # 节点端点)。DSH 自带 Host/Origin 围栏 + 浏览器 cookie 鉴权,但暴露面确实
122
- # 变大了。所以:
123
- # · README 与面板都有明确提示
124
- # · 环境体检把这条列出来
125
- # · 用户可在 profile 里覆盖回 127.0.0.1(此时请改用 relay 模式,否则副机连不上)
130
+ # **本包不碰 webserver 的任何字段。** 理由:
126
131
  #
127
- # ── 优先级 ──────────────────────────────────────────────────────────────
132
+ # 1. host 必须是**具体某个本机接口地址**(如 Tailscale IP / 局域网 IP),
133
+ # 而这个值因人而异 —— 包里写死任何具体 IP 都是错的。
134
+ # 2. 插件在 `apply()` 里也**不能**自己读网卡再拼配置:loader 的 patch 是
135
+ # 启动前静态合并的,改不了自己所在那一行的 config。
136
+ # 3. 实际做法:**主机的 webserver 由用户/DSh 自己配**,我们只在
137
+ # 「环境体检」里检测"是否绑在可达地址",绑 loopback 就明确报错并给出
138
+ # 该填什么(含如何查自己的 Tailscale / 局域网 IP)。
128
139
  #
129
- # `cordis.yml` 头注释:应用顺序是 bundles → cordis.patch.yml → --patch overlays。
130
- # 所以**用户 profile 里的同名行会覆盖这里**(用户优先,符合预期)。
131
- - id: webserver
132
- config:
133
- host: '0.0.0.0'
134
- port: 19387
140
+ # 这就是 0575c6b 的可达性自检 + c4459c5 的环境体检的价值 —— **诊断而不是
141
+ # 代改配置**。2026-10-17 我想"帮用户改好",结果改出了启动崩溃。
package/lib/client.js CHANGED
@@ -1736,8 +1736,15 @@ window.__ModuleLoader__.load({
1736
1736
  h('div', null, '现在监听在 ', h('code', null, reach.origin || ''),
1737
1737
  ',只有本机能访问。副机用任何地址都连不上。'),
1738
1738
  h('div', { style: { marginTop: '6px' } }, '修法:在 profile 的 ',
1739
- h('code', null, 'cordis.patch.yml'), ' 里给 ', h('code', null, 'webserver'),
1740
- ' 行加 ', h('code', null, "host: '0.0.0.0'"), '(保留 port),然后重启 DSH。'),
1739
+ h('code', null, 'cordis.patch.yml'), ' 里把 ', h('code', null, 'webserver'),
1740
+ ' 的 ', h('code', null, 'host'), ' 改成一个具体的本机 IP,然后重启 DSH。'),
1741
+ // ⚠ 修法**不能写 0.0.0.0**(2026-10-18 事故):新版 dsh-host-webserver
1742
+ // 在 schema 校验阶段就拒绝通配地址,会让 **DSH 启动崩溃**
1743
+ // (webserver 是 required 插件)。必须绑一个具体的本机接口地址。
1744
+ h('div', { className: 'dra-dim', style: { marginTop: '2px' } },
1745
+ '⚠ 不能用 0.0.0.0 —— 新版 DSH 会拒绝并导致启动失败。'
1746
+ + '查地址:Tailscale 用 tailscale ip -4,局域网用 ipconfig。'
1747
+ + '推荐绑 Tailscale IP(固定不变、跨网可达)。'),
1741
1748
  reach.hint
1742
1749
  ? h('div', { className: 'dra-dim', style: { marginTop: '4px' } }, reach.hint)
1743
1750
  : null,
package/lib/env-check.js CHANGED
@@ -206,11 +206,30 @@ export function envChecks({
206
206
  // 修法给**可直接粘贴的片段**(v0.9.0 审查 L3:不假装有一键修复)。
207
207
  // 这是用户 profile 的配置,插件客观上不能静默代改 ——
208
208
  // 用户 profile 优先于包内 patch 是明确的设计。
209
- fix: "在 profile 的 cordis.patch.yml 里加下面这段(或把已有的 webserver 行改成它),然后重启 DSH:\n"
210
- + " - id: webserver\n"
211
- + " config:\n"
212
- + " host: '0.0.0.0'\n"
213
- + " port: 19387",
209
+ //
210
+ // ⚠ **绝对不要建议 `0.0.0.0`**(2026-10-18 事故)。
211
+ //
212
+ // 新版 `dsh-host-webserver` 在 schema 校验阶段就硬拒绝通配地址:
213
+ // host "0.0.0.0" is an unspecified (wildcard) address, which is not
214
+ // supported: binding every interface would expose remote code
215
+ // execution to the network; bind one concrete IPv4 or IPv6 address
216
+ // of a local interface instead
217
+ //
218
+ // 后果是 **DSH 启动崩溃**(webserver 是 required 插件,它挂了
219
+ // `webServer` 服务就缺失,至少 6 个插件排队等它)。v0.9.0 曾把这个
220
+ // 建议写进包内 patch,实测产生 10 份 crash 日志。
221
+ //
222
+ // 正确做法:绑**一个具体的本机接口地址**。推荐 Tailscale IP ——
223
+ // 它固定不变、跨网可达,正好是本插件的使用场景。
224
+ fix: '把 profile 的 cordis.patch.yml 里 webserver 的 host 改成'
225
+ + '**一个具体的本机 IP**(不能用 0.0.0.0 —— 新版 DSH 会拒绝并启动失败),'
226
+ + '然后**重启 DSH**。\n'
227
+ + ' 查地址:Tailscale 用 `tailscale ip -4`,局域网用 `ipconfig`。\n'
228
+ + ' 推荐绑 Tailscale IP(固定不变、跨网可达):\n'
229
+ + ' - id: webserver\n'
230
+ + ' config:\n'
231
+ + " host: '<你的具体 IP,例如 100.x.x.x>'\n"
232
+ + ' port: 19387',
214
233
  detail: loop
215
234
  ? `当前只监听 ${reachability.bound || '127.0.0.1'}(${reachability.origin || ''})—— 副机用任何地址都连不上`
216
235
  : `当前监听 ${reachability.origin || ''}`,
package/lib/hub/index.js CHANGED
@@ -305,14 +305,17 @@ export function applyHub(ctx, config = {}) {
305
305
  origin,
306
306
  loopback,
307
307
  hint: loopback
308
- ? "webServer 绑在 loopback,副机无法接入。在 profile 的 cordis.patch.yml 里给 webserver 行加 host: '0.0.0.0'(保留 port),然后重启 DSH。"
308
+ // ⚠ 不能建议 0.0.0.0(2026-10-18 事故):新版 dsh-host-webserver 在
309
+ // schema 校验阶段就拒绝通配地址 → webserver 无法激活 → webServer
310
+ // 服务缺失 → DSH 启动崩溃。必须绑一个**具体的本机接口地址**。
311
+ ? 'webServer 绑在 loopback,副机无法接入。在 profile 的 cordis.patch.yml 里把 webserver 的 host 改成一个具体的本机 IP(推荐 Tailscale IP:tailscale ip -4),然后重启 DSH。不能用 0.0.0.0 —— 新版 DSH 会拒绝并启动失败。'
309
312
  : '',
310
313
  }
311
314
  if (loopback) {
312
315
  log('warn', 'hub 绑在 loopback:副机将无法接入', {
313
316
  origin,
314
317
  bound: hostname,
315
- fix: "给 webserver 加 host: '0.0.0.0'",
318
+ fix: "把 webserver 的 host 改成具体的本机 IP(如 Tailscale IP),不能用 0.0.0.0",
316
319
  })
317
320
  } else {
318
321
  log('info', 'hub reachable from other hosts', { origin, bound: hostname })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-agent",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "type": "module",
5
5
  "description": "把另一台电脑变成你 DSH 的远程 Agent 同事:副机本地持久跑任务、独立 Agent 循环,只回传 handoff 摘要与产物清单,成品按需拉取。",
6
6
  "license": "MIT",
@@ -929,37 +929,55 @@ check('65', 'eslint 严格档:lint script + 配置文件 + CI job 三件套齐
929
929
  'lib/env-check.js(可达性/inject/roots/role + fix + 不代下载边界)')
930
930
  }
931
931
 
932
- /* ── 79~80. 开箱即用(2026-10-17)──
932
+ /* ── 79~80. 开箱即用(2026-10-17,2026-10-18 修订)──
933
933
  *
934
934
  * 产品标准:用户装完就该能用,不该读文档、改 YAML、猜字段。
935
935
  *
936
- * 此前三道门槛:
937
- * 1. `role` 默认 '' → 两台机器都要操作一次
938
- * 2. `webserver` 没绑 0.0.0.0 → 副机永远连不上(且完全静默)
939
- * 3. 副机表单 4 个字段 → 小白不知道节点 ID / 工作区填什么
940
- *
941
936
  * 核心设计:`role` 默认 **auto**,按 `hubUrl` 推断。
942
937
  * · 默认 hub → 副机装完以 hub 身份启动,注册无鉴权端点(**攻击面**)
943
938
  * · 默认 '' → 两台都要操作,不是开箱即用
944
939
  * · 默认 auto → 主机零操作、副机贴完配对码零操作,且不给副机开错端点
940
+ *
941
+ * ⚠ **2026-10-18 事故修订**:79 号断言原来**要求**包内 patch 绑
942
+ * `host: '0.0.0.0'` —— 那正是导致 **DSH 启动崩溃**的写法。新版
943
+ * `dsh-host-webserver` 在 schema 校验阶段就硬拒绝通配地址:
944
+ *
945
+ * host "0.0.0.0" is an unspecified (wildcard) address, which is not
946
+ * supported: binding every interface would expose remote code execution
947
+ * to the network; bind one concrete IPv4 or IPv6 address ...
948
+ *
949
+ * 后果:webserver(required)激活失败 → webServer 服务缺失 → 至少 6 个插件
950
+ * 排队等它 → host 启动失败(实测 10 份 crash 日志)。
951
+ *
952
+ * 所以断言方向**反过来**:现在要求包内 patch **不碰** webserver 的任何字段,
953
+ * 且所有提示文案都不得建议 0.0.0.0。
945
954
  */
946
955
  {
947
956
  const indexSrc = read('lib/index.js')
948
957
  const patchSrc = read('cordis.patch.yml')
949
958
  const clientSrc3 = read('lib/client.js')
950
959
  const setupSrc = read('lib/setup-routes.js')
951
-
952
- check('79', '开箱即用:role 默认 auto + 包内 patch 绑 0.0.0.0:19387',
960
+ const envSrc = read('lib/env-check.js')
961
+ const hubSrc = read('lib/hub/index.js')
962
+
963
+ // ⚠ 只扫**非注释行**:注释里必须保留那次事故的原文(含 `0.0.0.0` 与
964
+ // `- id: webserver` 示例),那是给后来者的警示 —— 不该被断言判违规。
965
+ // 这一点和 L3 断言踩过的坑同源(扫全文会命中"正确留档的注释")。
966
+ const patchCode = patchSrc
967
+ .split('\n')
968
+ .filter((l) => !/^\s*#/.test(l))
969
+ .join('\n')
970
+
971
+ check('79', '开箱即用:role 默认 auto;且包内 patch **不得**绑 0.0.0.0(会让 DSH 崩溃)',
953
972
  /\.default\('auto'\)/.test(indexSrc)
954
973
  && /ROLE\.AUTO/.test(indexSrc)
955
974
  && /config\.hubUrl/.test(indexSrc)
956
- // 包内 patch 必须 override webserver(否则副机连不上)
957
- && /^- id: webserver$/m.test(patchSrc)
958
- && /host:\s*'0\.0\.0\.0'/.test(patchSrc)
959
- && /port:\s*19387/.test(patchSrc)
960
- // 必须披露安全影响
961
- && /暴露/.test(patchSrc),
962
- 'lib/index.js(auto 推断)+ cordis.patch.yml(webserver 0.0.0.0:19387 + 安全披露)')
975
+ // ⚠ 关键:包内 patch 的**生效行**不得 override webserver 的 host
976
+ && !/^- id: webserver$/m.test(patchCode)
977
+ && !/host:\s*'0\.0\.0\.0'/.test(patchCode)
978
+ // 必须把这个坑写进注释(防止后来者又"帮用户改好")
979
+ && /启动崩溃/.test(patchSrc),
980
+ 'lib/index.js(auto 推断)+ cordis.patch.yml(生效行不碰 webserver + 事故留档)')
963
981
 
964
982
  check('80', '开箱即用:副机表单简化 + 工作区自动创建 + 首次自动体检',
965
983
  // 工作区预填
@@ -972,6 +990,19 @@ check('65', 'eslint 严格档:lint script + 配置文件 + CI job 三件套齐
972
990
  && /mkdirSync\(r, \{ recursive: true \}\)/.test(setupSrc)
973
991
  && /warnings: applyWarnings/.test(setupSrc),
974
992
  'lib/client.js(预填/折叠/自动体检)+ lib/setup-routes.js(自动建目录)')
993
+
994
+ // ── 79b:**所有面向用户的修法文案**都不得建议 0.0.0.0 ──────────────
995
+ //
996
+ // 这是本次事故最有价值的一条守卫:代码自己不能犯那个错,提示文案也不能。
997
+ // 检查的是字符串字面量(`host: '0.0.0.0'` 这种形态),不是注释里的说明。
998
+ check('79b', '所有修法文案都不得建议 host 0.0.0.0(会让 DSH 启动崩溃)',
999
+ !/host:\s*'0\.0\.0\.0'/.test(clientSrc3)
1000
+ && !/host:\s*'0\.0\.0\.0'/.test(envSrc)
1001
+ && !/加 host:\s*'0\.0\.0\.0'/.test(hubSrc)
1002
+ // 但要**明确警告不能用**它(否则用户可能自己想到这个"标准做法")
1003
+ && /不能用 0\.0\.0\.0/.test(envSrc)
1004
+ && /不能用 0\.0\.0\.0/.test(clientSrc3),
1005
+ 'lib/client.js + lib/env-check.js + lib/hub/index.js(修法不含 0.0.0.0,且有明确警告)')
975
1006
  }
976
1007
 
977
1008
  /* ── 81~82. 工作区 SMB 共享(2026-10-17)──
@@ -0,0 +1,148 @@
1
+ # dsh-remote-agent worker diagnostic
2
+ #
3
+ # Read-only: changes nothing. Paste into PowerShell on the WORKER machine:
4
+ # powershell -ExecutionPolicy Bypass -File .\diag-worker.ps1
5
+
6
+ $ErrorActionPreference = 'SilentlyContinue'
7
+ $profiles = Join-Path $env:USERPROFILE '.dsh\profiles'
8
+
9
+ function Line { param($t) Write-Host $t }
10
+
11
+ Write-Host ''
12
+ Write-Host '=========================================='
13
+ Write-Host ' dsh-remote-agent WORKER DIAGNOSTIC'
14
+ Write-Host '=========================================='
15
+ Write-Host ''
16
+
17
+ # ---- 1. profile dirs ----
18
+ Write-Host '[1] DSH profile directories'
19
+ if (Test-Path $profiles) {
20
+ Get-ChildItem $profiles -Directory | ForEach-Object {
21
+ $patch = Join-Path $_.FullName 'cordis.patch.yml'
22
+ if (Test-Path $patch) {
23
+ $f = Get-Item $patch
24
+ Line (" {0} patch={1}B mtime={2}" -f $_.Name, $f.Length, $f.LastWriteTime.ToString('MM-dd HH:mm:ss'))
25
+ } else {
26
+ Line (" {0} (no cordis.patch.yml)" -f $_.Name)
27
+ }
28
+ }
29
+ if ($env:DSH_PROFILE) {
30
+ Line (" env DSH_PROFILE = " + $env:DSH_PROFILE)
31
+ } else {
32
+ Line ' env DSH_PROFILE = (not set -> code defaults to "desktop")'
33
+ }
34
+ } else {
35
+ Line ' ERROR: no profile directory'
36
+ }
37
+ Write-Host ''
38
+
39
+ # ---- 2. remote-agent config per profile ----
40
+ Write-Host '[2] remote-agent config in each profile'
41
+ if (Test-Path $profiles) {
42
+ Get-ChildItem $profiles -Directory | ForEach-Object {
43
+ $patch = Join-Path $_.FullName 'cordis.patch.yml'
44
+ if (-not (Test-Path $patch)) { return }
45
+ $c = Get-Content $patch -Raw
46
+ if ($c -notmatch 'remote-agent') { return }
47
+
48
+ $roleM = [regex]::Match($c, '(?m)^\s*role:\s*(\S+)')
49
+ $injectM = [regex]::Match($c, '(?m)^\s*inject:\s*(.+)$')
50
+ $hubM = [regex]::Match($c, '(?m)^\s*hubUrl:\s*(\S+)')
51
+ $wsM = [regex]::Match($c, '(?ms)- id:\s*webserver.*?host:\s*(\S+)')
52
+ $tokM = [regex]::Match($c, '(?m)^\s*nodeToken:\s*(\S+)')
53
+
54
+ Line (' --- ' + $_.Name + ' ---')
55
+ if ($roleM.Success) { Line (' role = ' + $roleM.Groups[1].Value) } else { Line ' role = (NOT SET -> package default)' }
56
+ if ($injectM.Success) { Line (' inject = ' + $injectM.Groups[1].Value.Trim()) } else { Line ' inject = (not set)' }
57
+ if ($hubM.Success) { Line (' hubUrl = ' + $hubM.Groups[1].Value) } else { Line ' hubUrl = (not set)' }
58
+ if ($wsM.Success) { Line (' webserver = host=' + $wsM.Groups[1].Value) } else { Line ' webserver = (no webserver section)' }
59
+ if ($tokM.Success) { Line ' nodeToken = (set)' } else { Line ' nodeToken = (not set)' }
60
+ }
61
+ } else {
62
+ Line ' (skipped)'
63
+ }
64
+ Write-Host ''
65
+
66
+ # ---- 3. installed plugin version ----
67
+ Write-Host '[3] installed dsh-remote-agent version'
68
+ $found = $false
69
+ if (Test-Path $profiles) {
70
+ Get-ChildItem $profiles -Directory | ForEach-Object {
71
+ $nm = Join-Path $_.FullName 'node_modules\dsh-remote-agent'
72
+ $pj = Join-Path $nm 'package.json'
73
+ if (Test-Path $pj) {
74
+ $script:found = $true
75
+ $v = (Get-Content $pj -Raw | ConvertFrom-Json).version
76
+ $hasEnvCheck = Test-Path (Join-Path $nm 'lib\env-check.js')
77
+ $hasShare = Test-Path (Join-Path $nm 'lib\node\share-service.js')
78
+ $pkgPatch = Join-Path $nm 'cordis.patch.yml'
79
+ $pkgRole = ''
80
+ $hasWs = $false
81
+ if (Test-Path $pkgPatch) {
82
+ $pc = Get-Content $pkgPatch -Raw
83
+ $m = [regex]::Match($pc, '(?m)^\s*role:\s*(\S+)')
84
+ if ($m.Success) { $pkgRole = $m.Groups[1].Value }
85
+ $hasWs = $pc -match '(?m)^- id:\s*webserver'
86
+ }
87
+ Line (' ' + $_.Name + ': version ' + $v)
88
+ Line (' has lib/env-check.js (0.9+) : ' + $hasEnvCheck)
89
+ Line (' has lib/node/share-service.js : ' + $hasShare)
90
+ if ($pkgRole -ne '') { Line (' package default role : ' + $pkgRole) } else { Line ' package default role : (not found)' }
91
+ Line (' package has webserver binding : ' + $hasWs)
92
+ if (-not $hasEnvCheck) {
93
+ Line ' WARNING: this is pre-0.9, no "works out of the box"'
94
+ }
95
+ }
96
+ }
97
+ }
98
+ if (-not $script:found) {
99
+ Line ' ERROR: dsh-remote-agent not found under any profile'
100
+ }
101
+ Write-Host ''
102
+
103
+ # ---- 4. panel endpoints ----
104
+ Write-Host '[4] panel endpoint probe (is the plugin loaded at all?)'
105
+ $ports = @()
106
+ $dshPids = (Get-Process 'DeepSeek Harness').Id
107
+ if ($dshPids) {
108
+ $ports += (Get-NetTCPConnection -State Listen | Where-Object { $dshPids -contains $_.OwningProcess } | ForEach-Object { $_.LocalPort })
109
+ }
110
+ $ports += 19387
111
+ $ports = $ports | Sort-Object -Unique
112
+ foreach ($port in $ports) {
113
+ foreach ($path in @('/api/remote-agent/setup/state', '/api/remote-agent/status')) {
114
+ $url = 'http://127.0.0.1:' + $port + $path
115
+ try {
116
+ $r = Invoke-WebRequest $url -UseBasicParsing -TimeoutSec 3
117
+ $body = $r.Content
118
+ if ($body.Length -gt 240) { $body = $body.Substring(0, 240) + '...' }
119
+ Line (' [' + $port + '] ' + $path + ' -> HTTP ' + $r.StatusCode)
120
+ Line (' ' + $body)
121
+ } catch {
122
+ $code = $_.Exception.Response.StatusCode.value__
123
+ if ($code) {
124
+ Line (' [' + $port + '] ' + $path + ' -> HTTP ' + $code)
125
+ } else {
126
+ Line (' [' + $port + '] ' + $path + ' -> no listener')
127
+ }
128
+ }
129
+ }
130
+ }
131
+ Write-Host ''
132
+
133
+ # ---- 5. recent backups ----
134
+ Write-Host '[5] recent config backups (was the file rewritten?)'
135
+ if (Test-Path $profiles) {
136
+ Get-ChildItem $profiles -Recurse -File -Filter 'cordis.patch.yml*' |
137
+ Sort-Object LastWriteTime -Descending |
138
+ Select-Object -First 8 |
139
+ ForEach-Object {
140
+ Line (' ' + $_.LastWriteTime.ToString('MM-dd HH:mm:ss') + ' ' + $_.Length + 'B ' + $_.Name)
141
+ }
142
+ }
143
+ Write-Host ''
144
+
145
+ Write-Host '=========================================='
146
+ Write-Host ' END. Paste the whole output back.'
147
+ Write-Host '=========================================='
148
+ Write-Host ''