Commit Graph
18 Commits
Author SHA1 Message Date
jkooe cedc46d090 feat(v20): 版本纪律 + 发版自动化(对齐「做好版本功能 + GitHub 同步资产」)
## 一、版本功能(防新旧混用)
1. **单一版本源 + 构建注入**:根 package.json 的 version、git sha、构建时间经 esbuild
   `--define` 注入 agent;`system.info.build` 新增 `version/commit/built_at`(原有 `hash` 指纹不变)。
   开发态用 typeof 探测(未注入也不崩)。
2. **握手携带版本**:`auth_ok` 新增可选字段 `agent_version` / `build` / `protocol`(只增不减 → 旧控制端忽略即可)。
3. **能力级兼容比对**:新增纯函数 `capabilityDiff(local, remote)` ——
   - `missingOnRemote`:本地有远端没有 → **远端较旧**,提示更新被控端
   - `unknownLocally`:远端有本地没有 → **本控制端较旧**(不影响既有功能)
   控制端不比版本号而比**能力集合**:版本号可能相同而内容不同,能力集合不会骗人。
4. **CLI 体检**:`nodeagent info` / `connect` 直接显示被控端版本与能力对齐情况,
   把"调用时才炸"变成一句人话提示。

## 二、发版自动化(GitHub 资产同步)
`pnpm release <x.y.z|patch|minor|major>`:前置检查(干净/在 main/与远端同步)→ 改 7 个 package.json
→ 构建+打包 → 生成资产 → 提交打标签推送 → `gh release create` 上传。
- 资产:`agent.mjs`(**自更新载荷**)、`nodeagent-win-x64.zip`(人工安装包)、
  `latest.json`(version/commit/能力数 + 两个资产的 url 与 sha256)、`SHA256SUMS`
- `--dry-run` 无副作用演练;`--no-publish` 只到本地标签
- 能力数从 dist 动态取(避免写死漂移);刻意排除 `.github/`(缺 workflow 权限推不了)

## 三、兼容性契约文档
新增 `docs/VERSIONING.md`:semver 各位的含义、**只增不改**六条硬规则、
升级顺序建议(**先被控端**:新被控端向后兼容旧 CLI)、两条更新路径、发版流程、排查手册。

## 验证
- 单测 147 → **149**(新增 capabilityDiff 用例:远端过旧 / 本机过旧 / 完全一致 / 真实清单自比无差异)
- e2e 28 全绿;`node scripts/release.mjs 1.0.0 --dry-run` 演练通过且工作区无副作用
2026-10-04 23:54:51 +08:00
jkooe e95a0248da feat(v19): 拉取式自更新 system.agent.update(42 项)—— 回答「能否直接用 GitHub 做更新服务器」
## 结论:能,而且这是「控制端与被控端不可达」场景的正解
`deploy`(推送式)要求两者可达;本能力只要求**被控端能上网**,因此跨网段/NAT/异地都能更新。
GitHub Release / raw / 自建镜像 / 任意 http(s) 地址均可 —— 地址是参数,不写死。

## 实现(apps/agent/src/capabilities/selfupdate.ts)
下载 → **校验 sha256** → 备份 → **原子替换**(同目录 rename)→ 受控重启(复用 v7)→ CLI 复核指纹
- 出口:CLI `nodeagent update --url <u> --sha256 <h> [--dry-run]` · MCP `na_agent_update`
- 审计新增 `agent.update` 事件(记录 url / 前后指纹 / 字节数 / 备份路径)

## 安全底线(否则等于开放远程代码执行)
1. **sha256 必填**,且逐字节校验通过才替换;不符即丢弃并明确报错(未改动任何文件)
2. 只接受 http/https,拒绝 file:// 等其它 scheme
3. 大小上限 64MB + 下载超时(默认 120s)
4. **先备份再原子替换**,失败可回滚(返回 backup_path)
5. `dry_run` 只下载校验、不动文件 —— 用于「先确认能不能更」
6. 不用 fetch 的内置代理(Node 未暴露 undici):国内访问 GitHub 不稳时,
   把 url 指向**自建镜像/加速地址**即可,能力本身接受任意 http(s)

## 真机/本地验证(全程真跑,非纸面)
- e2e 新增:本地 HTTP 服务真跑下载 —— 正确哈希通过 / **错误哈希被拒** / 404 明确报错 /
  非 http(s) scheme 被拒 / 缺 sha256 协议层拒绝 ✓
- 完整流程本地实测:`dry-run 预检` → `真更新`(966KB)→ 备份逐字节一致 → 重启 →
  **复核到新指纹** → 能力 42 项 ✓
- 顺带修:返回值漏 `incoming_hash` 导致 CLI 复核被跳过(真机踩过)
- 单测 126 / e2e 28 全绿

## 已知约束(写进文档)
1. **首次仍要一次性安装**(install.ps1)—— 自更新能力本身得先在机器上,鸡生蛋无解
2. `node.exe`(Node 运行时)不在自更新范围,只换 agent.mjs
2026-10-04 23:03:37 +08:00
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
jkooe 9ef8978b92 feat(scripts): 网络救援脚本 rescue-network.ps1(网络变更失联后的本地恢复)
## 为什么需要它
`system.net.apply` 是两阶段提交(未确认自动回滚),但**万一回滚本身也失败**
(本次真机事故就是:变更脚本把地址删了却没加上),机器会落到 APIPA(169.254.x.x) 而彻底失联。
那时远程已无路可走,只能本地恢复 —— 这个脚本就是为那一天准备的。

## 能力
- `-ListOnly`:先看清各网卡状态/地址/默认路由(含「已连接但无地址」的失联特征标注)
- 自动识别失联网卡(已连接但无有效 IPv4),也可 -Interface 指定
- 带 -Ip/-PrefixLength/-Gateway/-Dns 恢复静态;不带 -Ip 则切回 DHCP
- **安全顺序**(与 agent 内实现一致):先加新地址 → 清旧地址与残留默认路由 → 补默认路由
- 自检并给出下一步命令(含可直接复制的 Mac 侧 connect 命令)

## 用法
    powershell -ExecutionPolicy Bypass -File rescue-network.ps1 -ListOnly
    powershell -ExecutionPolicy Bypass -File rescue-network.ps1 -Interface 'Ethernet' `
        -Ip 192.168.1.100 -PrefixLength 24 -Gateway 192.168.0.1
    powershell -ExecutionPolicy Bypass -File rescue-network.ps1 -Interface 'Ethernet'   # 切回 DHCP
2026-09-30 11:10:23 +08:00
jkooe 4d1aeada07 feat(v12.4): 事件直达 MCP 宿主(实时推送)+ 修两个真 bug
## 背景
v12 的事件订阅对 MCP 只走「被控端缓冲 + na_event_poll 拉取」。MCP 协议本身支持
服务端 -> 客户端通知,故补上实时推送:na_event_watch 的 notify=true 开启,
事件经 MCP 标准日志通知(notifications/message)即时送达宿主。

## 两条通路并存(互相兜底)
1) 推送:onEvent -> server.notification(notifications/message, logger=nodeagent.event)
   —— 宿主若不展示日志通知,则自动退化为拉取,无副作用
2) 拉取:na_event_poll(增量游标)
na_event_list 额外返回推送开关与已转发条数,便于判断该用哪条通路。

## 真机修掉的两个 bug
1. **notify 参数泄漏到能力侧**:notify 是 MCP 层开关,原样透传被被控端
   additionalProperties:false 拒绝(E_PARAM_INVALID)。改为在 toArgs 里显式构造,
   只带能力真正接受的字段。
2. **通知未声明 logging 能力导致 MCP 进程崩溃**:SDK 的 notification() 在未声明
   logging 能力时抛错,且因返回 Promise 未 catch -> 未处理拒绝直接终止进程
   (真机实测:watch 成功后一有事件,整个 MCP 服务就退出)。
   修法:capabilities 声明 logging + 通知调用补 catch(失败静默降级为拉取)。

## 端到端验证(scripts/verify-mcp-events.mjs,已收进仓库)
真实 MCP stdio 客户端驱动,对生产被控端实测:
- initialize ok;tools/list 31 个工具、事件类 4 个
- na_event_watch(notify=true) -> watch_id
- 制造 创建/修改/删除 -> **收到 3 条推送通知**(file created_or_removed / modified / ...)
- na_event_poll 返回缓冲事件(seq/watch_id/ts/action/target)
- na_event_list 显示「推送已开启(已转发 3 条)」
结论:推送 ✅ 生效;拉取 ✅ 生效

单测 89/89、e2e 21/21 全绿
2026-09-29 15:47:34 +08:00
jkooe 2dccec9a5c feat(D): 生产化 —— 无人值守模式 / 一键升级 / 文档口径更新
## D1 无人值守(install.ps1)
- 新增 -Unattended(S4U 登录类型):**无需保存密码,注销/重启后仍自动运行**
- 新增 -AtStartup:追加开机触发器;任务设置补 StartWhenAvailable(错过触发后补跑)
- **诚实标注能力边界**:S4U 运行在非交互会话,GUI 能力(截屏/UIA/键鼠)不可用;
  -Unattended 与 -AllowInput 冲突时自动关闭输入控制并回写配置
- 说明文档给出两种模式的对照表(交互模式支持 GUI,但注销即停)

## D2 一键升级(nodeagent deploy)
- system.info 新增自描述字段:pid / agent_script / agent_home / node_path
- deploy 流程:读取被控端入口路径 → 备份旧文件 → 分块上传新 agent.mjs
  → 调 system.agent.restart 受控重启 → 等待恢复并复验能力数
- 真机验证:
  * 首次(旧版 agent 无自描述):--path 指定 → 上传 853KB → PID 16492 / 31 项能力
  * 二次:自动识别路径 + 自动备份 → PID 34120 / 31 项能力(免 --path)

## D3 CI(待推送)
- windows-e2e.yml 增加单元测试步骤(3 行);因 OAuth token 缺 workflow scope,
  该改动暂留本地未推送,需用户授权后推送

## D4 文档口径更新(README)
- 能力总览补齐 31 项(新增:自持/异步任务/窗口/录屏/剪贴板/拖拽/审计校验)
- 统计数字更正:19 项能力 -> 31,15 个 MCP 工具 -> 26,补 61 项单元测试
- 路线图补 v7~v11+;安全章节补密钥库/Audit 链/ACL 细化
- 命令速查新增:restart / deploy / daemon / fanout / bg / tasks / clip / record /
  window.* / screen.find / mouse drag
- MCP 工具表按新工具重排;安装章节新增两种运行模式对照表

单测 61/61、e2e 21/21 全绿
2026-09-29 11:47:15 +08:00
jkooe fac51046cf feat(v10.1): CLI 常驻 daemon(nodeagentd)—— 连接池复用,批量调用提速
## 机制
- nodeagentd 常驻进程监听 UDS($TMPDIR/nodeagentd.sock),持有与被控端的
  长连接池(autoReconnect,每设备一条,LRU 上限 8,空闲 5min 释放)
- CLI withClient 检测 socket 存活 → 经 daemon JSON-lines 转发 invoke(虚拟 client);
  daemon 自身错误静默回退直连,业务错误原样透传 —— 零行为变化,纯加速层
- CLI 新命令: nodeagent daemon start|stop|status;打包新增 nodeagentd 产物

## 实测(真机)
- 轻调用(info): 752ms -> 527ms(省 TLS+hello/challenge/auth,约 30%)
- 轻调用 5 连发(clip.get): 2.58s -> 2.41s
- 重调用(ps 远端 CPU 采样 1.4s)提速不显著 —— 瓶颈在远端执行而非握手,符合预期
- unit 26/26、e2e 21/21 全绿
2026-09-29 10:55:38 +08:00
jkooe 3316f08c92 fix: 修复 Windows 安装后无法启动 —— PowerShell 写入的 JSON 带 UTF-8 BOM
真机报错:启动失败: Unexpected token '', "{"a"... is not valid JSON
根因:Windows PowerShell 5.1 的 Set-Content -Encoding UTF8 会写入 BOM(U+FEFF),
      Node 侧 JSON.parse 拒绝解析带 BOM 的文本。

双管齐下修复:
1. 写端 install.ps1:改用 [System.IO.File]::WriteAllText + UTF8Encoding($false)
   写出无 BOM 的 UTF-8
2. 读端容错(兼容已被写坏的配置):
   - apps/agent/src/config.ts:读配置时剥离开头 BOM
   - packages/client/src/config.ts:同上

验证:构造带 BOM 的 agent.json(ef bb bf 开头),agent 可正常启动;
单元 26/26、E2E 21/21 全绿
2026-09-27 19:32:53 +08:00
jkooe b350a41803 fix(install.ps1): 安装后验证端口监听,并在连接命令中填入真实 IP
真机安装反馈暴露两处缺陷:
1. 只打印计划任务状态(Ready)就宣告完成,未确认端口真的在监听 ——
   用户按提示连接时会超时,却得不到任何线索。
   现改为轮询等待端口进入 Listen(最多 15s),失败时打印任务最后运行结果
   与三步排查命令(手动启动任务 / 查进程 / 前台运行看报错)。
2. 连接提示里的 <THIS-IP> 是字面占位符,用户照抄会失败。
   现改为填入实际探测到的本机 IPv4。
2026-09-27 19:29:56 +08:00
jkooe 665c0e85c4 feat: Mac 控制端一键安装(pack:mac + install-macos.sh)
Mac 是控制端,不需要 Windows 安装包,但此前必须 cd 到仓库敲
node apps/cli/dist/index.js,太啰嗦。

新增:
- pnpm pack:mac(scripts/pack-macos.mjs):把 CLI 与 MCP 打包为带 shebang 的
  可执行单文件(195KB / 772KB),免 node_modules
- scripts/install-macos.sh:安装到 ~/.local/bin(已在 PATH)+ 打印 MCP 配置片段
- Mac 端不打 Node 运行时(控制端本机已有 Node,体积从约 50MB 降到约 1MB)

修复:execFileSync 不能接收带参数的命令字符串(改用 execSync)

验证:./nodeagent --help 与 MCP tools/list(18) 可直接执行;
install-macos.sh 安装后可在任意目录调用 nodeagent
2026-09-27 19:24:57 +08:00
jkooe 4c9b44288c fix(pack): esbuild 输入改用绝对路径,支持从任意目录调用
此前 pack.mjs 用相对路径 apps/agent/dist/index.js 作为 esbuild 输入,
从项目目录外调用(node <abs>/scripts/pack.mjs)时报
Could not resolve apps/agent/dist/index.js。

改为基于 __dirname 的绝对路径;README 补充两种调用方式并注明需在项目目录执行。

验证:cd /tmp && node <abs>/scripts/pack.mjs 可正常产出 32MB 安装包
2026-09-27 19:21:21 +08:00
jkooe 192e08b171 feat: Windows 便携安装包构建(免装 Node.js)
新增 pnpm pack:win(scripts/pack.mjs),一键产出 nodeagent-win-x64.zip:
- esbuild 打包被控端为单文件 bundle(含全部依赖,约 800KB)
- 自动下载 Node.js Windows x64 运行时(npmmirror,node.exe)
- 组装便携包并打 zip(node.exe + agent.mjs + install.ps1 + uninstall.ps1)

改造 install.ps1:
- 优先使用包内 node.exe + agent.mjs(便携包模式)
- 否则回退系统 Node.js + 仓库构建产物(开发模式)
- 无 Node.js 的 Windows 机器也能一键安装

配套:
- .gitignore 忽略 release/(产物体积大,不入库)
- README 安装说明改为「安装包(推荐)/ 开发模式」两方式

验证:打包产物 32MB;解压后 agent.mjs 可独立运行(19 项能力、wss 监听);
esbuild bundle 通过 banner 提供 createRequire 兼容 CJS 依赖(node-forge)
2026-09-27 18:03:04 +08:00
jkooe f4c605cc6d test+fix: 单元测试套件 + 两处细节修复
单元测试(新增,node:test 零依赖):
- tests/unit/: ulid / hmac / ed25519 / acl / validate 共 26 项用例
- 覆盖最关键的安全函数:签名验签、ACL 授权、参数校验
- 新增 pnpm test:unit;CI 增加单元测试步骤

细节修复:
1. screen.info 的 scale 硬编码为 1 → 改读注册表 HKCU AppliedDPI,
   真实反映 Windows DPI 缩放(100% -> 1.0, 150% -> 1.5)
2. verify.mjs 升级:支持多设备配置(resolveTarget)+ ed25519 模式 + Hub 中转
   (此前直接用旧的扁平配置 + 仅 psk,配了多设备会取不到 host)

验证:构建 + 类型检查全绿;单元 26/26;E2E 21/21;verify.mjs 13/0/3
2026-09-27 17:04:21 +08:00
jkooe 3d0eb1c696 feat(v4): 无感体验 —— 局域网自动发现 + 断线自动重连
自动发现(免手抄 IP):
- 被控端 UDP 心跳广播(默认 5s 一次,目标 255.255.255.255:8766)
- 控制端 Discovery 监听 + TTL 设备表(默认 30s 过期)
- 报文不含任何凭据;可 discovery.enabled:false 关闭
- 控制端取「报文来源 IP」作为可达地址(比被控端自报地址可靠)
- CLI: nodeagent discover [--wait 5];MCP: na_discover

断线自动重连:
- client 新增 autoReconnect(指数退避 1s→30s + ±15% 抖动)
- 重连前清空在途请求(统一 E_NODE_OFFLINE),避免悬挂
- onStateChange 回调(connected/reconnecting/closed)
- close() 会停止重连,不会关不掉

修复:UDP socket 需先 bind 再 setBroadcast(未绑定时报 EBADF)

验证:本地 E2E 19/19(新增发现 + 重连两用例);构建与类型检查全绿
2026-09-27 13:11:13 +08:00
jkooe 5b7e967868 feat(v3): 零信任安全 —— Ed25519 身份认证 + 能力级 ACL
认证(两模式并存,默认 psk 向后兼容):
- 新增 auth_mode: psk | ed25519
- ed25519:每控制端独立密钥对,挑战-应答签名验证(替代共享密钥)
- 密钥用 Node 内置 crypto 生成(零新依赖),Base64(DER) 单行格式
- 私钥存 <数据目录>/keys/ed25519.json(600 权限,永不外传)

授权(能力级 ACL):
- acl.clients[] 按调用方登记 pubkey + allow/deny(glob 匹配)
- 判定顺序 deny → allow → default_effect(默认拒绝)
- 未注册身份 / 验签失败 → 握手拒绝;未授权能力 → E_ACL_DENIED
- auth_ok 下发 authorized 子集,CLI 以 授权/未授权 标识展示授权矩阵

接入:
- CLI: keygen(生成密钥 + 打印 ACL 配置片段)、connect --auth-mode ed25519
- MCP: 按 auth_mode 自动选择认证方式
- install.ps1: 显式写 auth_mode + 零信任指引

验证:本地 E2E 17/17(新增零信任四项断言);手动端到端复验通过
2026-09-27 11:01:58 +08:00
jkooe 792937c6b0 feat(v2): 图形接管 —— 屏幕感知 + 输入控制
新增 7 项能力(总计 14):
- screen.info / screen.capture:显示器信息、截屏(region/scale/format)
- input.mouse.move/click/scroll、input.key.type/press:鼠标键盘控制

安全设计:
- input.* 为高危能力,由被控端 allow_input 开关统一管控,默认禁用
- 未开启时返回 E_CAPABILITY_DISABLED(附开启指引)
- 文本输入经 Base64 传参、PowerShell 侧解码,杜绝内容注入
- 按键名走白名单映射(虚拟键码),非法键名直接拒绝

实现:纯 PowerShell(System.Drawing 截屏 + user32.dll SendInput),
无原生编译、无第三方依赖。

接入:
- CLI: screen / screenshot / mouse / key 命令
- MCP: na_screenshot(直接返回图像)/ na_mouse / na_key
- install.ps1: 新增 -AllowInput 开关
- verify.mjs: 新增 v2.1~v2.3 验收项

验证:本地 E2E 16/16;远程验收 12 通过 / 0 失败
2026-09-27 03:43:10 +08:00
jkooe f1506b3fd0 test: 新增自动化真机验证体系
- tests/e2e/windows.mjs: Windows 专属能力测试(service.list / app.list / winget 装软件 / cmd 外壳)
- tests/e2e/run.mjs: service.list 断言改为平台感知(Windows 期望成功)
- scripts/verify.mjs: 远程验收脚本,对齐 PRD FR-01~FR-08 逐条验收 + Markdown 报告
- package.json: 新增 test:windows / verify 脚本
- README: 补充自动化真机验证三层体系说明

注: .github/workflows/windows-e2e.yml 需 workflow scope,待授权后单独提交
2026-09-27 01:24:52 +08:00
jkooe 77c024703a feat: nodeagent v1 初期开发完成
- packages/protocol: 协议定义(信封/方法/错误码/能力schema/校验/HMAC/ULID)
- packages/client: 控制端客户端库(连接/握手/invoke/超时)
- apps/agent: Windows 被控端(监听/HMAC认证/能力分发/7项能力/进程执行)
- apps/cli: 控制端 CLI(connect/info/status/ps/services/exec/apps/install/list/invoke)
- apps/mcp: MCP server(na_* 工具,接入 WorkBuddy)
- scripts: install.ps1 / uninstall.ps1(开机自启 + 防火墙 + 密钥生成)
- tests/e2e: 端到端测试 13/13 通过(握手/能力/超时强杀/参数校验/鉴权失败/未认证)
2026-09-27 01:06:35 +08:00