# 执行计划 — UI自动化用例录入优化

> **文档类型**: 执行计划文档
> **创建日期**: 2026-07-29
> **关联 PRD**: `_PRD_UI自动化用例录入优化.md` v1.0
> **负责人**: czj
> **优先级**: P0
> **状态**: 待评审

---

## 一、执行概述

### 1.1 需求概要

优化 UI 自动化用例的录入方式，核心变更：

1. **新建弹窗精简**：去除描述、优先级、状态、标签字段，仅保留用例名称、所属模块、测试步骤，`case_type` 默认固定为 `ui`
2. **意图步骤录入**：用自然语言描述步骤（如"输入用户名"），取代手动选择动作类型+写 CSS 选择器
3. **定位字段独立**：StepDefinition 新增 `locator_type` / `locator_value` 字段，与 `params.selector` 并存
4. **AI 定位辅助**：通过 Claude AI + Playwright 自动分析网页元素，根据步骤描述获取定位
5. **编辑回显增强**：编辑时回显定位信息，旧数据自动补齐

### 1.2 技术变更范围

| 层级 | 影响文件 | 变更类型 |
|------|---------|---------|
| 前端类型 | `frontend/src/types/case.ts` | 修改：StepDefinition 新增字段 |
| 前端组件 | `frontend/src/components/CaseStepEditor.vue` | 重构：改为 IntentStepEditor |
| 前端页面 | `frontend/src/views/Cases.vue` | 修改：弹窗字段精简，集成新步骤编辑器 |
| 前端页面 | `frontend/src/views/Settings.vue` | 新增：AI 配置管理 |
| 前端 API | `frontend/src/api/aiLocate.ts` | 新增：AI 定位接口客户端 |
| 后端 Schema | `backend/app/schemas/test_case.py` | 修改：StepDefinition 新增字段 |
| 后端路由 | `backend/app/routers/ai_locator.py` | 新增：AI 定位路由 |
| 后端服务 | `backend/app/services/ai_locator_service.py` | 新增：AI 定位核心逻辑 |
| 后端配置 | `backend/app/config.py` | 修改：新增 AI 配置项 |
| 后端入口 | `backend/app/main.py` | 修改：注册 AI 路由 |
| 后端脚本 | `backend/scripts/sync_to_server.py` | 新增：数据同步脚本 |

### 1.3 预计工期

| 阶段 | 内容 | 预计工时 |
|------|------|---------|
| 阶段一 | 数据类型定义 + 后端 Schema 更新 | 1 天 |
| 阶段二 | 意图步骤编辑器组件开发 | 2 天 |
| 阶段三 | 新建弹窗重构 + 旧数据兼容 | 1 天 |
| 阶段四 | AI 定位后端服务 | 2 天 |
| 阶段五 | 前端 AI 定位集成 + Settings 配置页 | 1.5 天 |
| 阶段六 | 数据同步脚本 + 全流程验证 | 0.5 天 |
| **总计** | | **8 天** |

---

## 二、任务分解

### 2.1 阶段一：数据类型定义与后端 Schema 更新（1 天）

#### 任务清单

| 任务ID | 任务名称 | 文件 | 说明 | 优先级 |
|--------|---------|------|------|--------|
| UI-OPT-001 | StepDefinition 前端新增定位字段 | `frontend/src/types/case.ts` | 新增 `locator_type` `locator_value` `actual` 字段 | P0 |
| UI-OPT-002 | StepDefinition 后端新增定位字段 | `backend/app/schemas/test_case.py` | Pydantic 同步新增 `locator_type` `locator_value` `actual` | P0 |
| UI-OPT-003 | 后端新增 AI 配置项 | `backend/app/config.py` | 新增 `AI_PROVIDER` `AI_API_KEY` `AI_MODEL` `AI_API_BASE_URL` | P0 |

#### 详细实施

**UI-OPT-001：前端类型定义修改**

```typescript
// frontend/src/types/case.ts 中 StepDefinition 新增字段
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
  locator_type?: string                    // 新增：定位类型 css/xpath/text/id/name
  locator_value?: string                   // 新增：定位值
}
```

**UI-OPT-002：后端 Pydantic Schema 同步修改**

```python
# backend/app/schemas/test_case.py 中 StepDefinition 新增字段
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="定位值")
```

---

### 2.2 阶段二：意图步骤编辑器组件开发（2 天）

#### 任务清单

| 任务ID | 任务名称 | 文件 | 说明 | 优先级 |
|--------|---------|------|------|--------|
| UI-OPT-004 | 创建 IntentStepEditor 组件 | `frontend/src/components/IntentStepEditor.vue` | 全新的意图步骤编辑器 | P0 |
| UI-OPT-005 | 步骤描述→动作推断工具函数 | `frontend/src/utils/stepParser.ts` | 根据步骤描述关键词推断 action 类型 | P0 |
| UI-OPT-006 | 定位信息获取按钮组件 | 集成在 IntentStepEditor 内 | 每行步骤的"获取定位"按钮 | P0 |

#### IntentStepEditor 组件设计

**组件接口**：
```typescript
// props
interface IntentStepEditorProps {
  modelValue: IntentStep[]       // 意图步骤数组（v-model）
  pageUrl?: string               // 目标网页 URL（可选，用于 AI 定位）
}

// emits
interface IntentStepEditorEmits {
  (e: 'update:modelValue', value: IntentStep[]): void
  (e: 'locate', index: number): void   // 触发获取定位
}

// 意图步骤数据结构
interface IntentStep {
  order: number
  description: string            // 步骤描述（对应 name）
  expected: string               // 预期结果
  actual: string                 // 实际结果（预留）
  waitAfter: number              // 等待时间(ms)
  locatorType: string            // 定位类型
  locatorValue: string           // 定位值
  // 保存时自动生成的字段
  _action?: string               // 推断的动作类型
  _params?: Record<string, any>  // 推断的动作参数
}
```

**组件模板结构**：
```html
<template>
  <div class="intent-step-editor">
    <!-- 步骤列表 -->
    <div v-for="(step, index) in steps" :key="index" class="step-item">
      <!-- 步骤头部：序号 + 操作按钮 -->
      <div class="step-header">
        <span class="step-order">{{ step.order }}</span>
        <el-button size="small" link :disabled="index===0" @click="moveStep(index, -1)">↑</el-button>
        <el-button size="small" link :disabled="index===steps.length-1" @click="moveStep(index, 1)">↓</el-button>
        <el-button size="small" type="danger" link @click="removeStep(index)">删除</el-button>
      </div>

      <!-- 步骤描述 + 等待时间（同一行） -->
      <div class="step-row">
        <el-input v-model="step.description" placeholder="步骤描述，如"输入用户名"" style="flex: 1" />
        <span class="wait-label">等待</span>
        <el-input-number v-model="step.waitAfter" :min="0" :step="100" size="small" style="width: 120px" />
        <span class="wait-label">ms</span>
      </div>

      <!-- 预期结果 -->
      <div class="step-row">
        <span class="field-label">预期结果</span>
        <el-input v-model="step.expected" placeholder="该步骤的预期结果（可选）" />
      </div>

      <!-- 实际结果（预留） -->
      <div class="step-row" v-if="showActual">
        <span class="field-label">实际结果</span>
        <el-input v-model="step.actual" placeholder="实际结果（预留）" />
      </div>

      <!-- 定位信息区域 -->
      <div class="step-row locator-row">
        <el-button size="small" @click="emit('locate', index)" :loading="locatingIndex === index">
          <el-icon><Search /></el-icon> 获取定位
        </el-button>
        <el-select v-model="step.locatorType" size="small" style="width: 120px" placeholder="定位类型">
          <el-option label="CSS" value="css" />
          <el-option label="XPath" value="xpath" />
          <el-option label="文本" value="text" />
          <el-option label="ID" value="id" />
          <el-option label="Name" value="name" />
        </el-select>
        <el-input v-model="step.locatorValue" size="small" placeholder="定位值，如 #username" style="flex: 1" />
      </div>
    </div>

    <!-- 添加步骤按钮 -->
    <el-button type="primary" plain size="small" class="add-step-btn" @click="addStep">
      <el-icon><Plus /></el-icon> 添加步骤
    </el-button>
  </div>
</template>
```

**UI-OPT-005：步骤描述→动作推断工具函数**

```typescript
// frontend/src/utils/stepParser.ts
/**
 * 根据步骤描述自动推断动作类型
 */
export function inferAction(description: string): string {
  const desc = description.toLowerCase()
  if (/打开|导航|访问|进入|跳转|前往/.test(desc)) return 'navigate'
  if (/点击|按下|选择|单击|双击/.test(desc)) return 'click'
  if (/输入|填写|填入|键入|填写/.test(desc)) return 'fill'
  if (/选择|下拉|选取/.test(desc)) return 'select'
  if (/悬停|鼠标移入|hover/.test(desc)) return 'hover'
  if (/等待|延时|等待元素/.test(desc)) return 'wait'
  if (/滚动|滑动|滚到/.test(desc)) return 'scroll'
  if (/按键盘|按|按键|按下.*键/.test(desc)) return 'press'
  if (/勾选|选中|勾上/.test(desc)) return 'check'
  if (/取消勾选|取消选中|取消勾/.test(desc)) return 'uncheck'
  if (/断言|验证|检查|确认|判断|检测/.test(desc)) return 'assert'
  if (/截图|截屏|屏幕截图/.test(desc)) return 'screenshot'
  return 'click'  // 默认动作
}

/**
 * 推断动作参数
 */
export function inferParams(description: string, locatorValue: string): Record<string, any> {
  const action = inferAction(description)
  const params: Record<string, any> = {}

  // 需要选择器的动作
  if (['click', 'fill', 'select', 'hover', 'wait', 'check', 'uncheck', 'press'].includes(action)) {
    if (locatorValue) params.selector = locatorValue
  }

  // fill 需要值
  if (action === 'fill') {
    // 从描述提取值：如"输入 admin" → value="admin"
    const match = description.match(/输入["「『""]?(.+?)["」』""]?$/)
    if (match) params.value = match[1]
  }

  // navigate 需要 URL
  if (action === 'navigate') {
    const match = description.match(/https?:\/\/[^\s]+/)
    if (match) params.url = match[0]
  }

  return params
}

/**
 * 合并意图步骤 → StepDefinition
 */
export function toStepDefinition(step: IntentStep): StepDefinition {
  const action = inferAction(step.description)
  const params = inferParams(step.description, step.locatorValue)

  // 如果有 locator_value，自动同步到 params.selector
  if (step.locatorValue && !params.selector) {
    params.selector = step.locatorValue
  }

  return {
    order: step.order,
    name: step.description,
    action,
    params,
    expected: step.expected || '',
    actual: step.actual || '',
    waitAfter: step.waitAfter || 0,
    locator_type: step.locatorType || '',
    locator_value: step.locatorValue || '',
  }
}
```

---

### 2.3 阶段三：新建弹窗重构 + 旧数据兼容（1 天）

#### 任务清单

| 任务ID | 任务名称 | 文件 | 说明 | 优先级 |
|--------|---------|------|------|--------|
| UI-OPT-007 | 简化新建弹窗字段 | `frontend/src/views/Cases.vue` | 去除描述/优先级/状态/标签，仅保留名称/模块/步骤，case_type 固定 ui | P0 |
| UI-OPT-008 | 集成 IntentStepEditor | `frontend/src/views/Cases.vue` | 替换 CaseStepEditor 为 IntentStepEditor | P0 |
| UI-OPT-009 | 保存时转换逻辑 | `frontend/src/views/Cases.vue` | 保存前将意图步骤转换为 StepDefinition 格式 | P0 |
| UI-OPT-010 | 编辑回显兼容 | `frontend/src/views/Cases.vue` | 编辑时回显定位字段，旧数据自动补齐 | P0 |

#### 弹窗布局变更

**优化前**（当前）：
```
用例名称 | 所属模块 | 用例类型 | 优先级 | 状态 | 描述 | 标签 | 测试步骤(旧编辑器)
```

**优化后**（仅 UI 自动化）：
```
用例名称 | 所属模块 | 测试步骤(新意图编辑器)
```

**创建/编辑弹窗重构代码要点**：

```typescript
// showCreateDialog 简化
const showCreateDialog = () => {
  editMode.value = 'create'
  editForm.id = ''
  editForm.name = ''
  editForm.module_id = filterModuleId.value || ''
  editForm.case_type = 'ui'                   // 固定 ui
  editForm.intentSteps = [{                    // 意图步骤
    order: 1,
    description: '',
    expected: '',
    actual: '',
    waitAfter: 0,
    locatorType: '',
    locatorValue: '',
  }]
  editDialogVisible.value = true
}

// showEditDialog 增强 — 兼容旧数据
const showEditDialog = (row: any) => {
  editMode.value = 'edit'
  editForm.id = row.id
  editForm.name = row.name
  editForm.module_id = row.moduleId || row.module_id
  editForm.case_type = 'ui'

  // 将现有步骤转换为意图步骤格式
  const rawSteps = row.steps || []
  editForm.intentSteps = rawSteps.length > 0
    ? rawSteps.map((s: any) => toIntentStep(s))
    : [{ order: 1, description: '', expected: '', actual: '', waitAfter: 0, locatorType: '', locatorValue: '' }]

  editDialogVisible.value = true
}

// 旧数据 → 意图步骤（自动补齐定位）
const toIntentStep = (step: any): IntentStep => {
  const intentStep: IntentStep = {
    order: step.order || 1,
    description: step.name || '',
    expected: step.expected || '',
    actual: step.actual || '',
    waitAfter: step.waitAfter || 0,
    locatorType: step.locator_type || '',
    locatorValue: step.locator_value || '',
  }

  // 自动补齐：如果 locator 为空但 params.selector 有值，从中提取
  if (!intentStep.locatorType && !intentStep.locatorValue && step.params?.selector) {
    const selector = step.params.selector
    if (selector.startsWith('//') || selector.startsWith('(')) {
      intentStep.locatorType = 'xpath'
    } else if (selector.startsWith('#') && !selector.includes(' ')) {
      intentStep.locatorType = 'id'
      intentStep.locatorValue = selector.substring(1)
    } else {
      intentStep.locatorType = 'css'
    }
    intentStep.locatorValue = selector
  }

  return intentStep
}

// 保存时转换
const saveCase = async () => {
  // ... 表单校验 ...

  // 意图步骤 → StepDefinition 数组
  const steps = editForm.intentSteps
    .filter((s: IntentStep) => s.description.trim())
    .map((s: IntentStep) => toStepDefinition(s))

  const data = {
    name: editForm.name,
    module_id: editForm.module_id,
    case_type: 'ui',
    steps,
  }

  // ... 调用 API ...
}
```

---

### 2.4 阶段四：AI 定位后端服务 + 工作脚本（2 天）

#### 任务清单

| 任务ID | 任务名称 | 文件 | 说明 | 优先级 |
|--------|---------|------|------|--------|
| UI-OPT-011 | 注册 AI 定位路由 | `backend/app/main.py` | 注册 `ai_locator` 路由 | P0 |
| UI-OPT-012 | 创建 AI 定位路由 | `backend/app/routers/ai_locator.py` | `POST /api/ai/locate` 创建任务 + `GET /api/ai/locate/{task_id}/result` 查询结果 | P0 |
| UI-OPT-013 | 创建 AI 定位 Worker 脚本 | `backend/scripts/ai_locate_worker.py` | Playwright 开网页 + DOM 分析 + 定位匹配核心逻辑 | P0 |
| UI-OPT-014 | 创建前端 AI 定位 API 客户端 | `frontend/src/api/aiLocate.ts` | 封装 axios 调用 | P0 |

#### 核心设计：SSH 调用宿主机 Claude Code

后端（容器内）不直接执行 Playwright，而是通过 **SSH 调用宿主机上的 Claude Code** 来完成定位分析：

```
POST /api/ai/locate
  → 后端创建任务文件 data/ai_locate_tasks/{task_id}.json
  → 后端通过 SSH 调用宿主机：
      ssh ubains@192.168.5.60 "cd /data/third_party/plat-auto-test && \
        python scripts/ai_locate_worker.py --task-id TASK_ID"
  → 返回 task_id，状态 pending

宿主机上脚本自动运行：
  → Playwright 打开目标网页 → 提取 DOM 元素
  → 调用 Claude Code CLI（claude -p 非交互模式）分析匹配
  → 结果写入共享 Volume 的 data/ai_locate_tasks/{task_id}_result.json

GET /api/ai/locate/{task_id}/result
  → 后端读取结果文件返回
  → 前端自动填充定位信息
```

**关键**：脚本中的 `call_claude_code()` 函数通过 `subprocess.run(['claude', '-p', prompt, '--print'])` 调用宿主机上的 Claude Code CLI，利用 Claude 的语义理解能力做精准定位匹配。算法做初步筛选 + Claude Code 做精确匹配，双重保障定位准确率。

#### API 设计

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

**请求体**:
```python
class LocateRequest(BaseModel):
    step_description: str = Field(..., description="步骤描述")
    page_url: str = Field(..., description="目标网页URL")
    expected: str = Field(default="", description="预期结果")
```

**响应体**:
```python
class LocateTaskResponse(BaseModel):
    task_id: str = Field(..., description="任务ID")
    status: str = Field(..., description="任务状态: pending/completed/failed")
    message: str = Field(default="", description="提示信息")
```

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

**响应体**:
```python
class LocateCandidate(BaseModel):
    locator_type: str = Field(..., description="定位类型")
    locator_value: str = Field(..., description="定位值")
    confidence: float = Field(..., ge=0, le=1, description="置信度")
    element_info: Dict[str, Any] = Field(default_factory=dict, description="元素信息")

class LocateResultResponse(BaseModel):
    task_id: str = Field(...)
    status: str = Field(..., description="pending/completed/failed")
    success: bool = Field(False)
    candidates: List[LocateCandidate] = Field(default_factory=list)
    message: str = Field(default="")
```

#### AI 定位 Worker 脚本（核心）

`backend/scripts/ai_locate_worker.py` — 完整的定位脚本，详见 PRD 文档 2.3.4 节。

**核心流程**：
1. 读取任务文件（或命令行参数）
2. 使用 Playwright 同步 API 打开目标网页（复用项目已有 Playwright 依赖和 UA 伪装配置）
3. 提取页面所有可交互元素的 DOM 信息
4. **关键词算法初步筛选** + **Claude Code CLI 精确匹配**（`claude -p` 非交互模式）双重保障
5. 输出候选定位列表，写入结果文件

**匹配策略**：
| 策略 | 优先级 | 说明 |
|---------|--------|------|
| id 精确匹配 | ⭐⭐⭐⭐⭐ | 元素 id 与步骤描述匹配 |
| placeholder 匹配 | ⭐⭐⭐⭐ | input 的 placeholder 包含步骤关键词 |
| aria-label 匹配 | ⭐⭐⭐⭐ | aria-label 属性匹配 |
| 文本内容匹配 | ⭐⭐⭐ | button/a 的文本内容匹配 |
| name 属性匹配 | ⭐⭐ | name 属性匹配 |
| 语义推断 | ⭐⭐ | 输入→input 优先，点击→button 优先 |
| **Claude Code 语义分析** | 🔥 **最高** | 算法置信度<0.6 时触发，Claude 精确理解语义 |

**使用方式**：
```bash
# 配合后端（通过任务 ID，SSH 自动调用）
python scripts/ai_locate_worker.py --task-id locate_abc123

# 独立使用（直接传参，测试用）
python scripts/ai_locate_worker.py --step-desc "输入用户名" --page-url "https://192.168.5.44/login"
```

#### 任务目录结构

```
data/
├── ai_locate_tasks/
│   ├── locate_abc123.json          # 任务文件（后端创建）
│   ├── locate_abc123_result.json   # 结果文件（Worker 写入）
│   └── ...
```

---

### 2.5 阶段五：前端 AI 定位集成（1 天）

#### 任务清单

| 任务ID | 任务名称 | 文件 | 说明 | 优先级 |
|--------|---------|------|------|--------|
| UI-OPT-015 | 前端 AI 定位 API 调用 | `frontend/src/api/aiLocate.ts` | 封装 axios 调用（创建任务 + 轮询结果） | P0 |
| UI-OPT-016 | IntentStepEditor 集成定位 | `frontend/src/components/IntentStepEditor.vue` | "获取定位"按钮调用后端，轮询结果后自动填充 | P0 |

#### 前端 AI 定位 API 客户端

```typescript
// frontend/src/api/aiLocate.ts
import request from '@/utils/request'

export interface LocateRequest {
  step_description: string
  page_url: string
  expected?: string
}

export interface LocateTaskResponse {
  task_id: string
  status: string
  message: string
}

export interface LocateCandidate {
  locator_type: string
  locator_value: string
  confidence: number
  element_info: Record<string, any>
}

export interface LocateResultResponse {
  task_id: string
  status: string
  success: boolean
  candidates: LocateCandidate[]
  message: string
}

export const aiLocateApi = {
  /** 创建定位任务 */
  locate(data: LocateRequest): Promise<LocateTaskResponse> {
    return request.post('/api/ai/locate', data)
  },

  /** 查询定位结果 */
  getResult(taskId: string): Promise<LocateResultResponse> {
    return request.get(`/api/ai/locate/${taskId}/result`)
  },
}
```

#### 前端工作流

```typescript
const handleLocate = async (index: number) => {
  const step = steps.value[index]
  if (!step.description.trim()) {
    ElMessage.warning('请先填写步骤描述')
    return
  }

  locatingIndex.value = index
  try {
    // 1. 创建定位任务
    const task = await aiLocateApi.locate({
      step_description: step.description,
      page_url: pageUrl.value,
      expected: step.expected,
    })

    ElMessage.info(`定位任务已创建 (${task.task_id})，请在 Claude Code 中运行定位脚本`)

    // 2. 提示用户运行脚本
    // 用户需在 Claude Code 会话中输入：
    // ! python scripts/ai_locate_worker.py --task-id ${task.task_id}

    // 3. 轮询结果（每 5 秒一次，最多 60 秒）
    let retries = 12
    while (retries > 0) {
      await new Promise(r => setTimeout(r, 5000))
      const result = await aiLocateApi.getResult(task.task_id)

      if (result.status === 'completed') {
        if (result.success && result.candidates.length > 0) {
          const best = result.candidates[0]
          step.locatorType = best.locator_type
          step.locatorValue = best.locator_value
          ElMessage.success(`定位成功 (置信度: ${Math.round(best.confidence * 100)}%)`)
        } else {
          ElMessage.warning(result.message || '未找到匹配元素，请手动输入')
        }
        return
      }

      if (result.status === 'failed') {
        ElMessage.error('定位失败: ' + result.message)
        return
      }

      retries--
    }

    // 超时提示
    ElMessage.warning('定位超时，请确认已在 Claude Code 中运行定位脚本')
  } catch (error: any) {
    ElMessage.error('定位失败: ' + (error.message || '未知错误'))
  } finally {
    locatingIndex.value = -1
  }
}
```

---

### 2.6 阶段六：数据同步脚本 + 全流程验证（0.5 天）

#### 任务清单

| 任务ID | 任务名称 | 文件 | 说明 | 优先级 |
|--------|---------|------|------|--------|
| UI-OPT-019 | 同步脚本 | `backend/scripts/sync_to_server.py` | 本地 SQLite 用例同步到服务器 MySQL | P1 |
| UI-OPT-020 | 全流程集成测试 | 手动测试 | 验证创建/编辑/定位/同步全流程 | P0 |

#### 同步脚本

```python
# backend/scripts/sync_to_server.py
"""
同步脚本 — 将本地 SQLite 中的用例同步到服务器 MySQL

用法:
    python scripts/sync_to_server.py [--case-id CASE_ID] [--all]

说明:
    默认只同步本次新增的用例（created_at 大于上次同步时间）
    --all 同步所有用例
    --case-id CASE_ID 同步指定用例
"""
```

---

## 三、文件变更清单

### 3.1 新增文件

| 文件 | 说明 |
|------|------|
| `frontend/src/components/IntentStepEditor.vue` | 意图步骤编辑器组件 |
| `frontend/src/utils/stepParser.ts` | 步骤描述解析工具函数 |
| `frontend/src/api/aiLocate.ts` | AI 定位 API 客户端 |
| `backend/app/routers/ai_locator.py` | AI 定位路由（任务文件机制） |
| `backend/scripts/ai_locate_worker.py` | AI 定位 Worker 脚本（Playwright DOM 分析） |
| `backend/scripts/sync_to_server.py` | 数据同步脚本 |

### 3.2 修改文件

| 文件 | 变更说明 |
|------|---------|
| `frontend/src/types/case.ts` | StepDefinition 新增 locator_type/locator_value/actual |
| `frontend/src/views/Cases.vue` | 弹窗精简 + 集成 IntentStepEditor + 保存转换逻辑 |
| `backend/app/schemas/test_case.py` | StepDefinition 新增定位字段 |
| `backend/app/config.py` | 新增 AI 配置项 |
| `backend/app/main.py` | 注册 ai_locator 路由 |

### 3.3 删除/废弃文件

| 文件 | 说明 |
|------|------|
| `frontend/src/components/CaseStepEditor.vue` | 被 IntentStepEditor 替代（可保留作为非 UI 类型的步骤编辑器） |

---

## 四、实施顺序与依赖关系

```mermaid
graph TD
    A[UI-OPT-001 前端类型定义] --> B[UI-OPT-004 创建IntentStepEditor]
    A --> C[UI-OPT-002 后端Schema更新]
    C --> D[UI-OPT-012 创建AI定位路由]
    D --> E[UI-OPT-013 创建AI定位服务]
    E --> F[UI-OPT-015 前端AI定位API]
    F --> B
    B --> G[UI-OPT-007 弹窗精简]
    G --> H[UI-OPT-008 集成IntentStepEditor]
    H --> I[UI-OPT-009 保存转换逻辑]
    H --> J[UI-OPT-010 编辑回显兼容]
    A --> J
    J --> K[UI-OPT-020 全流程测试]
    I --> K
    F --> K
    K --> L[UI-OPT-019 同步脚本]
```

**实施顺序建议**：

```
第1天：UI-OPT-001 → UI-OPT-002 → UI-OPT-003 → UI-OPT-005
第2-3天：UI-OPT-004 → UI-OPT-006
第4天：UI-OPT-007 → UI-OPT-008 → UI-OPT-009 → UI-OPT-010
第5-6天：UI-OPT-011 → UI-OPT-012 → UI-OPT-013 → UI-OPT-014
第7天：UI-OPT-015 → UI-OPT-016
第8天：UI-OPT-019 → UI-OPT-020
```

---

## 五、测试要点

### 5.1 功能测试

| 测试项 | 操作 | 预期结果 |
|--------|------|----------|
| 新建弹窗 | 点击"新建用例" | 仅显示用例名称、所属模块、测试步骤，用例类型不可见且固定 ui |
| 意图步骤添加 | 点击"添加步骤" | 新增一行空白步骤，自动编号 |
| 步骤描述输入 | 输入"输入用户名" | 文本正常显示 |
| 预期结果输入 | 输入"页面跳转首页" | 文本正常显示 |
| 等待时间 | 输入 500 | 数字框显示 500 |
| 定位类型选择 | 下拉选择 css | 选中值正确 |
| 定位值输入 | 输入 #username | 文本正常显示 |
| 保存步骤转换 | 保存用例 | 步骤描述→name，自动推断 action，locator 同步到 params.selector |
| 编辑回显 | 编辑已创建用例 | 所有字段正常回显，定位信息完整 |
| 旧数据兼容 | 编辑旧用例（无 locator 字段） | 从 params.selector 自动推断补齐 |
| 获取定位 | 点击"获取定位" | 调用后端，定位信息自动填入 |
| 定位失败 | 无匹配元素 | 提示"未找到匹配元素" |

### 5.2 兼容性测试

| 测试项 | 说明 |
|--------|------|
| 旧数据编辑 | 编辑旧用例（无 locator_type/value），数据不丢失，自动补齐 |
| 非 UI 类型用例 | 非 UI 类型（api/security 等）保留原有弹窗和 CaseStepEditor |
| 空步骤保存 | 步骤描述为空的步骤被过滤，不保存 |
| 批量保存 | 同时保存多个步骤，顺序正确，order 连续 |

### 5.3 回归测试

| 测试项 | 说明 |
|--------|------|
| 用例列表加载 | 新旧用例列表正常显示，分页正常 |
| 用例执行 | 旧用例执行不受影响，定位信息正确 |
| 用例复制 | 复制功能正常，步骤数据完整 |
| 用例导入导出 | 导入导出 JSON 格式兼容，新增字段正确处理 |

---

## 六、回滚方案

### 6.1 前端回滚

```bash
# 回滚 Cases.vue 到优化前版本
git checkout HEAD -- frontend/src/views/Cases.vue
git checkout HEAD -- frontend/src/components/CaseStepEditor.vue
# 删除新组件
rm frontend/src/components/IntentStepEditor.vue
rm frontend/src/utils/stepParser.ts
```

### 6.2 后端回滚

```bash
# 回滚 Schema 到旧版本
git checkout HEAD -- backend/app/schemas/test_case.py
# 删除新文件
rm backend/app/routers/ai_locator.py
rm backend/scripts/ai_locate_worker.py
```

### 6.3 数据兼容

- 新增字段 `locator_type` / `locator_value` 为 `Optional`，回滚后旧代码忽略即可
- 数据库 `steps` 字段为 JSON 类型，新增字段不影响旧数据读取

---

## 七、验收标准

| 验收项 | 预期结果 |
|--------|----------|
| 新建UI用例弹窗 | 仅显示"用例名称""所属模块""测试步骤"三个核心区域 |
| 意图步骤编辑 | 可添加/删除/移动步骤，每行显示步骤描述+预期结果+实际结果+等待时间+定位 |
| 定位字段独立 | 步骤编辑器显示定位类型下拉+定位值输入框 |
| 自动推断动作 | 保存时根据步骤描述自动推断 action 类型 |
| 编辑回显 | 编辑时 locator_type/locator_value 正确回显 |
| 旧数据兼容 | 编辑旧用例时从 params.selector 自动补齐定位 |
| 保存转换 | 意图步骤保存后转为 StepDefinition 格式，数据完整 |
| 类型默认值 | 新建UI用例时 case_type 固定为 `ui` |
| 全流程可用 | 创建→编辑→保存→执行 链路正常 |

---

## 八、相关文档

- PRD: `Docs/PRD/需求文档/用例管理/_PRD_UI自动化用例录入优化.md`
- 当前用例创建流程: `HANDOFF.md`
- 项目指南: `CLAUDE.md`

---

*本文档由 Claude Code 生成，遵循项目 PRD → 计划执行文档工作流规范。*