UI 自动化的固定坐标,必须绑一条可机检的前提断言 · v1
一句话律:硬编码坐标都有隐含前提(面板是关的 / 窗口是最大化的 / 输入框是空的)。前提不成立时坐标不会报错,它会落在别的东西上并照常执行。 所以坐标不能单独存在,必须和一条能当场校验的断言绑在一起。
为什么它比”坐标写错”危险一个量级
坐标写错是响亮的失败——点了个空白处,什么都没发生,你立刻知道。
前提不成立是安静的成功——点击、输入、Ctrl+A+Delete 全都正常执行了,只是执行在别的控件上。脚本一路绿灯跑完,产物参数正常,你要么很久以后才发现,要么根本发现不了。
实测最贵的一次:右侧文档面板开着时,输入框坐标 (3670,990) 实际落在文档编辑器里,一次 Ctrl+A + Delete 把一份 18KB 的 PLAN.md 清成了 101 字节。脚本没有任何异常输出。
实证
Codex 教程录屏项目,两批素材各自独立复现:
| 事故 | 前提是什么 | 前提不成立时坐标落在哪 |
|---|---|---|
| PLAN.md 被清空 18KB→101B | 右侧文档面板已关闭 | 文档编辑器 |
| 同根因第二次:确认语被打进 PLAN.md | 同上 | 同上 |
| 整段一声不响退出,什么都没打印 | Codex 在录制屏上最大化 | 窗口外的桌面背景(读到深色 → 判成”AI 还在跑”) |
| 段 13 连着两次没跑起来 | 输入框是空的 | 输入框有内容时发送键从浅灰变深,和”停止方块”一样暗 |
| ”找不到窗口” | 窗口没被最小化过 | 最小化窗口 GetWindowRect 只有 160×28,被按最小尺寸过滤的枚举漏掉 |
| 侧栏项目条目点错行 | 侧栏滚动在顶部 | 别的项目 |
共同点:没有一条是坐标本身算错了。
三验补充(2026-08-13):前提还有两个更隐蔽的形态
上表列的前提全是版面类的(面板开合 / 窗口最大化 / 输入框为空 / 侧栏滚动)。第三批(Codex 教程操作部分 26 段重录)暴露出两个新形态,它们比版面类更难想到:
| 新形态 | 事故 | 前提是什么 |
|---|---|---|
| 窗口身份 | find_any("ChatGPT") 抓到「ChatGPT - Google Chrome」,差点对着浏览器录整集 | 句柄指向的确实是目标进程 |
| 应用视图 | 316 第三段录成「常规设置」;随后三次 UI 探索连续失败 | 应用停在主界面,而不是设置页/全屏模态 |
- 窗口身份:ChatGPT 桌面端是 Electron,窗口类名和 Chrome 同为
Chrome_WidgetWin_1;浏览器开着 chatgpt.com 时标题也含「ChatGPT」。类名和标题双重撞车,按这两者找窗口必然出错。同一套坐标在错的窗口上照样能点、能打字——又是一次”安静的成功”。 - 应用视图:设置页是占满整屏的页面而非弹窗,一开就顶掉主界面,左上角标题区变空,“新建任务""输入框""工作区胶囊”整套坐标同时失效。而且它会跨脚本残留——上一个脚本退出时停在设置页,下一个脚本一进来所有坐标就是错的。
两条对应的解法已写进操作规则 7、8。
操作规则
-
每个坐标常量旁边写死它的前提,并把前提做成可机检的断言,不是注释里的一句提醒。注释拦不住三周后的自己。
-
断言写在动作之前。
ensure_layout()必须先于clear_box(),顺序反了就是清空文档——这条是用一份被删的文件换来的。 -
断言优先选位置无关的像素特征。本项目用”按钮区域最暗像素”一个量同时判三件事:
< 250:按钮确实在这个坐标上(面板开着的话这里是纯白)< 100:按钮是停止方块(AI 在跑)≈ 140:按钮是箭头(空闲)
好判据的标准是跟视图无关。试过”和参考图比对”,会因为视图切换(新建任务页 ↔ 对话页)产生难以解释的误判。
-
破坏性操作前必须过断言——Ctrl+A + Delete、拖放、点”确认”/“删除”/“提交”。非破坏性动作可以只在开头过一次。
-
窗口类前提要校验矩形,不能只发一条消息就当成功。
ShowWindow(SW_RESTORE)不保证回到最大化:窗口如果中途被人手动动过,restore 出来的是普通尺寸,所有基于”最大化”的坐标同时失效。发完SW_MAXIMIZE再读回 rect 比一遍。 -
应用状态机不要靠单次点击推断。本例要开一个挂在指定项目上的空白任务:只点侧栏项目条目 → 进它已有的对话;只点”新建任务” → 项目是空的。正解是三步组合(新建任务 → 选项目 → 搜索过滤到一条 → 点它),并用像素校验结果。
-
窗口一律按进程名认,不要按标题或类名(三验新增)。Electron 应用(ChatGPT 桌面端、VS Code、Discord…)类名清一色
Chrome_WidgetWin_1;标题由页面内容决定,随时会和别的窗口撞。用GetWindowThreadProcessId+QueryFullProcessImageNameW取 exe 名比对。 -
每条动作前把应用收敛到已知视图(三验新增)。别假设它还停在上一段结束时的样子——尤其设置页这种”占满整屏且跨脚本残留”的状态。写一个
ensure_main_view()放在所有坐标动作之前。 -
判据要能区分”没找到”和”找错了”(三验新增)。本例空输入框判据应为 0.0019,实测 0.99329(浏览器那块区域是深色),一眼就知道是抓错了窗口。如果判据只有”有没有内容”,会误判成”输入框有残留”,接着去清空一个浏览器页面——又是一次 PLAN.md 事故。
-
判据的方向要先想清楚:是”变化”还是”增加”?(三验新增)实测一次翻车:用”对话区暗像素增加”判断收到回复,结果发送后读数从 .00521 降到 .00056——新会话的大标题字号很大,暗像素比一句「2 + 2 = 4.」还多。素材本身是好的,白重录一条。凡是”内容变多 → 某指标变大”的直觉,都要先用两端实测标定一次。
边界
- 坐标由程序当场探测时不适用(读控件 rect、找蓝色链接块、按标题找窗口)。那类失败会返回
None,是响亮的失败,不需要额外断言。这条只针对硬编码常量。 - 断言本身也可能有隐含前提(“按钮亮度”这条的前提就是”输入框是空的”)。断言要写清自己的适用条件,否则只是把问题推后一层。
- 这条不反对硬编码坐标。UI 自动化里硬编码往往是最稳的做法,代价就是这一条断言。
同族
- 自动化产出双重验收_机器验参数人眼验内容_v1 —— 一体两面:那条管”结果对不对”(事后抽帧),本条管”动作发生在对的上下文里”(事前断言)。能写成断言的就别只靠事后抽帧,人眼关是兜底不是第一道防线。
- Windows下编码与DPI的所见非真相 —— 同源的”环境隐式转换”:DPI 缩放悄悄改比例,面板开合悄悄改布局,都是”看起来对,实际不对”。
验证状态
⭐⭐⭐ 三次验证
- 首验:Codex 教程实战六 19 段录制(
ensure_layout()面板前提、按钮亮度的”输入框须为空”前提) - 二验:Codex 教程清单 2/3/4 三项目 19 段录制(清空 PLAN.md ×2、窗口未最大化整段静默退出、最小化窗口枚举不到、侧栏行错位)
- 三验:Codex 教程操作部分 26 段重录(窗口身份撞车差点录错整集、设置页顶掉主界面、判据方向写反),并把前提从”版面”扩展到 “窗口身份”和”应用视图” 两个新形态
三次跨不同项目、不同界面版面独立复现。仍未离开录屏域——三次都来自同一条工具链(humancast + OBS)。真正的跨域验证应当来自别的 UI 自动化场景(浏览器插件、移动端、桌面测试框架)。
另:二验时写下的”最小化窗口枚举不到”这一条,三验时被同一个人在同一套工具上再次踩中(清屏函数把目标窗口自己也最小化了)。已知坑写进文档不等于不会再踩——真正拦住它的是断言,不是记忆。
关联文档
- 2026-08-01_Codex教程清单2-4_三项目19段录屏复盘(本律来源案例,含完整事故清单)
- 2026-07-31_Codex教程实战六_自动化录屏19段复盘(首验案例)
- 2026-08-13_Codex教程操作部分_26段重录复盘(三验案例:窗口身份 / 应用视图两个新形态)
- 自动化产出双重验收_机器验参数人眼验内容_v1(一体两面)
- Windows下编码与DPI的所见非真相(同源:环境隐式转换)
- 代码资产索引(humancast 工具登记处)
- 04_方法论与洞察索引