feat(v3.0.5): winget 三层 fallback + 截图助手集成 + 启动器自检

4 组改动闭环用户报障:

[B1] Node.js + Git MSI/EXE 直链兜底 (修复: 只有 PS7 有 MSI 兜底, Node/Git
     winget 失败后无后路, 新机装 Bookworm 必挂). 新增统一分派:
     MsiUrl → msiexec /quiet /qn, ExeUrl → Inno Setup /VERYSILENT.

[B2] install.ps1 -StartOnly 依赖缺失改 GUI 弹窗 + auto-setup.ps1 Phase 1
     失败时清除桌面僵尸快捷方式 (启动Bookworm.lnk / Bookworm.lnk /
     更新Bookworm.lnk). 根治 'claude.exe not found' 闪退.

[F1] winget 检测醒目提示: 不可用时弹 Information 列出 Store / GitHub /
     系统要求 / 备用方案, 但不阻断安装流程.

[F2] Phase 1 手动安装清单重构: 每依赖列 ManualUrl + 步骤 + 重启 EXE 指引 +
     排查清单.

[F3+F4] 新增 Phase 5.5 截图粘贴助手部署:
     - profile sentinel 追加式注入 (# BW_CLIP_START/END, 重装精准替换)
     - 截图 Toast 关闭 + 原值备份到 HKCU:\Software\Bookworm\ToastBackup
       (卸载可还原, Q2=B 方案)

$BWVersion = 3.0.5, EXE 198144 → 209920 bytes (+11776).
向后兼容: 老用户已装依赖全跳过, profile 无 BW_CLIP 块首次追加.
This commit is contained in:
bookworm 2026-04-24 18:59:16 +08:00
parent 85f2bcd52f
commit 55ac953cd8
2 changed files with 271 additions and 24 deletions

View File

@ -20,7 +20,7 @@ param(
$ErrorActionPreference = "Stop"
# ─── 版本号 (每次更新递增, build.ps1 自动读取) ──────
$BWVersion = "3.0.4" # fix: Key 验证模型兜底 + 默认模型反推 + Win11 指纹 wmic→CIM 三级 fallback
$BWVersion = "3.0.5" # feat: winget 三层 fallback + 截图粘贴助手 + 启动器自检 + 僵尸快捷方式清理
# DryRun 模式日志标记
if ($DryRun) { $global:BWDryRun = $DryRun } else { $global:BWDryRun = $null }
@ -791,17 +791,48 @@ if (Is-DryRun "env") {
}
$deps = @(
# 核心依赖 (缺失则尝试自动安装)
@{ Name = "Node.js"; Cmd = "node"; WingetId = "OpenJS.NodeJS.LTS"; NpmPkg = $null; PipPkg = $null; Core = $true }
@{ Name = "Git"; Cmd = "git"; WingetId = "Git.Git"; NpmPkg = $null; PipPkg = $null; Core = $true }
@{ Name = "PowerShell 7"; Cmd = "pwsh"; WingetId = "Microsoft.PowerShell"; NpmPkg = $null; PipPkg = $null; Core = $true; MsiUrl = "https://github.com/PowerShell/PowerShell/releases/download/v7.4.6/PowerShell-7.4.6-win-x64.msi" }
@{ Name = "Claude Code"; Cmd = "claude"; WingetId = $null; NpmPkg = "@anthropic-ai/claude-code"; PipPkg = $null; Core = $true }
# 核心依赖 (三层 fallback: winget → MSI/EXE 直链 → 手动指引)
# v3.0.5: 所有核心依赖都补齐直链兜底,防 winget 不可用时 Phase 1 直接挂
@{ Name = "Node.js"; Cmd = "node"; WingetId = "OpenJS.NodeJS.LTS"; NpmPkg = $null; PipPkg = $null; Core = $true
MsiUrl = "https://nodejs.org/dist/v22.11.0/node-v22.11.0-x64.msi"
ManualUrl = "https://nodejs.org/zh-cn/download" }
@{ Name = "Git"; Cmd = "git"; WingetId = "Git.Git"; NpmPkg = $null; PipPkg = $null; Core = $true
# Git for Windows 是 Inno Setup (非 MSI), 用 /VERYSILENT /NORESTART 静默
ExeUrl = "https://github.com/git-for-windows/git/releases/download/v2.47.1.windows.2/Git-2.47.1.2-64-bit.exe"
ExeArgs = @("/VERYSILENT", "/NORESTART", "/NOCANCEL", "/SP-", "/SUPPRESSMSGBOXES")
ManualUrl = "https://git-scm.com/download/win" }
@{ Name = "PowerShell 7"; Cmd = "pwsh"; WingetId = "Microsoft.PowerShell"; NpmPkg = $null; PipPkg = $null; Core = $true
MsiUrl = "https://github.com/PowerShell/PowerShell/releases/download/v7.4.6/PowerShell-7.4.6-win-x64.msi"
ManualUrl = "https://github.com/PowerShell/PowerShell/releases/latest" }
@{ Name = "Claude Code"; Cmd = "claude"; WingetId = $null; NpmPkg = "@anthropic-ai/claude-code"; PipPkg = $null; Core = $true
ManualUrl = "https://docs.anthropic.com/en/docs/claude-code/overview" }
# Python 移到可选依赖 (不在此列表, 由 line 753 单独处理)
)
$hasWinget = Test-Cmd "winget"
$installed = @()
# v3.0.5: winget 不可用时醒目提示 (但不阻断, MSI/EXE 直链兜底可以继续)
if (-not $hasWinget) {
$wingetHint = @"
检测到 winget (Windows 包管理器) 未就绪
Bookworm 将改用 MSI/EXE 直链自动下载 Node.js Git无需 winget 也能完成安装请耐心等待
若想获取 winget推荐两条路径
1. 应用商店 搜索"应用安装程序" (App Installer)
2. GitHub https://aka.ms/getwinget
系统要求Windows 10 1809+ / Windows 11 任意版本
仍无法安装 winget?很可能是企业镜像锁定或 Win10 1803 及更早版本
无需纠结本安装器下一步的 MSI/EXE 直链兜底会自动处理
若直链也失败会弹窗给出手动安装清单
"@
Show-MsgBox $wingetHint "winget 未就绪 (不阻断)" "OK" "Information"
Log-Warn "winget 不可用, 将走 MSI/EXE 直链兜底"
}
foreach ($dep in $deps) {
if (Test-Cmd $dep.Cmd) {
$ver = try { & $dep.Cmd --version 2>$null | Select-Object -First 1 } catch { "installed" }
@ -823,30 +854,49 @@ foreach ($dep in $deps) {
Log-Fail "$($dep.Name) 安装失败: $_"
}
}
# v3.0.3: winget 缺席/失败时, 用 MSI 直链兜底 (主要针对 Win10 21H1 及更早版本无 winget / AppInstaller 过老)
if (-not (Test-Cmd $dep.Cmd) -and $dep.MsiUrl) {
Log-Warn "$($dep.Name) winget 路径失败, 改走 MSI 直链..."
$msiPath = Join-Path $env:TEMP "bw-$($dep.Cmd)-installer.msi"
# v3.0.3/v3.0.5: winget 缺席/失败时, 用 MSI/EXE 直链兜底
# v3.0.5 扩展: Git 是 Inno Setup (非 MSI), 分 MsiUrl 和 ExeUrl 两条分派
if (-not (Test-Cmd $dep.Cmd) -and ($dep.MsiUrl -or $dep.ExeUrl)) {
$isExe = [bool]$dep.ExeUrl
$installerUrl = if ($isExe) { $dep.ExeUrl } else { $dep.MsiUrl }
$ext = if ($isExe) { "exe" } else { "msi" }
$installerPath = Join-Path $env:TEMP "bw-$($dep.Cmd)-installer.$ext"
Log-Warn "$($dep.Name) winget 路径失败, 改走 $($ext.ToUpper()) 直链..."
try {
# TLS 1.2 强制 (Win10 默认 TLS 1.0/1.1 访问 GitHub 会断)
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
Log-Info " 下载 $($dep.MsiUrl.Substring($dep.MsiUrl.LastIndexOf('/')+1))"
Invoke-WebRequest -Uri $dep.MsiUrl -OutFile $msiPath -UseBasicParsing -TimeoutSec 300 -EA Stop
if ((Get-Item $msiPath).Length -lt 1MB) { throw "MSI 下载不完整: $((Get-Item $msiPath).Length) bytes" }
Log-Info " 安装 MSI (静默, 需 1-2 分钟)..."
$p = Start-Process "msiexec.exe" -ArgumentList "/i", "`"$msiPath`"", "/quiet", "/qn", "/norestart", "ADD_PATH=1" -Wait -PassThru -NoNewWindow
Remove-Item $msiPath -Force -EA SilentlyContinue
Log-Info " 下载 $($installerUrl.Substring($installerUrl.LastIndexOf('/')+1))"
Invoke-WebRequest -Uri $installerUrl -OutFile $installerPath -UseBasicParsing -TimeoutSec 300 -EA Stop
if ((Get-Item $installerPath).Length -lt 1MB) { throw "$($ext.ToUpper()) 下载不完整: $((Get-Item $installerPath).Length) bytes" }
Log-Info " 安装 $($ext.ToUpper()) (静默, 需 1-3 分钟)..."
if ($isExe) {
# Inno Setup (Git for Windows): /VERYSILENT /NORESTART /SP- /SUPPRESSMSGBOXES
$exeArgs = if ($dep.ExeArgs) { $dep.ExeArgs } else { @("/VERYSILENT", "/NORESTART", "/NOCANCEL", "/SP-", "/SUPPRESSMSGBOXES") }
$p = Start-Process $installerPath -ArgumentList $exeArgs -Wait -PassThru -NoNewWindow
} else {
# MSI (Node.js / PS7): msiexec /i ... /quiet /qn /norestart ADD_PATH=1
$p = Start-Process "msiexec.exe" -ArgumentList "/i", "`"$installerPath`"", "/quiet", "/qn", "/norestart", "ADD_PATH=1" -Wait -PassThru -NoNewWindow
}
Remove-Item $installerPath -Force -EA SilentlyContinue
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
# PS7 MSI 默认装到 %ProgramFiles%\PowerShell\7\pwsh.exe, 强制补 PATH
$ps7Dir = "$env:ProgramFiles\PowerShell\7"
if ((Test-Path "$ps7Dir\pwsh.exe") -and ($env:Path -notlike "*$ps7Dir*")) { $env:Path = "$ps7Dir;$env:Path" }
# 常见安装位置强制补 PATH (避免 User env 延迟刷新)
$commonDirs = @(
"$env:ProgramFiles\PowerShell\7",
"$env:ProgramFiles\Git\cmd",
"$env:ProgramFiles\Git\bin",
"$env:ProgramFiles\nodejs",
"$env:APPDATA\npm"
)
foreach ($d in $commonDirs) {
if ((Test-Path $d) -and ($env:Path -notlike "*$d*")) { $env:Path = "$d;$env:Path" }
}
if (Test-Cmd $dep.Cmd) {
Log-OK "$($dep.Name) MSI 安装成功 (exit $($p.ExitCode))"
Log-OK "$($dep.Name) $($ext.ToUpper()) 安装成功 (exit $($p.ExitCode))"
$installed += $dep.Name
} else {
Log-Fail "$($dep.Name) MSI 安装后仍找不到 pwsh (exit $($p.ExitCode))"
Log-Fail "$($dep.Name) $($ext.ToUpper()) 安装后仍找不到 $($dep.Cmd) (exit $($p.ExitCode))"
}
} catch { Log-Fail "$($dep.Name) MSI 直链失败: $_" }
} catch { Log-Fail "$($dep.Name) $($ext.ToUpper()) 直链失败: $_" }
}
elseif ($dep.NpmPkg -and (Test-Cmd "npm")) {
try {
@ -978,7 +1028,45 @@ if (-not (Test-Cmd "node") -or -not (Test-Cmd "git") -or -not (Test-Cmd "claude"
if (-not (Test-Cmd "node")) { $missing += "Node.js" }
if (-not (Test-Cmd "git")) { $missing += "Git" }
if (-not (Test-Cmd "claude")) { $missing += "Claude Code" }
Show-MsgBox "以下核心依赖安装失败: $($missing -join ', ')`n`n请手动安装后重新运行。`n`nNode.js: https://nodejs.org`nGit: https://git-scm.com" "安装中断" "OK" "Error"
# v3.0.5: 清除桌面僵尸快捷方式, 防用户点击空快捷方式触发 'claude.exe not found'
try {
$desktop = [System.Environment]::GetFolderPath("Desktop")
foreach ($lnk in @("启动Bookworm.lnk", "更新Bookworm.lnk", "Bookworm.lnk")) {
$lnkPath = Join-Path $desktop $lnk
if (Test-Path $lnkPath) {
Remove-Item -LiteralPath $lnkPath -Force -EA SilentlyContinue
Log-Warn "已移除僵尸快捷方式: $lnk (Phase 1 未完成)"
}
}
} catch {}
# v3.0.5: 手动安装清单 (winget 失败 + 直链失败时最后一根稻草)
$manualGuide = "以下核心依赖自动安装失败:`n " + ($missing -join ", ") + "`n`n"
$manualGuide += "【手动安装清单】请按顺序操作:`n`n"
if ($missing -contains "Node.js") {
$manualGuide += "1. Node.js`n"
$manualGuide += " 下载: https://nodejs.org/zh-cn/download`n"
$manualGuide += " 选 LTS → Windows Installer (.msi) → 双击安装`n`n"
}
if ($missing -contains "Git") {
$manualGuide += "2. Git for Windows`n"
$manualGuide += " 下载: https://git-scm.com/download/win`n"
$manualGuide += " 选 64-bit Git → 双击安装 (默认选项即可)`n`n"
}
if ($missing -contains "Claude Code") {
$manualGuide += "3. Claude Code (需先装好 Node.js)`n"
$manualGuide += " 命令行执行: npm i -g @anthropic-ai/claude-code`n`n"
}
$manualGuide += "【完成后】`n"
$manualGuide += " 重新双击 Bookworm-Setup.exe 即可继续安装`n"
$manualGuide += " (已装成功的依赖会被自动跳过)`n`n"
$manualGuide += "【仍有问题?】`n"
$manualGuide += " 1. 检查是否以管理员身份运行`n"
$manualGuide += " 2. 检查网络能否访问上述下载地址`n"
$manualGuide += " 3. 联系 Bookworm 管理员`n"
Show-MsgBox $manualGuide "安装中断 — 手动安装指引" "OK" "Error"
exit 1
}
if (-not (Test-Cmd "pwsh")) {
@ -1675,6 +1763,133 @@ if (Test-Path $templateFile) {
$env:CLAUDE_HOME = $ClaudeDir
# ========================================================================
# Phase 5.5: 截图粘贴助手部署 (v3.0.5 新增)
# ========================================================================
# 目标: Shift+Win+S 截图后, 在 Claude Code CLI Ctrl+V 直接粘贴为 [Image #N] 附件
# 分发文件: $ClaudeDir\scripts\ClipImageWatcher.ps1 (由 bookworm-portable-config 仓库提供)
# 注入点:
# 1. $HOME\Documents\PowerShell\profile.ps1 (sentinel 追加式, Q1=A)
# 2. HKCU:\...Microsoft.ScreenSketch\...\Enabled=0 (备份原值到 HKCU:\Software\Bookworm\ToastBackup, 卸载时还原, Q2=B)
# ========================================================================
Log-Info "[5.5/$TOTAL_PHASES] 截图粘贴助手部署"
Update-Progress-SubStatus "配置截图粘贴助手..."
$clipWatcherSrc = Join-Path $ClaudeDir "scripts\ClipImageWatcher.ps1"
if (Test-Path $clipWatcherSrc) {
# ── 5.5a: profile sentinel 注入 (幂等, 精准替换 BW_CLIP_START..END 中间块) ──
$psProfileDir = Join-Path $env:USERPROFILE "Documents\PowerShell"
$psProfilePath = Join-Path $psProfileDir "profile.ps1"
if (-not (Test-Path $psProfileDir)) { New-Item -ItemType Directory -Path $psProfileDir -Force | Out-Null }
$sentinelStart = "# BW_CLIP_START v3.0.5 — 不要手动修改此块 (Bookworm 自动生成)"
$sentinelEnd = "# BW_CLIP_END"
$bwClipBlock = @"
$sentinelStart
`$script:ClipWatcherPath = "`$HOME\.claude\scripts\ClipImageWatcher.ps1"
function Start-ClipWatch {
[CmdletBinding()]
param([int]`$IntervalMs = 400, [switch]`$Foreground)
if (-not (Test-Path `$script:ClipWatcherPath)) {
Write-Error "找不到监听脚本: `$script:ClipWatcherPath"
return
}
if (`$Foreground) {
& pwsh -NoProfile -ExecutionPolicy Bypass -File `$script:ClipWatcherPath -IntervalMs `$IntervalMs
return
}
`$logDir = Join-Path `$env:TEMP 'claude-clip'
if (-not (Test-Path `$logDir)) { New-Item -ItemType Directory -Path `$logDir -Force | Out-Null }
`$logPath = Join-Path `$logDir 'watcher.log'
`$psi = New-Object System.Diagnostics.ProcessStartInfo
`$psi.FileName = 'pwsh'
`$psi.Arguments = "-NoProfile -ExecutionPolicy Bypass -File ```"`$(`$script:ClipWatcherPath)```" -IntervalMs `$IntervalMs"
`$psi.UseShellExecute = `$false
`$psi.CreateNoWindow = `$true
`$psi.WindowStyle = 'Hidden'
`$psi.RedirectStandardOutput = `$true
`$psi.RedirectStandardError = `$true
`$p = [System.Diagnostics.Process]::Start(`$psi)
`$global:ClipWatchPid = `$p.Id
Register-ObjectEvent -InputObject `$p -EventName OutputDataReceived -Action {
if (`$EventArgs.Data) { Add-Content -Path `$using:logPath -Value `$EventArgs.Data -Encoding UTF8 }
} | Out-Null
Register-ObjectEvent -InputObject `$p -EventName ErrorDataReceived -Action {
if (`$EventArgs.Data) { Add-Content -Path `$using:logPath -Value "[ERR] `$(`$EventArgs.Data)" -Encoding UTF8 }
} | Out-Null
`$p.BeginOutputReadLine()
`$p.BeginErrorReadLine()
Write-Host "[ClipWatch] 已静默启动 (PID=`$(`$p.Id))。日志: `$logPath" -ForegroundColor Cyan
}
function Stop-ClipWatch {
if (`$global:ClipWatchPid) {
try { Stop-Process -Id `$global:ClipWatchPid -Force -ErrorAction Stop; Write-Host "[ClipWatch] 已停止 PID=`$global:ClipWatchPid" -ForegroundColor Yellow }
catch { Write-Warning "停止失败: `$(`$_.Exception.Message)" }
`$global:ClipWatchPid = `$null
} else { Write-Host "[ClipWatch] 未在运行。" -ForegroundColor DarkGray }
}
function Get-ClipWatchDir {
`$d = Join-Path `$env:TEMP 'claude-clip'
if (Test-Path `$d) { Get-ChildItem `$d | Sort-Object LastWriteTime -Descending | Select-Object -First 10 }
else { Write-Host "尚未产生截图。" }
}
# 自动启动监听器 (全局只跑一份, 用 CommandLine 查重)
try {
`$watcherName = Split-Path `$script:ClipWatcherPath -Leaf
`$running = Get-CimInstance Win32_Process -Filter "Name='pwsh.exe'" -ErrorAction SilentlyContinue |
Where-Object { `$_.CommandLine -like "*`$watcherName*" }
if (-not `$running -and (Test-Path `$script:ClipWatcherPath)) { Start-ClipWatch }
} catch {}
$sentinelEnd
"@
# 读现有 profile, 精准替换 sentinel 中间块 (不动其他内容)
$existing = if (Test-Path $psProfilePath) { Get-Content $psProfilePath -Raw -Encoding UTF8 } else { "" }
$pattern = [regex]::Escape($sentinelStart) + "[\s\S]*?" + [regex]::Escape($sentinelEnd)
if ($existing -match $pattern) {
$updated = [regex]::Replace($existing, $pattern, [regex]::Escape($bwClipBlock) -replace '\\(.)', '$1', 1)
# 上面 Regex.Replace 的 replacement 参数需要 escape 一次, 改用简单字符串替换
$updated = $existing -replace $pattern, [System.Text.RegularExpressions.Regex]::Unescape([regex]::Escape($bwClipBlock))
} else {
$updated = if ($existing) { $existing.TrimEnd() + "`n`n" + $bwClipBlock + "`n" } else { $bwClipBlock + "`n" }
}
# 无 BOM UTF-8 写入 (PS7 能正确识别, PS5.1 也 OK)
[System.IO.File]::WriteAllText($psProfilePath, $updated, [System.Text.UTF8Encoding]::new($false))
Log-OK "PowerShell profile 已注入 BW_CLIP 块: $psProfilePath"
# ── 5.5b: 截图 Toast 关闭 + 备份原值用于卸载还原 ──
$toastReg = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Notifications\Settings\Microsoft.ScreenSketch_8wekyb3d8bbwe!App"
$bwBackup = "HKCU:\Software\Bookworm\ToastBackup"
try {
$origVal = $null
if (Test-Path $toastReg) {
$origVal = (Get-ItemProperty -Path $toastReg -Name "Enabled" -EA SilentlyContinue).Enabled
}
# 备份原值到 HKCU:\Software\Bookworm\ToastBackup (卸载时用于还原)
if (-not (Test-Path $bwBackup)) { New-Item -Path $bwBackup -Force | Out-Null }
$bwBackupProps = Get-ItemProperty -Path $bwBackup -EA SilentlyContinue
if (-not $bwBackupProps -or -not $bwBackupProps.ScreenSketchToast_Backed) {
# 只在首次备份 (防重装覆盖用户手动改过的值)
$bakValue = if ($null -eq $origVal) { "__ABSENT__" } else { $origVal.ToString() }
New-ItemProperty -Path $bwBackup -Name "ScreenSketchToast_Original" -Value $bakValue -PropertyType String -Force | Out-Null
New-ItemProperty -Path $bwBackup -Name "ScreenSketchToast_Backed" -Value 1 -PropertyType DWord -Force | Out-Null
Log-Info "已备份 Toast 原值到 $bwBackup (卸载时自动还原)"
}
# 关 Toast
if (-not (Test-Path $toastReg)) { New-Item -Path $toastReg -Force | Out-Null }
New-ItemProperty -Path $toastReg -Name "Enabled" -Value 0 -PropertyType DWord -Force | Out-Null
Log-OK "截图工具 Toast 已关闭 (原值已备份)"
} catch {
Log-Warn "截图 Toast 关闭失败 (不影响截图本身): $_"
}
} else {
Log-Warn "ClipImageWatcher.ps1 未分发到 $clipWatcherSrc, 跳过截图助手部署"
}
# ========================================================================
# Phase 6: MCP 验证 + 自动安装
# ========================================================================

View File

@ -429,14 +429,46 @@ foreach ($c in $checks) {
if (-not (Test-Command "claude") -or -not (Test-Command "node") -or -not (Test-Command "git")) {
Install-MissingDeps
}
# 再次验证
# v3.0.5: 再次验证 — StartOnly 场景加 GUI 弹窗 (防止 console 闪退用户看不见)
# 触发条件: 用户双击老快捷方式, 但 Phase 1 之前失败导致 claude/node 未装
function Show-MissingDepGui {
param([string]$depName, [string]$installCmd)
try {
Add-Type -AssemblyName System.Windows.Forms -EA Stop
$msg = @"
检测到 $depName 未安装无法启动 Bookworm
最可能原因:
上次安装器未完成 (Phase 1 环境检测被中断)
推荐修复
1. 双击桌面或下载目录的 Bookworm-Setup.exe
2. 安装器会自动补装缺失的依赖
3. 已装好的部分会被跳过, 不会重复
手动修复
$installCmd
完成后再次点击启动快捷方式即可
"@
[System.Windows.Forms.MessageBox]::Show($msg, "Bookworm 启动失败 — $depName 未安装", 'OK', 'Error') | Out-Null
} catch { }
}
if (-not (Test-Command "claude")) {
Write-Host "`n [ABORT] Claude Code 未安装" -ForegroundColor Red
Write-Host " 安装: npm i -g @anthropic-ai/claude-code" -ForegroundColor Gray
if ($StartOnly) { Show-MissingDepGui "Claude Code" "npm i -g @anthropic-ai/claude-code" }
exit 1
}
if (-not (Test-Command "node")) {
Write-Host "`n [ABORT] Node.js 未安装" -ForegroundColor Red
if ($StartOnly) { Show-MissingDepGui "Node.js" "https://nodejs.org/zh-cn/download 下载 LTS .msi" }
exit 1
}
if (-not (Test-Command "git")) {
Write-Host "`n [ABORT] Git 未安装" -ForegroundColor Red
if ($StartOnly) { Show-MissingDepGui "Git" "https://git-scm.com/download/win 下载 64-bit" }
exit 1
}