2026-04-06 14:42:51 +08:00
|
|
|
|
<#
|
|
|
|
|
|
.SYNOPSIS
|
|
|
|
|
|
Bookworm Portable - 全自动一键安装器
|
|
|
|
|
|
.DESCRIPTION
|
|
|
|
|
|
全新电脑从零到 Bookworm 完全就绪,最大程度自动化。
|
|
|
|
|
|
7 阶段: 环境检测 → 依赖安装 → 网络诊断 → 仓库克隆 → 凭证解密 → MCP 验证 → 启动
|
|
|
|
|
|
需要人工输入时弹出 GUI 对话框。
|
|
|
|
|
|
.USAGE
|
|
|
|
|
|
.\auto-setup.ps1
|
|
|
|
|
|
.\auto-setup.ps1 -SkipLaunch # 安装但不启动
|
|
|
|
|
|
#>
|
|
|
|
|
|
param(
|
|
|
|
|
|
[switch]$SkipLaunch
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
|
|
|
|
|
|
|
|
# ─── 路径定义 ────────────────────────────────────────
|
2026-04-07 18:28:12 +08:00
|
|
|
|
# PS2EXE 兼容: $MyInvocation.MyCommand.Path 在 EXE 启动时为空,需用 Process MainModule
|
|
|
|
|
|
$ScriptDir = if ($PSScriptRoot) {
|
|
|
|
|
|
$PSScriptRoot
|
|
|
|
|
|
} elseif ([System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName -match '\.exe$') {
|
|
|
|
|
|
Split-Path -Parent ([System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName)
|
|
|
|
|
|
} elseif ($MyInvocation.MyCommand.Path) {
|
|
|
|
|
|
Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$PWD.Path
|
|
|
|
|
|
}
|
2026-04-06 14:42:51 +08:00
|
|
|
|
$ClaudeDir = Join-Path $env:USERPROFILE ".claude"
|
|
|
|
|
|
$BackupDir = Join-Path $env:USERPROFILE ".claude.bw-backup"
|
|
|
|
|
|
$GitUrl = "https://code.letcareme.com/bookworm/bookworm-config.git"
|
|
|
|
|
|
$BootUrl = "https://code.letcareme.com/bookworm/bookworm-boot.git"
|
2026-04-07 18:28:12 +08:00
|
|
|
|
$BootDir = Join-Path $ScriptDir "bookworm-boot"
|
|
|
|
|
|
$SecretsEnc = Join-Path $BootDir "secrets.enc"
|
2026-04-06 14:42:51 +08:00
|
|
|
|
$TOTAL_PHASES = 7
|
|
|
|
|
|
|
|
|
|
|
|
# ─── GUI 初始化 ─────────────────────────────────────
|
|
|
|
|
|
Add-Type -AssemblyName System.Windows.Forms
|
|
|
|
|
|
Add-Type -AssemblyName System.Drawing
|
|
|
|
|
|
[System.Windows.Forms.Application]::EnableVisualStyles()
|
|
|
|
|
|
|
|
|
|
|
|
# ─── 颜色输出 ────────────────────────────────────────
|
|
|
|
|
|
function Log-OK($msg) { Write-Host " [OK] $msg" -ForegroundColor Green }
|
|
|
|
|
|
function Log-Info($msg) { Write-Host " [..] $msg" -ForegroundColor Cyan }
|
|
|
|
|
|
function Log-Warn($msg) { Write-Host " [!] $msg" -ForegroundColor Yellow }
|
|
|
|
|
|
function Log-Fail($msg) { Write-Host " [!!] $msg" -ForegroundColor Red }
|
|
|
|
|
|
function Log-Phase($n, $title) {
|
|
|
|
|
|
Write-Host ""
|
|
|
|
|
|
Write-Host " [$n/$TOTAL_PHASES] $title" -ForegroundColor White -BackgroundColor DarkBlue
|
|
|
|
|
|
Write-Progress -Activity "Bookworm 自动安装" -Status "$title" -PercentComplete ([int]($n / $TOTAL_PHASES * 100))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function Test-Cmd($cmd) { [bool](Get-Command $cmd -ErrorAction SilentlyContinue) }
|
|
|
|
|
|
|
|
|
|
|
|
# ─── GUI 对话框 ─────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
function Show-MsgBox($text, $title = "Bookworm 安装", $buttons = "OK", $icon = "Information") {
|
|
|
|
|
|
[System.Windows.Forms.MessageBox]::Show($text, $title, $buttons, $icon)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 22:47:04 +08:00
|
|
|
|
function Parse-AuthCode-GUI {
|
|
|
|
|
|
param([string]$code)
|
|
|
|
|
|
$code = $code.Trim()
|
|
|
|
|
|
if ($code -notmatch '^BW-(\d{8})-([A-Fa-f0-9]{24})$') { return $null }
|
|
|
|
|
|
$expiryStr = $Matches[1]
|
|
|
|
|
|
$token = $Matches[2].ToLower()
|
|
|
|
|
|
$today = (Get-Date).ToString("yyyyMMdd")
|
|
|
|
|
|
if ([int]$expiryStr -lt [int]$today) { return 'EXPIRED' }
|
|
|
|
|
|
return $token
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function Show-AuthCodeDialog($attempt = 1, $maxAttempts = 3) {
|
2026-04-06 14:42:51 +08:00
|
|
|
|
$form = New-Object System.Windows.Forms.Form
|
2026-04-06 22:47:04 +08:00
|
|
|
|
$form.Text = "Bookworm - 授权码验证 ($attempt/$maxAttempts)"
|
|
|
|
|
|
$form.Size = New-Object System.Drawing.Size(480, 240)
|
2026-04-06 14:42:51 +08:00
|
|
|
|
$form.StartPosition = "CenterScreen"
|
|
|
|
|
|
$form.FormBorderStyle = "FixedDialog"
|
|
|
|
|
|
$form.MaximizeBox = $false
|
|
|
|
|
|
$form.MinimizeBox = $false
|
|
|
|
|
|
$form.TopMost = $true
|
|
|
|
|
|
|
|
|
|
|
|
$label = New-Object System.Windows.Forms.Label
|
2026-04-06 22:47:04 +08:00
|
|
|
|
$label.Location = New-Object System.Drawing.Point(20, 18)
|
|
|
|
|
|
$label.Size = New-Object System.Drawing.Size(440, 36)
|
2026-04-06 22:57:47 +08:00
|
|
|
|
$label.Text = "请输入管理员提供的授权码:`n格式: BW-YYYYMMDD-XXXXXXXXXXXXXXXXXXXXXXXX"
|
2026-04-06 22:47:04 +08:00
|
|
|
|
$label.Font = New-Object System.Drawing.Font("Segoe UI", 9)
|
2026-04-06 14:42:51 +08:00
|
|
|
|
$form.Controls.Add($label)
|
|
|
|
|
|
|
2026-04-06 22:47:04 +08:00
|
|
|
|
# 授权码可见 (用于粘贴验证), 不用 PasswordChar
|
|
|
|
|
|
$codeBox = New-Object System.Windows.Forms.TextBox
|
|
|
|
|
|
$codeBox.Location = New-Object System.Drawing.Point(20, 65)
|
|
|
|
|
|
$codeBox.Size = New-Object System.Drawing.Size(430, 30)
|
|
|
|
|
|
$codeBox.Font = New-Object System.Drawing.Font("Consolas", 11)
|
|
|
|
|
|
$codeBox.CharacterCasing = "Upper" # 自动转大写
|
|
|
|
|
|
$form.Controls.Add($codeBox)
|
|
|
|
|
|
|
|
|
|
|
|
$hint = New-Object System.Windows.Forms.Label
|
|
|
|
|
|
$hint.Location = New-Object System.Drawing.Point(20, 100)
|
|
|
|
|
|
$hint.Size = New-Object System.Drawing.Size(440, 20)
|
|
|
|
|
|
$hint.Text = "提示: 直接粘贴管理员发送的授权码即可 (Ctrl+V)"
|
|
|
|
|
|
$hint.Font = New-Object System.Drawing.Font("Segoe UI", 8)
|
|
|
|
|
|
$hint.ForeColor = [System.Drawing.Color]::Gray
|
|
|
|
|
|
$form.Controls.Add($hint)
|
2026-04-06 14:42:51 +08:00
|
|
|
|
|
|
|
|
|
|
$btnOK = New-Object System.Windows.Forms.Button
|
2026-04-06 22:47:04 +08:00
|
|
|
|
$btnOK.Location = New-Object System.Drawing.Point(250, 145)
|
2026-04-06 14:42:51 +08:00
|
|
|
|
$btnOK.Size = New-Object System.Drawing.Size(90, 35)
|
2026-04-06 22:47:04 +08:00
|
|
|
|
$btnOK.Text = "验证"
|
2026-04-06 14:42:51 +08:00
|
|
|
|
$btnOK.DialogResult = [System.Windows.Forms.DialogResult]::OK
|
|
|
|
|
|
$form.AcceptButton = $btnOK
|
|
|
|
|
|
$form.Controls.Add($btnOK)
|
|
|
|
|
|
|
|
|
|
|
|
$btnCancel = New-Object System.Windows.Forms.Button
|
2026-04-06 22:47:04 +08:00
|
|
|
|
$btnCancel.Location = New-Object System.Drawing.Point(350, 145)
|
2026-04-06 14:42:51 +08:00
|
|
|
|
$btnCancel.Size = New-Object System.Drawing.Size(80, 35)
|
2026-04-06 22:47:04 +08:00
|
|
|
|
$btnCancel.Text = "跳过"
|
2026-04-06 14:42:51 +08:00
|
|
|
|
$btnCancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
|
|
|
|
|
|
$form.CancelButton = $btnCancel
|
|
|
|
|
|
$form.Controls.Add($btnCancel)
|
|
|
|
|
|
|
2026-04-06 22:47:04 +08:00
|
|
|
|
$form.Add_Shown({ $codeBox.Focus() })
|
2026-04-06 14:42:51 +08:00
|
|
|
|
$result = $form.ShowDialog()
|
|
|
|
|
|
|
|
|
|
|
|
if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
|
2026-04-06 22:47:04 +08:00
|
|
|
|
return $codeBox.Text.Trim()
|
2026-04-06 14:42:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
return $null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function Show-GiteaCredentialDialog {
|
|
|
|
|
|
$form = New-Object System.Windows.Forms.Form
|
|
|
|
|
|
$form.Text = "Bookworm - Gitea 登录"
|
|
|
|
|
|
$form.Size = New-Object System.Drawing.Size(420, 280)
|
|
|
|
|
|
$form.StartPosition = "CenterScreen"
|
|
|
|
|
|
$form.FormBorderStyle = "FixedDialog"
|
|
|
|
|
|
$form.MaximizeBox = $false
|
|
|
|
|
|
$form.TopMost = $true
|
|
|
|
|
|
|
|
|
|
|
|
$lblInfo = New-Object System.Windows.Forms.Label
|
|
|
|
|
|
$lblInfo.Location = New-Object System.Drawing.Point(20, 15)
|
|
|
|
|
|
$lblInfo.Size = New-Object System.Drawing.Size(360, 25)
|
|
|
|
|
|
$lblInfo.Text = "输入 Gitea 账号 (code.letcareme.com)"
|
|
|
|
|
|
$lblInfo.Font = New-Object System.Drawing.Font("Segoe UI", 10)
|
|
|
|
|
|
$form.Controls.Add($lblInfo)
|
|
|
|
|
|
|
|
|
|
|
|
$lblUser = New-Object System.Windows.Forms.Label
|
|
|
|
|
|
$lblUser.Location = New-Object System.Drawing.Point(20, 50)
|
|
|
|
|
|
$lblUser.Size = New-Object System.Drawing.Size(80, 25)
|
|
|
|
|
|
$lblUser.Text = "用户名:"
|
|
|
|
|
|
$form.Controls.Add($lblUser)
|
|
|
|
|
|
|
|
|
|
|
|
$txtUser = New-Object System.Windows.Forms.TextBox
|
|
|
|
|
|
$txtUser.Location = New-Object System.Drawing.Point(100, 48)
|
|
|
|
|
|
$txtUser.Size = New-Object System.Drawing.Size(280, 25)
|
|
|
|
|
|
$txtUser.Font = New-Object System.Drawing.Font("Consolas", 11)
|
|
|
|
|
|
$form.Controls.Add($txtUser)
|
|
|
|
|
|
|
|
|
|
|
|
$lblPass = New-Object System.Windows.Forms.Label
|
|
|
|
|
|
$lblPass.Location = New-Object System.Drawing.Point(20, 90)
|
|
|
|
|
|
$lblPass.Size = New-Object System.Drawing.Size(80, 25)
|
|
|
|
|
|
$lblPass.Text = "密码:"
|
|
|
|
|
|
$form.Controls.Add($lblPass)
|
|
|
|
|
|
|
|
|
|
|
|
$txtPass = New-Object System.Windows.Forms.TextBox
|
|
|
|
|
|
$txtPass.Location = New-Object System.Drawing.Point(100, 88)
|
|
|
|
|
|
$txtPass.Size = New-Object System.Drawing.Size(280, 25)
|
|
|
|
|
|
$txtPass.PasswordChar = '*'
|
|
|
|
|
|
$txtPass.Font = New-Object System.Drawing.Font("Consolas", 11)
|
|
|
|
|
|
$form.Controls.Add($txtPass)
|
|
|
|
|
|
|
|
|
|
|
|
$btnOK = New-Object System.Windows.Forms.Button
|
|
|
|
|
|
$btnOK.Location = New-Object System.Drawing.Point(200, 140)
|
|
|
|
|
|
$btnOK.Size = New-Object System.Drawing.Size(90, 35)
|
|
|
|
|
|
$btnOK.Text = "登录"
|
|
|
|
|
|
$btnOK.DialogResult = [System.Windows.Forms.DialogResult]::OK
|
|
|
|
|
|
$form.AcceptButton = $btnOK
|
|
|
|
|
|
$form.Controls.Add($btnOK)
|
|
|
|
|
|
|
|
|
|
|
|
$btnCancel = New-Object System.Windows.Forms.Button
|
|
|
|
|
|
$btnCancel.Location = New-Object System.Drawing.Point(300, 140)
|
|
|
|
|
|
$btnCancel.Size = New-Object System.Drawing.Size(80, 35)
|
|
|
|
|
|
$btnCancel.Text = "取消"
|
|
|
|
|
|
$btnCancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
|
|
|
|
|
|
$form.CancelButton = $btnCancel
|
|
|
|
|
|
$form.Controls.Add($btnCancel)
|
|
|
|
|
|
|
|
|
|
|
|
$form.Add_Shown({ $txtUser.Focus() })
|
|
|
|
|
|
$result = $form.ShowDialog()
|
|
|
|
|
|
|
|
|
|
|
|
if ($result -eq [System.Windows.Forms.DialogResult]::OK -and $txtUser.Text -and $txtPass.Text) {
|
|
|
|
|
|
return @{ User = $txtUser.Text; Pass = $txtPass.Text }
|
|
|
|
|
|
}
|
|
|
|
|
|
return $null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ─── openssl 检测 ────────────────────────────────────
|
|
|
|
|
|
function Find-OpenSSL {
|
|
|
|
|
|
$cmd = Get-Command openssl -ErrorAction SilentlyContinue
|
|
|
|
|
|
if ($cmd) { return $cmd.Source }
|
|
|
|
|
|
$paths = @(
|
|
|
|
|
|
"C:\Program Files\Git\usr\bin\openssl.exe",
|
|
|
|
|
|
"D:\Git\usr\bin\openssl.exe",
|
|
|
|
|
|
"C:\Program Files\Git\mingw64\bin\openssl.exe",
|
|
|
|
|
|
"D:\Git\mingw64\bin\openssl.exe"
|
|
|
|
|
|
)
|
|
|
|
|
|
return $paths | Where-Object { Test-Path $_ } | Select-Object -First 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ─── 凭证缓存 (Windows Credential Manager) ─────────
|
|
|
|
|
|
function Get-CachedSecrets {
|
|
|
|
|
|
try {
|
|
|
|
|
|
$regPath = "HKCU:\Software\Bookworm\CachedEnv"
|
|
|
|
|
|
if (-not (Test-Path $regPath)) { return $false }
|
|
|
|
|
|
$expiry = (Get-ItemProperty $regPath -Name "_expiry" -ErrorAction SilentlyContinue)._expiry
|
|
|
|
|
|
if (-not $expiry -or [datetime]$expiry -le (Get-Date)) {
|
|
|
|
|
|
Remove-Item $regPath -Recurse -Force -ErrorAction SilentlyContinue
|
|
|
|
|
|
return $false
|
|
|
|
|
|
}
|
|
|
|
|
|
$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++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return ($loaded -gt 0 -and $env:ANTHROPIC_API_KEY)
|
|
|
|
|
|
} catch { return $false }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function Save-SecretsToCache {
|
|
|
|
|
|
try {
|
|
|
|
|
|
$regPath = "HKCU:\Software\Bookworm\CachedEnv"
|
|
|
|
|
|
if (-not (Test-Path $regPath)) { New-Item $regPath -Force | Out-Null }
|
|
|
|
|
|
$keys = @("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 $keys) {
|
|
|
|
|
|
$v = [System.Environment]::GetEnvironmentVariable($k, "Process")
|
|
|
|
|
|
if ($v) { Set-ItemProperty $regPath -Name $k -Value $v -Force }
|
|
|
|
|
|
}
|
|
|
|
|
|
Set-ItemProperty $regPath -Name "_expiry" -Value (Get-Date).Date.AddDays(1).ToString("o") -Force
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ─── 桌面快捷方式 ──────────────────────────────────
|
|
|
|
|
|
function New-DesktopShortcuts {
|
|
|
|
|
|
try {
|
|
|
|
|
|
$shell = New-Object -ComObject WScript.Shell
|
|
|
|
|
|
$desktop = $shell.SpecialFolders("Desktop")
|
|
|
|
|
|
|
2026-04-07 18:28:12 +08:00
|
|
|
|
# 快速启动 (bat 文件位于 bookworm-boot 仓库内)
|
2026-04-06 14:42:51 +08:00
|
|
|
|
$shortcut = $shell.CreateShortcut("$desktop\Bookworm.lnk")
|
2026-04-07 18:28:12 +08:00
|
|
|
|
$batPath = Join-Path $BootDir "启动Bookworm.bat"
|
|
|
|
|
|
if (-not (Test-Path $batPath)) { $batPath = Join-Path $BootDir "Bookworm-OneClick.bat" }
|
2026-04-06 14:42:51 +08:00
|
|
|
|
$shortcut.TargetPath = $batPath
|
2026-04-07 18:28:12 +08:00
|
|
|
|
$shortcut.WorkingDirectory = $BootDir
|
2026-04-06 14:42:51 +08:00
|
|
|
|
$shortcut.Description = "Bookworm Smart Assistant"
|
|
|
|
|
|
$shortcut.Save()
|
|
|
|
|
|
|
|
|
|
|
|
# 更新启动
|
|
|
|
|
|
$shortcut2 = $shell.CreateShortcut("$desktop\更新Bookworm.lnk")
|
2026-04-07 18:28:12 +08:00
|
|
|
|
$updateBat = Join-Path $BootDir "更新并启动Bookworm.bat"
|
2026-04-06 14:42:51 +08:00
|
|
|
|
if (Test-Path $updateBat) {
|
|
|
|
|
|
$shortcut2.TargetPath = $updateBat
|
2026-04-07 18:28:12 +08:00
|
|
|
|
$shortcut2.WorkingDirectory = $BootDir
|
2026-04-06 14:42:51 +08:00
|
|
|
|
$shortcut2.Description = "更新并启动 Bookworm"
|
|
|
|
|
|
$shortcut2.Save()
|
|
|
|
|
|
}
|
|
|
|
|
|
Log-OK "桌面快捷方式已创建"
|
|
|
|
|
|
} catch { Log-Warn "快捷方式创建失败: $_" }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ========================================================================
|
|
|
|
|
|
# Banner
|
|
|
|
|
|
# ========================================================================
|
|
|
|
|
|
Write-Host ""
|
|
|
|
|
|
Write-Host " +---------------------------------------------------+" -ForegroundColor Cyan
|
|
|
|
|
|
Write-Host " | |" -ForegroundColor Cyan
|
|
|
|
|
|
Write-Host " | Bookworm Portable - 全自动安装器 |" -ForegroundColor Cyan
|
|
|
|
|
|
Write-Host " | 92 Skills / 18 Agents / 34 Hooks |" -ForegroundColor Cyan
|
|
|
|
|
|
Write-Host " | |" -ForegroundColor Cyan
|
|
|
|
|
|
Write-Host " +---------------------------------------------------+" -ForegroundColor Cyan
|
|
|
|
|
|
Write-Host ""
|
|
|
|
|
|
|
|
|
|
|
|
# ========================================================================
|
|
|
|
|
|
# Phase 1: 环境检测 + 依赖自动安装
|
|
|
|
|
|
# ========================================================================
|
|
|
|
|
|
Log-Phase 1 "环境检测 + 依赖自动安装"
|
|
|
|
|
|
|
|
|
|
|
|
# 刷新 PATH
|
|
|
|
|
|
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
|
|
|
|
|
|
|
|
|
|
|
$deps = @(
|
2026-04-06 19:48:48 +08:00
|
|
|
|
@{ Name = "Node.js"; Cmd = "node"; WingetId = "OpenJS.NodeJS.LTS"; NpmPkg = $null; PipPkg = $null }
|
|
|
|
|
|
@{ Name = "Git"; Cmd = "git"; WingetId = "Git.Git"; NpmPkg = $null; PipPkg = $null }
|
|
|
|
|
|
@{ Name = "PowerShell 7"; Cmd = "pwsh"; WingetId = "Microsoft.PowerShell"; NpmPkg = $null; PipPkg = $null }
|
|
|
|
|
|
@{ Name = "Python 3.12"; Cmd = "python"; WingetId = "Python.Python.3.12"; NpmPkg = $null; PipPkg = $null }
|
|
|
|
|
|
@{ Name = "Claude Code"; Cmd = "claude"; WingetId = $null; NpmPkg = "@anthropic-ai/claude-code"; PipPkg = $null }
|
2026-04-06 14:42:51 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
$hasWinget = Test-Cmd "winget"
|
|
|
|
|
|
$installed = @()
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($dep in $deps) {
|
|
|
|
|
|
if (Test-Cmd $dep.Cmd) {
|
|
|
|
|
|
$ver = try { & $dep.Cmd --version 2>$null | Select-Object -First 1 } catch { "installed" }
|
|
|
|
|
|
Log-OK "$($dep.Name) $ver"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log-Warn "$($dep.Name) 未安装, 正在自动安装..."
|
|
|
|
|
|
|
|
|
|
|
|
if ($dep.WingetId -and $hasWinget) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
$output = winget install $dep.WingetId --accept-source-agreements --accept-package-agreements 2>&1
|
|
|
|
|
|
$output | Select-Object -Last 3 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
|
|
|
|
|
|
# 刷新 PATH
|
|
|
|
|
|
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
|
|
|
|
|
if (Test-Cmd $dep.Cmd) {
|
|
|
|
|
|
Log-OK "$($dep.Name) 安装成功"
|
|
|
|
|
|
$installed += $dep.Name
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log-Fail "$($dep.Name) 安装后仍无法找到, 可能需要重启终端"
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
Log-Fail "$($dep.Name) 安装失败: $_"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
elseif ($dep.NpmPkg -and (Test-Cmd "npm")) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
Write-Host " npm i -g $($dep.NpmPkg) ..." -ForegroundColor Gray
|
|
|
|
|
|
npm i -g $dep.NpmPkg 2>&1 | Select-Object -Last 3 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
|
|
|
|
|
|
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
|
|
|
|
|
if (Test-Cmd $dep.Cmd) {
|
|
|
|
|
|
Log-OK "$($dep.Name) 安装成功"
|
|
|
|
|
|
$installed += $dep.Name
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch { Log-Fail "$($dep.Name) npm 安装失败: $_" }
|
|
|
|
|
|
}
|
|
|
|
|
|
elseif (-not $hasWinget) {
|
|
|
|
|
|
Log-Fail "$($dep.Name) 需要手动安装 (winget 不可用)"
|
|
|
|
|
|
Show-MsgBox "$($dep.Name) 未安装且 winget 不可用。`n请手动安装后重新运行。`n`nNode.js: https://nodejs.org`nGit: https://git-scm.com" "缺少依赖" "OK" "Error"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Claude Code 依赖 npm, 需要在 Node.js 安装后再检查
|
|
|
|
|
|
if (-not (Test-Cmd "claude") -and (Test-Cmd "npm")) {
|
|
|
|
|
|
Log-Info "安装 Claude Code..."
|
|
|
|
|
|
npm i -g @anthropic-ai/claude-code 2>&1 | Select-Object -Last 3 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
|
|
|
|
|
|
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
|
|
|
|
|
if (Test-Cmd "claude") { Log-OK "Claude Code 安装成功" } else { Log-Fail "Claude Code 安装失败" }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 19:48:48 +08:00
|
|
|
|
# uv 依赖 Python, 需要在 Python 安装后再检查
|
|
|
|
|
|
if (Test-Cmd "python") {
|
|
|
|
|
|
if (Test-Cmd "uv") {
|
|
|
|
|
|
$uvVer = try { & uv --version 2>$null | Select-Object -First 1 } catch { "installed" }
|
|
|
|
|
|
Log-OK "uv $uvVer"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log-Info "安装 uv (Python 包管理器)..."
|
|
|
|
|
|
try {
|
|
|
|
|
|
& python -m pip install uv --quiet 2>&1 | Select-Object -Last 2 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
|
|
|
|
|
|
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
|
|
|
|
|
# pip install 的 Scripts 目录可能不在 PATH, 追加
|
|
|
|
|
|
$pyScripts = Join-Path (Split-Path (& python -c "import sys; print(sys.executable)") -Parent) "Scripts"
|
|
|
|
|
|
if (Test-Path $pyScripts) { $env:Path += ";$pyScripts" }
|
|
|
|
|
|
if (Test-Cmd "uv") {
|
|
|
|
|
|
Log-OK "uv 安装成功"
|
|
|
|
|
|
$installed += "uv"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log-Warn "uv 安装后未找到, 尝试 uvx 替代检查..."
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch { Log-Warn "uv 安装失败: $_" }
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log-Warn "Python 未安装, 跳过 uv (部分 MCP 不可用)"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 14:42:51 +08:00
|
|
|
|
# OpenSSL (随 Git 安装)
|
|
|
|
|
|
$opensslCmd = Find-OpenSSL
|
|
|
|
|
|
if ($opensslCmd) { Log-OK "OpenSSL: $opensslCmd" } else { Log-Warn "OpenSSL 未找到 (凭证解密可能失败)" }
|
|
|
|
|
|
|
2026-04-06 19:48:48 +08:00
|
|
|
|
# 最终检查 (核心四件套必须)
|
|
|
|
|
|
if (-not (Test-Cmd "node") -or -not (Test-Cmd "git") -or -not (Test-Cmd "claude") -or -not (Test-Cmd "pwsh")) {
|
2026-04-06 14:42:51 +08:00
|
|
|
|
$missing = @()
|
|
|
|
|
|
if (-not (Test-Cmd "node")) { $missing += "Node.js" }
|
|
|
|
|
|
if (-not (Test-Cmd "git")) { $missing += "Git" }
|
2026-04-06 19:48:48 +08:00
|
|
|
|
if (-not (Test-Cmd "pwsh")) { $missing += "PowerShell 7" }
|
2026-04-06 14:42:51 +08:00
|
|
|
|
if (-not (Test-Cmd "claude")) { $missing += "Claude Code" }
|
2026-04-06 19:48:48 +08:00
|
|
|
|
Show-MsgBox "以下核心依赖安装失败: $($missing -join ', ')`n`n请手动安装后重新运行。" "安装中断" "OK" "Error"
|
2026-04-06 14:42:51 +08:00
|
|
|
|
exit 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 19:48:48 +08:00
|
|
|
|
# 定位 pwsh.exe 完整路径 (供后续 settings.json 配置使用)
|
|
|
|
|
|
$PwshPath = (Get-Command pwsh -ErrorAction SilentlyContinue).Source
|
|
|
|
|
|
if (-not $PwshPath) {
|
|
|
|
|
|
# winget 默认安装路径
|
|
|
|
|
|
$defaultPaths = @(
|
|
|
|
|
|
"$env:ProgramFiles\PowerShell\7\pwsh.exe",
|
|
|
|
|
|
"${env:ProgramFiles(x86)}\PowerShell\7\pwsh.exe",
|
|
|
|
|
|
"$env:LOCALAPPDATA\Microsoft\PowerShell\pwsh.exe"
|
|
|
|
|
|
)
|
|
|
|
|
|
$PwshPath = $defaultPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($PwshPath) {
|
|
|
|
|
|
Log-OK "PowerShell 7 路径: $PwshPath"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log-Warn "pwsh 可执行但无法定位完整路径, 使用 'pwsh'"
|
|
|
|
|
|
$PwshPath = "pwsh"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 可选依赖警告 (不阻断)
|
|
|
|
|
|
$optionalMissing = @()
|
|
|
|
|
|
if (-not (Test-Cmd "python")) { $optionalMissing += "Python 3.12" }
|
|
|
|
|
|
if (-not (Test-Cmd "uv")) { $optionalMissing += "uv" }
|
|
|
|
|
|
if ($optionalMissing.Count -gt 0) {
|
|
|
|
|
|
Log-Warn "可选依赖未就绪: $($optionalMissing -join ', ') (部分 MCP 不可用)"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 14:42:51 +08:00
|
|
|
|
if ($installed.Count -gt 0) {
|
|
|
|
|
|
Log-OK "本次新安装: $($installed -join ', ')"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ========================================================================
|
|
|
|
|
|
# Phase 2: 网络诊断
|
|
|
|
|
|
# ========================================================================
|
|
|
|
|
|
Log-Phase 2 "网络诊断"
|
|
|
|
|
|
|
|
|
|
|
|
# 代理检测
|
|
|
|
|
|
$env:NO_PROXY = "bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1"
|
|
|
|
|
|
$env:no_proxy = $env:NO_PROXY
|
|
|
|
|
|
|
|
|
|
|
|
$proxyFound = $false
|
|
|
|
|
|
|
|
|
|
|
|
# .NET 系统代理
|
|
|
|
|
|
if (-not $env:HTTPS_PROXY) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
$proxyUri = [System.Net.WebRequest]::DefaultWebProxy.GetProxy("https://api.anthropic.com")
|
|
|
|
|
|
if ($proxyUri -and $proxyUri.Authority -ne "api.anthropic.com") {
|
|
|
|
|
|
$env:HTTPS_PROXY = "http://$($proxyUri.Authority)"
|
|
|
|
|
|
$env:HTTP_PROXY = $env:HTTPS_PROXY
|
|
|
|
|
|
Log-OK "系统代理: $($env:HTTPS_PROXY)"
|
|
|
|
|
|
$proxyFound = $true
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 注册表 IE 代理
|
|
|
|
|
|
if (-not $proxyFound -and -not $env:HTTPS_PROXY) {
|
|
|
|
|
|
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
|
|
|
|
|
|
Log-OK "IE 代理: $proxy"
|
|
|
|
|
|
$proxyFound = $true
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 端口扫描
|
|
|
|
|
|
if (-not $proxyFound -and -not $env:HTTPS_PROXY) {
|
|
|
|
|
|
$ports = @(7890,7891,7893,10792,10793,10808,10809,1080,1087,8080,8118)
|
|
|
|
|
|
foreach ($port in $ports) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
$tcp = New-Object System.Net.Sockets.TcpClient
|
|
|
|
|
|
$ar = $tcp.BeginConnect("127.0.0.1", $port, $null, $null)
|
|
|
|
|
|
$ok = $ar.AsyncWaitHandle.WaitOne(500)
|
|
|
|
|
|
if ($ok) { $tcp.EndConnect($ar); $tcp.Close()
|
|
|
|
|
|
$env:HTTPS_PROXY = "http://127.0.0.1:$port"
|
|
|
|
|
|
$env:HTTP_PROXY = $env:HTTPS_PROXY
|
|
|
|
|
|
Log-OK "本地代理端口: $port"
|
|
|
|
|
|
$proxyFound = $true
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
$tcp.Close()
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($env:HTTPS_PROXY) { $proxyFound = $true }
|
|
|
|
|
|
|
|
|
|
|
|
if (-not $proxyFound) {
|
|
|
|
|
|
Log-Warn "未检测到代理/VPN"
|
|
|
|
|
|
$r = Show-MsgBox "未检测到代理/VPN 软件。`n国内 Claude Code 需要代理才能启动。`n`n请先启动代理软件 (Clash / V2Ray / 快柠檬),`n然后点击 '重试'。`n`n或点击 '忽略' 继续 (可能失败)。" "网络警告" "AbortRetryIgnore" "Warning"
|
|
|
|
|
|
if ($r -eq "Retry") {
|
|
|
|
|
|
# 重试代理检测
|
|
|
|
|
|
try {
|
|
|
|
|
|
$proxyUri = [System.Net.WebRequest]::DefaultWebProxy.GetProxy("https://api.anthropic.com")
|
|
|
|
|
|
if ($proxyUri -and $proxyUri.Authority -ne "api.anthropic.com") {
|
|
|
|
|
|
$env:HTTPS_PROXY = "http://$($proxyUri.Authority)"
|
|
|
|
|
|
$env:HTTP_PROXY = $env:HTTPS_PROXY
|
|
|
|
|
|
Log-OK "系统代理: $($env:HTTPS_PROXY)"
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
} elseif ($r -eq "Abort") { exit 1 }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Log-OK "NO_PROXY: bww.letcareme.com, code.letcareme.com"
|
|
|
|
|
|
|
|
|
|
|
|
# 连通性测试
|
|
|
|
|
|
Write-Host ""
|
|
|
|
|
|
Log-Info "测试网络连通性..."
|
|
|
|
|
|
|
|
|
|
|
|
$netTests = @(
|
|
|
|
|
|
@{ Name = "Gitea 代码仓库"; Url = "https://code.letcareme.com"; Direct = $true }
|
|
|
|
|
|
@{ Name = "API 中转站"; Url = "https://bww.letcareme.com"; Direct = $true }
|
|
|
|
|
|
@{ Name = "Claude API"; Url = "https://api.anthropic.com"; Direct = $false }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($t in $netTests) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
$req = [System.Net.HttpWebRequest]::Create($t.Url)
|
|
|
|
|
|
$req.Timeout = 8000
|
|
|
|
|
|
$req.Method = "HEAD"
|
|
|
|
|
|
if ($t.Direct) { $req.Proxy = [System.Net.GlobalProxySelection]::GetEmptyWebProxy() }
|
|
|
|
|
|
$resp = $req.GetResponse()
|
|
|
|
|
|
$code = [int]$resp.StatusCode
|
|
|
|
|
|
$resp.Close()
|
|
|
|
|
|
Log-OK "$($t.Name) ($($t.Url)) - HTTP $code"
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
$errMsg = $_.Exception.InnerException.Message
|
|
|
|
|
|
if (-not $errMsg) { $errMsg = $_.Exception.Message }
|
|
|
|
|
|
# 非 200 但能连上也算成功 (如 401, 403)
|
|
|
|
|
|
if ($errMsg -match '40[0-9]|30[0-9]') {
|
|
|
|
|
|
Log-OK "$($t.Name) - 可达 (需认证)"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log-Warn "$($t.Name) - 不可达: $($errMsg.Substring(0, [Math]::Min(60, $errMsg.Length)))"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ========================================================================
|
|
|
|
|
|
# Phase 3: 仓库克隆
|
|
|
|
|
|
# ========================================================================
|
|
|
|
|
|
Log-Phase 3 "同步 Bookworm 配置"
|
|
|
|
|
|
|
|
|
|
|
|
# 配置 git credential helper
|
|
|
|
|
|
git config --global credential.helper store 2>$null
|
|
|
|
|
|
|
|
|
|
|
|
# 克隆/更新 config 仓库 (.claude/)
|
|
|
|
|
|
if (Test-Path (Join-Path $ClaudeDir ".git")) {
|
|
|
|
|
|
Log-Info "配置仓库已存在, 更新中..."
|
|
|
|
|
|
Push-Location $ClaudeDir
|
|
|
|
|
|
try {
|
|
|
|
|
|
$stash = git stash 2>&1
|
|
|
|
|
|
git pull --rebase 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
|
|
|
|
|
|
if ($stash -notmatch 'No local changes') { git stash pop 2>&1 | Out-Null }
|
|
|
|
|
|
Log-OK "配置仓库已更新"
|
|
|
|
|
|
} catch { Log-Warn "git pull 失败, 使用本地版本" }
|
|
|
|
|
|
finally { Pop-Location }
|
|
|
|
|
|
}
|
|
|
|
|
|
elseif (Test-Path $ClaudeDir) {
|
|
|
|
|
|
# 已有 .claude 但非 git — 备份后克隆
|
|
|
|
|
|
Log-Info "备份现有 .claude/ 并克隆..."
|
|
|
|
|
|
if (Test-Path $BackupDir) { Remove-Item $BackupDir -Recurse -Force }
|
|
|
|
|
|
Rename-Item $ClaudeDir $BackupDir
|
|
|
|
|
|
|
|
|
|
|
|
# 可能需要 Gitea 凭证
|
|
|
|
|
|
$cred = Show-GiteaCredentialDialog
|
|
|
|
|
|
if ($cred) {
|
|
|
|
|
|
$credUrl = $GitUrl -replace '://', "://$($cred.User):$($cred.Pass)@"
|
|
|
|
|
|
git clone --depth 1 $credUrl $ClaudeDir 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
git clone --depth 1 $GitUrl $ClaudeDir 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (Test-Path (Join-Path $ClaudeDir "CLAUDE.md")) {
|
|
|
|
|
|
Log-OK "配置仓库克隆成功 (旧目录已备份)"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log-Fail "克隆失败"
|
|
|
|
|
|
if (Test-Path $BackupDir) { Rename-Item $BackupDir $ClaudeDir }
|
|
|
|
|
|
Show-MsgBox "配置仓库克隆失败。`n请检查网络和 Gitea 账号密码。" "克隆失败" "OK" "Error"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else {
|
|
|
|
|
|
# 全新安装
|
|
|
|
|
|
Log-Info "首次安装, 克隆配置仓库..."
|
|
|
|
|
|
$cred = Show-GiteaCredentialDialog
|
|
|
|
|
|
if ($cred) {
|
|
|
|
|
|
$credUrl = $GitUrl -replace '://', "://$($cred.User):$($cred.Pass)@"
|
|
|
|
|
|
git clone --depth 1 $credUrl $ClaudeDir 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
git clone --depth 1 $GitUrl $ClaudeDir 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (Test-Path (Join-Path $ClaudeDir "CLAUDE.md")) {
|
|
|
|
|
|
Log-OK "配置仓库克隆成功"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log-Fail "克隆失败"
|
|
|
|
|
|
Show-MsgBox "配置仓库克隆失败。`n请检查网络连接和 Gitea 账号。" "克隆失败" "OK" "Error"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 创建本地运行时目录
|
|
|
|
|
|
$dirs = @("debug","sessions","cache","backups","telemetry","shell-snapshots","projects","memory")
|
|
|
|
|
|
foreach ($d in $dirs) {
|
|
|
|
|
|
$p = Join-Path $ClaudeDir $d
|
|
|
|
|
|
if (-not (Test-Path $p)) { New-Item -ItemType Directory -Path $p -Force | Out-Null }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 18:28:12 +08:00
|
|
|
|
# ─── 克隆/更新 bookworm-boot (含 crypto-helper.js + secrets-*.enc + install.ps1) ───
|
|
|
|
|
|
if (Test-Path (Join-Path $BootDir ".git")) {
|
|
|
|
|
|
Log-Info "boot 仓库已存在, 更新中..."
|
|
|
|
|
|
Push-Location $BootDir
|
|
|
|
|
|
try {
|
|
|
|
|
|
git pull --rebase 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
|
|
|
|
|
|
Log-OK "boot 仓库已更新"
|
|
|
|
|
|
} catch { Log-Warn "boot 仓库更新失败, 使用本地版本" }
|
|
|
|
|
|
finally { Pop-Location }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log-Info "克隆 boot 仓库 (含解密工具与凭证)..."
|
|
|
|
|
|
if (-not $cred) { $cred = Show-GiteaCredentialDialog }
|
|
|
|
|
|
if ($cred) {
|
|
|
|
|
|
$bootCredUrl = $BootUrl -replace '://', "://$($cred.User):$($cred.Pass)@"
|
|
|
|
|
|
git clone --depth 1 $bootCredUrl $BootDir 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
git clone --depth 1 $BootUrl $BootDir 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (-not (Test-Path (Join-Path $BootDir "crypto-helper.js"))) {
|
|
|
|
|
|
Log-Fail "boot 仓库克隆失败 (crypto-helper.js 缺失)"
|
|
|
|
|
|
Show-MsgBox "boot 仓库克隆失败。`n请检查网络和 Gitea 凭证。" "克隆失败" "OK" "Error"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
}
|
|
|
|
|
|
Log-OK "boot 仓库克隆成功 → $BootDir"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 14:42:51 +08:00
|
|
|
|
# ========================================================================
|
|
|
|
|
|
# Phase 4: 凭证解密 (GUI 弹窗)
|
|
|
|
|
|
# ========================================================================
|
|
|
|
|
|
Log-Phase 4 "凭证解密"
|
|
|
|
|
|
|
|
|
|
|
|
$secretsDecrypted = $false
|
|
|
|
|
|
|
|
|
|
|
|
# 先查缓存
|
|
|
|
|
|
if (Get-CachedSecrets) {
|
|
|
|
|
|
Log-OK "从本日缓存加载凭证 (免密)"
|
|
|
|
|
|
$secretsDecrypted = $true
|
|
|
|
|
|
}
|
2026-04-06 19:48:48 +08:00
|
|
|
|
# 再解密 (缓存命中则跳过)
|
|
|
|
|
|
if (-not $secretsDecrypted) {
|
2026-04-07 18:28:12 +08:00
|
|
|
|
$cryptoHelper = Join-Path $BootDir "crypto-helper.js"
|
2026-04-06 15:04:02 +08:00
|
|
|
|
$useNode = (Test-Cmd "node") -and (Test-Path $cryptoHelper)
|
|
|
|
|
|
if (-not $useNode -and -not $opensslCmd) {
|
|
|
|
|
|
Log-Fail "无解密工具 (需要 Node.js 或 OpenSSL)"
|
|
|
|
|
|
}
|
2026-04-07 18:28:12 +08:00
|
|
|
|
elseif ((Test-Path $SecretsEnc) -or (Get-ChildItem $BootDir -Filter "secrets-*.enc" -ErrorAction SilentlyContinue)) {
|
2026-04-06 22:47:04 +08:00
|
|
|
|
$validAttempts = 0
|
|
|
|
|
|
while ($validAttempts -lt 3) {
|
|
|
|
|
|
$rawCode = Show-AuthCodeDialog ($validAttempts + 1) 3
|
|
|
|
|
|
if (-not $rawCode) {
|
|
|
|
|
|
Log-Warn "用户跳过授权码输入"
|
2026-04-06 14:42:51 +08:00
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 22:47:04 +08:00
|
|
|
|
$token = Parse-AuthCode-GUI $rawCode
|
|
|
|
|
|
if ($token -eq 'EXPIRED') {
|
|
|
|
|
|
Show-MsgBox "授权码已过期。`n请联系管理员获取新授权码。" "授权码过期" "OK" "Warning"
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if (-not $token) {
|
2026-04-06 22:57:47 +08:00
|
|
|
|
Show-MsgBox "格式错误。`n正确格式: BW-YYYYMMDD-XXXXXXXXXXXXXXXXXXXXXXXX`n`n请检查后重新粘贴。" "格式错误" "OK" "Warning"
|
2026-04-06 22:47:04 +08:00
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
$validAttempts++
|
|
|
|
|
|
|
2026-04-06 23:39:17 +08:00
|
|
|
|
# 按 token 前8位定位 .enc 文件 (多用户独立 Key),回退 secrets.enc
|
|
|
|
|
|
$fileId = $token.Substring(0, 8)
|
2026-04-07 18:28:12 +08:00
|
|
|
|
$encFile = Join-Path $BootDir "secrets-$fileId.enc"
|
2026-04-06 23:39:17 +08:00
|
|
|
|
if (-not (Test-Path $encFile)) { $encFile = $SecretsEnc }
|
|
|
|
|
|
if (-not (Test-Path $encFile)) {
|
|
|
|
|
|
Show-MsgBox "未找到对应凭证文件。`n请确认管理员已推送 secrets-$fileId.enc 到 Gitea`n并重新运行安装器(会自动拉取)。" "文件未找到" "OK" "Warning"
|
|
|
|
|
|
$token = $null
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 14:42:51 +08:00
|
|
|
|
try {
|
2026-04-06 15:04:02 +08:00
|
|
|
|
if ($useNode) {
|
2026-04-06 23:39:17 +08:00
|
|
|
|
$decrypted = & node $cryptoHelper decrypt $token $encFile 2>&1
|
2026-04-06 15:04:02 +08:00
|
|
|
|
$decExit = $LASTEXITCODE
|
|
|
|
|
|
} else {
|
2026-04-06 23:39:17 +08:00
|
|
|
|
$decrypted = & $opensslCmd enc -aes-256-cbc -d -pbkdf2 -iter 600000 -in $encFile -pass "pass:$token" 2>$null
|
2026-04-06 15:04:02 +08:00
|
|
|
|
$decExit = $LASTEXITCODE
|
|
|
|
|
|
}
|
2026-04-06 23:39:17 +08:00
|
|
|
|
$token = $null
|
2026-04-06 14:42:51 +08:00
|
|
|
|
|
2026-04-06 15:04:02 +08:00
|
|
|
|
if ($decExit -eq 0 -and $decrypted -and $decrypted -notmatch 'WRONG_PASSWORD|WRONG_FORMAT|bad decrypt|bad magic') {
|
2026-04-06 14:42:51 +08:00
|
|
|
|
$count = 0
|
|
|
|
|
|
foreach ($line in $decrypted -split "`n") {
|
|
|
|
|
|
$line = $line.Trim()
|
|
|
|
|
|
if (-not $line -or $line -notmatch '=') { continue }
|
|
|
|
|
|
$key = ($line -split '=', 2)[0].Trim()
|
|
|
|
|
|
$value = ($line -split '=', 2)[1].Trim()
|
|
|
|
|
|
if ($key -and $value) {
|
|
|
|
|
|
[System.Environment]::SetEnvironmentVariable($key, $value, "Process")
|
|
|
|
|
|
Log-OK "已注入: $key"
|
|
|
|
|
|
$count++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-06 22:47:04 +08:00
|
|
|
|
$decrypted = $null
|
2026-04-06 14:42:51 +08:00
|
|
|
|
$secretsDecrypted = $true
|
|
|
|
|
|
|
2026-04-06 22:47:04 +08:00
|
|
|
|
$cacheResult = Show-MsgBox "授权码验证成功 ($count 个凭证)。`n`n是否缓存至今日 23:59?`n(下次启动免输授权码)" "本日免密" "YesNo" "Question"
|
2026-04-06 14:42:51 +08:00
|
|
|
|
if ($cacheResult -eq "Yes") {
|
|
|
|
|
|
Save-SecretsToCache
|
|
|
|
|
|
Log-OK "凭证已缓存至今日 23:59"
|
|
|
|
|
|
}
|
|
|
|
|
|
break
|
|
|
|
|
|
} else {
|
2026-04-06 22:47:04 +08:00
|
|
|
|
$token = $null
|
|
|
|
|
|
$remaining = 3 - $validAttempts
|
|
|
|
|
|
if ($remaining -gt 0) {
|
2026-04-06 23:39:17 +08:00
|
|
|
|
Show-MsgBox "授权码无效(解密失败),剩余重试: $remaining 次" "验证失败" "OK" "Warning"
|
2026-04-06 14:42:51 +08:00
|
|
|
|
} else {
|
2026-04-06 22:47:04 +08:00
|
|
|
|
Show-MsgBox "3 次验证均失败。`n请联系管理员重新获取授权码。" "解密失败" "OK" "Error"
|
2026-04-06 14:42:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
2026-04-06 22:47:04 +08:00
|
|
|
|
$token = $null
|
2026-04-06 14:42:51 +08:00
|
|
|
|
Log-Warn "解密异常: $_"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-06 23:39:17 +08:00
|
|
|
|
else {
|
|
|
|
|
|
Log-Warn "未找到任何 secrets*.enc,跳过凭证解密"
|
2026-04-06 14:42:51 +08:00
|
|
|
|
}
|
2026-04-06 19:48:48 +08:00
|
|
|
|
} # end if (-not $secretsDecrypted)
|
2026-04-06 14:42:51 +08:00
|
|
|
|
|
|
|
|
|
|
# ========================================================================
|
|
|
|
|
|
# Phase 5: 配置渲染
|
|
|
|
|
|
# ========================================================================
|
|
|
|
|
|
Log-Phase 5 "配置渲染"
|
|
|
|
|
|
|
|
|
|
|
|
$templateFile = Join-Path $ClaudeDir "settings.template.json"
|
|
|
|
|
|
$settingsFile = Join-Path $ClaudeDir "settings.json"
|
|
|
|
|
|
|
|
|
|
|
|
if (Test-Path $templateFile) {
|
|
|
|
|
|
$claudeRoot = $ClaudeDir.Replace('\', '/')
|
|
|
|
|
|
$homeDir = $env:USERPROFILE
|
2026-04-06 19:48:48 +08:00
|
|
|
|
# pwsh 路径转正斜杠供 JSON 使用 (C:/Program Files/PowerShell/7/pwsh.exe)
|
|
|
|
|
|
$pwshJsonPath = if ($PwshPath) { $PwshPath.Replace('\', '/') } else { "pwsh" }
|
2026-04-06 14:42:51 +08:00
|
|
|
|
|
|
|
|
|
|
$content = Get-Content $templateFile -Raw
|
|
|
|
|
|
$content = $content -replace '\{\{CLAUDE_ROOT\}\}', $claudeRoot
|
|
|
|
|
|
$content = $content -replace '\{\{HOME\}\}', ($homeDir -replace '\\', '\\')
|
2026-04-06 19:48:48 +08:00
|
|
|
|
$content = $content -replace '\{\{PWSH_PATH\}\}', ($pwshJsonPath -replace '\\', '\\')
|
2026-04-06 14:42:51 +08:00
|
|
|
|
|
|
|
|
|
|
Set-Content $settingsFile -Value $content -Encoding UTF8
|
2026-04-06 19:48:48 +08:00
|
|
|
|
Log-OK "settings.json 已渲染 (ROOT=$claudeRoot, SHELL=$pwshJsonPath)"
|
2026-04-06 14:42:51 +08:00
|
|
|
|
|
|
|
|
|
|
# settings.local.template.json
|
|
|
|
|
|
$localTpl = Join-Path $ClaudeDir "settings.local.template.json"
|
|
|
|
|
|
$localSet = Join-Path $ClaudeDir "settings.local.json"
|
|
|
|
|
|
if (Test-Path $localTpl) {
|
|
|
|
|
|
$lc = Get-Content $localTpl -Raw
|
|
|
|
|
|
$lc = $lc -replace '\{\{CLAUDE_ROOT\}\}', $claudeRoot
|
|
|
|
|
|
$lc = $lc -replace '\{\{HOME\}\}', ($homeDir -replace '\\', '\\')
|
|
|
|
|
|
$lc = $lc -replace '\{\{USERNAME\}\}', $env:USERNAME
|
2026-04-06 19:48:48 +08:00
|
|
|
|
$lc = $lc -replace '\{\{PWSH_PATH\}\}', ($pwshJsonPath -replace '\\', '\\')
|
2026-04-06 14:42:51 +08:00
|
|
|
|
Set-Content $localSet -Value $lc -Encoding UTF8
|
|
|
|
|
|
Log-OK "settings.local.json 已渲染"
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log-Warn "settings.template.json 不存在, 跳过渲染"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$env:CLAUDE_HOME = $ClaudeDir
|
|
|
|
|
|
|
|
|
|
|
|
# ========================================================================
|
|
|
|
|
|
# Phase 6: MCP 验证 + 自动安装
|
|
|
|
|
|
# ========================================================================
|
2026-04-06 19:48:48 +08:00
|
|
|
|
Log-Phase 6 "MCP 服务验证 + 预安装"
|
2026-04-06 14:42:51 +08:00
|
|
|
|
|
2026-04-06 19:48:48 +08:00
|
|
|
|
# ── 6a: Bookworm 完整性检查 ──
|
2026-04-06 14:42:51 +08:00
|
|
|
|
$skillCount = 0; $hookCount = 0
|
|
|
|
|
|
$skillsDir = Join-Path $ClaudeDir "skills"
|
|
|
|
|
|
$hooksDir = Join-Path $ClaudeDir "hooks"
|
|
|
|
|
|
if (Test-Path $skillsDir) { $skillCount = (Get-ChildItem $skillsDir -Directory -ErrorAction SilentlyContinue).Count }
|
|
|
|
|
|
if (Test-Path $hooksDir) { $hookCount = (Get-ChildItem $hooksDir -Filter "*.js" -File -ErrorAction SilentlyContinue).Count }
|
|
|
|
|
|
|
|
|
|
|
|
$claudeMdOK = $false
|
|
|
|
|
|
$claudeMdPath = Join-Path $ClaudeDir "CLAUDE.md"
|
|
|
|
|
|
if (Test-Path $claudeMdPath) {
|
|
|
|
|
|
$cm = Get-Content $claudeMdPath -Raw -ErrorAction SilentlyContinue
|
|
|
|
|
|
$claudeMdOK = $cm -match "Bookworm"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$settingsOK = $false
|
|
|
|
|
|
if (Test-Path $settingsFile) {
|
|
|
|
|
|
$sc = Get-Content $settingsFile -Raw -ErrorAction SilentlyContinue
|
|
|
|
|
|
$settingsOK = $sc -match '"hooks"'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$checks = @(
|
|
|
|
|
|
@{ Name = "CLAUDE.md (Bookworm 指令)"; OK = $claudeMdOK }
|
|
|
|
|
|
@{ Name = "Skills ($skillCount 个)"; OK = ($skillCount -gt 50) }
|
|
|
|
|
|
@{ Name = "Hooks ($hookCount 个)"; OK = ($hookCount -gt 10) }
|
|
|
|
|
|
@{ Name = "Settings hooks 配置"; OK = $settingsOK }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
$allOK = $true
|
|
|
|
|
|
foreach ($c in $checks) {
|
|
|
|
|
|
if ($c.OK) { Log-OK $c.Name } else { Log-Fail $c.Name; $allOK = $false }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 19:48:48 +08:00
|
|
|
|
# ── 6b: API 凭证检查 ──
|
2026-04-06 14:42:51 +08:00
|
|
|
|
Write-Host ""
|
|
|
|
|
|
Log-Info "API 凭证检查..."
|
|
|
|
|
|
if ($env:ANTHROPIC_API_KEY) { Log-OK "ANTHROPIC_API_KEY 已配置" } else { Log-Fail "ANTHROPIC_API_KEY 未配置" }
|
|
|
|
|
|
if ($env:ANTHROPIC_BASE_URL) { Log-OK "ANTHROPIC_BASE_URL 已配置" } else { Log-Warn "ANTHROPIC_BASE_URL 未配置 (将使用默认)" }
|
|
|
|
|
|
|
2026-04-06 19:48:48 +08:00
|
|
|
|
# ── 6c: MCP npx 包预缓存 ──
|
2026-04-06 14:42:51 +08:00
|
|
|
|
Write-Host ""
|
2026-04-06 19:48:48 +08:00
|
|
|
|
Log-Info "MCP 预安装 (npx 包预缓存)..."
|
|
|
|
|
|
|
|
|
|
|
|
# Tier 1: 纯 npx, 无需 Key
|
|
|
|
|
|
$npxPackages = @(
|
|
|
|
|
|
@{ Name = "context7"; Pkg = "@upstash/context7-mcp@2.1.1" }
|
|
|
|
|
|
@{ Name = "sequential-thinking"; Pkg = "@modelcontextprotocol/server-sequential-thinking@2025.12.18" }
|
|
|
|
|
|
@{ Name = "playwright"; Pkg = "@playwright/mcp@0.0.68" }
|
|
|
|
|
|
@{ Name = "session-continuity"; Pkg = "claude-session-continuity-mcp@1.13.0" }
|
|
|
|
|
|
@{ Name = "notebooklm"; Pkg = "notebooklm-mcp@latest" }
|
|
|
|
|
|
@{ Name = "cloudflare-docs"; Pkg = "mcp-remote" }
|
|
|
|
|
|
@{ Name = "chrome-devtools"; Pkg = "chrome-devtools-mcp@0.18.1" }
|
|
|
|
|
|
# Tier 2: npx + Key
|
|
|
|
|
|
@{ Name = "github"; Pkg = "@modelcontextprotocol/server-github" }
|
|
|
|
|
|
@{ Name = "slack"; Pkg = "@modelcontextprotocol/server-slack" }
|
|
|
|
|
|
@{ Name = "firecrawl"; Pkg = "firecrawl-mcp" }
|
|
|
|
|
|
@{ Name = "mcp-image"; Pkg = "mcp-image" }
|
|
|
|
|
|
@{ Name = "google-drive"; Pkg = "@piotr-agier/google-drive-mcp" }
|
|
|
|
|
|
)
|
2026-04-06 14:42:51 +08:00
|
|
|
|
|
2026-04-06 19:48:48 +08:00
|
|
|
|
$mcpOK = 0; $mcpFail = 0
|
|
|
|
|
|
foreach ($mcp in $npxPackages) {
|
|
|
|
|
|
$idx = $mcpOK + $mcpFail + 1
|
|
|
|
|
|
Write-Host " [$idx/$($npxPackages.Count)] $($mcp.Name) " -NoNewline -ForegroundColor Gray
|
|
|
|
|
|
try {
|
|
|
|
|
|
# npm cache add 只下载到缓存, 不执行 (MCP 是 stdio 长驻进程, 不能用 --help)
|
|
|
|
|
|
$proc = Start-Process npm.cmd -ArgumentList "cache", "add", $mcp.Pkg -NoNewWindow -PassThru -RedirectStandardOutput "NUL" -RedirectStandardError "NUL"
|
|
|
|
|
|
$exited = $proc.WaitForExit(60000) # 60 秒超时
|
|
|
|
|
|
if (-not $exited) { $proc.Kill(); throw "timeout" }
|
|
|
|
|
|
if ($proc.ExitCode -eq 0) {
|
|
|
|
|
|
Write-Host "[cached]" -ForegroundColor Green
|
|
|
|
|
|
$mcpOK++
|
|
|
|
|
|
} else { throw "exit $($proc.ExitCode)" }
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
Write-Host "[FAIL: $_]" -ForegroundColor Red
|
|
|
|
|
|
$mcpFail++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Log-OK "npx 预缓存: $mcpOK 成功, $mcpFail 失败 (共 $($npxPackages.Count))"
|
|
|
|
|
|
|
|
|
|
|
|
# ── 6d: Playwright 浏览器安装 ──
|
|
|
|
|
|
Write-Host ""
|
|
|
|
|
|
Log-Info "Playwright 浏览器安装..."
|
2026-04-06 14:42:51 +08:00
|
|
|
|
try {
|
2026-04-06 19:48:48 +08:00
|
|
|
|
$pwBrowserPath = Join-Path $env:USERPROFILE "AppData\Local\ms-playwright"
|
|
|
|
|
|
if (Test-Path (Join-Path $pwBrowserPath "chromium-*")) {
|
|
|
|
|
|
Log-OK "Playwright Chromium 已存在"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Write-Host " 下载 Chromium (首次约 150MB, 最长等 5 分钟)..." -ForegroundColor Gray
|
|
|
|
|
|
$pwProc = Start-Process npx.cmd -ArgumentList "-y", "playwright", "install", "chromium" -NoNewWindow -PassThru
|
|
|
|
|
|
$pwExited = $pwProc.WaitForExit(300000) # 5 分钟超时
|
|
|
|
|
|
if (-not $pwExited) { $pwProc.Kill(); Log-Warn "Playwright 下载超时, 跳过" }
|
|
|
|
|
|
elseif (Test-Path (Join-Path $pwBrowserPath "chromium-*")) {
|
|
|
|
|
|
Log-OK "Playwright Chromium 安装成功"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log-Warn "Playwright Chromium 安装可能未完成"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch { Log-Warn "Playwright 浏览器安装失败: $_ (不影响核心功能)" }
|
|
|
|
|
|
|
|
|
|
|
|
# ── 6e: Python MCP (uvx) 验证 ──
|
|
|
|
|
|
Write-Host ""
|
|
|
|
|
|
if (Test-Cmd "uvx") {
|
|
|
|
|
|
Log-Info "Python MCP 验证 (uvx)..."
|
|
|
|
|
|
|
|
|
|
|
|
$uvxPackages = @(
|
|
|
|
|
|
@{ Name = "windows-mcp"; Args = @("--python", "3.13", "windows-mcp") }
|
|
|
|
|
|
@{ Name = "atlassian"; Args = @("mcp-atlassian") }
|
|
|
|
|
|
)
|
2026-04-06 14:42:51 +08:00
|
|
|
|
|
2026-04-06 19:48:48 +08:00
|
|
|
|
foreach ($pkg in $uvxPackages) {
|
|
|
|
|
|
Write-Host " $($pkg.Name) " -NoNewline -ForegroundColor Gray
|
|
|
|
|
|
try {
|
|
|
|
|
|
# uv tool install 只下载不启动 (MCP 是 stdio 长驻进程, --help 会挂起)
|
|
|
|
|
|
$installArgs = @("tool", "install") + $pkg.Args
|
|
|
|
|
|
$proc = Start-Process uv -ArgumentList $installArgs -NoNewWindow -PassThru -RedirectStandardOutput "NUL" -RedirectStandardError "NUL"
|
|
|
|
|
|
$exited = $proc.WaitForExit(90000) # 90 秒超时 (Python 包较大)
|
|
|
|
|
|
if (-not $exited) { $proc.Kill(); throw "timeout" }
|
|
|
|
|
|
Write-Host "[ready]" -ForegroundColor Green
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
Write-Host "[skip: $_]" -ForegroundColor Yellow
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-06 14:42:51 +08:00
|
|
|
|
} else {
|
2026-04-06 19:48:48 +08:00
|
|
|
|
Log-Warn "uvx 不可用, 跳过 Python MCP (windows-mcp, atlassian)"
|
2026-04-06 14:42:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 19:48:48 +08:00
|
|
|
|
# ── 6f: 可选 API Key 提示 ──
|
2026-04-06 14:42:51 +08:00
|
|
|
|
$optional = @(
|
|
|
|
|
|
@{ Key = "GITHUB_PERSONAL_ACCESS_TOKEN"; Name = "GitHub MCP" }
|
|
|
|
|
|
@{ Key = "FIRECRAWL_API_KEY"; Name = "Firecrawl MCP" }
|
|
|
|
|
|
@{ Key = "SLACK_BOT_TOKEN"; Name = "Slack MCP" }
|
2026-04-06 19:48:48 +08:00
|
|
|
|
@{ Key = "BROWSERBASE_API_KEY"; Name = "Browserbase MCP" }
|
|
|
|
|
|
@{ Key = "GEMINI_API_KEY"; Name = "MCP Image / Browserbase" }
|
|
|
|
|
|
@{ Key = "ATLASSIAN_API_TOKEN"; Name = "Atlassian MCP" }
|
2026-04-06 14:42:51 +08:00
|
|
|
|
)
|
|
|
|
|
|
$missingOpt = $optional | Where-Object { -not [System.Environment]::GetEnvironmentVariable($_.Key, "Process") }
|
|
|
|
|
|
if ($missingOpt.Count -gt 0) {
|
|
|
|
|
|
Write-Host ""
|
2026-04-06 19:48:48 +08:00
|
|
|
|
Write-Host " 可选 MCP (Key 未配置, 不影响核心功能):" -ForegroundColor DarkGray
|
|
|
|
|
|
foreach ($m in $missingOpt) { Write-Host " [-] $($m.Name) ($($m.Key))" -ForegroundColor DarkGray }
|
2026-04-06 14:42:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ========================================================================
|
|
|
|
|
|
# Phase 7: 完成 + 启动
|
|
|
|
|
|
# ========================================================================
|
|
|
|
|
|
Log-Phase 7 "安装完成"
|
|
|
|
|
|
|
|
|
|
|
|
Write-Progress -Activity "Bookworm 自动安装" -Completed
|
|
|
|
|
|
|
|
|
|
|
|
# 创建桌面快捷方式
|
|
|
|
|
|
New-DesktopShortcuts
|
|
|
|
|
|
|
|
|
|
|
|
Write-Host ""
|
|
|
|
|
|
if ($allOK -and $env:ANTHROPIC_API_KEY) {
|
|
|
|
|
|
Write-Host " +---------------------------------------------------+" -ForegroundColor Green
|
|
|
|
|
|
Write-Host " | |" -ForegroundColor Green
|
|
|
|
|
|
Write-Host " | Bookworm Portable 安装成功! |" -ForegroundColor Green
|
|
|
|
|
|
Write-Host " | $skillCount Skills / $hookCount Hooks / 全部就绪 |" -ForegroundColor Green
|
|
|
|
|
|
Write-Host " | |" -ForegroundColor Green
|
|
|
|
|
|
Write-Host " +---------------------------------------------------+" -ForegroundColor Green
|
|
|
|
|
|
|
|
|
|
|
|
if (-not $SkipLaunch) {
|
|
|
|
|
|
Write-Host ""
|
|
|
|
|
|
Log-Info "正在启动 Claude Code..."
|
|
|
|
|
|
Write-Host ""
|
2026-04-06 20:34:00 +08:00
|
|
|
|
& claude --dangerously-skip-permissions
|
2026-04-06 14:42:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Write-Host " +---------------------------------------------------+" -ForegroundColor Yellow
|
|
|
|
|
|
Write-Host " | |" -ForegroundColor Yellow
|
|
|
|
|
|
Write-Host " | 安装完成 (部分功能可能受限) |" -ForegroundColor Yellow
|
|
|
|
|
|
Write-Host " | |" -ForegroundColor Yellow
|
|
|
|
|
|
Write-Host " +---------------------------------------------------+" -ForegroundColor Yellow
|
|
|
|
|
|
|
|
|
|
|
|
$issues = @()
|
|
|
|
|
|
if (-not $allOK) { $issues += "- Bookworm 配置不完整" }
|
|
|
|
|
|
if (-not $env:ANTHROPIC_API_KEY) { $issues += "- API 凭证未解密" }
|
|
|
|
|
|
$issueText = $issues -join "`n"
|
|
|
|
|
|
|
|
|
|
|
|
$launchResult = Show-MsgBox "安装完成, 但存在以下问题:`n$issueText`n`n是否仍然启动 Claude Code?`n(将以受限模式运行)" "安装警告" "YesNo" "Warning"
|
|
|
|
|
|
if ($launchResult -eq "Yes" -and -not $SkipLaunch) {
|
2026-04-06 20:34:00 +08:00
|
|
|
|
& claude --dangerously-skip-permissions
|
2026-04-06 14:42:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|