Skip to content

OpenClaw 将消息 UI 从通道原生格式(Discord components、Slack blocks、Teams Adaptive Cards)统一为语义化的 presentation 字段。核心模型包含 tone、title、blocks(text/context/divider/buttons/select),并自动降级不支持的元素为文本。通道插件通过 renderPresentationpresentationCapabilities 声明能力,delivery.pin 替代了原有的 channelData.telegram.pin。当前重构已实现共享智能体、CLI、插件出站能力及投递面,但 channelData 中私有传输元数据仍待清理。如需调试 Discord 导入扩散或通道插件运行时惰性加载,请参考本计划。

OpenClaw 消息展示重构计划:理解 presentation 字段与通道渲染

状态

重构已实现于共享智能体、CLI、插件能力及出站投递面:

  • ReplyPayload.presentation 携带语义化消息 UI。
  • ReplyPayload.delivery.pin 携带发送消息的置顶请求。
  • 共享消息操作使用 presentationdeliverypin 替代了 provider 原生的 componentsblocksbuttonscard
  • 核心层根据插件声明的出站能力自动渲染或降级 presentation。
  • Discord、Slack、Telegram、Mattermost、MS Teams、Feishu 的渲染器已消费通用契约。
  • Discord 通道控制面代码不再导入 Carbon 驱动的 UI 容器。

规范文档现已移至 Message Presentation。将本计划作为历史实现上下文;契约、渲染器或降级行为变更时请更新规范指南。

问题

当前通道 UI 分散在多个不兼容的表面:

  • 核心通过 buildCrossContextComponents 持有一个 Discord 形状的跨上下文渲染钩子。
  • Discord channel.ts 可以通过 DiscordUiContainer 导入原生 Carbon UI,从而将运行时 UI 依赖拉入通道插件控制面。
  • 智能体和 CLI 暴露原生负载逃生口(如 Discord components、Slack blocks、Telegram / Mattermost buttons、Teams / Feishu card)。
  • ReplyPayload.channelData 既携带传输提示也携带原生 UI 信封。
  • 泛型 interactive 模型存在,但比 Discord、Slack、Teams、Feishu、LINE、Telegram、Mattermost 已使用的更丰富布局窄。

这使核心层感知了原生 UI 形状,削弱了插件运行时惰性,并给智能体太多 provider 特定的表达相同消息意图的方式。

目标

  • 核心根据声明的能力决定消息的最佳语义展示。
  • 扩展声明能力并将语义展示渲染为原生传输负载。
  • Web 控制 UI 与聊天原生 UI 保持分离。
  • 通过共享智能体或 CLI 消息面不暴露原生通道负载。
  • 不支持的展示特性自动降级为最佳文本表示。
  • 发送行为(如置顶已发消息)是通用投递元数据,而非展示。

非目标

  • 不为 buildCrossContextComponents 提供向后兼容 shim。
  • 不公开 componentsblocksbuttonscard 的原生逃生口。
  • 不导入通道原生 UI 库到核心。
  • 不提供捆绑通道的 provider 特定 SDK seam。

目标模型

ReplyPayload 中添加核心拥有的 presentation 字段。

ts
type MessagePresentationTone = "neutral" | "info" | "success" | "warning" | "danger";

type MessagePresentation = {
  tone?: MessagePresentationTone;
  title?: string;
  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;
  style?: "primary" | "secondary" | "success" | "danger";
};

type MessagePresentationOption = {
  label: string;
  value: string;
};

interactive 在迁移期间成为 presentation 的子集:

  • interactive 的 text block 映射到 presentation.blocks[].type = "text"
  • interactive 的 buttons block 映射到 presentation.blocks[].type = "buttons"
  • interactive 的 select block 映射到 presentation.blocks[].type = "select"

外部智能体和 CLI schema 现在使用 presentationinteractive 作为现有回复生产者的内部遗留解析/渲染辅助保留。公开的生产者面向 API 将 interactive 视为已弃用。运行时支持继续存在,使现有的审批辅助和旧插件正常工作,同时新代码使用 presentation

投递元数据

添加核心拥有的 delivery 字段用于非 UI 的发送行为。

ts
type ReplyPayloadDelivery = {
  pin?:
    | boolean
    | {
        enabled: boolean;
        notify?: boolean;
        required?: boolean;
      };
};

语义:

  • delivery.pin = true 表示置顶第一条成功投递的消息。
  • notify 默认值 false
  • required 默认值 false;不支持的通道或置顶失败自动降级(继续投递)。
  • 手动 pinunpinlist-pins 消息操作对现有消息保留。

当前 Telegram ACP 主题绑定应从 channelData.telegram.pin = true 迁移到 delivery.pin = true

运行时能力契约

将展示和投递渲染钩子添加到运行时出站适配器,而非控制面通道插件。

ts
type ChannelPresentationCapabilities = {
  supported: boolean;
  buttons?: boolean;
  selects?: boolean;
  context?: boolean;
  divider?: boolean;
  tones?: MessagePresentationTone[];
  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;
    };
  };
};

type ChannelDeliveryCapabilities = {
  pinSentMessage?: boolean;
};

type ChannelOutboundAdapter = {
  presentationCapabilities?: ChannelPresentationCapabilities;

  renderPresentation?: (params: {
    payload: ReplyPayload;
    presentation: MessagePresentation;
    ctx: ChannelOutboundSendContext;
  }) => ReplyPayload | null;

  deliveryCapabilities?: ChannelDeliveryCapabilities;

  pinDeliveredMessage?: (params: {
    cfg: OpenClawConfig;
    accountId?: string | null;
    to: string;
    threadId?: string | number | null;
    messageId: string;
    notify: boolean;
  }) => Promise<void>;
};

核心行为:

  • 解析目标通道和运行时适配器。
  • 询问展示能力。
  • 在渲染前降级不支持的块并应用通用能力限制。
  • 调用 renderPresentation
  • 如果没有渲染器,将 presentation 转换为文本回退。
  • 成功发送后,若 delivery.pin 请求且支持,调用 pinDeliveredMessage

通道映射

Discord

  • 在运行时模块中将 presentation 渲染为 components v2 和 Carbon 容器。
  • 将强调色辅助保留在轻模块中。
  • 从通道插件控制面代码中移除 DiscordUiContainer 的导入。

Slack

  • presentation 渲染为 Block Kit。
  • 移除智能体和 CLI 中的 blocks 输入。

Telegram

  • 文本、context、divider 渲染为文本。
  • 当配置且允许目标表面时,actions 和 select 渲染为内联键盘。
  • 内联按钮禁用时使用文本回退。
  • ACP 主题置顶迁移到 delivery.pin

Mattermost

  • 当配置时,actions 渲染为交互按钮。
  • 其他块使用文本回退。

MS Teams

  • presentation 渲染为 Adaptive Cards。
  • 保留手动 pin / unpin / list-pins 操作。
  • 可选:如果目标会话的 Graph 支持可靠,实现 pinDeliveredMessage

Feishu

  • presentation 渲染为交互卡片。
  • 保留手动 pin / unpin / list-pins 操作。
  • 可选:如果 API 行为可靠,实现 pinDeliveredMessage

LINE

  • 在可能的情况下将 presentation 渲染为 Flex 或模板消息。
  • 不支持的块回退为文本。
  • channelData 移除 LINE UI 负载。

简单或受限通道

  • 将 presentation 转换为带有保守格式的文本。

重构步骤

  1. 重新应用 Discord 发布修复:将 ui-colors.ts 从 Carbon 驱动的 UI 拆分,并从 extensions/discord/src/channel.ts 移除 DiscordUiContainer
  2. presentationdelivery 添加到 ReplyPayload、出站负载归一化、投递摘要和钩子负载。
  3. 在狭窄的 SDK/运行时子路径中添加 MessagePresentation schema 和解析辅助。
  4. 用语义展示能力替换消息能力 buttonscardscomponentsblocks
  5. 添加运行时出站适配器钩子用于展示渲染和投递置顶。
  6. buildCrossContextPresentation 替换跨上下文组件构建。
  7. 删除 src/infra/outbound/channel-adapters.ts 并从通道插件类型中移除 buildCrossContextComponents
  8. maybeApplyCrossContextMarker 改为附加 presentation 而非原生参数。
  9. 更新插件调度发送路径,仅消费语义展示和投递元数据。
  10. 移除智能体和 CLI 原生的负载参数:componentsblocksbuttonscard
  11. 移除创建原生消息工具 schema 的 SDK 辅助,替换为展示 schema 辅助。
  12. channelData 移除 UI/原生信封;只保留传输元数据,直到每个剩余字段被审查。
  13. 迁移 Discord、Slack、Telegram、Mattermost、MS Teams、Feishu、LINE 的渲染器。
  14. 更新文档:消息 CLI、通道页面、插件 SDK、能力指南。
  15. 对 Discord 和相关通道入口点运行导入扩散分析。

步骤 1-11 和 13-14 已在此重构中为共享智能体、CLI、插件能力和出站适配器契约实现。步骤 12 仍是一个更深的内部清理步骤,用于处理 provider 私有的 channelData 传输信封。步骤 15 仍是一个后续验证,如果我们希望获得超出类型/测试门控的量化导入扩散数据。

测试

添加或更新:

  • presentation 归一化测试。
  • presentation 自动降级测试(针对不支持的块)。
  • 跨上下文标记测试(插件调度和核心投递路径)。
  • 通道渲染矩阵测试:Discord、Slack、Telegram、Mattermost、MS Teams、Feishu、LINE 以及文本回退。
  • 消息工具 schema 测试,证明原生字段已移除。
  • CLI 测试,证明原生标志已移除。
  • Discord 入口点导入惰性回归测试(涉及 Carbon)。
  • 投递置顶测试(Telegram 和通用回退)。

未决问题

  • 第一遍是否应为 Discord、Slack、MS Teams、Feishu 实现 delivery.pin,还是仅 Telegram 优先?
  • delivery 是否应最终吸收现有字段(如 replyToIdreplyToCurrentsilentaudioAsVoice),还是保持只关注发送后行为?
  • 展示是否应直接支持图片或文件引用,还是媒体应暂时保持独立于 UI 布局?

相关链接

常见问题

我如何知道我的通道插件是否已使用新的 presentation 模型?

检查通道插件是否实现了 renderPresentationpresentationCapabilitiesChannelOutboundAdapter 中。如果没有,通道仍在渲染原生负载(如 componentsblocks),应尽快迁移。

delivery.pin 不生效怎么办?

首先确认通道能力声明 pinSentMessage: true。然后检查 delivery.pin 是否设置为 true(或 { enabled: true })。Telegram 的 ACP 主题置顶已迁移至此字段;对于其他通道(Discord、Slack 等),需视平台 API 支持情况实现 pinDeliveredMessage。如果不支持,核心会静默降级。

我想在智能体回复中使用 buttons 或 selects,该用什么 schema?

使用 ReplyPayload.presentation.blocks,其中包含 type: "buttons"type: "select"。不要再用旧的 interactive 或原生字段(componentsblockscard)。旧 schema 已被弃用但仍支持。