<# .SYNOPSIS Bookworm Portable - 安装/启动脚本 .DESCRIPTION 从 Gitea 私有仓克隆 Bookworm 配置到目标机, 解密凭证, 渲染模板, 启动 Claude Code. .USAGE # 首次安装 (从 Gitea 克隆) .\install.ps1 # 仅启动 (已安装过) .\install.ps1 -StartOnly # 指定 Gitea 地址 .\install.ps1 -GitUrl "http://8.138.11.105:3000/bookworm/bookworm-config.git" #> param( [string]$GitUrl = "https://code.letcareme.com/bookworm/bookworm-config.git", [switch]$StartOnly, [switch]$SkipSecrets ) $ErrorActionPreference = "Stop" # ─── 路径定义 ──────────────────────────────────────── $ScriptDir = if ($MyInvocation.MyCommand.Path) { Split-Path -Parent $MyInvocation.MyCommand.Path } else { $PWD.Path } $ClaudeTarget = Join-Path $env:USERPROFILE ".claude" $BackupPath = Join-Path $env:USERPROFILE ".claude.bw-backup" $SecretsEnc = Join-Path $ScriptDir "secrets.enc" $TemplateFile = Join-Path $ClaudeTarget "settings.template.json" $LocalTplFile = Join-Path $ClaudeTarget "settings.local.template.json" $SettingsFile = Join-Path $ClaudeTarget "settings.json" $LocalSetFile = Join-Path $ClaudeTarget "settings.local.json" $DebugDir = Join-Path $ClaudeTarget "debug" # ─── openssl 检测 ──────────────────────────────────── $cmd = Get-Command openssl -ErrorAction SilentlyContinue $opensslCmd = if ($cmd) { $cmd.Source } else { $null } if (-not $opensslCmd) { $searchPaths = @("C:\Program Files\Git\usr\bin\openssl.exe", "D:\Git\usr\bin\openssl.exe", "D:\Git\mingw64\bin\openssl.exe", "C:\Program Files\Git\mingw64\bin\openssl.exe") $opensslCmd = $searchPaths | Where-Object { Test-Path $_ } | Select-Object -First 1 } # ─── 辅助函数 ──────────────────────────────────────── function Write-Banner { Write-Host "" Write-Host " ╔══════════════════════════════════════════╗" -ForegroundColor Cyan Write-Host " ║ Bookworm Portable Installer v1.1 ║" -ForegroundColor Cyan Write-Host " ╚══════════════════════════════════════════╝" -ForegroundColor Cyan Write-Host "" } function Test-Command($cmd) { return [bool](Get-Command $cmd -ErrorAction SilentlyContinue) } function Decrypt-Secrets { if ($SkipSecrets -or -not (Test-Path $SecretsEnc)) { Write-Host " [!] 跳过凭证解密 (无 secrets.enc)" -ForegroundColor Yellow return } if (-not $opensslCmd) { Write-Host " [!] openssl 未找到,跳过凭证解密" -ForegroundColor Yellow return } $password = Read-Host " 输入主密码解密凭证" -AsSecureString $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password) $plainPwd = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) try { # 通过 stdin 传入密码,避免进程列表泄露 $decrypted = $plainPwd | & $opensslCmd enc -aes-256-cbc -d -pbkdf2 -iter 600000 -in $SecretsEnc -pass stdin 2>&1 if ($LASTEXITCODE -ne 0) { throw "解密失败,密码错误?" } # 使用 IndexOf 正确分割 key=value (value 中可能含 = 号) $decrypted -split "`n" | ForEach-Object { $line = $_.Trim() if ($line -and $line.Contains('=')) { $eqIdx = $line.IndexOf('=') $key = $line.Substring(0, $eqIdx).Trim() $val = $line.Substring($eqIdx + 1).Trim() [System.Environment]::SetEnvironmentVariable($key, $val, "Process") Write-Host " [OK] 已注入: $key" -ForegroundColor Green } } } catch { Write-Host " [ERROR] 凭证解密失败: $_" -ForegroundColor Red # 检查关键凭证是否存在 if (-not $env:ANTHROPIC_API_KEY) { Write-Host " [WARN] ANTHROPIC_API_KEY 未设置,Claude Code 可能无法使用中转站" -ForegroundColor Yellow $continue = Read-Host " 继续启动? (y/n)" if ($continue -ne 'y') { exit 1 } } } finally { $plainPwd = $null [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } } function Render-SettingsTemplate { if (-not (Test-Path $TemplateFile)) { Write-Host " [!] 未找到 settings.template.json,跳过渲染" -ForegroundColor Yellow return } $claudeRoot = $ClaudeTarget.Replace('\', '/') # HOME 保留反斜杠格式,与 Claude Code 原始行为一致 $homeDir = $env:USERPROFILE $content = Get-Content $TemplateFile -Raw $content = $content -replace '\{\{CLAUDE_ROOT\}\}', $claudeRoot $content = $content -replace '\{\{HOME\}\}', ($homeDir -replace '\\', '\\') Set-Content $SettingsFile -Value $content -Encoding UTF8 Write-Host " [OK] settings.json 已渲染 (ROOT=$claudeRoot)" -ForegroundColor Green # 渲染 settings.local.template.json (如果存在) if (Test-Path $LocalTplFile) { $localContent = Get-Content $LocalTplFile -Raw $localContent = $localContent -replace '\{\{CLAUDE_ROOT\}\}', $claudeRoot $localContent = $localContent -replace '\{\{HOME\}\}', ($homeDir -replace '\\', '\\') $localContent = $localContent -replace '\{\{USERNAME\}\}', $env:USERNAME Set-Content $LocalSetFile -Value $localContent -Encoding UTF8 Write-Host " [OK] settings.local.json 已渲染" -ForegroundColor Green } } # ─── 代理自动检测 ──────────────────────────────────── function Detect-SystemProxy { # 如果已手动设置了 HTTPS_PROXY,直接使用 if ($env:HTTPS_PROXY) { Write-Host " [OK] 已设置 HTTPS_PROXY=$($env:HTTPS_PROXY)" -ForegroundColor Green return } Write-Host " 检测系统代理..." -ForegroundColor Gray # 方法1: 通过 .NET 获取系统代理 (最可靠,支持 Clash/V2Ray/快柠檬/系统代理) try { $proxyUri = [System.Net.WebRequest]::DefaultWebProxy.GetProxy("https://api.anthropic.com") if ($proxyUri -and $proxyUri.Authority -ne "api.anthropic.com") { $proxyUrl = "http://$($proxyUri.Authority)" $env:HTTPS_PROXY = $proxyUrl $env:HTTP_PROXY = $proxyUrl Write-Host " [OK] 检测到系统代理: $proxyUrl" -ForegroundColor Green return } } catch {} # 方法2: 读取注册表 IE 代理设置 try { $reg = Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings" -ErrorAction SilentlyContinue if ($reg.ProxyEnable -eq 1 -and $reg.ProxyServer) { $proxy = $reg.ProxyServer if ($proxy -notmatch '^http') { $proxy = "http://$proxy" } $env:HTTPS_PROXY = $proxy $env:HTTP_PROXY = $proxy Write-Host " [OK] 检测到 IE 代理: $proxy" -ForegroundColor Green return } } catch {} # 方法3: 扫描常见代理端口 $commonPorts = @(7890, 7891, 7893, 10792, 10793, 10808, 10809, 1080, 1087, 8080, 8118) foreach ($port in $commonPorts) { try { $tcp = New-Object System.Net.Sockets.TcpClient $tcp.Connect("127.0.0.1", $port) $tcp.Close() $env:HTTPS_PROXY = "http://127.0.0.1:$port" $env:HTTP_PROXY = "http://127.0.0.1:$port" Write-Host " [OK] 检测到本地代理端口: $port" -ForegroundColor Green return } catch {} } # 未找到代理 Write-Host " [!!] 未检测到代理/VPN" -ForegroundColor Red Write-Host "" Write-Host " Claude Code 需要代理才能在国内使用 (启动时检查 api.anthropic.com)" -ForegroundColor Yellow Write-Host " 请先启动代理软件 (Clash/V2Ray/快柠檬等),然后重新运行本脚本" -ForegroundColor Yellow Write-Host "" Write-Host " 如已有代理,可手动指定:" -ForegroundColor Gray Write-Host " `$env:HTTPS_PROXY = 'http://127.0.0.1:端口号'" -ForegroundColor Gray Write-Host " pwsh -ExecutionPolicy Bypass -File install.ps1" -ForegroundColor Gray Write-Host "" $continue = Read-Host " 无代理继续? (y/n,无代理大概率启动失败)" if ($continue -ne 'y') { exit 1 } } # ─── 主流程 ────────────────────────────────────────── Write-Banner # 步骤 0: 前置检查 Write-Host "[0/7] 前置检查..." -ForegroundColor White $checks = @( @{ Name = "Claude Code"; OK = (Test-Command "claude") } @{ Name = "Node.js"; OK = (Test-Command "node") } @{ Name = "Git"; OK = (Test-Command "git") } ) foreach ($c in $checks) { $icon = if ($c.OK) { "[OK]" } else { "[!!]" } $color = if ($c.OK) { "Green" } else { "Red" } Write-Host " $icon $($c.Name)" -ForegroundColor $color } if (-not (Test-Command "claude")) { Write-Host "`n [ABORT] Claude Code 未安装,请先安装后重试" -ForegroundColor Red exit 1 } if (-not (Test-Command "node")) { Write-Host "`n [ABORT] Node.js 未安装,Bookworm Hooks 无法运行" -ForegroundColor Red exit 1 } # 步骤 0.5: 代理检测 (国内必须) Write-Host "`n[0.5/7] 代理检测..." -ForegroundColor White Detect-SystemProxy # 步骤 1: 解密凭证 (进程级环境变量,不写磁盘) Write-Host "`n[1/7] 解密凭证..." -ForegroundColor White Decrypt-Secrets # 步骤 2: 克隆/更新仓库 if (-not $StartOnly) { Write-Host "`n[2/7] 同步 Bookworm 配置..." -ForegroundColor White if (Test-Path $ClaudeTarget) { $isGit = Test-Path (Join-Path $ClaudeTarget ".git") if ($isGit) { Write-Host " 已有仓库,执行 git pull..." Push-Location $ClaudeTarget $prevEAP = $ErrorActionPreference $ErrorActionPreference = "Continue" try { git stash 2>&1 | Out-Null git pull --rebase 2>&1 | ForEach-Object { Write-Host " $_" } git stash pop 2>&1 | Out-Null } catch { Write-Host " [WARN] git pull 失败: $_" -ForegroundColor Yellow Write-Host " 使用本地现有版本继续" -ForegroundColor Yellow } finally { $ErrorActionPreference = $prevEAP Pop-Location } } else { # 安全克隆: 先克隆到临时目录,成功后再替换 Write-Host " 备份现有 .claude/ 并克隆..." $tempClone = "$ClaudeTarget.bw-clone-temp" if (Test-Path $tempClone) { Remove-Item $tempClone -Recurse -Force } $prevEAP = $ErrorActionPreference $ErrorActionPreference = "Continue" git clone --depth 1 $GitUrl $tempClone 2>&1 | ForEach-Object { Write-Host " $_" } $cloneExit = $LASTEXITCODE $ErrorActionPreference = $prevEAP if ($cloneExit -ne 0 -or -not (Test-Path (Join-Path $tempClone "CLAUDE.md"))) { Write-Host " [ERROR] 克隆失败 (exit=$cloneExit)" -ForegroundColor Red if (Test-Path $tempClone) { Remove-Item $tempClone -Recurse -Force -ErrorAction SilentlyContinue } Write-Host " 原始 .claude/ 未被修改" -ForegroundColor Yellow exit 1 } # 克隆成功,执行替换 if (Test-Path $BackupPath) { Remove-Item $BackupPath -Recurse -Force } Rename-Item $ClaudeTarget $BackupPath Rename-Item $tempClone $ClaudeTarget Write-Host " [OK] 克隆完成,原始配置已备份到 .claude.bw-backup/" -ForegroundColor Green } } else { Write-Host " 首次安装,克隆仓库..." $prevEAP = $ErrorActionPreference $ErrorActionPreference = "Continue" git clone --depth 1 $GitUrl $ClaudeTarget 2>&1 | ForEach-Object { Write-Host " $_" } $cloneExit = $LASTEXITCODE $ErrorActionPreference = $prevEAP if ($cloneExit -ne 0 -or -not (Test-Path (Join-Path $ClaudeTarget "CLAUDE.md"))) { Write-Host " [ERROR] 克隆失败 (exit=$cloneExit)" -ForegroundColor Red Write-Host "" Write-Host " 可能原因:" -ForegroundColor Yellow Write-Host " - Gitea 服务不可达 (检查 https://code.letcareme.com)" -ForegroundColor Yellow Write-Host " - 网络连接问题 (检查 DNS 和防火墙)" -ForegroundColor Yellow Write-Host " - Git 凭证错误 (检查用户名密码)" -ForegroundColor Yellow Write-Host "" Write-Host " 离线模式: 如有本地 .claude 备份,运行:" -ForegroundColor Gray Write-Host " Copy-Item .claude.bw-backup .claude -Recurse" -ForegroundColor Gray exit 1 } } } else { Write-Host "`n[2/7] StartOnly 模式,跳过同步" -ForegroundColor Gray } # 步骤 3: 完整性校验 $integrityFile = Join-Path $ClaudeTarget "integrity.sha256" if (Test-Path $integrityFile) { Write-Host "`n[4/7] 完整性校验..." -ForegroundColor White $failures = @() Get-Content $integrityFile | ForEach-Object { if ($_ -match '^([a-f0-9]{64})\s+(.+)$') { $expectedHash = $Matches[1] $filePath = Join-Path $ClaudeTarget $Matches[2] if (Test-Path $filePath) { $actualHash = (Get-FileHash $filePath -Algorithm SHA256).Hash.ToLower() if ($actualHash -ne $expectedHash) { $failures += $Matches[2] } } } } if ($failures.Count -gt 0) { Write-Host " [WARN] 以下文件哈希不匹配:" -ForegroundColor Yellow $failures | ForEach-Object { Write-Host " $_" -ForegroundColor Yellow } Write-Host " 可能原因: 仓库内容被修改或本地有改动" -ForegroundColor Yellow $continue = Read-Host " 继续? (y/n)" if ($continue -ne 'y') { exit 1 } } else { Write-Host " [OK] 所有文件完整性校验通过" -ForegroundColor Green } } else { Write-Host "`n[4/7] 跳过完整性校验 (无 integrity.sha256)" -ForegroundColor Gray } # 步骤 4: 渲染 settings.json Write-Host "`n[5/7] 渲染配置模板..." -ForegroundColor White Render-SettingsTemplate # 步骤 5: 确保必要目录存在 Write-Host "`n[6/7] 初始化本地目录..." -ForegroundColor White $localDirs = @("debug", "sessions", "cache", "backups", "telemetry", "shell-snapshots", "projects", "memory") foreach ($d in $localDirs) { $dirPath = Join-Path $ClaudeTarget $d if (-not (Test-Path $dirPath)) { New-Item -ItemType Directory -Path $dirPath -Force | Out-Null Write-Host " 创建: $d/" -ForegroundColor Gray } } # 设置环境变量 (进程级) $env:CLAUDE_HOME = $ClaudeTarget Write-Host " [OK] CLAUDE_HOME=$ClaudeTarget" -ForegroundColor Green # 验证环境变量传递 $nodeCheck = & node -e "console.log(process.env.CLAUDE_HOME || 'NOT_SET')" 2>$null if ($nodeCheck -eq $ClaudeTarget) { Write-Host " [OK] Node.js 环境变量传递验证通过" -ForegroundColor Green } else { Write-Host " [WARN] Node.js 环境变量传递异常,hooks 可能无法正确解析路径" -ForegroundColor Yellow } # 步骤 6: 启动 Claude Code Write-Host "`n[7/7] 启动 Claude Code..." -ForegroundColor White Write-Host "" Write-Host " ╔══════════════════════════════════════════╗" -ForegroundColor Green Write-Host " ║ Bookworm 就绪! 正在启动 Claude Code... ║" -ForegroundColor Green Write-Host " ╚══════════════════════════════════════════╝" -ForegroundColor Green Write-Host "" & claude