oh-my-claudecode OpenClaw/Clawhip 路由契约解读:基于 signal 的统一事件载荷与下游路由实践
【免费下载链接】oh-my-claudecodeTeams-first Multi-agent orchestration for Claude Code项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-claudecode
本篇技术指南围绕 oh-my-claudecode 项目中 OpenClaw/Clawhip 路由契约 展开,系统讲解该项目通过 OpenClaw bridge 向原生 Clawhip 风格消费者输出的"归一化事件契约":如何在保留原始 hook 事件event的同时,新增可供路由与去重过滤的signal对象,并让 HTTP 网关与原生命令行网关收到完全一致的逻辑载荷。读完本文,你将掌握signal的字段语义、全部高优先级 route key、噪音抑制机制,以及基于 src/openclaw 源码的底层实现原理。
一、为什么需要统一路由契约
oh-my-claudecode(简称 OMC)为 Claude Code 提供了大量 hook 生命周期事件(会话开始/结束、工具调用、提问、关键词命中等)。在早期形态下,这些事件以各不相同的原始载荷直接推送给下游消费者,导致两类问题:
- 事件命名与字段不统一:会话、测试、PR、提问等逻辑信号与原始 hook 名称混杂,下游消费者不得不在裸 hook 名上做脆弱的字符串路由。
- HTTP 网关与命令行网关体验割裂:HTTP 网关接收的是结构化 JSON,而基于 shell 命令的原生 Clawhip 网关只能拿到零散的模板变量,两边无法共享同一套路由逻辑。
docs/OPENCLAW-ROUTING.md定义的正是一个"归一化事件契约",其三大目标(文档 Goals 章节)可概括为:
- 保留向后兼容:原始 hook 事件
event继续原样保留; - 引入路由面:新增归一化的
signal对象,用于路由与去重友好过滤; - 传输同构:让 command / 原生网关与 HTTP 网关收到同一逻辑载荷结构。
二、统一 Payload 结构
HTTP 网关收到的 JSON 具有如下结构(来自 文档 Payload shape 章节,字段注释与实现对齐):
{ "event": "post-tool-use", "instruction": "...", "timestamp": "2026-03-09T00:00:00.000Z", "sessionId": "...", "projectPath": "...", "projectName": "...", "tmuxSession": "...", "tmuxTail": "...", "signal": { "kind": "test", "name": "test-run", "phase": "failed", "routeKey": "test.failed", "priority": "high", "toolName": "Bash", "command": "pnpm test", "testRunner": "package-test", "summary": "FAIL src/example.test.ts | ..." }, "context": { "sessionId": "...", "projectPath": "...", "toolName": "Bash" } }在源码层面,该结构的类型定义位于 src/openclaw/types.ts,其中OpenClawPayload(L111-L139)定义了event / instruction / timestamp / sessionId / projectPath / projectName / tmuxSession / tmuxTail / channel / to / threadId / signal / context等字段。需要特别说明的两个安全设计:
channel、to、threadId来自OPENCLAW_REPLY_CHANNEL / OPENCLAW_REPLY_TARGET / OPENCLAW_REPLY_THREAD环境变量,用于支持 Discord 等渠道的回执路由。context是白名单子集:见 src/openclaw/index.ts 中的buildWhitelistedContext,仅显式挑选已知字段拼装,杜绝敏感数据外泄。
三、signal契约详解
signal是本次契约的核心新增面,字段语义见 文档信号字段表,对应 TypeScript 类型为 src/openclaw/types.ts 中的OpenClawSignalKind / OpenClawSignalPhase / OpenClawSignalPriority / OpenClawSignal(L66-L109)。
| 字段 | 含义 | 允许取值(源码中的联合类型) |
|---|---|---|
kind | 路由家族(Routing family) | session、tool、test、pull-request、question、keyword |
name | 稳定的逻辑信号名 | 如session、tool-use、test-run、pull-request-create、ask-user-question、keyword-detected |
phase | 生命周期阶段 | started、finished、failed、idle、detected、requested |
routeKey | 供下游消费者使用的规范化路由键 | 详见下文"高优先级 route key" |
priority | 相对优先级 | high(运维型信号:生命周期/测试/PR/提问)、low(一般工具噪音) |
除上表字段外,signal 在适用场景下还可能携带以下附加字段(文档 Additional fields):
toolName—— 触发的工具名(如Bash、Edit、Write)command—— 触发路由判断的 Bash 命令原文testRunner—— 归一化后的测试运行器标识prUrl—— 从gh pr create输出中提取的 PR 链接summary—— 用于路由与调试的短摘要
3.1 signal 是如何从原始事件推导出来的
所有signal均由 src/openclaw/signal.ts 中的buildOpenClawSignal(event, context)依据事件类型分派生成,各 hook 事件到 signal 的映射为:
session-start→{ kind: "session", phase: "started", routeKey: "session.started", priority: "high" }session-end→{ kind: "session", phase: "finished", routeKey: "session.finished", priority: "high" },并用context.reason生成 summarystop→{ kind: "session", phase: "idle", routeKey: "session.idle", priority: "high" }keyword-detector(即UserPromptSubmitbridge 面)→{ kind: "keyword", routeKey: "keyword.detected", priority: "low" }ask-user-question→{ kind: "question", routeKey: "question.requested", priority: "high" }pre-tool-use/post-tool-use→ 走buildToolSignal(见下)- 未识别事件兜底 →
{ kind: "tool", routeKey: "tool.finished", priority: "low" }
值得关注的是buildToolSignal内嵌的启发式判定逻辑(src/openclaw/signal.ts#L124-L173),它把原始工具输入输出"升华"为语义信号:
- 测试命令识别:当 Bash 命令命中测试模式表(L8-L16)时,升级为
kind: "test"信号。该表内置了package-test(npm/pnpm/yarn/bun test)、vitest、jest、pytest、cargo-test、go-test、make-test共 7 类运行器,并填充testRunner字段。 - PR 创建识别:当命令匹配
gh pr create时,升级为kind: "pull-request"信号,并尝试从输出中提取 GitHub PR URL(正则见 L5-L6);phase 决定 routeKey 是pull-request.started/pull-request.failed/pull-request.created。 - 工具成败判定:Bash 按输出内容特征(
error:、failed、FAIL、exit code: [1-9]、permission denied、fatal:等,L46-L63)判断finished/failed;Edit/Write则按写失败特征(write failed、read-only、no such file等)判断。普通工具成功即tool.finished(priority: "low"),失败即tool.failed(priority: "high")。 - 噪音清理:会剥离 Claude 临时目录的
permission denied .../T/claude-*-cwd报错、Error: Exit code N前缀等已知噪音(L3-L4)。 - 摘要压缩:
summarize将输出压缩为最多 4 行、160 字符的单行摘要(L92-L105)。
四、原生 command 网关契约
docs/OPENCLAW-ROUTING.md明确规定:command 网关通过两种通道获得与 HTTP 网关完全相同的归一化载荷。
- 模板变量:
{{payloadJson}}—— 完整归一化 payload 的 JSON 序列化串。 - 环境变量:
OPENCLAW_PAYLOAD_JSON—— 与上述同源的完整 JSON。
同时还会收到三个便于轻量路由的便捷环境变量:
OPENCLAW_SIGNAL_ROUTE_KEY(如test.failed)OPENCLAW_SIGNAL_PHASE(如failed)OPENCLAW_SIGNAL_KIND(如test)
这意味着无论传输层是 HTTP 还是 shell 命令,原生 Clawhip 路由都只面对同一套契约(文档 Native command gateway contract)。
从实现看,command 网关的唤起由 src/openclaw/dispatcher.ts 的wakeCommandGateway完成(L143-L187):
- 模板中的
{{variable}}占位符在插值前会经过shellEscapeArg单引号包裹转义(L83-L85),防止命令注入; - 命令通过
sh -c以execFile异步执行,非阻塞、带超时(默认 10s); - 上述四个环境变量正是在此注入子进程 env(L168-L176)。
同理,HTTP 网关由wakeGateway(L90-L135)唤起,并内置 URL 校验规则:必须 HTTPS,仅localhost/127.0.0.1/::1允许明文 HTTP(便于本地开发)。
五、当前高优先级 route key 清单
文档明确列出的"现役高优先级 route key"如下(文档清单):
session.startedsession.finishedsession.idlequestion.requestedtest.startedtest.finishedtest.failedpull-request.startedpull-request.createdpull-request.failedtool.failed
而通用的tool.started/tool.finished保留为低优先级兜底信号(src/openclaw/signal.ts#L164-L172 中tool分支:仅失败时priority升为high)。keyword.detected同样是低优先级信号,适用于"提示已提交但无需运维介入"的场景。
5.1 路由键与优先级对齐源码
signal 的kind/name/phase/routeKey/priority五个字段在 src/openclaw/signal.ts 的各分支中一一落地。整体规律可归纳为:routeKey = "{kind}.{phase}"(如test.failed),特殊场景使用语义化子键(如pull-request.created);凡是生命周期、测试、PR、提问信号一律priority: "high",普通工具过程信号为low。这与文档 Stability notes 中"消费者应优先过滤signal.priority === "high"或显式匹配signal.routeKey,而不是直接在裸 hook 名上路由"的建议完全一致。
六、噪音抑制(Noise Reduction)
高频事件天然会产生流量噪音。文档给出三层抑制手段(文档 Noise reduction),底层由 src/openclaw/dedupe.ts 的shouldCollapseOpenClawBurst实现:
AskUserQuestion 只发专用信号:
ask-user-question现在只发出question.requested信号,不再叠加发射通用工具生命周期事件,避免下游被同一条提问刷屏(src/openclaw/signal.ts#L211-L219)。attached-tmux 生命周期突发折叠:OpenClaw 在派发前会对重复的 tmux 会话生命周期突发做折叠(collapse),折叠维度统一为
{projectPath, tmuxSession}(源码键形如"session.started::{projectPath}::{tmuxSession}"),各类事件的具体窗口为:
| 事件面 | 折叠键 | 窗口 |
|---|---|---|
session-start | session.started::{scope} | 10 000 ms(START_WINDOW_MS) |
keyword-detector(prompt 提交突发) | session.prompt-submitted::{scope}::{sha1(prompt前12位)} | 4 000 ms(PROMPT_WINDOW_MS) |
stop | session.stopped::{scope} | 12 000 ms(STOP_WINDOW_MS) |
session-end | session.finished::{scope} | 12 000 ms(STOP_WINDOW_MS) |
keyword 折叠会对 prompt 先做空白归一化并截断到 400 字符再取 SHA-1 摘要(src/openclaw/dedupe.ts#L236-L242),确保"语义相同的连续提交"被识别为重复。
- 终端状态滞后抑制:在
stop/session-end之后60 秒(TERMINAL_STATE_SUPPRESSION_WINDOW_MS)内到达的滞后session-start、stop事件会被判定为过期(isObsoleteAfterTerminalState,src/openclaw/dedupe.ts#L314-L341)而丢弃,用于吸收子进程启动延迟、detach/re-attach 时序造成的 hook 乱序。
去重状态采用磁盘持久化 + 进程锁实现:状态记录写入项目目录下.omc/state/openclaw-event-dedupe.json(原子写),锁文件.omc/state/openclaw-event-dedupe.lock带 pid + 随机 token,支持 2s 获取超时、20ms 重试、10s 陈旧锁清理与 6 小时状态 TTL 修剪(src/openclaw/dedupe.ts#L23-L41)。被折叠的事件在唤起结果中会以skipped: "deduped"标记(src/openclaw/types.ts#L171-L182)。
七、稳定性与兼容性说明
文档 Stability notes 确立了三条兼容红线(文档章节),在源码中均有对应约束:
- 原始
event名称完全保留,用于向后兼容 ——OpenClawPayload.event仍携带裸 hook 名,旧消费者无需迁移即可继续工作。 signal是新建原生 Clawhip 集成的首选路由面—— 字段有严格联合类型约束(src/openclaw/types.ts#L66-L109),并在生成阶段集中推导,保证所有网关收到的 signal 形状一致。context是白名单子集:内部原始工具输入/输出(toolInput/toolOutput)只用于推导归一化 signal,绝不进入payload.context(src/openclaw/types.ts#L147-L168 的注释明确此点),从类型层杜绝敏感数据随事件外发。
八、集成接入要点与配置
若要实际启用 OpenClaw 网关,需结合 README.md OpenClaw Integration 章节 完成三步:
- 启用开关:设置环境变量
OMC_OPENCLAW=1(配置读取器在 src/openclaw/config.ts 中会先校验该开关,未开启直接返回null,保证零开销)。 - 编写配置文件
~/.claude/omc_config.openclaw.json,示意结构如下:
{ "enabled": true, "gateways": { "my-gateway": { "url": "https://your-gateway.example.com/wake", "headers": { "Authorization": "Bearer YOUR_TOKEN" }, "method": "POST", "timeout": 10000 } }, "hooks": { "session-start": { "gateway": "my-gateway", "instruction": "Session started for {{projectName}}", "enabled": true }, "post-tool-use": { "gateway": "my-gateway", "instruction": "{{payloadJson}}", "enabled": true } } }若网关配置为命令型,需把 gateway 改为"type": "command"并提供"command"模板;其中instruction可引用{{payloadJson}}在内的全套模板变量。配置文件路径也可通过OMC_OPENCLAW_CONFIG覆盖,OMC_OPENCLAW_DEBUG=1可开启调试日志。相关类型定义集中在 src/openclaw/types.ts 的OpenClawConfig / OpenClawGatewayConfig / OpenClawHookMapping。
- 观察事件流:桥接层的实际唤起点位于 src/hooks/bridge.ts,通过
_openclaw.wake(...)分别在keyword-detector(L1654)、stop(L1876)、session-start(L1979)、ask-user-question(L2542)、pre-tool-use(L2760)、post-tool-use(L2994)等事件触发(bridge.ts L2308-L2317 定义了该可测试包装器)。主入口wakeOpenClaw(src/openclaw/index.ts#L75-L209)负责串联:读取配置 → 解析事件映射 → 构建 signal → 突发折叠判断 → 自动捕获 tmux 尾部输出 → 构造模板变量与 payload → 按网关类型唤起。参考网关实现见 scripts/openclaw-gateway-demo.mjs。
8.1 事件到信号的关键模板变量
桥接层为每个模板插值准备了丰富变量(src/openclaw/index.ts#L140-L165),除{{payloadJson}}与{{instruction}}外还包括:
- 会话维度:
{{sessionId}}、{{projectPath}}、{{projectName}}、{{tmuxSession}}、{{timestamp}} - signal 维度:
{{signalKind}}、{{signalName}}、{{signalPhase}}、{{signalRouteKey}}、{{signalPriority}}、{{signalSummary}} - 上下文维度:
{{toolName}}、{{prompt}}、{{contextSummary}}、{{question}}、{{reason}}、{{tmuxTail}}、{{command}}、{{testRunner}}、{{prUrl}} - 回执维度:
{{replyChannel}}、{{replyTarget}}、{{replyThread}}
需要留意:HTTP 网关的 instruction 由interpolateInstruction处理,未解析的变量会原样保留而非替换为空(src/openclaw/dispatcher.ts#L60-L67);而 command 网关模板中的变量则统一走 shell 转义(见前文),二者语义略有差异。
九、消费端路由建议与小结
基于上述契约,原生 Clawhip 消费者在接入时应遵循以下最佳实践:
- 首选
signal.routeKey做精确路由,例如订阅test.failed以触发失败告警、订阅pull-request.created触发 PR 通知; - 次选
signal.priority === "high"做通配过滤,将tool.started/tool.finished等低优先级噪音天然挡在门外; - 不要直接依赖裸 hook 名(如
post-tool-use)做业务分发,因为同一 hook 名可能承载多种逻辑语义,只有 signal 才是"归一化后的语义真相"; - 对会话维度事件注意突发折叠语义:
session-start/stop/session-end及 prompt 提交会按{projectPath, tmuxSession}做窗口去重,接收端无需重复实现幂等。
总体上,oh-my-claudecode 的 OpenClaw/Clawhip 路由契约是一套兼顾**向后兼容(保留 event)、向前演进(新增 signal)与传输同构(HTTP/command 共享 payload)**的三层设计。它把「会话、工具、测试、PR、提问、关键词」六大信号家族收敛到统一字段语义与 route key 空间,让下游消费者无论走 HTTP 还是原生 shell 命令,都能用同一套逻辑完成路由、过滤与去重 —— 这也正是docs/OPENCLAW-ROUTING.md作为集成契约文档的全部价值所在。
【免费下载链接】oh-my-claudecodeTeams-first Multi-agent orchestration for Claude Code项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-claudecode
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考