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,
|
2026-04-06 20:34:00 +08:00
|
|
|
|
[switch]$SkipLaunch, # 仅安装不启动 (由调用方负责启动)
|
|
|
|
|
|
[switch]$AutoAccept # 豁免所有人工确认环节
|
2026-04-05 23:34:27 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
$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
|
2026-04-24 20:47:16 +08:00
|
|
|
|
Write-Host " | Bookworm Portable Installer v1.6 |" -ForegroundColor Cyan
|
|
|
|
|
|
Write-Host " | Claude Code 国内一键就绪 |" -ForegroundColor Cyan
|
2026-04-05 23:34:27 +08:00
|
|
|
|
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) {
|
2026-04-06 21:49:50 +08:00
|
|
|
|
Add-Type -AssemblyName System.Security
|
2026-04-05 23:34:27 +08:00
|
|
|
|
$props = Get-ItemProperty $regPath -ErrorAction SilentlyContinue
|
|
|
|
|
|
$loaded = 0
|
|
|
|
|
|
foreach ($p in $props.PSObject.Properties) {
|
|
|
|
|
|
if ($p.Name -match '^[A-Z_]+$') {
|
2026-04-06 21:49:50 +08:00
|
|
|
|
$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")
|
2026-04-05 23:34:27 +08:00
|
|
|
|
$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 }
|
2026-04-06 21:49:50 +08:00
|
|
|
|
Add-Type -AssemblyName System.Security
|
2026-04-05 23:34:27 +08:00
|
|
|
|
$envKeys = @("ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL", "GITHUB_PERSONAL_ACCESS_TOKEN",
|
2026-04-06 21:49:50 +08:00
|
|
|
|
"SLACK_BOT_TOKEN", "ATLASSIAN_API_TOKEN", "BROWSERBASE_API_KEY",
|
|
|
|
|
|
"FIRECRAWL_API_KEY", "GEMINI_API_KEY")
|
2026-04-05 23:34:27 +08:00
|
|
|
|
foreach ($k in $envKeys) {
|
|
|
|
|
|
$v = [System.Environment]::GetEnvironmentVariable($k, "Process")
|
2026-04-06 21:49:50 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-04-05 23:34:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
$expiry = (Get-Date).Date.AddDays(1).ToString("o")
|
|
|
|
|
|
Set-ItemProperty $regPath -Name "_expiry" -Value $expiry -Force
|
2026-04-06 21:49:50 +08:00
|
|
|
|
Write-Host " [OK] 凭证已缓存至今日 23:59 (DPAPI 加密, 下次免密)" -ForegroundColor Green
|
2026-04-05 23:34:27 +08:00
|
|
|
|
} 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 {
|
2026-04-25 18:19:07 +08:00
|
|
|
|
# v3.0.11 架构重构: .lnk 直调 pwsh + claude.ps1 绝对路径 (1 跳直链)
|
2026-04-05 23:34:27 +08:00
|
|
|
|
$desktop = [System.Environment]::GetFolderPath("Desktop")
|
|
|
|
|
|
$bootDir = $ScriptDir
|
2026-04-22 01:02:43 +08:00
|
|
|
|
$lnkPath = Join-Path $desktop "启动Bookworm.lnk"
|
2026-04-25 18:19:07 +08:00
|
|
|
|
|
|
|
|
|
|
# 定位 pwsh.exe
|
|
|
|
|
|
$pwshExe = (Get-Command pwsh -ErrorAction SilentlyContinue).Source
|
|
|
|
|
|
if (-not $pwshExe) {
|
|
|
|
|
|
foreach ($p in @("$env:ProgramFiles\PowerShell\7\pwsh.exe", "${env:ProgramFiles(x86)}\PowerShell\7\pwsh.exe")) {
|
|
|
|
|
|
if (Test-Path $p) { $pwshExe = $p; break }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (-not $pwshExe) {
|
|
|
|
|
|
Write-Host " [!] pwsh.exe 未找到, 跳过桌面快捷方式 (建议先装 PS7)" -ForegroundColor Yellow
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 定位 claude.ps1
|
|
|
|
|
|
$claudePs1 = $null
|
|
|
|
|
|
$claudeCmd = Get-Command claude -ErrorAction SilentlyContinue
|
|
|
|
|
|
if ($claudeCmd -and $claudeCmd.Source -and $claudeCmd.Source.EndsWith('.ps1') -and (Test-Path $claudeCmd.Source)) {
|
|
|
|
|
|
$claudePs1 = $claudeCmd.Source
|
|
|
|
|
|
}
|
|
|
|
|
|
if (-not $claudePs1) {
|
2026-04-05 23:34:27 +08:00
|
|
|
|
try {
|
2026-04-25 18:19:07 +08:00
|
|
|
|
$npmPrefix = (& npm config get prefix 2>$null | Select-Object -First 1).Trim()
|
|
|
|
|
|
$candidate = Join-Path $npmPrefix "claude.ps1"
|
|
|
|
|
|
if (Test-Path $candidate) { $claudePs1 = $candidate }
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (-not $claudePs1) {
|
|
|
|
|
|
foreach ($p in @("$env:APPDATA\npm\claude.ps1", "$env:ProgramFiles\nodejs\claude.ps1")) {
|
|
|
|
|
|
if (Test-Path $p) { $claudePs1 = $p; break }
|
2026-04-05 23:34:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-25 18:19:07 +08:00
|
|
|
|
if (-not $claudePs1) {
|
|
|
|
|
|
Write-Host " [!] claude.ps1 未找到, 跳过桌面快捷方式 (Claude Code 装好后重跑可补)" -ForegroundColor Yellow
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-04-22 01:02:43 +08:00
|
|
|
|
|
2026-04-25 18:19:07 +08:00
|
|
|
|
try {
|
|
|
|
|
|
$shell = New-Object -ComObject WScript.Shell
|
|
|
|
|
|
$shortcut = $shell.CreateShortcut($lnkPath)
|
|
|
|
|
|
$shortcut.TargetPath = $pwshExe
|
|
|
|
|
|
$shortcut.Arguments = "-NoLogo -NoExit -File `"$claudePs1`" --dangerously-skip-permissions"
|
|
|
|
|
|
$shortcut.WorkingDirectory = $env:USERPROFILE
|
|
|
|
|
|
$shortcut.Description = "Bookworm Smart Assistant (v3.0.11 直调)"
|
|
|
|
|
|
$iconPath = Join-Path $bootDir "bookworm-desktop.ico"
|
|
|
|
|
|
if (-not (Test-Path $iconPath)) { $iconPath = Join-Path $bootDir "bookworm.ico" }
|
|
|
|
|
|
if (Test-Path $iconPath) { $shortcut.IconLocation = "$iconPath,0" }
|
|
|
|
|
|
$shortcut.Save()
|
|
|
|
|
|
|
|
|
|
|
|
# 自验证
|
|
|
|
|
|
$verify = $shell.CreateShortcut($lnkPath)
|
|
|
|
|
|
if ($verify.TargetPath -eq $pwshExe -and $verify.Arguments -match [regex]::Escape($claudePs1)) {
|
|
|
|
|
|
Write-Host " [OK] 桌面快捷方式已创建: 启动Bookworm.lnk → pwsh + claude.ps1" -ForegroundColor Green
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Write-Host " [!] 桌面快捷方式自验证失败" -ForegroundColor Yellow
|
|
|
|
|
|
Remove-Item $lnkPath -Force -EA SilentlyContinue
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
Write-Host " [!] 桌面快捷方式创建失败: $_" -ForegroundColor Gray
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 迁移清理老 Bookworm.lnk
|
2026-04-22 01:02:43 +08:00
|
|
|
|
$oldLnk = Join-Path $desktop "Bookworm.lnk"
|
|
|
|
|
|
if ((Test-Path $oldLnk) -and (Test-Path $lnkPath)) {
|
2026-04-25 18:19:07 +08:00
|
|
|
|
try { Remove-Item -LiteralPath $oldLnk -Force -ErrorAction Stop } catch {}
|
2026-04-22 01:02:43 +08:00
|
|
|
|
}
|
2026-04-05 23:34:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 22:47:04 +08:00
|
|
|
|
function Parse-AuthCode {
|
|
|
|
|
|
param([string]$code)
|
|
|
|
|
|
$code = $code.Trim()
|
|
|
|
|
|
# 格式: BW-YYYYMMDD-24位HexToken
|
|
|
|
|
|
if ($code -notmatch '^BW-(\d{8})-([A-Fa-f0-9]{24})$') {
|
2026-04-06 22:57:47 +08:00
|
|
|
|
Write-Host " [!!] 格式错误,应为 BW-YYYYMMDD-XXXXXXXXXXXXXXXXXXXXXXXX" -ForegroundColor Red
|
2026-04-06 22:47:04 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 23:39:17 +08:00
|
|
|
|
function Resolve-SecretsFile {
|
|
|
|
|
|
param([string]$token)
|
|
|
|
|
|
# 优先找 secrets-{token前8位}.enc (多用户独立 Key),回退 secrets.enc
|
|
|
|
|
|
$fileId = $token.Substring(0, 8)
|
|
|
|
|
|
$perUser = Join-Path $ScriptDir "secrets-$fileId.enc"
|
|
|
|
|
|
if (Test-Path $perUser) { return $perUser }
|
|
|
|
|
|
if (Test-Path $SecretsEnc) { return $SecretsEnc }
|
|
|
|
|
|
return $null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-05 23:34:27 +08:00
|
|
|
|
function Decrypt-Secrets {
|
2026-04-06 23:39:17 +08:00
|
|
|
|
if ($SkipSecrets) { return }
|
2026-04-05 23:34:27 +08:00
|
|
|
|
|
|
|
|
|
|
# 优先用 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"
|
|
|
|
|
|
|
2026-04-06 22:57:47 +08:00
|
|
|
|
$validAttempts = 0
|
|
|
|
|
|
$totalAttempts = 0
|
|
|
|
|
|
while ($validAttempts -lt 3 -and $totalAttempts -lt 10) {
|
|
|
|
|
|
$totalAttempts++
|
|
|
|
|
|
$label = if ($validAttempts -gt 0) { " 重新输入授权码 (第 $($validAttempts+1)/3 次)" } else { " 输入授权码 (格式: BW-YYYYMMDD-...)" }
|
2026-04-06 22:47:04 +08:00
|
|
|
|
$authCodeRaw = Read-Host $label
|
|
|
|
|
|
$plainPwd = Parse-AuthCode $authCodeRaw
|
2026-04-06 23:39:17 +08:00
|
|
|
|
if (-not $plainPwd) { continue }
|
|
|
|
|
|
$validAttempts++
|
|
|
|
|
|
|
|
|
|
|
|
# 按 token 前8位定位 .enc 文件
|
|
|
|
|
|
$encFile = Resolve-SecretsFile $plainPwd
|
|
|
|
|
|
if (-not $encFile) {
|
|
|
|
|
|
Write-Host " [!!] 未找到对应的凭证文件 (secrets-*.enc / secrets.enc)" -ForegroundColor Red
|
|
|
|
|
|
Write-Host " 请确认管理员已推送对应文件到 Gitea 并重新拉取" -ForegroundColor Yellow
|
|
|
|
|
|
$plainPwd = $null
|
2026-04-06 22:47:04 +08:00
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-04-05 23:34:27 +08:00
|
|
|
|
|
|
|
|
|
|
$prevEAP = $ErrorActionPreference
|
|
|
|
|
|
$ErrorActionPreference = "Continue"
|
|
|
|
|
|
|
|
|
|
|
|
if ($useNode) {
|
2026-04-06 23:39:17 +08:00
|
|
|
|
$decrypted = & node $cryptoHelper decrypt $plainPwd $encFile 2>&1
|
2026-04-05 23:34:27 +08:00
|
|
|
|
$decExit = $LASTEXITCODE
|
|
|
|
|
|
} else {
|
2026-04-06 23:39:17 +08:00
|
|
|
|
$decrypted = $plainPwd | & $opensslCmd enc -aes-256-cbc -d -pbkdf2 -iter 600000 -md sha256 -in $encFile -pass stdin 2>&1
|
2026-04-05 23:34:27 +08:00
|
|
|
|
$decExit = $LASTEXITCODE
|
|
|
|
|
|
}
|
|
|
|
|
|
$ErrorActionPreference = $prevEAP
|
|
|
|
|
|
$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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 22:57:47 +08:00
|
|
|
|
$remaining = 3 - $validAttempts
|
2026-04-05 23:34:27 +08:00
|
|
|
|
if ($remaining -gt 0) {
|
2026-04-06 22:57:47 +08:00
|
|
|
|
Write-Host " [!!] 授权码无效 (解密失败),剩余重试: $remaining 次" -ForegroundColor Red
|
2026-04-05 23:34:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Write-Host ""
|
2026-04-06 22:57:47 +08:00
|
|
|
|
Write-Host " [ABORT] 3 次授权码均无效,凭证未解密" -ForegroundColor Red
|
|
|
|
|
|
Write-Host " 请确认授权码是否正确,或联系管理员重新生成" -ForegroundColor Yellow
|
2026-04-05 23:34:27 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-04-24 18:59:16 +08:00
|
|
|
|
# v3.0.5: 再次验证 — StartOnly 场景加 GUI 弹窗 (防止 console 闪退用户看不见)
|
|
|
|
|
|
# 触发条件: 用户双击老快捷方式, 但 Phase 1 之前失败导致 claude/node 未装
|
|
|
|
|
|
function Show-MissingDepGui {
|
|
|
|
|
|
param([string]$depName, [string]$installCmd)
|
|
|
|
|
|
try {
|
|
|
|
|
|
Add-Type -AssemblyName System.Windows.Forms -EA Stop
|
|
|
|
|
|
$msg = @"
|
|
|
|
|
|
检测到 $depName 未安装,无法启动 Bookworm。
|
|
|
|
|
|
|
|
|
|
|
|
最可能原因:
|
|
|
|
|
|
上次安装器未完成 (Phase 1 环境检测被中断)
|
|
|
|
|
|
|
|
|
|
|
|
【推荐修复】
|
|
|
|
|
|
1. 双击桌面或下载目录的 Bookworm-Setup.exe
|
|
|
|
|
|
2. 安装器会自动补装缺失的依赖
|
|
|
|
|
|
3. 已装好的部分会被跳过, 不会重复
|
|
|
|
|
|
|
|
|
|
|
|
【手动修复】
|
|
|
|
|
|
$installCmd
|
|
|
|
|
|
|
|
|
|
|
|
完成后再次点击启动快捷方式即可。
|
|
|
|
|
|
"@
|
|
|
|
|
|
[System.Windows.Forms.MessageBox]::Show($msg, "Bookworm 启动失败 — $depName 未安装", 'OK', 'Error') | Out-Null
|
|
|
|
|
|
} catch { }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-05 23:34:27 +08:00
|
|
|
|
if (-not (Test-Command "claude")) {
|
|
|
|
|
|
Write-Host "`n [ABORT] Claude Code 未安装" -ForegroundColor Red
|
|
|
|
|
|
Write-Host " 安装: npm i -g @anthropic-ai/claude-code" -ForegroundColor Gray
|
2026-04-24 18:59:16 +08:00
|
|
|
|
if ($StartOnly) { Show-MissingDepGui "Claude Code" "npm i -g @anthropic-ai/claude-code" }
|
2026-04-05 23:34:27 +08:00
|
|
|
|
exit 1
|
|
|
|
|
|
}
|
|
|
|
|
|
if (-not (Test-Command "node")) {
|
|
|
|
|
|
Write-Host "`n [ABORT] Node.js 未安装" -ForegroundColor Red
|
2026-04-24 18:59:16 +08:00
|
|
|
|
if ($StartOnly) { Show-MissingDepGui "Node.js" "https://nodejs.org/zh-cn/download 下载 LTS .msi" }
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
}
|
|
|
|
|
|
if (-not (Test-Command "git")) {
|
|
|
|
|
|
Write-Host "`n [ABORT] Git 未安装" -ForegroundColor Red
|
|
|
|
|
|
if ($StartOnly) { Show-MissingDepGui "Git" "https://git-scm.com/download/win 下载 64-bit" }
|
2026-04-05 23:34:27 +08:00
|
|
|
|
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 }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-24 20:47:16 +08:00
|
|
|
|
# v3.0.5: 阈值按脱敏分发版 (bookworm-portable-config.git) 实际内容定
|
|
|
|
|
|
# 管理员自用的完整版 (bookworm-config.git) 含 90+ skills, 分发版精简到核心 14+
|
2026-04-05 23:34:27 +08:00
|
|
|
|
$skillCount = 0
|
|
|
|
|
|
if (Test-Path $skillsDir) {
|
|
|
|
|
|
$skillCount = (Get-ChildItem $skillsDir -Directory -ErrorAction SilentlyContinue).Count
|
|
|
|
|
|
}
|
2026-04-24 20:47:16 +08:00
|
|
|
|
$bwChecks += @{ Name = "Skills ($skillCount 个)"; OK = ($skillCount -ge 10) }
|
2026-04-05 23:34:27 +08:00
|
|
|
|
|
|
|
|
|
|
# 检查 Hooks
|
|
|
|
|
|
$hookCount = 0
|
|
|
|
|
|
if (Test-Path $hooksDir) {
|
|
|
|
|
|
$hookCount = (Get-ChildItem $hooksDir -Filter "*.js" -File -ErrorAction SilentlyContinue).Count
|
|
|
|
|
|
}
|
2026-04-24 20:47:16 +08:00
|
|
|
|
$bwChecks += @{ Name = "Hooks ($hookCount 个)"; OK = ($hookCount -ge 3) }
|
2026-04-05 23:34:27 +08:00
|
|
|
|
|
|
|
|
|
|
# 检查 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
|
2026-04-24 20:47:16 +08:00
|
|
|
|
Write-Host " ║ [!] 警告: Bookworm 系统核心资产不足 ║" -ForegroundColor Yellow
|
2026-04-05 23:34:27 +08:00
|
|
|
|
Write-Host " ║ Claude Code 将以原生模式启动 (无 Skills/Hooks) ║" -ForegroundColor Yellow
|
2026-04-24 20:47:16 +08:00
|
|
|
|
Write-Host " ║ 建议: 检查网络后不加 -StartOnly 重新运行同步 ║" -ForegroundColor Yellow
|
2026-04-05 23:34:27 +08:00
|
|
|
|
Write-Host " ╚══════════════════════════════════════════════════════╝" -ForegroundColor Yellow
|
|
|
|
|
|
} else {
|
2026-04-24 20:47:16 +08:00
|
|
|
|
Write-Host " [OK] Bookworm 分发版就绪 ($skillCount Skills / $hookCount Hooks / Settings)" -ForegroundColor Green
|
2026-04-05 23:34:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# --- 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-22 01:02:43 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
# StartOnly 路径 (老 Bookworm.lnk 指向此): 跑幂等迁移, 单次 ~10ms
|
|
|
|
|
|
# 让只从不点「更新Bookworm」的老用户也自动完成快捷方式命名统一
|
|
|
|
|
|
New-DesktopShortcuts
|
2026-04-05 23:34:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 20:34:00 +08:00
|
|
|
|
# 启动 Claude Code (同步执行, 窗口类型由调用方 .bat 决定)
|
|
|
|
|
|
if ($SkipLaunch) {
|
|
|
|
|
|
Write-Host " [OK] 安装完成 (由调用方负责启动)" -ForegroundColor Green
|
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
|
|
|
|
}
|