Приём (Ingest) стал быстрым: синхронно только парс magnet, синтез контекста из полей ссылки, атомарный дедуп и запись загрузки в новое состояние `catched` — ответ клиенту сразу. Медленный вывод имени (LLM) и добавление в qBittorrent вынесены в асинхронный шаг машины состояний, который двигает worker. - store: состояние `catched` (нетерминальное, активная группа); атомарный переход PromoteCatched (catched → downloading + display_name) с гардом state='catched' (ре-валидация после сетевых вызовов вне блокировки) - ingest: убраны namer/qbt из пути приёма; пишем `catched`, отвечаем сразу - worker.processCatched: вне w.mu выводит имя и qbt.Add, под w.mu — короткий переход; сбой add оставляет catched (ретрай тиком); предохранитель catch_timeout → failed(qbit_add)+notify; catched исключён из проверок пропажи - config: worker.catch_timeout (дефолт 10m) - веб-UI: бейдж catched, активная группа, самозавершающийся htmx-поллинг карточки/страницы до перехода в downloading; Telegram-текст без сырого catched - OpenSpec: дельты ingest/download-tracking/web-ui влиты в спеки, change заархивирован Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
79 lines
6.5 KiB
Markdown
79 lines
6.5 KiB
Markdown
## Why
|
||
|
||
Сейчас приём (`Ingest`) синхронно делает всё: парсит источник, выводит
|
||
отображаемое имя (потенциально **медленный вызов LLM** в `namer.DeriveName`) и
|
||
добавляет источник в qBittorrent — и только потом отвечает клиенту. Долгий LLM
|
||
и внешний запрос к qBit задерживают ответ HTTP API / веб-UI / Telegram и
|
||
расширяют окно «строка в БД есть, в qBittorrent ещё нет».
|
||
|
||
Идея: сделать приём **быстрым** — синхронно только валидировать и сохранить
|
||
загрузку (новое состояние `catched`), сразу вернув ответ; вывод имени и
|
||
добавление в qBittorrent вынести в отдельный **асинхронный шаг машины
|
||
состояний**, который двигает worker.
|
||
|
||
## What Changes
|
||
|
||
- Новое состояние **`catched`** — загрузка поймана и персистентно сохранена
|
||
(быстрый путь). Нетерминальное, активное (участвует в инварианте «≤1 активная
|
||
загрузка на infohash»).
|
||
- **Приём (`ingest`) — быстрый**: парс magnet, извлечение инфохэшей, синтез
|
||
контекста из полей ссылки (дёшево, без сети), атомарный дедуп и запись
|
||
загрузки в `catched`. Ответ клиенту сразу. Синхронного вывода имени и
|
||
добавления в qBittorrent в приёме больше нет.
|
||
- **Асинхронный шаг (`download-tracking`, worker)**: на каждом тике worker
|
||
подхватывает `catched`-загрузки, выводит отображаемое имя из контекста (LLM +
|
||
фолбек), добавляет источник в qBittorrent (категория/savepath/rename) и
|
||
переводит `catched → downloading` (отдельного `added-to-qbittorrent` нет —
|
||
`downloading` и так значит «в qBit, возможно metaDL»).
|
||
- **Обработка сбоев вне запроса клиента**: неуспешный `add` оставляет загрузку
|
||
в `catched` (worker перетыкивает на следующем тике); предохранитель
|
||
`catch_timeout` уводит долго-зависший `catched` в `failed`
|
||
(`error_code` `qbit_add`) с уведомлением автора.
|
||
- `catched` **исключён** из проверок «раздача не найдена» (у него раздачи нет
|
||
по дизайну) — ни поллинг, ни сверка не считают его рассинхроном/orphaned.
|
||
- **Веб-UI** показывает промежуточное состояние `catched` (бейдж/фаза
|
||
жизненного цикла), заголовок деградирует, пока имя не выведено (фолбек уже
|
||
есть). `catched` попадает в активную группу списка.
|
||
|
||
## Capabilities
|
||
|
||
### New Capabilities
|
||
|
||
_Нет._ Изменение переиспользует существующие capabilities.
|
||
|
||
### Modified Capabilities
|
||
|
||
- `ingest`: приём становится быстрым — сохранение в `catched` и мгновенный
|
||
ответ; синхронный вывод имени и добавление в qBittorrent из приёма убраны
|
||
(переезжают в асинхронный шаг). Требования по выводу имени переформулированы:
|
||
выполняются на шаге добавления, а не в пути ответа клиента.
|
||
- `download-tracking`: добавляется шаг «добавление пойманной загрузки в
|
||
qBittorrent» (вывод имени + `add` + переход `catched → downloading`),
|
||
предохранитель `catch_timeout`, исключение `catched` из проверок пропажи
|
||
раздачи.
|
||
- `web-ui`: человекочитаемый бейдж для `catched`; активная группа включает
|
||
`catched`; заголовок при пустом имени — по фолбеку; карточка `catched`
|
||
самообновляется htmx-поллингом до перехода в `downloading`.
|
||
|
||
_Без спек-правок:_ `state-reconciliation` и `live-status` не меняются.
|
||
Исключение `catched` из проверок рассинхрона нормативно закреплено требованием
|
||
`download-tracking` «catched не считается пропажей раздачи» (само поведение уже
|
||
верно — `catched` не входит в `desyncStates` и дебаунс пропажи источника его не
|
||
трогает). Иллюстративное перечисление активных состояний в тексте
|
||
`state-reconciliation` остаётся на кросс-ссылке и будет выверено при следующем
|
||
касании этой спеки. `live-status`: у `catched` нет qBit-телеметрии по дизайну —
|
||
живой прогресс для него корректно отсутствует.
|
||
|
||
## Impact
|
||
|
||
- Код: `internal/store` (состояние `StateCatched`, группа active), `internal/
|
||
ingest` (быстрый путь: убрать namer/qbt-add, писать `catched`), `internal/
|
||
worker` (новый шаг обработки `catched`: namer + qbt-add + переход, таймаут),
|
||
`internal/httpapi`+`internal/tgbot` (без правок API — ответ и так по Result),
|
||
веб-UI шаблоны (бейдж/фаза `catched`).
|
||
- Конфиг: новый `catch_timeout` (предохранитель), дефолт консервативный.
|
||
- Данные: у активной загрузки теперь есть фаза без infohash-раздачи; схема БД
|
||
не меняется (используем существующие `state`/`error_code`/`error_msg`).
|
||
- Совместимость: существующие загрузки не затронуты; переход одноразовый на
|
||
уровне логики приёма.
|