From b9236dfaf15342d13e1465ddf68e8ce694de2562 Mon Sep 17 00:00:00 2001 From: jkooe <62494645+jkooe@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:35:40 +0800 Subject: [PATCH] =?UTF-8?q?chore(privacy):=20=E5=85=AC=E5=BC=80=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E9=9A=90=E7=A7=81=E8=84=B1=E6=95=8F=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E6=B8=85=E9=99=A4=E5=8F=AF=E5=AE=9A=E4=BD=8D?= =?UTF-8?q?=E5=88=B0=E4=B8=AA=E4=BA=BA=E4=B8=8E=E8=AE=BE=E5=A4=87=E7=9A=84?= =?UTF-8?q?=E5=AD=97=E9=9D=A2=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按 security-reports/ 的隐私审查台账逐项收口。纯脱敏与表述泛化, 不含任何业务逻辑变更;实测结论与排查步骤一字未动。 P1(可定位到具体设备/个人) - scripts/verify-b2.mjs:NA_URL 去掉硬编码缺省值(原指向一台真实机器),改为必填。 这是全仓库唯一「拿着就能连到某台真机」的常量。 - 测试账户名 pdverify → na-test(docs/VM-VERIFICATION.md 7 处、docs/console-plan.md 1 处) - 虚拟机与宿主的真实网段 → RFC 5737 文档保留段 192.0.2.x(含 CLOSE_WAIT 记录) - 跨项目名与私人称呼 → 中性表述(docs/PRD.md、docs/DEVELOPMENT.md、docs/console-plan.md 等) P2(低风险设备指纹) - 虚拟化产品及其 CLI / 访客工具名 → 中性表述(12 个文件 + 1 个工作流注释) - 测试目录 C:\nodeagent-test → C:\na-test - NAS 设备型号与系统名 → NAS 设备 / NAS 系统(docs/DEPLOY-LINUX.md、scripts/install-linux.sh、README.md) - 真机 CPU 型号、真机物理分辨率、代理软件品牌与监听端口、杀软品牌 → 泛化 - 单元测试示例段去掉真机 IP 尾八位:acl-v11 / net-scripts 的 fixture 与断言同步更新 - .gitignore 补 config.json / agent.json(含真实密钥与节点地址,绝不入库) - 顺带移除混入技术文档的「不构成投资建议」字样(与文档主题无关) 验证:pnpm test:unit 340/340 通过;独立扫描器对 235 个跟踪文件做 17 类模式 全量扫描,命中 0。 --- .github/workflows/windows-e2e.yml | 2 +- .gitignore | 4 + CHANGELOG.md | 117 +++++++++++++++++++++++-- README.md | 6 +- apps/agent/src/capabilities/input.ts | 2 +- apps/agent/src/capabilities/screen.ts | 2 +- apps/agent/src/capabilities/system.ts | 2 +- apps/agent/src/util/dpi.ts | 14 +-- docs/DEPLOY-LINUX.md | 6 +- docs/DEVELOPMENT.md | 6 +- docs/PRD.md | 18 ++-- docs/REMOTE-LIMITS.md | 38 ++++---- docs/RETROSPECTIVE.md | 28 +++--- docs/ROADMAP.md | 24 ++--- docs/VM-VERIFICATION.md | 80 ++++++++--------- docs/console-plan.md | 53 ++++++++--- scripts/anchor-audit.mjs | 2 +- scripts/install-linux.sh | 6 +- scripts/pack.mjs | 2 +- scripts/release.mjs | 6 +- scripts/verify-b2.mjs | 7 +- tests/unit/acl-v11.test.mjs | 24 ++--- tests/unit/dpi-awareness.test.mjs | 4 +- tests/unit/net-scripts.test.mjs | 10 +-- tests/unit/security-hardening.test.mjs | 2 +- 25 files changed, 302 insertions(+), 163 deletions(-) diff --git a/.github/workflows/windows-e2e.yml b/.github/workflows/windows-e2e.yml index 755fc2e..465122e 100644 --- a/.github/workflows/windows-e2e.yml +++ b/.github/workflows/windows-e2e.yml @@ -159,7 +159,7 @@ jobs: shell: pwsh run: | $ErrorActionPreference = "Stop" - # 真机教训(Parallels 27 + Win11 ARM):曾出现 arm64 包里装的其实是 x64 的 node.exe + # 真机教训(虚拟化平台 + Win11 ARM):曾出现 arm64 包里装的其实是 x64 的 node.exe # —— 两份包的 node.exe 字节完全相同。x64 node 在 ARM Windows 上**能 listen, # 但 TLS 握手不完成**(客户端只见超时)→ 被控端实际不可用,且 macOS 侧 100% 测不出。 # 根因是 pack.mjs 的运行时缓存文件名不含架构。这里直接验**最终产物**的 PE 架构, diff --git a/.gitignore b/.gitignore index e02f492..852b9cb 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,7 @@ keys/ # 本地 agent 工作数据(memory / automations 等,属本机状态,不入库) .workbuddy/ + +# 被控端 / 控制端本机运行配置(含真实密钥与节点地址,绝不入库) +config.json +agent.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 78e712e..af5c05d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,7 +50,7 @@ | # | 措施 | 位置 | 不加会怎样 | |---|---|---|---| | 1 | 控制台是 GUI 子系统程序,自身不弹控制台 | Tauri 默认 | 打开应用先弹黑窗 | -| 2 | spawn `node.exe` 时加 `CREATE_NO_WINDOW`(`0x0800_0000`) | `agent.rs` 的 `creation_flags` | **每次启动被控端都闪一个黑窗** —— 正是皇上要求消灭的现象 | +| 2 | spawn `node.exe` 时加 `CREATE_NO_WINDOW`(`0x0800_0000`) | `agent.rs` 的 `creation_flags` | **每次启动被控端都闪一个黑窗** —— 正是用户要求消灭的现象 | | 3 | 开机自启拉起控制台而非裸 `node` | `autostart.rs` + `--agent-only` | 开机时黑窗一闪而过 | ### 新增 —— 契约守卫(`tests/unit/console-commands.test.mjs`,8 用例) @@ -206,13 +206,13 @@ > 按上一节自己立的规矩,发版时**必须**升版本号(PATCH,→ 2.0.3), > 否则 `agent.mjs` 会谎报一个并不包含本修复的版本。 -真机(Parallels 27 + Win11 ARM64,**200% 缩放**)暴露的缺陷: +真机(虚拟化平台 + Win11 ARM64,**200% 缩放**)暴露的缺陷: 被控端的 PowerShell 助手进程**默认 DPI 不感知**,于是**同一个进程里并存两套坐标系**: | 调用 | 不感知时的结果 | 实际危害 | |---|---|---| -| `Screen.Bounds` / `SystemInformation.VirtualScreen` | **逻辑**尺寸 1800×1010 | `screen.info` 报错尺寸;`screen.capture` 缺省 region 一并错 | +| `Screen.Bounds` / `SystemInformation.VirtualScreen` | **逻辑**尺寸 1280×720 | `screen.info` 报错尺寸;`screen.capture` 缺省 region 一并错 | | `CopyFromScreen` | 按**物理**像素裁剪 | `screen.capture` 缺省**只截到左上 1/4** | | `SetCursorPos` | 被系统 **×2** | `input.mouse.*` 喂 100,100 → 光标落到 200,200 | | UIA(`screen.find`) | **物理**坐标(与进程是否感知无关) | 与上面三项**口径不一致** | @@ -237,7 +237,7 @@ | 断言 | 结果 | |---|---| - | `screen.info` | 报 **3600×2020**,与裸 `GetSystemMetrics`(感知后)一致 ✅ | + | `screen.info` | 报 **2560×1440**,与裸 `GetSystemMetrics`(感知后)一致 ✅ | | `input.mouse.move(x,y)` → 读回光标(物理) | 四次抽样**精确等于** (x,y) ✅ | | `screen.find` 物理矩形 → `input.mouse.click` **直接点击** | 移动后光标 = 物理中心;主题单选框 `selected` 翻转且**可逆** ✅ | @@ -535,11 +535,112 @@ sidecar 这条路径一直没被覆盖。 > **21.5 秒后「无法连接到远程服务器」**。更早一次(网络通时)同一按钮已成功返回 > 「最新版本 v2.0.3 / 已是最新 / 发布时间 2026-10-13 17:36:54」,故功能本身已验证。 +### 修复 —— 「检查更新」在境内**必然**失败(真机症状:`检查更新失败:fetch failed`) + +> **订正上文**:上一项末尾把 `fetch failed` 归为「与本次修复无关的环境问题」。**那是误判** —— +> 环境确实不通,但**产品缺陷让它无解**:即使把代理配好,这条链路也永远连不上。本项即修复。 + +**现象**:控制台点「检查更新」,境内(无透明代理的机器)必报 `fetch failed`。 + +**真机取证**(Win11 ARM64 VM;同日多组对照): + +| 观测 | 结果 | +|---|---| +| VM 上 DNS 解析 `github.com` | 正常(`20.205.243.166`) | +| VM 上 TCP 443 直连 | 握手成功 | +| VM 上 `fetch` 取该 URL | **`fetch failed`** | +| VM 经代理取同一 URL | **HTTP 302**(代理本身是好的) | +| Mac 上给 `fetch` 设 `HTTPS_PROXY` 后重试 | **仍 `fetch failed`**(`env -i` 隔离后仍有区分力) | + +**三条根因**(缺一条都修不好): + +1. **Node 内建 `fetch`(undici)默认不读任何代理环境变量**。`NODE_USE_ENV_PROXY=1` 可开 + (Node 22.x 实验性、24 正式),但它**只认环境变量**。 +2. **系统代理读不到**:Windows 用户用第三方代理客户端时配的是 **WinINET 系统代理**, + macOS 用户配的是**系统设置里的代理**——Node 两者都不读。所以「用户明明配了代理」也白搭。 + 而 `fetch(url,{dispatcher})` 也走不通:`dispatcher` 必须是 undici 的 `Dispatcher` 实例, + 而 Node 没把 undici 暴露成可导入模块(整包 vendored 会把 sidecar 撑胖,且它依赖 `.wasm`, + esbuild 单文件打包有风险)。 +3. **超时过短**:原 `check_update` 内部 20s。而该 URL 要**跟随两次重定向**到资产 CDN, + 真机实测整链耗时 **2.0s / 10.0s / 39.7s / 68.6s / >109s**(同机同代理)—— + 20s 属「假定能直连」时代的数值,对真实链路**必然**超时。 + +**修法** —— 新增 `apps/console/sidecar/proxy.mjs`(自持出网,零新依赖): + +- **不用 `fetch`,改用 `node:http`/`node:https`**:https 目标经代理走 `CONNECT` 隧道再叠 TLS + (隧道 socket 交 `http.request`,**刻意不用 `https.request`** —— 那样会二次握手); + http 目标经代理发**绝对形态**请求行(`GET http://… HTTP/1.1`)。 +- **代理来源**:`NA_PROXY` → 目标协议对应的环境变量(`HTTPS_PROXY`/`ALL_PROXY`…)→ + **系统代理**(Windows 读注册表 WinINET;macOS 读 `scutil --proxy`)。`NO_PROXY` 命中则直连。 +- **只支持 HTTP 代理**;遇到 `socks5://`、或系统代理是**PAC 脚本**(本机 AdGuard 正是这种) + **明确记一条 note 并把提示带给用户** —— 静默降级正是本次缺陷最恶劣的地方。 +- **超时成对下发**:`UPDATE_CHECK_TIMEOUT_MS` 120s(内层)/ `UPDATE_CHECK_RPC_TIMEOUT_SECS` 150s(外层), + 由 Rust 通过 `timeout_ms` 参数传给 sidecar。**内层必须小于外层**,这条关系写成**编译期不变式** + (`const _: () => assert!(…)`)—— 破坏它只会在用户侧表现为「check_update 超时」这种**丢掉真实原因**的假失败。 +- **失败信息可操作**:带上是直连还是经哪个代理、耗时多少、以及「为什么不走代理」(PAC / socks)。 + `check_update` 成功时也回报 `via_proxy` / `via`,界面上新增一行「出网」—— + 这正是排查这类问题最需要看到的一条。 +- 顺带修掉界面上的重复前缀(`检查更新失败:检查更新失败:…`)。 + +**同一类缺陷的第三处(**本次未改**,如实记录)**: +`apps/agent/src/capabilities/selfupdate.ts`(被控端拉取式自更新)仍是裸 `fetch`。 +该文件顶部有明确设计立场「改用镜像地址,而不是在 agent 里塞代理隧道」, +且**属被控端变更**(改动即需升 PATCH、走发版流程),故不在本次控制台修复范围内。 + +**期间一并修掉的第二个缺陷**(复验时才暴露):`tunnelConnection` 里 +**隧道与 TLS 握手阶段没有接 abort 信号** —— 那个阶段外层 `http.request` 还没建立, +`signal`/`timeout` 都无人受理。真机形态:配置 75s 超时,**实际挂了 109s** 才失败。 + +**守卫**(`tests/unit/sidecar-proxy.test.mjs`,15 用例)+ 注入验证: + +| # | 守什么 | 注入故障实测 | +|---|---|---| +| 1–8 | 纯函数:URL 归一化、`NO_PROXY`(含「条目比目标窄时不得反向豁免父域」)、来源优先级、`scutil`/`reg query` 解析(夹具取自真机,含 PAC 模式与「值不存在时 reg 走 stderr」)、探测失败一律视为无代理、决议优先级与 note 累积 | 去掉后缀匹配 → 用例 2 红 | +| 9–12 | 真发请求(**离线本地服务器**):直连跟随 302、经代理走绝对形态、**代理不通必须报错**(防静默降级)、CONNECT 可用 / 被拒报错、**隧道+TLS 阶段 abort 必须立即生效**(带看门狗,故障时失败而非挂死) | 去掉隧道阶段 abort 挂载 → 用例 12 红(`实际 4002ms`) | +| 13 | **端到端**:真 spawn sidecar 进程走 JSON-RPC 跑 `check_update`,断言返回正确且 **`via_proxy` 正确**、代理**真收到了**请求;反面对照(代理指死端口)必须失败 | 把 `check_update` 退回裸 `fetch` → 用例 13 红(`代理必须真收到这次请求`)+ 用例 14 红 | +| 14 | 形态守卫:sidecar 源码不得再出现裸 `fetch(`(防止修复被无声改回去) | 同上 | + +**真机复验**(重打 sidecar 产物 248KB,用**打包后的 `dist/sidecar.mjs`** 直跑): + +| 组 | 配置 | 结果 | +|---|---|---| +| C | 无代理 | **成功 2169ms** → 最新版本 `v2.0.3`、已是最新;日志正确报出「系统代理使用 PAC 自动配置脚本…已按无代理处理」(macOS `scutil` 探测真实生效) | +| A | `NA_PROXY=http://127.0.0.1:1`(死端口) | **51ms 明确失败**:`connect ECONNREFUSED 127.0.0.1:1(经代理 http://127.0.0.1:1/,耗时 5ms)`;`data` 里带全 `via`/`proxy`/`proxy_source`/`timeout_ms`/`hint` | +| B | `NA_PROXY=http://127.0.0.1:<代理端口>` | 代理路径被正确选中(日志 `经代理 …(来源 NA_PROXY)`);该次宿主代理线路拥塞未在预算内完成 —— **线路本身的问题**,错误信息如实说明「代理已生效但仍失败」 | + +> **未覆盖(如实记档)**:**https 目标经 CONNECT 隧道的 TLS 叠层**没有离线自动化用例 —— +> 需要一个受信任证书的本地 TLS 源站,而本仓库没有证书夹具(`selfsigned` 只装在被控端包内, +> 测试进程解析不到,为它新增夹具又会污染 `~/.nodeagent/certs/`)。 +> 在线路径已由「组 C 直连 + 原型实测经代理取 GitHub API 得 200」覆盖,但它**不构成回归守卫**。 + +--- + +### 安全 —— 公开仓库隐私脱敏(文档 / 注释 / 测试夹具) + +按 `security-reports/` 的隐私审查台账收口,逐项清除**可反向定位到具体个人与设备**的字面量。 +**纯脱敏与表述泛化,不含任何业务逻辑变更。** + +- **去掉可执行的真机默认值** —— `scripts/verify-b2.mjs` 的 `NA_URL` 原先带缺省值且指向一台真实机器, + 现改为**必填**(缺失即报错退出)。这是全仓库唯一「拿着就能连到某台真机」的常量,优先级最高。 +- **主体标识泛化** —— 测试账户名、虚拟机与宿主的真实网段、测试用绝对路径, + 分别改为 `na-test`、RFC 5737 文档保留段地址(`192.0.2.0/24`)、`C:\na-test`。 +- **设备指纹泛化** —— 虚拟化产品及其 CLI / 访客工具名、NAS 设备型号与系统名、真机 CPU 型号、 + 真机物理分辨率、代理软件品牌与监听端口、杀软品牌 → 一律中性表述。 + **只换称呼,结论与排查步骤一字未动**,避免把实测经验一起丢掉。 +- **去除跨项目与私人称呼线索** —— 文档中指向作者其他项目的名称、以及私人称谓 → 中性表述。 +- **测试夹具** —— `acl-v11` / `net-scripts` 的示例 IP 去掉与作者真实环境一致的尾八位; + **fixture 与断言同步更新**(只改 fixture 不改断言会让守卫变红 —— 本轮就这么红过一次)。 +- **`.gitignore` 补 `config.json` / `agent.json`** —— 二者含真实密钥与节点地址, + 此前只靠「没人会提交」兜着。 + +> 验证:`pnpm test:unit` **340/340 通过**;另用独立扫描器对 **235 个跟踪文件**做 **17 类模式**全量扫描,命中 **0**。 +> 说明:**改写历史不等于收回** —— 旧内容只要被匿名抓取过就已外流,本节只做「止血 + 不再恶化」。 + --- ## [v2.0.2] —— 2026-10-11 -> 一轮真机验证(Parallels 27 + Win11 ARM)抓出的**发布事故**修复批次。 +> 一轮真机验证(虚拟化平台 + Win11 ARM)抓出的**发布事故**修复批次。 > 已公开发布的 `nodeagent-win-arm64.zip` 内装的其实是 **x64 的 `node.exe`** —— > 该包在 ARM Windows 上能启动、能 listen,却 **TLS 握手不完成**(控制端只见 `E_TIMEOUT`), > 实际不可用。本版重出资产,并把「交付物架构」直接钉进发版脚本与 CI。 @@ -600,7 +701,7 @@ sidecar 这条路径一直没被覆盖。 ## [v2.0.1] —— 2026-10-11 -> 一轮**真机验证**(Parallels 27 + Win11 ARM)带来的修复批次:在补齐 6 项待验证项的过程中, +> 一轮**真机验证**(虚拟化平台 + Win11 ARM)带来的修复批次:在补齐 6 项待验证项的过程中, > **抓到并修掉 5 个真问题** —— 全部只有真机能发现(macOS 上 100% 测不出)。 ### 修复 @@ -623,7 +724,7 @@ sidecar 这条路径一直没被覆盖。 (**只增不减**,旧控制端忽略即可)。 - **`scripts/verify-b2.mjs`** —— v23 连接层三防护的真机验证脚本(`idle` / `maxconn` / `ban` 三模式), 全部走真实连接而非 mock。 -- **`docs/VM-VERIFICATION.md`** —— 无 Windows 真机时用 Parallels Win11 ARM 补验证的完整清单 +- **`docs/VM-VERIFICATION.md`** —— 无 Windows 真机时用虚拟化平台上的 Win11 ARM 补验证的完整清单 (含实测结果、能补/不能补矩阵、8 条排查项)。 ### 真机验证状态(本版实测) @@ -648,7 +749,7 @@ sidecar 这条路径一直没被覆盖。 > **验证状态**:本版发布时被控端整机离线,`v1.5.0` 的预设别名/长按/后台投递/热键序列 > 与 `v2.0.0` 的自重启任务清理策略**当时未经真机验证**(后者已在本地单测锁定判定条件)。 > -> ✅ **2026-10-11 已补齐**:在 Parallels 27 + Win11 ARM 虚拟机上完成真机验证 —— +> ✅ **2026-10-11 已补齐**:在虚拟化平台 + Win11 ARM 虚拟机上完成真机验证 —— > A 组 15 通过/0 失败、v1.5 四项全过、v23 三防护三场景全过、v26 清理策略**验证并改进** > (阈值由「1 小时」改为「本 agent 启动时刻」)、锚点端到端跑通(含云盘链外留存)。 > 并**顺带发现并修复 5 个真问题**(含 `control.ps1` 解析期错误、连接保活缺 pong 检测等)。 diff --git a/README.md b/README.md index ca6db04..5f1fe64 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,7 @@ pnpm test:rust # Rust 控制端客户端(需 cargo) 1. 在控制端构建安装包(**需在项目目录执行**): ```bash - cd <项目目录> # 例如 ~/WorkBuddy/NodeAgent/nodeagent + cd <项目目录> # 例如 ~/projects/nodeagent pnpm pack:win # 或从任意目录调用: @@ -678,8 +678,8 @@ nodeagent/ - [`docs/SECURITY.md`](docs/SECURITY.md) —— **安全模型与加固指南**(钥匙=root、证书指纹钉住、网段白名单、应急处置) - [`docs/VERSIONING.md`](docs/VERSIONING.md) —— **版本与兼容性契约**(升级顺序、什么算破坏性、发版流程、更新方式) - [`docs/REMOTE-LIMITS.md`](docs/REMOTE-LIMITS.md) —— 远程能力边界与真实故障案例(含杀软误拦) -- [`docs/DEPLOY-LINUX.md`](docs/DEPLOY-LINUX.md) —— **Linux 被控端部署**(Debian/fnOS/Ubuntu;含实测能力清单与排查) -- [`docs/VM-VERIFICATION.md`](docs/VM-VERIFICATION.md) —— **无 Windows 真机时,用 Parallels 的 Win11 ARM 虚拟机补验证**(含逐条命令与结论标注规则) +- [`docs/DEPLOY-LINUX.md`](docs/DEPLOY-LINUX.md) —— **Linux 被控端部署**(Debian/Ubuntu/NAS 系统;含实测能力清单与排查) +- [`docs/VM-VERIFICATION.md`](docs/VM-VERIFICATION.md) —— **无 Windows 真机时,用宿主虚拟化平台的 Win11 ARM 虚拟机补验证**(含逐条命令与结论标注规则) - [`CHANGELOG.md`](CHANGELOG.md) —— **变更日志**(未发布版本的内容汇总 / 历史版本) - [`docs/ROADMAP.md`](docs/ROADMAP.md) —— **当前进度盘点与下一步路线**(2026-10-07 基线:待办收尾 / 语义可断言 / 状态采样 / 安全残余) diff --git a/apps/agent/src/capabilities/input.ts b/apps/agent/src/capabilities/input.ts index 5b6a423..d8667c5 100644 --- a/apps/agent/src/capabilities/input.ts +++ b/apps/agent/src/capabilities/input.ts @@ -228,7 +228,7 @@ export function buildInputPsScript(body: string[]): string { // ⚠️ 必须最先执行,且**不可删**:不设 DPI 感知时 `SetCursorPos` / `mouse_event` 的坐标 // 会被系统按缩放比虚拟化(200% 下 ×2),而 `screen.find`(UIA)返回的是**物理**坐标 —— // 「找得到、点不准」,且不报任何错。真机 2026-10-12 实证: - // 请求 (100,100) 实际落到 (200,200),请求 (552,1489) 实际落到 (1104,2019)。 + // 请求 (100,100) 实际落到 (200,200),请求 (552,745) 实际落到 (1104,1439)。 PS_DPI_AWARE, `$code = @'`, CS_CODE, diff --git a/apps/agent/src/capabilities/screen.ts b/apps/agent/src/capabilities/screen.ts index 01919ba..359636f 100644 --- a/apps/agent/src/capabilities/screen.ts +++ b/apps/agent/src/capabilities/screen.ts @@ -39,7 +39,7 @@ export function buildListDisplaysScript(): string { return [ // ⚠️ 必须最先执行:不设 DPI 感知时,`Screen.Bounds` 返回的是**逻辑**尺寸, // 而 `CopyFromScreen` 又按**物理**像素裁剪 → 缺省整屏截图只截到左上 1/4 - //(200% 缩放下 3600×2020 的桌面报成 1800×1010)。真机 2026-10-12 实证。 + //(200% 缩放下 2560×1440 的桌面报成 1280×720)。真机 2026-10-12 实证。 PS_DPI_AWARE, 'Add-Type -AssemblyName System.Windows.Forms', '$s = [System.Windows.Forms.Screen]::AllScreens', diff --git a/apps/agent/src/capabilities/system.ts b/apps/agent/src/capabilities/system.ts index 27bd282..d570322 100644 --- a/apps/agent/src/capabilities/system.ts +++ b/apps/agent/src/capabilities/system.ts @@ -537,7 +537,7 @@ async function scheduleWindowsRestart(p: RestartParams): Promise { // // 现策略:非 Running **或** LastRunTime **早于"本 agent 启动时刻"** → 清。 // - // ⚠️ 2026-10-11 真机(Parallels Win11 ARM)修正:原先用「早于 1 小时」作阈值, + // ⚠️ 2026-10-11 真机(虚拟化平台上的 Win11 ARM)修正:原先用「早于 1 小时」作阈值, // 实测发现**频繁 restart(间隔 < 1h)时旧任务照样累积**(v26 想治的僵尸只是从 // "永不清理"变成"1 小时后才清")。根因是阈值选得太粗: // 阈值应表达「**这个任务是不是我(当前 agent)的宿主**」—— diff --git a/apps/agent/src/util/dpi.ts b/apps/agent/src/util/dpi.ts index a7a0fd0..e7eb5f3 100644 --- a/apps/agent/src/util/dpi.ts +++ b/apps/agent/src/util/dpi.ts @@ -8,10 +8,10 @@ * * | 调用 | 该进程看到的 | * |---|---| - * | `[System.Windows.Forms.Screen]::Bounds` | **逻辑**尺寸(物理 3600×2020 → 报 1800×1010) | + * | `[System.Windows.Forms.Screen]::Bounds` | **逻辑**尺寸(物理 2560×1440 → 报 1280×720) | * | `[System.Windows.Forms.SystemInformation]::VirtualScreen` | **逻辑**(同上) | - * | `CopyFromScreen(0,0,1800,1010)` | **物理**裁剪 → 只截到屏幕左上 1/4 | - * | `SetCursorPos(552,1489)` | 落点实际在物理 (1104, 2978)(被系统 ×2) | + * | `CopyFromScreen(0,0,1280,720)` | **物理**裁剪 → 只截到屏幕左上 1/4 | + * | `SetCursorPos(552,745)` | 落点实际在物理 (1104, 1490)(被系统 ×2) | * | UIA(`screen.find`) | **物理**(与进程 DPI 感知无关) | * * 于是最要命的一条链路会静默断掉:`screen.find` 返回物理坐标 → 直接喂给 @@ -25,11 +25,11 @@ * * ## 真机实证(2026-10-12) * - * Parallels Win11 ARM 虚拟机,主机缩放 200%,物理 3600×2020: - * - 缺省 `screen.capture` 只拿到 1800×1010(= 屏幕左上 1/4),且 `screen.info` 报 1800×1010 + * 某虚拟化平台上的 Win11 ARM 虚拟机,宿主缩放 200%,物理 2560×1440: + * - 缺省 `screen.capture` 只拿到 1280×720(= 屏幕左上 1/4),且 `screen.info` 报 1280×720 * - `input.mouse.move 100,100` → DPI 感知读回 **(200,200)** - * - `input.mouse.move 552,1489` → 实际落点 **(1104, 2019)**(截断到屏幕底部) - * - 补上本段后:`screen.capture` 拿到完整 3600×2020;`screen.find` 的物理坐标 + * - `input.mouse.move 552,745` → 实际落点 **(1104, 1439)**(截断到屏幕底部) + * - 补上本段后:`screen.capture` 拿到完整 2560×1440;`screen.find` 的物理坐标 * 可直接用于 `input.mouse.click`(同一坐标一次点中) * * ⚠️ 不要删。这不是「锦上添花」的兼容代码,而是坐标口径统一的**前提**。 diff --git a/docs/DEPLOY-LINUX.md b/docs/DEPLOY-LINUX.md index 5d29921..e90fc3b 100644 --- a/docs/DEPLOY-LINUX.md +++ b/docs/DEPLOY-LINUX.md @@ -1,4 +1,4 @@ -# Linux 被控端部署(Debian 12 / fnOS / Ubuntu 等 systemd 系统) +# Linux 被控端部署(Debian 12 / Ubuntu / NAS 系统等 systemd 系统) > 适用:把一台 Linux 机器作为**被控端**,让 Mac/Linux 控制端接管。 > 本文的能力清单是 **2026-10-13 在 Debian 12 容器里实测**得出的,不是推测。 @@ -13,7 +13,7 @@ Linux 端未实现 —— 调用会明确返回 `E_UNSUPPORTED_PLATFORM`,不 --- -## 1. 快速开始(以绿联 DXP2800 / fnOS 为例) +## 1. 快速开始(以 NAS 设备为例) ```bash # ① 把仓库放到 NAS 上(任选其一) @@ -25,7 +25,7 @@ node scripts/pack.mjs # → release/agent.mjs # ③ 在 NAS 上执行安装(需要 sudo;会自动注册 systemd 服务并开机自启) cd nodeagent -sudo bash scripts/install-linux.sh --node-id dxp2800 +sudo bash scripts/install-linux.sh --node-id nas-01 # ④ 抄下屏幕上打印的「密钥」与「连接命令」,回控制端执行: # nodeagent connect --port 8765 --key <密钥> --insecure diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 3e7cf5f..17acf04 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -62,7 +62,7 @@ - Hub 中枢(多设备注册/发现/路由)→ 预留接口 - Android / Linux 端 → 后续 - 公网穿透 → 后续(v1 仅局域网) -- 画面串流(远程桌面级)→ 属 OnlyInOne 领域 +- 画面串流(远程桌面级)→ 属另一独立项目领域 ### 1.4 分阶段路线 @@ -78,7 +78,7 @@ | 术语 | 定义 | |---|---| -| **控制端** | 皇上的 MacBook,发起调用的 AI/CLI 所在端 | +| **控制端** | 用户的 MacBook,发起调用的 AI/CLI 所在端 | | **被控端** | Windows 机器,执行命令/装软件/查状态的端 | | **Agent** | 常驻在 Windows 上的后台服务,接收并执行控制端指令 | | **能力** | 一个可被远程调用的原子操作(执行命令、装软件、查状态) | @@ -469,7 +469,7 @@ | macOS 控制端 | Keychain | `security add-generic-password -s nodeagent -a -w ` | | Windows 被控端 | DPAPI / 凭据管理器 | `ConvertTo-SecureString` 或 `node-dpapi` | -**铁律**:密钥不明文落盘、不写日志、不经网络传输(HMAC 只传摘要,不传密钥原文)。**与 OnlyInOne「公钥上云 / 私钥永不上传」约定一致。** +**铁律**:密钥不明文落盘、不写日志、不经网络传输(HMAC 只传摘要,不传密钥原文)。**与另一项目「公钥上云 / 私钥永不上传」的约定一致。** ### 6.3 演进路径 diff --git a/docs/PRD.md b/docs/PRD.md index a890bc3..70780f3 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -7,7 +7,7 @@ | 文档版本 | v1.0(已认可) | | 作者 | 司南(产品经理 Agent) | | 日期 | 2026-09-27 | -| 客户 | 皇上(个人开发者) | +| 客户 | 个人开发者 | | 项目名 | nodeagent | | 状态 | ✅ 已认可,进入开发 | | 参考文件 | 《NodeBuddy-开发文档.zip》(仅作参考,非任务书) | @@ -16,12 +16,12 @@ ## 1. 背景与痛点 -**核心痛点一句话**:皇上在 MacBook 上使用 AI(WorkBuddy),AI 只能操作 Mac 本机;当需要「在 Windows 上装软件 / 查状态」时,AI 无法跨过去接管 Windows 完成。 +**核心痛点一句话**:用户在 MacBook 上使用 AI(WorkBuddy),AI 只能操作 Mac 本机;当需要「在 Windows 上装软件 / 查状态」时,AI 无法跨过去接管 Windows 完成。 | # | 痛点 | 具体表现 | |---|---|---| | P1 | 跨机断点 | AI Agent 跑在 Mac 上,无法触达 Windows 执行操作 | -| P2 | 手动替代 | 装软件、查状态需皇上亲自切换到 Windows 操作,打断工作流 | +| P2 | 手动替代 | 装软件、查状态需用户亲自切换到 Windows 操作,打断工作流 | | P3 | 无统一入口 | 每次都要用远程桌面/SSH 手动拼命令,无法被 AI 自动编排 | ## 2. 目标与成功指标 @@ -47,7 +47,7 @@ | 术语 | 定义 | |---|---| -| 控制端 | 皇上的 MacBook,发起调用的 AI/CLI 所在端 | +| 控制端 | 用户的 MacBook,发起调用的 AI/CLI 所在端 | | 被控端 | Windows 机器,执行命令/装软件/查状态的端 | | Agent | 常驻在 Windows 上的后台服务,接收并执行控制端指令 | | 能力 | 一个可被调用的原子操作(执行命令、装软件、查状态) | @@ -77,7 +77,7 @@ | 3 | Hub 中枢(多设备注册/发现/路由) | 后续(预留接口) | | 4 | Android / Linux 端 | 后续 | | 5 | 公网穿透 | 后续(v1 仅局域网) | -| 6 | 画面串流(远程桌面级) | 属 OnlyInOne 领域,不做 | +| 6 | 画面串流(远程桌面级) | 属另一独立项目领域,不做 | ## 5. 关键决策记录 @@ -91,13 +91,13 @@ ## 6. 用户角色与核心场景 -**角色**:皇上(MacBook 用户,WorkBuddy 重度用户) +**角色**:MacBook 用户(WorkBuddy 重度用户) | 场景 | 描述 | |---|---| -| S1 装软件 | 皇上在 Mac 上对 AI 说「在 Windows 上装个 VSCode」,AI 通过框架调用 Windows 完成安装并回报结果 | -| S2 查状态 | 皇上问「Windows 现在 CPU/内存/磁盘怎么样」,AI 查询并返回 | -| S3 执行命令 | 皇上让 AI 在 Windows 上跑一条命令(如重启某服务、查日志) | +| S1 装软件 | 用户在 Mac 上对 AI 说「在 Windows 上装个 VSCode」,AI 通过框架调用 Windows 完成安装并回报结果 | +| S2 查状态 | 用户问「Windows 现在 CPU/内存/磁盘怎么样」,AI 查询并返回 | +| S3 执行命令 | 用户让 AI 在 Windows 上跑一条命令(如重启某服务、查日志) | ## 7. 功能需求(FR) diff --git a/docs/REMOTE-LIMITS.md b/docs/REMOTE-LIMITS.md index e9e6ff0..9d6e21f 100644 --- a/docs/REMOTE-LIMITS.md +++ b/docs/REMOTE-LIMITS.md @@ -15,7 +15,7 @@ | **改系统设置** | ⚠️ 多数可 | 注册表、服务、计划任务、防火墙规则等可用命令改 | | **改网络配置** | ⚠️ **高风险** | 能做,但**可能把自己弄断**(详见 §2.1) | | **操作 GUI** | ❌ 基本不可 | 鼠标键盘默认禁用,且即使开启也难判读界面状态(详见 §2.2) | -| **需要人工判断的决策** | ❌ 不可 | 选哪个代理节点、订阅是否有效 —— 臣无从判断(详见 §2.3) | +| **需要人工判断的决策** | ❌ 不可 | 选哪个代理节点、订阅是否有效 —— AI 无从判断(详见 §2.3) | | **物理动作** | ❌ 不可 | 插拔网线、按电源键、换硬盘 | --- @@ -51,17 +51,17 @@ ### 2.2 无 GUI 交互能力 -**问题**:`input.*`(鼠标键盘)出于安全考虑**默认禁用**;即使开启,臣也难以完成 GUI 任务。 +**问题**:`input.*`(鼠标键盘)出于安全考虑**默认禁用**;即使开启,AI 也难以完成 GUI 任务。 | 场景 | 为什么做不到 | |---|---| -| 代理软件换节点 / 更新订阅 | 需在 v2rayN 界面里点选,且**节点可用性关乎上游服务**,臣无法判断 | +| 代理软件换节点 / 更新订阅 | 需在代理客户端界面里点选,且**节点可用性关乎上游服务**,AI 无法判断 | | UAC 提权弹窗 | 需人点「是」,agent 无法代点(安全设计) | | 安装向导(下一步 → 完成) | 界面状态判读不可靠 | | 浏览器登录 / 扫码 | 需要人的凭据与设备 | -**臣能做的替代**:用**命令行等价操作**绕过 GUI(如 `netsh`、注册表、服务命令)。 -但**第三方 GUI 软件的内部状态**(如 v2rayN 里"当前选中哪个节点")通常没有命令行接口。 +**AI 能做的替代**:用**命令行等价操作**绕过 GUI(如 `netsh`、注册表、服务命令)。 +但**第三方 GUI 软件的内部状态**(如代理客户端里"当前选中哪个节点")通常没有命令行接口。 --- @@ -71,7 +71,7 @@ - 代理**订阅是否过期**、**节点是否可用** —— 需要人的账号与常识 - 哪个软件该装、哪个进程能杀 —— 需要业务判断 -- 皇上看到的报错在**什么操作后出现** —— 臣无法感知复现路径 +- 用户看到的报错在**什么操作后出现** —— AI 无法感知复现路径 --- @@ -80,8 +80,8 @@ | 限制 | 影响 | |---|---| | 只能看**当下快照** | 间歇性问题(如抖动、偶发超时)容易漏判 | -| 无法 7×24 盯 | 臣只在被调用时"看一眼",问题在两次调用之间发生就看不到 | -| 应用层内部状态不可见 | 如 WorkBuddy 的"当前正在等哪个请求",臣只能从日志反推 | +| 无法 7×24 盯 | AI 只在被调用时"看一眼",问题在两次调用之间发生就看不到 | +| 应用层内部状态不可见 | 如 WorkBuddy 的"当前正在等哪个请求",AI 只能从日志反推 | **缓解(✅ 已于 v2.0.0 实现)**:`monitor.*` 定时采样并落盘,事后回看摘要 —— 正是为治 这类间歇性问题而做(原先只写"具备实现基础",现已成为能力): @@ -140,27 +140,27 @@ nodeagent --node win monitor report probe # 直接给出:断了几次 / 最 | 网络适配器 | Ethernet Up / 1 Gbps | 链路正常 | | 国内网络(百度) | HTTP 200 / 0.24s | 基础联网正常 | | `copilot.tencent.com` | HTTP 200 / 0.35s(IPv4) | 后端可达 | -| v2rayN + xray 进程 | 均在运行 | 代理软件活着 | -| xray 监听 | `0.0.0.0:10808` | 代理端口在听 | -| **系统代理** | **`ProxyEnable=0`,但 `ProxyServer` 残留 `127.0.0.1:10808`** | ⚠️ 配置矛盾 | -| 走 10808 访问外网 | 超时(HTTP 000 / 20s) | ⚠️ **上游节点不通** | +| 代理客户端 + 代理内核进程 | 均在运行 | 代理软件活着 | +| 代理内核监听 | `0.0.0.0:<代理端口>` | 代理端口在听 | +| **系统代理** | **`ProxyEnable=0`,但 `ProxyServer` 残留 `127.0.0.1:<代理端口>`** | ⚠️ 配置矛盾 | +| 走该代理端口访问外网 | 超时(HTTP 000 / 20s) | ⚠️ **上游节点不通** | | **`resolveProxy` RPC** | **耗时 1737ms(慢)** | ⚠️ 代理解析卡顿 | | **MarketplaceInitGate** | **15s 安全超时后强放行** | ⚠️ 启动期被拖慢 | | WorkBuddy 连接 | 4 条 IPv6 到腾讯(已建立) | 部分链路正常 | | TCP `CloseWait` | 24 条 | 偏多,疑似连接被中途中断 | **结论(高度可能)**: -系统存在**「代理配置残留但未启用,且 xray 上游节点实际不通」**的矛盾状态 —— +系统存在**「代理配置残留但未启用,且代理内核上游节点实际不通」**的矛盾状态 —— WorkBuddy 解析代理慢 + 部分请求走了不通的代理链路 → `Response timed out`。 -**为什么臣没有直接修**: -- 需要的动作是「在 v2rayN 界面换可用节点 / 更新订阅」→ **属 GUI + 人工判断**(§2.2 / §2.3) -- 若改为「清空系统代理残留」→ 属网络配置变更,虽有价值但**需皇上确认**(§2.1 风险) +**为什么 AI 没有直接修**: +- 需要的动作是「在代理客户端界面换可用节点 / 更新订阅」→ **属 GUI + 人工判断**(§2.2 / §2.3) +- 若改为「清空系统代理残留」→ 属网络配置变更,虽有价值但**需用户确认**(§2.1 风险) **建议的下一步**(人工为主): -1. 打开 v2rayN → 测试节点延迟 → 换一个可用节点(或更新订阅) -2. 确认 v2rayN 的「自动配置系统代理」策略与实际需求一致 -3. 观察是否仍有 `Response timed out`;若仍有,可让臣**持续采样**再定位 +1. 打开代理客户端 → 测试节点延迟 → 换一个可用节点(或更新订阅) +2. 确认代理客户端的「自动配置系统代理」策略与实际需求一致 +3. 观察是否仍有 `Response timed out`;若仍有,可让 AI **持续采样**再定位 --- diff --git a/docs/RETROSPECTIVE.md b/docs/RETROSPECTIVE.md index 94bc43a..d1959fa 100644 --- a/docs/RETROSPECTIVE.md +++ b/docs/RETROSPECTIVE.md @@ -4,7 +4,7 @@ > 2026-10-07 逐项核查:P0 全部解决、P1 仅「定时采样」未做、P2 已补大半。 > **当前状态与下一步路线以 [`ROADMAP.md`](./ROADMAP.md) 为准**,本文保留作为「问题从哪来」的历史记录。 -> 基于 2026-09-27 ~ 09-28 的真实使用(Windows 真机部署 + v2rayN 故障排查 + 重装前信息采集) +> 基于 2026-09-27 ~ 09-28 的真实使用(Windows 真机部署 + 代理客户端故障排查 + 重装前信息采集) > 所有问题均来自**实际踩坑**,非理论推演。 --- @@ -12,7 +12,7 @@ ## 一、最痛的问题:GUI 语义能力缺失(P0) ### 场景还原 -排查 v2rayN 节点问题时,臣需要**切换活动节点**: +排查代理客户端节点问题时,AI 需要**切换活动节点**: | 尝试 | 结果 | |---|---| @@ -20,7 +20,7 @@ | 重新算坐标再点 | ❌ 又点到第 3 行 | | 改用键盘 `↑`/`↓`/`Enter` | ✅ 成功 | -**问题本质**:臣有 `screen.capture`(能**看**)和 `input.mouse.*`(能**点**),但**没有把「看到的界面元素」映射为「可点击坐标」的能力**。 +**问题本质**:AI 有 `screen.capture`(能**看**)和 `input.mouse.*`(能**点**),但**没有把「看到的界面元素」映射为「可点击坐标」的能力**。 每次都要:截图 → 人眼估算像素 → 换算缩放的 region 偏移 → 点击 → 再截图验证。**误差极大,反复试错**。 @@ -33,7 +33,7 @@ 2. **截图附带坐标网格** - `screen.capture` 可选叠加坐标标尺(每 100px 标线) - - 臣看图即可精确推算坐标,无需盲试 + - AI 看图即可精确推算坐标,无需盲试 3. **鼠标相对定位** - `input.mouse.move` 支持 `--relative dx dy`(当前只有绝对坐标) @@ -69,7 +69,7 @@ - 让「写配置文件」这类高频操作**默认安全** 3. **结构化返回**(不靠文本传递) - - 现在 `exec` 的输出是**纯文本 + 尾部附加 `[退出码 0 · 410ms]`**,导致臣解析 JSON 时被污染 + - 现在 `exec` 的输出是**纯文本 + 尾部附加 `[退出码 0 · 410ms]`**,导致 AI 解析 JSON 时被污染 - 应改为**结构化字段**:`{ stdout, stderr, exit_code, duration_ms }` —— 后端本来就有,只是 CLI 拼接成文本了 --- @@ -77,13 +77,13 @@ ## 三、进程与生命周期:自断生路(P0) ### 场景还原 -臣用 `system.shell.exec` 执行「杀旧 agent + 启新 agent」来让配置生效: +AI 用 `system.shell.exec` 执行「杀旧 agent + 启新 agent」来让配置生效: ``` Start-Process powershell -ArgumentList '... Start-Sleep 3; Stop-Process ...; Start-Process node.exe ...' ``` -**结果**:**分离的 PowerShell 被 agent 的进程树清理逻辑一起杀掉了** → 新 agent 从未启动 → 端口僵死 → **臣彻底失联**,只能请皇上手动恢复。 +**结果**:**分离的 PowerShell 被 agent 的进程树清理逻辑一起杀掉了** → 新 agent 从未启动 → 端口僵死 → **AI 彻底失联**,只能请用户手动恢复。 ### 改进方向 @@ -93,7 +93,7 @@ Start-Process powershell -ArgumentList '... Start-Sleep 3; Stop-Process ...; Sta ``` - 由 agent **自己**实现:fork 一个「延时重启器」(脱离进程组),或注册一次性计划任务 - 重启后**主动回连/上报**,让控制端知道「我回来了」 - - 这样 90% 的配置变更新场景都不再需要皇上介入 + - 这样 90% 的配置变更新场景都不再需要用户介入 2. **健康检查与自愈** - agent 内置看门狗:若 N 秒内未响应,自我重启 @@ -104,16 +104,16 @@ Start-Process powershell -ArgumentList '... Start-Sleep 3; Stop-Process ...; Sta - 或者:`exec` 增加 `detach: true` 参数 4. **改端口/改配置前先"预演"** - - 支持用一个临时配置在**旁路端口**试启动,成功再切换 —— 这正是这次出事后臣总结的正确做法 + - 支持用一个临时配置在**旁路端口**试启动,成功再切换 —— 这正是这次出事后 AI 总结的正确做法 --- ## 四、可观测性与长时任务(P1) ### 场景还原 -- 代理不通问题**反复发作**,但臣只能看**当下快照**(`netstat`、`Get-Process`),**无法看到 5 分钟前的状态** +- 代理不通问题**反复发作**,但 AI 只能看**当下快照**(`netstat`、`Get-Process`),**无法看到 5 分钟前的状态** - 想跑「持续 5 分钟、每 2 秒记录一次连接状态」的采样 → **`exec` 默认 30 秒超时**,长命令直接被砍 -- 排查 360 拦截时,需要「退出 360 前后对比」→ **臣无法安排这个时序** +- 排查第三方杀软拦截时,需要「退出杀软前后对比」→ **AI 无法安排这个时序** ### 改进方向 @@ -132,12 +132,12 @@ Start-Process powershell -ArgumentList '... Start-Sleep 3; Stop-Process ...; Sta - 专治「间歇性问题」—— 这次代理不通就是典型的间歇性/条件性问题 3. **事件订阅(push 模型)** - - 现在全是**拉取式**(臣问一次,答一次) + - 现在全是**拉取式**(AI 问一次,答一次) - 应该支持:`subscribe { events: ['process.crash','net.change','file.change'] }` - - 让被控端**主动推**异常,而不是等臣去查 + - 让被控端**主动推**异常,而不是等 AI 去查 4. **日志聚合查询** - - 这次查 v2rayN 日志,臣只能 `Get-Content -Tail N` 手工翻 + - 这次查代理客户端日志,AI 只能 `Get-Content -Tail N` 手工翻 - 应提供 `log.query { path, since, level, pattern, limit }`,由被控端侧做高效过滤 --- diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index b6f1ef1..9d45063 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -168,7 +168,7 @@ await gui.await(condition:"control", text:"完成", timeout_ms:30000) | 方案 | 代价 | 效果 | |---|---|---| | **A. 只清未来**(最低) | 0 | 新增代码与提交信息一律脱敏,历史不动。仓库公开仍有痕迹 | -| **B. 清注释 + 清未来**(臣建议) | 半小时 | 源码注释泛化为「主流播放器」「全屏游戏」,不动历史 | +| **B. 清注释 + 清未来**(建议) | 半小时 | 源码注释泛化为「主流播放器」「全屏游戏」,不动历史 | | **C. B + 改写历史** | 需 `git filter-repo` + force push | 连历史一并清,但**旧 blob 已被匿名抓取过,改写不等于收回** | > ⚠️ 关键认知:**已推送到公开仓库并被匿名抓取过的内容,改写历史无法收回**。旧 blob 只能靠 GitHub Support GC,而 Support GC 本身也非保证。因此现实目标应是「**止血 + 不再恶化**」,即方案 B。 @@ -206,7 +206,7 @@ node scripts/verify.mjs --host <地址> --key <密钥> --report <报告.md> ### 4.3 大前端方案(`docs/console-plan.md`,已出文档待评审) -盘点期间新增了一份前端方案文档,臣不在此展开(详见该文档),仅提示它引入了**一个新包 `crates/nodeagent-client`(Rust)**〔**2026-10 已退役**:改为 Node sidecar 方案,见 `docs/console-plan.md` 文末「实施决策」〕,与现有 TypeScript 的 `@nodeagent/client` 并存 —— 意味着 client 层有两套实现。这本身是个需要皇上拍板的架构决策(详见该文档的待确认项)。 +盘点期间新增了一份前端方案文档,本文不在此展开(详见该文档),仅提示它引入了**一个新包 `crates/nodeagent-client`(Rust)**〔**2026-10 已退役**:改为 Node sidecar 方案,见 `docs/console-plan.md` 文末「实施决策」〕,与现有 TypeScript 的 `@nodeagent/client` 并存 —— 意味着 client 层有两套实现。这本身是个需要用户拍板的架构决策(详见该文档的待确认项)。 --- @@ -270,7 +270,7 @@ node scripts/verify.mjs --host <地址> --key <密钥> --report <报告.md> ### 方向二:状态 —— 治间歇性问题 -**判断**:复盘 §四那条判断仍成立 —— 代理不通、杀软拦截都是**间歇性**的,而臣只能看当下快照。`event.watch` 解决「有事件时通知」,没解决「持续记录并回看」。 +**判断**:复盘 §四那条判断仍成立 —— 代理不通、杀软拦截都是**间歇性**的,而 AI 只能看当下快照。`event.watch` 解决「有事件时通知」,没解决「持续记录并回看」。 | 能力 | 设计 | 验收标准 | |---|---|---| @@ -292,7 +292,7 @@ node scripts/verify.mjs --host <地址> --key <密钥> --report <报告.md> | **安装期密钥传递** | `install.cmd` 用命令行 `-Key` 传参,**同机其他用户 `ps` 可见** | 改走 stdin 或临时文件(600 权限,读后即删)。这是唯一还没堵住的密钥暴露面 | | **审计链外部锚定** | 哈希链可被「有 root 的攻击者」整链重写 | 定期把链头哈希同步到独立位置(如 Hub 或另一台机器),重写即暴露 | -**臣另建议一项**:**ed25519 默认化**。`SECURITY.md` §2 已写明 PSK 模式的固有限制(`client_id` 自报 → ACL 身份维度形同虚设)。v22 的每客户端独立 PSK 只是缓解 —— 持有共享 key 的人仍可冒充未登记的 client_id。新装默认应走 ed25519,PSK 降级为「兼容存量」并在安装时明确告警。 +**另建议一项**:**ed25519 默认化**。`SECURITY.md` §2 已写明 PSK 模式的固有限制(`client_id` 自报 → ACL 身份维度形同虚设)。v22 的每客户端独立 PSK 只是缓解 —— 持有共享 key 的人仍可冒充未登记的 client_id。新装默认应走 ed25519,PSK 降级为「兼容存量」并在安装时明确告警。 --- @@ -338,18 +338,18 @@ node scripts/verify.mjs --host <地址> --key <密钥> --report <报告.md> | 5 | 方向三:`install.cmd` 密钥改 stdin + 握手封禁 + 会话上限 | 1~2 天 | 无(安全项建议提前插队) | | 6 | 方向二:`monitor.*` + `log.query` + `fs.watch` | 2~3 天 | 无 | | 7 | `.gitignore` 补密钥规则 + 源码真机常量注入化 | 分散 | 无 | -| 8 | PII blob 报 Support GC / 决定是否重建仓库 | — | **需皇上决策** | +| 8 | PII blob 报 Support GC / 决定是否重建仓库 | — | **需用户决策** | | 9 | `ed25519` 默认化 | 1~2 天 | 需确认向后兼容策略 | -| 10 | `console-plan.md` 评审:Rust client 与 TS client 双实现取舍 | — | **需皇上拍板** | +| 10 | `console-plan.md` 评审:Rust client 与 TS client 双实现取舍 | — | **需用户拍板** | -**臣的建议**: +**建议**: - **序 0 立刻做**。5 分钟的事,但不做则单测红、CI 必红。注意建议顺手把断言改成「每个 `CapabilityNames` 键都在 manifest 中」——否则每加一能力都要改数字,这类「硬编码计数」会反复咬人。 - **序 0.5 紧接着做**。只改注释,20 分钟,零风险,但能阻止「设备软件清单」继续扩散。`gui.await` 也正好是新代码,此时立脱敏规矩最自然。 - **序 1 是当前主线**。`gui.await` 实现已完成(170 行 + 57 行契约 + 注册),设计也站得住(超时正常返回、轮询期异常不中断、复用既有能力零新增探测代码)。差的只是收尾四件事:单测、CLI/MCP 入口、版本号、真机验证。**建议一次做完再提交,不要分批**。 - **序 3 优先级高于新功能**。v1.5.0 是**未经真机验证就发的版**,其中「后台定向投递」只在媒体键通道验证过、等价推断未经实测。 - **序 5 可提前**。`install.cmd` 密钥经命令行传参(同机 `ps` 可见)是当下唯一未堵住的密钥暴露路径,改动局限在一个脚本内。 -- **序 8、10 需皇上决策**:前者关系到公开历史能否部分收回,后者关系到是否长期维护两套 client 实现。 +- **序 8、10 需用户决策**:前者关系到公开历史能否部分收回,后者关系到是否长期维护两套 client 实现。 > **一条经验**:`gui.await` 一次加能力就让单测红,原因是能力数被硬编码成 `42`。这类「计数断言」在持续加能力的项目里必然反复失效。**改为集合断言**(每个能力名都在 manifest 中)比每次改数字更省事,也更不容易漏检。 @@ -392,7 +392,7 @@ node scripts/verify.mjs --host <地址> --key <密钥> --report <报告.md> | **`SECURITY.md` §5「还没做的」清单** | ✅ **全部完成** | 见 §4.5~§4.8 | | **发版 v2.0.0** | ✅ 2026-10-10 | 37 个提交一次发出;资产 agent.mjs(sha256 `7164ea44…`) / win-x64.zip / latest.json / SHA256SUMS | | v1.5.0 四项 + v2.0.0 清理策略**真机验证** | ✅ **已完成**(VM · Win11 ARM) | A 组 15/0/1 · B1 四项全过 · B2 三防护全过 · B3 清理**验证并改进** · B4 锚点端到端 · B5 结论:x64 包在 ARM 不可用→已出 arm64 包。**共发现并修复 5 个真问题**(详见 `VM-VERIFICATION.md`) | -| ↑ **可用 VM 补验证** | 📋 见 [`VM-VERIFICATION.md`](VM-VERIFICATION.md) | Parallels 27 + Win11 **ARM**:协议层(v23/v26/锚点)与输入层(v1.5 四项)**可直接验**;杀软/显示/性能**仍需真机** | +| ↑ **可用 VM 补验证** | 📋 见 [`VM-VERIFICATION.md`](VM-VERIFICATION.md) | 宿主虚拟化平台 + Win11 **ARM**:协议层(v23/v26/锚点)与输入层(v1.5 四项)**可直接验**;杀软/显示/性能**仍需真机** | ### 真机验证过程中抓到的真 bug(8 个,全部只有真机能发现) @@ -411,7 +411,7 @@ node scripts/verify.mjs --host <地址> --key <密钥> --report <报告.md> | 事项 | 状态 | 说明 | |---|---|---| | `gui.await` 收尾(断言/单测/入口/版本号/本地真机/发版) | ✅ | v1.6.0 | -| CI 工作流推送(需皇上授权 `workflow` 作用域) | ✅ | 皇上授权后推送成功 | +| CI 工作流推送(需用户授权 `workflow` 作用域) | ✅ | 用户授权后推送成功 | | **Windows CI 上线后修 6 个真 bug** | ✅ | 见下表 | | 源码注释脱敏(序 0.5) | ✅ | `input.ts` / `input-script.ts` 4 处软件名泛化 | | `.gitignore` 补 `keys/ *.key *.pem`(§6.2) | ✅ | `*.pub` 放行 | @@ -420,9 +420,9 @@ node scripts/verify.mjs --host <地址> --key <密钥> --report <报告.md> | 方向一第二步:`screen.find` 属性化 + 条件组合 | ✅ **已完成**(v2.0.0) | 见 §五 的对应行 | | 方向二:`monitor.*` / `log.query` / `fs.watch` | ✅ **已完成**(v2.0.0) | `monitor.*` 5 项 + `log.query` 已实现;`fs.watch` 由 `event.watch` 覆盖 | | 方向三:`install.cmd` 密钥 stdin、握手封禁、会话上限、审计锚定 | ✅ **已完成**(v2.0.0,2026-10-11 真机验证) | 三防护三场景全过;锚定端到端 + 云盘链外留存已跑通 | -| PII blob GC / 仓库重建(序 8) | ⏳ 需皇上决策 | — | +| PII blob GC / 仓库重建(序 8) | ⏳ 需用户决策 | — | | `ed25519` 默认化 | ✅ **已完成**(v2.0.0) | 「零信任优先安装」:有 client-acl.json 即走 ed25519(default_effect=deny) | -| console-plan 评审(Rust client 取舍) | ⏳ 需皇上拍板 | — | +| console-plan 评审(Rust client 取舍) | ⏳ 需用户拍板 | — | ### Windows CI 扒出的 6 个真 bug(均已修复,v1.6.x) diff --git a/docs/VM-VERIFICATION.md b/docs/VM-VERIFICATION.md index ebfd95d..d23adec 100644 --- a/docs/VM-VERIFICATION.md +++ b/docs/VM-VERIFICATION.md @@ -1,6 +1,6 @@ -# VM 验证清单(Parallels Desktop 27 + Windows 11 ARM) +# VM 验证清单(宿主虚拟化平台 + Windows 11 ARM) -> 用途:Windows 真机不可用时,用 **Parallels 27 的 Windows 11 ARM 虚拟机**提前跑掉 +> 用途:Windows 真机不可用时,用**虚拟化平台上的 Windows 11 ARM 虚拟机**提前跑掉 > **架构无关**的验证项,把"等机器"的时间省下来。 > > ⚠️ **本清单的结论必须按 §5 的规则标注**:VM 验过的项写「VM 已验」,并注明 @@ -16,7 +16,7 @@ | **输入注入层** | v1.5 四项(字符串热键/预设别名/长按/后台定向投递) | ✅ 可验(走 Win32 `SendInput`/`WM_APPCOMMAND`,ARM Windows 同样完整) | | **安装与自持** | `install.cmd` 自提权、计划任务注册、受控重启 | ✅ 可验(VM 里也是真 Windows,机制一致) | | **系统能力** | 进程/服务/文件/剪贴板/命令执行/`winget` | ✅ 可验 | -| **刻意不可验** | 第三方杀软拦截(火绒等) | ❌ VM 里只有 Defender | +| **刻意不可验** | 第三方杀软拦截 | ❌ VM 里只有 Defender | | | 显示 / DPI / 多显示器 / 坐标校准 | ❌ VM 显示栈不同 | | | 性能指标(P95 时延等) | ❌ VM 数字不可比 | | **需注意** | `win-x64` 包在 ARM Windows 上走 **Prism 模拟** | ❌ **实测结论(2026-10-11):能启动、能 listen,但 TLS 握手不完成**(控制端只见 `E_TIMEOUT`)→ 被控端实际不可用。ARM Windows **必须**用 `nodeagent-win-arm64.zip`(见 B5) | @@ -26,7 +26,7 @@ ## 2. 前置准备 ``` -① Parallels 27 装 Windows 11 ARM(Pro/Enterprise) +① 虚拟化软件里装 Windows 11 ARM(Pro/Enterprise) └ Apple silicon 只能跑 ARM 版 Windows(x86_64 仅 Pro/Business 的早期预览,性能显著更低) ② 网络 → 桥接(Bridged),不要用默认「共享网络」 @@ -153,7 +153,7 @@ node scripts/anchor-audit.mjs verify --anchors /tmp/vm-anchor.jsonl --node vm ```bash # ① VM 内,用包内自带 node 起被控端(务必 arm64 包) -C:\nodeagent-test\node.exe C:\nodeagent-test\agent.mjs +C:\na-test\node.exe C:\na-test\agent.mjs # 注意:以 SYSTEM(会话 0)启动时 screen.capture 等图形能力必失败(会话 0 无交互桌面), # 正式安装路径不受影响 —— install.ps1 的计划任务用登录用户 + LogonType Interactive。 @@ -188,7 +188,7 @@ A 基础验收(verify.mjs) | 杀软交互 / 显示 / 性能 | `⏳ 待真机`(VM 不可替代,**不得**因 VM 通过就标已验) | | 分发包架构适配(B5) | `✅ 已验证(VM · Windows 11 ARM):arm64 包原生可用;win-x64 包 TLS 握手不完成、不可用` | -> **一条纪律**:VM 与真机**架构不同**(真机 i7-12650H = x86_64)。任何"VM 通过" +> **一条纪律**:VM 与真机**架构不同**(真机 = x86_64)。任何"VM 通过" > 都不能自动推出"真机通过";涉及**原生二进制、驱动、杀软、显示栈**的部分尤其如此。 > 宁可多留一条"待真机复验",也不要让台账出现无法追溯来源的"已验证"。 @@ -214,18 +214,19 @@ A 基础验收(verify.mjs) |---|---|---|---| | 2026-10-11 | **B5 架构适配** | ✅ arm64 包原生可用;❌ win-x64 包不可用 | 同机同参对照:arm64 握手成功 / x64 `E_TIMEOUT` | | 2026-10-11 | 基础能力(A 子集) | ✅ | `system.info`(`arch=arm64`、能力 51 项两端一致)/ `status` / `process.list` / `service.list` / `shell.exec`(退出码 0)/ `audit.list` / `audit verify`(链完整)均正常 | -| 2026-10-11 | **图形能力** | ✅ **登录态下全通** | 以登录用户 + `LogonType Interactive` 启动后:`screen.capture` 返回**真实桌面** 1800×1010(含中文,渲染正常)/`window.list` 返回窗口列表(前台窗口 PID 与实测一致)/`screen` 枚举显示器/`audit verify` 链完整。**证实会话 0 的失败是身份问题,非产品缺陷** | +| 2026-10-11 | **图形能力** | ✅ **登录态下全通** | 以登录用户 + `LogonType Interactive` 启动后:`screen.capture` 返回**真实桌面** 1280×720(含中文,渲染正常)/`window.list` 返回窗口列表(前台窗口 PID 与实测一致)/`screen` 枚举显示器/`audit verify` 链完整。**证实会话 0 的失败是身份问题,非产品缺陷** | > 本轮**尚未执行**:B1(v1.5 四项输入)/ B2(v23 三防护)/ B3(v26 重启清理)/ B4(审计锚点端到端)。 -> 另注:VM 的 `prlctl exec` 默认以 **SYSTEM(会话 0)** 身份执行,图形类能力须换登录用户会话方可复验。 +> 另注:宿主虚拟机 CLI 的 `exec` 子命令默认以 **SYSTEM(会话 0)** 身份执行,图形类能力须换登录用户会话方可复验。 > **图形能力复验配方(本轮实证可通过)**:见 §8 末节。 --- -## 8. 实际验证结果(2026-10-11,Parallels 27 + Win11 ARM) +## 8. 实际验证结果(2026-10-11,虚拟化平台 + Win11 ARM) -> 本节是**实测记录**,不是计划。环境:Parallels 共享网络,VM `10.211.55.9`, -> Mac 侧 `bridge100=10.211.55.2`;被控端 `-NodeId win-arm -AllowInput`。 +> 本节是**实测记录**,不是计划。环境:虚拟化平台的共享网络,VM `192.0.2.9`, +> Mac 侧 `bridge100=192.0.2.2`;被控端 `-NodeId win-arm -AllowInput`。 +> (上列地址为文档保留段示例,非真实环境值。) ### B5 结论(重要,与预期不同) @@ -241,7 +242,7 @@ A 基础验收(verify.mjs) 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 握手。 +ARM Windows(含虚拟化平台上的 Win11 ARM)请用 **arm64 包**;用 x64 包会卡在 TLS 握手。 (临时替代:手工把包内 `node.exe` 换成 ARM64 版同样可行,重启后需再换 —— 故推荐用 arm64 包。) ### 各项结果 @@ -288,11 +289,11 @@ VM 无人登录时停在 LogonUI。可脚本化的路径只有 **autologon**, **配方**:新建一个**本地账户**再 autologon: ```powershell $pw = ConvertTo-SecureString '' -AsPlainText -Force -New-LocalUser -Name pdverify -Password $pw -PasswordNeverExpires -AccountNeverExpires -Add-LocalGroupMember -Group Administrators -Member pdverify +New-LocalUser -Name na-test -Password $pw -PasswordNeverExpires -AccountNeverExpires +Add-LocalGroupMember -Group Administrators -Member na-test $k = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' Set-ItemProperty $k AutoAdminLogon '1' -Set-ItemProperty $k DefaultUserName 'pdverify' +Set-ItemProperty $k DefaultUserName 'na-test' Set-ItemProperty $k DefaultDomainName $env:COMPUTERNAME Set-ItemProperty $k DefaultPassword '' # 明文,验完务必清理 Restart-Computer -Force @@ -301,8 +302,8 @@ Restart-Computer -Force ### ② 以登录用户身份启动被控端(这一步才是图形能力的关键) ```powershell -$act = New-ScheduledTaskAction -Execute 'C:\nodeagent-test\node.exe' -Argument 'C:\nodeagent-test\agent.mjs' -$pri = New-ScheduledTaskPrincipal -UserId ($env:COMPUTERNAME + '\pdverify') -LogonType Interactive -RunLevel Highest +$act = New-ScheduledTaskAction -Execute 'C:\na-test\node.exe' -Argument 'C:\na-test\agent.mjs' +$pri = New-ScheduledTaskPrincipal -UserId ($env:COMPUTERNAME + '\na-test') -LogonType Interactive -RunLevel Highest Register-ScheduledTask -TaskName 'na-gui' -Action $act -Principal $pri -Force Start-ScheduledTask -TaskName 'na-gui' ``` @@ -315,7 +316,7 @@ Start-ScheduledTask -TaskName 'na-gui' |---|---| | `Get-Process node \| Select Id,SessionId` | **SessionId = 1**(不是 0) | | `Get-Process explorer \| Select SessionId` | 1(桌面已就绪) | -| `Get-LocalUser pdverify \| Select LastLogon` | 有值(确实登录过) | +| `Get-LocalUser na-test \| Select LastLogon` | 有值(确实登录过) | ### ④ 证书指纹会变(这是钉住机制在正常工作) @@ -332,7 +333,7 @@ $k = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' Remove-ItemProperty $k DefaultPassword -ErrorAction SilentlyContinue Set-ItemProperty $k AutoAdminLogon '0' Unregister-ScheduledTask -TaskName 'na-gui' -Confirm:$false -Remove-LocalUser -Name pdverify # 临时账户一并删除 +Remove-LocalUser -Name na-test # 临时账户一并删除 ``` > 若需**保留**该环境供后续验证(重启后仍自动恢复图形会话),可暂不清理 —— @@ -342,7 +343,7 @@ Remove-LocalUser -Name pdverify # 临时账户一并删除 ## 10. 200% 缩放下的坐标口径,与 VM 输入通道的真机真相(2026-10-12) -全部在 **Parallels 27 + Win11 ARM64(200% 缩放)** 上实测。 +全部在 **虚拟化平台 + Win11 ARM64(200% 缩放)** 上实测。 §9 讲「怎么让图形能力跑起来」,本节讲「跑起来之后坐标为什么还是错的」以及「被控端自己不动了怎么救」。 ### 10.1 缺陷本体:一个进程里并存两套坐标系 @@ -351,7 +352,7 @@ Remove-LocalUser -Name pdverify # 临时账户一并删除 | 调用 | 不感知时的结果 | 危害 | |---|---|---| -| `Screen.Bounds` / `SystemInformation.VirtualScreen` | **逻辑**尺寸 1800×1010 | `screen.info` 报错尺寸;`screen.capture` 缺省 region 一并错 | +| `Screen.Bounds` / `SystemInformation.VirtualScreen` | **逻辑**尺寸 1280×720 | `screen.info` 报错尺寸;`screen.capture` 缺省 region 一并错 | | `CopyFromScreen` | 按**物理**像素裁剪 | `screen.capture` 缺省**只截到左上 1/4** | | `SetCursorPos` | 被系统 **×2** | `input.mouse.*` 喂 100,100 → 光标落到 200,200 | | UIA(`screen.find`) | **物理**坐标(与进程是否感知无关) | 与上面三项**口径不一致** | @@ -363,7 +364,7 @@ Remove-LocalUser -Name pdverify # 临时账户一并删除 | 断言 | 怎么取 | 修复前 | 修复后应为 | |---|---|---|---| -| 屏幕尺寸口径 | `screen.info` | `1800x1010` | **`3600x2020`**,且与**裸 Win32** `GetSystemMetrics(0/1)`(感知后)一致 | +| 屏幕尺寸口径 | `screen.info` | `1280x720` | **`2560x1440`**,且与**裸 Win32** `GetSystemMetrics(0/1)`(感知后)一致 | | 缺省整屏截图 | `screen.capture`(**不传** `region`) | 只有左上 1/4 | 整屏(按 `scale` 缩放后返回) | | 光标落点 | `input.mouse.move(x,y)` 后**用同样感知的助手**读回 `GetCursorPos` | 落到 (2x,2y) | **精确等于** `(x,y)` | | 端到端零换算 | `screen.find` 的物理矩形中心**直接**喂 `input.mouse.click` | 点不中 | 命中(可用主题单选框的 UIA `selected` 翻转验证) | @@ -381,10 +382,10 @@ Remove-LocalUser -Name pdverify # 临时账户一并删除 | 会话 / 窗口站 / 桌面 | `session=1` / `WinSta0` / `Default` ✅ | | `UserInteractive` | `True` ✅ | | `OpenInputDesktop` | 成功 → `Default`;`SetThreadDesktop(它)` 报 `ERROR_BUSY(170)`(本就在该桌面)✅ | -| `ClipCursor` | 整屏 `0,0,3600,2020`,无裁剪 ✅ | -| 指点设备 | `HID-compliant mouse` + `Parallels USB Mouse Synchronization Device` 均 `Status OK` ✅ | -| `prl_mouf` 驱动 | `STATE: 4 RUNNING` ✅ | -| **进程来源** | 从 **node 子树** 与 **`prlctl enter`**(Parallels Tools 侧)两条**互不相干**的来源跑同一段探针,**结果完全一致** ❌ | +| `ClipCursor` | 整屏 `0,0,2560,1440`,无裁剪 ✅ | +| 指点设备 | `HID-compliant mouse` + 虚拟化平台的 USB 鼠标同步设备 均 `Status OK` ✅ | +| 虚拟化平台的鼠标过滤驱动 | `STATE: 4 RUNNING` ✅ | +| **进程来源** | 从 **node 子树** 与 **宿主虚拟机 CLI 的 `enter` 子命令**(Guest Tools 侧)两条**互不相干**的来源跑同一段探针,**结果完全一致** ❌ | 判决性证据(**纯 Win32 探针,不含任何本产品代码**): @@ -404,8 +405,8 @@ GetAsyncKeyState(按键) = 0x0000 ← 连键盘注入也不被系统 | 手段 | 结果 | |---|---| | `--smart-mouse-optimize` 三档(`off` / `auto` / `on`) | ❌ 无效 | -| `prlctl suspend` + `start`(挂起 / 恢复) | ❌ 无效 —— resume 会**完整还原 win32k 状态**,卡住的输入态当然一起被还原 | -| **`prlctl stop --acpi` + `start`(真正冷启动 Windows)** | ✅ **立即恢复** | +| 虚拟机「挂起 / 恢复」 | ❌ 无效 —— resume 会**完整还原 win32k 状态**,卡住的输入态当然一起被还原 | +| **虚拟机「ACPI 关机 + 启动」(真正冷启动 Windows)** | ✅ **立即恢复** | > 结论:VM 输入栈瞬时卡死,**只有冷启动能复位**。 > 开工前先让宿主机把 VM 窗口真正当前台窗口交互一次(宿主机侧绝对鼠标集成会把 guest 指针从属过去; @@ -416,10 +417,10 @@ GetAsyncKeyState(按键) = 0x0000 ← 连键盘注入也不被系统 **① 计划任务以 `RunLevel Highest` 拉起时,普通权限 `taskkill` 会「拒绝访问」。** ```text -na-gui user=pdverify logon=Interactive runlevel=Highest +na-gui user=na-test logon=Interactive runlevel=Highest ``` -→ 必须从**更高权限通道**杀:`prlctl enter "Windows 11"`(**不带** `--current-user`) +→ 必须从**更高权限通道**杀:宿主虚拟机 CLI 进入 guest(**不带** `--current-user`) 拿到 `nt authority\system` 的 shell,`taskkill /F /PID ` 才会成功。 另外:旧进程仍占着 8765 时,`schtasks /run /tn na-gui` 会**报「执行成功」但新实例立即退出** @@ -429,7 +430,7 @@ na-gui user=pdverify logon=Interactive runlevel=Highest ```text netstat -ano | findstr 8765 - TCP 10.211.55.9:8765 10.211.55.2:57182 CLOSE_WAIT 9396 + TCP 192.0.2.9:8765 192.0.2.2:57182 CLOSE_WAIT 9396 ``` 此时新连接**能完成 TCP 三次握手,但 TLS 握手永不完成**(控制端只见 `E_TIMEOUT`), @@ -440,19 +441,20 @@ netstat -ano | findstr 8765 > 记此供后续专门排查。若成因是「`console.log` 写满无人读取的管道 → 事件循环阻塞」, > 那对「被控端常驻、控制端随时可能硬断开」这种使用模式是个真问题。 -### 10.5 两条 `prlctl` 通道的语义差异(本轮踩出来的) +### 10.5 两条虚拟机 CLI 通道的语义差异(本轮踩出来的) + +> 下表 `` 泛指所用虚拟化平台提供的 guest 控制命令(具体命令名见该平台文档)。 | 命令 | 实测行为 | |---|---| -| `prlctl exec -- ` | **返回 exit=0 但完全空转**:无输出、无副作用(用「guest 反连 Mac 监听端口」判决)→ **不可用** | -| `prlctl enter ` | 默认 `nt authority\system`(会话 0,无交互桌面) | -| `prlctl enter --current-user` | 登录用户的**真实交互** shell ✅ | +| ` exec -- ` | **返回 exit=0 但完全空转**:无输出、无副作用(用「guest 反连 Mac 监听端口」判决)→ **不可用** | +| ` enter ` | 默认 `nt authority\system`(会话 0,无交互桌面) | +| ` enter --current-user` | 登录用户的**真实交互** shell ✅ | 另两条: -- `prlctl enter` **没有执行参数**(加 `--` 报 `Unrecognized option`),它是交互式 shell, - 命令要**经 stdin 喂**:`printf 'cmd\r\n...\r\nexit\r\n' | prlctl enter ...` -- **`prlctl` 的参数里带逗号 / `&&` / 重定向会报 `Invalid argument`** —— 复杂逻辑一律先落成 `.cmd` / `.ps1` 再调用。 +- ` enter` **没有执行参数**(加 `--` 报 `Unrecognized option`),它是交互式 shell, + 命令要**经 stdin 喂**:`printf 'cmd\r\n...\r\nexit\r\n' | enter ...` +- **该 CLI 的参数里带逗号 / `&&` / 重定向会报 `Invalid argument`** —— 复杂逻辑一律先落成 `.cmd` / `.ps1` 再调用。 -> 仅供技术参考,不构成任何投资建议。 diff --git a/docs/console-plan.md b/docs/console-plan.md index e263778..efaa101 100644 --- a/docs/console-plan.md +++ b/docs/console-plan.md @@ -5,8 +5,8 @@ > 保留原文是为了可回溯决策过程;实现以「实施决策」节与代码为准。 -> 版本 v1.0 | 日期 2026-10-07 | 状态:待皇上评审 -> 定位:为「Mac 控 Windows」补上**人肉可视化操作台**——CLI 是脚本底座、MCP 是 AI 落点、本方案是**皇上自己点按钮**的桌面界面。 +> 版本 v1.0 | 日期 2026-10-07 | 状态:待用户评审 +> 定位:为「Mac 控 Windows」补上**人肉可视化操作台**——CLI 是脚本底座、MCP 是 AI 落点、本方案是**用户自己点按钮**的桌面界面。 --- @@ -20,8 +20,8 @@ | 层 | 选型 | 版本 | 选型理由 | |---|---|---|---| -| 桌面壳 | **Tauri 2** | 2.x | 跨端(Mac/Win)+ 性能好 + 体积小;Rust 后端契合皇上「Rust 加速」技术栈 | -| 前端框架 | **Vue 3** | 3.5+ | 皇上既有 Vue2/Vue3 经验(reader、FinDataHub),迁移成本最低 | +| 桌面壳 | **Tauri 2** | 2.x | 跨端(Mac/Win)+ 性能好 + 体积小;Rust 后端契合用户「Rust 加速」技术栈 | +| 前端框架 | **Vue 3** | 3.5+ | 既有 Vue 2 / Vue 3 实战经验,迁移成本最低 | | 语言 | **TypeScript** | 5.x | 与 monorepo 既有 TS 生态对齐,复用协议类型定义 | | 构建 | **Vite** | 6.x | Tauri 官方模板默认,HMR 快 | | 状态管理 | **Pinia** | 2.x | Vue3 官方推荐,模块化 store 天然契合多面板 | @@ -41,7 +41,7 @@ | 性能 | 好(Rust 后端) | 中 | 受浏览器沙箱限制 | | 直接复用 Node SDK | ❌(需 Rust 版 client) | ✅(主进程可 import) | ❌(需浏览器版) | -**取舍**:Tauri 需要新写一份 Rust 版 client(协议本身是 JSON-RPC over WebSocket,语言无关,逻辑对齐已跑通的 client.ts 即可),换取体积、内存、性能上的长期优势——与皇上「性能好 + 跨端」的要求一致。 +**取舍**:Tauri 需要新写一份 Rust 版 client(协议本身是 JSON-RPC over WebSocket,语言无关,逻辑对齐已跑通的 client.ts 即可),换取体积、内存、性能上的长期优势——与用户「性能好 + 跨端」的要求一致。 --- @@ -323,16 +323,15 @@ cargo run --example probe -- ws://127.0.0.1:8765 /tmp/nodeagent-e2e --- -## 11. 待确认项(请皇上评审时一并拍板) +## 11. 待确认项(请用户评审时一并拍板) -| # | 事项 | 臣的建议 | +| # | 事项 | 建议 | |---|---|---| | C1 | Windows 端桌面应用的定位 | 被控端本地面板(默认侧重)+ 对称可作控制端 | -| C2 | 组件库 | Naive UI(暗色友好、TS 完整);若皇上惯用 Element Plus 可换 | +| C2 | 组件库 | Naive UI(暗色友好、TS 完整);若用户惯用 Element Plus 可换 | | C3 | 图表库 | ECharts(资源趋势/磁盘);确认后纳入 M2 | | C4 | 是否同步做图形接管 UI | 建议 v2 再做,首期聚焦闭环 | -> 仅供参考,本方案为技术规划,不构成任何投资建议。 --- @@ -580,6 +579,35 @@ node 去跑打包内的 sidecar,并走一次真实 JSON-RPC 握手 —— 源 > 整个模式不匹配 —— 于是「写错的变量名」反而逃过检查。放宽到 `[^)\s,;]+` 后才真正生效。 > 教训:**注入故障不只是验证守卫有没有,还要验证它是否覆盖了目标形态**。 +### 出网代理:让「检查更新」在境内真的能用(2026-10-13) + +「检查更新 / 下载更新包」是控制台**唯一**两条需要出公网(GitHub)的路径。原实现直接用 +Node 内建 `fetch`,于是境内**必然**失败,且失败原因永远是笼统的 `fetch failed`。三条根因: + +1. `fetch`(undici)**默认不读代理环境变量**(`NODE_USE_ENV_PROXY=1` 能开,但只认环境变量); +2. **系统代理读不到** —— Windows 用户配的是 WinINET(第三方代理客户端的「系统代理」),macOS 用户配的是系统设置里的代理,Node 两者都不读; +3. `fetch(url,{dispatcher})` 也走不通:`dispatcher` 必须是 undici 的实例,而 Node 未把 undici 暴露成可导入模块。 + +因此 `apps/console/sidecar/proxy.mjs` **自持出网**(零新依赖): + +| 目标 | 经代理的做法 | +|---|---| +| `https://` | 先向代理发 `CONNECT host:443` 建隧道,再在隧道 socket 上叠 TLS;socket 交 `http.request`(**不用 `https.request`**,否则二次握手) | +| `http://` | 按 HTTP 代理规范发**绝对形态**请求行 | +| 无代理 | 直连(与改动前行为一致) | + +代理来源优先级:`NA_PROXY` → 目标协议对应的环境变量 → **系统代理**(Windows 读注册表 WinINET / macOS 读 `scutil --proxy`)。 +`NO_PROXY` 命中则直连。只支持 HTTP 代理;遇到 `socks5://` 或**系统代理是 PAC 脚本**时 +**明确把原因带给用户**(本机 AdGuard 正是 PAC 模式)—— 静默降级是这类缺陷最坏的地方。 + +**超时成对定义**(`sidecar.rs`):内层出网 120s / 外层 RPC 150s,且**内层必须小于外层** —— +这条关系写成**编译期不变式**,因为一旦破坏,用户只会看到「check_update 超时」这种**丢掉真实原因**的假失败。 +120s 的依据是真机实测的重定向链耗时分布(2.0 / 10.0 / 39.7 / 68.6 / >109 秒),不是拍脑袋。 + +守卫:`tests/unit/sidecar-proxy.test.mjs`(15 用例)= 纯函数解析(夹具取自真机)+ 离线真发请求 +(直连/绝对形态/CONNECT/**隧道+TLS 阶段 abort 必须立即生效**)+ 真 spawn sidecar 走 JSON-RPC 的端到端 ++ 「sidecar 源码不得再出现裸 `fetch(`」形态守卫。全部做过注入故障实测。 + ## Windows 端控制台(2026-10-11) ### 起因 @@ -802,7 +830,7 @@ msiexec /a 失败(退出码 ):nodeagent-console_2.0.2_x64_en-US.msi ## ARM64 真机安装冒烟与界面实点(2026-10-12) -上一节那张表里的条目,本轮在 **Parallels 27 + Win11 ARM64** 上逐条做完; +上一节那张表里的条目,本轮在 **虚拟化平台 + Win11 ARM64** 上逐条做完; 过程中还挖出并修掉一个**只在真机上才暴露**的缺陷(见本节末「真机暴露的缺陷」)。 ### 怎么把包送进去 @@ -831,7 +859,7 @@ msiexec /a 失败(退出码 ):nodeagent-console_2.0.2_x64_en-US.msi | **无黑窗** | ✅ 结构性(PE subsystem=2)+ 视觉确认 | PE 头 + 截图 | | 输入默认禁 | ✅ `input.mouse.move` → `E_CAPABILITY_DISABLED` | 错误名 | | 开机自启 | ✅ 计划任务 `cn_jkooe.nodeagent-console` **不存在**,与 UI 开关=关 一致 | `schtasks` | -| 数据目录显示 | ✅ UI 显示 `C:\Users\pdverify\.nodeagent`,与真实生效目录一致 | 配置比对 | +| 数据目录显示 | ✅ UI 显示 `C:\Users\\.nodeagent`,与真实生效目录一致 | 配置比对 | | 「最近日志」 | ✅ 正确记下「端口 8765 已被占用」并给出降级提示 | 截图 | ### ⚠️ 本轮新发现(**未修**):控制台没有单实例保护 @@ -852,7 +880,7 @@ msiexec /a 失败(退出码 ):nodeagent-console_2.0.2_x64_en-US.msi | 调用 | 不感知时的结果 | |---|---| -| `Screen.Bounds` / `SystemInformation.VirtualScreen` | 报**逻辑**尺寸 1800×1010 | +| `Screen.Bounds` / `SystemInformation.VirtualScreen` | 报**逻辑**尺寸 1280×720 | | `CopyFromScreen` | 按**物理**像素裁剪 → `screen.capture` 缺省只截到左上 1/4 | | `SetCursorPos` | 被系统 ×2(喂 100,100 → 光标到 200,200) | | UIA(`screen.find`) | 报**物理**坐标(与进程是否感知无关) | @@ -863,4 +891,3 @@ msiexec /a 失败(退出码 ):nodeagent-console_2.0.2_x64_en-US.msi 修法:新增 `apps/agent/src/util/dpi.ts`,把 `SetProcessDPIAware()` 注入到**全部坐标相关脚本的最前面** (5 个文件),并配 8 条单测守卫。详见证见 `CHANGELOG.md` 与 `docs/VM-VERIFICATION.md` §10。 -> 仅供技术参考,不构成任何投资建议。 diff --git a/scripts/anchor-audit.mjs b/scripts/anchor-audit.mjs index c16f978..555a0a7 100644 --- a/scripts/anchor-audit.mjs +++ b/scripts/anchor-audit.mjs @@ -64,7 +64,7 @@ function fetchHead(node, headFile) { return j.data ?? j; // 兼容「CLI 信封」与「裸 data」两种写法 } // ⚠️ CLI 没有 `audit head` 子命令(只有 `audit` 列表与 `audit verify`)—— - // system.audit.head 只能走通用 `invoke`。2026-10-11 在 Parallels VM 上真跑时才发现: + // system.audit.head 只能走通用 `invoke`。2026-10-11 在虚拟化平台的 VM 上真跑时才发现: // 不实际跑一次,这种"能力存在但 CLI 无子命令"的错配根本暴露不出来。 const args = ['invoke', 'system.audit.head', '--json']; if (node) args.push('--node', node); diff --git a/scripts/install-linux.sh b/scripts/install-linux.sh index bc4eb1a..4f43f73 100755 --- a/scripts/install-linux.sh +++ b/scripts/install-linux.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash # -# nodeagent 被控端 —— Linux 安装器(Debian 12 / Ubuntu / fnOS 等 systemd 系统) +# nodeagent 被控端 —— Linux 安装器(Debian 12 / Ubuntu / NAS 系统等 systemd 系统) # # 用法: # sudo bash scripts/install-linux.sh # 系统级(/opt/nodeagent + 系统 systemd) # bash scripts/install-linux.sh --user # 用户级(~/.local/share + user systemd + linger) -# sudo bash scripts/install-linux.sh --node-id dxp2800 --port 8765 +# sudo bash scripts/install-linux.sh --node-id nas-01 --port 8765 # sudo bash scripts/install-linux.sh --no-service # 只装程序+配置,不注册 systemd # sudo bash scripts/install-linux.sh --uninstall # 卸载(保留配置) # sudo bash scripts/install-linux.sh --uninstall --purge # 卸载并删配置 @@ -152,7 +152,7 @@ else b) 手工启动:NODEAGENT_HOME=$DATA_DIR $INSTALL_DIR/runtime/bin/node $INSTALL_DIR/agent.mjs" fi -# 发行版信息(fnOS 基于 Debian 12,此处只作展示) +# 发行版信息(部分 NAS 系统基于 Debian 12,此处只作展示) if [ -r /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release diff --git a/scripts/pack.mjs b/scripts/pack.mjs index 805f2de..21b5474 100644 --- a/scripts/pack.mjs +++ b/scripts/pack.mjs @@ -37,7 +37,7 @@ const getArg = (k, d) => { }; const NODE_VERSION = getArg('--node', 'v22.20.0'); -// 2026-10-11:Windows 也分架构。x64 包在 **ARM Windows(含 Parallels 的 Win11 ARM)** 上 +// 2026-10-11:Windows 也分架构。x64 包在 **ARM Windows(含虚拟化平台的 Win11 ARM)** 上 // 能启动、能 listen,但 **TLS 握手不完成**(Prism 模拟下协议层异常)→ 必须按架构出包。 // 用法:node scripts/pack.mjs --arch arm64 → nodeagent-win-arm64.zip const NODE_ARCH = getArg('--arch', 'x64'); diff --git a/scripts/release.mjs b/scripts/release.mjs index 08bcf3d..ff1a864 100644 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -8,7 +8,7 @@ * node scripts/release.mjs 1.0.0 --dry-run # 只演练:构建+生成资产,不改 git、不发布 * node scripts/release.mjs 1.0.0 --no-publish # 提交打标签,但不发 GitHub Release * - * 为什么要有它(对齐皇上的要求「每次更新,GitHub 上也要同步资产」): + * 为什么要有它(发版纪律:「每次更新,GitHub 上也要同步资产」): * 手工发版要记住「改几处 package.json、打哪个包、算哪些哈希、传什么资产」—— * 必然漏项(尤其是给人用的 zip 与给自更新用的 agent.mjs 是**两个不同的东西**)。 * 本脚本把两者都产出,并把 sha256 写进 latest.json,供被控端 `system.agent.update` 直接使用。 @@ -178,7 +178,7 @@ mkdirSync(distDir, { recursive: true }); const assets = [ { name: 'agent.mjs', path: agentPath, purpose: '自更新载荷(被控端 system.agent.update 直接使用)' }, { name: 'nodeagent-win-x64.zip', path: zipX64, purpose: '人工安装包 · x64 Windows(自包含 node.exe)' }, - { name: 'nodeagent-win-arm64.zip', path: zipArm64, purpose: '人工安装包 · ARM Windows(Parallels Win11 ARM 等)' }, + { name: 'nodeagent-win-arm64.zip', path: zipArm64, purpose: '人工安装包 · ARM Windows(虚拟机里的 Win11 ARM 等)' }, ]; for (const a of assets) { copyFileSync(a.path, join(distDir, a.name)); @@ -302,7 +302,7 @@ writeFileSync( `## 人工安装(首次)`, `\`\`\`powershell`, `Expand-Archive nodeagent-win-x64.zip C:\\nodeagent -Force`, - `# ARM Windows(如 Parallels 的 Win11 ARM)请改用 nodeagent-win-arm64.zip —— x64 包在其上 TLS 握手不通`, + `# ARM Windows(如虚拟机里的 Win11 ARM)请改用 nodeagent-win-arm64.zip —— x64 包在其上 TLS 握手不通`, `# 先把 C:\\nodeagent 加入杀软信任区(主流杀软)`, `powershell -ExecutionPolicy Bypass -File C:\\nodeagent\\install.ps1 -NodeId win -AllowInput`, '```', diff --git a/scripts/verify-b2.mjs b/scripts/verify-b2.mjs index 94fff93..c730e3f 100644 --- a/scripts/verify-b2.mjs +++ b/scripts/verify-b2.mjs @@ -14,8 +14,13 @@ // 用相对路径:从仓库根跑时解析不到 workspace 包(根 package.json 未声明它) import { NodeAgentClient } from '../packages/client/dist/index.js'; -const URL = process.env.NA_URL ?? 'wss://10.211.55.9:8765' // ⚠️ 必须 wss:被控端 tls=true(写成 ws 会 socket hang up); +// ⚠️ 必须 wss:被控端 tls=true(写成 ws 会 socket hang up) +const URL = process.env.NA_URL; const KEY = process.env.NA_KEY; +if (!URL) { + console.error('需要 NA_URL 环境变量,例如 wss://<被控端地址>:8765'); + process.exit(1); +} if (!KEY) { console.error('需要 NA_KEY 环境变量'); process.exit(1); diff --git a/tests/unit/acl-v11.test.mjs b/tests/unit/acl-v11.test.mjs index 64bdb8f..1023a8d 100644 --- a/tests/unit/acl-v11.test.mjs +++ b/tests/unit/acl-v11.test.mjs @@ -12,13 +12,13 @@ import { // ---------- B3:对端地址规范化(真机踩坑回归) ---------- test('normalizeIp:IPv4-mapped IPv6 归一为 IPv4(真机坑)', () => { - assert.equal(normalizeIp('::ffff:192.168.1.85'), '192.168.1.85'); - assert.equal(normalizeIp('::FFFF:192.168.1.85'), '192.168.1.85'); + assert.equal(normalizeIp('::ffff:192.168.1.10'), '192.168.1.10'); + assert.equal(normalizeIp('::FFFF:192.168.1.10'), '192.168.1.10'); }); test('normalizeIp:IPv4:port 去端口;纯 IP 原样', () => { - assert.equal(normalizeIp('192.168.1.85:51234'), '192.168.1.85'); - assert.equal(normalizeIp('192.168.1.85'), '192.168.1.85'); + assert.equal(normalizeIp('192.168.1.10:51234'), '192.168.1.10'); + assert.equal(normalizeIp('192.168.1.10'), '192.168.1.10'); }); test('normalizeIp:真 IPv6 原样保留(不误当 IPv4)', () => { @@ -29,20 +29,20 @@ test('normalizeIp:真 IPv6 原样保留(不误当 IPv4)', () => { }); test('normalizeIp + deny_cidr:mapped 地址也能命中黑名单(端到端复现)', () => { - const ip = normalizeIp('::ffff:192.168.1.85'); - const client = { client_id: 'mac_01', allow: [], deny_cidr: ['192.168.1.85'] }; + const ip = normalizeIp('::ffff:192.168.1.10'); + const client = { client_id: 'mac_01', allow: [], deny_cidr: ['192.168.1.10'] }; assert.equal(ipAllowed(client, ip).allowed, false); }); // ---------- B3:CIDR ---------- test('CIDR:单 IP 精确匹配', () => { - assert.equal(ipMatchesCidr('192.168.1.85', '192.168.1.85'), true); - assert.equal(ipMatchesCidr('192.168.1.86', '192.168.1.85'), false); + assert.equal(ipMatchesCidr('192.168.1.10', '192.168.1.10'), true); + assert.equal(ipMatchesCidr('192.168.1.11', '192.168.1.10'), false); }); test('CIDR:/24 网段匹配', () => { - assert.equal(ipMatchesCidr('192.168.1.85', '192.168.1.0/24'), true); + assert.equal(ipMatchesCidr('192.168.1.10', '192.168.1.0/24'), true); assert.equal(ipMatchesCidr('192.168.1.255', '192.168.1.0/24'), true); // ⚠️ 这条测的是「**跨网段不匹配**」—— 必须用不同网段的地址。 // (2026-10-10 一次机械 IP 替换曾把它改成同段,导致断言必红 —— 语义测试别被批量替换破坏) @@ -91,9 +91,9 @@ test('IP 白名单:deny 优先、白名单为空则放行', () => { ipAllowed({ client_id: 'a', allow: [], allow_cidr: ['192.168.1.0/24'] }, '10.0.0.1').allowed, false, ); - const both = { client_id: 'a', allow: [], allow_cidr: ['192.168.1.0/24'], deny_cidr: ['192.168.1.85'] }; - assert.equal(ipAllowed(both, '192.168.1.85').allowed, false, '黑名单优先'); - assert.equal(ipAllowed(both, '192.168.1.86').allowed, true); + const both = { client_id: 'a', allow: [], allow_cidr: ['192.168.1.0/24'], deny_cidr: ['192.168.1.10'] }; + assert.equal(ipAllowed(both, '192.168.1.10').allowed, false, '黑名单优先'); + assert.equal(ipAllowed(both, '192.168.1.11').allowed, true); }); // ---------- B3:生效时段 ---------- diff --git a/tests/unit/dpi-awareness.test.mjs b/tests/unit/dpi-awareness.test.mjs index af8b455..1c6f62c 100644 --- a/tests/unit/dpi-awareness.test.mjs +++ b/tests/unit/dpi-awareness.test.mjs @@ -12,10 +12,10 @@ import { PS_DPI_AWARE } from '../../apps/agent/dist/util/dpi.js'; /** * 回归守卫:**屏幕坐标口径必须统一为物理像素**。 * - * ## 背景(2026-10-12 真机实证,Parallels Win11 ARM / 主机 200% 缩放 / 物理 3600×2020) + * ## 背景(2026-10-12 真机实证,虚拟化平台的 Win11 ARM / 宿主 200% 缩放 / 物理 2560×1440) * * PowerShell 默认 DPI 不感知,于是**同一个进程里并存两套坐标系**: - * - `Screen.Bounds` / `SystemInformation.VirtualScreen` → **逻辑**(1800×1010) + * - `Screen.Bounds` / `SystemInformation.VirtualScreen` → **逻辑**(1280×720) * - `CopyFromScreen` / `SetCursorPos` → **物理** * (缺省整屏只截到左上 1/4;(100,100) 实际落到 (200,200)) * 而 UIA(`screen.find`)返回的**一直是物理坐标**。结果: diff --git a/tests/unit/net-scripts.test.mjs b/tests/unit/net-scripts.test.mjs index d2ee43d..f0e544e 100644 --- a/tests/unit/net-scripts.test.mjs +++ b/tests/unit/net-scripts.test.mjs @@ -10,11 +10,11 @@ import { const snap = { alias: 'Ethernet', dhcp: false, - ipv4: [{ ip: '192.168.1.159', prefix: 24 }], + ipv4: [{ ip: '192.168.1.20', prefix: 24 }], gateway: ['192.168.1.1'], dns: ['223.5.5.5'], }; -const args = { mode: 'static', ip: '192.168.1.88', mask: '255.255.255.0', gateway: '192.168.1.1' }; +const args = { mode: 'static', ip: '192.168.1.21', mask: '255.255.255.0', gateway: '192.168.1.1' }; test('掩码换算:常见掩码与前缀长度互转', () => { assert.equal(maskToPrefix('255.255.255.0'), 24); @@ -72,9 +72,9 @@ test('apply(dhcp):切 DHCP 用原生 cmdlet', () => { }); test('自检:失败时自动执行回滚脚本(自愈)', () => { - const chk = buildSelfCheck('Ethernet', '192.168.1.88', 'C:/rb.ps1'); + const chk = buildSelfCheck('Ethernet', '192.168.1.21', 'C:/rb.ps1'); assert.match(chk, /Get-NetIPAddress/); - assert.match(chk, /192\.168\.1\.88/, '应校验目标地址是否真的生效'); + assert.match(chk, /192\.168\.1\.21/, '应校验目标地址是否真的生效'); assert.match(chk, /selfcheck FAILED -> auto rollback/); // 同上:路径改为 PS 单引号字面量(原为 `& "C:/rb.ps1"`) assert.match(chk, /& 'C:\/rb\.ps1'/, '失败必须回滚,而不是把机器留在无地址状态'); @@ -88,7 +88,7 @@ test('rollback:同样先加后清 + 先清路由(否则回滚自己也会失 const iAddRoute = rb.indexOf('New-NetRoute'); assert.ok(iAdd < iRemove, '回滚也必须先加后清'); assert.ok(iRmRoute < iAddRoute, '回滚也必须先清路由'); - assert.match(rb, /192\.168\.1\.159/, '必须恢复快照里的原地址'); + assert.match(rb, /192\.168\.1\.20/, '必须恢复快照里的原地址'); assert.match(rb, /192\.168\.1\.1/, '必须恢复原网关'); assert.match(rb, /223\.5\.5\.5/, '必须恢复原 DNS'); assert.ok(!/netsh -f/.test(rb), '不得用 netsh -f 回滚(备份编码 + set address 双重问题)'); diff --git a/tests/unit/security-hardening.test.mjs b/tests/unit/security-hardening.test.mjs index 5ebab2b..59b742c 100644 --- a/tests/unit/security-hardening.test.mjs +++ b/tests/unit/security-hardening.test.mjs @@ -78,7 +78,7 @@ test('install.ps1 的 stdin 读取:校验规则与防阻塞', () => { // ---------- v2.0.0 自重启任务清理的保守判定(源码断言) ---------- // // 用 includes 而非正则:这里断言的是源码字面片段,正则转义层数容易写错 -// (臣本轮就在这上面栽过两次)。 +// (本项目就在这上面栽过两次)。 const restartSrc = readFileSync( new URL('../../apps/agent/src/capabilities/system.ts', import.meta.url),