bookworm-boot/install.ps1
bookworm b83c508c22 feat: replace master password auth with time-limited authorization codes
Auth codes use format BW-YYYYMMDD-TOKEN (24-hex, 96-bit entropy).
Token doubles as the AES-256-CBC decryption key for secrets.enc.
Expiry is enforced client-side; format/expiry errors don't consume
the 3 valid-attempt quota.

- gen-authcode.js: new admin tool — generates BW auth code + re-encrypts secrets.enc
- install.ps1: Parse-AuthCode validates format/expiry, Decrypt-Secrets uses token as key
- auto-setup.ps1: Show-AuthCodeDialog WinForms input + Parse-AuthCode-GUI loop
- Bookworm-Setup.sh: parse_authcode() bash function + while-loop with format/expiry handling

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

777 lines
34 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]$SkipLaunch, # 仅安装不启动 (由调用方负责启动)
[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) {
$regPath = "HKCU:\Software\Bookworm\CachedEnv"
if (Test-Path $regPath) {
Add-Type -AssemblyName System.Security
$props = Get-ItemProperty $regPath -ErrorAction SilentlyContinue
$loaded = 0
foreach ($p in $props.PSObject.Properties) {
if ($p.Name -match '^[A-Z_]+$') {
$val = $p.Value
try {
# DPAPI 解密 (Base64 → byte[] → 明文)
$bytes = [Security.Cryptography.ProtectedData]::Unprotect(
[Convert]::FromBase64String($val), $null,
[Security.Cryptography.DataProtectionScope]::CurrentUser)
$val = [Text.Encoding]::UTF8.GetString($bytes)
} catch {
# 回退: 旧版明文缓存兼容
}
[System.Environment]::SetEnvironmentVariable($p.Name, $val, "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 {
cmdkey /generic:bookworm-secrets /user:bw /pass:cached 2>$null | Out-Null
$regPath = "HKCU:\Software\Bookworm\CachedEnv"
if (-not (Test-Path $regPath)) { New-Item $regPath -Force | Out-Null }
Add-Type -AssemblyName System.Security
$envKeys = @("ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL", "GITHUB_PERSONAL_ACCESS_TOKEN",
"SLACK_BOT_TOKEN", "ATLASSIAN_API_TOKEN", "BROWSERBASE_API_KEY",
"FIRECRAWL_API_KEY", "GEMINI_API_KEY")
foreach ($k in $envKeys) {
$v = [System.Environment]::GetEnvironmentVariable($k, "Process")
if ($v) {
# DPAPI 加密: 明文 → byte[] → ProtectedData → Base64 存入注册表
$bytes = [Text.Encoding]::UTF8.GetBytes($v)
$enc = [Security.Cryptography.ProtectedData]::Protect(
$bytes, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
Set-ItemProperty $regPath -Name $k -Value ([Convert]::ToBase64String($enc)) -Force
}
}
$expiry = (Get-Date).Date.AddDays(1).ToString("o")
Set-ItemProperty $regPath -Name "_expiry" -Value $expiry -Force
Write-Host " [OK] 凭证已缓存至今日 23:59 (DPAPI 加密, 下次免密)" -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 Parse-AuthCode {
param([string]$code)
$code = $code.Trim()
# 格式: BW-YYYYMMDD-24位HexToken
if ($code -notmatch '^BW-(\d{8})-([A-Fa-f0-9]{24})$') {
Write-Host " [!!] 格式错误,应为 BW-YYYYMMDD-XXXXXXXXXXXXXXXXXXXX XXXX" -ForegroundColor Red
return $null
}
$expiryStr = $Matches[1]
$token = $Matches[2].ToLower() # 解密用小写
$today = (Get-Date).ToString("yyyyMMdd")
if ([int]$expiryStr -lt [int]$today) {
$d = "$($expiryStr.Substring(0,4))-$($expiryStr.Substring(4,2))-$($expiryStr.Substring(6,2))"
Write-Host " [!!] 授权码已过期 (有效期至 $d)" -ForegroundColor Red
Write-Host " 请联系管理员获取新授权码" -ForegroundColor Yellow
return $null
}
return $token
}
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 { " 输入授权码 (格式: BW-YYYYMMDD-...)" }
$authCodeRaw = Read-Host $label
$plainPwd = Parse-AuthCode $authCodeRaw
if (-not $plainPwd) {
# 格式错误或已过期: 不计入密码重试, 直接继续
$attempt--
$maxRetries-- # 最多给 3 次有效尝试
if ($maxRetries -lt 1) { break }
continue
}
$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
# 清除内存中的 token
$plainPwd = $null
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
}
}
# 启动 Claude Code (同步执行, 窗口类型由调用方 .bat 决定)
if ($SkipLaunch) {
Write-Host " [OK] 安装完成 (由调用方负责启动)" -ForegroundColor Green
} else {
& claude --dangerously-skip-permissions
}