feat: v2.3.0 - Phase 4 改为用户直接输入中转站凭证

Win: Show-ApiKeyDialog GUI + Test-ApiKey 验证
Mac: 调用 ~/.claude/change-key.js (统一逻辑)
两者都保留旧授权码流程作为向后兼容

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
bookworm 2026-04-17 00:22:17 +08:00
parent 58099da6da
commit f3a58e1c6d
3 changed files with 176 additions and 8 deletions

View File

@ -1,6 +1,6 @@
#!/bin/bash
# ============================================================
# Bookworm Smart Assistant - macOS 全自动安装 v2.2.4
# Bookworm Smart Assistant - macOS 全自动安装 v2.3.0
#
# 用法 (任选一种):
# 方式1: 下载后运行
@ -39,7 +39,7 @@ echo " | _ \\ / _ \\ / _ \\| |/ /\\ \\ /\\ / / _ \\| '__| '\`_ \` _ \\"
echo " | |_) | (_) | (_) | < \\ V V / (_) | | | | | | | |"
echo " |____/ \\___/ \\___/|_|\\_\\ \\_/\\_/ \\___/|_| |_| |_| |_|"
echo ""
echo -e " ${BOLD}全自动安装 v2.2.4 — macOS${NC}"
echo -e " ${BOLD}全自动安装 v2.3.0 — macOS${NC}"
echo -e " ${BLUE}92 Skills | 18 Agents | 34 Hooks${NC}"
echo -e "${NC}"

View File

@ -35,7 +35,7 @@ banner() {
echo " | |_) | (_) | (_) | < \\ V V / (_) | | | | | | | |"
echo " |____/ \\___/ \\___/|_|\\_\\ \\_/\\_/ \\___/|_| |_| |_| |_|"
echo ""
echo -e " ${BOLD}Portable macOS Setup v2.2.4${NC}"
echo -e " ${BOLD}Portable macOS Setup v2.3.0${NC}"
echo -e " ${BLUE}92 Skills | 18 Agents | 34 Hooks${NC}"
echo -e "${NC}"
}
@ -321,7 +321,36 @@ parse_authcode() {
# 先尝试缓存
if load_cached_secrets 2>/dev/null; then
: # 缓存加载成功
elif [ -f "$SECRETS_ENC" ] || ls "$BOOT_DIR"/secrets-*.enc 2>/dev/null | head -1 | grep -q .; then
else
# 优先级 3: 调用 change-key.js 让用户直接输入中转站凭证 (v2.3)
CHANGE_KEY_JS="$CLAUDE_DIR/change-key.js"
if [ -f "$CHANGE_KEY_JS" ] && command -v node &>/dev/null; then
echo ""
info "配置中转站凭证 (中转站: https://bww.letcareme.com)"
for attempt in 1 2 3; do
echo ""
read -rs -p " 粘贴凭证 (第 $attempt/3 次, 留空跳过): " UCRED
echo ""
[ -z "$UCRED" ] && { warn "已跳过"; break; }
if node "$CHANGE_KEY_JS" "$UCRED" 2>&1; then
export ANTHROPIC_API_KEY="$UCRED"
export ANTHROPIC_BASE_URL="https://bww.letcareme.com"
UCRED=""
save_secrets_to_cache
success "凭证已写入并缓存"
echo ""
info "换凭证方式: 重跑安装器 / bash ~/.claude/change-key.sh / Claude Code 里 /change-key"
break
else
UCRED=""
[ $attempt -lt 3 ] && warn "验证失败, 剩余 $((3-attempt))" || fail "3 次失败"
fi
done
fi
fi
# 优先级 4: 授权码模式 (向后兼容旧用户)
if [ -z "$ANTHROPIC_API_KEY" ] && { [ -f "$SECRETS_ENC" ] || ls "$BOOT_DIR"/secrets-*.enc 2>/dev/null | head -1 | grep -q .; }; then
DECRYPTED=""
valid_attempts=0
total_attempts=0

View File

@ -16,7 +16,7 @@ param(
$ErrorActionPreference = "Stop"
# ─── 版本号 (每次更新递增, build.ps1 自动读取) ──────
$BWVersion = "2.2.4"
$BWVersion = "2.3.0"
# ─── B4: 单实例保护 (防止双击两次导致竞态) ─────────
$mutexCreated = $false
@ -352,6 +352,105 @@ function Show-AuthCodeDialog($attempt = 1, $maxAttempts = 3) {
return $null
}
# ─── 中转站 API Key 输入对话框 + 验证 (v2.3 新增) ──
function Show-ApiKeyDialog($attempt = 1, $maxAttempts = 3, $existingKey = "") {
$form = New-Object System.Windows.Forms.Form
$form.Text = "Bookworm - 中转站 API Key ($attempt/$maxAttempts)"
$form.Size = New-Object System.Drawing.Size(520, 280)
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedDialog"
$form.MaximizeBox = $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(470, 50)
$lblInfo.Text = "请粘贴你的中转站 API Key`n(bww.letcareme.com 注册后在后台获取, sk- 开头)"
$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, 75)
$keyBox.Size = New-Object System.Drawing.Size(470, 30)
$keyBox.Font = New-Object System.Drawing.Font("Consolas", 10)
$keyBox.Text = $existingKey
$keyBox.PasswordChar = '*'
$form.Controls.Add($keyBox)
$chkShow = New-Object System.Windows.Forms.CheckBox
$chkShow.Location = New-Object System.Drawing.Point(20, 110)
$chkShow.Size = New-Object System.Drawing.Size(150, 25)
$chkShow.Text = "显示 Key"
$chkShow.Add_CheckedChanged({ if ($chkShow.Checked) { $keyBox.PasswordChar = [char]0 } else { $keyBox.PasswordChar = '*' } })
$form.Controls.Add($chkShow)
$lblHint = New-Object System.Windows.Forms.Label
$lblHint.Location = New-Object System.Drawing.Point(20, 140)
$lblHint.Size = New-Object System.Drawing.Size(470, 40)
$lblHint.Text = "首次使用请先注册并充值:`nhttps://bww.letcareme.com"
$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(280, 195)
$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(390, 195)
$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 }
try {
$body = '{"model":"claude-sonnet-4-5","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"
$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
} 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
}
return $false
} catch {
return $false
}
}
function Show-GiteaCredentialDialog {
$form = New-Object System.Windows.Forms.Form
$form.Text = "Bookworm - Gitea 登录"
@ -1017,12 +1116,52 @@ if (-not $secretsDecrypted -and (Get-CachedSecrets)) {
Log-OK "从 Registry 缓存加载凭证"
$secretsDecrypted = $true
}
# 优先级 3: 解密 (缓存均未命中时)
# 优先级 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"
$keyAttempts = 0
$maxKeyAttempts = 3
while ($keyAttempts -lt $maxKeyAttempts) {
$keyAttempts++
$apiKey = Show-ApiKeyDialog $keyAttempts $maxKeyAttempts
if (-not $apiKey) {
Log-Warn "用户取消 API Key 输入"
break
}
# 基础格式校验
if ($apiKey.Length -lt 20) {
Show-MsgBox "API Key 格式错误 (长度过短)。`n请检查后重试。" "格式错误" "OK" "Warning"
continue
}
Log-Info "正在验证 API Key..."
if (Test-ApiKey $apiKey) {
# 验证通过, 写入环境变量 + 缓存
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", $apiKey, "Process")
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", $apiKey, "User")
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_BASE_URL", "https://bww.letcareme.com", "Process")
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_BASE_URL", "https://bww.letcareme.com", "User")
Log-OK "API Key 已验证并写入环境变量 (永久生效)"
$secretsDecrypted = $true
Save-SecretsToCache
Show-MsgBox "API Key 验证成功!`n`n已写入系统环境变量, 任何终端输入 claude 即可启动。`n`n以后想换 Key, 可以:`n1. 双击桌面 '更换Key.bat'`n2. 或 Claude Code 里输入 /change-key`n3. 或重跑安装器" "验证成功" "OK" "Information"
break
} else {
$remaining = $maxKeyAttempts - $keyAttempts
if ($remaining -gt 0) {
Show-MsgBox "API Key 验证失败 (无法连接或认证错误)。`n剩余重试: $remaining`n`n请检查:`n1. Key 是否正确 (sk- 开头)`n2. 中转站是否有余额`n3. 网络和代理是否正常" "验证失败" "OK" "Warning"
} else {
Show-MsgBox "3 次验证均失败。`n`n请检查 Key 和网络, 或联系管理员。" "验证失败" "OK" "Error"
}
}
}
}
# 优先级 4: 解密授权码 (向后兼容, 旧用户保留)
if (-not $secretsDecrypted) {
$cryptoHelper = Join-Path $BootDir "crypto-helper.js"
if (-not (Test-Cmd "node") -or -not (Test-Path $cryptoHelper)) {
Log-Fail "解密需要 Node.js (Phase 1 应已安装)"
Show-MsgBox "解密凭证需要 Node.js但未检测到。`n请确认 Phase 1 安装成功后重试。" "缺少 Node.js" "OK" "Error"
Log-Info "跳过授权码解密"
}
elseif ((Test-Path $SecretsEnc) -or (Get-ChildItem $BootDir -Filter "secrets-*.enc" -ErrorAction SilentlyContinue)) {
# 强制要求授权码 — 不允许跳过 (跳过 = 无法使用)