提交 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()
# ==================== 目录配置 ====================
# 任务文件目录(与项目 data 目录一致)
TASK_DIR = Path(__file__).parent.parent.parent / "data" / "ai_locate_tasks"
# 任务文件目录:容器内 /app/data 映射到宿主机 /data/third_party/plat-auto-test/data
# 优先使用共享 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"
......
......@@ -34,31 +34,64 @@ from pathlib import Path
def extract_interactive_elements(page) -> list:
"""提取页面中所有可交互元素的信息"""
"""提取页面中所有可交互元素的信息(含 micro-app 微前端容器)"""
elements = page.evaluate("""
() => {
const interactive = [];
const tags = ['input', 'button', 'a', 'select', 'textarea', 'label'];
const seen = new Set();
// 在给定根节点下收集可交互元素
function collect(root, prefix) {
if (!root) return;
root.querySelectorAll(tags.join(',')).forEach(el => {
const rect = el.getBoundingClientRect();
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 = {
tag: el.tagName.toLowerCase(),
type: el.type || '',
id: el.id || '',
name: el.name || '',
class: (typeof el.className === 'string' ? 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,
scope: prefix || 'main',
};
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') || ''));
}
});
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);
// 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;
......@@ -261,10 +294,12 @@ def run_locate(step_description: str, page_url: str, expected: str = "") -> dict
'--disable-gpu',
]
)
# 内网自签证书需忽略 HTTPS 错误
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',
ignore_https_errors=True,
)
page = context.new_page()
page.add_init_script("""
......@@ -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.wait_for_timeout(2000) # 等待 micro-app 等微前端框架渲染
page_title = page.title()
print(f"[AI定位] 页面加载完成: {page_title}")
print(f"[AI定位] 页面加载完成: {page_title} (url={page.url})")
# 提取可交互元素
elements = extract_interactive_elements(page)
......@@ -318,8 +355,11 @@ def main():
if args.task_id:
# 任务文件方式:配合后端使用
script_dir = Path(__file__).parent.parent
task_dir = script_dir / 'data' / 'ai_locate_tasks'
# 优先使用与容器共享的 data 目录(/data/third_party/plat-auto-test/data)
# 容器内 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"
result_file = task_dir / f"{args.task_id}_result.json"
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论