2026-04-05 23:34:27 +08:00
<#
. 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 ( '\' , '/' )
2026-04-06 19:48:48 +08:00
$homeDir = $env:USERPROFILE . Replace ( '\' , '/' )
# 定位 pwsh 路径 (正斜杠供 JSON)
$pwshExe = ( Get-Command pwsh -ErrorAction SilentlyContinue )
$pwshJsonPath = if ( $pwshExe ) { $pwshExe . Source . Replace ( '\' , '/' ) } else { " pwsh " }
2026-04-05 23:34:27 +08:00
$content = Get-Content $TemplateFile -Raw
$content = $content -replace '\{\{CLAUDE_ROOT\}\}' , $claudeRoot
2026-04-06 19:48:48 +08:00
$content = $content -replace '\{\{HOME\}\}' , $homeDir
$content = $content -replace '\{\{PWSH_PATH\}\}' , $pwshJsonPath
2026-04-05 23:34:27 +08:00
Set-Content $SettingsFile -Value $content -Encoding UTF8
2026-04-06 19:48:48 +08:00
Write-Host " [OK] settings.json 已渲染 (ROOT= $claudeRoot , SHELL= $pwshJsonPath ) " -ForegroundColor Green
2026-04-05 23:34:27 +08:00
# 渲染 settings.local.template.json (如果存在)
if ( Test-Path $LocalTplFile ) {
$localContent = Get-Content $LocalTplFile -Raw
$localContent = $localContent -replace '\{\{CLAUDE_ROOT\}\}' , $claudeRoot
2026-04-06 19:48:48 +08:00
$localContent = $localContent -replace '\{\{HOME\}\}' , $homeDir
2026-04-05 23:34:27 +08:00
$localContent = $localContent -replace '\{\{USERNAME\}\}' , $env:USERNAME
2026-04-06 19:48:48 +08:00
$localContent = $localContent -replace '\{\{PWSH_PATH\}\}' , $pwshJsonPath
2026-04-05 23:34:27 +08:00
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
}
}
2026-04-06 19:48:48 +08:00
# 始终在独立 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
2026-04-06 20:18:30 +08:00
Start-Process $pwshCmd . Source -ArgumentList " -NoLogo " , " -NoExit " , " -Command " , " & claude --dangerously-skip-permissions " -WorkingDirectory $env:USERPROFILE -WindowStyle Normal
2026-04-06 19:48:48 +08:00
} else {
2026-04-06 20:18:30 +08:00
& claude - -dangerously -skip -permissions
2026-04-06 19:48:48 +08:00
}