Архив OpenSpec: wiki-module (#91) #93
@ -161,13 +161,15 @@ MUST NOT менять `issue_note`. При завершении `ensure_issue`
|
||||
Таблица `sync_run` SHALL хранить каждую загрузку состояния: `started_at`, `finished_at`, `kind` (`keys | jql`), `query`,
|
||||
`issues_count`, `links_count`, `jira_calls`, `rate_limited`, `command_id`. При повторной выдаче команды `refresh` хаб
|
||||
SHALL обнулить `issues_count` и `links_count` её `sync_run` (`started_at` не меняется); при переводе `refresh` в `failed`
|
||||
по пределу выдач — проставить `finished_at`. Таблица `agent_status` (одна строка) SHALL хранить `last_seen_at`,
|
||||
`agent_version`, `settings_version`, `current_command_id`, `last_jira_call_at`. Через API и MCP `agent_status` SHALL
|
||||
отдаваться с признаком `online` (истина, если `last_seen_at` моложе 5 минут) и списком `redelivered` — команд в
|
||||
`claimed`, выданных больше одного раза (`id`, `type`, `claimed_at`, `deliveries`).
|
||||
по пределу выдач — проставить `finished_at`. Таблица `agent_status` SHALL хранить по строке на агента (ключ `agent` —
|
||||
имя модуля, см. `modules`, «Агенты модулей»): `last_seen_at`, `agent_version`, `settings_version`, `current_command_id`,
|
||||
`last_call_at` (время последнего обращения агента к внешней системе; от jira-agent приходит полем `last_jira_call_at`).
|
||||
Через API и MCP статус агента SHALL отдаваться с признаком `online` (истина, если `last_seen_at` моложе 5 минут) и
|
||||
списком `redelivered` — команд его модуля в `claimed`, выданных больше одного раза (`id`, `type`, `claimed_at`,
|
||||
`deliveries`).
|
||||
|
||||
#### Scenario: Агент давно не выходил на связь
|
||||
- **WHEN** `last_seen_at` старше 5 минут
|
||||
- **WHEN** `last_seen_at` агента `jira` старше 5 минут
|
||||
- **THEN** `agent_status` через API и MCP отдаётся с `online = false`
|
||||
|
||||
#### Scenario: Довыдача refresh не удваивает счётчики
|
||||
@ -178,6 +180,10 @@ SHALL обнулить `issues_count` и `links_count` её `sync_run` (`started
|
||||
- **WHEN** `refresh` переведён в `failed` «агент не отчитался после 3 выдач»
|
||||
- **THEN** у его `sync_run` проставлен `finished_at`
|
||||
|
||||
#### Scenario: Статусы агентов раздельно
|
||||
- **WHEN** агент `jira` выходил на связь минуту назад, агент `wiki` — никогда
|
||||
- **THEN** `GET /agents` отдаёт `jira` с `online = true` и `wiki` с `online = false`, `last_seen_at = null`
|
||||
|
||||
### Requirement: Настройки
|
||||
Таблица `setting` SHALL хранить парами ключ–JSON: `project_key`, `field_ids`, `issue_types`, `priority_map`, `link_types`,
|
||||
`transitions_map`, `pacing`, `tokens` (агентский и клиентский), `settings_version` (увеличивается при любой
|
||||
@ -220,15 +226,17 @@ SHALL обнулить `issues_count` и `links_count` её `sync_run` (`started
|
||||
|
||||
### Requirement: Проекты
|
||||
Таблица `project` SHALL хранить проекты моста — подмножества задач Jira-проекта `project_key`, выделенные меткой:
|
||||
`label` (PK, метка Jira), `name`, `board_id` (номер доски Jira или `NULL`), `board_type` (`scrum` | `kanban` | `NULL` —
|
||||
тип ещё не известен), `created_at`. Метка SHALL быть непустой строкой до 100 символов без пробельных и управляющих символов (категория
|
||||
Unicode `Cc`), запятых, кавычек, скобок и обратной косой черты; две метки, совпадающие без учёта регистра, MUST NOT существовать одновременно. Название SHALL быть
|
||||
непустым после обрезки пробелов, до 100 символов. `board_id` — целое больше нуля или `NULL`. `board_type` SHALL
|
||||
записываться только по результату `refresh` от агента (см. `agent`). Метку, название и доску проекта после создания
|
||||
хаб не меняет (переименования и удаления нет). Базовая JQL SHALL вычисляться из проектов:
|
||||
`project = <project_key> AND labels in ("<метка>", …)` — каждая метка в двойных кавычках, метки по created_at, затем
|
||||
label, через `, `; без проектов базовой JQL
|
||||
нет (`null`).
|
||||
`label` (PK, метка Jira), `name`, `created_at`. Метка SHALL быть непустой строкой до 100 символов без пробельных и
|
||||
управляющих символов (категория Unicode `Cc`), запятых, кавычек, скобок и обратной косой черты; две метки, совпадающие
|
||||
без учёта регистра, MUST NOT существовать одновременно. Название SHALL быть непустым после обрезки пробелов, до 100
|
||||
символов. Метку и название после создания хаб не меняет (переименования и удаления нет).
|
||||
|
||||
Доска проекта SHALL храниться в таблице модуля jira `jira_project`: `label` (PK, ссылка на проект), `board_id` (номер
|
||||
доски Jira или `NULL`), `board_type` (`scrum` | `kanban` | `NULL` — тип ещё не известен). `board_id` — целое больше нуля
|
||||
или `NULL`, после создания не меняется; `board_type` SHALL записываться только по результату `refresh` от агента (см.
|
||||
`agent`). Миграция v7 SHALL перенести `board_id` и `board_type` всех проектов из `project` в `jira_project`. Базовая JQL
|
||||
SHALL вычисляться из проектов: `project = <project_key> AND labels in ("<метка>", …)` — каждая метка в двойных
|
||||
кавычках, метки по created_at, затем label, через `, `; без проектов базовой JQL нет (`null`).
|
||||
|
||||
Для проекта P задачи зеркала делятся на *свои* — с меткой P в `labels` (точное совпадение) — и *соседей* — задачи без
|
||||
метки P, связанные со своей связью `issue_link` в любую сторону, и эпики своих (`epic_key` своей задачи), если они есть в
|
||||
@ -259,6 +267,11 @@ label, через `, `; без проектов базовой JQL
|
||||
- **WHEN** есть проект `AWPCA`, создаётся проект `awpca`
|
||||
- **THEN** отказ «проект с такой меткой уже есть», проект один
|
||||
|
||||
#### Scenario: Доски после миграции v7
|
||||
- **WHEN** база v6 с проектами `AWPCA` (`board_id = 4216`, `scrum`) и `sfm_kedr` (без доски) мигрирует в v7
|
||||
- **THEN** в `project` нет колонок `board_id`, `board_type`; в `jira_project` — `AWPCA` 4216 `scrum` и `sfm_kedr` `NULL`
|
||||
`NULL`; перед миграцией сделана копия базы
|
||||
|
||||
### Requirement: Журнал изменений задач
|
||||
Таблица `issue_change` SHALL хранить журнал изменений задач Jira по строке на поле записи журнала: `history_id` (id
|
||||
записи журнала Jira), `seq` (номер поля в записи, с 0), `key`, `at` (время записи в ISO-8601 UTC, приведённое хабом),
|
||||
|
||||
@ -8,14 +8,17 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача
|
||||
### Requirement: Процесс и доступ
|
||||
`jira-hub serve` SHALL запускать один процесс: HTTP на `0.0.0.0:<port>` (по умолчанию 8790), база и настройки в
|
||||
`~/.local/share/jira-hub/` (переопределяется `JIRA_HUB_HOME`). Все маршруты API SHALL требовать
|
||||
`Authorization: Bearer <token>`: агентский токен открывает только `/agent/*`, клиентский — всё остальное. Исключение —
|
||||
`Authorization: Bearer <token>`: токен агента модуля открывает только `/agent/*` (маршруты ядра — от имени этого агента,
|
||||
маршруты модуля со scope `agent` — только агенту своего модуля, см. `modules`, «Агенты модулей»), клиентский — всё
|
||||
остальное. `jira-hub tokens` SHALL печатать клиентский токен и токены агентов с именами модулей. Исключение —
|
||||
раздача веб-приложения (`GET /app`, `GET /app/…`) и перенаправления `GET /`, `GET /links`: это статические файлы без
|
||||
данных, они отдаются без токена. Ответы API — JSON, ошибки — `{error, detail}` с кодами 400 (неверное тело или
|
||||
`Content-Length`), 401, 404, 409, 413 (тело больше предела), 422, 500 (внутренняя ошибка); реле общей папки
|
||||
дополнительно отвечает 502. Методы, кроме GET, POST и PUT, отвечают 501 стандартной страницей `http.server`, не JSON.
|
||||
дополнительно отвечает 502. Методы, кроме GET, POST, PUT и PATCH, отвечают 501 стандартной страницей `http.server`, не
|
||||
JSON.
|
||||
|
||||
#### Scenario: Чужой токен
|
||||
- **WHEN** агентским токеном вызван `GET /issues`
|
||||
- **WHEN** токеном агента `jira` вызван `GET /issues`
|
||||
- **THEN** 401
|
||||
|
||||
#### Scenario: Неподдерживаемый метод
|
||||
@ -26,6 +29,10 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача
|
||||
- **WHEN** `GET /app/board` без заголовка `Authorization`, сборка приложения есть
|
||||
- **THEN** 200, `Content-Type: text/html; charset=utf-8`, тело — `index.html` сборки; данных зеркала в нём нет
|
||||
|
||||
#### Scenario: Токены после миграции
|
||||
- **WHEN** база v6 с агентским токеном T мигрировала в v7, запущен `jira-hub tokens`
|
||||
- **THEN** вывод содержит клиентский токен, `jira: T` и новый токен `wiki`
|
||||
|
||||
### Requirement: Маршруты агента
|
||||
Хаб SHALL предоставлять агенту следующие маршруты (все под агентским токеном):
|
||||
- `POST /agent/claim` `{agent_version, settings_version, last_jira_call_at, update_state, launcher_version}` — долгий
|
||||
@ -333,29 +340,42 @@ SHALL скопировать файл базы в `backups/<дата-время>
|
||||
- **THEN** 401
|
||||
|
||||
### Requirement: Публикация версии агента
|
||||
`jira-hub agent publish PATH` SHALL публиковать файл агента: текст в UTF-8 не больше 1 МБ, ровно одна строка
|
||||
`AGENT_VERSION = <целое>`, версия больше опубликованной. Файл SHALL записываться в `<JIRA_HUB_HOME>/release/agent.py`
|
||||
(каталог 0700), затем `manifest.json` `{version, sha256, size, published_at}`; оба — через временный файл с `fsync` и
|
||||
переименование, манифест последним. Прежний `agent.py` на время публикации SHALL переименовываться в `agent.py.prev`
|
||||
и удаляться после записи манифеста; если запись нового файла или манифеста не удалась (`OSError`), хаб SHALL вернуть
|
||||
`agent.py.prev` на место `agent.py`, удалить временные файлы и завершить команду сообщением «не удалось записать
|
||||
публикацию: <ошибка>» и кодом 1 — опубликованной остаётся прежняя версия. Команда SHALL работать без запущенного
|
||||
`serve` и печатать «опубликована v<N>, sha256 <первые 12 символов>…, <size> байт»; при ошибке — сообщение и код 1,
|
||||
опубликованное не меняется. Хаб SHALL читать манифест при каждом `claim`, `GET /agent/release` и `GET /agent`; манифест
|
||||
годен, только если это объект, `version` — целое число (не логическое) не меньше 1, `sha256` — строка из 64 символов
|
||||
`[0-9a-f]`; негодный или нечитаемый манифест означает «публикации нет» и предупреждение в лог — одно на каждый новый
|
||||
текст ошибки. Файл `agent.py`, не декодируемый как UTF-8 при совпавшем sha256, означает «публикации нет»
|
||||
(`GET /agent/release` — 404 `no_release`). `GET /agent` SHALL дополняться полями `release` (`{version, sha256,
|
||||
published_at}` или `null`), `update_state`, `launcher_version`, `agent_reported` (был ли `claim` с момента запуска хаба)
|
||||
и `update_hint`: «агент запущен без run.py — обновится только вручную» (опубликована версия выше версии агента, агент
|
||||
онлайн, `agent_reported = true`, `launcher_version = null`), «откат с v<to> на v<from>: <error>» (`rolled_back`),
|
||||
«v<to> отвергнута: <error>» (`rejected`), иначе `null`.
|
||||
`jira-hub agent publish [--module <модуль>] PATH` SHALL публиковать файл агента модуля (по умолчанию `jira`;
|
||||
неизвестный модуль или модуль без команд — код 2 и список модулей с агентами в stderr): текст в UTF-8 не больше 1 МБ,
|
||||
ровно одна строка `AGENT_VERSION = <целое>`, версия больше опубликованной для этого модуля. Файл SHALL записываться в
|
||||
`<JIRA_HUB_HOME>/release/<модуль>/agent.py` (каталоги 0700), затем `manifest.json` `{version, sha256, size,
|
||||
published_at}`; оба — через временный файл с `fsync` и переименование, манифест последним. Прежний `agent.py` на время
|
||||
публикации SHALL переименовываться в `agent.py.prev` и удаляться после записи манифеста; если запись нового файла или
|
||||
манифеста не удалась (`OSError`), хаб SHALL вернуть `agent.py.prev` на место `agent.py`, удалить временные файлы и
|
||||
завершить команду сообщением «не удалось записать публикацию: <ошибка>» и кодом 1 — опубликованной остаётся прежняя
|
||||
версия. Команда SHALL работать без запущенного `serve` и печатать «опубликована v<N>, sha256 <первые 12 символов>…,
|
||||
<size> байт»; при ошибке — сообщение и код 1, опубликованное не меняется. При старте хаб SHALL один раз перенести
|
||||
прежнюю публикацию `release/agent.py` и `release/manifest.json` в `release/jira/`, если `release/jira/` ещё нет; ошибка
|
||||
переноса — предупреждение в лог, хаб стартует. Хаб SHALL читать манифест модуля агента при каждом `claim`,
|
||||
`GET /agent/release` и `GET /agent`; манифест годен, только если это объект, `version` — целое число (не логическое) не
|
||||
меньше 1, `sha256` — строка из 64 символов `[0-9a-f]`; негодный или нечитаемый манифест означает «публикации нет» и
|
||||
предупреждение в лог — одно на каждый новый текст ошибки. Файл `agent.py`, не декодируемый как UTF-8 при совпавшем
|
||||
sha256, означает «публикации нет» (`GET /agent/release` — 404 `no_release`). `GET /agent?module=<модуль>` (без
|
||||
параметра — `jira`) и каждый элемент `GET /agents` SHALL дополняться полями `release` (`{version, sha256,
|
||||
published_at}` или `null`), `update_state`, `launcher_version`, `agent_reported` (был ли `claim` этого агента с момента
|
||||
запуска хаба) и `update_hint`: «агент запущен без run.py — обновится только вручную» (опубликована версия выше версии
|
||||
агента, агент онлайн, `agent_reported = true`, `launcher_version = null`), «откат с v<to> на v<from>: <error>»
|
||||
(`rolled_back`), «v<to> отвергнута: <error>» (`rejected`), иначе `null`.
|
||||
|
||||
#### Scenario: Публикация новой версии
|
||||
- **WHEN** опубликована v5, `jira-hub agent publish jira-agent/src/agent.py` с `AGENT_VERSION = 6`
|
||||
- **THEN** в `release/` — новый `agent.py` и манифест с `version = 6`, `agent.py.prev` нет; вывод «опубликована v6,
|
||||
- **THEN** в `release/jira/` — новый `agent.py` и манифест с `version = 6`, `agent.py.prev` нет; вывод «опубликована v6,
|
||||
sha256 …»; код 0
|
||||
|
||||
#### Scenario: Публикация eva-agent
|
||||
- **WHEN** `jira-hub agent publish --module wiki eva-agent/src/agent.py` с `AGENT_VERSION = 1`, опубликована jira v14
|
||||
- **THEN** в `release/wiki/` — v1; `claim` агента `wiki` получает `agent_release {version: 1, …}`, `claim` агента `jira` —
|
||||
`{version: 14, …}`
|
||||
|
||||
#### Scenario: Неизвестный модуль
|
||||
- **WHEN** `jira-hub agent publish --module nope agent.py`
|
||||
- **THEN** код 2, в stderr «неизвестный модуль nope» и список модулей с агентами; `release/` не изменён
|
||||
|
||||
#### Scenario: Версия не выше опубликованной
|
||||
- **WHEN** опубликована v6, публикуется файл с `AGENT_VERSION = 6`
|
||||
- **THEN** «опубликована v6, новая должна быть больше», код 1; `release/` не изменён
|
||||
@ -367,7 +387,7 @@ published_at}` или `null`), `update_state`, `launcher_version`, `agent_report
|
||||
#### Scenario: Диск полон при записи манифеста
|
||||
- **WHEN** опубликована v8, публикуется v9, запись `manifest.json` завершилась `OSError`
|
||||
- **THEN** «не удалось записать публикацию: …», код 1, без трейсбека; `agent.py` и манифест — прежние v8,
|
||||
`GET /agent/release` отдаёт v8; временных файлов и `agent.py.prev` в `release/` нет
|
||||
`GET /agent/release` отдаёт v8; временных файлов и `agent.py.prev` в `release/jira/` нет
|
||||
|
||||
#### Scenario: Негодный манифест
|
||||
- **WHEN** в `manifest.json` `version = 6.9` (или `true`, или `sha256` короче 64 символов), хаб получает 100 `claim`
|
||||
@ -382,6 +402,10 @@ published_at}` или `null`), `update_state`, `launcher_version`, `agent_report
|
||||
- **THEN** `GET /agent` → `agent_reported = false`, `launcher_version = null`, `update_hint = null`; после первого
|
||||
`claim` — `agent_reported = true`, `launcher_version` из `claim`
|
||||
|
||||
#### Scenario: Перенос прежней публикации
|
||||
- **WHEN** хаб с `release/agent.py` v14 и манифестом без `release/jira/` запущен на новом коде
|
||||
- **THEN** публикация v14 лежит в `release/jira/`, `claim` агента `jira` получает `agent_release {version: 14, …}`
|
||||
|
||||
### Requirement: Раздача веб-приложения
|
||||
Хаб SHALL отдавать собранное веб-приложение (спека `web-app`) из каталога пакета `src/jira_hub/web/app/`, читая файлы
|
||||
с диска на каждый запрос:
|
||||
@ -459,22 +483,33 @@ sha256 (без расширения и имени вложения: имя за
|
||||
|
||||
### Requirement: Маршруты проектов
|
||||
Хаб SHALL предоставлять под клиентским токеном:
|
||||
- `GET /projects` → список `{label, name, board_id, board_type, created_at, issues_count}` по `created_at`, затем `label`;
|
||||
`issues_count` — задачи зеркала с меткой проекта.
|
||||
- `POST /projects` `{label, name, board_id?, author?}` → 201 `{project, batch_id}`: в одной транзакции проект
|
||||
записывается и ставится пакет «Новый проект <метка>» из одного `refresh` по базовой JQL, уже включающей новую метку;
|
||||
автор пакета — `author` (по умолчанию `api`). Неверные поля (см. `data-model`, «Проекты») — 422 с указанием поля;
|
||||
метка, совпадающая с существующей без учёта регистра, — 409. При ошибке ни проекта, ни пакета нет.
|
||||
- `GET /projects` → список проектов по `created_at`, затем `label`; каждый — плоский объект из полей ядра (`label`,
|
||||
`name`, `created_at`) и полей всех модулей (`modules`, «Поля проекта от модулей»). Модуль jira даёт `board_id`,
|
||||
`board_type`, `issues_count` (задачи зеркала с меткой проекта), модуль wiki — `wiki_root`, `wiki_root_title`,
|
||||
`wiki_pages`, `wiki_synced_at`, `wiki_sync_error`.
|
||||
- `POST /projects` `{label, name, author?, <поля модулей>}` → 201 `{project, batch_id}`: в одной транзакции проект
|
||||
записывается, модули записывают свои поля и ставят свои пакеты; `batch_id` — пакет первого модуля, вернувшего пакет
|
||||
(у jira — «Новый проект <метка>» из одного `refresh` по базовой JQL, уже включающей новую метку). Автор пакетов —
|
||||
`author` (по умолчанию `api`). Неизвестное поле, неверные поля ядра (см. `data-model`, «Проекты») или модуля — 422 с
|
||||
указанием поля; метка, совпадающая с существующей без учёта регистра, — 409. При ошибке ни проекта, ни пакетов нет.
|
||||
- `PATCH /projects/{label}` `{<изменяемые поля модулей>, author?}` → 200 `{project, batch_ids}`: модули правят свои поля
|
||||
и ставят пакеты в одной транзакции. Неизменяемое поле (метка, название, `board_id`) — 422 «<поле> после создания не
|
||||
меняется»; неизвестное поле или неверное значение — 422 с указанием поля; проекта нет — 404.
|
||||
|
||||
#### Scenario: Новый проект
|
||||
- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", board_id: 5120, author: "viewer"}`, есть проект `AWPCA`
|
||||
- **THEN** 201; `GET /projects` отдаёт два проекта, у `STOCKS_DRRK` `board_type = null`; в очереди пакет автора `viewer`
|
||||
с `refresh {jql: 'project = AURORA AND labels in ("AWPCA", "STOCKS_DRRK")'}`
|
||||
- **THEN** 201; `GET /projects` отдаёт два проекта, у `STOCKS_DRRK` `board_type = null`, `wiki_root = null`; в очереди
|
||||
пакет автора `viewer` с `refresh {jql: 'project = AURORA AND labels in ("AWPCA", "STOCKS_DRRK")'}`
|
||||
|
||||
#### Scenario: Проект без доски
|
||||
- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК"}`
|
||||
- **THEN** 201, `board_id = null`
|
||||
|
||||
#### Scenario: Проект с корнем вики
|
||||
- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", wiki_root: "DOC-001234"}`
|
||||
- **THEN** 201; в очереди пакет `refresh` и пакет `wiki_sync {label: "STOCKS_DRRK", root: "DOC-001234"}`; `batch_id` —
|
||||
пакет `refresh`
|
||||
|
||||
#### Scenario: Повтор метки
|
||||
- **WHEN** `POST /projects {label: "AWPCA", name: "…"}`, проект `AWPCA` есть
|
||||
- **THEN** 409, пакет не поставлен
|
||||
@ -483,6 +518,18 @@ sha256 (без расширения и имени вложения: имя за
|
||||
- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: " "}`
|
||||
- **THEN** 422 с указанием поля `name`
|
||||
|
||||
#### Scenario: Неизвестное поле
|
||||
- **WHEN** `POST /projects {label: "X", name: "X", board: 5}`
|
||||
- **THEN** 422 с указанием поля `board`, проекта нет
|
||||
|
||||
#### Scenario: Корень вики у существующего проекта
|
||||
- **WHEN** `PATCH /projects/AWPCA {wiki_root: "DOC-000777"}`
|
||||
- **THEN** 200, `batch_ids` — один пакет `wiki_sync`; `GET /projects` отдаёт `AWPCA` с `wiki_root = DOC-000777`
|
||||
|
||||
#### Scenario: Доску не меняют
|
||||
- **WHEN** `PATCH /projects/AWPCA {board_id: 1}`
|
||||
- **THEN** 422 «board_id после создания не меняется», доска прежняя
|
||||
|
||||
### Requirement: Приём журнала изменений
|
||||
Хаб SHALL принимать от агента журнал изменений задач маршрутом `POST /agent/history` (агентский токен) с телом
|
||||
`{command_id, changes: [...]}`, элемент — `{history_id, seq, key, created, author, field_id, field_name, from,
|
||||
|
||||
@ -247,3 +247,42 @@ user scope Claude Code как `jira-hub` (`uv run --project <путь> jira-hub
|
||||
скачано) и #31338 «запись.mp4» на 25 МБ
|
||||
- **THEN** в ответе «вложения (2):», строка #31338 с «больше 10 МБ — только в Jira», строка #31337 с «в #101, скачано»; у
|
||||
комментария 101 строка «файлы: #31337 скрин 1.png»
|
||||
|
||||
### Requirement: MCP вики
|
||||
`jira-hub mcp wiki [--project <метка>]` SHALL запускать по stdio MCP-сервер модуля `wiki` с именем `jira-hub-wiki`,
|
||||
ходящий в HTTP API хаба клиентским токеном; хаб недоступен — каждый инструмент отвечает «хаб не запущен: jira-hub serve».
|
||||
Привязка к проекту проверяется так же, как у сервера jira («Привязка сессии к проекту»). Инструменты:
|
||||
|
||||
| Инструмент | Параметры | Ответ |
|
||||
|---|---|---|
|
||||
| `projects` | — | таблица: метка, название, корень вики, страниц, последняя синхронизация; текущий проект отмечен |
|
||||
| `project_create` | `label`, `name`, `wiki_root` (необязательно) | `POST /projects` с автором `claude`; проект и фрагмент `.mcp.json` с записью `jira-hub-wiki` |
|
||||
| `root_set` | `root` | `PATCH /projects/<текущий> {wiki_root}`; новый корень и id пакета синхронизации |
|
||||
| `tree` | `code` (необязательно), `depth` (по умолчанию 2) | дерево от корня или от `code`: отступ, код, название, размер |
|
||||
| `page` | `code` или `title`, `offset` (по умолчанию 0) | шапка (код, путь в дереве, адрес в Eva, дата синхронизации, вложения с `id`) и Markdown с `offset` до 20 000 символов, в конце — «продолжение: offset=<N>», если текст длиннее; `title` ищется точно, иначе — до 10 кандидатов с кодами |
|
||||
| `search` | `query`, `limit` (по умолчанию 10) | страницы: код, название, путь, фрагмент |
|
||||
| `attachment` | `id` | ставит скачивание, ждёт до 60 с; готово — путь к копии `downloads/wiki-<id>/<имя>`; иначе состояние и подсказка повторить |
|
||||
| `sync` | `wait` (по умолчанию `false`) | ставит синхронизацию (или сообщает о текущей) и id пакета; с `wait` — ждёт до 300 с и отдаёт итог |
|
||||
| `status` | — | корень, последняя синхронизация, ошибка, пакет в работе, агент `wiki`: онлайн/офлайн, версия, публикация, обновление, запускатель (как `agent_status` сервера jira) |
|
||||
|
||||
Инструменты, кроме `projects` и `project_create`, SHALL требовать привязки к проекту. В привязанной сессии без корня
|
||||
(кроме `root_set`, `status`) ответ SHALL быть «у проекта <метка> нет корня вики: root_set DOC-…»; с корнем, но без
|
||||
синхронизаций — «вики проекта ещё не синхронизирована: sync». Набор инструментов и их описания у сервера `jira-hub`
|
||||
(модуль jira) SHALL остаться прежним.
|
||||
|
||||
#### Scenario: Поиск из сессии awp-ca
|
||||
- **WHEN** сервер запущен с `--project AWPCA`, копия синхронизирована, вызван `search {query: "статус согласован"}`
|
||||
- **THEN** ответ — страницы с кодами и фрагментами; запрос в хаб — `GET /wiki/search?project=AWPCA&q=…`
|
||||
|
||||
#### Scenario: Длинная страница
|
||||
- **WHEN** `page {code: DOC-3}`, Markdown 45 000 символов
|
||||
- **THEN** первые 20 000 символов и «продолжение: offset=20000»; `page {code: DOC-3, offset: 40000}` — остаток без строки
|
||||
продолжения
|
||||
|
||||
#### Scenario: Корня нет
|
||||
- **WHEN** сессия `sfm_kedr` без корня, вызван `tree`
|
||||
- **THEN** ответ «у проекта sfm_kedr нет корня вики: root_set DOC-…», запроса `GET /wiki/tree` нет
|
||||
|
||||
#### Scenario: Сервер jira не изменился
|
||||
- **WHEN** запущен `jira-hub mcp --project AWPCA`
|
||||
- **THEN** имя сервера `jira-hub`, список инструментов и описания совпадают с эталоном до change
|
||||
|
||||
@ -26,3 +26,63 @@
|
||||
- **WHEN** хаб после перестройки стартует на базе v6 с агентом v14
|
||||
- **THEN** схема остаётся v6, копия базы не создаётся; адреса API, веба, ответы и инструменты MCP — прежние; агент берёт
|
||||
команды и отчитывается как раньше
|
||||
|
||||
### Requirement: Агенты модулей
|
||||
У каждого подключённого модуля, объявляющего типы команд, SHALL быть свой агент; имя агента — имя модуля (`jira` —
|
||||
jira-agent, `wiki` — eva-agent). Агент SHALL определяться по токену: `setting.tokens = {client, agents: {<модуль>:
|
||||
<токен>}}`, недостающий токен агента хаб создаёт при старте. Хаб SHALL для каждого агента отдельно:
|
||||
- выдавать в `claim` только команды типов его модуля, с одной командой в полёте на агента (агенты работают
|
||||
параллельно);
|
||||
- хранить строку `agent_status` (ключ — имя агента);
|
||||
- держать опубликованный релиз `release/<модуль>/` и состояние самообновления;
|
||||
- отдавать в `GET /agent/settings` ключи ядра и ключи для агента только своего модуля;
|
||||
- пускать на маршруты модуля со scope `agent` только агента этого модуля (токен другого агента — 401).
|
||||
|
||||
Миграция v7 SHALL сохранить прежний агентский токен как токен агента `jira`, перенести строку `agent_status` в агента
|
||||
`jira` и прежнюю публикацию `release/agent.py` в `release/jira/`. jira-agent v14 SHALL работать без обновления.
|
||||
|
||||
#### Scenario: jira-agent после выкатки
|
||||
- **WHEN** хаб v6 с агентским токеном T и публикацией v14 обновлён до v7, jira-agent v14 шлёт `claim` с токеном T
|
||||
- **THEN** 200; ответ содержит `agent_release {version: 14, …}`; `GET /agent` показывает агента `jira` с версией 14
|
||||
|
||||
#### Scenario: Агенты не берут чужие команды
|
||||
- **WHEN** в очереди пакет 1 с `ensure_issue` и пакет 2 с `wiki_sync`, оба `queued`; агент `wiki` шлёт `claim`
|
||||
- **THEN** выдан `wiki_sync`; следующий `claim` агента `jira` получает `ensure_issue`, хотя `wiki_sync` ещё `claimed`
|
||||
|
||||
#### Scenario: Чужой маршрут агента
|
||||
- **WHEN** токеном агента `wiki` вызван `POST /agent/state`
|
||||
- **THEN** 401, зеркало не меняется
|
||||
|
||||
#### Scenario: Настройки только своего модуля
|
||||
- **WHEN** токеном агента `wiki` вызван `GET /agent/settings`
|
||||
- **THEN** в ответе есть `wiki_pacing` и `settings_version`, ключей jira (`project_key`, `transitions_map`) нет
|
||||
|
||||
### Requirement: Поля проекта от модулей
|
||||
Проект ядра SHALL состоять из `label`, `name`, `created_at`; остальные поля проекта SHALL объявлять модули: имя, тип,
|
||||
проверку, изменяемость после создания, подпись и подсказку. Модуль SHALL сам хранить свои поля и отдавать их значения
|
||||
(включая вычисляемые) для списка проектов. При старте хаб SHALL отвергать два модуля с одним именем поля или поле
|
||||
модуля с именем поля ядра — ошибка старта с именем поля и модулей. В ядре SHALL NOT оставаться знания о полях модулей
|
||||
(доска, число задач, корень вики).
|
||||
|
||||
#### Scenario: Конфликт полей
|
||||
- **WHEN** два модуля объявляют поле проекта `root`
|
||||
- **THEN** хаб не стартует, ошибка называет поле и оба модуля
|
||||
|
||||
#### Scenario: Поля jira прежние
|
||||
- **WHEN** после миграции v7 вызван `GET /projects`
|
||||
- **THEN** у `AWPCA` `board_id = 4216`, `board_type = scrum`, `issues_count` — как до миграции
|
||||
|
||||
### Requirement: Подготовка отчёта команды модулем
|
||||
Контракт `CommandHooks` SHALL содержать два необязательных хука. `before_result(conn, cmd, status, result, files_dir)`
|
||||
SHALL вызываться ядром при отчёте по выданной команде до транзакции отчёта, чтобы модуль готовил тяжёлую работу (перевод,
|
||||
запись файлов) вне блокировки базы; `ValueError` из него SHALL отдаваться как ошибка сервера, а не как неверный статус
|
||||
отчёта. `abort_result(conn, cmd)` SHALL вызываться, когда отчёт не дошёл до `after_result` (ранний выход, исключение), и
|
||||
снимать подготовку `before_result`. Модуль без этих хуков SHALL работать как прежде.
|
||||
|
||||
#### Scenario: Подготовка до транзакции
|
||||
- **WHEN** агент шлёт отчёт `done` по команде типа модуля с `before_result`
|
||||
- **THEN** `before_result` вызван до `BEGIN IMMEDIATE`, после фиксации транзакции — `after_result`
|
||||
|
||||
#### Scenario: Отчёт прерван
|
||||
- **WHEN** `before_result` выполнен, а транзакция отчёта завершилась исключением
|
||||
- **THEN** вызван `abort_result`, подготовка снята, команда остаётся выданной
|
||||
|
||||
@ -457,12 +457,13 @@ SHALL сообщать об этом с кнопкой «Закрыть»; ха
|
||||
234,4 КБ»; щелчок по любой из них скачивает файл
|
||||
|
||||
### Requirement: Проект в приложении
|
||||
Доска и список задач SHALL показывать один проект — текущий. Текущий проект SHALL задаваться параметром `project` в
|
||||
адресе `/app/board` и `/app/issues`; без параметра — последний выбранный в этом браузере (`localStorage`), если он есть
|
||||
в `GET /projects`, иначе первый из `GET /projects`; выбранный проект записывается в адрес без новой записи истории.
|
||||
Проекта из адреса нет в `GET /projects` — страница SHALL показывать «проект <метка> не найден» и выпадающий список
|
||||
проектов вместо данных. Проектов нет — доска и список SHALL показывать приглашение создать проект («+ Новый проект…»).
|
||||
Переходы «Доска связей» / «Задачи» SHALL сохранять текущий проект. Карточка задачи (`/app/issues/{key}`) от проекта не зависит.
|
||||
Доска, список задач и вики SHALL показывать один проект — текущий. Текущий проект SHALL задаваться параметром
|
||||
`project` в адресе `/app/board`, `/app/issues` и `/app/wiki`; без параметра — последний выбранный в этом браузере
|
||||
(`localStorage`), если он есть в `GET /projects`, иначе первый из `GET /projects`; выбранный проект записывается в адрес
|
||||
без новой записи истории. Проекта из адреса нет в `GET /projects` — страница SHALL показывать «проект <метка> не найден»
|
||||
и выпадающий список проектов вместо данных. Проектов нет — доска, список и вики SHALL показывать приглашение создать
|
||||
проект («+ Новый проект…»). Переходы между «Доска связей», «Задачи» и «Вики» SHALL сохранять текущий проект. Карточка
|
||||
задачи (`/app/issues/{key}`) от проекта не зависит.
|
||||
|
||||
Доска SHALL загружать `GET /board?project=<текущий>`; «Обновить из Jira» — `POST /refresh {full: true}`, обновляет все
|
||||
проекты. Раскладка, дорожки и фильтры доски в `localStorage` SHALL храниться отдельно для каждого проекта;
|
||||
@ -471,9 +472,11 @@ SHALL сообщать об этом с кнопкой «Закрыть»; ха
|
||||
«Спринт» нет — показаны все задачи».
|
||||
|
||||
Форма «Новый проект» (пункт «+ Новый проект…» выпадающего списка) SHALL открываться окном поверх страницы: метка,
|
||||
название, номер доски Jira (необязательно; подсказка «scrum или kanban определится сам»). «Создать» — `POST /projects`
|
||||
с автором `viewer`; ошибка хаба 409/422 — текст `detail` рядом с кнопкой, окно остаётся открытым; успех — окно
|
||||
закрывается, текущим становится новый проект, боковая панель показывает ход пакета обновления из ответа (`batch_id`).
|
||||
название и поля модулей — номер доски Jira (необязательно; подсказка «scrum или kanban определится сам») и корень вики
|
||||
(необязательно; подсказка «DOC-… или ссылка на страницу Eva»). Пустое необязательное поле в запрос не попадает.
|
||||
«Создать» — `POST /projects` с автором `viewer`; ошибка хаба 409/422 — текст `detail` рядом с кнопкой, окно остаётся
|
||||
открытым; успех — окно закрывается, текущим становится новый проект, боковая панель показывает ход пакета обновления из
|
||||
ответа (`batch_id`).
|
||||
|
||||
#### Scenario: Первый вход после выкатки
|
||||
- **WHEN** открыт `/app/board` без `project`, в `localStorage` выбора нет, проекты `AWPCA` и `STOCKS_DRRK`
|
||||
@ -488,10 +491,14 @@ SHALL сообщать об этом с кнопкой «Закрыть»; ха
|
||||
- **THEN** «проект NOPE не найден» и список проектов; запроса `GET /issues` нет
|
||||
|
||||
#### Scenario: Создание проекта из формы
|
||||
- **WHEN** в форме введены `STOCKS_DRRK`, «Запасы ДРРК», доска пусто; нажато «Создать»
|
||||
- **WHEN** в форме введены `STOCKS_DRRK`, «Запасы ДРРК», доска и корень вики пусто; нажато «Создать»
|
||||
- **THEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", author: "viewer"}`; текущий проект — `STOCKS_DRRK`;
|
||||
боковая панель показывает «пакет #N выполняется»
|
||||
|
||||
#### Scenario: Проект с корнем вики из формы
|
||||
- **WHEN** в форме введены `STOCKS_DRRK`, «Запасы ДРРК», корень `DOC-001234`; нажато «Создать»
|
||||
- **THEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", wiki_root: "DOC-001234", author: "viewer"}`
|
||||
|
||||
#### Scenario: Метка занята
|
||||
- **WHEN** в форме введена метка `awpca`, хаб ответил 409
|
||||
- **THEN** окно открыто, рядом с «Создать» — текст ошибки хаба
|
||||
@ -658,3 +665,31 @@ Semibold 14/20, заголовок страницы Bold 20/28. Страницы
|
||||
#### Scenario: Агент старой версии
|
||||
- **WHEN** нажато «Скачать», хаб ответил 409 «агент v13 не умеет скачивать вложения — нужен v14»
|
||||
- **THEN** под вложением этот текст и «Повторить»
|
||||
|
||||
### Requirement: Вики проекта
|
||||
Приложение SHALL иметь страницу «Вики» (`/app/wiki?project=<метка>`, пункт боковой панели; текущий проект — как у доски
|
||||
и задач, «Проект в приложении»). Страница SHALL показывать:
|
||||
- дерево страниц проекта (`GET /wiki/tree`) слева, свёрнутое до первого уровня; узел раскрывается по клику;
|
||||
- выбранную страницу справа (`GET /wiki/pages/{code}`): заголовок, дата синхронизации, ссылка «открыть в Eva»,
|
||||
Markdown с таблицами, без сырого HTML; ссылки `page:<код>` открывают страницу вики (код в адресе `&page=<код>`),
|
||||
ссылки `attachment:<id>` скачивают вложение (запрос, опрос состояния, сохранение файла браузером), состояние
|
||||
`failed` — текст ошибки рядом со ссылкой;
|
||||
- поиск над деревом (`GET /wiki/search`): список результатов с фрагментами вместо дерева, пока строка поиска не пуста;
|
||||
- кнопку «Обновить вики» (`POST /wiki/sync`) и плашку хода синхронизации в боковой панели; ответ 409 — плашка текущего
|
||||
пакета;
|
||||
- проекта без корня — форму «Корень вики (DOC-… или ссылка)» с кнопкой «Привязать» (`PATCH /projects/{label}`); 422 —
|
||||
текст ошибки у поля;
|
||||
- корень есть, синхронизаций нет — «вики ещё не синхронизирована» и кнопку «Обновить вики»;
|
||||
- последняя синхронизация с ошибкой — плашку с текстом ошибки над страницей.
|
||||
|
||||
#### Scenario: Открыть страницу по ссылке
|
||||
- **WHEN** на странице DOC-3 нажата ссылка `page:DOC-2`
|
||||
- **THEN** адрес `/app/wiki?project=AWPCA&page=DOC-2`, справа DOC-2, в дереве DOC-2 выделен
|
||||
|
||||
#### Scenario: Проект без корня
|
||||
- **WHEN** открыта `/app/wiki?project=sfm_kedr`, корня нет; введено `DOC-001234`, нажато «Привязать»
|
||||
- **THEN** `PATCH /projects/sfm_kedr {wiki_root: "DOC-001234"}`; боковая панель показывает ход пакета синхронизации
|
||||
|
||||
#### Scenario: Поиск
|
||||
- **WHEN** в строке поиска «согласов»
|
||||
- **THEN** вместо дерева — результаты `GET /wiki/search?project=AWPCA&q=согласов`; очистка строки возвращает дерево
|
||||
|
||||
159
openspec/specs/wiki/spec.md
Normal file
159
openspec/specs/wiki/spec.md
Normal file
@ -0,0 +1,159 @@
|
||||
# wiki Specification
|
||||
|
||||
## Purpose
|
||||
Модуль `wiki`: корень EVA Wiki у проекта, Markdown-копия дерева страниц, синхронизация через агента `wiki` (eva-agent), поиск, вложения по запросу и маршруты вики.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Корень вики проекта
|
||||
Модуль `wiki` SHALL давать проекту поле `wiki_root` — код корневого документа Eva вида `DOC-<цифры>` (регистр
|
||||
приводится к верхнему; ссылка на документ Eva с таким кодом принимается и сводится к коду) или `null`. Поле
|
||||
необязательно при создании и изменяемо после (`PATCH /projects/{label}`). Задание или смена корня SHALL ставить пакет
|
||||
синхронизации проекта; смена корня SHALL оставлять прежнюю копию до успешной синхронизации нового. Сброс в `null`
|
||||
SHALL удалять копию проекта, его страницы, индекс и строки вложений. Модуль SHALL хранить для проекта `root_title`,
|
||||
`synced_at`, `pages_count`, `sync_error`.
|
||||
|
||||
#### Scenario: Ссылка вместо кода
|
||||
- **WHEN** `PATCH /projects/AWPCA {wiki_root: "https://eva.example.ru/project/Document/doc-001234"}`
|
||||
- **THEN** 200, `wiki_root = DOC-001234`, поставлен `wiki_sync {label: AWPCA, root: DOC-001234}`
|
||||
|
||||
#### Scenario: Неверный код
|
||||
- **WHEN** `PATCH /projects/AWPCA {wiki_root: "страница про статусы"}`
|
||||
- **THEN** 422 с указанием поля `wiki_root`, корень прежний, пакета нет
|
||||
|
||||
#### Scenario: Сброс корня
|
||||
- **WHEN** у `AWPCA` синхронизировано 120 страниц, `PATCH /projects/AWPCA {wiki_root: null}`
|
||||
- **THEN** каталога `wiki/AWPCA/` нет, страниц и вложений проекта в базе нет; `GET /projects` — `wiki_root = null`
|
||||
|
||||
### 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` текущего. Смена или сброс корня
|
||||
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` текущего пакета, нового пакета нет
|
||||
|
||||
### Requirement: Markdown-копия вики
|
||||
Копия проекта SHALL лежать в `<JIRA_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 в копию не попадает.
|
||||
|
||||
#### 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** в HTML `<a href="project/Document/DOC-019834">Модель данных</a>`
|
||||
- **THEN** в `index.md` — `[Модель данных](page:DOC-019834)`
|
||||
|
||||
### Requirement: Вложения вики по запросу
|
||||
`POST /wiki/attachments/{id}/fetch` SHALL ставить пакет из одной команды чтения `wiki_fetch {attachment_id, url}` и
|
||||
переводить вложение в `fetching`; для `fetching` — ответ текущим состоянием без нового пакета, для `ready` — сразу
|
||||
`ready`. Агент `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/` не убран
|
||||
|
||||
### 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}]}`; страницы нет в копии — 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`
|
||||
Loading…
x
Reference in New Issue
Block a user