自动发布系统为什么需要在写入之前停一下?

自动化最容易给人一种错觉:既然机器可以在几秒钟内完成操作,就应该让它从发现文件一直跑到文章上线,中间不要停顿。事实上,速度越快、权限越大,系统越需要在真正写入之前设置一道明确的停线。这个停顿并不是人工流程残留下来的迟钝,而是把“准备做什么”与“已经改变了什么”分开的技术边界。没有这条边界,一次文件名写错、一段登录状态过期,或者一个分类名称不一致,都可能在网站上形成一串需要人工收拾的结果。

所谓写入前停一下,不是要求每篇文章都等人点击确认。更有效的做法,是让程序在一个只读阶段完成所有能够提前完成的判断:目录是否正确,元数据是否齐全,中英文标题是否对应,正文长度是否达到要求,category 的层级是否明确,tags 有没有重复,slug 是否会与现有文章冲突,发布时间是否能够被 WordPress 正确解释,以及浏览器登录状态是否仍然有效。只有这些检查同时通过,系统才跨过写入边界。

第一道区别,是“文件存在”不等于“文章可以发布”。

一个目录里出现新的 Markdown 文件,只能说明文件被创建了。它可能还在修改,可能只有中文而没有英文,也可能是系列说明、研究笔记或临时提纲。如果自动化仅凭“发现新文件”就行动,目录结构事实上变成了一个隐蔽的发布按钮,而写作者未必意识到自己已经按下它。更稳妥的设计要求每个批次有独立的 publication metadata,每篇文章也有自己的 meta;只有 enabledready 同时成立,文件才从普通内容变成待执行指令。

这也说明了为什么待发布目录必须与历史稿件、素材和脚本分开。扫描整个项目看起来方便,却会把系统不知道如何解释的文件也纳入权限范围。一个边界清楚的 queue 不只是整理习惯,它在缩小自动化能够造成影响的表面积。程序不需要“理解”项目里的所有内容,只需要拒绝队列之外的一切。

第二道区别,是“内容有效”不等于“目标明确”。

一篇文字完整的文章,如果没有固定 slug、category 和 tags,仍然不是完整的发布任务。程序若自行猜测,今天可能把文章放进 Digital Life,明天又因为标题出现 AI 而新建另一个分类。长期运行后,网站的思想结构会被自动化的临时判断慢慢改变。元数据的作用,就是把这些判断变成可以检查、可以审阅、可以重复执行的明确输入。自动化可以负责创建缺少的 subcategory,但它不能凭模糊相似度决定作者想建立什么分类。

双语标题尤其需要独立保存。把中英文标题拼在一个 WordPress 标题里,虽然省掉翻译界面的操作,却会让两种语言的页面都显示同一串混合文字。正确的流程应当把英文标题作为默认标题,再由 TranslatePress 为同一个字符串保存中文标题。这样,中英文网址使用同一个 slug,访客看到的 H1 却与当前语言一致。写入前检查必须确认两个标题都存在,而且与正文开头完全一致;否则浏览器自动化可能选中错误的字符串。

第三道区别,是“拥有凭据”不等于“可以安全执行”。

WordPress OAuth token 能够调用 REST API,却不能自动变成 wp-admin 的浏览器会话。TranslatePress 的标题处理需要一个真实登录过的浏览器状态。若系统等到文章已经创建之后才发现 Cookie 过期,就会留下正文已写入、中文标题却没有完成的半成品。更合理的顺序,是在任何 API 写入之前先启动无界面 Chromium,访问后台并确认仍处于登录状态。预检失败时整批停止,网站保持不变。

这种设计也避免把 WordPress 密码长期交给自动化。用户可以在可信的本机浏览器中完成一次正常登录和二次验证,然后把有限期限的会话状态作为 secret 提供给后台 Job。Job 使用会话,不读取密码;会话失效后明确报错,等待重新授权。它没有试图绕过验证,而是把验证看成自动化权限的一部分。

第四道区别,是“调用成功”不等于“结果正确”。

HTTP 返回成功,只说明服务器接受了请求。它不能证明文章进入了预期分类,不能证明状态确实是 draft,也不能证明中文页面显示中文标题。可靠的发布流程需要分层核验:REST 返回的文章 ID、slug 和状态要被记录;分类与 tags 要通过 ID 绑定;TranslatePress 保存后要分别打开英文和中文页面;页面上的 H1 必须与元数据精确相等。核验不是额外装饰,而是自动化最后一次承认自己可能做错的机会。

对 draft 也不能省略这些检查。草稿不会公开,但它仍然是网站数据库中的真实对象,也可能被编辑、预览或错误地转为公开。把测试文章设为 draft,只是降低影响,并没有取消正确性的要求。测试真正要证明的,是从 GitHub 变更、私有镜像、Azure 身份、容器 secret、WordPress API 到无界面浏览器这一整条路径都按预期工作,而不是仅仅证明某个脚本在本机能够运行。

第五道区别,是“可以重试”不等于“可以重复创建”。

云端任务可能因为网络中断、平台重启或浏览器页面加载超时而失败。失败后重新运行是正常能力,但如果每次重试都生成一篇新文章,可靠性机制反而会制造重复内容。因此,slug 应当承担幂等键的作用。系统在写入前先查找同 slug 的对象,再根据 metadata 明确选择 skip、update 或 error。测试草稿适合使用 update:第一次运行创建草稿,后续重试只修复同一个对象,不会产生第二篇。

幂等并不意味着可以随意覆盖。update 必须是文章作者或发布配置明确选择的策略,而不是程序遇到冲突后的默认猜测。对已经公开、可能有人阅读或链接的文章,未经明确授权的覆盖尤其危险。自动化应当把冲突暴露出来,而不是为了让流程显示绿色就消除差异。

第六道区别,是“后台运行”不等于“永远运行”。

发布系统没有必要维持一个持续在线的服务器,也不需要开放网页界面等待外部请求。更合适的形态是有限任务:GitHub Actions 发现待发布目录出现真实变更,构建私有镜像,更新没有 ingress 的 Container Apps Job,然后启动一次执行。Job 校验、写入、核验并退出;没有新内容时保持零实例。这样既缩小攻击面,也让成本与实际执行时间对应。

触发条件同样需要停线思想。代码、说明文件或测试变化可以重新部署镜像,但不应自动发布文章。只有 publication metadata、文章 meta、正文或系列 Page 等真实队列文件发生变化,发布步骤才启动。部署与发布是两个不同动作;把它们拆开,才能在改进程序时不意外改变网站内容。

第七道区别,是“秘密被保存”不等于“秘密进入代码”。

OAuth token、浏览器会话和镜像拉取凭据都属于运行环境,而不是项目内容。它们可以被 Container Apps 作为 secret 引用,却不应出现在 Git 提交、构建日志、命令输出或文章 metadata 中。仓库可以保存变量名称和 secret reference,但不能保存值。即使仓库是私有的,这条边界仍然重要,因为版本历史会长期保留,而运行凭据应当能够独立轮换和失效。

日志也要遵守同样原则。一个好的执行报告会列出 publication ID、文章 slug、WordPress 对象 ID、执行动作和核验结果,却不会打印 access token 或 Cookie。可观察性不是把所有信息都输出,而是提供足够判断流程在哪里成功、在哪里停止的非敏感证据。

真正可靠的自动化,不追求从不停止。

它追求的是在错误仍然便宜时停止。目录错误在本地校验阶段被发现,成本只是修改一个文件;浏览器会话失效在预检阶段被发现,成本只是重新登录;slug 冲突在写入前被发现,成本只是确认策略。若这些问题一直被带到公开发布之后,成本就变成删除重复文章、修复链接、重建翻译,甚至解释为什么读者看到了一篇不完整的内容。

因此,写入前的停顿并不是自动化的反面。它是让自动化能够长期被信任的结构:输入要有边界,意图要有 metadata,凭据要在运行环境中隔离,写入要幂等,浏览器要先预检,结果要从两个语言页面核验,任务完成后要退出。系统越能清楚地说明自己为什么继续、为什么停止,人就越有可能把真实而重要的工作交给它。


了解 Geoffrey Chen 的更多信息

订阅后即可通过电子邮件收到最新文章。