《货舱里装着一整个文明》第一卷已经写到第 13 章,草稿都是本地 markdown 文件,一章一个。前五章手动登录 wp-admin 发,五章还行,再往后每章都这么操作,不如写个脚本。
最终效果:
pwsh -File tools/publish-chapters.ps1 # 发布第 1–5 章
pwsh -File tools/publish-chapters.ps1 -Start 6 -End 8 # 发布第 6–8 章
一条命令,把指定章节区间发到「小说」分类。这篇文章记录设计决策和踩坑过程。认证部分单独成文(见技术笔记分类的《三种方式登录 WordPress REST API》),这里只交代结论:站点是纯 HTTP,采用「登录表单 + Cookie + Nonce」方案。
一、先明确输入和输出
草稿文件结构是固定的(自己定的):
06-草稿/
第001章.md
第002章.md
...
每个文件长这样:
# 第001章 · 首航
> 本章细纲:首航立人设(嘴碎怂包新手)……
> 本章目标:500 字内让读者听见老锈在吐槽……
---
老锈在我第一次深空航行的第三十七秒开口了。
三段:H1 章节标题、内部备注(两行 > 开头)、正文(--- 之后)。发布要求:
- 文章标题用「《货舱里装着一整个文明》+ 章节标题」。博客是技术 + 生活 + 小说混排,不带书名,章节标题在首页信息流里认不出来
- 内部备注不发出去——那是创作手记,不是正文
- 一章 = 一篇文章,归「小说」分类,状态直接已发布
- 幂等:同一命令重跑不产生重复文章;正文改过后重跑,应更新原文
二、三个关键设计决策
1. 内容清洗:只取 --- 之后的正文
转换规则很简单:
- 丢弃第一条
---之前的所有内容(H1、细纲备注、空行) - 正文按空行分段,每段包
<p></p> - 转义 HTML 实体(
&、<、>)
WordPress REST 的 content 字段直接存 HTML,不会做 markdown 转换——这一步不能省,否则原文直接提交,前端会把整章渲染成一整行。
# 按空行分段
$paras = [System.Collections.Generic.List[string]]::new()
$cur = [System.Collections.Generic.List[string]]::new()
foreach ($ln in $bodyLines) {
if ($ln.Trim() -eq '') {
if ($cur.Count -gt 0) { $paras.Add(($cur -join "`n")); $cur.Clear() }
} else {
$cur.Add($ln)
}
}
$html = ($paras | ForEach-Object {
$esc = $_ -replace '&', '&' -replace '<', '<' -replace '>', '>'
"<p>$esc</p>"
}) -join "`n"
2. 幂等:先查后做
每章发布前,先按标题在「小说」分类里查同名文章:
- 查到 →
POST /wp/v2/posts/{id}更新标题和正文 - 没查到 →
POST /wp/v2/posts新建
这一条让脚本的语义从「发布」变成「同步」:任何时候、跑多少遍,站点状态都和本地文件一致。前五章的正文我改过几轮(统一第一人称、修硬伤),每轮都是重跑一遍命令,站点自动跟上,没有手动核对过。
3. 分类:先查后建
「小说」分类在脚本第一次运行时自动创建,已存在则直接取 id。脚本不依赖任何人工前置操作。
三、踩坑记录
坑比设计更有意思。
坑 1:PowerShell 拒绝在 HTTP 上发凭证。 Invoke-RestMethod -Credential 在明文连接上会直接报错,要加 -AllowUnencryptedAuthentication 才肯发。不知道这个参数之前,对着报错看了很久。
坑 2:登录表单有 test cookie 机制。 直接 POST 登录,收到「Cookies 被阻止或者您的浏览器不支持」。必须先把登录页 GET 一次拿到 test cookie,再 POST。这步不报错、不提示,纯靠读 WordPress 源码才知道。
坑 3:最隐蔽的一个——构造表单对象失败,脚本却没停。 FormUrlEncodedContent 不接受 OrderedDictionary,得先转成 List[KeyValuePair]。更坑的是 New-Object 失败后脚本继续往下走,PostAsync 发了一个空 body 的请求,登录静默失败,错误在两步之后才以「找不到 nonce」的形式爆出来。教训:关键中间结果必须立即断言——登录 POST 之后现在会马上检查响应,不符合预期立刻抛错。
坑 4:WebSession 的 Cookie 解析不可靠。 用 PowerShell 7.6 的 Invoke-WebRequest -WebSession,登录明明成功(302),会话里的 Cookie 集合却解析出名字、值全空的条目——Cookie 在响应头里,但没进集合。换成 .NET HttpClient(Cookie 容器由运行时管理),一次解决。
坑 5:UTF-8 要显式指定。 中文 JSON body 用 New-Object StringContent($json, [System.Text.Encoding]::UTF8, 'application/json')。编码不赌默认值,谁也不想为博客乱码调半天。
坑 6:别假设环境站在你这边。 最开始的 Basic Auth 401(细节见上一篇)。总结成一条:动工前先验证认证通路,拿 GET /users/me 这种只读接口试,通了再干,不通就换方案,别在正式流程里撞。
四、后续还能做什么
脚本现在做到「一章一篇」,够用且稳。下一步想法:
- 章节末尾自动插入「上一章 / 下一章」链接
- 自动生成连载目录页,挂到主页
- 封面图支持:章节文件同目录放一张图,走
media端点上传后设为 featured image - 站点上 HTTPS 后,认证换成 Application Password,把登录表单模拟那一步去掉
自动化的乐趣在于:写完一章,跑一行命令,关掉编辑器,章节已经挂在站上了。创作状态不用切去后台打断。

发表回复