ИИ-агент собирает админку каталога по одной инструкции

ИИ-агент собирает админку каталога по одной инструкции

Шесть частей цикла собрали библиотечный каталог с закрытой админкой на Vue, читательскими аккаунтами, фильтрами с честными ошибками, обложками из медиатеки, двумя языками и заказом в типографию по подписанному вебхуку. Всё это существует в живом проекте, и всё это кто-то заводил: типы контента, сценарии, маршруты, шаблоны. Пришло время рассказать, кто именно заводил их в демо и почему следующую фичу вы можете не заводить руками вовсе.

В этой части каталогу выдачи книг оформляет ИИ-агент — тот же класс инструментов, что Claude Code, Cursor или Qwen Code. Человек пишет инструкцию на одну страницу: какой тип контента нужен, какие эндпоинты, какая панель в интерфейсе. Дальше агент читает пакет документации платформы и работает командной строкой. Заводит запросы, собирает сценарии, публикует маршруты, перебирает ошибки по журналу прогонов. Предыдущие части цикла: сборка закрытой админки, читательские аккаунты, фильтры и честные ошибки, обложки из медиатеки, два языка из одной записи, внешний сервис и вебхук.

Отдельного «встроенного агента» в платформе нет — и не нужно. Достаточно того, что весь сайт доступен через API, а правила работы с ним собраны в пакет, который понимает любой внешний инструмент.

Пакет документации, ключ, командная строка

У агента три вещи, и все три выдаёт платформа.

Пакет dynapi-site-dev — инструкции в формате скилла: когда и как заводить типы, из чего собирается сценарий, чем читается сайт, где ловятся ошибки. Живёт по адресу dynapi.ru/cms/agent-skill/, оттуда его читает человек, скачивает архивом agent-skill.zip или кладёт в свой редактор кода как справочник проекта. Внутри — восемь документов: рабочие процессы, справочник GraphQL API, каталог действий и условий сценария, правила Liquid, механика member-аккаунтов. Тем же пакетом пользуется документация для людей: в разделе про ИИ-агентов те же правила изложены по-человечески.

Ключ проекта — dca_…, создаётся в админке в разделе «Ключи доступа». Для сборки сайта хватает областей site:manage, data:write и media:write; ключ сам определяет, какому проекту принадлежит запрос. Ключ не коммитится и не вставляется в тексты сценариев — он живёт в переменной окружения DYNAPI_APIKEY.

flowctl — командная строка, скачивается из того же пакета (agent-skill/cli/flowctl-linux-amd64, есть сборки под Windows и macOS). Адрес GraphQL-эндпоинта в неё зашит, поэтому настройка — это передать ключ. Внутри — entity для типов контента и записей, query для библиотеки запросов, validate и dryrun для проверки сценариев до публикации, apply для импорта, route для карты маршрутов, files push для шаблонов и статики, runs для журнала прогонов, media и secret для библиотеки и секретов.

Инструкция на одну страницу

Качество сборки определяется качеством инструкции. Хороший документ описывает результат, а не клики: «тип такой-то с такими-то полями, эндпоинты с такими контрактами, панель с таким поведением». Остальное — правила работы и явный список «за кадром», чтобы агент не раздувал объём самодеятельностью.

Инструкция для выдач книг выглядит так, и дальше по тексту агент исполняет её буквально:

# Задача для агента: выдачи книг в каталоге

Проект — каталог небольшой библиотеки на `biblio.dynapi.ru`.
Работаешь по скиллу `dynapi-site-dev`; инструмент — `flowctl`, ключ в
переменной `DYNAPI_APIKEY`. Ничего не изобретай: контракты действий и
условий — в справочниках скилла, и всё опубликованное проверяй живьём.

## Что нужно построить

Читатели берут книги на руки. Добавь каталогу выдачи: один тип контента,
четыре сценария-эндпоинта и раздел «Выдачи» в админке каталога.

### 1. Тип «Выдача» (loan)

| поле     | тип                                          | правила      |
|----------|----------------------------------------------|--------------|
| book     | запись → Книга                               | обязательное |
| member   | запись → Читатель                            | обязательное |
| due_date | дата                                         | обязательное |
| status   | выбор: active «На руках» / returned «Возвращена» | обязательное |

Схему заведи в редакторе схемы или мутацией `createEntity`
(api-reference §4). Прежде чем писать сценарии, дождись пересборки схемы:
`flowctl entity schema loan` должен показать все четыре поля.

### 2. GET /api/members — читатели для селекта

members-only. JSON: `{"members":[{"id","name","email"}…]}`, по алфавиту имён.
Запрос сохрани в библиотеку запросов под именем `members_list`.

### 3. GET /api/loans — активные выдачи

members-only. JSON: `{"loans":[{"id","due_date","book":{title},"member":{email}}…]}`.
Только `status = active`, по возрастанию `due_date`. Запрос — в библиотеку
под именем `loans_list`.

### 4. POST /api/loans/issue — выдать книгу

members-only. JSON-тело: `{"book_id", "member_id", "due_date"}`.
Проверки по очереди, у каждой свой ответ 400 с именем поля:
`book_id` и `member_id` — UUID, `due_date` — дата `ГГГГ-ММ-ДД`.
Успех: `createLoan` со `status: active` → 201 `{"loan":{…}}`.
Если записи с таким id нет — 400 `{"error":"REF_NOT_FOUND"}`.
Мутацию можно оставить инлайн: библиотека — про чтение страниц,
не про разовые действия.

### 5. POST /api/loans/return — принять книгу обратно

members-only. JSON-тело: `{"loan_id"}`; UUID-проверка → 400 с именем поля.
`updateLoan(status: returned)`. Выдачи с таким id нет →
404 `{"error":"NOT_FOUND"}`, успех → 200 `{"ok":true}`.

### 6. Раздел «Выдачи» в админке (spa/)

Под каталогом — панель «Выдачи»: таблица активных выдач (книга, читатель,
«вернуть до», кнопка «Вернул»). Сама выдача — из строки каталога: кнопка
«Выдать» раскрывает в строке селект читателя и дату возврата. Ошибки — как в
форме книг: баннер с сообщением сервера и подсветка поля. После выдачи и
возврата перечитывай список. Обе локали словаря (RU и EN). Пересобери бандл,
запушь статику (`flowctl files push`) и подними `?v=` в `admin.liquid`.

## Правила работы

- Цепочка на каждый сценарий: `validate` → `dryrun` (пустое тело, мусор в
  полях, счастливый путь) → `apply` вместе с маршрутом → живая проверка →
  `runs`. Dry-run мокает graphql-действие — честный ответ сценария
  проверяется только живым вызовом и журналом прогонов.
- Маршруты members-only: JSON-клиент без сессии получает 403, а не редирект.
- Ключ не попадает в файлы репозитория и в тексты сценариев.
- Секретов в этой задаче нет; если появятся — только `secret://`, не инлайн.

## За кадром (не делаешь)

- Одна книга — одна активная выдача: проверку не вводим.
- Уведомления читателям: в демо нет SMTP.
- История возвратов: выданные и возвращённые показываем только активные.

Инструкция для агента в рендере: заголовок «Задача для агента: выдачи книг в каталоге», таблица полей типа loan, ниже разделы с контрактами эндпоинтов, правилами работы и списком «за кадром»

В документе нет ни одного документа сценария и ни одной строчки Liquid. Только контракты, имена запросов в библиотеке, коды ошибок и список того, что НЕ делать.

Тип, сценарии, репетиция

Первый шаг — схема. Тип «Выдача» появляется в редакторе типов: две ссылки-записи, дата и список с двумя вариантами.

Схема типа Loan: поля book, member, due_date с пометкой «Обязательно» и раскрытый редактор поля status с вариантами active «На руках» и returned «Возвращена»

Инструкция разрешает и второй путь — мутацию createEntity с рецептом из api-reference: для агента разница только в том, куда нажимать. flowctl entity schema loan дожидается пересборки схемы и печатает все четыре поля.

Дальше — сценарии. Каждый проходит цепочку «собрал → проверил → опубликовал». Оффлайн-проверка validate ловит синтаксис Liquid и инварианты графа. Репетиция dryrun исполняет несохранённый документ против мок-запроса. Пустое тело, мусор в полях, счастливый путь — по три-четыре прогона на сценарий, каждый ответ виден до публикации. Для выдачи книги цепочка условий выглядит так:

{
  "id": "c_book",
  "type": "condition",
  "data": {
    "predicate": {
      "combinator": "and",
      "clauses": [
        { "field": "json.book_id", "operator": "matches",
          "value": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" }
      ]
    }
  }
}

Три такие условия по очереди проверяют book_id, member_id и формат даты, каждый провал ведёт в свой ответ 400 с именем поля. За ними graphql-узел создаёт выдачу со status: "active", финальное условие смотрит в results и выбирает между 201 и REF_NOT_FOUND. Публикация — flowctl apply; маршрут остаётся черновиком, поэтому следом идёт flowctl route save --publish. Канва сценария после сборки.

Редактор сценария loans_issue: граф из условия начала, трёх условий-проверок с ветками ДА/НЕТ к JSON-ответам 400, graphql-узла createLoan и финального условия с ветками к ответу {“loan”: …} и к REF_NOT_FOUND

Чтение — members_list и loans_list — уезжает в библиотеку запросов, как требует скилл: страница читает данные по имени записи библиотеки, и правка запроса потом делается в одном месте. Разовые мутации агент оставил инлайн — инструкция это явно разрешила.

Живой прогон ловит то, что репетиция мокает

Первый живой клик по «Выдать» вернул ошибку. Репетиция была зелёной по всем веткам, но dry-run не исполняет graphql-узел по-настоящему — он подкладывает ответ из мок-контекста. Журнал прогонов показал точную причину:

graphql: graphql errors: Argument "input" has invalid value
{book: $book, member: $member, dueDate: $due, status: "active"}.
In field "dueDate": Unknown field. In field "due_date": Expected "String!", found null.;
Cannot query field "dueDate" on type "Loan". Did you mean "due_date"?

Агент назвал поле в camelCase — dueDate, а схема отдаёт поле под слагом как есть — due_date. Правка коснулась текста запроса в узле, той же строки в записи loans_list и выборки поля в ответе. Перепубликация, живой прогон — и выдача оформилась.

Вкладка «Запуски» сценария loans_issue: сверху зелёные прогоны с пометкой ОК, ниже раскрыт упавший прогон с красным шагом graphql и жёлтой диагностикой field_not_resolved: results.created.createLoan.id

Виновата слепая вера в зелёную репетицию, агенты ни при чём. Инструкция потому и требует живую проверку после публикации: dry-run отвечает на вопрос «верно ли расставлены ветки», журнал прогонов — «что реально сказал GraphQL».

Проверка человеком

Агент закончил — слово за человеком. В админке у книги появилась кнопка «Выдать», раскрывающая строку с селектом читателя и датой возврата:

Каталог книг с открытой строкой выдачи у «Маленького принца»: селект с Анной · biblio@dynapi.ru, дата 01.10.2026, кнопки «Выдать» и «Отмена», у книг пилюли статусов, у части книг обложки из медиатеки

Под каталогом — панель активных выдач. «Мастер и Маргарита» ушла читателю до 24 сентября, кнопка «Вернул» закрывает выдачу:

Панель «Выдачи: 1» под каталогом: строка «Мастер и Маргарита», читатель reader@biblio.test, вернуть до 2026-09-24, красная кнопка «Вернул»

Живая проверка прошла по всем веткам, не только по счастливой: выдача без читателя вернула 400 с подсветкой селекта, возврат убрал строку из панели, JSON-запрос без сессии получил 403 AUTHENTICATION_REQUIRED вместо редиректа. Каталог из статьи живой — biblio.dynapi.ru, читатель reader@biblio.test с паролем chitalka-2026, выдачи можно попробовать самому.

Что у агента отобрано

Платформа сознательно обходится без встроенного агента. Агент вне платформы — значит, он работает с теми же правами, что вы ему выдали, и ни одним больше. Ключ с тремя областями не умеет менять тариф, удалять проект и читать чужие проекты. Ключ отзывается одной кнопкой в том же разделе — сессия агента закончилась, доступ закончился.

Решения остаются за человеком, и инструкция это фиксирует. Что строить и зачем — написано человеком. Что НЕ строить — перечислено в «за кадром». Секреты агенту не выдают в тексте задачи — для них в платформе одностороннее хранилище из прошлой части, и в этой задаче их не было вовсе.

Собранный агентом код проходит обычную ревью-дорожку: всё, что он сделал, лежит в ветке step-10-loans репозитория демо — тип, четыре сценария, записи библиотеки, обновлённое Vue-приложение и та самая INSTRUCTION-LOANS.md. Читается как обычный пул-реквест.

На этом цикл замкнут. Семь частей описали один путь: типы контента превращаются в GraphQL-схему, сценарии — в страницы и API, сайт живёт в одном origin, а сборку можно доверить и человеку, и агенту. Обе руки — ваши.

Что осталось за кадром

flowctl entity create умеет наполнять типы записями — сиды каталога в репозитории залиты именно им. Пакет скилла обновляется вместе с платформой, и страницы agent-skill собираются из того же репозитория, что и код, и попадают в sitemap как документация.

Для задач побольше инструкция разрастается в план с этапами и проверками — механика та же: результат, контракты, правила, «за кадром». История с due_date — лучший кандидат на добавку в справочник скилла: поля в GraphQL называются ровно как слаги, и camelCase там не живёт.


Все записи