早上 6:00,你还在睡觉,日报自己跑完了。collect.py 抓完四个源,curate.py 调 Hermes 把条目整理成正文,render.py 出网页------一条链在一个没人看着的时刻走完,执行记录落在磁盘上,等你起床再翻。
上篇回顾:推送有了,还差到点自动跑。那会儿跑一次日报要手动敲三下------collect、curate、render 各一条命令。这不叫自动化,叫「每天记得按一下」。这篇把「人肉定时」换成 Hermes 自己的调度器:用 cron 子命令建一个真实的 job,每天 6:00 把管线自动跑完。
这篇做两件事:用 cron 把 collect→curate→render 串成真实的定时任务;把「无人值守」的前提和兜底讲透。前提是 gateway 常驻,兜底是跨天去重。这两条,恰恰是大多数定时任务教程不讲的部分。
手动三步的尽头是无人值守
先把这轮要加的东西放进全景图,后面每一节都是给图里的一格装细节。

调度不是另起炉灶,是载体项目走到这的自然演化------手动三步跑顺了,下一步就是让它自己跑。
hermes cron 全家桶:十个子命令
先看命令面。hermes cron --help 把全部子命令列得很清楚,逐字:
text
usage: hermes cron [-h] [--accept-hooks]
{list,create,add,edit,pause,resume,run,remove,rm,delete,status,runs,history,tick}
...
Manage scheduled tasks
positional arguments:
{list,create,add,edit,pause,resume,run,remove,rm,delete,status,runs,history,tick}
list List scheduled jobs
create (add) Create a scheduled job
edit Edit an existing scheduled job
pause Pause a scheduled job
resume Resume a paused job
run Run a job on the next scheduler tick
remove (rm, delete)
Remove a scheduled job
status Check if cron scheduler is running
runs (history) Show durable execution attempts
tick Run due jobs once and exit
options:
-h, --help show this help message and exit
--accept-hooks Auto-approve unseen shell hooks without a TTY prompt
(equivalent to HERMES_ACCEPT_HOOKS=1 /
hooks_auto_accept: true).
十个子命令(usage 里多出来的 add/rm/delete/history 是别名,不算单独功能),按用途分两组:管理 job 本身(list/create/edit/pause/resume/remove),触发与追溯(run/status/runs/tick)。顶层那个 --accept-hooks 是让无人值守时自动批准未见过的 shell 钩子,等同 HERMES_ACCEPT_HOOKS=1,后面手动触发时会用到。
create 的关键选项,hermes cron create --help 逐字:
text
usage: hermes cron create [-h] [--name NAME] [--deliver DELIVER]
[--repeat REPEAT] [--skill SKILLS] [--script SCRIPT]
[--no-agent] [--workdir WORKDIR]
schedule [prompt]
positional arguments:
schedule Schedule like '30m', 'every 2h', or '0 9 * * *'
prompt Optional self-contained prompt or task instruction
options:
-h, --help show this help message and exit
--name NAME Optional human-friendly job name
--deliver DELIVER Delivery target: origin, local, telegram, discord,
signal, or platform:chat_id
--repeat REPEAT Optional repeat count
--skill SKILLS Attach a skill. Repeat to add multiple skills.
--script SCRIPT Path to a script under ~/.hermes/scripts/. Default mode:
script stdout is injected into the agent's prompt each
run. With --no-agent: the script IS the job and its
stdout is delivered verbatim. .sh/.bash files run via
bash, everything else via Python.
--no-agent Skip the LLM entirely --- run --script on schedule and
deliver its stdout directly. Empty stdout = silent.
Classic watchdog pattern (memory alerts, disk alerts, CI
pings).
--workdir WORKDIR Absolute path for the job to run from. Injects AGENTS.md
/ CLAUDE.md / .cursorrules from that directory and uses
it as the cwd for terminal/file/code_exec tools. Omit to
preserve old behaviour (no project context files).
两个位置参数:schedule 和 prompt。schedule 支持两种写法------cron 表达式(0 9 * * *)和自然语言(30m、every 2h)。后面我用 0 6 * * *,五段依次是「分 时 日 月 周」,代表每天 6:00。
几个选项先划重点:
--script:指定 scripts/ 目录下的脚本。默认模式下脚本 stdout 注入 agent 的 prompt;--no-agent下脚本本身就是 job。--no-agent:完全跳过 LLM,按计划跑脚本、把 stdout 直接投递。空输出 = 静默,这是 watchdog 模式。--workdir:job 的运行目录,会把该目录的 AGENTS.md / CLAUDE.md / .cursorrules 注入上下文。--deliver:输出投递目标,默认 local。
改 job 用 hermes cron edit <id>,选项和 create 基本对齐(--schedule / --script / --no-agent / --workdir 都有),还多了 --agent 能把 no-agent 的 job 改回 agent 模式。这串帮助里有几个设计判断,后面展开。先落地。
把管线接进 cron:建一个真实的 job
先写调度入口脚本。cron --script 要求脚本路径在 scripts/ 下------Windows 的 Hermes home 是 %LOCALAPPDATA%\hermes,所以放在 %LOCALAPPDATA%\hermes\scripts\run_daily_report.py。逻辑很直接:os.chdir 到日报目录,按序用 subprocess 跑 collect→curate→render,stdout 输出每步的最后一行作简报。
python
#!/usr/bin/env python3
"""H7 调度入口:6:00 无人值守跑「各家 AI 动态日报」管线。
串起 collect → curate → render 三步,stdout 输出简报,供 cron 投递。
注:curate.py 内部已调 hermes chat 汇总,此脚本本身不需要 LLM;
用 hermes cron --no-agent 挂载,脚本即 job、stdout 即投递内容。
"""
import os
import subprocess
import sys
DAILY = r"D:\CF-AICoding\hermes-lab\daily-report"
STEPS = ["collect.py", "curate.py", "render.py"] # 顺序即依赖
def main() -> int:
os.chdir(DAILY)
lines = []
for step in STEPS:
p = subprocess.run(
[sys.executable, step],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
out = (p.stdout or "").strip()
err = (p.stderr or "").strip()
if p.returncode != 0:
print(f"[fail] {step} rc={p.returncode}")
if err:
print(err)
return p.returncode
# 取该步最后一行输出作简报;无输出则如实标注
tail = out.splitlines()[-1] if out else "(no output)"
lines.append(f"[ok] {step}: {tail}")
print("\n".join(lines))
return 0
if __name__ == "__main__":
sys.exit(main())
一个细节:curate.py 内部已经调 hermes chat 汇总,所以这个调度脚本本身不需要 LLM。用 --no-agent 挂载,脚本即 job、stdout 即投递内容,外层不用再起一个 agent 循环。
建 job 的命令和输出,逐字取自本机:
text
$ hermes cron create "0 6 * * *" --name "daily-report-0600" --script "run_daily_report.py" --no-agent --workdir "D:\CF-AICoding\hermes-lab\daily-report"
Created job: 21967688cba0
Name: daily-report-0600
Schedule: 0 6 * * *
Script: run_daily_report.py
Mode: no-agent (script stdout delivered directly)
Workdir: D:\CF-AICoding\hermes-lab\daily-report
Next run: 2026-08-29T06:00:00+08:00
⚠ Gateway is not running --- jobs won't fire automatically.
Start it with: hermes gateway install
sudo hermes gateway install --system # Linux servers
Check status: hermes cron status
job 建好了,id 是 21967688cba0,Next run 已经算到次日 6:00。看到这一步,你八成以为建好 cron job 就完事------不是。最后三行那个 ⚠ 警告,才是这篇最重要的工程事实。
无人值守的前提:gateway 常驻
hermes cron status 把话说得很直白,逐字:

解法两条路:hermes gateway install 把 gateway 装成服务(Windows 装成 Scheduled Task,登录自启;Linux 装成 system 服务),或者 hermes gateway run 前台一直跑着。装成服务才是真正无人值守的前提------前台跑意味着你得留着那个终端。
诚实边界:本机我没把 gateway 装成服务,cron 到 6:00 自动 fire 的那一下我也没真等过。这篇讲的「无人值守自动触发」机制是实打实的------create 的 ⚠ 警告、cron status 的检查,都是实跑取证的输出。但自动触发那一刻,标「待核实」。我验证的是手动触发路径,下一节讲。
手动触发与追溯:cron run + runs + 落盘
gateway 没跑,不等于 job 不能跑。cron 有个专门绕开 gateway 的调试通道:cron run <id> 直接触发。
text
$ hermes cron run 21967688cba0 --accept-hooks
Triggered job: daily-report-0600 (21967688cba0)
Next run: 2026-08-29T06:00:00+08:00
Ran now: succeeded.
cron runs 记录这次执行------durable execution,执行记录持久化,不是进程内存里过一下就没了:
text
0cb54fe76a424dcdb73bacb7bf70f67f completed job=21967688cba0 source=direct 2026-08-28T21:25:27.719765+08:00
source=direct,记录的是我这次手动触发;自动触发时这个字段会不一样,但本机没真等过 6:00,具体值我没取到,不瞎写。每一行的执行 ID,都能对着 executions.db 追。
执行输出落在 %LOCALAPPDATA%\hermes\cron\output\<job_id>\<时间戳>.md。我这次触发的输出,逐字:
text
# Cron Job: daily-report-0600
**Job ID:** 21967688cba0
**Run Time:** 2026-08-28 21:25:45
**Mode:** no_agent (script)
---
[ok] collect.py: - 跨天去重:过滤掉 24 条昨天已报道的标题
[ok] curate.py: 今日无新动态:24 条与昨日已报道重复,已剔除。日报沿用昨日内容。
[ok] render.py: 已报道档案更新:新增 0 条标题 → history.json
注意 Mode 是 no_agent (script)------没有 agent 循环,纯脚本,这是 --no-agent 的直接体现。文件路径带着 job id 和时间戳,每次执行一落盘,天然可追溯。
顺带说明:这个 job 的 deliver 是 local,输出只落本地文件。系列第 7 篇《多渠道接入》的 push.py 还没接进调度------推送要凭据,本机没配,标「待核实」。推送跑通的无人值守,等凭据配好才算数。
重复触发也不会重复:跨天去重兜底
上面那份输出,内行人一眼能看出它有多「安全」。当时 24 条新闻全都报道过了,于是 collect 全部滤掉、curate 走了「今日无新动态」守卫、render 归档 0 条。
这意味着:即使 cron 在无人值守下重复触发、触发时机错了、一天跑了两遍,日报也不会产生两份重复内容。跨天去重在这儿成了调度的安全网------系列第 4 篇《记忆系统拆解》做的去重,反过来给调度兜底。
curate.py 里那个守卫是关键(逐字):items 为空时不白调 Hermes,直接写「今日无新动态」。空输出时 --no-agent 的 stdout 就一句话,投递也不会变成垃圾。
自动化最怕的不是「跑挂了」,是「跑挂了还产出一堆重复的东西」。跨天去重把后一种风险焊死了。
job 生命周期:pause / resume / tick
job 不是建好就一劳永逸。生命周期命令,逐字:
text
$ hermes cron pause 21967688cba0
Paused job: daily-report-0600 (21967688cba0)
$ hermes cron resume 21967688cba0
Resumed job: daily-report-0600 (21967688cba0)
Next run: 2026-08-29T06:00:00+08:00
resume 之后 Next run 还是 2026-08-29T06:00------暂停不改变下一次运行的时间点。cron tick 是「把到期 job 跑一遍就退出」,我触发它时没有到期 job,它静默返回,一个字都不输出。这个行为后面在工程判断里会用到。
job 的完整状态,画成生命周期图:

持久化落在 %LOCALAPPDATA%\hermes\cron\ 下:jobs.json 存 job 定义,executions.db 存执行记录,output/ 存每次 stdout。jobs.json 里这个 job 的完整字段,逐字(节选):
json
{
"id": "21967688cba0",
"name": "daily-report-0600",
"script": "run_daily_report.py",
"no_agent": true,
"schedule": { "kind": "cron", "expr": "0 6 * * *", "display": "0 6 * * *" },
"repeat": { "times": null, "completed": 1 },
"enabled": true,
"state": "scheduled",
"next_run_at": "2026-08-29T06:00:00+08:00",
"last_run_at": "2026-08-28T21:25:45.675342+08:00",
"last_status": "ok",
"deliver": "local",
"workdir": "D:\\CF-AICoding\\hermes-lab\\daily-report"
}
enabled / state 是 pause / resume 改的;last_run_at / last_status 是我手动触发后更新的。整个 job 就是一份可读的 JSON------调度器重启、机器重启,都靠这份文件恢复现场。
什么任务值得上调度
cron 用起来了,该回答一个更本质的问题:什么任务值得上调度?我的判断三条,缺一不可。
第一,固定时间。日报在每天 6:00,时间点是定的。一个「等我有空再跑」的任务,不需要 cron。
第二,确定性流程。collect→curate→render 每一步都是脚本,输入输出确定,没有需要 agent 临时决策、来回确认的环节。凡是跑一半会卡住问你「要不要继续」的任务,都不适合无人值守------没人回答它。
第三,可离线查结果。执行输出落盘,你起床再翻,不耽误事。反过来,一个「跑完必须立刻有人处理」的任务,调度只负责跑,不负责唤醒人。
对照这三条,日报是教科书式的调度对象。顺带说省 token:--no-agent 模式下,cron 只跑脚本,不起 agent 循环。curate.py 内部调 Hermes 汇总那一下才消耗 token,外层没有额外 LLM 开销。这是最省的定时跑法------脚本即 job,stdout 即投递内容。
还有 watchdog 模式:--no-agent 帮助里那句「Empty stdout = silent」。意思是脚本输出为空时,这次执行就是安静的,不产生任何投递。适合「有告警才说话」的任务------磁盘超了才输出一行。

结论:无人值守上线,新问题也跟着来
回到开头那个画面。日报现在能自己跑了------job 建好、手动触发验证跑通、执行输出可追溯,跨天去重兜底。README 那句「H7 用 Hermes 的 cron 在 6:00 自动跑」的承诺,兑现了一半:job 在,手动触发在;6:00 自动那一下,等 gateway 装成服务才算数。
但无人值守不是把问题消掉了,是把问题换了一批。以前你盯着跑,现在没人盯------跑挂了谁发现?以前 key 在环境变量里,现在多了一个常驻的 gateway 进程------凭据面变大了;以前手动跑一次花一次钱,现在每天自动跑------成本不声不响地累积。这三件,就是我下一篇要处理的:安全、本地模型、成本运维。
跑起来的调度,让日报第一次像一个「产品」而不是一个「脚本」。但产品是有运维责任的。下一篇见。
最后留个问题:如果让你给自己的 agent 配一个定时任务,你想让它每天替你做什么?查天气、盯服务器告警、把关注的博客打包成早报、还是让它定时清理你的下载目录?
想看我怎么处理无人值守的安全与运维,关注别丢。