Files
avandClaude Opus 4.8 e9ec26d09a Ingest: контекст распознавания из полей magnet-ссылки
Синтезируем контекст из полей самой magnet-ссылки (dn, xl, tr/xs, kt) без
сети и дополняем им текст от транспорта: пользовательский текст первым, при
пустом — синтез единственный. Обогащённый контекст идёт только в
download.Context (его читают recognition и веб-UI); вход namer и отображаемое
имя не меняются — строки-факты (Размер:/Трекер:) в display_name не текут.

- magnet.Info: поля ExactLength/Sources/Keywords + Info.Context() (синтез,
  отсев dn-заглушек *-topic-<id>, домен трекера, человекочитаемый размер)
- Parse устойчив к ссылке в процент-кодировке (разовый QueryUnescape)
- ingest: mergeContext → download.Context, namer на сыром req.Context
- Влита дельта capability ingest в openspec/specs, change заархивирован

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:38:01 +03:00

155 lines
11 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.
## Context
Ingest (`internal/ingest/ingest.go`) сейчас принимает `Request{Source,
Context}`, где `Context` — опциональный текст от транспорта (Telegram-парсер
чистит пересланное сообщение бота; HTTP-форма отдаёт поле `context`). Из
magnet парсер (`internal/magnet/magnet.go`) достаёт только `xt` (хеши), `dn`,
`tr`. `req.Context` кладётся в `download.Context` и подаётся в
`namer.DeriveName(ctx, req.Context, info.DisplayName)``dn` идёт отдельной
«подсказкой». Важно: `dn` в `download.Context` **не** попадает, поэтому
recognition его сейчас не видит вовсе.
`download.Context` имеет двух потребителей: recognition-промпт
(`internal/recognize/prompt.go:87`) и веб-UI страницы загрузки
(`internal/httpapi/download.go:100`) — синтез станет виден обоим.
При приёме голого magnet без текста `download.Context` почти пуст, и
recognition теряет дешёвый сигнал (имя релиза, размер, происхождение), который
физически лежит в самой ссылке.
## Goals / Non-Goals
**Goals:**
- Выжать в контекст распознавания максимум из полей magnet (`dn`, `xl`,
`tr`/`xs`, `kt`) без сетевых запросов.
- Дополнять этим контекстом пользовательский текст (не терять его, ставить
первым), а при пустом тексте — синтезировать контекст целиком.
- Обогащённый контекст идёт **только в `download.Context`** (recognition +
UI). Вход namer и отображаемое имя не меняем.
**Non-Goals:**
- Дообогащение со страницы трекера (`*-topic-<id>` → HTTP). Отдельная задача.
- Изменение схемы БД, API транспортов, сигнатуры `naming.DeriveName`.
- Изменение вывода отображаемого имени: строки-факты (`Размер:`, `Трекер:`)
не должны становиться `display_name`.
- Использование дерева файлов торрента (оно приходит от qBittorrent позже, не
из ссылки) — вне этого change.
## Decisions
### Р1. Расширяем `magnet.Info`, синтез контекста — в `internal/magnet`
`Info` получает поля `ExactLength int64` (из `xl`), `Sources []string` (из
`xs`), `Keywords []string` (из `kt`). `DisplayName`, `Trackers` уже есть.
Функцию синтеза человекочитаемого контекста (`func (Info) Context() string`
или `SynthContext`) размещаем в пакете `magnet` — она чисто выводится из
полей `Info`, легко юнит-тестируется в изоляции и не тянет зависимостей.
Ingest лишь склеивает результат с `req.Context`.
_Альтернатива:_ синтез внутри ingest. Отвергнуто — раздувает ingest и хуже
тестируется; знание формата полей magnet логичнее держать в `magnet`.
Значения полей (`dn`, `xs`, `kt`) приходят уже раскодированными: `url.Query()`
снимает процент-кодировку — ручной urldecode не нужен. Дополнительно `Parse`
устойчив к ссылке, скопированной целиком в процент-кодировке
(`magnet%3A%3Fxt%3D…`, например вытащенной из другого URL): при неудаче
прямого разбора делаем разовый `QueryUnescape` и пробуем снова. Unescape
применяется только на этом фолбек-пути, поэтому значимый `+` в query обычной
ссылки не затрагивается.
### Р2. Формат синтезированного контекста — строки-факты
Синтез собирает набор коротких строк (по одной на факт) и склеивает через
`\n`, в духе того, что уже кладёт Telegram-парсер:
```
<dn как есть, если не заглушка>
Размер: ≈ 2.1 GiB
Трекер: rutracker.org
Ключевые слова: <kt>
```
Это дружелюбно и к LLM (namer/recognition), и к алгоритмическому фолбеку
namer, который берёт «первую содержательную строку» — поэтому строка `dn`
идёт первой. Точные ярлыки строк уточняются при apply; спека фиксирует состав,
не формулировки.
### Р3. Слияние — «дополняет всегда», результат только в `download.Context`
Итоговый контекст = `join(nonEmpty(userText, synthFromMagnet))`. Если
`userText` пуст — остаётся только синтез; если синтез пуст — только текст;
если пусты оба — пустая строка (приём штатно проходит с пустым контекстом,
как сейчас). Пользовательский текст идёт **первым** (он содержательнее).
Результат кладётся **только** в `download.Context` (его читают recognition и
UI). В namer он **не** передаётся — см. Р5.
_Альтернатива:_ синтез только при пустом тексте. Отвергнуто пользователем —
теряем размер/происхождение, когда текст есть, но беден.
### Р4. Отсев заглушки `dn`
`dn` вида `*-topic-<id>` (частый случай рутрекера: `dn=rutracker-topic-6514485`)
как строку-название не берём — это не имя, а идентификатор темы. Детектим
простым паттерном (`(?i)-topic-\w+$` / `^\w+-topic-`). Остальные поля (размер,
домен) синтезируем в любом случае. Домен трекера при этом всё равно
сообщит происхождение (`rutracker.org`).
### Р5. Namer НЕ получает синтез — вход именования не меняем
`namer.DeriveName(ctx, req.Context, info.DisplayName)` остаётся как сейчас:
пользовательский текст + `dn`-подсказка. Синтезированные строки-факты в namer
**не** попадают.
_Почему:_ фолбек namer (`fallbackName``firstMeaningfulLine`) берёт первую
содержательную строку контекста как название. Если подать туда синтез, то на
самом частом сценарии (голый rutracker-magnet: `dn=*-topic-<id>` — заглушка,
`xl`/`kt` нет) первой строкой окажется `Трекер: t-ru.org`, и она станет и
`qBit rename`, и `download.display_name` (заголовок карточки в UI) — регресс
против нынешнего `rutracker-topic-<id>`. Значимое имя namer и так получает
через `dn`-hint, а размер/домен для *имени* бесполезны. Поэтому синтез —
только в `download.Context` для recognition; именование не трогаем.
_Альтернатива:_ учить `fallbackName` отбрасывать строки `^<Ярлык>:\s`.
Отвергнуто — лишняя связанность namer с форматом синтеза; чище просто не
подавать синтез в namer.
## Risks / Trade-offs
- [Синтетический контекст «зашумляет» вход recognition — размер/домен могут
сбить LLM] → Факты подаём короткими помеченными строками (`Размер:`,
`Трекер:`), отделимыми от названия; recognition уже толерантен к «грязному»
контексту (Telegram-пересылки). Держим синтез лаконичным.
- [Синтез виден в веб-UI (страница загрузки рендерит `download.Context`)] →
Ожидаемо и полезно (пользователь видит, откуда взят контекст). Держим
строки-факты короткими и человекочитаемыми ради этого же.
- [Дубль факта: `xl` даёт `Размер:`, а в тексте Telegram размер уже есть] →
На практике редко (реальные rutracker-magnet'ы `xl` не несут). Дедупликацию
не делаем — recognition толерантен к повтору; помечаем как известный
трейд-офф.
- [Ложное срабатывание отсева `dn`-заглушки на реальном имени с «-topic-»] →
Паттерн якорим на `-topic-<токен>` в начале/конце строки; при сомнении
трактуем консервативно (лучше включить лишнее имя, чем потерять). Покрываем
тестом на `rutracker-topic-*` и на обычное имя.
- [`xl` в разных единицах/формах] → По спецификации magnet `xl` — байты
(десятичное целое). Парсим строго; при ошибке поле пропускаем (best-effort,
приём не валим).
- [Разные транспорты дублируют логику склейки] → Склейку делаем один раз в
ingest (общий use-case для всех транспортов), транспорты не трогаем.
## Migration Plan
Изменение аддитивное и обратносовместимое: новые поля `Info` опциональны,
контекст лишь обогащается. Схема БД не меняется, миграций нет. Откат —
ревертом кода; уже созданные `download.Context` остаются валидными.
## Open Questions
- Точные ярлыки строк-фактов (`Размер:` vs `size ~`) и локаль размера —
решаем при apply, на спеку не влияет.
- Стоит ли нормализовать домен трекера (убирать `www.`, `bt.`, `ann`-хосты) —
мелочь реализации, вынесем в helper с тестом.