Plugin maintainer reference
Представление сообщений
Представление сообщений — это общий контракт OpenClaw для форматированного интерфейса исходящих сообщений в чатах. Он позволяет агентам, командам CLI, потокам подтверждения и плагинам однократно описать назначение сообщения, а каждому плагину канала — отобразить его в наиболее подходящем нативном формате.
Используйте представление для переносимого интерфейса сообщений: текстовых разделов, небольшого контекстного текста или нижнего колонтитула, разделителей, диаграмм, таблиц, кнопок, меню выбора, а также заголовка и тона карточки.
Не добавляйте в общий инструмент сообщений новые нативные поля провайдеров, такие как Discord components, Slack
blocks, Telegram buttons, Teams card или Feishu card. Это выходные данные рендерера,
которыми управляет плагин канала.
Контракт
Авторы плагинов импортируют публичный контракт из:
MessagePresentation, ReplyPayloadDelivery,} from "openclaw/plugin-sdk/interactive-runtime";Структура:
type MessagePresentation = { title?: string; tone?: "neutral" | "info" | "success" | "warning" | "danger"; blocks: MessagePresentationBlock[];}; type MessagePresentationBlock = | { type: "text"; text: string } | { type: "context"; text: string } | { type: "divider" } | { type: "buttons"; buttons: MessagePresentationButton[] } | { type: "select"; placeholder?: string; options: MessagePresentationOption[] } | { type: "chart"; chartType: "pie"; title: string; segments: Array<{ label: string; value: number }>; } | { type: "chart"; chartType: "bar" | "area" | "line"; title: string; categories: string[]; series: Array<{ name: string; values: number[] }>; xLabel?: string; yLabel?: string; } | { type: "table"; caption: string; headers: string[]; rows: Array<Array<string | number>>; rowHeaderColumnIndex?: number; }; type MessagePresentationAction = | { type: "command"; command: string } | { type: "callback"; value: string } | { type: "approval"; approvalId: string; approvalKind: "exec" | "plugin"; decision: "allow-once" | "allow-always" | "deny"; } | { type: "url"; url: string } | { type: "web-app"; url: string }; type MessagePresentationButton = { label: string; action?: MessagePresentationAction; /** Legacy callback value. Prefer action for new controls. */ value?: string; /** @deprecated Use an action with type "url". */ url?: string; /** @deprecated Use an action with type "web-app". */ webApp?: { url: string }; /** @deprecated Use an action with type "web-app". */ web_app?: { url: string }; priority?: number; disabled?: boolean; reusable?: boolean; style?: "primary" | "secondary" | "success" | "danger";}; type MessagePresentationOption = { label: string; action?: Extract<MessagePresentationAction, { type: "command" | "callback" }>; /** Legacy callback value. Prefer action for new controls. */ value?: string;}; type ReplyPayloadDelivery = { pin?: | boolean | { enabled: boolean; notify?: boolean; required?: boolean; };};Семантика кнопок:
action.type: "command"запускает нативную слеш-команду через путь обработки команд ядра. Используйте это для кнопок и меню встроенных команд.action.type: "callback"передаёт непрозрачные данные плагина через путь обработки взаимодействий канала. Плагины каналов не должны интерпретировать данные обратного вызова как слеш-команды.action.type: "approval"определяет одно сохраняемое подтверждение оператора, его явный типexecилиpluginи запрошенное решение. Плагины каналов кодируют это действие в закрытый для транспорта обратный вызов и разрешают его через службу подтверждений; они не должны анализировать текст команды/approveили определять тип по идентификатору.action.type: "url"открывает обычную ссылку.action.type: "web-app"запускает нативное веб-приложение канала.value— устаревшее непрозрачное значение обратного вызова. Новые элементы управления должны использоватьaction, чтобы плагины каналов могли сопоставлять команды и обратные вызовы без догадок по тексту.url,webAppиweb_appпо-прежнему принимаются как устаревшие входные данные на границе. Нормализаторы сохраняют эти поля, чтобы рендереры могли отличать устаревшую семантику выпущенных версий от явных типизированных действий. Новые источники должны использоватьaction.labelобязателен и также используется в резервном текстовом представлении.styleносит рекомендательный характер. Рендереры должны заменять неподдерживаемые стили безопасным значением по умолчанию, а не завершать отправку с ошибкой.priorityнеобязателен. Если канал объявляет ограничения на количество действий и некоторые элементы управления необходимо отбросить, ядро в первую очередь сохраняет кнопки с более высоким приоритетом и оставляет исходный порядок кнопок с одинаковым приоритетом. Если все элементы управления помещаются, авторский порядок сохраняется.disabledнеобязателен. Каналы должны явно включить поддержку с помощьюsupportsDisabled; в противном случае ядро преобразует отключённый элемент управления в неинтерактивный резервный текст. В резервном тексте отключённая кнопка всегда отображается только как метка, даже если она содержит действиеcommand.reusableнеобязателен. Каналы, поддерживающие повторно используемые нативные обратные вызовы, могут сохранять доступность действия после успешного взаимодействия. Используйте его для повторяемых или идемпотентных действий, таких как обновление, просмотр или получение дополнительных сведений; не задавайте его для обычных одноразовых подтверждений и деструктивных действий.
Семантика выбора:
options[].actionпринимает толькоcommandилиcallback; действия подтверждения и ссылки доступны только для кнопок.options[].value— устаревшее выбранное прикладное значение.placeholderносит рекомендательный характер и может игнорироваться каналами без нативной поддержки выбора.- Если канал не поддерживает элементы выбора, резервный текст перечисляет метки.
Семантика диаграмм:
pieтребует положительных значений сегментов.bar,areaиlineиспользуют один упорядоченный массивcategories. Каждый ряд предоставляет ровно одно конечное значение для каждой категории в том же порядке.- Метки категорий и названия рядов должны быть уникальными. Недопустимые или неполные блоки диаграмм отбрасываются при нормализации, а не изменяют данные без уведомления.
- Нативное отображение диаграмм явно включается через
presentationCapabilities.charts. Другие каналы получают заголовок диаграммы, оси, категории, ряды и значения в виде детерминированного текста. Он также служит резервным вариантом для обеспечения доступности.
Семантика таблиц:
-
caption— обязательный краткий заголовок.headersдолжен содержать хотя бы одну уникальную непустую метку столбца. -
rowsдолжен содержать хотя бы одну строку. В каждой строке должна быть ровно одна ячейка на каждый заголовок, а каждая ячейка должна содержать непустую строку или конечное число. -
rowHeaderColumnIndex— необязательный индекс с отсчётом от нуля, определяющий столбец, ячейки которого нативные рендереры должны предоставлять как заголовки строк. -
Нормализация таблицы атомарна. Недопустимый заголовок, заголовок столбца, ширина строки, ячейка или индекс заголовка строки приводит к отбрасыванию блока таблицы вместо усечения или исправления его данных.
-
Нативное отображение таблиц явно включается через
presentationCapabilities.tables. Другие каналы получают заголовок и каждую строку в виде детерминированного линейного текста со свёрнутыми внутренними пробелами:text Открытая воронка (таблица)- Клиент: Acme; Этап: Выиграно; ARR: 125000- Клиент: Globex; Этап: Проверка; ARR: 82000
Отдельного дискриминатора report нет. Формируйте отчёт из title,
tone, text, context, chart, table и блоков действий. Благодаря этому каждый
блок можно отображать независимо, а полный отчёт получает такое же
детерминированное резервное текстовое представление.
Примеры источников
Простая карточка:
{ "title": "Подтверждение развёртывания", "tone": "warning", "blocks": [ { "type": "text", "text": "Canary готов к продвижению." }, { "type": "context", "text": "Сборка 1234, тестирование в промежуточной среде пройдено." }, { "type": "buttons", "buttons": [ { "label": "Подтвердить", "action": { "type": "callback", "value": "deploy:approve" }, "style": "success" }, { "label": "Отклонить", "action": { "type": "callback", "value": "deploy:decline" }, "style": "danger" } ] } ]}Кнопка только со ссылкой URL:
{ "blocks": [ { "type": "text", "text": "Примечания к выпуску готовы." }, { "type": "buttons", "buttons": [ { "label": "Открыть примечания", "action": { "type": "url", "url": "https://example.com/release" } } ] } ]}Кнопка мини-приложения Telegram:
{ "blocks": [ { "type": "buttons", "buttons": [ { "label": "Запустить", "action": { "type": "web-app", "url": "https://example.com/app" } } ] } ]}Меню выбора:
{ "title": "Выберите среду", "blocks": [ { "type": "select", "placeholder": "Среда", "options": [ { "label": "Canary", "value": "env:canary" }, { "label": "Рабочая среда", "value": "env:prod" } ] } ]}Диаграмма:
{ "blocks": [ { "type": "chart", "chartType": "line", "title": "Квартальная выручка", "categories": ["1-й квартал", "2-й квартал", "3-й квартал"], "series": [ { "name": "Продукт", "values": [120, 145, 138] }, { "name": "Услуги", "values": [80, 95, 104] } ], "xLabel": "Квартал", "yLabel": "Выручка" } ]}Табличный отчёт:
{ "title": "Отчёт по воронке", "tone": "info", "blocks": [ { "type": "text", "text": "Текущие возможности по этапам." }, { "type": "table", "caption": "Открытая воронка", "headers": ["Клиент", "Этап", "ARR"], "rows": [ ["Acme", "Выиграно", 125000], ["Globex", "Проверка", 82000] ], "rowHeaderColumnIndex": 0 }, { "type": "context", "text": "Обновлено из снимка CRM." } ]}Отправка через CLI:
openclaw message send --channel slack \ --target channel:C123 \ --message "Подтверждение развёртывания" \ --presentation '{"title":"Подтверждение развёртывания","tone":"warning","blocks":[{"type":"text","text":"Canary готов."},{"type":"buttons","buttons":[{"label":"Подтвердить","value":"deploy:approve","style":"success"},{"label":"Отклонить","value":"deploy:decline","style":"danger"}]}]}'Закреплённая доставка:
openclaw message send --channel telegram \ --target -1001234567890 \ --message "Тема открыта" \ --pinЗакреплённая доставка с явным JSON:
{ "pin": { "enabled": true, "notify": true, "required": false }}Контракт рендерера
Плагины каналов объявляют поддержку отображения в своём адаптере исходящих сообщений:
const adapter: ChannelOutboundAdapter = { deliveryMode: "direct", presentationCapabilities: { supported: true, buttons: true, selects: true, context: true, divider: true, charts: false, tables: false, limits: { actions: { maxActions: 25, maxActionsPerRow: 5, maxRows: 5, maxLabelLength: 80, maxValueBytes: 100, supportsStyles: true, supportsDisabled: false, }, selects: { maxOptions: 25, maxLabelLength: 100, maxValueBytes: 100, }, text: { maxLength: 2000, encoding: "characters", markdownDialect: "discord-markdown", }, }, }, deliveryCapabilities: { pin: true, }, renderPresentation({ payload, presentation, ctx }) { return renderNativePayload(payload, presentation, ctx); }, async pinDeliveredMessage({ target, messageId, pin }) { await pinNativeMessage(target, messageId, { notify: pin.notify === true }); },};Логические значения возможностей описывают, какие элементы средство визуализации может сделать интерактивными. Необязательные
limits описывают универсальную оболочку, которую ядро может адаптировать перед вызовом
средства визуализации:
type ChannelPresentationCapabilities = { supported?: boolean; buttons?: boolean; selects?: boolean; context?: boolean; divider?: boolean; charts?: boolean; tables?: boolean; limits?: { actions?: { maxActions?: number; maxActionsPerRow?: number; maxRows?: number; maxLabelLength?: number; maxValueBytes?: number; supportsStyles?: boolean; supportsDisabled?: boolean; supportsLayoutHints?: boolean; }; selects?: { maxOptions?: number; maxLabelLength?: number; maxValueBytes?: number; }; text?: { maxLength?: number; encoding?: "characters" | "utf8-bytes" | "utf16-units"; markdownDialect?: "plain" | "markdown" | "html" | "slack-mrkdwn" | "discord-markdown"; supportsEdit?: boolean; }; };};Перед визуализацией ядро применяет универсальные ограничения к семантическим элементам управления. Средства визуализации по-прежнему отвечают за окончательную проверку и обрезку с учётом особенностей провайдера: количества нативных блоков, размера карточки, ограничений URL и особенностей провайдера, которые невозможно выразить в универсальном контракте. Если ограничения удаляют из блока все элементы управления, ядро сохраняет метки как неинтерактивный контекстный текст, чтобы у доставленного сообщения оставался видимый резервный вариант.
Основной процесс визуализации
В каноническом исходящем пути, используемом CLI и стандартными действиями с сообщениями, ядро:
- Нормализует полезную нагрузку представления.
- Определяет исходящий адаптер целевого канала.
- Считывает
presentationCapabilities. - Применяет универсальные ограничения возможностей, например на количество действий, длину меток и
количество вариантов выбора, если адаптер их объявляет. Блоки диаграмм и таблиц
преобразуются в детерминированный текст, если адаптер явно не объявляет
charts: trueилиtables: trueсоответственно. - Вызывает
renderPresentation, если адаптер может визуализировать полезную нагрузку. - Использует консервативный текстовый резервный вариант, если адаптер отсутствует или не может выполнить визуализацию.
- Отправляет полученную полезную нагрузку через обычный путь доставки канала.
- Применяет метаданные доставки, например
delivery.pin, после первой успешно отправленной части сообщения.
Локальные для канала пути ответов или предпросмотра, которые напрямую используют ReplyPayload,
должны либо входить в этот канонический путь, либо формировать тот же резервный вариант
представления перед преобразованием полезной нагрузки в обычный текст или медиафайлы.
Ядро отвечает за резервное поведение, поэтому источники могут оставаться независимыми от каналов. Плагины каналов отвечают за нативную визуализацию и обработку взаимодействий.
Правила упрощения
Представление должно быть безопасно для отправки в каналы с ограниченными возможностями.
Резервный текст включает:
titleв первой строке- блоки
textкак обычные абзацы - блоки
contextкак компактные строки контекста - блоки
dividerкак визуальный разделитель - метки кнопок, включая URL для кнопок-ссылок
- метки вариантов выбора
- заголовок, тип, оси, категории, ряды и значения диаграммы
- подпись таблицы, заголовки и значения каждой строки
Видимость резервных значений кнопок
Если канал не может визуализировать интерактивные элементы управления, значения кнопок и вариантов выбора преобразуются в обычный текст. Резервное поведение сохраняет удобство использования, не раскрывая непрозрачные данные обратного вызова:
- Действия типа
commandвизуализируются какlabel: \command`` so users can copy the command and run it manually in the channel input. callback-typed actions and legacyvaluefields render as label-only. The opaque callback value is not exposed in fallback text.approval-typed actions render label-only. Approval IDs and decisions are transport data and are not exposed through generic scalar helpers or fallback text.url/web-appactions and deprecatedurl/webApp/web_appВходные данные отображают текст URL рядом с меткой кнопки, поскольку URL предназначен для пользователя.- Варианты выбора отображаются только как метки. Базовое значение варианта не раскрывается в резервном тексте.
Адаптеры каналов, добавляющие в резервный интерфейс инструкции по ручному вводу команд (например, инструкции для комментариев к документам Feishu), должны определять наличие команды по тем же блокам представления, которые использует средство резервной визуализации, чтобы текст инструкции появлялся только тогда, когда действительно показана команда для ручного ввода.
Неподдерживаемые нативные элементы управления должны упрощаться, а не приводить к сбою всей отправки. Примеры:
- Telegram с отключёнными встроенными кнопками отправляет резервный текст.
- Канал без поддержки выбора выводит варианты выбора в виде текста.
- Канал без нативной поддержки диаграмм выводит данные диаграммы в виде текста.
- Канал без нативной поддержки таблиц выводит каждую строку таблицы в виде текста.
- Кнопка только с URL становится либо нативной кнопкой-ссылкой, либо резервной строкой с URL.
- Сбои необязательного закрепления не приводят к сбою доставки сообщения.
Основное исключение — delivery.pin.required: true; если закрепление указано как
обязательное, а канал не может закрепить отправленное сообщение, доставка завершается с ошибкой.
Сопоставление провайдеров
Текущие встроенные средства визуализации:
| Канал | Целевой формат нативной визуализации | Примечания |
|---|---|---|
| Discord | Компоненты и контейнеры компонентов | Сохраняет устаревший channelData.discord.components для существующих источников нативных полезных нагрузок провайдера, но новые общие отправки должны использовать presentation. |
| Feishu | Интерактивные карточки | Заголовок карточки может использовать title; основная часть не дублирует этот заголовок. |
| Matrix | Резервный текст и структурированное поле события | Кнопки и варианты выбора объявлены как поддерживаемые, но сейчас каждый блок визуализируется как вывод renderMessagePresentationFallbackText, передаваемый в поле события com.openclaw.presentation, а не как нативные интерактивные виджеты. |
| Mattermost | Текст и интерактивные свойства | Варианты выбора и разделители не поддерживаются; эти блоки преобразуются в текст. |
| Microsoft Teams | Adaptive Cards | Обычный текст message включается вместе с карточкой, если предоставлены оба варианта. Выбор, стили и отключённое состояние не поддерживаются. |
| Slack | Block Kit | Визуализирует chart как нативный data_visualization, а table — как нативный data_table; сохраняет устаревший channelData.slack.blocks, но новые общие отправки должны использовать presentation. |
| Telegram | Текст и встроенные клавиатуры | Для кнопок и вариантов выбора целевая поверхность должна поддерживать встроенные кнопки; в противном случае используется резервный текст. |
| Простые каналы | Резервный текст | Каналы без средства визуализации всё равно получают читаемый вывод. |
Совместимость с нативными полезными нагрузками провайдера — это переходная возможность для существующих источников ответов. Она не является основанием для добавления новых общих нативных полей.
Представление и InteractiveReply
InteractiveReply — более старое внутреннее подмножество, используемое вспомогательными средствами
подтверждения и взаимодействия. Оно поддерживает:
- текст
- кнопки
- варианты выбора
MessagePresentation — канонический общий контракт отправки. Он добавляет:
- заголовок
- тон
- контекст
- разделитель
- диаграмму
- таблицу
- кнопки только с URL
- универсальные метаданные доставки через
ReplyPayload.delivery
При адаптации старого кода используйте вспомогательные средства из
openclaw/plugin-sdk/interactive-runtime:
adaptMessagePresentationForChannel, applyPresentationActionLimits, hasMessagePresentationBlocks, interactiveReplyToPresentation, isMessagePresentationInteractiveBlock, normalizeMessagePresentation, presentationPageSize, presentationToInteractiveControlsReply, presentationToInteractiveReply, renderMessagePresentationChartFallbackText, renderMessagePresentationFallbackText, renderMessagePresentationTableFallbackText, resolveMessagePresentationActionValue, resolveMessagePresentationButtonAction, resolveMessagePresentationControlValue, resolveMessagePresentationOptionAction,} from "openclaw/plugin-sdk/interactive-runtime";Новый код должен напрямую принимать или создавать MessagePresentation. Существующие
полезные нагрузки interactive являются устаревшим подмножеством presentation; поддержка
во время выполнения сохраняется для старых источников.
Неустаревшие вспомогательные средства, о которых полезно знать:
normalizeMessagePresentation(raw)/hasMessagePresentationBlocks(value)проверяют и преобразуют нетипизированную полезную нагрузку (например, JSON из флага CLI--presentation) вMessagePresentation.isMessagePresentationInteractiveBlock(block)сужает тип блока до объединенияbuttons|select.resolveMessagePresentationButtonAction(button)иresolveMessagePresentationOptionAction(option)возвращают каноническое типизированное действие, принимая устаревшие граничные поля. Явно заданныйactionвсегда имеет приоритет.resolveMessagePresentationActionValue(action)/resolveMessagePresentationControlValue(control)считывают только скалярные значения команд и обратных вызовов. Нескалярное каноническое действие никогда не переходит к устаревшему теневомуvalue, поэтому идентификаторы подтверждений и цели ссылок сохраняют свои типы.renderMessagePresentationChartFallbackText(block)/renderMessagePresentationTableFallbackText(block)преобразуют один блок структурированных данных в детерминированный текст для резервных путей конкретного канала.
Устаревшие типы InteractiveReply* и вспомогательные средства преобразования помечены
в SDK как @deprecated:
InteractiveReply,InteractiveReplyBlock,InteractiveReplyButton,InteractiveReplyOption,InteractiveReplySelectBlockиInteractiveReplyTextBlocknormalizeInteractiveReply(...)hasInteractiveReplyBlocks(...)interactiveReplyToPresentation(...)presentationToInteractiveReply(...)presentationToInteractiveControlsReply(...)resolveInteractiveTextFallback(...)reduceInteractiveReply(...)
presentationToInteractiveReply(...) и
presentationToInteractiveControlsReply(...) остаются доступными как мосты рендеринга
для устаревших реализаций каналов. Новый код-производитель не должен их вызывать;
отправляйте presentation, а адаптация ядра/канала выполнит рендеринг.
Для вспомогательных функций подтверждения также есть замены, ориентированные прежде всего на представление:
- используйте
buildApprovalPresentationFromActionDescriptors(...)вместоbuildApprovalInteractiveReplyFromActionDescriptors(...) - используйте
buildApprovalPresentation(...)вместоbuildApprovalInteractiveReply(...) - используйте
buildExecApprovalPresentation(...)вместоbuildExecApprovalInteractiveReply(...)
Эти поставляемые построители по-прежнему используют команды для совместимости с плагинами. Код Gateway
и встроенных каналов, владеющий устойчивым типом подтверждения, должен использовать
buildTypedApprovalPresentation(...),
buildTypedExecApprovalPendingReplyPayload(...) или
buildTypedPluginApprovalPendingReplyPayload(...), чтобы транспортные реализации получали
явное действие approval, а не выводили семантику из текста /approve.
renderMessagePresentationFallbackText(...) возвращает пустую строку для
блоков представления без текстового резервного варианта, например представления,
состоящего только из разделителя. Транспортные реализации, которым требуется непустое тело отправки, могут передать
emptyFallback, чтобы явно включить минимальное тело без изменения стандартного контракта
резервного варианта.
Закрепление при доставке
Закрепление относится к поведению доставки, а не к представлению. Используйте delivery.pin вместо
нативных полей провайдера, таких как channelData.telegram.pin.
Семантика:
pin: trueзакрепляет первое успешно доставленное сообщение.pin.notifyпо умолчанию имеет значениеfalse.pin.requiredпо умолчанию имеет значениеfalse.- Необязательное закрепление при сбое не прерывает доставку и оставляет отправленное сообщение без изменений.
- Сбой обязательного закрепления приводит к сбою доставки.
- Для сообщений, разбитых на части, закрепляется первая доставленная часть, а не последняя.
Ручные действия над сообщениями pin, unpin и pins по-прежнему доступны для существующих
сообщений, если провайдер поддерживает эти операции.
Контрольный список автора плагина
- Объявите
presentationизdescribeMessageTool(...), если канал может отображать семантическое представление или безопасно переходить к упрощённому варианту. - Добавьте
presentationCapabilitiesв исходящий адаптер среды выполнения. - Реализуйте
renderPresentationв коде среды выполнения, а не в коде настройки плагина плоскости управления. - Не используйте нативные библиотеки пользовательского интерфейса в горячих путях настройки и каталога.
- Объявите общие ограничения возможностей в
presentationCapabilities.limits, если они известны. - Сохраняйте итоговые ограничения платформы в рендерере и тестах.
- Добавьте тесты резервного варианта для неподдерживаемых диаграмм, таблиц, кнопок, списков выбора, URL-кнопок,
дублирования заголовка и текста, а также смешанных отправок
messageиpresentation. - Добавляйте поддержку закрепления при доставке через
deliveryCapabilities.pinиpinDeliveredMessageтолько тогда, когда провайдер может закрепить идентификатор отправленного сообщения. - Не предоставляйте новые нативные поля карточек, блоков, компонентов или кнопок провайдера через общую схему действий над сообщениями.