# 执行计划：复杂用例通用执行机制 - 语义化用例执行

> **文档版本**: v1.0
> **创建日期**: 2026-08-18
> **关联PRD**: `_PRD_复杂用例通用执行机制_语义化用例执行.md`
> **预计工期**: 分 6 阶段，每阶段独立交付验证

---

## 一、执行概述

### 1.1 执行目标

解决复杂 UI 用例（如「新建会议」21 步）反复"打补丁"才能稳定的通用问题，实现范式转变：**从"记录选择器"到"记录业务意图"**。

1. 语义目标数据模型（`semantic`/`verify`/`run_on` 字段）
2. 上下文系统与变量替换（`{__CTX:key__}`）
3. 执行期语义定位解析器（wire keyword_matcher 进执行链）
4. 运行时页面身份感知（页面注册表 + scope）
5. 状态验证防假通过（verify）
6. 数据自建/自清理（api_call + finally）
7. 映射表 v2 显式字典（废弃 30+ 条模糊评分）

**硬性约束**：现有 21 步用例「会议管理-新建会议-czj录入」必须全程通过（不填新字段、不依赖新机制），作为每阶段回归基线。

### 1.2 改动范围

| 模块 | 文件 | 改动类型 |
|------|------|----------|
| 步骤Schema | `backend/app/schemas/test_case.py` | 增强（新字段） |
| 上下文 | `backend/app/executors/ui_context.py` | **新增** |
| 执行器 | `backend/app/executors/playwright_executor.py` | 增强（渲染/解析/api_call/finally/verify） |
| 语义解析器 | `backend/app/services/semantic_resolver.py` | **新增**（核心） |
| 页面识别 | `backend/app/services/page_url_service.py` + `backend/app/data/page_url_mapping.json` | 增强（注册表） |
| 元素映射 | `backend/app/services/element_mapping_service.py` + `backend/app/data/elements_mapping.json` | 重构（v2 字典） |
| 关键词匹配 | `backend/app/services/keyword_matcher.py` | 增强（facade） |
| Claude服务 | `backend/app/services/claude_service.py` | 增强（min_confidence） |
| 智能定位 | `backend/app/services/smart_locate_service.py` | 修复（L610 dead-code） |
| 接口测试执行器 | `backend/app/executors/api_test_executor.py` | 复用，不改 |
| 前端类型 | `frontend/src/types/case.ts` | 同步新字段 |
| 前端编辑器 | `frontend/src/components/CaseStepEditor.vue` | 增强（semantic 表单+断言对齐） |
| 前端录制 | `frontend/src/views/Recorder.vue` + `frontend/src/utils/stepParser.ts` | 同步 |
| 配置 | `backend/app/config.py` | 新增 4 个 flag |

### 1.3 不改动的部分

- 数据库 schema（`semantic`/`verify`/`run_on` 是 `steps` JSON 列内步骤的 dict 字段，无新表/新列）
- 现有动作分发逻辑（12 种 action 不变，仅新增 `api_call`）
- 现有 `_do_click`/`_do_fill`/`_do_assert` 的成熟回退策略（复用）
- 现有 `execute_case` 主流程骨架（只在指定点插入）
- 被测系统（5.44/5.69）无改动

---

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

### 阶段 1：数据模型 + 上下文骨架

#### 任务 1.1：StepDefinition 新增字段

**文件**: `backend/app/schemas/test_case.py`（L23-57）

**实施步骤**：

1. `StepDefinition` 新增 3 个可选顶层字段：

```python
semantic: Optional[Dict[str, Any]] = Field(
    None, description="语义目标：{type, text, contains, scope, role, nth, container, save_as}")
verify: Optional[Dict[str, Any]] = Field(
    None, description="状态验证：{type, selector, text, timeout, must}")
run_on: Optional[str] = Field(
    None, description="执行时机: always/finally/on_failure/on_success（缺省 always）")
```

2. `TestCaseBase` 新增 `parameters` 字段（模型已有列，schema 未暴露）：

```python
parameters: Dict[str, Any] = Field(default_factory=dict, description="用例参数（上下文变量初始值）")
```

3. `TestCaseCreate`/`TestCaseUpdate` 继承 `TestCaseBase`/新增 `parameters` 可选字段。

**验收标准**：
- `StepDefinition(semantic={...}).model_dump()` 保留 semantic 字段（Pydantic 不丢弃）
- 老步骤（无新字段）`model_dump()` 与改造前一致
- `backend/tests/` 跑 schema 相关测试无回归

#### 任务 1.2：ExecutionContext + 渲染器

**文件**: 新增 `backend/app/executors/ui_context.py`

**实施步骤**：

1. 复用 `TemplateResolver`（PATTERN 支持冒号，天然兼容 `CTX:key`）：

```python
from app.executors.template_resolver import TemplateResolver

class ExecutionContext:
    def __init__(self, parameters: Optional[Dict] = None):
        self._resolver = TemplateResolver()
        self._values = dict(parameters or {})
        # 缺省内置变量
        self._values.setdefault("caseName", "")
        self._values.setdefault("caseId", "")
        # 先渲染 parameters 自身的占位符（如 {"meetingName":"自动化-{__RANDOM_NAME_8__}"}）
        for k, v in list(self._values.items()):
            self._values[k] = self._render_value(v)

    def set(self, key, value): self._values[key] = value
    def get(self, key): return self._values.get(key)

    def render(self, value):  # 递归 dict/list/str
        return self._render_value(value)

    def _render_value(self, value):
        if isinstance(value, dict): return {k: self._render_value(v) for k, v in value.items()}
        if isinstance(value, list): return [self._render_value(v) for v in value]
        if isinstance(value, str) and "__" in value:
            s = value
            # 先跑内置生成器 {__NOW__} 等
            s = self._resolver.resolve(s)
            # 再替换 {__CTX:key__}
            s = re.sub(r"\{__CTX:([^}]+?)__\}", lambda m: str(self._values.get(m.group(1), m.group(0))), s)
            return s
        return value
```

2. `CTX:` 正则：`\{__CTX:([^}]+?)__\}`

**验收标准**（单测 `backend/tests/test_ui_context.py`）：
- `{__RANDOM_NAME_8__}` → 8 位随机串
- `{__CTX:meetingName__}` → 渲染为参数值
- `{__CTX:meetingName__}的议题` → `自动化-xxx的议题`
- 未知占位符保留原文
- 无 `__` 的字符串不做处理（老用例 no-op）

#### 任务 1.3：执行器接入 ctx

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

**实施步骤**：

1. `__init__`（L134 附近）加 `self._ctx = None`
2. `execute_case`（L844 入口）重置：`self._ctx = ExecutionContext(case.get("parameters"))`，并 `set("caseName", case.get("name"))`/`set("caseId", case.get("id"))`
3. `execute_step`（L1066 `params = step.get("params", {})` 之后）渲染 params 局部副本：
   ```python
   params = step.get("params", {}) or {}
   if self._ctx:
       params = self._ctx.render(params)
   ```
   注：渲染前 `params` 可能已经是 dict；`render` 返回新 dict，不写回 step，防重复渲染。

**验收标准**：
- 老用例（无 `__` 占位符）渲染结果与原始 params 一致
- `run_all_cases_sync` 跑多用例时 ctx 每用例重置（无跨用例污染）
- 21 步用例本地回归通过

#### 任务 1.4：前端类型同步

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

**实施步骤**：

```ts
export interface SemanticTarget {
  type?: string; text?: string; contains?: string; scope?: string;
  role?: string; nth?: number; container?: { type?: string; selector?: string; title?: string };
  save_as?: string | Record<string, string>;
}
export interface StepVerify {
  type?: string; selector?: string; text?: string; timeout?: number; must?: boolean;
}
export interface StepDefinition {
  // ...现有字段
  semantic?: SemanticTarget;
  verify?: StepVerify;
  runOn?: 'always' | 'finally' | 'on_failure' | 'on_success';
}
export interface TestCase {
  // ...现有字段
  parameters?: Record<string, any>;
}
```

**验收标准**：`npm run build`（类型检查）通过。

---

### 阶段 2：页面注册表 + 运行时页面身份感知

#### 任务 2.1：page_url_mapping.json 升级注册表

**文件**: `backend/app/data/page_url_mapping.json`

**实施步骤**：

1. 保留现有 `create_meeting` 页，新增字段 `url_patterns`（正则数组）+ `fingerprint`（`{selector, text, min_count}` 数组）
2. 补充 `login` / `home` / `meeting_detail` 页条目（URL 模式参考执行器 `_detect_login_state` L538-567 的信号）
3. 结构保持 `{ "version": ..., "pages": [...] }` 或兼容现有加载代码

**验收标准**：JSON 合法；`PageUrlService` 现有 `recognize_page` 不报错（兼容性）。

#### 任务 2.2：recognize_current_page

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

**实施步骤**：

```python
def recognize_current_page(self, page) -> Optional[dict]:
    """运行时根据当前浏览器 URL + DOM 指纹识别页面身份
    Args:
        page: Playwright Page 对象
    Returns: {"id", "name", "confidence"} 或 None
    """
    url = page.url  # 先经 _replace_base_url 归一化
    # 1. URL patterns 计分
    candidates = []
    for p in self._registry["pages"]:
        score = 0
        for pat in p.get("url_patterns", []):
            if re.search(pat, url): score += 1
        if score: candidates.append((score, p))
    if len(candidates) == 1:
        return {"id": candidates[0][1]["id"], "name": candidates[0][1]["name"], "confidence": 1.0}
    # 2. 0 或歧义 → fingerprint 判存
    best = None
    for p in self._registry["pages"]:
        hit = 0
        for fp in p.get("fingerprint", []):
            try:
                el = page.query_selector(fp["selector"])
                if not el: continue
                if fp.get("text"):
                    if (el.text_content() or "").strip() != fp["text"]: continue
                hit += 1
            except Exception: pass
        if hit >= len([f for f in p.get("fingerprint", []) if f.get("min_count", 1)]):
            best = {"id": p["id"], "name": p["name"], "confidence": 0.8}
            break
    return best
```

**验收标准**：识别不到返回 None；URL 命中单页直接返回；本地 debug 验证 login/home/create_meeting 可识别。

#### 任务 2.3：执行器维护 _current_scope

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

**实施步骤**：

1. `__init__` 加 `self._current_scope = None`
2. 登录成功（`_detect_login_state` 判定后）与 `_navigate_to_target_page`（L828-842）成功后调用 `recognize_current_page` 更新 `self._current_scope`
3. 本阶段仅记录，不参与定位（零行为变化）

**验收标准**：21 步用例照跑；日志可见 scope 识别结果。

---

### 阶段 3：语义解析器接入（核心）

#### 任务 3.1：新增 semantic_resolver.py

**文件**: 新增 `backend/app/services/semantic_resolver.py`

**实施步骤**：

```python
class SemanticResolver:
    def resolve_step(self, page, step, scope, ctx) -> Optional[List[str]]:
        """返回选择器链；无法解析返回 None"""
        if step.get("semantic"):
            return self._resolve_semantic(page, step["semantic"], scope, ctx)
        if self._has_explicit_locator(step):
            return None
        if settings.USE_KEYWORD_MATCHER:
            return self._resolve_by_keyword(page, step, scope, ctx)
        return None

    def _resolve_semantic(self, page, target, scope, ctx):
        # 1. 文本渲染
        text = ctx.render(target.get("text")) if ctx else target.get("text")
        # 2. scope 校验（不符→已登录可导航→直达，否则抛 ScopeMismatch）
        # 3. 映射表 v2 精确键 lookup_by_key(scope, text)
        # 4. 规则候选：extract_keywords + match_element_by_keywords（scope 已知时收缩 base_selector 到容器）
        # 5. DOM 过滤：contains/container/nth
        # 6. 歧义仲裁：len(candidates)>1 → get_candidate_details + claude_service.rank_candidates
        # 7. 产出选择器链：语义定位器 → DOM 派生 CSS → 表格行回退
        # 8. save_as：提取元素文本/input_value 写 ctx
```

**关键实现细节**：
- scope 已知时 `match_element_by_keywords` 的 `base_selector` 收缩：页面注册表 page 条目提供容器选择器前缀（如 `.el-dialog`），提升性能与准确率
- 表格行回退选择器与 `_do_click` 策略 5/6 一致：`.el-table__row:has-text("X") .el-checkbox`
- `_do_click` 已有的 `img:nth-of-type(2)` 类需求 → semantic `nth` 字段处理

**验收标准**：语义目标 `{type:"tab", text:"预定会议", scope:"create_meeting"}` 在真实创建会议页返回稳定选择器链。

#### 任务 3.2：wire 到 execute_step

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

**实施步骤**：

1. `__init__` 加 `self._semantic_resolver = SemanticResolver()`
2. `execute_step`：`params` 渲染（任务 1.3）之后、Phase 3 映射注入（L1073）之前插入：
   ```python
   if settings.SEMANTIC_ENABLED:
       chain = self._semantic_resolver.resolve_step(
           self._page, step, self._current_scope, self._ctx)
       if chain:
           existing = params.get("selectors") or (params.get("selector") and [params["selector"]]) or []
           params["selectors"] = list(chain) + existing   # 显式 selectors 兜底
   ```

**验收标准**：有 semantic 字段的步骤走解析；无 semantic 字段的步骤零影响。

#### 任务 3.3：keyword_matcher facade

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

**实施步骤**：

```python
def resolve_selectors(self, page, step, scope=None) -> Optional[List[str]]:
    """语义解析器调用入口：legacy 自然语言步骤 → 选择器链"""
    name = step.get("name", "")
    action = step.get("action", "click")
    keywords = self.extract_keywords(name)
    elements, selectors = self.match_element_by_keywords(page, keywords, action)
    if not elements and selectors: return [s["selector"] for s in selectors]
    if not selectors:
        el, sel = self.find_element_by_semantic(page, keywords, action)
        return [s.get("selector") for s in sel] if sel else None
    return [s["selector"] for s in selectors[:3]]
```

**验收标准**：legacy 步骤（开启 `USE_KEYWORD_MATCHER`）能返回候选选择器链。

#### 任务 3.4：Claude 按需仲裁 + 修 dead-code

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

**实施步骤**：

1. `rank_candidates` 加 `min_confidence: float = 0.0` 参数：规则置信度 ≥ 阈值或候选 ≤1 时短路不调 Claude
2. `smart_locate_service.py` L610：`self.steps`/`step_idx` 未定义 → 修复为遍历传入的 steps（当前方法签名传 `self._step_results` 或改签名），使 Claude 路径真正可用

**验收标准**：双候选且规则可判时不调 Claude；真实歧义时才调；dead-code 修复后 Claude 路径返回真实 reason。

#### 任务 3.5：debug 只读接口

**文件**: 新增 `backend/app/routers/debug.py`（或复用现有 debug 路由）

**实施步骤**：

```python
@router.get("/resolve")
async def debug_resolve(case_id: str, order: int):
    """在真实页面返回语义目标解析链（只读，不执行）"""
```

**验收标准**：在真实页面（5.44）对语义化步骤返回解析链；无副作用。

---

### 阶段 4：状态验证 + api_call + finally

#### 任务 4.1：_verify_step_state

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

**实施步骤**：

```python
def _verify_step_state(self, step, params):
    """把 verify 配置翻译成 _do_assert 调用，复用 10 种断言"""
    v = step.get("verify") or {}
    if not v: return True
    must = v.get("must", False)
    vtype = v.get("type", "text_visible")
    # 构造 assert params，映射到 _do_assert（L1898-2056）：
    #   text_visible → {"type":"contains","expected":text,"selector":...}
    #   element_visible → {"type":"visible","selector":...}
    #   url_contains → {"type":"url_contains","expected":...}
    #   dialog → {"type":"element_exists","selector":".el-dialog:has-text(...)"}
    try:
        ok = self._do_assert(params, expected)
        if not ok and must: return False
        return True
    except Exception as e:
        if must: raise
        return True
```

**实施步骤**：
1. `execute_step` 动作执行成功（passed）后、记录结果前调用 `_verify_step_state`
2. `verify.must=false`（缺省）失败只 warn；`true` 失败标记步骤 failed
3. `config.enforce_verification`（默认 false）：为 true 时 `expected` 非空且无 verify 的 click/fill 步骤走默认校验（弹窗/文本/URL）

**验收标准**：`verify{type:text_visible, text:"预定会议", must:true}` 在切换失败时步骤真实 failed。

#### 任务 4.2：_do_api_call

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

**实施步骤**：

```python
def _do_api_call(self, params):
    """复用 ApiTestExecutor 发送单次 HTTP 请求"""
    from app.executors.api_test_executor import ApiTestExecutor
    from app.executors.template_resolver import TemplateResolver
    api_case = params.get("api_case") or {}
    save_as = params.get("save_as")
    # 渲染 api_case（body/url/headers）
    resolver = TemplateResolver()  # 或复用 self._ctx
    api_case = self._ctx.render(api_case) if self._ctx else api_case
    client_config = {"target": {"base_url": settings.TARGET_URL, "verify_ssl": False, "timeout": 30}}
    ex = ApiTestExecutor(client_config)
    try:
        ex.start()
        result = ex.execute_case({"id": "", "name": api_case.get("name", ""), "steps": api_case.get("steps", {})})
    finally:
        ex.stop()
    if result.status != "passed":
        raise Exception(f"api_call 失败: {result.error}")
    # save_as: 从响应体 JSONPath 提取写 ctx
    if save_as and self._ctx:
        body = result.step.response_body if result.step else ""
        import json
        for key, path in save_as.items():
            value = AssertionEngine._extract_json_path(body, path)
            self._ctx.set(key, value)
    return result
```

**实施步骤**：
1. 动作分发（L1133-1196）加 `elif action == "api_call": result = self._do_api_call(params)`
2. `_do_api_call` 内部对 `params` 先渲染（`api_case` 的 body/url/headers）

**验收标准**：api_call 步骤真实发送请求；`save_as` 从响应体取到值写入 ctx；失败时步骤真实 failed。

#### 任务 4.3：run_on finally 语义

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

**实施步骤**：

1. `execute_case` 主循环（L937-1014）"失败后标记剩余跳过"逻辑（L1005-1013）：只跳过 `run_on in ("always", "on_success")` 的步骤；`"finally"`/`"on_failure"` 不跳过
2. 外层 `finally`（L1025-1027）调 `_run_finally_steps(steps, result, callback)`：
   ```python
   def _run_finally_steps(self, steps, result, callback):
       for step in steps:
           if step.get("run_on") != "finally": continue
           try:
               sr = self.execute_step(step, callback=callback)
               if sr.status != "passed" and result.status in ("passed", None):
                   result.status = "failed"
                   result.error_message = (result.error_message or "") + "；清理步骤失败"
           except Exception as e:
               if result.status in ("passed", None):
                   result.status = "failed"
                   result.error_message = (result.error_message or "") + f"；清理步骤异常: {e}"
   ```

**验收标准**：主流程失败时 finally 步骤仍执行；finally 失败不覆盖主结果（仅标注）；异常路径也执行。

---

### 阶段 5：映射表 v2 显式字典

#### 任务 5.1：elements_mapping.json v2 迁移

**文件**: `backend/app/data/elements_mapping.json`

**实施步骤**：

1. 按 PRD 第五章格式重建：`version: "2.0"` + `pages`（每个含 id/name/url_patterns/fingerprint/elements）+ `shared_elements`
2. 现有 231 条按 tier 归入对应 page 的 `elements`（键保留原语义名，值为 `{selectors:[...], type, role, description}`）
3. 保留旧 v1 扁平结构作为兼容备份（或迁移脚本 `backend/scripts/migrate_mapping_v2.py` 输出新文件）

**验收标准**：JSON 合法；`lookup_by_key(page_id, key)` 命中；`get_selector` 老路径不报错。

#### 任务 5.2：element_mapping_service 加 lookup_by_key + 关停评分

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

**实施步骤**：

```python
def lookup_by_key(self, page_id: str, key: str) -> Optional[Dict]:
    """v2 显式字典精确查找（无评分）"""
    page = self._v2_registry.get(page_id)
    if page:
        el = page.get("elements", {}).get(key)
        if el: return {"selectors": el["selectors"], "type": el.get("type"), "key": key}
    shared = self._v2_registry.get("shared_elements", {}).get(key)
    if shared: return {"selectors": shared["selectors"], "type": shared.get("type"), "key": key}
    return None
```

**实施步骤**：
1. `__init__` 加载 v2 结构（兼容 v1 扁平结构共存）
2. `get_selector`（L132-168）保留，但 `_exact_match`/`_fuzzy_match`（L224-484）前加 `if not settings.USE_FUZZY_MAPPING: return None`（默认关）
3. 执行器 Phase 3 注入（L1073-1104）改为优先 `lookup_by_key(self._current_scope, step.get("element_key") or step.semantic.text)`，未命中回退 legacy

**验收标准**：`USE_FUZZY_MAPPING=false` 时 `get_selector` 直接返回 None；`lookup_by_key` 精确命中；全量用例回归对比通过率持平或更稳。

#### 任务 5.3：映射表管理（可选）

**文件**: `backend/scripts/` 离线脚本

**实施步骤**：用 `selector_extractor.extract_selectors`（L190-379）+ `smart_locate_service` 对真实页面实测，回填 v2 表缺项。

**验收标准**：新增条目均为显式键 + 选择器链（无评分规则）。

---

### 阶段 6：前端支持 + 语义化重写示范

#### 任务 6.1：CaseStepEditor 增强

**文件**: `frontend/src/components/CaseStepEditor.vue`

**实施步骤**：

1. 新增"语义目标"编辑区：type 下拉（button/input/checkbox/row_checkbox/tab/icon/text）、text/contains/scope/nth/container 输入、save_as
2. `ASSERT_TYPE_OPTIONS`（L282-291）8 种补到 10 种；`url` → `url_contains`（后端 `_do_assert` L2006 同时兼容 `url`）

**验收标准**：编辑器可编辑 semantic/verify 字段；断言类型前后端对齐。

#### 任务 6.2：Recorder / stepParser / types 同步

**文件**: `frontend/src/views/Recorder.vue` + `frontend/src/utils/stepParser.ts` + `frontend/src/types/case.ts`

**实施步骤**：
1. `recorder.py`（L149-178）`StepDefinition.model_dump` 天然透传新字段——前端录制代理若发 semantic 则自动保存
2. `stepParser.ts`（L95-110）从描述推断 semantic target（如"点击【X】"→`{type:button, text:X}`）

**验收标准**：录制/编辑的语义化步骤保存后 schema 校验通过。

#### 任务 6.3：语义化重写「新建会议」用例

**文件**: 新建用例（API 或脚本 `backend/scripts/`）

**实施步骤**：

按 PRD 第十章示范重写，关键点：
- `parameters`: `{"meetingName":"自动化-{__RANDOM_NAME_8__}", "room":"北京展厅会议室", "participant":"admin@xty12"}`
- 步骤 10/13/15/19 引用 `{__CTX:...__}`
- 步骤 12/14/15/17/20/21 用 semantic + verify
- 新增步骤 22 `api_call` + `run_on: finally` 清理

**验收标准**：新旧两版用例都通过；新版不依赖 `USE_FUZZY_MAPPING`；连跑两次无残留会议。

---

## 三、验收标准汇总

| 阶段 | 验收 |
|------|------|
| 1 | 渲染器单测全过；21 步用例回归通过 |
| 2 | debug 返回识别页；行为零变化 |
| 3 | `/api/debug/resolve` 真实页面返回解析链；老用例零影响 |
| 4 | api_call/finally/verify 单测 + 连跑两次无残留 |
| 5 | 全量回归持平或更稳；`USE_FUZZY_MAPPING` 可回切 |
| 6 | 新旧两版用例都通过；`npm run build` + pytest 回归 |
| 总 | 每阶段部署服务器 5.60 验证；更新 HANDOFF |

---

## 四、测试计划

### 4.1 单元测试

- `backend/tests/test_ui_context.py`（新增）：渲染器
- `backend/tests/test_semantic_resolver.py`（新增）：语义目标 → 选择器链（mock page）
- `backend/tests/test_element_mapping_service.py`：加 v2 lookup 用例
- `backend/tests/test_schemas.py`：新字段透传

### 4.2 集成验证

- `/api/debug/resolve` 真实页面（5.44）解析链
- 服务器 5.60 部署后：API 触发 + UI 页面点击双链路执行
- 新"API 建会议+UI 取消+finally 清理"用例连跑两次，断言 5.44 会议列表 0 残留

### 4.3 回归基线

- 21 步用例「会议管理-新建会议-czj录入」每阶段跑通
- `pytest tests/ -v --cov=app` 后端全量
- `npm run build` 前端构建

---

## 五、风险评估

| 风险 | 影响 | 缓解 |
|------|------|------|
| Pydantic 丢字段 | semantic 静默丢弃 | 同步声明 schema；recorder 透传测试 |
| 渲染误伤老用例 | 回归 | 仅含 `__` 才渲染；每阶段 flag 可关 |
| 解析器注入坏选择器 | 执行失败 | 仅 semantic 存在才启用；显式 selectors 兜底；debug 接口预检 |
| 关停模糊评分回归 | 老用例失败 | `USE_FUZZY_MAPPING` 可回切；回归只加显式条目 |
| finally 掩盖主结果 | 误判 | finally 失败不覆盖主状态，仅标注 |
| 前端映射表延迟 | 阶段 5 受阻 | 阶段 1-4 不依赖；v1 表兜底 |

---

## 六、实施记录

| 日期 | 阶段 | 状态 | 备注 |
|------|------|------|------|
| 2026-08-18 | PRD + 计划 | ✅ | 本文档 |
| 2026-08-18 | 阶段 1 | ⏳ | 待实施 |
| 2026-08-18 | 阶段 2 | ⏳ | 待实施 |
| 2026-08-18 | 阶段 3 | ⏳ | 待实施 |
| 2026-08-18 | 阶段 4 | ⏳ | 待实施 |
| 2026-08-18 | 阶段 5 | ⏳ | 待实施 |
| 2026-08-18 | 阶段 6 | ⏳ | 待实施 |

---

## 七、后续工作

- 前端开发按 PRD 第五章提供各业务页面映射表 v2
- 测试数据自建能力扩展到更多业务域（会议室、参会人、工单等）
- 语义目标类型扩展（拖拽、上传、分页翻页等）
- 用例间 setup/teardown（用 `depends_on` + `config.teardown` 扩展）

---

## 八、附录

### 8.1 关键文件位置

- `backend/app/executors/playwright_executor.py`：execute_step L1031、动作分发 L1133、_do_click L1308、_do_assert L1898、execute_case L844
- `backend/app/schemas/test_case.py`：StepDefinition L23-57
- `backend/app/executors/template_resolver.py`：PATTERN L34
- `backend/app/services/element_mapping_service.py`：get_selector L132、_exact_match L224
- `backend/app/services/page_url_service.py`：recognize_page L119
- `backend/app/executors/api_test_executor.py`：execute_case L424
- `backend/app/services/keyword_matcher.py`：match_element_by_keywords L468、find_element_by_semantic L901
- `backend/app/services/claude_service.py`：rank_candidates L81

### 8.2 Feature Flags

| 配置 | 默认 | 位置 |
|------|------|------|
| `SEMANTIC_ENABLED` | `true` | `backend/app/config.py` |
| `USE_KEYWORD_MATCHER` | `false` | `backend/app/config.py` |
| `USE_FUZZY_MAPPING` | `false` | `backend/app/config.py` |
| `ENFORCE_VERIFICATION` | `false` | `backend/app/config.py` |
