tgbot: download id моноширинным (code) для tap-to-copy

Включён HTML parse mode у всех исходящих сообщений бота (send + edit-путь
refreshCard), download id выводится моноширинным <code> — в клиентах Telegram
по нему работает tap-to-copy (скопировать id для /download/{id} или диагностики).
Префикс # / download_id= остаётся вне <code>, чтобы копировался чистый id.

Parse mode делает разметку значимой для всех текстов, поэтому добавлен
escape-хелпер и экранированы все внешние/недоверенные фрагменты: display name,
распознанное название, источник/контекст, целевой путь, причины, provider,
error_code/error_msg (инвариант «выход LLM недоверенный»). esc применяется
последним шагом, после усечения, чтобы обрез не разрубил HTML-сущность.

Capability notifications: два ADDED-требования (формат id + экранирование).
Беклог: задача закрыта, зонтичный telegram-revyu-uvedomleniy обновлён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-18 15:17:26 +03:00
co-authored by Claude Opus 4.8
parent 85e27b28e2
commit 4a58b0bda0
11 changed files with 330 additions and 69 deletions
@@ -0,0 +1,50 @@
## Why
Download id в уведомлениях бота сейчас уходит обычным текстом с префиксом `#`
(`internal/tgbot/bot.go`, `render.go`). В клиентах Telegram по моноширинному
`<code>`-тексту работает tap-to-copy — удобно скопировать id для перехода на
`/download/{id}` или для диагностики по логам. Обычный текст так скопировать
нельзя.
Чтобы получить `<code>`, нужно включить у исходящих сообщений parse mode. Это
делает разметку значимой для **всех** текстов бота: спецсимволы (`<`, `>`, `&`)
в display name, путях, распознанных названиях, причинах и тексте ошибок сломают
сообщение или будут истолкованы как разметка. Escape-хелпера сейчас нет — его
надо ввести и аккуратно применить ко всем внешним фрагментам.
## What Changes
- **Download id выводится моноширинным** (`<code>{id}</code>`) во всех
уведомлениях бота — tap-to-copy. Визуальный префикс (`#` / `download_id=`)
остаётся вне `<code>`, чтобы копировался чистый id.
- **Включается HTML parse mode** у всех исходящих сообщений — как у `send()`, так
и у edit-пути обновления карточки (`refreshCard`).
- **Вводится escape-хелпер**; все внешние/недоверенные фрагменты (display name,
распознанное название, источник/контекст, путь плана, причины распознавания,
provider, `error_code`/`error_msg`) экранируются перед вставкой в размеченное
сообщение. Инвариант «выход LLM недоверенный» распространяется на разметку.
- Секреты по-прежнему не попадают в сообщения и логи (без изменений).
## Capabilities
### New Capabilities
Нет.
### Modified Capabilities
- `notifications`: добавляется требование к **формату** уведомлений — download id
моноширинным (tap-to-copy) и инвариант безопасного экранирования внешнего
текста при включённом форматировании. Условия и события доставки уведомлений
(падение, review, готовность, рассинхрон) без изменений.
## Impact
- **Спеки:** дельта `notifications` (два ADDED-требования: формат id и
экранирование).
- **Код:** `internal/tgbot/bot.go` (`send` — ParseMode HTML; `refreshCard`
ParseMode HTML на edit; композиция id как `<code>`), `internal/tgbot/render.go`
(escape-хелпер + экранирование всех внешних фрагментов, id в `<code>`).
- **Тесты:** `internal/tgbot/bot_test.go` — обновить ожидания текста (id теперь в
`<code>`), добавить проверку экранирования спецсимволов во внешнем фрагменте.
- **Миграции БД:** нет.