В карточке подтверждения и уведомлении о готовности бот теперь показывает
запись матча метабазы — провайдер, id и кликабельную ссылку на страницу
записи, — как веб-страница /download/{id} и экран ревью. Так ошибочную
привязку видно и из Telegram.
Билдер URL записи (providerURL/matchURL) вынесен из internal/httpapi в ядро
internal/worker (worker.ProviderURL + метод (*ReviewData).MatchURL()), чтобы
оба транспорта строили ссылку одинаково; httpapi делегирует туда. baseLine
переведён на эффективные provider/id (с учётом ручных правок), URL в href
экранируется escHref (сверх esc закрывает кавычку — иначе изготовленный id
разорвал бы атрибут и Telegram отклонил бы сообщение). При отсутствии матча
карточка ревью показывает «нет матча», уведомление о готовности строку
опускает.
Capability notifications: ADDED «Показ записи матча метабазы» + MODIFIED
требования об экранировании (id матча и URL, контекст href).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5.8 KiB
Design
Контекст
Билдер URL записи метабазы сейчас живёт в транспорте internal/httpapi
(providerURL — канонический URL по provider/id/type; matchURL — выбор ссылки
матча: URL выбранного кандидата, если его provider+id совпадают с эффективными,
иначе канонический). Telegram-транспорт (internal/tgbot) не может переиспользовать
эти функции: транспорт не должен зависеть от другого транспорта, да и незачем
дублировать логику. Оба транспорта уже зависят от ядра internal/worker и его
ReviewData.
Решение
Билдер URL — в ядро worker
Переносим в internal/worker:
func ProviderURL(provider, id, mediaType string) string— экспортируемая чистая функция (канонический URL; пустой id или неизвестный провайдер → пусто).func (rd *ReviewData) MatchURL() string— метод: приоритет URL выбранного кандидата (при совпадении provider+id с эффективными), иначеProviderURL(rd.Provider, rd.ProviderID, string(rd.Plan.Type)). Тип медиа для URL всегдаrd.Plan.Type(так и звали оба вызова в httpapi), поэтому метод берёт его сам — вызывающему не нужно передавать.
httpapi делегирует: view.MatchURL = rd.MatchURL(); sourceMatchURL зовёт
worker.ProviderURL(...). Обратный разбор parseProviderURL (URL → provider/id)
и парсинг ручного ввода остаются в httpapi — это транспортный ввод, не общий
билдер. Тесты providerURL/matchURL переезжают в internal/worker.
Что показываем в боте
baseLine меняем: принимает *worker.ReviewData (а не сырой *store.Recognition),
использует эффективные rd.Provider/rd.ProviderID (как веб — с учётом
ручных правок) и rd.MatchURL():
- есть URL →
<a href="URL">provider id ↗</a>(provider, id экранированы черезesc; URL — черезescHref, см. «Безопасность»); - нет URL →
provider idтекстом (как раньше); - матча нет (
""/none) → возвращает пусто (вызывающий решает, показывать ли индикатор).
Строку матча показываем в двух местах:
- карточка ревью (
reviewCard) — точка подтверждения, где привязку ещё можно поправить; строка «База: …» уже была, добавляем в неё ссылку и переводим на эффективный провайдер. Когда матча нет — сохраняем текущее поведение: «База: нет матча» (в ревью полезно видеть, что база не выбрана). - уведомление о готовности (
renderDone) — добавляем строку «База: …только при наличии матча, чтобы ошибочную привязку было видно и в финальном пинге (файлы уже разложены, но расхождение заметно сразу). Без матча строку опускаем — в готовности «нет матча» лишний шум.
Асимметрия «нет матча» между поверхностями осознанная: индикатор в ревью помогает
(можно добавить базу), в финальном пинге — нет. baseLine поэтому отдаёт пусто на
«нет матча», а текст «нет матча» подставляет reviewCard (единственная
поверхность, где он нужен).
Прочие уведомления (падение/рассинхрон) матч не показывают: там нет подтверждённого результата раскладки, релевантна причина сбоя, а не запись базы.
Безопасность
provider, id, URL — недоверенные (метабаза/LLM/ручной ввод). provider и id
экранируются через esc (текстовый контекст). Для URL контекст другой —
значение атрибута href, а esc (tgbotapi.EscapeText(ModeHTML)) заменяет
только <, >, & и не трогает ". Между тем ProviderURL подставляет id
в URL сырым ("…/title/" + id), а id недоверенный: id с " разорвал бы атрибут
href и Telegram отклонил бы сообщение (parse error) → уведомление о матче тихо
не доставилось бы. Поэтому URL экранируем хелпером escHref, который поверх esc
дополнительно заменяет " на " (порядок безопасен: esc уже перевёл & в
&, так что & в " не удвоится). Ссылка рисуется только при непустом
URL из MatchURL(). Миграции не нужны — данные матча уже в БД (recognition,
metadata_candidate).