1. 从二十分钟到两分钟,这个效率账到底怎么算
一篇内容从写完到真正在站点上跑起来,中间到底要经过多少道手?我拿自己维护的几个 Z-Blog 站点做过一次完整计时:打开后台、登录、新建文章、填标题、粘贴正文、处理图片、选分类、打标签、写摘要、设别名、调发布时间、预览、发布、再回前台检查排版——这一套动作走下来,手快的人也要十五到二十分钟,手慢或者中途被别的事打断,半小时都打不住。如果一天要发三篇,光“搬运”这件事就能吃掉一个上午。
这个项目要解决的就是这段“搬运时间”。核心思路很直接:把 Z-Blog 的文章发布能力封装成一个可被调用的技能(Skill),挂到 WorkBuddy 这类自动化助手框架上,让“给一篇内容 + 一句指令”直接产出“已发布并可访问的文章链接”。标题里说的“二十分钟压到两分钟”,压缩掉的不是写作时间,而是所有机械性的后台操作时间。
适合看这篇的人有三类:一是自己维护 Z-Blog 站点的独立博主,日常更新频率不低但被后台操作拖累;二是手里管着多个站点、需要批量分发内容的人;三是对自动化助手技能开发感兴趣、想拿一个真实场景练手的开发者。哪怕你之前没写过 Skill,只要懂一点 HTTP 请求和基本的配置思维,跟着走也能落地。
我先把结论摆在这:这套方案的技术底座是Z-Blog 的 XML-RPC 接口(老版本)或REST 风格的发布接口(新版本),中间用一层轻量的适配逻辑把“内容”翻译成接口能吃的参数,外层再包一个 WorkBuddy 能识别的技能描述文件。整条链路没有黑魔法,全是可调试、可回滚的常规操作。下面我把设计思路、接口细节、实操步骤和踩过的坑一层层拆开讲。
2. 整体设计思路与方案选型拆解
2.1 为什么不做浏览器自动化,而选接口直连
一提到“自动发文章”,很多人的第一反应是用浏览器自动化工具去模拟点击。我一开始也试过这条路,结论是:能用,但不值得。原因有三个。
第一是稳定性。浏览器自动化依赖页面结构,Z-Blog 后台的主题、插件、版本一变,选择器就可能失效,今天能跑的脚本明天就报错。而接口是官方约定好的契约,只要版本不大改,参数结构基本稳定。
第二是速度。模拟点击要等页面加载、等 DOM 渲染、等 AJAX 回包,一个发布动作光等待就得好几秒。接口直连是一次 HTTP 请求的事,几百毫秒就回来了。标题里“两分钟”其实还留了余量,纯接口调用加上内容预处理,通常几十秒就够。
第三是可维护性。接口调用的失败信息是明确的(比如返回码、错误描述),排查起来有方向;浏览器自动化的失败往往是“元素没找到”,你得截图、录屏、猜原因,调试成本高得多。
注意:选接口直连的前提是你的 Z-Blog 开启了对应的发布接口。老版本走 XML-RPC,需要在后台设置里显式打开;新版本一般有 REST 接口,权限通过应用密码或 Token 控制。这一步没开,后面全白搭。
2.2 技能(Skill)这一层到底承担什么角色
WorkBuddy 这类框架里的“技能”,本质是一份描述文件 + 一段执行逻辑。描述文件告诉助手“这个技能叫什么、什么时候该用、需要哪些输入”,执行逻辑负责“拿到输入后怎么调接口、怎么处理返回”。
我把它拆成三块职责:
- 输入归一化:用户可能给一段 Markdown、一个本地文件路径、甚至一段纯文本,技能要先把它们统一成“标题 + 正文 + 元数据”的结构。
- 参数映射:把归一化后的结构翻译成 Z-Blog 接口要求的字段,比如
title、description、categories、tags、post_status等。 - 结果回传:接口返回文章 ID 或链接后,整理成人类可读的结果,最好直接给出可点击的访问地址。
这样分层的好处是,哪天 Z-Blog 接口变了,只需要改“参数映射”这一层,输入和输出逻辑不用动。我见过不少人把这三件事揉成一坨代码,结果接口一升级就得重写,非常痛苦。
2.3 内容预处理的边界在哪里
有个问题必须先想清楚:技能要不要负责“排版美化”?我的答案是只做必要的结构化处理,不做主观美化。
必要的结构化处理包括:把 Markdown 转成 Z-Blog 能正确渲染的 HTML、处理图片的引用路径、生成摘要、从正文里提取候选标签。这些是“不处理就会出错”的事。
主观美化比如加什么样式、用什么字体、段落间距多少,这些应该由 Z-Blog 的主题和 CSS 决定,技能不该越界。一旦技能开始管样式,你就等于把主题的活儿又干了一遍,维护成本翻倍,而且换个主题全乱套。
我踩过的坑:早期版本我在技能里硬编码了一段内联样式,结果换主题后文章排版和站点整体风格打架,只能一篇篇回去改。后来把样式全部交还给主题,技能只输出干净的语义化 HTML,问题就没了。
3. 核心细节解析与实操要点
3.1 Z-Blog 发布接口的字段结构
不管走 XML-RPC 还是 REST,核心字段大同小异。我把关键字段和它们的“坑点”列成一张表,这张表是我反复调试后总结的,比官方文档更贴近实战。
| 字段名 | 含义 | 常见坑点 |
|---|---|---|
| title | 文章标题 | 不能为空,部分版本对长度有限制 |
| description | 正文内容 | 必须是接口能接受的格式,Markdown 要先转 HTML |
| categories | 分类 | 传的是分类 ID 不是名称,需要先查 |
| tags | 标签 | 部分版本要求标签已存在,否则静默丢弃 |
| post_status | 发布状态 | 草稿/发布的值各版本不同,要实测 |
| wp_slug / alias | 别名 | 不传会自动生成,中文标题生成的别名可能很难看 |
| post_date | 发布时间 | 时区问题高发区,建议显式传本地时间 |
这里重点说两个最容易翻车的地方。
分类 ID 的获取。接口发布时categories要的是数字 ID,但你在写内容时脑子里想的是“技术”这个分类名。所以技能里必须有一个“分类名到 ID”的映射步骤。我的做法是启动时调一次分类列表接口,把结果缓存成字典,发布时直接查表。缓存要设过期时间,不然你在后台新建了分类,技能这边还不知道。
时区。这是最隐蔽的坑。服务器时区、Z-Blog 配置时区、你本地时区,三者不一致时,文章发布时间会莫名其妙偏移几个小时。我的处理方式是:技能里统一用本地时间生成post_date,并在配置里显式声明时区偏移,不依赖服务器默认值。实测下来这样最稳。
3.2 内容归一化的具体规则
输入可能是 Markdown,也可能是纯文本,还可能是带 front-matter 的文件。我定的归一化规则是这样的:
- 如果输入带 front-matter(
---包裹的元数据块),先解析出标题、分类、标签、摘要。 - 正文部分如果是 Markdown,用转换器转成 HTML;如果已经是 HTML,原样保留。
- 标题缺失时,取正文第一个一级标题;再没有就用正文前二十个字兜底。
- 摘要缺失时,剥离 HTML 标签后取前一百二十个字。
- 标签缺失时,从正文里按词频提取候选,但不自动写入,而是回传给用户确认。
最后这条很重要。自动打标签看起来智能,实际上经常打出莫名其妙的词,污染站点标签体系。我宁愿多一步确认,也不要事后清理一堆垃圾标签。
3.3 图片处理这个隐形时间黑洞
标题说“二十分钟压到两分钟”,很多人以为省的是打字时间,其实图片处理才是大头。一篇带五张图的内容,手动上传、复制链接、替换引用,轻松花掉十分钟。
技能里必须把图片处理自动化。我的方案是:正文里的图片引用如果是本地路径,技能先调 Z-Blog 的媒体上传接口把图传上去,拿到远程 URL,再替换正文里的引用。这一步做完,图片就跟着文章一起“落地”了。
提示:上传接口一般也走同一套鉴权。如果图片较多,建议串行上传并加重试,别并发,否则容易触发服务端的频率限制。
3.4 鉴权方式的选择
Z-Blog 的接口鉴权,老版本常用用户名加密码,新版本推荐用应用密码或 Token。我的建议是能用 Token 就别用主密码。原因很简单:Token 可以单独吊销,泄露了影响面小;主密码一旦写进配置文件,风险大得多。
Token 的存放也有讲究。别硬编码在技能代码里,放到环境变量或独立的配置文件,并且确保这个文件不被提交到任何公开仓库。我见过有人把带 Token 的配置直接推到公开代码托管平台,结果站点被灌了一堆垃圾文章,教训很深刻。
4. 实操过程与核心环节实现
4.1 环境准备与依赖确认
动手之前,先把这几件事确认清楚,能省掉后面大量返工。
- Z-Blog 版本号,以及它支持的接口类型(XML-RPC 还是 REST)。
- 后台是否已开启对应接口,路径通常在“设置”或“插件”里。
- 用于鉴权的账号,以及它的发布权限是否足够。
- 本地运行环境,Python 3.8 以上即可,主要依赖
requests和markdown两个库。
依赖安装就一行:
pip install requests markdown如果你打算把技能跑在容器里,记得把时区环境变量设对,比如TZ=Asia/Shanghai,否则前面说的时区坑会准时找上门。
4.2 技能描述文件的写法
WorkBuddy 识别技能靠的是描述文件。我用的结构大致是这样,字段名按你所用框架的规范调整:
name: zblog-publisher description: 将一篇内容发布到 Z-Blog 站点 inputs: - name: content type: string description: 文章内容,支持 Markdown 或 HTML - name: title type: string required: false - name: category type: string required: false - name: tags type: array required: false描述里的description要写得让助手能判断“什么时候该调用这个技能”。我写的是“当用户要求把内容发布到 Z-Blog 站点时使用”,简单直接,实测触发准确率很高。写得太花哨反而容易误触发。
4.3 核心发布逻辑的实现
下面是发布逻辑的骨架,我做了简化,保留了关键步骤和注释。真实项目里我会把配置、鉴权、请求封装成独立模块,这里为了讲清楚流程放在一起。
import requests import markdown from datetime import datetime # 配置从环境变量读取,不硬编码 API_URL = "https://your-site.example/xmlrpc.php" USERNAME = "your-account" TOKEN = "your-app-token" def normalize_content(raw, title=None, category=None, tags=None): """把各种输入归一化成发布所需的结构""" html = markdown.markdown(raw, extensions=["extra", "codehilite"]) if not title: # 兜底:取正文前二十字 title = raw.strip()[:20] summary = raw.replace("#", "").strip()[:120] return { "title": title, "description": html, "category": category, "tags": tags or [], "summary": summary, } def get_category_id(name): """分类名转 ID,实际项目里应带缓存""" # 调用分类列表接口,查表返回 ... def publish(post): """调用发布接口""" payload = { "title": post["title"], "description": post["description"], "categories": [get_category_id(post["category"])], "mt_keywords": ",".join(post["tags"]), "post_status": "publish", "date_created": datetime.now().isoformat(), } resp = requests.post( API_URL, json={"method": "metaWeblog.newPost", "params": [1, USERNAME, TOKEN, payload, True]}, timeout=30, ) resp.raise_for_status() return resp.json()几个实现细节值得展开。
超时一定要设。我设的是 30 秒,因为带图片上传时请求会变长。不设超时的话,网络一抖动,技能就卡死在那,用户体验极差。
错误处理要分层。网络错误、鉴权错误、参数错误要分开捕获,返回不同的提示。我见过把所有异常都吞成“发布失败”的实现,用户根本不知道是 Token 过期还是分类名写错了。
返回结果要可读。接口返回的通常是文章 ID,技能要把它拼成完整链接再回传,比如https://your-site.example/post/123.html,用户点一下就能验证。
4.4 图片上传的串联
图片处理我单独抽成一个函数,在正文归一化之后、发布之前调用:
import re def upload_images(html, upload_func): """找出本地图片引用,上传后替换为远程地址""" pattern = r'<img[^>]+src="([^"]+)"' def replace(match): src = match.group(1) if src.startswith("http"): return match.group(0) # 已是远程,跳过 remote_url = upload_func(src) return match.group(0).replace(src, remote_url) return re.sub(pattern, replace, html)upload_func负责读本地文件、调上传接口、返回远程 URL。这里我特意做成串行,前面说过并发容易触发限流。如果图片确实多,加个简单的间隔,比如每张之间停 0.5 秒,稳得多。
4.5 端到端跑一遍的实测记录
我用一篇约两千字、带四张图的内容做了一次完整测试。时间分布大致是:内容归一化加 Markdown 转换约 1 秒,四张图串行上传约 8 秒,发布请求约 0.6 秒,总计不到 10 秒。加上我在助手界面里输入指令、确认标签的时间,整体控制在两分钟以内,和标题说的完全对得上。
对比手动操作:同样的内容,我在后台一步步来,计时是 18 分钟出头。差距主要就在图片上传和字段填写上。这个账算下来,一天发三篇,一个月能省下十几个小时。
5. 常见问题与排查技巧实录
5.1 发布成功但前台看不到
这是最高频的问题。接口返回成功,文章 ID 也有了,但前台访问 404 或者列表里没有。排查顺序我总结成一张表:
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 返回成功但 404 | 状态是草稿 | 检查 post_status 的值 |
| 列表不显示 | 分类 ID 错误 | 核对分类映射缓存 |
| 链接打不开 | 别名冲突 | 检查 alias 是否重复 |
| 内容空白 | 正文格式不对 | 确认 HTML 是否被转义 |
我遇到最多的是第一种。不同 Z-Blog 版本对“发布”状态的值定义不一样,有的是字符串publish,有的是数字1。这个只能实测,文档经常滞后。我的做法是第一次接入时,用草稿状态发一篇测试,确认能查到,再改成发布状态。
5.2 中文乱码
中文乱码通常出在两个环节:请求编码和数据库编码。请求侧要确保 Content-Type 带charset=utf-8,正文在发送前是 UTF-8 编码。数据库侧如果站点本身编码不是 UTF-8,那接口传得再对也会乱。这个属于站点配置问题,技能层面解决不了,但可以在技能里加一个“编码自检”,发现异常时提示用户去检查站点配置。
5.3 Token 过期或权限不足
表现是接口返回鉴权失败。排查很简单:拿 Token 单独发一个最简单的请求(比如获取分类列表),能通说明 Token 有效,问题在权限;不通说明 Token 本身有问题。我建议技能里加一个“连接测试”功能,配置完先测一下,别等到发布时才报错。
5.4 标签静默丢失
前面提过,部分版本要求标签必须已存在,否则不报错但也不写入。这个坑很隐蔽,因为接口返回是成功的。我的应对是在发布前先查一次标签列表,把不存在的标签挑出来提示用户,让用户决定是新建还是忽略。多这一步,能避免大量“文章发了但标签没了”的困惑。
5.5 独家避坑心得
说几个文档里不会写、但实际很要命的点。
别在高峰期批量发布。如果你一次要发多篇,别用循环猛发。服务端可能有频率限制,触发后轻则限流重则封 IP。我的做法是每篇之间停几秒,宁可慢一点。
保留发布日志。每次发布记录时间、标题、返回的文章 ID。出问题时这份日志就是你的排查依据。我吃过没日志的亏,文章发重复了都不知道是哪次调用出的问题。
技能要能“只发草稿”。有时候你只是想先存着,回头再检查。技能里留一个“草稿模式”开关,比每次改代码强得多。
配置文件别进版本库。前面说过,这里再强调一次。用.gitignore把配置文件和日志都排除掉,这是基本的安全习惯。
6. 这套技能还能怎么扩展
把发布这条链路跑通之后,能扩展的方向其实不少,我挑几个自己已经在用的说说。
多站点分发。配置文件里维护一个站点列表,技能接收内容后依次发布到多个 Z-Blog 站点。注意每个站点的分类和标签体系可能不同,映射关系要分开维护。我现在管着三个站点,一次指令全部分发,省事很多。
定时发布。接口一般支持传未来的发布时间,技能里加一个“定时”参数,配合系统的定时任务,就能实现“晚上写好,早上自动发”。这个对做内容排期的人特别有用。
发布前的内容检查。在归一化之后、发布之前,插一段检查逻辑:标题长度、摘要是否存在、图片是否都能访问、有没有明显的错别字。检查不通过就拦下来提示,避免把半成品发出去。
回滚能力。发布成功后记录文章 ID,如果发现发错了,技能提供“撤回”功能,调删除接口把文章删掉。这个在批量分发时尤其重要,发错一篇不可怕,发错一批才要命。
我个人在实际操作中的体会是,这类自动化技能的价值不在于“炫技”,而在于把重复劳动彻底消灭掉。你花一个下午把技能搭好,后面每次发布都省十几分钟,一个月下来就是实打实的时间。而且因为流程标准化了,出错率反而比手动操作低——手动操作会累、会走神、会漏填字段,机器不会。
最后分享一个小技巧:技能刚上线时,别急着全自动。先让它“生成发布参数但不真正提交”,你人工核对几次,确认参数都对,再打开真正发布的开关。这个“干跑”阶段能帮你发现绝大多数配置问题,比事后回滚省心得多。