Appearance
OpenClaw 消息呈现(Message Presentation)是智能体、CLI 命令和频道插件共享的富文本输出契约。它让你在任意频道上使用统一的语义结构:文本段落、小字上下文、分割线、按钮、选择菜单、卡片标题与色调。频道插件负责转换成原生组件。降级规则保证即使在功能受限的频道上也能安全输出可读文本。CLI 命令 openclaw message send --presentation 可直接发送富消息。
OpenClaw 消息呈现配置指南:卡片、按钮、选择菜单与富文本
契约
插件作者从以下路径引入公开类型:
ts
import type {
MessagePresentation,
ReplyPayloadDelivery,
} from "openclaw/plugin-sdk/interactive-runtime";类型定义:
ts
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 MessagePresentationButton = {
label: string;
value?: string;
url?: string;
webApp?: { url: string };
/** @deprecated Use webApp. Accepted for legacy JSON payloads only. */
web_app?: { url: string };
priority?: number;
disabled?: boolean;
reusable?: boolean;
style?: "primary" | "secondary" | "success" | "danger";
};
type MessagePresentationOption = {
label: string;
value: string;
};
type ReplyPayloadDelivery = {
pin?:
| boolean
| {
enabled: boolean;
notify?: boolean;
required?: boolean;
};
};按钮语义:
value是应用动作值,通过频道的现有交互路径回传。url是链接按钮,可与value共存或单独使用。webApp描述频道原生 Web App 按钮。Telegram 渲染为web_app,仅支持私聊。向后兼容保留web_app,但 TypeScript 生产者应使用webApp。label必填,同时用作文本回退。style是建议性字段,渲染器应将不支持的 style 映射到安全默认值,不抛错。priority可选。当频道有动作数量上限且必须丢弃控件时,核心保留优先级更高的按钮,同优先级按钮保持原始顺序。所有控件都能放下时按作者顺序显示。disabled可选。频道必须声明supportsDisabled才能支持禁用控件;否则核心将禁用控件降级为非交互回退文本。reusable可选。支持可复用原生回调的频道可在交互成功后保持动作可用。用于可重复或幂等动作(如刷新、检查、更多详情);标准的一次性审批和破坏性动作不要设置。
选择菜单语义:
options[].value是选中后的应用值。placeholder是建议性字段,频道不支持选择时忽略。- 如果频道不支持选择,回退文本会列出选项标签。
生产者示例
简单卡片:
json
{
"title": "Deploy approval",
"tone": "warning",
"blocks": [
{ "type": "text", "text": "Canary is ready to promote." },
{ "type": "context", "text": "Build 1234, staging passed." },
{
"type": "buttons",
"buttons": [
{ "label": "Approve", "value": "deploy:approve", "style": "success" },
{ "label": "Decline", "value": "deploy:decline", "style": "danger" }
]
}
]
}仅 URL 的链接按钮:
json
{
"blocks": [
{ "type": "text", "text": "Release notes are ready." },
{
"type": "buttons",
"buttons": [{ "label": "Open notes", "url": "https://example.com/release" }]
}
]
}Telegram Mini App 按钮:
json
{
"blocks": [
{
"type": "buttons",
"buttons": [{ "label": "Launch", "web_app": { "url": "https://example.com/app" } }]
}
]
}选择菜单:
json
{
"title": "Choose environment",
"blocks": [
{
"type": "select",
"placeholder": "Environment",
"options": [
{ "label": "Canary", "value": "env:canary" },
{ "label": "Production", "value": "env:prod" }
]
}
]
}CLI 发送命令:
bash
openclaw message send --channel slack \
--target channel:C123 \
--message "Deploy approval" \
--presentation '{"title":"Deploy approval","tone":"warning","blocks":[{"type":"text","text":"Canary is ready."},{"type":"buttons","buttons":[{"label":"Approve","value":"deploy:approve","style":"success"},{"label":"Decline","value":"deploy:decline","style":"danger"}]}]}'固定消息(已钉选):
bash
openclaw message send --channel telegram \
--target -1001234567890 \
--message "Topic opened" \
--pin显式 JSON 固定参数:
json
{
"pin": {
"enabled": true,
"notify": true,
"required": false
}
}渲染器契约
频道插件在出站适配器上声明渲染支持:
ts
const adapter: ChannelOutboundAdapter = {
deliveryMode: "direct",
presentationCapabilities: {
supported: true,
buttons: true,
selects: true,
context: true,
divider: true,
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 描述通用边界,核心会在调用渲染器前进行适配:
ts
type ChannelPresentationCapabilities = {
supported?: boolean;
buttons?: boolean;
selects?: boolean;
context?: boolean;
divider?: 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 限制等)。如果限制移除块中的所有控件,核心会保留标签作为非交互上下文文本,确保交付消息仍有可见回退。
核心渲染流程
当 ReplyPayload 或消息动作包含 presentation 时,核心执行:
- 规范化 presentation 负载。
- 解析目标频道的出站适配器。
- 读取
presentationCapabilities。 - 当适配器声明了限制时,应用通用能力限制(动作数量、标签长度、选择选项数等)。
- 当适配器能渲染负载时,调用
renderPresentation。 - 当适配器不存在或无法渲染时,回退到保守文本。
- 通过正常频道交付路径发送结果负载。
- 在首次成功发送后应用交付元数据,如
delivery.pin。
核心拥有回退行为,生产者可以保持频道无关。频道插件拥有原生渲染和交互处理。
降级规则
Presentation 必须能在受限频道上安全发送。
回退文本包括:
title作为第一行text块作为普通段落context块作为紧凑上下文行divider块作为视觉分隔线- 按钮标签(链接按钮包含 URL)
- 选择菜单选项标签
不支持的本地控件应降级而不是导致整个发送失败。示例:
- Telegram 内联按钮禁用时发送文本回退。
- 不支持选择的频道将选项列表展示为文本。
- URL 按钮降级为原生链接按钮或超链接行。
- 可选的固定失败不会导致消息发送失败。
主要例外是 delivery.pin.required: true:如果固定被标记为必需且频道无法固定已发送消息,则交付失败。
提供商映射
当前内置渲染器:
| 频道 | 原生渲染目标 | 备注 |
|---|---|---|
| Discord | 组件及组件容器 | 保留旧版 channelData.discord.components,但新建共享发送应使用 presentation。 |
| Slack | Block Kit | 保留旧版 channelData.slack.blocks,但新建共享发送应使用 presentation。 |
| Telegram | 文本 + 内联键盘 | 按钮/选择需要目标表面支持内联按钮能力,否则使用文本回退。 |
| Mattermost | 文本 + 交互属性 | 其他块降级为文本。 |
| Microsoft Teams | Adaptive Cards | 如果同时提供 message 和 card,纯文本 message 会一并包含在卡片中。 |
| Feishu | 交互卡片 | 卡片标题可以使用 title,正文避免重复标题。 |
| 纯文本频道 | 文本回退 | 没有渲染器的频道仍然能获得可读输出。 |
提供商原生负载兼容性是为现有 reply 生产者提供的过渡便利,不是添加新的共享原生字段的理由。
Presentation 与 InteractiveReply 的对比
InteractiveReply 是审批和交互辅助函数中使用的旧版内部子集。支持:
- text
- buttons
- selects
MessagePresentation 是规范的共享发送契约。增加:
- title
- tone
- context
- divider
- 仅 URL 按钮
- 通过
ReplyPayload.delivery的通用交付元数据
桥接旧代码时使用 openclaw/plugin-sdk/interactive-runtime 中的辅助函数:
ts
import {
adaptMessagePresentationForChannel,
applyPresentationActionLimits,
interactiveReplyToPresentation,
normalizeMessagePresentation,
presentationPageSize,
presentationToInteractiveControlsReply,
presentationToInteractiveReply,
renderMessagePresentationFallbackText,
} from "openclaw/plugin-sdk/interactive-runtime";新代码应直接接受或生成 MessagePresentation。现有的 interactive 负载是 presentation 的已弃用子集;运行时会保持对旧生产者的支持。
旧版 InteractiveReply* 类型和转换辅助函数在 SDK 中标记为 @deprecated:
InteractiveReply,InteractiveReplyBlock,InteractiveReplyButton,InteractiveReplyOption,InteractiveReplySelectBlock,InteractiveReplyTextBlocknormalizeInteractiveReply(...)hasInteractiveReplyBlocks(...)interactiveReplyToPresentation(...)presentationToInteractiveReply(...)presentationToInteractiveControlsReply(...)resolveInteractiveTextFallback(...)reduceInteractiveReply(...)
presentationToInteractiveReply(...) 和 presentationToInteractiveControlsReply(...) 仍然可作为渲染器桥接函数用于旧版频道实现。新生产者代码不应调用它们;发送 presentation,让核心/频道适配处理渲染。
审批辅助函数也有 presentation-first 的替换:
- 使用
buildApprovalPresentationFromActionDescriptors(...)替代buildApprovalInteractiveReplyFromActionDescriptors(...) - 使用
buildApprovalPresentation(...)替代buildApprovalInteractiveReply(...) - 使用
buildExecApprovalPresentation(...)替代buildExecApprovalInteractiveReply(...)
renderMessagePresentationFallbackText(...) 对于没有文本回退的 presentation 块(如仅分割线)返回空字符串。要求非空发送体的传输可以传递 emptyFallback 来使用最小体,同时不改变默认回退契约。
交付固定(Pin)
固定是交付行为,不是 presentation。使用 delivery.pin 而不是提供商原生字段(如 channelData.telegram.pin)。
语义:
pin: true固定第一个成功交付的消息。pin.notify默认为false。pin.required默认为false。- 可选的固定失败降级,保留已发送消息。
- 必需的固定失败会导致交付失败。
- 分块消息只固定第一个交付块,不是尾块。
手动 pin、unpin、pins 消息动作仍然用于旧消息(如果提供商支持这些操作)。
插件作者检查清单
- 当频道能渲染或安全降级语义 presentation 时,在
describeMessageTool(...)中声明presentation。 - 在运行时出站适配器中添加
presentationCapabilities。 - 在运行时代码中实现
renderPresentation,不要在控制平面插件设置代码中实现。 - 避免将原生 UI 库放在热设置/目录路径中。
- 在
presentationCapabilities.limits中声明已知的通用能力限制。 - 在渲染器和测试中保留最终的平台限制。
- 为不支持的按钮、选择、URL 按钮、标题/文本重复以及混合的
message+presentation发送添加回退测试。 - 仅当提供商能固定已发送消息ID时,通过
deliveryCapabilities.pin和pinDeliveredMessage添加交付固定支持。 - 不要在共享消息动作 schema 中暴露新的提供商原生卡片/块/组件/按钮字段。
相关文档
常见问题
为什么我发送的 presentation 在频道上只显示纯文本,没有按钮?
检查频道插件的出站适配器是否声明了 presentationCapabilities.buttons: true。如果没有,核心会回退到文本。另外确认 --presentation JSON 是否符合类型定义,以及频道内联按钮能力未禁用。
如何支持禁用按钮(disabled)?
频道必须在其出站适配器的 presentationCapabilities.limits.actions 中设置 supportsDisabled: true。如果设置为 false 或未设置,核心会将 disabled 按钮降级为非交互回退文本。
固定消息(pin)在 Telegram 上不生效怎么办?
确保你使用 openclaw message send --pin 或者 delivery.pin 字段。Telegram 要求机器人有对应频道的 pin 权限。如果 pin.required 为 true 且固定失败,整个发送会失败。对于分块消息,只有第一个块会被固定。