用 Python 把 AI 写好的文章送进 WordPress 草稿箱

老K 19 0

[aru_42]翻了下博客,上一篇还是 6 月写的,转眼又过去几个月。平时折腾的东西不少,真要整理成文章,就总想着“等有空再写”。

这次我想试着让 AI 帮忙整理资料、写初稿,再把文章直接送进 WordPress 后台。我自己看完,改好,再点发布。先把这一段接通,也顺便记一下做法。

用 Python 把 AI 写好的文章送进 WordPress 草稿箱

写作 → 检查 → 草稿 → 人工审核。流程示意,非后台截图。

我用的是自建 WordPress。它自带 REST API,可以从外部读取和编辑文章,所以这一步不用另外装 AI 写作插件。下面用 Python 把认证、存草稿和读回检查串起来。

先把“写文章”和“存文章”分开

这里容易混在一起:AI 助手负责查资料、整理正文;Python 脚本负责把准备好的正文保存到 WordPress。光运行下面的脚本,还不会凭空生成一篇文章。

我现在先在本机助手里完成写作。等初稿出来,整理成一个 JSON 文件,再交给脚本。这样可以先把接口跑通,后面要加定时任务,也知道是哪一步出了问题。

文章状态我固定用 draft。让 AI 帮我写可以,发布前我还是要看一遍,尤其是代码、版本和它引用的资料。

先给它一个专用账号

第一步,我先打开这个地址,看看能不能拿到文章列表:

https://你的域名/wp-json/wp/v2/posts?per_page=1

返回 JSON 就说明公开读取正常。到这里还没有登录,也不代表能写文章。

接着在 WordPress 后台新建一个专用账号。只需要写自己的新文章,“作者”角色就够用;我后面还想整理旧文章,所以这次用“编辑”角色。没必要为内容维护给管理员权限。

在这个用户的资料页里找到“应用程序密码”,起个能看懂的名字,比如 Blog Draft Writer,生成后保存。接口用的是这个密码,不是平时登录后台的主密码。以后不用了,去用户资料页单独撤销就行。

顺手记一下:应用程序密码继承所属账号的权限。“只保存草稿”是下面脚本的约束,不是 WordPress 给这串密码加的权限限制。

把配置放在本机,别写进代码

我把配置单独放到 ~/.config/wordpress/site.json。如果目录还没有,先建出来:

mkdir -p ~/.config/wordpress

然后新建 site.json,填入自己的站点地址、登录用户名和刚生成的应用程序密码:

{
  "site_url": "https://your-blog.example",
  "username": "blog-ai",
  "application_password": "在本机填写应用程序密码"
}

这里的域名和用户名都是示例,要换成自己的。站点地址用最终的 HTTPS 地址;如果 WordPress 装在子目录,也把子目录带上。

macOS / Linux 下,我再给这个文件限制一下权限:

chmod 600 ~/.config/wordpress/site.json

配置文件留在本机,不放进 Git 仓库。运行时只传路径,终端历史里就不会留下整串密码。

用 Python 把草稿存进去

下面保存为 wp_draft.py。我这次用 Python 3.9.6 验证;代码只用标准库,不需要先装 requests。

它默认只检查认证。确认账号没问题,再加 --write 和文章文件路径,才会写入。写完会重新读取文章,核对状态、标题和正文。

import argparse
import base64
import json
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.parse import urlsplit
from urllib.request import Request, HTTPRedirectHandler, build_opener


class NoRedirect(HTTPRedirectHandler):
    def redirect_request(self, *args, **kwargs):
        return None


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--config", required=True)
    parser.add_argument("--article")
    parser.add_argument("--write", action="store_true")
    args = parser.parse_args()
    cfg = json.loads(Path(args.config).expanduser().read_text(encoding="utf-8"))
    site = cfg["site_url"].rstrip("/")
    url = urlsplit(site)
    if (url.scheme != "https" or not url.hostname or url.username
            or url.password or url.query or url.fragment):
        raise SystemExit("site_url 必须是没有账号、查询参数或片段的 HTTPS 站点地址")
    token = base64.b64encode(
        (cfg["username"] + ":" + cfg["application_password"]).encode("utf-8")
    ).decode("ascii")
    opener = build_opener(NoRedirect)

    def api(route, payload=None):
        data = None if payload is None else json.dumps(
            payload, ensure_ascii=False
        ).encode("utf-8")
        request = Request(site + "/wp-json/wp/v2/" + route, data=data,
                          headers={"Authorization": "Basic " + token,
                                   "Content-Type": "application/json",
                                   "Accept": "application/json"})
        with opener.open(request, timeout=30) as response:
            return json.load(response)

    me = api("users/me?context=edit")
    if not me.get("capabilities", {}).get("edit_posts"):
        raise SystemExit("当前账号没有文章编辑权限")
    print("认证通过,用户 ID:", me["id"])
    if not args.write:
        print("只读检查完成。创建草稿需同时指定 --write 和 --article。")
        return
    if not args.article:
        parser.error("--write 必须配合 --article 使用")
    article = json.loads(Path(args.article).expanduser().read_text(encoding="utf-8"))
    if not isinstance(article, dict):
        raise SystemExit("文章文件必须是 JSON 对象")
    payload = {key: article[key] for key in
               ("title", "content", "slug", "excerpt", "categories")
               if key in article}
    if not all(isinstance(payload.get(k), str) and payload[k].strip()
               for k in ("title", "content")):
        raise SystemExit("文章必须包含 title 和 content")
    payload["status"] = "draft"
    post = api("posts", payload)
    saved = api(f"posts/{post['id']}?context=edit")
    if (saved["status"] != "draft"
            or saved["title"]["raw"] != payload["title"]
            or saved["content"]["raw"].strip() != payload["content"].strip()):
        raise SystemExit("写入后核验失败,请到后台检查,不要直接重试")
    print("草稿 ID:", saved["id"])
    print("后台编辑:", site + "/wp-admin/post.php?post="
          + str(saved["id"]) + "&action=edit")


if __name__ == "__main__":
    try:
        main()
    except HTTPError as error:
        raise SystemExit("HTTP " + str(error.code)
                         + ":检查认证和权限;若写入结果不明,先查后台再重试")
    except (URLError, TimeoutError):
        raise SystemExit("网络请求失败;若已提交写入,先查后台确认结果")

我没有让请求自动跟随跳转。带着认证信息请求时,先确认地址更稳妥;如果遇到 301 / 302,先把配置改成站点实际使用的 HTTPS 地址。

先只读跑一次:

python3 wp_draft.py --config ~/.config/wordpress/site.json

正常的话,会看到用户 ID,以及“只读检查完成”的提示。这一步不会新增文章。

然后准备 article.json。正文用 HTML,下面放一个最小例子:

{
  "title": "给博客接入 AI 的第一步:保存草稿",
  "slug": "ai-blog-draft-first-step",
  "content": "<p>这里是初稿正文。</p><h2>先解决什么问题</h2><p>这里继续写。</p>",
  "excerpt": "用 WordPress REST API 把一篇初稿送进后台。"
}

最后提交:

python3 wp_draft.py --config ~/.config/wordpress/site.json \
  --article article.json --write

终端会输出草稿 ID 和后台编辑链接。打开链接就能继续改文章;图片、代码高亮和手机上的排版,我会在发布前再看一遍。

我这次已经在自己的站点走完了认证、保存和读回核验。这份脚本是一次创建的最小例子,重复运行写入命令,会再创建文章。给定时任务用的时候,要额外记录文章 ID、提交前查重,不能把它直接当成完整的自动维护程序。

几个值得记住的小地方

超时以后,先去后台看。客户端没等到响应,服务器也可能已经存好了。直接再跑一次,容易多出一篇草稿。确认是否生成,再决定要不要重试。

401 先查认证,403 再查权限和拦截。用户名要用登录用户名,密码要用应用程序密码;反向代理如果没传递 Authorization,也会认证失败。如果返回的是防火墙页面,就先看响应内容和防火墙日志,不要一上来把防护都关掉。

正文里的代码要转义。我会用 <pre><code> 包代码块,再对代码里的 <、>、& 做 HTML 转义,避免被浏览器当成标签。实际显示还要看主题和高亮插件。

草稿核验失败,也先查后台。不同角色或插件可能处理 HTML。示例按原文比较正文;如果站点改写了内容,它会提示检查,而不会默默当作成功。

更新旧文章之前,先留一份原文。这篇只创建新草稿。以后做旧文维护,我会先看修改前后的差异,再更新原文章,不会把已发布文章改成草稿来代替审核。

我现在先这样用

先让 AI 沿着博客原有的技术方向整理一篇初稿,核对资料、跑一下代码,然后保存到草稿箱。我有空时进去看,改掉不合适的说法,确认以后再发布。

目前每周任务还是在本机运行,电脑和应用开着时才能执行。把本文这个脚本搬到服务器,只是换了个存文章的位置;要独立完成每周写作,还需要把 AI 的运行环境、检索和调度一起配好。我先把本机流程用顺。

后面真要扩展,我会先补查重和运行记录。能稳定送来一篇值得看的草稿,比一口气生成很多篇更有用。

这次核对时用到的文档,留在这里方便以后查:

发表评论 取消回复

Warning: Undefined variable $user_ID in /www/wwwroot/x4v.cn/wp-content/themes/mkBlog/comments.php on line 30

Warning: Undefined variable $user_ID in /www/wwwroot/x4v.cn/wp-content/themes/mkBlog/comments.php on line 51
OwO 图片 链接 代码

分享