#!/usr/bin/env bash # ============================================================== # aiproxynode setup script (standalone, generated by build.py) # ============================================================== # _common.sh — aiproxynode setup scripts shared library (macOS) # Will be inlined into each setup-*.sh at build time. set -eo pipefail AIPROXY_BASE='https://aiproxynode.com' BACKUP_SUFFIX=$(date +%Y%m%d) # ANSI 颜色 C_RESET='' C_RED='' C_GREEN='' C_YELLOW='' C_CYAN='' C_GRAY='' print_header() { local scenario="$1" printf ' ' printf "${C_CYAN}========================================${C_RESET} " printf "${C_CYAN} aiproxynode 客户端配置脚本${C_RESET} " printf "${C_CYAN} 场景: %s${C_RESET} " "$scenario" printf "${C_CYAN} 平台: macOS (bash)${C_RESET} " printf "${C_CYAN}========================================${C_RESET} " } read_token() { printf "${C_YELLOW}[1/4] 请输入您的 API Token (sk-* 格式)${C_RESET} " printf '> ' # 静默读入(不回显);Ctrl-C 也要恢复 echo,否则终端瞎掉 local _old_stty _old_stty=$(stty -g 2>/dev/null) trap 'stty "$_old_stty" 2>/dev/null; trap - INT TERM EXIT' INT TERM EXIT stty -echo 2>/dev/null read -r TOKEN stty "$_old_stty" 2>/dev/null trap - INT TERM EXIT printf ' ' TOKEN=$(printf '%s' "$TOKEN" | tr -d '[:space:]') if [ -z "$TOKEN" ]; then printf "${C_RED}错误: Token 不能为空${C_RESET} " exit 2 fi case "$TOKEN" in sk-*) ;; *) printf "${C_RED}错误: Token 必须以 sk- 开头${C_RESET} " exit 2 ;; esac } mask_token() { local t="$1" local len=${#t} if [ "$len" -le 10 ]; then printf 'sk-***' else printf '%s***%s' "${t:0:6}" "${t: -4}" fi } backup_file() { local path="$1" if [ -f "$path" ]; then local bak="${path}.bak.${BACKUP_SUFFIX}" cp "$path" "$bak" printf "${C_GRAY} 已备份: %s${C_RESET} " "$bak" fi } # merge_json # json_patch 是一个 jq 兼容的对象字面量,例:'{"a":1,"b":"c"}' merge_json() { local path="$1" local patch="$2" if ! command -v jq >/dev/null 2>&1; then printf "${C_RED}错误: macOS 没装 jq。请先运行: brew install jq${C_RESET} " exit 3 fi local dir dir=$(dirname "$path") [ -d "$dir" ] || mkdir -p "$dir" local existing='{}' if [ -f "$path" ] && [ -s "$path" ]; then if ! jq empty "$path" 2>/dev/null; then printf "${C_RED}错误: 现有 JSON 文件解析失败,未写入: %s${C_RESET} " "$path" printf "${C_YELLOW}原文件未动。手工写入以下字段:${C_RESET} %s " "$patch" exit 3 fi existing=$(cat "$path") fi local tmp tmp=$(mktemp) printf '%s' "$existing" | jq --argjson p "$patch" '. + $p' > "$tmp" mv "$tmp" "$path" } # detect_rc_file → 输出 rc 文件路径 detect_rc_file() { local shell_name shell_name=$(basename "${SHELL:-/bin/zsh}") case "$shell_name" in zsh) printf '%s/.zshrc' "$HOME" ;; bash) printf '%s/.bash_profile' "$HOME" ;; *) printf '%s/.profile' "$HOME" printf "${C_YELLOW} 警告: 检测到非 zsh/bash shell ($shell_name),写入 ~/.profile 兼容运行${C_RESET} " >&2 ;; esac } # write_env_fenced ... write_env_fenced() { local rc="$1"; shift [ -f "$rc" ] || touch "$rc" # 删除旧围栏 if grep -q '# >>> aiproxynode >>>' "$rc"; then local tmp tmp=$(mktemp) awk ' /# >>> aiproxynode >>>/ { skip=1; next } /# <<< aiproxynode << "$tmp" mv "$tmp" "$rc" fi { printf ' # >>> aiproxynode >>> ' for kv in "$@"; do printf 'export %s ' "$kv" printf "${C_GRAY} shell env: %s${C_RESET} " "$kv" >&2 done printf '# <<< aiproxynode <<< ' } >> "$rc" } # test_endpoint → 0=pass, 1=fail test_endpoint() { local token="$1" local model="$2" printf "${C_YELLOW}[3/4] 验证连通性 (POST /v1/chat/completions)...${C_RESET} " local resp http_code body tmpfile tmpfile=$(mktemp /tmp/aiproxy_resp.XXXXXX) resp=$(curl -sS -o "$tmpfile" -w '%{http_code}' \ -X POST "$AIPROXY_BASE/v1/chat/completions" \ -H "Authorization: Bearer $token" \ -H 'Content-Type: application/json' \ -d "{\"model\":\"$model\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":16}" \ 2>&1) || true http_code="$resp" body=$(cat "$tmpfile" 2>/dev/null || echo '') rm -f "$tmpfile" if [ "$http_code" != "200" ]; then printf "${C_RED} HTTP %s ✗${C_RESET} " "$http_code" printf "${C_RED} 响应: %s${C_RESET} " "$body" return 1 fi printf "${C_GREEN} HTTP 200 ✓${C_RESET} " if command -v jq >/dev/null 2>&1; then local content content=$(printf '%s' "$body" | jq -r '.choices[0].message.content // ""') if [ -z "$content" ]; then printf "${C_RED} content 为空 ✗${C_RESET} " return 1 fi local preview="${content:0:40}" [ ${#content} -gt 40 ] && preview="${preview}..." printf "${C_GREEN} content 非空 ✓ (返回: \"%s\")${C_RESET} " "$preview" else if printf '%s' "$body" | grep -q '"content"[[:space:]]*:[[:space:]]*"[^"]'; then printf "${C_GREEN} content 非空 ✓${C_RESET} " else printf "${C_RED} content 似乎为空 ✗${C_RESET} " return 1 fi fi return 0 } print_failure_hints() { printf ' ' printf "${C_RED}验证失败,可能的原因:${C_RESET} " printf "${C_YELLOW} □ Token 输入错误 → 重新运行脚本输入${C_RESET} " printf "${C_YELLOW} □ Token 已在 aiproxynode 后台删除 → 重新创建${C_RESET} " printf "${C_YELLOW} □ Token 绑的分组不对 → Claude 场景需要 kiro 分组,GPT 场景需要 gptpuls 分组${C_RESET} " printf " ${C_YELLOW}配置已写入,但未生效。请处理后重新运行脚本。${C_RESET} " } print_success() { printf " ${C_GREEN}[4/4] 完成 ✓${C_RESET} " printf "${C_CYAN}下一步:${C_RESET} " local i=1 for step in "$@"; do printf ' %d) %s ' "$i" "$step" i=$((i+1)) done printf " ${C_CYAN}如果遇到问题:${C_RESET} " printf ' - "no available channels" → 检查 token 绑的分组 ' printf ' - "invalid api key" → token 可能填错或已删除 ' printf ' - 详见 https://docs.aiproxynode.com/api.html ' } print_header 'Cursor + Claude' read_token printf "${C_YELLOW}[2/4] 写入配置...${C_RESET}\n" MODEL='claude-opus-4-7' SETTINGS="$HOME/Library/Application Support/Cursor/User/settings.json" backup_file "$SETTINGS" PATCH=$(printf '{"cursor.openai.baseUrl":"%s/v1","cursor.openai.apiKey":"%s"}' \ "$AIPROXY_BASE" "$TOKEN") merge_json "$SETTINGS" "$PATCH" printf "${C_GRAY} 配置文件: %s${C_RESET}\n" "$SETTINGS" printf "${C_GRAY}[3/4] 跳过在线连通性验证(按用户配置)${C_RESET}\n" print_success \ 'Cursor → Settings → Models 里手动添加自定义 model: claude-opus-4-7' \ 'Chat 中切换到 claude-opus-4-7 即可使用' \ 'Cursor Free 不支持 named models,必须 Pro 订阅;Free 用户建议改用 setup-vscode-codex'