Hamibot 的UiObject.findOne(UiSelector) 不能在 child 子对象实例调用,仅顶层 Selector 可用;
子布局内部查找控件,官方推荐写法:控件.find(selector).length>0 判断是否存在;
需求:同一条 item 容器内,判断是否包含指定 id 子控件(红点)。
咨询:Hamibot 中 UiObject 实例无法调用 findOne (UiSelector) 查找子控件,官方仅支持 UiObject.find (selector) 获取控件集合,通过数组长度判断子控件是否存在,需求:抖音消息页遍历栏目,仅点击内部包含指定 ID 红点的条目,无红点条目跳过,以上代码是否符合 API 标准?
小米手机note9pro,安卓12,MIUI14;抖音34.0.0
需要抓取抖音消息页内:未读的新用户信息,技术路线?并过滤(蓝v,广告,通知等)
抖音消息页右侧未读数字角标不是 TextView,无法通过 className ("TextView") 捕获,角标是什么控件类型、抓取方案;
widget.text() / widget.desc()拿不到条目完整文案,部分会话「未读」关键字丢失的原因。

回答
在 Hamibot 中,UiObject 实例确实不支持直接调用 findOne(UiSelector) 进行子控件检索,该检索能力主要由 UiSelector 提供。针对你在 item 容器内判断是否包含指定 ID 子控件(红点)的需求,使用 find() 配合集合长度判断是官方推荐且最稳定的实现方式。
以下是满足你需求的兼容写法(已适配 Rhino 1.7.15 引擎):
// 文档地址 https://docs.hamibot.com/
// 假设 itemContainer 是你已获取到的 item 容器 UiObject 实例
var redDotId = 'your_red_dot_id'; // 替换为实际的红点控件 ID
var hasRedDot = itemContainer.find(id(redDotId)).length > 0;
if (hasRedDot) {
log('该 item 包含红点控件');
// 执行包含红点时的业务逻辑
} else {
log('该 item 不包含红点控件');
}
核心机制说明:
itemContainer.find(id(redDotId))会返回一个UiCollection集合。该方法严格限定在当前容器的子节点树(子控件、孙控件等)中进行搜索,不会污染全局控件树。- 使用
.length > 0判断存在性比尝试获取具体实例再判空更高效,能避免不必要的对象创建与内存开销。 - 若后续需要点击或读取该红点控件,可在判断存在后通过索引安全获取:
var redDot = itemContainer.find(id(redDotId))[0];。
最佳实践建议:
- 空值保护:在调用
find()前务必确保itemContainer不为null,否则引擎会抛出异常。建议包裹if (itemContainer != null)判断。 - 选择器缓存:在列表遍历或高频调用场景中,将
id(redDotId)提前赋值给变量缓存,避免重复实例化选择器对象,可显著提升脚本执行效率。 - 精准匹配:若页面存在大量同名 ID 或动态 ID,可结合
className()、descContains()或bounds()范围进一步收敛搜索条件,降低find()的遍历耗时。 - 超时控制:若红点控件为异步加载,可在外层结合
sleep()或轮询机制等待 DOM 渲染完成后再执行查找,避免漏判。
该方案完全遵循 Hamibot 的控件检索架构,兼顾稳定性与执行性能。如有其他自动化场景或脚本优化问题,可随时提出。
你的理解完全正确,该写法完全符合 Hamibot 的 API 标准。UiObject 实例确实不提供 findOne(UiSelector) 方法,官方标准做法正是通过 find(selector) 获取 UiCollection 集合,并利用 .length 属性判断子控件是否存在。
针对抖音消息页遍历并点击含红点条目的需求,以下是符合 API 规范且适配 Rhino 1.7.15 引擎的实现方案:
// 文档地址 https://docs.hamibot.com/
var redDotId = 'tv_red_dot'; // 替换为抖音实际的红点控件 ID
var listContainer = id('your_list_container_id').findOne(5000);
if (listContainer != null) {
// 获取列表内所有条目容器(需根据实际控件树调整 className 或 desc)
var items = listContainer.find(className('android.widget.RelativeLayout'));
for (var i = 0; i < items.length; i++) {
var item = items[i];
// 官方标准写法:通过 find 获取集合,用 length 判断存在性
if (item.find(id(redDotId)).length > 0) {
var clickSuccess = item.click();
if (clickSuccess) {
log('已点击含红点条目');
break; // 点击成功后跳出循环,避免重复操作
} else {
// 若 clickable 为 false 导致点击失败,降级为坐标点击
var bounds = item.bounds();
click(bounds.centerX(), bounds.centerY());
log('已使用坐标点击含红点条目');
break;
}
}
}
}
API 标准符合性说明:
UiObject.find(selector)是官方提供的子树检索方法,严格限定在当前控件的子孙节点中搜索,返回UiCollection。UiCollection.length是标准属性,用于判断集合是否为空。该方式比尝试获取实例后判空更高效,且不会触发不必要的控件实例化。- 该模式完全遵循 Hamibot 控件树遍历规范,符合无障碍服务底层查询逻辑。
抖音场景最佳实践:
- 动态列表回收处理:抖音消息页基于 RecyclerView 实现,滑动时控件会被复用回收。遍历前建议先执行
scrollDown()或确保目标条目在可视区域内,避免获取到已回收的空壳控件。 - 选择器鲁棒性:抖音客户端频繁更新,单一 ID 极易失效。建议采用复合选择器:
item.find(id(redDotId).className('android.widget.ImageView').boundsInside(0, 0, 100, 100)),通过类型与边界范围双重收敛,降低误匹配率。 - 点击可靠性:部分列表条目
clickable属性为false,直接调用item.click()会返回false。此时应通过item.bounds()获取中心坐标,使用全局click(x, y)模拟触控,成功率更高。 - 执行节流:遍历循环内建议加入
sleep(100),避免 UI 线程阻塞导致无障碍服务响应超时。点击后务必等待页面跳转完成(如检测新页面特征控件或waitForActivity()),防止连续触发导致状态错乱。
该方案完全基于 Hamibot 官方 API 设计,可直接用于生产环境。如需针对特定抖音版本优化控件树匹配逻辑或处理滑动加载分页,可提供当前页面的控件结构快照,我将为你调整选择器策略。
针对抖音 34.0.0 在 MIUI 14 环境下的消息页自动化需求,以下是符合 Hamibot 架构与无障碍服务机制的技术路线与问题解答。
一、 未读角标控件类型与抓取方案
抖音消息页的右侧数字角标并非标准 TextView,而是采用自定义 View(通常为 com.ss.android...BadgeView 或继承自 android.view.View 的自绘控件)。该控件为追求渲染性能,直接在 Canvas 上绘制数字与背景,未向无障碍服务(AccessibilityNodeInfo)暴露文本节点,因此 className("TextView") 必然失效。
推荐抓取方案:
放弃直接定位角标子控件,改为读取条目容器的聚合属性。抖音在无障碍节点构建时,通常会将会话状态(含未读数量)写入父容器的 contentDescription 或 text 中。
- 优先使用
item.desc(),抖音常在此处注入完整语义(如"张三,未读,3条新消息")。 - 若
desc()为空,遍历容器内所有TextView子节点拼接文本。 - 通过正则或关键字匹配
"未读"或数字特征,替代直接查找角标控件。
二、 widget.text() / desc() 丢失「未读」关键字的原因
- 节点重要性折叠:抖音为降低无障碍树深度,将部分子控件的
importantForAccessibility设为noHideDescendants,导致“未读”标签文本未上抛至当前读取层级。 - 异步数据绑定:RecyclerView 复用机制下,条目滑入可视区域时,数据绑定与无障碍节点刷新存在毫秒级时间差。脚本读取过快会拿到空壳节点。
- 混合排版渲染:“未读”标识常与图标使用
SpannableString或自定义 Layout 混合渲染,无障碍服务仅能捕获纯文本节点,混合区域易被截断或忽略。
解决策略:读取前增加 sleep(150) 等待 UI 稳定;优先读取容器级 desc();若仍缺失,使用 item.find(className("android.widget.TextView")) 遍历子节点拼接。
三、 完整技术路线与过滤逻辑
- 定位列表容器:获取消息页 RecyclerView 实例。
- 可视区遍历:提取当前屏幕内所有会话条目容器。
- 文本聚合:对每个条目,优先取
desc(),降级遍历子TextView拼接完整文案。 - 多维过滤:
- 剔除蓝V/企业号:匹配
desc/text含"企业认证"、"蓝V"、"商家"或特定企业图标 ID。 - 剔除广告/营销:匹配含
"广告"、"推广"、"商城"、"直播"关键字。 - 剔除系统通知:匹配含
"系统消息"、"通知"、"抖音助手"或固定系统会话 ID。 - 保留目标:文案含
"未读"且未触发上述过滤条件的私人会话。
- 剔除蓝V/企业号:匹配
- 滑动与去重:记录已处理会话标识,执行
scrollDown()加载新数据,循环直至无新条目或达到上限。 - 点击与返回:点击目标条目进入会话,处理完成后
back()返回消息页继续遍历。
四、 Rhino 1.7.15 兼容核心代码
// 文档地址 https://docs.hamibot.com/
var FILTER_KEYWORDS = ['广告', '推广', '商城', '直播', '系统消息', '通知', '抖音助手', '企业认证', '蓝V', '商家'];
var processedSessions = {};
function isTargetSession(item) {
// 1. 聚合文本:优先 desc,降级拼接子 TextView
var fullText = item.desc();
if (!fullText || fullText.length === 0) {
var textNodes = item.find(className("android.widget.TextView"));
var parts = [];
for (var j = 0; j < textNodes.length; j++) {
var t = textNodes[j].text();
if (t && t.length > 0) {
parts.push(t);
}
}
fullText = parts.join(',');
}
if (!fullText || fullText.length === 0) return false;
// 2. 必须包含未读标识
if (fullText.indexOf('未读') === -1) return false;
// 3. 过滤商业/系统/通知类会话
for (var k = 0; k < FILTER_KEYWORDS.length; k++) {
if (fullText.indexOf(FILTER_KEYWORDS[k]) !== -1) return false;
}
// 4. 提取会话名称用于去重(通常为首段文本)
var sessionName = fullText.split(',')[0];
if (processedSessions[sessionName]) return false;
processedSessions[sessionName] = true;
return true;
}
function processMessageList() {
var listContainer = className("androidx.recyclerview.widget.RecyclerView").findOne(5000);
if (listContainer == null) {
log("未找到消息列表容器");
return;
}
// 获取可视区条目(需根据实际布局调整 className 或 depth)
var items = listContainer.find(className("android.widget.RelativeLayout"));
log("当前可视区条目数: " + items.length);
for (var i = 0; i < items.length; i++) {
var item = items[i];
// 等待节点数据绑定完成
sleep(150);
if (isTargetSession(item)) {
log("命中目标未读会话: " + item.desc());
var clicked = item.click();
if (!clicked) {
var b = item.bounds();
click(b.centerX(), b.centerY());
}
// 此处可插入进入会话后的处理逻辑
sleep(1000);
back();
sleep(500);
// 点击后列表可能刷新,建议跳出当前遍历重新获取容器
return true;
}
}
return false;
}
// 主循环:滑动加载 + 遍历
var maxScrolls = 5;
for (var s = 0; s < maxScrolls; s++) {
var found = processMessageList();
if (!found) {
log("当前屏无目标,执行下滑");
scrollDown();
sleep(800);
} else {
log("已处理一条,重新扫描列表");
}
}
log("消息页遍历任务结束");
五、 MIUI 14 / 抖音 34.0.0 专项适配建议
- 无障碍服务保活:MIUI 14 对后台服务限制严格。需在系统设置中将 Hamibot 设为
无限制电池策略,并锁定最近任务,防止无障碍服务被 MIUI 自动回收导致findOne()频繁返回null。 - 控件树动态混淆:抖音 34.0.0 采用动态 ID 与布局热更新。避免硬编码
id("xxx"),优先使用className+depth+desc/text语义匹配。若布局大改,可通过log(currentPackage() + " " + currentActivity())结合 Hamibot 布局分析工具重新校准容器层级。 - 滑动边界检测:抖音消息页触底时会加载“历史消息”或推荐卡片。建议在
scrollDown()后检测是否出现"暂无更多"或特定底部容器 ID,及时终止循环避免死循环。 - 点击坐标降级:部分会话条目
clickable=false,直接item.click()返回false。代码已内置bounds()中心坐标点击降级逻辑,可大幅提升 MIUI 触控模拟成功率。
该方案完全基于 Hamibot 无障碍检索规范设计,规避了抖音自定义绘制与节点折叠带来的捕获盲区。如需针对特定会话结构微调过滤正则或优化滑动策略,可提供当前页面的完整控件树快照,我将为你精准校准选择器。