把发博客做成 RPA:一次 Qoder Skill 的设计、踩坑与落地
一、起因:每次发文,都在重复同一套动作
登录后台、打开写文章页、填标题、选分类、粘贴正文、存草稿、确认、发布——这套动作我重复了很多次。每次让 AI 帮忙发,它都要“看”页面、“点”按钮,慢,而且消耗 token。
于是有了一个很自然的目标:把这套流程固化成一个可复用的 Qoder Skill,最好能像 RPA 一样,一条命令跑完。
这篇文章记录整个过程:Skill 的原理、第一版怎么做、验证码怎么挡住去路、最后如何用 Playwright 持久登录加本地脚本把流程跑通。
二、先弄明白:Qoder Skill 到底是什么
Skill 不是一段提示词,而是一个“给 AI 的工作手册加工具箱”。
一个 Skill 就是一个目录,核心是一个 SKILL.md:
---
name: emlog-publish
description: 把对话中的 Markdown 内容发布到 emlog 博客,按指定分类保存草稿,确认后发布。当用户说“把以上内容发布到我的博客”时使用。
---
name:技能名,只能用小写字母、数字和连字符description:决定“什么时候自动加载这个技能”,要写清楚做什么、什么场景触发
SKILL.md 的正文不会一直占用上下文,而是分三层加载:
- 元数据(name 加 description):常驻,决定是否触发
- 正文(SKILL.md):触发后才加载
- 附带资源(scripts、references、assets):需要时才读取或执行
这就是所谓的“渐进式披露”:平时只带一张名片,真要用时才翻开手册。
Skill 还可以带三类资源:
scripts/:可执行脚本,适合确定性、重复性的操作references/:参考资料,按需加载assets/:输出要用的模板、图片等
安装位置分两种:项目级 .qoder/skills/,用户级 ~/.qoder-cn/skills/。这次做的是用户级,所有项目都能用。
三、第一版:让 AI 用浏览器帮我发文
动手前先把需求问清楚,就四件事:
- 目标:把指定内容发布到我的博客
- 触发:我说“把以上内容发布到我的博客”
- 输入输出:输入 Markdown 加分类,输出文章链接
- 边界:不碰密码、不改设置、未经确认不发布
第一版很直接:用浏览器自动化登录后台、打开写文章页、填标题、选分类、粘贴正文、存草稿,确认后再点发布。分类只从后台现有分类里选,找不到就停下来问,不猜。
草稿优先、确认后发布的规则,也是在这一版定下来的。
四、拦路虎:图形验证码
真正跑的时候,问题来了:后台登录有图形验证码。
我先试了纯 HTTP 登录——拿账号密码直接 POST。结果服务端返回“验证码错误”,密码还没轮到被校验。又去试 emlog 传统的离线写作接口,xmlrpc.php 和 rsd.php 都是 404。
摆在面前的路只有几条:
- 关掉登录验证码——最省事,但等于拆掉一层防护
- 手动导出 Cookie——不改设置,但会话过期就要重新导出
- 用 Playwright 持久登录——保留验证码,首次人工过一次,之后复用登录状态
最后选了第三条。理由很简单:不绕过安全机制,也不牺牲自动化。
五、侦察:把后台表单摸清楚
Playwright 首次登录时,脚本预填账号密码,人只需要输入验证码。登录成功后会保存一个独立的浏览器配置目录,之后所有自动化都复用这个会话。
接下来是只读侦察:打开写文章页,把表单结构导出来看。关键发现有几个:
- 标题输入框:
input[name="title"] - 分类不是复选框,是隐藏的
select[name="sort"],选项自带分类 ID - 编辑器是 editormd,底层就是 Markdown
- “保存草稿”按钮
#savedf,内部 POST 到article_save.php?action=autosave - “立即发布”按钮
#pubPost,提交整个表单
最惊喜的是第三点:编辑器原生支持 Markdown。这意味着脚本不需要把 Markdown 转成 HTML,直接把原文写进编辑器即可,格式保真度最高。
六、把流程固化成脚本
有了这些信息,就可以写确定性脚本了。核心只有几步:
// 复用持久化登录状态
const context = await chromium.launchPersistentContext(profileDir, {
executablePath: edgePath,
headless: true,
});
// 打开写文章页,等编辑器就绪
await page.goto(adminUrl + '/article.php?action=write');
await page.waitForFunction(() => window.Editor && document.querySelector('.CodeMirror'));
// 填标题
await page.fill('#title', title);
// 写正文:同时同步 CodeMirror 和表单 textarea
await page.evaluate((md) => {
document.querySelector('.CodeMirror').CodeMirror.setValue(md);
document.querySelector('textarea[name="logcontent"]').value = md;
}, markdown);
// 选分类:设置隐藏 select 的值
await page.evaluate((category) => {
const sel = document.querySelector('select[name="sort"]');
const opt = [...sel.options].find(o => o.textContent.trim() === category);
sel.value = opt.value;
}, category);
// 存草稿(内部就是 autosave 接口)
await page.click('#savedf');
// 确认后发布
await page.click('#pubPost');
外面再包一层命令行:
emlog-publish.cmd --file post.md --category "开发相关"
emlog-publish.cmd --publish-id 67
第一条返回草稿 ID 和链接,第二条把草稿发布。分类不存在时脚本会列出所有可选分类并退出,不瞎猜。
七、安全怎么保证
自动化最怕把凭据写死在代码里,这次特意做了几条约束:
- 密码只从环境变量读取,不写进脚本、配置或日志
- 验证码永远不自动识别,需要时停下来人工完成
- 登录态存在独立的浏览器配置目录里,定期可以清理
- 未经确认不发布;不删除、不修改已有文章
会话失效时,跑一次 --login,浏览器会弹出并预填账号密码,人只需要输验证码。其余时间,脚本完全自动。
八、实测:从草稿到发布,再到删除
写完后做了一次完整闭环测试:
- 生成一篇测试草稿,回读编辑页,确认标题、分类、Markdown 原文、草稿状态全部正确
- 实际发布,打开公开页,确认标题、二级标题、列表、代码块都正常渲染
- 从文章列表删除测试文章,确认公开链接回落到首页
测试数据没有留在博客里。整个流程除了首次登录的验证码,没有任何人工步骤。
九、经验总结
做完这件事,有几个感受:
Skill 的价值不在“会聊天”,而在“能沉淀流程”。 把一次成功的操作写成 SKILL.md 加脚本,下次就不用重新摸索。
确定性的部分交给脚本,不确定的部分留给模型。 登录、填表、提交这些动作,脚本比模型快且稳;遇到页面改版、结构不认识时,再让模型用浏览器兜底。
先跑通,再优化。 第一版用浏览器也能发,但慢且费 token;跑通之后才知道瓶颈在哪,值不值得固化成脚本。
验证要闭环。 不只是“接口返回成功”,还要回读草稿、检查公开页、清理测试数据。否则所谓的成功可能只是自我感觉良好。
十、下一步
这套方案目前还有两个边界:本地图片需要先上传到媒体库,脚本暂不处理;如果 emlog 升级改了表单结构,选择器要跟着更新。
但主线已经跑通了:一篇文章从 Markdown 到草稿、再到发布,一条命令完成。对个人博客来说,这已经足够把重复劳动交给机器。
而你现在看到的这篇文章,就是由这套流程发布出去的。