bookworm-boot/install.ps1
leesu 889b485933 feat: auto-detect system proxy for China users
- Detect via .NET DefaultWebProxy (covers Clash/V2Ray/快柠檬/system proxy)
- Detect via IE registry proxy settings
- Scan common local proxy ports as fallback
- Clear error message + manual override instructions if no proxy found
2026-04-01 17:53:51 +08:00

375 lines
16 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.

<#
.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