Files
nodeagent/scripts/control.ps1
T
jkooe cedc46d090 feat(v20): 版本纪律 + 发版自动化(对齐「做好版本功能 + GitHub 同步资产」)
## 一、版本功能(防新旧混用)
1. **单一版本源 + 构建注入**:根 package.json 的 version、git sha、构建时间经 esbuild
   `--define` 注入 agent;`system.info.build` 新增 `version/commit/built_at`(原有 `hash` 指纹不变)。
   开发态用 typeof 探测(未注入也不崩)。
2. **握手携带版本**:`auth_ok` 新增可选字段 `agent_version` / `build` / `protocol`(只增不减 → 旧控制端忽略即可)。
3. **能力级兼容比对**:新增纯函数 `capabilityDiff(local, remote)` ——
   - `missingOnRemote`:本地有远端没有 → **远端较旧**,提示更新被控端
   - `unknownLocally`:远端有本地没有 → **本控制端较旧**(不影响既有功能)
   控制端不比版本号而比**能力集合**:版本号可能相同而内容不同,能力集合不会骗人。
4. **CLI 体检**:`nodeagent info` / `connect` 直接显示被控端版本与能力对齐情况,
   把"调用时才炸"变成一句人话提示。

## 二、发版自动化(GitHub 资产同步)
`pnpm release <x.y.z|patch|minor|major>`:前置检查(干净/在 main/与远端同步)→ 改 7 个 package.json
→ 构建+打包 → 生成资产 → 提交打标签推送 → `gh release create` 上传。
- 资产:`agent.mjs`(**自更新载荷**)、`nodeagent-win-x64.zip`(人工安装包)、
  `latest.json`(version/commit/能力数 + 两个资产的 url 与 sha256)、`SHA256SUMS`
- `--dry-run` 无副作用演练;`--no-publish` 只到本地标签
- 能力数从 dist 动态取(避免写死漂移);刻意排除 `.github/`(缺 workflow 权限推不了)

## 三、兼容性契约文档
新增 `docs/VERSIONING.md`:semver 各位的含义、**只增不改**六条硬规则、
升级顺序建议(**先被控端**:新被控端向后兼容旧 CLI)、两条更新路径、发版流程、排查手册。

## 验证
- 单测 147 → **149**(新增 capabilityDiff 用例:远端过旧 / 本机过旧 / 完全一致 / 真实清单自比无差异)
- e2e 28 全绿;`node scripts/release.mjs 1.0.0 --dry-run` 演练通过且工作区无副作用
2026-10-04 23:54:51 +08:00

238 lines
10 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<#
.SYNOPSIS
nodeagent - control operations for an installed Windows agent.
.DESCRIPTION
Backs control.cmd. Everything Chinese-facing lives here (rather than in
the .cmd) because CMD's code page cannot round-trip UTF-8, while this
file is saved as UTF-8 **with BOM** so Windows PowerShell 5.1 reads it
correctly. PowerShell 5.1 parses BOM-less .ps1 as ANSI/GBK and the
Chinese comments then break string boundaries -> bogus syntax errors.
.PARAMETER Action
status | start | stop | restart | logs | uninstall | uninstall-purge
.EXAMPLE
.\control.ps1 -Action status
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('status', 'start', 'stop', 'restart', 'logs', 'uninstall', 'uninstall-purge')]
[string]$Action,
[int]$Port = 8765
)
$ErrorActionPreference = "Stop"
function Write-Step($m) { Write-Host "[*] $m" -ForegroundColor Cyan }
function Write-Ok($m) { Write-Host "[+] $m" -ForegroundColor Green }
function Write-Warn($m) { Write-Host "[!] $m" -ForegroundColor Yellow }
function Write-Err($m) { Write-Host "[x] $m" -ForegroundColor Red }
$taskName = "nodeagent"
# 数据目录:必须与 agent 侧一致 —— apps/agent/src/config.ts 的 agentDir() 是
# process.env.NODEAGENT_HOME ?? join(homedir(), '.nodeagent')
# 计划任务里若设了 NODEAGENT_HOME,只查 %USERPROFILE%\.nodeagent 会读错配置。
$cfgDir = if ($env:NODEAGENT_HOME) { $env:NODEAGENT_HOME } else { Join-Path $env:USERPROFILE ".nodeagent" }
$cfgPath = Join-Path $cfgDir "agent.json"
$auditLog = Join-Path $cfgDir "audit.log"
function Test-Admin {
([Security.Principal.WindowsPrincipal] `
[Security.Principal.WindowsIdentity]::GetCurrent()
).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Get-TaskState {
try { (Get-ScheduledTask -TaskName $taskName -ErrorAction Stop).State.ToString() }
catch { "NotInstalled" }
}
# ── status ────────────────────────────────────────────────────────────────────
function Show-Status {
Write-Host ""
Write-Host " nodeagent 状态" -ForegroundColor White
Write-Host " --------------" -ForegroundColor DarkGray
# 计划任务
$state = Get-TaskState
if ($state -eq "NotInstalled") {
Write-Warn "计划任务 '$taskName' 未安装(请先双击 install.cmd)"
} else {
$color = if ($state -eq "Running") { "Green" } else { "Yellow" }
Write-Host " 计划任务 : " -NoNewline; Write-Host $state -ForegroundColor $color
$info = Get-ScheduledTaskInfo -TaskName $taskName -ErrorAction SilentlyContinue
if ($info) {
Write-Host " 上次运行 : $($info.LastRunTime) 结果码 $($info.LastTaskResult)"
}
}
# 进程
$procs = @(Get-Process node -ErrorAction SilentlyContinue)
if ($procs.Count -gt 0) {
Write-Host " 进程 : " -NoNewline
Write-Host "$($procs.Count) 个 node 进程 (PID $($procs.Id -join ', '))" -ForegroundColor Green
} else {
Write-Host " 进程 : " -NoNewline; Write-Host "无" -ForegroundColor DarkGray
}
# 端口
$listen = @(Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue)
if ($listen.Count -gt 0) {
Write-Host " 端口 : " -NoNewline
Write-Host "$Port 正在监听 (PID $($listen[0].OwningProcess))" -ForegroundColor Green
} else {
Write-Host " 端口 : " -NoNewline
Write-Host "$Port 未监听" -ForegroundColor Yellow
}
# 配置
if (Test-Path $cfgPath) {
try {
$cfg = Get-Content $cfgPath -Raw | ConvertFrom-Json
Write-Host " 节点 ID : $($cfg.node_id)"
Write-Host " 鉴权模式 : $($cfg.auth_mode)"
$inp = if ($cfg.allow_input) { "已启用" } else { "已禁用" }
Write-Host " 输入控制 : $inp"
$scheme = if ($cfg.tls) { "wss" } else { "ws" }
Write-Host " 协议 : $scheme://0.0.0.0:$Port"
} catch {
Write-Warn "配置文件解析失败: $cfgPath"
}
} else {
Write-Warn "未找到配置文件: $cfgPath"
}
# 本机 IP
$ips = @()
try {
$ips = @(Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Where-Object { $_.IPAddress -notlike '127.*' -and $_.PrefixOrigin -ne 'WellKnown' } |
Select-Object -ExpandProperty IPAddress)
} catch { }
Write-Host " 本机 IP : $($ips -join ', ')"
Write-Host ""
if ($state -eq "NotInstalled") {
Write-Warn "尚未安装 —— 请双击 install.cmd 一键部署"
} elseif ($listen.Count -eq 0) {
Write-Warn "端口未监听 —— 试试菜单里的「重启」,或双击 install.cmd 重新部署"
}
}
# ── start / stop / restart ────────────────────────────────────────────────────
function Start-Agent {
if ((Get-TaskState) -eq "NotInstalled") {
Write-Err "计划任务未安装,请先双击 install.cmd"
return
}
Write-Step "启动计划任务..."
Start-ScheduledTask -TaskName $taskName
# 轮询端口真正起来(最多 15s),而不是盲等
for ($i = 0; $i -lt 15; $i++) {
Start-Sleep -Seconds 1
if (Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue) {
Write-Ok "agent 已启动并在 $Port 监听"
return
}
}
Write-Warn "任务已触发,但 $Port 仍未监听"
$info = Get-ScheduledTaskInfo -TaskName $taskName -ErrorAction SilentlyContinue
if ($info) { Write-Warn "上次运行结果码: $($info.LastTaskResult)(0 = 成功,267009 = 任务已在运行)" }
}
function Stop-Agent {
if ((Get-TaskState) -eq "NotInstalled") {
Write-Err "计划任务未安装"
return
}
Write-Step "停止计划任务..."
Stop-ScheduledTask -TaskName $taskName
Start-Sleep -Seconds 1
# 兜底:计划任务偶尔留孤儿子进程
$left = @(Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue)
if ($left.Count -gt 0) {
Write-Warn "端口 $Port 仍被占用 (PID $($left[0].OwningProcess)),可能有残留进程"
Write-Warn "可手动结束: Stop-Process -Id $($left[0].OwningProcess) -Force"
} else {
Write-Ok "agent 已停止"
}
}
# ── logs ──────────────────────────────────────────────────────────────────────
function Show-Logs {
if (-not (Test-Path $auditLog)) {
Write-Warn "未找到审计日志: $auditLog"
Write-Warn "(审计日志需在 agent 配置中开启 audit.enabled)"
return
}
Write-Step "审计日志末尾 40 条: $auditLog"
Write-Host ""
# 只取尾部若干行,不把整个日志(默认上限 10MB x 5 份)读进内存。
# -Encoding UTF8 在 5.1 下能正确识别 UTF-8;解析前仍剥一次 BOM。
$lines = @(Get-Content -LiteralPath $auditLog -Tail 40 -Encoding UTF8 -ErrorAction SilentlyContinue |
Where-Object { $_.Trim() })
if ($lines.Count -eq 0) { Write-Warn "日志为空"; return }
foreach ($line in $lines) {
if ($line[0] -eq [char]0xFEFF) { $line = $line.Substring(1) }
try {
$e = $line | ConvertFrom-Json
# ts 是 Unix 毫秒(见 apps/agent/src/audit.ts 的 AuditEntry)
$ts = "?"
if ($e.ts) {
try { $ts = ([DateTimeOffset]::FromUnixTimeMilliseconds([int64]$e.ts)).LocalDateTime.ToString("MM-dd HH:mm:ss") }
catch { $ts = "$($e.ts)" }
}
$ty = if ($e.type) { $e.type } else { "?" }
$ci = if ($e.client_id) { $e.client_id } else { "-" }
$extra = ""
if ($e.capability) { $extra += " $($e.capability)" }
if ($e.status) { $extra += " [$($e.status)]" }
if ($e.duration_ms) { $extra += " $($e.duration_ms)ms" }
if ($e.error) { $extra += " ERROR=$($e.error)" }
if ($e.reason) { $extra += " reason=$($e.reason)" }
Write-Host (" {0,-15} {1,-20} {2,-14}{3}" -f $ts, $ty, $ci, $extra) -ForegroundColor DarkGray
} catch {
Write-Host " $line" -ForegroundColor DarkGray
}
}
Write-Host ""
}
# ── uninstall ─────────────────────────────────────────────────────────────────
function Uninstall-Agent([bool]$Purge) {
if (-not (Test-Admin)) {
Write-Err "卸载需要管理员权限,请右键 control.cmd → 以管理员身份运行"
return
}
Write-Step "停止并注销计划任务..."
Stop-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
Write-Step "移除防火墙规则..."
Get-NetFirewallRule -DisplayName "nodeagent (TCP $Port)" -ErrorAction SilentlyContinue |
Remove-NetFirewallRule -ErrorAction SilentlyContinue
if ($Purge) {
Write-Step "清除配置目录..."
Remove-Item -Recurse -Force $cfgDir -ErrorAction SilentlyContinue
Write-Ok "已彻底卸载(含配置与审计日志)"
} else {
Write-Ok "已卸载(配置保留在 $cfgDir)"
}
}
# ── dispatch ──────────────────────────────────────────────────────────────────
switch ($Action) {
'status' { Show-Status }
'start' { Start-Agent }
'stop' { Stop-Agent }
'restart' { Stop-Agent; Start-Sleep -Seconds 1; Start-Agent }
'logs' { Show-Logs }
'uninstall' { Uninstall-Agent -Purge $false }
'uninstall-purge' { Uninstall-Agent -Purge $true }
}