Files
avandClaude Opus 4.8 f75d6b1f91 tgbot: выбор кандидата метабазы inline-кнопками в карточке ревью
Когда для распознавания сохранены кандидаты метабазы, карточка подтверждения
бота показывает кнопку «🗂 База (N)». По ней двухшагово (как delete/dismiss)
разворачивается список кандидатов inline-кнопками; выбор пиннит источник через
worker.ChooseCandidate (ручной матч, без авто-раскладки) и обновляет карточку.
Веб остаётся точкой точных правок (ручной ввод id/URL, «без базы»).

Безопасность границы: id кандидата из callback_data валидируется как ULID
(ident.Parse) до доменного вызова, как в вебе. Текст inline-кнопок Telegram не
парсится как HTML — название кандидата в подписи не экранируется.

SDD: change telegram-vybor-nahodok — дельта notifications (ADDED «Выбор
кандидата метабазы из карточки подтверждения бота») + review (MODIFIED
«Разделение труда транспортов»: быстрый выбор кандидата — Telegram-действие).
Влито в specs, change заархивирован. Миграций БД нет (кандидаты уже в БД).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 16:02:47 +03:00

17 KiB
Raw Permalink Blame History

notifications Specification

Purpose

Уведомление автора загрузки о значимых событиях: падения (failed/stuck, включая приёмный qbit_add мимо поллинга) с дебаунсом повторов, приглашение в review и готовность, рассинхрон (orphaned/target_missing). Единое место доставки пингов, над которым транспорты (Telegram и др.) — тонкие адаптеры.

Requirements

Requirement: Уведомление о падении загрузки

Любой переход загрузки в failed/stuck система SHALL сопровождать уведомлением автора загрузки через настроенный механизм (notifier), чтобы падение не оставалось незамеченным. Это SHALL включать приёмное падение qbit_add (не удалось добавить раздачу в qBittorrent), которое идёт мимо поллинг-цикла worker.

Scenario: Уведомление при падении приёма

  • GIVEN приём загрузки, где добавление в qBittorrent не удалось
  • WHEN загрузка помечается failed с error_code qbit_add
  • THEN автор загрузки получает уведомление о падении

Requirement: Дебаунс повторных падений

Повторные падения одной задачи в пределах окна дебаунса система SHALL уведомлять лишь один раз, чтобы мерцающий stalled-торрент (stuckdownloading) не спамил автора.

Scenario: Мерцающий stalled не спамит

  • GIVEN задача, многократно переходящая stuckdownloading в пределах окна дебаунса
  • WHEN происходят повторные падения
  • THEN уведомление отправляется один раз за окно

Requirement: Пинг о входе в review и готовности

При переходе загрузки в review система SHALL пинговать автора (сообщение в Telegram / бейдж в вебе) — пользователя зовут, а не он опрашивает. После успешного применения (готовность) система SHALL показывать, что создано.

Scenario: Пинг при входе в review

  • GIVEN загрузка переходит в review
  • WHEN происходит переход
  • THEN автор получает пинг с приглашением подтвердить раскладку

Requirement: Уведомление о рассинхроне

При переходе задачи в orphaned или target_missing система SHALL уведомлять автора загрузки через настроенный механизм уведомлений (notifier), чтобы рассинхрон не оставался незамеченным.

Scenario: Уведомление при потере источника

  • WHEN задача переходит в orphaned
  • THEN автор загрузки получает уведомление о рассинхроне

Requirement: Download id в уведомлениях моноширинным для tap-to-copy

Уведомления, содержащие download id, система SHALL отображать id моноширинным блоком (в Telegram — <code>), чтобы в клиенте работало tap-to-copy: id можно скопировать одним касанием для перехода на /download/{id} или диагностики по логам. Визуальный префикс (# / download_id=) SHALL оставаться вне моноширинного блока, чтобы копировался чистый id без лишних символов.

Scenario: Id карточки review копируется одним касанием

  • GIVEN уведомление о входе загрузки в review с download id
  • WHEN бот рендерит сообщение
  • THEN download id выводится моноширинным блоком (tap-to-copy), а префикс # остаётся обычным текстом вне блока

Scenario: Id в сообщении об ошибке копируется

  • GIVEN сообщение об отказе операции с download_id
  • WHEN бот рендерит сообщение
  • THEN значение download id выводится моноширинным блоком для копирования

Requirement: Экранирование внешнего текста при форматированных уведомлениях

При включённом форматировании исходящих сообщений (parse mode) система MUST экранировать все внешние/недоверенные фрагменты перед вставкой в размеченное сообщение: display name, распознанное название, источник/контекст, целевой путь, причины распознавания, provider, id матча, ссылку на запись метабазы (URL), код и текст ошибки. Экранирование MUST учитывать контекст вставки: для значения в атрибуте (URL в href) — в том числе кавычку, чтобы недоверенное значение не разорвало атрибут. Это защищает от того, что спецсимволы разметки сломают сообщение или что разметка будет инъектирована из недоверенного источника (инвариант «выход LLM недоверенный»). Секреты (токены/ключи/пароли) MUST NOT попадать в текст уведомлений и логи.

Scenario: Спецсимволы в названии не ломают разметку

  • GIVEN уведомление, где display name или распознанное название содержит символы разметки (<, >, &)
  • WHEN бот рендерит форматированное сообщение
  • THEN эти символы экранируются, сообщение доставляется корректно, а разметка из недоверенного текста не интерпретируется

Scenario: Внешний путь и причины экранируются

  • GIVEN уведомление с целевым путём плана и причинами распознавания
  • WHEN бот рендерит форматированное сообщение
  • THEN символы разметки в пути и причинах экранируются перед вставкой

Scenario: Кавычка в URL записи не разрывает атрибут href

  • GIVEN уведомление со ссылкой на запись метабазы, где id (а значит URL) содержит кавычку
  • WHEN бот рендерит ссылку матча
  • THEN кавычка в значении href экранируется, атрибут остаётся целым и сообщение доставляется

Requirement: Показ записи матча метабазы в уведомлениях

Уведомления и подтверждения бота по загрузке с матчем метабазы система SHALL сопровождать записью матча: provider и id, а при возможности построить URL записи — ссылкой на страницу записи (тот же канонический билдер URL, что и веб). Это SHALL применяться в карточке подтверждения (review) и в уведомлении о готовности. Provider и id SHALL отражать эффективный выбор (с учётом ручных правок), консистентно с веб-страницей загрузки и экраном ревью. Когда URL не строится, матч SHALL показываться текстом (provider и id без ссылки), чтобы ошибочную привязку было видно из бота.

Отсутствие матча (none/пусто) поверхности отражают по-разному: карточка подтверждения SHALL показывать явный индикатор «нет матча» (в ревью полезно видеть, что база не выбрана), а уведомление о готовности строку матча в этом случае SHALL опускать (в финальном пинге «нет матча» — шум).

Provider, id и URL — недоверенные (метабаза/LLM/ручной ввод), поэтому система MUST экранировать их перед вставкой в размеченное сообщение так, чтобы значение не могло разорвать разметку в своём контексте (для URL в атрибуте href — с учётом кавычки), иначе изготовленный id способен сломать сообщение и подавить доставку уведомления (инвариант «выход LLM недоверенный»).

Scenario: Матч со ссылкой в карточке review

  • GIVEN загрузка в review с подтверждённым матчем метабазы, для которого строится URL записи
  • WHEN бот рендерит карточку подтверждения
  • THEN матч выводится ссылкой на страницу записи с provider и id, а provider, id и URL экранированы (в т.ч. кавычка в значении href)

Scenario: Матч без строящегося URL показывается текстом

  • GIVEN загрузка с матчем, для провайдера которого URL записи не строится
  • WHEN бот рендерит карточку подтверждения
  • THEN матч выводится текстом (provider и id) без ссылки

Scenario: Показ матча в уведомлении о готовности

  • GIVEN загрузка с матчем метабазы, перешедшая в готовность
  • WHEN бот рендерит уведомление о готовности
  • THEN уведомление содержит запись матча (provider, id, при возможности — ссылку), чтобы ошибочную привязку было видно после раскладки

Scenario: Нет матча — индикатор в review, пропуск в готовности

  • GIVEN загрузка без матча метабазы (none/пусто)
  • WHEN бот рендерит карточку подтверждения, а затем уведомление о готовности
  • THEN карточка подтверждения показывает индикатор «нет матча», а уведомление о готовности строку матча не содержит

Scenario: Эффективный провайдер после ручной правки

  • GIVEN загрузка, где провайдер/id матча переопределены вручную
  • WHEN бот рендерит запись матча
  • THEN показываются эффективные provider и id (как на веб-странице загрузки), а не значения сырого распознавания

Requirement: Выбор кандидата метабазы из карточки подтверждения бота

Карточка подтверждения бота SHALL предлагать выбрать источник из сохранённых кандидатов метабазы inline-кнопками, когда для распознавания загрузки есть хотя бы один такой кандидат (типичный триггер ревью — несколько подходящих записей без единичного сильного матча), а не молча оставлять загрузку с первым/лучшим или без базы. Когда сохранённых кандидатов нет, карточка кнопку выбора базы показывать SHALL NOT. Выбор SHALL быть двухшаговым: кнопка выбора базы разворачивает список кандидатов (по кнопке на кандидата, выбранный помечен), а нажатие кандидата пиннит его как источник и обновляет карточку. Отдельная кнопка SHALL возвращать карточку без изменений.

Выбор кандидата из бота — ручной матч (пиннинг источника): он SHALL NOT запускать авто-раскладку; раскладка выполняется отдельным подтверждением («Применить»). Бот SHALL оставаться быстрым выбором из готового короткого списка кандидатов, тогда как точные правки (ручной ввод id/URL, «без базы») — на веб- поверхности.

Id кандидата приходит в callback_data и является недоверенным входом: перед доменным вызовом выбора система MUST валидировать его как ULID на границе транспорта; невалидный id MUST отклоняться без изменения состояния загрузки. Текст inline-кнопок Telegram не интерпретируется как разметка, поэтому название кандидата в подписи кнопки инъекции разметки не создаёт.

Scenario: Карточка с кандидатами предлагает выбор базы

  • GIVEN загрузка в review, для распознавания которой сохранены кандидаты метабазы
  • WHEN бот рендерит карточку подтверждения
  • THEN карточка содержит кнопку выбора базы, ведущую к списку кандидатов inline-кнопками

Scenario: Выбор кандидата пиннит источник без авто-раскладки

  • GIVEN развёрнутый в карточке список кандидатов метабазы
  • WHEN пользователь нажимает кнопку кандидата
  • THEN этот кандидат закрепляется как источник (ручной матч), карточка обновляется на месте, а авто-раскладка не запускается — для раскладки нужно явное «Применить»

Scenario: Id кандидата из callback валидируется на границе

  • GIVEN callback выбора кандидата, где значение id кандидата недоверенное/невалидное
  • WHEN бот обрабатывает callback
  • THEN id кандидата валидируется как ULID до доменного вызова, а невалидное значение отклоняется без изменения состояния загрузки

Scenario: Возврат из списка не меняет состояние

  • GIVEN развёрнутый в карточке список кандидатов
  • WHEN пользователь нажимает кнопку возврата
  • THEN бот возвращает исходную карточку подтверждения, не меняя выбранный источник

Scenario: Без кандидатов кнопки выбора базы нет

  • GIVEN загрузка в review, для распознавания которой кандидаты метабазы не сохранены
  • WHEN бот рендерит карточку подтверждения
  • THEN карточка не содержит кнопку выбора базы