Skip to content

Commit 6d2a3ea

Browse files
committed
feat: implement multi-frame crawler for iframe support
1 parent 4853c51 commit 6d2a3ea

File tree

2 files changed

+106
-1
lines changed

2 files changed

+106
-1
lines changed

webqa_agent/crawler/deep_crawler.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,17 @@ async def crawl(
222222
page = self.page
223223

224224
try:
225+
226+
try:
227+
if hasattr(page, 'frames') and len(page.frames) > 1:
228+
_, merged_id_map = await self.crawl_all_frames(page=page, enable_highlight=highlight)
229+
return CrawlResultModel(
230+
flat_element_map=ElementMap(data=merged_id_map or {}),
231+
element_tree={}
232+
)
233+
except Exception:
234+
pass
235+
225236
# Build JavaScript payload for element detection
226237
payload = (
227238
f"(() => {{"
@@ -394,6 +405,99 @@ def _dedupe_consecutive(seq):
394405
# Return as compact JSON array
395406
return json.dumps(items, ensure_ascii=False, separators=(",", ":"))
396407

408+
async def crawl_all_frames(self, page=None, enable_highlight=False):
409+
"""
410+
爬取主页面及所有 iframe 的元素(支持跨域与嵌套),并返回统一的 id 列表与映射。
411+
412+
返回值:
413+
(ids, id_map)
414+
- ids: List[str] 高亮编号(全局唯一)
415+
- id_map: Dict[str, Dict] 元素信息,包含 tagName/className/innerText/center_x/center_y
416+
其中 center_x/center_y 为“主页面视口坐标”,可直接用于 page.mouse.click
417+
"""
418+
if not page:
419+
page = self.page
420+
421+
import logging
422+
423+
ids = []
424+
merged_id_map: Dict[str, Dict[str, Any]] = {}
425+
426+
# helper: frame scroll
427+
async def _get_frame_scroll(f):
428+
try:
429+
return await f.evaluate("() => ({x: window.scrollX, y: window.scrollY})")
430+
except Exception:
431+
return {"x": 0, "y": 0}
432+
433+
# helper: accumulate iframe offsets to top-page viewport
434+
async def _accumulate_iframe_offsets(f):
435+
total_left, total_top = 0, 0
436+
cur = f
437+
while True:
438+
parent = cur.parent_frame
439+
if not parent:
440+
break
441+
try:
442+
el = await cur.frame_element()
443+
rect = await el.evaluate("(el) => el.getBoundingClientRect()")
444+
total_left += rect.get('left', 0) or 0
445+
total_top += rect.get('top', 0) or 0
446+
except Exception:
447+
pass
448+
cur = parent
449+
return total_left, total_top
450+
451+
# 1) main frame first (base = 0)
452+
try:
453+
payload_main = f"window.__highlightBase__ = 0; window._highlight = {str(enable_highlight).lower()};\n{self.read_js(self.DETECTOR_JS)}"
454+
await page.evaluate(payload_main)
455+
main_tree, main_id_map = await page.evaluate("buildElementTree()")
456+
# 保持与单 frame 逻辑一致:直接使用 detector 返回的文档坐标(含主页面滚动)
457+
for k, v in (main_id_map or {}).items():
458+
key = str(k)
459+
ids.append(key)
460+
merged_id_map[key] = {kk: v.get(kk) for kk in ('tagName','className','innerText','center_x','center_y') if v.get(kk) is not None}
461+
except Exception as e:
462+
logging.warning(f"Main frame crawl failed: {e}")
463+
464+
# 2) sub frames
465+
frames = page.frames
466+
for idx, frame in enumerate(frames):
467+
if frame == page.main_frame:
468+
continue
469+
try:
470+
highlight_base = (idx + 1) * 1000
471+
payload = f"window.__highlightBase__ = {highlight_base}; window._highlight = {str(enable_highlight).lower()};\n{self.read_js(self.DETECTOR_JS)}"
472+
await frame.evaluate(payload)
473+
iframe_tree, iframe_id_map = await frame.evaluate("buildElementTree()")
474+
frame_scroll = await _get_frame_scroll(frame)
475+
total_left, total_top = await _accumulate_iframe_offsets(frame)
476+
top_scroll = await _get_frame_scroll(page)
477+
478+
for k, v in (iframe_id_map or {}).items():
479+
try:
480+
# frame document -> frame viewport
481+
vx = (v.get('center_x') or 0) - (frame_scroll.get('x') or 0)
482+
vy = (v.get('center_y') or 0) - (frame_scroll.get('y') or 0)
483+
# frame viewport -> top-page viewport
484+
gvx = total_left + vx
485+
gvy = total_top + vy
486+
# top-page viewport -> top-page document
487+
gx = gvx + (top_scroll.get('x') or 0)
488+
gy = gvy + (top_scroll.get('y') or 0)
489+
v['center_x'] = gx
490+
v['center_y'] = gy
491+
except Exception:
492+
pass
493+
key = str(k)
494+
ids.append(key)
495+
merged_id_map[key] = {kk: v.get(kk) for kk in ('tagName','className','innerText','center_x','center_y') if v.get(kk) is not None}
496+
except Exception as e:
497+
logging.warning(f"Sub frame crawl failed: {e}")
498+
499+
return ids, merged_id_map
500+
397501
# ------------------------------------------------------------------------
398502
# DOM CACHE MANAGEMENT
399503
# ------------------------------------------------------------------------

webqa_agent/crawler/js/element_detector.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
window._viewportOnly = window._viewportOnly ?? false; // Viewport-only detection filter
3333
window._filterMedia = window._filterMedia ?? false; // Media element filter for highlighting
3434
let idCounter = 1;
35-
let highlightIndex = 1;
35+
// let highlightIndex = 1;
36+
let highlightIndex = (typeof window.__highlightBase__ === 'number' ? window.__highlightBase__ : 0) + 1;
3637
const elementToId = new WeakMap();
3738
const highlightMap = new WeakMap();
3839
let highlightIdMap = new WeakMap();

0 commit comments

Comments
 (0)