CDP 自动化网页编辑器:三个静默失败的坑与回读校验

6 阅读5分钟

CDP 自动化网页编辑器:三个静默失败的坑与回读校验

把内容发到十几个平台,最麻烦的不是"找不到按钮"。按钮就在那里,选择器也对,脚本退出码 0,日志里一行 error 都没有——但东西没发出去。踩到的坑集中在三类静默失败上。

一、登录判据:别看头像

一次典型误判:脚本报"未登录",人工打开同一个浏览器,账号明明还挂着。原因是判据选错了元素——不少站点用 canvas 或懒加载渲染头像,脚本查 img.avatar 得到 false,不代表会话失效,只代表那张图还没画出来。

可靠判据是"能不能直达受保护的路由":

location.href.includes('/editor/')
  && !!document.querySelector('input.title-input')

未登录时站点会 302 跳转,这条判据跟着跳转结果走,不依赖视觉元素。

二、受控输入:直接改 value 不触发状态

React 和 Vue 的受控输入框,DOM 上的 value 和框架内存里的 state 是两份数据。直接 el.value = title 只改了前者,提交时框架读的是后者——空的。正确做法是用原型上的原生 setter 绕过劫持,再补发事件:

const el = document.querySelector('input.title-input');
const setter = Object.getOwnPropertyDescriptor(
  window.HTMLInputElement.prototype, 'value').set;
setter.call(el, title);
el.dispatchEvent(new Event('input',  {bubbles: true}));
el.dispatchEvent(new Event('change', {bubbles: true}));

顺序不能反:先 setter 再事件,反过来事件派发时框架读到的还是旧值。

三、正文:改 DOM 不等于改文档模型

富文本和 Markdown 编辑器基本都在 DOM 之上有自己一层文档模型。往 textarea 写值、往 contenteditable 塞 innerHTML,界面看着有字,提交上去是空文档。CodeMirror 5 直接拿实例:

const cm = document.querySelector('.CodeMirror').CodeMirror;
cm.setValue(markdown);
cm.refresh();
cm.getValue().length;   // 回读

setValue 走编辑器自己的变更通道,文档模型会同步更新;refresh() 重算行高和可视区,跳过这步在长文里会出现末尾段落不渲染。Draft.js 一类没有这种入口,只能粘贴或逐段键入,而粘贴常常无视光标位置直接追加——同一段粘两次就是两份,这类要改成"一次性构造完整文档"并用 [data-block="true"] 的块数校验。

四、点击:三种点法的成功率差很多

同一个按钮、同一个浏览器,三种点法结果完全不同:

点法结果
el.click()无请求、无报错、页面不变
CDP 坐标点击无请求、无报错、页面不变
PointerEvent 序列请求发出,操作生效

一部分前端组件把逻辑挂在 pointer 事件上(按下 / 抬起 / 点击的完整序列),只派发 click 在它看来不成立,于是静默返回。可用序列:

const o = {bubbles:true, cancelable:true, view:window, pointerId:1,
           pointerType:'mouse', isPrimary:true, button:0, clientX:x, clientY:y};
el.dispatchEvent(new PointerEvent('pointerdown', o));
el.dispatchEvent(new PointerEvent('pointerup',   o));
el.dispatchEvent(new MouseEvent('click', o));

关键判据是网络请求,不是界面变化。 点之前在页面上挂 window.fetch hook 记录出站请求,点完读:列表空着说明事件根本没被框架收到,问题在前端;有请求再去看状态码。这一条能省掉大量"以为平台拒绝、其实是没点中"的误判。

顺手定了一条规矩:同一个按钮试三种点法都不发请求,就停手换路,不要继续加变体。反复重点同一处最耗时间,而且结果不能说明任何问题——真正该查的是按钮 handler 的前置校验条件(表单哪一项没通过),或者干脆走接口重放。

五、回读校验:字数对不上就是没进去

注入成功与否别看编辑器有没有字,用编辑器自己的读数:CodeMirror 用 cm.getValue().length,富文本用块数或实例字数,表单用 el.value.length。

我们内部是硬比对:源文本长度必须等于回读长度。实测一篇 2931 字符的稿子,回读 2931,页面显示字符数也是 2931,三条一致才算注入完成。差一个字符就停下来查——往下走就是发一篇空稿或半篇稿。块数还有一个用处:Markdown 的空行丢了字数不变、段落却被并成一段,这时候读块数比读字数灵敏得多。

六、交付前:匿名可达才算真发出去

发布成功页、后台列表、"已发布"标签,这三个都在登录态下;用户点开链接是匿名态。所以交付前必须无 cookie 验一遍:

curl -sL -o /dev/null -w '%{http_code} final=%{url_effective}\n' \
  -m 20 -A "Mozilla/5.0" "<文章URL>"

200 才算发出去。final= 指向登录页,说明该账号访问权限是"仅登录用户可见"——发布确实成功,但用户点开会要求登录,这种链接不能当成果交付。容易自欺的点是:带 cookie 的浏览器能正常读到正文,所以"我自己能看到"证明不了"别人能看到"。同理,"后台列表里有这篇"也不等于用户能打开,判据只认无 cookie 请求的最终跳转地址。

七、台账:手工发的也得记

我们有过一天:手工在三个平台各发一条,后台都有回执,但发布台账里一条没有——因为台账只由自动流水线写,手工操作绕过了它。后果是统计失真:查近 30 天记录了 462 条,全是定时任务发的老平台,手工发的一条不在里面。

规矩:任何一次发布成功(自动、半自动、纯手工)立刻补一条记录,字段至少四个——平台、标题、URL、时间。数据链路断了,后面的判断全是错的。

小结

三个静默失败的共同点:它们都不报错。唯一能证明操作真的发生的,是回读的数据和发出去的请求。所以流程固定成五步:直达受保护路由判登录 → 原生 setter 填受控输入 → 编辑器实例写入正文 → 回读长度硬比对 → 匿名请求验可达。少任何一步,都可能在一个"看起来成功"的状态里交付空结果。