Добавил конвенции для конфигурации и сделал рефакторинг кода

This commit is contained in:
av
2026-06-28 20:53:10 +03:00
parent 9cfccc7b4a
commit 84ffe0733e
11 changed files with 371 additions and 55 deletions
+123
View File
@@ -0,0 +1,123 @@
# Конфигурация
Конвенция: *как* устроена и грузится конфигурация jellybit (TOML).
Правила оформления кода (How), не спецификация поведения.
Краткая выжимка и инварианты — в [CLAUDE.md](../../CLAUDE.md), раздел
«Конвенции кода».
> Каркас. Загрузчик `internal/config/config.go` уже грузит TOML; валидация
> на старте — в работе (`TODO`), обкатывается на следующем шаге.
## Принципы
- **Конфигурация — только TOML.** Env-переменные для конфига **не
используем**: окружение наследуется дочерними процессами и видно через
`/proc/<pid>/environ` — для секретов это слабее файла под `0600`.
- Грузим **один раз при старте** в одну типизированную структуру `Config`
(под-структуры по секциям). Дальше по коду читаем только её — никаких
`os.Getenv`/чтения файла в бизнес-коде, только загрузчик `internal/config`.
- Конфиг **неизменяем** после старта; смена параметров — рестарт процесса.
## Файл и поиск
- Имя конфига по умолчанию — **`config.toml`**, ищется в **рабочей
директории** процесса.
- Путь переопределяется опцией **`--config=path`**.
- Образец в репозитории — **`config.example.toml`** (см. ниже); реальный
`config.toml` не коммитится.
## config.example.toml — самодокументируемый образец
`config.example.toml` коммитим как единый справочник по конфигу: все секции
и все поля. **Каждое поле снабжаем комментарием**, из которого ясно:
- **зачем** поле — что оно меняет в поведении;
- **диапазон/допустимые значения** — перечисление или границы;
- **единицы измерения**, если применимо — секунды/миллисекунды, байты/КБ,
доля `01` и т.п.
```toml
[worker]
poll_interval = "5s" # как часто опрашивать qBittorrent; Go-duration (s/m/h)
magnet_timeout = "30m" # ждать метаданные magnet не дольше; Go-duration
[recognition]
auto_confidence_threshold = 0.85 # порог авто-раскладки без ревью; доля 0.0–1.0
[llm]
max_retries = 3 # попыток получить валидный ответ LLM; целое ≥ 0
```
Секретные поля оставляем пустыми — значение приходит из деплоя (см.
«Секреты»).
## Поля по дискриминатору `type`
Когда набор полей секции зависит от поля-дискриминатора `type` (выбор одного
из бекендов/внешних сервисов — напр. `[llm].type`), обязательность и
опциональность полей определяются значением `type`, а не фиксированы для
секции.
- **Валидация — по `type`.** Для каждого поддерживаемого `type` свой набор
обязательных полей; поля, относящиеся к другим `type`, не требуются.
Неизвестный `type` → ошибка на старте с перечислением поддерживаемых.
- **Образец — по `type`.** В `config.example.toml`:
- основной (дефолтный) `type` **предзаполнен** рабочими значениями;
- альтернативные `type`**блоками-комментариями ниже**, каждый со своим
описанием полей (зачем/диапазон/единицы — как у обычных полей);
- так из примера видны все варианты и поля каждого, не открывая код.
```toml
[llm]
type = "openai-compat" # бекенд LLM; варианты ниже
base_url = "http://host.docker.internal:1234/v1" # эндпоинт OpenAI-совместимого API
api_key = "" # ключ; пусто для keyless-local (LM Studio)
model = "qwen2.5-32b-instruct" # имя модели у провайдера
# --- альтернативный бекенд: type = "<other>" ---
# [llm]
# type = "<other>" # описание варианта
# ... # его обязательные/опциональные поля
```
## Секреты
Секреты доставляет **деплой**, рендеря их прямо в `config.toml` (jellybit:
Ansible + Vault). Приложение просто читает TOML — отдельного слоя секретов
в коде нет. Источник истины секрета — внешнее хранилище деплоя (Vault), не
репозиторий и не env.
- Секретные поля jellybit: `qbittorrent.password`, `llm.api_key`,
`metadata.*.api_key`, `jellyfin.api_key`, `telegram.token`.
- Рендеренный `config.toml` (с секретами) **не коммитится**; права `0600`,
владелец — runtime-пользователь (`1000:1000`).
- В `config.example.toml` секретные поля — пустые строки.
- Загрузчик на старте проверяет, что обязательные секреты не пусты (ловит
криво отрендеренный файл) — см. «Валидация и fail-fast».
- В логи секреты не попадают — см. [logging.md](logging.md), «Безопасность».
## Валидация и fail-fast
Конфиг валидируем **на старте, до приёма трафика**. Невалидный конфиг —
лог `ERROR` и выход с ненулевым кодом (не стартуем «наполовину»).
Что проверяем (jellybit):
- обязательные поля заданы (напр. `qbittorrent.url`, `paths.*`,
`storage.db_path`);
- пути `paths.movies`/`series`/`downloads` существуют и доступны; целевые —
под единой песочницей (см. инварианты в [CLAUDE.md](../../CLAUDE.md));
- диапазоны: `recognition.auto_confidence_threshold` ∈ [0, 1],
`llm.max_retries` ≥ 0;
- длительности парсятся (`llm.timeout`, `worker.poll_interval`, …);
- включённые секции консистентны: `metadata.tmdb.enabled` → задан `api_key`;
`jellyfin.enabled` → заданы `url`+`api_key`; `telegram.enabled``token`.
## Структура в коде
- Весь разбор и валидация — в `internal/config`; наружу отдаётся готовая
`Config`.
- Одна корневая структура `Config` с под-структурами по секциям
(`QBittorrent`, `Paths`, `LLM`, `Metadata`, `Jellyfin`, `Worker`,
`Recognition`, `Telegram`, `HTTP`, `Log`).