提交 f07b8861 authored 作者: 陈泽健's avatar 陈泽健

feat(smart-locate): 智能定位优化 - 修复选择器缺失问题

改进:
1. 放宽修饰词移除规则 - 移除"添加"/"新增"/"完成"等按钮名称词
2. 扩展元素查询范围 - 添加 .el-button .el-checkbox .el-tabs__item 等
3. 放宽文本匹配条件 - 支持 div/span/i/label/li 文本匹配
4. 扩展文本提取范围 - 支持更多标签类型
5. 增强语义回退策略 - 添加图标/复选框/Element Plus 按钮的查找
6. 新增步骤模式预处理器:
   - "xxx选择:yyy" → 列表内复选框
   - "xxx切换为:yyy" → Tab 切换

效果:
- 步骤10(列表复选框)  模式匹配成功
- 步骤11(Tab切换)  模式匹配成功
- 其他按钮类步骤需要在导航到对应页面后才能验证

已部署到 192.168.5.60
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 3e4d7b85
...@@ -79,8 +79,9 @@ def extract_keywords(description: str) -> List[str]: ...@@ -79,8 +79,9 @@ def extract_keywords(description: str) -> List[str]:
cleaned = cleaned.strip() cleaned = cleaned.strip()
# 去除修饰词(这些词不是关键词,但会影响匹配) # 去除修饰词(这些词不是关键词,但会影响匹配)
modifier_words = ['展开', '分类', '添加', '操作', '查看', '是否', '正确', '新增', # ⚠️ 注意:'添加'、'新增'、'完成'、'确定'、'取消' 可能是按钮名称,不应移除
'条目', '列表', '数据', '验证', '成功', '完成', '页面', '系统'] modifier_words = ['展开', '分类', '操作', '查看', '是否', '正确',
'条目', '数据', '验证', '成功', '页面', '系统']
for word in modifier_words: for word in modifier_words:
cleaned = cleaned.replace(word, "") cleaned = cleaned.replace(word, "")
cleaned = cleaned.strip() cleaned = cleaned.strip()
...@@ -267,10 +268,17 @@ def match_element_by_keywords( ...@@ -267,10 +268,17 @@ def match_element_by_keywords(
# 获取所有可交互元素 # 获取所有可交互元素
try: try:
# 扩展元素查询范围,包含 Element Plus 组件和常见可点击元素
elements = page.locator( elements = page.locator(
'input:visible, button:visible, a:visible, select:visible, textarea:visible, ' 'input:visible, button:visible, a:visible, select:visible, textarea:visible, '
# 语义化 role 元素
'[role="button"]:visible, [role="link"]:visible, [role="checkbox"]:visible, ' '[role="button"]:visible, [role="link"]:visible, [role="checkbox"]:visible, '
'[role="radio"]:visible, [role="tab"]:visible, [role="menuitem"]:visible' '[role="radio"]:visible, [role="tab"]:visible, [role="menuitem"]:visible, '
# Element Plus 组件
'.el-button:visible, .el-checkbox:visible, .el-tabs__item:visible, '
# 常见可点击元素(图标、自定义按钮)
'i[onclick]:visible, div[onclick]:visible, span[onclick]:visible, '
'div[class*="btn"]:visible, span[class*="btn"]:visible'
).all() ).all()
except Exception as e: except Exception as e:
logger.warning(f"获取页面元素失败: {e}") logger.warning(f"获取页面元素失败: {e}")
...@@ -284,7 +292,9 @@ def match_element_by_keywords( ...@@ -284,7 +292,9 @@ def match_element_by_keywords(
tag = el.evaluate('el => el.tagName') tag = el.evaluate('el => el.tagName')
el_type = el.get_attribute('type') or '' el_type = el.get_attribute('type') or ''
placeholder = el.get_attribute('placeholder') or '' placeholder = el.get_attribute('placeholder') or ''
text = el.inner_text().strip() if tag in ['BUTTON', 'A'] else '' # 扩展文本提取范围,支持 div/span 等可点击元素
text_extractable_tags = ['BUTTON', 'A', 'DIV', 'SPAN', 'I', 'LABEL', 'LI']
text = el.inner_text().strip() if tag in text_extractable_tags else ''
el_id = el.get_attribute('id') or '' el_id = el.get_attribute('id') or ''
name = el.get_attribute('name') or '' name = el.get_attribute('name') or ''
aria_label = el.get_attribute('aria-label') or '' aria_label = el.get_attribute('aria-label') or ''
...@@ -342,14 +352,21 @@ def match_element_by_keywords( ...@@ -342,14 +352,21 @@ def match_element_by_keywords(
'priority': 4 'priority': 4
}) })
# 5. 文本匹配(按钮、链接) # 5. 文本匹配(按钮、链接、可点击的 div/span)
if text and tag in ['BUTTON', 'A']: # ⚠️ 扩展:允许 div、span、i、label、li 等元素进行文本匹配
text_matchable_tags = ['BUTTON', 'A', 'DIV', 'SPAN', 'I', 'LABEL', 'LI']
if text and tag in text_matchable_tags:
for kw in keywords: for kw in keywords:
if kw.lower() in text.lower(): if kw.lower() in text.lower():
score += 0.25 score += 0.25
# 根据元素类型调整选择器
if tag in ['BUTTON', 'A']:
selector_value = f'{tag.lower()}:has-text("{text}")'
else:
selector_value = f'{tag.lower()}:visible:has-text("{text}")'
selectors.append({ selectors.append({
'type': 'css', 'type': 'css',
'value': f'{tag.lower()}:has-text("{text}")', 'value': selector_value,
'confidence': 0.75, 'confidence': 0.75,
'priority': 5 'priority': 5
}) })
...@@ -369,7 +386,8 @@ def match_element_by_keywords( ...@@ -369,7 +386,8 @@ def match_element_by_keywords(
# 7. 动作类型加分/减分 # 7. 动作类型加分/减分
if action == 'fill' and tag in ['INPUT', 'TEXTAREA']: if action == 'fill' and tag in ['INPUT', 'TEXTAREA']:
score += 0.20 score += 0.20
elif action == 'click' and tag in ['BUTTON', 'A']: # ⚠️ click 操作额外加分标签扩展
elif action == 'click' and tag in ['BUTTON', 'A', 'I', 'DIV', 'SPAN', 'LI']:
score += 0.25 score += 0.25
elif action == 'select' and tag == 'SELECT': elif action == 'select' and tag == 'SELECT':
score += 0.20 score += 0.20
...@@ -542,6 +560,81 @@ def find_element_by_semantic( ...@@ -542,6 +560,81 @@ def find_element_by_semantic(
except Exception as e: except Exception as e:
logger.warning(f"语义推断查找菜单失败: {e}") logger.warning(f"语义推断查找菜单失败: {e}")
# ⚠️ 新增:如果描述包含"图标"或"icon",查找 i 元素
if any('图标' in kw or 'icon' in kw.lower() for kw in keywords):
try:
icon_elements = page.locator('i:visible, .icon:visible').all()
if icon_elements:
for icon in icon_elements:
aria_label = icon.get_attribute('aria-label') or ''
title = icon.get_attribute('title') or ''
class_name = icon.get_attribute('class') or ''
icon_info = f"{aria_label} {title} {class_name}"
if any(kw.lower() in icon_info.lower() for kw in keywords):
return icon, [{
'type': 'css',
'value': f'i.{class_name.split()[0]}' if class_name else 'i',
'confidence': 0.70,
'priority': 1
}]
# 回退:返回第一个可见图标
if icon_elements:
return icon_elements[0], [{
'type': 'css',
'value': 'i:visible',
'confidence': 0.60,
'priority': 1
}]
except Exception as e:
logger.warning(f"语义推断查找图标失败: {e}")
# ⚠️ 新增:如果描述包含"复选框"或"勾选",查找 checkbox
if any('复选框' in kw or '勾选' in kw or 'checkbox' in kw.lower() for kw in keywords):
try:
checkboxes = page.locator('.el-checkbox:visible, [role="checkbox"]:visible').all()
if checkboxes:
for cb in checkboxes:
text = cb.inner_text().strip()
if any(kw in text for kw in keywords):
return cb, [{
'type': 'css',
'value': f'.el-checkbox:has-text("{text}")',
'confidence': 0.75,
'priority': 1
}]
if checkboxes:
return checkboxes[0], [{
'type': 'css',
'value': '.el-checkbox:visible',
'confidence': 0.60,
'priority': 1
}]
except Exception as e:
logger.warning(f"语义推断查找复选框失败: {e}")
# ⚠️ 新增:如果描述包含"按钮"或"button",找所有可见按钮(包括 Element Plus)
if any('按钮' in kw or 'button' in kw.lower() for kw in keywords):
try:
buttons = page.locator('button:visible, .el-button:visible').all()
if buttons:
for btn in buttons:
text = btn.inner_text().strip()
if any(kw.lower() in text.lower() for kw in keywords):
return btn, [{
'type': 'css',
'value': f'button:has-text("{text}")',
'confidence': 0.80,
'priority': 1
}]
return buttons[0], [{
'type': 'css',
'value': 'button:visible',
'confidence': 0.60,
'priority': 1
}]
except Exception as e:
logger.warning(f"语义推断查找Element Plus按钮失败: {e}")
# wait 动作:找容器元素 # wait 动作:找容器元素
if action == 'wait': if action == 'wait':
container_selectors = [ container_selectors = [
......
...@@ -519,6 +519,23 @@ class SmartLocateService: ...@@ -519,6 +519,23 @@ class SmartLocateService:
keywords = extract_keywords(name) keywords = extract_keywords(name)
logger.debug(f"提取关键词: {keywords}") logger.debug(f"提取关键词: {keywords}")
# ⚠️ 新增:步骤模式预处理
# 识别"xxx选择:yyy"(列表复选框)和"xxx切换为:yyy"(Tab切换)模式
pattern_result = self._preprocess_step_pattern(name, action)
if pattern_result and pattern_result['selectors']:
result['success'] = True
result['selectors'] = {
'primary': pattern_result['selectors'][0],
'candidates': [
{'type': 'css', 'value': sel, 'confidence': pattern_result['confidence'], 'priority': i + 1}
for i, sel in enumerate(pattern_result['selectors'])
]
}
result['params']['selector'] = pattern_result['selectors'][0]
result['message'] = f"模式匹配成功: {pattern_result['type']}"
logger.info(f"步骤 {order} 匹配特殊模式: {pattern_result['type']}")
return result
# ⚠️ 新增:检查是否需要在抽屉内查找 # ⚠️ 新增:检查是否需要在抽屉内查找
drawer_keywords = ['资产管理', '资产信息', '资产故障', '资产设备', drawer_keywords = ['资产管理', '资产信息', '资产故障', '资产设备',
'会议预约', '会议运维', '会议转录', '信息发布', '会议预约', '会议运维', '会议转录', '信息发布',
...@@ -682,6 +699,78 @@ class SmartLocateService: ...@@ -682,6 +699,78 @@ class SmartLocateService:
logger.warning(f"执行步骤验证失败: {e}") logger.warning(f"执行步骤验证失败: {e}")
return False return False
def _preprocess_step_pattern(self, step_name: str, action: str) -> Optional[Dict[str, Any]]:
"""
预处理特殊步骤模式,生成选择器
处理模式:
1. "xxx选择:yyy" → 列表内复选框(如"会议室选择:北京展厅会议室")
2. "xxx切换为:yyy" / "切换到:yyy" → Tab切换项(如"时间切换为:预定会议")
Args:
step_name (str): 步骤名称
action (str): 动作类型
Returns:
Optional[Dict]: 模式匹配结果,包含:
- type: 模式类型
- selectors: 候选选择器列表
- confidence: 置信度
Example:
>>> _preprocess_step_pattern("会议室选择:北京展厅会议室", "click")
{
'type': 'list_checkbox',
'selectors': [
'.el-checkbox:has-text("北京展厅会议室")',
'.el-table__row:has-text("北京展厅会议室") .el-checkbox'
],
'confidence': 0.85
}
"""
import re
if action != 'click':
return None
# 模式1:列表选择模式("xxx选择:yyy")
# 适用于:会议室选择、设备选择、人员选择等
match = re.search(r'选择[::]\s*(.+)$', step_name)
if match:
item_name = match.group(1).strip()
logger.info(f"识别列表选择模式,目标项: {item_name}")
return {
'type': 'list_checkbox',
'selectors': [
f'.el-checkbox:has-text("{item_name}")',
f'.el-table__row:has-text("{item_name}") .el-checkbox',
f'.el-tree-node:has-text("{item_name}") .el-checkbox',
f'li:has-text("{item_name}") .el-checkbox',
f'[class*="list"]:has-text("{item_name}") .el-checkbox',
],
'confidence': 0.85
}
# 模式2:Tab切换模式("xxx切换为:yyy" / "切换到:yyy")
match = re.search(r'切换[为到]?[::]\s*(.+)$', step_name)
if match:
tab_name = match.group(1).strip()
logger.info(f"识别Tab切换模式,目标Tab: {tab_name}")
return {
'type': 'tab_item',
'selectors': [
f'.el-tabs__item:has-text("{tab_name}")',
f'[role="tab"]:has-text("{tab_name}")',
f'.tab-item:has-text("{tab_name}")',
f'li[role="tab"]:has-text("{tab_name}")',
],
'confidence': 0.85
}
return None
def _get_login_selector(self, step_name: str) -> Optional[str]: def _get_login_selector(self, step_name: str) -> Optional[str]:
""" """
获取登录相关步骤和导航步骤的选择器(使用已验证的模板) 获取登录相关步骤和导航步骤的选择器(使用已验证的模板)
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论