diff --git a/CHANGELOG.md b/CHANGELOG.md index 20786fa..8b20fb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,6 +97,59 @@ **新步骤必须逐条核对前置产物由哪一步生成、顺序是否在其后** —— 本地验证时产物早就在,会把这层依赖完全掩盖)。 - `.github/workflows/windows-e2e.yml`:步骤名与注释同步为含 `agent.mjs` 的说明。 +### 新增 —— Windows 端控制台安装包(`.github/workflows/console-windows.yml`) + +控制台此前**只在 macOS 上出过包**(`.app` / `.dmg`),而它的主战场恰恰是 +「在 Windows 上管控其他机器」。本轮补上 Windows 构建:**双架构矩阵** —— +`windows-latest` 出 x64 `.msi` + NSIS `-setup.exe`,`windows-11-arm` 出 arm64 NSIS `-setup.exe`。 + +- **为什么必须双架构**:与 v2.0.2 那次事故同源 —— 包里装着错架构的 `node.exe` 时, + ARM Windows 上「能启动、能 listen,但 TLS 握手不完成」,而 macOS 侧 100% 复现不出。 + 控制台更甚:它内含 node 运行时,还要用这个 node **托起本机被控端**,即自己就是 TLS 服务端。 +- **为什么用原生 ARM runner 而非交叉编译**:`prepare-console.mjs` 的自检要**真执行 `node.exe`**, + x64 机器跑不了 arm64 二进制;交叉编译会逼着引入「跳过自检」这类**削弱验证**的开关 —— + 而产出错架构运行时的时候,自检正是唯一能拦住它的那道闸。 +- **`cfg!(windows)` 分支第一次被编译器看到**:macOS 上的 `cargo check` **不会编译 `cfg(windows)` 代码**, + 故 `taskkill` 收进程树、PowerShell 计划任务、`node.exe` 路径等分支此前从未产出过 Windows 产物。 +- **实测产物**:x64 `.msi` **34.2 MiB** / NSIS **23.2 MiB**;arm64 NSIS **19.5 MiB**(均低于 40MB 目标)。 + `if-no-files-found: error` + `fail-fast: false`:一条架构没产出就报错,同时一条挂了不掩盖另一条的结果。 +- **尚未接入 `scripts/release.mjs`**:安装包目前只作为 CI artifact 产出,未随 Release 发布。 + +> **首个 run 两个 job 全红,但错的不是构建,是断言。**(run `38151366534`) +> 两个 job 的失败点完全一致,而日志显示构建链路**一路全绿**:构建机架构(arm64 runner 上 +> `process.arch = arm64`,原生属实)、随包 `node.exe` 的 PE Machine(`0x8664` / `0xaa64`)、 +> `tauri build`、产物自举(打包内 node v22.23.3 真跑起打包内 sidecar)四步全 success。 +> 死掉的是本步自己写的守卫 —— 原断言「安装包体积 > 50MB」,理由是「含 Node 运行时约 80MB」, +> 这**把未压缩的目录形态(`.app` ≈ 92 MiB)当成了压缩安装包的体积**: +> LZMA 下 `node.exe` 自身能压到 1/3~1/4,实测 `.msi` 34.2 MiB、NSIS 19.5~23.2 MiB,阈值必然误杀。 +> +> 更根本的问题是:**体积阈值属于「中间逻辑自洽」式断言**。包大不等于 `node.exe` 真在里面 +> (可能打进的是别的大文件),包小也不等于没在里面(压缩率会随 Node 版本漂移)。 +> 判据必须落在**产物内容**上,不能落在**间接指标**上。 +> +> 改为**解开安装包查内容**(`.msi` 走 `msiexec /a` 管理安装、NSIS 走 `/S /D=` 静默安装, +> 两者都是各自格式的核心能力,不引任何外部依赖),一次钉死三件事: +> ① `node.exe` / `sidecar.mjs` / `agent.mjs` **三样随包资源必须都在**(缺任一,真机上就是 +> 「应用能开、但托不起被控端」);② `node.exe` 的 **PE Machine = 目标架构**(最终交付物级架构断言); +> ③ `node.exe` 解压后 **> 40MB**(体积在这是**正确用法**:内容已验明后的自明性检查, +> 而不是「运行时在不在」的**替代判据**)。附带收益:**真装一遍本身就是安装器冒烟**。 +> 另给该步加 `timeout-minutes: 8`,避免静默安装万一卡住把 job 拖满 60 分钟。 +> +> **第二个 run(`38152850819`)两 job 仍同点红,但已是另一处原因,且同样是静默失效**: +> 日志里 `msiexec /a 失败(退出码 )` —— `$LASTEXITCODE` 是**空的**,而整步从开始到抛错 +> 只过了 **0.8 秒**。根因是 `msiexec.exe` 与 NSIS 安装包都是 **GUI 子系统程序**, +> PowerShell 用 `&` 调用时**既不等待其结束、也不设置 `$LASTEXITCODE`** +> (`$null -ne 0` 判真 → 安装器还没跑完就抛错,报错读起来像「安装失败」,实为「压根没等」); +> arm64 那条没有 MSI、直奔 NSIS,失败形态一模一样,正是同一根因的第二个样本。 +> 修法:改 `Start-Process -Wait -PassThru` 读 `.ExitCode`;参数拼成**单个字符串**传, +> 避免数组被自动加引号而破坏 NSIS `/D=` 的「原样、不加引号、且居末」硬要求; +> 加 `Wait-PayloadRoot`(30 秒收敛)防安装器「自己先退出、子进程继续装」的竞态误报。 +> +> **第三个 run(`38154282653`)双绿**,断言给出产物实证 —— x64 `.msi`/`-setup.exe` 内均解出 +> **`node.exe` 82.9 MB / PE `0x8664`**、arm64 `-setup.exe` 内解出 **`node.exe` 73.5 MB / PE `0xaa64`**, +> 三样随包资源齐全(sidecar 232 KB、agent 1060 KB)。 +> 这一步证明的恰是体积断言永远无法区分的那件事:**19.5MB 的压缩包里真的装着 73.5MB 的 arm64 运行时**。 + ### 变更 —— 界面一律不使用图标 - **去掉全部 emoji 图标**:路由表 `meta.icon` 字段整体移除,侧栏导航由「图标 + 标题」改为**纯文字**。 diff --git a/README.md b/README.md index 518fdaf..872e2dc 100644 --- a/README.md +++ b/README.md @@ -347,6 +347,9 @@ Tauri 侧经 Cargo path 依赖),**不引入第二套协议实现**。 默认页面就是「本机服务」——点按钮即可启动 / 停止 / 重启本机被控端, 并支持检查更新、查看日志、后台常驻(托盘)、开机自启。 +**界面**:导航为**纯文字**(不使用图标);配色为两套中性灰阶主题 —— +「简约白」与「高级灰」,侧栏底部一键切换。 + | 页面 | 能力 | |---|---| | **本机服务**(默认) | 本机被控端启停/重启、连接本机、节点 ID 与密钥、开机自启、后台运行、异常自动重启、检查并应用更新 | @@ -360,12 +363,29 @@ Tauri 侧经 Cargo path 依赖),**不引入第二套协议实现**。 | 审计 | `system.audit.list` 筛选 + `system.audit.verify` 链完整性校验 + 导出 JSONL | ```bash -pnpm -r build # 含 console 前端 -cd apps/console && pnpm tauri dev # 开发态(起 Vite + Tauri 窗口) -pnpm --filter nodeagent-console bundle # 打包(含 sidecar + Node 运行时 + 被控端脚本) -# macOS 实测:.app 90.8 MiB / .dmg 33.78 MiB(分发件,其中 Node 运行时 86MB) +pnpm -r build # 含 console 前端 +cd apps/console && pnpm tauri dev # 开发态(起 Vite + Tauri 窗口) +pnpm --filter nodeagent-console bundle # 打包(含 sidecar + Node 运行时 + 被控端脚本) +# macOS 实测:.app 92.2 MiB / .dmg 33.78 MiB(分发件,其中 Node 运行时 86MB) ``` +Windows 版由 CI 产出(`.github/workflows/console-windows.yml`),**双架构各一套**: + +| 架构 | runner | 安装包 | 实测体积 | +|---|---|---|---| +| x64 | `windows-latest` | `.msi`(企业批量部署)+ `-setup.exe`(NSIS,双击安装) | 34.2 MiB / 23.2 MiB | +| arm64 | `windows-11-arm` | `-setup.exe`(NSIS) | 19.5 MiB | + +> 为什么必须两套:控制台内含 Node 运行时,且要用它**托起本机被控端**(即自己就是 TLS 服务端)。 +> 若在 ARM Windows 上装到 x64 的运行时,会**能启动、能监听,但 TLS 握手不完成** —— +> 客户端只见超时,且开发机上 100% 复现不出。CI 里为此加了三道断言: +> 「构建机架构 ⇄ 目标架构」、「随包 `node.exe` 的 PE Machine」、 +> 以及**解开安装包查内容**(三样随包资源齐全 + 包内 `node.exe` 的 PE Machine 对得上 —— +> 前两条查的是打包**前**的中间产物,这一条查的才是**最终交付物**; +> 实测解出 x64 `node.exe` 82.9 MB / `0x8664`、arm64 73.5 MB / `0xaa64`)。 + +> 控制台安装包目前**只作为 CI artifact 产出**,尚未接入 `scripts/release.mjs` 随 Release 发布。 + ⚠️ 平台差异:`app.*` 与 `system.service.list` **仅在被控端为 Windows 时可用**, 非 Windows 被控端会返回 `E_UNSUPPORTED_PLATFORM`。控制台连接后会探测 `system.info.os` 并**事前禁用**这些入口,而不是发出注定失败的请求。 diff --git a/docs/console-plan.md b/docs/console-plan.md index ceba9f8..21410a0 100644 --- a/docs/console-plan.md +++ b/docs/console-plan.md @@ -224,7 +224,7 @@ nodeagent/ | 命令闭环成功率 | 成功返回 ÷ 发起 | ≥ 95% | | 内网调用 P95 时延 | 前端点击到结果渲染 | < 3s | | 应用内存占用 | Mac 版空闲常驻 RSS | < 150MB(远低于 Electron) | -| 打包体积 | **用户实际下载的分发件** | Mac `.dmg` < 40MB(实测 **33.8MB** ✓)/Win 安装包 < 40MB(待实测) | +| 打包体积 | **用户实际下载的分发件** | Mac `.dmg` < 40MB(实测 **33.8MB** ✓)/Win 安装包 < 40MB(实测 ✓ `.msi` **34.2 MiB**、NSIS **19.5~23.2 MiB**) | | 双端编译 | `pnpm build` 一次产出两端 | 100% | > **⚠️ 口径调整(2026-10-11,待确认)**:原指标写的是「Mac `.app` < 30MB / Win 安装包 < 40MB」。 @@ -580,9 +580,152 @@ node 去跑打包内的 sidecar,并走一次真实 JSON-RPC 握手 —— 源 > 整个模式不匹配 —— 于是「写错的变量名」反而逃过检查。放宽到 `[^)\s,;]+` 后才真正生效。 > 教训:**注入故障不只是验证守卫有没有,还要验证它是否覆盖了目标形态**。 +## Windows 端控制台(2026-10-11) + +### 起因 + +控制台此前**只在 macOS 上出过包**(`.app` / `.dmg`)。而它的主要用户场景恰恰是 +「在 Windows 机器上管控其他机器」——只出 macOS 版等于把主战场让开了。 + +排查后发现:代码层面对 Windows 其实是备好的(`cfg!(windows)` 分支、`node.exe` 路径、 +`creation_flags`、PowerShell 计划任务、`.ico` 图标、`prepare-console.mjs` 的平台自适应都在), +**缺的只是「真在 Windows 上构建一次」这一环**。 + +> 这里有个容易被忽视的点:macOS 上的 `cargo check` **不会编译 `cfg(windows)` 代码**, +> 所以那些 Windows 分支在此之前**从未被编译器看过一眼**。而 `cfg!(windows)` 这种运行时宏 +> 虽然两边都会编译,却也只是类型检查、从未产出过 Windows 产物。 + +### 为什么必须双架构 + +被控端发布包出过「arm64 包里装的其实是 x64 `node.exe`」的事故,后果是: +在 ARM Windows 上**能启动、能 listen,但 TLS 握手不完成**(客户端只见超时), +且 macOS 侧 100% 复现不出。 + +控制台同理,而且更甚 —— 它内含 node 运行时,还要用这个 node **托起本机被控端**, +即自己就是 TLS 服务端。所以 x64 / arm64 必须各出一套,不能只出默认架构。 + +### 为什么用原生 ARM runner,而不是交叉编译 + +GitHub 提供 ARM64 Windows runner(`windows-11-arm`,公共仓库免费), +本仓库是 public,故 arm64 可以在它上面**原生构建**。 + +交叉编译(在 x64 runner 上编 arm64)看似省事,实则要付两笔代价: +1. 需要 VS 的 ARM64 工具链,且 Tauri 的 NSIS/MSI bundler 对 arm64 支持面较窄; +2. 更关键:`prepare-console.mjs` 的**自检要真执行 `node.exe`** + (x64 机器跑不了 arm64 二进制),会逼着引入「跳过自检」这类**削弱验证**的开关。 + 而当产出的是「架构错的运行时」时,自检正是唯一能拦住它的那道闸。 + +宁可等原生 runner,也不引入削弱验证的分支。 + +### 三条断言是必需项,不是顺手检查 + +| 断言 | 为什么不能省 | +|---|---| +| **构建机架构 ⇄ 目标架构**(`node -p process.arch`) | ARM runner 镜像里确实存在 x64 模拟的组件(社区实测 python 即为 AMD64)。一旦 Node 也是模拟版,`process.arch` 会是 `x64` → 把 x64 的 `node.exe` 打进 arm64 安装包 → **真机上能启动但 TLS 握手不完成,而 CI 全绿** | +| **随包 `node.exe` 的 PE Machine**(打包前,查 `src-tauri/runtime/`) | 与 `pack.mjs` 的双架构校验同源:不只看「脚本跑完了」,而是直接读最终要打进包的那个二进制头。中间逻辑再自洽,也可能因上游给了错架构的二进制而产出错包。放在打包前,是为了让失败点更靠近原因 | +| **安装包里确实装着三样东西**(打包后,解开安装包查内容) | 见下节。它才是**最终交付物级**的断言 | + +### 首个 run 双双失败:错的不是构建,是断言本身 + +`console-windows.yml` 的第一个 run(`38151366534`)两个 job 全红,且**失败点完全一致**。 +但取日志后发现,**构建链路一路全绿**: + +| 步骤 | x64 job | arm64 job | +|---|---|---| +| 构建机架构 | ✓ | ✓ `process.arch = arm64`(原生 ARM runner 属实) | +| 随包 `node.exe` PE | ✓ `0x8664` | ✓ `0xaa64`(73.5 MB) | +| `tauri build` | ✓ `.msi` 34.2 MiB + `-setup.exe` 23.2 MiB | ✓ `-setup.exe` 19.5 MiB | +| 产物自举 | ✓ 打包内 node v22.23.3 真跑起打包内 sidecar | ✓ | +| **体积断言** | ✗ `34.2 < 50` | ✗ `19.5 < 50` | + +死的是最后一步,而这一步是**守卫自己写错了**。原断言是「安装包体积 > 50MB」, + +> 另断言安装包体积 > 50MB:含 Node 运行时时 x64 约 82MB / arm64 约 72MB, +> 体积过小意味着运行时没打进去 —— 那种包能装,但一启动就找不到 node。 + +错在把**未压缩的目录形态**(`.app` ≈ 92 MiB)当成了**压缩安装包的体积**。 +安装包是压缩容器:LZMA 下 `node.exe` 自身就能压到 1/3~1/4,实测 `.msi` 34.2 MiB、 +NSIS 19.5~23.2 MiB,阈值 50MB 必然误杀。 + +> 更根本的问题:**体积阈值属于「中间逻辑自洽」式断言**。包大不等于 `node.exe` 真在里面 +> (可能打进的是别的大文件),包小也不等于没在里面(压缩率会随 Node 版本漂移)。 +> 这与「被控端 zip 里塞了错架构的 node」那条教训是同一类 —— 判据必须落在**产物内容**上, +> 不能落在**间接指标**上。 + +改为**解开安装包、查内容**(`.msi` 走 `msiexec /a` 管理安装、NSIS 走 `/S /D=` 静默安装, +两者都是各自格式的核心能力,不引外部依赖),一次钉死三件事: + +| 检查 | 挡住什么 | +|---|---| +| `node.exe` / `sidecar.mjs` / `agent.mjs` **三样都在** | 缺任一,真机上就是「应用能开、但托不起被控端」 | +| `node.exe` 的 **PE Machine = 目标架构** | 「arm64 包里装的是 x64 node.exe」那类**只在真机暴露**的事故 | +| `node.exe` 解压后 **> 40MB** | 塞了个占位文件(这里是「体积」的**正确用法**:作为内容已验明后的自明性检查,而不是作为「运行时在不在」的**替代判据**) | + +附带收益:**真装一遍本身就是安装器冒烟** —— 比只读目录更强。 +另给该步加 `timeout-minutes: 8`,避免静默安装万一卡住把 job 拖满 60 分钟。 + +### 第二个 run 仍红:`&` 调 GUI 程序不等待,也不设退出码 + +第二个 run(`38152850819`)两个 job **仍同点红**,但已是另一处原因,且同样是**静默失效**。 +日志里只有一行关键信息: + +``` +msiexec /a 失败(退出码 ):nodeagent-console_2.0.2_x64_en-US.msi +``` + +`$LASTEXITCODE` 是**空的**,而整步从开始到抛错只过了 **0.8 秒** —— 根本没等安装器跑完。 + +**根因**:`msiexec.exe` 与 NSIS 安装包都是 **GUI 子系统程序**,PowerShell 用 `&` 调用时 +**既不等待其结束、也不设置 `$LASTEXITCODE`**。于是 `$null -ne 0` 判真, +在安装器还没跑完时就当场抛错 —— 报错读起来像「安装失败」,实为「压根没等」。 +(arm64 那条没有 MSI、直奔 NSIS,失败形态一模一样,正是同一根因的第二个样本。) + +**修法**: +- 两处都改 `Start-Process -Wait -PassThru`,读 `$p.ExitCode`(确定性等待),并打印实际退出码; +- 参数拼成**单个字符串**传 `-ArgumentList`,避免数组被 PowerShell 自动加引号 —— + 那会破坏 NSIS `/D=`「必须原样、不加引号、且是最后一个参数」的硬要求; +- 新增 `Wait-PayloadRoot`(最多 30 秒收敛):安装器可能「自己先退出、由子进程继续装」, + 避免竞态误报「找不到 node.exe」; +- `msiexec` 退出码 3010(成功但需重启)按成功处理。 + +### 第三个 run:双绿,且断言给出了产物实证 + +第三个 run(`38154282653`)**两个 job 全绿**,第 14 步的实际输出: + +| 检查对象 | 安装包体积 | 安装器退出码 | 解开后 `node.exe` | PE Machine | 另两样 | +|---|---|---|---|---|---| +| x64 `.msi` | 34.2 MiB | 0 | **82.9 MB** | **`0x8664`** ✓ | sidecar 232 KB / agent 1060 KB | +| x64 `-setup.exe`(NSIS) | 23.2 MiB | 0 | **82.9 MB** | **`0x8664`** ✓ | 同上 | +| arm64 `-setup.exe`(NSIS) | 19.5 MiB | 0 | **73.5 MB** | **`0xaa64`** ✓ | 同上 | + +这一步的价值在于:**它证明的正是「19.5MB 的压缩包里真的装着 73.5MB 的 arm64 `node.exe`」** —— +这恰是体积断言永远无法区分的那件事(19.5MB 与「没打运行时」的包在体积上无法区分)。 +另:`/D=` 确实生效(安装位置即 `pkg-extract\nsis`),故 NSIS 的默认安装位置候选列表是备用保险。 + +### 产物形态 + +| 架构 | runner | 安装包 | 实测体积 | +|---|---|---|---| +| x64 | `windows-latest` | `.msi`(企业批量部署)+ `-setup.exe`(NSIS,普通用户双击) | **34.2 MiB** / **23.2 MiB** | +| arm64 | `windows-11-arm` | `-setup.exe`(NSIS)—— WiX 的 arm64 MSI 支持面较窄,不值得为它引入不确定性 | **19.5 MiB** | + +对照 macOS:`.app` 92.2 MiB(目录形态)/`.dmg` 33.8 MiB(压缩分发件)。 +**两者不可直接比** —— 这正是上面那条错断言的来源。 + +工作流 `console-windows.yml` 与 `windows-e2e.yml` **刻意分开**: +后者是「被控端在 Windows 上的行为验证」,跑一遍约 4 分钟、含安装/卸载真机冒烟; +前者要装 Rust 工具链、编 Tauri(冷编 10 分钟级)。目标不同、耗时量级不同, +混在一起会让「改一行前端也要等 15 分钟」。 + +### 仍未做 + +- **控制台尚未接入 `scripts/release.mjs`**(该脚本目前只出被控端包)。 + 控制台的安装包目前只作为 CI artifact 产出,未随 Release 发布。 +- 真机安装冒烟(在真实 Windows 上装一遍、点一遍)仍需图形环境人工执行。 + ## 界面观感约定(2026-10-11) -两轮纯界面调整,方向都是「少即是多」。理由记下来,免得日后被当成"没做完"再补回去。 +两轮纯界面调整,方向都是「少即是多」。理由记下来,免得日后被当成「没做完」再补回去。 ### 一、一律不使用图标