# PRD — P1 遗留待办：验证步骤前端适配与定位器优化

> **文档版本**: v1.0
> **创建日期**: 2026-08-21
> **作者**: Claude Code（czj 确认）
> **状态**: 待确认
> **关联文档**: `HANDOFF_UI自动化.md`（P1 待办项）

---

## 一、背景与问题

### 1.1 背景

`HANDOFF_UI自动化.md` 遗留两个 P1 待办项，经代码调研确认当前状态：

| P1 待办 | 当前状态 | 问题本质 |
|---------|---------|---------|
| 前端适配验证步骤的 `verify_method` 参数 | 后端生成字段但前端无 UI、执行器不消费 | 字段设计割裂（`verify_method` vs `verify` 顶层字段） |
| 增加更多菜单的定位器优化（滚动、备选选择器） | `page_url_mapping.json` 仅注册 6 个页面，19 个二级菜单无条目 | `_do_fill` 无滚动兜底、二级菜单无页面注册 |

### 1.2 问题详情

#### 1.2.1 `verify_method` 字段设计割裂

代码调研结论（详见探索报告）：

| 项 | 现状 | 问题 |
|----|------|------|
| `smart_locate_service.py`（L597/L648/L694） | 智能定位生成 `params.verify_method`（`message`/`search`/`text_visible` 3 个值） | 写入的是 `params` 顶层，**未设置 `verify` 顶层字段** |
| `playwright_executor.py` `_verify_step_state`（L2748） | 仅消费 `step.get("verify")` 顶层字段，结构 `{"type": "...", "selector": "...", "text": "...", "must": bool}` | **完全不消费 `verify_method`** |
| `_build_verify_assert`（L2799） | verify 类型：`text_visible`/`element_visible`/`text_hidden`/`element_exists`/`url_contains`/`dialog`/`value`/`count` | 类型与前端 `ASSERT_TYPE_OPTIONS`（10 种 assert 类型）**不一致** |
| 前端 `CaseStepEditor.vue`（L479） | `ASSERT_TYPE_OPTIONS` 10 种：visible/hidden/text/contains/value/count/url_contains/title/element_exists/element_count_min | 缺少 `body_not_contains`（后端 `api_test_executor.py` L231 已支持但 `playwright_executor.py` `_do_assert` **不支持**） |
| 前端 `IntentStepEditor.vue` | **完全没有** verify/semantic 编辑区 | 意图模式下验证步骤无法编辑 |

**根因链**：`verify_method` 是旧方案遗留字段，只标记"验证方式"但未形成可持久化/可消费的 `verify` 步骤配置 → 前端无处展示、执行器不执行、智能定位结果"看着有验证、实际不验证"。

#### 1.2.2 二级菜单定位器缺失

| 项 | 现状 | 影响 |
|----|------|------|
| `page_url_mapping.json` | 仅 6 个页面（create_meeting/login/home/meeting_detail/meeting_list/statistics） | 功能中心 19 个二级菜单（壁纸推送/脚本命令/应用管理/屏幕管理/软件卸载/设备网络/门口屏发布/播放器发布/资产信息/资产设备/运维设备/运维巡检/工单列表/我的工单/告警工单/信息窗管理/消息通知/会务统筹/会务工单）**均无页面注册** |
| `_do_fill` | 无显式 `scroll_into_view_if_needed()` | `_do_click` 已有（L2019/L2068/L2161），fill 依赖 click 间接触发，独立输入场景缺少滚动兜底 |
| 选择器解析 `_resolve_selectors`（L677） | 已支持 `params.selectors`（数组）+ `params.selector`（单个）双模式 + `page_key/element_key` 查 SelectorMapper | 多选择器回退机制完善，但存量步骤多只用单个选择器 |

---

## 二、目标与非目标

### 2.1 目标

1. **统一验证字段模型**：智能定位同时输出 `verify` 顶层字段（执行器真实消费），`verify_method` 作为兼容标记保留
2. **前端补全验证类型**：`ASSERT_TYPE_OPTIONS` 新增 `body_not_contains`；verify 编辑区使用独立的 `VERIFY_TYPE_OPTIONS`（与后端 `_build_verify_assert` 对齐）
3. **意图步骤编辑器支持验证**：`IntentStepEditor.vue` 新增 verify 折叠编辑区（round-trip 保留语义字段）
4. **执行器补全 `body_not_contains` 断言**：`_do_assert` 支持页面响应体/文本不包含校验
5. **二级菜单页面注册**：为 19 个二级菜单补充 `page_url_mapping.json` 条目（url_patterns + fingerprint），提升 scope 识别与直达导航能力
6. **定位滚动优化**：`_do_fill` 增加 `scroll_into_view_if_needed()` 兜底

### 2.2 非目标

1. 不替换现有执行器/语义解析链（老步骤走原路径，完全向后兼容）
2. 不做智能定位 API 与用例执行路径的统一（P2，另行评估）
3. 不完善 `menu_mapping.py` 全量二级菜单 URL（P2）
4. 不做 Claude 调用性能优化（P2）

---

## 三、方案设计

### 3.1 验证字段统一（P1-1 核心）

```
旧方案：params.verify_method = "message"           （前端无 UI、执行器不消费）
新方案：verify = {"type": "dialog"}                 （执行器 _verify_step_state 消费）
        verify = {"type": "text_visible", "text": target_text}
```

`smart_locate_service.py` 三处 `verify_method` 赋值处同步设置 `verify` 顶层字段：

| verify_method | verify 顶层字段 | 说明 |
|---|---|---|
| `message`（L597） | `{"type": "dialog"}` | .el-message--success 提示出现 |
| `search`（L648） | `{"type": "element_visible"}` | 搜索框可见 |
| `text_visible`（L694） | `{"type": "text_visible", "text": target_text}` | 指定文本可见 |

**理由**：`verify` 顶层字段是 `step.get("verify")` 读取字段（`_verify_step_state` L2768），`params.verify_method` 无任何消费方。前后端数据链路打通后，智能定位产出的步骤保存到数据库，前端编辑回显时 `stepParser.ts` round-trip 保留 `step.verify`（已支持，L210-211/L262-263）。

### 3.2 执行器新增 `body_not_contains` 断言（P1-1）

`_do_assert` 新增分支（与 `api_test_executor.py` L231 语义对齐）：

```
assert_type == "body_not_contains":
  1. 有 selector → 取该元素 innerText（多选择器逐个尝试）
  2. 无 selector → 取 page.content() 全文文本
  3. expected 为空 → 视为失败（防假通过，与 api 版一致）
  4. expected 不在文本中 → 通过
```

### 3.3 前端 CaseStepEditor 补全（P1-1）

1. `ASSERT_TYPE_OPTIONS` 新增：`body_not_contains`（响应体/页面不包含）
2. 新增独立 `VERIFY_TYPE_OPTIONS`（与后端 `_build_verify_assert` 对齐）：

| value | label | 需要 selector | 需要 text |
|-------|-------|:---:|:---:|
| `text_visible` | 文本可见 | ✓ | ✓ |
| `element_visible` | 元素可见 | ✓ | ✗ |
| `text_hidden` | 文本隐藏 | ✓ | ✓ |
| `element_exists` | 元素存在 | ✓ | ✗ |
| `url_contains` | URL 包含 | ✗ | ✓ |
| `dialog` | 弹窗提示 | ✗ | ✓ |
| `value` | 输入值相等 | ✓ | ✓ |
| `count` | 元素数量 | ✓ | ✓ |

3. verify 编辑区改用 `VERIFY_TYPE_OPTIONS`（不再复用 `ASSERT_TYPE_OPTIONS`），`verifyNeedsSelector`/`verifyNeedsText` 同步调整

### 3.4 前端 IntentStepEditor 新增 verify 编辑区（P1-1）

在预期结果区域下方新增折叠高级区（样式参考 CaseStepEditor `.step-advanced`）：
- verify 类型选择（VERIFY_TYPE_OPTIONS）
- selector / text / timeout / must 字段
- **不改动** `stepParser.ts` round-trip（已验证保留 `step.verify`），仅新增 UI

### 3.5 二级菜单页面注册（P1-2）

`page_url_mapping.json` 新增 19 个二级菜单条目：
- url 字段已知的用直达 URL（参考 `menu_mapping.py` `MENU_BLOCK_ROUTES` 的 `/#/meetingV3?meetingV3=%2F...%2F{Page}` 模式）
- url_patterns 用页面标识（如 `WallpaperPush`、`ScriptCommand` 等，需按实际页面实测）
- fingerprint 用页面标题或唯一文本（待 `/sut-explore` 真机探测确认后填充，本阶段先注册结构 + 可识别模式）
- ⚠️ **注意**：url_patterns/fingerprint 仅用于 scope 识别，**不影响存量用例**；新增条目后 `page_url_service` 单例不热加载需重启 uvicorn

### 3.6 定位滚动优化（P1-2）

`_do_fill` 在 fill 前增加 `scroll_into_view_if_needed()`（与 `_do_click` L2068 一致），失败不阻塞（try/except 包裹）。

---

## 四、验收标准

| # | 验收项 | 验证方式 |
|---|--------|---------|
| 1 | 智能定位验证类步骤返回 `verify` 顶层字段 | 手动触发 `POST /api/element/smart-locate`，检查返回步骤含 verify |
| 2 | `_do_assert` 支持 `body_not_contains` | 后端单测 + 含该断言的 api_call 步骤执行 |
| 3 | 前端 assert 动作下拉含 `body_not_contains` | `npm run build` + 页面检查 |
| 4 | verify 编辑区用独立类型（text_visible/dialog 等） | 页面检查 CaseStepEditor 高级区 |
| 5 | IntentStepEditor 可编辑 verify | 页面检查 + 保存回显不丢 |
| 6 | 存量用例零回归 | 后端 `pytest` 全量 + 前端 `npm run build` |
| 7 | `_do_fill` 滚动兜底 | 代码 review + 含 fill 的用例执行**

## 五、风险评估

| 风险 | 等级 | 缓解 |
|------|------|------|
| `page_url_mapping.json` 二级菜单 URL 需真机探测确认 | 中 | 本阶段先注册结构 + 可识别模式，具体 URL 走 `/sut-explore` 实测后补 |
| verify 类型与 assert 类型拆分后存量 verify 数据兼容 | 低 | 前端仅 UI 展示层拆分，存取仍为 `verify` 顶层字段 dict，round-trip 不丢 |
| `body_not_contains` 页面全文误判（弹层文本干扰） | 低 | 无 selector 时取 `page.content()`，与 api 版语义一致；优先推荐带 selector 使用 |