bookworm-boot/tools/gen-launcher-bats.ps1
bookworm 609e82bac0 hotfix(v3.0.10): Base64 '-or' 括号 bug + dry-run 实跑验证护栏
v3.0.9 Base64 脚本启动即报:
  Test-Path: A parameter cannot be found that matches parameter name 'or'.
  claude.exe not found

根因: 'Test-Path (Join-Path $p claude.ps1) -or (Test-Path ...)' 中 -or
被当成 Test-Path 的命名参数. PSParser 静态检查看合法, 运行时炸.

修复:
  F1: 括号修正 — $hasClaude 抽为独立变量, 三元 -or 每项带外括号
  F2: gen-launcher-bats.ps1 强制加 dry-run 实跑验证护栏
      解码后的 Base64 脚本必须被 pwsh -File 实跑到底部 __BW_DRYRUN_OK__
      才算通过. 检查 ErrorRecord / ParameterBindingException / 未知命令.
      任何未来 Base64 改动都被此验证拦截.

验证层级教训:
  PSParser = 抓语法   / 抓不到参数绑定错
  dry-run  = 抓运行时 / 抓不到业务逻辑
  smoke    = 抓业务   / 需要前两层通过

EXE 220160 → 220672 bytes (+512)
2026-04-24 22:48:08 +08:00

219 lines
9.5 KiB
PowerShell
Raw 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.

# Bookworm Portable 启动器 bat 生成工具 (v3.0.6)
# 用途: 从单一明文 PowerShell 脚本生成两个 bat, 避免手工同步 Base64 字符串不一致
# 用法: pwsh -NoProfile -File tools/gen-launcher-bats.ps1
# 输出: 启动Bookworm.bat + 更新并启动Bookworm.bat (覆盖写入)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$launchBat = Join-Path $repoRoot "启动Bookworm.bat"
$updateBat = Join-Path $repoRoot "更新并启动Bookworm.bat"
# ─── 明文: 三层 PATH 修复 + DPAPI 加载 + claude 诊断 + 启动 ─────────
# v3.0.9: 增加 npm config get prefix 动态查询, 兼容 nvm/fnm/Program Files 等非标准 npm 位置
$plainScript = @'
Add-Type -AssemblyName System.Security
# 层 1: Machine + User env PATH (标准 Windows 环境变量)
$env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User')
# 层 2: npm config get prefix (真实 npm 全局目录, 兼容 nvm/fnm/标准安装/Program Files)
try {
$npmPrefix = (& npm config get prefix 2>$null | Select-Object -First 1).Trim()
if ($npmPrefix -and (Test-Path $npmPrefix) -and ($env:Path -notlike "*$npmPrefix*")) {
$env:Path = "$npmPrefix;$env:Path"
}
} catch {}
# 层 3: 常见 npm global 硬编码兜底 (npm 本身不在 PATH 时无法 query)
$npmCandidates = @(
"$env:APPDATA\npm",
"$env:ProgramFiles\nodejs",
"${env:ProgramFiles(x86)}\nodejs",
"$env:LOCALAPPDATA\npm"
)
foreach ($p in $npmCandidates) {
if (-not (Test-Path $p)) { continue }
$hasClaude = (Test-Path (Join-Path $p 'claude.ps1')) -or (Test-Path (Join-Path $p 'claude.cmd')) -or (Test-Path (Join-Path $p 'claude'))
if ($hasClaude -and ($env:Path -notlike "*$p*")) {
$env:Path = "$p;$env:Path"
}
}
# DPAPI 加载缓存凭证
$r = 'HKCU:\Software\Bookworm\CachedEnv'
try {
(Get-ItemProperty $r -EA Stop).PSObject.Properties | Where-Object { $_.Name -match '^[A-Z_]+$' } | ForEach-Object {
$v = $_.Value
try {
$b = [Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String($v), $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
$v = [Text.Encoding]::UTF8.GetString($b)
} catch {}
[Environment]::SetEnvironmentVariable($_.Name, $v, 'Process')
}
} catch {}
if (-not (Get-Command claude -ErrorAction SilentlyContinue)) {
Write-Host ''
Write-Host ' [!] claude 命令未找到 (已尝试 3 层 PATH 修复仍失败)' -ForegroundColor Red
Write-Host ''
Write-Host ' 诊断信息:' -ForegroundColor Yellow
Write-Host " npm prefix: $(try { (& npm config get prefix 2>$null) } catch { '(npm 不可用)' })" -ForegroundColor Gray
Write-Host ' PATH 片段 (npm/nodejs/pwsh/Git):' -ForegroundColor Gray
($env:Path -split ';') | Where-Object { $_ -match 'npm|nodejs|pwsh|Git' } | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray }
Write-Host ''
Write-Host ' 修复: 重新运行 Bookworm-Setup.exe (v3.0.9+) 即可自动补全' -ForegroundColor Green
Write-Host ''
Read-Host '按回车关闭'
return
}
& claude --dangerously-skip-permissions
'@
# ─── Base64-UTF-16LE 编码 ─────────────────────────────────
$bytes = [System.Text.Encoding]::Unicode.GetBytes($plainScript)
$enc = [Convert]::ToBase64String($bytes)
# 健康检查
if ($enc.Length -gt 7500) { throw "Base64 长度 $($enc.Length) 超 bat 变量安全上限 7500" }
$bad = $enc -replace '[A-Za-z0-9+/=]', ''
if ($bad) { throw "Base64 含非法字符: [$bad]" }
Write-Host "[gen-launcher-bats] Base64 长度: $($enc.Length), 纯字符集检查 OK" -ForegroundColor Green
# ─── bat 1: 启动Bookworm.bat ──────────────────────────────
$launch = @"
@echo off
chcp 65001 > nul
cd /d "%~dp0"
::
set NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1
set no_proxy=%NO_PROXY%
:: (bookworm-boot + .claude , )
echo [..] ...
git pull --rebase >nul 2>nul
git -C "%USERPROFILE%\.claude" pull --rebase >nul 2>nul
set USE_WT=0
where wt >nul 2>nul && set USE_WT=1
set USE_PWSH7=0
where pwsh >nul 2>nul && set USE_PWSH7=1
:: v3.0.6: Base64-UTF-16LE (PATH + DPAPI + claude + )
:: A-Za-z0-9+/= , wt.exe ';' tab ( 64856bc )
:: -d "%CD%" , -d "%~dp0" ( 0c33109 )
:: : pwsh -NoProfile -File tools/gen-launcher-bats.ps1
set ENC=$enc
:: : wt + pwsh7
if %USE_WT% equ 1 if %USE_PWSH7% equ 1 (
start "" wt new-tab --title "Bookworm Smart Assistant" -d "%CD%" -- pwsh -NoLogo -NoExit -EncodedCommand %ENC%
exit
)
:: 2: wt + powershell 5.1
if %USE_WT% equ 1 if %USE_PWSH7% equ 0 (
start "" wt new-tab --title "Bookworm Smart Assistant" -d "%CD%" -- powershell -NoLogo -ExecutionPolicy Bypass -NoExit -EncodedCommand %ENC%
exit
)
:: 3: conhost + pwsh7 ( wt ; tab , Base64 )
if %USE_PWSH7% equ 1 (
start "Bookworm Smart Assistant" pwsh -NoLogo -NoExit -EncodedCommand %ENC%
exit
)
:: 4: 退 PowerShell 5.1 (, install.ps1 -StartOnly )
title Bookworm Portable
echo.
echo [!] PowerShell 7 , 使 PowerShell 5.1
echo.
powershell -ExecutionPolicy Bypass -File install.ps1 -StartOnly -AutoAccept
if %errorlevel% neq 0 (
echo.
echo 退...
pause > nul
)
"@
# ─── bat 2: 更新并启动Bookworm.bat ───────────────────────
$update = @"
@echo off
chcp 65001 > nul
cd /d "%~dp0"
::
set NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1
set no_proxy=%NO_PROXY%
:: (bookworm-boot + .claude )
echo [..] ...
git pull --rebase >nul 2>nul
git -C "%USERPROFILE%\.claude" pull --rebase >nul 2>nul
:: v3.0.6: Bookworm.bat Base64 (DPAPI + PATH + claude )
set ENC=$enc
:: pwsh7
where pwsh >nul 2>nul
if %errorlevel% equ 0 (
:: pwsh7: (SkipLaunch claude), -EncodedCommand
pwsh -NoLogo -ExecutionPolicy Bypass -File "%~dp0install.ps1" -AutoAccept -SkipLaunch
start "Bookworm Smart Assistant" pwsh -NoLogo -NoExit -EncodedCommand %ENC%
exit
)
:: 退 PowerShell 5.1: ++ ()
title Bookworm Portable
powershell -ExecutionPolicy Bypass -File "%~dp0install.ps1" -AutoAccept
if %errorlevel% neq 0 (
echo.
echo 退...
pause > nul
)
"@
# ─── 写入 ─────────────────────────────────────────────────
# bat 文件默认期望 GBK/ANSI, 但脚本顶部 chcp 65001 已切换到 UTF-8, 用无 BOM UTF-8 写入
[System.IO.File]::WriteAllText($launchBat, $launch, [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($updateBat, $update, [System.Text.UTF8Encoding]::new($false))
Write-Host "[gen-launcher-bats] ✓ 启动Bookworm.bat ($((Get-Item $launchBat).Length) bytes)" -ForegroundColor Green
Write-Host "[gen-launcher-bats] ✓ 更新并启动Bookworm.bat ($((Get-Item $updateBat).Length) bytes)" -ForegroundColor Green
# ─── Round-trip 验证 (v3.0.10: 除了 PARSE 还要 lint + 实跑不启动 claude) ────
$decoded = [System.Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($enc))
$err = $null
[void][System.Management.Automation.Language.Parser]::ParseInput($decoded, [ref]$null, [ref]$err)
if ($err) { throw "解码后脚本 PARSE ERR: $($err[0])" }
Write-Host "[gen-launcher-bats] ✓ PARSE OK ($($decoded.Length) chars)" -ForegroundColor Green
# 实跑验证 (v3.0.10: 截断到 & claude 之前, 只跑 PATH 修复 + DPAPI 加载, 不启动 claude)
# 这能抓出 PARSE 通过但运行时报错的 bug (例如 -or 被当 Test-Path 参数)
$runnable = $decoded -replace '& claude --dangerously-skip-permissions', 'Write-Host "__BW_DRYRUN_OK__"'
$tmpPs1 = Join-Path $env:TEMP "bw-launcher-dryrun-$(Get-Random).ps1"
Set-Content -Path $tmpPs1 -Value $runnable -Encoding UTF8
try {
$dryRunOutput = (& pwsh -NoProfile -ExecutionPolicy Bypass -File $tmpPs1 2>&1 | Out-String)
# 只抓真正的 PS 错误 (ErrorRecord / cannot be found / parameter name / 等)
$errorPatterns = @(
'cannot be found that matches parameter name',
'A parameter cannot be found',
'CommandNotFoundException',
'ParameterBindingException',
'is not recognized as',
'cannot find.*because it does not exist',
'RuntimeException'
)
$hasError = $false
foreach ($pat in $errorPatterns) {
if ($dryRunOutput -match $pat) { $hasError = $true; break }
}
# 必须看到 dry-run 成功标记才算通过
$reachedEnd = $dryRunOutput -match '__BW_DRYRUN_OK__'
if ($hasError -or -not $reachedEnd) {
Write-Host "[gen-launcher-bats] ✗ 实跑验证失败:" -ForegroundColor Red
Write-Host $dryRunOutput -ForegroundColor DarkRed
throw "Base64 解码后脚本运行时错误 (hasError=$hasError, reachedEnd=$reachedEnd)"
}
Write-Host "[gen-launcher-bats] ✓ 实跑通过 (dry-run 到达 __BW_DRYRUN_OK__ 标记)" -ForegroundColor Green
} finally {
Remove-Item $tmpPs1 -Force -EA SilentlyContinue
}