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>
This commit is contained in:
av
2026-07-18 16:02:47 +03:00
co-authored by Claude Opus 4.8
parent 10a6348d39
commit f75d6b1f91
12 changed files with 541 additions and 12 deletions
+28
View File
@@ -43,6 +43,7 @@ type Reviewer interface {
ReviewData(ctx context.Context, id string) (*worker.ReviewData, error)
Apply(ctx context.Context, id string) error
Refine(ctx context.Context, id string, hint string) error
ChooseCandidate(ctx context.Context, id, candidateID string) error
SetType(ctx context.Context, id string, mediaType string) error
Defer(ctx context.Context, id string) error
Cancel(ctx context.Context, id string) error
@@ -362,6 +363,33 @@ func (b *Bot) handleCallback(ctx context.Context, cq *tgbotapi.CallbackQuery) {
b.answer(cq.ID, "Жду подсказку")
b.send(chatID, "Ответьте сообщением с подсказкой для #"+idCode(id)+".", nil)
return
case "sources":
// Первый шаг выбора базы: перечитываем задачу ради свежего списка
// кандидатов и разворачиваем клавиатуру списка. Домен не трогаем.
rd, rerr := b.reviewer.ReviewData(ctx, id)
if rerr != nil {
b.log.Warn("telegram sources review data failed", "download_id", id, "error", rerr)
b.answer(cq.ID, "Ошибка")
return
}
b.answer(cq.ID, "")
b.editMarkup(chatID, msgID, b.candidatesKeyboard(rd))
return
case "srcback":
// Возврат из списка кандидатов к обычной карточке (домен не трогаем).
b.answer(cq.ID, "Отменено")
b.refreshCard(ctx, chatID, msgID, id)
return
case "pick":
// Выбор кандидата: val — id кандидата из callback_data (недоверенный
// вход), валидируем как ULID на границе до доменного вызова.
candidateID, perr := ident.Parse(val)
if perr != nil {
b.answer(cq.ID, "Кнопка устарела — откройте задачу в вебе")
return
}
err = b.reviewer.ChooseCandidate(ctx, id, candidateID)
note = "Источник выбран"
default:
b.answer(cq.ID, "")
return