Files
ai-setup/ai-setup.sh
Виталий Никитенко a4feeca0bb feat: индикация доступности VLESS серверов (TCP check)
При выборе Y скрипт проверяет TCP-коннект к IP:port каждого сервера
и показывает цветной статус:
- зелёный ✓ доступен
- красный ✗ недоступен

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 13:23:16 +07:00

1058 lines
40 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
flow = get('flow')
vtype = get('type')
if not vtype:
vtype = 'xhttp' if not flow else 'tcp'
print(f'VL_UUID={uuid}')
print(f'VL_ADDRESS={host}')
print(f'VL_PORT={port}')
print(f'VL_ENCRYPTION={get(\"encryption\")}')
print(f'VL_FLOW={flow}')
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={vtype}')
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=()
_VL_IPS=()
_VL_PORTS=()
_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 || [[ -n "$line" ]]; do
[[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ -z "$line" ]] && continue
_vl_rest="${line#vless://}"
_vl_rest="${_vl_rest#*@}"
_vl_ip="${_vl_rest%%:*}"
_vl_tmp="${_vl_rest#*:}"
_vl_port="${_vl_tmp%%\?*}"
_vl_name="${line##*#}"
[[ "$_vl_name" == "$line" ]] && _vl_name=""
_VL_URLS+=("$line")
_VL_LABELS+=("$_vl_ip ($_vl_name)")
_VL_IPS+=("$_vl_ip")
_VL_PORTS+=("$_vl_port")
done < "$_SERVERS_FILE"
if [ "${#_VL_URLS[@]}" -eq 0 ]; then
err "Нет VLESS серверов в $_SERVERS_FILE"
fi
# Проверка доступности
echo ""
info "Проверяю доступность серверов..."
_VL_STATUS=()
for i in "${!_VL_IPS[@]}"; do
_ip="${_VL_IPS[$i]}"
_port="${_VL_PORTS[$i]}"
_label="${_VL_LABELS[$i]}"
printf " %-45s" "$((i+1))) $_label"
if timeout 3 bash -c "echo >/dev/tcp/$_ip/$_port" 2>/dev/null; then
echo -e "${GREEN}✓ доступен${NC}"
_VL_STATUS+=("ok")
else
echo -e "${RED}✗ недоступен${NC}"
_VL_STATUS+=("fail")
fi
done
echo ""
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 (без проксирования)"
# ── Откат всех настроек vless ──────────────────────────────
info "Откатываю настройки прокси..."
# Останавливаем и отключаем xray
sudo systemctl stop xray 2>/dev/null || true
sudo systemctl disable xray 2>/dev/null || true
success "xray остановлен и отключён"
# Системный прокси → none
if command -v gsettings &>/dev/null; then
gsettings set org.gnome.system.proxy mode 'none' 2>/dev/null || true
success "Системный прокси отключён"
fi
# Firefox → direct (type=0)
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", 0);
FJSEOF
success "Firefox переключён на прямой доступ"
fi
# Включаем IPv6 обратно
sudo rm -f /etc/sysctl.d/99-disable-ipv6.conf
sudo sysctl -w net.ipv6.conf.all.disable_ipv6=0 2>/dev/null || true
sudo sysctl -w net.ipv6.conf.default.disable_ipv6=0 2>/dev/null || true
sudo systemctl restart systemd-resolved 2>/dev/null || true
success "IPv6 восстановлен"
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
# xhttpSettings — только для xhttp-транспорта
_VL_XHTTP=""
if [ "$VL_TYPE" = "xhttp" ]; then
_VL_XHTTP=$(printf ',\n "xhttpSettings": {\n "path": "%s",\n "mode": "%s"\n }' "$VL_PATH" "$VL_MODE")
fi
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": "$VL_FLOW"
}
]
}
]
},
"streamSettings": {
"network": "$VL_TYPE",
"security": "$VL_SECURITY",
"realitySettings": {
"serverName": "$VL_SNI",
"fingerprint": "$VL_FP",
"publicKey": "$VL_PBK",
"shortId": "$VL_SID"
}$_VL_XHTTP
}
}
]
}
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}"