Andrey Epifancev 71c15d1830 design.md: решения 19–20 по Task 4 (#83)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:37:30 +04:00

176 lines
24 KiB
Markdown
Raw 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
`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.)