# 执行计划：修复智能定位选择器不准的5个核心缺陷

> **文档版本**: v1.0
> **创建日期**: 2026-08-10
> **关联问题处理**: `_问题处理_智能定位选择器不准5个核心缺陷.md`
> **预计工期**: 1天（4个任务，共约6小时）
> **状态**: ⏳ 待实施

---

## 一、执行概述

### 1.1 执行目标

修复智能定位选择器生成中的5个核心缺陷，确保：

1. 回退选择器不再使用宽泛的 `input:visible`，改用 placeholder 关键词匹配
2. 文本匹配优先限定元素类型（`button:has-text()` 优先于 `div:has-text()`）
3. 步骤执行后验证页面状态（弹窗/抽屉是否打开）
4. 选择器生成带父容器上下文限定
5. 为重新对会议管理用例跑智能定位打好基础

### 1.2 改动范围

| 文件 | 改动类型 | 涉及缺陷 |
|------|----------|---------|
| `backend/app/services/keyword_matcher.py` | 修改 | 缺陷1、缺陷2、缺陷4 |
| `backend/app/services/smart_locate_service.py` | 修改 | 缺陷3 |
| `backend/app/services/selector_extractor.py` | 引用 | 缺陷4（利用现有组合选择器） |

### 1.3 不改动的部分

- 数据库 schema（无变更）
- API 接口（无变更）
- 前端代码（无变更）
- 已有测试用例数据（无变更）

---

## 二、任务分解

### 任务1：修复 fill 回退选择器（缺陷1，约1h）

**目标**：`find_element_by_semantic()` 中 fill 动作回退时，用 placeholder 关键词匹配替代 `input:visible`

**文件**：`backend/app/services/keyword_matcher.py` 第828-863行

**改动**：

```python
# 修改前：find_element_by_semantic() 的 fill 分支
if action == 'fill':
    inputs = page.locator('input:visible, textarea:visible').all()
    if inputs:
        if len(inputs) == 1:
            return inputs[0], [{'value': 'input:visible', ...}]  # 宽泛回退
        # ...
        return inputs[0], [{'value': 'input:visible', ...}]  # 宽泛回退
```

```python
# 修改后：
def find_element_by_semantic(page, keywords, action, timeout=5000):
    if action == 'fill':
        inputs = page.locator('input:visible, textarea:visible').all()
        if inputs:
            # 1. 优先找 placeholder 匹配关键词的输入框
            for inp in inputs:
                placeholder = inp.get_attribute('placeholder') or ''
                if any(kw.lower() in placeholder.lower() for kw in keywords):
                    return inp, [{
                        'type': 'css',
                        'value': f'input[placeholder*="{placeholder}"]',
                        'confidence': 0.85,
                        'priority': 1
                    }]
            
            # 2. 其次找 label 关联的输入框（通过 aria-labelledby 或父元素 label）
            for inp in inputs:
                el_id = inp.get_attribute('id') or ''
                label = page.locator(f'label[for="{el_id}"]').first
                if label:
                    label_text = label.inner_text().strip()
                    if any(kw.lower() in label_text.lower() for kw in keywords):
                        return inp, [{
                            'type': 'css',
                            'value': f'#{el_id}',
                            'confidence': 0.80,
                            'priority': 1
                        }]
            
            # 3. 最后回退：选择与关键词最相关的输入框（基于 name/type 等属性）
            text_inputs = [inp for inp in inputs 
                          if (inp.get_attribute('type') or 'text') in ['text', 'email', 'tel', 'password', 'search', '']]
            if text_inputs:
                # 用第一个文本输入框，但选择器带上 placeholder 或 name 属性
                first = text_inputs[0]
                name = first.get_attribute('name') or ''
                placeholder = first.get_attribute('placeholder') or ''
                if name:
                    selector = f'input[name="{name}"]'
                elif placeholder:
                    selector = f'input[placeholder*="{placeholder}"]'
                else:
                    selector = 'input[type="text"]:visible'
                return first, [{'type': 'css', 'value': selector, 'confidence': 0.65, 'priority': 1}]
            
            # 绝对回退
            return inputs[0], [{'type': 'css', 'value': 'input[type="text"]:visible', 'confidence': 0.50, 'priority': 2}]
```

**验收标准**：
- 步骤"输入会议名称：xxx" → 选择器 `input[placeholder*="会议名称"]` 或类似
- 不再出现 `input:visible` 作为 fill 回退（除非万不得已）
- 已有的 fill 步骤（如登录模板）继续正常工作

---

### 任务2：文本匹配优先限定元素类型（缺陷2，约1h）

**目标**：`match_element_by_keywords()` 文本匹配时，点击操作优先匹配 `button:has-text()` 而非 `div:has-text()`

**文件**：`backend/app/services/keyword_matcher.py` 第595-613行 + `find_element_by_semantic()` 的 click 分支

**改动A**：在 `match_element_by_keywords()` 中，点击动作时对 DIV/SPAN/I 等非按钮标签的选择器降级

```python
# 修改前（第602-606行）：
if tag in ['BUTTON', 'A']:
    selector_value = f'{tag.lower()}:has-text("{text}")'
else:
    selector_value = f'{tag.lower()}:visible:has-text("{text}")'

# 修改后：
if tag in ['BUTTON', 'A']:
    selector_value = f'{tag.lower()}:has-text("{text}")'
elif action == 'click' and tag in ['DIV', 'SPAN', 'I']:
    # 点击动作下，对非按钮标签降级处理
    # 使用更精确的选择器，并降低优先级
    selector_value = f'{tag.lower()}:visible:has-text("{text}")'
    # 同时尝试生成 button 替代选择器
    alt_selector = f'button:has-text("{text}")'
    selectors.append({
        'type': 'css',
        'value': alt_selector,
        'confidence': 0.70,
        'priority': 3,
        'match_type': 'contains'
    })
else:
    selector_value = f'{tag.lower()}:visible:has-text("{text}")'
```

**改动B**：在 `find_element_by_semantic()` 的 click 分支中，优先使用 `button:has-text()` 定位

```python
# 修改 find_element_by_semantic() 的 click 动作（第868行附近）
if action == 'click':
    # 先尝试 button 定位（最精确）
    try:
        for kw in keywords:
            btn_selector = f'button:has-text("{kw}")'
            btn = page.locator(btn_selector).first
            if btn and btn.is_visible():
                return btn, [{
                    'type': 'css', 'value': btn_selector,
                    'confidence': 0.85, 'priority': 1
                }]
    except Exception:
        pass
    
    # 再尝试 role="button" 定位
    try:
        for kw in keywords:
            role_selector = f'[role="button"]:has-text("{kw}")'
            el = page.locator(role_selector).first
            if el and el.is_visible():
                return el, [{
                    'type': 'css', 'value': role_selector,
                    'confidence': 0.80, 'priority': 2
                }]
    except Exception:
        pass
    
    # 最后才是原有的 div/span 匹配逻辑
    ...原有代码...
```

**验收标准**：
- 步骤"点击【新建会议】按钮" → 优先生成 `button:has-text("新建会议")`
- 步骤"点击确定" → 优先生成 `button:has-text("确定")`
- 按钮类步骤不再生成 `div:visible:has-text("新建会议")` 作为首选

---

### 任务3：步骤执行后验证页面状态（缺陷3，约2h）

**目标**：`_execute_step()` 中点击操作后，验证页面状态是否变化

**文件**：`backend/app/services/smart_locate_service.py` 第721-785行

**改动**：在 `_execute_step()` 的 click 分支中，增加页面状态验证逻辑

```python
def _execute_step(self, result: Dict[str, Any]) -> bool:
    action = result.get('action', '')
    params = result.get('params', {})
    selector = params.get('selector', '')
    step_name = result.get('name', '')
    
    if not selector:
        return False
    
    try:
        page = self.executor._page
        
        if action == 'fill':
            ...原有逻辑...
        
        elif action == 'click':
            # 等待元素可见
            page.wait_for_selector(selector, timeout=5000)
            # 点击
            page.click(selector)
            # 等待页面响应
            page.wait_for_timeout(800)
            
            # === 新增：页面状态验证 ===
            # 1. 检测是否是功能中心点击
            if '功能中心' in step_name:
                try:
                    page.wait_for_selector('.el-drawer', timeout=5000)
                    page.wait_for_timeout(500)
                    logger.debug("功能中心抽屉已打开")
                except Exception:
                    logger.warning("功能中心抽屉未打开，标记为失败")
                    return False
            
            # 2. 检测弹窗是否出现（如果步骤名称包含"新建"、"编辑"等弹窗关键词）
            dialog_keywords = ['新建', '编辑', '添加', '新增', '创建', '修改', '详情']
            if any(kw in step_name for kw in dialog_keywords):
                dialog_opened = False
                for dialog_sel in ['.el-dialog', '.el-drawer', '.el-message-box']:
                    try:
                        page.wait_for_selector(dialog_sel, timeout=3000)
                        dialog_opened = True
                        logger.debug(f"弹窗已打开: {dialog_sel}")
                        break
                    except Exception:
                        continue
                
                if not dialog_opened:
                    logger.warning(f"步骤 '{step_name}' 点击后未检测到弹窗，可能点击失败")
                    # 不立即返回 False，让后续步骤自行判断
                    # 但记录警告
            
            # 3. 检测页面是否跳转（如果步骤名称包含"进入"、"打开"等导航关键词）
            nav_keywords = ['进入', '打开', '跳转', '导航']
            if any(kw in step_name for kw in nav_keywords):
                page.wait_for_timeout(1000)
                # 不检测具体 URL，因为 SPA 前端路由可能不触发 URL 变化
            
            logger.debug(f"点击成功: {selector}")
        
        elif action == 'wait':
            ...原有逻辑...
        
        return True
    
    except Exception as e:
        logger.warning(f"执行步骤验证失败: {e}")
        return False
```

**验收标准**：
- 点击"新建会议"后，检测到 `.el-dialog` 或 `.el-drawer` 出现
- 如果弹窗未出现，日志记录警告
- 功能中心点击后，等待 `.el-drawer` 出现，超时则返回 False

---

### 任务4：选择器生成加父容器上下文限定（缺陷4，约2h）

**目标**：在 `match_element_by_keywords()` 生成选择器时，自动检测父容器并添加前缀

**文件**：`backend/app/services/keyword_matcher.py`（主要） + `selector_extractor.py`（引用）

**改动**：在 `match_element_by_keywords()` 中，对每个匹配的元素检测父容器

```python
# 在 match_element_by_keywords() 中，为每个元素添加父容器检测逻辑
def _detect_parent_container(element) -> Dict[str, Any]:
    """检测元素的父容器信息"""
    container_info = {}
    try:
        # 使用 evaluate 向上遍历 DOM 查找父容器
        js_code = """
        (el) => {
            let parent = el.parentElement;
            let depth = 0;
            while (parent && depth < 5) {
                const cls = parent.className || '';
                const tag = parent.tagName || '';
                // 检测弹窗
                if (cls.includes('el-dialog') || cls.includes('el-dialog__wrapper')) {
                    const title = parent.querySelector('.el-dialog__title');
                    return { type: 'dialog', selector: '.el-dialog', title: title ? title.innerText.trim() : '' };
                }
                // 检测抽屉
                if (cls.includes('el-drawer')) {
                    const header = parent.querySelector('.el-drawer__header');
                    return { type: 'drawer', selector: '.el-drawer', title: header ? header.innerText.trim() : '' };
                }
                // 检测表单
                if (tag === 'FORM') {
                    return { type: 'form', selector: 'form' };
                }
                // 检测表格行
                if (cls.includes('el-table__row') || cls.includes('el-table__body')) {
                    return { type: 'table', selector: '.el-table' };
                }
                // 检测 Tab
                if (cls.includes('el-tabs') || cls.includes('el-tab-pane')) {
                    return { type: 'tab', selector: '.el-tabs' };
                }
                parent = parent.parentElement;
                depth++;
            }
            return {};
        }
        """
        container_info = element.evaluate(js_code)
    except Exception as e:
        logger.debug(f"检测父容器失败: {e}")
    return container_info or {}
```

然后在生成选择器时，如果检测到父容器，为选择器添加前缀：

```python
# 在生成每个选择器后，添加父容器前缀
container_info = _detect_parent_container(el)
if container_info:
    parent_selector = container_info.get('selector', '')
    if parent_selector:
        for sel in selectors:
            if not sel['value'].startswith(parent_selector):
                # 生成组合选择器
                combined_sel = f"{parent_selector} {sel['value']}"
                selectors.append({
                    'type': 'css',
                    'value': combined_sel,
                    'confidence': sel['confidence'] + 0.05,  # 组合选择器置信度略高
                    'priority': 3,  # 组合选择器优先级 3（低于语义但高于单一）
                    'match_type': sel.get('match_type', 'contains'),
                    'container': container_info.get('type', '')
                })
```

**验收标准**：
- 弹窗内的"确定"按钮 → 生成 `.el-dialog button:has-text("确定")`
- 抽屉内的"新建会议"按钮 → 生成 `.el-drawer button:has-text("新建会议")`
- 表单内的输入框 → 生成 `form input[placeholder*="名称"]`

---

## 三、实施顺序与依赖关系

```
任务1 (fill回退优化) ──→ 无依赖
任务2 (文本匹配优化) ──→ 无依赖
任务3 (页面状态感知) ──→ 无依赖
任务4 (父容器上下文) ──→ 无依赖

任务1-4 可并行实施，但推荐按顺序实施以便逐一验证
```

## 四、验收测试

### 4.1 单元测试验证

| 任务 | 验证方法 | 预期结果 |
|------|---------|---------|
| 任务1 | 检查 `find_element_by_semantic` 中 fill 分支 | 不再生成 `input:visible` 作为回退 |
| 任务2 | 检查 `match_element_by_keywords` 中 click 动作的选择器 | `button:has-text()` 优先于 `div:has-text()` |
| 任务3 | 检查 `_execute_step` 中 click 后的状态检测逻辑 | 弹窗检测代码存在且正确 |
| 任务4 | 检查 `_detect_parent_container` 函数 | 能正确识别弹窗/抽屉/表单 |

### 4.2 集成测试

| 测试场景 | 测试方法 |
|---------|---------|
| 智能定位 API | 调用 `/api/element/smart-locate` 验证返回的选择器质量 |
| 会议管理用例 | 重新智能定位，检查步骤9-21选择器是否合理 |

### 4.3 回归验证

| 用例 | 预期 |
|------|------|
| 系统设置-页面访问验证 | 6/6 步骤继续通过 |
| 信息发布-页面访问验证 | 通过 |

---

## 五、风险评估

| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|---------|
| 父容器检测误判（错误添加前缀） | 中 | 中 | 使用 `confidence` 字段标记，不影响原有选择器 |
| 弹窗检测超时导致误判失败 | 高 | 低 | 使用短超时（3s）+ 不直接返回 False，仅记录警告 |
| 对已有选择器格式产生冲突 | 低 | 高 | 组合选择器作为新增候选，不影响主选择器 |
| 修改后 selectors 列表变长 | 中 | 低 | Claude 调用时限制候选数量 |

---

## 六、实施记录

| 日期 | 任务 | 完成内容 | 状态 |
|------|------|---------|------|
| - | 任务1 | fill 回退优化 | ⏳ 待开始 |
| - | 任务2 | 文本匹配优化 | ⏳ 待开始 |
| - | 任务3 | 页面状态感知 | ⏳ 待开始 |
| - | 任务4 | 父容器上下文限定 | ⏳ 待开始 |

---

## 七、后续工作

### 方案B（任务1-4完成后）

1. 重新对会议管理用例 `case_13650e0406e64779b66e6fa5de35c24a` 跑智能定位
2. 验证步骤9-21选择器是否正确
3. 如有必要，手动修正少量选择器
4. 全量执行验证

### 长期优化方向

1. Docker 容器安装中文字体（修复截图乱码）
2. 维护 jieba 自定义词典
3. 前端适配 `verify_method` 参数

---

## 八、相关文档

| 文档 | 路径 |
|------|------|
| 问题处理 | `_问题处理_智能定位选择器不准5个核心缺陷.md` |
| HANDOFF UI自动化 | `HANDOFF_UI自动化.md` |
| 智能定位准确率提升 PRD | `Docs/PRD/需求文档/用例管理/_PRD_智能定位准确率提升_聚焦核心快速见效.md` |
| 智能定位执行计划 | `Docs/PRD/需求文档/用例管理/_执行计划_智能定位准确率提升_聚焦核心快速见效.md` |