Compare commits

..

No commits in common. "main" and "v2.0" have entirely different histories.
main ... v2.0

63 changed files with 1411 additions and 13396 deletions

10
.gitignore vendored
View File

@ -1,10 +0,0 @@
secrets.txt
users.txt
authcode-history.log
auto-setup.ps1.bak-*
.tmp-authcodes.json
.tmp-release.json
.tmp-*.png
Bookworm-AuthGen.exe
管理员SOP.html
dist/

View File

@ -1,366 +0,0 @@
#!/bin/bash
# ============================================================
# Bookworm Smart Assistant - macOS 全自动安装 v2.3.1
#
# 用法 (任选一种):
# 方式1: 下载后运行
# chmod +x Bookworm-OneClick-Mac.sh && ./Bookworm-OneClick-Mac.sh
#
# 方式2: 一行命令远程安装
# curl -fsSL https://bookworm.letcareme.com/download/Bookworm-OneClick-Mac.sh | bash
#
# 兼容: macOS 12+ (Monterey/Ventura/Sonoma/Sequoia), Intel & Apple Silicon
# ============================================================
set -e
# ─── 颜色 ───
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m'; BOLD='\033[1m'
# ─── 配置 ───
GITEA_URL="https://code.letcareme.com/bookworm/bookworm-boot.git"
BOOT_DIR="$HOME/bookworm-boot"
CLAUDE_DIR="$HOME/.claude"
TOTAL_STEPS=8
info() { echo -e "${BLUE} [INFO]${NC} $1"; }
success() { echo -e "${GREEN} [OK]${NC} $1"; }
warn() { echo -e "${YELLOW} [!]${NC} $1"; }
fail() { echo -e "${RED} [!!]${NC} $1"; }
step() { echo -e "\n${BOLD} [$1/$TOTAL_STEPS]${NC} ${CYAN}$2${NC}"; }
# ─── Banner ───
echo ""
echo -e "${CYAN}"
echo " ____ _"
echo " | __ ) ___ ___ | | ____ _____ _ __ _ __ ___"
echo " | _ \\ / _ \\ / _ \\| |/ /\\ \\ /\\ / / _ \\| '__| '\`_ \` _ \\"
echo " | |_) | (_) | (_) | < \\ V V / (_) | | | | | | | |"
echo " |____/ \\___/ \\___/|_|\\_\\ \\_/\\_/ \\___/|_| |_| |_| |_|"
echo ""
echo -e " ${BOLD}全自动安装 v2.3.1 — macOS${NC}"
echo -e " ${BLUE}92 Skills | 18 Agents | 34 Hooks${NC}"
echo -e "${NC}"
# ============================================================
# 1. Homebrew
# ============================================================
step 1 "检查 Homebrew"
if ! command -v brew &>/dev/null; then
warn "Homebrew 未安装, 正在安装 (可能需要输入系统密码)..."
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Apple Silicon PATH
if [ -f /opt/homebrew/bin/brew ]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
# 持久化到 shell profile
PROFILE="$HOME/.zprofile"
if ! grep -q 'homebrew' "$PROFILE" 2>/dev/null; then
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> "$PROFILE"
fi
fi
success "Homebrew 安装完成"
else
success "Homebrew $(brew --version | head -1 | awk '{print $2}')"
fi
# ============================================================
# 2. Node.js
# ============================================================
step 2 "检查 Node.js"
if ! command -v node &>/dev/null; then
info "通过 Homebrew 安装 Node.js LTS..."
brew install node
success "Node.js $(node -v) 安装完成"
else
success "Node.js $(node -v)"
fi
# ============================================================
# 3. Git
# ============================================================
step 3 "检查 Git"
if ! command -v git &>/dev/null; then
info "通过 Homebrew 安装 Git..."
brew install git
success "Git $(git --version | awk '{print $3}') 安装完成"
else
success "Git $(git --version | awk '{print $3}')"
fi
# ============================================================
# 4. OpenSSL (凭证解密需要)
# ============================================================
step 4 "检查 OpenSSL"
OPENSSL_CMD=""
for p in /opt/homebrew/opt/openssl/bin/openssl /usr/local/opt/openssl/bin/openssl openssl; do
if command -v "$p" &>/dev/null; then
OPENSSL_CMD="$p"
break
fi
done
if [ -z "$OPENSSL_CMD" ]; then
info "通过 Homebrew 安装 OpenSSL..."
brew install openssl
OPENSSL_CMD="/opt/homebrew/opt/openssl/bin/openssl"
success "OpenSSL 安装完成"
else
success "OpenSSL: $($OPENSSL_CMD version 2>/dev/null | head -1)"
fi
# ============================================================
# 5. Claude Code
# ============================================================
step 5 "检查 Claude Code"
if ! command -v claude &>/dev/null; then
info "通过 npm 安装 Claude Code..."
npm i -g @anthropic-ai/claude-code
success "Claude Code 安装完成"
else
success "Claude Code $(claude --version 2>/dev/null || echo 'installed')"
fi
# ============================================================
# 6. 代理检测
# ============================================================
step 6 "检测网络代理"
export NO_PROXY="bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1"
export no_proxy="$NO_PROXY"
PROXY_FOUND=""
# 环境变量
if [ -n "$HTTPS_PROXY" ] || [ -n "$https_proxy" ]; then
PROXY_FOUND="${HTTPS_PROXY:-$https_proxy}"
success "环境变量代理: $PROXY_FOUND"
fi
# macOS 系统代理
if [ -z "$PROXY_FOUND" ]; then
PROXY_HOST=$(scutil --proxy 2>/dev/null | grep "HTTPSProxy" | awk '{print $3}')
PROXY_PORT=$(scutil --proxy 2>/dev/null | grep "HTTPSPort" | awk '{print $3}')
if [ -n "$PROXY_HOST" ] && [ "$PROXY_HOST" != "0" ] && [ -n "$PROXY_PORT" ] && [ "$PROXY_PORT" != "0" ]; then
PROXY_FOUND="http://$PROXY_HOST:$PROXY_PORT"
export HTTPS_PROXY="$PROXY_FOUND"
export HTTP_PROXY="$PROXY_FOUND"
success "macOS 系统代理: $PROXY_FOUND"
fi
fi
# 常见端口扫描
if [ -z "$PROXY_FOUND" ]; then
for PORT in 7890 7893 7891 1087 1080 8118; do
if nc -z -w1 127.0.0.1 $PORT 2>/dev/null; then
PROXY_FOUND="http://127.0.0.1:$PORT"
export HTTPS_PROXY="$PROXY_FOUND"
export HTTP_PROXY="$PROXY_FOUND"
success "本地代理端口: $PORT"
break
fi
done
fi
if [ -z "$PROXY_FOUND" ]; then
warn "未检测到代理。在国内 Claude Code 可能无法启动。"
warn "请启动代理软件 (ClashX / Surge / V2Ray) 后重试。"
echo ""
read -p " 无代理继续? (y/n): " CONTINUE
if [ "$CONTINUE" != "y" ]; then exit 1; fi
fi
success "NO_PROXY: bww.letcareme.com,code.letcareme.com"
# ============================================================
# 7. 克隆/更新 Bookworm
# ============================================================
step 7 "同步 Bookworm 配置"
git config --global credential.helper osxkeychain 2>/dev/null || true
if [ -d "$BOOT_DIR/.git" ]; then
info "引导仓库已存在, 更新..."
cd "$BOOT_DIR"
git pull --ff-only 2>/dev/null || git pull
success "引导仓库已更新"
else
if [ -d "$BOOT_DIR" ]; then rm -rf "$BOOT_DIR"; fi
info "首次下载 (需输入 Gitea 用户名密码)..."
git clone "$GITEA_URL" "$BOOT_DIR"
cd "$BOOT_DIR"
success "引导仓库克隆完成"
fi
# 执行 macOS 安装脚本 (如存在)
if [ -f "$BOOT_DIR/install-mac.sh" ]; then
info "执行 install-mac.sh..."
bash "$BOOT_DIR/install-mac.sh"
elif [ -f "$BOOT_DIR/install.sh" ]; then
info "执行 install.sh..."
bash "$BOOT_DIR/install.sh"
else
# 回退: 手动执行核心配置步骤
info "未找到安装脚本, 执行基础配置..."
# Keychain 缓存
KC_SVC="bookworm-secrets"
KC_ACCT="$(whoami)"
_kc_load() {
local cached
cached=$(security find-generic-password -s "$KC_SVC" -a "$KC_ACCT" -w 2>/dev/null) || return 1
local expiry_date
expiry_date=$(echo "$cached" | head -1 | sed 's/EXPIRY=//')
[ "$expiry_date" != "$(date +%Y-%m-%d)" ] && { security delete-generic-password -s "$KC_SVC" -a "$KC_ACCT" 2>/dev/null; return 1; }
local count=0
while IFS= read -r line; do
[ -z "$line" ] && continue; [[ "$line" == EXPIRY=* ]] && continue
local key="${line%%=*}" value="${line#*=}"
key=$(echo "$key" | tr -d ' ')
[ -n "$key" ] && [ -n "$value" ] && export "$key=$value" && count=$((count + 1))
done <<< "$cached"
[ $count -gt 0 ] && [ -n "$ANTHROPIC_API_KEY" ] && { success "从 Keychain 缓存加载 $count 个凭证 (免密)"; return 0; }
return 1
}
_kc_save() {
local data="EXPIRY=$(date +%Y-%m-%d)"
for k in ANTHROPIC_API_KEY ANTHROPIC_BASE_URL GITHUB_PERSONAL_ACCESS_TOKEN SLACK_BOT_TOKEN ATLASSIAN_API_TOKEN BROWSERBASE_API_KEY FIRECRAWL_API_KEY; do
local v="${!k}"; [ -n "$v" ] && data="$data
$k=$v"
done
security add-generic-password -s "$KC_SVC" -a "$KC_ACCT" -w "$data" -U 2>/dev/null && \
success "凭证已缓存至今日 23:59 (下次免密)" || true
}
# 解密工具: 优先 node crypto-helper.js (BWENC1 格式), 回退 openssl
CRYPTO_HELPER="$BOOT_DIR/crypto-helper.js"
_do_decrypt() {
local pass="$1" enc="$2"
if command -v node &>/dev/null && [ -f "$CRYPTO_HELPER" ]; then
node "$CRYPTO_HELPER" decrypt "$pass" "$enc" 2>/dev/null
elif [ -n "$OPENSSL_CMD" ]; then
$OPENSSL_CMD enc -aes-256-cbc -d -pbkdf2 -iter 600000 -in "$enc" -pass pass:"$pass" 2>/dev/null
else
return 1
fi
}
# 解密凭证 (先查缓存)
SECRETS_ENC="$BOOT_DIR/secrets.enc"
if _kc_load 2>/dev/null; then
: # 缓存命中
elif [ -f "$SECRETS_ENC" ]; then
echo ""
for attempt in 1 2 3; do
read -rs -p " 输入主密码解密凭证 (第 $attempt/3 次): " PASSWORD
echo ""
DECRYPTED=$(_do_decrypt "$PASSWORD" "$SECRETS_ENC") || true
PASSWORD=""
if [ -n "$DECRYPTED" ]; then
while IFS= read -r line; do
[ -z "$line" ] && continue
key="${line%%=*}"
value="${line#*=}"
key=$(echo "$key" | tr -d ' ')
if [ -n "$key" ] && [ -n "$value" ]; then
export "$key=$value"
success "已注入: $key"
fi
done <<< "$DECRYPTED"
DECRYPTED=""
echo ""
read -p " 今日内免密启动? (y/n): " _cache_yn
[ "$_cache_yn" = "y" ] || [ "$_cache_yn" = "Y" ] && _kc_save
break
else
if [ $attempt -lt 3 ]; then
warn "密码错误, 剩余重试: $((3 - attempt))"
else
fail "3 次密码均错误, 凭证未解密"
warn "可稍后手动配置 API Key"
fi
fi
done
fi
# 克隆 .claude 配置仓库
CLAUDE_REPO="https://code.letcareme.com/bookworm/bookworm-config.git"
if [ -d "$CLAUDE_DIR/.git" ]; then
info "更新 .claude 配置..."
cd "$CLAUDE_DIR" && git pull 2>/dev/null || true
elif [ ! -f "$CLAUDE_DIR/CLAUDE.md" ]; then
info "克隆 .claude 配置..."
if [ -d "$CLAUDE_DIR" ]; then
mv "$CLAUDE_DIR" "$CLAUDE_DIR.bak.$(date +%s)"
fi
git clone --depth 1 "$CLAUDE_REPO" "$CLAUDE_DIR" 2>/dev/null || warn "配置仓库克隆失败"
fi
# 创建本地目录
for d in debug sessions cache backups telemetry memory projects; do
mkdir -p "$CLAUDE_DIR/$d" 2>/dev/null
done
success "基础配置完成"
fi
# ============================================================
# 8. 终端别名 + 完成
# ============================================================
step 8 "配置终端快捷命令"
# 检测 shell
SHELL_RC="$HOME/.zshrc"
if [ -n "$BASH_VERSION" ] && [ -f "$HOME/.bashrc" ]; then
SHELL_RC="$HOME/.bashrc"
fi
ALIAS_MARKER="# Bookworm Portable aliases"
if ! grep -q "$ALIAS_MARKER" "$SHELL_RC" 2>/dev/null; then
cat >> "$SHELL_RC" << 'ALIASES'
# Bookworm Portable aliases
alias bw='cd ~/bookworm-boot && NO_PROXY="bww.letcareme.com,code.letcareme.com,localhost,127.0.0.1" claude'
alias bw-update='cd ~/bookworm-boot && git pull && cd ~/.claude && git pull && echo "Updated!"'
ALIASES
success "已添加到 $SHELL_RC:"
info " bw — 启动 Bookworm"
info " bw-update — 更新 Bookworm"
else
success "终端别名已配置"
fi
# ─── 完成 ───
echo ""
echo -e "${GREEN} ============================================================${NC}"
echo -e "${GREEN} Bookworm Smart Assistant for macOS 安装完成!${NC}"
echo -e "${GREEN} ============================================================${NC}"
echo ""
echo -e " 已安装:"
echo -e " ${GREEN}[v]${NC} Homebrew ${GREEN}[v]${NC} Node.js $(node -v 2>/dev/null)"
echo -e " ${GREEN}[v]${NC} Git ${GREEN}[v]${NC} OpenSSL"
echo -e " ${GREEN}[v]${NC} Claude Code ${GREEN}[v]${NC} Bookworm (92 Skills)"
echo ""
echo -e " ${BOLD}启动方式:${NC}"
echo -e " 终端输入: ${CYAN}bw${NC}"
echo -e " 或: ${CYAN}cd ~/bookworm-boot && claude${NC}"
echo ""
echo -e " ${BOLD}更新:${NC}"
echo -e " 终端输入: ${CYAN}bw-update${NC}"
echo ""
# 询问是否立即启动
read -p " 立即启动 Bookworm? (y/n): " START_NOW
if [ "$START_NOW" = "y" ] || [ "$START_NOW" = "Y" ]; then
echo ""
info "正在启动 Claude Code..."
cd "$HOME"
export NO_PROXY="bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1"
exec claude
fi

View File

@ -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

163
Bookworm-Setup.bat Normal file
View File

@ -0,0 +1,163 @@
@echo off
chcp 65001 > nul
title Bookworm Smart Assistant - 一键安装
color 1F
:: 中转站在国内,不走代理
set NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1
set no_proxy=%NO_PROXY%
echo.
echo +================================================+
echo ^| ^|
echo ^| Bookworm Smart Assistant ^|
echo ^| 一键安装程序 v1.3 ^|
echo ^| ^|
echo ^| 92 Skills / 18 Agents / 29 Hooks ^|
echo ^| ^|
echo +================================================+
echo.
:: ─── 检查依赖 ───────────────────────────────────────
echo [1/4] 检查环境...
echo.
where git >nul 2>nul
if %errorlevel% neq 0 (
echo [!!] Git 未安装
echo.
echo 请先安装 Git:
echo 下载: https://git-scm.com/download/win
echo 安装后重新运行本程序
echo.
pause
exit /b 1
)
echo [OK] Git
where node >nul 2>nul
if %errorlevel% neq 0 (
echo [!!] Node.js 未安装
echo.
echo 请先安装 Node.js:
echo 下载: https://nodejs.org (选 LTS 版本)
echo 安装后重新打开本程序
echo.
pause
exit /b 1
)
echo [OK] Node.js
where claude >nul 2>nul
if %errorlevel% neq 0 (
echo [..] Claude Code 未安装,正在安装...
call npm i -g @anthropic-ai/claude-code
if %errorlevel% neq 0 (
echo [!!] Claude Code 安装失败
echo 手动安装: npm i -g @anthropic-ai/claude-code
pause
exit /b 1
)
)
echo [OK] Claude Code
echo.
:: ─── 选择安装目录 ───────────────────────────────────
set "INSTALL_DIR=%USERPROFILE%\bookworm-boot"
:: ─── 克隆/更新仓库 ──────────────────────────────────
echo [2/4] 下载 Bookworm...
echo.
if exist "%INSTALL_DIR%\.git" (
echo 已安装,更新到最新版...
cd /d "%INSTALL_DIR%"
git pull 2>&1
) else (
if exist "%INSTALL_DIR%" (
echo 目录已存在但非 git 仓库,清理后重新下载...
rmdir /s /q "%INSTALL_DIR%" 2>nul
)
echo 首次下载...
git config --global credential.helper store
git clone https://code.letcareme.com/bookworm/bookworm-boot.git "%INSTALL_DIR%" 2>&1
if %errorlevel% neq 0 (
echo.
echo [!!] 下载失败
echo 请检查:
echo - 网络是否正常
echo - 是否能访问 https://code.letcareme.com
echo - Gitea 用户名密码是否正确
echo.
pause
exit /b 1
)
)
echo.
echo [OK] Bookworm 文件已就绪
echo.
:: ─── 创建桌面快捷方式 ───────────────────────────────
echo [3/4] 创建桌面快捷方式...
echo.
:: 用 PowerShell 创建 .lnk
powershell -ExecutionPolicy Bypass -Command ^
"$s=(New-Object -COM WScript.Shell).CreateShortcut('%USERPROFILE%\Desktop\Bookworm.lnk');^
$s.TargetPath='%INSTALL_DIR%\启动Bookworm.bat';^
$s.WorkingDirectory='%INSTALL_DIR%';^
$s.Description='Bookworm Smart Assistant';^
$s.Save()" 2>nul
if %errorlevel% equ 0 (
echo [OK] 桌面快捷方式: Bookworm
) else (
echo [!] 快捷方式创建失败 (不影响使用)
)
powershell -ExecutionPolicy Bypass -Command ^
"$s=(New-Object -COM WScript.Shell).CreateShortcut('%USERPROFILE%\Desktop\更新Bookworm.lnk');^
$s.TargetPath='%INSTALL_DIR%\更新并启动Bookworm.bat';^
$s.WorkingDirectory='%INSTALL_DIR%';^
$s.Description='Bookworm 更新并启动';^
$s.Save()" 2>nul
echo.
:: ─── 启动 Bookworm ──────────────────────────────────
echo [4/4] 启动 Bookworm...
echo.
echo +================================================+
echo ^| ^|
echo ^| 安装完成! ^|
echo ^| ^|
echo ^| 桌面已创建快捷方式: ^|
echo ^| Bookworm - 日常启动 ^|
echo ^| 更新Bookworm - 同步后启动 ^|
echo ^| ^|
echo ^| 接下来会启动 Bookworm ^|
echo ^| 请输入管理员提供的主密码 ^|
echo ^| ^|
echo +================================================+
echo.
cd /d "%INSTALL_DIR%"
:: 打开使用教程
if exist "%INSTALL_DIR%\guide.html" (
start "" "%INSTALL_DIR%\guide.html"
)
:: 启动安装脚本
where pwsh >nul 2>nul
if %errorlevel% equ 0 (
pwsh -ExecutionPolicy Bypass -File install.ps1 -AutoAccept
) else (
powershell -ExecutionPolicy Bypass -File install.ps1 -AutoAccept
)
if %errorlevel% neq 0 (
echo.
echo 启动失败,请查看上方错误信息
pause
)

Binary file not shown.

View File

@ -1,686 +0,0 @@
#!/bin/bash
# ============================================================
# Bookworm Portable - macOS Setup (从 boot 仓库内运行)
# Version: 3.0.11
#
# 用法: 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
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
BOLD='\033[1m'
# 配置
BOOT_DIR="$(cd "$(dirname "$0")" && pwd)"
CLAUDE_DIR="$HOME/.claude"
CONFIG_REPO="https://code.letcareme.com/bookworm/bookworm-config.git"
SECRETS_ENC="$BOOT_DIR/secrets.enc"
TOTAL_STEPS=6
banner() {
echo ""
echo -e "${CYAN} ____ _"
echo " | __ ) ___ ___ | | ____ _____ _ __ _ __ ___"
echo " | _ \\ / _ \\ / _ \\| |/ /\\ \\ /\\ / / _ \\| '__| '\` _ \\"
echo " | |_) | (_) | (_) | < \\ V V / (_) | | | | | | | |"
echo " |____/ \\___/ \\___/|_|\\_\\ \\_/\\_/ \\___/|_| |_| |_| |_|"
echo ""
echo -e " ${BOLD}Portable macOS Setup v2.3.1${NC}"
echo -e " ${BLUE}92 Skills | 18 Agents | 34 Hooks${NC}"
echo -e "${NC}"
}
info() { echo -e " ${BLUE}[INFO]${NC} $1"; }
success() { echo -e " ${GREEN}[OK]${NC} $1"; }
warn() { echo -e " ${YELLOW}[!]${NC} $1"; }
fail() { echo -e " ${RED}[!!]${NC} $1"; }
step() { echo -e "\n${BOLD} [$1/$TOTAL_STEPS]${NC} ${CYAN}$2${NC}"; }
# ============================================================
# Banner
# ============================================================
banner
# ============================================================
# Step 1: 检查并安装依赖
# ============================================================
step 1 "检查依赖软件"
# Homebrew
if ! command -v brew &>/dev/null; then
warn "Homebrew 未安装, 正在安装 (可能需要输入系统密码)..."
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
if [ -f /opt/homebrew/bin/brew ]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
PROFILE="$HOME/.zprofile"
if ! grep -q 'homebrew' "$PROFILE" 2>/dev/null; then
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> "$PROFILE"
fi
fi
success "Homebrew 安装完成"
else
success "Homebrew $(brew --version | head -1 | awk '{print $2}')"
fi
# Node.js
if ! command -v node &>/dev/null; then
info "通过 Homebrew 安装 Node.js..."
brew install node
success "Node.js $(node -v) 安装完成"
else
success "Node.js $(node -v)"
fi
# Git
if ! command -v git &>/dev/null; then
info "通过 Homebrew 安装 Git..."
brew install git
success "Git $(git --version | awk '{print $3}') 安装完成"
else
success "Git $(git --version | awk '{print $3}')"
fi
# OpenSSL
OPENSSL_CMD=""
for p in /opt/homebrew/opt/openssl/bin/openssl /usr/local/opt/openssl/bin/openssl openssl; do
if command -v "$p" &>/dev/null; then
OPENSSL_CMD="$p"
break
fi
done
if [ -z "$OPENSSL_CMD" ]; then
info "通过 Homebrew 安装 OpenSSL..."
brew install openssl
OPENSSL_CMD="/opt/homebrew/opt/openssl/bin/openssl"
success "OpenSSL 安装完成"
else
success "OpenSSL: $($OPENSSL_CMD version 2>/dev/null | head -1)"
fi
# Claude Code
if ! command -v claude &>/dev/null; then
info "通过 npm 安装 Claude Code..."
npm i -g @anthropic-ai/claude-code
success "Claude Code 安装完成"
else
success "Claude Code $(claude --version 2>/dev/null || echo 'installed')"
fi
# ============================================================
# Step 2: 检测代理
# ============================================================
step 2 "检测网络代理"
export NO_PROXY="bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1"
export no_proxy="$NO_PROXY"
PROXY_FOUND=""
# 环境变量
if [ -n "$HTTPS_PROXY" ] || [ -n "$https_proxy" ]; then
PROXY_FOUND="${HTTPS_PROXY:-$https_proxy}"
success "环境变量代理: $PROXY_FOUND"
fi
# macOS 系统代理
if [ -z "$PROXY_FOUND" ]; then
PROXY_HOST=$(scutil --proxy 2>/dev/null | grep "HTTPSProxy" | awk '{print $3}')
PROXY_PORT=$(scutil --proxy 2>/dev/null | grep "HTTPSPort" | awk '{print $3}')
if [ -n "$PROXY_HOST" ] && [ "$PROXY_HOST" != "0" ] && [ -n "$PROXY_PORT" ] && [ "$PROXY_PORT" != "0" ]; then
PROXY_FOUND="http://$PROXY_HOST:$PROXY_PORT"
export HTTPS_PROXY="$PROXY_FOUND"
export HTTP_PROXY="$PROXY_FOUND"
success "macOS 系统代理: $PROXY_FOUND"
fi
fi
# 常见端口扫描 (500ms 超时)
if [ -z "$PROXY_FOUND" ]; then
for PORT in 7890 7893 7891 1087 1080 8118; do
if nc -z -w1 127.0.0.1 $PORT 2>/dev/null; then
PROXY_FOUND="http://127.0.0.1:$PORT"
export HTTPS_PROXY="$PROXY_FOUND"
export HTTP_PROXY="$PROXY_FOUND"
success "本地代理端口: $PORT"
break
fi
done
fi
if [ -z "$PROXY_FOUND" ]; then
warn "未检测到代理。在国内 Claude Code 可能无法启动。"
warn "请启动代理软件 (ClashX / Surge / V2Ray) 后重试。"
echo ""
read -p " 无代理继续? (y/n): " CONTINUE
if [ "$CONTINUE" != "y" ]; then exit 1; fi
fi
success "NO_PROXY: bww.letcareme.com,code.letcareme.com"
# ============================================================
# Step 3: 克隆/更新配置仓库到 ~/.claude
# ============================================================
step 3 "同步 Bookworm 配置"
git config --global credential.helper osxkeychain 2>/dev/null || true
if [ -d "$CLAUDE_DIR/.git" ]; then
info "配置仓库已存在, 更新..."
cd "$CLAUDE_DIR"
# 设置 git 身份 (auto-resolve 需要)
git config user.email "bookworm@auto.local" 2>/dev/null
git config user.name "Bookworm" 2>/dev/null
# 清除冲突状态 (运行时文件不重要, 后续会重新渲染)
git reset --hard HEAD 2>/dev/null || true
if git pull --rebase --autostash 2>/dev/null; then
success "配置仓库已更新"
else
warn "git pull 失败, 使用本地版本"
fi
cd "$BOOT_DIR"
elif [ -f "$CLAUDE_DIR/CLAUDE.md" ]; then
warn "~/.claude 已存在但非 git 仓库, 备份后克隆..."
mv "$CLAUDE_DIR" "$CLAUDE_DIR.bak.$(date +%s)"
git clone --depth 1 "$CONFIG_REPO" "$CLAUDE_DIR"
success "配置仓库克隆完成 (旧目录已备份)"
else
info "首次安装, 克隆配置仓库 (需输入 Gitea 密码)..."
mkdir -p "$(dirname "$CLAUDE_DIR")"
git clone --depth 1 "$CONFIG_REPO" "$CLAUDE_DIR"
success "配置仓库克隆完成"
fi
# 创建本地运行时目录
for d in debug sessions cache backups telemetry memory projects; do
mkdir -p "$CLAUDE_DIR/$d" 2>/dev/null
done
# ============================================================
# Step 4: 解密凭证 (含 Keychain 本日免密)
# ============================================================
step 4 "解密凭证"
# ─── v3.0.1: $BW_LICENSE_KEY 静默激活 (零输入路径) ───
# 若 install.sh 通过 env 传入 License Key (BW-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX), 优先走这条
# activate.js 已支持 HTTPS_PROXY 的 HTTP CONNECT 隧道 (Gitea ce354ca)
ACTIVATE_JS="$CLAUDE_DIR/lib/activate.js"
BW_TOKEN_FILE="$HOME/.claude/.bw-token"
if [ -n "$BW_LICENSE_KEY" ] && [[ "$BW_LICENSE_KEY" =~ ^BW-[A-F0-9]{4}(-[A-F0-9]{4}){5}$ ]] && [ -f "$ACTIVATE_JS" ] && command -v node &>/dev/null; then
info "检测到 \$BW_LICENSE_KEY, 静默激活..."
if printf '%s' "$BW_LICENSE_KEY" | node "$ACTIVATE_JS" 2>&1 | tail -3 | grep -q "OK\|激活成功"; then
if [ -f "$BW_TOKEN_FILE" ]; then
success "License 静默激活成功"
else
warn "activate.js 返回 OK 但 .bw-token 未生成, 回退到交互模式"
fi
else
warn "静默激活失败, 回退到交互模式 (中转站 sk-Key 流程)"
fi
unset BW_LICENSE_KEY # 清掉, 不在子进程泄露
fi
# Keychain 缓存相关
KEYCHAIN_SERVICE="bookworm-secrets"
KEYCHAIN_ACCOUNT="$(whoami)"
CACHE_LOADED=false
# 尝试从 Keychain 加载缓存
load_cached_secrets() {
local cached
cached=$(security find-generic-password -s "$KEYCHAIN_SERVICE" -a "$KEYCHAIN_ACCOUNT" -w 2>/dev/null) || return 1
# 检查是否过期 (缓存格式: EXPIRY=ISO日期\nKEY=VALUE\n...)
local expiry
expiry=$(echo "$cached" | head -1)
local expiry_date="${expiry#EXPIRY=}"
local today
today=$(date +%Y-%m-%d)
if [ "$expiry_date" != "$today" ]; then
# 已过期,删除缓存
security delete-generic-password -s "$KEYCHAIN_SERVICE" -a "$KEYCHAIN_ACCOUNT" 2>/dev/null || true
return 1
fi
# 加载环境变量 (跳过 EXPIRY 行)
local count=0
while IFS= read -r line; do
[ -z "$line" ] && continue
[[ "$line" == EXPIRY=* ]] && continue
local key="${line%%=*}"
local value="${line#*=}"
key=$(echo "$key" | tr -d ' ')
if [ -n "$key" ] && [ -n "$value" ]; then
export "$key=$value"
count=$((count + 1))
fi
done <<< "$cached"
if [ $count -gt 0 ] && [ -n "$ANTHROPIC_API_KEY" ]; then
success "从 Keychain 缓存加载 $count 个凭证 (免密)"
CACHE_LOADED=true
return 0
fi
return 1
}
# 保存凭证到 Keychain
save_secrets_to_cache() {
local today
today=$(date +%Y-%m-%d)
local data="EXPIRY=$today"
local env_keys="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"
for k in $env_keys; do
local v="${!k}"
if [ -n "$v" ]; then
data="$data
$k=$v"
fi
done
security add-generic-password -s "$KEYCHAIN_SERVICE" -a "$KEYCHAIN_ACCOUNT" -w "$data" -U 2>/dev/null && \
success "凭证已缓存至今日 23:59 (下次免密)" || \
warn "Keychain 缓存失败 (不影响使用)"
}
# 解密工具: 优先 node crypto-helper.js (BWENC1 格式), 回退 openssl
CRYPTO_HELPER="$BOOT_DIR/crypto-helper.js"
_decrypt_secrets() {
local pass="$1" enc="$2"
if command -v node &>/dev/null && [ -f "$CRYPTO_HELPER" ]; then
node "$CRYPTO_HELPER" decrypt "$pass" "$enc" 2>/dev/null
elif [ -n "$OPENSSL_CMD" ]; then
$OPENSSL_CMD enc -aes-256-cbc -d -pbkdf2 -iter 600000 -in "$enc" -pass pass:"$pass" 2>/dev/null
else
return 1
fi
}
# 按 token 前8位定位 .enc 文件 (多用户模式),回退 secrets.enc
resolve_secrets_file() {
local token="$1"
local file_id="${token:0:8}"
local per_user="$BOOT_DIR/secrets-${file_id}.enc"
if [ -f "$per_user" ]; then
echo "$per_user"
elif [ -f "$SECRETS_ENC" ]; then
echo "$SECRETS_ENC"
else
echo ""
fi
}
# 解析授权码: BW-YYYYMMDD-TOKEN (24位Hex)
# 返回: 小写 token (成功) | "EXPIRED" (已过期) | "" (格式错误)
parse_authcode() {
local code
code=$(echo "$1" | tr '[:lower:]' '[:upper:]' | tr -d ' ')
# 格式校验: BW-8位数字-24位Hex
if [[ ! "$code" =~ ^BW-([0-9]{8})-([A-F0-9]{24})$ ]]; then
echo ""
return
fi
local expiry_str="${BASH_REMATCH[1]}"
local token_upper="${BASH_REMATCH[2]}"
local today
today=$(date +%Y%m%d)
if [ "$expiry_str" -lt "$today" ]; then
echo "EXPIRED"
return
fi
echo "$token_upper" | tr '[:upper:]' '[:lower:]' # 兼容 bash 3.2 (macOS 默认)
}
# 先尝试缓存
if load_cached_secrets 2>/dev/null; then
: # 缓存加载成功
else
# 优先级 3: 调用 change-key.js 验证+持久化 (stdin 管道, 无 argv 泄露)
CHANGE_KEY_JS="$CLAUDE_DIR/change-key.js"
if [ -f "$CHANGE_KEY_JS" ] && command -v node &>/dev/null; then
echo ""
info "配置中转站凭证 (https://bww.letcareme.com)"
for attempt in 1 2 3; do
echo ""
read -rs -p " 粘贴凭证 (第 $attempt/3 次, 输入不显示, 留空跳过): " UCRED
echo ""
[ -z "$UCRED" ] && { warn "已跳过"; break; }
if printf '%s' "$UCRED" | node "$CHANGE_KEY_JS"; then
UCRED=""
success "凭证已验证并持久化"
echo ""
info "换凭证方式:"
info " 1. 重跑安装器"
info " 2. bash ~/.claude/change-key.sh"
info " 3. Claude Code 里: /change-key"
break
else
UCRED=""
[ $attempt -lt 3 ] && warn "验证失败, 剩余 $((3-attempt))" || fail "3 次失败"
fi
done
fi
fi
# 优先级 3.5: v3.0.1 新增 — 直接输入 sk- Key (中转站 Key) + 5 模型候选验证
# 适用: fresh install 没 change-key.js, 没 .enc 文件的新用户 (BYOK)
if [ -z "$ANTHROPIC_API_KEY" ]; then
# 测 sk- Key 是否可调通 (5 模型候选, 中转站白名单)
validate_sk_key() {
local key="$1"
local baseurl="${ANTHROPIC_BASE_URL:-https://bww.letcareme.com}"
local models=("claude-opus-4-7" "claude-opus-4-6" "claude-opus-4-6-thinking" "claude-sonnet-4-6" "claude-sonnet-4-6-thinking")
for model in "${models[@]}"; do
local code
code=$(curl -sS -o /dev/null -w "%{http_code}" --max-time 15 --noproxy '*' \
-X POST "$baseurl/v1/messages" \
-H "x-api-key: $key" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d "{\"model\":\"$model\",\"max_tokens\":1,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}" 2>/dev/null)
# 401/403 认证失败, 立即退, 不继续试
[[ "$code" == "401" || "$code" == "403" ]] && { echo "AUTH_FAIL"; return 1; }
# 200 或 400 都说明 Key 通过, 400 只是请求体问题
[[ "$code" == "200" || "$code" == "400" ]] && { echo "OK"; return 0; }
# 503/404 继续试下个模型
done
echo "NO_CHANNEL" # 全部 503 = 中转站无渠道
return 1
}
echo ""
info "配置中转站 API Key (没有的话去 bww.letcareme.com 注册+充值)"
for attempt in 1 2 3; do
echo ""
read -rs -p " 粘贴 sk- Key (第 $attempt/3 次, 输入不显示, 留空跳过): " SK_KEY
echo ""
[ -z "$SK_KEY" ] && { warn "已跳过"; break; }
# 基础格式校验
if [[ ! "$SK_KEY" =~ ^sk- ]] || [ ${#SK_KEY} -lt 20 ]; then
warn "格式错误 (应 sk- 开头, 至少 20 字符), 请重试"
continue
fi
info "验证中 (试 5 个模型候选)..."
result=$(validate_sk_key "$SK_KEY")
case "$result" in
OK)
success "sk- Key 验证成功"
# v3.0.1: chmod 600 防同机其它 uid 读取 + 清 .bak 残留 (red-team-attacker P0)
for rc in "$HOME/.zshrc" "$HOME/.bashrc"; do
[ -f "$rc" ] || touch "$rc"
# BSD sed (macOS 默认): -i '' 无 .bak; GNU sed (Linux): -i 无 .bak
if sed --version 2>/dev/null | grep -q GNU; then
sed -i '/^export ANTHROPIC_API_KEY=/d' "$rc" 2>/dev/null || true
sed -i '/^export ANTHROPIC_BASE_URL=/d' "$rc" 2>/dev/null || true
else
sed -i '' '/^export ANTHROPIC_API_KEY=/d' "$rc" 2>/dev/null || true
sed -i '' '/^export ANTHROPIC_BASE_URL=/d' "$rc" 2>/dev/null || true
fi
echo "export ANTHROPIC_API_KEY=\"$SK_KEY\"" >> "$rc"
echo "export ANTHROPIC_BASE_URL=\"https://bww.letcareme.com\"" >> "$rc"
chmod 600 "$rc" # 只 owner 可读, 防同机 uid 泄露
done
# 扫残留 .bak 副本 (可能含旧 Key)
rm -f "$HOME/.zshrc.bak" "$HOME/.bashrc.bak" 2>/dev/null || true
export ANTHROPIC_API_KEY="$SK_KEY"
export ANTHROPIC_BASE_URL="https://bww.letcareme.com"
# 存 Keychain 本日免密
security add-generic-password -s "$KEYCHAIN_SERVICE" -a "$KEYCHAIN_ACCOUNT" -w "ANTHROPIC_API_KEY=$SK_KEY
ANTHROPIC_BASE_URL=https://bww.letcareme.com
EXPIRY=$(date -v+1d -u +%FT%TZ 2>/dev/null || date -u -d '+1 day' +%FT%TZ)" -U 2>/dev/null || true
SK_KEY=""
break
;;
AUTH_FAIL)
warn "Key 无效或余额为 0 (中转站返回 401/403)"
SK_KEY=""
[ $attempt -lt 3 ] && continue || { fail "3 次失败, 跳过 sk- 配置"; break; }
;;
NO_CHANNEL)
fail "中转站没有可用 Claude 渠道 (5 模型全返 503). 联系中转站客服"
SK_KEY=""
break
;;
*)
warn "验证异常, 剩余 $((3-attempt))"
SK_KEY=""
;;
esac
done
fi
# 优先级 4: 授权码模式 (向后兼容旧用户)
if [ -z "$ANTHROPIC_API_KEY" ] && { [ -f "$SECRETS_ENC" ] || ls "$BOOT_DIR"/secrets-*.enc 2>/dev/null | head -1 | grep -q .; }; then
DECRYPTED=""
valid_attempts=0
total_attempts=0
while [ $valid_attempts -lt 3 ] && [ $total_attempts -lt 10 ]; do
echo ""
read -p " 输入授权码 (BW-YYYYMMDD-XXXXXX, 第 $((valid_attempts+1))/3 次): " AUTH_CODE
total_attempts=$((total_attempts + 1))
TOKEN=$(parse_authcode "$AUTH_CODE")
AUTH_CODE=""
if [ "$TOKEN" = "EXPIRED" ]; then
warn "授权码已过期, 请联系管理员获取新授权码"
continue # 不消耗有效次数
elif [ -z "$TOKEN" ]; then
warn "授权码格式错误 (格式: BW-YYYYMMDD-XXXXXXXXXXXXXXXXXXXXXXXX)"
continue # 不消耗有效次数
fi
valid_attempts=$((valid_attempts + 1))
ENC_FILE=$(resolve_secrets_file "$TOKEN")
if [ -z "$ENC_FILE" ]; then
FILE_ID="${TOKEN:0:8}"
warn "未找到对应凭证文件 (secrets-${FILE_ID}.enc / secrets.enc)"
warn "请联系管理员确认已推送对应文件,然后重新运行安装器"
TOKEN=""
continue
fi
DECRYPTED=$(_decrypt_secrets "$TOKEN" "$ENC_FILE") || true
TOKEN=""
if [ -n "$DECRYPTED" ]; then
# 白名单校验 (与 Windows 版对齐)
ALLOWED_KEYS="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"
while IFS= read -r line; do
[ -z "$line" ] && continue
key="${line%%=*}"
value="${line#*=}"
key=$(echo "$key" | tr -d ' ')
if [ -n "$key" ] && [ -n "$value" ]; then
# 白名单 + 长度校验
if echo "$ALLOWED_KEYS" | grep -qw "$key" && [ ${#value} -lt 512 ]; then
export "$key=$value"
success "已注入: $key"
else
warn "跳过未知 key: $key"
fi
fi
done <<< "$DECRYPTED"
DECRYPTED=""
# 自动缓存 (不再询问, 与 Windows 版对齐)
save_secrets_to_cache
break
else
if [ $valid_attempts -lt 3 ]; then
warn "授权码无效 (解密失败), 剩余重试: $((3 - valid_attempts))"
else
fail "3 次授权码均无效, 凭证未解密"
warn "可稍后手动配置 API Key"
fi
fi
done
else
if [ ! -f "$SECRETS_ENC" ]; then
warn "secrets.enc 不存在, 跳过凭证解密"
info "请联系管理员获取授权码"
fi
fi
# ─── v3.0.2: 预填 ~/.claude.json 跳过 Claude Code 2.0.1 的登录选择页 ───
# Win auto-setup.ps1:1361-1369 等价逻辑: 两选项都走 anthropic.com OAuth, 国内不通
# 预填 hasCompletedOnboarding + customApiKeyResponses.approved 直接进主界面
if [ -n "$ANTHROPIC_API_KEY" ] && command -v node &>/dev/null; then
BW_KEY_PREFIX="${ANTHROPIC_API_KEY:0:20}" node -e '
const fs = require("fs"), p = require("path");
const H = process.env.HOME || process.env.USERPROFILE;
const f = p.join(H, ".claude.json");
const prefix = process.env.BW_KEY_PREFIX || "";
let d = {};
try { d = JSON.parse(fs.readFileSync(f, "utf8")); } catch (e) {}
d.hasCompletedOnboarding = true;
d.hasSeenWelcome = true;
d.bypassPermissionsModeAccepted = true;
d.customApiKeyResponses = d.customApiKeyResponses || { approved: [], rejected: [] };
if (prefix && !d.customApiKeyResponses.approved.includes(prefix)) {
d.customApiKeyResponses.approved.push(prefix);
}
if (d.numStartups === undefined) d.numStartups = 5;
if (!d.projects) d.projects = {};
fs.writeFileSync(f, JSON.stringify(d, null, 2));
' 2>/dev/null && success "Claude Code onboarding 已预填 (跳过 2.0.1 登录选择页)" || warn ".claude.json onboarding 预填失败 (首次启动可能需手工过登录画面)"
fi
# ── MCP 注入到 ~/.claude.json (Claude Code v2.1+ 正确位置) ──
INJECT_SCRIPT="$CLAUDE_DIR/inject-mcp.js"
MCP_INJECTED=false
# 方案 A: 调用 config 仓库里的 inject-mcp.js
if [ -f "$INJECT_SCRIPT" ] && command -v node &>/dev/null; then
MCP_OUT=$(node "$INJECT_SCRIPT" 2>&1) && {
success "$MCP_OUT"
MCP_INJECTED=true
} || warn "inject-mcp.js 执行失败"
fi
# 方案 B: 内嵌 fallback (git pull 失败时)
if [ "$MCP_INJECTED" = false ] && command -v node &>/dev/null; then
info "inject-mcp.js 不可用, 使用内嵌 MCP 注入..."
FALLBACK_JS=$(mktemp /tmp/bw-mcp-XXXXXX.js)
cat > "$FALLBACK_JS" << 'MCPEOF'
var fs=require("fs"),p=require("path");
var H=process.env.HOME||process.env.USERPROFILE;
var f=p.join(H,".claude.json");
var d={};try{d=JSON.parse(fs.readFileSync(f,"utf8"))}catch(e){}
var N="npx",Y="--yes",S={};
S.context7={command:N,args:[Y,"@upstash/context7-mcp@2.1.1"],type:"stdio"};
S.playwright={command:N,args:[Y,"@playwright/mcp@0.0.68","--headless"],type:"stdio"};
S["session-continuity"]={command:N,args:[Y,"claude-session-continuity-mcp@1.13.0"],type:"stdio"};
S["browser-mcp"]={command:N,args:[Y,"@browsermcp/mcp@latest"],type:"stdio"};
S["desktop-commander"]={command:N,args:[Y,"@wonderwhy-er/desktop-commander@latest"],type:"stdio"};
S["chrome-devtools"]={command:N,args:[Y,"chrome-devtools-mcp@0.18.1"],type:"stdio"};
S.github={command:N,args:[Y,"@modelcontextprotocol/server-github"],type:"stdio"};
S.slack={command:N,args:[Y,"@modelcontextprotocol/server-slack"],type:"stdio"};
S.firecrawl={command:N,args:[Y,"firecrawl-mcp"],type:"stdio"};
S["mcp-image"]={command:N,args:[Y,"mcp-image"],type:"stdio"};
S["google-drive"]={command:N,args:[Y,"@piotr-agier/google-drive-mcp"],type:"stdio"};
S.browserbase={command:N,args:[Y,"@anthropic-ai/browserbase-mcp"],type:"stdio"};
S.notebooklm={command:N,args:[Y,"notebooklm-mcp@latest"],type:"stdio"};
S.cloudflare={command:N,args:[Y,"mcp-remote","https://docs.mcp.cloudflare.com/sse"],type:"stdio"};
S.mobile={command:N,args:[Y,"@mobilenext/mobile-mcp@0.0.35"],type:"stdio"};
var K="@modelcontextprotocol/server-sequential-thinking";
S["sequential-thinking"]={command:N,args:[Y,K+"@2025.12.18"],type:"stdio"};
S.linear={type:"http",url:"https://mcp.linear.app/mcp"};
S.supabase={type:"http",url:"https://mcp.supabase.com/mcp?project_ref=oepmihbtoylosbsxlmfo"};
S.figma={type:"http",url:"https://mcp.figma.com/mcp"};
S["windows-mcp"]={command:"uvx",args:["--python","3.13","windows-mcp"],type:"stdio"};
S.atlassian={command:"uvx",args:["mcp-atlassian"],type:"stdio"};
S["computer-control-mcp"]={command:"uvx",args:["computer-control-mcp@latest"],type:"stdio"};
d.mcpServers=S;
fs.writeFileSync(f,JSON.stringify(d,null,2));
console.log("OK: "+Object.keys(S).length+" MCP servers (fallback)");
MCPEOF
MCP_OUT=$(node "$FALLBACK_JS" 2>&1) && success "$MCP_OUT" || warn "MCP fallback 注入失败"
rm -f "$FALLBACK_JS"
fi
# 渲染 settings.json (替换占位符)
TEMPLATE_FILE="$CLAUDE_DIR/settings.template.json"
SETTINGS_FILE="$CLAUDE_DIR/settings.json"
if [ -f "$TEMPLATE_FILE" ]; then
CLAUDE_ROOT=$(echo "$CLAUDE_DIR" | sed 's/\\/\//g')
SHELL_BIN="${SHELL:-/bin/zsh}"
sed "s|{{CLAUDE_ROOT}}|$CLAUDE_ROOT|g; s|{{HOME}}|$HOME|g; s|{{PWSH_PATH}}|$SHELL_BIN|g" "$TEMPLATE_FILE" > "$SETTINGS_FILE"
success "settings.json 已渲染 (SHELL=$SHELL_BIN)"
fi
# ============================================================
# Step 5: 配置终端别名
# ============================================================
step 5 "配置终端快捷命令"
SHELL_RC="$HOME/.zshrc"
if [ -n "$BASH_VERSION" ] && [ -f "$HOME/.bashrc" ]; then
SHELL_RC="$HOME/.bashrc"
fi
ALIAS_MARKER="# Bookworm Portable aliases"
if ! grep -q "$ALIAS_MARKER" "$SHELL_RC" 2>/dev/null; then
cat >> "$SHELL_RC" << 'ALIASES'
# Bookworm Portable aliases
alias bw='NO_PROXY="bww.letcareme.com,code.letcareme.com,localhost,127.0.0.1" ANTHROPIC_MODEL="${ANTHROPIC_MODEL:-claude-opus-4-7}" claude --dangerously-skip-permissions'
alias bw-update='cd ~/bookworm-boot && git pull && cd ~/.claude && git pull && echo "Updated!"'
ALIASES
success "已添加到 $SHELL_RC:"
info " bw -- 启动 Bookworm"
info " bw-update -- 更新 Bookworm"
else
# 更新旧别名 (bookworm → bw)
if grep -q "alias bookworm=" "$SHELL_RC" 2>/dev/null; then
# 删除旧别名块,然后追加新的
sed -i '' '/# Bookworm Portable aliases/,/^$/d' "$SHELL_RC" 2>/dev/null || true
cat >> "$SHELL_RC" << 'ALIASES'
# Bookworm Portable aliases
alias bw='NO_PROXY="bww.letcareme.com,code.letcareme.com,localhost,127.0.0.1" ANTHROPIC_MODEL="${ANTHROPIC_MODEL:-claude-opus-4-7}" claude --dangerously-skip-permissions'
alias bw-update='cd ~/bookworm-boot && git pull && cd ~/.claude && git pull && echo "Updated!"'
ALIASES
success "终端别名已更新 (bookworm → bw)"
else
success "终端别名已配置"
fi
fi
# ============================================================
# Step 6: 完成
# ============================================================
step 6 "安装完成"
echo ""
echo -e "${GREEN} ============================================================${NC}"
echo -e "${GREEN} Bookworm Smart Assistant for macOS 安装完成!${NC}"
echo -e "${GREEN} ============================================================${NC}"
echo ""
echo -e " 已安装:"
echo -e " ${GREEN}[v]${NC} Homebrew ${GREEN}[v]${NC} Node.js $(node -v 2>/dev/null)"
echo -e " ${GREEN}[v]${NC} Git ${GREEN}[v]${NC} OpenSSL"
echo -e " ${GREEN}[v]${NC} Claude Code ${GREEN}[v]${NC} Bookworm (92 Skills)"
echo ""
echo -e " ${BOLD}启动方式:${NC}"
echo -e " 终端输入: ${CYAN}bw${NC}"
echo ""
echo -e " ${BOLD}更新:${NC}"
echo -e " 终端输入: ${CYAN}bw-update${NC}"
echo ""
# 询问是否立即启动
read -p " 立即启动 Bookworm? (y/n): " START_NOW
if [ "$START_NOW" = "y" ] || [ "$START_NOW" = "Y" ]; then
echo ""
info "正在启动 Claude Code..."
cd "$HOME"
export NO_PROXY="bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1"
# v3.0.1: 默认模型 (中转站兼容, 默认 claude-sonnet-4-5 会 503)
export ANTHROPIC_MODEL="${ANTHROPIC_MODEL:-claude-opus-4-7}"
exec claude --dangerously-skip-permissions
fi

View File

@ -6,8 +6,8 @@
deploy-gitea.sh ECS Gitea 部署 (服务端,执行一次)
prepare-repo.ps1 仓库准备 (本机执行一次)
encrypt-secrets.ps1 凭证加密 (本机执行一次)
settings.template.json settings.json 模板
settings.local.template.json settings.local.json 模板 (权限白名单)
(settings.template.json 已由 build-portable.js 管理,存于 config 仓库)
install.ps1 安装/启动 (目标机执行)
stop.ps1 清理/卸载 (目标机执行)

View File

@ -1,482 +0,0 @@
<#
.SYNOPSIS
Bookworm 授权码生成器 (管理员 GUI 工具)
.DESCRIPTION
内部调用 node gen-authcode.js, 提供可视化界面生成多用户授权码
打包命令: build.ps1 -Admin
#>
# 全局错误捕获 (PS2EXE -NoOutput 会吞掉所有错误, 这里确保弹窗显示)
try {
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
[System.Windows.Forms.Application]::EnableVisualStyles()
# ─── 路径 ─────────────────────────────────────────────
$ScriptDir = if ($PSScriptRoot) { $PSScriptRoot }
elseif ([System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName -match '\.exe$') {
Split-Path -Parent ([System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName)
} elseif ($MyInvocation.MyCommand.Path) {
Split-Path -Parent $MyInvocation.MyCommand.Path
} else { $PWD.Path }
# gen-authcode.js / secrets.txt 查找: 当前目录 → 父目录 (dist/ 内运行时)
$GenScript = Join-Path $ScriptDir "gen-authcode.js"
if (-not (Test-Path $GenScript)) {
$parentDir = Split-Path $ScriptDir -Parent
$GenScript = Join-Path $parentDir "gen-authcode.js"
if (Test-Path $GenScript) { $ScriptDir = $parentDir } # 切到父目录
}
$SecretsTxt = Join-Path $ScriptDir "secrets.txt"
# ─── 品牌色 ───────────────────────────────────────────
$brandBlue = [System.Drawing.Color]::FromArgb(88, 101, 242)
$brandDark = [System.Drawing.Color]::FromArgb(24, 25, 38)
$brandLight = [System.Drawing.Color]::FromArgb(245, 246, 250)
$cardBg = [System.Drawing.Color]::FromArgb(248, 249, 253)
$successGreen = [System.Drawing.Color]::FromArgb(35, 134, 54)
$warningOrange = [System.Drawing.Color]::FromArgb(227, 137, 29)
$textPrimary = [System.Drawing.Color]::FromArgb(36, 41, 47)
$textSecondary = [System.Drawing.Color]::FromArgb(110, 119, 129)
$inputBorder = [System.Drawing.Color]::FromArgb(208, 215, 222)
# ─── 主窗口 ───────────────────────────────────────────
$form = New-Object System.Windows.Forms.Form
$form.Text = "Bookworm 授权码生成器"
$form.ClientSize = New-Object System.Drawing.Size(540, 594)
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedSingle"
$form.MaximizeBox = $false
$form.BackColor = [System.Drawing.Color]::White
$form.Font = New-Object System.Drawing.Font("Segoe UI", 9)
# ─── 标题栏 (深色渐变 + 副标题) ──────────────────────
$header = New-Object System.Windows.Forms.Panel
$header.Dock = "Top"
$header.Size = New-Object System.Drawing.Size(540, 70)
$header.BackColor = $brandDark
$form.Controls.Add($header)
$titleLabel = New-Object System.Windows.Forms.Label
$titleLabel.Location = New-Object System.Drawing.Point(24, 12)
$titleLabel.Size = New-Object System.Drawing.Size(500, 28)
$titleLabel.Text = "Bookworm 授权码生成器"
$titleLabel.Font = New-Object System.Drawing.Font("Segoe UI", 15, [System.Drawing.FontStyle]::Bold)
$titleLabel.ForeColor = [System.Drawing.Color]::White
$header.Controls.Add($titleLabel)
$subtitleLabel = New-Object System.Windows.Forms.Label
$subtitleLabel.Location = New-Object System.Drawing.Point(24, 42)
$subtitleLabel.Size = New-Object System.Drawing.Size(500, 18)
$subtitleLabel.Text = "管理员专用 · 为每位用户生成独立授权码与加密凭证"
$subtitleLabel.Font = New-Object System.Drawing.Font("Segoe UI", 8.5)
$subtitleLabel.ForeColor = [System.Drawing.Color]::FromArgb(160, 170, 200)
$header.Controls.Add($subtitleLabel)
# ═══ 输入卡片 (Panel 模拟卡片) ═══════════════════════
$inputCard = New-Object System.Windows.Forms.Panel
$inputCard.Location = New-Object System.Drawing.Point(20, 84)
$inputCard.Size = New-Object System.Drawing.Size(500, 186)
$inputCard.BackColor = $cardBg
$form.Controls.Add($inputCard)
# ── 卡片标题
$inputTitle = New-Object System.Windows.Forms.Label
$inputTitle.Location = New-Object System.Drawing.Point(16, 10)
$inputTitle.Size = New-Object System.Drawing.Size(200, 20)
$inputTitle.Text = "用户信息"
$inputTitle.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
$inputTitle.ForeColor = $brandBlue
$inputCard.Controls.Add($inputTitle)
# ── 用户名
$lblUser = New-Object System.Windows.Forms.Label
$lblUser.Location = New-Object System.Drawing.Point(16, 40)
$lblUser.Size = New-Object System.Drawing.Size(90, 22)
$lblUser.Text = "用户标识"
$lblUser.ForeColor = $textPrimary
$lblUser.TextAlign = [System.Drawing.ContentAlignment]::MiddleRight
$inputCard.Controls.Add($lblUser)
$txtUser = New-Object System.Windows.Forms.TextBox
$txtUser.Location = New-Object System.Drawing.Point(114, 38)
$txtUser.Size = New-Object System.Drawing.Size(370, 26)
$txtUser.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$txtUser.BorderStyle = "FixedSingle"
$inputCard.Controls.Add($txtUser)
# ── Relay Key
$lblKey = New-Object System.Windows.Forms.Label
$lblKey.Location = New-Object System.Drawing.Point(16, 74)
$lblKey.Size = New-Object System.Drawing.Size(90, 22)
$lblKey.Text = "Relay Key"
$lblKey.ForeColor = $textPrimary
$lblKey.TextAlign = [System.Drawing.ContentAlignment]::MiddleRight
$inputCard.Controls.Add($lblKey)
$txtKey = New-Object System.Windows.Forms.TextBox
$txtKey.Location = New-Object System.Drawing.Point(114, 72)
$txtKey.Size = New-Object System.Drawing.Size(370, 26)
$txtKey.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$txtKey.PasswordChar = '*'
$txtKey.BorderStyle = "FixedSingle"
$inputCard.Controls.Add($txtKey)
$chkShowKey = New-Object System.Windows.Forms.CheckBox
$chkShowKey.Location = New-Object System.Drawing.Point(114, 102)
$chkShowKey.Size = New-Object System.Drawing.Size(100, 20)
$chkShowKey.Text = "显示 Key"
$chkShowKey.ForeColor = $textSecondary
$chkShowKey.Font = New-Object System.Drawing.Font("Segoe UI", 8)
$chkShowKey.Add_CheckedChanged({ $txtKey.PasswordChar = if ($chkShowKey.Checked) { [char]0 } else { '*' } })
$inputCard.Controls.Add($chkShowKey)
# ── 有效期
$lblDays = New-Object System.Windows.Forms.Label
$lblDays.Location = New-Object System.Drawing.Point(16, 132)
$lblDays.Size = New-Object System.Drawing.Size(90, 22)
$lblDays.Text = "有效期"
$lblDays.ForeColor = $textPrimary
$lblDays.TextAlign = [System.Drawing.ContentAlignment]::MiddleRight
$inputCard.Controls.Add($lblDays)
$cmbDays = New-Object System.Windows.Forms.ComboBox
$cmbDays.Location = New-Object System.Drawing.Point(114, 130)
$cmbDays.Size = New-Object System.Drawing.Size(80, 26)
$cmbDays.DropDownStyle = "DropDownList"
$cmbDays.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$null = $cmbDays.Items.AddRange(@(7, 14, 30, 60, 90, 180, 365))
$cmbDays.SelectedIndex = 2
$inputCard.Controls.Add($cmbDays)
$lblDaysUnit = New-Object System.Windows.Forms.Label
$lblDaysUnit.Location = New-Object System.Drawing.Point(198, 132)
$lblDaysUnit.Size = New-Object System.Drawing.Size(30, 22)
$lblDaysUnit.Text = ""
$lblDaysUnit.ForeColor = $textSecondary
$inputCard.Controls.Add($lblDaysUnit)
$lblDaysHint = New-Object System.Windows.Forms.Label
$lblDaysHint.Location = New-Object System.Drawing.Point(240, 132)
$lblDaysHint.Size = New-Object System.Drawing.Size(240, 22)
$lblDaysHint.Text = ""
$lblDaysHint.ForeColor = $textSecondary
$lblDaysHint.Font = New-Object System.Drawing.Font("Segoe UI", 8.5)
$inputCard.Controls.Add($lblDaysHint)
$cmbDays.Add_SelectedIndexChanged({
$d = [int]$cmbDays.SelectedItem
$lblDaysHint.Text = "-> 到期: " + (Get-Date).AddDays($d).ToString("yyyy-MM-dd")
})
$cmbDays.SelectedIndex = 2
# ── 环境状态行 ──
$nodeOK = [bool](Get-Command node -ErrorAction SilentlyContinue)
$secretsOK = Test-Path $SecretsTxt
$lblStatus1 = New-Object System.Windows.Forms.Label
$lblStatus1.Location = New-Object System.Drawing.Point(16, 160)
$lblStatus1.Size = New-Object System.Drawing.Size(230, 18)
$lblStatus1.Font = New-Object System.Drawing.Font("Segoe UI", 8)
if ($secretsOK) {
$lineCount = (Get-Content $SecretsTxt -ErrorAction SilentlyContinue | Where-Object { $_ -match '=' }).Count
$lblStatus1.Text = "secrets.txt: $lineCount 个凭证"
$lblStatus1.ForeColor = $successGreen
} else {
$lblStatus1.Text = "secrets.txt: 未找到"
$lblStatus1.ForeColor = [System.Drawing.Color]::Red
}
$inputCard.Controls.Add($lblStatus1)
$lblStatus2 = New-Object System.Windows.Forms.Label
$lblStatus2.Location = New-Object System.Drawing.Point(250, 160)
$lblStatus2.Size = New-Object System.Drawing.Size(230, 18)
$lblStatus2.Font = New-Object System.Drawing.Font("Segoe UI", 8)
if ($nodeOK) {
$nodeVer = try { (& node --version 2>$null) } catch { "" }
$lblStatus2.Text = "Node.js: $nodeVer"
$lblStatus2.ForeColor = $successGreen
} else {
$lblStatus2.Text = "Node.js: 未安装"
$lblStatus2.ForeColor = [System.Drawing.Color]::Red
}
$inputCard.Controls.Add($lblStatus2)
# ═══ 操作按钮 ═════════════════════════════════════════
$btnGenerate = New-Object System.Windows.Forms.Button
$btnGenerate.Location = New-Object System.Drawing.Point(138, 282)
$btnGenerate.Size = New-Object System.Drawing.Size(180, 42)
$btnGenerate.Text = " 生成授权码"
$btnGenerate.Font = New-Object System.Drawing.Font("Segoe UI", 11, [System.Drawing.FontStyle]::Bold)
$btnGenerate.FlatStyle = "Flat"
$btnGenerate.BackColor = $brandBlue
$btnGenerate.ForeColor = [System.Drawing.Color]::White
$btnGenerate.FlatAppearance.BorderSize = 0
$btnGenerate.Cursor = [System.Windows.Forms.Cursors]::Hand
$btnGenerate.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
$form.Controls.Add($btnGenerate)
$btnClear = New-Object System.Windows.Forms.Button
$btnClear.Location = New-Object System.Drawing.Point(330, 282)
$btnClear.Size = New-Object System.Drawing.Size(72, 42)
$btnClear.Text = "清空"
$btnClear.Font = New-Object System.Drawing.Font("Segoe UI", 9)
$btnClear.FlatStyle = "Flat"
$btnClear.BackColor = [System.Drawing.Color]::White
$btnClear.ForeColor = $textSecondary
$btnClear.FlatAppearance.BorderColor = $inputBorder
$btnClear.FlatAppearance.BorderSize = 1
$form.Controls.Add($btnClear)
# ═══ 结果卡片 ═════════════════════════════════════════
$resultCard = New-Object System.Windows.Forms.Panel
$resultCard.Location = New-Object System.Drawing.Point(20, 338)
$resultCard.Size = New-Object System.Drawing.Size(500, 220)
$resultCard.BackColor = $cardBg
$form.Controls.Add($resultCard)
$lblResultTitle = New-Object System.Windows.Forms.Label
$lblResultTitle.Location = New-Object System.Drawing.Point(16, 10)
$lblResultTitle.Size = New-Object System.Drawing.Size(200, 20)
$lblResultTitle.Text = "生成结果"
$lblResultTitle.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
$lblResultTitle.ForeColor = $brandBlue
$resultCard.Controls.Add($lblResultTitle)
$txtAuthCode = New-Object System.Windows.Forms.TextBox
$txtAuthCode.Location = New-Object System.Drawing.Point(16, 38)
$txtAuthCode.Size = New-Object System.Drawing.Size(380, 34)
$txtAuthCode.Font = New-Object System.Drawing.Font("Consolas", 13, [System.Drawing.FontStyle]::Bold)
$txtAuthCode.ReadOnly = $true
$txtAuthCode.BackColor = [System.Drawing.Color]::White
$txtAuthCode.ForeColor = $brandDark
$txtAuthCode.BorderStyle = "FixedSingle"
$txtAuthCode.Text = ""
$resultCard.Controls.Add($txtAuthCode)
$btnCopy = New-Object System.Windows.Forms.Button
$btnCopy.Location = New-Object System.Drawing.Point(404, 38)
$btnCopy.Size = New-Object System.Drawing.Size(80, 34)
$btnCopy.Text = "复制"
$btnCopy.Font = New-Object System.Drawing.Font("Segoe UI", 9)
$btnCopy.FlatStyle = "Flat"
$btnCopy.BackColor = $brandBlue
$btnCopy.ForeColor = [System.Drawing.Color]::White
$btnCopy.FlatAppearance.BorderSize = 0
$btnCopy.Cursor = [System.Windows.Forms.Cursors]::Hand
$btnCopy.Enabled = $false
$resultCard.Controls.Add($btnCopy)
$lblDetails = New-Object System.Windows.Forms.Label
$lblDetails.Location = New-Object System.Drawing.Point(16, 82)
$lblDetails.Size = New-Object System.Drawing.Size(468, 66)
$lblDetails.Text = "点击「生成授权码」后,结果将显示在此处。"
$lblDetails.ForeColor = $textSecondary
$lblDetails.Font = New-Object System.Drawing.Font("Segoe UI", 8.5)
$resultCard.Controls.Add($lblDetails)
# 推送到 Gitea 按钮
$btnPush = New-Object System.Windows.Forms.Button
$btnPush.Location = New-Object System.Drawing.Point(16, 156)
$btnPush.Size = New-Object System.Drawing.Size(468, 38)
$btnPush.Text = "推送到 Gitea (git add + commit + push)"
$btnPush.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$btnPush.FlatStyle = "Flat"
$btnPush.BackColor = $successGreen
$btnPush.ForeColor = [System.Drawing.Color]::White
$btnPush.FlatAppearance.BorderSize = 0
$btnPush.Cursor = [System.Windows.Forms.Cursors]::Hand
$btnPush.Enabled = $false
$resultCard.Controls.Add($btnPush)
# ═══ 状态栏 ═══════════════════════════════════════════
$statusBar = New-Object System.Windows.Forms.Label
$statusBar.Location = New-Object System.Drawing.Point(0, 568)
$statusBar.Size = New-Object System.Drawing.Size(540, 26)
$statusBar.BackColor = $brandDark
$statusBar.ForeColor = [System.Drawing.Color]::FromArgb(160, 170, 200)
$statusBar.Text = " 就绪 | $ScriptDir"
$statusBar.Font = New-Object System.Drawing.Font("Segoe UI", 8)
$statusBar.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
$form.Controls.Add($statusBar)
# ─── 事件处理 ─────────────────────────────────────────
$btnCopy.Add_Click({
if ($txtAuthCode.Text) {
[System.Windows.Forms.Clipboard]::SetText($txtAuthCode.Text)
$statusBar.Text = " 已复制到剪贴板"
$statusBar.ForeColor = $successGreen
}
})
$btnPush.Add_Click({
$btnPush.Enabled = $false
$btnPush.Text = "推送中..."
$statusBar.Text = " git add + commit + push ..."
$statusBar.ForeColor = $warningOrange
[System.Windows.Forms.Application]::DoEvents()
try {
$gitExe = (Get-Command git -ErrorAction Stop).Source
$userName = if ($global:lastGenUser) { $global:lastGenUser } else { "user" }
# git add secrets-*.enc
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $gitExe
$psi.Arguments = "add secrets-*.enc"
$psi.UseShellExecute = $false
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
$psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8
$psi.CreateNoWindow = $true
$psi.WorkingDirectory = $ScriptDir
$p = [System.Diagnostics.Process]::Start($psi)
$p.WaitForExit(15000)
# git commit
$psi.Arguments = "commit -m `"add user $userName`""
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardOutput.ReadToEnd() | Out-Null
$p.WaitForExit(15000)
# git push
$statusBar.Text = " git push 中 (可能需要几秒)..."
[System.Windows.Forms.Application]::DoEvents()
$psi.Arguments = "push"
$p = [System.Diagnostics.Process]::Start($psi)
$pushOut = $p.StandardError.ReadToEnd() # git push 输出在 stderr
$p.WaitForExit(30000)
if ($p.ExitCode -eq 0) {
$btnPush.Text = "已推送"
$btnPush.BackColor = [System.Drawing.Color]::FromArgb(200, 220, 200)
$btnPush.ForeColor = $successGreen
$statusBar.Text = " 推送成功 — 现在可以把授权码发给 $userName"
$statusBar.ForeColor = $successGreen
} else {
throw "git push 失败: $pushOut"
}
} catch {
$btnPush.Text = "推送失败 (点击重试)"
$btnPush.BackColor = [System.Drawing.Color]::FromArgb(220, 200, 200)
$btnPush.ForeColor = [System.Drawing.Color]::Red
$btnPush.Enabled = $true
$statusBar.Text = " 推送失败: $_"
$statusBar.ForeColor = [System.Drawing.Color]::Red
[System.Windows.Forms.MessageBox]::Show("推送失败:`n$_`n`n请检查 Git 配置和网络。", "Git 错误", "OK", "Error")
}
})
$btnClear.Add_Click({
$txtUser.Text = ""
$txtKey.Text = ""
$txtAuthCode.Text = ""
$lblDetails.Text = ""
$btnCopy.Enabled = $false
$statusBar.Text = " 已清空"
$statusBar.ForeColor = [System.Drawing.Color]::Gray
})
$btnGenerate.Add_Click({
# 校验
$user = $txtUser.Text.Trim()
$key = $txtKey.Text.Trim()
$days = $cmbDays.SelectedItem.ToString()
if (-not $user) {
[System.Windows.Forms.MessageBox]::Show("请输入用户标识", "缺少用户名", "OK", "Warning")
$txtUser.Focus(); return
}
if (-not $key) {
[System.Windows.Forms.MessageBox]::Show("请输入 Relay Sub-Key`n(从中转站后台为用户创建)", "缺少 Sub-Key", "OK", "Warning")
$txtKey.Focus(); return
}
if (-not (Test-Path $SecretsTxt)) {
[System.Windows.Forms.MessageBox]::Show("secrets.txt 不存在:`n$SecretsTxt`n`n请先创建 secrets.txt (每行 KEY=VALUE)", "缺少凭证文件", "OK", "Error")
return
}
if (-not $nodeOK) {
[System.Windows.Forms.MessageBox]::Show("Node.js 未安装。`n请先安装: https://nodejs.org", "缺少 Node.js", "OK", "Error")
return
}
$statusBar.Text = " 生成中..."
$statusBar.ForeColor = $warningOrange
$btnGenerate.Enabled = $false
[System.Windows.Forms.Application]::DoEvents()
try {
# 用 Process + UTF8 编码读取 (PS2EXE 默认 GBK 会乱码)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = (Get-Command node -ErrorAction Stop).Source
$psi.Arguments = "`"$GenScript`" $days -k `"$key`" -u `"$user`""
$psi.UseShellExecute = $false
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
$psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8
$psi.CreateNoWindow = $true
$psi.WorkingDirectory = $ScriptDir
$proc = [System.Diagnostics.Process]::Start($psi)
$stdout = $proc.StandardOutput.ReadToEnd()
$stderr = $proc.StandardError.ReadToEnd()
$proc.WaitForExit()
if ($proc.ExitCode -ne 0) {
throw "gen-authcode.js 退出码 $($proc.ExitCode)`n$stderr"
}
# 解析输出 (编码无关: PS2EXE 下中文可能乱码, 只匹配 ASCII 格式)
$authCode = if ($stdout -match '(BW-\d{8}-[A-F0-9]{24})') { $Matches[1] } else { "" }
$fileId = if ($stdout -match '(secrets-[a-f0-9]{8}\.enc)') { $Matches[1] } else { "" }
$expiry = if ($stdout -match '(\d{4}-\d{2}-\d{2})') { $Matches[1] } else { "" }
if ($authCode) {
$txtAuthCode.Text = $authCode
$lblDetails.Text = "用户: $user`n加密文件: $fileId`n有效期: $days 天 (至 $expiry)`n路径: $ScriptDir\$fileId"
$btnCopy.Enabled = $true
$btnPush.Enabled = $true
$global:lastGenUser = $user
$global:lastGenFile = $fileId
$statusBar.Text = " 生成成功 — 点击「推送到 Gitea」完成部署"
$statusBar.ForeColor = $successGreen
# 追加历史记录 (仅本机, 已在 .gitignore)
$historyFile = Join-Path $ScriptDir "authcode-history.log"
$logLine = "$(Get-Date -Format 'yyyy-MM-dd HH:mm') $($user.PadRight(12)) $fileId $($days)天 至$expiry $authCode"
try { Add-Content -Path $historyFile -Value $logLine -Encoding utf8 } catch {}
} else {
throw "无法解析授权码输出:`n$stdout"
}
} catch {
$txtAuthCode.Text = ""
$lblDetails.Text = "错误: $_"
$btnCopy.Enabled = $false
$statusBar.Text = " 生成失败"
$statusBar.ForeColor = [System.Drawing.Color]::Red
[System.Windows.Forms.MessageBox]::Show("生成失败:`n$_", "错误", "OK", "Error")
} finally {
$btnGenerate.Enabled = $true
}
})
# ─── 启动 ─────────────────────────────────────────────
$form.Add_Shown({ $txtUser.Focus() })
[System.Windows.Forms.Application]::Run($form)
} catch {
# 全局错误弹窗 (PS2EXE 下唯一的错误可见方式)
$errMsg = "Bookworm AuthGen 启动失败:`n`n" +
"错误: $($_.Exception.Message)`n" +
"行号: $($_.InvocationInfo.ScriptLineNumber)`n" +
"代码: $($_.InvocationInfo.Line.Trim())`n`n" +
"ScriptDir: $ScriptDir`n" +
"GenScript: $GenScript`n" +
"SecretsTxt: $SecretsTxt"
try {
[System.Windows.Forms.MessageBox]::Show($errMsg, "AuthGen 错误", "OK", "Error")
} catch {
# 如果连 MsgBox 都失败 (WinForms 未加载), 写文件
$errMsg | Out-File "$env:TEMP\bookworm-authgen-error.txt" -Encoding utf8
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

View File

@ -1,118 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<defs>
<!-- ── FILTER: Center Glow ── -->
<filter id="center-glow" x="-60%" y="-60%" width="220%" height="220%">
<feGaussianBlur in="SourceGraphic" stdDeviation="8" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="center-glow-sm" x="-80%" y="-80%" width="260%" height="260%">
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="dot-glow" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur in="SourceGraphic" stdDeviation="2.5" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<!-- ── RADIAL BG ── -->
<radialGradient id="bg-gradient" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#141420"/>
<stop offset="100%" stop-color="#0C0C14"/>
</radialGradient>
<!-- ── CENTER GLOW GRADIENT ── -->
<radialGradient id="center-gradient" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#FFFFFF" stop-opacity="1"/>
<stop offset="30%" stop-color="#FFFFFF" stop-opacity="0.9"/>
<stop offset="60%" stop-color="#C8D8FF" stop-opacity="0.4"/>
<stop offset="100%" stop-color="#A0C0FF" stop-opacity="0"/>
</radialGradient>
<!-- ── MONOCHROME GRADIENT ── -->
<radialGradient id="mono-center" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#FFFFFF"/>
<stop offset="30%" stop-color="#FFE88A" stop-opacity="0.9"/>
<stop offset="100%" stop-color="#C8A050" stop-opacity="0"/>
</radialGradient>
<!-- ════════════════════════════════════════════
FULL QUANTUM ROTATION SYMBOL (512×512 viewBox)
Fibonacci Spiral: r = 18 × e^(bθ), b = ln(φ)/(π/2)
32 dots from outermost to center
════════════════════════════════════════════ -->
</defs>
<!-- Background -->
<rect width="512" height="512" fill="url(#bg-gradient)"/>
<!-- Golden spiral path — r(θ)=3.5×e^(0.3063θ), θ∈[0,13.5], ~2.15 turns -->
<path d="M 259.5,256 L 259.6,255.8 L 259.6,255.5 L 259.6,255.3 L 259.7,255 L 259.7,254.7 L 259.6,254.4 L 259.6,254.2 L 259.5,253.9 L 259.5,253.6 L 259.4,253.3 L 259.2,253 L 259.1,252.8 L 258.9,252.5 L 258.7,252.2 L 258.5,252 L 258.3,251.7 L 258,251.5 L 257.8,251.2 L 257.5,251 L 257.2,250.8 L 256.8,250.7 L 256.5,250.5 L 256.1,250.4 L 255.7,250.3 L 255.3,250.2 L 254.9,250.1 L 254.5,250.1 L 254,250.1 L 253.6,250.1 L 253.1,250.2 L 252.7,250.2 L 252.2,250.4 L 251.8,250.5 L 251.3,250.7 L 250.9,250.9 L 250.4,251.2 L 250,251.5 L 249.6,251.8 L 249.2,252.2 L 248.8,252.6 L 248.4,253 L 248,253.5 L 247.7,254 L 247.4,254.5 L 247.2,255.1 L 246.9,255.7 L 246.8,256.3 L 246.6,256.9 L 246.5,257.6 L 246.4,258.3 L 246.4,259 L 246.4,259.7 L 246.5,260.4 L 246.6,261.2 L 246.8,261.9 L 247.1,262.6 L 247.3,263.4 L 247.7,264.1 L 248.1,264.8 L 248.6,265.5 L 249.1,266.2 L 249.7,266.9 L 250.3,267.5 L 251,268.1 L 251.7,268.7 L 252.5,269.3 L 253.4,269.7 L 254.3,270.2 L 255.2,270.6 L 256.2,270.9 L 257.2,271.1 L 258.3,271.3 L 259.4,271.5 L 260.5,271.5 L 261.7,271.5 L 262.8,271.4 L 264,271.2 L 265.2,270.9 L 266.4,270.6 L 267.6,270.1 L 268.8,269.6 L 270,269 L 271.1,268.3 L 272.3,267.4 L 273.3,266.5 L 274.4,265.5 L 275.4,264.4 L 276.3,263.3 L 277.2,262 L 278,260.7 L 278.8,259.2 L 279.4,257.7 L 279.9,256.1 L 280.4,254.5 L 280.8,252.8 L 281,251 L 281.1,249.2 L 281.1,247.3 L 281,245.5 L 280.7,243.5 L 280.3,241.6 L 279.8,239.7 L 279.1,237.7 L 278.3,235.8 L 277.3,233.9 L 276.2,232 L 274.9,230.2 L 273.5,228.4 L 271.9,226.7 L 270.2,225 L 268.3,223.5 L 266.3,222 L 264.1,220.7 L 261.9,219.5 L 259.4,218.4 L 256.9,217.5 L 254.3,216.7 L 251.6,216.1 L 248.7,215.7 L 245.8,215.4 L 242.8,215.4 L 239.8,215.5 L 236.7,215.9 L 233.6,216.5 L 230.4,217.3 L 227.3,218.3 L 224.2,219.6 L 221.1,221.1 L 218,222.9 L 215,224.9 L 212.1,227.1 L 209.3,229.6 L 206.6,232.3 L 204.1,235.3 L 201.7,238.5 L 199.5,241.9 L 197.4,245.5 L 195.6,249.3 L 194.1,253.4 L 192.7,257.6 L 191.7,262 L 190.9,266.5 L 190.4,271.2 L 190.2,276 L 190.4,280.9 L 190.9,285.9 L 191.7,290.9 L 192.9,296 L 194.5,301.1 L 196.5,306.2 L 198.8,311.2 L 201.6,316.1 L 204.7,321 L 208.2,325.7 L 212.1,330.3 L 216.4,334.7 L 221.1,338.9 L 226.2,342.9 L 231.6,346.5 L 237.4,349.9 L 243.5,352.9 L 250,355.6 L 256.7,357.8 L 263.7,359.7 L 271,361.1 L 278.5,362 L 286.2,362.4 L 294.1,362.3 L 302.2,361.6 L 310.3,360.4 L 318.5,358.6 L 326.7,356.2 L 334.9,353.2 L 343.1,349.6 L 351.2,345.3 L 359.1,340.4 L 366.8,334.9 L 374.3,328.7 L 381.5,322 L 388.4,314.5 L 394.9,306.5 L 400.9,297.9 L 406.5,288.7 L 411.5,278.9 L 416,268.6 L 419.8,257.9 L 423,246.6 L 425.5,234.9 L 427.2,222.9 L 428.1,210.5 L 428.1,197.8 L 427.3,184.9 L 425.6,171.7 L 422.9,158.5 L 419.3,145.2 L 414.7,131.9 L 409.1,118.6 L 402.5,105.6 L 394.9,92.7 L 386.2,80.1"
fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="1.5" stroke-linecap="round"/>
<!-- 48 quantum dots — mathematically placed along golden spiral -->
<!-- OUTER → teal/cyan (small dots) -->
<circle cx="386.2" cy="80.1" r="3" fill="#20E0C0" opacity="0.75"/>
<circle cx="416" cy="135.3" r="3.2" fill="#20E0C0" opacity="0.76"/>
<circle cx="427.8" cy="191.5" r="3.5" fill="#20E0C0" opacity="0.76"/>
<circle cx="423.7" cy="243.9" r="3.7" fill="#28D0D0" opacity="0.77"/>
<circle cx="406.4" cy="288.9" r="3.9" fill="#30C8E8" opacity="0.77"/>
<!-- BLUE band -->
<circle cx="379.5" cy="323.9" r="4.2" fill="#4080FF" opacity="0.78"/>
<circle cx="346.9" cy="347.7" r="4.4" fill="#4080FF" opacity="0.78"/>
<circle cx="312" cy="360.1" r="4.6" fill="#4080FF" opacity="0.79"/>
<circle cx="278.2" cy="362" r="4.9" fill="#4888FF" opacity="0.79"/>
<circle cx="248" cy="354.8" r="5.1" fill="#5070FF" opacity="0.8"/>
<!-- INDIGO band -->
<circle cx="223.3" cy="340.7" r="5.3" fill="#6060F0" opacity="0.8" filter="url(#dot-glow)"/>
<circle cx="205.3" cy="321.9" r="5.6" fill="#6060F0" opacity="0.81" filter="url(#dot-glow)"/>
<circle cx="194.4" cy="300.8" r="5.8" fill="#7050EC" opacity="0.81" filter="url(#dot-glow)"/>
<circle cx="190.3" cy="279.3" r="6" fill="#7848E8" opacity="0.82" filter="url(#dot-glow)"/>
<!-- PURPLE band -->
<circle cx="192.2" cy="259.4" r="6.3" fill="#8838E4" opacity="0.82" filter="url(#dot-glow)"/>
<circle cx="199.1" cy="242.5" r="6.5" fill="#A020E0" opacity="0.83" filter="url(#dot-glow)"/>
<circle cx="209.5" cy="229.4" r="6.7" fill="#A820E0" opacity="0.83" filter="url(#dot-glow)"/>
<circle cx="222.1" cy="220.6" r="7" fill="#B018D8" opacity="0.84" filter="url(#dot-glow)"/>
<circle cx="235.4" cy="216.1" r="7.2" fill="#B818D0" opacity="0.84" filter="url(#dot-glow)"/>
<!-- MAGENTA band -->
<circle cx="248.3" cy="215.6" r="7.4" fill="#C828C0" opacity="0.85" filter="url(#dot-glow)"/>
<circle cx="259.7" cy="218.5" r="7.7" fill="#D030A8" opacity="0.85" filter="url(#dot-glow)"/>
<circle cx="269" cy="224" r="7.9" fill="#D83898" opacity="0.86" filter="url(#dot-glow)"/>
<circle cx="275.7" cy="231.3" r="8.1" fill="#E03888" opacity="0.86" filter="url(#dot-glow)"/>
<circle cx="279.7" cy="239.4" r="8.4" fill="#E84078" opacity="0.87" filter="url(#dot-glow)"/>
<!-- HOT PINK → ORANGE transition -->
<circle cx="281.1" cy="247.6" r="8.6" fill="#F04878" opacity="0.87" filter="url(#dot-glow)"/>
<circle cx="280.2" cy="255.1" r="8.9" fill="#F05068" opacity="0.88" filter="url(#dot-glow)"/>
<circle cx="277.5" cy="261.5" r="9.1" fill="#F05858" opacity="0.88" filter="url(#dot-glow)"/>
<circle cx="273.5" cy="266.4" r="9.3" fill="#F06048" opacity="0.89" filter="url(#dot-glow)"/>
<circle cx="268.6" cy="269.7" r="9.6" fill="#F06838" opacity="0.89" filter="url(#dot-glow)"/>
<!-- ORANGE band -->
<circle cx="263.5" cy="271.3" r="9.8" fill="#FF7818" opacity="0.9" filter="url(#dot-glow)"/>
<circle cx="258.7" cy="271.4" r="10" fill="#FF8418" opacity="0.9" filter="url(#dot-glow)"/>
<circle cx="254.3" cy="270.2" r="10.3" fill="#FF9018" opacity="0.91" filter="url(#dot-glow)"/>
<circle cx="250.9" cy="268" r="10.5" fill="#FF9C18" opacity="0.91" filter="url(#dot-glow)"/>
<circle cx="248.4" cy="265.2" r="10.7" fill="#FFA820" opacity="0.92" filter="url(#dot-glow)"/>
<circle cx="246.9" cy="262.1" r="11" fill="#FFB420" opacity="0.92" filter="url(#dot-glow)"/>
<!-- GOLD band (innermost dots) -->
<circle cx="246.4" cy="259" r="11.2" fill="#FFC028" opacity="0.93" filter="url(#dot-glow)"/>
<circle cx="246.8" cy="256.2" r="11.4" fill="#FFC830" opacity="0.93" filter="url(#dot-glow)"/>
<circle cx="247.9" cy="253.8" r="11.7" fill="#FFD038" opacity="0.94" filter="url(#dot-glow)"/>
<circle cx="249.4" cy="251.9" r="11.9" fill="#FFD840" opacity="0.94" filter="url(#dot-glow)"/>
<circle cx="251.3" cy="250.7" r="12.1" fill="#FFE040" opacity="0.95" filter="url(#dot-glow)"/>
<circle cx="253.2" cy="250.1" r="12.4" fill="#FFE448" opacity="0.95" filter="url(#dot-glow)"/>
<circle cx="255.1" cy="250.1" r="12.6" fill="#FFE850" opacity="0.96" filter="url(#dot-glow)"/>
<circle cx="256.7" cy="250.6" r="12.8" fill="#FFE858" opacity="0.96" filter="url(#dot-glow)"/>
<circle cx="258" cy="251.5" r="13.1" fill="#FFE860" opacity="0.97" filter="url(#dot-glow)"/>
<circle cx="259" cy="252.5" r="13.3" fill="#FFE860" opacity="0.97" filter="url(#dot-glow)"/>
<circle cx="259.5" cy="253.7" r="13.5" fill="#FFE860" opacity="0.98" filter="url(#dot-glow)"/>
<circle cx="259.7" cy="254.9" r="13.8" fill="#FFE860" opacity="0.98" filter="url(#dot-glow)"/>
<circle cx="259.5" cy="256" r="14" fill="#FFE860" opacity="0.99" filter="url(#dot-glow)"/>
<!-- CENTER — white core with glow -->
<circle cx="256" cy="256" r="50" fill="url(#center-gradient)" filter="url(#center-glow)"/>
<circle cx="256" cy="256" r="18" fill="#FFFFFF" filter="url(#center-glow)"/>
<circle cx="256" cy="256" r="10" fill="#FFFFFF"/>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

183
build.ps1
View File

@ -1,183 +0,0 @@
<#
.SYNOPSIS
Bookworm Portable 打包工具 (管理员使用)
.DESCRIPTION
auto-setup.ps1 打包为 Bookworm-Setup.exe (PS2EXE)
gen-authcode.js 打包为 gen-authcode.exe (pkg)
输出到 dist\ 目录
.USAGE
.\build.ps1 # 打包两个
.\build.ps1 -Setup # 只打包用户安装器
.\build.ps1 -Admin # 只打包管理员工具
#>
param(
[switch]$Setup, # 只打 Bookworm-Setup.exe
[switch]$Admin # 只打 gen-authcode.exe
)
$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$DistDir = Join-Path $ScriptDir "dist"
# 默认两个都打
$buildSetup = $Setup -or (-not $Setup -and -not $Admin)
$buildAdmin = $Admin -or (-not $Setup -and -not $Admin)
function Write-Step($msg) {
Write-Host ""
Write-Host " ── $msg" -ForegroundColor Cyan
}
function Write-OK($msg) { Write-Host " [OK] $msg" -ForegroundColor Green }
function Write-Warn($msg) { Write-Host " [!] $msg" -ForegroundColor Yellow }
function Write-Fail($msg) { Write-Host " [!!] $msg" -ForegroundColor Red }
Write-Host ""
Write-Host " +------------------------------------------+" -ForegroundColor Cyan
Write-Host " | Bookworm Portable — Build Script |" -ForegroundColor Cyan
Write-Host " +------------------------------------------+" -ForegroundColor Cyan
if (-not (Test-Path $DistDir)) {
New-Item -ItemType Directory $DistDir | Out-Null
Write-OK "创建 dist\ 目录"
}
# ════════════════════════════════════════════════════════
# 1. Bookworm-Setup.exe (auto-setup.ps1 → PS2EXE)
# ════════════════════════════════════════════════════════
if ($buildSetup) {
Write-Step "打包 Bookworm-Setup.exe (PS2EXE)"
# 安装/检查 PS2EXE
if (-not (Get-Command Invoke-ps2exe -ErrorAction SilentlyContinue)) {
Write-Warn "PS2EXE 未安装,正在安装..."
Install-Module ps2exe -Scope CurrentUser -Force -AllowClobber
Import-Module ps2exe
}
$inputPs1 = Join-Path $ScriptDir "auto-setup.ps1"
$outputExe = Join-Path $DistDir "Bookworm-Setup.exe"
if (-not (Test-Path $inputPs1)) {
Write-Fail "找不到 auto-setup.ps1"
exit 1
}
Write-Host " 输入: $inputPs1" -ForegroundColor Gray
Write-Host " 输出: $outputExe" -ForegroundColor Gray
# 从 auto-setup.ps1 读取版本号
$versionLine = Select-String -Path $inputPs1 -Pattern '^\$BWVersion\s*=\s*"([^"]+)"' | Select-Object -First 1
$bwVer = if ($versionLine) { $versionLine.Matches[0].Groups[1].Value } else { "0.0.0" }
Write-Host " 版本: $bwVer" -ForegroundColor Gray
# 优先用桌面专用 B 圆图标, 回退到 galaxy
$iconFile = Join-Path $ScriptDir "bookworm-desktop.ico"
if (-not (Test-Path $iconFile)) {
$iconFile = Join-Path $ScriptDir "bookworm.ico"
}
$ps2exeArgs = @{
InputFile = $inputPs1
OutputFile = $outputExe
Title = "Bookworm Portable Setup v$bwVer"
Description = "Bookworm Smart Assistant 安装向导 v$bwVer"
Company = "Bookworm"
Version = "$bwVer.0"
NoConsole = $true
NoOutput = $true
NoError = $true
}
if (Test-Path $iconFile) {
$ps2exeArgs.IconFile = $iconFile
Write-Host " 图标: $iconFile" -ForegroundColor Gray
}
Invoke-ps2exe @ps2exeArgs
if (Test-Path $outputExe) {
$sizeKB = [math]::Round((Get-Item $outputExe).Length / 1KB)
Write-OK "Bookworm-Setup.exe 打包完成 (${sizeKB} KB)"
} else {
Write-Fail "Bookworm-Setup.exe 打包失败"
exit 1
}
}
# ════════════════════════════════════════════════════════
# 2. Bookworm-AuthGen.exe (admin-authcode-gui.ps1 → PS2EXE)
# ════════════════════════════════════════════════════════
if ($buildAdmin) {
Write-Step "打包 Bookworm-AuthGen.exe (PS2EXE GUI)"
$inputPs1 = Join-Path $ScriptDir "admin-authcode-gui.ps1"
$outputExe = Join-Path $DistDir "Bookworm-AuthGen.exe"
if (-not (Test-Path $inputPs1)) {
Write-Fail "找不到 admin-authcode-gui.ps1"
exit 1
}
Write-Host " 输入: $inputPs1" -ForegroundColor Gray
Write-Host " 输出: $outputExe" -ForegroundColor Gray
# 优先用书虫学者图标, 回退到 B 圆
$adminIcon = Join-Path $ScriptDir "admin-authcode.ico"
if (-not (Test-Path $adminIcon)) { $adminIcon = Join-Path $ScriptDir "bookworm-desktop.ico" }
$ps2exeArgs = @{
InputFile = $inputPs1
OutputFile = $outputExe
Title = "Bookworm AuthCode Generator"
Description = "Bookworm 授权码生成器 (管理员工具)"
Company = "Bookworm"
Version = "1.5.1.0"
NoConsole = $true
NoOutput = $true
NoError = $true
}
if (Test-Path $adminIcon) {
$ps2exeArgs.IconFile = $adminIcon
Write-Host " 图标: $adminIcon" -ForegroundColor Gray
}
Invoke-ps2exe @ps2exeArgs
if (Test-Path $outputExe) {
$sizeKB = [math]::Round((Get-Item $outputExe).Length / 1KB)
Write-OK "Bookworm-AuthGen.exe 打包完成 (${sizeKB} KB)"
} else {
Write-Fail "Bookworm-AuthGen.exe 打包失败"
exit 1
}
}
# ════════════════════════════════════════════════════════
# 完成
# ════════════════════════════════════════════════════════
Write-Host ""
Write-Host " ============================================" -ForegroundColor Green
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
}
Write-Host ""
Write-Host " 分发说明:" -ForegroundColor Gray
Write-Host " Bookworm-Setup.exe → 用户安装器 (公开下载)" -ForegroundColor Gray
Write-Host " Bookworm-AuthGen.exe → 管理员授权码生成器 (勿对外分发)" -ForegroundColor Gray
Write-Host ""

View File

@ -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 }

View File

@ -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
}

View File

@ -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

View File

@ -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 <password> > secrets.enc
* 解密: node crypto-helper.js decrypt <password> < 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 <password>'); 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 <password> [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 <password> > secrets.enc');
console.error(' decrypt: node crypto-helper.js decrypt <password> < secrets.enc');
console.error(' interactive: node crypto-helper.js decrypt-file secrets.enc');
process.exit(1);
}

View File

@ -1,227 +0,0 @@
#!/bin/bash
# ============================================================
# Bookworm Portable - Gitea 一键部署脚本
# 在阿里云 ECS 上部署 Gitea 私有 Git 服务
# ============================================================
# 用法: ssh root@YOUR_ECS_IP 'bash -s' < deploy-gitea.sh
# 或: scp deploy-gitea.sh root@YOUR_ECS_IP:/tmp/ && ssh root@YOUR_ECS_IP 'bash /tmp/deploy-gitea.sh'
# ============================================================
set -euo pipefail
GITEA_VER="1.22.6"
GITEA_BIN="/usr/local/bin/gitea"
GITEA_USER="git"
GITEA_HOME="/home/git"
GITEA_DATA="/var/lib/gitea"
GITEA_PORT=3300
# ─── 管理员配置 (部署前修改) ──────────────────────────
ADMIN_USER="${GITEA_ADMIN_USER:-bookworm}"
ADMIN_PASS="${GITEA_ADMIN_PASS:-}"
ADMIN_EMAIL="${GITEA_ADMIN_EMAIL:-admin@localhost}"
echo "========================================="
echo " Bookworm Gitea 部署 v1.1"
echo "========================================="
# 0. 管理员密码检查
if [ -z "$ADMIN_PASS" ]; then
echo ""
echo "[!] 请设置管理员密码 (至少 8 位):"
read -rs ADMIN_PASS
if [ ${#ADMIN_PASS} -lt 8 ]; then
echo "[ERROR] 密码至少 8 位"
exit 1
fi
echo ""
fi
# 1. 创建 git 用户
if ! id "$GITEA_USER" &>/dev/null; then
echo "[1/8] 创建 git 用户..."
adduser --system --shell /bin/bash --gecos 'Gitea' \
--group --disabled-password --home "$GITEA_HOME" "$GITEA_USER"
else
echo "[1/8] git 用户已存在,跳过"
fi
# 2. 创建目录结构
echo "[2/8] 创建数据目录..."
mkdir -p "$GITEA_DATA"/{custom,data,log}
chown -R "$GITEA_USER":"$GITEA_USER" "$GITEA_DATA"
chmod -R 750 "$GITEA_DATA"
# 3. 下载 Gitea 二进制 + SHA256 校验
download_and_verify() {
local ver="$1"
local bin="$2"
local base_url="https://dl.gitea.com/gitea/$ver"
local tmp_bin="${bin}.tmp"
local tmp_sha="${bin}.sha256"
echo " 下载 gitea-$ver-linux-amd64..."
wget -q --show-progress -O "$tmp_bin" "$base_url/gitea-$ver-linux-amd64"
echo " 下载 SHA256 校验文件..."
wget -q -O "$tmp_sha" "$base_url/gitea-$ver-linux-amd64.sha256"
echo " 验证完整性..."
# 校验文件格式: hash filename
local expected_hash
expected_hash=$(awk '{print $1}' "$tmp_sha")
local actual_hash
actual_hash=$(sha256sum "$tmp_bin" | awk '{print $1}')
if [ "$expected_hash" != "$actual_hash" ]; then
echo "[ERROR] SHA256 校验失败!"
echo " 期望: $expected_hash"
echo " 实际: $actual_hash"
rm -f "$tmp_bin" "$tmp_sha"
exit 1
fi
echo " [OK] SHA256 校验通过"
mv "$tmp_bin" "$bin"
chmod +x "$bin"
rm -f "$tmp_sha"
}
if [ -f "$GITEA_BIN" ]; then
CURRENT_VER=$($GITEA_BIN --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' || echo "unknown")
echo "[3/8] Gitea 已安装 (v$CURRENT_VER)"
if [ "$CURRENT_VER" = "$GITEA_VER" ]; then
echo " 版本匹配,跳过下载"
else
echo " 升级到 v$GITEA_VER..."
systemctl stop gitea 2>/dev/null || true
download_and_verify "$GITEA_VER" "$GITEA_BIN"
fi
else
echo "[3/8] 下载 Gitea v$GITEA_VER..."
download_and_verify "$GITEA_VER" "$GITEA_BIN"
fi
# 4. 创建 systemd 服务
echo "[4/8] 配置 systemd 服务..."
cat > /etc/systemd/system/gitea.service << 'EOF'
[Unit]
Description=Gitea (Git with a cup of tea)
After=syslog.target network.target
[Service]
RestartSec=2s
Type=simple
User=git
Group=git
WorkingDirectory=/var/lib/gitea
ExecStart=/usr/local/bin/gitea web --config /var/lib/gitea/custom/conf/app.ini
Restart=always
Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea
[Install]
WantedBy=multi-user.target
EOF
# 5. 获取公网 IP (带校验)
echo "[5/8] 检测公网 IP..."
PUBLIC_IP=$(curl -s --max-time 5 ifconfig.me 2>/dev/null || echo "")
if ! echo "$PUBLIC_IP" | grep -qE '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$'; then
PUBLIC_IP="8.138.11.105"
echo " [!] 自动检测失败,使用默认: $PUBLIC_IP"
else
echo " [OK] 公网 IP: $PUBLIC_IP"
fi
# 6. 初始化配置 (如果不存在)
if [ ! -f "$GITEA_DATA/custom/conf/app.ini" ]; then
echo "[6/8] 生成初始配置..."
mkdir -p "$GITEA_DATA/custom/conf"
install -m 600 -o "$GITEA_USER" -g "$GITEA_USER" /dev/null "$GITEA_DATA/custom/conf/app.ini"
cat > "$GITEA_DATA/custom/conf/app.ini" << EOF
[server]
HTTP_PORT = $GITEA_PORT
ROOT_URL = http://$PUBLIC_IP:$GITEA_PORT/
LFS_START_SERVER = true
LFS_JWT_SECRET = $(openssl rand -base64 32)
[database]
DB_TYPE = sqlite3
PATH = $GITEA_DATA/data/gitea.db
[repository]
ROOT = $GITEA_HOME/gitea-repositories
DEFAULT_BRANCH = main
[security]
INSTALL_LOCK = true
SECRET_KEY = $(openssl rand -base64 32)
INTERNAL_TOKEN = $(openssl rand -base64 64 | tr -d '\n')
[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = true
DEFAULT_ALLOW_CREATE_ORGANIZATION = false
ENABLE_CAPTCHA = true
[log]
MODE = file
LEVEL = Info
ROOT_PATH = $GITEA_DATA/log
EOF
chown "$GITEA_USER":"$GITEA_USER" "$GITEA_DATA/custom/conf/app.ini"
chmod 600 "$GITEA_DATA/custom/conf/app.ini"
else
echo "[6/8] 配置已存在,跳过"
fi
# 7. 启动服务
echo "[7/8] 启动 Gitea..."
systemctl daemon-reload
systemctl enable gitea
systemctl restart gitea
# 等待启动
sleep 3
if ! systemctl is-active --quiet gitea; then
echo "[ERROR] Gitea 启动失败,检查日志:"
echo " journalctl -u gitea -n 50"
exit 1
fi
echo " [OK] Gitea 服务已启动"
# 8. 自动创建管理员账号 (消除安装向导窗口期)
echo "[8/8] 创建管理员账号..."
if sudo -u "$GITEA_USER" "$GITEA_BIN" admin user list \
--config "$GITEA_DATA/custom/conf/app.ini" 2>/dev/null | grep -q "$ADMIN_USER"; then
echo " [!] 管理员 $ADMIN_USER 已存在,跳过"
else
sudo -u "$GITEA_USER" "$GITEA_BIN" admin user create \
--config "$GITEA_DATA/custom/conf/app.ini" \
--username "$ADMIN_USER" \
--password "$ADMIN_PASS" \
--email "$ADMIN_EMAIL" \
--admin \
--must-change-password=false
echo " [OK] 管理员 $ADMIN_USER 已创建"
fi
echo ""
echo "========================================="
echo " Gitea 部署成功!"
echo "========================================="
echo ""
echo " 访问地址: http://$PUBLIC_IP:$GITEA_PORT"
echo " 管理员: $ADMIN_USER"
echo " 状态: INSTALL_LOCK=true, 注册已关闭"
echo ""
echo " 下一步:"
echo " 1. 登录 http://$PUBLIC_IP:$GITEA_PORT"
echo " 2. 创建私有仓库: bookworm-config"
echo " 3. 创建私有仓库: bookworm-boot"
echo ""
echo " 安全提醒:"
echo " - 确保阿里云安全组仅允许你的 IP 访问端口 $GITEA_PORT"
echo " - 建议后续配置 HTTPS (Let's Encrypt + Nginx 反代)"
echo " - 建议启用 2FA: 设置 -> 安全 -> 两步验证"
echo "========================================="

View File

@ -1,179 +0,0 @@
#!/usr/bin/env node
/**
* Bookworm tool_use 诊断脚本
* 测试中转站是否正确透传 Anthropic tool_use 协议
* 用法: node diagnose-tooluse.js
*/
const https = require('https');
const http = require('http');
const API_KEY = process.env.ANTHROPIC_API_KEY;
const BASE_URL = process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com';
if (!API_KEY) {
console.error('[FAIL] ANTHROPIC_API_KEY 未设置');
process.exit(1);
}
console.log(`[INFO] 中转站: ${BASE_URL}`);
console.log(`[INFO] API Key: ${API_KEY.substring(0, 8)}...`);
console.log('');
// 测试 1: 非流式 tool_use
async function testToolUse() {
const url = new URL('/v1/messages', BASE_URL);
const isHttps = url.protocol === 'https:';
const lib = isHttps ? https : http;
const body = JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 200,
tools: [{
name: 'get_disk_info',
description: 'Get disk space information',
input_schema: {
type: 'object',
properties: { drive: { type: 'string', description: 'Drive letter' } },
required: ['drive']
}
}],
tool_choice: { type: 'tool', name: 'get_disk_info' },
messages: [{ role: 'user', content: 'Check disk C:' }]
});
return new Promise((resolve, reject) => {
const req = lib.request(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
'anthropic-version': '2023-06-01'
}
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const json = JSON.parse(data);
resolve({ status: res.statusCode, body: json });
} catch (e) {
resolve({ status: res.statusCode, raw: data.substring(0, 500) });
}
});
});
req.on('error', reject);
req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout')); });
req.write(body);
req.end();
});
}
// 测试 2: 流式 tool_use
async function testStreamToolUse() {
const url = new URL('/v1/messages', BASE_URL);
const isHttps = url.protocol === 'https:';
const lib = isHttps ? https : http;
const body = JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 200,
stream: true,
tools: [{
name: 'get_disk_info',
description: 'Get disk space information',
input_schema: {
type: 'object',
properties: { drive: { type: 'string', description: 'Drive letter' } },
required: ['drive']
}
}],
tool_choice: { type: 'tool', name: 'get_disk_info' },
messages: [{ role: 'user', content: 'Check disk C:' }]
});
return new Promise((resolve, reject) => {
const req = lib.request(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
'anthropic-version': '2023-06-01'
}
}, (res) => {
let data = '';
let hasToolUseStart = false;
let hasInputJsonDelta = false;
res.on('data', chunk => {
const text = chunk.toString();
data += text;
if (text.includes('"type":"tool_use"') || text.includes('"type": "tool_use"')) hasToolUseStart = true;
if (text.includes('input_json_delta')) hasInputJsonDelta = true;
});
res.on('end', () => {
resolve({
status: res.statusCode,
hasToolUseStart,
hasInputJsonDelta,
sample: data.substring(0, 800)
});
});
});
req.on('error', reject);
req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout')); });
req.write(body);
req.end();
});
}
(async () => {
// 测试 1: 非流式
console.log('=== 测试 1: 非流式 tool_use ===');
try {
const r = await testToolUse();
if (r.status !== 200) {
console.log(`[FAIL] HTTP ${r.status}`);
console.log(JSON.stringify(r.body || r.raw, null, 2).substring(0, 300));
} else {
const content = r.body?.content || [];
const toolUseBlock = content.find(b => b.type === 'tool_use');
if (toolUseBlock) {
console.log(`[PASS] 收到 tool_use block: name=${toolUseBlock.name}, id=${toolUseBlock.id}`);
console.log(` input: ${JSON.stringify(toolUseBlock.input)}`);
} else {
console.log('[FAIL] 未收到 tool_use block!');
console.log(' content types:', content.map(b => b.type).join(', ') || '(empty)');
console.log(' stop_reason:', r.body?.stop_reason);
if (content[0]?.text) console.log(' text:', content[0].text.substring(0, 200));
}
}
} catch (e) {
console.log(`[FAIL] 请求失败: ${e.message}`);
}
console.log('');
// 测试 2: 流式
console.log('=== 测试 2: 流式 tool_use (Claude Code 实际使用模式) ===');
try {
const r = await testStreamToolUse();
if (r.status !== 200) {
console.log(`[FAIL] HTTP ${r.status}`);
console.log(r.sample?.substring(0, 300));
} else {
console.log(` tool_use block in stream: ${r.hasToolUseStart ? '[PASS]' : '[FAIL]'}`);
console.log(` input_json_delta events: ${r.hasInputJsonDelta ? '[PASS]' : '[FAIL]'}`);
if (!r.hasToolUseStart) {
console.log(' stream sample:', r.sample?.substring(0, 400));
}
}
} catch (e) {
console.log(`[FAIL] 请求失败: ${e.message}`);
}
console.log('');
console.log('=== 诊断结论 ===');
console.log('如果两项测试均 PASS: 中转站 tool_use 正常');
console.log('如果非流式 PASS 流式 FAIL: 中转站流式 SSE 处理有 bug, 联系管理员升级 NewAPI');
console.log('如果均 FAIL: 中转站不支持 Anthropic tool_use, 需开启原生 Anthropic 透传模式');
})();

124
download.html Normal file
View File

@ -0,0 +1,124 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bookworm - 下载安装</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0d1117;
color: #e6edf3;
font-family: -apple-system, 'Segoe UI', 'Microsoft YaHei', sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 2rem;
}
.card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 16px;
padding: 3rem;
max-width: 520px;
width: 100%;
text-align: center;
}
.logo {
font-size: 3rem;
margin-bottom: 1rem;
}
h1 { font-size: 1.8rem; margin-bottom: 0.5rem; }
h1 span { color: #58a6ff; }
.subtitle { color: #8b949e; margin-bottom: 2rem; font-size: 0.95rem; }
.badges {
display: flex; gap: 0.5rem; justify-content: center;
margin-bottom: 2rem; flex-wrap: wrap;
}
.badge {
background: #0d1117; border: 1px solid #30363d; border-radius: 20px;
padding: 0.25rem 0.75rem; font-size: 0.8rem; color: #8b949e;
}
.badge b { color: #e6edf3; }
.btn {
display: inline-block;
background: linear-gradient(135deg, #238636, #2ea043);
color: white;
font-size: 1.1rem;
font-weight: 600;
padding: 0.9rem 2.5rem;
border-radius: 8px;
text-decoration: none;
border: none;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
margin-bottom: 1rem;
}
.btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(35,134,54,0.4); }
.btn:active { transform: translateY(0); }
.size { color: #8b949e; font-size: 0.8rem; margin-bottom: 2rem; }
.steps {
text-align: left;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 8px;
padding: 1.2rem 1.5rem;
margin-top: 1rem;
font-size: 0.9rem;
line-height: 2;
}
.steps .num {
display: inline-block;
background: #58a6ff;
color: #000;
width: 20px; height: 20px;
border-radius: 50%;
text-align: center;
font-size: 0.75rem;
font-weight: 700;
line-height: 20px;
margin-right: 0.5rem;
}
.steps .dim { color: #8b949e; }
.req {
text-align: left;
margin-top: 1.5rem;
font-size: 0.8rem;
color: #8b949e;
}
.req b { color: #d29922; }
</style>
</head>
<body>
<div class="card">
<div class="logo">&#128218;</div>
<h1>Bookworm <span>Portable</span></h1>
<p class="subtitle">AI 编程助手 — 一键安装,即刻使用</p>
<div class="badges">
<span class="badge"><b>97</b> Skills</span>
<span class="badge"><b>18</b> Agents</span>
<span class="badge"><b>28</b> Hooks</span>
<span class="badge"><b>AES-256</b> 加密</span>
</div>
<a class="btn" href="https://code.letcareme.com/bookworm/bookworm-boot/raw/branch/main/Bookworm-Setup.bat" download="Bookworm-Setup.bat">
&#11015; 下载安装程序
</a>
<p class="size">Bookworm-Setup.bat (4 KB) — 双击即可安装</p>
<div class="steps">
<span class="num">1</span> 下载上方 .bat 文件<br>
<span class="num">2</span> 双击运行 <span class="dim">(如提示安全警告,选 "仍要运行")</span><br>
<span class="num">3</span> 输入管理员提供的密码<br>
<span class="num">&#10003;</span> 完成!桌面出现 Bookworm 图标
</div>
<div class="req">
<b>&#9888; 前置要求:</b><br>
&#8226; Node.js (<a href="https://nodejs.org" style="color:#58a6ff">下载</a>) + Git (<a href="https://git-scm.com" style="color:#58a6ff">下载</a>)<br>
&#8226; 代理/VPN 软件 (国内必须,用于首次连接验证)
</div>
</div>
</body>
</html>

View File

@ -1,166 +0,0 @@
#!/usr/bin/env node
'use strict';
/**
* Bookworm 授权码生成工具 (管理员使用)
*
* 用法:
* node gen-authcode.js <days> [选项]
*
* 选项:
* --relay-key, -k <key> 中转站限额子 Key (替换 ANTHROPIC_API_KEY)
* --user, -u <name> 用户标识 (仅用于显示, 不影响加密)
* [secrets.txt路径] 明文凭证文件 (默认: ./secrets.txt)
*
* 示例:
* node gen-authcode.js 30 # 共享 Key (单用户)
* node gen-authcode.js 30 --relay-key sk-relay-xxx # 独立限额 Key
* node gen-authcode.js 90 -k sk-relay-xxx -u alice # 指定用户名
*
* 原理:
* 1. 生成随机 24位Hex Token (96bit )
* 2. Token 前8位 = 文件 ID 输出 secrets-XXXXXXXX.enc (多用户模式)
* --relay-key 输出 secrets.enc (单用户/共享模式)
* 3. 授权码 BW-YYYYMMDD-TOKEN (发给用户)
* 4. 安全说明: Token = 解密密钥, YYYYMMDD = 客户端到期校验
*/
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const ALGO = 'aes-256-cbc';
const ITERATIONS = 600000;
const DIGEST = 'sha256';
const SALT_LEN = 16;
const KEY_LEN = 32;
const IV_LEN = 16;
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()]);
return Buffer.concat([Buffer.from('BWENC1'), salt, encrypted]);
}
// ─── 解析 CLI 参数 ───────────────────────────────────────
const rawArgs = process.argv.slice(2);
const DAYS = parseInt(rawArgs[0]);
if (!DAYS || DAYS < 1 || DAYS > 3650) {
console.error('用法: node gen-authcode.js <有效天数> [选项]');
console.error('选项:');
console.error(' --relay-key, -k <key> 中转站限额子 Key');
console.error(' --user, -u <name> 用户标识 (仅显示)');
console.error(' [secrets.txt路径] 默认: ./secrets.txt');
console.error('');
console.error('示例:');
console.error(' node gen-authcode.js 30 # 共享模式');
console.error(' node gen-authcode.js 30 -k sk-relay-xxx -u alice # 独立限额');
process.exit(1);
}
let relayKey = null;
let userName = null;
let secretsTxtArg = null;
for (let i = 1; i < rawArgs.length; i++) {
const a = rawArgs[i];
if (a === '--relay-key' || a === '-k') { relayKey = rawArgs[++i]; }
else if (a === '--user' || a === '-u') { userName = rawArgs[++i]; }
else if (!a.startsWith('-')) { secretsTxtArg = a; }
}
// pkg 打包后 __filename 指向虚拟快照路径,需用 process.execPath 获取真实目录
const SCRIPT_DIR = path.dirname(
typeof process.pkg !== 'undefined' ? process.execPath : path.resolve(__filename)
);
const SECRETS_TXT = secretsTxtArg || path.join(SCRIPT_DIR, 'secrets.txt');
if (!fs.existsSync(SECRETS_TXT)) {
console.error(`[错误] 找不到 secrets.txt: ${SECRETS_TXT}`);
console.error('请先创建 secrets.txt, 每行格式: KEY=VALUE');
process.exit(1);
}
if (relayKey && !/^[A-Za-z0-9\-_.]+$/.test(relayKey)) {
console.error('[错误] --relay-key 格式不合法');
process.exit(1);
}
// ─── 生成到期日 ──────────────────────────────────────────
const expiry = new Date();
expiry.setDate(expiry.getDate() + DAYS);
const pad = n => String(n).padStart(2, '0');
const expiryStr = `${expiry.getFullYear()}${pad(expiry.getMonth()+1)}${pad(expiry.getDate())}`;
const expiryDisplay = `${expiryStr.slice(0,4)}-${expiryStr.slice(4,6)}-${expiryStr.slice(6,8)}`;
// ─── 生成随机 Token ──────────────────────────────────────
const token = crypto.randomBytes(12).toString('hex'); // 小写 24位
const authCode = `BW-${expiryStr}-${token.toUpperCase()}`;
const fileId = token.slice(0, 8); // 前8位作为文件 ID
// ─── 构建待加密内容 ──────────────────────────────────────
let secretsPlain = fs.readFileSync(SECRETS_TXT, 'utf8').trim();
const multiUser = !!relayKey;
if (multiUser) {
// 用中转站 relay key 替换 ANTHROPIC_API_KEY
if (/^ANTHROPIC_API_KEY=/m.test(secretsPlain)) {
secretsPlain = secretsPlain.replace(
/^ANTHROPIC_API_KEY=.*/m,
`ANTHROPIC_API_KEY=${relayKey}`
);
} else {
secretsPlain = `ANTHROPIC_API_KEY=${relayKey}\n${secretsPlain}`;
}
}
// ─── 加密并写出 ──────────────────────────────────────────
const encBuffer = encrypt(secretsPlain, token);
let outFileName, outFilePath;
if (multiUser) {
outFileName = `secrets-${fileId}.enc`;
} else {
outFileName = 'secrets.enc';
}
outFilePath = path.join(SCRIPT_DIR, outFileName);
fs.writeFileSync(outFilePath, encBuffer);
// ─── 输出 ────────────────────────────────────────────────
const modeLabel = multiUser
? `多用户独立 Key (${userName || '未命名'})`
: '共享 Key (单/多用户共享)';
console.log('\n═══════════════════════════════════════════════════');
console.log(' Bookworm 授权码生成完毕');
console.log('═══════════════════════════════════════════════════');
console.log('');
console.log(` 模式: ${modeLabel}`);
if (userName) console.log(` 用户: ${userName}`);
console.log(` 授权码: ${authCode}`);
console.log(` 有效期: ${DAYS} 天 (至 ${expiryDisplay})`);
if (multiUser) {
console.log(` Relay Key: ${relayKey.slice(0,12)}... (已替换 ANTHROPIC_API_KEY)`);
console.log(` 文件 ID: ${fileId}${outFileName}`);
}
console.log('');
console.log(' ▶ 操作步骤:');
console.log(` 1. 将授权码发给用户: ${authCode}`);
console.log(` 2. 推送 ${outFileName} 到 Gitea:`);
console.log(` git add ${outFileName} && git commit -m "add user ${userName || fileId}" && git push`);
console.log('');
console.log(' ⚠ 安全提醒:');
console.log(' - 授权码即解密密钥, 请勿通过不安全渠道明文发送');
console.log(` - 到期日 (${expiryDisplay}) 后自动失效`);
if (multiUser) {
console.log(` - 各用户 secrets-XXXXXXXX.enc 独立, 轮换互不影响`);
}
console.log('═══════════════════════════════════════════════════\n');

View File

@ -1,51 +0,0 @@
<#
.SYNOPSIS
生成 integrity.sha256 文件
.DESCRIPTION
计算 hooks 和关键配置文件的 SHA256 哈希,
写入 integrity.sha256 install.ps1 校验.
每次 prepare-repo.ps1 推送前应运行此脚本.
.USAGE
.\generate-integrity.ps1
#>
$ClaudeDir = Join-Path $env:USERPROFILE ".claude"
$OutputFile = Join-Path $ClaudeDir "integrity.sha256"
if (-not (Test-Path $ClaudeDir)) {
Write-Host "[ERROR] .claude 目录不存在" -ForegroundColor Red
exit 1
}
Write-Host " 生成 integrity.sha256..." -ForegroundColor Cyan
# 需要校验的关键文件模式
$patterns = @(
"hooks/*.js",
"hooks/lib/*.js",
"scripts/paths.config.js",
"CLAUDE.md",
"settings.json",
"settings.local.template.json"
)
$hashes = @()
foreach ($pattern in $patterns) {
$fullPattern = Join-Path $ClaudeDir $pattern
foreach ($file in (Get-Item $fullPattern -ErrorAction SilentlyContinue)) {
$hash = (Get-FileHash $file.FullName -Algorithm SHA256).Hash.ToLower()
$relPath = $file.FullName.Substring($ClaudeDir.Length + 1).Replace('\', '/')
$hashes += "$hash $relPath"
}
}
# 去重并排序
$hashes = $hashes | Sort-Object -Unique
$hashes | Set-Content $OutputFile -Encoding UTF8
$count = $hashes.Count
Write-Host " [OK] 已生成 $count 个文件哈希" -ForegroundColor Green
Write-Host " 路径: $OutputFile" -ForegroundColor Gray
Write-Host ""
Write-Host " 注意: 请在 git commit 前运行此脚本" -ForegroundColor Yellow

File diff suppressed because it is too large Load Diff

780
guide.html Normal file
View File

@ -0,0 +1,780 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bookworm Portable - 保姆式安装手册</title>
<style>
:root {
--bg: #0d1117;
--card: #161b22;
--border: #30363d;
--text: #e6edf3;
--text-dim: #8b949e;
--accent: #58a6ff;
--green: #3fb950;
--yellow: #d29922;
--red: #f85149;
--purple: #bc8cff;
--cyan: #39d2c0;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: -apple-system, 'Segoe UI', 'Microsoft YaHei', sans-serif;
line-height: 1.7;
padding: 0;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
.header {
background: linear-gradient(135deg, #1a1f35 0%, #0d1117 100%);
border-bottom: 1px solid var(--border);
padding: 3rem 2rem;
text-align: center;
}
.header pre {
color: var(--cyan);
font-size: 0.65rem;
line-height: 1.2;
margin-bottom: 1rem;
display: inline-block;
text-align: left;
}
.header h1 { font-size: 2rem; font-weight: 700; margin-bottom: 0.5rem; }
.header h1 span { color: var(--accent); }
.header p { color: var(--text-dim); font-size: 1.05rem; }
.badge-row { display: flex; gap: 0.8rem; justify-content: center; margin-top: 1rem; flex-wrap: wrap; }
.badge {
display: inline-flex; align-items: center; gap: 0.4rem;
background: var(--card); border: 1px solid var(--border);
border-radius: 20px; padding: 0.3rem 0.9rem; font-size: 0.85rem; color: var(--text-dim);
}
.badge strong { color: var(--text); }
.container { max-width: 900px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
.section { margin-bottom: 2.5rem; }
.section h2 {
font-size: 1.4rem; margin-bottom: 1rem; padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 0.6rem;
}
.section h2 .num {
background: var(--accent); color: #000; width: 28px; height: 28px;
border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
font-size: 0.85rem; font-weight: 700; flex-shrink: 0;
}
.code-block {
background: #0d1117; border: 1px solid var(--border); border-radius: 8px;
padding: 1rem 1.2rem; margin: 0.8rem 0; overflow-x: auto; position: relative;
cursor: pointer; transition: border-color 0.2s;
}
.code-block:hover { border-color: var(--accent); }
.code-block::after {
content: "点击复制"; position: absolute; top: 0.4rem; right: 0.6rem;
font-size: 0.7rem; color: var(--text-dim); background: var(--card);
padding: 0.15rem 0.5rem; border-radius: 4px; border: 1px solid var(--border);
opacity: 0; transition: opacity 0.2s;
}
.code-block:hover::after { opacity: 1; }
.code-block.copied::after { content: "已复制!"; color: var(--green); opacity: 1; }
.code-block code {
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
font-size: 0.9rem; line-height: 1.6; color: var(--text);
white-space: pre-wrap; word-break: break-all;
}
.code-block .label {
position: absolute; top: 0.4rem; left: 0.6rem;
font-size: 0.65rem; color: var(--text-dim); background: var(--card);
padding: 0.1rem 0.5rem; border-radius: 4px; border: 1px solid var(--border);
text-transform: uppercase; letter-spacing: 0.5px;
}
.code-block.has-label { padding-top: 2rem; }
.cmd { color: var(--green); }
.flag { color: var(--yellow); }
.url { color: var(--accent); }
.comment { color: var(--text-dim); }
.card {
background: var(--card); border: 1px solid var(--border);
border-radius: 10px; padding: 1.2rem 1.5rem; margin: 0.8rem 0;
}
.card h3 { font-size: 1rem; margin-bottom: 0.5rem; color: var(--accent); }
.card p { color: var(--text-dim); font-size: 0.95rem; }
.step { display: flex; gap: 1rem; margin: 1.2rem 0; align-items: flex-start; }
.step-icon {
width: 40px; height: 40px; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 1.2rem; font-weight: 700; flex-shrink: 0; margin-top: 0.1rem;
}
.step-icon.green { background: rgba(63,185,80,0.15); border: 1px solid rgba(63,185,80,0.3); color: var(--green); }
.step-icon.blue { background: rgba(88,166,255,0.15); border: 1px solid rgba(88,166,255,0.3); color: var(--accent); }
.step-icon.yellow { background: rgba(210,153,34,0.15); border: 1px solid rgba(210,153,34,0.3); color: var(--yellow); }
.step-icon.red { background: rgba(248,81,73,0.15); border: 1px solid rgba(248,81,73,0.3); color: var(--red); }
.step-icon.purple { background: rgba(188,140,255,0.15); border: 1px solid rgba(188,140,255,0.3); color: var(--purple); }
.step-content { flex: 1; }
.step-content h4 { font-size: 1.05rem; margin-bottom: 0.3rem; }
.step-content p { color: var(--text-dim); font-size: 0.9rem; }
table { width: 100%; border-collapse: collapse; margin: 0.8rem 0; font-size: 0.9rem; }
th, td { padding: 0.6rem 1rem; text-align: left; border-bottom: 1px solid var(--border); }
th { color: var(--text-dim); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.5px; }
td code { background: var(--bg); padding: 0.15rem 0.4rem; border-radius: 4px; font-size: 0.85rem; border: 1px solid var(--border); }
.alert {
border-radius: 8px; padding: 1rem 1.2rem; margin: 1rem 0;
font-size: 0.9rem; display: flex; gap: 0.8rem; align-items: flex-start;
}
.alert-icon { font-size: 1.2rem; flex-shrink: 0; line-height: 1.5; }
.alert.warning { background: rgba(210,153,34,0.08); border: 1px solid rgba(210,153,34,0.3); }
.alert.danger { background: rgba(248,81,73,0.08); border: 1px solid rgba(248,81,73,0.3); }
.alert.info { background: rgba(88,166,255,0.08); border: 1px solid rgba(88,166,255,0.3); }
.alert.success { background: rgba(63,185,80,0.08); border: 1px solid rgba(63,185,80,0.3); }
.checklist { list-style: none; padding: 0; }
.checklist li {
padding: 0.5rem 0; padding-left: 2rem; position: relative;
border-bottom: 1px solid rgba(48,54,61,0.5); color: var(--text-dim);
}
.checklist li::before {
content: ""; position: absolute; left: 0; top: 0.65rem;
width: 18px; height: 18px; border: 2px solid var(--border); border-radius: 4px;
}
.checklist li.done::before {
background: var(--green); border-color: var(--green);
content: ""; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E");
background-size: 14px; background-position: center; background-repeat: no-repeat;
}
.checklist li.done { color: var(--text); }
.checklist li strong { color: var(--text); }
.flow {
display: flex; align-items: center; justify-content: center;
gap: 0; margin: 1.5rem 0; flex-wrap: wrap;
}
.flow-node {
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
padding: 0.6rem 1rem; font-size: 0.85rem; text-align: center; min-width: 100px;
}
.flow-node.active { border-color: var(--green); box-shadow: 0 0 8px rgba(63,185,80,0.2); }
.flow-arrow { color: var(--text-dim); font-size: 1.2rem; padding: 0 0.3rem; }
.screenshot-note {
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
padding: 0.8rem 1rem; margin: 0.5rem 0; font-size: 0.85rem; color: var(--text-dim);
border-left: 3px solid var(--yellow);
}
.screenshot-note strong { color: var(--yellow); }
.footer {
text-align: center; padding: 2rem; color: var(--text-dim);
font-size: 0.8rem; border-top: 1px solid var(--border);
}
@media (max-width: 600px) {
.header { padding: 2rem 1rem; }
.header pre { font-size: 0.5rem; }
.header h1 { font-size: 1.5rem; }
.container { padding: 1.5rem 1rem; }
.flow { flex-direction: column; }
.flow-arrow { transform: rotate(90deg); }
.step { flex-direction: column; gap: 0.5rem; }
}
</style>
</head>
<body>
<div class="header">
<pre>
____ _
| __ ) ___ ___ | | ____ _____ _ __ _ __ ___
| _ \ / _ \ / _ \| |/ /\ \ /\ / / _ \| '__| '_ ` _ \
| |_) | (_) | (_) | < \ V V / (_) | | | | | | | |
|____/ \___/ \___/|_|\_\ \_/\_/ \___/|_| |_| |_| |_|
</pre>
<h1>Bookworm <span>Portable</span> 保姆式安装手册</h1>
<p>从零开始,一步步教你在任意 Windows 电脑上激活 Bookworm</p>
<div class="badge-row">
<span class="badge"><strong>92</strong> Skills</span>
<span class="badge"><strong>18</strong> Agents</span>
<span class="badge"><strong>29</strong> Hooks</span>
<span class="badge"><strong>AES-256</strong> 加密</span>
<span class="badge"><strong>HTTPS</strong> 传输</span>
</div>
<a href="/Bookworm-Setup.bat" download style="display:inline-block;margin-top:1.2rem;padding:0.7rem 2rem;background:var(--accent);color:#000;font-weight:700;border-radius:8px;font-size:1rem;text-decoration:none;transition:opacity 0.2s" onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">&#11015; 下载一键安装器</a>
</div>
<div class="container">
<!-- 整体流程 -->
<div class="section">
<h2>整体流程概览</h2>
<div class="card" style="text-align:center;border-color:var(--green);margin-bottom:1.5rem">
<h3 style="color:var(--green);margin-bottom:0.5rem">最快方式:一键安装器</h3>
<p style="color:var(--text-dim);font-size:0.9rem;margin-bottom:0.8rem">获取 <strong>Bookworm-Setup.bat</strong> (4KB) &#8594; 双击运行 &#8594; 输入密码 &#8594; 完成</p>
<p style="font-size:0.8rem;color:var(--text-dim)">安装器自动检测依赖、下载配置、创建桌面快捷方式、启动 Claude Code</p>
</div>
<p style="text-align:center;color:var(--text-dim);font-size:0.85rem;margin-bottom:0.5rem">手动安装流程:</p>
<div class="flow">
<div class="flow-node">安装依赖<br><small style="color:var(--text-dim)">Node.js + Git</small></div>
<span class="flow-arrow">&#10132;</span>
<div class="flow-node">安装 Claude Code<br><small style="color:var(--text-dim)">npm 全局安装</small></div>
<span class="flow-arrow">&#10132;</span>
<div class="flow-node">双击安装器<br><small style="color:var(--text-dim)">或 git clone</small></div>
<span class="flow-arrow">&#10132;</span>
<div class="flow-node">输入密码<br><small style="color:var(--text-dim)">主密码</small></div>
<span class="flow-arrow">&#10132;</span>
<div class="flow-node active">开始使用<br><small style="color:var(--green)">Bookworm 激活</small></div>
</div>
<p style="text-align:center;color:var(--text-dim);font-size:0.9rem">首次安装约 10 分钟(含依赖下载),之后每次双击启动约 10-30 秒</p>
</div>
<!-- ============================================================ -->
<!-- 第一步:安装依赖 -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">1</span>安装依赖软件</h2>
<div class="alert danger">
<span class="alert-icon">&#9888;</span>
<div>
<strong>国内必须:代理/VPN 软件</strong><br>
Claude Code 启动时会检查 <code>api.anthropic.com</code>,国内无法直连。<br>
请先安装并启动代理软件Clash / V2Ray / 快柠檬 / 任意 VPN安装脚本会<strong>自动检测</strong>系统代理。<br>
无代理 = Claude Code 无法启动。
</div>
</div>
<div class="alert info">
<span class="alert-icon">&#128161;</span>
<div>
<strong>中转站不走代理</strong><br>
API 中转站 <code>bww.letcareme.com</code> 部署在国内阿里云,<strong>不需要通过代理访问</strong><br>
安装脚本已自动设置 <code>NO_PROXY=bww.letcareme.com,code.letcareme.com</code>,无需手动配置。<br>
如果代理软件有"绕过规则"设置,建议把 <code>*.letcareme.com</code> 加入直连列表。
</div>
</div>
<p>需要安装以下软件。如果已装过可跳到下一步。</p>
<!-- Node.js -->
<div class="step">
<div class="step-icon blue">A</div>
<div class="step-content">
<h4>安装 Node.js必须</h4>
<p>去官网下载 LTS 版本安装包,双击安装,一路 Next 即可。</p>
</div>
</div>
<div class="card">
<h3>方式一:官网下载(推荐)</h3>
<p>打开浏览器访问 <a href="https://nodejs.org/zh-cn" target="_blank">https://nodejs.org</a>,点击绿色的 <strong>"LTS 推荐"</strong> 按钮下载,双击 .msi 文件安装,全部默认 Next。</p>
</div>
<div class="card">
<h3>方式二PowerShell 命令安装</h3>
<p>右键开始菜单 → 选择 <strong>"PowerShell (管理员)"</strong><strong>"终端 (管理员)"</strong>,然后执行:</p>
</div>
<div class="code-block has-label" onclick="copyCode(this)">
<span class="label">PowerShell (管理员)</span>
<code><span class="comment"># 下载 Node.js 安装包</span>
Invoke-WebRequest -Uri "https://nodejs.org/dist/v22.15.0/node-v22.15.0-x64.msi" -OutFile "$env:TEMP\node-install.msi"
<span class="comment"># 运行安装(会弹出安装向导,一路 Next</span>
Start-Process msiexec.exe -ArgumentList "/i $env:TEMP\node-install.msi" -Wait</code>
</div>
<div class="alert warning">
<span class="alert-icon">&#9888;</span>
<div>
<strong>安装完成后必须重开 PowerShell</strong><br>
关闭当前 PowerShell 窗口,重新打开一个新的,否则 <code>node</code><code>npm</code> 命令找不到。
</div>
</div>
<p style="margin-top:0.8rem">验证安装成功:</p>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">node</span> -v <span class="comment"># 应显示 v22.x.x</span>
<span class="cmd">npm</span> -v <span class="comment"># 应显示 10.x.x</span></code>
</div>
<div class="screenshot-note">
<strong>如果 npm 报 "执行策略" 错误?</strong> 执行以下命令后重试:<br>
<code>Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned</code><br>
提示确认时输入 <strong>Y</strong> 回车。
</div>
<!-- Git -->
<div class="step" style="margin-top:1.5rem">
<div class="step-icon blue">B</div>
<div class="step-content">
<h4>安装 Git必须</h4>
<p>去官网下载安装,全部默认设置即可。</p>
</div>
</div>
<div class="card">
<p>打开 <a href="https://git-scm.com/download/win" target="_blank">https://git-scm.com/download/win</a>,下载 <strong>"64-bit Git for Windows Setup"</strong>,双击安装,全部 Next。</p>
</div>
<p>验证:</p>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">git</span> --version <span class="comment"># 应显示 git version 2.x.x</span></code>
</div>
<!-- PowerShell 7 -->
<div class="step" style="margin-top:1.5rem">
<div class="step-icon yellow">C</div>
<div class="step-content">
<h4>安装 PowerShell 7推荐</h4>
<p>Windows 自带的 PowerShell 5.1 有中文兼容问题,建议升级到 7。</p>
</div>
</div>
<div class="code-block has-label" onclick="copyCode(this)">
<span class="label">PowerShell (管理员)</span>
<code><span class="cmd">winget</span> install Microsoft.PowerShell</code>
</div>
<p style="color:var(--text-dim);font-size:0.85rem">如果没有 winget<a href="https://github.com/PowerShell/PowerShell/releases" target="_blank">GitHub Releases</a> 下载 .msi 安装包。安装后用 <code>pwsh</code> 命令启动新版 PowerShell。</p>
</div>
<!-- ============================================================ -->
<!-- 第二步:安装 Claude Code -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">2</span>安装 Claude Code</h2>
<div class="step">
<div class="step-icon green">1</div>
<div class="step-content">
<h4>全局安装</h4>
<p>在 PowerShell 中执行(不需要管理员权限):</p>
</div>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">npm</span> i -g @anthropic-ai/claude-code</code>
</div>
<p style="color:var(--text-dim);font-size:0.85rem">安装过程需要几分钟,等待完成即可。</p>
<div class="step">
<div class="step-icon green">2</div>
<div class="step-content">
<h4>验证安装</h4>
</div>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">claude</span> --version <span class="comment"># 应显示版本号</span></code>
</div>
<div class="alert info">
<span class="alert-icon">&#128161;</span>
<div>
<strong>不需要登录 Claude 账号!</strong><br>
Bookworm 使用中转站 API安装 Claude Code 后直接进入下一步,不用执行 <code>claude login</code>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- 第三步:安装 Bookworm -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">3</span>安装 Bookworm核心步骤</h2>
<div class="step">
<div class="step-icon purple">1</div>
<div class="step-content">
<h4>克隆引导仓库</h4>
<p>在 PowerShell 中执行以下命令。系统会提示输入用户名和密码。</p>
</div>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">git clone</span> <span class="url">https://code.letcareme.com/bookworm/bookworm-boot.git</span>
<span class="cmd">cd</span> bookworm-boot</code>
</div>
<div class="screenshot-note">
<strong>弹出用户名密码?</strong> 输入管理员提供给你的 Gitea 账号密码。这是 Gitea 的密码,不是主密码。
</div>
<div class="step">
<div class="step-icon purple">2</div>
<div class="step-content">
<h4>双击运行安装脚本</h4>
<p>双击文件夹里的 <strong>更新并启动Bookworm.bat</strong>,脚本会自动完成所有配置。</p>
</div>
</div>
<div class="card">
<h3>或者用命令行运行</h3>
<p>如果双击 .bat 不起作用,在 PowerShell 中手动执行:</p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">pwsh</span> <span class="flag">-ExecutionPolicy Bypass</span> -File install.ps1</code>
</div>
<p style="color:var(--text-dim);font-size:0.85rem">没有 pwsh 可用 <code>powershell</code> 替代。</p>
<div class="step">
<div class="step-icon purple">3</div>
<div class="step-content">
<h4>输入主密码</h4>
<p>脚本会提示 <strong>"输入主密码解密凭证"</strong>,输入管理员提供的<strong>主密码</strong>(不是 Gitea 密码),按回车。</p>
<p>密码输入时不显示字符,这是正常的。<strong>输错了可以重试,最多 3 次。</strong></p>
</div>
</div>
<div class="screenshot-note">
<strong>两个密码不要搞混:</strong><br>
<strong>Gitea 密码</strong> = 克隆仓库时输入的,用于下载文件<br>
<strong>主密码</strong> = 解密 API 凭证时输入的,用于启动 Claude Code
</div>
<div class="step">
<div class="step-icon green">4</div>
<div class="step-content">
<h4>等待完成</h4>
<p>脚本会显示步骤进度 [1/9] 到 [9/9],自动完成:</p>
<ul style="color:var(--text-dim);font-size:0.9rem;padding-left:1.2rem;margin-top:0.3rem">
<li>[1/9] 前置检查 (Claude Code / Node.js / Git)</li>
<li>[2/9] 自动检测代理 + 设置 NO_PROXY</li>
<li>[3/9] 解密凭证 (输入主密码)</li>
<li>[4/9] 同步配置 (下载 92 个 Skills)</li>
<li>[5/9] 完整性校验 (SHA256 哈希验证)</li>
<li>[6/9] 渲染配置模板</li>
<li>[7/9] 初始化本地目录</li>
<li>[8/9] Bookworm 系统验证 + MCP 检查</li>
<li>[9/9] 启动 Claude Code</li>
</ul>
</div>
</div>
<div class="alert success">
<span class="alert-icon">&#10003;</span>
<div>
<strong>看到 "Bookworm 就绪" 绿色横幅就说明成功了!</strong><br>
Claude Code 启动后,脚本会验证 Skills/Hooks/配置 完整性,全部 [OK] 后进入 Bookworm 模式。<br>
所有 API 请求通过中转站转发,不需要自己的 Claude 账号。
</div>
</div>
<div class="alert warning">
<span class="alert-icon">&#9888;</span>
<div>
<strong>看到 "原生模式启动" 黄色横幅?</strong><br>
说明 Bookworm 配置不完整。请不加 -StartOnly 重新运行安装脚本,或联系管理员。
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- 日常使用 -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">4</span>日常使用</h2>
<div class="card">
<h3>方法一:双击 .bat 文件(推荐,最简单)</h3>
<p>bookworm-boot 文件夹里有两个 .bat 文件:</p>
</div>
<table>
<tr><th>文件</th><th>作用</th><th>适用场景</th></tr>
<tr>
<td><strong>启动Bookworm.bat</strong></td>
<td>快速启动,不更新配置</td>
<td>每天日常使用</td>
</tr>
<tr>
<td><strong>更新并启动Bookworm.bat</strong></td>
<td>先同步最新 Skills 再启动</td>
<td>管理员通知有更新时</td>
</tr>
</table>
<p style="color:var(--text-dim);font-size:0.85rem;margin-top:0.5rem">双击即可,无需打开 PowerShell无需记命令。脚本会自动检测 PowerShell 7/5.1。</p>
<div class="alert info">
<span class="alert-icon">&#128161;</span>
<div>
<strong>启动时显示 "有 N 个新更新可用"</strong><br>
说明管理员更新了 Skills 或 Hooks。双击 <strong>更新并启动Bookworm.bat</strong> 即可同步。
</div>
</div>
<div class="card" style="margin-top:1rem">
<h3>方法二:命令行(备用)</h3>
<p>如果 .bat 文件无法运行,在 PowerShell 中手动执行:</p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="comment"># 快速启动</span>
<span class="cmd">cd</span> bookworm-boot
<span class="cmd">pwsh</span> <span class="flag">-ExecutionPolicy Bypass</span> -File install.ps1 <span class="flag">-StartOnly</span>
<span class="comment"># 同步更新后启动</span>
<span class="cmd">cd</span> bookworm-boot
<span class="cmd">pwsh</span> <span class="flag">-ExecutionPolicy Bypass</span> -File install.ps1</code>
</div>
</div>
<!-- ============================================================ -->
<!-- 使用完毕 -->
<!-- ============================================================ -->
<!-- ============================================================ -->
<!-- 密码说明 -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">5</span>密码说明</h2>
<div class="card">
<h3>本系统有两个密码,不要搞混</h3>
<table>
<tr><th>名称</th><th>用途</th><th>何时输入</th></tr>
<tr><td><strong>Gitea 密码</strong></td><td>下载文件(克隆仓库)</td><td>首次安装时 git 弹出要求</td></tr>
<tr><td><strong>主密码</strong></td><td>解密 API 凭证</td><td>每次启动脚本提示输入</td></tr>
</table>
</div>
<div class="alert info">
<span class="alert-icon">&#128161;</span>
<div>
<strong>密码输错了?</strong> 最多可以重试 3 次不用紧张。3 次都错才会退出。
</div>
</div>
<div class="card" style="margin-top:0.8rem">
<h3>本日免密功能</h3>
<p>首次解密成功后,脚本会询问 <strong>"今日内免密启动? (y/n)"</strong></p>
<p><strong>y</strong> 后,当天再次启动无需输入主密码,次日自动过期。</p>
<p style="color:var(--text-dim);font-size:0.85rem;margin-top:0.3rem">凭证缓存在 Windows Credential Manager 中DPAPI 加密,仅当前用户可读)。</p>
</div>
<div class="alert warning" style="margin-top:0.8rem">
<span class="alert-icon">&#128274;</span>
<div><strong>主密码无法找回</strong> &#8212; 忘记后联系管理员重新生成 secrets.enc。</div>
</div>
</div>
<!-- ============================================================ -->
<!-- 使用完毕 -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">6</span>使用完毕 &mdash; 清理 / 卸载</h2>
<div class="card" style="border-color:var(--green)">
<h3 style="color:var(--green)">最简单:双击 卸载Bookworm.bat</h3>
<p>bookworm-boot 文件夹里的 <strong>卸载Bookworm.bat</strong>,双击即可一键完整卸载:终止进程 + 清除凭证 + 恢复原始配置 + 删除桌面快捷方式。</p>
</div>
<p style="margin-top:1rem;color:var(--text-dim);font-size:0.9rem">或者用命令行精细控制:</p>
<table>
<tr><th>场景</th><th>命令</th><th>说明</th></tr>
<tr>
<td><strong>基础清理</strong></td>
<td><code>pwsh -File stop.ps1</code></td>
<td>清除环境变量,保留配置供下次快速启动</td>
</tr>
<tr>
<td><strong>完整恢复</strong></td>
<td><code>pwsh -File stop.ps1 -Restore</code></td>
<td>删除 Bookworm恢复电脑原始状态</td>
</tr>
<tr>
<td><strong>深度清理</strong></td>
<td><code>pwsh -File stop.ps1 -Restore -Deep</code></td>
<td>完整恢复 + 清除历史 + 清除 Git/凭证缓存</td>
</tr>
</table>
<div class="alert danger">
<span class="alert-icon">&#9888;</span>
<div>
<strong>在他人电脑/公用电脑上务必卸载:</strong><br>
双击 <strong>卸载Bookworm.bat</strong> 或执行 <code>pwsh -File stop.ps1 -Restore -Deep</code>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- 故障排查 -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">!</span>常见问题排查</h2>
<div class="card">
<h3>&#10060; 输入 node -v 或 npm -v 提示 "无法识别"</h3>
<p><strong>原因:</strong>安装 Node.js 后没有重开 PowerShell 窗口PATH 没刷新。</p>
<p><strong>解决:</strong>关闭当前 PowerShell重新打开一个新的窗口再试。</p>
<p>如果还不行,手动刷新 PATH</p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code>$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
<span class="cmd">node</span> -v</code>
</div>
<div class="card">
<h3>&#10060; npm 报 "执行策略" / "Execution Policy" 错误</h3>
<p><strong>原因:</strong>Windows 默认禁止运行脚本。</p>
<p><strong>解决:</strong></p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code>Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned</code>
</div>
<p style="color:var(--text-dim);font-size:0.85rem">提示确认时输入 <strong>Y</strong> 回车。之后 npm 和 pwsh 脚本都能正常运行。</p>
<div class="card">
<h3>&#10060; 提示 "openssl 未找到"</h3>
<p><strong>原因:</strong>解密凭证需要 openssl它随 Git for Windows 一起安装。</p>
<p><strong>解决:</strong>确认 Git 已安装。脚本会自动搜索 <code>C:\Program Files\Git</code><code>D:\Git</code> 下的 openssl。</p>
</div>
<div class="card">
<h3>&#10060; git clone 失败 / 认证失败</h3>
<p><strong>解决步骤:</strong></p>
<ol style="color:var(--text-dim);padding-left:1.5rem;margin-top:0.3rem">
<li>浏览器打开 <code>https://code.letcareme.com</code> 确认网站可访问</li>
<li>确认用户名密码正确(区分大小写)</li>
<li>如果开了 2FA需要用 Access Token 替代密码</li>
<li>检查网络是否需要代理</li>
</ol>
</div>
<div class="card">
<h3>&#10060; 解密凭证失败 / 主密码错误</h3>
<p><strong>原因:</strong>主密码区分大小写,且无法找回。</p>
<p><strong>解决:</strong>仔细检查密码是否正确。如确认忘记,联系管理员重新生成 <code>secrets.enc</code></p>
</div>
<div class="card">
<h3>&#10060; Claude Code 启动后没有 Bookworm 横幅</h3>
<p><strong>原因:</strong>配置文件未正确同步。</p>
<p><strong>解决:</strong>不加 <code>-StartOnly</code> 重新运行安装脚本,让它重新 clone</p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">pwsh</span> <span class="flag">-ExecutionPolicy Bypass</span> -File install.ps1</code>
</div>
<div class="card">
<h3>&#10060; 安装包下载太慢</h3>
<p><strong>解决:</strong>Node.js 官网在国内可能较慢,可以用淘宝镜像:</p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="comment"># 设置 npm 淘宝镜像(加速下载)</span>
<span class="cmd">npm</span> config set registry https://registry.npmmirror.com
<span class="comment"># 然后重新安装 Claude Code</span>
<span class="cmd">npm</span> i -g @anthropic-ai/claude-code</code>
</div>
<div class="card">
<h3>&#10060; ECONNRESET / "Unable to connect to API"</h3>
<p><strong>原因:</strong>代理软件把国内中转站 <code>bww.letcareme.com</code> 的流量也走了国际线路,导致连接被重置。</p>
<p><strong>解决:</strong>在 PowerShell 中手动设置 NO_PROXY 后重试:</p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="comment"># 设置中转站直连(不走代理)</span>
$env:NO_PROXY = "bww.letcareme.com,code.letcareme.com"
<span class="comment"># 重新启动</span>
<span class="cmd">cd</span> bookworm-boot
<span class="cmd">pwsh</span> <span class="flag">-ExecutionPolicy Bypass</span> -File install.ps1 <span class="flag">-StartOnly</span></code>
</div>
<p style="color:var(--text-dim);font-size:0.85rem">新版安装脚本已自动设置 NO_PROXY<code>git pull</code> 更新后此问题不再出现。</p>
<div class="card">
<h3>&#10060; "Not logged in" / 直接运行 claude 报错</h3>
<p><strong>原因:</strong>API 凭证是进程级环境变量,只在安装脚本启动的进程中有效。新开 PowerShell 窗口直接运行 <code>claude</code> 没有凭证。</p>
<p><strong>解决:</strong><strong>不要直接运行 <code>claude</code></strong>,必须通过以下方式启动:</p>
<ul style="color:var(--text-dim);font-size:0.9rem;padding-left:1.2rem;margin-top:0.3rem">
<li>双击桌面 <strong>Bookworm</strong> 快捷方式</li>
<li>双击 <strong>启动Bookworm.bat</strong></li>
<li>命令行:<code>pwsh -ExecutionPolicy Bypass -File install.ps1 -StartOnly</code></li>
</ul>
</div>
<div class="card">
<h3>&#10060; 完整性校验不匹配(大量文件 WARN</h3>
<p><strong>原因:</strong>本地配置文件已被更新(管理员推送了新版本),但 <code>integrity.sha256</code> 未同步更新。</p>
<p><strong>解决:</strong><strong>y</strong> 继续即可,不影响使用。管理员会在下个版本同步哈希文件。</p>
</div>
<div class="card">
<h3>&#10060; 需要自己的 Claude 账号吗?</h3>
<p><strong>不需要。</strong>所有 API 请求通过中转站转发,消耗中转站额度。目标机不需要任何 Anthropic 账号或订阅。</p>
</div>
</div>
<!-- ============================================================ -->
<!-- 安装检查清单 -->
<!-- ============================================================ -->
<div class="section">
<h2>安装检查清单</h2>
<p style="color:var(--text-dim);margin-bottom:0.5rem">逐项确认,全部打勾即可开始使用:</p>
<ul class="checklist">
<li><strong>Node.js 已安装</strong> &mdash; <code>node -v</code> 显示版本号</li>
<li><strong>Git 已安装</strong> &mdash; <code>git --version</code> 显示版本号</li>
<li><strong>npm 可用</strong> &mdash; <code>npm -v</code> 显示版本号(如报错先设 ExecutionPolicy</li>
<li><strong>Claude Code 已安装</strong> &mdash; <code>claude --version</code> 显示版本号</li>
<li><strong>PowerShell 7 已安装</strong> &mdash; <code>pwsh --version</code> 显示 7.x推荐但非必须</li>
<li><strong>已获取 Gitea 账号密码</strong> &mdash; 管理员提供</li>
<li><strong>已获取主密码</strong> &mdash; 管理员提供(用于解密 API 凭证)</li>
<li><strong>能访问 code.letcareme.com</strong> &mdash; 浏览器打开确认</li>
<li><strong>代理/VPN 已启动</strong> &mdash; 国内必须,脚本自动检测 (Clash/V2Ray/快柠檬等)</li>
</ul>
</div>
<!-- ============================================================ -->
<!-- 快速参考卡片 -->
<!-- ============================================================ -->
<div class="section">
<h2>快速参考</h2>
<table>
<tr><th>操作</th><th>最简方式</th><th>命令行方式</th></tr>
<tr><td>首次安装</td><td>git clone + 双击<br><strong>更新并启动Bookworm.bat</strong></td><td><code>pwsh -ExecutionPolicy Bypass -File install.ps1</code></td></tr>
<tr><td>快速启动</td><td>双击 <strong>启动Bookworm.bat</strong></td><td><code>pwsh -File install.ps1 -StartOnly</code></td></tr>
<tr><td>同步更新</td><td>双击 <strong>更新并启动Bookworm.bat</strong></td><td><code>pwsh -File install.ps1</code></td></tr>
<tr><td>基础清理</td><td colspan="2"><code>pwsh -ExecutionPolicy Bypass -File stop.ps1</code></td></tr>
<tr><td>完整恢复</td><td colspan="2"><code>pwsh -ExecutionPolicy Bypass -File stop.ps1 -Restore</code></td></tr>
<tr><td>深度清理</td><td colspan="2"><code>pwsh -ExecutionPolicy Bypass -File stop.ps1 -Restore -Deep</code></td></tr>
</table>
</div>
<!-- ============================================================ -->
<!-- 安全须知 -->
<!-- ============================================================ -->
<div class="section">
<h2>安全须知</h2>
<table>
<tr><th>特性</th><th>规格</th></tr>
<tr><td>凭证加密</td><td>AES-256-CBC + PBKDF2 (600,000 迭代)</td></tr>
<tr><td>传输加密</td><td>HTTPS (TLS 1.2+, Let's Encrypt 证书)</td></tr>
<tr><td>凭证存储</td><td>进程级环境变量 + 可选本日缓存 (Windows Credential Manager, DPAPI 加密, 当日 23:59 过期)</td></tr>
<tr><td>登录保护</td><td>fail2ban (5 次失败/小时 &#8594; 封禁 24 小时)</td></tr>
</table>
<div class="alert warning" style="margin-top:1rem">
<span class="alert-icon">&#128274;</span>
<div>
<strong>主密码无法找回</strong> &mdash; 请妥善保管。忘记后需管理员重新生成加密凭证。
</div>
</div>
</div>
</div>
<div class="footer">
Bookworm Portable v1.5 &mdash; 保姆式安装手册<br>
&copy; 2026 Bookworm Smart Assistant
</div>
<script>
function copyCode(el) {
const code = el.querySelector('code').innerText;
navigator.clipboard.writeText(code).then(() => {
el.classList.add('copied');
setTimeout(() => el.classList.remove('copied'), 1500);
});
}
</script>
</body>
</html>

View File

@ -1,104 +0,0 @@
/**
* Bookworm MCP 注入脚本 v2 同步 Bookworm v6.5.1 全部 22 portable MCP
* 安全合并到 ~/.claude.json ()
* 用法: node inject-mcp.js
*/
const fs = require("fs");
const path = require("path");
const HOME = process.env.USERPROFILE || process.env.HOME || "";
const f = path.join(HOME, ".claude.json");
let d = {};
try { d = JSON.parse(fs.readFileSync(f, "utf8")); } catch (e) {}
d.mcpServers = {
// ── Tier 1: npx (无需 API Key) ──
"context7": {
command: "npx.cmd", args: ["--yes", "@upstash/context7-mcp@2.1.1"], type: "stdio"
},
"sequential-thinking": {
command: "npx.cmd", args: ["--yes", "@modelcontextprotocol/server-sequential-thinking@2025.12.18"], type: "stdio"
},
"playwright": {
command: "npx.cmd", args: ["--yes", "@playwright/mcp@0.0.68", "--headless"], type: "stdio"
},
"session-continuity": {
command: "npx.cmd", args: ["--yes", "claude-session-continuity-mcp@1.13.0"], type: "stdio"
},
"browser-mcp": {
command: "npx.cmd", args: ["--yes", "@browsermcp/mcp@latest"], type: "stdio"
},
"desktop-commander": {
command: "npx.cmd", args: ["--yes", "@wonderwhy-er/desktop-commander@latest"], type: "stdio",
env: { PUPPETEER_SKIP_DOWNLOAD: "true", PUPPETEER_EXECUTABLE_PATH: "C:/Program Files/Google/Chrome/Application/chrome.exe" }
},
"chrome-devtools": {
command: "npx.cmd",
args: ["--yes", "chrome-devtools-mcp@0.18.1",
"--executablePath", "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
"--viewport", "1280x720", "--proxyServer", "http://127.0.0.1:7893"],
type: "stdio"
},
"mobile": {
command: "npx.cmd", args: ["--yes", "@mobilenext/mobile-mcp@0.0.35"], type: "stdio",
env: { ANDROID_HOME: path.join(HOME, "android-sdk") }
},
// ── Tier 2: npx + API Key (凭证从环境变量读取) ──
"github": {
command: "npx.cmd", args: ["--yes", "@modelcontextprotocol/server-github"], type: "stdio",
env: { GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GITHUB_PERSONAL_ACCESS_TOKEN || "" }
},
"slack": {
command: "npx.cmd", args: ["--yes", "@modelcontextprotocol/server-slack"], type: "stdio",
env: { SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN || "", SLACK_TEAM_ID: "T0A4L1JLEER" }
},
"firecrawl": {
command: "npx.cmd", args: ["--yes", "firecrawl-mcp"], type: "stdio",
env: { FIRECRAWL_API_KEY: process.env.FIRECRAWL_API_KEY || "" }
},
"mcp-image": {
command: "npx.cmd", args: ["--yes", "mcp-image"], type: "stdio",
env: { GEMINI_API_KEY: process.env.GEMINI_API_KEY || "", IMAGE_OUTPUT_DIR: path.join(HOME, "Pictures/mcp-images") }
},
"google-drive": {
command: "npx.cmd", args: ["--yes", "@piotr-agier/google-drive-mcp"], type: "stdio",
env: { GOOGLE_DRIVE_OAUTH_CREDENTIALS: path.join(HOME, ".config/google-drive-mcp/gcp-oauth.keys.json") }
},
"browserbase": {
command: "npx.cmd", args: ["--yes", "@anthropic-ai/browserbase-mcp"], type: "stdio",
env: { BROWSERBASE_API_KEY: process.env.BROWSERBASE_API_KEY || "", BROWSERBASE_PROJECT_ID: "d3dbb32f-be2f-4e3a-b9ec-68e27474763c" }
},
// ── Tier 3: npx + 代理 (需要外网访问) ──
"notebooklm": {
command: "npx.cmd", args: ["--yes", "notebooklm-mcp@latest"], type: "stdio",
env: { https_proxy: "http://127.0.0.1:7893", http_proxy: "http://127.0.0.1:7893" }
},
"cloudflare": {
command: "npx.cmd", args: ["--yes", "mcp-remote", "https://docs.mcp.cloudflare.com/sse"], type: "stdio",
env: { https_proxy: "http://127.0.0.1:7893", http_proxy: "http://127.0.0.1:7893" }
},
// ── Tier 4: HTTP (零安装, 云端托管) ──
"linear": { type: "http", url: "https://mcp.linear.app/mcp" },
"supabase": { type: "http", url: "https://mcp.supabase.com/mcp?project_ref=oepmihbtoylosbsxlmfo" },
"figma": { type: "http", url: "https://mcp.figma.com/mcp" },
// ── Tier 5: Python/uvx (需要 Python + uv) ──
"windows-mcp": { command: "uvx", args: ["--python", "3.13", "windows-mcp"], type: "stdio" },
"atlassian": {
command: "uvx", args: ["mcp-atlassian"], type: "stdio",
env: {
JIRA_URL: "https://huakoh.atlassian.net", JIRA_USERNAME: "huakoh449@gmail.com",
JIRA_API_TOKEN: process.env.ATLASSIAN_API_TOKEN || "",
CONFLUENCE_URL: "https://huakoh.atlassian.net/wiki", CONFLUENCE_USERNAME: "huakoh449@gmail.com",
CONFLUENCE_API_TOKEN: process.env.ATLASSIAN_API_TOKEN || ""
}
},
"computer-control-mcp": { command: "uvx", args: ["computer-control-mcp@latest"], type: "stdio" }
};
fs.writeFileSync(f, JSON.stringify(d, null, 2));
const count = Object.keys(d.mcpServers).length;
console.log("OK: " + count + " MCP servers -> " + f);

View File

@ -1,8 +0,0 @@
#!/bin/bash
# ============================================================
# install-mac.sh — 重定向到 Bookworm-Setup.sh
#
# 多个文档引用此文件名,实际安装逻辑在 Bookworm-Setup.sh 中。
# ============================================================
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
exec bash "$SCRIPT_DIR/Bookworm-Setup.sh" "$@"

View File

@ -19,7 +19,6 @@ param(
[string]$GitUrl = "https://code.letcareme.com/bookworm/bookworm-config.git",
[switch]$StartOnly,
[switch]$SkipSecrets,
[switch]$SkipLaunch, # 仅安装不启动 (由调用方负责启动)
[switch]$AutoAccept # 豁免所有人工确认环节
)
@ -50,8 +49,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.4 |" -ForegroundColor Cyan
Write-Host " | 92 Skills / 18 Agents / 29 Hooks |" -ForegroundColor Cyan
Write-Host " +------------------------------------------+" -ForegroundColor Cyan
Write-Host ""
}
@ -65,24 +64,14 @@ function Get-CachedSecrets {
try {
$cred = cmdkey /list 2>$null | Select-String "bookworm-secrets"
if ($cred) {
# 从 Credential Manager 读取缓存的环境变量
$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")
[System.Environment]::SetEnvironmentVariable($p.Name, $p.Value, "Process")
$loaded++
}
}
@ -98,26 +87,21 @@ function Get-CachedSecrets {
function Save-SecretsToCache {
try {
# 用 Credential Manager 标记缓存存在
cmdkey /generic:bookworm-secrets /user:bw /pass:cached 2>$null | Out-Null
# 用 HKCU 注册表存凭证值 (DPAPI 保护, 仅当前用户可读)
$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")
"SLACK_BOT_TOKEN", "ATLASSIAN_API_TOKEN", "BROWSERBASE_API_KEY", "FIRECRAWL_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
}
if ($v) { Set-ItemProperty $regPath -Name $k -Value $v -Force }
}
# 设置过期时间 (今日 23:59:59)
$expiry = (Get-Date).Date.AddDays(1).ToString("o")
Set-ItemProperty $regPath -Name "_expiry" -Value $expiry -Force
Write-Host " [OK] 凭证已缓存至今日 23:59 (DPAPI 加密, 下次免密)" -ForegroundColor Green
Write-Host " [OK] 凭证已缓存至今日 23:59 (下次免密)" -ForegroundColor Green
} catch {}
}
@ -172,156 +156,60 @@ function Install-MissingDeps {
# ─── 桌面快捷方式 ────────────────────────────────────
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
}
# 启动Bookworm 快捷方式 — 优先 pwsh (PS7),回退 powershell (PS5)
$lnkPath = Join-Path $desktop "Bookworm.lnk"
if (-not (Test-Path $lnkPath)) {
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" }
$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()
# 自验证 (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
}
$psVer = if ($hasPwsh) { "PowerShell 7" } else { "PowerShell 5.1" }
Write-Host " [OK] 桌面快捷方式已创建: Bookworm ($psVer)" -ForegroundColor Green
} 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 {}
Write-Host " [!] 桌面快捷方式创建失败 (不影响使用)" -ForegroundColor Gray
}
}
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
if ($SkipSecrets -or -not (Test-Path $SecretsEnc)) {
Write-Host " [!] 跳过凭证解密 (无 secrets.enc)" -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
if (-not $opensslCmd) {
Write-Host " [!] openssl 未找到,跳过凭证解密" -ForegroundColor Yellow
return
}
$maxRetries = 3
for ($attempt = 1; $attempt -le $maxRetries; $attempt++) {
$label = if ($attempt -gt 1) { " 重新输入主密码 (第 $attempt/$maxRetries 次)" } else { " 输入主密码解密凭证" }
$password = Read-Host $label -AsSecureString
$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password)
$plainPwd = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = "Continue"
if ($useNode) {
$decrypted = & node $cryptoHelper decrypt $plainPwd $encFile 2>&1
$decrypted = $plainPwd | & $opensslCmd enc -aes-256-cbc -d -pbkdf2 -iter 600000 -in $SecretsEnc -pass stdin 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') {
# 清除内存中的密码
$plainPwd = $null
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
if ($decExit -eq 0 -and $decrypted) {
# 解密成功,注入环境变量
$decrypted -split "`n" | ForEach-Object {
$line = $_.Trim()
if ($line -and $line.Contains('=')) {
@ -335,15 +223,18 @@ function Decrypt-Secrets {
return
}
$remaining = 3 - $validAttempts
# 解密失败
$remaining = $maxRetries - $attempt
if ($remaining -gt 0) {
Write-Host " [!!] 授权码无效 (解密失败),剩余重试: $remaining" -ForegroundColor Red
Write-Host " [!!] 密码错误,剩余重试: $remaining" -ForegroundColor Red
}
}
# 3次全部失败
Write-Host ""
Write-Host " [ABORT] 3 次授权码均无效,凭证未解密" -ForegroundColor Red
Write-Host " 请确认授权码是否正确,或联系管理员重新生成" -ForegroundColor Yellow
Write-Host " [ABORT] 3 次密码均错误" -ForegroundColor Red
Write-Host " 请确认主密码是否正确 (区分大小写至少12位)" -ForegroundColor Yellow
Write-Host " 如忘记密码,请联系管理员重新生成 secrets.enc" -ForegroundColor Yellow
exit 1
}
@ -354,26 +245,22 @@ function Render-SettingsTemplate {
}
$claudeRoot = $ClaudeTarget.Replace('\', '/')
$homeDir = $env:USERPROFILE.Replace('\', '/')
# 定位 pwsh 路径 (正斜杠供 JSON)
$pwshExe = (Get-Command pwsh -ErrorAction SilentlyContinue)
$pwshJsonPath = if ($pwshExe) { $pwshExe.Source.Replace('\', '/') } else { "pwsh" }
# HOME 保留反斜杠格式,与 Claude Code 原始行为一致
$homeDir = $env:USERPROFILE
$content = Get-Content $TemplateFile -Raw
$content = $content -replace '\{\{CLAUDE_ROOT\}\}', $claudeRoot
$content = $content -replace '\{\{HOME\}\}', $homeDir
$content = $content -replace '\{\{PWSH_PATH\}\}', $pwshJsonPath
$content = $content -replace '\{\{HOME\}\}', ($homeDir -replace '\\', '\\')
Set-Content $SettingsFile -Value $content -Encoding UTF8
Write-Host " [OK] settings.json 已渲染 (ROOT=$claudeRoot, SHELL=$pwshJsonPath)" -ForegroundColor Green
Write-Host " [OK] settings.json 已渲染 (ROOT=$claudeRoot)" -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 '\{\{HOME\}\}', ($homeDir -replace '\\', '\\')
$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
}
@ -419,15 +306,12 @@ function Detect-SystemProxy {
}
} catch {}
# 方法3: 扫描常见代理端口 (500ms 超时,避免阻塞)
# 方法3: 扫描常见代理端口
$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.Connect("127.0.0.1", $port)
$tcp.Close()
$env:HTTPS_PROXY = "http://127.0.0.1:$port"
$env:HTTP_PROXY = "http://127.0.0.1:$port"
@ -469,46 +353,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
}
@ -563,10 +415,9 @@ if (-not $StartOnly) {
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
$stashOutput = git stash 2>&1
$hasStash = $stashOutput -notmatch 'No local changes'
git stash 2>&1 | Out-Null
git pull --rebase 2>&1 | ForEach-Object { Write-Host " $_" }
if ($hasStash) { git stash pop 2>&1 | Out-Null }
git stash pop 2>&1 | Out-Null
}
catch {
Write-Host " [WARN] git pull 失败: $_" -ForegroundColor Yellow
@ -720,20 +571,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 +605,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 依赖检查 (中文提醒) ---
@ -775,15 +625,12 @@ if (-not $hasPython) {
$mcpWarnings += " 安装: https://www.python.org/downloads/ 或 winget install Python.Python.3.12"
}
# Playwright (浏览器自动化 MCP) — 轻量检测,避免 npx 触发安装
# Playwright (浏览器自动化 MCP)
$hasPlaywright = $false
try {
$pwPath = & npm list -g @playwright/mcp 2>$null
if ($pwPath -and $pwPath -notmatch 'empty') { $hasPlaywright = $true }
} catch {}
try { $null = npx --yes @playwright/mcp --help 2>$null; $hasPlaywright = $true } catch {}
if (-not $hasPlaywright) {
$mcpWarnings += " [!] Playwright MCP 未安装 - 浏览器自动化功能不可用"
$mcpWarnings += " 安装: npm i -g @playwright/mcp && npx playwright install"
$mcpWarnings += " 安装: npx playwright install"
}
# 检查关键 API Key 环境变量
@ -855,15 +702,6 @@ if (-not $StartOnly) {
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
}
& claude

View File

@ -1,54 +0,0 @@
'use strict';
/**
* Circuit Breaker 管理端点补丁
* 追加到 routes/admin.js 末尾 ( module.exports 函数内)
*
* 用法: 部署时将此内容追加到 admin.js registerAdminRoutes 函数体内
*/
// ─── 熔断器状态查看 (admin) ───
// routes['GET:/v1/admin/circuit-breaker'] = async (req, res) => {
// requireAdmin(req);
// const cbStatus = deps.circuitBreaker.getStatus();
// const cbLog = deps.circuitBreaker.getTransitionLog(20);
// json(res, 200, { ok: true, breakers: cbStatus, recentTransitions: cbLog });
// };
// ─── 熔断器重置 (admin) ───
// routes['POST:/v1/admin/circuit-breaker/reset'] = async (req, res) => {
// requireAdmin(req);
// const body = await parseJsonBody(req);
// if (body.provider) {
// deps.circuitBreaker.reset(body.provider);
// json(res, 200, { ok: true, reset: body.provider });
// } else {
// deps.circuitBreaker.resetAll();
// json(res, 200, { ok: true, reset: 'all' });
// }
// };
module.exports = function patchAdminRoutes(routes, deps) {
const { json, parseJsonBody, requireAdmin } = deps;
routes['GET:/v1/admin/circuit-breaker'] = async (req, res) => {
requireAdmin(req);
const cbStatus = deps.circuitBreaker.getStatus();
const cbLog = deps.circuitBreaker.getTransitionLog ? deps.circuitBreaker.getTransitionLog(20) : [];
json(res, 200, { ok: true, breakers: cbStatus, recentTransitions: cbLog });
};
routes['POST:/v1/admin/circuit-breaker/reset'] = async (req, res) => {
requireAdmin(req);
const body = await parseJsonBody(req);
if (body.provider) {
deps.circuitBreaker.reset(body.provider);
json(res, 200, { ok: true, reset: body.provider });
} else if (deps.circuitBreaker.resetAll) {
deps.circuitBreaker.resetAll();
json(res, 200, { ok: true, reset: 'all' });
} else {
json(res, 400, { error: '请指定 provider' });
}
};
};

View File

@ -1,228 +0,0 @@
'use strict';
/**
* Circuit Breaker v2 增强版 per-Provider 熔断器
*
* 改进点:
* 1. 半衰期自动恢复 (失败计数随时间衰减, 避免永久 OPEN)
* 2. 可配置阈值 (通过环境变量覆盖)
* 3. 管理端点支持 (getStatus + reset + resetAll)
* 4. 统计指标 (总请求数成功率平均恢复时间)
* 5. 事件日志 (状态变迁记录, 供审计)
*
* @module src/circuit-breaker
*/
const STATES = { CLOSED: 'closed', OPEN: 'open', HALF_OPEN: 'half_open' };
// 可通过环境变量覆盖
const FAILURE_THRESHOLD = parseInt(process.env.CB_FAILURE_THRESHOLD) || 5;
const RECOVERY_TIMEOUT = parseInt(process.env.CB_RECOVERY_TIMEOUT) || 30000;
const SUCCESS_THRESHOLD = parseInt(process.env.CB_SUCCESS_THRESHOLD) || 2;
const HALF_LIFE_MS = parseInt(process.env.CB_HALF_LIFE_MS) || 120000; // 2 分钟半衰期
const breakers = {};
// 状态变迁日志 (保留最近 100 条)
const MAX_LOG_SIZE = 100;
const transitionLog = [];
function _log(provider, from, to, reason) {
const entry = {
ts: new Date().toISOString(),
provider,
from,
to,
reason,
};
transitionLog.push(entry);
if (transitionLog.length > MAX_LOG_SIZE) transitionLog.shift();
}
function _getBreaker(provider) {
if (!breakers[provider]) {
breakers[provider] = {
state: STATES.CLOSED,
failures: 0,
lastFailureAt: 0,
successCount: 0,
totalTrips: 0,
// 统计
totalRequests: 0,
totalSuccesses: 0,
totalFailures: 0,
lastTripAt: 0,
lastRecoveryAt: 0,
tripDurations: [], // 最近 10 次恢复耗时(ms)
};
}
return breakers[provider];
}
/**
* 半衰期衰减: failures 随时间指数衰减
* 避免偶尔的瞬态错误累积到阈值
*/
function _decayedFailures(b) {
if (b.failures === 0 || b.lastFailureAt === 0) return 0;
const elapsed = Date.now() - b.lastFailureAt;
if (elapsed <= 0) return b.failures;
// 每过一个半衰期, failures 减半
const decayFactor = Math.pow(0.5, elapsed / HALF_LIFE_MS);
return b.failures * decayFactor;
}
/**
* 检查是否允许向 provider 发送请求
*/
function canRequest(provider) {
const b = _getBreaker(provider);
if (b.state === STATES.CLOSED) {
b.totalRequests++;
return true;
}
if (b.state === STATES.OPEN) {
if (Date.now() - b.lastFailureAt > RECOVERY_TIMEOUT) {
const prev = b.state;
b.state = STATES.HALF_OPEN;
b.successCount = 0;
_log(provider, prev, STATES.HALF_OPEN, 'recovery_timeout_elapsed');
b.totalRequests++;
return true;
}
b.totalRejected = (b.totalRejected || 0) + 1;
return false;
}
// HALF_OPEN: 允许有限探测
b.totalRequests++;
return true;
}
/**
* 记录成功
*/
function recordSuccess(provider) {
const b = _getBreaker(provider);
b.totalSuccesses++;
if (b.state === STATES.HALF_OPEN) {
b.successCount++;
if (b.successCount >= SUCCESS_THRESHOLD) {
const prev = b.state;
b.state = STATES.CLOSED;
b.failures = 0;
b.successCount = 0;
b.lastRecoveryAt = Date.now();
// 记录恢复耗时
if (b.lastTripAt > 0) {
b.tripDurations.push(Date.now() - b.lastTripAt);
if (b.tripDurations.length > 10) b.tripDurations.shift();
}
_log(provider, prev, STATES.CLOSED, `${SUCCESS_THRESHOLD}_consecutive_successes`);
}
} else if (b.state === STATES.CLOSED) {
// 使用衰减后的失败数, 而非直接清零
// 这样偶尔的成功会让累积的失败自然消散
b.failures = Math.max(0, _decayedFailures(b) - 0.5);
}
}
/**
* 记录失败
*/
function recordFailure(provider) {
const b = _getBreaker(provider);
b.totalFailures++;
b.lastFailureAt = Date.now();
if (b.state === STATES.HALF_OPEN) {
const prev = b.state;
b.state = STATES.OPEN;
b.totalTrips++;
b.lastTripAt = Date.now();
_log(provider, prev, STATES.OPEN, 'half_open_failure');
} else if (b.state === STATES.CLOSED) {
// 使用衰减后的值 + 1
b.failures = _decayedFailures(b) + 1;
if (b.failures >= FAILURE_THRESHOLD) {
const prev = b.state;
b.state = STATES.OPEN;
b.totalTrips++;
b.lastTripAt = Date.now();
_log(provider, prev, STATES.OPEN, `${FAILURE_THRESHOLD}_failures_reached`);
}
}
}
/**
* 获取所有熔断器状态快照 (管理后台用)
*/
function getStatus() {
const snapshot = {};
for (const [name, b] of Object.entries(breakers)) {
const successRate = b.totalRequests > 0
? Math.round(b.totalSuccesses / b.totalRequests * 10000) / 100
: 100;
const avgRecoveryMs = b.tripDurations.length > 0
? Math.round(b.tripDurations.reduce((a, c) => a + c, 0) / b.tripDurations.length)
: 0;
snapshot[name] = {
state: b.state,
failures: Math.round(_decayedFailures(b) * 100) / 100,
rawFailures: b.failures,
totalTrips: b.totalTrips,
lastFailureAt: b.lastFailureAt ? new Date(b.lastFailureAt).toISOString() : null,
lastRecoveryAt: b.lastRecoveryAt ? new Date(b.lastRecoveryAt).toISOString() : null,
stats: {
totalRequests: b.totalRequests,
totalSuccesses: b.totalSuccesses,
totalFailures: b.totalFailures,
successRate: successRate + '%',
avgRecoveryMs,
},
};
}
return snapshot;
}
/**
* 获取状态变迁日志
*/
function getTransitionLog(limit) {
const n = limit || 20;
return transitionLog.slice(-n);
}
/**
* 手动重置单个 provider
*/
function reset(provider) {
if (breakers[provider]) {
const prev = breakers[provider].state;
breakers[provider].state = STATES.CLOSED;
breakers[provider].failures = 0;
breakers[provider].lastFailureAt = 0;
breakers[provider].successCount = 0;
_log(provider, prev, STATES.CLOSED, 'manual_reset');
}
}
/**
* 重置所有 provider
*/
function resetAll() {
for (const name of Object.keys(breakers)) {
reset(name);
}
}
module.exports = {
canRequest, recordSuccess, recordFailure,
getStatus, getTransitionLog,
reset, resetAll,
STATES,
};

View File

@ -1,141 +0,0 @@
#!/bin/bash
# ============================================================
# Bookworm Web API — 稳定性优化补丁部署脚本
# 用法: ssh root@8.138.11.105 'bash -s' < deploy-patches.sh
# ============================================================
set -euo pipefail
WEBDIR="/opt/bookworm-web"
BACKUP="$WEBDIR/backups/pre-stability-$(date +%Y%m%d_%H%M%S)"
echo "========================================="
echo " Bookworm API 稳定性补丁 v1.0"
echo "========================================="
# 1. 备份当前文件
echo "[1/6] 备份当前文件..."
mkdir -p "$BACKUP/src" "$BACKUP/routes"
cp "$WEBDIR/src/proxy.js" "$BACKUP/src/"
cp "$WEBDIR/src/llm-router.js" "$BACKUP/src/"
cp "$WEBDIR/src/circuit-breaker.js" "$BACKUP/src/"
cp "$WEBDIR/routes/admin.js" "$BACKUP/routes/"
echo " 备份到: $BACKUP"
# 2. 部署 proxy-v2
echo "[2/6] 部署 src/proxy.js (keepAlive + 重试 + SSE心跳)..."
cat > "$WEBDIR/src/proxy.js" << 'PROXYEOF'
PROXY_PLACEHOLDER
PROXYEOF
echo " [OK] proxy.js 已更新"
# 3. 部署 llm-router-v2
echo "[3/6] 部署 src/llm-router.js (SSE心跳 + backpressure + 重试)..."
cat > "$WEBDIR/src/llm-router.js" << 'ROUTEREOF'
ROUTER_PLACEHOLDER
ROUTEREOF
echo " [OK] llm-router.js 已更新"
# 4. 部署 circuit-breaker-v2
echo "[4/6] 部署 src/circuit-breaker.js (半衰期 + 管理端点 + 统计)..."
cat > "$WEBDIR/src/circuit-breaker.js" << 'CBEOF'
CB_PLACEHOLDER
CBEOF
echo " [OK] circuit-breaker.js 已更新"
# 5. 追加 Circuit Breaker 管理端点到 admin.js
echo "[5/6] 追加 Circuit Breaker 管理端点..."
# 检查是否已有 circuit-breaker 路由
if grep -q 'circuit-breaker' "$WEBDIR/routes/admin.js" 2>/dev/null; then
echo " [SKIP] admin.js 已含 circuit-breaker 路由"
else
# 在 admin.js 末尾的 }; 之前注入
# 策略: 在 server.js 中追加路由注册
cat >> "$WEBDIR/server.js.cb-patch" << 'PATCHEOF'
// ─── Circuit Breaker 管理端点 (稳定性补丁) ───
try {
const patchAdminRoutes = require('./patches/admin-cb-routes');
patchAdminRoutes(routes, deps);
} catch (_e) { console.warn('[patch] CB admin routes failed:', _e.message); }
PATCHEOF
# 实际注入方式: 直接修改 admin.js 在最后的 }; 前插入
ADMIN_FILE="$WEBDIR/routes/admin.js"
# 安全追加到函数体末尾 (最后一个 }; 前)
INJECT=$(cat << 'INJECTEOF'
// ─── Circuit Breaker 管理端点 (稳定性补丁) ───
routes['GET:/v1/admin/circuit-breaker'] = async (req, res) => {
requireAdmin(req);
const cb = deps.circuitBreaker;
const cbStatus = cb.getStatus ? cb.getStatus() : {};
const cbLog = cb.getTransitionLog ? cb.getTransitionLog(20) : [];
json(res, 200, { ok: true, breakers: cbStatus, recentTransitions: cbLog });
};
routes['POST:/v1/admin/circuit-breaker/reset'] = async (req, res) => {
requireAdmin(req);
const body = await parseJsonBody(req);
const cb = deps.circuitBreaker;
if (body.provider) {
cb.reset(body.provider);
json(res, 200, { ok: true, reset: body.provider });
} else if (cb.resetAll) {
cb.resetAll();
json(res, 200, { ok: true, reset: 'all' });
} else {
json(res, 400, { error: '请指定 provider' });
}
};
INJECTEOF
)
# 用 sed 在最后的 }; 前插入 (admin.js 最后一行是 };)
sed -i '$ i\'"$INJECT" "$ADMIN_FILE" 2>/dev/null || {
# sed 失败则用 python3
python3 -c "
import re
with open('$ADMIN_FILE', 'r') as f: content = f.read()
inject = '''$INJECT'''
# 在最后一个 }; 前插入
pos = content.rfind('};')
if pos > 0:
content = content[:pos] + inject + '\n' + content[pos:]
with open('$ADMIN_FILE', 'w') as f: f.write(content)
print(' [OK] admin.js 已注入 CB 端点 (python)')
else:
print(' [WARN] 未找到插入点')
"
}
echo " [OK] admin.js 已注入 CB 管理端点"
rm -f "$WEBDIR/server.js.cb-patch"
fi
# 6. 清理 Nginx 重复配置
echo "[6/6] 检查 Nginx 配置..."
NGINX_CONF="/etc/nginx/conf.d/bookworm-web.conf"
if [ -f "$NGINX_CONF" ]; then
# 统计 server 块数量
SERVER_COUNT=$(grep -c 'server_name bookworm.letcareme.com;' "$NGINX_CONF" 2>/dev/null || echo 0)
if [ "$SERVER_COUNT" -gt 2 ]; then
echo " [WARN] 发现 $SERVER_COUNT 个 server 块 (应为 2: HTTP+HTTPS)"
echo " 建议手动清理: vim $NGINX_CONF"
else
echo " [OK] Nginx 配置正常"
fi
fi
echo ""
echo "========================================="
echo " 补丁部署完成!"
echo "========================================="
echo ""
echo " 下一步:"
echo " 1. 语法检查: cd $WEBDIR && node --check server.js"
echo " 2. 重启服务: pm2 reload bookworm-web"
echo " 3. 验证健康: curl http://127.0.0.1:3211/health"
echo " 4. 验证CB端点: curl -H 'Authorization: Admin TOKEN' http://127.0.0.1:3211/v1/admin/circuit-breaker"
echo ""
echo " 回滚:"
echo " cp $BACKUP/src/* $WEBDIR/src/"
echo " cp $BACKUP/routes/* $WEBDIR/routes/"
echo " pm2 reload bookworm-web"
echo "========================================="

View File

@ -1,611 +0,0 @@
'use strict';
/**
* LLM 路由 v2 稳定性增强版
*
* 改进点:
* 1. SSE 心跳 ( 15s :ping, 防止 Nginx 超时断连)
* 2. backpressure 处理 (暂停上游 when 客户端消费慢)
* 3. 非流式请求瞬态重试 (429/502/503 + ECONNRESET)
* 4. 流式错误发送 SSE error event 而非静默断开
* 5. 连接/读取超时分离 (15s/180s)
*
* @module src/llm-router
*/
const https = require('https');
const http = require('http');
const { URL } = require('url');
// ─── DNS 预解析缓存 (B2 修复: 最大 100 条, 防内存泄露) ───
const dns = require('dns');
const _dnsCache = new Map();
const DNS_CACHE_TTL = 300000; // 5 分钟
const DNS_MAX_ENTRIES = 100;
function cachedLookup(hostname, options, callback) {
if (typeof options === 'function') { callback = options; options = {}; }
const cached = _dnsCache.get(hostname);
if (cached && Date.now() - cached.ts < DNS_CACHE_TTL) {
return process.nextTick(() => callback(null, cached.address, cached.family));
}
dns.lookup(hostname, options, (err, address, family) => {
if (!err) {
// 超限时淘汰最旧条目
if (_dnsCache.size >= DNS_MAX_ENTRIES) {
const oldest = _dnsCache.keys().next().value; // Map 保持插入顺序
_dnsCache.delete(oldest);
}
_dnsCache.set(hostname, { address, family, ts: Date.now() });
}
callback(err, address, family);
});
}
// ─── SSRF 防护 (B1 修复: 从 proxy.js 同步) ───
const ALLOWED_LLM_HOSTS = new Set([
'api.anthropic.com', 'api.openai.com',
'dashscope.aliyuncs.com', 'api.deepseek.com', 'api.moonshot.cn',
'open.bigmodel.cn', 'ark.cn-beijing.volces.com',
'api.hunyuan.cloud.tencent.com', 'qianfan.baidubce.com',
'openrouter.ai',
]);
if (process.env.ALLOWED_API_HOSTS) {
for (const h of process.env.ALLOWED_API_HOSTS.split(',')) {
if (h.trim()) ALLOWED_LLM_HOSTS.add(h.trim());
}
}
function _isPrivateHost(hostname) {
const lower = hostname.replace(/^\[|\]$/g, '').toLowerCase();
if (/^f[cd][0-9a-f]{2}:/.test(lower)) return true;
if (/^fe[89ab][0-9a-f]:/.test(lower)) return true;
if (lower === '::1' || lower === '::') return true;
if (hostname.startsWith('[::ffff:')) return _isPrivateHost(hostname.slice(8, -1));
const parts = hostname.split('.');
if (parts.length === 4 && parts.every(p => /^\d+$/.test(p))) {
const [a, b] = parts.map(Number);
if (a === 10 || a === 127 || a === 0) return true;
if (a === 172 && b >= 16 && b <= 31) return true;
if (a === 192 && b === 168) return true;
if (a === 169 && b === 254) return true;
}
return hostname === 'localhost';
}
function _validateLLMBaseUrl(baseUrl) {
if (!baseUrl) return;
let url;
try { url = new URL(baseUrl); } catch { throw { status: 400, message: 'base_url 格式无效' }; }
if (ALLOWED_LLM_HOSTS.has(url.hostname)) return;
if (_isPrivateHost(url.hostname)) throw { status: 403, message: '不允许访问内网地址' };
throw { status: 403, message: '不允许的 LLM API 地址' };
}
// 启动预热
for (const h of ['dashscope.aliyuncs.com', 'api.deepseek.com', 'api.moonshot.cn', 'open.bigmodel.cn', 'ark.cn-beijing.volces.com', 'api.hunyuan.cloud.tencent.com', 'qianfan.baidubce.com']) {
dns.lookup(h, () => {});
}
// ─── HTTP Agent: keepAlive 连接池 + DNS 缓存 ───
const httpsAgent = new https.Agent({
keepAlive: true, maxSockets: 10, maxFreeSockets: 5,
keepAliveMsecs: 30000, lookup: cachedLookup,
});
const httpAgent = new http.Agent({
keepAlive: true, maxSockets: 10, maxFreeSockets: 5,
keepAliveMsecs: 30000, lookup: cachedLookup,
});
// ─── 重试配置 ───
const RETRYABLE_STATUS = new Set([429, 502, 503, 504]);
const RETRYABLE_ERRORS = new Set(['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'EPIPE', 'EAI_AGAIN']);
const MAX_RETRIES = 2;
function isRetryable(err, statusCode) {
if (statusCode && RETRYABLE_STATUS.has(statusCode)) return true;
if (err && RETRYABLE_ERRORS.has(err.code)) return true;
if (err && /socket hang up|ECONNRESET|ETIMEDOUT/i.test(err.message)) return true;
return false;
}
function retryDelay(attempt) {
return 1000 * Math.pow(2, attempt) + Math.random() * 500;
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
// ─── SSE 心跳 ───
const SSE_HEARTBEAT_MS = 15000;
function startSSEHeartbeat(res) {
const timer = setInterval(() => {
if (res.writableEnded || res.destroyed) { clearInterval(timer); return; }
try { res.write(':ping\n\n'); } catch { clearInterval(timer); }
}, SSE_HEARTBEAT_MS);
if (timer.unref) timer.unref();
return timer;
}
// ─── Provider 配置 ───
const PROVIDERS = {
anthropic: {
baseUrl: 'https://api.anthropic.com',
pathPrefix: '/v1/messages',
authHeader: 'x-api-key',
versionHeader: { 'anthropic-version': process.env.ANTHROPIC_API_VERSION || '2023-06-01' },
modelPrefixes: ['claude-'],
buildBody: (opts) => ({
model: opts.model,
messages: opts.messages,
max_tokens: opts.maxTokens || 8192,
stream: opts.stream || false,
...(opts.systemPrompt ? { system: opts.systemPrompt } : {}),
}),
parseUsage: (data) => ({
input_tokens: data?.usage?.input_tokens || 0,
output_tokens: data?.usage?.output_tokens || 0,
}),
},
openai: {
baseUrl: 'https://api.openai.com',
pathPrefix: '/v1/chat/completions',
authHeader: 'Authorization',
authPrefix: 'Bearer ',
versionHeader: {},
modelPrefixes: ['gpt-', 'o1-', 'o3-', 'o4-', 'chatgpt-'],
buildBody: (opts) => {
const isReasoningModel = /^(o1|o3|o4)-/i.test(opts.model);
const tokenParam = isReasoningModel
? { max_completion_tokens: opts.maxTokens || 8192 }
: { max_tokens: opts.maxTokens || 8192 };
return {
model: opts.model,
messages: [
...(opts.systemPrompt ? [{ role: 'system', content: opts.systemPrompt }] : []),
...opts.messages,
],
...tokenParam,
stream: opts.stream || false,
};
},
parseUsage: (data) => ({
input_tokens: data?.usage?.prompt_tokens || 0,
output_tokens: data?.usage?.completion_tokens || 0,
}),
},
qwen: {
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode',
pathPrefix: '/v1/chat/completions',
authHeader: 'Authorization',
authPrefix: 'Bearer ',
versionHeader: {},
modelPrefixes: ['qwen'],
buildBody: (opts) => ({
model: opts.model,
messages: [
...(opts.systemPrompt ? [{ role: 'system', content: opts.systemPrompt }] : []),
...opts.messages,
],
max_tokens: opts.maxTokens || 8192,
stream: opts.stream || false,
enable_thinking: false,
}),
parseUsage: (data) => ({
input_tokens: data?.usage?.prompt_tokens || 0,
output_tokens: data?.usage?.completion_tokens || 0,
}),
},
deepseek: {
baseUrl: 'https://api.deepseek.com',
pathPrefix: '/v1/chat/completions',
authHeader: 'Authorization',
authPrefix: 'Bearer ',
versionHeader: {},
modelPrefixes: ['deepseek-'],
buildBody: (opts) => ({
model: opts.model,
messages: [
...(opts.systemPrompt ? [{ role: 'system', content: opts.systemPrompt }] : []),
...opts.messages,
],
max_tokens: opts.maxTokens || 8192,
stream: opts.stream || false,
}),
parseUsage: (data) => ({
input_tokens: data?.usage?.prompt_tokens || 0,
output_tokens: data?.usage?.output_tokens || 0,
}),
},
kimi: {
baseUrl: 'https://api.moonshot.cn',
pathPrefix: '/v1/chat/completions',
authHeader: 'Authorization',
authPrefix: 'Bearer ',
versionHeader: {},
modelPrefixes: ['moonshot-', 'kimi'],
buildBody: (opts) => ({
model: opts.model,
messages: [
...(opts.systemPrompt ? [{ role: 'system', content: opts.systemPrompt }] : []),
...opts.messages,
],
max_tokens: opts.maxTokens || 8192,
stream: opts.stream || false,
}),
parseUsage: (data) => ({
input_tokens: data?.usage?.prompt_tokens || 0,
output_tokens: data?.usage?.completion_tokens || 0,
}),
},
zhipu: {
baseUrl: 'https://open.bigmodel.cn/api/paas',
pathPrefix: '/v4/chat/completions',
authHeader: 'Authorization',
authPrefix: 'Bearer ',
versionHeader: {},
modelPrefixes: ['glm-'],
buildBody: (opts) => ({
model: opts.model,
messages: [
...(opts.systemPrompt ? [{ role: 'system', content: opts.systemPrompt }] : []),
...opts.messages,
],
max_tokens: opts.maxTokens || 4096,
stream: opts.stream || false,
}),
parseUsage: (data) => ({
input_tokens: data?.usage?.prompt_tokens || 0,
output_tokens: data?.usage?.completion_tokens || 0,
}),
},
volcengine: {
baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
pathPrefix: '/chat/completions',
authHeader: 'Authorization',
authPrefix: 'Bearer ',
versionHeader: {},
modelPrefixes: ['doubao-', 'ep-'],
buildBody: (opts) => ({
model: opts.model,
messages: [
...(opts.systemPrompt ? [{ role: 'system', content: opts.systemPrompt }] : []),
...opts.messages,
],
max_tokens: opts.maxTokens || 4096,
stream: opts.stream || false,
}),
parseUsage: (data) => ({
input_tokens: data?.usage?.prompt_tokens || 0,
output_tokens: data?.usage?.completion_tokens || 0,
}),
},
hunyuan: {
baseUrl: 'https://api.hunyuan.cloud.tencent.com',
pathPrefix: '/v1/chat/completions',
authHeader: 'Authorization',
authPrefix: 'Bearer ',
versionHeader: {},
modelPrefixes: ['hunyuan-'],
buildBody: (opts) => ({
model: opts.model,
messages: [
...(opts.systemPrompt ? [{ role: 'system', content: opts.systemPrompt }] : []),
...opts.messages,
],
...(opts.maxTokens ? { max_tokens: opts.maxTokens } : {}),
stream: opts.stream || false,
}),
parseUsage: (data) => ({
input_tokens: data?.usage?.prompt_tokens || 0,
output_tokens: data?.usage?.completion_tokens || 0,
}),
},
baidu: {
baseUrl: 'https://qianfan.baidubce.com',
pathPrefix: '/v2/chat/completions',
authHeader: 'Authorization',
authPrefix: 'Bearer ',
versionHeader: {},
modelPrefixes: ['ernie-'],
buildBody: (opts) => ({
model: opts.model,
messages: [
...(opts.systemPrompt ? [{ role: 'system', content: opts.systemPrompt }] : []),
...opts.messages,
],
max_tokens: opts.maxTokens || 4096,
stream: opts.stream || false,
}),
parseUsage: (data) => ({
input_tokens: data?.usage?.prompt_tokens || 0,
output_tokens: data?.usage?.completion_tokens || 0,
}),
},
};
function detectProvider(model) {
if (!model) return 'qwen';
const lower = model.toLowerCase();
for (const [name, config] of Object.entries(PROVIDERS)) {
if (config.modelPrefixes.some(prefix => lower.startsWith(prefix))) {
return name;
}
}
return 'qwen';
}
function getProviderConfig(providerName, overrideBaseUrl) {
const config = PROVIDERS[providerName];
if (!config) throw { status: 400, message: `不支持的 provider: ${providerName}` };
// B1 修复: SSRF 防护
if (overrideBaseUrl) _validateLLMBaseUrl(overrideBaseUrl);
return { ...config, baseUrl: overrideBaseUrl || config.baseUrl };
}
function listProviders() {
return Object.entries(PROVIDERS).map(([name, config]) => ({
name, models: config.modelPrefixes, baseUrl: config.baseUrl,
}));
}
/**
* 核心 LLM 请求 非流式 (支持重试)
*/
function _sendNonStream(provider, apiKey, body, isHttps, requestOpts) {
return new Promise((resolve, reject) => {
const transport = isHttps ? https : http;
const proxyReq = transport.request(requestOpts, (proxyRes) => {
proxyReq.setTimeout(180_000);
const chunks = [];
proxyRes.on('data', c => chunks.push(c));
proxyRes.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf8');
try { resolve({ status: proxyRes.statusCode, data: JSON.parse(raw), headers: proxyRes.headers }); }
catch { resolve({ status: proxyRes.statusCode, data: raw, headers: proxyRes.headers }); }
});
proxyRes.on('error', reject);
});
proxyReq.on('error', (err) => {
err.message = `LLM 请求失败: ${err.message}`;
reject(err);
});
proxyReq.on('timeout', () => {
proxyReq.destroy(new Error('LLM API 连接超时 (30s)'));
});
proxyReq.write(JSON.stringify(body));
proxyReq.end();
});
}
/**
* 核心 LLM 请求 流式 (不重试, 带心跳+backpressure)
*/
function _sendStream(provider, apiKey, body, res, isHttps, requestOpts) {
return new Promise((resolve, reject) => {
const transport = isHttps ? https : http;
const proxyReq = transport.request(requestOpts, (proxyRes) => {
proxyReq.setTimeout(300_000); // 流式读取 5 分钟
if (proxyRes.statusCode !== 200) {
const chunks = [];
proxyRes.on('data', c => chunks.push(c));
proxyRes.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf8');
try { resolve({ status: proxyRes.statusCode, data: JSON.parse(raw), streamed: false }); }
catch { resolve({ status: proxyRes.statusCode, data: { error: raw }, streamed: false }); }
});
proxyRes.on('error', reject);
return;
}
// SSE headers
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no',
});
// 启动心跳
const heartbeatTimer = startSSEHeartbeat(res);
let lastDataLine = '';
let inThink = false;
let fullText = '';
proxyRes.on('data', c => {
const text = c.toString();
const lines = text.split('\n');
const outLines = [];
for (const line of lines) {
if (line.startsWith('data: ') && line !== 'data: [DONE]') {
const jsonStr = line.slice(6);
lastDataLine = jsonStr;
try {
const parsed = JSON.parse(jsonStr);
const delta = parsed.choices?.[0]?.delta;
if (delta) {
if (delta.reasoning_content !== undefined) {
delete delta.reasoning_content;
}
if (delta.content) {
let content = delta.content;
if (inThink) {
const endIdx = content.indexOf('</think>');
if (endIdx !== -1) { inThink = false; content = content.slice(endIdx + 8); }
else content = '';
}
if (!inThink && content.includes('<think>')) {
const startIdx = content.indexOf('<think>');
const endIdx = content.indexOf('</think>', startIdx);
if (endIdx !== -1) { content = content.slice(0, startIdx) + content.slice(endIdx + 8); }
else { content = content.slice(0, startIdx); inThink = true; }
}
delta.content = content;
if (content) fullText += content;
}
}
outLines.push('data: ' + JSON.stringify(parsed));
} catch {
outLines.push(line);
}
} else {
outLines.push(line);
}
}
// backpressure 处理
const canWrite = res.write(outLines.join('\n'));
if (!canWrite) {
proxyRes.pause();
res.once('drain', () => proxyRes.resume());
}
});
proxyRes.on('end', () => {
clearInterval(heartbeatTimer);
res.end();
let usage = null;
try {
const parsed = JSON.parse(lastDataLine);
if (parsed.usage) {
usage = {
input_tokens: parsed.usage.prompt_tokens || parsed.usage.input_tokens || 0,
output_tokens: parsed.usage.completion_tokens || parsed.usage.output_tokens || 0,
};
}
} catch { /* 无 usage */ }
resolve({ streamed: true, status: 200, usage, fullText: fullText || undefined });
});
proxyRes.on('error', (err) => {
clearInterval(heartbeatTimer);
// 发送 SSE 错误事件
try {
res.write(`data: ${JSON.stringify({ error: { message: err.message, type: 'stream_error' } })}\n\n`);
} catch { /* ignore */ }
res.end();
reject(err);
});
res.on('close', () => {
clearInterval(heartbeatTimer);
proxyReq.destroy();
});
});
proxyReq.on('error', (err) => {
err.message = `LLM 流式请求失败: ${err.message}`;
reject(err);
});
proxyReq.on('timeout', () => {
proxyReq.destroy(new Error('LLM API 连接超时 (30s)'));
});
proxyReq.write(JSON.stringify(body));
proxyReq.end();
});
}
/**
* 通用 LLM 请求 自动选择流式/非流式, 非流式带重试
*/
async function sendLLMRequest(provider, apiKey, body, res, stream) {
const config = PROVIDERS[provider.name || provider] || PROVIDERS.qwen;
const base = provider.baseUrl || config.baseUrl;
let baseUrl;
try { baseUrl = new URL(base); }
catch { throw { status: 400, message: `base_url 格式无效: ${base}` }; }
const fullPath = baseUrl.pathname.replace(/\/$/, '') + config.pathPrefix;
const url = new URL(fullPath, base);
const isHttps = url.protocol === 'https:';
const headers = {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(JSON.stringify(body)),
...config.versionHeader,
};
if (config.authHeader === 'Authorization') {
headers['Authorization'] = (config.authPrefix || '') + apiKey;
} else {
headers[config.authHeader] = apiKey;
}
const requestOpts = {
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: url.pathname,
method: 'POST',
headers,
agent: isHttps ? httpsAgent : httpAgent,
timeout: 30_000, // 连接超时 30s
};
if (stream && res) {
return _sendStream(provider, apiKey, body, res, isHttps, requestOpts);
}
// 非流式: 带重试
let lastError = null;
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try {
const result = await _sendNonStream(provider, apiKey, body, isHttps, requestOpts);
if (RETRYABLE_STATUS.has(result.status) && attempt < MAX_RETRIES) {
await sleep(retryDelay(attempt));
continue;
}
return result;
} catch (err) {
lastError = err;
if (isRetryable(err) && attempt < MAX_RETRIES) {
await sleep(retryDelay(attempt));
continue;
}
throw err;
}
}
throw lastError;
}
// --- Circuit Breaker + EWMA hooks ---
let _circuitBreaker = null;
let _providerHealth = null;
function initPerformanceHooks(opts) {
_circuitBreaker = opts.circuitBreaker || null;
_providerHealth = opts.providerHealth || null;
}
function sendLLMRequestWithCB(provider, apiKey, body, res, stream) {
const providerName = provider.name || provider;
const startMs = Date.now();
if (_circuitBreaker && !_circuitBreaker.canRequest(providerName)) {
return Promise.reject(Object.assign(new Error('Circuit Breaker OPEN: ' + providerName), { status: 503 }));
}
return sendLLMRequest(provider, apiKey, body, res, stream).then(
(result) => {
const elapsed = Date.now() - startMs;
const success = result.streamed ? true : (result.status >= 200 && result.status < 500);
if (_circuitBreaker && success) _circuitBreaker.recordSuccess(providerName);
if (_providerHealth && _providerHealth.recordRequestLatency) _providerHealth.recordRequestLatency(providerName, elapsed, success);
return result;
},
(err) => {
const elapsed = Date.now() - startMs;
if (_circuitBreaker) _circuitBreaker.recordFailure(providerName);
if (_providerHealth && _providerHealth.recordRequestLatency) _providerHealth.recordRequestLatency(providerName, elapsed, false);
throw err;
}
);
}
module.exports = {
detectProvider, getProviderConfig, listProviders,
sendLLMRequest: sendLLMRequestWithCB,
sendLLMRequestRaw: sendLLMRequest,
initPerformanceHooks,
PROVIDERS,
};

View File

@ -1,349 +0,0 @@
'use strict';
/**
* BYOK 代理 v2 稳定性增强版
*
* 改进点:
* 1. keepAlive 连接池 (复用 TCP 连接, 减少 TLS 握手)
* 2. 可重试的瞬态错误 (ECONNRESET, ETIMEDOUT, 429, 502, 503)
* 3. SSE 心跳 ( 15s 发送 :ping, 防止 Nginx/CDN 超时断连)
* 4. backpressure 处理 (res.write 返回 false 时暂停上游)
* 5. 分离连接超时(15s)和读取超时(180s)
* 6. 详细错误分类与日志
*
* @module src/proxy
*/
const https = require('https');
const http = require('http');
const { URL } = require('url');
// ─── ❶ SSRF 防护base_url 白名单 ───
const ALLOWED_API_HOSTS = new Set([
'api.anthropic.com',
]);
if (process.env.ALLOWED_API_HOSTS) {
for (const h of process.env.ALLOWED_API_HOSTS.split(',')) {
if (h.trim()) ALLOWED_API_HOSTS.add(h.trim());
}
}
function isPrivateHost(hostname) {
// IPv6 mapped IPv4
if (hostname.startsWith('[::ffff:')) {
return isPrivateHost(hostname.slice(8, -1));
}
// IPv6 私有地址段: ULA (fc00::/7), Link-local (fe80::/10), loopback (::1), unspecified (::)
const lower = hostname.replace(/^\[|\]$/g, '').toLowerCase();
if (/^f[cd][0-9a-f]{2}:/.test(lower)) return true; // fc00::/7 (ULA)
if (/^fe[89ab][0-9a-f]:/.test(lower)) return true; // fe80::/10 (link-local)
if (lower === '::1' || lower === '::') return true;
// IPv4 私有地址
const parts = hostname.split('.');
if (parts.length === 4 && parts.every(p => /^\d+$/.test(p))) {
const [a, b] = parts.map(Number);
if (a === 10) return true;
if (a === 172 && b >= 16 && b <= 31) return true;
if (a === 192 && b === 168) return true;
if (a === 127) return true;
if (a === 169 && b === 254) return true;
if (a === 0) return true;
}
return hostname === 'localhost' || hostname === '[::1]';
}
function validateBaseUrl(baseUrl) {
if (!baseUrl) return;
let url;
try {
url = new URL(baseUrl);
} catch {
throw { status: 400, message: 'base_url 格式无效' };
}
if (ALLOWED_API_HOSTS.has(url.hostname)) return;
if (isPrivateHost(url.hostname)) {
throw { status: 403, message: '不允许访问内网地址' };
}
// W10 修复: 非白名单公网地址也拒绝 (防止变成开放代理)
throw { status: 403, message: '不允许的 API 地址,请联系管理员将域名加入白名单' };
}
// ─── ❷ keepAlive 连接池 ───
const httpsAgent = new https.Agent({
keepAlive: true,
maxSockets: 10,
maxFreeSockets: 5,
keepAliveMsecs: 30000,
timeout: 15000,
});
const httpAgent = new http.Agent({
keepAlive: true,
maxSockets: 10,
maxFreeSockets: 5,
keepAliveMsecs: 30000,
timeout: 15000,
});
// ─── ❸ 重试配置 ───
const RETRYABLE_CODES = new Set([429, 502, 503, 504]);
const RETRYABLE_ERRORS = new Set(['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'EPIPE', 'EAI_AGAIN', 'UND_ERR_SOCKET']);
const MAX_RETRIES = 2;
const RETRY_BASE_DELAY = 1000; // 1s 指数退避
function isRetryable(err, statusCode) {
if (statusCode && RETRYABLE_CODES.has(statusCode)) return true;
if (err && err.code && RETRYABLE_ERRORS.has(err.code)) return true;
if (err && err.message && /socket hang up|ECONNRESET|ETIMEDOUT/i.test(err.message)) return true;
return false;
}
function retryDelay(attempt) {
// 指数退避 + 抖动: 1s, 2s + random(0-500ms)
return RETRY_BASE_DELAY * Math.pow(2, attempt) + Math.random() * 500;
}
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
// ─── ❹ SSE 心跳 ───
const SSE_HEARTBEAT_INTERVAL = 15000; // 每 15 秒
function startSSEHeartbeat(res) {
const timer = setInterval(() => {
if (res.writableEnded || res.destroyed) {
clearInterval(timer);
return;
}
try {
res.write(':ping\n\n');
} catch {
clearInterval(timer);
}
}, SSE_HEARTBEAT_INTERVAL);
// 不阻止进程退出
if (timer.unref) timer.unref();
return timer;
}
// ─── BYOK 代理核心 ───
async function proxyChat(opts, res) {
const {
apiKey,
model = 'claude-opus-4-7',
messages,
maxTokens = 8192,
stream = false,
baseUrl,
systemPrompt,
} = opts;
validateBaseUrl(baseUrl);
const base = baseUrl || process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com';
const url = new URL('/v1/messages', base);
const isHttps = url.protocol === 'https:';
const body = {
model,
messages,
max_tokens: maxTokens,
stream,
};
if (systemPrompt) body.system = systemPrompt;
const payload = JSON.stringify(body);
const requestOpts = {
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': process.env.ANTHROPIC_API_VERSION || '2023-06-01',
'Content-Length': Buffer.byteLength(payload),
},
agent: isHttps ? httpsAgent : httpAgent,
timeout: 15000, // 连接超时 15s
};
// ─── 流式请求 (不重试, 因为 headers 一旦发送不可回退) ───
if (stream) {
return _proxyChatStream(requestOpts, payload, res, isHttps);
}
// ─── 非流式请求 (支持重试) ───
let lastError = null;
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try {
const result = await _proxyChatOnce(requestOpts, payload, isHttps);
// 上游返回可重试状态码
if (result.status && RETRYABLE_CODES.has(result.status) && attempt < MAX_RETRIES) {
const delay = result.status === 429
? _parseRetryAfter(result.headers) || retryDelay(attempt)
: retryDelay(attempt);
await sleep(delay);
continue;
}
return result;
} catch (err) {
lastError = err;
if (isRetryable(err) && attempt < MAX_RETRIES) {
await sleep(retryDelay(attempt));
continue;
}
throw err;
}
}
throw lastError;
}
// 解析 Retry-After 头 (秒或日期)
function _parseRetryAfter(headers) {
const val = headers && headers['retry-after'];
if (!val) return null;
const secs = parseInt(val, 10);
if (!isNaN(secs) && secs > 0 && secs < 120) return secs * 1000;
return null;
}
// 单次非流式请求
function _proxyChatOnce(requestOpts, payload, isHttps) {
return new Promise((resolve, reject) => {
const transport = isHttps ? https : http;
const proxyReq = transport.request(requestOpts, (proxyRes) => {
// 连接成功 → 切换为读取超时 180s
proxyReq.setTimeout(180_000);
const chunks = [];
proxyRes.on('data', (chunk) => chunks.push(chunk));
proxyRes.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf8');
try {
resolve({ status: proxyRes.statusCode, data: JSON.parse(raw), headers: proxyRes.headers });
} catch {
resolve({ status: proxyRes.statusCode, data: raw, headers: proxyRes.headers });
}
});
proxyRes.on('error', reject);
});
proxyReq.on('error', reject);
proxyReq.on('timeout', () => {
proxyReq.destroy(new Error('Claude API 连接超时 (15s)'));
});
proxyReq.write(payload);
proxyReq.end();
});
}
// 流式请求 (不重试)
function _proxyChatStream(requestOpts, payload, res, isHttps) {
return new Promise((resolve, reject) => {
const transport = isHttps ? https : http;
const proxyReq = transport.request(requestOpts, (proxyRes) => {
// 连接成功 → 切换为读取超时 300s (流式更长)
proxyReq.setTimeout(300_000);
// 上游返回错误: 不走 SSE, 收集后 JSON 返回
if (proxyRes.statusCode !== 200) {
const chunks = [];
proxyRes.on('data', (chunk) => chunks.push(chunk));
proxyRes.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf8');
try {
resolve({ status: proxyRes.statusCode, data: JSON.parse(raw), streamed: false });
} catch {
resolve({ status: proxyRes.statusCode, data: { error: raw }, streamed: false });
}
});
proxyRes.on('error', reject);
return;
}
// 正常 SSE 透传
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no',
});
// 启动心跳
const heartbeatTimer = startSSEHeartbeat(res);
let tokensIn = 0, tokensOut = 0;
let fullText = '';
proxyRes.on('data', (chunk) => {
// backpressure: 如果客户端消费慢, 暂停上游
const canWrite = res.write(chunk);
if (!canWrite) {
proxyRes.pause();
res.once('drain', () => proxyRes.resume());
}
// 提取用量 + 全文
try {
const text = chunk.toString();
for (const line of text.split('\n')) {
if (!line.startsWith('data: ') || line === 'data: [DONE]') continue;
const obj = JSON.parse(line.slice(6));
if (obj.type === 'message_start' && obj.message?.usage) {
tokensIn = obj.message.usage.input_tokens || 0;
} else if (obj.type === 'content_block_delta' && obj.delta?.text) {
fullText += obj.delta.text;
} else if (obj.type === 'message_delta' && obj.usage) {
tokensOut = obj.usage.output_tokens || 0;
}
}
} catch { /* 解析失败不影响透传 */ }
});
proxyRes.on('end', () => {
clearInterval(heartbeatTimer);
res.end();
resolve({ streamed: true, status: 200, usage: { tokensIn, tokensOut }, fullText: fullText || undefined });
});
proxyRes.on('error', (err) => {
clearInterval(heartbeatTimer);
// 尝试发送 SSE 错误事件后关闭
try {
res.write(`data: ${JSON.stringify({ type: 'error', error: { type: 'stream_error', message: err.message } })}\n\n`);
} catch { /* ignore */ }
res.end();
reject(err);
});
// 客户端断开时清理
res.on('close', () => {
clearInterval(heartbeatTimer);
proxyReq.destroy();
});
});
proxyReq.on('error', (err) => {
reject(err);
});
proxyReq.on('timeout', () => {
proxyReq.destroy(new Error('Claude API 连接超时 (15s)'));
});
proxyReq.write(payload);
proxyReq.end();
});
}
module.exports = { proxyChat, validateBaseUrl };

View File

@ -1,217 +0,0 @@
#!/usr/bin/env python3
"""
更新 guide.html + quick-start.html
- 添加 OneClick 一键安装器下载
- 更新版本号: 34 Hooks, v2.0
- 三平台下载按钮
"""
import re, os
WEBDIR = '/opt/bookworm-web/public'
# ═══════════════════════════════════════
# 1. guide.html
# ═══════════════════════════════════════
guide = os.path.join(WEBDIR, 'guide.html')
with open(guide, 'r', encoding='utf-8') as f:
html = f.read()
# (a) 版本号 29 Hooks → 34 Hooks
html = html.replace('29 Hooks', '34 Hooks')
# (b) 版本号 v1.5 → v2.0
html = html.replace('Portable v1.5', 'Portable v2.0')
html = html.replace('v1.5 |', 'v2.0 |')
# (c) 替换单一下载按钮为三平台下载
old_download = '''<a href="/Bookworm-Setup.bat" download style="display:inline-block;margin-top:1.2rem;padding:0.7rem 2rem;background:var(--accent);color:#000;font-weight:700;border-radius:8px;font-size:1rem;text-decoration:none;transition:opacity 0.2s" onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">&#11015; 下载一键安装器</a>'''
new_download = '''<div style="display:flex;gap:0.8rem;justify-content:center;margin-top:1.2rem;flex-wrap:wrap">
<a href="/download/Bookworm-OneClick.bat" download style="display:inline-flex;align-items:center;gap:0.4rem;padding:0.7rem 1.5rem;background:var(--accent);color:#000;font-weight:700;border-radius:8px;font-size:0.95rem;text-decoration:none;transition:opacity 0.2s" onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">&#11015; Windows 11 一键安装</a>
<a href="/download/Bookworm-OneClick-Win10.bat" download style="display:inline-flex;align-items:center;gap:0.4rem;padding:0.7rem 1.5rem;background:var(--purple);color:#000;font-weight:700;border-radius:8px;font-size:0.95rem;text-decoration:none;transition:opacity 0.2s" onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">&#11015; Windows 10 一键安装</a>
<a href="/download/Bookworm-OneClick-Mac.sh" download style="display:inline-flex;align-items:center;gap:0.4rem;padding:0.7rem 1.5rem;background:var(--green);color:#000;font-weight:700;border-radius:8px;font-size:0.95rem;text-decoration:none;transition:opacity 0.2s" onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">&#11015; macOS 一键安装</a>
</div>
<p style="text-align:center;color:var(--text-dim);font-size:0.8rem;margin-top:0.5rem">全新电脑? 双击即装 &mdash; 自动安装 Node.js + Git + Claude Code + Bookworm 配置</p>'''
html = html.replace(old_download, new_download)
# (d) 更新 "最快方式" 卡片 — 提及全自动
old_fastest = '''<h3 style="color:var(--green);margin-bottom:0.5rem">最快方式:一键安装器</h3>
<p style="color:var(--text-dim);font-size:0.9rem;margin-bottom:0.8rem">获取 <strong>Bookworm-Setup.bat</strong> (4KB) &#8594; 双击运行 &#8594; 输入密码 &#8594; 完成</p>
<p style="font-size:0.8rem;color:var(--text-dim)">安装器自动检测依赖下载配置创建桌面快捷方式启动 Claude Code</p>'''
new_fastest = '''<h3 style="color:var(--green);margin-bottom:0.5rem">最快方式:全自动一键安装器 v2.0</h3>
<p style="color:var(--text-dim);font-size:0.9rem;margin-bottom:0.8rem">下载 <strong>OneClick 安装器</strong> &#8594; 双击运行 &#8594; 输入密码 &#8594; 完成</p>
<p style="font-size:0.8rem;color:var(--text-dim)">全新电脑零依赖自动安装 Node.js + Git + Claude Code + 下载配置 + 创建桌面快捷方式</p>
<div style="display:flex;gap:0.5rem;justify-content:center;margin-top:0.6rem;flex-wrap:wrap;font-size:0.8rem">
<span style="background:rgba(88,166,255,0.1);border:1px solid rgba(88,166,255,0.3);padding:0.2rem 0.6rem;border-radius:12px">Win 11: winget 自动装</span>
<span style="background:rgba(188,140,255,0.1);border:1px solid rgba(188,140,255,0.3);padding:0.2rem 0.6rem;border-radius:12px">Win 10: 下载 MSI 静默装</span>
<span style="background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);padding:0.2rem 0.6rem;border-radius:12px">macOS: Homebrew 自动装</span>
</div>'''
html = html.replace(old_fastest, new_fastest)
# (e) 更新手动流程提示
old_manual = '<p style="text-align:center;color:var(--text-dim);font-size:0.85rem;margin-bottom:0.5rem">手动安装流程:</p>'
new_manual = '<p style="text-align:center;color:var(--text-dim);font-size:0.85rem;margin-bottom:0.5rem">如需手动安装(一键安装器失败时的备选方案):</p>'
html = html.replace(old_manual, new_manual)
# (f) 更新首次安装行 "快速参考" 表
old_ref = '<tr><td>首次安装</td><td>git clone + 双击<br><strong>更新并启动Bookworm.bat</strong></td><td><code>pwsh -ExecutionPolicy Bypass -File install.ps1</code></td></tr>'
new_ref = '<tr><td>首次安装</td><td>双击 <strong>Bookworm-OneClick.bat</strong><br><span style="font-size:0.8rem;color:var(--text-dim)">自动装 Node.js + Git + Claude Code</span></td><td><code>pwsh -ExecutionPolicy Bypass -File install.ps1</code></td></tr>'
html = html.replace(old_ref, new_ref)
# (g) 在 "安装检查清单" 前插入 OneClick 安装段
oneclick_section = '''
<!-- ============================================================ -->
<!-- 全自动安装 (新增 v2.0) -->
<!-- ============================================================ -->
<div class="section" id="oneclick">
<h2><span class="num" style="background:var(--green)">0</span>全自动一键安装 (推荐)</h2>
<div class="alert success">
<span class="alert-icon">&#9889;</span>
<div>
<strong>全新电脑? 从这里开始!</strong><br>
OneClick 安装器会自动完成所有步骤安装 Node.jsGitClaude Code下载 Bookworm 配置创建桌面快捷方式<br>
你只需要输入 Gitea 密码和主密码
</div>
</div>
<table>
<tr><th>系统</th><th>安装文件</th><th>安装方式</th><th>前提条件</th></tr>
<tr>
<td><strong>Windows 11</strong></td>
<td><a href="/download/Bookworm-OneClick.bat" download>Bookworm-OneClick.bat</a></td>
<td>winget 自动安装</td>
<td> (winget 内置)</td>
</tr>
<tr>
<td><strong>Windows 10</strong></td>
<td><a href="/download/Bookworm-OneClick-Win10.bat" download>Bookworm-OneClick-Win10.bat</a></td>
<td>winget 优先, 回退下载 MSI/EXE</td>
<td> (1809+)</td>
</tr>
<tr>
<td><strong>macOS 12+</strong></td>
<td><a href="/download/Bookworm-OneClick-Mac.sh" download>Bookworm-OneClick-Mac.sh</a></td>
<td>Homebrew 自动安装</td>
<td> (自动装 Homebrew)</td>
</tr>
</table>
<div class="step">
<div class="step-icon green">1</div>
<div class="step-content">
<h4>Windows: 下载并双击</h4>
<p>下载对应版本 双击运行 UAC "" 按提示输入 Gitea 密码和主密码 完成</p>
</div>
</div>
<div class="step">
<div class="step-icon green">2</div>
<div class="step-content">
<h4>macOS: 终端一行命令</h4>
<p>打开终端粘贴以下命令并回车</p>
</div>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">curl</span> -fsSL -o ~/bookworm-install.sh https://bookworm.letcareme.com/download/Bookworm-OneClick-Mac.sh && <span class="cmd">bash</span> ~/bookworm-install.sh</code>
</div>
<div class="alert info">
<span class="alert-icon">&#128161;</span>
<div>
<strong>OneClick 安装器已包含以下所有步骤</strong><br>
如果一键安装成功可跳过下方的手动步骤 1-3直接查看<a href="#daily">每日使用</a>部分
</div>
</div>
</div>
'''
# 在 "安装检查清单" 部分前插入
checklist_marker = '<!-- ============================================================ -->\n <!-- 安装检查清单 -->'
html = html.replace(checklist_marker, oneclick_section + ' ' + checklist_marker)
# (h) 更新时间首次安装 "约 10 分钟" → 区分 OneClick / 手动
old_time = '首次安装约 10 分钟(含依赖下载),之后每次双击启动约 10-30 秒'
new_time = '一键安装约 5 分钟(自动下载依赖),手动安装约 15 分钟。之后每次双击启动约 10-30 秒'
html = html.replace(old_time, new_time)
with open(guide, 'w', encoding='utf-8') as f:
f.write(html)
print(f'[OK] guide.html 已更新 ({len(html)} bytes)')
# ═══════════════════════════════════════
# 2. quick-start.html
# ═══════════════════════════════════════
qs = os.path.join(WEBDIR, 'quick-start.html')
with open(qs, 'r', encoding='utf-8') as f:
qhtml = f.read()
# (a) 版本号
qhtml = qhtml.replace('v1.5 |', 'v2.0 |')
qhtml = qhtml.replace('29 Hooks', '34 Hooks')
qhtml = qhtml.replace('Portable v1.5', 'Portable v2.0')
# (b) 更新启动流程 — 添加 OneClick
old_flow = '''<div class="flow">
<div class="flow-node">开代理</div>
<span class="flow-arrow">&#10132;</span>
<div class="flow-node">双击 <strong>Bookworm</strong></div>
<span class="flow-arrow">&#10132;</span>
<div class="flow-node">等横幅出现</div>
<span class="flow-arrow">&#10132;</span>
<div class="flow-node active">开始工作</div>
</div>'''
new_flow = '''<div class="flow">
<div class="flow-node">开代理</div>
<span class="flow-arrow">&#10132;</span>
<div class="flow-node">双击 <strong>Bookworm</strong></div>
<span class="flow-arrow">&#10132;</span>
<div class="flow-node">等横幅出现</div>
<span class="flow-arrow">&#10132;</span>
<div class="flow-node active">开始工作</div>
</div>
<div class="card highlight" style="margin-bottom:6px">
<h3>全新电脑? OneClick 一键安装器</h3>
<p>下载 <code>Bookworm-OneClick.bat</code> (Win11) / <code>*-Win10.bat</code> (Win10) / <code>*-Mac.sh</code> (macOS) 双击即装<br>
<span style="font-size:8pt">自动安装 Node.js + Git + Claude Code + Bookworm零依赖</span></p>
</div>'''
qhtml = qhtml.replace(old_flow, new_flow)
# (c) 启动表添加 OneClick 行
old_startup_table_end = '<tr><td><span class="dot red"></span><strong>命令行启动</strong></td>'
new_startup_row = '''<tr><td><span class="dot green"></span><strong>首次安装</strong></td><td>双击 <code>Bookworm-OneClick.bat</code> (全自动装依赖+配置+启动)</td></tr>
<tr><td><span class="dot red"></span><strong>命令行启动</strong></td>'''
qhtml = qhtml.replace(old_startup_table_end, new_startup_row)
with open(qs, 'w', encoding='utf-8') as f:
f.write(qhtml)
print(f'[OK] quick-start.html 已更新 ({len(qhtml)} bytes)')
# ═══════════════════════════════════════
# 3. guide-mac.html (同步版本号)
# ═══════════════════════════════════════
guide_mac = os.path.join(WEBDIR, 'guide-mac.html')
if os.path.exists(guide_mac):
with open(guide_mac, 'r', encoding='utf-8') as f:
mhtml = f.read()
mhtml = mhtml.replace('29 Hooks', '34 Hooks')
mhtml = mhtml.replace('Portable v1.5', 'Portable v2.0')
mhtml = mhtml.replace('v1.5 |', 'v2.0 |')
# 添加 OneClick Mac 下载按钮 (如有旧按钮替换)
old_mac_dl = 'Bookworm-Setup.sh'
new_mac_dl = '/download/Bookworm-OneClick-Mac.sh'
mhtml = mhtml.replace('href="/Bookworm-Setup.sh"', f'href="{new_mac_dl}"')
with open(guide_mac, 'w', encoding='utf-8') as f:
f.write(mhtml)
print(f'[OK] guide-mac.html 已更新 ({len(mhtml)} bytes)')
print('\n[DONE] 所有页面更新完成')

View File

@ -1,197 +0,0 @@
<#
.SYNOPSIS
Bookworm Portable - 仓库准备脚本
.DESCRIPTION
将当前 .claude/ 目录初始化为 Git 仓库,
排除敏感文件和大体积依赖, 推送到 Gitea.
.USAGE
# 首次准备 (初始化 + 推送)
.\prepare-repo.ps1 -GitUrl "http://8.138.11.105:3000/bookworm/bookworm-config.git"
#>
param(
[Parameter(Mandatory=$true)]
[string]$GitUrl
)
$ErrorActionPreference = "Stop"
$ClaudeDir = Join-Path $env:USERPROFILE ".claude"
Write-Host ""
Write-Host " Bookworm Portable - 仓库准备" -ForegroundColor Cyan
Write-Host " =============================" -ForegroundColor Cyan
Write-Host ""
if (-not (Test-Path $ClaudeDir)) {
Write-Host "[ERROR] .claude 目录不存在: $ClaudeDir" -ForegroundColor Red
exit 1
}
# 1. settings.template.json (由 build-portable.js 自动生成,无需手动拷贝)
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
Write-Host "[1/6] settings.template.json 由 build-portable.js 管理,跳过" -ForegroundColor Gray
# 2. 创建 .gitignore
Write-Host "[2/6] 生成 .gitignore..." -ForegroundColor White
$gitignore = @"
# ===== Bookworm Portable .gitignore =====
# 凭证与密钥 (绝不提交)
.credentials.json
.hmac-key
secrets.enc
*.key
*.pem
*.token
*.secret
.env
.env.*
# 本地运行时 (每台机器不同)
settings.json
settings.local.json
# 大体积/不可移植目录
mcp-servers/
node_modules/
file-history/
vendor/
# 大体积 Skill (含二进制/node_modules目标机按需安装)
skills/gstack/
skills/browse/
skills/*/dist/
skills/*/.git/
skills/*/node_modules/
# 测试文件 (28MB不需要部署)
hooks/__tests__/
# 缓存与临时文件
cache/
paste-cache/
sessions/
shell-snapshots/
telemetry/
repos/
plugins/
tasks/
teams/
backups/
# 调试日志 (本地生成)
debug/
# 项目级会话数据 (含用户特定路径)
projects/
# 运营敏感数据
memory/
# OS 文件
Thumbs.db
desktop.ini
.DS_Store
# 数据库和临时文件
*.sqlite
*.db
*.test.tmp
"@
Set-Content (Join-Path $ClaudeDir ".gitignore") -Value $gitignore -Encoding UTF8
Write-Host " [OK] .gitignore 已写入" -ForegroundColor Green
# 3. 初始化 Git 仓库
Write-Host "[3/6] 初始化 Git 仓库..." -ForegroundColor White
Push-Location $ClaudeDir
try {
if (-not (Test-Path ".git")) {
git init
git checkout -b main
Write-Host " [OK] Git 仓库已初始化" -ForegroundColor Green
}
else {
Write-Host " [!] 已有 .git跳过初始化" -ForegroundColor Yellow
}
# 4. 暂存文件
Write-Host "[4/6] 暂存文件..." -ForegroundColor White
git add -A
# 人工确认待提交内容
$fileList = git diff --cached --name-only
$fileCount = ($fileList | Measure-Object -Line).Lines
$sizeEstimate = git diff --cached --stat | Select-Object -Last 1
Write-Host " 待提交: $fileCount 个文件" -ForegroundColor Gray
Write-Host " $sizeEstimate" -ForegroundColor DarkGray
# 检查是否有异常大文件
$largeFiles = git diff --cached --numstat | ForEach-Object {
$parts = $_ -split '\t'
if ($parts[2]) { $parts[2] }
} | ForEach-Object {
$item = Get-Item (Join-Path $ClaudeDir $_) -ErrorAction SilentlyContinue
$size = if ($item) { $item.Length } else { 0 }
if ($size -and $size -gt 1MB) {
[PSCustomObject]@{ File = $_; SizeMB = [math]::Round($size / 1MB, 1) }
}
}
if ($largeFiles) {
Write-Host ""
Write-Host " [!] 发现大文件 (>1MB):" -ForegroundColor Yellow
$largeFiles | ForEach-Object { Write-Host " $($_.SizeMB)MB $($_.File)" -ForegroundColor Yellow }
Write-Host ""
}
$confirm = Read-Host " 确认提交以上文件? (y/n)"
if ($confirm -ne 'y') {
Write-Host " 已取消" -ForegroundColor Yellow
exit 0
}
# 5. 提交
Write-Host "[5/6] 提交..." -ForegroundColor White
git commit -m "Bookworm v6.5.1 portable commit
Includes: CLAUDE.md, skills (92), agents (18), hooks (29),
scripts, constitution
Excludes: credentials, mcp-servers, node_modules, cache,
sessions, debug logs, project-specific data, gstack/browse binaries"
Write-Host " [OK] 提交完成" -ForegroundColor Green
# 6. 推送到 Gitea
Write-Host "[6/6] 推送到 Gitea..." -ForegroundColor White
$remoteExists = git remote -v 2>$null | Select-String "origin"
if (-not $remoteExists) {
git remote add origin $GitUrl
}
else {
git remote set-url origin $GitUrl
}
Write-Host " 推送到: $GitUrl" -ForegroundColor Gray
git push -u origin main 2>&1 | ForEach-Object { Write-Host " $_" }
} finally {
Pop-Location
}
# 统计
$repoSize = git -C $ClaudeDir count-objects -vH 2>$null | Select-String "size-pack"
Write-Host ""
Write-Host " ╔══════════════════════════════════════════╗" -ForegroundColor Green
Write-Host " ║ 仓库准备完成! ║" -ForegroundColor Green
Write-Host " ╠══════════════════════════════════════════╣" -ForegroundColor Green
Write-Host " ║ 远程: $GitUrl" -ForegroundColor Green
Write-Host "$repoSize" -ForegroundColor Green
Write-Host " ╚══════════════════════════════════════════╝" -ForegroundColor Green
Write-Host ""
Write-Host " 下一步:" -ForegroundColor Yellow
Write-Host " 1. 运行 .\encrypt-secrets.ps1 创建加密凭证" -ForegroundColor Yellow
Write-Host " 2. 将 USB 内容复制到 U 盘:" -ForegroundColor Yellow
Write-Host " install.ps1 + stop.ps1 + secrets.enc" -ForegroundColor Yellow
Write-Host " 3. 在目标机运行: .\install.ps1" -ForegroundColor Yellow

View File

@ -47,7 +47,7 @@
# [1/6] 前置检查 (缺依赖自动提示 winget 安装)
# [2/6] 代理自动检测 (无需手动找端口)
# [3/6] 解密凭证 (输入主密码, 最多3次重试, 可选本日免密)
# [4/6] 同步配置 (git clone 92 Skills / 18 Agents / 34 Hooks)
# [4/6] 同步配置 (git clone 92 Skills / 18 Agents / 29 Hooks)
# [5/6] 渲染模板 + 初始化 + Bookworm 完整性验证 + MCP 检查
# [6/6] 启动 Claude Code
@ -146,30 +146,30 @@
---- 6.2 更新 boot 仓库脚本 ----
cd C:\Users\leesu\AppData\Local\Temp\bookworm-boot
cp C:\Users\leesu\Documents\bookworm-portable\install.ps1 .
cp C:\Users\leesu\Documents\bookworm-portable\stop.ps1 .
cp C:\Users\leesu\Documents\bookworm-portable\guide.html .
cp C:\Users\leesu\Documents\bookworm-portable\secrets.enc .
cp "C:\Users\leesu\Documents\bookworm-portable\启动Bookworm.bat" .
cp "C:\Users\leesu\Documents\bookworm-portable\更新并启动Bookworm.bat" .
cp "C:\Users\leesu\Documents\bookworm-portable\卸载Bookworm.bat" .
cp "C:\Users\leesu\Documents\bookworm-portable\Bookworm-Setup.bat" .
cp C:\Users\leesu\Desktop\bookworm-portable\install.ps1 .
cp C:\Users\leesu\Desktop\bookworm-portable\stop.ps1 .
cp C:\Users\leesu\Desktop\bookworm-portable\guide.html .
cp C:\Users\leesu\Desktop\bookworm-portable\secrets.enc .
cp "C:\Users\leesu\Desktop\bookworm-portable\启动Bookworm.bat" .
cp "C:\Users\leesu\Desktop\bookworm-portable\更新并启动Bookworm.bat" .
cp "C:\Users\leesu\Desktop\bookworm-portable\卸载Bookworm.bat" .
cp "C:\Users\leesu\Desktop\bookworm-portable\Bookworm-Setup.bat" .
git add -A
git commit -m "update boot scripts"
git push https://bookworm:[密码]@code.letcareme.com/bookworm/bookworm-boot.git main
---- 6.3 重新加密凭证 ----
pwsh -ExecutionPolicy Bypass -File C:\Users\leesu\Documents\bookworm-portable\encrypt-secrets.ps1
pwsh -ExecutionPolicy Bypass -File C:\Users\leesu\Desktop\bookworm-portable\encrypt-secrets.ps1
# 加密完成后同步到 boot 仓库 (参照 6.2)
---- 6.4 解密验证 ----
pwsh -ExecutionPolicy Bypass -File C:\Users\leesu\Documents\bookworm-portable\encrypt-secrets.ps1 -Decrypt
pwsh -ExecutionPolicy Bypass -File C:\Users\leesu\Desktop\bookworm-portable\encrypt-secrets.ps1 -Decrypt
---- 6.5 更新 Bookworm Web 下载页的安装器 ----
scp C:\Users\leesu\Documents\bookworm-portable\Bookworm-Setup.bat root@8.138.11.105:/opt/bookworm-web/public/
scp C:\Users\leesu\Desktop\bookworm-portable\Bookworm-Setup.bat root@8.138.11.105:/opt/bookworm-web/public/
================================================================================
七、SSH 服务器管理
@ -279,6 +279,7 @@
├── deploy-gitea.sh Gitea 部署 (ECS)
├── setup-https.sh HTTPS 配置 (ECS)
├── secure-firewall.sh 防火墙加固 (ECS)
├── settings.template.json settings.json 模板
├── settings.local.template.json settings.local.json 模板
├── guide.html HTML 保姆式教程
├── quick-reference.txt 本文档

View File

@ -74,31 +74,7 @@
<div class="header">
<h1>Bookworm <span>Portable</span> 日常速查卡</h1>
<div class="ver">v1.5 | 92 Skills / 18 Agents / 34 Hooks<br>打印后贴在显示器旁边</div>
</div>
<!-- ==================== 安装前准备 ==================== -->
<div class="section">
<span class="section-title red">安装前准备 (仅首次)</span>
<div class="card warn">
<h3>&#9888; 首次运行 Bookworm-Setup.exe 前,请确认以下两项</h3>
<table>
<tr><th style="width:30%">检查项</th><th>操作</th></tr>
<tr>
<td><strong>1. PowerShell 执行策略</strong></td>
<td><kbd>Win</kbd>+<kbd>R</kbd> 输入 <code>powershell</code> 回车,执行:<br>
<code>Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force</code><br>
<span style="font-size:8pt;color:#64748b">否则 npm/node 脚本会被系统阻止运行</span></td>
</tr>
<tr>
<td><strong>2. 代理已开启</strong></td>
<td>启动 Clash / V2Ray / 快柠檬(任选一个),安装器需要下载依赖</td>
</tr>
</table>
<p style="font-size:8pt;color:#64748b;margin-top:4px">如果电脑没有 wingetWin10 旧版本),安装器可能无法自动装 Node.js 和 Git。请手动安装<br>
Node.js &rarr; <code>https://nodejs.org</code> &nbsp;|&nbsp; Git &rarr; <code>https://git-scm.com</code></p>
</div>
<div class="ver">v1.5 | 92 Skills / 18 Agents / 29 Hooks<br>打印后贴在显示器旁边</div>
</div>
<!-- ==================== 打开 PowerShell ==================== -->

View File

@ -1,87 +0,0 @@
#!/bin/bash
# ============================================================
# Bookworm Portable - 防火墙加固 (P1-2)
# 配置 UFW + fail2ban 保护 Gitea
# ============================================================
# 用法: ssh root@8.138.11.105 'bash -s' < secure-firewall.sh
# ============================================================
set -euo pipefail
echo "========================================="
echo " Bookworm 防火墙加固 v1.0"
echo "========================================="
# 1. 确保 Gitea 只监听 127.0.0.1 (Nginx 反代已处理外部访问)
GITEA_INI="/var/lib/gitea/custom/conf/app.ini"
if [ -f "$GITEA_INI" ]; then
if grep -q "^HTTP_ADDR.*=.*127.0.0.1" "$GITEA_INI"; then
echo "[1/3] Gitea 已绑定 127.0.0.1"
else
echo "[1/3] 配置 Gitea 仅本地监听..."
sed -i "s/^HTTP_ADDR\s*=.*/HTTP_ADDR = 127.0.0.1/" "$GITEA_INI" 2>/dev/null || \
sed -i "/^\[server\]/a HTTP_ADDR = 127.0.0.1" "$GITEA_INI"
systemctl restart gitea
echo " [OK] Gitea 已限制为本地监听"
fi
else
echo "[1/3] [!] Gitea 配置不存在,跳过"
fi
# 2. 安装配置 fail2ban
echo "[2/3] 配置 fail2ban..."
if ! command -v fail2ban-client &>/dev/null; then
apt-get update -qq && apt-get install -y -qq fail2ban
fi
# Gitea 登录失败过滤器
cat > /etc/fail2ban/filter.d/gitea.conf << 'EOF'
[Definition]
failregex = .*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from <HOST>
ignoreregex =
EOF
# Gitea jail 配置
cat > /etc/fail2ban/jail.d/gitea.conf << EOF
[gitea]
enabled = true
port = http,https
filter = gitea
logpath = /var/lib/gitea/log/gitea.log
maxretry = 5
findtime = 3600
bantime = 86400
action = iptables-multiport[name=gitea, port="http,https"]
EOF
systemctl restart fail2ban
echo " [OK] fail2ban 已配置 (5次失败/小时 → 封禁24小时)"
# 3. 输出阿里云安全组配置指引
echo "[3/3] 安全组配置指引..."
echo ""
echo "========================================="
echo " 阿里云安全组配置 (手动操作)"
echo "========================================="
echo ""
echo " 1. 登录阿里云控制台 → ECS → 安全组"
echo " 2. 找到实例 8.138.11.105 所在安全组"
echo " 3. 添加入方向规则:"
echo ""
echo " ┌────────────┬──────────┬───────────────────────────┐"
echo " │ 端口 │ 协议 │ 授权对象 │"
echo " ├────────────┼──────────┼───────────────────────────┤"
echo " │ 443/443 │ TCP │ 0.0.0.0/0 (HTTPS 公开) │"
echo " │ 80/80 │ TCP │ 0.0.0.0/0 (重定向用) │"
echo " │ 22/22 │ TCP │ 你的 IP/32 │"
echo " ├────────────┼──────────┼───────────────────────────┤"
echo " │ 3300/3300 │ TCP │ 拒绝 0.0.0.0/0 │"
echo " │ │ │ (Gitea 仅本地,不需公开) │"
echo " └────────────┴──────────┴───────────────────────────┘"
echo ""
echo " 4. 删除任何允许 3300 端口公开访问的规则"
echo ""
echo " 查看你当前的公网 IP:"
echo " curl -s ifconfig.me"
echo ""
echo "========================================="

View File

@ -1,67 +0,0 @@
{
"permissions": {
"allow": [
"Bash(ssh root@8.138.11.105:*)",
"Bash(scp *root@8.138.11.105:*)",
"Bash(echo:*)",
"Bash(ls:*)",
"Bash(claude config:*)",
"Bash(claude mcp:*)",
"Bash(node:*)",
"Bash(node --version:*)",
"Bash(node --check:*)",
"Bash(uv pip:*)",
"Bash(xargs grep:*)",
"Bash(for f:*)",
"Bash(do echo:*)",
"Bash(done)",
"mcp__playwright__browser_navigate",
"mcp__playwright__browser_click",
"mcp__playwright__browser_take_screenshot",
"mcp__playwright__browser_resize",
"mcp__playwright__browser_snapshot",
"mcp__playwright__browser_fill_form",
"mcp__playwright__browser_evaluate",
"mcp__playwright__browser_wait_for",
"mcp__playwright__browser_type",
"mcp__chrome-devtools__list_pages",
"mcp__chrome-devtools__navigate_page",
"mcp__chrome-devtools__emulate",
"mcp__chrome-devtools__take_screenshot",
"mcp__chrome-devtools__new_page",
"mcp__chrome-devtools__evaluate_script",
"mcp__chrome-devtools__list_network_requests",
"mcp__chrome-devtools__performance_start_trace",
"mcp__chrome-devtools__performance_stop_trace",
"mcp__chrome-devtools__take_snapshot"
]
},
"mcpServers": {
"context7": {
"command": "npx",
"args": ["-y", "@upstash/context7-mcp@latest"]
},
"sequential-thinking": {
"command": "npx",
"args": ["-y", "@anthropic-ai/mcp-sequential-thinking@latest"]
},
"playwright": {
"command": "npx",
"args": ["-y", "@anthropic-ai/mcp-playwright@latest"]
},
"firecrawl": {
"command": "npx",
"args": ["-y", "firecrawl-mcp@latest"],
"env": {
"FIRECRAWL_API_KEY": "${FIRECRAWL_API_KEY}"
}
},
"github": {
"command": "npx",
"args": ["-y", "@anthropic-ai/mcp-github@latest"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
}
}
}
}

163
settings.template.json Normal file
View File

@ -0,0 +1,163 @@
{
"env": {
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1",
"HOME": "{{HOME}}",
"SUPABASE_ACCESS_TOKEN": "${SUPABASE_ACCESS_TOKEN}"
},
"permissions": {
"allow": [
"Read",
"Glob",
"Grep",
"WebSearch",
"WebFetch",
"Skill",
"Task",
"TaskCreate",
"TaskUpdate",
"TaskList",
"TaskGet",
"AskUserQuestion"
]
},
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "node {{CLAUDE_ROOT}}/hooks/prompt-dispatcher.js",
"timeout": 3000
}
]
},
{
"hooks": [
{
"type": "command",
"command": "node {{CLAUDE_ROOT}}/hooks/clipboard-image-hook.js",
"timeout": 5000
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node {{CLAUDE_ROOT}}/hooks/bash-precheck-dispatcher.js",
"timeout": 5000
}
]
},
{
"matcher": "Skill",
"hooks": [
{
"type": "command",
"command": "node {{CLAUDE_ROOT}}/hooks/route-compliance-gate.js",
"timeout": 2000
}
]
},
{
"matcher": "mcp__",
"hooks": [
{
"type": "command",
"command": "node {{CLAUDE_ROOT}}/hooks/mcp-safety-gate.js",
"timeout": 3000
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write|NotebookEdit",
"hooks": [
{
"type": "command",
"command": "node {{CLAUDE_ROOT}}/hooks/post-edit-dispatcher.js",
"timeout": 8000
}
]
},
{
"matcher": "Skill|Agent",
"hooks": [
{
"type": "command",
"command": "node {{CLAUDE_ROOT}}/hooks/memory-persistence-trigger.js",
"timeout": 2000
}
]
},
{
"matcher": "Edit|Write|Skill|Agent|Bash|mcp__.*",
"hooks": [
{
"type": "command",
"command": "node {{CLAUDE_ROOT}}/hooks/activity-logger.js",
"timeout": 2000
}
]
},
{
"matcher": "Edit|Write|Skill|Agent|Bash|mcp__.*",
"hooks": [
{
"type": "command",
"command": "node {{CLAUDE_ROOT}}/hooks/session-heartbeat.js",
"timeout": 2000
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node {{CLAUDE_ROOT}}/hooks/build-outcome-tracker.js",
"timeout": 3000
}
]
},
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "node {{CLAUDE_ROOT}}/hooks/constitution-delivery-reminder.js",
"timeout": 3000
}
]
}
],
"SubagentStart": [
{
"hooks": [
{
"type": "command",
"command": "node {{CLAUDE_ROOT}}/hooks/subagent-route-injector.js",
"timeout": 2000
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node {{CLAUDE_ROOT}}/hooks/stop-dispatcher.js 2>>{{CLAUDE_ROOT}}/debug/hook-errors.log || true",
"timeout": 5000
}
]
}
]
},
"effortLevel": "high",
"skipDangerousModePermissionPrompt": false
}

View File

@ -1,616 +0,0 @@
#!/usr/bin/env node
'use strict';
/**
* Bookworm Portable - 全自动安装引擎 v3.0
* @module setup-all
*/
// W3: Node.js 版本检查
if (parseInt(process.versions.node) < 14) {
console.error(' [!!] Node.js 版本过低 (' + process.version + '), 需要 14.0+');
console.error(' 请更新: https://nodejs.org/');
process.exit(1);
}
const { execSync, spawnSync, spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const crypto = require('crypto');
const os = require('os');
// ─── 配置 ───
const HOME = os.homedir();
const BOOT_DIR = path.join(HOME, 'bookworm-boot');
const CLAUDE_DIR = path.join(HOME, '.claude');
const GITEA_BOOT = 'https://code.letcareme.com/bookworm/bookworm-boot.git';
const GITEA_CONFIG = 'https://code.letcareme.com/bookworm/bookworm-config.git';
const NPM_MIRROR = 'https://registry.npmmirror.com';
const SCRIPT_DIR = __dirname;
// ─── 颜色输出 ───
const c = {
reset: '\x1b[0m', bold: '\x1b[1m',
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
blue: '\x1b[34m', cyan: '\x1b[36m', dim: '\x1b[90m',
};
function ok(msg) { console.log(` ${c.green}[OK]${c.reset} ${msg}`); }
function warn(msg) { console.log(` ${c.yellow}[!]${c.reset} ${msg}`); }
function fail(msg) { console.log(` ${c.red}[!!]${c.reset} ${msg}`); }
function info(msg) { console.log(` ${c.dim}[..]${c.reset} ${msg}`); }
function step(n, total, msg) {
console.log(`\n ${c.bold}[${n}/${total}]${c.reset} ${c.cyan}${msg}${c.reset}`);
}
// ─── 工具函数 ───
function hasCmd(cmd) {
if (!/^[a-zA-Z0-9._-]+$/.test(cmd)) return false; // B3: 防命令注入
try {
execSync(`where ${cmd}`, { stdio: 'pipe' });
return true;
} catch { return false; }
}
function run(cmd, opts = {}) {
try {
return execSync(cmd, {
stdio: opts.silent ? 'pipe' : 'inherit',
encoding: 'utf8',
timeout: opts.timeout || 300000,
env: { ...process.env, ...opts.env },
...opts,
});
} catch (e) {
if (opts.ignoreError) return '';
throw e;
}
}
function runSilent(cmd) {
try {
return execSync(cmd, { stdio: 'pipe', encoding: 'utf8', timeout: 60000 });
} catch { return ''; }
}
function wingetInstall(id, name) {
if (!hasCmd('winget')) {
warn(`winget 不可用, 请手动安装 ${name}`);
return false;
}
info(`通过 winget 安装 ${name}...`);
try {
run(`winget install ${id} --accept-source-agreements --accept-package-agreements --silent`, { timeout: 600000 });
refreshPath();
ok(`${name} 安装成功`);
return true;
} catch (e) {
fail(`${name} 安装失败: ${e.message}`);
return false;
}
}
function refreshPath() {
try {
const sysPath = runSilent('reg query "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment" /v Path')
.match(/REG_\w+\s+(.+)/)?.[1] || '';
const usrPath = runSilent('reg query "HKCU\\Environment" /v Path')
.match(/REG_\w+\s+(.+)/)?.[1] || '';
const extra = [
'C:\\Program Files\\nodejs',
'C:\\Program Files\\Git\\cmd',
'C:\\Program Files\\Git\\usr\\bin',
'C:\\Program Files\\PowerShell\\7',
path.join(HOME, 'AppData\\Local\\Microsoft\\WinGet\\Packages'),
path.join(HOME, 'AppData\\Roaming\\npm'),
].join(';');
// I2: 动态扫描 Python 安装路径
const pyBase = path.join(HOME, 'AppData', 'Local', 'Programs', 'Python');
let pyPaths = '';
try {
if (fs.existsSync(pyBase)) {
for (const d of fs.readdirSync(pyBase)) {
pyPaths += ';' + path.join(pyBase, d) + ';' + path.join(pyBase, d, 'Scripts');
}
}
} catch {}
process.env.PATH = `${sysPath};${usrPath};${extra}${pyPaths}`;
} catch {}
}
function askPassword(prompt) {
return new Promise((resolve) => {
process.stdout.write(prompt);
// Windows: 用 PowerShell 隐藏输入
try {
const result = execSync(
'powershell -Command "[Runtime.InteropServices.Marshal]::PtrToStringBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR((Read-Host -AsSecureString)))"',
{ stdio: ['inherit', 'pipe', 'pipe'], encoding: 'utf8', timeout: 120000 }
).trim();
resolve(result);
} catch {
// W5: 回退明文输入, 给出警告
warn('PowerShell 不可用, 密码将以明文显示');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
rl.question('', (ans) => { rl.close(); resolve(ans.trim()); });
}
});
}
// ─── 加密/解密 (与 crypto-helper.js 同格式) ───
function decryptSecrets(filePath, password) {
const data = fs.readFileSync(filePath);
const magic = data.slice(0, 6).toString();
if (magic !== 'BWENC1') throw new Error('WRONG_FORMAT');
const salt = data.slice(6, 22);
const encrypted = data.slice(22);
const derived = crypto.pbkdf2Sync(password, salt, 600000, 48, 'sha256');
const key = derived.slice(0, 32);
const iv = derived.slice(32, 48);
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
try {
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8');
} catch {
throw new Error('WRONG_PASSWORD');
}
}
function escVbs(s) { return s.replace(/"/g, '""'); } // B4: 防 VBS 注入
function createShortcut(name, target, workDir) {
const vbs = path.join(os.tmpdir(), `bw_sc_${Date.now()}.vbs`);
const desktop = path.join(HOME, 'Desktop');
const lnkPath = escVbs(path.join(desktop, name + '.lnk'));
fs.writeFileSync(vbs, `Set ws = CreateObject("WScript.Shell")
Set sc = ws.CreateShortcut("${lnkPath}")
sc.TargetPath = "${escVbs(target)}"
sc.WorkingDirectory = "${escVbs(workDir)}"
sc.Description = "Bookworm Smart Assistant"
sc.Save
`);
try { execSync(`cscript //nologo "${vbs}"`, { stdio: 'pipe' }); return true; }
catch { return false; }
finally { try { fs.unlinkSync(vbs); } catch {} }
}
// W8: 动态检测 Git Bash 路径
function findGitBash() {
const candidates = ['C:\\Program Files\\Git\\bin\\bash.exe', 'C:\\Program Files (x86)\\Git\\bin\\bash.exe'];
try {
const gitPath = runSilent('where git').trim().split('\n')[0];
if (gitPath) candidates.unshift(path.join(path.dirname(path.dirname(gitPath)), 'bin', 'bash.exe'));
} catch {}
return candidates.find(p => fs.existsSync(p)) || candidates[0];
}
// ─── Banner ───
function banner() {
console.log(`
${c.cyan}
____ _
| __ ) ___ ___ | | ____ _____ _ __ ___
| _ \\ / _ \\ / _ \\| |/ /\\ \\ /\\ / / _ \\| '__|
| |_) | (_) | (_) | < \\ V V / (_) | |
|____/ \\___/ \\___/|_|\\_\\ \\_/\\_/ \\___/|_|
${c.bold}全自动安装引擎 v3.0${c.cyan}
双击即装: Node + Git + Python + PS7 + Claude + MCP
${c.reset}
`);
}
// ═══════════════════════════════════════════
// 主流程
// ═══════════════════════════════════════════
async function main() {
// ─── 启动模式: 已安装过 → 静默更新 + 直接启动 ───
const isInstalled = hasCmd('claude') && fs.existsSync(path.join(CLAUDE_DIR, 'CLAUDE.md'));
const startOnly = process.argv.includes('--start') || process.argv.includes('-s');
if (isInstalled && startOnly) {
return quickStart();
}
banner();
const TOTAL = 9;
let errors = 0;
// ─── 1. Git ───
step(1, TOTAL, '安装 Git');
if (hasCmd('git')) {
ok('Git 已安装');
} else {
if (!wingetInstall('Git.Git', 'Git')) errors++;
}
// ─── 2. Python ───
step(2, TOTAL, '安装 Python');
if (hasCmd('python') || hasCmd('python3') || hasCmd('py')) {
ok('Python 已安装');
} else {
if (!wingetInstall('Python.Python.3.12', 'Python 3.12')) errors++;
refreshPath();
}
// ─── 3. PowerShell 7 ───
step(3, TOTAL, '安装 PowerShell 7');
if (hasCmd('pwsh')) {
ok('PowerShell 7 已安装');
} else {
if (wingetInstall('Microsoft.PowerShell', 'PowerShell 7')) {
// 设 PS7 为 Windows Terminal 默认配置文件
try {
const wtSettings = path.join(HOME, 'AppData', 'Local', 'Packages',
'Microsoft.WindowsTerminal_8wekyb3d8bbwe', 'LocalState', 'settings.json');
if (fs.existsSync(wtSettings)) {
let wt = JSON.parse(fs.readFileSync(wtSettings, 'utf8'));
// 找到 PS7 的 profile GUID
const ps7Profile = (wt.profiles?.list || []).find(p =>
p.source === 'Windows.Terminal.PowershellCore' || (p.name || '').includes('PowerShell 7')
);
if (ps7Profile && ps7Profile.guid) {
wt.defaultProfile = ps7Profile.guid;
fs.writeFileSync(wtSettings, JSON.stringify(wt, null, 4));
ok('PS7 已设为 Windows Terminal 默认终端');
}
}
} catch { warn('Windows Terminal 默认配置未修改 - 不影响使用'); }
} else {
errors++;
}
}
// ─── 4. Claude Code ───
step(4, TOTAL, '安装 Claude Code');
// I3: 用 --registry 参数, 不污染全局 .npmrc
if (hasCmd('claude')) {
ok('Claude Code 已安装');
} else {
info('通过 npm 安装 Claude Code - 淘宝镜像加速...');
try {
run(`npm i -g @anthropic-ai/claude-code --registry ${NPM_MIRROR}`, { timeout: 600000 });
ok('Claude Code 安装成功');
} catch {
fail('Claude Code 安装失败');
errors++;
}
}
// ─── 5. 克隆 Bookworm 配置 ───
step(5, TOTAL, '同步 Bookworm 配置');
// 设置 git credential helper
run('git config --global credential.helper manager', { ignoreError: true, silent: true });
// 克隆 bookworm-boot
if (fs.existsSync(path.join(BOOT_DIR, '.git'))) {
info('bookworm-boot 已存在, 更新...');
run('git pull', { cwd: BOOT_DIR, ignoreError: true });
} else {
info('首次下载 bookworm-boot (需输入 Gitea 用户名密码)...');
try {
run(`git clone "${GITEA_BOOT}" "${BOOT_DIR}"`);
ok('bookworm-boot 克隆成功');
} catch {
fail('bookworm-boot 克隆失败 - 检查网络和 Gitea 凭证');
errors++;
}
}
// 克隆 bookworm-config → ~/.claude
if (fs.existsSync(path.join(CLAUDE_DIR, '.git', 'config'))) {
info('.claude 配置已存在, 更新...');
try {
const stashOut = run('git stash', { cwd: CLAUDE_DIR, ignoreError: true, silent: true }) || '';
run('git pull --rebase', { cwd: CLAUDE_DIR, ignoreError: true, silent: true });
if (stashOut.includes('Saved working directory')) {
run('git stash pop', { cwd: CLAUDE_DIR, ignoreError: true, silent: true });
}
} catch {}
} else if (!fs.existsSync(path.join(CLAUDE_DIR, 'CLAUDE.md'))) {
info('首次下载 .claude 配置...');
// 备份现有
if (fs.existsSync(CLAUDE_DIR)) {
const backup = CLAUDE_DIR + '.bak.' + Date.now();
fs.renameSync(CLAUDE_DIR, backup);
ok(`现有 .claude 已备份到 ${path.basename(backup)}`);
}
try {
run(`git clone --depth 1 "${GITEA_CONFIG}" "${CLAUDE_DIR}"`);
ok('.claude 配置克隆成功');
} catch {
fail('.claude 配置克隆失败');
errors++;
}
} else {
ok('.claude 配置已存在');
}
// 确保本地目录
for (const d of ['debug', 'sessions', 'cache', 'backups', 'memory', 'projects']) {
const p = path.join(CLAUDE_DIR, d);
if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
}
// ─── 6. 凭证解密 ───
step(6, TOTAL, '解密凭证');
const secretsFile = path.join(BOOT_DIR, 'secrets.enc');
if (!fs.existsSync(secretsFile)) {
warn('secrets.enc 不存在, 跳过凭证解密');
} else if (process.env.ANTHROPIC_API_KEY) {
ok('API Key 已设置 (缓存有效)');
} else {
let decrypted = false;
for (let attempt = 1; attempt <= 3; attempt++) {
const label = attempt > 1
? ` 重新输入主密码 (第 ${attempt}/3 次): `
: ' 输入主密码解密凭证: ';
const password = await askPassword(label);
try {
const text = decryptSecrets(secretsFile, password);
// 注入环境变量 (B5: 不打印 key 名称, 防截屏泄露)
let injectedCount = 0;
for (const line of text.split('\n')) {
const trimmed = line.trim();
if (!trimmed || !trimmed.includes('=')) continue;
const eqIdx = trimmed.indexOf('=');
const key = trimmed.slice(0, eqIdx).trim();
const val = trimmed.slice(eqIdx + 1).trim();
if (key && val) { process.env[key] = val; injectedCount++; }
}
ok(`已注入 ${injectedCount} 个环境变量`);
decrypted = true;
break;
} catch (e) {
if (e.message === 'WRONG_PASSWORD') {
const remaining = 3 - attempt;
if (remaining > 0) fail(`密码错误, 剩余重试: ${remaining}`);
} else if (e.message === 'WRONG_FORMAT') {
fail('secrets.enc 格式不兼容, 请联系管理员');
break;
}
}
}
if (!decrypted) {
fail('凭证解密失败 — Claude Code 将以登录模式启动');
errors++;
}
}
// 渲染 settings.json
const templateFile = path.join(CLAUDE_DIR, 'settings.template.json');
const settingsFile = path.join(CLAUDE_DIR, 'settings.json');
if (fs.existsSync(templateFile)) {
let tpl = fs.readFileSync(templateFile, 'utf8');
const claudeRoot = CLAUDE_DIR.replace(/\\/g, '/');
tpl = tpl.replace(/\{\{CLAUDE_ROOT\}\}/g, claudeRoot);
tpl = tpl.replace(/\{\{HOME\}\}/g, HOME.replace(/\\/g, '/')); // B6: 统一正斜杠
fs.writeFileSync(settingsFile, tpl);
ok('settings.json 已渲染');
}
// 渲染 settings.local.template.json
const localTpl = path.join(CLAUDE_DIR, 'settings.local.template.json');
const localSet = path.join(CLAUDE_DIR, 'settings.local.json');
if (fs.existsSync(localTpl)) {
let tpl = fs.readFileSync(localTpl, 'utf8');
tpl = tpl.replace(/\{\{CLAUDE_ROOT\}\}/g, CLAUDE_DIR.replace(/\\/g, '/'));
tpl = tpl.replace(/\{\{HOME\}\}/g, HOME.replace(/\\/g, '/')); // B6: 统一正斜杠
tpl = tpl.replace(/\{\{USERNAME\}\}/g, os.userInfo().username);
fs.writeFileSync(localSet, tpl);
ok('settings.local.json 已渲染');
}
// ─── 7. MCP + hooks 依赖 ───
step(7, TOTAL, 'MCP 与 hooks 依赖');
// MCP 配置写入 ~/.claude.json (Claude Code 的全局 MCP 存储位置)
const claudeJson = path.join(HOME, '.claude.json');
try {
let globalCfg = {};
if (fs.existsSync(claudeJson)) {
globalCfg = JSON.parse(fs.readFileSync(claudeJson, 'utf8'));
}
// 基础 MCP 列表 (npx 方式, 无需预装, 首次调用自动下载)
const baseMcps = {
'context7': {
command: 'npx.cmd', args: ['-y', '@upstash/context7-mcp@latest'], type: 'stdio'
},
'sequential-thinking': {
command: 'npx.cmd', args: ['-y', '@modelcontextprotocol/server-sequential-thinking@latest'], type: 'stdio'
},
'playwright': {
command: 'npx.cmd', args: ['-y', '@playwright/mcp@latest', '--headless'], type: 'stdio'
},
'firecrawl': {
command: 'npx.cmd', args: ['-y', 'firecrawl-mcp'], type: 'stdio',
env: { FIRECRAWL_API_KEY: '${FIRECRAWL_API_KEY}' }
},
'github': {
command: 'npx.cmd', args: ['-y', '@modelcontextprotocol/server-github'], type: 'stdio',
env: { GITHUB_PERSONAL_ACCESS_TOKEN: '${GITHUB_PERSONAL_ACCESS_TOKEN}' }
},
'linear': { type: 'http', url: 'https://mcp.linear.app/mcp' },
'figma': { type: 'http', url: 'https://mcp.figma.com/mcp' },
'supabase': { type: 'http', url: 'https://mcp.supabase.com/mcp?project_ref=oepmihbtoylosbsxlmfo' },
};
// 合并: 不覆盖用户已有配置
if (!globalCfg.mcpServers) globalCfg.mcpServers = {};
let added = 0;
for (const [name, cfg] of Object.entries(baseMcps)) {
if (!globalCfg.mcpServers[name]) {
globalCfg.mcpServers[name] = cfg;
added++;
}
}
fs.writeFileSync(claudeJson, JSON.stringify(globalCfg, null, 2));
ok(`MCP 配置已写入 ~/.claude.json (新增 ${added} 个, 总计 ${Object.keys(globalCfg.mcpServers).length} 个)`);
} catch (e) {
warn('MCP 配置写入失败: ' + e.message);
}
// npm install in .claude for hooks
if (fs.existsSync(path.join(CLAUDE_DIR, 'package.json'))) {
info('安装 hooks 依赖...');
run(`npm install --omit=dev --registry ${NPM_MIRROR}`, { cwd: CLAUDE_DIR, ignoreError: true, silent: true });
ok('hooks 依赖已安装');
}
// Python MCP 依赖
const pyCmd = hasCmd('python') ? 'python' : (hasCmd('python3') ? 'python3' : (hasCmd('py') ? 'py' : null));
if (pyCmd) {
info('安装 Python MCP 依赖...');
run(`${pyCmd} -m pip install askui scrapling --quiet`, { ignoreError: true, silent: true });
ok('Python MCP 依赖已安装');
}
// ─── 8. 桌面快捷方式 ───
step(8, TOTAL, '创建桌面快捷方式');
// 启动脚本: 用 pwsh/powershell 运行 install.ps1
const launchBat = path.join(BOOT_DIR, '启动Bookworm.bat');
if (fs.existsSync(launchBat)) {
if (createShortcut('Bookworm', launchBat, BOOT_DIR)) ok('Bookworm 快捷方式');
} else {
// 回退: 直接创建 claude 启动快捷方式
const claudePath = runSilent('where claude').trim().split('\n')[0];
if (claudePath) {
if (createShortcut('Bookworm', claudePath, HOME)) ok('Bookworm 快捷方式');
}
}
const updateBat = path.join(BOOT_DIR, '更新并启动Bookworm.bat');
if (fs.existsSync(updateBat)) {
if (createShortcut('更新Bookworm', updateBat, BOOT_DIR)) ok('更新Bookworm 快捷方式');
}
// ─── 9. 完成 ───
step(9, TOTAL, '安装完成');
console.log(`
${c.green}
安装完成!
[v] Node.js [v] Git [v] Python
[v] PS7 [v] Claude [v] MCP
[v] Bookworm - 92 Skills / 18 Agents
桌面快捷方式: Bookworm / 更新Bookworm
${c.reset}
`);
if (errors > 0) {
warn(`安装过程中有 ${errors} 个警告, 请查看上方日志`);
}
// 打开使用教程
const guide = path.join(BOOT_DIR, 'guide.html');
if (fs.existsSync(guide)) {
try { execSync(`start "" "${guide}"`, { stdio: 'pipe' }); } catch {}
}
// 询问是否启动
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
rl.question('\n 按回车启动 Bookworm (或输入 n 退出): ', (ans) => {
rl.close();
if (ans.trim().toLowerCase() === 'n') return;
console.log(`\n ${c.cyan}正在启动 Claude Code...${c.reset}\n`);
// 设置必要环境变量
const env = { ...process.env };
// W10: 追加而非覆盖用户已有 NO_PROXY
const existingNoProxy = process.env.NO_PROXY || process.env.no_proxy || '';
const bwDomains = 'bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1';
env.NO_PROXY = existingNoProxy ? `${existingNoProxy},${bwDomains}` : bwDomains;
env.CLAUDE_CODE_GIT_BASH_PATH = findGitBash();
const child = spawn('claude', [], {
stdio: 'inherit',
env,
cwd: HOME,
shell: true,
});
child.on('exit', () => process.exit(0));
});
}
// ═══════════════════════════════════════════
// 快速启动模式: 静默更新 + 直接启动 (已安装过的机器)
// ═══════════════════════════════════════════
async function quickStart() {
console.log(` ${c.cyan}Bookworm 快速启动${c.reset} — 检查更新中...`);
let updated = false;
// 1. 静默更新 bookworm-boot
if (fs.existsSync(path.join(BOOT_DIR, '.git'))) {
try {
const before = runSilent(`git -C "${BOOT_DIR}" rev-parse HEAD`).trim();
run(`git -C "${BOOT_DIR}" pull --ff-only`, { ignoreError: true, silent: true, timeout: 15000 });
const after = runSilent(`git -C "${BOOT_DIR}" rev-parse HEAD`).trim();
if (before !== after) { ok('bookworm-boot 已更新'); updated = true; }
} catch {}
}
// 2. 静默更新 bookworm-config (.claude)
if (fs.existsSync(path.join(CLAUDE_DIR, '.git', 'config'))) {
try {
const before = runSilent(`git -C "${CLAUDE_DIR}" rev-parse HEAD`).trim();
const stashOut = run(`git -C "${CLAUDE_DIR}" stash`, { ignoreError: true, silent: true }) || '';
run(`git -C "${CLAUDE_DIR}" pull --rebase`, { ignoreError: true, silent: true, timeout: 15000 });
if (stashOut.includes('Saved working directory')) {
run(`git -C "${CLAUDE_DIR}" stash pop`, { ignoreError: true, silent: true });
}
const after = runSilent(`git -C "${CLAUDE_DIR}" rev-parse HEAD`).trim();
if (before !== after) { ok('.claude 配置已更新'); updated = true; }
} catch {}
}
// 3. 更新后重新渲染模板
if (updated) {
const templateFile = path.join(CLAUDE_DIR, 'settings.template.json');
const settingsFile = path.join(CLAUDE_DIR, 'settings.json');
if (fs.existsSync(templateFile)) {
let tpl = fs.readFileSync(templateFile, 'utf8');
tpl = tpl.replace(/\{\{CLAUDE_ROOT\}\}/g, CLAUDE_DIR.replace(/\\/g, '/'));
tpl = tpl.replace(/\{\{HOME\}\}/g, HOME.replace(/\\/g, '/'));
fs.writeFileSync(settingsFile, tpl);
}
const localTpl = path.join(CLAUDE_DIR, 'settings.local.template.json');
const localSet = path.join(CLAUDE_DIR, 'settings.local.json');
if (fs.existsSync(localTpl)) {
let tpl = fs.readFileSync(localTpl, 'utf8');
tpl = tpl.replace(/\{\{CLAUDE_ROOT\}\}/g, CLAUDE_DIR.replace(/\\/g, '/'));
tpl = tpl.replace(/\{\{HOME\}\}/g, HOME.replace(/\\/g, '/'));
tpl = tpl.replace(/\{\{USERNAME\}\}/g, os.userInfo().username);
fs.writeFileSync(localSet, tpl);
}
ok('配置模板已重新渲染');
// hooks 依赖更新
if (fs.existsSync(path.join(CLAUDE_DIR, 'package.json'))) {
run(`npm install --omit=dev --registry ${NPM_MIRROR}`, { cwd: CLAUDE_DIR, ignoreError: true, silent: true });
}
}
if (!updated) ok('已是最新版本');
// 4. 启动 Claude Code
console.log(`\n ${c.cyan}启动 Claude Code...${c.reset}\n`);
const env = { ...process.env };
const existingNoProxy = process.env.NO_PROXY || process.env.no_proxy || '';
const bwDomains = 'bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1';
env.NO_PROXY = existingNoProxy ? `${existingNoProxy},${bwDomains}` : bwDomains;
env.CLAUDE_CODE_GIT_BASH_PATH = findGitBash();
const child = spawn('claude', [], { stdio: 'inherit', env, cwd: HOME, shell: true });
child.on('exit', () => process.exit(0));
}
main().catch(e => {
fail(`安装引擎异常: ${e.message}`);
console.error(e.stack);
process.exit(1);
});

View File

@ -1,119 +0,0 @@
#!/bin/bash
# ============================================================
# Bookworm Portable - HTTPS 配置脚本 (P1-1)
# 为 Gitea 配置 Nginx HTTPS 反代
# ============================================================
# 前提: deploy-gitea.sh 已执行, Nginx + certbot 已安装
# 用法: ssh root@8.138.11.105 'bash -s' < setup-https.sh
# ============================================================
set -euo pipefail
DOMAIN="code.letcareme.com"
GITEA_PORT=3300
CERT_DIR="/etc/letsencrypt/live/$DOMAIN"
echo "========================================="
echo " Bookworm HTTPS 配置 v1.0"
echo "========================================="
# 1. 检查证书
if [ ! -d "$CERT_DIR" ]; then
echo "[1/4] 证书不存在,申请新证书..."
certbot certonly --nginx -d "$DOMAIN" --non-interactive --agree-tos --email leesu@letcareme.com
else
echo "[1/4] 证书已存在: $CERT_DIR"
fi
# 2. 创建 Nginx 配置
echo "[2/4] 配置 Nginx 反代..."
cat > /etc/nginx/sites-available/gitea.conf << EOF
# Bookworm Gitea - HTTPS 反向代理
server {
listen 80;
server_name $DOMAIN;
return 301 https://\$host\$request_uri;
}
server {
listen 443 ssl http2;
server_name $DOMAIN;
ssl_certificate $CERT_DIR/fullchain.pem;
ssl_certificate_key $CERT_DIR/privkey.pem;
# 安全头
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
# Git LFS 和大文件上传
client_max_body_size 512M;
location / {
proxy_pass http://127.0.0.1:$GITEA_PORT;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
# WebSocket 支持 (Gitea 通知)
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
}
}
EOF
# 启用站点
ln -sf /etc/nginx/sites-available/gitea.conf /etc/nginx/sites-enabled/gitea.conf
# 3. 测试并重载 Nginx
echo "[3/4] 测试 Nginx 配置..."
nginx -t
systemctl reload nginx
echo " [OK] Nginx 已重载"
# 4. 更新 Gitea ROOT_URL
GITEA_INI="/var/lib/gitea/custom/conf/app.ini"
if [ -f "$GITEA_INI" ]; then
echo "[4/4] 更新 Gitea ROOT_URL..."
# 更新端口
sed -i "s/^HTTP_PORT\s*=.*/HTTP_PORT = $GITEA_PORT/" "$GITEA_INI"
# 更新 ROOT_URL 为 HTTPS
sed -i "s|^ROOT_URL\s*=.*|ROOT_URL = https://$DOMAIN/|" "$GITEA_INI"
# 确保 Gitea 只监听本地
if ! grep -q "HTTP_ADDR" "$GITEA_INI"; then
sed -i "/^\[server\]/a HTTP_ADDR = 127.0.0.1" "$GITEA_INI"
else
sed -i "s/^HTTP_ADDR\s*=.*/HTTP_ADDR = 127.0.0.1/" "$GITEA_INI"
fi
systemctl restart gitea
sleep 2
if systemctl is-active --quiet gitea; then
echo " [OK] Gitea 已重启 (端口 $GITEA_PORT, 仅本地监听)"
else
echo " [ERROR] Gitea 重启失败"
journalctl -u gitea -n 10
exit 1
fi
else
echo "[4/4] [!] Gitea 配置不存在,请先运行 deploy-gitea.sh"
fi
echo ""
echo "========================================="
echo " HTTPS 配置完成!"
echo "========================================="
echo ""
echo " 访问: https://$DOMAIN"
echo " HTTP → HTTPS 自动跳转: 已启用"
echo " HSTS: 已启用 (1年)"
echo " Gitea 端口: $GITEA_PORT (仅 127.0.0.1)"
echo ""
echo " 证书续期: certbot 已自动配置 cron"
echo " 验证: curl -I https://$DOMAIN"
echo "========================================="

View File

@ -1,95 +0,0 @@
#!/usr/bin/env node
/**
* sync-version.js stats-compiled.json 同步版本号到 boot 仓库文件
*
* 用法: node sync-version.js [--dry-run]
*
* 替换规则:
* {N} Skills stats.summary.skills
* {N} Agents stats.summary.agents
* {N} Hooks stats.summary.hooks (settings.json 注册的总数)
*/
'use strict';
const fs = require('fs');
const path = require('path');
const DRY_RUN = process.argv.includes('--dry-run');
const BOOT_DIR = __dirname;
const STATS_PATH = path.join(process.env.HOME || process.env.USERPROFILE, '.claude', 'stats-compiled.json');
// 读取 stats
let stats;
try {
stats = JSON.parse(fs.readFileSync(STATS_PATH, 'utf8'));
} catch (e) {
console.error(` [FAIL] 无法读取 ${STATS_PATH}: ${e.message}`);
process.exit(1);
}
const skills = stats.summary.skills;
const agents = stats.summary.agents;
const hooks = stats.summary.hooks;
const version = stats.summary?.version || stats.version || 'v6.5.1';
console.log(` sync-version: ${skills} Skills / ${agents} Agents / ${hooks} Hooks (${version})`);
// 需要更新的文件 (相对于 boot 仓库)
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',
'quick-reference.txt',
];
// 替换模式: 匹配 "数字 Skills"、"数字 Agents"、"数字 Hooks"
// 支持多种分隔格式: "92 Skills"、"92 个 Skills"、"<strong>92</strong> Skills"
const REPLACEMENTS = [
// 纯文本格式: "92 Skills"、"92 个 Skills"
{ pattern: /\b\d+ Skills/g, replacement: `${skills} Skills` },
{ pattern: /\b\d+ Agents/g, replacement: `${agents} Agents` },
{ pattern: /\b\d+ Hooks/g, replacement: `${hooks} Hooks` },
{ pattern: /\b\d+ 个 Skills/g, replacement: `${skills} 个 Skills` },
// HTML badge 格式: <strong>92</strong> Skills
{ pattern: /<strong>\d+<\/strong> Skills/g, replacement: `<strong>${skills}</strong> Skills` },
{ pattern: /<strong>\d+<\/strong> Agents/g, replacement: `<strong>${agents}</strong> Agents` },
{ pattern: /<strong>\d+<\/strong> Hooks/g, replacement: `<strong>${hooks}</strong> Hooks` },
// PS1 格式: "92 Skills / 18 Agents / 34 Hooks"
{ pattern: /\b\d+ Skills \/ \d+ Agents \/ \d+ Hooks/g, replacement: `${skills} Skills / ${agents} Agents / ${hooks} Hooks` },
// bat 格式: "92 Skills / 18 Agents"
{ pattern: /\b\d+ Skills \/ \d+ Agents/g, replacement: `${skills} Skills / ${agents} Agents` },
// "Bookworm (92 Skills)" / "Bookworm - 92 Skills"
{ pattern: /Bookworm \(\d+ Skills\)/g, replacement: `Bookworm (${skills} Skills)` },
{ pattern: /Bookworm - \d+ Skills/g, replacement: `Bookworm - ${skills} Skills` },
];
let totalChanged = 0;
for (const file of FILES) {
const filePath = path.join(BOOT_DIR, file);
if (!fs.existsSync(filePath)) continue;
let content = fs.readFileSync(filePath, 'utf8');
const original = content;
for (const { pattern, replacement } of REPLACEMENTS) {
content = content.replace(pattern, replacement);
}
if (content !== original) {
if (!DRY_RUN) {
fs.writeFileSync(filePath, content, 'utf8');
}
totalChanged++;
console.log(` ${DRY_RUN ? '[DRY] ' : ''}更新: ${file}`);
}
}
console.log(` 同步完成: ${totalChanged}/${FILES.length} 个文件更新`);
if (DRY_RUN) console.log(' [DRY RUN] 未实际写入');

View File

@ -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
}

View File

@ -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

View File

@ -1,181 +0,0 @@
#!/bin/bash
# ============================================================
# Bookworm Portable - macOS 卸载脚本
# 对标 Windows 版 stop.ps1 + 卸载Bookworm.bat
#
# 用法:
# bash uninstall-mac.sh # 基础清理 (保留配置)
# bash uninstall-mac.sh --restore # 完整恢复 (删除 Bookworm)
# bash uninstall-mac.sh --deep # 深度清理 (含历史+凭证)
# ============================================================
set -e
# 颜色
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m'; BOLD='\033[1m'
# 路径
CLAUDE_DIR="$HOME/.claude"
BACKUP_DIR="$HOME/.claude.bw-backup"
BOOT_DIR="$HOME/bookworm-boot"
# 参数解析
RESTORE=false
DEEP=false
for arg in "$@"; do
case "$arg" in
--restore) RESTORE=true ;;
--deep) DEEP=true ;;
esac
done
info() { echo -e " ${BLUE}[INFO]${NC} $1"; }
success() { echo -e " ${GREEN}[OK]${NC} $1"; }
warn() { echo -e " ${YELLOW}[!]${NC} $1"; }
fail() { echo -e " ${RED}[!!]${NC} $1"; }
echo ""
echo -e "${CYAN} Bookworm Portable - macOS 卸载${NC}"
echo -e "${CYAN} ================================${NC}"
echo ""
if $RESTORE || $DEEP; then
echo -e " 将执行:"
echo -e " - 终止 Claude Code 进程"
echo -e " - 清除环境变量"
$RESTORE && echo -e " - 删除 ~/.claude 配置目录"
$RESTORE && echo -e " - 恢复原始 .claude 备份 (如有)"
$DEEP && echo -e " - 清除 shell 历史敏感条目"
$DEEP && echo -e " - 清除 Git 凭证 (Keychain)"
$DEEP && echo -e " - 清除终端别名"
echo ""
read -p " 确认卸载? (y/n): " CONFIRM
if [ "$CONFIRM" != "y" ] && [ "$CONFIRM" != "Y" ]; then
echo " 已取消"
exit 0
fi
fi
echo ""
# ─── 1/5: 终止 Claude Code 进程 ───
echo -e "${BOLD} [1/5]${NC} 终止 Claude Code 进程..."
CLAUDE_PIDS=$(pgrep -f "claude" 2>/dev/null || true)
if [ -n "$CLAUDE_PIDS" ]; then
kill $CLAUDE_PIDS 2>/dev/null || true
sleep 2
# 强制终止残留
kill -9 $CLAUDE_PIDS 2>/dev/null || true
success "进程已终止"
else
info "无 Claude Code 进程运行"
fi
# 清理残留 node hook 进程
NODE_HOOK_PIDS=$(pgrep -f "\.claude/hooks" 2>/dev/null || true)
if [ -n "$NODE_HOOK_PIDS" ]; then
kill $NODE_HOOK_PIDS 2>/dev/null || true
info "Hook 子进程已清理"
fi
# ─── 2/5: 清除环境变量 ───
echo -e "${BOLD} [2/5]${NC} 清除环境变量..."
ENV_VARS=(
CLAUDE_HOME CLAUDE_ROOT ANTHROPIC_API_KEY
ANTHROPIC_BASE_URL SUPABASE_ACCESS_TOKEN
GITHUB_PERSONAL_ACCESS_TOKEN SLACK_BOT_TOKEN
ATLASSIAN_API_TOKEN BROWSERBASE_API_KEY
FIRECRAWL_API_KEY
)
CLEARED=0
for v in "${ENV_VARS[@]}"; do
if [ -n "${!v}" ]; then
unset "$v"
CLEARED=$((CLEARED + 1))
fi
done
success "已清除 $CLEARED 个环境变量"
# ─── 3/5: Git 凭证清除 ───
echo -e "${BOLD} [3/5]${NC} 清除 Git 凭证缓存..."
# macOS Keychain 凭证
for host in code.letcareme.com 8.138.11.105; do
security delete-internet-password -s "$host" 2>/dev/null && info "Keychain: $host 已清除" || true
# git credential reject
printf "protocol=https\nhost=%s\n\n" "$host" | git credential reject 2>/dev/null || true
done
success "Git 凭证已清除"
# ─── 4/5: 恢复/删除 .claude 目录 ───
if $RESTORE; then
echo -e "${BOLD} [4/5]${NC} 恢复原始 .claude 目录..."
if [ -d "$CLAUDE_DIR" ]; then
rm -rf "$CLAUDE_DIR"
info "已删除 Bookworm 配置"
fi
if [ -d "$BACKUP_DIR" ]; then
mv "$BACKUP_DIR" "$CLAUDE_DIR"
success "原始 .claude 已恢复"
else
warn "无备份可恢复 (.claude.bw-backup 不存在)"
fi
# 删除 boot 仓库
if [ -d "$BOOT_DIR" ]; then
rm -rf "$BOOT_DIR"
info "已删除 ~/bookworm-boot"
fi
else
echo -e "${BOLD} [4/5]${NC} 保留 Bookworm 配置 (使用 --restore 可恢复原始)"
fi
# ─── 5/5: 深度清理 ───
if $DEEP; then
echo -e "${BOLD} [5/5]${NC} 深度清理..."
# 清除 shell 历史中的敏感条目
for histfile in "$HOME/.zsh_history" "$HOME/.bash_history"; do
if [ -f "$histfile" ]; then
BEFORE=$(wc -l < "$histfile")
grep -v -i -E 'secrets\.enc|ANTHROPIC_API_KEY|api[_-]?key|bookworm-portable|主密码' "$histfile" > "${histfile}.tmp" 2>/dev/null || true
mv "${histfile}.tmp" "$histfile" 2>/dev/null || true
AFTER=$(wc -l < "$histfile")
info "$(basename $histfile): 清理 $((BEFORE - AFTER)) 条敏感记录"
fi
done
# 清除终端别名
for rcfile in "$HOME/.zshrc" "$HOME/.bashrc"; do
if [ -f "$rcfile" ] && grep -q "Bookworm Portable aliases" "$rcfile" 2>/dev/null; then
# 删除别名块 (标记行 + 后续 alias 行)
sed -i '' '/# Bookworm Portable aliases/,/^$/d' "$rcfile" 2>/dev/null || true
info "$(basename $rcfile): 别名已清除"
fi
done
# 清除 macOS Keychain 中的 Bookworm 凭证
security delete-generic-password -s "bookworm-secrets" 2>/dev/null && info "Keychain: bookworm-secrets 已清除" || true
success "深度清理完成"
else
echo -e "${BOLD} [5/5]${NC} 跳过深度清理 (使用 --deep 可清理历史+凭证)"
fi
# ─── 完成 ───
echo ""
echo -e "${GREEN} ================================${NC}"
if $RESTORE; then
echo -e "${GREEN} Bookworm 已完全卸载${NC}"
echo -e "${GREEN} 可安全删除 bookworm-boot 文件夹${NC}"
else
echo -e "${GREEN} Bookworm 已清理完毕${NC}"
echo -e "${GREEN} 配置保留,可重新启动${NC}"
fi
echo -e "${GREEN} ================================${NC}"
echo ""

View File

@ -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

View File

@ -1,22 +1,49 @@
@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
:: 清除凭证缓存注册表
reg delete "HKCU\Software\Bookworm" /f 2>nul
echo.
echo ====================================
echo Bookworm 已完全卸载
echo 可安全删除 bookworm-boot 文件夹
echo ====================================
echo.
pause

View File

@ -1,44 +1,27 @@
@echo off
chcp 65001 > nul
:: v3.0.11 架构重构: 桌面 .lnk 已直调 pwsh + claude.ps1 绝对路径, 不再走本 bat.
:: 此文件仅保留作为兼容入口 (老用户已经把 .bat 加到收藏夹/开始菜单的场景),
:: 内部行为简化为转发到桌面 .lnk 触发统一启动路径.
::
:: 如果用户双击本 .bat (而非桌面 .lnk), 直接 invoke pwsh + claude.ps1 启动:
title Bookworm Portable - 启动
cd /d "%~dp0"
setlocal
:: 中转站在国内,不走代理
set NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1
set no_proxy=%NO_PROXY%
:: 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
echo.
echo ====================================
echo Bookworm Portable - 快速启动
echo ====================================
echo.
where pwsh >nul 2>nul
if %errorlevel% equ 0 (
pwsh -ExecutionPolicy Bypass -File install.ps1 -StartOnly -AutoAccept
) else (
powershell -ExecutionPolicy Bypass -File install.ps1 -StartOnly -AutoAccept
)
:: 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
if %errorlevel% neq 0 (
echo.
echo 启动失败,按任意键退出...
pause > nul
)
:: 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%"
)
:: 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

View File

@ -1,59 +0,0 @@
@echo off
chcp 65001 >nul
setlocal enabledelayedexpansion
echo.
echo ========================================================
echo Create Admin PowerShell Shortcut for Claude Code
echo ========================================================
echo.
:: Check admin privileges
net session >nul 2>&1
if %errorlevel% neq 0 (
echo [!] Requesting administrator privileges...
powershell -Command "Start-Process '%~f0' -Verb RunAs"
exit /b
)
:: Detect PowerShell path
set "PS_PATH="
for %%p in (
"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe"
"%ProgramFiles%\PowerShell\7\pwsh.exe"
"%ProgramFiles(x86)%\PowerShell\7\pwsh.exe"
) do (
if exist %%p (
set "PS_PATH=%%p"
goto :ps_found
)
)
:ps_found
if "!PS_PATH!" == "" (
echo [ERROR] PowerShell not found
pause
exit /b 1
)
echo [OK] PowerShell: !PS_PATH!
:: Create admin shortcut
set "SHORTCUT_PATH=%USERPROFILE%\Desktop\Claude Code (Admin Terminal).lnk"
powershell -NoProfile -Command "$WshShell = New-Object -ComObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut('%SHORTCUT_PATH%'); $Shortcut.TargetPath = '!PS_PATH!'; $Shortcut.Arguments = '-NoExit -Command \"Write-Host ''Claude Code Admin Terminal Ready'' -ForegroundColor Green; Write-Host ''Type: claude'' -ForegroundColor Yellow\"'; $Shortcut.WorkingDirectory = '%USERPROFILE%'; $Shortcut.Description = 'PowerShell with Admin Rights for Claude Code'; $Shortcut.Save(); $bytes = [System.IO.File]::ReadAllBytes('%SHORTCUT_PATH%'); $bytes[0x15] = $bytes[0x15] -bor 0x20; [System.IO.File]::WriteAllBytes('%SHORTCUT_PATH%', $bytes)"
if exist "%SHORTCUT_PATH%" (
echo.
echo [SUCCESS] Admin terminal shortcut created!
echo.
echo [Usage]
echo 1. Double-click "Claude Code (Admin Terminal)" on desktop
echo 2. Type: claude
echo 3. Claude Code will run with administrator privileges
echo.
) else (
echo [ERROR] Failed to create shortcut
)
pause

View File

@ -1,162 +0,0 @@
@echo off
setlocal enabledelayedexpansion
:: Check for administrator privileges
net session >nul 2>&1
if %errorlevel% neq 0 (
echo Requesting administrator privileges...
powershell -Command "Start-Process '%~f0' -Verb RunAs"
exit /b
)
title PowerShell 7 Auto Setup
echo.
echo ========================================================
echo PowerShell 7 Auto Setup
echo Automatic Installation and Configuration
echo ========================================================
echo.
:: Check if PowerShell 7 is already installed
where pwsh >nul 2>nul
if %errorlevel% equ 0 (
echo [OK] PowerShell 7 is already installed
for /f "tokens=*" %%i in ('pwsh -NoProfile -Command "$PSVersionTable.PSVersion.ToString()"') do set PWSH_VERSION=%%i
echo [INFO] Version: !PWSH_VERSION!
echo.
goto :configure
)
echo [INFO] PowerShell 7 not found. Starting installation...
echo.
:: Method 1: Try winget (Windows 10 1809+)
echo [1/3] Trying winget installation...
winget --version >nul 2>nul
if %errorlevel% equ 0 (
echo [INFO] Installing via winget...
winget install --id Microsoft.PowerShell --silent --accept-package-agreements --accept-source-agreements
if %errorlevel% equ 0 (
echo [OK] PowerShell 7 installed successfully via winget
goto :verify_install
)
)
:: Method 2: Try Chocolatey
echo [2/3] Trying Chocolatey installation...
where choco >nul 2>nul
if %errorlevel% equ 0 (
echo [INFO] Installing via Chocolatey...
choco install powershell-core -y
if %errorlevel% equ 0 (
echo [OK] PowerShell 7 installed successfully via Chocolatey
goto :verify_install
)
)
:: Method 3: Direct MSI download
echo [3/3] Downloading PowerShell 7 MSI installer...
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ProgressPreference = 'SilentlyContinue'; $latestRelease = Invoke-RestMethod 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest'; $msiAsset = $latestRelease.assets | Where-Object { $_.name -like '*win-x64.msi' } | Select-Object -First 1; if ($msiAsset) { Write-Host '[INFO] Downloading' $msiAsset.name; Invoke-WebRequest -Uri $msiAsset.browser_download_url -OutFile '%TEMP%\PowerShell-7.msi'; exit 0; } else { Write-Host '[ERROR] Failed to find MSI asset'; exit 1; }"
if %errorlevel% neq 0 (
echo [ERROR] Failed to download PowerShell 7
echo.
echo Please install manually from: https://github.com/PowerShell/PowerShell/releases
pause
exit /b 1
)
echo [INFO] Installing PowerShell 7...
msiexec /i "%TEMP%\PowerShell-7.msi" /quiet /norestart ADD_EXPLORER_CONTEXT_MENU_OPENPOWERSHELL=1 ADD_FILE_CONTEXT_MENU_RUNPOWERSHELL=1 ENABLE_PSREMOTING=1 REGISTER_MANIFEST=1
set INSTALL_EXIT=%errorlevel%
del "%TEMP%\PowerShell-7.msi" >nul 2>&1
if %INSTALL_EXIT% neq 0 (
echo [ERROR] Installation failed with exit code %INSTALL_EXIT%
pause
exit /b 1
)
:verify_install
echo.
echo [INFO] Verifying installation...
timeout /t 2 /nobreak >nul
where pwsh >nul 2>nul
if %errorlevel% neq 0 (
echo [ERROR] PowerShell 7 installation verification failed
echo [INFO] Trying to refresh PATH...
set "PATH=%PATH%;C:\Program Files\PowerShell\7"
where pwsh >nul 2>nul
if %errorlevel% neq 0 (
echo [ERROR] Still cannot find pwsh.exe
echo [INFO] Please restart your computer and try again
pause
exit /b 1
)
)
for /f "tokens=*" %%i in ('pwsh -NoProfile -Command "$PSVersionTable.PSVersion.ToString()"') do set PWSH_VERSION=%%i
echo [OK] PowerShell 7 installed successfully
echo [INFO] Version: !PWSH_VERSION!
echo.
:configure
echo ========================================================
echo Configuring Default Terminal Settings...
echo ========================================================
echo.
:: Get PowerShell 7 installation path
for /f "tokens=*" %%i in ('where pwsh') do set PWSH_PATH=%%i
echo [INFO] PowerShell 7 path: !PWSH_PATH!
echo.
:: Configure Windows Terminal default profile (if installed)
echo [1/4] Configuring Windows Terminal...
reg query "HKCU\Console\%%Startup" >nul 2>&1
if %errorlevel% equ 0 (
reg add "HKCU\Console\%%Startup" /v DelegationConsole /t REG_SZ /d "{574e775e-4f2a-5b96-ac1e-a2962a402336}" /f >nul 2>&1
reg add "HKCU\Console\%%Startup" /v DelegationTerminal /t REG_SZ /d "{574e775e-4f2a-5b96-ac1e-a2962a402336}" /f >nul 2>&1
echo [OK] Windows Terminal configured
) else (
echo [SKIP] Windows Terminal not found
)
:: Set PowerShell 7 as default for .ps1 files
echo [2/4] Configuring file associations...
assoc .ps1=Microsoft.PowerShellScript.1 >nul 2>&1
ftype Microsoft.PowerShellScript.1="!PWSH_PATH!" -NoLogo -ExecutionPolicy Bypass -File "%%1" %%* >nul 2>&1
echo [OK] .ps1 files associated with PowerShell 7
:: Add PowerShell 7 to App Paths (for Win+R)
echo [3/4] Configuring Win+R shortcut...
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\pwsh.exe" /ve /t REG_SZ /d "!PWSH_PATH!" /f >nul 2>&1
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\powershell.exe" /ve /t REG_SZ /d "!PWSH_PATH!" /f >nul 2>&1
echo [OK] Win+R configured (type 'pwsh' or 'powershell')
:: Set PowerShell 7 as default shell for developers
echo [4/4] Configuring developer settings...
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v DefaultTerminalApplication /t REG_SZ /d "{574e775e-4f2a-5b96-ac1e-a2962a402336}" /f >nul 2>&1
echo [OK] Developer settings configured
echo.
echo ========================================================
echo Installation and Configuration Complete!
echo ========================================================
echo.
echo [OK] PowerShell 7 installed: !PWSH_VERSION!
echo [OK] Default terminal configured
echo [OK] File associations updated
echo [OK] Win+R shortcut configured
echo.
echo [IMPORTANT] Please restart your computer for all changes
echo to take effect, especially Win+R shortcut.
echo.
echo After restart:
echo - Press Win+R and type 'pwsh' or 'powershell'
echo - Right-click .ps1 files to run with PowerShell 7
echo - Windows Terminal will use PowerShell 7 by default
echo.
pause

View File

@ -1,29 +0,0 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const os = require('os');
const { execSync } = require('child_process');
console.log('MCP Auto Loader v1.0');
console.log('====================\n');
const outputDir = 'E:\';
const services = {
playwright: '@modelcontextprotocol/server-playwright',
'chrome-devtools': '@executeautomation/chrome-devtools-mcp',
github: '@modelcontextprotocol/server-github',
slack: '@modelcontextprotocol/server-slack',
linear: '@modelcontextprotocol/server-linear'
};
const mcpServers = {};
for (const [name, pkg] of Object.entries(services)) {
mcpServers[name] = {
command: 'cmd',
args: ['/c', 'npx', '-y', pkg]
};
}
const configPath = path.join(outputDir, 'mcp-configs-summary.json');
fs.writeFileSync(configPath, JSON.stringify({ mcpServers }, null, 2));
console.log('Config saved to:', configPath);

View File

@ -1,265 +0,0 @@
# MCP Configuration Wizard
Universal MCP configuration tool for Claude Code - works on any system, any user.
## 🎯 Features
- **Auto-Detection**: Automatically detects your system, existing configs, and environment
- **Interactive Wizard**: Step-by-step guided setup
- **Cross-Platform**: Works on Windows, macOS, and Linux
- **Safe Installation**: Backup existing configs before changes
- **Export/Import**: Share configurations between machines
- **19 MCP Services**: Pre-configured popular services
## 📦 Quick Start
### 1. Install Dependencies
```bash
npm install
```
### 2. Detect Your System
```bash
npm run detect
```
This will scan your system and detect:
- Claude Code installation
- Existing MCP configurations
- Environment variables
- Installed npm packages
### 3. Run Configuration Wizard
```bash
npm start
```
Choose from 4 modes:
1. **Quick Setup** - Install recommended services (no API keys needed)
2. **Custom Setup** - Choose specific services
3. **Import** - Load configuration from file
4. **Export** - Save current config for another machine
### 4. Apply Configuration
```bash
npm run apply
```
This will:
- Install MCP configuration to `~/.claude.json`
- Generate environment variable scripts
- Create installation guide
### 5. Restart Claude Code
Close and reopen Claude Code to load the new MCP services.
## 🔧 Configuration Modes
### Quick Setup (Recommended for beginners)
Installs 6 essential services that work without API keys:
- playwright - Browser automation
- chrome-devtools - Chrome DevTools integration
- context7 - Programming documentation
- deep-research - Research assistant
- sequential-thinking - Reasoning engine
- scrapling - Web scraping
### Custom Setup (For advanced users)
Choose from 19 services across categories:
- **Automation**: playwright, chrome-devtools, browserbase
- **Development**: github, vercel, firebase
- **Communication**: slack
- **Productivity**: linear, notion
- **Documentation**: context7
- **Research**: deep-research
- **Web**: scrapling, firecrawl
- **Database**: supabase
- **Monitoring**: sentry
- **Infrastructure**: cloudflare
### Import/Export
**Export** your configuration to share with another machine:
```bash
npm start
# Select option 4
```
**Import** a configuration file:
```bash
npm start
# Select option 3
# Enter path to config file
```
## 📋 Available Services
### No API Key Required (9 services)
| Service | Description | Category |
|---------|-------------|----------|
| playwright | Browser automation testing | Automation |
| chrome-devtools | Chrome DevTools integration | Automation |
| context7 | Programming documentation | Documentation |
| deep-research | Deep research assistant | Research |
| sequential-thinking | Sequential reasoning | Reasoning |
| scrapling | Web scraping tool | Web |
| figma | Figma design integration | Design |
### API Key Required (10 services)
| Service | Description | Setup Link |
|---------|-------------|------------|
| github | GitHub repository integration | [Get Token](https://github.com/settings/tokens) |
| slack | Slack team collaboration | [Create App](https://api.slack.com/apps) |
| linear | Linear project management | [API Settings](https://linear.app/settings/api) |
| browserbase | Cloud browser service | [Dashboard](https://browserbase.com) |
| cloudflare | Cloudflare CDN management | [API Tokens](https://dash.cloudflare.com/profile/api-tokens) |
| firecrawl | Intelligent web crawler | [Sign Up](https://firecrawl.dev) |
| supabase | Supabase database | [API Settings](https://supabase.com/dashboard) |
| sentry | Error monitoring | [Auth Tokens](https://sentry.io/settings/account/api/auth-tokens/) |
| notion | Notion knowledge base | [Integrations](https://www.notion.so/my-integrations) |
| vercel | Vercel deployment | [Tokens](https://vercel.com/account/tokens) |
| firebase | Firebase backend | [Service Accounts](https://console.firebase.google.com) |
## 🔐 Environment Variables
The wizard generates scripts to set environment variables:
**Windows (PowerShell):**
```powershell
.\apply-env.ps1
```
**Unix/Mac (Bash/Zsh):**
```bash
source ./apply-env.sh
```
Or set them manually from `generated-env.json`.
## 📁 Generated Files
After running the wizard, you'll get:
- `detection-result.json` - System detection results
- `generated-claude.json` - MCP configuration for Claude Code
- `generated-env.json` - Environment variables (JSON format)
- `apply-env.ps1` - PowerShell script (Windows)
- `apply-env.sh` - Shell script (Unix/Mac)
- `INSTALL.md` - Installation guide
## 🚀 Usage Examples
### Example 1: First-time setup on new machine
```bash
npm install
npm run detect
npm start
# Choose option 1 (Quick Setup)
npm run apply
# Restart Claude Code
```
### Example 2: Add GitHub integration
```bash
npm start
# Choose option 2 (Custom Setup)
# Enter: github
# Enter your GitHub token
npm run apply
# Restart Claude Code
```
### Example 3: Export config for another machine
```bash
npm start
# Choose option 4 (Export)
# Copy mcp-config-export.json to other machine
```
On the other machine:
```bash
npm install
npm start
# Choose option 3 (Import)
# Enter path to mcp-config-export.json
npm run apply
```
## 🛠️ Manual Installation
If automatic installation fails:
1. Copy content from `generated-claude.json`
2. Paste into `~/.claude.json` (create if doesn't exist)
3. Set environment variables from `generated-env.json`
4. Restart Claude Code
## ⚠️ Troubleshooting
### "Cannot find module 'inquirer'"
```bash
npm install
```
### "Permission denied" on Unix/Mac
```bash
chmod +x apply-env.sh
```
### PowerShell execution policy error
```powershell
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
```
### MCP services not loading
1. Check `~/.claude.json` exists and has `mcpServers` section
2. Verify environment variables are set: `echo $GITHUB_PERSONAL_ACCESS_TOKEN`
3. Restart Claude Code completely
4. Check Claude Code logs for errors
## 📖 Documentation
- [MCP Protocol](https://modelcontextprotocol.io/)
- [Claude Code](https://claude.ai/code)
- [Service Documentation](./docs/services.md)
## 🤝 Contributing
Contributions welcome! To add a new MCP service:
1. Add service definition to `MCP_SERVICES` in `wizard.js`
2. Include package name, description, category, and required env vars
3. Test with `npm start`
4. Submit PR
## 📄 License
MIT
## 🔗 Links
- [MCP Official Site](https://modelcontextprotocol.io/)
- [Claude Code](https://claude.ai/code)
- [GitHub Repository](https://github.com/your-repo/mcp-config-wizard)
---
**Version**: 1.0.0
**Last Updated**: 2026-04-04
**Maintained by**: Bookworm Team

View File

@ -1,64 +0,0 @@
@echo off
chcp 65001 >nul
title MCP Configuration Wizard - Setup
echo.
echo ╔════════════════════════════════════════════════════════╗
echo ║ MCP Configuration Wizard - First Time Setup ║
echo ╚════════════════════════════════════════════════════════╝
echo.
REM 检查 Node.js 是否安装
where node >nul 2>nul
if %errorlevel% neq 0 (
echo [ERROR] Node.js is not installed!
echo.
echo Please install Node.js from: https://nodejs.org/
echo.
pause
exit /b 1
)
echo [OK] Node.js found:
node --version
echo.
REM 检查是否已安装依赖
if exist "node_modules\" (
echo [OK] Dependencies already installed
echo.
goto :run_wizard
)
echo [INFO] Installing dependencies...
echo This may take a minute...
echo.
call npm install
if %errorlevel% neq 0 (
echo.
echo [ERROR] Failed to install dependencies
echo.
pause
exit /b 1
)
echo.
echo [OK] Dependencies installed successfully
echo.
:run_wizard
echo ════════════════════════════════════════════════════════
echo Starting Configuration Wizard...
echo ════════════════════════════════════════════════════════
echo.
node wizard.js
echo.
echo ════════════════════════════════════════════════════════
echo Wizard completed
echo ════════════════════════════════════════════════════════
echo.
pause

View File

@ -1,60 +0,0 @@
#!/bin/bash
# MCP Configuration Wizard - Launcher for Unix/Mac
echo ""
echo "╔════════════════════════════════════════════════════════╗"
echo "║ MCP Configuration Wizard - First Time Setup ║"
echo "╚════════════════════════════════════════════════════════╝"
echo ""
# 检查 Node.js
if ! command -v node &> /dev/null; then
echo "[ERROR] Node.js is not installed!"
echo ""
echo "Please install Node.js from: https://nodejs.org/"
echo ""
read -p "Press Enter to exit..."
exit 1
fi
echo "[OK] Node.js found: $(node --version)"
echo ""
# 检查依赖
if [ -d "node_modules" ]; then
echo "[OK] Dependencies already installed"
echo ""
else
echo "[INFO] Installing dependencies..."
echo "This may take a minute..."
echo ""
npm install
if [ $? -ne 0 ]; then
echo ""
echo "[ERROR] Failed to install dependencies"
echo ""
read -p "Press Enter to exit..."
exit 1
fi
echo ""
echo "[OK] Dependencies installed successfully"
echo ""
fi
# 运行向导
echo "════════════════════════════════════════════════════════"
echo "Starting Configuration Wizard..."
echo "════════════════════════════════════════════════════════"
echo ""
node wizard.js
echo ""
echo "════════════════════════════════════════════════════════"
echo "Wizard completed"
echo "════════════════════════════════════════════════════════"
echo ""
read -p "Press Enter to exit..."

View File

@ -1,119 +0,0 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const os = require('os');
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function question(prompt) {
return new Promise(resolve => rl.question(prompt, resolve));
}
async function main() {
console.log('╔════════════════════════════════════════════════════════╗');
console.log('║ MCP Configuration Installer ║');
console.log('╚════════════════════════════════════════════════════════╝\n');
// 检查生成的配置文件
const generatedConfig = path.join(__dirname, 'generated-claude.json');
if (!fs.existsSync(generatedConfig)) {
console.log('✗ No configuration found. Run "node wizard.js" first.');
rl.close();
return;
}
const config = JSON.parse(fs.readFileSync(generatedConfig, 'utf-8'));
console.log(`Found configuration with ${Object.keys(config.mcpServers).length} MCP services\n`);
// 目标配置文件路径
const targetConfig = path.join(os.homedir(), '.claude.json');
let existingConfig = {};
// 检查现有配置
if (fs.existsSync(targetConfig)) {
console.log('⚠ Existing .claude.json found\n');
console.log('Installation options:');
console.log(' 1. Merge - Keep existing settings, add/update MCP servers');
console.log(' 2. Replace - Overwrite with new configuration');
console.log(' 3. Backup & Replace - Backup existing, then replace');
console.log(' 4. Cancel\n');
const choice = await question('Select option (1-4): ');
switch (choice) {
case '1':
existingConfig = JSON.parse(fs.readFileSync(targetConfig, 'utf-8'));
existingConfig.mcpServers = {
...(existingConfig.mcpServers || {}),
...config.mcpServers
};
break;
case '2':
existingConfig = config;
break;
case '3':
const backupPath = targetConfig + `.backup-${Date.now()}`;
fs.copyFileSync(targetConfig, backupPath);
console.log(`\n✓ Backup created: ${backupPath}`);
existingConfig = config;
break;
case '4':
console.log('\nInstallation cancelled');
rl.close();
return;
default:
console.log('\nInvalid option');
rl.close();
return;
}
} else {
existingConfig = config;
}
// 写入配置
try {
fs.writeFileSync(targetConfig, JSON.stringify(existingConfig, null, 2));
console.log(`\n✓ Configuration installed to: ${targetConfig}`);
} catch (e) {
console.log(`\n✗ Failed to write configuration: ${e.message}`);
console.log('\nManual installation:');
console.log(` 1. Copy ${generatedConfig}`);
console.log(` 2. To ${targetConfig}`);
rl.close();
return;
}
// 应用环境变量
const envFile = path.join(__dirname, 'generated-env.json');
if (fs.existsSync(envFile)) {
console.log('\n=== Environment Variables ===\n');
console.log('Environment variables need to be set manually:');
if (os.platform() === 'win32') {
console.log('\nWindows: Run the PowerShell script:');
console.log(' .\\apply-env.ps1');
} else {
console.log('\nUnix/Mac: Source the shell script:');
console.log(' source ./apply-env.sh');
}
console.log('\nOr set them manually from: generated-env.json');
}
console.log('\n╔════════════════════════════════════════════════════════╗');
console.log('║ Installation Complete ║');
console.log('╚════════════════════════════════════════════════════════╝');
console.log('\n✓ MCP configuration installed successfully');
console.log('\n📋 Next steps:');
console.log(' 1. Set environment variables (if needed)');
console.log(' 2. Restart Claude Code');
console.log(' 3. Verify MCP services are loaded\n');
rl.close();
}
main().catch(console.error);

View File

@ -1,142 +0,0 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const os = require('os');
const { execSync } = require('child_process');
console.log('╔════════════════════════════════════════════════════════╗');
console.log('║ MCP Configuration Wizard - System Detection ║');
console.log('╚════════════════════════════════════════════════════════╝\n');
// 检测系统信息
const detection = {
timestamp: new Date().toISOString(),
system: {
platform: os.platform(),
username: os.userInfo().username,
homedir: os.homedir(),
nodeVersion: process.version
},
claude: {
configDir: null,
configFile: null,
hasExistingConfig: false,
existingMcpServers: []
},
environment: {
variables: {},
mcpRelated: []
},
npm: {
globalPackages: [],
mcpPackages: []
}
};
// 1. 检测 Claude Code 配置目录
console.log('🔍 Detecting Claude Code configuration...');
const possibleConfigDirs = [
path.join(os.homedir(), '.claude'),
path.join(os.homedir(), 'AppData', 'Roaming', 'Claude'),
path.join(os.homedir(), 'Library', 'Application Support', 'Claude')
];
for (const dir of possibleConfigDirs) {
if (fs.existsSync(dir)) {
detection.claude.configDir = dir;
console.log(` ✓ Found: ${dir}`);
break;
}
}
if (!detection.claude.configDir) {
console.log(' ⚠ Claude config directory not found, will create on apply');
}
// 检测现有配置文件
const configFile = path.join(os.homedir(), '.claude.json');
if (fs.existsSync(configFile)) {
detection.claude.configFile = configFile;
detection.claude.hasExistingConfig = true;
try {
const config = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
if (config.mcpServers) {
detection.claude.existingMcpServers = Object.keys(config.mcpServers);
console.log(` ✓ Existing config found with ${detection.claude.existingMcpServers.length} MCP servers`);
}
} catch (e) {
console.log(' ⚠ Config file exists but cannot be parsed');
}
} else {
console.log(' No existing .claude.json found');
}
// 2. 检测环境变量
console.log('\n🔍 Detecting environment variables...');
const mcpEnvVars = [
'GITHUB_PERSONAL_ACCESS_TOKEN',
'SLACK_BOT_TOKEN', 'SLACK_TEAM_ID',
'LINEAR_API_KEY',
'BROWSERBASE_PROJECT_ID', 'BROWSERBASE_API_KEY',
'CLOUDFLARE_API_TOKEN',
'FIRECRAWL_API_KEY',
'SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY',
'SENTRY_AUTH_TOKEN',
'NOTION_API_KEY',
'VERCEL_TOKEN',
'FIREBASE_PROJECT_ID', 'FIREBASE_PRIVATE_KEY', 'FIREBASE_CLIENT_EMAIL'
];
mcpEnvVars.forEach(varName => {
const value = process.env[varName];
if (value) {
detection.environment.mcpRelated.push(varName);
detection.environment.variables[varName] = value.substring(0, 10) + '...'; // 只保存前缀用于检测
}
});
console.log(` ✓ Found ${detection.environment.mcpRelated.length} MCP-related environment variables`);
detection.environment.mcpRelated.forEach(v => console.log(` - ${v}`));
// 3. 检测已安装的 npm 包
console.log('\n🔍 Detecting installed npm packages...');
try {
const globalList = execSync('npm list -g --depth=0 --json', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
const packages = JSON.parse(globalList);
if (packages.dependencies) {
detection.npm.globalPackages = Object.keys(packages.dependencies);
// 筛选 MCP 相关包
const mcpKeywords = ['mcp', 'modelcontextprotocol', 'claude'];
detection.npm.mcpPackages = detection.npm.globalPackages.filter(pkg =>
mcpKeywords.some(kw => pkg.toLowerCase().includes(kw))
);
if (detection.npm.mcpPackages.length > 0) {
console.log(` ✓ Found ${detection.npm.mcpPackages.length} MCP packages installed globally`);
detection.npm.mcpPackages.forEach(p => console.log(` - ${p}`));
} else {
console.log(' No MCP packages found (will use npx)');
}
}
} catch (e) {
console.log(' ⚠ Cannot detect npm packages (npm may not be installed)');
}
// 4. 保存检测结果
const outputFile = path.join(__dirname, 'detection-result.json');
fs.writeFileSync(outputFile, JSON.stringify(detection, null, 2));
console.log('\n╔════════════════════════════════════════════════════════╗');
console.log('║ Detection Complete ║');
console.log('╚════════════════════════════════════════════════════════╝');
console.log(`\n📄 Results saved to: ${outputFile}`);
console.log('\n📊 Summary:');
console.log(` Platform: ${detection.system.platform}`);
console.log(` User: ${detection.system.username}`);
console.log(` Claude Config: ${detection.claude.configDir || 'Not found'}`);
console.log(` Existing MCP Servers: ${detection.claude.existingMcpServers.length}`);
console.log(` Environment Variables: ${detection.environment.mcpRelated.length}`);
console.log(` MCP Packages: ${detection.npm.mcpPackages.length}`);
console.log('\n💡 Next step: Run "node wizard.js" to configure MCP services');

View File

@ -1,19 +0,0 @@
{
"name": "mcp-config-wizard",
"version": "1.0.0",
"description": "Universal MCP Configuration Wizard for Claude Code",
"main": "wizard.js",
"scripts": {
"start": "node wizard.js",
"detect": "node detect.js",
"apply": "node apply.js"
},
"keywords": ["mcp", "claude-code", "configuration"],
"author": "Bookworm Team",
"license": "MIT",
"dependencies": {
"inquirer": "^8.2.5",
"chalk": "^4.1.2",
"ora": "^5.4.1"
}
}

View File

@ -1,496 +0,0 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const os = require('os');
const readline = require('readline');
// MCP 服务定义
const MCP_SERVICES = {
// 无需环境变量的服务
'playwright': {
package: '@modelcontextprotocol/server-playwright',
description: '浏览器自动化测试',
category: 'automation',
envVars: []
},
'chrome-devtools': {
package: '@executeautomation/chrome-devtools-mcp',
description: 'Chrome 开发者工具集成',
category: 'automation',
envVars: []
},
'context7': {
package: 'context7-mcp',
description: '编程文档查询',
category: 'documentation',
envVars: []
},
'deep-research': {
package: 'deep-research-mcp',
description: '深度研究助手',
category: 'research',
envVars: []
},
'sequential-thinking': {
package: '@modelcontextprotocol/server-sequential-thinking',
description: '顺序思考推理',
category: 'reasoning',
envVars: []
},
'scrapling': {
package: 'scrapling-mcp',
description: '网页抓取工具',
category: 'web',
envVars: []
},
'figma': {
package: '@figma/mcp-server-figma',
description: 'Figma 设计工具集成',
category: 'design',
envVars: [],
oauth: true
},
// 需要环境变量的服务
'github': {
package: '@modelcontextprotocol/server-github',
description: 'GitHub 代码仓库集成',
category: 'development',
envVars: ['GITHUB_PERSONAL_ACCESS_TOKEN'],
setup: 'https://github.com/settings/tokens'
},
'slack': {
package: '@modelcontextprotocol/server-slack',
description: 'Slack 团队协作',
category: 'communication',
envVars: ['SLACK_BOT_TOKEN', 'SLACK_TEAM_ID'],
setup: 'https://api.slack.com/apps'
},
'linear': {
package: '@modelcontextprotocol/server-linear',
description: 'Linear 项目管理',
category: 'productivity',
envVars: ['LINEAR_API_KEY'],
setup: 'https://linear.app/settings/api'
},
'browserbase': {
package: '@browserbasehq/mcp-server-browserbase',
description: '云端浏览器服务',
category: 'automation',
envVars: ['BROWSERBASE_PROJECT_ID', 'BROWSERBASE_API_KEY'],
setup: 'https://browserbase.com'
},
'cloudflare': {
package: '@cloudflare/mcp-server-cloudflare',
description: 'Cloudflare CDN 管理',
category: 'infrastructure',
envVars: ['CLOUDFLARE_API_TOKEN'],
setup: 'https://dash.cloudflare.com/profile/api-tokens'
},
'firecrawl': {
package: '@mendable/firecrawl-mcp',
description: '智能网页爬虫',
category: 'web',
envVars: ['FIRECRAWL_API_KEY'],
setup: 'https://firecrawl.dev'
},
'supabase': {
package: '@supabase/mcp-server-supabase',
description: 'Supabase 数据库',
category: 'database',
envVars: ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY'],
setup: 'https://supabase.com/dashboard/project/_/settings/api'
},
'sentry': {
package: '@sentry/mcp-server',
description: 'Sentry 错误监控',
category: 'monitoring',
envVars: ['SENTRY_AUTH_TOKEN'],
setup: 'https://sentry.io/settings/account/api/auth-tokens/'
},
'notion': {
package: '@notionhq/mcp-server',
description: 'Notion 知识库',
category: 'productivity',
envVars: ['NOTION_API_KEY'],
setup: 'https://www.notion.so/my-integrations'
},
'vercel': {
package: '@vercel/mcp-server',
description: 'Vercel 部署平台',
category: 'deployment',
envVars: ['VERCEL_TOKEN'],
setup: 'https://vercel.com/account/tokens'
},
'firebase': {
package: '@firebase/mcp-server',
description: 'Firebase 后端服务',
category: 'backend',
envVars: ['FIREBASE_PROJECT_ID', 'FIREBASE_PRIVATE_KEY', 'FIREBASE_CLIENT_EMAIL'],
setup: 'https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk'
}
};
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function question(prompt) {
return new Promise(resolve => rl.question(prompt, resolve));
}
async function main() {
console.log('╔════════════════════════════════════════════════════════╗');
console.log('║ MCP Configuration Wizard v1.0 ║');
console.log('║ Universal Setup Tool for Claude Code ║');
console.log('╚════════════════════════════════════════════════════════╝\n');
// 加载检测结果
let detection = null;
const detectionFile = path.join(__dirname, 'detection-result.json');
if (fs.existsSync(detectionFile)) {
detection = JSON.parse(fs.readFileSync(detectionFile, 'utf-8'));
console.log('✓ Loaded system detection results\n');
} else {
console.log('⚠ No detection results found. Run "node detect.js" first.\n');
const proceed = await question('Continue anyway? (y/n): ');
if (proceed.toLowerCase() !== 'y') {
rl.close();
return;
}
}
console.log('This wizard will help you configure MCP services for Claude Code.\n');
console.log('Available configuration modes:');
console.log(' 1. Quick Setup - Install recommended services');
console.log(' 2. Custom Setup - Choose services manually');
console.log(' 3. Import from file - Load existing configuration');
console.log(' 4. Export current config - Save for another machine\n');
const mode = await question('Select mode (1-4): ');
const config = {
mcpServers: {},
environment: {}
};
switch (mode) {
case '1':
await quickSetup(config, detection);
break;
case '2':
await customSetup(config, detection);
break;
case '3':
await importConfig(config);
break;
case '4':
await exportConfig(detection);
rl.close();
return;
default:
console.log('Invalid selection');
rl.close();
return;
}
// 生成配置文件
await generateConfig(config, detection);
rl.close();
}
async function quickSetup(config, detection) {
console.log('\n=== Quick Setup ===\n');
console.log('Installing recommended services:');
const recommended = [
'playwright', 'chrome-devtools', 'context7',
'deep-research', 'sequential-thinking', 'scrapling'
];
recommended.forEach(name => {
const service = MCP_SERVICES[name];
console.log(`${name} - ${service.description}`);
addService(config, name, service);
});
console.log('\nOptional services with API keys:');
console.log(' - github, slack, linear, supabase, etc.\n');
const addMore = await question('Configure services with API keys? (y/n): ');
if (addMore.toLowerCase() === 'y') {
await configureApiServices(config, detection);
}
}
async function customSetup(config, detection) {
console.log('\n=== Custom Setup ===\n');
console.log('Available services by category:\n');
const categories = {};
Object.entries(MCP_SERVICES).forEach(([name, service]) => {
if (!categories[service.category]) {
categories[service.category] = [];
}
categories[service.category].push({ name, ...service });
});
for (const [category, services] of Object.entries(categories)) {
console.log(`\n${category.toUpperCase()}:`);
services.forEach((s, i) => {
const envInfo = s.envVars.length > 0 ? ' (requires API key)' : '';
console.log(` ${i + 1}. ${s.name} - ${s.description}${envInfo}`);
});
}
console.log('\nEnter service names separated by commas (or "all" for all services):');
const selection = await question('Services: ');
if (selection.toLowerCase() === 'all') {
Object.entries(MCP_SERVICES).forEach(([name, service]) => {
addService(config, name, service);
});
} else {
const selected = selection.split(',').map(s => s.trim());
selected.forEach(name => {
if (MCP_SERVICES[name]) {
addService(config, name, MCP_SERVICES[name]);
}
});
}
await configureApiServices(config, detection);
}
async function configureApiServices(config, detection) {
console.log('\n=== API Key Configuration ===\n');
for (const [name, service] of Object.entries(config.mcpServers)) {
const serviceDef = MCP_SERVICES[name];
if (serviceDef.envVars && serviceDef.envVars.length > 0) {
console.log(`\n${name} requires:`);
serviceDef.envVars.forEach(v => console.log(` - ${v}`));
if (serviceDef.setup) {
console.log(` Setup: ${serviceDef.setup}`);
}
// 检查是否已有环境变量
const hasExisting = detection && serviceDef.envVars.every(v =>
detection.environment.mcpRelated.includes(v)
);
if (hasExisting) {
console.log(' ✓ Found existing environment variables');
const useExisting = await question(' Use existing? (y/n): ');
if (useExisting.toLowerCase() === 'y') {
continue;
}
}
const configure = await question(' Configure now? (y/n): ');
if (configure.toLowerCase() === 'y') {
for (const varName of serviceDef.envVars) {
const value = await question(` ${varName}: `);
if (value) {
config.environment[varName] = value;
}
}
}
}
}
}
async function importConfig(config) {
console.log('\n=== Import Configuration ===\n');
const filePath = await question('Enter path to config file (.json): ');
if (fs.existsSync(filePath)) {
const imported = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
if (imported.mcpServers) {
config.mcpServers = imported.mcpServers;
console.log(`✓ Imported ${Object.keys(config.mcpServers).length} services`);
}
if (imported.environment) {
config.environment = imported.environment;
console.log(`✓ Imported ${Object.keys(config.environment).length} environment variables`);
}
} else {
console.log('✗ File not found');
}
}
async function exportConfig(detection) {
console.log('\n=== Export Configuration ===\n');
if (!detection || !detection.claude.hasExistingConfig) {
console.log('✗ No existing configuration found');
return;
}
const configFile = path.join(os.homedir(), '.claude.json');
const config = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
const exportData = {
mcpServers: config.mcpServers || {},
environment: {}
};
// 导出环境变量
if (detection.environment.mcpRelated.length > 0) {
console.log('Export environment variables?');
const exportEnv = await question('(y/n): ');
if (exportEnv.toLowerCase() === 'y') {
detection.environment.mcpRelated.forEach(varName => {
const value = process.env[varName];
if (value) {
exportData.environment[varName] = value;
}
});
}
}
const outputPath = path.join(__dirname, 'mcp-config-export.json');
fs.writeFileSync(outputPath, JSON.stringify(exportData, null, 2));
console.log(`\n✓ Configuration exported to: ${outputPath}`);
}
function addService(config, name, service) {
const isWindows = os.platform() === 'win32';
const command = isWindows ? 'cmd' : 'npx';
const args = isWindows
? ['/c', 'npx', '-y', service.package]
: ['-y', service.package];
config.mcpServers[name] = { command, args };
if (service.envVars && service.envVars.length > 0) {
config.mcpServers[name].env = {};
service.envVars.forEach(v => {
config.mcpServers[name].env[v] = `\${${v}}`;
});
}
}
async function generateConfig(config, detection) {
console.log('\n=== Generating Configuration Files ===\n');
// 1. 生成 .claude.json
const claudeConfigPath = path.join(__dirname, 'generated-claude.json');
fs.writeFileSync(claudeConfigPath, JSON.stringify({ mcpServers: config.mcpServers }, null, 2));
console.log(`✓ Generated: ${claudeConfigPath}`);
// 2. 生成环境变量文件
if (Object.keys(config.environment).length > 0) {
const envPath = path.join(__dirname, 'generated-env.json');
fs.writeFileSync(envPath, JSON.stringify(config.environment, null, 2));
console.log(`✓ Generated: ${envPath}`);
// 生成 PowerShell 脚本Windows
if (os.platform() === 'win32') {
const psScript = generatePowerShellScript(config.environment);
const psPath = path.join(__dirname, 'apply-env.ps1');
fs.writeFileSync(psPath, psScript);
console.log(`✓ Generated: ${psPath}`);
}
// 生成 Shell 脚本Unix
const shScript = generateShellScript(config.environment);
const shPath = path.join(__dirname, 'apply-env.sh');
fs.writeFileSync(shPath, shScript);
fs.chmodSync(shPath, '755');
console.log(`✓ Generated: ${shPath}`);
}
// 3. 生成安装说明
const readme = generateReadme(config, detection);
const readmePath = path.join(__dirname, 'INSTALL.md');
fs.writeFileSync(readmePath, readme);
console.log(`✓ Generated: ${readmePath}`);
console.log('\n╔════════════════════════════════════════════════════════╗');
console.log('║ Configuration Generated Successfully ║');
console.log('╚════════════════════════════════════════════════════════╝');
console.log('\nNext steps:');
console.log(' 1. Review generated files');
console.log(' 2. Run "node apply.js" to install configuration');
console.log(' 3. Restart Claude Code');
}
function generatePowerShellScript(env) {
let script = '# MCP Environment Variables Setup\n';
script += '# Generated by MCP Configuration Wizard\n\n';
script += '$ErrorActionPreference = "Continue"\n\n';
script += 'Write-Host "Setting up MCP environment variables..." -ForegroundColor Cyan\n\n';
Object.entries(env).forEach(([key, value]) => {
script += `[System.Environment]::SetEnvironmentVariable("${key}", "${value}", [System.EnvironmentVariableTarget]::User)\n`;
script += `Write-Host "[OK] ${key}" -ForegroundColor Green\n`;
});
script += '\nWrite-Host "\\nEnvironment variables configured successfully!" -ForegroundColor Green\n';
script += 'Write-Host "Please restart Claude Code to apply changes" -ForegroundColor Yellow\n';
script += 'Read-Host "\\nPress Enter to exit"\n';
return script;
}
function generateShellScript(env) {
let script = '#!/bin/bash\n';
script += '# MCP Environment Variables Setup\n';
script += '# Generated by MCP Configuration Wizard\n\n';
Object.entries(env).forEach(([key, value]) => {
script += `export ${key}="${value}"\n`;
});
script += '\necho "Environment variables configured for this session"\n';
script += 'echo "To make permanent, add these exports to your ~/.bashrc or ~/.zshrc"\n';
return script;
}
function generateReadme(config, detection) {
let readme = '# MCP Configuration Installation Guide\n\n';
readme += 'Generated by MCP Configuration Wizard\n\n';
readme += `## System Information\n\n`;
if (detection) {
readme += `- Platform: ${detection.system.platform}\n`;
readme += `- User: ${detection.system.username}\n`;
readme += `- Node.js: ${detection.system.nodeVersion}\n\n`;
}
readme += `## Configured Services (${Object.keys(config.mcpServers).length})\n\n`;
Object.keys(config.mcpServers).forEach(name => {
const service = MCP_SERVICES[name];
readme += `- **${name}**: ${service.description}\n`;
});
readme += '\n## Installation Steps\n\n';
readme += '### 1. Apply Environment Variables\n\n';
if (os.platform() === 'win32') {
readme += '**Windows:**\n```powershell\n.\\apply-env.ps1\n```\n\n';
}
readme += '**Unix/Mac:**\n```bash\nsource ./apply-env.sh\n```\n\n';
readme += '### 2. Install MCP Configuration\n\n';
readme += '```bash\nnode apply.js\n```\n\n';
readme += '### 3. Restart Claude Code\n\n';
readme += 'Close and reopen Claude Code to load the new MCP services.\n\n';
readme += '## Manual Installation\n\n';
readme += 'If automatic installation fails:\n\n';
readme += '1. Copy `generated-claude.json` content to `~/.claude.json`\n';
readme += '2. Set environment variables from `generated-env.json`\n';
readme += '3. Restart Claude Code\n';
return readme;
}
main().catch(console.error);

View File

@ -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 } }"

View File

@ -0,0 +1,28 @@
@echo off
chcp 65001 > nul
title Bookworm Portable - 更新并启动
cd /d "%~dp0"
:: 中转站在国内,不走代理
set NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1
set no_proxy=%NO_PROXY%
echo.
echo ====================================
echo Bookworm Portable - 更新并启动
echo (同步最新 Skills/Hooks 后启动)
echo ====================================
echo.
where pwsh >nul 2>nul
if %errorlevel% equ 0 (
pwsh -ExecutionPolicy Bypass -File install.ps1 -AutoAccept
) else (
powershell -ExecutionPolicy Bypass -File install.ps1 -AutoAccept
)
if %errorlevel% neq 0 (
echo.
echo 启动失败,按任意键退出...
pause > nul
)