## 结论:能,而且这是「控制端与被控端不可达」场景的正解 `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
150 lines
7.2 KiB
PowerShell
150 lines
7.2 KiB
PowerShell
<#
|
||
.SYNOPSIS
|
||
nodeagent 网络救援脚本 —— 被控端因网络变更失联后的本地恢复。
|
||
|
||
.DESCRIPTION
|
||
背景:`system.net.apply` 是两阶段提交(未确认自动回滚),但**如果回滚本身也失败**
|
||
(例如变更脚本把地址删掉了却没加上),机器可能落到 APIPA(169.254.x.x)而彻底失联。
|
||
这个脚本就是为那一天准备的:在**被控端本机**以管理员运行,把网络恢复到可用状态。
|
||
|
||
它做的事:
|
||
1. 列出所有网卡与当前地址(先看清现状)
|
||
2. 找出「有物理连接、但没有有效 IPv4」的网卡(典型的失联状态)
|
||
3. 把指定/自动识别的网卡设回给定静态地址,或切回 DHCP
|
||
4. 用**原生 NetTCPIP cmdlet** 按安全顺序执行:
|
||
先加地址 → 再清其它地址与残留默认路由 → 最后补默认路由
|
||
(顺序颠倒会踩 Windows 的「already exists」而再次失败 —— 真机事故根因)
|
||
|
||
.PARAMETER Interface
|
||
网卡别名(如「Ethernet」)。省略则自动选取「已连接但无有效 IPv4」的那块。
|
||
|
||
.PARAMETER Ip / PrefixLength / Gateway / Dns
|
||
要恢复的静态配置;省略 Ip 则切回 DHCP。
|
||
|
||
.PARAMETER ListOnly
|
||
只看现状,不做任何变更。
|
||
|
||
.EXAMPLE
|
||
# 先看现状
|
||
powershell -ExecutionPolicy Bypass -File rescue-network.ps1 -ListOnly
|
||
|
||
.EXAMPLE
|
||
# 恢复为原静态地址(最常用)
|
||
powershell -ExecutionPolicy Bypass -File rescue-network.ps1 `
|
||
-Interface 'Ethernet' -Ip 192.168.1.100 -PrefixLength 24 -Gateway 192.168.0.1
|
||
|
||
.EXAMPLE
|
||
# 切回 DHCP(家里路由器会重新分配地址)
|
||
powershell -ExecutionPolicy Bypass -File rescue-network.ps1 -Interface 'Ethernet'
|
||
#>
|
||
[CmdletBinding()]
|
||
param(
|
||
[string]$Interface,
|
||
[string]$Ip,
|
||
[int]$PrefixLength = 24,
|
||
[string]$Gateway,
|
||
[string[]]$Dns,
|
||
[switch]$ListOnly
|
||
)
|
||
|
||
$ErrorActionPreference = 'Continue'
|
||
|
||
function Write-Step($m) { Write-Host "[*] $m" -ForegroundColor Cyan }
|
||
function Write-Ok($m) { Write-Host "[+] $m" -ForegroundColor Green }
|
||
function Write-Warn2($m) { Write-Host "[!] $m" -ForegroundColor Yellow }
|
||
function Write-Err2($m) { Write-Host "[x] $m" -ForegroundColor Red }
|
||
|
||
Write-Host ""
|
||
Write-Host " nodeagent 网络救援" -ForegroundColor White
|
||
Write-Host " ------------------" -ForegroundColor DarkGray
|
||
|
||
# ---------- 1. 现状 ----------
|
||
Write-Step "当前网卡与地址:"
|
||
$adapters = Get-NetAdapter | Sort-Object ifIndex
|
||
foreach ($a in $adapters) {
|
||
$addrs = @(Get-NetIPAddress -InterfaceIndex $a.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue |
|
||
Where-Object { $_.PrefixOrigin -ne 'WellKnown' } | ForEach-Object { "$($_.IPAddress)/$($_.PrefixLength)" })
|
||
$dhcp = (Get-NetIPInterface -InterfaceIndex $a.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue).Dhcp
|
||
$mark = if ($a.Status -eq 'Up' -and $addrs.Count -eq 0) { ' <== 已连接但无地址(疑似失联)' } else { '' }
|
||
Write-Host (" {0,-22} {1,-8} dhcp={2,-8} {3}{4}" -f $a.Name, $a.Status, $dhcp, ($addrs -join ', '), $mark)
|
||
}
|
||
|
||
$gw = @(Get-NetRoute -AddressFamily IPv4 -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue |
|
||
ForEach-Object { "$($_.NextHop) (ifIndex $($_.ifIndex))" })
|
||
# 注意:PowerShell 5.1 的 if 不能当表达式内联在括号里 —— 必须先算好再拼(写错会直接语法错误)
|
||
$gwText = if ($gw.Count -gt 0) { $gw -join ', ' } else { '(无 —— 这就是回包发不出去的原因)' }
|
||
Write-Step ("默认路由: " + $gwText)
|
||
|
||
if ($ListOnly) { Write-Host ""; Write-Ok "仅查看模式,未做任何变更"; exit 0 }
|
||
|
||
# ---------- 2. 选目标网卡 ----------
|
||
if (-not $Interface) {
|
||
$cand = $adapters | Where-Object {
|
||
$_.Status -eq 'Up' -and
|
||
@(Get-NetIPAddress -InterfaceIndex $_.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue |
|
||
Where-Object { $_.PrefixOrigin -ne 'WellKnown' }).Count -eq 0
|
||
} | Select-Object -First 1
|
||
if (-not $cand) {
|
||
$cand = $adapters | Where-Object { $_.Status -eq 'Up' } | Select-Object -First 1
|
||
}
|
||
if (-not $cand) { Write-Err2 "找不到可用的已连接网卡,请用 -Interface 指定"; exit 1 }
|
||
$Interface = $cand.Name
|
||
Write-Warn2 "未指定网卡,自动选用:$Interface"
|
||
}
|
||
|
||
if (-not (Get-NetAdapter -Name $Interface -ErrorAction SilentlyContinue)) {
|
||
Write-Err2 "网卡不存在:$Interface"; exit 1
|
||
}
|
||
|
||
# ---------- 3. 执行 ----------
|
||
if (-not $Ip) {
|
||
Write-Step "切回 DHCP:$Interface"
|
||
Set-NetIPInterface -InterfaceAlias $Interface -Dhcp Enabled -ErrorAction SilentlyContinue
|
||
Set-DnsClientServerAddress -InterfaceAlias $Interface -ResetServerAddresses -ErrorAction SilentlyContinue
|
||
Write-Ok "已切回 DHCP(稍等几秒应拿到地址)"
|
||
} else {
|
||
Write-Step "恢复静态地址 $Ip/$PrefixLength (网卡 $Interface)"
|
||
Set-NetIPInterface -InterfaceAlias $Interface -Dhcp Disabled -ErrorAction SilentlyContinue
|
||
|
||
# ⚠️ 顺序不可颠倒:先加新地址(过渡期链路不断),再清旧地址/残留路由,最后补路由。
|
||
# 颠倒会触发 Windows 的「实例已存在」→ 地址加不上 → 再次失联(真机事故根因)。
|
||
New-NetIPAddress -InterfaceAlias $Interface -IPAddress $Ip -PrefixLength $PrefixLength -ErrorAction SilentlyContinue | Out-Null
|
||
|
||
Get-NetRoute -InterfaceAlias $Interface -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue |
|
||
Remove-NetRoute -Confirm:$false -ErrorAction SilentlyContinue
|
||
|
||
Get-NetIPAddress -InterfaceAlias $Interface -AddressFamily IPv4 -ErrorAction SilentlyContinue |
|
||
Where-Object { $_.IPAddress -ne $Ip -and $_.PrefixOrigin -ne 'WellKnown' } |
|
||
ForEach-Object { Remove-NetIPAddress -IPAddress $_.IPAddress -InterfaceAlias $Interface -Confirm:$false -ErrorAction SilentlyContinue }
|
||
|
||
if ($Gateway) {
|
||
New-NetRoute -InterfaceAlias $Interface -DestinationPrefix '0.0.0.0/0' -NextHop $Gateway -ErrorAction SilentlyContinue | Out-Null
|
||
}
|
||
if ($Dns -and $Dns.Count -gt 0) {
|
||
Set-DnsClientServerAddress -InterfaceAlias $Interface -ServerAddresses $Dns -ErrorAction SilentlyContinue
|
||
}
|
||
}
|
||
|
||
Start-Sleep -Seconds 3
|
||
|
||
# ---------- 4. 自检 ----------
|
||
Write-Step "自检:"
|
||
$now = @(Get-NetIPAddress -InterfaceAlias $Interface -AddressFamily IPv4 -ErrorAction SilentlyContinue |
|
||
Where-Object { $_.PrefixOrigin -ne 'WellKnown' } | ForEach-Object { "$($_.IPAddress)/$($_.PrefixLength)" })
|
||
$nowGw = @(Get-NetRoute -InterfaceAlias $Interface -AddressFamily IPv4 -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue |
|
||
ForEach-Object { $_.NextHop })
|
||
|
||
if ($now.Count -gt 0) {
|
||
Write-Ok ("地址: " + ($now -join ', '))
|
||
if ($Ip -and $nowGw.Count -eq 0) {
|
||
Write-Warn2 "没有默认路由 —— 只能同网段互访;请补 -Gateway"
|
||
} elseif ($nowGw.Count -gt 0) {
|
||
Write-Ok ("默认路由: " + ($nowGw -join ', '))
|
||
}
|
||
Write-Host ""
|
||
Write-Ok "网络已恢复。可在 Mac 上执行: nodeagent connect $($now[0].Split('/')[0]) --port 8765 --key <PSK> --insecure"
|
||
} else {
|
||
Write-Err2 "仍然没有有效地址 —— 请检查网线/交换机,或尝试: rescue-network.ps1 -Interface '$Interface'(不带 -Ip,切回 DHCP)"
|
||
exit 1
|
||
}
|