""" 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