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выполняются снова, только если первый запрос завершился ошибкой; если он перестал отвечать (тайм-аут, обрыв соединения), ключ остаётся зарезервированным в состоянии ожидания.
- Записывается одним пакетом вместе с ответом: загрузка, правка и удаление медиа, папки медиа, записи моделей и меню, настройки Site Chrome и SEO, корзина, восстановление, окончательное удаление, назначение, новый язык, создание импорта, экспорта, источника импорта или роли, удаление роли и импорт элемента из источника медиа. Если ключ занят, но ответ не сохранён, значит, запись не состоялась и при повторе команда выполняется заново. Если запись прошла, а потерялся только ответ, повтор отвечает по тому, что было записано, с
- Если ключ остался в состоянии ожидания, возвращается
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 —404media_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(по умолчанию) отвечает409media_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в том виде, в каком они были прочитаны. Устаревшее значение даёт409media_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 никогда не выбирает его автоматически. СcopyDee Wan получает файл из плагина и проверяет его, как любую загрузку: виды, форматы и лимиты размера сайта, и новый файл больше никогда не зависит от источника. СreferenceDee 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). Затем:
- Загрузите файл частями:
PUT /imports/{id}/archive/parts/{n}сContent-Type: application/octet-stream, по 5 МиБ на часть, каждая часть, кроме последней, ровно 5 МиБ, нумерация с 1. Действует ограничение размера для формата. Номер части привязывается к SHA-256 её байтов до того, как сохранён хоть один байт, — здесь и в загрузке через админку одинаково: повторная отправка тех же байтов отвечает как раньше; другие байты для привязанной части дают409 import_part_conflict, какой бы ни был ключ, и ничего из них не сохраняется. Содержимое по URL никогда не скачивается. POST /imports/{id}/archive/complete, если нужно, сclient_sha256всего файла. Для.deewanодин вызов проверяет не более 32 МиБ. Покаoutcome.moreравенtrue, вызывайтеadvanceили повторяйте завершение с новым ключом идемпотентности. Проверка продолжается с сохранённого места по ID задания.client_sha256_matchesпоявляется только после проверки всего файла.POST /imports/{id}/advance, пока ответ больше ничего не просит. Каждый вызов выполняет на сервере один ограниченный шаг: подготовку файла, его чтение, составление списка работ, проверку или импорт пакета. Шаг, который Worker не успевает закончить за один запрос, продолжается при следующем вызове. Весь импорт никогда не выполняется одним вызовом.- Если
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 меняет модели напрямую, без предварительного рассмотрения предложения человеком. Каждая операция записи использует механизм применения или удаления из раздела Конструктор контента: те же проверки, отказы при несовместимых изменениях, правила связей, перенос прав и аудит.
GET /models/{id}возвращает полноеdefinitionиrevision.- Передавайте эту
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.
Предложения изменений схемы
Заголовок раздела «Предложения изменений схемы»Вместо изменения модели агент может предложить новую модель или изменённое определение; человек проверяет его в разделе Конструктор контента и применяет обычным способом: Сохранить, проверка несовместимых изменений, применение.
GET /models/{id}возвращает полноеdefinitionмодели (modelиfields, у каждого поля естьid),updated_atиrevision.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: в одном запросе превышен лимит обращений к политикам. Отправляйте меньше записей за раз.
Командная строка и MCP
Заголовок раздела «Командная строка и MCP»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 helpnode 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.jsonnode dee-wan-manage.mjs upload-media --url https://cms.example --file photo.webpnode 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.examplenode dee-wan-manage.mjs status --url https://cms.examplenode dee-wan-manage.mjs logout --url https://cms.examplelogin выполняет вход под вашей собственной учётной записью, по тому же принципу, что и при подключении приложения:
- Читает
/.well-known/oauth-authorization-server. Издатель (issuer) должен совпадать с адресом, а эндпоинты токенов, регистрации и отзыва должны находиться на нём. PKCES256должен поддерживаться. - Запускает слушатель только на
127.0.0.1, на порту, который выбирает система, и регистрирует клиента, единственный redirect URI которого —http://127.0.0.1:<port>/callback. - Открывает
/connectв браузере с PKCES256и случайнымstate.--no-openвместо этого выводит адрес.--scope "content:read content:write"запрашивает меньше областей; без него запрашиваются все области. Сайт вы выбираете на странице согласия. - Callback должен быть
GET /callbackс тем жеstate, издателем вissи ровно однимcode. Другие пути отвечают404. Отмена завершает вход с кодом выхода1. Через 5 минут вход прекращается с кодом выхода2. - Обменивает код и сохраняет вход.
Неудачный или отменённый вход ничего не записывает. Сохранённый вход остаётся прежним.
Сохранённый вход:
- Один на адрес, в каталоге конфигурации операционной системы:
~/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.xmlnode dee-wan-manage.mjs advance-import --id <job id> --follownode 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.jsonupload-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она останавливается и выводит ответ.
Удалённый MCP
Заголовок раздела «Удалённый MCP»Для подключения агентов по 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 выполняет вход человека под его собственной учётной записью. Ничего не устанавливается.
- Клиент вызывает адрес MCP без токена и получает
401сWWW-Authenticate: Bearer error="invalid_token", resource_metadata="…". - Он читает
/.well-known/oauth-protected-resource/api/manage/mcpи/.well-known/oauth-authorization-server, регистрируется черезPOST /api/oauth/registerи открывает страницу/connectв админке. - Пользователь при необходимости входит, выбирает сайт и разрешённые приложению области из запрошенных и нажимает Подключить. Отмена возвращает приложению
access_denied. - Клиент обменивает код на
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 называет предложение.
Чего нет в v1
Заголовок раздела «Чего нет в v1»- Изменений рабочих процессов и принудительной публикации.
- Секретов, способов входа, провайдеров удостоверений, развёртывания, провайдеров исходного кода и доменов.
- Администрирования плагинов и команд плагинов. Установка, разрешения, отключение и собственная работа плагина идут через админку и отдельный протокол плагинов. 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
Коммит для обновления: не определён: в рабочей копии есть изменения. Примечания к выпуску