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

feat: 本地元素定位功能 - 实际访问页面获取真实元素定位器

新增本地元素定位 API,通过 Playwright 实际访问被测页面,提取真实元素定位器:
- backend/app/routers/element_locator.py: 新增 /api/element/locate 端点,支持 auto_login 和 micro-app 微前端
- backend/app/main.py: 注册 element_locator 路由
- frontend/src/api/elementLocate.ts: 新增前端元素定位 API 接口
- frontend/src/components/IntentStepEditor.vue: 修改 handleLocate 调用本地 API,简化流程

优势:本地开发可用,无需容器化部署;响应更快;支持离线使用
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 b0f0d211
......@@ -27,7 +27,7 @@ from fastapi.responses import FileResponse
from app.config import settings
from app.database import init_db
from app.routers import modules, cases, executions, recorder, stats, reports, cleanup, batch, dependencies, security, ai_locator, device_sim, system
from app.routers import modules, cases, executions, recorder, stats, reports, cleanup, batch, dependencies, security, ai_locator, device_sim, system, element_locator
# 配置日志
logging.basicConfig(
......@@ -164,6 +164,12 @@ app.include_router(
tags=["系统管理"]
)
app.include_router(
element_locator.router,
prefix="/api/element",
tags=["元素定位"]
)
# ==================== 根路径 ====================
......
此差异已折叠。
/**
* 元素定位 API
*
* 提供本地元素定位功能,通过 Playwright 访问被测页面,
* 提取可交互元素,匹配步骤描述,返回候选定位器列表。
*
* @module api/elementLocate
* @author czj
* @date 2026-08-03
*/
import request from '@/utils/request'
/**
* 元素定位请求参数
*/
export interface LocateRequest {
/** 步骤描述,如"输入用户名" */
step_description: string
/** 目标页面 URL */
page_url: string
/** 预期结果(可选) */
expected?: string
/** 是否自动登录(默认 true) */
auto_login?: boolean
}
/**
* 定位候选
*/
export interface LocateCandidate {
/** 定位类型: css/xpath/text/id/name */
locator_type: string
/** 定位值 */
locator_value: string
/** 置信度 0-1 */
confidence: number
/** 元素信息 */
element_info: Record<string, any>
}
/**
* 元素定位响应
*/
export interface LocateResponse {
/** 是否成功 */
success: boolean
/** 候选定位器列表(按置信度降序) */
candidates: LocateCandidate[]
/** 消息 */
message: string
/** 页面截图(base64,可选) */
screenshot?: string
}
/**
* 元素定位 API
*/
export const elementLocateApi = {
/**
* 执行元素定位
*
* @param data - 定位请求参数
* @returns 定位结果
*
* @example
* const result = await elementLocateApi.locate({
* step_description: '输入用户名',
* page_url: 'https://192.168.5.44/',
* auto_login: true,
* })
*
* if (result.success && result.candidates.length > 0) {
* const best = result.candidates[0]
* console.log(`定位成功: ${best.locator_value} (${best.confidence})`)
* }
*/
async locate(data: LocateRequest): Promise<LocateResponse> {
const response = await request.post('/api/element/locate', data, {
timeout: 60000, // 60秒超时(Playwright 启动较慢)
})
return response.data
},
}
\ No newline at end of file
......@@ -172,7 +172,7 @@ import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Top, Bottom, Delete, Plus, Search } from '@element-plus/icons-vue'
import type { IntentStep } from '@/utils/stepParser'
import { aiLocateApi } from '@/api/aiLocate'
import { elementLocateApi } from '@/api/elementLocate'
import { usePageUrl } from '@/utils/usePageUrl'
const props = defineProps<{
......@@ -238,15 +238,20 @@ const emitChange = () => {
emit('update:modelValue', steps.value)
}
// ==================== AI 定位 ====================
// ==================== 元素定位 ====================
/**
* 获取定位信息
* 获取定位信息(本地 Playwright 方式)
*
* 步骤:
* 1. 创建定位任务(后端写入任务文件)
* 2. 后端 SSH 调用宿主机 Claude Code 执行定位脚本
* 3. 轮询结果,自动填充到定位字段
* 流程:
* 1. 调用本地元素定位 API(/api/element/locate)
* 2. 后端启动 Playwright 访问页面、提取元素、匹配定位器
* 3. 自动填充定位信息到步骤中
*
* 优势:
* - 本地开发可用,无需容器化部署
* - 响应更快(无 SSH 开销)
* - 支持离线使用(不依赖 Claude Code)
*/
const handleLocate = async (index: number) => {
const step = steps.value[index]
......@@ -262,60 +267,33 @@ const handleLocate = async (index: number) => {
}
locatingIndex.value = index
try {
// 1. 创建定位任务
const task = await aiLocateApi.locate({
ElMessage.info('正在启动浏览器访问页面...')
// 调用本地元素定位 API
const result = await elementLocateApi.locate({
step_description: step.description,
page_url: pageUrl.value,
expected: step.expected,
auto_login: true, // 默认自动登录
})
ElMessage.info(`定位任务已创建 (${task.task_id}),正在等待结果...`)
// 2. 轮询结果(每 3 秒一次,最多 60 秒)
let retries = 20
while (retries > 0) {
await new Promise(r => setTimeout(r, 3000))
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 || '未找到匹配元素,请手动输入定位信息')
}
emitChange()
return
}
if (result.success && result.candidates.length > 0) {
// 取置信度最高的候选
const best = result.candidates[0]
if (result.status === 'failed') {
ElMessage.error('定位失败: ' + result.message)
return
}
// 自动填充定位信息
step.locatorType = best.locator_type
step.locatorValue = best.locator_value
retries--
ElMessage.success(`定位成功!置信度 ${(best.confidence * 100).toFixed(0)}%`)
emitChange()
} else {
ElMessage.warning(result.message || '未找到匹配元素,请手动输入定位信息')
}
// 超时:检查是否有结果文件但状态未更新
try {
const result = await aiLocateApi.getResult(task.task_id)
if (result.status === 'completed' && 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)}%)`)
emitChange()
return
}
} catch {
// 忽略
}
ElMessage.warning('定位超时,请确认宿主机上定位脚本已运行,或手动输入定位信息')
} catch (error: any) {
console.error('元素定位失败:', error)
ElMessage.error('定位失败: ' + (error.message || '未知错误'))
} finally {
locatingIndex.value = -1
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论