From 848c89cec89fc8a107a3771b87acfb149a8a5b06 Mon Sep 17 00:00:00 2001 From: stepan Date: Tue, 15 Sep 2026 21:59:43 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BD=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=98=D0=98-=D0=BF=D1=80=D0=BE=D0=B2=D0=B0=D0=B9?= =?UTF-8?q?=D0=B4=D0=B5=D1=80=D0=B0=20=D0=B2=20=D0=BA=D0=BE=D0=BD=D1=81?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API-карта.md | 19 +++-- Архитектура-10-operator-analytics-contract.md | 70 +++++++++++++++++++ Инструкция-пользователя.md | 14 ++-- ТЗ.md | 8 ++- Техническая-документация.md | 39 ++++++----- 5 files changed, 116 insertions(+), 34 deletions(-) diff --git a/API-карта.md b/API-карта.md index 9d8999e..2621f75 100644 --- a/API-карта.md +++ b/API-карта.md @@ -138,17 +138,19 @@ | `POST /dialogs/{dialog_id}/backfill` | Догнать сообщения одного диалога | — | `{ok: true, processed: int}` *(фронт не вызывает — только сервер)* | | `POST /dialogs/preview` | Последние сообщения диалога (свежие из TG, старые из БД) | `{dialogId, limit?=24 (clamp 1..50)}` | `{items: [§4.11 сообщение]}` | -### 3.4 Settings / rates / meta (settings_routes.py) — 6 +### 3.4 Settings / rates / meta (settings_routes.py) — 5 | METHOD /api/… | Назначение | Request body | Response | |---|---|---|---| | `GET /settings` | Публичные настройки (секреты замаскированы) | — | §4.6 (полный settings) | -| `PATCH /settings` | Частичное обновление (см. §4.6 список ключей). Инварианты: `archiveAfterDays` 1..30, `minLen` 10..500, `discJoinLimit` 1..200, `discJoinDelayMin/Max` 5..600 (min≤max), `discEvalSample` 3..30, `discEvalThreshold` 1..100; `aiConfigs` apiKey ≥8 → шифруется; `myPrompts` ≤100. ⚠ `tgKeys` удалён из настроек тенанта — ключи Telegram задаёт оператор глобально. ⚠ Ответ — **весь** public settings (фронт затирает локальное состояние ответом) | произвольный dict из публичных ключей | §4.6 | -| `POST /ai/check` | Проверка подключения AI-провайдера | — | `{ok: bool, message: str}` + поля статуса провайдера | +| `PATCH /settings` | Частичное обновление (см. §4.6 список ключей). Инварианты: `archiveAfterDays` 1..30, `minLen` 10..500, `discJoinLimit` 1..200, `discJoinDelayMin/Max` 5..600 (min≤max), `discEvalSample` 3..30, `discEvalThreshold` 1..100; `myPrompts` ≤100. ⚠ `tgKeys`, `aiProvider` и `aiConfigs` удалены из настроек тенанта — ключи Telegram и конфигурацию ИИ задаёт оператор глобально. ⚠ Ответ — **весь** public settings (фронт затирает локальное состояние ответом) | произвольный dict из публичных ключей | §4.6 | | `GET /rates` | Курсы валют | — | `{base: "RUB", rates: {CODE: num}, source: "cbr"\|"mock", updatedAt: ms\|null}` | | `POST /rates/refresh` | Принудительно обновить курсы (ЦБ/мок) | — | `{ok: bool, rates: {base, rates, source, updatedAt}}` — ⚠ фронт передаёт `r.rates` в `applyRates` | | `GET /meta/constants` | Валюты/стадии/палитра | — | `{currencies: [{code,name,symbol}], stages: [§4.4], palette: ["#…"]}` *(фронт не вызывает — зашиты в data.js)* | +⚠ `POST /ai/check` удалён: проверка связи с провайдером ИИ теперь операторская — +`POST /api/operator/settings/ai-config/check` (раздел «ИИ» консоли оператора). + ### 3.5 Детальные операции карточки (бывший Projects, projects_routes.py) — 15 Все операции — над ресурсом `/api/cards/{cardId}` (см. §4.1); отдельного `/api/projects` больше нет. @@ -349,14 +351,12 @@ links/files/history/tzText/reminder` — модули; `isNew/prevCol/isVacancy/ "discJoinLimit": 50, "discJoinDelayMin": 50, "discJoinDelayMax": 70, "discEvalSample": 10, "discEvalThreshold": 40, // (не используется фронтом) "discPaused": false, "colState": {}, // colState — то же, что GET /columns/state - "aiProvider": "deepseek", - "aiConfigs": { "deepseek": {"baseUrl": "https://api.deepseek.com", "model": "…", "keySet": true, "keyMasked": "sk-12…3456"} }, - "providers": [{"id":"deepseek","name":"DeepSeek","base":"…","local":false,"models":[…]}, …] } ``` -Ключи, которые фронт шлёт в PATCH (по одному/группами): `aiProvider`, `aiConfigs{:{baseUrl,model,apiKey?}}`, `aiPrompt`, `cardPrompt`, `aiFilterPrompt`, `stopPhrases`, `domainDescription`, `domainKeywords`, `hireMarkers`, `levelTerms`, `resumeMarkers`, `blockResumes`, `myPrompts`, `autoArchive`, `archiveAfterDays`, `aiEnabled`, `aiFilterEnabled`, `minLen`, `conversionOn`, `targetCurrency`, `rateSource`, `remindersEnabled`, `mlEnabled`, `wantedType`, `budgetRequiredHire`, `budgetRequiredOrder`, `hireLabel`, `orderLabel`, `autoMonitorNew`, `discJoinLimit`, `discJoinDelayMin`, `discJoinDelayMax`, `discPaused`. -⚠ Ответ PATCH — **полный** settings: `schedulePersist`/`saveAiSettings`/`saveDiscQuota` применяют его целиком к локальному state (источник истины после клампов). +Ключи, которые фронт шлёт в PATCH (по одному/группами): `aiPrompt`, `cardPrompt`, `aiFilterPrompt`, `stopPhrases`, `domainDescription`, `domainKeywords`, `hireMarkers`, `levelTerms`, `resumeMarkers`, `blockResumes`, `myPrompts`, `autoArchive`, `archiveAfterDays`, `aiEnabled`, `aiFilterEnabled`, `minLen`, `conversionOn`, `targetCurrency`, `rateSource`, `remindersEnabled`, `mlEnabled`, `wantedType`, `budgetRequiredHire`, `budgetRequiredOrder`, `hireLabel`, `orderLabel`, `autoMonitorNew`, `discJoinLimit`, `discJoinDelayMin`, `discJoinDelayMax`, `discPaused`. +⚠ Ответ PATCH — **полный** settings: `schedulePersist`/`saveDiscQuota` применяют его целиком к локальному state (источник истины после клампов). ⚠ **Изменение (решение владельца, вариант A):** ключей Telegram (`api_id`/`api_hash`) в настройках тенанта больше нет — они задаются оператором глобально (ТЗ §4.1/§8.1), см. `docs/architecture/2026-09-10-operator-analytics-contract.md` (раздел «Операторские настройки»). Вкладка Telegram у тенанта остаётся (подключение аккаунта, `GET /api/tg/status`). +⚠ **Изменение (2026-09-14):** настройки ИИ-провайдера (`aiProvider`, `aiConfigs`, `providers`) из настроек тенанта убраны — провайдера, модель, адрес и ключ задаёт оператор в консоли (раздел «ИИ», `GET/PUT /api/operator/settings/ai-config`); все ИИ-вызовы всех пользователей идут на эту конфигурацию. ### 4.7 Промпты - `aiPrompt`, `aiFilterPrompt`, `cardPrompt` — plain string, редактируются на вкладке ИИ; содержат плейсхолдеры `{domain}`/`{keywords}`. @@ -383,7 +383,6 @@ links/files/history/tzText/reminder` — модули; `isNew/prevCol/isVacancy/ `{passed, wouldCreateCard, targetContainer, matchHits, parsed, stages:[{stage, pass, skipped, reason, kw, label}]}`. Коды `stage`: `length|stop|resume|type|exclude|ml|ai|spam_ai|budget`; `skipped=true` — этап выключен настройкой. `parsed` — разбор текста (поля карточки) либо null. Запись в систему не производится. -- **`POST /api/ai/check`**: `{ok: bool, message: string, local?, keySet?}`. - Комментарии карточки: `{id, by: string, text, time: string}` — `by` всегда «Вы», `time` «только что». --- @@ -402,7 +401,7 @@ links/files/history/tzText/reminder` — модули; `isNew/prevCol/isVacancy/ |---|---:| | Auth `/api/auth` | 4 | | Telegram `/api/tg` | 14 | -| Settings/rates/meta (`/api/settings`, `/api/ai/check`, `/api/rates`) | 5 | +| Settings/rates/meta (`/api/settings`, `/api/rates`) | 4 | | Processing `/api/pipeline` (+ `/api/admin/check-message`) | 7 | | ML `/api/ml` | 5 | | Discovery `/api/discovery` | 13 | diff --git a/Архитектура-10-operator-analytics-contract.md b/Архитектура-10-operator-analytics-contract.md index 915963d..0ad9f08 100644 --- a/Архитектура-10-operator-analytics-contract.md +++ b/Архитектура-10-operator-analytics-contract.md @@ -275,3 +275,73 @@ > Примечание для вкладки Telegram у тенанта: `GET /api/tg/status` остаётся (подключение аккаунта), > поле `keysSet` отражает глобальные ключи; команды `start-phone`/`start-qr` без ключей отвечают > `400 { "detail": "Ключи Telegram не заданы оператором" }`. + +--- + +## Операторские настройки: глобальная конфигурация ИИ + +Провайдер, модель, адрес API и ключ ИИ задаёт оператор **глобально**, едины для всех тенантов +(в настройках тенанта ключей `aiProvider`/`aiConfigs` больше нет). Хранилище — системная таблица +`public.global_settings` (ключ `aiConfig`), `apiKey` хранится зашифрованным (`enc:`) и наружу +не отдаётся. На эту конфигурацию работают все ИИ-вызовы всех тенантов: классификация, фильтр, +ключи поиска, карточки (`AiProviderConfigBuilder`). + +### GET /api/operator/settings/ai-config + +Маскированный снимок конфигурации вместе с каталогом провайдеров для выбора. + +**200** + +```json +{ + "providerId": "deepseek", + "baseUrl": "https://api.deepseek.com", + "model": "deepseek-v4-flash", + "keySet": true, + "keyMasked": "sk-o…-123", + "providers": [ + { "id": "deepseek", "name": "DeepSeek", "base": "https://api.deepseek.com", "local": false, "models": ["…"] } + ] +} +``` + +- `providerId` — пусто, если конфигурацию ещё не задавали. +- `baseUrl`/`model` — эффективные значения (адрес каталога, первая модель каталога), если не переопределены. +- `keyMasked` — **маска** (пусто / `x…` / `1234…5678`); открытый ключ не возвращается никогда. +- `providers` — каталог `AiProviders` (`id`/`name`/`base`/`local`/`models`); адрес каталогных облачных + провайдеров фиксирован (SSRF-гейт), свой `baseUrl` задаётся только локальным и `custom`. + +**Коды**: `200`, `401`. + +### PUT /api/operator/settings/ai-config + +Сохранение/смена. Поля можно передавать **по отдельности** (частичное обновление): непереданное поле +(`null` или отсутствие в JSON) сохраняет текущее значение. Если конфигурации ещё нет, `providerId` обязателен. +При смене провайдера `baseUrl`/`model`/`apiKey` не переносятся от старого (дефолты каталога); ключ +меняется только при явной передаче (маска не принимается). + +**Тело** + +```json +{ "providerId": "deepseek", "model": "deepseek-v4-pro", "apiKey": "sk-…" } +``` + +**200** — маскированный снимок (форма как у GET). + +**Ошибки** + +- `400 { "detail": "Укажите хотя бы одно поле (providerId, baseUrl, model, apiKey)" }` +- `400 { "detail": "Провайдер не из списка разрешённых" }` +- `400 { "detail": "Конфигурация ИИ ещё не задана — укажите providerId" }` +- `400 { "detail": "Укажите model — у выбранного провайдера нет моделей по умолчанию" }` +- `400 { "detail": "API-ключ должен быть не короче 8 символов, без маски" }` +- `401 { "detail": "Требуется вход оператора" }` + +### POST /api/operator/settings/ai-config/check + +Проверка связи с сохранённым провайдером (`200` — результат `AiCheckResultDto`). Локальный провайдер +отвечает `ok: true` без HTTP; облачный — запрос к списку моделей (приватные адреса запрещены, SSRF-гейт). +`400 { "detail": "Сначала сохраните конфигурацию ИИ" }` — конфигурации ещё нет. + +**Аудит**: событие `ai_config_changed` (актор `operator`, `tenantId: null`, детали +`{providerId, baseUrl, model, keySet}` — без ключа). diff --git a/Инструкция-пользователя.md b/Инструкция-пользователя.md index 35717bf..c72a038 100644 --- a/Инструкция-пользователя.md +++ b/Инструкция-пользователя.md @@ -186,12 +186,13 @@ **Настройки → Telegram:** подключение аккаунта, авто-мониторинг новых чатов. **Настройки → ИИ:** -- провайдер и модель (можно выбрать один, включая локальные OpenAI-совместимые); -- ключ API (хранится зашифрованно); -- **промпты**: базовый (не меняется) + свой промпт; библиотека готовых промптов по сферам - с поиском и категориями; сохранённые свои промпты («Мои промпты»); - вкл/выкл ИИ и ИИ-фильтр. Если ML уже уверенно обрабатывает поток — система подскажет, - что ИИ можно отключить. + что ИИ можно отключить; +- **промпты**: базовый (не меняется) + свой промпт; библиотека готовых промптов по сферам + с поиском и категориями; сохранённые свои промпты («Мои промпты»). + +Провайдера, модель и API-ключ задаёт оператор сервиса — они едины для всех пользователей +и в кабинете не настраиваются. **Настройки → ML:** включение, обучение на ваших действиях, проверка модели на сообщении/канале, сброс обучения, показатели самооценки. @@ -261,6 +262,9 @@ сырой JSON доступен под спойлером. - **Аналитика** — обзор за период (число пространств, расход токенов, входы/выходы/неудачные входы), расход токенов с группировкой по дням/пространствам/провайдерам/моделям и лента действий. +- **ИИ** — глобальный провайдер ИИ: выбор провайдера из каталога, модель, адрес API (для локальных + и «Другого») и API-ключ, а также проверка связи. Конфигурация единая для всех пользователей; + ключ хранится зашифрованным и показывается только маской. - **Состояние системы** — доступность ядра, базы данных и сервисов (Telegram, ИИ, ML). > **Dev-окружение:** вход в консоль — `operator`/`operator`. В обычной (прод) сборке учётные diff --git a/ТЗ.md b/ТЗ.md index 31c4f22..0f0b7dd 100644 --- a/ТЗ.md +++ b/ТЗ.md @@ -191,9 +191,9 @@ Telegram-аккаунт, выбирает каналы/группы для мо ## 8. Настройки тенанта - Telegram: ключи приложения (оператор), подключение аккаунта, авто-мониторинг новых. -- ИИ: провайдер (один; включая локальные), модель, ключ (хранится зашифрованно), - промпты (базовый + свой), библиотека готовых промптов по сферам + «мои промпты», - вкл/выкл ИИ, вкл/выкл ИИ-фильтр. +- ИИ: провайдер (один; включая локальные), модель и ключ задаёт **оператор** глобально (едины для всех + тенантов; в консоли оператора раздел «ИИ», ключ хранится зашифрованно); пользователю — промпты + (базовый + свой), библиотека готовых промптов по сферам + «мои промпты», вкл/выкл ИИ, вкл/выкл ИИ-фильтр. - ML: вкл/выкл, обучение на действиях, проверка на сообщении/канале, сброс, самооценка («ML справляется с последними N сообщениями — ИИ можно отключить»). - Обработка: стоп-фразы, длина, резюме, тип заявки, домен/ключи, маркеры найма/заказа. @@ -221,6 +221,8 @@ Telegram-аккаунт, выбирает каналы/группы для мо ## 10. Админка оператора - Тенанты: создание, инвайты, статус, лимиты/бюджеты, приостановка. +- Глобальные настройки сервиса: ключи приложения Telegram и конфигурация ИИ-провайдера + (провайдер/модель/baseUrl/ключ; ключ зашифрован, наружу — маска) с проверкой связи. - Health всех сервисов и очередей. - Аудит: входы/выходы, инвайты, impersonation, действия оператора и пользователей тенанта (создание/перенос/удаление карточек, комментарии, контейнеры, настройки, каналы). diff --git a/Техническая-документация.md b/Техническая-документация.md index e458951..1826635 100644 --- a/Техническая-документация.md +++ b/Техническая-документация.md @@ -121,8 +121,10 @@ global_settings(key, value, updated_at) > `token_usage_events` — история расхода токенов (этап 10, T2; подробнее — §13.10). > `global_settings` — глобальные настройки уровня сервиса; сейчас хранит ключи приложения Telegram -> (`telegramKeys`: `api_id`/`api_hash`, hash — в `enc:`), которые задаёт **оператор** глобально -> (ручки `GET/PUT /api/operator/settings/telegram-keys`); тенант ключи не видит/не задаёт. +> (`telegramKeys`: `api_id`/`api_hash`, hash — в `enc:`) и конфигурацию ИИ-провайдера (`aiConfig`: +> `providerId`/`baseUrl`/`model`/`apiKey` — в `enc:`), которые задаёт **оператор** глобально +> (ручки `GET/PUT /api/operator/settings/telegram-keys` и `/ai-config`, проверка связи — +> `POST /api/operator/settings/ai-config/check`); тенант эти настройки не видит и не задаёт. > Операторские таблицы этапа 7 (`operators`, `operator_sessions`, `tenant_limits`, `audit_log`) и их > контур описаны в §13.8. С этапа 12 счётчики распределённого rate-limit и попыток входа — > `public.rate_limit_counters` (см. §10). @@ -468,7 +470,8 @@ DEAL_MTLS_ENABLED=0|1 DEAL_MTLS_CERT_PASSWORD=... DEAL_DEFAULT_AI_BU ответ — полный снимок; секреты наружу только масками `keyMasked`/`apiId`; внутренние ключи `ratesCache`/`mlDecisions`/`aiDecisions` не публикуются); - шифрование секретов AI/Telegram: AES-256-GCM, в БД — `enc:` + Base64 (ключ — env/file, см. §13.4a); -- проверка подключения ИИ: `POST /api/ai/check` (локальный провайдер / HTTP-проверка облачного); +- проверка подключения ИИ: `POST /api/operator/settings/ai-config/check` (операторская; локальный провайдер / + HTTP-проверка облачного); - курсы валют: `GET /api/rates`, `POST /api/rates/refresh` (кэш `ratesCache` в settings; `mock`/ЦБ); - ML-панель на детерминированной заглушке: `GET /api/ml/status`, `POST /api/ml/reset|predict` (candidates → `{items:[]}`, apply → 404 — нет telegram-данных до этапа 6); @@ -746,28 +749,30 @@ health — `GET /api/health` → `{"ok":true,"service":"deal"}`. ### 4a. Шифрование секретов настроек (ключи AI/Telegram) -Секреты (`aiConfigs[].apiKey`) хранятся в `settings.ValueJson` шифротекстом: -`enc:` + Base64(nonce‖ct‖tag), AES-256-GCM (nonce 12 Б, tag 16 Б). Ключ шифрования — env -`DEAL_ENCRYPTION_KEY` (32 байта в urlsafe-Base64); при отсутствии в dev берётся/создаётся файл -`/data/encryption.key` (путь переопределяется env `DEAL_ENCRYPTION_KEY_FILE`) — -при генерации лог-warning. Невалидный env-ключ — ошибка при старте. Наружу секреты не отдаются: -в GET/PATCH `/api/settings` только маски `keyMasked` (первые 4 + «…» + последние 4, len≤8 — как есть) +Секреты хранятся шифротекстом `enc:` + Base64(nonce‖ct‖tag), AES-256-GCM (nonce 12 Б, tag 16 Б). +Ключ шифрования — env `DEAL_ENCRYPTION_KEY` (32 байта в urlsafe-Base64); при отсутствии в dev +берётся/создаётся файл `/data/encryption.key` (путь переопределяется env +`DEAL_ENCRYPTION_KEY_FILE`) — при генерации лог-warning. Невалидный env-ключ — ошибка при старте. +Наружу секреты не отдаются: только маски `keyMasked` (первые 4 + «…» + последние 4, len≤8 — как есть) и `keySet`. > Исторический раздел (этап 2). С этапа 12 ключей Telegram (`tgKeys`/`apiId`/`apiHash`) в настройках > тенанта нет — они задаются **оператором** глобально (таблица `public.global_settings`, > `GET/PUT /api/operator/settings/telegram-keys`; hash шифруется тем же AES-256-GCM). +> +> С 2026-09-14 там же живёт и конфигурация ИИ-провайдера (ключ `aiConfig` в `public.global_settings`, +> `GET/PUT /api/operator/settings/ai-config`): провайдер, модель, baseUrl и API-ключ задаёт оператор, +> все ИИ-вызовы всех тенантов идут на эту конфигурацию; в настройках тенанта ключей `aiProvider`/ +> `aiConfigs` больше нет. ### 4b. Эндпоинты этапа 2 (настройки тенанта; сессия `deal_session` обязательна, иначе 401) - `GET /api/settings` — публичный снимок дерева настроек: дефолты модуля, перекрытые - переопределениями из `settings` тенанта; включает списки `providers`/`aiConfigs`/`tgKeys`/`myPrompts`. + переопределениями из `settings` тенанта; включает `myPrompts` и колонки, но не настройки + ИИ-провайдера (они операторские). `PATCH /api/settings` — частичное обновление (невалидное поле мягко пропускается, ответ — полный снимок). Побочные эффекты: при `rateSource` — фоновый refresh курсов. Внутренние ключи (`ratesCache`, `mlDecisions`, `aiDecisions`) в GET/PATCH не участвуют. -- `POST /api/ai/check` — проверка подключения активного провайдера (`aiProvider` + `aiConfigs`, ключ - расшифровывается): локальный провайдер → `ok:true` «Локальный сервер…»; облачный — HTTP `GET - {base}/models`; без ключа → «Не задан API-ключ». - `GET /api/rates` / `POST /api/rates/refresh` — курсы к RUB (`base` = `RUB`); источник по `rateSource` (`mock` — константа, `cbr` — ЦБ РФ, ≤4 запроса/сутки, интервал 6 ч; `USDT`=`USD`); кэш — внутренняя настройка `ratesCache` `{rates, source, updatedAtMs}`. @@ -1107,7 +1112,8 @@ docker compose -f deploy/compose.dev.yml down # погасить ст #### AI-фасад ai-service (Ruling 5) - Без БД: core передаёт в теле запроса заполненные промпты (подстановка `{domain}`/`{keywords}`), конфиг - провайдера (id/base/model/apiKey/api_style — расшифрованный из `aiConfigs`) и текст. Методы: `Filter` + провайдера (id/base/model/apiKey/api_style — расшифрованный из глобальной конфигурации оператора, + ключ `aiConfig`) и текст. Методы: `Filter` → {pass,reason}; `Classify` → {ok,json} (json-строку маппит core в `AiParsedLeadDto`, строгий маппинг); `GenerateKeywords` → {keywords}; `EvaluateFit` → {fit,reason} (Discovery). - Транспорт: OpenAI-совместимые `POST {base}/chat/completions` (Bearer) и Anthropic @@ -1123,8 +1129,9 @@ docker compose -f deploy/compose.dev.yml down # погасить ст (`PUT /api/operator/settings/telegram-keys`, hash шифруется) → `POST /api/tg/start-qr` → QR-скан → фаза `ready` («Telegram подключён, сессия сохранена»), затем реальные диалоги/мониторинг/«Перечитать»/ discovery-поиск и вступления. В настройках тенанта ключей нет (решение владельца, вариант A). -- LLM: `PATCH /api/settings` `aiConfigs`/`aiProvider` (напр. DeepSeek или локальный OpenAI-совместимый) → - `POST /api/ai/check`; реальная классификация/фильтр/генерация ключей при `Services__Ai__UseLocal=false`. +- LLM: оператор задаёт провайдера и модель в консоли (`PUT /api/operator/settings/ai-config`, напр. DeepSeek + или локальный OpenAI-совместимый) → `POST /api/operator/settings/ai-config/check`; реальная + классификация/фильтр/генерация ключей при `Services__Ai__UseLocal=false`. - Сквозной smoke стека — `scripts/dev-smoke.sh` (одна команда; Docker Desktop должен быть поднят). ### 8. Этап 7 — SaaS-контур (Tasks 1–14; бэкапы — §13.9; финальные доки — Task 16): оператор/инвайты/лимиты/аудит/rate-limit/mTLS/логи/compose-prod