#!/usr/bin/env bash # ============================================================ # AI Setup - Claude / GPT-5.5 / DeepSeek / Kimi / Gemini # Запуск: bash ai-setup.sh # ============================================================ CONFIG_DIR="$HOME/.config/ai-setup" BIN_DIR="$HOME/.local/bin" NPM_GLOBAL="$HOME/.npm-global" PROXY_BIN="$BIN_DIR/claude-code-proxy" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" GLOBAL_RULES_SOURCE="$SCRIPT_DIR/home-configs/GLOBAL_RULES.md" 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 ai-setup.sh" exit 1 fi info "Проверяю зависимости (python3)..." if ! command -v python3 &>/dev/null; then err "Python 3 не установлен, но он требуется для работы скрипта." fi success "Python 3 найден" # ── VLESS URL parser ─────────────────────────────────────────── # Принимает vless:// URL, устанавливает переменные VL_* parse_vless_url() { local url="$1" eval "$(python3 -c " import urllib.parse, sys url = sys.argv[1] rest = url[8:] # strip 'vless://' at_pos = rest.index('@') uuid = rest[:at_pos] rest = rest[at_pos+1:] colon_pos = rest.index(':') q_pos = rest.index('?') host = rest[:colon_pos] port = rest[colon_pos+1:q_pos] rest = rest[q_pos+1:] hash_pos = rest.index('#') if '#' in rest else len(rest) qs = rest[:hash_pos] name = rest[hash_pos+1:] if '#' in rest else '' params = urllib.parse.parse_qs(qs) def get(p, default=''): vals = params.get(p, [default]) return vals[0] if vals else default print(f'VL_UUID={uuid}') print(f'VL_ADDRESS={host}') print(f'VL_PORT={port}') print(f'VL_ENCRYPTION={get(\"encryption\")}') print(f'VL_SECURITY={get(\"security\")}') print(f'VL_SNI={get(\"sni\")}') print(f'VL_FP={get(\"fp\", \"chrome\")}') print(f'VL_PBK={get(\"pbk\")}') print(f'VL_SID={get(\"sid\")}') print(f'VL_TYPE={get(\"type\", \"xhttp\")}') print(f'VL_PATH={urllib.parse.unquote(get(\"path\", \"/\"))}') print(f'VL_MODE={get(\"mode\", \"auto\")}') print(f'VL_NAME={name}') " "$url")" } # ── 0. Выбор режима работы (vless / direct) ───────────────── read -r -p "Установить встроенный vless? [Y/n] " _vless_ans _vless_ans="${_vless_ans:-Y}" if [[ "$_vless_ans" =~ ^[Yy]$ ]]; then USE_VLESS=1 # Читаем список серверов _VL_URLS=() _VL_LABELS=() _SERVERS_FILE="$SCRIPT_DIR/home-configs/vless/servers.conf" if [ ! -f "$_SERVERS_FILE" ]; then err "Файл servers.conf не найден: $_SERVERS_FILE" fi while IFS= read -r line; do [[ "$line" =~ ^[[:space:]]*# ]] && continue [[ -z "$line" ]] && continue _vl_rest="${line#vless://}" _vl_rest="${_vl_rest#*@}" _vl_ip="${_vl_rest%%:*}" _vl_name="${line##*#}" [[ "$_vl_name" == "$line" ]] && _vl_name="" _VL_URLS+=("$line") _VL_LABELS+=("$_vl_ip ($_vl_name)") done < "$_SERVERS_FILE" if [ "${#_VL_URLS[@]}" -eq 0 ]; then err "Нет VLESS серверов в $_SERVERS_FILE" fi echo "" info "Доступные VLESS серверы:" for i in "${!_VL_LABELS[@]}"; do echo -e " ${GREEN}$((i+1))${NC}) ${_VL_LABELS[$i]}" done read -r -p "Выбери сервер [1-${#_VL_URLS[@]}]: " _vl_choice _vl_choice="${_vl_choice:-1}" if ! [[ "$_vl_choice" =~ ^[0-9]+$ ]] || [ "$_vl_choice" -lt 1 ] || [ "$_vl_choice" -gt "${#_VL_URLS[@]}" ]; then err "Неверный выбор: $_vl_choice" fi _VL_SELECTED="${_VL_URLS[$((_vl_choice-1))]}" parse_vless_url "$_VL_SELECTED" info "Выбран: $VL_ADDRESS ($VL_NAME)" else USE_VLESS=0 info "Режим: direct (без проксирования)" # Останавливаем и отключаем xray (мог остаться от предыдущей установки) sudo systemctl stop xray 2>/dev/null || true sudo systemctl disable xray 2>/dev/null || true fi # ── 1. npm prefix в домашнюю папку ────────────────────────── info "Настраиваю npm prefix..." mkdir -p "$NPM_GLOBAL" npm config set prefix "$NPM_GLOBAL" success "npm prefix -> $NPM_GLOBAL" export PATH="$NPM_GLOBAL/bin:$BIN_DIR:$PATH" # ── 2. Node.js ─────────────────────────────────────────────── info "Проверяю Node.js..." if ! command -v node &>/dev/null; then info "Попытка установки Node.js (нужен sudo)..." if command -v apt-get &>/dev/null; then curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - sudo apt-get install -y nodejs elif command -v dnf &>/dev/null; then sudo dnf install -y nodejs else warn "Не удалось определить пакетный менеджер. Установите Node.js вручную." fi fi if command -v node &>/dev/null; then success "Node.js $(node --version)" else warn "Node.js не найден. Некоторые функции могут не работать." fi # ── 2.5. proxychains-ng + xray (только в режиме vless) ────── if [ "$USE_VLESS" -eq 1 ]; then info "Проверяю proxychains-ng..." if ! command -v proxychains4 &>/dev/null; then info "Устанавливаю proxychains-ng (нужен sudo)..." if command -v apt-get &>/dev/null; then sudo apt-get install -y proxychains-ng elif command -v dnf &>/dev/null; then sudo dnf install -y proxychains-ng else warn "Не удалось установить proxychains-ng автоматически. Установите вручную." fi fi if command -v proxychains4 &>/dev/null; then success "proxychains4 найден" else warn "proxychains4 не найден. Продолжаю без проксирования." fi info "Устанавливаю xray..." # Останавливаем старый процесс (мог остаться от предыдущей установки) sudo systemctl stop xray 2>/dev/null || true XRAY_VERSION="26.3.27" XRAY_ARCH="64" XRAY_URL="https://github.com/XTLS/Xray-core/releases/download/v${XRAY_VERSION}/Xray-linux-${XRAY_ARCH}.zip" TMPDIR=$(mktemp -d) curl -fsSL "$XRAY_URL" -o "$TMPDIR/xray.zip" unzip -q "$TMPDIR/xray.zip" -d "$TMPDIR" sudo install -m 755 "$TMPDIR/xray" /usr/local/bin/xray rm -rf "$TMPDIR" sudo mkdir -p /etc/xray sudo tee /etc/xray/config.json > /dev/null << XRAYEOF { "log": { "loglevel": "warning" }, "inbounds": [ { "port": 1080, "listen": "127.0.0.1", "protocol": "socks", "settings": { "udp": true } }, { "port": 2080, "listen": "127.0.0.1", "protocol": "http" } ], "outbounds": [ { "protocol": "vless", "settings": { "vnext": [ { "address": "$VL_ADDRESS", "port": $VL_PORT, "users": [ { "id": "$VL_UUID", "encryption": "$VL_ENCRYPTION", "flow": "" } ] } ] }, "streamSettings": { "network": "$VL_TYPE", "security": "$VL_SECURITY", "realitySettings": { "serverName": "$VL_SNI", "fingerprint": "$VL_FP", "publicKey": "$VL_PBK", "shortId": "$VL_SID" }, "xhttpSettings": { "path": "$VL_PATH", "mode": "$VL_MODE" } } } ] } XRAYEOF sudo chmod 644 /etc/xray/config.json sudo tee /etc/systemd/system/xray.service > /dev/null << 'SVCEOF' [Unit] Description=Xray Service After=network.target [Service] ExecStart=/usr/local/bin/xray run -c /etc/xray/config.json Restart=on-failure [Install] WantedBy=multi-user.target SVCEOF # Удаляем чужие drop-in оверрайды (могут переопределять ExecStart на старый конфиг) sudo rm -rf /etc/systemd/system/xray.service.d/ # Удаляем старый дефолтный конфиг xray из других путей sudo rm -rf /usr/local/etc/xray/ sudo systemctl daemon-reload sudo systemctl enable --now xray success "xray установлен и запущен" # ── Отключение IPv6 (VLESS не тянет IPv6, браузеры зависают) ── info "Отключаю IPv6 на уровне системы..." sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1 sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1 sudo tee /etc/sysctl.d/99-disable-ipv6.conf > /dev/null << 'SYSCTEOF' # Отключение IPv6 — требуется для стабильной работы VLESS/xray net.ipv6.conf.all.disable_ipv6=1 net.ipv6.conf.default.disable_ipv6=1 SYSCTEOF sudo systemctl restart systemd-resolved success "IPv6 отключён, DNS-кэш очищен" cp "$SCRIPT_DIR/home-configs/proxychains/proxychains-xray.conf" "$HOME/.proxychains-xray.conf" success "Proxychains конфиг обновлён" # ── Настройка Firefox на SOCKS5 + remote DNS ──────────────── info "Настраиваю Firefox на SOCKS5 прокси..." FIREFOX_PROFILE="" if [ -d "$HOME/snap/firefox/common/.mozilla/firefox" ]; then FIREFOX_PROFILE=$(find "$HOME/snap/firefox/common/.mozilla/firefox" -name "*.default*" -type d | head -1) elif [ -d "$HOME/.mozilla/firefox" ]; then FIREFOX_PROFILE=$(find "$HOME/.mozilla/firefox" -name "*.default*" -type d | head -1) fi if [ -n "$FIREFOX_PROFILE" ]; then cat > "$FIREFOX_PROFILE/user.js" << 'FJSEOF' user_pref("network.proxy.type", 1); user_pref("network.proxy.socks", "127.0.0.1"); user_pref("network.proxy.socks_port", 1080); user_pref("network.proxy.socks_remote_dns", true); user_pref("network.proxy.http", ""); user_pref("network.proxy.http_port", 0); user_pref("network.proxy.ssl", ""); user_pref("network.proxy.ssl_port", 0); FJSEOF success "Firefox настроен на SOCKS5 (профиль: $FIREFOX_PROFILE)" else warn "Firefox не найден, пропускаю настройку прокси" fi # ── Настройка системного прокси (для Chrome/Chromium) ─────── info "Настраиваю системный прокси..." if command -v gsettings &>/dev/null; then gsettings set org.gnome.system.proxy mode 'manual' 2>/dev/null || true gsettings set org.gnome.system.proxy.http host '127.0.0.1' 2>/dev/null || true gsettings set org.gnome.system.proxy.http port 2080 2>/dev/null || true gsettings set org.gnome.system.proxy.socks host '127.0.0.1' 2>/dev/null || true gsettings set org.gnome.system.proxy.socks port 1080 2>/dev/null || true success "Системный прокси настроен (HTTP 2080 + SOCKS 1080)" else warn "gsettings не найден, пропускаю настройку системного прокси" fi fi # ── 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 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 "$BIN_DIR" 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 (маппинг effort: xhigh→high) ───────── EFFORT_PROXY_BIN="$BIN_DIR/claude-gpt-effort-proxy.py" cat > "$EFFORT_PROXY_BIN" << 'PYEOF' #!/usr/bin/env python3 """Effort mapping proxy for GPT backend. claude-code-proxy now accepts: low, medium, high, max (no "xhigh"). Claude Code may send "xhigh" effort - we map it to "high". """ import http.client, http.server, sys, logging, socketserver 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 logging.basicConfig( filename="/tmp/claude-gpt-effort-proxy.log", level=logging.INFO, format='{"t":"%(asctime)s","level":"%(levelname)s","msg":"%(message)s"}', datefmt='%Y-%m-%dT%H:%M:%S', ) log = logging.getLogger("effort-proxy") # Потокобезопасный сервер - обрабатывает несколько запросов одновременно class _ThreadedServer(socketserver.ThreadingMixIn, http.server.HTTPServer): daemon_threads = True class _Proxy(http.server.BaseHTTPRequestHandler): def proxy_request(self): body = b"" if cl := self.headers.get("Content-Length"): body = self.rfile.read(int(cl)) # Маппинг xhigh→high: claude-code-proxy больше не принимает xhigh body = body.replace(b'"xhigh"', b'"high"') try: conn = http.client.HTTPConnection("127.0.0.1", 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: log.error("proxy error: %s", 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 log.info("effort-proxy starting on 127.0.0.1:%d → upstream %d", LISTEN_PORT, UPSTREAM_PORT) _ThreadedServer(("127.0.0.1", LISTEN_PORT), _Proxy).serve_forever() PYEOF chmod +x "$EFFORT_PROXY_BIN" success "claude-gpt-effort-proxy -> $EFFORT_PROXY_BIN" # ── 4c. antigravity CLI (Gemini) ────────────────────────── info "Проверяю antigravity CLI (agy)..." if ! command -v agy &>/dev/null; then info "Устанавливаю agy..." curl -fsSL https://antigravity.google/cli/install.sh | bash success "agy установлен" else success "agy уже установлен: $(agy --version 2>/dev/null | head -1)" fi # ── 6. Папка для конфигов ──────────────────────────────────── mkdir -p "$CONFIG_DIR" # ── 6.5. Генерация глобальных правил агентов ───────────────── info "Обновляю глобальные правила агентов..." [ -f "$GLOBAL_RULES_SOURCE" ] || err "Файл глобальных правил не найден: $GLOBAL_RULES_SOURCE" cp "$GLOBAL_RULES_SOURCE" "$CONFIG_DIR/global_rules.md" success "Глобальные правила обновлены: $CONFIG_DIR/global_rules.md" info "Обновляю native rule-файлы агентов..." mkdir -p "$HOME/.codex" "$HOME/.kimi-code" "$HOME/.claude" "$HOME/.gemini" cp "$CONFIG_DIR/global_rules.md" "$HOME/.codex/AGENTS.md" cp "$CONFIG_DIR/global_rules.md" "$HOME/.kimi-code/AGENTS.md" cp "$CONFIG_DIR/global_rules.md" "$HOME/.claude/CLAUDE.md" cp "$CONFIG_DIR/global_rules.md" "$HOME/.gemini/GEMINI.md" success "Native rule-файлы обновлены" # ── 6.6. Деплой Claude skills ──────────────────────────────── info "Обновляю Claude skills..." SKILLS_SRC="$SCRIPT_DIR/home-configs/claude/skills" SKILLS_DST="$HOME/.claude/skills" if [ -d "$SKILLS_SRC" ]; then mkdir -p "$SKILLS_DST" for skill_dir in "$SKILLS_SRC"/*; do [ -d "$skill_dir" ] || continue skill_name=$(basename "$skill_dir") mkdir -p "$SKILLS_DST/$skill_name" cp -r "$skill_dir/"* "$SKILLS_DST/$skill_name/" done success "Claude skills обновлены" else info "Папка со skills не найдена, пропускаю" fi # ── 7. Очистка старых функций из .bashrc / .zshrc ─────────── clean_rc() { local rc_file="$1" if [ -f "$rc_file" ] && grep -q "# === CLAUDE LAUNCHER ===" "$rc_file"; then info "Очищаю старые функции из $rc_file..." python3 - "$rc_file" <<'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 success "Старые функции удалены из $rc_file" fi } clean_rc "$HOME/.bashrc" clean_rc "$HOME/.zshrc" add_path_to_rc() { local rc_file="$1" if [ -f "$rc_file" ]; then if ! grep -q 'NPM_GLOBAL' "$rc_file" 2>/dev/null; then cat >> "$rc_file" << 'PATHEOF' # Claude Code Launcher PATH export NPM_GLOBAL="$HOME/.npm-global" export PATH="$NPM_GLOBAL/bin:$HOME/.local/bin:$PATH" PATHEOF success "PATH добавлен в $rc_file" fi fi } add_path_to_rc "$HOME/.bashrc" [ -f "$HOME/.zshrc" ] && add_path_to_rc "$HOME/.zshrc" # ── 8. Генерация Standalone скриптов ──────────────────────── info "Генерирую standalone скрипты в $BIN_DIR..." HELPERS_FILE="$BIN_DIR/ai-api-helpers.sh" cat > "$HELPERS_FILE" << 'HELPEREOF' #!/usr/bin/env bash # _claude_test_api: Send 1-token test to an Anthropic-compatible endpoint _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_test_openai_api: Send a tiny test to an OpenAI-compatible endpoint _claude_test_openai_api() { local url="$1" api_key="$2" model="${3:-kimi-k2.6}" local response response=$(curl -s -w "\n%{http_code}" --max-time 30 "$url" \ -H "Authorization: Bearer $api_key" \ -H "Content-Type: application/json" \ -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') } _handle_openai_api_response() { local provider="$1" local code="$2" local body="$3" local topup_url="$4" # Используем глобальную переменную _API_RET вместо return, # потому что bash return умеет только 0-255, а HTTP-коды # вроде 401/429 обрезаются (401 % 256 = 145). _API_RET=0 local _emsg case "$code" in 200) echo -e "\033[0;32mOK\033[0m" _API_RET=0 ;; 401|403) _emsg=$(_claude_extract_error "$body") echo "" echo -e "\033[0;31m[ОШИБКА АВТОРИЗАЦИИ]\033[0m Авторизация $provider недействительна (HTTP $code)." [ -n "$_emsg" ] && echo " $_emsg" _API_RET=401 ;; 429) _emsg=$(_claude_extract_error "$body") echo "" echo -e "\033[0;33m[ЛИМИТ ИСЧЕРПАН]\033[0m Баланс/лимит $provider исчерпан." [ -n "$_emsg" ] && echo " $_emsg" [ -n "$topup_url" ] && echo " $topup_url" _API_RET=429 ;; 000) echo "" echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить ключ (нет сети?). Продолжаю..." _API_RET=0 ;; *) _emsg=$(_claude_extract_error "$body") echo "" echo -e "\033[0;31m[ОШИБКА]\033[0m API $provider вернул HTTP $code." [ -n "$_emsg" ] && echo " $_emsg" _API_RET=$code ;; esac return 0 } _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() { 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 } _handle_api_response() { local provider="$1" local code="$2" local body="$3" local topup_url="$4" # Используем глобальную переменную _API_RET вместо return, # потому что bash return умеет только 0-255, а HTTP-коды # вроде 401/429 обрезаются (401 % 256 = 145). _API_RET=0 local _emsg case "$code" in 200) echo -e "\033[0;32mOK\033[0m" _API_RET=0 ;; 401|403) _emsg=$(_claude_extract_error "$body") echo "" echo -e "\033[0;31m[ОШИБКА АВТОРИЗАЦИИ]\033[0m Авторизация $provider недействительна (HTTP $code)." [ -n "$_emsg" ] && echo " $_emsg" _API_RET=401 ;; 429) _emsg=$(_claude_extract_error "$body") echo "" echo -e "\033[0;33m[ЛИМИТ ИСЧЕРПАН]\033[0m Баланс/лимит $provider исчерпан." [ -n "$_emsg" ] && echo " $_emsg" [ -n "$topup_url" ] && echo " $topup_url" _API_RET=429 ;; 000) echo "" echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить ключ (нет сети?). Продолжаю..." _API_RET=0 ;; 400) _emsg=$(_claude_extract_error "$body") if echo "${_emsg:-$body}" | grep -qi "RESOURCE_EXHAUSTED"; then echo "" echo -e "\033[0;33m[КВОТА ИСЧЕРПАНА]\033[0m Лимит запросов исчерпан." [ -n "$topup_url" ] && echo " $topup_url" _API_RET=429 else # 400 = auth is valid, but max_tokens=1 is too small for thinking models echo -e "\033[0;32mOK\033[0m" _API_RET=0 fi ;; *) _emsg=$(_claude_extract_error "$body") echo "" echo -e "\033[0;31m[ОШИБКА]\033[0m API $provider вернул HTTP $code." [ -n "$_emsg" ] && echo " $_emsg" _API_RET=$code ;; esac return 0 } _open_browser() { local url="$1" if command -v xdg-open &>/dev/null; then xdg-open "$url" 2>/dev/null elif command -v open &>/dev/null; then open "$url" 2>/dev/null elif command -v sensible-browser &>/dev/null; then sensible-browser "$url" 2>/dev/null else echo "Откройте вручную: $url"; fi } _build_ai_sys_prompt() { local global_rules="$HOME/.config/ai-setup/global_rules.md" local global_rendered="" [ -f "$global_rules" ] && global_rendered="$(cat "$global_rules" 2>/dev/null)" # Нативные глобальные правила: только global_rules.md, без проектного контекста. # Проектные *.md файлы агент должен читать из текущего репозитория сам, если умеет. # Для агентов без надежного нативного чтения проектный контекст добавляется ниже в prompt. mkdir -p "$HOME/.codex" "$HOME/.kimi-code" "$HOME/.claude" "$HOME/.gemini" echo "$global_rendered" > "$HOME/.codex/AGENTS.md" echo "$global_rendered" > "$HOME/.kimi-code/AGENTS.md" echo "$global_rendered" > "$HOME/.claude/CLAUDE.md" echo "$global_rendered" > "$HOME/.gemini/GEMINI.md" local sp="=== ГЛОБАЛЬНЫЕ ПРАВИЛА ===\n" [ -n "$global_rendered" ] && sp+="$global_rendered\n\n" sp+="=== ПРАВИЛА ПРОЕКТА ===\n" for f in *.md; do [ -f "$f" ] && sp+="\n--- Файл $f ---\n$(cat "$f")\n" done echo -e "$sp" } HELPEREOF chmod +x "$HELPERS_FILE" # === ai-gpt === cat > "$BIN_DIR/ai-gpt" << 'GPTEOF' #!/usr/bin/env bash # ai-gpt - запуск нативного OpenAI Codex source "$HOME/.local/bin/ai-api-helpers.sh" 2>/dev/null || true codex_bin="$HOME/.npm-global/bin/codex" [ ! -f "$codex_bin" ] && codex_bin="$(command -v codex 2>/dev/null)" if [ -z "$codex_bin" ] || [ ! -f "$codex_bin" ]; then echo "OpenAI Codex не найден. Устанавливаю..." curl -fsSL https://chatgpt.com/codex/install.sh | sh codex_bin="$HOME/.npm-global/bin/codex" [ ! -f "$codex_bin" ] && codex_bin="$(command -v codex 2>/dev/null)" # Fallback: если curl-установка не сработала (например, 403 от Cloudflare), ставим через npm if [ -z "$codex_bin" ] || [ ! -f "$codex_bin" ]; then echo "Установка через curl не удалась, пробую npm..." npm install -g @openai/codex codex_bin="$HOME/.npm-global/bin/codex" [ ! -f "$codex_bin" ] && codex_bin="$(command -v codex 2>/dev/null)" fi fi if [ -z "$codex_bin" ] || [ ! -f "$codex_bin" ]; then echo "Ошибка: не удалось установить OpenAI Codex." exit 1 fi _build_ai_sys_prompt > /dev/null # сохраняет в ~/.codex/AGENTS.md (codex читает авто) exec "$codex_bin" --dangerously-bypass-approvals-and-sandbox "$@" GPTEOF chmod +x "$BIN_DIR/ai-gpt" # === ai-deepseek === cat > "$BIN_DIR/ai-deepseek" << 'DEEPSEEKEOF' #!/usr/bin/env bash source "$HOME/.local/bin/ai-api-helpers.sh" 2>/dev/null || true key_file="$HOME/.config/ai-setup/deepseek_key" api_key="" reauth=0 [ -f "$key_file" ] && api_key=$(cat "$key_file") if [ -n "$api_key" ]; then echo -n "Проверка сохранённого DeepSeek ключа... " _claude_test_api "https://api.deepseek.com/anthropic/v1/messages" "x-api-key: $api_key" "deepseek-v4-flash" _handle_api_response "DeepSeek" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://platform.deepseek.com/top_up" ret=$_API_RET if [ $ret -eq 401 ]; then rm -f "$key_file" api_key="" reauth=1 elif [ $ret -eq 429 ]; then echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] " read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac elif [ $ret -ne 0 ]; then exit 1 fi fi if [ -z "$api_key" ] && [ "$reauth" -eq 1 ]; then echo -n "Хотите ввести новый DeepSeek ключ? [Y/n] " read -r _ans; case "${_ans:-Y}" in [Yy]*) ;; *) exit 1 ;; esac fi if [ -z "$api_key" ]; then echo "Получить ключ: https://platform.deepseek.com/api_keys" read -r -p "Введите ваш DeepSeek API ключ: " api_key [ -z "$api_key" ] && { echo "Выход."; exit 1; } echo -n "Проверяю ключ и баланс... " _claude_test_api "https://api.deepseek.com/anthropic/v1/messages" "x-api-key: $api_key" "deepseek-v4-flash" _handle_api_response "DeepSeek" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://platform.deepseek.com/top_up" ret=$_API_RET if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then mkdir -p "$(dirname "$key_file")" echo "$api_key" > "$key_file" chmod 600 "$key_file" echo "Ключ сохранён." if [ $ret -eq 429 ]; then echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] " read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac fi else echo "Ключ НЕ сохранён." exit 1 fi fi SYS_PROMPT=$(_build_ai_sys_prompt) 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 --dangerously-skip-permissions --system-prompt "$SYS_PROMPT" "$@" DEEPSEEKEOF chmod +x "$BIN_DIR/ai-deepseek" # === ai-kimi === cat > "$BIN_DIR/ai-kimi" << 'KIMIEOF' #!/usr/bin/env bash # ai-kimi - запуск Claude Code через официальный Kimi Code API source "$HOME/.local/bin/ai-api-helpers.sh" 2>/dev/null || true key_file="$HOME/.config/ai-setup/kimi_key" api_key="" [ -f "$key_file" ] && api_key=$(cat "$key_file") if [ -n "$api_key" ]; then echo -n "Проверка сохранённого Kimi ключа... " _claude_test_api "https://api.kimi.com/coding/v1/messages" "x-api-key: $api_key" "kimi-k2.6" _handle_api_response "Kimi" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://www.kimi.com/code" ret=$_API_RET if [ $ret -eq 401 ]; then rm -f "$key_file" api_key="" elif [ $ret -eq 429 ]; then echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] " read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac elif [ $ret -ne 0 ]; then exit 1 fi fi if [ -z "$api_key" ]; then echo "Получить ключ: https://www.kimi.com/code" read -r -p "Введите ваш Kimi API ключ: " api_key [ -z "$api_key" ] && { echo "Выход."; exit 1; } echo -n "Проверяю ключ и баланс... " _claude_test_api "https://api.kimi.com/coding/v1/messages" "x-api-key: $api_key" "kimi-k2.6" _handle_api_response "Kimi" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://www.kimi.com/code" ret=$_API_RET if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then mkdir -p "$(dirname "$key_file")" echo "$api_key" > "$key_file" chmod 600 "$key_file" echo "Ключ сохранён." if [ $ret -eq 429 ]; then echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] " read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac fi else echo "Ключ НЕ сохранён." exit 1 fi fi if ! command -v claude &>/dev/null; then echo "Ошибка: Claude Code не найден. Установите через npm:" echo " npm install -g @anthropic-ai/claude-code" exit 1 fi SYS_PROMPT=$(_build_ai_sys_prompt) ANTHROPIC_BASE_URL=https://api.kimi.com/coding \ 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 --dangerously-skip-permissions --system-prompt "$SYS_PROMPT" "$@" KIMIEOF chmod +x "$BIN_DIR/ai-kimi" # === ai-gemini === cat > "$BIN_DIR/ai-gemini" << 'GEMINIEOF' #!/usr/bin/env bash # ============================================================ # ai-gemini - запуск нативного antigravity CLI (agy) # ============================================================ agy_bin="$HOME/.local/bin/agy" [ ! -f "$agy_bin" ] && agy_bin="$(command -v agy 2>/dev/null)" if [ -z "$agy_bin" ] || [ ! -f "$agy_bin" ]; then echo "Antigravity CLI (agy) не найден. Устанавливаю..." curl -fsSL https://antigravity.google/cli/install.sh | bash agy_bin="$HOME/.local/bin/agy" [ ! -f "$agy_bin" ] && agy_bin="$(command -v agy 2>/dev/null)" fi if [ -z "$agy_bin" ] || [ ! -f "$agy_bin" ]; then echo "Ошибка: не удалось установить antigravity CLI." exit 1 fi source "$HOME/.local/bin/ai-api-helpers.sh" 2>/dev/null || true SYS_PROMPT=$(_build_ai_sys_prompt) if [ $# -eq 0 ]; then exec "$agy_bin" --dangerously-skip-permissions -i "$SYS_PROMPT\n\nПрочитай правила выше и коротко подтверди готовность к работе." else ARGS=("$@") INJECTED=0 for i in "${!ARGS[@]}"; do if [[ "${ARGS[$i]}" == "-i" || "${ARGS[$i]}" == "-p" || "${ARGS[$i]}" == "--prompt-interactive" || "${ARGS[$i]}" == "--print" ]]; then ARGS[$((i+1))]="$SYS_PROMPT\n\nЗапрос пользователя:\n${ARGS[$((i+1))]}" INJECTED=1 break fi done exec "$agy_bin" --dangerously-skip-permissions "${ARGS[@]}" fi GEMINIEOF chmod +x "$BIN_DIR/ai-gemini" # === ai-claude === cat > "$BIN_DIR/ai-claude" << 'CLAUDEEOF' #!/usr/bin/env bash # ai-claude - запуск оригинального Claude Code (Anthropic) source "$HOME/.local/bin/ai-api-helpers.sh" 2>/dev/null || true SYS_PROMPT=$(_build_ai_sys_prompt) exec claude --dangerously-skip-permissions --system-prompt "$SYS_PROMPT" "$@" CLAUDEEOF chmod +x "$BIN_DIR/ai-claude" # ── 8.5. Proxychains инъекция (только в режиме vless) ──────── if [ "$USE_VLESS" -eq 1 ]; then info "Включаю proxychains4 в ai-лаунчеры..." sed -i 's/^exec "\$codex_bin"/exec proxychains4 -f "\$HOME\/\.proxychains-xray\.conf" "\$codex_bin"/' "$BIN_DIR/ai-gpt" sed -i 's/^claude --dangerously-skip-permissions/proxychains4 -f "\$HOME\/\.proxychains-xray\.conf" claude --dangerously-skip-permissions/' "$BIN_DIR/ai-deepseek" sed -i 's/^claude --dangerously-skip-permissions/proxychains4 -f "\$HOME\/\.proxychains-xray\.conf" claude --dangerously-skip-permissions/' "$BIN_DIR/ai-kimi" sed -i 's/^\([[:space:]]*\)exec "\$agy_bin"/\1exec proxychains4 -f "\$HOME\/\.proxychains-xray\.conf" "\$agy_bin"/' "$BIN_DIR/ai-gemini" sed -i 's/^exec claude/exec proxychains4 -f "\$HOME\/\.proxychains-xray\.conf" claude/' "$BIN_DIR/ai-claude" success "proxychains4 интегрирован" fi info "Удаляю старые версии скриптов (claude_*)..." rm -f "$BIN_DIR/claude_gpt" "$BIN_DIR/claude_deepseek" "$BIN_DIR/claude_kimi" "$BIN_DIR/claude_gemini" "$BIN_DIR/claude_api_helpers.sh" success "Скрипты сгенерированы." # ── 9. Итог ────────────────────────────────────────────────── echo "" echo -e "${GREEN}════════════════════════════════════════════════════${NC}" echo -e "${GREEN} Установка завершена!${NC}" echo -e "${GREEN}════════════════════════════════════════════════════${NC}" echo "" echo "Доступные команды (теперь это независимые скрипты в ~/.local/bin):" echo -e " ${CYAN}ai-claude${NC} - Оригинальный Claude Code (Anthropic)" echo -e " ${CYAN}ai-gpt${NC} - OpenAI Codex (нативный CLI, автоустановка)" echo -e " ${CYAN}ai-deepseek${NC} - DeepSeek (API ключ сохраняется)" echo -e " ${CYAN}ai-kimi${NC} - Kimi K2.6 (через Claude Code, API ключ сохраняется)" echo -e " ${CYAN}ai-gemini${NC} - Gemini (нативный agy CLI, автоустановка)" echo "" echo -e "${YELLOW}⚠️ Для Gemini используйте отдельный Google-аккаунт!${NC}" echo "" echo -e "Чтобы команды были доступны сразу, выполните: ${GREEN}exec bash${NC}"