200 lines
7.3 KiB
Python
200 lines
7.3 KiB
Python
"""
|
||
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
|