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

fix(ai-locate): 修复任务路径/权限/HTTPS,支持 micro-app 微前端定位

- 任务目录对齐容器共享 volume(/app/data  宿主机 data)
- SSH 调用改用 python3 + backend/scripts 路径
- 忽略内网自签 HTTPS 证书错误
- 提取元素支持 micro-app / micro-app-body 容器
- SPA 页面额外等待 2s 渲染
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 0d1979ff
...@@ -35,8 +35,11 @@ router = APIRouter() ...@@ -35,8 +35,11 @@ router = APIRouter()
# ==================== 目录配置 ==================== # ==================== 目录配置 ====================
# 任务文件目录(与项目 data 目录一致) # 任务文件目录:容器内 /app/data 映射到宿主机 /data/third_party/plat-auto-test/data
TASK_DIR = Path(__file__).parent.parent.parent / "data" / "ai_locate_tasks" # 优先使用共享 data 目录,保证容器与宿主机脚本读写同一位置
_SHARED_DATA = Path("/app/data/ai_locate_tasks")
_LOCAL_DATA = Path(__file__).parent.parent.parent / "data" / "ai_locate_tasks"
TASK_DIR = _SHARED_DATA if _SHARED_DATA.parent.exists() else _LOCAL_DATA
# 部署目录(服务器上) # 部署目录(服务器上)
DEPLOY_DIR = "/data/third_party/plat-auto-test" DEPLOY_DIR = "/data/third_party/plat-auto-test"
......
...@@ -34,22 +34,31 @@ from pathlib import Path ...@@ -34,22 +34,31 @@ from pathlib import Path
def extract_interactive_elements(page) -> list: def extract_interactive_elements(page) -> list:
"""提取页面中所有可交互元素的信息""" """提取页面中所有可交互元素的信息(含 micro-app 微前端容器)"""
elements = page.evaluate(""" elements = page.evaluate("""
() => { () => {
const interactive = []; const interactive = [];
const tags = ['input', 'button', 'a', 'select', 'textarea', 'label']; const tags = ['input', 'button', 'a', 'select', 'textarea', 'label'];
const seen = new Set();
document.querySelectorAll(tags.join(',')).forEach(el => { // 在给定根节点下收集可交互元素
function collect(root, prefix) {
if (!root) return;
root.querySelectorAll(tags.join(',')).forEach(el => {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return; if (rect.width === 0 || rect.height === 0) return;
// 去重:用位置+标签作为 key
const key = `${el.tagName}_${Math.round(rect.x)}_${Math.round(rect.y)}_${el.placeholder||''}_${(el.textContent||'').trim().slice(0,20)}`;
if (seen.has(key)) return;
seen.add(key);
const info = { const info = {
tag: el.tagName.toLowerCase(), tag: el.tagName.toLowerCase(),
type: el.type || '', type: el.type || '',
id: el.id || '', id: el.id || '',
name: el.name || '', name: el.name || '',
class: el.className || '', class: (typeof el.className === 'string' ? el.className : '') || '',
text: (el.textContent || '').trim().slice(0, 100), text: (el.textContent || '').trim().slice(0, 100),
placeholder: el.placeholder || '', placeholder: el.placeholder || '',
'aria-label': el.getAttribute('aria-label') || '', 'aria-label': el.getAttribute('aria-label') || '',
...@@ -57,9 +66,33 @@ def extract_interactive_elements(page) -> list: ...@@ -57,9 +66,33 @@ def extract_interactive_elements(page) -> list:
href: el.href || '', href: el.href || '',
value: el.value || '', value: el.value || '',
visible: el.offsetParent !== null, visible: el.offsetParent !== null,
scope: prefix || 'main',
}; };
interactive.push(info); interactive.push(info);
}); });
}
// 1. 主文档
collect(document, 'main');
// 2. micro-app 微前端容器(jd-micro / micro-app 框架)
document.querySelectorAll('micro-app').forEach(ma => {
// micro-app-body 是实际内容容器
const body = ma.querySelector('micro-app-body') || ma;
collect(body, 'micro-app:' + (ma.getAttribute('name') || ''));
// 也检查 shadowRoot
if (ma.shadowRoot) {
collect(ma.shadowRoot, 'shadow:' + (ma.getAttribute('name') || ''));
}
});
// 3. 普通 iframe
document.querySelectorAll('iframe').forEach((iframe, idx) => {
try {
const doc = iframe.contentDocument || iframe.contentWindow?.document;
if (doc) collect(doc, 'iframe:' + idx);
} catch (e) { /* cross-origin */ }
});
return interactive; return interactive;
} }
...@@ -261,10 +294,12 @@ def run_locate(step_description: str, page_url: str, expected: str = "") -> dict ...@@ -261,10 +294,12 @@ def run_locate(step_description: str, page_url: str, expected: str = "") -> dict
'--disable-gpu', '--disable-gpu',
] ]
) )
# 内网自签证书需忽略 HTTPS 错误
context = browser.new_context( context = browser.new_context(
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/126.0.0.0', user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/126.0.0.0',
locale='zh-CN', locale='zh-CN',
timezone_id='Asia/Shanghai', timezone_id='Asia/Shanghai',
ignore_https_errors=True,
) )
page = context.new_page() page = context.new_page()
page.add_init_script(""" page.add_init_script("""
...@@ -272,9 +307,11 @@ def run_locate(step_description: str, page_url: str, expected: str = "") -> dict ...@@ -272,9 +307,11 @@ def run_locate(step_description: str, page_url: str, expected: str = "") -> dict
""") """)
# 打开目标页面 # 打开目标页面
# SPA / 微前端可能需要额外等待渲染
page.goto(page_url, wait_until='networkidle', timeout=30000) page.goto(page_url, wait_until='networkidle', timeout=30000)
page.wait_for_timeout(2000) # 等待 micro-app 等微前端框架渲染
page_title = page.title() page_title = page.title()
print(f"[AI定位] 页面加载完成: {page_title}") print(f"[AI定位] 页面加载完成: {page_title} (url={page.url})")
# 提取可交互元素 # 提取可交互元素
elements = extract_interactive_elements(page) elements = extract_interactive_elements(page)
...@@ -318,8 +355,11 @@ def main(): ...@@ -318,8 +355,11 @@ def main():
if args.task_id: if args.task_id:
# 任务文件方式:配合后端使用 # 任务文件方式:配合后端使用
script_dir = Path(__file__).parent.parent # 优先使用与容器共享的 data 目录(/data/third_party/plat-auto-test/data)
task_dir = script_dir / 'data' / 'ai_locate_tasks' # 容器内 TASK_DIR 映射到宿主机 DEPLOY_DIR/data/ai_locate_tasks
deploy_data_dir = Path('/data/third_party/plat-auto-test/data/ai_locate_tasks')
script_data_dir = Path(__file__).parent.parent / 'data' / 'ai_locate_tasks'
task_dir = deploy_data_dir if deploy_data_dir.exists() else script_data_dir
task_file = task_dir / f"{args.task_id}.json" task_file = task_dir / f"{args.task_id}.json"
result_file = task_dir / f"{args.task_id}_result.json" result_file = task_dir / f"{args.task_id}_result.json"
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论