Исправить 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:
624
claude_setup.sh
Normal file → Executable file
624
claude_setup.sh
Normal file → Executable 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=$!
|
||||
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 \
|
||||
|
||||
Reference in New Issue
Block a user