给 AI 助手装上博客园发布能力:cnblogs-publish Skill 安装与踩坑实录
起因
日常在本地写 Markdown,发到博客园要经过「复制 → 粘进编辑器 → 传图 → 选分类」一整套动作。既然博客园一直保留着 MetaWeblog 接口(XML-RPC),这件事就可以交给 AI 助手:一句话,文章就上去了。
于是装一个现成的 Skill 来解决。这篇文章把安装、配置和踩到的坑记下来。
它是什么
cnblogs-publish 是博客园官方仓库 cnblogs/cnblogs-skills 里的一个 Agent Skill。结构很朴素:
cnblogs-publish/
├── SKILL.md # 技能说明,agent 靠它决定何时调用
├── scripts/cnblogs.py # 主脚本,直连 rpc.cnblogs.com/metaweblog/<博客名>
├── scripts/dialogs.py # 原生输入对话框
├── scripts/secretstore.py # 系统凭据库读写
├── scripts/md2html.py # 内置简易 Markdown 转 HTML
├── references/ # MetaWeblog API 备忘、安全模型
└── tests/
它的能力是:把本地 Markdown 一键发布,标题、frontmatter、分类、标签、本地图片上传全自动处理。
安装
把目录放到 agent 的 skills 根目录即可,无需编译:
git clone --depth 1 https://github.com/cnblogs/cnblogs-skills.git /tmp/cnblogs-skills
cp -r /tmp/cnblogs-skills/cnblogs-publish ~/.dsh/skills/
放好之后,status 子命令能自检环境和凭据状态:
python scripts/cnblogs.py status --json
返回里几个字段值得留意:credential_backend 说明凭据存在哪(本机是 windows-credential-manager),credentials_present 表示有没有配过。
凭据设计:这一块做得比多数同类工具都干净
发布类工具最大的隐患是令牌怎么存。这个 Skill 的处理方式值得单独说:
- 绝不让用户在聊天里提供凭据。用户名、博客名、令牌全部通过操作系统原生对话框收集,输入内容不经过对话上下文。
- 凭据只进系统凭据库:Windows 凭据管理器(DPAPI 保护)、macOS 钥匙串、Linux Secret Service。配置文件
config.json里只有排版偏好,一个身份字段都没有。 - 凭据只在脚本进程内展开,不写入文件、不进命令行参数、不进环境变量,也不打印。
- 输出统一脱敏:错误信息里如果混进了博客 URL,会被替换成
***。
所以 setup 会连弹三个输入框,依次填登录用户名、博客名、访问令牌。令牌在博客园后台「设置 → 开放 API / 服务访问」开启后生成。
踩坑:一个 HTTP 500 三种含义
第一次配置,凭据成功写进了凭据管理器,但紧接着鉴权失败:
{"ok": false, "error": "服务端返回异常:HTTP 500 Internal Server Error", "code": 3}
错误文本到此为止,没有任何有用信息。而且容易误导——看到 500 第一反应是「服务端挂了」或「令牌错了」,但这两者都不是。
排查的关键是别猜,去做对照实验。接口是 https://rpc.cnblogs.com/metaweblog/<博客名>,博客名直接体现在 URL 路径里,所以可以构造三组请求来切分假设(都用假凭据,不涉及真实令牌)。
结果非常清晰:
| 请求 | 服务端返回 | 说明 |
|---|---|---|
| 真实博客名 + 假凭据 | Fault 401「请配置正确的用户名与访问令牌」 |
博客名解析成功,进到了鉴权环节 |
| 不存在的博客名 + 假凭据 | HTTP 500 Internal Server Error |
博客名没解析到,鉴权之前就崩了 |
| 空 XML-RPC 请求体 | HTTP 500(任何博客名都一样) |
⚠️ 所以单看 500 并不能区分,必须用格式完整的请求做对照 |
结论:HTTP 500 在这里的真实含义是「博客名在 rpc 端找不到」,而不是令牌问题。如果是用户名或令牌不对,服务端会规规矩矩返回 401 的 XML-RPC Fault。
那博客名为什么会错?因为 setup 的第 2 个输入框会把用户名预填为默认值——看到框里有内容就直接回车,用户名就被当成博客名存下来了。而我当时确实这么做了。
重新运行 setup --reconfigure,把博客名改成博客首页地址 www.cnblogs.com/ 后面那一段(不是昵称、不是标题、不是完整网址),马上通过:
{"ok": true, "action": "setup", "credentials_saved": true, "auth_ok": true, "blog_count": 1}
再跑一次 check,blog_count: 1,分类接口能正常拉回 101 个分类。链路打通。
日常用法
# 默认非公开,存草稿/私有,不进入审核
python scripts/cnblogs.py publish 文章.md --json
# 显式公开发布,可能进入人工审核
python scripts/cnblogs.py publish 文章.md --publish --json
# 更新(默认同样非公开;已公开的文章要更新必须再带 --publish)
python scripts/cnblogs.py update <postid> 文章.md --json
# 其他
python scripts/cnblogs.py list --count 10 --json # 最近文章
python scripts/cnblogs.py categories --json # 分类
python scripts/cnblogs.py upload a.png --json # 上传图片
几个容易忽略的细节:
- 默认非公开,这是个好默认值——草稿不进入审核,可立即安全保存;只有显式加
--publish才公开发布。 - Markdown 模式会自动附加特殊分类
[Markdown],这是博客园按 Markdown 渲染的开关,别删。 - 公开发布后如果首页没有、文章页 404,不要重发。那是人工审核期,重复发布只会产生重复文章;用
get <postid>确认 API 侧记录即可。 - 脚本默认会在正文顶部加一条可见的「AI 生成声明」,可用
--no-ai-generated关闭。
小结
三件事值得记下来:
- 错误码的语义比错误文本更重要。同一个 500 可能是「路由没找到」「请求体坏了」「服务内部异常」,光看状态码永远在猜。
- 用已知正确的对照组切分假设。拿两个真实博客名和一个不存在的博客名各打一次,问题立刻收窄到「博客名解析」这一层。
- 凭据交给操作系统。Agent 需要的是「能调用发布动作」,不是「知道令牌是什么」。原生对话框 + 系统凭据库这个组合,让两边都安全。
装好之后,写文章和发布之间的最后一段手工步骤就消失了。

浙公网安备 33010602011771号