Skip to content

触发器(Triggers)

1.3.0 新增

触发器系统自 v1.3.0 起提供。在 v1.2.0 及更早版本里,工作流和员工对话只能被手动调起。

触发器是什么:把"系统里发生的事件"和"要执行的动作"连起来。事件可以是定时(cron)、是 webhook 来了、是某个渠道收到消息、是某个员工跑完了某次对话、是另一个工作流跑完了。动作可以是启动某个工作流,也可以是直接给某个员工发消息让它处理。

触发器不是什么

  • 不是 cron 任务管理器替代品——mate_cron_job 仍然存在并独立运作;触发器复用它的 ShedLock + 调度器底座,但不写入 mate_cron_job
  • 不是 IFTTT / n8n 风格的可拖拉自动化——触发器只负责"事件 → 动作"的路由;复杂逻辑放到 工作流
  • 不是 webhook 的全功能 dispatcher——它只做去重 / 限流 / bot self-msg 过滤 / pattern 匹配,不替你解析复杂业务报文

v1.3.0 范围

v0 = 6 种 pattern type + 2 种 dispatch target(agent / workflow)。安全治理(事件去重、per-trigger rate limit、循环保护、bot 自消息过滤)是默认开的。


一分钟看懂

jsonc
// 触发器:每天早上 9 点跑一次"晨报工作流"
{
  "name": "daily-morning-report",
  "patternType": "cron",
  "patternJson": {
    "cronExpression": "0 0 9 * * *",
    "timezone": "Asia/Shanghai"
  },
  "targetType": "workflow",
  "targetId": 12345,
  "payloadTemplate": "{ \"date\": \"{{ now | date('yyyy-MM-dd') }}\" }",
  "rateLimitPerMin": 10,
  "dedupWindowSecs": 60,
  "botSelfFilter": true,
  "enabled": true
}

每天 9 点 → 后端通过 CronDelegationPort 抢到 ShedLock 锁 → 渲染 payload → 调起 workflow 12345 异步运行。其它实例同一时刻被锁挡住,不会重复触发。


6 种 pattern type

实现在 TriggerPatternMatcher.java。每个 pattern 对应 trigger 行的 pattern_json 列里一段 JSON。未列出的字段表示 v0 不识别——matcher 对未知字段直接忽略。

Pattern触发时机pattern_json 字段复用约束
cron按 cron 表达式定时(不进 ingest 管道,由 scheduler 直跑)cronExpressiontimezone复用 cron/ 模块的 ShedLock + Spring TaskScheduler;不写 mate_cron_job 实体、不调 CronJobService
webhook通用事件入口透传(v0 不做更细过滤——secret 校验在 channel 层;trigger 这边只看 patternType=webhook 命中)(v0 无字段)通过 POST /api/v1/triggers/events 入口 + envelope wrap
channel_message渠道收到消息channelType(可选,按 envelope data.channelType 比对)、senderEquals(可选,按 sender id 精确比对)旁路 ChannelWebhookController,原路由不变
agent_lifecycle员工生命周期事件agentId(可选)、phase(可选,取值 spawned / terminated / crashed挂在 ReActLifecycleListener
content_match内容包含 substring 才命中substring必填,envelope 的 data.content 字段大小写不敏感包含匹配)通用过滤层,事件源由 envelope 决定
workflow_completion工作流跑完进入终态sourceWorkflowId(可选)、stateFilter(可选,取值 completed / failed / any监听 WorkflowEngine 终态事件;A→B→A 递归保护见下文

未知 pattern type 默认 fail-closed——typo 或将来加的 pattern 不会偷偷把 workspace 内所有 trigger 都点燃。

不在 v1.3.0 里schedule(不带 cron 的定时如"30 分钟后")、外部 MQ 监听(Kafka / Pulsar / RocketMQ)、metrics / threshold 告警触发。


事件治理(默认开)

Bot self-msg 过滤(默认绑定为 noop)

某些渠道(飞书 / 钉钉 / 企微)会把 bot 自己发的消息也回流为 channel_message 事件。框架层通过 trigger 行的 bot_self_filter 字段(默认 true)+ BotSelfFilter SPI 协作过滤。

v0 默认实现是 noop

开箱默认绑的是 NoopBotSelfFilter——isBotSelf(...) 永远返回 false。这意味着 bot_self_filter=true 的 trigger 现在不真过滤任何事件。要让过滤真正生效,需要 channel 适配器侧注册一个真正能识别自己 bot id 的 BotSelfFilter Spring Bean(替换默认实现)。这是有意设计的——避免一个错误的 default 实现把所有合法的 bot 间通讯都误杀。

要单独让一条 trigger 接受自己 bot 的消息(极少见,比如 bot 发特殊命令触发清理流程),把这条 trigger 的 bot_self_filterfalse

事件去重

事件经 TriggerEventIngestService 派发时,引擎在 mate_trigger_event 表上查 dedup_key 是否已经在 dedupWindowSecs(默认 60s)时窗内入过库。已经在 → 直接丢弃,连 fire_count 都不++。

默认 dedupWindowSecs = 60。提高这个值可以扛更长时间的网关重投递;调到 0 关闭去重(不推荐)。

Per-trigger rate limit

每个 trigger 单独限速:1 分钟最多 rateLimitPerMin 次(默认 10)。命中限速的事件被丢弃,重试,mate_trigger_event 行;mate_trigger.last_error 字段会被刷成 "rate-limited" 便于运维查。

channel_message 类 trigger 通常要调高(瞬时群发);workflow_completion 类通常调低(防止 A→B→A 链路加速)。

递归循环保护

workflow_completion trigger 启动的 workflow 又触发另一个 workflow_completion……dispatch 链超过 5 层 → 引擎切断 + 告警。这是防止"A 写消息触发 B,B 写消息又触发 A"递归。

Webhook ACK 时序

HTTP 入口(POST /api/v1/triggers/events)收到事件 → envelope wrap → dedup check → bot-self check → rate limit check → 立即 ACK 200 → 异步 dispatch。这意味着:

  • 上游网关(飞书 / 钉钉 等)拿到 200 就不再重投
  • 实际 dispatch 失败 → mate_trigger.last_error 被刷新;同 dedup_key 再来仍然被去重挡掉,不重试

如果你需要"dispatch 成功才 ACK"语义,目前没有——v0 故意设计为 fire-and-forget 扛峰值。


在 UI 里管理触发器

1.4.0 调整:合并进"调度中心"

v1.4.0 起,定时任务触发器合并为单个调度中心页面(设置 → 调度中心,路由 /settings/scheduler),分三个 tab:计划任务(Scheduled Jobs)/ 事件触发器(Event Triggers)/ 运行历史(Run History)。每个 tab 标题旁带条目计数;右上角动作按钮随当前 tab 变化(计划任务 / 触发器 tab 是"新建",历史 tab 是"刷新");运行历史横跨两者,定时任务和触发器的执行记录都在这里看。

老路由会自动重定向:/cron-jobs/settings/triggers 分别落到调度中心对应的 tab。

入口

设置 → 调度中心(侧栏)→ 事件触发器 tab。触发器列表在 v1.4.0 里从原来的宽表格改版为规则卡片——每条 trigger 一张卡,pattern type / target / 启停状态一目了然。点 + 新建触发器 打开抽屉。

创建 trigger

抽屉里按 6 种 pattern type 各自结构化表单填字段——不需要手写 pattern_json

  • cron → cron 表达式输入框 + 时区下拉 + 下一次触发时间预览。表达式可手输,也可点输入框旁的编辑按钮打开可视化 cron 编辑器(见下)
  • channel_message → 渠道类型可选 + (可选)按 sender id 精确匹配
  • agent_lifecycle → agent 可选 + phase(spawned / terminated / crashed)可选
  • content_match → substring 输入(必填),匹配 envelope 的 data.content
  • workflow_completion → 上游 workflow 可选 + state filter(completed / failed / any)可选
  • webhook → v0 没有额外字段(透传一切)

填完保存 → trigger 入库;enabled=true 时立即注册到对应引擎(cron 注册到 ShedLock;其它走 envelope 路由)。

可视化 cron 编辑器(1.4.0 新增)

cron 表达式不必手写。点表达式输入框旁的编辑按钮打开分段编辑器:分钟 / 小时 / 日 / 月 / 星期 各占一个 tab,每段可选"每个 / 指定值 / 区间 / 步进";上方一排预设(每分钟、整点、每天午夜、每周一……)一键填入;底部是实时可读预览,把当前表达式翻译成人话(例如"每天 09:00")。

这个编辑器是计划任务和触发器共用的同一个组件:

  • 计划任务5 段 cron(分 时 日 月 周)
  • 触发器6 段 cron(带秒:秒 分 时 日 月 周)——多出最前面的秒字段

输入框本身也带一行可读预览,不打开编辑器也能确认你手输的表达式解析成了什么。


调度任务类型(task type)

调度中心 计划任务 tab 里的每条任务都有一个 task_type,决定它跑起来做什么。这是 cron 任务类型的权威清单(事件触发器的 6 种 pattern type 见上文):

task type行为是否绑定员工备注
text / agent / reminder按 cron 调起一次员工对话(必填 agent)经典定时对话;结果路由到对应会话
wiki_process按 cron 离线处理某个知识库1.4.0 新增——见下

wiki_process:错峰处理知识库(1.4.0 新增)

wiki_process 让你把知识库的处理安排到业务低峰时段离线跑,而不是上传完就立刻占满处理队列。它不绑定任何员工——它是个系统任务,不开对话、不进聊天。

新建时只需要填:

  • cron 表达式(用上面的可视化编辑器,5 段)
  • 知识库选择器——这次任务要处理哪个 KB
  • 可选的 "强制重新处理" 开关——开了就连已处理过的原始材料一起重跑(force

每次到点,任务把该 KB 的原始材料异步入队处理,并在运行历史里记一行结果,形如 queued N raw material(s)(开了强制会带 (force) 后缀)。注意它不路由到任何对话——它只是把活儿丢进处理队列,进度去 LLM Wiki 页面看。

Payload template

payload_template 字段是 Pebble 模板字符串,渲染后作为 dispatch target(agent 对话或 workflow run)的输入。

jsonc
"payload_template": "{
  \"date\": \"{{ now | date('yyyy-MM-dd') }}\",
  \"trigger\": \"{{ trigger.name }}\",
  \"sourceEvent\": {{ event | toJson }}
}"

模板可访问的变量:

  • now —— 当前时间
  • trigger.{name,id,workspaceId} —— 当前触发器
  • event —— 当前事件 envelope(workspaceId / senderId / data JSON 等)

查看触发历史

mate_trigger_event 表存的是去重元数据——一行记录含 trigger_id / dedup_key / received_at / expires_at,不存 envelope 副本本身。要审计具体一次事件的内容,查 mate_trigger.last_error + dispatch 日志。

mate_trigger.fire_count 诚实记录有效 dispatch 次数(不计被去重 / 限速过滤掉的);mate_trigger.last_error 记录最近一次失败原因。


API 参考

所有 endpoint 在 /api/v1/triggers/ 下。v1.3.0 实际暴露的就这些——RFC 里规划的 /webhook/{slug} / /test-fire / /{id}/events 暂未实装。

方法路径说明
GET/api/v1/triggers列当前 workspace 所有 trigger
GET/api/v1/triggers/{id}获取详情
POST/api/v1/triggers新建 trigger;若 enabled=true 立即注册到 scheduler / 路由
PUT/api/v1/triggers/{id}更新(包括启用 / 禁用——改 enabled 字段即可);pattern_json 改动时 pattern_version++,跨实例自取消旧 future
DELETE/api/v1/triggers/{id}软删(等同禁用)
POST/api/v1/triggers/events统一事件入口——任何 webhook / channel adapter / 内部模块送一份 envelope 进来;引擎做 dedup / bot-self / rate limit / pattern match / dispatch;返回 per-trigger 命中 / 丢弃汇总

跟现有 cron 模块的关系

不取代,只复用

v1.3.0 之前 MateClaw 已经有一个独立的 cron 子系统(mate_cron_job 表 + CronJobService)。Trigger 系统不取代它——

  • 老的 cron 任务(task_type = text / agent / reminder)仍然在 Cron Jobs 页面管理
  • 新的 trigger cron 在 Triggers 页面管理
  • 两者共享底层 ShedLock 锁表 + Spring TaskScheduler 线程池
  • mate_cron_job 列表不会显示 trigger cron;反过来也是

为什么不合并?因为 mate_cron_job 老表的 task_type / agentId 必填等字段不适合 workflow target。强行扩列会破坏既有 product 约束。CronDelegationPort 是 v0 的最小化解——共享调度底座,分离持久层。mate_cron_job 整体收敛到 trigger 是后续版本的工作。


跨实例一致性(多副本部署)

CronDelegationPort 的所有方法是进程局部的——本地 ScheduledFuture 只在本 JVM 注册,不持久化 handle。跨实例靠:

  1. 每个实例启动时调 syncFromDatabase() 扫所有 enabled cron trigger 注册本地
  2. 修改 trigger 时 pattern_version++ + 取消本地 future
  3. 每次 fire 前重新读 trigger 行,patternVersion 不匹配则本地短路自取消(说明被别的实例改过)
  4. ShedLock 锁名 = "mate-trigger-{triggerId}",跨实例互斥
  5. 周期 @Scheduled(fixedDelay=60s) syncFromDatabase() 兜底收敛

实战意义:你正常 rolling-deploy 多副本不需要做任何额外动作——新实例起来自动接管,老实例本地 future 走完最后一轮就停。


数据模型

mate_trigger —— 触发器配置

主要字段:

字段类型用途
pattern_typevarchar6 种 pattern 之一
pattern_jsonTEXT该 pattern 的过滤参数 JSON
target_typevarcharagentworkflow
target_idbigint对应 agent / workflow 主键
payload_templateTEXTPebble 渲染模板
dedup_window_secsint去重窗口(秒)
rate_limit_per_minint每分钟最大 fire 次数
bot_self_filterbool是否启用 bot self 过滤(默认 true,但默认实现是 noop)
pattern_versionbigint乐观并发 lamport 计数器,每次 pattern_json 改动 +1;跨实例 fire 前比对自取消
fire_countbigint有效 dispatch 次数(不计去重 / 限速过滤掉的)
last_errorvarchar最近一次失败原因(含 "rate-limited" / 异常 message)
enabledbool软启停开关
deletedint软删标志

mate_trigger_event —— 去重元数据

仅用于去重判定,不存 envelope 副本本身

字段类型用途
idbigint主键
trigger_idbigint关联 trigger
dedup_keyvarchar唯一索引,引擎按此 key 在 dedup_window_secs 时窗内做去重判定
received_attimestamp入库时间
expires_attimestamp去重窗口过期时间,超过此点同 key 可以重新入库

设计取舍

v0 故意不把 envelope 全文写进 mate_trigger_event——大体量渠道事件全量持久化撑不住库。事件正文的审计依赖 channel 层日志 + agent / workflow 层的 run 记录。如果未来需要"事件回放"等能力,再加 envelope 持久化列。


已知限制(v1.3.0)

  • 没有可视化 trigger → workflow 串联图——多 trigger 投递到同 workflow 在 UI 上看是两个独立列表
  • 没有 trigger 间优先级 / 依赖——同一事件命中多 trigger 时按数据库 id 升序串行 dispatch
  • Webhook 入口没鉴权 IP allowlist——只有 secret header;如果你需要更强的 IP 限制,前置 nginx / 网关
  • agent_lifecycle 不区分会话级和 step 级——员工一次对话内多次 step 失败只会触发一次 failed
  • 没有事件回放——mate_trigger_event 是只读历史,没有"重新派发这条事件"的按钮(v1 加)

故障排查

现象排查
Cron trigger 没触发1) enabled=true? 2) cron 表达式 + 时区是否解析为下次时间?UI 编辑器有预览; 3) ShedLock 锁是否被另一实例长持?查 shedlock
事件 POST /events 返回 200 但 dispatch 没发生返回体里有 per-trigger fire / drop 汇总——看是否被 BOT_SELF / RATE_LIMITED / DEDUPED / PATTERN_MISMATCH 标了原因
channel_message 触发不起来1) envelope 的 data.channelType 拼写大小写是否和 trigger 的 pattern_json.channelType 匹配?2) bot_self_filter=true 但有自定义 BotSelfFilter 实现把它过掉了?3) content_matchsubstring 是否真的出现在 envelope 的 data.content
agent_lifecycle 没触发检查 pattern_json.phasespawned / terminated / crashed 之一(不是 started / completed / failed
重启后 cron trigger 不再触发看启动日志 syncFromDatabase() 是否报错;常见是表损坏 / pattern_json 反序列化失败
mate_trigger.last_error"rate-limited"调高 rate_limit_per_min 或者把 trigger 拆成多条按 group 分流
bot_self_filter=true 没起作用确认 BotSelfFilter 是否真有非 noop 实现——默认 NoopBotSelfFilter 永远返回 false

相关链接