Andrey Epifancev 67f1a5a096 Каркас eva-agent: общие части jira-agent v14 (#1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 04:40:33 +04:00

14 KiB
Raw Blame History

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 и адаптируются: журнал, транспорт, самообновление, запускатель.
  12. ref вложений и ссылок — значение атрибута после unescape (так отдаёт html.parser). Обоснование: хаб (convert.py, BeautifulSoup .get("src"/"href")) ищет запись по тому же unescaped-значению. Цена ошибки: адреса с &amp; не сопоставятся, вложение останется неразрешённым; правка одной строки.

  13. При расхождении плана с кодом хаба по протоколу прав код хаба. Поручение владельца; расхождения фиксируются находками O-n, хаб не правится. Цена ошибки: агент подгонится под ошибочное поведение хаба; видно в e2e.

  14. Каркас EvaClient создан в задаче 1; Runner(cfg, hub, eva, journal). В EvaClient — только Bearer-заголовок, TLS-настройки, счётчики calls/rate_limited и время последнего вызова (last_call_at, last_call_iso). Обоснование: Runner и Executor из jira-agent опираются на эти поля клиента (пауза между командами, отчёт); без каркаса общие тесты цикла не перенести. JSON-RPC и повторы добавляют задачи 2–3. Цена ошибки: перенос полей в другой класс.

  15. claim шлёт last_call_at, а не last_jira_call_at. Обоснование: хаб (core/api.py) читает body.get("last_call_at", body.get("last_jira_call_at")); новое имя не привязано к Jira. Поля отчёта jira_calls/rate_limited остаются как есть: хаб читает только их. Цена ошибки: если хаб уберёт новое имя, время последнего вызова перестанет доходить (только статус).

  16. Jira-специфичные тесты jira-agent не переносились; test_clients.py написан заново на MiniServer. Исключены: JiraClient (multipart, 429/503), Executor, refresh, история, вложения Jira. Сценарии Http (перенаправления, обрывы, не-JSON, предел fetch_bytes) сохранены на маленьком локальном сервере, фейка Eva в задаче 1 нет (появляется в задаче 2). Обоснование: брифом перенос ограничен общими частями; Jira в агенте нет. Цена ошибки: пропущенный сценарий Http; виден по сравнению со списком тестов jira-agent.

  17. Тесты цикла работают на заглушке wiki_fetch (failed «не реализовано»); паузу и время вызова Eva тесты задают вручную (eva.last_call_at, eva._touch()). Результат done проверяется через подмену Executor.run/do_wiki_fetch. Обоснование: уточнение к задаче 1. Цена ошибки: тесты цикла придётся уточнить, когда команды заработают.

  18. test_run_py_is_copy ищет jira-agent/src/run.py на 1–3 уровня выше корня репозитория; нет файла — skip с причиной. Путь из плана (../../jira-agent) верен для основного checkout, из worktree .wt/<срез> — на уровень глубже. Цена ошибки: тест тихо пропускается на нестандартной раскладке каталогов.

  19. .gitignore дополнен agent.py.new, agent.py.prev, __pycache__/, *.pyc (рабочие файлы самообновления и кэш). Цена ошибки: нет.