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>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
## 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 с тестом.
|
||||
Reference in New Issue
Block a user