From a56d67c012ab41d5bb2117908624d1f43ec4eb4a Mon Sep 17 00:00:00 2001 From: jkooe <62494645+jkooe@users.noreply.github.com> Date: Mon, 5 Oct 2026 02:50:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(v22):=20=E8=87=AA=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=9C=A8=20Windows=20=E4=B8=8A=E5=BF=85=E9=A1=BB**=E5=B0=B1?= =?UTF-8?q?=E5=9C=B0=E8=A6=86=E5=86=99**=E8=80=8C=E9=9D=9E=20rename?= =?UTF-8?q?=EF=BC=88=E7=9C=9F=E6=9C=BA=E6=9A=B4=E9=9C=B2=E7=9A=84=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E5=B7=AE=E5=BC=82=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 现象(真机 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` —— 符合设计意图 --- apps/agent/src/capabilities/selfupdate.ts | 27 +++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/apps/agent/src/capabilities/selfupdate.ts b/apps/agent/src/capabilities/selfupdate.ts index a79cf44..d84340c 100644 --- a/apps/agent/src/capabilities/selfupdate.ts +++ b/apps/agent/src/capabilities/selfupdate.ts @@ -167,15 +167,38 @@ export async function agentUpdate(args: Args): Promise { 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',