# 执行计划：智能定位准确率提升 - 聚焦核心快速见效

> **文档版本**: v1.0
> **创建日期**: 2026-08-06
> **关联PRD**: `_PRD_智能定位准确率提升_聚焦核心快速见效.md`
> **预计工期**: 2-3周（10-15个工作日）

---

## 一、执行概述

### 1.1 执行目标

将智能定位准确率从 ~60% 提升至 ≥80%，重点实现：
1. 关键词提取优化（jieba分词）
2. 评分体系归一化
3. Playwright语义定位器支持
4. 组合选择器生成
5. Claude语义增强优化
6. iframe/微前端选择器穿透

### 1.2 改动范围

| 模块 | 文件 | 改动类型 |
|------|------|----------|
| 关键词匹配 | `backend/app/services/keyword_matcher.py` | 重构 |
| 选择器提取 | `backend/app/services/selector_extractor.py` | 增强 |
| Playwright执行器 | `backend/app/executors/playwright_executor.py` | 增强 |
| Claude服务 | `backend/app/services/claude_service.py` | 增强 |
| 智能定位服务 | `backend/app/services/smart_locate_service.py` | 修改 |
| 依赖配置 | `backend/requirements.txt` | 新增jieba |

### 1.3 不改动的部分

- 数据库schema（无新增表/字段）
- API接口（无新增/修改）
- 前端代码（无改动）
- 测试用例数据（无改动）

---

## 二、任务分解与实施计划

### Phase 1：关键词提取优化（3天）

#### 任务1.1：引入jieba分词

**文件**: `backend/app/services/keyword_matcher.py`

**实施步骤**：

1. 在 `requirements.txt` 添加依赖：
   ```
   jieba>=0.42.1
   ```

2. 重构 `extract_keywords()` 函数：
   ```python
   import jieba
   import jieba.posseg as pseg
   
   def extract_keywords(description: str) -> list:
       # 1. 提取【】内完整词组（最高优先级）
       bracket_keywords = re.findall(r'【(.+?)】', description)
       
       # 2. jieba分词
       words = pseg.cut(description)
       
       # 3. 按词性过滤：保留名词(n/nr/ns/nz)、动词(vn)
       filtered_words = [
           word for word, flag in words
           if flag in ['n', 'nr', 'ns', 'nz', 'vn', 'v']
           and len(word) >= 2  # 过滤单字
       ]
       
       # 4. 合并并去重
       all_keywords = bracket_keywords + filtered_words
       return list(dict.fromkeys(all_keywords))  # 保持顺序去重
   ```

3. 删除旧的暴力替换逻辑（第77-79行）
4. 删除2-gram和单字关键词生成逻辑（第108-118行）

**验收标准**：
- "点击【新建会议】按钮" → keywords=["新建会议"]
- "点击展开" → keywords=["展开"]（不再为空）
- "输入会议名称：自动化新建会议" → keywords=["会议名称", "自动化", "新建会议"]（无噪声）

---

#### 任务1.2：精简修饰词列表

**文件**: `backend/app/services/keyword_matcher.py`

**实施步骤**：

1. 修改 `modifier_words`（第83-84行）：
   ```python
   # 旧：过于激进
   modifier_words = {'展开', '分类', '操作', '查看', '是否', '正确', '条目', '数据', '验证', '成功', '页面', '系统'}
   
   # 新：仅保留真正的无意义修饰词
   modifier_words = {'的', '了', '着', '过', '一下', '是否', '正确', '是否成功'}
   ```

2. 在jieba分词后，根据词性决定是否保留，而非硬编码移除

**验收标准**：
- "点击系统设置" → keywords=["系统设置"]（完整保留）
- "点击展开" → keywords=["展开"]（保留）

---

#### 任务1.3：extract_value正则增强

**文件**: `backend/app/services/keyword_matcher.py`

**实施步骤**：

修改 `extract_value_from_description`（第156行）：
```python
# 旧正则
pattern1 = r'(?:输入|填写|填入|键入|写入)\s*[^：:]*[：:]\s*([\w@.\-]+)\s*$'

# 新正则 - 支持中文值、空格、特殊字符
pattern1 = r'(?:输入|填写|填入|键入|写入)\s*[^：:]*[：:]\s*(.+?)\s*$'
```

**验收标准**：
- "输入备注：这是一个测试备注" → value="这是一个测试备注"
- "输入用户名：admin@xty" → value="admin@xty"

---

### Phase 2：评分体系归一化（2天）

#### 任务2.1：评分归一化实现

**文件**: `backend/app/services/keyword_matcher.py`

**实施步骤**：

1. 定义新的评分常量：
   ```python
   # 精确匹配 vs 包含匹配
   EXACT_MATCH_SCORE = 1.0
   CONTAINS_MATCH_SCORE = 0.3
   
   # 维度权重
   DIMENSION_WEIGHTS = {
       'data-testid': 0.20,
       'id': 0.18,
       'name': 0.15,
       'placeholder': 0.15,
       'aria-label': 0.12,
       'text': 0.15,
       'action_type': 0.05,
   }
   ```

2. 重构 `match_element_by_keywords()` 评分逻辑：
   ```python
   def match_element_by_keywords(page, keywords, action_type):
       # ... 元素遍历 ...
       
       for el in elements:
           score_details = {}
           
           # 对每个维度计算精确匹配/包含匹配
           if el_id:
               if keyword == el_id:
                   score_details['id'] = EXACT_MATCH_SCORE * DIMENSION_WEIGHTS['id']
               elif keyword in el_id:
                   score_details['id'] = CONTAINS_MATCH_SCORE * DIMENSION_WEIGHTS['id']
           
           # ... 其他维度类似 ...
           
           # 归一化
           matched_weights = sum(DIMENSION_WEIGHTS[k] for k in score_details)
           final_score = sum(score_details.values()) / matched_weights if matched_weights > 0 else 0
           
           # 动作类型适配
           final_score = apply_action_type_adjustment(final_score, el, action_type)
       
       return sorted(elements, key=lambda x: x['score'], reverse=True)
   ```

**验收标准**：
- 精确匹配的元素排序高于包含匹配的元素
- 多维度弱匹配不会超过单维度强匹配

---

#### 任务2.2：动作类型适配优化

**文件**: `backend/app/services/keyword_matcher.py`

**实施步骤**：

修改 `apply_action_type_adjustment`：
```python
def apply_action_type_adjustment(score, element, action_type):
    tag = element.get('tag', '').lower()
    
    if action_type == 'click':
        # click动作对INPUT大幅减分
        if tag == 'input':
            score *= 0.1  # 更激进（原0.3）
        # click动作对BUTTON/DIV/A/LI加分
        elif tag in ['button', 'a', 'div', 'li', 'span']:
            score += 0.2
    
    elif action_type == 'fill':
        # fill动作对INPUT/TEXTAREA加分
        if tag in ['input', 'textarea']:
            score += 0.2
        # fill动作对非输入元素减分
        else:
            score *= 0.3
    
    return min(score, 1.0)  # 上限为1.0
```

**验收标准**：
- click动作优先匹配BUTTON而非INPUT
- fill动作优先匹配INPUT而非BUTTON

---

### Phase 3：Playwright语义定位器支持（2天）

#### 任务3.1：语义定位器格式定义

**文件**: `backend/app/services/selector_extractor.py`

**实施步骤**：

1. 新增语义选择器类型：
   ```python
   SEMANTIC_SELECTOR_TYPES = {
       'role': 'getByRole',
       'text': 'getByText',
       'placeholder': 'getByPlaceholder',
       'label': 'getByLabel',
       'testid': 'getByTestId',
   }
   ```

2. 优先提取语义选择器：
   ```python
   def extract_semantic_selectors(element):
       selectors = []
       
       # 1. role定位器
       role = element.get_attribute('role')
       aria_label = element.get_attribute('aria-label')
       if role:
           if aria_label:
               selectors.append(f"role:{role}[name=\"{aria_label}\"]")
           else:
               selectors.append(f"role:{role}")
       
       # 2. text定位器
       text = element.inner_text().strip()
       if text and len(text) <= 50:  # 文本不太长
           selectors.append(f"text:{text}")
       
       # 3. placeholder定位器
       placeholder = element.get_attribute('placeholder')
       if placeholder:
           selectors.append(f"placeholder:{placeholder}")
       
       # 4. label定位器
       # ...
       
       # 5. testid定位器
       testid = element.get_attribute('data-testid')
       if testid:
           selectors.append(f"testid:{testid}")
       
       return selectors
   ```

**验收标准**：
- 按钮元素生成 `role:button[name="确定"]` 格式选择器
- 输入框生成 `placeholder:请输入会议名称` 格式选择器

---

#### 任务3.2：执行器支持语义定位器

**文件**: `backend/app/executors/playwright_executor.py`

**实施步骤**：

1. 在 `_resolve_selectors` 中识别语义定位器格式：
   ```python
   def _resolve_selectors(self, params):
       selectors = []
       
       # 解析选择器列表
       for s in params.get('selectors', []):
           if s.startswith('role:'):
               selectors.append(('semantic', 'role', s[5:]))
           elif s.startswith('text:'):
               selectors.append(('semantic', 'text', s[5:]))
           elif s.startswith('placeholder:'):
               selectors.append(('semantic', 'placeholder', s[12:]))
           # ... 其他语义格式
           else:
               selectors.append(('css_xpath', s))
       
       return selectors
   ```

2. 在 `_do_click` 等方法中使用语义API：
   ```python
   def _do_click(self, selectors):
       for selector_type, selector_value in selectors:
           if selector_type == 'semantic':
               loc_type, loc_value = selector_value
               
               if loc_type == 'role':
                   # 解析 role:button[name="确定"]
                   if '[name=' in loc_value:
                       role, name = parse_role_selector(loc_value)
                       locator = self._page.get_by_role(role, name=name)
                   else:
                       locator = self._page.get_by_role(loc_value)
               
               elif loc_type == 'text':
                   locator = self._page.get_by_text(loc_value)
               
               # ... 其他语义类型
               
               try:
                   locator.click(timeout=5000)
                   return True
               except:
                   continue
           
           else:  # css_xpath
               # 原有的CSS/XPath逻辑
               ...
   ```

**验收标准**：
- `role:button[name="登录"]` 能正确点击登录按钮
- `placeholder:请输入会议名称` 能正确填写输入框
- 向后兼容现有的CSS选择器

---

### Phase 4：组合选择器生成（2天）

#### 任务4.1：组合选择器算法实现

**文件**: `backend/app/services/selector_extractor.py`

**实施步骤**：

1. 实现组合选择器生成逻辑：
   ```python
   def generate_combined_selectors(element, page):
       combined = []
       
       # 获取元素的父容器信息
       parent_info = get_parent_container(element)
       
       # 策略1：弹窗/对话框内的元素
       if parent_info.get('is_dialog'):
           dialog_selector = f".el-dialog"
           element_selector = get_element_basic_selector(element)
           combined.append(f"{dialog_selector} {element_selector}")
       
       # 策略2：特定卡片/区域内的元素
       card = find_ancestor_with_class(element, ['card', 'item', 'row'])
       if card:
           card_text = card.inner_text()[:20]
           card_selector = f".card:has-text(\"{card_text}\")"
           element_selector = get_element_basic_selector(element)
           combined.append(f"{card_selector} >> {element_selector}")
       
       # 策略3：Tab内的元素
       tab = find_active_tab(element)
       if tab:
           tab_selector = f".el-tabs [aria-selected=\"true\"]"
           element_selector = get_element_basic_selector(element)
           combined.append(f"{tab_selector} >> {element_selector}")
       
       return combined
   ```

2. 在 `extract_selectors` 中增加组合选择器：
   ```python
   def extract_selectors(element, page):
       # 原有的单一选择器
       selectors = extract_basic_selectors(element)
       
       # 新增：组合选择器
       combined = generate_combined_selectors(element, page)
       selectors.extend(combined)
       
       # 按优先级排序
       return sort_by_priority(selectors)
   ```

**验收标准**：
- 弹窗内的"确定"按钮生成 `.el-dialog button:has-text("确定")`
- 表格行内的"编辑"按钮生成 `.el-table__row:has-text("会议室A") >> .edit-btn`

---

#### 任务4.2：选择器优先级体系统一

**文件**: `backend/app/services/selector_extractor.py`

**实施步骤**：

更新 `SELECTOR_PRIORITY`：
```python
SELECTOR_PRIORITY = {
    # 语义定位器（最高优先级）
    'role': 1,
    'text': 1,
    'placeholder': 1,
    'label': 1,
    'testid': 1,
    
    # data-testid
    'data-testid': 2,
    
    # 组合选择器
    'combined': 3,
    
    # ID
    'id': 4,
    
    # 其他
    'name': 5,
    'placeholder_attr': 6,
    'aria-label': 7,
    'text_selector': 8,
    'class': 9,
    'xpath': 10,
}
```

**验收标准**：
- `keyword_matcher.py` 和 `selector_extractor.py` 的优先级一致

---

### Phase 5：Claude语义增强优化（2天）

#### 任务5.1：单候选Claude验证

**文件**: `backend/app/services/smart_locate_service.py`

**实施步骤**：

修改 `_locate_single_step`：
```python
# 旧逻辑：只在多候选时调用Claude
if self.use_claude and len(elements_list) > 1:
    selected_idx = self.claude_service.rank_candidates(...)
    
# 新逻辑：所有候选都调用Claude验证
if self.use_claude and len(elements_list) >= 1:
    result = self.claude_service.rank_candidates_with_confirmation(
        candidates=candidates,
        step_name=step.name,
        page_url=page.url,
        previous_steps=steps[:step.order-1],
    )
    
    if result['confirmed']:
        selected_idx = result['selected_idx']
    else:
        # Claude认为当前候选都不匹配，尝试下一个候选或返回失败
        return {'success': False, 'message': result['reason']}
```

---

#### 任务5.2：Prompt上下文增强

**文件**: `backend/app/services/claude_service.py`

**实施步骤**：

1. 扩充Prompt模板：
   ```python
   ELEMENT_SELECTION_PROMPT = """
   你是一个UI自动化测试元素定位专家。根据以下信息选择最匹配的元素。
   
   ## 页面上下文
   - 当前URL: {page_url}
   - 当前路由: {current_route}
   
   ## 步骤上下文
   - 当前步骤: {current_step}
   - 前一步骤: {previous_step}
   
   ## 选择规则
   1. 优先选择可见且可交互的元素
   2. 优先选择在当前活动区域（弹窗/抽屉/Tab）内的元素
   3. 如果步骤描述包含特定区域关键词，优先在对应区域查找
   4. 避免选择装饰性元素（如图标、分隔线）
   
   ## 候选元素
   {candidates_json}
   
   ## 输出格式
   返回JSON：
   {{
     "selected_idx": 索引,
     "confirmed": true/false,
     "reason": "选择原因/拒绝原因"
   }}
   """
   ```

2. 增强 `_build_prompt` 保留完整候选信息：
   ```python
   def _build_prompt(candidates, page_url, previous_steps):
       enhanced_candidates = []
       for i, c in enumerate(candidates):
           enhanced_candidates.append({
               'index': i,
               'tag': c['tag'],
               'text': c.get('text', ''),
               'selector': c['selector'],
               'class': c.get('attributes', {}).get('class', ''),
               'placeholder': c.get('attributes', {}).get('placeholder', ''),
               'aria-label': c.get('attributes', {}).get('aria-label', ''),
               'position': c.get('position', {}),
               'parent': c.get('parent_info', {}),
           })
       # ...
   ```

---

#### 任务5.3：索引对齐修复

**文件**: `backend/app/services/smart_locate_service.py`

**实施步骤**：

确保 `get_candidate_details` 和 `match_element_by_keywords` 共享同一元素列表：
```python
def _locate_single_step(self, step, page):
    # 获取候选元素列表（包含完整信息）
    elements_list, selectors_list = self.keyword_matcher.match_element_by_keywords(
        page, keywords, action_type, return_full_info=True
    )
    
    # 候选详情直接从elements_list提取，保证索引一致
    candidates = []
    for i, el in enumerate(elements_list):
        candidates.append({
            'index': i,
            'tag': el['tag'],
            'text': el['text'],
            'selector': selectors_list[i]['value'],
            'attributes': el['attributes'],
            'position': el['position'],
        })
    
    # Claude选择
    result = self.claude_service.rank_candidates(candidates, ...)
    selected_idx = result['selected_idx']
    
    # 索引一致，直接使用
    primary_selector = selectors_list[selected_idx]['value']
```

---

### Phase 6：iframe/微前端选择器穿透（2天）

#### 任务6.1：frame路径选择器格式

**文件**: `backend/app/services/selector_extractor.py`, `backend/app/services/keyword_matcher.py`

**实施步骤**：

1. 定义frame路径选择器格式：
   ```python
   # 格式: iframe[selector] >> element_selector
   # 示例:
   # iframe[src*="meeting"] >> button:has-text("新建会议")
   # iframe[name="micro-app"] >> input[placeholder*="名称"]
   ```

2. 在 `keyword_matcher.py` 遍历iframe时记录frame信息：
   ```python
   def match_element_by_keywords(page, keywords, action_type):
       elements = []
       
       # 主页面元素
       main_elements = page.locator(base_selector).all()
       for el in main_elements:
           elements.append({
               'element': el,
               'frame_path': None,  # 主页面
           })
       
       # iframe元素
       for frame in page.frames:
           if frame == page.main_frame:
               continue
           
           frame_selector = get_frame_selector(frame)  # iframe[src*="xxx"]
           frame_elements = frame.locator(base_selector).all()
           
           for el in frame_elements:
               elements.append({
                   'element': el,
                   'frame_path': frame_selector,
               })
       
       return elements
   ```

3. 选择器生成时包含frame路径：
   ```python
   def build_selector_with_frame(element_selector, frame_path):
       if frame_path:
           return f"{frame_path} >> {element_selector}"
       return element_selector
   ```

---

#### 任务6.2：执行器frame穿透优化

**文件**: `backend/app/executors/playwright_executor.py`

**实施步骤**：

在 `_do_click` 等方法中支持frame路径选择器：
```python
def _do_click(self, selectors):
    for selector_type, selector_value in selectors:
        # 检查是否是frame路径选择器
        if ' >> ' in selector_value and selector_value.startswith('iframe'):
            frame_selector, element_selector = selector_value.split(' >> ', 1)
            
            # 定位到frame
            frame = self._page.frame_locator(frame_selector)
            
            try:
                frame.locator(element_selector).click(timeout=5000)
                return True
            except:
                continue
        
        else:
            # 原有的主页面逻辑
            ...
```

---

## 三、验收标准

### 3.1 功能验收清单

| # | 验收项 | 验收方法 | 负责人 |
|---|--------|---------|--------|
| 1 | jieba分词替换暴力替换 | 单元测试 + 调试输出 | - |
| 2 | 修饰词精简后不再空关键词 | 测试"点击展开"、"点击系统设置" | - |
| 3 | 中文值提取 | 测试"输入备注：这是一个测试备注" | - |
| 4 | 评分归一化 | 验证精确匹配排名高于包含匹配 | - |
| 5 | 动作类型适配 | 验证click优先匹配BUTTON | - |
| 6 | 语义定位器格式支持 | 测试role:/text:/placeholder:格式 | - |
| 7 | 执行器语义API | 测试getByRole/getByText执行 | - |
| 8 | 组合选择器生成 | 测试弹窗内按钮、表格行按钮 | - |
| 9 | 单候选Claude验证 | 测试单候选场景是否调用Claude | - |
| 10 | Prompt上下文增强 | 检查Claude调用日志中是否含页面URL | - |
| 11 | 索引对齐修复 | 测试多候选场景是否选择正确 | - |
| 12 | frame路径选择器 | 测试微前端内元素定位 | - |

### 3.2 准确率验收

| 用例类型 | 测试用例 | 预期结果 |
|---------|---------|---------|
| 简单页面访问 | 信息发布-页面访问验证 | 100%步骤定位成功 |
| 二级菜单导航 | 工单列表-页面访问验证 | 100%步骤定位成功 |
| 表单填写 | 会议管理-新建会议 | ≥75%步骤定位成功 |
| 微前端操作 | - | ≥65%步骤定位成功 |

### 3.3 回归验收

| 用例 | 预期结果 |
|------|---------|
| 系统设置-页面访问验证 | 6/6步骤通过 |
| 信息发布-页面访问验证 | 通过 |
| 已有的CSS选择器用例 | 通过 |

---

## 四、测试计划

### 4.1 单元测试

| 模块 | 测试文件 | 测试内容 |
|------|---------|---------|
| keyword_matcher | `tests/test_keyword_matcher.py` | 关键词提取、评分计算、值提取 |
| selector_extractor | `tests/test_selector_extractor.py` | 语义选择器提取、组合选择器生成 |
| playwright_executor | `tests/test_playwright_executor.py` | 语义定位器执行、frame穿透 |

### 4.2 集成测试

| 测试场景 | 测试用例 |
|---------|---------|
| 智能定位API | 调用 `/api/element/smart-locate` 验证返回的选择器 |
| 端到端执行 | 执行用例验证选择器能正确操作 |

### 4.3 准确率测试

使用现有的20+个UI用例批量测试，统计定位成功率。

---

## 五、风险评估

| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|---------|
| jieba分词对专业术语不准 | 中 | 中 | 维护自定义词典 |
| 语义定位器格式冲突 | 低 | 高 | 使用明确前缀，单元测试覆盖 |
| Claude耗时增加 | 高 | 中 | 单候选用简化Prompt |
| iframe选择器兼容性 | 中 | 中 | 渐进式支持，保留原有逻辑 |

---

## 六、实施记录

| 日期 | 阶段 | 完成内容 | 状态 |
|------|------|---------|------|
| - | Phase 1 | 关键词提取优化 | 待开始 |
| - | Phase 2 | 评分体系归一化 | 待开始 |
| - | Phase 3 | Playwright语义定位器 | 待开始 |
| - | Phase 4 | 组合选择器生成 | 待开始 |
| - | Phase 5 | Claude语义增强 | 待开始 |
| - | Phase 6 | iframe穿透优化 | 待开始 |

---

## 七、后续工作

### 7.1 本次不实施的功能

- 自愈合机制（中期目标）
- DOM指纹（中期目标）
- 视觉定位（长期目标）
- 本地LLM部署（长期目标）

### 7.2 优化方向

- 根据实际测试结果调整评分权重
- 维护项目特定的jieba自定义词典
- 收集Claude调用日志优化Prompt

---

## 八、附录

### 8.1 相关文档

| 文档 | 路径 |
|------|------|
| PRD文档 | `Docs/PRD/需求文档/用例管理/_PRD_智能定位准确率提升_聚焦核心快速见效.md` |
| 元素定位方案对比 | `Docs/PRD/需求文档/用例管理/_分析报告_元素定位方案对比.md` |
| UI自动化交接 | `HANDOFF_UI自动化.md` |

### 8.2 关键代码位置

| 文件 | 关键函数/类 |
|------|------------|
| `keyword_matcher.py` | `extract_keywords()`, `match_element_by_keywords()`, `extract_value_from_description()` |
| `selector_extractor.py` | `extract_selectors()`, `extract_semantic_selectors()`, `generate_combined_selectors()` |
| `playwright_executor.py` | `_resolve_selectors()`, `_do_click()`, `_do_fill()` |
| `claude_service.py` | `rank_candidates()`, `_build_prompt()` |
| `smart_locate_service.py` | `_locate_single_step()`, `get_candidate_details()` |
