发布于 更新于 约 6 分钟读完 · 2,327 字

本文目录 8 节
  1. 为什么它比”坐标写错”危险一个量级
  2. 实证
  3. 三验补充(2026-08-13):前提还有两个更隐蔽的形态
  4. 操作规则
  5. 边界
  6. 同族
  7. 验证状态
  8. 关联文档

UI 自动化的固定坐标,必须绑一条可机检的前提断言

一句话律:硬编码坐标都有隐含前提(面板是关的 / 窗口是最大化的 / 输入框是空的)。前提不成立时坐标不会报错,它会落在别的东西上并照常执行。 所以坐标不能单独存在,必须和一条能当场校验的断言绑在一起。

为什么它比”坐标写错”危险一个量级

坐标写错是响亮的失败——点了个空白处,什么都没发生,你立刻知道。

前提不成立是安静的成功——点击、输入、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。

操作规则

  1. 每个坐标常量旁边写死它的前提,并把前提做成可机检的断言,不是注释里的一句提醒。注释拦不住三周后的自己。

  2. 断言写在动作之前。ensure_layout() 必须先于 clear_box(),顺序反了就是清空文档——这条是用一份被删的文件换来的。

  3. 断言优先选位置无关的像素特征。本项目用”按钮区域最暗像素”一个量同时判三件事:

    • < 250:按钮确实在这个坐标上(面板开着的话这里是纯白)
    • < 100:按钮是停止方块(AI 在跑)
    • ≈ 140:按钮是箭头(空闲)

    好判据的标准是跟视图无关。试过”和参考图比对”,会因为视图切换(新建任务页 ↔ 对话页)产生难以解释的误判。

  4. 破坏性操作前必须过断言——Ctrl+A + Delete、拖放、点”确认”/“删除”/“提交”。非破坏性动作可以只在开头过一次。

  5. 窗口类前提要校验矩形,不能只发一条消息就当成功。ShowWindow(SW_RESTORE) 不保证回到最大化:窗口如果中途被人手动动过,restore 出来的是普通尺寸,所有基于”最大化”的坐标同时失效。发完 SW_MAXIMIZE 再读回 rect 比一遍。

  6. 应用状态机不要靠单次点击推断。本例要开一个挂在指定项目上的空白任务:只点侧栏项目条目 → 进它已有的对话;只点”新建任务” → 项目是空的。正解是三步组合(新建任务 → 选项目 → 搜索过滤到一条 → 点它),并用像素校验结果。

  7. 窗口一律按进程名认,不要按标题或类名(三验新增)。Electron 应用(ChatGPT 桌面端、VS Code、Discord…)类名清一色 Chrome_WidgetWin_1;标题由页面内容决定,随时会和别的窗口撞。用 GetWindowThreadProcessId + QueryFullProcessImageNameW 取 exe 名比对。

  8. 每条动作前把应用收敛到已知视图(三验新增)。别假设它还停在上一段结束时的样子——尤其设置页这种”占满整屏且跨脚本残留”的状态。写一个 ensure_main_view() 放在所有坐标动作之前。

  9. 判据要能区分”没找到”和”找错了”(三验新增)。本例空输入框判据应为 0.0019,实测 0.99329(浏览器那块区域是深色),一眼就知道是抓错了窗口。如果判据只有”有没有内容”,会误判成”输入框有残留”,接着去清空一个浏览器页面——又是一次 PLAN.md 事故。

  10. 判据的方向要先想清楚:是”变化”还是”增加”?(三验新增)实测一次翻车:用”对话区暗像素增加”判断收到回复,结果发送后读数从 .00521 降到 .00056——新会话的大标题字号很大,暗像素比一句「2 + 2 = 4.」还多。素材本身是好的,白重录一条。凡是”内容变多 → 某指标变大”的直觉,都要先用两端实测标定一次。

边界

  • 坐标由程序当场探测时不适用(读控件 rect、找蓝色链接块、按标题找窗口)。那类失败会返回 None,是响亮的失败,不需要额外断言。这条只针对硬编码常量。
  • 断言本身也可能有隐含前提(“按钮亮度”这条的前提就是”输入框是空的”)。断言要写清自己的适用条件,否则只是把问题推后一层。
  • 这条不反对硬编码坐标。UI 自动化里硬编码往往是最稳的做法,代价就是这一条断言。

同族

验证状态

⭐⭐⭐ 三次验证

  • 首验:Codex 教程实战六 19 段录制(ensure_layout() 面板前提、按钮亮度的”输入框须为空”前提)
  • 二验:Codex 教程清单 2/3/4 三项目 19 段录制(清空 PLAN.md ×2、窗口未最大化整段静默退出、最小化窗口枚举不到、侧栏行错位)
  • 三验:Codex 教程操作部分 26 段重录(窗口身份撞车差点录错整集、设置页顶掉主界面、判据方向写反),并把前提从”版面”扩展到 “窗口身份”和”应用视图” 两个新形态

三次跨不同项目、不同界面版面独立复现。仍未离开录屏域——三次都来自同一条工具链(humancast + OBS)。真正的跨域验证应当来自别的 UI 自动化场景(浏览器插件、移动端、桌面测试框架)。

另:二验时写下的”最小化窗口枚举不到”这一条,三验时被同一个人在同一套工具上再次踩中(清屏函数把目标窗口自己也最小化了)。已知坑写进文档不等于不会再踩——真正拦住它的是断言,不是记忆。

关联文档

类型/协作工具链主题/自动化主题/UI自动化工具/humancast