184 lines
18 KiB
Markdown
184 lines
18 KiB
Markdown
## MODIFIED Requirements
|
||
|
||
### Requirement: Синхронизация вики
|
||
Команда `wiki_sync {label, root}` SHALL быть командой чтения модуля `wiki` (без копии базы) в пакете с `project =
|
||
label`. Её SHALL ставить: `POST /wiki/sync {project}`, задание или смена корня, фоновая работа хаба — по каждому
|
||
проекту с корнем, если с последней успешной или неудачной синхронизации прошло больше `wiki_auto_sync_hours`
|
||
(настройка модуля, по умолчанию 24; `0` — выключено). Пока у проекта есть пакет `wiki_sync` не в терминальном статусе,
|
||
новый не ставится: `POST /wiki/sync` отвечает 409 `sync_in_progress` с `batch_id` текущего. Любая другая постановка
|
||
пакета с `wiki_sync` (`POST /batches`, повтор команды `POST /commands/{id}/retry`) SHALL отклоняться с 422, если у
|
||
проекта уже есть команда `wiki_sync` с тем же `root` в `queued` или `claimed`; текст ошибки называет пакет идущей
|
||
синхронизации. Команда с другим `root` не мешает: смена корня ставит синхронизацию нового корня, пока старая ещё
|
||
выдана агенту. Смена или сброс корня SHALL отменять невыданные (`queued`) пакеты `wiki_sync` проекта и при новом корне ставить новый; отчёт `done` по
|
||
команде, чей `root` не равен текущему корню проекта, SHALL отбрасываться без изменения копии и данных
|
||
(`result.discarded = "корень сменился"`).
|
||
|
||
Агент `wiki` SHALL прислать все страницы поддерева корня (включая корень) пачками `POST /agent/wiki/pages {command_id,
|
||
pages}` — каждая страница `{code, eva_id, title, parent_code, orderno, url, html, attachments: [{ref, url, name}],
|
||
links: [{ref, code}]}`, где `attachments` — ссылки и картинки на файлы Eva, `links` — ссылки на документы Eva, `ref` —
|
||
значение атрибута `src`/`href` ровно как в `html`, `url` — абсолютный адрес, `name` — `alt`/`title`, текст ссылки или
|
||
имя файла; в пачке не больше 50 страниц, — и отчитаться `done` с `result = {root_title, codes, eva_calls}`, где
|
||
`codes` — коды всех страниц дерева в порядке обхода. Хаб SHALL при приёме пачки переводить HTML в Markdown вне
|
||
транзакции записи и складывать результат во временную стадию команды, не трогая копию. Пачка по
|
||
команде не в `claimed` или не `wiki_sync` — 409; неверная форма страницы — 422 `bad_pages` с кодом страницы и полем.
|
||
|
||
По отчёту `done` хаб SHALL одной операцией заменить копию проекта (см. «Markdown-копия вики»), страницы, индекс поиска и
|
||
вложения и записать `synced_at`, `pages_count`, `root_title`, очистив `sync_error`. Код из `codes` без присланной
|
||
страницы — команда `failed` «нет страницы <код>». Отчёт `failed` или перевод в `failed` по пределу выдач SHALL записать
|
||
`sync_error`; копия, страницы и вложения остаются прежними. Повторная выдача команды SHALL очищать её стадию. Стадия
|
||
очищается при любом терминальном статусе команды.
|
||
|
||
#### Scenario: Первая синхронизация
|
||
- **WHEN** у `AWPCA` корень DOC-1 с детьми DOC-2, DOC-3; агент прислал три страницы одной пачкой и `done {root_title:
|
||
"АРМ КА", codes: [DOC-1, DOC-2, DOC-3]}`
|
||
- **THEN** в `wiki/AWPCA/` три `index.md`; `GET /projects` — `wiki_pages = 3`, `wiki_root_title = АРМ КА`,
|
||
`wiki_synced_at` заполнен
|
||
|
||
#### Scenario: Неудачная синхронизация не портит копию
|
||
- **WHEN** копия `AWPCA` из 3 страниц, следующая синхронизация завершилась `failed` «Eva: HTTP 503»
|
||
- **THEN** копия и поиск прежние; `wiki_sync_error = "Eva: HTTP 503"`
|
||
|
||
#### Scenario: Потерянная пачка
|
||
- **WHEN** отчёт `done` с `codes: [DOC-1, DOC-2]`, пачки пришли только для DOC-1
|
||
- **THEN** команда `failed` «нет страницы DOC-2», копия прежняя
|
||
|
||
#### Scenario: Страница удалена в Eva
|
||
- **WHEN** в прошлой копии были DOC-1, DOC-2, DOC-3, новая синхронизация вернула DOC-1, DOC-3
|
||
- **THEN** каталога DOC-2 нет, поиск его не находит, его вложения удалены
|
||
|
||
#### Scenario: Корень исправлен, пока агент не установлен
|
||
- **WHEN** у `AWPCA` корень DOC-12 и пакет `wiki_sync` в `queued`; `PATCH /projects/AWPCA {wiki_root: "DOC-21"}`
|
||
- **THEN** прежний пакет `cancelled`, поставлен `wiki_sync {root: DOC-21}`
|
||
|
||
#### Scenario: Поздний отчёт по старому корню
|
||
- **WHEN** `wiki_sync {root: DOC-12}` выдан агенту, корень сменён на DOC-21, агент отчитался `done`
|
||
- **THEN** копия и страницы `AWPCA` не изменились, команда `done` с `result.discarded`
|
||
|
||
#### Scenario: Повторный запрос синхронизации
|
||
- **WHEN** пакет `wiki_sync` проекта `AWPCA` выполняется, вызван `POST /wiki/sync {project: AWPCA}`
|
||
- **THEN** 409 `sync_in_progress` с `batch_id` текущего пакета, нового пакета нет
|
||
|
||
#### Scenario: Повтор упавшей синхронизации во время идущей
|
||
- **WHEN** команда `wiki_sync {label: AWPCA, root: DOC-1}` 40 в `failed`, фоновая работа поставила пакет 41 с
|
||
`wiki_sync {root: DOC-1}` (`queued`); `POST /commands/40/retry`
|
||
- **THEN** 422, в тексте — пакет 41; нового пакета нет
|
||
|
||
#### Scenario: Смена корня при выданной синхронизации
|
||
- **WHEN** `wiki_sync {root: DOC-12}` выдан агенту (`claimed`); `PATCH /projects/AWPCA {wiki_root: "DOC-21"}`
|
||
- **THEN** 200, поставлен `wiki_sync {root: DOC-21}`
|
||
|
||
### Requirement: Markdown-копия вики
|
||
Копия проекта SHALL лежать в `<SDLC_HUB_HOME>/wiki/<метка>/` (каталоги 0700): у каждой страницы — каталог
|
||
`NN_<название>` внутри каталога родителя (`NN` — двузначная позиция среди соседей по `orderno`, затем по коду; у корня
|
||
номера нет; название без символов `<>:"/\|?*` и управляющих, обрезано до 80 символов; совпадение у соседей —
|
||
суффикс `_2`, `_3`), в нём `index.md`: frontmatter `code`, `title`, `url`, `synced_at`, строка `# <title>` и текст. В
|
||
корне копии — `_tree.json`, список `{code, title, depth, path}` в порядке обхода. Копия SHALL собираться рядом
|
||
(`wiki/.<метка>.new/`) и подменять прежнюю переименованием; незавершённую подмену хаб SHALL доводить при старте.
|
||
|
||
HTML страницы SHALL переводиться в Markdown (заголовки `#`); ссылка или картинка на файл Eva (по списку `attachments` агента: адрес на хосте Eva
|
||
с путём `/files/…`, относительный адрес — тоже Eva; миниатюра `<адрес>.meta/thumbnail.jpg` означает файл `<адрес>`, а
|
||
ссылки на `static/…` — значки интерфейса — из текста удаляются) SHALL заменяться на
|
||
`[📎 <имя>](attachment:<id>)`, где `id` — строка вложения; ссылка на документ Eva с кодом `DOC-<цифры>` (в Eva — относительный
|
||
`project/Document/DOC-…`) — на `[<текст>](page:<код>)`; прочие ссылки остаются. Сырой HTML в копию не попадает.
|
||
|
||
Перевод SHALL давать корректный Markdown на любом HTML Eva:
|
||
- картинка внутри ссылки на то же вложение — на любой глубине вложенности (`<a><span><img></span></a>`) — даёт одну
|
||
ссылку на вложение;
|
||
- вложение внутри ссылки на документ Eva выводится отдельной ссылкой на вложение перед ссылкой на страницу; текст
|
||
ссылки на страницу — её текст без вложения, пустой — код страницы; ссылка внутри ссылки не появляется;
|
||
- при нескольких записях `attachments` или `links` с одним `ref` действует первая;
|
||
- пробел и круглые скобки в адресе внешней ссылки или картинки кодируются (`%20`, `%28`, `%29`);
|
||
- текст ссылки не может экранировать её закрывающую скобку: хвостовая `\` экранируется.
|
||
|
||
#### Scenario: Раскладка
|
||
- **WHEN** корень DOC-1 «АРМ КА», дети DOC-3 «Статусы» (`orderno` 1) и DOC-2 «Роли/права» (`orderno` 2)
|
||
- **THEN** `wiki/AWPCA/АРМ КА/index.md`, `…/01_Статусы/index.md`, `…/02_Роли_права/index.md`; `_tree.json` в порядке
|
||
DOC-1, DOC-3, DOC-2
|
||
|
||
#### Scenario: Картинка на странице
|
||
- **WHEN** в HTML DOC-3 `<img src="/files/obj/CmfDocument/CmfDocument%3Aa08/CmfDocument%3A<uuid>/scheme.png?version=1">`
|
||
- **THEN** в `index.md` — `[📎 scheme.png](attachment:<id>)`, у вложения `<id>` `state = none`, файл не скачан
|
||
|
||
#### Scenario: Прикреплённый файл с миниатюрой
|
||
- **WHEN** на странице миниатюра `…/<uuid>/model.xlsx.meta/thumbnail.jpg?version=1`, значок `static/attach-file.png` и
|
||
ссылка на `…/<uuid>/model.xlsx`
|
||
- **THEN** одно вложение `model.xlsx` с адресом файла; в тексте одна ссылка `[📎 model.xlsx](attachment:<id>)`, значка нет
|
||
|
||
#### Scenario: Миниатюра в обёртке
|
||
- **WHEN** `<a href="…/model.xlsx"><span><img src="…/model.xlsx.meta/thumbnail.jpg"></span></a> текст`
|
||
- **THEN** в тексте `[📎 model.xlsx](attachment:<id>) текст`
|
||
|
||
#### Scenario: Ссылка на другую страницу
|
||
- **WHEN** в HTML `<a href="project/Document/DOC-019834">Модель данных</a>`
|
||
- **THEN** в `index.md` — `[Модель данных](page:DOC-019834)`
|
||
|
||
#### Scenario: Картинка-ссылка на другую страницу
|
||
- **WHEN** `<a href="project/Document/DOC-1"><img src="…/scheme.png"></a>`, `scheme.png` — вложение
|
||
- **THEN** в тексте `[📎 scheme.png](attachment:<id>) [DOC-1](page:DOC-1)`
|
||
|
||
#### Scenario: Пробелы и скобки во внешнем адресе
|
||
- **WHEN** `<a href="http://ex.com/a b(c).html">док</a>`
|
||
- **THEN** в тексте `[док](http://ex.com/a%20b%28c%29.html)`
|
||
|
||
#### Scenario: Обратная косая черта в конце текста
|
||
- **WHEN** `<a href="project/Document/DOC-1">путь C:\</a>`
|
||
- **THEN** ссылка на `page:DOC-1` с текстом «путь C:\» остаётся ссылкой при разборе Markdown
|
||
|
||
### Requirement: Вложения вики по запросу
|
||
`POST /wiki/attachments/{id}/fetch` SHALL ставить пакет из одной команды чтения `wiki_fetch {attachment_id, url}` и
|
||
переводить вложение в `fetching`; для `fetching` — ответ текущим состоянием без нового пакета, для `ready` — сразу
|
||
`ready`. Вложение в `ready`, чьего файла нет в `files/`, SHALL считаться нескачанным: запрос ставит скачивание заново.
|
||
Агент `wiki` SHALL скачать файл с токеном Eva, отказаться от файла больше предела хранилища ядра (10 МБ) с
|
||
ошибкой «файл больше 10 МБ — откройте в Eva», загрузить файл `POST /agent/files` и отчитаться `done {file, size}`. Хаб
|
||
SHALL проверить, что файл есть в `files/` и размер совпадает, и перевести вложение в `ready`; иначе команда и вложение
|
||
`failed`. Адрес не на хосте Eva из конфига агента агент SHALL отвергать без запроса (`failed` «чужой хост»).
|
||
Файлы вложений в `ready` SHALL считаться ссылками для уборки `files/`. `GET /wiki/attachments/{id}` отдаёт
|
||
`{id, name, page_code, url, state, size, error}`, `GET /wiki/attachments/{id}/content` — содержимое (`ready`) или 409
|
||
`not_ready`.
|
||
|
||
#### Scenario: Скачивание картинки
|
||
- **WHEN** вложение 7 в `none`, `POST /wiki/attachments/7/fetch`, агент прислал файл 300 КБ
|
||
- **THEN** вложение 7 `ready`, `size = 307200`, `GET /wiki/attachments/7/content` — файл
|
||
|
||
#### Scenario: Слишком большой файл
|
||
- **WHEN** агент отчитался `failed` «файл больше 10 МБ — откройте в Eva»
|
||
- **THEN** вложение `failed` с этим текстом; `GET …/content` — 409 `not_ready`
|
||
|
||
#### Scenario: Вложение переживает синхронизацию
|
||
- **WHEN** вложение 7 `ready`, синхронизация вернула ту же страницу с той же ссылкой
|
||
- **THEN** у вложения тот же `id` 7 и `state = ready`, файл в `files/` не убран
|
||
|
||
#### Scenario: Файл вложения пропал
|
||
- **WHEN** вложение 7 `ready`, его файла нет в `files/`; `POST /wiki/attachments/7/fetch`
|
||
- **THEN** 202, вложение 7 `fetching`, поставлен пакет `wiki_fetch {attachment_id: 7}`
|
||
|
||
### Requirement: Маршруты вики
|
||
Хаб SHALL предоставлять под клиентским токеном (проект — `project=<метка>`, неизвестный — 404, без корня — 409
|
||
`no_root`):
|
||
- `GET /wiki/status?project=` — `{root, root_title, synced_at, pages, error, batch: {id, status} | null}`;
|
||
- `GET /wiki/tree?project=` — `{"pages": [{code, title, depth, parent_code, size}]}` в порядке обхода;
|
||
- `GET /wiki/pages/{code}?project=` — `{code, title, path, url, synced_at, markdown, attachments: [{id, name, state,
|
||
size, error, image}]}`; `image` — `true`, если имя файла в адресе вложения (последний сегмент пути без запроса,
|
||
раскодированный) оканчивается на `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.bmp` без учёта регистра, иначе `false`
|
||
(имя вложения из Eva для этого не годится: у картинок это часто `alt` без расширения); страницы нет в копии — 404;
|
||
- `GET /wiki/search?project=&q=&limit=` (`limit` 1–50, по умолчанию 10) — `{"results": [{code, title, path, snippet}]}` по
|
||
убыванию релевантности; слова запроса ищутся по префиксу, пустой запрос — 422;
|
||
- `POST /wiki/sync {project, author?}` → 201 `{batch_id}` (409 — см. «Синхронизация вики»);
|
||
- маршруты вложений — см. «Вложения вики по запросу».
|
||
|
||
Под токеном агента `wiki` — `POST /agent/wiki/pages` (см. «Синхронизация вики»).
|
||
|
||
#### Scenario: Поиск
|
||
- **WHEN** в копии `AWPCA` страница DOC-3 «Статусы» содержит «статус «На согласовании» ставит аналитик»;
|
||
`GET /wiki/search?project=AWPCA&q=согласов`
|
||
- **THEN** первым — DOC-3 с фрагментом, содержащим «На согласовании»
|
||
|
||
#### Scenario: Проект без корня
|
||
- **WHEN** у `sfm_kedr` нет корня, `GET /wiki/tree?project=sfm_kedr`
|
||
- **THEN** 409 `no_root`
|
||
|
||
#### Scenario: Признак картинки
|
||
- **WHEN** у вложения 9 страницы DOC-3 имя «Схема расчёта суммы элиминации по тройке», адрес
|
||
`…/vgo-matching-1.png?version=1`; у вложения 8 имя `attachment`, адрес `…/model.xlsx`
|
||
- **THEN** в `GET /wiki/pages/DOC-3?project=AWPCA` у 9 `image = true`, у 8 `image = false`
|