Deal — единая кодовая база
ci / build-test (push) Canceled after 0s

SaaS-мониторинг Telegram: ядро (модули Cards/Kanban/Pipeline/Tenants/Settings/
Discovery, Api, Infrastructure), сервисы telegram/ai/ml/storage, фронт Vue,
контракты и grpc-hosting, деплой-конфиги (dev/prod/observability/CI-раннер),
Gitea Actions CI, документация (ТЗ, техдок, api-map, код-стайл, планы, бэклог).

Текущее состояние: все этапы роадмапа 0–12 закрыты, сборка 5 sln 0/0,
тесты 1340/130/52/38/9 зелёные.
This commit is contained in:
Rustam Khalimov
2026-09-11 23:56:47 +03:00
commit 27c7831910
1383 changed files with 158436 additions and 0 deletions
@@ -0,0 +1,52 @@
# Task 7 — Отчёт: API Discovery
## Статус
Выполнено. Роутер `/api/discovery` реализован, зарегистрирован в `main.py`, проверен на живом контейнере.
## Файлы
- Создан: `backend/app/routers/discovery_routes.py` (prefix `/api/discovery`, tags `discovery`, auth `current_login`).
- Изменён: `backend/app/main.py` — импорт `discovery_routes` и добавление в цикл `include_router`.
## Что сделано
### Эндпоинты
- `GET /tasks`, `POST /tasks`, `PATCH /tasks/{id}`, `DELETE /tasks/{id}`;
- `POST /tasks/{id}/start`, `POST /tasks/{id}/pause`;
- `POST /tasks/{id}/generate-keywords` — ИИ-генерация ключей по описанию задачи;
- `GET /tasks/{id}/candidates?status=new|review|joined|rejected` (необязателен; невалидный статус — 422 через `Literal`);
- `POST /candidates/{dialog_id}/join` — ручное вступление (вне квот/пауз воркера);
- `POST /candidates/{dialog_id}/reject` — отклонение с добавлением в чёрный список;
- `GET /blacklist`, `DELETE /blacklist/{dialog_id}`;
- `GET /tasks/{id}/log`.
### Модели и контракт
- `TaskCreate` / `TaskPatch` — camelCase-поля без алиасов (как `PreviewBody` в `tg_routes`), необязательные поля `None` исключаются через `model_dump(exclude_none=True)`, чтобы `create_task`/`patch_task` сами подставляли дефолты (в т.ч. `threshold`/`sampleSize` из настроек).
- `POST /tasks` отдаёт созданную задачу как есть (200); списки — `{"items": [...]}`; delete — `{"ok": true}`.
- Обработка: `ValueError` → 400, `KeyError` → 404; для отсутствующих задачи/кандидата — явные 404-хелперы (`_task_or_404`, `_candidate_or_404`, чтение кандидата через `store: SELECT * FROM disc_candidates WHERE dialog_id=?` как в брифе).
- `generate-keywords`: если `!aiEnabled` или у активного провайдера нет ключа (и не local) → `{"keywords": [], "error": "..."}` с HTTP 200; иначе `ai_service.chat_json(промпт RU+EN 1016, user=описание)``{"keywords": [...]}` (чистка: строки, без пустых/длинных/повторов, страховочный лимит 30); ошибка провайдера → `{"keywords": [], "error": str}`. Пустое описание → error-ветка.
- `join`: статус `joined` → 400; `tg.discovery_join(username)``tg.add_dialog_monitored(...)``discovery.mark_joined(auto=False)`; ошибка Telegram → 400 с текстом.
- `reject`: статус `joined` → 400 «Уже вступили — удалите источник из каналов»; иначе `mark_rejected(reason="отклонено вручную")`.
- Роутер чисто проходит `ruff check` и `py_compile`.
## Вывод проверок
1. `cd /c/telbase && python -m py_compile backend/app/routers/discovery_routes.py backend/app/main.py``PY_COMPILE_OK`; `ruff check backend/app/routers/discovery_routes.py` → clean.
2. `docker compose build app``Image telbase-app Built`; `docker compose up -d app` → контейнер пересоздан, `/api/health` → 200.
3. Живой API (логин admin/admin, куки):
- `POST /api/discovery/tasks {"name":"","planJoins":1}`**400** `{"detail":"Укажите название задачи"}`;
- `POST /api/discovery/tasks` корректная (plan=1, ключи пустые) → **200**, задача `status:"draft"`, дефолты `threshold:40/sampleSize:10` подставлены;
- `GET /api/discovery/tasks`**200** `{"items":[задача]}`;
- `GET /api/discovery/tasks/{id}/candidates?status=review`**200** `{"items":[]}`; `GET /api/discovery/tasks/{id}/log`**200** `{"items":[]}`; `GET /api/discovery/blacklist`**200** `{"items":[]}`;
- `POST /api/discovery/tasks/{id}/generate-keywords`**200** (в этом окружении ключ ИИ настроен и `aiEnabled=true`) → реальный вызов провайдера, ответ `{"keywords":[14 строк RU+EN]}` (happy path);
- error-ветка: временно `PATCH /api/settings {"aiEnabled":false}``generate-keywords`**200** `{"keywords":[],"error":"ИИ выключен в настройках (aiEnabled)"}`; настройка возвращена в `true`;
- `POST /api/discovery/tasks/{id}/start` при пустых ключах → **400** `{"detail":"Нет ключевых слов для поиска — добавьте их в задачу"}`;
- `PATCH /api/discovery/tasks/{id}` (name+keywords) → **200**, поля обновлены; `POST .../pause`**200** `status:"paused"`;
- `DELETE /api/discovery/tasks/{id}`**200** `{"ok":true}`; повторный `GET /tasks`**200** `{"items":[]}`;
- `start`/`PATCH`/`candidates` по несуществующей задаче → **404** `{"detail":"Задача не найдена"}`;
- `POST /api/discovery/candidates/{id}/join` и `/reject` по несуществующему кандидату → **404** `{"detail":"Кандидат не найден"}`;
- `GET /api/discovery/tasks/{id}/candidates?status=bogus`**422** (валидация `Literal`);
- `DELETE /api/discovery/blacklist/{id}` (нет записи) → **200** `{"ok":true}`.
## Concerns
1. Ветки `join`/`reject` с реальным кандидатом и реальным `tg.discovery_join` (в т.ч. «уже joined» → 400 и ошибка Telegram → 400) живьём не гонялись — нужен подключённый Telegram-аккаунт и настоящий кандидат; это ручная проверка уровня Task 10. Контрактные 404/422 проверены.
2. `generate-keywords` в проверке реально дёрнул настроенного провайдера (сетевой вызов). Error-ветка проверена переключением `aiEnabled`; ветка «ключ не задан» воспроизводится так же, но отдельно не гонялась, чтобы не трогать `aiConfigs`.
3. В `main.py` остались pre-existing предупреждения ruff (не связаны с задачей): неиспользуемый импорт `pathlib.Path` (F401) и серия `# noqa: BLE001` на голых `except Exception:` без `as` (RUF100 — ruff не считает BLE001 срабатывающим на таких обработчиках). Не правил: файл вне объёма, `py_compile` чист.
4. Под Windows/MSYS кириллица в `curl -d '...'` ломает тело запроса («There was an error parsing the body») — проверки с кириллицей делались через `--data @файл` (UTF-8). К продакшену отношения не имеет.