feat(v3.0.11): 架构重构 — 启动链路 7 跳 → 1 跳直链

用户报障驱动 (v3.0.3-3.0.10 共 9 轮迭代后接受架构原罪).

旧架构: .lnk → bat → wt → pwsh → Base64 → DPAPI → claude (7 跳)
新架构: .lnk → pwsh → claude.ps1 (1 跳, 凭证由 profile 异步加载)

ADR-001 桌面 .lnk 直调 pwsh + claude.ps1 绝对路径
  - 安装时 Get-Command claude / npm config get prefix 定位 claude.ps1
  - bake 进 .lnk Args, 运行时不依赖任何 PATH 重载
  - 自验证: 创建后回读 .lnk 确认 Target/Args 完整, 失败立即删除拒绝交付

ADR-002 DPAPI 凭证加载迁移到 PowerShell profile sentinel
  - BW_CRED_START..END 块写入 ~/Documents/PowerShell/profile.ps1
  - pwsh 启动自动 source, 与启动链路彻底解耦
  - profile 编辑用 String.Replace 字面替换 (避开 -replace $ backreference)

ADR-003 启动 / 更新功能拆分
  - 启动Bookworm.lnk → 仅启动 (1 跳)
  - 更新Bookworm.lnk → 仅 git pull
  - 更新并启动Bookworm.bat → 重命名为 更新Bookworm.bat (仅同步)
  - tools/gen-launcher-bats.ps1 归档 (新架构不需要 Base64 生成器)

8 个失败模式中 7 个被架构性消除:
   F1 wt ; 切 tab (无 wt)
   F2 cmd 转义 (无 cmd 启动路径)
   F3 wt -d 引号 (无 wt -d)
   F4 Base64 解码语义错 (无 Base64)
   F5 PATH 时序 (绝对路径不依赖 PATH)
   F6 claude shim (直调 .ps1)
   F8 wt 自身 issue (无 wt)
  ⚠️ F7 DPAPI 跨用户 (设计限制, 跨用户需重激活)

EXE 220672 → 227840 bytes (+7168, ADR-002 profile 注入函数)
This commit is contained in:
bookworm 2026-04-25 18:19:07 +08:00
parent 609e82bac0
commit 1421338da3
6 changed files with 269 additions and 129 deletions

View File

@ -48,7 +48,7 @@ trap {
}
# ─── 版本号 (每次更新递增, build.ps1 自动读取) ──────
$BWVersion = "3.0.10" # hotfix: Base64 脚本 -or 括号导致 Test-Path 参数错 (gen-launcher-bats 加实跑 dry-run 防护)
$BWVersion = "3.0.11" # 架构重构: 桌面 .lnk 直调 pwsh+claude.ps1 (1跳直链), 凭证迁移 profile sentinel, 启动/更新拆分
# DryRun 模式日志标记
if ($DryRun) { $global:BWDryRun = $DryRun } else { $global:BWDryRun = $null }
@ -742,51 +742,172 @@ function Save-SecretsToCache {
}
}
Set-ItemProperty $regPath -Name "_expiry" -Value (Get-Date).Date.AddDays(1).ToUniversalTime().ToString("o") -Force
# v3.0.11: 同步注入 profile.ps1 sentinel 块 (pwsh 启动时自动加载凭证, 与启动链路解耦)
try { Inject-CredentialLoaderProfile } catch { Bw-Log "WARN" "profile 凭证块注入失败: $_" }
} catch {}
}
# v3.0.11 ADR-002: DPAPI 凭证加载迁移到 PowerShell profile
# 桌面 .lnk 直调 pwsh + claude.ps1 后, 凭证由 pwsh profile 启动 hook 自动加载,
# 完全脱离 bat/Base64 链路. profile 编辑用 String.Replace 字面替换 (不走 -replace
# 的 $ backreference 语义, 避免 v3.0.5 BW_CLIP 块踩过的同类 bug)
function Inject-CredentialLoaderProfile {
$psProfileDir = Join-Path $env:USERPROFILE "Documents\PowerShell"
if (-not (Test-Path $psProfileDir)) { New-Item -ItemType Directory -Path $psProfileDir -Force | Out-Null }
$psProfilePath = Join-Path $psProfileDir "profile.ps1"
$sentinelStart = "# BW_CRED_START v3.0.11 — 不要手动修改 (Bookworm 启动凭证 DPAPI 自动加载)"
$sentinelEnd = "# BW_CRED_END"
# 注: here-string 内 PS 变量用反引号 ` 转义保留为字面量, 由 profile 加载时再求值
$block = @"
$sentinelStart
# pwsh 启动时自动从 HKCU:\Software\Bookworm\CachedEnv 解密凭证
# 写入 Process 作用域 env, claude.ps1 启动后能直接读到 ANTHROPIC_API_KEY 等
try {
`$bwReg = 'HKCU:\Software\Bookworm\CachedEnv'
if (Test-Path `$bwReg) {
Add-Type -AssemblyName System.Security -ErrorAction Stop
(Get-ItemProperty `$bwReg -ErrorAction 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 {}
$sentinelEnd
"@
$existing = if (Test-Path $psProfilePath) { Get-Content $psProfilePath -Raw -Encoding UTF8 } else { "" }
$pattern = [regex]::Escape($sentinelStart) + "[\s\S]*?" + [regex]::Escape($sentinelEnd)
$regex = [regex]::new($pattern)
$match = $regex.Match($existing)
if ($match.Success) {
# 字面替换 — 绕过 -replace 的 $ backreference 语义
$updated = $existing.Replace($match.Value, $block)
} else {
$updated = if ($existing) { $existing.TrimEnd() + "`n`n" + $block + "`n" } else { $block + "`n" }
}
[System.IO.File]::WriteAllText($psProfilePath, $updated, [System.Text.UTF8Encoding]::new($false))
Bw-Log "OK" "BW_CRED 块已注入 PowerShell profile: $psProfilePath"
}
# ─── 桌面快捷方式 ──────────────────────────────────
function New-DesktopShortcuts {
# v3.0.11 架构重构: 桌面 .lnk 直调 pwsh + claude.ps1 绝对路径
# 不再走 bat → wt → Base64 → DPAPI 7 跳链路, 消除 F1-F8 大部分失败模式
# 启动 lnk: Target=pwsh.exe, Args=-NoLogo -NoExit -File "<claude.ps1 绝对路径>" --dangerously-skip-permissions
# 更新 lnk: Target=cmd.exe, Args=/c "git pull..." (失败也不影响启动 lnk)
try {
$shell = New-Object -ComObject WScript.Shell
$desktop = $shell.SpecialFolders("Desktop")
# 桌面专用图标 (Bookworm 蓝紫渐变 B 圆, 多尺寸 ICO)
$iconPath = Join-Path $BootDir "bookworm-desktop.ico"
if (-not (Test-Path $iconPath)) {
# 回退到 EXE 图标 (bookworm.ico)
$iconPath = Join-Path $BootDir "bookworm.ico"
# ── 1. 定位 pwsh.exe 绝对路径 ─────────────────────────
$pwshExe = (Get-Command pwsh -ErrorAction SilentlyContinue).Source
if (-not $pwshExe -or -not (Test-Path $pwshExe)) {
# 兜底候选 (winget 默认 / MSI 默认)
foreach ($p in @("$env:ProgramFiles\PowerShell\7\pwsh.exe", "${env:ProgramFiles(x86)}\PowerShell\7\pwsh.exe", "$env:LOCALAPPDATA\Microsoft\PowerShell\pwsh.exe")) {
if (Test-Path $p) { $pwshExe = $p; break }
}
}
if (-not $pwshExe -or -not (Test-Path $pwshExe)) {
Log-Fail "找不到 pwsh.exe 绝对路径, 拒绝创建桌面快捷方式 (v3.0.11 架构强依赖 pwsh)"
Show-MsgBox "无法定位 pwsh.exe 绝对路径.`n`nPowerShell 7 可能未正确安装. 请先解决 PS7 安装问题再重跑安装器." "v3.0.11 桌面快捷方式创建失败" "OK" "Error"
return
}
# 步骤 1: 快速启动 (bat 文件位于 bookworm-boot 仓库内)
# ── 2. 定位 claude.ps1 绝对路径 ───────────────────────
# 优先 Get-Command claude (运行时 PATH 里的)
$claudePs1 = $null
$claudeCmd = Get-Command claude -ErrorAction SilentlyContinue
if ($claudeCmd) {
$src = $claudeCmd.Source
if ($src -and $src.EndsWith('.ps1') -and (Test-Path $src)) { $claudePs1 = $src }
}
# 兜底 npm config get prefix
if (-not $claudePs1) {
try {
$npmPrefix = (& npm config get prefix 2>$null | Select-Object -First 1).Trim()
$candidate = Join-Path $npmPrefix "claude.ps1"
if (Test-Path $candidate) { $claudePs1 = $candidate }
} catch {}
}
# 最终硬编码兜底
if (-not $claudePs1) {
foreach ($p in @("$env:APPDATA\npm\claude.ps1", "$env:ProgramFiles\nodejs\claude.ps1", "$env:LOCALAPPDATA\npm\claude.ps1")) {
if (Test-Path $p) { $claudePs1 = $p; break }
}
}
if (-not $claudePs1 -or -not (Test-Path $claudePs1)) {
Log-Fail "找不到 claude.ps1 绝对路径, 拒绝创建桌面快捷方式"
Show-MsgBox "无法定位 claude.ps1 文件.`n`nClaude Code 可能未正确安装. 请先解决 Claude Code 安装问题 (npm i -g @anthropic-ai/claude-code) 再重跑安装器." "v3.0.11 桌面快捷方式创建失败" "OK" "Error"
return
}
Log-OK "v3.0.11 启动路径已锁定: pwsh=$pwshExe; claude.ps1=$claudePs1"
# ── 3. 桌面图标 ───────────────────────────────────────
$iconPath = Join-Path $BootDir "bookworm-desktop.ico"
if (-not (Test-Path $iconPath)) { $iconPath = Join-Path $BootDir "bookworm.ico" }
# ── 4. 启动 .lnk: 直调 pwsh + claude.ps1 (1 跳链路) ──
$newLnk = "$desktop\启动Bookworm.lnk"
$shortcut = $shell.CreateShortcut($newLnk)
$batPath = Join-Path $BootDir "启动Bookworm.bat"
if (-not (Test-Path $batPath)) { $batPath = Join-Path $BootDir "Bookworm-OneClick.bat" }
$shortcut.TargetPath = $batPath
$shortcut.WorkingDirectory = $BootDir
$shortcut.Description = "Bookworm Smart Assistant - 智能助手"
$shortcut.TargetPath = $pwshExe
$shortcut.Arguments = "-NoLogo -NoExit -File `"$claudePs1`" --dangerously-skip-permissions"
$shortcut.WorkingDirectory = $env:USERPROFILE
$shortcut.Description = "Bookworm Smart Assistant (v3.0.11 直调架构)"
if (Test-Path $iconPath) { $shortcut.IconLocation = "$iconPath,0" }
$shortcut.Save()
# 步骤 2: 迁移清理老 Bookworm.lnk (v3.0.3 及以前命名), 必须在新 lnk 确认存在后, 防空窗
# ── 5. 自验证: 读回 .lnk 确认 Target/Args 写入完整 ──
$verify = $shell.CreateShortcut($newLnk)
if ($verify.TargetPath -ne $pwshExe) {
Log-Fail "启动.lnk 自验证失败: TargetPath 不匹配 ($($verify.TargetPath) vs $pwshExe)"
Remove-Item $newLnk -Force -EA SilentlyContinue
Show-MsgBox "桌面快捷方式自验证失败 (Target 不匹配). 已删除避免坏快捷方式. 请重跑安装器." "v3.0.11 自验证失败" "OK" "Error"
return
}
if ($verify.Arguments -notmatch [regex]::Escape("-File `"$claudePs1`"")) {
Log-Fail "启动.lnk 自验证失败: Arguments 不含 claude.ps1 路径"
Remove-Item $newLnk -Force -EA SilentlyContinue
return
}
Log-OK "启动Bookworm.lnk 创建并自验证通过"
# ── 6. 迁移清理老 lnk (v3.0.3 及以前的 Bookworm.lnk) ──
$oldLnk = "$desktop\Bookworm.lnk"
if ((Test-Path $oldLnk) -and (Test-Path $newLnk)) {
try { Remove-Item -LiteralPath $oldLnk -Force -ErrorAction Stop; Log-OK "已清理旧快捷方式 Bookworm.lnk (迁移到「启动Bookworm」)" }
catch { Log-Warn "无法清理旧 Bookworm.lnk: $_" }
if (Test-Path $oldLnk) {
try { Remove-Item -LiteralPath $oldLnk -Force -ErrorAction Stop; Log-Info "已清理旧 Bookworm.lnk (迁移到 v3.0.11 架构)" } catch {}
}
# 更新启动
$shortcut2 = $shell.CreateShortcut("$desktop\更新Bookworm.lnk")
$updateBat = Join-Path $BootDir "更新并启动Bookworm.bat"
if (Test-Path $updateBat) {
$shortcut2.TargetPath = $updateBat
$shortcut2.WorkingDirectory = $BootDir
$shortcut2.Description = "更新并启动 Bookworm"
if (Test-Path $iconPath) { $shortcut2.IconLocation = "$iconPath,0" }
$shortcut2.Save()
# ── 7. 更新 .lnk: 仅 git pull, 失败不影响启动 lnk ────
$updateLnk = "$desktop\更新Bookworm.lnk"
$updateBat = Join-Path $BootDir "更新Bookworm.bat"
# 兼容旧名 (gen-launcher-bats 之前生成的)
if (-not (Test-Path $updateBat)) {
$oldUpdateBat = Join-Path $BootDir "更新并启动Bookworm.bat"
if (Test-Path $oldUpdateBat) { $updateBat = $oldUpdateBat }
}
Log-OK "桌面快捷方式已创建 (含 Bookworm 图标)"
if (Test-Path $updateBat) {
$u = $shell.CreateShortcut($updateLnk)
$u.TargetPath = $updateBat
$u.WorkingDirectory = $BootDir
$u.Description = "更新 Bookworm 配置 (git pull)"
if (Test-Path $iconPath) { $u.IconLocation = "$iconPath,0" }
$u.Save()
Log-OK "更新Bookworm.lnk 创建"
} else {
Log-Info "更新.bat 未找到, 跳过更新 lnk (启动 lnk 已就绪, 不影响使用)"
}
Log-OK "v3.0.11 桌面快捷方式架构 (1 跳直调) 创建完成"
} catch { Log-Warn "快捷方式创建失败: $_" }
}

View File

@ -172,39 +172,74 @@ function Install-MissingDeps {
# ─── 桌面快捷方式 ────────────────────────────────────
function New-DesktopShortcuts {
# v3.0.11 架构重构: .lnk 直调 pwsh + claude.ps1 绝对路径 (1 跳直链)
$desktop = [System.Environment]::GetFolderPath("Desktop")
$bootDir = $ScriptDir
# 步骤 1: 确保新「启动Bookworm.lnk」存在 (缺失时创建)
$lnkPath = Join-Path $desktop "启动Bookworm.lnk"
if (-not (Test-Path $lnkPath)) {
# 定位 pwsh.exe
$pwshExe = (Get-Command pwsh -ErrorAction SilentlyContinue).Source
if (-not $pwshExe) {
foreach ($p in @("$env:ProgramFiles\PowerShell\7\pwsh.exe", "${env:ProgramFiles(x86)}\PowerShell\7\pwsh.exe")) {
if (Test-Path $p) { $pwshExe = $p; break }
}
}
if (-not $pwshExe) {
Write-Host " [!] pwsh.exe 未找到, 跳过桌面快捷方式 (建议先装 PS7)" -ForegroundColor Yellow
return
}
# 定位 claude.ps1
$claudePs1 = $null
$claudeCmd = Get-Command claude -ErrorAction SilentlyContinue
if ($claudeCmd -and $claudeCmd.Source -and $claudeCmd.Source.EndsWith('.ps1') -and (Test-Path $claudeCmd.Source)) {
$claudePs1 = $claudeCmd.Source
}
if (-not $claudePs1) {
try {
$npmPrefix = (& npm config get prefix 2>$null | Select-Object -First 1).Trim()
$candidate = Join-Path $npmPrefix "claude.ps1"
if (Test-Path $candidate) { $claudePs1 = $candidate }
} catch {}
}
if (-not $claudePs1) {
foreach ($p in @("$env:APPDATA\npm\claude.ps1", "$env:ProgramFiles\nodejs\claude.ps1")) {
if (Test-Path $p) { $claudePs1 = $p; break }
}
}
if (-not $claudePs1) {
Write-Host " [!] claude.ps1 未找到, 跳过桌面快捷方式 (Claude Code 装好后重跑可补)" -ForegroundColor Yellow
return
}
try {
$shell = New-Object -ComObject WScript.Shell
$shortcut = $shell.CreateShortcut($lnkPath)
$scriptPath = Join-Path $bootDir "install.ps1"
$hasPwsh = [bool](Get-Command pwsh -ErrorAction SilentlyContinue)
$psExe = if ($hasPwsh) { (Get-Command pwsh).Source } else { "powershell.exe" }
$shortcut.TargetPath = $psExe
$shortcut.Arguments = "-NoLogo -ExecutionPolicy Bypass -Command `"Set-Item Env:NO_PROXY 'bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1'; & '$scriptPath' -StartOnly -AutoAccept`""
$shortcut.WorkingDirectory = $bootDir
$shortcut.Description = "Bookworm Smart Assistant"
$shortcut.TargetPath = $pwshExe
$shortcut.Arguments = "-NoLogo -NoExit -File `"$claudePs1`" --dangerously-skip-permissions"
$shortcut.WorkingDirectory = $env:USERPROFILE
$shortcut.Description = "Bookworm Smart Assistant (v3.0.11 直调)"
$iconPath = Join-Path $bootDir "bookworm-desktop.ico"
if (-not (Test-Path $iconPath)) { $iconPath = Join-Path $bootDir "bookworm.ico" }
if (Test-Path $iconPath) { $shortcut.IconLocation = "$iconPath,0" }
$shortcut.Save()
$psVer = if ($hasPwsh) { "PowerShell 7" } else { "PowerShell 5.1" }
Write-Host " [OK] 桌面快捷方式已创建: 启动Bookworm ($psVer)" -ForegroundColor Green
} catch {
Write-Host " [!] 桌面快捷方式创建失败 (不影响使用)" -ForegroundColor Gray
# 自验证
$verify = $shell.CreateShortcut($lnkPath)
if ($verify.TargetPath -eq $pwshExe -and $verify.Arguments -match [regex]::Escape($claudePs1)) {
Write-Host " [OK] 桌面快捷方式已创建: 启动Bookworm.lnk → pwsh + claude.ps1" -ForegroundColor Green
} else {
Write-Host " [!] 桌面快捷方式自验证失败" -ForegroundColor Yellow
Remove-Item $lnkPath -Force -EA SilentlyContinue
}
} catch {
Write-Host " [!] 桌面快捷方式创建失败: $_" -ForegroundColor Gray
}
# 步骤 2: 迁移清理老 Bookworm.lnk (v3.0.3 及以前命名), 必须在新 lnk 确认存在后, 防空窗
# 迁移清理老 Bookworm.lnk
$oldLnk = Join-Path $desktop "Bookworm.lnk"
if ((Test-Path $oldLnk) -and (Test-Path $lnkPath)) {
try {
Remove-Item -LiteralPath $oldLnk -Force -ErrorAction Stop
Write-Host " [MIGRATE] 已清理旧快捷方式 Bookworm.lnk (已替换为启动Bookworm)" -ForegroundColor DarkGray
} catch {
Write-Host " [!] 无法清理旧 Bookworm.lnk (不影响使用)" -ForegroundColor Gray
}
try { Remove-Item -LiteralPath $oldLnk -Force -ErrorAction Stop } catch {}
}
}

File diff suppressed because one or more lines are too long

33
更新Bookworm.bat Normal file
View File

@ -0,0 +1,33 @@
@echo off
chcp 65001 > nul
cd /d "%~dp0"
:: v3.0.11 架构重构: 更新 .bat 仅做 git pull, 不启动 claude.
:: 启动 claude 由独立的 启动Bookworm.lnk → pwsh + claude.ps1 完成 (1 跳直链)
:: 这样 git pull 失败也不会拖累启动, 解耦关键
echo.
echo Bookworm 配置同步
echo ============================================
echo.
:: 同步 bookworm-boot 仓库 (本目录)
echo [1/2] 同步启动器目录 (bookworm-boot)...
git pull --rebase 2>&1
if %errorlevel% neq 0 (
echo [!] bookworm-boot git pull 失败 ^(不影响启动 lnk^)
)
:: 同步 ~/.claude 配置仓库 (Skill/hook/agents)
echo.
echo [2/2] 同步 Claude 配置 (.claude/)...
git -C "%USERPROFILE%\.claude" pull --rebase 2>&1
if %errorlevel% neq 0 (
echo [!] .claude git pull 失败 ^(不影响启动 lnk^)
)
echo.
echo ============================================
echo 完成. 双击桌面「启动Bookworm」启动 Claude.
echo ============================================
echo.
pause

File diff suppressed because one or more lines are too long