Модуль wiki, агенты модулей, поля проектов (#91) #92

Merged
aep merged 46 commits from feat/91-wiki into main 2026-10-24 09:05:09 +03:00
2 changed files with 69 additions and 19 deletions
Showing only changes of commit da4bc337d6 - Show all commits

View File

@ -192,22 +192,41 @@ jira безвреден.
неудачи) прошло больше `wiki_auto_sync_hours` (настройка модуля, по умолчанию 24, `0` — выключено) и у проекта нет
пакета `wiki_sync` в работе.
Хаб не ставит пакет, пока предыдущий `wiki_sync` проекта в работе: отвечает 409 с id пакета, MCP показывает его
состояние.
`POST /wiki/sync` и фоновая работа не ставят пакет, пока предыдущий `wiki_sync` проекта в работе: ответ 409 с id
пакета, MCP показывает его состояние.
**Смена и сброс корня** не упираются в это правило. `PATCH` отменяет ещё не выданные (`queued`) пакеты `wiki_sync`
проекта. При новом корне он ставит новый пакет; уже выданный агенту пакет доживает. Отчёт `done` по команде, у которой
`params.root` не равен текущему корню проекта (сменён или сброшен), отбрасывается: копия и данные не меняются, команда
`done` с пометкой в `result.discarded = "корень сменился"`. Типичный случай: корень проставили до установки eva-agent,
потом исправили опечатку.
**Агент** (контракт; решения внутри агента — его change):
1. Берёт корень `CmfDocument.get` и все страницы поддерева, как `collect_subtree` в `eva_wiki_export.py`.
2. По каждой странице берёт `text`.
3. Отправляет страницы пачками `POST /agent/wiki/pages {command_id, pages: [{code, eva_id, title, parent_code,
orderno, url, html, attachments: [{url, name}]}]}`. В пачке не больше 50 страниц и не больше 4 МБ тела.
4. Отчитывается `done` с `result = {root_title, codes: [<все коды дерева в порядке обхода>], eva_calls}`.
3. Разбирает ссылки страницы. Это делает агент, а не хаб: только он знает `eva.base_url`.
- **Вложение** — `<img src>`/`<a href>` на хост Eva с путём по правилу из Context.
- **Ссылка на страницу** — ссылка на хост Eva с кодом `DOC-<цифры>` в адресе.
- По каждой ссылке агент шлёт:
- `ref` — значение атрибута ровно как в HTML, по нему хаб находит место замены;
- `url` — абсолютный адрес (только у вложения);
- `name` — `alt` или `title` картинки, текст ссылки, иначе имя файла из пути;
- `code` — у ссылки на страницу.
4. Отправляет страницы пачками `POST /agent/wiki/pages {command_id, pages: [{code, eva_id, title, parent_code,
orderno, url, html, attachments: [{ref, url, name}], links: [{ref, code}]}]}`. В пачке не больше 50 страниц и не
больше 4 МБ тела.
5. Отчитывается `done` с `result = {root_title, codes: [<все коды дерева в порядке обхода>], eva_calls}`.
6. В `wiki_fetch` скачивает только адреса на хосте `eva.base_url`, иначе `failed` «чужой хост». Так токен Eva не уходит
на чужой адрес.
**Хаб.**
- Пачки складываются в `wiki_sync_stage` без изменения копии.
- Отчёт `done` применяется одной транзакцией (`CommandHooks.on_result`):
- HTML переводится в Markdown;
- Markdown-копия собирается в `wiki/.<label>.new/`, затем атомарно подменяет `wiki/<label>/` (переименование;
старая копия удаляется после подмены);
- **Приём пачки.** HTML каждой страницы сразу переводится в Markdown, ссылки заменяются по `ref`. Вместо `id`
вложения ставится заглушка, её заполняет применение. Результат кладётся в `wiki_sync_stage`, копия не меняется.
Перевод идёт вне транзакции записи (markdownify на десятках МБ занимает секунды) и не держит блокировку базы, в
которой ждут `claim` jira-agent и веб.
- **Отчёт `done`.** Сначала, вне транзакции, Markdown-копия собирается из стадии в `wiki/.<label>.new/` с
предварительно выделенными `id` вложений. Затем одной короткой транзакцией (`CommandHooks.on_result`):
- каталог атомарно подменяется: переименование, старая копия удаляется после подмены;
- `wiki_page` и FTS пишутся заново;
- для `wiki_attachment` при той же паре «адрес + страница» сохраняются `id` и скачанный файл, новые пары
добавляются, пропавшие удаляются;
@ -225,8 +244,8 @@ jira безвреден.
Безопасное название — как `safe_name` скрипта; при совпадении у соседей хаб добавляет `_2`, `_3`.
**HTML → Markdown** делает хаб: зависимость `markdownify`, ATX-заголовки. Ссылки:
- **картинка или файл Eva** (тот же хост и путь по правилу из Context) заменяется на `[📎 <имя>](attachment:<id>)`:
**HTML → Markdown** делает хаб: зависимость `markdownify`, ATX-заголовки. Ссылки (классифицирует агент, см. выше):
- **картинка или файл Eva** заменяется на `[📎 <имя>](attachment:<id>)`:
по `id` инструмент `attachment` и веб скачивают файл;
- **ссылка на страницу Eva с кодом `DOC-<цифры>`** заменяется на `[<текст>](page:<код>)`: по коду её открывает
инструмент `page`, а в вебе она ведёт на страницу вики, если та есть в дереве проекта;
@ -333,7 +352,10 @@ jira безвреден.
### 16. Порядок работ
1. Ядро: виды агентов (решения 1–7) — jira-agent v14 работает. Живая проверка: kickstart, claim от v14 проходит.
1. Ядро: виды агентов (решения 1–7) — jira-agent v14 работает. Живой проверки посередине нет: launchd запускает хаб
из основного checkout, а частичная v7 на живой базе сломала бы последующие шаги миграции. Совместимость с v14
проверяется тестом: снимок схемы v6 с досками и токеном → миграция → `claim`, `GET /agent/settings`,
`/agent/state` с прежним токеном, формой и ответами v14. Живая проверка — после merge.
2. Ядро: поля проектов (решение 8) и снятие долга.
3. Модуль wiki в хабе: данные, синхронизация с имитацией агента в тестах, вложения, HTTP, MCP.
4. Веб.
@ -397,3 +419,16 @@ jira безвреден.
вики.
11. **Ежедневная синхронизация — фоновой работой хаба** (решение 10). Обоснование: прецедент — автообновление
зеркала jira. Цена ошибки — отключение настройкой `wiki_auto_sync_hours = 0`.
12. **Одна живая проверка — после merge, без промежуточной** (решение 16; builder check). Обоснование: частичная v7 на
живой базе ломает последующие шаги, а launchd смотрит в основной checkout. Цена ошибки — несовместимость с v14
всплывёт после выкатки; страхуют тест миграции на снимке v6 и откат на копию базы.
13. **HTML → Markdown при приёме пачки, вне транзакции** (решение 10; builder check). Обоснование: перевод десятков МБ
под `BEGIN IMMEDIATE` блокирует `claim` jira-agent и веб. Цена ошибки — стадия в базе крупнее (Markdown вместо
HTML — обычно меньше).
14. **Ссылки классифицирует агент, хаб заменяет по `ref`** (решение 10; builder check). Обоснование: адрес Eva знает
только агент, хабу он не нужен. Цена ошибки — контракт `pages` меняется в обоих репозиториях.
15. **`wiki_fetch` только на хост `eva.base_url`** (решение 10; builder check). Обоснование: токен Eva не уходит на
чужой адрес. Цена ошибки — вложение на другом хосте открывается только ссылкой.
16. **Смена корня отменяет невыданные `wiki_sync`, поздний `done` старого корня отбрасывается** (решение 10; builder
check). Обоснование: корень правят, пока eva-agent ещё не установлен; копия старого дерева не должна встать под
новый корень. Цена ошибки — лишняя синхронизация.

View File

@ -25,12 +25,18 @@ SHALL удалять копию проекта, его страницы, инд
label`. Её SHALL ставить: `POST /wiki/sync {project}`, задание или смена корня, фоновая работа хаба — по каждому
проекту с корнем, если с последней успешной или неудачной синхронизации прошло больше `wiki_auto_sync_hours`
(настройка модуля, по умолчанию 24; `0` — выключено). Пока у проекта есть пакет `wiki_sync` не в терминальном статусе,
новый не ставится: `POST /wiki/sync` отвечает 409 `sync_in_progress` с `batch_id` текущего.
новый не ставится: `POST /wiki/sync` отвечает 409 `sync_in_progress` с `batch_id` текущего. Смена или сброс корня
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: [{url, name}]}`, в
пачке не больше 50 страниц, — и отчитаться `done` с `result = {root_title, codes, eva_calls}`, где `codes` — коды всех
страниц дерева в порядке обхода. Хаб SHALL складывать пачки во временную стадию команды, не трогая копию. Пачка по
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-копия вики»), страницы, индекс поиска и
@ -57,6 +63,14 @@ pages}` — каждая страница `{code, eva_id, title, parent_code, or
- **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` текущего пакета, нового пакета нет
@ -69,7 +83,7 @@ pages}` — каждая страница `{code, eva_id, title, parent_code, or
корне копии — `_tree.json`, список `{code, title, depth, path}` в порядке обхода. Копия SHALL собираться рядом
(`wiki/.<метка>.new/`) и подменять прежнюю переименованием; незавершённую подмену хаб SHALL доводить при старте.
HTML страницы SHALL переводиться в Markdown (заголовки `#`); ссылка или картинка на файл Eva (тот же хост, путь с
HTML страницы SHALL переводиться в Markdown (заголовки `#`); ссылка или картинка на файл Eva (по списку `attachments` агента: тот же хост, путь с
`files`, `file`, `attachments`, `attachment`, `download`, `media` или `upload`) SHALL заменяться на
`[📎 <имя>](attachment:<id>)`, где `id` — строка вложения; ссылка на документ Eva с кодом `DOC-<цифры>` — на
`[<текст>](page:<код>)`; прочие ссылки остаются. Сырой HTML в копию не попадает.
@ -89,7 +103,8 @@ HTML страницы SHALL переводиться в Markdown (заголов
`ready`. Агент `wiki` SHALL скачать файл с токеном Eva, отказаться от файла больше предела хранилища ядра (10 МБ) с
ошибкой «файл больше 10 МБ — откройте в Eva», загрузить файл `POST /agent/files` и отчитаться `done {file, size}`. Хаб
SHALL проверить, что файл есть в `files/` и размер совпадает, и перевести вложение в `ready`; иначе команда и вложение
`failed`. Файлы вложений в `ready` SHALL считаться ссылками для уборки `files/`. `GET /wiki/attachments/{id}` отдаёт
`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`.