docs: change eva-agent-mvp и план (#1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f9442624ce
commit
487391eddc
202
docs/superpowers/plans/2026-10-24-eva-agent-mvp.md
Normal file
202
docs/superpowers/plans/2026-10-24-eva-agent-mvp.md
Normal file
@ -0,0 +1,202 @@
|
||||
# eva-agent MVP — план реализации
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** агент на виртуалке банка выполняет команды модуля `wiki` хаба: `wiki_sync` (дерево EVA Wiki под корнем → пачки страниц хабу) и `wiki_fetch` (вложение до 10 МБ → хранилище хаба), с журналом, транспортом через общую папку и самообновлением, как jira-agent.
|
||||
|
||||
**Architecture:** один файл `src/agent.py` (Python 3.9, stdlib) — копия общих частей jira-agent v14 (HTTP, `FolderTransport`, `HubClient`, `Journal`, `Runner`, `Updater`, CLI) плюс `EvaClient` (JSON-RPC 2.2), `LinkParser` (классификация ссылок HTML) и `Executor` с `do_wiki_sync`/`do_wiki_fetch`; `src/run.py` — дословная копия запускателя.
|
||||
|
||||
**Tech Stack:** Python 3.9.6 (`/usr/bin/python3`), только stdlib, `unittest`.
|
||||
|
||||
**Spec:** контракт — `../../jira-hub/.wt/wiki/openspec/changes/wiki-module/` (worktree хаба, ветка `feat/91-wiki`, PR jira-hub#92): `specs/wiki/spec.md` («Синхронизация вики», «Вложения вики по запросу»), `design.md` решения 10, 11, 14 и Context; решения агента — `openspec/changes/eva-agent-mvp/design.md` этого репозитория (1–11). Код хаба, принимающий агента: `src/jira_hub/modules/wiki/{stage.py,sync.py,attachments.py,commands.py}` в том же worktree хаба.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Python 3.9.6, только стандартная библиотека; синтаксис 3.10+ (`match`, `X | Y` в аннотациях функций) и сторонние импорты запрещены — ловит `tests/test_py39.py`.
|
||||
- Тесты: `/usr/bin/python3 -W error::ResourceWarning -m unittest discover -s tests -t .` — зелёный весь набор после каждой задачи.
|
||||
- `AGENT_VERSION = 1` — ровно одна строка `AGENT_VERSION = <целое>` в `src/agent.py`.
|
||||
- `src/run.py` — байт в байт `../../jira-agent/src/run.py` (сверка `cmp` в тесте `test_run_py_is_copy`, путь к jira-agent — `../../jira-agent/src/run.py` от корня worktree; если файла нет — тест пропускается с причиной).
|
||||
- Токен Eva — только в `agent.json` (не в git: `.gitignore` получает `agent.json`, `*.jsonl`, `update.json`, `*.lock`); в логах и отчётах его нет (тест `test_token_not_logged`).
|
||||
- Протокол хаба: `POST /agent/claim`, `GET /agent/settings`, `GET /agent/commands/{id}`, `POST /agent/commands/{id}/result {status, result, error, jira_calls, rate_limited}`, `GET /agent/release`, `POST /agent/files {content}` → `{file, size}`, `POST /agent/wiki/pages {command_id, pages}`. Форма страницы — `stage._check` хаба.
|
||||
- Не трогать репозитории `jira-agent` (только читать), `jira-hub` (только читать и запускать тестовый хаб во временном `JIRA_HUB_HOME`), живой хаб и `~/.local/share/jira-hub`.
|
||||
- Коммиты — по-русски, последней строкой `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. Дерево Eva с циклом (страница — родитель сама себе или через `tree_parent`) и страница с двумя родителями — обход конечен, страница в отчёте один раз: `test_tree_cycle_and_two_parents` (Task 4).
|
||||
2. HTML с относительными и абсолютными адресами, миниатюрами `.meta/thumbnail.jpg`, `static/`, чужим хостом, `DOC-` в адресе другого хоста — классификация как в design 6: `tests/test_links.py` (Task 3).
|
||||
3. Eva отвечает 429 с `Retry-After`, 503, обрывом соединения, ошибкой JSON-RPC — повторы и итоговый `failed` с текстом без токена: `tests/test_eva_client.py` (Task 2).
|
||||
4. Вложение без `Content-Length` и больше 10 МБ — чтение обрывается на 10 МБ + 1 байт, `failed` с текстом спеки: `test_fetch_too_big_streaming` (Task 5).
|
||||
5. Перезапуск агента посреди `wiki_sync` (журнал: started без finished) — команда выполняется заново, хаб получает полный набор пачек и один отчёт: `test_recover_resyncs` (Task 4).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Каркас — общие части jira-agent, конфиг, запускатель
|
||||
|
||||
**Files:**
|
||||
- Create: `src/agent.py`, `src/run.py` (копия), `src/agent.example.json`, `tests/{__init__.py,_common.py,fake_hub.py,fake_relay.py,test_py39.py,test_clients.py,test_folder.py,test_journal.py,test_runner.py,test_update.py,test_launcher.py,test_self_update_e2e.py}`, `.gitignore` (дописать)
|
||||
- Modify: `CLAUDE.md`, `ARCHITECTURE.md` (заполнить шаблоны по образцу `../../jira-agent/CLAUDE.md` и `ARCHITECTURE.md`, для eva-agent), `README.md` (создать: назначение, установка — Task 6 допишет)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `load_config(path) -> dict` (обязательны `hub_url`, `hub_token`, `eva.base_url`, `eva.token`; `folder:` — абсолютный путь; умолчания как у jira-agent).
|
||||
- Копия из jira-agent без изменений смысла: `Http`, `HttpError`, `RedirectAway`, `TooBig`, `FolderTransport`, `Journal`, `Updater`, `acquire_lock`, `launcher_version`, `cmd_run`, `cmd_prune`, `main`.
|
||||
- `HubClient` — без jira-маршрутов (`state`, `known_keys`, `history`), плюс `wiki_pages(command_id, pages) -> dict`.
|
||||
- `Runner` — вызывает `Executor.run(cmd) -> dict` (`{status, result, error, jira_calls, rate_limited}`), как в jira-agent.
|
||||
- `Executor(eva, hub, settings)` с `do_wiki_sync`/`do_wiki_fetch`, в этой задаче — `failed` «не реализовано».
|
||||
- `cmd_check(cfg) -> int` проверяет только хаб; Eva добавит Task 6.
|
||||
- Тесты общих частей переносятся из `../../jira-agent/tests/` с заменой: Jira → нет, `ensure_*` → команда-заглушка `wiki_fetch` с `failed`. Сценарии журнала, папки, 401/403-паузы, самообновления и запускателя сохраняются.
|
||||
|
||||
- [ ] **Step 1:** перенести тесты общих частей, они упадут — `agent.py` нет.
|
||||
- [ ] **Step 2:** собрать `src/agent.py` из jira-agent v14:
|
||||
- вырезать `JiraClient`, jira-методы `Executor`, `ConfigGap` и проверки Jira;
|
||||
- `AGENT_VERSION = 1`;
|
||||
- `run.py` скопировать.
|
||||
- [ ] **Step 3:** весь набор — PASS; `test_run_py_is_copy` и `test_py39` зелёные.
|
||||
- [ ] **Step 4: Commit** «Каркас eva-agent: общие части jira-agent v14 (#1)».
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Клиент Eva
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/agent.py` (класс `EvaClient(Http)` после `HubClient`)
|
||||
- Create: `tests/fake_eva.py`, `tests/test_eva_client.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `EvaClient(cfg_eva: dict, timeout, verify_ssl, ca_bundle)`, его методы:
|
||||
- `call(method, **kwargs) -> Any` — `POST <base>/api/?m=<method>`, `Bearer`; повторы по design 4; `error` в ответе — `EvaError("<method>: <error>")`;
|
||||
- `get_doc(code, fields) -> dict | None`;
|
||||
- `list_docs(flt, fields) -> list[dict]` — страницы по 200 через `slice`; обёрнутый результат `{items|result}` разворачивается;
|
||||
- `download(url, limit) -> bytes` — поток с обрывом на `limit + 1`; больше лимита — `TooBig`.
|
||||
- Свойства: `calls` (число вызовов), `rate_limited` (число 429), `pacing` (пауза, с; ставит Runner из `wiki_pacing`).
|
||||
- `tests/fake_eva.py: FakeEva` — `http.server` на порту 0:
|
||||
- хранит документы `{id, code, name, parent, tree_parent, project, orderno, text}`;
|
||||
- поддерживает фильтры `code ==`, `project.code ==`, `tree_parent.code ==`, `slice`;
|
||||
- сценарии сбоев: `fail_next(status, times, retry_after=None)`, `rpc_error_next(text)`;
|
||||
- файлы `/files/...` с `Content-Length` или без него;
|
||||
- проверяет `Authorization`.
|
||||
|
||||
- [ ] **Step 1: Падающие тесты:**
|
||||
- `test_call_ok`;
|
||||
- `test_retry_429_retry_after` — ждал `Retry-After` через подменённый `sleep`;
|
||||
- `test_retry_503_then_ok`;
|
||||
- `test_gives_up_after_4`;
|
||||
- `test_rpc_error`;
|
||||
- `test_list_pages_200` — 450 документов за три вызова;
|
||||
- `test_pacing_between_calls`;
|
||||
- `test_download_limit` — без `Content-Length`, 10 МБ + 1;
|
||||
- `test_token_not_logged` — токен не встречается ни в логе, ни в тексте ошибок.
|
||||
- [ ] **Step 2: FAIL. Step 3: Реализовать. Step 4: PASS.**
|
||||
- [ ] **Step 5: Commit** «Клиент Eva JSON-RPC (#1)».
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Разбор ссылок страницы
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/agent.py` (функция `classify_links(html, base_url) -> tuple[list, list]` и `class _LinkParser(HTMLParser)`)
|
||||
- Create: `tests/test_links.py`, `tests/fixtures/pages/*.html` (синтетические, форматы адресов из design хаба, Context; текст ТЗ не копировать)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `classify_links(html: str, base_url: str) -> (attachments: [{ref, url, name}], links: [{ref, code}])` по design 6–7, в порядке появления, без склейки повторов.
|
||||
|
||||
- [ ] **Step 1: Падающие тесты:**
|
||||
|
||||
```python
|
||||
def test_image_relative_files() # src="/files/obj/CmfDocument/CmfDocument%3Aa08/CmfDocument%3A<uuid>/img.png?version=1" →
|
||||
# url "https://eva.test/files/obj/…/img.png" (без query), name "img.png"
|
||||
def test_thumbnail_maps_to_file() # ".../model.xlsx.meta/thumbnail.jpg?version=1" → url ".../model.xlsx", name из alt или "model.xlsx"
|
||||
def test_file_link_same_url() # <a href=".../model.xlsx">model.xlsx</a> → тот же url, что у миниатюры
|
||||
def test_static_ignored() # src="static/attach-file.png" → ни в одном списке
|
||||
def test_page_link_relative() # href="project/Document/DOC-019834" → links [{ref, code: "DOC-019834"}]
|
||||
def test_foreign_host() # href="https://other.test/files/x.png" и "https://other.test/project/Document/DOC-1" → пусто
|
||||
def test_name_unquoted() # ".../%D0%9F%D0%9C%D0%9F.xlsx" без alt и текста → name "ПМП.xlsx"
|
||||
def test_ref_exact() # ref равен атрибуту байт в байт (с & → как отдаёт HTMLParser: unescape; проверить, что хаб ищет так же — см. convert.py хаба)
|
||||
```
|
||||
|
||||
Для `test_ref_exact` надо сверить, с чем хаб сравнивает `ref`: с сырым значением атрибута или с unescape. Это видно в `../../jira-hub/.wt/wiki/src/jira_hub/modules/wiki/convert.py`. Записать, как сделано, в отчёт и в design (решение 12).
|
||||
- [ ] **Step 2: FAIL. Step 3: Реализовать. Step 4: PASS.**
|
||||
- [ ] **Step 5: Commit** «Разбор ссылок страниц Eva (#1)».
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Команда `wiki_sync`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/agent.py` (`Executor.do_wiki_sync`, функции `build_children(docs)`, `walk(root, children)`), `tests/fake_hub.py` (`POST /agent/wiki/pages` — записывает пачки; проверка формы по образцу `stage._check` хаба: `code`, `title`, `html` строки; ≤ 50 страниц)
|
||||
- Create: `tests/test_wiki_sync.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `EvaClient` (Task 2), `classify_links` (Task 3), `HubClient.wiki_pages` (Task 1).
|
||||
- Produces: `Executor.do_wiki_sync(params {label, root}) -> {status, result {root_title, codes, eva_calls}, error, jira_calls, rate_limited}` по design 5; пачки — `MAX_PAGES = 50`, `MAX_BATCH_BYTES = 4 * 1024 * 1024`.
|
||||
|
||||
- [ ] **Step 1: Падающие тесты:**
|
||||
|
||||
```python
|
||||
def test_sync_tree_order() # корень DOC-1 (project P), дети DOC-3 (orderno 1), DOC-2 (orderno 2), внук DOC-4 под DOC-3 →
|
||||
# codes [DOC-1, DOC-3, DOC-4, DOC-2]; parent_code; url ".../project/Document/DOC-3"
|
||||
def test_sync_batches_50() # 120 страниц → пачки 50, 50, 20; codes — 120
|
||||
def test_sync_batch_bytes() # страницы по 1,5 МБ → пачка не больше 4 МБ
|
||||
def test_tree_cycle_and_two_parents() # A.parent=B, B.tree_parent=A; C с parent=корень и tree_parent=DOC-3 → каждый код один раз
|
||||
def test_root_without_project() # у корня нет project → обход через tree_parent.code
|
||||
def test_root_missing() # get_doc → None → failed «страница DOC-9 не найдена или нет прав»
|
||||
def test_eva_failure_reported() # Eva 503 четыре раза подряд → failed, текст содержит "503", токена в тексте нет
|
||||
def test_hub_rejects_batch() # fake_hub 422 на пачку → failed с detail хаба
|
||||
def test_recover_resyncs() # журнал started без finished, команда claimed в хабе → выполняется заново: полные пачки, один done
|
||||
def test_counts() # jira_calls == eva.calls, rate_limited == числу 429
|
||||
```
|
||||
|
||||
- [ ] **Step 2: FAIL. Step 3: Реализовать. Step 4: PASS.**
|
||||
- [ ] **Step 5: Commit** «Команда wiki_sync (#1)».
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Команда `wiki_fetch`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/agent.py` (`Executor.do_wiki_fetch`)
|
||||
- Create: `tests/test_wiki_fetch.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `Executor.do_wiki_fetch(params {attachment_id, url}) -> {status, result {file, size}, …}` по design 8; использует `HubClient.upload_file` (как у jira-agent `fetch_attachment`).
|
||||
|
||||
- [ ] **Step 1: Падающие тесты:**
|
||||
- `test_fetch_ok` — 300 КБ; `done`, `size` 307200; файл у fake_hub совпадает побайтно;
|
||||
- `test_fetch_foreign_host` — `failed` «чужой хост», у fake_eva нет запросов;
|
||||
- `test_fetch_too_big_header` — `Content-Length` 11 МБ, тело не читается;
|
||||
- `test_fetch_too_big_streaming` — без `Content-Length`;
|
||||
- `test_fetch_404` — «файл не найден в Eva»;
|
||||
- `test_fetch_empty_file` — 0 байт; `done`, `size` 0: хаб принимает пустой файл от агента.
|
||||
- [ ] **Step 2: FAIL. Step 3: Реализовать. Step 4: PASS.**
|
||||
- [ ] **Step 5: Commit** «Команда wiki_fetch (#1)».
|
||||
|
||||
---
|
||||
|
||||
### Task 6: `check`, документация, сквозной прогон с хабом
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/agent.py` (`cmd_check` — хаб и Eva по design 10), `README.md`, `CLAUDE.md`, `ARCHITECTURE.md`, `openspec/changes/eva-agent-mvp/design.md` (решения исполнителей), `src/agent.example.json`
|
||||
- Create: `scripts/e2e-hub.sh`, `tests/test_check.py`
|
||||
|
||||
**Interfaces:**
|
||||
- `scripts/e2e-hub.sh`:
|
||||
- поднимает тестовый хаб из `../../jira-hub/.wt/wiki`: `uv run --project <путь> jira-hub serve --port 0`, неудобно — тогда фиксированный свободный порт, например 8795, с `JIRA_HUB_HOME=$(mktemp -d)`;
|
||||
- поднимает `FakeEva` с деревом из 5 страниц, картинкой и файлом;
|
||||
- заводит проект `E2E` с `wiki_root` через `POST /projects` клиентским токеном из `jira-hub tokens`;
|
||||
- запускает агент `src/agent.py run` с токеном агента `wiki` до первого `done`; нужен флаг `--once` или тайм-аут — решает исполнитель, без изменения протокола;
|
||||
- проверяет `GET /wiki/tree?project=E2E` (5 страниц), `GET /wiki/pages/<код>` (Markdown со ссылкой `attachment:`), затем `POST /wiki/attachments/<id>/fetch` и `GET …/content`;
|
||||
- печатает «e2e: ok»; хаб и фейк гасятся по `trap`.
|
||||
- README — установка на виртуалку:
|
||||
- каталог рядом с jira-agent (например `C:\…\eva-agent\` или `~/eva-agent/`);
|
||||
- файлы `agent.py`, `run.py`, `agent.json` (из примера: `hub_url` = тот же `folder:<путь>`, что у jira-agent, `hub_token` = «агент wiki» из `jira-hub tokens`, `eva.base_url`, `eva.token` — профиль Eva → Безопасность);
|
||||
- `python3 agent.py check`, затем `python3 run.py`;
|
||||
- автозапуск — как у jira-agent;
|
||||
- выкатка новых версий — `uv run --project ../jira-hub jira-hub agent publish --module wiki src/agent.py`.
|
||||
|
||||
- [ ] **Step 1:** тесты `test_check` (хаб ok и Eva ok → 0; Eva 401 → 3, «eva: …»). FAIL → реализовать → PASS.
|
||||
- [ ] **Step 2:** `bash scripts/e2e-hub.sh` → «e2e: ok». Хаб — только из worktree `feat/91-wiki` и только во временном `JIRA_HUB_HOME`.
|
||||
- [ ] **Step 3:** документация.
|
||||
- [ ] **Step 4:** весь набор — PASS.
|
||||
- [ ] **Step 5: Commit** «check, README, сквозной прогон с хабом (#1)».
|
||||
2
openspec/changes/eva-agent-mvp/.openspec.yaml
Normal file
2
openspec/changes/eva-agent-mvp/.openspec.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-24
|
||||
111
openspec/changes/eva-agent-mvp/design.md
Normal file
111
openspec/changes/eva-agent-mvp/design.md
Normal file
@ -0,0 +1,111 @@
|
||||
# eva-agent-mvp — решения агента
|
||||
|
||||
Спека и дизайн — в `jira-hub/openspec/changes/wiki-module/` (issue aep/jira-hub#91, PR aep/jira-hub#92):
|
||||
- решения 10, 11, 14 и Context (форматы ссылок Eva);
|
||||
- требования `wiki` «Синхронизация вики» и «Вложения вики по запросу»;
|
||||
- `modules` «Агенты модулей»;
|
||||
- `hub-api` «Маршруты агента» (общие `/agent/*`).
|
||||
|
||||
Issue агента — aep/eva-agent#1. Версия агента — 1. Агент работает за модуль `wiki`: имя агента в хабе `wiki`, токен —
|
||||
`jira-hub tokens` → «агент wiki».
|
||||
|
||||
Владелец 24.10.2026 разрешил делать агента ночью без обсуждения («завтра хочу уже иметь рабочий mvp»). Поэтому все
|
||||
решения ниже приняты агентом и перечислены в разделе «Decisions made without discussion».
|
||||
|
||||
## Context
|
||||
|
||||
- **Источник для копирования — jira-agent v14** (`../jira-agent/src/agent.py`, `run.py`). Его части, не связанные с
|
||||
Jira, отлажены с v2 по v14:
|
||||
- HTTP-слой: `Http`, `HttpError`, `RedirectAway`, `TooBig`, `_SameOriginRedirect`;
|
||||
- транспорт `FolderTransport` и клиент хаба `HubClient`;
|
||||
- журнал `Journal`, цикл `Runner`, самообновление `Updater`;
|
||||
- `acquire_lock`, CLI `run|check|selftest|prune`, `launcher_version`.
|
||||
- **Клиент Eva и обход дерева — по образцу** `../eva_wiki_export.py`:
|
||||
- JSON-RPC 2.2: `POST <base>/api/?m=<метод>`, `Authorization: Bearer`, тело `{jsonrpc: "2.2", method, callid,
|
||||
kwargs}`;
|
||||
- повторы на 429/502/503/504 и сетевых сбоях;
|
||||
- `CmfDocument.get`/`list`, страницы списка по 200;
|
||||
- дерево — по `parent`/`tree_parent` из списка документов проекта (`project.code`), запасной путь — обход через
|
||||
`tree_parent.code`.
|
||||
- **Хаб принимает:**
|
||||
- пачки `POST /agent/wiki/pages {command_id, pages}` — до 50 страниц, тело до 16 МБ (`MAX_BODY` хаба);
|
||||
- форму страницы проверяет `modules/wiki/stage.py`;
|
||||
- отчёт `wiki_sync done {root_title, codes, eva_calls}`: `codes[0]` должен быть кодом корня;
|
||||
- отчёт `wiki_fetch done {file, size}`: файл должен лежать в хранилище хаба с этим размером.
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
Дата всех решений — 2026-10-24.
|
||||
|
||||
1. **Скопировать общие части jira-agent v14, а не выделять библиотеку.**
|
||||
Обоснование: решение владельца 23.10 — однофайловый агент на stdlib на виртуалке. Копия сохраняет отлаженное
|
||||
поведение.
|
||||
Цена ошибки: правки общих частей придётся повторять в двух агентах.
|
||||
2. **`run.py` — дословная копия запускателя jira-agent** (`LAUNCHER_VERSION = 2`), вместе с переменной окружения
|
||||
`JIRA_AGENT_LAUNCHER`.
|
||||
Обоснование: в протоколе самообновления важны имена `agent.py`, `update.json` и `AGENT_VERSION`; переменная —
|
||||
внутреннее дело пары запускатель–агент.
|
||||
Цена ошибки: переименование переменной в обоих файлах.
|
||||
3. **Конфиг `agent.json`:**
|
||||
- обязательны `hub_url`, `hub_token`, `eva.base_url`, `eva.token`;
|
||||
- необязательны `verify_ssl`, `ca_bundle`, `timeout` (60), `poll_seconds` (5), `auto_update` (true).
|
||||
|
||||
Обоснование: формат jira-agent, где ключ `jira` заменён на `eva`.
|
||||
Цена ошибки: правка шаблона и `load_config`.
|
||||
4. **Пауза между вызовами Eva — `wiki_pacing` из `GET /agent/settings`** (по умолчанию 0.2 с).
|
||||
- 429/502/503/504 и сетевые сбои: до 4 попыток с ожиданием 1, 2, 4 с.
|
||||
- При 429 учитывается `Retry-After` (не больше 60 с).
|
||||
- Число 429 уходит в `rate_limited` отчёта, число вызовов Eva — в `jira_calls`: поле протокола хаба, имя общее
|
||||
для всех агентов.
|
||||
|
||||
Цена ошибки: Eva режет запросы; правится настройкой без выпуска агента.
|
||||
5. **`wiki_sync` — порядок работы:**
|
||||
1. Корень берётся через `CmfDocument.get` по коду из `params.root`. Нет корня — `failed` «страница <код> не найдена
|
||||
или нет прав».
|
||||
2. Дерево строится как в `collect_subtree`.
|
||||
3. Обход в глубину от корня с защитой от циклов (`seen` по `id`). Соседи — по `(orderno is None, orderno, name)`.
|
||||
4. По каждой странице отдельный `CmfDocument.get(fields=["code","name","text"])`.
|
||||
5. Страницы уходят пачками по мере обхода: 50 страниц или 4 МБ JSON, что наступит раньше.
|
||||
6. Отчёт `codes` — коды в порядке обхода, корень первым.
|
||||
- Поля страницы: `eva_id = str(id)`, `parent_code` — код родителя по обходу (у корня `null`), `orderno` — целое или
|
||||
`null`, `url = urljoin(base_url + "/", "project/Document/<code>")`.
|
||||
|
||||
Цена ошибки: дерево не совпадёт с Eva; лечится правкой обхода и новой версией через самообновление.
|
||||
6. **Ссылки разбирает `html.parser.HTMLParser` из stdlib.**
|
||||
- Берутся `img[src]` и `a[href]`.
|
||||
- Адрес разрешается `urljoin(base_url + "/", ref)`. «Свой» — тот же `scheme` и `netloc`, что у `eva.base_url`.
|
||||
- **Вложение** — свой адрес с путём, начинающимся на `/files/`. Из `url` убираются query и fragment. Миниатюра
|
||||
`<путь>.meta/thumbnail.jpg` означает файл `<путь>`.
|
||||
- **Ссылка на страницу** — свой адрес с `DOC-<цифры>` в пути (код в верхнем регистре).
|
||||
- **`static/…`** и прочие ссылки не передаются: значки убирает хаб, прочие остаются в тексте.
|
||||
- **Имя вложения** — `alt` или `title`, затем текст ссылки, затем последний сегмент пути после `unquote`.
|
||||
- Каждая ссылка передаётся отдельной записью `{ref, url, name}` или `{ref, code}`, `ref` — ровно значение
|
||||
атрибута. Повторы по `url` склеивает хаб.
|
||||
|
||||
Обоснование: форматы из выгрузки ТЗ АРМ КА (design хаба, Context).
|
||||
Цена ошибки: вложение продублируется или потеряется; проверяется на первой живой синхронизации, правится агентом.
|
||||
7. **Без `?version=1` в `url` вложения.**
|
||||
Обоснование: миниатюра и файл дают один адрес, хаб склеивает вложение.
|
||||
Цена ошибки: если Eva без `version` не отдаёт файл, `wiki_fetch` падает; правка одной строки.
|
||||
8. **`wiki_fetch`:**
|
||||
- адрес не своего хоста — `failed` «чужой хост», запроса нет;
|
||||
- файл скачивается с Bearer-токеном Eva потоком до 10 МБ + 1 байт;
|
||||
- `Content-Length` больше 10 МБ или прочитано больше — `failed` «файл больше 10 МБ — откройте в Eva»;
|
||||
- иначе `POST /agent/files` и `done {file, size}`;
|
||||
- HTTP 404 — `failed` «файл не найден в Eva».
|
||||
|
||||
Цена ошибки: большие файлы только ссылкой.
|
||||
9. **Обе команды только читают Eva, поэтому восстановление после перезапуска выполняет их заново.** Журнал нужен
|
||||
только для досылки отчёта; `wiki_sync` не продолжается с середины. Хаб очищает стадию при повторной выдаче.
|
||||
Цена ошибки: лишняя полная перекачка после перезапуска.
|
||||
10. **`check`:**
|
||||
- хаб: `GET /agent/settings` своим токеном;
|
||||
- Eva: `CmfDocument.list(filter=[], fields=["code"], slice=[0, 1])`;
|
||||
- вывод «хаб: ok», «eva: ok» или текст ошибки, код 0 или 3, как у jira-agent;
|
||||
- `selftest` — версия плюс `check`.
|
||||
11. **Тесты:**
|
||||
- `unittest` на `/usr/bin/python3` (3.9);
|
||||
- фейки `tests/fake_hub.py` (как у jira-agent, плюс `POST /agent/wiki/pages`), `tests/fake_eva.py` (JSON-RPC на
|
||||
`http.server`), `tests/fake_relay.py`;
|
||||
- `tests/test_py39.py` — копия.
|
||||
- Тесты общих частей переносятся из jira-agent и адаптируются: журнал, транспорт, самообновление, запускатель.
|
||||
17
openspec/changes/eva-agent-mvp/proposal.md
Normal file
17
openspec/changes/eva-agent-mvp/proposal.md
Normal file
@ -0,0 +1,17 @@
|
||||
## Why
|
||||
|
||||
Модулю `wiki` хаба (jira-hub#91, PR #92) нужен агент на виртуалке банка: Eva доступна только из сети банка, токен Eva
|
||||
не должен покидать виртуалку. Решение владельца 23.10.2026: отдельный агент, не jira-agent.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Новый агент `eva-agent`: `src/agent.py` (Python 3.9, stdlib) и `src/run.py`. Транспорт, журнал и самообновление
|
||||
такие же, как у jira-agent v14.
|
||||
- Команды хаба `wiki_sync` и `wiki_fetch`. Их контракт описан в `jira-hub/openspec/changes/wiki-module/specs/wiki/spec.md`.
|
||||
- README по установке на виртуалку и сквозной прогон с тестовым хабом.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Запись в Eva и история изменений.
|
||||
- Частичная перекачка дерева (нужна дата изменения документа в Eva).
|
||||
- Общая библиотека с jira-agent.
|
||||
Loading…
x
Reference in New Issue
Block a user