autool/DroidBot/platforms/web/web_input_event.py
2026-06-17 19:44:18 +08:00

454 lines
18 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Web Input Event Implementation
Concrete implementation of AbstractInputEvent for Web applications.
"""
from typing import Optional, Dict, Any, List
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from ...core.abstract_input_event import (
AbstractInputEvent, EventType, BaseTouchEvent, BaseLongTouchEvent,
BaseSwipeEvent, BaseScrollEvent, BaseSetTextEvent, BaseKeyEvent,
BaseKillAppEvent
)
class WebTouchEvent(BaseTouchEvent):
"""
Web 点击事件实现
"""
def send(self, device: 'AbstractDevice') -> bool:
"""
发送点击事件到 Web 设备
:param device: Web 设备对象
:return: 是否发送成功
"""
try:
# 如果有视图信息,通过坐标查找元素并点击
if (self.view or (self.x is not None and self.y is not None)) and device.driver:
if self.view:
bounds = self.view.get('bounds', [[0, 0], [0, 0]])
x = (bounds[0][0] + bounds[1][0]) / 2
y = (bounds[0][1] + bounds[1][1]) / 2
else: # self.x and self.y
x = self.x
y = self.y
if getattr(device, "engine", "selenium") == "playwright":
device.driver.mouse.click(x, y)
return True
# 使用JavaScript执行点击避免坐标越界问题
js_script = f"""
var el = document.elementFromPoint({x}, {y});
if (el) {{
el.click();
return true;
}}
return false;
"""
success = device.driver.execute_script(js_script)
if not success:
device.logger.warning(f"Could not find element at ({x}, {y}) to click.")
return success
return False
except Exception as e:
device.logger.error(f"Failed to send Web touch event: {e}")
return False
class WebLongTouchEvent(BaseLongTouchEvent):
"""
Web 长按事件实现
"""
def send(self, device: 'AbstractDevice') -> bool:
"""
发送长按事件到 Web 设备
:param device: Web 设备对象
:return: 是否发送成功
"""
try:
x, y = None, None
if self.view:
bounds = self.view.get('bounds', [[0, 0], [0, 0]])
x = (bounds[0][0] + bounds[1][0]) / 2
y = (bounds[0][1] + bounds[1][1]) / 2
elif self.x is not None and self.y is not None:
x = self.x
y = self.y
if x is not None and y is not None and device.driver:
if getattr(device, "engine", "selenium") == "playwright":
device.driver.mouse.move(x, y)
device.driver.mouse.down()
import time
time.sleep(self.duration / 1000.0)
device.driver.mouse.up()
return True
# 使用JavaScript模拟长按事件
element = device.driver.execute_script(f"return document.elementFromPoint({x}, {y});")
if element:
js_script = """
var element = arguments[0];
var duration = arguments[1];
var touchStart = new TouchEvent('touchstart', {
bubbles: true, cancelable: true,
touches: [{ clientX: arguments[2], clientY: arguments[3] }]
});
var touchEnd = new TouchEvent('touchend', {
bubbles: true, cancelable: true
});
element.dispatchEvent(touchStart);
setTimeout(function() {
element.dispatchEvent(touchEnd);
}, duration);
"""
device.driver.execute_script(js_script, element, self.duration, x, y)
return True
return False
except Exception as e:
device.logger.error(f"Failed to send Web long touch event: {e}")
return False
class WebSwipeEvent(BaseSwipeEvent):
"""
Web 滑动事件实现
"""
def send(self, device: 'AbstractDevice') -> bool:
"""
发送滑动事件到 Web 设备
:param device: Web 设备对象
:return: 是否发送成功
"""
try:
if device.driver:
if getattr(device, "engine", "selenium") == "playwright":
device.driver.mouse.move(self.start_x, self.start_y)
device.driver.mouse.down()
import time
time.sleep(0.05)
# move in steps for swipe gesture
device.driver.mouse.move(self.end_x, self.end_y, steps=10)
time.sleep(0.1)
device.driver.mouse.up()
return True
# 使用JavaScript模拟滑动事件
js_script = f"""
// 创建触摸开始事件
var touchStart = new TouchEvent('touchstart', {{
bubbles: true,
cancelable: true,
touches: [{{
clientX: {self.start_x},
clientY: {self.start_y}
}}]
}});
// 创建触摸移动事件
var touchMove = new TouchEvent('touchmove', {{
bubbles: true,
cancelable: true,
touches: [{{
clientX: {self.end_x},
clientY: {self.end_y}
}}]
}});
// 创建触摸结束事件
var touchEnd = new TouchEvent('touchend', {{
bubbles: true,
cancelable: true
}});
// 在body元素上分发事件
document.body.dispatchEvent(touchStart);
// 短暂延迟后分发移动事件
setTimeout(function() {{
document.body.dispatchEvent(touchMove);
// 再次延迟后分发结束事件
setTimeout(function() {{
document.body.dispatchEvent(touchEnd);
}}, 100);
}}, 50);
"""
device.driver.execute_script(js_script)
return True
return False
except Exception as e:
device.logger.error(f"Failed to send Web swipe event: {e}")
return False
class WebScrollEvent(BaseScrollEvent):
"""
Web 滚动事件实现
"""
def send(self, device: 'AbstractDevice') -> bool:
"""
发送滚动事件到 Web 设备
:param device: Web 设备对象
:return: 是否发送成功
"""
try:
if device.driver:
if getattr(device, "engine", "selenium") == "playwright":
if self.direction == self.DIRECTION_UP:
device.driver.keyboard.press("PageUp")
elif self.direction == self.DIRECTION_DOWN:
device.driver.keyboard.press("PageDown")
elif self.direction == self.DIRECTION_LEFT:
device.driver.keyboard.press("ArrowLeft")
elif self.direction == self.DIRECTION_RIGHT:
device.driver.keyboard.press("ArrowRight")
return True
# 使用 ActionChains 执行滚动操作
actions = ActionChains(device.driver)
# 根据方向执行滚动
if self.direction == self.DIRECTION_UP:
actions.send_keys(Keys.PAGE_UP)
elif self.direction == self.DIRECTION_DOWN:
actions.send_keys(Keys.PAGE_DOWN)
elif self.direction == self.DIRECTION_LEFT:
actions.send_keys(Keys.ARROW_LEFT)
elif self.direction == self.DIRECTION_RIGHT:
actions.send_keys(Keys.ARROW_RIGHT)
actions.perform()
return True
return False
except Exception as e:
device.logger.error(f"Failed to send Web scroll event: {e}")
return False
class WebSetTextEvent(BaseSetTextEvent):
"""
Web 文本输入事件实现
"""
def send(self, device: 'AbstractDevice') -> bool:
"""
发送文本输入事件到 Web 设备
:param device: Web 设备对象
:return: 是否发送成功
"""
try:
if self.view and device.driver:
bounds = self.view.get('bounds', [[0, 0], [0, 0]])
x = (bounds[0][0] + bounds[1][0]) / 2
y = (bounds[0][1] + bounds[1][1]) / 2
if getattr(device, "engine", "selenium") == "playwright":
escaped_text = self.text.replace('`', '\\`').replace('$', '\\$')
js_script = f"""() => {{
var element = document.elementFromPoint({x}, {y});
if (element) {{
element.focus();
element.value = '';
}}
}}"""
device.driver.evaluate(js_script)
device.driver.keyboard.type(self.text)
device.driver.evaluate(f"""() => {{
var element = document.elementFromPoint({x}, {y});
if (element) {{
element.dispatchEvent(new Event('input', {{ bubbles: true }}));
element.dispatchEvent(new Event('change', {{ bubbles: true }}));
}}
}}""")
return True
# 使用JavaScript找到元素并设置文本
element = device.driver.execute_script(f"return document.elementFromPoint({x}, {y});")
if element:
# 先点击元素以获取焦点
try:
device.driver.execute_script("arguments[0].click();", element)
except Exception as e:
device.logger.warning(f"Failed to focus element before setting text: {e}")
# 清空现有文本并输入新文本
device.driver.execute_script("""
var element = arguments[0];
var text = arguments[1];
// 清空现有文本
element.value = '';
// 设置新文本
element.value = text;
// 触发input和change事件
var event = new Event('input', { bubbles: true });
element.dispatchEvent(event);
var changeEvent = new Event('change', { bubbles: true });
element.dispatchEvent(changeEvent);
""", element, self.text)
return True
return False
except Exception as e:
device.logger.error(f"Failed to send Web set text event: {e}")
return False
class WebKeyEvent(BaseKeyEvent):
"""
Web 按键事件实现
"""
def send(self, device: 'AbstractDevice') -> bool:
"""
发送按键事件到 Web 设备
:param device: Web 设备对象
:return: 是否发送成功
"""
try:
if device.driver:
if getattr(device, "engine", "selenium") == "playwright":
if self.key_name == self.KEY_BACK:
device.driver.go_back()
return True
key_map = {
self.KEY_HOME: "Home",
self.KEY_ENTER: "Enter",
self.KEY_ESCAPE: "Escape"
}
key = key_map.get(self.key_name, self.key_name)
device.driver.keyboard.press(key)
return True
# 使用 ActionChains 执行按键操作
actions = ActionChains(device.driver)
# 映射按键名称到 Keys 常量
key_map = {
self.KEY_HOME: Keys.HOME,
self.KEY_ENTER: Keys.ENTER,
self.KEY_ESCAPE: Keys.ESCAPE
}
if self.key_name == self.KEY_BACK:
# 对于 Web 平台BACK 通常意味着浏览器后退
device.back()
return True
key = key_map.get(self.key_name, self.key_name)
actions.send_keys(key)
actions.perform()
return True
return False
except Exception as e:
device.logger.error(f"Failed to send Web key event: {e}")
return False
class WebKillAppEvent(BaseKillAppEvent):
"""
Web 应用重置事件
清除浏览器状态Cookies/Storage、关闭多余标签页、导航回初始页面。
对应 DroidBot 框架中的 KillAppEvent在探索循环启动和停滞恢复时调用。
"""
def send(self, device: 'AbstractDevice') -> bool:
"""
重置 Web 应用状态
:param device: Web 设备对象
:return: 是否发送成功
"""
try:
if not device.driver:
device.logger.warning("WebKillAppEvent: driver not available")
return False
# 1. 清除浏览器存储状态
try:
if getattr(device, "engine", "selenium") == "playwright":
device.driver.evaluate("try { localStorage.clear(); } catch(e) {}")
device.driver.evaluate("try { sessionStorage.clear(); } catch(e) {}")
if hasattr(device, 'playwright_context') and device.playwright_context:
device.playwright_context.clear_cookies()
else:
device.driver.execute_script(
"try { localStorage.clear(); } catch(e) {}"
"try { sessionStorage.clear(); } catch(e) {}"
)
device.driver.delete_all_cookies()
except Exception as e:
device.logger.warning(f"清除浏览器存储失败(可忽略): {e}")
# 2. 关闭所有多余标签页,只保留第一个
try:
if getattr(device, "engine", "selenium") == "playwright":
if hasattr(device, 'playwright_context') and device.playwright_context:
pages = device.playwright_context.pages
if len(pages) > 1:
for p in pages[1:]:
p.close()
device.driver = pages[0]
device.driver.bring_to_front()
device.logger.info(f"关闭了 {len(pages) - 1} 个多余标签页")
else:
handles = device.driver.window_handles
if len(handles) > 1:
first_handle = handles[0]
for h in handles[1:]:
device.driver.switch_to.window(h)
device.driver.close()
device.driver.switch_to.window(first_handle)
device.logger.info(f"关闭了 {len(handles) - 1} 个多余标签页")
except Exception as e:
device.logger.warning(f"关闭多余标签页失败: {e}")
# 3. 导航回初始页面
app_url = device.app_url
if app_url:
if getattr(device, "engine", "selenium") == "playwright":
device.driver.goto(app_url)
if hasattr(device, "_wait_for_page_ready"):
device._wait_for_page_ready()
device._current_url = device.driver.url
else:
device.driver.get(app_url)
if hasattr(device, "_wait_for_page_ready"):
device._wait_for_page_ready()
device._current_url = device.driver.current_url
device.logger.info(f"已导航回初始页面: {app_url}")
# 4. 更新窗口句柄缓存
if getattr(device, "engine", "selenium") != "playwright":
device._window_handles = set(device.driver.window_handles)
return True
except Exception as e:
device.logger.error(f"WebKillAppEvent 执行失败: {e}")
return False