用 AI 写了个 codex-switch 一键切换 Codex 多套 API 配置
- 内容介绍
- 文章标签
- 相关推荐
天天给我的 codex 换各种不同的中转 API 有点麻烦,让 codex 给我写了一个快速命令行切换的命令,分享给各位佬,请自行修改使用。
- 输入 codex-switch 即可进入交互式选择
- 支持粘贴大量文本(如配置json)自动解析添加配置
CleanShot 2026-04-05 at 15.22.35@2x990×342 77.4 KB
添加新的配置:
CleanShot 2026-04-05 at 15.23.35@2x1170×406 75.5 KB
脚本如下,直接粘贴给你的 codex 或者其他 AI 工具来帮你安装、改动、调整:
#!/bin/bash
# codex-switch 一键安装脚本
# 用法: bash install-codex-switch.sh
# 或: curl -fsSL <url> | bash
set -e
CODEX_DIR="$HOME/.codex"
SCRIPT_PATH="$CODEX_DIR/switch-api.sh"
PROFILES_DIR="$CODEX_DIR/profiles"
echo ""
echo " ┌──────────────────────────────────────┐"
echo " │ codex-switch 安装程序 │"
echo " │ Codex API 多配置快速切换工具 │"
echo " └──────────────────────────────────────┘"
echo ""
# 检查 codex 是否已安装
if [[ ! -d "$CODEX_DIR" ]]; then
echo " ❌ 未检测到 ~/.codex 目录,请先安装 OpenAI Codex CLI"
exit 1
fi
# 创建 profiles 目录
mkdir -p "$PROFILES_DIR"
# 备份旧脚本
if [[ -f "$SCRIPT_PATH" ]]; then
cp "$SCRIPT_PATH" "${SCRIPT_PATH}.bak.$(date +%Y%m%d%H%M%S)"
echo " 📦 已备份旧版本"
fi
# 写入主脚本
cat > "$SCRIPT_PATH" << 'MAINSCRIPT'
#!/bin/zsh
# Codex API 快速切换脚本
# 用法: codex-switch — 方向键交互选择
# codex-switch <名称> — 直接切换
# codex-switch add — 添加新配置
# codex-switch rm <名称> — 删除配置
# codex-switch list — 列出所有配置
CODEX_DIR="$HOME/.codex"
PROFILES_DIR="$CODEX_DIR/profiles"
# ── 工具函数 ─────────────────────────────────
load_profile() {
local file="$1"
unset desc auth_mode api_key config
source "$file"
}
get_profile_names() {
local files=("$PROFILES_DIR"/*.conf(N))
for f in "${files[@]}"; do
echo "${f:t:r}"
done
}
current_profile() {
local names=($(get_profile_names))
for name in "${names[@]}"; do
load_profile "$PROFILES_DIR/$name.conf"
if [[ "$auth_mode" == "chatgpt" ]]; then
grep -q 'model_provider' "$CODEX_DIR/config.toml" 2>/dev/null || { echo "$name"; return; }
else
local url
url=$(echo "$config" | grep 'base_url' | head -1 | sed 's/.*= *"//;s/".*//')
[[ -n "$url" ]] && grep -q "$url" "$CODEX_DIR/config.toml" 2>/dev/null && { echo "$name"; return; }
fi
done
echo "unknown"
}
get_tail() {
sed -n '/^\[projects\./,$p' "$CODEX_DIR/config.toml"
}
# ── 方向键交互菜单 ───────────────────────────
arrow_menu() {
local names=($(get_profile_names))
local count=${#names[@]}
if (( count == 0 )); then
echo "❌ 没有配置文件,请先运行: codex-switch add"
return 1
fi
local current
current="$(current_profile)"
local -a descs
local sel=1
for (( i=1; i<=count; i++ )); do
load_profile "$PROFILES_DIR/${names[$i]}.conf"
descs[$i]="$desc"
[[ "${names[$i]}" == "$current" ]] && sel=$i
done
tput civis 2>/dev/null
draw_menu() {
(( ${1:-0} )) && printf "\033[%dA" "$count"
for (( i=1; i<=count; i++ )); do
local marker=" "
local style="\033[0m"
[[ "${names[$i]}" == "$current" ]] && marker="✦ "
if (( i == sel )); then
style="\033[1;36m"
printf "\r\033[K${style} ▸ %-14s %s${marker:+ \033[2m%s\033[0m}\033[0m\n" "${names[$i]}" "${descs[$i]}" "$( [[ "${names[$i]}" == "$current" ]] && echo '当前' )"
else
printf "\r\033[K${style} %-14s %s${marker:+ \033[2m%s\033[0m}\033[0m\n" "${names[$i]}" "${descs[$i]}" "$( [[ "${names[$i]}" == "$current" ]] && echo '当前' )"
fi
done
}
echo ""
echo " \033[1mCodex API 配置切换\033[0m (↑↓ 选择, Enter 确认, q 退出)"
echo " ─────────────────────────────────────────────"
echo ""
draw_menu 0
while true; do
read -rsk1 key
case "$key" in
$'\e')
read -rsk1 -t 0.1 key2
read -rsk1 -t 0.1 key3
case "$key2$key3" in
'[A') (( sel > 1 )) && (( sel-- )) ;;
'[B') (( sel < count )) && (( sel++ )) ;;
esac
draw_menu 1
;;
k) (( sel > 1 )) && (( sel-- )); draw_menu 1 ;;
j) (( sel < count )) && (( sel++ )); draw_menu 1 ;;
$'\n'|'')
tput cnorm 2>/dev/null
echo ""
switch_to "${names[$sel]}"
return
;;
q|Q)
tput cnorm 2>/dev/null
echo ""
echo " 已取消"
return 1
;;
esac
done
}
# ── 切换逻辑 ─────────────────────────────────
switch_to() {
local name="$1"
local file="$PROFILES_DIR/$name.conf"
if [[ ! -f "$file" ]]; then
echo "❌ 配置不存在: $name"
echo " 可用: $(get_profile_names | tr '\n' ' ')"
return 1
fi
load_profile "$file"
local tail
tail="$(get_tail)"
printf '%s\n\n%s\n' "$config" "$tail" > "$CODEX_DIR/config.toml"
if [[ "$auth_mode" == "apikey" && -n "$api_key" ]]; then
cat > "$CODEX_DIR/auth.json" <<AUTHEOF
{
"auth_mode": "apikey",
"OPENAI_API_KEY": "$api_key"
}
AUTHEOF
else
if [[ -f "$CODEX_DIR/auth.json" ]] && command -v jq &>/dev/null; then
local cur
cur="$(cat "$CODEX_DIR/auth.json")"
echo "$cur" | jq '.auth_mode = "chatgpt" | .OPENAI_API_KEY = null' > "$CODEX_DIR/auth.json"
else
echo '{"auth_mode":"chatgpt","OPENAI_API_KEY":null}' > "$CODEX_DIR/auth.json"
fi
if ! jq -e '.tokens.refresh_token' "$CODEX_DIR/auth.json" &>/dev/null; then
echo "⚠️ 需要重新登录: codex --login"
fi
fi
echo "✅ 已切换到: $desc"
}
# ── 添加新配置 ───────────────────────────────
parse_pasted_text() {
local text="$1"
_parsed_url=$(echo "$text" | grep -oE 'base_url\s*=\s*"[^"]+"' | head -1 | sed 's/.*= *"//;s/"//')
[[ -z "$_parsed_url" ]] && _parsed_url=$(echo "$text" | grep -oE 'https?://[A-Za-z0-9._~:/?#@!$&()*+,;=-]+' | head -1)
_parsed_url="${_parsed_url%[,;'\"\}\)]}"
_parsed_key=$(echo "$text" | grep -oE '(sk-[A-Za-z0-9_-]{20,}|key-[A-Za-z0-9_-]{20,})' | head -1)
if [[ -z "$_parsed_key" ]]; then
_parsed_key=$(echo "$text" | grep -oiE '(OPENAI_API_KEY|api_key)["\s:=]+\s*"?([^",\s}]+)' | head -1 | sed 's/.*["= :]//')
_parsed_key="${_parsed_key%[\"'\},]}"
fi
_parsed_model=$(echo "$text" | grep -E '^\s*model\s*=' | grep -v 'model_provider\|model_reasoning\|model_context\|model_auto\|review_model' | head -1 | sed 's/.*= *"//;s/".*//')
_parsed_wire=$(echo "$text" | grep -oE 'wire_api\s*=\s*"[^"]+"' | head -1 | sed 's/.*= *"//;s/"//')
_parsed_name=$(echo "$text" | grep -E '^\s*name\s*=' | head -1 | sed 's/.*= *"//;s/".*//')
if [[ -z "$_parsed_name" && -n "$_parsed_url" ]]; then
_parsed_name=$(echo "$_parsed_url" | sed 's|https\?://||;s|/.*||;s|^api\.||;s|^www\.||;s|\.com$||;s|\.net$||;s|\.io$||;s|\.ai$||;s|\.org$||;s|\.cn$||' | tr '.' '-')
fi
}
add_profile() {
echo ""
echo " \033[1m添加新的 API 配置\033[0m"
echo " ─────────────────"
echo ""
echo " 可以直接粘贴包含 URL / Key 的文本(如配置片段、JSON 等)"
echo " 粘贴完成后按两次 Enter 结束;或直接按 Enter 进入手动输入模式"
echo ""
printf " ▸ 粘贴或输入: "
local pasted="" line="" empty_count=0
while true; do
IFS= read -r line
if [[ -z "$line" ]]; then
(( empty_count++ ))
(( empty_count >= 2 )) && break
[[ -z "$pasted" ]] && break
pasted+=$'\n'
else
empty_count=0
pasted+="${line}"$'\n'
fi
done
local pname="" pdesc="" purl="" pkey="" pmodel="" pwire=""
if [[ -n "$pasted" ]]; then
parse_pasted_text "$pasted"
echo ""
echo " \033[1m已识别:\033[0m"
[[ -n "$_parsed_name" ]] && echo " 名称: $_parsed_name"
[[ -n "$_parsed_url" ]] && echo " URL: $_parsed_url"
[[ -n "$_parsed_key" ]] && echo " Key: ${_parsed_key:0:12}...${_parsed_key: -4}"
[[ -n "$_parsed_model" ]] && echo " 模型: $_parsed_model"
[[ -n "$_parsed_wire" ]] && echo " Wire: $_parsed_wire"
echo ""
pname="$_parsed_name"
purl="$_parsed_url"
pkey="$_parsed_key"
pmodel="$_parsed_model"
pwire="$_parsed_wire"
fi
printf " 配置名称 [${pname:-myapi}]: "
read -r input; [[ -n "$input" ]] && pname="$input"
pname="${pname:-myapi}"
pname="${pname// /-}"
if [[ -f "$PROFILES_DIR/$pname.conf" ]]; then
printf " ⚠️ \"$pname\" 已存在,覆盖? [y/N]: "
read -rk1 yn; echo
[[ "$yn" != [yY] ]] && { echo " 已取消"; return 1; }
fi
printf " 描述 [${pname} API]: "
read -r input; pdesc="${input:-${pname} API}"
printf " API Base URL [${purl:-}]: "
read -r input; [[ -n "$input" ]] && purl="$input"
if [[ -z "$purl" ]]; then
echo " ❌ URL 不能为空"; return 1
fi
if [[ -n "$pkey" ]]; then
printf " API Key [${pkey:0:12}...${pkey: -4}]: "
else
printf " API Key: "
fi
read -r input; [[ -n "$input" ]] && pkey="$input"
if [[ -z "$pkey" ]]; then
echo " ❌ API Key 不能为空"; return 1
fi
printf " 模型名称 [${pmodel:-gpt-5.4}]: "
read -r input; [[ -n "$input" ]] && pmodel="$input"
pmodel="${pmodel:-gpt-5.4}"
printf " Wire API (responses/chat) [${pwire:-responses}]: "
read -r input; [[ -n "$input" ]] && pwire="$input"
pwire="${pwire:-responses}"
cat > "$PROFILES_DIR/$pname.conf" <<PROFEOF
desc="$pdesc"
auth_mode="apikey"
api_key="$pkey"
config='model_provider = "OpenAI"
model = "$pmodel"
review_model = "$pmodel"
model_reasoning_effort = "xhigh"
disable_response_storage = true
network_access = "enabled"
windows_wsl_setup_acknowledged = true
model_context_window = 1000000
model_auto_compact_token_limit = 900000
personality = "pragmatic"
approvals_reviewer = "user"
service_tier = "fast"
[model_providers.OpenAI]
name = "OpenAI"
base_url = "$purl"
wire_api = "$pwire"
requires_openai_auth = true'
PROFEOF
echo ""
echo " ✅ 配置 \"$pname\" 已保存"
printf " 立即切换到此配置? [Y/n]: "
read -rk1 sw; echo
[[ "$sw" != [nN] ]] && switch_to "$pname"
}
# ── 删除配置 ─────────────────────────────────
rm_profile() {
local name="$1"
if [[ -z "$name" ]]; then
echo "用法: codex-switch rm <配置名>"
return 1
fi
local file="$PROFILES_DIR/$name.conf"
if [[ ! -f "$file" ]]; then
echo "❌ 配置不存在: $name"
return 1
fi
printf " 确认删除 \"$name\"? [y/N]: "
read -rk1 yn; echo
if [[ "$yn" == [yY] ]]; then
rm "$file"
echo "✅ 已删除: $name"
else
echo "已取消"
fi
}
# ── 列出配置 ─────────────────────────────────
list_profiles() {
local names=($(get_profile_names))
local current
current="$(current_profile)"
echo ""
echo " \033[1m已有配置\033[0m ($PROFILES_DIR/)"
echo " ─────────────────"
for name in "${names[@]}"; do
load_profile "$PROFILES_DIR/$name.conf"
local mark=""
[[ "$name" == "$current" ]] && mark=" ← 当前"
printf " %-14s %s\033[2m%s\033[0m\n" "$name" "$desc" "$mark"
done
echo ""
}
# ── 帮助 ─────────────────────────────────────
show_help() {
local current
current="$(current_profile)"
echo ""
echo " \033[1mcodex-switch\033[0m — Codex API 多配置快速切换"
echo ""
echo " \033[1m用法:\033[0m"
echo " codex-switch 交互式选择(↑↓ 方向键)"
echo " codex-switch <名称> 直接切换到指定配置"
echo " codex-switch add 添加新配置(支持粘贴自动识别)"
echo " codex-switch rm <名称> 删除配置"
echo " codex-switch list 列出所有配置"
echo " codex-switch help 显示此帮助"
echo ""
echo " \033[1m当前:\033[0m $current"
echo " \033[1m配置目录:\033[0m $PROFILES_DIR/"
echo ""
}
# ── 入口 ─────────────────────────────────────
case "${1:-}" in
add) add_profile ;;
rm|remove) rm_profile "$2" ;;
list|ls) list_profiles ;;
help|-h|--help) show_help ;;
"") arrow_menu ;;
*) switch_to "$1" ;;
esac
MAINSCRIPT
chmod +x "$SCRIPT_PATH"
echo " ✅ 脚本已安装到 $SCRIPT_PATH"
# 创建 profiles 目录
mkdir -p "$PROFILES_DIR"
# 创建官方配置(如果不存在)
if [[ ! -f "$PROFILES_DIR/official.conf" ]]; then
cat > "$PROFILES_DIR/official.conf" << 'OFFCONF'
desc="官方 OpenAI (ChatGPT 认证)"
auth_mode="chatgpt"
config='model = "gpt-5.4"
model_reasoning_effort = "xhigh"
personality = "pragmatic"
approvals_reviewer = "user"
service_tier = "fast"'
OFFCONF
echo " 📄 已创建默认配置: official"
fi
# 设置 shell alias
setup_alias() {
local shell_rc=""
if [[ -n "$ZSH_VERSION" ]] || [[ "$SHELL" == */zsh ]]; then
shell_rc="$HOME/.zshrc"
elif [[ -n "$BASH_VERSION" ]] || [[ "$SHELL" == */bash ]]; then
shell_rc="$HOME/.bashrc"
fi
if [[ -z "$shell_rc" ]]; then
echo " ⚠️ 无法检测 shell 类型,请手动添加 alias:"
echo " alias codex-switch='source ~/.codex/switch-api.sh'"
return
fi
local alias_line="alias codex-switch='source ~/.codex/switch-api.sh'"
if grep -qF 'codex-switch' "$shell_rc" 2>/dev/null; then
echo " ✅ alias 已存在于 $shell_rc"
else
echo "" >> "$shell_rc"
echo "# codex-switch: Codex API 多配置切换" >> "$shell_rc"
echo "$alias_line" >> "$shell_rc"
echo " ✅ alias 已添加到 $shell_rc"
fi
}
setup_alias
echo ""
echo " ┌────────────────────────────────────────┐"
echo " │ 安装完成! │"
echo " │ │"
echo " │ 重新打开终端,或运行: │"
echo " │ source ~/.zshrc │"
echo " │ │"
echo " │ 然后: │"
echo " │ codex-switch 交互式选择 │"
echo " │ codex-switch add 添加新配置 │"
echo " │ codex-switch rm <名> 删除配置 │"
echo " │ codex-switch list 查看所有配置 │"
echo " │ codex-switch help 帮助信息 │"
echo " └────────────────────────────────────────┘"
echo ""
网友解答:
--【壹】--:
一直在用 正版 cc 订阅,所以一直没敢用 cc switch 看来我重复造轮子了,现在马上去下。
--【贰】--:
ccswitch可以直接切
--【叁】--:
感谢大佬了。
--【肆】--:
cc switch不是有这个功能嘛
--【伍】--:
我也vibe了一个tui的,自用还是比较舒适的
--【陆】--:
我前几天写了一个类似于ccswitch的命令行切换工具(https://github.com/realihang/cc-switch-linux.git)跟你这个思路差不多 需要的友友欢迎star!
天天给我的 codex 换各种不同的中转 API 有点麻烦,让 codex 给我写了一个快速命令行切换的命令,分享给各位佬,请自行修改使用。
- 输入 codex-switch 即可进入交互式选择
- 支持粘贴大量文本(如配置json)自动解析添加配置
CleanShot 2026-04-05 at 15.22.35@2x990×342 77.4 KB
添加新的配置:
CleanShot 2026-04-05 at 15.23.35@2x1170×406 75.5 KB
脚本如下,直接粘贴给你的 codex 或者其他 AI 工具来帮你安装、改动、调整:
#!/bin/bash
# codex-switch 一键安装脚本
# 用法: bash install-codex-switch.sh
# 或: curl -fsSL <url> | bash
set -e
CODEX_DIR="$HOME/.codex"
SCRIPT_PATH="$CODEX_DIR/switch-api.sh"
PROFILES_DIR="$CODEX_DIR/profiles"
echo ""
echo " ┌──────────────────────────────────────┐"
echo " │ codex-switch 安装程序 │"
echo " │ Codex API 多配置快速切换工具 │"
echo " └──────────────────────────────────────┘"
echo ""
# 检查 codex 是否已安装
if [[ ! -d "$CODEX_DIR" ]]; then
echo " ❌ 未检测到 ~/.codex 目录,请先安装 OpenAI Codex CLI"
exit 1
fi
# 创建 profiles 目录
mkdir -p "$PROFILES_DIR"
# 备份旧脚本
if [[ -f "$SCRIPT_PATH" ]]; then
cp "$SCRIPT_PATH" "${SCRIPT_PATH}.bak.$(date +%Y%m%d%H%M%S)"
echo " 📦 已备份旧版本"
fi
# 写入主脚本
cat > "$SCRIPT_PATH" << 'MAINSCRIPT'
#!/bin/zsh
# Codex API 快速切换脚本
# 用法: codex-switch — 方向键交互选择
# codex-switch <名称> — 直接切换
# codex-switch add — 添加新配置
# codex-switch rm <名称> — 删除配置
# codex-switch list — 列出所有配置
CODEX_DIR="$HOME/.codex"
PROFILES_DIR="$CODEX_DIR/profiles"
# ── 工具函数 ─────────────────────────────────
load_profile() {
local file="$1"
unset desc auth_mode api_key config
source "$file"
}
get_profile_names() {
local files=("$PROFILES_DIR"/*.conf(N))
for f in "${files[@]}"; do
echo "${f:t:r}"
done
}
current_profile() {
local names=($(get_profile_names))
for name in "${names[@]}"; do
load_profile "$PROFILES_DIR/$name.conf"
if [[ "$auth_mode" == "chatgpt" ]]; then
grep -q 'model_provider' "$CODEX_DIR/config.toml" 2>/dev/null || { echo "$name"; return; }
else
local url
url=$(echo "$config" | grep 'base_url' | head -1 | sed 's/.*= *"//;s/".*//')
[[ -n "$url" ]] && grep -q "$url" "$CODEX_DIR/config.toml" 2>/dev/null && { echo "$name"; return; }
fi
done
echo "unknown"
}
get_tail() {
sed -n '/^\[projects\./,$p' "$CODEX_DIR/config.toml"
}
# ── 方向键交互菜单 ───────────────────────────
arrow_menu() {
local names=($(get_profile_names))
local count=${#names[@]}
if (( count == 0 )); then
echo "❌ 没有配置文件,请先运行: codex-switch add"
return 1
fi
local current
current="$(current_profile)"
local -a descs
local sel=1
for (( i=1; i<=count; i++ )); do
load_profile "$PROFILES_DIR/${names[$i]}.conf"
descs[$i]="$desc"
[[ "${names[$i]}" == "$current" ]] && sel=$i
done
tput civis 2>/dev/null
draw_menu() {
(( ${1:-0} )) && printf "\033[%dA" "$count"
for (( i=1; i<=count; i++ )); do
local marker=" "
local style="\033[0m"
[[ "${names[$i]}" == "$current" ]] && marker="✦ "
if (( i == sel )); then
style="\033[1;36m"
printf "\r\033[K${style} ▸ %-14s %s${marker:+ \033[2m%s\033[0m}\033[0m\n" "${names[$i]}" "${descs[$i]}" "$( [[ "${names[$i]}" == "$current" ]] && echo '当前' )"
else
printf "\r\033[K${style} %-14s %s${marker:+ \033[2m%s\033[0m}\033[0m\n" "${names[$i]}" "${descs[$i]}" "$( [[ "${names[$i]}" == "$current" ]] && echo '当前' )"
fi
done
}
echo ""
echo " \033[1mCodex API 配置切换\033[0m (↑↓ 选择, Enter 确认, q 退出)"
echo " ─────────────────────────────────────────────"
echo ""
draw_menu 0
while true; do
read -rsk1 key
case "$key" in
$'\e')
read -rsk1 -t 0.1 key2
read -rsk1 -t 0.1 key3
case "$key2$key3" in
'[A') (( sel > 1 )) && (( sel-- )) ;;
'[B') (( sel < count )) && (( sel++ )) ;;
esac
draw_menu 1
;;
k) (( sel > 1 )) && (( sel-- )); draw_menu 1 ;;
j) (( sel < count )) && (( sel++ )); draw_menu 1 ;;
$'\n'|'')
tput cnorm 2>/dev/null
echo ""
switch_to "${names[$sel]}"
return
;;
q|Q)
tput cnorm 2>/dev/null
echo ""
echo " 已取消"
return 1
;;
esac
done
}
# ── 切换逻辑 ─────────────────────────────────
switch_to() {
local name="$1"
local file="$PROFILES_DIR/$name.conf"
if [[ ! -f "$file" ]]; then
echo "❌ 配置不存在: $name"
echo " 可用: $(get_profile_names | tr '\n' ' ')"
return 1
fi
load_profile "$file"
local tail
tail="$(get_tail)"
printf '%s\n\n%s\n' "$config" "$tail" > "$CODEX_DIR/config.toml"
if [[ "$auth_mode" == "apikey" && -n "$api_key" ]]; then
cat > "$CODEX_DIR/auth.json" <<AUTHEOF
{
"auth_mode": "apikey",
"OPENAI_API_KEY": "$api_key"
}
AUTHEOF
else
if [[ -f "$CODEX_DIR/auth.json" ]] && command -v jq &>/dev/null; then
local cur
cur="$(cat "$CODEX_DIR/auth.json")"
echo "$cur" | jq '.auth_mode = "chatgpt" | .OPENAI_API_KEY = null' > "$CODEX_DIR/auth.json"
else
echo '{"auth_mode":"chatgpt","OPENAI_API_KEY":null}' > "$CODEX_DIR/auth.json"
fi
if ! jq -e '.tokens.refresh_token' "$CODEX_DIR/auth.json" &>/dev/null; then
echo "⚠️ 需要重新登录: codex --login"
fi
fi
echo "✅ 已切换到: $desc"
}
# ── 添加新配置 ───────────────────────────────
parse_pasted_text() {
local text="$1"
_parsed_url=$(echo "$text" | grep -oE 'base_url\s*=\s*"[^"]+"' | head -1 | sed 's/.*= *"//;s/"//')
[[ -z "$_parsed_url" ]] && _parsed_url=$(echo "$text" | grep -oE 'https?://[A-Za-z0-9._~:/?#@!$&()*+,;=-]+' | head -1)
_parsed_url="${_parsed_url%[,;'\"\}\)]}"
_parsed_key=$(echo "$text" | grep -oE '(sk-[A-Za-z0-9_-]{20,}|key-[A-Za-z0-9_-]{20,})' | head -1)
if [[ -z "$_parsed_key" ]]; then
_parsed_key=$(echo "$text" | grep -oiE '(OPENAI_API_KEY|api_key)["\s:=]+\s*"?([^",\s}]+)' | head -1 | sed 's/.*["= :]//')
_parsed_key="${_parsed_key%[\"'\},]}"
fi
_parsed_model=$(echo "$text" | grep -E '^\s*model\s*=' | grep -v 'model_provider\|model_reasoning\|model_context\|model_auto\|review_model' | head -1 | sed 's/.*= *"//;s/".*//')
_parsed_wire=$(echo "$text" | grep -oE 'wire_api\s*=\s*"[^"]+"' | head -1 | sed 's/.*= *"//;s/"//')
_parsed_name=$(echo "$text" | grep -E '^\s*name\s*=' | head -1 | sed 's/.*= *"//;s/".*//')
if [[ -z "$_parsed_name" && -n "$_parsed_url" ]]; then
_parsed_name=$(echo "$_parsed_url" | sed 's|https\?://||;s|/.*||;s|^api\.||;s|^www\.||;s|\.com$||;s|\.net$||;s|\.io$||;s|\.ai$||;s|\.org$||;s|\.cn$||' | tr '.' '-')
fi
}
add_profile() {
echo ""
echo " \033[1m添加新的 API 配置\033[0m"
echo " ─────────────────"
echo ""
echo " 可以直接粘贴包含 URL / Key 的文本(如配置片段、JSON 等)"
echo " 粘贴完成后按两次 Enter 结束;或直接按 Enter 进入手动输入模式"
echo ""
printf " ▸ 粘贴或输入: "
local pasted="" line="" empty_count=0
while true; do
IFS= read -r line
if [[ -z "$line" ]]; then
(( empty_count++ ))
(( empty_count >= 2 )) && break
[[ -z "$pasted" ]] && break
pasted+=$'\n'
else
empty_count=0
pasted+="${line}"$'\n'
fi
done
local pname="" pdesc="" purl="" pkey="" pmodel="" pwire=""
if [[ -n "$pasted" ]]; then
parse_pasted_text "$pasted"
echo ""
echo " \033[1m已识别:\033[0m"
[[ -n "$_parsed_name" ]] && echo " 名称: $_parsed_name"
[[ -n "$_parsed_url" ]] && echo " URL: $_parsed_url"
[[ -n "$_parsed_key" ]] && echo " Key: ${_parsed_key:0:12}...${_parsed_key: -4}"
[[ -n "$_parsed_model" ]] && echo " 模型: $_parsed_model"
[[ -n "$_parsed_wire" ]] && echo " Wire: $_parsed_wire"
echo ""
pname="$_parsed_name"
purl="$_parsed_url"
pkey="$_parsed_key"
pmodel="$_parsed_model"
pwire="$_parsed_wire"
fi
printf " 配置名称 [${pname:-myapi}]: "
read -r input; [[ -n "$input" ]] && pname="$input"
pname="${pname:-myapi}"
pname="${pname// /-}"
if [[ -f "$PROFILES_DIR/$pname.conf" ]]; then
printf " ⚠️ \"$pname\" 已存在,覆盖? [y/N]: "
read -rk1 yn; echo
[[ "$yn" != [yY] ]] && { echo " 已取消"; return 1; }
fi
printf " 描述 [${pname} API]: "
read -r input; pdesc="${input:-${pname} API}"
printf " API Base URL [${purl:-}]: "
read -r input; [[ -n "$input" ]] && purl="$input"
if [[ -z "$purl" ]]; then
echo " ❌ URL 不能为空"; return 1
fi
if [[ -n "$pkey" ]]; then
printf " API Key [${pkey:0:12}...${pkey: -4}]: "
else
printf " API Key: "
fi
read -r input; [[ -n "$input" ]] && pkey="$input"
if [[ -z "$pkey" ]]; then
echo " ❌ API Key 不能为空"; return 1
fi
printf " 模型名称 [${pmodel:-gpt-5.4}]: "
read -r input; [[ -n "$input" ]] && pmodel="$input"
pmodel="${pmodel:-gpt-5.4}"
printf " Wire API (responses/chat) [${pwire:-responses}]: "
read -r input; [[ -n "$input" ]] && pwire="$input"
pwire="${pwire:-responses}"
cat > "$PROFILES_DIR/$pname.conf" <<PROFEOF
desc="$pdesc"
auth_mode="apikey"
api_key="$pkey"
config='model_provider = "OpenAI"
model = "$pmodel"
review_model = "$pmodel"
model_reasoning_effort = "xhigh"
disable_response_storage = true
network_access = "enabled"
windows_wsl_setup_acknowledged = true
model_context_window = 1000000
model_auto_compact_token_limit = 900000
personality = "pragmatic"
approvals_reviewer = "user"
service_tier = "fast"
[model_providers.OpenAI]
name = "OpenAI"
base_url = "$purl"
wire_api = "$pwire"
requires_openai_auth = true'
PROFEOF
echo ""
echo " ✅ 配置 \"$pname\" 已保存"
printf " 立即切换到此配置? [Y/n]: "
read -rk1 sw; echo
[[ "$sw" != [nN] ]] && switch_to "$pname"
}
# ── 删除配置 ─────────────────────────────────
rm_profile() {
local name="$1"
if [[ -z "$name" ]]; then
echo "用法: codex-switch rm <配置名>"
return 1
fi
local file="$PROFILES_DIR/$name.conf"
if [[ ! -f "$file" ]]; then
echo "❌ 配置不存在: $name"
return 1
fi
printf " 确认删除 \"$name\"? [y/N]: "
read -rk1 yn; echo
if [[ "$yn" == [yY] ]]; then
rm "$file"
echo "✅ 已删除: $name"
else
echo "已取消"
fi
}
# ── 列出配置 ─────────────────────────────────
list_profiles() {
local names=($(get_profile_names))
local current
current="$(current_profile)"
echo ""
echo " \033[1m已有配置\033[0m ($PROFILES_DIR/)"
echo " ─────────────────"
for name in "${names[@]}"; do
load_profile "$PROFILES_DIR/$name.conf"
local mark=""
[[ "$name" == "$current" ]] && mark=" ← 当前"
printf " %-14s %s\033[2m%s\033[0m\n" "$name" "$desc" "$mark"
done
echo ""
}
# ── 帮助 ─────────────────────────────────────
show_help() {
local current
current="$(current_profile)"
echo ""
echo " \033[1mcodex-switch\033[0m — Codex API 多配置快速切换"
echo ""
echo " \033[1m用法:\033[0m"
echo " codex-switch 交互式选择(↑↓ 方向键)"
echo " codex-switch <名称> 直接切换到指定配置"
echo " codex-switch add 添加新配置(支持粘贴自动识别)"
echo " codex-switch rm <名称> 删除配置"
echo " codex-switch list 列出所有配置"
echo " codex-switch help 显示此帮助"
echo ""
echo " \033[1m当前:\033[0m $current"
echo " \033[1m配置目录:\033[0m $PROFILES_DIR/"
echo ""
}
# ── 入口 ─────────────────────────────────────
case "${1:-}" in
add) add_profile ;;
rm|remove) rm_profile "$2" ;;
list|ls) list_profiles ;;
help|-h|--help) show_help ;;
"") arrow_menu ;;
*) switch_to "$1" ;;
esac
MAINSCRIPT
chmod +x "$SCRIPT_PATH"
echo " ✅ 脚本已安装到 $SCRIPT_PATH"
# 创建 profiles 目录
mkdir -p "$PROFILES_DIR"
# 创建官方配置(如果不存在)
if [[ ! -f "$PROFILES_DIR/official.conf" ]]; then
cat > "$PROFILES_DIR/official.conf" << 'OFFCONF'
desc="官方 OpenAI (ChatGPT 认证)"
auth_mode="chatgpt"
config='model = "gpt-5.4"
model_reasoning_effort = "xhigh"
personality = "pragmatic"
approvals_reviewer = "user"
service_tier = "fast"'
OFFCONF
echo " 📄 已创建默认配置: official"
fi
# 设置 shell alias
setup_alias() {
local shell_rc=""
if [[ -n "$ZSH_VERSION" ]] || [[ "$SHELL" == */zsh ]]; then
shell_rc="$HOME/.zshrc"
elif [[ -n "$BASH_VERSION" ]] || [[ "$SHELL" == */bash ]]; then
shell_rc="$HOME/.bashrc"
fi
if [[ -z "$shell_rc" ]]; then
echo " ⚠️ 无法检测 shell 类型,请手动添加 alias:"
echo " alias codex-switch='source ~/.codex/switch-api.sh'"
return
fi
local alias_line="alias codex-switch='source ~/.codex/switch-api.sh'"
if grep -qF 'codex-switch' "$shell_rc" 2>/dev/null; then
echo " ✅ alias 已存在于 $shell_rc"
else
echo "" >> "$shell_rc"
echo "# codex-switch: Codex API 多配置切换" >> "$shell_rc"
echo "$alias_line" >> "$shell_rc"
echo " ✅ alias 已添加到 $shell_rc"
fi
}
setup_alias
echo ""
echo " ┌────────────────────────────────────────┐"
echo " │ 安装完成! │"
echo " │ │"
echo " │ 重新打开终端,或运行: │"
echo " │ source ~/.zshrc │"
echo " │ │"
echo " │ 然后: │"
echo " │ codex-switch 交互式选择 │"
echo " │ codex-switch add 添加新配置 │"
echo " │ codex-switch rm <名> 删除配置 │"
echo " │ codex-switch list 查看所有配置 │"
echo " │ codex-switch help 帮助信息 │"
echo " └────────────────────────────────────────┘"
echo ""
网友解答:
--【壹】--:
一直在用 正版 cc 订阅,所以一直没敢用 cc switch 看来我重复造轮子了,现在马上去下。
--【贰】--:
ccswitch可以直接切
--【叁】--:
感谢大佬了。
--【肆】--:
cc switch不是有这个功能嘛
--【伍】--:
我也vibe了一个tui的,自用还是比较舒适的
--【陆】--:
我前几天写了一个类似于ccswitch的命令行切换工具(https://github.com/realihang/cc-switch-linux.git)跟你这个思路差不多 需要的友友欢迎star!

