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` 演练通过且工作区无副作用
This commit is contained in:
1 parent
aa72241080
commit
cedc46d090
20 files changed
+1262
-17
No files matched your search
@@ -0,0 +1,83 @@
|
||||
@echo off
|
||||
REM ============================================================================
|
||||
REM nodeagent - control menu (Windows)
|
||||
REM
|
||||
REM Double-click this file to manage an already-installed agent:
|
||||
REM 1) status - task / process / port / config at a glance
|
||||
REM 2) start - start the scheduled task
|
||||
REM 3) stop - stop the scheduled task
|
||||
REM 4) restart - stop then start
|
||||
REM 5) logs - tail the agent's audit log
|
||||
REM 6) uninstall - remove task + firewall rule (7) purge config and exit
|
||||
REM
|
||||
REM Usage: control.cmd [port]
|
||||
REM port defaults to 8765; pass it only if the agent uses another.
|
||||
REM
|
||||
REM Pure ASCII on purpose - see the note in install.cmd. The heavy lifting
|
||||
REM (and all Chinese output) lives in control.ps1, which carries a UTF-8 BOM
|
||||
REM so Windows PowerShell 5.1 parses it correctly.
|
||||
REM ============================================================================
|
||||
setlocal EnableExtensions
|
||||
|
||||
set "HERE=%~dp0"
|
||||
set "HELPER=%HERE%control.ps1"
|
||||
set "PORT=%~1"
|
||||
if not defined PORT set "PORT=8765"
|
||||
|
||||
if not exist "%HELPER%" (
|
||||
echo [x] control.ps1 not found next to this file.
|
||||
echo Folder: "%HERE%"
|
||||
if not defined CI pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:MENU
|
||||
cls
|
||||
echo ==========================================================
|
||||
echo nodeagent control console
|
||||
echo ==========================================================
|
||||
echo 1) Status
|
||||
echo 2) Start agent
|
||||
echo 3) Stop agent
|
||||
echo 4) Restart agent
|
||||
echo 5) Show recent audit log
|
||||
echo 6) Uninstall ^(keep config^)
|
||||
echo 7) Uninstall and purge config
|
||||
echo Q) Quit
|
||||
echo ==========================================================
|
||||
echo.
|
||||
|
||||
set "CHOICE="
|
||||
REM <nul makes `set /p` fail immediately -> CHOICE stays empty. Combined with
|
||||
REM CI (see below) that exits instead of looping forever on piped/empty stdin.
|
||||
set /p "CHOICE=Select: "
|
||||
|
||||
if not defined CHOICE (
|
||||
if defined CI goto :QUIT
|
||||
echo.
|
||||
echo (no input - exiting)
|
||||
goto :QUIT
|
||||
)
|
||||
|
||||
if /i "%CHOICE%"=="Q" goto :QUIT
|
||||
if "%CHOICE%"=="1" call :RUN status
|
||||
if "%CHOICE%"=="2" call :RUN start
|
||||
if "%CHOICE%"=="3" call :RUN stop
|
||||
if "%CHOICE%"=="4" call :RUN restart
|
||||
if "%CHOICE%"=="5" call :RUN logs
|
||||
if "%CHOICE%"=="6" call :RUN uninstall
|
||||
if "%CHOICE%"=="7" call :RUN uninstall-purge
|
||||
goto :MENU
|
||||
|
||||
:QUIT
|
||||
endlocal
|
||||
exit /b 0
|
||||
|
||||
:RUN
|
||||
REM %1 = subcommand forwarded to control.ps1
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%HELPER%" -Action %1 -Port %PORT%
|
||||
echo.
|
||||
if not defined CI pause
|
||||
goto :MENU
|
||||
REM unreachable; keeps `call :RUN` from falling through if the goto is removed
|
||||
exit /b 0
|
||||
@@ -0,0 +1,237 @@
|
||||
<#
|
||||
.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 }
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
@echo off
|
||||
REM ============================================================================
|
||||
REM nodeagent - one-click installer launcher (Windows)
|
||||
REM
|
||||
REM Double-click this file. It will:
|
||||
REM 1) self-elevate to Administrator (UAC prompt) if not already elevated
|
||||
REM 2) make sure a pre-shared key exists in PSK.txt (generate one if absent)
|
||||
REM 3) run install.ps1, which registers the scheduled task, opens the
|
||||
REM firewall and starts the agent
|
||||
REM
|
||||
REM Usage: install.cmd [port]
|
||||
REM port defaults to 8765. Pass a port only when the default is taken.
|
||||
REM
|
||||
REM IMPORTANT - why this file is pure ASCII:
|
||||
REM The CMD.exe code page (936/GBK on Chinese Windows) can NOT round-trip
|
||||
REM UTF-8 Chinese reliably. Any non-ASCII byte here risks mojibake or a
|
||||
REM broken command. All Chinese-facing text is delegated to install.ps1,
|
||||
REM which is saved as UTF-8 **with BOM** so Windows PowerShell 5.1 reads
|
||||
REM it correctly. Keep this launcher ASCII-only.
|
||||
REM
|
||||
REM NOTE - install.ps1 MUST keep its UTF-8 BOM. PowerShell 5.1 parses
|
||||
REM BOM-less .ps1 as ANSI/GBK, and the Chinese comments then break the
|
||||
REM string boundaries -> a cascade of bogus syntax errors.
|
||||
REM ============================================================================
|
||||
setlocal EnableExtensions
|
||||
|
||||
set "HERE=%~dp0"
|
||||
set "PS1=%HERE%install.ps1"
|
||||
set "PSKFILE=%HERE%PSK.txt"
|
||||
set "PORT=%~1"
|
||||
if not defined PORT set "PORT=8765"
|
||||
|
||||
if not exist "%PS1%" (
|
||||
echo [x] install.ps1 not found next to this file.
|
||||
echo Folder: "%HERE%"
|
||||
echo Keep install.ps1, node.exe, agent.mjs and this launcher together.
|
||||
if not defined CI pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM --- 1) elevate to Administrator -------------------------------------------
|
||||
REM fltmc is used instead of `net session` because the latter depends on the
|
||||
REM Server service, which is disabled on some builds - that would make an
|
||||
REM already-elevated shell look unelevated and trigger a pointless UAC loop.
|
||||
fltmc >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [*] Requesting Administrator rights...
|
||||
echo A new window will open. Continue there; this one closes by itself.
|
||||
REM Start-Process re-launches this .cmd elevated. %PORT% (already resolved to
|
||||
REM its default) is forwarded so "install.cmd 18770" survives the elevation.
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
|
||||
"Start-Process -FilePath '%~f0' -ArgumentList '%PORT%' -Verb RunAs" >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [x] Elevation was declined - cannot register a scheduled task.
|
||||
if not defined CI pause
|
||||
exit /b 1
|
||||
)
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
REM --- 2) make sure the pre-shared key exists ---------------------------------
|
||||
set "PSK="
|
||||
if exist "%PSKFILE%" (
|
||||
REM Read + Trim in one shot: a stray CR from CRLF or an editor-added trailing
|
||||
REM space would otherwise end up inside -Key and break authentication.
|
||||
for /f "delims=" %%K in ('powershell -NoProfile -ExecutionPolicy Bypass -Command ^
|
||||
"if (Test-Path -LiteralPath '%PSKFILE%') { (Get-Content -LiteralPath '%PSKFILE%' -Raw).Trim() }"') do set "PSK=%%K"
|
||||
)
|
||||
|
||||
if not defined PSK (
|
||||
echo [*] No usable PSK.txt found - generating a new pre-shared key...
|
||||
REM 32 random bytes via .NET, hex-encoded to 64 chars.
|
||||
for /f "delims=" %%K in ('powershell -NoProfile -ExecutionPolicy Bypass -Command ^
|
||||
"$b=New-Object byte[] 32; [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b); ($b ^| ForEach-Object { $_.ToString('x2') }) -join ''"') do set "PSK=%%K"
|
||||
|
||||
if not defined PSK (
|
||||
echo [x] Failed to generate a key. Aborting.
|
||||
if not defined CI pause
|
||||
exit /b 1
|
||||
)
|
||||
REM Plain hex, no BOM: the file is read with `set /p` / Get-Content, and a
|
||||
REM leading U+FEFF would corrupt the first byte of the key.
|
||||
>"%PSKFILE%" echo %PSK%
|
||||
echo [+] New key written to "%PSKFILE%"
|
||||
) else (
|
||||
echo [+] Using key from PSK.txt
|
||||
)
|
||||
|
||||
REM --- 3) install -------------------------------------------------------------
|
||||
echo.
|
||||
echo [*] Installing nodeagent ^(node: %COMPUTERNAME%, port %PORT%, input control ON^)...
|
||||
echo.
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%PS1%" -NodeId "%COMPUTERNAME%" -Key "%PSK%" -AllowInput -Port %PORT%
|
||||
set "RC=%ERRORLEVEL%"
|
||||
|
||||
echo.
|
||||
if "%RC%"=="0" (
|
||||
echo [+] Done. The agent now starts automatically at every logon.
|
||||
echo To manage it later, double-click control.cmd in this folder.
|
||||
) else (
|
||||
echo [x] install.ps1 exited with code %RC%.
|
||||
echo If the error mentions a missing file, re-extract the whole package.
|
||||
echo If Windows Firewall or a security suite blocked something, allow it and retry.
|
||||
)
|
||||
echo.
|
||||
REM Keep the window open so the user can read the output. CI sets CI=true,
|
||||
REM where `pause` would hang forever waiting for a keypress.
|
||||
if not defined CI pause
|
||||
exit /b %RC%
|
||||
+11
-1
@@ -2,6 +2,12 @@
|
||||
.SYNOPSIS
|
||||
nodeagent - Windows agent installer.
|
||||
|
||||
⚠️ 本文件必须以 **UTF-8 BOM** 保存(文件头那个看不见的 U+FEFF 就是 BOM)。
|
||||
Windows PowerShell 5.1 读**无 BOM** 的 .ps1 会按系统 ANSI(中文 Windows = GBK)
|
||||
解析,本文件里的中文注释会解成乱码并被误认为引号 → 整份脚本 ParseError
|
||||
(报错位置彼此无关,真正的病灶在靠前处)。真机踩过:2026-10-04。
|
||||
另:第 4 步写 agent.json 时反而必须**无 BOM**(BOM 对 .ps1 必需、对 JSON 有害)。
|
||||
|
||||
.DESCRIPTION
|
||||
Registers the nodeagent agent as a scheduled task (auto-start on logon),
|
||||
opens the inbound firewall port, and prints connection info for the Mac side.
|
||||
@@ -97,7 +103,11 @@ Write-Ok "Node.js v$ver"
|
||||
Write-Ok "Agent entry: $agentJs"
|
||||
|
||||
# 4. Config + pre-shared key
|
||||
$cfgDir = Join-Path $env:USERPROFILE ".nodeagent"
|
||||
# 数据目录必须与 agent 侧一致 —— apps/agent/src/config.ts 的 agentDir() 是
|
||||
# process.env.NODEAGENT_HOME ?? join(homedir(), '.nodeagent')
|
||||
# 若计划任务环境里设了 NODEAGENT_HOME 而这里写死 USERPROFILE\.nodeagent,
|
||||
# 就会「配置写A、读取B」,表现为装完连不上(E_AUTH_FAILED)。
|
||||
$cfgDir = if ($env:NODEAGENT_HOME) { $env:NODEAGENT_HOME } else { Join-Path $env:USERPROFILE ".nodeagent" }
|
||||
$cfgPath = Join-Path $cfgDir "agent.json"
|
||||
New-Item -ItemType Directory -Force -Path $cfgDir | Out-Null
|
||||
|
||||
|
||||
+96
-5
@@ -7,12 +7,20 @@
|
||||
* 产物: <out>/nodeagent-win-x64.zip
|
||||
* ├── node.exe Node.js 运行时(免装 Node.js)
|
||||
* ├── agent.mjs esbuild 打包的被控端(含全部依赖)
|
||||
* ├── install.ps1 一键安装
|
||||
* └── uninstall.ps1 一键卸载
|
||||
* ├── install.cmd 一键安装(双击即用,自提权 + 自动生成 PSK)
|
||||
* ├── control.cmd 控制台菜单(启动/停止/状态/日志/卸载)
|
||||
* ├── install.ps1 底层安装脚本(由 install.cmd 调用)
|
||||
* ├── control.ps1 底层控制脚本(由 control.cmd 调用)
|
||||
* ├── uninstall.ps1 底层卸载脚本
|
||||
* └── PSK.txt 预共享密钥(首次运行时由 install.cmd 生成)
|
||||
*/
|
||||
import { execFileSync, execSync } from 'node:child_process';
|
||||
import { copyFileSync, createWriteStream, existsSync, mkdirSync, rmSync } from 'node:fs';
|
||||
import {
|
||||
copyFileSync, createWriteStream, existsSync, mkdirSync,
|
||||
readFileSync, rmSync, writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { get } from 'node:https';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
@@ -62,6 +70,18 @@ async function main() {
|
||||
execSync('pnpm -r build', { cwd: root, stdio: 'inherit' });
|
||||
|
||||
log('\n=== ② esbuild 打包被控端 ===');
|
||||
// 单一版本源:根 package.json 的 version + git sha + 构建时间,
|
||||
// 经 esbuild --define 注入,避免「版本号写在多处必然不同步」。
|
||||
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
||||
const commit = (() => {
|
||||
try {
|
||||
return execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: root }).toString().trim();
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
})();
|
||||
const builtAt = new Date().toISOString();
|
||||
log(` 版本 ${pkg.version} @ ${commit}(${builtAt})`);
|
||||
mkdirSync(OUT_DIR, { recursive: true });
|
||||
const bundleOut = join(OUT_DIR, 'agent.mjs');
|
||||
const esbuild = join(root, 'node_modules', '.bin', 'esbuild');
|
||||
@@ -75,6 +95,9 @@ async function main() {
|
||||
'--target=node22',
|
||||
'--format=esm',
|
||||
'--banner:js=import { createRequire } from "node:module"; const require = createRequire(import.meta.url);',
|
||||
`--define:__AGENT_VERSION__='"${pkg.version}"'`,
|
||||
`--define:__BUILD_COMMIT__='"${commit}"'`,
|
||||
`--define:__BUILD_TIME__='"${builtAt}"'`,
|
||||
`--outfile=${bundleOut}`,
|
||||
],
|
||||
{ stdio: 'inherit' },
|
||||
@@ -94,9 +117,76 @@ async function main() {
|
||||
execSync(`unzip -o -j "${tmpZip}" "*/node.exe" -d "${PKG_DIR}"`, { stdio: 'inherit' });
|
||||
copyFileSync(bundleOut, join(PKG_DIR, 'agent.mjs'));
|
||||
copyFileSync(join(root, 'scripts', 'install.ps1'), join(PKG_DIR, 'install.ps1'));
|
||||
copyFileSync(join(root, 'scripts', 'control.ps1'), join(PKG_DIR, 'control.ps1'));
|
||||
copyFileSync(join(root, 'scripts', 'uninstall.ps1'), join(PKG_DIR, 'uninstall.ps1'));
|
||||
|
||||
// 一键入口:.cmd 必须**纯 ASCII**。CMD 的代码页(中文 Windows = 936/GBK)
|
||||
// 无法可靠往返 UTF-8,中文提示一律交给带 BOM 的 .ps1 去输出。
|
||||
for (const cmd of ['install.cmd', 'control.cmd']) {
|
||||
copyFileSync(join(root, 'scripts', cmd), join(PKG_DIR, cmd));
|
||||
}
|
||||
|
||||
// 防呆 A:产物里的 .ps1 必须带 UTF-8 BOM,否则被控端的 **Windows PowerShell 5.1**
|
||||
// 会按 ANSI/GBK 解析无 BOM 脚本,中文注释破坏字符串边界 → 整份脚本 ParseError。
|
||||
// 真机踩过(2026-10-04):CI 全绿(冒烟用 pwsh/PS7),5.1 真机直接报
|
||||
// 「","后面缺少表达式」。此处宁可在打包阶段失败,也不要把坏包发出去。
|
||||
for (const ps1 of ['install.ps1', 'control.ps1', 'uninstall.ps1']) {
|
||||
const p = join(PKG_DIR, ps1);
|
||||
const head = readFileSync(p).subarray(0, 3);
|
||||
if (!(head[0] === 0xef && head[1] === 0xbb && head[2] === 0xbf)) {
|
||||
fail(`${ps1} 缺少 UTF-8 BOM —— Windows PowerShell 5.1 会解析失败。` +
|
||||
`请先给 scripts/${ps1} 加上 BOM 再打包。`);
|
||||
}
|
||||
}
|
||||
log(' .ps1 BOM 检查通过(兼容 Windows PowerShell 5.1)');
|
||||
|
||||
// 防呆 B:.cmd 里出现非 ASCII 字节 = 中文乱码/命令截断的隐患。
|
||||
for (const cmd of ['install.cmd', 'control.cmd']) {
|
||||
const buf = readFileSync(join(PKG_DIR, cmd));
|
||||
const bad = [...buf].findIndex((b) => b > 0x7f);
|
||||
if (bad >= 0) {
|
||||
fail(`${cmd} 含非 ASCII 字节(偏移 ${bad})—— CMD 代码页无法可靠处理,` +
|
||||
`请把该文件改为纯 ASCII,中文提示交给 .ps1。`);
|
||||
}
|
||||
}
|
||||
log(' .cmd 纯 ASCII 检查通过(CMD 代码页安全)');
|
||||
|
||||
// PSK.txt:预置一把随机密钥,让 install.cmd 真正「零输入」。
|
||||
// 用 --psk 传入指定密钥;不传则随机生成 32 字节 hex。
|
||||
// 文件是纯 hex 文本,无需 BOM(install.cmd 用 `set /p` 读)。
|
||||
const pskArg = getArg('--psk', '');
|
||||
const psk = pskArg || randomBytes(32).toString('hex');
|
||||
if (!/^[0-9a-fA-F]{32,128}$/.test(psk)) {
|
||||
fail(`--psk 格式非法(需 32-128 位十六进制): ${psk}`);
|
||||
}
|
||||
writeFileSync(join(PKG_DIR, 'PSK.txt'), `${psk}\n`, 'utf8');
|
||||
log(` PSK.txt 已生成(${psk.length} 位${pskArg ? ',来自 --psk' : ',随机'})`);
|
||||
|
||||
// README:给第一次上手的人看一眼该双击哪个文件
|
||||
writeFileSync(join(PKG_DIR, 'README.txt'), [
|
||||
'nodeagent - Windows 被控端',
|
||||
'==========================',
|
||||
'',
|
||||
'【怎么装】双击 install.cmd',
|
||||
' 1) 会弹一次「用户账户控制」→ 点「是」(必须管理员,装计划任务要用)',
|
||||
' 2) 等它跑完,最后会打印连接信息',
|
||||
' 3) 把那行 nodeagent connect ... 拿到 Mac 上执行',
|
||||
'',
|
||||
'【日常管理】双击 control.cmd',
|
||||
' 1 状态 / 2 启动 / 3 停止 / 4 重启 / 5 日志 / 6 卸载 / 7 彻底卸载',
|
||||
'',
|
||||
'【密钥】PSK.txt 里是预共享密钥(= Mac 端 --key 的值)',
|
||||
' 丢了可以删掉 PSK.txt 重装,会自动生成新的',
|
||||
'',
|
||||
'【注意】',
|
||||
' * 整个文件夹可以固定放在 D:\\nodeagent,但**装完不要移动或改名**',
|
||||
' * agent 每次开机登录后自动启动,无需再双击任何东西',
|
||||
' * 首次运行若被杀软拦截,把本文件夹加入信任区后重试',
|
||||
'',
|
||||
].join('\n'), 'utf8');
|
||||
|
||||
const files = execSync(`ls -la "${PKG_DIR}"`, { encoding: 'utf8' });
|
||||
log(files.split('\n').slice(0, 8).join('\n'));
|
||||
log(files.split('\n').slice(0, 12).join('\n'));
|
||||
|
||||
log('\n=== ⑤ 打 zip ===');
|
||||
const zipOut = join(OUT_DIR, 'nodeagent-win-x64.zip');
|
||||
@@ -105,7 +195,8 @@ async function main() {
|
||||
|
||||
const size = execSync(`du -sh "${zipOut}"`, { encoding: 'utf8' }).split('\t')[0];
|
||||
log(`\n✓ 完成: ${zipOut}(${size.trim()})`);
|
||||
log(' 分发方式:解压后右键以管理员运行 install.ps1 即可');
|
||||
log(' 分发方式:解压到固定目录 → 双击 install.cmd(自提权,零命令行知识)');
|
||||
log(' 日后管理:双击 control.cmd');
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 发版脚本:一条命令完成「改版本 → 构建 → 打包 → 生成校验和与清单 → 提交打标签 → 发布 GitHub Release」。
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/release.mjs 1.0.0 # 指定版本
|
||||
* node scripts/release.mjs patch|minor|major # 按当前版本自增
|
||||
* node scripts/release.mjs 1.0.0 --dry-run # 只演练:构建+生成资产,不改 git、不发布
|
||||
* node scripts/release.mjs 1.0.0 --no-publish # 提交打标签,但不发 GitHub Release
|
||||
*
|
||||
* 为什么要有它(对齐皇上的要求「每次更新,GitHub 上也要同步资产」):
|
||||
* 手工发版要记住「改几处 package.json、打哪个包、算哪些哈希、传什么资产」——
|
||||
* 必然漏项(尤其是给人用的 zip 与给自更新用的 agent.mjs 是**两个不同的东西**)。
|
||||
* 本脚本把两者都产出,并把 sha256 写进 latest.json,供被控端 `system.agent.update` 直接使用。
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const argv = process.argv.slice(2);
|
||||
const flags = new Set(argv.filter((a) => a.startsWith('--')));
|
||||
const arg = argv.find((a) => !a.startsWith('--'));
|
||||
|
||||
const dryRun = flags.has('--dry-run');
|
||||
const publish = !flags.has('--no-publish');
|
||||
|
||||
const sh = (cmd, args, opts = {}) =>
|
||||
execFileSync(cmd, args, { cwd: root, encoding: 'utf8', ...opts }).toString().trim();
|
||||
|
||||
function fail(msg) {
|
||||
console.error(`✗ ${msg}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function bump(cur, kind) {
|
||||
const [a, b, c] = cur.split('.').map((n) => Number.parseInt(n, 10));
|
||||
if ([a, b, c].some((n) => Number.isNaN(n))) fail(`现有版本号无法解析: ${cur}`);
|
||||
if (kind === 'patch') return `${a}.${b}.${c + 1}`;
|
||||
if (kind === 'minor') return `${a}.${b + 1}.0`;
|
||||
if (kind === 'major') return `${a + 1}.0.0`;
|
||||
return null;
|
||||
}
|
||||
|
||||
const pkgPath = join(root, 'package.json');
|
||||
const cur = JSON.parse(readFileSync(pkgPath, 'utf8')).version;
|
||||
const version = ['patch', 'minor', 'major'].includes(arg) ? bump(cur, arg) : (arg ?? '');
|
||||
if (!version) fail('用法: node scripts/release.mjs <1.2.3|patch|minor|major> [--dry-run] [--no-publish]');
|
||||
if (!/^\d+\.\d+\.\d+$/.test(version)) fail(`版本号格式应为 x.y.z,收到 ${version}`);
|
||||
|
||||
console.log(`=== nodeagent 发版 v${version}(当前 ${cur})${dryRun ? ' [dry-run]' : ''} ===`);
|
||||
|
||||
// ---------- ① 前置检查 ----------
|
||||
if (!dryRun) {
|
||||
// 刻意排除 .github/:其改动需要 workflow 权限才能推送,
|
||||
// 若把它算进「干净检查」会让发版永远卡住(见 docs/VERSIONING.md §6)
|
||||
const dirty = sh('git', ['status', '--porcelain', '--', '.', ':(exclude).github'])
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
if (dirty) fail(`工作区不干净(已忽略 .github/),请先提交:\n${dirty}`);
|
||||
const branch = sh('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
|
||||
if (branch !== 'main') fail(`当前分支是 ${branch},发版请在 main 上`);
|
||||
sh('git', ['fetch', 'origin', 'main']);
|
||||
if (sh('git', ['rev-parse', 'HEAD']) !== sh('git', ['rev-parse', 'origin/main'])) {
|
||||
fail('本地 main 与远端不一致,请先 pull/push(避免发版后才发现分叉)');
|
||||
}
|
||||
}
|
||||
|
||||
const remote = sh('git', ['remote', 'get-url', 'origin'])
|
||||
.replace(/^git@github\.com:/, 'https://github.com/')
|
||||
.replace(/\.git$/, '');
|
||||
const repoPath = remote.replace(/^https:\/\/github\.com\//, '');
|
||||
if (!/^[\w.-]+\/[\w.-]+$/.test(repoPath)) fail(`无法从 remote 推断 GitHub 仓库: ${remote}`);
|
||||
|
||||
// ---------- ② 改版本(单一版本源:根 + 各 workspace)----------
|
||||
const pkgFiles = ['package.json'];
|
||||
for (const f of sh('git', ['ls-files', '*package.json']).split('\n').filter(Boolean)) {
|
||||
if (f === 'package.json' || f.includes('node_modules')) continue;
|
||||
pkgFiles.push(f);
|
||||
}
|
||||
if (dryRun) {
|
||||
// dry-run 绝不改文件 —— 演练就该是无副作用的
|
||||
console.log(`(dry-run)将把 ${pkgFiles.length} 个 package.json 的 version 改为 ${version}`);
|
||||
console.log('(dry-run)将执行:构建 → pack:win → pack:mac → 生成资产 → 提交打标签 → gh release create');
|
||||
process.exit(0);
|
||||
}
|
||||
for (const f of pkgFiles) {
|
||||
const p = join(root, f);
|
||||
const j = JSON.parse(readFileSync(p, 'utf8'));
|
||||
if (!j.version) continue;
|
||||
j.version = version;
|
||||
writeFileSync(p, JSON.stringify(j, null, 2) + '\n');
|
||||
}
|
||||
console.log(`✓ 版本号已写入 ${pkgFiles.length} 个 package.json`);
|
||||
|
||||
// ---------- ③ 构建 + 打包 ----------
|
||||
console.log('=== 构建 ===');
|
||||
sh('pnpm', ['-r', 'build'], { stdio: 'inherit' });
|
||||
console.log('=== 打包 ===');
|
||||
sh('pnpm', ['pack:win'], { stdio: 'inherit' });
|
||||
try {
|
||||
sh('pnpm', ['pack:mac'], { stdio: 'inherit' });
|
||||
} catch {
|
||||
console.warn('(pack:mac 跳过:非 macOS 或未安装依赖)');
|
||||
}
|
||||
|
||||
const agentPath = join(root, 'release', 'nodeagent-win-x64', 'agent.mjs');
|
||||
const zipPath = join(root, 'release', 'nodeagent-win-x64.zip');
|
||||
if (!existsSync(agentPath)) fail(`找不到 agent 产物: ${agentPath}`);
|
||||
if (!existsSync(zipPath)) fail(`找不到 zip 产物: ${zipPath}`);
|
||||
|
||||
// ---------- ④ 生成资产(校验和 + latest.json)----------
|
||||
const sha256 = (p) => createHash('sha256').update(readFileSync(p)).digest('hex');
|
||||
const distDir = join(root, 'release', 'dist');
|
||||
mkdirSync(distDir, { recursive: true });
|
||||
|
||||
const assets = [
|
||||
{ name: 'agent.mjs', path: agentPath, purpose: '自更新载荷(被控端 system.agent.update 直接使用)' },
|
||||
{ name: 'nodeagent-win-x64.zip', path: zipPath, purpose: '人工安装包(自包含 node.exe,免装 Node.js)' },
|
||||
];
|
||||
for (const a of assets) {
|
||||
copyFileSync(a.path, join(distDir, a.name));
|
||||
a.bytes = readFileSync(a.path).length;
|
||||
a.sha256 = sha256(a.path);
|
||||
}
|
||||
|
||||
const commit = sh('git', ['rev-parse', '--short', 'HEAD']);
|
||||
const builtAt = new Date().toISOString();
|
||||
// 能力数从清单动态取,避免写死后漂移
|
||||
let capabilityCount = 0;
|
||||
try {
|
||||
const mod = await import(join(root, 'packages', 'protocol', 'dist', 'index.js'));
|
||||
capabilityCount = mod.CAPABILITY_MANIFEST?.length ?? 0;
|
||||
} catch {
|
||||
console.warn('(未能从 dist 读取能力数,latest.json 里记为 0)');
|
||||
}
|
||||
const base = `https://github.com/${repoPath}/releases/download/v${version}`;
|
||||
const latest = {
|
||||
version,
|
||||
commit,
|
||||
built_at: builtAt,
|
||||
protocol: '1.0',
|
||||
capabilities: capabilityCount,
|
||||
agent: {
|
||||
url: `${base}/agent.mjs`,
|
||||
sha256: assets[0].sha256,
|
||||
bytes: assets[0].bytes,
|
||||
note: '自更新用:nodeagent update --url <url> --sha256 <sha256>',
|
||||
},
|
||||
zip: { url: `${base}/nodeagent-win-x64.zip`, sha256: assets[1].sha256, bytes: assets[1].bytes },
|
||||
};
|
||||
writeFileSync(join(distDir, 'latest.json'), JSON.stringify(latest, null, 2) + '\n');
|
||||
|
||||
const sums = assets.map((a) => `${a.sha256} ${a.name}`).join('\n') + `\n`;
|
||||
writeFileSync(join(distDir, 'SHA256SUMS'), sums);
|
||||
|
||||
console.log('=== 资产 ===');
|
||||
for (const a of assets) {
|
||||
console.log(` ${a.name.padEnd(26)} ${(a.bytes / 1024 / 1024).toFixed(1)}MB sha256=${a.sha256.slice(0, 12)}…`);
|
||||
}
|
||||
console.log(` latest.json / SHA256SUMS`);
|
||||
|
||||
// ---------- ⑤ 提交 + 标签 + 推送 ----------
|
||||
sh('git', ['add', '-A', '--', '.', ':(exclude).github']);
|
||||
sh('git', ['commit', '-m', `chore(release): v${version}`]);
|
||||
sh('git', ['tag', '-a', `v${version}`, '-m', `nodeagent v${version}`]);
|
||||
sh('git', ['push', 'origin', 'main']);
|
||||
sh('git', ['push', 'origin', `v${version}`]);
|
||||
console.log(`✓ 已提交并推送标签 v${version}`);
|
||||
|
||||
// ---------- ⑥ 发布 GitHub Release ----------
|
||||
if (!publish) {
|
||||
console.log('(--no-publish:已跳过 GitHub Release)');
|
||||
process.exit(0);
|
||||
}
|
||||
const notes = join(distDir, 'RELEASE_NOTES.md');
|
||||
writeFileSync(
|
||||
notes,
|
||||
[
|
||||
`# nodeagent v${version}`,
|
||||
``,
|
||||
`- 构建: \`${commit}\` @ ${builtAt}`,
|
||||
`- 能力: ${latest.capabilities} 项`,
|
||||
``,
|
||||
`## 资产`,
|
||||
...assets.map((a) => `- \`${a.name}\` — ${a.purpose}`),
|
||||
`- \`latest.json\` — 版本清单(含 sha256),供被控端自更新校验`,
|
||||
`- \`SHA256SUMS\` — 校验和`,
|
||||
``,
|
||||
`## 更新被控端(拉取式,只需它能上网)`,
|
||||
'```bash',
|
||||
`nodeagent --node <设备名> update \\`,
|
||||
` --url ${latest.agent.url} \\`,
|
||||
` --sha256 ${latest.agent.sha256}`,
|
||||
'```',
|
||||
``,
|
||||
`## 人工安装(首次)`,
|
||||
`\`\`\`powershell`,
|
||||
`Expand-Archive nodeagent-win-x64.zip C:\\nodeagent -Force`,
|
||||
`# 先把 C:\\nodeagent 加入杀软信任区(主流杀软/杀软)`,
|
||||
`powershell -ExecutionPolicy Bypass -File C:\\nodeagent\\install.ps1 -NodeId win -AllowInput`,
|
||||
'```',
|
||||
].join('\n') + '\n',
|
||||
);
|
||||
|
||||
sh('gh', [
|
||||
'release',
|
||||
'create',
|
||||
`v${version}`,
|
||||
'--title',
|
||||
`nodeagent v${version}`,
|
||||
'--notes-file',
|
||||
notes,
|
||||
join(distDir, 'agent.mjs'),
|
||||
join(distDir, 'nodeagent-win-x64.zip'),
|
||||
join(distDir, 'latest.json'),
|
||||
join(distDir, 'SHA256SUMS'),
|
||||
]);
|
||||
console.log(`✓ GitHub Release v${version} 已发布`);
|
||||
console.log('');
|
||||
console.log('被控端更新命令:');
|
||||
console.log(` nodeagent --node <设备名> update --url ${latest.agent.url} --sha256 ${latest.agent.sha256}`);
|
||||
Reference in new issue
Block a user