Compare commits
No commits in common. "main" and "v3.0.3-shortcut-migration" have entirely different histories.
main
...
v3.0.3-sho
Binary file not shown.
@ -1,16 +1,12 @@
|
||||
#!/bin/bash
|
||||
# ============================================================
|
||||
# Bookworm Portable - macOS Setup (从 boot 仓库内运行)
|
||||
# Version: 3.0.11
|
||||
# Version: 3.0.3
|
||||
#
|
||||
# 用法: cd ~/bookworm-boot && bash Bookworm-Setup.sh
|
||||
#
|
||||
# 前提: 已 git clone bookworm-boot 到本地
|
||||
# 功能: 检查依赖 → 代理检测 → 克隆配置 → 解密凭证 → 配置别名
|
||||
#
|
||||
# v3.0.11 注: macOS 不受 Windows wt+Base64 启动链路问题影响
|
||||
# (Mac shell 无 wt, claude 启动经标准 PATH 即可),
|
||||
# 此版本仅同步主版本号. 核心逻辑保持 v3.0.3 流程稳定.
|
||||
# ============================================================
|
||||
|
||||
set -e
|
||||
|
||||
1126
auto-setup.ps1
1126
auto-setup.ps1
File diff suppressed because it is too large
Load Diff
14
build.ps1
14
build.ps1
@ -157,20 +157,6 @@ Write-Host " 打包完成!输出目录: dist\" -ForegroundColor Green
|
||||
Write-Host " ============================================" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# v3.1.1: build 后自动跑 E2E 行为测试 (闭合 L8: 防 v3.0.10 -or 类运行时 bug)
|
||||
$e2eTest = Join-Path $ScriptDir "tools\test-launcher-e2e.ps1"
|
||||
if (Test-Path $e2eTest) {
|
||||
Write-Host " ── 运行 E2E 行为测试 (build 后自动护栏)" -ForegroundColor Cyan
|
||||
& pwsh -NoProfile -File $e2eTest 2>&1 | ForEach-Object { Write-Host " $_" }
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host ""
|
||||
Write-Host " [!] E2E 测试失败 (exit $LASTEXITCODE)" -ForegroundColor Red
|
||||
Write-Host " EXE 已生成但启动器契约/wrapper 有问题, 修复后重打包" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
Get-ChildItem $DistDir | ForEach-Object {
|
||||
$sizeMB = [math]::Round($_.Length / 1MB, 1)
|
||||
Write-Host " $($_.Name.PadRight(30)) ${sizeMB} MB" -ForegroundColor White
|
||||
|
||||
457
bw-doctor.ps1
457
bw-doctor.ps1
@ -1,457 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Bookworm Portable 体检工具 (v3.1.3)
|
||||
|
||||
.DESCRIPTION
|
||||
13 维度自检, 覆盖 auto-setup.ps1 7 阶段所有安装产物.
|
||||
输出彩色 PASS/WARN/FAIL 报告, 不修改任何文件.
|
||||
|
||||
维度:
|
||||
[1] PowerShell 7 (pwsh)
|
||||
[2] Node.js
|
||||
[3] Git
|
||||
[4] Claude Code (claude.ps1 可达)
|
||||
[5] 桌面 .lnk (启动/更新 + Args 契约)
|
||||
[6] DPAPI 凭证 (HKCU CachedEnv)
|
||||
[7] Profile BW_CRED 块 (PS7 + PS5.1)
|
||||
[8] Profile BW_CLIP 块 (PS7)
|
||||
[9] 环境变量 (ANTHROPIC_*)
|
||||
[10] ~/.claude 完整性 (CLAUDE.md / Skills / Hooks / Settings)
|
||||
[11] API 中转站连通 (bww.letcareme.com)
|
||||
[12] Worker 连通 (bookworm-router)
|
||||
[13] Gitea 连通 (code.letcareme.com)
|
||||
|
||||
.NOTES
|
||||
用法: pwsh -NoProfile -ExecutionPolicy Bypass -File bw-doctor.ps1
|
||||
日志: $env:TEMP\bw-doctor.log
|
||||
退出码: 0 = 全 PASS / 1 = 有 FAIL
|
||||
#>
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
$doctorLog = Join-Path $env:TEMP "bw-doctor.log"
|
||||
$pass = 0; $warn = 0; $fail = 0
|
||||
$results = @()
|
||||
|
||||
function Log-Doctor {
|
||||
param([string]$Msg)
|
||||
try { "[$([DateTime]::Now.ToString('yyyy-MM-dd HH:mm:ss'))] $Msg" | Out-File -FilePath $doctorLog -Append -Encoding UTF8 -EA SilentlyContinue } catch {}
|
||||
}
|
||||
|
||||
function Report {
|
||||
param([string]$Dim, [string]$Status, [string]$Detail)
|
||||
$color = switch ($Status) { 'PASS' { 'Green' } 'WARN' { 'Yellow' } 'FAIL' { 'Red' } default { 'Gray' } }
|
||||
$icon = switch ($Status) { 'PASS' { [char]0x2714 } 'WARN' { '!' } 'FAIL' { [char]0x2718 } default { '?' } }
|
||||
$line = " $icon [$Status] $Dim"
|
||||
if ($Detail) { $line += " — $Detail" }
|
||||
Write-Host $line -ForegroundColor $color
|
||||
Log-Doctor "$Status $Dim $Detail"
|
||||
switch ($Status) {
|
||||
'PASS' { $script:pass++ }
|
||||
'WARN' { $script:warn++ }
|
||||
'FAIL' { $script:fail++ }
|
||||
}
|
||||
$script:results += @{ Dim = $Dim; Status = $Status; Detail = $Detail }
|
||||
}
|
||||
|
||||
function Test-Cmd($name) { return [bool](Get-Command $name -EA SilentlyContinue) }
|
||||
|
||||
# ── Banner ──
|
||||
Write-Host ""
|
||||
Write-Host " +------------------------------------------+" -ForegroundColor Cyan
|
||||
Write-Host " | Bookworm Doctor v3.1.3 |" -ForegroundColor Cyan
|
||||
Write-Host " | 13 维度健康体检 |" -ForegroundColor Cyan
|
||||
Write-Host " +------------------------------------------+" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Log-Doctor "=== Bookworm Doctor v3.1.3 START ==="
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# [1/13] PowerShell 7
|
||||
# ══════════════════════════════════════════════════════
|
||||
Write-Host " [1/13] PowerShell 7" -ForegroundColor Cyan
|
||||
$pwshCmd = Get-Command pwsh -EA SilentlyContinue
|
||||
if ($pwshCmd) {
|
||||
try {
|
||||
$pwshVer = (& pwsh -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' 2>$null).Trim()
|
||||
Report "[1] PowerShell 7" "PASS" "v$pwshVer ($($pwshCmd.Source))"
|
||||
} catch {
|
||||
Report "[1] PowerShell 7" "WARN" "pwsh 可达但版本查询失败"
|
||||
}
|
||||
} else {
|
||||
Report "[1] PowerShell 7" "FAIL" "pwsh 不在 PATH — 桌面 .lnk 强依赖 pwsh.exe"
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# [2/13] Node.js
|
||||
# ══════════════════════════════════════════════════════
|
||||
Write-Host " [2/13] Node.js" -ForegroundColor Cyan
|
||||
if (Test-Cmd "node") {
|
||||
try {
|
||||
$nodeVer = (& node --version 2>$null).Trim()
|
||||
Report "[2] Node.js" "PASS" "$nodeVer"
|
||||
} catch {
|
||||
Report "[2] Node.js" "WARN" "node 可达但版本查询失败"
|
||||
}
|
||||
} else {
|
||||
Report "[2] Node.js" "FAIL" "node 不在 PATH — Claude Code 强依赖"
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# [3/13] Git
|
||||
# ══════════════════════════════════════════════════════
|
||||
Write-Host " [3/13] Git" -ForegroundColor Cyan
|
||||
if (Test-Cmd "git") {
|
||||
try {
|
||||
$gitVer = (& git --version 2>$null).Trim()
|
||||
Report "[3] Git" "PASS" "$gitVer"
|
||||
} catch {
|
||||
Report "[3] Git" "WARN" "git 可达但版本查询失败"
|
||||
}
|
||||
} else {
|
||||
Report "[3] Git" "FAIL" "git 不在 PATH — 配置同步强依赖"
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# [4/13] Claude Code
|
||||
# ══════════════════════════════════════════════════════
|
||||
Write-Host " [4/13] Claude Code" -ForegroundColor Cyan
|
||||
$claudePs1Found = $null
|
||||
|
||||
$claudeCmd = Get-Command claude -EA SilentlyContinue
|
||||
if ($claudeCmd -and $claudeCmd.Source -and $claudeCmd.Source.EndsWith('.ps1') -and (Test-Path $claudeCmd.Source)) {
|
||||
$claudePs1Found = $claudeCmd.Source
|
||||
}
|
||||
if (-not $claudePs1Found) {
|
||||
try {
|
||||
$npmPrefix = (& npm config get prefix 2>$null | Select-Object -First 1).Trim()
|
||||
$candidate = Join-Path $npmPrefix "claude.ps1"
|
||||
if (Test-Path $candidate) { $claudePs1Found = $candidate }
|
||||
} catch {}
|
||||
}
|
||||
if (-not $claudePs1Found) {
|
||||
foreach ($p in @("$env:APPDATA\npm\claude.ps1", "$env:ProgramFiles\nodejs\claude.ps1", "$env:LOCALAPPDATA\npm\claude.ps1")) {
|
||||
if (Test-Path $p) { $claudePs1Found = $p; break }
|
||||
}
|
||||
}
|
||||
|
||||
if ($claudePs1Found) {
|
||||
try {
|
||||
$claudeVer = (& claude --version 2>$null | Select-Object -First 1).Trim()
|
||||
Report "[4] Claude Code" "PASS" "$claudeVer ($claudePs1Found)"
|
||||
} catch {
|
||||
Report "[4] Claude Code" "PASS" "claude.ps1 存在: $claudePs1Found (版本查询跳过)"
|
||||
}
|
||||
} else {
|
||||
Report "[4] Claude Code" "FAIL" "claude.ps1 不可达 — 运行 npm i -g @anthropic-ai/claude-code"
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# [5/13] 桌面 .lnk
|
||||
# ══════════════════════════════════════════════════════
|
||||
Write-Host " [5/13] 桌面 .lnk" -ForegroundColor Cyan
|
||||
$desktop = [Environment]::GetFolderPath('Desktop')
|
||||
$requiredLnks = @('启动Bookworm.lnk', '更新Bookworm.lnk')
|
||||
$optionalLnks = @('体检Bookworm.lnk', '卸载Bookworm.lnk')
|
||||
$lnkMissing = @()
|
||||
$lnkPresent = @()
|
||||
|
||||
foreach ($n in $requiredLnks) {
|
||||
$p = Join-Path $desktop $n
|
||||
if (Test-Path $p) { $lnkPresent += $n } else { $lnkMissing += $n }
|
||||
}
|
||||
|
||||
$optPresent = @()
|
||||
foreach ($n in $optionalLnks) {
|
||||
$p = Join-Path $desktop $n
|
||||
if (Test-Path $p) { $optPresent += $n }
|
||||
}
|
||||
|
||||
if ($lnkMissing.Count -eq 0) {
|
||||
# 验证启动 .lnk Args 契约 (4 项: pwsh TargetPath / bw-launch.ps1 / --dangerously-skip-permissions / -ExecutionPolicy Bypass)
|
||||
$launchLnk = Join-Path $desktop '启动Bookworm.lnk'
|
||||
$argsOK = $true
|
||||
$argsDetail = ""
|
||||
try {
|
||||
$shell = New-Object -ComObject WScript.Shell
|
||||
$sc = $shell.CreateShortcut($launchLnk)
|
||||
$checks = @(
|
||||
@{ Name = "TargetPath=pwsh"; OK = ($sc.TargetPath -match 'pwsh\.exe$') }
|
||||
@{ Name = "bw-launch.ps1"; OK = ($sc.Arguments -match 'bw-launch\.ps1') }
|
||||
@{ Name = "--dangerously-skip-permissions"; OK = ($sc.Arguments -match '--dangerously-skip-permissions') }
|
||||
@{ Name = "-ExecutionPolicy Bypass"; OK = ($sc.Arguments -match '-ExecutionPolicy Bypass') }
|
||||
)
|
||||
$badChecks = $checks | Where-Object { -not $_.OK }
|
||||
if ($badChecks) {
|
||||
$argsOK = $false
|
||||
$argsDetail = "契约失败: " + (($badChecks | ForEach-Object { $_.Name }) -join ', ')
|
||||
}
|
||||
} catch { $argsOK = $false; $argsDetail = "读取 .lnk 异常" }
|
||||
|
||||
if ($argsOK) {
|
||||
$extra = if ($optPresent.Count -gt 0) { " + $($optPresent -join '/')" } else { "" }
|
||||
Report "[5] 桌面 .lnk" "PASS" "$($lnkPresent -join ' + ')$extra — 4 项契约 OK"
|
||||
} else {
|
||||
Report "[5] 桌面 .lnk" "FAIL" "$argsDetail"
|
||||
}
|
||||
} else {
|
||||
Report "[5] 桌面 .lnk" "FAIL" "缺失: $($lnkMissing -join ', ') — 重跑 Bookworm-Setup.exe"
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# [6/13] DPAPI 凭证
|
||||
# ══════════════════════════════════════════════════════
|
||||
Write-Host " [6/13] DPAPI 凭证" -ForegroundColor Cyan
|
||||
$regPath = "HKCU:\Software\Bookworm\CachedEnv"
|
||||
if (Test-Path $regPath) {
|
||||
try {
|
||||
Add-Type -AssemblyName System.Security -EA Stop
|
||||
$props = Get-ItemProperty $regPath -EA Stop
|
||||
$envNames = $props.PSObject.Properties | Where-Object { $_.Name -match '^[A-Z_]+$' }
|
||||
$decrypted = 0
|
||||
foreach ($ev in $envNames) {
|
||||
try {
|
||||
$bytes = [System.Security.Cryptography.ProtectedData]::Unprotect(
|
||||
[Convert]::FromBase64String($ev.Value), $null,
|
||||
[System.Security.Cryptography.DataProtectionScope]::CurrentUser)
|
||||
$decrypted++
|
||||
} catch {}
|
||||
}
|
||||
if ($envNames.Count -gt 0 -and $decrypted -eq $envNames.Count) {
|
||||
Report "[6] DPAPI 凭证" "PASS" "$decrypted/$($envNames.Count) 密钥可解密"
|
||||
} elseif ($decrypted -gt 0) {
|
||||
Report "[6] DPAPI 凭证" "WARN" "$decrypted/$($envNames.Count) 可解密 (部分失败)"
|
||||
} else {
|
||||
Report "[6] DPAPI 凭证" "FAIL" "0/$($envNames.Count) 可解密 — DPAPI 可能跨用户失效"
|
||||
}
|
||||
} catch {
|
||||
Report "[6] DPAPI 凭证" "WARN" "HKCU 存在但读取异常: $($_.Exception.Message)"
|
||||
}
|
||||
} else {
|
||||
Report "[6] DPAPI 凭证" "FAIL" "HKCU:\Software\Bookworm\CachedEnv 不存在 — 未安装或已卸载"
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# [7/13] Profile BW_CRED 块
|
||||
# ══════════════════════════════════════════════════════
|
||||
Write-Host " [7/13] Profile BW_CRED" -ForegroundColor Cyan
|
||||
$credBlockOK = 0; $credBlockMissing = @()
|
||||
foreach ($entry in @(
|
||||
@{ Name = "PS7"; Path = (Join-Path $env:USERPROFILE "Documents\PowerShell\profile.ps1") }
|
||||
@{ Name = "PS5.1"; Path = (Join-Path $env:USERPROFILE "Documents\WindowsPowerShell\profile.ps1") }
|
||||
)) {
|
||||
if (Test-Path $entry.Path) {
|
||||
$c = Get-Content $entry.Path -Raw -EA SilentlyContinue
|
||||
if ($c -match 'BW_CRED_START' -and $c -match 'BW_CRED_END') {
|
||||
$credBlockOK++
|
||||
} else {
|
||||
$credBlockMissing += $entry.Name
|
||||
}
|
||||
} else {
|
||||
$credBlockMissing += "$($entry.Name)(文件不存在)"
|
||||
}
|
||||
}
|
||||
if ($credBlockOK -ge 1 -and $credBlockMissing.Count -eq 0) {
|
||||
Report "[7] Profile BW_CRED" "PASS" "PS7 + PS5.1 双 profile sentinel 完整"
|
||||
} elseif ($credBlockOK -ge 1) {
|
||||
Report "[7] Profile BW_CRED" "WARN" "部分缺失: $($credBlockMissing -join ', ')"
|
||||
} else {
|
||||
Report "[7] Profile BW_CRED" "FAIL" "BW_CRED 块不存在 — 启动时无法自动加载凭证"
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# [8/13] Profile BW_CLIP 块
|
||||
# ══════════════════════════════════════════════════════
|
||||
Write-Host " [8/13] Profile BW_CLIP" -ForegroundColor Cyan
|
||||
$clipProfile = Join-Path $env:USERPROFILE "Documents\PowerShell\profile.ps1"
|
||||
if (Test-Path $clipProfile) {
|
||||
$c = Get-Content $clipProfile -Raw -EA SilentlyContinue
|
||||
if ($c -match 'BW_CLIP_START' -and $c -match 'BW_CLIP_END') {
|
||||
Report "[8] Profile BW_CLIP" "PASS" "截图粘贴助手 sentinel 完整"
|
||||
} else {
|
||||
Report "[8] Profile BW_CLIP" "WARN" "BW_CLIP 块缺失 (截图粘贴功能不可用, 非核心)"
|
||||
}
|
||||
} else {
|
||||
Report "[8] Profile BW_CLIP" "WARN" "PS7 profile.ps1 不存在"
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# [9/13] 凭证注入链路 (DPAPI → profile → 运行时 Key)
|
||||
# ══════════════════════════════════════════════════════
|
||||
Write-Host " [9/13] 凭证注入链路" -ForegroundColor Cyan
|
||||
# Bookworm 的设计: DPAPI 加密存 HKCU → profile.ps1 启动时解密 export → 运行时 env 可用
|
||||
# 不应检查 User 持久环境变量 (Key 明文存 User env 反而不安全)
|
||||
$chainOK = $true; $chainDetail = @()
|
||||
|
||||
# 环节 1: DPAPI 存储有 Key
|
||||
$regPath = "HKCU:\Software\Bookworm\CachedEnv"
|
||||
$dpapiHasKey = $false
|
||||
if (Test-Path $regPath) {
|
||||
try {
|
||||
$props = Get-ItemProperty $regPath -EA SilentlyContinue
|
||||
$apiKeyProp = $props.PSObject.Properties | Where-Object { $_.Name -eq 'ANTHROPIC_API_KEY' }
|
||||
if ($apiKeyProp) { $dpapiHasKey = $true; $chainDetail += "DPAPI=OK" }
|
||||
} catch {}
|
||||
}
|
||||
if (-not $dpapiHasKey) { $chainOK = $false; $chainDetail += "DPAPI=MISSING" }
|
||||
|
||||
# 环节 2: profile 有 BW_CRED 块
|
||||
$ps7Profile = Join-Path $env:USERPROFILE "Documents\PowerShell\profile.ps1"
|
||||
$profileHasCred = $false
|
||||
if (Test-Path $ps7Profile) {
|
||||
$c = Get-Content $ps7Profile -Raw -EA SilentlyContinue
|
||||
if ($c -match 'BW_CRED_START') { $profileHasCred = $true; $chainDetail += "Profile=OK" }
|
||||
}
|
||||
if (-not $profileHasCred) { $chainOK = $false; $chainDetail += "Profile=MISSING" }
|
||||
|
||||
# 环节 3: BASE_URL (可选但推荐)
|
||||
$baseUrl = [Environment]::GetEnvironmentVariable('ANTHROPIC_BASE_URL', 'User')
|
||||
if (-not $baseUrl) { $baseUrl = $env:ANTHROPIC_BASE_URL }
|
||||
if ($baseUrl) { $chainDetail += "BaseURL=$baseUrl" } else { $chainDetail += "BaseURL=default" }
|
||||
|
||||
if ($chainOK) {
|
||||
Report "[9] 凭证注入链路" "PASS" ($chainDetail -join ' → ')
|
||||
} else {
|
||||
Report "[9] 凭证注入链路" "FAIL" ($chainDetail -join ' → ') + " — 重跑 Bookworm-Setup.exe"
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# [10/13] ~/.claude 完整性
|
||||
# ══════════════════════════════════════════════════════
|
||||
Write-Host " [10/13] ~/.claude 完整性" -ForegroundColor Cyan
|
||||
$claudeDir = Join-Path $env:USERPROFILE ".claude"
|
||||
$intChecks = @()
|
||||
|
||||
# CLAUDE.md
|
||||
$claudeMdPath = Join-Path $claudeDir "CLAUDE.md"
|
||||
$claudeMdOK = $false
|
||||
if (Test-Path $claudeMdPath) {
|
||||
$cm = Get-Content $claudeMdPath -Raw -EA SilentlyContinue
|
||||
$claudeMdOK = $cm -match "Bookworm"
|
||||
}
|
||||
$intChecks += @{ Name = "CLAUDE.md"; OK = $claudeMdOK }
|
||||
|
||||
# Skills
|
||||
$skillsDir = Join-Path $claudeDir "skills"
|
||||
$skillCount = 0
|
||||
if (Test-Path $skillsDir) { $skillCount = @(Get-ChildItem $skillsDir -Directory -EA SilentlyContinue).Count }
|
||||
$intChecks += @{ Name = "Skills ($skillCount, 需>=10)"; OK = ($skillCount -ge 10) }
|
||||
|
||||
# Hooks
|
||||
$hooksDir = Join-Path $claudeDir "hooks"
|
||||
$hookCount = 0
|
||||
if (Test-Path $hooksDir) { $hookCount = @(Get-ChildItem $hooksDir -Filter "*.js" -File -EA SilentlyContinue).Count }
|
||||
$intChecks += @{ Name = "Hooks ($hookCount, 需>=3)"; OK = ($hookCount -ge 3) }
|
||||
|
||||
# Settings hooks
|
||||
$settingsFile = Join-Path $claudeDir "settings.json"
|
||||
$settingsOK = $false
|
||||
if (Test-Path $settingsFile) {
|
||||
$sc = Get-Content $settingsFile -Raw -EA SilentlyContinue
|
||||
$settingsOK = $sc -match '"hooks"'
|
||||
}
|
||||
$intChecks += @{ Name = "Settings hooks"; OK = $settingsOK }
|
||||
|
||||
$intFails = $intChecks | Where-Object { -not $_.OK }
|
||||
if ($intFails.Count -eq 0) {
|
||||
Report "[10] ~/.claude 完整性" "PASS" "CLAUDE.md + $skillCount Skills + $hookCount Hooks + Settings"
|
||||
} else {
|
||||
$failNames = ($intFails | ForEach-Object { $_.Name }) -join ', '
|
||||
Report "[10] ~/.claude 完整性" "FAIL" "缺失: $failNames"
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# [11/13] API 中转站连通
|
||||
# ══════════════════════════════════════════════════════
|
||||
Write-Host " [11/13] API 中转站" -ForegroundColor Cyan
|
||||
$apiBaseUrl = [Environment]::GetEnvironmentVariable('ANTHROPIC_BASE_URL', 'User')
|
||||
if (-not $apiBaseUrl) { $apiBaseUrl = $env:ANTHROPIC_BASE_URL }
|
||||
if (-not $apiBaseUrl) { $apiBaseUrl = "https://bww.letcareme.com" }
|
||||
|
||||
try {
|
||||
# 中转站对境外 IP 返 503, NO_PROXY 直连
|
||||
$savedProxy = $env:NO_PROXY
|
||||
$env:NO_PROXY = "bww.letcareme.com,letcareme.com,localhost,127.0.0.1"
|
||||
|
||||
$resp = Invoke-WebRequest -Uri "$apiBaseUrl/v1/models" -Method Head -TimeoutSec 10 -UseBasicParsing -EA Stop
|
||||
$env:NO_PROXY = $savedProxy
|
||||
Report "[11] API 中转站" "PASS" "$apiBaseUrl (HTTP $($resp.StatusCode))"
|
||||
} catch {
|
||||
$env:NO_PROXY = $savedProxy
|
||||
$errMsg = $_.Exception.Message
|
||||
if ($errMsg -match '40[0-9]|301|302|503') {
|
||||
Report "[11] API 中转站" "PASS" "$apiBaseUrl 可达 (HTTP 错误码 = 网络通)"
|
||||
} else {
|
||||
Report "[11] API 中转站" "FAIL" "$apiBaseUrl 不可达: $errMsg"
|
||||
}
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# [12/13] Worker 连通
|
||||
# ══════════════════════════════════════════════════════
|
||||
Write-Host " [12/13] Worker" -ForegroundColor Cyan
|
||||
try {
|
||||
$workerUrl = "https://bookworm-router.bookworm-api.workers.dev/config"
|
||||
$resp = Invoke-RestMethod -Uri $workerUrl -TimeoutSec 10 -EA Stop
|
||||
Report "[12] Worker" "PASS" "bookworm-router /config 可达"
|
||||
} catch {
|
||||
$errMsg = $_.Exception.Message
|
||||
if ($errMsg -match '403|404') {
|
||||
Report "[12] Worker" "PASS" "Worker 可达 (HTTP 错误码 = 网络通)"
|
||||
} else {
|
||||
Report "[12] Worker" "WARN" "Worker 不可达: $errMsg (非核心, 仅影响默认 model)"
|
||||
}
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# [13/13] Gitea 连通
|
||||
# ══════════════════════════════════════════════════════
|
||||
Write-Host " [13/13] Gitea" -ForegroundColor Cyan
|
||||
try {
|
||||
$savedProxy = $env:NO_PROXY
|
||||
$env:NO_PROXY = "code.letcareme.com,letcareme.com,localhost,127.0.0.1"
|
||||
$resp = Invoke-WebRequest -Uri "https://code.letcareme.com" -Method Head -TimeoutSec 10 -UseBasicParsing -EA Stop
|
||||
$env:NO_PROXY = $savedProxy
|
||||
Report "[13] Gitea" "PASS" "code.letcareme.com 可达"
|
||||
} catch {
|
||||
$env:NO_PROXY = $savedProxy
|
||||
$errMsg = $_.Exception.Message
|
||||
if ($errMsg -match '40[0-9]|301|302|503') {
|
||||
Report "[13] Gitea" "PASS" "code.letcareme.com 可达 (HTTP 错误码 = 网络通)"
|
||||
} else {
|
||||
Report "[13] Gitea" "FAIL" "code.letcareme.com 不可达: $errMsg — 配置同步将失败"
|
||||
}
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# Summary
|
||||
# ══════════════════════════════════════════════════════
|
||||
$total = $pass + $warn + $fail
|
||||
Write-Host ""
|
||||
Write-Host " +------------------------------------------+" -ForegroundColor Cyan
|
||||
Write-Host " | 体检报告 |" -ForegroundColor Cyan
|
||||
Write-Host " +------------------------------------------+" -ForegroundColor Cyan
|
||||
|
||||
$passLine = " | PASS: $pass / $total"
|
||||
$warnLine = " | WARN: $warn"
|
||||
$failLine = " | FAIL: $fail"
|
||||
|
||||
Write-Host $passLine -ForegroundColor Green
|
||||
if ($warn -gt 0) { Write-Host $warnLine -ForegroundColor Yellow }
|
||||
if ($fail -gt 0) { Write-Host $failLine -ForegroundColor Red }
|
||||
|
||||
Write-Host " |" -ForegroundColor Cyan
|
||||
if ($fail -eq 0 -and $warn -eq 0) {
|
||||
Write-Host " | [ALL GREEN] Bookworm 完全健康" -ForegroundColor Green
|
||||
} elseif ($fail -eq 0) {
|
||||
Write-Host " | [HEALTHY] 核心功能正常, 有 $warn 项可优化" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host " | [NEEDS FIX] $fail 项异常需修复" -ForegroundColor Red
|
||||
Write-Host " | 修复: 重跑 Bookworm-Setup.exe 或联系管理员" -ForegroundColor Red
|
||||
}
|
||||
Write-Host " | 日志: $doctorLog" -ForegroundColor Gray
|
||||
Write-Host " +------------------------------------------+" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
Log-Doctor "=== DONE: PASS=$pass WARN=$warn FAIL=$fail ==="
|
||||
|
||||
if ($fail -gt 0) { exit 1 } else { exit 0 }
|
||||
163
bw-launch.ps1
163
bw-launch.ps1
@ -1,163 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Bookworm 启动 wrapper (v3.1.0 引入)
|
||||
|
||||
.DESCRIPTION
|
||||
桌面 .lnk 调用此 wrapper 而非直调 claude.ps1, 让启动逻辑可热修复 (改 wrapper 不需重 bake .lnk).
|
||||
|
||||
职责:
|
||||
1. 动态查找 claude.ps1 真实路径 (npm config get prefix → 兜底候选)
|
||||
2. claude.ps1 stale (npm uninstall/换版本/换 prefix) → 弹清晰 GUI 引导, 不再"快捷方式失效"
|
||||
3. 失败时不静默, 写日志 + GUI 弹窗
|
||||
|
||||
与 .lnk 的契约:
|
||||
.lnk Args = -NoLogo -NoExit -ExecutionPolicy Bypass -File "<bw-launch.ps1 绝对路径>"
|
||||
--dangerously-skip-permissions
|
||||
$args[0..N] 转发给 claude.ps1
|
||||
|
||||
.NOTES
|
||||
v3.1.0 (2026-04-25) — 引入 wrapper 模式 (闭合 L4 局限: bake claude.ps1 路径 stale)
|
||||
v3.1.2 (2026-04-26) — 加 nvm/fnm/volta shim 探测 (闭合 L9)
|
||||
分发: Phase 7 安装时复制到 $BootDir\bw-launch.ps1
|
||||
#>
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
$bwLaunchLog = Join-Path $env:TEMP "bw-launch.log"
|
||||
|
||||
function Write-BwLaunchLog {
|
||||
param([string]$Level, [string]$Msg)
|
||||
try {
|
||||
$line = "[$([DateTime]::Now.ToString('yyyy-MM-dd HH:mm:ss'))] [$Level] $Msg"
|
||||
$line | Out-File -FilePath $bwLaunchLog -Append -Encoding UTF8 -EA SilentlyContinue
|
||||
} catch {}
|
||||
}
|
||||
|
||||
# v3.1.2 L12: log rotation — >1MB 归档, 保留最近 3 个 .bak
|
||||
foreach ($logName in @("bw-launch.log", "bw-crash.log", "bw-phase4-validate.log", "bw-doctor.log")) {
|
||||
$logPath = Join-Path $env:TEMP $logName
|
||||
try {
|
||||
if ((Test-Path $logPath) -and ((Get-Item $logPath -EA SilentlyContinue).Length -gt 1MB)) {
|
||||
$ts = [DateTime]::Now.ToString('yyyyMMdd-HHmmss')
|
||||
Copy-Item $logPath "$logPath.bak.$ts" -Force -EA Stop
|
||||
Remove-Item $logPath -Force -EA Stop
|
||||
# 保留最近 3 个 .bak, 删旧的
|
||||
$baks = Get-ChildItem "$logPath.bak.*" -EA SilentlyContinue | Sort-Object LastWriteTime -Descending
|
||||
if ($baks.Count -gt 3) { $baks | Select-Object -Skip 3 | Remove-Item -Force -EA SilentlyContinue }
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function Show-LaunchError {
|
||||
param([string]$Title, [string]$Body)
|
||||
Write-BwLaunchLog "ERROR" "$Title :: $Body"
|
||||
Write-Host ""
|
||||
Write-Host " [!] $Title" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
Write-Host $Body -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host " 日志: $bwLaunchLog" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
try {
|
||||
Add-Type -AssemblyName System.Windows.Forms -EA Stop
|
||||
[System.Windows.Forms.MessageBox]::Show("$Body`n`n详情见: $bwLaunchLog", "Bookworm 启动失败 — $Title", 'OK', 'Error') | Out-Null
|
||||
} catch {}
|
||||
Read-Host "按回车关闭"
|
||||
}
|
||||
|
||||
Write-BwLaunchLog "INFO" "bw-launch wrapper 启动 args=$($args -join ' ')"
|
||||
|
||||
# ── 1. PATH 三层重载 (即便桌面 .lnk 不依赖 PATH, 子 claude.ps1 调 node 仍依赖) ──
|
||||
$env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User')
|
||||
|
||||
try {
|
||||
$npmPrefix = (& npm config get prefix 2>$null | Select-Object -First 1).Trim()
|
||||
if ($npmPrefix -and (Test-Path $npmPrefix) -and ($env:Path -notlike "*$npmPrefix*")) {
|
||||
$env:Path = "$npmPrefix;$env:Path"
|
||||
}
|
||||
} catch {
|
||||
Write-BwLaunchLog "WARN" "npm config get prefix 失败: $_"
|
||||
}
|
||||
|
||||
foreach ($p in @("$env:APPDATA\npm", "$env:ProgramFiles\nodejs", "$env:LOCALAPPDATA\npm")) {
|
||||
if ((Test-Path $p) -and ($env:Path -notlike "*$p*")) {
|
||||
$env:Path = "$p;$env:Path"
|
||||
}
|
||||
}
|
||||
|
||||
# v3.1.2 L9: nvm/fnm/volta shim 探测 (版本管理器装的 node 不在默认 PATH)
|
||||
$shimPaths = @()
|
||||
if ($env:NVM_HOME) { $shimPaths += "$env:NVM_HOME"; $shimPaths += (Join-Path $env:NVM_HOME "nodejs") }
|
||||
if ($env:NVM_SYMLINK) { $shimPaths += $env:NVM_SYMLINK }
|
||||
if ($env:FNM_DIR) { $shimPaths += (Join-Path $env:FNM_DIR "aliases\default") }
|
||||
if ($env:FNM_MULTISHELL_PATH) { $shimPaths += $env:FNM_MULTISHELL_PATH }
|
||||
if ($env:VOLTA_HOME) { $shimPaths += (Join-Path $env:VOLTA_HOME "bin") }
|
||||
foreach ($sp in $shimPaths) {
|
||||
if ($sp -and (Test-Path $sp) -and ($env:Path -notlike "*$sp*")) {
|
||||
$env:Path = "$sp;$env:Path"
|
||||
Write-BwLaunchLog "INFO" "shim PATH 补充: $sp"
|
||||
}
|
||||
}
|
||||
|
||||
# ── 2. 动态定位 claude.ps1 ──
|
||||
$claudePs1 = $null
|
||||
|
||||
# 优先 Get-Command (PATH 已重载)
|
||||
$claudeCmd = Get-Command claude -ErrorAction SilentlyContinue
|
||||
if ($claudeCmd -and $claudeCmd.Source -and $claudeCmd.Source.EndsWith('.ps1') -and (Test-Path $claudeCmd.Source)) {
|
||||
$claudePs1 = $claudeCmd.Source
|
||||
Write-BwLaunchLog "INFO" "claude.ps1 from Get-Command: $claudePs1"
|
||||
}
|
||||
|
||||
# 兜底 npm config get prefix
|
||||
if (-not $claudePs1) {
|
||||
try {
|
||||
$candidate = Join-Path $npmPrefix "claude.ps1"
|
||||
if (Test-Path $candidate) {
|
||||
$claudePs1 = $candidate
|
||||
Write-BwLaunchLog "INFO" "claude.ps1 from npm prefix: $claudePs1"
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
# 最终硬编码兜底
|
||||
if (-not $claudePs1) {
|
||||
foreach ($p in @("$env:APPDATA\npm\claude.ps1", "$env:ProgramFiles\nodejs\claude.ps1", "$env:LOCALAPPDATA\npm\claude.ps1")) {
|
||||
if (Test-Path $p) {
|
||||
$claudePs1 = $p
|
||||
Write-BwLaunchLog "INFO" "claude.ps1 from hardcoded: $claudePs1"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $claudePs1 -or -not (Test-Path $claudePs1)) {
|
||||
$diag = "未找到 claude.ps1 文件.`n`n"
|
||||
$diag += "诊断信息:`n"
|
||||
$diag += " npm config get prefix: $(if ($npmPrefix) { $npmPrefix } else { '(查询失败)' })`n"
|
||||
$diag += " PATH 中 npm/nodejs 片段:`n"
|
||||
foreach ($p in (($env:Path -split ';') | Where-Object { $_ -match 'npm|nodejs' })) {
|
||||
$diag += " $p`n"
|
||||
}
|
||||
$diag += "`n修复方案:`n"
|
||||
$diag += " 方案 A: 命令行运行 npm i -g @anthropic-ai/claude-code`n"
|
||||
$diag += " 方案 B: 重新双击 Bookworm-Setup.exe 修复安装"
|
||||
Show-LaunchError "Claude Code 未找到" $diag
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ── 3. 启动 claude.ps1 + 转发 $args (--dangerously-skip-permissions 等) ──
|
||||
Write-BwLaunchLog "INFO" "调用 claude.ps1 args=$($args -join ' ')"
|
||||
try {
|
||||
& $claudePs1 @args
|
||||
$exitCode = $LASTEXITCODE
|
||||
Write-BwLaunchLog "INFO" "claude.ps1 退出码: $exitCode"
|
||||
if ($exitCode -ne 0) {
|
||||
Write-Host ""
|
||||
Write-Host " [!] Claude 进程退出码: $exitCode" -ForegroundColor Yellow
|
||||
Write-Host " 日志: $bwLaunchLog" -ForegroundColor Gray
|
||||
}
|
||||
exit $exitCode
|
||||
} catch {
|
||||
Show-LaunchError "Claude 启动异常" "异常信息: $($_.Exception.Message)"
|
||||
exit 1
|
||||
}
|
||||
387
bw-ota.ps1
387
bw-ota.ps1
@ -1,387 +0,0 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Bookworm Portable OTA 更新检查器 (启动时自动调用)
|
||||
|
||||
.DESCRIPTION
|
||||
轻量版同步: 检查远端版本 → 用户确认 → 增量同步 → 验签 → 原子替换.
|
||||
设计原则: fail-open (任何异常不阻断启动), 24h 冷却, 用户确认制.
|
||||
|
||||
与 bookworm-sync.ps1 的区别:
|
||||
- Token 从 DPAPI 加密文件读取 (安装时写入)
|
||||
- Pubkey 内嵌安装目录
|
||||
- 失败不阻断启动
|
||||
- 版本相同跳过
|
||||
|
||||
.NOTES
|
||||
Author: Bookworm Admin
|
||||
Version: 1.0.0 (2026-04-27)
|
||||
License: Private
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$Force,
|
||||
[switch]$SkipConfirm,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
$OtaDir = Join-Path $env:USERPROFILE '.claude\.bw-ota'
|
||||
$ClaudeRoot = Join-Path $env:USERPROFILE '.claude'
|
||||
$ConfigFile = Join-Path $OtaDir 'config.json'
|
||||
$CredFile = Join-Path $OtaDir 'pull-cred.dpapi'
|
||||
$PubKeyFile = Join-Path $OtaDir 'signing-pubkey.pem'
|
||||
|
||||
# ========== 日志 ==========
|
||||
function Write-Ota ($m, $c = 'Cyan') { Write-Host "[Bookworm OTA] $m" -ForegroundColor $c }
|
||||
function Write-OtaOk ($m) { Write-Ota $m 'Green' }
|
||||
function Write-OtaWarn ($m) { Write-Ota $m 'Yellow' }
|
||||
function Write-OtaErr ($m) { Write-Ota $m 'Red' }
|
||||
|
||||
# ========== OTA 基础设施检查 ==========
|
||||
function Test-OtaReady {
|
||||
if (-not (Test-Path $OtaDir)) { return $false }
|
||||
if (-not (Test-Path $ConfigFile)) { return $false }
|
||||
if (-not (Test-Path $CredFile)) { return $false }
|
||||
if (-not (Test-Path $PubKeyFile)) { return $false }
|
||||
return $true
|
||||
}
|
||||
|
||||
# ========== 配置读写 ==========
|
||||
function Read-OtaConfig {
|
||||
try {
|
||||
$raw = Get-Content -Raw $ConfigFile
|
||||
if ($raw.Length -gt 0 -and [int][char]$raw[0] -eq 0xFEFF) { $raw = $raw.Substring(1) }
|
||||
return $raw | ConvertFrom-Json
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
function Save-OtaConfig ($cfg) {
|
||||
$json = $cfg | ConvertTo-Json -Depth 4
|
||||
[IO.File]::WriteAllText($ConfigFile, $json, [Text.UTF8Encoding]::new($false))
|
||||
}
|
||||
|
||||
# ========== 冷却检查 (24h) ==========
|
||||
function Test-Cooldown ($cfg) {
|
||||
if ($Force) { return $false }
|
||||
$interval = if ($cfg.checkInterval) { $cfg.checkInterval } else { 86400 }
|
||||
$lastCheck = if ($cfg.lastCheck) { $cfg.lastCheck } else { 0 }
|
||||
$now = [int][DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
||||
return ($now - $lastCheck) -lt $interval
|
||||
}
|
||||
|
||||
# ========== DPAPI 凭证解密 ==========
|
||||
function Read-Credential {
|
||||
try {
|
||||
Add-Type -AssemblyName System.Security
|
||||
$encrypted = [IO.File]::ReadAllBytes($CredFile)
|
||||
$plain = [Security.Cryptography.ProtectedData]::Unprotect(
|
||||
$encrypted,
|
||||
[Text.Encoding]::UTF8.GetBytes('bookworm-ota-salt'),
|
||||
[Security.Cryptography.DataProtectionScope]::CurrentUser
|
||||
)
|
||||
$json = [Text.Encoding]::UTF8.GetString($plain) | ConvertFrom-Json
|
||||
return $json
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
# ========== 语义版本比较 (major.minor.patch) ==========
|
||||
function Compare-SemVer ($local, $remote) {
|
||||
$lParts = ($local -replace '^v', '') -split '\.' | ForEach-Object { [int]$_ }
|
||||
$rParts = ($remote -replace '^v', '') -split '\.' | ForEach-Object { [int]$_ }
|
||||
for ($i = 0; $i -lt [Math]::Max($lParts.Count, $rParts.Count); $i++) {
|
||||
$l = if ($i -lt $lParts.Count) { $lParts[$i] } else { 0 }
|
||||
$r = if ($i -lt $rParts.Count) { $rParts[$i] } else { 0 }
|
||||
if ($r -gt $l) { return 1 }
|
||||
if ($r -lt $l) { return -1 }
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
# ========== 本地版本号 ==========
|
||||
function Get-LocalVersion {
|
||||
$versionFile = Join-Path $ClaudeRoot 'VERSION'
|
||||
if (Test-Path $versionFile) {
|
||||
return (Get-Content -Raw $versionFile).Trim()
|
||||
}
|
||||
$statsFile = Join-Path $ClaudeRoot 'stats-compiled.json'
|
||||
if (Test-Path $statsFile) {
|
||||
try {
|
||||
$stats = Get-Content -Raw $statsFile | ConvertFrom-Json
|
||||
return $stats.summary.version
|
||||
} catch { }
|
||||
}
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
# ========== Basic Auth 头构造 ==========
|
||||
function Get-AuthHeaders ($cred) {
|
||||
$pair = "$($cred.user):$($cred.pass)"
|
||||
$bytes = [Text.Encoding]::UTF8.GetBytes($pair)
|
||||
$b64 = [Convert]::ToBase64String($bytes)
|
||||
return @{ Authorization = "Basic $b64" }
|
||||
}
|
||||
|
||||
# ========== 远端版本查询 (Gitea raw, 3s 超时) ==========
|
||||
function Get-RemoteVersion ($cred, $cfg) {
|
||||
$repoUrl = if ($cfg.repoUrl) { $cfg.repoUrl } else { 'https://code.letcareme.com/bookworm/bookworm-smart-assistant' }
|
||||
$apiUrl = "$repoUrl/raw/branch/main/VERSION"
|
||||
try {
|
||||
$headers = Get-AuthHeaders $cred
|
||||
$resp = Invoke-WebRequest -Uri $apiUrl -Headers $headers -UseBasicParsing -TimeoutSec 3
|
||||
return $resp.Content.Trim()
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
# ========== 远端最新 tag 查询 (备用) ==========
|
||||
function Get-RemoteLatestTag ($cred, $cfg) {
|
||||
$repoUrl = if ($cfg.repoUrl) { $cfg.repoUrl } else { 'https://code.letcareme.com/bookworm/bookworm-smart-assistant' }
|
||||
$host_ = ([Uri]$repoUrl).Host
|
||||
$repoPath = ([Uri]$repoUrl).AbsolutePath.TrimStart('/')
|
||||
$apiUrl = "https://$host_/api/v1/repos/$repoPath/tags?limit=1"
|
||||
try {
|
||||
$headers = Get-AuthHeaders $cred
|
||||
$resp = Invoke-WebRequest -Uri $apiUrl -Headers $headers -UseBasicParsing -TimeoutSec 3
|
||||
$tags = $resp.Content | ConvertFrom-Json
|
||||
if ($tags.Count -gt 0) { return $tags[0].name }
|
||||
} catch { }
|
||||
return $null
|
||||
}
|
||||
|
||||
# ========== 同步核心 (精简版 bookworm-sync.ps1) ==========
|
||||
function Invoke-OtaSync ($cred, $cfg, $remoteVersion) {
|
||||
$repoUrl = if ($cfg.repoUrl) { $cfg.repoUrl } else { 'https://code.letcareme.com/bookworm/bookworm-smart-assistant' }
|
||||
$host_ = ([Uri]$repoUrl).Host
|
||||
$repoPath = ([Uri]$repoUrl).AbsolutePath.TrimStart('/')
|
||||
$ref = if ($remoteVersion -and $remoteVersion -match '^v?\d') { $remoteVersion } else { 'main' }
|
||||
if ($ref -notmatch '^v') { $ref = "v$ref" }
|
||||
|
||||
$stageDir = Join-Path $env:TEMP "bw-ota-$([Guid]::NewGuid().ToString().Substring(0,8))"
|
||||
|
||||
# 1. Clone (凭证通过 credential manager 传递, 不嵌入 URL)
|
||||
Write-Ota "下载 $ref ..."
|
||||
$plainUrl = "https://${host_}/${repoPath}.git"
|
||||
$credInput = "protocol=https`nhost=${host_}`nusername=$($cred.user)`npassword=$($cred.pass)`n`n"
|
||||
$credInput | & git credential approve 2>$null
|
||||
$cloneArgs = @('-c', 'core.longpaths=true', 'clone', '--depth', '1', '--branch', $ref, '--single-branch', $plainUrl, $stageDir)
|
||||
& git @cloneArgs 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "clone 失败 (exit $LASTEXITCODE)" }
|
||||
$gitDir = Join-Path $stageDir '.git'
|
||||
if (Test-Path $gitDir) { Remove-Item -Recurse -Force $gitDir }
|
||||
|
||||
# 2. 验签 (Ed25519)
|
||||
Write-Ota "验证签名..."
|
||||
$integrityPath = Join-Path $stageDir 'INTEGRITY.sha256'
|
||||
$sigPath = Join-Path $stageDir 'INTEGRITY.sha256.sig'
|
||||
if (-not (Test-Path $integrityPath) -or -not (Test-Path $sigPath)) {
|
||||
throw "包缺失签名文件"
|
||||
}
|
||||
$verifyScript = @'
|
||||
const fs=require('fs'),crypto=require('crypto');
|
||||
const d=fs.readFileSync(process.argv[2]),s=Buffer.from(fs.readFileSync(process.argv[3],'utf8').trim(),'hex');
|
||||
const k=crypto.createPublicKey(fs.readFileSync(process.argv[4],'utf8'));
|
||||
process.exit(crypto.verify(null,d,k,s)?0:1);
|
||||
'@
|
||||
$tmpV = Join-Path $env:TEMP "bw-ota-verify-$([Guid]::NewGuid().ToString().Substring(0,8)).js"
|
||||
[IO.File]::WriteAllText($tmpV, $verifyScript, [Text.UTF8Encoding]::new($false))
|
||||
node $tmpV $integrityPath $sigPath $PubKeyFile 2>&1 | Out-Null
|
||||
$sigOk = $LASTEXITCODE -eq 0
|
||||
Remove-Item $tmpV -Force -ErrorAction SilentlyContinue
|
||||
if (-not $sigOk) { throw "Ed25519 验签失败! 包可能被篡改." }
|
||||
Write-OtaOk "签名验证通过"
|
||||
|
||||
# 3. 逐文件哈希
|
||||
Write-Ota "校验文件完整性..."
|
||||
$lines = Get-Content $integrityPath
|
||||
$fail = 0
|
||||
foreach ($line in $lines) {
|
||||
if ($line -notmatch '^([a-f0-9]{64})\s{2}(.+)$') { throw "INTEGRITY 格式错误" }
|
||||
$expected = $Matches[1]; $relPath = $Matches[2]
|
||||
$abs = Join-Path $stageDir $relPath
|
||||
if (-not (Test-Path $abs -PathType Leaf)) { continue }
|
||||
$actual = (Get-FileHash -Path $abs -Algorithm SHA256).Hash.ToLower()
|
||||
if ($actual -ne $expected) { $fail++ }
|
||||
}
|
||||
if ($fail -gt 0) { throw "完整性校验失败: $fail 处哈希不匹配" }
|
||||
Write-OtaOk "文件完整性校验通过 ($($lines.Count) 个文件)"
|
||||
|
||||
# 4. 渲染 settings.template.json
|
||||
$tplPath = Join-Path $stageDir 'settings.template.json'
|
||||
if (Test-Path $tplPath) {
|
||||
$raw = Get-Content -Raw $tplPath
|
||||
if ($raw.Length -gt 0 -and [int][char]$raw[0] -eq 0xFEFF) { $raw = $raw.Substring(1) }
|
||||
$rootFwd = $ClaudeRoot -replace '\\', '/'
|
||||
$homeFwd = $env:USERPROFILE -replace '\\', '/'
|
||||
$rendered = $raw -replace '\{\{CLAUDE_ROOT\}\}', $rootFwd -replace '\{\{HOME\}\}', $homeFwd
|
||||
$outPath = Join-Path $stageDir 'settings.json'
|
||||
[IO.File]::WriteAllText($outPath, $rendered, [Text.UTF8Encoding]::new($false))
|
||||
}
|
||||
|
||||
# 5. 保留本机私有
|
||||
$preserveList = @(
|
||||
'memory', 'projects', 'sessions', 'session-env', 'session-state', 'sessions.db',
|
||||
'tasks', 'teams',
|
||||
'pinned-sessions.json', 'pinned-sessions.json.tmp',
|
||||
'history.jsonl', 'evolution-log.jsonl',
|
||||
'.credentials.json', '.bw-token', '.hmac-key', '.skill-cache',
|
||||
'file-history', 'image-cache', 'paste-cache', 'debug', 'telemetry',
|
||||
'cache', 'plans', 'plugins', 'shell-snapshots', 'vendor',
|
||||
'repos', 'backups', 'archives',
|
||||
'mcp-servers', 'node_modules',
|
||||
'settings.local.json', 'settings.json.bak.*',
|
||||
'auto-sync-repos.json', 'scheduled_tasks.lock',
|
||||
'.bw-ota'
|
||||
)
|
||||
$preserved = 0
|
||||
if (Test-Path $ClaudeRoot) {
|
||||
foreach ($pat in $preserveList) {
|
||||
$items = @(Get-ChildItem -Path $ClaudeRoot -Filter $pat -Force -ErrorAction SilentlyContinue)
|
||||
foreach ($item in $items) {
|
||||
$target = Join-Path $stageDir $item.Name
|
||||
if (Test-Path $target) { Remove-Item -Recurse -Force $target }
|
||||
Copy-Item -Path $item.FullName -Destination $target -Recurse -Force
|
||||
$preserved++
|
||||
}
|
||||
}
|
||||
}
|
||||
Write-Ota "已保留 $preserved 项本机数据 (copy)"
|
||||
|
||||
# DryRun: 验证到此为止, 不做原子替换
|
||||
if ($DryRun) {
|
||||
$fileCount = (Get-ChildItem -Path $stageDir -Recurse -File).Count
|
||||
Write-OtaOk "[DryRun] 验证通过! staging 含 $fileCount 个文件, 跳过原子替换"
|
||||
Remove-Item -Recurse -Force $stageDir -ErrorAction SilentlyContinue
|
||||
return
|
||||
}
|
||||
|
||||
# 6. 备份 + 原子替换 (先备份再替换, 任一步失败不丢数据)
|
||||
$bakFull = $null
|
||||
if (Test-Path $ClaudeRoot) {
|
||||
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$bakLeaf = ".claude.bak-$ts"
|
||||
$bakFull = Join-Path (Split-Path -Parent $ClaudeRoot) $bakLeaf
|
||||
try {
|
||||
Rename-Item -Path $ClaudeRoot -NewName $bakLeaf -ErrorAction Stop
|
||||
} catch {
|
||||
throw "无法备份 .claude (可能被占用): $_"
|
||||
}
|
||||
}
|
||||
try {
|
||||
Copy-Item -Path $stageDir -Destination $ClaudeRoot -Recurse -Force -ErrorAction Stop
|
||||
Remove-Item -Path $stageDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
} catch {
|
||||
if ($bakFull -and (Test-Path $bakFull)) {
|
||||
Remove-Item -Path $ClaudeRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Rename-Item -Path $bakFull -NewName (Split-Path -Leaf $ClaudeRoot)
|
||||
Write-OtaWarn "替换失败, 已回滚到原版本"
|
||||
}
|
||||
throw "替换失败: $_"
|
||||
}
|
||||
|
||||
# 7. 清理旧备份 (保留最近 3 个)
|
||||
$parent = Split-Path -Parent $ClaudeRoot
|
||||
$baks = @(Get-ChildItem -Path $parent -Directory -Filter ".claude.bak-*" -ErrorAction SilentlyContinue | Sort-Object Name -Descending)
|
||||
if ($baks.Count -gt 3) {
|
||||
$baks | Select-Object -Skip 3 | ForEach-Object {
|
||||
Remove-Item -Recurse -Force $_.FullName -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# 8. 写入 VERSION 到新 .claude
|
||||
$versionPath = Join-Path $ClaudeRoot 'VERSION'
|
||||
if (-not (Test-Path $versionPath)) {
|
||||
[IO.File]::WriteAllText($versionPath, "$remoteVersion`n", [Text.UTF8Encoding]::new($false))
|
||||
}
|
||||
|
||||
Write-OtaOk "更新完成! $ref"
|
||||
}
|
||||
|
||||
# ========== 主流程 (fail-open 包裹) ==========
|
||||
function Invoke-OtaMain {
|
||||
try {
|
||||
# 基础设施检查
|
||||
if (-not (Test-OtaReady)) {
|
||||
return
|
||||
}
|
||||
|
||||
$cfg = Read-OtaConfig
|
||||
if (-not $cfg) { return }
|
||||
|
||||
# 冷却检查
|
||||
if (Test-Cooldown $cfg) {
|
||||
return
|
||||
}
|
||||
|
||||
# 解密凭证
|
||||
$cred = Read-Credential
|
||||
if (-not $cred -or -not $cred.user -or -not $cred.pass) {
|
||||
Write-OtaWarn "凭证解密失败, 跳过更新检查"
|
||||
return
|
||||
}
|
||||
|
||||
# 本地 vs 远端版本
|
||||
$localVer = Get-LocalVersion
|
||||
if ($DryRun) { Write-Ota "[DryRun 模式] 仅验证, 不替换文件" 'Yellow' }
|
||||
Write-Ota "当前版本: $localVer"
|
||||
|
||||
$remoteVer = Get-RemoteVersion $cred $cfg
|
||||
if (-not $remoteVer) {
|
||||
$remoteVer = Get-RemoteLatestTag $cred $cfg
|
||||
if ($remoteVer) { $remoteVer = $remoteVer -replace '^v', '' }
|
||||
}
|
||||
|
||||
# 更新 lastCheck
|
||||
$cfg.lastCheck = [int][DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
||||
Save-OtaConfig $cfg
|
||||
|
||||
if (-not $remoteVer) {
|
||||
Write-OtaWarn "无法获取远端版本, 跳过"
|
||||
return
|
||||
}
|
||||
|
||||
$localClean = ($localVer -replace '^v', '').Trim()
|
||||
$remoteClean = ($remoteVer -replace '^v', '').Trim()
|
||||
|
||||
if ($localClean -eq $remoteClean) {
|
||||
Write-OtaOk "v$localClean 已是最新"
|
||||
return
|
||||
}
|
||||
|
||||
$cmp = Compare-SemVer $localClean $remoteClean
|
||||
if ($cmp -le 0) {
|
||||
Write-OtaOk "v$localClean 已是最新 (远端 v$remoteClean 非更高版本)"
|
||||
return
|
||||
}
|
||||
|
||||
# 发现新版本 (远端 > 本地)
|
||||
Write-Host ""
|
||||
Write-Ota "发现新版本 v$remoteClean (当前 v$localClean)" 'Magenta'
|
||||
|
||||
if (-not $SkipConfirm) {
|
||||
Write-Host "[Bookworm OTA] 按 Enter 更新 / Ctrl+C 跳过: " -ForegroundColor Yellow -NoNewline
|
||||
try { [void][Console]::ReadLine() }
|
||||
catch {
|
||||
Write-Ota "已跳过更新"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Invoke-OtaSync $cred $cfg $remoteClean
|
||||
Write-Host ""
|
||||
}
|
||||
catch {
|
||||
Write-OtaWarn "更新检查异常: $($_.Exception.Message)"
|
||||
Write-OtaWarn "跳过更新, 继续启动..."
|
||||
}
|
||||
}
|
||||
|
||||
Invoke-OtaMain
|
||||
139
install.ps1
139
install.ps1
@ -50,8 +50,8 @@ if (-not $opensslCmd) {
|
||||
function Write-Banner {
|
||||
Write-Host ""
|
||||
Write-Host " +------------------------------------------+" -ForegroundColor Cyan
|
||||
Write-Host " | Bookworm Portable Installer v1.6 |" -ForegroundColor Cyan
|
||||
Write-Host " | Claude Code 国内一键就绪 |" -ForegroundColor Cyan
|
||||
Write-Host " | Bookworm Portable Installer v1.5 |" -ForegroundColor Cyan
|
||||
Write-Host " | 92 Skills / 18 Agents / 34 Hooks |" -ForegroundColor Cyan
|
||||
Write-Host " +------------------------------------------+" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
}
|
||||
@ -172,79 +172,39 @@ function Install-MissingDeps {
|
||||
|
||||
# ─── 桌面快捷方式 ────────────────────────────────────
|
||||
function New-DesktopShortcuts {
|
||||
# v3.0.11 架构重构: .lnk 直调 pwsh + claude.ps1 绝对路径 (1 跳直链)
|
||||
$desktop = [System.Environment]::GetFolderPath("Desktop")
|
||||
$bootDir = $ScriptDir
|
||||
|
||||
# 步骤 1: 确保新「启动Bookworm.lnk」存在 (缺失时创建)
|
||||
$lnkPath = Join-Path $desktop "启动Bookworm.lnk"
|
||||
|
||||
# 定位 pwsh.exe
|
||||
$pwshExe = (Get-Command pwsh -ErrorAction SilentlyContinue).Source
|
||||
if (-not $pwshExe) {
|
||||
foreach ($p in @("$env:ProgramFiles\PowerShell\7\pwsh.exe", "${env:ProgramFiles(x86)}\PowerShell\7\pwsh.exe")) {
|
||||
if (Test-Path $p) { $pwshExe = $p; break }
|
||||
}
|
||||
}
|
||||
if (-not $pwshExe) {
|
||||
Write-Host " [!] pwsh.exe 未找到, 跳过桌面快捷方式 (建议先装 PS7)" -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
|
||||
# v3.1.0: 优先用 wrapper bw-launch.ps1, 闭合 claude.ps1 路径 stale (L4)
|
||||
$bwLaunchPs1 = Join-Path $bootDir "bw-launch.ps1"
|
||||
if (-not (Test-Path $bwLaunchPs1)) {
|
||||
Write-Host " [!] bw-launch.ps1 wrapper 未找到, 跳过桌面快捷方式" -ForegroundColor Yellow
|
||||
Write-Host " $bwLaunchPs1 应由 bookworm-boot git 仓库提供" -ForegroundColor Gray
|
||||
return
|
||||
}
|
||||
|
||||
# 装机时自检 claude.ps1 当前可达 (运行时由 wrapper 兜底)
|
||||
$claudeCheck = $null
|
||||
$claudeCmd = Get-Command claude -ErrorAction SilentlyContinue
|
||||
if ($claudeCmd -and $claudeCmd.Source -and $claudeCmd.Source.EndsWith('.ps1')) { $claudeCheck = $claudeCmd.Source }
|
||||
if (-not $claudeCheck) {
|
||||
if (-not (Test-Path $lnkPath)) {
|
||||
try {
|
||||
$npmPrefix = (& npm config get prefix 2>$null | Select-Object -First 1).Trim()
|
||||
$cand = Join-Path $npmPrefix "claude.ps1"
|
||||
if (Test-Path $cand) { $claudeCheck = $cand }
|
||||
} catch {}
|
||||
}
|
||||
if (-not $claudeCheck) {
|
||||
Write-Host " [!] claude.ps1 装机时不可达, 跳过桌面快捷方式" -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
$shell = New-Object -ComObject WScript.Shell
|
||||
$shortcut = $shell.CreateShortcut($lnkPath)
|
||||
$shortcut.TargetPath = $pwshExe
|
||||
$shortcut.Arguments = "-NoLogo -NoExit -ExecutionPolicy Bypass -File `"$bwLaunchPs1`" --dangerously-skip-permissions"
|
||||
$shortcut.WorkingDirectory = $env:USERPROFILE
|
||||
$shortcut.Description = "Bookworm Smart Assistant (v3.1.0 wrapper)"
|
||||
$iconPath = Join-Path $bootDir "bookworm-desktop.ico"
|
||||
if (-not (Test-Path $iconPath)) { $iconPath = Join-Path $bootDir "bookworm.ico" }
|
||||
if (Test-Path $iconPath) { $shortcut.IconLocation = "$iconPath,0" }
|
||||
$shortcut.Save()
|
||||
|
||||
# 自验证 (4 项)
|
||||
$verify = $shell.CreateShortcut($lnkPath)
|
||||
$okTarget = $verify.TargetPath -eq $pwshExe
|
||||
$okPath = $verify.Arguments -match [regex]::Escape($bwLaunchPs1)
|
||||
$okPerm = $verify.Arguments -match "--dangerously-skip-permissions"
|
||||
$okBypass = $verify.Arguments -match "-ExecutionPolicy Bypass"
|
||||
if ($okTarget -and $okPath -and $okPerm -and $okBypass) {
|
||||
Write-Host " [OK] 桌面快捷方式已创建并通过 4 项自验证 (v3.1.0 wrapper)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " [!] 桌面快捷方式自验证失败 (Target=$okTarget Path=$okPath Perm=$okPerm Bypass=$okBypass)" -ForegroundColor Yellow
|
||||
Remove-Item $lnkPath -Force -EA SilentlyContinue
|
||||
$shell = New-Object -ComObject WScript.Shell
|
||||
$shortcut = $shell.CreateShortcut($lnkPath)
|
||||
$scriptPath = Join-Path $bootDir "install.ps1"
|
||||
$hasPwsh = [bool](Get-Command pwsh -ErrorAction SilentlyContinue)
|
||||
$psExe = if ($hasPwsh) { (Get-Command pwsh).Source } else { "powershell.exe" }
|
||||
$shortcut.TargetPath = $psExe
|
||||
$shortcut.Arguments = "-NoLogo -ExecutionPolicy Bypass -Command `"Set-Item Env:NO_PROXY 'bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1'; & '$scriptPath' -StartOnly -AutoAccept`""
|
||||
$shortcut.WorkingDirectory = $bootDir
|
||||
$shortcut.Description = "Bookworm Smart Assistant"
|
||||
$shortcut.Save()
|
||||
$psVer = if ($hasPwsh) { "PowerShell 7" } else { "PowerShell 5.1" }
|
||||
Write-Host " [OK] 桌面快捷方式已创建: 启动Bookworm ($psVer)" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host " [!] 桌面快捷方式创建失败 (不影响使用)" -ForegroundColor Gray
|
||||
}
|
||||
} catch {
|
||||
Write-Host " [!] 桌面快捷方式创建失败: $_" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# 迁移清理老 Bookworm.lnk
|
||||
# 步骤 2: 迁移清理老 Bookworm.lnk (v3.0.3 及以前命名), 必须在新 lnk 确认存在后, 防空窗
|
||||
$oldLnk = Join-Path $desktop "Bookworm.lnk"
|
||||
if ((Test-Path $oldLnk) -and (Test-Path $lnkPath)) {
|
||||
try { Remove-Item -LiteralPath $oldLnk -Force -ErrorAction Stop } catch {}
|
||||
try {
|
||||
Remove-Item -LiteralPath $oldLnk -Force -ErrorAction Stop
|
||||
Write-Host " [MIGRATE] 已清理旧快捷方式 Bookworm.lnk (已替换为启动Bookworm)" -ForegroundColor DarkGray
|
||||
} catch {
|
||||
Write-Host " [!] 无法清理旧 Bookworm.lnk (不影响使用)" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -469,46 +429,14 @@ foreach ($c in $checks) {
|
||||
if (-not (Test-Command "claude") -or -not (Test-Command "node") -or -not (Test-Command "git")) {
|
||||
Install-MissingDeps
|
||||
}
|
||||
# v3.0.5: 再次验证 — StartOnly 场景加 GUI 弹窗 (防止 console 闪退用户看不见)
|
||||
# 触发条件: 用户双击老快捷方式, 但 Phase 1 之前失败导致 claude/node 未装
|
||||
function Show-MissingDepGui {
|
||||
param([string]$depName, [string]$installCmd)
|
||||
try {
|
||||
Add-Type -AssemblyName System.Windows.Forms -EA Stop
|
||||
$msg = @"
|
||||
检测到 $depName 未安装,无法启动 Bookworm。
|
||||
|
||||
最可能原因:
|
||||
上次安装器未完成 (Phase 1 环境检测被中断)
|
||||
|
||||
【推荐修复】
|
||||
1. 双击桌面或下载目录的 Bookworm-Setup.exe
|
||||
2. 安装器会自动补装缺失的依赖
|
||||
3. 已装好的部分会被跳过, 不会重复
|
||||
|
||||
【手动修复】
|
||||
$installCmd
|
||||
|
||||
完成后再次点击启动快捷方式即可。
|
||||
"@
|
||||
[System.Windows.Forms.MessageBox]::Show($msg, "Bookworm 启动失败 — $depName 未安装", 'OK', 'Error') | Out-Null
|
||||
} catch { }
|
||||
}
|
||||
|
||||
# 再次验证
|
||||
if (-not (Test-Command "claude")) {
|
||||
Write-Host "`n [ABORT] Claude Code 未安装" -ForegroundColor Red
|
||||
Write-Host " 安装: npm i -g @anthropic-ai/claude-code" -ForegroundColor Gray
|
||||
if ($StartOnly) { Show-MissingDepGui "Claude Code" "npm i -g @anthropic-ai/claude-code" }
|
||||
exit 1
|
||||
}
|
||||
if (-not (Test-Command "node")) {
|
||||
Write-Host "`n [ABORT] Node.js 未安装" -ForegroundColor Red
|
||||
if ($StartOnly) { Show-MissingDepGui "Node.js" "https://nodejs.org/zh-cn/download 下载 LTS .msi" }
|
||||
exit 1
|
||||
}
|
||||
if (-not (Test-Command "git")) {
|
||||
Write-Host "`n [ABORT] Git 未安装" -ForegroundColor Red
|
||||
if ($StartOnly) { Show-MissingDepGui "Git" "https://git-scm.com/download/win 下载 64-bit" }
|
||||
exit 1
|
||||
}
|
||||
|
||||
@ -720,20 +648,19 @@ if (Test-Path $claudeMd) {
|
||||
$bwChecks += @{ Name = "CLAUDE.md (文件不存在!)"; OK = $false }
|
||||
}
|
||||
|
||||
# v3.0.5: 阈值按脱敏分发版 (bookworm-portable-config.git) 实际内容定
|
||||
# 管理员自用的完整版 (bookworm-config.git) 含 90+ skills, 分发版精简到核心 14+
|
||||
# 检查 Skills
|
||||
$skillCount = 0
|
||||
if (Test-Path $skillsDir) {
|
||||
$skillCount = (Get-ChildItem $skillsDir -Directory -ErrorAction SilentlyContinue).Count
|
||||
}
|
||||
$bwChecks += @{ Name = "Skills ($skillCount 个)"; OK = ($skillCount -ge 10) }
|
||||
$bwChecks += @{ Name = "Skills ($skillCount 个)"; OK = ($skillCount -gt 50) }
|
||||
|
||||
# 检查 Hooks
|
||||
$hookCount = 0
|
||||
if (Test-Path $hooksDir) {
|
||||
$hookCount = (Get-ChildItem $hooksDir -Filter "*.js" -File -ErrorAction SilentlyContinue).Count
|
||||
}
|
||||
$bwChecks += @{ Name = "Hooks ($hookCount 个)"; OK = ($hookCount -ge 3) }
|
||||
$bwChecks += @{ Name = "Hooks ($hookCount 个)"; OK = ($hookCount -gt 10) }
|
||||
|
||||
# 检查 settings.json hooks 配置
|
||||
$hasHooks = $false
|
||||
@ -755,12 +682,12 @@ foreach ($c in $bwChecks) {
|
||||
if (-not $allOK) {
|
||||
Write-Host ""
|
||||
Write-Host " ╔══════════════════════════════════════════════════════╗" -ForegroundColor Yellow
|
||||
Write-Host " ║ [!] 警告: Bookworm 系统核心资产不足 ║" -ForegroundColor Yellow
|
||||
Write-Host " ║ [!] 警告: Bookworm 系统不完整 ║" -ForegroundColor Yellow
|
||||
Write-Host " ║ Claude Code 将以原生模式启动 (无 Skills/Hooks) ║" -ForegroundColor Yellow
|
||||
Write-Host " ║ 建议: 检查网络后不加 -StartOnly 重新运行同步 ║" -ForegroundColor Yellow
|
||||
Write-Host " ║ 建议: 不加 -StartOnly 重新运行 install.ps1 同步 ║" -ForegroundColor Yellow
|
||||
Write-Host " ╚══════════════════════════════════════════════════════╝" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host " [OK] Bookworm 分发版就绪 ($skillCount Skills / $hookCount Hooks / Settings)" -ForegroundColor Green
|
||||
Write-Host " [OK] Bookworm 系统完整 ($skillCount Skills / $hookCount Hooks)" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# --- MCP 依赖检查 (中文提醒) ---
|
||||
|
||||
@ -137,7 +137,7 @@ function startSSEHeartbeat(res) {
|
||||
async function proxyChat(opts, res) {
|
||||
const {
|
||||
apiKey,
|
||||
model = 'claude-opus-4-7',
|
||||
model = 'claude-sonnet-4-5-20250514',
|
||||
messages,
|
||||
maxTokens = 8192,
|
||||
stream = false,
|
||||
|
||||
@ -1,218 +0,0 @@
|
||||
# Bookworm Portable 启动器 bat 生成工具 (v3.0.6)
|
||||
# 用途: 从单一明文 PowerShell 脚本生成两个 bat, 避免手工同步 Base64 字符串不一致
|
||||
# 用法: pwsh -NoProfile -File tools/gen-launcher-bats.ps1
|
||||
# 输出: 启动Bookworm.bat + 更新并启动Bookworm.bat (覆盖写入)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$launchBat = Join-Path $repoRoot "启动Bookworm.bat"
|
||||
$updateBat = Join-Path $repoRoot "更新并启动Bookworm.bat"
|
||||
|
||||
# ─── 明文: 三层 PATH 修复 + DPAPI 加载 + claude 诊断 + 启动 ─────────
|
||||
# v3.0.9: 增加 npm config get prefix 动态查询, 兼容 nvm/fnm/Program Files 等非标准 npm 位置
|
||||
$plainScript = @'
|
||||
Add-Type -AssemblyName System.Security
|
||||
# 层 1: Machine + User env PATH (标准 Windows 环境变量)
|
||||
$env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User')
|
||||
# 层 2: npm config get prefix (真实 npm 全局目录, 兼容 nvm/fnm/标准安装/Program Files)
|
||||
try {
|
||||
$npmPrefix = (& npm config get prefix 2>$null | Select-Object -First 1).Trim()
|
||||
if ($npmPrefix -and (Test-Path $npmPrefix) -and ($env:Path -notlike "*$npmPrefix*")) {
|
||||
$env:Path = "$npmPrefix;$env:Path"
|
||||
}
|
||||
} catch {}
|
||||
# 层 3: 常见 npm global 硬编码兜底 (npm 本身不在 PATH 时无法 query)
|
||||
$npmCandidates = @(
|
||||
"$env:APPDATA\npm",
|
||||
"$env:ProgramFiles\nodejs",
|
||||
"${env:ProgramFiles(x86)}\nodejs",
|
||||
"$env:LOCALAPPDATA\npm"
|
||||
)
|
||||
foreach ($p in $npmCandidates) {
|
||||
if (-not (Test-Path $p)) { continue }
|
||||
$hasClaude = (Test-Path (Join-Path $p 'claude.ps1')) -or (Test-Path (Join-Path $p 'claude.cmd')) -or (Test-Path (Join-Path $p 'claude'))
|
||||
if ($hasClaude -and ($env:Path -notlike "*$p*")) {
|
||||
$env:Path = "$p;$env:Path"
|
||||
}
|
||||
}
|
||||
# DPAPI 加载缓存凭证
|
||||
$r = 'HKCU:\Software\Bookworm\CachedEnv'
|
||||
try {
|
||||
(Get-ItemProperty $r -EA Stop).PSObject.Properties | Where-Object { $_.Name -match '^[A-Z_]+$' } | ForEach-Object {
|
||||
$v = $_.Value
|
||||
try {
|
||||
$b = [Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String($v), $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
|
||||
$v = [Text.Encoding]::UTF8.GetString($b)
|
||||
} catch {}
|
||||
[Environment]::SetEnvironmentVariable($_.Name, $v, 'Process')
|
||||
}
|
||||
} catch {}
|
||||
if (-not (Get-Command claude -ErrorAction SilentlyContinue)) {
|
||||
Write-Host ''
|
||||
Write-Host ' [!] claude 命令未找到 (已尝试 3 层 PATH 修复仍失败)' -ForegroundColor Red
|
||||
Write-Host ''
|
||||
Write-Host ' 诊断信息:' -ForegroundColor Yellow
|
||||
Write-Host " npm prefix: $(try { (& npm config get prefix 2>$null) } catch { '(npm 不可用)' })" -ForegroundColor Gray
|
||||
Write-Host ' PATH 片段 (npm/nodejs/pwsh/Git):' -ForegroundColor Gray
|
||||
($env:Path -split ';') | Where-Object { $_ -match 'npm|nodejs|pwsh|Git' } | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray }
|
||||
Write-Host ''
|
||||
Write-Host ' 修复: 重新运行 Bookworm-Setup.exe (v3.0.9+) 即可自动补全' -ForegroundColor Green
|
||||
Write-Host ''
|
||||
Read-Host '按回车关闭'
|
||||
return
|
||||
}
|
||||
& claude --dangerously-skip-permissions
|
||||
'@
|
||||
|
||||
# ─── Base64-UTF-16LE 编码 ─────────────────────────────────
|
||||
$bytes = [System.Text.Encoding]::Unicode.GetBytes($plainScript)
|
||||
$enc = [Convert]::ToBase64String($bytes)
|
||||
|
||||
# 健康检查
|
||||
if ($enc.Length -gt 7500) { throw "Base64 长度 $($enc.Length) 超 bat 变量安全上限 7500" }
|
||||
$bad = $enc -replace '[A-Za-z0-9+/=]', ''
|
||||
if ($bad) { throw "Base64 含非法字符: [$bad]" }
|
||||
|
||||
Write-Host "[gen-launcher-bats] Base64 长度: $($enc.Length), 纯字符集检查 OK" -ForegroundColor Green
|
||||
|
||||
# ─── bat 1: 启动Bookworm.bat ──────────────────────────────
|
||||
$launch = @"
|
||||
@echo off
|
||||
chcp 65001 > nul
|
||||
cd /d "%~dp0"
|
||||
|
||||
:: 中转站在国内,不走代理
|
||||
set NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1
|
||||
set no_proxy=%NO_PROXY%
|
||||
|
||||
:: 静默自动更新 (bookworm-boot + .claude 配置, 失败不阻断启动)
|
||||
echo [..] 检查更新...
|
||||
git pull --rebase >nul 2>nul
|
||||
git -C "%USERPROFILE%\.claude" pull --rebase >nul 2>nul
|
||||
|
||||
set USE_WT=0
|
||||
where wt >nul 2>nul && set USE_WT=1
|
||||
|
||||
set USE_PWSH7=0
|
||||
where pwsh >nul 2>nul && set USE_PWSH7=1
|
||||
|
||||
:: v3.0.6: Base64-UTF-16LE (PATH 重载 + DPAPI 凭证加载 + claude 诊断 + 启动)
|
||||
:: 纯 A-Za-z0-9+/= 字符集, 避免 wt.exe 的 ';' 切 tab 误切 (修复 64856bc 症状一)
|
||||
:: -d "%CD%" 无尾反斜杠, 避免 -d "%~dp0" 的转义引号 (修复 0c33109 症状二)
|
||||
:: 重新生成: pwsh -NoProfile -File tools/gen-launcher-bats.ps1
|
||||
set ENC=$enc
|
||||
|
||||
:: 优先路径: wt + pwsh7
|
||||
if %USE_WT% equ 1 if %USE_PWSH7% equ 1 (
|
||||
start "" wt new-tab --title "Bookworm Smart Assistant" -d "%CD%" -- pwsh -NoLogo -NoExit -EncodedCommand %ENC%
|
||||
exit
|
||||
)
|
||||
|
||||
:: 路径 2: wt + powershell 5.1
|
||||
if %USE_WT% equ 1 if %USE_PWSH7% equ 0 (
|
||||
start "" wt new-tab --title "Bookworm Smart Assistant" -d "%CD%" -- powershell -NoLogo -ExecutionPolicy Bypass -NoExit -EncodedCommand %ENC%
|
||||
exit
|
||||
)
|
||||
|
||||
:: 路径 3: conhost + pwsh7 (无 wt 就不会有 ; 切 tab 问题, 但仍用 Base64 统一)
|
||||
if %USE_PWSH7% equ 1 (
|
||||
start "Bookworm Smart Assistant" pwsh -NoLogo -NoExit -EncodedCommand %ENC%
|
||||
exit
|
||||
)
|
||||
|
||||
:: 路径 4: 回退 PowerShell 5.1 (最低保障, 交给 install.ps1 -StartOnly 处理)
|
||||
title Bookworm Portable
|
||||
echo.
|
||||
echo [!] PowerShell 7 未安装, 使用 PowerShell 5.1
|
||||
echo.
|
||||
powershell -ExecutionPolicy Bypass -File install.ps1 -StartOnly -AutoAccept
|
||||
if %errorlevel% neq 0 (
|
||||
echo.
|
||||
echo 启动失败,按任意键退出...
|
||||
pause > nul
|
||||
)
|
||||
"@
|
||||
|
||||
# ─── bat 2: 更新并启动Bookworm.bat ───────────────────────
|
||||
$update = @"
|
||||
@echo off
|
||||
chcp 65001 > nul
|
||||
cd /d "%~dp0"
|
||||
|
||||
:: 中转站在国内,不走代理
|
||||
set NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1
|
||||
set no_proxy=%NO_PROXY%
|
||||
|
||||
:: 静默自动更新 (bookworm-boot + .claude 配置)
|
||||
echo [..] 同步更新...
|
||||
git pull --rebase >nul 2>nul
|
||||
git -C "%USERPROFILE%\.claude" pull --rebase >nul 2>nul
|
||||
|
||||
:: v3.0.6: 同启动Bookworm.bat 的 Base64 (DPAPI + PATH 重载 + claude 启动)
|
||||
set ENC=$enc
|
||||
|
||||
:: 检测 pwsh7 可用性
|
||||
where pwsh >nul 2>nul
|
||||
if %errorlevel% equ 0 (
|
||||
:: pwsh7: 先同步配置 (SkipLaunch 不启动 claude), 再用 -EncodedCommand 在新窗口启动
|
||||
pwsh -NoLogo -ExecutionPolicy Bypass -File "%~dp0install.ps1" -AutoAccept -SkipLaunch
|
||||
start "Bookworm Smart Assistant" pwsh -NoLogo -NoExit -EncodedCommand %ENC%
|
||||
exit
|
||||
)
|
||||
|
||||
:: 回退 PowerShell 5.1: 一次调用完成更新+加载凭证+启动 (消除双次调用)
|
||||
title Bookworm Portable
|
||||
powershell -ExecutionPolicy Bypass -File "%~dp0install.ps1" -AutoAccept
|
||||
if %errorlevel% neq 0 (
|
||||
echo.
|
||||
echo 启动失败,按任意键退出...
|
||||
pause > nul
|
||||
)
|
||||
"@
|
||||
|
||||
# ─── 写入 ─────────────────────────────────────────────────
|
||||
# bat 文件默认期望 GBK/ANSI, 但脚本顶部 chcp 65001 已切换到 UTF-8, 用无 BOM UTF-8 写入
|
||||
[System.IO.File]::WriteAllText($launchBat, $launch, [System.Text.UTF8Encoding]::new($false))
|
||||
[System.IO.File]::WriteAllText($updateBat, $update, [System.Text.UTF8Encoding]::new($false))
|
||||
Write-Host "[gen-launcher-bats] ✓ 启动Bookworm.bat ($((Get-Item $launchBat).Length) bytes)" -ForegroundColor Green
|
||||
Write-Host "[gen-launcher-bats] ✓ 更新并启动Bookworm.bat ($((Get-Item $updateBat).Length) bytes)" -ForegroundColor Green
|
||||
|
||||
# ─── Round-trip 验证 (v3.0.10: 除了 PARSE 还要 lint + 实跑不启动 claude) ────
|
||||
$decoded = [System.Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($enc))
|
||||
$err = $null
|
||||
[void][System.Management.Automation.Language.Parser]::ParseInput($decoded, [ref]$null, [ref]$err)
|
||||
if ($err) { throw "解码后脚本 PARSE ERR: $($err[0])" }
|
||||
Write-Host "[gen-launcher-bats] ✓ PARSE OK ($($decoded.Length) chars)" -ForegroundColor Green
|
||||
|
||||
# 实跑验证 (v3.0.10: 截断到 & claude 之前, 只跑 PATH 修复 + DPAPI 加载, 不启动 claude)
|
||||
# 这能抓出 PARSE 通过但运行时报错的 bug (例如 -or 被当 Test-Path 参数)
|
||||
$runnable = $decoded -replace '& claude --dangerously-skip-permissions', 'Write-Host "__BW_DRYRUN_OK__"'
|
||||
$tmpPs1 = Join-Path $env:TEMP "bw-launcher-dryrun-$(Get-Random).ps1"
|
||||
Set-Content -Path $tmpPs1 -Value $runnable -Encoding UTF8
|
||||
try {
|
||||
$dryRunOutput = (& pwsh -NoProfile -ExecutionPolicy Bypass -File $tmpPs1 2>&1 | Out-String)
|
||||
# 只抓真正的 PS 错误 (ErrorRecord / cannot be found / parameter name / 等)
|
||||
$errorPatterns = @(
|
||||
'cannot be found that matches parameter name',
|
||||
'A parameter cannot be found',
|
||||
'CommandNotFoundException',
|
||||
'ParameterBindingException',
|
||||
'is not recognized as',
|
||||
'cannot find.*because it does not exist',
|
||||
'RuntimeException'
|
||||
)
|
||||
$hasError = $false
|
||||
foreach ($pat in $errorPatterns) {
|
||||
if ($dryRunOutput -match $pat) { $hasError = $true; break }
|
||||
}
|
||||
# 必须看到 dry-run 成功标记才算通过
|
||||
$reachedEnd = $dryRunOutput -match '__BW_DRYRUN_OK__'
|
||||
if ($hasError -or -not $reachedEnd) {
|
||||
Write-Host "[gen-launcher-bats] ✗ 实跑验证失败:" -ForegroundColor Red
|
||||
Write-Host $dryRunOutput -ForegroundColor DarkRed
|
||||
throw "Base64 解码后脚本运行时错误 (hasError=$hasError, reachedEnd=$reachedEnd)"
|
||||
}
|
||||
Write-Host "[gen-launcher-bats] ✓ 实跑通过 (dry-run 到达 __BW_DRYRUN_OK__ 标记)" -ForegroundColor Green
|
||||
} finally {
|
||||
Remove-Item $tmpPs1 -Force -EA SilentlyContinue
|
||||
}
|
||||
@ -1,123 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
v3.1.1: 启动器 .lnk 端到端行为测试
|
||||
|
||||
.DESCRIPTION
|
||||
模拟双击桌面 .lnk → pwsh → bw-launch.ps1 → claude.ps1 链路.
|
||||
检测点:
|
||||
1. PS 进程能拉起 (无 ExecutionPolicy 拒绝 / 无 wt 切 tab 错)
|
||||
2. bw-launch.ps1 PATH 三层重载执行无错
|
||||
3. claude.ps1 解析能找到 (不一定真启动 Claude TUI, 用 --version 探测)
|
||||
4. 整体退出码 = 0 (任何运行时错就拒绝)
|
||||
|
||||
替代手动双击, 集成到 build.ps1 后自动跑, 提前抓 v3.0.10 -or 类 bug.
|
||||
|
||||
.NOTES
|
||||
用法: pwsh -NoProfile -File tools/test-launcher-e2e.ps1
|
||||
退出码: 0=PASS / 1=FAIL
|
||||
日志: $env:TEMP\bw-e2e-test.log
|
||||
#>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$bwLaunchPs1 = Join-Path (Split-Path -Parent $PSScriptRoot) "bw-launch.ps1"
|
||||
$logFile = Join-Path $env:TEMP "bw-e2e-test.log"
|
||||
|
||||
if (-not (Test-Path $bwLaunchPs1)) {
|
||||
Write-Host "[FAIL] bw-launch.ps1 缺失: $bwLaunchPs1" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "[e2e] 测试目标: $bwLaunchPs1" -ForegroundColor Cyan
|
||||
|
||||
# Test 1: PS 解析 wrapper 文件 (静态 syntax)
|
||||
$err = $null
|
||||
[void][System.Management.Automation.Language.Parser]::ParseFile($bwLaunchPs1, [ref]$null, [ref]$err)
|
||||
if ($err) {
|
||||
Write-Host "[FAIL] bw-launch.ps1 PARSE 错: $($err.Count)" -ForegroundColor Red
|
||||
$err | Select-Object -First 3 | ForEach-Object { Write-Host " L$($_.Extent.StartLineNumber): $($_.Message)" -ForegroundColor Red }
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[e2e ✓] Test 1 — bw-launch.ps1 PARSE OK" -ForegroundColor Green
|
||||
|
||||
# Test 2: 实跑 wrapper, claude 不可用场景 (期望 GUI 弹窗 + 退出码 1, 不是闪退)
|
||||
# 用临时 PATH 隔离 claude
|
||||
$pwshExe = (Get-Command pwsh -EA SilentlyContinue).Source
|
||||
if (-not $pwshExe) {
|
||||
Write-Host "[SKIP] pwsh 不可用, 跳过实跑测试" -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 给 wrapper 一个无 claude 的 PATH 子环境, 验证 stale 路径分支不闪退
|
||||
$testScript = @'
|
||||
# 隔离 PATH 让 claude 找不到
|
||||
$env:Path = "C:\Windows\System32"
|
||||
# 关掉 GUI 弹窗 (CI 无 desktop)
|
||||
[System.Reflection.Assembly]::Load('System.Windows.Forms') | Out-Null
|
||||
$env:BW_E2E_NOGUI = "1"
|
||||
'@
|
||||
|
||||
Write-Host "[e2e] Test 2 — wrapper 静态分析 (运行时缺 claude 应清晰报错而非闪退)" -ForegroundColor Cyan
|
||||
$content = Get-Content $bwLaunchPs1 -Raw
|
||||
$expectedFeatures = @{
|
||||
"PATH 三层重载" = ($content -match 'GetEnvironmentVariable.*Machine' -and $content -match 'npm config get prefix')
|
||||
"claude.ps1 fallback 链" = ($content -match 'claude\.ps1' -and $content -match 'Get-Command claude')
|
||||
"失败 GUI 弹窗" = ($content -match 'MessageBox\]::Show')
|
||||
"失败日志写入" = ($content -match 'bw-launch\.log')
|
||||
"args 转发到 claude" = ($content -match '\$claudePs1.*@args' -or $content -match '\$args')
|
||||
"exit code 传播" = ($content -match 'exit \$exitCode' -or $content -match 'exit \$LASTEXITCODE')
|
||||
}
|
||||
$failedFeatures = $expectedFeatures.GetEnumerator() | Where-Object { -not $_.Value } | ForEach-Object { $_.Key }
|
||||
if ($failedFeatures) {
|
||||
Write-Host "[FAIL] wrapper 缺关键特性:" -ForegroundColor Red
|
||||
$failedFeatures | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[e2e ✓] Test 2 — wrapper 6 项特性齐全" -ForegroundColor Green
|
||||
|
||||
# Test 3: .lnk Args 4 项契约验证 (auto-setup.ps1 / install.ps1 一致性)
|
||||
Write-Host "[e2e] Test 3 — .lnk Args 契约 (auto-setup + install 双向一致)" -ForegroundColor Cyan
|
||||
$autoSetup = Get-Content (Join-Path (Split-Path -Parent $PSScriptRoot) "auto-setup.ps1") -Raw
|
||||
$installPs1 = Get-Content (Join-Path (Split-Path -Parent $PSScriptRoot) "install.ps1") -Raw
|
||||
|
||||
$contractChecks = @{
|
||||
"auto-setup.ps1 .lnk Args 含 -ExecutionPolicy Bypass" = ($autoSetup -match '\$shortcut\.Arguments\s*=[^\r\n]*-ExecutionPolicy Bypass')
|
||||
"auto-setup.ps1 .lnk Args 含 bwLaunchPs1 变量" = ($autoSetup -match '\$shortcut\.Arguments\s*=[^\r\n]*\$bwLaunchPs1')
|
||||
"auto-setup.ps1 .lnk Args 含 --skip-permissions" = ($autoSetup -match '\$shortcut\.Arguments\s*=[^\r\n]*dangerously-skip-permissions')
|
||||
"install.ps1 .lnk Args 含 -ExecutionPolicy Bypass" = ($installPs1 -match '\$shortcut\.Arguments\s*=[^\r\n]*-ExecutionPolicy Bypass')
|
||||
"install.ps1 .lnk Args 含 bwLaunchPs1 变量" = ($installPs1 -match '\$shortcut\.Arguments\s*=[^\r\n]*\$bwLaunchPs1')
|
||||
"install.ps1 .lnk Args 含 --skip-permissions" = ($installPs1 -match '\$shortcut\.Arguments\s*=[^\r\n]*dangerously-skip-permissions')
|
||||
}
|
||||
$contractFails = $contractChecks.GetEnumerator() | Where-Object { -not $_.Value } | ForEach-Object { $_.Key }
|
||||
if ($contractFails) {
|
||||
Write-Host "[FAIL] .lnk Args 契约不一致:" -ForegroundColor Red
|
||||
$contractFails | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[e2e ✓] Test 3 — .lnk Args 6 项契约一致" -ForegroundColor Green
|
||||
|
||||
# Test 4: profile 双注入契约
|
||||
Write-Host "[e2e] Test 4 — profile 双注入契约 (PS7 + PS5.1)" -ForegroundColor Cyan
|
||||
$profileChecks = @{
|
||||
"auto-setup.ps1 注入 PS7 profile (Documents\PowerShell)" = ($autoSetup -match 'Documents\\PowerShell.*profile\.ps1' -or $autoSetup -match 'Documents\\\\PowerShell')
|
||||
"auto-setup.ps1 注入 PS5.1 profile (Documents\WindowsPowerShell)" = ($autoSetup -match 'Documents\\WindowsPowerShell' -or $autoSetup -match 'WindowsPowerShell')
|
||||
"auto-setup.ps1 sentinel BW_CRED_START v3.1.0" = ($autoSetup -match 'BW_CRED_START v3\.1\.0')
|
||||
"auto-setup.ps1 字面替换 (String.Replace)" = ($autoSetup -match '\.Replace\(\$match\.Value')
|
||||
}
|
||||
$profileFails = $profileChecks.GetEnumerator() | Where-Object { -not $_.Value } | ForEach-Object { $_.Key }
|
||||
if ($profileFails) {
|
||||
Write-Host "[FAIL] profile 注入契约缺失:" -ForegroundColor Red
|
||||
$profileFails | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[e2e ✓] Test 4 — profile 双注入契约齐全" -ForegroundColor Green
|
||||
|
||||
# All passed
|
||||
Write-Host ""
|
||||
Write-Host "━━━ E2E 测试 SUMMARY ━━━" -ForegroundColor Green
|
||||
Write-Host " Test 1: bw-launch.ps1 PARSE ✓"
|
||||
Write-Host " Test 2: wrapper 6 项特性齐全 ✓"
|
||||
Write-Host " Test 3: .lnk Args 6 项契约一致 ✓"
|
||||
Write-Host " Test 4: profile 双注入契约齐全 ✓"
|
||||
Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Green
|
||||
Write-Host "[PASS] E2E 测试通过 (4/4)" -ForegroundColor Green
|
||||
exit 0
|
||||
@ -1,162 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
v3.1.3: Bookworm 完整卸载实<EFBFBD><EFBFBD>脚本
|
||||
|
||||
.DESCRIPTION
|
||||
由 卸载Bookworm.bat 调用. 集中所有 PS 清理逻辑, 避免 cmd 多行 PS 转义.
|
||||
|
||||
清理项:
|
||||
[1] 桌面 4-5 个 .lnk (启动/更新/体检/卸载/旧 Bookworm.lnk)
|
||||
[2] ~/.claude/ 整个目录 (skills/hooks/agents/凭证)
|
||||
[3] HKCU:\Software\Bookworm (DPAPI 缓存 + Toast 备份)
|
||||
[4] PS7 + PS5.1 双 profile.ps1 的 BW_CRED + BW_CLIP 块
|
||||
[5] HKCU 截图 Toast 设置还原 (从备份)
|
||||
[6] ANTHROPIC_* + BW_LICENSE_KEY User 环境变量
|
||||
[7] 提示用户手动删 bookworm-boot 目录
|
||||
|
||||
保留 (公共依赖, 不主动卸):
|
||||
- Node.js / Git / PowerShell 7 / Claude Code (npm i -g)
|
||||
#>
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
Add-Type -AssemblyName System.Windows.Forms -EA SilentlyContinue
|
||||
|
||||
# ── 二次确认 ──
|
||||
$msg = "确定要卸载 Bookworm 吗?`n`n"
|
||||
$msg += "将删除:`n"
|
||||
$msg += " - 桌面 4-5 个快捷方式`n"
|
||||
$msg += " - ~/.claude 中 Bookworm 注入的内容 (skills/hooks/agents/scripts)`n"
|
||||
$msg += " - HKCU DPAPI 凭证缓存`n"
|
||||
$msg += " - PowerShell profile (PS7+PS5.1) 中的 BW_* 块`n"
|
||||
$msg += " - 还原截图 Toast 默认设置`n"
|
||||
$msg += " - ANTHROPIC_* User 环境变量`n`n"
|
||||
$msg += "保留:`n"
|
||||
$msg += " - ~/.claude/CLAUDE.md, memory/, projects/ (用户自有内容)`n"
|
||||
$msg += " - Node.js / Git / PowerShell 7 / Claude Code"
|
||||
|
||||
$confirm = [System.Windows.Forms.MessageBox]::Show($msg, 'Bookworm 卸载二次确认', 'YesNo', 'Warning')
|
||||
if ($confirm -ne 'Yes') {
|
||||
Write-Host "Bookworm 卸载已取消" -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
$logLines = @()
|
||||
function Log { param([string]$m); Write-Host $m -ForegroundColor Cyan; $script:logLines += $m }
|
||||
|
||||
# ── [1/7] 桌面 .lnk ──
|
||||
Log "[1/7] 删桌面快捷方式"
|
||||
$desk = [Environment]::GetFolderPath('Desktop')
|
||||
foreach ($n in @('启动Bookworm.lnk','更新Bookworm.lnk','体检Bookworm.lnk','卸载Bookworm.lnk','Bookworm.lnk')) {
|
||||
$p = Join-Path $desk $n
|
||||
if (Test-Path $p) {
|
||||
try { Remove-Item $p -Force -EA Stop; Log " ✓ 删 $n" }
|
||||
catch { Log " [!] 删 $n 失败: $_" }
|
||||
}
|
||||
}
|
||||
|
||||
# ── [2/7] ~/.claude Bookworm 注入内容 (精准删除, 保留用户自有配置) ──
|
||||
Log "[2/7] 清 ~/.claude/ 中 Bookworm 注入的文件"
|
||||
$claudeDir = Join-Path $env:USERPROFILE '.claude'
|
||||
if (Test-Path $claudeDir) {
|
||||
# 只删 Bookworm 管理的子目录和文件, 不删用户自己的 CLAUDE.md / memory / projects
|
||||
$bwManagedDirs = @('skills', 'hooks', 'agents', 'scripts', 'constitution', 'patches', 'session-state')
|
||||
foreach ($d in $bwManagedDirs) {
|
||||
$dp = Join-Path $claudeDir $d
|
||||
if (Test-Path $dp) {
|
||||
try { Remove-Item $dp -Recurse -Force -EA Stop; Log " ✓ 删 $d/" }
|
||||
catch { Log " [!] 删 $d/ 失败: $_" }
|
||||
}
|
||||
}
|
||||
# 删 Bookworm 的配置文件 (settings.json 含 hooks 注册)
|
||||
foreach ($f in @('settings.json', 'settings.local.json', 'stats-compiled.json')) {
|
||||
$fp = Join-Path $claudeDir $f
|
||||
if (Test-Path $fp) {
|
||||
try { Remove-Item $fp -Force -EA Stop; Log " ✓ 删 $f" }
|
||||
catch { Log " [!] 删 $f 失败: $_" }
|
||||
}
|
||||
}
|
||||
Log " · 保留 ~/.claude/CLAUDE.md, memory/, projects/ (用户自有内容)"
|
||||
}
|
||||
|
||||
# ── [3/7] HKCU DPAPI 凭证 + Toast 备份元数据 ──
|
||||
Log "[3/7] 清 HKCU:\Software\Bookworm DPAPI 凭证"
|
||||
# 先读 Toast 备份, 后面 [5] 用
|
||||
$toastOrig = $null
|
||||
$bak = 'HKCU:\Software\Bookworm\ToastBackup'
|
||||
if (Test-Path $bak) {
|
||||
try { $toastOrig = (Get-ItemProperty -Path $bak -Name 'ScreenSketchToast_Original' -EA SilentlyContinue).ScreenSketchToast_Original } catch {}
|
||||
}
|
||||
foreach ($r in @('HKCU:\Software\Bookworm\CachedEnv','HKCU:\Software\Bookworm\ToastBackup','HKCU:\Software\Bookworm')) {
|
||||
if (Test-Path $r) {
|
||||
try { Remove-Item $r -Recurse -Force -EA Stop; Log " ✓ 删 $r" }
|
||||
catch { Log " [!] 删 $r 失败: $_" }
|
||||
}
|
||||
}
|
||||
|
||||
# ── [4/7] profile.ps1 BW_CRED + BW_CLIP 块清理 (PS7+PS5.1) ──
|
||||
Log "[4/7] 清 profile.ps1 BW_CRED + BW_CLIP 块"
|
||||
foreach ($pdir in @(
|
||||
(Join-Path $env:USERPROFILE 'Documents\PowerShell'),
|
||||
(Join-Path $env:USERPROFILE 'Documents\WindowsPowerShell')
|
||||
)) {
|
||||
$pp = Join-Path $pdir 'profile.ps1'
|
||||
if (-not (Test-Path $pp)) { continue }
|
||||
try {
|
||||
$c = Get-Content $pp -Raw -Encoding UTF8
|
||||
$orig = $c.Length
|
||||
# 兼容 v3.0.x / v3.1.x 所有 sentinel 版本
|
||||
$c = [regex]::Replace($c, '# BW_CRED_START[\s\S]*?# BW_CRED_END\r?\n?', '')
|
||||
$c = [regex]::Replace($c, '# BW_CLIP_START[\s\S]*?# BW_CLIP_END\r?\n?', '')
|
||||
if ($c.Length -ne $orig) {
|
||||
[System.IO.File]::WriteAllText($pp, $c, [System.Text.UTF8Encoding]::new($false))
|
||||
Log " ✓ 清理 $pp (移除 $($orig - $c.Length) 字节)"
|
||||
} else {
|
||||
Log " · $pp 无 BW_* 块, 跳过"
|
||||
}
|
||||
} catch { Log " [!] $pp 处理失败: $_" }
|
||||
}
|
||||
|
||||
# ── [5/7] 还原截图 Toast ──
|
||||
Log "[5/7] 还原截图 Toast 设置"
|
||||
$toastReg = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Notifications\Settings\Microsoft.ScreenSketch_8wekyb3d8bbwe!App'
|
||||
if ($toastOrig -eq '__ABSENT__') {
|
||||
if (Test-Path $toastReg) {
|
||||
try { Remove-ItemProperty -Path $toastReg -Name 'Enabled' -EA Stop; Log " ✓ Toast Enabled 项已删除 (恢复默认)" }
|
||||
catch { Log " [!] Toast 还原失败: $_" }
|
||||
}
|
||||
} elseif ($toastOrig) {
|
||||
try {
|
||||
if (-not (Test-Path $toastReg)) { New-Item -Path $toastReg -Force | Out-Null }
|
||||
Set-ItemProperty -Path $toastReg -Name 'Enabled' -Value ([int]$toastOrig) -Type DWord -Force
|
||||
Log " ✓ Toast Enabled 还原为 $toastOrig"
|
||||
} catch { Log " [!] Toast 还原失败: $_" }
|
||||
} else {
|
||||
Log " · 无 Toast 备份记录, 跳过"
|
||||
}
|
||||
|
||||
# ── [6/7] ANTHROPIC_* User env ──
|
||||
Log "[6/7] 清 ANTHROPIC_* / BW_LICENSE_KEY User 环境变量"
|
||||
foreach ($ev in @('ANTHROPIC_API_KEY','ANTHROPIC_BASE_URL','ANTHROPIC_MODEL','BW_LICENSE_KEY')) {
|
||||
$v = [Environment]::GetEnvironmentVariable($ev, 'User')
|
||||
if ($v) {
|
||||
try { [Environment]::SetEnvironmentVariable($ev, $null, 'User'); Log " ✓ 清 $ev" }
|
||||
catch { Log " [!] 清 $ev 失败: $_" }
|
||||
}
|
||||
}
|
||||
|
||||
# ── [7/7] 提示手动删 bookworm-boot ──
|
||||
Log "[7/7] 手动收尾"
|
||||
Log " 请手动删除 bookworm-boot 目录 (本脚本无法删除自身所在目录):"
|
||||
Log " 例如: $env:USERPROFILE\Downloads\bookworm-boot"
|
||||
|
||||
# ── 完成弹窗 ──
|
||||
$endMsg = "Bookworm 卸载完成.`n`n"
|
||||
$endMsg += "执行摘要:`n"
|
||||
$endMsg += ($logLines -join "`n")
|
||||
$endMsg += "`n`n保留的公共依赖 (如不再需要可手动卸):`n"
|
||||
$endMsg += " - Node.js: %ProgramFiles%\nodejs`n"
|
||||
$endMsg += " - Git for Windows: %ProgramFiles%\Git`n"
|
||||
$endMsg += " - PowerShell 7: %ProgramFiles%\PowerShell\7`n"
|
||||
$endMsg += " - Claude Code: 命令行运行 npm uninstall -g @anthropic-ai/claude-code"
|
||||
[System.Windows.Forms.MessageBox]::Show($endMsg, 'Bookworm 卸载完成', 'OK', 'Information') | Out-Null
|
||||
exit 0
|
||||
@ -1,22 +1,50 @@
|
||||
@echo off
|
||||
chcp 65001 > nul
|
||||
:: v3.1.3: Bookworm 完整卸载脚本
|
||||
:: 删除: 桌面 .lnk × 4 / ~/.claude / DPAPI HKCU 凭证 /
|
||||
:: 双 profile (PS7+PS5.1) 的 BW_CRED + BW_CLIP 块 / Toast 备份还原 /
|
||||
:: ANTHROPIC_* User 环境变量
|
||||
:: 保留: Node/Git/PS7/Claude Code (公共依赖, 不主动卸)
|
||||
:: 调用配套 PS 脚本完成所有清理 (cmd 不擅长 PS 多行)
|
||||
title Bookworm Portable - 卸载
|
||||
cd /d "%~dp0"
|
||||
|
||||
setlocal
|
||||
where pwsh >nul 2>nul && set "PSH=pwsh" || set "PSH=powershell"
|
||||
echo.
|
||||
echo ====================================
|
||||
echo Bookworm Portable - 完整卸载
|
||||
echo ====================================
|
||||
echo.
|
||||
echo 将执行:
|
||||
echo - 终止 Claude Code 进程
|
||||
echo - 清除所有环境变量和凭证缓存
|
||||
echo - 恢复原始 .claude 目录
|
||||
echo - 清除 PowerShell 历史和 Git 凭证
|
||||
echo - 删除桌面快捷方式
|
||||
echo.
|
||||
|
||||
set "UNINST_PS1=%~dp0卸载Bookworm-impl.ps1"
|
||||
if not exist "%UNINST_PS1%" (
|
||||
echo [!] 卸载实现脚本缺失: %UNINST_PS1%
|
||||
echo [!] 请重跑 Bookworm-Setup.exe Phase 3 重新克隆 bookworm-boot
|
||||
pause
|
||||
exit /b 1
|
||||
:: AutoAccept: 卸载确认已豁免
|
||||
:: set /p confirm=" 确认卸载? (y/n): "
|
||||
:: if /i not "%confirm%"=="y" (
|
||||
:: echo 已取消
|
||||
:: pause
|
||||
:: exit /b
|
||||
:: )
|
||||
echo [AutoAccept] 自动确认卸载
|
||||
|
||||
echo.
|
||||
|
||||
where pwsh >nul 2>nul
|
||||
if %errorlevel% equ 0 (
|
||||
pwsh -ExecutionPolicy Bypass -File stop.ps1 -Restore -Deep
|
||||
) else (
|
||||
powershell -ExecutionPolicy Bypass -File stop.ps1 -Restore -Deep
|
||||
)
|
||||
|
||||
%PSH% -NoProfile -ExecutionPolicy Bypass -File "%UNINST_PS1%"
|
||||
endlocal
|
||||
:: 删除桌面快捷方式
|
||||
del "%USERPROFILE%\Desktop\Bookworm.lnk" 2>nul
|
||||
del "%USERPROFILE%\Desktop\更新Bookworm.lnk" 2>nul
|
||||
|
||||
:: 清除凭证缓存注册表
|
||||
reg delete "HKCU\Software\Bookworm" /f 2>nul
|
||||
|
||||
echo.
|
||||
echo ====================================
|
||||
echo Bookworm 已完全卸载
|
||||
echo 可安全删除 bookworm-boot 文件夹
|
||||
echo ====================================
|
||||
echo.
|
||||
pause
|
||||
|
||||
@ -1,44 +1,58 @@
|
||||
@echo off
|
||||
chcp 65001 > nul
|
||||
:: v3.0.11 架构重构: 桌面 .lnk 已直调 pwsh + claude.ps1 绝对路径, 不再走本 bat.
|
||||
:: 此文件仅保留作为兼容入口 (老用户已经把 .bat 加到收藏夹/开始菜单的场景),
|
||||
:: 内部行为简化为转发到桌面 .lnk 触发统一启动路径.
|
||||
::
|
||||
:: 如果用户双击本 .bat (而非桌面 .lnk), 直接 invoke pwsh + claude.ps1 启动:
|
||||
cd /d "%~dp0"
|
||||
|
||||
setlocal
|
||||
:: 默认模型 (中转站兼容白名单, Claude Code 2.0.x 默认 4-5 会 503)
|
||||
if not defined ANTHROPIC_MODEL set ANTHROPIC_MODEL=claude-opus-4-7
|
||||
|
||||
:: 1. 定位 pwsh.exe (PS7 必需, 启动器 v3.0.11 强依赖)
|
||||
set "PWSH_EXE="
|
||||
where pwsh >nul 2>nul && for /f "delims=" %%i in ('where pwsh') do if not defined PWSH_EXE set "PWSH_EXE=%%i"
|
||||
if not defined PWSH_EXE if exist "%ProgramFiles%\PowerShell\7\pwsh.exe" set "PWSH_EXE=%ProgramFiles%\PowerShell\7\pwsh.exe"
|
||||
if not defined PWSH_EXE if exist "%LOCALAPPDATA%\Microsoft\PowerShell\pwsh.exe" set "PWSH_EXE=%LOCALAPPDATA%\Microsoft\PowerShell\pwsh.exe"
|
||||
if not defined PWSH_EXE (
|
||||
echo [!] PowerShell 7 未安装. 请先重跑 Bookworm-Setup.exe 装好 PS7.
|
||||
pause
|
||||
exit /b 1
|
||||
:: 中转站在国内,不走代理
|
||||
set NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1
|
||||
set no_proxy=%NO_PROXY%
|
||||
|
||||
:: 静默自动更新 (bookworm-boot + .claude 配置, 失败不阻断启动)
|
||||
echo [..] 检查更新...
|
||||
git pull --rebase >nul 2>nul
|
||||
git -C "%USERPROFILE%\.claude" pull --rebase >nul 2>nul
|
||||
|
||||
:: === 终端选择: Windows Terminal > conhost ===
|
||||
:: wt.exe 支持完整 Unicode 渲染 + 自由缩放不闪退
|
||||
set USE_WT=0
|
||||
where wt >nul 2>nul && set USE_WT=1
|
||||
|
||||
:: === Shell 选择: pwsh7 > powershell 5.1 ===
|
||||
:: pwsh7 启动更快, 支持更多语法, Claude Code TUI 渲染更好
|
||||
set USE_PWSH7=0
|
||||
where pwsh >nul 2>nul && set USE_PWSH7=1
|
||||
|
||||
:: 构建凭证加载命令 (DPAPI 解密)
|
||||
set CRED_CMD=Add-Type -AssemblyName System.Security;$r='HKCU:\Software\Bookworm\CachedEnv';try{(Get-ItemProperty $r -EA Stop).PSObject.Properties^|Where-Object{$_.Name-match'^[A-Z_]+$'}^|ForEach-Object{$v=$_.Value;try{$b=[Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String($v),$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);$v=[Text.Encoding]::UTF8.GetString($b)}catch{};[Environment]::SetEnvironmentVariable($_.Name,$v,'Process')}}catch{};
|
||||
|
||||
:: 优先路径: wt + pwsh7
|
||||
if %USE_WT% equ 1 if %USE_PWSH7% equ 1 (
|
||||
start "" wt new-tab --title "Bookworm Smart Assistant" -d "%~dp0" -- pwsh -NoLogo -NoExit -Command "%CRED_CMD% claude --dangerously-skip-permissions"
|
||||
exit
|
||||
)
|
||||
|
||||
:: 2. 定位 claude.ps1 (优先 npm prefix, 兜底常见位置)
|
||||
set "CLAUDE_PS1="
|
||||
for /f "delims=" %%i in ('npm config get prefix 2^>nul') do set "NPM_PREFIX=%%i"
|
||||
if defined NPM_PREFIX if exist "%NPM_PREFIX%\claude.ps1" set "CLAUDE_PS1=%NPM_PREFIX%\claude.ps1"
|
||||
if not defined CLAUDE_PS1 if exist "%APPDATA%\npm\claude.ps1" set "CLAUDE_PS1=%APPDATA%\npm\claude.ps1"
|
||||
if not defined CLAUDE_PS1 if exist "%ProgramFiles%\nodejs\claude.ps1" set "CLAUDE_PS1=%ProgramFiles%\nodejs\claude.ps1"
|
||||
if not defined CLAUDE_PS1 (
|
||||
echo [!] claude.ps1 未找到. 请重跑 Bookworm-Setup.exe 修复 Claude Code 安装.
|
||||
pause
|
||||
exit /b 1
|
||||
:: 路径 2: wt + powershell 5.1
|
||||
if %USE_WT% equ 1 if %USE_PWSH7% equ 0 (
|
||||
start "" wt new-tab --title "Bookworm Smart Assistant" -d "%~dp0" -- powershell -NoLogo -ExecutionPolicy Bypass -NoExit -Command "%CRED_CMD% claude --dangerously-skip-permissions"
|
||||
exit
|
||||
)
|
||||
|
||||
:: 3. OTA 自动更新检查 (fail-open: 脚本不存在或报错均不阻断启动)
|
||||
set "OTA_SCRIPT=%USERPROFILE%\.claude\.bw-ota\bw-ota.ps1"
|
||||
if exist "%OTA_SCRIPT%" (
|
||||
"%PWSH_EXE%" -NoLogo -ExecutionPolicy Bypass -File "%OTA_SCRIPT%"
|
||||
:: 路径 3: conhost + pwsh7
|
||||
if %USE_PWSH7% equ 1 (
|
||||
start "Bookworm Smart Assistant" pwsh -NoLogo -NoExit -Command "%CRED_CMD% claude --dangerously-skip-permissions"
|
||||
exit
|
||||
)
|
||||
|
||||
:: 4. 直调 pwsh + claude.ps1 (无 wt / 无 Base64 / 无 DPAPI in-bat)
|
||||
:: 凭证由 pwsh profile.ps1 BW_CRED_START..END 块自动加载
|
||||
"%PWSH_EXE%" -NoLogo -NoExit -File "%CLAUDE_PS1%" --dangerously-skip-permissions
|
||||
|
||||
endlocal
|
||||
:: 路径 4: 回退 PowerShell 5.1 (最低保障)
|
||||
title Bookworm Portable
|
||||
echo.
|
||||
echo [!] PowerShell 7 未安装, 使用 PowerShell 5.1
|
||||
echo.
|
||||
powershell -ExecutionPolicy Bypass -File install.ps1 -StartOnly -AutoAccept
|
||||
if %errorlevel% neq 0 (
|
||||
echo.
|
||||
echo 启动失败,按任意键退出...
|
||||
pause > nul
|
||||
)
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
@echo off
|
||||
chcp 65001 > nul
|
||||
cd /d "%~dp0"
|
||||
:: v3.1.1 架构: 更新 .bat 仅做 git pull, 完成后弹 GUI 让用户决定是否立即启动.
|
||||
:: 启动 claude 由独立的 启动Bookworm.lnk → pwsh + bw-launch.ps1 完成 (1 跳直链)
|
||||
|
||||
echo.
|
||||
echo Bookworm 配置同步
|
||||
echo ============================================
|
||||
echo.
|
||||
|
||||
set HAS_FAIL=0
|
||||
|
||||
:: 同步 bookworm-boot 仓库 (本目录)
|
||||
echo [1/2] 同步启动器目录 (bookworm-boot)...
|
||||
git pull --rebase 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo [!] bookworm-boot git pull 失败 ^(不影响启动 lnk^)
|
||||
set HAS_FAIL=1
|
||||
)
|
||||
|
||||
:: 同步 ~/.claude 配置仓库 (Skill/hook/agents)
|
||||
echo.
|
||||
echo [2/2] 同步 Claude 配置 (.claude/)...
|
||||
git -C "%USERPROFILE%\.claude" stash -q 2>nul
|
||||
git -C "%USERPROFILE%\.claude" pull --rebase 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo [!] .claude git pull 失败 ^(不影响启动 lnk^)
|
||||
set HAS_FAIL=1
|
||||
)
|
||||
git -C "%USERPROFILE%\.claude" stash pop -q 2>nul
|
||||
|
||||
echo.
|
||||
echo ============================================
|
||||
if %HAS_FAIL% equ 0 (
|
||||
echo [OK] 所有同步完成
|
||||
) else (
|
||||
echo [!] 部分同步失败 ^(详见上方日志, 启动仍可正常使用^)
|
||||
)
|
||||
echo ============================================
|
||||
|
||||
:: v3.1.1 (闭合 L6): 完成后 GUI 询问是否立即启动 Claude (闭合 "更新完了不知道下一步")
|
||||
:: 用 PowerShell 弹 MessageBox YesNo, Yes → 触发桌面启动 lnk; No → 直接退出
|
||||
where pwsh >nul 2>nul && set "PSH=pwsh" || set "PSH=powershell"
|
||||
%PSH% -NoProfile -Command "Add-Type -AssemblyName System.Windows.Forms; $r = [System.Windows.Forms.MessageBox]::Show('配置同步完成. 是否立即启动 Bookworm Claude?', 'Bookworm 同步完成', 'YesNo', 'Question'); if ($r -eq 'Yes') { $lnk = Join-Path ([Environment]::GetFolderPath('Desktop')) '启动Bookworm.lnk'; if (Test-Path $lnk) { Start-Process $lnk } else { Write-Host 'lnk 缺失, 请重跑 Bookworm-Setup.exe' -ForegroundColor Yellow; pause } }"
|
||||
30
更新并启动Bookworm.bat
Normal file
30
更新并启动Bookworm.bat
Normal file
@ -0,0 +1,30 @@
|
||||
@echo off
|
||||
chcp 65001 > nul
|
||||
cd /d "%~dp0"
|
||||
|
||||
:: 中转站在国内,不走代理
|
||||
set NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1
|
||||
set no_proxy=%NO_PROXY%
|
||||
|
||||
:: 静默自动更新 (bookworm-boot + .claude 配置)
|
||||
echo [..] 同步更新...
|
||||
git pull --rebase >nul 2>nul
|
||||
git -C "%USERPROFILE%\.claude" pull --rebase >nul 2>nul
|
||||
|
||||
:: 检测 pwsh7 可用性
|
||||
where pwsh >nul 2>nul
|
||||
if %errorlevel% equ 0 (
|
||||
:: pwsh7: 先同步配置, 再在新窗口启动 claude (从注册表缓存加载 DPAPI 凭证)
|
||||
pwsh -NoLogo -ExecutionPolicy Bypass -File "%~dp0install.ps1" -AutoAccept -SkipLaunch
|
||||
start "Bookworm Smart Assistant" pwsh -NoLogo -NoExit -Command "Add-Type -AssemblyName System.Security;$r='HKCU:\Software\Bookworm\CachedEnv';try{(Get-ItemProperty $r -EA Stop).PSObject.Properties|Where-Object{$_.Name-match'^[A-Z_]+$'}|ForEach-Object{$v=$_.Value;try{$b=[Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String($v),$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);$v=[Text.Encoding]::UTF8.GetString($b)}catch{};[Environment]::SetEnvironmentVariable($_.Name,$v,'Process')}}catch{};& claude --dangerously-skip-permissions"
|
||||
exit
|
||||
)
|
||||
|
||||
:: 回退 PowerShell 5.1: 一次调用完成更新+加载凭证+启动 (消除双次调用)
|
||||
title Bookworm Portable
|
||||
powershell -ExecutionPolicy Bypass -File "%~dp0install.ps1" -AutoAccept
|
||||
if %errorlevel% neq 0 (
|
||||
echo.
|
||||
echo 启动失败,按任意键退出...
|
||||
pause > nul
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user