4 Commits
Author SHA1 Message Date
jkooe 3ff8e86fe8 chore(release): v1.4.3 2026-10-05 11:35:53 +08:00
jkooe 7689a1f80c fix(v22): 构建改为**确定性** —— 去掉注入的构建时间,否则「指纹比对是否最新」永远失真
## 现象(真机执行 `deploy --check`)
明明被控端就跑着 v1.4.2、本地也是 v1.4.2,却报「版本不一致」:
    本地 : 47326da097c7  (976 KB)
    远端 : 6499b3f7ad3c  (node v22.20.0)

## 根因
打包时注入了 `__BUILD_TIME__`,于是**同一份代码每次打包的字节都不同** →
内容哈希无法充当「构建身份」,`--check` / 部署后校验只能靠"这次上传的文件"比对,
跨批次就必然误报。

## 修复
- `pack.mjs` 不再注入构建时间(版本与 commit 仍是确定性的)
- `system.info.build.built_at` 改用**入口文件的 mtime**(mtime 不在文件内容里,
  因此不破坏字节确定性,同时仍能反映构建时刻)

## 验证
- 连打两次包(间隔 2s),指纹完全相同:`b13ce7d3ba74d0e0b293649f` ✓
- 真机更新:指纹 b13ce7d3ba74、42 项能力、证书钉住一致 ✓
- **再次执行 `--check` → 「✓ 已是最新,无需更新」**(此前同版本却报不一致)✓

## 意义
产物变为**可复现**(reproducible):同一 commit 打出同一份字节,
sha256 可以放心用作分发与更新的身份标识 —— 这对一条「下载即执行代码」的更新通道很关键。
2026-10-05 11:27:28 +08:00
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
10 changed files with 40 additions and 15 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nodeagent/agent",
"version": "1.4.1",
"version": "1.4.3",
"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',
+5 -5
View File
@@ -55,7 +55,6 @@ async function isAdmin(): Promise<boolean> {
// 故一律用 typeof 探测,绝不能直接引用(否则 dev 启动就 ReferenceError)。
declare const __AGENT_VERSION__: string | undefined;
declare const __BUILD_COMMIT__: string | undefined;
declare const __BUILD_TIME__: string | undefined;
/** v21:读取当前生效的来源白名单(用于 system.info 上报)。 */
function configAllowFrom(): string[] | null {
@@ -67,10 +66,9 @@ function configAllowFrom(): string[] | null {
}
}
function injected(name: 'version' | 'commit' | 'time'): string {
function injected(name: 'version' | 'commit'): string {
if (name === 'version') return typeof __AGENT_VERSION__ === 'undefined' ? 'dev' : __AGENT_VERSION__;
if (name === 'commit') return typeof __BUILD_COMMIT__ === 'undefined' ? 'dev' : __BUILD_COMMIT__;
return typeof __BUILD_TIME__ === 'undefined' ? 'dev' : __BUILD_TIME__;
return typeof __BUILD_COMMIT__ === 'undefined' ? 'dev' : __BUILD_COMMIT__;
}
const STARTED_AT = Date.now();
@@ -118,7 +116,9 @@ export function buildInfo(): {
// v20:语义化版本 + 提交 + 构建时间(来自构建期注入)
version: injected('version'),
commit: injected('commit'),
built_at: injected('time'),
// 构建时间取入口文件的 mtime(而非构建期注入)—— 注入会让每次打包字节不同,
// 破坏「指纹 = 构建身份」的确定性(v22 真机踩过)
built_at: buildCache.mtime_ms ? new Date(buildCache.mtime_ms).toISOString() : 'unknown',
// v21:TLS 证书指纹(控制端据此钉住被控端身份)
cert_sha256: currentCertFingerprint(),
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nodeagent/cli",
"version": "1.4.1",
"version": "1.4.3",
"private": true,
"type": "module",
"main": "./dist/index.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nodeagent/hub",
"version": "1.4.1",
"version": "1.4.3",
"private": true,
"type": "module",
"main": "./dist/index.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nodeagent/mcp",
"version": "1.4.1",
"version": "1.4.3",
"private": true,
"type": "module",
"main": "./dist/index.js",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "nodeagent",
"private": true,
"version": "1.4.1",
"version": "1.4.3",
"description": "跨机 AI 接管框架 —— 让 Mac 上的 AI 无缝接管 Windows,装软件、查状态",
"type": "module",
"engines": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nodeagent/client",
"version": "1.4.1",
"version": "1.4.3",
"private": true,
"type": "module",
"main": "./dist/index.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nodeagent/protocol",
"version": "1.4.1",
"version": "1.4.3",
"private": true,
"type": "module",
"main": "./dist/index.js",
+3 -1
View File
@@ -97,11 +97,13 @@ async function main() {
'--target=node22',
'--format=esm',
'--banner:js=import { createRequire } from "node:module"; const require = createRequire(import.meta.url);',
// 刻意**不注入构建时间**:注入后同一份代码每次打包字节都不同,
// 「指纹比对是否最新」就永远失真(真机踩过:明明同版本却报不一致)。
// 构建时间改由入口文件的 mtime 反映(见 system.ts 的 build.built_at)。
// 注意: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' },