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

200 lines
7.3 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 Device State Implementation
Concrete implementation of AbstractDeviceState for Web applications.
"""
import hashlib
from typing import Optional, Dict, Any, List
from ...core.abstract_device_state import AbstractDeviceState
class WebDeviceState(AbstractDeviceState):
"""
Web 设备状态的具体实现
"""
def __init__(self, device, views: List[Dict[str, Any]] = None,
tag: str = None, screenshot_path: str = None):
"""
初始化 Web 设备状态
"""
super().__init__(device, tag=tag, screenshot_path=screenshot_path)
self._views = views or []
self._window_title = device.get_page_title() if device else ""
self._url = device.get_current_url() if device else ""
self._generate_view_strs()
# 缓存
self._state_str = None
self._structure_str = None
# ==================== 视图信息 ====================
@property
def views(self) -> List[Dict[str, Any]]:
"""获取视图列表"""
return self._views
# ==================== 状态标识 ====================
@property
def state_str(self) -> str:
"""获取状态的唯一标识字符串"""
if self._state_str is None:
view_signatures = sorted(self._get_view_signature(view) for view in self._views)
state_raw = f"url={self._url}&title={self._window_title}&views=" + ",".join(view_signatures)
self._state_str = hashlib.md5(state_raw.encode('utf-8')).hexdigest()
return self._state_str
@property
def structure_str(self) -> str:
"""获取状态的结构标识(忽略控件文本内容)"""
if self._structure_str is None:
view_signatures = sorted(self._get_content_free_view_signature(view) for view in self._views)
structure_raw = f"url={self._url}&title={self._window_title}&structure=" + ",".join(view_signatures)
self._structure_str = hashlib.md5(structure_raw.encode('utf-8')).hexdigest()
return self._structure_str
@property
def foreground_page(self) -> Optional[str]:
"""返回当前页面 URL 作为页面标识"""
return self._url
@property
def search_content(self) -> str:
"""获取用于搜索的文本内容"""
texts = []
for view in self._views:
text = view.get('text', '')
if text:
texts.append(text)
if texts:
return ' '.join(texts)
return self._window_title
def _generate_view_strs(self):
"""为每个 DOM 控件生成稳定标识。"""
for idx, view in enumerate(self._views):
bounds = view.get('bounds', [[0, 0], [0, 0]])
x1, y1 = bounds[0]
x2, y2 = bounds[1]
class_name = view.get('class_name', 'dom')
resource_id = view.get('resource_id', '')
text = (view.get('text') or view.get('content_description') or '')[:30]
view['temp_id'] = idx
if 'children' not in view or not isinstance(view['children'], list):
view['children'] = []
if 'parent' not in view:
view['parent'] = -1
if 'view_str' not in view or not view['view_str']:
view['view_str'] = f"web_{idx}_{class_name}_{resource_id}_{x1}_{y1}_{x2}_{y2}_{text}"
@staticmethod
def _get_view_signature(view_dict: Dict[str, Any]) -> str:
"""获取带文本内容的视图签名。"""
if 'signature' in view_dict:
return view_dict['signature']
bounds = view_dict.get('bounds', [[0, 0], [0, 0]])
text = view_dict.get('text', '')
class_name = view_dict.get('class_name', 'unknown')
clickable = view_dict.get('clickable', False)
editable = view_dict.get('editable', False)
scrollable = view_dict.get('scrollable', False)
signature = f"{class_name}:{bounds}:{text}:{clickable}:{editable}:{scrollable}"
view_dict['signature'] = signature
return signature
@staticmethod
def _get_content_free_view_signature(view_dict: Dict[str, Any]) -> str:
"""获取忽略文本内容的视图签名。"""
bounds = view_dict.get('bounds', [[0, 0], [0, 0]])
class_name = view_dict.get('class_name', 'unknown')
clickable = view_dict.get('clickable', False)
editable = view_dict.get('editable', False)
scrollable = view_dict.get('scrollable', False)
return f"{class_name}:{bounds}:{clickable}:{editable}:{scrollable}"
# ==================== 输入事件 ====================
def get_possible_input(self) -> List:
"""获取当前状态可能的输入事件列表"""
from .web_input_event import (
WebTouchEvent, WebScrollEvent, WebKeyEvent, WebSetTextEvent
)
if self._possible_events:
return [] + self._possible_events
possible_events = []
enabled_view_ids = []
touch_exclude_view_ids = set()
for view in self._views:
if view.get('enabled', True) and view.get('visible', True):
enabled_view_ids.append(view['temp_id'])
for view_id in enabled_view_ids:
view = self._views[view_id]
if view.get('clickable', False):
possible_events.append(WebTouchEvent(view=view))
touch_exclude_view_ids.add(view_id)
touch_exclude_view_ids.update(view.get('children', []))
# 添加滚动事件
possible_events.append(WebScrollEvent(direction='up'))
possible_events.append(WebScrollEvent(direction='down'))
for view_id in enabled_view_ids:
view = self._views[view_id]
if view.get('editable', False):
possible_events.append(WebSetTextEvent(view=view, text="test"))
touch_exclude_view_ids.add(view_id)
for view_id in enabled_view_ids:
if view_id in touch_exclude_view_ids:
continue
view = self._views[view_id]
children = view.get('children', [])
if children:
continue
possible_events.append(WebTouchEvent(view=view))
# 添加常用按键事件
possible_events.append(WebKeyEvent('ESCAPE'))
possible_events.append(WebKeyEvent('ENTER'))
self._possible_events = possible_events
return [] + possible_events
# ==================== 序列化 ====================
def to_dict(self) -> Dict[str, Any]:
"""序列化为字典"""
return {
'tag': self.tag,
'url': self._url,
'window_title': self._window_title,
'state_str': self.state_str,
'structure_str': self.structure_str,
'foreground_page': self.foreground_page,
'views': self._views,
'screenshot_path': self.screenshot_path,
'width': self.width,
'height': self.height,
}
# ==================== 应用信息 ====================
def get_app_page_depth(self) -> int:
"""
获取应用页面深度
对于 Web只要没有超出域名都视为正常应用内。
返回 0。
"""
return 0