三套 API 都告诉我成功了,浏览器一个字都没收到

1 阅读1分钟

我在写一个跨 Windows 的浏览器自动化脚本。往页面里输一段中文,是里面最基础的一步——打开编辑器,定位输入框,把字敲进去。

这件事我在三层 API 上各试了一遍。

每一层都告诉我成功了。浏览器一个字都没收到。

先说怎么排除的

因为「没输入成功」这种结论太容易骗人了,我先把能排除的全排掉。

第一,前台窗口是不是靶场?读系统句柄:SETFG=True FG=9439172 TARGET=9439172 MATCH,桌面是可交互的。窗口对得上。

第二,焦点是不是落在输入框上?每次实验前读 document.activeElement,全程是 t。焦点也对。

第三,会不会是事件发去了别的标签页?所以我不用输入框监听,改成在 window 上挂捕获阶段的 keydown。只要事件进了浏览器,window 一定记录得到。

三项都排掉之后,观测口径才立住:返回值、window 收到几个 keydown、输入框的值。

第一层:操作系统

SendInput 配 KEYEVENTF_UNICODE,往输入框里打「中文测试」。

四个字符,每个字符发按下和抬起,一共八个事件。函数返回 8。

八个,全部投递成功,没有一个失败。

然后浏览器那边:keydown 收到 0 个,输入框是空的。

我一开始以为这是 KEYEVENTF_UNICODE 的问题——听说 Chromium 会丢弃合成的 Unicode 按键。所以我又做了对照:换成剪贴板 Ctrl+V,走完全不同的通道。

Ctrl+V 的 SendInput 返回 2,2。

输入框还是空的。

对照组一挂,前面那个归因就站不住了。不是 Unicode 按键被丢弃,是**SendInput 整体送不到 Chromium**。

到这里我停了一下,因为这个结果比「某个 API 有 bug」难受得多。它没有 bug,它忠实地完成了自己的职责:把事件投递进系统输入队列。只是队列这一层,压根不是目标窗口会看的地方。

我顺手查了句柄,确认不是送错了窗口——不是,前面查过了,是对的。

第二层:浏览器调试协议

Input.insertText 一行就进去了。四个中文字符,输入框里是「中文测试」。

但它不产生 keydown。

同一个域里还有另一个方法,Input.dispatchKeyEvent。我拿它敲了个字母,发现一件更绕的事。

rawKeyDown 发了一次。keydown 记到 1 个,输入框是空的。 keyDown 带上 text 再发。keydown 同样是 1 个,输入框里有内容了。

同一个函数名,同一个 Input 域,只是 type 参数不同,出来的语义完全不一样。rawKeyDown 只发按键不写字,keyDown 带上 text 才会写字。

两次调用都没有报错。看返回值,你分不出哪个是哪个。

第三层,以及我没料到的地方

到这里我以为结论已经很清楚了:底层不可靠,往上选一个可靠的用。insertText 稳,就它了。

然后我想到一个很自然的问题:我凭什么确定它成功了?

之前每次验证,我看的是输入框的值或者字数计数器。但更省事的做法是监听 keydown——有事件进来,说明字进去了。

这个做法在 insertText 上必然失效。因为 insertText 根本不产生 keydown。它不是模拟按键,它是直接改 DOM。

唯一真正把中文送进编辑器的这条路,恰恰是唯一一个不会发出按键信号的路。

我用来验证输入成功的那个信号,在最可靠的方案上是缺席的。

这比「返回值不可信」难受。返回值不可信,至少还有个替代品。这里是:验证手段和正确方案在结构上互斥,无论怎么调都调不到一起。

我现在怎么做

写完之后我改了三处。

一是所有文字输入统一走 insertText,不再碰合成按键。中英混排也不用切输入法,中文和英文在它这里没有任何区别。

二是判断成功的标准只有一条:读目标的可观测状态。输入框的值、字数计数器、正文 DOM。任何一层的返回值都不作为依据——SendInput 返回 8 那次已经替我证过一次了。

三是写完一个输入动作,立刻回读一次。不靠日志,不靠返回值,读目标。

慢。比我原来想的那种「发完就往下走」慢很多。但慢的代价我能看见。

顺便一件事

写实验脚本的时候我踩了一个更小的坑,比上面三个都轻,但很像。

我有个循环,要给页面敲三个字母。我写成了 cdp.py key a、key b、key c,外面套了错误重定向。

key 这个子命令的按键表里只有十个特殊键,字母不在里面。它对 a 抛的是异常,不是「敲了个 a」。

我的重定向把异常吞了。循环跑完,三次调用看起来都执行了,实际上一个字都没发。

等我发现的时候,页面上干干净净,没有任何报错痕迹。

同一个病,长得小一点的样子。

写完我回头改了一遍那个实验脚本。最先要改的不是文章,是那句 2>&1。

它让我在错误发生的那一刻就没了声音。我花时间最多的不是查 Chromium为什么不收事件,是查「为什么连报错都看不到」。