906 lines
36 KiB
Bash
Executable File
906 lines
36 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ============================================================
|
||
# Claude Code Setup — Anthropic / GPT-5.5 / DeepSeek / Kimi / Gemini
|
||
# Запуск: bash claude_setup.sh
|
||
# ============================================================
|
||
|
||
BASHRC="$HOME/.bashrc"
|
||
CONFIG_DIR="$HOME/.config/claude-launcher"
|
||
DEEPSEEK_KEY_FILE="$CONFIG_DIR/deepseek_key"
|
||
NPM_GLOBAL="$HOME/.npm-global"
|
||
PROXY_BIN="$HOME/.local/bin/claude-code-proxy"
|
||
|
||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
|
||
success() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||
err() { echo -e "${RED}[ERR]${NC} $*"; exit 1; }
|
||
|
||
# Запрет запуска от root
|
||
if [ "$EUID" -eq 0 ]; then
|
||
echo -e "${RED}Не запускайте этот скрипт через sudo!${NC}"
|
||
echo "Запустите просто: bash claude_setup.sh"
|
||
exit 1
|
||
fi
|
||
|
||
# ── 1. npm prefix в домашнюю папку ──────────────────────────
|
||
info "Настраиваю npm prefix..."
|
||
mkdir -p "$NPM_GLOBAL"
|
||
npm config set prefix "$NPM_GLOBAL"
|
||
success "npm prefix -> $NPM_GLOBAL"
|
||
|
||
# Добавляем npm-global в PATH прямо сейчас (для этого запуска скрипта)
|
||
export PATH="$NPM_GLOBAL/bin:$HOME/.local/bin:$PATH"
|
||
|
||
# Прописываем в .bashrc если ещё нет
|
||
if ! grep -q 'NPM_GLOBAL' "$BASHRC" 2>/dev/null; then
|
||
cat >> "$BASHRC" << 'PATHEOF'
|
||
|
||
# npm global без sudo
|
||
export NPM_GLOBAL="$HOME/.npm-global"
|
||
export PATH="$NPM_GLOBAL/bin:$HOME/.local/bin:$PATH"
|
||
PATHEOF
|
||
success "npm PATH добавлен в $BASHRC"
|
||
else
|
||
success "npm PATH уже есть в $BASHRC"
|
||
fi
|
||
|
||
# ── 2. Node.js ───────────────────────────────────────────────
|
||
info "Проверяю Node.js..."
|
||
if ! command -v node &>/dev/null; then
|
||
info "Устанавливаю Node.js (нужен sudo)..."
|
||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||
sudo apt-get install -y nodejs
|
||
fi
|
||
success "Node.js $(node --version)"
|
||
|
||
# ── 3. Claude Code ───────────────────────────────────────────
|
||
info "Проверяю Claude Code..."
|
||
if ! command -v claude &>/dev/null; then
|
||
info "Устанавливаю Claude Code..."
|
||
# Пробуем официальный инсталлер
|
||
if curl -fsSL https://claude.ai/install.sh | bash 2>/dev/null; then
|
||
success "Claude Code установлен (официальный инсталлер)"
|
||
else
|
||
# Fallback: npm в наш prefix (без sudo)
|
||
npm install -g @anthropic-ai/claude-code
|
||
success "Claude Code установлен (npm)"
|
||
fi
|
||
else
|
||
success "Claude Code уже установлен: $(claude --version 2>/dev/null | head -1)"
|
||
fi
|
||
|
||
# ── 4. claude-code-proxy (GPT) ───────────────────────────────
|
||
mkdir -p "$HOME/.local/bin"
|
||
|
||
install_proxy() {
|
||
info "Устанавливаю claude-code-proxy..."
|
||
ARCH=$(uname -m)
|
||
case "$ARCH" in
|
||
x86_64) ARCH_TAG="amd64" ;;
|
||
aarch64) ARCH_TAG="arm64" ;;
|
||
*) err "Неизвестная архитектура: $ARCH" ;;
|
||
esac
|
||
|
||
LATEST=$(curl -fsSL "https://api.github.com/repos/raine/claude-code-proxy/releases/latest" \
|
||
| grep '"tag_name"' | sed 's/.*"tag_name": *"\(.*\)".*/\1/')
|
||
[ -z "$LATEST" ] && err "Не удалось получить версию claude-code-proxy с GitHub"
|
||
|
||
TMP=$(mktemp -d)
|
||
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"
|
||
tar -xzf "$TMP/proxy.tar.gz" -C "$TMP"
|
||
BINARY=$(find "$TMP" -name "claude-code-proxy" -type f | head -1)
|
||
[ -z "$BINARY" ] && err "Бинарник не найден в архиве"
|
||
cp "$BINARY" "$PROXY_BIN"
|
||
chmod +x "$PROXY_BIN"
|
||
success "claude-code-proxy $LATEST -> $PROXY_BIN"
|
||
}
|
||
|
||
if [ -f "$PROXY_BIN" ]; then
|
||
CURRENT_VER=$("$PROXY_BIN" --version 2>/dev/null | head -1 || echo "unknown")
|
||
success "claude-code-proxy уже установлен ($CURRENT_VER)"
|
||
else
|
||
install_proxy
|
||
fi
|
||
|
||
# ── 4b. effort-proxy wrapper (патч xhigh→max для claude-code-proxy) ─────────
|
||
EFFORT_PROXY_BIN="$HOME/.local/bin/claude-gpt-effort-proxy.py"
|
||
cat > "$EFFORT_PROXY_BIN" << 'PYEOF'
|
||
#!/usr/bin/env python3
|
||
"""Reverse proxy: rewrites "xhigh" effort → "max" for claude-code-proxy (bug in ≤0.0.13)."""
|
||
import http.client, http.server, sys
|
||
|
||
UPSTREAM_PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 18766
|
||
LISTEN_PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 18765
|
||
|
||
class _Proxy(http.server.BaseHTTPRequestHandler):
|
||
def proxy_request(self):
|
||
body = b""
|
||
if cl := self.headers.get("Content-Length"):
|
||
body = self.rfile.read(int(cl))
|
||
body = body.replace(b'"xhigh"', b'"max"')
|
||
try:
|
||
conn = http.client.HTTPConnection("localhost", UPSTREAM_PORT, timeout=300)
|
||
hdrs = dict(self.headers)
|
||
hdrs.pop("Host", None)
|
||
hdrs["Content-Length"] = str(len(body))
|
||
conn.request(self.command, self.path, body=body or None, headers=hdrs)
|
||
resp = conn.getresponse()
|
||
self.send_response(resp.status, resp.reason)
|
||
chunked = False
|
||
for k, v in resp.getheaders():
|
||
if k.lower() == "transfer-encoding" and "chunked" in v.lower():
|
||
chunked = True
|
||
self.send_header(k, v)
|
||
self.end_headers()
|
||
while chunk := resp.read(4096):
|
||
try:
|
||
if chunked:
|
||
self.wfile.write(f"{len(chunk):X}\r\n".encode() + chunk + b"\r\n")
|
||
else:
|
||
self.wfile.write(chunk)
|
||
self.wfile.flush()
|
||
except (BrokenPipeError, ConnectionResetError):
|
||
break
|
||
if chunked:
|
||
try: self.wfile.write(b"0\r\n\r\n")
|
||
except: pass
|
||
conn.close()
|
||
except Exception as e:
|
||
try: self.send_error(502, str(e))
|
||
except: pass
|
||
do_GET = do_POST = do_PUT = do_DELETE = do_HEAD = proxy_request
|
||
def log_message(self, *args): pass
|
||
|
||
http.server.HTTPServer(("localhost", LISTEN_PORT), _Proxy).serve_forever()
|
||
PYEOF
|
||
chmod +x "$EFFORT_PROXY_BIN"
|
||
success "claude-gpt-effort-proxy -> $EFFORT_PROXY_BIN"
|
||
|
||
# ── 5. antigravity-claude-proxy (Gemini) ────────────────────
|
||
info "Проверяю antigravity-claude-proxy..."
|
||
if command -v antigravity-claude-proxy &>/dev/null || command -v acc &>/dev/null; then
|
||
success "antigravity-claude-proxy уже установлен"
|
||
else
|
||
info "Устанавливаю antigravity-claude-proxy (npm, без sudo)..."
|
||
npm install -g antigravity-claude-proxy@latest
|
||
success "antigravity-claude-proxy установлен"
|
||
fi
|
||
|
||
# ── 6. Папка для конфигов ────────────────────────────────────
|
||
mkdir -p "$CONFIG_DIR"
|
||
|
||
# ── 7. Функции в .bashrc ─────────────────────────────────────
|
||
info "Прописываю функции запуска в $BASHRC..."
|
||
|
||
MARKER="# === CLAUDE LAUNCHER ==="
|
||
|
||
if grep -q "$MARKER" "$BASHRC" 2>/dev/null; then
|
||
warn "Блок уже есть в $BASHRC — обновляю..."
|
||
python3 - "$BASHRC" <<'PYEOF'
|
||
import sys
|
||
path = sys.argv[1]
|
||
with open(path, 'r') as f:
|
||
content = f.read()
|
||
marker = '# === CLAUDE LAUNCHER ==='
|
||
end_marker = '# === END CLAUDE LAUNCHER ==='
|
||
start = content.find(marker)
|
||
end = content.find(end_marker)
|
||
if start != -1 and end != -1:
|
||
new_content = content[:start] + content[end + len(end_marker):].lstrip('\n')
|
||
with open(path, 'w') as f:
|
||
f.write(new_content)
|
||
PYEOF
|
||
fi
|
||
|
||
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_gpt ────────────────────────────────────────────────
|
||
claude_gpt() {
|
||
local proxy_bin="$HOME/.local/bin/claude-code-proxy"
|
||
|
||
if [ ! -f "$proxy_bin" ]; then
|
||
echo "Ошибка: claude-code-proxy не найден. Перезапустите claude_setup.sh"
|
||
return 1
|
||
fi
|
||
|
||
# Проверяем авторизацию
|
||
if ! "$proxy_bin" codex auth status &>/dev/null; then
|
||
echo ""
|
||
echo "Авторизация ChatGPT не найдена."
|
||
echo "Сейчас появится ссылка или откроется браузер..."
|
||
echo ""
|
||
if ! "$proxy_bin" codex auth login 2>&1; then
|
||
echo ""
|
||
echo "Если браузер не открылся, попробуйте device flow:"
|
||
echo " claude-code-proxy codex auth device"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
# Запускаем прокси в фоне (если ещё не запущен)
|
||
local proxy_pid="" wrapper_pid=""
|
||
local effort_proxy="$HOME/.local/bin/claude-gpt-effort-proxy.py"
|
||
|
||
# Если прокси занял порт 18765 (старый формат без wrapper) — мигрируем
|
||
if pgrep -f "claude-code-proxy serve" &>/dev/null && \
|
||
! pgrep -f "claude-gpt-effort-proxy" &>/dev/null; then
|
||
pkill -f "claude-code-proxy serve" 2>/dev/null
|
||
sleep 0.5
|
||
fi
|
||
|
||
# Реальный прокси на 18766
|
||
if ! pgrep -f "claude-code-proxy serve" &>/dev/null; then
|
||
PORT=18766 "$proxy_bin" serve &>/tmp/claude-code-proxy.log &
|
||
proxy_pid=$!
|
||
local _j=0
|
||
while [ $_j -lt 10 ]; do
|
||
sleep 1
|
||
curl -sf --max-time 1 http://localhost:18766/ &>/dev/null
|
||
[ "$?" -ne 7 ] && break
|
||
_j=$((_j + 1))
|
||
done
|
||
fi
|
||
|
||
# Wrapper (xhigh→max патч) на 18765
|
||
if ! pgrep -f "claude-gpt-effort-proxy" &>/dev/null; then
|
||
python3 "$effort_proxy" 18766 18765 &>/tmp/claude-gpt-effort-proxy.log &
|
||
wrapper_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; любой другой = wrapper слушает
|
||
_i=$((_i + 1))
|
||
done
|
||
fi
|
||
|
||
# Убиваем прокси и wrapper при любом выходе из функции
|
||
trap '[ -n "$proxy_pid" ] && kill "$proxy_pid" 2>/dev/null; [ -n "$wrapper_pid" ] && kill "$wrapper_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" ] && [ "$_CLAUDE_TEST_CODE" != "400" ]; 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
|
||
;;
|
||
400)
|
||
# Прокси работает — 400 означает только, что тест-запрос не содержит обязательное поле «instructions»
|
||
# Авторизация уже проверена через codex auth status; запускаем Claude
|
||
echo -e "\033[0;32mOK\033[0m"
|
||
;;
|
||
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"
|
||
echo ""
|
||
echo "Попробуйте:"
|
||
echo " • Перезапустить claude_gpt"
|
||
echo " • Переавторизоваться: claude-code-proxy codex auth logout && claude-code-proxy codex auth login"
|
||
return 1
|
||
;;
|
||
esac
|
||
|
||
# Сохраняем Anthropic credentials перед запуском — команда /logout внутри Claude Code
|
||
# удаляет их, хотя в режиме GPT они не нужны, но потом сломают оригинальный claude
|
||
local _creds_file="$HOME/.claude/.credentials.json"
|
||
local _creds_backup=""
|
||
[ -f "$_creds_file" ] && _creds_backup=$(cat "$_creds_file")
|
||
|
||
echo -e "\033[0;33m[ИНФО]\033[0m Режим ChatGPT. Для выхода: Ctrl+C или /exit"
|
||
echo -e " Команда \033[1m/logout\033[0m выйдет из Anthropic, а не из ChatGPT."
|
||
echo ""
|
||
|
||
ANTHROPIC_BASE_URL=http://localhost:18765 \
|
||
ANTHROPIC_AUTH_TOKEN=dummy \
|
||
ANTHROPIC_MODEL=gpt-5.5 \
|
||
ANTHROPIC_DEFAULT_OPUS_MODEL=gpt-5.5 \
|
||
ANTHROPIC_DEFAULT_SONNET_MODEL=gpt-5.5 \
|
||
ANTHROPIC_DEFAULT_HAIKU_MODEL=gpt-5.4-mini \
|
||
CLAUDE_CODE_SUBAGENT_MODEL=gpt-5.4-mini \
|
||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||
claude "$@"
|
||
|
||
# Восстанавливаем credentials если /logout их удалил
|
||
if [ -n "$_creds_backup" ] && [ ! -f "$_creds_file" ]; then
|
||
mkdir -p "$(dirname "$_creds_file")"
|
||
echo "$_creds_backup" > "$_creds_file"
|
||
echo ""
|
||
echo -e "\033[0;33m[ИНФО]\033[0m Anthropic credentials восстановлены после /logout внутри сессии."
|
||
fi
|
||
|
||
if [ -n "$proxy_pid" ]; then
|
||
kill "$proxy_pid" 2>/dev/null
|
||
wait "$proxy_pid" 2>/dev/null
|
||
fi
|
||
}
|
||
|
||
# ── claude_deepseek ───────────────────────────────────────────
|
||
claude_deepseek() {
|
||
local key_file="$HOME/.config/claude-launcher/deepseek_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 ключ не найден."
|
||
echo "Получить ключ: https://platform.deepseek.com/api_keys"
|
||
echo ""
|
||
read -r -p "Введите ваш DeepSeek API ключ: " api_key
|
||
echo ""
|
||
|
||
if [ -z "$api_key" ]; then
|
||
echo "Ключ не введён. Выход."
|
||
return 1
|
||
fi
|
||
|
||
echo "Проверяю ключ и баланс..."
|
||
_claude_test_api "https://api.deepseek.com/anthropic/v1/messages" "Authorization: Bearer $api_key" "deepseek-v4-flash"
|
||
|
||
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 \
|
||
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
||
ANTHROPIC_MODEL=deepseek-v4-pro \
|
||
ANTHROPIC_DEFAULT_OPUS_MODEL=deepseek-v4-pro \
|
||
ANTHROPIC_DEFAULT_SONNET_MODEL=deepseek-v4-pro \
|
||
ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-v4-flash \
|
||
CLAUDE_CODE_SUBAGENT_MODEL=deepseek-v4-flash \
|
||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||
claude "$@"
|
||
}
|
||
|
||
# ── claude_kimi ─────────────────────────────────────────────
|
||
claude_kimi() {
|
||
local key_file="$HOME/.config/claude-launcher/kimi_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 ключ не найден."
|
||
echo "Получить ключ: https://platform.moonshot.ai/console/api-keys"
|
||
echo ""
|
||
read -r -p "Введите ваш Kimi API ключ (начинается с 'sk-'): " api_key
|
||
echo ""
|
||
|
||
if [ -z "$api_key" ]; then
|
||
echo "Ключ не введён. Выход."
|
||
return 1
|
||
fi
|
||
|
||
echo "Проверяю ключ и баланс..."
|
||
_claude_test_api "https://api.moonshot.ai/anthropic/v1/messages" "Authorization: Bearer $api_key" "kimi-k2.6"
|
||
|
||
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 \
|
||
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
||
ANTHROPIC_MODEL=kimi-k2.6 \
|
||
ANTHROPIC_DEFAULT_OPUS_MODEL=kimi-k2.6 \
|
||
ANTHROPIC_DEFAULT_SONNET_MODEL=kimi-k2.6 \
|
||
ANTHROPIC_DEFAULT_HAIKU_MODEL=kimi-k2.6 \
|
||
CLAUDE_CODE_SUBAGENT_MODEL=kimi-k2.6 \
|
||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||
claude "$@"
|
||
}
|
||
|
||
# ── claude_gemini ─────────────────────────────────────────────
|
||
claude_gemini() {
|
||
local acc_cmd=""
|
||
command -v antigravity-claude-proxy &>/dev/null && acc_cmd="antigravity-claude-proxy"
|
||
command -v acc &>/dev/null && acc_cmd="acc"
|
||
|
||
if [ -z "$acc_cmd" ]; then
|
||
echo "Ошибка: antigravity-claude-proxy не найден. Перезапустите claude_setup.sh"
|
||
return 1
|
||
fi
|
||
|
||
# Запускаем прокси в фоне если не запущен
|
||
local proxy_pid=""
|
||
if ! curl -sf http://localhost:8080/health &>/dev/null; then
|
||
"$acc_cmd" start &>/tmp/antigravity-proxy.log &
|
||
proxy_pid=$!
|
||
echo "Запускаю Gemini прокси..."
|
||
local i=0
|
||
while [ $i -lt 15 ]; do
|
||
sleep 1
|
||
curl -sf http://localhost:8080/health &>/dev/null && break
|
||
i=$((i+1))
|
||
done
|
||
fi
|
||
|
||
# ── Проверка аккаунтов ──
|
||
local has_auth total_count invalid_count
|
||
has_auth=$(curl -sf "http://localhost:8080/account-limits" 2>/dev/null || echo "")
|
||
|
||
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 ""
|
||
echo -e "\033[1;33m⚠️ ВНИМАНИЕ: Используйте ОТДЕЛЬНЫЙ Google-аккаунт!\033[0m"
|
||
echo " Google может заблокировать аккаунты, использующие этот прокси."
|
||
echo ""
|
||
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
|
||
;;
|
||
400)
|
||
_emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY")
|
||
echo ""
|
||
if echo "$_emsg" | grep -q "RESOURCE_EXHAUSTED"; then
|
||
local _reset
|
||
_reset=$(echo "$_emsg" | sed -n 's/.*reset after \([^.]*\).*/\1/p' 2>/dev/null || true)
|
||
echo -e "\033[0;33m[КВОТА ИСЧЕРПАНА]\033[0m Все Gemini аккаунты исчерпали лимит запросов."
|
||
[ -n "$_reset" ] && echo " Квота обновится через: $_reset"
|
||
echo ""
|
||
echo "Что делать:"
|
||
echo " • Подождите сброса квоты и повторите попытку"
|
||
echo " • Или добавьте новый аккаунт Google через 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
|
||
else
|
||
echo -e "\033[0;31m[ОШИБКА]\033[0m Прокси Gemini вернул HTTP 400."
|
||
[ -n "$_emsg" ] && echo " $_emsg"
|
||
echo ""
|
||
echo "Попробуйте:"
|
||
echo " • Перезапустить прокси: перезапустите claude_gemini"
|
||
echo " • Проверить статус аккаунтов: http://localhost:8080"
|
||
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"
|
||
echo ""
|
||
echo "Попробуйте:"
|
||
echo " • Перезапустить claude_gemini"
|
||
echo " • Проверить статус аккаунтов: http://localhost:8080"
|
||
return 1
|
||
;;
|
||
esac
|
||
|
||
echo "Запускаю Claude Code с Gemini..."
|
||
|
||
ANTHROPIC_BASE_URL=http://localhost:8080 \
|
||
ANTHROPIC_AUTH_TOKEN=dummy \
|
||
ANTHROPIC_MODEL=gemini-pro-agent \
|
||
ANTHROPIC_DEFAULT_OPUS_MODEL=gemini-pro-agent \
|
||
ANTHROPIC_DEFAULT_SONNET_MODEL=gemini-3-flash-agent \
|
||
ANTHROPIC_DEFAULT_HAIKU_MODEL=gemini-3.5-flash-low \
|
||
CLAUDE_CODE_SUBAGENT_MODEL=gemini-3.5-flash-low \
|
||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||
claude "$@"
|
||
|
||
if [ -n "$proxy_pid" ]; then
|
||
kill "$proxy_pid" 2>/dev/null
|
||
wait "$proxy_pid" 2>/dev/null
|
||
fi
|
||
}
|
||
|
||
# === END CLAUDE LAUNCHER ===
|
||
BASHEOF
|
||
|
||
success "Функции добавлены в $BASHRC"
|
||
|
||
# ── 8. Итог ──────────────────────────────────────────────────
|
||
echo ""
|
||
echo -e "${GREEN}════════════════════════════════════════════════════${NC}"
|
||
echo -e "${GREEN} Установка завершена!${NC}"
|
||
echo -e "${GREEN}════════════════════════════════════════════════════${NC}"
|
||
echo ""
|
||
echo "Доступные команды:"
|
||
echo -e " ${CYAN}claude_gpt${NC} — GPT-5.5 (ChatGPT Plus/Pro, браузерная авторизация)"
|
||
echo -e " ${CYAN}claude_deepseek${NC} — DeepSeek (API ключ сохраняется)"
|
||
echo -e " ${CYAN}claude_kimi${NC} — Kimi K2.6 (Moonshot AI, API ключ сохраняется)"
|
||
echo -e " ${CYAN}claude_gemini${NC} — Gemini (Google OAuth через браузер)"
|
||
echo ""
|
||
echo "Все команды принимают стандартные флаги:"
|
||
echo -e " ${CYAN}claude_gpt --dangerously-skip-permissions${NC}"
|
||
echo ""
|
||
echo -e "${YELLOW}⚠️ Для Gemini используйте отдельный Google-аккаунт!${NC}"
|
||
echo ""
|
||
echo "Управление:"
|
||
echo -e " DeepSeek ключ: ${CYAN}rm $DEEPSEEK_KEY_FILE${NC} (сбросить)"
|
||
echo -e " Kimi ключ: ${CYAN}rm $CONFIG_DIR/kimi_key${NC} (сбросить)"
|
||
echo -e " GPT статус: ${CYAN}claude-code-proxy codex auth status${NC}"
|
||
echo -e " GPT выйти: ${CYAN}claude-code-proxy codex auth logout${NC}"
|
||
echo -e " Gemini WebUI: ${CYAN}http://localhost:8080${NC} (когда прокси запущен)"
|
||
echo ""
|
||
|
||
if [ "$0" != "${BASH_SOURCE[0]}" ]; then
|
||
return 0 2>/dev/null || exit 0
|
||
fi
|
||
|
||
echo -e "${CYAN}Перезапускаю shell для применения функций...${NC}"
|
||
exec bash
|
||
echo ""
|