表单提交了,但什么都没发生:3 个隐藏必填、2 秒 Toast 与后台标签的静默丢弃
自动化脚本的最后一公里,往往是最安静的一段:标题填了、正文灌了、发布按钮也点了——然后网络面板里一个请求都没有。没有报错,没有超载,没有任何异常。脚本"成功"了,事情一件也没发生。
这篇文章记的是我在一个晚上连踩的四类这种坑(2026-10-12 实测),每一类都给出判据和修法,代码可直接跑。
一、总纲:把"能点"和"能过"分开
绝大多数静默失败,根因是同一个思维惯性:脚本以"元素可交互"为通过标准,而平台以"字段合法"为通过标准。按钮没被禁用、点击没抛异常,只证明前者;后者不满足时,前端直接拦下,连请求都不会发。
所以提交前要补四道断言,缺哪道,哪类坑就会漏过去:
- 可见性断言:点的按钮必须是
getBoundingClientRect().width > 0的那个(页面上常有 rect 为 0 的隐藏副本); - 必填断言:每个必填字段逐个读回值,单选要读
checked,不要读文字; - 反馈断言:提交后必须抓到"成功"或"失败"的明确信号,抓不到就判失败,不要默认成功;
- 状态断言:
document.visibilityState必须是visible,否则真实输入可能整个被丢弃(下面第三节)。
二、第一类坑:三个隐藏必填项
现象
腾讯云开发者社区的发文抽屉,重开时三个 radio 全部是未选中状态;摘要和文章标签也是空的。脚本逐项填了标题、导入了正文,点「确认发布」——页面没反应,网络面板里唯一的请求是几秒前自动保存草稿的那一个。
真因
发布前置校验有三项,任何一项不满足就直接 return,不发请求、不弹窗、不报错:
- 文章来源单选必须显式选中「原创」(重开抽屉时三个 radio 会全部复位成 unchecked);
- 摘要必填;
- 文章标签必填。
前两项还算好查——读一眼面板就知道空着。第三项是最阴的:漏填时平台确实会弹提示,但那提示是个2 秒就消失的 Toast。
2 秒 Toast 怎么抓
先看看普通写法为什么必然错过:
awaitclick(publishBtn);awaitsleep(2000);// 等"应该出现"的提示constmsg=awaitreadToast();// 读的时候,提示早就没了await sleep(2000)之后再读,Toast 已经消失,读到的永远是空。轮询间隔只要大于 Toast 寿命,就永远抓不到。
正解是在点击之前就挂上监听,用 MutationObserver 把每条 Toast 的文本和时间戳当场记下来:
// 点击「发布」之前执行consttoasts=[];constmo=newMutationObserver(()=>{document.querySelectorAll('.cdc-message-tip-container, [class*=message-tip], [class*=toast]').forEach(el=>{constt=(el.innerText||'').trim();if(t)toasts.push({t,at:Date.now()});});});mo.observe(document.body,{childList:true,subtree:true,characterData:true});// ……点击发布……awaitsleep(3000);mo.disconnect();// 判据:toasts 里出现「请选择文章标签」= 被前端拦了if(toasts.some(x=>x.t.includes('请选择'))){thrownewError('发布被拦截:'+toasts.map(x=>x.t).join(' | '));}这套监听对"2 秒消失"的任何提示都管用。实测抓到的那条原文是「请选择文章标签」——五个字都不差,但任何事后采样都看不到它。
配套的修法也直接:标签输入框是input.cdc-tags-input__input,建议项必须按坐标过滤再点(y在 200–640、x在 850–1330 以内),否则会误点到编辑器工具栏的「标题」「粗体」按钮上——这是同一个输入框位置太居中造成的连带坑。
三、第二类坑:后台标签页把真实输入整个丢掉
现象
同一个脚本,窗口在前台时全绿;窗口一旦被挡住(或者标签切到后台),标签写不进去、键盘事件没反应、鼠标坐标点击全部落空。不报错,就是没效果。
真因链
- 标签页转到后台,
document.visibilityState === 'hidden'; - 此时 Chrome 会静默丢弃
Input.insertText、Input.dispatchKeyEvent和真实坐标的Input.dispatchMouseEvent——调用全部返回成功,事件一个都不派发; - 连带效应:依赖 CSS 动画的元素会停在初始位置。实测某平台的发布抽屉用
fadeInRightBig入场(translateX(2000px) → 0,1 秒),后台标签里动画不推进,抽屉永久停在屏幕外(实测left=2404,而视口只有 1394)。后面所有基于坐标的操作自然全部打空。
修法
关键一步只有一行,但必须在每个新的 CDP 连接里重新执行:
awaitsend('Emulation.setFocusEmulationEnabled',{enabled:true});只调Page.bringToFront是不够的——visibilityState仍然是hidden,输入照样被丢。开了焦点模拟之后,后台标签的插入文本、按键、点击才真正派发得出去。
抽屉停屏外的问题则用强制落定解决:
constw=document.querySelector('.editor-dialog__wrapper');w.classList.remove('animated','fadeInRightBig','fadeInRight','fadeIn');w.style.transform='none';w.style.animation='none';落定后补一道横向断言,防止"面板在屏外但看起来正常":
constr=w.getBoundingClientRect();if(!(r.left>-2&&r.left<window.innerWidth-50))thrownewError('发布面板横向停在屏外');四、第三类坑:平台会替你"加戏"
现象
另一篇文章发布前,标签栏明明只有手填的 3 个,点发布却被服务端一句「标签个数最多不能超过 5 个」打回。
真因
回读最终 chips 才发现是 6 个:除了手填的 3 个,平台从标题里自动提取了 3 个(标题含「CSS」「滑块」「鼠标事件」,存草稿时被自动摘成标签)。手填 3 + 自动 3 = 6,超限。
这类"平台替你补字段"的行为每家都有,只是补的地方不一样:有的补标签,有的补摘要,有的把外链图片统一替换成报错占位图。共同点是脚本不知道它补过。
修法
提交前把最终状态完整读一遍,别只数自己写进去的:
constchips=[...document.querySelectorAll('.has-list span')].map(e=>(e.textContent||'').replace(/[\s\u200b-\u200d\ufeff]/g,'')).filter((t,i,a)=>t&&a.indexOf(t)===i);console.log('最终标签:',chips.join(','));if(chips.length>5)thrownewError('标签超限: '+chips.length);实测处理动作就是删掉最没有检索价值的那一个(「鼠标事件」),保留 5 个,再点发布,一次通过。
五、把四道断言做成一个提交前的自检函数
把上面所有判据收敛成一个函数,发布前调用,任一不过就抛错,宁可中断也不要静默失败:
asyncfunctionpreflight(page){constr=awaitpage.evaluate(()=>{consterrs=[];// 1. 可见性if(document.visibilityState!=='visible')errs.push('visibilityState=hidden');// 2. 必填:逐个读值consttitle=document.querySelector('input.article-bar__title, #title');if(title&&!title.value.trim())errs.push('标题空');constbody=document.querySelector('textarea.write-area, .cm-content, pre.editor__inner');if(body&&(body.value||body.textContent||'').trim().length<100)errs.push('正文过短');// 3. 单选/复选读 checked,不读文字document.querySelectorAll('input[type=radio][required]').forEach(r=>{constname=r.name;constg=document.querySelectorAll(`input[name="${name}"]`);if(![...g].some(x=>x.checked))errs.push('单选未选: '+name);});// 4. 标签计数(含平台自动补的)constchips=[...document.querySelectorAll('.has-list span, [class*=tag] span')].map(e=>(e.textContent||'').replace(/[\s\u200b-\u200d\ufeff]/g,'')).filter(Boolean);if(chips.length>5)errs.push('标签超限: '+chips.length);returnerrs;});if(r.length)thrownewError('提交前自检未过: '+r.join('; '));}六、结论
这四类坑没有一个是"报错",所以任何一个常规的异常捕获都抓不到它们。能发现的只有两种时候:一是事后人工核对线上结果,二是事前把判据写全。
| 坑 | 判据 | 修法 |
|---|---|---|
| 隐藏必填 | 逐个读字段值/checked,不读文字 | 提交前 preflight |
| 2 秒 Toast | MutationObserver 事前监听 | 挂 MO 再点发布 |
| 后台标签丢输入 | visibilityState + 抽屉 left | setFocusEmulationEnabled + 落定动画 |
| 平台自动补字段 | 数最终 chips,不数手填的 | 提交前完整回读 |
最后一条经验:脚本安静地返回成功,是自动化里最贵的一种失败。它不污染日志,不触发告警,只是让你以为活干完了。所有关键动作后面,都跟一条能证伪的断言。