Skip to content

开发 OpenClaw 消息通道插件时,message 适配器是核心接口,负责定义消息的完整生命周期:接收、路由、持久化发送、收据、直播预览以及接收确认策略。新代码应使用 channel-message-runtime 中的 sendDurableMessageBatchwithDurableMessageSendContextdeliverInboundReplyWithMessageSendContext 进行发送;旧版 deliverOutboundPayloads 仅用于兼容。适配器需要声明能力(text、replyTo、thread 等),并通过合约测试验证。如果已有 outbound 适配器,可用 createChannelMessageAdapterFromOutbound 桥接,无需重写发送逻辑。

OpenClaw channel message 适配器开发指南

通道插件应从 openclaw/plugin-sdk/channel-message 暴露一个 message 适配器。该适配器描述平台支持的原生消息生命周期:

text
接收 -> 路由并记录 -> 智能体处理 -> 持久化最终发送
发送 -> 渲染批处理 -> 平台 I/O -> 收据 -> 生命周期副作用
直播预览 -> 最终编辑或回退 -> 收据

Core 负责队列、持久化、通用重试策略、钩子、收据和共享的 message 工具。插件负责原生发送/编辑/删除调用、目标归一化、平台线程、选中引用、通知标志、账户状态和平台特定副作用。

请结合 构建通道插件 使用本页。

channel-message 子路径足够轻量,可在热加载的插件引导文件(如 channel.ts)中使用:它暴露适配器合约、能力证明、收据和兼容性外观,而不加载出站发送逻辑。运行时发送助手位于 openclaw/plugin-sdk/channel-message-runtime,适用于已经进行异步消息 I/O 的监控/发送代码路径。

新通道和插件发送代码应使用 channel-message-runtime 中的消息生命周期助手:sendDurableMessageBatchwithDurableMessageSendContextdeliverInboundReplyWithMessageSendContext。旧版 deliverOutboundPayloads(...) 助手在 openclaw/plugin-sdk/outbound-runtime 中,是用于出站内部、恢复和旧适配器的已废弃兼容/运行时底层。新通道或插件发送路径不应使用它。

sendDurableMessageBatch(...) 返回明确的生命周期结果:

  • sent – 至少一个可见的平台消息已投递。
  • suppressed – 不应将无平台消息视为缺失。稳定的原因包括 cancelled_by_message_sending_hookempty_after_message_sending_hookno_visible_payloadadapter_returned_no_identity 和旧版 no_visible_result
  • partial_failed – 至少一个平台消息已投递,但后续有效载荷或副作用失败。结果包含已投递的收据前缀外加失败信息。
  • failed – 未产生任何平台收据。

当批处理中同时包含已发送、已抑制和已失败的有效载荷时,使用 payloadOutcomes。不要通过检查旧版直接投递数组是否为空来推断钩子取消。

仍需缓冲式回复分发器的兼容性分发器应使用 createChannelMessageReplyPipeline(...)(来自 openclaw/plugin-sdk/channel-message)构建回复前缀选项,然后调用运行时的 channel.turn.runPrepared(...)。这样可以在共享的 turn 生命周期上保持会话记录和分发顺序,而无需添加另一个公共 turn 包装器。

最小适配器

大多数新通道插件可以从一个小的适配器开始:

typescript
import {
  defineChannelMessageAdapter,
  createMessageReceiptFromOutboundResults,
} from "openclaw/plugin-sdk/channel-message";

export const demoMessageAdapter = defineChannelMessageAdapter({
  id: "demo",
  durableFinal: {
    capabilities: {
      text: true,
      replyTo: true,
      thread: true,
      messageSendingHooks: true,
    },
  },
  send: {
    text: async ({ cfg, to, text, accountId, replyToId, threadId, signal }) => {
      const sent = await sendDemoMessage({
        cfg,
        to,
        text,
        accountId: accountId ?? undefined,
        replyToId: replyToId ?? undefined,
        threadId: threadId == null ? undefined : String(threadId),
        signal,
      });

      return {
        receipt: createMessageReceiptFromOutboundResults({
          results: [{ channel: "demo", messageId: sent.id, conversationId: to }],
          kind: "text",
          threadId: threadId == null ? undefined : String(threadId),
          replyToId: replyToId ?? undefined,
        }),
      };
    },
  },
});

然后将其附加到通道插件:

typescript
export const demoPlugin = createChatChannelPlugin({
  base: {
    id: "demo",
    message: demoMessageAdapter,
    // 其他通道插件字段
  },
});

只声明适配器实际保留的能力。每个声明的能力都应有一个合约测试。

从 outbound 桥接

如果通道已有兼容的 outbound 适配器,建议派生 message 适配器,而不是重复发送代码:

typescript
import { createChannelMessageAdapterFromOutbound } from "openclaw/plugin-sdk/channel-message";

const demoMessageAdapter = createChannelMessageAdapterFromOutbound({
  id: "demo",
  outbound: demoOutboundAdapter,
});

桥接会将旧的 outbound 发送结果转换为 MessageReceipt 值。新代码应全程传递收据,并仅在兼容性边界使用 listMessageReceiptPlatformIds(...)resolveMessageReceiptPrimaryId(...) 派生旧版 ID。 如果未提供接收策略,createChannelMessageAdapterFromOutbound(...) 使用 manual 接收确认策略。这使插件拥有的平台确认显式化,而不会改变在通用接收上下文之外确认 webhook、socket 或轮询偏移量的通道。

Message 工具发送

共享的 message(action="send") 路径应与最终回复使用相同的核心投递生命周期。如果通道需要针对工具发送进行特定提供商塑造,应在 actions.prepareSendPayload(...) 中实现,而不是在 actions.handleAction(...) 中发送。

prepareSendPayload(...) 接收归一化的核心 ReplyPayload 及完整的动作上下文。返回一个有效载荷,其中通道特定数据放在 payload.channelData.<channel> 中,然后让 core 调用 sendMessage(...)、消息生命周期运行时、预写队列、消息发送钩子、重试、恢复和确认清理。生命周期运行时可能内部调用 deliverOutboundPayloads(...) 作为兼容性底层,但通道插件不应为新发送行为直接调用它。

仅在无法将发送表示为持久化有效载荷时返回 null,例如包含不可序列化的组件工厂。Core 会保留旧版插件动作回退以保持兼容,但新通道发送功能应表示为持久化有效载荷数据。

typescript
export const demoActions: ChannelMessageActionAdapter = {
  describeMessageTool: () => ({ actions: ["send"], capabilities: ["presentation"] }),
  prepareSendPayload: ({ ctx, payload }) => {
    if (ctx.action !== "send") {
      return null;
    }
    return {
      ...payload,
      channelData: {
        ...payload.channelData,
        demo: {
          ...(payload.channelData?.demo as object | undefined),
          nativeCard: ctx.params.card,
        },
      },
    };
  },
};

outbound 适配器随后在 sendPayload 内部读取 payload.channelData.demo。这样既保持了平台特定渲染在插件中,又让 core 继续负责持久化、重试、恢复、钩子和确认。

已准备的 message(action="send") 有效载荷和通用最终回复投递默认使用带尽力而为队列的 core 投递。只有在 core 验证通道能够解决崩溃后发送结果未知的问题后,才启用必需的持久化队列。如果适配器无法实现 reconcileUnknownSend,则保持准备好的发送路径为尽力而为;core 仍会尝试预写队列,但队列持久化或不确定的崩溃恢复不属于必需的投递合约。

持久化最终能力

持久化最终投递是按副作用选择性加入的。Core 仅当适配器声明了有效载荷和投递选项所需的所有能力时,才使用通用持久化投递。

能力何时声明
text适配器可以发送文本并返回收据。
media媒体发送为每个可见平台消息返回收据。
payload适配器保留丰富回复有效载荷语义,不仅仅是文本和一个媒体 URL。
replyTo原生回复目标到达平台。
thread原生线程、话题或频道线程目标到达平台。
silent通知抑制到达平台。
nativeQuote选中的引用元数据到达平台。
messageSendingHooksCore 的消息发送钩子可以在平台 I/O 前取消或重写内容。
batch多部分渲染的批处理可以作为持久化计划重播。
reconcileUnknownSend适配器可以在没有盲目重播的情况下解决 unknown_after_send 恢复问题。
afterSendSuccess通道本地发送后副作用运行一次。
afterCommit通道本地提交后副作用运行一次。

尽力而为的最终投递不需要 reconcileUnknownSend;当适配器保留有效载荷的可见语义时,它使用共享的生命周期,如果队列持久化不可用,则回退到直接平台 I/O。必需的持久化最终投递必须显式要求 reconcileUnknownSend。如果适配器无法确定已开始/未知的发送是否到达平台,则不要声明该能力;Core 会在队列前拒绝必需的持久化投递。

当调用方需要持久化投递时,使用以下方式推导需求,而不是手动构建映射:

typescript
import { deriveDurableFinalDeliveryRequirements } from "openclaw/plugin-sdk/channel-message";

const requiredCapabilities = deriveDurableFinalDeliveryRequirements({
  payload,
  replyToId,
  threadId,
  silent,
  payloadTransport: true,
  extraCapabilities: {
    nativeQuote: hasSelectedQuote(payload),
  },
});

messageSendingHooks 默认是必需的。仅当某个路径有意无法运行全局消息发送钩子时,才设置 messageSendingHooks: false

持久化发送合约

持久化最终发送比旧版通道拥有的投递有更严格的语义:

  • 在平台 I/O 之前创建持久化意图。
  • 如果持久化投递返回已处理的结果,则不要回退到旧版发送。
  • 将钩子取消和无发送结果视为终结。
  • 仅将 unsupported 作为预意图结果处理。
  • 对于必需的持久化,如果队列无法记录平台发送已开始,则在平台 I/O 前失败。
  • 对于必需的最终投递和必需的已准备消息工具发送,预检查 reconcileUnknownSend;恢复必须能够确认已发送的消息,或仅在适配器证明原始发送未发生时重播。
  • 对于 best_effort,队列写入失败可以回退到直接平台 I/O。
  • 将中止信号转发到媒体加载和平台发送。
  • 在队列确认后运行提交后钩子;尽力而为的直接回退在成功平台 I/O 后运行这些钩子,因为没有持久化队列提交。
  • 为每个可见的平台消息 ID 返回收据。
  • 当平台可以检查不确定的发送是否已到达用户时,使用 reconcileUnknownSend

此合约避免了崩溃后的重复发送,并避免绕过消息发送取消钩子。

收据

MessageReceipt 是平台接受的新内部记录:

typescript
type MessageReceipt = {
  primaryPlatformMessageId?: string;
  platformMessageIds: string[];
  parts: MessageReceiptPart[];
  threadId?: string;
  replyToId?: string;
  editToken?: string;
  deleteToken?: string;
  sentAt: number;
  raw?: readonly MessageReceiptSourceResult[];
};

在适配现有发送结果时使用 createMessageReceiptFromOutboundResults(...)。当直播预览消息成为最终收据时,使用 createPreviewMessageReceipt(...)。避免添加新的所有者本地 messageIds 字段。旧版 ChannelDeliveryResult.messageIds 仍然在兼容性边界产生。

直播预览

支持流式草稿预览或进度更新的通道应声明直播能力:

typescript
const demoMessageAdapter = defineChannelMessageAdapter({
  id: "demo",
  live: {
    capabilities: {
      draftPreview: true,
      previewFinalization: true,
      progressUpdates: true,
      quietFinalization: true,
    },
    finalizer: {
      capabilities: {
        finalEdit: true,
        normalFallback: true,
        discardPending: true,
        previewReceipt: true,
        retainOnAmbiguousFailure: true,
      },
    },
  },
});

使用 defineFinalizableLivePreviewAdapter(...)deliverWithFinalizableLivePreviewAdapter(...) 进行运行时终结。终结器决定最终回复是编辑预览原位、发送正常回退、丢弃待定预览状态、在模糊失败编辑后保留而不复制消息,以及返回最终收据。

接收确认策略

控制平台确认时机的入站接收器应声明接收策略:

typescript
const demoMessageAdapter = defineChannelMessageAdapter({
  id: "demo",
  receive: {
    defaultAckPolicy: "after_agent_dispatch",
    supportedAckPolicies: ["after_receive_record", "after_agent_dispatch"],
  },
});

未声明接收策略的适配器默认为:

typescript
{
  receive: {
    defaultAckPolicy: "manual",
    supportedAckPolicies: ["manual"],
  },
}

当平台没有要延迟的确认、已在异步处理前确认,或需要协议特定的响应语义时,使用默认值。仅当接收器确实使用接收上下文将平台确认推迟到稍后阶段时,才声明分阶段策略之一。

策略:

策略使用时机
after_receive_record入站事件被解析并记录后可以确认平台。
after_agent_dispatch平台应等待,直到智能体分发被接受。
after_durable_send平台应等待,直到最终投递有持久化决策。
manual插件拥有确认权,因为平台语义不匹配任何通用阶段。

在延迟确认状态的接收器中使用 createMessageReceiveContext(...),并在接收器需要测试某个阶段是否满足配置的策略时使用 shouldAckMessageAfterStage(...)

合约测试

能力声明是插件合约的一部分。用测试支持它们:

typescript
import {
  verifyChannelMessageAdapterCapabilityProofs,
  verifyChannelMessageLiveCapabilityAdapterProofs,
  verifyChannelMessageLiveFinalizerProofs,
  verifyChannelMessageReceiveAckPolicyAdapterProofs,
} from "openclaw/plugin-sdk/channel-message";

it("backs declared message capabilities", async () => {
  await expect(
    verifyChannelMessageAdapterCapabilityProofs({
      adapterName: "demo",
      adapter: demoMessageAdapter,
      proofs: {
        text: async () => {
          const result = await demoMessageAdapter.send!.text!(textCtx);
          expect(result.receipt.platformMessageIds).toContain("msg-1");
        },
        replyTo: async () => {
          await demoMessageAdapter.send!.text!({ ...textCtx, replyToId: "parent-1" });
          expect(sendDemoMessage).toHaveBeenCalledWith(
            expect.objectContaining({
              replyToId: "parent-1",
            }),
          );
        },
        messageSendingHooks: () => {
          expect(demoMessageAdapter.durableFinal!.capabilities!.messageSendingHooks).toBe(true);
        },
      },
    }),
  ).resolves.toContainEqual({ capability: "text", status: "verified" });
});

当适配器声明直播和接收特性时,添加相应的证明套件。缺少证明应导致测试失败,而不是静默扩大持久化表面。

已废弃的兼容性 API

这些 API 仍可导入用于第三方兼容性。新通道代码不应使用它们。

废弃的 API替代方案
openclaw/plugin-sdk/channel-reply-pipelineopenclaw/plugin-sdk/channel-message
createChannelTurnReplyPipeline(...)兼容性分发器使用 createChannelMessageReplyPipeline(...),新通道代码使用 message 适配器
buildChannelMessageReplyDispatchBase(...)createChannelMessageReplyPipeline(...)channel.turn.runPrepared(...),或新通道代码使用 message 适配器
dispatchChannelMessageReplyWithBase(...)createChannelMessageReplyPipeline(...)channel.turn.runPrepared(...),或新通道代码使用 message 适配器
recordChannelMessageReplyDispatch(...)createChannelMessageReplyPipeline(...)channel.turn.runPrepared(...),或新通道代码使用 message 适配器
deliverOutboundPayloads(...)sendDurableMessageBatch(...)deliverInboundReplyWithMessageSendContext(...)(来自 channel-message-runtime
deliverDurableInboundReplyPayload(...)deliverInboundReplyWithMessageSendContext(...)(来自 openclaw/plugin-sdk/channel-message-runtime
dispatchInboundReplyWithBase(...)createChannelMessageReplyPipeline(...)channel.turn.runPrepared(...),或新通道代码使用 message 适配器
recordInboundSessionAndDispatchReply(...)createChannelMessageReplyPipeline(...)channel.turn.runPrepared(...),或新通道代码使用 message 适配器
resolveChannelSourceReplyDeliveryMode(...)resolveChannelMessageSourceReplyDeliveryMode(...)
deliverFinalizableDraftPreview(...)defineFinalizableLivePreviewAdapter(...)deliverWithFinalizableLivePreviewAdapter(...)
DraftPreviewFinalizerDraftLivePreviewFinalizerDraft
DraftPreviewFinalizerResultLivePreviewFinalizerResult

兼容性分发器仍可通过消息外观使用 createReplyPrefixContext(...)createReplyPrefixOptions(...)createTypingCallbacks(...)。新生命周期代码应避免使用旧的 channel-reply-pipeline 子路径。

迁移清单

  1. 添加 message: defineChannelMessageAdapter(...)message: createChannelMessageAdapterFromOutbound(...) 到通道插件。
  2. 从 text、media 和 payload 发送返回 MessageReceipt
  3. 仅声明有原生行为和测试支持的能力。
  4. deriveDurableFinalDeliveryRequirements(...) 替换手写的持久化需求映射。
  5. 当通道原位编辑草稿消息时,将预览终结移至直播预览助手。
  6. 仅当接收器确实可以延迟平台确认时,才声明接收确认策略。
  7. 仅在兼容性边界保留旧版回复分发助手。

常见问题

如何从 deliverOutboundPayloads 迁移到新发送 API?

新发送路径应使用 sendDurableMessageBatch(...)withDurableMessageSendContext(...)deliverInboundReplyWithMessageSendContext(...)deliverOutboundPayloads 仅用于旧适配器兼容性,不应在新通道或插件发送代码中使用。如果已有 outbound 适配器,可先使用 createChannelMessageAdapterFromOutbound 桥接,再逐步替换为消息生命周期助手。

reconcileUnknownSend 什么时候必须声明?

当你的适配器需要支持必需的持久化最终投递(required durable final delivery)时,必须声明并实现 reconcileUnknownSend。这使 Core 在崩溃恢复后能够确定发送是否已到达平台,避免重复发送。如果适配器无法分辨,则不要声明该能力,Core 会在队列前拒绝必需持久化投递,并将投递回退为尽力而为。

旧的 channel-reply-pipeline 还可以使用吗?

是的,它仍可导入用于第三方兼容性,但新代码不应使用。新通道插件应使用 channel-message 适配器替代。如果需要旧版回复分发器行为,可使用 createChannelMessageReplyPipeline(...)channel.turn.runPrepared(...) 保持兼容,而不引入新的公开 turn 包装器。