跳转至

消息

入站拦截、回复接管与出站投递策略。属于 Plugin hooks 指南的一部分。

消息钩子

对于入站拦截,before_dispatch 在普通模型分发之前接收传入消息。返回 { handled: true, text: "..." } 以发送最终回复,或返回 { handled: true } 以在没有文本的情况下处理消息。这是一种接管声明,而非用于重写出站或入站内容的 API。

reply_dispatch 是高级的接管接缝:它接收最终化的消息上下文和宿主分发器,处理结果会报告 queuedFinal 和投递 counts。对于简单的合成回复,请使用 before_agent_reply;要转换出站负载,请使用下面的发送钩子。

运行时接管应将 ctx.onAgentRunStart、ctx.userTurnTranscriptRecorder 以及可选的 ctx.prepareAssistantTranscriptMessage 转发给其运行时辅助程序。ACP 分发辅助程序会自动转发全部三者。共享录制器,这样运行时和 Gateway 就不会各自独立地追加相同的用户轮次;仅在转录写入成功后标记运行时持久化。

宿主提供的预处理器在正式的助手追加之前记录显示所有权,使用的是在仅转录钩子之前捕获的原始运行时文本。它保留原始内容和 ID,且不授予任何文件访问或写入权限。请将其保持在进程内,并绑定到其所属轮次;当该轮次中止、被替换或完成后,它将原样返回消息。

可选的第三个 onAgentRunStart 参数可以提供带有 getResult() 回调的 completionSource: "reply-dispatch"。宿主必须同步返回 "reply-dispatch" 以接受完成所有权;观察者和其他回调结果不会改变生命周期完成状态。包装器必须转发每个回调参数及其返回值。分发落定后,getResult() 提供权威的 terminalOutcome,并且在助手写入成功时提供其 assistantTranscript 回执(目标、消息 ID、幂等键和可选的投影锚点)。宿主随后从已投递的钩子后负载中发出一次聊天完成,同时保留运行时生命周期事件。回执可防止重复追加;它不授权向被替换的会话进行写入。对于事件流已拥有聊天完成的运行时,请省略此声明。

对于仅限 ACP 的分发器,请使用 eligibleDispatchKinds: ["acp"]。宿主会对已解析的目标(包括会话绑定)进行分类,并将 ctx.dispatchKind 传递为 acp 或 agent。存储的 ACP 元数据和 ACP 会话键都会选择 acp;缺少 ACP 绑定不会回退到 agent 分发。宿主在调用之前以及决定钩子是否阻止持久化聊天准入时,会应用相同的资格检查。因此,仅限 ACP 的钩子不会阻止普通的 agent 会话。省略、为空、格式错误或部分未知的资格列表仍然不受限制。缺失或未知的分发上下文也会使钩子保持有资格。

使用消息钩子进行通道级路由和投递策略:

  • message_received:观察入站内容、发送者、threadId、messageId、senderId、可选的运行/会话关联、有序的 media、规范化后的 location、通道提供时的稳定 providerUpdate 身份,以及元数据。
  • message_sending:重写 content 或返回 { cancel: true }。
  • reply_payload_sending:重写规范化后的 ReplyPayload 对象(包括 presentation、delivery、媒体引用和文本),或返回 { cancel: true }。
  • message_sent:观察最终成功或失败。

对于纯音频 TTS 回复,即使通道负载没有可见的文本/字幕,content 也可能包含隐藏的语音转录文本。重写该 content 只会更新钩子可见的转录文本;它不会被渲染为媒体字幕。

reply_payload_sending 事件可能包含 usageState,即尽力而为的实时逐轮模型/用量/上下文快照。持久化投递、恢复的重放以及没有精确运行关联的回复会省略它。

消息钩子上下文在可用时会公开稳定的关联字段:ctx.sessionKey、ctx.runId、ctx.messageId、ctx.senderId、ctx.trace、ctx.traceId、ctx.spanId、ctx.parentSpanId 和 ctx.callDepth。当通道具有经过可见性过滤的引用消息数据时,入站和 before_dispatch 上下文还会公开回复元数据:replyToId、replyToIdFull、replyToBody、replyToSender 和 replyToIsQuote。在读取旧版元数据之前,请优先使用这些一等字段。

before_dispatch 在其事件和上下文中都会接收正式的入站 messageId。

在使用通道特定的元数据之前,请优先使用类型化的 threadId 和 replyToId 字段。

入站声明事件和消息接收事件将 media?: PluginHookMediaFact[] 公开为标准的附件 API。每个 fact 可携带 path、url、contentType、kind、transcribed、messageId 和 workspaceDir;数组位置即附件身份。当远程附件尚未在本地暂存时,media 会被省略,mediaStagingPending: true,并且 originalMedia 包含提供方侧的 facts。在后续的暂存事件提供 media 之前,请勿将 originalMedia.path 视为本地可读。

单数/复数形式的 mediaPath、mediaUrl、mediaType、mediaPaths、mediaUrls、mediaTypes 以及对应的 originalMedia* 元数据属性是已弃用的兼容性别名。新的钩子应使用类型化的顶层数组。

决策规则:

  • 带有 cancel: true 的 message_sending 是终止性的。
  • 带有 cancel: false 的 message_sending 被视为未做决定。
  • 每个 message_sending 处理器都会接收原始事件内容。最后返回的 content 生效;后续处理器仍可取消投递。
  • reply_payload_sending 在负载规范化之后、通道投递之前运行,包括路由回原始通道的回复。处理器按顺序运行,每个处理器都会看到由更高优先级处理器产生的最新负载。
  • reply_payload_sending 负载不会公开运行时信任标记(如 trustedLocalMedia);插件可以编辑负载形状,但不能授予本地媒体信任。
  • message_sending 可以在取消时返回 cancelReason 和有界 metadata。新的消息生命周期 API 会将此公开为原因 cancelled_by_message_sending_hook 的被抑制投递结果;旧版直接投递出于兼容性继续返回空结果数组。
  • message_sent 仅用于观察。处理器失败会被记录,且不会改变投递结果。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 cl/openclaw