#!/usr/bin/env bash # Tumpai Handbook · unified line switcher for macOS / Linux. # # Switch to Tumpai CN2 line: # bash <(curl -fsSL https://tumpai-handbook.pages.dev/switch-line.sh) cn2 # # Switch back to Tumpai normal line: # bash <(curl -fsSL https://tumpai-handbook.pages.dev/switch-line.sh) normal set -e if [ -t 1 ]; then C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YLW=$'\033[33m' C_BLU=$'\033[34m'; C_BLD=$'\033[1m'; C_RST=$'\033[0m' else C_RED=""; C_GRN=""; C_YLW=""; C_BLU=""; C_BLD=""; C_RST="" fi ENV_KEY_NAME="TUMPAI_API_KEY" PROVIDER_NAME="tumpai" CODEX_MODEL_DEFAULT="${CODEX_MODEL:-gpt-5.5}" GEMINI_API_VERSION="${GOOGLE_GENAI_API_VERSION:-${GEMINI_API_VERSION:-v1beta}}" GEMINI_AUTH_MECHANISM="${GEMINI_API_KEY_AUTH_MECHANISM:-x-goog-api-key}" GEMINI_MODEL_VALUE="${GEMINI_MODEL:-auto-gemini-3}" FABLE_MODEL_ID="${CLAUDE_TUMPAI_FABLE_MODEL:-${ANTHROPIC_DEFAULT_FABLE_MODEL:-claude-fable-5}}" FABLE_MODEL_NAME="${CLAUDE_TUMPAI_FABLE_MODEL_NAME:-Fable 5}" FABLE_MODEL_DESCRIPTION="${CLAUDE_TUMPAI_FABLE_MODEL_DESCRIPTION:-Most capable for your hardest and longest-running tasks}" ok() { echo "${C_GRN}OK${C_RST} $*"; } info() { echo "${C_BLU}INFO${C_RST} $*"; } warn() { echo "${C_YLW}WARN${C_RST} $*"; } fail() { echo "${C_RED}ERROR${C_RST} $*" >&2; exit 1; } title() { echo; echo "${C_BLD}${C_BLU}==> $*${C_RST}"; } usage() { cat <<'HELP' 用法: bash <(curl -fsSL https://tumpai-handbook.pages.dev/switch-line.sh) cn2 bash <(curl -fsSL https://tumpai-handbook.pages.dev/switch-line.sh) normal 选项: -l, --line normal|cn2 指定 Tumpai 线路 -p, --provider normal|cn2 兼容旧参数名 -y, --yes 跳过确认 -h, --help 查看帮助 环境变量: TUMPAI_SWITCH_LINE normal 或 cn2 TUMPAI_SWITCH_YES=1 等同 --yes TUMPAI_API_KEY 预填 Tumpai 客户端密钥 说明: 本脚本会同时切换: - Codex/OpenAI: ~/.codex/config.toml 的 model_providers.tumpai.base_url - Claude Code: ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN - Gemini CLI: GOOGLE_GEMINI_BASE_URL / GEMINI_API_KEY 默认不做健康探测;正常 Claude 请求会进入所选中转站,并由服务端住宅代理配置处理。 HELP } normalize_line() { case "$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" in normal|tumpai|default|standard|1) printf 'normal' ;; cn2|premium|jp|jingpin|2) printf 'cn2' ;; *) return 1 ;; esac } escape_dq() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\$/\\$/g; s/`/\\`/g' } remove_marked_block() { local file="$1" local marker="$2" [ -f "$file" ] || return 0 if [ "$PLATFORM" = "mac" ]; then sed -i '' "/# >>> $marker >>>/,/# <<< $marker <</dev/null || true else sed -i "/# >>> $marker >>>/,/# <<< $marker <</dev/null || true fi } write_codex_config() { title "2/6 写入 Codex 配置" local cfg_dir="$HOME/.codex" local cfg_file="$cfg_dir/config.toml" local tmp_cfg mkdir -p "$cfg_dir" if [ -f "$cfg_file" ]; then local bak="$cfg_file.bak-$(date +%Y%m%d-%H%M%S)" cp "$cfg_file" "$bak" ok "已备份旧 Codex 配置到 $bak" fi if [ ! -s "$cfg_file" ]; then cat > "$cfg_file" < "$tmp_cfg" mv "$tmp_cfg" "$cfg_file" fi chmod 600 "$cfg_file" 2>/dev/null || true ok "Codex/OpenAI base_url=$CODEX_BASE_URL" } write_shell_env() { title "3/6 写入 shell 环境变量" local shell_name="${SHELL:-}" local 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 local rc_files=("$shell_rc") case "$shell_name" in *zsh) rc_files+=("$HOME/.zshenv" "$HOME/.zprofile") ;; *bash) if [ "$PLATFORM" = "mac" ]; then rc_files+=("$HOME/.bashrc"); else rc_files+=("$HOME/.bash_profile"); fi ;; esac local rc for rc in "${rc_files[@]}"; do remove_marked_block "$rc" "tumpai-line-switch" remove_marked_block "$rc" "claude-tumpai" remove_marked_block "$rc" "claude-provider-switch" done local key_esc claude_esc codex_esc gemini_esc fable_id_esc fable_name_esc fable_desc_esc api_version_esc auth_esc gemini_model_esc key_esc="$(escape_dq "$TARGET_KEY")" claude_esc="$(escape_dq "$CLAUDE_BASE_URL")" codex_esc="$(escape_dq "$CODEX_BASE_URL")" gemini_esc="$(escape_dq "$GEMINI_BASE_URL")" fable_id_esc="$(escape_dq "$FABLE_MODEL_ID")" fable_name_esc="$(escape_dq "$FABLE_MODEL_NAME")" fable_desc_esc="$(escape_dq "$FABLE_MODEL_DESCRIPTION")" api_version_esc="$(escape_dq "$GEMINI_API_VERSION")" auth_esc="$(escape_dq "$GEMINI_AUTH_MECHANISM")" gemini_model_esc="$(escape_dq "$GEMINI_MODEL_VALUE")" case "$shell_name" in *fish) mkdir -p "$(dirname "$shell_rc")" cat >> "$shell_rc" <>> tumpai-line-switch >>> set -gx TUMPAI_API_KEY "$key_esc" set -gx ANTHROPIC_AUTH_TOKEN "$key_esc" set -gx ANTHROPIC_BASE_URL "$claude_esc" set -gx ANTHROPIC_DEFAULT_FABLE_MODEL "$fable_id_esc" set -gx ANTHROPIC_DEFAULT_FABLE_MODEL_NAME "$fable_name_esc" set -gx ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION "$fable_desc_esc" 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 -e ANTHROPIC_CUSTOM_MODEL_OPTION set -e ANTHROPIC_CUSTOM_MODEL_OPTION_NAME set -e ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION set -e ANTHROPIC_CUSTOM_MODEL_OPTION_SUPPORTED_CAPABILITIES set -e CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY set -gx GEMINI_API_KEY "$key_esc" set -gx GOOGLE_GEMINI_BASE_URL "$gemini_esc" set -gx GOOGLE_GENAI_API_VERSION "$api_version_esc" set -gx GEMINI_API_KEY_AUTH_MECHANISM "$auth_esc" set -gx GEMINI_MODEL "$gemini_model_esc" # Codex provider base_url is stored in ~/.codex/config.toml: $codex_esc # <<< tumpai-line-switch <<< EOF ;; *) for rc in "${rc_files[@]}"; do mkdir -p "$(dirname "$rc")" cat >> "$rc" <>> tumpai-line-switch >>> export TUMPAI_API_KEY="$key_esc" export ANTHROPIC_AUTH_TOKEN="$key_esc" export ANTHROPIC_BASE_URL="$claude_esc" export ANTHROPIC_DEFAULT_FABLE_MODEL="$fable_id_esc" export ANTHROPIC_DEFAULT_FABLE_MODEL_NAME="$fable_name_esc" export ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION="$fable_desc_esc" unset ANTHROPIC_DEFAULT_OPUS_MODEL unset ANTHROPIC_DEFAULT_SONNET_MODEL unset ANTHROPIC_DEFAULT_HAIKU_MODEL unset ANTHROPIC_MODEL unset ANTHROPIC_SMALL_FAST_MODEL unset ANTHROPIC_CUSTOM_MODEL_OPTION unset ANTHROPIC_CUSTOM_MODEL_OPTION_NAME unset ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION unset ANTHROPIC_CUSTOM_MODEL_OPTION_SUPPORTED_CAPABILITIES unset CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY export GEMINI_API_KEY="$key_esc" export GOOGLE_GEMINI_BASE_URL="$gemini_esc" export GOOGLE_GENAI_API_VERSION="$api_version_esc" export GEMINI_API_KEY_AUTH_MECHANISM="$auth_esc" export GEMINI_MODEL="$gemini_model_esc" # Codex provider base_url is stored in ~/.codex/config.toml: $codex_esc # <<< tumpai-line-switch <<< EOF done ;; esac export TUMPAI_API_KEY="$TARGET_KEY" export ANTHROPIC_AUTH_TOKEN="$TARGET_KEY" export ANTHROPIC_BASE_URL="$CLAUDE_BASE_URL" export ANTHROPIC_DEFAULT_FABLE_MODEL="$FABLE_MODEL_ID" export ANTHROPIC_DEFAULT_FABLE_MODEL_NAME="$FABLE_MODEL_NAME" export ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION="$FABLE_MODEL_DESCRIPTION" unset ANTHROPIC_DEFAULT_OPUS_MODEL unset ANTHROPIC_DEFAULT_SONNET_MODEL unset ANTHROPIC_DEFAULT_HAIKU_MODEL unset ANTHROPIC_MODEL unset ANTHROPIC_SMALL_FAST_MODEL unset ANTHROPIC_CUSTOM_MODEL_OPTION unset ANTHROPIC_CUSTOM_MODEL_OPTION_NAME unset ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION unset ANTHROPIC_CUSTOM_MODEL_OPTION_SUPPORTED_CAPABILITIES unset CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY export GEMINI_API_KEY="$TARGET_KEY" export GOOGLE_GEMINI_BASE_URL="$GEMINI_BASE_URL" export GOOGLE_GENAI_API_VERSION="$GEMINI_API_VERSION" export GEMINI_API_KEY_AUTH_MECHANISM="$GEMINI_AUTH_MECHANISM" export GEMINI_MODEL="$GEMINI_MODEL_VALUE" ok "已写入 ${rc_files[*]}(重新打开终端即可生效)" } write_gemini_env_file() { title "4/6 写入 Gemini CLI .env" local gemini_dir="$HOME/.gemini" local gemini_env="$gemini_dir/.env" mkdir -p "$gemini_dir" touch "$gemini_env" remove_marked_block "$gemini_env" "gemini-tumpai" remove_marked_block "$gemini_env" "tumpai-line-switch" local key_esc gemini_esc api_version_esc auth_esc model_esc key_esc="$(escape_dq "$TARGET_KEY")" gemini_esc="$(escape_dq "$GEMINI_BASE_URL")" api_version_esc="$(escape_dq "$GEMINI_API_VERSION")" auth_esc="$(escape_dq "$GEMINI_AUTH_MECHANISM")" model_esc="$(escape_dq "$GEMINI_MODEL_VALUE")" cat >> "$gemini_env" <>> gemini-tumpai >>> TUMPAI_API_KEY="$key_esc" GEMINI_API_KEY="$key_esc" GOOGLE_GEMINI_BASE_URL="$gemini_esc" GOOGLE_GENAI_API_VERSION="$api_version_esc" GEMINI_API_KEY_AUTH_MECHANISM="$auth_esc" GEMINI_MODEL="$model_esc" # <<< gemini-tumpai <<< EOF chmod 600 "$gemini_env" 2>/dev/null || true ok "Gemini base_url=$GEMINI_BASE_URL" } cleanup_caches() { title "5/6 清理旧缓存" local claude_cache="$HOME/.claude/cache/gateway-models.json" if [ -f "$claude_cache" ]; then rm -f "$claude_cache" ok "已清理 $claude_cache" else info "未发现 Claude Code 网关模型缓存" fi } check_gateway() { local name="$1" local url="$2" local auth_mode="$3" local check_file http_code check_file="$(mktemp "${TMPDIR:-/tmp}/tumpai_line_check.XXXXXX")" if [ "$auth_mode" = "x-goog-api-key" ]; then http_code="$(curl -s -o "$check_file" -w "%{http_code}" --max-time 15 -H "x-goog-api-key: $TARGET_KEY" "$url" 2>/dev/null || true)" else http_code="$(curl -s -o "$check_file" -w "%{http_code}" --max-time 15 -H "Authorization: Bearer $TARGET_KEY" "$url" 2>/dev/null || true)" fi [ -n "$http_code" ] || http_code="000" case "$http_code" in 200) ok "$name 连接正常" ;; 401|403) warn "$name 返回 HTTP $http_code,请确认 API Key 是否正确" ;; 000) warn "无法连接 $name,请检查网络或稍后重试" ;; *) warn "$name 返回 HTTP $http_code,配置已写入,可稍后再试" ;; esac rm -f "$check_file" } verify_gateways() { title "6/6 跳过主动探测" if ! command -v curl >/dev/null 2>&1; then warn "未找到 curl,跳过探测" return fi if [ "${TUMPAI_SWITCH_PROBE:-0}" != "1" ]; then info "默认不做健康探测;正常 Claude 请求会进入 ${CLAUDE_BASE_URL},并由服务端住宅代理配置处理。" info "如需只探测非 Claude 入口,可重新运行前设置 TUMPAI_SWITCH_PROBE=1。" return fi title "6/6 验证非 Claude 入口连通性" check_gateway "Codex/OpenAI $DISPLAY_NAME" "$CODEX_BASE_URL/models" "bearer" check_gateway "Gemini $DISPLAY_NAME" "$GEMINI_BASE_URL/$GEMINI_API_VERSION/models" "$GEMINI_AUTH_MECHANISM" info "已跳过 Claude 主动探测:Claude 上游认证必须由服务端住宅代理链路处理,脚本不会触发 VPS 直连。" } LINE="${TUMPAI_SWITCH_LINE:-}" ASSUME_YES="${TUMPAI_SWITCH_YES:-0}" while [ $# -gt 0 ]; do case "$1" in normal|Normal|NORMAL|tumpai|Tumpai|TUMPAI|default|Default|DEFAULT|standard|Standard|STANDARD|cn2|CN2|premium|Premium|PREMIUM|jp|JP|jingpin|Jingpin|JINGPIN) LINE="$1" shift ;; -l|--line|-p|--provider) shift [ $# -gt 0 ] || fail "--line 后面需要跟 normal 或 cn2" LINE="$1" shift ;; -y|--yes) ASSUME_YES=1 shift ;; -h|--help) usage exit 0 ;; *) usage fail "未知参数:$1" ;; esac done NON_INTERACTIVE=0 if [ ! -t 0 ]; then if [ -r /dev/tty ] && { exec /dev/null; then : else NON_INTERACTIVE=1 ASSUME_YES=1 fi fi if [ -z "$LINE" ]; then if [ "$NON_INTERACTIVE" = "1" ]; then fail "非交互模式需要指定 TUMPAI_SWITCH_LINE=normal|cn2" fi echo "请选择 Tumpai 要使用的线路(Codex / Claude / Gemini 会一起切换):" echo " 1) 普通线路 https://api.tumpai.site:2053" echo " 2) 精品线路 https://cn2-api.tumpai.site:2053" read -rp "${C_BLD}输入 1/2 或 normal/cn2: ${C_RST}" LINE case "$LINE" in 1) LINE="normal" ;; 2) LINE="cn2" ;; esac fi LINE="$(normalize_line "$LINE")" || fail "线路只能是 normal 或 cn2" case "$LINE" in normal) DISPLAY_NAME="Tumpai 普通线路" LINE_ROOT="https://api.tumpai.site:2053" ;; cn2) DISPLAY_NAME="Tumpai 精品 CN2 线路" LINE_ROOT="https://cn2-api.tumpai.site:2053" ;; esac CODEX_BASE_URL="$LINE_ROOT/v1" CLAUDE_BASE_URL="$LINE_ROOT" GEMINI_BASE_URL="$LINE_ROOT/api/provider/antigravity" case "$GEMINI_AUTH_MECHANISM" in x-goog-api-key|bearer) ;; *) fail "GEMINI_API_KEY_AUTH_MECHANISM 只能是 x-goog-api-key 或 bearer,当前为:$GEMINI_AUTH_MECHANISM" ;; esac TARGET_KEY="" KEY_SOURCE="" for name in TUMPAI_API_KEY GEMINI_API_KEY ANTHROPIC_AUTH_TOKEN CODEX_API_KEY; do value="${!name:-}" if [ -n "$value" ]; then TARGET_KEY="$value" KEY_SOURCE="$name" break fi done echo cat <