diff --git a/core/uiauto_helper.py b/core/uiauto_helper.py index 21b005c..a0d5140 100644 --- a/core/uiauto_helper.py +++ b/core/uiauto_helper.py @@ -112,12 +112,16 @@ def get_elements(serial): return False, f"抓取失败: {e}" -def _flatten(node, out, depth=0, path=""): +def _flatten(node, out, depth=0, path="", ctx=None, tag_index=0): """递归扁平化元素树,提取关键属性供前端选择。 uiautodev dump_hierarchy 返回 Node 格式: {key, name, bounds(归一化浮点), rect({x,y,width,height}), properties({所有属性}), children} 其中 properties 包含原始 XML 属性(resource-id/text/content-desc/class/bounds 字符串等)。 + + ctx: 最近一个有唯一属性(resource-id/text/content-desc)祖先的 xpath。 + 无唯一属性的元素用它限定范围生成精确选择器,避免裸 class(如 //ImageView)过宽误点。 + tag_index: 本节点在父节点同 class 兄弟中的序号(1-based),用于消歧。 """ if not isinstance(node, dict): return @@ -128,51 +132,58 @@ def _flatten(node, out, depth=0, path=""): if not bounds_str and node.get("rect"): r = node["rect"] bounds_str = f"[{r['x']},{r['y']}][{r['x']+r['width']},{r['y']+r['height']}]" - # 关键属性(前端选择器用) + + rid = props.get("resource-id", "") + text = props.get("text", "") + desc = props.get("content-desc", "") + cls = props.get("class", "") + tag = cls.split(".")[-1] if cls else "*" + + # 推荐选择器:唯一属性优先;否则用最近锚点祖先限定范围(带同class序号消歧) + if rid: + suggested = {"type": "xpath", "value": f'//*[@resource-id="{rid}"]'} + child_ctx = suggested["value"] + elif text: + suggested = {"type": "xpath", "value": f'//*[@text="{text}"]'} + child_ctx = suggested["value"] + elif desc: + suggested = {"type": "xpath", "value": f'//*[@content-desc="{desc}"]'} + child_ctx = suggested["value"] + else: + if ctx: + seg = f"/{tag}" + (f"[{tag_index}]" if tag_index > 1 else "") + suggested = {"type": "xpath", "value": f"{ctx}{seg}"} + child_ctx = f"{ctx}{seg}" + else: + # 无唯一属性且无锚点祖先:裸 class 过宽,标记 broad 让前端提示 + suggested = {"type": "xpath", "value": f"//{tag}", "broad": True} + child_ctx = None + item = { "depth": depth, "path": path, "name": name, - "resource_id": props.get("resource-id", ""), - "text": props.get("text", ""), - "description": props.get("content-desc", ""), - "class": props.get("class", ""), + "resource_id": rid, + "text": text, + "description": desc, + "class": cls, "package": props.get("package", ""), "clickable": props.get("clickable", ""), "bounds": bounds_str, - # 推荐选择器(优先级:resource-id > text > description > class) - "suggested": _suggest_selector(props), + # 推荐选择器(resource-id > text > description > 锚点祖先限定class) + "suggested": suggested, } out.append(item) - # 递归子节点 + # 递归子节点,同时计算每个子节点在父下同 class 兄弟中的序号 children = node.get("children") or [] for i, child in enumerate(children): - _flatten(child, out, depth + 1, f"{path}/{i}") - - -def _suggest_selector(attrs): - """根据属性推荐最佳选择器(前端一键填入用)。 - - 优先返回 xpath(最稳定,uiautomator2 原生支持), - 其次 resourceId / text / description / class。 - """ - cls = attrs.get("class", "") - rid = attrs.get("resource-id", "") - text = attrs.get("text", "") - desc = attrs.get("content-desc", "") - # 优先生成 xpath:resource-id 最稳定 - if rid: - # 简短类名(去掉包前缀) - short_cls = cls.split(".")[-1] if cls else "*" - return {"type": "xpath", - "value": f'//*[@resource-id="{rid}"]'} - if text: - return {"type": "xpath", - "value": f'//*[@text="{text}"]'} - if desc: - return {"type": "xpath", - "value": f'//*[@content-desc="{desc}"]'} - if cls: - return {"type": "xpath", - "value": f'//{cls}'} - return {"type": "xpath", "value": "//*"} + cprops = child.get("properties") or {} + ccls = cprops.get("class", "") + ctag = ccls.split(".")[-1] if ccls else "*" + same = 1 + for prev in children[:i]: + pcls = (prev.get("properties") or {}).get("class", "") + ptag = pcls.split(".")[-1] if pcls else "*" + if ptag == ctag: + same += 1 + _flatten(child, out, depth + 1, f"{path}/{i}", child_ctx, same) diff --git a/tasks/douyin/actions/like.py b/tasks/douyin/actions/like.py index a349e26..1224196 100644 --- a/tasks/douyin/actions/like.py +++ b/tasks/douyin/actions/like.py @@ -2,9 +2,9 @@ 两种方式: by_element — 用 xpath 找右侧红心按钮点击(相对定位,不依赖视频卡片序号) - double_click — 双击屏幕中央(兜底,不依赖元素,抖音"双击点赞"手势) + double_click — 双击屏幕中央(需明确选择此方式,抖音"双击点赞"手势) -定位器随抖音版本变,by_element 找不到会自动回退双击,不会卡住。 +by_element 找不到点赞按钮时**跳过本次,不点屏幕**(避免直播/无按钮页误触)。 要适配新版本抖音:用 weditor 重新抓当前界面,调整下面定位逻辑。 """ from core.actions import BaseAction, register_action, should_trigger @@ -33,7 +33,10 @@ class LikeAction(BaseAction): if self._like_by_element(d): worker.set_action("点赞(红心)") return True - # 找不到红心,回退双击 + # 找不到点赞按钮:跳过本次,不点屏幕,避免误触(如直播/无点赞按钮的页面) + worker.set_action("点赞(未找到,跳过)") + return False + # 仅在明确选择 double_click 方式时才双击屏幕 ok = self._double_click_center(d) if ok: worker.set_action("点赞(双击)") diff --git a/templates/admin/monitor.html b/templates/admin/monitor.html index c912794..7368402 100644 --- a/templates/admin/monitor.html +++ b/templates/admin/monitor.html @@ -1888,6 +1888,7 @@ var _stepEditor={ '
'+indent+esc(el.name||el.class||'')+'
'+ '
'+attrs.join('')+'
'+ ''+esc(el.suggested.type)+''+ + (el.suggested&&el.suggested.broad?' ⚠宽泛':'')+ ''; }).join(''); document.getElementById('el-list').innerHTML=html||'
无元素
';