那天下午我在干一件很琐碎的事:把一篇文章发到公众号和掘金。
草稿早就存好了,正文、图片、摘要都齐了,就差把封面传上去。封面那一步要点系统的文件选择框,鼠标键盘都够不着,只能走 Chrome 的调试协议把文件塞进去。
我那个脚本里有个现成的子命令干这件事。几行代码。写的时候顺手,注释里还标了「已验证」。
那天下午我第一次真正跑它。它报错了。
那一刻我才反应过来,那个「已验证」验的是什么------验的是语法没错、子命令注册上了。真正跑一次上传,一次都没有。
那天我一整天都在被自己的工具骗
往前捋捋,从头一天下午到晚上,我一共踩了七次。它们的共同点不是复杂,是都不报错。
最轻的一次是查封面。我写脚本判断封面传上去没有,脚本说没有------hasImg: false,零张图。我正打算去改数据结构,因为我记得传过。截了个图一看,封面好好地挂在右上角。
问题出在我查错了地方。真正存封面的是一块区域的背景图,不是 <img> 标签。查 <img> 当然查不到。这不算难查,难的是在我改数据结构之前,没人告诉我「你查的那儿压根不可能有图」。
第二次是摘要。字数计数器那儿显示 52/120,我一看还剩一半富余,正准备往里加两句。真实数字是 118/120,只剩两个字的空间。那个 52 是从手机预览的 iframe 里读的,读的是个副本。
第三次最气人。粘完正文,手机预览面板里完完整整一篇,段落、配图、标题全在,看着就是成了。实际正文编辑器里是空的。那个面板是预览,它先给你渲染一份候选副本,让人以为已经进去了。
后面还有三次,一次比一次贵。封面弹窗里四个按钮,用 getElementById 一个都查不到------它们是 class 不是 id。改视口尺寸的那个调试命令,返回一个空对象,看着像成功,其实什么都不干,因为它绑在连接会话上,而我这个脚本每条命令都重开一次连接。表格更离谱,草稿里两个表格老老实实躺在那儿,一发表,全变成了一段段文字,因为编辑器在保存的时候会拿富文本反推 Markdown,而它不认识表格。
这七次里,真正让我停下来想了一会儿的,是最后那次。
我以为我在验证,其实在演
出错之后我回头去查代码,才看到问题在哪儿:这个脚本的底层,每执行一条命令就新开一个连接。而浏览器调试协议里用来定位元素的编号,不是拿到就能用的------你得先在当前这条连接上把文档上下文建起来。
所以我原来那三行代码,第一步建上下文、取到编号,第二步换了一条新连接,拿这个编号去查元素。新连接上还没建上下文,那个编号就用不了。
修法不复杂。但让我在原地停了一会儿的是另一件事:
我为什么要给一个从没跑过的命令标「已验证」?
不是手滑。当时的判断链是这样的:这个命令能正常解析,参数能注册进去,报错信息也写得清楚------一个写对了一半的东西,看起来和跑通没什么区别。
更要命的是时间差。那个「已验证」是我自己在同一个下午写上去的,隔了几个小时,它第一次真跑。第一次跑就报错。
至于为什么这几个小时里没人发现:整个项目写了八个月,同一件事我早就验证过很多次了,只不过验证的是手动那条路 ------点「从图片库选择」,右上角「上传文件」,系统弹框,手动选。每次都成功。setfile 是我为了省掉这几下点击另外加的。
那条路确实成功。所以我给它盖的章,验的是别的东西。一个不会自己叫的坏东西,比一个会叫的坏东西安全得多,也危险得多。
后来的事更难堪
当天晚上我还在这个家目录里 git init 了。不是在项目目录里,是在上一级。
家目录里有什么,我大概知道:几个云厂商的密钥目录、SSH 的配置、各家 CLI 存 token 的文件。git init 也确实成功了,它从来不问你要不要。
真正提交之前我才想起这事,那时候还没 commit,风险是零。我给自己写了一份忽略规则,用的是「先全挡、再放行要提交的那一个目录」------后来知道,靠逐条列黑名单是挡不住的,你总会漏一条。
然后我装了个专门扫密钥的工具去扫。它在我自己的脚本目录里翻出两个文件,里面硬编码了带签名的图片地址。那种地址谁拿到都能用。
那两个文件是我前两天自己写的探针,作用是把图片塞进编辑器。我写它们的时候,只想着「先跑通」,没想过它会被谁看到------我以为没人看的东西,和会被提交的东西,是同一个目录。
现在我的做法很笨
第一,截图才算数。页面上看不见的东西,我就不当它存在。hasImg: false 这种结论,配一张截图一起看。
第二,从服务端读回来才算数。编辑器里显示存好了不算,重新开一个标签,看服务器到底收没收。
第三,写完一个命令,立刻拿一个真数据跑一遍。不用假数据,不用「应该可以」。
这三条都很慢,比以前慢很多。但慢的代价我能看见。
我还在想第四条,暂时没想明白。可能是:别让自己写的工具,成为你最信任的那个。 我对它没有戒心,因为它是我写的,我清楚每一行。这恰恰是问题------陌生工具的毛病我会怀疑,自己写的不会。
写下来主要是给自己看
这篇东西没什么实用价值,教程里不会这么讲。但我今天下午损失的时间,够我把工具重写一遍。
如果别人也遇到过类似的事------自动化跑完了,界面看着没问题,隔几天才发现某一步从来没生效过------那至少说明不止我一个人会这样。
附:本文引用
一、官方文档只说了一半
第二节里那个技术解释,我原来写的是「那个编号只在当前连接里有效 」。 写这篇的时候我去查了官方规范,想给它找个出处------没找到。
Chrome DevTools Protocol 官方规范(devtools-protocol 仓库的 browser_protocol.json, 2026-10-13 取,1,419,927 字节)里,NodeId 的定义全文就一句:
Unique DOM node identifier.
没有「会话」,没有「仅在当前连接内有效」,什么限定都没有。 引一句没说的话,那不叫引用,叫编。 所以那句话我拿不出处,也就不该那么写。
二、我自己测了一遍,测出来我原来的说法是错的
既然文档证明不了,那就测。实验在 experiments/nodeid-scope/, 脚本、原始输出、每一版翻车都在里面。
四臂:
臂 / 做什么 / 结果
- A ------ 会话 A 取编号 → A 内自查可用 → 断开 → 会话 B 直接用 ------ ✗ 失败
- B(阳性对照) ------ 会话 B 自己建上下文取编号,同会话内用 ------ ✓
- C(反向自测) ------ 编一个编号 999999999 ------ ✗ 同样失败
- D(补漏) ------ 新会话先建上下文 ,再 用 A 留下的旧编号 ------ ⚠️ 成功了
两个会话取到的编号数值都是 1。
D 臂推翻了 A 臂:只要新连接先把文档上下文建起来,A 留下的那个编号照样能用。
所以真实机制是------不是「编号跨会话就死」,是「你这条连接还没建上下文」 。 我原来那三行代码的毛病,是跳过了建上下文就直接用编号, 不是「每条命令重开连接」这件事本身有多严重。
顺带一提,修法也跟着变。我原文写的是「三步串在同一个连接里做完」------ 测完才发现每步各自先建一次上下文也能work,我当时那个「必须串起来」的紧张, 多半也是猜的。
三、这个实验的第一版也骗了我
第一版只有 A/B/C 三臂,结论是「跨会话失效,坐实」,我很满意。
是 D 臂把它推翻的。问题出在阳性对照上:A 臂和 B 臂取到的编号数值相同(都是 1), 而 B 臂自己先建了上下文------等于替 A 臂把那个 1 准备好了。 于是 A 臂的失败被我很自然地读成「跨会话失效」,实际是「没先准备」。
这跟这篇讲的是同一件事:一个不会自己叫的坏东西,比一个会叫的坏东西安全得多, 也危险得多。 我那个实验里,「对照」是有的,只是没跟实验组对齐, 对齐不好的时候,对照组会替实验组把准备工作做完。
四、本文没有引用学术文献
这篇没有做文献综述,也没有引用任何论文或书籍。 第二节那个技术问题我查的是协议规范和实测,不是文献。 写清楚是为了避免读者以为我做过文献工作。
五、取到的 / 没取到的
来源 / 状态
- CDP 官方规范
browser_protocol.json------ 取到,但它证明不了我原来那句话 devtools-protocol仓库的pdl/DOM.pdl------ 两个分支(master / main)均 404chromedevtools.github.io的 HTML 页 ------ 只是重定向壳,内容在 JS 里,正文取不到
没有取到的东西我列在这儿,不用记忆去补。