## 皇上的问题 「这个版本如果有更新,可以在客户端上便捷的更新吗?」 答案:本来就有 `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 全绿
47 lines
1.8 KiB
JavaScript
47 lines
1.8 KiB
JavaScript
#!/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' });
|