OpenClaw webhook 收不到消息:先分清三条链路,再按注册、鉴权、载荷逐段定位
“webhook 收不到消息”这句话在 OpenClaw 里至少对应三种完全不同的故障。有人是指外部系统 POST 过来,网关那边一点反应没有;有人是指定时任务跑完了,自己的接收端没收到那个 POST;还有人是在配 webhooks 插件,路由怎么都返回 401。
麻烦的地方在于,这三条链路在官方文档里的配置块不是同一个:入站端点在 hooks.*,出站投递在 cron.*,插件路由在 plugins.entries.webhooks.config.routes。连鉴权用的自定义请求头名字都不一样。你照着某一篇的写法去查另一条链路,只会越查越乱。
所以排查的第一步不是看日志,是先确认自己在说哪一个 webhook。下面按这三条链路分别过一遍:怎么注册、怎么鉴权、载荷长什么样、以及”没动静”的时候该往哪儿看。
三条链路先对上号
| 叫法 | 方向 | 配置位置 | 鉴权头 | 典型用途 |
|---|---|---|---|---|
| Gateway hooks 端点 | 入站(外部 → OpenClaw) | hooks.enabled / token / path | Authorization: Bearer 或 x-openclaw-token | 外部事件唤醒主会话、触发一次 agent turn |
| automations webhook 投递 | 出站(OpenClaw → 你的服务) | cron.webhookToken / cron.webhookSsrfPolicy,任务上加 --webhook <url> | Authorization: Bearer <cron.webhookToken> | 把定时任务的完成事件 POST 给下游 |
| webhooks 插件路由 | 入站(外部 → TaskFlow) | plugins.entries.webhooks.config.routes | Authorization: Bearer 或 x-openclaw-webhook-secret | 让 Zapier、n8n、CI 创建和驱动托管 TaskFlow |
注意第三行的自定义头是 x-openclaw-webhook-secret,跟第一行的 x-openclaw-token 不是一个东西。官方文档在插件页的相关链接里也专门标了一句:Gateway 的 hooks.* 是另一套通用 HTTP 端点,跟这个插件的路由不是同一个功能。
入站:Gateway hooks 端点怎么注册和鉴权
入站端点默认不开,要在配置里显式打开:
{
hooks: {
enabled: true,
token: "shared-secret",
path: "/hooks",
},
}
每个请求都必须带 hook token,两种写法二选一:Authorization: Bearer <token>(文档推荐这个)或者 x-openclaw-token: <token>。查询串里的 token 会被直接拒绝——如果你的上游系统只会把密钥拼在 URL 上,这条链路走不通,得在中间加一层能改请求头的代理。
内置两个端点。POST /hooks/wake 是往选定 agent 的主会话里塞一个系统事件:
curl -X POST http://127.0.0.1:18789/hooks/wake \
-H 'Authorization: Bearer SECRET' \
-H 'Content-Type: application/json' \
-d '{"text":"New email received","mode":"now","agentId":"main"}'
text 必填,mode 取 now 或 next-heartbeat(默认 now),agentId 在”配置的 agent 编队没有隐式或保留的旧属主”时是必填的。多 agent 环境里漏掉 agentId 是很常见的一种”没反应”。
POST /hooks/agent 则是直接跑一次 agent turn,默认隔离会话。可用字段:message(必填)、name、agentId、sessionKey、sessionMode、idempotencyKey、wakeMode、deliver、channel、to、accountId、model、thinking、timeoutSeconds。
自定义名字的端点(POST /hooks/<name>)走 hooks.mappings,可以用模板或代码转换把任意载荷映射成 wake 或 agent 动作。映射出来的 agent 动作,响应契约跟 POST /hooks/agent 完全一致。
入站没动静:按状态码和投递绑定去查
/hooks/agent 的 HTTP 响应只等到 runner 准入,不等 agent turn 跑完。一个 200 最多可能花 15 秒,它的含义仅仅是”这次运行进入了 agent runner”。所以拿 200 当”任务已完成”来判断,本身就会误导排查方向。
准入前的失败会返回 { ok: false, error, runId },状态码含义是固定的:
| 状态码 | 含义 | 该怎么办 |
|---|---|---|
| 400 | 投递坐标或账号选择非法 | 改请求再重试,这次运行根本没排上 |
| 409 | 目标会话变了或拒绝接新活 | 先解决会话冲突再重试 |
| 502 | 网关或 cron 准备阶段就失败了 | 进入 runner 之前挂的,查网关本身 |
| 503 | 15 秒内没完成 runner 准入 | 超时的排队工作会被取消,不会稍后自己跑起来 |
如果拿到的是 200,人却”没收到消息”,多半是投递绑定的问题。这套绑定是在隔离运行被调度之前就定死的,规则很硬:
channel和to都不给 → 只跑完成流程,结果通过 hook completion event 呈现,不会往聊天里发。- 开着投递的前提下只给一个 → 请求直接 400,并且不排任何运行。
- announce 投递必须要一个具体的 channel,webhook hook 永远不会继承主会话的
last频道或收件人。 deliver: false→ 保持完成流程,忽略任何投递目的地。channel和to都给具体值 → 启用直接 announce 投递。- 多账号频道要选账号得配
accountId;未知、禁用或非法的账号 ID 返回 400 且不排运行。
“以为会发到我平时聊天那个窗口”是这里最容易踩的:主会话的 last 路由在 hook 这条路上不生效。
还有几个会让请求被静默挡住的配置项:hooks.allowedAgentIds 限制这个端点能选哪个 agent(包括省略 agentId 时的默认 agent);sessionKey 要生效必须 hooks.allowRequestSessionKey=true;持久化的直连 hook 还额外要求显式 sessionKey 加上非空的 hooks.allowedSessionKeyPrefixes 白名单。另外 hooks.path 不允许设成 /,会被拒绝。
出站:定时任务的 --webhook 送不到接收端
这条是反方向。在任务上加 --webhook <url> 就把完成后的运行载荷 POST 给一个 HTTP 端点:
openclaw automations create "0 18 * * 1-5" \
"Summarize today's deploys as JSON." \
--name "Deploy digest" \
--webhook "https://example.invalid/openclaw/cron"
第一个坑是互斥:webhook 投递不能和聊天投递的参数一起用(--announce、--channel、--to、--thread-id、--account)。想两边都要,得换思路。
第二个坑更常见——SSRF 守卫。每一个出站的自动化 webhook 都走严格 SSRF 检查,环回地址、私网/内网、链路本地以及其它特殊用途目标默认一律拒绝,这条对主投递、完成与失败目的地、失败告警 webhook 全都适用。也就是说,你把接收端跑在同一台机器上、写个 http://127.0.0.1:9000/...,它就是不发。要放行只能给精确的主机名或 IP 豁免:
{
cron: {
webhookSsrfPolicy: {
allowedHostnames: ["127.0.0.1"],
},
},
}
dangerouslyAllowPrivateNetwork: true 是更宽的开关,官方明确建议优先用窄的 allowedHostnames,只有当每一个配置的自动化 webhook 都可以访问受信私网服务时才考虑它。策略不写就是严格模式。
第三个坑是鉴权:cron.webhookToken 会以 Authorization: Bearer <token> 发在出站 POST 上。webhook URL 不允许内嵌用户名密码,接收端支持 bearer 就用 webhookToken。这类共享密钥怎么存、怎么轮换,可以参考密钥与凭据管理那篇。
失败通知走的是另一条目的地路径:全局默认在 cron.failureAlert(mode/channel/to/accountId),单个任务用 job.delivery.failureDestination 覆盖;两个都没设而任务本身是 announce 投递的话,失败通知回落到那个 announce 目标。注意 delivery.failureDestination 只在 sessionTarget="isolated" 的任务上支持,除非主投递模式就是 webhook。失败 webhook 会保留结构化的原始错误给诊断集成用,聊天里的失败通知则显示归一化后的原因,运行开始时刻可以从结构化的 runAtMs 字段读。
还有个容易忽略的自动保护:时间型循环任务连续 10 次执行失败会被自动禁用,调度计算连续 3 次出错也会。禁用后默认列表看不见它,得用 openclaw automations list --all 才能看到,修好原因后用 openclaw automations enable <jobId> 恢复。如果你的接收端挂了一整天,回头发现任务”彻底不响了”,先往这里看——这跟定时任务不触发是两种不同的停摆原因。
插件版 webhook:401 和限流也长得像”收不到”
webhooks 插件跑在 Gateway 进程里,出厂不带任何路由,加上至少一条路由之前它就是个空操作。远程网关要在那台主机上装好、配好,然后重启网关。
它的请求处理是有固定顺序的:先查 HTTP 方法(只收 POST)和 Content-Type: application/json,然后是固定窗口限流(每个 path + 客户端 IP 组合每 60 秒 120 个请求,最多跟踪 4096 个 key),再是并发限制(同一 key 8 个在途请求,同样上限 4096 个 key),之后才是共享密钥鉴权,最后才读 body(256 KB / 15 秒上限)。前面某一关没过,后面的关卡根本不会走到——所以一个被限流挡下的请求,表现和鉴权失败并不相同,别一上来就怀疑密钥。
密钥这边有个很隐蔽的失败模式:secret 支持 SecretRef({ source: "env" | "file" | "exec" | "store", provider, id }),而 SecretRef 是解析进网关启动时的配置快照的。某条路由的密钥解析不出来时,网关照常运行,那条路由也照常注册着,但它是冷的——请求一律收到一个笼统的 401 鉴权失败。其它路由不受影响。修好 SecretRef 来源之后必须 reload 或重启网关让新快照生效,光改环境变量不重启是没用的。另外 SecretRef 的值永远不会在公开请求路径上解析。
变更类动作(set_waiting、resume_flow、finish_flow、fail_flow、request_cancel)需要带 flowId 和 expectedRevision 做乐观并发控制,版本过期会返回 409 revision_conflict。这个不是”收不到”,但很容易被上游重试逻辑当成失败无限重发,反过来把自己撞进限流。
一条固定的自查顺序
不管哪条链路,官方给的命令阶梯是同一套:
openclaw status
openclaw gateway status
openclaw automations status
openclaw automations list
openclaw automations runs --id <jobId> --limit 20
openclaw system heartbeat last
openclaw logs --follow
openclaw doctor
排出站问题时,automations runs --id <jobId> 是主战场——每次自动化运行都会建一条后台任务记录,运行历史和失败原因都在里面。排入站问题时,openclaw logs --follow 配合一次手工 curl 更直接:能确认请求到底有没有进到网关。想更系统地看网关侧的日志与诊断开关,可以看网关诊断与日志。
要提醒一句的是 hooks.enabled 这个开关同时管着别的东西:当 hooks.enabled=true 且 hooks.gmail.account 有值时,网关开机会自动起 gog gmail watch serve 并自动续期,OPENCLAW_SKIP_GMAIL_WATCHER=1 可以退出这个行为。所以为了调 webhook 去开 hooks.enabled,可能会顺带把 Gmail 监听也拉起来。
这篇没覆盖的部分
Gmail Pub/Sub 那条链路(openclaw webhooks gmail setup / run、Pub/Sub 主题订阅、Tailscale Funnel 暴露推送端点)自成一套,它的失败点在 GCP 侧和 gog 侧,跟上面三条链路的排查方法不通用,得单独看。
流式调度目前也没有原生的 WebSocket 源,官方写的是 V1 不含,需要用 argv 命令桥接(例如 websocat wss://example.invalid/events)。如果你想用长连接推事件而不是 HTTP POST,现在只能走这条绕路。
安全边界这块,文档的态度很明确:hook 端点应该放在环回地址、tailnet 或者可信反向代理后面,用专门的 hook token 而不是复用网关鉴权 token,hooks.path 保持在专用子路径上,hook 载荷默认会被安全边界包裹。如果排查过程中你为了图快把端点直接暴露到公网,那问题就从”收不到”变成别的了。
最后,本文所有配置项、字段名和状态码都来自 OpenClaw 官方文档,没有安装和实测环节。具体到你那套环境,参数取值和版本行为请以你本地的 openclaw doctor 输出和官方文档为准。事件驱动的内部 hooks 是另一套机制,跟这里的 HTTP 端点容易混淆,可以对照hooks 的触发点与用法一起看。
延伸阅读
- 从头读起:OpenClaw 架构总览:网关、Agent、渠道、节点、插件五层各管什么,出问题该往哪查
- 本专题共 40 篇,完整分组目录见专题页
- OpenClaw hooks 怎么用:事件清单、目录结构与”装了不跑”的三种原因
- OpenClaw 常驻指令怎么写:把「每次都要提醒」换成一份带边界的长期授权
本文依据 OpenClaw 官方仓库(github.com/openclaw/openclaw)docs/ 下的官方文档整理,核对日 2026-08-17。
我们没有安装或运行过 OpenClaw,因此不涉及界面外观、操作手感与实测耗时的任何描述;
文中的默认值、命令与配置项均为文档口径,不构成对实际运行结果的保证。
该项目迭代很快,请以仓库最新内容为准。接入即时通讯平台前,请自行确认所在平台的规则与合规要求。