ARTICLE · 1283661
OpenClaw 一天提交 627 次:闸门才是本事
📅 2026-10-30

周一早上打开电脑,我发现一件挺离谱的事:OpenClaw 的作者 Peter Steinberger,有一天往代码仓库里提交了 627 次。
我第一反应是"这人是不是不睡觉"。第二反应是"这数据准吗"。所以我当天用 GitHub 公开 API 自己数了一遍——627,一次不差。但真正让我坐直的,不是这个数字,是数完之后看到的另一组数。
▎ 01 · 627 次提交里,只有 2 次是"做新东西"
我写了个脚本,把 2026-02-22 那天他所有的提交信息拉下来自己分类:
# 数某人某天提交了多少次,以及每次属于什么类型
python3 - <<'EOF'
import urllib.request, json, re, time, collections
UA = {"User-Agent": "my-script", "Accept": "application/vnd.github+json"}
def api(url):
fora in range(4):
try:
r= urllib.request.urlopen(
urllib.request.Request(url,headers=UA), timeout=30)
returnr.status, json.loads(r.read()), r.headers.get("Link")
exceptException as e:
ifa == 3: raise
time.sleep(4)
day = "2026-02-22"
msgs, page = [], 1
while True:
st,d, link = api(f"https://api.github.com/repos/openclaw/openclaw/commits"
f"?author=steipete&since={day}T00:00:00Z&until={day}T23:59:59Z"
f"&per_page=100&page={page}")
forc in d:
msgs.append(c["commit"]["message"].split("\n")[0])
m= re.search(r'[?&]page=(\d+)[^>]*>; rel="next"', link or "")
ifnot m: break
page= int(m.group(1))
def typ(msg):
mm= re.match(r'^(\w+)(\([^)]*\))?(!)?:', msg)
returnmm.group(1) if mm else "other"
c = collections.Counter(typ(x) for x in msgs)
print("总数:", len(msgs))
for k, v in c.most_common():
print(f"{k}:{v}({100*v/len(msgs):.1f}%)")
EOF
跑出来的分布是这样的:
提交类型 | 次数 | 占比 |
test(测试) | 319 | 50.9% |
fix(修复) | 144 | 23.0% |
refactor(重构) | 81 | 12.9% |
docs(文档) | 36 | 5.7% |
perf(性能) | 35 | 5.6% |
chore(杂务) | 8 | 1.3% |
feat(新功能) | 2 | 0.3% |
build / style | 2 | 0.3% |
一天 627 次提交,新功能只有 2 次。剩下 625 次,全是在给已有的东西加固:写测试、修 bug、重构、补文档。
而且那天的提交信息里,Merge 开头的合并提交是 0 次。

▎ 02 · 真正的分水岭:过不了测试就不提交
我一开始以为这是"AI 手快"。直到我把他那天的提交间隔也统计出来,才看懂这里的差别。
相邻两次提交的中位间隔是 64 秒,最短 0 秒(同一秒提交两次),最长 24388 秒(约 6.8 小时),当天活跃跨度 26.15 小时。
更关键的是时段分布:
UTC 时段 | 提交次数 |
00 点 | 41 |
01–06 点 | 0 |
07 点 | 49 |
11 点 | 66 |
23 点 | 25 |
凌晨 0 点有 41 次提交,然后 1 点到 6 点整整齐齐的 0 次。这不是人在熬夜——这更像是机器在跑一个批次,跑完了就停,人不在旁边盯着。
那"手快"到底意味着什么?它意味着每一次提交背后,都有一道机器能自己判断的闸门:改完代码,自动跑测试,测试不过就不提交,改到过了才提交。
这就是"627 次提交不等于 627 次翻车"的原因。数量大不是因为乱,而是因为每次都先过了关。
▎ 03 · 我们自己的流水线:同样一分钟一次,结果完全不同
看完他的数据,我第一反应是去翻我们自己那台机器。我们也有一套 OpenClaw 流水线在跑,每天定时起一堆任务。
我写了个脚本直接读 OpenClaw 的状态库(只读打开,不动正在跑的东西):
# 数清一条 AI 流水线一天跑了多少次任务、失败多少次
import sqlite3, collections, datetime, os
DB = os.path.expanduser("~/.openclaw/state/openclaw.sqlite")
con = sqlite3.connect(f"file:{DB}?mode=ro", uri=True)# 只读,绝不改动
rows = con.execute(
"selectcreated_at, status, tool_use_count from task_runs").fetchall()
def ts(v):
v= int(v)
returndatetime.datetime.fromtimestamp(v/1000.0 if v > 1e12 else v)
per_day, per_fail = collections.Counter(), collections.Counter()
for created_at, status, _ in rows:
d= ts(created_at).strftime("%Y-%m-%d")
per_day[d]+= 1
ifstatus != "succeeded":
per_fail[d]+= 1
for d in sorted(per_day)[-7:]:
t,f = per_day[d], per_fail[d]
print(f"{d}总{t:4d}失败{f:3d}成功率{100*(t-f)/t:5.1f}%")
结果是:
日期 | 总任务 | 失败 | 成功率 |
2026-10-26 | 151 | 23 | 84.8% |
2026-10-27 | 227 | 52 | 77.1% |
2026-10-28 | 328 | 41 | 87.5% |
10-28 那天跑了 328 次,跨度 22.2 小时,相邻任务中位间隔 65 秒。
跟那位作者的 64 秒,基本一样。
节奏一模一样,结果完全两样。 他的 627 次提交全部过闸门,我们的 328 次里有 41 次是失败的。更刺眼的是定时任务这一层:

总回执 526成功 415失败 111错误率 21.1%
34模型限流/过载
30任务超时
23agent数据库锁
7投递目标歧义
▎ 04 · 最贵的一个教训:脚本跑通 ≠ 老板收到
我接着去查真正投递出去的东西——不是脚本有没有 exit 0,是老板的飞书里到底有没有那个文件。
查法很直接,读会话数据库,看每条定时任务的最后一条助手消息里到底有没有附件行:
# 定时任务"跑完了"和"老板真收到了"是两件事
import sqlite3
con = sqlite3.connect(
"file:/Users/wuliaott/.hermes/profiles/xiaohao/state.db?mode=ro",uri=True)
rows = con.execute("""
selects.title, s.end_reason,
(selectcount(*) from messages m
wherem.session_id = s.id and m.role = 'assistant'
andm.content like '%MEDIA:%') as media_n
fromsessions s
wheres.title like '%日更%' or s.title like '%撰写%'
""").fetchall()
ok = sum(1 for r in rows if r[2] > 0)
print(f"定时任务 {len(rows)} 次,真正带附件交付 {ok} 次,未交付率 {100*(len(rows)-ok)/len(rows):.1f}%")
结果是:107 次定时任务,只有 83 次真正把文件送到了老板手里,24 次没有——未交付率 22.4%。
更气人的是,这 24 次里有 22 次的 `end_reason` 写着 `cron_complete`,也就是调度器认为"任务圆满完成"。
那 22 次断在哪?看最后一条消息,全是同一句:
`[CRON_FAILURE]` 本轮未完成文章交付:达到最大工具调用轮次……
模型在跑完最后一步之前断流了。产物可能全在磁盘上,脚本可能 exit 0,但那一行附件指令从来没被抄进最终回复,所以老板那边零感知。
这就是他 627 次提交和我们 328 次任务之间真正的差别:他的每一次提交都被"测试通过"卡着;我们的大部分任务没有任何东西卡着。
▎ 05 · 抄作业:三步给你的流水线装上闸门
回到最初的问题——我们能不能把"过不了闸就不算完成"这套搬过来?能,而且不用重写系统。
第一步:给每个定时任务加一个硬性验收门。
别让"跑完了"由模型自己宣布。写一个校验脚本,让它在最后一步必须返回非零退出码:
#!/usr/bin/env bash
# verify_delivery.sh —— 挂在定时任务最后一步
set -u
DOCX="$1"
if [ ! -s "$DOCX" ]; then
echo"FAIL: 成品不存在或为空 ($DOCX)" >&2
exit1# ← 非零退出 = 本轮不算完成
fi
python3 verify_docx_clean.py "$DOCX" --expect-images 3 || exit 1
python3 gzh_gate.py draft.md --account ai --images 3 --docx "$DOCX" || exit 1
echo "PASS: 全部门禁通过"
exit 0
第二步:把"交付"当成产物本身来验,而不是当成日志。
判断有没有送到,只有两种可靠办法——要么查最终回复里有没有附件行,要么查收件方的会话记录。别用"脚本有没有报错"当交付凭证。上面那段 media_n 的 SQL 就是直接查收件记录,比任何自我报告都硬。
第三步:让失败可读,别让它静默。
把错误分成三类分别记:模型限流(34 次)、任务超时(30 次)、数据库锁(23 次)。这三类的解法完全不同——限流要错峰、超时要拆任务、数据库锁要串行化。混在一起叫"失败了",你永远查不出该改哪。
我们自己的失败率是 21.1%,但每一类都能对上号。能对号入座的失败,才叫可用的失败数据。
▎ 避坑 · 三条别这么用
一、别把"提交次数多"当成效率高。 那 627 次里 319 次是测试,只有 2 次是新功能。如果你的流水线每天产出 300 个任务,但其中一半是失败重试,那你在跑噪音,不在跑流水线。先看类型分布,再看总数。
二、别用 `exit 0` 当成"交付成功"。 这是我们踩得最贵的一个坑。调度器说 cron_complete、脚本说成功、文件躺在磁盘上——三样都成立,但老板什么都没收到。验收要验最终产物和收件记录,不要验执行过程。
三、别让定时任务在没人盯着的时候自己跑长任务。 那位作者的流水线之所以敢在凌晨 0 点跑 41 次,是因为测试会替他把关。我们那 22 次断流,都是在长链路跑完前的最后一口气断的。跑不完的任务,宁可拆短、留检查点,也别指望它一口气跑到终点。
▎ 结尾
说句实话,我们这套跑了一天 328 次,还是有 41 次失败、22 次没送到。我不敢说抄到了他的水平。
但有一件事变了:现在每一次失败,我都知道它卡在限流、超时还是数据库锁,知道该改哪儿。 这比"看起来在跑"值钱多了。
所谓 AI 流水线的差距,从来不在一天跑了几百次,而在于每次跑完,你能不能拿得出一份"确实送到了"的证据。
如果你也想给自己的定时任务装一道闸门,最快的办法是:先写那个返回非零退出码的校验脚本,再把它挂到任务最后一步。就这一件事。














































































