
1. 为什么你的 AutoJs 脚本总是点不到控件写 AutoJs 脚本最让人抓狂的瞬间不是语法报错而是脚本跑完了、日志也没红但屏幕上那个按钮纹丝不动。你盯着代码里的id(xxx).findOne().click()反复确认明明昨天还能用今天打开 App 就失效了。这类问题九成出在控件定位环节而不是点击动作本身。AutoJs 的控件操作建立在安卓无障碍服务之上核心链路是UiSelector 负责“描述你要找什么”UiObject 负责“拿到这个控件后干什么”。UiSelector 是一组筛选条件的集合你可以按 text、desc、id、className、depth、index 等属性层层过滤UiObject 则是筛选结果的对象封装能读取属性、执行点击、长按、setText也能向上找 parent、向下找 child。理解这两者的分工是排查定位失效的前提。这篇内容面向正在写安卓自动化脚本、被控件定位反复折磨的开发者。我会先讲清楚 UiSelector 的筛选逻辑和 UiObject 的操作边界再给出可直接复制的选择器配置片段和验证步骤最后说明如何通过 TaoToken 统一 Key 与 API 通道完成接口调用配置让脚本里的网络请求部分也能稳定跑通。整套流程你可以跟着做不需要 root 权限。2. UiSelector 与 UiObject 的定位原理拆解2.1 UiSelector 的筛选条件是怎么叠加的UiSelector 不是“找到唯一控件”的魔法它更像 SQL 的 where 子句。你调用text(登录)时它返回的是一个选择器对象此时还没有真正去屏幕里找。只有调用findOne()、findOnce()、find()、exists()这些终结方法时才会触发一次遍历。多个条件之间是 AND 关系。比如let selector id(com.example.app:id/btn_login) .className(android.widget.Button) .text(登录) .clickable(true);这段代码的意思是id 是 btn_login、类名是 Button、文本是“登录”、且可点击的控件。四个条件同时满足才会命中。条件越多越精确但也越容易因为某个属性变化而整体失效。实际调试时我建议先用最少的条件定位再逐步加约束。几个高频属性的含义需要记牢text是控件显示的文字desc是 contentDescription无障碍描述id是资源 IDclassName是控件类型depth是控件在树中的层级index是同级中的序号。其中id最稳定text最容易变depth和index最脆弱——界面稍微改版就会错位。2.2 UiObject 能做什么、不能做什么UiObject 是选择器命中后的对象。拿到它之后你可以let obj id(com.example.app:id/btn_login).findOne(); if (obj) { log(文本 obj.text()); log(类名 obj.className()); log(是否可点击 obj.clickable()); obj.click(); }注意findOne()默认会等待一段时间约 1 秒找不到返回 null。如果你确定控件已经存在用findOnce()更快但它不等待。find()返回的是 UiCollection可以遍历。UiObject 不能凭空创建也不能跨进程操作。它只能操作当前无障碍服务能看到的控件。如果目标控件在 WebView 里、在游戏自绘引擎里、或者被系统权限隔离UiObject 就无能为力。这时候需要换思路比如用坐标点击或图像识别。2.3 控件树层级与 depth 的坑很多失效案例源于 depth 写死。比如你昨天用depth(22)定位到了通讯录里的某个 TextView今天 App 更新加了一层容器depth 变成 23脚本就找不到。depth 是相对根节点的层数任何布局调整都会影响它。更稳的做法是用id加className组合或者用textContains、textStartsWith做模糊匹配。如果必须用层级可以用child()、parent()做相对定位let parent id(com.example.app:id/list_container).findOne(); let target parent.child(3).findOne();这样即使整体层级变了只要父子关系不变定位依然有效。3. 可复制的 UiSelector 配置与 TaoToken 接入片段3.1 一份可直接套用的选择器配置下面这段配置覆盖了常见的定位场景你可以按需删减。建议把它放在脚本开头的配置区方便统一维护// selector-config.js const SELECTORS { // 登录按钮优先 id兜底 text loginBtn: () id(com.example.app:id/btn_login) .className(android.widget.Button) .findOne(), // 搜索框用 desc 定位避免 text 被清空后失效 searchBox: () desc(搜索).className(android.widget.EditText).findOne(), // 列表项用 textContains 模糊匹配 listItem: (keyword) textContains(keyword) .className(android.widget.TextView) .findOne(), // 弹窗关闭按钮用 depth 加 index 兜底 closeBtn: () depth(12).index(2).clickable(true).findOne() }; // 使用示例 let btn SELECTORS.loginBtn(); if (btn) { btn.click(); toast(已点击登录); } else { toast(未找到登录按钮请检查选择器); }这份配置的关键点是每个选择器都返回 UiObject 或 null调用方必须做空值判断。我见过太多脚本直接.click()链式调用一旦 findOne 返回 null 就抛异常中断。3.2 TaoToken 统一 Key 与 API 通道配置脚本里如果需要调用大模型接口做文本理解、控件语义匹配或者把运行日志上报到统一平台就需要一个稳定的 API 通道。TaoToken 提供统一的 Key 管理和 API 入口避免在脚本里散落多个平台的密钥。配置分两步。第一步在控制台创建 API Key第二步在脚本里通过 HTTP 请求调用。AutoJs 内置了http.post方法可以直接发请求// taotoken-config.js const TAOTOKEN_BASE_URL https://taotoken.net/api; const TAOTOKEN_API_KEY sk-你的Key; // 从控制台复制 function callTaoToken(messages) { let url TAOTOKEN_BASE_URL /v1/chat/completions; let headers { Content-Type: application/json, Authorization: Bearer TAOTOKEN_API_KEY }; let body { model: gpt-4o-mini, messages: messages, temperature: 0.3 }; let res http.postJson(url, body, { headers: headers, timeout: 30000 }); if (res.statusCode 200) { return res.body.json().choices[0].message.content; } else { log(请求失败 res.statusCode res.body.string()); return null; } }如果你用的是 Claude Code 或 Cline 这类编码工具配置方式略有不同。以 Claude Code 为例需要在 settings 里指定 Base URL 和 Key{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的Key } }Model ID 根据你实际使用的模型填写比如claude-sonnet-4-20250514。三件套缺一不可Base URL、Key、Model ID。少任何一个都会报 401 或 model not found。3.3 把接口调用嵌进控件定位流程一个实用场景是当 UiSelector 找不到控件时把当前界面的控件树 dump 出来发给模型做语义分析让它建议更合适的选择器。这样能把调试时间从半小时压缩到几分钟function smartLocate(description) { let obj textContains(description).findOne(); if (obj) return obj; // 兜底dump 控件树交给模型分析 let tree dumpWindowHierarchy(); let suggestion callTaoToken([ { role: system, content: 你是 AutoJs 控件定位助手根据控件树返回最可能的 UiSelector 表达式。 }, { role: user, content: 目标描述 description \n控件树 tree } ]); log(模型建议 suggestion); return null; }这段代码把定位失败从“死胡同”变成了“有反馈的调试过程”。4. 验证请求与成功结果确认4.1 先验证控件定位是否命中写完选择器后不要急着跑完整脚本。先单独验证定位// 验证脚本 let obj id(com.example.app:id/btn_login).findOne(); if (obj) { log(命中控件); log(text obj.text()); log(className obj.className()); log(bounds obj.bounds()); log(clickable obj.clickable()); } else { log(未命中尝试打印控件树); log(dumpWindowHierarchy()); }bounds()返回控件在屏幕上的矩形区域格式是[left, top, right, bottom]。如果 bounds 是[0,0,0,0]说明控件存在但不可见可能是被遮挡或还没渲染完。这时候加sleep(1000)再试。4.2 再验证 TaoToken 接口是否通接口验证用一个最小请求let result callTaoToken([ { role: user, content: 回复 OK 两个字母即可 } ]); log(接口返回 result);如果返回OK说明 Base URL、Key、Model ID 三件套都正确。如果返回 401检查 Key 是否复制完整、有没有多余空格。如果返回 404检查 Base URL 是否写成了https://taotoken.net/api而不是其他路径。如果返回 model not found检查 Model ID 拼写。4.3 完整链路跑通的结果长什么样把控件定位和接口调用串起来后一次成功的运行日志应该是这样的[10:23:01] 命中控件 [10:23:01] text 登录 [10:23:01] bounds [540,1820,1080,1940] [10:23:02] 已点击登录 [10:23:03] 接口返回OK [10:23:03] 脚本执行完成每一步都有明确输出出问题时能快速定位是控件没找到还是接口没通。5. 常见报错与排查对照5.1 控件定位类报错报错Cannot read property click of null这是最常见的。原因是findOne()返回了 null但代码直接调用了.click()。修复方式是加空值判断let obj id(xxx).findOne(); if (obj) { obj.click(); } else { log(控件未找到); }报错local proxy failed或请求超时如果你在脚本里配置了网络请求出现这个报错通常是 Base URL 写错或网络不通。检查TAOTOKEN_BASE_URL是否完整末尾不要多加斜杠。AutoJs 的http.postJson默认超时较短建议显式设置timeout: 30000。报错reading choices这个报错说明接口返回了非预期结构代码却直接读了choices[0]。常见原因是返回了错误信息而不是正常响应。修复方式是先判断 statusCodeif (res.statusCode 200) { let data res.body.json(); if (data.choices data.choices.length 0) { return data.choices[0].message.content; } } log(响应异常 res.body.string()); return null;报错OAuth相关错误如果你用 Claude Code 接入出现 OAuth 报错通常是因为同时配置了官方登录态和自定义 Base URL。需要在 settings 里明确指定ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY并确保没有残留的 OAuth token 冲突。5.2 控件树 dump 为空怎么办有时候dumpWindowHierarchy()返回空字符串。这通常是因为无障碍服务没开启或者当前界面是系统级弹窗。检查手机设置里的无障碍服务是否已启用 AutoJs。如果是系统弹窗UiSelector 可能无法访问需要换用坐标点击。5.3 选择器命中多个控件findOne()只返回第一个匹配项。如果页面上有多个相同 text 的控件你可能点错了。这时候用find()拿到集合再按 index 或 bounds 筛选let collection text(确定).find(); log(命中数量 collection.size()); collection.forEach((item, i) { log(i bounds item.bounds()); }); // 选择第二个 if (collection.size() 1) { collection.get(1).click(); }6. 把定位调试和接口配置固化成流程控件定位这件事靠运气试出来的选择器迟早会失效。更靠谱的做法是把它当成一个可调试、可验证、可回退的工程问题。我的习惯是每个脚本都带一个debug开关打开时打印控件树和 bounds关闭时只跑正常逻辑。这样线上出问题能快速复现。接口部分同理。把 Base URL、Key、Model ID 抽到独立配置文件不要硬编码在业务逻辑里。TaoToken 的统一通道让这件事变简单一个 Key 管多个模型换模型只改 Model ID不用动请求代码。需要创建 Key 的话从控制台的 API Keys 页面生成然后对照接入文档确认路径格式。如果你还在选长期用的编码方案Coding Plan 适合把这类脚本开发和调试固化下来模型对话入口则适合临时验证某个选择器表达式或接口返回是否符合预期。把调试流程跑顺之后你会发现 AutoJs 脚本的稳定性瓶颈往往不在点击动作而在定位策略和网络通道这两端。