176 lines
24 KiB
Markdown
176 lines
24 KiB
Markdown
## Context
|
||
|
||
`main` da8f5c9, схема v5, агент v13. Устройство, на которое опираются правки:
|
||
|
||
- **Зеркало** — `mirror.apply_state`: upsert `issue`, замена `issue_link` и `issue_comment` задачи присланными (поле
|
||
`comments` необязательно — без него прежние комментарии остаются). Поля зеркала агент берёт одним `search`
|
||
(`state_fields`), `attachment` в них нет.
|
||
- **Хранилище файлов** — `files.py`: `<данные>/files/<sha256>`, `put` (атомарно, предел 10 МБ, повтор обновляет mtime),
|
||
`sweep` раз в сутки из `tick` (старше 30 дней, кроме файлов `ensure_attachment` в `queued`/`claimed`). Маршруты
|
||
`POST /files` (клиент) и `GET /agent/files/{sha}` (агент, base64). Тело запроса — до 16 МБ (`MAX_BODY`): файл 10 МБ в
|
||
base64 ≈ 14 МБ проходит, в том числе через реле папки.
|
||
- **Очередь** — пакеты, одна команда в полёте, пауза по `pacing` типа, хвостовой `refresh` по ключам пакета (кроме
|
||
пакетов только из `refresh`), довыдача до 3 раз, подтверждение по состоянию.
|
||
- **Агент** — `Executor.do_<тип>`; неизвестный тип — `failed` «неизвестный тип команды …». `JiraClient` умеет JSON и
|
||
multipart-загрузку (`upload`), считает вызовы и повторяет 429/503.
|
||
- **MCP** — ходит в хаб только через `client`; из `files` берёт только `MAX_SIZE`; `HubClient.from_home` знает каталог
|
||
данных хаба (`JIRA_HUB_HOME`).
|
||
- **Веб** — карточка `IssueCard.tsx`, рендер вики-разметки `web/src/issues/wiki/` (`!картинка!` сейчас — текст как есть),
|
||
адрес Jira — константа `JIRA_URL` в `web/src/board/constants.ts`.
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:** список вложений задачи в зеркале без содержимого; привязка вложений к описанию и комментариям по ссылкам в
|
||
тексте; скачивание одного вложения по запросу из MCP (путь к локальной копии с исходным именем) и из веба (сохранение
|
||
файлом); одна команда на вложение при параллельных запросах.
|
||
|
||
**Non-Goals и Anti-Goals:** см. proposal.md.
|
||
|
||
## Decisions
|
||
|
||
1. **Список вложений — в `refresh`, отдельной таблицей `issue_attachment` (схема v6).** Поле `attachment` в `search`
|
||
приходит целиком (без постраничности, в отличие от комментариев), поэтому лишних вызовов Jira нет. Таблица — часть
|
||
зеркала: пишет только `apply_state`, присланное `attachments` заменяет список задачи, без поля — список остаётся
|
||
(агент ≤ v13). PK — `attachment_id` (id вложения в Jira глобален). `created_at` — UTC для сортировки, как у
|
||
комментариев.
|
||
2. **Скачанное — отдельная таблица `attachment_file`, не колонка зеркала.** Граница «зеркало пишет только
|
||
`apply_state`» сохраняется; скачанный файл переживает замену списка вложений. Строка без файла в хранилище (убран
|
||
уборкой) — «не скачано», без отдельной чистки таблицы.
|
||
3. **Состояние вложения вычисляется при чтении** (`ready` / `fetching` / `failed` / `none`) из `attachment_file`,
|
||
хранилища и последней `fetch_attachment` по этому id. Отдельного статуса нет — нечему рассинхронизироваться.
|
||
4. **Команда `fetch_attachment {key, attachment_id}` в общей очереди.** Скачивание — обычная команда: темп, одна
|
||
команда в полёте, довыдача, журнал агента и восстановление работают как есть. Пакет из одной команды, без хвостового
|
||
`refresh` (в Jira ничего не меняется) и без проекта (вложение соседа тоже можно скачать). Пауза по умолчанию 2 с, как у
|
||
`refresh` (только чтение). Подтверждения по состоянию нет: потерянный отчёт закрывает довыдача — повтор безопасен. Код, перебирающий типы
|
||
команд, SHALL знать новый тип: `commands.check_confirmed` (иначе `None` и 500 на отчёте после `refresh`), признак
|
||
«пакет с записями» в `queue.claim` (иначе копия базы на каждое скачивание), выборка команд задачи в карточке.
|
||
*Отвергнуто:* отдельный канал «срочных» запросов мимо очереди — второй цикл в агенте, второй источник темпа к Jira.
|
||
5. **Файл от агента к хабу — `POST /agent/files {content}` (base64), в результате команды — только sha.** Результат
|
||
команды повторяется каждые 10 с до подтверждения и пишется в журнал агента; 14 МБ base64 там не нужны. Маршрут —
|
||
зеркало `POST /files` под агентским токеном, с теми же проверками. Отчёт `done` хаб принимает, только если файл с таким
|
||
sha и размером лежит в хранилище.
|
||
6. **Адрес содержимого агент собирает сам** — `<base_url>/secure/attachment/<id>/<имя>`, а не берёт `content` из ответа
|
||
Jira: Basic-авторизация уходит только на `base_url` (анти-цель). Перед скачиванием — `GET /issue/{key}?fields=attachment`:
|
||
проверка, что вложение ещё есть, и размер из Jira до скачивания (больше 10 МБ — не качать).
|
||
7. **Одна команда на вложение.** `POST /attachments/{id}/fetch` в одной транзакции записи проверяет `fetching`/`ready` и
|
||
ставит пакет. Веб и MCP могут жать сколько угодно — команда одна.
|
||
8. **Предел 10 МБ — в трёх местах:** хаб отказывает в постановке по размеру из зеркала (413, без пакета), агент — по
|
||
размеру из Jira (на случай, если файл заменили), хранилище — `files.put` (последний рубеж).
|
||
9. **Старый агент — 409 `agent_too_old` до постановки.** Агент v13 на `fetch_attachment` ответил бы «неизвестный тип
|
||
команды»; хаб проверяет `agent_status.agent_version` и сразу говорит, что нужен v14. Версия неизвестна (агент не
|
||
выходил на связь) — ставим: команда дождётся агента. `min_agent_version` не поднимается.
|
||
10. **Ссылки на вложения ищет хаб**, а не каждый интерфейс: MCP и веб получают готовые `attachment_ids`. Разбор —
|
||
регулярные выражения по описанию и тексту комментариев (`!имя!`, `!имя|…!`, `[^имя]`, `[текст|^имя]`), имя — точное
|
||
совпадение, при повторе имени — самое новое вложение (так делает Jira). Веб дополнительно превращает такие ссылки в
|
||
отрендеренном тексте в кнопки скачивания по тем же правилам (список вложений у него есть).
|
||
11. **MCP кладёт копию с исходным именем в `<данные хаба>/downloads/<id>/<имя>`.** Файл хранилища без имени и
|
||
расширения Claude не откроет (анти-цель). Каталог по id — нет конфликтов имён; уборка `downloads/` — тем же `tick`
|
||
хаба (30 дней). MCP читает файл через `GET /attachments/{id}/content` (граница «MCP — только через `client`»), а не
|
||
из `files/` напрямую.
|
||
12. **MCP `attachment` ждёт сам** (опрос раз в 2 с, по умолчанию до 120 с, не больше 600): обычно агент успевает за
|
||
секунды; не успел — понятный ответ, повторный вызов не ставит вторую команду.
|
||
13. **Веб скачивает через `Blob`.** Маршруты хаба требуют токен в заголовке, ссылка `<a href>` его не передаст;
|
||
`fetch` → `Blob` → временная ссылка `download`. Файл до 10 МБ — память браузера не проблема. Пока агент везёт файл —
|
||
опрос раз в 2 с, пока карточка открыта на этой задаче; файл сохраняется сам, без второго нажатия.
|
||
14. **Уборка.** Скачанные вложения от уборки не защищены (их можно скачать снова); выдача `GET …/content` обновляет
|
||
mtime, так что файлы, которые смотрят, живут дольше.
|
||
15. **Порядок выкатки — хаб первым.** Хаб v6 со старым агентом: списков вложений нет (поле не приходит), скачивание
|
||
отвечает 409. Агент v14 со старым хабом: поле `attachments` старый хаб игнорирует (`apply_state` берёт известные
|
||
колонки), `fetch_attachment` старый хаб не выдаёт.
|
||
16. **Скачивания не заслоняют записи.** Пакеты «Скачать вложение …» не делают копию базы (в Jira не пишут) и не входят
|
||
в «последние 10 команд по задаче» карточки и MCP `issue`: при разборе ошибки с десятком файлов там остаются правки
|
||
моста в Jira. В общем списке пакетов они видны, как есть.
|
||
17. **Перенаправления при скачивании.** `urllib` следует 3xx и переносит `Authorization`; агент SHALL не выполнять
|
||
перенаправление на другой хост (обработчик перенаправлений или проверка адреса), а на тот же — может.
|
||
18. **Неизвестный размер и пустые файлы.** Вложение без размера ставится, агент обрывает чтение после 10 МБ; пустое
|
||
вложение принимается от агента (`POST /agent/files`), хотя `POST /files` пустой файл по-прежнему отвергает.
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- **Ожидание в очереди.** Скачивание стоит за командами, поставленными раньше (полный `refresh` идёт минуты). Принимаем:
|
||
MCP отвечает «ещё идёт», веб показывает «Загружается…».
|
||
- **Объём через общую папку.** Файл 10 МБ — запрос ≈ 14 МБ JSON через реле. В пределах `MAX_BODY`; если реле окажется
|
||
медленным на таких файлах — снижать предел, а не менять транспорт.
|
||
- **Имя с запрещёнными символами** (`:` и др. — Jira банка их хранит, см. #43) — в копии MCP заменяются на `_`; в вебе
|
||
браузер правит имя сам.
|
||
- **Ссылка по имени после переименования** в Jira не находит вложение — показывается исходником. Редко.
|
||
- **Удалённое в Jira вложение** остаётся в зеркале до ближайшего `refresh` задачи; скачивание ответит понятной ошибкой.
|
||
|
||
## Migration Plan
|
||
|
||
1. PR хаба: схема v6, `attachments` в `apply_state`, `POST /agent/files`, `fetch_attachment` в контракте и очереди,
|
||
маршруты `/attachments/…`, ссылки в `GET /issues/{key}`, MCP (`issue`, `comment`, `attachment`), веб. После merge —
|
||
`git pull`, `make web`, перезапуск хаба владельцем, `/mcp` → reconnect.
|
||
2. PR агента: v14 (`attachments` в состоянии, `fetch_attachment`). После merge — `jira-hub agent publish
|
||
../jira-agent/src/agent.py`; `agent_status` показывает v14; `min_agent_version` не поднимать.
|
||
3. Живая проверка: полный `refresh` → у задач с вложениями список в `issue`; MCP `attachment` по скриншоту и по Excel
|
||
(кириллическое имя с пробелами) → путь, файл открывается; повторный вызов — без новой команды; кнопка «Скачать» в
|
||
карточке → файл сохранён; вложение > 10 МБ, если найдётся, → отказ без команды.
|
||
|
||
Откат: агент — публикуется v15 без новых возможностей (только вверх); хаб — предыдущий коммит, таблицы v6 остаются и не
|
||
мешают, `schema_version = 6` старый хаб не понизит.
|
||
|
||
## Open Questions
|
||
|
||
Нет.
|
||
|
||
## Decisions made without discussion
|
||
|
||
1. 2026-10-22 — **привязка вложения к комментарию — только по ссылке в тексте** (`!имя!`, `[^имя]`), без догадок по
|
||
автору и времени. Обоснование: Jira сама пишет ссылку при перетаскивании файла в комментарий; эвристика даёт ложные
|
||
привязки. Цена ошибки: файл, приложенный без ссылки, виден только в общем списке — добавить эвристику позже.
|
||
2. 2026-10-22 — **скачивание — команда общей очереди (`fetch_attachment`), а не отдельный канал.** Обоснование: темп,
|
||
довыдача, журнал агента — без нового кода. Цена ошибки: ожидание за длинным `refresh`; если мешает — приоритет
|
||
`fetch_attachment` в выдаче.
|
||
3. 2026-10-22 — **файл от агента — отдельным маршрутом `POST /agent/files`, в отчёте только sha.** Обоснование: отчёт
|
||
повторяется и пишется в журнал агента. Цена ошибки: нет.
|
||
4. 2026-10-22 — **адрес содержимого собирает агент из `base_url`.** Обоснование: учётные данные не уходят на чужой адрес.
|
||
Цена ошибки: если в Jira банка адрес вложений отличается от `/secure/attachment/<id>/<имя>` — выяснится на живой
|
||
проверке, поправить сборку адреса.
|
||
5. 2026-10-22 — **копия для Claude — `<данные хаба>/downloads/<id>/<имя>`, уборка 30 дней.** Обоснование: исходное имя и
|
||
расширение нужны, чтобы Claude открыл файл; каталог хаба уже под `0700`. Цена ошибки: Claude в другом репозитории
|
||
может спросить разрешение на чтение вне проекта — один раз на сессию.
|
||
6. 2026-10-22 — **старый агент — 409 `agent_too_old` до постановки, `min_agent_version` не поднимается.** Обоснование:
|
||
понятная ошибка вместо «неизвестный тип команды», старый агент продолжает работать. Цена ошибки: нет.
|
||
7. 2026-10-22 — **MCP `attachment` ждёт по умолчанию 120 с, максимум 600.** Обоснование: обычно агент успевает за
|
||
секунды, а чат не должен висеть дольше. Цена ошибки: при занятой очереди — повторный вызов.
|
||
8. 2026-10-22 — **ссылки на вложения в тексте веба — кнопка со скрепкой, без картинки.** Обоснование: решение владельца
|
||
«превью не делаем»; ссылка в тексте ведёт к тому же скачиванию. Цена ошибки: нет.
|
||
9. 2026-10-22 — **пауза `fetch_attachment` по умолчанию 2 с.** Обоснование: только чтение, как `refresh`. Цена ошибки:
|
||
нет — меняется настройкой `pacing`.
|
||
10. 2026-10-22 — **пакеты скачивания не делают копию базы и не входят в «последние 10 команд по задаче».** Обоснование:
|
||
находка рецензента — иначе серия скачиваний вытесняет копии перед записями и команды-записи из карточки. Цена
|
||
ошибки: скачивания по задаче видны только в списке пакетов.
|
||
11. 2026-10-22 — **перенаправление на другой хост при скачивании — `failed`, без перехода.** Обоснование: находка
|
||
рецензента — `urllib` унёс бы `Authorization` на чужой хост. Цена ошибки: если Jira банка отдаёт вложения через
|
||
другой хост — скачивание не работает до решения владельца, видно на живой проверке.
|
||
12. 2026-10-22 — **вложение без размера ставится, предел 10 МБ проверяет агент по прочитанному.** Обоснование: Jira
|
||
может не отдать размер; отказывать в таком вложении — терять файл. Цена ошибки: агент читает до 10 МБ лишнего.
|
||
13. 2026-10-22 — **пустое вложение принимается от агента.** Обоснование: в Jira бывают пустые файлы; иначе вечный
|
||
`failed`. Цена ошибки: нет.
|
||
14. 2026-10-22 — **`command_id`/`batch_id` в состоянии вложения: при `fetching` — у живой (`queued`/`claimed`) команды, иначе у
|
||
последней `fetch_attachment` по этому id; `ready` требует и строку `attachment_file`, и файл в хранилище.** Обоснование:
|
||
повторный `POST …/fetch` должен вернуть ту же команду, что идёт, а не историческую; строка без файла — «не скачано»
|
||
(решение 2). Цена ошибки: интерфейс покажет чужой command_id в нейтральных состояниях — правка одной функции.
|
||
(R-1, правило исполнителя Task 2.)
|
||
15. 2026-10-22 — **при повторе имени «самое новое» вложение — максимум по `(created_at, число id, id)`.** Обоснование:
|
||
детерминированный выбор при равных временах (Jira отдаёт id по возрастанию). Цена ошибки: ссылка по имени укажет на
|
||
соседнее вложение — исправляется порядком ключа.
|
||
16. 2026-10-22 — **порядок отказов в `request_fetch`: сначала `agent_too_old` (409), потом размер (413).** Обоснование: старый
|
||
агент не скачает и маленький файл, а размер человек увидит и так; отказ по агенту не зависит от вложения. Цена ошибки:
|
||
для большого файла при старом агенте покажем 409 вместо 413 — несущественно. (R-3, T3.1.)
|
||
17. 2026-10-22 — **`TooLarge` наследует `CommandError` (в `POST /batches` — 422), в маршруте `…/fetch` ловится раньше и даёт
|
||
413; `attachment_id` в команде — только цифры; оба отказа `done` (нет файла, чужой размер) дают один текст «хаб: файл
|
||
<sha> не найден в хранилище».** Обоснование: одна проверка размера на постановку из любого входа; Jira-id — число.
|
||
Цена ошибки: поменять класс исключения / текст. (R-4, решение исполнителя Task 3.)
|
||
18. 2026-10-22 — **`pacing_from_config` и запасной зазор: `fetch_attachment` = 2 с и там, и в `DEFAULT_PACING`.** Обоснование:
|
||
иначе `init` из config расходится с умолчанием и сохранённая настройка без нового типа ломает выдачу. Цена ошибки: нет.
|
||
(R-5.)
|
||
19. 2026-10-22 — **`sweep_downloads` считает возраст каталога вложения по самому новому из mtime каталога и его файлов; сбой
|
||
уборки `downloads/` логируется и не мешает уборке `files/`.** Обоснование: копию, которую открывали недавно, не
|
||
удалять; побочная уборка не должна ломать основную. Цена ошибки: преждевременное или запоздалое удаление копии
|
||
(её MCP создаст заново). (R-6, решение исполнителя Task 4.)
|
||
20. 2026-10-22 — **неизвестный id вложения в `GET /attachments/{id}` — 404 `no_attachment`; `Content-Type` из `mime_type` только
|
||
при формате тип/подтип, иначе `application/octet-stream`.** Обоснование: единые коды вложений; заголовок не берётся
|
||
из недоверенных данных Jira как есть. Цена ошибки: нет. (R-7.)
|