Files
nodeagent/scripts/update-win.mjs
T
jkooe e6a9a3d7ea feat(v18): 更新链路可靠化 —— 构建指纹 + 备份 + 轮询校验(附 6 个真机 bug 修复)
## 皇上的问题
「这个版本如果有更新,可以在客户端上便捷的更新吗?」
答案:本来就有 `nodeagent deploy`(比最初的一键升级还早),但**它不可信**:
部署后只看「PID 变了 / 能力数对了」,证明不了真的换成了新代码(旧版可能能力数一样),
而且备份与失败判定都存在问题。本轮把这条链路做成**可信**的。

## 新增
1. **构建指纹**:agent 上报 `build.hash`(运行中脚本的 sha256 前 12 位)+ bytes/mtime/node/uptime。
   部署后按指纹校验「远端真的是新代码」,而非靠 PID/能力数猜。
2. **`deploy --check`**:只比对指纹不动文件(退出码 2 = 需要更新),适合脚本/巡检。
3. **`deploy --force`**:指纹相同也强制覆盖。
4. **`pnpm update:win [设备名]`**:一条命令完成「打包 → 校验产物确实更新 → 部署」
   (脚本刻意防呆:pack 后产物时间戳没变就拒绝部署,避免部署旧包 —— 真机踩过)。
5. **NODEAGENT_DEBUG=1**:CLI 打印异常堆栈(异步链问题必备)。

## 修掉的 6 个真机 bug(本轮逐个实测暴露)
1. **备份用 `Copy-Item`**(Windows 专用 shell 命令)→ macOS 被控端静默不备份。
   改为协议自带 fs.read/fs.write,跨平台。
2. **备份读取漏 `encoding: 'base64'`** → 二进制被按 utf8 读(损坏),再当 base64 写回
   → 备份文件只有 33 字节垃圾(实测:`fs.read` 返回 524288 字符而非 base64)。
3. **备份循环用 fs.read 返回的 `offset` 当前进量** → 那是「本次读取的起始位置」,
   于是原地打转 → 死循环 + 文件暴涨 + 部署卡死 200s。
   改为自算 len(对齐 cmdPush 成熟写法)+ 无进展即跳出。
4. **轮询用 withClient → 直接退出进程**:`withClientDirect` 在连接失败且是 ClientError 时
   调 `fail()` 退出 —— 轮询第一次连接必失败(agent 正在重启)→ CLI 立刻退出,
   报 E_NODE_OFFLINE,而**部署其实已成功**(本轮 3 次「失败」全是这个假象)。
   新增 `probeOnce()`:永不抛出的探测函数,专用于「等待上线/重启」类重试。
5. **盲等 9 秒**判定部署结果 → 改为**按墙钟**限时轮询指纹(90s),失败时报真实耗时。
6. **重启会切断连接**:该中断属预期,此前未处理会把成功误判为失败 → 显式吞掉并说明。

## 验证(本地全程真跑,非纸面)
用「起本地 agent → 造一个改动过的 bundle → 部署」的方式把整条链路跑通:
- 同版本 `--check` → 「已是最新」退出码 0 ✓
- 不同版本 `--check` → 报不一致退出码 2 ✓
- 完整部署 → 备份(979578B,`cmp` 逐字节一致)→ 上传 → 重启 → **指纹校验通过** ✓
- 防呆:文件过小/不存在被拒 ✓
- 单测 125 / e2e 27 全绿
2026-10-04 22:54:45 +08:00

47 lines
1.8 KiB
JavaScript
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.
#!/usr/bin/env node
/**
* 一条命令更新被控端(Windows):打包 → 部署 → 校验。
*
* 用法:
* node scripts/update-win.mjs [设备名] [--check] [--force]
* 设备名省略时用控制端配置里的 current
*
* 等价于:
* pnpm pack:win
* nodeagent --node <设备名> deploy release/nodeagent-win-x64/agent.mjs
*
* 为什么要有它:手动流程要记住「先 pack、产物在 release/nodeagent-win-x64/、再 deploy」
* 三个知识点,容易出错(尤其 pack 失败时 deploy 的是旧包 —— 真机踩过)。
* 本脚本在 pack 之后**校验产物确已更新**,再部署。
*/
import { execFileSync } from 'node:child_process';
import { existsSync, statSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const args = process.argv.slice(2);
const flags = args.filter((a) => a.startsWith('--'));
const node = args.find((a) => !a.startsWith('--')) ?? '';
const bundle = join(root, 'release', 'nodeagent-win-x64', 'agent.mjs');
const before = existsSync(bundle) ? statSync(bundle).mtimeMs : 0;
console.log('=== ① 打包(pnpm pack:win)===');
execFileSync('pnpm', ['pack:win'], { cwd: root, stdio: 'inherit' });
if (!existsSync(bundle)) {
console.error('✗ 打包后仍找不到产物:', bundle);
process.exit(1);
}
const after = statSync(bundle).mtimeMs;
if (after === before) {
console.error('✗ 产物时间戳未变化 —— 打包可能失败,拒绝部署旧包(这是刻意的保护)');
process.exit(1);
}
console.log(`✓ 产物已更新:${bundle}`);
console.log('=== ② 部署到被控端 ===');
const target = node ? ['--node', node] : [];
execFileSync('nodeagent', [...target, 'deploy', bundle, ...flags], { cwd: root, stdio: 'inherit' });