Compare commits
2 Commits
main
...
901af7d17d
| Author | SHA1 | Date | |
|---|---|---|---|
| 901af7d17d | |||
| 22b4daa5ce |
@@ -2,4 +2,4 @@
|
||||
|
||||
Данные правила применяются ко всем агентам, работающим конкретно в этом репозитории.
|
||||
|
||||
1. **Полная перегенерация standalone-скриптов при запуске сетап-скрипта (Full regeneration of standalone scripts on setup run):** При каждом запуске `ai-setup.sh` все генерируемые скрипты в выбранном `BIN_DIR` (`~/bin`, если он уже есть в `PATH`, иначе `~/.local/bin`: `ai-gpt`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-eltex`, `ai-eltex-qwen`, `ai-mimo`, `ai-qwen`, `ai-openrouter`, `ai-gemini`, `ai-api-helpers.sh`, `ai-claude`, `claude-gpt-effort-proxy.py`) должны быть **полностью перезаписаны** актуальными версиями. Запрещено выполнять слияние (merge) старого и нового содержимого или дополнение (append). Скрипт обязан привести все генерируемые файлы к эталонному виду, однозначно определяемому текущей конфигурацией.
|
||||
1. **Полная перегенерация standalone-скриптов при запуске сетап-скрипта (Full regeneration of standalone scripts on setup run):** При каждом запуске `ai-setup.sh` все генерируемые скрипты в выбранном `BIN_DIR` (`~/bin`, если он уже есть в `PATH`, иначе `~/.local/bin`: `ai-gpt`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-openrouter`, `ai-gemini`, `ai-api-helpers.sh`, `ai-claude`, `claude-gpt-effort-proxy.py`) должны быть **полностью перезаписаны** актуальными версиями. Запрещено выполнять слияние (merge) старого и нового содержимого или дополнение (append). Скрипт обязан привести все генерируемые файлы к эталонному виду, однозначно определяемому текущей конфигурацией.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Как это работает
|
||||
|
||||
`/effort` и `AI_EFFORT` относятся к лаунчерам, которые запускают Claude Code:
|
||||
`ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-mimo`, `ai-qwen`, `ai-openrouter`, `ai-opencode`.
|
||||
`ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-openrouter`, `ai-opencode`.
|
||||
|
||||
Нативные лаунчеры `ai-gpt` (OpenAI Codex), `ai-gemini` (Antigravity `agy`) и `ai-cursor`
|
||||
(Cursor CLI `agent`) не используют эту схему. У них свои настройки reasoning внутри
|
||||
@@ -13,13 +13,13 @@
|
||||
|
||||
## Таблица маппинга
|
||||
|
||||
| Claude Code `/effort` | Anthropic (`ai-claude`) | DeepSeek V4 (`ai-deepseek`) | Kimi K2.7/K2.6 (`ai-kimi`) | GLM 5.3 (`ai-glm`) | OpenRouter/GPT-5.5 (`ai-openrouter`) | OpenCode Go (`ai-opencode`) |
|
||||
| Claude Code `/effort` | Anthropic (`ai-claude`) | DeepSeek V4 (`ai-deepseek`) | Kimi K2.7/K2.6 (`ai-kimi`) | GLM 5.2 (`ai-glm`) | OpenRouter/GPT-5.5 (`ai-openrouter`) | OpenCode Go (`ai-opencode`) |
|
||||
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| `low` | нативно `low` | нативно `low` | thinking on | нативно `low` | без локального маппинга | без локального маппинга |
|
||||
| `medium` | нативно `medium` | провайдер поднимает до `high` | thinking on | провайдер опускает до `low` | без локального маппинга | без локального маппинга |
|
||||
| `high` | нативно `high` | нативно `high` | thinking on | нативно `high` | без локального маппинга | без локального маппинга |
|
||||
| `xhigh` | нативно `xhigh` | провайдер поднимает до `high` | thinking on | провайдер опускает до `high` | без локального маппинга | без локального маппинга |
|
||||
| `max` | нативно `max` | нативно `max` | thinking on | нативно `max` | без локального маппинга | без локального маппинга |
|
||||
| `low` | нативно `low` | провайдер поднимает до `high` | thinking on | на стороне сервера | без локального маппинга | без локального маппинга |
|
||||
| `medium` | нативно `medium` | провайдер поднимает до `high` | thinking on | на стороне сервера | без локального маппинга | без локального маппинга |
|
||||
| `high` | нативно `high` | нативно `high` | thinking on | на стороне сервера | без локального маппинга | без локального маппинга |
|
||||
| `xhigh` | нативно `xhigh` | провайдер поднимает до `max` | thinking on | на стороне сервера | без локального маппинга | без локального маппинга |
|
||||
| `max` | нативно `max` | нативно `max` | thinking on | на стороне сервера | без локального маппинга | без локального маппинга |
|
||||
|
||||
Для OpenRouter текущий лаунчер не делает локального преобразования effort и отправляет
|
||||
значение дальше через Claude Code/OpenRouter. Конкретная интерпретация зависит от выбранной
|
||||
@@ -45,13 +45,12 @@ low -> medium -> high -> xhigh -> max
|
||||
### DeepSeek V4
|
||||
|
||||
```text
|
||||
low -> high -> max
|
||||
high -> max
|
||||
```
|
||||
|
||||
- 3 реальных уровня.
|
||||
- `low` остаётся `low` — реально снижает thinking бюджет.
|
||||
- `medium` и `xhigh` фактически поднимаются до `high`.
|
||||
- `max` остаётся `max` — максимальный бюджет thinking.
|
||||
- 2 реальных уровня.
|
||||
- `low` и `medium` фактически поднимаются до `high`.
|
||||
- `xhigh` фактически поднимается до `max`.
|
||||
- Маппинг выполняется на стороне DeepSeek API.
|
||||
- Дефолт `ai-deepseek`: `high`.
|
||||
|
||||
@@ -66,25 +65,15 @@ on / off
|
||||
- Градаций глубины reasoning нет.
|
||||
- Дефолт `ai-kimi`: `high`.
|
||||
|
||||
### GLM 5.3 (Zhipu AI)
|
||||
### GLM 5.2 (Zhipu AI)
|
||||
|
||||
```text
|
||||
low -> high -> max
|
||||
high / max
|
||||
```
|
||||
|
||||
- 3 реальных уровня, thinking выключить нельзя (`thinking.type=disabled` тоже даёт reasoning).
|
||||
- Anthropic-endpoint Z.ai читает `output_config.effort` от Claude Code и принимает все 5 уровней
|
||||
(невалидное значение -> HTTP 400 `reasoning_effort must be one of: none, minimal, low, medium, high, xhigh, max`).
|
||||
- `medium` фактически опускается до `low`, `xhigh` - до `high`.
|
||||
- Если effort не передан, Z.ai использует `max`. Поэтому `ai-glm` выставляет
|
||||
`CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1` - иначе Claude Code не считает `glm-5.3` effort-capable
|
||||
и параметр не уходит.
|
||||
- Маппинг выполняется на стороне Z.ai API, `ai-glm` локально effort не преобразует.
|
||||
- Дефолт `ai-glm`: `xhigh` (нативный `high` у Z.ai), задаётся через CLI `--effort` при каждом запуске.
|
||||
- Официально Z.ai маппинг `medium`/`xhigh` не документирует. Он выведен замером 2026-09-25:
|
||||
по 3 запроса на уровень, средний `output_tokens` на счётной задаче - `low` ~1.5K,
|
||||
`medium` ~1.1K, `high` ~3.4K, `xhigh` ~2.8K, `max` ~3.8K. `low`/`medium` уверенно короче
|
||||
остальных, а `high` и `max` на такой выборке пересекаются по разбросу.
|
||||
- Маппинг выполняется на стороне Zhipu AI API.
|
||||
- Текущий `ai-glm` не делает локального преобразования effort.
|
||||
- Дефолт `ai-glm`: `high`.
|
||||
|
||||
### OpenRouter / GPT-5.5
|
||||
|
||||
@@ -99,51 +88,6 @@ low -> high -> max
|
||||
Effort не преобразуется локально — thinking budget не имеет прямого эффекта на OpenCode Go бекенд.
|
||||
Дефолт `ai-opencode`: `high`.
|
||||
|
||||
### Xiaomi MiMo V2.6
|
||||
|
||||
```text
|
||||
thinking on (единственный уровень)
|
||||
```
|
||||
|
||||
- `ai-mimo` работает через Claude Code с Anthropic-эндпоинтом Xiaomi (Token Plan региона
|
||||
подписки или `api.xiaomimimo.com`).
|
||||
- Claude Code 2.1.294 отправляет `thinking: {type: "adaptive"}` и `output_config.effort` со всеми
|
||||
5 уровнями и для `mimo-v2.6-pro`, и для `mimo-v2.6-flash`, даже без
|
||||
`CLAUDE_CODE_ALWAYS_ENABLE_EFFORT` (проверено перехватом запроса).
|
||||
- MiMo effort игнорирует. В доке Anthropic API MiMo из параметров рассуждения есть только
|
||||
`thinking.type` (`enabled` по умолчанию / `disabled` / `adaptive`), `effort` и `budget_tokens` нет.
|
||||
Невалидные `output_config.effort=bogus` и `thinking.type=bogus` дают HTTP 200.
|
||||
- Замер 2026-10-13, по 3 запроса на уровень, средний `output_tokens`. Pro, счётная задача:
|
||||
без effort ~310, `low` ~340, `high` ~280, `max` ~315. Pro, задача на включения-исключения:
|
||||
`low` ~970, `max` ~825. Flash на ней же: `low` ~460, `max` ~580. Разброс внутри уровня больше
|
||||
разницы между уровнями, тренда нет.
|
||||
- Реально переключается только thinking: `thinking.type=disabled` даёт 4 токена без размышлений
|
||||
и неверные ответы (2 из 3). Claude Code его для MiMo не отправляет.
|
||||
- `thinking.type=adaptive` (его шлёт Claude Code) MiMo отрабатывает так же, как `enabled`:
|
||||
размышления есть даже на «скажи привет», на задаче на включения-исключения `adaptive` ~670,
|
||||
`enabled` ~600 токенов при разбросе 356-1161. `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1`
|
||||
в Claude Code 2.1.294 запрос не меняет, да и не нужен.
|
||||
- Глубину размышлений ограничивает только `max_tokens`: `ai-mimo` выставляет
|
||||
`CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000` (потолок Claude Code, MiMo принимает до 131072).
|
||||
- Дефолт `ai-mimo`: `max`, задаётся через CLI `--effort` при каждом запуске (как у `ai-eltex-qwen`).
|
||||
На размышления не влияет, но если Xiaomi начнёт учитывать effort, лаунчер сразу будет на максимуме.
|
||||
|
||||
### Qwen3.8 (Alibaba Token Plan)
|
||||
|
||||
```text
|
||||
low -> medium -> high -> xhigh -> max (принимаются, у Max тренда нет)
|
||||
```
|
||||
|
||||
- `ai-qwen` работает через Claude Code с Anthropic-эндпоинтом Alibaba Model Studio
|
||||
(`token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic`).
|
||||
- Qwen проверяет `output_config.effort`: `bogus` даёт HTTP 400 со списком
|
||||
`low, medium, high, xhigh, max`. `thinking.type=adaptive` принимается, размышления есть всегда.
|
||||
- Замер 2026-10-20, задача на включения-исключения, `output_tokens`. `qwen3.8-max`, по 3 запроса:
|
||||
`low` 435/513/615, `high` 363/412/662, `max` 397/539/657 - разброс больше разницы, тренда нет.
|
||||
`qwen3.8-flash`, по 2 запроса: `low` ~430, `max` ~790.
|
||||
- Дефолт `ai-qwen`: `max` через CLI `--effort` при каждом запуске, `CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1`
|
||||
как страховка.
|
||||
|
||||
`claude-gpt-effort-proxy.py` все еще генерируется как совместимый helper для старого
|
||||
`claude-code-proxy` backend-а. Если использовать его вручную, он маппит только
|
||||
`xhigh` -> `high`, потому что `claude-code-proxy` принимает `low`, `medium`, `high`, `max`.
|
||||
@@ -155,16 +99,14 @@ low -> medium -> high -> xhigh -> max (принимаются, у Max тренд
|
||||
| Anthropic | Не нужен | - |
|
||||
| DeepSeek V4 | DeepSeek API | На стороне сервера |
|
||||
| Kimi K2.7/K2.6 | Moonshot API | На стороне сервера |
|
||||
| GLM 5.3 | Z.ai API (`low,medium→low`, `high,xhigh→high`, `max`) | На стороне сервера |
|
||||
| Xiaomi MiMo V2.6 | Не маппится: MiMo принимает `output_config.effort`, но игнорирует его | - |
|
||||
| Qwen3.8 | Alibaba API, значение уходит как есть | На стороне сервера |
|
||||
| GLM 5.2 | Zhipu AI API | На стороне сервера |
|
||||
| OpenRouter/GPT-5.5 | Локального маппинга нет | `ai-openrouter` отправляет значение как есть |
|
||||
| OpenCode Go | Локального маппинга нет | `ai-opencode` отправляет значение через routatic-proxy |
|
||||
| Legacy GPT proxy | helper-маппинг `xhigh` -> `high` | `~/bin/claude-gpt-effort-proxy.py` или `~/.local/bin/claude-gpt-effort-proxy.py` |
|
||||
|
||||
## Persistence effort между сессиями
|
||||
|
||||
Каждый Claude Code лаунчер (`ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-mimo`, `ai-qwen`, `ai-openrouter`, `ai-opencode`)
|
||||
Каждый Claude Code лаунчер (`ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-openrouter`, `ai-opencode`)
|
||||
запоминает свой уровень effort отдельно в `settings.json` лаунчера.
|
||||
|
||||
`/effort` внутри сессии работает для всех уровней. `CLAUDE_CODE_EFFORT_LEVEL` не используется.
|
||||
@@ -177,17 +119,10 @@ AI_EFFORT=high ai-deepseek # включить high
|
||||
ai-deepseek # без флага - использовать уровень из settings.json
|
||||
```
|
||||
|
||||
Дефолты: `xhigh` для `ai-claude` и `ai-glm`, `max` для `ai-eltex`, `ai-eltex-qwen`, `ai-mimo` и `ai-qwen`, `high` для
|
||||
остальных Claude Code лаунчеров. `ai-glm`, `ai-eltex`, `ai-eltex-qwen`, `ai-mimo` и `ai-qwen` передают уровень через CLI
|
||||
`--effort`, поэтому каждый запуск стартует на дефолте (или на `AI_EFFORT`), а `/effort` внутри сессии
|
||||
действует до её конца.
|
||||
Дефолты: `xhigh` для `ai-claude`, `high` для остальных Claude Code лаунчеров.
|
||||
|
||||
## Рекомендации
|
||||
|
||||
- Для повседневной работы: `high` или `xhigh`.
|
||||
- `max` effort имеет реальный эффект у Anthropic, DeepSeek и GLM; у Kimi это всё тот же thinking on.
|
||||
- `low`/`medium` у Kimi фактически не снижают reasoning; у DeepSeek `low` и у GLM `low`/`medium` дают реальный уровень `low` и экономят бюджет.
|
||||
- У GLM осмысленно выбирать только `low`, `high` (или `xhigh`) и `max`.
|
||||
- `ai-eltex` (21.10): effort у `anth-glm-5.3` (Anthropic-эндпоинт Z.ai через прокси) работает: `low` - без
|
||||
размышлений, `medium`/`high`/`max` - с размышлениями. Как и `ai-glm`, стартует на `max`
|
||||
через CLI `--effort`, `AI_EFFORT=high ai-eltex` работает. У `ai-eltex-qwen` прокси effort игнорирует, `/effort` и `AI_EFFORT` там ни на что не влияют.
|
||||
- `max` effort имеет реальный эффект у Anthropic и DeepSeek; у Kimi и GLM это всё тот же thinking on / маппинг на стороне сервера.
|
||||
- `low`/`medium` у DeepSeek, Kimi и GLM фактически не снижают reasoning.
|
||||
|
||||
@@ -7,7 +7,7 @@ bash setup.sh
|
||||
```
|
||||
|
||||
Установит лаунчеры
|
||||
`ai-claude`, `ai-gpt`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-eltex`, `ai-eltex-qwen`, `ai-mimo`, `ai-qwen`, `ai-openrouter`, `ai-opencode`, `ai-gemini`, `ai-cursor`
|
||||
`ai-claude`, `ai-gpt`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-openrouter`, `ai-opencode`, `ai-gemini`, `ai-cursor`
|
||||
в `~/bin`, если он уже есть в `PATH`, иначе в `~/.local/bin/`, и запишет API-ключи.
|
||||
|
||||
После установки, если команды не видны:
|
||||
@@ -15,11 +15,6 @@ bash setup.sh
|
||||
exec bash
|
||||
```
|
||||
|
||||
Обновление всех AI-инструментов одной командой:
|
||||
```bash
|
||||
./setup.sh update
|
||||
```
|
||||
|
||||
## 2. Настроить сеть (Amnezia + ru-bypass)
|
||||
|
||||
Нужно один раз на каждой машине. .ru трафик идёт напрямую,
|
||||
|
||||
32
README.md
32
README.md
@@ -56,15 +56,6 @@ bash setup.sh
|
||||
exec bash
|
||||
```
|
||||
|
||||
### Обновление одной командой
|
||||
|
||||
```bash
|
||||
./setup.sh update # или: ./setup.sh --update
|
||||
```
|
||||
|
||||
То же, что пункт меню `7) Обновить`: `git pull --ff-only` + перегенерация всех
|
||||
`ai-*` лаунчеров через `scripts/ai-setup.sh`. Без аргументов открывается меню.
|
||||
|
||||
## Сеть: ru-bypass + kill switch
|
||||
|
||||
Полная документация: [`home-configs/network/README.md`](home-configs/network/README.md)
|
||||
@@ -113,7 +104,7 @@ sudo GATEWAY=10.0.0.1 DEV=eth0 bash scripts/ru-bypass.sh
|
||||
- `ru-bypass.timer` - ежедневно запускает обновление RIPE-списка и маршрутов
|
||||
- NM dispatcher `/etc/NetworkManager/dispatcher.d/99-ru-bypass` - перезапускает скрипт при поднятии amn0
|
||||
- `/etc/ru-bypass.conf` - root-конфиг для запусков из systemd/NM dispatcher
|
||||
- `/etc/hosts` блок для локальных Eltex-доменов (между `# ru-bypass: local hosts` и `# ru-bypass: end`; ручные записи вне блока скрипт не трогает)
|
||||
- `/etc/hosts` блок для локальных Eltex-доменов
|
||||
|
||||
При каждом запуске:
|
||||
|
||||
@@ -160,11 +151,7 @@ bash tests/test_network.sh # полные тесты
|
||||
- `ai-gpt` - нативный OpenAI Codex CLI
|
||||
- `ai-deepseek` - Claude Code через DeepSeek API
|
||||
- `ai-kimi` - Claude Code через официальный Kimi Code API (`api.kimi.com/coding`)
|
||||
- `ai-glm` - Claude Code через Zhipu AI GLM 5.3 API (`api.z.ai/api/anthropic`)
|
||||
- `ai-eltex` - Claude Code через корпоративный LLM-прокси Eltex (`http://llm.eltex.loc:4000`, LiteLLM, team-подписка GLM). Ключ выдают админы ([дока](https://intdocs.eltex.loc/pages/viewpage.action?pageId=262329878)), модель `anth-glm-5.3` (GLM 5.3 на Anthropic-эндпоинте Z.ai) во всех слотах `/model`: картинки через серверный `analyze_image`, как в `ai-glm`, effort, кеш, окно 1M; если ключу недоступна `anth-glm-5.3`, лаунчер выходит с понятной ошибкой; переопределение: `ELTEX_MODEL`, `ELTEX_BASE_URL`
|
||||
- `ai-eltex-qwen` - то же через корпоративный LLM-прокси Eltex, но на моделях Qwen со своим ключом от админов: по умолчанию `Eltex-Coder-Senior` (Qwen3.8-27B), если она есть у ключа, иначе `Eltex-Qwen3_8-Flash-Next`; ключ можно задать в `AI_ELTEX_LLM_API_KEY`, как в онбординге Eltex AI; переопределение: `ELTEX_QWEN_MODEL`, `ELTEX_BASE_URL`
|
||||
- `ai-mimo` - Claude Code через Xiaomi MiMo V2.6 (Anthropic-совместимый API, Token Plan или pay-as-you-go): всегда самая умная модель `mimo-v2.6-pro` (во всех слотах и сабагентах, `--model opus` на каждом запуске), effort `max`, вывод до 128000 токенов, окно 1M. Эндпоинт региона подписки (`token-plan-sgp`/`-ams`, для `sk-` ключа `api.xiaomimimo.com`) находится перебором при вводе ключа; переопределение: `MIMO_BASE_URL` (нужно для `token-plan-cn`: он в перебор не входит, с наших машин не отвечает)
|
||||
- `ai-qwen` - Claude Code через Alibaba Cloud Model Studio (Token Plan, Anthropic-совместимый API `token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic`): общий ключ вшит в скрипт, запускается сразу без ввода ключа. `qwen3.8-max` в opus/sonnet и сабагентах (`--model opus` на каждом запуске), `qwen3.8-flash` в haiku, effort `max`, окно 1M, картинки видят обе модели
|
||||
- `ai-glm` - Claude Code через Zhipu AI GLM 5.2 API (`api.z.ai/api/anthropic`)
|
||||
- `ai-openrouter` - Claude Code через OpenRouter (GPT-5.5, Grok, Qwen, GLM 5.2 и др.)
|
||||
- `ai-opencode` - Claude Code через OpenCode Go + routatic-proxy (DeepSeek, Kimi, Qwen, GLM, MiniMax, MiMo)
|
||||
- `ai-gemini` - нативный Antigravity CLI `agy`
|
||||
@@ -179,7 +166,7 @@ bash tests/test_network.sh # полные тесты
|
||||
|
||||
## Android CLI
|
||||
|
||||
[Google Android CLI](https://d.android.com/tools/agents) (preview) - официальный терминальный инструмент для Android-разработки, заточенный под AI-агентов. По умолчанию **не устанавливается**: `scripts/ai-setup.sh` (пункты меню 1 и 7) его не трогает. Ставится только по выбору - `bash setup.sh` → пункт `8) Android CLI` (или напрямую `bash scripts/android-cli-setup.sh`).
|
||||
`scripts/ai-setup.sh` также устанавливает [Google Android CLI](https://d.android.com/tools/agents) (preview) - официальный терминальный инструмент для Android-разработки, заточенный под AI-агентов.
|
||||
|
||||
Что устанавливается:
|
||||
- `android` - бинарь CLI в `~/.local/bin/` (или `~/bin` через symlink)
|
||||
@@ -206,7 +193,7 @@ android update # обновить сам Android CLI
|
||||
Интеграция с Claude Code:
|
||||
|
||||
```bash
|
||||
# После установки android-cli-setup.sh выполняется автоматически:
|
||||
# После установки ai-setup.sh выполняется автоматически:
|
||||
android init
|
||||
```
|
||||
|
||||
@@ -222,9 +209,6 @@ android init
|
||||
- `~/.config/ai-setup/deepseek_key` - DeepSeek API key (права 600)
|
||||
- `~/.config/ai-setup/kimi_key` - Kimi API key (права 600)
|
||||
- `~/.config/ai-setup/glm_key` - Zhipu AI (GLM) API key (права 600)
|
||||
- `~/.config/ai-setup/eltex_key` - ключ корпоративного LLM-прокси Eltex от админов (права 600)
|
||||
- `~/.config/ai-setup/eltex_qwen_key` - ключ для моделей Qwen на том же прокси (права 600)
|
||||
- `~/.config/ai-setup/mimo_key` - Xiaomi MiMo API key (права 600), `mimo_base_url` - найденный для него эндпоинт
|
||||
- `~/.config/ai-setup/openrouter_key` - OpenRouter API key (права 600)
|
||||
- `~/.config/ai-setup/opencode_key` - OpenCode Go API key (права 600)
|
||||
- `~/.config/ai-setup/global_rules.md` - глобальные правила агентов
|
||||
@@ -233,16 +217,16 @@ android init
|
||||
## Права запуска агентов
|
||||
|
||||
- `ai-gpt` использует `--dangerously-bypass-approvals-and-sandbox`
|
||||
- `ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-eltex`, `ai-eltex-qwen`, `ai-mimo`, `ai-qwen`, `ai-openrouter`, `ai-opencode`, `ai-gemini` используют `--dangerously-skip-permissions`
|
||||
- `ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-openrouter`, `ai-opencode`, `ai-gemini` используют `--dangerously-skip-permissions`
|
||||
- `ai-cursor` использует `--force` (алиас `--yolo`)
|
||||
|
||||
Удобно для локального coding workflow, но это не sandbox для недоверенного кода.
|
||||
|
||||
## Правила агентов
|
||||
|
||||
В `home-configs/GLOBAL_RULES.md` - компактные правила о границах автономности, минимальных
|
||||
изменениях, пропорциональной проверке, работе с Git и контекстом проекта, а также пользовательские
|
||||
настройки: русский язык, доступ к Redmine и запрет на commit/staging без явной команды.
|
||||
В `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,92 +1,100 @@
|
||||
# Global AI Agent Rules
|
||||
|
||||
Use these defaults across projects, then apply any more specific repository instructions. Prefer clear outcomes and proportional safeguards over process for its own sake.
|
||||
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
|
||||
|
||||
## 1. Resolve Intent Without Needless Blocking
|
||||
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
||||
|
||||
- Inspect the available context before asking the user for information.
|
||||
- Ask a focused question only when an ambiguity would materially change the result, create meaningful risk, or cannot be resolved from the repository or task context.
|
||||
- Otherwise choose the simplest reasonable interpretation and state only assumptions that materially affect the result.
|
||||
- Surface important tradeoffs and recommend a simpler or safer approach when it better serves the user's goal.
|
||||
## 1. Think Before Coding
|
||||
|
||||
## 2. Keep Changes Minimal and Scoped
|
||||
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
||||
|
||||
- Implement only the requested behavior and the support required to make it correct.
|
||||
- Do not add speculative features, single-use abstractions, unrequested configurability, or handling for implausible scenarios.
|
||||
- Preserve existing public behavior and local conventions unless the task requires changing them.
|
||||
- Do not refactor, reformat, or clean up unrelated code. Mention unrelated issues only when they are relevant risks or blockers.
|
||||
- Remove imports, variables, functions, files, or comments made obsolete by your own changes; leave pre-existing dead code alone unless asked.
|
||||
- Every changed line should trace to the requested outcome.
|
||||
Before implementing:
|
||||
- State your assumptions explicitly. If uncertain, ask.
|
||||
- If multiple interpretations exist, present them - don't pick silently.
|
||||
- If a simpler approach exists, say so. Push back when warranted.
|
||||
- If something is unclear, stop. Name what's confusing. Ask.
|
||||
|
||||
## 3. Act Autonomously Within the Request
|
||||
## 2. Simplicity First
|
||||
|
||||
- For requests to answer, explain, review, diagnose, or plan, inspect the relevant materials and report the result. Do not implement changes unless the request also asks for them.
|
||||
- For requests to change, build, or fix, perform safe in-scope local edits and validation without asking for routine confirmation.
|
||||
- Ask before destructive or difficult-to-recover actions, external writes or publication, purchases, or a material expansion of scope.
|
||||
- Exception for Eltex merge-request reviews: a bare review request or explicit `eltex-review` invocation authorizes the skill's documented GitLab writes, including inline discussions, approval, and its thank-you note. Explicit chat-only wording keeps the review read-only.
|
||||
- Preserve unrelated user changes and adapt to a dirty working tree. Never discard or overwrite work you did not create.
|
||||
**Minimum code that solves the problem. Nothing speculative.**
|
||||
|
||||
## 4. Plan and Verify Proportionally
|
||||
- No features beyond what was asked.
|
||||
- No abstractions for single-use code.
|
||||
- No "flexibility" or "configurability" that wasn't requested.
|
||||
- No error handling for impossible scenarios.
|
||||
- If you write 200 lines and it could be 50, rewrite it.
|
||||
|
||||
- Define observable success criteria before changing code.
|
||||
- For non-trivial multi-step work, keep a brief plan whose steps include their verification. Skip planning ceremony for simple tasks.
|
||||
- For a bug fix, reproduce the failure first when feasible. Add a focused regression test when it provides durable value and the repository has an appropriate test layer.
|
||||
- Run the smallest relevant checks first, then broader checks when the change's risk justifies them.
|
||||
- Do not claim completion without fresh evidence. Report the commands run, their results, and anything that remains unverified.
|
||||
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
||||
|
||||
## 5. Keep Git Changes Reviewable
|
||||
## 3. Surgical Changes
|
||||
|
||||
- Never run `git commit` unless the user explicitly asks for a commit.
|
||||
- Do not stage changes unless the user explicitly asks for staging, committing, or another operation that requires it. Do not disturb changes already staged by the user.
|
||||
- Leave tracked edits unstaged so they remain visible in `git diff`.
|
||||
- Remember that untracked files do not appear in ordinary `git diff`. Show them with `git status --short`, list their paths in the handoff, and use `git diff --no-index -- /dev/null <path>` when a patch view is useful.
|
||||
- Before handing off, inspect both `git diff` and `git status --short` and distinguish your changes from unrelated existing work.
|
||||
**Touch only what you must. Clean up only your own mess.**
|
||||
|
||||
## 6. Load Relevant Project Context
|
||||
When editing existing code:
|
||||
- Don't "improve" adjacent code, comments, or formatting.
|
||||
- Don't refactor things that aren't broken.
|
||||
- Match existing style, even if you'd do it differently.
|
||||
- If you notice unrelated dead code, mention it - don't delete it.
|
||||
|
||||
- Before acting, read the applicable agent instruction files and the project documentation directly relevant to the task.
|
||||
- Follow referenced documents when they are needed to understand requirements, commands, architecture, or validation. Do not scan unrelated Markdown files indiscriminately.
|
||||
- Use the repository's existing tools, conventions, and test commands unless the task requires otherwise.
|
||||
When your changes create orphans:
|
||||
- Remove imports/variables/functions that YOUR changes made unused.
|
||||
- Don't remove pre-existing dead code unless asked.
|
||||
|
||||
## 7. Communication Style
|
||||
The test: Every changed line should trace directly to the user's request.
|
||||
|
||||
- Reply in Russian, use informal "ты", and communicate like a friendly programmer teammate.
|
||||
- Lead with the outcome and keep the explanation concise while preserving evidence, material caveats, and next steps.
|
||||
- Keep identifiers, commands, exact error messages, and source quotations in their original language when accuracy matters.
|
||||
- Humor, irony, and swearing are welcome when they fit the conversation; clarity and respect come first.
|
||||
## 4. Goal-Driven Execution
|
||||
|
||||
## 8. Reusable Skills
|
||||
**Define success criteria. Loop until verified.**
|
||||
|
||||
- After completing the current task, propose a focused reusable skill only when the user has repeated a workflow and reuse would clearly help.
|
||||
- Do not interrupt active work to create a skill, and do not create or edit skill files without explicit user approval.
|
||||
- If approved skill changes live outside the current repository, report their exact paths and how the user can inspect them.
|
||||
Transform tasks into verifiable goals:
|
||||
- "Add validation" -> "Write tests for invalid inputs, then make them pass"
|
||||
- "Fix the bug" -> "Write a test that reproduces it, then make it pass"
|
||||
- "Refactor X" -> "Ensure tests pass before and after"
|
||||
|
||||
## 9. Redmine (red.eltex.loc)
|
||||
For multi-step tasks, state a brief plan:
|
||||
```
|
||||
1. [Step] -> verify: [check]
|
||||
2. [Step] -> verify: [check]
|
||||
3. [Step] -> verify: [check]
|
||||
```
|
||||
|
||||
- When the request or an Eltex branch identifies a Redmine issue, read its specification, status, and comments before implementing. Eltex branches commonly start with the issue number (for example, `485448_keyboard_opening_fix` maps to issue `485448`).
|
||||
- Use the personal API key stored only in `~/.config/ai-setup/redmine_key` (mode 600):
|
||||
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
||||
|
||||
---
|
||||
|
||||
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
|
||||
|
||||
# Global Rules for All AI Agents
|
||||
|
||||
The rules below are mandatory for every interaction and task. They are intentionally placed after the general coding guidelines above, but they have higher priority when a conflict exists.
|
||||
|
||||
1. **Communication style:**
|
||||
Always reply in Russian, in a friendly peer-to-peer tone, using informal "ты". Appropriate swearing, humor, sarcasm, and irony are allowed and welcome. Communicate like a live programmer teammate, not like a dry robot.
|
||||
|
||||
2. **No commits without an explicit request:**
|
||||
Never run `git commit` unless the user has directly and unambiguously asked for it. The final commit always remains with the user, or is made strictly by the user's command.
|
||||
|
||||
3. **Plain git diff visibility:**
|
||||
All changes must remain visible to the user through the standard `git diff` command. Leave modified files in the working directory unstaged. Do not add files to the Git index with `git add` unless the user explicitly asks for staging, committing, or another action that requires staging, because staging hides changes from plain `git diff`.
|
||||
|
||||
4. **Project context:**
|
||||
At the start of work, pay close attention to all provided project `.md` files, because they are provided automatically and contain the current repository's context and specifics.
|
||||
|
||||
5. **Reusable skills:**
|
||||
When the user repeats the same instruction, output format, correction, or workflow, treat it as a candidate for a reusable skill.
|
||||
|
||||
Do not interrupt active work just to create a skill. Finish the current task first, then briefly propose the skill name, trigger description, and what files or tools it should contain.
|
||||
|
||||
Do not create or edit skill files silently. Create or update a skill only after explicit user approval.
|
||||
|
||||
Keep skills focused: prefer several small skills over one broad skill. A useful skill should have a precise description for when to use it, short instructions for how to act, and reusable tools, templates, references, or examples when they make the result more stable.
|
||||
|
||||
If skill files are outside the current git repository, clearly state the exact paths changed and how the user can inspect them, because those changes are not visible in the project `git diff`.
|
||||
|
||||
6. **Redmine (red.eltex.loc) access:**
|
||||
Redmine issues require authentication. To read a task's spec and comments, use the personal API key stored in `~/.config/ai-setup/redmine_key` (chmod 600). Eltex branches usually start with the issue number (e.g. `485448_keyboard_opening_fix` → issue `485448` → https://red.eltex.loc/issues/485448). Fetch the issue with its comments (journals) via:
|
||||
```bash
|
||||
curl -fsS -H "X-Redmine-API-Key: $(cat ~/.config/ai-setup/redmine_key)" \
|
||||
curl -s -H "X-Redmine-API-Key: $(cat ~/.config/ai-setup/redmine_key)" \
|
||||
"https://red.eltex.loc/issues/<N>.json?include=journals"
|
||||
```
|
||||
- Read `issue.description`, `issue.status.name`, and recent non-empty `issue.journals[].notes`. If the key is missing or the request fails, report that instead of guessing the task requirements.
|
||||
- Never print, log, or write the API key into repository files.
|
||||
|
||||
## 10. GitLab (gitlab.eltex.loc)
|
||||
|
||||
- GitLab access uses **per-project** tokens stored in `~/.config/ai-setup/` (mode 600). Each one is a project access token and works only for its own project:
|
||||
|
||||
| File | Project | id |
|
||||
| :--- | :--- | :--- |
|
||||
| `gitlab_token_superapp` | `voip/vp-apps/superapp-launcher` | 4928 |
|
||||
| `gitlab_token_common_library` | `voip/vp-apps/common-library` | 4825 |
|
||||
| `gitlab_token_smart_home` | `android_apps/smart_home` | 1409 |
|
||||
|
||||
- Pick the file matching the project you are working with. A token from another project returns HTTP 404, not 403, so a wrong token looks exactly like a missing project — check the token before concluding the resource does not exist.
|
||||
```bash
|
||||
curl -fsS -H "PRIVATE-TOKEN: $(cat ~/.config/ai-setup/gitlab_token_superapp)" \
|
||||
"https://gitlab.eltex.loc/api/v4/projects/4928/merge_requests/241"
|
||||
```
|
||||
- There is no generic `gitlab_token` fallback any more. Tools that accept an explicit token path (for example the `eltex-review` helper) must be given `--token-file <path>`.
|
||||
- `local.properties` in superapp-launcher also holds `gitlab.token` and `redmine.apiKey`; it is gitignored and must stay that way.
|
||||
- Never print, log, or write these tokens into repository files.
|
||||
Read `issue.description` for the spec, `issue.journals[].notes` for comments (recent non-empty ones matter most), `issue.status.name` for status. Keep the key only in that file — never write it into files tracked by git.
|
||||
|
||||
@@ -6,7 +6,7 @@ The custom statusline script renders **only ASCII-safe characters and ANSI color
|
||||
|
||||
- ASCII brackets `[ ]` for git branch and effort level
|
||||
- ANSI 256-color escape sequences for model names, effort levels, and rate-limit percentages
|
||||
- The `$` symbol for the DeepSeek balance (USD only)
|
||||
- The `¥` symbol (U+00A5) for CNY balance in DeepSeek output
|
||||
|
||||
It does **not** use Nerd Font glyphs, powerline separators, or emoji.
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (c) 2026 Виталий Никитенко. MIT License.
|
||||
# UserPromptSubmit hook: перехватывает /logout, /logout-glm, /logout-eltex, /logout-eltex-qwen, /logout-mimo, /logout-deepseek, /logout-kimi.
|
||||
# UserPromptSubmit hook: перехватывает /logout, /logout-glm, /logout-deepseek, /logout-kimi.
|
||||
# Удаляет сохранённый API-ключ провайдера, чтобы при следующем запуске лаунчер
|
||||
# запросил новый.
|
||||
# /logout-glm | /logout-eltex | /logout-eltex-qwen | /logout-mimo | /logout-deepseek | /logout-kimi — явно указанный провайдер.
|
||||
# /logout (без суффикса) — провайдер по AI_LAUNCHER (glm/eltex/eltex-qwen/mimo/deepseek/kimi).
|
||||
# /logout-glm | /logout-deepseek | /logout-kimi — явно указанный провайдер.
|
||||
# /logout (без суффикса) — провайдер по AI_LAUNCHER (glm/deepseek/kimi).
|
||||
# Для ai-claude и прочих лаунчеров /logout отступает (exit 0) — пусть работает
|
||||
# нативный oauth-logout. exit 2 после удаления: LLM не нужна, и нативная
|
||||
# /logout не пытается сделать бессмысленный oauth-logout для api-key провайдеров.
|
||||
@@ -17,14 +17,11 @@ cmd=$(echo "$normalized" | awk '{print $1}')
|
||||
target=""
|
||||
case "$cmd" in
|
||||
logout-glm) target=glm ;;
|
||||
logout-eltex) target=eltex ;;
|
||||
logout-eltex-qwen) target=eltex-qwen ;;
|
||||
logout-mimo) target=mimo ;;
|
||||
logout-deepseek) target=deepseek ;;
|
||||
logout-kimi) target=kimi ;;
|
||||
logout)
|
||||
case "${AI_LAUNCHER:-claude}" in
|
||||
glm|eltex|eltex-qwen|mimo|deepseek|kimi) target="$AI_LAUNCHER" ;;
|
||||
glm|deepseek|kimi) target="$AI_LAUNCHER" ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
;;
|
||||
@@ -38,18 +35,6 @@ case "$target" in
|
||||
rm -f "$CFG/glm_key"
|
||||
echo "✓ GLM ключ удалён. Перезапусти ai-glm, чтобы ввести новый." >&2
|
||||
;;
|
||||
eltex)
|
||||
rm -f "$CFG/eltex_key"
|
||||
echo "✓ Eltex ключ удалён. Перезапусти ai-eltex, чтобы ввести новый." >&2
|
||||
;;
|
||||
eltex-qwen)
|
||||
rm -f "$CFG/eltex_qwen_key"
|
||||
echo "✓ Eltex Qwen ключ удалён. Перезапусти ai-eltex-qwen, чтобы ввести новый." >&2
|
||||
;;
|
||||
mimo)
|
||||
rm -f "$CFG/mimo_key" "$CFG/mimo_base_url"
|
||||
echo "✓ MiMo ключ удалён. Перезапусти ai-mimo, чтобы ввести новый." >&2
|
||||
;;
|
||||
deepseek)
|
||||
rm -f "$CFG/deepseek_key"
|
||||
echo "✓ DeepSeek ключ удалён. Перезапусти ai-deepseek, чтобы ввести новый." >&2
|
||||
|
||||
@@ -4,12 +4,6 @@ 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')
|
||||
# Суффикс [1m] в отображении не нужен: окно контекста задаётся
|
||||
# CLAUDE_CODE_MAX_CONTEXT_TOKENS, а не суффиксом модели
|
||||
model="${model/\[1m\]/}"
|
||||
model="${model% \[1M\]}"
|
||||
model="${model% (1M context)}"
|
||||
model_id="${model_id/\[1m\]/}"
|
||||
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')
|
||||
@@ -17,18 +11,15 @@ week_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty')
|
||||
ctx_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
|
||||
|
||||
|
||||
# Разделитель сегментов: жирная серая средняя точка ·
|
||||
_sep() { printf " \033[1m\033[38;5;240m·\033[00m "; }
|
||||
|
||||
# Цвет effort: насыщенные, глубже чем пастельные (jewel tones)
|
||||
_effort_color() {
|
||||
case "$1" in
|
||||
low) printf '\033[38;5;220mlow\033[00m' ;; # золотой
|
||||
medium) printf '\033[38;5;50mmedium\033[00m' ;; # насыщенный teal
|
||||
high) printf '\033[38;5;38mhigh\033[00m' ;; # глубокий голубой
|
||||
xhigh) printf '\033[38;5;206mxhigh\033[00m' ;; # насыщенный фиолетовый
|
||||
max) printf '\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" ;;
|
||||
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
|
||||
}
|
||||
|
||||
@@ -38,9 +29,6 @@ _brand_color() {
|
||||
deepseek) printf '\033[38;5;69m' ;; # DeepSeek фирменный синий
|
||||
claude) printf '\033[38;5;173m' ;; # Anthropic оранжевый
|
||||
kimi) printf '\033[38;5;81m' ;; # Kimi голубой
|
||||
glm) printf '\033[38;5;223m' ;; # Z.ai кремовый (бренд-фон z.ai, #FFE4B5)
|
||||
mimo) printf '\033[38;5;202m' ;; # Xiaomi оранжевый (#FF6900)
|
||||
qwen) printf '\033[38;5;99m' ;; # Qwen фиолетовый (#615CED)
|
||||
openrouter) printf '\033[38;5;135m' ;; # OpenRouter фиолетовый
|
||||
*) printf '\033[38;5;223m' ;; # кремовый (fallback)
|
||||
esac
|
||||
@@ -51,7 +39,7 @@ 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" ] && { _sep; printf "\033[38;5;111m%s\033[00m" "$branch"; }
|
||||
[ -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")
|
||||
@@ -96,20 +84,18 @@ if [ -n "$model" ]; then
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
[ -n "$account" ] && { _sep; printf "%s%s\033[00m" "$brand_color" "$account"; }
|
||||
[ -n "$account" ] && printf " %s[%s]\033[00m" "$brand_color" "$account"
|
||||
fi
|
||||
|
||||
# Аккаунт/ключ Kimi — показываем alias (account1/account2/...).
|
||||
if [ "${AI_LAUNCHER:-}" = "kimi" ]; then
|
||||
kimi_account=$(cat "$HOME/.config/ai-setup/kimi_keys/current" 2>/dev/null)
|
||||
[ -n "$kimi_account" ] && { _sep; printf "%s%s\033[00m" "$brand_color" "$kimi_account"; }
|
||||
[ -n "$kimi_account" ] && printf " %s[%s]\033[00m" "$brand_color" "$kimi_account"
|
||||
fi
|
||||
if [ -n "$effort" ]; then
|
||||
_sep
|
||||
printf " %s%s " "$brand_color" "$model"
|
||||
_effort_color "$effort"
|
||||
else
|
||||
_sep
|
||||
printf " %s%s\033[00m" "$brand_color" "$model"
|
||||
fi
|
||||
fi
|
||||
@@ -152,7 +138,7 @@ 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" ] && { _sep; printf "\033[38;5;78m%s\033[00m" "$balance"; }
|
||||
[ -n "$balance" ] && printf " \033[38;5;78m%s\033[00m" "$balance"
|
||||
fi
|
||||
|
||||
# Фоновое обновление баланса (не чаще раза в 30 секунд)
|
||||
@@ -173,12 +159,13 @@ if [[ "$model_id" == *deepseek* ]]; then
|
||||
import sys, json
|
||||
d = json.load(sys.stdin)
|
||||
infos = d.get('balance_infos', [])
|
||||
symbols = {'USD': '\$', 'CNY': '\u00a5'}
|
||||
parts = []
|
||||
for info in infos:
|
||||
if info.get('currency', '').upper() != 'USD':
|
||||
continue
|
||||
curr = info.get('currency', '')
|
||||
total = info.get('total_balance', '0')
|
||||
parts.append(f'\${total}')
|
||||
sym = symbols.get(curr, curr)
|
||||
parts.append(f'{sym}{total}')
|
||||
if parts:
|
||||
print(' '.join(parts))
|
||||
" 2>/dev/null)
|
||||
@@ -195,7 +182,7 @@ elif [ "${AI_LAUNCHER:-}" = "openrouter" ]; then
|
||||
cache_file="$HOME/.cache/ai-setup/openrouter_balance"
|
||||
if [ -f "$cache_file" ]; then
|
||||
balance=$(head -1 "$cache_file")
|
||||
[ -n "$balance" ] && { _sep; printf "\033[38;5;78m%s\033[00m" "$balance"; }
|
||||
[ -n "$balance" ] && printf " \033[38;5;78m%s\033[00m" "$balance"
|
||||
fi
|
||||
|
||||
# Фоновое обновление баланса (не чаще раза в 30 секунд)
|
||||
@@ -260,31 +247,13 @@ else
|
||||
five_int=$(printf '%.0f' "$five_pct")
|
||||
remaining=$(fmt_remaining "$five_reset")
|
||||
color=$(pct_color "$five_int")
|
||||
_sep; printf "%s%s:%s%%\033[00m" "$color" "$remaining" "$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")
|
||||
_sep; printf "%s%s:%s%%\033[00m" "$color" "$remaining" "$week_int"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Z.ai (ai-glm, ai-eltex) отдаёт usage только в конце ответа, в начале потока
|
||||
# input_tokens=0, поэтому пока идёт запрос, Claude Code присылает used_percentage=0.
|
||||
# Держим последнее ненулевое значение этой сессии.
|
||||
session_id=$(echo "$input" | jq -r '.session_id // empty')
|
||||
if [ -n "$session_id" ]; then
|
||||
ctx_cache="$HOME/.cache/ai-setup/ctx_last_${session_id}"
|
||||
if [ -n "$ctx_pct" ] && [ "$(printf '%.0f' "$ctx_pct")" -gt 0 ]; then
|
||||
if [ ! -f "$ctx_cache" ]; then
|
||||
mkdir -p "$HOME/.cache/ai-setup"
|
||||
# Раз за сессию чистим кэш старых сессий
|
||||
find "$HOME/.cache/ai-setup" -name 'ctx_last_*' -mtime +7 -delete 2>/dev/null
|
||||
fi
|
||||
echo "$ctx_pct" > "$ctx_cache"
|
||||
elif [ -f "$ctx_cache" ]; then
|
||||
ctx_pct=$(cat "$ctx_cache")
|
||||
printf " %s%s:%s%%\033[00m" "$color" "$remaining" "$week_int"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -294,7 +263,7 @@ fi
|
||||
if [ -n "$ctx_pct" ]; then
|
||||
ctx_int=$(printf '%.0f' "$ctx_pct")
|
||||
color=$(pct_color "$ctx_int")
|
||||
_sep; printf "%sctx:%s%%\033[00m" "$color" "$ctx_int"
|
||||
printf " %sctx:%s%%\033[00m" "$color" "$ctx_int"
|
||||
|
||||
# Звуковой сигнал при первом достижении 60%
|
||||
alert_file="$HOME/.cache/ai-setup/ctx_alert_state"
|
||||
|
||||
@@ -56,7 +56,7 @@ before.rules: 10/8, 172.16/12,
|
||||
5. Добавляет маршруты для RFC1918 диапазонов (`10/8`, `172.16/12`, `192.168/16`) - нужно для `*.loc`
|
||||
6. Добавляет встроенные исключения kill switch (`mattermost.eltex-co.ru`, `elph.eltex-co.ru`, `10.80.0.15`)
|
||||
7. Добавляет пользовательские исключения из `AMNEZIA_SERVER` и `KILL_SWITCH_EXCEPTIONS`
|
||||
8. Обновляет блок локальных Eltex-хостов в `/etc/hosts` (только между маркерами `# ru-bypass: local hosts` / `# ru-bypass: end`, ручные записи сохраняются)
|
||||
8. Обновляет блок локальных Eltex-хостов в `/etc/hosts`
|
||||
9. Если задан `LOCAL_DNS`, настраивает `resolvectl` для `~loc` на интерфейсе `DEV`
|
||||
10. Обновляет `/etc/ufw/before.rules`, прямые iptables-правила и отключает IPv6 при активном kill switch
|
||||
11. При первом запуске: устанавливает systemd service/timer и NetworkManager dispatcher
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (c) 2026 Виталий Никитенко. MIT License.
|
||||
# ============================================================
|
||||
# AI Setup - Claude / GPT-5.5 / Grok / Qwen / DeepSeek / Kimi / GLM / Eltex / Eltex Qwen / Gemini
|
||||
# AI Setup - Claude / GPT-5.5 / Grok / Qwen / DeepSeek / Kimi / GLM / Gemini
|
||||
# Запуск: bash ai-setup.sh
|
||||
# ============================================================
|
||||
|
||||
@@ -267,6 +267,56 @@ else
|
||||
success "agy уже установлен: $(agy --version 2>/dev/null | head -1)"
|
||||
fi
|
||||
|
||||
# ── 4d. Google Android CLI ───────────────────────────────────
|
||||
info "Проверяю Android CLI..."
|
||||
ANDROID_BIN="$HOME/.local/bin/android"
|
||||
[ ! -f "$ANDROID_BIN" ] && ANDROID_BIN="$(command -v android 2>/dev/null || true)"
|
||||
|
||||
if [ -z "$ANDROID_BIN" ] || [ ! -f "$ANDROID_BIN" ]; then
|
||||
info "Устанавливаю Android CLI..."
|
||||
_uname_s=$(uname -s)
|
||||
_uname_m=$(uname -m)
|
||||
ANDROID_PLATFORM=""
|
||||
case "${_uname_s}-${_uname_m}" in
|
||||
Linux-x86_64) ANDROID_PLATFORM="linux_x86_64" ;;
|
||||
Linux-aarch64) ANDROID_PLATFORM="linux_arm64" ;;
|
||||
Linux-arm64) ANDROID_PLATFORM="linux_arm64" ;;
|
||||
Darwin-x86_64) ANDROID_PLATFORM="darwin_x86_64" ;;
|
||||
Darwin-arm64) ANDROID_PLATFORM="darwin_arm64" ;;
|
||||
Darwin-aarch64) ANDROID_PLATFORM="darwin_arm64" ;;
|
||||
MINGW*|MSYS*|CYGWIN*-x86_64) ANDROID_PLATFORM="windows_x86_64" ;;
|
||||
esac
|
||||
|
||||
if [ -n "$ANDROID_PLATFORM" ]; then
|
||||
ANDROID_INSTALL_URL="https://dl.google.com/android/cli/latest/${ANDROID_PLATFORM}/install.sh"
|
||||
if curl -fsSL "$ANDROID_INSTALL_URL" | bash; then
|
||||
success "Android CLI установлен"
|
||||
else
|
||||
warn "Не удалось установить Android CLI через официальный скрипт"
|
||||
fi
|
||||
else
|
||||
warn "Неизвестная платформа ${_uname_s} ${_uname_m}. Пропускаю Android CLI."
|
||||
fi
|
||||
else
|
||||
success "Android CLI уже установлен: $($ANDROID_BIN --version 2>/dev/null | head -1)"
|
||||
fi
|
||||
|
||||
# Официальный install.sh ставит бинарь в ~/.local/bin/android.
|
||||
# Если пользователь использует ~/bin вместо ~/.local/bin - symlink.
|
||||
if [ "$BIN_DIR" != "$HOME/.local/bin" ] && [ -f "$HOME/.local/bin/android" ] && [ ! -f "$BIN_DIR/android" ]; then
|
||||
ln -sf "$HOME/.local/bin/android" "$BIN_DIR/android"
|
||||
success "Android CLI symlink -> $BIN_DIR/android"
|
||||
fi
|
||||
|
||||
# Разворачиваем Android CLI skills для агентов (Claude Code и др.)
|
||||
if command -v android &>/dev/null; then
|
||||
info "Запускаю android init для деплоя skills..."
|
||||
mkdir -p "$HOME/.claude/skills"
|
||||
android init || warn "android init завершился с ошибкой"
|
||||
else
|
||||
info "Android CLI не найден в PATH, пропускаю android init"
|
||||
fi
|
||||
|
||||
# ── 5. routatic-proxy (OpenCode Go → Claude Code) ──────────
|
||||
info "Проверяю routatic-proxy..."
|
||||
ROUTATIC_BIN="$HOME/.local/bin/routatic-proxy"
|
||||
@@ -656,24 +706,25 @@ try:
|
||||
d = json.load(sys.stdin)
|
||||
available = d.get('is_available', False)
|
||||
infos = d.get('balance_infos', [])
|
||||
# Показываем только USD (CNY не выводим)
|
||||
infos = [i for i in infos if i.get('currency', '').upper() == 'USD']
|
||||
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} {status}')
|
||||
print(f' \033[1;36m💰 Баланс DeepSeek:\033[0m {total} {curr} {status}')
|
||||
if float(granted) > 0:
|
||||
print(f' └─ Начислено: \${granted}')
|
||||
print(f' └─ Начислено: {granted} {curr}')
|
||||
if float(topped_up) > 0:
|
||||
print(f' └─ Пополнено: \${topped_up}')
|
||||
cache_parts.append(f'\${total}')
|
||||
# Cache USD balance for statusline
|
||||
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)
|
||||
@@ -1192,9 +1243,7 @@ fi
|
||||
|
||||
_build_ai_sys_prompt > /dev/null # сохраняет в ~/.codex/AGENTS.md (codex читает авто)
|
||||
_setup_codex_sound_hooks
|
||||
exec "$codex_bin" --dangerously-bypass-approvals-and-sandbox \
|
||||
-c 'tui.status_line=["model-with-reasoning","git-branch","context-used","five-hour-limit","current-dir"]' \
|
||||
"$@"
|
||||
exec "$codex_bin" --dangerously-bypass-approvals-and-sandbox "$@"
|
||||
GPTEOF
|
||||
chmod +x "$BIN_DIR/ai-gpt"
|
||||
|
||||
@@ -1511,7 +1560,7 @@ chmod +x "$BIN_DIR/ai-kimi"
|
||||
# === ai-glm ===
|
||||
cat > "$BIN_DIR/ai-glm" << 'GLMEOF'
|
||||
#!/usr/bin/env bash
|
||||
# ai-glm - запуск Claude Code через Zhipu AI GLM 5.3 (Anthropic-compatible API)
|
||||
# ai-glm - запуск Claude Code через Zhipu AI GLM 5.2 (Anthropic-compatible API)
|
||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||||
|
||||
key_file="$HOME/.config/ai-setup/glm_key"
|
||||
@@ -1531,7 +1580,7 @@ glm_base_url="${GLM_BASE_URL:-https://api.z.ai/api/anthropic}"
|
||||
|
||||
if [ -n "$api_key" ]; then
|
||||
echo -n "Проверка сохранённого GLM ключа... "
|
||||
_claude_test_api "$glm_base_url/v1/messages" "x-api-key: $api_key" "glm-5.3"
|
||||
_claude_test_api "$glm_base_url/v1/messages" "x-api-key: $api_key" "glm-5.2"
|
||||
_handle_api_response "GLM" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://z.ai/manage-apikey/apikey-list"
|
||||
ret=$_API_RET
|
||||
if [ $ret -eq 401 ]; then
|
||||
@@ -1572,7 +1621,7 @@ if [ -z "$api_key" ]; then
|
||||
[ -z "$api_key" ] && { echo "Выход."; exit 1; }
|
||||
|
||||
echo -n "Проверяю ключ... "
|
||||
_claude_test_api "$glm_base_url/v1/messages" "x-api-key: $api_key" "glm-5.3"
|
||||
_claude_test_api "$glm_base_url/v1/messages" "x-api-key: $api_key" "glm-5.2"
|
||||
_handle_api_response "GLM" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://z.ai/manage-apikey/apikey-list"
|
||||
ret=$_API_RET
|
||||
if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then
|
||||
@@ -1595,621 +1644,37 @@ trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
|
||||
_build_ai_sys_prompt > "$_PROMPT_FILE"
|
||||
export AI_LAUNCHER=glm
|
||||
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/glm"
|
||||
# Пикер: все три слота = glm-5.3. availableModels НЕ задаём (он схлопывает пикер).
|
||||
# Claude Code навязывает 3 слота; незаданный показал бы чужой Claude, поэтому
|
||||
# opus+sonnet+haiku = glm-5.3. Окно контекста у всех слотов задаёт env
|
||||
# CLAUDE_CODE_MAX_CONTEXT_TOKENS ниже — суффикс [1m] больше не нужен.
|
||||
_setup_isolated_config glm opus xhigh ''
|
||||
# ai-glm стартует на /effort xhigh через CLI --effort (перебивает effortLevel из
|
||||
# settings.json при каждом запуске). Z.ai сворачивает 5 уровней Claude Code в 3
|
||||
# нативных GLM 5.3: low,medium→low; high,xhigh→high; max→max (см. EFFORT_MAPPING.md),
|
||||
# то есть xhigh = нативный high.
|
||||
# Пикер: Opus/Sonnet = GLM 5.2, Haiku = GLM 5.2[1m]. availableModels НЕ задаём
|
||||
# (он схлопывает пикер). Claude Code навязывает 3 слота; незаданный показал бы
|
||||
# чужой Claude, поэтому opus+sonnet = glm-5.2, haiku = glm-5.2[1m].
|
||||
_setup_isolated_config glm opus high ''
|
||||
# ai-glm стартует на /effort max. max невалиден в settings.json (там только
|
||||
# low/medium/high/xhigh), поэтому задаём через CLI --effort, а не _apply_effort.
|
||||
# CLAUDE_CODE_ALWAYS_ENABLE_EFFORT заставляет Claude Code отправлять effort в API
|
||||
# для GLM (иначе glm-5.3 не распознан как effort-capable и параметр не уходит,
|
||||
# а Z.ai по умолчанию берёт max).
|
||||
# /effort внутри сессии работает; ручной override запуска: AI_EFFORT=max ai-glm.
|
||||
# CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000: по умолчанию Claude Code просит у glm-5.3
|
||||
# max_tokens=32000 (размышления + ответ), на трудных задачах GLM в них не укладывается
|
||||
# и ответ обрывается. Z.ai принимает и 128000.
|
||||
# CLAUDE_CODE_DISABLE_1M_CONTEXT НЕ выставляем: GLM 5.3 реально держит 1M контекста,
|
||||
# для GLM (иначе glm-5.2 не распознан как effort-capable и параметр не уходит).
|
||||
# /effort внутри сессии работает; ручной override запуска: AI_EFFORT=high ai-glm.
|
||||
# CLAUDE_CODE_DISABLE_1M_CONTEXT НЕ выставляем: GLM 5.2 реально держит 1M контекста,
|
||||
# а флаг заставлял бы счётчик ctx: считать окно = 200K (врал в ~5 раз). Прочие
|
||||
# лаунчеры (deepseek/kimi/openrouter/opencode) флаг оставляют — у них другой контекст
|
||||
# и флаг убирает [1M]-дубли из пикера; ai-claude его и так не использует.
|
||||
# CLAUDE_CODE_MAX_CONTEXT_TOKENS: glm-* нет в реестре моделей Claude Code, поэтому
|
||||
# окно по умолчанию считается = 200K — ctx: врал бы в ~5 раз, а автокомпакт жал бы
|
||||
# сессию в 200K. Задаём реальное окно 1M явно; env применяется к не-claude моделям
|
||||
# независимо от суффикса, поэтому [1m] у Haiku-слота не нужен и убран.
|
||||
ANTHROPIC_BASE_URL="$glm_base_url" \
|
||||
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL=glm-5.3 \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="GLM 5.3" \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="GLM 5.3 — флагман Zhipu AI" \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL=glm-5.3 \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="GLM 5.3" \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="GLM 5.3 — флагман Zhipu AI" \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL=glm-5.3 \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="GLM 5.3" \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="GLM 5.3 — флагман Zhipu AI" \
|
||||
CLAUDE_CODE_SUBAGENT_MODEL=glm-5.3 \
|
||||
CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000 \
|
||||
CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL=glm-5.2 \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="GLM 5.2" \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="GLM 5.2 — флагман Zhipu AI" \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL=glm-5.2 \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="GLM 5.2" \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="GLM 5.2 — флагман Zhipu AI" \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL='glm-5.2[1m]' \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="GLM 5.2 [1M]" \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="GLM 5.2 [1M] — расширенный контекст" \
|
||||
CLAUDE_CODE_SUBAGENT_MODEL=glm-5.2 \
|
||||
CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1 \
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||||
claude --dangerously-skip-permissions --effort "${AI_EFFORT:-xhigh}" --system-prompt-file "$_PROMPT_FILE" "$@"
|
||||
claude --dangerously-skip-permissions --effort "${AI_EFFORT:-max}" --system-prompt-file "$_PROMPT_FILE" "$@"
|
||||
GLMEOF
|
||||
chmod +x "$BIN_DIR/ai-glm"
|
||||
|
||||
# === ai-eltex ===
|
||||
cat > "$BIN_DIR/ai-eltex" << 'ELTEXEOF'
|
||||
#!/usr/bin/env bash
|
||||
# ai-eltex - запуск Claude Code через корпоративный LLM-прокси Eltex (LiteLLM, team-подписка GLM).
|
||||
# Персональный ключ выдают админы. Дока:
|
||||
# https://intdocs.eltex.loc/pages/viewpage.action?pageId=262329878
|
||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||||
|
||||
key_file="$HOME/.config/ai-setup/eltex_key"
|
||||
api_key=""
|
||||
reauth=0
|
||||
|
||||
# Принудительный сброс сохранённого ключа через AI_RESET_KEY=1
|
||||
if [ "${AI_RESET_KEY:-0}" == "1" ] && [ -f "$key_file" ]; then
|
||||
rm -f "$key_file"
|
||||
api_key=""
|
||||
fi
|
||||
|
||||
[ -f "$key_file" ] && api_key=$(cat "$key_file")
|
||||
|
||||
eltex_base_url="${ELTEX_BASE_URL:-http://llm.eltex.loc:4000}"
|
||||
|
||||
# _eltex_check_key: проверка ключа через GET /v1/models. Виртуальному ключу LiteLLM
|
||||
# доступны только LLM-роуты, а /v1/models заодно отдаёт модели, разрешённые ключу.
|
||||
_eltex_check_key() {
|
||||
local response
|
||||
# Без "|| echo 000": при сетевой ошибке -w сам печатает 000.
|
||||
response=$(curl -s -w "\n%{http_code}" --max-time 15 "$eltex_base_url/v1/models" \
|
||||
-H "Authorization: Bearer $1" \
|
||||
2>/dev/null)
|
||||
_CLAUDE_TEST_CODE=$(echo "$response" | tail -1)
|
||||
_CLAUDE_TEST_BODY=$(echo "$response" | sed '$d')
|
||||
if [ "$_CLAUDE_TEST_CODE" = "000" ]; then
|
||||
echo ""
|
||||
echo -e "\033[0;31m[СЕТЬ]\033[0m Прокси $eltex_base_url недоступен."
|
||||
echo " Нужна сеть Eltex. Если не резолвится llm.eltex.loc - перезапусти ru-bypass (добавит его в /etc/hosts)"
|
||||
echo " или задай адрес явно: ELTEX_BASE_URL=http://172.16.4.68:4000 ai-eltex"
|
||||
exit 1
|
||||
fi
|
||||
_handle_openai_api_response "Eltex LLM" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Лимит team-подписки исчерпан - обратись к админам"
|
||||
}
|
||||
|
||||
# _eltex_pick_models: модель anth-glm-5.3 из _CLAUDE_TEST_BODY (последний ответ /v1/models).
|
||||
# Если имя с суффиксом (anth-glm-5.3-2), берём точное имя, иначе первое с тем же префиксом;
|
||||
# имена групп доступа (ZAI-Test-Group) так не попадут. Список не распарсился
|
||||
# (429/сетевая ошибка/5xx) - стандартное имя наугад. Распарсился, но anth-glm-5.3 нет -
|
||||
# ошибка в stderr, model пустой, возврат 1: дефолтный запуск невозможен.
|
||||
_eltex_pick_models() {
|
||||
local picked avail
|
||||
if ! picked=$(echo "$_CLAUDE_TEST_BODY" | python3 -c '
|
||||
import sys, json
|
||||
ids = [m["id"] for m in json.load(sys.stdin)["data"]]
|
||||
def pick(exact, prefix, skip=None):
|
||||
if exact in ids:
|
||||
return exact
|
||||
return next((i for i in ids if i.startswith(prefix) and not (skip and skip in i)), "-")
|
||||
print(pick("anth-glm-5.3", "anth-glm-5.3-"), " ".join(ids) or "(пусто)")' 2>/dev/null); then
|
||||
model="anth-glm-5.3"
|
||||
return 0
|
||||
fi
|
||||
read -r model avail <<< "$picked"
|
||||
if [ "$model" = "-" ]; then
|
||||
model=""
|
||||
echo -e "\033[0;31m[МОДЕЛЬ]\033[0m Ключу недоступна модель anth-glm-5.3. Прокси отдаёт: $avail" >&2
|
||||
echo " Напиши админам про доступ к anth-glm-5.3 или задай модель явно: ELTEX_MODEL=<имя> ai-eltex" >&2
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
if [ -n "$api_key" ]; then
|
||||
echo -n "Проверка сохранённого Eltex ключа... "
|
||||
_eltex_check_key "$api_key"
|
||||
ret=$_API_RET
|
||||
if [ $ret -eq 401 ]; then
|
||||
rm -f "$key_file"
|
||||
api_key=""
|
||||
reauth=1
|
||||
elif [ $ret -eq 429 ]; then
|
||||
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
|
||||
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
|
||||
elif [ $ret -ne 0 ]; then
|
||||
exit 1
|
||||
elif [ -z "${ELTEX_MODEL:-}" ] && ! _eltex_pick_models; then
|
||||
# Ключ валиден, но anth-glm-5.3 недоступна (не та группа доступа) - авто-логаут,
|
||||
# как при 401: такой ключ не годится для дефолтного запуска, сразу просим новый.
|
||||
echo " Сохранённый ключ удалён (авто-логаут)."
|
||||
rm -f "$key_file"
|
||||
api_key=""
|
||||
reauth=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$api_key" ] && [ "$reauth" -eq 1 ]; then
|
||||
echo -n "Хотите ввести новый Eltex ключ? [Y/n] "
|
||||
read -r _ans; case "${_ans:-Y}" in [Yy]*) ;; *) exit 1 ;; esac
|
||||
fi
|
||||
|
||||
if [ -z "$api_key" ]; then
|
||||
echo "Ключ выдают админы: https://intdocs.eltex.loc/pages/viewpage.action?pageId=262329878"
|
||||
read -r -p "Введите ваш Eltex LLM ключ (sk-...): " api_key
|
||||
[ -z "$api_key" ] && { echo "Выход."; exit 1; }
|
||||
|
||||
echo -n "Проверяю ключ... "
|
||||
_eltex_check_key "$api_key"
|
||||
ret=$_API_RET
|
||||
if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then
|
||||
if [ -z "${ELTEX_MODEL:-}" ] && ! _eltex_pick_models; then
|
||||
echo "Ключ НЕ сохранён."
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$(dirname "$key_file")"
|
||||
echo "$api_key" > "$key_file"
|
||||
chmod 600 "$key_file"
|
||||
echo "Ключ сохранён."
|
||||
if [ $ret -eq 429 ]; then
|
||||
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
|
||||
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
|
||||
fi
|
||||
else
|
||||
echo "Ключ НЕ сохранён."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Модель: ELTEX_MODEL, иначе anth-glm-5.3 - glm-5.3 на Anthropic-эндпоинте Z.ai, Claude Code
|
||||
# ходит в обычный /v1/messages прокси. Проверено 21.10 на всех 6 ключах пилота: картинки
|
||||
# через серверный analyze_image (Read png -> server_tool_use analyze_image -> верный ответ,
|
||||
# как в ai-glm), effort, кеш (~95% входа со второго вызова), окно >200K. zai-glm-5.3
|
||||
# (OpenAI-эндпоинт, без картинок) и flash не используем. Сырой путь /anthropic на этом
|
||||
# прокси не настроен (21.10: 401 "No Anthropic credential"). Первую модель из
|
||||
# /v1/models НЕ берём: там бывают не те модели и имена групп доступа (ZAI-Test-Group),
|
||||
# которые не запустить. Если у ключа нет anth-glm-5.3 - авто-логаут выше (свежевведённый
|
||||
# такой ключ не сохраняется), вместо 400/403 внутри Claude Code. Модель не выбиралась
|
||||
# (429/сетевая ошибка или задан ELTEX_MODEL) - выбираем здесь.
|
||||
[ -n "${model:-}" ] || _eltex_pick_models 2>/dev/null || true
|
||||
model="${ELTEX_MODEL:-${model:-anth-glm-5.3}}"
|
||||
# Окно 1M через суффикс [1m], а не глобальным CLAUDE_CODE_MAX_CONTEXT_TOKENS (через прокси
|
||||
# прошли запросы на 598K 07.10 и 309K 21.10); суффикс Claude Code в API не шлёт.
|
||||
opus_model="$model"
|
||||
case "$model" in anth-glm-5.3*) opus_model="${model}[1m]" ;; esac
|
||||
echo "Модель: $model"
|
||||
|
||||
_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=eltex
|
||||
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/eltex"
|
||||
# Пикер: все слоты (opus, sonnet, haiku) = anth-glm-5.3, других моделей ai-eltex не использует.
|
||||
# Незаданный слот показал бы чужой Claude, а haiku Claude Code берёт и для фоновых задач
|
||||
# вроде заголовков. availableModels НЕ задаём (он схлопывает пикер).
|
||||
# Effort как у ai-glm: старт на max через CLI --effort (перебивает effortLevel из
|
||||
# settings.json; AI_EFFORT=max в settings Claude Code не принимает и шлёт high), /effort
|
||||
# в сессии работает. Z.ai учитывает output_config.effort (21.10): low - без размышлений,
|
||||
# medium/high/max - с размышлениями.
|
||||
# CLAUDE_CODE_ALWAYS_ENABLE_EFFORT - страховка, как в ai-glm.
|
||||
# Имена-алиасы claude-* на прокси НЕ используем: claude-3-5-sonnet Claude Code считает
|
||||
# старым Claude 3.5 Sonnet (без effort и thinking, max_tokens=8192); у claude-5-5-sonnet
|
||||
# один деплоймент на всех, модели говорят, что она Claude Sonnet, а statusline показывает
|
||||
# аккаунт Claude.ai. На качество имя не влияет (09.10, одинаковый запрос с разной строкой
|
||||
# "You are powered by": разброс внутри одного имени больше разницы между именами).
|
||||
# CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 вместо дефолтных 32000, как у ai-glm.
|
||||
# API_TIMEOUT_MS из доки админов: прокси бывает медленным.
|
||||
# Окно anth-glm-5.3 - 1M через [1m] (см. выше). Автокомпакт на 200K из доки админов и
|
||||
# CLAUDE_CODE_DISABLE_1M_CONTEXT не ставим: ctx: показывал бы занятость в ~5 раз больше
|
||||
# реальной.
|
||||
# ENABLE_TOOL_SEARCH НЕ включаем: на Anthropic-эндпоинте Z.ai
|
||||
# GLM с ним путается - ищет Read/Bash через ToolSearch и решает, что их нет. Цена -
|
||||
# схемы встроенных тулов в каждом запросе (~+11K входных токенов).
|
||||
_setup_isolated_config eltex opus xhigh ''
|
||||
# /logout-eltex: без зарегистрированной команды Claude Code отвечает "Unknown command" и
|
||||
# не зовёт UserPromptSubmit-хук (logout-account-hook.sh), а встроенный /logout хук обходит.
|
||||
# Заглушка-команда в конфиге лаунчера пропускает /logout-eltex до хука, он удаляет ключ.
|
||||
mkdir -p "$CLAUDE_CONFIG_DIR/commands"
|
||||
cat > "$CLAUDE_CONFIG_DIR/commands/logout-eltex.md" <<'CMDEOF'
|
||||
---
|
||||
description: Удалить сохранённый ключ ai-eltex (обрабатывает хук, без LLM)
|
||||
---
|
||||
Хук не сработал: удали ~/.config/ai-setup/eltex_key вручную и перезапусти ai-eltex (или запусти AI_RESET_KEY=1 ai-eltex).
|
||||
CMDEOF
|
||||
ANTHROPIC_BASE_URL="$eltex_base_url" \
|
||||
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL="$opus_model" \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="$model" \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="GLM 5.3: картинки, effort, окно 1M" \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL="$opus_model" \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="$model" \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="GLM 5.3: картинки, effort, окно 1M" \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL="$opus_model" \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="$model" \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="GLM 5.3: картинки, effort, окно 1M" \
|
||||
CLAUDE_CODE_SUBAGENT_MODEL="$opus_model" \
|
||||
CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \
|
||||
CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1 \
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||||
API_TIMEOUT_MS=600000 \
|
||||
claude --dangerously-skip-permissions --effort "${AI_EFFORT:-max}" --system-prompt-file "$_PROMPT_FILE" "$@"
|
||||
ELTEXEOF
|
||||
chmod +x "$BIN_DIR/ai-eltex"
|
||||
|
||||
# === ai-eltex-qwen ===
|
||||
cat > "$BIN_DIR/ai-eltex-qwen" << 'ELTEXQWENEOF'
|
||||
#!/usr/bin/env bash
|
||||
# ai-eltex-qwen - запуск Claude Code через корпоративный LLM-прокси Eltex на моделях Qwen
|
||||
# (Eltex-Coder-Senior = Qwen3.8-27B, Eltex-Qwen3_8-Flash-Next = Qwen3.8-Flash-Next-FP8).
|
||||
# Ключ отдельный от ai-eltex, его выдают админы.
|
||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||||
|
||||
key_file="$HOME/.config/ai-setup/eltex_qwen_key"
|
||||
api_key=""
|
||||
reauth=0
|
||||
|
||||
# Принудительный сброс сохранённого ключа через AI_RESET_KEY=1
|
||||
if [ "${AI_RESET_KEY:-0}" == "1" ] && [ -f "$key_file" ]; then
|
||||
rm -f "$key_file"
|
||||
api_key=""
|
||||
fi
|
||||
|
||||
[ -f "$key_file" ] && api_key=$(cat "$key_file")
|
||||
# Ключ из онбординга Eltex AI (переменная для opencode), если своего файла ещё нет
|
||||
if [ -z "$api_key" ] && [ "${AI_RESET_KEY:-0}" != "1" ]; then
|
||||
api_key="${AI_ELTEX_LLM_API_KEY:-}"
|
||||
fi
|
||||
|
||||
eltex_base_url="${ELTEX_BASE_URL:-http://llm.eltex.loc:4000}"
|
||||
|
||||
# _eltex_check_key: проверка ключа через GET /v1/models (как в ai-eltex), заодно отдаёт
|
||||
# модели, разрешённые ключу.
|
||||
_eltex_check_key() {
|
||||
local response
|
||||
# Без "|| echo 000": при сетевой ошибке -w сам печатает 000.
|
||||
response=$(curl -s -w "\n%{http_code}" --max-time 15 "$eltex_base_url/v1/models" \
|
||||
-H "Authorization: Bearer $1" \
|
||||
2>/dev/null)
|
||||
_CLAUDE_TEST_CODE=$(echo "$response" | tail -1)
|
||||
_CLAUDE_TEST_BODY=$(echo "$response" | sed '$d')
|
||||
if [ "$_CLAUDE_TEST_CODE" = "000" ]; then
|
||||
echo ""
|
||||
echo -e "\033[0;31m[СЕТЬ]\033[0m Прокси $eltex_base_url недоступен."
|
||||
echo " Нужна сеть Eltex. Если не резолвится llm.eltex.loc - перезапусти ru-bypass (добавит его в /etc/hosts)"
|
||||
echo " или задай адрес явно: ELTEX_BASE_URL=http://172.16.4.68:4000 ai-eltex-qwen"
|
||||
exit 1
|
||||
fi
|
||||
_handle_openai_api_response "Eltex LLM" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Лимит исчерпан - обратись к админам"
|
||||
}
|
||||
|
||||
if [ -n "$api_key" ]; then
|
||||
echo -n "Проверка сохранённого Eltex Qwen ключа... "
|
||||
_eltex_check_key "$api_key"
|
||||
ret=$_API_RET
|
||||
if [ $ret -eq 401 ]; then
|
||||
rm -f "$key_file"
|
||||
api_key=""
|
||||
reauth=1
|
||||
elif [ $ret -eq 429 ]; then
|
||||
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
|
||||
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
|
||||
elif [ $ret -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$api_key" ] && [ "$reauth" -eq 1 ]; then
|
||||
echo -n "Хотите ввести новый Eltex Qwen ключ? [Y/n] "
|
||||
read -r _ans; case "${_ans:-Y}" in [Yy]*) ;; *) exit 1 ;; esac
|
||||
fi
|
||||
|
||||
if [ -z "$api_key" ]; then
|
||||
echo "Ключ выдают админы (ключ для Eltex-Coder-Senior, не GLM-ключ от ai-eltex)."
|
||||
echo "Можно задать его в AI_ELTEX_LLM_API_KEY, как в онбординге Eltex AI."
|
||||
read -r -p "Введите ваш Eltex Qwen ключ (sk-...): " api_key
|
||||
[ -z "$api_key" ] && { echo "Выход."; exit 1; }
|
||||
|
||||
echo -n "Проверяю ключ... "
|
||||
_eltex_check_key "$api_key"
|
||||
ret=$_API_RET
|
||||
if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then
|
||||
mkdir -p "$(dirname "$key_file")"
|
||||
echo "$api_key" > "$key_file"
|
||||
chmod 600 "$key_file"
|
||||
echo "Ключ сохранён."
|
||||
if [ $ret -eq 429 ]; then
|
||||
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
|
||||
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
|
||||
fi
|
||||
else
|
||||
echo "Ключ НЕ сохранён."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Модель: ELTEX_QWEN_MODEL, иначе Eltex-Coder-Senior, если она есть у ключа, иначе
|
||||
# Eltex-Qwen3_8-Flash-Next. Эмбеддер/реранкер из /v1/models для чата не годятся.
|
||||
model="${ELTEX_QWEN_MODEL:-}"
|
||||
if [ -z "$model" ]; then
|
||||
model=$(echo "$_CLAUDE_TEST_BODY" | python3 -c '
|
||||
import sys, json
|
||||
ids = [m["id"] for m in json.load(sys.stdin).get("data", [])]
|
||||
print(next((m for m in ("Eltex-Coder-Senior", "Eltex-Qwen3_8-Flash-Next") if m in ids), ""))' 2>/dev/null)
|
||||
fi
|
||||
model="${model:-Eltex-Coder-Senior}"
|
||||
echo "Модель: $model"
|
||||
|
||||
_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=eltex-qwen
|
||||
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/eltex-qwen"
|
||||
# Пикер: все три слота = модель ключа (незаданный слот показал бы чужой Claude).
|
||||
# Окно 200K: у Qwen3.8-Flash-Next max_input_tokens=204800. CLAUDE_CODE_DISABLE_1M_CONTEXT
|
||||
# обязателен: без него Claude Code на чистом конфиге переводит opus в opus[1m] (окно 1M).
|
||||
# Effort прокси игнорирует (08.10: low/high = 3227/3297, 09.10: low/max = 4424-5838/
|
||||
# 977-3370 выходных токенов), /effort ни на что не влияет. Стартуем на max через CLI
|
||||
# --effort, как ai-glm и ai-eltex. Картинки Qwen3.8-Flash-Next видит сама.
|
||||
# max_output_tokens=8192 из /models прокси не применяет (09.10 принят max_tokens=64000),
|
||||
# поэтому вывод поднят до 64000, как у ai-glm и ai-eltex.
|
||||
_setup_isolated_config eltex-qwen opus high ''
|
||||
ANTHROPIC_BASE_URL="$eltex_base_url" \
|
||||
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL="$model" \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="$model" \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="$model - корпоративный прокси Eltex" \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL="$model" \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="$model" \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="$model - корпоративный прокси Eltex" \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL="$model" \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="$model" \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="$model - корпоративный прокси Eltex" \
|
||||
CLAUDE_CODE_SUBAGENT_MODEL="$model" \
|
||||
CLAUDE_CODE_DISABLE_1M_CONTEXT=1 \
|
||||
CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||||
API_TIMEOUT_MS=600000 \
|
||||
claude --dangerously-skip-permissions --effort "${AI_EFFORT:-max}" --system-prompt-file "$_PROMPT_FILE" "$@"
|
||||
ELTEXQWENEOF
|
||||
chmod +x "$BIN_DIR/ai-eltex-qwen"
|
||||
|
||||
# === ai-mimo ===
|
||||
cat > "$BIN_DIR/ai-mimo" << 'MIMOEOF'
|
||||
#!/usr/bin/env bash
|
||||
# ai-mimo - запуск Claude Code через Xiaomi MiMo V2.6 (Anthropic-compatible API, Token Plan)
|
||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||||
|
||||
key_file="$HOME/.config/ai-setup/mimo_key"
|
||||
url_file="$HOME/.config/ai-setup/mimo_base_url"
|
||||
api_key=""
|
||||
reauth=0
|
||||
|
||||
# Принудительный сброс сохранённого ключа через AI_RESET_KEY=1
|
||||
if [ "${AI_RESET_KEY:-0}" == "1" ]; then
|
||||
rm -f "$key_file" "$url_file"
|
||||
fi
|
||||
|
||||
[ -f "$key_file" ] && api_key=$(cat "$key_file")
|
||||
|
||||
# Ключ Token Plan (tp-...) работает только на эндпоинте региона подписки (sgp/ams/cn),
|
||||
# ключ pay-as-you-go (sk-...) - на api.xiaomimimo.com. Эндпоинт находим перебором при
|
||||
# проверке ключа и запоминаем в mimo_base_url; MIMO_BASE_URL перебивает.
|
||||
mimo_base_url="${MIMO_BASE_URL:-}"
|
||||
[ -z "$mimo_base_url" ] && [ -f "$url_file" ] && mimo_base_url=$(cat "$url_file")
|
||||
|
||||
# _mimo_check_key: 1-токенный запрос с ключом $1. Без известного эндпоинта перебирает
|
||||
# регионы: чужой регион отвечает 401/403, первый другой ответ задаёт mimo_base_url.
|
||||
# Недоступный эндпоинт (код 000*: _claude_test_api при сетевой ошибке отдаёт 000000)
|
||||
# пропускаем. token-plan-cn в переборе нет: с наших машин он не отвечает (таймаут 15 с),
|
||||
# для него MIMO_BASE_URL=https://token-plan-cn.xiaomimimo.com/anthropic.
|
||||
# Bearer, как у Claude Code с ANTHROPIC_AUTH_TOKEN.
|
||||
_mimo_check_key() {
|
||||
local url auth_code="" auth_body=""
|
||||
if [ -n "$mimo_base_url" ]; then
|
||||
_claude_test_api "$mimo_base_url/v1/messages" "Authorization: Bearer $1" "mimo-v2.6-flash"
|
||||
return
|
||||
fi
|
||||
for url in https://token-plan-sgp.xiaomimimo.com/anthropic \
|
||||
https://token-plan-ams.xiaomimimo.com/anthropic \
|
||||
https://api.xiaomimimo.com/anthropic; do
|
||||
_claude_test_api "$url/v1/messages" "Authorization: Bearer $1" "mimo-v2.6-flash"
|
||||
case "$_CLAUDE_TEST_CODE" in
|
||||
401|403) auth_code="$_CLAUDE_TEST_CODE"; auth_body="$_CLAUDE_TEST_BODY" ;;
|
||||
000*) ;;
|
||||
*) mimo_base_url="$url"; return ;;
|
||||
esac
|
||||
done
|
||||
# Ни один эндпоинт не принял ключ. Отказ в авторизации разберёт _handle_api_response,
|
||||
# а без сети эндпоинт не узнать и запускать Claude Code некуда.
|
||||
if [ -n "$auth_code" ]; then
|
||||
_CLAUDE_TEST_CODE="$auth_code"
|
||||
_CLAUDE_TEST_BODY="$auth_body"
|
||||
return
|
||||
fi
|
||||
echo ""
|
||||
echo -e "\033[0;31m[СЕТЬ]\033[0m Эндпоинты MiMo недоступны, ключ не проверен."
|
||||
echo " Задай эндпоинт явно: MIMO_BASE_URL=https://token-plan-sgp.xiaomimimo.com/anthropic ai-mimo"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# _mimo_check_result: разбор ответа проверки. 402 на pay-as-you-go эндпоинте значит ключ
|
||||
# sk-... с нулевым балансом: подписку Token Plan он не тратит, для неё свой ключ tp-...
|
||||
_mimo_check_result() {
|
||||
_handle_api_response "MiMo" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Лимит Token Plan: https://platform.xiaomimimo.com"
|
||||
if [ "$_API_RET" = "402" ] && [ "$mimo_base_url" = "https://api.xiaomimimo.com/anthropic" ]; then
|
||||
echo " Это ключ pay-as-you-go (sk-...), подписку Token Plan он не использует."
|
||||
echo " Для Token Plan создай ключ на странице Token Plan: Dedicated API Key -> Create API Key (tp-...)."
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -n "$api_key" ]; then
|
||||
echo -n "Проверка сохранённого MiMo ключа... "
|
||||
_mimo_check_key "$api_key"
|
||||
_mimo_check_result
|
||||
ret=$_API_RET
|
||||
if [ $ret -eq 401 ]; then
|
||||
rm -f "$key_file" "$url_file"
|
||||
api_key=""
|
||||
mimo_base_url="${MIMO_BASE_URL:-}"
|
||||
reauth=1
|
||||
elif [ $ret -eq 429 ]; then
|
||||
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
|
||||
read -r _ans
|
||||
case "${_ans:-N}" in
|
||||
[Yy]*) ;;
|
||||
*)
|
||||
echo -n "Хотите ввести новый MiMo ключ? [Y/n] "
|
||||
read -r _ans2
|
||||
case "${_ans2:-Y}" in
|
||||
[Yy]*)
|
||||
rm -f "$key_file" "$url_file"
|
||||
api_key=""
|
||||
mimo_base_url="${MIMO_BASE_URL:-}"
|
||||
reauth=1
|
||||
;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
elif [ $ret -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$api_key" ] && [ "$reauth" -eq 1 ]; then
|
||||
echo -n "Хотите ввести новый MiMo ключ? [Y/n] "
|
||||
read -r _ans; case "${_ans:-Y}" in [Yy]*) ;; *) exit 1 ;; esac
|
||||
fi
|
||||
|
||||
if [ -z "$api_key" ]; then
|
||||
echo "Получить ключ: https://platform.xiaomimimo.com (Token Plan - tp-..., pay-as-you-go - sk-...)"
|
||||
read -r -p "Введите ваш Xiaomi MiMo API ключ: " api_key
|
||||
[ -z "$api_key" ] && { echo "Выход."; exit 1; }
|
||||
|
||||
echo -n "Проверяю ключ... "
|
||||
_mimo_check_key "$api_key"
|
||||
_mimo_check_result
|
||||
ret=$_API_RET
|
||||
if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then
|
||||
mkdir -p "$(dirname "$key_file")"
|
||||
echo "$api_key" > "$key_file"
|
||||
echo "$mimo_base_url" > "$url_file"
|
||||
chmod 600 "$key_file" "$url_file"
|
||||
echo "Ключ сохранён (эндпоинт: $mimo_base_url)."
|
||||
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
|
||||
|
||||
_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=mimo
|
||||
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/mimo"
|
||||
# Модель: всегда самая умная MiMo - mimo-v2.6-pro во всех трёх слотах и в сабагентах, как
|
||||
# у ai-glm. Flash не используем: ai-mimo задуман как максимум качества, а не экономия
|
||||
# кредитов. pro-ultraspeed - тот же Pro, только быстрее и в ~10 раз дороже.
|
||||
# mimo-v2.5-pro/mimo-v2.5 сняты 21.10.2026 (с 14.10 их роутят на v2.6-pro/v2.6-flash):
|
||||
# https://mimo.mi.com/docs/en-US/updates/deprecate
|
||||
# availableModels НЕ задаём (он схлопывает пикер). --model opus при каждом запуске: выбор
|
||||
# через /model живёт до конца сессии, следующий запуск снова на Pro.
|
||||
# CLAUDE_CODE_MAX_CONTEXT_TOKENS: mimo-* нет в реестре моделей Claude Code, без env окно
|
||||
# считалось бы = 200K (ctx: врал бы в ~5 раз, автокомпакт жал бы сессию). У Pro реальное
|
||||
# окно 1M, поэтому CLAUDE_CODE_DISABLE_1M_CONTEXT не выставляем.
|
||||
# CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000: потолок Claude Code (131072 он режет до 128000),
|
||||
# MiMo V2.6 отдаёт до 131072. По умолчанию незнакомой модели досталось бы 32000 на
|
||||
# размышления + ответ.
|
||||
# Размышления: MiMo думает всегда. Claude Code шлёт thinking adaptive, а MiMo отрабатывает
|
||||
# его так же, как enabled (замер 13.10: даже на "скажи привет" есть thinking, длина та же).
|
||||
# Effort MiMo игнорирует: Claude Code шлёт output_config.effort (и без
|
||||
# CLAUDE_CODE_ALWAYS_ENABLE_EFFORT), MiMo принимает даже effort=bogus, а длина размышлений
|
||||
# от low до max не меняется (см. EFFORT_MAPPING.md). Стартуем на max через CLI --effort,
|
||||
# как ai-eltex-qwen: если Xiaomi начнёт его учитывать, будет максимум.
|
||||
_setup_isolated_config mimo opus high ''
|
||||
# /logout-mimo: без зарегистрированной команды Claude Code отвечает "Unknown command" и
|
||||
# не зовёт UserPromptSubmit-хук (см. ai-eltex).
|
||||
mkdir -p "$CLAUDE_CONFIG_DIR/commands"
|
||||
cat > "$CLAUDE_CONFIG_DIR/commands/logout-mimo.md" <<'CMDEOF'
|
||||
---
|
||||
description: Удалить сохранённый ключ ai-mimo (обрабатывает хук, без LLM)
|
||||
---
|
||||
Хук не сработал: удали ~/.config/ai-setup/mimo_key вручную и перезапусти ai-mimo (или запусти AI_RESET_KEY=1 ai-mimo).
|
||||
CMDEOF
|
||||
ANTHROPIC_BASE_URL="$mimo_base_url" \
|
||||
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL=mimo-v2.6-pro \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="MiMo V2.6 Pro" \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="Xiaomi MiMo V2.6 Pro - флагман для сложных задач" \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL=mimo-v2.6-pro \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="MiMo V2.6 Pro" \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="Xiaomi MiMo V2.6 Pro - флагман для сложных задач" \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL=mimo-v2.6-pro \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="MiMo V2.6 Pro" \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="Xiaomi MiMo V2.6 Pro - флагман для сложных задач" \
|
||||
CLAUDE_CODE_SUBAGENT_MODEL=mimo-v2.6-pro \
|
||||
CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000 \
|
||||
CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000 \
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||||
claude --dangerously-skip-permissions --model opus --effort "${AI_EFFORT:-max}" --system-prompt-file "$_PROMPT_FILE" "$@"
|
||||
MIMOEOF
|
||||
chmod +x "$BIN_DIR/ai-mimo"
|
||||
|
||||
# === ai-qwen ===
|
||||
cat > "$BIN_DIR/ai-qwen" << 'QWENEOF'
|
||||
#!/usr/bin/env bash
|
||||
# ai-qwen - запуск Claude Code через Alibaba Cloud Model Studio (Qwen 3.8, Token Plan,
|
||||
# Anthropic-compatible API). Ключ общий и вшит в скрипт: запускается сразу, без ввода ключа.
|
||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||||
|
||||
qwen_base_url="https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic"
|
||||
api_key="sk-sp-H.LEDIL.UJfr.MEUCIBmN5Rtjyn9iKPnhw2sym_1auFL4YZ4tnJh54TurNMo8AiEA1SbkS2Gt872ncq-32xK2ZxlpZxS3ldNHnTgX6dNCIok"
|
||||
|
||||
_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=qwen
|
||||
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/qwen"
|
||||
# Модели (список ключа: .../compatible-mode/v1/models): qwen3.8-max - самая умная, она в
|
||||
# opus/sonnet и в сабагентах; haiku = qwen3.8-flash (быстрая, Claude Code берёт слот haiku
|
||||
# и для фоновых задач вроде заголовков - так дешевле для лимита Token Plan).
|
||||
# availableModels НЕ задаём (он схлопывает пикер). --model opus при каждом запуске, как в ai-mimo.
|
||||
# Окно: у обеих моделей вход до 983616 токенов (замер 20.10: ответ "Range of input length
|
||||
# should be [1, 983616]"). qwen3.8-* нет в реестре Claude Code, без
|
||||
# CLAUDE_CODE_MAX_CONTEXT_TOKENS окно считалось бы = 200K. Автокомпакт с окном 1000000
|
||||
# срабатывает раньше предела API. Картинки обе модели видят сами.
|
||||
# Effort Qwen проверяет (bogus -> 400, low/medium/high/xhigh/max принимаются), но у Max тренда
|
||||
# нет, у Flash max размышляет почти вдвое дольше low (см. EFFORT_MAPPING.md). Стартуем на max
|
||||
# через CLI --effort, как ai-mimo. CLAUDE_CODE_ALWAYS_ENABLE_EFFORT - страховка, как в ai-glm.
|
||||
# CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 вместо дефолтных 32000, как у ai-glm.
|
||||
_setup_isolated_config qwen opus high ''
|
||||
ANTHROPIC_BASE_URL="$qwen_base_url" \
|
||||
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL=qwen3.8-max \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="Qwen3.8 Max" \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="Qwen3.8 Max - флагман Alibaba для сложных задач" \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL=qwen3.8-max \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="Qwen3.8 Max" \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="Qwen3.8 Max - флагман Alibaba для сложных задач" \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL=qwen3.8-flash \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="Qwen3.8 Flash" \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="Qwen3.8 Flash - быстрая" \
|
||||
CLAUDE_CODE_SUBAGENT_MODEL=qwen3.8-max \
|
||||
CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000 \
|
||||
CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \
|
||||
CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1 \
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||||
claude --dangerously-skip-permissions --model opus --effort "${AI_EFFORT:-max}" --system-prompt-file "$_PROMPT_FILE" "$@"
|
||||
QWENEOF
|
||||
chmod +x "$BIN_DIR/ai-qwen"
|
||||
|
||||
# === ai-openrouter ===
|
||||
cat > "$BIN_DIR/ai-openrouter" << 'OPENROUTEREOF'
|
||||
#!/usr/bin/env bash
|
||||
@@ -2510,11 +1975,7 @@ chmod +x "$BIN_DIR/ai-opencode"
|
||||
source "$BIN_DIR/ai-api-helpers.sh"
|
||||
_setup_isolated_config deepseek opus high ''
|
||||
_setup_isolated_config kimi opus high ''
|
||||
_setup_isolated_config glm opus xhigh ''
|
||||
_setup_isolated_config eltex opus xhigh ''
|
||||
_setup_isolated_config eltex-qwen opus high ''
|
||||
_setup_isolated_config mimo opus high ''
|
||||
_setup_isolated_config qwen opus high ''
|
||||
_setup_isolated_config glm opus high ''
|
||||
_setup_isolated_config openrouter "openai/gpt-5.5" high ''
|
||||
_setup_isolated_config opencode deepseek-v4-pro high ''
|
||||
|
||||
@@ -2525,7 +1986,7 @@ rm -f "$BIN_DIR/claude_gpt" "$BIN_DIR/claude_deepseek" "$BIN_DIR/claude_kimi" "$
|
||||
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-glm" "$HOME/.local/bin/ai-eltex" "$HOME/.local/bin/ai-eltex-qwen" "$HOME/.local/bin/ai-mimo" "$HOME/.local/bin/ai-qwen" "$HOME/.local/bin/ai-openrouter" "$HOME/.local/bin/ai-gemini" "$HOME/.local/bin/ai-cursor" "$HOME/.local/bin/ai-claude" \
|
||||
"$HOME/.local/bin/ai-glm" "$HOME/.local/bin/ai-openrouter" "$HOME/.local/bin/ai-gemini" "$HOME/.local/bin/ai-cursor" "$HOME/.local/bin/ai-claude" \
|
||||
"$HOME/.local/bin/ai-opencode" \
|
||||
"$HOME/.local/bin/ai-api-helpers.sh" "$HOME/.local/bin/claude-gpt-effort-proxy.py"
|
||||
fi
|
||||
@@ -2544,11 +2005,7 @@ 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-glm${NC} - GLM 5.3 (через Claude Code, Zhipu AI API)"
|
||||
echo -e " ${CYAN}ai-eltex${NC} - Корпоративный LLM-прокси Eltex (через Claude Code, ключ от админов)"
|
||||
echo -e " ${CYAN}ai-eltex-qwen${NC} - Корпоративный LLM-прокси Eltex, модели Qwen (через Claude Code, свой ключ)"
|
||||
echo -e " ${CYAN}ai-mimo${NC} - Xiaomi MiMo V2.6 Pro, максимум рассуждений (через Claude Code, Token Plan)"
|
||||
echo -e " ${CYAN}ai-qwen${NC} - Qwen3.8 Max (через Claude Code, Alibaba Token Plan, ключ встроен)"
|
||||
echo -e " ${CYAN}ai-glm${NC} - GLM 5.2 (через Claude Code, Zhipu AI API)"
|
||||
echo -e " ${CYAN}ai-openrouter${NC} - OpenRouter (через Claude Code: GPT-5.5, Grok, Qwen, GLM 5.2 и др.)"
|
||||
echo -e " ${CYAN}ai-opencode${NC} - OpenCode Go (через Claude Code + routatic-proxy: DeepSeek, Kimi, Qwen, GLM, MiniMax, MiMo)"
|
||||
echo ""
|
||||
@@ -2557,7 +2014,7 @@ echo -e " ${CYAN}ai-gpt${NC} - OpenAI Codex (нативный CLI
|
||||
echo -e " ${CYAN}ai-gemini${NC} - Gemini (нативный agy CLI, автоустановка)"
|
||||
echo -e " ${CYAN}ai-cursor${NC} - Cursor (нативный Cursor CLI, автоустановка)"
|
||||
echo ""
|
||||
echo " Android CLI (не ставится по умолчанию):"
|
||||
echo -e " ${CYAN}android${NC} - Google Android CLI: bash setup.sh → пункт 8"
|
||||
echo " Android CLI:"
|
||||
echo -e " ${CYAN}android${NC} - Google Android CLI (SDK, эмулятор, проекты)"
|
||||
echo ""
|
||||
echo -e "Чтобы команды были доступны сразу, выполните: ${GREEN}exec bash${NC}"
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (c) 2026 Виталий Никитенко. MIT License.
|
||||
# android-cli-setup.sh — установка Google Android CLI (по выбору, пункт 8 в setup.sh)
|
||||
# Запуск: bash scripts/android-cli-setup.sh
|
||||
|
||||
# Автоопределение: ~/bin если есть в PATH, иначе ~/.local/bin
|
||||
if [[ ":$PATH:" == *":$HOME/bin:"* ]]; then
|
||||
BIN_DIR="$HOME/bin"
|
||||
else
|
||||
BIN_DIR="$HOME/.local/bin"
|
||||
fi
|
||||
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
|
||||
success() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
|
||||
# Запрет запуска от root
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
echo -e "${RED}Не запускайте этот скрипт через sudo!${NC}"
|
||||
echo "Запустите просто: bash scripts/android-cli-setup.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "Проверяю Android CLI..."
|
||||
ANDROID_BIN="$HOME/.local/bin/android"
|
||||
[ ! -f "$ANDROID_BIN" ] && ANDROID_BIN="$(command -v android 2>/dev/null || true)"
|
||||
|
||||
if [ -z "$ANDROID_BIN" ] || [ ! -f "$ANDROID_BIN" ]; then
|
||||
info "Устанавливаю Android CLI..."
|
||||
_uname_s=$(uname -s)
|
||||
_uname_m=$(uname -m)
|
||||
ANDROID_PLATFORM=""
|
||||
case "${_uname_s}-${_uname_m}" in
|
||||
Linux-x86_64) ANDROID_PLATFORM="linux_x86_64" ;;
|
||||
Linux-aarch64) ANDROID_PLATFORM="linux_arm64" ;;
|
||||
Linux-arm64) ANDROID_PLATFORM="linux_arm64" ;;
|
||||
Darwin-x86_64) ANDROID_PLATFORM="darwin_x86_64" ;;
|
||||
Darwin-arm64) ANDROID_PLATFORM="darwin_arm64" ;;
|
||||
Darwin-aarch64) ANDROID_PLATFORM="darwin_arm64" ;;
|
||||
MINGW*|MSYS*|CYGWIN*-x86_64) ANDROID_PLATFORM="windows_x86_64" ;;
|
||||
esac
|
||||
|
||||
if [ -n "$ANDROID_PLATFORM" ]; then
|
||||
ANDROID_INSTALL_URL="https://dl.google.com/android/cli/latest/${ANDROID_PLATFORM}/install.sh"
|
||||
if curl -fsSL "$ANDROID_INSTALL_URL" | bash; then
|
||||
success "Android CLI установлен"
|
||||
else
|
||||
warn "Не удалось установить Android CLI через официальный скрипт"
|
||||
fi
|
||||
else
|
||||
warn "Неизвестная платформа ${_uname_s} ${_uname_m}. Пропускаю Android CLI."
|
||||
fi
|
||||
else
|
||||
success "Android CLI уже установлен: $($ANDROID_BIN --version 2>/dev/null | head -1)"
|
||||
fi
|
||||
|
||||
# Официальный install.sh ставит бинарь в ~/.local/bin/android.
|
||||
# Если пользователь использует ~/bin вместо ~/.local/bin - symlink.
|
||||
if [ "$BIN_DIR" != "$HOME/.local/bin" ] && [ -f "$HOME/.local/bin/android" ] && [ ! -f "$BIN_DIR/android" ]; then
|
||||
ln -sf "$HOME/.local/bin/android" "$BIN_DIR/android"
|
||||
success "Android CLI symlink -> $BIN_DIR/android"
|
||||
fi
|
||||
|
||||
# Разворачиваем Android CLI skills для агентов (Claude Code и др.)
|
||||
if command -v android &>/dev/null; then
|
||||
info "Запускаю android init для деплоя skills..."
|
||||
mkdir -p "$HOME/.claude/skills"
|
||||
android init || warn "android init завершился с ошибкой"
|
||||
else
|
||||
info "Android CLI не найден в PATH, пропускаю android init"
|
||||
fi
|
||||
@@ -162,12 +162,10 @@ fi
|
||||
# Без этого DNS через VPN может вернуть внешний IP вместо внутреннего,
|
||||
# и трафик пойдёт через VPN вместо прямого соединения.
|
||||
HOSTS_MARKER="# ru-bypass: local hosts"
|
||||
HOSTS_END_MARKER="# ru-bypass: end"
|
||||
|
||||
# Удаляем только свой блок (от маркера до конечного маркера).
|
||||
# Записи, добавленные вручную вне блока, не трогаем.
|
||||
# Блок старых версий без конечного маркера удаляется до конца файла.
|
||||
sed -i "/^$HOSTS_MARKER\$/,/^$HOSTS_END_MARKER\$/d" /etc/hosts
|
||||
# Удаляем старый блок целиком (от маркера до конца файла)
|
||||
# и заодно чистим дубликаты от старых версий скрипта (без маркера)
|
||||
sed -i "/$HOSTS_MARKER/,\$ d; /eltex\.loc/d; /eltex-co\.ru/d" /etc/hosts
|
||||
|
||||
# Добавляем актуальные
|
||||
cat >> /etc/hosts <<_HOSTS
|
||||
@@ -182,10 +180,8 @@ $HOSTS_MARKER
|
||||
172.16.5.63 proxy.eltex.loc
|
||||
10.80.0.16 ssw.eltex.loc
|
||||
172.16.5.78 nexus.eltex.loc
|
||||
172.16.4.68 llm.eltex.loc llm2.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 study-git.eltex-co.ru study-red.eltex-co.ru
|
||||
$HOSTS_END_MARKER
|
||||
_HOSTS
|
||||
|
||||
echo "Локальные хосты: *.eltex.loc, mattermost, elph → /etc/hosts"
|
||||
@@ -245,8 +241,7 @@ if [ -n "${ALL_EXC// }" ]; then
|
||||
if echo "$item" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"; then
|
||||
ips="$item"
|
||||
else
|
||||
ips=$(getent ahostsv4 "$item" 2>/dev/null | awk '{print $1}' | sort -u)
|
||||
[ -z "$ips" ] && ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u)
|
||||
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
|
||||
@@ -300,8 +295,7 @@ if [ -n "${ALL_EXC// }" ]; then
|
||||
if echo "$item" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"; then
|
||||
ips="$item"
|
||||
else
|
||||
ips=$(getent ahostsv4 "$item" 2>/dev/null | awk '{print $1}' | sort -u)
|
||||
[ -z "$ips" ] && ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u)
|
||||
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
|
||||
|
||||
45
setup.sh
45
setup.sh
@@ -18,34 +18,6 @@ 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"; }
|
||||
|
||||
# Обновление: git pull + перегенерация всех ai-* лаунчеров (пункт меню 7)
|
||||
do_update() {
|
||||
local repo_dir
|
||||
repo_dir="$(pwd)"
|
||||
echo -e "${BLD}Обновляем репозиторий...${CLR}"
|
||||
git -C "$repo_dir" pull --ff-only
|
||||
echo ""
|
||||
echo -e "${BLD}Перегенерация скриптов...${CLR}"
|
||||
bash "$repo_dir/scripts/ai-setup.sh"
|
||||
}
|
||||
|
||||
# CLI-режим: ./setup.sh update (или --update) — обновление без меню
|
||||
case "${1:-}" in
|
||||
update|--update)
|
||||
_log "setup" "update (CLI) на $(hostname)"
|
||||
do_update
|
||||
exit $?
|
||||
;;
|
||||
"")
|
||||
# без аргументов — интерактивное меню ниже
|
||||
;;
|
||||
*)
|
||||
echo "Неизвестный аргумент: $1"
|
||||
echo "Использование: ./setup.sh [update] — без аргументов открывается меню"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if command -v whiptail >/dev/null 2>&1; then
|
||||
MENU_ARGS=()
|
||||
MENU_ARGS+=("1" "AI-инструменты (установить лаунчеры + ключи)")
|
||||
@@ -107,7 +79,7 @@ else
|
||||
echo -e " ${GRY}git pull + перегенерация всех скриптов в ~/bin (или ~/.local/bin).${CLR}"
|
||||
echo ""
|
||||
echo -e " ${BLD}8) Android CLI${CLR}"
|
||||
echo -e " ${GRY}Установить Google Android CLI для Android-разработки (пункт 1 его не ставит).${CLR}"
|
||||
echo -e " ${GRY}Установить Google Android CLI для Android-разработки.${CLR}"
|
||||
echo ""
|
||||
echo -n "Выбери [1-8] или Enter для выхода: "
|
||||
read -r choice
|
||||
@@ -230,7 +202,7 @@ case "$choice" in
|
||||
fi
|
||||
|
||||
echo -e "${BLD}AI инструменты:${CLR}"
|
||||
for cmd in ai-claude ai-gpt ai-deepseek ai-kimi ai-glm ai-eltex ai-eltex-qwen ai-mimo ai-qwen ai-openrouter ai-opencode ai-gemini android; do
|
||||
for cmd in ai-claude ai-gpt ai-deepseek ai-kimi ai-glm ai-openrouter ai-opencode ai-gemini android; do
|
||||
if command -v "$cmd" &>/dev/null; then
|
||||
echo -e " ${GRN}✓${CLR} $cmd"
|
||||
else
|
||||
@@ -241,7 +213,7 @@ case "$choice" in
|
||||
echo ""
|
||||
echo -e "${BLD}API ключи:${CLR}"
|
||||
cfg="$HOME/.config/ai-setup"
|
||||
for f in deepseek_key kimi_key glm_key eltex_key eltex_qwen_key mimo_key openrouter_key opencode_key; do
|
||||
for f in deepseek_key kimi_key glm_key openrouter_key opencode_key; do
|
||||
name="${f/_key/}"
|
||||
if [ -s "$cfg/$f" ]; then
|
||||
echo -e " ${GRN}✓${CLR} $name"
|
||||
@@ -252,7 +224,7 @@ case "$choice" in
|
||||
|
||||
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" "GLM:api.z.ai" "MiMo:api.xiaomimimo.com" "Qwen:token-plan.ap-southeast-1.maas.aliyuncs.com" "OpenRouter:openrouter.ai" "OpenCode:opencode.ai"; do
|
||||
for entry in "Anthropic:api.anthropic.com" "DeepSeek:api.deepseek.com" "OpenAI:api.openai.com" "Kimi:api.kimi.com" "GLM:api.z.ai" "OpenRouter:openrouter.ai" "OpenCode:opencode.ai"; do
|
||||
_name="${entry%%:*}"
|
||||
_host="${entry##*:}"
|
||||
_ms=$(curl -s -o /dev/null -w "%{time_connect}" --max-time 5 "https://$_host" 2>/dev/null)
|
||||
@@ -276,10 +248,15 @@ case "$choice" in
|
||||
bash tests/test_network.sh
|
||||
;;
|
||||
7)
|
||||
do_update
|
||||
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"
|
||||
;;
|
||||
8)
|
||||
bash scripts/android-cli-setup.sh
|
||||
bash scripts/ai-setup.sh
|
||||
;;
|
||||
"")
|
||||
exit 0
|
||||
|
||||
@@ -8,8 +8,6 @@ set -euo pipefail
|
||||
SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/scripts/ai-setup.sh"
|
||||
GLOBAL_RULES_SOURCE="$(cd "$(dirname "$0")/.." && pwd)/home-configs/GLOBAL_RULES.md"
|
||||
ADD_HOOK="$(cd "$(dirname "$0")/.." && pwd)/home-configs/claude/hooks/add-account-hook.sh"
|
||||
ANDROID_SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/scripts/android-cli-setup.sh"
|
||||
SETUP_MENU="$(cd "$(dirname "$0")/.." && pwd)/setup.sh"
|
||||
PASS=0; FAIL=0
|
||||
|
||||
ok() { echo "[PASS] $1"; PASS=$((PASS+1)); }
|
||||
@@ -19,11 +17,6 @@ fail() { echo "[FAIL] $1"; FAIL=$((FAIL+1)); }
|
||||
GPT_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-gpt"/,/^GPTEOF/' "$SCRIPT")
|
||||
KIMI_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-kimi"/,/^KIMIEOF/' "$SCRIPT")
|
||||
GLM_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-glm"/,/^GLMEOF/' "$SCRIPT")
|
||||
ELTEX_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-eltex"/,/^ELTEXEOF/' "$SCRIPT")
|
||||
ELTEX_QWEN_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-eltex-qwen"/,/^ELTEXQWENEOF/' "$SCRIPT")
|
||||
MIMO_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-mimo"/,/^MIMOEOF/' "$SCRIPT")
|
||||
QWEN_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-qwen"/,/^QWENEOF/' "$SCRIPT")
|
||||
LOGOUT_HOOK="$(cd "$(dirname "$0")/.." && pwd)/home-configs/claude/hooks/logout-account-hook.sh"
|
||||
GEMINI_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-gemini"/,/^GEMINIEOF/' "$SCRIPT")
|
||||
OPENCODE_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-opencode"/,/^OPENCODEEOF/' "$SCRIPT")
|
||||
CURSOR_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-cursor"/,/^CURSOREOF/' "$SCRIPT")
|
||||
@@ -48,15 +41,6 @@ test_gpt_no_proxy() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ── ai-gpt: informative Codex status line ───────────────────────────────────
|
||||
test_gpt_status_line() {
|
||||
if echo "$GPT_SECTION" | grep -Fq 'tui.status_line=["model-with-reasoning","git-branch","context-used","five-hour-limit","current-dir"]'; then
|
||||
ok "ai-gpt: shows model, branch, context usage, primary limit, and directory"
|
||||
else
|
||||
fail "ai-gpt: missing configured Codex status line"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── ai-kimi: launches claude with Kimi backend ────────────────────────────
|
||||
test_kimi_claude_launcher() {
|
||||
if echo "$KIMI_SECTION" | grep -q 'claude --dangerously-skip-permissions' \
|
||||
@@ -101,384 +85,12 @@ test_glm_claude_launcher() {
|
||||
# ── ai-glm: uses official Zhipu AI API ────────────────────────────────────
|
||||
test_glm_official_api() {
|
||||
if echo "$GLM_SECTION" | grep -q 'api.z.ai/api/anthropic' \
|
||||
&& echo "$GLM_SECTION" | grep -q 'ANTHROPIC_DEFAULT_OPUS_MODEL=glm-5.3' \
|
||||
&& echo "$GLM_SECTION" | grep -q 'ANTHROPIC_DEFAULT_HAIKU_MODEL=glm-5.3' \
|
||||
&& echo "$GLM_SECTION" | grep -q 'CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000' \
|
||||
&& echo "$GLM_SECTION" | grep -q 'ANTHROPIC_DEFAULT_OPUS_MODEL=glm-5.2' \
|
||||
&& echo "$GLM_SECTION" | grep -q "ANTHROPIC_DEFAULT_HAIKU_MODEL='glm-5.2\[1m\]'" \
|
||||
&& echo "$GLM_SECTION" | grep -q 'x-api-key'; then
|
||||
ok "ai-glm: uses official Zhipu AI API (glm-5.3 everywhere, ctx window 1M via env)"
|
||||
ok "ai-glm: uses official Zhipu AI API (glm-5.2 opus/sonnet, glm-5.2[1m] haiku)"
|
||||
else
|
||||
fail "ai-glm: must use official Zhipu AI API with glm-5.3, 1M context window and x-api-key"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── ai-eltex: Claude Code через корпоративный прокси Eltex ─────────────────
|
||||
test_eltex_claude_launcher() {
|
||||
if echo "$ELTEX_SECTION" | grep -q 'claude --dangerously-skip-permissions' \
|
||||
&& echo "$ELTEX_SECTION" | grep -q 'ELTEX_BASE_URL:-http://llm.eltex.loc:4000' \
|
||||
&& echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_BASE_URL="$eltex_base_url"' \
|
||||
&& echo "$ELTEX_SECTION" | grep -q 'CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/eltex"' \
|
||||
&& echo "$ELTEX_SECTION" | grep -q 'key_file="$HOME/.config/ai-setup/eltex_key"'; then
|
||||
ok "ai-eltex: launches claude via llm.eltex.loc with its own key and config dir"
|
||||
else
|
||||
fail "ai-eltex: must launch claude via llm.eltex.loc with eltex_key and cfg/eltex"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── ai-eltex: модель берётся из /v1/models ключа ──────────────────────────
|
||||
test_eltex_model_from_key() {
|
||||
if echo "$ELTEX_SECTION" | grep -q '/v1/models' \
|
||||
&& echo "$ELTEX_SECTION" | grep -Fq 'model="${ELTEX_MODEL:-${model:-anth-glm-5.3}}"' \
|
||||
&& echo "$ELTEX_SECTION" | grep -Fq 'pick("anth-glm-5.3", "anth-glm-5.3-")' \
|
||||
&& ! echo "$ELTEX_SECTION" | grep -q 'ids\[0\]' \
|
||||
&& echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_DEFAULT_OPUS_MODEL="$opus_model"' \
|
||||
&& echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_DEFAULT_SONNET_MODEL="$opus_model"' \
|
||||
&& echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_DEFAULT_HAIKU_MODEL="$opus_model"'; then
|
||||
ok "ai-eltex: anth-glm-5.3 by default, never the first /v1/models entry (ELTEX_MODEL overrides)"
|
||||
else
|
||||
fail "ai-eltex: must default to anth-glm-5.3 (not the first /v1/models entry), ELTEX_MODEL overrides"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── ai-eltex: anth-glm-5.3 во всех слотах пикера (имена с суффиксом тоже) ──
|
||||
test_eltex_three_slots() {
|
||||
local tmp ok_run=1 out
|
||||
tmp=$(mktemp -d)
|
||||
mkdir -p "$tmp/bin" "$tmp/mock" "$tmp/home/.config/ai-setup"
|
||||
echo "$HELPERS_SECTION" | HELPERS_FILE="$tmp/bin/ai-api-helpers.sh" bash
|
||||
echo "$ELTEX_SECTION" | BIN_DIR="$tmp/bin" bash
|
||||
cat > "$tmp/mock/curl" <<'CURLEOF'
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n%s' "$MODELS_BODY" "200"
|
||||
CURLEOF
|
||||
cat > "$tmp/mock/claude" <<'CLAUDEEOF'
|
||||
#!/usr/bin/env bash
|
||||
echo "SLOTS opus=$ANTHROPIC_DEFAULT_OPUS_MODEL sonnet=$ANTHROPIC_DEFAULT_SONNET_MODEL haiku=$ANTHROPIC_DEFAULT_HAIKU_MODEL sub=$CLAUDE_CODE_SUBAGENT_MODEL"
|
||||
CLAUDEEOF
|
||||
chmod +x "$tmp/mock/curl" "$tmp/mock/claude"
|
||||
echo sk-test > "$tmp/home/.config/ai-setup/eltex_key"
|
||||
# Как на прокси 21.10: zai-модели идут первыми, плюс имя группы доступа
|
||||
out=$(HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
|
||||
MODELS_BODY='{"data":[{"id":"zai-glm-5.3-flash"},{"id":"anth-glm-5.3"},{"id":"zai-glm-5.3"},{"id":"ZAI-Test-Group"}]}' \
|
||||
bash "$tmp/bin/ai-eltex" 2>&1)
|
||||
echo "$out" | grep -Fq 'SLOTS opus=anth-glm-5.3[1m] sonnet=anth-glm-5.3[1m] haiku=anth-glm-5.3[1m] sub=anth-glm-5.3[1m]' || ok_run=0
|
||||
# Имя с суффиксом
|
||||
out=$(HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
|
||||
MODELS_BODY='{"data":[{"id":"zai-glm-5.3-2"},{"id":"anth-glm-5.3-2"}]}' \
|
||||
bash "$tmp/bin/ai-eltex" 2>&1)
|
||||
echo "$out" | grep -Fq 'SLOTS opus=anth-glm-5.3-2[1m] sonnet=anth-glm-5.3-2[1m] haiku=anth-glm-5.3-2[1m]' || ok_run=0
|
||||
if [ "$ok_run" -eq 1 ]; then
|
||||
ok "ai-eltex: anth-glm-5.3[1m] in every picker slot, zai/flash never picked"
|
||||
else
|
||||
fail "ai-eltex: every picker slot must be anth-glm-5.3[1m], zai/flash must never be picked"
|
||||
fi
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
|
||||
# ── ai-eltex: нет anth-glm-5.3 у ключа -> авто-логаут, свежий ключ не сохраняется ──
|
||||
test_eltex_autologout_on_missing_model() {
|
||||
local tmp ok_run=1 rc=0 out
|
||||
tmp=$(mktemp -d)
|
||||
mkdir -p "$tmp/bin" "$tmp/mock" "$tmp/home/.config/ai-setup"
|
||||
echo "$HELPERS_SECTION" | HELPERS_FILE="$tmp/bin/ai-api-helpers.sh" bash
|
||||
echo "$ELTEX_SECTION" | BIN_DIR="$tmp/bin" bash
|
||||
# Прокси отвечает 200, но anth-glm-5.3 в списке моделей ключа нет
|
||||
cat > "$tmp/mock/curl" <<'CURLEOF'
|
||||
#!/usr/bin/env bash
|
||||
if [[ "$*" == *"/v1/models"* ]]; then
|
||||
printf '%s\n%s' "$MODELS_BODY" "200"
|
||||
else
|
||||
printf '%s\n%s' '{"ok":true}' "200"
|
||||
fi
|
||||
CURLEOF
|
||||
printf '#!/usr/bin/env bash\nexit 0\n' > "$tmp/mock/claude"
|
||||
chmod +x "$tmp/mock/curl" "$tmp/mock/claude"
|
||||
# Сохранённый ключ: авто-логаут (файл удалён), новый ключ не вводим -> выход 1
|
||||
echo sk-old > "$tmp/home/.config/ai-setup/eltex_key"
|
||||
out=$(printf 'n\n' | HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
|
||||
MODELS_BODY='{"data":[{"id":"zai-glm-5.3-flash-2"},{"id":"ZAI-Test-Group"}]}' \
|
||||
bash "$tmp/bin/ai-eltex" 2>&1) || rc=$?
|
||||
[ "$rc" -eq 1 ] || ok_run=0
|
||||
echo "$out" | grep -q 'Ключу недоступна модель anth-glm-5.3' || ok_run=0
|
||||
echo "$out" | grep -q 'авто-логаут' || ok_run=0
|
||||
[ ! -f "$tmp/home/.config/ai-setup/eltex_key" ] || ok_run=0
|
||||
# Свежий ключ без anth-glm-5.3 не сохраняется вовсе
|
||||
rc=0
|
||||
out=$(printf 'sk-new\n' | HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
|
||||
MODELS_BODY='{"data":[{"id":"zai-glm-5.3-flash-2"}]}' \
|
||||
bash "$tmp/bin/ai-eltex" 2>&1) || rc=$?
|
||||
[ "$rc" -eq 1 ] || ok_run=0
|
||||
echo "$out" | grep -q 'Ключ НЕ сохранён' || ok_run=0
|
||||
[ ! -f "$tmp/home/.config/ai-setup/eltex_key" ] || ok_run=0
|
||||
# ELTEX_MODEL перебивает проверку: ключ живёт, запуск идёт
|
||||
echo sk-keep > "$tmp/home/.config/ai-setup/eltex_key"
|
||||
rc=0
|
||||
out=$(HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
|
||||
MODELS_BODY='{"data":[{"id":"zai-glm-5.3-flash-2"}]}' \
|
||||
ELTEX_MODEL=anth-glm-5.3-2 bash "$tmp/bin/ai-eltex" 2>&1) || rc=$?
|
||||
[ "$rc" -eq 0 ] || ok_run=0
|
||||
echo "$out" | grep -q 'Модель: anth-glm-5.3-2' || ok_run=0
|
||||
[ "$(cat "$tmp/home/.config/ai-setup/eltex_key")" = "sk-keep" ] || ok_run=0
|
||||
if [ "$ok_run" -eq 1 ]; then
|
||||
ok "ai-eltex: missing anth-glm-5.3 auto-logouts saved key, fresh key not saved, ELTEX_MODEL bypasses"
|
||||
else
|
||||
fail "ai-eltex: missing anth-glm-5.3 must auto-logout (saved key removed, fresh key not saved, ELTEX_MODEL bypass)"
|
||||
fi
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
|
||||
# ── ai-eltex: без ENABLE_TOOL_SEARCH (на anthropic-пути GLM теряет Read/Bash) ──
|
||||
test_eltex_no_tool_search() {
|
||||
if echo "$ELTEX_SECTION" | grep -q '^ENABLE_TOOL_SEARCH='; then
|
||||
fail "ai-eltex: must not force ENABLE_TOOL_SEARCH (GLM loses core tools on anthropic path)"
|
||||
else
|
||||
ok "ai-eltex: ENABLE_TOOL_SEARCH not forced"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── ai-eltex: окно 1M, как у ai-glm (иначе ctx: завышен в ~5 раз) ─────────
|
||||
test_eltex_context_window() {
|
||||
if echo "$ELTEX_SECTION" | grep -Fq 'anth-glm-5.3*) opus_model="${model}[1m]"' \
|
||||
&& ! echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_MAX_CONTEXT_TOKENS=' \
|
||||
&& ! echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_DISABLE_1M_CONTEXT=' \
|
||||
&& ! echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_AUTO_COMPACT_WINDOW='; then
|
||||
ok "ai-eltex: 1M window for anth-glm-5.3 via [1m], no global MAX_CONTEXT / DISABLE_1M"
|
||||
else
|
||||
fail "ai-eltex: 1M for anth-glm-5.3 via [1m], no global CLAUDE_CODE_MAX_CONTEXT_TOKENS / DISABLE_1M / AUTO_COMPACT_WINDOW"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── ai-eltex-qwen: свой ключ и конфиг, модели Qwen, окно 200K ─────────────
|
||||
test_eltex_qwen_launcher() {
|
||||
if echo "$ELTEX_QWEN_SECTION" | grep -q 'claude --dangerously-skip-permissions' \
|
||||
&& echo "$ELTEX_QWEN_SECTION" | grep -q 'key_file="$HOME/.config/ai-setup/eltex_qwen_key"' \
|
||||
&& echo "$ELTEX_QWEN_SECTION" | grep -q 'CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/eltex-qwen"' \
|
||||
&& echo "$ELTEX_QWEN_SECTION" | grep -q 'model="${ELTEX_QWEN_MODEL:-}"' \
|
||||
&& echo "$ELTEX_QWEN_SECTION" | grep -q '("Eltex-Coder-Senior", "Eltex-Qwen3_8-Flash-Next")' \
|
||||
&& echo "$ELTEX_QWEN_SECTION" | grep -q 'api_key="${AI_ELTEX_LLM_API_KEY:-}"' \
|
||||
&& echo "$ELTEX_QWEN_SECTION" | grep -q '^CLAUDE_CODE_DISABLE_1M_CONTEXT=1'; then
|
||||
ok "ai-eltex-qwen: own key/config, Coder-Senior then Flash-Next, 200K window"
|
||||
else
|
||||
fail "ai-eltex-qwen: must use eltex_qwen_key, cfg/eltex-qwen, Qwen model preference and DISABLE_1M"
|
||||
fi
|
||||
}
|
||||
|
||||
test_logout_eltex_qwen() {
|
||||
local tmp_home rc=0
|
||||
tmp_home=$(mktemp -d)
|
||||
mkdir -p "$tmp_home/.config/ai-setup"
|
||||
echo "sk-test" > "$tmp_home/.config/ai-setup/eltex_qwen_key"
|
||||
echo "sk-glm" > "$tmp_home/.config/ai-setup/eltex_key"
|
||||
echo '{"prompt":"/logout"}' | HOME="$tmp_home" AI_LAUNCHER=eltex-qwen bash "$LOGOUT_HOOK" 2>/dev/null || rc=$?
|
||||
if [ "$rc" -eq 2 ] && [ ! -f "$tmp_home/.config/ai-setup/eltex_qwen_key" ] && [ -f "$tmp_home/.config/ai-setup/eltex_key" ]; then
|
||||
ok "logout hook: /logout in ai-eltex-qwen removes only eltex_qwen_key"
|
||||
else
|
||||
fail "logout hook: /logout in ai-eltex-qwen must remove eltex_qwen_key and keep eltex_key (rc=$rc)"
|
||||
fi
|
||||
rm -rf "$tmp_home"
|
||||
}
|
||||
|
||||
# ── ai-eltex / ai-eltex-qwen: effort max по умолчанию через CLI, вывод 64000 ────
|
||||
test_eltex_effort() {
|
||||
if echo "$ELTEX_SECTION" | grep -q -- '--effort "${AI_EFFORT:-max}"' \
|
||||
&& echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1' \
|
||||
&& echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000' \
|
||||
&& ! echo "$ELTEX_SECTION" | grep -q '^_apply_effort'; then
|
||||
ok "ai-eltex: starts with --effort max by default, AI_EFFORT via CLI, 64000 output tokens"
|
||||
else
|
||||
fail "ai-eltex: must start with --effort \"\${AI_EFFORT:-max}\", CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1, MAX_OUTPUT_TOKENS=64000, no _apply_effort"
|
||||
fi
|
||||
}
|
||||
|
||||
test_eltex_qwen_effort() {
|
||||
if echo "$ELTEX_QWEN_SECTION" | grep -q -- '--effort "${AI_EFFORT:-max}"' \
|
||||
&& echo "$ELTEX_QWEN_SECTION" | grep -q '^CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000' \
|
||||
&& ! echo "$ELTEX_QWEN_SECTION" | grep -q '^_apply_effort'; then
|
||||
ok "ai-eltex-qwen: starts with --effort max by default, 64000 output tokens"
|
||||
else
|
||||
fail "ai-eltex-qwen: must start with --effort \"\${AI_EFFORT:-max}\" and MAX_OUTPUT_TOKENS=64000, no _apply_effort"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── ai-eltex: команда /logout-eltex зарегистрирована (иначе Unknown command, хук не зовётся) ──
|
||||
test_eltex_logout_command() {
|
||||
if echo "$ELTEX_SECTION" | grep -q 'cat > "$CLAUDE_CONFIG_DIR/commands/logout-eltex.md"'; then
|
||||
ok "ai-eltex: registers /logout-eltex command so the logout hook receives it"
|
||||
else
|
||||
fail "ai-eltex: must create \$CLAUDE_CONFIG_DIR/commands/logout-eltex.md"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── logout hook: /logout-eltex удаляет eltex_key ──────────────────────────
|
||||
test_logout_eltex() {
|
||||
local tmp_home rc=0
|
||||
tmp_home=$(mktemp -d)
|
||||
mkdir -p "$tmp_home/.config/ai-setup"
|
||||
echo "sk-test" > "$tmp_home/.config/ai-setup/eltex_key"
|
||||
echo '{"prompt":"/logout"}' | HOME="$tmp_home" AI_LAUNCHER=eltex bash "$LOGOUT_HOOK" 2>/dev/null || rc=$?
|
||||
if [ "$rc" -eq 2 ] && [ ! -f "$tmp_home/.config/ai-setup/eltex_key" ]; then
|
||||
ok "logout hook: /logout in ai-eltex removes eltex_key"
|
||||
else
|
||||
fail "logout hook: /logout in ai-eltex must remove eltex_key (rc=$rc)"
|
||||
fi
|
||||
rm -rf "$tmp_home"
|
||||
}
|
||||
|
||||
# ── ai-mimo: Claude Code через Xiaomi MiMo V2.6 ───────────────────────────
|
||||
test_mimo_launcher() {
|
||||
if echo "$MIMO_SECTION" | grep -q 'claude --dangerously-skip-permissions' \
|
||||
&& echo "$MIMO_SECTION" | grep -q 'key_file="$HOME/.config/ai-setup/mimo_key"' \
|
||||
&& echo "$MIMO_SECTION" | grep -q 'CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/mimo"' \
|
||||
&& echo "$MIMO_SECTION" | grep -q '^ANTHROPIC_BASE_URL="$mimo_base_url"' \
|
||||
&& echo "$MIMO_SECTION" | grep -q '^ANTHROPIC_DEFAULT_OPUS_MODEL=mimo-v2.6-pro' \
|
||||
&& echo "$MIMO_SECTION" | grep -q '^ANTHROPIC_DEFAULT_SONNET_MODEL=mimo-v2.6-pro' \
|
||||
&& echo "$MIMO_SECTION" | grep -q '^ANTHROPIC_DEFAULT_HAIKU_MODEL=mimo-v2.6-pro' \
|
||||
&& echo "$MIMO_SECTION" | grep -q '^CLAUDE_CODE_SUBAGENT_MODEL=mimo-v2.6-pro' \
|
||||
&& echo "$MIMO_SECTION" | grep -q -- 'claude --dangerously-skip-permissions --model opus ' \
|
||||
&& echo "$MIMO_SECTION" | grep -q '^CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000' \
|
||||
&& echo "$MIMO_SECTION" | grep -q '^CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000' \
|
||||
&& ! echo "$MIMO_SECTION" | grep -q '^CLAUDE_CODE_DISABLE_1M_CONTEXT=' \
|
||||
&& echo "$MIMO_SECTION" | grep -q 'cat > "$CLAUDE_CONFIG_DIR/commands/logout-mimo.md"'; then
|
||||
ok "ai-mimo: own key/config, Pro in every slot and subagents, --model opus, 128000 output, 1M window, /logout-mimo"
|
||||
else
|
||||
fail "ai-mimo: must use mimo_key, cfg/mimo, mimo-v2.6-pro everywhere, --model opus, 128000 output, 1M window and logout-mimo.md"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── ai-mimo: эндпоинт региона Token Plan находится перебором и запоминается ──
|
||||
test_mimo_region_probe() {
|
||||
local tmp ok_run=1
|
||||
tmp=$(mktemp -d)
|
||||
mkdir -p "$tmp/bin" "$tmp/mock" "$tmp/home"
|
||||
echo "$HELPERS_SECTION" | HELPERS_FILE="$tmp/bin/ai-api-helpers.sh" bash
|
||||
echo "$MIMO_SECTION" | BIN_DIR="$tmp/bin" bash
|
||||
# Ключ подходит только к региону ams, sgp недоступен по сети (как curl: -w печатает
|
||||
# 000 и exit 28), остальные эндпоинты отвечают 401. MOCK_DOWN=1 - сети нет совсем.
|
||||
cat > "$tmp/mock/curl" <<CURLEOF
|
||||
#!/usr/bin/env bash
|
||||
for a in "\$@"; do
|
||||
case "\$a" in
|
||||
https://*) echo "\$a" >> "$tmp/curl.log" ;;
|
||||
esac
|
||||
case "\$a" in
|
||||
https://*) [ -n "\${MOCK_DOWN:-}" ] && { printf '\n000'; exit 28; } ;;
|
||||
esac
|
||||
case "\$a" in
|
||||
https://api.xiaomimimo.com/*) [ -n "\${MOCK_SK:-}" ] && { printf '{"error":{"message":"Insufficient account balance"}}\n402'; exit 0; } ;;
|
||||
https://token-plan-*) [ -n "\${MOCK_SK:-}" ] && { printf '{"error":{"message":"invalid api key"}}\n401'; exit 0; } ;;
|
||||
esac
|
||||
case "\$a" in
|
||||
https://token-plan-sgp.xiaomimimo.com/*) printf '\n000'; exit 28 ;;
|
||||
https://token-plan-ams.xiaomimimo.com/*) printf '{"content":[]}\n200'; exit 0 ;;
|
||||
https://*) printf '{"error":{"message":"invalid api key"}}\n401'; exit 0 ;;
|
||||
esac
|
||||
done
|
||||
CURLEOF
|
||||
printf '#!/usr/bin/env bash\necho "$ANTHROPIC_BASE_URL" > "%s/claude.url"\n' "$tmp" > "$tmp/mock/claude"
|
||||
chmod +x "$tmp/mock/curl" "$tmp/mock/claude"
|
||||
_run_mimo() {
|
||||
(cd "$tmp" && printf 'tp-test\n' | HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
|
||||
MIMO_BASE_URL= AI_RESET_KEY= AI_EFFORT= bash "$tmp/bin/ai-mimo" >/dev/null 2>&1)
|
||||
}
|
||||
local ams=https://token-plan-ams.xiaomimimo.com/anthropic
|
||||
_run_mimo || ok_run=0
|
||||
[ "$(cat "$tmp/claude.url" 2>/dev/null)" = "$ams" ] || ok_run=0
|
||||
[ "$(cat "$tmp/home/.config/ai-setup/mimo_base_url" 2>/dev/null)" = "$ams" ] || ok_run=0
|
||||
[ "$(cat "$tmp/home/.config/ai-setup/mimo_key" 2>/dev/null)" = "tp-test" ] || ok_run=0
|
||||
# Повторный запуск: сохранённый эндпоинт, без перебора регионов.
|
||||
rm -f "$tmp/curl.log" "$tmp/claude.url"
|
||||
_run_mimo || ok_run=0
|
||||
[ "$(cat "$tmp/curl.log" 2>/dev/null)" = "$ams/v1/messages" ] || ok_run=0
|
||||
[ "$(cat "$tmp/claude.url" 2>/dev/null)" = "$ams" ] || ok_run=0
|
||||
# Без сети: ключ не сохраняется и Claude Code не запускается с пустым эндпоинтом.
|
||||
rm -rf "$tmp/home" "$tmp/claude.url"
|
||||
MOCK_DOWN=1 _run_mimo && ok_run=0
|
||||
[ ! -e "$tmp/claude.url" ] && [ ! -e "$tmp/home/.config/ai-setup/mimo_key" ] || ok_run=0
|
||||
# Ключ pay-as-you-go без баланса: Token Plan отвечает 401, api.xiaomimimo.com - 402,
|
||||
# нужна подсказка про ключ tp-...
|
||||
rm -rf "$tmp/home"
|
||||
local out
|
||||
out=$(cd "$tmp" && printf 'sk-test\n' | HOME="$tmp/home" PATH="$tmp/mock:$PATH" MOCK_SK=1 \
|
||||
MIMO_BASE_URL= AI_RESET_KEY= AI_EFFORT= bash "$tmp/bin/ai-mimo" 2>&1) && ok_run=0
|
||||
echo "$out" | grep -q 'Dedicated API Key' || ok_run=0
|
||||
[ ! -e "$tmp/claude.url" ] && [ ! -e "$tmp/home/.config/ai-setup/mimo_key" ] || ok_run=0
|
||||
if [ "$ok_run" -eq 1 ]; then
|
||||
ok "ai-mimo: finds the Token Plan region by probing (skips unreachable), saves and reuses it"
|
||||
else
|
||||
fail "ai-mimo: region probe must skip unreachable regions, pick the one that accepts the key and reuse it"
|
||||
fi
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
|
||||
# ── ai-mimo: effort max по умолчанию через CLI (MiMo effort игнорирует) ────
|
||||
test_mimo_effort() {
|
||||
if echo "$MIMO_SECTION" | grep -q -- '--effort "${AI_EFFORT:-max}"' \
|
||||
&& ! echo "$MIMO_SECTION" | grep -q '^_apply_effort'; then
|
||||
ok "ai-mimo: starts with --effort max by default, AI_EFFORT via CLI"
|
||||
else
|
||||
fail "ai-mimo: must start with --effort \"\${AI_EFFORT:-max}\", no _apply_effort"
|
||||
fi
|
||||
}
|
||||
|
||||
test_logout_mimo() {
|
||||
local tmp_home rc=0
|
||||
tmp_home=$(mktemp -d)
|
||||
mkdir -p "$tmp_home/.config/ai-setup"
|
||||
echo "tp-test" > "$tmp_home/.config/ai-setup/mimo_key"
|
||||
echo "https://token-plan-ams.xiaomimimo.com/anthropic" > "$tmp_home/.config/ai-setup/mimo_base_url"
|
||||
echo '{"prompt":"/logout-mimo"}' | HOME="$tmp_home" AI_LAUNCHER=claude bash "$LOGOUT_HOOK" 2>/dev/null || rc=$?
|
||||
if [ "$rc" -eq 2 ] && [ ! -f "$tmp_home/.config/ai-setup/mimo_key" ] && [ ! -f "$tmp_home/.config/ai-setup/mimo_base_url" ]; then
|
||||
ok "logout hook: /logout-mimo removes mimo_key and mimo_base_url"
|
||||
else
|
||||
fail "logout hook: /logout-mimo must remove mimo_key and mimo_base_url (rc=$rc)"
|
||||
fi
|
||||
rm -rf "$tmp_home"
|
||||
}
|
||||
|
||||
# ── ai-qwen: Claude Code через Alibaba Token Plan, ключ вшит ───────────────
|
||||
test_qwen_launcher() {
|
||||
if echo "$QWEN_SECTION" | grep -q 'CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/qwen"' \
|
||||
&& echo "$QWEN_SECTION" | grep -q '^ANTHROPIC_BASE_URL="$qwen_base_url"' \
|
||||
&& echo "$QWEN_SECTION" | grep -q '^ANTHROPIC_DEFAULT_OPUS_MODEL=qwen3.8-max' \
|
||||
&& echo "$QWEN_SECTION" | grep -q '^ANTHROPIC_DEFAULT_SONNET_MODEL=qwen3.8-max' \
|
||||
&& echo "$QWEN_SECTION" | grep -q '^ANTHROPIC_DEFAULT_HAIKU_MODEL=qwen3.8-flash' \
|
||||
&& echo "$QWEN_SECTION" | grep -q '^CLAUDE_CODE_SUBAGENT_MODEL=qwen3.8-max' \
|
||||
&& echo "$QWEN_SECTION" | grep -q '^CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000' \
|
||||
&& echo "$QWEN_SECTION" | grep -q -- 'claude --dangerously-skip-permissions --model opus --effort "${AI_EFFORT:-max}"'; then
|
||||
ok "ai-qwen: own config, qwen3.8-max in opus/sonnet/subagents, flash in haiku, 1M window, effort max"
|
||||
else
|
||||
fail "ai-qwen: must use cfg/qwen, qwen3.8-max/flash slots, 1M window, --model opus and --effort max"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── ai-qwen: стартует сразу со вшитым ключом, ничего не спрашивая ─────────
|
||||
test_qwen_starts_without_prompt() {
|
||||
local tmp url token ok_run=1
|
||||
tmp=$(mktemp -d)
|
||||
mkdir -p "$tmp/bin" "$tmp/mock" "$tmp/home"
|
||||
echo "$HELPERS_SECTION" | HELPERS_FILE="$tmp/bin/ai-api-helpers.sh" bash
|
||||
echo "$QWEN_SECTION" | BIN_DIR="$tmp/bin" bash
|
||||
printf '#!/usr/bin/env bash\necho "$ANTHROPIC_BASE_URL $ANTHROPIC_AUTH_TOKEN" > "%s/claude.env"\n' "$tmp" > "$tmp/mock/claude"
|
||||
chmod +x "$tmp/mock/claude"
|
||||
(cd "$tmp" && HOME="$tmp/home" PATH="$tmp/mock:$PATH" AI_EFFORT= bash "$tmp/bin/ai-qwen" </dev/null >/dev/null 2>&1) || ok_run=0
|
||||
read -r url token < "$tmp/claude.env" 2>/dev/null || ok_run=0
|
||||
[ "${url:-}" = "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic" ] || ok_run=0
|
||||
case "${token:-}" in sk-sp-?*) ;; *) ok_run=0 ;; esac
|
||||
if [ "$ok_run" -eq 1 ]; then
|
||||
ok "ai-qwen: starts Claude Code with the built-in endpoint and key, no prompt"
|
||||
else
|
||||
fail "ai-qwen: must start Claude Code right away with the built-in Token Plan endpoint and sk-sp- key"
|
||||
fi
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
|
||||
# ── ai-glm: default effort is xhigh (still overridable via AI_EFFORT) ──────
|
||||
test_glm_default_effort() {
|
||||
if echo "$GLM_SECTION" | grep -q -- '--effort "${AI_EFFORT:-xhigh}"' \
|
||||
&& echo "$GLM_SECTION" | grep -q '^CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000' \
|
||||
&& echo "$GLM_SECTION" | grep -q 'CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1'; then
|
||||
ok "ai-glm: starts with --effort xhigh by default, effort sent to Z.ai, 64000 output tokens"
|
||||
else
|
||||
fail "ai-glm: must start with --effort \"\${AI_EFFORT:-xhigh}\", CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1 and MAX_OUTPUT_TOKENS=64000"
|
||||
fail "ai-glm: must use official Zhipu AI API with glm-5.2 and x-api-key"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -502,14 +114,20 @@ test_cursor_native_launcher() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ── global rules: source and native rule files ───────────────────────
|
||||
test_global_rules_source_installed() {
|
||||
if [ -s "$GLOBAL_RULES_SOURCE" ] \
|
||||
# ── global rules: Karpathy-style guidelines and native rule files ───────────
|
||||
test_global_rules_include_quality_guidelines() {
|
||||
karpathy_line=$(grep -n '^## 1\. Think Before Coding$' "$GLOBAL_RULES_SOURCE" | head -1 | cut -d: -f1)
|
||||
global_line=$(grep -n '^# Global Rules for All AI Agents$' "$GLOBAL_RULES_SOURCE" | head -1 | cut -d: -f1)
|
||||
if [ -n "$karpathy_line" ] \
|
||||
&& [ -n "$global_line" ] \
|
||||
&& [ "$karpathy_line" -lt "$global_line" ] \
|
||||
&& grep -q 'Always reply in Russian' "$GLOBAL_RULES_SOURCE" \
|
||||
&& grep -q 'Plain git diff visibility' "$GLOBAL_RULES_SOURCE" \
|
||||
&& grep -q 'GLOBAL_RULES_SOURCE=' "$SCRIPT" \
|
||||
&& grep -q 'cp "$GLOBAL_RULES_SOURCE" "$CONFIG_DIR/global_rules.md"' "$SCRIPT"; then
|
||||
ok "global rules: non-empty source file is installed as global_rules.md"
|
||||
ok "global rules: source file includes English Karpathy guidelines before user rules"
|
||||
else
|
||||
fail "global rules: missing source file or installation wiring"
|
||||
fail "global rules: missing source file, English Karpathy guidelines, or user rules"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -557,33 +175,9 @@ test_script_syntax() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Android CLI: opt-in install (setup.sh item 8), not part of ai-setup.sh ─
|
||||
test_android_script_syntax() {
|
||||
if bash -n "$ANDROID_SCRIPT" 2>&1; then
|
||||
ok "android-cli: android-cli-setup.sh passes 'bash -n'"
|
||||
else
|
||||
fail "android-cli: android-cli-setup.sh has syntax errors"
|
||||
fi
|
||||
}
|
||||
|
||||
test_android_not_installed_by_default() {
|
||||
if ! grep -q 'dl.google.com/android/cli' "$SCRIPT" && ! grep -q 'android init' "$SCRIPT"; then
|
||||
ok "android-cli: ai-setup.sh does not install Android CLI by default"
|
||||
else
|
||||
fail "android-cli: ai-setup.sh still installs Android CLI"
|
||||
fi
|
||||
}
|
||||
|
||||
test_android_menu_item_runs_script() {
|
||||
if awk '/^ 8\)/,/;;/' "$SETUP_MENU" | grep -q 'bash scripts/android-cli-setup.sh'; then
|
||||
ok "android-cli: setup.sh item 8 runs android-cli-setup.sh"
|
||||
else
|
||||
fail "android-cli: setup.sh item 8 does not run android-cli-setup.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Android CLI: install block exists ─────────────────────────────────────
|
||||
test_android_cli_install_url() {
|
||||
if grep -q 'dl.google.com/android/cli/latest' "$ANDROID_SCRIPT"; then
|
||||
if grep -q 'dl.google.com/android/cli/latest' "$SCRIPT"; then
|
||||
ok "android-cli: install URL for Android CLI is present"
|
||||
else
|
||||
fail "android-cli: missing install URL for Android CLI"
|
||||
@@ -591,7 +185,7 @@ test_android_cli_install_url() {
|
||||
}
|
||||
|
||||
test_android_init_called() {
|
||||
if grep -q 'android init' "$ANDROID_SCRIPT"; then
|
||||
if grep -q 'android init' "$SCRIPT"; then
|
||||
ok "android-cli: android init is invoked after install"
|
||||
else
|
||||
fail "android-cli: missing android init invocation"
|
||||
@@ -599,7 +193,7 @@ test_android_init_called() {
|
||||
}
|
||||
|
||||
test_android_symlink_to_bin_dir() {
|
||||
if grep -q 'ln -sf "$HOME/.local/bin/android" "$BIN_DIR/android"' "$ANDROID_SCRIPT"; then
|
||||
if grep -q 'ln -sf "$HOME/.local/bin/android" "$BIN_DIR/android"' "$SCRIPT"; then
|
||||
ok "android-cli: symlink to BIN_DIR is handled"
|
||||
else
|
||||
fail "android-cli: missing symlink to BIN_DIR"
|
||||
@@ -697,40 +291,17 @@ test_add_hook_syntax() {
|
||||
test_script_syntax
|
||||
test_gpt_autoinstall
|
||||
test_gpt_no_proxy
|
||||
test_gpt_status_line
|
||||
test_kimi_claude_launcher
|
||||
test_kimi_official_api
|
||||
test_kimi_no_artemox
|
||||
test_glm_claude_launcher
|
||||
test_glm_official_api
|
||||
test_glm_default_effort
|
||||
test_eltex_claude_launcher
|
||||
test_eltex_model_from_key
|
||||
test_eltex_three_slots
|
||||
test_eltex_autologout_on_missing_model
|
||||
test_eltex_no_tool_search
|
||||
test_eltex_context_window
|
||||
test_eltex_logout_command
|
||||
test_eltex_effort
|
||||
test_eltex_qwen_effort
|
||||
test_logout_eltex
|
||||
test_eltex_qwen_launcher
|
||||
test_logout_eltex_qwen
|
||||
test_mimo_launcher
|
||||
test_mimo_region_probe
|
||||
test_mimo_effort
|
||||
test_logout_mimo
|
||||
test_qwen_launcher
|
||||
test_qwen_starts_without_prompt
|
||||
test_gemini_native_launcher
|
||||
test_cursor_native_launcher
|
||||
test_global_rules_source_installed
|
||||
test_global_rules_include_quality_guidelines
|
||||
test_native_rule_files_generated
|
||||
test_codex_sound_hooks_detach_stdio
|
||||
test_fix7_trap_tmp
|
||||
test_android_script_syntax
|
||||
test_android_not_installed_by_default
|
||||
test_android_menu_item_runs_script
|
||||
test_android_cli_install_url
|
||||
test_android_init_called
|
||||
test_android_symlink_to_bin_dir
|
||||
|
||||
Reference in New Issue
Block a user