fix(auto-setup): v3.0.4 Key 验证 + 默认模型 + 指纹 Win11 兼容

茶师兄 Win11 机器真实报障驱动 4 Bug 闭环:

B1: 移除 change-key.js 优先验证分支 (硬编码 haiku → sonnet-only 套餐必 403)
    统一走 Test-ApiKey (多模型 fallback + 三值错误分类).

B2: 默认 ANTHROPIC_MODEL 兜底 opus-4-7 → sonnet-4-6 (基础套餐覆盖面最广).
    Test-ApiKey 成功时记录 $script:LastValidatedModel → Phase 4 末尾覆盖为真·可用模型.

B4: stderr 不再 2>&1 | Out-Null. PASS/AUTH_FAIL/NETWORK_ACCEPT 三态记录到
    $env:TEMP\bw-phase4-validate.log 并附失败模型列表便于报障.

B3 在 bookworm-portable-config.git c46f0b6 解决 (fingerprint.js wmic→CIM).

Test-ApiKey 候选顺序: sonnet-4-6 > opus-4-7 > opus-4-6 > thinking 变体.
401/403 不再立即 return false, 改累积到 $authFailModels, 全候选失败才判 false.
This commit is contained in:
bookworm 2026-04-22 20:05:56 +08:00
parent 235839a880
commit 85f2bcd52f

View File

@ -20,7 +20,7 @@ param(
$ErrorActionPreference = "Stop"
# ─── 版本号 (每次更新递增, build.ps1 自动读取) ──────
$BWVersion = "3.0.3" # +PS7 MSI 直链兜底 (Win10 无 winget 仍能装 pwsh, 避免跌落到 PS 5.1)
$BWVersion = "3.0.4" # fix: Key 验证模型兜底 + 默认模型反推 + Win11 指纹 wmic→CIM 三级 fallback
# DryRun 模式日志标记
if ($DryRun) { $global:BWDryRun = $DryRun } else { $global:BWDryRun = $null }
@ -488,23 +488,24 @@ function Show-LicenseKeyDialog($attempt = 1, $maxAttempts = 3) {
# 向中转站发一个最小请求验证 key 是否可用
function Test-ApiKey([string]$apiKey, [string]$baseUrl = "https://bww.letcareme.com") {
$script:LastValidatedModel = $null
$script:LastAuthFailCodes = @()
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
# v3.0.4: 模型候选按中转站兼容性+套餐覆盖面排序
# sonnet-4-6 放首位 - 中转站基础套餐都有, opus-4-7 可能被低档套餐 403
# 去掉 haiku (多数中转站已移除), 新增 sonnet-4-5 兼容老套餐
$modelCandidates = @(
"claude-sonnet-4-6",
"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 无效
# 错误分类:
# $true = 认证通过 (200/400) — $script:LastValidatedModel 记录通过的 model
# $false = 认证失败 (全部候选都返 401/403) 明确 Key 无效或权限不足
# $null = 网络/中转站故障 (5xx/404/timeout), 非 Key 问题, 外层应放行
$lastStatus = 0
$authFailModels = @()
$hadNetworkError = $false
foreach ($model in $modelCandidates) {
try {
@ -521,21 +522,27 @@ function Test-ApiKey([string]$apiKey, [string]$baseUrl = "https://bww.letcareme.
$req.ContentLength = $bytes.Length
$stream = $req.GetRequestStream(); $stream.Write($bytes, 0, $bytes.Length); $stream.Close()
$resp = $req.GetResponse(); $resp.Close()
$script:LastValidatedModel = $model
return $true # 200 = Key 有效
} catch [System.Net.WebException] {
$lastStatus = 0
try { $lastStatus = [int]$_.Exception.Response.StatusCode } catch {}
# 401/403 = 明确认证失败, 立即返回 false
if ($lastStatus -eq 401 -or $lastStatus -eq 403) { return $false }
# 401/403 = 这个 model 维度的认证/权限失败, 继续试其他 model (v3.0.4 核心改动)
# 只有全部候选都 401/403 才认 Key 无效; 任一 200/400 就算 Key 有效
if ($lastStatus -eq 401 -or $lastStatus -eq 403) {
$authFailModels += "$model=$lastStatus"
continue
}
# 400 = 请求体问题 (模型名等), Key 本身通过
if ($lastStatus -eq 400) { return $true }
if ($lastStatus -eq 400) { $script:LastValidatedModel = $model; return $true }
# 5xx/404/0 = 网络或中转站故障, 不能归咎 Key
$hadNetworkError = $true
continue
} catch { $hadNetworkError = $true; continue }
}
$script:LastAuthFailCodes = $authFailModels
# 全部候选都遇网络故障 → 返回 $null (外层应: 接受 Key, 首次真实请求时再判)
if ($hadNetworkError) { return $null }
if ($hadNetworkError -and $authFailModels.Count -eq 0) { return $null }
return $false
}
@ -1299,17 +1306,18 @@ if (-not $secretsDecrypted -and (Get-CachedSecrets)) {
$secretsDecrypted = $true
}
# v3.0.1: 强制设默认模型 = claude-opus-4-7 (中转站支持, Claude Code 2.0.1 默认 4-5 会 503)
# 顺序: 已显式设置 > Worker /config 拉取 > fallback 硬编码
# v3.0.4: 默认模型优先级重排(修复: opus-4-7 硬编码导致 sonnet-only 套餐用户启动后全 403
# 顺序: 已显式设置 > Test-ApiKey 实际通过的 model (延后设) > Worker /config > 兜底 sonnet-4-6
# 注: 实际验证通过后会在 Phase 4 末尾用 $script:LastValidatedModel 覆盖为"真·可用模型"
if (-not $env:ANTHROPIC_MODEL) {
$defaultModel = "claude-opus-4-7"
$defaultModel = "claude-sonnet-4-6" # v3.0.4: 兜底改 sonnet-4-6 (基础套餐都有, opus 常被低档 403)
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 (中转站兼容)"
Log-OK "ANTHROPIC_MODEL=$defaultModel (暂设, Phase 4 验证通过后会根据实际可用 model 覆盖)"
}
# 优先级 3: 直接输入中转站 API Key (v2.3 新增)
if (-not $secretsDecrypted) {
@ -1330,30 +1338,40 @@ if (-not $secretsDecrypted) {
continue
}
Log-Info "正在验证..."
# P1-2: 统一调用 change-key.js (stdin 管道, 无 argv 泄露)
$injectJs = Join-Path $ClaudeDir "change-key.js"
# v3.0.4: 统一走 Test-ApiKey (移除 change-key.js 优先分支)
# 删除原因: change-key.js 硬编码 claude-3-haiku-20240307 做验证,
# sonnet-only 套餐用户的 Key 在 haiku 必返 403 → 误判 Key 无效,
# 而 Test-ApiKey 本来就支持多模型 fallback + 三值错误分类, 覆盖更广.
# 同步修复: stderr 不再 2>&1 | Out-Null, 改为记录到 phase4-validate.log 便于用户报障
# 残留清理: 如 .claude/change-key.js 存在, 保留但不再调用 (未来版本会彻底移除)
$ckOk = $false
if ((Test-Path $injectJs) -and (Test-Cmd "node")) {
try {
$apiKey | & node $injectJs 2>&1 | Out-Null
$ckOk = ($LASTEXITCODE -eq 0)
} catch { $ckOk = $false }
} else {
# v3.0.1: Test-ApiKey 三值返回 (true=有效, false=认证失败, null=网络故障放行)
$checkResult = Test-ApiKey $apiKey
$phase4Log = Join-Path $env:TEMP "bw-phase4-validate.log"
if ($checkResult -eq $true) {
$ckOk = $true
Log-OK "Key 在线验证通过"
$validatedModel = if ($script:LastValidatedModel) { $script:LastValidatedModel } else { "(unknown)" }
Log-OK "Key 在线验证通过 (model=$validatedModel)"
try { "[$([DateTime]::Now.ToString('s'))] PASS model=$validatedModel" | Out-File -FilePath $phase4Log -Append -Encoding UTF8 } catch {}
} elseif ($null -eq $checkResult) {
# 网络故障, 不能归咎 Key, 先接受 (首次真实请求时再判)
# 全部候选网络故障, 不能归咎 Key, 先接受 (首次真实请求时再判)
$ckOk = $true
Log-Warn "中转站暂不可达, 暂接受此 Key (首次使用时若失败请重装换 Key)"
try { "[$([DateTime]::Now.ToString('s'))] NETWORK_ACCEPT baseUrl=https://bww.letcareme.com" | Out-File -FilePath $phase4Log -Append -Encoding UTF8 } catch {}
} else {
$ckOk = $false # 明确认证失败
# 全部候选都 401/403 = 明确认证失败
$ckOk = $false
$failDetail = if ($script:LastAuthFailCodes) { $script:LastAuthFailCodes -join ',' } else { 'all_models_auth_fail' }
Log-Warn "Key 被拒绝 ($failDetail)"
try { "[$([DateTime]::Now.ToString('s'))] AUTH_FAIL $failDetail" | Out-File -FilePath $phase4Log -Append -Encoding UTF8 } catch {}
}
if ($ckOk) {
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", $apiKey, "User")
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_BASE_URL", "https://bww.letcareme.com", "User")
# v3.0.4: 如果 Test-ApiKey 返回了真实通过的模型, 用它覆盖先前设的兜底默认
if ($script:LastValidatedModel) {
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_MODEL", $script:LastValidatedModel, "User")
$env:ANTHROPIC_MODEL = $script:LastValidatedModel
Log-OK "ANTHROPIC_MODEL 锁定为实际可用: $($script:LastValidatedModel)"
}
}
if ($ckOk) {