写在前面
这篇博客的发布方式,一年里换了三代。
第一代是后台网页:写完稿子,打开管理端,粘贴 Markdown,传封面,填分类标签,检查预览,点发布。一篇文章的"发布动作"大概要点十几次鼠标,而且每次粘贴完都得警惕编辑器把代码块格式吃掉。
第二代换成了命令行,本地写好 Markdown,一条命令导入。第三代是最近才升级的——把导入、元数据、封面、验证、归档整个流程包进一个脚本,一条命令发布一篇,一个循环发布一批。这篇记录第三代长什么样,以及升级路上踩的三个真实的坑。
一、为什么值得自动化
先说清楚自动化的对象:发布动作,不是写作。
写作没法自动化——选题、读代码、组织语言是这件事的全部价值所在。但发布动作是纯机械的:传文件、填字段、点按钮、检查。机械流程有两个特征:可以标准化,且标准化之后必然会出现一次"漏了一步"——忘传封面、忘选分类、粘贴时丢了正文最后一段。
自动化真正的收益不是快,是把"漏了一步"变成"脚本退出码非零"。
另外还有密钥问题。网页发布靠浏览器登录态,命令行发布靠 PAT(个人访问令牌)。令牌这种东西不能躺在本地的明文文件里——我的做法是把它放进 macOS 钥匙串,脚本运行时现取:
export HALO_TOKEN=$(security find-generic-password -s zcode.halo.pat -a default -w)
发布用的临时环境变量,用完即走,不落盘。(钥匙串管理的完整方案之前写过一篇,这里不重复。)
二、一条流水线的六个步骤
脚本的核心流程,每篇文章六步:
# 1. 导入 Markdown(--force 允许重复执行时覆盖同名草稿)
halo post import-markdown --file "$f" --profile sailtrack --json --force
# 2. 更新元数据:slug、标题、封面、分类、标签,并发布
halo post update "$name" --slug "$slug" --title "$title" \
--cover "/upload/$cover" --publish true \
--categories "后端开发" --tags "$tags"
# 3. 备份正文(发布后正文可能被服务端重建,必须留底)
curl -s -H "Authorization: Bearer $HALO_TOKEN" \
"$BASE/apis/.../posts/$name/head-content?raw=true&rawType=markdown" > head.json
# 4. 回填发布时间(API 更新元数据,正文原样带回)
curl -s -X PUT --data-binary @put.json "$BASE/apis/.../posts/$name"
# 5. 四项验证,任何一项不过都算失败
# 发布时间 / 正文逐字一致 / 文章页 200 / 封面 200
# 6. 全部通过,本地 Markdown 从 drafts/ 移入 posts/ 归档
两个设计决定值得展开。
清单驱动,不用命令行参数。 每篇文章的 slug、标题、封面文件、标签都在一张 manifest 表里(TSV,一篇文章一行),脚本按行读。想发布哪几篇就传行号:publish.sh 3 5 7。好处是"发布计划"本身是一份可检查的文件——发布前扫一眼 manifest,比扫眼脚本参数踏实得多。
slug 是约定的一部分。 我的每篇文章用 8 位随机字母数字做 slug,本地文件名用完整中文标题。这样 URL 短而不可读(我不在乎 URL 里的关键词),文件系统里却一目了然。有个小坑:更新文章时如果不带 --slug 参数,Halo 会重新生成 slug——所有 update 命令必须显式带上,这条写进了脚本的固定动作。
三、验证清单是流水线的底线
第六步的四项验证,是我认为整段脚本里最值钱的部分:
✓ publishTime 与预期一致
✓ 正文与备份逐字一致(8712 字节)
✓ /archives/{slug} → 200
✓ /upload/cover-xxx.png → 200
正文逐字一致尤其重要,因为它防的是一个隐蔽行为:通过 API 更新文章元数据时,正文会被服务端按提交内容重建。如果脚本在某一步把正文传丢或传错,文章照样发布成功、页面照样 200——你不会收到任何报错,直到某天自己翻旧文才发现代码块变成了乱码。所以流程是:更新前备份正文原文,更新后拉回来逐字节比对,不一致就报警并把本地文件留在待发布区。
验证要在发布后做,而不是发布中做。 发布接口返回 200 只代表"服务器收下了",不代表"读者看到的是对的"。封面 200 检查的就是这类问题——附件上传成功但文章引用路径写错,发布接口完全无感,读者端却是裂图。
四、三个真实的坑
流水线跑通之前,撞了三个值得一记的坑。
坑一:乐观锁 409。 脚本先"更新并发布",紧接着"回填发布时间"。第二步偶尔撞上 409 Conflict——因为发布动作在服务端是异步收尾的:接口返回了,后台还在改文章的版本号,脚本紧接着 PUT 携带的是发布前拿到的旧版本号,乐观锁直接拒绝。第一次修复是"失败就重试",但重试带着旧版本号,重试三次全部失败。正确做法是:每次重试前重新拉取文章的最新版本,用新版本号组装请求。教训:带乐观锁的系统里,“重试"永远等于"重取 + 重试”。
坑二:CDN 525。 批量发布到一半,所有请求开始失败,错误码 525。一开始怀疑是脚本把服务器打挂了——但连博客首页都是 525,这说明问题在源站,不在发布脚本。这次的经验是先把"我的请求"和"站点健康"分开判断:curl 一下首页,首页挂了就不是脚本的事,停下来等源站恢复。那天源站宕了近一小时后自愈,脚本带着重试逻辑继续跑完。
坑三:本机 DNS 劫持。 排查 525 时差点被带偏:本机开了 fake-IP 模式的代理,所有域名解析都返回保留网段的假地址,连指定公共 DNS 查询都被拦截。想拿真实 IP 得绕道 DoH(DNS over HTTPS)。这条不是博客的问题,是排查网络问题的通用提醒:先确认你的诊断工具本身没被劫持,否则看到的世界全是假的。
五、自动化到哪为止
流水线覆盖了机械部分,但有三件事我刻意留给人:
选题和排期。 哪篇先发、间隔多久,是内容判断不是机械动作,manifest 手工排。
封面。 脚本会检查封面 URL 是否 200,但"这张封面配不配这篇文章"没法检查,人工过目。
发布后的通读。 脚本验证的是字节一致性,不是阅读体验——格式对了但读起来别扭的段落,只有自己通读才能发现。
自动化有个容易上头的方向:把这三个也"解决掉"(比如 AI 生成封面直接上传)。我不排斥,但每个自动化都要先回答:这一步出错时,脚本发现得了吗?发现不了的地方,就留给人。
六、小结
三点体会:
- 自动化的第一收益是防错,不是省时间。 “漏传封面"从"读者看裂图"变成"脚本验证失败”,这条流水线就值回票价了。
- 带乐观锁的系统里,重试 = 重取 + 重试。 拿着旧版本号重试一百次也是 409。
- 先分清"我的问题"和"站点的问题"。 排查前先打一条与脚本无关的健康检查,能省掉一整段错误方向的努力。
现在发布一篇的完整成本是:写完 Markdown,往 manifest 加一行,跑一条命令,看一排绿色的对勾。省下来的十分钟,全部还给写作本身。
默认评论
Halo系统提供的评论