Разработка плагинов
Плагин — это отдельно развёртываемый сервис без доступа к привязкам базы данных ядра. Серверные команды передаются по HTTPS. Проверенный встроенный интерфейс работает в изолированном фрейме, а его действия проверяет хост. Это руководство охватывает весь цикл: манифест, установку, выдачу прав, запуск, публикацию с проверкой условий и удаление.
Вам нужно место, где запустить HTTPS-сервис, и место, где хранить его состояние. Эталонный плагин (исходный код
плагина Scheduling, plugins/scheduling в репозитории Dee Wan) использует Cloudflare Worker с базой данных D1,
и этот документ исходит из такой конфигурации, но protocol v1 ничего из этого
не требует.
Каждый запрос и ответ ниже воспроизводится через настоящие маршруты в собственных тестах Dee Wan. Примеры соответствуют ответам сервера. Идентификаторы и секреты вымышлены и намеренно недействительны.
- Protocol v1 — каждый маршрут, заголовок, тело, исход и правило повтора.
- Безопасность плагинов — токены, делегирование, область действия и модель угроз.
- Тестирование плагинов — проверки контракта и локальная конфигурация из двух сервисов.
- Машиночитаемые:
schemas/manifest-v1.schema.json,schemas/protocol-v1.schema.json.
За что отвечаете вы
Заголовок раздела «За что отвечаете вы»| За что отвечаете вы | За что отвечает Dee Wan |
|---|---|
| Ваш сервис, его база данных, его часы и его повторы | Контент, версии, правила workflow, права доступа, аудит |
| Ваши собственные браузерные сессии и ваш собственный UI | Допустим ли переход и его атомарное применение |
| Когда отправлять команду | Разрешено ли её выполнить |
| Удаление ваших собственных данных | Отзыв вашего доступа |
Dee Wan никогда не доверяет утверждению плагина о том, что переход разрешён, что версия текущая или что снятие с публикации безопасно. Всё это перепроверяется внутри мутации.
1. Опубликуйте манифест
Заголовок раздела «1. Опубликуйте манифест»Манифест — это один HTTPS-документ, описывающий ваш плагин и то, что ему нужно. Dee Wan получает его в три момента и никогда — при обычных запросах:
- Проверка получает и валидирует его, показывает администратору и ничего не сохраняет.
- Установка получает его снова, снова валидирует и сохраняет эту копию. Сохранённая копия — это то, что используют все последующие проверки, поэтому при установке отдавайте тот же манифест, что и при проверке.
- Обновление снова получает его по сохранённому URL манифеста, когда администратор принимает новый релиз (см. Выпуск нового релиза).
{ "manifest_version": 1, "id": "com.example.myplugin", "name": "My Plugin", "version": "0.1.0", "protocol_versions": [1], "base_url": "https://plugin.example", "activation_url": "https://plugin.example/dee-wan/activate", "management_url": "https://plugin.example/installations/{installation_id}", "requested_capabilities": [ "content:read", "workflow:read", "workflow:publish", "workflow:unpublish" ], "content_actions": [{ "id": "schedule", "label": "Schedule" }]}Перед подготовкой манифеста ознакомьтесь с правилами ниже. Все они проверяются автоматически:
id— стабильный идентификатор в формате обратного домена: строчные буквы, начинается с буквы, минимум три метки, разделённые точками, не более 128 символов. Он никогда не меняется между релизами.version— это ваш релиз, и он меняется свободно.name— от 1 до 64 символов, без управляющих символов и без<или>.version— семантическое версионирование,MAJOR.MINOR.PATCHс необязательным суффиксом-prerelease(без суффикса+build), не более 64 символов.manifest_version,protocol_versionsиversion— три разные вещи.manifest_versionравен1. Вprotocol_versionsперечисляются от 1 до 8 поддерживаемых вами версий протокола. Хотя бы одну из них должно поддерживать ядро, иначе манифест отклоняется сprotocol_unsupported.requested_capabilitiesперечисляет хотя бы одну возможность, без дубликатов.- Каждый URL — HTTPS, не содержит учётных данных и фрагмента, и все они — плюс сам URL
манифеста — должны иметь ОДИН origin. Второй origin — это
manifest_origin_mismatch. - Неизвестные ключи верхнего уровня отклоняются. Манифест не может полагаться на поле, которое это ядро игнорирует, поэтому нельзя выпустить ключ сегодня в надежде, что его начнут учитывать позже.
- Редиректы отклоняются при получении манифеста и при активации. Отдавайте оба напрямую.
- Приватные, loopback, link-local и зарезервированные адреса назначения отклоняются. Для локальной разработки см. Тестирование плагина.
content_actionsнеобязателен и содержит не более четырёх пар{ id, label }с уникальными id.idсоответствует^[a-z][a-z0-9_-]{0,31}$.label— от 1 до 24 символов без управляющих символов и без<или>. Без разметки, без скриптов, без CSS, без URL иконки: ядро рендерит собственную кнопку в собственном стиле и только для записей моделей, к которым установке плагина предоставлен доступ.manage_viewsнеобязателен: список прямых страниц управления из закрытого словаря:calendarиreleases. Для каждой ядро показывает ссылку с собственной подписью у установки плагина в Настройки → Плагины, рядом с редакционным календарём в разделе «Обзор» и в списке «Контент». Плагин не задаёт подпись.management_urlможет содержать{installation_id}и служит только для навигации. Он никогда не получает долгоживущих учётных данных в URL.ui_extensionsнеобязателен: от 1 до 16 декларативных дополнений к экранам Dee Wan. См. Карточки и редакторы полей.
Полная схема, сгенерированная из объекта, по которому валидирует ядро, —
schemas/manifest-v1.schema.json.
Карточки и редакторы полей
Заголовок раздела «Карточки и редакторы полей»ui_extensions добавляет карточки и редакторы полей. Декларативные элементы используют компоненты ядра.
Необязательный проверенный frame открывает изолированный редактор или экран управления с origin плагина.
Фрейм получает ограниченное значение поля и MessageChannel, без сессии администратора и токена
инсталляции. Изменения поля проходят обычное сохранение; объявленные операции — проверку полномочий
и подтверждение хоста. Редактор не меняет тип хранения поля.
"ui_extensions": [ { "id": "schedule-card", "placement": "entry_sidebar", "title": "Schedule", "description": "Plan when this goes live.", "launch": { "kind": "content_action", "action": "schedule" } }, { "id": "calendar-row", "placement": "settings_row", "title": "Calendar", "launch": { "kind": "manage_view", "view": "calendar" } }, { "id": "priority", "placement": "field_editor", "control": "select", "label": "Priority", "help_text": "How soon this should go out.", "options": [ { "value": "low", "label": "Low" }, { "value": "high", "label": "High" } ] }]- У каждой записи есть
id, который соответствует^[a-z][a-z0-9_-]{0,31}$, уникален в манифесте и не меняется между релизами. На него ссылаются поля моделей.manage,core,dee-wan,workflow,history,seo,previewиsettingsзарезервированы. placement— этоentry_sidebar,page_sidebar,settings_rowилиfield_editor. Всё остальное отклоняется.- У карточки есть
title(от 1 до 40 символов), необязательныйdescription(от 1 до 160) и одинlaunch.{ "kind": "content_action", "action": "<id>" }открывает одно из вашихcontent_actionsдля элемента на экране.{ "kind": "manage_view", "view": "calendar" }открывает одно из вашихmanage_views. Запуск того, чего манифест не объявляет, отклоняется. Карточкаsettings_rowзапускает только страницу управления. Каждый запуск идёт через делегированный запуск из раздела 5. - Редактор поля рисует обычное поле.
control— этоtext,textareaилиurlдля текстового поля иnumber,boolean,dateилиselectдля поля этого типа. Сохранённое значение остаётся значением этого типа.label(от 1 до 40) называет редактор, аhelp_text(от 1 до 200) показывается под ним. - Редактор
selectперечисляет от 1 до 50options, каждая —{ "value", "label" }с уникальным значением. Редакторы text и textarea могут задатьvalidation.min_lengthиmax_length. Редактор URL может задатьmax_length, а Dee Wan добавляет своё правилоhttp(s). Редакторы чисел могут задатьminиmax. Редакторы дат могут задатьmin_dateиmax_dateв видеYYYY-MM-DD. Больше ничего не принимается, поэтому при сохранении Dee Wan проверяет каждое объявленное правило. - Редактор
groupрисует одно JSON-поле как небольшую форму. Он перечисляет от 1 до 8members, у каждого уникальныйkey(^[a-z][a-z0-9_]{0,31}$),labelиcontrol:text(с обязательнымmax_lengthдо 500),number(min,max,integer),booleanилиselect(до 12options).requiredделает поле типа text, number или select обязательным. Значение — один JSON-объект не более 8192 байт. Когда создатель сайта выбирает ваш редактор, Dee Wan записывает в поле закрытую схему. По умолчанию обязательные элементы можно оставить пустыми при сохранении черновика; при строгой проверке модели их нужно заполнять перед каждым сохранением. Неверные переданные значения, значения вне допустимых границ или вариантов, неизвестные ключи и значения, не являющиеся объектом, по-прежнему отклоняются. Перед публикацией обязательные элементы проверяются в той сохранённой версии, которая будет опубликована. - Создатель сайта выбирает ваш редактор для поля модели. Dee Wan копирует его правила и варианты в поле, поэтому поле сохраняет их, если ваш плагин исчезнет. Если инсталляция отключена или удалена либо релиз убрал редактор, поле сохраняет значение, а Dee Wan рисует свой редактор с пометкой, где назван ваш плагин.
- Текст печатный, без управляющих символов и без
<или>. Неизвестные ключи отклоняются.
Операции и политики
Заголовок раздела «Операции и политики»operations — действия, которые Dee Wan выполняет на вашем сервисе и показывает со своей формой и
своим результатом. Человек запускает операцию из раздела Настройки → Плагины (scope: "site") или из записи
(scope: "entry"). Агент запускает её через API управления или MCP. policies — проверки, которые
Dee Wan запрашивает перед сохранением, восстановлением, удалением, публикацией, снятием с публикации или
переходом workflow. Политика save или restore может также привести в порядок контент, о котором её спрашивают.
"operations": [ { "id": "schedule-status", "label": "Schedule status", "description": "Pending scheduled changes for this entry.", "endpoint": "https://plugin.example/dee-wan/operations/schedule-status", "scope": "entry", "audience": "interactive", "requires": ["content:read"], "result": ["rows"] }]audienceобязателен. Операцииinteractiveпоявляются в админке, API управления, CLI и MCP. Операцииautomationтам не появляются, и их маршруты выполнения отклоняют такие операции; вызывает одну из них только именованный процесс Dee Wan, например планировщик контента, вызывающийschedule-draftплагина Scheduling.endpointнаходится на едином origin вашего манифеста.requiresназывает возможности, которые вы тоже запрашиваете. Если хотя бы одна из них не выдана, операция не предлагается.inputперечисляет не больше 8 полей:text,number,booleanилиselect. Dee Wan рисует форму и проверяет значения.resultназывает компоненты, которые может использовать ваш ответ:text,rows,links(только https) иjson. Добавляйте"destructive": trueтолько если операция меняет или удаляет данные; тогда Dee Wan просит человека подтвердить.- Dee Wan отправляет POST с небольшим конвертом на
endpoint. Обменяйте егоcodeнаPOST /api/plugin/v1/invocations/exchangeсвоим токеном, затем ответьте на POST{ "status": "completed", "result": { "blocks": [...] } }или{ "status": "refused", "code", "message" }. Отвечайте только после обмена. Формы и лимиты — в справочнике протокола: Операции и политики. - Обмен для операции
entryтакже возвращаетdelegation_id: делегирование, привязанное к человеку, который её запустил, к вашей установке, к записи и её модели. Сохраните его, если операция создаёт отложенное задание, например расписание, и используйте его так же, как делегирование запуска. Dee Wan заново проверяет полномочия пользователя при каждом использовании. Обмен операцииsiteвозвращает делегирование, как при запускеmanage: без привязки к записи и с разрешёнными моделями, которые пользователь может читать. Операции релизов Scheduling используют это делегирование для каждой записи. Политика получаетnull.
"policies": [ { "id": "tone", "label": "Tone check", "kind": "save", "endpoint": "https://plugin.example/dee-wan/policies/tone", "requires": [] }]kind— одно из:save: каждый язык, значения, связи, медиа или настройки поиска которого меняет сохранение. Вы получаете изменённые ключи вchanged_fieldsи новые части вvalues,links(поле связи → id записей),media(медиаполе → id медиа) иseo. Общие связи и медиа приходят с основным языком. Об изменении только связей, медиа или SEO тоже спрашивают (seo— ключ настроек поиска).restore: восстановление старой версии (action: "version", те же факты, что при сохранении) или возврат записи из корзины (action: "trash"). При восстановлении версии вызываются и политикиsave.delete: запись или язык перемещены в корзину (action: "trash") или удалены окончательно (action: "purge"). Вы получаете весь черновик каждого языка.publish: полная версия каждого языка, который публикуется.unpublish: версия каждого языка, которая снимается с публикации и больше не выдаётся.transition: каждый переход между этапами работы, сtransition: { from, to }. Проверка вызывается и для релизов по расписанию, и для переходов из других плагинов;caller.viaуказывает, кто выполняет переход.
- Ответьте
{ "decision": "allow" },{ "decision": "warning", "message", "fields"? }или{ "decision": "refuse", "code", "message", "fields"? }.fields— ключи полей; редактор показывает ваше сообщение у них. Предупреждение пропускает запись; второй раз отправлять не нужно. - Политика
saveилиrestoreможет вместо этого ответить{ "decision": "transform", "message"?, "values"?, "links"?, "media"?, "seo"? }. Это заменяет указанные части записи и языка, о которых вас спросили, по ключу поля. Список заменяет весь список. Общие связи и медиа берутся только из ответа для основного языка; восстановление принимает толькоvaluesиseo. Перед сохранением Dee Wan проверяет результат по всем правилам, действующим для пользовательского ввода. Если проверка не пройдена, сохранение отклоняется сplugin_policy_transform_invalidи причиной. Если две политики по-разному меняют одно поле, сохранение отклоняется сplugin_policy_transform_conflict. Затем редактор показывает изменённые вашей политикой поля и ваше сообщение и заново загружает сохранённые значения. Повторное подтверждение не требуется.
{ "decision": "transform", "message": "Trimmed spaces and normalized the title.", "values": { "title": "Harbour tours" }}- Отказ всегда останавливает запись. Поведение при тайм-ауте, сбое или недопустимом ответе определяется настройкой сайта в разделе Настройки → Плагины: по умолчанию запись проходит с предупреждением; с Блокировать изменения без ответа она отклоняется.
- Проверка вызывается на всех путях записи: в редакторе, при сохранении страниц, восстановлении, работе с корзиной, массовых действиях, обработке электронных таблиц, импорте, через API управления и из других плагинов. Transform меняет только текущую запись; изменить этап процесса публикации он не может.
- Исключение — плановая очистка записей, у которых истёк срок хранения в корзине. Она не выполняет
действие от имени пользователя, поэтому политика не вызывается. Политика
deleteуже проверялась при перемещении записи в корзину и будет проверена снова при ручном окончательном удалении. - История версий показывает
Changed by <plugin> · <policy>у версии, которую изменил ваш transform.
Столбцы списка контента и действия над строками
Заголовок раздела «Столбцы списка контента и действия над строками»Операция page добавляет столбец в список контента модели, к которой плагину предоставлен доступ.
Операция entry с соответствующей отметкой становится действием над выбранными пользователем строками.
Обе используют описанный выше механизм вызова. Каждую ячейку и каждый результат отображает Dee Wan.
"operations": [ { "id": "schedule-column", "label": "Scheduled", "endpoint": "https://plugin.example/dee-wan/operations/schedule-column", "scope": "page", "audience": "column", "requires": ["content:read"], "result": ["cells"], "cell": "badge" }, { "id": "schedule-status", "label": "Schedule status", "endpoint": "https://plugin.example/dee-wan/operations/schedule-status", "scope": "entry", "audience": "interactive", "requires": ["content:read"], "result": ["rows"], "list_action": true }]- У столбца
scope: "page",audience: "column", один видcell(text,badgeилиlink),result: ["cells"], безinputи безdestructive. Егоlabel— заголовок столбца. - Обмен возвращает
$defs/column_request: модель и не более 50 идентификаторов строк, которые пользователь может читать. Значения не передаются; читайте нужные данные в пределах выданных прав. Ответьте одним блокомcellsне более чем с одной ячейкой на каждый идентификатор, о котором вас спросили:{ "id", "text" }плюсtoneдля метки или https-urlдля ссылки. Текст — не более 80 символов. Ячейка для другого идентификатора, повторный идентификатор, ссылка без https-urlилиurlу метки делают весь ответ недействительным. Пустой список — допустимый ответ. - Столбец не создаёт делегирования, никогда не перечисляется и не запускается как обычная операция, не пишет строку аудита и не хранит повторяемый ответ. Ничего не сохраняйте, отвечая на него.
- Действие над строками — интерактивная операция
entryсlist_action: true. Dee Wan запускает её по одному разу для каждой из не более 25 выбранных строк, у каждой свой вызов, делегирование, ключ идемпотентности и запись аудита, поэтому отвечайте на каждую как на операциюentry.
Блоки форматированного текста
Заголовок раздела «Блоки форматированного текста»rich_blocks объявляет типы блоков, которые авторы могут вставлять в форматированный текст. Вы передаёте только данные. Dee Wan
рисует поля, показывает превью и отрисовывает блок по декларативному дереву. В этом дереве нет
исполняемого скрипта или удалённых вызовов. HTML/CSS/JS поставщика хранится в отдельном блоке
snippet; его превью и публичный вывод работают в изолированном фрейме.
"rich_blocks": [ { "name": "callout", "version": 2, "label": "Callout", "description": "A boxed note", "fields": [ { "key": "tone", "control": "select", "label": "Tone", "required": true, "default": "info", "options": [{ "value": "info", "label": "Info" }, { "value": "warning", "label": "Warning" }] }, { "key": "title", "control": "text", "label": "Title", "required": true, "validation": { "max_length": 80 } }, { "key": "image", "control": "media", "label": "Image", "media_kind": "image" } ], "preview": { "tone": "info", "title": "Heads up" }, "render": { "tone": "tone", "nodes": [ { "element": "strong", "children": [{ "field": "title" }] }, { "element": "img", "src": "image", "alt": "title" } ] }, "migrations": [{ "from": 1, "to": 2, "steps": [{ "op": "rename", "from": "heading", "to": "title" }] }] }]- Блок хранится как
<your plugin id>/<name>, с версией, с которой он был сохранён, и своими значениями. - Элементы управления:
text,textarea,number,boolean,select,urlиmedia(media_kind:image,document,audio,videoилиfile;file— загрузка, которую пропускают настройки медиа сайта). Правила:required,min_length/max_length,min/max/integer, варианты выбора. Шаблонов нет. Значениеurl— адресhttp(s)или путь на сайте. Не больше 24 полей. rich_textоткрывает встроенный редактор форматированного текста. Значение —{ "richText": "<p>Текст</p>" }: канонический HTML хоста, до 10 000 символов. Привязка{ "field": "body" }внутри блочного элемента выводит абзацы, списки, ссылки, выделение, изображения и файлы. В сохранённом блоке текст находится в<div data-rich-field="body">; обычные правила обхода медиа, закрепления файлов за версиями, импорта и экспорта действуют и здесь. Вложенные блоки плагинов, сниппеты и встраивания не поддерживаются. Строчный блок плагина не может объявлятьrich_text. Приватные файлы разрешены только в приватной модели.relationзадаётmodelsи необязательныйmultiple; использует обычный выбор связей и актуальное право человека на чтение.groupзадаётfields, необязательныеrepeatableиmax_items: до трёх уровней групп и пятидесяти элементов в группе. Вложенные медиа сохраняют обычные привязки.{ "group", "children" }и{ "each", "children" }задают область дочерних связок рендера.render.nodes— дерево из:{ "element", "children" }изdiv section aside p span strong em small h2 h3 h4 blockquote ul ol li figure figcaption hr br;{ "element": "a", "href": <url or media field> };{ "element": "img", "src": <image field>, "alt"?: <text field> };{ "text" };{ "field" }(select показывает подпись варианта); и{ "when", "equals"?, "children" }. Не больше 64 узлов, глубина до 8 уровней.render.toneназывает поле select, значение которого (neutral,info,success,warning,danger) окрашивает блок. Каждое значение экранируется; при небезопасном адресе дочернее содержимое выводится без ссылки.- Без
renderблок всё равно можно редактировать и хранить, но рендереры показывают его как неподдерживаемый: ничего на публичной странице, заглушка в предпросмотре. - Повышайте
version, когда меняются поля, и добавляйте по одной миграции на шаг (toравноfrom + 1):rename,set_defaultиdrop. Сохранённые блоки сохраняют свою версию. Рендереры и редактор мигрируют в памяти; сохранённый блок меняется, когда автор его обновляет. - Отключение или удаление вашего плагина никогда не удаляет блоки. Они остаются сохранёнными со своей версией и значениями, и ваш следующий релиз может их мигрировать.
- Контент проверяется при сохранении только по активным плагинам. См.
правила сохранения. API управления перечисляет определения через
GET /api/manage/v1/rich-blocks(listRichBlockDefinitions); админка читаетGET /api/plugins/rich-blocks; рендереры читаютGET /v1/_rich-blocks.
Источники медиа
Заголовок раздела «Источники медиа»media_sources превращает ваш сервис в библиотеку, которую люди просматривают в окне выбора медиа Dee Wan, в разделе
Источники, рядом с собственной библиотекой сайта. Запросите media:source и объявите не больше четырёх источников:
"requested_capabilities": ["media:source"],"media_sources": [ { "id": "library", "label": "Reference images", "description": "Seven self-made CC0 files.", "endpoint": "https://media-source.example/dee-wan/sources/library", "modes": ["copy", "reference"], "delivery_origins": ["https://media-source.example"] }]modes обязателен. copy позволяет копировать файл в собственную библиотеку сайта; reference позволяет
вместо этого привязать его, а байты хранит ваш сервис. Источник объявляет один режим или оба, а Dee Wan никогда
не угадывает режим по URL или имени. delivery_origins (от одного до четырёх чистых origin, origin вашего
манифеста может быть одним из них) обязателен с reference и отсутствует без него.
- Ядро вызывает
endpointчерез тот же обмен вызова, что и операцию: оно отправляет POST с конвертом, вы обмениваете код своим токеном, а связка содержитkind: "media_source",idвашего источника и не содержит делегирования. Связанный вход —$defs/media_source_request:{ "action": "browse", "query", "cursor", "limit" }или{ "action": "item", "item_id" }. - Отвечайте на просмотр
{ "status": "completed", "items": [...], "next_cursor": "..." | null }, а на элемент —{ "status": "completed", "item": {...} }. У каждого элемента естьid,title,file_name,mime_type,thumbnail_url,download_url(обязателен, когда источник объявляетcopy),delivery_url(обязателен, когда он объявляетreference) иattribution(source_name,licenseи необязательныеauthor,author_url,license_url,source_url). Добавьтеwidth,heightиbyte_size, если знаете их.descriptionизображения (не больше 125 символов) становится его альтернативным текстом на языке сайта по умолчанию. Отказывайте через{ "status": "refused", "code", "message" }; окно выбора показывает ваше сообщение. thumbnail_urlиdownload_urlдолжны быть на origin вашего манифеста, аdelivery_url— на одном изdelivery_originsисточника. Окно выбора загружает превью прямо у вас без referrer и без учётных данных Dee Wan. Когда человек добавляет элемент, ядро снова спрашиваетitemи само выполняет GETdownload_url: без учётных данных, без перенаправлений, 20 секунд, лимит медиафайла. Подписывайтеdownload_url, если ваши файлы не публичны.- Ядро проверяет байты по обычным правилам загрузки: допустимые виды, форматы и лимиты размера сайта,
а затем выполняет проверку дубликатов. Копия — обычный медиафайл, принадлежащий сайту. Он сохраняет ваш
attributionи id вашего плагина, источника и элемента, которые детали медиа показывают как его авторство, а копии сайта переносят. Опубликованные страницы получают толькоcredit(author,source,license,url), показанный под изображением. После копирования сайт больше ничего не загружает из вашего сервиса, поэтому страницы продолжают работать, даже если сервис станет недоступен. - Режим reference добавляет ссылку на файл вместо копирования. Ядро ничего не загружает: строка библиотеки хранит ваш
delivery_urlкак адрес, id вашего плагина, источника и элемента как идентификаторы, а также объявленные вами метаданные и авторство; тот же элемент, добавленный снова, подчиняетсяon_duplicate. Опубликованные страницы, копии сайта и генерируемые сайты используютdelivery_urlкак есть, поэтому адрес должен оставаться стабильным и доступным, пока его используют сайты. Dee Wan не проксирует его, не может заменить или отредактировать его байты (media_external_bytes_unavailable) и никогда не обращается к вам, когда человек удаляет строку библиотеки. Если ваш плагин удалён или отключён, существующие страницы продолжают использовать сохранённый адрес, сведения о медиа сообщают, что источник недоступен, а новые привязки из него отклоняются. Добавляйте режим через обязательное поле запросаmode; режим, которого источник не объявил, отклоняется с 422media_source_mode_unsupported. - Просмотр ничего не меняет и не аудируется. Человеку нужен
media:upload. Отключение установки плагина или отзывmedia:sourceсразу скрывает источник. - API управления предлагает то же:
listMediaSources,browseMediaSourceиimportMediaSourceItem(областьmedia:write, телоitem_idиmode). См. API управления. plugins/media-source-reference— небольшой полный пример с тестами. Набор conformance проверяет источник с его subjectmediaSource(см. Тестирование плагина).
Хуки перед операцией
Заголовок раздела «Хуки перед операцией»hooks позволяют вашему сервису принять решение до того, как Dee Wan примет данные. Объявите не более одного хука каждого вида: upload_metadata вызывается перед сохранением нового файла библиотеки, comment_moderation — перед сохранением комментария посетителя.
"hooks": [ { "id": "upload-check", "label": "Upload check", "kind": "upload_metadata", "endpoint": "https://plugin.example/dee-wan/hooks/upload", "requires": [] }, { "id": "comment-check", "label": "Comment check", "kind": "comment_moderation", "endpoint": "https://plugin.example/dee-wan/hooks/comment", "requires": [] }]Хук выключен, пока администратор сайта не включит его для инсталляции в разделе Настройки → Плагины. Объявление ничего не запрашивает и ничего не запускает. Неподдерживаемый kind отклоняется при проверке манифеста.
- Core вызывает
endpointчерез тот же обмен вызовами, что и операцию. В привязкеkind: "hook",idвашего хука,caller: nullи нет делегирования. Входные данные — только то, что нужно для решения: для загрузки$defs/hook_upload_facts(имя файла, MIME-тип, размер, папка и альтернативные тексты, но не байты); для комментария$defs/hook_comment_facts(запись, id комментария, родитель, имя автора, сайт и текст, но не адрес почты). Кто спрашивает, не называется. - Отвечайте на загрузку через
$defs/hook_upload_reply:{ "decision": "accept" }, при желании сnormalized(новоеfilename, сохраняющее расширение, и альтернативные тексты только на языках, которые загрузка уже назвала), либо{ "decision": "refuse", "code", "message" }. Загружающий видит ваше сообщение (422upload_hook_refused). - Отвечайте на комментарий через
$defs/hook_comment_reply:{ "decision": "approve" },{ "decision": "hold" }(комментарий ждёт модератора) или{ "decision": "reject" }(сохраняется как спам или, с"as": "refused", как отклонённый). - Отвечайте в течение 5 секунд. Если хук не уложился в срок, недоступен, вернул ответ вне разрешённого набора или был отключён во время работы, одобрения не будет: загрузка отклоняется (503
upload_hook_unavailable, ничего не сохраняется), а комментарий остаётся на модерации. - У запроса есть стабильный идентификатор повтора. При повторной загрузке или повторной отправке комментария в течение 24 часов используется ваш первый принятый ответ без нового вызова; неудавшийся запрос выполняется снова.
2. Обработайте запрос активации
Заголовок раздела «2. Обработайте запрос активации»При установке Dee Wan передаёт плагину учётные данные. Поэтому до первой установки в вашем сервисе
уже должен работать маршрут activation_url. Выпустите одноразовый код активации, передайте его
администратору по отдельному каналу и ожидайте запрос. Код должен соответствовать ^[A-Za-z0-9_-]{16,128}$. Всё остальное Dee Wan отклоняет как
invalid_activation_code ещё до вызова вашего сервиса. Когда администратор выполнит установку, Dee Wan отправит POST с этим телом на
activation_url:
{ "kind": "activate", "installation_id": "plg_0000000000000000000000000000d1ee", "site_id": "cdocssite00000000000000001", "dee_wan_base_url": "https://cms.example", "protocol_version": 1, "token": "dwp_ExampleTokenNotRealExampleTokenNotRealExamp", "activation_code": "activation-code-0123456789"}Что должен сделать ваш обработчик:
- Израсходовать
activation_codeровно один раз и отклонить неизвестный, использованный или просроченный код. Это единственное, что доказывает, что вызывающий — тот администратор, которому вы дали код. - Проверить, что
protocol_version— одна из поддерживаемых вами версий, аdee_wan_base_url— origin, который вы принимаете. - Сохранить
tokenкак свой секрет для этой инсталляции — в зашифрованном виде, никогда не в логах, никогда в URL или теле ошибки. - Ответить 2xx в течение 5 секунд, с телом не более 32 КБ. Только тогда ядро помечает инсталляцию как
activeи сохраняет лишь SHA-256-хеш токена. Показать токен снова оно уже никогда не сможет.
Пока ваш обработчик не вернул ответ, инсталляция всё ещё pending, а токен ожидающей инсталляции
отвечает plugin_unauthorized. Поэтому не вызывайте /api/plugin/v1 изнутри обработчика
активации — сохраните токен, ответьте и сделайте первый вызов после этого.
Куда обращаться: каждый запрос протокола идёт на dee_wan_base_url, за которым следует /api/plugin/v1,
например <dee_wan_base_url>/api/plugin/v1/context. Храните базовый URL вместе с токеном и принимайте
только ожидаемые origin.
kind, равный rotate, — та же передача для существующей инсталляции: замените сохранённый токен.
Оба вида несут код, потому что ротация — это передача, а не сброс.
Когда активация не удаётся. Ответ не 2xx, редирект, тело больше 32 КБ или отсутствие ответа в течение 5 секунд — всё это приводит к сбою установки: ядро удаляет свою ожидающую инсталляцию и показывает администратору причину. Однако ядро не видит, что сделала ваша сторона. Если ваш обработчик сохранил токен и затем ответил слишком поздно, у вас теперь есть токен для инсталляции, которой больше не существует, а ваш код активации уже израсходован. Учитывайте это:
- Обработчик должен завершаться быстро, с запасом до тайм-аута.
- Считайте сохранённую инсталляцию неподтверждённой до первого успешного вызова с её токеном и
удаляйте ту, чей токен ни разу не сработал и теперь отвечает
plugin_unauthorized. - Попросите администратора выпустить для повторной попытки новый код активации. Повторная попытка — это новая установка
с новым
installation_id, а не вторая попытка старой.
3. Проверка, затем установка
Заголовок раздела «3. Проверка, затем установка»Администратор с site:plugins сначала проверяет манифест. Проверка ничего не сохраняет — она
нужна для того, чтобы одобрение было осознанным.

Проверка манифеста
curl -X POST "https://cms.example/api/plugins/review" \ -H "X-Site-Id: cdocssite00000000000000001" \ -H "Content-Type: application/json" \ -b "__Secure-better-auth.session_token=$SESSION" \ --data '{"manifest_url":"https://plugin.example/dee-wan/manifest.json"}'{ "data": { "manifest": { "manifest_version": 1, "id": "com.example.myplugin", "name": "My Plugin", "version": "0.1.0", "protocol_versions": [1], "base_url": "https://plugin.example", "activation_url": "https://plugin.example/dee-wan/activate", "management_url": "https://plugin.example/installations/{installation_id}", "requested_capabilities": [ "content:read", "workflow:read", "workflow:publish", "workflow:unpublish" ], "content_actions": [{ "id": "schedule", "label": "Schedule" }] }, "protocol_version": 1, "review_digest": "5ad97eed147d0a0f9f8934fa20485f287e656d72499fc739c831d2748680e49a" }}Затем администратор выполняет установку: одобренные возможности, одобренные id моделей и ваш одноразовый код. Установка
снова получает и валидирует манифест, и сохраняется именно эта вторая копия. Одобренный набор
должен быть подмножеством того, что запросил этот манифест. Пустой model_ids означает полное отсутствие доступа к контенту
— а не все модели. Передайте полученный review_digest в expected_review_digest, чтобы установить
именно проверенный манифест с проверенного URL. При изменении сервер вернёт
409 manifest_changed_since_review до активации.
Установка, которая выполняет активацию
curl -X POST "https://cms.example/api/plugins" \ -H "X-Site-Id: cdocssite00000000000000001" \ -H "Content-Type: application/json" \ -b "__Secure-better-auth.session_token=$SESSION" \ --data '{"manifest_url":"https://plugin.example/dee-wan/manifest.json","activation_code":"activation-code-0123456789","expected_review_digest":"5ad97eed147d0a0f9f8934fa20485f287e656d72499fc739c831d2748680e49a","capabilities":["content:read","workflow:read","workflow:publish","workflow:unpublish"],"model_ids":["cdocsmodel0000000000000001"]}'{ "data": { "id": "plg_0000000000000000000000000000d1ee", "plugin_id": "com.example.myplugin", "plugin_name": "My Plugin", "plugin_version": "0.1.0", "protocol_version": 1, "state": "active", "capabilities": [ "content:read", "workflow:read", "workflow:publish", "workflow:unpublish" ], "requested_capabilities": [ "content:read", "workflow:read", "workflow:publish", "workflow:unpublish" ], "model_ids": ["cdocsmodel0000000000000001"], "content_actions": [{ "id": "schedule", "label": "Schedule" }], "manage_views": [], "ui_extensions": [], "operations": [], "policies": [], "policy_modes": {}, "hooks": [], "hook_modes": {}, "manifest_url": "https://plugin.example/dee-wan/manifest.json", "management_origin": "https://plugin.example", "created_at": "2026-01-01T00:00:00.000Z", "updated_at": "2026-01-01T00:00:00.000Z" }}Ни токен, ни хеш не появляются в этом ответе, в любом последующем чтении или в записи аудита, которую создаёт установка. Единственная копия — у вашего сервиса.
4. Ограничение выданных прав
Заголовок раздела «4. Ограничение выданных прав»Выданные права можно изменить в любой момент, но только в пределах запроса из сохранённого манифеста. Исключение возможности или модели действует уже со следующего запроса: ждать сброса кеша не нужно.
Отзыв возможности, которая этому плагину оказалась не нужна
curl -X PUT "https://cms.example/api/plugins/plg_0000000000000000000000000000d1ee/grant" \ -H "X-Site-Id: cdocssite00000000000000001" \ -H "Content-Type: application/json" \ -b "__Secure-better-auth.session_token=$SESSION" \ --data '{"capabilities":["content:read","workflow:read","workflow:publish"],"model_ids":["cdocsmodel0000000000000001"]}'{ "data": { "id": "plg_0000000000000000000000000000d1ee", "plugin_id": "com.example.myplugin", "plugin_name": "My Plugin", "plugin_version": "0.1.0", "protocol_version": 1, "state": "active", "capabilities": ["content:read", "workflow:read", "workflow:publish"], "requested_capabilities": [ "content:read", "workflow:read", "workflow:publish", "workflow:unpublish" ], "model_ids": ["cdocsmodel0000000000000001"], "content_actions": [{ "id": "schedule", "label": "Schedule" }], "manage_views": [], "ui_extensions": [], "operations": [], "policies": [], "policy_modes": {}, "hooks": [], "hook_modes": {}, "manifest_url": "https://plugin.example/dee-wan/manifest.json", "management_origin": "https://plugin.example", "created_at": "2026-01-01T00:00:00.000Z", "updated_at": "2026-01-01T00:00:00.000Z" }}Запрос возможности за пределами сохранённого манифеста возвращает 400 invalid_capabilities. Позже выданные
права можно снова расширить, но только в пределах запроса из сохранённого манифеста.
Выпуск нового релиза
Заголовок раздела «Выпуск нового релиза»Публикация нового манифеста по вашему URL не меняет существующую установку плагина. Для этого администратор должен явно выполнить два действия:
- Принять релиз через
POST /api/plugins/:id/upgradeс телом{"expected_version": "<the version they reviewed>"}. Ядро снова получает манифест по сохранённому URL манифеста и отклоняет его какmanifest_changed_since_review, если егоversionне та, что названа, какmanifest_identity_changed, если изменился егоidили origin егоbase_url, и какprotocol_changed, если изменилась версия протокола, которую согласовало бы ядро. При успехе ядро сохраняет новый манифест и оставляет только те выданные возможности, которые новый манифест всё ещё запрашивает. Принятие релиза может сузить выданные права, но никогда не расширяет их. - Расширить права, если релиз запрашивает новую возможность, через
PUT /api/plugins/:id/grant, как выше. Теперь это разрешено, потому что сохранённый манифест её запрашивает.
Поэтому релиз, которому нужна новая возможность, должен сказать об этом в собственных примечаниях к релизу: пока
администратор не выполнит оба шага, ваш сервис продолжает работать с прежними правами, а команда, которой
нужна новая возможность, отвечает capability_refused.
5. Открытие вашего UI через делегированный запуск
Заголовок раздела «5. Открытие вашего UI через делегированный запуск»Обычный делегированный запуск открывает вашу страницу управления в новой вкладке. Объявленный и
проверенный frame открывает изолированный редактор или представление управления внутри админки;
действия проверяет хост. Фрейм не получает сессию администратора или код запуска.
Cookie сессии Dee Wan остаются в Dee Wan. Делегированный запуск выпускает одноразовый код, действительный 60 секунд,
хранящийся хешированным и привязанный к инсталляции, сайту, пользователю, действию и — для действия над контентом — к одному экземпляру.
Есть два вида запуска. manage без instance_id открывает ваш интерфейс для всей установки плагина
и требует site:plugins. Запуск manage с объявленным представлением предназначен для редакционной работы:
право site:plugins ему не нужно. Достаточно одной модели, к которой плагину предоставлен доступ и которую
пользователь может читать — все записи или только свои. Действие над контентом использует один из id
content_actions манифеста и instance_id; ему нужно право чтения выбранной записи. Если её модель
не входит в права установки плагина, запуск отклоняется с model_not_granted. Административный интерфейс
показывает действие только для записей моделей, к которым плагину предоставлен доступ.
Запуск manage может также содержать "view", одно из значений manage_views вашего манифеста. Тогда URL запуска
получает dee_wan_view=<view> рядом с dee_wan_launch. Это лишь подсказка о целевой странице: обменяйте код как
обычно, затем откройте эту страницу в созданной сессии. Подсказка ничего не разрешает, поэтому неизвестное значение считайте отсутствием подсказки.
View, который ваш манифест не объявляет, отклоняется как invalid_launch_action.
Запуск может начаться и с агента. Учётные данные управления или подключённое приложение с областью
plugins:use создают ту же ссылку через API управления или MCP (см.
Запуск плагинов). Ядро выполняет те же проверки, и ссылку
всё равно открывает человек. Ваша страница видит обычный запуск.
Запрос URL запуска
curl -X POST "https://cms.example/api/plugins/plg_0000000000000000000000000000d1ee/launch" \ -H "X-Site-Id: cdocssite00000000000000001" \ -H "Content-Type: application/json" \ -b "__Secure-better-auth.session_token=$SESSION" \ --data '{"action":"schedule","instance_id":"cdocsinstance00000000000001"}'{ "data": { "url": "https://plugin.example/installations/plg_0000000000000000000000000000d1ee?dee_wan_launch=dwl_ExampleLaunchCodeExampleLaunchCodeExampleLa" }}Браузер переходит туда. Ответ запуска от Dee Wan содержит
Referrer-Policy: no-referrer, но страница, на которую попадает браузер, — ваша, как и остальная
часть работы:
- Сами отправляйте
Referrer-Policy: no-referrerна своей странице управления, чтобы код в её URL не мог утечь через referrer. - Не загружайте никаких сторонних ресурсов до обмена.
- Как только обмен прошёл успешно, перенаправьте на ту же страницу без
dee_wan_launchв URL, чтобы код не оставался в адресной строке или истории браузера. Эталонный плагин отвечает303 See Otherна чистый URL.
Ваш БЭКЕНД обменивает код, используя ваш токен инсталляции:
Обмен кода на делегирование
curl -X POST "https://cms.example/api/plugin/v1/launch/exchange" \ -H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \ -H "Content-Type: application/json" \ --data '{"code":"dwl_ExampleLaunchCodeExampleLaunchCodeExampleLa"}'{ "data": { "delegation_id": "pld_0000000000000000000000000000d1ee", "installation_id": "plg_0000000000000000000000000000d1ee", "site_id": "cdocssite00000000000000001", "action": "schedule", "instance_id": "cdocsinstance00000000000001", "model_ids": ["cdocsmodel0000000000000001"], }}Теперь создайте СВОЙ cookie сессии для этого браузера и свяжите с ним delegation_id.
Делегирование не является учётными данными и само по себе не предоставляет прав. Оно указывает
пользователя, запустившего плагин. Передавайте его с каждой последующей командой, чтобы ядро
могло проверить актуальные полномочия этого пользователя в момент выполнения.
Для чего можно использовать делегирование:
- Делегирование действия над контентом (с
instance_id) действует только на элементы из той же группы переводов, что и этот экземпляр. Указание его для любого другого элемента — этоrequester_unauthorized. - Делегирование manage (
instance_idравенnull) действует на элементы моделей из егоmodel_ids. - Каждое делегирование содержит
model_ids: модели из выданного набора, которые пользователь мог читать при запуске — все записи или только свои. Для действия над контентом это модель выбранной записи. Показывайте данные и выполняйте действия только в этих пределах. Запрос к записи другой модели или к записи, которую пользователь больше не может читать, возвращаетrequester_unauthorized. GET /api/plugin/v1/delegations/<delegation_id>возвращает текущую область делегирования: каждую модель, доступные пользователю виды переходов иentries. Выполняйте запрос перед показом каждой страницы. Предлагайте перенос или отмену запланированной работы только для указанного вида.entries: "all": человек читает каждый элемент этой модели, и виды переходов действуют для каждого.entries: "checked": человек читает или изменяет там только свои элементы (read:own,update:own). СпроситеGET /api/plugin/v1/delegations/<delegation_id>/entries?instance_id=…(до 100 id), какие элементы показывать и что разрешено для каждого. Не показывайте то, что он не вернул. Не копируйте, кто чем владеет, в своё хранилище; спрашивайте заново при каждом запросе.- Делегирование, созданное через учётные данные управления, ограничено возможностями установки плагина,
которые разрешены областями доступа этих учётных данных. Команда вне этих пределов возвращает
capability_refused. - Ядро не проверяет
actionделегирования, когда вы отправляете команду. Оно нужно для того, чтобы ваш собственный UI знал, какой экран показать. - У делегирования нет ограничения по времени, потому что запланированная работа может выполниться намного позже того, как человек открыл ваш UI. Оно перестаёт работать, когда у этого человека больше нет полномочий, нужных команде, — он потерял право доступа, свою роль на сайте или учётную запись, — а также когда инсталляция отключена, удалена или её токен больше не действителен. Текущие полномочия человека перечитываются при каждой команде.
6. Чтение контента и фиксация версии перед сменой статуса
Заголовок раздела «6. Чтение контента и фиксация версии перед сменой статуса»Прежде чем запланировать операцию, прочитайте запись с делегированием. Блок workflow показывает,
что пользователь вправе сделать сейчас, и возвращает значения, которые нужно зафиксировать.
Чтение элемента требует content:read. Блок workflow — а с ним и workflow.revision, который
должна отправлять каждая команда перехода, — возвращается, только если инсталляция также имеет workflow:read.
Без неё вы получаете только сводку, и закреплять нечего. Поэтому плагин, который отправляет команды,
запрашивает обе возможности.
Делегированное чтение также возвращает saved с учётом текущих прав пользователя на поля и языки.
Ниже показаны поля сводки.
Чтение одной записи от имени пользователя, запустившего плагин
curl "https://cms.example/api/plugin/v1/content/cdocsinstance00000000000001?delegation_id=pld_0000000000000000000000000000d1ee" \ -H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp"{ "data": { "instance_id": "cdocsinstance00000000000001", "primary_instance_id": "cdocsinstance00000000000001", "translation_group_id": "cdocsgroup0000000000000001", "model_id": "cdocsmodel0000000000000001", "model_slug": "article", "lang": "en", "label": "Hello", "slug": "hello", "current_version_id": "cdocsversion00000000000001", "published_version_id": null, "workflow": { "state": "draft", "revision": 0, "transitions": [{ "to": "published", "kind": "publish" }] } }}Сохраните в своей записи:
primary_instance_id— экземпляр, к которому обращается переход. Публикация применяется ко всей группе переводов, а первичный экземпляр — это дескриптор группы.current_version_idдля публикации обычного черновика илиpublished_version_idдля снятия с публикации. Это и есть закрепление.workflow.revision.- Выбранные вами
toиkind, которые должны быть одним из предложенныхtransitions.
Независимая будущая версия создаётся через POST /api/plugin/v1/content/:instanceId/futures
и читается через GET /api/plugin/v1/content/:instanceId/futures/:futureId. Для публикации закрепите
current_version_id этой ветки вместе с future_draft_id и expected_future_revision. Ядро связывает
ветку с установкой плагина, выпуском и записью, а при начале команды публикации фиксирует её точную ревизию.
Проверяются текущие права, этап работы, обязательные поля и задачи проверки именно этой версии;
обычный черновик не меняется. Если результат неизвестен, повторяйте ту же команду с той же версией
и ключом идемпотентности. В примере ниже публикуется обычный черновик.
7. Отправка команды
Заголовок раздела «7. Отправка команды»Отправьте одну операцию изменения со всеми зафиксированными условиями и ключом идемпотентности, который можно повторно вычислить.
Публикация закреплённой версии
curl -X POST "https://cms.example/api/plugin/v1/content/cdocsinstance00000000000001/transitions/published" \ -H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: sched:plg_0000000000000000000000000000d1ee:sch_0001:r1" \ --data '{"kind":"publish","expected_current_version_id":"cdocsversion00000000000001","expected_workflow_revision":0,"delegation_id":"pld_0000000000000000000000000000d1ee"}'{ "outcome": { "code": "transition_applied", "message": "The transition was applied.", "retry": false }, "data": { "instance_id": "cdocsinstance00000000000001", "primary_instance_id": "cdocsinstance00000000000001", "from": "draft", "to": "published", "published": true, "published_changed": true, "published_version_id": "cdocsversion00000000000001", "revision": 1, "redirect": { "status": "none", "reason": "no_public_address" }, "policy_results": [] }}redirect присутствует, когда переход публикует. Он сообщает об автоматическом перенаправлении, которое ядро
сделало для изменённого адреса, или о причине, по которой его нет. Повтор той же команды возвращает это же тело.
Выводите ключ из своей собственной записи и её ревизии, никогда не из часов. Эталонный плагин
использует sched:<installation_id>:<record_id>:r<revision> — это форма ключа выше. Ядро
принимает любые от 1 до 128 печатных символов ASCII без пробелов. Тогда:
- Точный повтор возвращает первый результат как
idempotent_replay. Повтор после таймаута безопасен. - Публикация, которая меняет адрес, записывает своё перенаправление после фиксации перехода. Пока этот
исход не записан, команда и её повторы отвечают
temporary_failureс сообщением, что переход применён. Повторите с тем же ключом и телом. Результата безredirectвы никогда не получите. - Тот же ключ с другими входными данными — это
idempotency_key_reused. Перепланирование — это новая ревизия, а значит, и новый ключ. - Для каждого отказа указан код. Все отказы окончательны, кроме
rate_limitedиtemporary_failure. Прочитайте таблицу в Protocol v1 и обработайте каждый. Не превращайте неизвестный сбой в успех и не повторяйте бесконечно.
Журнал аудита записывает это как действие плагина, а не пользователя: событие workflow указывает инсталляцию, id, имя и принятую версию вашего плагина, версию протокола, дайджест ключа идемпотентности и человека, чьё делегирование это авторизовало. Админ-интерфейс показывает имя вашего плагина в качестве действующего лица.
8. Удаление
Заголовок раздела «8. Удаление»Удаление сначала выполняет отзыв. Состояние становится revoked, оба хеша токена удаляются, и этот токен
уже никогда не может стать действительным.
Удаление
curl -X DELETE "https://cms.example/api/plugins/plg_0000000000000000000000000000d1ee" \ -H "X-Site-Id: cdocssite00000000000000001" \ -b "__Secure-better-auth.session_token=$SESSION"{ "data": { "id": "plg_0000000000000000000000000000d1ee", "state": "revoked" } }Каждый последующий запрос, включая уже находящийся в вашей очереди
curl "https://cms.example/api/plugin/v1/context" \ -H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp"{ "outcome": { "code": "plugin_unauthorized", "message": "The installation token is missing, unknown, disabled or revoked.", "retry": false }}Из этого следуют две вещи, и обе должны быть в вашей собственной документации:
- Атрибуция в аудите сохраняется. Событие workflow хранит снимок вашей инсталляции, а не внешний ключ, поэтому удаление не может стереть, кто что опубликовал.
- Dee Wan не утверждает, что удалил данные, которые хранятся у вас. За удаление в вашем сервисе отвечаете вы. В документации укажите, какие данные вы храните, как долго и как оператор может их удалить. Ответ эталонного плагина — Планирование от начала до конца.
Что строить дальше
Заголовок раздела «Что строить дальше»Protocol v1 намеренно не даёт вам ни планировщика, ни очереди, ни потока событий. Если ваш плагин действует позже, на вас лежат:
- управление временным правом на обработку (lease), чтобы два ваших воркера не могли одновременно выполнить одно задание, —
Idempotency-Key— это вторая защита ядра, а не ваша первая. - ограниченные повторы с джиттером, лимитом попыток и лимитом возраста, а также соблюдение
Retry-Afterв пределах выбранного вами максимума. - таблица окончательных отказов, которую могут прочитать ваши пользователи, потому что каждый условный отказ означает, что человеку нужно что-то решить.
- ваша собственная авторизация для вашего собственного UI. Ядро проверяет запуск. После этого сессия ваша.
Исходный код плагина Scheduling (plugins/scheduling в репозитории Dee Wan) — полный рабочий
пример всех четырёх пунктов. Его поведение описано на странице Плагин планирования.
Повторно используемые связи
Заголовок раздела «Повторно используемые связи»Поля связей хранят ID записей. При публичном чтении цель определяется по обычным правилам доступа
к опубликованным связям и выбора языка. { "field": "target" } показывает текущий опубликованный
заголовок или имя. { "field": "target", "property": "address" } показывает доступное скалярное
поле цели. Ссылка с href: "target" использует опубликованный адрес Page, Blog или управляемого
маршрута либо публичное поле url цели. Если публичного адреса нет, дочернее содержимое ссылки
выводится без элемента ссылки. Отсутствующие, черновые, снятые с публикации, перемещённые в корзину
или недоступные цели не раскрывают ни подписей, ни адресов. Приватные поля не выдаются. Подписи —
результат чтения; при обычном сохранении принимаются ID, а не объекты с присланными подписями и URL.
Копия сайта и история содержат проверяемый по количеству и контрольной сумме rich-relations.json.
Он хранит версию, указатель на rich HTML, указатель на ID внутри блока, ID цели и модели. Вложенные
и повторяемые ссылки переназначаются после создания записей. Отсутствующая текущая цель блокирует
импорт; пропущенная цель старой истории получает обычное именованное предупреждение. Для сниппетов
владелец — snippet:<source id>. Если подходящей исходной декларации нет, экспорт отклоняется с именованной ошибкой.
Восстановите исходную декларацию и повторите экспорт. Декларация назначения не определяет ID источника.
Объявление встроенных элементов
Заголовок раздела «Объявление встроенных элементов»Укажите inline: true в объявлении rich_blocks, чтобы элемент можно было вставить внутрь абзаца.
Поля, значения, ссылки на файлы и записи, версии и миграции используют существующий контракт блоков.
Хранимый элемент — <span data-inline="plugin"> с атрибутами data-type, data-version и data-values.
Объявленные файлы остаются пустыми ссылками внутри него. Не передавайте готовый HTML.
Дерево отображения допускает только span, strong, em, small, a, img и br, в том числе
внутри условий и групп. Блочные элементы и тон блока отклоняются. Встроенный элемент нельзя помещать
внутрь существующей ссылки. Диалог использует те же поля Dee Wan, а публичное представление — тот же
экранирующий рендерер, что и обычный блок.
Метаданные от плагинов
Заголовок раздела «Метаданные от плагинов»Объявите ограниченные дополнения в renderer_metadata и запросите renderer:metadata. Каждое
объявление содержит key, label, meta_names, link_rels и schema_types. Доступ предоставляется
при проверке установки; новые права после обновления остаются выключенными до проверки. Дополнение
может передавать только объявленные имена метаданных, виды ссылок (alternate, related, author,
license) и типы JSON-LD.
Используйте делегированный POST /content/:instanceId/renderer-metadata с заголовком Idempotency-Key
и телом { delegation_id, locale, expected_version_id, contributions }. У каждого дополнения есть
объявленный key и необязательные meta, links, json_ld. Запись заменяет дополнения только этого
плагина на указанном языке, создаёт обычную версию контента и остаётся черновиком до публикации.
Устаревшая версия, отсутствие права или ограничение полей/языков отклоняют запись целиком.
Данные входят в принятую версию контента. Публичная выдача не обращается к плагину. Заголовки и описания SEO, canonical/robots, подтверждения сайта и опубликованные языковые адреса имеют приоритет. Дополнения объединяются по ID плагина и ключу; одинаковые имена и языковые ссылки разрешаются детерминированно. Разметка, исполняемые адреса, произвольные контексты и неверные ссылки отклоняются. Управляемый и генерируемый сайты и подписанный предпросмотр используют общую сериализацию SEO; предпросмотр по-прежнему запрещён для индексации.
Получатель письма из записи
Заголовок раздела «Получатель письма из записи»Объявите mail_templates и запросите mail:send. Администратор проверяет текст и правило выбора получателя в Настройки → Плагины → Шаблоны писем. Плагин не передаёт адреса или произвольный текст.
Для шаблона с entry_author или entry_assignee вызовите POST /api/plugin/v1/mail/send с Idempotency-Key и {delegation_id, template, instance_id, variables}. Запись должна входить в доступные инсталляции и делегированию модели и записи; инициатор должен видеть её и иметь site:plugins. Подходит делегирование действия над соответствующей записью. Для заданных адресов по-прежнему нужно делегирование manage без instance_id.
entry_author выбирает аккаунт, создавший выбранную запись в CMS (content_instance.created_by_id), а не публичного автора блога, связанный профиль или последнего редактора. entry_assignee выбирает текущего назначенного пользователя из процесса согласования (content_workflow.assigned_to_id). Аккаунт должен быть активным, иметь текущий доступ к сайту и право читать запись; активному суперадминистратору отдельная роль на сайте не нужна. Отсутствие подходящего получателя даёт validation_failed; ответ не содержит адреса. При неопределённом результате повторите тот же запрос с тем же ключом. Система использует сохранённые письмо и получателя и снова проверяет исходный аккаунт; смена рецензента не перенаправляет повтор. Ограничения и результаты см. в протоколе.
Стабильная версия ещё не выпущена (pre-MVP)
Коммит исходного кода: e207e63239a627fbd2ae9c414ef7c6eb19fcfe5a (с локальными изменениями)
Дата сборки (UTC): 2026-10-21T09:28:24.214Z
Коммит для обновления: не определён: в рабочей копии есть изменения. Примечания к выпуску