sync: v3.0.1 auto-setup.ps1 + Setup.sh (P0 fixes)

This commit is contained in:
Bookworm 2026-04-21 01:54:42 +08:00
parent 9668a58480
commit 080ff71653
2 changed files with 2096 additions and 1750 deletions

View File

@ -211,6 +211,25 @@ done
# ============================================================
step 4 "解密凭证"
# ─── v3.0.1: $BW_LICENSE_KEY 静默激活 (零输入路径) ───
# 若 install.sh 通过 env 传入 License Key (BW-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX), 优先走这条
# activate.js 已支持 HTTPS_PROXY 的 HTTP CONNECT 隧道 (Gitea ce354ca)
ACTIVATE_JS="$CLAUDE_DIR/lib/activate.js"
BW_TOKEN_FILE="$HOME/.claude/.bw-token"
if [ -n "$BW_LICENSE_KEY" ] && [[ "$BW_LICENSE_KEY" =~ ^BW-[A-F0-9]{4}(-[A-F0-9]{4}){5}$ ]] && [ -f "$ACTIVATE_JS" ] && command -v node &>/dev/null; then
info "检测到 \$BW_LICENSE_KEY, 静默激活..."
if printf '%s' "$BW_LICENSE_KEY" | node "$ACTIVATE_JS" 2>&1 | tail -3 | grep -q "OK\|激活成功"; then
if [ -f "$BW_TOKEN_FILE" ]; then
success "License 静默激活成功"
else
warn "activate.js 返回 OK 但 .bw-token 未生成, 回退到交互模式"
fi
else
warn "静默激活失败, 回退到交互模式 (中转站 sk-Key 流程)"
fi
unset BW_LICENSE_KEY # 清掉, 不在子进程泄露
fi
# Keychain 缓存相关
KEYCHAIN_SERVICE="bookworm-secrets"
KEYCHAIN_ACCOUNT="$(whoami)"
@ -349,6 +368,92 @@ else
fi
fi
# 优先级 3.5: v3.0.1 新增 — 直接输入 sk- Key (中转站 Key) + 5 模型候选验证
# 适用: fresh install 没 change-key.js, 没 .enc 文件的新用户 (BYOK)
if [ -z "$ANTHROPIC_API_KEY" ]; then
# 测 sk- Key 是否可调通 (5 模型候选, 中转站白名单)
validate_sk_key() {
local key="$1"
local baseurl="${ANTHROPIC_BASE_URL:-https://bww.letcareme.com}"
local models=("claude-opus-4-7" "claude-opus-4-6" "claude-opus-4-6-thinking" "claude-sonnet-4-6" "claude-sonnet-4-6-thinking")
for model in "${models[@]}"; do
local code
code=$(curl -sS -o /dev/null -w "%{http_code}" --max-time 15 --noproxy '*' \
-X POST "$baseurl/v1/messages" \
-H "x-api-key: $key" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d "{\"model\":\"$model\",\"max_tokens\":1,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}" 2>/dev/null)
# 401/403 认证失败, 立即退, 不继续试
[[ "$code" == "401" || "$code" == "403" ]] && { echo "AUTH_FAIL"; return 1; }
# 200 或 400 都说明 Key 通过, 400 只是请求体问题
[[ "$code" == "200" || "$code" == "400" ]] && { echo "OK"; return 0; }
# 503/404 继续试下个模型
done
echo "NO_CHANNEL" # 全部 503 = 中转站无渠道
return 1
}
echo ""
info "配置中转站 API Key (没有的话去 bww.letcareme.com 注册+充值)"
for attempt in 1 2 3; do
echo ""
read -rs -p " 粘贴 sk- Key (第 $attempt/3 次, 输入不显示, 留空跳过): " SK_KEY
echo ""
[ -z "$SK_KEY" ] && { warn "已跳过"; break; }
# 基础格式校验
if [[ ! "$SK_KEY" =~ ^sk- ]] || [ ${#SK_KEY} -lt 20 ]; then
warn "格式错误 (应 sk- 开头, 至少 20 字符), 请重试"
continue
fi
info "验证中 (试 5 个模型候选)..."
result=$(validate_sk_key "$SK_KEY")
case "$result" in
OK)
success "sk- Key 验证成功"
# v3.0.1: chmod 600 防同机其它 uid 读取 + 清 .bak 残留 (red-team-attacker P0)
for rc in "$HOME/.zshrc" "$HOME/.bashrc"; do
[ -f "$rc" ] || touch "$rc"
# BSD sed (macOS 默认): -i '' 无 .bak; GNU sed (Linux): -i 无 .bak
if sed --version 2>/dev/null | grep -q GNU; then
sed -i '/^export ANTHROPIC_API_KEY=/d' "$rc" 2>/dev/null || true
sed -i '/^export ANTHROPIC_BASE_URL=/d' "$rc" 2>/dev/null || true
else
sed -i '' '/^export ANTHROPIC_API_KEY=/d' "$rc" 2>/dev/null || true
sed -i '' '/^export ANTHROPIC_BASE_URL=/d' "$rc" 2>/dev/null || true
fi
echo "export ANTHROPIC_API_KEY=\"$SK_KEY\"" >> "$rc"
echo "export ANTHROPIC_BASE_URL=\"https://bww.letcareme.com\"" >> "$rc"
chmod 600 "$rc" # 只 owner 可读, 防同机 uid 泄露
done
# 扫残留 .bak 副本 (可能含旧 Key)
rm -f "$HOME/.zshrc.bak" "$HOME/.bashrc.bak" 2>/dev/null || true
export ANTHROPIC_API_KEY="$SK_KEY"
export ANTHROPIC_BASE_URL="https://bww.letcareme.com"
# 存 Keychain 本日免密
security add-generic-password -s "$KEYCHAIN_SERVICE" -a "$KEYCHAIN_ACCOUNT" -w "ANTHROPIC_API_KEY=$SK_KEY
ANTHROPIC_BASE_URL=https://bww.letcareme.com
EXPIRY=$(date -v+1d -u +%FT%TZ 2>/dev/null || date -u -d '+1 day' +%FT%TZ)" -U 2>/dev/null || true
SK_KEY=""
break
;;
AUTH_FAIL)
warn "Key 无效或余额为 0 (中转站返回 401/403)"
SK_KEY=""
[ $attempt -lt 3 ] && continue || { fail "3 次失败, 跳过 sk- 配置"; break; }
;;
NO_CHANNEL)
fail "中转站没有可用 Claude 渠道 (5 模型全返 503). 联系中转站客服"
SK_KEY=""
break
;;
*)
warn "验证异常, 剩余 $((3-attempt))"
SK_KEY=""
;;
esac
done
fi
# 优先级 4: 授权码模式 (向后兼容旧用户)
if [ -z "$ANTHROPIC_API_KEY" ] && { [ -f "$SECRETS_ENC" ] || ls "$BOOT_DIR"/secrets-*.enc 2>/dev/null | head -1 | grep -q .; }; then
DECRYPTED=""
@ -495,7 +600,7 @@ if ! grep -q "$ALIAS_MARKER" "$SHELL_RC" 2>/dev/null; then
cat >> "$SHELL_RC" << 'ALIASES'
# Bookworm Portable aliases
alias bw='NO_PROXY="bww.letcareme.com,code.letcareme.com,localhost,127.0.0.1" claude --dangerously-skip-permissions'
alias bw='NO_PROXY="bww.letcareme.com,code.letcareme.com,localhost,127.0.0.1" ANTHROPIC_MODEL="${ANTHROPIC_MODEL:-claude-opus-4-7}" claude --dangerously-skip-permissions'
alias bw-update='cd ~/bookworm-boot && git pull && cd ~/.claude && git pull && echo "Updated!"'
ALIASES
success "已添加到 $SHELL_RC:"
@ -509,7 +614,7 @@ else
cat >> "$SHELL_RC" << 'ALIASES'
# Bookworm Portable aliases
alias bw='NO_PROXY="bww.letcareme.com,code.letcareme.com,localhost,127.0.0.1" claude --dangerously-skip-permissions'
alias bw='NO_PROXY="bww.letcareme.com,code.letcareme.com,localhost,127.0.0.1" ANTHROPIC_MODEL="${ANTHROPIC_MODEL:-claude-opus-4-7}" claude --dangerously-skip-permissions'
alias bw-update='cd ~/bookworm-boot && git pull && cd ~/.claude && git pull && echo "Updated!"'
ALIASES
success "终端别名已更新 (bookworm → bw)"
@ -547,5 +652,7 @@ if [ "$START_NOW" = "y" ] || [ "$START_NOW" = "Y" ]; then
info "正在启动 Claude Code..."
cd "$HOME"
export NO_PROXY="bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1"
# v3.0.1: 默认模型 (中转站兼容, 默认 claude-sonnet-4-5 会 503)
export ANTHROPIC_MODEL="${ANTHROPIC_MODEL:-claude-opus-4-7}"
exec claude --dangerously-skip-permissions
fi

View File

@ -10,13 +10,21 @@
.\auto-setup.ps1 -SkipLaunch # 安装但不启动
#>
param(
[switch]$SkipLaunch
[switch]$SkipLaunch,
# DryRun: 只跑指定 Phase 做验证, 不落任何副作用. 空值 = 正常全流程安装
# 值: env (Phase 1 环境检测) / deps (Phase 2 依赖安装) / net (Phase 3 网络诊断)
# / repo (Phase 3 git clone) / creds (Phase 4 凭证) / license (Phase 4.5)
[string]$DryRun = ""
)
$ErrorActionPreference = "Stop"
# ─── 版本号 (每次更新递增, build.ps1 自动读取) ──────
$BWVersion = "3.0.0-beta"
$BWVersion = "3.0.1" # +DryRun 支持 +Test-ApiKey Proxy=new() 直连 +UTF-8 BOM 处理
# DryRun 模式日志标记
if ($DryRun) { $global:BWDryRun = $DryRun } else { $global:BWDryRun = $null }
function Is-DryRun($phase) { return ($global:BWDryRun -eq $phase -or $global:BWDryRun -eq "all") }
# ─── B4: 单实例保护 (防止双击两次导致竞态) ─────────
$mutexCreated = $false
@ -418,37 +426,117 @@ function Show-ApiKeyDialog($attempt = 1, $maxAttempts = 3, $existingKey = "") {
return $null
}
# ─── Bookworm License Key 输入对话框 (v3.0 专用, 格式 BW-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX) ──
function Show-LicenseKeyDialog($attempt = 1, $maxAttempts = 3) {
$form = New-Object System.Windows.Forms.Form
$form.Text = "Bookworm - License 激活 ($attempt/$maxAttempts)"
$form.Size = New-Object System.Drawing.Size(540, 270)
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedDialog"
$form.MaximizeBox = $false
$form.MinimizeBox = $false
$form.TopMost = $true
$form.BackColor = [System.Drawing.Color]::White
$lblInfo = New-Object System.Windows.Forms.Label
$lblInfo.Location = New-Object System.Drawing.Point(20, 15)
$lblInfo.Size = New-Object System.Drawing.Size(490, 46)
$lblInfo.Text = "请粘贴 Bookworm License Key`n格式: BW-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX (6 段 4 位十六进制)"
$lblInfo.Font = New-Object System.Drawing.Font("Segoe UI", 9)
$form.Controls.Add($lblInfo)
$keyBox = New-Object System.Windows.Forms.TextBox
$keyBox.Location = New-Object System.Drawing.Point(20, 70)
$keyBox.Size = New-Object System.Drawing.Size(490, 30)
$keyBox.Font = New-Object System.Drawing.Font("Consolas", 11)
$keyBox.CharacterCasing = "Upper"
$keyBox.MaxLength = 34
$form.Controls.Add($keyBox)
$lblHint = New-Object System.Windows.Forms.Label
$lblHint.Location = New-Object System.Drawing.Point(20, 110)
$lblHint.Size = New-Object System.Drawing.Size(490, 60)
$lblHint.Text = "提示: 大写字母 + 数字, 中间 5 个短横线`n常见混淆: O→0 (零), I→1 (一), L→1 (一)`n没 Key? 联系管理员获取"
$lblHint.Font = New-Object System.Drawing.Font("Segoe UI", 8)
$lblHint.ForeColor = [System.Drawing.Color]::FromArgb(100, 110, 130)
$form.Controls.Add($lblHint)
$btnOK = New-Object System.Windows.Forms.Button
$btnOK.Location = New-Object System.Drawing.Point(300, 185)
$btnOK.Size = New-Object System.Drawing.Size(100, 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(410, 185)
$btnCancel.Size = New-Object System.Drawing.Size(100, 35)
$btnCancel.Text = "跳过"
$btnCancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$form.CancelButton = $btnCancel
$form.Controls.Add($btnCancel)
$form.Add_Shown({ $keyBox.Focus() })
$result = $form.ShowDialog()
if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
return $keyBox.Text.Trim()
}
return $null
}
# 向中转站发一个最小请求验证 key 是否可用
function Test-ApiKey([string]$apiKey, [string]$baseUrl = "https://bww.letcareme.com") {
if (-not $apiKey -or $apiKey.Length -lt 10) { return $false }
# 中转站/官方的模型命名差异大, 依次试候选列表, 任一 2xx/4xx(非认证) 即认 Key 有效
# 顺序: 当前主流 (4-6/4-7) → 老版兼容 (4-5/3-5)
# 中转站 (bww.letcareme.com) 目前限定这 5 个模型, 严格匹配防 503 误判
# 默认主模型: claude-opus-4-7
$modelCandidates = @(
"claude-opus-4-7",
"claude-opus-4-6",
"claude-opus-4-6-thinking",
"claude-sonnet-4-6",
"claude-sonnet-4-6-thinking"
)
# v3.0.1 错误分类 (red-team-logic P0):
# $true = 认证通过 (200/400)
# $false = 认证失败 (401/403) 明确 Key 无效
# $null = 网络/中转站故障 (5xx/404/timeout), 非 Key 问题, 外层应放行
$lastStatus = 0
$hadNetworkError = $false
foreach ($model in $modelCandidates) {
try {
$body = '{"model":"claude-sonnet-4-5","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}'
$body = '{"model":"' + $model + '","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}'
$req = [System.Net.WebRequest]::Create("$baseUrl/v1/messages")
$req.Method = "POST"
$req.ContentType = "application/json"
$req.Headers["x-api-key"] = $apiKey
$req.Headers["anthropic-version"] = "2023-06-01"
# 中转站 bww.letcareme.com 对境外 IP 返 503, 强制直连绕代理
$req.Proxy = [System.Net.WebProxy]::new()
$req.Timeout = 10000
$bytes = [System.Text.Encoding]::UTF8.GetBytes($body)
$req.ContentLength = $bytes.Length
$stream = $req.GetRequestStream()
$stream.Write($bytes, 0, $bytes.Length)
$stream.Close()
$resp = $req.GetResponse()
$resp.Close()
return $true
$stream = $req.GetRequestStream(); $stream.Write($bytes, 0, $bytes.Length); $stream.Close()
$resp = $req.GetResponse(); $resp.Close()
return $true # 200 = Key 有效
} catch [System.Net.WebException] {
# 401/403 = 认证失败, 其他网络错误也返回 false
$statusCode = 0
try { $statusCode = [int]$_.Exception.Response.StatusCode } catch {}
# 如果返回 200/400 说明 key 有效 (400 可能是请求体问题, 但 key 本身通过)
if ($statusCode -ge 200 -and $statusCode -lt 500 -and $statusCode -ne 401 -and $statusCode -ne 403) {
return $true
$lastStatus = 0
try { $lastStatus = [int]$_.Exception.Response.StatusCode } catch {}
# 401/403 = 明确认证失败, 立即返回 false
if ($lastStatus -eq 401 -or $lastStatus -eq 403) { return $false }
# 400 = 请求体问题 (模型名等), Key 本身通过
if ($lastStatus -eq 400) { return $true }
# 5xx/404/0 = 网络或中转站故障, 不能归咎 Key
$hadNetworkError = $true
continue
} catch { $hadNetworkError = $true; continue }
}
# 全部候选都遇网络故障 → 返回 $null (外层应: 接受 Key, 首次真实请求时再判)
if ($hadNetworkError) { return $null }
return $false
} catch {
return $false
}
}
function Show-GiteaCredentialDialog {
@ -673,6 +761,11 @@ Log-Phase 1 "环境检测 + 依赖自动安装"
# 刷新 PATH
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
# DryRun 模式: 只做 Phase 1 检测, 不动任何东西
if (Is-DryRun "env") {
Log-Info "[DryRun=env] Phase 1 只检测 PATH + 依赖可达性, 跳过后续步骤"
}
$deps = @(
# 核心依赖 (缺失则尝试自动安装)
@{ Name = "Node.js"; Cmd = "node"; WingetId = "OpenJS.NodeJS.LTS"; NpmPkg = $null; PipPkg = $null; Core = $true }
@ -873,10 +966,14 @@ if ($installed.Count -gt 0) {
Log-OK "本次新安装: $($installed -join ', ')"
}
# DryRun "env" 退出点: Phase 1 完成即退
if (Is-DryRun "env") { Log-OK "[DryRun=env] 完成, 退出"; exit 0 }
# ========================================================================
# Phase 2: 网络诊断
# ========================================================================
Log-Phase 2 "网络诊断"
if (Is-DryRun "net") { Log-Info "[DryRun=net] 只跑网络诊断, 跳过 Phase 3+" }
# 代理检测
$env:NO_PROXY = "bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1"
@ -983,10 +1080,14 @@ foreach ($t in $netTests) {
}
}
# DryRun "net" 退出点: Phase 2 完成即退
if (Is-DryRun "net") { Log-OK "[DryRun=net] 网络诊断完成, 退出"; exit 0 }
# ========================================================================
# Phase 3: 仓库克隆
# ========================================================================
Log-Phase 3 "同步 Bookworm 配置"
if (Is-DryRun "repo") { Log-Info "[DryRun=repo] 测试 git clone 完成后退出, 不做凭证解密/配置渲染" }
# B3: 使用 Windows Credential Manager (DPAPI 加密) 替代明文 store
git config --global credential.helper manager 2>$null
@ -1031,30 +1132,44 @@ elseif (Test-Path $ClaudeDir) {
if (Test-Path $BackupDir) { Remove-Item $BackupDir -Recurse -Force }
Rename-Item $ClaudeDir $BackupDir
# v3.0.1 先匿名克隆 (repos 已 public + REQUIRE_SIGNIN_VIEW=false), 失败才弹凭证
$r = Run-CmdWithUI "git" @("clone", "--depth", "1", $GitUrl, $ClaudeDir) "克隆配置仓库 (匿名)" 180000
if (-not (Test-Path (Join-Path $ClaudeDir "CLAUDE.md"))) {
Log-Warn "匿名克隆失败, 弹凭证对话框重试..."
$cred = Show-GiteaCredentialDialog
$cloneUrl = if ($cred) { $GitUrl -replace '://', "://$([System.Uri]::EscapeDataString($cred.User)):$([System.Uri]::EscapeDataString($cred.Pass))@" } else { $GitUrl }
$r = Run-CmdWithUI "git" @("clone", "--depth", "1", $cloneUrl, $ClaudeDir) "克隆配置仓库" 180000
if ($cred) {
$cloneUrl = $GitUrl -replace '://', "://$([System.Uri]::EscapeDataString($cred.User)):$([System.Uri]::EscapeDataString($cred.Pass))@"
$r = Run-CmdWithUI "git" @("clone", "--depth", "1", $cloneUrl, $ClaudeDir) "克隆配置仓库 (认证)" 180000
if (Test-Path (Join-Path $ClaudeDir "CLAUDE.md")) { Cache-GitCredentials $cred }
}
}
if (Test-Path (Join-Path $ClaudeDir "CLAUDE.md")) {
Log-OK "配置仓库克隆成功 (旧目录已备份)"
Cache-GitCredentials $cred
} else {
Log-Fail "克隆失败"
if (Test-Path $BackupDir) { Rename-Item $BackupDir $ClaudeDir }
Show-MsgBox "配置仓库克隆失败。`n请检查网络和 Gitea 账号密码" "克隆失败" "OK" "Error"
Show-MsgBox "配置仓库克隆失败。`n请检查网络连接" "克隆失败" "OK" "Error"
exit 1
}
}
else {
Log-Info "首次安装, 克隆配置仓库..."
Log-Info "首次安装, 匿名克隆配置仓库..."
# v3.0.1 先匿名, 失败才弹凭证
$r = Run-CmdWithUI "git" @("clone", "--depth", "1", $GitUrl, $ClaudeDir) "克隆配置仓库 (匿名)" 180000
if (-not (Test-Path (Join-Path $ClaudeDir "CLAUDE.md"))) {
Log-Warn "匿名克隆失败, 弹凭证对话框重试..."
$cred = Show-GiteaCredentialDialog
$cloneUrl = if ($cred) { $GitUrl -replace '://', "://$([System.Uri]::EscapeDataString($cred.User)):$([System.Uri]::EscapeDataString($cred.Pass))@" } else { $GitUrl }
$r = Run-CmdWithUI "git" @("clone", "--depth", "1", $cloneUrl, $ClaudeDir) "克隆配置仓库" 180000
if ($cred) {
$cloneUrl = $GitUrl -replace '://', "://$([System.Uri]::EscapeDataString($cred.User)):$([System.Uri]::EscapeDataString($cred.Pass))@"
$r = Run-CmdWithUI "git" @("clone", "--depth", "1", $cloneUrl, $ClaudeDir) "克隆配置仓库 (认证)" 180000
if (Test-Path (Join-Path $ClaudeDir "CLAUDE.md")) { Cache-GitCredentials $cred }
}
}
if (Test-Path (Join-Path $ClaudeDir "CLAUDE.md")) {
Log-OK "配置仓库克隆成功"
Cache-GitCredentials $cred
} else {
Log-Fail "克隆失败"
Show-MsgBox "配置仓库克隆失败。`n请检查网络连接和 Gitea 账号" "克隆失败" "OK" "Error"
Show-MsgBox "配置仓库克隆失败。`n请检查网络连接" "克隆失败" "OK" "Error"
exit 1
}
}
@ -1074,41 +1189,66 @@ if (Test-Path (Join-Path $BootDir ".git")) {
if ($r.OK) { Log-OK "boot 仓库已更新" } else { Log-Warn "boot 仓库更新失败, 使用本地版本" }
} catch { Log-Warn "boot 仓库更新失败, 使用本地版本" }
} else {
Log-Info "克隆 boot 仓库 (含解密工具与凭证)..."
# F-09 fix: 始终弹凭证对话框, 不依赖 config 分支的 $cred 残留值
Log-Info "匿名克隆 boot 仓库 (含解密工具)..."
# v3.0.1: 先匿名 (repo 已公开), 失败才弹凭证
$r = Run-CmdWithUI "git" @("clone", "--depth", "1", $BootUrl, $BootDir) "克隆 boot 仓库 (匿名)" 180000
if (-not (Test-Path (Join-Path $BootDir "crypto-helper.js"))) {
Log-Warn "匿名克隆失败, 弹凭证对话框重试..."
$cred = Show-GiteaCredentialDialog
$bootCloneUrl = if ($cred) { $BootUrl -replace '://', "://$([System.Uri]::EscapeDataString($cred.User)):$([System.Uri]::EscapeDataString($cred.Pass))@" } else { $BootUrl }
$r = Run-CmdWithUI "git" @("clone", "--depth", "1", $bootCloneUrl, $BootDir) "克隆 boot 仓库" 180000
if ($cred) {
$bootCloneUrl = $BootUrl -replace '://', "://$([System.Uri]::EscapeDataString($cred.User)):$([System.Uri]::EscapeDataString($cred.Pass))@"
$r = Run-CmdWithUI "git" @("clone", "--depth", "1", $bootCloneUrl, $BootDir) "克隆 boot 仓库 (认证)" 180000
if (Test-Path (Join-Path $BootDir "crypto-helper.js")) { Cache-GitCredentials $cred }
}
}
if (-not (Test-Path (Join-Path $BootDir "crypto-helper.js"))) {
Log-Fail "启动工具包下载失败"
Show-MsgBox "Bookworm 启动工具包下载失败。`n`n请检查:`n1. Gitea 账号和密码是否正确`n2. 网络连接是否正常`n3. 代理软件是否已启动`n`n然后重新运行安装器即可。" "下载失败" "OK" "Error"
Show-MsgBox "Bookworm 启动工具包下载失败。`n`n请检查:`n1. 网络和代理是否正常`n2. Gitea (code.letcareme.com) 是否可达`n`n重新运行安装器即可。" "下载失败" "OK" "Error"
exit 1
}
Log-OK "boot 仓库克隆成功 → $BootDir"
Cache-GitCredentials $cred # F-09 fix: 缓存 boot 仓库凭证
}
# DryRun "repo" 退出点: Phase 3 git clone 完成即退
if (Is-DryRun "repo") { Log-OK "[DryRun=repo] git clone 完成, 退出"; exit 0 }
# ========================================================================
# Phase 4: 凭证解密 (GUI 弹窗)
# ========================================================================
Log-Phase 4 "凭证解密"
if (Is-DryRun "creds") { Log-Info "[DryRun=creds] 只测 sk- Key 验证, 不持久化, 不进入 Phase 5" }
$secretsDecrypted = $false
# 优先级 1: User 级环境变量已有 (上次安装已永久写入)
# v3.0.1: 静默使用旧 Key 改为明确询问 (防误用吊销的老 Key)
$existingKey = [System.Environment]::GetEnvironmentVariable("ANTHROPIC_API_KEY", "User")
$existingUrl = [System.Environment]::GetEnvironmentVariable("ANTHROPIC_BASE_URL", "User")
if ($existingKey) {
# 注入到当前 Process (User 环境变量新终端才生效, 当前进程需手动加载)
$keyPreview = if ($existingKey.Length -ge 14) { $existingKey.Substring(0,10) + "..." + $existingKey.Substring($existingKey.Length - 4) } else { $existingKey.Substring(0, [Math]::Min(6, $existingKey.Length)) + "..." }
$urlPreview = if ($existingUrl) { $existingUrl } else { "(默认中转站)" }
$useExistingChoice = Show-MsgBox "检测到系统已保存的 API Key:`n`n Key : $keyPreview`n URL : $urlPreview`n`n是 = 继续使用这把 Key`n否 = 更换为新 Key (弹输入框)" "已有 API Key" "YesNo" "Question"
if ($useExistingChoice -eq "Yes") {
# 复用旧 Key
$env:ANTHROPIC_API_KEY = $existingKey
if ($existingUrl) { $env:ANTHROPIC_BASE_URL = $existingUrl }
# 加载其他 Key (如果有)
foreach ($k in $CacheAllowedKeys) {
$v = [System.Environment]::GetEnvironmentVariable($k, "User")
if ($v) { [System.Environment]::SetEnvironmentVariable($k, $v, "Process") }
}
Log-OK "从系统环境变量加载凭证 (已有安装记录, 免输授权码)"
Log-OK "继续使用已保存 Key ($keyPreview)"
$secretsDecrypted = $true
} else {
# 用户选择换 Key: 彻底清除所有缓存层 (否则 Priority 2 DPAPI 会复活旧 Key)
Log-Info "用户选择更换 Key, 清除所有层缓存, 转到手动输入"
# 层 1: User env var
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", $null, "User")
$env:ANTHROPIC_API_KEY = $null
# 层 2: DPAPI Registry 缓存 (HKCU:\Software\Bookworm\CachedEnv)
try { Remove-Item "HKCU:\Software\Bookworm\CachedEnv" -Recurse -Force -ErrorAction SilentlyContinue } catch {}
# URL 保留不清, 新 Key 默认还指向同一中转站
Log-Info "已清 User env + DPAPI cache, 下面将弹输入框让你粘新 Key"
}
}
# 优先级 2: Registry DPAPI 缓存
@ -1116,6 +1256,19 @@ if (-not $secretsDecrypted -and (Get-CachedSecrets)) {
Log-OK "从 Registry 缓存加载凭证"
$secretsDecrypted = $true
}
# v3.0.1: 强制设默认模型 = claude-opus-4-7 (中转站支持, Claude Code 2.0.1 默认 4-5 会 503)
# 顺序: 已显式设置 > Worker /config 拉取 > fallback 硬编码
if (-not $env:ANTHROPIC_MODEL) {
$defaultModel = "claude-opus-4-7"
try {
$cfg = Invoke-RestMethod "https://bookworm-router.bookworm-api.workers.dev/config" -TimeoutSec 8 -ErrorAction Stop
if ($cfg.default_model) { $defaultModel = $cfg.default_model }
} catch {}
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_MODEL", $defaultModel, "User")
$env:ANTHROPIC_MODEL = $defaultModel
Log-OK "ANTHROPIC_MODEL=$defaultModel (中转站兼容)"
}
# 优先级 3: 直接输入中转站 API Key (v2.3 新增)
if (-not $secretsDecrypted) {
Show-MsgBox "欢迎使用 Bookworm Portable`n`n接下来需要配置你的中转站 API Key。`n`n如果还没有, 请先去 https://bww.letcareme.com 注册并充值获取。" "配置 API Key" "OK" "Information"
@ -1144,8 +1297,18 @@ if (-not $secretsDecrypted) {
$ckOk = ($LASTEXITCODE -eq 0)
} catch { $ckOk = $false }
} else {
# 回退: 内置验证 + .NET API (change-key.js 不存在时)
$ckOk = Test-ApiKey $apiKey
# v3.0.1: Test-ApiKey 三值返回 (true=有效, false=认证失败, null=网络故障放行)
$checkResult = Test-ApiKey $apiKey
if ($checkResult -eq $true) {
$ckOk = $true
Log-OK "Key 在线验证通过"
} elseif ($null -eq $checkResult) {
# 网络故障, 不能归咎 Key, 先接受 (首次真实请求时再判)
$ckOk = $true
Log-Warn "中转站暂不可达, 暂接受此 Key (首次使用时若失败请重装换 Key)"
} else {
$ckOk = $false # 明确认证失败
}
if ($ckOk) {
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", $apiKey, "User")
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_BASE_URL", "https://bww.letcareme.com", "User")
@ -1278,24 +1441,61 @@ if (Test-Path $bwTokenFile) {
} catch {}
}
if ($needLicenseActivate -and (Test-Path $bwActivateJs) -and (Test-Cmd "node")) {
$activateChoice = Show-MsgBox "是否激活 Bookworm License (用于加载 Skill 专家)`n`n如果有 License Key (BW-XXXX-XXXX-...), 点是`n如果没有, 点否 (可后续用 change-key.bat 激活)" "License 激活" "YesNo" "Information"
# 零输入快路径: $env:BW_LICENSE_KEY 预设时直接激活, 不弹 GUI
# (install.ps1 通过环境变量传递, 格式: BW-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX)
$envKey = $env:BW_LICENSE_KEY
if ($envKey -and $envKey -match '^BW-[A-F0-9]{4}(-[A-F0-9]{4}){5}$') {
Log-Info "检测到 `$env:BW_LICENSE_KEY, 静默激活..."
try {
$envKey | & node $bwActivateJs 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0 -and (Test-Path $bwTokenFile)) {
Log-OK "License 静默激活成功"
} else {
Log-Warn "静默激活失败 (退出码 $LASTEXITCODE), 回退到 GUI"
$envKey = $null # 失败后走 GUI 补救
}
} catch {
Log-Warn "静默激活异常: $_, 回退到 GUI"
$envKey = $null
}
# 清掉 env 防止后续进程泄露
Remove-Item Env:\BW_LICENSE_KEY -ErrorAction SilentlyContinue
}
# 仍未激活 → 走 GUI 交互流 (带三次重试)
if (-not (Test-Path $bwTokenFile)) {
$activateChoice = Show-MsgBox "是否激活 Bookworm License (用于加载 Skill 专家)`n`n如果有 License Key (BW-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX), 点是`n如果没有, 点否 (可后续用 change-key.bat 激活)" "License 激活" "YesNo" "Information"
if ($activateChoice -eq "Yes") {
$licKey = Show-ApiKeyDialog 1 1 "" # 复用现有对话框
if ($licKey) {
$attempt = 0
while ($attempt -lt 3 -and -not (Test-Path $bwTokenFile)) {
$attempt++
$licKey = Show-LicenseKeyDialog $attempt 3
if (-not $licKey) { break } # 用户取消
if ($licKey -notmatch '^BW-[A-F0-9]{4}(-[A-F0-9]{4}){5}$') {
Show-MsgBox "格式错误! License Key 必须是 6 段 4 位十六进制`n例: BW-1A2B-3C4D-5E6F-7890-ABCD-EF12" "格式错误 ($attempt/3)" "OK" "Warning"
continue
}
try {
$licKey | & node $bwActivateJs 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
if ($LASTEXITCODE -eq 0 -and (Test-Path $bwTokenFile)) {
Log-OK "License 激活成功"
break
} else {
Log-Warn "License 激活失败 (可用 change-key.bat 重试)"
Show-MsgBox "激活失败 (可能 Key 已吊销/达设备上限/过期, 或代理不稳)`n重试或点取消跳过" "激活失败 ($attempt/3)" "OK" "Warning"
}
} catch {
Show-MsgBox "激活异常: $_" "激活失败 ($attempt/3)" "OK" "Error"
}
} catch { Log-Warn "License 激活异常: $_" }
}
} else {
Log-Info "已跳过 License 激活"
}
}
}
# DryRun "creds" / "license" 退出点: Phase 4 / 4.5 完成即退
if (Is-DryRun "creds") { Log-OK "[DryRun=creds] 凭证验证完成, 退出"; exit 0 }
if (Is-DryRun "license") { Log-OK "[DryRun=license] License 激活完成, 退出"; exit 0 }
# ========================================================================
# Phase 5: 配置渲染
# ========================================================================
@ -1424,10 +1624,14 @@ if (Test-Path $settingsFile) {
$settingsOK = $sc -match '"hooks"'
}
# v3.0.1 修复: Skills 门槛从 >50 改为 >=10
# 原因: 架构是 14 本地 + 79 云端懒加载, 本地只缓存常用 skill
# >50 的旧门槛和架构不匹配, 每次装完都误报 "配置不完整"
$checks = @(
@{ Name = "CLAUDE.md (Bookworm 指令)"; OK = $claudeMdOK }
@{ Name = "Skills ($skillCount 个)"; OK = ($skillCount -gt 50) }
@{ Name = "Hooks ($hookCount 个)"; OK = ($hookCount -gt 10) }
@{ Name = "Skills ($skillCount 本地 + 79 云端懒加载)"; OK = ($skillCount -ge 10) }
# v3.0.1: hook 阈值从 >10 改为 >=3 (config repo 分发精简版只有 4 个核心 hook)
@{ Name = "Hooks ($hookCount 核心)"; OK = ($hookCount -ge 3) }
@{ Name = "Settings hooks 配置"; OK = $settingsOK }
)
@ -1710,21 +1914,54 @@ if ($allOK -and $env:ANTHROPIC_API_KEY) {
$splash.Close()
$splash.Dispose()
# 启动 Bookworm — 优先 Windows Terminal > pwsh > cmd.exe
if (-not $SkipLaunch) {
$startBat = Join-Path $BootDir "启动Bookworm.bat"
if (Test-Path $startBat) {
Start-Process -FilePath $startBat -WorkingDirectory $BootDir
# v3.0.1: Bun crash 预检 (Claude Code 2.1.x 内置 Bun, 部分 Win10/CPU 触发 Illegal instruction)
# 先静默跑 `claude --version` 测试 3 秒, 若 crash 自动降到 2.0.1 纯 Node 版
$claudeVerOK = $false
try {
# 加超时: Bun crash 可能导致 claude --version 挂起不退出, -Wait 会卡死
$proc = Start-Process -FilePath "claude" -ArgumentList "--version" -NoNewWindow -PassThru -RedirectStandardOutput "$env:TEMP\bw-claude-ver.txt" -RedirectStandardError "$env:TEMP\bw-claude-err.txt" -WindowStyle Hidden
$exited = $proc.WaitForExit(8000) # 8 秒硬超时
if (-not $exited) {
try { $proc.Kill() } catch {}
Log-Warn "Claude Code --version 超过 8 秒无响应 (疑似 Bun 卡死)"
} elseif ($proc.ExitCode -eq 0) {
$out = Get-Content "$env:TEMP\bw-claude-ver.txt" -Raw -ErrorAction SilentlyContinue
if ($out -match "\d+\.\d+\.\d+") { $claudeVerOK = $true; Log-OK "Claude Code 自检: $($Matches[0])" }
} else {
$claudeCmd = "claude --dangerously-skip-permissions"
if (Get-Command wt.exe -ErrorAction SilentlyContinue) {
# Windows Terminal: 现代 UI, 支持全屏/标签页
Start-Process wt.exe -ArgumentList "new-tab", "-d", $BootDir, "--title", "Bookworm v$BWVersion", "cmd", "/k", $claudeCmd
} elseif ($PwshPath -and (Test-Path $PwshPath)) {
Start-Process $PwshPath -ArgumentList "-NoExit", "-Command", "cd '$BootDir'; $claudeCmd" -WorkingDirectory $BootDir
} else {
Start-Process cmd.exe -ArgumentList "/k", "title Bookworm v$BWVersion && cd /d `"$BootDir`" && $claudeCmd"
Log-Warn "Claude Code 自检退出码非 0: $($proc.ExitCode) (可能 Bun Illegal instruction)"
}
Remove-Item "$env:TEMP\bw-claude-ver.txt","$env:TEMP\bw-claude-err.txt" -EA SilentlyContinue
} catch {}
if (-not $claudeVerOK) {
Log-Warn "Claude Code 2.1.x Bun 自检失败 (可能 Illegal instruction), 自动降级到 2.0.1 纯 Node 版..."
Run-CmdWithUI "npm" @("uninstall", "-g", "@anthropic-ai/claude-code") "卸载现版" 120000 | Out-Null
$r = Run-CmdWithUI "npm" @("install", "-g", "@anthropic-ai/claude-code@2.0.1") "安装 2.0.1 纯 Node 版" 180000
if ($r.OK) { Log-OK "Claude Code 降级到 2.0.1 (Bun crash 规避)" } else { Log-Warn "降级失败, 仍尝试原版启动" }
}
# v3.0.1: 启动 Bookworm — 彻底绕开 .bat + claude.cmd shim
# 直接调 claude.ps1 (npm 全局装时生成, PS 原生, 不会经 cmd.exe)
# 若没 claude.ps1 (极旧 npm), 回退 node 直接跑 claude.js
if (-not $SkipLaunch) {
$claudeShim = "$env:APPDATA\npm\claude.ps1"
$claudeJsRel = "node_modules\@anthropic-ai\claude-code\cli.js"
$claudeJs = "$env:APPDATA\npm\$claudeJsRel"
# 构造 pwsh 里跑 claude 的命令 (不用 cmd shim)
$launchCmd = if (Test-Path $claudeShim) {
"& '$claudeShim' --dangerously-skip-permissions"
} elseif (Test-Path $claudeJs) {
"& node '$claudeJs' --dangerously-skip-permissions"
} else {
# 最后回退: 让 pwsh 的 Get-Command 找 claude (可能仍走 .cmd)
"claude --dangerously-skip-permissions"
}
# 默认模型双保险
$launchCmd = "`$env:ANTHROPIC_MODEL='claude-opus-4-7'; $launchCmd"
$shellExe = if ($PwshPath -and (Test-Path $PwshPath)) { $PwshPath } else { "powershell" }
if (Get-Command wt.exe -ErrorAction SilentlyContinue) {
Start-Process wt.exe -ArgumentList "new-tab", "-d", $BootDir, "--title", "Bookworm v$BWVersion", $shellExe, "-NoLogo", "-NoExit", "-Command", $launchCmd
} else {
Start-Process $shellExe -ArgumentList "-NoLogo", "-NoExit", "-Command", "cd '$BootDir'; $launchCmd" -WorkingDirectory $BootDir
}
}
@ -1739,10 +1976,12 @@ if ($allOK -and $env:ANTHROPIC_API_KEY) {
$launchResult = Show-MsgBox "安装完成, 但存在以下问题:`n$issueText`n`n是否仍然启动 Claude Code?`n(将以受限模式运行)`n`n日志: $BWLogFile" "安装警告" "YesNo" "Warning"
if ($launchResult -eq "Yes" -and -not $SkipLaunch) {
$claudeCmd = "claude --dangerously-skip-permissions"
# v3.0.1: 统一用 pwsh (PS7) 启动, 不再用 cmd.exe
$shellExe = if ($PwshPath -and (Test-Path $PwshPath)) { $PwshPath } else { "powershell" }
if (Get-Command wt.exe -ErrorAction SilentlyContinue) {
Start-Process wt.exe -ArgumentList "new-tab", "--title", "Bookworm", "cmd", "/k", $claudeCmd
Start-Process wt.exe -ArgumentList "new-tab", "--title", "Bookworm", $shellExe, "-NoExit", "-Command", $claudeCmd
} else {
Start-Process cmd.exe -ArgumentList "/k", $claudeCmd
Start-Process $shellExe -ArgumentList "-NoExit", "-Command", $claudeCmd
}
}
}