Skip to content

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 时,核心执行:

  1. 规范化 presentation 负载。
  2. 解析目标频道的出站适配器。
  3. 读取 presentationCapabilities
  4. 当适配器声明了限制时,应用通用能力限制(动作数量、标签长度、选择选项数等)。
  5. 当适配器能渲染负载时,调用 renderPresentation
  6. 当适配器不存在或无法渲染时,回退到保守文本。
  7. 通过正常频道交付路径发送结果负载。
  8. 在首次成功发送后应用交付元数据,如 delivery.pin

核心拥有回退行为,生产者可以保持频道无关。频道插件拥有原生渲染和交互处理。

降级规则

Presentation 必须能在受限频道上安全发送。

回退文本包括:

  • title 作为第一行
  • text 块作为普通段落
  • context 块作为紧凑上下文行
  • divider 块作为视觉分隔线
  • 按钮标签(链接按钮包含 URL)
  • 选择菜单选项标签

不支持的本地控件应降级而不是导致整个发送失败。示例:

  • Telegram 内联按钮禁用时发送文本回退。
  • 不支持选择的频道将选项列表展示为文本。
  • URL 按钮降级为原生链接按钮或超链接行。
  • 可选的固定失败不会导致消息发送失败。

主要例外是 delivery.pin.required: true:如果固定被标记为必需且频道无法固定已发送消息,则交付失败。

提供商映射

当前内置渲染器:

频道原生渲染目标备注
Discord组件及组件容器保留旧版 channelData.discord.components,但新建共享发送应使用 presentation
SlackBlock Kit保留旧版 channelData.slack.blocks,但新建共享发送应使用 presentation
Telegram文本 + 内联键盘按钮/选择需要目标表面支持内联按钮能力,否则使用文本回退。
Mattermost文本 + 交互属性其他块降级为文本。
Microsoft TeamsAdaptive 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, InteractiveReplyTextBlock
  • normalizeInteractiveReply(...)
  • 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
  • 可选的固定失败降级,保留已发送消息。
  • 必需的固定失败会导致交付失败。
  • 分块消息只固定第一个交付块,不是尾块。

手动 pinunpinpins 消息动作仍然用于旧消息(如果提供商支持这些操作)。

插件作者检查清单

  • 当频道能渲染或安全降级语义 presentation 时,在 describeMessageTool(...) 中声明 presentation
  • 在运行时出站适配器中添加 presentationCapabilities
  • 在运行时代码中实现 renderPresentation,不要在控制平面插件设置代码中实现。
  • 避免将原生 UI 库放在热设置/目录路径中。
  • presentationCapabilities.limits 中声明已知的通用能力限制。
  • 在渲染器和测试中保留最终的平台限制。
  • 为不支持的按钮、选择、URL 按钮、标题/文本重复以及混合的 message + presentation 发送添加回退测试。
  • 仅当提供商能固定已发送消息ID时,通过 deliveryCapabilities.pinpinDeliveredMessage 添加交付固定支持。
  • 不要在共享消息动作 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.requiredtrue 且固定失败,整个发送会失败。对于分块消息,只有第一个块会被固定。