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