openclaw源码解读(16)从日志追踪 OpenClaw 消息路由与 Hook 执行引擎2
一条 Matrix 消息从「路由入队」到「LLM 流式返回」,中间到底发生了什么?
本文以 AgentTeams 场景下的一段真实运行日志为线索,逐行定位到 OpenClaw 源码,还原 Embedded Agent Runner 的完整执行链路。
背景
某天晚上,我给AgentTeams 多 Agent 框架系统 ClawForge 里的一个 Worker(code-analyst,运行时是 OpenClaw)发了一条消息。Worker 很快回复了 。
日志第5行:2026-08-11T04:39:16.529+00:00 [plugins] [hooks] running reply_dispatch (1 handlers, first-claim wins)
日志里 (1 handlers, first-claim wins) 说明只注册了 1 个选手。这个选手就是 Matrix plugin 注册到 reply_dispatch 钩子上的处理函数,负责把消息分发到正确的 session。那么,这个 handler 具体是谁注册的?
log: [plugins] [hooks] running reply_dispatch (1 handlers, first-claim wins)
↓
runReplyDispatch() L1042 ← 真正的入口
↓
runClaimingHook("reply_dispatch", ...) L1042-1046
↓
getHooksForName(registry, "reply_dispatch") L636
↓
runClaimingHooksList(...) L643
从代码上看,调用链如下:runClaimingHooksList被用在runClaimingHook和runClaimingHookForPlugin,在runClaimingHook里,hooks是通过hookname过滤出注册过的hook。从runClaimingHook里跟进去,注册的对象是registry,全文检索,registry有两个类:HookRunnerRegistry和GlobalHookRunnerRegistry,这两个类都是hook-registry.types.js
继续跟进,真正执行注册的是:hook-registry-types.ts,里面有两个方法进行注册:HookRunnerRegistry和GlobalHookRunnerRegistry。它注册的时候只是把合法的state和type注册到hook上,合法的state在hook-registry-types.ts里,lobalHookRunnerRegistry.plugins[].status 只接受 "loaded" 、 "disabled" 、"error" 三个字面量。type在hook-types.js里,PluginHookName 定义了 41 个合法的 hook 名字,PluginHookRegistration 定义了 hook 注册的结构体。
进入hook-registry.types.ts去看,Registry 类型:
HookRunnerRegistry = { // 纯 hook 存储
hooks: PluginLegacyHookRegistration[]; // 旧版 hook
typedHooks: TypedPluginHookRegistration[]; // 新版 typed hook
}
GlobalHookRunnerRegistry = HookRunnerRegistry & {
plugins: [{ id, status: "loaded" | "disabled" | "error" }]; // + 插件状态
}
hook-registry-types.ts vs hook-types.ts 的分工
hook-types.ts hook-registry-types.ts
(定义"什么是合法的 hook") (定义"hook 存在哪里")
│ │
▼ ▼
PluginHookName ┌─ HookRunnerRegistry
├─ "reply_dispatch" │ ├─ hooks[] (旧版)
├─ "before_agent_reply" │ └─ typedHooks[] (新版 PluginHookRegistration)
├─ "inbound_claim" │
└─ ...(共 41 个合法名字) └─ GlobalHookRunnerRegistry extends HookRunnerRegistry
└─ plugins[] ← 这里才有 status
PluginHookRegistration<K> ├─ "loaded"
├─ pluginId: string ├─ "disabled"
├─ hookName: K ← 必须来自 PluginHookName └─ "error" 合法的 state 就这 3 种 ✅
├─ handler: PluginHookHandlerMap[K]
├─ priority?: number
├─ timeoutMs?: number
└─ source: string
这两个 type 文件只是定义结构。实际填充 registry 的是 createHookRunner:
// hooks.ts L234
export function createHookRunner(registry: GlobalHookRunnerRegistry, options) {
// 传入的 registry 由调用方(plugin loader)在启动时填充好 hooks[] 和 typedHooks[]
// createHookRunner 只是把 registry 包一层执行框架
}
真正往 registry.typedHooks[] 里 push 注册项的,是 plugin loader(plugin-loader.ts),它在 Gateway 启动时扫描所有插件目录,逐个 load 并注册。
从插件加载到 registry.typedHooks.push() 的完整链路
Gateway 启动
▼
plugin-loader 扫描插件目录
▼
registry.ts 加载每个插件 → 调用插件的 SKILL.md / index.ts
│ │
│ 插件代码里调用:api.registerHook("reply_dispatch", myHandler)
│ ▼
▼ plugin-api.types.ts:L197(定义插件能调用的 API 接口)
registry-api.ts:L376
▼
registry-registrars-tools-hooks.ts:L397
registerTypedHook(record, hookName, handler, opts, policy)
├─ L404: isPluginHookName() ← 校验 hookName 是否在 41 个合法名字里
├─ L433: allowPromptInjection ← 安全检查:是否允许注入 prompt
├─ L454: allowConversationAccess ← 安全检查:非内置插件需要显式开启
└─ L478: registry.typedHooks.push({
pluginId: record.id,
hookName: effectiveHookName,
handler: effectiveHandler, ← 真正的"选手"
priority: opts?.priority,
source: record.source,
})
数据流方向:
插件源码 registry-api.ts registry-registrars HookRunnerRegistry
"我想注册 → {registerTypedHook} → 安全检查 + push → typedHooks[]
reply_ │
dispatch" hooks.ts: getHooksForName()
runClaimingHook()
runClaimingHooksList()
handler(event, ctx) ← 真正执行
一句话总结: 插件通过 api.registerHook("reply_dispatch", handler) 注册 → 经过安全检查 → push 进 registry.typedHooks[] → hooks.ts 的执行框架在合适的时机取出并调用。
正在规划《OpenClaw源码解读》书籍,欢迎出版社编辑交流
更多推荐




所有评论(0)