dsh-remote-agent 0.9.8 → 0.9.10

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,109 @@
9
9
 
10
10
  ---
11
11
 
12
+ ## [0.9.10] - 2026-10-20
13
+
14
+ ### 文档:README 补齐四项能力的介绍
15
+
16
+ 用户反馈(第 2 次):「readme 好像没有功能相关的介绍」。0.9.8 加了四项能力,
17
+ 但 README 只更新了工具数量 —— **没有任何功能介绍**。
18
+
19
+ ### 新增三节
20
+
21
+ | 小节 | 内容 |
22
+ |---|---|
23
+ | **七、一键修复** | 两步流程(预览/确认)+ 6 条护栏表(只改托管块 / 备份 / 写后校验 / 回滚 / 撤销 / 绝不用 0.0.0.0)|
24
+ | **八、外部工具检测** | 四个工具「缺了会怎样」表 + **为什么不代下载**的边界说明 |
25
+ | **九、文件写入与进程诊断** | 写操作 5 条护栏表 + `remote_kill` 三道护栏(含拒绝时的具体提示语)|
26
+
27
+ ### 更新已有章节
28
+
29
+ - **五、出问题能自己诊断**:补「一键修复」「错误翻译成人话」「连接失败可见」
30
+ 「外部工具检测」四行;「面板运行中分组」补上"最近一步在干什么"
31
+ - **用法 / 读副机上的文件**:原写"只读,**没有任何写操作**" —— 这句在 0.9.8
32
+ 之后已经不准确,改成"**这一组**是只读的",并新增两小节:
33
+ - 「改副机上的文件(写操作,需确认)」—— 含 `resolveWritable` 的
34
+ 设计理由(为什么不能复用只读的 `resolveAny`)
35
+ - 「排查副机上的进程与端口」—— 含三个典型用法示例 + kill 三道护栏表
36
+ - **安全**:从 3 条扩到 6 条(写操作、kill、SMB),并新增一张
37
+ **「本插件主动放宽边界的三处」** 表 —— 把所有放宽集中列在一处,别处没有
38
+ - **结构**:补 5 个新模块(`auto-fix` / `explain-error` / `external-tools` /
39
+ `listen-server` / `instance-config`)
40
+
41
+ ### 写文档时的一条原则
42
+
43
+ 不只写"有什么功能",还写**"为什么这么做"**:
44
+
45
+ - 一键修复为什么是两步 → 因为改的是 DSH 自己的配置,我们踩过事故
46
+ - 外部工具为什么不代下载 → 因为会变成软件分发方,供应链责任全落在我们头上
47
+ - 写操作为什么不递归删 → 因为不可逆损失太大
48
+ - kill 为什么拒杀父进程 → 因为副机失联后你彻底失去远程修复能力
49
+
50
+ ### 新增断言
51
+
52
+ doc-drift **96**:README 必须有七/八/九三节标题 + 「主动放宽边界」+
53
+ 「为什么不代下载」+「三道护栏」。这样以后加功能忘了写文档会立刻失败。
54
+
55
+ > 写断言时踩了个小坑:变量名用了 `readme`,与模块顶部的 `readme` 同名,
56
+ > 被 eslint 的 `no-shadow` 拦下。改名 `rootReadme3`。
57
+
58
+ ```
59
+ packaging 4 项
60
+ doc-drift 132/132
61
+ diag:inject 11 项
62
+ tests 1032(1030 pass / 0 fail / 2 skip)
63
+ lint 干净
64
+ ```
65
+
66
+ ## [0.9.9] - 2026-10-20
67
+
68
+ > **0.9.8 的 CI 回归修复。** 本地全绿,但 macOS 与 Windows 上失败。
69
+
70
+ ### 修复:测试断言直接比绝对路径(realpath 与平台表示差异)
71
+
72
+ **现象**(CI run `39389528713`,macOS + Windows 失败,Linux 通过):
73
+
74
+ macOS: actual '/private/var/folders/.../b.txt'
75
+ expected '/var/folders/.../b.txt'
76
+
77
+ Windows: actual 'C:\Users\runneradmin\AppData\Local\Temp\...\b.txt'
78
+ expected 'C:\Users\RUNNER~1\AppData\Local\Temp\...\b.txt'
79
+
80
+ **根因**:`fs-service` 的写操作返回 **realpath 后**的路径 —— 这是**刻意的正确行为**
81
+ (realpath 才能识破软链逃逸与 8.3 短名)。但测试用 `path.join(root, ...)`
82
+ 直接比字符串:
83
+
84
+ - **macOS**:`/var` 是 `/private/var` 的软链
85
+ - **Windows CI**:`RUNNER~1` 是 `runneradmin` 的 8.3 短名
86
+
87
+ 两者都让"直接比字符串"失败。**Linux 恰好没有这层间接,所以本地和 ubuntu 都过。**
88
+
89
+ **修法**:这条用例真正要验证的是"移动成功且内容对",不是"路径字符串相等"。
90
+ 改成比 `basename` + 文件内容:
91
+
92
+ ```js
93
+ assert.equal(path.basename(r.to), 'b.txt')
94
+ assert.equal(fs.readFileSync(b, 'utf8'), 'A')
95
+ ```
96
+
97
+ ### 0.9.8 的四项能力(本版包含)
98
+
99
+ | 能力 | 关键护栏 |
100
+ |---|---|
101
+ | **一键修复** | 只改托管块 / 写前备份 / 写后校验 / 二次确认 / 可撤销 / 绝不用 0.0.0.0 |
102
+ | **外部工具检测** | 只探测 + 给命令,**不代下载**(守供应链边界)|
103
+ | **写操作 ×4** | confirm 必填 / 默认不覆盖 / 不递归删 / 原子写 / 8MB 上限 / roots 护栏 |
104
+ | **进程诊断 ×3** | kill 三道护栏:confirm / 拒杀自身与父进程 / 拒杀系统关键进程 |
105
+ | **UI 折叠** | 首屏只留当下需要的;配置/体检/共享收进 `<details>` |
106
+
107
+ ```
108
+ packaging 4 项
109
+ doc-drift 131/131
110
+ diag:inject 11 项
111
+ tests 1032(1030 pass / 0 fail / 2 skip)
112
+ lint 干净
113
+ ```
114
+
12
115
  ## [0.9.8] - 2026-10-20
13
116
 
14
117
  ### 四项能力(用户要求:一键修复 / 自动装依赖 / 更多工具 / 重写 UI)
package/README.md CHANGED
@@ -160,9 +160,13 @@ node bin/dsh-remote-setup.js --doctor 你的配置.yml
160
160
  | 功能 | 说明 |
161
161
  |---|---|
162
162
  | **环境体检** | 面板一键体检 8 项:角色配置、`inject` 依赖、**Web 服务是否绑在可达地址**、端口、防火墙、`roots` 是否存在、Tailscale、副机在线。**每条都给具体修法**,能自动修的标出来 |
163
- | **可达性自检** | 启动时检查 `webServer` 是否绑在 loopback —— 绑了副机永远连不上,且主机侧毫无异常。绑了会打明确 warn + 面板红色横幅 |
163
+ | **一键修复** | 机械可判定的项(`webserver` host、`role`)可以**预览后一键修**。见下面「七、一键修复」 |
164
+ | **错误翻译成人话** | `ECONNREFUSED` / `401` / `timeout after 40000ms` 这类原始错误统一翻译成「哪里没对 + 下一步做什么」。覆盖 errno、**17 个协议错误码**、HTTP 状态、文案特征 |
165
+ | **连接失败可见** | 记录**每一次**副机连接尝试(成功/失败 + 原因 + 时间 + 远端地址)。面板直接显示「最后尝试 2 分钟前 —— 握手超时 → 检查…」,并区分**从没连过** / **连过又断了**(排查方向完全不同)|
166
+ | **外部工具检测** | 一键探测 Tailscale / PowerShell / git / Node.js 在不在,缺了告诉你「为什么需要 + 安装命令 + 官方链接」。见下面「八、外部工具」 |
167
+ | **可达性自检** | 启动时检查端点是否绑在 loopback —— 绑了副机永远连不上,且主机侧毫无异常。绑了会打明确 warn + 面板红色横幅 |
164
168
  | **队列健康快照** | 副机随资源帧上报 `queued` / `running` / `maxConcurrent` / 最久等待;堵死时面板与 `remote_nodes` 都会警告 |
165
- | **面板「运行中」分组** | 运行中的任务置顶,显示**已跑多久**(判断"是不是卡住了"的唯一线索)|
169
+ | **面板「运行中」分组** | 运行中的任务置顶,显示**已跑多久**,并直接显示**最近一步在干什么**(`最近:调用工具 Write · 共 23 步`)—— 不用展开详情 |
166
170
  | **取消真打断** | `remote_cancel` 真的中止 agent 循环(不只是置个标志等它自己跑完)|
167
171
  | **审计日志** | 派发/取消/拉取/审批全部留痕(`~/.dsh-remote-agent/audit.jsonl`,可轮转)|
168
172
 
@@ -177,6 +181,73 @@ node bin/dsh-remote-setup.js --doctor 你的配置.yml
177
181
  | **凭证不离开副机** | 协议里不带任何模型 key;副机用自己的模型配置跑自己的循环 |
178
182
  | **零 npm 依赖** | 只用 `node:` 内置模块,无构建步骤,`lib/` 就是产物 |
179
183
 
184
+ ### 七、一键修复(会改 DSH 的配置,所以有两步确认)
185
+
186
+ 副机连不上的**最常见原因**是:`webserver` 绑在 `127.0.0.1`,或者 `remote-agent`
187
+ 的 `role` 没写对。这些是**机械可判定**的,可以自动修。
188
+
189
+ 但 `cordis.patch.yml` 是 DSH 自己的配置文件,改它有风险 —— 所以流程刻意做成两步:
190
+
191
+ ```
192
+ [预览修复] → 只读。列出"将要改什么" + "不会做什么"
193
+ [确认修复] → 才写盘(必须带 confirm: "yes")
194
+ ```
195
+
196
+ **护栏**(每一条都对应一次真实事故):
197
+
198
+ | 护栏 | 防的是什么 |
199
+ |---|---|
200
+ | **只改托管块** | `# --- dsh-remote-agent (managed) ---` 之间;你手写的其它条目**一个字节不碰** |
201
+ | **写前必备份** | `.bak-<时间戳>`,随时可回 |
202
+ | **写后必校验** | 重新解析,确认条目在、**没有重复 id**(同 id 会挂载两次导致启动失败)|
203
+ | **失败必回滚** | 保留备份并提示用「撤销」 |
204
+ | **可一键撤销** | `action: 'undo'` 用备份覆盖回去 |
205
+ | **绝不用 `0.0.0.0`** | 新版 DSH **硬拒绝**通配地址,会直接启动崩溃(我们踩过,10 份 crash 日志)。地址自动选 Tailscale / 私网 IP |
206
+
207
+ **只修机械可判定的项**。端口占用、防火墙、装软件、模型凭证这些**需要人判断**的,
208
+ 只给建议不动手。
209
+
210
+ ### 八、外部工具检测(只告诉你缺什么,不代下载)
211
+
212
+ 有些工具**缺了会静默失败**,而错误表现完全不像"缺个工具":
213
+
214
+ | 工具 | 缺了会怎样 |
215
+ |---|---|
216
+ | **Tailscale** | 跨网时副机连不上,而错误表现只是"连接超时" —— 很难想到是缺个 VPN 工具。同一局域网可以不用 |
217
+ | **PowerShell** | Windows 上 SMB 共享、部分 `exec` 任务依赖它。缺了表现为"命令找不到" |
218
+ | **git** | 仓库感知调度(`remote_task` 的 `repo` 参数)失效,但用户不知道为什么 |
219
+ | **Node.js** | 副机的 headless 后端要独立 node 才能 spawn 子进程,缺了 agent 模式退化 |
220
+
221
+ 面板「检测工具」一键探测,缺的项给你**为什么需要 + 可复制的安装命令 + 官方下载页**。
222
+
223
+ > **为什么不代下载?**
224
+ >
225
+ > 零 npm 依赖是核心卖点;而代下载第三方二进制会让我们变成软件分发方 ——
226
+ > 装哪个版本、从哪下、校验和怎么保证、装失败算谁的、供应链攻击面,全落在我们头上。
227
+ > 所以只做「探测 + 告诉你命令」,执行权在你。
228
+
229
+ ### 九、文件写入与进程诊断(会改副机状态,所以必须确认)
230
+
231
+ 前四个文件工具全是**只读**。这一组会**改副机**,所以都要求 `confirm: "yes"`。
232
+
233
+ **写操作**(`remote_write` / `remote_mkdir` / `remote_move` / `remote_delete`):
234
+
235
+ | 护栏 | 说明 |
236
+ |---|---|
237
+ | **默认不覆盖** | 目标已存在直接拒绝,要覆盖必须显式 `overwrite: true` —— 防"AI 手滑清空已有文件" |
238
+ | **不递归删除** | 只删**单个文件或空目录**;非空目录一律拒绝。真有需要请用 `exec` 显式删除(那是可查的命令)|
239
+ | **原子写** | 临时文件 + rename,失败不留半截文件 |
240
+ | **8MB 上限** | 防一次写入把副机磁盘写满(副机是常驻进程,磁盘满会让任务/日志/状态全失败)|
241
+ | **roots 护栏** | 目标与**父目录**都必须在 roots 内(防软链逃逸);`denyPaths` 同样生效 |
242
+
243
+ **进程诊断**(`remote_ps` / `remote_port` / `remote_kill`):
244
+
245
+ - `remote_ps` / `remote_port` **只读** —— 排查「端口被谁占了」「那个卡住的进程还在不在」
246
+ - `remote_kill` **不可逆**,三道护栏:
247
+ 1. 必须 `confirm: "yes"`
248
+ 2. **拒绝杀本插件进程与其父进程** —— 否则副机失联,你会彻底失去远程修复能力
249
+ 3. **拒绝杀系统关键进程**(System / csrss / wininit / services / lsass 等)
250
+
180
251
  ### 工具速查(共 19 个)
181
252
 
182
253
  | 类别 | 工具 |
@@ -404,7 +475,7 @@ ack 自动按明文兼容旧中继继续),安全边界与已知局限见
404
475
  | `remote_grep` | `path`、`pattern`(必填)、`glob`、`regex`、`ignoreCase`、`maxMatches` | 副机本地全文搜索(纯 JS,不 spawn 外部命令);上限 2000 文件 / 单文件 2MB / 命中 200 / 10 秒 |
405
476
  | `remote_tree` | `path`(必填)、`depth` | 目录树摘要(深度 1-6,默认 3;条目 ≤2000) |
406
477
 
407
- **边界(与任务链路同一条底线)**:只读,没有任何写操作;路径必须是绝对路径
478
+ **边界(与任务链路同一条底线)**:**这一组是只读的**;路径必须是绝对路径
408
479
  且落在该节点 `roots` 内,超出即拒(`workspace-denied`);`denyPaths` 同样生效
409
480
  (被禁条目连名字都不上报);符号链接只列条目不解引用。文件操作走独立的
410
481
  `fs/req`↔`fs/res` 帧,**不进任务队列、不占并发槽**,且在 relay 模式下与任务
@@ -415,6 +486,78 @@ ack 自动按明文兼容旧中继继续),安全边界与已知局限见
415
486
 
416
487
  </details>
417
488
 
489
+ ### 改副机上的文件(写操作,需确认)
490
+
491
+ 上面四个是只读的。要**写**,用这四个 —— 它们都会**改副机状态**,所以
492
+ **必须传 `confirm: "yes"`**:
493
+
494
+ ```js
495
+ remote_write { path, content, overwrite?, confirm: "yes" }
496
+ remote_mkdir { path, confirm: "yes" }
497
+ remote_move { from, to, overwrite?, confirm: "yes" }
498
+ remote_delete { path, confirm: "yes" }
499
+ ```
500
+
501
+ <details>
502
+ <summary>写操作的护栏(为什么它们安全得起来)</summary>
503
+
504
+ | 护栏 | 具体行为 |
505
+ |---|---|
506
+ | **默认不覆盖** | `remote_write` 遇到已存在文件**直接拒绝**。要覆盖必须显式 `overwrite: true` —— 这条防的是"AI 手滑清空你的配置文件" |
507
+ | **不递归删除** | `remote_delete` 只删**单个文件或空目录**。非空目录返回 `artifact-denied`,并告诉你"非空目录请用 exec 显式删除"。递归删除太容易造成不可逆损失,我们刻意不做 |
508
+ | **原子写** | 先写 `<file>.dshra-tmp-<pid>-<ts>` 再 `rename`。中途崩溃不会留下半截文件 |
509
+ | **8MB 上限** | 单次写入超过 8MB 直接拒(`artifact-too-large`)。副机是常驻进程,磁盘满会让任务、日志、状态文件全失败 |
510
+ | **roots 护栏(加强版)** | 只读工具的 `resolveAny` 要求路径**已存在**;写操作改用 `resolveWritable`:目标可以不存在,但对**它第一个存在的祖先**做 realpath 再校验 roots —— 这样既能新建文件,又防"roots 内的软链指向外面" |
511
+ | **父目录也校验** | 除了目标本身,它的父目录也必须 realpath 后在 roots 内 |
512
+ | **符号链接只删链接** | `remote_delete` 用 `lstat`,删的是链接本身,不会顺着链接把目标删掉 |
513
+
514
+ **跨盘移动会明确报错**(`EXDEV`),不做静默复制 —— 大文件会卡住,且语义不同
515
+ (复制不等于移动)。真要跨盘请用 `exec` 的 `cp` / `mv`。
516
+
517
+ </details>
518
+
519
+ ### 排查副机上的进程与端口
520
+
521
+ 远程排查时你不在副机前面,看不见任务管理器。这三个工具补上这一块:
522
+
523
+ ```js
524
+ remote_ps { node?, limit?, filter? } // 列进程(只读)
525
+ remote_port { port } // 查端口占用(只读)
526
+ remote_kill { pid, confirm: "yes" } // 杀进程(不可逆)
527
+ ```
528
+
529
+ 典型用法:
530
+
531
+ ```
532
+ 「副机上那个 node 进程还在吗?」 → remote_ps { filter: "node" }
533
+ 「19387 到底有没有在监听?谁占着?」 → remote_port { port: 19387 }
534
+ 「把那个卡死的 PID 12345 杀掉」 → remote_kill { pid: 12345, confirm: "yes" }
535
+ ```
536
+
537
+ <details>
538
+ <summary><code>remote_kill</code> 的三道护栏</summary>
539
+
540
+ 杀错进程的后果可能很严重(副机上任务全崩,甚至把 DSH 自己杀掉导致彻底失联),
541
+ 所以有三道独立护栏:
542
+
543
+ | # | 护栏 | 拒绝时的提示 |
544
+ |---|---|---|
545
+ | 1 | **必须 `confirm: "yes"`** | `杀进程需要显式传 confirm: "yes"(不可逆操作)` |
546
+ | 2 | **拒绝杀本插件进程与父进程** | `拒绝:这是本插件的父进程(通常是 DSH 本体,杀掉会让副机失联)` |
547
+ | 3 | **拒绝杀系统关键进程** | `拒绝:lsass 是系统关键进程,杀掉可能导致系统不可用` |
548
+
549
+ 第 2 道最重要:如果 AI 把副机的 DSH 杀了,你就**彻底失去远程修复能力** ——
550
+ 只能跑到那台机器前手动重启。这是最坏的结果,所以硬编码拒绝。
551
+
552
+ 受保护的系统进程名(Windows):`System` / `csrss.exe` / `wininit.exe` /
553
+ `winlogon.exe` / `services.exe` / `lsass.exe` / `smss.exe` / `svchost.exe` /
554
+ `dwm.exe` / `explorer.exe` 等。
555
+
556
+ > ⚠ 这些护栏防的是**误判**,不是恶意 —— 有管理员权限的人总能绕过。
557
+ > 它们的作用是让"AI 顺手杀错"这件事不会发生。
558
+
559
+ </details>
560
+
418
561
  ---
419
562
 
420
563
 
@@ -429,13 +572,30 @@ ack 自动按明文兼容旧中继继续),安全边界与已知局限见
429
572
 
430
573
  ## 安全
431
574
 
432
- **这个插件让主机能在另一台电脑上读写文件、执行命令。** 部署前请读
433
- **[SECURITY.md](SECURITY.md)**,尤其这三条:
575
+ **这个插件让主机能在另一台电脑上读写文件、执行命令、结束进程。** 部署前请读
576
+ **[SECURITY.md](SECURITY.md)**,尤其这几条:
434
577
 
435
578
  1. **`nodeToken` 默认不校验** —— 公网部署**必须**配。留空只适用于完全可信的私网
436
579
  (Tailscale / 同机 / 可信 LAN)。
437
580
  2. **传输不自带 TLS** —— 公网直连必须在前面放 TLS 反代,用 `wss://`。
438
581
  3. **`roots` 是真正的安全边界** —— 只给任务需要的目录,不要给整个盘或用户 home。
582
+ 4. **写操作会改副机文件** —— `remote_write` / `remote_mkdir` / `remote_move` /
583
+ `remote_delete` 都要求 `confirm: "yes"`;默认不覆盖、不递归删。如果副机上
584
+ 有**不该被 AI 改**的东西,用 `denyPaths` 明确挡掉,或者干脆不给那个目录 roots。
585
+ 5. **`remote_kill` 会结束进程** —— 同样要求 `confirm: "yes"`,并硬编码拒绝
586
+ 杀本插件进程、父进程(通常是 DSH 本体)与系统关键进程。
587
+ 6. **SMB 共享会绕过上面全部边界** —— 开启后 `roots` / `denyPaths` 全部失效
588
+ (Windows 内核直读,插件拦不住)。默认只读,且需要二次确认。
589
+
590
+ ### 本插件主动放宽边界的三处(都在这里,别处没有)
591
+
592
+ | 能力 | 放宽了什么 | 补偿措施 |
593
+ |---|---|---|
594
+ | **写操作**(4 个工具)| 从"只读"到"可改文件" | `confirm` 必填 · 默认不覆盖 · 不递归删 · 原子写 · 8MB 上限 · roots + 父目录校验 |
595
+ | **进程诊断**(`remote_kill`)| 从"只读"到"可结束进程" | `confirm` 必填 · 拒杀自身/父进程/系统进程 |
596
+ | **SMB 共享** | `roots` / `denyPaths` **完全失效** | 默认只读 · 二次确认 · 明确文档说明风险 |
597
+
598
+ 除此之外,插件不做任何越界的事。
439
599
 
440
600
  ---
441
601
 
@@ -446,8 +606,13 @@ ack 自动按明文兼容旧中继继续),安全边界与已知局限见
446
606
  | `lib/index.js` | 插件入口(角色分发器:`role: auto` / `hub` / `worker` / `both`) |
447
607
  | `lib/hub/**` | 主机侧:节点注册表、**19 个工具**、subagent provider、UI 路由、环境体检 |
448
608
  | `lib/hub/views.js` | **投影共享层** —— 工具与 UI 用同一份字段白名单 |
449
- | `lib/node/**` | 副机侧:拨入、任务引擎、产物服务、exec/agent 双后端、SMB 共享 |
609
+ | `lib/node/**` | 副机侧:拨入、任务引擎、产物服务、exec/agent 双后端、SMB 共享、文件读写、进程诊断 |
450
610
  | `lib/env-check.js` | 环境体检(命令行与面板共用一份逻辑) |
611
+ | `lib/auto-fix.js` | 一键修复(只改托管块 + 备份 + 写后校验 + 可撤销) |
612
+ | `lib/explain-error.js` | 错误翻译层(errno + 17 个协议码 + HTTP + 文案特征 → 人话 + 下一步) |
613
+ | `lib/external-tools.js` | 外部工具检测(Tailscale/PowerShell/git/Node,只给命令不代下载) |
614
+ | `lib/listen-server.js` | 独立监听端口(副机拨入,不依赖 DSH 的 webserver 配置) |
615
+ | `lib/instance-config.js` | 独立配置文件(`~/.dsh-remote-agent/instance.json`,抗 DSH 重写) |
451
616
  | `lib/client.js` | 浏览器半边(Web 面板) |
452
617
  | `lib/protocol.js` | 线协议(帧类型、错误码、`PROTOCOL_VERSION`) |
453
618
  | `lib/e2e.js` | 中继链路端到端加密(X25519 握手 + ed25519 认证 + AES-256-GCM 帧封) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-agent",
3
- "version": "0.9.8",
3
+ "version": "0.9.10",
4
4
  "type": "module",
5
5
  "description": "把另一台电脑变成你 DSH 的远程 Agent 同事:副机本地持久跑任务、独立 Agent 循环,只回传 handoff 摘要与产物清单,成品按需拉取。",
6
6
  "license": "MIT",
@@ -1337,6 +1337,38 @@ check('65', 'eslint 严格档:lint script + 配置文件 + CI job 三件套齐
1337
1337
  'lib/client.js(Fold 组件 + dra-fold 样式 + 默认展开策略)')
1338
1338
  }
1339
1339
 
1340
+ /* ── 96. README 必须介绍四项新能力(2026-10-20)──
1341
+ *
1342
+ * 起因:0.9.8 加了四项能力(一键修复 / 外部工具 / 写操作 / 进程诊断),
1343
+ * 但 README 只更新了工具数量,**没有任何功能介绍** —— 用户反馈"readme 好像
1344
+ * 没有功能相关的介绍"(这是第 2 次)。
1345
+ *
1346
+ * 断言做法:每个能力在 README 里必须有对应的小节标题。这样以后加功能忘了
1347
+ * 写文档,这条会立刻失败。
1348
+ */
1349
+ {
1350
+ // ⚠ 变量名不能叫 readme —— 模块顶部已有 `const readme`(用于 read() 帮助),
1351
+ // 同名会被 eslint 的 no-shadow 拦下。
1352
+ const rootReadme3 = read('README.md')
1353
+ const required = [
1354
+ ['七、一键修复', '一键修复'],
1355
+ ['八、外部工具检测', '外部工具检测'],
1356
+ ['九、文件写入与进程诊断', '写操作 + 进程诊断'],
1357
+ ]
1358
+ const missing = required.filter(([heading]) => !rootReadme3.includes(heading)).map(([, name]) => name)
1359
+
1360
+ check('96', 'README 介绍了四项新能力(一键修复 / 外部工具 / 写操作 / 进程诊断)',
1361
+ missing.length === 0
1362
+ // 安全边界也必须写清楚(写操作与 kill 是主动放宽边界的能力)
1363
+ && /主动放宽边界/.test(rootReadme3)
1364
+ // 每项都要说明"为什么这么做",而不只是"有什么功能"
1365
+ && /为什么不代下载/.test(rootReadme3)
1366
+ && /三道护栏/.test(rootReadme3),
1367
+ missing.length === 0
1368
+ ? 'README.md(七/八/九 三节 + 安全边界 + 设计理由)'
1369
+ : `README.md 缺少小节:${missing.join('、')}`)
1370
+ }
1371
+
1340
1372
  /* ── 输出 ── */
1341
1373
  const failed = results.filter((r) => !r.ok)
1342
1374
  if (process.argv.includes('--json')) {