Files
nodeagent/docs/VM-VERIFICATION.md
T
jkooe fbcb8f8408 fix(v23): 连接保活补 pong 检测(治僵尸连接占名额)+ B2 三防护真机验证通过
## 真机发现(Parallels Win11 ARM,B2 验证过程中撞到)
心跳只发 `ping()` 而**不检查 pong**,且 ws 库默认不会因「ping 无 pong」断开 ——
客户端进程已死(TCP 未发 FIN)时,连接会一直挂在 `states` 里占 `max_connections` 名额。
叠加「有任务/订阅时清扫保守跳过」的既有策略 → `states.size` 虚高 → **把正常控制端挡在外面**
(本次实测就把自己锁在外面了)。

## 改法
- `attachConnection` 里加 `ws.on('pong')`:**客户端回应也算活跃**(更新 lastActiveAt)
- 心跳里:若某连接静默超过 `idle_timeout_ms * 2` → `ws.terminate()`(标准 ws 保活)
  → 僵尸连接不会再永久占位

## B2 三场景真机验证(全部通过)
- ① **握手封禁**:配 `max_attempts=3` → 3 次错误密钥被拒后,**正确密钥也被拒** ✓
- ② **连接上限**:配 `max_connections=6` → 第 1~6 个成功、**第 7 个被拒** ✓
- ③ **空闲断开**:配 `idle_timeout_ms=20000` → 静默 **40 秒**被断开 ✓
  (符合 20s 阈值 + 30s 清扫周期)
- 新增 `scripts/verify-b2.mjs`(三模式:idle / maxconn / ban),全部走**真实连接**

## 验证过程中另修的两个自测问题(都记进文档)
1. 脚本默认 URL 写成了 `ws://` —— 被控端 `tls:true`,**必须 wss**(否则 socket hang up)
2. 测 `max_connections` 时值太小会**把自己锁在外面**;且被拒连接会被计入握手失败形成恶性循环
   → 结论:测试要留余量,并给自己配 `auth_ban.whitelist`

## 收尾
测试用的 `security` 段已用**备份恢复**(`agent.json.before-b2`);pong 修复已 `deploy` 到 VM
(指纹 abca2b8168f0)。
2026-10-11 01:13:12 +08:00

11 KiB
Raw Blame History

VM 验证清单(Parallels Desktop 27 + Windows 11 ARM)

用途:Windows 真机不可用时,用 Parallels 27 的 Windows 11 ARM 虚拟机提前跑掉 架构无关的验证项,把"等机器"的时间省下来。

⚠️ 本清单的结论必须按 §5 的规则标注:VM 验过的项写「VM 已验」,并注明 是否仍需真机复验。不要把 VM 结论当作真机结论 —— 二者架构不同。


1. 能补什么 / 不能补什么

类别 项 VM 可否
协议层(与架构无关) v23 三防护(握手封禁/连接上限/空闲断开)、审计锚点端到端、v26 计划任务清理逻辑 ✅ 可验,结论直接可用
输入注入层 v1.5 四项(字符串热键/预设别名/长按/后台定向投递) ✅ 可验(走 Win32 SendInput/WM_APPCOMMAND,ARM Windows 同样完整)
安装与自持 install.cmd 自提权、计划任务注册、受控重启 ✅ 可验(VM 里也是真 Windows,机制一致)
系统能力 进程/服务/文件/剪贴板/命令执行/winget ✅ 可验
刻意不可验 第三方杀软拦截(火绒等) ❌ VM 里只有 Defender
显示 / DPI / 多显示器 / 坐标校准 ❌ VM 显示栈不同
性能指标(P95 时延等) ❌ VM 数字不可比
需注意 win-x64 包在 ARM Windows 上走 Prism 模拟 ⚠️ 能跑但慢;这本身是值得记录的兼容性结论(见 B5)

2. 前置准备

① Parallels 27 装 Windows 11 ARM(Pro/Enterprise)
   └ Apple silicon 只能跑 ARM 版 Windows(x86_64 仅 Pro/Business 的早期预览,性能显著更低)

② 网络 → 桥接(Bridged),不要用默认「共享网络」
   └ 共享/NAT 下 VM 拿不到同网段 IP → UDP 广播发现(v4)测不了,Mac 也未必能直连

③ VM 内装 Node:优先 **ARM64 版**(避开 Prism 模拟层)
   └ 若想验分发包,另见 B5(用项目打的 win-x64 包)

④ VM 内起被控端(二选一)
   a. 免安装:双击 run-portable.cmd        (不注册计划任务,适合快速验能力)
   b. 正式装:解压 nodeagent-win-x64.zip → 双击 install.cmd(自提权,会建计划任务)

⑤ Mac 端登记设备并连接
   nodeagent --node vm connect <VM的IP> --key <密钥> --insecure
   nodeagent --node vm info          # 看到 51 项能力即通

建议设备名用 vm,与真机 win 分开 —— 后续所有命令都在 --node vm 上跑。


3. 验证清单

A. 基础验收(17 项,一步到位)

# 对齐 PRD FR-01~FR-08 的全套验收,可同时输出 Markdown 报告
node scripts/verify.mjs --host <VM的IP> --port 8765 --key <密钥> --insecure \
  --report /tmp/vm-verify.md

判定:脚本自报通过项;报告存档(日后对比真机报告,差异项即为"架构相关")。


B. 待办专项(逐条)

B1. v1.5 四项(该版当初未经真机就发版)

# ① 字符串热键(分隔符 + - 空格、大小写不敏感)
nodeagent --node vm key press "ctrl+shift+esc"
#   期望:{ pressed: true, chords: [...] }(被控端返回里**没有 keys 字段**,别按它判)

# ② 预设别名(约 50 个语义名)
nodeagent --node vm key press copy
#   期望:同上,且 args 走 preset 分支(非 hotkey)

# ③ 长按
nodeagent --node vm key press f24 --hold 800
#   期望:按下并保持 800ms

# ④ 后台定向投递(PostMessage 到指定进程,窗口被遮挡也生效)
nodeagent --node vm ps                      # 先取一个目标 PID
nodeagent --node vm key press f24 --route post --pid <PID>
#   期望:{ pressed: true };目标进程窗口被遮挡/最小化时**仍然生效**

判定:四条都返回 pressed: true 且无异常。 选键建议:用 f24 / shift / copy 这类无副作用的键(F24 几乎无程序绑定)。

B2. v23 三防护(配置在 agent.json 的 security 段)

// VM 被控端数据目录下的 agent.json
{ "security": { "auth_ban": { "max_attempts": 3, "ban_ms": 60000 }, "idle_timeout_ms": 60000 } }
# ① 握手失败封禁:连续用**错误密钥**连 -> 达阈值后**正确密钥也被拒**
for i in 1 2 3; do nodeagent --node tmp connect <IP> --key WRONG --insecure; done
nodeagent --node tmp connect <IP> --key <正确密钥> --insecure
#   期望:前几次报 E_AUTH_FAILED;达到 max_attempts 后,即使是正确密钥也报被封禁

# ② 连接上限:并发开连接直到第 N+1 个被拒
#   期望:超出上限的连接被拒(E_NODE_BUSY 或等价错误)

# ③ 空闲断开:连上后静置超过 idle_timeout_ms
#   期望:连接被服务端主动断开,控制端收到 closed

判定:三种防护各自可达且恢复后正常(封禁到期、连接释放、重连成功)。

B3. v26 自重启任务清理(治"Running 僵尸")

# ① 触发受控重启
nodeagent --node vm restart              # 重启会让配置变更生效,连接会短暂断开
# ② 重连后查看计划任务
nodeagent --node vm exec "schtasks /query /fo csv /tn \"nodeagent-selfrestart*\""

判定:

  • 清理条件为「非 Running 或 LastRunTime 早于 1 小时」
  • 刚创建的那个任务(当前 agent 的宿主)必须仍在 ← 若它被清掉,说明误杀了宿主(严重)
  • 一小时后复查:旧任务应已被清理,不再累积

B4. 审计锚点端到端(此前只做了离线 fixture 验证)

# ① 生成锚点(拉 VM 的链头)
node scripts/anchor-audit.mjs collect --node vm --out /tmp/vm-anchor.jsonl --note "VM 验证"
# ② 上传到链外(云盘 …/Project/NodeAgent/anchors/,文件名带年月日)
# ③ 从云盘下载回来再比对
node scripts/anchor-audit.mjs verify --anchors /tmp/vm-anchor.jsonl --node vm
#   期望:ok:true,verdict 为「链已增长」或「与锚点完全一致」
#   (刚锚定就比对通常显示"已增长" —— 锚定动作自身也写一条审计,属正常)

判定:collect 能拿到链头、云盘上传成功、verify 返回 ok:true。

B5. win-x64 包在 ARM Windows 上的兼容性(额外结论)

# 把项目打的包解压到 VM,用**包内自带的 x64 Node**起被控端(走 Prism 模拟)
# 期望:能起、能被连接、基础能调用

判定:能跑 → 说明分发包对 ARM Windows 也友好(值得写进 README 的加分结论); 不能跑或异常 → 记录现象,并在文档里注明"分发包需 ARM64 版本方能原生运行"。


4. 一键顺序(建议)

A 基础验收(verify.mjs)
  → B1 v1.5 四项
    → B2 v23 三防护(改 agent.json 后需 restart)
      → B3 v26 清理(restart 后查任务)
        → B4 锚点端到端
          → B5 x64 包兼容性

每步完成后把结论记到 docs/ROADMAP.md §九 台账,并标注来源是 VM 还是真机。


5. 结论标注规则(强制)

情形 台账/CHANGELOG 写法
架构无关项(B2/B3/B4)在 VM 验过 ✅ 已验证(VM · Windows 11 ARM)
输入/系统能力(A/B1)在 VM 验过 ✅ 已验证(VM),若涉及坐标/DPI 则加注「坐标类待真机复验」
杀软交互 / 显示 / 性能 ⏳ 待真机(VM 不可替代,不得因 VM 通过就标已验)
x64 包在 ARM 上跑通(B5) ✅ 兼容性结论:win-x64 包可在 ARM Windows 运行(经 Prism)

一条纪律:VM 与真机架构不同(真机 i7-12650H = x86_64)。任何"VM 通过" 都不能自动推出"真机通过";涉及原生二进制、驱动、杀软、显示栈的部分尤其如此。 宁可多留一条"待真机复验",也不要让台账出现无法追溯来源的"已验证"。


6. 已知 VM 环境差异(排查时先看这里)

现象 原因 处理
discover(UDP 广播发现)无结果 网络是共享/NAT 改桥接
分辨率/坐标与真机不符 VM 显示栈不同 只做逻辑验证,坐标待真机
性能数字明显偏低 VM + 可能的 Prism 模拟 不要采信 VM 的性能指标
杀软相关错误复现不了 VM 里只有 Defender 需真机(或另行安装第三方杀软)
x64 Node 启动慢/异常 走 Prism 模拟 换 ARM64 Node 复核

7. 实际验证结果(2026-10-11,Parallels 27 + Win11 ARM)

本节是实测记录,不是计划。环境:Parallels 共享网络,VM 10.211.55.9, Mac 侧 bridge100=10.211.55.2;被控端 -NodeId win-arm -AllowInput。

B5 结论(重要,与预期不同)

组合 结果
win-x64 包(内含 x64 Node) ⚠️ 能启动、能 listen 8765,但 TLS 握手 60s 不完成 → 协议层不可用(Prism 模拟下)
换成 ARM64 Node(同版本 v22.20.0) ✅ 完全正常:TLS 握手成功、连接、51 项能力

结论:nodeagent-win-x64.zip 不能在 ARM Windows 上实用。

解决方案(已落地):scripts/pack.mjs 新增 --arch:

node scripts/pack.mjs --arch arm64     # → release/nodeagent-win-arm64.zip(内含 ARM64 node.exe)
node scripts/pack.mjs                  # 默认仍出 x64 包(向后兼容)

ARM Windows(含 Parallels Win11 ARM)请用 arm64 包;用 x64 包会卡在 TLS 握手。 (临时替代:手工把包内 node.exe 换成 ARM64 版同样可行,重启后需再换 —— 故推荐用 arm64 包。)

各项结果

项 结果
A 组(verify.mjs 17 项) ✅ 15 通过 / 0 失败 / 1 跳过
B1 v1.5 四项 ✅ 全过(含 route=post 实测命中 138 窗口)
B4 锚点端到端 ✅ 全过(含上传云盘)
B2 v23 三防护 ✅ 三场景全通过:① 握手封禁(3 次错误密钥后连正确密钥也被拒)② 连接上限(配 6 → 第 7 个被拒)③ 空闲断开(静默 40s 被断,符合 idle 20s + 清扫周期 30s)
B3 v26 清理 ✅ 验证并改进了策略:实测发现「1 小时」粗阈值下频繁 restart 仍累积 → 改为「早于本 agent 启动时刻」精确阈值(部署到 VM 实测:restart 后旧任务被自动清理、宿主未误杀)

实测新增的排查项(补进 §6)

现象 原因 处理
装了却连不上(TLS 握手不完成) x64 Node 在 Prism 下协议层异常 换 ARM64 Node
control.ps1 报"变量引用无效" 脚本里 $var: 被当 drive 引用(解析期错误 → 整个脚本废) 已修(写 ${var});已加守卫
直接跑 install.ps1 后找不到 PSK.txt 只有 install.cmd 才产生 PSK.txt 密钥在 <数据目录>\agent.json 的 key 字段
装完输入类能力不可用 install.cmd 不传 -AllowInput(默认 false) 用 install.ps1 -AllowInput
nodeagent --node x cmd 报"未知命令" --node 前置需等号 写 --node=x,或放命令后
计划任务启动后端口不通 node.exe 被手工进程占用(覆盖失败)+ 8765 被占(任务起来即冲突退出) 先 Get-Process node | Stop-Process -Force 再解压/启动
自测时 socket hang up URL 写成 ws:// 而服务端是 wss(tls:true) 用 wss://
测 max_connections 把自己锁在外面 测试值太小 + 被拒连接会被计入握手失败 → 恶性循环 ① 测试时留余量 ② 给自己加 auth_ban.whitelist