28 KiB
MODIFIED Requirements
Requirement: Маршруты агента
Хаб SHALL предоставлять агенту следующие маршруты (все под агентским токеном):
POST /agent/claim{agent_version, settings_version, last_jira_call_at, update_state, launcher_version}— долгий запрос до 30 с; ответ{command: {id, type, params, pause_seconds}, settings_version, agent_release}или{command: null, settings_version, agent_release}, гдеagent_release—{version, sha256}опубликованной версии агента илиnull; обновляетagent_status(last_jira_call_at = nullне затирает прежнее значение).update_state(объект илиnull,errorхранится до 500 символов) иlauncher_version(целое илиnull) необязательны; хаб SHALL хранить последние значения в памяти процесса, значения неверного типа игнорировать, не отвергаяclaim. Если есть команда вclaimed,claimSHALL выдать её снова (повторная выдача, см.commands), а не следующую из очереди.GET /agent/settings— настройки для агента (без токенов).GET /agent/commands/{id}— команда по id (для восстановления из журнала), включаяpause_seconds.POST /agent/commands/{id}/result{status, result, error, jira_calls, rate_limited}— идемпотентно: повторный отчёт по команде в терминальном статусе отвечает 200 без изменений, кроме позднего отчётаdoneсresult.keyпоensure_issueвcancelledилиfailedбез результата (результат записывается, статус не меняется, заметкаnew:Nпереносится на ключ). Отчёт по команде вqueuedSHALL отвергаться 409not_claimed.errorхаб SHALL обрезать до 1500 символов.POST /agent/state{command_id, issues: [...], links: [...], neighbor_keys: [...], page, last_page}— загрузка состояния пачками;neighbor_keys— ключи вне базовой JQL, дочитанные по связям; хаб обновляет зеркало иsync_run. Каждая пачка применяется независимо;pageиlast_pageнеобязательны и хабом не используются. Поле задачиcommentsнеобязательно; если оно есть, хаб SHALL до записи проверить его форму: список объектов,id— непустая строка,body— строка,author,created,updated— строка илиnull; нарушение — 422bad_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 — 404no_release.GET /agent/files/{sha256}— файл вложения из хранилища хаба{file, size, content}(content— base64 содержимого); файла нет — 404no_file.POST /agent/files{content}— файл вложения, скачанный агентом из Jira, в хранилище хаба; ответ{file, size}; проверки и ошибки — как уPOST /files(см. «Хранилище файлов вложений»), кроме пустого файла: от агента он принимается (в Jira бывают пустые вложения).
Scenario: Выдача с подстановкой ключа
- WHEN следующая команда ссылается на
new:60, аensure_issueдляnew:60завершилась сAURORA-1590 - THEN
claimотдаёт команду сAURORA-1590в параметрах
Scenario: Повторный отчёт
- WHEN агент дважды прислал
resultдля команды 15 - THEN второй ответ 200, статус и результат не изменились
Scenario: Отчёт по невыданной команде
- WHEN
POST /agent/commands/16/resultсstatus = done, команда 16 вqueued - THEN 409
not_claimed, команда осталасьqueued
Scenario: Длинная ошибка
- WHEN агент прислал
failedсerrorдлиной 5000 символов - THEN в
command.errorпервые 1500 символов
Scenario: Поздний отчёт после предела выдач
- WHEN
ensure_issue {temp_key: new:8, …}переведена хабом вfailed«агент не отчитался после 3 выдач», затем агент прислалdoneсresult.key = AURORA-1630 - THEN команда
failed, вresult—AURORA-1630; заметкаnew:8перенесена наAURORA-1630
Scenario: Публикация в ответе claim
- WHEN опубликована v6, агент v5 отправил
claim, команд нет - THEN ответ
{command: null, settings_version, agent_release: {version: 6, sha256: …}}
Scenario: Состояние обновления от агента
- WHEN агент прислал в
claimupdate_state = {state: rolled_back, from_version: 6, to_version: 7, error: "код 1 через 3 с"}иlauncher_version = 1 - THEN
GET /agentотдаёт этоupdate_state,launcher_version = 1иupdate_hint«откат с v7 на v6: код 1 через 3 с»
Scenario: Скачивание версии
- WHEN агентским токеном
GET /agent/release, опубликована v6 - THEN 200
{version: 6, sha256, content}, sha256 отcontentв UTF-8 равенsha256; клиентским токеном — 401; без публикации — 404no_release
Scenario: Скачивание файла вложения
- WHEN агентским токеном
GET /agent/files/<sha>, файл загружен - THEN 200
{file: <sha>, size, content}, sha256 от декодированногоcontentравен<sha>; клиентским токеном — 401; файла нет — 404no_file
Scenario: Неверный комментарий в пачке
- WHEN
POST /agent/stateс задачами X и Y, у Y комментарий{id: "7", body: 42} - THEN 422
bad_stateс ключом Y и полемbody; ни X, ни Y в зеркале не изменились
Requirement: Маршруты интерфейсов
Хаб SHALL предоставлять интерфейсам (MCP, веб-приложение) следующие маршруты под клиентским токеном:
GET /issuesс фильтрамиproject(разрез по проекту, см.data-model, «Проекты»: свои и соседи,neighborотносительно проекта; неизвестный проект — 404),sprint,assignee,work_kind,fact_status,status,label,epic,neighbor,q(подстрока темы),hide_closed(1/true— без задач, чейstatusвходит в настройкуclosed_statuses; задача без статуса не закрыта),limit/offset— строки зеркала (все поляissue) сfact_statusиwork_kindиз заметок; сокращение до карточки — дело интерфейса. Безproject— всё зеркало,neighbor— признак зеркала (вне базовой 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 команд по ней (без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}(с командами иproject),POST /batches/{id}/cancel— отменяетqueuedкоманды пакета; ответ — вид пакета с полемstill_claimed: список{id, type}команд пакета вclaimed(они будут выполнены агентом; остановить — отмена команды сforce).POST /commands/{id}/retry{author?, project?}— новая команда с теми же параметрами в новом пакете «Повтор команды N»; хаб снимает с параметров то, что дописывает сам (board_idуensure_sprint), и проверяет команду заново.projectпередан и у исходного пакета другой проект (или его нет) — 422 «команда #N не из проекта P»; не передан — новый пакет получает проект исходного и проходит его проверки.POST /commands/{id}/cancel— только дляqueued; дляclaimed— 409, если не переданforce: true.POST /refresh{keys | jql | full: true}→ пакет из одногоrefresh;full— по базовой JQL из проектов, без проектов — 422 «нет проектов».GET /agent,GET /sync-runs?limit=,GET /settings,PUT /settings(частично; поднимаетsettings_version; ответ — все настройки после записи, как уGET /settings, включаяsettings_version).
Scenario: Фильтр по спринту
- WHEN
GET /issues?sprint=Спринт 2 - THEN только задачи с таким
sprint, отсортированы по сроку, затем по ключу
Scenario: Список проекта
- WHEN свои задачи
STOCKS_DRRK— AURORA-1 и AURORA-4, сосед — AURORA-2;GET /issues?project=STOCKS_DRRK - THEN три задачи, у AURORA-2
neighbor = true; сneighbor=1— только AURORA-2; задач только с меткойAWPCAнет
Scenario: Неизвестный проект
- WHEN
GET /issues?project=NOPE - THEN 404
Scenario: Пакеты проекта
- WHEN пакет 10 поставлен с
project = STOCKS_DRRK, пакет 11 — без проекта;GET /batches?project=STOCKS_DRRK - THEN в ответе только пакет 10
Scenario: Повтор чужой команды
- WHEN команда 40 из пакета проекта
AWPCA;POST /commands/40/retry {project: "STOCKS_DRRK"} - THEN 422 «команда #40 не из проекта STOCKS_DRRK», пакет не создан
Scenario: Повтор ensure_sprint
- WHEN
ensure_sprint41 failed, в еёparamsboard_id = 4216;POST /commands/41/retry - THEN новый пакет создан,
board_idв новой команде вычислен хабом заново
Scenario: Полный прогон без проектов
- WHEN проектов нет,
POST /refresh {full: true} - THEN 422 «нет проектов», пакет не поставлен
Scenario: Отмена взятой команды
- WHEN
POST /commands/15/cancelбезforce, командаclaimed - THEN 409 с
claimed_at; сforce: true—cancelled, следующий отчёт агента по ней принимается, статус не меняется
Scenario: Поздний отчёт по отменённому созданию
- WHEN
ensure_issue {temp_key: new:7, …}отменена сforce: true, затем агент прислалdoneсresult.key = AURORA-1620 - THEN команда
cancelled, вresult—AURORA-1620; заметкаnew:7перенесена наAURORA-1620
Scenario: Дубль темы в пакете
- WHEN
POST /batchesс двумяensure_issue, у обеихsummary = "Шахматка: комментарии" - THEN 422 с темой «Шахматка: комментарии», пакет не создан
Scenario: Тема не строка
- WHEN
POST /batchesсensure_issue {temp_key: new:1, summary: 42, …} - THEN 422 с указанием параметра
summary, пакет не создан
Scenario: Отмена пакета с командой у агента
- WHEN в пакете 9 команда 30
claimed, команды 31 и 32queued;POST /batches/9/cancel - THEN 31 и 32
cancelled; в ответеstill_claimed = [{id: 30, type: …}]; 30 остаётсяclaimed
Scenario: Ответ на запись настроек
- WHEN
PUT /settings {relay_dir: "/Volumes/externalSSD/utm/rshb/jira-bridge"} - THEN 200; ответ содержит
relay_dirс записанным значением, новуюsettings_versionи остальные настройки; токенов в ответе нет
Scenario: Карточка с комментариями и связями
- WHEN у AURORA-9001 два комментария (14.10 и 15.10), исходящая связь на AURORA-9002 («API свода», «Новая») и на AURORA-1, которой нет в зеркале; эпик AURORA-9100 «Тестовый эпик»
- THEN
GET /issues/AURORA-9001отдаётcommentsс комментарием от 15.10 первым, у связи на AURORA-9002 —summary = "API свода",status = "Новая", у связи на AURORA-1 —summary = null,status = null;epic_summary = "Тестовый эпик"
Scenario: Без закрытых
- WHEN
closed_statuses = ["Готово", "Снято"], в зеркале задачи в статусах «В работе», «Готово», «Снято» и без статуса;GET /issues?hide_closed=1 - THEN в ответе задачи «В работе» и без статуса; без
hide_closed— все четыре
Requirement: Хранилище файлов вложений
Хаб SHALL хранить файлы вложений в каталоге files/ внутри каталога данных, по одному файлу на содержимое с именем
sha256 (без расширения и имени вложения: имя задаёт команда). Каталог SHALL иметь права 0700 (выставляются при создании
и при каждом старте, как у backups/). POST /files:
contentне строка или не base64 — 400bad_request; пустой файл — 422empty_file;- файл больше 10 МБ (10 485 760 байт) после декодирования — 413
too_large(тело запроса до 16 МБ проходит); - запись атомарная: временный файл в
files/, затем переименование; повторная загрузка того же содержимого отвечает тем жеfileи не создаёт второй копии. Файлы в бэкапы базы MUST NOT входить. Фоновыйtickхаба SHALL не чаще раза в сутки в пределах работы процесса (после старта — на первом тике) удалять файлы старше 30 дней (по времени изменения), на которые не ссылается ни одна командаensure_attachmentвqueuedилиclaimed(скачанные вложенияattachment_fileот уборки не защищены: их можно скачать снова), и подкаталогиdownloads/<attachment_id>/(копии MCP, см.mcp) старше 30 дней по времени изменения; повтор команды после уборки отвергается при постановке 422 «файл не загружен: POST /files». Повторная загрузка того же содержимого SHALL обновлять время изменения файла; выдача файлаGET /attachments/{id}/content— тоже.
Scenario: Загрузка файла
- WHEN
POST /files {content: base64("abc")} - THEN 200
{file: "ba7816bf…", size: 3}; вfiles/файлba7816bf…с содержимымabc
Scenario: Повторная загрузка
- WHEN тот же
POST /filesвторой раз - THEN 200 с тем же
file; вfiles/по-прежнему один файл
Scenario: Слишком большой файл
- WHEN
POST /filesс файлом 11 МБ - THEN 413
too_large, вfiles/ничего не появилось
Scenario: Уборка старых файлов
- WHEN в
files/файл A старше 30 дней без ссылок, файл B старше 30 дней, на него ссылаетсяensure_attachmentвqueued, файл C загружен вчера - THEN после уборки удалён только A
Scenario: Скачанное вложение и уборка
- WHEN в
files/файл D скачан агентом (attachment_file) 40 дней назад и с тех пор не выдавался; вdownloads/каталог31337/создан 40 дней назад - THEN после уборки D и
downloads/31337/удалены;stateвложения —none
Requirement: Резервные копии
Перед выдачей первой команды пакета, содержащего записи в Jira (любая команда, кроме refresh и fetch_attachment), хаб
SHALL скопировать файл базы в backups/<дата-время>-<batch>.sqlite и хранить последние backup_keep копий.
Scenario: Ротация
- WHEN копий больше
backup_keep - THEN самые старые удаляются до
backup_keep
Scenario: Скачивание без копии
- WHEN выдаётся
fetch_attachmentпакета «Скачать вложение …» - THEN копия базы не создаётся
ADDED Requirements
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); нет в зеркале — 404no_attachment.POST /attachments/{id}/fetch{author?}— запросить скачивание. Нет в зеркале — 404no_attachment;state = readyилиfetching— ответ как уGET /attachments/{id}, новой команды нет; размер больше 10 МБ — 413too_large; последний известный хабу агент (agent_status.agent_version) старше v14 — 409agent_too_old«агент v не умеет скачивать вложения — нужен 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; иначе 404not_fetched. Отчётdoneпоfetch_attachment(POST /agent/commands/{id}/result) SHALL записыватьattachment_fileизresult.fileиresult.size, если файлfileесть в хранилище и его размер равенsize; иначе команда SHALL становитьсяfailed«хаб: файл не найден в хранилище» и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«хаб: файл не найден в хранилище»;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"]