diff --git a/Bookworm-Setup.exe b/Bookworm-Setup.exe index 6f3d84e..b767293 100644 Binary files a/Bookworm-Setup.exe and b/Bookworm-Setup.exe differ diff --git a/auto-setup.ps1 b/auto-setup.ps1 index afef95c..216c08a 100644 --- a/auto-setup.ps1 +++ b/auto-setup.ps1 @@ -3,7 +3,7 @@ Bookworm Portable - 全自动一键安装器 .DESCRIPTION 全新电脑从零到 Bookworm 完全就绪,最大程度自动化。 - 7 阶段: 环境检测 → 依赖安装 → 网络诊断 → 仓库克隆 → 凭证解密 → MCP 验证 → 启动 + 8 阶段: 环境检测 → 依赖安装 → 网络诊断 → 仓库克隆 → 凭证解密 → MCP 验证 → 环境加固 → OTA 基础设施 → 启动 需要人工输入时弹出 GUI 对话框。 .USAGE .\auto-setup.ps1 @@ -48,7 +48,7 @@ trap { } # ─── 版本号 (每次更新递增, build.ps1 自动读取) ────── -$BWVersion = "3.1.3" # hotfix: 卸载精准删除 (不删用户自有 ~/.claude) + 体检凭证链路检测修正 +$BWVersion = "3.2.0" # feat: Phase 8 OTA 自动更新基础设施 (pubkey/DPAPI凭证/bw-ota.ps1) # DryRun 模式日志标记 if ($DryRun) { $global:BWDryRun = $DryRun } else { $global:BWDryRun = $null } @@ -2548,6 +2548,109 @@ if (-not (Test-Cmd "claude")) { New-DesktopShortcuts } +# ======================================================================== +# Phase 8: OTA 自动更新基础设施 +# ======================================================================== +Log-Phase 8 "OTA 自动更新基础设施" + +try { + $otaDir = Join-Path $ClaudeDir '.bw-ota' + if (-not (Test-Path $otaDir)) { New-Item -ItemType Directory -Path $otaDir -Force | Out-Null } + + # 8a: 写入 Ed25519 签名公钥 (从主机 admin-private 导出, base64 硬编码) + $pubKeyB64 = 'MCowBQYDK2VwAyEAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXo=' + $pubKeyPemPath = Join-Path $otaDir 'signing-pubkey.pem' + $pubKeyContent = "-----BEGIN PUBLIC KEY-----`n$pubKeyB64`n-----END PUBLIC KEY-----`n" + # 检查是否需要从 admin-private 读取真实公钥 + $adminPub = Join-Path $env:USERPROFILE 'bookworm-admin-private\ed25519-sync.pub.pem' + if (Test-Path $adminPub) { + $pubKeyContent = Get-Content -Raw $adminPub + Log-OK "8a 签名公钥已从 admin-private 读取" + } else { + # 从 .claude/tools/ 导出包中的公钥读取 (Portable config 仓库已含) + $repoPub = Join-Path $ClaudeDir 'bw-signing-pubkey.pem' + if (Test-Path $repoPub) { + $pubKeyContent = Get-Content -Raw $repoPub + Log-OK "8a 签名公钥已从配置仓库读取" + } else { + Log-Warn "8a 签名公钥未找到, OTA 验签将不可用" + } + } + [IO.File]::WriteAllText($pubKeyPemPath, $pubKeyContent, [Text.UTF8Encoding]::new($false)) + + # 8b: DPAPI 加密 Gitea 凭证 (复用 Phase 3 已缓存的 credential manager) + $credFilePath = Join-Path $otaDir 'pull-cred.dpapi' + $giteaUser = $null; $giteaPass = $null + try { + $fillInput = "protocol=https`nhost=code.letcareme.com`n`n" + $fillOutput = $fillInput | git credential fill 2>$null + if ($fillOutput) { + foreach ($line in ($fillOutput -split "`n")) { + if ($line -match '^username=(.+)') { $giteaUser = $Matches[1].Trim() } + if ($line -match '^password=(.+)') { $giteaPass = $Matches[1].Trim() } + } + } + } catch {} + if ($giteaUser -and $giteaPass) { + Add-Type -AssemblyName System.Security + $credJson = @{ user = $giteaUser; pass = $giteaPass } | ConvertTo-Json -Compress + $plainBytes = [Text.Encoding]::UTF8.GetBytes($credJson) + $encrypted = [Security.Cryptography.ProtectedData]::Protect( + $plainBytes, + [Text.Encoding]::UTF8.GetBytes('bookworm-ota-salt'), + [Security.Cryptography.DataProtectionScope]::CurrentUser + ) + [IO.File]::WriteAllBytes($credFilePath, $encrypted) + Log-OK "8b Gitea 凭证已 DPAPI 加密存储" + } else { + Log-Warn "8b Gitea 凭证未找到, OTA 更新将不可用" + } + + # 8c: OTA 配置文件 + $otaConfig = @{ + repoUrl = 'https://code.letcareme.com/bookworm/bookworm-smart-assistant' + checkInterval = 86400 + lastCheck = 0 + autoUpdate = $false + channel = 'stable' + } | ConvertTo-Json -Depth 4 + $otaConfigPath = Join-Path $otaDir 'config.json' + [IO.File]::WriteAllText($otaConfigPath, $otaConfig, [Text.UTF8Encoding]::new($false)) + Log-OK "8c OTA 配置已写入" + + # 8d: 复制 bw-ota.ps1 (从安装器同目录或 BootDir) + $otaScript = Join-Path $otaDir 'bw-ota.ps1' + $otaSrc = $null + foreach ($candidate in @( + (Join-Path $ScriptDir 'bw-ota.ps1'), + (Join-Path $BootDir 'bw-ota.ps1') + )) { + if (Test-Path $candidate) { $otaSrc = $candidate; break } + } + if ($otaSrc) { + Copy-Item -Path $otaSrc -Destination $otaScript -Force + Log-OK "8d bw-ota.ps1 已部署到 $otaDir" + } else { + Log-Warn "8d bw-ota.ps1 未找到, OTA 功能不可用" + } + + # 8e: 写入 VERSION 文件 (首次安装基准) + $versionFile = Join-Path $ClaudeDir 'VERSION' + if (-not (Test-Path $versionFile)) { + $statsFile = Join-Path $ClaudeDir 'stats-compiled.json' + $ver = 'unknown' + if (Test-Path $statsFile) { + try { $ver = (Get-Content -Raw $statsFile | ConvertFrom-Json).version -replace '^v', '' } catch {} + } + [IO.File]::WriteAllText($versionFile, "$ver`n", [Text.UTF8Encoding]::new($false)) + Log-OK "8e VERSION 文件已写入: $ver" + } + + Log-OK "Phase 8 OTA 基础设施就绪" +} catch { + Bw-Log "WARN" "Phase 8 OTA 设置失败 (不影响主安装): $_" +} + if ($allOK -and $env:ANTHROPIC_API_KEY) { Bw-Log "DONE" "v$BWVersion 安装成功 ($skillCount Skills / $hookCount Hooks)" diff --git a/bw-ota.ps1 b/bw-ota.ps1 new file mode 100644 index 0000000..5e508bc --- /dev/null +++ b/bw-ota.ps1 @@ -0,0 +1,385 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Bookworm Portable OTA 更新检查器 (启动时自动调用) + +.DESCRIPTION + 轻量版同步: 检查远端版本 → 用户确认 → 增量同步 → 验签 → 原子替换. + 设计原则: fail-open (任何异常不阻断启动), 24h 冷却, 用户确认制. + + 与 bookworm-sync.ps1 的区别: + - Token 从 DPAPI 加密文件读取 (安装时写入) + - Pubkey 内嵌安装目录 + - 失败不阻断启动 + - 版本相同跳过 + +.NOTES + Author: Bookworm Admin + Version: 1.0.0 (2026-04-27) + License: Private +#> + +[CmdletBinding()] +param( + [switch]$Force, + [switch]$SkipConfirm, + [switch]$DryRun +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +$OtaDir = Join-Path $env:USERPROFILE '.claude\.bw-ota' +$ClaudeRoot = Join-Path $env:USERPROFILE '.claude' +$ConfigFile = Join-Path $OtaDir 'config.json' +$CredFile = Join-Path $OtaDir 'pull-cred.dpapi' +$PubKeyFile = Join-Path $OtaDir 'signing-pubkey.pem' + +# ========== 日志 ========== +function Write-Ota ($m, $c = 'Cyan') { Write-Host "[Bookworm OTA] $m" -ForegroundColor $c } +function Write-OtaOk ($m) { Write-Ota $m 'Green' } +function Write-OtaWarn ($m) { Write-Ota $m 'Yellow' } +function Write-OtaErr ($m) { Write-Ota $m 'Red' } + +# ========== OTA 基础设施检查 ========== +function Test-OtaReady { + if (-not (Test-Path $OtaDir)) { return $false } + if (-not (Test-Path $ConfigFile)) { return $false } + if (-not (Test-Path $CredFile)) { return $false } + if (-not (Test-Path $PubKeyFile)) { return $false } + return $true +} + +# ========== 配置读写 ========== +function Read-OtaConfig { + try { + $raw = Get-Content -Raw $ConfigFile + if ($raw.Length -gt 0 -and [int][char]$raw[0] -eq 0xFEFF) { $raw = $raw.Substring(1) } + return $raw | ConvertFrom-Json + } catch { + return $null + } +} + +function Save-OtaConfig ($cfg) { + $json = $cfg | ConvertTo-Json -Depth 4 + [IO.File]::WriteAllText($ConfigFile, $json, [Text.UTF8Encoding]::new($false)) +} + +# ========== 冷却检查 (24h) ========== +function Test-Cooldown ($cfg) { + if ($Force) { return $false } + $interval = if ($cfg.checkInterval) { $cfg.checkInterval } else { 86400 } + $lastCheck = if ($cfg.lastCheck) { $cfg.lastCheck } else { 0 } + $now = [int][DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + return ($now - $lastCheck) -lt $interval +} + +# ========== DPAPI 凭证解密 ========== +function Read-Credential { + try { + Add-Type -AssemblyName System.Security + $encrypted = [IO.File]::ReadAllBytes($CredFile) + $plain = [Security.Cryptography.ProtectedData]::Unprotect( + $encrypted, + [Text.Encoding]::UTF8.GetBytes('bookworm-ota-salt'), + [Security.Cryptography.DataProtectionScope]::CurrentUser + ) + $json = [Text.Encoding]::UTF8.GetString($plain) | ConvertFrom-Json + return $json + } catch { + return $null + } +} + +# ========== 语义版本比较 (major.minor.patch) ========== +function Compare-SemVer ($local, $remote) { + $lParts = ($local -replace '^v', '') -split '\.' | ForEach-Object { [int]$_ } + $rParts = ($remote -replace '^v', '') -split '\.' | ForEach-Object { [int]$_ } + for ($i = 0; $i -lt [Math]::Max($lParts.Count, $rParts.Count); $i++) { + $l = if ($i -lt $lParts.Count) { $lParts[$i] } else { 0 } + $r = if ($i -lt $rParts.Count) { $rParts[$i] } else { 0 } + if ($r -gt $l) { return 1 } + if ($r -lt $l) { return -1 } + } + return 0 +} + +# ========== 本地版本号 ========== +function Get-LocalVersion { + $versionFile = Join-Path $ClaudeRoot 'VERSION' + if (Test-Path $versionFile) { + return (Get-Content -Raw $versionFile).Trim() + } + $statsFile = Join-Path $ClaudeRoot 'stats-compiled.json' + if (Test-Path $statsFile) { + try { + $stats = Get-Content -Raw $statsFile | ConvertFrom-Json + return $stats.summary.version + } catch { } + } + return 'unknown' +} + +# ========== Basic Auth 头构造 ========== +function Get-AuthHeaders ($cred) { + $pair = "$($cred.user):$($cred.pass)" + $bytes = [Text.Encoding]::UTF8.GetBytes($pair) + $b64 = [Convert]::ToBase64String($bytes) + return @{ Authorization = "Basic $b64" } +} + +# ========== 远端版本查询 (Gitea raw, 3s 超时) ========== +function Get-RemoteVersion ($cred, $cfg) { + $repoUrl = if ($cfg.repoUrl) { $cfg.repoUrl } else { 'https://code.letcareme.com/bookworm/bookworm-smart-assistant' } + $apiUrl = "$repoUrl/raw/branch/main/VERSION" + try { + $headers = Get-AuthHeaders $cred + $resp = Invoke-WebRequest -Uri $apiUrl -Headers $headers -UseBasicParsing -TimeoutSec 3 + return $resp.Content.Trim() + } catch { + return $null + } +} + +# ========== 远端最新 tag 查询 (备用) ========== +function Get-RemoteLatestTag ($cred, $cfg) { + $repoUrl = if ($cfg.repoUrl) { $cfg.repoUrl } else { 'https://code.letcareme.com/bookworm/bookworm-smart-assistant' } + $host_ = ([Uri]$repoUrl).Host + $repoPath = ([Uri]$repoUrl).AbsolutePath.TrimStart('/') + $apiUrl = "https://$host_/api/v1/repos/$repoPath/tags?limit=1" + try { + $headers = Get-AuthHeaders $cred + $resp = Invoke-WebRequest -Uri $apiUrl -Headers $headers -UseBasicParsing -TimeoutSec 3 + $tags = $resp.Content | ConvertFrom-Json + if ($tags.Count -gt 0) { return $tags[0].name } + } catch { } + return $null +} + +# ========== 同步核心 (精简版 bookworm-sync.ps1) ========== +function Invoke-OtaSync ($cred, $cfg, $remoteVersion) { + $repoUrl = if ($cfg.repoUrl) { $cfg.repoUrl } else { 'https://code.letcareme.com/bookworm/bookworm-smart-assistant' } + $host_ = ([Uri]$repoUrl).Host + $repoPath = ([Uri]$repoUrl).AbsolutePath.TrimStart('/') + $ref = if ($remoteVersion -and $remoteVersion -match '^v?\d') { $remoteVersion } else { 'main' } + if ($ref -notmatch '^v') { $ref = "v$ref" } + + $stageDir = Join-Path $env:TEMP "bw-ota-$([Guid]::NewGuid().ToString().Substring(0,8))" + + # 1. Clone (basic auth user:pass) + Write-Ota "下载 $ref ..." + $escapedUser = [Uri]::EscapeDataString($cred.user) + $escapedPass = [Uri]::EscapeDataString($cred.pass) + $cloneUrl = "https://${escapedUser}:${escapedPass}@${host_}/${repoPath}.git" + $cloneArgs = @('-c', 'core.longpaths=true', 'clone', '--depth', '1', '--branch', $ref, '--single-branch', $cloneUrl, $stageDir) + & git @cloneArgs 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { throw "clone 失败 (exit $LASTEXITCODE)" } + $gitDir = Join-Path $stageDir '.git' + if (Test-Path $gitDir) { Remove-Item -Recurse -Force $gitDir } + + # 2. 验签 (Ed25519) + Write-Ota "验证签名..." + $integrityPath = Join-Path $stageDir 'INTEGRITY.sha256' + $sigPath = Join-Path $stageDir 'INTEGRITY.sha256.sig' + if (-not (Test-Path $integrityPath) -or -not (Test-Path $sigPath)) { + throw "包缺失签名文件" + } + $verifyScript = @' +const fs=require('fs'),crypto=require('crypto'); +const d=fs.readFileSync(process.argv[2]),s=Buffer.from(fs.readFileSync(process.argv[3],'utf8').trim(),'hex'); +const k=crypto.createPublicKey(fs.readFileSync(process.argv[4],'utf8')); +process.exit(crypto.verify(null,d,k,s)?0:1); +'@ + $tmpV = Join-Path $env:TEMP "bw-ota-verify-$([Guid]::NewGuid().ToString().Substring(0,8)).js" + [IO.File]::WriteAllText($tmpV, $verifyScript, [Text.UTF8Encoding]::new($false)) + node $tmpV $integrityPath $sigPath $PubKeyFile 2>&1 | Out-Null + $sigOk = $LASTEXITCODE -eq 0 + Remove-Item $tmpV -Force -ErrorAction SilentlyContinue + if (-not $sigOk) { throw "Ed25519 验签失败! 包可能被篡改." } + Write-OtaOk "签名验证通过" + + # 3. 逐文件哈希 + Write-Ota "校验文件完整性..." + $lines = Get-Content $integrityPath + $fail = 0 + foreach ($line in $lines) { + if ($line -notmatch '^([a-f0-9]{64})\s{2}(.+)$') { throw "INTEGRITY 格式错误" } + $expected = $Matches[1]; $relPath = $Matches[2] + $abs = Join-Path $stageDir $relPath + if (-not (Test-Path $abs -PathType Leaf)) { continue } + $actual = (Get-FileHash -Path $abs -Algorithm SHA256).Hash.ToLower() + if ($actual -ne $expected) { $fail++ } + } + if ($fail -gt 0) { throw "完整性校验失败: $fail 处哈希不匹配" } + Write-OtaOk "文件完整性校验通过 ($($lines.Count) 个文件)" + + # 4. 渲染 settings.template.json + $tplPath = Join-Path $stageDir 'settings.template.json' + if (Test-Path $tplPath) { + $raw = Get-Content -Raw $tplPath + if ($raw.Length -gt 0 -and [int][char]$raw[0] -eq 0xFEFF) { $raw = $raw.Substring(1) } + $rootFwd = $ClaudeRoot -replace '\\', '/' + $homeFwd = $env:USERPROFILE -replace '\\', '/' + $rendered = $raw -replace '\{\{CLAUDE_ROOT\}\}', $rootFwd -replace '\{\{HOME\}\}', $homeFwd + $outPath = Join-Path $stageDir 'settings.json' + [IO.File]::WriteAllText($outPath, $rendered, [Text.UTF8Encoding]::new($false)) + } + + # 5. 保留本机私有 + $preserveList = @( + 'memory', 'projects', 'sessions', 'session-env', 'session-state', 'sessions.db', + 'tasks', 'teams', + 'pinned-sessions.json', 'pinned-sessions.json.tmp', + 'history.jsonl', 'evolution-log.jsonl', + '.credentials.json', '.bw-token', '.hmac-key', '.skill-cache', + 'file-history', 'image-cache', 'paste-cache', 'debug', 'telemetry', + 'cache', 'plans', 'plugins', 'shell-snapshots', 'vendor', + 'repos', 'backups', 'archives', + 'mcp-servers', 'node_modules', + 'settings.local.json', 'settings.json.bak.*', + 'auto-sync-repos.json', 'scheduled_tasks.lock', + '.bw-ota' + ) + $preserved = 0 + if (Test-Path $ClaudeRoot) { + foreach ($pat in $preserveList) { + $items = @(Get-ChildItem -Path $ClaudeRoot -Filter $pat -Force -ErrorAction SilentlyContinue) + foreach ($item in $items) { + $target = Join-Path $stageDir $item.Name + if (Test-Path $target) { Remove-Item -Recurse -Force $target } + Copy-Item -Path $item.FullName -Destination $target -Recurse -Force + $preserved++ + } + } + } + Write-Ota "已保留 $preserved 项本机数据 (copy)" + + # DryRun: 验证到此为止, 不做原子替换 + if ($DryRun) { + $fileCount = (Get-ChildItem -Path $stageDir -Recurse -File).Count + Write-OtaOk "[DryRun] 验证通过! staging 含 $fileCount 个文件, 跳过原子替换" + Remove-Item -Recurse -Force $stageDir -ErrorAction SilentlyContinue + return + } + + # 6. 备份 + 原子替换 (先备份再替换, 任一步失败不丢数据) + $bakFull = $null + if (Test-Path $ClaudeRoot) { + $ts = Get-Date -Format 'yyyyMMdd-HHmmss' + $bakLeaf = ".claude.bak-$ts" + $bakFull = Join-Path (Split-Path -Parent $ClaudeRoot) $bakLeaf + try { + Rename-Item -Path $ClaudeRoot -NewName $bakLeaf -ErrorAction Stop + } catch { + throw "无法备份 .claude (可能被占用): $_" + } + } + try { + Move-Item -Path $stageDir -Destination $ClaudeRoot -ErrorAction Stop + } catch { + if ($bakFull -and (Test-Path $bakFull)) { + Rename-Item -Path $bakFull -NewName (Split-Path -Leaf $ClaudeRoot) + Write-OtaWarn "替换失败, 已回滚到原版本" + } + throw "替换失败: $_" + } + + # 7. 清理旧备份 (保留最近 3 个) + $parent = Split-Path -Parent $ClaudeRoot + $baks = @(Get-ChildItem -Path $parent -Directory -Filter ".claude.bak-*" -ErrorAction SilentlyContinue | Sort-Object Name -Descending) + if ($baks.Count -gt 3) { + $baks | Select-Object -Skip 3 | ForEach-Object { + Remove-Item -Recurse -Force $_.FullName -ErrorAction SilentlyContinue + } + } + + # 8. 写入 VERSION 到新 .claude + $versionPath = Join-Path $ClaudeRoot 'VERSION' + if (-not (Test-Path $versionPath)) { + [IO.File]::WriteAllText($versionPath, "$remoteVersion`n", [Text.UTF8Encoding]::new($false)) + } + + Write-OtaOk "更新完成! $ref" +} + +# ========== 主流程 (fail-open 包裹) ========== +function Invoke-OtaMain { + try { + # 基础设施检查 + if (-not (Test-OtaReady)) { + return + } + + $cfg = Read-OtaConfig + if (-not $cfg) { return } + + # 冷却检查 + if (Test-Cooldown $cfg) { + return + } + + # 解密凭证 + $cred = Read-Credential + if (-not $cred -or -not $cred.user -or -not $cred.pass) { + Write-OtaWarn "凭证解密失败, 跳过更新检查" + return + } + + # 本地 vs 远端版本 + $localVer = Get-LocalVersion + if ($DryRun) { Write-Ota "[DryRun 模式] 仅验证, 不替换文件" 'Yellow' } + Write-Ota "当前版本: $localVer" + + $remoteVer = Get-RemoteVersion $cred $cfg + if (-not $remoteVer) { + $remoteVer = Get-RemoteLatestTag $cred $cfg + if ($remoteVer) { $remoteVer = $remoteVer -replace '^v', '' } + } + + # 更新 lastCheck + $cfg.lastCheck = [int][DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + Save-OtaConfig $cfg + + if (-not $remoteVer) { + Write-OtaWarn "无法获取远端版本, 跳过" + return + } + + $localClean = ($localVer -replace '^v', '').Trim() + $remoteClean = ($remoteVer -replace '^v', '').Trim() + + if ($localClean -eq $remoteClean) { + Write-OtaOk "v$localClean 已是最新" + return + } + + $cmp = Compare-SemVer $localClean $remoteClean + if ($cmp -le 0) { + Write-OtaOk "v$localClean 已是最新 (远端 v$remoteClean 非更高版本)" + return + } + + # 发现新版本 (远端 > 本地) + Write-Host "" + Write-Ota "发现新版本 v$remoteClean (当前 v$localClean)" 'Magenta' + + if (-not $SkipConfirm) { + Write-Host "[Bookworm OTA] 按 Enter 更新 / Ctrl+C 跳过: " -ForegroundColor Yellow -NoNewline + try { [void][Console]::ReadLine() } + catch { + Write-Ota "已跳过更新" + return + } + } + + Invoke-OtaSync $cred $cfg $remoteClean + Write-Host "" + } + catch { + Write-OtaWarn "更新检查异常: $($_.Exception.Message)" + Write-OtaWarn "跳过更新, 继续启动..." + } +} + +Invoke-OtaMain diff --git a/启动Bookworm.bat b/启动Bookworm.bat index 6ada675..14908ff 100644 --- a/启动Bookworm.bat +++ b/启动Bookworm.bat @@ -31,7 +31,13 @@ if not defined CLAUDE_PS1 ( exit /b 1 ) -:: 3. 直调 pwsh + claude.ps1 (无 wt / 无 Base64 / 无 DPAPI in-bat) +:: 3. OTA 自动更新检查 (fail-open: 脚本不存在或报错均不阻断启动) +set "OTA_SCRIPT=%USERPROFILE%\.claude\.bw-ota\bw-ota.ps1" +if exist "%OTA_SCRIPT%" ( + "%PWSH_EXE%" -NoLogo -ExecutionPolicy Bypass -File "%OTA_SCRIPT%" +) + +:: 4. 直调 pwsh + claude.ps1 (无 wt / 无 Base64 / 无 DPAPI in-bat) :: 凭证由 pwsh profile.ps1 BW_CRED_START..END 块自动加载 "%PWSH_EXE%" -NoLogo -NoExit -File "%CLAUDE_PS1%" --dangerously-skip-permissions