site logo

Marico's space

LinkedIn悄然从ProseMirror迁移到Quill——导致所有使用Composer的浏览器自动化工具全部失效

AI技术与应用 2026-05-03 21:11:51 13

最近帮朋友排查一个自动化工具的问题,本来以为只是个例,结果顺藤摸瓜发现 LinkedIn 偷偷换掉了整个编辑器底层框架。这个故事挺有意思的,不光是对搞自动化的人有用,也让我重新思考了「平台依赖」这件事。

原文章讲的是 LinkedIn 从 ProseMirror 迁移到 Quill 导致自动化工具失效。技术细节很扎实,值得一读。

问题爆发

上周我刚给 MCP 服务修了 LinkedIn 的 ProseMirror composer 相关的问题,效果挺好。结果两天之后,所有 LinkedIn 帖子自动化的功能全部挂掉了。

这是事后复盘,换了什么、怎么发现的,以及为什么「自动化平台」的故事几乎总是这样收场。

崩溃现场

症状非常明确。MCP 服务的 safari_fill 工具——通过遍历 React Fiber 调用 editor.commands.setContent(html) 来填充 ProseMirror——现在一碰到 contenteditable 就崩溃,helper daemon 直接挂掉,composer 对话框也消失了。

URL 没变,表面看 DOM 结构也没变, selectors 也没换,但底层编辑器已经不是同一个了。

DOM 不会说谎

我打开浏览器控制台跑了几个常规探测:

const el = document.querySelector('[contenteditable="true"]');
el.editor // -> undefined
el.closest('.ProseMirror') // -> null
el.closest('.ql-editor') // -> <div class="ql-editor">

真相就在这。.ql-editor 是 Quill 的标志性类名。LinkedIn 在 2026 年初某个时间点把帖子编辑器从 ProseMirror 换成了 Quill,我翻遍了也没找到任何公告。

为什么会崩溃

Quill 和 ProseMirror 一样,不允许你「直接」往 contenteditable 里塞文本。两个编辑器都维护一个内部模型—— Quill 叫它 Delta——DOM 只是这个模型的下游。

如果你绕过模型直接写 DOM,会发生两件事:

  1. 模型和 DOM 对不上。
  2. 下一个用户触发的操作(按键、保存)会触发重渲染,因为 diff 对不上直接报错。

这就是 composer 挂掉的原因。我的填充脚本往 innerText 里写了内容,Delta 状态以为编辑器还是空的,React 树试图调和,直接崩了。Swift daemon 收到级联异常,干脆自己也崩了。

修复方案:按 Quill 期望的方式驱动它

Quill 提供了程序化 API,只是需要先拿到实例的引用。我摸索出的查找顺序:

  1. 向上查找祖先节点,找到 class 为 .ql-container 的元素。
  2. 尝试 .__quill——Quill 2.x 会把实例直接挂在这个属性上。
  3. 回退到 React Fiber:沿着 fiber 链向上找 memoizedProps.quillstateNode.quill(LinkedIn 把 Quill 包装在 React 组件里,实例放在 props 中)。
  4. 如果还是找不到,回退到真正的 CGEvent Cmd+V 粘贴——Quill 会响应 clipboard 事件,且 isTrusted: true 的优先级最高。

拿到实例之后,实际的填充就一行代码:

quill.setContents([{ insert: text + '\n' }], 'api');

'api' 这个 source 参数是关键。它告诉 Quill:「这是通过你自己的 API 进来的,模型和 DOM 一起更新。」文本提交成功,Delta 保持一致,React 父组件也不会试图和一个损坏的模型做调和。

关于平台自动化学到的

两个教训,都很老,但值得反复记住:

富文本编辑器不是稳定的接口。 ProseMirror 和 Quill 有不同的 API、不同的状态模型、不同的「什么算真正的编辑」规则。只针对其中一个写代码,平台随时可能换掉它。LinkedIn 悄无声息地做了这次切换,我之所以知道,只是因为我的代码崩了。

DOM 是最低公倍数,编辑器模型才是真正的那层。 所有在 contenteditable 上合成事件的自动化工具,实际都在真相的下一层操作。有时候行得通(编辑器做了调和);有时候行不通(编辑器崩溃或者悄悄丢弃输入)。稳妥的做法永远是找到编辑器实例,调用它的 API。

还有第三个教训,更让人不舒服:我没法在 LinkedIn 上完整验证我的修复,因为 LinkedIn 在无头环境下的模态框打开行为本身就有问题。composer 按钮接受了点击,模态框的 DOM 也出现了,但就是没有视觉呈现。所以 Quill 检测逻辑是上了——也在测试页上验证过——但 LinkedIn 特定的实机路径还卡在一个我还没解决的模态框问题上。

这就是平台自动化的质感。两个毫不相关的 bug,同一周,同一个目标。每一个看起来都像另一个。修了一个,另一个伪装成回归。

总结

如果你在构建任何往第三方富文本编辑器里打字的东西——钉钉、飞书、Discord、知乎、富文本编辑器——编辑器的身份是你和平台之间的契约的一部分,而平台不欠你这个契约的稳定性。在运行时检测编辑器类型。对未知情况留有回退(真实 clipboard 事件最好不过)。记录检测结果,这样当它变化时,你从自己的遥测数据知道,而不是半夜收到消息才知道。

动手之前先读一下 contenteditable 的 class 列表。ProseMirror 和 Quill 有不同的签名特征,DOM 会告诉你你在处理什么——只要你去看。

修复已发布于 safari-mcp@2.10.2。源码在 GitHub

原文链接:https://dev.to/achiya-automation/linkedin-quietly-migrated-from-prosemirror-to-quill-and-broke-every-browser-automation-tool-that-4927