From b126ce2d49c8d3610005c0010e63ba1c0cc20a5a Mon Sep 17 00:00:00 2001 From: empty Date: Sun, 28 Dec 2025 20:47:43 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E6=8E=A2=E7=B4=A2?= =?UTF-8?q?=E5=99=A8=E5=85=83=E7=B4=A0=E5=AE=9A=E4=BD=8D=E7=B2=BE=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 改进 _find_element_by_name: - 使用直接文本匹配 + 短文本匹配策略 - 检查 cursor:pointer 样式 - 按元素面积排序,选择最精确匹配 - 限制元素大小避免匹配到容器 - 转义特殊字符防止 JS 注入 坐标定位效果: - 之前: 所有元素 (960, 540) - 现在: 分析概览 (40,105), 系统管理 (40,551), 搜索 (1634,25) --- src/agent/explorer.py | 72 +++++++++++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/src/agent/explorer.py b/src/agent/explorer.py index c81e367..571d2f0 100644 --- a/src/agent/explorer.py +++ b/src/agent/explorer.py @@ -243,34 +243,68 @@ class FeatureExplorer: def _find_element_by_name(self, name: str) -> Optional[tuple]: """通过元素名称/文本查找坐标(使用 DOM 而非 AI,更快)""" + # 转义特殊字符 + escaped_name = name.replace("\\", "\\\\").replace('"', '\\"').replace("'", "\\'") + try: result = self.browser.page.evaluate(f''' () => {{ - // 搜索包含该文本的可点击元素 - const text = "{name}"; - const clickable = ['A', 'BUTTON', 'INPUT', 'LI', 'SPAN', 'DIV']; + const searchText = "{escaped_name}"; + const clickable = ['A', 'BUTTON', 'INPUT', 'LI', 'SPAN', 'DIV', 'NAV', 'LABEL']; - // 遍历所有元素 + // 收集所有匹配的元素 + const matches = []; const all = document.querySelectorAll('*'); + for (const el of all) {{ - if (el.textContent && el.textContent.trim().includes(text)) {{ - if (clickable.includes(el.tagName) || - el.onclick || - el.getAttribute('role') === 'button' || - el.getAttribute('role') === 'menuitem' || - el.classList.contains('clickable')) {{ - const r = el.getBoundingClientRect(); - if (r.width > 0 && r.height > 0) {{ - return {{ - found: true, - x: Math.round(r.left + r.width / 2), - y: Math.round(r.top + r.height / 2) - }}; - }} + // 只检查直接文本内容,不包含子元素的文本 + const directText = Array.from(el.childNodes) + .filter(n => n.nodeType === 3) // 文本节点 + .map(n => n.textContent.trim()) + .join(''); + + // 或者元素的完整文本很短(小于搜索文本的2倍) + const fullText = el.textContent?.trim() || ''; + + // 匹配条件 + const hasDirectMatch = directText.includes(searchText); + const hasShortMatch = fullText.includes(searchText) && fullText.length < searchText.length * 3; + + if ((hasDirectMatch || hasShortMatch) && + (clickable.includes(el.tagName) || + el.onclick || + el.getAttribute('role') === 'button' || + el.getAttribute('role') === 'menuitem' || + el.getAttribute('role') === 'link' || + window.getComputedStyle(el).cursor === 'pointer')) {{ + + const r = el.getBoundingClientRect(); + if (r.width > 0 && r.height > 0 && r.width < 800 && r.height < 200) {{ + matches.push({{ + el: el, + rect: r, + textLen: fullText.length, + area: r.width * r.height + }}); }} }} }} - return {{ found: false }}; + + if (matches.length === 0) {{ + return {{ found: false }}; + }} + + // 选择面积最小的匹配元素(最精确) + matches.sort((a, b) => a.area - b.area); + const best = matches[0]; + + return {{ + found: true, + x: Math.round(best.rect.left + best.rect.width / 2), + y: Math.round(best.rect.top + best.rect.height / 2), + tagName: best.el.tagName, + text: best.el.textContent.substring(0, 50) + }}; }} ''') if result.get("found"):