Cron 内部机制
Cron 子系统提供计划任务执行功能——从简单的一次性延迟任务到重复执行的 cron 表达式任务,支持技能注入和跨平台投递。
关键文件
| 文件 | 用途 |
|---|---|
cron/jobs.py | 任务模型、存储、对 jobs.json 的原子读写 |
cron/scheduler.py | 调度器循环——到期任务检测、执行、重复跟踪 |
tools/cronjob_tools.py | 面向模型的 cronjob 工具注册和处理器 |
gateway/run.py | 网关集成——在长运行循环中执行 cron 滴答 |
hermes_cli/cron.py | CLI hermes cron 子命令 |
调度模型
支持四种调度格式:
| 格式 | 示例 | 行为 |
|---|---|---|
| 相对延迟 | 30m、2h、1d | 一次性,在指定时长后触发 |
| 间隔 | every 2h、every 30m | 重复执行,按固定间隔触发 |
| Cron 表达式 | 0 9 * * * | 标准 5 字段 cron 语法(分钟、小时、日、月、星期) |
| ISO 时间戳 | 2025-01-15T09:00:00 | 一次性,在指定时间精确触发 |
面向模型的接口是一个单一的 cronjob 工具,支持动作式操作:create、list、update、pause、resume、run、remove。
任务存储
任务存储在 ~/.hermes/cron/jobs.json 中,采用原子写入语义(先写入临时文件,再重命名)。每个任务记录包含:
{
"id": "a1b2c3d4e5f6",
"name": "每日简报",
"prompt": "总结今天的人工智能新闻和融资情况",
"schedule": {
"kind": "cron",
"expr": "0 9 * * *",
"display": "0 9 * * *"
},
"skills": ["ai-funding-daily-report"],
"deliver": "telegram:-1001234567890",
"repeat": {
"times": null,
"completed": 42
},
"state": "scheduled",
"enabled": true,
"next_run_at": "2025-01-16T09:00:00Z",
"last_run_at": "2025-01-15T09:00:00Z",
"last_status": "ok",
"created_at": "2025-01-01T00:00:00Z",
"model": null,
"provider": null,
"script": null
}任务生命周期状态
| 状态 | 含义 |
|---|---|
scheduled(已调度) | 活跃,将在下一个计划时间触发 |
paused(已暂停) | 已挂起——恢复前不会触发 |
completed(已完成) | 重复次数已用尽,或一次性任务已触发 |
running(运行中) | 当前正在执行(瞬态状态) |
向后兼容
较旧的任务可能包含单个 skill 字段而非 skills 数组。调度器在加载时会自动归一化——单个 skill 被提升为 skills: [skill]。
调度器运行机制
滴答周期
调度器按周期性的滴答运行(默认:每 60 秒):
tick()
1. 获取调度器锁(防止滴答重叠)
2. 从 jobs.json 加载所有任务
3. 筛选到期任务(next_run <= now 且 state == "scheduled")
4. 对每个到期任务:
a. 将状态设置为 "running"
b. 创建全新的 AIAgent 会话(无对话历史)
c. 按顺序加载附加的技能(作为用户消息注入)
d. 通过代理运行任务提示
e. 将响应投递到配置的目标
f. 更新运行次数,计算下一次运行时间
g. 如果重复次数已用尽 → 状态设为 "completed"
h. 否则 → 状态设为 "scheduled"
5. 将更新后的任务写回 jobs.json
6. 释放调度器锁网关集成
在网关模式下,调度器在专用的后台线程中运行(gateway/run.py 中的 _start_cron_ticker),该线程在处理消息的同时每 60 秒调用一次 scheduler.tick()。
在 CLI 模式下,cron 任务仅在运行 hermes cron 命令或在活跃的 CLI 会话期间触发。
全新会话隔离
每个 cron 任务在全新的代理会话中运行:
- 不保留先前运行的对话历史
- 不记忆之前的 cron 执行记录(除非持久化到记忆/文件中)
- 提示必须自包含——cron 任务无法提出澄清性问题
cronjob工具集被禁用(递归防护)
技能支持的任务
cron 任务可以通过 skills 字段附加一个或多个技能。执行时:
- 按指定顺序加载技能
- 每个技能的 SKILL.md 内容作为上下文注入
- 任务的提示作为任务指令追加
- 代理处理组合后的技能上下文和提示
这使得可重用、经过测试的工作流成为可能,而无需将完整指令粘贴到 cron 提示中。例如:
创建每日融资报告 → 附加 "ai-funding-daily-report" 技能
脚本支持的任务
任务还可以通过 script 字段附加一个 Python 脚本。该脚本在每次代理轮次之前运行,其标准输出作为上下文注入到提示中。这支持数据收集和变化检测模式:
# ~/.hermes/scripts/check_competitors.py
import requests, json
# 获取竞争对手发布说明,与上次运行进行差异比较
# 将摘要输出到标准输出——代理进行分析和报告脚本超时默认为 120 秒。_get_script_timeout() 通过三层链解析超时限制:
- 模块级覆盖——
_SCRIPT_TIMEOUT(用于测试/猴子补丁)。仅在与默认值不同时使用。 - 环境变量——
HERMES_CRON_SCRIPT_TIMEOUT - 配置——
config.yaml中的cron.script_timeout_seconds(通过load_config()读取) - 默认值——120 秒
提供者恢复
run_job() 将用户配置的回退提供者和凭据池传递给 AIAgent 实例:
- 回退提供者——从
config.yaml读取fallback_providers(列表)或fallback_model(旧版字典),匹配网关的_load_fallback_model()模式。作为fallback_model=传递给AIAgent.__init__,该方法将两种格式归一化为回退链。 - 凭据池——通过
agent.credential_pool中的load_pool(provider)加载,使用解析后的运行时提供者名称。仅在池中有凭据时传递(pool.has_credentials())。在 429/速率限制错误时启用同提供者密钥轮换。
这镜像了网关的行为——没有它,cron 代理会在遇到速率限制时直接失败而不尝试恢复。
投递模型
Cron 任务结果可以投递到任何支持的平台:
| 目标 | 语法 | 示例 |
|---|---|---|
| 源聊天 | origin | 投递到创建任务的聊天 |
| 本地文件 | local | 保存到 ~/.hermes/cron/output/ |
| Telegram | telegram 或 telegram:<chat_id> | telegram:-1001234567890 |
| Discord | discord 或 discord:#channel | discord:#engineering |
| Slack | slack | 投递到 Slack 首页频道 |
whatsapp | 投递到 WhatsApp 首页 | |
| Signal | signal | 投递到 Signal |
| Matrix | matrix | 投递到 Matrix 首页房间 |
| Mattermost | mattermost | 投递到 Mattermost 首页 |
email | 通过电子邮件投递 | |
| SMS | sms | 通过短信投递 |
| Home Assistant | homeassistant | 投递到 HA 对话 |
| DingTalk(钉钉) | dingtalk | 投递到钉钉 |
| Feishu(飞书) | feishu | 投递到飞书 |
| WeCom(企业微信) | wecom | 投递到企业微信 |
| Weixin(微信) | weixin | 投递到微信 |
| BlueBubbles | bluebubbles | 通过 BlueBubbles 投递到 iMessage |
| QQ Bot | qqbot | 通过官方 API v2 投递到 QQ |
对于 Telegram 话题,使用格式 telegram:<chat_id>:<thread_id>(例如 telegram:-1001234567890:17585)。
响应包装
默认情况下(cron.wrap_response: true),cron 投递会包装以下内容:
- 标识 cron 任务名称和任务的头部
- 说明代理无法在对话中看到已投递消息的尾部
cron 响应中的 [SILENT] 前缀会完全抑制投递——适用于仅需写入文件或执行副作用的任务。
会话隔离
Cron 投递不会镜像到网关会话的对话历史中。它们仅存在于 cron 任务自己的会话中。这防止了目标聊天对话中出现消息交替违规。
递归防护
Cron 运行的会话中禁用了 cronjob 工具集。这防止了:
- 调度任务创建新的 cron 任务
- 递归调度导致令牌消耗暴涨
- 在任务内部意外修改任务调度
锁定
调度器使用跨进程的基于文件的锁定(Unix 上使用 fcntl.flock,Windows 上使用 msvcrt.locking)来防止重叠滴答执行同一到期任务批次两次——即使在网关的进程内滴答器和独立的 hermes cron / 手动 tick() 调用之间也是如此。如果无法获取锁,tick() 立即返回 0。
CLI 接口
hermes cron CLI 提供直接的任务管理:
hermes cron list # 显示所有任务
hermes cron create # 交互式任务创建(别名:add)
hermes cron edit <job_id> # 编辑任务配置
hermes cron pause <job_id> # 暂停运行中的任务
hermes cron resume <job_id> # 恢复已暂停的任务
hermes cron run <job_id> # 立即触发执行
hermes cron remove <job_id> # 删除任务