# PRD 需求文档 — UI自动化用例录入优化

> **文档类型**: PRD 需求文档
> **创建日期**: 2026-07-29
> **作者**: czj
> **优先级**: P0
> **状态**: 待评审

---

## 一、需求背景

### 1.1 当前痛点

当前平台的用例创建流程存在以下问题：

| 问题 | 说明 | 影响 |
|------|------|------|
| 步骤录入门槛高 | 需要用户手动选择动作类型（click/fill/navigate等）+ 手写 CSS 选择器 | 测试工程师需具备前端知识，上手成本高 |
| 表单字段冗余 | 描述、优先级、状态、标签、JSON步骤等字段对 UI 自动化用例非必需 | 弹窗信息过载，操作效率低 |
| 定位元素获取繁琐 | 每次需打开浏览器 F12 → 找元素 → 复制选择器 → 粘贴到步骤 | 操作链条长，易出错，页面改版后需重复操作 |
| 缺乏智能辅助 | 无 AI 能力辅助生成定位元素 | 定位工作全靠手动，效率低 |

### 1.2 目标用户

| 用户角色 | 使用场景 | 核心需求 |
|---------|---------|---------|
| 测试工程师 | 编写UI自动化测试用例 | 无需懂前端知识，用自然语言描述步骤即可创建用例 |
| 业务人员 | 参与功能验证 | 提供操作步骤意图，自动生成可执行的测试用例 |

### 1.3 需求目标

1. **意图步骤录入**：用户通过自然语言描述步骤意图（如"输入用户名""点击登录按钮"），系统自动转换成可执行的测试步骤
2. **弹窗精简**：新建UI自动化用例弹窗只保留核心字段（用例名称、所属模块、测试步骤），默认用例类型为UI自动化
3. **定位智能化**：结合 Claude AI + Playwright 自动分析目标网页 DOM，根据步骤意图自动获取元素定位信息
4. **编辑回显**：编辑用例时回显定位类型、定位值，缺失定位时自动补齐

---

## 二、功能需求

### 2.1 新建用例弹窗优化

#### 2.1.1 当前弹窗表单字段

| 字段 | 控件 | 说明 |
|------|------|------|
| 用例名称 | el-input | 必填 |
| 所属模块 | el-tree-select | 必填 |
| 用例类型 | el-select | 默认 ui |
| 优先级 | el-select | ❌ 去除 |
| 状态 | el-radio-group | ❌ 去除 |
| 描述 | el-input textarea | ❌ 去除 |
| 标签 | el-select multiple | ❌ 去除 |
| 测试步骤 | CaseStepEditor | 保留但重构 |

#### 2.1.2 优化后弹窗布局（UI自动化用例）

```
┌─────────────────────────────────────────────────────┐
│  新建用例                                            │
├─────────────────────────────────────────────────────┤
│  用例名称: [________________________________]  *必填  │
│  所属模块: [___________________________ ▼]  *必填     │
│  ─────────────────────────────────────────────────  │
│  ▼ 测试步骤                                          │
│  ┌─────────────────────────────────────────────┐    │
│  │ ① 步骤描述: [输入用户名_______________]  [等待:_] │    │
│  │    预期结果: [___________________________]      │    │
│  │    实际结果: [___________________________]      │    │
│  │    [获取定位 ▼] 定位类型: [css ▼] 定位值: [#username]│    │
│  ├─────────────────────────────────────────────┤    │
│  │ ② 步骤描述: [点击登录按钮_____________]  [等待:_] │    │
│  │    预期结果: [页面跳转首页_______________]      │    │
│  │    实际结果: [___________________________]      │    │
│  │    [获取定位 ▼] 定位类型: [css ▼] 定位值: [.btn-login]│    │
│  ├─────────────────────────────────────────────┤    │
│  │  [+] 添加步骤                                   │    │
│  └─────────────────────────────────────────────┘    │
│                                                     │
│        [取消]                    [保存]              │
└─────────────────────────────────────────────────────┘
```

#### 2.1.3 字段变更清单

| 变更 | 操作 | 说明 |
|------|------|------|
| 用例名称 | ✅ 保留 | 必填，位置提到最上方 |
| 所属模块 | ✅ 保留 | 必填，紧随用例名称 |
| 用例类型 | 🔒 隐藏 | **默认值固定为 `ui`**，不再显示选择器，仅 UI 自动化用例使用此弹窗 |
| 优先级 | ❌ 去除 | 由后端默认值 `medium` 处理 |
| 状态 | ❌ 去除 | 由后端默认值 `active` 处理 |
| 描述 | ❌ 去除 | UI 自动化用例不需要 |
| 标签 | ❌ 去除 | UI 自动化用例不需要 |
| 测试步骤 | 🔄 重构 | 改为意图步骤编辑器（见 2.2） |

### 2.2 意图步骤编辑器（IntentStepEditor）

#### 2.2.1 步骤字段结构

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `order` | int | ✅ | 步骤顺序，自动编号 |
| `description` | string | ✅ | 步骤意图描述，如"输入用户名""点击登录按钮" |
| `expected` | string | ❌ | 预期结果，如"页面跳转到首页" |
| `actual` | string | ❌ | 实际结果（预留，用于执行后记录） |
| `wait_after` | number | ❌ | 步骤执行后等待时间（毫秒），默认 0 |
| `locator_type` | string | ❌ | 定位类型：`css` / `xpath` / `text` / `id` / `name` |
| `locator_value` | string | ❌ | 定位值，如 `#username`、`//button[@type="submit"]` |

#### 2.2.2 步骤编辑器功能

| 功能 | 说明 |
|------|------|
| 添加步骤 | 点击【+ 添加步骤】按钮，新增一行空白步骤 |
| 删除步骤 | 每行末尾删除按钮 |
| 上移/下移 | 调整步骤顺序，自动重排 order |
| 步骤描述输入 | 文本输入框，填写自然语言意图 |
| 预期结果输入 | 文本输入框，填写该步骤的预期结果 |
| 实际结果输入 | 文本输入框，预留字段 |
| 等待时间输入 | 数字输入框，接在步骤描述后面 |
| 获取定位按钮 | 手动触发 AI 获取定位元素 |
| 定位类型选择 | 下拉选择：css / xpath / text / id / name |
| 定位值输入 | 文本输入框，显示定位值 |

#### 2.2.3 步骤与执行器的映射关系

意图步骤保存时，需要转换为执行器可识别的步骤格式。转换规则：

| 步骤描述关键词 | 动作类型 | 说明 |
|--------------|---------|------|
| 包含"打开""导航""访问""进入" | `navigate` | 导航到 URL |
| 包含"点击""按下""选择" | `click` | 点击元素 |
| 包含"输入""填写""填入""键入" | `fill` | 填充输入框 |
| 包含"选择""下拉" | `select` | 下拉选择 |
| 包含"悬停""鼠标移入" | `hover` | 悬停元素 |
| 包含"等待""延时" | `wait` | 等待元素或延时 |
| 包含"滚动" | `scroll` | 滚动页面 |
| 包含"按""按键" | `press` | 按键操作 |
| 包含"勾选""选中" | `check` | 勾选复选框 |
| 包含"取消勾选""取消选中" | `uncheck` | 取消勾选 |
| 包含"断言""验证""检查""确认" | `assert` | 断言验证 |
| 包含"截图" | `screenshot` | 截图 |

**保存时的数据转换**：

意图步骤 → 保存到数据库的 StepDefinition 格式：
```json
{
  "order": 1,
  "name": "输入用户名",
  "action": "fill",
  "params": {
    "selector": "#username",
    "value": "admin"
  },
  "expected": "输入框显示 admin",
  "locator_type": "css",
  "locator_value": "#username",
  "wait_after": 500
}
```

### 2.3 AI 获取定位元素

#### 2.3.1 核心思路

**服务器端**（192.168.5.60）已安装 Claude Code CLI，后端通过**任务文件机制**与宿主机上的 Claude Code 协作：

1. 后端接收前端"获取定位"请求，将任务写入共享 Volume 的任务文件
2. 宿主机上的定位脚本 `ai_locate_worker.py` 被调用：
   - 宿主机 Claude Code 通过 Playwright 打开目标网页
   - 提取 DOM 元素 → Claude Code 分析匹配 → 输出结构化定位信息
3. 结果写入共享 Volume 的结果文件
4. 前端轮询获取结果，自动填充定位信息

**关键设计**：Claude Code 在宿主机上运行，通过共享 Volume（`data/ai_locate_tasks/`）与容器内的后端交换数据。Playwright 利用宿主机上安装的 Chromium 打开目标网页。

#### 2.3.2 功能流程

```
用户填写步骤描述（如"输入用户名"）
    │
    ├── 用户点击"获取定位"按钮
    │
    ├── 前端调用后端 POST /api/ai/locate
    │   └── 请求体：{ step_description, page_url, expected }
    │
    ├── 后端（容器内）：
    │   ├── 创建任务文件 data/ai_locate_tasks/{task_id}.json
    │   ├── 通过 SSH 调用宿主机脚本：
    │   │   ssh ubains@host "cd /data/third_party/plat-auto-test && \
    │   │       python scripts/ai_locate_worker.py --task-id {task_id}"
    │   │   （或通过 docker exec 到宿主机执行）
    │   └── 返回 task_id，状态 pending
    │
    ├── 宿主机上 ai_locate_worker.py 运行：
    │   ├── 1. 读取任务文件（步骤描述、目标网页 URL）
    │   │
    │   ├── 2. Playwright 打开目标网页
    │   │   ├── 提取所有可交互元素
    │   │   │   ├── input（含 placeholder、name、id、type）
    │   │   │   ├── button（含文本、id、class）
    │   │   │   ├── a（含文本、href、id）
    │   │   │   ├── select（含 name、id）
    │   │   │   └── 其他可操作元素
    │   │   └── 提取页面标题、URL 等上下文信息
    │   │
    │   ├── 3. Claude Code 分析匹配
    │   │   ├── 输入：步骤描述 + 页面元素列表
    │   │   ├── Claude Code 根据语义匹配最合适的元素
    │   │   └── 输出结构化 JSON：{ locator_type, locator_value, confidence }
    │   │
    │   └── 4. 写入结果文件 data/ai_locate_tasks/{task_id}_result.json
    │
    ├── 前端轮询 GET /api/ai/locate/{task_id}/result
    │
    └── 定位信息自动填入步骤的定位类型和定位值
         │
         └── 用户可手动修改定位信息
```

#### 2.3.3 后端 AI 定位接口

**创建定位任务**: `POST /api/ai/locate`

**请求体**:
```json
{
  "step_description": "输入用户名",
  "page_url": "https://192.168.5.44/login",
  "expected": "输入框显示 admin"
}
```

**响应体**:
```json
{
  "task_id": "locate_abc123",
  "status": "pending",
  "message": "定位任务已创建"
}
```

**查询定位结果**: `GET /api/ai/locate/{task_id}/result`

**响应体**:
```json
{
  "task_id": "locate_abc123",
  "status": "completed",
  "success": true,
  "candidates": [
    {
      "locator_type": "css",
      "locator_value": "#username",
      "confidence": 0.95,
      "element_info": {
        "tag": "input",
        "attributes": {
          "id": "username",
          "name": "username",
          "placeholder": "请输入用户名",
          "type": "text"
        }
      }
    },
    {
      "locator_type": "xpath",
      "locator_value": "//input[@id='username']",
      "confidence": 0.90,
      "element_info": {}
    }
  ]
}
```

#### 2.3.4 AI 定位脚本（ai_locate_worker.py）

核心脚本，在宿主机上通过 Claude Code 调用 Playwright 打开目标网页，提取 DOM 元素信息，Claude Code 分析匹配后输出定位结果。

```python
# backend/scripts/ai_locate_worker.py
"""
AI 定位工作脚本

在宿主机上运行，利用 Claude Code 的分析能力 + Playwright 的网页访问能力，
根据步骤描述自动获取目标网页上的元素定位信息。

用法：
    # 配合后端（任务文件方式）
    python scripts/ai_locate_worker.py --task-id TASK_ID
    
    # 独立使用
    python scripts/ai_locate_worker.py --step-desc "输入用户名" --page-url "https://..."


工作流程：
    1. 读取任务文件（或命令行参数）
    2. 使用 Playwright 打开目标网页，提取所有可交互元素
    3. 将元素信息整理后，通过 Claude Code 分析匹配步骤描述
    4. 输出结构化定位结果（JSON格式）
"""

import argparse
import json
import os
import subprocess
import sys
import tempfile
from pathlib import Path
from playwright.sync_api import sync_playwright


def extract_interactive_elements(page) -> list:
    """提取页面中所有可交互元素的信息"""
    elements = page.evaluate("""
        () => {
            const interactive = [];
            const tags = ['input', 'button', 'a', 'select', 'textarea', 'label'];

            document.querySelectorAll(tags.join(',')).forEach(el => {
                const rect = el.getBoundingClientRect();
                if (rect.width === 0 || rect.height === 0) return;

                const info = {
                    tag: el.tagName.toLowerCase(),
                    type: el.type || '',
                    id: el.id || '',
                    name: el.name || '',
                    class: el.className || '',
                    text: (el.textContent || '').trim().slice(0, 100),
                    placeholder: el.placeholder || '',
                    'aria-label': el.getAttribute('aria-label') || '',
                    'data-testid': el.getAttribute('data-testid') || '',
                    href: el.href || '',
                    value: el.value || '',
                    visible: el.offsetParent !== null,
                };
                interactive.push(info);
            });

            return interactive;
        }
    """)
    return elements


def call_claude_code(step_description: str, elements: list, page_title: str) -> dict:
    """
    调用 Claude Code CLI 分析 DOM 元素，匹配步骤描述
    
    将元素信息格式化为 prompt，调用 `claude -p` 非交互模式
    获取 Claude 的结构化分析结果
    """
    # 构建元素摘要（限制数量避免 token 过长）
    element_summary = json.dumps(elements[:50], ensure_ascii=False, indent=2)
    
    prompt = f"""你是一个UI自动化测试的定位助手。请根据步骤描述，从网页元素列表中找出最匹配的元素。

步骤描述：{step_description}

页面标题：{page_title}

可交互元素列表（JSON格式）：
{element_summary}

请分析以上元素，找出与步骤描述最匹配的元素，并输出以下JSON格式（不要输出其他内容）：
{{
  "locator_type": "css 或 xpath 或 text 或 id",
  "locator_value": "定位值",
  "confidence": 0.0-1.0的小数表示置信度,
  "reason": "简要说明为什么匹配这个元素"
}}

如果找不到匹配的元素，输出：
{{"locator_type": "", "locator_value": "", "confidence": 0, "reason": "未找到匹配元素"}}
"""

    # 调用 Claude Code CLI（非交互模式）
    # 使用 -p 参数传入 prompt，--print 输出结果
    try:
        result = subprocess.run(
            ['claude', '-p', prompt, '--print'],
            capture_output=True,
            text=True,
            timeout=30,
            env={**os.environ, 'CLAUDE_CODE_HEADLESS': '1'}
        )
        
        if result.returncode == 0:
            # 解析 Claude 输出的 JSON
            output = result.stdout.strip()
            # 提取 JSON 部分（Claude 可能输出额外的文本）
            json_start = output.find('{')
            json_end = output.rfind('}') + 1
            if json_start >= 0 and json_end > json_start:
                json_str = output[json_start:json_end]
                return json.loads(json_str)
        
        return {"locator_type": "", "locator_value": "", "confidence": 0, "reason": f"Claude调用失败: {result.stderr[:200]}"}
    
    except subprocess.TimeoutExpired:
        return {"locator_type": "", "locator_value": "", "confidence": 0, "reason": "Claude调用超时"}
    except Exception as e:
        return {"locator_type": "", "locator_value": "", "confidence": 0, "reason": f"异常: {str(e)}"}


def build_locator_candidates(step_description: str, elements: list, page_title: str) -> list:
    """
    通过 Claude Code 分析匹配，生成定位候选列表
    
    先用关键词算法做初步筛选，再交给 Claude Code 做精确匹配
    """
    # 1. 关键词算法初步筛选（快速过滤）
    candidates = []
    desc_lower = step_description.lower()
    
    for el in elements:
        score = 0.0
        reasons = []
        
        # id 精确匹配
        if el['id'] and (el['id'].lower() in desc_lower or desc_lower in el['id'].lower()):
            score += 0.4
            reasons.append(f"id匹配: {el['id']}")
        
        # placeholder 匹配
        if el['placeholder']:
            for kw in desc_lower.split():
                if kw in el['placeholder'].lower():
                    score += 0.3
                    reasons.append(f"placeholder匹配: {el['placeholder']}")
                    break
        
        # aria-label 匹配
        if el['aria-label']:
            for kw in desc_lower.split():
                if kw in el['aria-label'].lower():
                    score += 0.3
                    break
        
        # 文本内容匹配
        if el['text']:
            for kw in desc_lower.split():
                if kw in el['text'].lower():
                    score += 0.25
                    reasons.append(f"文本匹配: {el['text']}")
                    break
        
        # name 属性匹配
        if el['name'] and (el['name'].lower() in desc_lower or desc_lower in el['name'].lower()):
            score += 0.2
        
        # 语义推断
        if any(kw in desc_lower for kw in ['输入', '填写', '键入']):
            if el['tag'] in ['input', 'textarea']:
                score += 0.15
        if any(kw in desc_lower for kw in ['点击', '按下', '选择']):
            if el['tag'] in ['button', 'a']:
                score += 0.15
        
        if score > 0.15:  # 阈值过滤
            locator = _build_locator(step_description, el)
            candidates.append({
                **locator,
                'confidence': round(score, 2),
                'element_info': el,
                'reasons': reasons,
            })
    
    # 2. 如果没有找到高置信度的候选，调用 Claude Code 做精确分析
    if not candidates or candidates[0]['confidence'] < 0.6:
        claude_result = call_claude_code(step_description, elements, page_title)
        if claude_result.get('locator_type') and claude_result.get('confidence', 0) > 0.5:
            # 如果 Claude Code 找到的匹配不在已有候选中，添加到最前面
            claude_match = {
                'locator_type': claude_result['locator_type'],
                'locator_value': claude_result['locator_value'],
                'confidence': claude_result.get('confidence', 0.8),
                'element_info': {},
                'reasons': [claude_result.get('reason', 'Claude Code 分析匹配')],
            }
            candidates.insert(0, claude_match)
    
    # 按置信度降序排列
    candidates.sort(key=lambda x: x['confidence'], reverse=True)
    return candidates[:5]


def _build_locator(description: str, el: dict) -> dict:
    """根据元素信息生成最佳定位方式"""
    if el['id']:
        return {'locator_type': 'css', 'locator_value': f"#{el['id']}"}
    if el['data-testid']:
        return {'locator_type': 'css', 'locator_value': f"[data-testid='{el['data-testid']}']"}
    if el['aria-label']:
        return {'locator_type': 'css', 'locator_value': f"[aria-label='{el['aria-label']}']"}
    if el['name']:
        return {'locator_type': 'css', 'locator_value': f"{el['tag']}[name='{el['name']}']"}
    if el['placeholder']:
        return {'locator_type': 'css', 'locator_value': f"[placeholder='{el['placeholder']}']"}
    if el['text'] and el['tag'] in ['button', 'a']:
        return {'locator_type': 'text', 'locator_value': el['text']}
    return {'locator_type': 'xpath', 'locator_value': f"//{el['tag']}[@id='{el['id']}']" if el['id'] else ''}


def run_locate(step_description: str, page_url: str, expected: str = "") -> dict:
    """执行定位——主流程"""
    print(f"[AI定位] 步骤描述: {step_description}")
    print(f"[AI定位] 目标页面: {page_url}")

    with sync_playwright() as p:
        # 使用 Chromium 打开网页（复用项目 UA 伪装配置）
        browser = p.chromium.launch(
            headless=True,
            args=[
                '--disable-blink-features=AutomationControlled',
                '--disable-automation',
                '--no-sandbox',
                '--disable-dev-shm-usage',
                '--disable-gpu',
            ]
        )
        context = browser.new_context(
            user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/126.0.0.0',
            locale='zh-CN',
            timezone_id='Asia/Shanghai',
        )
        page = context.new_page()
        page.add_init_script("""
            Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
        """)

        # 打开目标页面
        page.goto(page_url, wait_until='networkidle', timeout=30000)
        page_title = page.title()
        print(f"[AI定位] 页面加载完成: {page_title}")

        # 提取可交互元素
        elements = extract_interactive_elements(page)
        print(f"[AI定位] 提取到 {len(elements)} 个可交互元素")

        browser.close()

    # 匹配元素（通过算法 + Claude Code）
    candidates = build_locator_candidates(step_description, elements, page_title)
    print(f"[AI定位] 匹配到 {len(candidates)} 个候选元素")

    # 构建结果
    result = {
        'success': len(candidates) > 0,
        'candidates': candidates,
        'message': f"定位成功，找到 {len(candidates)} 个候选" if candidates else "未找到匹配元素",
    }

    if candidates:
        best = candidates[0]
        print(f"\n[AI定位] 最佳匹配:")
        print(f"  定位类型: {best['locator_type']}")
        print(f"  定位值: {best['locator_value']}")
        print(f"  置信度: {best['confidence']}")
        print(f"  元素标签: {best['element_info'].get('tag', '')}")
        print(f"  元素属性: id={best['element_info'].get('id','')}, name={best['element_info'].get('name','')}")

    return result


def main():
    parser = argparse.ArgumentParser(description='AI 定位工作脚本（服务器端）')
    parser.add_argument('--task-id', help='任务ID（从后端获取）')
    parser.add_argument('--step-desc', help='步骤描述（直接传入）')
    parser.add_argument('--page-url', help='目标网页 URL')
    parser.add_argument('--expected', default='', help='预期结果')
    args = parser.parse_args()

    if args.task_id:
        # 任务文件方式：配合后端使用
        task_dir = Path(__file__).parent.parent / 'data' / 'ai_locate_tasks'
        task_file = task_dir / f"{args.task_id}.json"
        result_file = task_dir / f"{args.task_id}_result.json"

        if not task_file.exists():
            print(f"❌ 任务文件不存在: {task_file}")
            return

        with open(task_file, 'r') as f:
            task = json.load(f)

        result = run_locate(
            step_description=task['step_description'],
            page_url=task['page_url'],
            expected=task.get('expected', ''),
        )

        result['task_id'] = args.task_id
        result['status'] = 'completed'
        task_dir.mkdir(parents=True, exist_ok=True)
        with open(result_file, 'w') as f:
            json.dump(result, f, ensure_ascii=False, indent=2)

        print(f"\n✅ 定位结果已写入: {result_file}")

    else:
        # 直接运行
        result = run_locate(
            step_description=args.step_desc,
            page_url=args.page_url,
            expected=args.expected,
        )
        print(json.dumps(result, ensure_ascii=False, indent=2))


if __name__ == '__main__':
    main()
```

#### 2.3.5 服务器端集成方式

后端（容器内）通过 SSH 调用宿主机上的定位脚本：

```python
# backend/app/services/ai_locator_service.py

import subprocess
import json
from pathlib import Path

class AILocatorService:
    """AI 定位服务——通过宿主机 Claude Code 获取定位"""
    
    TASK_DIR = Path("data/ai_locate_tasks")
    DEPLOY_DIR = "/data/third_party/plat-auto-test"
    HOST_USER = "ubains"
    HOST = "192.168.5.60"
    
    def create_task(self, step_description: str, page_url: str, expected: str = "") -> dict:
        """创建定位任务并调用宿主机 Claude Code 执行"""
        import uuid
        task_id = f"locate_{uuid.uuid4().hex[:12]}"
        
        # 1. 创建任务文件
        self.TASK_DIR.mkdir(parents=True, exist_ok=True)
        task_file = self.TASK_DIR / f"{task_id}.json"
        with open(task_file, 'w') as f:
            json.dump({
                "step_description": step_description,
                "page_url": page_url,
                "expected": expected,
            }, f, ensure_ascii=False)
        
        # 2. SSH 到宿主机执行定位脚本（异步）
        ssh_cmd = (
            f"ssh {self.HOST_USER}@{self.HOST} "
            f"\"cd {self.DEPLOY_DIR} && python scripts/ai_locate_worker.py --task-id {task_id}\""
        )
        subprocess.Popen(ssh_cmd, shell=True)
        
        return {"task_id": task_id, "status": "pending"}
    
    def get_result(self, task_id: str) -> dict:
        """查询定位结果"""
        result_file = self.TASK_DIR / f"{task_id}_result.json"
        if not result_file.exists():
            return {"task_id": task_id, "status": "pending", "success": False, "candidates": []}
        
        with open(result_file, 'r') as f:
            return json.load(f)
```

#### 2.3.6 优势与注意事项

**优势**：
- ✅ **零额外 API 成本**：利用服务器已有的 Claude Code CLI
- ✅ **全自动**：无需人工干预，后端自动创建任务→调用宿主机→获取结果
- ✅ **Playwright 已就绪**：容器内已有 Playwright，宿主机也可安装
- ✅ **Claude Code 分析精准**：利用 Claude 的语义理解能力，匹配准确率高
- ✅ **本地+服务器统一**：同一套脚本，本地手动运行，服务器自动运行

**注意事项**：
- 🔑 容器需配置 SSH 免密登录到宿主机（或通过 Docker socket 通信）
- ⏱️ 首次定位约 10-20 秒（Playwright 开网页 + Claude Code 分析）
- 💾 任务文件会占用少量磁盘空间，建议定期清理

### 2.4 编辑用例回显

#### 2.4.1 编辑时的数据回显

编辑用例时，`StepDefinition` 中的以下字段需回显到意图步骤编辑器：

| 意图步骤字段 | 对应 StepDefinition 字段 | 回显规则 |
|-------------|------------------------|---------|
| `description` | `name` | 直接回显 |
| `expected` | `expected` | 直接回显 |
| `wait_after` | `wait_after` | 直接回显 |
| `locator_type` | `locator_type`（新增） | 直接回显 |
| `locator_value` | `locator_value`（新增） | 直接回显 |

#### 2.4.2 自动补齐缺失定位

编辑时如果步骤的 `locator_type` / `locator_value` 为空（旧数据或未获取定位），自动触发补齐逻辑：

| 情形 | 处理方式 |
|------|---------|
| `params.selector` 有值但 `locator_type/value` 为空 | 解析 `params.selector` 自动推断类型并填充 |
| 所有定位字段都为空 | 在编辑弹窗中显示"获取定位"按钮，用户手动触发 |

#### 2.4.3 转换逻辑

意图步骤编辑器保存时，自动执行以下转换：

```
意图步骤数据
  → 自动推断 action 类型（根据关键词）
  → 同步 locator_type + locator_value 到 params.selector
  → 生成最终的 StepDefinition 格式
  → 保存到数据库
```

### 2.5 数据同步到服务器

#### 2.5.1 同步流程

```
本地创建用例（SQLite 数据库）
  → 验证用例完整可用
  → 通过 HTTP API 同步到服务器
  → 服务器写入 MySQL 数据库
  → 服务器上的测试平台即可使用该用例
```

#### 2.5.2 同步方式（二选一）

| 方式 | 实现 | 说明 |
|------|------|------|
| **API 推送** | 前端新增"同步到服务器"按钮，调用 `POST http://192.168.5.60/api/cases` | 逐个推送到服务器，适合少量用例 |
| **批量同步脚本** | 后端 `/scripts/sync_to_server.py` | 读取本地 SQLite 用例，批量写入服务器 MySQL |

---

## 三、数据结构变更

### 3.1 StepDefinition 新增字段

**前端 TypeScript** (`frontend/src/types/case.ts`):

```typescript
export interface StepDefinition {
  order: number
  name: string           // 对应步骤描述
  action: string         // 动作类型（自动推断）
  params: Record<string, any>
  expected?: string      // 预期结果
  actual?: string        // 实际结果（预留）
  selectors?: string[]
  pageKey?: string
  elementKey?: string
  force?: boolean
  waitAfter?: number     // 等待时间(ms)

  // === 新增字段 ===
  /** 定位类型: css / xpath / text / id / name */
  locator_type?: string
  /** 定位值 */
  locator_value?: string
}
```

**后端 Pydantic** (`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="")
    actual: str = Field(default="")  # 新增：实际结果
    selectors: Optional[List[str]] = Field(None)
    page_key: Optional[str] = Field(None)
    element_key: Optional[str] = Field(None)
    force: Optional[bool] = Field(None)
    wait_after: Optional[int] = Field(None)
    # 新增定位字段
    locator_type: Optional[str] = Field(None, description="定位类型: css/xpath/text/id/name")
    locator_value: Optional[str] = Field(None, description="定位值")
```

### 3.2 新增 AI 配置模型

**前端 Settings.vue** 新增配置项：

```typescript
interface AIConfig {
  provider: 'claude' | 'none'
  api_key: string
  model: string
  api_base_url: string
}
```

**后端** (`backend/app/config.py`):

```python
class Settings(BaseSettings):
    # AI 配置
    AI_PROVIDER: str = "claude"
    AI_API_KEY: str = ""
    AI_MODEL: str = "claude-sonnet-5"
    AI_API_BASE_URL: str = "https://api.anthropic.com"
```

---

## 四、非功能需求

| 需求 | 说明 |
|------|------|
| 定位准确性 | AI 匹配的定位元素点击成功率 ≥ 90% |
| 定位失败处理 | 置信度低于 60% 时返回多个候选供用户选择，或提示手动输入 |
| 编辑兼容性 | 旧数据（不含 locator_type/value）编辑时正常回显，不丢失数据 |
| 响应时间 | AI 定位请求 ≤ 10 秒（含 Playwright 开网页 + AI 分析） |
| 同步可靠性 | 同步到服务器时支持失败重试，同步失败提示具体原因 |

---

## 五、验收标准

| 测试项 | 预期结果 |
|--------|----------|
| 新建UI用例弹窗简化 | 仅显示"用例名称""所属模块""测试步骤"三个核心区域 |
| 意图步骤编辑 | 可添加/删除/移动步骤，每行支持步骤描述+预期结果+实际结果+等待时间 |
| 定位类型/值独立字段 | 步骤编辑器显示定位类型下拉+定位值输入框 |
| 编辑回显 | 编辑时 locator_type/locator_value 正确回显 |
| 自动补齐定位 | 旧数据（params.selector 有值但 locator 为空）编辑时自动推断补齐 |
| AI 获取定位 | 点击"获取定位"后，定位信息自动填入 |
| 保存转换 | 意图步骤保存后转换为执行器可识别的 StepDefinition 格式 |
| 类型默认值 | 新建UI用例时 case_type 固定为 `ui` |
| 旧数据兼容 | 编辑原有用例时，所有字段正常回显，数据不丢失 |

---

## 六、相关文档

- `frontend/src/views/Cases.vue` — 用例管理页面（弹窗修改）
- `frontend/src/components/CaseStepEditor.vue` — 当前步骤编辑器（重构为 IntentStepEditor）
- `frontend/src/types/case.ts` — 类型定义（新增定位字段）
- `backend/app/schemas/test_case.py` — Pydantic 模式（新增定位字段）
- `backend/app/routers/cases.py` — 用例路由
- `backend/app/executors/playwright_executor.py` — 执行器（AI 定位时打开网页）
- `backend/app/config.py` — 配置（AI 配置项）
- `frontend/src/views/Settings.vue` — 系统配置（AI 配置页面）

---

## 七、实施范围

### 本次实现（Phase 5.1）

| 模块 | 内容 | 预计工时 |
|------|------|---------|
| 前端弹窗优化 | 精简新建用例弹窗字段 | 1 天 |
| 意图步骤编辑器 | 重构 CaseStepEditor → IntentStepEditor | 2 天 |
| 定位字段新增 | StepDefinition 新增 locator_type/locator_value | 0.5 天 |
| 编辑回显 | 编辑时回显定位字段，自动补齐 | 1 天 |
| AI 定位接口 | 后端 `POST /api/ai/locate` + 任务文件机制 | 1 天 |
| AI 定位脚本 | `scripts/ai_locate_worker.py` Playwright DOM 分析脚本 | 1 天 |
| 前端 AI 触发 | "获取定位"按钮 + 定位结果回填 | 1 天 |
| 数据同步脚本 | 本地到服务器的同步工具 | 0.5 天 |
| **总计** | | **8 天** |

### 后续迭代

| 模块 | 内容 | 说明 |
|------|------|------|
| AI 定位脚本监听模式 | 脚本持续监听任务目录，自动处理新任务 | 减少手动操作 |
| 批量同步 | 批量同步+冲突检测 | 数据量大时 |
| 定位自动验证 | 保存前 Playwright 验证定位有效性 | 提升可靠性 |

---

*本文档由 Claude Code 生成，遵循项目 PRD 文档规范。*