Obsidian + Claude Code 搭自动化发布客户端:Playwright × 掘金/知乎/CSDN 的全链路踩坑实录
AI工具人PM 的实战笔记:前几篇讲了掘金、知乎各自的自动化踩坑。这三篇合起来——我用 Obsidian + Playwright + Claude Code + Electron,从源码文件到三个平台一键发布,搭了一整套系统。这不是教程,是我的真实踩坑记录。
起因:三个平台的编辑器让我疯掉了
掘金是 CKEditor(直接往 DOM 里注入 HTML),知乎是 Draft.js(基于 React 的富文本编辑器,数据存在 Immutable.js 结构里,DOM 上看不到正文内容),CSDN 又是另一套编辑模式。
每次手动发一篇文章要开三个浏览器窗口,切换七八次——标题复制一次,内容复制三次(格式还不完全一样),标签一个个手选。
如果一天发一篇,一个月就是三十次这样的循环。但真正要命的是:
手动发的时候你不知道失败原因是什么。页面卡了?Cookie 过期了?网络超时了?你只能盯着转圈的那个按钮,不知道问题出在哪。
自动化是出路。但问题在于:怎么确保你的自动化真的跑通了?
架构:Obsidian 写 → Python 发 → Electron 看
┌─────────────────────────────────────────────────┐ │ Obsidian (Markdown 源文件) │ │ └── 待发布/ ← frontmatter: title, url, status │ │ └── 已发布/ ← 各平台归档 │ ├─────────────────────────────────────────────────┤ │ Python 后端 (vault-scripts/) │ │ ├── daily_publish.py ← 调度 + CLI 入口 │ │ ├── juejin.py ← 掘金 Playwright 引擎 │ │ ├── zhihu.py ← 知乎 Playwright 引擎 │ │ └── csdn.py ← CSDN Playwright 引擎 │ ├─────────────────────────────────────────────────┤ │ Electron 客户端 (pubhub) │ │ ├── main.js ← 主进程 + IPC handler │ │ ├── preload.js ← 安全桥接 │ │ ├── renderer/ ← Vue.js 前端 │ │ │ ├── index.html ← 表格列表 + 详情弹窗 │ │ │ ├── app.js ← 发布状态管理 │ │ │ └── style.css ← 暗色侧边栏 + 进度面板 │ │ └── data/ ← article_cache.json │ └─────────────────────────────────────────────────┘核心设计原则: -Obsidian 是唯一的源。不用维护多份内容,frontmatter 字段统一管理 URL 和状态 -Python 负责重活。Playwright 控制 Chromium,模拟人在浏览器里的每一步操作 -Electron 提供反馈。CLI 脚本跑完后你知道发了吗——前端可视化看到每个平台的具体状态
IPC:主进程和渲染进程的对话
Electron 的安全模型规定渲染进程不能直接调用 Node API。所以需要一个中间层:
// preload.js — 安全的桥接 contextBridge.exposeInMainWorld('electronAPI', { scanArticles: () => ipcRenderer.invoke('scan-articles'), publishArticle: (title, platform) => ipcRenderer.invoke('publish-article', title, platform) }); // main.js — 实际执行 ipcMain.handle('publish-article', async (event, articleTitle, platform) => { const pythonScript = path.join(obsidianRoot, 'vault-scripts/daily_publish.py'); execFile('python', [pythonScript, '--once', '--title', articleTitle, '--platform', platform], ...); // stdout/stderr 解析为 JSON 返回给前端 });关键点: -nodeIntegration: false+contextIsolation: true— 渲染进程沙箱化 ---title+--platform— 精准路由到目标文章和目标平台,不走全量队列 - 正则解析 stdout — Python 输出终端日志 → 前端能解析每个平台的状态
坑一:logger is not defined
CSDN 发布脚本的某一行用了logger.log(25, ...)打了一条 debug 日志,但文件头部没 import logging。结果整篇文章的发布流程中断,stderr 被吞掉,前端收到的错误信息是空白。
修复方式就是在文件开头加上:
import logging logger = logging.getLogger("csdn_publisher")这个小 bug 卡了我一个小时——因为错误不在 daily_publish.py,不在 main.js,而在 csdn.py 内部。调试的时候需要一层层追 traceback。
坑二:Playwright inner_text(timeout=N) 新版已废弃
旧代码里写了opt.inner_text(timeout=100).strip()。在 Playwright 1.40+ 版本中,inner_text()不再接受 timeout 参数——这个参数在更早的版本已经移除了,只是之前的版本没有报错。
去掉 timeout 参数后,所有平台的自动化才真正跑通。这提醒我们:自动化脚本的生命周期比你想的长,依赖库更新时它也会跟着变老。
坑三:数据去重合并
同一个文章可能同时出现在待发布/和已发布/(比如刚发布还没从待发布目录移走)。扫描时需要按标题 key 去重并合并平台信息:
pending: [{zhihu: success}, {juejin: pending}] archive/csdn/: [{csdn: success}] 合并后: [{zhihu: success}, {juejin: pending}, {csdn: success}]如果不去重,同一篇文章会在缓存里出现两条记录,前端显示的统计数据就会翻倍。
坑四:文件删除顺序
最危险的一步:发布成功后要删除待发布目录中的原文件。但如果删除过早(比如先删源文件再归档),一旦归档步骤失败,文章就永久丢失了。
正确的顺序: 1. 更新 frontmatter,写入各平台 URL 2. 拷贝到已发布目录(备份) 3. 确认拷贝成功 4. 才删除待发布目录中的文件
效果对比
| 维度 | 手动 | 客户端 |
|---|---|---|
| 单篇文章发布 | ~15 分钟,三平台来回切换 | ~10 秒自动完成 |
| 状态追踪 | 打开三个网站逐个确认 | 一行表格一目了然 |
| 失败排查 | 不确定哪个环节出问题 | 逐平台显示具体错误原因 |
| 重试操作 | 从头再来一遍 | 失败按钮可直接点重试 |
| 历史记录 | 没有 | 每篇文章的前后状态可追溯 |
总结:为什么一定要做这个客户端?
手动自动化都好用,但自动化的价值不在于快,而在于可见。
一个 Python 脚本就能搞定批量发布——我早就有了。但它的问题是:脚本跑完后你不知道结果。stdout 是文本,你需要 grep 才能看懂。
加了一个 Electron 客户端,把每个平台的发布状态、URL、时间戳、错误信息全部可视化呈现出来。这才是完整的闭环:
写 → 自动化发 → 看到结果 → 有问题可以重试 → 下次不再踩同样的坑这个流程不是"省了多少分钟"的问题,而是让整个发布过程变成了可观测、可控制的系统。
对产品经理来说,这是一个把模糊过程变成清晰数据的典型案例——不是所有的东西都需要 GUI,但当你能看到每个环节的真实状态时,决策的质量会显著提升。