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>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
# 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`).
|
||||
Reference in New Issue
Block a user