feat(抓取): 选择器优先语义化(同 id 多实例用 @text 限定,而非序号)+ 修掉两类"死选择器"

语义消歧(B 项):主属性在整棵树里重复时,先找第二个属性把目标单独圈出来 ——
  //*[@resource-id="x" and @text="我"]   (次属性 text > content-desc > class,
                                        单个不够就两两组合)
标 semantic:true + via;只有组合也分不开(列表里同 id 同文字)才退回
  (//*[@resource-id="x"])[k]            (标 indexed,前端黄标提醒脆弱)
抖音底部导航正是这个场景:tab 个数随灰度版本变(4 个 ↔ 3 个),序号必然错位。

顺带修掉两个结构性缺陷(给上面做验证时逐条 lxml 求值发现的,均非本次引入):
1. 结构步进把 class 当标签名 —— dump 的 XML 标签**一律是 <node>**,class 在
   @class 上,所以 //FrameLayout[1]/… 这类路径**永远零命中**;改 *[@class="…"][n]
2. 兜底结构路径用 @index 定位兄弟 —— 实测同级 index 会重复(状态栏/内容区/
   导航栏三个兄弟全是 index="0");改按子节点位置 //hierarchy/*[1]/*[2]

前端:抓取列表把 text/content-desc 排到最前并加粗上色(最稳的定位依据);
序号型从蓝标改**黄标 ⚠ 序号 k/n**,语义型给**绿标 ✓ 语义**;属性页新增
「选择器稳定性」一行说明这个选择器靠什么定位、会不会因界面变化失效。

真机实测(192.168.20.100,248 个元素):
  精确命中目标 221 → 247 | 死选择器 26 → 0 | 语义型 0 → 26(序号型 141 → 115)
  「我」的语义选择器经 /api/steps/test 真机点击 → 命中 ✓

文档:TASK_DEV §5.3/5.4(含两个 XPath 坑)、API §8(suggested 字段表 +
snapshot 行)、research/U2_ELEMENT_SELECTORS §五/§六、backlog ②标记完成。
This commit is contained in:
2026-09-13 22:05:47 +08:00
parent 4ea777a55a
commit 0bc713137d
7 changed files with 203 additions and 47 deletions
+13 -5
View File
@@ -882,7 +882,12 @@ var _stepEditor={
if(!box)return;
if(!el){box.innerHTML='<div class="se-empty">点左侧图上的元素,或点「层级」里的条目看属性</div>';return;}
var sg=el.suggested||{};
var rows=[['建议选择器', sg.value||'(无可靠选择器)'],
// 稳定性说明:把"这个选择器靠什么定位"写清楚,用户才知道它会不会因为界面变化而失效
var stab=sg.semantic?'✓ 语义型 —— 靠 @'+(sg.via||'')+' 限定,不依赖同类元素个数,换设备/换版本仍命中'
:sg.indexed?'⚠ 序号型 —— 依赖「同类元素共 '+sg.total+' 个、取第 '+sg.occ+' 个」,界面一变就失配'
:sg.broad?'⚠ 宽泛 —— 无唯一属性,可能误匹配其他元素'
:(sg.value?'✓ 唯一属性,直接定位':'—');
var rows=[['建议选择器', sg.value||'(无可靠选择器)'],['选择器稳定性', stab],
['class', el.class||''], ['resource-id', el.resource_id||''],
['text', el.text||''], ['content-desc', el.description||''],
['package', el.package||''], ['clickable', el.clickable||''],
@@ -1103,10 +1108,12 @@ var _stepEditor={
var self=this;
var html=list.map(function(el){
var idx=self._elData.indexOf(el);
// 属性顺序有讲究:**文字/描述排最前并加粗**——它们是最稳的定位依据,
// 也是"一眼认出这是哪个元素"的凭据(id 只是补充)。见 doc/TASK_DEV.md §5.4。
var attrs=[];
if(el.resource_id)attrs.push('<span>id:'+esc(el.resource_id)+'</span>');
if(el.text)attrs.push('<span>text:'+esc(el.text)+'</span>');
if(el.description)attrs.push('<span>desc:'+esc(el.description)+'</span>');
if(el.text)attrs.push('<span class="ep-text">text:<b>'+esc(el.text)+'</b></span>');
if(el.description)attrs.push('<span class="ep-desc">desc:<b>'+esc(el.description)+'</b></span>');
if(el.resource_id)attrs.push('<span class="ep-id">id:'+esc(el.resource_id)+'</span>');
var indent=' '.repeat(Math.min(el.depth||0,6));
var sg=el.suggested||{};
if(sg.invalid)attrs.push('<span style="color:#dc2626">⚠ '+esc(sg.reason||'无可用属性')+'</span>');
@@ -1125,7 +1132,8 @@ var _stepEditor={
'<div class="ep-info"><div class="ep-name">'+indent+esc(el.name||el.class||'')+'</div>'+
'<div class="ep-attrs">'+attrs.join('')+'</div></div>'+
'<span class="ep-sel">'+esc(sg.type)+'</span>'+
(sg.indexed?' <span class="label label-info" title="界面中同属性元素共 '+sg.total+' 个,按从上到下这是第 '+sg.occ+' 个,选择器已带序号精确定位">#'+sg.occ+'/'+sg.total+'</span>':'')+
(sg.semantic?' <span class="label label-success" title="语义选择器:用 '+esc(sg.via||'')+' 限定,只认「这个元素长什么样」,不认「同类元素有几个」—— 换设备、换版本、界面增减元素都照样命中">✓ 语义</span>':'')+
(sg.indexed?' <span class="label label-warning" title="⚠ 序号型:依赖「同属性元素共 '+sg.total+' 个、这是第 '+sg.occ+' 个」。界面上多/少一个同类元素(如抖音灰度版少一个 tab)就会指到别的元素上 —— 建议改选带 text 或 content-desc 的元素,拿到语义选择器">⚠ 序号 '+sg.occ+'/'+sg.total+'</span>':'')+
(sg.broad?' <span class="label label-warning" title="无唯一属性,选择器可能误匹配其他元素,建议选带文字/id 的元素">⚠宽泛</span>':'')+
acts+
'</div>';