#!/usr/bin/env bash # Tumpai Handbook · Claude Code installer for macOS / Linux. # # Recommended: # bash <(curl -fsSL https://tumpai-handbook.pages.dev/install-claude.sh) # # Unattended: # TUMPAI_API_KEY=sk-xxx CLAUDE_TUMPAI_YES=1 bash <(curl -fsSL https://tumpai-handbook.pages.dev/install-claude.sh) set -e BASE_URL="${ANTHROPIC_BASE_URL:-${CLAUDE_BASE_URL:-https://api.tumpai.site:2053}}" BASE_URL="${BASE_URL%/}" if [ "${BASE_URL%/v1}" != "$BASE_URL" ]; then BASE_URL="${BASE_URL%/v1}" fi ENV_KEY_NAME="TUMPAI_API_KEY" ASSUME_YES="${CLAUDE_TUMPAI_YES:-${CLAUDE_YES:-0}}" for arg in "$@"; do case "$arg" in -y|--yes) ASSUME_YES=1 ;; -h|--help) cat <&2; exit 1; } title() { echo; echo "${C_BLD}${C_BLU}==> $*${C_RST}"; } NON_INTERACTIVE=0 if [ ! -t 0 ]; then if [ -r /dev/tty ] && exec /dev/null; then : else warn "未检测到可交互终端,进入非交互模式(需提前设置 TUMPAI_API_KEY 或 ANTHROPIC_AUTH_TOKEN)" NON_INTERACTIVE=1 ASSUME_YES=1 fi fi OS="$(uname -s)" ARCH="$(uname -m)" case "$OS" in Darwin) PLATFORM="mac" ;; Linux) PLATFORM="linux" ;; *) fail "暂不支持的系统:$OS(Windows 请使用 install-claude.ps1)" ;; esac echo cat </dev/null 2>&1; then ok "已安装 claude($(claude --version 2>/dev/null || echo unknown))" else info "开始运行官方 Claude Code 安装器" curl -fsSL https://claude.ai/install.sh | bash || fail "Claude Code 安装失败" export PATH="$HOME/.local/bin:$HOME/.claude/local:$PATH" if command -v claude >/dev/null 2>&1; then ok "Claude Code 已安装:$(claude --version 2>/dev/null || echo 已就绪)" else warn "安装器已完成,但当前终端还找不到 claude。开新终端后再运行 claude。" fi fi title "2/4 录入 API 密钥" API_KEY="${TUMPAI_API_KEY:-${ANTHROPIC_AUTH_TOKEN:-${CLAUDE_API_KEY:-}}}" if [ -n "$API_KEY" ]; then ok "已从环境变量读取密钥(${#API_KEY} 字符)" elif [ "$NON_INTERACTIVE" = "1" ]; then fail "非交互模式但未提供 TUMPAI_API_KEY 或 ANTHROPIC_AUTH_TOKEN" else echo "请粘贴服务方发放的 Tumpai API Key:" echo " · 这把密钥会写入本机 shell 配置" echo " · 对 Claude Code 会映射为 ANTHROPIC_AUTH_TOKEN" echo " · 不会上传到网站" echo while [ -z "$API_KEY" ]; do read -rp "${C_BLD}API Key: ${C_RST}" API_KEY if [ -z "$API_KEY" ]; then warn "密钥为空,请重新输入" continue fi if [ "${#API_KEY}" -lt 10 ]; then warn "密钥太短(${#API_KEY} 字符),看起来不太像" read -rp "继续使用此密钥?[y/N] " YN case "$YN" in [Yy]*) break ;; *) API_KEY="" ;; esac fi done ok "已收到密钥(${#API_KEY} 字符)" fi title "3/4 写入环境变量" SHELL_NAME="${SHELL:-}" SHELL_RC="" case "$SHELL_NAME" in *zsh) SHELL_RC="$HOME/.zshrc" ;; *bash) if [ "$PLATFORM" = "mac" ]; then SHELL_RC="$HOME/.bash_profile"; else SHELL_RC="$HOME/.bashrc"; fi ;; *fish) SHELL_RC="$HOME/.config/fish/config.fish" ;; *) SHELL_RC="$HOME/.profile" ;; esac RC_FILES=("$SHELL_RC") case "$SHELL_NAME" in *zsh) RC_FILES+=("$HOME/.zshenv") ;; *bash) if [ "$PLATFORM" = "mac" ]; then RC_FILES+=("$HOME/.bashrc"); else RC_FILES+=("$HOME/.bash_profile"); fi ;; esac for rc in "${RC_FILES[@]}"; do [ -f "$rc" ] || continue if [ "$PLATFORM" = "mac" ]; then sed -i '' "/# >>> claude-tumpai >>>/,/# <<< claude-tumpai <</dev/null || true else sed -i "/# >>> claude-tumpai >>>/,/# <<< claude-tumpai <</dev/null || true fi done case "$SHELL_NAME" in *fish) mkdir -p "$(dirname "$SHELL_RC")" cat >> "$SHELL_RC" <>> claude-tumpai >>> set -gx $ENV_KEY_NAME "$API_KEY" set -gx ANTHROPIC_AUTH_TOKEN "$API_KEY" set -gx ANTHROPIC_BASE_URL "$BASE_URL" set -e ANTHROPIC_DEFAULT_OPUS_MODEL set -e ANTHROPIC_DEFAULT_SONNET_MODEL set -e ANTHROPIC_DEFAULT_HAIKU_MODEL set -e ANTHROPIC_MODEL set -e ANTHROPIC_SMALL_FAST_MODEL set -gx CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY "1" # <<< claude-tumpai <<< EOF ;; *) for rc in "${RC_FILES[@]}"; do mkdir -p "$(dirname "$rc")" cat >> "$rc" <>> claude-tumpai >>> export $ENV_KEY_NAME="$API_KEY" export ANTHROPIC_AUTH_TOKEN="$API_KEY" export ANTHROPIC_BASE_URL="$BASE_URL" unset ANTHROPIC_DEFAULT_OPUS_MODEL unset ANTHROPIC_DEFAULT_SONNET_MODEL unset ANTHROPIC_DEFAULT_HAIKU_MODEL unset ANTHROPIC_MODEL unset ANTHROPIC_SMALL_FAST_MODEL export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY="1" # <<< claude-tumpai <<< EOF done ;; esac export "$ENV_KEY_NAME=$API_KEY" export ANTHROPIC_AUTH_TOKEN="$API_KEY" export ANTHROPIC_BASE_URL="$BASE_URL" unset ANTHROPIC_DEFAULT_OPUS_MODEL unset ANTHROPIC_DEFAULT_SONNET_MODEL unset ANTHROPIC_DEFAULT_HAIKU_MODEL unset ANTHROPIC_MODEL unset ANTHROPIC_SMALL_FAST_MODEL export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY="1" ok "已写入 ${RC_FILES[*]}(开新终端即可生效)" CLAUDE_CFG_DIR="$HOME/.claude" CLAUDE_SETTINGS_FILE="$CLAUDE_CFG_DIR/settings.json" mkdir -p "$CLAUDE_CFG_DIR" if [ -f "$CLAUDE_SETTINGS_FILE" ]; then CLAUDE_SETTINGS_BAK="$CLAUDE_SETTINGS_FILE.bak-$(date +%Y%m%d-%H%M%S)" cp "$CLAUDE_SETTINGS_FILE" "$CLAUDE_SETTINGS_BAK" ok "已备份 Claude Code 设置到 $CLAUDE_SETTINGS_BAK" fi write_claude_yolo_settings() { if command -v node >/dev/null 2>&1; then node - "$CLAUDE_SETTINGS_FILE" <<'NODE' const fs = require('fs'); const file = process.argv[2]; let settings = {}; if (fs.existsSync(file)) { const raw = fs.readFileSync(file, 'utf8').trim(); if (raw) settings = JSON.parse(raw); } if (!settings || typeof settings !== 'object' || Array.isArray(settings)) settings = {}; settings.$schema = settings.$schema || 'https://json.schemastore.org/claude-code-settings.json'; if (!settings.permissions || typeof settings.permissions !== 'object' || Array.isArray(settings.permissions)) { settings.permissions = {}; } settings.permissions.defaultMode = 'bypassPermissions'; settings.permissions.skipDangerousModePermissionPrompt = true; fs.writeFileSync(file, JSON.stringify(settings, null, 2) + '\n'); NODE return fi if command -v python3 >/dev/null 2>&1; then python3 - "$CLAUDE_SETTINGS_FILE" <<'PY' import json import os import sys file = sys.argv[1] settings = {} if os.path.exists(file): raw = open(file, encoding="utf-8").read().strip() if raw: settings = json.loads(raw) if not isinstance(settings, dict): settings = {} settings.setdefault("$schema", "https://json.schemastore.org/claude-code-settings.json") permissions = settings.get("permissions") if not isinstance(permissions, dict): permissions = {} settings["permissions"] = permissions permissions["defaultMode"] = "bypassPermissions" permissions["skipDangerousModePermissionPrompt"] = True with open(file, "w", encoding="utf-8") as f: json.dump(settings, f, ensure_ascii=False, indent=2) f.write("\n") PY return fi cat > "$CLAUDE_SETTINGS_FILE" <<'EOF' { "$schema": "https://json.schemastore.org/claude-code-settings.json", "permissions": { "defaultMode": "bypassPermissions", "skipDangerousModePermissionPrompt": true } } EOF } if write_claude_yolo_settings; then ok "已开启 Claude Code YOLO 模式:permissions.defaultMode=bypassPermissions" else warn "自动合并 Claude Code settings.json 失败;已保留备份,请手动检查 $CLAUDE_SETTINGS_FILE" fi title "4/4 验证中转站" CHECK_FILE="$(mktemp "${TMPDIR:-/tmp}/claude_tumpai_check.XXXXXX")" HTTP_CODE="$(curl -s -o "$CHECK_FILE" -w "%{http_code}" \ --max-time 15 \ -H "Authorization: Bearer $API_KEY" \ "$BASE_URL/v1/models" || echo "000")" case "$HTTP_CODE" in 200) ok "服务可达,密钥有效" ;; 401|403) warn "服务可达,但密钥被拒绝(HTTP $HTTP_CODE)。请检查密钥。" ;; 000) warn "无法连接 $BASE_URL,请检查网络或稍后重试" ;; *) warn "返回 HTTP $HTTP_CODE,可继续打开 Claude Code 手动测试" ;; esac rm -f "$CHECK_FILE" echo cat <