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

docs: 新增语义化用例执行机制 PRD 与执行计划文档

阶段 6 交付工作流:PRD → 执行计划 → 代码 → 验证 → HANDOFF,文档先行可追溯。
- _PRD_复杂用例通用执行机制_语义化用例执行.md
- _执行计划_复杂用例通用执行机制_语义化用例执行.md
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 d0794041
# PRD — 复杂用例通用执行机制:语义化用例执行
> **文档版本**: v1.0
> **创建日期**: 2026-08-18
> **作者**: Claude Code(czj 确认)
> **状态**: 待确认
> **关联文档**: `HANDOFF_UI自动化.md`(会话 9-20 全部补丁记录)
---
## 一、背景与问题
### 1.1 当前困境:复杂用例靠"打补丁"才能稳定
目标用例「会议管理-新建会议-czj录入」(21 步)经过 10+ 个会话、几十处补丁后才在服务器 5.60 稳定(API + UI 双链路 100% 通过)。但每处修复都是**针对特定页面/特定文本的特例**
| 补丁位置 | 补丁内容 | 本质 |
|---------|---------|------|
| `element_mapping_service.py` `_exact_match`(L224-441) | 30+ 条加分规则("预定会议"+20、"编辑"+12、"完成"+16……) | 在模糊文本评分上叠补丁,规则互相踩踏 |
| `playwright_executor.py` | `img:nth-of-type(2)` 编辑按钮、行级 checkbox、原选择器优先等特例 | 为特定 DOM 结构写死 |
| `elements_mapping.json` | 逐条补子页面条目 | 每个新页面都要人工探测 |
**结果**:每个新复杂用例(新建工单、预订会议室、新建会议)都要重新走"实测 DOM → 加映射条目 → 调加分 → 部署验证"的循环,问题无限重复。
### 1.2 根本原因:范式缺陷
当前范式是:**存储易碎 CSS 选择器 + 模糊文本评分匹配 + 无上下文 + 无页面状态**
1. **选择器易碎**:录制/手写的 CSS 选择器依赖页面结构,动态渲染数据(会议室名称、参会人账号)无法写入静态选择器
2. **模糊评分不可靠**:30+ 条加分规则是在"文本相似度"上人工堆叠,无法根治"步骤文本 → 错误页面层级元素"的误配
3. **无执行期上下文**:UI 执行链路完全没有变量替换(`execution_service.py` 直接传 `case.to_dict()`),步骤之间无法传递数据
4. **无页面状态感知**`page_url_service.recognize_page` 是"从静态步骤预测目标页",不是"运行时读取当前浏览器在哪一页";点击后不校验页面状态 → 存在"假通过"
5. **无数据自建/自清理**:测试数据依赖被测系统已有数据,残留会议污染下次执行
### 1.3 方案来源(前端协同)
前端开发人员将提供**页面元素 + 页面路由映射表**
- 静态页面元素基本**唯一**`data-key` / `data-id` / 稳定 class / 唯一文本)
- 主要难点是**动态渲染数据**(会议室名称、参会人账号等运行期值)
### 1.4 解决思路
**范式转变:从"记录选择器"到"记录业务意图"。**
用例步骤不再只存 CSS 选择器,而是存**业务意图**`semantic` 目标:按钮[预定会议]、表格行[会议室=X]、勾选[参会人=admin]),执行期由解析器结合页面真实 DOM 现场定位;配合**上下文变量**(动态数据)、**页面状态验证**(防假通过)、**数据自建/自清理**(无残留)三大机制。
---
## 二、目标与非目标
### 2.1 目标
1. **建立语义化步骤模型**:步骤新增 `semantic`(语义目标)/`verify`(状态验证)/`run_on`(执行时机)字段,业务意图显式表达
2. **执行期现场定位**:语义目标 → 候选选择器 → DOM 验证 → 执行,复用现有全部成熟点击/填充回退策略
3. **上下文变量替换**:用例参数 + 前步 `save_as` 数据 + 内置生成器,UI 执行链路首次支持变量
4. **运行时页面身份感知**:页面注册表 + URL 模式 + DOM 指纹,语义目标带 `scope` 根治跨层级误配
5. **状态验证防假通过**:每步执行后校验预期页面状态,失败即真实失败
6. **数据自建/自清理**`api_call` 步骤自建数据,`finally` 步骤无论成败都清理
7. **映射表降级为显式查找字典**:废弃 30+ 条模糊评分,前端映射表按"页面作用域 + 显式语义键"精确查找
8. **完全向后兼容**:现有 21 步用例不填新字段、不依赖新机制,必须持续通过
### 2.2 非目标
1. 不替换现有执行器——语义化是**新增的步骤表示能力**,老步骤走原路径
2. 不引入坐标定位(已论证不稳定,见 `_难点分析_智能定位功能策略可行性评估报告.md`
3. 不做全局用例前置/后置(用 `run_on="finally"` 用例内清理,后续用 `depends_on` 扩展)
4. 不依赖每步 Claude 仲裁(规则优先,Claude 仅候选歧义时按需)
5. 不修改前端代码(使用前端已提供的 data 属性/稳定选择器)
---
## 三、总体架构
### 3.1 选择器优先级链(重构后)
```
execute_step 顶部(读 params 之后、Phase 3 映射注入之前):
1. step.semantic 结构化语义目标(新增,最高)
2. params.selector/selectors/page_key+element_key(显式选择器,录制/手写)
3. mapping v2 显式字典 lookup_by_key(page_id, element_key)(前端映射表,精确查找)
4. keyword_matcher 规则解析(仅 legacy 自然语言步骤 + 配置开关)
5. Claude 仲裁(仅候选歧义时)
```
解析结果统一**注入 `params["selectors"]`**,继续走现有 `_do_click`/`_do_fill`/`_do_check`/`_do_assert` 全部成熟回退策略(iframe/force/表格行/checkbox 状态校验),最大化复用与向后兼容。
### 3.2 架构图
```
┌────────────────────────────────────────────────────────────────┐
│ Playwright 执行器 │
│ playwright_executor.py │
│ execute_step: ctx.render(params) → semantic 解析 → 映射 v2 → 动作 │
└───────────────┬──────────────────────────────────┬─────────────┘
│ │
┌───────────▼──────────┐ ┌─────────────▼───────────┐
│ ExecutionContext │ │ SemanticResolver │
│ ui_context.py │ │ semantic_resolver.py │
│ {__CTX:key__} 渲染 │ │ 目标→候选→DOM验证→注入链 │
└──────────────────────┘ └─────────────┬───────────┘
┌──────────────────┬──────────────────┬─────▼──────────┐
▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 页面注册表 │ │ 映射表 v2 │ │ keyword_ │ │ Claude 仲裁 │
│ page_url_ │ │ 显式字典 │ │ matcher 规则 │ │ rank_candidates│
│ mapping.json │ │ lookup_by_key│ │ (legacy) │ │ (按需) │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
```
---
## 四、六大机制设计
### 4.1 语义目标数据模型(机制一)
**位置**`StepDefinition` 新增**顶层可选字段** `semantic` / `verify` / `run_on`(不放 params——params 执行期被 Phase 3 改写污染;顶层字段 recorder `model_dump` 天然透传)。
**注意**:Pydantic v2 默认 `extra="ignore"`,前端若发 `semantic` 而 schema 未声明会被静默丢弃——schema 必须同步声明。
```json
{
"order": 12,
"name": "时间切换为:预定会议",
"action": "click",
"semantic": {
"type": "tab",
"text": "预定会议",
"contains": null,
"scope": "create_meeting",
"role": "tab",
"nth": 0,
"container": { "type": "dialog", "selector": ".el-dialog", "title": "新建会议" },
"save_as": null
},
"verify": {
"type": "text_visible",
"selector": ".time_btn .el-tabs__item.is-active",
"text": "预定会议",
"timeout": 5000,
"must": true
},
"run_on": "always"
}
```
**`semantic.type`**(驱动候选生成策略):`button` / `input` / `textarea` / `checkbox` / `row_checkbox` / `row` / `tab` / `link` / `menu` / `icon` / `text` / `dialog`
**字段说明**
| 字段 | 说明 |
|------|------|
| `text` | 主匹配文本(渲染后用于映射表精确键 + DOM 文本/placeholder/aria-label 匹配) |
| `contains` | 行/单元格次级过滤(如表格行必须含 `admin@xty12`) |
| `scope` | 页面注册表 id,只在正确页面内定位(根治跨层级误配) |
| `role` | 语义角色(映射 Playwright `get_by_role`) |
| `container` | 父容器约束 `{type: dialog/drawer/table/tab/card, selector, title}` → 生成组合选择器 |
| `nth` | 同命中取第 N 个(如 `.delimg img:nth-of-type(2)`) |
| `save_as` | 把定位到的元素文本/值写入上下文 |
**与现有 `selectors` 的关系**`semantic` 存在时解析器产出选择器链注入 `params["selectors"]`,二者不互斥;显式 `selectors` 始终是兜底链。
### 4.2 上下文系统与变量替换(机制二)
**位置**:新增 `backend/app/executors/ui_context.py`**复用 `TemplateResolver`**(PATTERN `\{__([A-Za-z_+0-9:]+?)__\}` 允许冒号)。
**占位符语法**(与现有 `{__XXX__}` 兼容):
| 占位符 | 说明 | 来源 |
|--------|------|------|
| `{__NOW__}` `{__NOW_+2_H__}` `{__RANDOM_NAME_8__}` `{__UUID__}` `{__RANDOM_INT_1_100__}` | 内置生成器 | 复用 `TemplateResolver` |
| `{__CTX:meetingName__}` | 引用上下文变量 | 新增 `CTX:` token handler |
| `{__CTX:meetingName__}的议题` | 占位符可嵌任意字符串 | 递归字符串替换 |
**`ExecutionContext`**
- `values: Dict[str, Any]`,初始化来源:`case.parameters``{"meetingName":"自动化-{__RANDOM_NAME_8__}", "room":"北京展厅会议室", ...}``execute_case` 入口按序渲染一次);缺省内置 `{__CTX:caseName__}` / `{__CTX:caseId__}`
- `render(value)`:递归 dict/list/str;字符串先跑内置生成器,再替换 `{__CTX:key__}`;未解析占位符保留原文 + 日志警告
- `set(key, value)` / `get(key)`
**渲染点**
- `execute_step` 顶部整步渲染(url/value/selector/expected/semantic.text/verify.text)
- `_do_api_call` 内对 api_case 的 body/url/headers 再渲染
- **渲染后不写回 step**,只作用于 params 局部副本——防重复渲染放大 `{__NOW__}` 类值
**防污染**`run_all_cases_sync`(execution_service.py L306-376)在专用线程复用一个 executor 跑多用例——`execute_case` 入口必须**每用例重置 ctx**
### 4.3 执行期定位解析器(机制三,核心)
**位置**:新增 `backend/app/services/semantic_resolver.py`
**核心流程**
```
resolve_step(page, step, scope, ctx):
if step.get("semantic"): return resolve_semantic(page, step["semantic"], scope, ctx)
if has_explicit_locator(step): return None # 用原样 params
if config.USE_KEYWORD_MATCHER: return resolve_by_keyword(page, step, scope, ctx)
return None
```
**resolve_semantic(语义目标 → 选择器链)**
1. **文本渲染**`text = ctx.render(target.text)`
2. **Scope 校验**`target.scope` 与当前页面身份不符 → 若已登录且注册表有该页 URL → 直达导航;否则抛 `ScopeMismatch`(杜绝跨层级误配)
3. **映射表 v2 精确键**`mapping.lookup_by_key(target.scope, target.text or element_key)` → 命中直接返回选择器链
4. **规则候选**`keywords = extract_keywords(text)`;按 `type` 映射动作;`match_element_by_keywords(page, keywords, action)`;无命中走 `find_element_by_semantic`
- **性能关键**`match_element_by_keywords` 全页扫会枚举上千元素——scope 已知时把 `base_selector` 收缩到 scope 容器内,既提速又防跨容器误配
5. **DOM 过滤**:按 `contains` / `container` / `nth` 过滤候选
6. **歧义仲裁(Claude 按需)**:仅当过滤后候选 **>1** 且规则无法用 `container`/`nth` 定夺时,`get_candidate_details``claude_service.rank_candidates``confirmed=false` 或失败回退第 0 个
7. **产出选择器链**(注入 `params["selectors"]`):语义定位器(`role:/text:/placeholder:/label:/testid:`)→ DOM 派生 CSS → 表格行回退(`.el-table__row:has-text(...)`,与 `_do_click` 策略 5/6 一致)
8. **save_as**:定位后提取元素文本/input_value 写入 ctx
**resolve_by_keyword(legacy 自然语言步骤过渡路径)**`extract_keywords``match_element_by_keywords` → 歧义走 Claude → 注入。这是把 `keyword_matcher.py`(现在完全未接入执行器)真正 wire 到执行链。
**wire 点**`execute_step``params = step.get("params", {})`(L1066)之后、Phase 3 映射注入(L1073)之前调用,命中则 `params["selectors"] = chain`
### 4.4 页面注册表(机制四:运行时页面身份感知)
**位置**`page_url_mapping.json` 从"1 页静态识别"升级为"页面注册表";`page_url_service.py` 新增 `recognize_current_page`
```json
{
"pages": [
{ "id": "create_meeting", "name": "新建会议",
"url_patterns": [".*meetingV3.*CreateMeeting.*"],
"fingerprint": [
{ "selector": ".el-dialog__title", "text": "新建会议", "min_count": 1 },
{ "selector": ".topic_item input", "min_count": 1 }
],
"skip_steps": { ... } },
{ "id": "login", "name": "登录页",
"url_patterns": [".*#/login.*", ".*platform%2Flogin.*"],
"fingerprint": [ { "selector": "input[placeholder*=\"手机号\"]", "min_count": 1 } ] },
{ "id": "home", "name": "主页",
"url_patterns": [".*Home.*"],
"fingerprint": [ { "selector": ".block", "min_count": 1 } ] },
{ "id": "meeting_detail", "name": "会议详情",
"url_patterns": [".*MeetingDetail.*"],
"fingerprint": [ { "selector": ".buttom_btn", "min_count": 1 } ] }
]
}
```
**`recognize_current_page(page)`**
- `url_patterns` 逐一 `re.search(page.url)` 计分(URL 先经 `_replace_base_url` 归一化)
- URL 命中 ≤1 页直接返回;0 或歧义时跑 `fingerprint``page.query_selector(sel)` 且(text 非空时)`text_content()==text` 计数 ≥ `min_count`
- 返回 `(scope_id, confidence)`,全无命中返回 `None`
**运行时**:登录成功/导航后更新 `self._current_scope`;语义目标带 `scope` 时只有 `current_scope == scope`(或可导航)才放行解析。
### 4.5 状态验证(机制五:防假通过)
**位置**`execute_step` 动作执行后、记录结果前。
**`verify` 字段**(顶层可选):
| verify.type | 含义 | 复用 |
|------------|------|------|
| `text_visible` / `text_hidden` | 文本出现/消失 | `_do_assert` contains |
| `element_visible` / `element_hidden` | 元素可见/隐藏 | `_do_assert` visible/hidden |
| `url_contains` | URL 包含 | `_do_assert` url_contains |
| `dialog` / `drawer` | 弹窗/抽屉出现 | `_do_assert` element_exists |
| `value` | 输入值相等 | `_do_assert` value |
- `verify.must` 缺省 `false`:验证失败只 warn 不 raise;`true` 则步骤真实失败
- `expected` 是"人类可读期望",`verify` 是"机器可校验状态";`config.enforce_verification`(默认 false)开启时,`expected` 非空且无 `verify` 的 click/fill 步骤走默认校验
### 4.6 数据自建/自清理(机制六:api_call + finally)
**`api_call` 步骤**(新 action):
```json
{
"order": 1, "name": "创建会议数据", "action": "api_call",
"params": {
"api_case": {
"name": "创建会议API",
"steps": {
"method": "POST", "url": "/api/meeting/create",
"body": { "name": "{__CTX:meetingName__}", "roomId": "r1" },
"assertions": [ { "type": "status_code", "operator": "equals", "value": 200 } ]
}
},
"save_as": { "meetingId": "$.data.id" }
}
}
```
- **复用 `ApiTestExecutor`**`api_test_executor.py` L375):构造 client_config(base_url=`settings.TARGET_URL`),`start()``execute_case(api_case)``stop()`。同步、不依赖浏览器、无 Windows/Playwright 事件循环冲突,可在 UI 执行线程内直接调用
- `save_as``AssertionEngine._extract_json_path`(L318)从响应体取路径值写 ctx
- `ApiCaseResult.status == "failed"` → raise(步骤真实失败)
**`run_on` 语义**(顶层可选,默认 `"always"`):
| 值 | 行为 |
|----|------|
| `always` | 正常步骤 |
| `finally` | 无论前面成败都执行;失败不掩盖主结果(主结果 passed 而 finally 失败 → 用例 failed 并标注"清理步骤失败") |
| `on_failure` / `on_success` | 条件执行(预留) |
**execute_case 改动**
- 主循环遇失败时,"剩余步骤标记跳过"只跳过 `run_on=="always"``"on_success"``"finally"`/`"on_failure"` 不跳过
- 外层 `finally``_run_finally_steps`(异常路径也保证执行)
---
## 五、映射表 v2 格式规范(前端开发填写)
**目标**`elements_mapping.json` 从"231 条扁平 + 模糊评分"升级为"页面作用域 + 显式语义键 + 选择器链"。
**关键变化**
1. 元素**嵌套在 page 下**,键即显式语义键(`lookup_by_key(page_id, key)` 纯 dict 查找,无评分)
2. 每元素是**有序选择器链**`selectors` 数组,高优先级在前)
3. `type`/`role` 供语义目标类型映射
4. `shared_elements` 处理登录页/全局弹窗按钮等跨页元素
5. 旧 v1 扁平 `elements` 保留加载,legacy 键仍可用
```json
{
"version": "2.0",
"updated_at": "2026-08-18",
"source": "前端开发人员提供:页面元素 + 页面路由映射表",
"pages": [
{
"id": "create_meeting",
"name": "新建会议",
"url": "https://.../#/meetingV3?...#/CreateMeeting",
"url_patterns": [".*meetingV3.*CreateMeeting.*"],
"fingerprint": [
{ "selector": ".el-dialog__title", "text": "新建会议", "min_count": 1 },
{ "selector": ".topic_item input", "min_count": 1 }
],
"elements": {
"预定会议": {
"selectors": [".time_btn .btns:has-text(\"预定会议\")", "button:has-text(\"预定会议\")"],
"type": "tab", "role": "tab",
"description": "时间切换 Tab:预定会议"
},
"会议名称输入框": {
"selectors": ["input[placeholder*=\"会议名称\"]", ".topic_item input"],
"type": "input",
"description": "创建会议页会议名称输入框"
},
"会议室选择": {
"selectors": [".el-table__row:has-text(\"北京展厅会议室\") .el-checkbox", ".el-checkbox:has-text(\"北京展厅会议室\")"],
"type": "row_checkbox",
"description": "右侧会议室表格行勾选"
},
"编辑按钮": {
"selectors": [".room .delimg", ".room .delimg img"],
"type": "icon",
"description": "左侧会议室框编辑图标(切换参会人配置模式)"
},
"参会人勾选": {
"selectors": [".el-table__row:has-text(\"admin@xty12\") .el-checkbox", ".el-checkbox:has-text(\"admin@xty12\")"],
"type": "row_checkbox"
},
"确定创建按钮": { "selectors": ["button:has-text(\"确定创建\")"], "type": "button" },
"取消会议按钮": { "selectors": [".buttom_btn button:has-text(\"取消会议\")"], "type": "button" }
}
}
],
"shared_elements": {
"登录页-账号输入框": { "selectors": ["input[placeholder*=\"手机号\"]"], "type": "input" },
"登录页-密码输入框": { "selectors": ["input[type=\"password\"]"], "type": "input" },
"登录按钮": { "selectors": ["button:has-text(\"登录\")"], "type": "button" },
"全局确定按钮": { "selectors": [".el-message-box button.el-button--primary:has-text(\"确定\")"], "type": "button" }
}
}
```
**前端开发交付要求**
- 每个业务页面:`id`(如 `create_meeting`)+ `url_patterns`(进入该页的 URL 正则)+ `fingerprint`(页面特征元素选择器)+ `elements`(页面内全部可交互元素)
- 每个元素:`selectors` 数组(2-3 个回退)+ `type`(button/input/tab/row_checkbox/icon 等)
- 跨页元素(登录、全局弹窗按钮)放 `shared_elements`
- **动态数据不写进映射表**:会议室名称、参会人账号等运行期值由语义目标 `text`/`contains` 携带(渲染自上下文),映射表只放静态结构
---
## 六、分阶段实施计划
| 阶段 | 内容 | 关键文件 | 验证 |
|------|------|---------|------|
| **1** | 数据模型 + 上下文骨架:`semantic`/`verify`/`run_on` 字段、`parameters` 暴露、`ui_context.py`、执行器 ctx 渲染 | `schemas/test_case.py``executors/ui_context.py``executors/playwright_executor.py``frontend/src/types/case.ts` | 渲染器单测 + 21 步用例回归 |
| **2** | 页面注册表 + 运行时页面身份:`url_patterns`+`fingerprint``recognize_current_page` | `page_url_mapping.json``page_url_service.py` | debug 接口返回识别页,行为零变化 |
| **3** | 语义解析器接入(核心):`semantic_resolver.py`、keyword_matcher wire、Claude 按需 | `services/semantic_resolver.py``playwright_executor.py``keyword_matcher.py``claude_service.py` | `/api/debug/resolve` 真实页面返回解析链 |
| **4** | 状态验证 + api_call + finally | `playwright_executor.py``_verify_step_state`/`_do_api_call`/`_run_finally_steps`) | 新用例连跑两次断言无残留 |
| **5** | 映射表 v2 + 废弃模糊评分 | `elements_mapping.json``element_mapping_service.py``lookup_by_key`) | 全量回归对比,回归只加显式条目 |
| **6** | 前端支持 + 语义化重写示范 | `CaseStepEditor.vue``Recorder.vue``stepParser.ts``types/case.ts`、新建语义化用例 | 新旧两版用例都通过 |
---
## 七、配置项
| 配置 | 默认 | 说明 |
|------|------|------|
| `SEMANTIC_ENABLED` | `true` | 总开关:关闭后执行器完全走旧范式 |
| `USE_KEYWORD_MATCHER` | `false` | legacy 自然语言步骤的 keyword_matcher 回退 |
| `USE_FUZZY_MAPPING` | `false` | 映射表 v1 模糊评分(阶段 5 后关停,可回切) |
| `ENFORCE_VERIFICATION` | `false` | `expected` 非空且无 `verify` 时强制默认校验 |
---
## 八、验收标准
### 8.1 功能验收
| 场景 | 预期 |
|------|------|
| 语义目标步骤执行 | 解析器命中,复用成熟点击/填充策略,步骤通过 |
| 动态数据 | `{__CTX:meetingName__}` 渲染为同一值,创建/断言/清理引用一致 |
| 跨层级误配 | `scope` 不符时报 `ScopeMismatch` 或直达导航,不再误配到错误页面元素 |
| 假通过 | `verify.must=true` 步骤状态验证失败 → 真实 failed |
| 残留数据 | `finally` 步骤无论成败都执行,连跑两次无残留会议 |
| 老用例回归 | 现有 21 步用例全程通过(不填新字段) |
### 8.2 精度验收
| 指标 | 当前 | 目标 |
|------|------|------|
| 复杂用例从编写到稳定 | 10+ 会话 / 几十处补丁 | 语义化步骤一次编写基本稳定 |
| 映射表命中 | 模糊评分误配风险 | 显式键精确查找 100% |
| 动态数据定位 | 无机制,靠打补丁 | 上下文 + 行级文本定位 |
### 8.3 回退验证
| 场景 | 预期 |
|------|------|
| `SEMANTIC_ENABLED=false` | 执行器走旧范式,行为与改造前一致 |
| 语义目标无法解析 | 回退到显式 `selectors` 兜底链 |
| Claude 调用失败/超时 | 回退第 0 个候选,不阻塞执行 |
| 页面识别不到 | `recognize_current_page` 返回 None,零行为变化 |
---
## 九、风险评估
| 风险 | 影响 | 缓解 |
|------|------|------|
| 语义目标 schema 未同步声明被丢弃 | 静默丢字段 | schema 同步声明,recorder 测试透传 |
| 渲染误伤老用例 | 老用例回归 | 渲染仅在含 `__` 时生效;每阶段 flag 可关 |
| 解析器注入坏选择器 | 执行失败 | 仅 `semantic` 存在才启用;显式 selectors 兜底;debug 接口预检 |
| 关停模糊评分致回归 | 老用例失败 | `USE_FUZZY_MAPPING` 可回切;回归只加显式条目 |
| finally 抛错掩盖主结果 | 结果误判 | finally 失败不覆盖主状态,仅标注 |
| 前端映射表延迟提供 | 阶段 5 受阻 | 阶段 1-4 不依赖映射表;v1 表继续兜底 |
---
## 十、「新建会议」语义化重写示范
用例级参数:
```json
"parameters": {
"meetingName": "自动化-{__RANDOM_NAME_8__}",
"room": "北京展厅会议室",
"participant": "admin@xty12"
}
```
| 原步骤 | 语义化改写 |
|--------|-----------|
| 12 时间切换为:预定会议 | `semantic{type:tab, text:预定会议, scope:create_meeting}` + `verify{text_visible, text:预定会议}` |
| 13 会议议题输入 | `semantic{type:input, text:议题, scope:create_meeting}``params.value="{__CTX:meetingName__}的议题"` |
| 14 编辑按钮 | `semantic{type:icon, text:编辑, scope:create_meeting, container:{type:card, selector:".room"}}` + `verify{element_visible, selector:".room .user_list"}` |
| 15 勾选参会人 | `semantic{type:row_checkbox, text:"{__CTX:participant__}", contains:"{__CTX:participant__}", scope:create_meeting, container:{type:table, selector:".el-table"}}` |
| 17 确定创建 | `semantic{type:button, text:确定创建, scope:create_meeting, container:{type:dialog}}` + `verify{dialog, text:创建成功}` |
| 20 取消会议 | `semantic{type:button, text:取消会议, scope:meeting_detail, container:{type:footer, selector:".buttom_btn"}}` + verify 弹窗 |
| 21 二次确认确定 | `semantic{type:button, text:确定, scope:meeting_detail, container:{type:dialog, title:取消会议}}` + verify 取消成功 |
| 新增 22 finally 清理 | `action:api_call, run_on:finally`,查询 `{__CTX:meetingName__}` 断言无残留 |
**动态数据处理**:步骤 10 会议名称、步骤 13 议题、步骤 15 参会人、finally 清理全部引用同一 `{__CTX:...__}` 上下文,保证创建/断言/清理一致;动态数据不写映射表,由语义目标 text/contains 携带。
---
## 十一、参考资料
- `HANDOFF_UI自动化.md` — 会话 9-20 全部补丁记录(本 PRD 的问题来源)
- `backend/app/executors/playwright_executor.py` — 执行引擎(execute_step L1031、_do_click L1308、_do_assert L1898)
- `backend/app/services/element_mapping_service.py` — 元素映射服务(_exact_match L224 待废弃)
- `backend/app/services/keyword_matcher.py` — 语义→元素管线(未接入执行器)
- `backend/app/services/page_url_service.py` + `backend/app/data/page_url_mapping.json` — 页面识别
- `backend/app/executors/template_resolver.py` — 变量模板解析器(复用)
- `backend/app/executors/api_test_executor.py` — 接口测试执行器(api_call 复用)
- `backend/app/schemas/test_case.py` — StepDefinition(L23-57 待扩展)
- `_PRD_元素映射表定位方案_前端key-value键值_提升定位稳定性.md` — 映射表 v1 PRD
- `_难点分析_智能定位功能策略可行性评估报告.md` — 方案可行性评估
---
*本文档待用户确认后进入执行计划阶段(/prd-plan)。*
# 执行计划:复杂用例通用执行机制 - 语义化用例执行
> **文档版本**: 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` |
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论