From 048f6b1770a75db1526974e1d72bc386ab24ebc6 Mon Sep 17 00:00:00 2001 From: vitaly Date: Sun, 31 May 2026 00:53:53 +0700 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=207=20=D0=B1=D0=B0=D0=B3=D0=BE=D0=B2=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=81=D0=BB=D0=B5=20code=20review:=20=D1=8D=D0=BA=D1=81?= =?UTF-8?q?=D0=BF=D0=BE=D1=80=D1=82=20=D0=BA=D0=BB=D1=8E=D1=87=D0=B0,=20?= =?UTF-8?q?=D1=83=D1=82=D0=B5=D1=87=D0=BA=D0=B0=20=D0=BF=D1=80=D0=BE=D0=BA?= =?UTF-8?q?=D1=81=D0=B8,=20re-=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- claude_setup.sh | 624 ++++++++++++++++++++++++++++++++++++++------ tests/test_fixes.sh | 304 +++++++++++++++++++++ 2 files changed, 841 insertions(+), 87 deletions(-) mode change 100644 => 100755 claude_setup.sh create mode 100755 tests/test_fixes.sh diff --git a/claude_setup.sh b/claude_setup.sh old mode 100644 new mode 100755 index 5201e7e..cca8c23 --- a/claude_setup.sh +++ b/claude_setup.sh @@ -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 [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=$! - sleep 1 + 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 - mkdir -p "$(dirname "$key_file")" - echo "$api_key" > "$key_file" - chmod 600 "$key_file" - echo "Ключ действителен, баланс в порядке. Ключ сохранён." - elif [ "$http_code" = "000" ]; then - 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 "") - echo "" - echo -e "\033[0;31m[ОШИБКА]\033[0m Ключ действителен, но аккаунт заблокирован." - echo " Причина: $err_msg" - echo " Пополните баланс: https://platform.deepseek.com/top_up" - echo " Ключ НЕ сохранён — сначала пополните счёт." - return 1 - elif [ "$http_code" = "401" ] || [ "$http_code" = "403" ]; then - echo "Ключ недействителен (HTTP $http_code). Ключ не сохранён." - 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" - echo "Ключ не сохранён." - return 1 - fi + case "$_CLAUDE_TEST_CODE" in + 200) + mkdir -p "$(dirname "$key_file")" + echo "$api_key" > "$key_file" + chmod 600 "$key_file" + echo -e "\033[0;32mКлюч действителен, баланс в порядке. Ключ сохранён.\033[0m" + ;; + 000) + echo "Не удалось проверить ключ (нет сети?). Сохраняю без проверки..." + mkdir -p "$(dirname "$key_file")" + echo "$api_key" > "$key_file" + chmod 600 "$key_file" + ;; + 429) + _emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY") + echo "" + echo -e "\033[0;31m[ОШИБКА]\033[0m Ключ действителен, но аккаунт заблокирован." + [ -n "$_emsg" ] && echo " Причина: $_emsg" + echo " Пополните баланс: https://platform.deepseek.com/top_up" + echo " Ключ НЕ сохранён — сначала пополните счёт." + return 1 + ;; + 401|403) + echo -e "\033[0;31mКлюч недействителен (HTTP $_CLAUDE_TEST_CODE).\033[0m Ключ не сохранён." + return 1 + ;; + *) + _emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY") + [ -z "$_emsg" ] && _emsg="HTTP $_CLAUDE_TEST_CODE" + echo "Ошибка API: $_emsg" + echo "Ключ не сохранён." + return 1 + ;; + 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 - mkdir -p "$(dirname "$key_file")" - echo "$api_key" > "$key_file" - chmod 600 "$key_file" - echo "Ключ действителен, баланс в порядке. Ключ сохранён." - elif [ "$http_code" = "000" ]; then - 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 "") - echo "" - echo -e "\033[0;31m[ОШИБКА]\033[0m Ключ действителен, но аккаунт заблокирован." - echo " Причина: $err_msg" - echo " Пополните баланс: https://platform.moonshot.ai/console/billing" - echo " Ключ НЕ сохранён — сначала пополните счёт." - return 1 - elif [ "$http_code" = "401" ] || [ "$http_code" = "403" ]; then - echo "Ключ недействителен (HTTP $http_code). Ключ не сохранён." - 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" - echo "Ключ не сохранён." - return 1 - fi + case "$_CLAUDE_TEST_CODE" in + 200) + mkdir -p "$(dirname "$key_file")" + echo "$api_key" > "$key_file" + chmod 600 "$key_file" + echo -e "\033[0;32mКлюч действителен, баланс в порядке. Ключ сохранён.\033[0m" + ;; + 000) + echo "Не удалось проверить ключ (нет сети?). Сохраняю без проверки..." + mkdir -p "$(dirname "$key_file")" + echo "$api_key" > "$key_file" + chmod 600 "$key_file" + ;; + 429) + _emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY") + echo "" + echo -e "\033[0;31m[ОШИБКА]\033[0m Ключ действителен, но аккаунт заблокирован." + [ -n "$_emsg" ] && echo " Причина: $_emsg" + echo " Пополните баланс: https://platform.moonshot.ai/console/billing" + echo " Ключ НЕ сохранён — сначала пополните счёт." + return 1 + ;; + 401|403) + echo -e "\033[0;31mКлюч недействителен (HTTP $_CLAUDE_TEST_CODE).\033[0m Ключ не сохранён." + return 1 + ;; + *) + _emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY") + [ -z "$_emsg" ] && _emsg="HTTP $_CLAUDE_TEST_CODE" + echo "Ошибка API: $_emsg" + echo "Ключ не сохранён." + return 1 + ;; + 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 \ diff --git a/tests/test_fixes.sh b/tests/test_fixes.sh new file mode 100755 index 0000000..138a4b3 --- /dev/null +++ b/tests/test_fixes.sh @@ -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