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 зелёные.
61 lines
9.6 KiB
Markdown
61 lines
9.6 KiB
Markdown
# Task 6 — Отчёт: воркер Discovery (поиск → оценка → авто-вступление)
|
||
|
||
## Статус
|
||
✅ Реализовано и проверено (py_compile + сборка образа + офлайн-прогон на временной БД с моками Telegram/пауз).
|
||
|
||
## Файлы
|
||
- Создан: `backend/app/services/discovery_worker.py` — `async def tick() -> dict` + шаги.
|
||
- Изменён: `backend/app/main.py` — фоновый цикл `_discovery_loop` (каждые 5 c: `await tick()`, исключения — `log.exception`) и запуск в lifespan в списке задач рядом с `_pipeline_loop`.
|
||
|
||
## Что сделано
|
||
|
||
### Структура `tick()` (одно действие за вызов, возврат `{"action": ..., "taskId": ...}`)
|
||
Приоритеты (как в брифе + пожелание про стоп-кран):
|
||
0. `ban_guard.global_paused()` → сразу `{"action": "none"}` (стоп-кран останавливает весь tick).
|
||
1. **План выполнен** (`joined >= planJoins` у любой running-задачи) → `status='done'` + лог `done` («план выполнен: вступили X из Y»). Идёт ДО поиска/оценки/join, чтобы задачу с выполненным планом не продолжать обрабатывать (и чтобы освободился бюджет планов).
|
||
2. **Шаг поиска**: первая running-задача с `searchDone=False` → ключ `keywords[searchIdx]` → `tg.discovery_search` → каждый результат `discovery.add_candidate` (kind нормализуется: `канал/группа/чат → channel/group/forum`) → `advance_search` → при переходе в `searchDone` лог `search` «поиск завершён: N кандидатов» (N — счётчик `found`). Паузы между поисками — внутри `discovery_search`. Пустые/съехавшие ключи закрываются без сетевого вызова.
|
||
3. **Шаг оценки**: первый кандидат `status='new'`:
|
||
- `discovery_info` → `participants`, `kind` (`forum`, если `is_forum`; иначе маппинг RU-kind), обновляются name/username/hue;
|
||
- `minSubscribers`>0: participants меньше → `delete_candidate` + лог `skip` «мало участников (X < min)»; participants не получены → метка «участники не подтверждены»;
|
||
- `discovery_read`: `ok=False` → `review` + метка «канал: история недоступна» (channel) или «закрытая группа (история скрыта) — вступите сами» (group/forum); контент не оценивается, для ru-задач добавляется метка «язык не подтверждён»;
|
||
- язык (только ru-задачи): `False` → `delete_candidate` + skip «язык не русский»; `None` → метка «язык не подтверждён»;
|
||
- объём: содержательных <3 → `review` + метка «мало сообщений» (решает человек);
|
||
- контент: не-форум — `evaluate_sample` по всей выборке, `fitRatio` общий; форум — `group_by_topic`, `evaluate_sample` по каждой теме, заполняется `topics` (`{topicId, title, fitCount, total, fitRatio, passed}`), вердикт — есть ≥1 проходная тема, `fitRatio` — агрегат fit из N по всей выборке; `passed()` → `review` (метки/fitRatio/topics), иначе `delete_candidate` + skip «мало подходящих (X из N)»;
|
||
- `bump_counter(evaluated)` при выходе кандидата из `new` (review или delete).
|
||
4. **Авто-вступление** (отдельный проход, приоритет ниже оценки): задача `autoJoin=True` + кандидат `review` + `ban_guard.can_auto_join()` (иначе `none`):
|
||
- повторная проверка «мы не состоим» прямым SQL по `dialogs`/`disc_blacklist` (интерфейсы Task 3 не менялись) — если уже вступили/в чёрном списке → `mark_rejected` + лог `reject`, action `reject`;
|
||
- `await ban_guard.wait_join_delay()` (50–70 с);
|
||
- `tg.discovery_join(username)` → `mark_joined(auto=True)` → `tg.add_dialog_monitored(dialogId, name, username, kind, hue)`;
|
||
- `FloodWaitError` → `ban_guard.note_flood()` (идемпотентно) + лог `flood`; прочие ошибки → лог `error` (кандидат остаётся `review` для ретрая).
|
||
|
||
Возвращаемые action: `search|review|skip|join|reject|flood|error|done|none`. Метки/topics-контракт продублирован в docstring модуля.
|
||
|
||
### Контракт меток и тем (в docstring `discovery_worker.py`)
|
||
- `marks` — список строк: «участники не подтверждены», «язык не подтверждён», «канал: история недоступна», «закрытая группа (история скрыта) — вступите сами», «мало сообщений».
|
||
- `topics` — список dict для форумов: `{"topicId": str|int, "title": str, "fitCount": int, "total": int, "fitRatio": float, "passed": bool}`. Общий вердикт форума — есть хотя бы одна проходная тема; `fitRatio` кандидата — агрегат по всей выборке; для не-форумов `topics` не заполняется.
|
||
|
||
## Вывод проверок
|
||
1. `cd /c/telbase && python -m py_compile backend/app/services/discovery_worker.py backend/app/main.py` → `PY_COMPILE_OK` (без ошибок).
|
||
2. `docker compose build app` → `Image telbase-app Built` (3 c, кэш).
|
||
3. Офлайн-сценарий в контейнере на временной БД (`LEADRADAR_DATA=/tmp/lr_w6`, `MSYS_NO_PATHCONV=1`, `--entrypoint sh`), Telegram-методы и `wait_join_delay` замоканы, оценка — реальная (эвристика: `aiEnabled/mlEnabled=False`):
|
||
```
|
||
SEARCH_OK # пустая система → none; поиск: кандидат добавлен, «уже мониторится» пропущен, «поиск завершён: 1 кандидатов»
|
||
EVAL_REVIEW_OK # оценка: review, fitRatio 0.75, langRu=True
|
||
EVAL_LANG_SKIP_OK # язык не русский → delete + skip
|
||
EVAL_FORUM_OK # форум: kind=forum, topics по темам (passed/нет), fitRatio агрегат
|
||
EVAL_FEW_OK # <3 сообщений → review + «мало сообщений»
|
||
EVAL_NO_HISTORY_OK # история недоступна → review + «канал: история недоступна» + «язык не подтверждён»
|
||
AUTOJOIN_DONE_OK # join → joined/autoJoined + dialogs(monitor) + join_auto; план → done + лог done
|
||
REJECT_RECHECK_OK # «вступили между оценкой и join» → mark_rejected + reject (без join)
|
||
TASK6_OFFLINE_OK
|
||
```
|
||
4. Диагностика `discovery_worker.py` — без ошибок и предупреждений (ruff I/SIM/default — чисто; импорты/`_we_are_in`/`suppress` приведены к правилам).
|
||
|
||
## Concerns
|
||
1. Полный прогон на живом аккаунте — Task 10 вручную. Ветки `flood` и `error` (реальные FloodWaitError/сетевые ошибки join) офлайн не воспроизводятся — только код-ревью и логика Task 4 (`discovery_join` сам фиксирует flood и пробрасывает).
|
||
2. Ветка «повторная проверка перед join» использует `mark_rejected`, который по контракту Task 3 добавляет источник в `disc_blacklist` — даже когда «уже вступили между оценкой и join». Для поиска это безвредно (источник и так отсекается по `dialogs`), но чёрный список формально пополняется. Если это нежелательно — можно ввести отдельный helper (интерфейсы Task 3 не менялись).
|
||
3. `minSubscribers`-ветка «участники не подтверждены» помечает кандидата только когда минимум задан (`minSubscribers>0`); при `min=0` отсутствие participants не метка (участники не критерий).
|
||
4. Стоп-кран `discPaused` (`ban_guard.global_paused()`) останавливает весь tick — включая поиск и оценку, не только авто-join (по требованию задания).
|
||
5. Running-задача без работы (поиск завершён, кандидатов нет, `autoJoin=False`) остаётся running и даёт `{"action":"none"}` каждые 5 c — завершение/удаление такой задачи за пользователем (по брифу).
|
||
6. `fitRatio` форума — агрегат по всей выборке (fit из N), а не максимум темы; вердикт форума — «есть ≥1 проходная тема». Формат зафиксирован в docstring и в этом отчёте (UI Task 9 показывает темы с per-topic X из N).
|