在大多数人的 OpenClaw 部署里,定时任务(Cron)就像是机器的内脏运动。前一天谈话记录总结在凌晨 1 点醒来,个人知识库维护在周日触发。你听得见显卡风扇在 3 点钟突然加速的轰鸣,但当你问OpenClaw要一份系统定时任务清单,或者问它最近一次系统任务执行情况时,它可能会象个手忙脚乱的实习生一样,紧张兮兮地回复你:“没有找到相关命令”,或者“没有权限执行”。

其实任务肯定执行过了,问题在于谁能看到它在跑什么

cron-command_image_1

如果你不能随时回答“哪些任务在排队”、“任务定义是什么”、“上次留下了什么证据”,那自动化就不是你的工具,而是一个你无法审计的投机系统。

我们需要把这种“观察监督权”从黑箱里拽出来。

第一步:确认控制面的真实入口

人们对于自己不了解的事物通常有两种反应:夸大其辞,或者哂然一笑。互联网上对待OpenClaw的态度与此严丝合扣:等待看笑话的人,和把权限全部甩给OpeClaw的人。这两种态度都有问题。“未历其境,难知其味”。不是躬身亲尝,不可能了解AI Agent的发展。而全权托付AI Agent的人,出了事又难免觉得它盛名之下,其实难负。

举个例子:OpenClaw只是个AI Agent的框架,具体操纵它的是大模型。训练大模型时的语料里可没有OpenClaw这个产品。所以它对于系统命令的习惯都是基于Linux的记录,比如,要求列出系统定时任务,它很自然就会输入指令:crontab -l。但实际上,在OpenClaw内部,这个需求对应的指令是:node /app/openclaw.mjs cron list 。OpenClaw找不到crontab -l这个指令,就会报告“没有找到相关命令”,任务失败。不了解这个系统的人可能因此沮丧,觉得这个AI Agent什么也做不了。其实只是你没找到做对事情的路径。想象一下人类世界,如果让一个没有经过严格训练的普通人坐到F-15飞机的座舱,面对满屏花花绿绿的指示灯和琳琅满目的按钮,他又能做什么呢?

所以,不是AI Agent不好用,是你需要先了解它能做什么,怎么做。而不是期待它能解决所有事情,或者认为它什么也做不好。

转回定时任务这个话题,它是我们用好OpenClaw的核心任务。所以三个核心指令是必须十分清晰的

  • 定时器里有什么任务?列一个清单出来
  • 定时器里某一个具体的任务的详细情况什么?
  • 最近一次触发定时器的任务完成情况如何?

确认程序位置

docker exec -it openclaw sh -lc 'ls -la /app/openclaw.mjs'

确认数据路径

docker exec -it openclaw sh -lc 'ls -la /home/node/.openclaw/cron'

路径映射如果错位,就会制造出“文件不存在”的幻觉。

第二步:构建三条稳定的“治理接口”

我们不指望 Agent 能学会复杂的 Linux 命令,我们要给它三条确定的物理出口。这些脚本要放在 workspace/skills/bin 目录下。

首先要创建目录:在宿主机执行(会写入持久化目录):

mkdir -p /opt/openclaw/data/workspace/skills/bin

1. cron_list —— 确认系统的节律

这不只是一个定时器任务清单,它是系统还在呼吸的证明。通过拉出这个清单,你会知道系统每天会定时做些什么任务。

cat > /opt/openclaw/data/workspace/skills/bin/cron_list.sh <<'SH'
#!/bin/sh
set -eu
node /app/openclaw.mjs cron list
SH
chmod +x /opt/openclaw/data/workspace/skills/bin/cron_list.sh

2. cron_show —— 任务的骨骼解剖

如果你想知道某个任务到底是干什么的,你需要查阅 jobs.json。我们用一段 Python 代码来实现它,因为 Python 在处理 JSON 时比 Shell 更像一把精准的手术刀。

cat > /opt/openclaw/data/workspace/skills/bin/cron_show.sh <<'SH'  
#!/bin/sh  
set -eu  
JOB_ID="${1:-}"  
python3 - <<'PY' "$JOB_ID"  
import json, pathlib, sys, re  
job_id = sys.argv[1].strip()  
if not job_id:  
raise SystemExit("usage: cron_show.sh <jobId>")  
# 允许 uuid 或你这种 jobId 字符串(你现在是 uuid)  
if not re.fullmatch(r"[0-9a-fA-F-]{8,64}", job_id):  
raise SystemExit("invalid jobId format")  
p = pathlib.Path("/home/node/.openclaw/cron/jobs.json")  
data = json.loads(p.read_text(encoding="utf-8"))  
jobs = data.get("jobs", data)  
cand = None  
if isinstance(jobs, dict):  
cand = jobs.get(job_id)  
if cand is None:  
for v in jobs.values():  
if (v.get("id") or v.get("jobId") or v.get("name")) == job_id:  
cand = v; break  
elif isinstance(jobs, list):  
for v in jobs:  
if (v.get("id") or v.get("jobId") or v.get("name")) == job_id:  
cand = v; break  
print(json.dumps(cand, ensure_ascii=False, indent=2))  
PY  
SH  
chmod +x /opt/openclaw/data/workspace/skills/bin/cron_show.sh

3. cron_last_run —— 寻找机器留下的证据

执行本身是短暂的,但痕迹是永恒的。这个脚本会翻遍所有的运行记录,告诉你最近一次发生了什么。

cat > /opt/openclaw/data/workspace/skills/bin/cron_last_run.sh <<'SH'  
#!/bin/sh  
set -eu  
JOB_ID="${1:-}"  
python3 - <<'PY' "$JOB_ID"  
import json, pathlib, sys, re  
  
job_id = sys.argv[1].strip()  
if not job_id:  
raise SystemExit("usage: cron_last_run.sh <jobId>")  
if not re.fullmatch(r"[0-9a-fA-F-]{8,64}", job_id):  
raise SystemExit("invalid jobId format")  
  
runs_dir = pathlib.Path("/home/node/.openclaw/cron/runs")  
if not runs_dir.exists():  
raise SystemExit("runs dir not found: " + str(runs_dir))  
  
latest = None  
latest_ts = -1  
  
def try_update(obj):  
global latest, latest_ts  
if not isinstance(obj, dict):  
return  
if obj.get("jobId") != job_id:  
return  
ts = obj.get("ts") or obj.get("runAtMs") or 0  
try:  
ts = int(ts)  
except Exception:  
ts = 0  
if ts > latest_ts:  
latest_ts = ts  
latest = obj  
  
# 扫描所有文件,不限制扩展名;支持:  
# - 单个 JSON 文件  
# - JSON Lines(每行一个 JSON 对象)  
for p in runs_dir.rglob("*"):  
if not p.is_file():  
continue  
try:  
text = p.read_text(encoding="utf-8", errors="ignore")  
except Exception:  
continue  
  
# 快速过滤:文件里不含 jobId 直接跳过  
if job_id not in text:  
continue  
  
# 先尝试整个文件当 JSON  
try:  
obj = json.loads(text)  
try_update(obj)  
continue  
except Exception:  
pass  
  
# 再按 jsonl 逐行解析  
for line in text.splitlines():  
if job_id not in line:  
continue  
line = line.strip()  
if not line or line[0] not in "{[":  
continue  
try:  
obj = json.loads(line)  
except Exception:  
continue  
try_update(obj)  
  
if latest is None:  
print("no run found for jobId:", job_id)  
else:  
out = {  
"ts": latest.get("ts"),  
"jobId": latest.get("jobId"),  
"status": latest.get("status"),  
"summary": latest.get("summary"),  
"sessionId": latest.get("sessionId"),  
"sessionKey": latest.get("sessionKey"),  
"durationMs": latest.get("durationMs"),  
"nextRunAtMs": latest.get("nextRunAtMs"),  
"delivered": latest.get("delivered"),  
"deliveryStatus": latest.get("deliveryStatus"),  
}  
print(json.dumps(out, ensure_ascii=False, indent=2))  
PY  
SH  
  
chmod +x /opt/openclaw/data/workspace/skills/bin/cron_last_run.sh

第三步:权限边界的再分配

现在,你需要在 /opt/openclaw/data/exec-approvals.jso 中完成最后一步:治理。

不要给 Agent 所有的权限。你应该通过白名单(allowlist)明确告诉它,只能动这三个脚本。

JSON

"allowlist": [
  { "pattern": "/home/node/.openclaw/workspace/skills/bin/cron_list.sh" },
  { "pattern": "/home/node/.openclaw/workspace/skills/bin/cron_show.sh" },
  { "pattern": "/home/node/.openclaw/workspace/skills/bin/cron_last_run.sh" }
]

这不是在限制它的能力,而是在定义它的边界。通过这种方式,你并没有扩大系统的攻击面,你只是把“观察权”固化在了这三个路径上。

第四步:更新AGENTS.md

你需要让OpenClaw更新它的执行规则,也就是记录到AGENTS.md。在与它的对话里输入以下内容。然后尝试这几个命令,检验你的成果。

将这三条指令加入你的系统规则里

当需要查看定时任务:只运行
/home/node/.openclaw/workspace/skills/bin/cron_list.sh

当需要查看某个 job 详情:只运行
/home/node/.openclaw/workspace/skills/bin/cron_show.sh <jobId>

找该 job 最近一次 run 的 summary:只运行
/home/node/.openclaw/workspace/skills/bin/cron_last_run.sh <jobId>

结论:从执行者到规则制定者

这三段脚本不是简单的工具,它们是你的审计接口。

当自动化到点触发,OpenClaw 会读取 jobs.json,创建一个隔离的会话,生成运行记录,并尝试交付结果。任务结束后,自动化似乎消失了。OpenClaw事后是无法访问一个删除的session,了解这些定时任务信息的。但因为有了这三个脚本,它变成了一种可审计的存在形式

你不再是那个盯着终端屏幕发呆、祈祷任务正常跑通的“工具搬运工”。你现在是规则的定义者。风扇仍然在转,但现在,你能听懂它的声音了。