事件与调度

本章讲 Ameba-Claw「自己动起来」的两条主线。「事件路由与自动化」介绍入站事件如何按声明式规则匹配并触发动作(交给 Agent 推理、直接调用能力、跑脚本、转发消息等);「定时调度」介绍到点或周期性地触发任务,它既能直接执行动作,也能只发一个事件、把后续交给事件路由处理。两者配合,设备就能在无人干预时按规则和时间自主响应。

事件路由与自动化

Ameba-Claw 是**事件驱动**的:无论是 IM 消息、定时触发、还是外设中断,都被包装成统一的**事件(Event)**,投递给事件路由器 claw_event_dispatcher。路由器按一组**规则**匹配事件、执行**动作**——可以直接调能力、跑 Lua、唤醒 Agent、发消息,或再抛新事件。

这套机制让"收到 X 就做 Y"这类自动化无需改固件即可配置,也是理解整个 harness 的钥匙。

事件模型

事件由 claw_event_t 描述,主要字段:

字段

说明

event_id

事件唯一编号(缺省自动分配)

event_type

"message" (IM 消息)或自定义触发类型

source_cap

产生该事件的来源能力名

source_channel / target_channel

来源 / 目标通道(如 telegramfeishuweb

chat_id / sender_id / message_id

聊天、发送者、消息标识

event_key / correlation_id

事件键(供规则精确匹配)

content_type

内容类型(如 text

text / payload_json

消息正文 / JSON 负载(堆分配)

session_policy

会话策略:CHAT (按渠道+chat_id 归属会话)或 TRIGGER (按触发源归属会话)

发布事件

能力实现(尤其是 IM 网关)在合适时机通过 claw_event_publisher.h 的接口发布事件:

  • claw_event_dispatcher_publish_message(source_cap, channel, chat_id, text, sender_id, message_id) —— 发布一条文本消息事件。

  • claw_event_dispatcher_publish_trigger(source_cap, event_type, event_key, payload_json) —— 发布一条触发类事件。

  • claw_event_dispatcher_publish_event(src) —— 发布一个已完整填好各字段的事件(深拷贝全部字段),可指定会话与回复目标。

路由器在独立任务里异步处理队列中的事件,避免在发布方的回调栈里承担重活。

路由器的工作流程

事件从队列取出后,handle_event 按以下逻辑处理:

  1. 斜杠命令拦截:若是来自已注册 IM 通道的 message 事件,先尝试当作会话斜杠命令(/new/list 等)处理;命中则直接返回,不进入规则匹配。

  2. 规则匹配:在锁内逐条与规则集匹配,命中的规则快照到堆上(保留各自的上下文),受**冷却时间**约束(cooldown_ms 内不重复触发)。命中一条 consume_on_match=true 的规则后停止继续匹配。

  3. 动作执行:脱锁后,对每条命中的规则逐一执行其动作列表。

  4. 兜底路由:若没有任何规则命中,且事件是 message 类型、且路由器配置了 default_route_messages_to_agent,则把消息默认交给 Agent 推理(并先给来源通道发一句"处理中"的即时 ack)。

  5. 调度器内建路由:来自 cap_scheduler 的 Agent 唤醒事件即使没有任何用户规则匹配,也会直接送进 Agent——一个定时任务即一个意图,无需额外规则(用户规则匹配了则以用户规则为准)。

规则文件

规则持久化在 vfs:/router_rules/router_rules.json,是一个规则对象数组,顺序即匹配顺序。每条规则的字段:

字段

说明

id

规则唯一标识

enabled

是否启用(缺省 true

consume_on_match

命中后是否阻止继续向下匹配(缺省 false

ack

命中后可选的即时确认消息模板(空 = 默认)

cooldown_ms

两次触发之间的最小间隔毫秒(0 = 不限)

vars

规则级变量对象,可在模板里用 @{vars.*} 引用

match

匹配条件对象(见下)

actions

动作列表(见下)

匹配条件 match

字段

说明

event_type

匹配事件类型(如 message、自定义触发类型)

source_cap

匹配来源能力名

channel / chat_id

匹配来源通道 / 聊天 ID

event_key

与事件 correlation_id 精确匹配

text_contains

正文子串匹配(空 = 不限)

text + text_match_rule

正文模式匹配。text_match_ruleexact (精确相等)或 prefix (前缀命令式);prefix 会左裁剪正文并把命令后面的参数暴露为 @{match.remainder}

留空的字段视为通配。所有非空字段都命中才算匹配。

动作列表 actions

每个动作对象用 kind 指定类型:

kind

用途

rtk_agent

唤醒 Agent 处理该事件(异步);不经过额外配置就能回复来源会话

rtk_cap

不经过 LLM,直接调用某个能力。cap = 能力 id;input = 参数(对象/数组,作为模板渲染)

rtk_script

运行一个 Lua 脚本。script = 绝对 .lua 路径;mode = sync 时阻塞直到脚本返回(其结果可经 @{last.output} 传递给后续动作),否则异步;input = 传给脚本的 JSON 参数

rtk_send

向 IM 通道发送消息。目标通道依次取 cap 字段 → 事件的 target_channel → 来源通道;input = 纯文本消息模板

rtk_emit

生成一个新事件再交给路由器(可实现 A→B 规则链);input 里的 event_type / text / payload 覆写派生事件的对应字段

rtk_drop

丢弃事件

动作还支持这些可选字段:

  • on_error —— 该动作失败后:continue (默认,继续本规则后续动作)或 stop (停止本规则剩余动作)。

  • capture_output —— 把本动作的输出写入 @{last.output},供后续动作引用。

  • only_if —— 单条件守卫 { "left": "@{...}", "op": "...", "right": "..." }op 支持 eq / ne / gt / lt / ge / le / contains / exists。两侧都能解析为数字时按数值比较,否则按字符串比较。守卫不成立则跳过该动作。

模板占位符

动作的 inputack 等模板里可以用 @{点分路径} 引用运行时数据;未命中的占位符渲染为空(不会漏出 @{...} 原文)。可用的根:

内容

@{event.*}

事件字段全名,如 @{event.text}@{event.chat_id}@{event.payload.foo}

@{ev.*}

事件字段短别名,如 @{ev.type}@{ev.text}

@{vars.*}

规则级变量(规则的 vars 对象)

@{match.text} / @{match.rule} / @{match.remainder}

文本匹配信息;remainderprefix 命令后的剩余参数

@{rule.id}

当前规则 id

@{last.output}

上一个 capture_output 动作的输出

对于 rtk_cap / rtk_emitinput 是对象,会在节点级渲染(引号安全);对于 rtk_sendinput 是纯字符串模板。

规则示例

前缀命令:收到 /run <脚本> 就跑对应 Lua 脚本,并把命令后的参数传进去:

{
  "id": "run_command",
  "match": { "event_type": "message", "text": "/run", "text_match_rule": "prefix" },
  "actions": [
    {
      "kind": "rtk_script",
      "script": "vfs:/scripts/runner.lua",
      "input": { "command": "@{match.remainder}" }
    }
  ]
}

把定时提醒事件转发到 Telegram:

{
  "id": "morning_reminder_to_tg",
  "match": { "event_type": "schedule", "event_key": "reminder" },
  "actions": [
    {
      "kind": "rtk_send",
      "cap": "telegram",
      "input": "@{event.text}"
    }
  ]
}

出站通道

rtk_send 动作最终通过出站通道注册表 claw_im_dispatch 把消息发出去:每个 IM 通道在装配时把自己的发送函数按通道名(telegramfeishuqqwechatwebserial)登记进去,路由器按目标通道名找到对应发送函数调用。

用工具动态管理规则

cap_router_mgr 向 LLM(以及 AT 控制台)暴露一组工具,可在运行时增删查改路由规则,改动即时生效并持久化到 router_rules.json

工具

说明

router_list_rules

列出全部规则

router_get_rule

按 id 查单条规则

router_add_rule

新增一条规则

router_update_rule

按 id 更新一条规则

router_remove_rule

按 id 删除一条规则

router_reload_rules

从磁盘重新加载全部规则

也可以在 Web 后台的文件管理里直接编辑 router_rules.json,编辑后调用 router_reload_rules 使其生效。

与定时任务、Agent 的衔接见 定时调度Agent 核心

定时调度

cap_scheduler 是 Ameba-Claw 的**时间触发中心**:到点/周期性地"做一件事"。它可以自己直接执行动作,也可以只发一个事件、把后续处理交给事件路由(见 事件路由与自动化)。任务持久化在 vfs:/scheduler/ 下,重启后自动恢复进度。

一个调度任务 = 触发(何时) + 动作(做什么)

通过 scheduler_add_job 创建或更新(按 id upsert)一个任务,任务由**触发**和**动作**两部分组成。

触发类型(kind

kind

说明

cron

重复的挂钟时间。cron_expr = 5 段 ,支持范围/列表/步进,例如 0 9 * * 1-5 = 周一到周五 09:00,30 8 * * * = 每天 08:30

once

一次性。at = YYYY-MM-DD HH:MM (本地时间)或 in_sec = 从现在起的秒数。触发一次后自动删除

interval

每 N 秒。设 interval_sec

on_event

系统事件触发。trigger_event 目前支持 wifi_connected (设备联网,每次开机触发一次,早于时钟同步)——这是"开机即运行"的触发器

动作类型(action

action

说明

agent (默认)

prompt = 自然语言指令;触发时 Agent 在你的会话里执行它、可用任意工具(搜索、发消息等),适合提醒/"查新闻告诉我"这类任务

cap

cap_id (+ cap_args)直接调用某能力(确定性、不走 LLM),例如跑一个 Lua 脚本

emit

event_type (+ payload_json)发布一个原始事件,交给路由规则处理(进阶)

可选字段:max_runs (0 = 不限)、end_at (本地时间,到点后停止)、id (复用以更新已有任务)。

备注

crononce 依赖准确的系统时间**并且**需要已配置时区;若 get_local_time 报"时区未配置",需先让用户设时区(set_timezone)。intervalon_event 不依赖时钟,上电即可计时。

调度与执行分离

cap_scheduler 本身不含具体业务逻辑。当动作是 agent 时,调度器发一个 Agent 唤醒事件——事件路由对来自 cap_scheduler 的这类事件有**内建路由**,即使没有任何用户规则也会直接唤醒 Agent(一个定时任务即一个意图)。当动作是 emit 时,调度器发布原始事件,具体行为由你在 router_rules.json 里写的匹配规则决定。

这种"调度与执行分离"的设计,让定时任务能复用事件路由的全部动作能力。

暴露给 LLM 的工具

工具

说明

scheduler_add_job

新增或更新(按 id upsert)一个调度任务

scheduler_list_jobs

列出所有任务及运行状态、下次触发时间、运行/错过次数

scheduler_get_job

按 id 获取单个任务的完整详情

scheduler_remove_job

按 id 删除任务

scheduler_enable_job / scheduler_disable_job

启用 / 禁用任务(禁用保留定义、停止触发)

scheduler_pause_job / scheduler_resume_job

临时暂停 / 恢复(如"跳过今天的闹钟")

scheduler_trigger_now

立即执行一次任务的动作(用于测试),不改变其调度计划

示例

每天早上 8 点在 Telegram 提醒(动作走 Agent,自动回到发起会话):

scheduler_add_job {
  "kind": "cron",
  "cron_expr": "0 8 * * *",
  "action": "agent",
  "prompt": "告诉用户该起床了,并顺带播报今天天气"
}

开机即运行一个后台 Lua 脚本(动作走 cap,确定性):

scheduler_add_job {
  "kind": "on_event",
  "trigger_event": "wifi_connected",
  "action": "cap",
  "cap_id": "lua_run_async",
  "cap_args": { "path": "vfs:/scripts/monitor.lua" }
}

时间同步

调度器依赖系统时间。设备上电后可能先基于不准确的时钟计时;首次 SNTP 同步成功后,系统会对调度器做一次时间校准(rebase),修正各任务的参考时间点,避免重复或遗漏触发。因此 cron / once 任务在时钟同步成功前不会触发。