4 Commits
Author SHA1 Message Date
jkooe 6c650244bf chore(release): v1.4.2 2026-10-05 02:51:31 +08:00
jkooe a56d67c012 fix(v22): 自更新在 Windows 上必须**就地覆写**而非 rename(真机暴露的平台差异)
## 现象(真机 192.168.1.100 上拉取式更新)
`✗ E_EXECUTION_FAILED: 替换脚本失败` —— 下载与哈希校验都过了,挂在最后一步「替换」。

## 根因
Windows **不允许重命名/替换正被运行进程打开的文件**(EPERM/EBUSY),而 node.exe
正持有 agent.mjs。POSIX 可以 unlink 打开中的文件,所以本地(macOS)测不出来。
这也解释了为什么推送式 `deploy` 一直没问题:它走**就地覆写**,而覆写打开中的文件
在 Windows 上是允许的(只有删除/重命名被禁)。

## 修复
- 改为 `writeFileSync(scriptPath, buf)` 就地覆写(先备份 + 已校验的暂存内容)
- **写后校验**:重算入口文件 sha256,与刚校验过的下载比对;不一致立即从备份回滚
- 清理暂存文件;错误信息带上 backup_path / staged_path 便于人工兜底

## 顺带的真机结论(都已在 192.168.1.100 上验证)
- **被控端访问不了 github.com**(`fetch failed`)→ 国内网络现实;改用局域网/自建镜像即可
  (本轮用 `http://192.168.1.100:8081/...` 拉取成功 ✓)→ 故 URL 参数化是必需的,不是花活
- **拉取式更新在 Windows 上可用**(内容确实变化:构建哈希 4833b0b5 → 216328d5 ✓,
  重启后复核通过 ✓)
- **Windows 音频能力真机跑通**(Core Audio COM):🔇 静音 · 音量 77%,可读回真实状态
- **证书指纹钉住**:普通 agent 更新不影响钉住(证书未变 → 仍一致 ✓);只有证书重建才触发
  `E_CERT_MISMATCH` —— 符合设计意图
2026-10-05 02:50:48 +08:00
jkooe 4106ada876 chore(release): v1.4.1 2026-10-05 02:46:37 +08:00
jkooe f88738d36d fix(v22): 修正构建期注入的版本号带多余引号(真机暴露)
## 现象
真机 `nodeagent info` 显示 `被控端版本 : v"1.4.0" @ "e841fbb"` —— 值里带了引号。

## 根因
esbuild 的 `--define:K=V` 中 **V 是按 JS 表达式解析的**。原先写成
`--define:__AGENT_VERSION__='"1.4.0"'`(单引号包裹),单引号被当成**字符串内容**,
于是运行时值变成 `"1.4.0"`(含引号)。产物内联可见:
`return false ? "dev" : '"e841fbb"';`

## 修复
改用 `JSON.stringify(v)`(纯双引号)→ 产物内联为 `return false ? "dev" : "1.4.0";` ✓
真机复核:`被控端版本 : v1.4.0 @ 50ac4b9  构建 05abbbc79abf` ✓

## 附带真机确认(v1.4.0 部署)
- 推送式部署 + **指纹校验通过**(能力 42 项、PID 16664)
- **证书指纹 TOFU 钉住生效**,且普通 agent 更新**不影响**钉住(证书未变 → 仍一致 ✓)——
  这正是设计意图:只有证书重建才会触发 E_CERT_MISMATCH
- **Windows 音频能力真机跑通**(Core Audio COM 路径):`✓ 已应用:🔇 静音 · 音量 77%`,
  且能**读回真实状态**(确定性设置而非切换)—— 这补上了此前只在 macOS 验证的空缺
2026-10-05 02:45:51 +08:00
9 changed files with 37 additions and 12 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nodeagent/agent",
"version": "1.4.0",
"version": "1.4.2",
"private": true,
"type": "module",
"main": "./dist/index.js",
+25 -2
View File
@@ -167,15 +167,38 @@ export async function agentUpdate(args: Args): Promise<unknown> {
detail: err instanceof Error ? err.message : String(err),
});
}
// ⚠️ 必须**就地覆写**,不能用 rename 覆盖 —— Windows 不允许重命名/替换「正被运行进程
// 打开的文件」(EPERM/EBUSY),而 node.exe 正持有 agent.mjs。真机踩过:rename 必失败。
// 覆写打开中的文件是允许的(只有删除/重命名被禁),这也正是推送式 deploy 能工作的原因。
try {
renameSync(staged, scriptPath);
writeFileSync(scriptPath, buf);
} catch (err) {
throw new CapabilityError(ErrorCodes.EXECUTION_FAILED, '替换脚本失败', {
throw new CapabilityError(ErrorCodes.EXECUTION_FAILED, '写入脚本失败(已保留备份,可回滚)', {
detail: err instanceof Error ? err.message : String(err),
backup_path: backup,
staged_path: staged,
});
}
// 写后校验:内容必须与刚校验过的下载一致,否则立即回滚(防止截断/半写)
const written = createHash('sha256').update(readFileSync(scriptPath)).digest('hex');
if (written !== got) {
try {
copyFileSync(backup, scriptPath); // 回滚
} catch {
/* 回滚失败时靠 backup_path 人工兜底 */
}
throw new CapabilityError(ErrorCodes.EXECUTION_FAILED, '写入后校验不一致,已自动回滚', {
expected: got.slice(0, 12),
actual: written.slice(0, 12),
backup_path: backup,
});
}
try {
unlinkSync(staged); // 暂存文件已并入入口文件,及时清理
} catch {
/* 清理失败不影响正确性 */
}
audit({
type: 'agent.update',
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nodeagent/cli",
"version": "1.4.0",
"version": "1.4.2",
"private": true,
"type": "module",
"main": "./dist/index.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nodeagent/hub",
"version": "1.4.0",
"version": "1.4.2",
"private": true,
"type": "module",
"main": "./dist/index.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nodeagent/mcp",
"version": "1.4.0",
"version": "1.4.2",
"private": true,
"type": "module",
"main": "./dist/index.js",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "nodeagent",
"private": true,
"version": "1.4.0",
"version": "1.4.2",
"description": "跨机 AI 接管框架 —— 让 Mac 上的 AI 无缝接管 Windows,装软件、查状态",
"type": "module",
"engines": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nodeagent/client",
"version": "1.4.0",
"version": "1.4.2",
"private": true,
"type": "module",
"main": "./dist/index.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nodeagent/protocol",
"version": "1.4.0",
"version": "1.4.2",
"private": true,
"type": "module",
"main": "./dist/index.js",
+5 -3
View File
@@ -97,9 +97,11 @@ async function main() {
'--target=node22',
'--format=esm',
'--banner:js=import { createRequire } from "node:module"; const require = createRequire(import.meta.url);',
`--define:__AGENT_VERSION__='"${pkg.version}"'`,
`--define:__BUILD_COMMIT__='"${commit}"'`,
`--define:__BUILD_TIME__='"${builtAt}"'`,
// 注意:define 的值按 **JS 表达式** 解析,故用双引号包裹;写成单引号会让引号成为值的一部分
// (真机踩过:版本显示成 v"1.4.0")。
`--define:__AGENT_VERSION__=${JSON.stringify(pkg.version)}`,
`--define:__BUILD_COMMIT__=${JSON.stringify(commit)}`,
`--define:__BUILD_TIME__=${JSON.stringify(builtAt)}`,
`--outfile=${bundleOut}`,
],
{ stdio: 'inherit' },