跳转到内容

渠道呈现重构计划

已针对共享代理、CLI、插件功能和出站交付表面实现:

  • ReplyPayload.presentation 承载语义消息 UI。
  • ReplyPayload.delivery.pin 承载已发送消息的固定请求。
  • 共享消息操作暴露 presentationdeliverypin,而不是提供商原生的 componentsblocksbuttonscard
  • 核心通过插件声明的出站功能来渲染或自动降级呈现。
  • 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 形状,削弱了插件运行时的延迟加载,并为代理提供了太多特定于提供商的方式来表达相同的消息意图。

  • 核心根据声明的功能决定消息的最佳语义呈现。
  • 扩展声明能力并将语义呈现渲染为原生传输负载。
  • Web 控制界面与聊天原生界面保持分离。
  • 原生渠道负载不会通过共享代理或 CLI 消息表面暴露。
  • 不支持的呈现功能会自动降级为最佳文本表示形式。
  • 置入已发送消息等传递行为属于通用传递元数据,而非呈现。
  • 不为 buildCrossContextComponents 提供向后兼容的填充层。
  • 不为 componentsblocksbuttonscard 提供公共的原生逃生舱。
  • 核心不导入渠道原生的 UI 库。
  • 没有针对打包渠道的特定于提供商的 SDK 缝隙。

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

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 文本块映射到 presentation.blocks[].type = "text"
  • interactive 按钮块映射到 presentation.blocks[].type = "buttons"
  • interactive 选择块映射到 presentation.blocks[].type = "select"

外部代理和 CLI 模式现在使用 presentationinteractive 仍然是现有回复生成器的内部旧版解析/渲染辅助工具。 面向生成器的公共 API 将 interactive 视为已弃用。运行时 支持仍然保留,以便现有的审批辅助工具和旧版插件继续 工作,同时新代码发出 presentation

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

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

将呈现和交付渲染挂钩添加到运行时出站适配器,而不是控制平面渠道插件。

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
  • 如果不存在渲染器,则将呈现转换为文本后备。
  • 成功发送后,当请求并支持 delivery.pin 时,调用 pinDeliveredMessage

Discord:

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

Slack:

  • presentation 渲染为 Block Kit。
  • 移除代理和 CLI blocks 输入。

Telegram:

  • 将文本、上下文和分隔线渲染为文本。
  • 在为目标界面配置并允许的情况下,将操作和选择渲染为内联键盘。
  • 当禁用内联按钮时使用文本后备。
  • 将 ACP 主题固定移动到 delivery.pin

Mattermost:

  • 在配置的位置将操作渲染为交互式按钮。
  • 将其他块渲染为文本后备。

MS Teams:

  • presentation 渲染为 Adaptive Cards。
  • 保留手动固定/取消固定/列出固定操作。
  • 如果 Graph 支持对目标对话可靠,则可选择实现 pinDeliveredMessage

Feishu:

  • presentation 渲染为交互式卡片。
  • 保留手动固定/取消固定/列出固定操作。
  • 如果 API 行为可靠,可选择实现 pinDeliveredMessage 以进行发送消息的固定。

LINE:

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

纯文本或受限渠道:

  • 将呈现转换为具有保守格式的文本。
  1. 重新应用 Discord 发布修复程序,将 ui-colors.ts 与基于 Carbon 的 UI 分离,并从 extensions/discord/src/channel.ts 中移除 DiscordUiContainer
  2. presentationdelivery 添加到 ReplyPayload、出站负载规范化、传递摘要和 hook 负载中。
  3. 在狭窄的 SDK/运行时子路径中添加 MessagePresentation schema 和解析器辅助程序。
  4. 用语义展示能力替换消息能力 buttonscardscomponentsblocks
  5. 添加用于呈现渲染和传递固定的运行时出站适配器钩子。
  6. buildCrossContextPresentation 替换跨上下文组件构造。
  7. 删除 src/infra/outbound/channel-adapters.ts 并从渠道插件类型中移除 buildCrossContextComponents
  8. 更改 maybeApplyCrossContextMarker 以附加 presentation 而不是原生参数。
  9. 更新插件分派发送路径,使其仅消耗语义呈现和传递元数据。
  10. 移除 agent 和 CLI 原生负载参数:componentsblocksbuttonscard
  11. 移除创建原生消息工具架构的 SDK 辅助函数,用呈现架构辅助函数替换它们。
  12. channelData 中移除 UI/原生信封;在审查每个剩余字段之前,仅保留传输元数据。
  13. 迁移 Discord、Slack、Telegram、Mattermost、MS Teams、飞书和 LINE 渲染器。
  14. 更新消息 CLI、渠道页面、插件 SDK 和能力手册的文档。
  15. 针对 Discord 和受影响的渠道入口点运行导入扩展分析。

步骤 1-11 和 13-14 已在此重构中为共享 agent、CLI、插件能力和出站适配器合同实现。步骤 12 仍然是对提供商私有 channelData 传输信封的更深层内部清理传递。步骤 15 仍然是后续验证,如果我们想要超出类型/测试范围的量化导入扇出数字。

添加或更新:

  • 呈现规范化测试。
  • 针对不受支持块的呈现自动降级测试。
  • 针对插件分派和核心传递路径的跨上下文标记测试。
  • Discord、Slack、Telegram、Mattermost、MS Teams、Feishu、LINE 和文本回退的通道渲染矩阵测试。
  • 消息工具 schema 测试,证明原生字段已移除。
  • CLI 测试,证明原生标志已移除。
  • Discord 入口点导入惰性回归,覆盖 Carbon。
  • 传递固定 测试,覆盖 Telegram 和通用回退。
  • 在第一阶段,是否应该为 Discord、Slack、MS Teams 和飞书实现 delivery.pin,还是仅先为 Telegram 实现?
  • delivery 最终是否应包含 replyToIdreplyToCurrentsilentaudioAsVoice 等现有字段,还是应专注于发送后的行为?
  • 演示是否应该直接支持图片或文件引用,或者媒体目前是否应与 UI 布局保持分离?