上个月 dev.to 上有篇 GDE 写的文章,标题大意是"用一个 Claude Code 技能把发布流程捋顺"。作者 xbill,东西叫 publishing-kit——一个 skill 加一组小脚本,把写稿、生封面、核对、发到四个平台串成一条链子。
不是我写的,我也没打算给谁写软文。我把仓库扒了一遍,真正让我坐直的不是"一键发四平台"这句宣传语,是它自己交待的一条自测结果。
它的 unwrap 会跳过块引用。那篇文章的 TL;DR 在源文件里正好是块引用,于是原文被原样发到线上,五行,一板一眼,读者点进来第一屏就是五条断句。更妙的地方在后头:负责检查这一步的脚本,也跳过块引用。
所以它检查了。它通过了。
画外音:职场上最怕的不是有人交了个烂活,是交烂活的人自己也做了验收,然后拍胸脯说没问题。
我盯着这条看了很久。不是因为难修——可能就几行——是因为它是个标本。一个"通过"和"真的对"之间那道缝的标本。
先说这套东西本来硬在哪
我不想显得是在挑刺,得把背景交待清楚:这工具整体思路比我见过的多数发布脚本都硬。
它的 preflight 一次性跑完全部检查,任何一个失败就返回非零退出码——封面是否已提交且与 HEAD 一致、几何尺寸、front matter 是否完整、有没有硬换行段落、每个已发布 URL 是否跟本地文件逐字节相同。注意最后一项,逐字节。
逐字节这件事是最不性感、也最没人愿意做的一项检查。多数人对付发布这件事的办法是"我看了一眼,长得挺像"。它不,它去比字节。
配套的 check-links 报错文案也很懂行:
FAIL: HTTP 200 但返回字节与磁盘不一致
漂亮就漂亮在这句话没说"检查失败"。它说的是,200 也可能是错的。HTTP 状态码是服务器的礼貌,不是内容的一致性。
更让我信它的是 check-facts 的自我认知——它从正文里榨出价格、度量值、版本号,然后把没出现在任何证据文件里的数字列出来,末尾补一句:我只能指出哪些结论是凭记忆写的,判断不了数字对不对。
一个工具主动告诉你它做不到什么,比它告诉你它能干什么值钱多了。我信它,就是从这句话开始的。
但正因为这样,unwrap 那个 bug 才刺眼
它不是"忘了写检查"。是写了检查,而且检查跑绿了。这俩是两种完全不同的病。
往深里看一层,这是结构性的错——写转换的人和写检查的人共享了同一份心智模型:"块引用是别人家的事,不归我管。"大概长这样:
# 示意,不是源码
def is_prose(line):
return not line.startswith(("#", "-", ">")) # > 是块引用,跳过
转换脚本用它决定哪些行要接回去,检查脚本用同一套判断决定哪些行要验。两边都客气地绕开了块引用。于是块引用被原样吐出去,检查一路放行。盲区不是共享一次,是被复用了两次。
不管这一段是两个真人先后写的,还是同一个 agent 一口气赶出来的,结果一样:一个跟被测逻辑共享假设的检查,等于没有检查,而且比没有检查更坏——没有检查你至少心虚,假通过的检查让你踏实。
这类 bug 为什么难发现,我想了很久。答案有点让人不舒服:你不会去复查一个已经绿了的东西。绿是终点,看见绿就往下走了。想抓它,你得有一个不共享假设的第二意见——换个人、换个上下文重写一遍检查,或者干脆别查了,冲到目标平台上去数。
同一种病的另一个症状
Medium 那条我一开始当笑话看,后来发现是同一个病。
它的 Medium HTML 产物是自包含的,图片嵌成 data URI。扔浏览器里打开,图片全在,好好的。粘到 Medium 编辑器里,四张图一张不剩——Medium 粘贴时直接丢掉 data URI。作者后来的记录我记得挺清楚:先是 4 张全丢,改成真实 URL 并先把图提交上去,4 张全在。
这个 4 和 4 才是重点。不是"我改了一下看起来没问题了",是"我去数了"。
本地预览通过是假的通过。你测的是你的浏览器能不能解 data URI,跟 Medium 要不要这张图,一点关系没有。这个道理放别处也一样——你在本地把迁移脚本 dry-run 通了,不代表生产库的约束跟本地一样;你在自己的 shell 里把环境变量配好了,不代表 CI 里那个也被改到了。测量点选错,测出来的绿就是安慰剂。
顺带说,dev.to 硬换行那条也是同一族。源文件按 95 列硬折行,dev.to 的渲染默认把硬换行当换行,于是发布后每个段落多出一个断。作者去数了——某篇已发布的文章 62 个段落里,47 个带多余换行,七成半。这个数字我得承认,如果是我,我大概不去数,我会觉得"看起来还行"。人家把仓库里的 95 列可读性和发布页面的一致性做成了两个能分开拨的旋钮,推送时自动取消换行,仓库副本照样能读。这个设计我服。
快照那条,我承认我熟
技能目录的位置随安装方式变——克隆时在 skills/publishing,从插件市场装时在带版本号的缓存路径里。写死的路径升级一次就废,这个大家都懂。
真正让我把电脑合上想了会儿的是后半句:插件市场安装会打快照,update 靠比较版本字符串判断要不要更新。也就是说,同一个版本号下改文件,改的东西不会进会话。
这条我太熟了。我有过改配置改到怀疑人生的经历——改了三遍,行为纹丝不动,最后发现跑的一直是当初那份快照。你以为你在验证刚改过的东西,其实你在验证一份三天前的旧拷贝。而且它安静得可怕,不报错,不提示,就是不听你的。
作者的解法很实在:迭代阶段别走市场装,老老实实软链接过去。我补一句我自己的版本——在迭代阶段,任何"缓存/快照/预编译"的便利都值得先怀疑一遍,因为它们的失效方式是静默的。
配料,还有一条别人的评论
同一份自测报告里还躺着一堆这个级别的缺陷,挑两个说。make-cover 的 --tile 参数因为值以连字符开头,被 argparse 当成选项吞了——这种坑查一百遍代码都查不出来,因为每一行都对。check-facts 把 127.0.0.1 读成了版本号——从内容里榨数字这活儿,边界比想象中糊。
还有一个不单拎出来我会不甘心:check-article 会把一张"已跟踪、但被重新生成过"的封面判成通过。同样是假通过,家人。
评论区有个叫 Alex Shev 的给了条我认为是整篇东西里最值钱的外部意见——这类自动化想更安全,前提是把"起草"和"不可逆操作"分开,并且保留最终文本、目标平台、时间戳的可审计日志。这跟我上面说的是同一件事的两面:真正按下发布键的还是人,脚本负责把证据摆齐。作者自己的流程也是这么设计的,Claude 写稿、生封面、核数字、跑预检、以草稿形式推上去,最后那一下是人按的。
那篇下面还有一条评论是来发 AI 工具广告的。放在这个主题下面,画外音:挺应景。
落到一条规则上
我不打算说"AI 写的检查不可信"这种正确的废话——真人写的检查一样会跟被测逻辑共享假设,我见过太多次了。
我想留的规则只有一条,具体到能执行:每次看到"通过",先问一句这个检查跟被检查的东西是不是同一套假设;分不开,就别信,去做一个笨的端到端。
放到发布这件事上,笨的端到端就是——真粘上去,然后在目标平台上数:段落数、图片数、地标数。作者那套东西里最打动我的恰恰是这个小动作:粘贴前先断言编辑器是空的,粘贴后统计地标数量,拿数字对数字。看起来土,但它是唯一不共享假设的那道闸。
这道闸的代价我曾经不肯付。后来证明我上面这个判断是错的——土办法省下的时间,远不如一次假通过亏掉的信任多。
本期缴税:一次"检查过了"换来一次"发出去才发现"。下次见 😅
