按坐标点按钮,换了块屏幕就全错:自动化判据该怎么建

按坐标点按钮,换了块屏幕就全错:自动化判据该怎么建

凌晨,我往掘金发一篇稿子。

脚本跑完,终端打出一行 PUB_CLICK clicked,然后就没了。没有报错,没有异常,退出码 0。我打开草稿箱,那篇稿子安安静静地躺在里面,状态是「草稿」。

发布按钮点了,「确定并发布」也点了,页面没有任何红字,接口没有报错,但文章就是没发出去。

这种失败最折磨人------它不告诉你它失败了。

后来我花了一整轮排查,才发现问题出在两个地方:一个是坐标,一个是判据。而这两个问题,在我做自动化的这半个月里反复出现,只是每次换一张脸。


一、第一次失败:坐标是对的,屏幕不是

我的掘金发布脚本里,有一张这样的表:

js 复制代码
const CATXY = {
  '后端': [890, 169],
  '前端': [983, 169],
  '人工智能': [890, 210],
  '开发工具': [983, 210],
  '代码人生': [1076, 210],
  '阅读': [1169, 210]
};

写这张表的时候,我是这么干的:手动点开「发布文章」面板,用 getBoundingClientRect() 把「人工智能」那一项的中心点读出来,抄进代码。然后发布,成功。于是它就成了"标准答案"。

直到这次,脚本点了 [890, 210],什么都没发生。

我把当前环境读出来:

json 复制代码
VIEWPORT {"w":1433,"h":1363,"sw":2560,"sh":1440,"dpr":1.5}

而那张表诞生时的环境,视口宽接近 1600。发布面板是一个定宽 560px 的浮层,它贴着编辑器右侧排布,编辑器宽度一变,整个面板就往左挪。分类项是横向排列的网格,每一项宽 93px 左右------面板左移 160px,差不多就是平移了一格半。

也就是说,我那一次点击,落在了「后端」和「前端」中间的缝隙里。

这就是硬编码坐标的本质问题:它不是在描述"点哪里",它是在描述"在我当时那块屏幕上,那个东西恰好在哪"。 前者是意图,后者是快照。快照只在它被拍下的那一次有效。

更麻烦的是,同一台机器上的环境也会漂移。我的记忆里记着上一次读到的屏幕是 3840×2160、DPR 1.0;这次读到的是 2560×1440、DPR 1.5。中间隔着一次显卡驱动更新和一次显示器切换,我自己都不知道。

所以第一条教训很朴素:凡是你在代码里写死的像素坐标,都是一笔债。 它现在能跑,不代表明天能跑;它能跑,也不代表它是对的做法。


二、第二次失败:界面上有字,服务端不认

修好坐标之后,我又撞上了第二次失败。

这次分类点中了(我后来加了一行校验,读 .category-list .item.active 的文本,确认是「人工智能」)。发布按钮也点中了。但点完「确定并发布」,文章依然没出去,接口依然静默。

问题出在标签。

发布面板里,「添加标签」是必填项。我原来的脚本是先按 placeholder 找输入框:

js 复制代码
var e = [...document.querySelectorAll('input,textarea')]
  .find(x => /搜索添加标签/.test(x.placeholder || ''));

返回 {}------一个空对象,什么都找不到。

我把面板的 DOM 扒出来看,才明白为什么:

html 复制代码
<div class="form-item-content">
  <div class="tag-input select">
    <div class="byte-select">
      <div class="byte-select__content-wrap">
        <div class="byte-select__placeholder">请搜索添加标签</div>
        <input class="byte-select__input">
      </div>
    </div>
  </div>
</div>

那句「请搜索添加标签」根本不是 input 的 placeholder 属性,它是一个兄弟节点 的文本。真正的 input 干干净净,一个属性都没有。

这是一个很典型的组件库做法:占位文案要跟着主题走、要能做动画、要能响应聚焦状态,塞进 placeholder 属性里就不好控制了,于是拿一个 div 盖在上面。肉眼看上去,它就是一个带占位符的输入框;对代码来说,它是一个空输入框加一块贴纸。

改成按结构定位之后,输入框找到了:

js 复制代码
var e = document.querySelector('.tag-input input.byte-select__input');

我往里面灌了「AI编程」,下拉项出来了,我去点它。然后界面上确实出现了「AI编程」四个字。

我一度以为这就成了。但发布还是失败。

真正的判据在 DOM 里:

html 复制代码
<!-- 没选中时 -->
<input class="byte-select__input">   <!-- 里面是文本,不是一个标签 -->

<!-- 选中之后 -->
<span class="byte-select__tag byte-tag byte-tag--checked">
  <span> AI编程 </span>
  <i class="byte-icon byte-icon--close-circle"></i>
</span>

在输入框里敲了字,和标签被选中,是两件完全不同的事。 前者只是输入框的值,你一点别的地方它就散了;后者才是一个真实的、会被提交的数据对象,它有一个 × 可以删掉。

界面上两种状态长得几乎一模一样------都显示「AI编程」四个字。唯一的区别是:一个旁边有个小叉,一个没有。

这就是我想说的第二个问题:自动化里最危险的不是报错,是"看起来对了"。


三、判据是有等级的

这两次失败其实指向同一件事:我需要一个不会骗我的判断依据。

复盘下来,我把用过的判据分成三档。

弱判据:文字。 "页面上出现了这个词,说明成功了。" 这次我踩了个特别蠢的坑------我写了个 toast 抓取逻辑,把页面上所有包含「请 / 失败 / 成功 / 重试」的短文本收集起来当提示看。结果抓出来这么一串:

css 复制代码
["保存成功", "请搜索添加标签", "请搜索添加专栏...", "请搜索添加话题...", "我在知乎补发一篇稿子,连着失败了四次。", "近期发布频率过高,请24小时后重试~", "### 成功判据不要看按钮文案"]

这里面只有第一个「保存成功」是真的提示。剩下的全是正文------因为我这篇稿子写的就是知乎的限流提示,原文里引用了那句「近期发布频率过高,请24小时后重试~」。我的抓取逻辑不区分正文和提示层,于是一股脑全捞了回来。

弱判据的问题不是它经常错,是它错了你不知道。你会拿着一堆噪音去推理,越推越偏。

中判据:DOM 状态和接口响应。 "页面上出现了 span.byte-select__tag,说明标签选上了。" "接口返回了 err_no: 0,说明发布成功了。"

这两个比文字可靠得多,但都有前提。DOM 状态要求你真的知道 成功态长什么样------我如果不知道有个 byte-select__tag,就只能靠眼睛看有没有「AI编程」四个字。接口响应要求你抓得到------这次我信心满满地开了 Network.enable,监听 loadingFinished 之后调 getResponseBody,想直接把服务端的 err_msg 打出来。结果 API_BODIES 是空的,一个都没抓着。

原因有两个:一是我的 URL 正则没覆盖到实际请求路径;二是 loadingFinished 之后响应体不保证还留在缓存里,尤其请求很快的时候。想抓响应体,更稳的做法是 Fetch.enable 在请求阶段就拦下来,或者干脆别抓,换个判据。

强判据:结果的对外可见状态。 "URL 跳到了 juejin.cn/published。" "用另一个标签页打开文章页,标题在,正文 4762 字。"

这是最高一档。它不看过程,只看终态;它不看页面上写了什么,只看外部世界变成什么样了。这次真正让我确认发布成功的,就是最后读到的那一行:

arduino 复制代码
URL_AFTER https://juejin.cn/published

前面所有的 PUB_CLICK clicked、所有的「界面上有 AI 编程四个字」,加起来都不如这一行有说服力。


四、把坐标换成结构

回到坐标的问题。既然写死像素是债,那该怎么写?

答案是:描述你想点的东西,而不是它在哪。

具体有三层,从糙到细。

第一层:按可见文本找。

js 复制代码
function rectOfText(txt) {
  var els = [...document.querySelectorAll('button,span,div,li,a,label')];
  for (var e of els) {
    var r = e.getBoundingClientRect();
    if (r.width < 2 || r.height < 2) continue;          // 隐藏的
    if (r.y < 0 || r.y > innerHeight) continue;          // 视口外的
    if ((e.textContent || '').trim() !== txt) continue;  // 文本不匹配
    return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
  }
  return null;
}

这一层最通用。它不需要你知道组件库的内部类名,只要按钮上写着「确定并发布」,你就能找到它。过滤掉 width < 2 的隐藏副本是关键------React 组件经常在 DOM 里留一份不在屏幕上的镜像,不过滤就会点到空气。

第二层:按结构类名找。

当你需要的元素没有文字(比如一个空输入框),就得靠结构:

js 复制代码
var e = document.querySelector('.tag-input input.byte-select__input');

这里的 .tag-input 是业务语义("标签输入区"),.byte-select__input 是组件库约定("输入框")。两个拼起来,比任何一个单独用都稳。

第三层:验证状态类。

找到还不够,点完之后要看状态有没有变:

js 复制代码
// 分类:看 active 类落在谁身上
var cat = document.querySelector('.category-list .item.active');

// 标签:看 tag 元素有没有生成
var chip = document.querySelector('.tag-input .byte-select__tag');

组件库一定会把状态暴露成类名,因为 CSS 要用它来画样式。这是免费的可观测性,不用就亏了。

三层加起来,逻辑就从「点 (890, 210)」变成了「找到写着『人工智能』的分类项 → 点它 → 确认 .item.active 现在是它」。屏幕怎么变,这段代码都不用改。


五、一个能复用的动作模板

把上面这些串起来,我现在每个关键动作都写成同一个形状:做之前先找,做之后先验。

js 复制代码
async function act(name, findJs, verifyJs, maxRetry) {
  for (var i = 0; i < (maxRetry || 3); i++) {
    var p = JSON.parse(await ev(findJs));
    if (!p.x) { console.log(name, 'NOT_FOUND'); return false; }
    await click(p.x, p.y);
    await sleep(1200);
    var ok = await ev(verifyJs);
    console.log(name, 'TRY' + i, 'OK=' + ok);
    if (ok === true || ok === 'true') return true;
  }
  console.log(name, 'FAILED_AFTER_RETRY');
  return false;
}

用起来是这样:

js 复制代码
await act('分类', 
  findByText('人工智能'), 
  `(document.querySelector('.category-list .item.active')||{}).textContent.trim()==='人工智能'`);

await act('标签',
  findBySel('.tag-input input.byte-select__input'),
  `!!document.querySelector('.tag-input .byte-select__tag')`);

这个模板的价值不在它多聪明,在于它把失败提前了。

原来的写法里,坐标错了 → 点了个空 → 脚本继续往下走 → 点「确定并发布」→ 服务端拒绝 → 脚本退出码 0。你拿到的是一个"跑完了但没生效"的结果,得自己去猜是哪一步出问题。

换成这个模板之后,坐标错了 → 第一行的 TRY0 OK=false 就告诉你了。失败发生在离原因最近的地方。

判据的另一个作用,是把"事后猜"变成"当场报"。


六、为什么自动化特别容易栽在这上面

写到这里我想多说一句:这两个坑,人操作都不会踩。

人不会按坐标点按钮。人点「人工智能」,是因为他看见了那三个字在那儿。屏幕分辨率变了,他照样点得中------他找的是字,不是位置。

人也不会把"输入框里有字"当成"标签已选中"。人会看见那个小叉,知道自己选上了;没看见,就知道还差一步。

自动化难的从来不是"让它做",而是"让它知道自己做得对不对"。 人手上的每一个动作,都伴随着一次无意识的确认------眼睛扫一下,状态就知道变了没有。而脚本没有这双眼睛,你得亲手给它装上。

所以我现在写脚本,花在"怎么验证"上的时间,比花在"怎么点"上的多。


七、一份自查清单

最后一轮下来,我给自己留了这份清单,每次写新平台的脚本都过一遍:

关于定位

  • 代码里有没有写死的像素坐标?有就换成按文本或类名找
  • 找元素时有没有过滤 width < 2 的隐藏副本?
  • 有没有过滤视口外的元素(r.y > innerHeight)?
  • 空输入框找不到时,占位文案是不是藏在兄弟节点里?

关于验证

  • 每个关键动作之后,有没有一个不依赖文字的状态判据?
  • 这个判据能不能区分"我做了一半"和"我做完了"?(比如输入框有字 vs 生成了 tag 元素)
  • 兜底判据是什么?(这次是 URL 跳 published)

关于误报

  • 抓提示信息时,有没有限定 position: fixed/absolute?
  • 有没有排除正文区域?(我那篇讲限流的稿子,正文里全是"请24小时后重试")
  • 抓接口响应时,正则是不是覆盖得太窄?

关于环境

  • 脚本里的坐标是在什么视口下取的?现在还是那个视口吗?

那天凌晨最后跑成功的输出是这样的:

objectivec 复制代码
CAT_ACTIVE 人工智能
TAGOPT {"t":"AI编程","x":1063,"y":360}
CHIP1 AI编程 AI编程
CONFIRM {"x":1285,"y":830}
URL_AFTER https://juejin.cn/published

URL_AFTER 那一行之前的所有输出,都只是过程。真正说明问题的,只有最后那一行。

判断一件事成没成,永远去看它对外部世界造成了什么改变,而不是去看它自己说它做了什么。

这句话放在自动化里成立,放在很多别的地方,好像也成立。

相关推荐
小虎AI生活2 小时前
AI 内容标识新规下的个人内容产线:一套每天 5 分钟的口播产稿法(附提示词)
aigc·ai编程
长明2 小时前
关于 AI 记忆系统的一轮思考
aigc·ai编程
文谦南宁AI普工2 小时前
越权链的三个可检测点:从"检测意图"转向"检测角色被使用"(附可运行检测器)
aigc·ai编程
HjhIron3 小时前
从零手写多Agent编排框架:AI Agent Harness 工程化实战指南
ai编程
文谦南宁AI普工3 小时前
从 least privilege 到 least agency:给 Agent 的凭证做一次"不该有权限"的审计
aigc·ai编程
疯狂的魔鬼3 小时前
写给 AI 的约束,为什么写了 40 条也没用
ai编程·vibecoding
用户539418729073 小时前
Cursor 用着用着就抽风?补全失灵、Agent 乱改、一直转圈、额度狂掉,我整理了一张对症排查表
ai编程·cursor
ZzT3 小时前
一周涨 24 万星的 REA,新手和逆向老手为什么评价相反
安全·ai编程