Files
avandClaude Opus 4.8 10a6348d39 tgbot: показывать запись матча метабазы (провайдер+id+ссылка)
В карточке подтверждения и уведомлении о готовности бот теперь показывает
запись матча метабазы — провайдер, 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>
2026-07-18 15:41:28 +03:00

76 lines
5.8 KiB
Markdown
Raw Permalink 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.
# 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`
дополнительно заменяет `"` на `&quot;` (порядок безопасен: `esc` уже перевёл `&` в
`&amp;`, так что `&` в `&quot;` не удвоится). Ссылка рисуется только при непустом
URL из `MatchURL()`. Миграции не нужны — данные матча уже в БД (`recognition`,
`metadata_candidate`).