dsh-remote-agent 0.9.0 → 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 +211 -0
- package/README.en.md +12 -5
- package/README.md +39 -12
- package/bin/dsh-remote-setup.js +75 -2
- package/cordis.patch.yml +31 -24
- package/lib/client.js +117 -11
- package/lib/env-check.js +99 -7
- package/lib/hub/index.js +5 -2
- package/lib/hub/registry-ingest.js +21 -2
- package/lib/hub/templates.js +5 -2
- package/lib/hub/ui-routes.js +14 -1
- package/lib/node/share-service.js +43 -2
- package/package.json +1 -1
- package/scripts/check-doc-drift.mjs +92 -15
- package/scripts/diag-worker.ps1 +148 -0
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,217 @@
|
|
|
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
|
+
|
|
114
|
+
## [0.9.1] - 2026-10-17
|
|
115
|
+
|
|
116
|
+
> **0.9.0 发布约 40 分钟后补发**。两个 High 都是真实功能失效
|
|
117
|
+
> (体检对新装用户全面误报、SMB 共享完全不可用),趁用户数还少尽快修正。
|
|
118
|
+
|
|
119
|
+
### 修复(High):环境体检对默认安装全面误报,且五项检查静默跳过
|
|
120
|
+
|
|
121
|
+
`/env-check` 路由传的是**原始 `config.role`**,而 0.9.0 起默认安装的生效配置
|
|
122
|
+
就是 `role: 'auto'`(`Config` 的 `.default('auto')` + 包内 patch 显式写入)。
|
|
123
|
+
`envChecks` 的判据 `isHub = role === 'hub' || role === 'both'` 拿到未解析的
|
|
124
|
+
`'auto'` 判 false,于是:
|
|
125
|
+
|
|
126
|
+
- **reachability / port / firewall / tailscale / nodes 五项全部跳过**
|
|
127
|
+
—— 而这五项正是体检存在的理由(副机连不上的静默失败)
|
|
128
|
+
- `inject` 按 worker 三件套判定 → hub 正确的 inject 反被判"缺服务"
|
|
129
|
+
- `role` 检查误报未配置
|
|
130
|
+
|
|
131
|
+
实测(修复前):
|
|
132
|
+
|
|
133
|
+
role=auto → { ok:false, failed:2, items:[role(FAIL), inject(FAIL)] }
|
|
134
|
+
|
|
135
|
+
**影响放大**:9.0 的"首次自动体检"让这份"双误报 + 零诊断"在**每个新装用户
|
|
136
|
+
首次打开面板时自动弹出**。
|
|
137
|
+
|
|
138
|
+
修复(双保险):
|
|
139
|
+
|
|
140
|
+
1. 路由改传 `resolveRole(config) || 'hub'`(与 `setup-routes` 同一份实现)
|
|
141
|
+
2. `envChecks` **自己**也解析 —— 即使调用方传错 `'auto'` 也能推断
|
|
142
|
+
(判据:配置里写了 role 就以配置为准交给 `resolveRole`;没这个字段才用
|
|
143
|
+
调用方给的值。两个极端写法都会误报,已由 5 个场景用例覆盖)
|
|
144
|
+
|
|
145
|
+
实测(修复后):
|
|
146
|
+
|
|
147
|
+
role=auto → role ✓ / inject ✓ / reachability ✗(正确报出 loopback)/ nodes ✗
|
|
148
|
+
|
|
149
|
+
### 修复(High):SMB「生成共享命令」100% 失败
|
|
150
|
+
|
|
151
|
+
面板的 `doPlan` **恒定发送 `dir: ''`** —— 面板里根本没有选择目录的 UI,
|
|
152
|
+
而副机侧 `if (dir === '') reply ok:false 'plan 需要 dir'` 按设计拒绝。
|
|
153
|
+
于是工作区共享的**唯一主动作完全不可用**(probe 与后端 RPC 正常,API 直调可绕)。
|
|
154
|
+
|
|
155
|
+
修复:
|
|
156
|
+
|
|
157
|
+
- 加目录选择 UI,数据源是 `node.roots`(`nodeView` 一直透出它,那是副机的
|
|
158
|
+
安全边界、也正是该共享的目录),默认第一项
|
|
159
|
+
- 无 `roots` 时退回可手填的输入框 + 说明原因
|
|
160
|
+
- 换副机时重置候选与 plan/probe 结果(否则会显示别人的共享命令)
|
|
161
|
+
- `dir` 为空时禁用按钮(不给用户"点了才失败")
|
|
162
|
+
|
|
163
|
+
### 修复(Medium):CLI `--doctor` 与面板体检是两套并行实现
|
|
164
|
+
|
|
165
|
+
三处注释曾声称"命令行与面板共用 `lib/env-check.js` 的同一份逻辑" —— 那是失真的:
|
|
166
|
+
CLI 走 `setup.js` 的 `doctorChecks`(14 项静态),面板走 `envChecks`(8 项运行时态),
|
|
167
|
+
**检查项不相交**。CLI 有 Node 版本 / hubUrl / token 但**没有 loopback 诊断**
|
|
168
|
+
(本版本最重要的那条),面板有 reachability 但没有前者。
|
|
169
|
+
|
|
170
|
+
修复:两段都跑、结果合并。顺带修掉 CLI 把 role 写死成 `'hub'` 的同类 bug
|
|
171
|
+
(`grab('role') || 'hub'` 会让 auto-worker 被当 hub 体检)→ 改用 `resolveRole`。
|
|
172
|
+
|
|
173
|
+
实测新增输出:`✓ Web 服务绑在可达地址 —— 当前监听 http://0.0.0.0:19387`
|
|
174
|
+
|
|
175
|
+
### 修复(Medium):模型下拉的「自定义…」永远展不开文本框
|
|
176
|
+
|
|
177
|
+
原实现靠 `fv.model === '__custom__'` 判定,但表单里存的是**真实模型名**或空串,
|
|
178
|
+
从来不是字面量 `'__custom__'`:
|
|
179
|
+
|
|
180
|
+
- 从"跟随默认"(model='')选自定义 → 旧值仍为 '' → `isCustom=false` → 弹回
|
|
181
|
+
- 从任一有效选项选自定义 → 旧模型名被保留 → `isCustom=false` → 同样弹回
|
|
182
|
+
|
|
183
|
+
唯一能进 `isCustom` 的路径是"表单预填了清单外模型名"(先有鸡)。
|
|
184
|
+
|
|
185
|
+
修复:加独立布尔 state `modelCustom`,`isCustom = modelCustom || (model 非空
|
|
186
|
+
且不在清单里)`。
|
|
187
|
+
|
|
188
|
+
### 修复(Low ×5)
|
|
189
|
+
|
|
190
|
+
- **templates.js 注释残留旧重试参数**(~1.3s / 封顶 200ms)与常量
|
|
191
|
+
(16 次 / 500ms / ~6s)不符 → 改为引用常量名 + 指向事故史。
|
|
192
|
+
**这条最容易埋雷**:后来者可能照注释把窗口"修"回去。
|
|
193
|
+
- **`net share` 文本解析对含空格路径截断**(`D:\Deepseek Harness file`
|
|
194
|
+
→ `D:\Deepseek`)→ 改用 `Get-SmbShare | ConvertTo-Json` 取结构化路径,
|
|
195
|
+
`net share` 仅作回退。本项目自己的目录名就带空格,这个 bug 在本机即会触发。
|
|
196
|
+
- **`autoFix` 承诺了不存在的能力**(全仓库无 fix 路由 / 无 `--fix` 参数,
|
|
197
|
+
面板也没修复按钮)→ 语义改为"修法机械、照抄即可",文案全部改写,
|
|
198
|
+
reachability 项直接给**可粘贴的 YAML 片段**。
|
|
199
|
+
- **README 两处数字漂移**:状态行仍写 0.8.1、断言数写 117(实际 118)→ 修正中英两版。
|
|
200
|
+
- **hub ingest 无终态守卫**:晚期 `TASK_END`/`TASK_SUMMARY` 可把已置
|
|
201
|
+
`FAILED(node-unreachable)` 改写回真实终态(良性但破坏"终态即终态"心智模型)
|
|
202
|
+
→ 加 `TERMINAL_STATUS.has(t.status)` 守卫(同终态重发幂等放行)。
|
|
203
|
+
|
|
204
|
+
### 新增:装配级测试(10 条)
|
|
205
|
+
|
|
206
|
+
`test/assembly-wiring.test.js` —— 全部锚在**接线处**而非模块内部:
|
|
207
|
+
|
|
208
|
+
两个 High 的共同点是"**单测覆盖了模块本身,没人测模块被装配进系统时的形状**"。
|
|
209
|
+
`envChecks({role:'hub'})` 全绿证明不了路由传对了;`buildShareCommand` 全绿
|
|
210
|
+
证明不了面板真的发了 `dir`。
|
|
211
|
+
|
|
212
|
+
另加 doc-drift 断言 84/85 钉住这两条接线,并做了**负向验证**
|
|
213
|
+
(回退修复后断言正确报 DRIFT)。
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
packaging 4 项
|
|
217
|
+
doc-drift 120/120
|
|
218
|
+
diag:inject 11 项
|
|
219
|
+
tests 949(948 pass / 0 fail / 1 skip)
|
|
220
|
+
lint 干净
|
|
221
|
+
```
|
|
222
|
+
|
|
12
223
|
## [0.9.0] - 2026-10-17
|
|
13
224
|
|
|
14
225
|
### ⚠ 行为变更:exec 任务默认超时 1 小时 → 10 分钟
|
package/README.en.md
CHANGED
|
@@ -32,21 +32,28 @@ explicitly pull them.
|
|
|
32
32
|
|
|
33
33
|
| | |
|
|
34
34
|
|---|---|
|
|
35
|
-
| **Status** | 0.
|
|
35
|
+
| **Status** | 0.9.0 —— hub + worker end-to-end usable; both `exec` and `agent` paths verified on two real machines; CI covers Windows / macOS / Linux |
|
|
36
36
|
| **DSH** | Desktop / CLI, verified on 0.2.0-rc.2 |
|
|
37
37
|
| **Platforms** | Windows (fully verified) · Linux / macOS (CI covered) |
|
|
38
38
|
| **Dependencies** | **Zero external npm dependencies** —— `node:` built-ins only |
|
|
39
39
|
| **Node** | `^22.19.0 \|\| >=24.0.0` |
|
|
40
|
-
| **Tests** | 938 (937 pass / 1 skip) ·
|
|
40
|
+
| **Tests** | 938 (937 pass / 1 skip) · 118 doc-drift assertions · eslint strict |
|
|
41
41
|
|
|
42
42
|
**Web panel**(sidebar → "远程同事" / Remote co-worker):node list with CPU /
|
|
43
43
|
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
|
@@ -30,19 +30,25 @@
|
|
|
30
30
|
|
|
31
31
|
| | |
|
|
32
32
|
|---|---|
|
|
33
|
-
| **状态** | 0.
|
|
33
|
+
| **状态** | 0.9.0 —— 主机 + 副机端到端可用;`exec` 与 `agent` 双链路均已在两台真机上验证通过;CI 覆盖 Windows / macOS / Linux |
|
|
34
34
|
| **DSH** | 桌面版 / CLI,0.2.0-rc.2 验证 |
|
|
35
35
|
| **平台** | Windows(全量验证)· Linux / macOS(CI 覆盖)|
|
|
36
36
|
| **依赖** | **零外部 npm 依赖** —— 只用 `node:` 内置模块 |
|
|
37
37
|
| **Node** | `^22.19.0 \|\| >=24.0.0` |
|
|
38
|
-
| **测试** | 938 项(937 pass / 1 skip)·
|
|
38
|
+
| **测试** | 938 项(937 pass / 1 skip)· 118 条文档漂移断言 · eslint 严格档 |
|
|
39
39
|
|
|
40
40
|
**Web 面板**(侧栏「远程同事」):节点列表(含 CPU/内存徽章)、派发任务、
|
|
41
41
|
任务状态、结果摘要、工作区变更、成品清单、日志尾巴、**环境体检**、
|
|
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/bin/dsh-remote-setup.js
CHANGED
|
@@ -31,6 +31,12 @@ import {
|
|
|
31
31
|
writePatchBlock,
|
|
32
32
|
} from '../lib/setup.js'
|
|
33
33
|
import { encodePairCode, decodePairCode } from '../lib/pairing.js'
|
|
34
|
+
// ── 运行时态体检(2026-10-17 修 v0.9.0 审查 M1)───────────────────
|
|
35
|
+
// CLI 此前只跑 setup.js 的 doctorChecks(14 项静态检查),**没有可达性 /
|
|
36
|
+
// loopback 诊断** —— 而它正是本版本最重要的那条。现在两段都跑、结果合并。
|
|
37
|
+
// resolveRole 保证 auto 被推断成实际角色(与运行时同一份实现)。
|
|
38
|
+
import { resolveRole } from '../lib/index.js'
|
|
39
|
+
import { envChecks, isLoopbackOrigin } from '../lib/env-check.js'
|
|
34
40
|
import { IDENTITY_FILE_NAME, readHubIdentity } from '../lib/identity.js'
|
|
35
41
|
|
|
36
42
|
const DIM = '\x1b[2m'
|
|
@@ -159,7 +165,11 @@ async function runDoctor(positionals) {
|
|
|
159
165
|
? [...rootsM[1].matchAll(/-\s*(.+)/g)].map((m) => m[1].replace(/^['"]|['"]$/g, '').trim())
|
|
160
166
|
: splitRoots(grab('roots'))
|
|
161
167
|
const config = {
|
|
162
|
-
|
|
168
|
+
// ⚠ 不要在这里默认成 'hub'(2026-10-17 修 v0.9.0 审查 M1):
|
|
169
|
+
// v0.9.0 起默认安装的生效配置是 `role: auto`。写死 'hub' 会让
|
|
170
|
+
// `--doctor` 把 auto-worker 当 hub 体检(判据全错)。
|
|
171
|
+
// 保留文件里的原值,让 resolveRole 去推断。
|
|
172
|
+
role: grab('role'),
|
|
163
173
|
hubUrl: grab('hubUrl'),
|
|
164
174
|
token: grab('token'),
|
|
165
175
|
nodeToken: grab('nodeToken'),
|
|
@@ -168,10 +178,73 @@ async function runDoctor(positionals) {
|
|
|
168
178
|
roots,
|
|
169
179
|
uiPullRoot: grab('uiPullRoot'),
|
|
170
180
|
}
|
|
171
|
-
const role = config.role === 'worker' ? 'worker' : 'hub'
|
|
172
181
|
|
|
182
|
+
// 角色解析用**与运行时同一份实现**(唯一真相):auto 在这里被推断成
|
|
183
|
+
// 实际角色,这样 doctor 报告的 role 与 hub 实际行为一致。
|
|
184
|
+
const resolvedRole = resolveRole(config) || ''
|
|
185
|
+
const role = resolvedRole === 'worker' || resolvedRole === 'both' ? 'worker' : 'hub'
|
|
186
|
+
|
|
187
|
+
// ⚠ 两段体检合并(2026-10-17 修 v0.9.0 审查 M1):
|
|
188
|
+
//
|
|
189
|
+
// 此前 CLI `--doctor`(lib/setup.js 的 doctorChecks,14 项)与面板体检
|
|
190
|
+
// (lib/env-check.js 的 envChecks,8 项)是**两套并行实现**,检查项不相交:
|
|
191
|
+
// · CLI 有 Node 版本 / hubUrl 合法性 / token / uiPullRoot
|
|
192
|
+
// 但**没有可达性/loopback 检查**(本版本最重要的诊断!)
|
|
193
|
+
// · 面板有 reachability / inject / roots / tailscale / nodes
|
|
194
|
+
// 但没有 Node 版本 / hubUrl / token
|
|
195
|
+
//
|
|
196
|
+
// 注释里三处声称"共用同一份逻辑"——那是失真的。现在的做法:
|
|
197
|
+
// **两段都跑,合并输出**。CLI 独有项(配置字段合法性)保留在 doctorChecks,
|
|
198
|
+
// 运行时态检查(可达性 / inject / roots 存在性)交给 envChecks。
|
|
199
|
+
//
|
|
200
|
+
// 这样既不失 CLI 的静态校验能力,又补上了 loopback 这个关键诊断。
|
|
173
201
|
const checks = doctorChecks({ role, config, lanIps: detectLanIps() })
|
|
174
202
|
|
|
203
|
+
// ── 追加 envChecks(运行时态检查)────────────────────────────────
|
|
204
|
+
//
|
|
205
|
+
// 探测参数:CLI 能真探测的才传,拿不到的留 null(envChecks 会跳过该项,
|
|
206
|
+
// 不会误报)。
|
|
207
|
+
// · webserver host 从配置文件里读(静态可判 —— 正是 loopback 检查的关键)
|
|
208
|
+
// · roots 存在性真查文件系统
|
|
209
|
+
try {
|
|
210
|
+
const wsHost = grab('host')
|
|
211
|
+
const wsPort = grab('port')
|
|
212
|
+
// 从配置里找 webserver 段(形如 `- id: webserver` 之后几行的 host/port)
|
|
213
|
+
const wsBlock = /- id:\s*webserver[\s\S]{0,400}?host:\s*['"]?([^'"\n]+)['"]?[\s\S]{0,120}?port:\s*(\d+)/.exec(raw)
|
|
214
|
+
const boundHost = wsBlock ? wsBlock[1].trim() : (wsHost || '')
|
|
215
|
+
const boundPort = wsBlock ? Number(wsBlock[2]) : (Number(wsPort) || 19387)
|
|
216
|
+
const loopback = isLoopbackOrigin(`http://${boundHost || '127.0.0.1'}:${boundPort}`)
|
|
217
|
+
const missingRoots = config.roots.filter((r) => {
|
|
218
|
+
try { return !fs.existsSync(r) } catch { return true }
|
|
219
|
+
})
|
|
220
|
+
const env = envChecks({
|
|
221
|
+
role: resolvedRole || 'hub',
|
|
222
|
+
config,
|
|
223
|
+
reachability: boundHost === '' && !wsBlock
|
|
224
|
+
? null
|
|
225
|
+
: {
|
|
226
|
+
bound: boundHost,
|
|
227
|
+
origin: `http://${boundHost || '127.0.0.1'}:${boundPort}`,
|
|
228
|
+
loopback: loopback === true,
|
|
229
|
+
hint: '',
|
|
230
|
+
},
|
|
231
|
+
missingRoots,
|
|
232
|
+
})
|
|
233
|
+
// 合并:envChecks 的项追加到 CLI 结果后(格式一致:{ok, label, fix})
|
|
234
|
+
for (const it of env.items) {
|
|
235
|
+
checks.push({
|
|
236
|
+
ok: it.ok,
|
|
237
|
+
// 带上 emoji 级的严重度提示:warn 不算失败但要看得见
|
|
238
|
+
label: `${it.label}${it.detail ? ` —— ${it.detail}` : ''}`,
|
|
239
|
+
fix: it.fix,
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
} catch (err) {
|
|
243
|
+
// envChecks 自身出错不能挡 CLI 体检(它只是补充)
|
|
244
|
+
checks.push({ ok: true, label: `(运行时态检查不可用:${String(err?.message ?? err)})`, fix: '' })
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
175
248
|
// 网络探活:worker 且 hubUrl 合法时试一把 TCP
|
|
176
249
|
if (role === 'worker' && config.hubUrl) {
|
|
177
250
|
try {
|
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 我想"帮用户改好",结果改出了启动崩溃。
|