Документация и решения 36–43: выкатка --module wiki, base_url, launchd (I-3, T6.1, T6.4, M-2, M-6, #1)
- CLAUDE.md: `jira-hub agent publish --module wiki src/agent.py`, порог `wiki_min_agent_version`. - README: `wiki_min_agent_version` вместо `min_agent_version`; `eva.base_url` — только origin; агент только для macOS (fcntl), пример launchd `ru.aepif.eva-agent`; строка про проверку SSO на первой живой синхронизации (M-5). - ARCHITECTURE: строки Eva и Executor по реализованному, строки ссылок (`_LinkParser`, `classify_links`) и дерева (`build_children`, `walk`). - design.md: решения 36–43, в том числе почему M-5 отложен до живой проверки. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
343c49fc2e
commit
cf7c7bde8c
@ -11,11 +11,13 @@
|
||||
| Настройки | `load_config` | читает `agent.json`, проверяет обязательные поля (`hub_url`, `hub_token`, `eva.base_url`, `eva.token`) | — |
|
||||
| Самообновление (состояние) | `read_update_state`, `write_update_state` | `update.json` рядом с `agent.py` | `os`, `json` |
|
||||
| HTTP | `Http`, `HttpError`, `RedirectAway`, `TooBig`, `_SameOriginRedirect` | запросы; `Http.send` — запрос с готовым телом; `fetch_bytes` — содержимое файла байтами, не больше предела; перенаправление на другой адрес — `RedirectAway` без перехода | `urllib` |
|
||||
| Eva | `EvaClient` | клиент Eva (Bearer-токен): счётчики вызовов и 429, время последнего вызова. Каркас; JSON-RPC и повторы — в задачах 2–3 | `Http` |
|
||||
| Eva | `EvaClient`, `EvaError`, `ForeignHost`, `_quote_url_part` | клиент Eva (Bearer-токен): JSON-RPC 2.2 `call` (`POST /api/?m=<метод>`, `error` в ответе — `EvaError`), повторы 429/502/503/504 и сетевых сбоев (4 попытки, `Retry-After`), пауза `pacing` между вызовами, токен вырезается из текстов ошибок; `get_doc`, `list_docs` (страницы по 200, стоп, если Eva не соблюдает `slice`), `download` (только адрес своего хоста, иначе `ForeignHost`; путь кодируется перед запросом; предел 10 МБ); счётчики вызовов и 429, время последнего вызова | `Http` |
|
||||
| Ссылки | `_LinkParser`, `_ParseError`, `classify_links` | `_LinkParser` (`html.parser`) собирает `img[src]` и `a[href]` в порядке появления, нестандартные `<![…]>` пропускает; `classify_links` делит их на вложения `{ref, url, name}` (свой хост, `/files/…`, миниатюра → файл) и ссылки на страницы `{ref, code}` (`DOC-<цифры>`); чужой хост, `static/` и битые адреса пропускает | `html.parser`, `urllib.parse` |
|
||||
| Дерево | `build_children`, `walk`, `_ref_code`, `_ref_id`, `_orderno` | `build_children` — `{код родителя: [дети]}`: с проектом — один список документов проекта и связи `parent`/`tree_parent` (ссылка разрешается в код через карту id → код), без проекта — обход в ширину по `tree_parent.code`; соседи по `(orderno, name, code)`. `walk` — обход в глубину, корень первым, каждый код один раз (циклы, два родителя) | `EvaClient` |
|
||||
| Папка | `FolderTransport` | транспорт `folder:` к хабу через общую папку: `req/<id>.json` → реле хаба → `resp/<id>.json`; ошибки как у `Http` | `os`, `uuid` |
|
||||
| Хаб | `HubClient` | `claim`, `release`, `settings`, `command`, `result`, `upload_file` (`POST /agent/files`), `wiki_pages` (`POST /agent/wiki/pages`); при `hub_url = folder:…` делегирует `FolderTransport` | `Http`, `FolderTransport` |
|
||||
| Журнал | `Journal`, `_parse_at` | дозапись `journal.jsonl`: `claimed` → `started` → `finished` → `acked`; восстановление; `prune` | файловая система |
|
||||
| Исполнитель | `Executor`, `HubError` | по методу `do_<тип>` на команду: `do_wiki_sync`, `do_wiki_fetch` (в каркасе — `failed` «не реализовано»); сбой хаба внутри команды — `HubError` | `EvaClient`, `HubClient` |
|
||||
| Исполнитель | `Executor`, `HubError` | по методу `do_<тип>` на команду. `do_wiki_sync`: корень, `build_children` + `walk`, по странице `CmfDocument.get` и `classify_links`, пачки в стадию хаба (50 страниц или 4 МБ; 413 — ошибка с кодом страницы/границами пачки), отчёт `{root_title, codes, eva_calls}`. `do_wiki_fetch`: `download` → `POST /agent/files`, отчёт `{attachment_id, file, size}`; «чужой хост», «файл больше 10 МБ», «файл не найден в Eva». Ошибки: сбой хаба — `HubError` («хаб: …»), `EvaError` — «Eva: …», 5xx и сеть — повтор через `Runner` | `EvaClient`, `HubClient`, `classify_links`, `build_children`, `walk` |
|
||||
| Самообновление | `Updater`, `launcher_version` | после `claim` без команды — проверка `agent_release`, скачивание, sha256, компиляция, `selftest`, `staged`; подтверждение `ok` новой версией | `HubClient`, `subprocess` |
|
||||
| Цикл | `Runner` | claim, пауза до команды (по времени последнего вызова Eva), журнал, выполнение, отчёт до подтверждения, восстановление при старте; выход `EXIT_UPDATE` (75) | `HubClient`, `EvaClient`, `Journal`, `Executor` |
|
||||
| CLI | `cmd_run`, `cmd_check`, `cmd_selftest`, `cmd_prune`, `main`, `acquire_lock` | команды `run` (`--once` — выйти при первом пустом claim), `check` (хаб `GET /agent/settings` + Eva `CmfDocument.list`; код 0 или 3), `selftest`, `prune` (`run` и `prune` берут блокировку `<журнал>.lock`); сигналы | `Runner`, `Journal` |
|
||||
@ -42,3 +44,5 @@
|
||||
|
||||
## Открытые вопросы
|
||||
- Живая проверка на виртуалке с настоящей Eva (после merge hub#92).
|
||||
- Ответ Eva на протухший токен (перенаправление на SSO, страница входа с кодом 200 или 401) — проверить при первой
|
||||
живой синхронизации (M-5, design 42).
|
||||
|
||||
@ -50,7 +50,8 @@
|
||||
## Первая подсистема
|
||||
MVP агента: каркас с общими частями (задача 1 плана `docs/superpowers/plans/`), затем клиент Eva, обход дерева,
|
||||
`wiki_sync`, `wiki_fetch`, установка и живая проверка. Версия агента — 1. Выкатка: `uv run --project ../jira-hub
|
||||
jira-hub agent publish src/agent.py` (из основного checkout), как у jira-agent; токен — «агент wiki».
|
||||
jira-hub agent publish --module wiki src/agent.py` (из основного checkout; без `--module` версия уйдёт агенту jira);
|
||||
порог — настройка хаба `wiki_min_agent_version`; токен — «агент wiki».
|
||||
|
||||
## OpenSpec и superpowers
|
||||
- Вход в любую задачу — superpowers:brainstorming. Skills openspec-propose и openspec-explore сами по себе не запускать.
|
||||
|
||||
53
README.md
53
README.md
@ -4,30 +4,75 @@
|
||||
Eva (`wiki_sync`) и скачивает вложения (`wiki_fetch`); в Eva только читает. Python 3.9, только стандартная библиотека,
|
||||
один файл `src/agent.py` и запускатель `src/run.py`.
|
||||
|
||||
Агент работает на macOS (виртуалка банка, как jira-agent): блокировка журнала — через `fcntl`, Windows не поддерживается.
|
||||
|
||||
## Установка на виртуалку
|
||||
|
||||
1. Каталог рядом с jira-agent, например `C:\...\eva-agent\` или `~/eva-agent/`.
|
||||
1. Локальный каталог рядом с jira-agent, например `~/eva-agent/` (запускать из него, не из общей папки).
|
||||
2. Положить в него `agent.py` и `run.py` (из `src/`) и `agent.json` (из `agent.example.json`):
|
||||
- `hub_url` — тот же `folder:<путь>`, что у jira-agent (общая папка с хабом; при VPN виртуалка не видит Mac mini по
|
||||
сети), либо `http://<хост>:8790`;
|
||||
- `hub_token` — строка «агент wiki» из `jira-hub tokens` (токен именно агента wiki, не jira и не клиентский);
|
||||
- `eva.base_url` — адрес Eva; `eva.token` — токен из профиля Eva (Безопасность);
|
||||
- `eva.base_url` — адрес Eva **только origin**: схема, хост и порт, без пути и без `/` в конце, например
|
||||
`https://eva.example.ru`. Агент сам добавляет к нему `/api/` (JSON-RPC), `/files/…` (вложения) и
|
||||
`/project/Document/<код>` (ссылка на страницу); «свой» адрес вложения сравнивается с ним же, поэтому путь в
|
||||
`base_url` (`https://eva.example.ru/eva`) ломает и вызовы, и проверку хоста;
|
||||
- `eva.token` — токен из профиля Eva (Безопасность);
|
||||
- `verify_ssl`, `ca_bundle` — при корпоративном сертификате.
|
||||
`agent.json` содержит токены: не коммитить, не выносить с виртуалки.
|
||||
3. Проверка: `python3 agent.py check` — печатает «хаб: доступен…» и «eva: ok»; код 0, при недоступности хаба или Eva — 3.
|
||||
4. Запуск: `python3 run.py` (запускатель следит за версиями и самообновлением). `python3 agent.py run` — без запускателя.
|
||||
5. Автозапуск — как у jira-agent (планировщик задач / launchd по тому же образцу); запускатель сам перезапускает агента.
|
||||
5. Автозапуск — launchd, как у jira-agent, со своей меткой (см. ниже); запускатель сам перезапускает агента.
|
||||
|
||||
Другие команды: `python3 agent.py run --once` (выполнить выданное и выйти при первом пустом claim), `selftest`
|
||||
(версия + `check`, код 0 или 3), `prune` (чистка журнала).
|
||||
|
||||
Проверить при первой живой синхронизации: как Eva отвечает на протухший токен — перенаправлением на SSO (агент
|
||||
не идёт на чужой адрес и пишет «перенаправление … отклонено»), страницей входа с кодом 200 («ответ не JSON») или 401.
|
||||
|
||||
## Автозапуск через launchd
|
||||
|
||||
Пример `~/Library/LaunchAgents/ru.aepif.eva-agent.plist` (метка своя, не `ru.aepif.jira-agent`: агенты запускаются
|
||||
и останавливаются независимо):
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key><string>ru.aepif.eva-agent</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/bin/python3</string>
|
||||
<string>-u</string>
|
||||
<string>/Users/USER/eva-agent/run.py</string>
|
||||
</array>
|
||||
<key>WorkingDirectory</key><string>/Users/USER/eva-agent</string>
|
||||
<key>RunAtLoad</key><true/>
|
||||
<key>KeepAlive</key><true/>
|
||||
<key>StandardOutPath</key><string>/Users/USER/eva-agent/agent.log</string>
|
||||
<key>StandardErrorPath</key><string>/Users/USER/eva-agent/agent.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
Подставить свой путь вместо `USER`. Запуск и остановка:
|
||||
|
||||
```
|
||||
launchctl load ~/Library/LaunchAgents/ru.aepif.eva-agent.plist
|
||||
launchctl unload ~/Library/LaunchAgents/ru.aepif.eva-agent.plist
|
||||
```
|
||||
|
||||
`WorkingDirectory` нужен, чтобы `agent.json` и `journal.jsonl` лежали рядом с `agent.py`.
|
||||
|
||||
## Выкатка новых версий
|
||||
|
||||
Из основного checkout репозитория (не из worktree), при поднятой версии `AGENT_VERSION` в `src/agent.py`:
|
||||
|
||||
uv run --project ../jira-hub jira-hub agent publish --module wiki src/agent.py
|
||||
|
||||
Агент подтянет её сам (`auto_update`); `min_agent_version` хаба поднимать после того, как `agent_status` покажет новую.
|
||||
Агент подтянет её сам (`auto_update`); настройку хаба `wiki_min_agent_version` (не `min_agent_version` — та у агента
|
||||
jira) поднимать после того, как `agent_status` покажет новую версию агента wiki.
|
||||
|
||||
## Разработка
|
||||
|
||||
|
||||
@ -212,3 +212,61 @@ Issue агента — aep/eva-agent#1. Версия агента — 1. Аге
|
||||
- Хаб принял всё, что требует его спека (расхождений с хабом не найдено).
|
||||
|
||||
Обоснование: поручение владельца (тестовый хаб только из worktree wiki, временный home, не живой). Цена ошибки: при переименовании модулей/маршрутов хаба прогон упадёт на `POST /projects` или `tokens` — это сигнал, а не порча данных.
|
||||
|
||||
36. **`classify_links` не падает на нестандартном HTML и битых адресах (I-1).**
|
||||
- На Python 3.9 `_markupbase` зовёт `error()` на `<![foo]>`, `<![ ]>` и подобном, а базовый `error()` бросает
|
||||
`NotImplementedError`. `_LinkParser.error()` бросает своё `_ParseError`, а `parse_marked_section` ловит его и
|
||||
пропускает конструкцию до `>` (как «мусорный комментарий»; нет `>` — ждёт данных, при `close()` её пропускает
|
||||
`goahead`). Известные секции (`<![if …]>`, `<![endif]>`, `<![CDATA[…]]>`) разбираются как раньше.
|
||||
- Простой no-op `error()` не годится: после него `parse_marked_section` падает на несвязанной переменной `match`, а
|
||||
`_scan_name` — на распаковке `None`.
|
||||
- Запасной путь: `feed`/`close` обёрнуты в `try` (`_ParseError`, `NotImplementedError`, `AssertionError`), собранное до
|
||||
сбоя сохраняется, в лог — строка.
|
||||
- Ссылка, на которой `urljoin`/`urlsplit` бросают `ValueError` (`http://[::1`), пропускается, остальные остаются.
|
||||
|
||||
Обоснование: одна битая конструкция в HTML страницы роняла весь `wiki_sync` «внутренней ошибкой агента».
|
||||
Цена ошибки: ссылка внутри нераспознанной секции не попадёт в `attachments`/`links` (останется в HTML как есть).
|
||||
37. **`wiki_fetch`: адрес кодируется перед запросом, отказ по хосту — `ForeignHost` (I-2).**
|
||||
- Путь и запрос после `base_url` кодируются `quote(…, safe="/%:@!$&'()*+,;=-._~")` (у запроса ещё `?`); готовые
|
||||
`%XX` не кодируются повторно, одиночный `%` без двух шестнадцатеричных цифр становится `%25`; фрагмент `#…`
|
||||
отбрасывается.
|
||||
- `download` бросает `ForeignHost("чужой хост")` (другие схема, хост, порт, userinfo, адрес не под `base_url`) или
|
||||
`ForeignHost("некорректный адрес")` (`urlsplit` не разбирает: `http://[::1`, порт не числом). `do_wiki_fetch`
|
||||
ловит только `ForeignHost`; прочие `ValueError` (в том числе `UnicodeEncodeError`) уходят в «внутреннюю ошибку
|
||||
агента», а не маскируются под «чужой хост».
|
||||
- `url` вложения в `classify_links` не перекодируется: это ключ склейки вложений в хабе, менять его форму — вне MVP.
|
||||
|
||||
Обоснование: имена файлов в Eva с пробелами и кириллицей; без кодирования `http.client` отказывал (`InvalidURL`
|
||||
повторялся как обрыв связи, `UnicodeEncodeError` выдавался за «чужой хост»).
|
||||
Цена ошибки: если Eva ждёт путь в другой кодировке (не UTF-8), файл не найдётся (404 «файл не найден в Eva»).
|
||||
38. **`EvaError` в команде — `failed` «Eva: <текст>» без повтора (M-1).** Текст уже очищен от токена (решение 23).
|
||||
Обоснование: ошибка JSON-RPC (нет прав, неверный фильтр) — ответ Eva, а не сбой агента; повтор не поможет.
|
||||
Цена ошибки: временная ошибка Eva, пришедшая в `error`, не повторится автоматически — повторяет пользователь.
|
||||
39. **413 хаба на пачку страниц — ошибка с кодом страницы или границами пачки (M-3).** «хаб: HTTP 413: страница DOC-x
|
||||
больше 16 МБ — хаб не принял: <ответ хаба>» для пачки из одной страницы; «пачка страниц DOC-a…DOC-b (N шт.) больше
|
||||
16 МБ …» иначе. Не повторяется. Пачка из нескольких страниц больше 16 МБ невозможна (пачка закрывается на 4 МБ), но
|
||||
текст на этот случай есть.
|
||||
Обоснование: по «HTTP 413» без кода не понять, какую страницу Eva чистить. Цена ошибки: нет.
|
||||
40. **`list_docs` останавливается, если ответ страницы не приносит новых записей (M-4).** Записи склеиваются по
|
||||
JSON-представлению (`sort_keys`); полная страница без новых записей — останов с записью в лог, собранное
|
||||
возвращается. Так ловятся оба случая «Eva игнорирует `slice`»: весь список каждый раз и одна и та же первая страница.
|
||||
Обоснование: иначе бесконечный цикл вызовов Eva. Цена ошибки: если Eva игнорирует `slice` и отдаёт только первые
|
||||
N записей, дерево будет неполным без явного отказа (видно в логе).
|
||||
41. **`Executor(eva, hub)`: удалены неиспользуемые `Executor.s` (настройки) и `Executor.sleep` (M-7).** Настройки,
|
||||
нужные исполнителю (`wiki_pacing`), ставит `Runner.load_settings` прямо в `EvaClient.pacing`.
|
||||
Цена ошибки: нет.
|
||||
42. **Перенаправление Eva на SSO (M-5) не чинится до живой проверки.** Сейчас перенаправление на другой адрес
|
||||
отклоняется (`RedirectAway` → «перенаправление … отклонено»), страница входа с кодом 200 даёт «ответ не JSON».
|
||||
Как именно Eva банка отвечает на протухший токен, неизвестно; чинить вслепую — угадывать формат. В README и
|
||||
ARCHITECTURE строка «проверить при первой живой синхронизации».
|
||||
Цена ошибки: при протухшем токене текст ошибки менее понятен («ответ не JSON» вместо «токен Eva недействителен»).
|
||||
43. **Документация (I-3, T6.1, T6.4, M-2, M-6).**
|
||||
- Выкатка — `jira-hub agent publish --module wiki src/agent.py` (без `--module` хаб публикует версию агенту `jira`,
|
||||
`registry.DEFAULT_AGENT`); порог — `wiki_min_agent_version` (ключ модуля wiki хаба, `agent_min_version_key`).
|
||||
- `eva.base_url` — только origin: к нему добавляются `/api/`, `/files/…`, `/project/Document/<код>`, и с ним же
|
||||
сравнивается адрес вложения. `agent.example.json` уже содержит origin без пути (`https://eva.example.ru`); JSON
|
||||
без комментариев, поэтому пояснение — в README.
|
||||
- Агент — только macOS (`fcntl` в `acquire_lock`): из README убраны `C:\…` и «планировщик задач», добавлен пример
|
||||
launchd с меткой `ru.aepif.eva-agent` по образцу jira-agent.
|
||||
|
||||
Цена ошибки: нет.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user