fix: 抓取元素回填垃圾选择器——无属性元素生成 //*[N](匹配任意节点点不到),改为 invalid 标记前端提示不可选;结构路径跳过无 class 层消除 //*[N] 前缀污染;模块拆分遗漏修复(get_all_worker_status/list_installed_apps 致 summary/设备已装应用 500)

This commit is contained in:
2026-08-19 15:10:26 +08:00
parent 265e47a1f4
commit a0916ebda2
3 changed files with 25 additions and 7 deletions
+13 -1
View File
@@ -190,9 +190,14 @@ def _extract(root, out):
desc = props.get("content-desc", "") desc = props.get("content-desc", "")
cls = props.get("class", "") cls = props.get("class", "")
tag = cls.split(".")[-1] if cls else "*" tag = cls.split(".")[-1] if cls else "*"
# 始终带同 class 兄弟序号(含 [1]),结构路径才精确无歧义 # 始终带同 class 兄弟序号(含 [1]),结构路径才精确无歧义;
# class 为空的层(tag=*)不参与结构路径,避免 //*[N] 前缀污染导致选择器定位到任意节点
if tag != "*":
seg = f"{tag}[{tag_index}]" seg = f"{tag}[{tag_index}]"
own_tag_path = f"{tag_path}/{seg}" if tag_path else seg own_tag_path = f"{tag_path}/{seg}" if tag_path else seg
else:
seg = ""
own_tag_path = tag_path
# ---- 推荐选择器:唯一属性 > 锚点祖先限定 > 全结构路径 ---- # ---- 推荐选择器:唯一属性 > 锚点祖先限定 > 全结构路径 ----
if rid: if rid:
@@ -205,6 +210,13 @@ def _extract(root, out):
val = f"{ctx}/{seg}" val = f"{ctx}/{seg}"
suggested = {"type": "xpath", "value": val} suggested = {"type": "xpath", "value": val}
child_ctx = val child_ctx = val
else:
if tag == "*":
# class 也为空的元素:结构路径 //*[N] 无意义(匹配到文档任意节点),
# 标记 invalid,前端提示不可选,避免回填垃圾选择器导致"点击不到"
suggested = {"type": "xpath", "value": "",
"invalid": True,
"reason": "该元素无可用属性(id/文本/class),无法生成可靠选择器"}
else: else:
# 无唯一属性且无锚点祖先:用从根开始的结构路径(脆弱,标记 broad 让前端提示) # 无唯一属性且无锚点祖先:用从根开始的结构路径(脆弱,标记 broad 让前端提示)
suggested = {"type": "xpath", "value": f"//{own_tag_path}", "broad": True} suggested = {"type": "xpath", "value": f"//{own_tag_path}", "broad": True}
+5
View File
@@ -998,6 +998,7 @@ var _stepEditor={
if(el.description)attrs.push('<span>desc:'+esc(el.description)+'</span>'); if(el.description)attrs.push('<span>desc:'+esc(el.description)+'</span>');
var indent=' '.repeat(Math.min(el.depth||0,6)); var indent=' '.repeat(Math.min(el.depth||0,6));
var sg=el.suggested||{}; var sg=el.suggested||{};
if(sg.invalid)attrs.push('<span style="color:#dc2626">⚠ '+esc(sg.reason||'无可用属性')+'</span>');
return '<div class="el-picker-item" data-idx="'+idx+'" title="点击填入: '+esc(sg.value||'')+'" onclick="_stepEditor._pickEl('+idx+')">'+ return '<div class="el-picker-item" data-idx="'+idx+'" title="点击填入: '+esc(sg.value||'')+'" onclick="_stepEditor._pickEl('+idx+')">'+
'<span class="ep-depth">'+(el.depth||0)+'</span>'+ '<span class="ep-depth">'+(el.depth||0)+'</span>'+
'<div class="ep-info"><div class="ep-name">'+indent+esc(el.name||el.class||'')+'</div>'+ '<div class="ep-info"><div class="ep-name">'+indent+esc(el.name||el.class||'')+'</div>'+
@@ -1022,6 +1023,10 @@ var _stepEditor={
_pickEl:function(i){ _pickEl:function(i){
var el=this._elData[i]; var el=this._elData[i];
if(!el||!this._elTargetInput)return; if(!el||!this._elTargetInput)return;
if(el.suggested&&el.suggested.invalid){
showToast(el.suggested.reason||'该元素无法生成选择器','error');
return;
}
// 填入推荐选择器值,并切换选择器类型 // 填入推荐选择器值,并切换选择器类型
this._elTargetInput.value=el.suggested.value; this._elTargetInput.value=el.suggested.value;
var sel=this._elTargetInput.closest('.sc-body').querySelector('[data-key="selector_type"]'); var sel=this._elTargetInput.closest('.sc-body').querySelector('[data-key="selector_type"]');
+3 -2
View File
@@ -9,8 +9,9 @@ from flask import (Blueprint, jsonify, request, Response, render_template)
from flask_login import login_required from flask_login import login_required
from core import device_pool from core import device_pool
from core.adb_helper import (screenshot, adb_connect, adb_connect_light, from core.adb_helper import (screenshot, list_installed_apps, adb_connect,
_ADB_LOCK, _adb) adb_connect_light, _ADB_LOCK, _adb)
from core.device_worker import get_all_worker_status
from web import context from web import context
from web.auth import perm_required, PERM_DEVICES from web.auth import perm_required, PERM_DEVICES
from web.common import _merged_device_list, _device_screen_state from web.common import _merged_device_list, _device_screen_state