dsh-remote-agent 0.9.1 → 0.9.3
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 +102 -0
- package/README.en.md +10 -3
- package/README.md +37 -10
- package/cordis.patch.yml +31 -24
- package/lib/client.js +9 -2
- package/lib/env-check.js +24 -5
- package/lib/hub/index.js +150 -3
- package/lib/index.js +24 -0
- package/lib/instance-config.js +207 -0
- package/lib/listen-server.js +262 -0
- package/lib/setup-routes.js +48 -0
- package/package.json +1 -1
- package/scripts/check-doc-drift.mjs +85 -15
- package/scripts/diag-worker.ps1 +148 -0
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
|
|
48
|
-
>
|
|
49
|
-
>
|
|
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`
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
#
|
|
102
|
+
# ⚠⚠ 绝对不要覆盖 webserver 的 host(2026-10-18 事故:我犯过这个错)
|
|
103
103
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
104
104
|
#
|
|
105
|
-
# ──
|
|
105
|
+
# ── 事故经过 ────────────────────────────────────────────────────────────
|
|
106
106
|
#
|
|
107
|
-
#
|
|
108
|
-
# 而那个 WebSocket upgrade 是注册在 DSH 的 `ctx.webServer` 上的
|
|
109
|
-
# (`ctx.webServer.registerUpgrade(...)`)—— **没有独立端口选项**。
|
|
107
|
+
# v0.9.0 为了让"副机连得上",我在包内 patch 里加了:
|
|
110
108
|
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
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
|
-
#
|
|
115
|
-
# 毫无异常(healthz 200、面板能开、工具都在)。用户只能猜"地址错了?
|
|
116
|
-
# token 错了?防火墙?" —— 2026-10-17 实测踩到,代价是好几轮排查。
|
|
126
|
+
# 这个错误比它想修的"副机连不上"严重得多:**连 DSH 都起不来了**。
|
|
117
127
|
#
|
|
118
|
-
# ──
|
|
128
|
+
# ── 所以现在的做法 ──────────────────────────────────────────────────────
|
|
119
129
|
#
|
|
120
|
-
#
|
|
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
|
-
#
|
|
130
|
-
#
|
|
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'), '
|
|
1740
|
-
'
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
@@ -16,6 +16,9 @@ import os from 'node:os'
|
|
|
16
16
|
import path from 'node:path'
|
|
17
17
|
import { ERROR_CODE, PROTOCOL_VERSION } from '../protocol.js'
|
|
18
18
|
import { acceptUpgrade } from '../ws.js'
|
|
19
|
+
// 独立监听端口(2026-10-18):副机拨入不再依赖 DSH 的 webserver 配置
|
|
20
|
+
// (DSH 每次启动重写 profile patch,会把 webserver 段冲掉)。
|
|
21
|
+
import { DEFAULT_LISTEN_PORT, resolveListenHost, startListenServer } from '../listen-server.js'
|
|
19
22
|
import { NodeRegistry } from './node-registry.js'
|
|
20
23
|
import { RelayLink } from './relay-link.js'
|
|
21
24
|
import { createPluginLogger } from './hub-util.js'
|
|
@@ -43,6 +46,20 @@ export function normalizeConfig(config = {}) {
|
|
|
43
46
|
return {
|
|
44
47
|
nodePath: typeof c.nodePath === 'string' && c.nodePath !== '' ? c.nodePath : NODE_UPGRADE_PATH,
|
|
45
48
|
healthPath: typeof c.healthPath === 'string' && c.healthPath !== '' ? c.healthPath : HEALTH_PATH,
|
|
49
|
+
// ── 独立监听端口(2026-10-18)──────────────────────────────────────
|
|
50
|
+
//
|
|
51
|
+
// 副机拨入不再依赖 DSH 的 webserver(DSH 每次启动重写 profile patch,
|
|
52
|
+
// 会把 webserver 段冲掉;而且新版硬拒绝 0.0.0.0)。插件自己开端口。
|
|
53
|
+
//
|
|
54
|
+
// listenHost:留空 = **自动选**(Tailscale IP > 私网 IP > 回环)。
|
|
55
|
+
// 显式填了就按填的绑(必须是具体 IP,不能 0.0.0.0 —— 与 DSH 同规则)。
|
|
56
|
+
// listenPort:默认 19387(与历史配置、配对码、文档一致)。
|
|
57
|
+
listenHost: typeof c.listenHost === 'string' ? c.listenHost.trim() : '',
|
|
58
|
+
// listenPort:默认 19387(与历史配置、配对码、文档一致)。
|
|
59
|
+
// **0 = 显式关闭独立监听**(测试 / 只走 relay / 已在别处暴露端点时用)。
|
|
60
|
+
listenPort: Number.isFinite(c.listenPort) && c.listenPort >= 0 && c.listenPort < 65536
|
|
61
|
+
? c.listenPort
|
|
62
|
+
: DEFAULT_LISTEN_PORT,
|
|
46
63
|
nodeToken: typeof c.nodeToken === 'string' ? c.nodeToken : '',
|
|
47
64
|
systemPrompt: c.systemPrompt !== false,
|
|
48
65
|
// F1-1:healthz 是否暴露计数。默认 true(便于运维自检);
|
|
@@ -305,14 +322,17 @@ export function applyHub(ctx, config = {}) {
|
|
|
305
322
|
origin,
|
|
306
323
|
loopback,
|
|
307
324
|
hint: loopback
|
|
308
|
-
|
|
325
|
+
// ⚠ 不能建议 0.0.0.0(2026-10-18 事故):新版 dsh-host-webserver 在
|
|
326
|
+
// schema 校验阶段就拒绝通配地址 → webserver 无法激活 → webServer
|
|
327
|
+
// 服务缺失 → DSH 启动崩溃。必须绑一个**具体的本机接口地址**。
|
|
328
|
+
? 'webServer 绑在 loopback,副机无法接入。在 profile 的 cordis.patch.yml 里把 webserver 的 host 改成一个具体的本机 IP(推荐 Tailscale IP:tailscale ip -4),然后重启 DSH。不能用 0.0.0.0 —— 新版 DSH 会拒绝并启动失败。'
|
|
309
329
|
: '',
|
|
310
330
|
}
|
|
311
331
|
if (loopback) {
|
|
312
332
|
log('warn', 'hub 绑在 loopback:副机将无法接入', {
|
|
313
333
|
origin,
|
|
314
334
|
bound: hostname,
|
|
315
|
-
fix: "
|
|
335
|
+
fix: "把 webserver 的 host 改成具体的本机 IP(如 Tailscale IP),不能用 0.0.0.0",
|
|
316
336
|
})
|
|
317
337
|
} else {
|
|
318
338
|
log('info', 'hub reachable from other hosts', { origin, bound: hostname })
|
|
@@ -380,7 +400,134 @@ export function applyHub(ctx, config = {}) {
|
|
|
380
400
|
}, 'remote-agent: relay-link')
|
|
381
401
|
}
|
|
382
402
|
|
|
383
|
-
// ──
|
|
403
|
+
// ── 独立监听端口(2026-10-18,**副机拨入的主通道**)───────────────────
|
|
404
|
+
//
|
|
405
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
406
|
+
// 为什么不再依赖 ctx.webServer
|
|
407
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
408
|
+
//
|
|
409
|
+
// 原先副机端点注册在 `ctx.webServer.registerUpgrade` 上,于是副机必须能访问
|
|
410
|
+
// **DSH 自己的 webserver 端口**。这带来两个绕不开的死结:
|
|
411
|
+
//
|
|
412
|
+
// 1. **DSH 每次启动都重写 `<profile>/cordis.patch.yml`**(实测:写进去的
|
|
413
|
+
// `webserver` 段在下次启动时被删掉,只剩 3 个 `ui-*` 条目)→ 绑回
|
|
414
|
+
// 127.0.0.1 → 副机永远连不上,而且主机侧毫无异常。
|
|
415
|
+
// 2. `webServer` 服务**激活即 listen**,没有 rebind 方法 → 插件运行时改不了。
|
|
416
|
+
//
|
|
417
|
+
// 而且新版 `dsh-host-webserver` 硬拒绝 `0.0.0.0`(直接让 DSH 启动崩溃,
|
|
418
|
+
// 我们踩过:10 份 crash 日志)。所以"帮用户把 webserver 改好"这条路是死的。
|
|
419
|
+
//
|
|
420
|
+
// ── 现在的做法 ──────────────────────────────────────────────────────────
|
|
421
|
+
//
|
|
422
|
+
// 插件自己用 `node:http` 起一个只服务节点端点的小 server(见
|
|
423
|
+
// lib/listen-server.js),**完全不依赖 DSH 的 webserver 配置**。
|
|
424
|
+
//
|
|
425
|
+
// 绑定地址自动选:配置的 listenHost > Tailscale IP > 私网 IP > 回环。
|
|
426
|
+
// 端口默认 19387(与历史配置、配对码、文档一致)。
|
|
427
|
+
//
|
|
428
|
+
// ⚠ 这是**新增的主通道**,不是替换:下面 webServer 的 registerUpgrade 仍然
|
|
429
|
+
// 保留 —— 老配置(webserver 恰好绑对了)的机器继续用那条路径,两条通道
|
|
430
|
+
// 并存不冲突(不同端口/不同 socket)。
|
|
431
|
+
let listenHandle
|
|
432
|
+
__reg(() => {
|
|
433
|
+
const listenHost = typeof cfg.listenHost === 'string' ? cfg.listenHost : ''
|
|
434
|
+
const port = Number.isFinite(cfg.listenPort) ? cfg.listenPort : DEFAULT_LISTEN_PORT
|
|
435
|
+
|
|
436
|
+
// ── `listenPort: 0` = 显式关闭独立监听(2026-10-18)────────────────
|
|
437
|
+
//
|
|
438
|
+
// 这个开关是**为测试与特殊部署**准备的:
|
|
439
|
+
// · 单测/e2e 里 ctx 是 mock,本模块会真的开端口 → 监听句柄不释放
|
|
440
|
+
// → 测试进程挂住(实测:hub-worker-e2e 90 秒不退出)。
|
|
441
|
+
// · 某些部署场景(只走 relay、或已在别处暴露端点)不需要这个端口。
|
|
442
|
+
//
|
|
443
|
+
// 另外**自动检测测试环境**:测试不该真的占用 19387(并行跑多个测试文件
|
|
444
|
+
// 时还会互相抢端口,而且监听句柄不释放会让 `npm test` 挂住)。
|
|
445
|
+
//
|
|
446
|
+
// ⚠ 检测方式是 `NODE_TEST_CONTEXT`(node --test 会设为 'child-v8' 等),
|
|
447
|
+
// **不是** `process.argv` —— 实测 `node --test` 子进程的 argv 里没有
|
|
448
|
+
// `--test`(那是父进程的参数),第一版用 argv 判断所以没生效。
|
|
449
|
+
//
|
|
450
|
+
// 显式传非默认 listenPort 时以显式为准(有测试专门验证监听行为时要用)。
|
|
451
|
+
const isUnderTest = typeof process.env.NODE_TEST_CONTEXT === 'string'
|
|
452
|
+
&& process.env.NODE_TEST_CONTEXT !== ''
|
|
453
|
+
const explicitPort = Number.isFinite(cfg.listenPort) && cfg.listenPort !== DEFAULT_LISTEN_PORT
|
|
454
|
+
if (port === 0 || (isUnderTest && !explicitPort)) {
|
|
455
|
+
log('info', 'independent listener disabled', {
|
|
456
|
+
reason: port === 0 ? 'listenPort: 0' : 'test environment (pass explicit listenPort to enable)',
|
|
457
|
+
})
|
|
458
|
+
return once(() => {})
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const picked = resolveListenHost({ listenHost })
|
|
462
|
+
let disposed = false
|
|
463
|
+
|
|
464
|
+
startListenServer({
|
|
465
|
+
host: picked.host,
|
|
466
|
+
port,
|
|
467
|
+
path: cfg.nodePath,
|
|
468
|
+
healthPath: cfg.healthPath,
|
|
469
|
+
healthBody: {
|
|
470
|
+
ok: true,
|
|
471
|
+
plugin: 'dsh-remote-agent',
|
|
472
|
+
role: 'hub',
|
|
473
|
+
protocol: PROTOCOL_VERSION,
|
|
474
|
+
via: 'listen-server',
|
|
475
|
+
},
|
|
476
|
+
onUpgrade: (conn, req) => {
|
|
477
|
+
// 与 webServer 那条路完全相同的鉴权与交接逻辑(一处不能少:
|
|
478
|
+
// token 校验是这条通道唯一的口令,跳过就是一个无鉴权入口)。
|
|
479
|
+
if (cfg.nodeToken !== '') {
|
|
480
|
+
const token = req.headers['x-dsh-token']
|
|
481
|
+
if (!safeEqual(token, cfg.nodeToken)) {
|
|
482
|
+
log('warn', 'node upgrade rejected: bad token (listen-server)', { remote: remoteAddress(req) })
|
|
483
|
+
try {
|
|
484
|
+
conn.close(1008, 'bad token')
|
|
485
|
+
} catch {
|
|
486
|
+
/* 已关 */
|
|
487
|
+
}
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
registry.attachDirect(conn, { remote: remoteAddress(req) })
|
|
492
|
+
},
|
|
493
|
+
log,
|
|
494
|
+
})
|
|
495
|
+
.then((h) => {
|
|
496
|
+
if (disposed) {
|
|
497
|
+
// 竞态:disposer 先跑了(插件卸载/HMR),把刚起来的 server 关掉
|
|
498
|
+
void h.close()
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
listenHandle = h
|
|
502
|
+
log('info', 'node endpoint ready (independent listener)', {
|
|
503
|
+
host: h.host,
|
|
504
|
+
port: h.port,
|
|
505
|
+
source: picked.source,
|
|
506
|
+
path: cfg.nodePath,
|
|
507
|
+
})
|
|
508
|
+
})
|
|
509
|
+
.catch((err) => {
|
|
510
|
+
// ⚠ 监听失败不能静默:那会让"副机连不上"变成又一个无头案。
|
|
511
|
+
// 但要**降级而非抛出** —— 本插件不该把整个 DSH 拖垮
|
|
512
|
+
// (webServer 那条通道可能仍然可用)。
|
|
513
|
+
log('error', 'independent listener failed; falling back to webServer-only', {
|
|
514
|
+
host: picked.host,
|
|
515
|
+
port,
|
|
516
|
+
error: String(err?.message ?? err),
|
|
517
|
+
hint: '端口被占用或该地址不可用。可在插件配置里改 listenPort / listenHost。',
|
|
518
|
+
})
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
return once(() => {
|
|
522
|
+
disposed = true
|
|
523
|
+
if (listenHandle) {
|
|
524
|
+
void listenHandle.close()
|
|
525
|
+
listenHandle = undefined
|
|
526
|
+
}
|
|
527
|
+
})
|
|
528
|
+
}, 'remote-agent: independent-listener')
|
|
529
|
+
|
|
530
|
+
// ── 直连 upgrade 路由(保留:webServer 路径恰好可用的场景)─────────────
|
|
384
531
|
__reg(() => {
|
|
385
532
|
const dispose = ctx.webServer.registerUpgrade({
|
|
386
533
|
path: cfg.nodePath,
|
package/lib/index.js
CHANGED
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
import z from '@deepseek-ai/schemastery'
|
|
23
23
|
import { applyHub, newNodeToken, normalizeConfig, NODE_UPGRADE_PATH, HEALTH_PATH } from './hub/index.js'
|
|
24
24
|
import { registerSetupRoutes } from './setup-routes.js'
|
|
25
|
+
// 配置分层(2026-10-18):DSH 的插件管理器会重写 cordis.patch.yml、冲掉手工写的
|
|
26
|
+
// 段(实测把我们的 role + webserver 整段删掉)。所以真正的配置另存一份在
|
|
27
|
+
// <stateDir>/instance.json,加载时覆盖回来。见 lib/instance-config.js 头注释。
|
|
28
|
+
import { mergeInstanceConfig } from './instance-config.js'
|
|
25
29
|
|
|
26
30
|
/** 角色取值。 */
|
|
27
31
|
export const ROLE = Object.freeze({ HUB: 'hub', WORKER: 'worker', BOTH: 'both', AUTO: 'auto' })
|
|
@@ -140,6 +144,26 @@ async function applyWorkerBranch(ctx, config, log) {
|
|
|
140
144
|
* @param {object} [config] 行配置(profiles/*.cordis.yml 里的 config)
|
|
141
145
|
*/
|
|
142
146
|
export function apply(ctx, config = {}) {
|
|
147
|
+
// ── 配置分层合并(2026-10-18,解决"配置被 DSH 重写冲掉")──────────────
|
|
148
|
+
//
|
|
149
|
+
// 实测事故:用户在 DSH 里卸载重装插件后,`cordis.patch.yml` 从 3962B/15 条
|
|
150
|
+
// 被重写成 696B/4 条 —— **我们的 role 与 webserver 段整段消失**。DSH 的插件
|
|
151
|
+
// 管理器改依赖时会重写那个文件,只保留它自己管理的条目。
|
|
152
|
+
//
|
|
153
|
+
// 结果就是"每次设置完重启又回引导页":写进去的 role 被外力冲掉,重启后
|
|
154
|
+
// 只剩包内默认(role: auto)。
|
|
155
|
+
//
|
|
156
|
+
// 所以配置分两层:
|
|
157
|
+
// 1. `cordis.patch.yml` 的 `- id: remote-agent` —— 只负责让 loader 加载
|
|
158
|
+
// 2. `<stateDir>/instance.json` —— 存真正的配置,**DSH 冲不掉**
|
|
159
|
+
//
|
|
160
|
+
// 这里把第 2 层合并进来,且**独立配置优先**(那是用户明确设置过的值)。
|
|
161
|
+
// 读失败/损坏时安静退回 loader config(配置读不出来不该让 DSH 起不来)。
|
|
162
|
+
const merged = mergeInstanceConfig(config)
|
|
163
|
+
if (merged.source) {
|
|
164
|
+
config = merged.config
|
|
165
|
+
}
|
|
166
|
+
|
|
143
167
|
const { role, invalid, unconfigured } = resolveRoleDetail(config)
|
|
144
168
|
const log = loggerOf(ctx)
|
|
145
169
|
// 配置写错必须有反馈。注意**不再回退 hub** —— 那会静默开一个
|