2026-04-01 14:30:05 +08:00
|
|
|
|
<#
|
2026-04-01 12:47:44 +08:00
|
|
|
|
.SYNOPSIS
|
|
|
|
|
|
Bookworm Portable - 安装/启动脚本
|
|
|
|
|
|
.DESCRIPTION
|
|
|
|
|
|
从 Gitea 私有仓克隆 Bookworm 配置到目标机,
|
|
|
|
|
|
解密凭证, 渲染模板, 启动 Claude Code.
|
|
|
|
|
|
.USAGE
|
|
|
|
|
|
# 首次安装 (从 Gitea 克隆)
|
|
|
|
|
|
.\install.ps1
|
|
|
|
|
|
|
|
|
|
|
|
# 仅启动 (已安装过)
|
|
|
|
|
|
.\install.ps1 -StartOnly
|
|
|
|
|
|
|
|
|
|
|
|
# 指定 Gitea 地址
|
2026-04-01 14:45:00 +08:00
|
|
|
|
.\install.ps1 -GitUrl "http://8.138.11.105:3000/bookworm/bookworm-config.git"
|
2026-04-01 12:47:44 +08:00
|
|
|
|
#>
|
|
|
|
|
|
|
|
|
|
|
|
param(
|
2026-04-01 14:45:00 +08:00
|
|
|
|
[string]$GitUrl = "https://code.letcareme.com/bookworm/bookworm-config.git",
|
2026-04-01 12:47:44 +08:00
|
|
|
|
[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 检测 ────────────────────────────────────
|
2026-04-01 16:07:18 +08:00
|
|
|
|
$cmd = Get-Command openssl -ErrorAction SilentlyContinue
|
|
|
|
|
|
$opensslCmd = if ($cmd) { $cmd.Source } else { $null }
|
2026-04-01 12:47:44 +08:00
|
|
|
|
if (-not $opensslCmd) {
|
2026-04-01 14:30:05 +08:00
|
|
|
|
$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
|
2026-04-01 12:47:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ─── 辅助函数 ────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 17:53:51 +08:00
|
|
|
|
# ─── 代理自动检测 ────────────────────────────────────
|
|
|
|
|
|
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 }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 12:47:44 +08:00
|
|
|
|
# ─── 主流程 ──────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
Write-Banner
|
|
|
|
|
|
|
|
|
|
|
|
# 步骤 0: 前置检查
|
2026-04-01 17:53:51 +08:00
|
|
|
|
Write-Host "[0/7] 前置检查..." -ForegroundColor White
|
2026-04-01 12:47:44 +08:00
|
|
|
|
$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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 17:53:51 +08:00
|
|
|
|
# 步骤 0.5: 代理检测 (国内必须)
|
|
|
|
|
|
Write-Host "`n[0.5/7] 代理检测..." -ForegroundColor White
|
|
|
|
|
|
Detect-SystemProxy
|
|
|
|
|
|
|
2026-04-01 12:47:44 +08:00
|
|
|
|
# 步骤 1: 解密凭证 (进程级环境变量,不写磁盘)
|
2026-04-01 17:53:51 +08:00
|
|
|
|
Write-Host "`n[1/7] 解密凭证..." -ForegroundColor White
|
2026-04-01 12:47:44 +08:00
|
|
|
|
Decrypt-Secrets
|
|
|
|
|
|
|
|
|
|
|
|
# 步骤 2: 克隆/更新仓库
|
|
|
|
|
|
if (-not $StartOnly) {
|
2026-04-01 17:53:51 +08:00
|
|
|
|
Write-Host "`n[2/7] 同步 Bookworm 配置..." -ForegroundColor White
|
2026-04-01 12:47:44 +08:00
|
|
|
|
|
|
|
|
|
|
if (Test-Path $ClaudeTarget) {
|
|
|
|
|
|
$isGit = Test-Path (Join-Path $ClaudeTarget ".git")
|
|
|
|
|
|
if ($isGit) {
|
|
|
|
|
|
Write-Host " 已有仓库,执行 git pull..."
|
|
|
|
|
|
Push-Location $ClaudeTarget
|
2026-04-01 16:19:33 +08:00
|
|
|
|
$prevEAP = $ErrorActionPreference
|
|
|
|
|
|
$ErrorActionPreference = "Continue"
|
2026-04-01 12:47:44 +08:00
|
|
|
|
try {
|
2026-04-01 16:19:33 +08:00
|
|
|
|
git stash 2>&1 | Out-Null
|
2026-04-01 12:47:44 +08:00
|
|
|
|
git pull --rebase 2>&1 | ForEach-Object { Write-Host " $_" }
|
2026-04-01 16:19:33 +08:00
|
|
|
|
git stash pop 2>&1 | Out-Null
|
2026-04-01 12:47:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch {
|
|
|
|
|
|
Write-Host " [WARN] git pull 失败: $_" -ForegroundColor Yellow
|
|
|
|
|
|
Write-Host " 使用本地现有版本继续" -ForegroundColor Yellow
|
|
|
|
|
|
}
|
2026-04-01 16:19:33 +08:00
|
|
|
|
finally {
|
|
|
|
|
|
$ErrorActionPreference = $prevEAP
|
|
|
|
|
|
Pop-Location
|
|
|
|
|
|
}
|
2026-04-01 12:47:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
else {
|
|
|
|
|
|
# 安全克隆: 先克隆到临时目录,成功后再替换
|
|
|
|
|
|
Write-Host " 备份现有 .claude/ 并克隆..."
|
|
|
|
|
|
$tempClone = "$ClaudeTarget.bw-clone-temp"
|
2026-04-01 16:19:33 +08:00
|
|
|
|
if (Test-Path $tempClone) { Remove-Item $tempClone -Recurse -Force }
|
|
|
|
|
|
$prevEAP = $ErrorActionPreference
|
|
|
|
|
|
$ErrorActionPreference = "Continue"
|
2026-04-01 12:47:44 +08:00
|
|
|
|
git clone --depth 1 $GitUrl $tempClone 2>&1 | ForEach-Object { Write-Host " $_" }
|
2026-04-01 16:19:33 +08:00
|
|
|
|
$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
|
|
|
|
|
|
}
|
2026-04-01 12:47:44 +08:00
|
|
|
|
|
|
|
|
|
|
# 克隆成功,执行替换
|
|
|
|
|
|
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 " 首次安装,克隆仓库..."
|
2026-04-01 16:19:33 +08:00
|
|
|
|
$prevEAP = $ErrorActionPreference
|
|
|
|
|
|
$ErrorActionPreference = "Continue"
|
2026-04-01 12:47:44 +08:00
|
|
|
|
git clone --depth 1 $GitUrl $ClaudeTarget 2>&1 | ForEach-Object { Write-Host " $_" }
|
2026-04-01 16:19:33 +08:00
|
|
|
|
$cloneExit = $LASTEXITCODE
|
|
|
|
|
|
$ErrorActionPreference = $prevEAP
|
|
|
|
|
|
|
|
|
|
|
|
if ($cloneExit -ne 0 -or -not (Test-Path (Join-Path $ClaudeTarget "CLAUDE.md"))) {
|
|
|
|
|
|
Write-Host " [ERROR] 克隆失败 (exit=$cloneExit)" -ForegroundColor Red
|
2026-04-01 12:47:44 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-01 16:19:33 +08:00
|
|
|
|
}
|
2026-04-01 12:47:44 +08:00
|
|
|
|
else {
|
2026-04-01 17:53:51 +08:00
|
|
|
|
Write-Host "`n[2/7] StartOnly 模式,跳过同步" -ForegroundColor Gray
|
2026-04-01 12:47:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 步骤 3: 完整性校验
|
|
|
|
|
|
$integrityFile = Join-Path $ClaudeTarget "integrity.sha256"
|
|
|
|
|
|
if (Test-Path $integrityFile) {
|
2026-04-01 17:53:51 +08:00
|
|
|
|
Write-Host "`n[4/7] 完整性校验..." -ForegroundColor White
|
2026-04-01 12:47:44 +08:00
|
|
|
|
$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 {
|
2026-04-01 17:53:51 +08:00
|
|
|
|
Write-Host "`n[4/7] 跳过完整性校验 (无 integrity.sha256)" -ForegroundColor Gray
|
2026-04-01 12:47:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 步骤 4: 渲染 settings.json
|
2026-04-01 17:53:51 +08:00
|
|
|
|
Write-Host "`n[5/7] 渲染配置模板..." -ForegroundColor White
|
2026-04-01 12:47:44 +08:00
|
|
|
|
Render-SettingsTemplate
|
|
|
|
|
|
|
|
|
|
|
|
# 步骤 5: 确保必要目录存在
|
2026-04-01 17:53:51 +08:00
|
|
|
|
Write-Host "`n[6/7] 初始化本地目录..." -ForegroundColor White
|
2026-04-01 12:47:44 +08:00
|
|
|
|
$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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 19:09:25 +08:00
|
|
|
|
# 步骤 6: Bookworm 完整性验证 + MCP 检查
|
|
|
|
|
|
Write-Host "`n[7/8] 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)
|
|
|
|
|
|
$hasPlaywright = $false
|
|
|
|
|
|
try { $null = npx --yes @playwright/mcp --help 2>$null; $hasPlaywright = $true } catch {}
|
|
|
|
|
|
if (-not $hasPlaywright) {
|
|
|
|
|
|
$mcpWarnings += " [!] Playwright MCP 未安装 - 浏览器自动化功能不可用"
|
|
|
|
|
|
$mcpWarnings += " 安装: 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 步骤 7: 启动 Claude Code
|
|
|
|
|
|
Write-Host "`n[8/8] 启动 Claude Code..." -ForegroundColor White
|
2026-04-01 12:47:44 +08:00
|
|
|
|
Write-Host ""
|
2026-04-01 19:09:25 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-04-01 12:47:44 +08:00
|
|
|
|
Write-Host ""
|
|
|
|
|
|
|
|
|
|
|
|
& claude
|