一条 Matrix 消息从「路由入队」到「LLM 流式返回」,中间到底发生了什么?

本文以 AgentTeams 场景下的一段真实运行日志为线索,逐行定位到 OpenClaw 源码,还原 Embedded Agent Runner 的完整执行链路。


背景

某天晚上,我给AgentTeams 多 Agent 框架系统 ClawForge 里的一个 Worker(code-analyst,运行时是 OpenClaw)发了一条消息。Worker 很快回复了 。

见 openclaw源码解读(15)

日志第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源码解读》书籍,欢迎出版社编辑交流

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐