Исправить 7 багов после code review: экспорт ключа, утечка прокси, re-валидация

- Fix 1: export ANTHROPIC_API_KEY при ручном вводе ключа в claude_anthropic ([K]-ветка);
  без export subprocess claude не видел ключ и падал с ошибкой авторизации
- Fix 2: trap RETURN в claude_gpt убивает прокси при любом ранем return 1,
  устраняя утечку фоновых процессов
- Fix 3: sleep 1 заменён на poll-цикл (10 попыток, curl exit 7 = connection refused);
  connection refused теперь не маскируется под «нет сети»
- Fix 4: после codex auth login в claude_gpt добавлена повторная проверка _claude_test_api;
  прежде claude запускался без подтверждения успешности reauth
- Fix 5: аналогичная re-валидация в claude_gemini после браузерной авторизации
  (ветки 401/403 и 429)
- Fix 6: prompt [c/Q] → [C/q] в 429-обработчике claude_anthropic — заглавная буква
  соответствует умолчанию (стандарт файла: CAPITAL = default)
- Fix 7: trap 'rm -rf "$TMP"' EXIT — одинарные кавычки + кавычки вокруг $TMP,
  предотвращают word-split при путях с пробелами

Добавлены тесты: tests/test_fixes.sh (21 тест, все проходят)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 00:53:53 +07:00
parent 68c731b72e
commit 048f6b1770
2 changed files with 841 additions and 87 deletions

562
claude_setup.sh Normal file → Executable file
View File

@@ -87,7 +87,7 @@ install_proxy() {
[ -z "$LATEST" ] && err "Не удалось получить версию claude-code-proxy с GitHub"
TMP=$(mktemp -d)
trap "rm -rf $TMP" EXIT
trap 'rm -rf "$TMP"' EXIT
URL="https://github.com/raine/claude-code-proxy/releases/download/${LATEST}/claude-code-proxy-linux-${ARCH_TAG}.tar.gz"
info "Скачиваю $URL"
curl -fsSL "$URL" -o "$TMP/proxy.tar.gz" || err "Не удалось скачать claude-code-proxy"
@@ -146,12 +146,204 @@ cat >> "$BASHRC" << 'BASHEOF'
# === CLAUDE LAUNCHER ===
# ── Shared auth validation helpers ──────────────────────────
# _claude_test_api: Send 1-token test to an Anthropic-compatible endpoint
# Usage: _claude_test_api <url> <auth_header> [model]
# auth_header format: "Authorization: Bearer TOKEN" or "x-api-key: KEY"
# Side effects: Sets globals _CLAUDE_TEST_CODE, _CLAUDE_TEST_BODY
_claude_test_api() {
local url="$1" auth_header="$2" model="${3:-claude-sonnet-4-6}"
local response
response=$(curl -s -w "\n%{http_code}" --max-time 15 "$url" \
-H "$auth_header" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d "{\"model\":\"$model\",\"max_tokens\":1,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}" \
2>/dev/null || echo "000")
_CLAUDE_TEST_CODE=$(echo "$response" | tail -1)
_CLAUDE_TEST_BODY=$(echo "$response" | sed '$d')
}
# _claude_extract_error: Extract error.message from Anthropic-style error JSON
_claude_extract_error() {
local body="$1"
echo "$body" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
e = d.get('error', {})
if isinstance(e, str):
print(e)
else:
msg = e.get('message', '')
if msg:
print(msg)
except:
pass
" 2>/dev/null
}
# _claude_offer_reauth: Ask user if they want to re-authenticate
# Returns 0 for yes, 1 for no
_claude_offer_reauth() {
local provider="$1"
echo ""
echo -n "Хотите выполнить повторную авторизацию $provider? [Y/n] "
local answer
read -r answer
case "${answer:-Y}" in
[Yy]|[Yy][Ee][Ss]) return 0 ;;
*) echo "Отменено."; return 1 ;;
esac
}
# ── claude_anthropic ──────────────────────────────────────────
claude_anthropic() {
unset ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN ANTHROPIC_MODEL
unset ANTHROPIC_DEFAULT_OPUS_MODEL ANTHROPIC_DEFAULT_SONNET_MODEL
unset ANTHROPIC_DEFAULT_HAIKU_MODEL CLAUDE_CODE_SUBAGENT_MODEL
unset ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN
unset CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
local _token="" _method=""
# Извлекаем OAuth-токен из credentials
_token=$(python3 -c "
import json, os
try:
with open(os.path.expanduser('~/.claude/.credentials.json')) as f:
d = json.load(f)
print(d.get('claudeAiOauth', {}).get('accessToken', ''))
except: pass
" 2>/dev/null)
if [ -n "$_token" ]; then
_method="oauth"
elif [ -n "${ANTHROPIC_API_KEY:-}" ]; then
_token="$ANTHROPIC_API_KEY"
_method="apikey"
fi
# ── Pre-launch auth validation ──
if [ "$_method" = "oauth" ]; then
# OAuth-токен предназначен для внутренней авторизации Claude Code, а не для прямых API-вызовов.
# Claude Code сам обрабатывает OAuth — прямой тест API с Bearer-токеном некорректен и даёт ложные 429.
: # пропускаем проверку, Claude Code обработает авторизацию самостоятельно
elif [ -n "$_token" ]; then
echo -n "Проверка авторизации Anthropic... "
_claude_test_api "https://api.anthropic.com/v1/messages" "x-api-key: $_token"
local _emsg
case "$_CLAUDE_TEST_CODE" in
200)
echo -e "\033[0;32mOK\033[0m"
;;
401)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
echo -e "\033[0;31mОШИБКА: Недействительная авторизация (HTTP 401)\033[0m"
[ -n "$_emsg" ] && echo " $_emsg"
echo ""
echo "API-ключ недействителен."
echo " export ANTHROPIC_API_KEY=sk-ant-..."
echo ""
read -r -p "Ввести новый ключ сейчас? [y/N] " _ans
case "${_ans:-N}" in
[Yy]*)
read -r -p "Введите ключ: " _token
[ -z "$_token" ] && { echo "Ключ не введён."; return 1; }
echo -n "Проверяю новый ключ... "
_claude_test_api "https://api.anthropic.com/v1/messages" "x-api-key: $_token"
case "$_CLAUDE_TEST_CODE" in
200) echo -e "\033[0;32mOK\033[0m"; ANTHROPIC_API_KEY="$_token" ;;
*) echo -e "\033[0;31mОШИБКА (HTTP $_CLAUDE_TEST_CODE)\033[0m"; return 1 ;;
esac
;;
*) return 1 ;;
esac
;;
403)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
echo ""
echo -e "\033[0;33m[ПРЕДУПРЕЖДЕНИЕ]\033[0m Доступ запрещён (HTTP 403) — возможно, подписка не позволяет."
[ -n "$_emsg" ] && echo " $_emsg"
echo ""
echo -n "Продолжить всё равно? (запросы могут не работать) [y/N] "
local _ans; read -r _ans
case "${_ans:-N}" in [Yy]*) ;; *) return 1 ;; esac
;;
429)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
echo ""
echo -e "\033[0;33m[ЛИМИТ]\033[0m Лимит запросов исчерпан или пустой баланс (HTTP 429)."
[ -n "$_emsg" ] && echo " $_emsg"
echo ""
echo "Варианты:"
echo " [C] Продолжить всё равно (может не работать)"
echo " [Q] Выйти"
echo -n "Выберите [C/q]: "
local _ans; read -r _ans
case "${_ans:-C}" in
[Cc]) ;; # продолжаем
*) return 1 ;;
esac
;;
000)
echo ""
echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить авторизацию (нет сети?). Продолжаю..."
;;
*)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
echo ""
echo -e "\033[0;31m[ОШИБКА]\033[0m API вернул HTTP $_CLAUDE_TEST_CODE"
[ -n "$_emsg" ] && echo " $_emsg"
echo ""
echo -n "Продолжить всё равно? [y/N] "
local _ans; read -r _ans
case "${_ans:-N}" in [Yy]*) ;; *) return 1 ;; esac
;;
esac
else
# Нет ни OAuth, ни API-ключа — предлагаем выбор
echo ""
echo -e "\033[1;33mАнтропная авторизация не найдена.\033[0m"
echo ""
echo "Варианты:"
echo " [L] Залогиниться через браузер (OAuth)"
echo " [K] Ввести API-ключ вручную"
echo " [Q] Выйти"
echo -n "Выберите [L/k/q]: "
local _ans; read -r _ans
case "${_ans:-L}" in
[Ll])
echo ""
echo "Открываю браузер для входа в Anthropic аккаунт..."
claude auth login || {
echo ""
echo -e "\033[0;31mНе удалось выполнить вход.\033[0m"
echo "Попробуйте вручную: claude auth login"
return 1
}
;;
[Kk])
echo ""
read -r -p "Введите Anthropic API ключ (sk-ant-...): " _token
[ -z "$_token" ] && { echo "Ключ не введён."; return 1; }
_method="apikey"
echo -n "Проверяю ключ... "
_claude_test_api "https://api.anthropic.com/v1/messages" "x-api-key: $_token"
case "$_CLAUDE_TEST_CODE" in
200) echo -e "\033[0;32mOK\033[0m"; export ANTHROPIC_API_KEY="$_token" ;;
*) echo -e "\033[0;31mОШИБКА (HTTP $_CLAUDE_TEST_CODE)\033[0m"; return 1 ;;
esac
;;
*) echo "Отменено."; return 1 ;;
esac
fi
# Явно задаём модели Anthropic, чтобы не подхватился deepseek-chat из settings.json
ANTHROPIC_MODEL=claude-sonnet-4-6 \
ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-4-8 \
ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4-6 \
ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-haiku-4-5-20251001 \
CLAUDE_CODE_SUBAGENT_MODEL=claude-haiku-4-5-20251001 \
claude "$@"
}
@@ -183,8 +375,72 @@ claude_gpt() {
if ! pgrep -f "claude-code-proxy serve" &>/dev/null; then
"$proxy_bin" serve &>/tmp/claude-code-proxy.log &
proxy_pid=$!
local _i=0
while [ $_i -lt 10 ]; do
sleep 1
curl -sf --max-time 1 http://localhost:18765/ &>/dev/null; local _ce=$?
[ "$_ce" -ne 7 ] && break # exit 7 = connection refused; любой другой = прокси слушает
_i=$((_i + 1))
done
fi
# Убиваем прокси при любом выходе из функции (early return или нормальный)
trap '[ -n "$proxy_pid" ] && kill "$proxy_pid" 2>/dev/null' RETURN
# ── Pre-launch API validation through proxy ──
echo -n "Проверка авторизации ChatGPT... "
_claude_test_api "http://localhost:18765/v1/messages" "x-api-key: dummy" "gpt-5.4-mini"
local _emsg
case "$_CLAUDE_TEST_CODE" in
200)
echo -e "\033[0;32mOK\033[0m"
;;
401|403)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
echo ""
echo -e "\033[0;31m[ОШИБКА АВТОРИЗАЦИИ]\033[0m Авторизация ChatGPT недействительна (HTTP $_CLAUDE_TEST_CODE)."
[ -n "$_emsg" ] && echo " $_emsg"
echo ""
echo "Очищаю недействительную авторизацию..."
"$proxy_bin" codex auth logout 2>/dev/null
if _claude_offer_reauth "ChatGPT"; then
echo "Запускаю повторную авторизацию..."
if ! "$proxy_bin" codex auth login 2>&1; then
echo ""
echo "Если браузер не открылся, попробуйте device flow:"
echo " claude-code-proxy codex auth device"
return 1
fi
echo -n "Проверяю авторизацию после входа... "
_claude_test_api "http://localhost:18765/v1/messages" "x-api-key: dummy" "gpt-5.4-mini"
if [ "$_CLAUDE_TEST_CODE" != "200" ]; then
echo -e "\033[0;31mОШИБКА (HTTP $_CLAUDE_TEST_CODE)\033[0m"
return 1
fi
echo -e "\033[0;32mOK\033[0m"
else
return 1
fi
;;
429)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
echo ""
echo -e "\033[0;33m[ЛИМИТ ИСЧЕРПАН]\033[0m Лимит ChatGPT исчерпан."
[ -n "$_emsg" ] && echo " $_emsg"
return 1
;;
000)
echo ""
echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить ChatGPT прокси (нет сети?). Продолжаю..."
;;
*)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
echo ""
echo -e "\033[0;31m[ОШИБКА]\033[0m Прокси вернул HTTP $_CLAUDE_TEST_CODE"
[ -n "$_emsg" ] && echo " $_emsg"
return 1
;;
esac
ANTHROPIC_BASE_URL=http://localhost:18765 \
ANTHROPIC_AUTH_TOKEN=dummy \
@@ -205,12 +461,66 @@ claude_gpt() {
# ── claude_deepseek ───────────────────────────────────────────
claude_deepseek() {
local key_file="$HOME/.config/claude-launcher/deepseek_key"
local api_key=""
local api_key="" reauth=0
local _emsg
# Read stored key
if [ -f "$key_file" ]; then
api_key=$(cat "$key_file")
fi
# Validate stored key if present
if [ -n "$api_key" ]; then
echo -n "Проверка сохранённого DeepSeek ключа... "
_claude_test_api "https://api.deepseek.com/anthropic/v1/messages" "Authorization: Bearer $api_key" "deepseek-v4-flash"
case "$_CLAUDE_TEST_CODE" in
200)
echo -e "\033[0;32mOK\033[0m"
;;
401|403)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
echo ""
echo -e "\033[0;31m[ОШИБКА АВТОРИЗАЦИИ]\033[0m Сохранённый ключ недействителен (HTTP $_CLAUDE_TEST_CODE)."
[ -n "$_emsg" ] && echo " $_emsg"
echo "Удаляю недействительный ключ..."
rm -f "$key_file"
api_key=""
reauth=1
;;
429)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
echo ""
echo -e "\033[0;33m[ЛИМИТ ИСЧЕРПАН]\033[0m Баланс DeepSeek пуст или превышен лимит."
[ -n "$_emsg" ] && echo " $_emsg"
echo " Пополните баланс: https://platform.deepseek.com/top_up"
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
local _ans; read -r _ans
case "${_ans:-N}" in [Yy]|[Yy][Ee][Ss]) ;; *) return 1 ;; esac
;;
000)
echo ""
echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить ключ (нет сети?). Продолжаю..."
;;
*)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
echo ""
echo -e "\033[0;31m[ОШИБКА]\033[0m API вернул HTTP $_CLAUDE_TEST_CODE"
[ -n "$_emsg" ] && echo " $_emsg"
return 1
;;
esac
fi
# Re-prompt if key was invalidated
if [ -z "$api_key" ] && [ "$reauth" -eq 1 ]; then
echo ""
echo -n "Хотите ввести новый DeepSeek ключ? [Y/n] "
local _ans; read -r _ans
case "${_ans:-Y}" in [Yy]|[Yy][Ee][Ss]) ;; *) return 1 ;; esac
fi
# Prompt for key if missing
if [ -z "$api_key" ]; then
echo ""
echo "DeepSeek API ключ не найден."
@@ -225,43 +535,42 @@ claude_deepseek() {
fi
echo "Проверяю ключ и баланс..."
local http_code http_body err_msg
http_body=$(curl -s -w "\n%{http_code}" \
https://api.deepseek.com/anthropic/v1/messages \
-H "Authorization: Bearer $api_key" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{"model":"deepseek-v4-flash","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}' 2>/dev/null || echo "000")
http_code=$(echo "$http_body" | tail -1)
http_body=$(echo "$http_body" | sed '$d')
_claude_test_api "https://api.deepseek.com/anthropic/v1/messages" "Authorization: Bearer $api_key" "deepseek-v4-flash"
if [ "$http_code" = "200" ]; then
case "$_CLAUDE_TEST_CODE" in
200)
mkdir -p "$(dirname "$key_file")"
echo "$api_key" > "$key_file"
chmod 600 "$key_file"
echo "Ключ действителен, баланс в порядке. Ключ сохранён."
elif [ "$http_code" = "000" ]; then
echo -e "\033[0;32mКлюч действителен, баланс в порядке. Ключ сохранён.\033[0m"
;;
000)
echo "Не удалось проверить ключ (нет сети?). Сохраняю без проверки..."
mkdir -p "$(dirname "$key_file")"
echo "$api_key" > "$key_file"
chmod 600 "$key_file"
elif [ "$http_code" = "429" ]; then
err_msg=$(echo "$http_body" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('error',{}).get('message',''))" 2>/dev/null || echo "")
;;
429)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
echo ""
echo -e "\033[0;31m[ОШИБКА]\033[0m Ключ действителен, но аккаунт заблокирован."
echo " Причина: $err_msg"
[ -n "$_emsg" ] && echo " Причина: $_emsg"
echo " Пополните баланс: https://platform.deepseek.com/top_up"
echo " Ключ НЕ сохранён — сначала пополните счёт."
return 1
elif [ "$http_code" = "401" ] || [ "$http_code" = "403" ]; then
echo "Ключ недействителен (HTTP $http_code). Ключ не сохранён."
;;
401|403)
echo -e "\033[0;31mКлюч недействителен (HTTP $_CLAUDE_TEST_CODE).\033[0m Ключ не сохранён."
return 1
else
err_msg=$(echo "$http_body" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('error',{}).get('message',''))" 2>/dev/null || echo "$http_body")
echo "Ошибка API (HTTP $http_code): $err_msg"
;;
*)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
[ -z "$_emsg" ] && _emsg="HTTP $_CLAUDE_TEST_CODE"
echo "Ошибка API: $_emsg"
echo "Ключ не сохранён."
return 1
fi
;;
esac
fi
ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic \
@@ -278,12 +587,66 @@ claude_deepseek() {
# ── claude_kimi ─────────────────────────────────────────────
claude_kimi() {
local key_file="$HOME/.config/claude-launcher/kimi_key"
local api_key=""
local api_key="" reauth=0
local _emsg
# Read stored key
if [ -f "$key_file" ]; then
api_key=$(cat "$key_file")
fi
# Validate stored key if present
if [ -n "$api_key" ]; then
echo -n "Проверка сохранённого Kimi ключа... "
_claude_test_api "https://api.moonshot.ai/anthropic/v1/messages" "Authorization: Bearer $api_key" "kimi-k2.6"
case "$_CLAUDE_TEST_CODE" in
200)
echo -e "\033[0;32mOK\033[0m"
;;
401|403)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
echo ""
echo -e "\033[0;31m[ОШИБКА АВТОРИЗАЦИИ]\033[0m Сохранённый ключ недействителен (HTTP $_CLAUDE_TEST_CODE)."
[ -n "$_emsg" ] && echo " $_emsg"
echo "Удаляю недействительный ключ..."
rm -f "$key_file"
api_key=""
reauth=1
;;
429)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
echo ""
echo -e "\033[0;33m[ЛИМИТ ИСЧЕРПАН]\033[0m Баланс Kimi пуст или превышен лимит."
[ -n "$_emsg" ] && echo " $_emsg"
echo " Пополните баланс: https://platform.moonshot.ai/console/billing"
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
local _ans; read -r _ans
case "${_ans:-N}" in [Yy]|[Yy][Ee][Ss]) ;; *) return 1 ;; esac
;;
000)
echo ""
echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить ключ (нет сети?). Продолжаю..."
;;
*)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
echo ""
echo -e "\033[0;31m[ОШИБКА]\033[0m API вернул HTTP $_CLAUDE_TEST_CODE"
[ -n "$_emsg" ] && echo " $_emsg"
return 1
;;
esac
fi
# Re-prompt if key was invalidated
if [ -z "$api_key" ] && [ "$reauth" -eq 1 ]; then
echo ""
echo -n "Хотите ввести новый Kimi ключ? [Y/n] "
local _ans; read -r _ans
case "${_ans:-Y}" in [Yy]|[Yy][Ee][Ss]) ;; *) return 1 ;; esac
fi
# Prompt for key if missing
if [ -z "$api_key" ]; then
echo ""
echo "Kimi (Moonshot AI) API ключ не найден."
@@ -298,43 +661,42 @@ claude_kimi() {
fi
echo "Проверяю ключ и баланс..."
local http_code http_body err_msg
http_body=$(curl -s -w "\n%{http_code}" \
https://api.moonshot.ai/anthropic/v1/messages \
-H "Authorization: Bearer $api_key" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{"model":"kimi-k2.6","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}' 2>/dev/null || echo "000")
http_code=$(echo "$http_body" | tail -1)
http_body=$(echo "$http_body" | sed '$d')
_claude_test_api "https://api.moonshot.ai/anthropic/v1/messages" "Authorization: Bearer $api_key" "kimi-k2.6"
if [ "$http_code" = "200" ]; then
case "$_CLAUDE_TEST_CODE" in
200)
mkdir -p "$(dirname "$key_file")"
echo "$api_key" > "$key_file"
chmod 600 "$key_file"
echo "Ключ действителен, баланс в порядке. Ключ сохранён."
elif [ "$http_code" = "000" ]; then
echo -e "\033[0;32mКлюч действителен, баланс в порядке. Ключ сохранён.\033[0m"
;;
000)
echo "Не удалось проверить ключ (нет сети?). Сохраняю без проверки..."
mkdir -p "$(dirname "$key_file")"
echo "$api_key" > "$key_file"
chmod 600 "$key_file"
elif [ "$http_code" = "429" ]; then
err_msg=$(echo "$http_body" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('error',{}).get('message',''))" 2>/dev/null || echo "")
;;
429)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
echo ""
echo -e "\033[0;31m[ОШИБКА]\033[0m Ключ действителен, но аккаунт заблокирован."
echo " Причина: $err_msg"
[ -n "$_emsg" ] && echo " Причина: $_emsg"
echo " Пополните баланс: https://platform.moonshot.ai/console/billing"
echo " Ключ НЕ сохранён — сначала пополните счёт."
return 1
elif [ "$http_code" = "401" ] || [ "$http_code" = "403" ]; then
echo "Ключ недействителен (HTTP $http_code). Ключ не сохранён."
;;
401|403)
echo -e "\033[0;31mКлюч недействителен (HTTP $_CLAUDE_TEST_CODE).\033[0m Ключ не сохранён."
return 1
else
err_msg=$(echo "$http_body" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('error',{}).get('message',''))" 2>/dev/null || echo "$http_body")
echo "Ошибка API (HTTP $http_code): $err_msg"
;;
*)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
[ -z "$_emsg" ] && _emsg="HTTP $_CLAUDE_TEST_CODE"
echo "Ошибка API: $_emsg"
echo "Ключ не сохранён."
return 1
fi
;;
esac
fi
ANTHROPIC_BASE_URL=https://api.moonshot.ai/anthropic \
@@ -373,15 +735,32 @@ claude_gemini() {
done
fi
# Проверяем наличие аккаунта
local acc_count
acc_count=$(curl -sf "http://localhost:8080/v1/models" 2>/dev/null | \
python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d.get('data',[],'')))" 2>/dev/null || echo "0")
local has_auth
# ── Проверка аккаунтов ──
local has_auth total_count invalid_count
has_auth=$(curl -sf "http://localhost:8080/account-limits" 2>/dev/null || echo "")
if [ -z "$has_auth" ] || echo "$has_auth" | grep -qE '"accounts":\s*\[\]'; then
if [ -n "$has_auth" ]; then
total_count=$(echo "$has_auth" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
print(len(d.get('accounts', [])))
except:
print(0)
" 2>/dev/null || echo "0")
invalid_count=$(echo "$has_auth" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
accounts = d.get('accounts', [])
invalid = [a for a in accounts if a.get('isInvalid')]
print(len(invalid))
except:
print(0)
" 2>/dev/null || echo "0")
fi
if [ -z "$has_auth" ] || [ "$total_count" = "0" ]; then
echo ""
echo "Google-аккаунт не найден."
echo ""
@@ -391,15 +770,86 @@ claude_gemini() {
echo "Открываю http://localhost:8080 в браузере..."
echo "Перейдите: Accounts → Add Account → войдите через Google."
echo ""
# Открываем браузер
xdg-open "http://localhost:8080" 2>/dev/null || \
sensible-browser "http://localhost:8080" 2>/dev/null || \
echo "Откройте вручную: http://localhost:8080"
echo "Нажмите Enter после завершения авторизации в браузере..."
read -r
elif [ "$invalid_count" -gt 0 ]; then
echo ""
echo -e "\033[0;33m[ПРЕДУПРЕЖДЕНИЕ]\033[0m Обнаружены проблемные аккаунты ($invalid_count из $total_count)."
echo "Проверьте статус: http://localhost:8080"
fi
# ── Pre-launch API test through proxy ──
echo -n "Проверка доступа к Gemini API... "
_claude_test_api "http://localhost:8080/v1/messages" "x-api-key: dummy" "gemini-3-flash-agent"
local _emsg
case "$_CLAUDE_TEST_CODE" in
200)
echo -e "\033[0;32mOK\033[0m"
;;
401|403)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
echo ""
echo -e "\033[0;31m[ОШИБКА АВТОРИЗАЦИИ]\033[0m Gemini аккаунты не авторизованы (HTTP $_CLAUDE_TEST_CODE)."
[ -n "$_emsg" ] && echo " $_emsg"
echo "Попробуйте переавторизоваться через http://localhost:8080"
if _claude_offer_reauth "Gemini"; then
xdg-open "http://localhost:8080" 2>/dev/null || \
sensible-browser "http://localhost:8080" 2>/dev/null || \
echo "Откройте http://localhost:8080"
echo "Нажмите Enter после авторизации..."
read -r
echo -n "Проверяю авторизацию Gemini... "
_claude_test_api "http://localhost:8080/v1/messages" "x-api-key: dummy" "gemini-3-flash-agent"
if [ "$_CLAUDE_TEST_CODE" != "200" ]; then
echo -e "\033[0;31mОШИБКА (HTTP $_CLAUDE_TEST_CODE)\033[0m"
return 1
fi
echo -e "\033[0;32mOK\033[0m"
else
return 1
fi
;;
429)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
echo ""
echo -e "\033[0;33m[ЛИМИТ ИСЧЕРПАН]\033[0m Все Gemini аккаунты исчерпали лимит."
[ -n "$_emsg" ] && echo " $_emsg"
echo "Подождите сброса лимитов или добавьте новый аккаунт."
if _claude_offer_reauth "Gemini (добавить аккаунт)"; then
xdg-open "http://localhost:8080" 2>/dev/null || \
sensible-browser "http://localhost:8080" 2>/dev/null || \
echo "Откройте http://localhost:8080"
echo "Нажмите Enter после добавления..."
read -r
echo -n "Проверяю авторизацию Gemini... "
_claude_test_api "http://localhost:8080/v1/messages" "x-api-key: dummy" "gemini-3-flash-agent"
if [ "$_CLAUDE_TEST_CODE" != "200" ]; then
echo -e "\033[0;31mОШИБКА (HTTP $_CLAUDE_TEST_CODE)\033[0m"
return 1
fi
echo -e "\033[0;32mOK\033[0m"
else
return 1
fi
;;
000)
echo ""
echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить Gemini прокси (нет сети?). Продолжаю..."
;;
*)
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
echo ""
echo -e "\033[0;31m[ОШИБКА]\033[0m Прокси вернул HTTP $_CLAUDE_TEST_CODE"
[ -n "$_emsg" ] && echo " $_emsg"
return 1
;;
esac
echo "Запускаю Claude Code с Gemini..."
ANTHROPIC_BASE_URL=http://localhost:8080 \

304
tests/test_fixes.sh Executable file
View File

@@ -0,0 +1,304 @@
#!/usr/bin/env bash
# tests/test_fixes.sh — unit tests for code-review fixes in claude_setup.sh
# Run: bash tests/test_fixes.sh
# Requires: bash 4+, curl (can be mocked via PATH)
set -euo pipefail
SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/claude_setup.sh"
PASS=0; FAIL=0
ok() { echo "[PASS] $1"; PASS=$((PASS+1)); }
fail() { echo "[FAIL] $1"; FAIL=$((FAIL+1)); }
# ── helpers ──────────────────────────────────────────────────────────────────
# Source only the heredoc functions, not the setup-script body.
# The heredoc begins after "cat >> \"$BASHRC\" << 'BASHEOF'" and contains
# all the launcher functions; we extract and source that block directly.
_source_functions() {
local tmp
tmp=$(mktemp)
awk '/^# === CLAUDE LAUNCHER ===/,/^# === END CLAUDE LAUNCHER ===/' "$SCRIPT" > "$tmp"
# shellcheck disable=SC1090
source "$tmp"
rm -f "$tmp"
}
# ── Fix 1: ANTHROPIC_API_KEY exported in manual-key path ────────────────────
test_fix1_export_api_key() {
# Extract the [Kk] branch from the script and confirm `export` keyword exists
local kk_block
kk_block=$(awk '/\[Kk\]/,/\[Ll\]/' "$SCRIPT" | grep 'ANTHROPIC_API_KEY')
if echo "$kk_block" | grep -q 'export ANTHROPIC_API_KEY'; then
ok "Fix1: [K] branch uses 'export ANTHROPIC_API_KEY'"
else
fail "Fix1: [K] branch missing 'export' for ANTHROPIC_API_KEY"
fi
}
# ── Fix 2: trap RETURN kills proxy on early exit ─────────────────────────────
test_fix2_trap_return() {
if grep -q "trap '.*kill.*proxy_pid.*' RETURN" "$SCRIPT"; then
ok "Fix2: trap RETURN for proxy cleanup present in claude_gpt"
else
fail "Fix2: trap RETURN for proxy cleanup missing in claude_gpt"
fi
}
# ── Fix 3: readiness loop replaces bare sleep 1 ──────────────────────────────
test_fix3_readiness_loop() {
# The old code had just "sleep 1" after starting proxy; now there's a while loop
local gpt_section
gpt_section=$(awk '/^claude_gpt\(\)/,/^}/' "$SCRIPT")
if echo "$gpt_section" | grep -q 'while \[ \$_i -lt'; then
ok "Fix3: readiness poll loop present in claude_gpt proxy start"
else
fail "Fix3: readiness poll loop missing in claude_gpt"
fi
# Confirm bare "sleep 1" is gone from the proxy-start section (the loop contains sleep 1 but in context)
# The old pattern was: proxy_pid=$!\n sleep 1\n fi
if echo "$gpt_section" | grep -qP 'proxy_pid=\$!\n\s+sleep 1\n\s+fi'; then
fail "Fix3: bare 'sleep 1' still present right after proxy_pid=\$!"
else
ok "Fix3: bare 'sleep 1; fi' pattern removed"
fi
}
# ── Fix 3b: curl exit-7 logic correct ────────────────────────────────────────
test_fix3b_exit7_logic() {
# Verify the comment and condition are as expected
if grep -q 'exit 7 = connection refused' "$SCRIPT"; then
ok "Fix3b: exit-7 comment present (connection refused check documented)"
else
fail "Fix3b: exit-7 comment missing"
fi
if grep -q '_ce.*-ne 7' "$SCRIPT"; then
ok "Fix3b: [ \$_ce -ne 7 ] break condition present"
else
fail "Fix3b: exit-7 break condition missing"
fi
}
# ── Fix 4: re-validate after claude_gpt reauth ───────────────────────────────
test_fix4_gpt_revalidate() {
local gpt_section
gpt_section=$(awk '/^claude_gpt\(\)/,/^}/' "$SCRIPT")
if echo "$gpt_section" | grep -q 'Проверяю авторизацию после входа'; then
ok "Fix4: re-validate after codex auth login present in claude_gpt"
else
fail "Fix4: re-validate after codex auth login missing in claude_gpt"
fi
}
# ── Fix 5: re-validate after claude_gemini reauth (both 401 and 429) ─────────
test_fix5_gemini_revalidate() {
local gemini_section
gemini_section=$(awk '/^claude_gemini\(\)/,/^}/' "$SCRIPT")
local count
count=$(echo "$gemini_section" | grep -c 'Проверяю авторизацию Gemini' || true)
if [ "$count" -ge 2 ]; then
ok "Fix5: re-validate after gemini reauth present in both 401/403 and 429 branches ($count occurrences)"
else
fail "Fix5: re-validate after gemini reauth missing or only in one branch (found $count)"
fi
}
# ── Fix 6: prompt [C/q] matches default C in 429 handler ─────────────────────
test_fix6_prompt_default() {
# The prompt should now show [C/q] (capital C = default) matching case "${_ans:-C}"
if grep -q '\[C/q\]' "$SCRIPT"; then
ok "Fix6: prompt shows [C/q] — capital C signals default=continue"
else
fail "Fix6: prompt still shows [c/Q] (misleading) or was changed incorrectly"
fi
# Confirm the default in case is still C
local anthropic_section
anthropic_section=$(awk '/^claude_anthropic\(\)/,/^}/' "$SCRIPT")
if echo "$anthropic_section" | grep -q '${_ans:-C}'; then
ok "Fix6: default in case is still C (continue on Enter)"
else
fail "Fix6: default in case changed unexpectedly"
fi
}
# ── Fix 7: trap quotes $TMP correctly ────────────────────────────────────────
test_fix7_trap_tmp() {
# Should be single-quoted trap so $TMP expands at execution, not definition
if grep -q "trap 'rm -rf \"\$TMP\"' EXIT" "$SCRIPT"; then
ok "Fix7: trap uses single quotes with quoted \"\$TMP\""
else
fail "Fix7: trap still uses double quotes or $TMP still unquoted at execution"
fi
# Old bad form should be gone
if grep -q 'trap "rm -rf \$TMP" EXIT' "$SCRIPT"; then
fail "Fix7: old unquoted trap form still present"
else
ok "Fix7: old unquoted trap form removed"
fi
}
# ── _claude_test_api function isolation ──────────────────────────────────────
test_globals_set_by_test_api() {
_source_functions
# Mock curl to return a fake 200 response
curl() {
echo '{"id":"msg_test"}'
echo "200"
}
export -f curl
_CLAUDE_TEST_CODE=""
_CLAUDE_TEST_BODY=""
_claude_test_api "http://fake.api/v1/messages" "x-api-key: testkey" "test-model"
if [ "$_CLAUDE_TEST_CODE" = "200" ]; then
ok "test_api: _CLAUDE_TEST_CODE set to 200"
else
fail "test_api: _CLAUDE_TEST_CODE='$_CLAUDE_TEST_CODE' expected '200'"
fi
if echo "$_CLAUDE_TEST_BODY" | grep -q '"id"'; then
ok "test_api: _CLAUDE_TEST_BODY contains response body"
else
fail "test_api: _CLAUDE_TEST_BODY='$_CLAUDE_TEST_BODY' missing body"
fi
unset -f curl
}
test_globals_set_on_curl_fail() {
_source_functions
# curl fails → fallback "000"
curl() { return 1; }
export -f curl
_CLAUDE_TEST_CODE=""
_claude_test_api "http://unreachable/" "x-api-key: k" "m"
if [ "$_CLAUDE_TEST_CODE" = "000" ]; then
ok "test_api: _CLAUDE_TEST_CODE=000 on curl failure"
else
fail "test_api: expected 000 on curl failure, got '$_CLAUDE_TEST_CODE'"
fi
unset -f curl
}
# ── _claude_extract_error ────────────────────────────────────────────────────
test_extract_error_message() {
_source_functions
local body='{"type":"error","error":{"type":"authentication_error","message":"Invalid API key"}}'
local result
result=$(_claude_extract_error "$body")
if [ "$result" = "Invalid API key" ]; then
ok "extract_error: extracts error.message from JSON"
else
fail "extract_error: expected 'Invalid API key', got '$result'"
fi
}
test_extract_error_empty_body() {
_source_functions
local result
result=$(_claude_extract_error "not json at all")
if [ -z "$result" ]; then
ok "extract_error: returns empty string on non-JSON input"
else
fail "extract_error: unexpected output '$result' on bad input"
fi
}
# ── _claude_offer_reauth ─────────────────────────────────────────────────────
test_offer_reauth_yes() {
_source_functions
# Simulate user typing "Y"
local result
result=$(echo "Y" | (
_claude_offer_reauth "TestProvider"
echo "retcode:$?"
))
if echo "$result" | grep -q "retcode:0"; then
ok "offer_reauth: returns 0 on 'Y'"
else
fail "offer_reauth: expected retcode 0 on Y, got: $result"
fi
}
test_offer_reauth_no() {
_source_functions
local result
result=$(echo "n" | (
_claude_offer_reauth "TestProvider"
echo "retcode:$?"
))
if echo "$result" | grep -q "retcode:1"; then
ok "offer_reauth: returns 1 on 'n'"
else
fail "offer_reauth: expected retcode 1 on n, got: $result"
fi
}
test_offer_reauth_enter_defaults_yes() {
_source_functions
local result
result=$(echo "" | (
_claude_offer_reauth "TestProvider"
echo "retcode:$?"
))
if echo "$result" | grep -q "retcode:0"; then
ok "offer_reauth: Enter (empty) defaults to Yes (retcode 0)"
else
fail "offer_reauth: Enter should default to Yes, got: $result"
fi
}
# ── bash syntax of the whole script ─────────────────────────────────────────
test_script_syntax() {
if bash -n "$SCRIPT" 2>&1; then
ok "syntax: claude_setup.sh passes 'bash -n'"
else
fail "syntax: claude_setup.sh has syntax errors"
fi
}
# ── run all tests ─────────────────────────────────────────────────────────────
test_script_syntax
test_fix1_export_api_key
test_fix2_trap_return
test_fix3_readiness_loop
test_fix3b_exit7_logic
test_fix4_gpt_revalidate
test_fix5_gemini_revalidate
test_fix6_prompt_default
test_fix7_trap_tmp
test_globals_set_by_test_api
test_globals_set_on_curl_fail
test_extract_error_message
test_extract_error_empty_body
test_offer_reauth_yes
test_offer_reauth_no
test_offer_reauth_enter_defaults_yes
echo ""
echo "Results: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] && exit 0 || exit 1