为什么需要 stream 触发器

前面写过「条件触发器」(at / every / cron),适合按时间点跑任务。但有一类需求它覆盖不了:某个长驻进程什么时候吐出某行日志,你并不知道,也不想每 30 秒去轮询一次。

典型场景:

  • 爬虫 / 训练任务跑在后台,一旦打印 ERROR 或 OutOfMemory,立刻通知你;
  • 自建服务启动完成后打印 listening on :8080,自动帮你做健康检查;
  • 监控脚本输出 ALERT disk>90%,马上发微信。

这类「进程说了算,时间说了不算」的事,就该用 stream 触发器。

stream 触发器长什么样

在 automations 的 add 动作里,schedule 改成 kind:"stream":

{
  "name": "log-error-watcher",
  "schedule": {
    "kind": "stream",
    "command": ["python3", "/opt/myapp/run.py"],
    "mode": "match",
    "match": "(?i)error|out of memory|traceback"
  },
  "payload": { "kind": "agentTurn", "message": "检测到进程报错,见触发输出。" }
}

关键字段:

| 字段 | 含义 |

| --- | --- |

| command | 被监督的进程启动命令(argv 数组) |

| mode | line 逐行触发 / match 正则命中才触发 |

| match | mode:"match" 时的正则表达式 |

| cwd | 可选,进程工作目录 |

| maxBatchBytes | 单次最多缓冲的日志字节,防刷屏 |

mode:"line" 是每行都触发一次,mode:"match" 只有命中正则才触发——绝大多数场景用 match,否则日志一多会把自动化跑爆。

实操一:进程报错即推微信

把下面这段存成 add_job.json(或直接用 automations 的 add 调用)。目标:监督一个会偶发报错的服务,命中 error 就把整段上下文交给 agent 总结并推送。

{
  "name": "myapi-error-watch",
  "schedule": {
    "kind": "stream",
    "command": ["node", "/srv/myapi/server.js"],
    "mode": "match",
    "match": "(?i)\\b(error|exception|unhandled)\\b"
  },
  "payload": {
    "kind": "agentTurn",
    "message": "后台 myapi 进程日志命中 error 关键字,请读取上下文并简要说明可能原因,必要时给出排查命令。",
    "timeoutSeconds": 120
  }
}

要点:

  1. 进程由 OpenClaw 拉起并接管输出,你不用自己 nohup &;
  1. 正则加 \b 边界,避免 terror(如单词里含 error)误命中;
  1. 触发时整个运行上下文都会作为该次运行的输入,agent 能看到前后日志。

实操二:服务启动完成自动探活

很多服务启动慢,你不想傻等。用 match 盯 listening 关键字,命中就跑一次健康检查:

{
  "name": "svc-ready-check",
  "schedule": {
    "kind": "stream",
    "command": ["/srv/myapi/start.sh"],
    "mode": "match",
    "match": "listening on"
  },
  "trigger": {
    "once": true,
    "script": "fetch('http://127.0.0.1:8080/health').then(r=>r.ok?{fire:true}:{fire:false}).catch(()=>({fire:false}))"
  }
}

这里用了 trigger.once: true,表示命中一次后就自动失效,避免重复触发。配合 trigger.script 还能做二次判断(上面的例子里只有健康检查真通过才 fire)。

几个踩坑点

  • stream 触发器依赖 cron.triggers.enabled。如果该开关被关,系统会明确告诉你「trigger watcher disabled」,此时应改用定时轮询或手动方案,而不是硬等。
  • 触发器脚本必须只读。trigger.script 里只能读状态、返回 {fire,message,state},不能执行写操作;真正干活的动作放在 payload 里。
  • 去重靠 state,不靠记忆。脚本每次返回一个 state 对象,下次运行会带着它。想避免同一条日志反复报警,就把已处理的位置 / 哈希存进 state,下次比对。
  • 进程退出不等于触发。stream 看的是「输出内容」,进程正常退出没打印关键行,触发器不会 fire。要捕获「进程挂了」本身,得在 payload 之外另加存活检查。
  • 日志量很大时设 maxBatchBytes,否则一次缓冲几 MB 会把单次运行撑爆。

和 cron / every 怎么选

| 需求 | 用谁 |

| --- | --- |

| 每天 03:00 发文章 | cron |

| 每 5 分钟检查一次 | every |

| 进程一报错就通知 | stream |

| 服务启动完成做探活 | stream + once |

| 某时间点一次性提醒 | at |

一句话:时间你能预判就用 cron/every;只有「进程自己知道什么时候」才用 stream。

小结

stream 触发器本质是「把进程的标准输出变成事件源」。它补上了定时任务覆盖不到的盲区——事件驱动而非时间驱动。配置记住三件事:command 交给 OpenClaw 拉起、mode:"match" 配好正则、trigger.once 控制是否只跑一次。剩下的,让日志替你按铃。


扫码关注公众号,获取更多 OpenClaw 实操技巧:

公众号二维码