一、背景:为什么我们需要一个「发布分发 Skill」
1.1、场景引入
在维护 BlogLoom 这套开源博客系统的过程中,我逐渐固定了一套「本地 Markdown 创作 → 平台统一管理」的内容工作流:文章先以 Markdown 形式沉淀在本地知识库里,顶部带一段标准元数据,再导入平台。 但很快遇到了一个非常具体的问题:同一篇文章,我往往还需要分发到 CSDN、掘金、公众号等多个站外渠道。 一开始我靠手动搬运:打开编辑器、复制正文、填标题、加标签、选专栏、传封面……一篇文章十分钟起步,批量分发更是机械又容易漏。 于是就有了 BlogLoom Publisher Skill ——把这些「重复点击」沉淀成一个可被 AI Agent 直接调用的技能包。1.2、手动分发的四大痛点
- 耗时:一篇长文光是粘贴、等图片上传、配标签就要数分钟;
- 易错:标签/专栏容易选错,封面忘记设置;
- 不统一:各渠道字段口径不同,靠人脑映射成本高;
- 难复用:每篇文章都要「重新点一遍」,没法沉淀为流程。
1.3、目标与范围
一句话描述目标:把符合统一 SOP 的本地 Markdown 博客,用自然语言即可分发到多个第三方渠道;初次对接渠道为 CSDN。
1.4、项目地址与专栏说明
本篇是 BlogLoom 分发 Skill 专栏 的开篇(BlogLoom分发Skill 专栏第 01 篇),后续该专栏会持续记录多渠道分发的设计与实践。
- BlogLoom 开源仓库:https://github.com/changluya/BlogLoom
- 本 Skill 目录:https://github.com/changluya/BlogLoom/tree/master/skills/blogloom-publisher-skill
skills/blogloom-publisher-skill 目录,与主站(blog-backend / blog-cms-ui / blog-view-ui)同仓维护。克隆后进入该目录即可使用:
二、技术栈选型:为什么是 Playwright + CDP
面对「自动化发布到第三方平台」,常见有三条路。我们逐一对比。2.1、方案一:直接调用平台 HTTP 接口
抓包找到发布接口,直接用代码构造请求。- 优点:快、资源占用低、可无头无浏览器。
- 缺点:接口带签名/加密参数(如 CSDN 的
x-ca-*系列),且随版本频繁变化;一旦改版就要重新逆向。 - 适用:内部可控、接口稳定的系统。
2.2、方案二:Selenium + WebDriver
传统浏览器自动化方案,生态成熟、资料多。- 优点:API 稳定、社区庞大、能复用已有
chromedriver经验。 - 缺点:与浏览器版本强绑定(driver 版本)、等待与稳定性需大量手工封装、对现代前端(异步渲染)不够友好、调试体验一般。
- 适用:老项目、已深度投入 Selenium 的场景。
2.3、方案三:Playwright + CDP(最终选用)
以 Playwright 作为主操作,CDP(Chrome DevTools Protocol)作为底层增强。- Playwright 主操作:自动等待元素可交互、跨浏览器、
locator语义清晰、有头/无头一键切换,尤其适合「模拟真人操作」; - CDP 底层增强:复用已有 Chrome 的登录态(
connectOverCDP)、读取 HttpOnly Cookie 判定登录、Input.insertText作为富文本写入兜底。
2.4、选型结论对比
三、核心设计准则
3.1、三层分离 + 按渠道分文件夹
Skill 内部严格分层,职责单一:selectors.js。站点改版时,只改选择器文件,业务流程代码零改动。
3.2、一个渠道,一个 SOP
参考 BlogLoom 主站的「场景路由」思想:文档不加倍,每个渠道维护一份 SOP 即可。scripts/channels/<name>/ + references/channels/<name>/sop.md + 路由表登记一行。
3.3、前置对齐统一博客 SOP
分发之前,先按标准生成发布输出博客sop.md 校验博客顶部元数据:
- 字段归一:兼容
category/categories、columns/column等写法; - 宽松解析:容忍 SOP 样例里常见的全角冒号、尾随逗号、全角引号,避免因笔误整段元数据丢失。
 显式标记,与 BlogLoom 平台导入规则保持一致。
3.4、模拟真人操作 + 登录态持久化
- 主操作全部走 Playwright:真实点击、输入、粘贴,默认有头浏览器,发布过程肉眼可见;
- 登录用独立的持久化 profile(
~/.blogloom-publisher/chrome-profile-<channel>),扫码一次后续免登录; - 也支持
--cdp连接你日常使用的 Chrome,直接复用现成登录态。
3.5、统一 CLI 契约与机器可读输出
四、能力全景:目前支持什么
4.1、动作矩阵
4.2、有头 / 无头可配置
PUBLISHER_MODE=headed|headless 全局控制。
4.3、主动登录(本地工作台定位)
这个 Skill 的定位是用户本地工作台:需要登录的动作检测到未登录时,会主动拉起浏览器让用户扫码,登录完成后自动继续。
无头模式无法扫码,会自动重启为有头浏览器;CI 场景可用 --no-login 直接报错退出。
五、实战:CSDN 发布链路拆解
5.1、登录态判定
在首页通过登录入口文案判定,再用 Cookie 交叉验证:- 找到
.toolbar-btn-loginfun且文案为「登录」→ 未登录; - 命中「创作」入口且非登录态,或 Cookie 含
UserToken+UserInfo→ 已登录。
5.2、进入编辑器与标题/正文写入
- 首页点「创作」(
.toolbar-btn-write-new)进入editor.csdn.net/md; - 标题默认是展示态(
.article-bar__title-display,显示「【无标题】」),点击后才显示隐藏的input.article-bar__title; - 正文是
<pre class="editor__inner markdown-highlighting" contenteditable>,需先清空默认欢迎内容,再按paste → cdp → keyboard策略粘贴:
5.3、发布弹窗配置
点「发布文章」(button.btn-publish)打开弹窗 .modal__publish-article,在里面配置:
- 标签
.mark_selection - 首图:已有图片列表第一张(
.img-selection-item img.select-cover) - 摘要
.desc-box .el-textarea__inner - 分类专栏(含二级
# xxx) - 文章类型 → 原创;可见范围 → 公开
- 创作声明 → 个人观点,仅供参考
5.4、删除与二次确认
delete 进入内容管理页 https://mp.csdn.net/mp_blog/manage,定位文章行 → 悬停右侧「…」→ 下拉「删除」→ 弹窗「确定」,随后回查该行已消失才算成功。
六、踩坑与优化(重点复盘)
这部分是整篇最真实的部分:每一条都是真机跑出来的坑。6.1、坑一:登录被「假阳性」
最初用 Cookie 里是否有UserToken/UserInfo/uuid_tt_dd/cnt_w 判定登录。结果发现 CSDN 在匿名态也会下发 uuid_tt_dd,导致「未登录」被误判为「已登录」,随后编辑器直接跳到登录页。
- 现象:
checkLogin说已登录,publish打开编辑器却是登录页。 - 优化:只认真正的登录凭证
UserToken/UserInfo,并在编辑器阶段判断是否被重定向到passport.csdn.net/login,是则抛AUTH_REQUIRED。
6.2、坑二:标题输入框是隐藏的
.article-bar__title 直接 waitFor(visible) 永远超时——因为它 display:none,真正显示的是 .article-bar__title-display。
- 优化:先点展示态,唤出隐藏 input,再填入并回读校验:
6.3、坑三:正文默认内容与富文本写入
CSDN 编辑器会预置一段「欢迎使用 Markdown 编辑器」的模板。如果不清空,发布出去的文章会多出无关内容。- 优化:写入前
Cmd/Ctrl + A+Backspace清空;写入采用剪贴板粘贴优先(最贴近真人),失败再用 CDPInput.insertText、最后键盘逐字兜底;写入后回读内容长度校验。
6.4、坑四:首图列表异步加载
发布弹窗里的「已有图片列表」是异步加载的。刚点开弹窗就找.img-selection-item,经常是空的,导致首图没选上。
- 优化:正文含图片时,按图片数量估算粘贴后的等待时间——默认 2s,每满 5 张图再多等 3s,给足图片上传/解析时间;再点发布,首图列表才可用。
6.5、坑五:分类专栏的隐藏复选框与误匹配
两个连环坑:- 勾选无效:分类专栏的勾选项其实是一个隐藏的
<input type="checkbox" class="tag__option-chk">,普通click()打不到,看着「选中了」,实际没生效; - 匹配错误:用「包含」反向匹配时,设置值
Maven&Gradle会被宽松地匹配到子类Maven。
- 优化 1:改为触发 DOM
click()勾选,并先判断checked避免反选,最后回读已选项:
- 优化 2:匹配改为评分制——精确
>已有包含设置值>设置值包含已有,取最高分,实测正确命中二级项# Maven&Gradle。
6.6、坑六:删除「假成功」
delete 第一次跑返回了 DELETED,但文章其实还在。
- 根因:CSDN 的确认弹窗类名是
.el_mcm-message-box(带下划线前缀el_mcm),而我们的选择器写的是.el-message-box,导致「确定」按钮根本没点到。 - 优化:
- 修正确认按钮选择器为
.btn-msg-confirm; - 增加结果校验——点完确认后回查内容管理列表,该行必须消失,否则返回
DELETE_UNVERIFIED,绝不「假成功」。
- 修正确认按钮选择器为
七、验证测试:把不稳定关进笼子
浏览器自动化天然依赖外部页面,怎么保证质量?我们的做法是 mock 单测 + 真实链路自测双层。7.1、mock 单测
实现了一个轻量 Playwright 模拟层(test/helpers/mock-page.js),支持 locator/click/fill/hover/filter/waitFor/evaluate 等子集,无需真实浏览器即可对每个工具做行为验证:
checkLogin / enterEditor / setTitle / setContent / preparePublish / publish / saveDraft / deleteBlog 等每一个脚本工具。
7.2、真实链路自测:test publish
Mock 能测逻辑,但测不出「页面改版」。所以提供一个组合自测:--dry-run 只填写不点发布,用来快速验证字段;test publish 用来验证完整链路且不留残留内容。
八、总结与展望
回顾这次从 0 到 1 的实践,可以浓缩为几条准则:- 背景驱动:先想清楚「是谁、在什么场景、反复做什么」,再动手;
- 技术选型要匹配问题:第三方平台发布,选「模拟真人操作」的 Playwright + CDP,而不是硬刚私有接口;
- 结构决定可维护性:按渠道分文件夹、选择器独立、一渠道一 SOP;
- 一切关键动作都要回读校验:警惕「假成功」;
- 测试双层:mock 单测守逻辑,真实链路自测守改版。
- 增加更多渠道(掘金、公众号等),验证「按渠道分层」的可扩展性;
- 支持封面图自动上传(远程
coverImg→ 下载 → 上传); - 为 Agent 提供更丰富的自然语言路由示例,让「一句话分发」更顺滑。
如果你也在做内容多渠道分发,希望这套「Playwright 主操作 + CDP 底层增强」的思路能给你一点参考。 项目地址:https://github.com/changluya/BlogLoom | Skill 目录:https://github.com/changluya/BlogLoom/tree/master/skills/blogloom-publisher-skill
整理者:长路 时间:2026.9.29