feat(抓取): 元素检查器改成本地版"大图 + 一次取齐"——复刻云检查器的体验

用户要的是 uiauto.dev 那个检查器的体验(左边大图点元素、右边层级树、选完回填),
并希望本地复刻(云页面跨域,拿不到它的选中结果,没法自动回传)。

- core/uiauto_helper.py: 新增 `snapshot(serial)`
  * **一个 u2 连接背靠背** dump_hierarchy() + screenshot():截图与元素树同源同刻,
    不再像原来那样分两个接口取(中间隔着 dump 本身的 1.3~1.8 秒)
  * `_xml_to_node()`:把 u2 的 XML 节点转成 uiautodev 那套结构,直接复用既有的
    选择器建议逻辑(不写第二遍)
  * **双截图校验**:dump 前后各截一张,差异明显就标 `unstable`,让前端明确提示
    "界面在变化中,请停在静止界面再抓",而不是悄悄给一个可能错位的框
- web/tasks_api.py: 新增 `GET /api/uiauto/snapshot`(登录 + 设备权限)
- static/admin/editor.js + templates/admin/monitor.html:
  * 抓取弹窗从 900px 加宽到 1280px,预览列从"固定 320px"改为铺满左侧 →
    **图片按原始分辨率 1:1 显示**(实测 720x1650 缩放 1.000),元素框严丝合缝;
    原来缩到 294px 时框全挤在一起,看着就像错位
  * 工具栏显示 `720x1650 · 247 个元素 · 2370ms`;界面在变时顶部弹黄色提示条
  * 鼠标在图上移动时高亮"最深命中"的元素框,便于确认真要点哪个

实测:抓取弹窗 1:1 显示、222 个框逐一贴合元素、无 JS 报错;
`unstable` 在静止界面为 false。
This commit is contained in:
2026-09-13 21:17:12 +08:00
parent accd06df4b
commit 37a2b1c59b
6 changed files with 190 additions and 58 deletions
+85
View File
@@ -258,3 +258,88 @@ def _extract(root, out):
count_attrs(root)
flatten(root)
# ================== 原生 u2 快照(截图 + 元素树一次取齐) ==================
def _xml_to_node(elem):
"""把 uiautomator2 的 XML 节点转成 uiautodev 那套 {name, properties, children}。
两边的属性名本来就一样(resource-id / text / content-desc / class / bounds 字符串),
所以只要套一层壳,就能原样复用上面的 _extract(选择器建议逻辑不用写第二遍)。
"""
props = dict(elem.attrib)
return {"name": props.get("class", ""), "properties": props,
"children": [_xml_to_node(c) for c in list(elem)]}
def _shots_differ(a, b, threshold=8):
"""两张截图是不是"明显不一样"(用来判断抓取期间界面有没有在动)。
逐像素求差的包围盒 → 用"变化区域占比"判断,避免个别像素抖动误报。
"""
try:
from PIL import ImageChops
if a.size != b.size:
return True
diff = ImageChops.difference(a.convert("RGB"), b.convert("RGB"))
bbox = diff.getbbox()
if not bbox:
return False
area = (bbox[2] - bbox[0]) * (bbox[3] - bbox[1])
total = a.size[0] * a.size[1]
# 变化面积超过阈值(默认 8%)才算"界面在动"
return (area / total) * 100.0 > threshold
except Exception:
return False
def snapshot(serial, quality=85):
"""一次取齐:设备截图 + 元素树(**同一个 u2 连接、背靠背取**)。
为什么不用现在的"两个接口":截图和元素树分两次取时,中间隔着 dump 本身的
1.3~1.8 秒;界面只要在动画(信息流/视频/加载),框就会落在旧位置上。
顺带做**双截图校验**:dump 前后各截一张,两张差得多就标 `unstable`,
让前端明确提示"界面在变化中,请停在静止界面再抓"——而不是悄悄给一个错位的框。
返回 (ok, 数据 | 错误信息)。数据形如:
{"image": "data:image/jpeg;base64,…", "width": 720, "height": 1650,
"elements": [...], "unstable": false, "cost_ms": 1800}
"""
import base64
import io
import time as _t
import xml.etree.ElementTree as ET
import uiautomator2 as u2
t0 = _t.time()
try:
d = u2.connect(serial)
img1 = d.screenshot() # 先截:用户看到的就是这一刻
xml = d.dump_hierarchy() # 慢的一步(1.3~1.8s)
img2 = d.screenshot() # 再截:和第一张比对
except Exception as e:
return False, f"抓取失败: {e}"
unstable = _shots_differ(img1, img2)
if not unstable:
img2.close()
try:
buf = io.BytesIO()
img1.convert("RGB").save(buf, format="JPEG", quality=quality)
image = "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode()
w, h = img1.size
except Exception as e:
return False, f"截图编码失败: {e}"
elements = []
try:
root = ET.fromstring(xml)
_extract(_xml_to_node(root), elements)
except Exception as e:
return False, f"元素树解析失败: {e}"
return True, {"image": image, "width": w, "height": h,
"elements": elements, "unstable": unstable,
"cost_ms": int((_t.time() - t0) * 1000)}