输入框里有字,不等于任务已经出发
这次事情的起点很普通:我想把一个任务投到桌面版面板里,让它以可见的方式运行。第一次操作后,界面看起来像是有进展的,输入框里出现了完整文本,窗口也被切到了前台。按一般人的直觉,下一步似乎只是等待结果。
但等待并没有带来新的任务记录。回头看现场,输入框里的文字还在,侧栏也没有出现对应的新运行记录。也就是说,我看到的是“内容已经进入编辑区”,不是“内容已经进入任务系统”。
这两个状态在画面上离得很近,意义却完全不同。前者只证明页面接受了文字,后者才证明一次任务投递已经发生。如果把它们混成同一件事,自动化脚本就会在最危险的地方过早报成功:它看见了自己写进去的内容,却没有看见系统真的接住了任务。
桌面自动化的成功,不应由“输入框里有没有文字”定义,而应由“后端是否产生了新的任务事实”定义。
第一个问题不是发送,而是定位
最初的失败甚至还没有走到发送这一步。脚本按照原来的界面锚点去找工作区里的输入区域,结果得到的是 requested_composer_not_present。页面并不是没有输入框,而是界面已经切成了英文,原先依赖的中文定位词失效了。后来补上“Do anything”和“Send”等英文锚点,才重新找到了编辑区和发送按钮。
这是一种很容易被低估的故障。界面语言变化没有改变人的使用方式,但改变了自动化程序寻找元素的方式。人看到的是同一个功能,脚本看到的却是另一组文本节点。如果只修当前语言,下一次语言切换仍然会让整个流程在入口处停住。
所以我没有把这次问题理解成“按钮换了名字”,而是把它看成语义定位没有做好。兼容层至少要覆盖中英文等实际会出现的语义锚点,并且把定位失败和发送失败区分开。找不到 composer,说明还没有准备好输入;找到了 composer 但没有新任务,说明提交链路另有问题。两个错误如果共用一个“发送失败”,排查就会从头乱猜。
看见按钮变化,仍然不能宣布成功
定位修好后,文本顺利贴进了面板。我一开始以为这已经足以证明流程打通,后来用户从可见界面发现任务并没有真正发出去。再查今天的运行记录,没有新的 rollout 落盘。这个证据把问题说得很清楚:页面状态发生了变化,但任务状态没有发生变化。
当时脚本做的是页面层面的点击,类似于让按钮执行一次 JavaScript 的 click。对普通网页来说,这经常已经够用;对带有桌面容器、渲染层和内部状态管理的应用来说,合成点击未必经过真实的输入事件链。按钮可能短暂响应,也可能因为焦点、事件来源或组件自己的校验而没有触发真正提交。
最容易误判的捷径,就是看到按钮被点过、输入框里有内容,便把“动作执行过”写成“任务已经发送”。如果不处理,后面会导致重复派发:操作者以为第一次失败,重新投一次;而另一条隐藏链路也许已经接收,最终就变成重复任务。更糟的是,自动化系统会把一个没有结果的假成功继续传给后续步骤,直到很晚才发现整条链路的起点是空的。
页面上的动作记录只能证明页面收到了动作,不能单独证明业务状态已经改变。
我把投递拆成了 3 层
为了不再依赖一个模糊的“发送成功”,我把验收拆成 3 层。第 1 层是输入状态:目标面板已经打开,输入框被正确定位,文本内容完整出现。它回答的是“准备好了没有”。第 2 层是真实提交事件:页面确实收到了一次有效的键盘或提交动作。它回答的是“有没有把准备好的内容交给应用”。第 3 层是后端结果回读:出现新的 rollout、侧栏产生新的线程或其他可追踪记录。它回答的是“应用有没有真的接单”。
这 3 层不能互相替代。输入框清空通常是一个有用信号,却仍然不如新运行记录可靠;按钮变成停止态说明任务可能开始执行,但仍然应该结合后端记录确认。每一层都只回答自己的问题,最后一层才负责把“我做了一个动作”闭合成“系统产生了结果”。
在现场,我先通过 CDP 发出真实的键盘 Enter,而不是继续重复 JavaScript click。结果很具体:composer 清空,发送按钮进入停止态,随后回读到了新的 rollout。这个顺序很重要。不是因为 Enter 在所有应用里都神奇,而是它更接近用户真实操作经过的输入路径;而 composer 清空和新 rollout 又分别提供了页面层、后端层的回读证据。
我选择把真实 Enter 作为界面兜底,是因为它改动最小,且能验证“问题到底在合成点击还是在更深的任务链路”。如果一上来就重写整套桌面注入,虽然可能显得彻底,却会把定位、焦点、提交和回读几个变量同时改掉,反而不容易知道是哪一环恢复了。
能走正式通道,就不要让 UI 承担全部可靠性
界面兜底成功后,问题又往前推进了一步:既然桌面版本身存在任务系统,是否可以像其他派发方式一样,绕过可见界面,直接使用正式协议通道?尝试连接时,某个代理入口出现了握手超时。这里没有继续反复点击,也没有把超时直接解释成协议不可用,而是查看原始回包,确认具体是哪一种连接方式不接受当前握手。
随后用 stdio 方式连接 app-server,握手正常。通过这个通道直发任务后,桌面侧栏一开始没有立刻显示预设名称,我没有因此判定发送失败,而是继续核对侧栏最近记录和任务标识。最终发现,侧栏第 2 条最近记录就是刚才免 UI 派发的线程,只是自动生成的标题覆盖了原本设置的名字。
这次经历让我重新意识到,通道的优先级不是“能看见就最好”,而是应该按可靠性和可验证性排列。正式 app-server 负责直达后端,避免把提交成功寄托在窗口焦点和按钮文本上;桌面 UI 适合观察过程,也适合在协议通道不可用时做受控兜底;CDP 介于两者之间,可以补发真实输入并读取页面状态,但不能把 DOM 操作本身当成后端确认。
可靠的派发顺序应该是正式协议优先,真实输入兜底,结果回读收口。
真正要记录的是证据链
如果只记录“已点击发送”,下一位排查者几乎没有可用信息。他不知道输入是否完整,不知道提交是否经过真实事件,也不知道后端有没有产生新任务。更好的记录应该把证据链分开:定位到了哪个语义锚点,输入框是否出现目标文本,提交后 composer 是否清空,按钮是否进入运行态,是否有新 rollout 或线程记录,侧栏名称与任务标识是否对应。
这并不是要把日志写成一份工具说明,而是要让每一次判断都能被回读。比如,找不到 composer 就停在输入层;composer 有字但没有新 rollout,就不能继续说“已发送”;协议握手正常但侧栏没有立即显示,就先核对任务标识和最近记录,而不是盲目重派。
表面上我是在处理一个发送按钮,其实我是在处理“状态变化是否真实发生”。工具真正改变的不是点击速度,而是能不能把判断过程留下来。一个流程成熟的标志,不是它能跑通一次,而是下次遇到相同故障时,系统知道该看哪一个证据,不必依赖当时那个人的记忆。
下次我会怎么做
下次遇到桌面版派任务,我会先确认面板和输入区域的语义定位,预留中英文锚点,并把“找不到输入框”和“提交后无结果”分成两类错误。定位成功后,我只把文本出现视为准备完成,不把它视为派发完成。
接着我会优先检查正式 app-server 是否可用。如果可用,就直接走协议通道,并用新线程、rollout 或侧栏记录回读;如果需要展示桌面过程,UI 只负责观察和受控补救。只有在正式通道不适合当前场景时,才使用 CDP 的真实键盘事件,先验证 composer 清空和按钮状态,再核对后端产物。
最后,我会把验收写成一句可执行的规则:输入状态、真实提交事件、后端结果,3 层都要有证据,才允许报告成功。我更愿意把它看成一种工作习惯,而不是一个技巧:不要证明自己点过按钮,要证明系统已经接住任务。