feat: Bookworm Portable v1.5 — 8 fixes (P0 NDA + P1 banners + P2 perf)
- P1: Banner v1.3→v1.5, Hooks 29→34 - P1: 卸载脚本补删 更新Bookworm.lnk - P1: git stash pop 安全检查 - P2: Playwright 检测改用 npm list - P2: 代理端口扫描 500ms async 超时 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
5e0ff18aa1
37
Bookworm-Install.bat
Normal file
37
Bookworm-Install.bat
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
chcp 65001 > nul 2>&1
|
||||||
|
title Bookworm Smart Assistant - 全自动安装 v3.0
|
||||||
|
|
||||||
|
:: 极简入口: 只负责确保 Node.js 存在, 核心逻辑全在 setup-all.js 中
|
||||||
|
:: 规则: 不超过 30 行, 不用 if(), 不嵌 PowerShell, 用 %~s 短路径
|
||||||
|
|
||||||
|
net session >nul 2>&1
|
||||||
|
if %errorlevel% equ 0 goto :HAS_ADMIN
|
||||||
|
echo Set s = CreateObject("Shell.Application") > "%TEMP%\bw_uac.vbs"
|
||||||
|
echo s.ShellExecute "cmd.exe", "/k cd /d ""%~sdp0"" ^& ""%~snx0""", "", "runas", 1 >> "%TEMP%\bw_uac.vbs"
|
||||||
|
cscript //nologo "%TEMP%\bw_uac.vbs"
|
||||||
|
del /f /q "%TEMP%\bw_uac.vbs" 2>nul
|
||||||
|
exit /b
|
||||||
|
|
||||||
|
:HAS_ADMIN
|
||||||
|
where node >nul 2>nul
|
||||||
|
if %errorlevel% equ 0 goto :HAS_NODE
|
||||||
|
echo [..] Node.js 未安装, 正在通过 winget 安装...
|
||||||
|
winget install OpenJS.NodeJS.LTS --accept-source-agreements --accept-package-agreements --silent 2>nul
|
||||||
|
for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v Path 2^>nul') do set "PATH=%%b"
|
||||||
|
set "PATH=%PATH%;C:\Program Files\nodejs"
|
||||||
|
where node >nul 2>nul
|
||||||
|
if %errorlevel% equ 0 goto :HAS_NODE
|
||||||
|
echo [!!] Node.js 安装失败。请手动下载: https://nodejs.org/
|
||||||
|
goto :END
|
||||||
|
|
||||||
|
:HAS_NODE
|
||||||
|
echo [OK] Node.js 就绪, 启动安装引擎...
|
||||||
|
node "%~dp0setup-all.js" %* || echo [!] 安装过程出错, 请查看上方日志
|
||||||
|
|
||||||
|
:END
|
||||||
|
echo.
|
||||||
|
echo 按任意键关闭...
|
||||||
|
pause > nul
|
||||||
|
endlocal
|
||||||
316
Bookworm-OneClick-Mac.sh
Normal file
316
Bookworm-OneClick-Mac.sh
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# Bookworm Smart Assistant - macOS 全自动安装 v2.0
|
||||||
|
#
|
||||||
|
# 用法 (任选一种):
|
||||||
|
# 方式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.0 — 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 "未找到安装脚本, 执行基础配置..."
|
||||||
|
|
||||||
|
# 解密凭证
|
||||||
|
SECRETS_ENC="$BOOT_DIR/secrets.enc"
|
||||||
|
if [ -f "$SECRETS_ENC" ] && [ -n "$OPENSSL_CMD" ]; then
|
||||||
|
echo ""
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
read -rs -p " 输入主密码解密凭证 (第 $attempt/3 次): " PASSWORD
|
||||||
|
echo ""
|
||||||
|
DECRYPTED=$($OPENSSL_CMD enc -aes-256-cbc -d -pbkdf2 -iter 600000 -in "$SECRETS_ENC" -pass pass:"$PASSWORD" 2>/dev/null) || 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"
|
||||||
|
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 && 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
|
||||||
266
Bookworm-OneClick-Win10.bat
Normal file
266
Bookworm-OneClick-Win10.bat
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 > nul 2>&1
|
||||||
|
title Bookworm Smart Assistant - 全自动安装 (Win10 兼容)
|
||||||
|
|
||||||
|
:: ─── 自动提升管理员权限 ──
|
||||||
|
:: 用 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 "TEMP_DL=%TEMP%\bookworm-setup"
|
||||||
|
set "ERRORS=0"
|
||||||
|
set "NEED_PATH_REFRESH=0"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo +============================================================+
|
||||||
|
echo ^| ^|
|
||||||
|
echo ^| Bookworm Smart Assistant ^|
|
||||||
|
echo ^| 全自动安装 v2.0 (Windows 10 兼容版) ^|
|
||||||
|
echo ^| ^|
|
||||||
|
echo ^| 兼容 Windows 10 1809+ / Windows 11 ^|
|
||||||
|
echo ^| 无需 winget, 通过直接下载安装包实现全自动 ^|
|
||||||
|
echo ^| ^|
|
||||||
|
echo +============================================================+
|
||||||
|
echo.
|
||||||
|
|
||||||
|
if not exist "%TEMP_DL%" mkdir "%TEMP_DL%"
|
||||||
|
|
||||||
|
:: ─── 检测安装方式: winget 优先, 回退直接下载 ─────────
|
||||||
|
set "HAS_WINGET=0"
|
||||||
|
where winget >nul 2>nul
|
||||||
|
if %errorlevel% equ 0 set "HAS_WINGET=1"
|
||||||
|
|
||||||
|
if "%HAS_WINGET%"=="1" (
|
||||||
|
echo [OK] 检测到 winget, 使用 winget 安装
|
||||||
|
) else (
|
||||||
|
echo [i] 未检测到 winget, 使用直接下载方式安装
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: ─── 步骤 1/7: 安装 Git ────────────────────────────
|
||||||
|
echo [1/7] 检查 Git...
|
||||||
|
where git >nul 2>nul
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
if "%HAS_WINGET%"=="1" (
|
||||||
|
echo [..] 通过 winget 安装 Git...
|
||||||
|
winget install Git.Git --accept-source-agreements --accept-package-agreements --silent
|
||||||
|
) else (
|
||||||
|
echo [..] 下载 Git 安装包...
|
||||||
|
powershell -Command "& {[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri 'https://github.com/git-for-windows/git/releases/download/v2.47.1.windows.2/Git-2.47.1.2-64-bit.exe' -OutFile '%TEMP_DL%\git-install.exe'}"
|
||||||
|
if exist "%TEMP_DL%\git-install.exe" (
|
||||||
|
echo [..] 静默安装 Git...
|
||||||
|
"%TEMP_DL%\git-install.exe" /VERYSILENT /NORESTART /NOCANCEL /SP- /CLOSEAPPLICATIONS /RESTARTAPPLICATIONS /COMPONENTS="icons,ext\reg\shellhere,assoc,assoc_sh"
|
||||||
|
if !errorlevel! neq 0 (
|
||||||
|
echo [!!] Git 安装失败 ^(exit: !errorlevel!^)
|
||||||
|
set /a ERRORS+=1
|
||||||
|
) else (
|
||||||
|
echo [OK] Git 安装完成
|
||||||
|
)
|
||||||
|
) else (
|
||||||
|
echo [!!] Git 下载失败, 请手动安装: https://git-scm.com
|
||||||
|
set /a ERRORS+=1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
set "NEED_PATH_REFRESH=1"
|
||||||
|
) else (
|
||||||
|
echo [OK] Git 已安装
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: ─── 步骤 2/7: 安装 Node.js ────────────────────────
|
||||||
|
echo [2/7] 检查 Node.js...
|
||||||
|
where node >nul 2>nul
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
if "%HAS_WINGET%"=="1" (
|
||||||
|
echo [..] 通过 winget 安装 Node.js LTS...
|
||||||
|
winget install OpenJS.NodeJS.LTS --accept-source-agreements --accept-package-agreements --silent
|
||||||
|
) else (
|
||||||
|
echo [..] 下载 Node.js LTS 安装包...
|
||||||
|
powershell -Command "& {[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri 'https://nodejs.org/dist/v22.14.0/node-v22.14.0-x64.msi' -OutFile '%TEMP_DL%\node-install.msi'}"
|
||||||
|
if exist "%TEMP_DL%\node-install.msi" (
|
||||||
|
echo [..] 静默安装 Node.js...
|
||||||
|
msiexec /i "%TEMP_DL%\node-install.msi" /qn /norestart
|
||||||
|
echo [OK] Node.js 安装完成
|
||||||
|
) else (
|
||||||
|
echo [!!] Node.js 下载失败, 请手动安装: https://nodejs.org
|
||||||
|
set /a ERRORS+=1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
set "NEED_PATH_REFRESH=1"
|
||||||
|
) else (
|
||||||
|
echo [OK] Node.js 已安装
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: ─── 刷新 PATH ──────────────────────────────────────
|
||||||
|
if "%NEED_PATH_REFRESH%"=="1" (
|
||||||
|
echo [..] 刷新系统 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!"
|
||||||
|
set "PATH=!PATH!;C:\Program Files\nodejs;C:\Program Files\Git\cmd;C:\Program Files\Git\usr\bin"
|
||||||
|
set "PATH=!PATH!;%APPDATA%\npm"
|
||||||
|
echo [OK] PATH 已刷新
|
||||||
|
echo.
|
||||||
|
)
|
||||||
|
|
||||||
|
:: ─── 二次验证 ────────────────────────────────────────
|
||||||
|
where git >nul 2>nul
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo [!!] Git 仍不可用 — 可能需要重启电脑后重新运行本程序
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
where node >nul 2>nul
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo [!!] Node.js 仍不可用 — 可能需要重启电脑后重新运行本程序
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
:: ─── 步骤 3/7: 安装 Claude Code ─────────────────────
|
||||||
|
echo [3/7] 检查 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 [..] 通过 npm 安装 Claude Code - 淘宝镜像加速...
|
||||||
|
call npm i -g @anthropic-ai/claude-code 2>&1
|
||||||
|
if !errorlevel! neq 0 (
|
||||||
|
echo [!!] Claude Code 安装失败
|
||||||
|
set /a ERRORS+=1
|
||||||
|
) else (
|
||||||
|
echo [OK] Claude Code 安装成功
|
||||||
|
)
|
||||||
|
) else (
|
||||||
|
echo [OK] Claude Code 已安装
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: ─── 步骤 4/7: 代理检测 ──────────────────
|
||||||
|
echo [4/7] 检测网络代理...
|
||||||
|
:: 纯 batch 实现, 不依赖 PowerShell, 不含括号
|
||||||
|
set "PROXY_FOUND="
|
||||||
|
netstat -an 2>nul | findstr "LISTENING" | findstr ":7890 :7893 :7891 :10792 :1080 :8118" >nul 2>nul
|
||||||
|
if !errorlevel! equ 0 set "PROXY_FOUND=1"
|
||||||
|
if defined PROXY_FOUND echo [OK] 检测到本地代理端口
|
||||||
|
if not defined PROXY_FOUND echo [!] 未检测到代理, Claude Code 在国内可能无法启动
|
||||||
|
if not defined PROXY_FOUND echo 请确保代理软件已启动
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: ─── 步骤 5/7: 克隆/更新 Bookworm ──────────────────
|
||||||
|
echo [5/7] 同步 Bookworm 配置...
|
||||||
|
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%" rmdir /s /q "%INSTALL_DIR%" 2>nul
|
||||||
|
echo 首次下载 - 需输入 Gitea 用户名密码...
|
||||||
|
git clone "%GITEA_URL%" "%INSTALL_DIR%" 2>&1
|
||||||
|
if !errorlevel! neq 0 (
|
||||||
|
echo [!!] 下载失败, 请检查网络和 Gitea 凭证
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
echo [OK] Bookworm 文件已就绪
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: ─── 步骤 6/7: 执行安装配置 ────────────────────────
|
||||||
|
echo [6/7] 执行安装配置...
|
||||||
|
echo.
|
||||||
|
echo 首次安装需要输入主密码来解密 API 凭证
|
||||||
|
echo - 主密码由管理员提供, 区分大小写
|
||||||
|
echo.
|
||||||
|
where pwsh >nul 2>nul
|
||||||
|
if !errorlevel! equ 0 (
|
||||||
|
pwsh -NoLogo -ExecutionPolicy Bypass -File "%INSTALL_DIR%\install.ps1"
|
||||||
|
) else (
|
||||||
|
powershell -NoLogo -ExecutionPolicy Bypass -File "%INSTALL_DIR%\install.ps1"
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: ─── 步骤 7/7: 桌面快捷方式 + 完成 ─────────────────
|
||||||
|
echo [7/7] 创建桌面快捷方式...
|
||||||
|
:: 用 VBScript 创建快捷方式, 避免 PowerShell 花括号被 cmd 截获
|
||||||
|
echo Set ws = CreateObject("WScript.Shell") > "%TEMP%\bw_shortcut.vbs"
|
||||||
|
echo Set sc = ws.CreateShortcut(ws.SpecialFolders("Desktop") ^& "\Bookworm.lnk") >> "%TEMP%\bw_shortcut.vbs"
|
||||||
|
echo sc.TargetPath = "%INSTALL_DIR%\启动Bookworm.bat" >> "%TEMP%\bw_shortcut.vbs"
|
||||||
|
echo sc.WorkingDirectory = "%INSTALL_DIR%" >> "%TEMP%\bw_shortcut.vbs"
|
||||||
|
echo sc.Description = "Bookworm Smart Assistant" >> "%TEMP%\bw_shortcut.vbs"
|
||||||
|
echo sc.Save >> "%TEMP%\bw_shortcut.vbs"
|
||||||
|
echo Set sc = ws.CreateShortcut(ws.SpecialFolders("Desktop") ^& "\更新Bookworm.lnk") >> "%TEMP%\bw_shortcut.vbs"
|
||||||
|
echo sc.TargetPath = "%INSTALL_DIR%\更新并启动Bookworm.bat" >> "%TEMP%\bw_shortcut.vbs"
|
||||||
|
echo sc.WorkingDirectory = "%INSTALL_DIR%" >> "%TEMP%\bw_shortcut.vbs"
|
||||||
|
echo sc.Save >> "%TEMP%\bw_shortcut.vbs"
|
||||||
|
cscript //nologo "%TEMP%\bw_shortcut.vbs" 2>nul
|
||||||
|
if !errorlevel! equ 0 echo [OK] 桌面快捷方式已创建
|
||||||
|
del /f /q "%TEMP%\bw_shortcut.vbs" 2>nul
|
||||||
|
|
||||||
|
if exist "%INSTALL_DIR%\guide.html" start "" "%INSTALL_DIR%\guide.html"
|
||||||
|
|
||||||
|
:: 清理临时文件
|
||||||
|
if exist "%TEMP_DL%" rmdir /s /q "%TEMP_DL%" 2>nul
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo +============================================================+
|
||||||
|
echo ^| ^|
|
||||||
|
echo ^| 安装完成! ^|
|
||||||
|
echo ^| ^|
|
||||||
|
echo ^| 已安装: ^|
|
||||||
|
echo ^| [v] Node.js LTS [v] Git ^|
|
||||||
|
echo ^| [v] Claude Code [v] Bookworm - 92 Skills ^|
|
||||||
|
echo ^| ^|
|
||||||
|
echo ^| 桌面快捷方式: ^|
|
||||||
|
echo ^| Bookworm — 日常启动 ^|
|
||||||
|
echo ^| 更新Bookworm — 同步最新版后启动 ^|
|
||||||
|
echo ^| ^|
|
||||||
|
echo +============================================================+
|
||||||
|
echo.
|
||||||
|
|
||||||
|
if %ERRORS% gtr 0 (
|
||||||
|
echo [注意] 安装过程中有 %ERRORS% 个警告, 请查看上方日志
|
||||||
|
echo.
|
||||||
|
)
|
||||||
|
|
||||||
|
echo 按任意键启动 Bookworm...
|
||||||
|
pause > nul
|
||||||
|
|
||||||
|
cd /d "%INSTALL_DIR%"
|
||||||
|
if exist "启动Bookworm.bat" (
|
||||||
|
call "启动Bookworm.bat"
|
||||||
|
) else (
|
||||||
|
where pwsh >nul 2>nul
|
||||||
|
if !errorlevel! equ 0 (
|
||||||
|
pwsh -NoLogo -ExecutionPolicy Bypass -File install.ps1 -StartOnly -AutoAccept
|
||||||
|
) else (
|
||||||
|
powershell -NoLogo -ExecutionPolicy Bypass -File install.ps1 -StartOnly -AutoAccept
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
endlocal
|
||||||
|
|
||||||
|
:: ─── 兜底: 任何情况下窗口不自动关闭 ──
|
||||||
|
echo.
|
||||||
|
echo 如看到此消息说明流程已结束,按任意键关闭窗口...
|
||||||
|
pause > nul
|
||||||
259
Bookworm-OneClick.bat
Normal file
259
Bookworm-OneClick.bat
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
@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/7: winget 检测 ──────────────────────────
|
||||||
|
echo [1/7] 检测包管理器...
|
||||||
|
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/7: 安装 Git ────────────────────────────
|
||||||
|
echo [2/7] 检查 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/7: 安装 Node.js ────────────────────────
|
||||||
|
echo [3/7] 检查 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.
|
||||||
|
|
||||||
|
:: ─── 刷新 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"
|
||||||
|
echo [OK] PATH 已刷新
|
||||||
|
echo.
|
||||||
|
)
|
||||||
|
|
||||||
|
:: ─── 二次验证: Git + Node ────────────────────────────
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
:: ─── 步骤 4/7: 安装 Claude Code ─────────────────────
|
||||||
|
echo [4/7] 检查 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.
|
||||||
|
|
||||||
|
:: ─── 步骤 5/7: 克隆/更新 Bookworm ──────────────────
|
||||||
|
echo [5/7] 同步 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.
|
||||||
|
|
||||||
|
:: ─── 步骤 6/7: 执行安装配置 ────────────────────────
|
||||||
|
echo [6/7] 执行安装配置...
|
||||||
|
echo.
|
||||||
|
|
||||||
|
if exist "%INSTALL_DIR%\install.ps1" (
|
||||||
|
:: 优先 pwsh (PowerShell 7), 回退 powershell (5.1)
|
||||||
|
where pwsh >nul 2>nul
|
||||||
|
if !errorlevel! equ 0 (
|
||||||
|
pwsh -NoLogo -ExecutionPolicy Bypass -File "%INSTALL_DIR%\install.ps1" -AutoAccept
|
||||||
|
) else (
|
||||||
|
powershell -NoLogo -ExecutionPolicy Bypass -File "%INSTALL_DIR%\install.ps1" -AutoAccept
|
||||||
|
)
|
||||||
|
) else (
|
||||||
|
echo [WARN] install.ps1 未找到, 跳过高级配置
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: ─── 步骤 7/7: 创建桌面快捷方式 + 完成 ──────────────
|
||||||
|
echo [7/7] 创建桌面快捷方式...
|
||||||
|
|
||||||
|
:: 用 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] 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
|
||||||
|
|
||||||
|
:: 启动
|
||||||
|
cd /d "%INSTALL_DIR%"
|
||||||
|
if exist "启动Bookworm.bat" (
|
||||||
|
call "启动Bookworm.bat"
|
||||||
|
) else (
|
||||||
|
where pwsh >nul 2>nul
|
||||||
|
if !errorlevel! equ 0 (
|
||||||
|
pwsh -NoLogo -ExecutionPolicy Bypass -File install.ps1 -StartOnly -AutoAccept
|
||||||
|
) else (
|
||||||
|
powershell -NoLogo -ExecutionPolicy Bypass -File install.ps1 -StartOnly -AutoAccept
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
endlocal
|
||||||
163
Bookworm-Setup.bat
Normal file
163
Bookworm-Setup.bat
Normal 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.5 ^|
|
||||||
|
echo ^| ^|
|
||||||
|
echo ^| 92 Skills / 18 Agents / 34 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
|
||||||
|
)
|
||||||
232
Bookworm-Setup.sh
Normal file
232
Bookworm-Setup.sh
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# Bookworm Portable - macOS One-Click Setup
|
||||||
|
# Version: 1.5
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
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' # No Color
|
||||||
|
BOLD='\033[1m'
|
||||||
|
|
||||||
|
# 配置
|
||||||
|
GITEA_URL="https://code.letcareme.com/bookworm/bookworm-boot.git"
|
||||||
|
BOOT_DIR="$HOME/bookworm-boot"
|
||||||
|
|
||||||
|
banner() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN} ____ _"
|
||||||
|
echo " | __ ) ___ ___ | | ____ _____ _ __ _ __ ___"
|
||||||
|
echo " | _ \\ / _ \\ / _ \\| |/ /\\ \\ /\\ / / _ \\| '__| '_ \` _ \\"
|
||||||
|
echo " | |_) | (_) | (_) | < \\ V V / (_) | | | | | | | |"
|
||||||
|
echo " |____/ \\___/ \\___/|_|\\_\\ \\_/\\_/ \\___/|_| |_| |_| |_|"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BOLD}Portable macOS Setup v1.5${NC}"
|
||||||
|
echo -e " ${BLUE}92 Skills | 18 Agents | 29 Hooks${NC}"
|
||||||
|
echo -e "${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||||
|
success() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||||
|
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
|
error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||||
|
step() { echo -e "\n${BOLD}[$1/$TOTAL_STEPS]${NC} ${CYAN}$2${NC}"; }
|
||||||
|
|
||||||
|
TOTAL_STEPS=6
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Step 0: 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)"
|
||||||
|
# Apple Silicon PATH
|
||||||
|
if [ -f /opt/homebrew/bin/brew ]; then
|
||||||
|
eval "$(/opt/homebrew/bin/brew shellenv)"
|
||||||
|
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> "$HOME/.zprofile"
|
||||||
|
fi
|
||||||
|
success "Homebrew 安装完成"
|
||||||
|
else
|
||||||
|
success "Homebrew $(brew --version | head -1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Node.js
|
||||||
|
if ! command -v node &> /dev/null; then
|
||||||
|
warn "Node.js 未安装,正在通过 Homebrew 安装..."
|
||||||
|
brew install node
|
||||||
|
success "Node.js 安装完成"
|
||||||
|
else
|
||||||
|
success "Node.js $(node -v)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Git
|
||||||
|
if ! command -v git &> /dev/null; then
|
||||||
|
warn "Git 未安装,正在通过 Homebrew 安装..."
|
||||||
|
brew install git
|
||||||
|
success "Git 安装完成"
|
||||||
|
else
|
||||||
|
success "Git $(git --version)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# openssl
|
||||||
|
OPENSSL_CMD=""
|
||||||
|
if command -v /opt/homebrew/opt/openssl/bin/openssl &> /dev/null; then
|
||||||
|
OPENSSL_CMD="/opt/homebrew/opt/openssl/bin/openssl"
|
||||||
|
elif command -v /usr/local/opt/openssl/bin/openssl &> /dev/null; then
|
||||||
|
OPENSSL_CMD="/usr/local/opt/openssl/bin/openssl"
|
||||||
|
elif command -v openssl &> /dev/null; then
|
||||||
|
OPENSSL_CMD="openssl"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$OPENSSL_CMD" ]; then
|
||||||
|
warn "OpenSSL 未找到,正在安装..."
|
||||||
|
brew install openssl
|
||||||
|
OPENSSL_CMD="/opt/homebrew/opt/openssl/bin/openssl"
|
||||||
|
success "OpenSSL 安装完成"
|
||||||
|
else
|
||||||
|
success "OpenSSL: $($OPENSSL_CMD version)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Claude Code
|
||||||
|
if ! command -v claude &> /dev/null; then
|
||||||
|
warn "Claude Code 未安装,正在通过 npm 安装..."
|
||||||
|
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 "检测网络代理"
|
||||||
|
|
||||||
|
# macOS 系统代理检测
|
||||||
|
PROXY_DETECTED=""
|
||||||
|
if [ -n "$https_proxy" ] || [ -n "$HTTPS_PROXY" ]; then
|
||||||
|
PROXY_DETECTED="${HTTPS_PROXY:-$https_proxy}"
|
||||||
|
success "环境变量代理: $PROXY_DETECTED"
|
||||||
|
elif [ -n "$http_proxy" ] || [ -n "$HTTP_PROXY" ]; then
|
||||||
|
PROXY_DETECTED="${HTTP_PROXY:-$http_proxy}"
|
||||||
|
success "环境变量代理: $PROXY_DETECTED"
|
||||||
|
else
|
||||||
|
# 尝试从 macOS 网络设置检测
|
||||||
|
SCUTIL_PROXY=$(scutil --proxy 2>/dev/null | grep -E "HTTPSPort|HTTPSProxy" || true)
|
||||||
|
if [ -n "$SCUTIL_PROXY" ]; 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" ]; then
|
||||||
|
PROXY_DETECTED="http://$PROXY_HOST:$PROXY_PORT"
|
||||||
|
export HTTPS_PROXY="$PROXY_DETECTED"
|
||||||
|
export HTTP_PROXY="$PROXY_DETECTED"
|
||||||
|
success "macOS 系统代理: $PROXY_DETECTED"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# 尝试常见端口
|
||||||
|
if [ -z "$PROXY_DETECTED" ]; then
|
||||||
|
for PORT in 7890 7893 1087 1080 8118; do
|
||||||
|
if nc -z 127.0.0.1 $PORT 2>/dev/null; then
|
||||||
|
PROXY_DETECTED="http://127.0.0.1:$PORT"
|
||||||
|
export HTTPS_PROXY="$PROXY_DETECTED"
|
||||||
|
export HTTP_PROXY="$PROXY_DETECTED"
|
||||||
|
success "本地代理端口: $PROXY_DETECTED"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$PROXY_DETECTED" ]; then
|
||||||
|
warn "未检测到代理。如果在国内,Claude Code 可能无法启动。"
|
||||||
|
warn "请启动代理软件 (ClashX / Surge / V2Ray) 后重试。"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# NO_PROXY 设置
|
||||||
|
export NO_PROXY="bww.letcareme.com,code.letcareme.com,localhost,127.0.0.1"
|
||||||
|
export no_proxy="$NO_PROXY"
|
||||||
|
success "NO_PROXY 已设置: bww.letcareme.com,code.letcareme.com"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Step 3: 克隆/更新引导仓库
|
||||||
|
# ============================================================
|
||||||
|
step 3 "同步引导仓库"
|
||||||
|
|
||||||
|
if [ -d "$BOOT_DIR/.git" ]; then
|
||||||
|
info "引导仓库已存在,正在更新..."
|
||||||
|
cd "$BOOT_DIR"
|
||||||
|
git pull --ff-only 2>/dev/null || git pull
|
||||||
|
success "引导仓库已更新"
|
||||||
|
else
|
||||||
|
info "正在克隆引导仓库..."
|
||||||
|
git clone "$GITEA_URL" "$BOOT_DIR"
|
||||||
|
cd "$BOOT_DIR"
|
||||||
|
success "引导仓库克隆完成"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Step 4: 运行安装脚本
|
||||||
|
# ============================================================
|
||||||
|
step 4 "执行安装"
|
||||||
|
|
||||||
|
if [ -f "$BOOT_DIR/install-mac.sh" ]; then
|
||||||
|
info "检测到 install-mac.sh,正在执行..."
|
||||||
|
cd "$BOOT_DIR"
|
||||||
|
bash install-mac.sh
|
||||||
|
else
|
||||||
|
# 如果还没有 mac 专用脚本,提示用户
|
||||||
|
warn "macOS 安装脚本尚未就绪。"
|
||||||
|
info "请联系管理员获取 install-mac.sh,或手动参考安装手册操作。"
|
||||||
|
info "安装手册: https://portable.bookwormweb.com/mac"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Step 5: 配置终端别名
|
||||||
|
# ============================================================
|
||||||
|
step 5 "配置终端快捷命令"
|
||||||
|
|
||||||
|
ZSHRC="$HOME/.zshrc"
|
||||||
|
ALIAS_MARKER="# Bookworm Portable aliases"
|
||||||
|
|
||||||
|
if ! grep -q "$ALIAS_MARKER" "$ZSHRC" 2>/dev/null; then
|
||||||
|
cat >> "$ZSHRC" << 'ALIASES'
|
||||||
|
|
||||||
|
# Bookworm Portable aliases
|
||||||
|
alias bookworm='cd ~/bookworm-boot && bash start-mac.sh'
|
||||||
|
alias bookworm-update='cd ~/bookworm-boot && bash install-mac.sh'
|
||||||
|
alias bookworm-stop='cd ~/bookworm-boot && bash stop-mac.sh'
|
||||||
|
ALIASES
|
||||||
|
success "已添加别名到 ~/.zshrc (bookworm / bookworm-update / bookworm-stop)"
|
||||||
|
else
|
||||||
|
success "终端别名已配置"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Step 6: 完成
|
||||||
|
# ============================================================
|
||||||
|
step 6 "安装完成"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}============================================================${NC}"
|
||||||
|
echo -e "${GREEN} Bookworm Portable for macOS 安装完成!${NC}"
|
||||||
|
echo -e "${GREEN}============================================================${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BOLD}日常启动:${NC} 在终端输入 ${CYAN}bookworm${NC}"
|
||||||
|
echo -e " ${BOLD}同步更新:${NC} 在终端输入 ${CYAN}bookworm-update${NC}"
|
||||||
|
echo -e " ${BOLD}卸载清理:${NC} 在终端输入 ${CYAN}bookworm-stop${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BLUE}安装手册:${NC} https://portable.bookwormweb.com/mac"
|
||||||
|
echo ""
|
||||||
51
README.txt
Normal file
51
README.txt
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
Bookworm Portable v1.4 - 纯云端便携部署工具包
|
||||||
|
================================================
|
||||||
|
|
||||||
|
=== 文件说明 ===
|
||||||
|
|
||||||
|
deploy-gitea.sh ECS Gitea 部署 (服务端,执行一次)
|
||||||
|
prepare-repo.ps1 仓库准备 (本机执行一次)
|
||||||
|
encrypt-secrets.ps1 凭证加密 (本机执行一次)
|
||||||
|
settings.template.json settings.json 模板
|
||||||
|
settings.local.template.json settings.local.json 模板 (权限白名单)
|
||||||
|
install.ps1 安装/启动 (目标机执行)
|
||||||
|
stop.ps1 清理/卸载 (目标机执行)
|
||||||
|
|
||||||
|
=== 一次性部署 ===
|
||||||
|
|
||||||
|
步骤 1: 部署 Gitea (ECS)
|
||||||
|
> scp deploy-gitea.sh root@8.138.11.105:/tmp/
|
||||||
|
> ssh root@8.138.11.105 "GITEA_ADMIN_PASS='你的密码' bash /tmp/deploy-gitea.sh"
|
||||||
|
> 登录 http://8.138.11.105:3000 创建两个私有仓库:
|
||||||
|
- bookworm-config (系统文件)
|
||||||
|
- bookworm-boot (引导脚本+加密凭证)
|
||||||
|
|
||||||
|
步骤 2: 推送 Bookworm 配置
|
||||||
|
> .\prepare-repo.ps1 -GitUrl "http://8.138.11.105:3000/bookworm/bookworm-config.git"
|
||||||
|
|
||||||
|
步骤 3: 加密凭证
|
||||||
|
> .\encrypt-secrets.ps1
|
||||||
|
> (输入中转站 API Key + MCP 凭证 + 设置主密码,至少 12 位)
|
||||||
|
|
||||||
|
步骤 4: 推送 boot 仓库
|
||||||
|
> 将 install.ps1, stop.ps1, secrets.enc 推送到 bookworm-boot 仓库
|
||||||
|
|
||||||
|
=== 目标机使用 ===
|
||||||
|
|
||||||
|
安装: .\install.ps1
|
||||||
|
清理: .\stop.ps1
|
||||||
|
恢复: .\stop.ps1 -Restore
|
||||||
|
深度: .\stop.ps1 -Deep
|
||||||
|
|
||||||
|
=== 目标机要求 ===
|
||||||
|
|
||||||
|
[必须] Claude Code, Node.js >= 18, Git
|
||||||
|
[可选] Python 3.x, openssl (Git for Windows 自带)
|
||||||
|
|
||||||
|
=== 安全规格 ===
|
||||||
|
|
||||||
|
加密: AES-256-CBC + PBKDF2 (600000 迭代, OWASP 2023)
|
||||||
|
凭证: 仅进程级环境变量,不写磁盘/注册表
|
||||||
|
Gitea: INSTALL_LOCK=true, 注册关闭, 管理员 CLI 创建
|
||||||
|
密码: openssl stdin 管道传入,不暴露在进程列表
|
||||||
|
校验: Gitea 二进制 SHA256 完整性校验
|
||||||
158
crypto-helper.js
Normal file
158
crypto-helper.js
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
#!/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);
|
||||||
|
}
|
||||||
227
deploy-gitea.sh
Normal file
227
deploy-gitea.sh
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
#!/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 "========================================="
|
||||||
85
download-panel.html
Normal file
85
download-panel.html
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<!-- DOWNLOAD Panel -->
|
||||||
|
<section class="panel" data-panel="download" style="display:none">
|
||||||
|
<div style="max-width:640px;margin:0 auto;padding:2rem 1rem">
|
||||||
|
|
||||||
|
<div style="text-align:center;margin-bottom:2rem">
|
||||||
|
<div style="font-size:3rem;margin-bottom:0.5rem">📚</div>
|
||||||
|
<h2 style="font-size:1.6rem;margin-bottom:0.3rem">Bookworm <span style="color:var(--accent,#58a6ff)">Portable</span></h2>
|
||||||
|
<p style="color:var(--text-secondary,#8b949e);font-size:0.95rem">AI 编程助手桌面版 — 一键安装到你的电脑</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;gap:0.5rem;justify-content:center;flex-wrap:wrap;margin-bottom:1.5rem">
|
||||||
|
<span style="background:var(--bg-tertiary,#21262d);border:1px solid var(--border,#30363d);border-radius:20px;padding:0.25rem 0.75rem;font-size:0.8rem"><b>92</b> Skills</span>
|
||||||
|
<span style="background:var(--bg-tertiary,#21262d);border:1px solid var(--border,#30363d);border-radius:20px;padding:0.25rem 0.75rem;font-size:0.8rem"><b>18</b> Agents</span>
|
||||||
|
<span style="background:var(--bg-tertiary,#21262d);border:1px solid var(--border,#30363d);border-radius:20px;padding:0.25rem 0.75rem;font-size:0.8rem"><b>29</b> Hooks</span>
|
||||||
|
<span style="background:var(--bg-tertiary,#21262d);border:1px solid var(--border,#30363d);border-radius:20px;padding:0.25rem 0.75rem;font-size:0.8rem"><b>AES-256</b> 加密</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:var(--bg-secondary,#161b22);border:1px solid var(--border,#30363d);border-radius:12px;padding:1.5rem;text-align:center;margin-bottom:1.5rem">
|
||||||
|
<h3 style="margin-bottom:0.8rem;font-size:1.1rem">Windows 桌面版</h3>
|
||||||
|
<p style="color:var(--text-secondary,#8b949e);font-size:0.9rem;margin-bottom:1.2rem">
|
||||||
|
在你的电脑上运行 Bookworm,完整的 Claude Code + 97 个专家 Skills<br>
|
||||||
|
离线配置,API 通过加密中转站转发
|
||||||
|
</p>
|
||||||
|
<a id="downloadSetupBtn" href="/api/download/setup" download="Bookworm-Setup.bat"
|
||||||
|
style="display:inline-block;background:linear-gradient(135deg,#238636,#2ea043);color:#fff;font-size:1rem;font-weight:600;padding:0.75rem 2rem;border-radius:8px;text-decoration:none;cursor:pointer">
|
||||||
|
⬇ 下载安装程序 (4 KB)
|
||||||
|
</a>
|
||||||
|
<p style="color:var(--text-secondary,#8b949e);font-size:0.75rem;margin-top:0.5rem">Bookworm-Setup.bat — 双击即可安装</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:var(--bg-secondary,#161b22);border:1px solid var(--border,#30363d);border-radius:12px;padding:1.2rem 1.5rem;margin-bottom:1.5rem">
|
||||||
|
<h4 style="margin-bottom:0.8rem;color:var(--accent,#58a6ff)">安装只需 3 步</h4>
|
||||||
|
<div style="display:flex;gap:0.8rem;align-items:flex-start;margin-bottom:0.6rem">
|
||||||
|
<span style="background:var(--accent,#58a6ff);color:#000;width:22px;height:22px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:0.75rem;font-weight:700;flex-shrink:0">1</span>
|
||||||
|
<span>下载上方安装程序,双击运行</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:0.8rem;align-items:flex-start;margin-bottom:0.6rem">
|
||||||
|
<span style="background:var(--accent,#58a6ff);color:#000;width:22px;height:22px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:0.75rem;font-weight:700;flex-shrink:0">2</span>
|
||||||
|
<span>输入管理员提供的密码</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:0.8rem;align-items:flex-start">
|
||||||
|
<span style="background:#3fb950;color:#000;width:22px;height:22px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:0.75rem;font-weight:700;flex-shrink:0">✓</span>
|
||||||
|
<span>完成!桌面出现 Bookworm 图标,双击启动</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:var(--bg-secondary,#161b22);border:1px solid var(--border,#30363d);border-radius:12px;padding:1.2rem 1.5rem;margin-bottom:1.5rem">
|
||||||
|
<h4 style="margin-bottom:0.5rem">系统要求</h4>
|
||||||
|
<table style="width:100%;font-size:0.85rem;border-collapse:collapse">
|
||||||
|
<tr style="border-bottom:1px solid var(--border,#30363d)"><td style="padding:0.4rem 0"><b>操作系统</b></td><td>Windows 10/11</td></tr>
|
||||||
|
<tr style="border-bottom:1px solid var(--border,#30363d)"><td style="padding:0.4rem 0"><b>Node.js</b></td><td>18+ (<a href="https://nodejs.org" target="_blank" style="color:var(--accent,#58a6ff)">下载</a>)</td></tr>
|
||||||
|
<tr style="border-bottom:1px solid var(--border,#30363d)"><td style="padding:0.4rem 0"><b>Git</b></td><td><a href="https://git-scm.com" target="_blank" style="color:var(--accent,#58a6ff)">下载</a></td></tr>
|
||||||
|
<tr><td style="padding:0.4rem 0"><b>网络</b></td><td>需要代理/VPN (国内)</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.8rem;margin-bottom:1.5rem">
|
||||||
|
<div style="background:var(--bg-secondary,#161b22);border:1px solid var(--border,#30363d);border-radius:8px;padding:1rem;text-align:center">
|
||||||
|
<div style="font-size:1.5rem;margin-bottom:0.3rem">⚡</div>
|
||||||
|
<div style="font-weight:600;font-size:0.9rem">30 秒启动</div>
|
||||||
|
<div style="color:var(--text-secondary,#8b949e);font-size:0.75rem">安装后每次双击即可</div>
|
||||||
|
</div>
|
||||||
|
<div style="background:var(--bg-secondary,#161b22);border:1px solid var(--border,#30363d);border-radius:8px;padding:1rem;text-align:center">
|
||||||
|
<div style="font-size:1.5rem;margin-bottom:0.3rem">🔒</div>
|
||||||
|
<div style="font-weight:600;font-size:0.9rem">AES-256 加密</div>
|
||||||
|
<div style="color:var(--text-secondary,#8b949e);font-size:0.75rem">凭证不落盘,进程级隔离</div>
|
||||||
|
</div>
|
||||||
|
<div style="background:var(--bg-secondary,#161b22);border:1px solid var(--border,#30363d);border-radius:8px;padding:1rem;text-align:center">
|
||||||
|
<div style="font-size:1.5rem;margin-bottom:0.3rem">🚀</div>
|
||||||
|
<div style="font-weight:600;font-size:0.9rem">97 个 AI 专家</div>
|
||||||
|
<div style="color:var(--text-secondary,#8b949e);font-size:0.75rem">全栈开发/安全/架构/测试</div>
|
||||||
|
</div>
|
||||||
|
<div style="background:var(--bg-secondary,#161b22);border:1px solid var(--border,#30363d);border-radius:8px;padding:1rem;text-align:center">
|
||||||
|
<div style="font-size:1.5rem;margin-bottom:0.3rem">🌐</div>
|
||||||
|
<div style="font-weight:600;font-size:0.9rem">中转站 API</div>
|
||||||
|
<div style="color:var(--text-secondary,#8b949e);font-size:0.75rem">无需 Claude 账号</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align:center;color:var(--text-secondary,#8b949e);font-size:0.75rem">
|
||||||
|
Bookworm Portable v1.3 — 如需帮助请联系管理员
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
124
download.html
Normal file
124
download.html
Normal 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">📚</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">
|
||||||
|
⬇ 下载安装程序
|
||||||
|
</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">✓</span> 完成!桌面出现 Bookworm 图标
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="req">
|
||||||
|
<b>⚠ 前置要求:</b><br>
|
||||||
|
• Node.js (<a href="https://nodejs.org" style="color:#58a6ff">下载</a>) + Git (<a href="https://git-scm.com" style="color:#58a6ff">下载</a>)<br>
|
||||||
|
• 代理/VPN 软件 (国内必须,用于首次连接验证)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
177
encrypt-secrets.ps1
Normal file
177
encrypt-secrets.ps1
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Bookworm Portable - 凭证加密工具
|
||||||
|
.DESCRIPTION
|
||||||
|
将 API Key 和 MCP 凭证加密为 secrets.enc,
|
||||||
|
存放于 Gitea/USB 上, 安装时解密为进程级环境变量.
|
||||||
|
.USAGE
|
||||||
|
# 交互式创建加密凭证文件
|
||||||
|
.\encrypt-secrets.ps1
|
||||||
|
|
||||||
|
# 从现有 .env 文件加密
|
||||||
|
.\encrypt-secrets.ps1 -FromFile "C:\path\to\.env"
|
||||||
|
|
||||||
|
# 解密验证
|
||||||
|
.\encrypt-secrets.ps1 -Decrypt
|
||||||
|
#>
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$FromFile,
|
||||||
|
[switch]$Decrypt
|
||||||
|
)
|
||||||
|
|
||||||
|
$ScriptDir = if ($MyInvocation.MyCommand.Path) {
|
||||||
|
Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
} else { $PWD.Path }
|
||||||
|
$SecretsEnc = Join-Path $ScriptDir "secrets.enc"
|
||||||
|
$TempFile = Join-Path $env:TEMP "bw-secrets-$([guid]::NewGuid().ToString('N').Substring(0,8)).tmp"
|
||||||
|
|
||||||
|
# 检查 openssl
|
||||||
|
$opensslCmd = Get-Command openssl -ErrorAction SilentlyContinue
|
||||||
|
if (-not $opensslCmd) {
|
||||||
|
# 搜索常见 Git 安装路径
|
||||||
|
$searchPaths = @(
|
||||||
|
"C:\Program Files\Git\usr\bin\openssl.exe",
|
||||||
|
"D:\Git\usr\bin\openssl.exe",
|
||||||
|
"D:\Git\mingw64\bin\openssl.exe",
|
||||||
|
"C:\Program Files\Git\mingw64\bin\openssl.exe",
|
||||||
|
"C:\Program Files (x86)\Git\usr\bin\openssl.exe"
|
||||||
|
)
|
||||||
|
$found = $searchPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||||
|
if ($found) {
|
||||||
|
$opensslCmd = $found
|
||||||
|
Write-Host "[INFO] 使用 Git 内置 openssl: $found" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "[ERROR] openssl 未找到。请确认 Git for Windows 已安装。" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$opensslCmd = $opensslCmd.Source
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── 解密模式 ─────────────────────────────────────────
|
||||||
|
if ($Decrypt) {
|
||||||
|
if (-not (Test-Path $SecretsEnc)) {
|
||||||
|
Write-Host "[ERROR] secrets.enc 不存在" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$password = Read-Host "输入主密码" -AsSecureString
|
||||||
|
$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password)
|
||||||
|
$plainPwd = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
|
||||||
|
|
||||||
|
Write-Host "`n=== 解密内容 ===" -ForegroundColor Cyan
|
||||||
|
# 通过 stdin 传入密码,避免进程列表泄露
|
||||||
|
$plainPwd | & $opensslCmd enc -aes-256-cbc -d -pbkdf2 -iter 600000 -in $SecretsEnc -pass stdin
|
||||||
|
Write-Host "`n=== 结束 ===" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
$plainPwd = $null
|
||||||
|
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── 加密模式 ─────────────────────────────────────────
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Bookworm Portable - 凭证加密工具" -ForegroundColor Cyan
|
||||||
|
Write-Host " =================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
if ($FromFile -and (Test-Path $FromFile)) {
|
||||||
|
Write-Host "从文件加载: $FromFile" -ForegroundColor Gray
|
||||||
|
$invalidLines = Get-Content $FromFile | Where-Object { $_.Trim() -and $_ -notmatch '^[A-Z][A-Z0-9_]+=.+$' }
|
||||||
|
if ($invalidLines) {
|
||||||
|
Write-Host "[WARN] 以下行格式不正确 (应为 KEY=value):" -ForegroundColor Yellow
|
||||||
|
$invalidLines | ForEach-Object { Write-Host " $_" -ForegroundColor Yellow }
|
||||||
|
}
|
||||||
|
Copy-Item $FromFile $TempFile
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "请输入凭证 (key=value 格式, 每行一个, 空行结束):" -ForegroundColor White
|
||||||
|
Write-Host "常见 Key:" -ForegroundColor Gray
|
||||||
|
Write-Host " ANTHROPIC_API_KEY=sk-ant-..." -ForegroundColor DarkGray
|
||||||
|
Write-Host " ANTHROPIC_BASE_URL=https://your-relay.com/v1" -ForegroundColor DarkGray
|
||||||
|
Write-Host " GITHUB_PERSONAL_ACCESS_TOKEN=ghp_..." -ForegroundColor DarkGray
|
||||||
|
Write-Host " SLACK_BOT_TOKEN=xoxb-..." -ForegroundColor DarkGray
|
||||||
|
Write-Host " ATLASSIAN_API_TOKEN=..." -ForegroundColor DarkGray
|
||||||
|
Write-Host " BROWSERBASE_API_KEY=..." -ForegroundColor DarkGray
|
||||||
|
Write-Host " BROWSERBASE_PROJECT_ID=..." -ForegroundColor DarkGray
|
||||||
|
Write-Host " FIRECRAWL_API_KEY=..." -ForegroundColor DarkGray
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
$lines = @()
|
||||||
|
while ($true) {
|
||||||
|
$line = Read-Host ">"
|
||||||
|
if ([string]::IsNullOrWhiteSpace($line)) { break }
|
||||||
|
if ($line -match '^[A-Z][A-Z0-9_]+=.+$') {
|
||||||
|
$lines += $line
|
||||||
|
$key = $line.Substring(0, $line.IndexOf('='))
|
||||||
|
Write-Host " [+] $key" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host " [!] 格式不正确,应为 KEY=value" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($lines.Count -eq 0) {
|
||||||
|
Write-Host "[!] 未输入任何凭证,退出" -ForegroundColor Yellow
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines -join "`n" | Set-Content $TempFile -NoNewline
|
||||||
|
}
|
||||||
|
|
||||||
|
# 设置密码并加密
|
||||||
|
Write-Host ""
|
||||||
|
$password1 = Read-Host "设置主密码 (用于解密, 至少 12 位)" -AsSecureString
|
||||||
|
$password2 = Read-Host "确认主密码" -AsSecureString
|
||||||
|
|
||||||
|
$bstr1 = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password1)
|
||||||
|
$bstr2 = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password2)
|
||||||
|
$pwd1 = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr1)
|
||||||
|
$pwd2 = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr2)
|
||||||
|
|
||||||
|
if ($pwd1 -ne $pwd2) {
|
||||||
|
Write-Host "[ERROR] 密码不匹配" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pwd1.Length -lt 12) {
|
||||||
|
Write-Host "[ERROR] 密码至少 12 位 (推荐 16+ 位混合字符)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# AES-256-CBC 加密, PBKDF2 600000 迭代 (OWASP 2023), 通过 stdin 传入密码
|
||||||
|
$pwd1 | & $opensslCmd enc -aes-256-cbc -pbkdf2 -iter 600000 -salt -in $TempFile -out $SecretsEnc -pass stdin
|
||||||
|
|
||||||
|
# 安全删除临时文件
|
||||||
|
if (Test-Path $TempFile) {
|
||||||
|
$bytes = [System.IO.File]::ReadAllBytes($TempFile)
|
||||||
|
[Array]::Clear($bytes, 0, $bytes.Length)
|
||||||
|
[System.IO.File]::WriteAllBytes($TempFile, $bytes)
|
||||||
|
Remove-Item $TempFile -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# 清除内存
|
||||||
|
$pwd1 = $null; $pwd2 = $null
|
||||||
|
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr1)
|
||||||
|
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr2)
|
||||||
|
|
||||||
|
$size = (Get-Item $SecretsEnc).Length
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " [OK] secrets.enc 已生成 ($size bytes)" -ForegroundColor Green
|
||||||
|
Write-Host " 加密: AES-256-CBC + PBKDF2 (600000 迭代)" -ForegroundColor Gray
|
||||||
|
Write-Host " 路径: $SecretsEnc" -ForegroundColor Gray
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " 重要提醒:" -ForegroundColor Yellow
|
||||||
|
Write-Host " - 主密码无法找回,请牢记" -ForegroundColor Yellow
|
||||||
|
Write-Host " - 推送到 Gitea bookworm-boot 仓库即可" -ForegroundColor Yellow
|
||||||
|
Write-Host " - 验证: .\encrypt-secrets.ps1 -Decrypt" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
# 确保任何退出路径都清理临时文件
|
||||||
|
if (Test-Path $TempFile -ErrorAction SilentlyContinue) {
|
||||||
|
Remove-Item $TempFile -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
51
generate-integrity.ps1
Normal file
51
generate-integrity.ps1
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<#
|
||||||
|
.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.template.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
|
||||||
790
guide-mac.html
Normal file
790
guide-mac.html
Normal file
@ -0,0 +1,790 @@
|
|||||||
|
<!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 for Mac - 保姆式安装手册</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, 'SF Pro Display', 'PingFang SC', 'Helvetica Neue', 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 .platform-badge {
|
||||||
|
display: inline-flex; align-items: center; gap: 0.4rem;
|
||||||
|
background: rgba(88,166,255,0.15); border: 1px solid rgba(88,166,255,0.3);
|
||||||
|
border-radius: 20px; padding: 0.3rem 0.9rem; font-size: 0.9rem;
|
||||||
|
color: var(--accent); margin-bottom: 0.8rem;
|
||||||
|
}
|
||||||
|
.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); }
|
||||||
|
|
||||||
|
.platform-switch {
|
||||||
|
display: flex; gap: 0.6rem; justify-content: center; margin-top: 1.2rem;
|
||||||
|
}
|
||||||
|
.platform-switch a {
|
||||||
|
display: inline-flex; align-items: center; gap: 0.4rem;
|
||||||
|
padding: 0.5rem 1.2rem; border-radius: 8px; font-size: 0.9rem;
|
||||||
|
font-weight: 600; text-decoration: none; transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.platform-switch .active {
|
||||||
|
background: var(--accent); color: #000;
|
||||||
|
}
|
||||||
|
.platform-switch .inactive {
|
||||||
|
background: var(--card); color: var(--text-dim);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.platform-switch .inactive:hover {
|
||||||
|
border-color: var(--accent); color: var(--text); text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 'SF Mono', 'Menlo', 'Monaco', 'Cascadia Code', 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>
|
||||||
|
<div class="platform-badge"> macOS Edition</div>
|
||||||
|
<h1>Bookworm <span>Portable</span> 保姆式安装手册</h1>
|
||||||
|
<p>从零开始,一步步教你在任意 Mac 电脑上激活 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>
|
||||||
|
<div class="platform-switch">
|
||||||
|
<a href="/mac" class="active"> macOS</a>
|
||||||
|
<a href="/" class="inactive"> Windows</a>
|
||||||
|
</div>
|
||||||
|
<a href="/Bookworm-Setup.sh" 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'">⬇ 下载一键安装脚本</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.sh</strong> → 在终端运行 → 输入密码 → 完成</p>
|
||||||
|
<p style="font-size:0.8rem;color:var(--text-dim)">脚本自动检测依赖、安装 Homebrew/Node.js/Git、下载配置、启动 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)">Homebrew + Node.js</small></div>
|
||||||
|
<span class="flow-arrow">➔</span>
|
||||||
|
<div class="flow-node">安装 Claude Code<br><small style="color:var(--text-dim)">npm 全局安装</small></div>
|
||||||
|
<span class="flow-arrow">➔</span>
|
||||||
|
<div class="flow-node">运行安装脚本<br><small style="color:var(--text-dim)">或 git clone</small></div>
|
||||||
|
<span class="flow-arrow">➔</span>
|
||||||
|
<div class="flow-node">输入密码<br><small style="color:var(--text-dim)">主密码</small></div>
|
||||||
|
<span class="flow-arrow">➔</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 分钟(含依赖下载),之后每次启动约 5-15 秒</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<!-- 第一步:安装依赖 -->
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<div class="section">
|
||||||
|
<h2><span class="num">1</span>安装依赖软件</h2>
|
||||||
|
|
||||||
|
<div class="alert danger">
|
||||||
|
<span class="alert-icon">⚠</span>
|
||||||
|
<div>
|
||||||
|
<strong>国内必须:代理/VPN 软件</strong><br>
|
||||||
|
Claude Code 启动时会检查 <code>api.anthropic.com</code>,国内无法直连。<br>
|
||||||
|
请先安装并启动代理软件(ClashX / Surge / V2Ray / 任意 VPN),安装脚本会<strong>自动检测</strong>系统代理。<br>
|
||||||
|
无代理 = Claude Code 无法启动。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert info">
|
||||||
|
<span class="alert-icon">💡</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>
|
||||||
|
|
||||||
|
<!-- Homebrew -->
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-icon blue">A</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<h4>安装 Homebrew(macOS 包管理器)</h4>
|
||||||
|
<p>Homebrew 是 macOS 上最常用的包管理器,后续用它安装 Node.js 和 Git。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>打开 <strong>终端</strong>(按 <code>⌘ + 空格</code> 搜索 "终端" 或 "Terminal"),执行:</p>
|
||||||
|
<div class="code-block" onclick="copyCode(this)">
|
||||||
|
<code><span class="cmd">/bin/bash</span> -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert info">
|
||||||
|
<span class="alert-icon">💡</span>
|
||||||
|
<div>
|
||||||
|
<strong>国内加速</strong><br>
|
||||||
|
如果下载太慢,可以使用清华镜像:<br>
|
||||||
|
<code>export HOMEBREW_BREW_GIT_REMOTE="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git"</code><br>
|
||||||
|
<code>export HOMEBREW_CORE_GIT_REMOTE="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git"</code><br>
|
||||||
|
设置后重新执行上面的安装命令。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert warning">
|
||||||
|
<span class="alert-icon">⚠</span>
|
||||||
|
<div>
|
||||||
|
<strong>Apple Silicon (M1/M2/M3/M4) 用户注意!</strong><br>
|
||||||
|
安装完成后,需要将 Homebrew 添加到 PATH。终端会提示你执行以下命令:
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="code-block" onclick="copyCode(this)">
|
||||||
|
<code><span class="comment"># Apple Silicon Mac 需要执行(Intel Mac 不需要)</span>
|
||||||
|
<span class="cmd">echo</span> 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
|
||||||
|
<span class="cmd">eval</span> "$(/opt/homebrew/bin/brew shellenv)"</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top:0.8rem">验证安装成功:</p>
|
||||||
|
<div class="code-block" onclick="copyCode(this)">
|
||||||
|
<code><span class="cmd">brew</span> --version <span class="comment"># 应显示 Homebrew 4.x.x</span></code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Node.js -->
|
||||||
|
<div class="step" style="margin-top:1.5rem">
|
||||||
|
<div class="step-icon blue">B</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<h4>安装 Node.js(必须)</h4>
|
||||||
|
<p>用 Homebrew 一行命令安装。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="code-block" onclick="copyCode(this)">
|
||||||
|
<code><span class="cmd">brew</span> install node</code>
|
||||||
|
</div>
|
||||||
|
<p style="color:var(--text-dim);font-size:0.85rem">也可以从 <a href="https://nodejs.org/zh-cn" target="_blank">nodejs.org</a> 下载 .pkg 安装包。</p>
|
||||||
|
|
||||||
|
<p style="margin-top:0.5rem">验证安装成功:</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>
|
||||||
|
|
||||||
|
<!-- Git -->
|
||||||
|
<div class="step" style="margin-top:1.5rem">
|
||||||
|
<div class="step-icon blue">C</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<h4>安装 Git(必须)</h4>
|
||||||
|
<p>macOS 通常自带 Git,如没有则用 Homebrew 安装。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="code-block" onclick="copyCode(this)">
|
||||||
|
<code><span class="comment"># 检查是否已安装</span>
|
||||||
|
<span class="cmd">git</span> --version
|
||||||
|
|
||||||
|
<span class="comment"># 如果提示安装 Xcode Command Line Tools,点击"安装"即可</span>
|
||||||
|
<span class="comment"># 或者用 Homebrew 安装:</span>
|
||||||
|
<span class="cmd">brew</span> install git</code>
|
||||||
|
</div>
|
||||||
|
</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>在终端中执行:</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">安装过程需要几分钟,等待完成即可。如果报权限错误,在前面加 <code>sudo</code>。</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">💡</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>在终端中执行以下命令。系统会提示输入用户名和密码。</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>在终端中执行安装脚本:</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="code-block" onclick="copyCode(this)">
|
||||||
|
<code><span class="cmd">bash</span> install-mac.sh</code>
|
||||||
|
</div>
|
||||||
|
<p style="color:var(--text-dim);font-size:0.85rem">如果提示权限不足:<code>chmod +x install-mac.sh && ./install-mac.sh</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/8] 到 [8/8],自动完成:</p>
|
||||||
|
<ul style="color:var(--text-dim);font-size:0.9rem;padding-left:1.2rem;margin-top:0.3rem">
|
||||||
|
<li>[1/8] 前置检查 (Claude Code / Node.js / Git / openssl)</li>
|
||||||
|
<li>[2/8] 自动检测代理 + 设置 NO_PROXY</li>
|
||||||
|
<li>[3/8] 解密凭证 (输入主密码)</li>
|
||||||
|
<li>[4/8] 同步配置 (下载 92 个 Skills)</li>
|
||||||
|
<li>[5/8] 完整性校验 (SHA256 哈希验证)</li>
|
||||||
|
<li>[6/8] 渲染配置模板</li>
|
||||||
|
<li>[7/8] Bookworm 系统验证 + MCP 检查</li>
|
||||||
|
<li>[8/8] 启动 Claude Code</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert success">
|
||||||
|
<span class="alert-icon">✓</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">⚠</span>
|
||||||
|
<div>
|
||||||
|
<strong>看到 "原生模式启动" 黄色横幅?</strong><br>
|
||||||
|
说明 Bookworm 配置不完整。请重新运行安装脚本,或联系管理员。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<!-- 日常使用 -->
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<div class="section">
|
||||||
|
<h2><span class="num">4</span>日常使用</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>方法一:终端别名(推荐,最简单)</h3>
|
||||||
|
<p>安装脚本已自动添加别名到 <code>~/.zshrc</code>,直接在终端输入:</p>
|
||||||
|
</div>
|
||||||
|
<div class="code-block" onclick="copyCode(this)">
|
||||||
|
<code><span class="cmd">bookworm</span> <span class="comment"># 快速启动</span>
|
||||||
|
<span class="cmd">bookworm-update</span> <span class="comment"># 同步更新后启动</span></code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="margin-top:1rem">
|
||||||
|
<h3>方法二:脚本命令</h3>
|
||||||
|
<p>如果别名不可用,在终端中手动执行:</p>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<tr><th>操作</th><th>命令</th><th>说明</th></tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>快速启动</strong></td>
|
||||||
|
<td><code>cd ~/bookworm-boot && bash start-mac.sh</code></td>
|
||||||
|
<td>直接启动,不更新配置</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>同步更新</strong></td>
|
||||||
|
<td><code>cd ~/bookworm-boot && bash install-mac.sh</code></td>
|
||||||
|
<td>先同步最新 Skills 再启动</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="alert info">
|
||||||
|
<span class="alert-icon">💡</span>
|
||||||
|
<div>
|
||||||
|
<strong>启动时显示 "有 N 个新更新可用"?</strong><br>
|
||||||
|
说明管理员更新了 Skills 或 Hooks。执行 <code>bookworm-update</code> 即可同步。
|
||||||
|
</div>
|
||||||
|
</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">💡</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">凭证缓存在 macOS 钥匙串 (Keychain) 中,仅当前用户可读。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert warning" style="margin-top:0.8rem">
|
||||||
|
<span class="alert-icon">🔒</span>
|
||||||
|
<div><strong>主密码无法找回</strong> — 忘记后联系管理员重新生成 secrets.enc。</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<!-- 使用完毕 -->
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<div class="section">
|
||||||
|
<h2><span class="num">6</span>使用完毕 — 清理 / 卸载</h2>
|
||||||
|
|
||||||
|
<p>在终端中执行清理命令:</p>
|
||||||
|
<table>
|
||||||
|
<tr><th>场景</th><th>命令</th><th>说明</th></tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>基础清理</strong></td>
|
||||||
|
<td><code>bash stop-mac.sh</code></td>
|
||||||
|
<td>清除环境变量,保留配置供下次快速启动</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>完整恢复</strong></td>
|
||||||
|
<td><code>bash stop-mac.sh --restore</code></td>
|
||||||
|
<td>删除 Bookworm,恢复电脑原始状态</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>深度清理</strong></td>
|
||||||
|
<td><code>bash stop-mac.sh --restore --deep</code></td>
|
||||||
|
<td>完整恢复 + 清除历史 + 清除 Git 凭证 + 钥匙串</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="alert danger">
|
||||||
|
<span class="alert-icon">⚠</span>
|
||||||
|
<div>
|
||||||
|
<strong>在他人电脑/公用电脑上务必清理:</strong><br>
|
||||||
|
执行 <code>cd ~/bookworm-boot && bash stop-mac.sh --restore --deep</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<!-- 故障排查 -->
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<div class="section">
|
||||||
|
<h2><span class="num">!</span>常见问题排查</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>❌ brew 命令找不到</h3>
|
||||||
|
<p><strong>原因:</strong>Homebrew 未添加到 PATH(Apple Silicon Mac 常见)。</p>
|
||||||
|
<p><strong>解决:</strong></p>
|
||||||
|
</div>
|
||||||
|
<div class="code-block" onclick="copyCode(this)">
|
||||||
|
<code><span class="comment"># Apple Silicon (M1/M2/M3/M4)</span>
|
||||||
|
<span class="cmd">eval</span> "$(/opt/homebrew/bin/brew shellenv)"
|
||||||
|
|
||||||
|
<span class="comment"># Intel Mac</span>
|
||||||
|
<span class="cmd">eval</span> "$(/usr/local/bin/brew shellenv)"</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>❌ npm 全局安装报 Permission denied</h3>
|
||||||
|
<p><strong>原因:</strong>macOS 默认目录权限限制。</p>
|
||||||
|
<p><strong>解决方式一(推荐):</strong>修改 npm 全局目录:</p>
|
||||||
|
</div>
|
||||||
|
<div class="code-block" onclick="copyCode(this)">
|
||||||
|
<code><span class="cmd">mkdir</span> -p ~/.npm-global
|
||||||
|
<span class="cmd">npm</span> config set prefix '~/.npm-global'
|
||||||
|
<span class="cmd">echo</span> 'export PATH=~/.npm-global/bin:$PATH' >> ~/.zshrc
|
||||||
|
<span class="cmd">source</span> ~/.zshrc
|
||||||
|
|
||||||
|
<span class="comment"># 然后重新安装</span>
|
||||||
|
<span class="cmd">npm</span> i -g @anthropic-ai/claude-code</code>
|
||||||
|
</div>
|
||||||
|
<p style="color:var(--text-dim);font-size:0.85rem">解决方式二:在命令前加 <code>sudo</code>(简单但不推荐长期使用)。</p>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>❌ 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>如果 macOS 弹出钥匙串对话框,点"始终允许"</li>
|
||||||
|
<li>检查网络是否需要代理</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>❌ 解密凭证失败 / 主密码错误</h3>
|
||||||
|
<p><strong>原因:</strong>主密码区分大小写,且无法找回。</p>
|
||||||
|
<p><strong>解决:</strong>仔细检查密码是否正确。如确认忘记,联系管理员重新生成 <code>secrets.enc</code>。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>❌ openssl 版本不兼容</h3>
|
||||||
|
<p><strong>原因:</strong>macOS 自带 LibreSSL,部分加密参数可能不同。</p>
|
||||||
|
<p><strong>解决:</strong>安装 OpenSSL:</p>
|
||||||
|
</div>
|
||||||
|
<div class="code-block" onclick="copyCode(this)">
|
||||||
|
<code><span class="cmd">brew</span> install openssl
|
||||||
|
<span class="comment"># 脚本会自动检测 Homebrew 安装的 openssl 路径</span></code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>❌ ECONNRESET / "Unable to connect to API"</h3>
|
||||||
|
<p><strong>原因:</strong>代理软件把国内中转站 <code>bww.letcareme.com</code> 的流量也走了国际线路。</p>
|
||||||
|
<p><strong>解决:</strong>手动设置 NO_PROXY 后重试:</p>
|
||||||
|
</div>
|
||||||
|
<div class="code-block" onclick="copyCode(this)">
|
||||||
|
<code><span class="comment"># 设置中转站直连(不走代理)</span>
|
||||||
|
<span class="cmd">export</span> NO_PROXY="bww.letcareme.com,code.letcareme.com"
|
||||||
|
|
||||||
|
<span class="comment"># 重新启动</span>
|
||||||
|
<span class="cmd">cd</span> ~/bookworm-boot
|
||||||
|
<span class="cmd">bash</span> start-mac.sh</code>
|
||||||
|
</div>
|
||||||
|
<p style="color:var(--text-dim);font-size:0.85rem">或在代理软件中将 <code>*.letcareme.com</code> 加入直连规则。</p>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>❌ "Not logged in" / 直接运行 claude 报错</h3>
|
||||||
|
<p><strong>原因:</strong>API 凭证是进程级环境变量,只在安装脚本启动的进程中有效。</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>终端输入 <code>bookworm</code>(推荐)</li>
|
||||||
|
<li><code>cd ~/bookworm-boot && bash start-mac.sh</code></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>❌ 安装包下载太慢</h3>
|
||||||
|
<p><strong>解决:</strong>设置淘宝镜像:</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"># Homebrew 清华镜像</span>
|
||||||
|
<span class="cmd">export</span> HOMEBREW_BREW_GIT_REMOTE="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git"
|
||||||
|
|
||||||
|
<span class="comment"># 然后重新安装</span>
|
||||||
|
<span class="cmd">npm</span> i -g @anthropic-ai/claude-code</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>❌ 需要自己的 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>Homebrew 已安装</strong> — <code>brew --version</code> 显示版本号</li>
|
||||||
|
<li><strong>Node.js 已安装</strong> — <code>node -v</code> 显示版本号</li>
|
||||||
|
<li><strong>Git 已安装</strong> — <code>git --version</code> 显示版本号</li>
|
||||||
|
<li><strong>npm 可用</strong> — <code>npm -v</code> 显示版本号</li>
|
||||||
|
<li><strong>Claude Code 已安装</strong> — <code>claude --version</code> 显示版本号</li>
|
||||||
|
<li><strong>已获取 Gitea 账号密码</strong> — 管理员提供</li>
|
||||||
|
<li><strong>已获取主密码</strong> — 管理员提供(用于解密 API 凭证)</li>
|
||||||
|
<li><strong>能访问 code.letcareme.com</strong> — 浏览器打开确认</li>
|
||||||
|
<li><strong>代理/VPN 已启动</strong> — 国内必须,脚本自动检测 (ClashX/Surge/V2Ray 等)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<!-- 快速参考卡片 -->
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>快速参考</h2>
|
||||||
|
<table>
|
||||||
|
<tr><th>操作</th><th>快捷方式</th><th>完整命令</th></tr>
|
||||||
|
<tr><td>首次安装</td><td>git clone + <code>bash install-mac.sh</code></td><td><code>cd ~/bookworm-boot && bash install-mac.sh</code></td></tr>
|
||||||
|
<tr><td>快速启动</td><td><code>bookworm</code></td><td><code>cd ~/bookworm-boot && bash start-mac.sh</code></td></tr>
|
||||||
|
<tr><td>同步更新</td><td><code>bookworm-update</code></td><td><code>cd ~/bookworm-boot && bash install-mac.sh</code></td></tr>
|
||||||
|
<tr><td>基础清理</td><td colspan="2"><code>cd ~/bookworm-boot && bash stop-mac.sh</code></td></tr>
|
||||||
|
<tr><td>完整恢复</td><td colspan="2"><code>cd ~/bookworm-boot && bash stop-mac.sh --restore</code></td></tr>
|
||||||
|
<tr><td>深度清理</td><td colspan="2"><code>cd ~/bookworm-boot && bash stop-mac.sh --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>进程级环境变量 + 可选本日缓存 (macOS Keychain, 当日 23:59 过期)</td></tr>
|
||||||
|
<tr><td>登录保护</td><td>fail2ban (5 次失败/小时 → 封禁 24 小时)</td></tr>
|
||||||
|
</table>
|
||||||
|
<div class="alert warning" style="margin-top:1rem">
|
||||||
|
<span class="alert-icon">🔒</span>
|
||||||
|
<div>
|
||||||
|
<strong>主密码无法找回</strong> — 请妥善保管。忘记后需管理员重新生成加密凭证。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Bookworm Portable v1.5 — macOS 保姆式安装手册<br>
|
||||||
|
© 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>
|
||||||
786
guide.html
Normal file
786
guide.html
Normal file
@ -0,0 +1,786 @@
|
|||||||
|
<!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>90+</strong> 专家技能</span>
|
||||||
|
<span class="badge"><strong>多模态</strong> AI 协作</span>
|
||||||
|
<span class="badge"><strong>AES-256</strong> 加密</span>
|
||||||
|
<span class="badge"><strong>HTTPS</strong> 传输</span>
|
||||||
|
<span class="badge"><strong>NDA</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'">⬇ 下载一键安装器</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) → 双击运行 → 输入密码 → 完成</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">➔</span>
|
||||||
|
<div class="flow-node">安装 Claude Code<br><small style="color:var(--text-dim)">npm 全局安装</small></div>
|
||||||
|
<span class="flow-arrow">➔</span>
|
||||||
|
<div class="flow-node">双击安装器<br><small style="color:var(--text-dim)">或 git clone</small></div>
|
||||||
|
<span class="flow-arrow">➔</span>
|
||||||
|
<div class="flow-node">输入密码<br><small style="color:var(--text-dim)">主密码</small></div>
|
||||||
|
<span class="flow-arrow">➔</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">⚠</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">💡</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">⚠</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">💡</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] 同步配置 (下载专家技能库)</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">✓</span>
|
||||||
|
<div>
|
||||||
|
<strong>看到 "Bookworm 就绪" 绿色横幅就说明成功了!</strong><br>
|
||||||
|
Claude Code 启动后,脚本会验证配置完整性,全部 [OK] 后进入 Bookworm 模式。<br>
|
||||||
|
所有 API 请求通过中转站转发,不需要自己的 Claude 账号。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert warning">
|
||||||
|
<span class="alert-icon">⚠</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">💡</span>
|
||||||
|
<div>
|
||||||
|
<strong>启动时显示 "有新更新可用"?</strong><br>
|
||||||
|
说明管理员推送了更新。双击 <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">💡</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">🔒</span>
|
||||||
|
<div><strong>主密码无法找回</strong> — 忘记后联系管理员重新生成 secrets.enc。</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<!-- 使用完毕 -->
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<div class="section">
|
||||||
|
<h2><span class="num">6</span>使用完毕 — 清理 / 卸载</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">⚠</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>❌ 输入 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>❌ 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>❌ 提示 "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>❌ 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>❌ 解密凭证失败 / 主密码错误</h3>
|
||||||
|
<p><strong>原因:</strong>主密码区分大小写,且无法找回。</p>
|
||||||
|
<p><strong>解决:</strong>仔细检查密码是否正确。如确认忘记,联系管理员重新生成 <code>secrets.enc</code>。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>❌ 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>❌ 安装包下载太慢</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>❌ 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>❌ "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>❌ 完整性校验不匹配(大量文件 WARN)</h3>
|
||||||
|
<p><strong>原因:</strong>本地配置文件已被更新(管理员推送了新版本),但 <code>integrity.sha256</code> 未同步更新。</p>
|
||||||
|
<p><strong>解决:</strong>选 <strong>y</strong> 继续即可,不影响使用。管理员会在下个版本同步哈希文件。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>❌ 需要自己的 Claude 账号吗?</h3>
|
||||||
|
<p><strong>不需要。</strong>所有 API 请求通过中转站转发,消耗中转站额度。目标机不需要任何 Anthropic 账号或订阅。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>❌ 询问 AI 系统内部信息时被拒绝了?</h3>
|
||||||
|
<p><strong>这是正常行为。</strong>Bookworm 的技能库、路由引擎、配置架构属于技术保密范围,AI 被设定为不披露这些信息。</p>
|
||||||
|
<p><strong>解决:</strong>直接告诉 AI 你要完成的任务(写代码、分析问题、设计方案等),它会自动调用最合适的专家能力来帮你。无需了解内部机制即可获得完整服务。</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> — <code>node -v</code> 显示版本号</li>
|
||||||
|
<li><strong>Git 已安装</strong> — <code>git --version</code> 显示版本号</li>
|
||||||
|
<li><strong>npm 可用</strong> — <code>npm -v</code> 显示版本号(如报错先设 ExecutionPolicy)</li>
|
||||||
|
<li><strong>Claude Code 已安装</strong> — <code>claude --version</code> 显示版本号</li>
|
||||||
|
<li><strong>PowerShell 7 已安装</strong> — <code>pwsh --version</code> 显示 7.x(推荐但非必须)</li>
|
||||||
|
<li><strong>已获取 Gitea 账号密码</strong> — 管理员提供</li>
|
||||||
|
<li><strong>已获取主密码</strong> — 管理员提供(用于解密 API 凭证)</li>
|
||||||
|
<li><strong>能访问 code.letcareme.com</strong> — 浏览器打开确认</li>
|
||||||
|
<li><strong>代理/VPN 已启动</strong> — 国内必须,脚本自动检测 (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 次失败/小时 → 封禁 24 小时)</td></tr>
|
||||||
|
</table>
|
||||||
|
<div class="alert warning" style="margin-top:1rem">
|
||||||
|
<span class="alert-icon">🔒</span>
|
||||||
|
<div>
|
||||||
|
<strong>主密码无法找回</strong> — 请妥善保管。忘记后需管理员重新生成加密凭证。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Bookworm Portable v1.5-NDA — 保姆式安装手册<br>
|
||||||
|
© 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>
|
||||||
726
install.ps1
Normal file
726
install.ps1
Normal file
@ -0,0 +1,726 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Bookworm Portable - 安装/启动脚本
|
||||||
|
.DESCRIPTION
|
||||||
|
从 Gitea 私有仓克隆 Bookworm 配置到目标机,
|
||||||
|
解密凭证, 渲染模板, 启动 Claude Code.
|
||||||
|
.USAGE
|
||||||
|
# 首次安装 (从 Gitea 克隆)
|
||||||
|
.\install.ps1
|
||||||
|
|
||||||
|
# 仅启动 (已安装过)
|
||||||
|
.\install.ps1 -StartOnly
|
||||||
|
|
||||||
|
# 指定 Gitea 地址
|
||||||
|
.\install.ps1 -GitUrl "https://code.letcareme.com/bookworm/bookworm-config.git"
|
||||||
|
#>
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$GitUrl = "https://code.letcareme.com/bookworm/bookworm-config.git",
|
||||||
|
[switch]$StartOnly,
|
||||||
|
[switch]$SkipSecrets,
|
||||||
|
[switch]$AutoAccept # 豁免所有人工确认环节
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# ─── 路径定义 ────────────────────────────────────────
|
||||||
|
$ScriptDir = if ($MyInvocation.MyCommand.Path) {
|
||||||
|
Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
} else { $PWD.Path }
|
||||||
|
$ClaudeTarget = Join-Path $env:USERPROFILE ".claude"
|
||||||
|
$BackupPath = Join-Path $env:USERPROFILE ".claude.bw-backup"
|
||||||
|
$SecretsEnc = Join-Path $ScriptDir "secrets.enc"
|
||||||
|
$TemplateFile = Join-Path $ClaudeTarget "settings.template.json"
|
||||||
|
$LocalTplFile = Join-Path $ClaudeTarget "settings.local.template.json"
|
||||||
|
$SettingsFile = Join-Path $ClaudeTarget "settings.json"
|
||||||
|
$LocalSetFile = Join-Path $ClaudeTarget "settings.local.json"
|
||||||
|
|
||||||
|
# ─── openssl 检测 ────────────────────────────────────
|
||||||
|
$cmd = Get-Command openssl -ErrorAction SilentlyContinue
|
||||||
|
$opensslCmd = if ($cmd) { $cmd.Source } else { $null }
|
||||||
|
if (-not $opensslCmd) {
|
||||||
|
$searchPaths = @("C:\Program Files\Git\usr\bin\openssl.exe", "D:\Git\usr\bin\openssl.exe", "D:\Git\mingw64\bin\openssl.exe", "C:\Program Files\Git\mingw64\bin\openssl.exe")
|
||||||
|
$opensslCmd = $searchPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── 辅助函数 ────────────────────────────────────────
|
||||||
|
|
||||||
|
function Write-Banner {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " +------------------------------------------+" -ForegroundColor Cyan
|
||||||
|
Write-Host " | Bookworm Portable Installer v1.5 |" -ForegroundColor Cyan
|
||||||
|
Write-Host " | 92 Skills / 18 Agents / 34 Hooks |" -ForegroundColor Cyan
|
||||||
|
Write-Host " +------------------------------------------+" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-Command($cmd) {
|
||||||
|
return [bool](Get-Command $cmd -ErrorAction SilentlyContinue)
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── 密码本日免输 (Windows Credential Manager) ──────
|
||||||
|
function Get-CachedSecrets {
|
||||||
|
try {
|
||||||
|
$cred = cmdkey /list 2>$null | Select-String "bookworm-secrets"
|
||||||
|
if ($cred) {
|
||||||
|
# 从 Credential Manager 读取缓存的环境变量
|
||||||
|
$regPath = "HKCU:\Software\Bookworm\CachedEnv"
|
||||||
|
if (Test-Path $regPath) {
|
||||||
|
$props = Get-ItemProperty $regPath -ErrorAction SilentlyContinue
|
||||||
|
$loaded = 0
|
||||||
|
foreach ($p in $props.PSObject.Properties) {
|
||||||
|
if ($p.Name -match '^[A-Z_]+$') {
|
||||||
|
[System.Environment]::SetEnvironmentVariable($p.Name, $p.Value, "Process")
|
||||||
|
$loaded++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($loaded -gt 0 -and $env:ANTHROPIC_API_KEY) {
|
||||||
|
Write-Host " [OK] 从本日缓存加载 $loaded 个凭证 (免密)" -ForegroundColor Green
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Save-SecretsToCache {
|
||||||
|
try {
|
||||||
|
# 用 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 }
|
||||||
|
$envKeys = @("ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL", "GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||||
|
"SLACK_BOT_TOKEN", "ATLASSIAN_API_TOKEN", "BROWSERBASE_API_KEY", "FIRECRAWL_API_KEY")
|
||||||
|
foreach ($k in $envKeys) {
|
||||||
|
$v = [System.Environment]::GetEnvironmentVariable($k, "Process")
|
||||||
|
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 (下次免密)" -ForegroundColor Green
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Clear-SecretsCache {
|
||||||
|
cmdkey /delete:bookworm-secrets 2>$null | Out-Null
|
||||||
|
Remove-Item "HKCU:\Software\Bookworm" -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── 依赖自动安装 ────────────────────────────────────
|
||||||
|
function Install-MissingDeps {
|
||||||
|
$missing = @()
|
||||||
|
if (-not (Test-Command "node")) { $missing += "Node.js" }
|
||||||
|
if (-not (Test-Command "git")) { $missing += "Git" }
|
||||||
|
if (-not (Test-Command "claude")) { $missing += "Claude Code" }
|
||||||
|
|
||||||
|
if ($missing.Count -eq 0) { return }
|
||||||
|
|
||||||
|
$hasWinget = Test-Command "winget"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " 缺少以下软件: $($missing -join ', ')" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
if ($hasWinget) {
|
||||||
|
$auto = if ($AutoAccept) { 'y' } else { Read-Host " 是否用 winget 自动安装? (y/n)" }
|
||||||
|
if ($auto -eq 'y') {
|
||||||
|
if ($missing -contains "Node.js") {
|
||||||
|
Write-Host " 安装 Node.js..." -ForegroundColor Gray
|
||||||
|
winget install OpenJS.NodeJS.LTS --accept-source-agreements --accept-package-agreements 2>&1 | Select-Object -Last 3 | ForEach-Object { Write-Host " $_" }
|
||||||
|
# 刷新 PATH
|
||||||
|
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
||||||
|
}
|
||||||
|
if ($missing -contains "Git") {
|
||||||
|
Write-Host " 安装 Git..." -ForegroundColor Gray
|
||||||
|
winget install Git.Git --accept-source-agreements --accept-package-agreements 2>&1 | Select-Object -Last 3 | ForEach-Object { Write-Host " $_" }
|
||||||
|
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
||||||
|
}
|
||||||
|
if ($missing -contains "Claude Code" -and (Test-Command "npm")) {
|
||||||
|
Write-Host " 安装 Claude Code..." -ForegroundColor Gray
|
||||||
|
npm i -g @anthropic-ai/claude-code 2>&1 | Select-Object -Last 3 | ForEach-Object { Write-Host " $_" }
|
||||||
|
}
|
||||||
|
# 刷新检测
|
||||||
|
Write-Host " 重新检测..." -ForegroundColor Gray
|
||||||
|
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " 请手动安装后重新运行本脚本:" -ForegroundColor Yellow
|
||||||
|
if ($missing -contains "Node.js") { Write-Host " Node.js: https://nodejs.org" -ForegroundColor Gray }
|
||||||
|
if ($missing -contains "Git") { Write-Host " Git: https://git-scm.com" -ForegroundColor Gray }
|
||||||
|
if ($missing -contains "Claude Code") { Write-Host " Claude: npm i -g @anthropic-ai/claude-code" -ForegroundColor Gray }
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── 桌面快捷方式 ────────────────────────────────────
|
||||||
|
function New-DesktopShortcuts {
|
||||||
|
$desktop = [System.Environment]::GetFolderPath("Desktop")
|
||||||
|
$bootDir = $ScriptDir
|
||||||
|
|
||||||
|
# 启动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)
|
||||||
|
$scriptPath = Join-Path $bootDir "install.ps1"
|
||||||
|
$hasPwsh = [bool](Get-Command pwsh -ErrorAction SilentlyContinue)
|
||||||
|
$psExe = if ($hasPwsh) { (Get-Command pwsh).Source } else { "powershell.exe" }
|
||||||
|
$shortcut.TargetPath = $psExe
|
||||||
|
$shortcut.Arguments = "-NoLogo -ExecutionPolicy Bypass -Command `"Set-Item Env:NO_PROXY 'bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1'; & '$scriptPath' -StartOnly -AutoAccept`""
|
||||||
|
$shortcut.WorkingDirectory = $bootDir
|
||||||
|
$shortcut.Description = "Bookworm Smart Assistant"
|
||||||
|
$shortcut.Save()
|
||||||
|
$psVer = if ($hasPwsh) { "PowerShell 7" } else { "PowerShell 5.1" }
|
||||||
|
Write-Host " [OK] 桌面快捷方式已创建: Bookworm ($psVer)" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host " [!] 桌面快捷方式创建失败 (不影响使用)" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Decrypt-Secrets {
|
||||||
|
if ($SkipSecrets -or -not (Test-Path $SecretsEnc)) {
|
||||||
|
Write-Host " [!] 跳过凭证解密 (无 secrets.enc)" -ForegroundColor Yellow
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# 优先用 Node.js 解密 (跨平台兼容性最高), 回退 openssl
|
||||||
|
$useNode = (Test-Command "node") -and (Test-Path (Join-Path $ScriptDir "crypto-helper.js"))
|
||||||
|
if (-not $useNode -and -not $opensslCmd) {
|
||||||
|
Write-Host " [!] node 和 openssl 均不可用,跳过凭证解密" -ForegroundColor Yellow
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$cryptoHelper = Join-Path $ScriptDir "crypto-helper.js"
|
||||||
|
|
||||||
|
$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) {
|
||||||
|
# Node.js 解密 (跨平台一致)
|
||||||
|
$decrypted = & node $cryptoHelper decrypt $plainPwd $SecretsEnc 2>&1
|
||||||
|
$decExit = $LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# openssl 回退
|
||||||
|
$decrypted = $plainPwd | & $opensslCmd enc -aes-256-cbc -d -pbkdf2 -iter 600000 -md sha256 -in $SecretsEnc -pass stdin 2>&1
|
||||||
|
$decExit = $LASTEXITCODE
|
||||||
|
}
|
||||||
|
$ErrorActionPreference = $prevEAP
|
||||||
|
|
||||||
|
# 清除内存中的密码
|
||||||
|
$plainPwd = $null
|
||||||
|
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
|
||||||
|
|
||||||
|
if ($decExit -eq 0 -and $decrypted -and $decrypted -notmatch 'PASSWORD_ERROR|FORMAT_ERROR|bad decrypt') {
|
||||||
|
# 解密成功,注入环境变量
|
||||||
|
$decrypted -split "`n" | ForEach-Object {
|
||||||
|
$line = $_.Trim()
|
||||||
|
if ($line -and $line.Contains('=')) {
|
||||||
|
$eqIdx = $line.IndexOf('=')
|
||||||
|
$key = $line.Substring(0, $eqIdx).Trim()
|
||||||
|
$val = $line.Substring($eqIdx + 1).Trim()
|
||||||
|
[System.Environment]::SetEnvironmentVariable($key, $val, "Process")
|
||||||
|
Write-Host " [OK] 已注入: $key" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# 解密失败
|
||||||
|
$remaining = $maxRetries - $attempt
|
||||||
|
if ($remaining -gt 0) {
|
||||||
|
Write-Host " [!!] 密码错误,剩余重试: $remaining 次" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3次全部失败
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " [ABORT] 3 次密码均错误" -ForegroundColor Red
|
||||||
|
Write-Host " 请确认主密码是否正确 (区分大小写)" -ForegroundColor Yellow
|
||||||
|
Write-Host " 如忘记密码,请联系管理员重新生成 secrets.enc" -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function Render-SettingsTemplate {
|
||||||
|
if (-not (Test-Path $TemplateFile)) {
|
||||||
|
Write-Host " [!] 未找到 settings.template.json,跳过渲染" -ForegroundColor Yellow
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$claudeRoot = $ClaudeTarget.Replace('\', '/')
|
||||||
|
# HOME 保留反斜杠格式,与 Claude Code 原始行为一致
|
||||||
|
$homeDir = $env:USERPROFILE
|
||||||
|
|
||||||
|
$content = Get-Content $TemplateFile -Raw
|
||||||
|
$content = $content -replace '\{\{CLAUDE_ROOT\}\}', $claudeRoot
|
||||||
|
$content = $content -replace '\{\{HOME\}\}', ($homeDir -replace '\\', '\\')
|
||||||
|
|
||||||
|
Set-Content $SettingsFile -Value $content -Encoding UTF8
|
||||||
|
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 -replace '\\', '\\')
|
||||||
|
$localContent = $localContent -replace '\{\{USERNAME\}\}', $env:USERNAME
|
||||||
|
Set-Content $LocalSetFile -Value $localContent -Encoding UTF8
|
||||||
|
Write-Host " [OK] settings.local.json 已渲染" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── 代理自动检测 ────────────────────────────────────
|
||||||
|
function Detect-SystemProxy {
|
||||||
|
# 中转站在国内阿里云,不走代理
|
||||||
|
$env:NO_PROXY = "bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1"
|
||||||
|
$env:no_proxy = $env:NO_PROXY
|
||||||
|
|
||||||
|
# 如果已手动设置了 HTTPS_PROXY,直接使用
|
||||||
|
if ($env:HTTPS_PROXY) {
|
||||||
|
Write-Host " [OK] 已设置 HTTPS_PROXY=$($env:HTTPS_PROXY)" -ForegroundColor Green
|
||||||
|
Write-Host " [OK] NO_PROXY=$($env:NO_PROXY)" -ForegroundColor Green
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host " 检测系统代理..." -ForegroundColor Gray
|
||||||
|
|
||||||
|
# 方法1: 通过 .NET 获取系统代理 (最可靠,支持 Clash/V2Ray/快柠檬/系统代理)
|
||||||
|
try {
|
||||||
|
$proxyUri = [System.Net.WebRequest]::DefaultWebProxy.GetProxy("https://api.anthropic.com")
|
||||||
|
if ($proxyUri -and $proxyUri.Authority -ne "api.anthropic.com") {
|
||||||
|
$proxyUrl = "http://$($proxyUri.Authority)"
|
||||||
|
$env:HTTPS_PROXY = $proxyUrl
|
||||||
|
$env:HTTP_PROXY = $proxyUrl
|
||||||
|
Write-Host " [OK] 检测到系统代理: $proxyUrl" -ForegroundColor Green
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
# 方法2: 读取注册表 IE 代理设置
|
||||||
|
try {
|
||||||
|
$reg = Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings" -ErrorAction SilentlyContinue
|
||||||
|
if ($reg.ProxyEnable -eq 1 -and $reg.ProxyServer) {
|
||||||
|
$proxy = $reg.ProxyServer
|
||||||
|
if ($proxy -notmatch '^http') { $proxy = "http://$proxy" }
|
||||||
|
$env:HTTPS_PROXY = $proxy
|
||||||
|
$env:HTTP_PROXY = $proxy
|
||||||
|
Write-Host " [OK] 检测到 IE 代理: $proxy" -ForegroundColor Green
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
# 方法3: 扫描常见代理端口 (500ms 超时,避免阻塞)
|
||||||
|
$commonPorts = @(7890, 7891, 7893, 10792, 10793, 10808, 10809, 1080, 1087, 8080, 8118)
|
||||||
|
foreach ($port in $commonPorts) {
|
||||||
|
try {
|
||||||
|
$tcp = New-Object System.Net.Sockets.TcpClient
|
||||||
|
$result = $tcp.BeginConnect("127.0.0.1", $port, $null, $null)
|
||||||
|
$success = $result.AsyncWaitHandle.WaitOne(500)
|
||||||
|
if (-not $success) { $tcp.Close(); continue }
|
||||||
|
$tcp.EndConnect($result)
|
||||||
|
$tcp.Close()
|
||||||
|
$env:HTTPS_PROXY = "http://127.0.0.1:$port"
|
||||||
|
$env:HTTP_PROXY = "http://127.0.0.1:$port"
|
||||||
|
Write-Host " [OK] 检测到本地代理端口: $port" -ForegroundColor Green
|
||||||
|
return
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 未找到代理
|
||||||
|
Write-Host " [!!] 未检测到代理/VPN" -ForegroundColor Red
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Claude Code 需要代理才能在国内使用 (启动时检查 api.anthropic.com)" -ForegroundColor Yellow
|
||||||
|
Write-Host " 请先启动代理软件 (Clash/V2Ray/快柠檬等),然后重新运行本脚本" -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " 如已有代理,可手动指定:" -ForegroundColor Gray
|
||||||
|
Write-Host " `$env:HTTPS_PROXY = 'http://127.0.0.1:端口号'" -ForegroundColor Gray
|
||||||
|
Write-Host " pwsh -ExecutionPolicy Bypass -File install.ps1" -ForegroundColor Gray
|
||||||
|
Write-Host ""
|
||||||
|
$continue = if ($AutoAccept) { 'y' } else { Read-Host " 无代理继续? (y/n,无代理大概率启动失败)" }
|
||||||
|
if ($continue -ne 'y') { exit 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── 主流程 ──────────────────────────────────────────
|
||||||
|
|
||||||
|
Write-Banner
|
||||||
|
|
||||||
|
# 步骤 1: 前置检查
|
||||||
|
Write-Host "[1/9] 前置检查..." -ForegroundColor White
|
||||||
|
$checks = @(
|
||||||
|
@{ Name = "Claude Code"; OK = (Test-Command "claude") }
|
||||||
|
@{ Name = "Node.js"; OK = (Test-Command "node") }
|
||||||
|
@{ Name = "Git"; OK = (Test-Command "git") }
|
||||||
|
)
|
||||||
|
foreach ($c in $checks) {
|
||||||
|
$icon = if ($c.OK) { "[OK]" } else { "[!!]" }
|
||||||
|
$color = if ($c.OK) { "Green" } else { "Red" }
|
||||||
|
Write-Host " $icon $($c.Name)" -ForegroundColor $color
|
||||||
|
}
|
||||||
|
if (-not (Test-Command "claude") -or -not (Test-Command "node") -or -not (Test-Command "git")) {
|
||||||
|
Install-MissingDeps
|
||||||
|
}
|
||||||
|
# 再次验证
|
||||||
|
if (-not (Test-Command "claude")) {
|
||||||
|
Write-Host "`n [ABORT] Claude Code 未安装" -ForegroundColor Red
|
||||||
|
Write-Host " 安装: npm i -g @anthropic-ai/claude-code" -ForegroundColor Gray
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if (-not (Test-Command "node")) {
|
||||||
|
Write-Host "`n [ABORT] Node.js 未安装" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 步骤 2: 代理检测 (国内必须)
|
||||||
|
Write-Host "`n[2/9] 代理检测..." -ForegroundColor White
|
||||||
|
|
||||||
|
Detect-SystemProxy
|
||||||
|
|
||||||
|
# 步骤 3: 解密凭证 (优先使用本日缓存)
|
||||||
|
Write-Host "`n[3/9] 解密凭证..." -ForegroundColor White
|
||||||
|
# 检查缓存是否过期
|
||||||
|
$cacheExpiry = $null
|
||||||
|
try {
|
||||||
|
$cacheExpiry = Get-ItemProperty "HKCU:\Software\Bookworm\CachedEnv" -Name "_expiry" -ErrorAction SilentlyContinue
|
||||||
|
} catch {}
|
||||||
|
$cacheValid = $false
|
||||||
|
if ($cacheExpiry -and $cacheExpiry._expiry) {
|
||||||
|
try { $cacheValid = [datetime]$cacheExpiry._expiry -gt (Get-Date) } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($cacheValid -and (Get-CachedSecrets)) {
|
||||||
|
# 缓存有效,跳过解密
|
||||||
|
} elseif ($AutoAccept) {
|
||||||
|
# AutoAccept 模式: 无缓存时跳过密码输入
|
||||||
|
Write-Host " [!] AutoAccept 模式: 无有效缓存,跳过凭证解密" -ForegroundColor Yellow
|
||||||
|
Write-Host " 如需凭证,请不加 -AutoAccept 手动运行一次" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Clear-SecretsCache
|
||||||
|
Decrypt-Secrets
|
||||||
|
# 解密成功后询问是否缓存
|
||||||
|
if ($env:ANTHROPIC_API_KEY) {
|
||||||
|
$cache = if ($AutoAccept) { 'y' } else { Read-Host " 今日内免密启动? (y/n)" }
|
||||||
|
if ($cache -eq 'y') { Save-SecretsToCache }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 自动配置 git credential helper (避免 clone 时反复要密码)
|
||||||
|
$prevEAP = $ErrorActionPreference
|
||||||
|
$ErrorActionPreference = "Continue"
|
||||||
|
git config --global credential.helper store 2>$null
|
||||||
|
$ErrorActionPreference = $prevEAP
|
||||||
|
|
||||||
|
# 步骤 4: 克隆/更新仓库
|
||||||
|
if (-not $StartOnly) {
|
||||||
|
Write-Host "`n[4/9] 同步 Bookworm 配置..." -ForegroundColor White
|
||||||
|
|
||||||
|
if (Test-Path $ClaudeTarget) {
|
||||||
|
$isGit = Test-Path (Join-Path $ClaudeTarget ".git")
|
||||||
|
if ($isGit) {
|
||||||
|
Write-Host " 已有仓库,执行 git pull..."
|
||||||
|
Push-Location $ClaudeTarget
|
||||||
|
$prevEAP = $ErrorActionPreference
|
||||||
|
$ErrorActionPreference = "Continue"
|
||||||
|
try {
|
||||||
|
$stashOutput = git stash 2>&1
|
||||||
|
$hasStash = $stashOutput -notmatch 'No local changes'
|
||||||
|
git pull --rebase 2>&1 | ForEach-Object { Write-Host " $_" }
|
||||||
|
if ($hasStash) { git stash pop 2>&1 | Out-Null }
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Host " [WARN] git pull 失败: $_" -ForegroundColor Yellow
|
||||||
|
Write-Host " 使用本地现有版本继续" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$ErrorActionPreference = $prevEAP
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# 安全克隆: 先克隆到临时目录,成功后再替换
|
||||||
|
Write-Host " 备份现有 .claude/ 并克隆..."
|
||||||
|
$tempClone = "$ClaudeTarget.bw-clone-temp"
|
||||||
|
if (Test-Path $tempClone) { Remove-Item $tempClone -Recurse -Force }
|
||||||
|
$prevEAP = $ErrorActionPreference
|
||||||
|
$ErrorActionPreference = "Continue"
|
||||||
|
git clone --depth 1 $GitUrl $tempClone 2>&1 | ForEach-Object { Write-Host " $_" }
|
||||||
|
$cloneExit = $LASTEXITCODE
|
||||||
|
$ErrorActionPreference = $prevEAP
|
||||||
|
|
||||||
|
if ($cloneExit -ne 0 -or -not (Test-Path (Join-Path $tempClone "CLAUDE.md"))) {
|
||||||
|
Write-Host " [ERROR] 克隆失败 (exit=$cloneExit)" -ForegroundColor Red
|
||||||
|
if (Test-Path $tempClone) { Remove-Item $tempClone -Recurse -Force -ErrorAction SilentlyContinue }
|
||||||
|
Write-Host " 原始 .claude/ 未被修改" -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 克隆成功,执行替换
|
||||||
|
if (Test-Path $BackupPath) { Remove-Item $BackupPath -Recurse -Force }
|
||||||
|
Rename-Item $ClaudeTarget $BackupPath
|
||||||
|
Rename-Item $tempClone $ClaudeTarget
|
||||||
|
Write-Host " [OK] 克隆完成,原始配置已备份到 .claude.bw-backup/" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host " 首次安装,克隆仓库..."
|
||||||
|
$prevEAP = $ErrorActionPreference
|
||||||
|
$ErrorActionPreference = "Continue"
|
||||||
|
git clone --depth 1 $GitUrl $ClaudeTarget 2>&1 | ForEach-Object { Write-Host " $_" }
|
||||||
|
$cloneExit = $LASTEXITCODE
|
||||||
|
$ErrorActionPreference = $prevEAP
|
||||||
|
|
||||||
|
if ($cloneExit -ne 0 -or -not (Test-Path (Join-Path $ClaudeTarget "CLAUDE.md"))) {
|
||||||
|
Write-Host " [ERROR] 克隆失败 (exit=$cloneExit)" -ForegroundColor Red
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " 可能原因:" -ForegroundColor Yellow
|
||||||
|
Write-Host " - Gitea 服务不可达 (检查 https://code.letcareme.com)" -ForegroundColor Yellow
|
||||||
|
Write-Host " - 网络连接问题 (检查 DNS 和防火墙)" -ForegroundColor Yellow
|
||||||
|
Write-Host " - Git 凭证错误 (检查用户名密码)" -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " 离线模式: 如有本地 .claude 备份,运行:" -ForegroundColor Gray
|
||||||
|
Write-Host " Copy-Item .claude.bw-backup .claude -Recurse" -ForegroundColor Gray
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "`n[4/9] StartOnly 模式,跳过同步" -ForegroundColor Gray
|
||||||
|
# 静默检测远程更新
|
||||||
|
$configDir = Join-Path $env:USERPROFILE ".claude"
|
||||||
|
if (Test-Path (Join-Path $configDir ".git")) {
|
||||||
|
$prevEAP2 = $ErrorActionPreference
|
||||||
|
$ErrorActionPreference = "Continue"
|
||||||
|
git -C $configDir fetch --quiet 2>$null
|
||||||
|
$behind = git -C $configDir rev-list "HEAD..origin/main" --count 2>$null
|
||||||
|
$ErrorActionPreference = $prevEAP2
|
||||||
|
if ($behind -and [int]$behind -gt 0) {
|
||||||
|
Write-Host " [!] Bookworm 有 $behind 个新更新可用" -ForegroundColor Yellow
|
||||||
|
Write-Host " 双击 '更新并启动Bookworm.bat' 可同步最新版本" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 步骤 5: 完整性校验
|
||||||
|
$integrityFile = Join-Path $ClaudeTarget "integrity.sha256"
|
||||||
|
if (Test-Path $integrityFile) {
|
||||||
|
Write-Host "`n[5/9] 完整性校验..." -ForegroundColor White
|
||||||
|
$failures = @()
|
||||||
|
Get-Content $integrityFile | ForEach-Object {
|
||||||
|
if ($_ -match '^([a-f0-9]{64})\s+(.+)$') {
|
||||||
|
$expectedHash = $Matches[1]
|
||||||
|
$filePath = Join-Path $ClaudeTarget $Matches[2]
|
||||||
|
if (Test-Path $filePath) {
|
||||||
|
$actualHash = (Get-FileHash $filePath -Algorithm SHA256).Hash.ToLower()
|
||||||
|
if ($actualHash -ne $expectedHash) {
|
||||||
|
$failures += $Matches[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($failures.Count -gt 0) {
|
||||||
|
Write-Host " [WARN] 以下文件哈希不匹配:" -ForegroundColor Yellow
|
||||||
|
$failures | ForEach-Object { Write-Host " $_" -ForegroundColor Yellow }
|
||||||
|
Write-Host " 可能原因: 仓库内容被修改或本地有改动" -ForegroundColor Yellow
|
||||||
|
$continue = if ($AutoAccept) { 'y' } else { Read-Host " 继续? (y/n)" }
|
||||||
|
if ($continue -ne 'y') { exit 1 }
|
||||||
|
} else {
|
||||||
|
Write-Host " [OK] 所有文件完整性校验通过" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "`n[5/9] 跳过完整性校验 (无 integrity.sha256)" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
# 步骤 6: 渲染 settings.json
|
||||||
|
Write-Host "`n[6/9] 渲染配置模板..." -ForegroundColor White
|
||||||
|
Render-SettingsTemplate
|
||||||
|
|
||||||
|
# 步骤 7: 确保必要目录存在
|
||||||
|
Write-Host "`n[7/9] 初始化本地目录..." -ForegroundColor White
|
||||||
|
$localDirs = @("debug", "sessions", "cache", "backups", "telemetry", "shell-snapshots", "projects", "memory")
|
||||||
|
foreach ($d in $localDirs) {
|
||||||
|
$dirPath = Join-Path $ClaudeTarget $d
|
||||||
|
if (-not (Test-Path $dirPath)) {
|
||||||
|
New-Item -ItemType Directory -Path $dirPath -Force | Out-Null
|
||||||
|
Write-Host " 创建: $d/" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 设置环境变量 (进程级)
|
||||||
|
$env:CLAUDE_HOME = $ClaudeTarget
|
||||||
|
Write-Host " [OK] CLAUDE_HOME=$ClaudeTarget" -ForegroundColor Green
|
||||||
|
|
||||||
|
# 验证环境变量传递
|
||||||
|
$nodeCheck = & node -e "console.log(process.env.CLAUDE_HOME || 'NOT_SET')" 2>$null
|
||||||
|
if ($nodeCheck -eq $ClaudeTarget) {
|
||||||
|
Write-Host " [OK] Node.js 环境变量传递验证通过" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [WARN] Node.js 环境变量传递异常,hooks 可能无法正确解析路径" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
# 步骤 8: Bookworm 完整性验证 + MCP 检查
|
||||||
|
Write-Host "`n[8/9] Bookworm 系统验证..." -ForegroundColor White
|
||||||
|
|
||||||
|
# --- Bookworm vs 原生 Claude Code 检测 ---
|
||||||
|
$bwChecks = @()
|
||||||
|
$claudeMd = Join-Path $ClaudeTarget "CLAUDE.md"
|
||||||
|
$skillsDir = Join-Path $ClaudeTarget "skills"
|
||||||
|
$hooksDir = Join-Path $ClaudeTarget "hooks"
|
||||||
|
$settingsF = Join-Path $ClaudeTarget "settings.json"
|
||||||
|
|
||||||
|
# 检查 CLAUDE.md
|
||||||
|
if (Test-Path $claudeMd) {
|
||||||
|
$content = Get-Content $claudeMd -Raw -ErrorAction SilentlyContinue
|
||||||
|
if ($content -match "Bookworm") {
|
||||||
|
$bwChecks += @{ Name = "CLAUDE.md (Bookworm 指令)"; OK = $true }
|
||||||
|
} else {
|
||||||
|
$bwChecks += @{ Name = "CLAUDE.md (缺少 Bookworm 指令)"; OK = $false }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$bwChecks += @{ Name = "CLAUDE.md (文件不存在!)"; OK = $false }
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查 Skills
|
||||||
|
$skillCount = 0
|
||||||
|
if (Test-Path $skillsDir) {
|
||||||
|
$skillCount = (Get-ChildItem $skillsDir -Directory -ErrorAction SilentlyContinue).Count
|
||||||
|
}
|
||||||
|
$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 -gt 10) }
|
||||||
|
|
||||||
|
# 检查 settings.json hooks 配置
|
||||||
|
$hasHooks = $false
|
||||||
|
if (Test-Path $settingsF) {
|
||||||
|
$sContent = Get-Content $settingsF -Raw -ErrorAction SilentlyContinue
|
||||||
|
if ($sContent -match '"hooks"') { $hasHooks = $true }
|
||||||
|
}
|
||||||
|
$bwChecks += @{ Name = "Settings hooks 配置"; OK = $hasHooks }
|
||||||
|
|
||||||
|
# 输出验证结果
|
||||||
|
$allOK = $true
|
||||||
|
foreach ($c in $bwChecks) {
|
||||||
|
$icon = if ($c.OK) { "[OK]" } else { "[!!]" }
|
||||||
|
$color = if ($c.OK) { "Green" } else { "Red" }
|
||||||
|
Write-Host " $icon $($c.Name)" -ForegroundColor $color
|
||||||
|
if (-not $c.OK) { $allOK = $false }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $allOK) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " ╔══════════════════════════════════════════════════════╗" -ForegroundColor Yellow
|
||||||
|
Write-Host " ║ [!] 警告: Bookworm 系统不完整 ║" -ForegroundColor Yellow
|
||||||
|
Write-Host " ║ Claude Code 将以原生模式启动 (无 Skills/Hooks) ║" -ForegroundColor Yellow
|
||||||
|
Write-Host " ║ 建议: 不加 -StartOnly 重新运行 install.ps1 同步 ║" -ForegroundColor Yellow
|
||||||
|
Write-Host " ╚══════════════════════════════════════════════════════╝" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " [OK] Bookworm 系统完整 ($skillCount Skills / $hookCount Hooks)" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- MCP 依赖检查 (中文提醒) ---
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " MCP 服务检查:" -ForegroundColor Gray
|
||||||
|
$mcpWarnings = @()
|
||||||
|
|
||||||
|
# Python (askui/pywinauto/com-server 需要)
|
||||||
|
$hasPython = [bool](Get-Command python -ErrorAction SilentlyContinue)
|
||||||
|
if (-not $hasPython) {
|
||||||
|
$mcpWarnings += " [!] Python 未安装 - askui/pywinauto/com-server MCP 不可用"
|
||||||
|
$mcpWarnings += " 安装: https://www.python.org/downloads/ 或 winget install Python.Python.3.12"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Playwright (浏览器自动化 MCP) — 轻量检测,避免 npx 触发安装
|
||||||
|
$hasPlaywright = $false
|
||||||
|
try {
|
||||||
|
$pwPath = & npm list -g @playwright/mcp 2>$null
|
||||||
|
if ($pwPath -and $pwPath -notmatch 'empty') { $hasPlaywright = $true }
|
||||||
|
} catch {}
|
||||||
|
if (-not $hasPlaywright) {
|
||||||
|
$mcpWarnings += " [!] Playwright MCP 未安装 - 浏览器自动化功能不可用"
|
||||||
|
$mcpWarnings += " 安装: npm i -g @playwright/mcp && npx playwright install"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查关键 API Key 环境变量
|
||||||
|
$apiChecks = @(
|
||||||
|
@{ Key = "ANTHROPIC_API_KEY"; Name = "Claude API (中转站)" }
|
||||||
|
@{ Key = "ANTHROPIC_BASE_URL"; Name = "API 中转站地址" }
|
||||||
|
)
|
||||||
|
foreach ($ak in $apiChecks) {
|
||||||
|
$val = [System.Environment]::GetEnvironmentVariable($ak.Key, "Process")
|
||||||
|
if (-not $val) {
|
||||||
|
$mcpWarnings += " [!] $($ak.Key) 未设置 - $($ak.Name)不可用"
|
||||||
|
$mcpWarnings += " 需要管理员重新加密凭证并更新 secrets.enc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 可选 MCP API Key 检查
|
||||||
|
$optionalApis = @(
|
||||||
|
@{ Key = "GITHUB_PERSONAL_ACCESS_TOKEN"; Name = "GitHub MCP"; Cmd = "仓库管理/代码搜索" }
|
||||||
|
@{ Key = "SLACK_BOT_TOKEN"; Name = "Slack MCP"; Cmd = "消息发送/频道管理" }
|
||||||
|
@{ Key = "BROWSERBASE_API_KEY"; Name = "Browserbase MCP"; Cmd = "云端浏览器" }
|
||||||
|
@{ Key = "FIRECRAWL_API_KEY"; Name = "Firecrawl MCP"; Cmd = "网页抓取/搜索" }
|
||||||
|
)
|
||||||
|
$missingOptional = @()
|
||||||
|
foreach ($api in $optionalApis) {
|
||||||
|
$val = [System.Environment]::GetEnvironmentVariable($api.Key, "Process")
|
||||||
|
if (-not $val) {
|
||||||
|
$missingOptional += $api
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($mcpWarnings.Count -gt 0) {
|
||||||
|
foreach ($w in $mcpWarnings) {
|
||||||
|
Write-Host $w -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($missingOptional.Count -gt 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " 可选 MCP 服务 (未配置, 不影响核心功能):" -ForegroundColor Gray
|
||||||
|
foreach ($m in $missingOptional) {
|
||||||
|
Write-Host " [-] $($m.Name) ($($m.Cmd))" -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
Write-Host " 如需使用, 请联系管理员将 API Key 加入 secrets.enc" -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($mcpWarnings.Count -eq 0) {
|
||||||
|
Write-Host " [OK] 核心 API 已配置" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# 步骤 9: 启动 Claude Code
|
||||||
|
Write-Host "`n[9/9] 启动 Claude Code..." -ForegroundColor White
|
||||||
|
Write-Host ""
|
||||||
|
if ($allOK) {
|
||||||
|
Write-Host " ╔══════════════════════════════════════════╗" -ForegroundColor Green
|
||||||
|
Write-Host " ║ Bookworm 就绪! 正在启动 Claude Code... ║" -ForegroundColor Green
|
||||||
|
Write-Host " ╚══════════════════════════════════════════╝" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " ╔══════════════════════════════════════════╗" -ForegroundColor Yellow
|
||||||
|
Write-Host " ║ 原生模式启动 (Bookworm 不完整) ║" -ForegroundColor Yellow
|
||||||
|
Write-Host " ╚══════════════════════════════════════════╝" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# 首次安装: 创建桌面快捷方式 + 打开使用教程
|
||||||
|
if (-not $StartOnly) {
|
||||||
|
New-DesktopShortcuts
|
||||||
|
$guidePath = Join-Path $ScriptDir "guide.html"
|
||||||
|
if (Test-Path $guidePath) {
|
||||||
|
Start-Process $guidePath
|
||||||
|
Write-Host " [OK] 使用教程已在浏览器打开" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& claude
|
||||||
206
lessons-learned.md
Normal file
206
lessons-learned.md
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
# Bookworm Portable 项目经验与避坑指南
|
||||||
|
|
||||||
|
> 2026-04-01 | 从可行性评估到 E2E 验证的完整实战记录
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、项目概要
|
||||||
|
|
||||||
|
**目标**: 让任意 Windows 电脑通过一行命令激活 Bookworm (97 Skills + 18 Agents + 28 Hooks) + 中转站 API
|
||||||
|
|
||||||
|
**最终架构**: Gitea 私有仓 (code.letcareme.com) + AES-256 加密凭证 + 自动代理检测 + 一键安装脚本
|
||||||
|
|
||||||
|
**耗时**: 1 天 (评估 → 设计 → 三路专家审查 → P0/P1 修复 → 部署 → E2E 验证 → 踩坑修复)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、关键决策与转折点
|
||||||
|
|
||||||
|
### 决策 1: USB → Git 方案切换
|
||||||
|
- **初始方案**: U盘存储全部系统文件 + 加密凭证
|
||||||
|
- **问题**: Windows NTFS Junction 跨驱动器失败 (实测 Accessible: False)、USB IOPS 瓶颈 (hook 延迟 200-600ms)、体积 2.1GB
|
||||||
|
- **最终方案**: Git + 模板渲染,USB 变为可选 (纯云端即可)
|
||||||
|
- **教训**: 方案评估阶段多投入时间做实测,不要假设跨驱动器操作能工作
|
||||||
|
|
||||||
|
### 决策 2: GitHub → 国内 Gitea
|
||||||
|
- **问题**: 国内无 VPN 无法访问 GitHub
|
||||||
|
- **方案**: 阿里云 ECS 自建 Gitea,国内直连
|
||||||
|
- **教训**: 面向国内用户的工具链必须全程可达,不能依赖海外服务
|
||||||
|
|
||||||
|
### 决策 3: bookworm.letcareme.com → code.letcareme.com
|
||||||
|
- **问题**: bookworm.letcareme.com 已被 Bookworm Web 项目 Nginx 配置占用
|
||||||
|
- **解决**: 换用 code.letcareme.com,申请新证书
|
||||||
|
- **教训**: 部署前检查域名占用,`grep -r "域名" /etc/nginx/` 先扫一遍
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、必踩的坑 (按严重度排序)
|
||||||
|
|
||||||
|
### 坑 1: Claude Code 国内启动检查 (CRITICAL)
|
||||||
|
```
|
||||||
|
现象: "Failed to connect to api.anthropic.com: ERR_BAD_REQUEST"
|
||||||
|
原因: Claude Code 启动时硬编码检查 api.anthropic.com,与 ANTHROPIC_BASE_URL 无关
|
||||||
|
影响: 国内无代理 = Claude Code 完全无法启动
|
||||||
|
解决: 必须有代理/VPN,且必须设 HTTPS_PROXY 环境变量 (Node.js 不读系统代理)
|
||||||
|
耗时: 2 小时排查
|
||||||
|
```
|
||||||
|
|
||||||
|
### 坑 2: Node.js 不读 Windows 系统代理 (CRITICAL)
|
||||||
|
```
|
||||||
|
现象: PowerShell (Invoke-WebRequest) 能通,Claude Code 不通
|
||||||
|
原因: Node.js 只读 HTTPS_PROXY/HTTP_PROXY 环境变量,不读 Windows IE/系统代理设置
|
||||||
|
解决: 用 [System.Net.WebRequest]::DefaultWebProxy.GetProxy() 发现实际代理端口
|
||||||
|
然后 $env:HTTPS_PROXY = "http://127.0.0.1:端口"
|
||||||
|
教训: 系统代理 ≠ 进程代理,Node.js 应用必须显式设环境变量
|
||||||
|
```
|
||||||
|
|
||||||
|
### 坑 3: settings.json 中 ${VAR} 不展开 (CRITICAL)
|
||||||
|
```
|
||||||
|
现象: "API Error: Invalid URL"
|
||||||
|
原因: settings.template.json 中 "ANTHROPIC_BASE_URL": "${ANTHROPIC_BASE_URL}"
|
||||||
|
Claude Code 把 ${ANTHROPIC_BASE_URL} 当字面字符串,不展开环境变量
|
||||||
|
实际发送的 URL 是 "${ANTHROPIC_BASE_URL}/v1/messages"
|
||||||
|
解决: 从 settings.json env 段删除 ANTHROPIC_API_KEY 和 ANTHROPIC_BASE_URL
|
||||||
|
让 install.ps1 注入的进程环境变量直接生效
|
||||||
|
教训: Claude Code settings.json 的 ${} 语法行为未文档化,不要假设它能展开
|
||||||
|
```
|
||||||
|
|
||||||
|
### 坑 4: PowerShell 5.1 的 ?. 语法 (HIGH)
|
||||||
|
```
|
||||||
|
现象: "表达式或语句中包含意外的标记 '?.Source'"
|
||||||
|
原因: ?. (null-conditional operator) 是 PS 7+ 专属语法,PS 5.1 不支持
|
||||||
|
解决: 改为 if ($x) { $x.Property } else { $null }
|
||||||
|
教训: 目标机可能只有 PS 5.1 (Windows 自带),所有脚本必须 PS 5.1 兼容
|
||||||
|
```
|
||||||
|
|
||||||
|
### 坑 5: PowerShell 5.1 的 git stderr 问题 (HIGH)
|
||||||
|
```
|
||||||
|
现象: git clone 实际成功但脚本报 [ERROR] 克隆失败
|
||||||
|
原因: git 把进度信息写到 stderr,PS 5.1 的 $ErrorActionPreference="Stop"
|
||||||
|
把 stderr 输出当成终止性错误抛出异常
|
||||||
|
解决: git 命令前临时设 $ErrorActionPreference = "Continue"
|
||||||
|
用 $LASTEXITCODE 判断实际成功/失败
|
||||||
|
教训: PS 5.1 + 外部命令 + Stop 模式 = 必炸,所有外部命令都要处理
|
||||||
|
```
|
||||||
|
|
||||||
|
### 坑 6: UTF-8 BOM 问题 (HIGH)
|
||||||
|
```
|
||||||
|
现象: "<# : 无法将 '<#' 项识别为 cmdlet" 或中文乱码
|
||||||
|
原因: Write 工具生成的文件无 BOM,PS 5.1 按 ANSI 解析 → 中文乱码
|
||||||
|
修复时 BOM 加了两次 → 双 BOM 导致 <# 注释块被当成代码
|
||||||
|
解决: 统一用 bash 添加一次 BOM: printf '\xEF\xBB\xBF' > tmp && cat file >> tmp
|
||||||
|
每次编辑后检查: head -c 3 file | xxd -p 应为 efbbbf
|
||||||
|
教训: 面向 PS 5.1 的 .ps1 文件必须有且仅有一个 UTF-8 BOM
|
||||||
|
```
|
||||||
|
|
||||||
|
### 坑 7: openssl 路径不在默认位置 (MEDIUM)
|
||||||
|
```
|
||||||
|
现象: "[ERROR] openssl 未找到"
|
||||||
|
原因: Git for Windows 安装在 D:\Git 而非默认 C:\Program Files\Git
|
||||||
|
解决: 搜索多个路径: C:\Program Files\Git, D:\Git, D:\Git\mingw64\bin
|
||||||
|
教训: 不要硬编码第三方软件路径,提供多路径搜索
|
||||||
|
```
|
||||||
|
|
||||||
|
### 坑 8: Gitea 端口被占用 (MEDIUM)
|
||||||
|
```
|
||||||
|
现象: 部署脚本设 3000 端口但已被 my-oa-frontend 容器占用
|
||||||
|
解决: 改为 3300 端口
|
||||||
|
教训: 部署前 netstat -tlnp | grep 端口 先检查
|
||||||
|
```
|
||||||
|
|
||||||
|
### 坑 9: Nginx 反代不传 Authorization 头 (MEDIUM)
|
||||||
|
```
|
||||||
|
现象: git push 通过 Nginx 反代时 404
|
||||||
|
原因: Nginx 默认传 Authorization 但旧的 bookworm-web.conf 先匹配了域名
|
||||||
|
解决: 换域名 + 添加 proxy_set_header Authorization $http_authorization
|
||||||
|
教训: 多个 Nginx 配置文件可能竞争同一个 server_name
|
||||||
|
```
|
||||||
|
|
||||||
|
### 坑 10: git clone 私有仓需要凭证 (LOW)
|
||||||
|
```
|
||||||
|
现象: install.ps1 中 git clone 静默失败 (无密码提示)
|
||||||
|
解决: 先手动 git clone 触发凭证输入 + git config credential.helper store
|
||||||
|
教训: 自动化脚本中的 git 操作需要预配置凭证
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、安全经验
|
||||||
|
|
||||||
|
### 三路专家审查的价值
|
||||||
|
- **红队**: 发现 14 个攻击向量,其中 INSTALL_LOCK=false 成功率 95%
|
||||||
|
- **代码审查**: 发现 5 个 Blocker + 13 个 Warning
|
||||||
|
- **架构审查**: 发现仓库体积失控 (预估 50MB 实际 500MB+)
|
||||||
|
- **交叉验证**: 三路独立发现的 openssl 命令行密码泄露问题,置信度极高
|
||||||
|
- **教训**: 多专家并行审查在 P0 修复前执行,投入产出比极高
|
||||||
|
|
||||||
|
### 安全加固清单 (实际落地的)
|
||||||
|
- [x] Gitea INSTALL_LOCK=true + 注册关闭 + CLI 创建管理员
|
||||||
|
- [x] Gitea 二进制 SHA256 校验
|
||||||
|
- [x] HTTPS (Let's Encrypt + Nginx 反代 + HSTS)
|
||||||
|
- [x] fail2ban (5次/小时 → 封24h)
|
||||||
|
- [x] Gitea 仅 127.0.0.1 监听
|
||||||
|
- [x] openssl 密码通过 stdin 管道 (不暴露进程列表)
|
||||||
|
- [x] PBKDF2 600,000 迭代 (OWASP 2023)
|
||||||
|
- [x] 凭证仅进程级环境变量 (不写磁盘)
|
||||||
|
- [x] .gitignore 排除凭证/大文件/敏感数据
|
||||||
|
- [x] git add 前人工确认 + 大文件检测
|
||||||
|
- [x] stop.ps1 清理 Git Credential Manager 缓存
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、PowerShell 跨版本兼容速查表
|
||||||
|
|
||||||
|
| PS 7+ 写法 | PS 5.1 兼容写法 |
|
||||||
|
|---|---|
|
||||||
|
| `$obj?.Property` | `if ($obj) { $obj.Property } else { $null }` |
|
||||||
|
| `$cmd?.Source` | `$cmd = Get-Command x -EA 0; if ($cmd) { $cmd.Source }` |
|
||||||
|
| `??` 空合并 | `if ($x) { $x } else { $default }` |
|
||||||
|
| UTF-8 无 BOM | 必须有 BOM: `\xEF\xBB\xBF` |
|
||||||
|
| `$ErrorActionPreference="Stop"` + git | 临时改 `Continue`,用 `$LASTEXITCODE` 判断 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、代理/VPN 检测方法速查
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 最可靠: .NET DefaultWebProxy (覆盖所有系统代理)
|
||||||
|
[System.Net.WebRequest]::DefaultWebProxy.GetProxy("https://api.anthropic.com")
|
||||||
|
|
||||||
|
# IE 注册表
|
||||||
|
Get-ItemProperty "HKCU:\...\Internet Settings" | Select ProxyServer, ProxyEnable
|
||||||
|
|
||||||
|
# 端口扫描 (Clash 7890, V2Ray 10808, 快柠檬 10792)
|
||||||
|
Test-NetConnection 127.0.0.1 -Port 7890
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、项目最终产出
|
||||||
|
|
||||||
|
```
|
||||||
|
13 个文件 (bookworm-portable/)
|
||||||
|
2 个 Gitea 仓库 (bookworm-config 514文件/14MB, bookworm-boot 6文件)
|
||||||
|
3 个 ECS 部署脚本 (Gitea + HTTPS + 防火墙)
|
||||||
|
7 项 P0 安全修复
|
||||||
|
4 项 P1 安全加固
|
||||||
|
1 个保姆式 HTML 教程
|
||||||
|
1 个快速参考 TXT 手册
|
||||||
|
1 份经验避坑指南 (本文档)
|
||||||
|
|
||||||
|
E2E 验证: 远程 Windows Server → 从零安装 → Claude Code 启动成功
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、如果重新来过,我会...
|
||||||
|
|
||||||
|
1. **一开始就做 PS 5.1 兼容测试** — 不假设目标机有 PS 7
|
||||||
|
2. **一开始就加代理检测** — 国内 Claude Code 的核心门槛
|
||||||
|
3. **不在 settings.json env 段放 ${} 变量** — 行为未文档化
|
||||||
|
4. **先检查域名和端口占用** — 再写部署脚本
|
||||||
|
5. **三路专家审查提前到设计阶段** — 而非代码写完后再审
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Bookworm Portable v1.2 | code.letcareme.com | 2026-04-01*
|
||||||
54
patches/admin-cb-routes.js
Normal file
54
patches/admin-cb-routes.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
'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' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
228
patches/circuit-breaker-v2.js
Normal file
228
patches/circuit-breaker-v2.js
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
'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,
|
||||||
|
};
|
||||||
141
patches/deploy-patches.sh
Normal file
141
patches/deploy-patches.sh
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
#!/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 "========================================="
|
||||||
611
patches/llm-router-v2.js
Normal file
611
patches/llm-router-v2.js
Normal file
@ -0,0 +1,611 @@
|
|||||||
|
'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,
|
||||||
|
};
|
||||||
349
patches/proxy-v2.js
Normal file
349
patches/proxy-v2.js
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
'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-sonnet-4-5-20250514',
|
||||||
|
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 };
|
||||||
217
patches/update-pages.py
Normal file
217
patches/update-pages.py
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
#!/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'">⬇ 下载一键安装器</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'">⬇ 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'">⬇ 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'">⬇ macOS 一键安装</a>
|
||||||
|
</div>
|
||||||
|
<p style="text-align:center;color:var(--text-dim);font-size:0.8rem;margin-top:0.5rem">全新电脑? 双击即装 — 自动安装 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) → 双击运行 → 输入密码 → 完成</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> → 双击运行 → 输入密码 → 完成</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">⚡</span>
|
||||||
|
<div>
|
||||||
|
<strong>全新电脑? 从这里开始!</strong><br>
|
||||||
|
OneClick 安装器会自动完成所有步骤:安装 Node.js、Git、Claude 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">💡</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">➔</span>
|
||||||
|
<div class="flow-node">双击 <strong>Bookworm</strong></div>
|
||||||
|
<span class="flow-arrow">➔</span>
|
||||||
|
<div class="flow-node">等横幅出现</div>
|
||||||
|
<span class="flow-arrow">➔</span>
|
||||||
|
<div class="flow-node active">开始工作</div>
|
||||||
|
</div>'''
|
||||||
|
|
||||||
|
new_flow = '''<div class="flow">
|
||||||
|
<div class="flow-node">开代理</div>
|
||||||
|
<span class="flow-arrow">➔</span>
|
||||||
|
<div class="flow-node">双击 <strong>Bookworm</strong></div>
|
||||||
|
<span class="flow-arrow">➔</span>
|
||||||
|
<div class="flow-node">等横幅出现</div>
|
||||||
|
<span class="flow-arrow">➔</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] 所有页面更新完成')
|
||||||
206
prepare-repo.ps1
Normal file
206
prepare-repo.ps1
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
<#
|
||||||
|
.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
|
||||||
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$templateSrc = Join-Path $ScriptDir "settings.template.json"
|
||||||
|
$templateDst = Join-Path $ClaudeDir "settings.template.json"
|
||||||
|
|
||||||
|
if (Test-Path $templateSrc) {
|
||||||
|
Copy-Item $templateSrc $templateDst -Force
|
||||||
|
Write-Host "[1/6] settings.template.json 已放入 .claude/" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "[1/6] [WARN] settings.template.json 不在脚本目录,请手动创建" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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, settings.template.json
|
||||||
|
|
||||||
|
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
|
||||||
300
quick-reference.txt
Normal file
300
quick-reference.txt
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
|
||||||
|
================================================================================
|
||||||
|
Bookworm Portable v1.4 — 快速参考手册
|
||||||
|
所有命令可直接复制粘贴执行
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
Gitea 地址: https://code.letcareme.com
|
||||||
|
管理员账号: [由管理员提供]
|
||||||
|
管理员密码: [由管理员提供]
|
||||||
|
中转站地址: [由管理员提供]
|
||||||
|
ECS 服务器: [由管理员提供]
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
一、首次安装 — 最简方式 (推荐)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
前置要求:
|
||||||
|
[必须] Node.js 18+ https://nodejs.org 下载 LTS
|
||||||
|
[必须] Git https://git-scm.com 下载安装
|
||||||
|
[必须] 代理/VPN Clash / V2Ray / 快柠檬 / 任意翻墙工具
|
||||||
|
(Claude Code 启动时检查 api.anthropic.com, 国内必须)
|
||||||
|
|
||||||
|
方法 A: 一键安装器 (最简单)
|
||||||
|
─────────────────────────────
|
||||||
|
1. 获取 Bookworm-Setup.bat (管理员发给你, 或从 Bookworm Web 下载页获取)
|
||||||
|
2. 双击运行
|
||||||
|
3. 按提示输入 Gitea 密码 + 主密码
|
||||||
|
4. 完成! 桌面自动出现 Bookworm 快捷方式
|
||||||
|
|
||||||
|
方法 B: 手动安装
|
||||||
|
─────────────────────────────
|
||||||
|
# 1. 确保代理/VPN 已启动
|
||||||
|
|
||||||
|
# 2. 安装 Claude Code (如未装)
|
||||||
|
npm i -g @anthropic-ai/claude-code
|
||||||
|
|
||||||
|
# 3. 克隆引导仓库
|
||||||
|
git clone https://code.letcareme.com/bookworm/bookworm-boot.git
|
||||||
|
|
||||||
|
# 4. 进入目录
|
||||||
|
cd bookworm-boot
|
||||||
|
|
||||||
|
# 5. 运行安装 (PowerShell 7 推荐, 5.1 也可)
|
||||||
|
pwsh -ExecutionPolicy Bypass -File install.ps1
|
||||||
|
|
||||||
|
# 脚本自动执行:
|
||||||
|
# [1/6] 前置检查 (缺依赖自动提示 winget 安装)
|
||||||
|
# [2/6] 代理自动检测 (无需手动找端口)
|
||||||
|
# [3/6] 解密凭证 (输入主密码, 最多3次重试, 可选本日免密)
|
||||||
|
# [4/6] 同步配置 (git clone 92 Skills / 18 Agents / 29 Hooks)
|
||||||
|
# [5/6] 渲染模板 + 初始化 + Bookworm 完整性验证 + MCP 检查
|
||||||
|
# [6/6] 启动 Claude Code
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
二、日常使用
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
方法 A: 双击 .bat (推荐)
|
||||||
|
─────────────────────────────
|
||||||
|
bookworm-boot 文件夹 (或桌面快捷方式):
|
||||||
|
启动Bookworm.bat 每天日常使用 (快速启动, 不更新)
|
||||||
|
更新并启动Bookworm.bat 管理员通知有更新时使用
|
||||||
|
|
||||||
|
启动时显示 "有 N 个新更新可用" → 双击 "更新并启动" 同步
|
||||||
|
|
||||||
|
方法 B: 命令行
|
||||||
|
─────────────────────────────
|
||||||
|
# 快速启动 (~10秒, 跳过 git clone)
|
||||||
|
cd bookworm-boot
|
||||||
|
pwsh -ExecutionPolicy Bypass -File install.ps1 -StartOnly
|
||||||
|
|
||||||
|
# 同步更新后启动
|
||||||
|
cd bookworm-boot
|
||||||
|
pwsh -ExecutionPolicy Bypass -File install.ps1
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
三、密码说明
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
本系统有两个密码, 不要搞混:
|
||||||
|
|
||||||
|
Gitea 密码 克隆仓库时输入, 用于下载文件
|
||||||
|
主密码 解密 API 凭证时输入, 用于启动 Claude Code
|
||||||
|
|
||||||
|
密码输错: 最多 3 次重试, 3 次失败自动退出
|
||||||
|
本日免密: 首次解密后脚本询问 "今日内免密启动? (y/n)"
|
||||||
|
选 y 后当天再次启动无需输密码, 次日自动过期
|
||||||
|
忘记密码: 联系管理员重新生成 secrets.enc
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
四、使用完毕清理
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
方法 A: 双击 卸载Bookworm.bat (最简单, 一键完整卸载)
|
||||||
|
|
||||||
|
方法 B: 命令行
|
||||||
|
─────────────────────────────
|
||||||
|
# 基础清理 (清除环境变量, 保留配置供下次使用)
|
||||||
|
pwsh -ExecutionPolicy Bypass -File stop.ps1
|
||||||
|
|
||||||
|
# 完整恢复 (删除 Bookworm, 恢复电脑原始 .claude 目录)
|
||||||
|
pwsh -ExecutionPolicy Bypass -File stop.ps1 -Restore
|
||||||
|
|
||||||
|
# 深度清理 (恢复 + 清除 PS 历史 + 清除 Git/凭证缓存)
|
||||||
|
pwsh -ExecutionPolicy Bypass -File stop.ps1 -Restore -Deep
|
||||||
|
|
||||||
|
[!] 在他人电脑/公用电脑上务必执行深度清理或双击 卸载Bookworm.bat
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
五、代理/VPN 相关
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
install.ps1 自动检测代理, 支持:
|
||||||
|
- 系统代理 (Windows 设置里的代理)
|
||||||
|
- Clash for Windows / Clash Verge
|
||||||
|
- V2Ray / V2RayN
|
||||||
|
- 快柠檬 / 任意 VPN
|
||||||
|
- 其他设置了系统代理的工具
|
||||||
|
|
||||||
|
如果自动检测失败, 手动指定:
|
||||||
|
$env:HTTPS_PROXY = "http://127.0.0.1:端口号"
|
||||||
|
$env:HTTP_PROXY = "http://127.0.0.1:端口号"
|
||||||
|
pwsh -ExecutionPolicy Bypass -File install.ps1
|
||||||
|
|
||||||
|
查找代理端口:
|
||||||
|
[System.Net.WebRequest]::DefaultWebProxy.GetProxy("https://api.anthropic.com")
|
||||||
|
# 输出中的 Port 就是代理端口
|
||||||
|
|
||||||
|
常见代理端口:
|
||||||
|
Clash: 7890
|
||||||
|
V2RayN: 10808 / 10809
|
||||||
|
快柠檬: 10792 (通过上述命令查询)
|
||||||
|
SSR: 1080
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
六、管理员操作
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
---- 6.1 推送 Bookworm 配置到 Gitea ----
|
||||||
|
|
||||||
|
cd C:\Users\leesu\.claude
|
||||||
|
git add -A
|
||||||
|
git commit -m "update bookworm config"
|
||||||
|
git push https://bookworm:[密码]@code.letcareme.com/bookworm/bookworm-config.git main
|
||||||
|
|
||||||
|
---- 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" .
|
||||||
|
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
|
||||||
|
# 加密完成后同步到 boot 仓库 (参照 6.2)
|
||||||
|
|
||||||
|
---- 6.4 解密验证 ----
|
||||||
|
|
||||||
|
pwsh -ExecutionPolicy Bypass -File C:\Users\leesu\Documents\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/
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
七、SSH 服务器管理
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
# 登录 ECS
|
||||||
|
ssh root@8.138.11.105
|
||||||
|
|
||||||
|
# Gitea 状态/日志/重启
|
||||||
|
ssh root@8.138.11.105 "systemctl status gitea"
|
||||||
|
ssh root@8.138.11.105 "journalctl -u gitea -n 50"
|
||||||
|
ssh root@8.138.11.105 "systemctl restart gitea"
|
||||||
|
|
||||||
|
# Nginx 状态
|
||||||
|
ssh root@8.138.11.105 "nginx -t && systemctl status nginx"
|
||||||
|
|
||||||
|
# fail2ban 封禁/解封
|
||||||
|
ssh root@8.138.11.105 "fail2ban-client status gitea"
|
||||||
|
ssh root@8.138.11.105 "fail2ban-client set gitea unbanip 1.2.3.4"
|
||||||
|
|
||||||
|
# 用户管理
|
||||||
|
ssh root@8.138.11.105 "sudo -u git gitea admin user list --config /var/lib/gitea/custom/conf/app.ini"
|
||||||
|
ssh root@8.138.11.105 "sudo -u git gitea admin user change-password --username bookworm --password NEW_PASSWORD --config /var/lib/gitea/custom/conf/app.ini"
|
||||||
|
|
||||||
|
# 磁盘/备份
|
||||||
|
ssh root@8.138.11.105 "du -sh /var/lib/gitea /home/git/gitea-repositories"
|
||||||
|
ssh root@8.138.11.105 "sudo -u git gitea dump -c /var/lib/gitea/custom/conf/app.ini --tempdir /tmp"
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
八、故障排查
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
问题: "Unable to connect to Anthropic services" / "ECONNREFUSED"
|
||||||
|
原因: 代理/VPN 未启动, 或 Node.js 未读到代理设置
|
||||||
|
解决: 1. 确认代理已启动
|
||||||
|
2. install.ps1 自动检测, 失败则手动:
|
||||||
|
$env:HTTPS_PROXY = "http://127.0.0.1:端口"
|
||||||
|
3. 查端口: [System.Net.WebRequest]::DefaultWebProxy.GetProxy(...)
|
||||||
|
|
||||||
|
问题: "API Error: Invalid URL"
|
||||||
|
原因: ANTHROPIC_BASE_URL 格式不对
|
||||||
|
解决: 确保 secrets.enc 中值为 https://bww.letcareme.com/v1 (末尾含 /v1)
|
||||||
|
不要在 settings.json env 段放 ANTHROPIC_BASE_URL
|
||||||
|
|
||||||
|
问题: "openssl 未找到"
|
||||||
|
解决: 确认 Git for Windows 已安装 (脚本搜索 C:\, D:\ 下的 Git 目录)
|
||||||
|
|
||||||
|
问题: "执行策略" 错误
|
||||||
|
解决: pwsh -ExecutionPolicy Bypass -File xxx.ps1
|
||||||
|
或: Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
|
||||||
|
|
||||||
|
问题: "?.Source" 语法错误
|
||||||
|
解决: 用 pwsh (PS 7) 或更新到最新 install.ps1 (已兼容 PS 5.1)
|
||||||
|
|
||||||
|
问题: 中文乱码 / "<#" 错误
|
||||||
|
解决: 用 pwsh (PS 7) 或 git pull 获取最新文件 (含 UTF-8 BOM)
|
||||||
|
|
||||||
|
问题: 克隆失败 / 认证失败
|
||||||
|
解决: 1. git config --global credential.helper store (install.ps1 已自动执行)
|
||||||
|
2. 手动测试: git clone https://code.letcareme.com/bookworm/bookworm-config.git test
|
||||||
|
3. 浏览器打开 https://code.letcareme.com 确认可达
|
||||||
|
|
||||||
|
问题: 解密失败 / 密码错误
|
||||||
|
解决: 区分大小写, 至少 12 位, 最多 3 次重试
|
||||||
|
忘记密码需管理员重新 encrypt-secrets.ps1
|
||||||
|
|
||||||
|
问题: Claude Code 启动无 Bookworm 横幅 / 显示 "原生模式"
|
||||||
|
解决: 不加 -StartOnly 重新运行 install.ps1 同步最新配置
|
||||||
|
|
||||||
|
问题: node -v / npm -v 无法识别
|
||||||
|
解决: 安装 Node.js 后必须重开 PowerShell
|
||||||
|
或: $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
||||||
|
|
||||||
|
问题: npm 安装太慢
|
||||||
|
解决: npm config set registry https://registry.npmmirror.com
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
九、安全规格
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
凭证加密: AES-256-CBC + PBKDF2 (600,000 迭代, OWASP 2023)
|
||||||
|
传输加密: HTTPS TLS 1.2+ (Let's Encrypt 证书, 自动续期)
|
||||||
|
凭证存储: 仅进程级环境变量, 不写磁盘, 不写注册表
|
||||||
|
免密缓存: Windows Credential Manager + DPAPI, 当日23:59过期
|
||||||
|
密码传递: openssl stdin 管道, 不暴露进程列表
|
||||||
|
登录保护: fail2ban (5 次失败/小时 -> 封禁 24 小时)
|
||||||
|
二进制校验: Gitea 下载 SHA-256 完整性校验
|
||||||
|
Gitea 绑定: 127.0.0.1:3300 仅本地, 通过 Nginx 443 反代
|
||||||
|
HSTS: Strict-Transport-Security max-age=31536000
|
||||||
|
注册控制: DISABLE_REGISTRATION=true, INSTALL_LOCK=true
|
||||||
|
代理检测: 3阶梯自动检测 (.NET DefaultWebProxy → 注册表 → 端口扫描)
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
十、文件清单
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
bookworm-portable\ (本机管理员工具包)
|
||||||
|
├── Bookworm-Setup.bat 一键安装器 (发给用户的唯一文件)
|
||||||
|
├── 启动Bookworm.bat 日常双击启动
|
||||||
|
├── 更新并启动Bookworm.bat 同步后启动
|
||||||
|
├── 卸载Bookworm.bat 一键完整卸载
|
||||||
|
├── install.ps1 核心安装逻辑 (含代理检测/依赖安装/验证)
|
||||||
|
├── stop.ps1 清理/恢复逻辑
|
||||||
|
├── encrypt-secrets.ps1 凭证加密/解密
|
||||||
|
├── generate-integrity.ps1 完整性哈希生成
|
||||||
|
├── prepare-repo.ps1 仓库初始化+推送
|
||||||
|
├── 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 本文档
|
||||||
|
├── download.html 独立下载页
|
||||||
|
├── download-panel.html Bookworm Web 嵌入面板
|
||||||
|
├── lessons-learned.md 踩坑经验 (10 项)
|
||||||
|
└── README.txt 简要说明
|
||||||
|
|
||||||
|
Gitea 仓库:
|
||||||
|
├── bookworm-config Skills/Agents/Hooks/Scripts (514 文件, 14MB)
|
||||||
|
└── bookworm-boot 安装器+脚本+凭证+教程
|
||||||
|
|
||||||
|
Bookworm Web 引流入口:
|
||||||
|
└── /app → "下载" Tab 内嵌下载页 + Bookworm-Setup.bat 直链
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
Bookworm Portable v1.4 | 2026-04-02 | code.letcareme.com
|
||||||
|
================================================================================
|
||||||
233
quick-start.html
Normal file
233
quick-start.html
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
<!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>
|
||||||
|
@page { size: A4; margin: 12mm; }
|
||||||
|
@media print {
|
||||||
|
body { background: #fff !important; color: #111 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||||
|
.no-print { display: none !important; }
|
||||||
|
}
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
background: #fff; color: #1a1a2e;
|
||||||
|
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||||
|
width: 210mm; min-height: 297mm; margin: 0 auto;
|
||||||
|
padding: 10mm 14mm; font-size: 10pt; line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.header { display: flex; justify-content: space-between; align-items: center; border-bottom: 2.5px solid #2563eb; padding-bottom: 8px; margin-bottom: 10px; }
|
||||||
|
.header h1 { font-size: 17pt; font-weight: 800; color: #1e293b; }
|
||||||
|
.header h1 span { color: #2563eb; }
|
||||||
|
.header .ver { font-size: 8pt; color: #64748b; text-align: right; line-height: 1.4; }
|
||||||
|
|
||||||
|
/* Section */
|
||||||
|
.section { margin-bottom: 10px; }
|
||||||
|
.section-title {
|
||||||
|
font-size: 11pt; font-weight: 700; color: #fff; background: #2563eb;
|
||||||
|
padding: 3px 10px; border-radius: 4px; margin-bottom: 6px; display: inline-block;
|
||||||
|
}
|
||||||
|
.section-title.orange { background: #ea580c; }
|
||||||
|
.section-title.green { background: #16a34a; }
|
||||||
|
.section-title.red { background: #dc2626; }
|
||||||
|
.section-title.purple { background: #7c3aed; }
|
||||||
|
|
||||||
|
/* Table */
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 9.5pt; margin-bottom: 6px; }
|
||||||
|
th { background: #f1f5f9; color: #475569; font-size: 8pt; text-transform: uppercase; letter-spacing: 0.5px; text-align: left; padding: 4px 8px; border-bottom: 1.5px solid #cbd5e1; }
|
||||||
|
td { padding: 5px 8px; border-bottom: 1px solid #e2e8f0; vertical-align: top; }
|
||||||
|
tr:last-child td { border-bottom: none; }
|
||||||
|
code { background: #f1f5f9; padding: 1px 5px; border-radius: 3px; font-family: 'Cascadia Code', 'Consolas', monospace; font-size: 8.5pt; color: #1e40af; }
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.card { border: 1.5px solid #e2e8f0; border-radius: 6px; padding: 8px 10px; margin-bottom: 6px; }
|
||||||
|
.card.highlight { border-color: #2563eb; background: #eff6ff; }
|
||||||
|
.card.warn { border-color: #ea580c; background: #fff7ed; }
|
||||||
|
.card.danger { border-color: #dc2626; background: #fef2f2; }
|
||||||
|
.card h3 { font-size: 9.5pt; font-weight: 700; margin-bottom: 3px; }
|
||||||
|
.card p, .card li { font-size: 9pt; color: #475569; }
|
||||||
|
|
||||||
|
/* Flow */
|
||||||
|
.flow { display: flex; align-items: center; gap: 0; justify-content: center; margin: 6px 0; }
|
||||||
|
.flow-node { background: #f8fafc; border: 1.5px solid #cbd5e1; border-radius: 6px; padding: 4px 12px; font-size: 9pt; text-align: center; font-weight: 600; }
|
||||||
|
.flow-node.active { border-color: #16a34a; background: #f0fdf4; color: #16a34a; }
|
||||||
|
.flow-arrow { color: #94a3b8; font-size: 12pt; padding: 0 2px; }
|
||||||
|
|
||||||
|
/* Two columns */
|
||||||
|
.cols { display: flex; gap: 10px; }
|
||||||
|
.cols > * { flex: 1; }
|
||||||
|
|
||||||
|
/* Shortcut key style */
|
||||||
|
kbd { display: inline-block; background: #f1f5f9; border: 1px solid #cbd5e1; border-radius: 3px; padding: 0 4px; font-size: 8pt; font-family: 'Consolas', monospace; box-shadow: 0 1px 0 #94a3b8; }
|
||||||
|
|
||||||
|
.footer { text-align: center; font-size: 7.5pt; color: #94a3b8; border-top: 1px solid #e2e8f0; padding-top: 6px; margin-top: 8px; }
|
||||||
|
.dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: 4px; vertical-align: middle; }
|
||||||
|
.dot.green { background: #16a34a; }
|
||||||
|
.dot.blue { background: #2563eb; }
|
||||||
|
.dot.red { background: #dc2626; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<h1>Bookworm <span>Portable</span> 日常速查卡</h1>
|
||||||
|
<div class="ver">v1.5 | 92 Skills / 18 Agents / 29 Hooks<br>打印后贴在显示器旁边</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ==================== 打开 PowerShell ==================== -->
|
||||||
|
<div class="section">
|
||||||
|
<span class="section-title">第零步:打开 PowerShell</span>
|
||||||
|
|
||||||
|
<div class="cols">
|
||||||
|
<div class="card highlight">
|
||||||
|
<h3>方式一:运行框(最快)</h3>
|
||||||
|
<p>按 <kbd>Win</kbd>+<kbd>R</kbd> → 输入 <code>powershell</code> → 回车<br>
|
||||||
|
<span style="font-size:8pt;color:#64748b">如已装 PowerShell 7,输入 <code>pwsh</code> 效果更好</span></p>
|
||||||
|
</div>
|
||||||
|
<div class="card highlight">
|
||||||
|
<h3>方式二:终端新标签页</h3>
|
||||||
|
<p>已打开终端/PowerShell 窗口时:<br>按 <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd> 打开新标签页 (自动用 PowerShell 7)</p>
|
||||||
|
</div>
|
||||||
|
<div class="card highlight">
|
||||||
|
<h3>方式三:搜索</h3>
|
||||||
|
<p>按 <kbd>Win</kbd> 键 → 输入 <strong>powershell</strong> → 点击 <strong>"PowerShell 7"</strong> 或 <strong>"Windows PowerShell"</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style="font-size:8.5pt;color:#64748b;margin-top:2px">打开后看到 <code>PS C:\Users\你的用户名></code> 即为成功。如果桌面有 Bookworm 快捷方式,可跳过此步直接双击。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ==================== 每日启动 ==================== -->
|
||||||
|
<div class="section">
|
||||||
|
<span class="section-title">每日启动 (30 秒)</span>
|
||||||
|
|
||||||
|
<div class="flow">
|
||||||
|
<div class="flow-node">开代理</div>
|
||||||
|
<span class="flow-arrow">➔</span>
|
||||||
|
<div class="flow-node">双击 <strong>Bookworm</strong></div>
|
||||||
|
<span class="flow-arrow">➔</span>
|
||||||
|
<div class="flow-node">等横幅出现</div>
|
||||||
|
<span class="flow-arrow">➔</span>
|
||||||
|
<div class="flow-node active">开始工作</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr><th style="width:30%">操作</th><th>做法</th></tr>
|
||||||
|
<tr><td><span class="dot green"></span><strong>快速启动</strong></td><td>双击桌面 <strong>Bookworm</strong> 快捷方式,或双击 <code>启动Bookworm.bat</code></td></tr>
|
||||||
|
<tr><td><span class="dot blue"></span><strong>更新后启动</strong></td><td>双击 <code>更新并启动Bookworm.bat</code> (管理员通知有更新时)</td></tr>
|
||||||
|
<tr><td><span class="dot red"></span><strong>命令行启动</strong></td><td><code>cd bookworm-boot</code> → <code>pwsh -ExecutionPolicy Bypass -File install.ps1 -StartOnly</code></td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="card warn">
|
||||||
|
<h3>⚠ 启动前必须开代理</h3>
|
||||||
|
<p>Clash / V2Ray / 快柠檬 — 任选一个开着即可。脚本自动检测,无需手动配置。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ==================== 关键规则 ==================== -->
|
||||||
|
<div class="section">
|
||||||
|
<span class="section-title red">三条铁律</span>
|
||||||
|
|
||||||
|
<div class="cols">
|
||||||
|
<div class="card danger">
|
||||||
|
<h3>❌ 不要直接运行 claude</h3>
|
||||||
|
<p>新开 PowerShell 输入 <code>claude</code> 会报 "Not logged in"。<strong>必须通过 .bat 或 install.ps1 启动</strong>,脚本会注入 API 凭证。</p>
|
||||||
|
</div>
|
||||||
|
<div class="card danger">
|
||||||
|
<h3>❌ 密码不要外传</h3>
|
||||||
|
<p><strong>Gitea 密码</strong> = 下载用<br><strong>主密码</strong> = 解密 API 凭证<br>两个密码都只能你自己知道。</p>
|
||||||
|
</div>
|
||||||
|
<div class="card danger">
|
||||||
|
<h3>❌ 公用电脑必须卸载</h3>
|
||||||
|
<p>离开前双击 <code>卸载Bookworm.bat</code>,清除全部凭证和配置痕迹。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ==================== 常用命令 ==================== -->
|
||||||
|
<div class="section">
|
||||||
|
<span class="section-title purple">Claude Code 常用操作</span>
|
||||||
|
|
||||||
|
<div class="cols">
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<tr><th>快捷键</th><th>功能</th></tr>
|
||||||
|
<tr><td><kbd>Enter</kbd></td><td>发送消息</td></tr>
|
||||||
|
<tr><td><kbd>Shift+Enter</kbd></td><td>换行 (不发送)</td></tr>
|
||||||
|
<tr><td><kbd>Esc</kbd></td><td>中断当前操作</td></tr>
|
||||||
|
<tr><td><kbd>Ctrl+C</kbd></td><td>退出 Claude Code</td></tr>
|
||||||
|
<tr><td><kbd>Tab</kbd></td><td>自动补全 / 选择建议</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<tr><th>斜杠命令</th><th>功能</th></tr>
|
||||||
|
<tr><td><code>/help</code></td><td>帮助信息</td></tr>
|
||||||
|
<tr><td><code>/clear</code></td><td>清除上下文 (长对话后用)</td></tr>
|
||||||
|
<tr><td><code>/compact</code></td><td>压缩上下文</td></tr>
|
||||||
|
<tr><td><code>/model</code></td><td>切换模型</td></tr>
|
||||||
|
<tr><td><code>/cost</code></td><td>查看本次消耗</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ==================== 故障速查 ==================== -->
|
||||||
|
<div class="section">
|
||||||
|
<span class="section-title orange">故障速查 (5 大常见问题)</span>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr><th style="width:30%">症状</th><th style="width:25%">原因</th><th>解决</th></tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>ECONNRESET</strong><br><span style="font-size:8pt;color:#64748b">Unable to connect to API</span></td>
|
||||||
|
<td>代理把国内中转站流量走了国际线路</td>
|
||||||
|
<td>设 <code>$env:NO_PROXY="bww.letcareme.com"</code> 或更新脚本 <code>git pull</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Not logged in</strong></td>
|
||||||
|
<td>直接运行了 <code>claude</code></td>
|
||||||
|
<td>关掉,改用 .bat 或 install.ps1 启动</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>密码错误 3 次退出</strong></td>
|
||||||
|
<td>主密码不对 (区分大小写)</td>
|
||||||
|
<td>仔细重试,忘记联系管理员</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>完整性校验 WARN</strong><br><span style="font-size:8pt;color:#64748b">大量文件哈希不匹配</span></td>
|
||||||
|
<td>配置已更新但哈希文件未同步</td>
|
||||||
|
<td>输入 <strong>y</strong> 继续,不影响使用</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>无代理继续?</strong></td>
|
||||||
|
<td>没检测到代理软件</td>
|
||||||
|
<td>先启动 Clash/V2Ray,再重新运行脚本</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ==================== 清理/卸载 ==================== -->
|
||||||
|
<div class="section">
|
||||||
|
<span class="section-title green">清理 / 卸载</span>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr><th>场景</th><th>做法</th></tr>
|
||||||
|
<tr><td><strong>日常关闭</strong></td><td>在 Claude Code 里按 <kbd>Ctrl+C</kbd> 退出,或直接关窗口。凭证自动清除。</td></tr>
|
||||||
|
<tr><td><strong>彻底卸载</strong></td><td>双击 <code>卸载Bookworm.bat</code> — 终止进程 + 清除凭证 + 恢复原始配置 + 删除快捷方式</td></tr>
|
||||||
|
<tr><td><strong>命令行卸载</strong></td><td><code>pwsh -ExecutionPolicy Bypass -File stop.ps1 -Restore -Deep</code></td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ==================== 联系方式 ==================== -->
|
||||||
|
<div class="card highlight" style="text-align:center">
|
||||||
|
<p style="font-size:9.5pt"><strong>遇到问题?</strong> 联系管理员 — 提供截图 + 错误信息,通常 5 分钟内解决</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Bookworm Portable v1.5 — 日常使用速查卡 — © 2026 Bookworm Smart Assistant — 建议打印后贴在显示器旁边
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
87
secure-firewall.sh
Normal file
87
secure-firewall.sh
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
#!/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 "========================================="
|
||||||
67
settings.local.template.json
Normal file
67
settings.local.template.json
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"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
163
settings.template.json
Normal 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
|
||||||
|
}
|
||||||
616
setup-all.js
Normal file
616
setup-all.js
Normal file
@ -0,0 +1,616 @@
|
|||||||
|
#!/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);
|
||||||
|
});
|
||||||
119
setup-https.sh
Normal file
119
setup-https.sh
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
#!/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 "========================================="
|
||||||
151
stop.ps1
Normal file
151
stop.ps1
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Bookworm Portable - 清理/卸载脚本
|
||||||
|
.DESCRIPTION
|
||||||
|
清除环境变量, 恢复原始 .claude 目录, 清理痕迹.
|
||||||
|
.USAGE
|
||||||
|
.\stop.ps1 # 标准清理 (保留 Bookworm 配置)
|
||||||
|
.\stop.ps1 -Restore # 完整恢复 (删除 Bookworm, 恢复备份)
|
||||||
|
.\stop.ps1 -Deep # 深度清理 (含 PS 历史)
|
||||||
|
#>
|
||||||
|
|
||||||
|
param(
|
||||||
|
[switch]$Restore,
|
||||||
|
[switch]$Deep
|
||||||
|
)
|
||||||
|
|
||||||
|
$ClaudeTarget = Join-Path $env:USERPROFILE ".claude"
|
||||||
|
$BackupPath = Join-Path $env:USERPROFILE ".claude.bw-backup"
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Bookworm Portable - 清理" -ForegroundColor Cyan
|
||||||
|
Write-Host " ========================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# 1. 终止 Claude Code 及 Node.js 子进程
|
||||||
|
$claudeProcs = Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -in @("claude", "claude-code") }
|
||||||
|
if ($claudeProcs) {
|
||||||
|
Write-Host "[1/5] 终止 Claude Code 进程..." -ForegroundColor Yellow
|
||||||
|
$claudeProcs | Stop-Process -Force
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
# 清理残留 node 子进程 (hooks)
|
||||||
|
Get-Process node -ErrorAction SilentlyContinue | Where-Object {
|
||||||
|
$_.CommandLine -match '\.claude[\\/]hooks'
|
||||||
|
} | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Host " [OK] 进程已终止" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "[1/5] 无 Claude Code 进程运行" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. 清除进程级环境变量
|
||||||
|
Write-Host "[2/5] 清除环境变量..." -ForegroundColor White
|
||||||
|
$envVars = @(
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
foreach ($v in $envVars) {
|
||||||
|
if ([System.Environment]::GetEnvironmentVariable($v, "Process")) {
|
||||||
|
[System.Environment]::SetEnvironmentVariable($v, $null, "Process")
|
||||||
|
Write-Host " 清除: $v" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host " [OK] 环境变量已清除" -ForegroundColor Green
|
||||||
|
|
||||||
|
# 3. 清除凭证缓存 (Credential Manager + 注册表)
|
||||||
|
Write-Host "[3/5] 清除凭证缓存..." -ForegroundColor White
|
||||||
|
cmdkey /delete:bookworm-secrets 2>$null | Out-Null
|
||||||
|
Remove-Item "HKCU:\Software\Bookworm" -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Host " 清除: Bookworm 本日免密缓存" -ForegroundColor Gray
|
||||||
|
|
||||||
|
# 4. 清除 Git 凭证缓存
|
||||||
|
Write-Host "[4/5] 清除 Git 凭证缓存..." -ForegroundColor White
|
||||||
|
$gitCredTargets = @("git:https://code.letcareme.com", "git:http://8.138.11.105", "git:https://8.138.11.105")
|
||||||
|
foreach ($target in $gitCredTargets) {
|
||||||
|
$exists = cmdkey /list 2>$null | Select-String $target
|
||||||
|
if ($exists) {
|
||||||
|
cmdkey /delete:$target 2>$null
|
||||||
|
Write-Host " 清除: $target" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# 通过 git credential reject 清除缓存
|
||||||
|
@("code.letcareme.com", "8.138.11.105") | ForEach-Object {
|
||||||
|
@"
|
||||||
|
protocol=https
|
||||||
|
host=$_
|
||||||
|
"@ | git credential reject 2>$null
|
||||||
|
}
|
||||||
|
Write-Host " [OK] Git 凭证已清除" -ForegroundColor Green
|
||||||
|
|
||||||
|
# 4. 恢复备份 (如果请求)
|
||||||
|
if ($Restore) {
|
||||||
|
Write-Host "[4/5] 恢复原始 .claude 目录..." -ForegroundColor White
|
||||||
|
if (Test-Path $BackupPath) {
|
||||||
|
if (Test-Path $ClaudeTarget) {
|
||||||
|
$retries = 3
|
||||||
|
while ($retries -gt 0) {
|
||||||
|
try {
|
||||||
|
Remove-Item $ClaudeTarget -Recurse -Force -ErrorAction Stop
|
||||||
|
break
|
||||||
|
} catch {
|
||||||
|
$retries--
|
||||||
|
if ($retries -gt 0) {
|
||||||
|
Write-Host " 文件占用,等待重试... ($retries)" -ForegroundColor Yellow
|
||||||
|
Start-Sleep 3
|
||||||
|
} else {
|
||||||
|
Write-Host " [ERROR] 无法删除 .claude,可能有文件被占用" -ForegroundColor Red
|
||||||
|
Write-Host " 请关闭所有 Node.js/Claude 进程后重试" -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host " 已删除 Bookworm 配置" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
Rename-Item $BackupPath $ClaudeTarget
|
||||||
|
Write-Host " [OK] 原始 .claude 已恢复" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host " [!] 无备份可恢复 (.claude.bw-backup 不存在)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "[4/5] 保留 Bookworm 配置 (使用 -Restore 可恢复原始)" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
# 5. 深度清理 (可选)
|
||||||
|
if ($Deep) {
|
||||||
|
Write-Host "[5/5] 深度清理..." -ForegroundColor White
|
||||||
|
|
||||||
|
# 清除 PowerShell 历史
|
||||||
|
$histFile = Join-Path $env:APPDATA "Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt"
|
||||||
|
if (Test-Path $histFile) {
|
||||||
|
# 仅删除含 bookworm/secrets/api_key 的行
|
||||||
|
$lines = Get-Content $histFile
|
||||||
|
$cleaned = $lines | Where-Object {
|
||||||
|
$_ -notmatch 'secrets\.enc|ANTHROPIC_API_KEY|api[_-]?key|bookworm-portable'
|
||||||
|
}
|
||||||
|
Set-Content $histFile -Value $cleaned
|
||||||
|
Write-Host " [OK] PS 历史已清理敏感条目" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# 清除 Claude Code 本地缓存
|
||||||
|
$cacheDir = Join-Path $env:LOCALAPPDATA "claude-code"
|
||||||
|
if (Test-Path $cacheDir) {
|
||||||
|
Write-Host " 发现本地缓存: $cacheDir" -ForegroundColor Yellow
|
||||||
|
Write-Host " (手动决定是否删除)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host " [OK] 深度清理完成" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "[5/5] 跳过深度清理 (使用 -Deep 可清理 PS 历史)" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " ╔══════════════════════════════════════════╗" -ForegroundColor Green
|
||||||
|
Write-Host " ║ Bookworm 已清理完毕 ║" -ForegroundColor Green
|
||||||
|
Write-Host " ╚══════════════════════════════════════════╝" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
50
卸载Bookworm.bat
Normal file
50
卸载Bookworm.bat
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 > nul
|
||||||
|
title Bookworm Portable - 卸载
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ====================================
|
||||||
|
echo Bookworm Portable - 完整卸载
|
||||||
|
echo ====================================
|
||||||
|
echo.
|
||||||
|
echo 将执行:
|
||||||
|
echo - 终止 Claude Code 进程
|
||||||
|
echo - 清除所有环境变量和凭证缓存
|
||||||
|
echo - 恢复原始 .claude 目录
|
||||||
|
echo - 清除 PowerShell 历史和 Git 凭证
|
||||||
|
echo - 删除桌面快捷方式
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: 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
|
||||||
|
)
|
||||||
|
|
||||||
|
:: 删除桌面快捷方式
|
||||||
|
del "%USERPROFILE%\Desktop\Bookworm.lnk" 2>nul
|
||||||
|
del "%USERPROFILE%\Desktop\更新Bookworm.lnk" 2>nul
|
||||||
|
|
||||||
|
:: 清除凭证缓存注册表
|
||||||
|
reg delete "HKCU\Software\Bookworm" /f 2>nul
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ====================================
|
||||||
|
echo Bookworm 已完全卸载
|
||||||
|
echo 可安全删除 bookworm-boot 文件夹
|
||||||
|
echo ====================================
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
17
启动Bookworm-v3.bat
Normal file
17
启动Bookworm-v3.bat
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
chcp 65001 > nul 2>&1
|
||||||
|
title Bookworm Smart Assistant
|
||||||
|
|
||||||
|
:: 日常启动入口: 静默更新 + 直接启动 (无需管理员权限)
|
||||||
|
set "NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1"
|
||||||
|
|
||||||
|
where node >nul 2>nul
|
||||||
|
if %errorlevel% equ 0 goto :RUN
|
||||||
|
echo [!!] Node.js 未安装, 请先运行 Bookworm-Install.bat
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:RUN
|
||||||
|
node "%~dp0setup-all.js" --start
|
||||||
|
endlocal
|
||||||
27
启动Bookworm.bat
Normal file
27
启动Bookworm.bat
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
@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 ====================================
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo.
|
||||||
|
echo 启动失败,按任意键退出...
|
||||||
|
pause > nul
|
||||||
|
)
|
||||||
28
更新并启动Bookworm.bat
Normal file
28
更新并启动Bookworm.bat
Normal 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
|
||||||
|
)
|
||||||
Loading…
Reference in New Issue
Block a user