标签: 自动化

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

    小说连载自动化:一条命令把 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,把登录表单模拟那一步去掉

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