Files
LocalNetMsg/docs/windows-compat.md
T
xiaoshuai d854c95ee6 fix(build): 修好 npm install 挂掉 (postinstall 只重建 better-sqlite3)
`npm install` 在 postinstall 阶段直接失败退出:

    node-hid 编译报错 -> node-addon-api/napi.h:2477 expected unqualified-id

根因不是 node-hid 坏了, 是重建范围错了:

1. postinstall 跑的是 `electron-builder install-app-deps`, 它重建**所有**原生模块;
2. 它给子进程传 `npm_config_build_from_source=false` —— 但这是**字符串** "false",
   npm 11 对未知的 npm_config_* 不做布尔转换;
3. pkg-prebuilds-verify 用 `if (process.env.npm_config_build_from_source)` 判断,
   字符串 "false" 在 JS 里是真值 -> 直接 exit 1 -> 强制源码编译;
4. node-hid 于是去编译它自带的 node-addon-api 3.2.1, 和现代 Node 头文件不兼容 -> 编译失败
   -> 整个 npm install 挂掉。

而 node-hid 根本不需要编译 —— 它自带 N-API 预编译产物, 任何 ABI 都能加载。

真正需要按 Electron ABI 重编的只有 better-sqlite3 (非 N-API, 绑死 V8/ABI,
Electron 32 -> node ABI 128); 其余 (@lydell/node-pty / node-hid /
@serialport/bindings-cpp / usb) 都是 N-API 预编译, 重编既没必要又有害。

改动:

- postinstall 换成 `node scripts/postinstall.js`:
  只重建 better-sqlite3, 装完**在 Electron 下真跑一次读写校验**, 失败就 exit 1
  —— 把"启动即崩"提前到 install 阶段暴露
- package.json 加 `allowScripts` 白名单 (npm 11 新机制, 目前只提醒不拦截,
  但迟早会拦; 顺便让 npm install 输出干净)
- AGENTS.md 坑 #3 改写成新规矩 + 完整根因链
- docs/windows-compat.md / docs/remote-terminal-forward-usb.md 里过时的
  "postinstall 走 install-app-deps" 表述一并纠正

验证:

- npm run build            ✅
- npm run typecheck        ✅ node + web
- npm run selftest         ✅ 64 通过 / 0 失败
- npm run selftest:sync    ✅ 全部通过
- npm run package:linux    ✅ 新 deb
- npm run selftest:package ✅ 4/4 (从 app.asar 真实 spawn pty)
2026-10-21 09:32:32 +08:00

180 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Windows 兼容性分析与注意事项
LocalNetMsg 是跨平台应用 (Electron 32 / Node 20 / Chromium 128)。这份文档记录
**Windows 上能跑什么、哪些地方有坑、以及已经修过的地方**,避免以后改代码时把 Windows 弄坏。
测试环境:Ubuntu 22.04 (x64) + Electron 32.3.3。Windows 侧结论基于
依赖包源码与打包产物的静态验证,**未在真实 Windows 机器上跑过**(见文末"待验证")。
---
## 1. 结论速览
| 功能 | Windows | 说明 |
|---|---|---|
| 聊天 / 文件传输 / 网盘 | ✅ | 纯 Node HTTP/WS,无平台依赖 |
| 局域网发现 | ✅ | UDP 广播,多网卡 + 255.255.255.255 兜底 |
| 远程终端 | ✅(修过) | 需 ConPTY (Win10 1809+),打包规则曾有坑,见 §3.1 |
| 串口转发 | ✅ | serialport 有 win32 预编译;虚拟串口走 com0com |
| USB (libusb 字节桥) | ⚠️ | node-usb 可用,但需 WinUSB 驱动 (Zadig) |
| USB/IP 真透传 | ❌ | Linux 内核特性,代码里已明确拒绝 |
| 开机自启 | ✅ | `app.setLoginItemSettings` |
| 托盘 / 通知 | ✅ | 托盘图标走 `resources/icon.png` |
---
## 2. 原生模块:全是 N-API 预编译,不需要针对 Electron 重编
这是 Windows 打包能不能成的第一道关。`build.npmRebuild` 是 `false`
(原生模块已在 Electron ABI 上,重编反而坏事),所以**所有原生模块必须自带
跨 ABI 的预编译二进制**。现状:
| 模块 | Windows 产物 | ABI | 是否需要编译 |
|---|---|---|---|
| `@lydell/node-pty` | `@lydell/node-pty-win32-x64` | N-API (`napi_register_module_v1`) | 否 |
| `node-hid` | `prebuilds/HID-win32-x64/node-napi-v4.node` | N-API | 否 |
| `@serialport/bindings-cpp` | `prebuilds/win32-x64/*.node` | N-API | 否 |
| `usb` (node-usb 3) | `@node-usb/usb-win32-x64-msvc` | napi-rs | 否 |
| `better-sqlite3` | 源码编译 | **非 N-API** | 是,靠 `postinstall` 的 `scripts/postinstall.js` |
> **坑**:`better-sqlite3` 是唯一需要编译的。`npmRebuild: false` 只影响打包期,
> `postinstall` 仍然会重编它。**如果用 `npm ci --ignore-scripts`,应用会启动即崩**
> (sqlite 打开失败)。
>
> `postinstall` **只重建 better-sqlite3 这一个包**,不是 `electron-builder install-app-deps`。
> 后者会重建所有原生模块,并给子进程传 `npm_config_build_from_source=false`;
> npm 11 对未知的 `npm_config_*` 保留原始字符串,子进程里它是字符串 `"false"`(JS 真值),
> `pkg-prebuilds-verify` 直接 exit 1 强制源码编译,node-hid 编译失败把整个
> `npm install` 拖垮 —— 而它自带 N-API 预编译产物,本来根本不用编译。
> 完整根因见 `AGENTS.md` 坑 #3。
node-pty 1.2 换成了"主包 + 平台包"拆包架构:主包只是 loader,
真正的二进制在 `@lydell/node-pty-<platform>-<arch>` 里。**换平台时别忘了
平台包也要在 asarUnpack 名单里**(见 §3.1)。
---
## 3. 已修复的 Windows 阻塞项
### 3.1 打包:node-pty 平台包没被解包 → Windows 终端起不来
**现象**:Linux 上终端正常,Windows 上一点就报 spawn 失败。
**根因**:`build.asarUnpack` 里写的是
```json
"**/node_modules/@lydell/node-pty-*/*" // ❌ 只匹配一层
```
`/*` 只匹配直接子项,于是 `prebuilds/` 目录本身匹配上了,但
`prebuilds/win32-x64/conpty.node` 这种**两层深**的文件没匹配上 → 留在 asar 里。
**为什么 Linux 上没暴露**:Linux 的 PTY 只需要一个 `pty.node`,
Electron 的 asar 层能 dlopen 归档里的 `.node`(实测可用)。
Windows 就不行了:
- `conpty.node` 要加载同目录的 **`conpty.dll`**(OS loader 从真实路径加载,asar 读不了)
- 还要 **`OpenConsole.exe`**(`CreateProcess` 必须是磁盘上的真文件)
- node-pty 自己就写了 `__dirname.replace('node_modules.asar', 'node_modules.asar.unpacked')`
—— 它**明确假设**平台包在 unpacked 目录里,因为 `worker_threads.Worker`
的脚本路径也必须是真实文件
**修复**:
```json
"**/node_modules/@lydell/node-pty-*/**/*" // ✅ 递归
```
自测 S7 会守住这条规则(防止以后又被改回单层)。
### 3.2 文件名:Windows 保留设备名 + 尾部点空格
Win32 的两个坑,Linux 上完全不存在:
- **保留设备名**:`CON` `PRN` `AUX` `NUL` `COM1-9` `LPT1-9` `CLOCK$`,
**带扩展名也算**(`CON.txt` 依然是控制台设备)。对这类名字 `open()` 打开的是设备,
数据会写进控制台或者直接失败。
- **尾部点/空格被悄悄吞掉**:`report.` 实际落到 `report`,
于是 `existsSync('report.')` 为真,而我们以为写的是另一个文件 —— 重名判断和实际落盘对不上。
**修复**:新增 `src/main/safe-name.ts` 的 `safeFileName()`,
在两处落盘前调用:
- `file-server.ts` 的 `targetPath()` —— 接收方保存上传文件
- `drive.ts` 的 `downloadDriveFile()` —— 名字来自**对端共享目录**,属于不可信输入
清洗策略是"最小必要":非法字符 `\ / : * ? " < > |` 全平台都清;
保留名和尾点尾空格**只在 win32 上处理**,避免无谓改掉 Linux 上合法的文件名。
### 3.3 终端工作目录
`process.env.HOME` 在 Windows 上通常不存在,但装了 Git Bash / MSYS 之后
可能被设成 POSIX 路径(`/c/Users/xxx`)—— 那个路径在 Win32 下不存在,pty 起不来。
**修复**:`shell.ts` 新增 `defaultCwd()`,win32 优先 `USERPROFILE`。
---
## 4. 已有 Windows 分支的地方(别改坏)
| 文件 | Windows 处理 |
|---|---|
| `remote/shell.ts` | `defaultShell()` 优先 PowerShell 7 (`pwsh`),其次 Windows PowerShell 5;用 `where` 而不是 `command -v` |
| `remote/vcom.ts` | 虚拟串口走 com0com(自动下载安装器 + `setupc.exe` 配对),Linux/macOS 走 socat |
| `permissions.ts` | `fixWindowsHID()` 用 Zadig 装 WinUSB 驱动;`diagnoseUsb()` 给 Windows 提示 |
| `notify.ts` | win32 分支单独处理通知 |
| `discovery.ts` | UDP `setBroadcast(true)`,同时发 255.255.255.255 / 127.0.0.1 / 每接口子网广播 |
Linux 专属逻辑(udev 规则、dialout 组、usbip、/sys/bus/usb 读取)都在
`platform === 'linux'` 判断内,Windows 不会走到。
---
## 5. 已知限制与注意事项
1. **ConPTY 需要 Windows 10 1809+**。更老的 Windows(Server 2016 及以前)
没有 ConPTY,node-pty 1.2 的 Windows 实现直接依赖它 → 终端不可用。
2. **Windows 防火墙**首次运行会弹窗问是否允许 Electron 访问网络。
用户若选了"取消",UDP 发现和 WS 都连不上 —— 这是最常见的"装完连不上"原因。
3. **USB 真透传需要 WinUSB 驱动**。node-usb 走 libusb/WinUSB,
Windows 上必须用 Zadig 把设备驱动换成 WinUSB,否则 `loadDevices()` 枚举不到
(代码里已能自动下载 Zadig 并提权安装)。
4. **虚拟串口要 admin**。com0com 装驱动必须管理员,代码走 `sudo-prompt` 提权。
5. **`i.family` 的取值**。`discovery.ts` 里判断 `i.family !== 'IPv4'`,
Node 18+ 才把 `family` 从数字 4 改成字符串 `'IPv4'`。Electron 32 = Node 20 ✓,
但别把这行改成数字比较。
6. **长路径**。Windows 默认 260 字符上限,`safeFileName` 把文件名截到 200 留余量,
但"深目录 + 长文件名"仍可能超限,需要系统开启 LongPathsEnabled。
7. **路径大小写不敏感**。`existsSync` 在 Windows 上大小写不敏感,
所以 `A.apk` 和 `a.apk` 会被当成同一个文件 —— 重名自增逻辑能正确处理,
但 Linux/macOS 上会存成两个文件。两端行为不一致,属预期。
---
## 6. 在 Windows 上验证的清单
代码改完,Windows 侧至少要过这一遍(本机无法替代):
- [ ] 安装后能启动、图标正常(见 `docs/connection-status.md` 里的图标坑)
- [ ] 侧边栏能发现到对端设备(UDP 广播 + 防火墙)
- [ ] 收发文字消息正常
- [ ] 收发文件正常,文件名带中文/特殊字符/`CON` 也不出错
- [ ] 网盘能浏览对方目录、能下载(含整目录保持层级)
- [ ] 远程终端能打开、能看到回显、能输入
- [ ] 串口列表能看到 COM 口(设备管理器里有)
- [ ] USB 模式能枚举到设备(装了 WinUSB 的话)
- [ ] 托盘图标正常、右键菜单能唤出窗口
- [ ] 开机自启开关生效
---
## 7. 相关文件
| 文件 | 作用 |
|---|---|
| `src/main/safe-name.ts` | 跨平台文件名清洗(Windows 保留名 / 尾点空格) |
| `src/main/remote/shell.ts` | 跨平台 shell 选择 + 默认工作目录 |
| `src/main/remote/vcom.ts` | 虚拟串口(socat / com0com) |
| `src/main/permissions.ts` | 权限诊断与一键修复(udev / dialout / WinUSB) |
| `build.asarUnpack` (package.json) | 原生模块解包规则,**必须递归** |