bookworm-boot/install.ps1
bookworm dee7742a12 feat: pwsh7默认终端 + Phase1依赖增强 + Phase6 MCP预安装
- Phase 1: +PowerShell 7/Python 3.12/uv, pwsh升为核心依赖
- Phase 4: 修复缓存命中后仍弹密码框的逻辑缺陷
- Phase 5: 新增 {{PWSH_PATH}} 占位符渲染
- Phase 6: npm cache add + uv tool install 预缓存(超时保护)
- Phase 7: Start-Process pwsh 独立窗口启动Claude
- install.ps1: Render函数添加 {{PWSH_PATH}} + Start-Process启动
- OneClick.bat: 新增步骤4/8安装PowerShell 7
- 所有.bat: start pwsh新窗口模式 + CRLF修复

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 19:48:48 +08:00

739 lines
33 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 "https://code.letcareme.com/bookworm/bookworm-config.git"
#>
param(
[string]$GitUrl = "https://code.letcareme.com/bookworm/bookworm-config.git",
[switch]$StartOnly,
[switch]$SkipSecrets,
[switch]$AutoAccept # 豁免所有人工确认环节
)
$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"
# ─── 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.5 |" -ForegroundColor Cyan
Write-Host " | 92 Skills / 18 Agents / 34 Hooks |" -ForegroundColor Cyan
Write-Host " +------------------------------------------+" -ForegroundColor Cyan
Write-Host ""
}
function Test-Command($cmd) {
return [bool](Get-Command $cmd -ErrorAction SilentlyContinue)
}
# ─── 密码本日免输 (Windows Credential Manager) ──────
function Get-CachedSecrets {
try {
$cred = cmdkey /list 2>$null | Select-String "bookworm-secrets"
if ($cred) {
# 从 Credential Manager 读取缓存的环境变量
$regPath = "HKCU:\Software\Bookworm\CachedEnv"
if (Test-Path $regPath) {
$props = Get-ItemProperty $regPath -ErrorAction SilentlyContinue
$loaded = 0
foreach ($p in $props.PSObject.Properties) {
if ($p.Name -match '^[A-Z_]+$') {
[System.Environment]::SetEnvironmentVariable($p.Name, $p.Value, "Process")
$loaded++
}
}
if ($loaded -gt 0 -and $env:ANTHROPIC_API_KEY) {
Write-Host " [OK] 从本日缓存加载 $loaded 个凭证 (免密)" -ForegroundColor Green
return $true
}
}
}
} catch {}
return $false
}
function Save-SecretsToCache {
try {
# 用 Credential Manager 标记缓存存在
cmdkey /generic:bookworm-secrets /user:bw /pass:cached 2>$null | Out-Null
# 用 HKCU 注册表存凭证值 (DPAPI 保护, 仅当前用户可读)
$regPath = "HKCU:\Software\Bookworm\CachedEnv"
if (-not (Test-Path $regPath)) { New-Item $regPath -Force | Out-Null }
$envKeys = @("ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL", "GITHUB_PERSONAL_ACCESS_TOKEN",
"SLACK_BOT_TOKEN", "ATLASSIAN_API_TOKEN", "BROWSERBASE_API_KEY", "FIRECRAWL_API_KEY")
foreach ($k in $envKeys) {
$v = [System.Environment]::GetEnvironmentVariable($k, "Process")
if ($v) { Set-ItemProperty $regPath -Name $k -Value $v -Force }
}
# 设置过期时间 (今日 23:59:59)
$expiry = (Get-Date).Date.AddDays(1).ToString("o")
Set-ItemProperty $regPath -Name "_expiry" -Value $expiry -Force
Write-Host " [OK] 凭证已缓存至今日 23:59 (下次免密)" -ForegroundColor Green
} catch {}
}
function Clear-SecretsCache {
cmdkey /delete:bookworm-secrets 2>$null | Out-Null
Remove-Item "HKCU:\Software\Bookworm" -Recurse -Force -ErrorAction SilentlyContinue
}
# ─── 依赖自动安装 ────────────────────────────────────
function Install-MissingDeps {
$missing = @()
if (-not (Test-Command "node")) { $missing += "Node.js" }
if (-not (Test-Command "git")) { $missing += "Git" }
if (-not (Test-Command "claude")) { $missing += "Claude Code" }
if ($missing.Count -eq 0) { return }
$hasWinget = Test-Command "winget"
Write-Host ""
Write-Host " 缺少以下软件: $($missing -join ', ')" -ForegroundColor Yellow
if ($hasWinget) {
$auto = if ($AutoAccept) { 'y' } else { Read-Host " 是否用 winget 自动安装? (y/n)" }
if ($auto -eq 'y') {
if ($missing -contains "Node.js") {
Write-Host " 安装 Node.js..." -ForegroundColor Gray
winget install OpenJS.NodeJS.LTS --accept-source-agreements --accept-package-agreements 2>&1 | Select-Object -Last 3 | ForEach-Object { Write-Host " $_" }
# 刷新 PATH
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
}
if ($missing -contains "Git") {
Write-Host " 安装 Git..." -ForegroundColor Gray
winget install Git.Git --accept-source-agreements --accept-package-agreements 2>&1 | Select-Object -Last 3 | ForEach-Object { Write-Host " $_" }
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
}
if ($missing -contains "Claude Code" -and (Test-Command "npm")) {
Write-Host " 安装 Claude Code..." -ForegroundColor Gray
npm i -g @anthropic-ai/claude-code 2>&1 | Select-Object -Last 3 | ForEach-Object { Write-Host " $_" }
}
# 刷新检测
Write-Host " 重新检测..." -ForegroundColor Gray
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
}
} else {
Write-Host " 请手动安装后重新运行本脚本:" -ForegroundColor Yellow
if ($missing -contains "Node.js") { Write-Host " Node.js: https://nodejs.org" -ForegroundColor Gray }
if ($missing -contains "Git") { Write-Host " Git: https://git-scm.com" -ForegroundColor Gray }
if ($missing -contains "Claude Code") { Write-Host " Claude: npm i -g @anthropic-ai/claude-code" -ForegroundColor Gray }
exit 1
}
}
# ─── 桌面快捷方式 ────────────────────────────────────
function New-DesktopShortcuts {
$desktop = [System.Environment]::GetFolderPath("Desktop")
$bootDir = $ScriptDir
# 启动Bookworm 快捷方式 — 优先 pwsh (PS7),回退 powershell (PS5)
$lnkPath = Join-Path $desktop "Bookworm.lnk"
if (-not (Test-Path $lnkPath)) {
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.Save()
$psVer = if ($hasPwsh) { "PowerShell 7" } else { "PowerShell 5.1" }
Write-Host " [OK] 桌面快捷方式已创建: Bookworm ($psVer)" -ForegroundColor Green
} catch {
Write-Host " [!] 桌面快捷方式创建失败 (不影响使用)" -ForegroundColor Gray
}
}
}
function Decrypt-Secrets {
if ($SkipSecrets -or -not (Test-Path $SecretsEnc)) {
Write-Host " [!] 跳过凭证解密 (无 secrets.enc)" -ForegroundColor Yellow
return
}
# 优先用 Node.js 解密 (跨平台兼容性最高), 回退 openssl
$useNode = (Test-Command "node") -and (Test-Path (Join-Path $ScriptDir "crypto-helper.js"))
if (-not $useNode -and -not $opensslCmd) {
Write-Host " [!] node 和 openssl 均不可用,跳过凭证解密" -ForegroundColor Yellow
return
}
$cryptoHelper = Join-Path $ScriptDir "crypto-helper.js"
$maxRetries = 3
for ($attempt = 1; $attempt -le $maxRetries; $attempt++) {
$label = if ($attempt -gt 1) { " 重新输入主密码 (第 $attempt/$maxRetries 次)" } else { " 输入主密码解密凭证" }
$password = Read-Host $label -AsSecureString
$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password)
$plainPwd = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = "Continue"
if ($useNode) {
# Node.js 解密 (跨平台一致)
$decrypted = & node $cryptoHelper decrypt $plainPwd $SecretsEnc 2>&1
$decExit = $LASTEXITCODE
} else {
# openssl 回退
$decrypted = $plainPwd | & $opensslCmd enc -aes-256-cbc -d -pbkdf2 -iter 600000 -md sha256 -in $SecretsEnc -pass stdin 2>&1
$decExit = $LASTEXITCODE
}
$ErrorActionPreference = $prevEAP
# 清除内存中的密码
$plainPwd = $null
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
if ($decExit -eq 0 -and $decrypted -and $decrypted -notmatch 'PASSWORD_ERROR|FORMAT_ERROR|bad decrypt') {
# 解密成功,注入环境变量
$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
}
}
return
}
# 解密失败
$remaining = $maxRetries - $attempt
if ($remaining -gt 0) {
Write-Host " [!!] 密码错误,剩余重试: $remaining" -ForegroundColor Red
}
}
# 3次全部失败
Write-Host ""
Write-Host " [ABORT] 3 次密码均错误" -ForegroundColor Red
Write-Host " 请确认主密码是否正确 (区分大小写)" -ForegroundColor Yellow
Write-Host " 如忘记密码,请联系管理员重新生成 secrets.enc" -ForegroundColor Yellow
exit 1
}
function Render-SettingsTemplate {
if (-not (Test-Path $TemplateFile)) {
Write-Host " [!] 未找到 settings.template.json跳过渲染" -ForegroundColor Yellow
return
}
$claudeRoot = $ClaudeTarget.Replace('\', '/')
$homeDir = $env:USERPROFILE.Replace('\', '/')
# 定位 pwsh 路径 (正斜杠供 JSON)
$pwshExe = (Get-Command pwsh -ErrorAction SilentlyContinue)
$pwshJsonPath = if ($pwshExe) { $pwshExe.Source.Replace('\', '/') } else { "pwsh" }
$content = Get-Content $TemplateFile -Raw
$content = $content -replace '\{\{CLAUDE_ROOT\}\}', $claudeRoot
$content = $content -replace '\{\{HOME\}\}', $homeDir
$content = $content -replace '\{\{PWSH_PATH\}\}', $pwshJsonPath
Set-Content $SettingsFile -Value $content -Encoding UTF8
Write-Host " [OK] settings.json 已渲染 (ROOT=$claudeRoot, SHELL=$pwshJsonPath)" -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
$localContent = $localContent -replace '\{\{USERNAME\}\}', $env:USERNAME
$localContent = $localContent -replace '\{\{PWSH_PATH\}\}', $pwshJsonPath
Set-Content $LocalSetFile -Value $localContent -Encoding UTF8
Write-Host " [OK] settings.local.json 已渲染" -ForegroundColor Green
}
}
# ─── 代理自动检测 ────────────────────────────────────
function Detect-SystemProxy {
# 中转站在国内阿里云,不走代理
$env:NO_PROXY = "bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1"
$env:no_proxy = $env:NO_PROXY
# 如果已手动设置了 HTTPS_PROXY直接使用
if ($env:HTTPS_PROXY) {
Write-Host " [OK] 已设置 HTTPS_PROXY=$($env:HTTPS_PROXY)" -ForegroundColor Green
Write-Host " [OK] NO_PROXY=$($env:NO_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: 扫描常见代理端口 (500ms 超时,避免阻塞)
$commonPorts = @(7890, 7891, 7893, 10792, 10793, 10808, 10809, 1080, 1087, 8080, 8118)
foreach ($port in $commonPorts) {
try {
$tcp = New-Object System.Net.Sockets.TcpClient
$result = $tcp.BeginConnect("127.0.0.1", $port, $null, $null)
$success = $result.AsyncWaitHandle.WaitOne(500)
if (-not $success) { $tcp.Close(); continue }
$tcp.EndConnect($result)
$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 = if ($AutoAccept) { 'y' } else { Read-Host " 无代理继续? (y/n无代理大概率启动失败)" }
if ($continue -ne 'y') { exit 1 }
}
# ─── 主流程 ──────────────────────────────────────────
Write-Banner
# 步骤 1: 前置检查
Write-Host "[1/9] 前置检查..." -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") -or -not (Test-Command "node") -or -not (Test-Command "git")) {
Install-MissingDeps
}
# 再次验证
if (-not (Test-Command "claude")) {
Write-Host "`n [ABORT] Claude Code 未安装" -ForegroundColor Red
Write-Host " 安装: npm i -g @anthropic-ai/claude-code" -ForegroundColor Gray
exit 1
}
if (-not (Test-Command "node")) {
Write-Host "`n [ABORT] Node.js 未安装" -ForegroundColor Red
exit 1
}
# 步骤 2: 代理检测 (国内必须)
Write-Host "`n[2/9] 代理检测..." -ForegroundColor White
Detect-SystemProxy
# 步骤 3: 解密凭证 (优先使用本日缓存)
Write-Host "`n[3/9] 解密凭证..." -ForegroundColor White
# 检查缓存是否过期
$cacheExpiry = $null
try {
$cacheExpiry = Get-ItemProperty "HKCU:\Software\Bookworm\CachedEnv" -Name "_expiry" -ErrorAction SilentlyContinue
} catch {}
$cacheValid = $false
if ($cacheExpiry -and $cacheExpiry._expiry) {
try { $cacheValid = [datetime]$cacheExpiry._expiry -gt (Get-Date) } catch {}
}
if ($cacheValid -and (Get-CachedSecrets)) {
# 缓存有效,跳过解密
} elseif ($AutoAccept) {
# AutoAccept 模式: 无缓存时跳过密码输入
Write-Host " [!] AutoAccept 模式: 无有效缓存,跳过凭证解密" -ForegroundColor Yellow
Write-Host " 如需凭证,请不加 -AutoAccept 手动运行一次" -ForegroundColor Yellow
} else {
Clear-SecretsCache
Decrypt-Secrets
# 解密成功后询问是否缓存
if ($env:ANTHROPIC_API_KEY) {
$cache = if ($AutoAccept) { 'y' } else { Read-Host " 今日内免密启动? (y/n)" }
if ($cache -eq 'y') { Save-SecretsToCache }
}
}
# 自动配置 git credential helper (避免 clone 时反复要密码)
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = "Continue"
git config --global credential.helper store 2>$null
$ErrorActionPreference = $prevEAP
# 步骤 4: 克隆/更新仓库
if (-not $StartOnly) {
Write-Host "`n[4/9] 同步 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 {
$stashOutput = git stash 2>&1
$hasStash = $stashOutput -notmatch 'No local changes'
git pull --rebase 2>&1 | ForEach-Object { Write-Host " $_" }
if ($hasStash) { 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[4/9] StartOnly 模式,跳过同步" -ForegroundColor Gray
# 静默检测远程更新
$configDir = Join-Path $env:USERPROFILE ".claude"
if (Test-Path (Join-Path $configDir ".git")) {
$prevEAP2 = $ErrorActionPreference
$ErrorActionPreference = "Continue"
git -C $configDir fetch --quiet 2>$null
$behind = git -C $configDir rev-list "HEAD..origin/main" --count 2>$null
$ErrorActionPreference = $prevEAP2
if ($behind -and [int]$behind -gt 0) {
Write-Host " [!] Bookworm 有 $behind 个新更新可用" -ForegroundColor Yellow
Write-Host " 双击 '更新并启动Bookworm.bat' 可同步最新版本" -ForegroundColor Yellow
}
}
}
# 步骤 5: 完整性校验
$integrityFile = Join-Path $ClaudeTarget "integrity.sha256"
if (Test-Path $integrityFile) {
Write-Host "`n[5/9] 完整性校验..." -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 = if ($AutoAccept) { 'y' } else { Read-Host " 继续? (y/n)" }
if ($continue -ne 'y') { exit 1 }
} else {
Write-Host " [OK] 所有文件完整性校验通过" -ForegroundColor Green
}
} else {
Write-Host "`n[5/9] 跳过完整性校验 (无 integrity.sha256)" -ForegroundColor Gray
}
# 步骤 6: 渲染 settings.json
Write-Host "`n[6/9] 渲染配置模板..." -ForegroundColor White
Render-SettingsTemplate
# 步骤 7: 确保必要目录存在
Write-Host "`n[7/9] 初始化本地目录..." -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
}
# 步骤 8: Bookworm 完整性验证 + MCP 检查
Write-Host "`n[8/9] Bookworm 系统验证..." -ForegroundColor White
# --- Bookworm vs 原生 Claude Code 检测 ---
$bwChecks = @()
$claudeMd = Join-Path $ClaudeTarget "CLAUDE.md"
$skillsDir = Join-Path $ClaudeTarget "skills"
$hooksDir = Join-Path $ClaudeTarget "hooks"
$settingsF = Join-Path $ClaudeTarget "settings.json"
# 检查 CLAUDE.md
if (Test-Path $claudeMd) {
$content = Get-Content $claudeMd -Raw -ErrorAction SilentlyContinue
if ($content -match "Bookworm") {
$bwChecks += @{ Name = "CLAUDE.md (Bookworm 指令)"; OK = $true }
} else {
$bwChecks += @{ Name = "CLAUDE.md (缺少 Bookworm 指令)"; OK = $false }
}
} else {
$bwChecks += @{ Name = "CLAUDE.md (文件不存在!)"; OK = $false }
}
# 检查 Skills
$skillCount = 0
if (Test-Path $skillsDir) {
$skillCount = (Get-ChildItem $skillsDir -Directory -ErrorAction SilentlyContinue).Count
}
$bwChecks += @{ Name = "Skills ($skillCount 个)"; OK = ($skillCount -gt 50) }
# 检查 Hooks
$hookCount = 0
if (Test-Path $hooksDir) {
$hookCount = (Get-ChildItem $hooksDir -Filter "*.js" -File -ErrorAction SilentlyContinue).Count
}
$bwChecks += @{ Name = "Hooks ($hookCount 个)"; OK = ($hookCount -gt 10) }
# 检查 settings.json hooks 配置
$hasHooks = $false
if (Test-Path $settingsF) {
$sContent = Get-Content $settingsF -Raw -ErrorAction SilentlyContinue
if ($sContent -match '"hooks"') { $hasHooks = $true }
}
$bwChecks += @{ Name = "Settings hooks 配置"; OK = $hasHooks }
# 输出验证结果
$allOK = $true
foreach ($c in $bwChecks) {
$icon = if ($c.OK) { "[OK]" } else { "[!!]" }
$color = if ($c.OK) { "Green" } else { "Red" }
Write-Host " $icon $($c.Name)" -ForegroundColor $color
if (-not $c.OK) { $allOK = $false }
}
if (-not $allOK) {
Write-Host ""
Write-Host " ╔══════════════════════════════════════════════════════╗" -ForegroundColor Yellow
Write-Host " ║ [!] 警告: Bookworm 系统不完整 ║" -ForegroundColor Yellow
Write-Host " ║ Claude Code 将以原生模式启动 (无 Skills/Hooks) ║" -ForegroundColor Yellow
Write-Host " ║ 建议: 不加 -StartOnly 重新运行 install.ps1 同步 ║" -ForegroundColor Yellow
Write-Host " ╚══════════════════════════════════════════════════════╝" -ForegroundColor Yellow
} else {
Write-Host " [OK] Bookworm 系统完整 ($skillCount Skills / $hookCount Hooks)" -ForegroundColor Green
}
# --- MCP 依赖检查 (中文提醒) ---
Write-Host ""
Write-Host " MCP 服务检查:" -ForegroundColor Gray
$mcpWarnings = @()
# Python (askui/pywinauto/com-server 需要)
$hasPython = [bool](Get-Command python -ErrorAction SilentlyContinue)
if (-not $hasPython) {
$mcpWarnings += " [!] Python 未安装 - askui/pywinauto/com-server MCP 不可用"
$mcpWarnings += " 安装: https://www.python.org/downloads/ 或 winget install Python.Python.3.12"
}
# Playwright (浏览器自动化 MCP) — 轻量检测,避免 npx 触发安装
$hasPlaywright = $false
try {
$pwPath = & npm list -g @playwright/mcp 2>$null
if ($pwPath -and $pwPath -notmatch 'empty') { $hasPlaywright = $true }
} catch {}
if (-not $hasPlaywright) {
$mcpWarnings += " [!] Playwright MCP 未安装 - 浏览器自动化功能不可用"
$mcpWarnings += " 安装: npm i -g @playwright/mcp && npx playwright install"
}
# 检查关键 API Key 环境变量
$apiChecks = @(
@{ Key = "ANTHROPIC_API_KEY"; Name = "Claude API (中转站)" }
@{ Key = "ANTHROPIC_BASE_URL"; Name = "API 中转站地址" }
)
foreach ($ak in $apiChecks) {
$val = [System.Environment]::GetEnvironmentVariable($ak.Key, "Process")
if (-not $val) {
$mcpWarnings += " [!] $($ak.Key) 未设置 - $($ak.Name)不可用"
$mcpWarnings += " 需要管理员重新加密凭证并更新 secrets.enc"
}
}
# 可选 MCP API Key 检查
$optionalApis = @(
@{ Key = "GITHUB_PERSONAL_ACCESS_TOKEN"; Name = "GitHub MCP"; Cmd = "仓库管理/代码搜索" }
@{ Key = "SLACK_BOT_TOKEN"; Name = "Slack MCP"; Cmd = "消息发送/频道管理" }
@{ Key = "BROWSERBASE_API_KEY"; Name = "Browserbase MCP"; Cmd = "云端浏览器" }
@{ Key = "FIRECRAWL_API_KEY"; Name = "Firecrawl MCP"; Cmd = "网页抓取/搜索" }
)
$missingOptional = @()
foreach ($api in $optionalApis) {
$val = [System.Environment]::GetEnvironmentVariable($api.Key, "Process")
if (-not $val) {
$missingOptional += $api
}
}
if ($mcpWarnings.Count -gt 0) {
foreach ($w in $mcpWarnings) {
Write-Host $w -ForegroundColor Yellow
}
}
if ($missingOptional.Count -gt 0) {
Write-Host ""
Write-Host " 可选 MCP 服务 (未配置, 不影响核心功能):" -ForegroundColor Gray
foreach ($m in $missingOptional) {
Write-Host " [-] $($m.Name) ($($m.Cmd))" -ForegroundColor DarkGray
}
Write-Host " 如需使用, 请联系管理员将 API Key 加入 secrets.enc" -ForegroundColor DarkGray
}
if ($mcpWarnings.Count -eq 0) {
Write-Host " [OK] 核心 API 已配置" -ForegroundColor Green
}
# 步骤 9: 启动 Claude Code
Write-Host "`n[9/9] 启动 Claude Code..." -ForegroundColor White
Write-Host ""
if ($allOK) {
Write-Host " ╔══════════════════════════════════════════╗" -ForegroundColor Green
Write-Host " ║ Bookworm 就绪! 正在启动 Claude Code... ║" -ForegroundColor Green
Write-Host " ╚══════════════════════════════════════════╝" -ForegroundColor Green
} else {
Write-Host " ╔══════════════════════════════════════════╗" -ForegroundColor Yellow
Write-Host " ║ 原生模式启动 (Bookworm 不完整) ║" -ForegroundColor Yellow
Write-Host " ╚══════════════════════════════════════════╝" -ForegroundColor Yellow
}
Write-Host ""
# 首次安装: 创建桌面快捷方式 + 打开使用教程
if (-not $StartOnly) {
New-DesktopShortcuts
$guidePath = Join-Path $ScriptDir "guide.html"
if (Test-Path $guidePath) {
Start-Process $guidePath
Write-Host " [OK] 使用教程已在浏览器打开" -ForegroundColor Gray
}
}
# 始终在独立 pwsh7 窗口中启动 Claude Code
# (bat 调 pwsh 子进程时窗口仍属 cmd.exe, 必须 Start-Process 新建窗口)
$pwshCmd = Get-Command pwsh -ErrorAction SilentlyContinue
if ($pwshCmd) {
Write-Host " [..] 正在启动 Claude Code (PowerShell 7)..." -ForegroundColor Cyan
Start-Process $pwshCmd.Source -ArgumentList "-NoLogo", "-NoExit", "-Command", "& claude" -WorkingDirectory $env:USERPROFILE -WindowStyle Normal
} else {
& claude
}