乐于分享
好东西不私藏

OpenClaw 开发系列:第5篇-事件钩子 — 在 Agent 生命周期中植入逻辑

OpenClaw 开发系列:第5篇-事件钩子 — 在 Agent 生命周期中植入逻辑

前置阅读:第 1 篇 — Plugin SDK 入门

本篇讲解 OpenClaw 的事件钩子系统。钩子是插件介入 Agent 生命周期的核心机制——通过钩子,你可以在工具调用前拦截、在消息发送前修改、在会话绑定后执行自定义逻辑,而不需要修改核心代码。


01

钩子系统概述

钩子(Hook)的本质是在特定事件发生时自动执行的回调函数。它是一种观察者模式的实现,但带有更精细的控制语义(后面会详细讲)。

打个比方:如果 OpenClaw 的运行是一条流水线,那么钩子就是在流水线的特定工位上安装的「质检站」——每当产品经过这个工位,质检站就自动工作,可以检查产品、修改产品,甚至拦截不让产品继续往下走。

OpenClaw 的钩子系统有两个注册入口,都在 register(api) 回调中通过 api 对象调用:

// 方式 1:通过 api.registerHook 注册api.registerHook("event_name", handler, options?);// 方式 2:通过 api.on 注册(语义等价)api.on("event_name", handler, options?);

两种方式功能一致,选哪个都可以。api.on 写法更简洁,api.registerHook 语义更显式。本篇后续示例统一使用 api.on


02

钩子优先级

在多插件环境下,可能有多个插件对同一个事件注册钩子。例如,一个安全插件和一个审计插件都注册了 before_tool_call。这时,执行顺序由优先级控制——优先级高的先执行:
api.on("before_tool_call", handler, {  priority: 10,  // 数字越大,优先级越高,越先执行});

默认优先级为 0。高优先级处理器先执行,低优先级后执行。如果高优先级处理器做出了终局性决策,低优先级处理器将被跳过。


03

核心钩子详解

before_tool_call

触发时机:Agent 决定调用某个工具,但在实际执行之前。这是插件介入工具执行的最后机会。

用途

  • 审计和日志记录

  • 安全检查和权限验证

  • 参数修改或注入

  • 阻止特定工具的执行

api.on("before_tool_call"async (context) => {  // context 包含本次工具调用的详细信息  const { toolName, parameters, conversationId } = context;  // toolName: 工具名称(如 "exec"、"web_search")  // parameters: Agent 传给工具的参数  // conversationId: 当前会话 ID  // 记录审计日志  api.logger.info(`Tool call: ${toolName}`, { parameters });  // 示例:阻止对危险工具的调用  if (toolName === "exec" && parameters.command.includes("rm -rf")) {    return { blocktrue };  // 返回 block: true 表示终局性阻止  }  // 示例:修改参数——强制开启安全搜索  if (toolName === "web_search" && !parameters.safeSearch) {    parameters.safeSearch = true;  // 直接修改 parameters 对象即可  }  // 不返回 block,或返回 { block: false } = 不干预,工具正常执行});

决策语义

注意:{ block: false }不是「允许调用」的意思,而是「我不做决定,让下一个处理器来」。如果你只想观察而不干预,不返回 block 字段即可。

message_sending

触发时机:Agent 生成回复后,消息发送到通道之前。

用途

  • 内容过滤和合规检查

  • 消息格式转换

  • 添加额外的上下文信息

  • 阻止不当消息的发送

api.on("message_sending"async (context) => {  const { message, channel, conversationId } = context;  // 示例:敏感词过滤  const sensitiveWords = ["机密""绝密""内部文件"];  for (const word of sensitiveWords) {    if (message.text?.includes(word)) {      api.logger.warn(`Blocked message containing: ${word}`, {        conversationId,      });      return { canceltrue };  // 终局性阻止    }  }  // 示例:为回复添加来源标注  if (message.text) {    message.text += "\n\n_此回复由 AI 助手生成_";  }  // 继续传播  return { cancelfalse };});

决策语义

onConversationBindingResolved

触发时机:会话绑定解析完成时。「会话绑定」是指 OpenClaw 确定某个对话应该使用哪个 Agent、哪个 Channel 的过程。这个钩子在实际开发中较少用到,一般用于需要基于会话上下文做初始化的场景。

用途

  • 会话级别的初始化

  • 基于会话上下文的配置加载

  • 会话级别的状态管理

api.onConversationBindingResolved(async (binding) => {  // binding 包含会话绑定信息  api.logger.info(    `Conversation resolved: ${binding.conversationId}, ` +    `agent: ${binding.agentId}, channel: ${binding.channelId}`  );});

04

钩子链传播机制

多个处理器对同一事件的执行遵循以下规则:

关键规则:

  1. 处理器按优先级从高到低依次执行

  2. 如果某个处理器返回终局性决策(block: true 或 cancel: true),传播立即停止

  3. block: false 和 cancel: false 不是「允许」,而是「不干预」

  4. 如果所有处理器都不做决策,默认行为继续执行(工具正常调用 / 消息正常发送)

这个设计避免了「一个插件阻止,另一个插件又允许」的歧义——第一个做出终局性决策的处理器说了算。


05

钩子的常见模式

模式 1:纯观察(审计日志)

api.on("before_tool_call"async (context) => {  api.logger.info("Tool call audit", {    tool: context.toolName,    params: JSON.stringify(context.parameters),    timestamp: new Date().toISOString(),  });  // 不返回任何决策字段 = 纯观察});

模式 2:条件拦截

api.on("before_tool_call"async (context) => {  if (context.toolName === "exec") {    const cmd = context.parameters.command as string;    const dangerous = ["sudo""rm -rf""format""del /S"];    if (dangerous.some(d => cmd.includes(d))) {      api.logger.warn(`Blocked dangerous command: ${cmd}`);      return { blocktrue };    }  }});

模式 3:参数注入

api.on("before_tool_call"async (context) => {  // 为所有工具调用添加审计标记  context.parameters._auditId = generateAuditId();  context.parameters._timestamp = Date.now();  // 不阻止,仅修改参数});

模式 4:消息后处理

api.on("message_sending"async (context) => {  if (context.message.text) {    // 统一格式化消息    context.message.text = formatResponse(context.message.text);    // 截断过长的消息    if (context.message.text.length > 4000) {      context.message.text =        context.message.text.substring(03997) + "...";    }  }});

06

钩子 vs 工具

钩子和工具都可以扩展 Agent 的行为,但适用场景不同:

选择原则

  • 如果是「每次都应该执行」的逻辑 → 钩子

  • 如果是「Agent 需要自主决策是否使用」的能力 → 工具

例如:审计日志适合用钩子(每次工具调用都要记录),而天气查询适合用工具(Agent 根据对话内容决定是否调用)。


07

完整示例:安全审计插件

import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";export default definePluginEntry({  id"audit-plugin",  name"Audit Plugin",  version"1.0.0",  register(api) {    // 工具调用审计    api.on("before_tool_call"async (context) => {      const entry = {        event"tool_call",        tool: context.toolName,        paramssanitizeForLog(context.parameters),        conversationId: context.conversationId,        timestampnew Date().toISOString(),      };      // 记录到日志      api.logger.info("AUDIT", entry);      // 危险操作拦截      if(isDangerousOperation(context.toolName, context.parameters)) {        api.logger.warn("BLOCKED dangerous operation", entry);        return { blocktrue };      }    });    // 消息发送审计    api.on("message_sending"async (context) => {      const entry = {        event"message_sending",        channel: context.channel,        textLength: context.message.text?.length ?? 0,        hasAttachments: (context.message.attachments?.length ?? 0) > 0,        conversationId: context.conversationId,        timestampnew Date().toISOString(),      };      api.logger.info("AUDIT", entry);      // 内容合规检查      if(containsProhibitedContent(context.message.text ?? "")) {        api.logger.warn("BLOCKED prohibited content", entry);        return { canceltrue };      }    });    // 会话绑定跟踪    api.onConversationBindingResolved(async (binding) => {      api.logger.info("AUDIT", {        event"conversation_resolved",        conversationId: binding.conversationId,        agentId: binding.agentId,        channelId: binding.channelId,      });    });  },});functionisDangerousOperation(toolName: string, params: Record<string, unknown>): boolean{  if(toolName === "exec") {    const cmd String(params.command ?? "");    return ["/sudo""rm -rf""mkfs""format "].some(d => cmd.includes(d));  }  return false;}functioncontainsProhibitedContent(text: string): boolean{  const prohibited = ["[PROHIBITED_PATTERN_1]""[PROHIBITED_PATTERN_2]"];  return prohibited.some(p => text.includes(p));}functionsanitizeForLog(params: Record<string, unknown>): Record<stringunknown{  // 移除敏感参数  const sanitized = { ...params };  if("apiKey" in sanitized) sanitized.apiKey = "***";  if("token" in sanitized) sanitized.token = "***";  return sanitized;}

08

小结

本篇覆盖了 OpenClaw 钩子系统的完整机制:

  • 钩子是插件介入 Agent 生命周期的核心机制

  • 两个注册方式:api.registerHook 和 api.on

  • 优先级控制多处理器的执行顺序

  • before_tool_call 和 message_sending 的终局性决策语义

  • 四种常见模式:纯观察、条件拦截、参数注入、消息后处理

  • 钩子适合横切关注点,工具适合业务能力


下一篇:上下文引擎 — 控制 Agent 的「视野」。我们将学习如何通过自定义上下文引擎精确控制 Agent 在每次对话中能「看到」什么信息。