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 зелёные.
53 lines
7.1 KiB
Markdown
53 lines
7.1 KiB
Markdown
# 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 10–16, 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). К продакшену отношения не имеет.
|