Files
Deal/.superpowers/sdd/channel-discovery/task-6-report.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

9.6 KiB
Raw Blame History

Task 6 — Отчёт: воркер Discovery (поиск → оценка → авто-вступление)

Статус

Реализовано и проверено (py_compile + сборка образа + офлайн-прогон на временной БД с моками Telegram/пауз).

Файлы

  • Создан: backend/app/services/discovery_worker.pyasync def tick() -> dict + шаги.
  • Изменён: backend/app/main.py — фоновый цикл _discovery_loop (каждые 5 c: await tick(), исключения — log.exception) и запуск в lifespan в списке задач рядом с _pipeline_loop.

Что сделано

Структура tick() (одно действие за вызов, возврат {"action": ..., "taskId": ...})

Приоритеты (как в брифе + пожелание про стоп-кран): 0. ban_guard.global_paused() → сразу {"action": "none"} (стоп-кран останавливает весь tick).

  1. План выполнен (joined >= planJoins у любой running-задачи) → status='done' + лог done («план выполнен: вступили X из Y»). Идёт ДО поиска/оценки/join, чтобы задачу с выполненным планом не продолжать обрабатывать (и чтобы освободился бюджет планов).
  2. Шаг поиска: первая running-задача с searchDone=False → ключ keywords[searchIdx]tg.discovery_search → каждый результат discovery.add_candidate (kind нормализуется: канал/группа/чат → channel/group/forum) → advance_search → при переходе в searchDone лог search «поиск завершён: N кандидатов» (N — счётчик found). Паузы между поисками — внутри discovery_search. Пустые/съехавшие ключи закрываются без сетевого вызова.
  3. Шаг оценки: первый кандидат status='new':
    • discovery_infoparticipants, kind (forum, если is_forum; иначе маппинг RU-kind), обновляются name/username/hue;
    • minSubscribers>0: participants меньше → delete_candidate + лог skip «мало участников (X < min)»; participants не получены → метка «участники не подтверждены»;
    • discovery_read: ok=Falsereview + метка «канал: история недоступна» (channel) или «закрытая группа (история скрыта) — вступите сами» (group/forum); контент не оценивается, для ru-задач добавляется метка «язык не подтверждён»;
    • язык (только ru-задачи): Falsedelete_candidate + skip «язык не русский»; None → метка «язык не подтверждён»;
    • объём: содержательных <3 → review + метка «мало сообщений» (решает человек);
    • контент: не-форум — evaluate_sample по всей выборке, fitRatio общий; форум — group_by_topic, evaluate_sample по каждой теме, заполняется topics ({topicId, title, fitCount, total, fitRatio, passed}), вердикт — есть ≥1 проходная тема, fitRatio — агрегат fit из N по всей выборке; passed()review (метки/fitRatio/topics), иначе delete_candidate + skip «мало подходящих (X из N)»;
    • bump_counter(evaluated) при выходе кандидата из new (review или delete).
  4. Авто-вступление (отдельный проход, приоритет ниже оценки): задача autoJoin=True + кандидат review + ban_guard.can_auto_join() (иначе none):
    • повторная проверка «мы не состоим» прямым SQL по dialogs/disc_blacklist (интерфейсы Task 3 не менялись) — если уже вступили/в чёрном списке → mark_rejected + лог reject, action reject;
    • await ban_guard.wait_join_delay() (5070 с);
    • tg.discovery_join(username)mark_joined(auto=True)tg.add_dialog_monitored(dialogId, name, username, kind, hue);
    • FloodWaitErrorban_guard.note_flood() (идемпотентно) + лог flood; прочие ошибки → лог error (кандидат остаётся review для ретрая).

Возвращаемые action: search|review|skip|join|reject|flood|error|done|none. Метки/topics-контракт продублирован в docstring модуля.

Контракт меток и тем (в docstring discovery_worker.py)

  • marks — список строк: «участники не подтверждены», «язык не подтверждён», «канал: история недоступна», «закрытая группа (история скрыта) — вступите сами», «мало сообщений».
  • topics — список dict для форумов: {"topicId": str|int, "title": str, "fitCount": int, "total": int, "fitRatio": float, "passed": bool}. Общий вердикт форума — есть хотя бы одна проходная тема; fitRatio кандидата — агрегат по всей выборке; для не-форумов topics не заполняется.

Вывод проверок

  1. cd /c/telbase && python -m py_compile backend/app/services/discovery_worker.py backend/app/main.pyPY_COMPILE_OK (без ошибок).
  2. docker compose build appImage telbase-app Built (3 c, кэш).
  3. Офлайн-сценарий в контейнере на временной БД (LEADRADAR_DATA=/tmp/lr_w6, MSYS_NO_PATHCONV=1, --entrypoint sh), Telegram-методы и wait_join_delay замоканы, оценка — реальная (эвристика: aiEnabled/mlEnabled=False):
SEARCH_OK            # пустая система → none; поиск: кандидат добавлен, «уже мониторится» пропущен, «поиск завершён: 1 кандидатов»
EVAL_REVIEW_OK        # оценка: review, fitRatio 0.75, langRu=True
EVAL_LANG_SKIP_OK     # язык не русский → delete + skip
EVAL_FORUM_OK         # форум: kind=forum, topics по темам (passed/нет), fitRatio агрегат
EVAL_FEW_OK           # <3 сообщений → review + «мало сообщений»
EVAL_NO_HISTORY_OK    # история недоступна → review + «канал: история недоступна» + «язык не подтверждён»
AUTOJOIN_DONE_OK      # join → joined/autoJoined + dialogs(monitor) + join_auto; план → done + лог done
REJECT_RECHECK_OK     # «вступили между оценкой и join» → mark_rejected + reject (без join)
TASK6_OFFLINE_OK
  1. Диагностика discovery_worker.py — без ошибок и предупреждений (ruff I/SIM/default — чисто; импорты/_we_are_in/suppress приведены к правилам).

Concerns

  1. Полный прогон на живом аккаунте — Task 10 вручную. Ветки flood и error (реальные FloodWaitError/сетевые ошибки join) офлайн не воспроизводятся — только код-ревью и логика Task 4 (discovery_join сам фиксирует flood и пробрасывает).
  2. Ветка «повторная проверка перед join» использует mark_rejected, который по контракту Task 3 добавляет источник в disc_blacklist — даже когда «уже вступили между оценкой и join». Для поиска это безвредно (источник и так отсекается по dialogs), но чёрный список формально пополняется. Если это нежелательно — можно ввести отдельный helper (интерфейсы Task 3 не менялись).
  3. minSubscribers-ветка «участники не подтверждены» помечает кандидата только когда минимум задан (minSubscribers>0); при min=0 отсутствие participants не метка (участники не критерий).
  4. Стоп-кран discPaused (ban_guard.global_paused()) останавливает весь tick — включая поиск и оценку, не только авто-join (по требованию задания).
  5. Running-задача без работы (поиск завершён, кандидатов нет, autoJoin=False) остаётся running и даёт {"action":"none"} каждые 5 c — завершение/удаление такой задачи за пользователем (по брифу).
  6. fitRatio форума — агрегат по всей выборке (fit из N), а не максимум темы; вердикт форума — «есть ≥1 проходная тема». Формат зафиксирован в docstring и в этом отчёте (UI Task 9 показывает темы с per-topic X из N).