问题与动机
我需要一个能把同一篇文章批量发到 10 个技术平台的工具:掘金、知乎、CSDN、B站专栏、开源中国、思否、头条号、博客园,外加两个 MetaWeblog 协议平台(自建 WordPress/Typecho、51CTO)。手动复制粘贴一遍就要半小时,改个标题得登 10 个后台,出错率还高。
试过 RSS 同步和 Zapier,前者覆盖不了 10 家,后者在登录态和富文本编辑器面前直接崩。最后只能自己写:Python + FastAPI + patchright(Playwright 的社区兼容 fork,在 Windows + 国内网络环境下用于浏览器自动化)+ Vue3 前端 + SQLite 队列。核心诉求不是“能发”,而是稳定——平台改版只改适配器,不重构主流程;登录一次长期复用;AI 生成的内容必须过门禁才放行。
架构:四层隔离,故障不扩散
整套系统分四层,每层职责单一:
- 适配器层:每个平台一个适配器,声明
needs_browser、login_url、home_url、key_selectors、selector_version,以及check_auth/whoami/publish/update四个方法。平台改版时只改这个文件,上层任务调度、门禁、队列完全不动。 - 浏览器层:patchright 驱动持久化 profile 目录。本文用的浏览器自动化库是 Playwright 的社区兼容 fork(包名 patchright),下文提到「Playwright」时指其 API 形态,实际调用的是 patchright;选它是因为在 Windows + 国内网络环境下对已装浏览器兼容性更好,与对抗性反检测无关。登录一次后 cookie 落在磁盘,后续任务复用。坑:profile 目录被占用会直接抛
ProcessSingleton错误,所以登录前必须先释放池内所有实例,否则新起浏览器就是报错。 - 门禁层:四道闸门——高危词安全扫描 → AIGC 显式标识注入 → 双模型二审(配了
REVIEW_MODEL走 LLM,没配降级本地启发式)→ 发布闸门(AI 源文章默认强制草稿人审,可用配置开关放开直发)。 - 任务层:SQLite 队列 + 任务表,支持发布/更新/抓取入库,失败留
last_error字段,方便回看。
这个分层不是为了好看。实际跑下来,掘金和知乎的 DOM 改动频率不同,CSDN 的编辑器是富文本而博客园是纯 Markdown,适配器隔离后,改一个平台不影响其他 9 个的回归测试。
踩坑实录:8 个真实故障
1. 掘金扫码成功但登录态丢失
现象:扫码登录成功,界面正常,但下次启动浏览器又要求重新扫码。
排查:cookie 没落盘。Chromium 关窗前的 cookie 刷盘时序不稳定,正常退出能写,异常退出(比如超时 kill)就丢。
修法:Python 侧在关窗前主动调 export_auth() 写 storage_state 快照,并保留关窗前刷新快照的逻辑。不依赖 Chromium 自己的 flush。
2. 博客园 MetaWeblog 发布 401
现象:调 blogger.getUsersBlogs 返回 401,文案是「请配置正确的用户名与访问令牌,密码登录已取消」。
排查:博客园是 MetaWeblog XML-RPC 协议,平台 XML-RPC 接口返回的错误文案明确写着『密码登录已取消,请在密码框中输入访问令牌』(2026-09 实测)。令牌需要在 设置→其他设置 里单独创建、默认不存在。页面上要点「创建令牌」才有。我曾把页面标签词 "MetaWeblog" 误当成登录名写进配置,排查方法是直接调 blogger.getUsersBlogs("", username, token) 看返回的 Fault 文案。
修法:配置里明确区分 meta_weblog_username 和 meta_weblog_token,文档里写清令牌生成路径。
3. Edge 自动化登录无反应
现象:用系统 Edge 自动化登录博客园,点「登录」按钮毫无反应,页面卡死在验证码区域。
排查:挂 console 监听,抓到一条警告:Tracking Prevention blocked。Edge 的跟踪防护拦截了 o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js 的存储访问,验证码组件永远不初始化。
根因:脏的是旧 profile 累积的拦截状态,新开的 profile 反而干净。
修法:登录前清理或新建 profile,或者在 Edge 里把该域名加跟踪防护白名单。
4. headless 与有头行为不一致
现象:有头模式下 Angular SPA 渲染正常,同一 profile 在 headless 下整页空白,0 console 输出。
排查:headless 模式下 JS 执行时序不同,Angular 的 zone.js 在 headless 里没正常初始化。
修法:改用无头模式 + 真实鼠标坐标点击(mouse.click(x, y)),比 locator.click() 稳定。不追求有头模式,统一无头。
5. 合成点击不触发 Angular 事件
现象:locator.click(force=True) 点在 <a> 上,Angular 的 (click) 绑定收不到,行内 HTML 完全不变。
排查:Playwright 的 click 是合成事件,Angular 的事件委托在某些版本下不响应合成事件的 isTrusted 标志。
修法:改成在元素上 evaluate('el => el.click()') 派发原生事件,Angular 能正常接收。
6. URL 跳转判断登录状态误判
现象:未登录时 i.cnblogs.com 会先跳到 account.cnblogs.com/signin 再被弹回,URL 一度含目标域名,导致 check_auth 误判为已登录。
修法:不用 URL 判断。改用浏览器上下文的 cookie jar 发独立 HTTP 请求探测后端接口,看返回码和 body 里的用户信息。
7. 平台选择器是硬契约
现象:e2e 脚本依赖 .vs-btn、.md-editor[contenteditable=true]、.el-table__row:visible、.acct-card、.el-tabs__item、header.hdr 等类名。改样式可以,改类名会静默打断所有自动化测试。
修法:适配器的 key_selectors 里写死这些选择器,并在 CI 里加一步选择器回归测试。类名变更前跑一遍 e2e,挂了立刻发现。
8. 顶栏在小屏被挤爆
现象:390px 视口下「新建」按钮越出视口 15px,CJK 文本被逐字竖排。
排查:根因是没有 white-space:nowrap、顶栏没有 overflow 兜底、全站只有一处 560px 媒体查询。
修法:给标签文字加 nowrap、≤767px 隐藏品牌文字、给顶栏加 overflow:hidden 兜底,并补一个 390/768/1024 三视口的回归测试,断言控件包围盒在视口内且文本单行。
合规边界
使用浏览器自动化发布内容前,先确认以下四条约束,这是系统内置的红线,不是可选配置:
- 只自动化自己账号下的内容:工具仅操作登录态对应的个人账号,不代管他人账号,不批量注册账号。
- 遵守各平台服务条款与 robots/频率限制:适配器层内置发布间隔与并发上限,不绕过平台频率限制;不采集其他用户数据。
- 验证码一律走人工接管(handoff)不自动绕过:遇到滑块、图片、短信验证码时暂停任务,提示人工介入,脚本不尝试解析或伪造验证码。
- 不做批量灌水、不伪造互动数据:工具仅用于发布原创或授权转载内容,不提供点赞、评论、阅读量等数据伪装功能。
配置与代码片段
以下三个代码片段来自实际运行环境,可直接复制使用。
config.json 的 AI/门禁配置片段
config.json 中门禁与 AI 相关配置如下,REVIEW_MODEL 为空时降级本地启发式,ALLOW_AI_DIRECT_PUBLISH 默认 false:
{
"ai": {
"REVIEW_MODEL": "",
"ALLOW_AI_DIRECT_PUBLISH": false,
"aigc_footer": "本文由 AI 辅助生成,内容基于实测经验整理,不构成对任何平台自动化行为的完整描述。",
"safety_scan": {
"enabled": true,
"blocked_terms_file": "data/blocked_terms.txt"
}
}
}
CLI 发稿命令
项目根目录下执行,--platforms 指定目标平台,--draft 强制走草稿人审,--dry-run 只跑门禁不实际发布:
python -m cli publish \
--file "blog/cnblogs-xmlrpc.md" \
--platforms cnblogs,wordpress,self-hosted-typecho \
--draft \
--dry-run
Python 调 MetaWeblog 自验片段
发布前用 blogger.getUsersBlogs 验访问令牌是否有效,避免 401 进队列后才发现:
import xmlrpc.client
METAWEBLOG_URL = "https://blog园.example/xmlrpc" # 替换为实际 MetaWeblog 端点
USERNAME = "your_username"
TOKEN = "your_access_token" # 在平台 设置→其他设置 中创建
client = xmlrpc.client.ServerProxy(METAWEBLOG_URL, allow_none=True)
try:
blogs = client.weblog.getUsersBlogs("", USERNAME, TOKEN)
print(f"登录成功,账号下有 {len(blogs)} 个博客:")
for b in blogs:
print(f" - {b.get('htmlUrl', 'unknown')}")
except xmlrpc.client.Fault as e:
print(f"认证失败 (Fault): code={e.code}, message={e.value}")
raise SystemExit(1)
AI 工程纪律:门禁不是摆设
写码前先做 Pattern Mining 读邻居代码,再写 RED 测试锁行为,然后 GREEN,最后五轴自审;台账记录 file:line 证据、预测反对意见与缓解措施,机器校验通过才提交。
AI 产出必须过门禁才能发布:安全扫描 → 二审 → AIGC 显式标识。改写和润色会整篇覆盖正文,所以每次覆盖后必须补回 AIGC 文末声明(幂等打标)。写作风格不是每次临时描述,而是落成程序内的风格档案文件,每次写稿/改写/润色都注入,改文件即改全平台产出风格。
这套纪律的核心是:AI 生成的内容不直接进生产环境。默认走草稿人审,只有配置开关打开后才允许直发。出问题的概率低到可以接受,但一旦出问题,last_error 和门禁日志能定位到是哪道闸拦的。
取舍:什么时候不该用这套
- 平台数量 < 3:手动发比维护自动化便宜。
- 内容更新频率 < 1 篇/周:登录态复用和队列系统的复杂度收益不大。
- 平台 API 开放且稳定:直接调 API 比浏览器自动化可靠得多。浏览器方案是最后的手段。
- 需要高并发:SQLite 队列 + 单 patchright 实例的吞吐上限明显,几百篇并发会排队。
检查清单
- 每个适配器的
key_selectors是否有 e2e 回归测试覆盖 - 登录态 snapshot 是否在浏览器关窗前主动写出
- MetaWeblog 平台是否使用访问令牌而非密码
- Edge profile 是否清理或新建,避免跟踪防护累积
- headless 模式下是否用真实鼠标坐标而非合成点击
- 登录状态判断是否走 cookie jar HTTP 探测而非 URL
- 选择器类名变更前是否跑过 e2e
- 小屏视口下顶栏是否有 overflow 兜底和 nowrap
- 发布前是否跑过
--dry-run验证门禁全链路 - 配置中
ALLOW_AI_DIRECT_PUBLISH是否保持false直到人工确认
本文由 AI 辅助生成,内容基于实测经验整理,不构成对任何平台自动化行为的完整描述。