Files
Deal/docs/superpowers/specs/2026-09-04-channel-discovery-design.md
T
stepan 45065d3202
ci / build-test (push) Successful in 2m44s
ci / build-test (pull_request) Successful in 2m46s
Восстановить docs/ как зеркало для агентов (ревью МР #11)
Ревью rust: перенос в вики не должен удалять из репозитория то, что нужно
агенту для работы (бэклог, статус, планы, код-стайл, спеки). Дублирование
вики и репы разрешено и обязательно: вики — актуальные версии для людей,
docs/ — зеркало для контекста агентов. README разведён по ролям.
2026-09-13 00:03:43 +03:00

213 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Поиск и подключение каналов (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.