# 执行器准确率优化 执行计划文档

> **文档版本**: v1.0
> **创建日期**: 2026-07-13
> **关联 PRD**: _PRD_需求文档_执行器准确率优化.md v1.0
> **负责人**: czj

---

## 一、执行概述

### 1.1 项目背景

`platform-auto-test` 平台的测试用例执行准确率低于预期。经对比同仓库 `pc自动化测试`（Playwright 原生 E2E 测试，51 个工作流，通过率 100%），发现差距主要在：选择器质量、等待策略、交互健壮性、重试粒度、日志反馈五个维度。本计划针对这些差距，从 `pc自动化测试` 提取已验证的最佳实践，融入平台执行器。

### 1.2 执行目标

| 目标 | 描述 | 验收标准 |
|------|------|---------|
| 多选择器回退 | 单步骤支持多个选择器，主选择器失败自动切换 | 录制时可指定 selectors 列表，执行时逐一尝试 |
| 显式等待 | 操作前等待元素可见，操作后等待页面动画完成 | 弹窗/列表加载后操作正常执行 |
| 交互健壮性 | force click 绕过覆盖层、fill 先清空再输入 | 覆盖层上元素可点击，输入框填充正确 |
| 单步重试 | 失败步骤自动重试，不重新执行前置步骤 | 某步失败后重试 2 次，前置步骤不再执行 |
| 实时反馈 | WebSocket 步骤回调接通，前端实时显示 | 执行时浏览器 console 可见 step_update 事件 |
| 向后兼容 | 现有 26 个用例无需修改 | 旧格式数据执行正常 |

### 1.3 技术选型

| 项目 | 选择 | 说明 |
|------|------|------|
| 执行器 | Playwright Sync API | 保持现有同步方案，避免 Windows 兼容问题 |
| 选择器回退 | SelectorMapper + 步骤级 selectors 列表 | 已有 SelectorMapper 直接集成 |
| 实时通信 | WebSocket（FastAPI 内置） | 已有 manager.py，接通回调即可 |
| 数据库 | SQLite | 步骤数据存 JSON 列，新字段 Optional 无 schema 变更 |

### 1.4 预计工期

| 阶段 | 内容 | 预计时间 |
|------|------|---------|
| Phase 1 | 执行器核心改进（P0） | 2-3 天 |
| Phase 2 | 数据结构统一（P0） | 1 天 |
| Phase 3 | 执行服务改进（P1） | 1 天 |
| **总计** | | **4-5 天** |

---

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

### 2.1 Phase 1: 执行器核心改进（预计 2-3 天）

#### 2.1.1 任务清单

| 任务ID | 任务名称 | 说明 | 优先级 | 预计工时 |
|--------|---------|------|--------|---------|
| P1-001 | 集成 SelectorMapper + 多选择器回退 | 导入 SelectorMapper，新增 `_resolve_selectors` 方法，改造所有 `_do_*` 动作方法 | P0 | 1 天 |
| P1-002 | 显式等待策略 | 新增 `_wait_for_element`、`_safe_is_visible`、`_post_action_wait` 方法，改造 execute_step | P0 | 0.5 天 |
| P1-003 | 强制点击 + 输入清空 | `_do_click` 支持 force 参数，`_do_fill` 执行 triple-click 清空模式 | P0 | 0.3 天 |
| P1-004 | 图标日志 | 全部 logger 改为图标前缀，execute_step 入口/出口加日志 | P0 | 0.2 天 |
| P1-005 | 加载遮罩两阶段等待 | 新增 `_wait_for_loading_overlay`，导航后和点击前调用 | P0 | 0.3 天 |

#### 2.1.2 代码变更

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

```python
# 新增导入
from app.utils.selector_mapper import SelectorMapper

# __init__ 新增
self._selector_mapper = SelectorMapper()

# 新增方法
def _resolve_selectors(self, params: dict) -> List[str]:
    """解析多选择器回退链"""
    # 1. 优先 params.selectors（列表）
    # 2. 回退 params.selector（单值）
    # 3. 若有 page_key+element_key，调 mapper 取完整链
    ...

def _wait_for_element(self, selector, timeout=None, state="visible") -> bool:
    """安全等待元素，不抛异常"""
    ...

def _safe_is_visible(self, selector: str) -> bool:
    """安全检查可见性，对应 .isVisible().catch(() => false)"""
    ...

def _post_action_wait(self, action_type: str):
    """按动作类型等待"""
    ...

def _wait_for_loading_overlay(self, timeout=10000):
    """两阶段加载遮罩等待"""
    ...

# 改造 _do_click — 多选择器 + force 参数
def _do_click(self, params: dict) -> None:
    selectors = self._resolve_selectors(params)
    force = params.get("force", False)
    for sel in selectors:
        try:
            self._wait_for_element(sel, timeout=5000)
            self._page.click(sel, timeout=5000, force=force)
            return
        except Exception as e:
            logger.debug(f"  ⚠ 点击失败 {sel}: {e}")
            continue
    # iframe fallback 保留
    ...

# 改造 _do_fill — triple-click 清空模式
def _do_fill(self, params: dict) -> None:
    selectors = self._resolve_selectors(params)
    value = params.get("value", "")
    for sel in selectors:
        try:
            self._wait_for_element(sel, timeout=5000)
            self._page.click(sel, click_count=3, force=True, timeout=5000)
            self._page.keyboard.press("Control+A")
            self._page.keyboard.press("Delete")
            self._page.wait_for_timeout(100)
            self._page.fill(sel, str(value))
            logger.info(f"  ✓ 填充: {sel} = {value}")
            return
        except Exception as e:
            logger.debug(f"  ⚠ 填充失败 {sel}: {e}")
            continue
    raise Exception(f"无法填充元素: {selectors}")

# 图标日志改造示例
# _do_click 成功: logger.info(f"  ✓ 点击: {selector}")
# _do_navigate 成功: logger.info(f"  ✓ 导航到: {url}")
# _do_assert 成功: logger.info(f"  ✓ 断言通过: {assert_type}")
# execute_step 入口: logger.info(f"  ⏳ 步骤 {order}/{total}: {name}")
# execute_step 成功: logger.info(f"  ✓ 步骤 {order} 完成 ({duration:.2f}s)")
# execute_step 失败: logger.warning(f"  ✗ 步骤 {order} 失败: {error}")
```

#### 2.1.3 验证方法

1. 执行一个旧格式用例（仅 `params.selector`），确认走通
2. 手动构造一个带 `selectors` 列表的用例，主选择器故意写错，检查备选是否生效
3. 检查控制台输出是否带 `✓`/`⏳`/`✗` 图标
4. 检查 force click 参数是否透传成功

---

### 2.2 Phase 2: 数据结构统一（预计 1 天）

#### 2.2.1 任务清单

| 任务ID | 任务名称 | 说明 | 优先级 | 预计工时 |
|--------|---------|------|--------|---------|
| P2-001 | 扩展 StepDefinition Schema | 增加 selectors/page_key/element_key/force/wait_after 可选字段 | P0 | 0.3 天 |
| P2-002 | Recorder 统一格式 | RecordedStep 复用 StepDefinition，add_step/add_assertion 存 selectors 列表 | P0 | 0.3 天 |
| P2-003 | Executor 读取新字段 | execute_step 读取 selectors/page_key/element_key 等新字段 | P0 | 0.2 天 |
| P2-004 | 前端类型同步 | StepDefinition 接口添加对应字段 | P0 | 0.2 天 |

#### 2.2.2 代码变更

**文件：`backend/app/schemas/test_case.py`**

```python
class StepDefinition(BaseModel):
    order: int = Field(..., ge=1)
    name: str = Field(..., min_length=1, max_length=200)
    action: str = Field(...)
    params: Dict[str, Any] = Field(default_factory=dict)
    expected: str = Field(default="")
    # 新增字段（全部 Optional，向后兼容）
    selectors: Optional[List[str]] = Field(None, description="选择器列表（带回退）")
    page_key: Optional[str] = Field(None, description="SelectorMapper 页面标识")
    element_key: Optional[str] = Field(None, description="元素标识")
    force: Optional[bool] = Field(None, description="是否强制点击")
    wait_after: Optional[int] = Field(None, description="操作后等待时间(ms)")
```

**文件：`backend/app/routers/recorder.py`**

- `RecordedStep` 改为指向 `StepDefinition`（或直接替换）
- `add_step` 接收 `selectors` 列表替代顶层 `selector`/`value`
- `add_assertion` 同步使用 `selectors` 列表
- `save_as_case` 删除手动转换逻辑

**文件：`frontend/src/types/case.ts`**

```typescript
export interface StepDefinition {
  order: number
  name: string
  action: string
  params: Record<string, any>
  expected?: string
  selectors?: string[]
  pageKey?: string
  elementKey?: string
  force?: boolean
  waitAfter?: number
}
```

#### 2.2.3 验证方法

1. 录制器创建用例 → 查 DB 的 steps JSON 含 `selectors` 列表
2. 旧格式用例（只有 `params.selector`）执行正常
3. 新用例（含 `selectors` 列表）执行时多选器回退生效

---

### 2.3 Phase 3: 执行服务改进（预计 1 天）

#### 2.3.1 任务清单

| 任务ID | 任务名称 | 说明 | 优先级 | 预计工时 |
|--------|---------|------|--------|---------|
| P3-001 | 单步重试 | execute_step 内层循环失败后重试，默认 2 次 | P1 | 0.3 天 |
| P3-002 | 接通 WebSocket 回调 | 将 _create_step_callback 传给 executor.execute_case | P1 | 0.5 天 |
| P3-003 | 前端实时时间线 | Execution.vue 增加 liveSteps 时间线展示 | P1 | 0.5 天 |

#### 2.3.2 代码变更

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

```python
# __init__ 新增
self.step_retry_count: int = self.config.get("step_retry_count", 2)

# execute_step 改造 — 单步重试
def execute_step(self, step, callback=None):
    ...
    for attempt in range(self.step_retry_count + 1):
        try:
            # 执行动作
            ...
            step_result.status = "passed"
            break
        except Exception as e:
            if attempt < self.step_retry_count:
                logger.warning(f"  ⚠ 步骤 {step_result.order} 第{attempt+1}次重试: {e}")
                step_result.log += f"\n⚠ 第{attempt+1}次重试..."
                self._page.wait_for_timeout(1000)
            else:
                raise
    ...
```

**文件：`backend/app/services/execution_service.py`**

```python
# run_all_cases_sync 改造 — 接收 loop + broadcast_fn，接通回调
def run_all_cases_sync(cases_list, exec_config, execution_id, loop, broadcast_fn):
    executor = PlaywrightExecutor(config=exec_config)
    executor.start()
    results = []
    try:
        for i, case_dict in enumerate(cases_list):
            case_id = case_dict.get("id")
            def step_callback(step_result, cid=case_id):
                try:
                    asyncio.run_coroutine_threadsafe(
                        broadcast_fn(execution_id, {
                            "type": "step_update",
                            "data": {
                                "execution_id": execution_id,
                                "case_id": cid,
                                "step_order": step_result.order,
                                "step_name": step_result.name,
                                "action": step_result.action,
                                "status": step_result.status,
                                "duration": step_result.duration,
                                "screenshot": step_result.screenshot,
                                "log": step_result.log,
                                "error": step_result.error,
                            },
                        }),
                        loop
                    )
                except Exception as e:
                    logger.error(f"步骤回调失败: {e}")
            result = executor.execute_case(case=case_dict, callback=step_callback)
            results.append(result)
    finally:
        executor.stop()
    return results

# 调用处
all_results = await loop.run_in_executor(
    None, run_all_cases_sync,
    [c["case_dict"] for c in cases_to_run],
    config, execution_id, loop, manager.broadcast,
)
```

#### 2.3.3 验证方法

1. 配置 `step_retry_count=2`，让步骤 3 故意失败，检查步骤 3 重试 2 次后标记 failed，步骤 1-2 不重新执行
2. 浏览器 WebSocket 连接后执行，console 检查 `step_update` 事件
3. 前端 Execution.vue 执行时时间线逐步更新

---

## 三、代码文件规划

### 3.1 修改文件清单

```
backend/
├── app/
│   ├── executors/
│   │   └── playwright_executor.py    # 核心：SelectorMapper 集成、多选器回退、等待策略、force、清空、图标日志、单步重试、加载遮罩
│   ├── utils/
│   │   └── selector_mapper.py        # 新增 resolve_selectors 方法（现有选择器定义不动）
│   ├── schemas/
│   │   └── test_case.py              # StepDefinition 扩展（全部 Optional）
│   ├── routers/
│   │   └── recorder.py               # RecordedStep 统一为 StepDefinition
│   └── services/
│       └── execution_service.py      # WebSocket 回调接通、step_retry_count 透传
frontend/
├── src/
│   ├── types/
│   │   └── case.ts                    # 类型同步
│   └── views/
│       └── Execution.vue              # 实时步骤时间线
```

### 3.2 依赖关系

```
Phase 1 (执行器) ← 独立，无外部依赖
    │
Phase 2 (数据结构) ← 独立，但需与 Phase 1 配合可获得完整效果
    │
Phase 3 (执行服务) ← 依赖 Phase 1（执行器改造完毕）/ Phase 2（新字段读取）
    │
    └→ 前端 Execution.vue ← 依赖 Phase 3 WebSocket 接通
```

建议按 Phase 1 → Phase 2 → Phase 3 顺序实施。

---

## 四、验证方案

### 4.1 单元测试

| 测试项 | 方法 | 预期 |
|-------|------|------|
| 多选择器回退 | 构造带 3 个选择器的 params，第 1 个无效 | 第 2 个或第 3 个生效，步骤 passed |
| 向后兼容 | 构造仅 params.selector 的 params | 执行正常，不抛 KeyError |
| 单步重试 | 设 step_retry_count=2，步骤抛异常 | 重试 2 次后 failed |
| force click | 传 force=True | click 调用的 force 参数为 True |

### 4.2 集成测试

| 测试项 | 方法 | 预期 |
|-------|------|------|
| 录制→执行闭环 | 录制器创建含 selectors 的用例 → 执行 | 步骤正确执行，DB 存储完整 |
| 旧数据回归 | 执行现有 26 个用例 | 通过率不低于改动前 |
| 实时反馈 | 浏览器 WebSocket 连接 → 执行 | step_update 事件实时推送 |
| Windows 兼容 | Windows 11 全流程执行 | Playwright 启动正常，执行完成 |

### 4.3 回滚方案

如果改动后出现异常：
1. 数据结构改动（Optional 字段）不影响旧数据，无需回滚
2. 执行器逻辑可恢复为 git checkout 旧版本
3. 数据库无迁移操作，不需回滚数据

---

## 五、参考文档

| 文档 | 路径 | 说明 |
|------|------|------|
| PRD 需求文档 | `Docs/PRD/自动化测试平台/_PRD_需求文档_执行器准确率优化.md` | 本计划对应需求 |
| 方案对比分析 | `Docs/自动化测试方案对比.md` | 两套方案多维度对比 |
| 执行准确率差异分析 | `Docs/执行准确率差异分析.md` | 根因分析和改进建议 |
| pc自动化测试设计文档 | `pc自动化测试/pc自动化测试/BookV3_E2E_设计文档.md` | 参考项目设计 |
| pc自动化测试 Page Object | `pc自动化测试/pc自动化测试/e2e-bookv3/pages/*.js` | 选择器策略参考 |
| SelectorMapper | `backend/app/utils/selector_mapper.py` | 待集成组件 |