Files
Deal/docs/superpowers/specs/2026-09-04-channel-discovery-design.md
T
Rustam Khalimov 27c7831910
ci / build-test (push) Canceled after 0s
Deal — единая кодовая база
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 зелёные.
2026-09-11 23:56:47 +03:00

18 KiB
Raw Blame History

Поиск и подключение каналов (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`

Правила создания:

  • Бюджет планов: сумма 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/<username>; система замечает вступление при синхронизации диалогов и предлагает добавить источник в мониторинг (метка «вступили, добавить в мониторинг?»).

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
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.