feat(v3.1.0): wrapper 模式 + 双 profile + 凭证注入失败弹窗
闭合 v3.0.11 已知 CRITICAL 局限 (3/4):
[L4] claude.ps1 路径迁移破坏 .lnk → 引入 wrapper bw-launch.ps1
桌面 .lnk Args 不再 bake claude.ps1 路径, 改指向 $BootDir\bw-launch.ps1
wrapper 启动时动态查 claude.ps1 (PATH 重载 + npm prefix + 硬编码兜底)
失败时写 bw-launch.log + GUI MessageBox 弹清晰诊断
好处: claude.ps1 路径变 → wrapper 自动重新查, .lnk 永不 stale
[L2] PS5.1-only 启动 profile BW_CRED 不加载 → 双 profile 注入
Inject-CredentialLoaderProfile 同时写:
Documents\PowerShell\profile.ps1 (PS7 主用)
Documents\WindowsPowerShell\profile.ps1 (PS5.1 兜底)
任一成功即 OK, 双方都失败才 throw
[L3] profile 注入失败 fail soft 不可见 → catch 弹窗
Show-MsgBox Warning 含影响 + 异常原文 + 3 种手动修复方案
不阻断主安装, Phase 5+ 继续
[L1] DPAPI 跨用户 (设计内禀, 文档化) — 未闭合, 用户换电脑/换 Win 用户重跑 EXE 重激活
新文件: bw-launch.ps1 (4472 字节, 启动 wrapper)
EXE 228352 → 231424 bytes (+3072)
向后兼容: v3.0.11 老 .lnk 直调 claude.ps1 仍能跑, 重装升级到 wrapper
This commit is contained in:
parent
4ad048040e
commit
e225a5c758
157
auto-setup.ps1
157
auto-setup.ps1
@ -48,7 +48,7 @@ trap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ─── 版本号 (每次更新递增, build.ps1 自动读取) ──────
|
# ─── 版本号 (每次更新递增, build.ps1 自动读取) ──────
|
||||||
$BWVersion = "3.0.11" # 架构重构: 桌面 .lnk 直调 pwsh+claude.ps1 (1跳直链), 凭证迁移 profile sentinel, 启动/更新拆分
|
$BWVersion = "3.1.0" # 健壮性: bw-launch wrapper (闭合 stale 路径) + PS5.1 双 profile + 凭证注入失败弹窗
|
||||||
|
|
||||||
# DryRun 模式日志标记
|
# DryRun 模式日志标记
|
||||||
if ($DryRun) { $global:BWDryRun = $DryRun } else { $global:BWDryRun = $null }
|
if ($DryRun) { $global:BWDryRun = $DryRun } else { $global:BWDryRun = $null }
|
||||||
@ -742,27 +742,40 @@ function Save-SecretsToCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Set-ItemProperty $regPath -Name "_expiry" -Value (Get-Date).Date.AddDays(1).ToUniversalTime().ToString("o") -Force
|
Set-ItemProperty $regPath -Name "_expiry" -Value (Get-Date).Date.AddDays(1).ToUniversalTime().ToString("o") -Force
|
||||||
# v3.0.11: 同步注入 profile.ps1 sentinel 块 (pwsh 启动时自动加载凭证, 与启动链路解耦)
|
# v3.0.11/v3.1.0: 同步注入 profile.ps1 sentinel 块
|
||||||
try { Inject-CredentialLoaderProfile } catch { Bw-Log "WARN" "profile 凭证块注入失败: $_" }
|
# v3.1.0 双 profile 注入 (PS7 + PS5.1), 失败显式弹窗 (闭合 L2/L3)
|
||||||
|
try { Inject-CredentialLoaderProfile }
|
||||||
|
catch {
|
||||||
|
$errMsg = $_.Exception.Message
|
||||||
|
Bw-Log "WARN" "profile 凭证块注入失败: $errMsg"
|
||||||
|
try {
|
||||||
|
$hint = "PowerShell profile 凭证块注入失败 (不阻断 v3.1.0 安装).`n`n"
|
||||||
|
$hint += "影响: 桌面快捷方式启动 Claude 时, 可能读不到 ANTHROPIC_API_KEY 凭证.`n`n"
|
||||||
|
$hint += "异常: $errMsg`n`n"
|
||||||
|
$hint += "修复方案 (任选一):`n"
|
||||||
|
$hint += " 方案 A (推荐): 重跑 Bookworm-Setup.exe, 凭证缓存有效会自动重试`n"
|
||||||
|
$hint += " 方案 B: 手动跑命令在 PowerShell 7 里:`n"
|
||||||
|
$hint += " pwsh -NoProfile -Command `"Inject-CredentialLoaderProfile`"`n"
|
||||||
|
$hint += " 方案 C: 启动 Claude 前手动 setx ANTHROPIC_API_KEY <你的Key>"
|
||||||
|
Show-MsgBox $hint "v3.1.0 profile 凭证注入失败 (可继续安装)" "OK" "Warning"
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
# v3.0.11 ADR-002: DPAPI 凭证加载迁移到 PowerShell profile
|
# v3.0.11/v3.1.0 ADR-002: DPAPI 凭证加载迁移到 PowerShell profile
|
||||||
# 桌面 .lnk 直调 pwsh + claude.ps1 后, 凭证由 pwsh profile 启动 hook 自动加载,
|
# 桌面 .lnk 直调 pwsh/wrapper 后, 凭证由 profile 启动 hook 自动加载, 完全脱离 bat/Base64.
|
||||||
# 完全脱离 bat/Base64 链路. profile 编辑用 String.Replace 字面替换 (不走 -replace
|
# v3.1.0 双 profile 注入 (PS7 + PS5.1), 闭合 L2 局限.
|
||||||
# 的 $ backreference 语义, 避免 v3.0.5 BW_CLIP 块踩过的同类 bug)
|
# profile 编辑用 String.Replace 字面替换 (不走 -replace 的 $ backreference 语义,
|
||||||
|
# 避免 v3.0.5 BW_CLIP 块踩过的同类 bug)
|
||||||
function Inject-CredentialLoaderProfile {
|
function Inject-CredentialLoaderProfile {
|
||||||
$psProfileDir = Join-Path $env:USERPROFILE "Documents\PowerShell"
|
$sentinelStart = "# BW_CRED_START v3.1.0 — 不要手动修改 (Bookworm 启动凭证 DPAPI 自动加载)"
|
||||||
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"
|
$sentinelEnd = "# BW_CRED_END"
|
||||||
|
|
||||||
# 注: here-string 内 PS 变量用反引号 ` 转义保留为字面量, 由 profile 加载时再求值
|
# 注: here-string 内 PS 变量用反引号 ` 转义保留为字面量, 由 profile 加载时再求值
|
||||||
$block = @"
|
$block = @"
|
||||||
$sentinelStart
|
$sentinelStart
|
||||||
# pwsh 启动时自动从 HKCU:\Software\Bookworm\CachedEnv 解密凭证
|
# pwsh / PS5.1 启动时自动从 HKCU:\Software\Bookworm\CachedEnv 解密凭证
|
||||||
# 写入 Process 作用域 env, claude.ps1 启动后能直接读到 ANTHROPIC_API_KEY 等
|
# 写入 Process 作用域 env, claude.ps1 启动后能直接读到 ANTHROPIC_API_KEY 等
|
||||||
try {
|
try {
|
||||||
`$bwReg = 'HKCU:\Software\Bookworm\CachedEnv'
|
`$bwReg = 'HKCU:\Software\Bookworm\CachedEnv'
|
||||||
@ -785,18 +798,49 @@ try {
|
|||||||
$sentinelEnd
|
$sentinelEnd
|
||||||
"@
|
"@
|
||||||
|
|
||||||
$existing = if (Test-Path $psProfilePath) { Get-Content $psProfilePath -Raw -Encoding UTF8 } else { "" }
|
# v3.1.0: 双 profile 路径 (PS7 主用, PS5.1 兜底)
|
||||||
$pattern = [regex]::Escape($sentinelStart) + "[\s\S]*?" + [regex]::Escape($sentinelEnd)
|
$profileTargets = @(
|
||||||
$regex = [regex]::new($pattern)
|
@{ Name = "PS7"; Dir = (Join-Path $env:USERPROFILE "Documents\PowerShell"); File = "profile.ps1" }
|
||||||
$match = $regex.Match($existing)
|
@{ Name = "PS5.1"; Dir = (Join-Path $env:USERPROFILE "Documents\WindowsPowerShell"); File = "profile.ps1" }
|
||||||
if ($match.Success) {
|
)
|
||||||
# 字面替换 — 绕过 -replace 的 $ backreference 语义
|
|
||||||
$updated = $existing.Replace($match.Value, $block)
|
$injected = @()
|
||||||
} else {
|
$failed = @()
|
||||||
$updated = if ($existing) { $existing.TrimEnd() + "`n`n" + $block + "`n" } else { $block + "`n" }
|
foreach ($t in $profileTargets) {
|
||||||
|
try {
|
||||||
|
if (-not (Test-Path $t.Dir)) { New-Item -ItemType Directory -Path $t.Dir -Force -EA Stop | Out-Null }
|
||||||
|
$psProfilePath = Join-Path $t.Dir $t.File
|
||||||
|
$existing = if (Test-Path $psProfilePath) { Get-Content $psProfilePath -Raw -Encoding UTF8 } else { "" }
|
||||||
|
# 同时清理 v3.0.11 旧 sentinel (BW_CRED_START v3.0.11 ...)
|
||||||
|
$oldPattern = "# BW_CRED_START v3\.0\.\d+[\s\S]*?# BW_CRED_END"
|
||||||
|
$existing = [regex]::Replace($existing, $oldPattern, "")
|
||||||
|
# 替换/追加 v3.1.0 块
|
||||||
|
$pattern = [regex]::Escape($sentinelStart) + "[\s\S]*?" + [regex]::Escape($sentinelEnd)
|
||||||
|
$regex = [regex]::new($pattern)
|
||||||
|
$match = $regex.Match($existing)
|
||||||
|
if ($match.Success) {
|
||||||
|
$updated = $existing.Replace($match.Value, $block)
|
||||||
|
} else {
|
||||||
|
$updated = if ($existing.Trim()) { $existing.TrimEnd() + "`n`n" + $block + "`n" } else { $block + "`n" }
|
||||||
|
}
|
||||||
|
[System.IO.File]::WriteAllText($psProfilePath, $updated, [System.Text.UTF8Encoding]::new($false))
|
||||||
|
$injected += "$($t.Name) ($psProfilePath)"
|
||||||
|
} catch {
|
||||||
|
$failed += "$($t.Name): $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($injected.Count -gt 0) {
|
||||||
|
Bw-Log "OK" "BW_CRED v3.1.0 块已注入 $($injected.Count) 个 profile: $($injected -join '; ')"
|
||||||
|
}
|
||||||
|
if ($failed.Count -gt 0) {
|
||||||
|
# 双 profile 至少一个成功不算失败 (PS7 默认场景写到一个就够)
|
||||||
|
Bw-Log "WARN" "部分 profile 注入失败: $($failed -join '; ')"
|
||||||
|
if ($injected.Count -eq 0) {
|
||||||
|
# 全失败才 throw → 由调用方 catch 弹窗
|
||||||
|
throw "所有 profile 注入失败: $($failed -join '; ')"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
[System.IO.File]::WriteAllText($psProfilePath, $updated, [System.Text.UTF8Encoding]::new($false))
|
|
||||||
Bw-Log "OK" "BW_CRED 块已注入 PowerShell profile: $psProfilePath"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ─── 桌面快捷方式 ──────────────────────────────────
|
# ─── 桌面快捷方式 ──────────────────────────────────
|
||||||
@ -823,34 +867,42 @@ function New-DesktopShortcuts {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── 2. 定位 claude.ps1 绝对路径 ───────────────────────
|
# ── 2. 定位 bw-launch.ps1 wrapper (v3.1.0 ADR) ───────
|
||||||
# 优先 Get-Command claude (运行时 PATH 里的)
|
# v3.1.0 改为 wrapper 模式: .lnk 调 bw-launch.ps1, wrapper 启动时动态查 claude.ps1
|
||||||
$claudePs1 = $null
|
# 闭合 L4 (claude.ps1 路径迁移破坏 .lnk).
|
||||||
$claudeCmd = Get-Command claude -ErrorAction SilentlyContinue
|
# bw-launch.ps1 由本 EXE 自带 (Phase 7 复制到 $BootDir), 路径稳定.
|
||||||
if ($claudeCmd) {
|
$bwLaunchPs1 = Join-Path $BootDir "bw-launch.ps1"
|
||||||
$src = $claudeCmd.Source
|
if (-not (Test-Path $bwLaunchPs1)) {
|
||||||
if ($src -and $src.EndsWith('.ps1') -and (Test-Path $src)) { $claudePs1 = $src }
|
# 兜底 1: 从本进程同目录拿 (开发场景)
|
||||||
}
|
$devCandidate = Join-Path (Split-Path -Parent $PSCommandPath) "bw-launch.ps1"
|
||||||
# 兜底 npm config get prefix
|
if ($devCandidate -and (Test-Path $devCandidate)) {
|
||||||
if (-not $claudePs1) {
|
Copy-Item $devCandidate $bwLaunchPs1 -Force -EA SilentlyContinue
|
||||||
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)) {
|
# 兜底 2: 从 bookworm-boot git 仓库 (clone 时已自带)
|
||||||
Log-Fail "找不到 claude.ps1 绝对路径, 拒绝创建桌面快捷方式"
|
if (-not (Test-Path $bwLaunchPs1)) {
|
||||||
Show-MsgBox "无法定位 claude.ps1 文件.`n`nClaude Code 可能未正确安装. 请先解决 Claude Code 安装问题 (npm i -g @anthropic-ai/claude-code) 再重跑安装器." "v3.0.11 桌面快捷方式创建失败" "OK" "Error"
|
Log-Fail "bw-launch.ps1 wrapper 缺失, 拒绝创建桌面快捷方式"
|
||||||
|
Show-MsgBox "Bookworm 启动 wrapper (bw-launch.ps1) 未找到.`n`n这表示 bookworm-boot git 仓库内容不完整, 请重跑 Bookworm-Setup.exe 让 Phase 3 重新克隆仓库." "v3.1.0 wrapper 缺失" "OK" "Error"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Log-OK "v3.0.11 启动路径已锁定: pwsh=$pwshExe; claude.ps1=$claudePs1"
|
|
||||||
|
# 兼容性自验证: 也确认 claude.ps1 当前可达 (装机时校验, 运行时由 wrapper 兜底)
|
||||||
|
$claudePs1Check = $null
|
||||||
|
try {
|
||||||
|
$cc = Get-Command claude -ErrorAction SilentlyContinue
|
||||||
|
if ($cc -and $cc.Source -and $cc.Source.EndsWith('.ps1') -and (Test-Path $cc.Source)) { $claudePs1Check = $cc.Source }
|
||||||
|
if (-not $claudePs1Check) {
|
||||||
|
$npmPrefix = (& npm config get prefix 2>$null | Select-Object -First 1).Trim()
|
||||||
|
$cand = Join-Path $npmPrefix "claude.ps1"
|
||||||
|
if (Test-Path $cand) { $claudePs1Check = $cand }
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
if (-not $claudePs1Check) {
|
||||||
|
Log-Fail "claude.ps1 装机时不可达, 拒绝创建桌面快捷方式 (即便 wrapper 也无法启动)"
|
||||||
|
Show-MsgBox "Claude Code 安装可能失败 (claude.ps1 不可达).`n`n请先 npm i -g @anthropic-ai/claude-code 验证安装, 再重跑 Bookworm-Setup.exe" "v3.1.0 Claude 安装异常" "OK" "Error"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log-OK "v3.1.0 启动路径已锁定: pwsh=$pwshExe; wrapper=$bwLaunchPs1; claude.ps1=$claudePs1Check"
|
||||||
|
|
||||||
# ── 3. 桌面图标 ───────────────────────────────────────
|
# ── 3. 桌面图标 ───────────────────────────────────────
|
||||||
$iconPath = Join-Path $BootDir "bookworm-desktop.ico"
|
$iconPath = Join-Path $BootDir "bookworm-desktop.ico"
|
||||||
@ -860,10 +912,11 @@ function New-DesktopShortcuts {
|
|||||||
$newLnk = "$desktop\启动Bookworm.lnk"
|
$newLnk = "$desktop\启动Bookworm.lnk"
|
||||||
$shortcut = $shell.CreateShortcut($newLnk)
|
$shortcut = $shell.CreateShortcut($newLnk)
|
||||||
$shortcut.TargetPath = $pwshExe
|
$shortcut.TargetPath = $pwshExe
|
||||||
# v3.0.11 P0 修复: 显式 -ExecutionPolicy Bypass 防 LTSC/组策略 AllSigned 拒绝未签名 npm shim
|
# v3.1.0: .lnk 调 wrapper, wrapper 内动态查 claude.ps1, 闭合 L4 stale 路径问题
|
||||||
$shortcut.Arguments = "-NoLogo -NoExit -ExecutionPolicy Bypass -File `"$claudePs1`" --dangerously-skip-permissions"
|
# -ExecutionPolicy Bypass 防 LTSC/组策略 AllSigned 拒绝未签名脚本
|
||||||
|
$shortcut.Arguments = "-NoLogo -NoExit -ExecutionPolicy Bypass -File `"$bwLaunchPs1`" --dangerously-skip-permissions"
|
||||||
$shortcut.WorkingDirectory = $env:USERPROFILE
|
$shortcut.WorkingDirectory = $env:USERPROFILE
|
||||||
$shortcut.Description = "Bookworm Smart Assistant (v3.0.11 直调架构)"
|
$shortcut.Description = "Bookworm Smart Assistant (v3.1.0 wrapper 架构)"
|
||||||
if (Test-Path $iconPath) { $shortcut.IconLocation = "$iconPath,0" }
|
if (Test-Path $iconPath) { $shortcut.IconLocation = "$iconPath,0" }
|
||||||
$shortcut.Save()
|
$shortcut.Save()
|
||||||
|
|
||||||
@ -871,7 +924,7 @@ function New-DesktopShortcuts {
|
|||||||
$verify = $shell.CreateShortcut($newLnk)
|
$verify = $shell.CreateShortcut($newLnk)
|
||||||
$checks = @{
|
$checks = @{
|
||||||
"TargetPath = pwshExe" = ($verify.TargetPath -eq $pwshExe)
|
"TargetPath = pwshExe" = ($verify.TargetPath -eq $pwshExe)
|
||||||
"Arguments 含 claude.ps1 字面路径" = ($verify.Arguments -match [regex]::Escape("`"$claudePs1`""))
|
"Arguments 含 bw-launch.ps1 字面路径" = ($verify.Arguments -match [regex]::Escape("`"$bwLaunchPs1`""))
|
||||||
"Arguments 含 --dangerously-skip-perms" = ($verify.Arguments -match "--dangerously-skip-permissions")
|
"Arguments 含 --dangerously-skip-perms" = ($verify.Arguments -match "--dangerously-skip-permissions")
|
||||||
"Arguments 含 -ExecutionPolicy Bypass" = ($verify.Arguments -match "-ExecutionPolicy Bypass")
|
"Arguments 含 -ExecutionPolicy Bypass" = ($verify.Arguments -match "-ExecutionPolicy Bypass")
|
||||||
}
|
}
|
||||||
|
|||||||
133
bw-launch.ps1
Normal file
133
bw-launch.ps1
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Bookworm 启动 wrapper (v3.1.0 引入)
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
桌面 .lnk 调用此 wrapper 而非直调 claude.ps1, 让启动逻辑可热修复 (改 wrapper 不需重 bake .lnk).
|
||||||
|
|
||||||
|
职责:
|
||||||
|
1. 动态查找 claude.ps1 真实路径 (npm config get prefix → 兜底候选)
|
||||||
|
2. claude.ps1 stale (npm uninstall/换版本/换 prefix) → 弹清晰 GUI 引导, 不再"快捷方式失效"
|
||||||
|
3. 失败时不静默, 写日志 + GUI 弹窗
|
||||||
|
|
||||||
|
与 .lnk 的契约:
|
||||||
|
.lnk Args = -NoLogo -NoExit -ExecutionPolicy Bypass -File "<bw-launch.ps1 绝对路径>"
|
||||||
|
--dangerously-skip-permissions
|
||||||
|
$args[0..N] 转发给 claude.ps1
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
v3.1.0 (2026-04-25) — 引入 wrapper 模式 (闭合 L4 局限: bake claude.ps1 路径 stale)
|
||||||
|
分发: Phase 7 安装时复制到 $BootDir\bw-launch.ps1
|
||||||
|
#>
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Continue"
|
||||||
|
$bwLaunchLog = Join-Path $env:TEMP "bw-launch.log"
|
||||||
|
|
||||||
|
function Write-BwLaunchLog {
|
||||||
|
param([string]$Level, [string]$Msg)
|
||||||
|
try {
|
||||||
|
$line = "[$([DateTime]::Now.ToString('yyyy-MM-dd HH:mm:ss'))] [$Level] $Msg"
|
||||||
|
$line | Out-File -FilePath $bwLaunchLog -Append -Encoding UTF8 -EA SilentlyContinue
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Show-LaunchError {
|
||||||
|
param([string]$Title, [string]$Body)
|
||||||
|
Write-BwLaunchLog "ERROR" "$Title :: $Body"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " [!] $Title" -ForegroundColor Red
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host $Body -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " 日志: $bwLaunchLog" -ForegroundColor Gray
|
||||||
|
Write-Host ""
|
||||||
|
try {
|
||||||
|
Add-Type -AssemblyName System.Windows.Forms -EA Stop
|
||||||
|
[System.Windows.Forms.MessageBox]::Show("$Body`n`n详情见: $bwLaunchLog", "Bookworm 启动失败 — $Title", 'OK', 'Error') | Out-Null
|
||||||
|
} catch {}
|
||||||
|
Read-Host "按回车关闭"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-BwLaunchLog "INFO" "bw-launch wrapper 启动 args=$($args -join ' ')"
|
||||||
|
|
||||||
|
# ── 1. PATH 三层重载 (即便桌面 .lnk 不依赖 PATH, 子 claude.ps1 调 node 仍依赖) ──
|
||||||
|
$env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User')
|
||||||
|
|
||||||
|
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 {
|
||||||
|
Write-BwLaunchLog "WARN" "npm config get prefix 失败: $_"
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($p in @("$env:APPDATA\npm", "$env:ProgramFiles\nodejs", "$env:LOCALAPPDATA\npm")) {
|
||||||
|
if ((Test-Path $p) -and ($env:Path -notlike "*$p*")) {
|
||||||
|
$env:Path = "$p;$env:Path"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 2. 动态定位 claude.ps1 ──
|
||||||
|
$claudePs1 = $null
|
||||||
|
|
||||||
|
# 优先 Get-Command (PATH 已重载)
|
||||||
|
$claudeCmd = Get-Command claude -ErrorAction SilentlyContinue
|
||||||
|
if ($claudeCmd -and $claudeCmd.Source -and $claudeCmd.Source.EndsWith('.ps1') -and (Test-Path $claudeCmd.Source)) {
|
||||||
|
$claudePs1 = $claudeCmd.Source
|
||||||
|
Write-BwLaunchLog "INFO" "claude.ps1 from Get-Command: $claudePs1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 兜底 npm config get prefix
|
||||||
|
if (-not $claudePs1) {
|
||||||
|
try {
|
||||||
|
$candidate = Join-Path $npmPrefix "claude.ps1"
|
||||||
|
if (Test-Path $candidate) {
|
||||||
|
$claudePs1 = $candidate
|
||||||
|
Write-BwLaunchLog "INFO" "claude.ps1 from npm prefix: $claudePs1"
|
||||||
|
}
|
||||||
|
} 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
|
||||||
|
Write-BwLaunchLog "INFO" "claude.ps1 from hardcoded: $claudePs1"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $claudePs1 -or -not (Test-Path $claudePs1)) {
|
||||||
|
$diag = "未找到 claude.ps1 文件.`n`n"
|
||||||
|
$diag += "诊断信息:`n"
|
||||||
|
$diag += " npm config get prefix: $(if ($npmPrefix) { $npmPrefix } else { '(查询失败)' })`n"
|
||||||
|
$diag += " PATH 中 npm/nodejs 片段:`n"
|
||||||
|
foreach ($p in (($env:Path -split ';') | Where-Object { $_ -match 'npm|nodejs' })) {
|
||||||
|
$diag += " $p`n"
|
||||||
|
}
|
||||||
|
$diag += "`n修复方案:`n"
|
||||||
|
$diag += " 方案 A: 命令行运行 npm i -g @anthropic-ai/claude-code`n"
|
||||||
|
$diag += " 方案 B: 重新双击 Bookworm-Setup.exe 修复安装"
|
||||||
|
Show-LaunchError "Claude Code 未找到" $diag
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 3. 启动 claude.ps1 + 转发 $args (--dangerously-skip-permissions 等) ──
|
||||||
|
Write-BwLaunchLog "INFO" "调用 claude.ps1 args=$($args -join ' ')"
|
||||||
|
try {
|
||||||
|
& $claudePs1 @args
|
||||||
|
$exitCode = $LASTEXITCODE
|
||||||
|
Write-BwLaunchLog "INFO" "claude.ps1 退出码: $exitCode"
|
||||||
|
if ($exitCode -ne 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " [!] Claude 进程退出码: $exitCode" -ForegroundColor Yellow
|
||||||
|
Write-Host " 日志: $bwLaunchLog" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
exit $exitCode
|
||||||
|
} catch {
|
||||||
|
Show-LaunchError "Claude 启动异常" "异常信息: $($_.Exception.Message)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
40
install.ps1
40
install.ps1
@ -189,26 +189,27 @@ function New-DesktopShortcuts {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
# 定位 claude.ps1
|
# v3.1.0: 优先用 wrapper bw-launch.ps1, 闭合 claude.ps1 路径 stale (L4)
|
||||||
$claudePs1 = $null
|
$bwLaunchPs1 = Join-Path $bootDir "bw-launch.ps1"
|
||||||
$claudeCmd = Get-Command claude -ErrorAction SilentlyContinue
|
if (-not (Test-Path $bwLaunchPs1)) {
|
||||||
if ($claudeCmd -and $claudeCmd.Source -and $claudeCmd.Source.EndsWith('.ps1') -and (Test-Path $claudeCmd.Source)) {
|
Write-Host " [!] bw-launch.ps1 wrapper 未找到, 跳过桌面快捷方式" -ForegroundColor Yellow
|
||||||
$claudePs1 = $claudeCmd.Source
|
Write-Host " $bwLaunchPs1 应由 bookworm-boot git 仓库提供" -ForegroundColor Gray
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if (-not $claudePs1) {
|
|
||||||
|
# 装机时自检 claude.ps1 当前可达 (运行时由 wrapper 兜底)
|
||||||
|
$claudeCheck = $null
|
||||||
|
$claudeCmd = Get-Command claude -ErrorAction SilentlyContinue
|
||||||
|
if ($claudeCmd -and $claudeCmd.Source -and $claudeCmd.Source.EndsWith('.ps1')) { $claudeCheck = $claudeCmd.Source }
|
||||||
|
if (-not $claudeCheck) {
|
||||||
try {
|
try {
|
||||||
$npmPrefix = (& npm config get prefix 2>$null | Select-Object -First 1).Trim()
|
$npmPrefix = (& npm config get prefix 2>$null | Select-Object -First 1).Trim()
|
||||||
$candidate = Join-Path $npmPrefix "claude.ps1"
|
$cand = Join-Path $npmPrefix "claude.ps1"
|
||||||
if (Test-Path $candidate) { $claudePs1 = $candidate }
|
if (Test-Path $cand) { $claudeCheck = $cand }
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
if (-not $claudePs1) {
|
if (-not $claudeCheck) {
|
||||||
foreach ($p in @("$env:APPDATA\npm\claude.ps1", "$env:ProgramFiles\nodejs\claude.ps1")) {
|
Write-Host " [!] claude.ps1 装机时不可达, 跳过桌面快捷方式" -ForegroundColor Yellow
|
||||||
if (Test-Path $p) { $claudePs1 = $p; break }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (-not $claudePs1) {
|
|
||||||
Write-Host " [!] claude.ps1 未找到, 跳过桌面快捷方式 (Claude Code 装好后重跑可补)" -ForegroundColor Yellow
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,10 +217,9 @@ function New-DesktopShortcuts {
|
|||||||
$shell = New-Object -ComObject WScript.Shell
|
$shell = New-Object -ComObject WScript.Shell
|
||||||
$shortcut = $shell.CreateShortcut($lnkPath)
|
$shortcut = $shell.CreateShortcut($lnkPath)
|
||||||
$shortcut.TargetPath = $pwshExe
|
$shortcut.TargetPath = $pwshExe
|
||||||
# v3.0.11 P0 修复: -ExecutionPolicy Bypass 防 LTSC/AllSigned 拒绝未签名 npm shim
|
$shortcut.Arguments = "-NoLogo -NoExit -ExecutionPolicy Bypass -File `"$bwLaunchPs1`" --dangerously-skip-permissions"
|
||||||
$shortcut.Arguments = "-NoLogo -NoExit -ExecutionPolicy Bypass -File `"$claudePs1`" --dangerously-skip-permissions"
|
|
||||||
$shortcut.WorkingDirectory = $env:USERPROFILE
|
$shortcut.WorkingDirectory = $env:USERPROFILE
|
||||||
$shortcut.Description = "Bookworm Smart Assistant (v3.0.11 直调)"
|
$shortcut.Description = "Bookworm Smart Assistant (v3.1.0 wrapper)"
|
||||||
$iconPath = Join-Path $bootDir "bookworm-desktop.ico"
|
$iconPath = Join-Path $bootDir "bookworm-desktop.ico"
|
||||||
if (-not (Test-Path $iconPath)) { $iconPath = Join-Path $bootDir "bookworm.ico" }
|
if (-not (Test-Path $iconPath)) { $iconPath = Join-Path $bootDir "bookworm.ico" }
|
||||||
if (Test-Path $iconPath) { $shortcut.IconLocation = "$iconPath,0" }
|
if (Test-Path $iconPath) { $shortcut.IconLocation = "$iconPath,0" }
|
||||||
@ -228,11 +228,11 @@ function New-DesktopShortcuts {
|
|||||||
# 自验证 (4 项)
|
# 自验证 (4 项)
|
||||||
$verify = $shell.CreateShortcut($lnkPath)
|
$verify = $shell.CreateShortcut($lnkPath)
|
||||||
$okTarget = $verify.TargetPath -eq $pwshExe
|
$okTarget = $verify.TargetPath -eq $pwshExe
|
||||||
$okPath = $verify.Arguments -match [regex]::Escape($claudePs1)
|
$okPath = $verify.Arguments -match [regex]::Escape($bwLaunchPs1)
|
||||||
$okPerm = $verify.Arguments -match "--dangerously-skip-permissions"
|
$okPerm = $verify.Arguments -match "--dangerously-skip-permissions"
|
||||||
$okBypass = $verify.Arguments -match "-ExecutionPolicy Bypass"
|
$okBypass = $verify.Arguments -match "-ExecutionPolicy Bypass"
|
||||||
if ($okTarget -and $okPath -and $okPerm -and $okBypass) {
|
if ($okTarget -and $okPath -and $okPerm -and $okBypass) {
|
||||||
Write-Host " [OK] 桌面快捷方式已创建并通过 4 项自验证" -ForegroundColor Green
|
Write-Host " [OK] 桌面快捷方式已创建并通过 4 项自验证 (v3.1.0 wrapper)" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host " [!] 桌面快捷方式自验证失败 (Target=$okTarget Path=$okPath Perm=$okPerm Bypass=$okBypass)" -ForegroundColor Yellow
|
Write-Host " [!] 桌面快捷方式自验证失败 (Target=$okTarget Path=$okPath Perm=$okPerm Bypass=$okBypass)" -ForegroundColor Yellow
|
||||||
Remove-Item $lnkPath -Force -EA SilentlyContinue
|
Remove-Item $lnkPath -Force -EA SilentlyContinue
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user