diff --git a/apps/agent/src/capabilities/system.ts b/apps/agent/src/capabilities/system.ts index 346e446..eee0c60 100644 --- a/apps/agent/src/capabilities/system.ts +++ b/apps/agent/src/capabilities/system.ts @@ -531,10 +531,15 @@ async function scheduleWindowsRestart(p: RestartParams): Promise { `$a = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument '${cmdLine.replace(/'/g, "''")}'`, // 不限时(默认 72h 后强制结束),保证 node 能长期运行 `$s = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Seconds 0)`, - // ⚠️ 必须用最高权限:agent 通常以管理员运行,普通权限的 taskkill 杀不掉它, + // ① 先试「最高权限」:agent 通常以管理员运行,普通权限的 taskkill 杀不掉它, // 否则会出现「新进程起来了但端口被占(EADDRINUSE)」。 `$p = New-ScheduledTaskPrincipal -UserId $env:USERNAME -RunLevel Highest -LogonType Interactive`, `Register-ScheduledTask -TaskName '${TASK}' -Action $a -Settings $s -Principal $p -Force | Out-Null`, + // ② 降级:**非管理员身份下 Highest 会被系统拒绝**(真机 2026-10-09 坐实: + // agent 以普通用户运行时,deploy/restart 一律报"注册重启任务失败 exit=1")。 + // 此时不指定 -Principal —— 默认继承当前用户权限级别(Limited), + // 注册能成功,且同权限下 taskkill 自己完全够用。 + `if ((Get-ScheduledTask -TaskName '${TASK}' -ErrorAction SilentlyContinue) -eq $null) { Register-ScheduledTask -TaskName '${TASK}' -Action $a -Settings $s -Force | Out-Null }`, // 校验注册真的成功(SilentlyContinue 会吞错,必须显式确认) `if ((Get-ScheduledTask -TaskName '${TASK}' -ErrorAction SilentlyContinue) -eq $null) { Write-Output 'register-failed'; exit 1 }`, `Start-ScheduledTask -TaskName '${TASK}'`, diff --git a/apps/cli/src/cmd/ops.ts b/apps/cli/src/cmd/ops.ts index f7b4a6b..f4b6b80 100644 --- a/apps/cli/src/cmd/ops.ts +++ b/apps/cli/src/cmd/ops.ts @@ -273,7 +273,10 @@ export async function cmdDeploy(localFile: string, opts: Options): Promise const uploadInfo = await withClient(async (c) => { // 备份走协议自带的 fs 能力(而非 shell 的 Copy-Item)—— // Copy-Item 是 Windows 专用,在 macOS 被控端上会静默失败(真机踩过:以为备份了,其实没有)。 - const backup = `${target}.bak-${Date.now()}`; + // 备份用**固定名**(覆盖式):此前用 `.bak-<时间戳>`,每部署一次就多一个文件 —— + // 真机 2026-10-09 发现 D:\Nodeagent 下堆了 15+ 个 bak(单文件 1MB,纯浪费)。 + // 部署回滚只需要「上一个可用版本」,一份就够,故改为覆盖写。 + const backup = `${target}.bak`; let backupPath = ''; try { const st = await c.invoke<{ size?: number; exists?: boolean }>(CapabilityNames.FsStat, { path: target }); @@ -329,6 +332,23 @@ export async function cmdDeploy(localFile: string, opts: Options): Promise offset += part.length; } console.log(`✓ 已上传 ${(buf.length / 1024).toFixed(0)} KB`); + + // best-effort 清理历史的时间戳备份(老格式 `.bak-`)。 + // 平台分流:Windows 用 PS,类 Unix 用 rm。清不掉不影响部署,只提示。 + try { + const isWin = String((info as { os?: string }).os ?? '').toLowerCase().startsWith('win'); + const cleanup = isWin + ? `Get-ChildItem '${target}.bak-*' -EA SilentlyContinue | Remove-Item -Force -EA SilentlyContinue; (Get-ChildItem '${target}.bak-*' -EA SilentlyContinue | Measure-Object).Count` + : `rm -f '${target}.bak-'* 2>/dev/null; ls '${target}.bak-'* 2>/dev/null | wc -l`; + const r = await c.invoke<{ stdout?: string }>(CapabilityNames.ShellExec, { command: cleanup, timeout_ms: 15_000 }); + const left = Number((r.data?.stdout ?? '').trim()); + if (r.status === 'ok' && Number.isFinite(left)) { + if (left === 0) console.log('✓ 已清理历史时间戳备份(老格式)'); + else console.log(`⚠️ 仍有 ${left} 个历史备份未清理(可手动删除)`); + } + } catch { + /* 清理是附加项,失败不影响部署结果 */ + } return { backupPath }; });