AIGC标识 同一个脚本,为什么不能跑第二遍:发布流水线的幂等性设计

同一个脚本,为什么不能跑第二遍:发布流水线的幂等性设计

凌晨一点,我把博客园的发布脚本跑了两遍。

第一遍成功了。第二遍是因为不确定第一次到底有没有生效——页面上没有任何"发布成功"的反馈,我不想赌。于是脚本又灌了一遍正文,又点了一次发布。

结果你猜到了:文章正文字数变成了 10468,而我的源文件是 5234 字。

正好两倍。

这不是 bug,这是幂等性缺失。同一个操作执行两次和执行一次,结果应该一样;而我这个脚本执行两次,结果是执行一次的两倍。


一、三个平台,三种性格

同一个晚上,我在三个平台上各撞了一次"重跑"问题,但表现完全不同。

博客园:重复即污染。 灌正文的脚本第二次执行时,没有先清空编辑器,直接把同样的 5234 字又贴了一遍。服务端老老实实存了 10468 字。发布也成功了——一篇正文重复两遍的文章堂而皇之上了线。

简书:重复即孤儿。 它的建稿接口是 POST,每次调用都会新建一篇草稿。我第一次跑,建了稿 A;第二次跑,建了稿 B。现在 A 和 B 内容一样,都在草稿箱里躺着,只有一个能发出去,另一个成了没人管的孤儿。

CSDN:重复即自愈。 它的打标签脚本我跑过至少五次。第一次因为标签输入框没弹出来失败了,第二次带上文章 ID 重跑,好了;后来想改标签,又跑,又好了。这个脚本每次执行都先导航到指定文章、先读当前状态、再做动作——它天生就是幂等的。

同样一套自动化思路,在三个平台上呈现出三种截然不同的性格。区别不在代码量,在有没有为"重跑"做过设计。


二、幂等是什么,为什么这里特别重要

幂等(idempotent)的定义很朴素:

f(x) 执行一次的结果 == f(x) 执行 n 次的结果

HTTP 规范早就把这个概念用在了方法语义上:GET、PUT、DELETE 被定义为幂等的——重复调用不会有额外副作用;POST 不是——每次调用都可能创建一个新资源。

自动化脚本恰好是 HTTP 方法的放大版。把发布流水线拆开,每个动作其实都能对到某个方法上:

动作 本质 幂等?
读文章当前正文 GET ✅ 幂等
灌正文(先清空再写) PUT ✅ 幂等
打标签(设成固定集合) PUT ✅ 幂等
建新稿 POST ❌ 不幂等
点发布 POST ❌ 不幂等
上传图片 POST ❌ 不幂等

关键洞察:一条发布链里,只有"建稿"和"发布"是天生不幂等的,其余全都可以设计成幂等。而我踩的坑,是把一个本该幂等的动作(灌正文)写成了不幂等。

为什么自动化场景对幂等的要求比人操作高得多?因为人天然不会重复。你编辑一篇文章,第二次打开编辑器,看到里面已有内容,你会先删掉再写——这个"先看再删"的动作是本能,不需要思考。而脚本只会无脑执行你给它的指令,它不会"看一眼"。

所以幂等性不是锦上添花,是把人类的本能翻译成代码。


三、幂等的三个零件

把博客园那次事故拆开,一个幂等的"灌正文"动作需要三个零件。

零件一:操作前的清理

错误写法:

// 直接把内容贴进去,不管里面原来有什么
await editor.focus();
await insert(markdown);

正确写法:

// 先清空,再灌入
await editor.focus();
await selectAll();      // Ctrl+A
await press('Delete');
await verifyEmpty();    // 确认真的是空的,不是"看起来是空的"
await insert(markdown);

注意最后那个 verifyEmpty。我这次的经历是:Ctrl+A + Delete 之后读长度,读到的是 0,我才敢灌。如果读到的是别的数字,说明清理没生效,得重来,不能往下走。

零件二:操作后的校验

灌完之后立刻读回来,和源文件比:

const expected = sourceFile.length;
const actual = await readEditorLength();

if (actual !== expected) {
  throw new Error('LENGTH_MISMATCH: expected ' + expected + ', got ' + actual);
}

这个校验是我这次唯一没做的一步。如果我做了,10468 会在发出前就被拦下,而不是等文章上线之后才发现。

校验必须是自动的、强制的、抛异常的。不能是"看一眼差不多就行"——脚本没有"差不多"这个判断能力,只有相等或不相等。

零件三:前置状态检查(可选,但能救大命)

在执行任何动作之前,先问一句"现在是什么状态":

const state = await readCurrentState();
if (state.isEmpty) {
  await fillAndPublish();
} else if (state.hasDraft) {
  await reuseDraft(state.draftId);   // 复用,别新建
} else {
  await createNew();                  // 万不得已才 POST
}

这个模式在简书上价值最大。它的建稿是不幂等的 POST,但如果前面加一层"先查有没有同标题的未发布草稿",重跑就不再产生孤儿——第二次跑会复用第一次建的那篇。


四、正面案例:CSDN 那个"打不死"的脚本

反过来看 CSDN。它的打标签脚本我重跑过五次,每次都收敛到同一个正确状态。为什么?

// 伪代码:cs-tag3.js 的核心结构
await navigateTo('/editor?articleId=' + articleId);   // 每次都回到同一篇文章
await waitForEditor();

const tags = await readCurrentTags();                 // 先读当前标签
const target = ['自动化', '运维'];

for (const t of target) {
  if (!tags.includes(t)) {                            // 已有就不加
    await addTag(t);
  }
}

await verify(target);                                 // 最后校验

三个性质让它天然幂等:

  1. 锚定同一个目标:靠 articleId 定位,不是"当前打开的是哪篇就操作哪篇"。
  2. 先读后写:加标签前先看已有标签,避免重复添加。
  3. 终态校验:最后检查标签集合是否等于目标集合。

这其实是声明式的做法:不说"做一次添加动作",而说"确保最终状态是这个集合"。前者是过程,重复执行会累积;后者是目标,重复执行会收敛。

把脚本从"执行动作"改写成"达成状态",是获得幂等性最省事的一条路。


五、还有一类幂等:故障恢复

幂等性还有个更实际的用途——失败后重跑。

发布链是会中途失败的。网络抖一下、验证码弹出来、平台限流、标签候选列表没加载完。这些失败发生在一个长链条的中间,前面成功的步骤不能白费。

这时候你只有两个选择:

  • 从头再跑一遍:要求整条链幂等,否则就会重复发布、重复建稿、重复上传。
  • 从断点续跑:要求每一步都有状态记录,能回答"做到哪了"。

我最终给发布链加的是第二种:每一步都把产物 ID 写进日志文件(文章 ID、note ID、slug),下一轮启动时先读日志,已经完成的步骤跳过,只做没做的。配合第一步的幂等清理,两头都有了保障。

# 断点续跑的日志长这样
51CTO=15000997
TENCENT=2760572
JIANSHU=144869424
CNBLOGS=23292556

下次跑同样一条链,脚本先 source 这个文件,发现五个平台都有 ID,就跳过发布,只做校验——确认线上文章确实存在、字数确实对。

"可重放"比"一次成功"更重要。 一次成功靠运气,可重放靠设计。


六、一份可以直接抄的清单

给所有要重复执行的脚本,按这个清单过一遍:

# 检查项 不做的后果
1 输入类操作前是否先清空 内容翻倍(我这次)
2 输入类操作后是否校验长度 脏数据直接上线
3 是否锚定唯一 ID,而非"当前页面" 操作错对象
4 创建类操作前是否先查重 孤儿资源堆积
5 是否"先读后写"而非无脑写 重复累积
6 终态是否校验 失败静默通过
7 是否有断点记录可续跑 失败后从头再来
8 是否显式 process.exit(0) 僵尸进程挂住整条链

最后一条和幂等无关,但它和"重跑"是同一类问题——我的四平台链曾经被一个不退出的僵尸进程堵死十小时。一次执行留下的痕迹,必须在执行结束时全部收干净:打开的连接要关,起的进程要退,建的临时标签要关。


七、小结

回到那个 10468。它教会我的不是"记得先清空编辑器"这种具体技巧,而是一个判断标准:

每写一个要重复执行的脚本,先问自己:如果我现在立刻再跑一次,会发生什么?

如果答案是"什么都不会发生,只是浪费几秒"——合格。
如果答案是"会再发一篇文章"——不合格,去补前置检查。
如果答案是"内容会翻倍"——不合格,去补清理和校验。
如果答案是"不知道"——那你迟早会知道,用事故的方式。

工程上把这种事叫幂等性。我更喜欢另一个说法:对重复的容忍度。真实世界里的操作一定会重复——手抖、网卡、不确定、想确认。一个不能容忍重复的系统,本质上是在要求操作者永远不犯错。

而那是不可能的要求。

posted @ 2026-10-14 20:41  第四公子  阅读(2)  评论(0)    收藏  举报