[aru_42]翻了下博客,上一篇还是 6 月写的,转眼又过去几个月。平时折腾的东西不少,真要整理成文章,就总想着“等有空再写”。
这次我想试着让 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 的运行环境、检索和调度一起配好。我先把本机流程用顺。
后面真要扩展,我会先补查重和运行记录。能稳定送来一篇值得看的草稿,比一口气生成很多篇更有用。
这次核对时用到的文档,留在这里方便以后查:
🎨 原创不易,支持请点赞、转载请注明本文作者为老K
