diff --git a/Bookworm-OneClick.bat b/Bookworm-OneClick.bat deleted file mode 100644 index 6f21557..0000000 --- a/Bookworm-OneClick.bat +++ /dev/null @@ -1,278 +0,0 @@ -@echo off -chcp 65001 > nul 2>&1 -title Bookworm Smart Assistant - 全自动安装 - -:: ─── 自动提升管理员权限 ── -:: 用 goto 而非 if() 避免文件名含括号(如"(2)")导致解析崩溃 -net session >nul 2>&1 -if %errorlevel% equ 0 goto :IS_ADMIN - -echo 需要管理员权限来安装软件,正在请求... -echo Set objShell = CreateObject("Shell.Application") > "%TEMP%\bw_elevate.vbs" -echo objShell.ShellExecute "cmd.exe", "/k cd /d ""%~dp0"" & ""%~nx0""", "", "runas", 1 >> "%TEMP%\bw_elevate.vbs" -cscript //nologo "%TEMP%\bw_elevate.vbs" -del /f /q "%TEMP%\bw_elevate.vbs" 2>nul -exit /b - -:IS_ADMIN - -:: ─── 初始化 ───────────────────────────────────────── -setlocal EnableDelayedExpansion -color 1F -set "NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1" -set "no_proxy=%NO_PROXY%" -set "INSTALL_DIR=%USERPROFILE%\bookworm-boot" -set "GITEA_URL=https://code.letcareme.com/bookworm/bookworm-boot.git" -set "ERRORS=0" -set "NEED_PATH_REFRESH=0" - -echo. -echo +============================================================+ -echo ^| ^| -echo ^| Bookworm Smart Assistant ^| -echo ^| 全自动安装程序 v2.0 ^| -echo ^| ^| -echo ^| 全新电脑? 没问题! 双击即可, 全程无需手动操作 ^| -echo ^| ^| -echo ^| 自动安装: Node.js + Git + Claude Code + Bookworm 配置 ^| -echo ^| ^| -echo +============================================================+ -echo. - -:: ─── 步骤 1/8: winget 检测 ────────────────────────── -echo [1/8] 检测包管理器... -where winget >nul 2>nul -if %errorlevel% neq 0 ( - echo. - echo [!!] winget 未安装 (Windows 10 1809+ / Windows 11 自带) - echo. - echo 请先安装 "应用安装程序": - echo 1. 打开 Microsoft Store - echo 2. 搜索 "应用安装程序" 或 "App Installer" - echo 3. 点击安装/更新 - echo 4. 安装后重新运行本程序 - echo. - pause - exit /b 1 -) -echo [OK] winget 可用 -echo. - -:: ─── 步骤 2/8: 安装 Git ──────────────────────────── -echo [2/8] 检查 Git... -where git >nul 2>nul -if %errorlevel% neq 0 ( - echo [..] Git 未安装, 正在通过 winget 安装... - winget install Git.Git --accept-source-agreements --accept-package-agreements --silent - if !errorlevel! neq 0 ( - echo [!!] Git 安装失败 - set /a ERRORS+=1 - ) else ( - echo [OK] Git 安装成功 - set "NEED_PATH_REFRESH=1" - ) -) else ( - echo [OK] Git 已安装 -) -echo. - -:: ─── 步骤 3/8: 安装 Node.js ──────────────────────── -echo [3/8] 检查 Node.js... -where node >nul 2>nul -if %errorlevel% neq 0 ( - echo [..] Node.js 未安装, 正在通过 winget 安装... - winget install OpenJS.NodeJS.LTS --accept-source-agreements --accept-package-agreements --silent - if !errorlevel! neq 0 ( - echo [!!] Node.js 安装失败 - set /a ERRORS+=1 - ) else ( - echo [OK] Node.js LTS 安装成功 - set "NEED_PATH_REFRESH=1" - ) -) else ( - echo [OK] Node.js 已安装 -) -echo. - -:: ─── 步骤 4/8: 安装 PowerShell 7 ──────────────────── -echo [4/8] 检查 PowerShell 7... -where pwsh >nul 2>nul -if %errorlevel% neq 0 ( - echo [..] PowerShell 7 未安装, 正在通过 winget 安装... - winget install Microsoft.PowerShell --accept-source-agreements --accept-package-agreements --silent - if !errorlevel! neq 0 ( - echo [!!] PowerShell 7 安装失败 - set /a ERRORS+=1 - ) else ( - echo [OK] PowerShell 7 安装成功 - set "NEED_PATH_REFRESH=1" - ) -) else ( - echo [OK] PowerShell 7 已安装 -) -echo. - -:: ─── 刷新 PATH (新装软件需要) ──────────────────────── -if "%NEED_PATH_REFRESH%"=="1" ( - echo [..] 刷新系统 PATH... - :: 重新加载 Machine + User PATH - for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v Path 2^>nul') do set "SYS_PATH=%%b" - for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v Path 2^>nul') do set "USR_PATH=%%b" - set "PATH=!SYS_PATH!;!USR_PATH!" - :: 同时添加常见 Node.js / Git 路径 - set "PATH=!PATH!;C:\Program Files\nodejs;C:\Program Files\Git\cmd;C:\Program Files\Git\usr\bin;C:\Program Files\PowerShell\7" - echo [OK] PATH 已刷新 - echo. -) - -:: ─── 二次验证: Git + Node + pwsh ───────────────────── -where git >nul 2>nul -if %errorlevel% neq 0 ( - echo [FATAL] Git 仍然不可用 - echo 请关闭此窗口, 手动安装 Git 后重新运行 - pause - exit /b 1 -) -where node >nul 2>nul -if %errorlevel% neq 0 ( - echo [FATAL] Node.js 仍然不可用 - echo 请关闭此窗口, 手动安装 Node.js 后重新运行 - pause - exit /b 1 -) -where pwsh >nul 2>nul -if %errorlevel% neq 0 ( - echo [WARN] PowerShell 7 未就绪, Claude Code 将使用 PowerShell 5.1 -) - -:: ─── 步骤 5/8: 安装 Claude Code ───────────────────── -echo [5/8] 检查 Claude Code... -:: 国内 npm 镜像 - 淘宝源, 避免 npmjs.org 超时 -call npm config set registry https://registry.npmmirror.com 2>nul -where claude >nul 2>nul -if %errorlevel% neq 0 ( - echo [..] Claude Code 未安装, 正在通过 npm 安装 - 淘宝镜像加速... - call npm i -g @anthropic-ai/claude-code 2>&1 - if !errorlevel! neq 0 ( - echo [!!] Claude Code 安装失败 - echo 请手动运行: npm i -g @anthropic-ai/claude-code - set /a ERRORS+=1 - ) else ( - echo [OK] Claude Code 安装成功 - ) -) else ( - echo [OK] Claude Code 已安装 -) -echo. - -:: ─── 步骤 6/8: 克隆/更新 Bookworm ────────────────── -echo [6/8] 同步 Bookworm 配置... - -:: 配置 git credential helper (免重复输密码) -git config --global credential.helper manager 2>nul - -if exist "%INSTALL_DIR%\.git" ( - echo 已有安装, 更新到最新版... - pushd "%INSTALL_DIR%" - git pull 2>&1 - popd -) else ( - if exist "%INSTALL_DIR%" ( - echo 清理非 git 目录后重新下载... - rmdir /s /q "%INSTALL_DIR%" 2>nul - ) - echo 首次下载 (需要输入 Gitea 用户名密码)... - git clone "%GITEA_URL%" "%INSTALL_DIR%" 2>&1 - if !errorlevel! neq 0 ( - echo. - echo [!!] 下载失败, 请检查: - echo - 网络是否正常 - echo - Gitea 用户名密码是否正确 - echo - 管理员是否已开通访问权限 - echo. - pause - exit /b 1 - ) -) -echo [OK] Bookworm 文件已就绪 -echo. - -:: ─── 步骤 7/8: 执行安装配置 ──────────────────────── -echo [7/8] 执行安装配置... -echo. - -if exist "%INSTALL_DIR%\install.ps1" ( - :: 安装配置 (不启动 claude, 最终块负责在 pwsh7 窗口中启动) - where pwsh >nul 2>nul - if !errorlevel! equ 0 ( - pwsh -NoLogo -ExecutionPolicy Bypass -File "%INSTALL_DIR%\install.ps1" -AutoAccept -SkipLaunch - ) else ( - powershell -NoLogo -ExecutionPolicy Bypass -File "%INSTALL_DIR%\install.ps1" -AutoAccept -SkipLaunch - ) -) else ( - echo [WARN] install.ps1 未找到, 跳过高级配置 -) -echo. - -:: ─── 步骤 8/8: 创建桌面快捷方式 + 完成 ────────────── -echo [8/8] 创建桌面快捷方式... - -:: 用 PowerShell 创建快捷方式 -powershell -ExecutionPolicy Bypass -Command ^ - "try{$s=(New-Object -COM WScript.Shell).CreateShortcut([IO.Path]::Combine([Environment]::GetFolderPath('Desktop'),'Bookworm.lnk'));^ - $s.TargetPath='%INSTALL_DIR%\启动Bookworm.bat';^ - $s.WorkingDirectory='%INSTALL_DIR%';^ - $s.Description='Bookworm Smart Assistant';^ - $s.Save();Write-Host ' [OK] 桌面快捷方式: Bookworm' -Fore Green}catch{Write-Host ' [!] 快捷方式创建失败' -Fore Yellow}" 2>nul - -powershell -ExecutionPolicy Bypass -Command ^ - "try{$s=(New-Object -COM WScript.Shell).CreateShortcut([IO.Path]::Combine([Environment]::GetFolderPath('Desktop'),'更新Bookworm.lnk'));^ - $s.TargetPath='%INSTALL_DIR%\更新并启动Bookworm.bat';^ - $s.WorkingDirectory='%INSTALL_DIR%';^ - $s.Description='Bookworm 更新并启动';^ - $s.Save();Write-Host ' [OK] 桌面快捷方式: 更新Bookworm' -Fore Green}catch{Write-Host ' [!] 快捷方式创建失败' -Fore Yellow}" 2>nul - -:: 打开使用教程 -if exist "%INSTALL_DIR%\guide.html" ( - start "" "%INSTALL_DIR%\guide.html" -) - -echo. -echo +============================================================+ -echo ^| ^| -echo ^| 安装完成! ^| -echo ^| ^| -echo ^| 已安装: ^| -echo ^| [v] Node.js LTS — JavaScript 运行时 ^| -echo ^| [v] Git — 版本控制与配置同步 ^| -echo ^| [v] PowerShell 7 — 现代终端环境 ^| -echo ^| [v] Claude Code — AI 编程助手 ^| -echo ^| [v] Bookworm — 92 Skills / 18 Agents ^| -echo ^| ^| -echo ^| 桌面快捷方式: ^| -echo ^| Bookworm — 日常启动 ^| -echo ^| 更新Bookworm — 同步最新版后启动 ^| -echo ^| ^| -echo ^| 首次启动需要输入管理员提供的主密码 ^| -echo ^| ^| -echo +============================================================+ -echo. - -if %ERRORS% gtr 0 ( - echo [注意] 安装过程中有 %ERRORS% 个警告, 请查看上方日志 - echo. -) - -echo 按任意键启动 Bookworm... -pause > nul - -:: 启动 — pwsh7 新窗口直接运行 claude -where pwsh >nul 2>nul -if !errorlevel! equ 0 ( - start "Bookworm Smart Assistant" pwsh -NoLogo -NoExit -Command "& claude --dangerously-skip-permissions" -) else ( - cd /d "%INSTALL_DIR%" - powershell -NoLogo -ExecutionPolicy Bypass -File "%INSTALL_DIR%\install.ps1" -StartOnly -AutoAccept -) - -endlocal diff --git a/auto-setup.ps1 b/auto-setup.ps1 index f69155c..d29cece 100644 --- a/auto-setup.ps1 +++ b/auto-setup.ps1 @@ -1746,7 +1746,7 @@ foreach ($d in $dirs) { if (-not (Test-Path $p)) { New-Item -ItemType Directory -Path $p -Force | Out-Null } } -# ─── 克隆/更新 bookworm-boot (含 crypto-helper.js + secrets-*.enc + install.ps1) ─── +# ─── 克隆/更新 bookworm-boot (公开仓; 核心启动件 bw-launch.ps1 等) ─── if (Test-Path (Join-Path $BootDir ".git")) { Log-Info "boot 仓库已存在, 更新中..." try { @@ -1756,10 +1756,10 @@ if (Test-Path (Join-Path $BootDir ".git")) { } else { Log-Info "匿名克隆 boot 仓库 (boot 保持公开)..." # [Path-A] boot 仓保持公开 (R4 决策); 匿名失败不再索取 Gitea 账密, 由下方完整性检查统一处置。 - # TODO [Path-A boot-strip]: 转私有任务须从公开 boot 剥离 admin/crypto 敏感件; - # 若 crypto-helper.js 被剥离, 下方完整性探针须改用其它留存文件 (本次不动该探针)。 + # [Path-A boot-strip 2026-06-30] crypto-helper.js/install.ps1 已从公开 boot 剥离; + # 克隆成功标志改用核心留存件 bw-launch.ps1 (原用 crypto-helper.js)。 $r = Run-CmdWithUI "git" @("clone", "--depth", "1", $BootUrl, $BootDir) "克隆 boot 仓库 (匿名)" 180000 - if (-not (Test-Path (Join-Path $BootDir "crypto-helper.js"))) { + if (-not (Test-Path (Join-Path $BootDir "bw-launch.ps1"))) { Log-Fail "启动工具包下载失败" Show-MsgBox "Bookworm 启动工具包下载失败。`n`n请检查:`n1. 网络和代理是否正常`n2. Gitea (code.letcareme.com) 是否可达`n`n重新运行安装器即可。" "下载失败" "OK" "Error" exit 1 diff --git a/crypto-helper.js b/crypto-helper.js deleted file mode 100644 index d982630..0000000 --- a/crypto-helper.js +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env node -'use strict'; -/** - * Bookworm Portable - Node.js 凭证加解密工具 - * 替代 openssl enc, 跨平台跨版本 100% 一致 - * - * 加密: echo "KEY=VALUE" | node crypto-helper.js encrypt > secrets.enc - * 解密: node crypto-helper.js decrypt < secrets.enc - * 交互解密: node crypto-helper.js decrypt-interactive secrets.enc - */ -const crypto = require('crypto'); -const fs = require('fs'); -const readline = require('readline'); - -const ALGO = 'aes-256-cbc'; -const ITERATIONS = 600000; -const DIGEST = 'sha256'; -const SALT_LEN = 16; -const IV_LEN = 16; -const KEY_LEN = 32; - -function deriveKey(password, salt) { - return crypto.pbkdf2Sync(password, salt, ITERATIONS, KEY_LEN + IV_LEN, DIGEST); -} - -function encrypt(plaintext, password) { - const salt = crypto.randomBytes(SALT_LEN); - const derived = deriveKey(password, salt); - const key = derived.slice(0, KEY_LEN); - const iv = derived.slice(KEY_LEN, KEY_LEN + IV_LEN); - const cipher = crypto.createCipheriv(ALGO, key, iv); - const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]); - // 格式: BWENC1 + salt(16) + encrypted - return Buffer.concat([Buffer.from('BWENC1'), salt, encrypted]); -} - -function decrypt(data, password) { - const magic = data.slice(0, 6).toString(); - if (magic !== 'BWENC1') { - throw new Error('WRONG_FORMAT'); - } - const salt = data.slice(6, 6 + SALT_LEN); - const encrypted = data.slice(6 + SALT_LEN); - const derived = deriveKey(password, salt); - const key = derived.slice(0, KEY_LEN); - const iv = derived.slice(KEY_LEN, KEY_LEN + IV_LEN); - const decipher = crypto.createDecipheriv(ALGO, key, iv); - try { - const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]); - return decrypted.toString('utf8'); - } catch (e) { - throw new Error('WRONG_PASSWORD'); - } -} - -// ─── CLI ─── -const [,, cmd, arg1, arg2] = process.argv; - -if (cmd === 'encrypt') { - const password = arg1; - if (!password) { console.error('Usage: node crypto-helper.js encrypt '); process.exit(1); } - let input = ''; - process.stdin.setEncoding('utf8'); - process.stdin.on('data', d => input += d); - process.stdin.on('end', () => { - const enc = encrypt(input.trim(), password); - process.stdout.write(enc); - }); - -} else if (cmd === 'decrypt') { - const password = arg1; - const file = arg2; // 可选: 文件路径, 否则从 stdin - if (!password) { console.error('Usage: node crypto-helper.js decrypt [file]'); process.exit(1); } - let data; - if (file) { - data = fs.readFileSync(file); - } else { - // Windows 兼容: 从 stdin 读取 buffer - const chunks = []; - const fd = fs.openSync(0, 'r'); - const buf = Buffer.alloc(4096); - let n; - while ((n = fs.readSync(fd, buf)) > 0) chunks.push(buf.slice(0, n)); - fs.closeSync(fd); - data = Buffer.concat(chunks); - } - try { - console.log(decrypt(data, password)); - } catch (e) { - if (e.message === 'WRONG_PASSWORD') { console.error('PASSWORD_ERROR'); process.exit(2); } - if (e.message === 'WRONG_FORMAT') { console.error('FORMAT_ERROR'); process.exit(3); } - throw e; - } - -} else if (cmd === 'decrypt-interactive' || cmd === 'decrypt-file') { - // 从文件解密, 交互输入密码, 输出 KEY=VALUE 到 stdout - const filePath = arg1; - if (!filePath || !fs.existsSync(filePath)) { console.error('File not found: ' + filePath); process.exit(1); } - const data = fs.readFileSync(filePath); - const maxRetries = parseInt(arg2) || 3; - - const rl = readline.createInterface({ input: process.stdin, output: process.stderr }); - - let attempt = 0; - function ask() { - attempt++; - const prompt = attempt > 1 ? ` 重新输入主密码 (第 ${attempt}/${maxRetries} 次): ` : ' 输入主密码解密凭证: '; - // 隐藏输入 (Windows 兼容) - if (process.platform === 'win32') { - process.stderr.write(prompt); - const { execSync } = require('child_process'); - try { - const pwd = execSync('powershell -Command "[Runtime.InteropServices.Marshal]::PtrToStringBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR((Read-Host -AsSecureString)))"', { stdio: ['inherit', 'pipe', 'pipe'] }).toString().trim(); - tryDecrypt(pwd); - } catch (e) { - rl.question(prompt, tryDecrypt); - } - } else { - rl.question(prompt, tryDecrypt); - } - } - - function tryDecrypt(password) { - try { - const result = decrypt(data, password); - rl.close(); - console.log(result); // stdout: KEY=VALUE lines - process.exit(0); - } catch (e) { - if (e.message === 'WRONG_PASSWORD') { - const remaining = maxRetries - attempt; - if (remaining > 0) { - process.stderr.write(` [!!] 密码错误,剩余重试: ${remaining} 次\n`); - ask(); - } else { - process.stderr.write(' [ABORT] 密码错误次数过多\n'); - rl.close(); - process.exit(2); - } - } else if (e.message === 'WRONG_FORMAT') { - process.stderr.write(' [ERROR] secrets.enc 格式不兼容, 请联系管理员重新生成\n'); - rl.close(); - process.exit(3); - } else { - throw e; - } - } - } - - ask(); - -} else { - console.error('Bookworm Crypto Helper'); - console.error(' encrypt: echo "K=V" | node crypto-helper.js encrypt > secrets.enc'); - console.error(' decrypt: node crypto-helper.js decrypt < secrets.enc'); - console.error(' interactive: node crypto-helper.js decrypt-file secrets.enc'); - process.exit(1); -} diff --git a/install.ps1 b/install.ps1 deleted file mode 100644 index 340210a..0000000 --- a/install.ps1 +++ /dev/null @@ -1,869 +0,0 @@ -<# -.SYNOPSIS - Bookworm Portable - 安装/启动脚本 -.DESCRIPTION - 从 Gitea 私有仓克隆 Bookworm 配置到目标机, - 解密凭证, 渲染模板, 启动 Claude Code. -.USAGE - # 首次安装 (从 Gitea 克隆) - .\install.ps1 - - # 仅启动 (已安装过) - .\install.ps1 -StartOnly - - # 指定 Gitea 地址 - .\install.ps1 -GitUrl "https://code.letcareme.com/bookworm/bookworm-config.git" -#> - -param( - [string]$GitUrl = "https://code.letcareme.com/bookworm/bookworm-config.git", - [switch]$StartOnly, - [switch]$SkipSecrets, - [switch]$SkipLaunch, # 仅安装不启动 (由调用方负责启动) - [switch]$AutoAccept # 豁免所有人工确认环节 -) - -$ErrorActionPreference = "Stop" - -# ─── 路径定义 ──────────────────────────────────────── -$ScriptDir = if ($MyInvocation.MyCommand.Path) { - Split-Path -Parent $MyInvocation.MyCommand.Path -} else { $PWD.Path } -$ClaudeTarget = Join-Path $env:USERPROFILE ".claude" -$BackupPath = Join-Path $env:USERPROFILE ".claude.bw-backup" -$SecretsEnc = Join-Path $ScriptDir "secrets.enc" -$TemplateFile = Join-Path $ClaudeTarget "settings.template.json" -$LocalTplFile = Join-Path $ClaudeTarget "settings.local.template.json" -$SettingsFile = Join-Path $ClaudeTarget "settings.json" -$LocalSetFile = Join-Path $ClaudeTarget "settings.local.json" - -# ─── openssl 检测 ──────────────────────────────────── -$cmd = Get-Command openssl -ErrorAction SilentlyContinue -$opensslCmd = if ($cmd) { $cmd.Source } else { $null } -if (-not $opensslCmd) { - $searchPaths = @("C:\Program Files\Git\usr\bin\openssl.exe", "D:\Git\usr\bin\openssl.exe", "D:\Git\mingw64\bin\openssl.exe", "C:\Program Files\Git\mingw64\bin\openssl.exe") - $opensslCmd = $searchPaths | Where-Object { Test-Path $_ } | Select-Object -First 1 -} - -# ─── 辅助函数 ──────────────────────────────────────── - -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 " +------------------------------------------+" -ForegroundColor Cyan - Write-Host "" -} - -function Test-Command($cmd) { - return [bool](Get-Command $cmd -ErrorAction SilentlyContinue) -} - -# ─── 密码本日免输 (Windows Credential Manager) ────── -function Get-CachedSecrets { - try { - $cred = cmdkey /list 2>$null | Select-String "bookworm-secrets" - if ($cred) { - $regPath = "HKCU:\Software\Bookworm\CachedEnv" - if (Test-Path $regPath) { - Add-Type -AssemblyName System.Security - $props = Get-ItemProperty $regPath -ErrorAction SilentlyContinue - $loaded = 0 - foreach ($p in $props.PSObject.Properties) { - if ($p.Name -match '^[A-Z_]+$') { - $val = $p.Value - try { - # DPAPI 解密 (Base64 → byte[] → 明文) - $bytes = [Security.Cryptography.ProtectedData]::Unprotect( - [Convert]::FromBase64String($val), $null, - [Security.Cryptography.DataProtectionScope]::CurrentUser) - $val = [Text.Encoding]::UTF8.GetString($bytes) - } catch { - # 回退: 旧版明文缓存兼容 - } - [System.Environment]::SetEnvironmentVariable($p.Name, $val, "Process") - $loaded++ - } - } - if ($loaded -gt 0 -and $env:ANTHROPIC_API_KEY) { - Write-Host " [OK] 从本日缓存加载 $loaded 个凭证 (免密)" -ForegroundColor Green - return $true - } - } - } - } catch {} - return $false -} - -function Save-SecretsToCache { - try { - cmdkey /generic:bookworm-secrets /user:bw /pass:cached 2>$null | Out-Null - $regPath = "HKCU:\Software\Bookworm\CachedEnv" - if (-not (Test-Path $regPath)) { New-Item $regPath -Force | Out-Null } - Add-Type -AssemblyName System.Security - $envKeys = @("ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL", "GITHUB_PERSONAL_ACCESS_TOKEN", - "SLACK_BOT_TOKEN", "ATLASSIAN_API_TOKEN", "BROWSERBASE_API_KEY", - "FIRECRAWL_API_KEY", "GEMINI_API_KEY") - foreach ($k in $envKeys) { - $v = [System.Environment]::GetEnvironmentVariable($k, "Process") - if ($v) { - # DPAPI 加密: 明文 → byte[] → ProtectedData → Base64 存入注册表 - $bytes = [Text.Encoding]::UTF8.GetBytes($v) - $enc = [Security.Cryptography.ProtectedData]::Protect( - $bytes, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser) - Set-ItemProperty $regPath -Name $k -Value ([Convert]::ToBase64String($enc)) -Force - } - } - $expiry = (Get-Date).Date.AddDays(1).ToString("o") - Set-ItemProperty $regPath -Name "_expiry" -Value $expiry -Force - Write-Host " [OK] 凭证已缓存至今日 23:59 (DPAPI 加密, 下次免密)" -ForegroundColor Green - } catch {} -} - -function Clear-SecretsCache { - cmdkey /delete:bookworm-secrets 2>$null | Out-Null - Remove-Item "HKCU:\Software\Bookworm" -Recurse -Force -ErrorAction SilentlyContinue -} - -# ─── 依赖自动安装 ──────────────────────────────────── -function Install-MissingDeps { - $missing = @() - if (-not (Test-Command "node")) { $missing += "Node.js" } - if (-not (Test-Command "git")) { $missing += "Git" } - if (-not (Test-Command "claude")) { $missing += "Claude Code" } - - if ($missing.Count -eq 0) { return } - - $hasWinget = Test-Command "winget" - Write-Host "" - Write-Host " 缺少以下软件: $($missing -join ', ')" -ForegroundColor Yellow - - if ($hasWinget) { - $auto = if ($AutoAccept) { 'y' } else { Read-Host " 是否用 winget 自动安装? (y/n)" } - if ($auto -eq 'y') { - if ($missing -contains "Node.js") { - Write-Host " 安装 Node.js..." -ForegroundColor Gray - winget install OpenJS.NodeJS.LTS --accept-source-agreements --accept-package-agreements 2>&1 | Select-Object -Last 3 | ForEach-Object { Write-Host " $_" } - # 刷新 PATH - $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") - } - if ($missing -contains "Git") { - Write-Host " 安装 Git..." -ForegroundColor Gray - winget install Git.Git --accept-source-agreements --accept-package-agreements 2>&1 | Select-Object -Last 3 | ForEach-Object { Write-Host " $_" } - $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") - } - if ($missing -contains "Claude Code" -and (Test-Command "npm")) { - Write-Host " 安装 Claude Code..." -ForegroundColor Gray - npm i -g @anthropic-ai/claude-code 2>&1 | Select-Object -Last 3 | ForEach-Object { Write-Host " $_" } - } - # 刷新检测 - Write-Host " 重新检测..." -ForegroundColor Gray - $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") - } - } else { - Write-Host " 请手动安装后重新运行本脚本:" -ForegroundColor Yellow - if ($missing -contains "Node.js") { Write-Host " Node.js: https://nodejs.org" -ForegroundColor Gray } - if ($missing -contains "Git") { Write-Host " Git: https://git-scm.com" -ForegroundColor Gray } - if ($missing -contains "Claude Code") { Write-Host " Claude: npm i -g @anthropic-ai/claude-code" -ForegroundColor Gray } - exit 1 - } -} - -# ─── 桌面快捷方式 ──────────────────────────────────── -function New-DesktopShortcuts { - # v3.0.11 架构重构: .lnk 直调 pwsh + claude.ps1 绝对路径 (1 跳直链) - $desktop = [System.Environment]::GetFolderPath("Desktop") - $bootDir = $ScriptDir - $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) { - 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 - } - } catch { - Write-Host " [!] 桌面快捷方式创建失败: $_" -ForegroundColor Gray - } - - # 迁移清理老 Bookworm.lnk - $oldLnk = Join-Path $desktop "Bookworm.lnk" - if ((Test-Path $oldLnk) -and (Test-Path $lnkPath)) { - try { Remove-Item -LiteralPath $oldLnk -Force -ErrorAction Stop } catch {} - } -} - -function Parse-AuthCode { - param([string]$code) - $code = $code.Trim() - # 格式: BW-YYYYMMDD-24位HexToken - if ($code -notmatch '^BW-(\d{8})-([A-Fa-f0-9]{24})$') { - Write-Host " [!!] 格式错误,应为 BW-YYYYMMDD-XXXXXXXXXXXXXXXXXXXXXXXX" -ForegroundColor Red - return $null - } - $expiryStr = $Matches[1] - $token = $Matches[2].ToLower() # 解密用小写 - $today = (Get-Date).ToString("yyyyMMdd") - if ([int]$expiryStr -lt [int]$today) { - $d = "$($expiryStr.Substring(0,4))-$($expiryStr.Substring(4,2))-$($expiryStr.Substring(6,2))" - Write-Host " [!!] 授权码已过期 (有效期至 $d)" -ForegroundColor Red - Write-Host " 请联系管理员获取新授权码" -ForegroundColor Yellow - return $null - } - return $token -} - -function Resolve-SecretsFile { - param([string]$token) - # 优先找 secrets-{token前8位}.enc (多用户独立 Key),回退 secrets.enc - $fileId = $token.Substring(0, 8) - $perUser = Join-Path $ScriptDir "secrets-$fileId.enc" - if (Test-Path $perUser) { return $perUser } - if (Test-Path $SecretsEnc) { return $SecretsEnc } - return $null -} - -function Decrypt-Secrets { - if ($SkipSecrets) { return } - - # 优先用 Node.js 解密 (跨平台兼容性最高), 回退 openssl - $useNode = (Test-Command "node") -and (Test-Path (Join-Path $ScriptDir "crypto-helper.js")) - if (-not $useNode -and -not $opensslCmd) { - Write-Host " [!] node 和 openssl 均不可用,跳过凭证解密" -ForegroundColor Yellow - return - } - $cryptoHelper = Join-Path $ScriptDir "crypto-helper.js" - - $validAttempts = 0 - $totalAttempts = 0 - while ($validAttempts -lt 3 -and $totalAttempts -lt 10) { - $totalAttempts++ - $label = if ($validAttempts -gt 0) { " 重新输入授权码 (第 $($validAttempts+1)/3 次)" } else { " 输入授权码 (格式: BW-YYYYMMDD-...)" } - $authCodeRaw = Read-Host $label - $plainPwd = Parse-AuthCode $authCodeRaw - if (-not $plainPwd) { continue } - $validAttempts++ - - # 按 token 前8位定位 .enc 文件 - $encFile = Resolve-SecretsFile $plainPwd - if (-not $encFile) { - Write-Host " [!!] 未找到对应的凭证文件 (secrets-*.enc / secrets.enc)" -ForegroundColor Red - Write-Host " 请确认管理员已推送对应文件到 Gitea 并重新拉取" -ForegroundColor Yellow - $plainPwd = $null - continue - } - - $prevEAP = $ErrorActionPreference - $ErrorActionPreference = "Continue" - - if ($useNode) { - $decrypted = & node $cryptoHelper decrypt $plainPwd $encFile 2>&1 - $decExit = $LASTEXITCODE - } else { - $decrypted = $plainPwd | & $opensslCmd enc -aes-256-cbc -d -pbkdf2 -iter 600000 -md sha256 -in $encFile -pass stdin 2>&1 - $decExit = $LASTEXITCODE - } - $ErrorActionPreference = $prevEAP - $plainPwd = $null - - if ($decExit -eq 0 -and $decrypted -and $decrypted -notmatch 'PASSWORD_ERROR|FORMAT_ERROR|bad decrypt') { - $decrypted -split "`n" | ForEach-Object { - $line = $_.Trim() - if ($line -and $line.Contains('=')) { - $eqIdx = $line.IndexOf('=') - $key = $line.Substring(0, $eqIdx).Trim() - $val = $line.Substring($eqIdx + 1).Trim() - [System.Environment]::SetEnvironmentVariable($key, $val, "Process") - Write-Host " [OK] 已注入: $key" -ForegroundColor Green - } - } - return - } - - $remaining = 3 - $validAttempts - if ($remaining -gt 0) { - Write-Host " [!!] 授权码无效 (解密失败),剩余重试: $remaining 次" -ForegroundColor Red - } - } - - Write-Host "" - Write-Host " [ABORT] 3 次授权码均无效,凭证未解密" -ForegroundColor Red - Write-Host " 请确认授权码是否正确,或联系管理员重新生成" -ForegroundColor Yellow - exit 1 -} - -function Render-SettingsTemplate { - if (-not (Test-Path $TemplateFile)) { - Write-Host " [!] 未找到 settings.template.json,跳过渲染" -ForegroundColor Yellow - return - } - - $claudeRoot = $ClaudeTarget.Replace('\', '/') - $homeDir = $env:USERPROFILE.Replace('\', '/') - # 定位 pwsh 路径 (正斜杠供 JSON) - $pwshExe = (Get-Command pwsh -ErrorAction SilentlyContinue) - $pwshJsonPath = if ($pwshExe) { $pwshExe.Source.Replace('\', '/') } else { "pwsh" } - - $content = Get-Content $TemplateFile -Raw - $content = $content -replace '\{\{CLAUDE_ROOT\}\}', $claudeRoot - $content = $content -replace '\{\{HOME\}\}', $homeDir - $content = $content -replace '\{\{PWSH_PATH\}\}', $pwshJsonPath - - Set-Content $SettingsFile -Value $content -Encoding UTF8 - Write-Host " [OK] settings.json 已渲染 (ROOT=$claudeRoot, SHELL=$pwshJsonPath)" -ForegroundColor Green - - # 渲染 settings.local.template.json (如果存在) - if (Test-Path $LocalTplFile) { - $localContent = Get-Content $LocalTplFile -Raw - $localContent = $localContent -replace '\{\{CLAUDE_ROOT\}\}', $claudeRoot - $localContent = $localContent -replace '\{\{HOME\}\}', $homeDir - $localContent = $localContent -replace '\{\{USERNAME\}\}', $env:USERNAME - $localContent = $localContent -replace '\{\{PWSH_PATH\}\}', $pwshJsonPath - Set-Content $LocalSetFile -Value $localContent -Encoding UTF8 - Write-Host " [OK] settings.local.json 已渲染" -ForegroundColor Green - } -} - -# ─── 代理自动检测 ──────────────────────────────────── -function Detect-SystemProxy { - # 中转站在国内阿里云,不走代理 - $env:NO_PROXY = "bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1" - $env:no_proxy = $env:NO_PROXY - - # 如果已手动设置了 HTTPS_PROXY,直接使用 - if ($env:HTTPS_PROXY) { - Write-Host " [OK] 已设置 HTTPS_PROXY=$($env:HTTPS_PROXY)" -ForegroundColor Green - Write-Host " [OK] NO_PROXY=$($env:NO_PROXY)" -ForegroundColor Green - return - } - - Write-Host " 检测系统代理..." -ForegroundColor Gray - - # 方法1: 通过 .NET 获取系统代理 (最可靠,支持 Clash/V2Ray/快柠檬/系统代理) - try { - $proxyUri = [System.Net.WebRequest]::DefaultWebProxy.GetProxy("https://api.anthropic.com") - if ($proxyUri -and $proxyUri.Authority -ne "api.anthropic.com") { - $proxyUrl = "http://$($proxyUri.Authority)" - $env:HTTPS_PROXY = $proxyUrl - $env:HTTP_PROXY = $proxyUrl - Write-Host " [OK] 检测到系统代理: $proxyUrl" -ForegroundColor Green - return - } - } catch {} - - # 方法2: 读取注册表 IE 代理设置 - try { - $reg = Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings" -ErrorAction SilentlyContinue - if ($reg.ProxyEnable -eq 1 -and $reg.ProxyServer) { - $proxy = $reg.ProxyServer - if ($proxy -notmatch '^http') { $proxy = "http://$proxy" } - $env:HTTPS_PROXY = $proxy - $env:HTTP_PROXY = $proxy - Write-Host " [OK] 检测到 IE 代理: $proxy" -ForegroundColor Green - return - } - } catch {} - - # 方法3: 扫描常见代理端口 (500ms 超时,避免阻塞) - $commonPorts = @(7890, 7891, 7893, 10792, 10793, 10808, 10809, 1080, 1087, 8080, 8118) - foreach ($port in $commonPorts) { - try { - $tcp = New-Object System.Net.Sockets.TcpClient - $result = $tcp.BeginConnect("127.0.0.1", $port, $null, $null) - $success = $result.AsyncWaitHandle.WaitOne(500) - if (-not $success) { $tcp.Close(); continue } - $tcp.EndConnect($result) - $tcp.Close() - $env:HTTPS_PROXY = "http://127.0.0.1:$port" - $env:HTTP_PROXY = "http://127.0.0.1:$port" - Write-Host " [OK] 检测到本地代理端口: $port" -ForegroundColor Green - return - } catch {} - } - - # 未找到代理 - Write-Host " [!!] 未检测到代理/VPN" -ForegroundColor Red - Write-Host "" - Write-Host " Claude Code 需要代理才能在国内使用 (启动时检查 api.anthropic.com)" -ForegroundColor Yellow - Write-Host " 请先启动代理软件 (Clash/V2Ray/快柠檬等),然后重新运行本脚本" -ForegroundColor Yellow - Write-Host "" - Write-Host " 如已有代理,可手动指定:" -ForegroundColor Gray - Write-Host " `$env:HTTPS_PROXY = 'http://127.0.0.1:端口号'" -ForegroundColor Gray - Write-Host " pwsh -ExecutionPolicy Bypass -File install.ps1" -ForegroundColor Gray - Write-Host "" - $continue = if ($AutoAccept) { 'y' } else { Read-Host " 无代理继续? (y/n,无代理大概率启动失败)" } - if ($continue -ne 'y') { exit 1 } -} - -# ─── 主流程 ────────────────────────────────────────── - -Write-Banner - -# 步骤 1: 前置检查 -Write-Host "[1/9] 前置检查..." -ForegroundColor White -$checks = @( - @{ Name = "Claude Code"; OK = (Test-Command "claude") } - @{ Name = "Node.js"; OK = (Test-Command "node") } - @{ Name = "Git"; OK = (Test-Command "git") } -) -foreach ($c in $checks) { - $icon = if ($c.OK) { "[OK]" } else { "[!!]" } - $color = if ($c.OK) { "Green" } else { "Red" } - Write-Host " $icon $($c.Name)" -ForegroundColor $color -} -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 -} - -# 步骤 2: 代理检测 (国内必须) -Write-Host "`n[2/9] 代理检测..." -ForegroundColor White - -Detect-SystemProxy - -# 步骤 3: 解密凭证 (优先使用本日缓存) -Write-Host "`n[3/9] 解密凭证..." -ForegroundColor White -# 检查缓存是否过期 -$cacheExpiry = $null -try { - $cacheExpiry = Get-ItemProperty "HKCU:\Software\Bookworm\CachedEnv" -Name "_expiry" -ErrorAction SilentlyContinue -} catch {} -$cacheValid = $false -if ($cacheExpiry -and $cacheExpiry._expiry) { - try { $cacheValid = [datetime]$cacheExpiry._expiry -gt (Get-Date) } catch {} -} - -if ($cacheValid -and (Get-CachedSecrets)) { - # 缓存有效,跳过解密 -} elseif ($AutoAccept) { - # AutoAccept 模式: 无缓存时跳过密码输入 - Write-Host " [!] AutoAccept 模式: 无有效缓存,跳过凭证解密" -ForegroundColor Yellow - Write-Host " 如需凭证,请不加 -AutoAccept 手动运行一次" -ForegroundColor Yellow -} else { - Clear-SecretsCache - Decrypt-Secrets - # 解密成功后询问是否缓存 - if ($env:ANTHROPIC_API_KEY) { - $cache = if ($AutoAccept) { 'y' } else { Read-Host " 今日内免密启动? (y/n)" } - if ($cache -eq 'y') { Save-SecretsToCache } - } -} - -# 自动配置 git credential helper (避免 clone 时反复要密码) -$prevEAP = $ErrorActionPreference -$ErrorActionPreference = "Continue" -git config --global credential.helper store 2>$null -$ErrorActionPreference = $prevEAP - -# 步骤 4: 克隆/更新仓库 -if (-not $StartOnly) { - Write-Host "`n[4/9] 同步 Bookworm 配置..." -ForegroundColor White - - if (Test-Path $ClaudeTarget) { - $isGit = Test-Path (Join-Path $ClaudeTarget ".git") - if ($isGit) { - Write-Host " 已有仓库,执行 git pull..." - Push-Location $ClaudeTarget - $prevEAP = $ErrorActionPreference - $ErrorActionPreference = "Continue" - try { - $stashOutput = git stash 2>&1 - $hasStash = $stashOutput -notmatch 'No local changes' - git pull --rebase 2>&1 | ForEach-Object { Write-Host " $_" } - if ($hasStash) { git stash pop 2>&1 | Out-Null } - } - catch { - Write-Host " [WARN] git pull 失败: $_" -ForegroundColor Yellow - Write-Host " 使用本地现有版本继续" -ForegroundColor Yellow - } - finally { - $ErrorActionPreference = $prevEAP - Pop-Location - } - } - else { - # 安全克隆: 先克隆到临时目录,成功后再替换 - Write-Host " 备份现有 .claude/ 并克隆..." - $tempClone = "$ClaudeTarget.bw-clone-temp" - if (Test-Path $tempClone) { Remove-Item $tempClone -Recurse -Force } - $prevEAP = $ErrorActionPreference - $ErrorActionPreference = "Continue" - git clone --depth 1 $GitUrl $tempClone 2>&1 | ForEach-Object { Write-Host " $_" } - $cloneExit = $LASTEXITCODE - $ErrorActionPreference = $prevEAP - - if ($cloneExit -ne 0 -or -not (Test-Path (Join-Path $tempClone "CLAUDE.md"))) { - Write-Host " [ERROR] 克隆失败 (exit=$cloneExit)" -ForegroundColor Red - if (Test-Path $tempClone) { Remove-Item $tempClone -Recurse -Force -ErrorAction SilentlyContinue } - Write-Host " 原始 .claude/ 未被修改" -ForegroundColor Yellow - exit 1 - } - - # 克隆成功,执行替换 - if (Test-Path $BackupPath) { Remove-Item $BackupPath -Recurse -Force } - Rename-Item $ClaudeTarget $BackupPath - Rename-Item $tempClone $ClaudeTarget - Write-Host " [OK] 克隆完成,原始配置已备份到 .claude.bw-backup/" -ForegroundColor Green - } - } - else { - Write-Host " 首次安装,克隆仓库..." - $prevEAP = $ErrorActionPreference - $ErrorActionPreference = "Continue" - git clone --depth 1 $GitUrl $ClaudeTarget 2>&1 | ForEach-Object { Write-Host " $_" } - $cloneExit = $LASTEXITCODE - $ErrorActionPreference = $prevEAP - - if ($cloneExit -ne 0 -or -not (Test-Path (Join-Path $ClaudeTarget "CLAUDE.md"))) { - Write-Host " [ERROR] 克隆失败 (exit=$cloneExit)" -ForegroundColor Red - Write-Host "" - Write-Host " 可能原因:" -ForegroundColor Yellow - Write-Host " - Gitea 服务不可达 (检查 https://code.letcareme.com)" -ForegroundColor Yellow - Write-Host " - 网络连接问题 (检查 DNS 和防火墙)" -ForegroundColor Yellow - Write-Host " - Git 凭证错误 (检查用户名密码)" -ForegroundColor Yellow - Write-Host "" - Write-Host " 离线模式: 如有本地 .claude 备份,运行:" -ForegroundColor Gray - Write-Host " Copy-Item .claude.bw-backup .claude -Recurse" -ForegroundColor Gray - exit 1 - } - } -} -else { - Write-Host "`n[4/9] StartOnly 模式,跳过同步" -ForegroundColor Gray - # 静默检测远程更新 - $configDir = Join-Path $env:USERPROFILE ".claude" - if (Test-Path (Join-Path $configDir ".git")) { - $prevEAP2 = $ErrorActionPreference - $ErrorActionPreference = "Continue" - git -C $configDir fetch --quiet 2>$null - $behind = git -C $configDir rev-list "HEAD..origin/main" --count 2>$null - $ErrorActionPreference = $prevEAP2 - if ($behind -and [int]$behind -gt 0) { - Write-Host " [!] Bookworm 有 $behind 个新更新可用" -ForegroundColor Yellow - Write-Host " 双击 '更新并启动Bookworm.bat' 可同步最新版本" -ForegroundColor Yellow - } - } -} - -# 步骤 5: 完整性校验 -$integrityFile = Join-Path $ClaudeTarget "integrity.sha256" -if (Test-Path $integrityFile) { - Write-Host "`n[5/9] 完整性校验..." -ForegroundColor White - $failures = @() - Get-Content $integrityFile | ForEach-Object { - if ($_ -match '^([a-f0-9]{64})\s+(.+)$') { - $expectedHash = $Matches[1] - $filePath = Join-Path $ClaudeTarget $Matches[2] - if (Test-Path $filePath) { - $actualHash = (Get-FileHash $filePath -Algorithm SHA256).Hash.ToLower() - if ($actualHash -ne $expectedHash) { - $failures += $Matches[2] - } - } - } - } - if ($failures.Count -gt 0) { - Write-Host " [WARN] 以下文件哈希不匹配:" -ForegroundColor Yellow - $failures | ForEach-Object { Write-Host " $_" -ForegroundColor Yellow } - Write-Host " 可能原因: 仓库内容被修改或本地有改动" -ForegroundColor Yellow - $continue = if ($AutoAccept) { 'y' } else { Read-Host " 继续? (y/n)" } - if ($continue -ne 'y') { exit 1 } - } else { - Write-Host " [OK] 所有文件完整性校验通过" -ForegroundColor Green - } -} else { - Write-Host "`n[5/9] 跳过完整性校验 (无 integrity.sha256)" -ForegroundColor Gray -} - -# 步骤 6: 渲染 settings.json -Write-Host "`n[6/9] 渲染配置模板..." -ForegroundColor White -Render-SettingsTemplate - -# 步骤 7: 确保必要目录存在 -Write-Host "`n[7/9] 初始化本地目录..." -ForegroundColor White -$localDirs = @("debug", "sessions", "cache", "backups", "telemetry", "shell-snapshots", "projects", "memory") -foreach ($d in $localDirs) { - $dirPath = Join-Path $ClaudeTarget $d - if (-not (Test-Path $dirPath)) { - New-Item -ItemType Directory -Path $dirPath -Force | Out-Null - Write-Host " 创建: $d/" -ForegroundColor Gray - } -} - -# 设置环境变量 (进程级) -$env:CLAUDE_HOME = $ClaudeTarget -Write-Host " [OK] CLAUDE_HOME=$ClaudeTarget" -ForegroundColor Green - -# 验证环境变量传递 -$nodeCheck = & node -e "console.log(process.env.CLAUDE_HOME || 'NOT_SET')" 2>$null -if ($nodeCheck -eq $ClaudeTarget) { - Write-Host " [OK] Node.js 环境变量传递验证通过" -ForegroundColor Green -} else { - Write-Host " [WARN] Node.js 环境变量传递异常,hooks 可能无法正确解析路径" -ForegroundColor Yellow -} - -# 步骤 8: Bookworm 完整性验证 + MCP 检查 -Write-Host "`n[8/9] Bookworm 系统验证..." -ForegroundColor White - -# --- Bookworm vs 原生 Claude Code 检测 --- -$bwChecks = @() -$claudeMd = Join-Path $ClaudeTarget "CLAUDE.md" -$skillsDir = Join-Path $ClaudeTarget "skills" -$hooksDir = Join-Path $ClaudeTarget "hooks" -$settingsF = Join-Path $ClaudeTarget "settings.json" - -# 检查 CLAUDE.md -if (Test-Path $claudeMd) { - $content = Get-Content $claudeMd -Raw -ErrorAction SilentlyContinue - if ($content -match "Bookworm") { - $bwChecks += @{ Name = "CLAUDE.md (Bookworm 指令)"; OK = $true } - } else { - $bwChecks += @{ Name = "CLAUDE.md (缺少 Bookworm 指令)"; OK = $false } - } -} else { - $bwChecks += @{ Name = "CLAUDE.md (文件不存在!)"; OK = $false } -} - -# v3.0.5: 阈值按脱敏分发版 (bookworm-portable-config.git) 实际内容定 -# 管理员自用的完整版 (bookworm-config.git) 含 90+ skills, 分发版精简到核心 14+ -$skillCount = 0 -if (Test-Path $skillsDir) { - $skillCount = (Get-ChildItem $skillsDir -Directory -ErrorAction SilentlyContinue).Count -} -$bwChecks += @{ Name = "Skills ($skillCount 个)"; OK = ($skillCount -ge 10) } - -# 检查 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) } - -# 检查 settings.json hooks 配置 -$hasHooks = $false -if (Test-Path $settingsF) { - $sContent = Get-Content $settingsF -Raw -ErrorAction SilentlyContinue - if ($sContent -match '"hooks"') { $hasHooks = $true } -} -$bwChecks += @{ Name = "Settings hooks 配置"; OK = $hasHooks } - -# 输出验证结果 -$allOK = $true -foreach ($c in $bwChecks) { - $icon = if ($c.OK) { "[OK]" } else { "[!!]" } - $color = if ($c.OK) { "Green" } else { "Red" } - Write-Host " $icon $($c.Name)" -ForegroundColor $color - if (-not $c.OK) { $allOK = $false } -} - -if (-not $allOK) { - Write-Host "" - Write-Host " ╔══════════════════════════════════════════════════════╗" -ForegroundColor Yellow - Write-Host " ║ [!] 警告: Bookworm 系统核心资产不足 ║" -ForegroundColor Yellow - Write-Host " ║ Claude Code 将以原生模式启动 (无 Skills/Hooks) ║" -ForegroundColor Yellow - Write-Host " ║ 建议: 检查网络后不加 -StartOnly 重新运行同步 ║" -ForegroundColor Yellow - Write-Host " ╚══════════════════════════════════════════════════════╝" -ForegroundColor Yellow -} else { - Write-Host " [OK] Bookworm 分发版就绪 ($skillCount Skills / $hookCount Hooks / Settings)" -ForegroundColor Green -} - -# --- MCP 依赖检查 (中文提醒) --- -Write-Host "" -Write-Host " MCP 服务检查:" -ForegroundColor Gray -$mcpWarnings = @() - -# Python (askui/pywinauto/com-server 需要) -$hasPython = [bool](Get-Command python -ErrorAction SilentlyContinue) -if (-not $hasPython) { - $mcpWarnings += " [!] Python 未安装 - askui/pywinauto/com-server MCP 不可用" - $mcpWarnings += " 安装: https://www.python.org/downloads/ 或 winget install Python.Python.3.12" -} - -# Playwright (浏览器自动化 MCP) — 轻量检测,避免 npx 触发安装 -$hasPlaywright = $false -try { - $pwPath = & npm list -g @playwright/mcp 2>$null - if ($pwPath -and $pwPath -notmatch 'empty') { $hasPlaywright = $true } -} catch {} -if (-not $hasPlaywright) { - $mcpWarnings += " [!] Playwright MCP 未安装 - 浏览器自动化功能不可用" - $mcpWarnings += " 安装: npm i -g @playwright/mcp && npx playwright install" -} - -# 检查关键 API Key 环境变量 -$apiChecks = @( - @{ Key = "ANTHROPIC_API_KEY"; Name = "Claude API (中转站)" } - @{ Key = "ANTHROPIC_BASE_URL"; Name = "API 中转站地址" } -) -foreach ($ak in $apiChecks) { - $val = [System.Environment]::GetEnvironmentVariable($ak.Key, "Process") - if (-not $val) { - $mcpWarnings += " [!] $($ak.Key) 未设置 - $($ak.Name)不可用" - $mcpWarnings += " 需要管理员重新加密凭证并更新 secrets.enc" - } -} - -# 可选 MCP API Key 检查 -$optionalApis = @( - @{ Key = "GITHUB_PERSONAL_ACCESS_TOKEN"; Name = "GitHub MCP"; Cmd = "仓库管理/代码搜索" } - @{ Key = "SLACK_BOT_TOKEN"; Name = "Slack MCP"; Cmd = "消息发送/频道管理" } - @{ Key = "BROWSERBASE_API_KEY"; Name = "Browserbase MCP"; Cmd = "云端浏览器" } - @{ Key = "FIRECRAWL_API_KEY"; Name = "Firecrawl MCP"; Cmd = "网页抓取/搜索" } -) -$missingOptional = @() -foreach ($api in $optionalApis) { - $val = [System.Environment]::GetEnvironmentVariable($api.Key, "Process") - if (-not $val) { - $missingOptional += $api - } -} - -if ($mcpWarnings.Count -gt 0) { - foreach ($w in $mcpWarnings) { - Write-Host $w -ForegroundColor Yellow - } -} - -if ($missingOptional.Count -gt 0) { - Write-Host "" - Write-Host " 可选 MCP 服务 (未配置, 不影响核心功能):" -ForegroundColor Gray - foreach ($m in $missingOptional) { - Write-Host " [-] $($m.Name) ($($m.Cmd))" -ForegroundColor DarkGray - } - Write-Host " 如需使用, 请联系管理员将 API Key 加入 secrets.enc" -ForegroundColor DarkGray -} - -if ($mcpWarnings.Count -eq 0) { - Write-Host " [OK] 核心 API 已配置" -ForegroundColor Green -} - -# 步骤 9: 启动 Claude Code -Write-Host "`n[9/9] 启动 Claude Code..." -ForegroundColor White -Write-Host "" -if ($allOK) { - Write-Host " ╔══════════════════════════════════════════╗" -ForegroundColor Green - Write-Host " ║ Bookworm 就绪! 正在启动 Claude Code... ║" -ForegroundColor Green - Write-Host " ╚══════════════════════════════════════════╝" -ForegroundColor Green -} else { - Write-Host " ╔══════════════════════════════════════════╗" -ForegroundColor Yellow - Write-Host " ║ 原生模式启动 (Bookworm 不完整) ║" -ForegroundColor Yellow - Write-Host " ╚══════════════════════════════════════════╝" -ForegroundColor Yellow -} -Write-Host "" - -# 首次安装: 创建桌面快捷方式 + 打开使用教程 -if (-not $StartOnly) { - New-DesktopShortcuts - $guidePath = Join-Path $ScriptDir "guide.html" - if (Test-Path $guidePath) { - Start-Process $guidePath - Write-Host " [OK] 使用教程已在浏览器打开" -ForegroundColor Gray - } -} else { - # StartOnly 路径 (老 Bookworm.lnk 指向此): 跑幂等迁移, 单次 ~10ms - # 让只从不点「更新Bookworm」的老用户也自动完成快捷方式命名统一 - New-DesktopShortcuts -} - -# 启动 Claude Code (同步执行, 窗口类型由调用方 .bat 决定) -if ($SkipLaunch) { - Write-Host " [OK] 安装完成 (由调用方负责启动)" -ForegroundColor Green -} else { - & claude --dangerously-skip-permissions -} diff --git a/sync-version.js b/sync-version.js index ac108f6..da6d3ee 100644 --- a/sync-version.js +++ b/sync-version.js @@ -38,10 +38,8 @@ console.log(` sync-version: ${skills} Skills / ${agents} Agents / ${hooks} Hook const FILES = [ 'Bookworm-Setup.sh', 'Bookworm-Setup.bat', - 'Bookworm-OneClick.bat', 'Bookworm-OneClick-Win10.bat', 'Bookworm-OneClick-Mac.sh', - 'install.ps1', 'guide.html', 'guide-mac.html', 'quick-start.html', diff --git a/tools/test-launcher-e2e.ps1 b/tools/test-launcher-e2e.ps1 index 1095386..8bfff03 100644 --- a/tools/test-launcher-e2e.ps1 +++ b/tools/test-launcher-e2e.ps1 @@ -74,18 +74,14 @@ if ($failedFeatures) { } 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 +# Test 3: .lnk Args 契约验证 (auto-setup.ps1; install.ps1 已剥离 → 仅验 auto-setup) +Write-Host "[e2e] Test 3 — .lnk Args 契约 (auto-setup)" -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) { @@ -93,7 +89,7 @@ if ($contractFails) { $contractFails | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } exit 1 } -Write-Host "[e2e ✓] Test 3 — .lnk Args 6 项契约一致" -ForegroundColor Green +Write-Host "[e2e ✓] Test 3 — .lnk Args 3 项契约一致 (auto-setup)" -ForegroundColor Green # Test 4: profile 双注入契约 Write-Host "[e2e] Test 4 — profile 双注入契约 (PS7 + PS5.1)" -ForegroundColor Cyan @@ -116,7 +112,7 @@ 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 3: .lnk Args 3 项契约一致 ✓" Write-Host " Test 4: profile 双注入契约齐全 ✓" Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Green Write-Host "[PASS] E2E 测试通过 (4/4)" -ForegroundColor Green