草稿箱里躺着 18 篇孤儿:发布流水线只管发,不管收尾

1 阅读10分钟

草稿箱里躺着 18 篇孤儿:发布流水线只管发,不管收尾

补发任务跑完,我去 51CTO 创作中心看了一眼,草稿箱计数上写着 18。

点进去只看得到 10 条——列表本身也只给第一页,这是另一个故事,先按下不表。这看得见的 10 条里,没有一篇是我真正想留的,全是「发布到一半没发成」留下的残骸:

同一篇稿子发到 6 个平台,为什么只有这里的图片全裂了?   2100231  01:39
同一篇稿子发到 6 个平台,为什么只有这里的图片全裂了?   2100229  01:34
同一篇稿子发到 6 个平台,为什么只有这里的图片全裂了?   2100227  01:32
同一篇稿子发到 6 个平台,为什么只有这里的图片全裂了?   2100224  01:28
同一篇稿子发到 6 个平台,为什么只有这里的图片全裂了?   2100220  01:17
把「一文多发」做成脚本流水线:CDP、滑块、二维码…       2100232  01:41
把「一文多发」做成脚本流水线:CDP、滑块、二维码…       2100230  01:36
把「一文多发」做成脚本流水线:CDP、滑块、二维码…       2100225  01:30
把「一文多发」做成脚本流水线:CDP、滑块、二维码…       2100222  01:18
把 1242 条法规做成离线可核查的知识库:一个基层执法场景… 2097267  10-12

两篇稿子,九份拷贝。同一篇最多留下了 5 个副本,标题一模一样,内容一模一样,创建时间相差不到半小时——那半小时我在反复重跑同一个脚本。

更麻烦的是:它们不会自己消失,而我又不太敢删。

这篇文章讲的是发布流水线里那个从来没人管的部分——失败之后留下的东西。

一、我做了什么,怎么做到了 18 篇

事情很简单。我要把 20 篇存量稿子补发到各个平台,写了个批量脚本,逻辑是:

  1. 打开发布页
  2. 填标题
  3. 粘正文
  4. 点发布
  5. 看结果,失败就重跑

前三步每次都成功。第四步有一半概率失败(原因另说),于是我重跑。

问题在于:第 1 步「打开发布页」本身就有副作用。 51CTO 的发布页在标题填进去的那一刻就会自动建一个草稿,不管你最后发不发得出去。脚本重跑一次,就多一个草稿。

那两篇我一共重跑了 9 次,一次都没成功。所以:

目标 2 篇 × 重跑 9 次 = 草稿 9 份
加上 10 月 12 日那篇没发出去的 = 10 份
页面计数 18 → 还有 8 份在第二页,我没看到

这个账是我事后才对上的。跑的过程中我完全没意识到,因为脚本的输出里没有任何一行提到草稿数量——它只打印「DONE」。

二、三种不同的孤儿

清点的时候我发现,这些残骸不是同一种东西,处理起来的难度也完全不同。

1. 失败残留:内容完整,只是没提交

最常见的一种。标题有、正文有、封面没选、标签 0 个、分类没选。

这类最好办,理论上重新打开点一下发布就行。但实际上我从草稿箱进去又试了一次,发布面板照样打不开(这个问题另一篇单独讲)。所以它们就一直躺着。

2. 空壳:有标题没正文

掘金那边留下过一个。原因是我取草稿 URL 的时候取错了:脚本输出里有一行 DRAFT_URL 指向 drafts/new,我拿它去发,发的是一篇刚建的空白草稿。

真正的草稿 ID 在输出末尾的另一行 URL 里。等我反应过来,草稿箱里已经多了一个空壳。

空壳的问题在于它长得跟正常草稿一模一样。列表页看标题是有的(因为标题是脚本设上去的),只有点进去才发现正文是空的。批量清理的时候如果只看标题,很容易把「有价值但未发布」的和「空壳」一起删掉。

3. 重复副本:同一篇的五份拷贝

同一篇稿子,五次重跑,五个草稿。标题完全相同。

这类最烦人,因为你没法从列表页判断哪个是「最新的」。五个创建时间相差半小时,但内容可能因为中途改过稿而略有不同。要清理就得一个个点进去比对。

而且列表页给的时间只精确到分钟,同一分钟建的两份(我确实遇到过)在列表上完全无法区分。

三、为什么清理比发布难

我的第一反应是:写个清理脚本,把标题重复的都删掉。

写完一半停下来了,因为发现清理这事儿有三个发布时没有的风险。

风险一:你看到的列表是不完整的

51CTO 创作中心的列表页,一页 10 条,分页控件点了不换页(真实鼠标点、坐标算对了、没遮挡,就是不换)。博客园后台、腾讯云后台同样。

也就是说,我看到的 18 篇可能只是前 8 篇,后面还有多少不知道。在这种基础上写「批量删除」,等于蒙着眼睛挥刀。

后来我是靠归档页和站内接口才拿到全量的,这是另一个话题。但结论先放在这里:在你确认能拿到全量列表之前,不要写任何带删除动作的脚本。

风险二:删除要过二次确认,模态框本身就是个坑

delete 按钮点下去会弹确认框,确认框里的「确定」按钮位置每次都可能不一样(有的平台居中,有的靠右,有的在浮层里带动画)。我在这上面翻过车:脚本连续点了两次「确定」,第二次点在了刚弹出来的新浮层上,把另一篇不该删的删了。

虽然那次删的是空壳,但我后背发凉。

风险三:删错了不可恢复

发布失败,最坏的结果是稿子没发出去,重跑一次就行。删除失败,最坏的结果是稿子没了。

这两个错误的代价完全不对称。所以在没有回收站、没有版本历史的平台上,批量删除必须留一步人工确认。

四、我现在用的三条规则

踩完这些坑,我把清理流程改成了下面三条。都很土,但没有一条是多余的。

规则一:发布成功才允许留草稿,失败立刻打标记

脚本里加了这么一段:发布判据没通过,就把这次建的草稿 ID 记进一个 orphans.txt。

# orphans.txt —— 每行:平台 草稿ID 标题 时间
51cto  2100231  同一篇稿子发到 6 个平台…  2026-10-17 01:39
51cto  2100232  把「一文多发」做成脚本流水线…  2026-10-17 01:41

不删,只记。删的事留到人清醒的时候做。

这样改之后有个意外好处:下次重跑之前,脚本可以先读这个文件,跳过已经有残留的稿子,草稿箱就不会再涨了。

规则二:动手之前先查草稿箱,别急着新建

原流程的第 1 步是「打开发布页」,现在改成:

  1. 先按标题指纹查一遍草稿箱
  2. 有,就直接打开那个草稿编辑
  3. 没有,才新建

标题指纹不用太复杂,取标题前 12 个字做包含匹配就够了。中文标题前 12 字重复的概率很低,够用。

这条规则把「重跑 = +1 草稿」变成了「重跑 = 复用草稿」,草稿箱立刻就不涨了。

规则三:清理脚本必须带 dry-run,且默认就是 dry-run

node cleanup.js --platform 51cto            # 只列清单,不删
node cleanup.js --platform 51cto --apply    # 真的删

默认的那个命令,输出长这样:

[清单] 共 21 条,按标题去重后 9 组
  组1 (x5) 同一篇稿子发到 6 个平台…        → 保留最新 2100231,建议删 4
  组2 (x4) 把「一文多发」做成脚本流水线…    → 保留最新 2100232,建议删 3
  组3 (x1) 滑块验证码为什么总拖不动…        → 无重复,不动
  ...
[未执行] 加 --apply 才会真的删

关键不是「保留最新」这个策略有多聪明,而是清单先出来,人先看一遍。我第一版脚本直接删,删到第三个的时候发现有个组里最新的那篇反而是空壳(因为最后一次跑的时候正文没粘进去),保留策略选错了。

五、清理动作的实际顺序

如果你也有一堆孤儿草稿要收拾,我建议的顺序是:

  1. 先想办法拿到全量列表,别信分页。归档页、站内接口、带后缀的作者页,三条路至少走通一条。
  2. 导出清单到文件,标题 + ID + 创建时间 + 正文长度(正文长度能一眼挑出空壳)。
  3. 人工过一遍,标出「要留的」「要删的」「不确定的」。不确定的一律不动。
  4. 按组删,一次一组,删完立刻重新导出清单核对数字是不是少了预期的那么多。数字不对就停。
  5. 留 10% 不删。听起来很蠢,但等你哪天发现某篇「重复副本」其实是改过稿的版本,你会感谢这一步。

六、这件事真正的教训

发布脚本写久了,会形成一种思维定式:关注「成功没成功」,不关注「留下了什么」。

退出码 0、日志没有 ERROR、作者页文章数 +1 —— 这三样齐了,我就认为这次发布是干净的。但事实上,同一次运行可能在三个地方留下了痕迹:草稿箱多了一个、标签框多了几个脏标签、剪贴板被覆盖了。

这些痕迹单个都不致命,堆起来就会反噬:草稿箱涨到 18 篇之后,我连「哪篇发过了」都判断不出来,只能靠记忆。而记忆是靠不住的。

所以现在我的发布脚本最后一步不是打印 SUCCESS,而是打印这次运行产生的副作用清单。 哪怕清单是空的,也要打印出来。空清单本身就是一个有价值的断言:它证明这次运行没有留下东西。


补一句结尾。我现在还没删那 18 篇,因为全量列表还没拿到手。等拿到了再动手——按第四节的规则,一次一组,删完核对。

急事慢做,尤其是不可逆的事。