小说连载自动化:一条命令把 13 章草稿推上 WordPress

作者:

《货舱里装着一整个文明》第一卷已经写到第 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. 文章标题用「《货舱里装着一整个文明》+ 章节标题」。博客是技术 + 生活 + 小说混排,不带书名,章节标题在首页信息流里认不出来
  2. 内部备注不发出去——那是创作手记,不是正文
  3. 一章 = 一篇文章,归「小说」分类,状态直接已发布
  4. 幂等:同一命令重跑不产生重复文章;正文改过后重跑,应更新原文

二、三个关键设计决策

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 '&', '&amp;' -replace '<', '&lt;' -replace '>', '&gt;'
    "<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,把登录表单模拟那一步去掉

自动化的乐趣在于:写完一章,跑一行命令,关掉编辑器,章节已经挂在站上了。创作状态不用切去后台打断。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注