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 зелёные.
213 lines
18 KiB
Markdown
213 lines
18 KiB
Markdown
# Поиск и подключение каналов (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/<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|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.
|