Compare commits
78 Commits
14b800e6fb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c2844ce92 | |||
| dfa706e7a0 | |||
| 648c9f068b | |||
| f3d1b6d5c5 | |||
| fe439fd4a6 | |||
| f8465580e0 | |||
| 07983ea84e | |||
| 222bb129eb | |||
| 54742d6a36 | |||
| 44e3ea90f9 | |||
| 3f61f15507 | |||
|
|
23256d9579 | ||
| cff3ed880d | |||
| 08f23e857e | |||
| 986abf5101 | |||
| 7187aa6669 | |||
| c86110fbd6 | |||
| 76fb86f910 | |||
| 50c26736f1 | |||
| 71ef0f76f3 | |||
| 88061f310a | |||
| c6161c3332 | |||
| 57d171a592 | |||
|
|
aed7468068 | ||
|
|
4ae5ca149a | ||
|
|
cb8c3c9544 | ||
|
|
d2bbcc7e33 | ||
|
|
e955c928d3 | ||
|
|
775bca1cee | ||
|
|
2079768318 | ||
|
|
cf34698116 | ||
|
|
6c7324bfd8 | ||
|
|
beca4de9cd | ||
|
|
398e57c648 | ||
| 5599194d92 | |||
| 2805ade773 | |||
| 2c8ff61968 | |||
| feb401f4ba | |||
| df401ca333 | |||
| 71c30b17bc | |||
| 7b0756bf96 | |||
| 7c8b3a7147 | |||
| 3b591766e1 | |||
| 6f623a5b3e | |||
| 7429802612 | |||
| 00722b430f | |||
| b37482312f | |||
| c266b64dab | |||
| 9e74d53025 | |||
| 0ba7416047 | |||
| 805951d920 | |||
| 3d21f6b620 | |||
| c9a0fca582 | |||
| 66a040cc03 | |||
| 9669f5ff15 | |||
| 5cb56124da | |||
| fcf810fd75 | |||
| 0b0d51b77c | |||
| c9db0be030 | |||
| a21a77723c | |||
| ac64bb1505 | |||
| bae481172f | |||
| 01b69c341b | |||
| 08be1dfc08 | |||
| 6b80364344 | |||
| fb952ad371 | |||
| dfaf835cb6 | |||
| 17c456ed4f | |||
| c1e68571f8 | |||
| 2f48d038bd | |||
| 3dfd7ff034 | |||
| 25d42e8b50 | |||
| 704e30d3be | |||
| 6120f34199 | |||
| 2f396ac27a | |||
|
|
2928298e6b | ||
|
|
7a3f14fa48 | ||
|
|
797e8448af |
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
Данные правила применяются ко всем агентам, работающим конкретно в этом репозитории.
|
Данные правила применяются ко всем агентам, работающим конкретно в этом репозитории.
|
||||||
|
|
||||||
1. **Полная перегенерация standalone-скриптов при запуске сетап-скрипта (Full regeneration of standalone scripts on setup run):** При каждом запуске `ai-setup.sh` все генерируемые скрипты в `~/.local/bin/` (`ai-gpt`, `ai-deepseek`, `ai-kimi`, `ai-gemini`, `ai-api-helpers.sh`, `ai-claude`) должны быть **полностью перезаписаны** актуальными версиями. Запрещено выполнять слияние (merge) старого и нового содержимого или дополнение (append). Скрипт обязан привести все генерируемые файлы к эталонному виду, однозначно определяемому текущей конфигурацией.
|
1. **Полная перегенерация standalone-скриптов при запуске сетап-скрипта (Full regeneration of standalone scripts on setup run):** При каждом запуске `ai-setup.sh` все генерируемые скрипты в `~/.local/bin/` (`ai-gpt`, `ai-deepseek`, `ai-kimi`, `ai-openrouter`, `ai-gemini`, `ai-api-helpers.sh`, `ai-claude`) должны быть **полностью перезаписаны** актуальными версиями. Запрещено выполнять слияние (merge) старого и нового содержимого или дополнение (append). Скрипт обязан привести все генерируемые файлы к эталонному виду, однозначно определяемому текущей конфигурацией.
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
|
|
||||||
## Таблица маппинга
|
## Таблица маппинга
|
||||||
|
|
||||||
> Актуально на 30 мая 2026 г.
|
> Актуально на 12 июня 2026 г.
|
||||||
|
|
||||||
| Claude Code<br>`/effort` | Anthropic<br>(Claude) | GPT-5.5<br>(ChatGPT) | DeepSeek V4 | Kimi K2.6<br>(Moonshot) | Gemini 3.x |
|
| Claude Code<br>`/effort` | Anthropic<br>(Claude) | GPT-5.5<br>(ChatGPT) | DeepSeek V4 | Kimi K2.7<br>(Moonshot) | Gemini 3.x |
|
||||||
|:---:|:---:|:---:|:---:|:---:|:---:|
|
|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
| `low` | ✅ `low` | ✅ `low` | ⬆ `high` | 🔛 thinking on | ✅ `LOW` |
|
| `low` | ✅ `low` | ✅ `low` | ⬆ `high` | 🔛 thinking on | ✅ `LOW` |
|
||||||
| `medium` | ✅ `medium` | ✅ `medium` | ⬆ `high` | 🔛 thinking on | ✅ `MEDIUM` |
|
| `medium` | ✅ `medium` | ✅ `medium` | ⬆ `high` | 🔛 thinking on | ✅ `MEDIUM` |
|
||||||
@@ -54,7 +54,7 @@ high → max
|
|||||||
- `xhigh` → автоматически поднимается до `max`
|
- `xhigh` → автоматически поднимается до `max`
|
||||||
- Маппинг выполняется на стороне DeepSeek API
|
- Маппинг выполняется на стороне DeepSeek API
|
||||||
|
|
||||||
### Kimi K2.6 (Moonshot AI)
|
### Kimi K2.7 (Moonshot AI)
|
||||||
```
|
```
|
||||||
on / off
|
on / off
|
||||||
```
|
```
|
||||||
@@ -80,11 +80,37 @@ MINIMAL → LOW → MEDIUM → HIGH
|
|||||||
| Anthropic | Не нужен | — |
|
| Anthropic | Не нужен | — |
|
||||||
| GPT-5.5 | effort-proxy (наш) | `~/.local/bin/claude-gpt-effort-proxy.py` |
|
| GPT-5.5 | effort-proxy (наш) | `~/.local/bin/claude-gpt-effort-proxy.py` |
|
||||||
| DeepSeek V4 | DeepSeek API | На стороне сервера |
|
| DeepSeek V4 | DeepSeek API | На стороне сервера |
|
||||||
| Kimi K2.6 | Moonshot API | На стороне сервера |
|
| Kimi K2.7 | Moonshot API | На стороне сервера |
|
||||||
| Gemini 3.x | antigravity-claude-proxy | npm пакет |
|
| Gemini 3.x | antigravity-claude-proxy | npm пакет |
|
||||||
|
|
||||||
|
## Persistence effort между сессиями
|
||||||
|
|
||||||
|
Каждый лаунчер (`ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-openrouter`) запоминает свой
|
||||||
|
уровень effort отдельно. Логика гибридная:
|
||||||
|
|
||||||
|
- **`low` / `medium` / `high` / `xhigh`** живут нативно в `settings.json` лаунчера.
|
||||||
|
`/effort` внутри сессии работает как обычно, уровень сохраняется между сессиями.
|
||||||
|
- **`max`** — единственный, который Claude Code **не сохраняет** в `settings.json`
|
||||||
|
(он session-only). Поэтому его восстанавливаем через `CLAUDE_CODE_EFFORT_LEVEL`.
|
||||||
|
Текущий уровень (включая `max`) статусбар пишет в `~/.cache/ai-setup/effort_<launcher>`.
|
||||||
|
|
||||||
|
**Важное следствие (только для `max`):** когда восстановлена `max`-сессия, выставлена
|
||||||
|
`CLAUDE_CODE_EFFORT_LEVEL=max`, и `/effort` внутри неё **не сменит** уровень
|
||||||
|
(env-переменная — жёсткий override Claude Code). На остальных уровнях `/effort` свободен.
|
||||||
|
|
||||||
|
**Как выйти из `max` (или форсить любой уровень):** перезапусти лаунчер с `AI_EFFORT`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AI_EFFORT=max ai-deepseek # включить и запомнить max
|
||||||
|
AI_EFFORT=high ai-deepseek # вернуться на high (выйти из max)
|
||||||
|
ai-deepseek # без флага - восстанавливает последний уровень
|
||||||
|
```
|
||||||
|
|
||||||
|
Дефолты при пустом кэше: `xhigh` для `ai-claude`, `high` для остальных.
|
||||||
|
|
||||||
## Рекомендации
|
## Рекомендации
|
||||||
|
|
||||||
- **Для повседневной работы:** `high` или `xhigh` — работает одинаково хорошо у всех провайдеров
|
- **Для повседневной работы:** `high` или `xhigh` — работает одинаково хорошо у всех провайдеров
|
||||||
- **`max` effort:** имеет реальный эффект только у **Anthropic** и **DeepSeek**. Для GPT маппится в `xhigh`, для Gemini и Kimi — в их максимальный уровень
|
- **`max` effort:** имеет реальный эффект только у **Anthropic** и **DeepSeek**. Для GPT маппится в `xhigh`, для Gemini и Kimi — в их максимальный уровень
|
||||||
- **`low`/`medium`:** у DeepSeek и Kimi фактически не снижают reasoning — DeepSeek поднимет до `high`, Kimi просто включит thinking
|
- **`low`/`medium`:** у DeepSeek и Kimi фактически не снижают reasoning — DeepSeek поднимет до `high`, Kimi просто включит thinking
|
||||||
|
- **Смена уровня:** на `low..xhigh` обычным `/effort`; из `max` — через `AI_EFFORT=<lvl> ai-<launcher>` (в max-сессии `/effort` залочен env-переменной, см. «Persistence effort»)
|
||||||
|
|||||||
61
QUICK_START.md
Normal file
61
QUICK_START.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Quick Start
|
||||||
|
|
||||||
|
## 1. Установить AI-инструменты
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Спросит про VLESS-прокси (можно пропустить — `n`), затем установит лаунчеры
|
||||||
|
`ai-claude`, `ai-gpt`, `ai-deepseek`, `ai-kimi`, `ai-openrouter`, `ai-gemini`
|
||||||
|
в `~/.local/bin/` и запишет API-ключи.
|
||||||
|
|
||||||
|
После установки, если команды не видны:
|
||||||
|
```bash
|
||||||
|
exec bash
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Настроить сеть (Amnezia + ru-bypass)
|
||||||
|
|
||||||
|
Нужно один раз на каждой машине. .ru трафик идёт напрямую,
|
||||||
|
остальное — через Amnezia. Если Amnezia падает, не-.ru блокируется.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запустить Amnezia, затем узнать gateway и интерфейс:
|
||||||
|
ip route show default
|
||||||
|
# Пример: default via 192.168.1.1 dev wlp1s0
|
||||||
|
|
||||||
|
# Запустить скрипт (дефолты: GATEWAY=192.168.1.1, DEV=wlp1s0)
|
||||||
|
sudo bash scripts/ru-bypass.sh
|
||||||
|
|
||||||
|
# Если параметры отличаются:
|
||||||
|
sudo GATEWAY=10.0.0.1 DEV=enp3s0 bash scripts/ru-bypass.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
После первого запуска systemd-сервисы установлены — при перезагрузке всё поднимается само.
|
||||||
|
|
||||||
|
## Проверка сети
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ip route get 8.8.8.8 # -> dev amn0 (через VPN)
|
||||||
|
ip route get $(dig +short ya.ru A | head -1) # -> dev wlp1s0 (напрямую)
|
||||||
|
bash tests/test_network.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Временно отключить VPN (нужен российский IP)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Выйти из Claude Code, затем:
|
||||||
|
sudo bash scripts/ks-off.sh
|
||||||
|
# Отключить Amnezia в GUI
|
||||||
|
|
||||||
|
# Вернуться к нормальному режиму:
|
||||||
|
# Подключить Amnezia в GUI, затем:
|
||||||
|
sudo bash scripts/ks-on.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Подробнее
|
||||||
|
|
||||||
|
- `README.md` — полное описание всего проекта
|
||||||
|
- `home-configs/network/README.md` — детали сетевой настройки
|
||||||
|
- `EFFORT_MAPPING.md` — таблица effort-уровней по провайдерам
|
||||||
260
README.md
260
README.md
@@ -1,59 +1,55 @@
|
|||||||
# AI Setup
|
# AI Setup
|
||||||
|
|
||||||
Набор shell-лаунчеров для локальной установки и запуска нескольких AI coding CLI из единой точки входа.
|
Набор shell-скриптов для установки AI coding CLI и настройки сети с Amnezia VPN.
|
||||||
|
|
||||||
Главный скрипт - `ai-setup.sh`. Он настраивает пользовательские директории, устанавливает глобальные правила агентов из `GLOBAL_RULES.md` и полностью перегенерирует standalone-скрипты в `~/.local/bin`.
|
Точка входа - `setup.sh` (мастер-скрипт с меню). Он вызывает нужный скрипт из `scripts/`.
|
||||||
|
|
||||||
## Что реально устанавливается и генерируется
|
## Структура репозитория
|
||||||
|
|
||||||
`ai-setup.sh` работает с такими путями:
|
|
||||||
|
|
||||||
- `~/.local/bin` - standalone-лаунчеры и вспомогательные скрипты.
|
|
||||||
- `~/.config/ai-setup` - сохранённые ключи и глобальные правила.
|
|
||||||
- `~/.npm-global` - пользовательский npm prefix.
|
|
||||||
|
|
||||||
После запуска генерируются или обновляются:
|
|
||||||
|
|
||||||
- `~/.local/bin/ai-claude`
|
|
||||||
- `~/.local/bin/ai-gpt`
|
|
||||||
- `~/.local/bin/ai-deepseek`
|
|
||||||
- `~/.local/bin/ai-kimi`
|
|
||||||
- `~/.local/bin/ai-gemini`
|
|
||||||
- `~/.local/bin/ai-api-helpers.sh`
|
|
||||||
- `~/.local/bin/claude-gpt-effort-proxy.py`
|
|
||||||
|
|
||||||
Все генерируемые standalone-скрипты полностью перезаписываются текущей эталонной версией из `ai-setup.sh`. Старое содержимое не сливается и не дописывается.
|
|
||||||
|
|
||||||
## Структура конфигов
|
|
||||||
|
|
||||||
Все конфиги, которые разворачиваются в домашнюю директорию, живут в папке `home-configs/`:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
setup.sh # мастер-скрипт с меню
|
||||||
|
scripts/
|
||||||
|
├── ai-setup.sh # устанавливает AI-инструменты в ~/.local/bin
|
||||||
|
├── ru-bypass.sh # .ru трафик напрямую, kill switch для остального
|
||||||
|
├── ks-off.sh # временно отключить kill switch
|
||||||
|
└── ks-on.sh # восстановить kill switch
|
||||||
home-configs/
|
home-configs/
|
||||||
├── GLOBAL_RULES.md # глобальные правила для всех агентов
|
├── GLOBAL_RULES.md # глобальные правила для всех агентов
|
||||||
└── claude/
|
├── claude/
|
||||||
└── skills/ # кастомные скиллы для Claude Code
|
│ └── skills/ # кастомные скиллы для Claude Code (и Gemini)
|
||||||
├── el-review/
|
├── network/
|
||||||
│ └── SKILL.md
|
│ └── README.md # подробная документация по сетевой настройке
|
||||||
└── el-review-heavy/
|
├── vless/
|
||||||
└── SKILL.md
|
│ └── servers.conf # список VLESS-серверов для прокси
|
||||||
|
└── proxychains/
|
||||||
|
└── proxychains-xray.conf # конфиг proxychains (SOCKS5 через xray)
|
||||||
|
tests/
|
||||||
|
├── test_fixes.sh # юнит-тесты структуры ai-setup.sh
|
||||||
|
└── test_network.sh # тесты маршрутизации
|
||||||
|
test_isolated.sh # проверка автоустановки ai-gpt и ai-kimi
|
||||||
```
|
```
|
||||||
|
|
||||||
При запуске `ai-setup.sh`:
|
|
||||||
- `GLOBAL_RULES.md` копируется в `~/.config/ai-setup/global_rules.md` и рассылается в нативные rule-файлы (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md` и т.д.)
|
|
||||||
- Скиллы из `home-configs/claude/skills/` копируются в `~/.claude/skills/`
|
|
||||||
|
|
||||||
Также скрипт при отсутствии скачивает `~/.local/bin/claude-code-proxy`, но текущий `ai-gpt` запускает нативный Codex CLI и не использует старую proxy-логику через `ANTHROPIC_BASE_URL`.
|
|
||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
|
|
||||||
Запускать от обычного пользователя, не через `sudo`:
|
Запускать от обычного пользователя, не через `sudo`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash ai-setup.sh
|
bash setup.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Скрипт прямо запрещает запуск от root. При этом, если Node.js не найден, он может попытаться установить Node.js через `apt-get` или `dnf` и тогда попросит `sudo` уже внутри этого шага.
|
Мастер-скрипт показывает меню:
|
||||||
|
|
||||||
|
```
|
||||||
|
Шаги для новой машины:
|
||||||
|
1) AI-инструменты
|
||||||
|
2) Сеть: ru-bypass + kill switch
|
||||||
|
|
||||||
|
Дополнительно (по необходимости):
|
||||||
|
3) Отключить kill switch
|
||||||
|
4) Включить kill switch
|
||||||
|
5) Статус
|
||||||
|
6) Проверить сеть
|
||||||
|
```
|
||||||
|
|
||||||
После установки, если shell ещё не видит новые команды:
|
После установки, если shell ещё не видит новые команды:
|
||||||
|
|
||||||
@@ -61,83 +57,159 @@ bash ai-setup.sh
|
|||||||
exec bash
|
exec bash
|
||||||
```
|
```
|
||||||
|
|
||||||
## Требования
|
## Сеть: ru-bypass + kill switch
|
||||||
|
|
||||||
- `bash`
|
Полная документация: [`home-configs/network/README.md`](home-configs/network/README.md)
|
||||||
- `curl`
|
|
||||||
- `python3`
|
|
||||||
- Node.js/npm для npm-глобальных инструментов
|
|
||||||
|
|
||||||
Если Node.js отсутствует, скрипт пытается поставить его автоматически для систем с `apt-get` или `dnf`. Для остальных систем Node.js нужно поставить вручную.
|
### Что это
|
||||||
|
|
||||||
## Команды
|
- **.ru сайты** (ozon.ru, госуслуги и др.) - идут **напрямую** через провайдера с российским IP
|
||||||
|
- **\*.loc офисные адреса** - тоже напрямую через локальный роутер
|
||||||
|
- **Всё остальное** - только через Amnezia VPN
|
||||||
|
- **Если Amnezia упала** - не-.ru трафик блокируется UFW (kill switch), .ru и *.loc продолжают работать
|
||||||
|
|
||||||
- `ai-claude` - запускает оригинальный Claude Code через `claude`.
|
### Как работает
|
||||||
- `ai-gpt` - запускает нативный OpenAI Codex CLI, при отсутствии пытается поставить его через `https://chatgpt.com/codex/install.sh`.
|
|
||||||
- `ai-deepseek` - запускает Claude Code через DeepSeek Anthropic-compatible API, проверяет и сохраняет DeepSeek API key.
|
|
||||||
- `ai-kimi` - запускает Claude Code через официальный Kimi Code API (`https://api.kimi.com/coding/`), проверяет и сохраняет Kimi API key.
|
|
||||||
- `ai-gemini` - запускает нативный Antigravity CLI `agy`, при отсутствии пытается поставить его через `https://antigravity.google/cli/install.sh`.
|
|
||||||
|
|
||||||
Для `ai-gemini` скрипт в конце отдельно предупреждает использовать отдельный Google-аккаунт.
|
Amnezia захватывает весь трафик двумя широкими маршрутами (`0.0.0.0/1` и `128.0.0.0/1` через `amn0`).
|
||||||
|
`ru-bypass.sh` добавляет тысячи более специфичных маршрутов для .ru IP-блоков через локальный роутер.
|
||||||
|
Ядро Linux выбирает самый специфичный маршрут - .ru идёт напрямую, остальное в amn0.
|
||||||
|
|
||||||
|
UFW настроен так:
|
||||||
|
```
|
||||||
|
default deny outgoing — запрещено всё по умолчанию
|
||||||
|
allow out on amn0 — через Amnezia можно всё
|
||||||
|
before.rules: ipset ru-direct — для .ru IP разрешён прямой выход
|
||||||
|
before.rules: RFC1918 — 10/8, 172.16/12, 192.168/16 тоже напрямую (*.loc)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Первый запуск
|
||||||
|
|
||||||
|
Через меню (`bash setup.sh` → пункт 2) - он автоматически определит GATEWAY и DEV
|
||||||
|
из `ip route show default` и сохранит их в `~/.config/ai-setup/network_$(hostname).conf`.
|
||||||
|
|
||||||
|
Или напрямую:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash scripts/ru-bypass.sh
|
||||||
|
# для другой сети:
|
||||||
|
sudo GATEWAY=10.0.0.1 DEV=eth0 bash scripts/ru-bypass.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Что устанавливается при первом запуске
|
||||||
|
|
||||||
|
- `ipset` (если не установлен)
|
||||||
|
- Скрипт копируется в `/usr/local/bin/ru-bypass.sh`
|
||||||
|
- `ru-ipset-restore.service` - восстанавливает ipset из файла **до старта UFW** при загрузке
|
||||||
|
- `ru-bypass.service` - обновляет RIPE-список и маршруты после network-online
|
||||||
|
- NM dispatcher `/etc/NetworkManager/dispatcher.d/99-ru-bypass` - перезапускает скрипт при поднятии amn0
|
||||||
|
|
||||||
|
При каждом запуске:
|
||||||
|
|
||||||
|
- Скачивает список .ru IP-блоков из RIPE (кэш 24ч, `/var/cache/ru-delegations.txt`)
|
||||||
|
- Обновляет ipset `ru-direct` (~11000 записей)
|
||||||
|
- Сохраняет ipset в `/etc/ipset.conf` для восстановления после ребута
|
||||||
|
- Добавляет маршруты через GATEWAY для всех .ru блоков и RFC1918
|
||||||
|
|
||||||
|
### ks-off / ks-on
|
||||||
|
|
||||||
|
Для ситуаций когда нужен временный доступ без VPN (сайты, блокирующие нероссийский IP):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Выйти из Claude Code
|
||||||
|
# 2. Отключить kill switch
|
||||||
|
sudo bash scripts/ks-off.sh
|
||||||
|
# 3. Отключить Amnezia в GUI
|
||||||
|
|
||||||
|
# Возврат:
|
||||||
|
# 4. Подключить Amnezia в GUI (дождаться amn0)
|
||||||
|
sudo bash scripts/ks-on.sh
|
||||||
|
# 5. Войти в Claude Code
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ip route get 8.8.8.8 # -> dev amn0 (Google через VPN)
|
||||||
|
ip route get 77.88.8.8 # -> dev wlp1s0 (ya.ru напрямую)
|
||||||
|
ip route get 10.10.0.1 # -> dev wlp1s0 (*.loc напрямую)
|
||||||
|
bash tests/test_network.sh # полные тесты
|
||||||
|
```
|
||||||
|
|
||||||
|
## AI инструменты
|
||||||
|
|
||||||
|
`scripts/ai-setup.sh` устанавливает и настраивает все AI CLI.
|
||||||
|
|
||||||
|
После запуска генерируются или обновляются в `~/.local/bin`:
|
||||||
|
|
||||||
|
- `ai-claude` - Claude Code (Anthropic API)
|
||||||
|
- `ai-gpt` - нативный OpenAI Codex CLI
|
||||||
|
- `ai-deepseek` - Claude Code через DeepSeek API
|
||||||
|
- `ai-kimi` - Claude Code через официальный Kimi Code API (`api.kimi.com/coding`)
|
||||||
|
- `ai-openrouter` - Claude Code через OpenRouter (GPT-5.5, Claude и др.)
|
||||||
|
- `ai-gemini` - нативный Antigravity CLI `agy`
|
||||||
|
- `ai-api-helpers.sh` - вспомогательные функции для лаунчеров
|
||||||
|
- `claude-gpt-effort-proxy.py` - прокси для маппинга effort-уровней (GPT backend)
|
||||||
|
|
||||||
|
Все генерируемые скрипты полностью перезаписываются при каждом запуске `scripts/ai-setup.sh`.
|
||||||
|
|
||||||
|
Также устанавливается:
|
||||||
|
- `~/.config/ai-setup/global_rules.md` и native rule-файлы (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, `~/.kimi-code/AGENTS.md`, `~/.gemini/GEMINI.md`)
|
||||||
|
- Скиллы из `home-configs/claude/skills/` в `~/.claude/skills/` и `~/.gemini/config/plugins/local-setup/skills/`
|
||||||
|
|
||||||
|
## VLESS / Xray (опционально)
|
||||||
|
|
||||||
|
При запуске `scripts/ai-setup.sh` спрашивает, нужен ли VLESS-прокси для AI API запросов.
|
||||||
|
|
||||||
|
Если выбрать **Y**:
|
||||||
|
- Читает список серверов из `home-configs/vless/servers.conf`
|
||||||
|
- Проверяет каждый сервер реальным curl'ом через SOCKS5
|
||||||
|
- Устанавливает Xray, генерирует конфиг, создаёт systemd сервис
|
||||||
|
- Все лаунчеры оборачиваются в `proxychains4`
|
||||||
|
|
||||||
|
Если выбрать **n** - VLESS отключается, прокси и IPv6 сбрасываются в дефолт.
|
||||||
|
|
||||||
## Ключи и конфиги
|
## Ключи и конфиги
|
||||||
|
|
||||||
- DeepSeek key хранится в `~/.config/ai-setup/deepseek_key` с правами `600`.
|
- `~/.config/ai-setup/deepseek_key` - DeepSeek API key (права 600)
|
||||||
- Kimi key хранится в `~/.config/ai-setup/kimi_key` с правами `600`.
|
- `~/.config/ai-setup/kimi_key` - Kimi API key (права 600)
|
||||||
- Исходник глобальных правил лежит в `home-configs/GLOBAL_RULES.md`.
|
- `~/.config/ai-setup/openrouter_key` - OpenRouter API key (права 600)
|
||||||
- При запуске глобальные правила пишутся в `~/.config/ai-setup/global_rules.md`.
|
- `~/.config/ai-setup/global_rules.md` - глобальные правила агентов
|
||||||
|
- `~/.config/ai-setup/network_$(hostname).conf` - сохранённые GATEWAY/DEV для текущей машины
|
||||||
При запуске `ai-setup.sh` сразу обновляются native rule-файлы:
|
|
||||||
|
|
||||||
- `~/.codex/AGENTS.md`
|
|
||||||
- `~/.kimi-code/AGENTS.md`
|
|
||||||
- `~/.claude/CLAUDE.md`
|
|
||||||
- `~/.gemini/GEMINI.md`
|
|
||||||
|
|
||||||
Лаунчеры дополнительно обновляют эти файлы через helper `_build_ai_sys_prompt` при своём запуске.
|
|
||||||
|
|
||||||
В native rule-файлы попадают только глобальные правила. Полный prompt с проектными `.md` используется в `ai-claude`, `ai-deepseek` и `ai-gemini`; `ai-gpt` и `ai-kimi` полагаются на native rule-файлы своих CLI.
|
|
||||||
|
|
||||||
## Права запуска агентов
|
## Права запуска агентов
|
||||||
|
|
||||||
Лаунчеры запускают CLI в максимально свободном режиме:
|
- `ai-gpt` использует `--dangerously-bypass-approvals-and-sandbox`
|
||||||
|
- `ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-openrouter`, `ai-gemini` используют `--dangerously-skip-permissions`
|
||||||
|
|
||||||
- `ai-gpt` использует `--dangerously-bypass-approvals-and-sandbox`.
|
Удобно для локального coding workflow, но это не sandbox для недоверенного кода.
|
||||||
- `ai-claude`, `ai-deepseek`, `ai-kimi` и `ai-gemini` используют `--dangerously-skip-permissions`.
|
|
||||||
|
|
||||||
Это удобно для локального coding workflow, но это не sandbox для недоверенного кода.
|
|
||||||
|
|
||||||
## Правила агентов
|
## Правила агентов
|
||||||
|
|
||||||
Действуют правила Карпати как есть: английский блок из `GLOBAL_RULES.md` устанавливается в `~/.config/ai-setup/global_rules.md` без перевода и смысловых правок.
|
В `home-configs/GLOBAL_RULES.md` - правила Карпати (Think Before Coding, Simplicity First,
|
||||||
|
Surgical Changes, Goal-Driven Execution) плюс пользовательские правила (отвечать по-русски,
|
||||||
|
не коммитить без команды, не делать git add без команды и др.).
|
||||||
|
|
||||||
Кратко правила Карпати:
|
`scripts/ai-setup.sh` копирует их в native rule-файлы всех CLI. Лаунчеры обновляют их при каждом запуске.
|
||||||
|
|
||||||
1. Think Before Coding - не гадать, явно проговаривать допущения, варианты и неясности.
|
## Effort Mapping
|
||||||
2. Simplicity First - писать минимальный код без speculative features и лишней конфигурируемости.
|
|
||||||
3. Surgical Changes - трогать только нужное, не рефакторить соседний код, чистить только свои следы.
|
|
||||||
4. Goal-Driven Execution - формулировать проверяемую цель и доводить работу до верификации.
|
|
||||||
|
|
||||||
Пользовательские глобальные правила:
|
`EFFORT_MAPPING.md` - таблица маппинга effort-уровней (`low`/`medium`/`high`/`xhigh`/`max`)
|
||||||
|
между провайдерами (Anthropic, GPT-5.5, DeepSeek V4, Kimi K2.6, Gemini 3.x).
|
||||||
|
|
||||||
1. Всегда отвечать по-русски, на "ты", дружелюбно и как живой программист.
|
`claude-gpt-effort-proxy.py` маппит `xhigh` → `high` для GPT-бэкенда (нет нативного `xhigh` у GPT).
|
||||||
2. Не выполнять `git commit` без прямой и однозначной просьбы.
|
|
||||||
3. Не выполнять `git add` без прямой просьбы, чтобы изменения оставались видны через обычный `git diff`.
|
|
||||||
4. Использовать обычный дефис `-`, не em dash.
|
|
||||||
5. В начале работы внимательно учитывать все проектные `.md` файлы.
|
|
||||||
6. При повторяющихся инструкциях предлагать reusable skill, но создавать или менять skill-файлы только после явного согласия.
|
|
||||||
|
|
||||||
Правило этого проекта:
|
## Требования
|
||||||
|
|
||||||
1. При каждом запуске `ai-setup.sh` все генерируемые standalone-скрипты в `~/.local/bin` должны полностью перезаписываться актуальными версиями. Merge и append старого содержимого запрещены.
|
- `bash`, `curl`, `python3`
|
||||||
|
- Node.js/npm (для ai-gpt через Codex CLI и ai-gemini через agy)
|
||||||
|
- `ipset` (устанавливается автоматически при запуске ru-bypass.sh)
|
||||||
|
- (опционально) `proxychains-ng` - для VLESS-режима
|
||||||
|
- (опционально) `gsettings` - для автонастройки системного прокси GNOME
|
||||||
|
|
||||||
## Тесты
|
## Тесты
|
||||||
|
|
||||||
В репозитории есть shell-тесты:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash tests/test_fixes.sh
|
bash tests/test_fixes.sh # структура ai-setup.sh и синтаксис bash
|
||||||
bash test_isolated.sh
|
bash test_isolated.sh # автоустановка ai-gpt и ai-kimi через mock curl
|
||||||
|
bash tests/test_network.sh # маршрутизация (нужен активный ru-bypass)
|
||||||
```
|
```
|
||||||
|
|
||||||
`tests/test_fixes.sh` проверяет структуру `ai-setup.sh` и синтаксис bash. `test_isolated.sh` проверяет автоустановочные URL для `ai-gpt` и `ai-kimi` через mock `curl`.
|
|
||||||
|
|||||||
33
home-configs/claude/hooks/account-email.sh
Normal file
33
home-configs/claude/hooks/account-email.sh
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Определяет email Claude.ai аккаунта по OAuth-токену в credentials-файле.
|
||||||
|
# Источник истины — сам токен (НЕ claude auth status, который читает
|
||||||
|
# рассинхронизированный oauthAccount из ~/.claude.json).
|
||||||
|
# Сначала локальный матчинг с сохранёнными accounts/, затем API /api/oauth/profile.
|
||||||
|
# Использование: account-email.sh [credentials-file] (по умолчанию ~/.claude/.credentials.json)
|
||||||
|
|
||||||
|
CREDS="${1:-$HOME/.claude/.credentials.json}"
|
||||||
|
ACCOUNTS_DIR="$HOME/.claude/accounts"
|
||||||
|
|
||||||
|
[ -f "$CREDS" ] || exit 0
|
||||||
|
token=$(jq -r '.claudeAiOauth.accessToken // empty' "$CREDS" 2>/dev/null)
|
||||||
|
[ -z "$token" ] && exit 0
|
||||||
|
|
||||||
|
# 1) Локальный матчинг по токену с сохранёнными аккаунтами (мгновенно)
|
||||||
|
if [ -d "$ACCOUNTS_DIR" ]; then
|
||||||
|
for f in "$ACCOUNTS_DIR"/*.credentials.json; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
t=$(jq -r '.claudeAiOauth.accessToken // empty' "$f" 2>/dev/null)
|
||||||
|
if [ -n "$t" ] && [ "$t" = "$token" ]; then
|
||||||
|
basename "$f" .credentials.json
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2) Достоверно через API (по реальному владельцу токена)
|
||||||
|
email=$(curl -s --max-time 15 "https://api.anthropic.com/api/oauth/profile" \
|
||||||
|
-H "Authorization: Bearer $token" \
|
||||||
|
-H "anthropic-beta: oauth-2025-04-20" 2>/dev/null \
|
||||||
|
| jq -r '.account.email // empty' 2>/dev/null)
|
||||||
|
[ -n "$email" ] && echo "$email"
|
||||||
|
exit 0
|
||||||
47
home-configs/claude/hooks/add-account-hook.sh
Executable file
47
home-configs/claude/hooks/add-account-hook.sh
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# UserPromptSubmit hook: перехватывает /add-account.
|
||||||
|
# 1) сохраняет текущий аккаунт по его реальному email (account-email.sh)
|
||||||
|
# 2) запускает oauth-логин в фоне (открывает браузер)
|
||||||
|
# 3) после логина фоновый процесс сам определяет email нового аккаунта по токену
|
||||||
|
# и сохраняет его credentials + делает current
|
||||||
|
|
||||||
|
input=$(cat)
|
||||||
|
prompt=$(echo "$input" | jq -r '.user_prompt // .prompt // empty' 2>/dev/null)
|
||||||
|
normalized=$(echo "$prompt" | sed 's|^[[:space:]]*/||; s|[[:space:]]*$||')
|
||||||
|
|
||||||
|
[ "$normalized" != "add-account" ] && exit 0
|
||||||
|
|
||||||
|
CREDS="$HOME/.claude/.credentials.json"
|
||||||
|
ACCOUNTS_DIR="$HOME/.claude/accounts"
|
||||||
|
CURRENT_FILE="$ACCOUNTS_DIR/current"
|
||||||
|
EMAIL_HELPER="$HOME/.claude/hooks/account-email.sh"
|
||||||
|
|
||||||
|
mkdir -p "$ACCOUNTS_DIR"
|
||||||
|
|
||||||
|
# Сохраняем текущий активный аккаунт под его реальным email (по токену)
|
||||||
|
if [ -f "$CREDS" ]; then
|
||||||
|
cur_email=$(bash "$EMAIL_HELPER" "$CREDS" 2>/dev/null)
|
||||||
|
if [ -n "$cur_email" ]; then
|
||||||
|
cp "$CREDS" "$ACCOUNTS_DIR/${cur_email}.credentials.json"
|
||||||
|
chmod 600 "$ACCOUNTS_DIR/${cur_email}.credentials.json"
|
||||||
|
echo "$cur_email" > "$CURRENT_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Фоновый процесс: логин нового аккаунта + автосохранение после успеха.
|
||||||
|
# claude auth login ждёт авторизации в браузере и завершается после неё,
|
||||||
|
# затем определяем email нового аккаунта по токену (через API) и сохраняем.
|
||||||
|
(
|
||||||
|
claude auth login --claudeai </dev/null >/tmp/claude-add-account.log 2>&1
|
||||||
|
new_email=$(bash "$EMAIL_HELPER" "$CREDS" 2>/dev/null)
|
||||||
|
if [ -n "$new_email" ] && [ -f "$CREDS" ]; then
|
||||||
|
cp "$CREDS" "$ACCOUNTS_DIR/${new_email}.credentials.json"
|
||||||
|
chmod 600 "$ACCOUNTS_DIR/${new_email}.credentials.json"
|
||||||
|
echo "$new_email" > "$CURRENT_FILE"
|
||||||
|
echo "SAVED: $new_email" >> /tmp/claude-add-account.log
|
||||||
|
fi
|
||||||
|
) &
|
||||||
|
disown
|
||||||
|
|
||||||
|
# exit 0: Claude загружает скилл add-account и говорит что делать
|
||||||
|
exit 0
|
||||||
54
home-configs/claude/hooks/switch-account-hook.sh
Executable file
54
home-configs/claude/hooks/switch-account-hook.sh
Executable file
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# UserPromptSubmit hook: перехватывает /switch-account и циклически меняет аккаунт.
|
||||||
|
# Текущий аккаунт определяется ПО ТОКЕНУ в .credentials.json (account-email.sh),
|
||||||
|
# а не по claude auth status — он читает рассинхронизированный oauthAccount.
|
||||||
|
# На Linux Claude Code перечитывает .credentials.json на лету: новый аккаунт
|
||||||
|
# применяется со следующего сообщения, перезапуск не нужен.
|
||||||
|
# exit 0 (не exit 2): /switch-account доходит до Claude, грузится скилл,
|
||||||
|
# отвечает "✓" (1 токен) — так перерисовывается статусная строка.
|
||||||
|
|
||||||
|
input=$(cat)
|
||||||
|
prompt=$(echo "$input" | jq -r '.user_prompt // .prompt // empty' 2>/dev/null)
|
||||||
|
normalized=$(echo "$prompt" | sed 's|^[[:space:]]*/||; s|[[:space:]]*$||')
|
||||||
|
|
||||||
|
[ "$normalized" != "switch-account" ] && exit 0
|
||||||
|
|
||||||
|
ACCOUNTS_DIR="$HOME/.claude/accounts"
|
||||||
|
CREDS="$HOME/.claude/.credentials.json"
|
||||||
|
CURRENT_FILE="$ACCOUNTS_DIR/current"
|
||||||
|
EMAIL_HELPER="$HOME/.claude/hooks/account-email.sh"
|
||||||
|
|
||||||
|
mkdir -p "$ACCOUNTS_DIR"
|
||||||
|
|
||||||
|
# Реальный текущий аккаунт — по токену активной сессии (не по хрупкому current)
|
||||||
|
current=$(bash "$EMAIL_HELPER" "$CREDS" 2>/dev/null)
|
||||||
|
[ -z "$current" ] && current=$(cat "$CURRENT_FILE" 2>/dev/null)
|
||||||
|
|
||||||
|
# Сохранить актуальные (возможно обновлённые рефрешем) токены под реальным email.
|
||||||
|
# current выведен из самого токена — порча файла другого аккаунта исключена.
|
||||||
|
if [ -n "$current" ] && [ -f "$CREDS" ]; then
|
||||||
|
cp "$CREDS" "$ACCOUNTS_DIR/${current}.credentials.json"
|
||||||
|
chmod 600 "$ACCOUNTS_DIR/${current}.credentials.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mapfile -t accounts < <(ls "$ACCOUNTS_DIR"/*.credentials.json 2>/dev/null \
|
||||||
|
| xargs -I{} basename {} .credentials.json | sort)
|
||||||
|
|
||||||
|
if [ ${#accounts[@]} -le 1 ]; then
|
||||||
|
echo "Только один аккаунт (${current:-нет}). Добавь второй через /add-account." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Найти следующий по кругу
|
||||||
|
idx=-1
|
||||||
|
for i in "${!accounts[@]}"; do
|
||||||
|
[ "${accounts[$i]}" = "$current" ] && idx=$i && break
|
||||||
|
done
|
||||||
|
next_idx=$(( (idx + 1) % ${#accounts[@]} ))
|
||||||
|
next="${accounts[$next_idx]}"
|
||||||
|
|
||||||
|
cp "$ACCOUNTS_DIR/${next}.credentials.json" "$CREDS"
|
||||||
|
chmod 600 "$CREDS"
|
||||||
|
echo "$next" > "$CURRENT_FILE"
|
||||||
|
|
||||||
|
exit 0
|
||||||
8
home-configs/claude/skills/add-account/SKILL.md
Normal file
8
home-configs/claude/skills/add-account/SKILL.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
name: add-account
|
||||||
|
description: Add a new Claude.ai account (handled by UserPromptSubmit hook, no LLM needed)
|
||||||
|
---
|
||||||
|
|
||||||
|
Хук сохранил текущий аккаунт и открыл браузер для логина нового. Ответь ТОЛЬКО этим текстом (без markdown, без лишних слов):
|
||||||
|
|
||||||
|
Браузер открыт — авторизуйся там. После авторизации новый аккаунт сохранится автоматически и сразу станет активным (Claude Code на Linux перечитывает токен на лету). /switch-account переключает между всеми сохранёнными аккаунтами по кругу.
|
||||||
@@ -31,6 +31,20 @@ description: Use when пользователь запрашивает тяжел
|
|||||||
- Для каждой предложи готовый дифф с исправлением
|
- Для каждой предложи готовый дифф с исправлением
|
||||||
- Объясни простым языком
|
- Объясни простым языком
|
||||||
|
|
||||||
|
## Примеры вызова
|
||||||
|
|
||||||
|
```
|
||||||
|
/el-review-heavy feature main
|
||||||
|
/el-review-heavy my-fix develop
|
||||||
|
/el-review-heavy TASK-123-payment-screen master
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ошибки и крайние случаи
|
||||||
|
|
||||||
|
- Если ветка не существует на remote - сообщи пользователю и не продолжай
|
||||||
|
- Если дифф пустой - сообщи "изменений между ветками нет" и не продолжай
|
||||||
|
- Если аргументы не указаны - попроси уточнить: `/el-review-heavy <source-ветка> <target-ветка>`
|
||||||
|
|
||||||
## Формат вывода
|
## Формат вывода
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -31,6 +31,20 @@ description: Use when пользователь запрашивает легко
|
|||||||
- Для каждой предложи готовый дифф с исправлением
|
- Для каждой предложи готовый дифф с исправлением
|
||||||
- Объясни простым языком
|
- Объясни простым языком
|
||||||
|
|
||||||
|
## Примеры вызова
|
||||||
|
|
||||||
|
```
|
||||||
|
/el-review feature main
|
||||||
|
/el-review my-fix develop
|
||||||
|
/el-review TASK-123-payment-screen master
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ошибки и крайние случаи
|
||||||
|
|
||||||
|
- Если ветка не существует на remote - сообщи пользователю и не продолжай
|
||||||
|
- Если дифф пустой - сообщи "изменений между ветками нет" и не продолжай
|
||||||
|
- Если аргументы не указаны - попроси уточнить: `/el-review <source-ветка> <target-ветка>`
|
||||||
|
|
||||||
## Формат вывода
|
## Формат вывода
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
8
home-configs/claude/skills/switch-account/SKILL.md
Normal file
8
home-configs/claude/skills/switch-account/SKILL.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
name: switch-account
|
||||||
|
description: Switch to next Claude.ai account (handled by UserPromptSubmit hook, no LLM needed)
|
||||||
|
---
|
||||||
|
|
||||||
|
Переключение аккаунта уже выполнено хуком до того, как ты это читаешь.
|
||||||
|
Ответь ровно одним символом: `✓`
|
||||||
|
Никаких инструментов. Никаких объяснений. Только `✓`.
|
||||||
282
home-configs/claude/statusline-command.sh
Executable file
282
home-configs/claude/statusline-command.sh
Executable file
@@ -0,0 +1,282 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
input=$(cat)
|
||||||
|
cwd=$(echo "$input" | jq -r '.cwd')
|
||||||
|
model=$(echo "$input" | jq -r '.model.display_name // empty')
|
||||||
|
model_id=$(echo "$input" | jq -r '.model.id // empty')
|
||||||
|
five_pct=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
|
||||||
|
five_reset=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty')
|
||||||
|
week_pct=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
|
||||||
|
week_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty')
|
||||||
|
ctx_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
|
||||||
|
|
||||||
|
|
||||||
|
# Цвет effort: насыщенные, глубже чем пастельные (jewel tones)
|
||||||
|
_effort_color() {
|
||||||
|
case "$1" in
|
||||||
|
low) printf '\033[38;5;220m[low]\033[00m' ;; # золотой
|
||||||
|
medium) printf '\033[38;5;50m[medium]\033[00m' ;; # насыщенный teal
|
||||||
|
high) printf '\033[38;5;38m[high]\033[00m' ;; # глубокий голубой
|
||||||
|
xhigh) printf '\033[38;5;206m[xhigh]\033[00m' ;; # насыщенный фиолетовый
|
||||||
|
max) printf '\033[38;5;210m[\033[38;5;220mm\033[38;5;114ma\033[38;5;50mx\033[38;5;206m]\033[00m' ;; # радуга
|
||||||
|
*) printf '\033[38;5;252m[%s]\033[00m' "$1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Брендовый цвет имени модели по лаунчеру
|
||||||
|
_brand_color() {
|
||||||
|
case "${1:-}" in
|
||||||
|
deepseek) printf '\033[38;5;69m' ;; # DeepSeek фирменный синий
|
||||||
|
claude) printf '\033[38;5;173m' ;; # Anthropic оранжевый
|
||||||
|
kimi) printf '\033[38;5;81m' ;; # Kimi голубой
|
||||||
|
openrouter) printf '\033[38;5;135m' ;; # OpenRouter фиолетовый
|
||||||
|
*) printf '\033[38;5;223m' ;; # кремовый (fallback)
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
branch=$(git -C "$cwd" --no-optional-locks symbolic-ref --short HEAD 2>/dev/null)
|
||||||
|
|
||||||
|
short_cwd="${cwd/#$HOME/\~}"
|
||||||
|
printf "\033[38;5;252m%s\033[00m" "$short_cwd"
|
||||||
|
|
||||||
|
[ -n "$branch" ] && printf " \033[38;5;252m[%s]\033[00m" "$branch"
|
||||||
|
if [ -n "$model" ]; then
|
||||||
|
brand_color=$(_brand_color "${AI_LAUNCHER:-}")
|
||||||
|
effort=$(echo "$input" | jq -r ".effort.level // empty")
|
||||||
|
# Ловим выбранный уровень в кэш лаунчера (чтобы запомнить max между сессиями).
|
||||||
|
# Когда CLAUDE_CODE_EFFORT_LEVEL выставлена (восстановленная max-сессия) - уровень
|
||||||
|
# форсится env, кэш НЕ трогаем, чтобы дисплей-баг (.effort.level=xhigh) не затёр max.
|
||||||
|
if [ -n "${AI_LAUNCHER:-}" ] && [ -z "${CLAUDE_CODE_EFFORT_LEVEL:-}" ] && [ -n "$effort" ]; then
|
||||||
|
effort_file="$HOME/.cache/ai-setup/effort_${AI_LAUNCHER}"
|
||||||
|
if [ "$effort" != "$(cat "$effort_file" 2>/dev/null)" ]; then
|
||||||
|
mkdir -p "$HOME/.cache/ai-setup"
|
||||||
|
echo "$effort" > "$effort_file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# Аккаунт Claude.ai актуален только для нативных моделей Claude
|
||||||
|
if [[ "$model_id" == claude-* ]]; then
|
||||||
|
account=$(cat ~/.claude/accounts/current 2>/dev/null)
|
||||||
|
ACCOUNTS_DIR="$HOME/.claude/accounts"
|
||||||
|
# Автоопределение: если current пуст или файл не существует —
|
||||||
|
# ищем аккаунт по access-токену в .credentials.json
|
||||||
|
if [ -z "$account" ] || [ ! -f "$ACCOUNTS_DIR/${account}.credentials.json" ]; then
|
||||||
|
current_token=$(jq -r '.claudeAiOauth.accessToken // empty' "$HOME/.claude/.credentials.json" 2>/dev/null)
|
||||||
|
if [ -n "$current_token" ]; then
|
||||||
|
for f in "$ACCOUNTS_DIR"/*.credentials.json; do
|
||||||
|
[ ! -f "$f" ] && continue
|
||||||
|
token=$(jq -r '.claudeAiOauth.accessToken // empty' "$f" 2>/dev/null)
|
||||||
|
if [ "$token" = "$current_token" ]; then
|
||||||
|
account=$(basename "$f" .credentials.json)
|
||||||
|
echo "$account" > "$ACCOUNTS_DIR/current"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
# Если токен не найден — спрашиваем haiku (один раз на аккаунт)
|
||||||
|
if [ -z "$account" ]; then
|
||||||
|
sentinel="$HOME/.cache/ai-setup/email_fetch_token"
|
||||||
|
prev_token=$(cat "$sentinel" 2>/dev/null)
|
||||||
|
if [ "$prev_token" != "$current_token" ]; then
|
||||||
|
mkdir -p "$HOME/.cache/ai-setup"
|
||||||
|
echo "$current_token" > "$sentinel"
|
||||||
|
email=$(unset ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN; \
|
||||||
|
echo 'какой имейл этого аккаунта? напиши только имейл без других слов.' | \
|
||||||
|
claude --print --model claude-haiku-4-5 --dangerously-skip-permissions \
|
||||||
|
--output-format text --max-turns 1 --tools "" --effort low 2>/dev/null)
|
||||||
|
email=$(echo "$email" | grep -oE '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' | head -1)
|
||||||
|
if [ -n "$email" ]; then
|
||||||
|
cp "$HOME/.claude/.credentials.json" "$ACCOUNTS_DIR/${email}.credentials.json"
|
||||||
|
chmod 600 "$ACCOUNTS_DIR/${email}.credentials.json"
|
||||||
|
account="$email"
|
||||||
|
echo "$account" > "$ACCOUNTS_DIR/current"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
[ -n "$account" ] && printf " %s[%s]\033[00m" "$brand_color" "$account"
|
||||||
|
fi
|
||||||
|
if [ -n "$effort" ]; then
|
||||||
|
printf " %s%s " "$brand_color" "$model"
|
||||||
|
_effort_color "$effort"
|
||||||
|
else
|
||||||
|
printf " %s%s\033[00m" "$brand_color" "$model"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Форматирует оставшееся время до сброса лимита
|
||||||
|
fmt_remaining() {
|
||||||
|
local reset_ts="$1"
|
||||||
|
local now
|
||||||
|
now=$(date +%s)
|
||||||
|
local diff=$(( reset_ts - now ))
|
||||||
|
[ "$diff" -le 0 ] && echo "скоро" && return
|
||||||
|
local d=$(( diff / 86400 ))
|
||||||
|
local h=$(( (diff % 86400) / 3600 ))
|
||||||
|
local m=$(( (diff % 3600) / 60 ))
|
||||||
|
if [ "$d" -gt 0 ]; then
|
||||||
|
echo "${d}д${h}ч"
|
||||||
|
elif [ "$h" -gt 0 ]; then
|
||||||
|
echo "${h}ч${m}м"
|
||||||
|
else
|
||||||
|
echo "${m}м"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Возвращает ANSI-цвет по проценту: зелёный <40%, жёлтый 40-60%, красный >=60%
|
||||||
|
pct_color() {
|
||||||
|
local pct="$1"
|
||||||
|
if [ "$pct" -lt 40 ]; then
|
||||||
|
printf '\033[38;5;114m' # мягкий зелёный
|
||||||
|
elif [ "$pct" -lt 60 ]; then
|
||||||
|
printf '\033[38;5;220m' # золотой (как effort low)
|
||||||
|
else
|
||||||
|
printf '\033[38;5;210m' # мягкий красный
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# --- Баланс DeepSeek ---
|
||||||
|
# Моментально показываем кэшированный баланс, в фоне обновляем через API.
|
||||||
|
if [[ "$model_id" == *deepseek* ]]; then
|
||||||
|
cache_file="$HOME/.cache/ai-setup/deepseek_balance"
|
||||||
|
if [ -f "$cache_file" ]; then
|
||||||
|
balance=$(head -1 "$cache_file")
|
||||||
|
[ -n "$balance" ] && printf " \033[38;5;78m%s\033[00m" "$balance"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Фоновое обновление баланса (не чаще раза в 30 секунд)
|
||||||
|
refresh_ts="$HOME/.cache/ai-setup/deepseek_balance_refresh_ts"
|
||||||
|
now=$(date +%s)
|
||||||
|
last=$(cat "$refresh_ts" 2>/dev/null || echo 0)
|
||||||
|
if [ $(( now - last )) -gt 30 ]; then
|
||||||
|
key_file="$HOME/.config/ai-setup/deepseek_key"
|
||||||
|
if [ -f "$key_file" ]; then
|
||||||
|
echo "$now" > "$refresh_ts" 2>/dev/null
|
||||||
|
(
|
||||||
|
api_key=$(cat "$key_file")
|
||||||
|
resp=$(curl -s --max-time 10 "https://api.deepseek.com/user/balance" \
|
||||||
|
-H "Authorization: Bearer $api_key" \
|
||||||
|
-H "Accept: application/json" 2>/dev/null)
|
||||||
|
if [ -n "$resp" ]; then
|
||||||
|
new_balance=$(echo "$resp" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
d = json.load(sys.stdin)
|
||||||
|
infos = d.get('balance_infos', [])
|
||||||
|
symbols = {'USD': '\$', 'CNY': '\u00a5'}
|
||||||
|
parts = []
|
||||||
|
for info in infos:
|
||||||
|
curr = info.get('currency', '')
|
||||||
|
total = info.get('total_balance', '0')
|
||||||
|
sym = symbols.get(curr, curr)
|
||||||
|
parts.append(f'{sym}{total}')
|
||||||
|
if parts:
|
||||||
|
print(' '.join(parts))
|
||||||
|
" 2>/dev/null)
|
||||||
|
if [ -n "$new_balance" ]; then
|
||||||
|
echo "$new_balance" > "$cache_file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
) &
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif [ "${AI_LAUNCHER:-}" = "openrouter" ]; then
|
||||||
|
# --- Баланс OpenRouter ---
|
||||||
|
# Моментально показываем кэшированный остаток, в фоне обновляем через API.
|
||||||
|
cache_file="$HOME/.cache/ai-setup/openrouter_balance"
|
||||||
|
if [ -f "$cache_file" ]; then
|
||||||
|
balance=$(head -1 "$cache_file")
|
||||||
|
[ -n "$balance" ] && printf " \033[38;5;78m%s\033[00m" "$balance"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Фоновое обновление баланса (не чаще раза в 30 секунд)
|
||||||
|
refresh_ts="$HOME/.cache/ai-setup/openrouter_balance_refresh_ts"
|
||||||
|
now=$(date +%s)
|
||||||
|
last=$(cat "$refresh_ts" 2>/dev/null || echo 0)
|
||||||
|
if [ $(( now - last )) -gt 30 ]; then
|
||||||
|
key_file="$HOME/.config/ai-setup/openrouter_key"
|
||||||
|
if [ -f "$key_file" ]; then
|
||||||
|
echo "$now" > "$refresh_ts" 2>/dev/null
|
||||||
|
(
|
||||||
|
api_key=$(cat "$key_file")
|
||||||
|
resp=$(curl -s --max-time 10 "https://openrouter.ai/api/v1/credits" \
|
||||||
|
-H "Authorization: Bearer $api_key" \
|
||||||
|
-H "Accept: application/json" 2>/dev/null)
|
||||||
|
if [ -n "$resp" ]; then
|
||||||
|
new_balance=$(echo "$resp" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
d = json.load(sys.stdin)
|
||||||
|
data = d.get('data', {})
|
||||||
|
total = data.get('total_credits', 0) or 0
|
||||||
|
usage = data.get('total_usage', 0) or 0
|
||||||
|
remaining = total - usage
|
||||||
|
print(f'${remaining:.2f}')
|
||||||
|
" 2>/dev/null)
|
||||||
|
if [ -n "$new_balance" ]; then
|
||||||
|
echo "$new_balance" > "$cache_file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
) &
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Рейт-лимиты для НЕ-DeepSeek/OpenRouter провайдеров
|
||||||
|
# Кеш специфичен для провайдера (model_id) И аккаунта (account): лимиты привязаны
|
||||||
|
# к аккаунту, поэтому при переключении /switch-account проценты не должны смешиваться.
|
||||||
|
_cache_key=$(echo "${model_id:-unknown}_${account:-}" | sed 's/[^a-zA-Z0-9._-]/_/g')
|
||||||
|
RATE_CACHE="$HOME/.cache/ai-setup/rate_limits_${_cache_key}.cache"
|
||||||
|
mkdir -p "$HOME/.cache/ai-setup"
|
||||||
|
|
||||||
|
# Если есть свежие данные - сохранить в кеш
|
||||||
|
if [ -n "$five_pct" ] || [ -n "$week_pct" ]; then
|
||||||
|
{
|
||||||
|
echo "FIVE_PCT=${five_pct}"
|
||||||
|
echo "FIVE_RESET=${five_reset}"
|
||||||
|
echo "WEEK_PCT=${week_pct}"
|
||||||
|
echo "WEEK_RESET=${week_reset}"
|
||||||
|
} > "$RATE_CACHE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Если нет данных - читать из кеша (старт сессии до первого запроса)
|
||||||
|
if [ -z "$five_pct" ] && [ -z "$week_pct" ] && [ -f "$RATE_CACHE" ]; then
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$RATE_CACHE" 2>/dev/null
|
||||||
|
five_pct="${FIVE_PCT:-}"
|
||||||
|
five_reset="${FIVE_RESET:-}"
|
||||||
|
week_pct="${WEEK_PCT:-}"
|
||||||
|
week_reset="${WEEK_RESET:-}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$five_pct" ] && [ -n "$five_reset" ]; then
|
||||||
|
five_int=$(printf '%.0f' "$five_pct")
|
||||||
|
remaining=$(fmt_remaining "$five_reset")
|
||||||
|
color=$(pct_color "$five_int")
|
||||||
|
printf " %s%s:%s%%\033[00m" "$color" "$remaining" "$five_int"
|
||||||
|
fi
|
||||||
|
if [ -n "$week_pct" ] && [ -n "$week_reset" ]; then
|
||||||
|
week_int=$(printf '%.0f' "$week_pct")
|
||||||
|
remaining=$(fmt_remaining "$week_reset")
|
||||||
|
color=$(pct_color "$week_int")
|
||||||
|
printf " %s%s:%s%%\033[00m" "$color" "$remaining" "$week_int"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ctx:0% при старте новой сессии (нет данных от API)
|
||||||
|
[ -z "$ctx_pct" ] && ctx_pct="0"
|
||||||
|
|
||||||
|
if [ -n "$ctx_pct" ]; then
|
||||||
|
ctx_int=$(printf '%.0f' "$ctx_pct")
|
||||||
|
color=$(pct_color "$ctx_int")
|
||||||
|
printf " %sctx:%s%%\033[00m" "$color" "$ctx_int"
|
||||||
|
|
||||||
|
# Звуковой сигнал при первом достижении 60%
|
||||||
|
alert_file="$HOME/.cache/ai-setup/ctx_alert_state"
|
||||||
|
if [ "$ctx_int" -ge 60 ]; then
|
||||||
|
if [ ! -f "$alert_file" ] || [ "$(cat "$alert_file")" != "alerted" ]; then
|
||||||
|
mkdir -p "$HOME/.cache/ai-setup"
|
||||||
|
echo "alerted" > "$alert_file"
|
||||||
|
(timeout 1s paplay /usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga 2>/dev/null; true) &
|
||||||
|
fi
|
||||||
|
elif [ "$ctx_int" -lt 50 ]; then
|
||||||
|
rm -f "$alert_file" 2>/dev/null
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
158
home-configs/network/README.md
Normal file
158
home-configs/network/README.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# Сетевые скрипты
|
||||||
|
|
||||||
|
Скрипты для окружения с **Amnezia VPN** и **UFW kill switch**.
|
||||||
|
|
||||||
|
Главная идея: весь не-.ru трафик идёт через Amnezia, .ru сайты (ozon.ru, госуслуги и т.д.) -
|
||||||
|
напрямую через провайдера с российским IP. Если Amnezia падает - не-.ru трафик
|
||||||
|
блокируется UFW, .ru продолжает работать.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Скрипты
|
||||||
|
|
||||||
|
| Файл | Назначение |
|
||||||
|
|---|---|
|
||||||
|
| `ru-bypass.sh` | Основной - настраивает .ru маршруты и ipset |
|
||||||
|
| `ks-off.sh` | Временно отключить UFW kill switch (доступ без VPN) |
|
||||||
|
| `ks-on.sh` | Восстановить UFW kill switch |
|
||||||
|
|
||||||
|
Все скрипты запускаются явно от root. Никакой автоустановки нет.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Как работает ru-bypass
|
||||||
|
|
||||||
|
### Принцип
|
||||||
|
|
||||||
|
Amnezia поднимает интерфейс `amn0` и захватывает весь трафик двумя широкими маршрутами:
|
||||||
|
- `0.0.0.0/1 dev amn0`
|
||||||
|
- `128.0.0.0/1 dev amn0`
|
||||||
|
|
||||||
|
`ru-bypass.sh` добавляет тысячи **более специфичных** маршрутов для .ru IP-блоков через
|
||||||
|
локальный роутер (например `95.173.0.0/16 via 192.168.1.1 dev wlp1s0`). Ядро Linux
|
||||||
|
выбирает самый специфичный маршрут - .ru уходит напрямую, всё остальное - в amn0.
|
||||||
|
|
||||||
|
### UFW kill switch
|
||||||
|
|
||||||
|
```
|
||||||
|
default deny outgoing — запрещено всё по умолчанию
|
||||||
|
allow out on amn0 — через Amnezia можно всё
|
||||||
|
before.rules: ipset ru-direct — для .ru IP разрешён прямой выход на wlp1s0
|
||||||
|
before.rules: 10/8, 172.16/12,
|
||||||
|
192.168/16 — RFC1918 (*.loc) разрешён прямой выход на wlp1s0
|
||||||
|
```
|
||||||
|
|
||||||
|
Если Amnezia падает: `amn0` исчезает, не-.ru трафик блокируется UFW. Маршруты .ru
|
||||||
|
через wlp1s0 и правило UFW для ipset остаются - .ru работает.
|
||||||
|
|
||||||
|
### Что делает скрипт при запуске
|
||||||
|
|
||||||
|
1. Скачивает список .ru IP-блоков из RIPE (кэш 24 часа в `/var/cache/ru-delegations.txt`)
|
||||||
|
2. Создаёт/обновляет ipset `ru-direct` (~11000 записей)
|
||||||
|
3. Сохраняет ipset в `/etc/ipset.conf`
|
||||||
|
4. Добавляет маршруты через локальный роутер для всех .ru блоков
|
||||||
|
5. Добавляет маршруты для RFC1918 диапазонов (`10/8`, `172.16/12`, `192.168/16`) - нужно для `*.loc`
|
||||||
|
6. При первом запуске: добавляет правила в `/etc/ufw/before.rules` и устанавливает systemd сервисы
|
||||||
|
|
||||||
|
### Сервисы (устанавливаются однократно)
|
||||||
|
|
||||||
|
- `ru-ipset-restore.service` - запускается **до UFW**, восстанавливает ipset из файла.
|
||||||
|
Нужен потому что UFW стартует рано и не знает об ipset `ru-direct`.
|
||||||
|
- `ru-bypass.service` - запускается после network-online, качает свежий RIPE-список и
|
||||||
|
добавляет маршруты.
|
||||||
|
- NM dispatcher `/etc/NetworkManager/dispatcher.d/99-ru-bypass` - автоматически перезапускает
|
||||||
|
скрипт когда amn0 поднимается (Amnezia переподключилась).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
### Первый запуск / обновление
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash scripts/ru-bypass.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт сам установит сервисы, добавит правило UFW и настроит NM dispatcher.
|
||||||
|
|
||||||
|
### Параметры для другой сети
|
||||||
|
|
||||||
|
По умолчанию используется домашняя сеть (`GATEWAY=192.168.1.1`, `DEV=wlp1s0`).
|
||||||
|
Для другой машины передай параметры через env:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo GATEWAY=10.0.0.1 DEV=enp3s0 bash ru-bypass.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Чтобы узнать нужные значения:
|
||||||
|
```bash
|
||||||
|
ip route show default
|
||||||
|
# Пример: default via 10.0.0.1 dev enp3s0 proto dhcp
|
||||||
|
# ^^^^^^^ ^^^^^^
|
||||||
|
# GATEWAY DEV
|
||||||
|
```
|
||||||
|
|
||||||
|
### Офисная машина
|
||||||
|
|
||||||
|
Если в офисе другой gateway и другой сетевой интерфейс - просто передай их через env.
|
||||||
|
Amnezia там тоже поднимает `amn0`, так что остальное работает одинаково.
|
||||||
|
|
||||||
|
Пример (офис с проводным интерфейсом):
|
||||||
|
```bash
|
||||||
|
sudo GATEWAY=192.168.0.1 DEV=eth0 bash ru-bypass.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ks-off.sh и ks-on.sh
|
||||||
|
|
||||||
|
Для ситуаций когда нужен временный доступ без VPN (сайты, которые блокируют нероссийские
|
||||||
|
IP, например для оплаты или личного кабинета).
|
||||||
|
|
||||||
|
### ks-off.sh - отключить kill switch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash scripts/ks-off.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Отключает UFW. После этого - отключить Amnezia через GUI. Трафик пойдёт напрямую через
|
||||||
|
провайдера (российский IP).
|
||||||
|
|
||||||
|
**Важно:** выйди из Claude Code перед этим - сессия будет идти с другого IP.
|
||||||
|
|
||||||
|
### ks-on.sh - восстановить kill switch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash scripts/ks-on.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Сначала подключи Amnezia через GUI (дождись появления amn0), потом запускай. Иначе
|
||||||
|
скрипт предупредит - без amn0 UFW сразу заблокирует весь интернет.
|
||||||
|
|
||||||
|
### Типичный workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
# Нужен ru-сайт без VPN:
|
||||||
|
1. Выйти из Claude Code
|
||||||
|
2. sudo bash ks-off.sh
|
||||||
|
3. Отключить Amnezia в GUI
|
||||||
|
|
||||||
|
# Возврат к нормальному режиму:
|
||||||
|
4. Подключить Amnezia в GUI (подождать amn0)
|
||||||
|
5. sudo bash ks-on.sh
|
||||||
|
6. Войти в Claude Code
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Проверка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Эти команды без sudo:
|
||||||
|
ip route get 8.8.8.8 # -> dev amn0 (Google через VPN)
|
||||||
|
ip route get 95.173.136.1 # -> dev wlp1s0 (ozon.ru напрямую)
|
||||||
|
ip route get $(dig +short api.anthropic.com A | head -1) # -> dev amn0
|
||||||
|
|
||||||
|
# Полные тесты:
|
||||||
|
bash tests/test_network.sh
|
||||||
|
```
|
||||||
@@ -5,10 +5,15 @@
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
CONFIG_DIR="$HOME/.config/ai-setup"
|
CONFIG_DIR="$HOME/.config/ai-setup"
|
||||||
BIN_DIR="$HOME/.local/bin"
|
# Автоопределение: ~/bin если есть в PATH, иначе ~/.local/bin
|
||||||
|
if [[ ":$PATH:" == *":$HOME/bin:"* ]]; then
|
||||||
|
BIN_DIR="$HOME/bin"
|
||||||
|
else
|
||||||
|
BIN_DIR="$HOME/.local/bin"
|
||||||
|
fi
|
||||||
NPM_GLOBAL="$HOME/.npm-global"
|
NPM_GLOBAL="$HOME/.npm-global"
|
||||||
PROXY_BIN="$BIN_DIR/claude-code-proxy"
|
PROXY_BIN="$BIN_DIR/claude-code-proxy"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
GLOBAL_RULES_SOURCE="$SCRIPT_DIR/home-configs/GLOBAL_RULES.md"
|
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'
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||||
@@ -250,12 +255,16 @@ FJSEOF
|
|||||||
success "Firefox переключён на прямой доступ"
|
success "Firefox переключён на прямой доступ"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Включаем IPv6 обратно
|
# Включаем IPv6 обратно (только если kill switch не активен)
|
||||||
sudo rm -f /etc/sysctl.d/99-disable-ipv6.conf
|
if ufw status | grep -qE "активен|active"; then
|
||||||
sudo sysctl -w net.ipv6.conf.all.disable_ipv6=0 2>/dev/null || true
|
info "UFW kill switch активен — оставляю IPv6 отключённым"
|
||||||
sudo sysctl -w net.ipv6.conf.default.disable_ipv6=0 2>/dev/null || true
|
else
|
||||||
sudo systemctl restart systemd-resolved 2>/dev/null || true
|
sudo rm -f /etc/sysctl.d/99-disable-ipv6.conf
|
||||||
success "IPv6 восстановлен"
|
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
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── 1. npm prefix в домашнюю папку ──────────────────────────
|
# ── 1. npm prefix в домашнюю папку ──────────────────────────
|
||||||
@@ -604,23 +613,232 @@ cp "$CONFIG_DIR/global_rules.md" "$HOME/.claude/CLAUDE.md"
|
|||||||
cp "$CONFIG_DIR/global_rules.md" "$HOME/.gemini/GEMINI.md"
|
cp "$CONFIG_DIR/global_rules.md" "$HOME/.gemini/GEMINI.md"
|
||||||
success "Native rule-файлы обновлены"
|
success "Native rule-файлы обновлены"
|
||||||
|
|
||||||
# ── 6.6. Деплой Claude skills ────────────────────────────────
|
# ── 6.6. Деплой Claude и Gemini skills ───────────────────────
|
||||||
info "Обновляю Claude skills..."
|
info "Обновляю skills для Claude и Gemini..."
|
||||||
SKILLS_SRC="$SCRIPT_DIR/home-configs/claude/skills"
|
SKILLS_SRC="$SCRIPT_DIR/home-configs/claude/skills"
|
||||||
SKILLS_DST="$HOME/.claude/skills"
|
CLAUDE_SKILLS_DST="$HOME/.claude/skills"
|
||||||
|
GEMINI_SKILLS_DST="$HOME/.gemini/config/plugins/local-setup/skills"
|
||||||
if [ -d "$SKILLS_SRC" ]; then
|
if [ -d "$SKILLS_SRC" ]; then
|
||||||
mkdir -p "$SKILLS_DST"
|
mkdir -p "$CLAUDE_SKILLS_DST" "$GEMINI_SKILLS_DST"
|
||||||
|
|
||||||
|
# Для Gemini нужен plugin.json, чтобы плагин со скилами загрузился
|
||||||
|
GEMINI_PLUGIN_DIR="$HOME/.gemini/config/plugins/local-setup"
|
||||||
|
cat <<EOF > "$GEMINI_PLUGIN_DIR/plugin.json"
|
||||||
|
{
|
||||||
|
"name": "local-setup",
|
||||||
|
"description": "Local custom skills deployed via ai-setup"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
for skill_dir in "$SKILLS_SRC"/*; do
|
for skill_dir in "$SKILLS_SRC"/*; do
|
||||||
[ -d "$skill_dir" ] || continue
|
[ -d "$skill_dir" ] || continue
|
||||||
skill_name=$(basename "$skill_dir")
|
skill_name=$(basename "$skill_dir")
|
||||||
mkdir -p "$SKILLS_DST/$skill_name"
|
|
||||||
cp -r "$skill_dir/"* "$SKILLS_DST/$skill_name/"
|
# Деплой для Claude
|
||||||
|
mkdir -p "$CLAUDE_SKILLS_DST/$skill_name"
|
||||||
|
cp -r "$skill_dir/"* "$CLAUDE_SKILLS_DST/$skill_name/"
|
||||||
|
|
||||||
|
# Деплой для Gemini (agy)
|
||||||
|
mkdir -p "$GEMINI_SKILLS_DST/$skill_name"
|
||||||
|
cp -r "$skill_dir/"* "$GEMINI_SKILLS_DST/$skill_name/"
|
||||||
done
|
done
|
||||||
success "Claude skills обновлены"
|
success "Skills обновлены для Claude и Gemini"
|
||||||
else
|
else
|
||||||
info "Папка со skills не найдена, пропускаю"
|
info "Папка со skills не найдена, пропускаю"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── 6.7. Статусная строка Claude Code ───────────────────────
|
||||||
|
info "Настраиваю статусную строку Claude Code..."
|
||||||
|
STATUSLINE_SRC="$SCRIPT_DIR/home-configs/claude/statusline-command.sh"
|
||||||
|
STATUSLINE_DST="$HOME/.claude/statusline-command.sh"
|
||||||
|
if [ -f "$STATUSLINE_SRC" ]; then
|
||||||
|
cp "$STATUSLINE_SRC" "$STATUSLINE_DST"
|
||||||
|
chmod +x "$STATUSLINE_DST"
|
||||||
|
# Вписываем statusLine в settings.json через python3
|
||||||
|
SETTINGS="$HOME/.claude/settings.json"
|
||||||
|
python3 - "$SETTINGS" "$STATUSLINE_DST" <<'PYEOF'
|
||||||
|
import sys, json, os
|
||||||
|
settings_path, script_path = sys.argv[1], sys.argv[2]
|
||||||
|
data = {}
|
||||||
|
if os.path.exists(settings_path):
|
||||||
|
with open(settings_path) as f:
|
||||||
|
try:
|
||||||
|
data = json.load(f)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
data["statusLine"] = {"type": "command", "command": f"bash {script_path}"}
|
||||||
|
# Дефолтный effort для ai-claude (не передаётся через CLI, берётся отсюда)
|
||||||
|
data.setdefault("effortLevel", "xhigh")
|
||||||
|
# SessionStart хук - триггерит вызов statusLine при старте сессии
|
||||||
|
if "hooks" not in data:
|
||||||
|
data["hooks"] = {}
|
||||||
|
if "SessionStart" not in data["hooks"]:
|
||||||
|
data["hooks"]["SessionStart"] = [{"hooks": [{"type": "command", "command": "true"}]}]
|
||||||
|
with open(settings_path, "w") as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
f.write("\n")
|
||||||
|
PYEOF
|
||||||
|
success "Статусная строка настроена"
|
||||||
|
else
|
||||||
|
warn "Файл $STATUSLINE_SRC не найден, пропускаю"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 6.7.0. Чистка устаревшего хука effort-save ──────────────
|
||||||
|
# effort/model теперь персистятся нативно в settings.json каждого CLAUDE_CONFIG_DIR
|
||||||
|
# (полная изоляция лаунчеров), самопальный кэш ~/.cache/ai-setup/{effort,model}_* не нужен.
|
||||||
|
# Удаляем старый хук с диска и из ~/.claude/settings.json (нотифаер в Stop сохраняем).
|
||||||
|
info "Удаляю устаревший хук effort-save..."
|
||||||
|
rm -f "$HOME/.claude/hooks/effort-save-hook.sh"
|
||||||
|
# Осиротевший кэш моделей (источник старой протечки между ai-*); effort_* НЕ трогаем -
|
||||||
|
# он снова используется для персиста effort (включая max) через CLAUDE_CODE_EFFORT_LEVEL.
|
||||||
|
rm -f "$HOME"/.cache/ai-setup/model_*
|
||||||
|
python3 - "$HOME/.claude/settings.json" <<'PYEOF'
|
||||||
|
import sys, json, os
|
||||||
|
settings_path = sys.argv[1]
|
||||||
|
if not os.path.exists(settings_path):
|
||||||
|
sys.exit(0)
|
||||||
|
try:
|
||||||
|
with open(settings_path) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
sys.exit(0)
|
||||||
|
stop = data.get("hooks", {}).get("Stop")
|
||||||
|
if isinstance(stop, list):
|
||||||
|
for entry in stop:
|
||||||
|
hooks = entry.get("hooks")
|
||||||
|
if isinstance(hooks, list):
|
||||||
|
entry["hooks"] = [h for h in hooks if "effort-save-hook" not in h.get("command", "")]
|
||||||
|
data["hooks"]["Stop"] = [e for e in stop if e.get("hooks")]
|
||||||
|
if not data["hooks"]["Stop"]:
|
||||||
|
del data["hooks"]["Stop"]
|
||||||
|
with open(settings_path, "w") as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
f.write("\n")
|
||||||
|
PYEOF
|
||||||
|
success "Старый хук effort-save удалён"
|
||||||
|
|
||||||
|
# ── 6.7.05. Хелпер account-email (определение email по токену) ──
|
||||||
|
# Вспомогательный скрипт для хуков switch-account/add-account.
|
||||||
|
# Не регистрируется в settings.json — вызывается из хуков напрямую.
|
||||||
|
EMAIL_HELPER_SRC="$SCRIPT_DIR/home-configs/claude/hooks/account-email.sh"
|
||||||
|
EMAIL_HELPER_DST="$HOME/.claude/hooks/account-email.sh"
|
||||||
|
mkdir -p "$HOME/.claude/hooks"
|
||||||
|
if [ -f "$EMAIL_HELPER_SRC" ]; then
|
||||||
|
cp "$EMAIL_HELPER_SRC" "$EMAIL_HELPER_DST"
|
||||||
|
chmod +x "$EMAIL_HELPER_DST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 6.7.1. Хук switch-account ───────────────────────────────────
|
||||||
|
info "Деплою хук switch-account..."
|
||||||
|
SWITCH_HOOK_SRC="$SCRIPT_DIR/home-configs/claude/hooks/switch-account-hook.sh"
|
||||||
|
SWITCH_HOOK_DST="$HOME/.claude/hooks/switch-account-hook.sh"
|
||||||
|
mkdir -p "$HOME/.claude/hooks"
|
||||||
|
if [ -f "$SWITCH_HOOK_SRC" ]; then
|
||||||
|
cp "$SWITCH_HOOK_SRC" "$SWITCH_HOOK_DST"
|
||||||
|
chmod +x "$SWITCH_HOOK_DST"
|
||||||
|
# Прописываем хук в settings.json (идемпотентно)
|
||||||
|
python3 - "$HOME/.claude/settings.json" "$SWITCH_HOOK_DST" <<'PYEOF'
|
||||||
|
import sys, json, os
|
||||||
|
settings_path, hook_path = sys.argv[1], sys.argv[2]
|
||||||
|
data = {}
|
||||||
|
if os.path.exists(settings_path):
|
||||||
|
with open(settings_path) as f:
|
||||||
|
try: data = json.load(f)
|
||||||
|
except json.JSONDecodeError: pass
|
||||||
|
data.setdefault("hooks", {}).setdefault("UserPromptSubmit", [{"hooks": []}])
|
||||||
|
hook_cmd = f'bash "{hook_path}"'
|
||||||
|
ups = data["hooks"]["UserPromptSubmit"]
|
||||||
|
already = any(
|
||||||
|
any(h.get("command", "") == hook_cmd for h in entry.get("hooks", []))
|
||||||
|
for entry in ups
|
||||||
|
)
|
||||||
|
if not already:
|
||||||
|
ups[0]["hooks"].append({"type": "command", "command": hook_cmd})
|
||||||
|
with open(settings_path, "w") as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
f.write("\n")
|
||||||
|
PYEOF
|
||||||
|
success "Хук switch-account установлен"
|
||||||
|
else
|
||||||
|
warn "Файл $SWITCH_HOOK_SRC не найден, пропускаю"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 6.7.2. Хук add-account ──────────────────────────────────────
|
||||||
|
info "Деплою хук add-account..."
|
||||||
|
ADD_HOOK_SRC="$SCRIPT_DIR/home-configs/claude/hooks/add-account-hook.sh"
|
||||||
|
ADD_HOOK_DST="$HOME/.claude/hooks/add-account-hook.sh"
|
||||||
|
mkdir -p "$HOME/.claude/hooks"
|
||||||
|
if [ -f "$ADD_HOOK_SRC" ]; then
|
||||||
|
cp "$ADD_HOOK_SRC" "$ADD_HOOK_DST"
|
||||||
|
chmod +x "$ADD_HOOK_DST"
|
||||||
|
# Прописываем хук в settings.json (идемпотентно)
|
||||||
|
python3 - "$HOME/.claude/settings.json" "$ADD_HOOK_DST" <<'PYEOF'
|
||||||
|
import sys, json, os
|
||||||
|
settings_path, hook_path = sys.argv[1], sys.argv[2]
|
||||||
|
data = {}
|
||||||
|
if os.path.exists(settings_path):
|
||||||
|
with open(settings_path) as f:
|
||||||
|
try: data = json.load(f)
|
||||||
|
except json.JSONDecodeError: pass
|
||||||
|
data.setdefault("hooks", {}).setdefault("UserPromptSubmit", [{"hooks": []}])
|
||||||
|
hook_cmd = f'bash "{hook_path}"'
|
||||||
|
ups = data["hooks"]["UserPromptSubmit"]
|
||||||
|
already = any(
|
||||||
|
any(h.get("command", "") == hook_cmd for h in entry.get("hooks", []))
|
||||||
|
for entry in ups
|
||||||
|
)
|
||||||
|
if not already:
|
||||||
|
ups[0]["hooks"].append({"type": "command", "command": hook_cmd})
|
||||||
|
with open(settings_path, "w") as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
f.write("\n")
|
||||||
|
PYEOF
|
||||||
|
success "Хук add-account установлен"
|
||||||
|
else
|
||||||
|
warn "Файл $ADD_HOOK_SRC не найден, пропускаю"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 6.8. Регистрация официального маркетплейса плагинов Claude ──
|
||||||
|
info "Настраиваю маркетплейс плагинов Claude Code..."
|
||||||
|
if ! command -v claude &>/dev/null; then
|
||||||
|
warn "claude не найден, пропускаю настройку маркетплейса"
|
||||||
|
else
|
||||||
|
existing=$(claude plugin marketplace list 2>/dev/null | grep "claude-plugins-official" || true)
|
||||||
|
if [ -n "$existing" ]; then
|
||||||
|
success "Маркетплейс claude-plugins-official уже добавлен"
|
||||||
|
else
|
||||||
|
# Берём токен из env или спрашиваем
|
||||||
|
if [ -z "$GITHUB_TOKEN" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Для установки плагинов Claude нужен GitHub Personal Access Token."
|
||||||
|
echo "Создать можно на: https://github.com/settings/tokens (без scope, только public repos)"
|
||||||
|
read -rp "GitHub PAT (или Enter чтобы пропустить): " GITHUB_TOKEN
|
||||||
|
fi
|
||||||
|
if [ -z "$GITHUB_TOKEN" ]; then
|
||||||
|
warn "Токен не указан, маркетплейс плагинов не настроен"
|
||||||
|
warn "Позже запустите: claude plugin marketplace add https://TOKEN@github.com/anthropics/claude-plugins-official.git"
|
||||||
|
else
|
||||||
|
if claude plugin marketplace add "https://${GITHUB_TOKEN}@github.com/anthropics/claude-plugins-official.git" 2>&1; then
|
||||||
|
success "Маркетплейс claude-plugins-official добавлен"
|
||||||
|
else
|
||||||
|
warn "Не удалось добавить маркетплейс, проверьте токен"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 6.9. Установка Claude Notifier ──────────────────────────
|
||||||
|
info "Устанавливаю Claude Notifier..."
|
||||||
|
if [ -f "$HOME/.claude/hooks/claude-notifier-on-stop.js" ]; then
|
||||||
|
success "Claude Notifier уже установлен"
|
||||||
|
else
|
||||||
|
if curl -fsSL https://raw.githubusercontent.com/ashmitb95/claude-notifier/main/install.sh | bash; then
|
||||||
|
success "Claude Notifier установлен"
|
||||||
|
else
|
||||||
|
warn "Не удалось установить Claude Notifier"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# ── 7. Очистка старых функций из .bashrc / .zshrc ───────────
|
# ── 7. Очистка старых функций из .bashrc / .zshrc ───────────
|
||||||
clean_rc() {
|
clean_rc() {
|
||||||
local rc_file="$1"
|
local rc_file="$1"
|
||||||
@@ -648,13 +866,14 @@ clean_rc "$HOME/.zshrc"
|
|||||||
|
|
||||||
add_path_to_rc() {
|
add_path_to_rc() {
|
||||||
local rc_file="$1"
|
local rc_file="$1"
|
||||||
|
local bin_rel="${BIN_DIR#$HOME/}"
|
||||||
if [ -f "$rc_file" ]; then
|
if [ -f "$rc_file" ]; then
|
||||||
if ! grep -q 'NPM_GLOBAL' "$rc_file" 2>/dev/null; then
|
if ! grep -q 'NPM_GLOBAL' "$rc_file" 2>/dev/null; then
|
||||||
cat >> "$rc_file" << 'PATHEOF'
|
cat >> "$rc_file" << PATHEOF
|
||||||
|
|
||||||
# Claude Code Launcher PATH
|
# Claude Code Launcher PATH
|
||||||
export NPM_GLOBAL="$HOME/.npm-global"
|
export NPM_GLOBAL="\$HOME/.npm-global"
|
||||||
export PATH="$NPM_GLOBAL/bin:$HOME/.local/bin:$PATH"
|
export PATH="\$NPM_GLOBAL/bin:\$HOME/${bin_rel}:\$PATH"
|
||||||
PATHEOF
|
PATHEOF
|
||||||
success "PATH добавлен в $rc_file"
|
success "PATH добавлен в $rc_file"
|
||||||
fi
|
fi
|
||||||
@@ -671,6 +890,8 @@ HELPERS_FILE="$BIN_DIR/ai-api-helpers.sh"
|
|||||||
cat > "$HELPERS_FILE" << 'HELPEREOF'
|
cat > "$HELPERS_FILE" << 'HELPEREOF'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
export TZ="Europe/Helsinki"
|
||||||
|
|
||||||
# _claude_test_api: Send 1-token test to an Anthropic-compatible endpoint
|
# _claude_test_api: Send 1-token test to an Anthropic-compatible endpoint
|
||||||
_claude_test_api() {
|
_claude_test_api() {
|
||||||
local url="$1" auth_header="$2" model="${3:-claude-sonnet-4-6}"
|
local url="$1" auth_header="$2" model="${3:-claude-sonnet-4-6}"
|
||||||
@@ -698,6 +919,86 @@ _claude_test_openai_api() {
|
|||||||
_CLAUDE_TEST_BODY=$(echo "$response" | sed '$d')
|
_CLAUDE_TEST_BODY=$(echo "$response" | sed '$d')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# _deepseek_balance: Query DeepSeek balance API and print info
|
||||||
|
_deepseek_balance() {
|
||||||
|
local api_key="$1"
|
||||||
|
local response
|
||||||
|
response=$(curl -s --max-time 10 "https://api.deepseek.com/user/balance" \
|
||||||
|
-H "Authorization: Bearer $api_key" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
2>/dev/null || echo "")
|
||||||
|
if [ -z "$response" ]; then
|
||||||
|
echo -e " \033[0;33m[БАЛАНС]\033[0m Не удалось получить баланс (сеть?)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "$response" | python3 -c "
|
||||||
|
import sys, json, os
|
||||||
|
try:
|
||||||
|
d = json.load(sys.stdin)
|
||||||
|
available = d.get('is_available', False)
|
||||||
|
infos = d.get('balance_infos', [])
|
||||||
|
if not infos:
|
||||||
|
print(' \033[0;33m[БАЛАНС]\033[0m Нет данных о балансе')
|
||||||
|
sys.exit(0)
|
||||||
|
symbols = {'USD': '\$', 'CNY': '¥'}
|
||||||
|
cache_parts = []
|
||||||
|
for info in infos:
|
||||||
|
curr = info.get('currency', '???')
|
||||||
|
total = info.get('total_balance', '0')
|
||||||
|
granted = info.get('granted_balance', '0')
|
||||||
|
topped_up = info.get('topped_up_balance', '0')
|
||||||
|
sym = symbols.get(curr, curr)
|
||||||
|
status = '✅ доступен' if available else '❌ не активен'
|
||||||
|
print(f' \033[1;36m💰 Баланс DeepSeek:\033[0m {total} {curr} {status}')
|
||||||
|
if float(granted) > 0:
|
||||||
|
print(f' └─ Начислено: {granted} {curr}')
|
||||||
|
if float(topped_up) > 0:
|
||||||
|
print(f' └─ Пополнено: {topped_up} {curr}')
|
||||||
|
cache_parts.append(f'{sym}{total}')
|
||||||
|
# Cache all currencies with symbols for statusline
|
||||||
|
if cache_parts:
|
||||||
|
cache_dir = os.path.expanduser('~/.cache/ai-setup')
|
||||||
|
os.makedirs(cache_dir, exist_ok=True)
|
||||||
|
with open(os.path.join(cache_dir, 'deepseek_balance'), 'w') as f:
|
||||||
|
f.write(' '.join(cache_parts) + '\n')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' ⚠️ Не удалось разобрать баланс: {e}')
|
||||||
|
" 2>/dev/null || echo -e " \033[0;33m[БАЛАНС]\033[0m Ошибка парсинга ответа"
|
||||||
|
}
|
||||||
|
|
||||||
|
# _openrouter_balance: Query OpenRouter credits API and print balance
|
||||||
|
# Uses /api/v1/credits (works with regular API key too)
|
||||||
|
_openrouter_balance() {
|
||||||
|
local api_key="$1"
|
||||||
|
local response
|
||||||
|
response=$(curl -s --max-time 10 "https://openrouter.ai/api/v1/credits" \
|
||||||
|
-H "Authorization: Bearer $api_key" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
2>/dev/null || echo "")
|
||||||
|
if [ -z "$response" ]; then
|
||||||
|
echo -e " \033[0;33m[БАЛАНС]\033[0m Не удалось получить баланс (сеть?)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "$response" | python3 -c "
|
||||||
|
import sys, json, os
|
||||||
|
try:
|
||||||
|
d = json.load(sys.stdin)
|
||||||
|
data = d.get('data', {})
|
||||||
|
total = data.get('total_credits', 0) or 0
|
||||||
|
usage = data.get('total_usage', 0) or 0
|
||||||
|
remaining = total - usage
|
||||||
|
cache_dir = os.path.expanduser('~/.cache/ai-setup')
|
||||||
|
os.makedirs(cache_dir, exist_ok=True)
|
||||||
|
cache_file = os.path.join(cache_dir, 'openrouter_balance')
|
||||||
|
# Зелёный как у DeepSeek: \033[38;5;78m
|
||||||
|
print(f' \033[1;36m💰 Баланс OpenRouter:\033[0m \033[38;5;78m\${remaining:.2f}\033[0m (загружено \${total:.2f}, потрачено \${usage:.2f})')
|
||||||
|
with open(cache_file, 'w') as f:
|
||||||
|
f.write(f'\${remaining:.2f}\n')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' ⚠️ Не удалось разобрать баланс: {e}')
|
||||||
|
" 2>/dev/null || echo -e " \033[0;33m[БАЛАНС]\033[0m Ошибка парсинга ответа"
|
||||||
|
}
|
||||||
|
|
||||||
_handle_openai_api_response() {
|
_handle_openai_api_response() {
|
||||||
local provider="$1"
|
local provider="$1"
|
||||||
local code="$2"
|
local code="$2"
|
||||||
@@ -735,6 +1036,11 @@ _handle_openai_api_response() {
|
|||||||
echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить ключ (нет сети?). Продолжаю..."
|
echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить ключ (нет сети?). Продолжаю..."
|
||||||
_API_RET=0
|
_API_RET=0
|
||||||
;;
|
;;
|
||||||
|
5[0-9][0-9])
|
||||||
|
echo ""
|
||||||
|
echo -e "\033[0;33m[СЕРВЕР]\033[0m $provider временно недоступен (HTTP $code). Продолжаю..."
|
||||||
|
_API_RET=0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
_emsg=$(_claude_extract_error "$body")
|
_emsg=$(_claude_extract_error "$body")
|
||||||
echo ""
|
echo ""
|
||||||
@@ -813,6 +1119,11 @@ _handle_api_response() {
|
|||||||
echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить ключ (нет сети?). Продолжаю..."
|
echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить ключ (нет сети?). Продолжаю..."
|
||||||
_API_RET=0
|
_API_RET=0
|
||||||
;;
|
;;
|
||||||
|
5[0-9][0-9])
|
||||||
|
echo ""
|
||||||
|
echo -e "\033[0;33m[СЕРВЕР]\033[0m $provider временно недоступен (HTTP $code). Продолжаю..."
|
||||||
|
_API_RET=0
|
||||||
|
;;
|
||||||
400)
|
400)
|
||||||
_emsg=$(_claude_extract_error "$body")
|
_emsg=$(_claude_extract_error "$body")
|
||||||
if echo "${_emsg:-$body}" | grep -qi "RESOURCE_EXHAUSTED"; then
|
if echo "${_emsg:-$body}" | grep -qi "RESOURCE_EXHAUSTED"; then
|
||||||
@@ -845,6 +1156,97 @@ _open_browser() {
|
|||||||
else echo "Откройте вручную: $url"; fi
|
else echo "Откройте вручную: $url"; fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# _setup_isolated_config: готовит изолированную папку CLAUDE_CONFIG_DIR для лаунчера.
|
||||||
|
# Каждый сторонний провайдер получает собственные settings.json и .claude.json,
|
||||||
|
# поэтому выбор модели и кэш кастом-моделей НЕ протекают между ai-* лаунчерами.
|
||||||
|
# Общие ресурсы (skills, CLAUDE.md) шарятся симлинком из ~/.claude.
|
||||||
|
# model и effortLevel сидируются как дефолты - выбор юзера через /model и /effort
|
||||||
|
# (для low/medium/high/xhigh) сохраняется нативно в этом же settings.json.
|
||||||
|
# Уровень max обрабатывается отдельно в _apply_effort (settings.json его не хранит).
|
||||||
|
# Использование: _setup_isolated_config <launcher> <default_model> <default_effort> <available_models_json>
|
||||||
|
_setup_isolated_config() {
|
||||||
|
local launcher="$1" default_model="$2" default_effort="${3:-high}" avail="${4:-}"
|
||||||
|
local cfg="$HOME/.config/ai-setup/cfg/$launcher"
|
||||||
|
mkdir -p "$cfg"
|
||||||
|
# Общие ресурсы из ~/.claude (единый источник правды)
|
||||||
|
ln -sfn "$HOME/.claude/skills" "$cfg/skills"
|
||||||
|
ln -sfn "$HOME/.claude/CLAUDE.md" "$cfg/CLAUDE.md"
|
||||||
|
[ -e "$HOME/.claude/agents" ] && ln -sfn "$HOME/.claude/agents" "$cfg/agents"
|
||||||
|
# .claude.json в свежей папке: пропускаем онбординг
|
||||||
|
[ -f "$cfg/.claude.json" ] || echo '{"hasCompletedOnboarding": true}' > "$cfg/.claude.json"
|
||||||
|
python3 - "$cfg/settings.json" "$HOME/.claude/statusline-command.sh" \
|
||||||
|
"$default_model" "$default_effort" "$avail" <<'PYEOF'
|
||||||
|
import sys, json, os
|
||||||
|
cfg_settings, statusline, model, effort, avail = sys.argv[1:6]
|
||||||
|
data = {}
|
||||||
|
if os.path.exists(cfg_settings):
|
||||||
|
try:
|
||||||
|
with open(cfg_settings) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# Структурные настройки лаунчера (переустанавливаем всегда)
|
||||||
|
data["statusLine"] = {"type": "command", "command": f"bash {statusline}"}
|
||||||
|
data["skipDangerousModePermissionPrompt"] = True
|
||||||
|
data.setdefault("hooks", {})["SessionStart"] = [{"hooks": [{"type": "command", "command": "true"}]}]
|
||||||
|
# availableModels - белый список пикера (политика лаунчера)
|
||||||
|
if avail:
|
||||||
|
data["availableModels"] = json.loads(avail)
|
||||||
|
else:
|
||||||
|
data.pop("availableModels", None)
|
||||||
|
# model/effortLevel - сидируем дефолты, не перетирая выбор юзера (нативный persistence)
|
||||||
|
if model:
|
||||||
|
data.setdefault("model", model)
|
||||||
|
data.setdefault("effortLevel", effort)
|
||||||
|
with open(cfg_settings, "w") as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
f.write("\n")
|
||||||
|
PYEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# _apply_effort: per-launcher persistence уровня effort (гибрид).
|
||||||
|
# - low/medium/high/xhigh живут нативно в settings.json лаунчера -> /effort работает,
|
||||||
|
# уровень сохраняется между сессиями, env-переменная НЕ ставится.
|
||||||
|
# - max единственный нельзя сохранить в settings.json (он session-only), поэтому
|
||||||
|
# его восстанавливаем через CLAUDE_CODE_EFFORT_LEVEL. В такой max-сессии /effort
|
||||||
|
# залочен env-переменной (ограничение Claude Code).
|
||||||
|
# Текущий уровень (вкл. max) ловит статусбар в ~/.cache/ai-setup/effort_<launcher>.
|
||||||
|
# Сменить уровень из max: AI_EFFORT=<lvl> ai-<launcher>.
|
||||||
|
# Использование: _apply_effort <launcher> <default_effort>
|
||||||
|
_apply_effort() {
|
||||||
|
local launcher="$1" default_effort="${2:-high}"
|
||||||
|
local f="$HOME/.cache/ai-setup/effort_${launcher}"
|
||||||
|
local settings="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json"
|
||||||
|
local eff
|
||||||
|
if [ -n "${AI_EFFORT:-}" ]; then
|
||||||
|
# Явный override: запоминаем и применяем
|
||||||
|
eff="$AI_EFFORT"
|
||||||
|
mkdir -p "$HOME/.cache/ai-setup"
|
||||||
|
echo "$eff" > "$f"
|
||||||
|
else
|
||||||
|
eff=$(cat "$f" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
if [ "$eff" = "max" ]; then
|
||||||
|
# Единственный способ восстановить max между сессиями
|
||||||
|
export CLAUDE_CODE_EFFORT_LEVEL=max
|
||||||
|
elif [ -n "${AI_EFFORT:-}" ] && [ -n "$eff" ]; then
|
||||||
|
# Явный сброс на low/medium/high/xhigh - пишем нативно в settings.json лаунчера
|
||||||
|
python3 - "$settings" "$eff" <<'PYEOF'
|
||||||
|
import sys, json, os
|
||||||
|
p, eff = sys.argv[1], sys.argv[2]
|
||||||
|
d = {}
|
||||||
|
if os.path.exists(p):
|
||||||
|
try: d = json.load(open(p))
|
||||||
|
except Exception: pass
|
||||||
|
d["effortLevel"] = eff
|
||||||
|
os.makedirs(os.path.dirname(p), exist_ok=True)
|
||||||
|
with open(p, "w") as fp:
|
||||||
|
json.dump(d, fp, indent=2, ensure_ascii=False); fp.write("\n")
|
||||||
|
PYEOF
|
||||||
|
fi
|
||||||
|
# Иначе (≤xhigh без AI_EFFORT): ничего не делаем - effortLevel уже персистнут нативно.
|
||||||
|
}
|
||||||
|
|
||||||
_build_ai_sys_prompt() {
|
_build_ai_sys_prompt() {
|
||||||
local global_rules="$HOME/.config/ai-setup/global_rules.md"
|
local global_rules="$HOME/.config/ai-setup/global_rules.md"
|
||||||
local global_rendered=""
|
local global_rendered=""
|
||||||
@@ -874,7 +1276,7 @@ chmod +x "$HELPERS_FILE"
|
|||||||
cat > "$BIN_DIR/ai-gpt" << 'GPTEOF'
|
cat > "$BIN_DIR/ai-gpt" << 'GPTEOF'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ai-gpt - запуск нативного OpenAI Codex
|
# ai-gpt - запуск нативного OpenAI Codex
|
||||||
source "$HOME/.local/bin/ai-api-helpers.sh" 2>/dev/null || true
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||||||
|
|
||||||
codex_bin="$HOME/.npm-global/bin/codex"
|
codex_bin="$HOME/.npm-global/bin/codex"
|
||||||
[ ! -f "$codex_bin" ] && codex_bin="$(command -v codex 2>/dev/null)"
|
[ ! -f "$codex_bin" ] && codex_bin="$(command -v codex 2>/dev/null)"
|
||||||
@@ -907,7 +1309,7 @@ chmod +x "$BIN_DIR/ai-gpt"
|
|||||||
# === ai-deepseek ===
|
# === ai-deepseek ===
|
||||||
cat > "$BIN_DIR/ai-deepseek" << 'DEEPSEEKEOF'
|
cat > "$BIN_DIR/ai-deepseek" << 'DEEPSEEKEOF'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
source "$HOME/.local/bin/ai-api-helpers.sh" 2>/dev/null || true
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||||||
|
|
||||||
key_file="$HOME/.config/ai-setup/deepseek_key"
|
key_file="$HOME/.config/ai-setup/deepseek_key"
|
||||||
api_key=""
|
api_key=""
|
||||||
@@ -930,6 +1332,7 @@ if [ -n "$api_key" ]; then
|
|||||||
elif [ $ret -ne 0 ]; then
|
elif [ $ret -ne 0 ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
_deepseek_balance "$api_key"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$api_key" ] && [ "$reauth" -eq 1 ]; then
|
if [ -z "$api_key" ] && [ "$reauth" -eq 1 ]; then
|
||||||
@@ -951,6 +1354,7 @@ if [ -z "$api_key" ]; then
|
|||||||
echo "$api_key" > "$key_file"
|
echo "$api_key" > "$key_file"
|
||||||
chmod 600 "$key_file"
|
chmod 600 "$key_file"
|
||||||
echo "Ключ сохранён."
|
echo "Ключ сохранён."
|
||||||
|
_deepseek_balance "$api_key"
|
||||||
if [ $ret -eq 429 ]; then
|
if [ $ret -eq 429 ]; then
|
||||||
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
|
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
|
||||||
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
|
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
|
||||||
@@ -961,16 +1365,33 @@ if [ -z "$api_key" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SYS_PROMPT=$(_build_ai_sys_prompt)
|
_PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
|
||||||
|
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
|
||||||
|
_build_ai_sys_prompt > "$_PROMPT_FILE"
|
||||||
|
export AI_LAUNCHER=deepseek
|
||||||
|
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/deepseek"
|
||||||
|
# Пикер: DeepSeek V4 Pro (opus+sonnet, дефолт) и DeepSeek V4 Flash (haiku).
|
||||||
|
# availableModels НЕ задаём: при кастомном провайдере он схлопывает пикер в Default.
|
||||||
|
# Claude Code навязывает 3 слота opus/sonnet/haiku; незаданный слот показал бы чужой
|
||||||
|
# Claude, поэтому sonnet тоже мапим на Pro (лёгкий дубль, но все пункты - DeepSeek).
|
||||||
|
# FABLE не навязывается - не задаём. DISABLE_1M убирает [1M] дубли из пикера.
|
||||||
|
_setup_isolated_config deepseek opus high ''
|
||||||
|
_apply_effort deepseek high
|
||||||
ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic \
|
ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic \
|
||||||
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
||||||
ANTHROPIC_MODEL=deepseek-v4-pro \
|
|
||||||
ANTHROPIC_DEFAULT_OPUS_MODEL=deepseek-v4-pro \
|
ANTHROPIC_DEFAULT_OPUS_MODEL=deepseek-v4-pro \
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="DeepSeek V4 Pro" \
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="DeepSeek V4 Pro - флагман для сложных задач" \
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL=deepseek-v4-pro \
|
ANTHROPIC_DEFAULT_SONNET_MODEL=deepseek-v4-pro \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="DeepSeek V4 Pro" \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="DeepSeek V4 Pro - флагман для сложных задач" \
|
||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-v4-flash \
|
ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-v4-flash \
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="DeepSeek V4 Flash" \
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="DeepSeek V4 Flash - быстрый и дешёвый" \
|
||||||
CLAUDE_CODE_SUBAGENT_MODEL=deepseek-v4-flash \
|
CLAUDE_CODE_SUBAGENT_MODEL=deepseek-v4-flash \
|
||||||
|
CLAUDE_CODE_DISABLE_1M_CONTEXT=1 \
|
||||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||||||
claude --dangerously-skip-permissions --system-prompt "$SYS_PROMPT" "$@"
|
claude --dangerously-skip-permissions --system-prompt-file "$_PROMPT_FILE" "$@"
|
||||||
DEEPSEEKEOF
|
DEEPSEEKEOF
|
||||||
chmod +x "$BIN_DIR/ai-deepseek"
|
chmod +x "$BIN_DIR/ai-deepseek"
|
||||||
|
|
||||||
@@ -978,7 +1399,7 @@ chmod +x "$BIN_DIR/ai-deepseek"
|
|||||||
cat > "$BIN_DIR/ai-kimi" << 'KIMIEOF'
|
cat > "$BIN_DIR/ai-kimi" << 'KIMIEOF'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ai-kimi - запуск Claude Code через официальный Kimi Code API
|
# ai-kimi - запуск Claude Code через официальный Kimi Code API
|
||||||
source "$HOME/.local/bin/ai-api-helpers.sh" 2>/dev/null || true
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||||||
|
|
||||||
key_file="$HOME/.config/ai-setup/kimi_key"
|
key_file="$HOME/.config/ai-setup/kimi_key"
|
||||||
api_key=""
|
api_key=""
|
||||||
@@ -987,7 +1408,7 @@ api_key=""
|
|||||||
|
|
||||||
if [ -n "$api_key" ]; then
|
if [ -n "$api_key" ]; then
|
||||||
echo -n "Проверка сохранённого Kimi ключа... "
|
echo -n "Проверка сохранённого Kimi ключа... "
|
||||||
_claude_test_api "https://api.kimi.com/coding/v1/messages" "x-api-key: $api_key" "kimi-k2.6"
|
_claude_test_api "https://api.kimi.com/coding/v1/messages" "x-api-key: $api_key" "kimi-k2.7"
|
||||||
_handle_api_response "Kimi" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://www.kimi.com/code"
|
_handle_api_response "Kimi" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://www.kimi.com/code"
|
||||||
ret=$_API_RET
|
ret=$_API_RET
|
||||||
if [ $ret -eq 401 ]; then
|
if [ $ret -eq 401 ]; then
|
||||||
@@ -1007,7 +1428,7 @@ if [ -z "$api_key" ]; then
|
|||||||
[ -z "$api_key" ] && { echo "Выход."; exit 1; }
|
[ -z "$api_key" ] && { echo "Выход."; exit 1; }
|
||||||
|
|
||||||
echo -n "Проверяю ключ и баланс... "
|
echo -n "Проверяю ключ и баланс... "
|
||||||
_claude_test_api "https://api.kimi.com/coding/v1/messages" "x-api-key: $api_key" "kimi-k2.6"
|
_claude_test_api "https://api.kimi.com/coding/v1/messages" "x-api-key: $api_key" "kimi-k2.7"
|
||||||
_handle_api_response "Kimi" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://www.kimi.com/code"
|
_handle_api_response "Kimi" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://www.kimi.com/code"
|
||||||
ret=$_API_RET
|
ret=$_API_RET
|
||||||
if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then
|
if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then
|
||||||
@@ -1031,19 +1452,130 @@ if ! command -v claude &>/dev/null; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SYS_PROMPT=$(_build_ai_sys_prompt)
|
_PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
|
||||||
|
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
|
||||||
|
_build_ai_sys_prompt > "$_PROMPT_FILE"
|
||||||
|
export AI_LAUNCHER=kimi
|
||||||
|
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/kimi"
|
||||||
|
# Пикер: Opus/Sonnet = Kimi K2.7 Code, Haiku = Kimi K2.6. availableModels НЕ задаём
|
||||||
|
# (он схлопывает пикер в Default). Claude Code навязывает 3 слота opus/sonnet/haiku;
|
||||||
|
# незаданный показал бы чужой Claude, поэтому opus+sonnet = K2.7, haiku = K2.6
|
||||||
|
# DISABLE_1M убирает [1M] дубли из пикера.
|
||||||
|
_setup_isolated_config kimi opus high ''
|
||||||
|
_apply_effort kimi high
|
||||||
ANTHROPIC_BASE_URL=https://api.kimi.com/coding \
|
ANTHROPIC_BASE_URL=https://api.kimi.com/coding \
|
||||||
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
||||||
ANTHROPIC_MODEL=kimi-k2.6 \
|
ANTHROPIC_DEFAULT_OPUS_MODEL=kimi-k2.7 \
|
||||||
ANTHROPIC_DEFAULT_OPUS_MODEL=kimi-k2.6 \
|
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="Kimi K2.7 Code" \
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL=kimi-k2.6 \
|
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="Kimi K2.7 Code — флагманский программист (Moonshot AI)" \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL=kimi-k2.7 \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="Kimi K2.7 Code" \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="Kimi K2.7 Code — флагманский программист (Moonshot AI)" \
|
||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL=kimi-k2.6 \
|
ANTHROPIC_DEFAULT_HAIKU_MODEL=kimi-k2.6 \
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="Kimi K2.6" \
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="Kimi K2.6 — быстрый универсал (Moonshot AI)" \
|
||||||
CLAUDE_CODE_SUBAGENT_MODEL=kimi-k2.6 \
|
CLAUDE_CODE_SUBAGENT_MODEL=kimi-k2.6 \
|
||||||
|
CLAUDE_CODE_DISABLE_1M_CONTEXT=1 \
|
||||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||||||
claude --dangerously-skip-permissions --system-prompt "$SYS_PROMPT" "$@"
|
claude --dangerously-skip-permissions --system-prompt-file "$_PROMPT_FILE" "$@"
|
||||||
KIMIEOF
|
KIMIEOF
|
||||||
chmod +x "$BIN_DIR/ai-kimi"
|
chmod +x "$BIN_DIR/ai-kimi"
|
||||||
|
|
||||||
|
# === ai-openrouter ===
|
||||||
|
cat > "$BIN_DIR/ai-openrouter" << 'OPENROUTEREOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ai-openrouter - запуск Claude Code через OpenRouter (любые модели)
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||||||
|
|
||||||
|
key_file="$HOME/.config/ai-setup/openrouter_key"
|
||||||
|
api_key=""
|
||||||
|
|
||||||
|
[ -f "$key_file" ] && api_key=$(cat "$key_file")
|
||||||
|
|
||||||
|
if [ -n "$api_key" ]; then
|
||||||
|
echo -n "Проверка сохранённого OpenRouter ключа... "
|
||||||
|
_claude_test_openai_api "https://openrouter.ai/api/v1/chat/completions" "$api_key" "openai/gpt-4o-mini"
|
||||||
|
_handle_openai_api_response "OpenRouter" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://openrouter.ai/settings/credits"
|
||||||
|
ret=$_API_RET
|
||||||
|
if [ $ret -eq 401 ]; then
|
||||||
|
rm -f "$key_file"
|
||||||
|
api_key=""
|
||||||
|
elif [ $ret -eq 429 ]; then
|
||||||
|
_openrouter_balance "$api_key"
|
||||||
|
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
|
||||||
|
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
|
||||||
|
elif [ $ret -ne 0 ]; then
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
_openrouter_balance "$api_key"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$api_key" ]; then
|
||||||
|
echo "Получить ключ: https://openrouter.ai/settings/keys"
|
||||||
|
read -r -p "Введите ваш OpenRouter API ключ: " api_key
|
||||||
|
[ -z "$api_key" ] && { echo "Выход."; exit 1; }
|
||||||
|
|
||||||
|
echo -n "Проверяю ключ и баланс... "
|
||||||
|
_claude_test_openai_api "https://openrouter.ai/api/v1/chat/completions" "$api_key" "openai/gpt-4o-mini"
|
||||||
|
_handle_openai_api_response "OpenRouter" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://openrouter.ai/settings/credits"
|
||||||
|
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 "Ключ сохранён."
|
||||||
|
_openrouter_balance "$api_key"
|
||||||
|
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
|
||||||
|
|
||||||
|
_PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
|
||||||
|
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
|
||||||
|
_build_ai_sys_prompt > "$_PROMPT_FILE"
|
||||||
|
export AI_LAUNCHER=openrouter
|
||||||
|
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/openrouter"
|
||||||
|
# openrouter - модели, которых НЕТ в других ai-* лаунчерах (без anthropic/deepseek/
|
||||||
|
# kimi/gemini). Пикер строится из 4 алиас-слотов + 1 custom-пункта (потолок Claude Code).
|
||||||
|
# availableModels НЕ задаём (он схлопывает пикер). Дефолт - GPT-5.5 (custom-пункт).
|
||||||
|
export ANTHROPIC_CUSTOM_MODEL_OPTION="openai/gpt-5.5"
|
||||||
|
export ANTHROPIC_CUSTOM_MODEL_OPTION_NAME="GPT-5.5"
|
||||||
|
export ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION="OpenAI GPT-5.5 (OpenRouter)"
|
||||||
|
_setup_isolated_config openrouter "openai/gpt-5.5" high ''
|
||||||
|
_apply_effort openrouter high
|
||||||
|
ANTHROPIC_BASE_URL=https://openrouter.ai/api \
|
||||||
|
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL=x-ai/grok-4.20 \
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="Grok 4.20" \
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="xAI Grok 4.20 (OpenRouter)" \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL=qwen/qwen3.7-max \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="Qwen3.7 Max" \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="Qwen3.7 Max (OpenRouter)" \
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL=minimax/minimax-m3 \
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="MiniMax M3" \
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="MiniMax M3 (OpenRouter)" \
|
||||||
|
ANTHROPIC_DEFAULT_FABLE_MODEL=meta-llama/llama-4-maverick \
|
||||||
|
ANTHROPIC_DEFAULT_FABLE_MODEL_NAME="Llama 4 Maverick" \
|
||||||
|
ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION="Meta Llama 4 Maverick (OpenRouter)" \
|
||||||
|
CLAUDE_CODE_SUBAGENT_MODEL=openai/gpt-5.5 \
|
||||||
|
CLAUDE_CODE_DISABLE_1M_CONTEXT=1 \
|
||||||
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||||||
|
claude --dangerously-skip-permissions --system-prompt-file "$_PROMPT_FILE" "$@"
|
||||||
|
OPENROUTEREOF
|
||||||
|
chmod +x "$BIN_DIR/ai-openrouter"
|
||||||
|
|
||||||
# === ai-gemini ===
|
# === ai-gemini ===
|
||||||
cat > "$BIN_DIR/ai-gemini" << 'GEMINIEOF'
|
cat > "$BIN_DIR/ai-gemini" << 'GEMINIEOF'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
@@ -1066,33 +1598,31 @@ if [ -z "$agy_bin" ] || [ ! -f "$agy_bin" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
source "$HOME/.local/bin/ai-api-helpers.sh" 2>/dev/null || true
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||||||
SYS_PROMPT=$(_build_ai_sys_prompt)
|
|
||||||
|
|
||||||
if [ $# -eq 0 ]; then
|
# agy нативно подтягивает правила и проектные .md файлы,
|
||||||
exec "$agy_bin" --dangerously-skip-permissions -i "$SYS_PROMPT\n\nПрочитай правила выше и коротко подтверди готовность к работе."
|
# поэтому ручная инъекция SYS_PROMPT больше не требуется.
|
||||||
else
|
exec "$agy_bin" --dangerously-skip-permissions "$@"
|
||||||
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
|
GEMINIEOF
|
||||||
chmod +x "$BIN_DIR/ai-gemini"
|
chmod +x "$BIN_DIR/ai-gemini"
|
||||||
|
# Подменяем путь к agy, если BIN_DIR отличается от ~/.local/bin
|
||||||
|
[ "$BIN_DIR" != "$HOME/.local/bin" ] && sed -i "s|\$HOME/\.local/bin|\$HOME/${BIN_DIR#$HOME/}|g" "$BIN_DIR/ai-gemini"
|
||||||
|
|
||||||
# === ai-claude ===
|
# === ai-claude ===
|
||||||
cat > "$BIN_DIR/ai-claude" << 'CLAUDEEOF'
|
cat > "$BIN_DIR/ai-claude" << 'CLAUDEEOF'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ai-claude - запуск оригинального Claude Code (Anthropic)
|
# ai-claude - запуск оригинального Claude Code (Anthropic)
|
||||||
source "$HOME/.local/bin/ai-api-helpers.sh" 2>/dev/null || true
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||||||
SYS_PROMPT=$(_build_ai_sys_prompt)
|
_PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
|
||||||
exec claude --dangerously-skip-permissions --system-prompt "$SYS_PROMPT" "$@"
|
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
|
||||||
|
_build_ai_sys_prompt > "$_PROMPT_FILE"
|
||||||
|
export AI_LAUNCHER=claude
|
||||||
|
# ai-claude работает в дефолтном ~/.claude (нативный логин и аккаунты).
|
||||||
|
# Модель хранится нативно в ~/.claude/settings.json; другие ai-* лаунчеры теперь
|
||||||
|
# изолированы в своих CLAUDE_CONFIG_DIR, поэтому в пикер не протекают чужие модели -
|
||||||
|
# показываются только нативные модели Claude Code.
|
||||||
|
_apply_effort claude xhigh
|
||||||
|
claude --dangerously-skip-permissions --system-prompt-file "$_PROMPT_FILE" "$@"
|
||||||
CLAUDEEOF
|
CLAUDEEOF
|
||||||
chmod +x "$BIN_DIR/ai-claude"
|
chmod +x "$BIN_DIR/ai-claude"
|
||||||
|
|
||||||
@@ -1102,14 +1632,23 @@ if [ "$USE_VLESS" -eq 1 ]; then
|
|||||||
sed -i 's/^exec "\$codex_bin"/exec proxychains4 -f "\$HOME\/\.proxychains-xray\.conf" "\$codex_bin"/' "$BIN_DIR/ai-gpt"
|
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-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/^claude --dangerously-skip-permissions/proxychains4 -f "\$HOME\/\.proxychains-xray\.conf" claude --dangerously-skip-permissions/' "$BIN_DIR/ai-kimi"
|
||||||
|
sed -i 's/^claude --dangerously-skip-permissions/proxychains4 -f "\$HOME\/\.proxychains-xray\.conf" claude --dangerously-skip-permissions/' "$BIN_DIR/ai-openrouter"
|
||||||
sed -i 's/^\([[:space:]]*\)exec "\$agy_bin"/\1exec proxychains4 -f "\$HOME\/\.proxychains-xray\.conf" "\$agy_bin"/' "$BIN_DIR/ai-gemini"
|
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"
|
sed -i 's/^claude --dangerously-skip-permissions/proxychains4 -f "\$HOME\/\.proxychains-xray\.conf" claude --dangerously-skip-permissions/' "$BIN_DIR/ai-claude"
|
||||||
success "proxychains4 интегрирован"
|
success "proxychains4 интегрирован"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
info "Удаляю старые версии скриптов (claude_*)..."
|
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"
|
rm -f "$BIN_DIR/claude_gpt" "$BIN_DIR/claude_deepseek" "$BIN_DIR/claude_kimi" "$BIN_DIR/claude_gemini" "$BIN_DIR/claude_api_helpers.sh"
|
||||||
|
|
||||||
|
# Если переехали на ~/bin — удаляем старые скрипты из ~/.local/bin
|
||||||
|
if [ "$BIN_DIR" != "$HOME/.local/bin" ]; then
|
||||||
|
warn "BIN_DIR=$BIN_DIR — удаляю старые скрипты из ~/.local/bin/ ..."
|
||||||
|
rm -f "$HOME/.local/bin/ai-gpt" "$HOME/.local/bin/ai-deepseek" "$HOME/.local/bin/ai-kimi" \
|
||||||
|
"$HOME/.local/bin/ai-openrouter" "$HOME/.local/bin/ai-gemini" "$HOME/.local/bin/ai-claude" \
|
||||||
|
"$HOME/.local/bin/ai-api-helpers.sh" "$HOME/.local/bin/claude-gpt-effort-proxy.py"
|
||||||
|
fi
|
||||||
|
|
||||||
success "Скрипты сгенерированы."
|
success "Скрипты сгенерированы."
|
||||||
|
|
||||||
# ── 9. Итог ──────────────────────────────────────────────────
|
# ── 9. Итог ──────────────────────────────────────────────────
|
||||||
@@ -1118,13 +1657,16 @@ echo -e "${GREEN}═════════════════════
|
|||||||
echo -e "${GREEN} Установка завершена!${NC}"
|
echo -e "${GREEN} Установка завершена!${NC}"
|
||||||
echo -e "${GREEN}════════════════════════════════════════════════════${NC}"
|
echo -e "${GREEN}════════════════════════════════════════════════════${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Доступные команды (теперь это независимые скрипты в ~/.local/bin):"
|
echo "Доступные команды (теперь это независимые скрипты в ~/${BIN_DIR#$HOME/}):"
|
||||||
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 ""
|
||||||
echo -e "${YELLOW}⚠️ Для Gemini используйте отдельный Google-аккаунт!${NC}"
|
echo " На базе Claude Code:"
|
||||||
|
echo -e " ${CYAN}ai-claude${NC} - Оригинальный Claude Code (Anthropic)"
|
||||||
|
echo -e " ${CYAN}ai-deepseek${NC} - DeepSeek (через Claude Code, API ключ сохраняется)"
|
||||||
|
echo -e " ${CYAN}ai-kimi${NC} - Kimi K2.7 Code (через Claude Code, API ключ сохраняется)"
|
||||||
|
echo -e " ${CYAN}ai-openrouter${NC} - OpenRouter (через Claude Code: GPT-5.5, Opus 4.8, Sonnet 4.6)"
|
||||||
|
echo ""
|
||||||
|
echo " Нативные CLI:"
|
||||||
|
echo -e " ${CYAN}ai-gpt${NC} - OpenAI Codex (нативный CLI, автоустановка)"
|
||||||
|
echo -e " ${CYAN}ai-gemini${NC} - Gemini (нативный agy CLI, автоустановка)"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "Чтобы команды были доступны сразу, выполните: ${GREEN}exec bash${NC}"
|
echo -e "Чтобы команды были доступны сразу, выполните: ${GREEN}exec bash${NC}"
|
||||||
1908
scripts/fuck-rkn.sh
Normal file
1908
scripts/fuck-rkn.sh
Normal file
File diff suppressed because it is too large
Load Diff
19
scripts/ks-off.sh
Normal file
19
scripts/ks-off.sh
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ks-off.sh — временно отключить kill switch UFW
|
||||||
|
# После этого отключи Amnezia через её GUI — трафик пойдёт напрямую через провайдера.
|
||||||
|
# Обратная команда: sudo bash ks-on.sh
|
||||||
|
|
||||||
|
if [ "$(id -u)" != "0" ]; then
|
||||||
|
echo "Запускай от root: sudo bash $0"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Отключаем UFW kill switch..."
|
||||||
|
ufw disable
|
||||||
|
echo ""
|
||||||
|
echo "Готово. UFW выключен."
|
||||||
|
echo "Теперь отключи Amnezia через её GUI — трафик пойдёт напрямую (российский IP)."
|
||||||
|
echo ""
|
||||||
|
echo "Чтобы вернуть kill switch: sudo bash ks-on.sh"
|
||||||
|
_log_file="${USER_HOME:-$HOME}/.config/ai-setup/setup.log"
|
||||||
|
printf '%s [ks-off] Kill switch отключён на %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$(hostname)" >> "$_log_file" 2>/dev/null || true
|
||||||
48
scripts/ks-on.sh
Normal file
48
scripts/ks-on.sh
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ks-on.sh — включить/восстановить kill switch UFW
|
||||||
|
# Перед этим подключи Amnezia через её GUI, иначе интернет будет полностью заблокирован.
|
||||||
|
|
||||||
|
if [ "$(id -u)" != "0" ]; then
|
||||||
|
echo "Запускай от root: sudo bash $0"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Проверяем что Amnezia поднята..."
|
||||||
|
if ! ip link show amn0 &>/dev/null && ! ip link show amnezia0 &>/dev/null; then
|
||||||
|
echo ""
|
||||||
|
echo "ВНИМАНИЕ: интерфейс amn0/amnezia0 не найден!"
|
||||||
|
echo "Похоже Amnezia не подключена."
|
||||||
|
echo "Если включить UFW сейчас — интернет полностью заблокируется."
|
||||||
|
echo ""
|
||||||
|
read -p "Всё равно включить kill switch? [y/N] " CONFIRM
|
||||||
|
if [ "$CONFIRM" != "y" ] && [ "$CONFIRM" != "Y" ]; then
|
||||||
|
echo "Отменено."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Отключаем IPv6 (утечка мимо UFW kill switch, UFW работает только с IPv4)
|
||||||
|
ipv6_cnt=$(ip -6 addr show scope global 2>/dev/null | grep -c 'inet6' || true)
|
||||||
|
if [ "$ipv6_cnt" -gt 0 ]; then
|
||||||
|
sysctl -w net.ipv6.conf.all.disable_ipv6=1 >/dev/null
|
||||||
|
sysctl -w net.ipv6.conf.default.disable_ipv6=1 >/dev/null
|
||||||
|
cat > /etc/sysctl.d/99-disable-ipv6.conf << 'SYSCTEOF'
|
||||||
|
# Отключение IPv6 — требуется для защиты kill switch (UFW работает только с IPv4)
|
||||||
|
net.ipv6.conf.all.disable_ipv6=1
|
||||||
|
net.ipv6.conf.default.disable_ipv6=1
|
||||||
|
SYSCTEOF
|
||||||
|
systemctl restart systemd-resolved 2>/dev/null || true
|
||||||
|
echo "IPv6 отключён ($ipv6_cnt адресов)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Включаем UFW kill switch..."
|
||||||
|
ufw default deny outgoing >/dev/null 2>&1 || true
|
||||||
|
ufw allow out on amn0 >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
ufw enable
|
||||||
|
echo ""
|
||||||
|
echo "Готово. Kill switch активен."
|
||||||
|
echo ""
|
||||||
|
ufw status | head -3
|
||||||
|
_log_file="${USER_HOME:-$HOME}/.config/ai-setup/setup.log"
|
||||||
|
printf '%s [ks-on] Kill switch включён на %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$(hostname)" >> "$_log_file" 2>/dev/null || true
|
||||||
398
scripts/ru-bypass.sh
Normal file
398
scripts/ru-bypass.sh
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ru-bypass.sh — .ru трафик напрямую мимо Amnezia, всё остальное через VPN + kill switch
|
||||||
|
#
|
||||||
|
# Принцип: ipset + более специфичные маршруты имеют приоритет над amn0.
|
||||||
|
# Kill switch (UFW) остаётся активным — не-.ru трафик при отвале Amnezia блокируется.
|
||||||
|
#
|
||||||
|
# Первый запуск устанавливает ipset, два systemd сервиса и NetworkManager dispatcher:
|
||||||
|
# - ru-ipset-restore.service запускается ДО UFW, восстанавливает ipset из файла
|
||||||
|
# - ru-bypass.service запускается после network-online, обновляет RIPE-список и маршруты
|
||||||
|
# Каждый запуск обновляет список .ru IP-блоков из RIPE (кэш 24ч).
|
||||||
|
#
|
||||||
|
# Использование: sudo bash ru-bypass.sh
|
||||||
|
|
||||||
|
# Сохраняем env-переменные до загрузки конфига (env имеет приоритет)
|
||||||
|
_env_gw="${GATEWAY:-}"
|
||||||
|
_env_dev="${DEV:-}"
|
||||||
|
_env_local_dns="${LOCAL_DNS:-}"
|
||||||
|
_env_amn_srv="${AMNEZIA_SERVER:-}"
|
||||||
|
_env_ks_exc="${KILL_SWITCH_EXCEPTIONS:-}"
|
||||||
|
|
||||||
|
# Загружаем сохранённый конфиг (для запуска из systemd/NM dispatcher без env)
|
||||||
|
[ -f /etc/ru-bypass.conf ] && . /etc/ru-bypass.conf
|
||||||
|
|
||||||
|
# ENV-переменные имеют приоритет над конфигом
|
||||||
|
[ -n "$_env_gw" ] && GATEWAY="$_env_gw"
|
||||||
|
[ -n "$_env_dev" ] && DEV="$_env_dev"
|
||||||
|
[ -n "$_env_local_dns" ] && LOCAL_DNS="$_env_local_dns"
|
||||||
|
[ -n "$_env_amn_srv" ] && AMNEZIA_SERVER="$_env_amn_srv"
|
||||||
|
[ -n "$_env_ks_exc" ] && KILL_SWITCH_EXCEPTIONS="$_env_ks_exc"
|
||||||
|
|
||||||
|
# Дефолты (если ни конфиг, ни env не задали значение)
|
||||||
|
GATEWAY="${GATEWAY:-192.168.1.1}"
|
||||||
|
DEV="${DEV:-wlp1s0}"
|
||||||
|
LOCAL_DNS="${LOCAL_DNS:-}"
|
||||||
|
AMNEZIA_SERVER="${AMNEZIA_SERVER:-}"
|
||||||
|
KILL_SWITCH_EXCEPTIONS="${KILL_SWITCH_EXCEPTIONS:-}"
|
||||||
|
|
||||||
|
# Базовые исключения, необходимые для работы корпоративных сервисов
|
||||||
|
# Добавляются автоматически, даже если не указаны в конфиге
|
||||||
|
_BUILTIN_EXCEPTIONS="mattermost.eltex-co.ru elph.eltex-co.ru 10.80.0.15"
|
||||||
|
for _exc in $_BUILTIN_EXCEPTIONS; do
|
||||||
|
case " $KILL_SWITCH_EXCEPTIONS " in
|
||||||
|
*" $_exc "*) ;;
|
||||||
|
*) KILL_SWITCH_EXCEPTIONS="$KILL_SWITCH_EXCEPTIONS $_exc" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
KILL_SWITCH_EXCEPTIONS="${KILL_SWITCH_EXCEPTIONS# }"
|
||||||
|
|
||||||
|
# Сохраняем конфиг для будущих запусков (systemd, NM dispatcher)
|
||||||
|
cat > /etc/ru-bypass.conf <<_CONF
|
||||||
|
GATEWAY="$GATEWAY"
|
||||||
|
DEV="$DEV"
|
||||||
|
LOCAL_DNS="$LOCAL_DNS"
|
||||||
|
AMNEZIA_SERVER="$AMNEZIA_SERVER"
|
||||||
|
KILL_SWITCH_EXCEPTIONS="$KILL_SWITCH_EXCEPTIONS"
|
||||||
|
_CONF
|
||||||
|
SETNAME="ru-direct"
|
||||||
|
CACHE="/var/cache/ru-delegations.txt"
|
||||||
|
IPSET_SAVE="/etc/ipset.conf"
|
||||||
|
RIPE_URL="https://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-latest"
|
||||||
|
SCRIPT_DEST="/usr/local/bin/ru-bypass.sh"
|
||||||
|
UFW_BEFORE="/etc/ufw/before.rules"
|
||||||
|
|
||||||
|
if [ "$(id -u)" != "0" ]; then
|
||||||
|
echo "Запускай от root: sudo bash $0"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Первичная настройка (однократно) ---
|
||||||
|
|
||||||
|
if ! command -v ipset >/dev/null 2>&1; then
|
||||||
|
echo "Устанавливаем ipset..."
|
||||||
|
apt-get install -y ipset
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Копируем скрипт в /usr/local/bin (нужно для systemd + NM dispatcher)
|
||||||
|
SELF=$(realpath "$0")
|
||||||
|
if [ "$SELF" != "$SCRIPT_DEST" ]; then
|
||||||
|
cp "$SELF" "$SCRIPT_DEST"
|
||||||
|
chmod +x "$SCRIPT_DEST"
|
||||||
|
echo "Скрипт скопирован в $SCRIPT_DEST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Сервис восстановления ipset ДО старта UFW (однократно)
|
||||||
|
RESTORE_SVC="/etc/systemd/system/ru-ipset-restore.service"
|
||||||
|
if [ ! -f "$RESTORE_SVC" ]; then
|
||||||
|
cat > "$RESTORE_SVC" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Restore ru-direct ipset before UFW starts
|
||||||
|
DefaultDependencies=no
|
||||||
|
Before=ufw.service network.target
|
||||||
|
After=local-fs.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/sbin/ipset restore -exist -file $IPSET_SAVE
|
||||||
|
RemainAfterExit=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable ru-ipset-restore.service
|
||||||
|
echo "Сервис ru-ipset-restore установлен (стартует до UFW)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Основной сервис обновления маршрутов (однократно)
|
||||||
|
BYPASS_SVC="/etc/systemd/system/ru-bypass.service"
|
||||||
|
if [ ! -f "$BYPASS_SVC" ]; then
|
||||||
|
cat > "$BYPASS_SVC" <<'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=Route .ru IP blocks directly (bypass Amnezia VPN)
|
||||||
|
After=network-online.target ru-ipset-restore.service
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/ru-bypass.sh
|
||||||
|
RemainAfterExit=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable ru-bypass.service
|
||||||
|
echo "Сервис ru-bypass установлен."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Timer для ежесуточного обновления (однократно)
|
||||||
|
BYPASS_TIMER="/etc/systemd/system/ru-bypass.timer"
|
||||||
|
if [ ! -f "$BYPASS_TIMER" ]; then
|
||||||
|
cat > "$BYPASS_TIMER" <<'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=Daily update of .ru IP routes (ru-bypass)
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=daily
|
||||||
|
Persistent=true
|
||||||
|
Unit=ru-bypass.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
EOF
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now ru-bypass.timer
|
||||||
|
echo "Timer ru-bypass.timer установлен (ежесуточное обновление RIPE)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# NetworkManager dispatcher — авто-перезапуск когда amn0 поднимается (однократно)
|
||||||
|
NM_DISPATCHER="/etc/NetworkManager/dispatcher.d/99-ru-bypass"
|
||||||
|
if [ ! -f "$NM_DISPATCHER" ]; then
|
||||||
|
cat > "$NM_DISPATCHER" <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
[ "$1" = "amn0" ] && [ "$2" = "up" ] && exec /usr/local/bin/ru-bypass.sh
|
||||||
|
EOF
|
||||||
|
chmod +x "$NM_DISPATCHER"
|
||||||
|
echo "NetworkManager dispatcher установлен."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Локальные хосты (фиксируем IP для доменов, которые должны резолвиться локально) ---
|
||||||
|
# Без этого DNS через VPN может вернуть внешний IP вместо внутреннего,
|
||||||
|
# и трафик пойдёт через VPN вместо прямого соединения.
|
||||||
|
HOSTS_MARKER="# ru-bypass: local hosts"
|
||||||
|
|
||||||
|
# Удаляем старый блок целиком (от маркера до конца файла)
|
||||||
|
# и заодно чистим дубликаты от старых версий скрипта (без маркера)
|
||||||
|
sed -i "/$HOSTS_MARKER/,\$ d; /eltex\.loc/d; /eltex-co\.ru/d" /etc/hosts
|
||||||
|
|
||||||
|
# Добавляем актуальные
|
||||||
|
cat >> /etc/hosts <<_HOSTS
|
||||||
|
$HOSTS_MARKER
|
||||||
|
# Eltex corporate services (*.eltex.loc, mattermost, elph)
|
||||||
|
172.16.0.3 eltex.loc
|
||||||
|
172.16.5.103 intdocs.eltex.loc
|
||||||
|
172.16.5.251 red.eltex.loc
|
||||||
|
172.16.1.17 gitlab.eltex.loc
|
||||||
|
172.16.1.106 pixso.eltex.loc
|
||||||
|
172.16.1.94 mcpe-builder.eltex.loc
|
||||||
|
172.16.5.63 proxy.eltex.loc
|
||||||
|
10.80.0.16 ssw.eltex.loc
|
||||||
|
172.16.5.78 nexus.eltex.loc
|
||||||
|
172.16.1.149 cpe-worker.eltex.loc
|
||||||
|
172.16.5.22 mattermost.eltex-co.ru elph.eltex-co.ru ecss-elph-proxy.eltex-co.ru
|
||||||
|
_HOSTS
|
||||||
|
|
||||||
|
echo "Локальные хосты: *.eltex.loc, mattermost, elph → /etc/hosts"
|
||||||
|
|
||||||
|
# --- Обновляем RIPE-список (кэш 24ч) ---
|
||||||
|
|
||||||
|
if [ ! -f "$CACHE" ] || [ $(( $(date +%s) - $(stat -c %Y "$CACHE" 2>/dev/null || echo 0) )) -gt 86400 ]; then
|
||||||
|
echo "Обновляем список .ru IP-блоков из RIPE..."
|
||||||
|
if curl -fsS -o "$CACHE.tmp" "$RIPE_URL"; then
|
||||||
|
mv "$CACHE.tmp" "$CACHE"
|
||||||
|
else
|
||||||
|
echo "Предупреждение: не удалось скачать RIPE-список"
|
||||||
|
if [ ! -f "$CACHE" ]; then exit 1; fi
|
||||||
|
echo "Используем старый кэш от $(date -r "$CACHE")"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Создаём/обновляем ipset ---
|
||||||
|
|
||||||
|
echo "Обновляем ipset $SETNAME..."
|
||||||
|
# create -exist: не падает если уже есть (UFW на него ссылается, destroy ломает цепочку)
|
||||||
|
ipset create "$SETNAME" hash:net -exist
|
||||||
|
# flush: очищаем записи, но сохраняем сам set (iptables-правило остаётся валидным)
|
||||||
|
ipset flush "$SETNAME"
|
||||||
|
|
||||||
|
python3 -c "
|
||||||
|
import ipaddress
|
||||||
|
|
||||||
|
entries = 0
|
||||||
|
with open('$CACHE') as f_in:
|
||||||
|
for line in f_in:
|
||||||
|
parts = line.strip().split('|')
|
||||||
|
if len(parts) < 5 or parts[1] != 'RU' or parts[2] != 'ipv4':
|
||||||
|
continue
|
||||||
|
ip_str, count = parts[3], int(parts[4])
|
||||||
|
first = ipaddress.IPv4Address(ip_str)
|
||||||
|
last = first + count - 1
|
||||||
|
for net in ipaddress.summarize_address_range(first, last):
|
||||||
|
print(f'add $SETNAME {net}')
|
||||||
|
entries += 1
|
||||||
|
import sys; print(f'# entries: {entries}', file=sys.stderr)
|
||||||
|
" 2>/tmp/ru-ipset-count.txt | ipset restore -exist -quiet
|
||||||
|
|
||||||
|
ENTRIES=$(ipset list "$SETNAME" 2>/dev/null | grep -c '/')
|
||||||
|
echo "ipset обновлён: $ENTRIES записей"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# --- Исключения для kill switch ---
|
||||||
|
|
||||||
|
# AMNEZIA_SERVER — IP/домены серверов Amnezia (нужны для поднятия VPN при активном kill switch)
|
||||||
|
# KILL_SWITCH_EXCEPTIONS — дополнительные IP/домены, доступные напрямую даже при kill switch
|
||||||
|
ALL_EXC="${AMNEZIA_SERVER} ${KILL_SWITCH_EXCEPTIONS}"
|
||||||
|
if [ -n "${ALL_EXC// }" ]; then
|
||||||
|
for item in $ALL_EXC; do
|
||||||
|
if echo "$item" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"; then
|
||||||
|
ips="$item"
|
||||||
|
else
|
||||||
|
ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u)
|
||||||
|
fi
|
||||||
|
for ip in $ips; do
|
||||||
|
ipset add ru-direct "$ip" -exist 2>/dev/null || true
|
||||||
|
echo "Исключение kill switch: $item → $ip (ipset)"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
ipset save ru-direct > /etc/ipset.conf
|
||||||
|
echo "ipset сохранён в /etc/ipset.conf"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# --- Добавляем маршруты ---
|
||||||
|
|
||||||
|
echo "Добавляем маршруты..."
|
||||||
|
rm -f /tmp/ru-routes.batch
|
||||||
|
python3 -c "
|
||||||
|
import ipaddress
|
||||||
|
|
||||||
|
with open('$CACHE') as f, open('/tmp/ru-routes.batch', 'w') as out:
|
||||||
|
count = 0
|
||||||
|
for line in f:
|
||||||
|
parts = line.strip().split('|')
|
||||||
|
if len(parts) < 5 or parts[1] != 'RU' or parts[2] != 'ipv4':
|
||||||
|
continue
|
||||||
|
ip_str, n = parts[3], int(parts[4])
|
||||||
|
first = ipaddress.IPv4Address(ip_str)
|
||||||
|
last = first + n - 1
|
||||||
|
for net in ipaddress.summarize_address_range(first, last):
|
||||||
|
out.write(f'route replace {net} via $GATEWAY dev $DEV\n')
|
||||||
|
count += 1
|
||||||
|
print(f'Маршрутов: {count}')
|
||||||
|
"
|
||||||
|
ip -force -batch /tmp/ru-routes.batch 2>/dev/null
|
||||||
|
|
||||||
|
# --- Маршруты для локальных сетей (*.loc, RFC1918) ---
|
||||||
|
|
||||||
|
LOCAL_NETS="10.0.0.0/8 172.16.0.0/12 192.168.0.0/16"
|
||||||
|
echo "Добавляем маршруты для локальных сетей (*.loc / RFC1918)..."
|
||||||
|
for net in $LOCAL_NETS; do
|
||||||
|
ip route replace "$net" via "$GATEWAY" dev "$DEV" 2>/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
# Маршруты для исключений kill switch
|
||||||
|
ALL_EXC="${AMNEZIA_SERVER} ${KILL_SWITCH_EXCEPTIONS}"
|
||||||
|
if [ -n "${ALL_EXC// }" ]; then
|
||||||
|
for item in $ALL_EXC; do
|
||||||
|
if echo "$item" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"; then
|
||||||
|
ips="$item"
|
||||||
|
else
|
||||||
|
ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u)
|
||||||
|
fi
|
||||||
|
for ip in $ips; do
|
||||||
|
ip route replace "$ip/32" via "$GATEWAY" dev "$DEV" 2>/dev/null
|
||||||
|
echo "Маршрут для исключения: $item → $ip"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# --- DNS для *.loc через LOCAL_DNS (если задан) ---
|
||||||
|
|
||||||
|
if [ -n "$LOCAL_DNS" ]; then
|
||||||
|
if command -v resolvectl >/dev/null 2>&1; then
|
||||||
|
resolvectl dns "$DEV" "$LOCAL_DNS" 2>/dev/null && \
|
||||||
|
resolvectl domain "$DEV" "~loc" 2>/dev/null && \
|
||||||
|
echo "DNS для *.loc → $LOCAL_DNS (интерфейс $DEV)"
|
||||||
|
else
|
||||||
|
echo "Предупреждение: resolvectl не найден, LOCAL_DNS=$LOCAL_DNS не применён"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Правила в UFW before.rules (обновляются при каждом запуске) ---
|
||||||
|
# Маркеры используются для идентификации правил; DEV всегда актуальный.
|
||||||
|
|
||||||
|
UFW_IPSET_MARKER="ru-bypass: ipset $SETNAME"
|
||||||
|
UFW_LOCAL_MARKER="ru-bypass: local-nets-bypass"
|
||||||
|
|
||||||
|
echo "Обновляем правила UFW before.rules..."
|
||||||
|
|
||||||
|
# Удаляем старые правила (если есть) — и в новом, и в старом формате маркеров
|
||||||
|
sed -i "/# $UFW_IPSET_MARKER/d; /# \.ru bypass (ipset $SETNAME)/d" "$UFW_BEFORE"
|
||||||
|
sed -i "/-A ufw-before-output -m set --match-set $SETNAME dst/d" "$UFW_BEFORE"
|
||||||
|
sed -i "/# $UFW_LOCAL_MARKER/d; /# local nets bypass (local-nets-bypass)/d" "$UFW_BEFORE"
|
||||||
|
sed -i "/-A ufw-before-output -d 10\.0\.0\.0\/8 -o/d" "$UFW_BEFORE"
|
||||||
|
sed -i "/-A ufw-before-output -d 172\.16\.0\.0\/12 -o/d" "$UFW_BEFORE"
|
||||||
|
sed -i "/-A ufw-before-output -d 192\.168\.0\.0\/16 -o/d" "$UFW_BEFORE"
|
||||||
|
|
||||||
|
# Добавляем правила заново с актуальным DEV
|
||||||
|
sed -i "0,/^COMMIT/{s/^COMMIT/# $UFW_IPSET_MARKER\n-A ufw-before-output -m set --match-set $SETNAME dst -o $DEV -j ACCEPT\nCOMMIT/}" "$UFW_BEFORE"
|
||||||
|
sed -i "0,/^COMMIT/{s/^COMMIT/# $UFW_LOCAL_MARKER\n-A ufw-before-output -d 10.0.0.0\/8 -o $DEV -j ACCEPT\n-A ufw-before-output -d 172.16.0.0\/12 -o $DEV -j ACCEPT\n-A ufw-before-output -d 192.168.0.0\/16 -o $DEV -j ACCEPT\nCOMMIT/}" "$UFW_BEFORE"
|
||||||
|
|
||||||
|
echo "UFW before.rules обновлён (ipset + локальные сети, DEV=$DEV)."
|
||||||
|
|
||||||
|
# --- Исправляем MANAGE_BUILTINS (должен быть yes, иначе before.rules не вызывается) ---
|
||||||
|
if grep -q '^MANAGE_BUILTINS=no' /etc/default/ufw 2>/dev/null; then
|
||||||
|
sed -i 's/^MANAGE_BUILTINS=no/MANAGE_BUILTINS=yes/' /etc/default/ufw
|
||||||
|
echo "UFW: MANAGE_BUILTINS исправлен (no → yes)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Настройка UFW default deny + allow amn0 (однократно) ---
|
||||||
|
ufw default deny outgoing >/dev/null 2>&1 || true
|
||||||
|
ufw allow out on amn0 >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
if grep -qE "$UFW_IPSET_MARKER|$UFW_LOCAL_MARKER" "$UFW_BEFORE" 2>/dev/null; then
|
||||||
|
|
||||||
|
if ufw status | grep -qE "активен|active"; then
|
||||||
|
ufw reload
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Прямые правила iptables (гарантия работы даже при MANAGE_BUILTINS=no) ---
|
||||||
|
echo "Добавляем прямые правила iptables..."
|
||||||
|
|
||||||
|
# Правило для ipset ru-direct (RU-IP + исключения kill switch)
|
||||||
|
iptables -C OUTPUT -m set --match-set "$SETNAME" dst -o "$DEV" -j ACCEPT 2>/dev/null || \
|
||||||
|
iptables -I OUTPUT 1 -m set --match-set "$SETNAME" dst -o "$DEV" -j ACCEPT
|
||||||
|
|
||||||
|
# Правила для локальных сетей (RFC1918)
|
||||||
|
for _net in 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16; do
|
||||||
|
iptables -C OUTPUT -d "$_net" -o "$DEV" -j ACCEPT 2>/dev/null || \
|
||||||
|
iptables -I OUTPUT 1 -d "$_net" -o "$DEV" -j ACCEPT
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "iptables: прямые правила добавлены."
|
||||||
|
|
||||||
|
# --- Отключаем IPv6 (утечка мимо UFW kill switch) ---
|
||||||
|
# UFW работает только с IPv4 — IPv6-трафик обходит kill switch полностью.
|
||||||
|
_ipv6_cnt=$(ip -6 addr show scope global 2>/dev/null | grep -c 'inet6' || true)
|
||||||
|
if [ "$_ipv6_cnt" -gt 0 ]; then
|
||||||
|
sysctl -w net.ipv6.conf.all.disable_ipv6=1 >/dev/null 2>&1
|
||||||
|
sysctl -w net.ipv6.conf.default.disable_ipv6=1 >/dev/null 2>&1
|
||||||
|
cat > /etc/sysctl.d/99-disable-ipv6.conf << 'SYSCTEOF'
|
||||||
|
# Отключение IPv6 — требуется для защиты kill switch (UFW работает только с IPv4)
|
||||||
|
net.ipv6.conf.all.disable_ipv6=1
|
||||||
|
net.ipv6.conf.default.disable_ipv6=1
|
||||||
|
SYSCTEOF
|
||||||
|
systemctl restart systemd-resolved 2>/dev/null || true
|
||||||
|
echo "IPv6: отключён ($_ipv6_cnt адресов) для защиты kill switch."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Готово."
|
||||||
|
RU_EXAMPLE=$(dig +short ya.ru A 2>/dev/null | head -1)
|
||||||
|
echo " ip route get 8.8.8.8 -> dev amn0 (через VPN)"
|
||||||
|
echo " ip route get ${RU_EXAMPLE:-<ya.ru ip>} -> dev $DEV (напрямую .ru)"
|
||||||
|
echo " ip route get 10.10.0.1 -> dev $DEV (напрямую *.loc / RFC1918)"
|
||||||
|
_log_file="${USER_HOME:-$HOME}/.config/ai-setup/setup.log"
|
||||||
|
printf '%s [ru-bypass] GATEWAY=%s DEV=%s, блоков: %s\n' \
|
||||||
|
"$(date '+%Y-%m-%d %H:%M:%S')" "$GATEWAY" "$DEV" "$ENTRIES" >> "$_log_file" 2>/dev/null || true
|
||||||
231
setup.sh
Executable file
231
setup.sh
Executable file
@@ -0,0 +1,231 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Мастер-скрипт. Запускай от обычного пользователя (sudo попросит сам где нужно).
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
BLD='\033[1m'
|
||||||
|
GRN='\033[0;32m'
|
||||||
|
YEL='\033[0;33m'
|
||||||
|
GRY='\033[0;37m'
|
||||||
|
CLR='\033[0m'
|
||||||
|
|
||||||
|
mkdir -p "$HOME/.config/ai-setup"
|
||||||
|
LOG="$HOME/.config/ai-setup/setup.log"
|
||||||
|
_log() { printf '%s [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" "$2" >> "$LOG"; }
|
||||||
|
|
||||||
|
if command -v whiptail >/dev/null 2>&1; then
|
||||||
|
choice=$(whiptail --title "AI Setup" \
|
||||||
|
--menu "Выбери действие (стрелки + Enter):" 22 70 9 \
|
||||||
|
"1" "AI-инструменты (установить лаунчеры + ключи)" \
|
||||||
|
"2" "Сеть: ru-bypass + kill switch" \
|
||||||
|
"" "─── Дополнительно ───────────────────────────" \
|
||||||
|
"3" "Отключить kill switch (прямой доступ без VPN)" \
|
||||||
|
"4" "Включить kill switch (восстановить защиту)" \
|
||||||
|
"5" "Статус (Amnezia, UFW, AI инструменты, ключи)" \
|
||||||
|
"6" "Проверить сеть (маршрутизация, geo)" \
|
||||||
|
"7" "Обновить (git pull + перегенерация скриптов)" \
|
||||||
|
3>&1 1>&2 2>&3) || exit 0
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLD}=== AI Setup ===${CLR}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YEL}Шаги для новой машины (выполнить по порядку):${CLR}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BLD}1) AI-инструменты${CLR}"
|
||||||
|
echo -e " ${GRY}Устанавливает ai-claude, ai-gpt, ai-deepseek, ai-gemini и др.${CLR}"
|
||||||
|
echo -e " ${GRY}Запрашивает API-ключи. Запускать один раз.${CLR}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BLD}2) Сеть: ru-bypass + kill switch${CLR}"
|
||||||
|
echo -e " ${GRY}.ru сайты (ozon, госуслуги) — напрямую с российским IP.${CLR}"
|
||||||
|
echo -e " ${GRY}*.loc офисные адреса — тоже напрямую.${CLR}"
|
||||||
|
echo -e " ${GRY}Всё остальное — только через Amnezia (kill switch).${CLR}"
|
||||||
|
echo -e " ${GRY}Запускать один раз на каждой машине.${CLR}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YEL}Дополнительно (по необходимости):${CLR}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BLD}3) Отключить kill switch${CLR}"
|
||||||
|
echo -e " ${GRY}Временно — когда нужен прямой доступ без VPN (российский IP).${CLR}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BLD}4) Включить kill switch${CLR}"
|
||||||
|
echo -e " ${GRY}Вернуть защиту обратно после отключения.${CLR}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BLD}5) Статус${CLR}"
|
||||||
|
echo -e " ${GRY}Amnezia, UFW, сервисы, установленные AI инструменты и ключи.${CLR}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BLD}6) Проверить сеть${CLR}"
|
||||||
|
echo -e " ${GRY}Тесты маршрутизации: .ru напрямую, остальное через Amnezia.${CLR}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BLD}7) Обновить${CLR}"
|
||||||
|
echo -e " ${GRY}git pull + перегенерация всех скриптов в ~/bin (или ~/.local/bin).${CLR}"
|
||||||
|
echo ""
|
||||||
|
echo -n "Выбери [1-7] или Enter для выхода: "
|
||||||
|
read -r choice
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -n "$choice" ] && _log "setup" "Пункт $choice на $(hostname)"
|
||||||
|
|
||||||
|
case "$choice" in
|
||||||
|
1)
|
||||||
|
bash scripts/ai-setup.sh
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
echo -e "${GRY}Нужно указать параметры твоей локальной сети:${CLR}"
|
||||||
|
echo -e "${GRY} GATEWAY — IP домашнего/офисного роутера (через него пойдёт .ru трафик напрямую)${CLR}"
|
||||||
|
echo -e "${GRY} DEV — сетевой интерфейс (wifi или провод), через который ты подключён к роутеру${CLR}"
|
||||||
|
echo -e "${GRY} LOCAL_DNS — IP офисного DNS-сервера для разрешения *.loc доменов (необязательно)${CLR}"
|
||||||
|
echo ""
|
||||||
|
mkdir -p "$HOME/.config/ai-setup"
|
||||||
|
cfg_dir="$HOME/.config/ai-setup"
|
||||||
|
# Показываем существующие профили
|
||||||
|
existing=$(ls "$cfg_dir"/network_*.conf 2>/dev/null | sed 's|.*/network_||;s|\.conf||' | tr '\n' ' ')
|
||||||
|
if [ -n "$existing" ]; then
|
||||||
|
echo -e "Существующие профили: ${BLD}${existing}${CLR}"
|
||||||
|
echo -e "${GRY}Введи имя профиля (home/office/$(hostname) и т.д.) или Enter для текущего${CLR}"
|
||||||
|
read -rp "Профиль [$(hostname)]: " chosen_profile
|
||||||
|
chosen_profile="${chosen_profile:-$(hostname)}"
|
||||||
|
else
|
||||||
|
chosen_profile="$(hostname)"
|
||||||
|
fi
|
||||||
|
net_conf="$cfg_dir/network_${chosen_profile}.conf"
|
||||||
|
auto_gw=$(ip route show default 2>/dev/null | awk '/default/ {print $3; exit}')
|
||||||
|
auto_dev=$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')
|
||||||
|
auto_gw="${auto_gw:-192.168.1.1}"
|
||||||
|
auto_dev="${auto_dev:-wlp1s0}"
|
||||||
|
saved_local_dns=""
|
||||||
|
saved_amn_srv=""
|
||||||
|
saved_ks_exc=""
|
||||||
|
if [ -f "$net_conf" ]; then
|
||||||
|
saved_gw=$(grep '^GATEWAY=' "$net_conf" | cut -d= -f2)
|
||||||
|
saved_dev=$(grep '^DEV=' "$net_conf" | cut -d= -f2)
|
||||||
|
saved_local_dns=$(grep '^LOCAL_DNS=' "$net_conf" | cut -d= -f2)
|
||||||
|
saved_amn_srv=$(grep '^AMNEZIA_SERVER=' "$net_conf" | cut -d= -f2)
|
||||||
|
saved_ks_exc=$(grep '^KILL_SWITCH_EXCEPTIONS=' "$net_conf" | cut -d= -f2)
|
||||||
|
auto_gw="${saved_gw:-$auto_gw}"
|
||||||
|
auto_dev="${saved_dev:-$auto_dev}"
|
||||||
|
echo -e "Загружены параметры профиля ${BLD}${chosen_profile}${CLR}: GATEWAY=${BLD}${auto_gw}${CLR} DEV=${BLD}${auto_dev}${CLR}"
|
||||||
|
else
|
||||||
|
echo -e "Новый профиль ${BLD}${chosen_profile}${CLR}. Определено автоматически: GATEWAY=${BLD}${auto_gw}${CLR} DEV=${BLD}${auto_dev}${CLR}"
|
||||||
|
fi
|
||||||
|
echo -e "${GRY}(просто Enter чтобы принять, или введи другое значение)${CLR}"
|
||||||
|
echo ""
|
||||||
|
read -rp "GATEWAY (IP роутера) [${auto_gw}]: " gw
|
||||||
|
read -rp "DEV (интерфейс) [${auto_dev}]: " dev
|
||||||
|
read -rp "LOCAL_DNS (DNS для *.loc) [${saved_local_dns:-пусто}]: " local_dns
|
||||||
|
read -rp "AMNEZIA_SERVER (IP/домен сервера Amnezia) [${saved_amn_srv:-пусто}]: " amn_srv
|
||||||
|
read -e -rp "KS_EXCEPTIONS (исключения kill switch: IP/домены через пробел): " -i "${saved_ks_exc}" ks_exc
|
||||||
|
gw="${gw:-$auto_gw}"
|
||||||
|
dev="${dev:-$auto_dev}"
|
||||||
|
[ "$local_dns" = "пусто" ] && local_dns=""
|
||||||
|
local_dns="${local_dns:-$saved_local_dns}"
|
||||||
|
[ "$amn_srv" = "пусто" ] && amn_srv=""
|
||||||
|
amn_srv="${amn_srv:-$saved_amn_srv}"
|
||||||
|
[ "$ks_exc" = "пусто" ] && ks_exc=""
|
||||||
|
printf 'GATEWAY=%s\nDEV=%s\nLOCAL_DNS=%s\nAMNEZIA_SERVER=%s\nKILL_SWITCH_EXCEPTIONS=%s\n' "$gw" "$dev" "$local_dns" "$amn_srv" "$ks_exc" > "$net_conf"
|
||||||
|
echo ""
|
||||||
|
sudo GATEWAY="$gw" DEV="$dev" LOCAL_DNS="$local_dns" AMNEZIA_SERVER="$amn_srv" KILL_SWITCH_EXCEPTIONS="$ks_exc" USER_HOME="$HOME" bash scripts/ru-bypass.sh
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
echo -e "${YEL}Перед этим выйди из Claude Code — сессия сменит IP.${CLR}"
|
||||||
|
echo -n "Продолжить? [y/N] "
|
||||||
|
read -r confirm
|
||||||
|
[ "$confirm" = "y" ] || [ "$confirm" = "Y" ] || exit 0
|
||||||
|
sudo USER_HOME="$HOME" bash scripts/ks-off.sh
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
sudo USER_HOME="$HOME" bash scripts/ks-on.sh
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
echo -e "${BLD}=== Статус ===${CLR}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${BLD}Сеть:${CLR}"
|
||||||
|
if ip link show amn0 &>/dev/null; then
|
||||||
|
echo -e " ${GRN}✓${CLR} Amnezia (amn0) подключена"
|
||||||
|
else
|
||||||
|
echo -e " ${YEL}✗${CLR} Amnezia (amn0) не найдена"
|
||||||
|
fi
|
||||||
|
if sudo ufw status 2>/dev/null | grep -qE "активен|active"; then
|
||||||
|
echo -e " ${GRN}✓${CLR} UFW kill switch активен"
|
||||||
|
else
|
||||||
|
echo -e " ${YEL}✗${CLR} UFW выключен"
|
||||||
|
fi
|
||||||
|
if systemctl is-active --quiet ru-bypass.service 2>/dev/null || systemctl is-enabled --quiet ru-bypass.service 2>/dev/null; then
|
||||||
|
echo -e " ${GRN}✓${CLR} ru-bypass.service установлен"
|
||||||
|
else
|
||||||
|
echo -e " ${YEL}✗${CLR} ru-bypass.service не установлен (запусти пункт 2)"
|
||||||
|
fi
|
||||||
|
if systemctl is-enabled --quiet ru-ipset-restore.service 2>/dev/null; then
|
||||||
|
echo -e " ${GRN}✓${CLR} ru-ipset-restore.service установлен"
|
||||||
|
else
|
||||||
|
echo -e " ${YEL}✗${CLR} ru-ipset-restore.service не установлен (запусти пункт 2)"
|
||||||
|
fi
|
||||||
|
ipv6_cnt=$(ip -6 addr show scope global 2>/dev/null | grep -c 'inet6' || true)
|
||||||
|
if [ "$ipv6_cnt" -eq 0 ]; then
|
||||||
|
echo -e " ${GRN}✓${CLR} IPv6 отключён (нет утечки)"
|
||||||
|
else
|
||||||
|
echo -e " ${YEL}!${CLR} IPv6 активен ($ipv6_cnt адресов) — возможна утечка, запусти пункт 4"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLD}AI инструменты:${CLR}"
|
||||||
|
for cmd in ai-claude ai-gpt ai-deepseek ai-kimi ai-openrouter ai-gemini; do
|
||||||
|
if command -v "$cmd" &>/dev/null; then
|
||||||
|
echo -e " ${GRN}✓${CLR} $cmd"
|
||||||
|
else
|
||||||
|
echo -e " ${YEL}✗${CLR} $cmd"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLD}API ключи:${CLR}"
|
||||||
|
cfg="$HOME/.config/ai-setup"
|
||||||
|
for f in deepseek_key kimi_key openrouter_key; do
|
||||||
|
name="${f/_key/}"
|
||||||
|
if [ -s "$cfg/$f" ]; then
|
||||||
|
echo -e " ${GRN}✓${CLR} $name"
|
||||||
|
else
|
||||||
|
echo -e " ${YEL}✗${CLR} $name (не задан)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLD}API доступность:${CLR}"
|
||||||
|
for entry in "Anthropic:api.anthropic.com" "DeepSeek:api.deepseek.com" "OpenAI:api.openai.com" "Kimi:api.kimi.com" "OpenRouter:openrouter.ai"; do
|
||||||
|
_name="${entry%%:*}"
|
||||||
|
_host="${entry##*:}"
|
||||||
|
_ms=$(curl -s -o /dev/null -w "%{time_connect}" --max-time 5 "https://$_host" 2>/dev/null)
|
||||||
|
if [ -n "$_ms" ] && [ "$_ms" != "0.000000" ]; then
|
||||||
|
echo -e " ${GRN}✓${CLR} $_name: ${_ms}s"
|
||||||
|
else
|
||||||
|
echo -e " ${YEL}✗${CLR} $_name: недоступен"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLD}Последние события:${CLR}"
|
||||||
|
if [ -f "$LOG" ]; then
|
||||||
|
tail -10 "$LOG" | sed 's/^/ /'
|
||||||
|
else
|
||||||
|
echo " (лог пуст)"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
6)
|
||||||
|
bash tests/test_network.sh
|
||||||
|
;;
|
||||||
|
7)
|
||||||
|
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
echo -e "${BLD}Обновляем репозиторий...${CLR}"
|
||||||
|
git -C "$REPO_DIR" pull --ff-only
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLD}Перегенерация скриптов...${CLR}"
|
||||||
|
bash "$REPO_DIR/scripts/ai-setup.sh"
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Неверный выбор."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -12,12 +12,12 @@ BIN_DIR="$TMPDIR/bin"
|
|||||||
mkdir -p "$BIN_DIR"
|
mkdir -p "$BIN_DIR"
|
||||||
|
|
||||||
# Извлекаем ai-gpt
|
# Извлекаем ai-gpt
|
||||||
awk '/^cat > "\$BIN_DIR\/ai-gpt"/,/^GPTEOF/' ai-setup.sh | \
|
awk '/^cat > "\$BIN_DIR\/ai-gpt"/,/^GPTEOF/' scripts/ai-setup.sh | \
|
||||||
sed "s|\\\$BIN_DIR|$BIN_DIR|g" | bash
|
sed "s|\\\$BIN_DIR|$BIN_DIR|g" | bash
|
||||||
chmod +x "$BIN_DIR/ai-gpt"
|
chmod +x "$BIN_DIR/ai-gpt"
|
||||||
|
|
||||||
# Извлекаем ai-kimi
|
# Извлекаем ai-kimi
|
||||||
awk '/^cat > "\$BIN_DIR\/ai-kimi"/,/^KIMIEOF/' ai-setup.sh | \
|
awk '/^cat > "\$BIN_DIR\/ai-kimi"/,/^KIMIEOF/' scripts/ai-setup.sh | \
|
||||||
sed "s|\\\$BIN_DIR|$BIN_DIR|g" | bash
|
sed "s|\\\$BIN_DIR|$BIN_DIR|g" | bash
|
||||||
chmod +x "$BIN_DIR/ai-kimi"
|
chmod +x "$BIN_DIR/ai-kimi"
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/ai-setup.sh"
|
SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/scripts/ai-setup.sh"
|
||||||
GLOBAL_RULES_SOURCE="$(cd "$(dirname "$0")/.." && pwd)/home-configs/GLOBAL_RULES.md"
|
GLOBAL_RULES_SOURCE="$(cd "$(dirname "$0")/.." && pwd)/home-configs/GLOBAL_RULES.md"
|
||||||
PASS=0; FAIL=0
|
PASS=0; FAIL=0
|
||||||
|
|
||||||
@@ -48,11 +48,12 @@ test_kimi_claude_launcher() {
|
|||||||
# ── ai-kimi: uses official Kimi API ──────────────────────────────────────
|
# ── ai-kimi: uses official Kimi API ──────────────────────────────────────
|
||||||
test_kimi_official_api() {
|
test_kimi_official_api() {
|
||||||
if echo "$KIMI_SECTION" | grep -q 'api.kimi.com/coding' \
|
if echo "$KIMI_SECTION" | grep -q 'api.kimi.com/coding' \
|
||||||
&& echo "$KIMI_SECTION" | grep -q 'ANTHROPIC_MODEL=kimi-k2.6' \
|
&& echo "$KIMI_SECTION" | grep -q 'ANTHROPIC_DEFAULT_OPUS_MODEL=kimi-k2.7' \
|
||||||
|
&& echo "$KIMI_SECTION" | grep -q 'ANTHROPIC_DEFAULT_HAIKU_MODEL=kimi-k2.6' \
|
||||||
&& ! echo "$KIMI_SECTION" | grep -q 'artemox'; then
|
&& ! echo "$KIMI_SECTION" | grep -q 'artemox'; then
|
||||||
ok "ai-kimi: uses official Kimi API and model"
|
ok "ai-kimi: uses official Kimi API (K2.7 opus/sonnet, K2.6 haiku)"
|
||||||
else
|
else
|
||||||
fail "ai-kimi: must use official Kimi API (api.kimi.com/coding) and model kimi-k2.6"
|
fail "ai-kimi: must use official Kimi API (api.kimi.com/coding) with K2.7 opus/sonnet, K2.6 haiku"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
118
tests/test_network.sh
Normal file
118
tests/test_network.sh
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Тесты сетевой настройки: Amnezia + ru-bypass + kill switch
|
||||||
|
# Запускать без sudo (проверяет что доступно обычному пользователю)
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GRN='\033[0;32m'
|
||||||
|
YEL='\033[0;33m'
|
||||||
|
CLR='\033[0m'
|
||||||
|
|
||||||
|
pass=0 fail=0
|
||||||
|
|
||||||
|
check() {
|
||||||
|
local desc="$1" expected="$2"
|
||||||
|
local actual
|
||||||
|
actual=$(eval "$3" 2>&1)
|
||||||
|
if echo "$actual" | grep -qE "$expected"; then
|
||||||
|
echo -e "${GRN}✓${CLR} $desc"
|
||||||
|
pass=$((pass+1))
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${CLR} $desc"
|
||||||
|
echo " ожидалось: $expected"
|
||||||
|
echo " получено: $(echo "$actual" | head -3)"
|
||||||
|
fail=$((fail+1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== 1. Проверка окружения ==="
|
||||||
|
check "Amnezia интерфейс (amn0) существует" "amn0" "ip link show amn0 2>/dev/null"
|
||||||
|
|
||||||
|
# Определяем DEV из конфига или из default route
|
||||||
|
if [ -f "$HOME/.config/ai-setup/network_$(hostname).conf" ]; then
|
||||||
|
source "$HOME/.config/ai-setup/network_$(hostname).conf"
|
||||||
|
fi
|
||||||
|
DEV="${DEV:-$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')}"
|
||||||
|
echo " DEV=$DEV (из конфига)"
|
||||||
|
|
||||||
|
IPSET_INFO=$(sudo ipset list ru-direct 2>/dev/null)
|
||||||
|
if [ -n "$IPSET_INFO" ]; then
|
||||||
|
echo -e "${GRN}✓${CLR} ipset ru-direct существует"
|
||||||
|
IPSET_COUNT=$(echo "$IPSET_INFO" | grep -c '/')
|
||||||
|
if [ "$IPSET_COUNT" -gt 100 ]; then
|
||||||
|
echo -e "${GRN}✓${CLR} ipset не пуст ($IPSET_COUNT блоков)"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${CLR} ipset слишком мал ($IPSET_COUNT блоков)"
|
||||||
|
fi
|
||||||
|
RU_IP=$(echo "$IPSET_INFO" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||||
|
else
|
||||||
|
echo -e "${YEL}?${CLR} ipset — проверь с sudo"
|
||||||
|
RU_IP=$(dig +short ya.ru A | head -1)
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo "=== 2. Маршрутизация .ru vs не-.ru ==="
|
||||||
|
check ".ru IP ($RU_IP) → НЕ через amn0" "$DEV" "ip route get $RU_IP 2>/dev/null"
|
||||||
|
check "8.8.8.8 → через amn0" "amn0" "ip route get 8.8.8.8 2>/dev/null"
|
||||||
|
check "1.1.1.1 → через amn0" "amn0" "ip route get 1.1.1.1 2>/dev/null"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 3. DNS резолвинг ==="
|
||||||
|
check "ozon.ru резолвится" "185\.73\." "dig +short ozon.ru A 2>/dev/null"
|
||||||
|
check "google.com резолвится" "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" "dig +short google.com A 2>/dev/null | head -1"
|
||||||
|
check "gosuslugi.ru резолвится" "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" "dig +short gosuslugi.ru A 2>/dev/null | head -1"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 4. Связность (Amnezia вкл) ==="
|
||||||
|
check "google.com отвечает (VPN)" "HTTP" "curl -sI --max-time 5 https://google.com 2>&1 | head -1"
|
||||||
|
check "ozon.ru отвечает (прямо)" "HTTP" "curl -sI --max-time 5 https://ozon.ru 2>&1 | head -1"
|
||||||
|
check "gosuslugi.ru отвечает (прямо)" "HTTP" "curl -sI --max-time 5 https://gosuslugi.ru 2>&1 | head -1"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 5. Инфраструктура (нужен sudo) ==="
|
||||||
|
UFW_STATUS=$(sudo ufw status 2>/dev/null)
|
||||||
|
if echo "$UFW_STATUS" | grep -qE "активен|active"; then
|
||||||
|
echo -e "${GRN}✓${CLR} UFW активен"
|
||||||
|
else
|
||||||
|
echo -e "${YEL}?${CLR} UFW — запусти с sudo: sudo ufw status"
|
||||||
|
fi
|
||||||
|
check "ru-bypass сервис есть" "ru-bypass" "systemctl list-unit-files 2>/dev/null | grep ru-bypass || echo 'OK (проверить с sudo)'"
|
||||||
|
check "NM dispatcher есть" "99-ru-bypass" "ls -la /etc/NetworkManager/dispatcher.d/99-ru-bypass 2>/dev/null"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 6. Краевые случаи ==="
|
||||||
|
check "api.anthropic.com → amn0" "amn0" "ip route get $(dig +short api.anthropic.com A | head -1) 2>/dev/null"
|
||||||
|
check "ya.ru → НЕ через amn0 (прямо)" "$DEV" "ip route get $(dig +short ya.ru A | head -1) 2>/dev/null"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 7. Geo: внешние IP ==="
|
||||||
|
DEV_DIRECT=$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')
|
||||||
|
ip_vpn=$(curl -s --max-time 10 https://ipinfo.io/ip 2>/dev/null)
|
||||||
|
ip_direct=$(curl -s --interface "$DEV_DIRECT" --max-time 5 https://ipinfo.io/ip 2>/dev/null)
|
||||||
|
echo " VPN IP (через amn0): ${ip_vpn:-недоступно}"
|
||||||
|
if [ -n "$ip_direct" ]; then
|
||||||
|
echo " Прямой IP (через ${DEV_DIRECT:-?}): $ip_direct"
|
||||||
|
if [ "$ip_direct" != "$ip_vpn" ]; then
|
||||||
|
echo -e " ${GRN}✓${CLR} IP разные — .ru идёт напрямую, остальное через VPN"
|
||||||
|
pass=$((pass+1))
|
||||||
|
else
|
||||||
|
echo -e " ${YEL}!${CLR} IP одинаковые — проверь маршрутизацию"
|
||||||
|
fail=$((fail+1))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Kill switch блокирует прямой доступ к ipinfo.io (не-.ru IP) — это штатное поведение.
|
||||||
|
# Вместо этого проверяем что ya.ru маршрутизируется через прямой интерфейс.
|
||||||
|
ya_ip=$(dig +short ya.ru A 2>/dev/null | head -1)
|
||||||
|
ya_dev=$(ip route get "$ya_ip" 2>/dev/null | awk '/dev/ {for(i=1;i<=NF;i++) if ($i=="dev") {print $(i+1); exit}}')
|
||||||
|
echo " Прямой IP: недоступен (UFW kill switch блокирует не-.ru трафик через ${DEV_DIRECT:-?})"
|
||||||
|
if [ "$ya_dev" = "$DEV_DIRECT" ]; then
|
||||||
|
echo -e " ${GRN}✓${CLR} .ru (ya.ru $ya_ip) → $ya_dev (напрямую, не через VPN)"
|
||||||
|
pass=$((pass+1))
|
||||||
|
else
|
||||||
|
echo -e " ${YEL}!${CLR} .ru (ya.ru $ya_ip) → ${ya_dev:-?} (ожидался $DEV_DIRECT)"
|
||||||
|
fail=$((fail+1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================="
|
||||||
|
echo -e "Итого: ${GRN}$pass пройдено${CLR}, ${RED}$fail провалено${CLR}"
|
||||||
|
[ "$fail" -eq 0 ] && echo -e "${GRN}ВСЁ ОК${CLR}" || echo -e "${RED}ЕСТЬ ПРОБЛЕМЫ${CLR}"
|
||||||
Reference in New Issue
Block a user