# Поиск и подключение каналов (Discovery) — дизайн > Исторический документ (дизайн Discovery прототипа LeadRadar, 2026-09-04). Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`. Дата: 2026-09-04 Статус: согласован с пользователем (правки от 2026-09-04 учтены) ## 1. Цель Пользователь даёт системе «задание»: найти Telegram-каналы и группы, в которых мы ещё **не состоим**, по описанию цели (например, «вакансии и фриланс для разработки») и подключить их к мониторингу. Система сама ищет кандидатов, оценивает их (по метаданным, языку и содержанию сообщений) и показывает человеку список «на рассмотрение»; человек решает — вступить и мониторить или отклонить. Возможен режим авто-вступления в рамках суточных квот и с паузами против бана. Ключевое правило: **источники, в которых мы уже состоим (вступили/мониторим), исключаются сразу и безусловно — независимо от запроса, ключей и настроек задачи.** Это глобальное правило системы: действует на всех этапах (поиск → оценка → вступление) и для всех задач. ## 2. Ограничения Telegram API (факты, на которых строится дизайн) 1. Глобального «поиска по критериям» в API нет. `contacts.search(q)` возвращает публичные каналы/группы/боты по **имени/username/запросу** — без фильтров по участникам, языку и содержимому. Всю дальнейшую фильтрацию делаем сами. 2. Число участников/описание — через `channels.getFullChannel`. Для публичных каналов доступно без вступления; для групп часто доступно только членам. 3. Чтение истории без вступления: публичные **каналы** — обычно можно; публичные **группы** — только если история открыта; иначе — только членам. 4. Массовый поиск/чтение/вступления с юзер-аккаунта ограничены эмпирически — нужны квоты, паузы и обработка `FloodWaitError`. ## 3. Понятия - **Задача (task)** — конфиг поиска: описание цели, ключи, фильтры, план, режим авто-вступления, статус/счётчики. Задач может быть несколько. - **Кандидат (candidate)** — найденный источник (канал/группа, для форумов — оценка по темам). Проходит стадии: `new → evaluated → review → joined | rejected`. - **Метки кандидата** — человекочитаемые пометки: «закрытая группа/канал», «форум», «не прочитано», «участники не подтверждены», «язык не подтверждён», «есть проходные темы». - **Чёрный список** — источники, отклонённые пользователем; поиск их больше не возвращает (снимается вручную). - **BanGuard** — единый менеджер квот и пауз для всех действий discovery (search/read/join/leave), общий для задач. ## 4. Задача: конфигурация и правила создания Поля задачи: | Поле | Назначение | По умолчанию | | --- | --- | --- | | `name` | название задачи | — | | `description` | описание цели (что ищем) | — | | `keywords` | поисковые ключи (генерирует ИИ, редактируются перед стартом) | [] | | `minSubscribers` | минимум участников (0 = не важно) | 0 | | `lang` | язык источников (`ru` / `any`) | `ru` | | `threshold` | доля подходящих сообщений, % | 40 | | `sampleSize` | сколько сообщений смотреть при оценке | 10 | | `planJoins` | план вступлений N | 1..50 | | `autoJoin` | авто-вступление подходящих | false | | `status` | `draft → running → paused → done | failed` | draft | Правила создания: - **Бюджет планов:** сумма `planJoins` всех задач в статусе не `done/failed` + `planJoins` новой ≤ суточного лимита вступлений (по умолчанию 50). Задача с планом 50 не даёт создать другую; план 25 оставляет максимум 25. - Запуск возможен только после генерации/подтверждения ключей. - При редактировании активной задачи план нельзя увеличить сверх свободного бюджета. ## 5. Пайплайн поиска (каскад фильтров) Выполняется фоновым воркером задачи строго через BanGuard (по одному действию, с паузами). Для каждого кандидата фильтры идут **по нарастающей стоимости**; при первом «нет» источник пропускается и берётся следующий: 1. **Поиск** — `contacts.search` по каждому ключу (с паузами). Кандидаты дедуплицируются по `dialog_id`/username. 2. **«Мы не состоим» — глобальный фильтр, применяется сразу и безусловно:** как только источник найден (независимо от запроса/ключей), он отбрасывается, если уже есть в `dialogs` (вступили/мониторим), в чёрном списке или уже обрабатывается/вступил/ждёт рассмотрения в другой задаче (глобальная дедупликация кандидатов). Остальные фильтры (участники/язык/контент) применяются уже после этого. Проверка повторяется непосредственно перед вступлением (между оценкой и join'ом кандидат мог быть добавлен вручную). 3. **Число участников** — если `minSubscribers` задано: - значение получено и меньше минимума → пропуск; - значение получить не удалось → **не пропускаем**, ставим метку «участники не подтверждены». 4. **Язык** — если `lang=ru`: по выборке сообщений эвристикой кириллицы (без ИИ); не удалось прочитать → метка «язык не подтверждён» (не пропуск). 5. **Содержимое** — оценка выборки сообщений (см. §6). Пометки «не подтверждено» — не ошибка, а сигнал человеку на экране рассмотрения. ## 6. Оценка содержимого (по темам, для форумов) - **Что считается «подходящим сообщением»:** сообщение проходит те же правила, что в основной системе (этап 1 → ML → ИИ), но **профиль оценки = профиль задачи** (описание + ключи задачи), а не глобальные настройки дашборда. Оценка ничего не создаёт: ни карточек, ни очереди, ни обучения ML. - Если ИИ выключен — оценка локальным разбором/ML. - **Каналы:** читаем до `sampleSize` последних сообщений; доля подходящих ≥ `threshold` → в «на рассмотрение». - **Открытые группы:** то же; чтение не удалось → «на рассмотрение» с меткой «открытая группа, не прочитана». - **Закрытые группы** (нашлись по ключам, история скрыта): сразу «на рассмотрение» с меткой «закрытая группа/канал» (+ метки неподтверждённых фильтров). Пользователь вступает сам. - **Форумы (группы с темами):** группа раскладывается по темам (`reply_to_top_id`): читаем выборку по активным темам, оценка считается **по темам** («тема: подходит X из N»). Группа подходящая, если есть ≥1 проходная тема. В превью — список тем с пометками проходная/нет. Имена тем, если API не отдаёт без членства, подставляем сниппетом первого сообщения темы. - Порог «40%» применяется к сообщениям темы/канала; если в выборке меньше 3 содержательных сообщений — кандидат идёт «на рассмотрение» с меткой «мало сообщений». ## 7. «На рассмотрение» и действия человека Экран по задаче содержит списки: **В обработке / На рассмотрении / Вступили / Отклонены**, плюс история. Кандидат на рассмотрении показывает: тип (канал/группа/форум), число участников, метки, долю «подходит X из N» и **почему подошло** (перечень подходящих сообщений/тем с причинами — как блок «попала по фильтру» в карточках), превью сообщений (для форумов — по темам). Действия: - **«Вступить и мониторить»** — `channels.joinChannel` (по username), добавление в `dialogs` с `monitor=1`, backfill последних ~10 сообщений. Ручной клик — **вне квот**. После вступления источник автоматически попадает под правило «мы состоим» и из поиска исключается. - **«Отклонить»** — источник в чёрный список (исключается из поиска во всех задачах). Если для оценки пришлось вступать — выходим (`channels.leaveChannel`) в рамках квот. Чёрный список редактируется вручную (можно снять). - **Закрытые группы:** вместо авто-вступления — кнопка-ссылка `t.me/`; система замечает вступление при синхронизации диалогов и предлагает добавить источник в мониторинг (метка «вступили, добавить в мониторинг?»). ## 8. Авто-вступление, квоты и анти-бан (BanGuard) - Суточный лимит вступлений — **50** (настройка), общий для всех задач, считаются только автоматические вступления. Ручные — без ограничений. - Авто-вступление включается на задачу (`autoJoin`). Подходящие кандидаты вступают сами. - Интервалы между автоматическими вступлениями: **случайно 50–70 секунд**; по одному действию, без параллелей. Поиск и чтение — мягкие паузы (единицы секунд + джиттер, переиспользуем значения анти-бана из telegram.py). - Задача «выполнена» при достижении плана вступлений. Если за сутки упёрлись в общий бюджет — авто-режим продолжает на следующий день (новый суточный бюджет). - `FloodWaitError` → пауза по секундам из ответа + запас; авто-вступления останавливаются до следующего дня при флуде. Общий «стоп-кран» — пауза всего discovery. - Все квоты/интервалы — настройки в UI. ## 9. Хранилище | Таблица | Назначение / ключевые поля | | --- | --- | | `disc_tasks` | задачи: name, description, keywords(JSON), min_subscribers, lang, threshold, sample_size, plan_joins, auto_join, status, counters (found/evaluated/joined/rejected), created/updated | | `disc_candidates` | dialog_id/username/name/kind(channel|group|forum)/hue, participants, lang_ru, join_failures, marks(JSON), topics(JSON: {topicId,title,fitCount,total,fitRatio,passed}), fit_ratio, status(new/review/joined/rejected), task_id, times | | `disc_blacklist` | dialog_id, name, reason, created_at | | `disc_log` | история задачи: task_id, event(search/evaluate/join/leave/flood/error/review/blacklist), text, created_at | Дубли кандидатов не создаются; источник, попавший в другую задачу или `dialogs`, из поиска исключается (правило «мы состоим» — глобальное). Реализация: у кандидата нет транзитного статуса `evaluated` (счётчик оценённых — на задаче); `join_failures` — неудачные авто-вступления подряд, после 3 кандидат удаляется. ## 10. API - `GET/POST/PATCH/DELETE /api/discovery/tasks` (создание с валидацией бюджета планов), `POST /api/discovery/tasks/{id}/start|pause` - `POST /api/discovery/tasks/{id}/generate-keywords` — ИИ генерирует ключи по описанию - `GET /api/discovery/tasks/{id}/candidates?status=review|joined|rejected` - `POST /api/discovery/candidates/{id}/join` (вступить и мониторить), `.../reject` - `GET /api/discovery/blacklist`, `DELETE /api/discovery/blacklist/{dialog_id}` - `GET /api/discovery/tasks/{id}/log` ## 11. UI Подвкладка **«Поиск»** на экране «Каналы»: - список задач (статус, прогресс, план/вступили, авто-режим) + «Новая задача»; - мастер задачи: описание → «Сгенерировать ключи ИИ» → редактирование ключей → фильтры/план/авто-режим → запуск; - по задаче: статус-лента (поиск → оценка → вступление), вкладки «В обработке / На рассмотрении / Вступили / Отклонены», история; - кандидат на рассмотрении раскрывается с превью и действиями; форум — по темам; - настройки квот (лимит/интервалы) — в том же экране или «Настройки → Telegram». ## 12. Интеграция с существующим кодом - Фоновый воркер discovery — отдельный цикл в `main.py` (как `_pipeline_loop`), сервис `app/services/discovery.py`, Telegram-действия — методы `TelegramManager` (поиск/join/leave/чтение) с общим pacing. - Переиспользуем: `rules/stage1/ML/AI` для оценки сообщений (новый лёгкий вызов с профилем задачи, без записи карточек), список `dialogs` для фильтра «мы состоим», синхронизацию диалогов для авто-добавления закрытых групп. - К основному пайплайну карточек, ML и ТЗ-логике не прикасаемся. ## 13. Вне рамок (сейчас) - Агрегаторы-каталоги как источник кандидатов. - «Похожие каналы» (`getChannelRecommendations`) от наших подписок — отдельная опция позже. - Авто-вступление в закрытые группы по инвайт-ссылкам (глобальным поиском они не находятся). ## 14. Значения по умолчанию (настраиваются в UI) суточный лимит вступлений = 50; интервал авто-вступлений = 50–70 с; выборка = 10 сообщений; порог = 40%; мин. содержательных сообщений для оценки = 3; язык = ru.