Перейти к содержимому
Версия 1.0 ещё не выпущена; проект находится на стадии pre-MVP. См. возможности и ограничения версии 1.0.

API управления CMS

За рамками версии 1.0. Версионируемый API для скриптов, командной строки и ИИ-агентов, управляющих контентом, моделями и настройками одного сайта. Он использует те же сервисы жизненного цикла, механизм применения моделей и обработчики, что и админка. Его следует отличать от CRUD-API сгенерированного бэкенда. Модель можно изменить напрямую с правом schema:write или предложить изменение человеку для применения в разделе Конструктор контента.

  • Машиночитаемый контракт: openapi-v1.json (OpenAPI 3.1).
  • Типы TypeScript: backend/src/lib/management/client-v1.generated.ts.
  • Клиент командной строки и MCP-сервер — файл dee-wan-manage.mjs, доступный для скачивания в разделе Настройки → API.
  • Удалённый MCP: POST /api/manage/mcp, те же операции по HTTPS без скачивания.
  • Все три генерируются из backend/src/lib/management/contract.ts командой npm run docs:management-api. npm run check:management-api завершается ошибкой, если любой из них устарел.

Создайте их в разделе Настройки → API → Учётные данные для управления. Нужно право site:settings на сайте.

  • Задайте имя, области доступа и, при желании, модели, которых они могут касаться.
  • Можно предоставить только те права, которыми вы сейчас обладаете. При выборе недоступных вам областей или моделей запрос отклоняется с 422 grant_not_held и их перечнем. В форме их нельзя выбрать.
  • Секрет показывается один раз. Хранится только его дайджест SHA-256. Скопируйте его до закрытия панели.
  • Формат: dwm_ и 43 символа base64url.
  • Отзыв выполняется на том же экране. Отозванный секрет перестаёт работать со следующего запроса.
  • Счётчик использования и время последнего использования не хранятся.

Учётные данные действуют от имени создавшего их человека. На каждом запросе API заново читает текущие права этого человека на сайте и оставляет только те, которые также разрешены учётными данными:

  • области доступа ограничивают, какие команды можно вызывать;
  • модели (если выбраны) ограничивают, какие модели контента можно читать или изменять;
  • роль создателя ограничивает всё остальное, включая правила владения update:own, право, которое требует каждый переход рабочего процесса, и ограничения роли по полям и языкам (Люди и доступ): списки и чтение записей не включают языки и поля, которые роль не может читать, а попытка записи за пределами прав роли возвращает field_denied или locale_denied.

Если создатель деактивирован или потерял доступ к сайту, учётные данные отвечают creator_unauthorized. Если создатель теряет право, учётные данные теряют его в тот же момент. Статус суперадминистратора у создателя не снимает ограничений: учётные данные действуют только в пределах заданных областей и моделей.

Область Разрешает Требуется от создателя
content:read Чтение сайта, моделей, записей, версий, зависимостей, предложений, страниц, медиа. Всегда включена. read на модели
content:write Создание и изменение черновиков, добавление языка, восстановление старой версии, изменение связанных записей (relate, related from). create / update на модели; для восстановления версии ещё content:restore
content:propose Предложение изменений полей на проверку редактору. read на модели
pages:write Сохранение в редакторе страниц. update на модели страниц
media:write Загрузка медиа, изменение альтернативного текста, точки фокуса, режима загрузки и приоритета получения файла, удаление неиспользуемых файлов, а также создание, переименование, перемещение и удаление папок медиатеки и перемещение файлов между ними. media:upload для загрузки, media:update для правки и папок, media:delete для удаления
lifecycle:publish Публикация и снятие с публикации без второго подтверждения человеком. update и право перехода
content:trash Перемещение записей или отдельных языков в корзину и восстановление. delete на модели
content:purge Окончательное удаление элемента корзины. delete на модели
workflow:move Шаги рабочего процесса, которые не публикуют и не снимают с публикации (отправка на проверку). update и право шага
workflow:assign Назначение следующего ответственного за запись; список доступных исполнителей. update на модели
settings:write Чтение и изменение настроек сайта: хранение версий и согласование, поведение при отсутствии перевода, медиафайлы, поиск, значения по умолчанию для Site Chrome, SEO, robots, перенаправления и меню. site:settings; для поиска вместо него — site:infra
schema:propose Предложение новой модели или изменения модели на проверку человеку; список и чтение этих предложений. model:apply
schema:write Прямое создание, изменение, замена и удаление моделей. model:apply; для удаления ещё model:delete
data:import Импорт от начала до конца: источники, задания, загрузка, выполнение шагов, сопоставления, пауза, продолжение, отмена, откат, квитанции и файлы проблем. Каждое действие по-прежнему проверяется по собственным правам создателя. data:import; для самих действий нужно ещё то же, что и для импорта в админке (запись моделей и записей, media:upload, model:apply, site:settings, публикация)
data:export Создание экспортов, их сборка и чтение диапазонами байтов. data:export
roles:write Создание, изменение и удаление ролей сайта, включая ограничения по полям и языкам. Учётные данные выдают только то, что есть у них самих. role:manage
plugins:use Список установленных плагинов, одноразовые ссылки, которые открывают раздел плагина или действие над записью, и выполнение объявленных операций плагина. То же, что нужно для такого запуска или выполнения в админке: read на записи (update для разрушающей операции) или выданная плагину модель, которую создатель читает, включая только свои записи; операции на уровне сайта нужно site:plugins

/roles перечисляет роли сайта с их полными наборами прав: models, system и необязательные limits по полям и языкам (Люди и доступ). Операции записи проходят те же проверки, что и в разделе Настройки → Роли: неизвестные права дают 400 invalid_permissions со всеми причинами, а набор сверх учётных данных — 403 escalation_refused. Верхнюю границу задают ограниченные права учётных данных, а не все права роли их создателя: учётные данные без content:write не могут выдать update, а без lifecycle:publish — content:publish. Подстановка всех моделей ("*") всегда выходит за пределы прав учётных данных. PUT /roles/{id} заменяет только названные поля; permissions заменяет весь набор. Нельзя удалить предустановленную роль, роль, назначенную пользователю или указанную в правиле Cloudflare Access, а также последнюю роль, позволяющую выдавать доступ.

  • Базовый путь: /api/manage/v1.
  • Отправляйте Authorization: Bearer <secret>. Сайт определяется учётными данными. Запрос с X-Site-Id или с указанием базы данных отклоняется.
  • В JSON-теле допустим только заданный набор ключей; неизвестный ключ даёт invalid_request.
  • Каждая операция записи требует Idempotency-Key: 1–128 видимых символов ASCII (0x21–0x7E, без пробелов). Исключения не принимают ключ, и их ячейка «Ключ» в таблице ниже пуста: запуски плагинов, проверка перенаправлений (она ничего не записывает) и четыре маршрута редактирования под /entries/{id}/editing. При повторе используйте тот же ключ и тело; первый результат возвращается с replayed: true (страницы: created: false). Ключ, использованный для другого запроса, даёт idempotency_key_reused. Ключи публикации, снятия с публикации и шагов процесса хранятся 30 дней, остальные — 24 часа. Ключи корзины, восстановления, окончательного удаления, назначения и нового языка хранят точный первый ответ, поэтому повтор окончательного удаления возвращает его, хотя записи уже нет.
  • Команда занимает свой ключ до того, как что-либо изменить. Второй запрос с тем же ключом и телом, пока первый ещё выполняется, ничего не меняет и даёт 409 idempotency_in_progress с Retry-After; повторите его с тем же ключом и телом, чтобы получить первый ответ. Отклонённая или неудавшаяся команда освобождает свой ключ. Этому правилу следуют правки метаданных медиа, удаления медиа, записи настроек, предложения и каждый шаг импорта и экспорта; при повторе возвращается исходный ответ, а не результат нового чтения.
  • Само по себе истечение времени не делает повтор безопасным. Если запрос, занявший ключ, перестал отвечать, дальнейшее поведение при повторе с этим ключом определяется подтверждённым результатом команды:
    • Записывается одним пакетом вместе с ответом: загрузка, правка и удаление медиа, папки медиа, записи моделей и меню, настройки Site Chrome и SEO, корзина, восстановление, окончательное удаление, назначение, новый язык, создание импорта, экспорта, источника импорта или роли, удаление роли и импорт элемента из источника медиа. Если ключ занят, но ответ не сохранён, значит, запись не состоялась и при повторе команда выполняется заново. Если запись прошла, а потерялся только ответ, повтор отвечает по тому, что было записано, с replayed: true. Файл, который сохранила остановившаяся загрузка, удаляется.
    • Результат хранится в отдельной записи либо операция допускает безопасный повтор: сохранения записей, публикация, снятие с публикации, перемещение, предложения, сохранения страниц, настройки, операции плагинов, части импорта, обновления ролей, настройки комментариев, модерация и удаления. Повтор выполняет команду снова. Если повтор выполняется после успешной модерации или удаления, возвращается conflict (для удаления — not_found) с текущим состоянием комментария.
    • Шаги задания: шаги экспорта и импорта, решения на проверке, пауза, отмена и завершение загрузки. При резервировании ключа фиксируется состояние задания до шага. Если задание с тех пор перешло дальше, при повторе возвращается его текущее состояние с replayed: true без выполнения команд. Если состояние не изменилось, шаг экспорта, решение на проверке, пауза, отмена и завершение загрузки выполняются снова: каждый из них — сравнение с заменой по этой позиции или установка целевого состояния. advanceImport, resumeImport, advanceImportUndo и retryImportUndo выполняются снова, только если первый запрос завершился ошибкой; если он перестал отвечать (тайм-аут, обрыв соединения), ключ остаётся зарезервированным в состоянии ожидания.
  • Если ключ остался в состоянии ожидания, возвращается 409 idempotency_in_progress без Retry-After: простое ожидание не решит проблему. Проверьте результат (для импорта — getImportJob) и при необходимости отправьте команду с новым ключом. Резервирование истекает вместе с ключом через 24 часа.
  • При загрузке принимаются разрешённые на сайте виды и форматы файлов (Настройки → Сайт → Медиафайлы): изображения, документы, аудио, видео и другие файлы для скачивания. Та же проверка выполняется для админки, этого API, плагинов, импорта и копий сайта. Тип проверяется по содержимому файла. Если содержимое, расширение или заявленный тип указывают на программу, скрипт или разметку, файл отклоняется (executable_file или unsupported_media_type для разметки). Каждый объект медиа содержит kind: image, document, audio, video или file. width и height изображения берутся из его собственного заголовка; поля width и height используются, только если заголовок их не сообщает.
  • edited_from загружает байты как изменённую копию файла медиатеки (например, обрезку, сделанную в другом инструменте). Оригинал сохраняется. В ответе для копии есть edited_from. Копия наследует теги оригинала, а также его альтернативный текст и папку, если они не заданы в запросе. Неизвестный id — 404 media_edit_source_not_found.
  • При загрузке через этот API весь файл хранится в памяти, поэтому размер ограничен 32 МиБ независимо от лимита для его вида. Админка потоково принимает большие документы, аудио, видео и прочие файлы до limits.streamed_max_bytes: 100 МБ или большего лимита тарифа, который развёртывание объявляет в MEDIA_REQUEST_BODY_LIMIT_BYTES (не больше 5 ГБ; см. лимиты Workers).
  • Загрузка медиа привязывает ключ к байтам, имени и типу файла и к полям alt_texts, width, height, aspectRatio, on_duplicate, folder_id и edited_from, а не к границе multipart. Ключ фиксируется в том же пакете, что и строка медиа, поэтому точный повтор возвращает записанный результат.
  • Если сайт уже хранит те же байты, решает on_duplicate. ask (по умолчанию) отвечает 409 media_duplicate с существующим файлом в existing и числом копий в copies и ничего не сохраняет и не записывает. use_existing отвечает 200 с существующим файлом и reused: true; повтор воспроизводит его как 200. copy загружает отдельную копию. Файлы другого сайта никогда не совпадают.
  • Медиатека общая для сайта: учётные данные, ограниченные частью моделей, всё равно видят и загружают все медиа.
  • Каждый объект медиа содержит folder_id или null для Unfiled. Загрузка может указать folder_id; повторно использованный дубликат остаётся на месте. Папки логические: ни сохранённый файл, ни адрес никогда не перемещаются.
  • /media/folders возвращает всё дерево в порядке обхода в глубину, каждая папка со своими revision, depth, собственным media_count, subtree_folder_count и subtree_media_count, плюс unfiled, total и лимиты: 5 уровней, 500 папок на сайт, имена от 1 до 80 символов, уникальные внутри родителя без учёта регистра. При переименовании и перемещении передавайте expected_revision; при удалении — expected_revision, expected_folders и expected_media в том виде, в каком они были прочитаны. Устаревшее значение даёт 409 media_folder_changed с текущей folder. Другие причины отклонения: media_folder_exists (с folder, у которой уже есть это имя), media_folder_cycle, media_folder_too_deep, media_folder_limit, media_folder_not_found. Удаление папки удаляет все папки под ней и ни одного файла: каждый файл становится Unfiled. /media/move помещает от 1 до 200 файлов в одну папку или в Unfiled с folder_id: null.
  • Каждый объект медиа также содержит default_focal ({ x, y } в долях или null), loading и fetch_priority. GET /media/{id} возвращает один объект той же структуры, что и элементы списка.
  • GET /media/{id}/usage отвечает, где используется файл, так же, как это показывает медиатека: used_by (не больше 50 записей, которые эти учётные данные могут читать), hidden (ссылки в контенте, который им читать нельзя: они считаются, но никогда не описываются), at_least, total (null, если подсчёт ограничен или не читается), more и unreadable.
  • PUT /media/{id} меняет то, что редактирует медиатека: alt_texts (код языка → текст; язык, которого нет в запросе, сохраняет свой текст, "" очищает его), default_focal (только для изображений, null очищает; иначе 400 media_framing_not_image), loading (eager, lazy) и fetch_priority (high, low, auto). Отправьте хотя бы одно поле. Сам файл, его размеры и адрес здесь никогда не меняются.
  • POST /media/{id}/replace принимает multipart с полем file и ключом идемпотентности. Новый файл должен быть того же вида. Идентификатор, ссылки из контента, папка, теги, авторство и кадрирование сохраняются. Неуказанные alt_texts, default_focal, loading и fetchPriority остаются прежними. Имя, размер, тип и размеры изображения обновляются по проверенному файлу. Адрес доставки может измениться — используйте url из ответа. Опубликованные версии, закрепившие прежний объект, сохраняют его байты, пока на объект ссылается хотя бы одна версия. Заменить файл, связанный с внешним источником, нельзя. Действуют те же проверки загрузки и атомарная замена, что и в медиатеке. Повторяйте запрос с тем же Idempotency-Key, файлом и полями. Операция TypeScript — replaceMedia, команда CLI — replace-media, инструмент MCP — replace_media; в удалённом MCP передайте id, content_base64 и file_name.
  • GET /media/{id}/originals возвращает текущие оригиналы по языкам (content:read): id объекта, lang, url, MIME-тип, размер в байтах и размеры изображения. У приватных файлов url равен null.
  • PUT /media/{id}/originals/{lang} принимает multipart с file и expected_object_id: пустая строка создаёт первый оригинал для языка, иначе передайте id текущего объекта. DELETE по тому же пути принимает JSON с непустым expected_object_id. Обоим запросам нужны media:write и Idempotency-Key; устаревший id даёт 409 media_changed. Меняется только указанный язык; id медиа, ссылки и остальные оригиналы сохраняются. Если оригинала для языка нет, доставка использует языковую цепочку сайта, затем основной оригинал. Опубликованные версии сохраняют закреплённые за ними байты. POST /media/{id}/replace по-прежнему заменяет основной оригинал.
  • TypeScript: listMediaOriginals, replaceMediaOriginal, removeMediaOriginal. CLI: list-media-originals, replace-media-original, remove-media-original. MCP: list_media_originals, replace_media_original, remove_media_original.
  • POST /media/{id}/remove удаляет файл, на который ничто не ссылается. Пока ссылки есть, он отвечает 409 media_in_use с теми же полями использования; media_usage_unreadable — если использование не удалось прочитать. Тогда ничего не удаляется. Успешный ответ содержит storage_cleanup: complete или pending, если запись удалена, но сохранённую копию пока не удалось убрать. Для MCP-клиентов эта операция помечена как удаляющая данные.
  • GET /media/sources перечисляет внешние источники медиа, которые активные плагины предлагают сайту (плагин с правом media:source). GET /media/sources/{installationId}/{sourceId}/items отвечает одной страницей источника, не больше 24 элементов, с поиском по q; передайте next_cursor обратно как cursor для следующей страницы. У каждого элемента есть заголовок, имя файла, тип, размер, thumbnail_url на origin плагина (загружайте без referrer) и attribution. Каждый источник перечисляет свои modes: copy, reference или оба. Просмотр ничего не сохраняет.
  • POST /media/sources/{installationId}/{sourceId}/import с item_id и mode добавляет один элемент в медиатеку. mode обязателен и равен одному из modes источника; Dee Wan никогда не выбирает его автоматически. С copy Dee Wan получает файл из плагина и проверяет его, как любую загрузку: виды, форматы и лимиты размера сайта, и новый файл больше никогда не зависит от источника. С reference Dee Wan ничего не загружает и сохраняет URL файла у плагина и идентификаторы элемента: объект медиа содержит external: true, а url — адрес файла у плагина; его байты нельзя заменить или отредактировать (media_external_bytes_unavailable, 409), а удаление убирает только строку медиатеки. folder_id и on_duplicate работают как при загрузке (для reference дубликат — тот же плагин, источник и элемент); use_existing отвечает существующим файлом с 200. Ответ для нового файла содержит attribution (имя источника, автор, лицензия, ссылки и id плагина, источника и элемента). Каждый объект медиа содержит attribution; у файлов, добавленных другим способом, это null. Отказы: media_source_unavailable (404), media_source_mode_unsupported (422, источник не объявляет этот mode), plugin_refused (422, с refusal плагина), plugin_unreachable, plugin_response_invalid и media_source_file_unreachable (502), plugin_timeout (504) и отказы загрузки, например media_format_not_enabled, media_kind_not_enabled и media_too_large. Аудит записывает это как загрузку медиа с указанием источника.
  • Ограничение частоты: 120 запросов в минуту на учётные данные. Сверх него: 429 с Retry-After.
Метод Путь Область Ключ
GET /site content:read
GET /models content:read
GET /entries?model_id=… content:read
POST /entries content:write обязателен
GET /entries/{id} content:read
PUT /entries/{id} content:write обязателен
GET /entries/{id}/versions content:read
GET /entries/{id}/versions/{versionId} content:read
GET /entries/{id}/dependents content:read
POST /entries/{id}/publish lifecycle:publish обязателен
POST /entries/{id}/unpublish lifecycle:publish обязателен
POST /entries/{id}/proposals content:propose обязателен
GET /entries/{id}/proposals content:read
GET /entries/{id}/editing?locale=…&kind=… content:read
POST /entries/{id}/editing/takeover content:write
PUT /entries/{id}/editing/draft content:write
POST /entries/{id}/editing/draft/discard content:write
GET /pages/{id} content:read
POST /pages/{id}/save pages:write обязателен
POST /pages/{id}/translations pages:write обязателен
GET /media content:read
POST /media (multipart, часть file) media:write обязателен
GET /media/folders content:read
POST /media/folders media:write обязателен
PUT /media/folders/{id} media:write обязателен
POST /media/folders/{id}/remove media:write обязателен
POST /media/move media:write обязателен
GET /media/sources media:write
GET /media/sources/{installationId}/{sourceId}/items media:write
POST /media/sources/{installationId}/{sourceId}/import media:write обязателен
GET /media/{id} content:read
GET /media/{id}/usage content:read
PUT /media/{id} media:write обязателен
POST /media/{id}/replace (multipart file) media:write обязателен
GET /media/{id}/originals content:read
PUT /media/{id}/originals/{lang} (multipart file, expected_object_id) media:write обязателен
DELETE /media/{id}/originals/{lang} media:write обязателен
POST /media/{id}/remove media:write обязателен
GET /languages content:read
POST /entries/{id}/locales content:write обязателен
POST /entries/{id}/versions/{versionId}/restore content:write обязателен
POST /entries/{id}/transition workflow:move обязателен
GET /entries/{id}/assignees workflow:assign
PUT /entries/{id}/assignment workflow:assign обязателен
GET /trash?model_id=… content:read
GET /entries/{id}/trash content:read
POST /entries/{id}/trash content:trash обязателен
POST /entries/{id}/trash/restore content:trash обязателен
POST /entries/{id}/trash/purge content:purge обязателен
POST /entries/relate content:write обязателен
GET /entries/{id}/related-from content:read
PUT /entries/{id}/related-from content:write обязателен
GET /menus settings:write
GET /menus/{key} settings:write
PUT /menus/{key} settings:write обязателен
POST /menus/{key}/languages settings:write обязателен
GET /models/{id} content:read
GET /models/proposals?state=… schema:propose
POST /models/proposals schema:propose обязателен
GET /models/proposals/{id} schema:propose
POST /models schema:write обязателен
PUT /models/{id} schema:write обязателен
PUT /models/{id}/folder schema:write обязателен
PUT /models/{id}/definition schema:write обязателен
POST /models/{id}/remove schema:write обязателен
GET /settings/editorial settings:write
PUT /settings/editorial settings:write обязателен
GET /settings/language-fallback settings:write
PUT /settings/language-fallback settings:write обязателен
GET /settings/media-files settings:write
PUT /settings/media-files settings:write обязателен
GET /settings/search settings:write
PUT /settings/search settings:write обязателен
GET /settings/chrome settings:write
PUT /settings/chrome settings:write обязателен
GET /settings/seo settings:write
POST /settings/seo/redirect-review settings:write
PUT /settings/seo settings:write обязателен
GET /imports/sources data:import
POST /imports/sources data:import обязателен
GET /imports?limit=… data:import
POST /imports data:import обязателен
GET /imports/{id} data:import
PUT /imports/{id}/archive/parts/{part} (binary) data:import обязателен
POST /imports/{id}/archive/complete data:import обязателен
POST /imports/{id}/advance?recheck=… data:import обязателен
GET /imports/{id}/findings data:import
GET /imports/{id}/mapping data:import
PUT /imports/{id}/mapping data:import обязателен
GET /imports/{id}/wordpress data:import
PUT /imports/{id}/wordpress data:import обязателен
GET /imports/{id}/assignments data:import
PUT /imports/{id}/assignments data:import обязателен
GET /imports/{id}/principals data:import
PUT /imports/{id}/principals data:import обязателен
POST /imports/{id}/principals/accept-suggestions data:import обязателен
POST /imports/{id}/pause data:import обязателен
POST /imports/{id}/resume data:import обязателен
POST /imports/{id}/cancel data:import обязателен
POST /imports/{id}/undo data:import обязателен
POST /imports/{id}/undo/retry data:import обязателен
GET /imports/{id}/artifacts/{name} data:import
POST /exports data:export обязателен
GET /exports/{id} data:export
POST /exports/{id}/advance data:export обязателен
GET /exports/{id}/artifact?offset=…&length=… data:export
GET /roles content:read
GET /roles/{id} content:read
POST /roles roles:write обязателен
PUT /roles/{id} roles:write обязателен
POST /roles/{id}/remove roles:write обязателен
GET /plugins plugins:use
GET /plugins/manage-views plugins:use
GET /plugins/content-actions?instance_id=… plugins:use
POST /plugins/{id}/manage-views/{view}/launch plugins:use
POST /plugins/{id}/content-actions/{action}/entries/{instanceId}/launch plugins:use
GET /plugins/operations?instance_id=… plugins:use
POST /plugins/{id}/operations/{operation}/invoke plugins:use обязателен
GET /rich-blocks content:read

Четыре маршрута редактирования позволяют читать и менять временную блокировку редактирования и общий черновик одной записи на одном языке (см. Блокировка редактирования и Общий черновик в разделе Конфликты). Они не принимают Idempotency-Key и не пишутся в историю аудита. Они проверяют владение записями и доступные клиенту для чтения поля и возвращают только поля, которые учётные данные могут читать.

Чтение и запись — отдельные права. Создатель, который может изменить запись, но не может читать эту строку или этот язык (если ему разрешено читать только свои записи, а эта запись принадлежит другому пользователю, или если ему запрещено читать на этом языке), всё равно может перехватить, записать и отменить черновик. Ни один ответ не содержит черновика: GET отклоняется (not_owner или locale_denied), перехват отвечает draft: null, 409 draft_conflict отвечает current: null, а ответ на сохранение черновика содержит только данные, которые отправил сам создатель. Такой создатель не может узнать ревизию черновика: отмените его без revision, затем пишите с expected_revision: 0. Создатель без права чтения модели вообще получает 403 на каждом маршруте редактирования, и ничего не меняется.

  • GET /entries/{id}/editing: параметры запроса locale и kind (content или page).
  • POST /entries/{id}/editing/takeover: тело { locale }.
  • PUT /entries/{id}/editing/draft: тело { locale, kind, baseline_version_id, expected_revision, data }. baseline_version_id — версия, от которой начат черновик, или null; data — значения полей черновика.
  • POST /entries/{id}/editing/draft/discard: тело { locale, kind } и необязательный revision, чтобы отбросить только эту ревизию.

Отказы на этих маршрутах:

  • 403 not_owner: создатель может читать или менять только свои записи и не владеет строкой записи на этом языке (или самой записью, если строки на этом языке ещё нет).
  • 403 locale_denied с locale: для чтения нужно право чтения на этом языке. Для перехвата, записи и отмены черновика нужно право изменения на нём или право создания, если строки записи на этом языке ещё нет.
  • 403 field_denied с fields: ["slides"]: черновик page для создателя, который не может читать секции страницы. Создатель, который не может читать название страницы, получает title и baseline.title как ""; при записи сохраняется текущее название страницы.
  • 423 locked с holder и recovery: другой пользователь редактирует запись на этом языке. Блокировка повторно проверяется внутри операции сохранения или отмены. Если за это время редактирование перехватили, операция тоже отклоняется и ничего не меняется. Отклонённая запись сохраняется как собственный черновик восстановления создателя (recovery). Отклонённая отмена ничего не сохраняет: recovery: null.
  • 409 draft_conflict с current: ревизия устарела. current и любой другой ответ с черновиком содержат только поля, которые создатель может читать. current равен null, если черновика нет или если создатель не может читать эту строку или этот язык.

/entries перечисляет основные записи групп переводов одной модели: model_id (обязательно), published, after, limit (1–50). Если создатель может читать только свои записи этой модели, ответ — listing_owner_restricted; читайте такие записи по id.

POST /entries и PUT /entries/{id} принимают списки связей в relations (поле → список). Поле связи с разными связями в каждом языке принимает их вместо этого в locale_relations (язык → поле → список); см. Разные связи в каждом языке. Связывание и Ссылаются сюда меняют список того языка, id записи которого назван.

Поле связи также принимает { connect, disconnect, move }. Операции выполняются в порядке: disconnect, затем connect, затем move. Элемент connect — это id (добавляется в конец) или { id, position }; move — это [{ id, position }] для уже связанных элементов. position — ровно одно из { before: id }, { after: id }, { start: true }, { end: true }; якорь — это id из списка на этом шаге. Отказы (422): relation_position_anchor_missing, relation_write_invalid, relation_write_duplicate (id одновременно в connect и move, одновременно в disconnect и move либо дважды в move), relation_move_target_missing и relation_position_not_ordered (позиция или move у поля с одной целью). Позиции определяются по списку, хранящемуся на момент выполнения операции; expected_current_version_id по-прежнему отклоняет устаревшее сохранение. Везде, где запись связи принимает id, она принимает и { model, id } со слагом модели цели; см. Связь с несколькими моделями.

/trash так же перечисляет корзину одной модели (model_id, after, limit). Каждый элемент — либо целая запись (scope: entry, по её основной записи), либо один язык, перемещённый туда отдельно (scope: language), с deleted_at и purge_after (через 30 дней).

/menus читает меню принятой версии Site Chrome и revision — принятую версию, из которой они взяты (null, если принятой версии нет). Меню — это key, name и variants: код языка → собственные items этого языка. placed_in сопоставляет языку место, где меню показывается на нём. /menus/{key} добавляет languages, served (для каждого языка сайта — версия, которую он отдаёт сегодня по строгому или fallback-правилу сайта, или null) и live (для каждой версии, затем для каждого id пункта — языки, на которых он показывается сегодня). Пункт, чья страница, запись блога или архив не опубликованы, на сайте скрыт. PUT /menus/{key} создаёт или заменяет одно меню со всеми его версиями: expected_revision, name, variants и необязательные placements (код языка → header/footer; язык, которого нет в списке, сохраняет своё размещение). Пункт указывает на target: entry (model, entry = translation_group_id), blog, archive (role category/tag/author, entry), url (https), email, phone или path, либо null для заголовка с пунктами под ним. Каждому пункту нужна подпись; id пунктов уникальны в пределах одной версии. POST /menus/{key}/languages добавляет одну версию: expected_revision, language (язык сайта) и copy_from (язык для копирования, с новыми id пунктов, или null для пустой версии). Он отвечает menu_not_found 404, menu_language_exists 409, menu_language_missing или menu_language_unknown 422. Каждое сохранение — новая версия Site Chrome, принятая сразу; непринятые изменения Site Chrome переносятся поверх неё с новым меню (draft: carried) либо остаются для ручного согласования (draft: needs_rebase). Устаревший expected_revision даёт 409 stale_document с revision.

Те же операции импорта и экспорта, что и в разделе Настройки → Перенос данных, но без админки. Каждый шаг выполняет собственный код импорта или экспорта админки с теми же отказами; здесь нет второго движка импорта.

Импорт. POST /imports с format (tar, tar.gz, json, transfer, wxr), новым source (label, system) или существующим source_id и, если нужно, destination. На сайте одновременно идёт только один импорт (409 import_already_active). Затем:

  1. Загрузите файл частями: PUT /imports/{id}/archive/parts/{n} с Content-Type: application/octet-stream, по 5 МиБ на часть, каждая часть, кроме последней, ровно 5 МиБ, нумерация с 1. Действует ограничение размера для формата. Номер части привязывается к SHA-256 её байтов до того, как сохранён хоть один байт, — здесь и в загрузке через админку одинаково: повторная отправка тех же байтов отвечает как раньше; другие байты для привязанной части дают 409 import_part_conflict, какой бы ни был ключ, и ничего из них не сохраняется. Содержимое по URL никогда не скачивается.
  2. POST /imports/{id}/archive/complete, если нужно, с client_sha256 всего файла. Для .deewan один вызов проверяет не более 32 МиБ. Пока outcome.more равен true, вызывайте advance или повторяйте завершение с новым ключом идемпотентности. Проверка продолжается с сохранённого места по ID задания. client_sha256_matches появляется только после проверки всего файла.
  3. POST /imports/{id}/advance, пока ответ больше ничего не просит. Каждый вызов выполняет на сервере один ограниченный шаг: подготовку файла, его чтение, составление списка работ, проверку или импорт пакета. Шаг, который Worker не успевает закончить за один запрос, продолжается при следующем вызове. Весь импорт никогда не выполняется одним вызовом.
  4. Если decision указывает на необходимое решение, примите его и вызовите следующий шаг импорта: задайте сопоставление Strapi (PUT /imports/{id}/mapping), параметры WordPress (PUT /imports/{id}/wordpress), назначения истории сайта (PUT /imports/{id}/assignments) или исправьте то, что перечисляет GET /imports/{id}/findings. ?recheck=true заново выполняет завершённую проверку. Перенос истории или импорт WordPress может также сопоставить людей, которым он приписывает контент, с участниками этого сайта: GET /imports/{id}/principals перечисляет каждого человека (ref, label источника, email, uses, suggestion, choice, mapped, problem) и активных members; PUT /imports/{id}/principals с { "ref", "user_id" } сопоставляет одного (user_id: null оставляет его меткой); В файлах .deewan списки людей и назначений разбиты на страницы: передавайте полученный next_cursor без изменений и limit (1–100, по умолчанию 50). Каждая страница людей включает список активных участников для выбора. POST /imports/{id}/principals/accept-suggestions принимает предложения по точному совпадению email только для выбранной страницы: передайте те же cursor и limit, что использовали в GET, затем перейдите по его next_cursor. Для старого JSON и WordPress команда принимает сразу все предложения, по которым ещё нет решения. Уже сохранённые решения остаются. Участник должен быть активен на этом сайте (409 principal_user_ineligible); неизвестный ref — 404 principal_not_in_transfer. Ничто не создаёт и не меняет аккаунт. Проверка блокирует с principal_mapping_invalid, если сопоставленный участник потерял доступ, и запуск проверяет снова перед записью. См. Импорт и миграция.

Более поздний импорт из того же источника создаёт новое и пропускает уже импортированное; он никогда не меняет контент, записанный более ранним импортом, а сохранение нового сопоставления ничего уже импортированного не меняет. Единственное исключение — меню WordPress, которое осталось точно таким, каким его записал последний импорт: в него добавляются пункты, которые теперь сопоставляются (Импорт и миграция). 5. После успешной актуальной проверки следующий вызов advance запускает импорт: вызов для проверенного задания служит разрешением на запуск.

Каждый ответ импорта, кроме артефактов, — это { "data": state }:

  • job: id, status, stage (upload, prepare, review, check, import, undo, done), format, source, plan_revision, upload (state, parts_received, bytes_received, part_size), progress, dry_run_state, undo_state, final_status, error, отметки времени.
  • findings: количество результатов проверки с уровнями blocking, warning и info.
  • decision: null или { kind, message }, где kind — одно из upload_needed, mapping_required, wordpress_mapping_required, findings_block, paused, blocked, cancel_pending, undo_refused, undo_failed.
  • more: следующий вызов advance сейчас может выполнить ещё один шаг.
  • next_actions: операции, доступные сейчас.
  • step и outcome: что выполнил этот запрос, или null.

pause останавливает импорт после пакета, который уже выполняется; resume продолжает приостановленное задание; cancel сохраняет уже импортированное и записывает квитанцию отмены (decision.kind: cancel_pending, пока очистка файлов не завершена). POST /imports/{id}/undo откатывает один пакет за вызов и является разрушающей операцией; undo/retry возвращает отклонённые единицы в очередь там, где это разрешает админка. GET /imports/{id}/findings?limit=&cursor= читает не больше 500 результатов проверки (по умолчанию 100), сначала самые серьёзные, затем в порядке обнаружения; передайте next_cursor как cursor, чтобы читать дальше (null после последней). GET /imports/{id}/mapping, GET /imports/{id}/wordpress, GET /imports/{id}/assignments и GET /imports/{id}/principals принимают те же limit (не больше 500, по умолчанию 200) и cursor. Их длинные списки содержат одну страницу в порядке админки: types Strapi, normalized.skipped_fields, каждый список normalized.group_keys и review.unresolved; posts, pages и entries WordPress; assignments истории; principals и members. totals считает каждый список целиком на его месте, а каждое другое поле, включая mapping, приходит целиком на каждой странице. GET /imports/{id}/artifacts/{name}?offset=&length= отдаёт диапазон байтов receipt, problems (problem-rows.csv), entries, media (манифесты), partition или plugin_data (файл данных плагинов WordPress, см. Импорт и миграция) — те же байты, что скачивает админка, — не больше 4 МиБ за запрос, как application/octet-stream с заголовками X-Artifact-*, описанными ниже; X-Artifact-Content-Type называет собственный тип файла. Отсутствующее задание и задание другого сайта отвечают одинаково: 404 job_not_found.

Экспорт. Вызовите POST /exports с product (bundle, content, csv, xlsx, history) и необязательными models и locale. Затем вызывайте POST /exports/{id}/advance, пока more не станет false. Копии сайта и перенос с историей используют возобновляемый формат .deewan, а данные записей — JSON Lines. Каждый вызов сохраняет следующую неизменяемую часть или продолжает сборку файла. Сборке тоже может потребоваться несколько вызовов. Готовый artifact содержит размер файла и полный SHA-256; только после этого отдельные части удаляются. В pages перечислены контрольные суммы, размеры, курсоры и признак readable для каждой собранной части. Для CSV и XLSX действуют отдельные ограничения.

GET /exports/{id}/artifact?offset=&length= читает завершённый файл, не больше 4 МиБ за запрос; до завершения это 409 export_not_final. Добавьте page=<n>, чтобы прочитать одну хранимую страницу, пока экспорт идёт; страница, удалённая после завершения, — 409 export_page_gone. Каждое чтение отвечает application/octet-stream с X-Artifact-Offset, X-Artifact-Length, X-Artifact-Total, X-Artifact-Sha256 и X-Artifact-Content-Type всего файла, X-Artifact-Next-Offset (в его конце отсутствует) и, для страницы, X-Artifact-Page и X-Artifact-Next-Page (в конце страницы, когда следующая страница собрана). Хранимый файл, не совпадающий с записанным, — 409 export_artifact_changed; чтение за концом — 400 artifact_range_invalid. Два одновременных продвижения никогда не теряют и не удваивают страницу: второе отвечает экспортом в том виде, в каком его оставило первое. Истёкшие и неудачные экспорты отклоняются, как в админке.

Для каждой операции записи нужен Idempotency-Key. В истории сохраняются операция, задание, формат, фаза и результат, но не содержимое файлов, значения контента, сопоставления, тексты результатов проверки или скачанные байты.

settings:write читает и меняет по одному типизированному разделу для каждой группы настроек. Каждая операция записи указывает прочитанную ревизию; если она устарела, ничего не меняется. Каждая операция использует тот же валидатор и формат хранения, что и соответствующий экран настроек, и записывает management.settings.update рядом с собственной записью этого экрана. Создатель должен иметь site:settings сейчас; для поиска вместо этого нужно site:infra.

Раздел Чтение Запись Ревизия
Хранение версий и проверка versions_to_keep, review_flow одно или оба expected_revisions.versions_to_keep, .review_flow
Отсутствующий язык policy (strict или fallback_to_default), chains, languages policy, chains (заменяется целиком) expected_revisions.policy, .chains
Медиафайлы kinds (у каждого enabled, max_bytes, formats), limits (min_bytes, buffered_max_bytes, streamed_max_bytes, kind_max_bytes) kinds (заменяется целиком; изображения остаются включены; для вида без formats используются значения по умолчанию) expected_revision
Поиск enabled, models, weights, modes, что можно добавить или настроить enabled, models, необязательные weights, необязательные modes (word, english_stem или substring для модели; без значения — word; english_stem на языке, отличном от английского, ищет целые слова) expected_revision
Значения по умолчанию Site Chrome footer.note по языкам, routes footer, routes expected_revision (принятая версия Site Chrome или null)
SEO значения по умолчанию seo, sitemap, robots, redirects любые из них expected_revision (принятая версия Site Chrome или null)
  • Устаревшая ревизия настройки даёт 409 settings_changed_concurrently с field. Изменение, которое называет настройку без её ревизии, даёт 400 invalid_request.
  • Хранение версий и проверка, отсутствующий язык, медиафайлы и поиск сохраняют свой ключ вместе с настройкой, одной записью. Повтор с тем же ключом и телом получает первый ответ, даже если ревизия уже изменилась. Запись, которая сохранена, но не завершена, отвечает 503 temporary_failure; повтор завершает её и воспроизводит ответ.
  • Валидаторы возвращают именованные причины, например invalid_versions_to_keep, review_flow_items_in_progress (записи ждут проверки; они перечислены), fallback_language_unknown, fallback_cycle, invalid_media_files, site_search_model_invalid, site_search_weights_invalid и site_search_mode_invalid.
  • Значения по умолчанию Site Chrome и SEO сохраняются как новая принятая версия Site Chrome, как меню: непринятые изменения переносятся (draft), а устаревшая ревизия даёт 409 stale_document.
  • Изображение для соцсетей или логотип указываются через og_image_media_id / logo_media_id из медиатеки этого сайта; сервер записывает их адрес и размер.
  • Перенаправления заменяются целиком. Точный источник, который является опубликованным адресом, зарезервированным адресом блога или именованным маршрутом, отклоняется. Изменение шаблонного правила требует redirect_review_digest: отправьте правила в /settings/seo/redirect-review, прочитайте, какие опубликованные страницы покрывает каждый шаблон, и отправьте его digest. Без него ответ — 409 redirect_review_stale с обзором.
  • Здесь недоступны секреты, пароли, способы входа, провайдеры удостоверений, токены плагинов, развёртывание, провайдеры исходного кода, домены и другая инфраструктура.

Для операций с комментариями используется маршрут комментариев админки с учётом ограничений учётных данных, поэтому действуют те же правила, отказы и ответы о конфликтах (см. Комментарии посетителей). Каждая операция записи требует Idempotency-Key и записывается в историю как management.comment.moderate, management.comment.delete или management.comment.settings: числа и целевое состояние, но никогда текст или email комментария.

Операция Область Что делает
GET /comments/settings (getCommentSettings) settings:write Каждая модель и то, принимают ли её записи комментарии.
PUT /comments/settings (updateCommentSettings) settings:write { model, enabled }: открывает или закрывает одну модель. Закрытие скрывает комментарии; ничего не удаляется.
GET /comments (listComments) lifecycle:publish Сначала новые, по 50 на страницу; state (по умолчанию pending), model, entry, lang, cursor. Без email.
GET /comments/{id} (getComment) lifecycle:publish Один комментарий с его приватным email, родителем и записью.
POST /comments/moderate (moderateComments) lifecycle:publish { to, comments: [{ id, expected }] }, не больше 25, каждый — только если он ещё в прочитанном состоянии.
POST /comments/publish-imported (publishImportedComment) lifecycle:publish Показывает один комментарий, импортированный без согласия. Согласие остаётся false.
POST /comments/remove (deleteComments) content:purge Удаляет не больше 25 комментариев с их ответами и реакциями. Также нужны lifecycle:publish и право удаления в модели каждого комментария.
  • Если состояние комментария изменилось после чтения, возвращается conflict с текущим состоянием. Попытка одобрить комментарий, импортированный без согласия, возвращает consent_missing.
  • Реакции предназначены для посетителей; менять их через API управления нельзя. Комментарий в списке несёт свои счётчики реакций.

Каждая операция записи указывает состояние, на основе которого она подготовлена. Если оно устарело, операция отклоняется без изменений.

  • Изменение записи: expected_current_version_id. Устаревшее: 409 version_conflict с current_version_id. Создание или изменение называет адреса в slugs, по одному на язык ({ "fr": "bonjour" }); язык, которого нет в списке, сохраняет свой адрес. Отклонённый адрес называет field: "slug" и свой locale. Создание на языке, которого нет на сайте, отклоняется 400 unknown_locale с lang. Если язык удалят во время создания, ответ — 409 language_changed, и ничего не записывается.
  • Публикация: to, expected_workflow_revision, expected_current_version_id. Более новый черновик никогда не публикуется незаметно: 409 scheduled_target_stale. Изменившееся состояние рабочего процесса: 409 workflow_conflict с revision.
  • Снятие с публикации: to, expected_workflow_revision, expected_published_version_id и acknowledged_dependents, равный ids из /entries/{id}/dependents, если на запись ссылается другой контент.
  • Один язык: добавьте locale к публикации или снятию с публикации, чтобы переместить только этот язык. to проверяется по собственному состоянию языка в workflow.locales, а снятие с публикации закрепляет собственную опубликованную версию этого языка. Без locale перемещаются все языки.
  • Сохранение страницы: expected_current_version_id и expected_facts_digest из GET /pages/{id}. При сохранении указывается один язык (locale), и только он меняется; у каждого языка свои секции.
  • Создание языковой версии страницы: locale (существующий язык записи без документа страницы или язык сайта, ещё не добавленный к странице), source (другой язык страницы для копирования или null для пустой) и expected_current_version_id. Копия сохраняет порядок, оформление, значения и ссылки на общие секции под новыми id секций. 409 translation_exists, 422 translation_source_unavailable.
  • Предложение: base_version_id должен быть текущей версией записи.
  • Блокировка редактирования: пока другой пользователь держит запись на этом языке открытой в редакторе, сохранение, переход, назначение, восстановление и перемещение в корзину для этой записи на этом языке отклоняются с 423 locked и holder. GET /entries/{id}/editing?locale=&kind= возвращает владельца блокировки и общий черновик (kind: content для редактора записи, page для компоновщика страниц); POST /entries/{id}/editing/takeover сразу снимает эту блокировку (редактор сохраняет свою работу как черновик восстановления), и после этого запись никем не заблокирована. Ваш собственный открытый редактор никогда не блокирует ваши учётные данные. Блокировка с истёкшим сроком не мешает работе.
  • Общий черновик: PUT /entries/{id}/editing/draft принимает kind и expected_revision (0, если черновика нет); устаревший: 409 draft_conflict с current. POST /entries/{id}/editing/draft/discard удаляет его. Черновик никогда не является версией; обновление записи очищает его.
  • Шаг процесса: to и expected_workflow_revision, необязательные note и locale. Шаг, который опубликовал бы или снял бы с публикации, даёт 422 transition_kind_mismatch: для него используйте публикацию или снятие с публикации. Шаг без locale между двумя неопубликованными этапами оставляет уже опубликованный язык там, где он есть; снимите этот язык с публикации отдельно. Шаг снимает назначение, как и в админке.
  • Назначение: expected_workflow_revision, assignee_id (человек из /assignees или null, чтобы снять) и необязательный due_date (YYYY-MM-DD, сегодня или позже). Устаревшее: 409 workflow_conflict с revision. Человек, который не может действовать с записью: 422 assignee_cannot_act.
  • Восстановление версии: expected_current_version_id. Старая версия становится новым текущим черновиком; опубликованная версия не меняется. Устаревшее: 409 version_conflict. Необязательный locale восстанавливает только этот язык: его поля, собственные списки связей и адрес берутся из версии; общие поля, общие связи, медиа и все другие языки остаются как есть. Язык, которого нет в версии или записи: 422 restore_locale_missing. Нужно право restore для этого языка.
  • Новый язык: lang из /languages, необязательный slug (null — без адреса; без slug — адрес основного языка, с суффиксом, если на этом языке он уже занят). Язык начинается пустым; следующее изменение записи заполняет его. Уже есть: 409 locale_exists с его instance_id.
  • Корзина: scope: entry перемещает все языки; scope: language — один язык, кроме основного (owner_language для основного). Опубликованный контент отклоняется с 409 content_live и live_languages: сначала снимите его с публикации. Восстановленная запись — черновик.
  • Восстановление из корзины: GET /entries/{id}/trash называет conflicts (блокируют) и references (ссылки, которые не будут работать). Восстановление отклоняется с 409 restore_conflict, пока адрес занят другим элементом.
  • Окончательное удаление: confirm повторяет id записи из пути. Удаляет элемент, его историю и комментарии посетителей, отменить нельзя. GET /entries/{id}/trash называет число удаляемых комментариев в comments, ответ — число удалённых в comments_deleted.
  • Связывание: ids (1–25 записей), field (поле связи многие-ко-многим, которое есть у всех них), connect и/или disconnect (всего 1–10 связанных записей, ни одной в обоих) и expected_versions, называющий текущую версию каждой записи. Каждая запись — отдельное сохранение черновика и получает свой результат: done, unchanged (уже как просили; ничего не записано), covered (это сделал другой язык той же записи; для поля с разными связями в каждом языке первый выбранный язык меняет собственные списки всех выбранных языков одним сохранением) или refused с reason, например version_conflict или batch_relation_exclusive для поля один-ко-многим. Отказ ничего другого не останавливает. Ничего не публикуется.
  • Ссылаются сюда: GET /entries/{id}/related-from перечисляет все поля связи любого вида (один-к-одному, многие-к-одному, один-ко-многим, многие-ко-многим), цель которых — модель этой записи, как groups по модели и полю источника с relation_type. У каждого источника есть отображаемый label, draft / live, version_id, can_change и readable; у источника, который учётные данные не могут читать, нет slug и label. PUT принимает field_id, source_id, action (add или remove) и expected_source_version_id. Обоим нужен read на этой записи; изменению нужен ещё update на источнике. Это собственное сохранение черновика источника: действуют его права, правила полей, кратность, правило одного владельца, правило иерархии и проверка версии. Оно никогда не публикует. Устаревшее: 409 version_conflict. Источник «к одному», который уже указывает на другую запись, отклоняется с 409 related_from_replace_unconfirmed и называет current и next (instance_id, slug, label; имена, которые учётные данные не могут читать, равны null). Чтобы подтвердить именно эту замену, передайте replace_target_id, равный current.instance_id. Ответ — outcome (saved или unchanged), новый version_id источника и обновлённый related_from.

GET /entries/{id} возвращает state рабочего процесса, revision и transitions, доступные этим учётным данным: kind: publish | unpublish с lifecycle:publish, kind: move с workflow:move. to должен быть одним из них; публикация и снятие с публикации отклоняют шаг другого вида с transition_kind_mismatch, так же поступает и шаг процесса.

Интеграция, которой не следует писать напрямую, может вместо этого предложить изменения полей:

{
"base_version_id": "<current version id>",
"fields": { "title": "A clearer title" },
"note": "Why"
}
  • До 50 полей модели со значениями, не более 64 КиБ значений, примечание не длиннее 2000 символов. Поля связей и медиа предлагать нельзя (proposal_field_unsupported).
  • Ничего не меняется, пока человек не применит предложение. Редактор записи показывает каждое открытое предложение над полями, а предложенное значение — рядом с каждым названным полем, с кнопками Применить и Отклонить.
  • Применить — обычное сохранение от имени применяющего человека с привязкой к base_version_id. Действуют его проверки, права, правила владения и рабочий процесс. Если запись изменилась после создания предложения, операция Применить отклоняется с proposal_stale и ничего не записывает.
  • Редактор отключает Применить, пока есть несохранённые правки. Отклонить оставляет запись как есть.
  • source предложения называет его автора: { "kind": "management", "id", "label" } для учётных данных или { "kind": "plugin", "id", "label", "requested_by" } для установки плагина, действующей от имени человека (docs/plugins/protocol-v1.md). Редактор показывает предложение плагина как Proposed by <plugin> · requested by <person>. Список предложений записи также включает { "kind": "ai", "id", "label" }, где id обозначает пакетное задание ИИ. Каждое предложение содержит revision; необязательное original_fields содержит доступные пользователю исходные значения предложенных полей.

schema:write меняет модели напрямую, без предварительного рассмотрения предложения человеком. Каждая операция записи использует механизм применения или удаления из раздела Конструктор контента: те же проверки, отказы при несовместимых изменениях, правила связей, перенос прав и аудит.

  1. GET /models/{id} возвращает полное definition и revision.
  2. Передавайте эту revision при записи:
  • POST /models с { "definition": { "model": …, "fields": [ … ] } } создаёт модель. Без id полей.

  • PUT /models/{id} с expected_revision и model (name, slug, description, settings, enableI18n) меняет модель и сохраняет её поля.

  • PUT /models/{id}/folder с { "folder": "Blog" } (от 1 до 40 символов или null) помещает модель в папку навигации по контенту или убирает её оттуда. Это только отображение: без expected_revision, revision модели не меняется, а listModels показывает папку как folder. Ответ — model_id, slug и folder.

  • PUT /models/{id}/definition с expected_revision и полным definition заменяет его: оставьте поле по его id, добавьте поле без id, уберите поле, чтобы удалить его.

  • POST /models/{id}/remove с expected_revision и confirm (снова id модели) удаляет модель без контента.

  • Ответ — model_id, slug, created_field_ids и renamed_from. После переименования или удаления dependents.complete говорит, перенесены ли или удалены все права ролей и области токенов со старым slug; при повторе его нет.

  • revision меняется при любом изменении модели или её полей. Устаревшая даёт 409 model_changed и ничего не записывает.

  • Изменение, небезопасное для существующего контента, отклоняется с 409 model_change_blocked и changes (kind, field, affected_count, message, suggestion). Ничего не переопределяется: сначала перенесите или очистите контент.

  • Остальные отказы сохраняют причины отказа из раздела Конструктор контента, например duplicate_field_key, field_not_found, unique_unsupported, model_name_collision, stale_role_grant, stale_token_scope, model_not_empty и model_has_bound_queries.

  • Создатель должен иметь model:apply сейчас, а для удаления ещё model:delete; без этого ответ — 403 missing_permission.

Вместо изменения модели агент может предложить новую модель или изменённое определение; человек проверяет его в разделе Конструктор контента и применяет обычным способом: Сохранить, проверка несовместимых изменений, применение.

  1. GET /models/{id} возвращает полное definition модели (model и fields, у каждого поля есть id), updated_at и revision.
  2. POST /models/proposals с изменённым определением:
{
"mode": "update",
"model_id": "<model id>",
"base_updated_at": "<updated_at from getModel>",
"definition": {
"model": { "name": "Article", "slug": "article", "enableI18n": true },
"fields": ["…"]
},
"note": "Why"
}
  • definition полное: сохраняйте каждое поле по его id, добавляйте поле без него, не включайте поле, чтобы удалить его. Порядок полей — порядок массива. config, derived, ui и validation — текст JSON, как их хранит Конструктор контента.
  • Новая модель — { "mode": "create", "definition": { … }, "note": … }, без id полей.
  • Не более 200 полей и примечание не длиннее 500 символов.
  • Определение разбирается и проверяется так же, как при применении. Отклонения возвращаются с теми же причинами, например duplicate_field_key, field_not_found, unique_unsupported, model_slug_taken и model_name_collision. Если модель изменилась после getModel, ответ — 409 model_changed с current_updated_at.
  • Изменения, которые сломали бы существующий контент, разрешены и перечислены в changes (kind, field, affected_count, message, suggestion). Перед применением человек видит ту же проверку ещё раз.
  • Создатель должен иметь model:apply сейчас; без него ответ — 403 missing_permission.
  • Раздел Конструктор контента → Модели показывает открытые предложения с кнопками Проверить и Отклонить. Проверить открывает редактор с предложенным определением; Сохранить применяет его как обычное изменение модели от имени сохраняющего человека и в той же записи отмечает предложение как applied. Сначала его можно отредактировать.
  • Предложение привязано к base_updated_at. Как только модель меняется, оно становится stale: true, его применение отклоняется с 409 schema_proposal_stale, и его можно только отклонить.
  • GET /models/proposals перечисляет предложения сайта, новые первыми (state фильтрует open, applied, dismissed). GET /models/proposals/{id} читает одно.

Агент может создать ссылку, по которой человек откроет установленный плагин. Создание ссылки не выполняет код плагина. Код выполняет объявленная операция.

  • GET /plugins перечисляет установленные плагины: state (active или disabled), launchable, а для активного плагина — его manage_views и content_actions. Они берутся из манифеста, сохранённого при установке или обновлении. GET /plugins/manage-views и GET /plugins/content-actions перечисляют те же элементы плоским списком. instance_id оставляет только действия, доступные для этой записи.
  • Действие над записью перечисляет model_ids: модели, выданные плагину, которые эти учётные данные могут читать.
  • GET /rich-blocks (content:read) перечисляет определения блоков форматированного текста каждого установленного плагина: поля, правила, примеры значений, дерево отрисовки и миграции, как <plugin id>/<name>. Записывайте блок в форматированное значение как канонический элемент figure (см. Блог → Блоки плагинов). Добавлять или менять можно только блоки активного плагина.
  • Показываются только запуски, которые учётные данные могут открыть сейчас, — и как элементы списка, и как инструменты. Разделу нужна выданная плагину модель в пределах моделей учётных данных, которую создатель читает, включая только свои записи. Действию нужна хотя бы одна модель в model_ids. Изменение прав видно уже на следующем запросе. Прямой запрос запуска также проходит все проверки.
  • POST /plugins/{id}/manage-views/{view}/launch и POST /plugins/{id}/content-actions/{action}/entries/{instanceId}/launch создают одну ссылку запуска. Ответ — 201 с outcome: launch_created, url, expires_at и single_use: true.
  • Ссылка содержит один код запуска. Он работает один раз и истекает через 60 секунд после создания. Человек открывает её в браузере, и плагин действует от имени создателя учётных данных — так же, как когда этот человек открывает плагин в админке. До этого плагин ничего не получает, и ответ никогда не сообщает, что плагин что-то сделал.
  • После открытия плагин действует в пределах предоставленного ему доступа и текущих прав создателя. Дополнительно действуют ограничения учётных данных по моделям и областям доступа. Плагину делегируются только возможности, разрешённые областями доступа: content:read даёт content:read и workflow:read; lifecycle:publish даёт workflow:publish, workflow:unpublish и workflow:locale; content:write даёт content:write, content:create и draft:write; content:trash даёт content:delete; media:write даёт media:write, media:read, media:update и media:source; content:propose даёт одноимённую. Команда, выходящая за эти пределы, отклоняется с capability_refused.
  • При каждом запуске повторно проверяются текущие права создателя, доступные учётным данным модели и предоставленные плагину права — так же, как в админке. Отключённый плагин даёт 409 installation_not_active; удалённый плагин или плагин другого сайта — 404 not_found.
  • Без Idempotency-Key: каждый вызов создаёт новую ссылку. Неиспользованные ссылки истекают.

Манифест плагина может объявить операции: небольшую форму и результат, которые Dee Wan отображает самостоятельно. Агент выполняет операцию и получает результат.

  • GET /plugins/operations перечисляет операции, которые эти учётные данные могут выполнить сейчас: scope (site или entry), destructive, input (объявленные поля), input_schema (JSON Schema для input), result (компоненты результата) и model_ids (для операции над записью — модели, на которых её можно выполнить). instance_id оставляет только операции этой записи. GET /plugins содержит тот же список для каждого плагина.
  • Операция показывается, только если плагин активен, ему предоставлены все необходимые возможности и они разрешены областями учётных данных. Для операции над записью модель должна быть доступна плагину, разрешена учётными данными и доступна создателю для чтения; для разрушающей операции нужен ещё update. Операции на уровне сайта нужно site:plugins. В этом случае плагин действует от имени создателя с доступными ему для чтения моделями, которые разрешены и плагину, и учётным данным, в пределах их областей доступа. Такие операции — releases, release-edit и release-run плагина Scheduling. Изменение разрешения или прав видно на следующем запросе.
  • POST /plugins/{id}/operations/{operation}/invoke выполняет её. Тело — { "instance_id"?: "…", "input"?: { … } }; instance_id обязателен для операции над записью и отклоняется для операции сайта. input допускает только заданные ключи: неизвестный ключ или значение вне границ поля дают 400 input_invalid с issues.
  • Ответ — 200 с outcome: completed и result.blocks: text, rows, links (только https) и json. Тот же Idempotency-Key с тем же телом возвращает первый результат с replayed: true; тот же ключ с другим телом — 409 idempotency_key_reused. Повтор сначала проверяется как новый запуск: если операция больше не доступна этим учётным данным, ответ — 404 operation_unavailable, а не старый результат.
  • Отказы: 404 operation_unavailable (неизвестна или сейчас недоступна этим учётным данным), 422 plugin_refused (собственный refusal: { code, message } плагина), 504 plugin_timeout, 502 plugin_unreachable, 502 plugin_response_invalid, 409 plugin_unavailable (плагин отключили или изменили либо сузили его права во время выполнения), 409 invocation_in_progress.
  • Плагин выполняет операцию от имени создателя учётных данных. Учётные данные он никогда не получает: ядро отправляет одноразовый код, и плагин обменивает его своим токеном установки. Выполнение пишется в аудит как plugin.operation.run с хешами значений и результата, а не с самими значениями.

Плагин может объявить политики контента: проверки, которые Dee Wan запрашивает перед сохранением, восстановлением, удалением, публикацией, снятием с публикации или переходом рабочего процесса. Обновление записи, сохранение страницы, восстановление версии, команда корзины и переход через этот API проверяются так же, как в редакторе, с caller.via: management_credential. GET /plugins перечисляет policies каждого плагина с их kind и mode.

  • Ответ содержит policy_results: сведения о политиках, выдавших предупреждение, не ответивших в режиме warn (outcome: unavailable с failure) или преобразовавших сохранённый контент (outcome: transformed, fields указывает изменённые поля). Для каждой указаны installation_id, plugin_name, policy_id, policy_label, locale, message и fields. Операция записи уже выполнена; не отправляйте её повторно. После преобразования прочитайте запись заново, чтобы получить сохранённые значения.
  • 422 plugin_policy_transform_invalid: преобразованный политикой контент нарушил правило. cause содержит code, message и detail правила. Ничего не записано.
  • 422 plugin_policy_transform_conflict: две политики по-разному изменили одно поле. Ничего не записано.
  • 422 plugin_policy_refused: политика отказала. Называет plugin, policy, policy_label, locale, собственные code и message плагина и fields. Ничего не записано.
  • 503 plugin_policy_unavailable: политика в режиме блокировки не ответила. Называет политику и failure. Ничего не записано; повторите позже.
  • 429 plugin_policy_budget_exhausted: в одном запросе превышен лимит обращений к политикам. Отправляйте меньше записей за раз.

dee-wan-manage.mjs — один файл без зависимостей. Нужен Node 18 или новее. Скачайте его в разделе Настройки → API. Учётные данные — DEE_WAN_MANAGEMENT_TOKEN, если он задан, иначе сохранённый вход для этого адреса (см. Вход из терминала). Адрес CMS — --url или DEE_WAN_URL. Адрес должен использовать https (http только на localhost, 127.0.0.1 или [::1]).

Окно терминала
export DEE_WAN_MANAGEMENT_TOKEN=<credential>
node dee-wan-manage.mjs help
node dee-wan-manage.mjs list-entries --url https://cms.example --model-id <model id>
node dee-wan-manage.mjs create-entry --url https://cms.example --body-file entry.json
node dee-wan-manage.mjs upload-media --url https://cms.example --file photo.webp
node dee-wan-manage.mjs launch-plugin-manage-view --url https://cms.example --id <installation id> --view calendar
  • Каждая команда — одна операция из таблицы выше в kebab case (publishEntry → publish-entry). Режима произвольных запросов нет.
  • Операции записи всегда передают Idempotency-Key. Ключ выводится в stderr; передайте его обратно через --key <key>, чтобы безопасно повторить.
  • Код выхода: 0 — успех; 1 — запрос отклонён Dee Wan (ответ выводится); 2 — ошибка вызова команды, сети или учётных данных.
Окно терминала
node dee-wan-manage.mjs login --url https://cms.example
node dee-wan-manage.mjs status --url https://cms.example
node dee-wan-manage.mjs logout --url https://cms.example

login выполняет вход под вашей собственной учётной записью, по тому же принципу, что и при подключении приложения:

  1. Читает /.well-known/oauth-authorization-server. Издатель (issuer) должен совпадать с адресом, а эндпоинты токенов, регистрации и отзыва должны находиться на нём. PKCE S256 должен поддерживаться.
  2. Запускает слушатель только на 127.0.0.1, на порту, который выбирает система, и регистрирует клиента, единственный redirect URI которого — http://127.0.0.1:<port>/callback.
  3. Открывает /connect в браузере с PKCE S256 и случайным state. --no-open вместо этого выводит адрес. --scope "content:read content:write" запрашивает меньше областей; без него запрашиваются все области. Сайт вы выбираете на странице согласия.
  4. Callback должен быть GET /callback с тем же state, издателем в iss и ровно одним code. Другие пути отвечают 404. Отмена завершает вход с кодом выхода 1. Через 5 минут вход прекращается с кодом выхода 2.
  5. Обменивает код и сохраняет вход.

Неудачный или отменённый вход ничего не записывает. Сохранённый вход остаётся прежним.

Сохранённый вход:

  • Один на адрес, в каталоге конфигурации операционной системы: ~/Library/Application Support/dee-wan на macOS, $XDG_CONFIG_HOME/dee-wan или ~/.config/dee-wan на Linux, %APPDATA%\dee-wan на Windows. Каталог — 0700, файл — 0600 (на Windows права выставляются по возможности).
  • Хранит адрес, id клиента, сайт, области и текущий токен обновления. Никогда — токен доступа. Ни одна команда не выводит токен.
  • Каждая команда обновляет сохранённые данные входа: блокирует их для этого адреса, использует токен обновления, записывает новый во временный файл в том же каталоге, сбрасывает данные на диск и заменяет старый файл переименованием. Две одновременные команды ждут друг друга, поэтому токен обновления никогда не используется дважды. Блокировка, оставленная завершившимся аварийно процессом или старше 2 минут, снимается.
  • Когда Dee Wan больше его не принимает (отозван, истёк после 30 дней без использования или человек потерял доступ к сайту), команда завершается с кодом 2 и просит снова выполнить login. Файл сохраняется.
  • Новый login для того же адреса заменяет его и отзывает старое подключение.
  • DEE_WAN_MANAGEMENT_TOKEN всегда имеет приоритет. Он никогда не сохраняется. status сообщает, когда он перекрывает сохранённый вход.

status показывает адрес, сохранённые сайт и области, сохранён ли id клиента и задан ли DEE_WAN_MANAGEMENT_TOKEN. Если нет ни того, ни другого, он завершается с кодом 2. logout сначала отзывает подключение на эндпоинте отзыва, а затем удаляет файл. Если отзыв не удался, файл сохраняется, а команда завершается с ненулевым кодом; запустите её снова.

node dee-wan-manage.mjs mcp --url <origin> запускает MCP-сервер через stdio. Его инструменты — те же операции в snake case (publish_entry) со схемами входных данных из контракта. Как и удалённый сервер, он показывает только инструменты, разрешённые областями учётных данных (читает их из GET /site), и инструменты плагинов ниже. Инструмент записи принимает необязательный idempotency_key и возвращает использованный ключ. Вызов отвечает structuredContent { status, idempotency_key?, body } и тем же объектом в виде JSON-текста. Раздел Настройки → API показывает конфигурацию клиента для копирования:

{
"mcpServers": {
"dee-wan": {
"command": "node",
"args": [
"/path/to/dee-wan-manage.mjs",
"mcp",
"--url",
"https://cms.example"
],
"env": { "DEE_WAN_MANAGEMENT_TOKEN": "<your credential>" }
}
}
}

Без env MCP-сервер использует сохранённый вход для --url и обновляет его под той же блокировкой, пока идёт сессия.

Инструмент загрузки читает путь к локальному файлу на машине, где запущен сервер.

Импорт и экспорт из командной строки:

Окно терминала
node dee-wan-manage.mjs create-import --body '{"format":"wxr","source":{"label":"Old blog","system":"wordpress"}}'
node dee-wan-manage.mjs upload-import-part --id <job id> --file export.xml
node dee-wan-manage.mjs advance-import --id <job id> --follow
node dee-wan-manage.mjs create-export --body '{"product":"bundle"}'
node dee-wan-manage.mjs advance-export --id <export id>
node dee-wan-manage.mjs read-export-chunk --id <export id> --out site.json
  • upload-import-part --file отправляет весь файл частями по 5 МиБ, затем завершает загрузку с SHA-256 файла. Ключ каждой части берётся из её байтов, поэтому повторный запуск воспроизводит результат; изменённые байты отклоняются. --part <n> отправляет только одну часть. Инструмент stdio MCP принимает file_path и необязательный part.
  • advance-import --follow продвигает импорт шаг за шагом, по одной строке на шаг в stderr, и останавливается, если нужно решение, импорт завершён или запрос отклонён; последнее состояние печатается как JSON. Через MCP каждый вызов — один шаг.
  • read-export-chunk --out <file> читает завершённый экспорт по частям, проверяет его SHA-256 и только потом записывает файл; с --page <n> он сохраняет одну хранимую страницу. --finish сначала продвигает экспорт до завершения, каждый шаг под ключом, выведенным из экспорта и шага, поэтому повторный запуск после потерянного ответа воспроизводит результат, а не собирает дважды.
  • read-import-artifact --out <file> так же сохраняет целый артефакт импорта.
  • get-import-findings, get-import-mapping, get-word-press-review, get-history-assignments и get-import-principals --all читают все страницы в один ответ.
  • При 429 и при 409 idempotency_in_progress с Retry-After команда ждёт Retry-After и повторяет запрос с тем же ключом. Без Retry-After она останавливается и выводит ответ.

Для подключения агентов по HTTPS скачивать файл не нужно. Укажите агенту адрес, и он предложит войти под вашей учётной записью (см. Подключите приложение):

https://cms.example/api/manage/mcp

Для автоматизации вместо этого отправляйте учётные данные управления. Раздел Настройки → API показывает оба варианта:

{
"mcpServers": {
"dee-wan": {
"type": "http",
"url": "https://cms.example/api/manage/mcp",
"headers": { "Authorization": "Bearer <your credential>" }
}
}
}
  • Каждый POST принимает одно сообщение JSON-RPC и возвращает один JSON-ответ. Сервер не создаёт MCP-сессий и не использует потоковые ответы: GET и DELETE дают 405. Уведомление — 202. Пакет отклоняется.
  • Версии протокола 2025-06-18 и 2025-03-26. Другой заголовок MCP-Protocol-Version даёт 400.
  • Учётные данные читаются заново на каждом сообщении. Отозванные дают 401 на следующем. Каждый POST считается в ограничении частоты один раз.
  • tools/list показывает только инструменты, разрешённые областями учётных данных. Вызов проходит ту же проверку областей, аудит, конфликты и идемпотентность, что и HTTP API, от имени создателя учётных данных.
  • Запрос со страницы браузера с другого источника даёт 403.
  • Удалённый инструмент загрузки принимает файл как content_base64 и file_name, не больше 8 МиБ после декодирования.
  • upload_import_part принимает одну часть как content_base64 с id и part. read_export_chunk и read_import_artifact отвечают одним диапазоном байтов как { content_base64, content_type, offset, length, total, next_offset, sha256, page, next_page }, но никогда целым большим файлом; content_type и sha256 описывают весь файл, а page и next_page равны null вне страницы экспорта. get_import_findings читает одну страницу результатов проверки.
  • С plugins:use каждый раздел и каждое действие над записью активного плагина — тоже инструмент, например plugin_com_example_scheduler_view_calendar (без аргументов) или plugin_com_example_scheduler_action_schedule (instance_id). Имя — это plugin_, id плагина, view или action и его id в нижнем регистре, остальные символы заменены на _. Инструменты читаются заново на каждом сообщении: у отключённого или удалённого плагина их нет уже на следующем. Если имена двух запусков совпали, совпали с именем основного инструмента или длиннее 64 символов, соответствующие инструменты не показываются; GET /plugins называет причину в mcp_tool.omitted_because. Вызов раздела или действия только создаёт ссылку запуска, как описано выше. Адаптер stdio показывает те же инструменты.
  • Каждая операция, которую учётные данные могут выполнить, тоже инструмент, например plugin_com_example_scheduler_op_status. Его аргументы — instance_id для операции над записью, input (input_schema операции) и необязательный idempotency_key. Вызов сразу выполняет операцию и возвращает её результат как structuredContent и текст. Аннотации сохраняют destructive. Инструмента, который учётные данные больше не могут выполнить, нет уже на следующем сообщении; его вызов даёт Unknown tool. Операция automation никогда не становится инструментом.
  • Оба транспорта дают одинаковые имена инструментов, схемы, аннотации, результаты и отказы. Отличается только инструмент загрузки: здесь base64, в адаптере stdio — путь к локальному файлу.
  • Учётные данные создаёт вошедший человек в разделе Настройки → API или подключение приложения.

MCP-клиент с поддержкой OAuth выполняет вход человека под его собственной учётной записью. Ничего не устанавливается.

  1. Клиент вызывает адрес MCP без токена и получает 401 с WWW-Authenticate: Bearer error="invalid_token", resource_metadata="…".
  2. Он читает /.well-known/oauth-protected-resource/api/manage/mcp и /.well-known/oauth-authorization-server, регистрируется через POST /api/oauth/register и открывает страницу /connect в админке.
  3. Пользователь при необходимости входит, выбирает сайт и разрешённые приложению области из запрошенных и нажимает Подключить. Отмена возвращает приложению access_denied.
  4. Клиент обменивает код на POST /api/oauth/token и вызывает адрес MCP с токеном доступа.

Правила:

  • Только OAuth 2.1 authorization code с PKCE S256. Только публичные клиенты: без секрета клиента.
  • URI перенаправления должны точно совпадать с зарегистрированными. Разрешены: https; http на 127.0.0.1 или localhost с любым портом; или частная схема настольного приложения (RFC 8252 7.1), например cursor://… или com.example.app:/cb. Частная схема — в нижнем регистре, от 2 до 64 символов, и это не javascript, data, file, vbscript, blob, about, filesystem, ftp, ws, wss, mailto, tel, sms, intent, chrome, chrome-extension или view-source. Без фрагмента и данных пользователя (user info); не больше 5 URI. Страница согласия показывает, куда вернётся приложение. Ссылка с неизвестным приложением или незарегистрированным redirect URI получает ответ на самой странице и никогда не перенаправляется.
  • Коды живут 60 секунд и работают один раз. При повторном использовании выданные при первом обмене токены отзываются.
  • Токены доступа (dwo_) действуют 10 минут. Токены обновления (dwr_) действуют 30 дней и меняются при каждом использовании. Каждый токен обновления работает один раз. Предъявление любого заменённого токена, сколько бы замен ни прошло, отзывает подключение. Хранятся только дайджесты SHA-256.
  • Приложение, запросившее неизвестную серверу область, получает invalid_scope. Приложению, не запросившему областей, предлагаются все.
  • POST /api/oauth/revoke (RFC 7009) отзывает всё подключение по любому из токенов.
  • Подключение действует так же, как учётные данные, созданные пользователем с одобренными областями доступа для всех моделей. content:read всегда включён. Текущие права человека перечитываются на каждом запросе: деактивация пользователя, удаление его из участников или деактивация сайта немедленно прекращают доступ.
  • Любой участник сайта может подключить приложение от своего имени. Он может выдать только то, что есть у него самого.
  • Раздел Настройки → API → Подключённые приложения показывает подключения: все — при наличии site:settings, иначе только ваши. Отозвать останавливает подключение сразу. «последнее использование {date}» — последний обмен токенов.
  • Динамическая регистрация, выдача и отзыв токенов ограничены по частоте для каждого IP. Подключения, коды и клиенты, которыми никто не пользуется, удаляет ежедневная очистка через 30 дней.
  • В продакшене Worker отвечает на <host>/api/* и <host>/.well-known/oauth-*.

Успех — { "data": … } (список версий: { "data": [...], "next_cursor": … }). Отказ — { "error": "…", "reason": "…" } и подробности. Обрабатывайте ошибки по reason; error содержит текст для человека.

Статус Причина Значение
400 invalid_request Тело, запрос или заголовок не соответствуют v1.
400 idempotency_key_required Команде нужен Idempotency-Key.
401 credential_invalid Секрет отсутствует, неизвестен или отозван.
403 creator_unauthorized Создатель неактивен или потерял доступ к сайту либо сам сайт неактивен.
403 scope_refused У учётных данных нет области (required называет её).
403 listing_owner_restricted Создатель может читать только свои записи этой модели.
409 idempotency_in_progress Запрос с этим ключом ещё выполняется. Повторите через время из Retry-After. Без него ключ остаётся в состоянии ожидания: проверьте результат, затем используйте новый ключ.
429 rate_limited Подождите Retry-After.
503 audit_unavailable Намерение для аудита не записалось; ничего не выполнено.
503 temporary_failure Запрос не завершился. error говорит, что известно: запись настроек или публикация может быть уже сохранена, а её результат ещё не записан. Команду с ключом повторите с тем же ключом и телом.

Причины жизненного цикла и страниц передаются без изменений, например not_found, model_denied, not_owner, field_denied, locale_denied, export_limited_role, forbidden, missing_permission, version_conflict, workflow_conflict, idempotency_key_reused, scheduled_target_stale, transition_kind_mismatch, illegal_transition, dependents_not_acknowledged, invalid_save_request, facts_conflict, no_changes, content_live, plugin_policy_refused, plugin_policy_unavailable, plugin_policy_budget_exhausted, plugin_policy_transform_invalid, plugin_policy_transform_conflict, media_in_use, media_usage_unreadable, media_framing_not_image, in_trash, not_in_trash, entry_in_trash, owner_language, restore_conflict, locale_exists, unknown_locale, language_changed, locked, lock_lost, draft_conflict, assignee_cannot_act, due_date_past, batch_relation_terms_invalid, batch_relation_field_invalid, batch_expected_versions_invalid, related_from_field_invalid, related_from_source_invalid, related_from_replace_invalid и related_from_replace_unconfirmed.

field_validation_failed (422) и field_value_taken (409) также несут locale и issues: [{ field, message, key?, params? }]. field — полный путь (faqs[2].question). message — на английском. key и params указывают исходное сообщение админки и его параметры, чтобы она могла показать перевод на выбранном языке. Другие клиенты могут их игнорировать.

Каждая операция записи фиксирует намерение до выполнения и результат после него в административной истории сайта. Исключения — запуски плагинов и маршруты блокировки редактирования и общего черновика; общий черновик — это несохранённая работа, а не версия. Действующее лицо — создатель; change.management_access называет учётные данные. Операции: management.content.save, management.content.transition, management.content.move, management.content.assign, management.content.locale.add, management.content.version.restore, management.content.trash, management.content.trash.restore, management.content.purge, management.content.relate, management.content.related_from, management.menu.save, management.menu.language_add, management.page.save, management.page.translation.start, management.media.upload, management.role.create, management.role.update, management.role.delete, management.media.folder.create, management.media.folder.update, management.media.folder.delete, management.media.move, management.media.update, management.media.delete, management.import.source.create, management.import.create, management.import.upload, management.import.advance, management.import.mapping, management.import.assignment, management.import.pause, management.import.resume, management.import.cancel, management.import.undo, management.export.create, management.export.advance, management.proposal.create, management.schema_proposal.create, management.model.apply, management.model.delete, management.settings.update, а также management.credential.issue / management.credential.revoke для самих учётных данных и management.oauth.connect / management.oauth.revoke для подключённого приложения. Операции записи подключённого приложения также содержат change.via — имя приложения, поэтому история читается как «человек через приложение». Версии и события рабочего процесса называют создателя, как при правке в админке. Версия применённого предложения называет человека, который его применил. Применённое предложение схемы — это обычный model.apply от имени сохранившего его человека, а change.proposal называет предложение.

  • Изменений рабочих процессов и принудительной публикации.
  • Секретов, способов входа, провайдеров удостоверений, развёртывания, провайдеров исходного кода и доменов.
  • Администрирования плагинов и команд плагинов. Установка, разрешения, отключение и собственная работа плагина идут через админку и отдельный протокол плагинов. API управления только перечисляет плагины, создаёт ссылки запуска и выполняет объявленные операции.
  • CRUD сгенерированного бэкенда. Через него нельзя управлять CMS.
Окно терминала
curl -s https://cms.example/api/manage/v1/entries \
-H "Authorization: Bearer $DEE_WAN_MANAGEMENT_TOKEN" \
-H "Idempotency-Key: create-hello-1" \
-H "Content-Type: application/json" \
-d '{"model_id":"<model id>","locales":{"en":{"title":"Hello"}}}'

Сайт подключён. Чтобы поддерживать его работу, настройте резервное копирование и восстановление. Чтобы расширить Dee Wan, начните с создания плагина. Автономный бэкенд остаётся необязательным.


Стабильная версия ещё не выпущена (pre-MVP)
Коммит исходного кода: fe23db481c422b40e317b92fc98116b4c0c26a87 (с локальными изменениями)
Дата сборки (UTC): 2026-10-18T09:22:34.630Z
Коммит для обновления: не определён: в рабочей копии есть изменения. Примечания к выпуску