#!/usr/bin/env bash ################################################################################ # 闪电API - Pi (pi coding agent) 一键安装脚本 # # 功能: 安装 Pi + 配置 闪电API (Claude + GPT 两个供应商) # 官网: flashapi.top # 模型调用: ai.flashapi.top (国内直连) / cdn.flashapi.top (海外CDN) # # 用法: curl -fsSL https://sh.flashapi.top/pi.sh | bash # # 不需要 sudo,所有操作在用户目录下完成 ################################################################################ set -euo pipefail export LANG=en_US.UTF-8 export LC_ALL=en_US.UTF-8 ################################################################################ # 常量定义 ################################################################################ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' WHITE='\033[1;37m' GRAY='\033[0;90m' BOLD='\033[1m' BLUE='\033[38;5;33m' BRIGHT_YELLOW='\033[38;5;226m' NC='\033[0m' OS_TYPE="" IS_WSL=false NPM_MIRROR="https://registry.npmmirror.com" PI_PACKAGE="@earendil-works/pi-coding-agent" PI_NODE_MIN_MAJOR=22 PI_NODE_MIN_MINOR=19 # Pi 配置目录: $PI_CODING_AGENT_DIR,默认 ~/.pi/agent (各平台一致) PI_AGENT_DIR="${PI_CODING_AGENT_DIR:-}" case "$PI_AGENT_DIR" in "") PI_AGENT_DIR="$HOME/.pi/agent" ;; "~") PI_AGENT_DIR="$HOME" ;; "~/"*) PI_AGENT_DIR="$HOME/${PI_AGENT_DIR#\~/}" ;; esac PI_MODELS_FILE="$PI_AGENT_DIR/models.json" PI_SETTINGS_FILE="$PI_AGENT_DIR/settings.json" PROVIDER_CLAUDE="flashapi-claude" PROVIDER_GPT="flashapi-gpt" DEFAULT_CLAUDE_MODEL="claude-opus-5-5" DEFAULT_GPT_MODEL="gpt-6.1-sol" DEFAULT_THINKING="medium" # 同时配置 Claude + GPT 时默认使用哪一家 (claude / gpt) DEFAULT_FAMILY_WHEN_BOTH="claude" FETCHED_IDS="" ################################################################################ # 工具函数 ################################################################################ print_banner() { echo "" echo -e " ${BOLD}${BRIGHT_YELLOW} ⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡${NC}" echo -e " ${BOLD}${BRIGHT_YELLOW} ██████╗ ██╗${NC}" echo -e " ${BOLD}${BRIGHT_YELLOW} ██╔══██╗██║${NC}" echo -e " ${BOLD}${BRIGHT_YELLOW} ██████╔╝██║${NC}" echo -e " ${BOLD}${BRIGHT_YELLOW} ██╔═══╝ ██║${NC}" echo -e " ${BOLD}${BRIGHT_YELLOW} ██║ ██║${NC}" echo -e " ${BOLD}${BRIGHT_YELLOW} ╚═╝ ╚═╝${NC}" echo -e " ${BOLD}${BLUE} ⚡ 闪电API × Pi ⚡${NC}" echo -e " ${BOLD}${BRIGHT_YELLOW} ⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡${NC}" echo "" echo -e " ${GRAY}───────────────────────────────────────────────────────${NC}" echo -e " ${WHITE}⚡ 闪电API${NC} ${GRAY}|${NC} ${CYAN}官网${NC} ${BOLD}${BRIGHT_YELLOW}flashapi.top${NC}" echo -e " ${WHITE}Pi Coding Agent 一键配置${NC} ${GRAY}|${NC} ${CYAN}让顶尖模型触手可及${NC}" echo -e " ${GRAY}───────────────────────────────────────────────────────${NC}" echo -e " ${GRAY}国内直连:${NC} ${GREEN}ai.flashapi.top${NC} ${GRAY}海外CDN:${NC} ${GREEN}cdn.flashapi.top${NC}" echo -e " ${GRAY}───────────────────────────────────────────────────────${NC}" echo "" } print_status() { local type="$1" local message="$2" case "$type" in ok) echo -e " ${GREEN}[⚡]${NC} $message" ;; info) echo -e " ${CYAN}[*]${NC} $message" ;; warn) echo -e " ${YELLOW}[!]${NC} $message" ;; error) echo -e " ${RED}[x]${NC} $message" ;; *) echo " $message" ;; esac } detect_os() { local uname_s uname_s="$(uname -s)" IS_WSL=false case "$uname_s" in Darwin) OS_TYPE="macos" ;; Linux) OS_TYPE="linux" if grep -qiE "(microsoft|wsl)" /proc/version 2>/dev/null; then IS_WSL=true fi ;; MINGW*|MSYS*|CYGWIN*) OS_TYPE="windows" ;; *) OS_TYPE="unknown" print_status "warn" "未知操作系统: ${uname_s},将尝试继续" ;; esac } # 读取密码输入 (bash/zsh 兼容,带 * 遮盖) read_secret() { local prompt="$1" local varname="$2" local input="" local char echo -en "$prompt" >&2 while true; do if [[ -n "${ZSH_VERSION:-}" ]]; then IFS= read -r -s -k1 char < /dev/tty else IFS= read -r -s -n1 char < /dev/tty fi if [[ $char == $'\n' ]] || [[ $char == $'\r' ]] || [[ -z "$char" ]]; then break elif [[ $char == $'\177' ]] || [[ $char == $'\b' ]]; then if [[ -n "$input" ]]; then input="${input%?}" echo -en "\b \b" >&2 fi else input+="$char" echo -n "*" >&2 fi done echo "" >&2 eval "$varname=\$input" } mask_key() { local value="$1" if [[ -z "$value" ]]; then echo "" return fi if [[ ${#value} -le 12 ]]; then echo "$value" return fi echo "${value:0:8}...${value: -4}" } backup_if_exists() { local file="$1" if [[ -f "$file" ]]; then local stamp backup stamp="$(date +%Y%m%d%H%M%S)" backup="${file}.bak.${stamp}" cp "$file" "$backup" chmod 600 "$backup" 2>/dev/null || true print_status "info" "已备份现有文件: $backup" fi } ask_yes_no() { local prompt="$1" local answer echo -en " ${YELLOW}${prompt} (y/N): ${NC}" >&2 read -r answer < /dev/tty [[ "$answer" == "y" || "$answer" == "Y" ]] } ################################################################################ # 功能 1: 安装 Pi ################################################################################ # 把 npm 全局 bin 目录加入当前进程 PATH (刚装完时 PATH 可能还没生效) add_npm_bin_to_path() { command -v npm >/dev/null 2>&1 || return 0 local prefix prefix="$(npm prefix -g 2>/dev/null || true)" [[ -z "$prefix" ]] && return 0 if [[ "$OS_TYPE" == "windows" ]]; then export PATH="$prefix:$PATH" else export PATH="$prefix/bin:$PATH" fi } check_pi_installed() { if command -v pi >/dev/null 2>&1; then local v v="$(pi --version 2>/dev/null | head -n 1 || true)" [[ -z "$v" ]] && v="unknown" echo "$v" return 0 fi return 1 } install_with_mirror() { print_status "info" "切换国内镜像 (npmmirror.com) 安装 ..." npm install -g --ignore-scripts "${PI_PACKAGE}@latest" --registry="$NPM_MIRROR" } install_pi() { echo "" print_status "info" "⚡ 正在安装 Pi ..." echo "" [[ "$IS_WSL" == "true" ]] && print_status "info" "检测到 WSL 环境" [[ "$OS_TYPE" == "windows" ]] && print_status "info" "检测到 Windows (Git Bash) 环境" local ver if ver="$(check_pi_installed)"; then print_status "ok" "Pi 已安装 ($ver)" echo "" if ! ask_yes_no "是否重新安装/升级到最新版?"; then print_status "info" "跳过安装" return 0 fi fi # 检查 npm if ! command -v npm >/dev/null 2>&1 || ! command -v node >/dev/null 2>&1; then print_status "error" "未找到 Node.js / npm,请先安装 Node.js (需要 v${PI_NODE_MIN_MAJOR}.${PI_NODE_MIN_MINOR}+)" echo "" echo -e " ${WHITE}安装方式:${NC}" echo -e " ${CYAN}官网下载:${NC} https://nodejs.org/" echo -e " ${CYAN}macOS:${NC} brew install node" echo -e " ${CYAN}Ubuntu:${NC} 参考 https://github.com/nodesource/distributions 安装 Node.js 22+" return 1 fi # 检查 Node 版本 (Pi 要求 >= 22.19.0) local node_ver node_major node_minor node_ver="$(node -v 2>/dev/null | sed 's/^v//')" node_major="$(echo "$node_ver" | cut -d. -f1)" node_minor="$(echo "$node_ver" | cut -d. -f2)" if [[ "$node_major" =~ ^[0-9]+$ ]] && [[ "$node_minor" =~ ^[0-9]+$ ]]; then if [[ "$node_major" -lt "$PI_NODE_MIN_MAJOR" ]] || \ { [[ "$node_major" -eq "$PI_NODE_MIN_MAJOR" ]] && [[ "$node_minor" -lt "$PI_NODE_MIN_MINOR" ]]; }; then print_status "warn" "当前 Node.js 版本 v${node_ver},Pi 需要 v${PI_NODE_MIN_MAJOR}.${PI_NODE_MIN_MINOR}+" print_status "info" "建议升级: https://nodejs.org/" echo "" if ! ask_yes_no "是否仍然继续安装?"; then print_status "info" "已取消" return 1 fi fi fi print_status "ok" "npm 已就绪 ($(npm --version 2>/dev/null)), Node $(node -v 2>/dev/null)" print_status "info" "使用 npm 安装 ${PI_PACKAGE} ..." echo "" if ! npm install -g --ignore-scripts "${PI_PACKAGE}@latest"; then print_status "warn" "默认源安装失败,可能是网络问题" if ! install_with_mirror; then print_status "error" "所有安装方式均失败,请检查网络连接" print_status "info" "如果提示 EACCES 权限错误,请参考 https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally" return 1 fi fi echo "" add_npm_bin_to_path hash -r 2>/dev/null || true if ver="$(check_pi_installed)"; then print_status "ok" "⚡ Pi 安装成功 ($ver)" else print_status "warn" "安装已完成,但未在 PATH 中找到 pi" print_status "info" "请重新打开终端后再试" fi } ################################################################################ # 功能 2: 配置 API ################################################################################ require_node() { if ! command -v node >/dev/null 2>&1; then print_status "error" "未找到 Node.js,无法写入 Pi 配置 (Pi 本身也依赖 Node.js)" print_status "info" "请先执行菜单 1 安装 Pi,或从 https://nodejs.org/ 安装 Node.js 22+" return 1 fi } # 读取我们管理的 provider 字段: read_provider_field read_provider_field() { [[ -f "$PI_MODELS_FILE" ]] || return 1 command -v node >/dev/null 2>&1 || return 1 MODELS_FILE="$PI_MODELS_FILE" PROVIDER="$1" FIELD="$2" node <<'NODE' 2>/dev/null const fs = require("fs"); const strip = (s) => s .replace(/^\uFEFF/, "") .replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*/g, (m) => (m[0] === '"' ? m : "")) .replace(/"(?:\\.|[^"\\])*"|,(\s*[}\]])/g, (m, tail) => tail ?? (m[0] === '"' ? m : "")); try { const data = JSON.parse(strip(fs.readFileSync(process.env.MODELS_FILE, "utf8"))); const p = data && data.providers && data.providers[process.env.PROVIDER]; const v = p && p[process.env.FIELD]; if (typeof v === "string" && v) { process.stdout.write(v); process.exit(0); } } catch {} process.exit(1); NODE } read_settings_field() { [[ -f "$PI_SETTINGS_FILE" ]] || return 1 command -v node >/dev/null 2>&1 || return 1 SETTINGS_FILE="$PI_SETTINGS_FILE" FIELD="$1" node <<'NODE' 2>/dev/null const fs = require("fs"); try { const data = JSON.parse(fs.readFileSync(process.env.SETTINGS_FILE, "utf8").replace(/^\uFEFF/, "")); const v = data[process.env.FIELD]; if (typeof v === "string" && v) { process.stdout.write(v); process.exit(0); } } catch {} process.exit(1); NODE } # 从 /v1/models 响应体中提取模型 ID (换行分隔) parse_model_ids() { BODY_FILE="$1" node <<'NODE' 2>/dev/null const fs = require("fs"); try { const j = JSON.parse(fs.readFileSync(process.env.BODY_FILE, "utf8")); const list = Array.isArray(j.data) ? j.data : (Array.isArray(j) ? j : []); const ids = list.map((m) => (m && typeof m.id === "string" ? m.id : "")).filter(Boolean); process.stdout.write(ids.join("\n")); } catch {} NODE } # 用 /v1/models 验证 Key,结果写入 FETCHED_IDS (换行分隔) # 返回: 0=200 成功 1=401 2=网络错误 3=其他状态码 fetch_models() { local base_url="$1" local api_key="$2" local body_file http_code FETCHED_IDS="" body_file="$(mktemp)" http_code="$(curl -s -o "$body_file" -w '%{http_code}' \ --max-time 15 \ -H "Authorization: Bearer ${api_key}" \ "${base_url}/v1/models" 2>/dev/null || true)" [[ -z "$http_code" ]] && http_code="000" if [[ "$http_code" == "200" ]]; then FETCHED_IDS="$(parse_model_ids "$body_file" || true)" rm -f "$body_file" return 0 fi rm -f "$body_file" case "$http_code" in 401) return 1 ;; 000) return 2 ;; *) FETCHED_IDS="$http_code"; return 3 ;; esac } # 过滤某家族的模型 ID: filter_family_ids (输入 FETCHED_IDS) filter_family_ids() { local family="$1" local id out="" while IFS= read -r id; do [[ -z "$id" ]] && continue case "$family" in claude) [[ "$id" == claude-* ]] || continue ;; gpt) [[ "$id" == gpt-* ]] || continue [[ "$id" == *compact* ]] && continue ;; esac out="${out}${id}," done < <结果变量名> # 成功时把该家族模型 ID (逗号分隔) 写入结果变量;返回 1 表示用户取消 validate_key() { local family="$1" local base_url="$2" local api_key="$3" local result_var="$4" local label="Claude" [[ "$family" == "gpt" ]] && label="GPT" print_status "info" "正在验证 ${label} 分组 API Key ..." local rc=0 fetch_models "$base_url" "$api_key" || rc=$? local ids="" case "$rc" in 0) ids="$(filter_family_ids "$family")" if [[ -n "$ids" ]]; then local count count="$(echo "$ids" | tr ',' '\n' | grep -c . || true)" print_status "ok" "⚡ ${label} Key 验证通过,获取到 ${count} 个 ${label} 模型" else print_status "warn" "Key 有效,但模型列表里没有 ${label} 模型 —— 这个 Key 可能不是 ${label} 分组" print_status "info" "请到 flashapi.top 确认该令牌所属分组,将只写入默认模型" fi ;; 1) print_status "error" "${label} API Key 无效 (401 Unauthorized)" if ! ask_yes_no "验证未通过,是否仍要保存?"; then print_status "info" "已取消配置" return 1 fi ;; 2) print_status "warn" "无法连接到 ${base_url} (网络错误)" if ! ask_yes_no "验证未通过,是否仍要保存?"; then print_status "info" "已取消配置" return 1 fi ;; *) print_status "warn" "API 返回状态码: ${FETCHED_IDS} (可能仍可用),将只写入默认模型" ;; esac eval "$result_var=\$ids" return 0 } # 合并写入 models.json + settings.json (node 处理 JSON) write_pi_config() { local base_url="$1" local claude_key="$2" local claude_ids="$3" local gpt_key="$4" local gpt_ids="$5" local default_provider="$6" local default_model="$7" mkdir -p "$PI_AGENT_DIR" backup_if_exists "$PI_MODELS_FILE" backup_if_exists "$PI_SETTINGS_FILE" MODELS_FILE="$PI_MODELS_FILE" SETTINGS_FILE="$PI_SETTINGS_FILE" \ BASE_URL="$base_url" \ CLAUDE_KEY="$claude_key" CLAUDE_IDS="$claude_ids" \ GPT_KEY="$gpt_key" GPT_IDS="$gpt_ids" \ PROVIDER_CLAUDE="$PROVIDER_CLAUDE" PROVIDER_GPT="$PROVIDER_GPT" \ DEFAULT_CLAUDE_MODEL="$DEFAULT_CLAUDE_MODEL" DEFAULT_GPT_MODEL="$DEFAULT_GPT_MODEL" \ DEFAULT_PROVIDER="$default_provider" DEFAULT_MODEL="$default_model" \ DEFAULT_THINKING="$DEFAULT_THINKING" \ node <<'NODE' const fs = require("fs"); const path = require("path"); const env = process.env; // 与 Pi 自身 (dist/utils/json.js) 一致的容错: 去掉 // 注释和尾逗号 const strip = (s) => s .replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*/g, (m) => (m[0] === '"' ? m : "")) .replace(/"(?:\\.|[^"\\])*"|,(\s*[}\]])/g, (m, tail) => tail ?? (m[0] === '"' ? m : "")); function readJson(file, tolerant) { if (!fs.existsSync(file)) return {}; const raw = fs.readFileSync(file, "utf8").replace(/^\uFEFF/, ""); if (!raw.trim()) return {}; try { return JSON.parse(raw); } catch (e) { if (!tolerant) throw new Error(`${file} 不是合法 JSON: ${e.message}`); } const data = JSON.parse(strip(raw)); console.error(`WARN_COMMENTS:${file}`); return data; } function writeJson(file, data) { const tmp = `${file}.tmp-${process.pid}`; fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, { encoding: "utf8", mode: 0o600 }); fs.renameSync(tmp, file); try { fs.chmodSync(file, 0o600); } catch {} } // Pi 会把 apiKey 中的 $NAME / ${NAME} 当环境变量,开头的 ! 当命令,这里做转义 function escapeConfigValue(v) { let s = v.replace(/\$/g, "$$$$"); if (s.startsWith("!")) s = "$" + s; return s; } // ── 模型元数据 ── const BIG_CLAUDE = ["claude-opus-5-5", "claude-sonnet-5-5", "claude-fable-5-1", "claude-fable-5", "claude-opus-5", "claude-sonnet-5", "claude-opus-4-8"]; const BIG_GPT = ["gpt-6.1-sol", "gpt-6-sol", "gpt-6-astra", "gpt-6-luna"]; // 思考档位映射: 取自 Pi 1.1.0 内置模型目录 (pi-ai providers/data/*.json) const L = { off: null, minimal: null, low: "low", medium: "medium", high: "high", xhigh: "xhigh", max: "max" }; const CLAUDE_THINKING = { "claude-opus-5-5": L, "claude-sonnet-5-5": L, "claude-haiku-5-5": L, "claude-fable-5-1": { off: null, xhigh: "xhigh", max: "max" }, "claude-fable-5": { off: null, xhigh: "xhigh", max: "max" }, "claude-opus-5": { off: null, xhigh: "xhigh", max: "max" }, "claude-opus-4-8": { xhigh: "xhigh", max: "max" }, "claude-opus-4-7": { xhigh: "xhigh", max: "max" }, "claude-sonnet-5": { xhigh: "xhigh", max: "max" }, "claude-opus-4-6": { max: "max" }, "claude-sonnet-4-6": { max: "max" }, }; const GPT56 = { off: "none", minimal: null, low: "low", medium: "medium", high: "high", xhigh: "xhigh", max: "max" }; const GPT_THINKING = { "gpt-6.1-sol": L, "gpt-6-astra": L, "gpt-6-sol": GPT56, "gpt-6-luna": GPT56, "gpt-5.6-sol": GPT56, "gpt-5.6-luna": GPT56, "gpt-5.6-terra": GPT56, "gpt-5.5": { off: "none", minimal: null, low: "low", medium: "medium", high: "high", xhigh: "xhigh", max: null }, }; function claudeThinking(id) { if (CLAUDE_THINKING[id]) return CLAUDE_THINKING[id]; // 未知的新一代 Claude (版本 >= 5) 按自适应思考处理 const m = /^claude-[a-z]+-(\d+)/.exec(id); if (m && Number(m[1]) >= 5 && Number(m[1]) < 100) return { off: null, xhigh: "xhigh", max: "max" }; return null; } function gptThinking(id) { if (GPT_THINKING[id]) return GPT_THINKING[id]; if (/^gpt-(6|[7-9])/.test(id)) return L; if (/^gpt-5\.6/.test(id)) return GPT56; return null; } function prettyName(id) { const parts = id.split("-"); if (parts[0] === "gpt") { // gpt-6.1-sol -> GPT-6.1 Sol const rest = parts.slice(2).map((p) => p.charAt(0).toUpperCase() + p.slice(1)); return [`GPT-${parts[1] || ""}`].concat(rest).join(" ").trim(); } const words = []; let nums = []; const flush = () => { if (nums.length) { words.push(nums.join(".")); nums = []; } }; for (const p of parts) { if (/^\d{1,2}$/.test(p)) { nums.push(p); continue; } flush(); if (/^\d{8}$/.test(p)) words.push(`(${p})`); else words.push(p.charAt(0).toUpperCase() + p.slice(1)); } flush(); return words.join(" "); } function buildModel(family, id) { const big = family === "claude" ? BIG_CLAUDE.includes(id) : BIG_GPT.includes(id); const model = { id, name: prettyName(id), reasoning: true, input: ["text", "image"], contextWindow: family === "claude" ? (big ? 1000000 : 200000) : (big ? 1050000 : 272000), maxTokens: family === "claude" ? (big ? 128000 : 64000) : 128000, }; const map = family === "claude" ? claudeThinking(id) : gptThinking(id); if (map) model.thinkingLevelMap = { ...map }; if (family === "claude" && map) model.compat = { forceAdaptiveThinking: true }; return model; } function buildModels(family, idsCsv, defaultId) { const ids = [defaultId]; for (const id of (idsCsv || "").split(",")) { const t = id.trim(); if (t && !ids.includes(t)) ids.push(t); } return ids.map((id) => buildModel(family, id)); } try { // ── models.json ── const models = readJson(env.MODELS_FILE, true); if (typeof models !== "object" || models === null || Array.isArray(models)) throw new Error("models.json 根节点不是对象"); if (typeof models.providers !== "object" || models.providers === null || Array.isArray(models.providers)) models.providers = {}; if (env.CLAUDE_KEY) { models.providers[env.PROVIDER_CLAUDE] = { name: "闪电API Claude", baseUrl: env.BASE_URL, api: "anthropic-messages", apiKey: escapeConfigValue(env.CLAUDE_KEY), models: buildModels("claude", env.CLAUDE_IDS, env.DEFAULT_CLAUDE_MODEL), }; } if (env.GPT_KEY) { models.providers[env.PROVIDER_GPT] = { name: "闪电API GPT", baseUrl: `${env.BASE_URL}/v1`, api: "openai-responses", apiKey: escapeConfigValue(env.GPT_KEY), models: buildModels("gpt", env.GPT_IDS, env.DEFAULT_GPT_MODEL), }; } // ── settings.json (Pi 用严格 JSON 解析) ── const settings = readJson(env.SETTINGS_FILE, false); if (typeof settings !== "object" || settings === null || Array.isArray(settings)) throw new Error("settings.json 根节点不是对象"); settings.defaultProvider = env.DEFAULT_PROVIDER; settings.defaultModel = env.DEFAULT_MODEL; settings.defaultThinkingLevel = env.DEFAULT_THINKING; // 用户若设置了 enabledModels (启动/切换范围),Pi 只会在其中选默认模型; // 追加我们的供应商通配,否则默认模型不会生效 if (Array.isArray(settings.enabledModels) && settings.enabledModels.length > 0) { const add = []; if (env.CLAUDE_KEY) add.push(`${env.PROVIDER_CLAUDE}/*`); if (env.GPT_KEY) add.push(`${env.PROVIDER_GPT}/*`); for (const p of add) { if (!settings.enabledModels.includes(p)) { settings.enabledModels.push(p); console.error(`WARN_ENABLED:${p}`); } } } fs.mkdirSync(path.dirname(env.MODELS_FILE), { recursive: true }); writeJson(env.MODELS_FILE, models); writeJson(env.SETTINGS_FILE, settings); } catch (e) { console.error(`ERROR:${e.message}`); process.exit(1); } NODE } choose_families() { echo -e " ${WHITE}请选择要配置的模型家族:${NC}" echo "" echo -e " ${BRIGHT_YELLOW}⚡${NC} ${CYAN}1)${NC} Claude + GPT ${GRAY}(回车默认)${NC}" echo -e " ${BRIGHT_YELLOW}⚡${NC} ${CYAN}2)${NC} 仅 Claude" echo -e " ${BRIGHT_YELLOW}⚡${NC} ${CYAN}3)${NC} 仅 GPT" echo "" echo -en " ${WHITE}请输入选项 [回车=1]: ${NC}" >&2 local choice read -r choice < /dev/tty case "$choice" in ""|1) DO_CLAUDE=true; DO_GPT=true; FAMILY_LABEL="Claude + GPT" ;; 2) DO_CLAUDE=true; DO_GPT=false; FAMILY_LABEL="仅 Claude" ;; 3) DO_CLAUDE=false; DO_GPT=true; FAMILY_LABEL="仅 GPT" ;; *) print_status "error" "无效选项,已取消"; return 1 ;; esac echo "" print_status "ok" "已选择: ${FAMILY_LABEL}" } configure_api() { echo "" print_status "info" "⚡ 配置 闪电API (Claude + GPT)" echo "" require_node || return 1 local existing existing="$(read_provider_field "$PROVIDER_CLAUDE" apiKey || true)" [[ -n "$existing" ]] && print_status "info" "当前 Claude Key: $(mask_key "$existing")" existing="$(read_provider_field "$PROVIDER_GPT" apiKey || true)" [[ -n "$existing" ]] && print_status "info" "当前 GPT Key: $(mask_key "$existing")" existing="$(read_provider_field "$PROVIDER_CLAUDE" baseUrl || read_provider_field "$PROVIDER_GPT" baseUrl || true)" [[ -n "$existing" ]] && print_status "info" "当前 BASE URL: ${existing}" echo "" DO_CLAUDE=false DO_GPT=false FAMILY_LABEL="" choose_families || return 1 echo "" # ── 选择线路 ── echo -e " ${WHITE}请选择 API 线路:${NC}" echo "" echo -e " ${BRIGHT_YELLOW}⚡${NC} ${CYAN}1)${NC} ${GREEN}国内直连${NC} - https://ai.flashapi.top" echo -e " ${GRAY}服务器位于香港,国内可直连,无需翻墙${NC}" echo "" echo -e " ${BRIGHT_YELLOW}⚡${NC} ${CYAN}2)${NC} ${GREEN}海外CDN${NC} - https://cdn.flashapi.top" echo -e " ${GRAY}全球CDN加速,适合海外用户${NC}" echo "" echo -en " ${WHITE}请输入选项 (1/2): ${NC}" >&2 local choice read -r choice < /dev/tty local base_url case "$choice" in 1) base_url="https://ai.flashapi.top" ;; 2) base_url="https://cdn.flashapi.top" ;; *) print_status "error" "无效选项,已取消"; return 1 ;; esac echo "" print_status "info" "已选择: ${base_url}" echo "" # ── 令牌分组提醒 ── echo -e " ${YELLOW}╔══════════════════════════════════════════════════════╗${NC}" echo -e " ${YELLOW}║${NC} ${WHITE}⚡ 重要提示${NC} ${YELLOW}║${NC}" echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}" echo -e " ${YELLOW}║${NC} ${GRAY}请先在官网${NC} ${BRIGHT_YELLOW}flashapi.top${NC} ${GRAY}创建${NC} ${GREEN}令牌分组${NC} ${YELLOW}║${NC}" echo -e " ${YELLOW}║${NC} ${GRAY}未创建令牌分组将无法获取模型列表!${NC} ${YELLOW}║${NC}" echo -e " ${YELLOW}╚══════════════════════════════════════════════════════╝${NC}" echo "" # ── 输入 API Key (密码遮盖) ── local claude_key="" gpt_key="" if [[ "$DO_CLAUDE" == "true" ]]; then read_secret " ${WHITE}请输入 Claude 分组 API Key: ${NC}" claude_key if [[ -z "$claude_key" ]]; then print_status "error" "API Key 不能为空,已取消" return 1 fi fi if [[ "$DO_GPT" == "true" ]]; then if [[ "$DO_CLAUDE" == "true" ]]; then read_secret " ${WHITE}请输入 GPT 分组 API Key ${GRAY}(回车 = 与 Claude Key 相同)${WHITE}: ${NC}" gpt_key [[ -z "$gpt_key" ]] && gpt_key="$claude_key" else read_secret " ${WHITE}请输入 GPT 分组 API Key: ${NC}" gpt_key fi if [[ -z "$gpt_key" ]]; then print_status "error" "API Key 不能为空,已取消" return 1 fi fi echo "" # ── 验证 ── local claude_ids="" gpt_ids="" if [[ "$DO_CLAUDE" == "true" ]]; then validate_key claude "$base_url" "$claude_key" claude_ids || return 1 fi if [[ "$DO_GPT" == "true" ]]; then validate_key gpt "$base_url" "$gpt_key" gpt_ids || return 1 fi echo "" local default_family="gpt" if [[ "$DO_CLAUDE" == "true" && "$DO_GPT" == "true" ]]; then default_family="$DEFAULT_FAMILY_WHEN_BOTH" elif [[ "$DO_CLAUDE" == "true" ]]; then default_family="claude" fi local default_provider default_model if [[ "$default_family" == "claude" ]]; then default_provider="$PROVIDER_CLAUDE" default_model="$DEFAULT_CLAUDE_MODEL" else default_provider="$PROVIDER_GPT" default_model="$DEFAULT_GPT_MODEL" fi # ── 写入 Pi 配置 ── local err_file err_file="$(mktemp)" if ! write_pi_config "$base_url" "$claude_key" "$claude_ids" "$gpt_key" "$gpt_ids" \ "$default_provider" "$default_model" 2>"$err_file"; then print_status "error" "写入配置失败: $(sed -n 's/^ERROR://p' "$err_file" | head -n 1)" print_status "info" "原文件未被修改 (如已备份,可在同目录找到 .bak 文件)" rm -f "$err_file" return 1 fi if grep -q '^WARN_COMMENTS:' "$err_file"; then print_status "warn" "models.json 中原有的注释/尾逗号已在重写时移除 (原文件已备份)" fi if grep -q '^WARN_ENABLED:' "$err_file"; then print_status "info" "检测到 settings.json 设置了 enabledModels,已追加: $(sed -n 's/^WARN_ENABLED://p' "$err_file" | tr '\n' ' ')" fi rm -f "$err_file" echo "" if [[ "$DO_CLAUDE" == "true" ]]; then print_status "ok" "Claude Key 已保存: $(mask_key "$claude_key") ${GRAY}(供应商 ${PROVIDER_CLAUDE})${NC}" print_status "ok" "Claude BASE URL: ${base_url} ${GRAY}(anthropic-messages)${NC}" fi if [[ "$DO_GPT" == "true" ]]; then print_status "ok" "GPT Key 已保存: $(mask_key "$gpt_key") ${GRAY}(供应商 ${PROVIDER_GPT})${NC}" print_status "ok" "GPT BASE URL: ${base_url}/v1 ${GRAY}(openai-responses)${NC}" fi print_status "ok" "默认模型: ${default_provider}/${default_model}" print_status "ok" "推理强度: ${DEFAULT_THINKING} (defaultThinkingLevel)" print_status "ok" "模型配置已写入: ${PI_MODELS_FILE}" print_status "ok" "默认设置已写入: ${PI_SETTINGS_FILE}" echo "" echo -e " ${WHITE}使用方法:${NC}" echo -e " ${CYAN}启动:${NC} pi" echo -e " ${CYAN}快速验证:${NC} pi -p \"Reply with exactly: OK\"" echo -e " ${CYAN}查看模型:${NC} pi --list-models flashapi" if [[ "$DO_GPT" == "true" ]]; then echo -e " ${CYAN}用 GPT:${NC} pi --provider ${PROVIDER_GPT} --model ${DEFAULT_GPT_MODEL}" fi if [[ "$DO_CLAUDE" == "true" ]]; then echo -e " ${CYAN}用 Claude:${NC} pi --provider ${PROVIDER_CLAUDE} --model ${DEFAULT_CLAUDE_MODEL}" fi echo -e " ${CYAN}会话中切换:${NC} 输入 /model 选择 (Ctrl+S 设为默认),/thinking 调整推理强度" if [[ "$DO_CLAUDE" == "true" ]]; then echo "" print_status "info" "提示: 若 Claude 请求报错 \"Requests from the ... framework are not supported\"," echo -e " ${GRAY}说明该 Claude Key 所在分组仅限官方客户端;请到 flashapi.top 在允许第三方工具的分组下${NC}" echo -e " ${GRAY}新建令牌,然后重新运行本脚本选择「2) 配置 闪电API」${NC}" fi } ################################################################################ # 状态检测与菜单 ################################################################################ show_status_summary() { echo -e " ${WHITE}当前状态:${NC}" local os_label="$OS_TYPE" [[ "$IS_WSL" == "true" ]] && os_label="linux (WSL)" echo -e " 系统环境: ${CYAN}${os_label}${NC}" local pi_ver if pi_ver="$(check_pi_installed 2>/dev/null)"; then echo -e " Pi: ${GREEN}已安装${NC} (${pi_ver})" else echo -e " Pi: ${RED}未安装${NC}" fi local key url key="$(read_provider_field "$PROVIDER_CLAUDE" apiKey 2>/dev/null || true)" if [[ -n "$key" ]]; then echo -e " Claude 配置: ${GREEN}已配置${NC} ($(mask_key "$key"))" else echo -e " Claude 配置: ${RED}未配置${NC}" fi key="$(read_provider_field "$PROVIDER_GPT" apiKey 2>/dev/null || true)" if [[ -n "$key" ]]; then echo -e " GPT 配置: ${GREEN}已配置${NC} ($(mask_key "$key"))" else echo -e " GPT 配置: ${RED}未配置${NC}" fi url="$(read_provider_field "$PROVIDER_CLAUDE" baseUrl 2>/dev/null || read_provider_field "$PROVIDER_GPT" baseUrl 2>/dev/null || true)" [[ -n "$url" ]] && echo -e " API 线路: ${GREEN}${url%/v1}${NC}" local dp dm dp="$(read_settings_field defaultProvider 2>/dev/null || true)" dm="$(read_settings_field defaultModel 2>/dev/null || true)" [[ -n "$dp" || -n "$dm" ]] && echo -e " 默认模型: ${CYAN}${dp}/${dm}${NC}" echo -e " Pi 配置目录: ${CYAN}${PI_AGENT_DIR}${NC}" echo "" } show_menu() { echo -e " ${WHITE}请选择操作:${NC}" echo "" echo -e " ${BRIGHT_YELLOW}⚡${NC} ${CYAN}1)${NC} 安装 Pi" echo -e " ${BRIGHT_YELLOW}⚡${NC} ${CYAN}2)${NC} 配置 闪电API (Claude + GPT)" echo -e " ${BRIGHT_YELLOW}⚡${NC} ${CYAN}3)${NC} 一键全部安装 (安装 + 配置)" echo -e " ${BRIGHT_YELLOW}⚡${NC} ${CYAN}0)${NC} 退出" echo "" echo -en " ${WHITE}请输入选项: ${NC}" >&2 } ################################################################################ # 主流程 ################################################################################ main() { detect_os print_banner show_status_summary while true; do show_menu local choice read -r choice < /dev/tty case "$choice" in 1) install_pi || true echo "" ;; 2) configure_api || true echo "" ;; 3) install_pi || true echo "" echo -e " ${GRAY}───────────────────────────────────────────────────────${NC}" if configure_api; then echo "" echo -e " ${GRAY}───────────────────────────────────────────────────────${NC}" echo "" print_status "ok" "⚡ 全部完成! 请重新打开终端后运行 pi" fi echo "" ;; 0) echo "" echo -e " ${BRIGHT_YELLOW}⚡${NC} ${WHITE}感谢使用闪电API,祝你Vibe Coding愉快!${NC}" echo -e " ${GRAY}官网: flashapi.top${NC}" echo "" exit 0 ;; *) print_status "error" "无效选项,请重新选择" echo "" ;; esac done } main "$@"