### Task 6: Воркер Discovery (поиск → оценка → авто-вступление) **Files:** - Create: `backend/app/services/discovery_worker.py` - Modify: `backend/app/main.py` (фоновый цикл `_discovery_loop`, каждые 5 c) **Interfaces:** - Consumes: `discovery` (Task 3), `tg.discovery_*` (Task 4), `discovery_eval` (Task 5), `ban_guard` (Task 2). - Produces: `async def tick() -> dict` — выполняет ОДНО действие и возвращает `{"action": ..., "taskId": ...}` (или `{"action": "none"}`). Логика tick (по одной задаче за вызов, начиная с самой старой running): 1. Если задача `search_done=False`: взять ключ `keywords[search_idx]`, вызвать `tg.discovery_search`; для каждого результата `discovery.add_candidate`; `discovery.advance_search(task_id)`; если `search_done` стал True — лог `search` «поиск завершён: N кандидатов». Возврат. 2. Иначе взять первого кандидата статуса `new` задачи: - `info = tg.discovery_info`; `participants`, `kind` (forum если `is_forum`); при заданном `min_subscribers` и participants НЕ None и меньше минимума — `set_candidate_status(...)` нет: просто `discovery.delete_candidate` + лог `skip`; если participants None — метка «участники не подтверждены» (идём дальше). - `read = tg.discovery_read(dialog_id, sample_size)`. - Если `read.ok=False` (история недоступна без членства): kind==channel → `review` с меткой «канал: история недоступна»; группа/форум → `review` с меткой «закрытая группа (история скрыта) — вступите сами»; оценка контента не производится, неподтверждённые фильтры помечаются. - Язык: если прочитано и `task.lang=='ru'`: `lang_ru=detect_lang_ru(...)`; False → удалить кандидата, лог `skip` «язык не русский»; None → метка «язык не подтверждён». - Оценка: `evaluate_sample`; `passed` → метки topics/fit → `review` + лог `review`; иначе удалить кандидата, лог `skip` «мало подходящих (X из N)». 3. Авто-вступление (отдельный проход tick, приоритет ниже оценки): если у running-задачи `auto_join` и есть кандидат `review` и `ban_guard.can_auto_join()`: - повторная проверка «мы не состоим» (`dialogs`/blacklist) → если вступили уже → `mark_rejected` с логом; - `await ban_guard.wait_join_delay()` (рандом 50–70 с — спейсинг авто-вступлений; ручные join из API паузу не делают); - `tg.discovery_join(username)` → `discovery.mark_joined(dialog_id, auto=True)` → `tg.add_dialog_monitored(...)`; при FloodWaitError → `ban_guard.note_flood()` + лог `flood`. 4. Если `task.joined >= task.plan_joins` → статус `done`, лог `done`. - [ ] **Step 1: Реализовать** `discovery_worker.py` и цикл в `main.py`. - [ ] **Step 2: Проверить компиляцию** и запуск без падений (воркер с пустыми таблицами делает `none`). Полный прогон — Task 10 вручную. ---