Files
Deal/docs/architecture/2026-09-10-operator-analytics-contract.md
T
stepan ea4ed73327
ci / build-test (pull_request) Successful in 2m58s
Обновить документацию под глобальную конфигурацию ИИ
ТЗ, api-map, техническая документация, инструкция пользователя и контракт операторских настроек: настройки ИИ-провайдера перенесены в консоль оператора, пользовательский POST /api/ai/check удалён.
2026-09-15 21:59:43 +03:00

18 KiB

Дейл — контракт операторской аналитики и аудита действий (этап 10, T1–T3)

Дата: 2026-09-10 Статус: контракт для фронта (оператор-консоль, T4). Источник истины для src/frontend. Связанные документы: docs/superpowers/plans/2026-09-10-deal-stage10-operator-analytics.md, docs/architecture/2026-09-10-unified-api-contract.md.

Общие правила

  • Только операторская сессия. Все ручки /api/operator/* (включая аналитику) требуют разрешённой операторской сессии (кука deal_operator_session). Без неё — 401 { "detail": "Требуется вход оператора" }.
  • Все ответы — JSON camelCase.
  • Время на wire в этих ручках — ISO-8601 (DateTimeOffset, UTC, напр. 2026-09-10T15:22:46.123Z). Query-параметры from/to и поля at/from/to используют ISO-8601 (как уже принято в GET /api/operator/audit). Ключи агрегатов groupBy=day — строки ГГГГ-ММ-ДД.
  • Диапазоны from/toвключительно; не заданы — без границы.
  • Ошибки — { "detail": "текст" }. Коды: 400 (некорректный ввод, напр. неизвестный groupBy), 401 (нет операторской сессии).
  • Все ручки аналитики — read-only (ничего не меняют).

Каталог событий аудита (для фильтров eventType / ленты действий)

К SaaS-событиям этапа 7 добавлены (этап 10, T1; актор tenant — действия пользователя тенанта):

eventType Когда
tenant_logout выход пользователя тенанта (POST /api/auth/logout)
operator_logout выход оператора (POST /api/operator/auth/logout)
invite_joined активация инвайта (POST /api/join)
card_created создание карточки
card_moved перенос карточки между контейнерами
card_trashed карточка отправлена в корзину
card_restored карточка возвращена из корзины/архива
card_deleted карточка удалена навсегда
card_comment_added добавлен комментарий к карточке
container_created создан контейнер/колонка
container_updated изменён контейнер/колонка
container_deleted удалён контейнер/колонка
settings_updated сохранены настройки тенанта
channel_enabled включён мониторинг канала Telegram
channel_created канал добавлен в каталог (резерв каталога)
telegram_linked аккаунт Telegram привязан (фаза ready)
telegram_keys_changed оператор изменил глобальные ключи Telegram (PUT /api/operator/settings/telegram-keys)

Типы акторов (actorType): tenant, operator, system. Секреты (пароли, токены, api-ключи) в detailJson не пишутся.


GET /api/operator/analytics/overview

Сводка за период: тенанты, расход токенов, события, входы/выходы/неудачные входы.

Query

Параметр Тип Обяз. Описание
from ISO-8601 нет начало периода (включительно)
to ISO-8601 нет конец периода (включительно)

200

{
  "tenantsTotal": 12,
  "tenantsActive": 10,
  "promptTokens": 1250000,
  "completionTokens": 320000,
  "totalTokens": 1570000,
  "tokenEvents": 842,
  "events": 5012,
  "logins": 320,
  "logouts": 288,
  "failedLogins": 17,
  "from": "2026-09-01T00:00:00Z",
  "to": "2026-10-01T00:00:00Z"
}
  • tokens*/tokenEvents — сумма по событиям public.token_usage_events за период.
  • events — число записей аудита за период.
  • logins = tenant_login_ok + operator_login_ok; logouts = tenant_logout + operator_logout; failedLogins = tenant_login_failed + operator_login_failed.

Коды: 200, 401.


GET /api/operator/analytics/tokens

Серия/агрегаты расхода токенов.

Query

Параметр Тип Обяз. Описание
groupBy enum нет day (дефолт) | tenant | provider | model
tenantId uuid нет фильтр по тенанту
from ISO-8601 нет начало периода (включительно)
to ISO-8601 нет конец периода (включительно)

200

{
  "groupBy": "day",
  "from": "2026-09-01T00:00:00Z",
  "to": "2026-10-01T00:00:00Z",
  "items": [
    { "key": "2026-09-10", "promptTokens": 1200, "completionTokens": 300, "totalTokens": 1500, "eventCount": 42 }
  ],
  "total": { "key": "total", "promptTokens": 1250000, "completionTokens": 320000, "totalTokens": 1570000, "eventCount": 842 }
}
  • key группы: dayГГГГ-ММ-ДД (сутки UTC); tenant — Guid D; provider — id провайдера (deepseek/openai/…, для ML — local); model — модель (ml для локальной ML-модели).
  • Порядок items: day — по возрастанию даты; tenant/provider/model — по убыванию totalTokens.
  • total — итог по всем строкам.

Коды: 200; 400 { "detail": "Неизвестная группировка (day|tenant|provider|model)" }; 401.


GET /api/operator/analytics/activity

Лента действий (аудит) с фильтрами и пагинацией.

Query

Параметр Тип Обяз. Описание
eventType string нет тип события (см. каталог)
actorType enum нет tenant | operator | system
actorId uuid нет идентификатор актора
tenantId uuid нет тенант
from ISO-8601 нет нижняя граница at (включительно)
to ISO-8601 нет верхняя граница at (включительно)
limit int нет размер страницы (дефолт 100, кламп 1..500)
offset int нет смещение (≥0)

200

{
  "items": [
    {
      "eventType": "card_moved",
      "actorType": "tenant",
      "actorId": "1f2e3d4c-5b6a-7980-1234-56789abcdef0",
      "tenantId": "aabbccdd-eeff-0011-2233-445566778899",
      "userName": "owner@example.com",
      "tenantName": "ООО «Ромашка»",
      "ip": "203.0.113.7",
      "changes": [
        { "field": "cardId", "to": "c_1a2b3c4d5e6f" },
        { "field": "to", "to": "planned" }
      ],
      "detailJson": "{\"changes\":[{\"field\":\"cardId\",\"to\":\"c_1a2b3c4d5e6f\"},{\"field\":\"to\",\"to\":\"planned\"}]}",
      "at": "2026-09-10T15:22:46.123Z",
      "id": 1042
    }
  ],
  "total": 5012,
  "limit": 100,
  "offset": 0
}
  • items — новые сверху (at DESC). total — полное число по фильтру (без limit/offset).
  • userName — логин реального пользователя (владелец пространства либо логин попытки); null, если не разрешён.
  • tenantName — имя пространства события (join с реестром); null, если не разрешено.
  • changes — человекочитаемые изменения параметров события (см. «Детали события»); [], если деталей нет.
  • detailJsonстрока сырого JSON деталей события (без секретов) для спойлера; может быть null.

Детали события (changes)

Детали события хранятся как JSON вида { "changes": [ { "field": "<код>", "from": "<было>", "to": "<стало>" } ] }, где field — стабильный код параметра, from — предыдущее значение (null — параметр задан впервые), to — новое. Сериализация деталей — AuditService.ToDetailJson, форма изменения — AuditDetails.Set/ AuditDetails.Change. Ядро отдаёт changes как есть; человекочитаемые названия событий, акторов, параметров и значений — в ресурсах интерфейса (ru.js: operator.event/operator.actor/operator.field/operator.value). Прежние записи плоского формата ({ "login": "..." }, пары old*/new*) читаются обратно совместимо.

Коды: 200, 401.


Расширение GET /api/operator/audit

К прежним фильтрам (eventType, actorType, tenantId, from, to, limit) добавлены:

Параметр Тип Обяз. Описание
actorId uuid нет фильтр по идентификатору актора
offset int нет смещение страницы (≥0, дефолт 0)

Ответ — прежний { "items": [...], "total": n } (поля items/total без изменений; форма записи — как в ленте действий выше). Коды: 200, 401.


Операторские настройки: глобальные ключи Telegram

Ключи приложения Telegram (api_id/api_hash) задаются оператором глобально (ТЗ §4.1/§8.1), едины для всех тенантов. Тенант их не видит и не задаёт (ключ tgKeys удалён из GET/PATCH /api/settings). Хранилище — системная таблица public.global_settings (ключ telegramKeys), apiHash хранится зашифрованным и наружу не отдаётся.

GET /api/operator/settings/telegram-keys

Маскированный снимок глобальных ключей.

200

{
  "apiId": "1234567",
  "apiHash": "abcd…mnop",
  "keysSet": true
}
  • apiId — открыт (не секрет; пусто — ключи не заданы оператором).
  • apiHashмаска (пусто / x… / 1234…5678); открытый секрет не возвращается никогда.
  • keysSettrue, если заданы оба ключа; в GET /api/tg/status это же значение в поле keysSet.

Коды: 200, 401.

PUT /api/operator/settings/telegram-keys

Сохранение/смена глобальных ключей. Поля можно передавать по отдельности (частичное обновление): непереданное поле (null или отсутствие в JSON) сохраняет текущее значение. Если ключей ещё нет, оба поля обязательны.

Тело

{ "apiId": "1234567", "apiHash": "abcdefghijklmnop" }   // полное обновление
{ "apiId": "7654321" }                      // только apiId — apiHash сохраняется
{ "apiHash": "newsecrethash12" }            // только apiHash — apiId сохраняется
  • apiId — если передан, строго 5–9 цифр; если не передан, берётся текущий (null = «не менялось»).
  • apiHash — если передан, непустой секрет (не маска и без префикса enc:), шифруется перед сохранением; если не передан, берётся текущий зашифрованный секрет.
  • Явное пустое значение ("") считается невалидным, а не «не менялось».

200 — маскированный снимок (форма как у GET).

Ошибки

  • 400 { "detail": "Укажите api_id и api_hash" } — не передано ни одного поля.
  • 400 { "detail": "Ключи ещё не заданы — укажите и api_id, и api_hash" } — частичное обновление, но ключей ещё нет (нельзя дополнить отсутствующее значение).
  • 400 { "detail": "api_id должен состоять из 5–9 цифр" }
  • 400 { "detail": "Укажите непустой api_hash" }
  • 401 { "detail": "Требуется вход оператора" }

Аудит: событие telegram_keys_changed (актор operator, tenantId: null, детали {apiId, apiHashSet} — без секрета).

Примечание для вкладки 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

{
  "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 не переносятся от старого (дефолты каталога); ключ меняется только при явной передаче (маска не принимается).

Тело

{ "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} — без ключа).