2026-04-05 23:34:27 +08:00
#!/bin/bash
# ============================================================
2026-04-06 13:48:05 +08:00
# Bookworm Portable - macOS Setup (从 boot 仓库内运行)
2026-04-21 19:05:01 +08:00
# Version: 3.0.3
2026-04-06 13:48:05 +08:00
#
# 用法: cd ~/bookworm-boot && bash Bookworm-Setup.sh
#
# 前提: 已 git clone bookworm-boot 到本地
# 功能: 检查依赖 → 代理检测 → 克隆配置 → 解密凭证 → 配置别名
2026-04-05 23:34:27 +08:00
# ============================================================
set -e
# 颜色定义
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
CYAN = '\033[0;36m'
2026-04-06 13:48:05 +08:00
NC = '\033[0m'
2026-04-05 23:34:27 +08:00
BOLD = '\033[1m'
# 配置
2026-04-06 13:48:05 +08:00
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
2026-04-05 23:34:27 +08:00
banner( ) {
echo ""
echo -e " ${ CYAN } ____ _ "
echo " | __ ) ___ ___ | | ____ _____ _ __ _ __ ___"
2026-04-06 13:48:05 +08:00
echo " | _ \\ / _ \\ / _ \\| |/ /\\ \\ /\\ / / _ \\| '__| '\` _ \\"
2026-04-05 23:34:27 +08:00
echo " | |_) | (_) | (_) | < \\ V V / (_) | | | | | | | |"
echo " |____/ \\___/ \\___/|_|\\_\\ \\_/\\_/ \\___/|_| |_| |_| |_|"
echo ""
2026-04-17 02:25:40 +08:00
echo -e " ${ BOLD } Portable macOS Setup v2.3.1 ${ NC } "
2026-04-06 00:20:09 +08:00
echo -e " ${ BLUE } 92 Skills | 18 Agents | 34 Hooks ${ NC } "
2026-04-05 23:34:27 +08:00
echo -e " ${ NC } "
}
2026-04-06 13:48:05 +08:00
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 } " ; }
2026-04-05 23:34:27 +08:00
# ============================================================
2026-04-06 13:48:05 +08:00
# Banner
2026-04-05 23:34:27 +08:00
# ============================================================
banner
# ============================================================
# Step 1: 检查并安装依赖
# ============================================================
step 1 "检查依赖软件"
# Homebrew
2026-04-06 13:48:05 +08:00
if ! command -v brew & >/dev/null; then
warn "Homebrew 未安装, 正在安装 (可能需要输入系统密码)..."
2026-04-05 23:34:27 +08:00
/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) "
2026-04-06 13:48:05 +08:00
PROFILE = " $HOME /.zprofile "
if ! grep -q 'homebrew' " $PROFILE " 2>/dev/null; then
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> " $PROFILE "
fi
2026-04-05 23:34:27 +08:00
fi
success "Homebrew 安装完成"
else
2026-04-06 13:48:05 +08:00
success " Homebrew $( brew --version | head -1 | awk '{print $2}' ) "
2026-04-05 23:34:27 +08:00
fi
# Node.js
2026-04-06 13:48:05 +08:00
if ! command -v node & >/dev/null; then
info "通过 Homebrew 安装 Node.js..."
2026-04-05 23:34:27 +08:00
brew install node
2026-04-06 13:48:05 +08:00
success " Node.js $( node -v) 安装完成 "
2026-04-05 23:34:27 +08:00
else
success " Node.js $( node -v) "
fi
# Git
2026-04-06 13:48:05 +08:00
if ! command -v git & >/dev/null; then
info "通过 Homebrew 安装 Git..."
2026-04-05 23:34:27 +08:00
brew install git
2026-04-06 13:48:05 +08:00
success " Git $( git --version | awk '{print $3}' ) 安装完成 "
2026-04-05 23:34:27 +08:00
else
2026-04-06 13:48:05 +08:00
success " Git $( git --version | awk '{print $3}' ) "
2026-04-05 23:34:27 +08:00
fi
2026-04-06 13:48:05 +08:00
# OpenSSL
2026-04-05 23:34:27 +08:00
OPENSSL_CMD = ""
2026-04-06 13:48:05 +08:00
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
2026-04-05 23:34:27 +08:00
if [ -z " $OPENSSL_CMD " ] ; then
2026-04-06 13:48:05 +08:00
info "通过 Homebrew 安装 OpenSSL..."
2026-04-05 23:34:27 +08:00
brew install openssl
OPENSSL_CMD = "/opt/homebrew/opt/openssl/bin/openssl"
success "OpenSSL 安装完成"
else
2026-04-06 13:48:05 +08:00
success " OpenSSL: $( $OPENSSL_CMD version 2>/dev/null | head -1) "
2026-04-05 23:34:27 +08:00
fi
# Claude Code
2026-04-06 13:48:05 +08:00
if ! command -v claude & >/dev/null; then
info "通过 npm 安装 Claude Code..."
2026-04-05 23:34:27 +08:00
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 "检测网络代理"
2026-04-06 13:48:05 +08:00
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 "
2026-04-05 23:34:27 +08:00
fi
fi
2026-04-06 13:48:05 +08:00
# 常见端口扫描 (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 可能无法启动。"
2026-04-05 23:34:27 +08:00
warn "请启动代理软件 (ClashX / Surge / V2Ray) 后重试。"
2026-04-06 13:48:05 +08:00
echo ""
read -p " 无代理继续? (y/n): " CONTINUE
if [ " $CONTINUE " != "y" ] ; then exit 1; fi
2026-04-05 23:34:27 +08:00
fi
2026-04-06 13:48:05 +08:00
success "NO_PROXY: bww.letcareme.com,code.letcareme.com"
2026-04-05 23:34:27 +08:00
# ============================================================
2026-04-06 13:48:05 +08:00
# Step 3: 克隆/更新配置仓库到 ~/.claude
2026-04-05 23:34:27 +08:00
# ============================================================
2026-04-06 13:48:05 +08:00
step 3 "同步 Bookworm 配置"
2026-04-05 23:34:27 +08:00
2026-04-06 13:48:05 +08:00
git config --global credential.helper osxkeychain 2>/dev/null || true
if [ -d " $CLAUDE_DIR /.git " ] ; then
info "配置仓库已存在, 更新..."
2026-04-13 22:32:02 +08:00
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
2026-04-05 23:34:27 +08:00
cd " $BOOT_DIR "
2026-04-06 13:48:05 +08:00
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 "配置仓库克隆完成 (旧目录已备份)"
2026-04-05 23:34:27 +08:00
else
2026-04-06 13:48:05 +08:00
info "首次安装, 克隆配置仓库 (需输入 Gitea 密码)..."
mkdir -p " $( dirname " $CLAUDE_DIR " ) "
git clone --depth 1 " $CONFIG_REPO " " $CLAUDE_DIR "
success "配置仓库克隆完成"
2026-04-05 23:34:27 +08:00
fi
2026-04-06 13:48:05 +08:00
# 创建本地运行时目录
for d in debug sessions cache backups telemetry memory projects; do
mkdir -p " $CLAUDE_DIR / $d " 2>/dev/null
done
2026-04-05 23:34:27 +08:00
# ============================================================
2026-04-06 14:05:29 +08:00
# Step 4: 解密凭证 (含 Keychain 本日免密)
2026-04-05 23:34:27 +08:00
# ============================================================
2026-04-06 13:48:05 +08:00
step 4 "解密凭证"
2026-04-05 23:34:27 +08:00
2026-04-21 01:54:42 +08:00
# ─── 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
2026-04-06 14:05:29 +08:00
# 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 "
2026-04-06 22:19:06 +08:00
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"
2026-04-06 14:05:29 +08:00
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 缓存失败 (不影响使用)"
}
2026-04-06 15:04:02 +08:00
# 解密工具: 优先 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
}
2026-04-06 23:39:17 +08:00
# 按 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
}
2026-04-06 22:47:04 +08:00
# 解析授权码: 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
2026-04-06 22:57:47 +08:00
echo " $token_upper " | tr '[:upper:]' '[:lower:]' # 兼容 bash 3.2 (macOS 默认)
2026-04-06 22:47:04 +08:00
}
2026-04-06 14:05:29 +08:00
# 先尝试缓存
if load_cached_secrets 2>/dev/null; then
: # 缓存加载成功
2026-04-17 00:22:17 +08:00
else
2026-04-17 02:25:40 +08:00
# 优先级 3: 调用 change-key.js 验证+持久化 (stdin 管道, 无 argv 泄露)
2026-04-17 00:22:17 +08:00
CHANGE_KEY_JS = " $CLAUDE_DIR /change-key.js "
if [ -f " $CHANGE_KEY_JS " ] && command -v node & >/dev/null; then
echo ""
2026-04-17 02:25:40 +08:00
info "配置中转站凭证 (https://bww.letcareme.com)"
2026-04-17 00:22:17 +08:00
for attempt in 1 2 3; do
echo ""
2026-04-17 02:25:40 +08:00
read -rs -p " 粘贴凭证 (第 $attempt /3 次, 输入不显示, 留空跳过): " UCRED
2026-04-17 00:22:17 +08:00
echo ""
[ -z " $UCRED " ] && { warn "已跳过" ; break; }
2026-04-17 02:25:40 +08:00
if printf '%s' " $UCRED " | node " $CHANGE_KEY_JS " ; then
2026-04-17 00:22:17 +08:00
UCRED = ""
2026-04-17 02:25:40 +08:00
success "凭证已验证并持久化"
2026-04-17 00:22:17 +08:00
echo ""
2026-04-17 02:25:40 +08:00
info "换凭证方式:"
info " 1. 重跑安装器"
info " 2. bash ~/.claude/change-key.sh"
info " 3. Claude Code 里: /change-key"
2026-04-17 00:22:17 +08:00
break
else
UCRED = ""
[ $attempt -lt 3 ] && warn " 验证失败, 剩余 $(( 3 - attempt)) 次 " || fail "3 次失败"
fi
done
fi
fi
2026-04-21 01:54:42 +08:00
# 优先级 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
2026-04-17 00:22:17 +08:00
# 优先级 4: 授权码模式 (向后兼容旧用户)
if [ -z " $ANTHROPIC_API_KEY " ] && { [ -f " $SECRETS_ENC " ] || ls " $BOOT_DIR " /secrets-*.enc 2>/dev/null | head -1 | grep -q .; } ; then
2026-04-06 13:48:05 +08:00
DECRYPTED = ""
2026-04-06 22:47:04 +08:00
valid_attempts = 0
2026-04-06 22:57:47 +08:00
total_attempts = 0
while [ $valid_attempts -lt 3 ] && [ $total_attempts -lt 10 ] ; do
2026-04-06 13:48:05 +08:00
echo ""
2026-04-06 22:47:04 +08:00
read -p " 输入授权码 (BW-YYYYMMDD-XXXXXX, 第 $(( valid_attempts+1)) /3 次): " AUTH_CODE
2026-04-06 22:57:47 +08:00
total_attempts = $(( total_attempts + 1 ))
2026-04-06 22:47:04 +08:00
TOKEN = $( parse_authcode " $AUTH_CODE " )
AUTH_CODE = ""
if [ " $TOKEN " = "EXPIRED" ] ; then
warn "授权码已过期, 请联系管理员获取新授权码"
2026-04-06 22:57:47 +08:00
continue # 不消耗有效次数
2026-04-06 22:47:04 +08:00
elif [ -z " $TOKEN " ] ; then
2026-04-06 22:57:47 +08:00
warn "授权码格式错误 (格式: BW-YYYYMMDD-XXXXXXXXXXXXXXXXXXXXXXXX)"
continue # 不消耗有效次数
2026-04-06 22:47:04 +08:00
fi
valid_attempts = $(( valid_attempts + 1 ))
2026-04-06 23:39:17 +08:00
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
2026-04-06 22:47:04 +08:00
TOKEN = ""
2026-04-06 13:48:05 +08:00
if [ -n " $DECRYPTED " ] ; then
2026-04-13 22:32:02 +08:00
# 白名单校验 (与 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"
2026-04-06 13:48:05 +08:00
while IFS = read -r line; do
[ -z " $line " ] && continue
key = " ${ line %%=* } "
value = " ${ line #*= } "
key = $( echo " $key " | tr -d ' ' )
if [ -n " $key " ] && [ -n " $value " ] ; then
2026-04-13 22:32:02 +08:00
# 白名单 + 长度校验
if echo " $ALLOWED_KEYS " | grep -qw " $key " && [ ${# value } -lt 512 ] ; then
export " $key = $value "
success " 已注入: $key "
else
warn " 跳过未知 key: $key "
fi
2026-04-06 13:48:05 +08:00
fi
done <<< " $DECRYPTED "
DECRYPTED = ""
2026-04-06 14:05:29 +08:00
2026-04-13 22:32:02 +08:00
# 自动缓存 (不再询问, 与 Windows 版对齐)
save_secrets_to_cache
2026-04-06 13:48:05 +08:00
break
else
2026-04-06 22:47:04 +08:00
if [ $valid_attempts -lt 3 ] ; then
warn " 授权码无效 (解密失败), 剩余重试: $(( 3 - valid_attempts)) 次 "
2026-04-06 13:48:05 +08:00
else
2026-04-06 22:47:04 +08:00
fail "3 次授权码均无效, 凭证未解密"
2026-04-06 13:48:05 +08:00
warn "可稍后手动配置 API Key"
fi
fi
done
2026-04-05 23:34:27 +08:00
else
2026-04-06 13:48:05 +08:00
if [ ! -f " $SECRETS_ENC " ] ; then
warn "secrets.enc 不存在, 跳过凭证解密"
2026-04-06 22:47:04 +08:00
info "请联系管理员获取授权码"
2026-04-06 13:48:05 +08:00
fi
fi
2026-04-21 19:05:01 +08:00
# ─── 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
2026-04-13 22:32:02 +08:00
# ── 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
2026-04-06 13:48:05 +08:00
# 渲染 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' )
2026-04-06 22:19:06 +08:00
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 ) "
2026-04-05 23:34:27 +08:00
fi
# ============================================================
# Step 5: 配置终端别名
# ============================================================
step 5 "配置终端快捷命令"
2026-04-06 13:48:05 +08:00
SHELL_RC = " $HOME /.zshrc "
if [ -n " $BASH_VERSION " ] && [ -f " $HOME /.bashrc " ] ; then
SHELL_RC = " $HOME /.bashrc "
fi
2026-04-05 23:34:27 +08:00
2026-04-06 13:48:05 +08:00
ALIAS_MARKER = "# Bookworm Portable aliases"
if ! grep -q " $ALIAS_MARKER " " $SHELL_RC " 2>/dev/null; then
cat >> " $SHELL_RC " << 'ALIA SES'
2026-04-05 23:34:27 +08:00
# Bookworm Portable aliases
2026-04-21 01:54:42 +08:00
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'
2026-04-06 13:48:05 +08:00
alias bw-update= 'cd ~/bookworm-boot && git pull && cd ~/.claude && git pull && echo "Updated!"'
2026-04-05 23:34:27 +08:00
ALIASES
2026-04-06 13:48:05 +08:00
success " 已添加到 $SHELL_RC : "
info " bw -- 启动 Bookworm"
info " bw-update -- 更新 Bookworm"
2026-04-05 23:34:27 +08:00
else
2026-04-06 13:48:05 +08:00
# 更新旧别名 (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 " << 'ALIA SES'
# Bookworm Portable aliases
2026-04-21 01:54:42 +08:00
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'
2026-04-06 13:48:05 +08:00
alias bw-update= 'cd ~/bookworm-boot && git pull && cd ~/.claude && git pull && echo "Updated!"'
ALIASES
success "终端别名已更新 (bookworm → bw)"
else
success "终端别名已配置"
fi
2026-04-05 23:34:27 +08:00
fi
# ============================================================
# Step 6: 完成
# ============================================================
step 6 "安装完成"
echo ""
2026-04-06 13:48:05 +08:00
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) "
2026-04-05 23:34:27 +08:00
echo ""
2026-04-06 13:48:05 +08:00
echo -e " ${ BOLD } 启动方式: ${ NC } "
echo -e " 终端输入: ${ CYAN } bw ${ NC } "
2026-04-05 23:34:27 +08:00
echo ""
2026-04-06 13:48:05 +08:00
echo -e " ${ BOLD } 更新: ${ NC } "
echo -e " 终端输入: ${ CYAN } bw-update ${ NC } "
2026-04-05 23:34:27 +08:00
echo ""
2026-04-06 13:48:05 +08:00
# 询问是否立即启动
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"
2026-04-21 01:54:42 +08:00
# v3.0.1: 默认模型 (中转站兼容, 默认 claude-sonnet-4-5 会 503)
export ANTHROPIC_MODEL = " ${ ANTHROPIC_MODEL :- claude -opus-4-7 } "
2026-04-06 22:06:06 +08:00
exec claude --dangerously-skip-permissions
2026-04-06 13:48:05 +08:00
fi