#!/usr/bin/env bash # ============================================================ # QCode.cc — Claude Code 一键配置脚本 # # 交互式: curl -fsSL https://qcode.cc/install/claude-code.sh | bash # 非交互: curl -fsSL https://qcode.cc/install/claude-code.sh | bash -s -- --key cr_xxx --yes # # 本脚本不需要 sudo。写入配置前会自动备份原文件。 # 文档:https://docs.qcode.cc/docs/getting-started/one-click-install # ============================================================ set -u SCRIPT_VERSION="2026.07.16" NPM_PKG="@anthropic-ai/claude-code" CLI_CMD="claude" MIN_NODE_MAJOR=20 NVM_INSTALL_URL="https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh" DOCS_URL="https://docs.qcode.cc/docs/getting-started/one-click-install" PROBE_URL="https://probe.qcode.cc/" DASHBOARD_URL="https://qcode.cc/dashboard" DOMAIN_HOSTS="api.qcode.cc asia.qcode.cc us.qcode.cc eu.qcode.cc" domain_label() { case "$1" in api.qcode.cc) printf '全球智能路由(Route 53 就近)' ;; asia.qcode.cc) printf '亚洲 · 香港' ;; us.qcode.cc) printf '北美 · 洛杉矶' ;; eu.qcode.cc) printf '欧洲 · 法兰克福' ;; esac } # ---------- 基础输出 ---------- say() { printf '%s\n' "$*"; } info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } ok() { printf '\033[1;32m ✓ \033[0m%s\n' "$*"; } warn() { printf '\033[1;33m ! \033[0m%s\n' "$*"; } fail() { printf '\033[1;31m ✗ \033[0m%s\n' "$*"; } # ---------- 参数 ---------- ARG_KEY="" ARG_DOMAIN="" ASSUME_YES=0 NO_INSTALL=0 NO_VERIFY=0 usage() { cat < QCode.cc API 密钥($DASHBOARD_URL 获取) --domain <名称> 接入域: api / asia / us / eu(缺省时自动测速选择) --yes, -y 全部确认取默认值(非交互) --no-install 只写配置,不安装 Node.js / Claude Code --no-verify 跳过配置后的连通性验证 --help 显示本帮助 --version 显示脚本版本 EOF } while [ $# -gt 0 ]; do case "$1" in --key) ARG_KEY="${2:-}"; shift 2 ;; --domain) ARG_DOMAIN="${2:-}"; shift 2 ;; --yes|-y) ASSUME_YES=1; shift ;; --no-install) NO_INSTALL=1; shift ;; --no-verify) NO_VERIFY=1; shift ;; --help|-h) usage; exit 0 ;; --version) say "$SCRIPT_VERSION"; exit 0 ;; *) fail "未知参数: $1"; usage; exit 2 ;; esac done # ---------- 交互通道(curl | bash 时 stdin 被管道占用,从 /dev/tty 读) ---------- HAS_TTY=0 if { exec 3/dev/null; then HAS_TTY=1 else exec 3<&0 fi read_input() { # $1=变量名 $2=提示 local __var="$1" __prompt="$2" __val="" IFS= read -r -u 3 -p "$__prompt" __val || true eval "$__var=\$__val" } read_secret() { # $1=变量名 $2=提示 local __var="$1" __prompt="$2" __val="" IFS= read -r -s -u 3 -p "$__prompt" __val || true printf '\n' eval "$__var=\$__val" } confirm() { # $1=提示 $2=默认(yes/no);--yes 模式直接取默认 local prompt="$1" def="${2:-no}" ans="" if [ "$ASSUME_YES" = 1 ] || [ "$HAS_TTY" = 0 ]; then [ "$def" = yes ]; return fi read_input ans "$prompt" if [ -z "$ans" ]; then [ "$def" = yes ]; return; fi case "$ans" in y|Y|yes|YES|Yes) return 0 ;; *) return 1 ;; esac } # ============================================================ say "" say " ██████╗ QCode.cc · Claude Code 一键配置 v$SCRIPT_VERSION" say " 无需 sudo · 自动备份原配置 · 配置后自动验证" say "" if ! command -v curl >/dev/null 2>&1; then fail "未找到 curl,请先安装 curl 后重试。" exit 1 fi # ---------- [1/5] 接入域:测速自选 ---------- SELECTED_HOST="" case "$ARG_DOMAIN" in api|asia|us|eu) SELECTED_HOST="$ARG_DOMAIN.qcode.cc" ;; "") ;; *) fail "--domain 只接受 api / asia / us / eu"; exit 2 ;; esac probe_ms() { # $1=host → 输出毫秒;失败输出空 local t t=$(curl -s -o /dev/null --connect-timeout 3 -m 6 -w '%{time_total}' "https://$1/v1/models" 2>/dev/null) || { printf ''; return; } awk -v s="$t" 'BEGIN{printf "%d", s*1000}' } if [ -z "$SELECTED_HOST" ]; then info "[1/5] 正在对 4 个接入域测速(各一次轻量请求)..." BEST_HOST=""; BEST_MS=999999 MENU_HOSTS=""; MENU_LINES="" i=0 for h in $DOMAIN_HOSTS; do i=$((i+1)) ms=$(probe_ms "$h") if [ -n "$ms" ] && [ "$ms" -lt "$BEST_MS" ]; then BEST_MS=$ms; BEST_HOST=$h; fi if [ -n "$ms" ]; then disp="${ms}ms"; else disp="超时"; fi MENU_HOSTS="$MENU_HOSTS $h" MENU_LINES="$MENU_LINES$i) $h — $(domain_label "$h") [$disp]\n" done if [ -z "$BEST_HOST" ]; then warn "四个接入域均探测失败(可能是本机网络问题),默认使用 api.qcode.cc" BEST_HOST="api.qcode.cc" fi say "" printf "$MENU_LINES" say "" DEF_IDX=0; i=0 for h in $MENU_HOSTS; do i=$((i+1)); [ "$h" = "$BEST_HOST" ] && DEF_IDX=$i; done choice="" if [ "$ASSUME_YES" = 1 ] || [ "$HAS_TTY" = 0 ]; then choice="$DEF_IDX" else read_input choice "请选择接入域 [回车 = $DEF_IDX(最快: $BEST_HOST)]: " fi [ -z "$choice" ] && choice="$DEF_IDX" i=0 for h in $MENU_HOSTS; do i=$((i+1)); [ "$i" = "$choice" ] && SELECTED_HOST="$h"; done if [ -z "$SELECTED_HOST" ]; then warn "无效选择,使用最快项 $BEST_HOST" SELECTED_HOST="$BEST_HOST" fi fi BASE_URL="https://$SELECTED_HOST/api" ok "接入域: $SELECTED_HOST → ANTHROPIC_BASE_URL=$BASE_URL" # ---------- [2/5] API 密钥 ---------- API_KEY="$ARG_KEY" if [ -z "$API_KEY" ]; then if [ "$HAS_TTY" = 0 ]; then fail "无终端可交互且未提供 --key。非交互用法:" say " curl -fsSL https://qcode.cc/install/claude-code.sh | bash -s -- --key cr_xxx --yes" exit 2 fi info "[2/5] 请输入 QCode.cc API 密钥($DASHBOARD_URL 复制,输入不回显)" read_secret API_KEY "API 密钥: " fi if [ -z "$API_KEY" ]; then fail "API 密钥不能为空。"; exit 1; fi case "$API_KEY" in cr_*) ok "已读取密钥: cr_****${API_KEY#"${API_KEY%????}"}" ;; *) warn "密钥通常以 cr_ 开头,当前输入不是。" if ! confirm "仍然继续?[y/N]: " no; then exit 1; fi ;; esac # ---------- [3/5] 写配置(shell rc 托管块) ---------- detect_rc_file() { case "${SHELL##*/}" in zsh) printf '%s/.zshrc' "${ZDOTDIR:-$HOME}" ;; bash) if [ "$(uname -s 2>/dev/null)" = "Darwin" ]; then printf '%s/.bash_profile' "$HOME"; else printf '%s/.bashrc' "$HOME"; fi ;; *) printf '%s/.profile' "$HOME" ;; esac } RC_FILE="$(detect_rc_file)" MARK_BEGIN="# >>> qcode.cc claude-code >>>" MARK_END="# <<< qcode.cc claude-code <<<" escape_sq() { printf '%s' "$1" | sed "s/'/'\\\\''/g"; } info "[3/5] 写入配置 → $RC_FILE" touch "$RC_FILE" STAMP="$(date +%Y%m%d%H%M%S)" if [ -s "$RC_FILE" ]; then cp "$RC_FILE" "$RC_FILE.bak.$STAMP" ok "已备份原文件: $RC_FILE.bak.$STAMP" fi TMP_RC="$RC_FILE.qcode.tmp.$$" # 1) 删除旧托管块 2) 清理块外游离的同名 export(含其他脚本残留) awk -v b="$MARK_BEGIN" -v e="$MARK_END" '$0==b{inblk=1;next} $0==e{inblk=0;next} !inblk' "$RC_FILE" \ | sed -e '/^[[:space:]]*export[[:space:]]\{1,\}ANTHROPIC_BASE_URL=/d' \ -e '/^[[:space:]]*export[[:space:]]\{1,\}ANTHROPIC_AUTH_TOKEN=/d' \ -e '/^[[:space:]]*export[[:space:]]\{1,\}CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=/d' > "$TMP_RC" { printf '\n%s\n' "$MARK_BEGIN" printf '# 由 qcode.cc 一键配置脚本写入 %s(重跑脚本会原地更新本块)\n' "$STAMP" printf 'export ANTHROPIC_BASE_URL="%s"\n' "$BASE_URL" printf "export ANTHROPIC_AUTH_TOKEN='%s'\n" "$(escape_sq "$API_KEY")" printf 'export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1\n' printf '%s\n' "$MARK_END" } >> "$TMP_RC" mv "$TMP_RC" "$RC_FILE" ok "已写入托管块(ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN / DISABLE_NONESSENTIAL_TRAFFIC)" if grep -q '^[[:space:]]*export[[:space:]]\{1,\}ANTHROPIC_API_KEY=' "$RC_FILE" 2>/dev/null; then warn "检测到 $RC_FILE 里还有 ANTHROPIC_API_KEY,可能与本配置叠加,建议手动检查。" fi export ANTHROPIC_BASE_URL="$BASE_URL" export ANTHROPIC_AUTH_TOKEN="$API_KEY" export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 # ---------- [4/5] Node.js / Claude Code(免 sudo) ---------- node_major() { command -v node >/dev/null 2>&1 || { printf ''; return; } node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1 } load_nvm() { export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" # shellcheck disable=SC1091 [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" >/dev/null 2>&1 } ensure_node() { local major; major="$(node_major)" if [ -n "$major" ] && [ "$major" -ge "$MIN_NODE_MAJOR" ]; then ok "Node.js 已满足要求: $(node -v)" return 0 fi [ -n "$major" ] && warn "当前 Node.js $(node -v) 低于 v$MIN_NODE_MAJOR" || warn "未检测到 Node.js" if ! confirm "通过 nvm 安装 Node.js LTS(用户目录,无需 sudo)?[Y/n]: " yes; then warn "已跳过 Node 安装。请自行安装 Node.js $MIN_NODE_MAJOR+ 后运行: npm install -g $NPM_PKG" return 1 fi load_nvm if ! command -v nvm >/dev/null 2>&1; then info "正在安装 nvm($HOME/.nvm,不需要 sudo)..." if ! curl -fsSL "$NVM_INSTALL_URL" | bash; then warn "nvm 安装失败(常见于网络问题)。请手动安装 Node.js 后运行: npm install -g $NPM_PKG" return 1 fi load_nvm fi if ! command -v nvm >/dev/null 2>&1; then warn "nvm 未能加载,请重开终端后重跑本脚本。" return 1 fi info "正在安装 Node.js LTS..." nvm install --lts >/dev/null 2>&1 && nvm alias default 'lts/*' >/dev/null 2>&1 && nvm use default >/dev/null 2>&1 local m2; m2="$(node_major)" if [ -n "$m2" ] && [ "$m2" -ge "$MIN_NODE_MAJOR" ]; then ok "Node.js 安装完成: $(node -v)"; return 0; fi warn "Node.js 安装未成功,请手动安装后运行: npm install -g $NPM_PKG" return 1 } install_cli() { info "正在安装 $NPM_PKG ..." if npm install -g "$NPM_PKG"; then return 0; fi warn "npm -g 安装失败,尝试改用用户目录前缀(~/.npm-global,无需 sudo)..." mkdir -p "$HOME/.npm-global" if npm_config_prefix="$HOME/.npm-global" npm install -g "$NPM_PKG"; then case ":$PATH:" in *":$HOME/.npm-global/bin:"*) ;; *) PATH="$HOME/.npm-global/bin:$PATH" ;; esac if ! grep -q '.npm-global/bin' "$RC_FILE" 2>/dev/null; then printf '\nexport PATH="$HOME/.npm-global/bin:$PATH" # qcode.cc install\n' >> "$RC_FILE" fi return 0 fi return 1 } ensure_cli() { load_nvm if command -v "$CLI_CMD" >/dev/null 2>&1; then ok "Claude Code 已安装: $($CLI_CMD --version 2>/dev/null || command -v $CLI_CMD)" return 0 fi if [ "$NO_INSTALL" = 1 ]; then warn "--no-install:跳过安装。请自行安装后运行: npm install -g $NPM_PKG" return 1 fi if ! confirm "未检测到 Claude Code,使用 npm 安装?[Y/n]: " yes; then warn "已跳过安装。请自行安装后运行: npm install -g $NPM_PKG" return 1 fi ensure_node || return 1 install_cli || { warn "Claude Code 安装失败,配置已写好,安装成功后即可直接使用。"; return 1; } ok "Claude Code 安装完成" } info "[4/5] 检查运行环境" CLI_READY=0 ensure_cli && CLI_READY=1 # ---------- [5/5] 端到端验证 ---------- verify_api() { info "[5/5] 正在验证接入(发送一条最小测试请求,费用可忽略)..." local tmp code t_used tmp="$(mktemp 2>/dev/null || printf '/tmp/qcode-verify.%s' "$$")" code=$(curl -s -m 30 -o "$tmp" -w '%{http_code}' -X POST "$BASE_URL/v1/messages" \ -H "Authorization: Bearer $API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ --data '{"model":"claude-haiku-4-5","max_tokens":16,"messages":[{"role":"user","content":"ping"}]}' 2>/dev/null) case "$code" in 200) ok "接入验证通过!模型响应正常。" say " ▸ 用量与请求明细可在 $PROBE_URL 输入你的密钥查看" return 0 ;; 401|403) fail "密钥无效或已过期(HTTP $code)。" say " ▸ 到 $DASHBOARD_URL 核对密钥(确认完整复制、cr_ 前缀在内)" say " ▸ 修正后重跑本脚本即可" ;; 400) if grep -q 'not a supported model' "$tmp" 2>/dev/null; then fail "模型不在白名单(HTTP 400)。请重跑脚本或查看 $DOCS_URL" else fail "请求被拒绝(HTTP 400): $(head -c 200 "$tmp" 2>/dev/null)" fi ;; 404) fail "接入地址错误(HTTP 404)。BASE_URL 应为 https://{域}/api 且末尾不带斜杠。" say " ▸ 当前写入: $BASE_URL(如仍失败请携带此信息联系支持)" ;; 429) ok "配置正确(HTTP 429 = 已到达服务但被限流),稍后即可正常使用。" return 0 ;; *) fail "网络或节点异常(HTTP ${code:-无响应})。" say " ▸ 建议重跑脚本换一个接入域(如 --domain asia),或稍后再试" say " ▸ 各域状态可在 $PROBE_URL 查看" ;; esac rm -f "$tmp" 2>/dev/null return 1 } VERIFY_OK=1 if [ "$NO_VERIFY" = 1 ]; then warn "--no-verify:已跳过验证。" else verify_api || VERIFY_OK=0 fi # ---------- 收尾 ---------- say "" say "────────────────────────────────────────" ok "配置完成!摘要:" say " 接入域 $SELECTED_HOST" say " 配置文件 $RC_FILE(托管块,可重跑覆盖)" say " 环境变量 ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN" if [ "$VERIFY_OK" = 1 ] && [ "$NO_VERIFY" = 0 ]; then say " 连通性 ✓ 已验证"; fi say "" say " 新开的终端会自动生效;当前已打开的其他终端请执行: source $RC_FILE" say " 文档与排错: $DOCS_URL" say "────────────────────────────────────────" if [ "$CLI_READY" = 1 ] && [ "$ASSUME_YES" = 0 ] && [ "$HAS_TTY" = 1 ]; then if confirm "是否立即启动 Claude Code?[y/N]: " no; then say "" exec