Merge pull request 'Архив OpenSpec: attachment-fetch (#83)' (#85) from chore/83-archive-attachment-fetch into main
This commit is contained in:
commit
46a6fc2e25
@ -108,7 +108,7 @@ Jira в текущем процессе, до него — `null`. На HTTP 429
|
||||
- **THEN** `last_jira_call_at` в `claim` — `null`; после первого вызова Jira следующий `claim` несёт его время
|
||||
|
||||
### Requirement: Чтение состояния
|
||||
`refresh` SHALL выполняться запросом `search` (`jql`, `fields` = поля зеркала включая `issuelinks`, `comment` и спринт,
|
||||
`refresh` SHALL выполняться запросом `search` (`jql`, `fields` = поля зеркала включая `issuelinks`, `comment`, `attachment` и спринт,
|
||||
`maxResults = 100`, постранично) и отправляться хабу пачками по 100 задач со связями; ключи из `issuelinks`, которых нет
|
||||
в выборке и в зеркале хаба (хаб сообщает список известных), агент SHALL дочитать одним `search` по `key in (...)`
|
||||
и отправить с признаком `neighbor`. Ошибка хаба при запросе известных ключей или отправке пачки SHALL попадать в
|
||||
@ -120,6 +120,11 @@ Jira в текущем процессе, до него — `null`. На HTTP 429
|
||||
постранично (`maxResults = 100`) и передать полный список. Комментарий без `id` или с пустым `id` агент SHALL отбросить
|
||||
с записью в лог (ключ задачи), остальные передать. Если дочитывание комментариев задачи вернуло 403 или 404, агент SHALL
|
||||
передать задачу без поля `comments` (хаб оставит прежние комментарии) с записью в лог; `refresh` продолжается.
|
||||
Каждая задача в состоянии (с v14) SHALL нести `attachments` — все вложения задачи из поля `attachment` ответа `search`
|
||||
в порядке Jira, каждое `{id, filename, size, mime_type, author, created}`: `id` — строка; `filename` — имя как есть;
|
||||
`size` — целое (байт) или `null`; `mime_type` — `mimeType` Jira или `null`; `author` — как у комментария; `created` —
|
||||
время Jira как есть. Содержимое вложений при `refresh` MUST NOT скачиваться. Вложение без `id` или без имени агент SHALL отбросить с
|
||||
записью в лог (ключ задачи). Нет поля `attachment` в ответе (поле скрыто настройками Jira) — `attachments = []`.
|
||||
|
||||
#### Scenario: Полная синхронизация
|
||||
- **WHEN** `refresh {jql: "project = AURORA AND labels = AWPCA"}` при 180 задачах
|
||||
@ -152,6 +157,13 @@ Jira в текущем процессе, до него — `null`. На HTTP 429
|
||||
- **WHEN** у задачи в Jira нет комментариев
|
||||
- **THEN** в её состоянии `comments = []`
|
||||
|
||||
#### Scenario: Вложения в состоянии
|
||||
- **WHEN** у AURORA-1650 в Jira два вложения: «скрин 1.png» (`mimeType = image/png`, 240000 байт, автор
|
||||
`{name: "potapova", displayName: "Потапова Анна"}`) и «ОСВ 60.xlsx»
|
||||
- **THEN** в состоянии AURORA-1650 `attachments` из двух элементов в порядке Jira, у первого `filename = "скрин 1.png"`,
|
||||
`size = 240000`, `mime_type = "image/png"`, `author = "Потапова Анна"`; число вызовов `search` не изменилось, обращений
|
||||
к содержимому вложений нет
|
||||
|
||||
### Requirement: Доставка и запуск
|
||||
README агента SHALL описывать: первую установку (копирование `agent.py`, `run.py` и `agent.example.json` через общую
|
||||
папку в локальный каталог виртуалки, заполнение `agent.json` с выбором транспорта в `hub_url`, `check`), запуск
|
||||
@ -493,3 +505,14 @@ from_string, to, to_string}`: `history_id` — id записи строкой; `
|
||||
#### Scenario: Хаб без журнала
|
||||
- **WHEN** `refresh {jql: base}` без `history`
|
||||
- **THEN** журнал не читается; в результате нет ключа `history`
|
||||
|
||||
### Requirement: Скачивание вложения
|
||||
Агент v14 SHALL выполнять `fetch_attachment` по контракту `commands` («Скачивание вложения по запросу»). Метод `JiraClient`
|
||||
для содержимого SHALL отдавать байты ответа без разбора JSON, с тем же счётом вызовов, повтором 429/503 и учётом времени
|
||||
последнего вызова, что и прочие вызовы Jira. Отправка хабу — `HubClient` (`POST /agent/files`), работает и через общую
|
||||
папку. В журнал агента (`journal.jsonl`) и в лог содержимое файла MUST NOT попадать — только id, имя, размер и sha256.
|
||||
|
||||
#### Scenario: Скачивание через общую папку
|
||||
- **WHEN** агент с `hub_url = folder:…` выполняет `fetch_attachment` вложения 3 МБ
|
||||
- **THEN** содержимое уходит хабу одним запросом `POST /agent/files` через папку (тело ≈ 4 МБ base64); отчёт `done` с
|
||||
`file` и `size`; в `journal.jsonl` нет содержимого файла
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
### Requirement: Команда — желаемый результат на одной задаче
|
||||
Каждая команда SHALL описывать желаемое состояние одной задачи Jira и выполняться агентом как «прочитать → сравнить →
|
||||
записать только разницу». Повторное выполнение любой команды с тем же результатом в Jira SHALL завершаться `done`
|
||||
без записи. Типы команд и их контракт:
|
||||
без записи. `fetch_attachment` в Jira не пишет: она читает файл вложения и передаёт его хабу; повтор безопасен. Типы команд и их контракт:
|
||||
|
||||
| Тип | Параметры | Что читает | Что пишет |
|
||||
|---|---|---|---|
|
||||
@ -20,6 +20,7 @@
|
||||
| `ensure_sprint` | `key`, `sprint` (имя или `backlog`); `board_id` хаб дописывает при постановке (от клиента — 422) | `GET /issue/{key}` (поле спринта), список спринтов доски `board_id` (активные и будущие) | `POST /rest/agile/1.0/sprint/{id}/issue` или `POST /rest/agile/1.0/backlog/issue` |
|
||||
| `ensure_attachment` | `key`, `file` (sha256 файла в хранилище хаба, 64 hex-символа), `filename` (имя вложения в Jira, без `/`, `\`, `"` и управляющих символов); `size` хаб дописывает при выдаче | `GET /issue/{key}?fields=attachment` | вложения с тем же `filename` и размером нет — файл у хаба (`GET /agent/files/{file}`), затем `POST /issue/{key}/attachments` (multipart); есть — ничего; результат `attachment_id` |
|
||||
| `refresh` | `keys` (список) или `jql`; `boards` (`[{id, type}]` — доски проектов, `type` — `scrum`, `kanban` или `null`) хаб дописывает при выдаче | `search` с полями зеркала, по 100 на страницу; плюс дочитывание соседей по `issuelinks`; доски с неизвестным типом — `GET /rest/agile/1.0/board/{id}` | ничего в Jira; состояние уходит в хаб; результат — `boards: {"<id>": "scrum" | "kanban" | null}` для досок, чей тип агент запрашивал |
|
||||
| `fetch_attachment` | `key`, `attachment_id` (id вложения Jira, строка из цифр) | `GET /issue/{key}?fields=attachment` (имя и размер вложения) | ничего в Jira; содержимое — `GET <base_url>/secure/attachment/{id}/{имя}`, затем хабу `POST /agent/files`; результат `{attachment_id, file, size}` |
|
||||
|
||||
Доску для `ensure_sprint` хаб SHALL определять при постановке: проект задачи — проекты, чьи метки есть у задачи в
|
||||
зеркале (для временного ключа — в `labels` её `ensure_issue` в пакете, после дописывания метки проекта); `board_id` —
|
||||
@ -108,7 +109,7 @@
|
||||
|
||||
### Requirement: Пакет и порядок выполнения
|
||||
Пакет SHALL создаваться одним вызовом с автором, заголовком и списком команд; хаб SHALL назначить `seq` по порядку списка
|
||||
и добавить в хвост `refresh` по всем ключам пакета (кроме пакетов, состоящих только из `refresh`). Хаб SHALL проверять
|
||||
и добавить в хвост `refresh` по всем ключам пакета (кроме пакетов, состоящих только из `refresh` и `fetch_attachment`). Хаб SHALL проверять
|
||||
типы параметров: текстовые параметры и ключи — строки (`null` у необязательного параметра — «не задан»; агент
|
||||
трактует его по контракту типа, для `ensure_fields` — очистка поля), `target` команды
|
||||
`ensure_status` — строка или список строк, `labels`, `labels_add`, `labels_remove`, `keys` — списки строк, `replace` —
|
||||
@ -131,7 +132,8 @@
|
||||
|
||||
### Requirement: Политика темпа и пауза на команде
|
||||
Хаб SHALL хранить `pacing` как минимальные зазоры в секундах по типу команды (по умолчанию: `ensure_issue` и
|
||||
`ensure_fields` 120, `ensure_comment`, `ensure_status`, `ensure_link`, `ensure_sprint`, `ensure_attachment` 30, `refresh` 2). При выдаче команды
|
||||
`ensure_fields` 120, `ensure_comment`, `ensure_status`, `ensure_link`, `ensure_sprint`, `ensure_attachment` 30, `refresh` и `fetch_attachment` 2; тип, которого нет в
|
||||
сохранённой настройке `pacing`, получает зазор по умолчанию). При выдаче команды
|
||||
хаб SHALL сообщить `pause_seconds` = зазор по типу выдаваемой команды; агент SHALL выдержать его от времени своего
|
||||
последнего вызова Jira. Внутри одной команды с несколькими записями (`ensure_link` с `replace`, путь переходов) агент
|
||||
SHALL выдерживать зазор типа команды между записями.
|
||||
@ -157,8 +159,8 @@ SHALL выдерживать зазор типа команды между за
|
||||
**Подтверждение по состоянию.** Если после любого `refresh` желаемый результат команды в `claimed` виден в зеркале, хаб
|
||||
SHALL перевести её в `confirmed_by_state`: `ensure_fields` — все поля совпадают (строки сравниваются без различия
|
||||
`\r\n` и `\n`; `null` и пустая строка равны); `ensure_status` — статус равен последнему в пути; `ensure_link` — связь
|
||||
есть и конфликтов нет; `ensure_sprint` — спринт совпадает. `ensure_issue`, `ensure_comment`, `ensure_attachment` и `refresh` по
|
||||
состоянию не подтверждаются (вложений в зеркале нет): потерянный отчёт по ним закрывается повторной выдачей (агент находит созданную задачу поиском дубля)
|
||||
есть и конфликтов нет; `ensure_sprint` — спринт совпадает. `ensure_issue`, `ensure_comment`, `ensure_attachment`, `fetch_attachment` и `refresh` по
|
||||
состоянию не подтверждаются (зеркало не показывает ни загрузку вложения, ни скачанный файл): потерянный отчёт по ним закрывается повторной выдачей (агент находит созданную задачу поиском дубля)
|
||||
или отменой человеком.
|
||||
|
||||
#### Scenario: Отчёт потерян, факт на месте
|
||||
@ -230,7 +232,7 @@ SHALL перевести её в `confirmed_by_state`: `ensure_fields` — вс
|
||||
- `ensure_fields`, `ensure_comment`, `ensure_status`, `ensure_sprint`, `ensure_attachment`: `key` SHALL быть временным
|
||||
ключом `ensure_issue` этого пакета или задачей зеркала с меткой P, иначе 422 «<key> не в проекте P»;
|
||||
- `ensure_link`: хотя бы один из `source`, `target` — временный ключ этого пакета или задача зеркала с меткой P;
|
||||
- `refresh` — без проверок проекта.
|
||||
- `refresh` и `fetch_attachment` — без проверок проекта.
|
||||
«Метка другого проекта» и «P в `labels_remove`» SHALL сравниваться без учёта регистра; принадлежность задачи проекту
|
||||
(метка P у задачи зеркала) — точным совпадением, как в разрезе проекта. Пакет без `project` эти проверки не проходит,
|
||||
метки хаб в нём не дописывает.
|
||||
@ -292,3 +294,49 @@ SHALL перевести её в `confirmed_by_state`: `ensure_fields` — вс
|
||||
#### Scenario: Хвост пакета до первой отметки
|
||||
- **WHEN** выдаётся хвостовой `refresh {keys: [X]}`, прогонов с `history_at` нет
|
||||
- **THEN** в выданных параметрах нет `history`
|
||||
|
||||
### Requirement: Скачивание вложения по запросу
|
||||
`fetch_attachment {key, attachment_id}` SHALL ставиться только хабом по запросу интерфейса (`POST /attachments/{id}/fetch`,
|
||||
см. `hub-api`) — отдельным пакетом из одной команды, без хвостового `refresh` и без проекта (`batch.project = NULL`),
|
||||
заголовок «Скачать вложение <filename> (<key>)»; через `POST /batches` её MAY поставить и клиент, с теми же проверками.
|
||||
При постановке хаб SHALL проверять: `attachment_id` — непустая строка из цифр; вложение с этим id есть в зеркале у задачи
|
||||
`key` (иначе 422 «у <key> нет вложения #<id> в зеркале — обновите задачу»); его размер в зеркале не больше 10 МБ
|
||||
(10 485 760 байт) — иначе 413 `too_large` «файл больше 10 МБ (<размер>) — скачайте в Jira»; размер неизвестен (`NULL`) —
|
||||
ставится, предел проверяет агент. Агент SHALL выполнять её так:
|
||||
1. `GET /issue/{key}?fields=attachment`; вложения с этим id нет — `failed` «вложение #<id> удалено из <key> в Jira»;
|
||||
2. размер по Jira больше 10 МБ — `failed` «файл больше 10 МБ (<размер>) — скачайте в Jira», содержимое не качается;
|
||||
размер неизвестен — качается, но чтение ответа SHALL обрываться на 10 485 761-м байте с тем же `failed`;
|
||||
3. содержимое — `GET` по адресу, собранному из `base_url` агента: `<base_url>/secure/attachment/<id>/<имя в URL-кодировке>`
|
||||
(адрес `content` из ответа Jira MUST NOT использоваться: учётные данные уходят только на `base_url`), таймаут не
|
||||
меньше 120 с; ответ Jira 429/503 — повтор по политике агента; перенаправление (3xx) агент MUST NOT выполнять с
|
||||
учётными данными на другой хост (схема, хост и порт не как у `base_url`) — `failed` «Jira перенаправила скачивание на
|
||||
<хост>»; длина полученного не равна известному размеру из шага 1 — `failed` «файл пришёл не целиком (<получено> из
|
||||
<размер>)»; пустое вложение (0 байт) — допустимо;
|
||||
4. `POST /agent/files {content}` (base64) хабу; ответ `{file, size}`; ошибка хаба — с префиксом «хаб: », недоступность
|
||||
хаба повторяется, как при отправке состояния;
|
||||
5. результат `done` `{attachment_id, file, size}`.
|
||||
По отчёту `done` хаб SHALL записать скачанный файл вложения (см. `data-model`, «Скачанные вложения»).
|
||||
|
||||
#### Scenario: Скачивание скриншота
|
||||
- **WHEN** у AURORA-1650 в зеркале вложение #31337 «скрин 1.png» 240 000 байт; поставлена `fetch_attachment {key:
|
||||
AURORA-1650, attachment_id: "31337"}`
|
||||
- **THEN** агент читает задачу, качает `<base_url>/secure/attachment/31337/%D1%81%D0%BA%D1%80%D0%B8%D0%BD%201.png`,
|
||||
отправляет содержимое хабу; отчёт `done {attachment_id: "31337", file: <sha256>, size: 240000}`; в хвосте пакета
|
||||
`refresh` нет
|
||||
|
||||
#### Scenario: Вложение удалили в Jira
|
||||
- **WHEN** вложение #31337 в зеркале есть, в Jira его уже нет
|
||||
- **THEN** команда `failed` «вложение #31337 удалено из AURORA-1650 в Jira»; хаб файл не записывает
|
||||
|
||||
#### Scenario: Слишком большой файл
|
||||
- **WHEN** в зеркале у вложения #40000 размер 25 000 000 байт; интерфейс просит его скачать
|
||||
- **THEN** 413 `too_large` «файл больше 10 МБ (23,8 МБ) — скачайте в Jira», пакет не создан
|
||||
|
||||
#### Scenario: Размер неизвестен
|
||||
- **WHEN** у вложения #50 в зеркале `size = NULL`, в Jira размера тоже нет, содержимое — 12 МБ
|
||||
- **THEN** постановка проходит; агент обрывает чтение после 10 МБ, команда `failed` «файл больше 10 МБ (больше 10,0 МБ) —
|
||||
скачайте в Jira»; хабу ничего не отправлено
|
||||
|
||||
#### Scenario: Перенаправление на другой хост
|
||||
- **WHEN** Jira на `GET <base_url>/secure/attachment/31337/…` ответила 302 на `https://files.example/…`
|
||||
- **THEN** агент туда не ходит, команда `failed` «Jira перенаправила скачивание на files.example»
|
||||
|
||||
@ -6,15 +6,15 @@
|
||||
## Requirements
|
||||
|
||||
### Requirement: Схема базы хаба
|
||||
База хаба SHALL быть одним файлом SQLite (WAL) с таблицами `issue`, `issue_link`, `issue_comment`, `issue_change`, `issue_note`,
|
||||
`batch`, `command`, `sync_run`, `agent_status`, `setting`, `project`. Схема SHALL создаваться и мигрироваться самим хабом при
|
||||
старте по номеру версии в `setting.schema_version` (текущая — 5). Миграция существующей базы SHALL выполняться в одной
|
||||
База хаба SHALL быть одним файлом SQLite (WAL) с таблицами `issue`, `issue_link`, `issue_comment`, `issue_change`, `issue_attachment`,
|
||||
`attachment_file`, `issue_note`, `batch`, `command`, `sync_run`, `agent_status`, `setting`, `project`. Схема SHALL создаваться и мигрироваться самим хабом при
|
||||
старте по номеру версии в `setting.schema_version` (текущая — 6). Миграция существующей базы SHALL выполняться в одной
|
||||
транзакции с блокировкой записи; в той же транзакции, после повторной проверки версии, хаб SHALL скопировать файл базы в
|
||||
`backups/<дата-время>-schema-v<старая>.sqlite` рядом с базой — одна копия на миграцию, сколько бы соединений ни
|
||||
открывалось одновременно. Каталог данных хаба и `backups/` SHALL иметь права `0700` (в базе токены): хаб выставляет их
|
||||
при создании и при каждом старте; если выставить не удалось — предупреждение в лог, работа продолжается. Денежных и
|
||||
дробных полей нет; даты — ISO-8601 строки в UTC, кроме `duedate` и плановых дат (даты без времени), строк времени
|
||||
Jira как есть (`issue_comment.created`/`updated`, `issue_change.created`) и значений полей журнала.
|
||||
Jira как есть (`issue_comment.created`/`updated`, `issue_change.created`, `issue_attachment.created`) и значений полей журнала.
|
||||
|
||||
Миграция v3 → v4 SHALL: создать `project` и колонку `batch.project` (у существующих пакетов `NULL`); разобрать прежнюю
|
||||
настройку `base_jql` вида `… labels = <M>` или `… labels in (<M1>, …)` и завести проекты с этими метками (название проекта
|
||||
@ -29,6 +29,9 @@ Jira как есть (`issue_comment.created`/`updated`, `issue_change.created`)
|
||||
`sync_run.history_at`; заполнить `issue_comment.created_at` у имеющихся комментариев приведением `created` к UTC (не
|
||||
разобралось — `NULL`). У имеющихся `sync_run` `history_at = NULL`.
|
||||
|
||||
Миграция v5 → v6 SHALL: создать пустые `issue_attachment` (с индексом по `key`) и `attachment_file`. Списки вложений
|
||||
появляются с первым `refresh` агента v14.
|
||||
|
||||
#### Scenario: Первый запуск
|
||||
- **WHEN** хаб стартует с несуществующим файлом базы
|
||||
- **THEN** файл создаётся, таблицы и индексы на месте, `setting.schema_version` = текущая; каталог данных с правами `0700`
|
||||
@ -36,24 +39,29 @@ Jira как есть (`issue_comment.created`/`updated`, `issue_change.created`)
|
||||
#### Scenario: Миграция базы v1
|
||||
- **WHEN** хаб стартует с базой `schema_version = 1`, в которой есть задачи, заметки и команда в `claimed`
|
||||
- **THEN** в `backups/` копия базы v1; добавлена `command.deliveries` (у `claimed` — 1, у остальных — 0); добавлена пустая
|
||||
`issue_comment`; данные на месте; `schema_version = 5`
|
||||
`issue_comment`; данные на месте; `schema_version = 6`
|
||||
|
||||
#### Scenario: Миграция базы v2
|
||||
- **WHEN** хаб стартует с базой `schema_version = 2`, в которой есть задачи, связи и заметки
|
||||
- **THEN** в `backups/` копия базы v2; добавлена пустая `issue_comment`; данные на месте; `schema_version = 5`
|
||||
- **THEN** в `backups/` копия базы v2; добавлена пустая `issue_comment`; данные на месте; `schema_version = 6`
|
||||
|
||||
#### Scenario: Миграция базы v3
|
||||
- **WHEN** хаб стартует с базой `schema_version = 3`, `base_jql = "project = AURORA AND labels = AWPCA"`, `board_id = 4216`,
|
||||
в `sync_run` полный прогон с этой `query`
|
||||
- **THEN** в `backups/` копия базы v3; проект `AWPCA` «АРМ КА» с `board_id = 4216`, `board_type = NULL`; ключи `base_jql`
|
||||
и `board_id` в `setting` на месте; `query` того прогона — `project = AURORA AND labels in ("AWPCA")`; задачи, заметки,
|
||||
пакеты на месте; `schema_version = 5`
|
||||
пакеты на месте; `schema_version = 6`
|
||||
|
||||
#### Scenario: Миграция базы v4
|
||||
- **WHEN** хаб стартует с базой `schema_version = 4`, у задачи X комментарий с `created = "2026-10-22T13:05:11.000+0300"`,
|
||||
в `sync_run` полный прогон
|
||||
- **THEN** в `backups/` копия базы v4; есть пустая `issue_change`; у комментария `created_at = "2026-10-22T10:05:11+00:00"`,
|
||||
`created` прежний; у прогона `history_at = NULL`; `schema_version = 5`
|
||||
`created` прежний; у прогона `history_at = NULL`; затем миграция v5 → v6; `schema_version = 6`
|
||||
|
||||
#### Scenario: Миграция базы v5
|
||||
- **WHEN** хаб стартует с базой `schema_version = 5`
|
||||
- **THEN** в `backups/` копия базы v5; есть пустые `issue_attachment` и `attachment_file`; прочие таблицы не тронуты;
|
||||
`schema_version = 6`
|
||||
|
||||
#### Scenario: refresh в очереди при миграции
|
||||
- **WHEN** в базе v3 стоит `queued` `refresh {jql: "project = AURORA AND labels = AWPCA"}`
|
||||
@ -74,10 +82,14 @@ Jira как есть (`issue_comment.created`/`updated`, `issue_change.created`)
|
||||
`sprint`, `jira_updated`, `fetched_at`, `neighbor` (0/1 — вне базовой JQL, дочитана по связи), `created`. Таблица `issue_link`
|
||||
SHALL хранить связи `(source_key, target_key, link_type)` с направлением по семантике Jira: `source` блокирует/ссылается
|
||||
на `target` (у `source` в GET это `outwardIssue`). Таблица `issue_comment` SHALL хранить комментарии задачи как есть:
|
||||
`key`, `comment_id` (PK вместе с `key`), `author`, `created`, `updated`, `body` (wiki). Обновлять `issue`, `issue_link` и
|
||||
`issue_comment` SHALL только загрузка состояния от агента; HTTP API интерфейсов и MCP MUST NOT писать в них. Присланные
|
||||
`key`, `comment_id` (PK вместе с `key`), `author`, `created`, `updated`, `body` (wiki). Таблица `issue_attachment` SHALL
|
||||
хранить список вложений задачи без содержимого: `attachment_id` (PK, id вложения Jira), `key`, `filename`, `size` (байт
|
||||
или `NULL`), `mime_type`, `author`, `created` (время Jira как есть), `created_at` (UTC, приведённое из `created`; не
|
||||
разобралось — `NULL`). Обновлять `issue`, `issue_link`, `issue_comment` и
|
||||
`issue_attachment` SHALL только загрузка состояния от агента; HTTP API интерфейсов и MCP MUST NOT писать в них. Присланные
|
||||
комментарии задачи SHALL заменять все её прежние комментарии; задача, присланная без поля `comments`, свои комментарии
|
||||
SHALL сохранять. Полный прогон (`refresh` по базовой JQL), завершившийся `done`, SHALL помечать `neighbor = 1` задачи, не
|
||||
SHALL сохранять. Так же со вложениями: присланное поле `attachments` заменяет все прежние вложения задачи, задача без
|
||||
поля (агент до v14) свои вложения сохраняет. Полный прогон (`refresh` по базовой JQL), завершившийся `done`, SHALL помечать `neighbor = 1` задачи, не
|
||||
обновлённые им, — только если прогон загрузил хотя бы одну задачу.
|
||||
|
||||
#### Scenario: Загрузка состояния заменяет связи
|
||||
@ -101,6 +113,14 @@ SHALL сохранять. Полный прогон (`refresh` по базово
|
||||
- **WHEN** у X в зеркале три комментария, агент v9 прислал X без поля `comments`
|
||||
- **THEN** поля X обновлены, три комментария на месте
|
||||
|
||||
#### Scenario: Вложения в состоянии
|
||||
- **WHEN** у X в зеркале вложения #1 и #2, агент v14 прислал X с `attachments = [{id: "2", …}, {id: "3", …}]`
|
||||
- **THEN** у X в `issue_attachment` #2 (поля из состояния) и #3; #1 удалено; `attachment_file` не тронута
|
||||
|
||||
#### Scenario: Состояние от агента до v14
|
||||
- **WHEN** у X в зеркале два вложения, агент v13 прислал X без поля `attachments`
|
||||
- **THEN** поля X обновлены, два вложения на месте
|
||||
|
||||
### Requirement: Служебные заметки
|
||||
Таблица `issue_note` SHALL хранить поля, которых нет в Jira: `key` (PK; ключ Jira или временный `new:<n>`),
|
||||
`fact_status`, `work_kind`, `done_text`, `rest_text`, `owner_note`, `updated_at`, `updated_by`. Обновление зеркала
|
||||
@ -298,3 +318,25 @@ label, через `, `; без проектов базовой JQL
|
||||
#### Scenario: Старый агент
|
||||
- **WHEN** агент v12 завершил `refresh` `done` без `result.history`
|
||||
- **THEN** у его `sync_run` `history_at = NULL`
|
||||
|
||||
### Requirement: Скачанные вложения
|
||||
Таблица `attachment_file` SHALL хранить, какие вложения уже скачаны в хранилище файлов хаба: `attachment_id` (PK),
|
||||
`file` (sha256 содержимого), `size`, `fetched_at` (UTC). Строку SHALL записывать (вставка или замена) только отчёт
|
||||
`done` по `fetch_attachment`; загрузка состояния её MUST NOT трогать, поэтому скачанный файл переживает обновление
|
||||
списка вложений. Строка без файла в хранилище (файл убран уборкой) SHALL считаться «не скачано».
|
||||
|
||||
Состояние вложения для интерфейсов (`state`) SHALL вычисляться при чтении:
|
||||
- `ready` — есть строка `attachment_file` и файл `file` есть в хранилище;
|
||||
- `fetching` — нет `ready`, и есть `fetch_attachment` с этим `attachment_id` в `queued` или `claimed`;
|
||||
- `failed` — нет ни того, ни другого, а последняя (по `id`) `fetch_attachment` с этим `attachment_id` — `failed` или
|
||||
`cancelled`; её `error` отдаётся вместе с состоянием;
|
||||
- `none` — вложение ни разу не запрашивали (или запрос был до уборки файла).
|
||||
|
||||
#### Scenario: Файл убран уборкой
|
||||
- **WHEN** у вложения #31337 строка `attachment_file`, файла `file` в `files/` нет; команд по нему в очереди нет, последняя
|
||||
`fetch_attachment` по нему — `done`
|
||||
- **THEN** `state = none`; новый запрос на скачивание ставит новую `fetch_attachment`
|
||||
|
||||
#### Scenario: Список вложений обновился
|
||||
- **WHEN** вложение #31337 скачано (`state = ready`), затем пришло состояние задачи с тем же вложением
|
||||
- **THEN** `state` по-прежнему `ready`
|
||||
|
||||
@ -47,12 +47,17 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача
|
||||
пачка применяется независимо; `page` и `last_page` необязательны и хабом не используются. Поле задачи `comments`
|
||||
необязательно; если оно есть, хаб SHALL до записи проверить его форму: список объектов, `id` — непустая строка, `body` —
|
||||
строка, `author`, `created`, `updated` — строка или `null`; нарушение — 422 `bad_state` с ключом задачи и именем поля,
|
||||
пачка не применяется.
|
||||
пачка не применяется. Поле задачи `attachments` (агент v14) необязательно и проверяется так же: список объектов, `id` —
|
||||
непустая строка, `filename` — непустая строка, `size` — неотрицательное целое или `null`, `mime_type`, `author`,
|
||||
`created` — строка или `null`.
|
||||
- `GET /agent/known-keys` — ключи зеркала (для определения соседей).
|
||||
- `GET /agent/release` — опубликованная версия агента `{version, sha256, content}` (`content` — текст `agent.py`);
|
||||
нет публикации или файл не сходится с манифестом по sha256 — 404 `no_release`.
|
||||
- `GET /agent/files/{sha256}` — файл вложения из хранилища хаба `{file, size, content}` (`content` — base64 содержимого);
|
||||
файла нет — 404 `no_file`.
|
||||
- `POST /agent/files` `{content}` — файл вложения, скачанный агентом из Jira, в хранилище хаба; ответ `{file, size}`;
|
||||
проверки и ошибки — как у `POST /files` (см. «Хранилище файлов вложений»), кроме пустого файла: от агента он
|
||||
принимается (в Jira бывают пустые вложения).
|
||||
|
||||
#### Scenario: Выдача с подстановкой ключа
|
||||
- **WHEN** следующая команда ссылается на `new:60`, а `ensure_issue` для `new:60` завершилась с `AURORA-1590`
|
||||
@ -108,15 +113,21 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача
|
||||
зеркала (вне базовой JQL).
|
||||
- `GET /issues/{key}` — задача целиком: поля, `epic_summary` (тема эпика из зеркала или `null`), связи в обе стороны
|
||||
(`links_out: [{target_key, link_type, summary, status}]`, `links_in: [{source_key, link_type, summary, status}]`, где
|
||||
`summary` и `status` — связанной задачи из зеркала или `null`, если её там нет), заметки, последние 10 команд по ней,
|
||||
`comments: [{comment_id, author, created, updated, body}]` — все комментарии, новые сверху (по `created`, затем
|
||||
`comment_id`, по убыванию).
|
||||
`summary` и `status` — связанной задачи из зеркала или `null`, если её там нет), заметки, последние 10 команд по ней (без `fetch_attachment`),
|
||||
`comments: [{comment_id, author, created, updated, body, attachment_ids}]` — все комментарии, новые сверху (по `created`, затем
|
||||
`comment_id`, по убыванию); `attachments: [{attachment_id, filename, size, mime_type, author, created, state, error}]` —
|
||||
все вложения задачи, новые сверху (по `created_at`, затем `attachment_id`, по убыванию), `state` и `error` — см.
|
||||
`data-model`, «Скачанные вложения» (`error` — только при `failed`, иначе `null`); `description_attachment_ids` и
|
||||
`attachment_ids` комментария — id вложений, на которые ссылается текст (см. «Ссылки на вложения в тексте»), в порядке
|
||||
первого упоминания, без повторов.
|
||||
- `PUT /issues/{key}/note` — заметки (частичное обновление, `updated_by` из тела).
|
||||
- `POST /batches` `{title, author, project?, commands: [...]}` → `{id, estimate_seconds}`; 422 при неизвестном типе,
|
||||
неверных параметрах (в том числе неверного типа значения), временном ключе без `ensure_issue`, двух и более
|
||||
`ensure_issue` с одинаковой `summary` (сравнение после обрезки пробелов по краям; в ответе перечислены повторяющиеся
|
||||
темы), по проверкам постановки из `commands` (тип `bug` без настройки, файл вложения не загружен, доска для
|
||||
`ensure_sprint`), а также по проверкам пакета проекта (см. `commands`), если передан `project`.
|
||||
- `GET /attachments/{id}`, `POST /attachments/{id}/fetch`, `GET /attachments/{id}/content` — вложения задач (см.
|
||||
«Скачивание вложений»).
|
||||
- `POST /files` `{content}` (`content` — base64 содержимого файла) → `{file, size}`, где `file` — sha256 содержимого
|
||||
(64 hex-символа, нижний регистр); см. «Хранилище файлов вложений».
|
||||
- `GET /batches?limit=&project=` (с `project` — только пакеты этого проекта), `GET /batches/{id}` (с командами и
|
||||
@ -198,13 +209,17 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача
|
||||
- **THEN** в ответе задачи «В работе» и без статуса; без `hide_closed` — все четыре
|
||||
|
||||
### Requirement: Резервные копии
|
||||
Перед выдачей первой команды пакета, содержащего записи в Jira, хаб SHALL скопировать файл базы в `backups/<дата-время>-<batch>.sqlite`
|
||||
и хранить последние `backup_keep` копий.
|
||||
Перед выдачей первой команды пакета, содержащего записи в Jira (любая команда, кроме `refresh` и `fetch_attachment`), хаб
|
||||
SHALL скопировать файл базы в `backups/<дата-время>-<batch>.sqlite` и хранить последние `backup_keep` копий.
|
||||
|
||||
#### Scenario: Ротация
|
||||
- **WHEN** копий больше `backup_keep`
|
||||
- **THEN** самые старые удаляются до `backup_keep`
|
||||
|
||||
#### Scenario: Скачивание без копии
|
||||
- **WHEN** выдаётся `fetch_attachment` пакета «Скачать вложение …»
|
||||
- **THEN** копия базы не создаётся
|
||||
|
||||
### Requirement: Автообновление зеркала
|
||||
Если `auto_refresh_minutes > 0`, агент на связи и есть хотя бы один проект, хаб SHALL поставить пакет «Автообновление»
|
||||
из `refresh {jql: <базовая JQL из проектов>}`, когда в очереди нет команд в `queued` и `claimed` и последний `sync_run`
|
||||
@ -414,9 +429,11 @@ sha256 (без расширения и имени вложения: имя за
|
||||
- запись атомарная: временный файл в `files/`, затем переименование; повторная загрузка того же содержимого отвечает тем
|
||||
же `file` и не создаёт второй копии.
|
||||
Файлы в бэкапы базы MUST NOT входить. Фоновый `tick` хаба SHALL не чаще раза в сутки в пределах работы процесса (после старта — на первом тике) удалять файлы старше 30 дней (по
|
||||
времени изменения), на которые не ссылается ни одна команда `ensure_attachment` в `queued` или `claimed`; повтор команды
|
||||
времени изменения), на которые не ссылается ни одна команда `ensure_attachment` в `queued` или `claimed` (скачанные
|
||||
вложения `attachment_file` от уборки не защищены: их можно скачать снова), и подкаталоги `downloads/<attachment_id>/`
|
||||
(копии MCP, см. `mcp`) старше 30 дней по времени изменения; повтор команды
|
||||
после уборки отвергается при постановке 422 «файл <sha> не загружен: POST /files». Повторная загрузка того же
|
||||
содержимого SHALL обновлять время изменения файла.
|
||||
содержимого SHALL обновлять время изменения файла; выдача файла `GET /attachments/{id}/content` — тоже.
|
||||
|
||||
#### Scenario: Загрузка файла
|
||||
- **WHEN** `POST /files {content: base64("abc")}`
|
||||
@ -435,6 +452,11 @@ sha256 (без расширения и имени вложения: имя за
|
||||
`queued`, файл C загружен вчера
|
||||
- **THEN** после уборки удалён только A
|
||||
|
||||
#### Scenario: Скачанное вложение и уборка
|
||||
- **WHEN** в `files/` файл D скачан агентом (`attachment_file`) 40 дней назад и с тех пор не выдавался; в `downloads/`
|
||||
каталог `31337/` создан 40 дней назад
|
||||
- **THEN** после уборки D и `downloads/31337/` удалены; `state` вложения — `none`
|
||||
|
||||
### Requirement: Маршруты проектов
|
||||
Хаб SHALL предоставлять под клиентским токеном:
|
||||
- `GET /projects` → список `{label, name, board_id, board_type, created_at, issues_count}` по `created_at`, затем `label`;
|
||||
@ -539,3 +561,60 @@ from_string, to, to_string}`. До записи хаб SHALL проверить
|
||||
#### Scenario: Задача с историей
|
||||
- **WHEN** у X в журнале смена статуса вчера и правка описания сегодня; `GET /issues/X`
|
||||
- **THEN** `changes` из двух элементов: первым — правка описания (`kind = description`), вторым — смена статуса
|
||||
|
||||
### Requirement: Скачивание вложений
|
||||
Хаб SHALL предоставлять интерфейсам под клиентским токеном:
|
||||
- `GET /attachments/{id}` — вложение из зеркала `{attachment_id, key, filename, size, mime_type, author, created, state,
|
||||
error, command_id, batch_id}`, где `command_id`/`batch_id` — последней `fetch_attachment` по нему (или `null`); нет в
|
||||
зеркале — 404 `no_attachment`.
|
||||
- `POST /attachments/{id}/fetch` `{author?}` — запросить скачивание. Нет в зеркале — 404 `no_attachment`; `state =
|
||||
ready` или `fetching` — ответ как у `GET /attachments/{id}`, новой команды нет; размер больше 10 МБ — 413 `too_large`;
|
||||
последний известный хабу агент (`agent_status.agent_version`) старше v14 — 409 `agent_too_old` «агент v<N> не умеет
|
||||
скачивать вложения — нужен v14» (версия неизвестна — проверка не делается); иначе хаб SHALL поставить пакет из одной
|
||||
`fetch_attachment` (см. `commands`, автор — из тела, иначе `api`) и ответить как `GET /attachments/{id}` с `state =
|
||||
fetching`. Проверка «уже ставится» и постановка SHALL выполняться в одной транзакции записи: два одновременных запроса
|
||||
ставят одну команду.
|
||||
- `GET /attachments/{id}/content` — содержимое при `state = ready`: 200, тело — байты файла, `Content-Type` — `mime_type`
|
||||
вложения (нет или не вида `тип/подтип` — `application/octet-stream`), `Content-Disposition: attachment;
|
||||
filename*=UTF-8''<имя в URL-кодировке>`, `X-Content-Type-Options: nosniff`, `Content-Length`; иначе 404 `not_fetched`.
|
||||
Отчёт `done` по `fetch_attachment` (`POST /agent/commands/{id}/result`) SHALL записывать `attachment_file` из
|
||||
`result.file` и `result.size`, если файл `file` есть в хранилище и его размер равен `size`; иначе команда SHALL
|
||||
становиться `failed` «хаб: файл <file> не найден в хранилище» и `attachment_file` не меняется.
|
||||
|
||||
#### Scenario: Первый запрос
|
||||
- **WHEN** вложение #31337 в зеркале, `state = none`, агент v14; `POST /attachments/31337/fetch {author: "web"}`
|
||||
- **THEN** создан пакет «Скачать вложение скрин 1.png (AURORA-1650)» из одной `fetch_attachment`, без хвостового
|
||||
`refresh`; ответ `state = fetching` с `command_id` и `batch_id`
|
||||
|
||||
#### Scenario: Повторный запрос во время скачивания
|
||||
- **WHEN** по #31337 команда в `queued`; MCP и веб одновременно вызывают `POST /attachments/31337/fetch`
|
||||
- **THEN** оба ответа — `state = fetching` с той же `command_id`; новых пакетов нет
|
||||
|
||||
#### Scenario: Файл уже у хаба
|
||||
- **WHEN** #31337 в `state = ready`; `GET /attachments/31337/content`
|
||||
- **THEN** 200, `Content-Type: image/png`, `Content-Disposition: attachment; filename*=UTF-8''%D1%81%D0%BA%D1%80%D0%B8%D0%BD%201.png`,
|
||||
тело — файл; время изменения файла в `files/` обновлено
|
||||
|
||||
#### Scenario: Старый агент
|
||||
- **WHEN** `agent_status.agent_version = 13`; `POST /attachments/31337/fetch`
|
||||
- **THEN** 409 `agent_too_old` «агент v13 не умеет скачивать вложения — нужен v14», пакета нет
|
||||
|
||||
#### Scenario: Отчёт без файла в хранилище
|
||||
- **WHEN** агент отчитался `done {file: <sha>, size: 100}`, а файла `<sha>` в `files/` нет
|
||||
- **THEN** команда `failed` «хаб: файл <sha> не найден в хранилище»; `state` вложения — `failed`
|
||||
|
||||
### Requirement: Ссылки на вложения в тексте
|
||||
Хаб SHALL находить в описании задачи и в тексте каждого комментария ссылки на вложения задачи по вики-разметке Jira:
|
||||
`!<имя>!` и `!<имя>|<параметры>!` (картинка; имя без `!`, `|` и переводов строк, не содержит `://`), `[^<имя>]` и
|
||||
`[<текст>|^<имя>]` (ссылка на вложение; имя без `]` и переводов строк). Имя SHALL сравниваться с `filename` вложений
|
||||
задачи точно (с учётом регистра); несколько вложений с одним именем — ссылка указывает на самое новое по `created_at`
|
||||
(как в Jira); имени нет среди вложений — ссылка не учитывается. Ссылки внутри `{code}`/`{noformat}` тоже учитываются.
|
||||
|
||||
#### Scenario: Скриншот в комментарии
|
||||
- **WHEN** у задачи вложения «скрин 1.png» (#31337) и «ОСВ 60.xlsx» (#31338); комментарий 101 — «см.
|
||||
!скрин 1.png|thumbnail! и [^ОСВ 60.xlsx], ещё раз !скрин 1.png!»; описание ссылается на «старый.png», которого нет
|
||||
- **THEN** у комментария 101 `attachment_ids = ["31337", "31338"]`; `description_attachment_ids = []`
|
||||
|
||||
#### Scenario: Два вложения с одним именем
|
||||
- **WHEN** у задачи два вложения «скрин.png» (#10 от 14.10, #20 от 20.10); комментарий — «!скрин.png!»
|
||||
- **THEN** у комментария `attachment_ids = ["20"]`
|
||||
|
||||
@ -22,8 +22,9 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм
|
||||
| `projects` | — | таблица: метка, название, доска, тип доски («неизвестен», пока агент не определил), число своих задач; в привязанной сессии текущий проект отмечен |
|
||||
| `project_create` | `label`, `name`, `board_id` (необязательно) | `POST /projects` с автором `claude`: проект, id пакета обновления и фрагмент `.mcp.json` для репозитория проекта (см. ниже); 409/422 — текст ошибки |
|
||||
| `issues` | фильтры как у `GET /issues` (кроме `project` — его задаёт привязка), `limit` (по умолчанию 50) | таблица: ключ, тема (до 80 символов), статус, срок, исполнитель, спринт, статус по факту |
|
||||
| `issue` | `key` | поля, связи (обе стороны, с типом и направлением), заметки, последние команды, комментарии: «комментарии (N):» и 5 последних (номер «#<comment_id>», автор, время, текст до 300 символов с «…»), «ещё M», если их больше 5; без комментариев — «комментариев нет» |
|
||||
| `comment` | `key`, `comment_id` (номер из ответа `issue`, «#» в начале допускается) | один комментарий целиком по `GET /issues/{key}`: строка «<key> #<id> <автор>, создан <created>» (и «, изменён <updated>», если `updated` есть и отличается от `created`), под ней полный текст с переносами строк; нет такого номера — «у <key> нет комментария #<id>» |
|
||||
| `issue` | `key` | поля, связи (обе стороны, с типом и направлением), заметки, последние команды, комментарии: «комментарии (N):» и 5 последних (номер «#<comment_id>», автор, время, текст до 300 символов с «…»), «ещё M», если их больше 5; без комментариев — «комментариев нет»; у комментария со ссылками на вложения — строка «файлы: #<id> <имя>, …»; затем вложения (см. «Вложения в MCP») |
|
||||
| `comment` | `key`, `comment_id` (номер из ответа `issue`, «#» в начале допускается) | один комментарий целиком по `GET /issues/{key}`: строка «<key> #<id> <автор>, создан <created>» (и «, изменён <updated>», если `updated` есть и отличается от `created`), под ней полный текст с переносами строк; нет такого номера — «у <key> нет комментария #<id>»; есть ссылки на вложения — после текста строка «вложения:» и по строке на каждое: «#<id> <имя> (<размер>)» |
|
||||
| `attachment` | `key`, `attachment` (номер вложения «#<id>» или «<id>», иначе — точное имя файла), `wait_seconds` (по умолчанию 120, не больше 600) | скачивает вложение и отдаёт путь к локальной копии (см. «Вложения в MCP») |
|
||||
| `notes_set` | `key`, подмножество полей заметок | обновлённые заметки |
|
||||
| `batch_create` | `title`, `commands` (список объектов по контракту `commands`; у `ensure_attachment` вместо `file` можно `path`) | `id`, `estimate_seconds`, число команд; 422 — текст ошибки валидации |
|
||||
| `batch_status` | `id` | статусы команд, ошибки, оценка остатка |
|
||||
@ -189,3 +190,50 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм
|
||||
#### Scenario: История в карточке
|
||||
- **WHEN** `issue {key: X}`, у X 7 элементов журнала
|
||||
- **THEN** ответ содержит «изменения (7):», 5 последних событий и «ещё 2»
|
||||
|
||||
### Requirement: Вложения в MCP
|
||||
`issue` SHALL после комментариев выводить «вложения (N):» и по строке на каждое вложение (новые сверху): «#<id> <имя>
|
||||
(<размер>) — <автор>, <created>», дальше через запятую: «в описании», если на вложение ссылается описание, «в #<comment_id>»
|
||||
для каждого ссылающегося комментария, «скачано», если `state = ready`, «больше 10 МБ — только в Jira» для таких файлов;
|
||||
без вложений — «вложений нет». Размер — в байтах, КБ или МБ с одним знаком после запятой («512 Б», «234,4 КБ», «2,3 МБ»);
|
||||
неизвестный — «размер неизвестен».
|
||||
|
||||
`attachment` SHALL:
|
||||
1. найти вложение по `GET /issues/{key}`: по номеру, иначе по точному имени (несколько с одним именем — самое новое, в
|
||||
ответ строка «у <key> N вложений «<имя>», взято самое новое #<id>»); не найдено — «у <key> нет вложения «<значение>»;
|
||||
список — issue <key>», в хаб больше не ходить;
|
||||
2. `POST /attachments/{id}/fetch` с автором `claude`; 413 — «файл больше 10 МБ (<размер>) — скачайте в Jira:
|
||||
задача <key>, вложение «<имя>»»; 409 и прочие ошибки хаба — их текст;
|
||||
3. пока `state = fetching` — опрашивать `GET /attachments/{id}` раз в 2 с, не дольше `wait_seconds`; не дождался —
|
||||
«скачивание ещё идёт (команда #<command_id>, агент <онлайн/офлайн>); вызовите attachment ещё раз — вторая команда не
|
||||
поставится»; `failed` — «не удалось скачать: <error>»;
|
||||
4. при `ready` — `GET /attachments/{id}/content` и записать файл в `<каталог данных хаба>/downloads/<id>/<имя>`, где имя —
|
||||
`filename` с заменой `/`, `\`, `:` и управляющих символов на `_` (пустое — `attachment-<id>`); каталог с правами
|
||||
`0700`; запись через временный файл и переименование; если там уже лежит файл того же размера — не перезаписывать;
|
||||
5. ответить «<абсолютный путь>» отдельной строкой и под ним «<имя> (<размер>), <mime_type>, задача <key>».
|
||||
|
||||
#### Scenario: Скриншот из комментария
|
||||
- **WHEN** `comment {key: "AURORA-1650", comment_id: "101"}` показал «вложения: #31337 скрин 1.png (234,4 КБ)»; вызван
|
||||
`attachment {key: "AURORA-1650", attachment: "#31337"}`, агент онлайн, файла у хаба нет
|
||||
- **THEN** поставлена `fetch_attachment`; через несколько секунд ответ — путь
|
||||
`…/jira-hub/downloads/31337/скрин 1.png` и строка «скрин 1.png (234,4 КБ), image/png, задача AURORA-1650»; файл по пути
|
||||
читается
|
||||
|
||||
#### Scenario: Повторный вызов
|
||||
- **WHEN** тот же вызов второй раз, файл у хаба (`ready`) и в `downloads/31337/`
|
||||
- **THEN** ответ сразу, команд в очереди не прибавилось
|
||||
|
||||
#### Scenario: Агент не успел
|
||||
- **WHEN** `attachment {…, wait_seconds: 30}`, агент занят долгим `refresh`
|
||||
- **THEN** через 30 с ответ «скачивание ещё идёт (команда #N, агент онлайн); вызовите attachment ещё раз — вторая
|
||||
команда не поставится»
|
||||
|
||||
#### Scenario: По имени
|
||||
- **WHEN** `attachment {key: "AURORA-1650", attachment: "ОСВ 60.xlsx"}`, вложение с таким именем одно
|
||||
- **THEN** скачивается оно; путь оканчивается на `/ОСВ 60.xlsx`
|
||||
|
||||
#### Scenario: Вложения в карточке
|
||||
- **WHEN** `issue {key: "AURORA-1650"}`, у задачи вложения #31337 «скрин 1.png» (на него ссылается комментарий 101,
|
||||
скачано) и #31338 «запись.mp4» на 25 МБ
|
||||
- **THEN** в ответе «вложения (2):», строка #31338 с «больше 10 МБ — только в Jira», строка #31337 с «в #101, скачано»; у
|
||||
комментария 101 строка «файлы: #31337 скрин 1.png»
|
||||
|
||||
@ -293,15 +293,18 @@ SHALL быть по умолчанию (в адресе ничего), «Все
|
||||
- «Описание» — текст, отрендеренный по требованию «Разметка Jira в карточке», с кнопкой «исходник» / «текст» в
|
||||
заголовке раздела; длиннее 8 строк — свёрнуто с кнопкой «развернуть» / «свернуть» в обоих режимах; без описания
|
||||
раздела нет;
|
||||
- «Вложения (N)» — новые сверху: значок по типу файла (картинка, таблица, PDF, прочее), имя, размер, «автор · дата» и
|
||||
кнопка скачивания (требование «Скачивание вложений в карточке»); без вложений раздела нет;
|
||||
- «Комментарии (N)» — новые сверху, автор, время, кнопка «исходник» / «текст» у каждого комментария, текст,
|
||||
отрендеренный по требованию «Разметка Jira в карточке»; кнопка «Скрыть» / «Показать» сворачивает раздел, выбор SHALL
|
||||
отрендеренный по требованию «Разметка Jira в карточке», под текстом — вложения, на которые ссылается комментарий
|
||||
(`attachment_ids`): строка «плашек» с именем и размером, щелчок — скачивание; кнопка «Скрыть» / «Показать» сворачивает раздел, выбор SHALL
|
||||
запоминаться в браузере для всех карточек; без комментариев — «комментариев нет»;
|
||||
- «Служебные заметки» (требование «Служебные заметки»);
|
||||
- «Прочие поля» — непустые из: тип, приоритет, автор, метки, плановое начало, обновлена в Jira, сосед по связям;
|
||||
- «Команды моста (N)» — 10 последних команд по задаче (id, пакет, тип, статус, время завершения, ошибка), раздел свёрнут
|
||||
при открытии; если команд не было — текст «команд по задаче не было».
|
||||
На ширине от 900 px «Связи», «Описание», «Комментарии» — в левой колонке, заметки, прочие поля и команды — в правой;
|
||||
уже — одна колонка: заметки, связи, описание, комментарии, прочие поля, команды. Если задачи нет в зеркале (404), окно
|
||||
На ширине от 900 px «Связи», «Описание», «Вложения», «Комментарии» — в левой колонке, заметки, прочие поля и команды — в правой;
|
||||
уже — одна колонка: заметки, связи, описание, вложения, комментарии, прочие поля, команды. Если задачи нет в зеркале (404), окно
|
||||
SHALL сообщать об этом с кнопкой «Закрыть»; хаб не отвечает — сообщение и «Повторить», ранее загруженные данные
|
||||
остаются.
|
||||
|
||||
@ -400,8 +403,12 @@ SHALL сообщать об этом с кнопкой «Закрыть»; ха
|
||||
`http`, `https`, `mailto`, иначе текст как есть; `[ключ задачи]`, `[текст|ключ задачи]` и ключ задачи в тексте вида
|
||||
`ПРОЕКТ-123`, где `ПРОЕКТ` — `project_key` из настроек хаба, — ссылка, открывающая карточку этой задачи в окне
|
||||
(`UTF-8`, `SHA-256` и ключи других проектов — текст);
|
||||
`[~логин]` — «@логин».
|
||||
Всё, что разборщик не распознал (`{panel}`, `{color}`, `!картинка!`, незакрытые маркеры и блоки и прочее), SHALL
|
||||
`[~логин]` — «@логин»;
|
||||
- ссылки на вложения задачи — `!имя!`, `!имя|параметры!`, `[^имя]`, `[текст|^имя]` с тем же разбором имени, что у хаба
|
||||
(`hub-api`, «Ссылки на вложения в тексте»), имя есть среди вложений задачи: ссылка-кнопка со значком скрепки и текстом
|
||||
— именем файла (у `[текст|^имя]` — текстом), щелчок — скачивание; картинка в тексте не показывается; имени нет среди
|
||||
вложений — исходник как есть.
|
||||
Всё, что разборщик не распознал (`{panel}`, `{color}`, `!картинка!` и `[^имя]` без такого вложения у задачи, незакрытые маркеры и блоки и прочее), SHALL
|
||||
показываться как текст исходника без потерь. Разметка MUST NOT превращаться в HTML строкой (`innerHTML`,
|
||||
`dangerouslySetInnerHTML`): текст задачи не может добавить на страницу элементы, атрибуты или скрипты. Если рендер
|
||||
блока завершился ошибкой, блок SHALL показываться исходником.
|
||||
@ -444,6 +451,11 @@ SHALL сообщать об этом с кнопкой «Закрыть»; ха
|
||||
**важно**`
|
||||
- **THEN** текст показан как есть, без курсива, зачёркивания, жирного и ссылок
|
||||
|
||||
#### Scenario: Скриншот в тексте комментария
|
||||
- **WHEN** комментарий — «см. !скрин 1.png|thumbnail!», у задачи есть вложение «скрин 1.png»
|
||||
- **THEN** вместо `!скрин 1.png|thumbnail!` — кнопка со скрепкой «скрин 1.png»; под комментарием плашка «скрин 1.png ·
|
||||
234,4 КБ»; щелчок по любой из них скачивает файл
|
||||
|
||||
### Requirement: Проект в приложении
|
||||
Доска и список задач SHALL показывать один проект — текущий. Текущий проект SHALL задаваться параметром `project` в
|
||||
адресе `/app/board` и `/app/issues`; без параметра — последний выбранный в этом браузере (`localStorage`), если он есть
|
||||
@ -613,3 +625,36 @@ Semibold 14/20, заголовок страницы Bold 20/28. Страницы
|
||||
#### Scenario: Карточка с историей
|
||||
- **WHEN** открыта карточка задачи с 12 событиями журнала
|
||||
- **THEN** блок «История (12)» показывает 10 последних событий и кнопку «Показать все (12)»; после нажатия — все 12
|
||||
|
||||
### Requirement: Скачивание вложений в карточке
|
||||
Кнопка скачивания вложения (в разделе «Вложения», плашка под комментарием, ссылка в тексте) SHALL работать одинаково:
|
||||
1. `POST /attachments/{id}/fetch {author: "web"}`;
|
||||
2. `state = ready` — `GET /attachments/{id}/content` с клиентским токеном, браузер сохраняет файл под именем `filename`
|
||||
(через `Blob` и временную ссылку `download`; адрес с токеном в строке браузера MUST NOT появляться);
|
||||
3. `state = fetching` — у всех кнопок этого вложения в карточке вместо «Скачать» — «Загружается…» (неактивна), опрос
|
||||
`GET /attachments/{id}` раз в 2 с, пока окно открыто на этой задаче; `ready` — шаг 2 сам, без второго нажатия; закрыли
|
||||
окно или ушли на другую задачу — опрос прекращается, команда остаётся в очереди (файл ляжет в хаб);
|
||||
4. `failed` — под вложением в разделе «Вложения» текст ошибки и кнопка «Повторить» (новый запрос); 409 `agent_too_old`
|
||||
и прочие ошибки хаба — их текст там же.
|
||||
Вложение с `state = ready` в разделе «Вложения» помечается «у хаба» и скачивается сразу. Размер неизвестен — «размер неизвестен» и обычная кнопка.
|
||||
Вложение больше 10 МБ —
|
||||
вместо кнопки ссылка «Открыть в Jira» (`<адрес Jira>/secure/attachment/<id>/<имя в URL-кодировке>`, новая вкладка) с
|
||||
подсказкой «больше 10 МБ — только в Jira»; плашка и ссылка в тексте ведут туда же.
|
||||
|
||||
#### Scenario: Скачать скриншот
|
||||
- **WHEN** в карточке AURORA-1650 нажато «Скачать» у «скрин 1.png», файла у хаба нет, агент онлайн
|
||||
- **THEN** кнопка — «Загружается…», через несколько секунд браузер сохраняет «скрин 1.png», кнопка снова «Скачать», у
|
||||
вложения отметка «у хаба»
|
||||
|
||||
#### Scenario: Окно закрыли во время скачивания
|
||||
- **WHEN** нажато «Скачать», затем окно карточки закрыто
|
||||
- **THEN** опросов `GET /attachments/{id}` больше нет; при следующем открытии карточки, если файл уже у хаба, вложение с
|
||||
отметкой «у хаба»
|
||||
|
||||
#### Scenario: Большой файл
|
||||
- **WHEN** у вложения «запись.mp4» размер 25 МБ
|
||||
- **THEN** вместо «Скачать» — ссылка «Открыть в Jira» с подсказкой «больше 10 МБ — только в Jira»
|
||||
|
||||
#### Scenario: Агент старой версии
|
||||
- **WHEN** нажато «Скачать», хаб ответил 409 «агент v13 не умеет скачивать вложения — нужен v14»
|
||||
- **THEN** под вложением этот текст и «Повторить»
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user