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

208 lines
7.3 KiB
Python
Raw 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.

"""
Windows Device State Implementation
Concrete implementation of AbstractDeviceState for Windows desktop applications.
Uses CV mode for UI element detection.
"""
import hashlib
from typing import Optional, Dict, Any, List
from ...core.abstract_device_state import AbstractDeviceState
from ...core.abstract_input_event import EventType
class WindowsDeviceState(AbstractDeviceState):
"""
Windows 设备状态的具体实现CV 模式)
使用 OmniParser 进行 UI 元素检测,无需 UIAutomation。
"""
def __init__(self, device, cv_views: List[Dict[str, Any]],
window_title: str = None, tag: str = None,
screenshot_path: str = None):
"""
初始化 Windows 设备状态
:param device: WindowsDevice 实例
:param cv_views: CV 检测得到的 ViewDict 列表
:param window_title: 当前窗口标题
:param tag: 状态标签
:param screenshot_path: 截图路径
"""
super().__init__(device, tag, screenshot_path)
self._cv_views = cv_views or []
self._window_title = window_title
# 生成视图字符串标识
self._generate_view_strs()
# 缓存
self._state_str = None
self._structure_str = None
# ==================== 视图信息 ====================
@property
def views(self) -> List[Dict[str, Any]]:
"""获取 CV 检测的视图列表"""
return self._cv_views
@property
def cv_views(self) -> List[Dict[str, Any]]:
"""获取 CV 视图列表(与 views 相同)"""
return self._cv_views
# ==================== 状态标识 ====================
@property
def state_str(self) -> str:
"""获取状态的唯一标识字符串"""
if self._state_str is None:
self._state_str = self._get_state_str()
return self._state_str
@property
def structure_str(self) -> str:
"""获取状态的结构标识(忽略内容)"""
if self._structure_str is None:
self._structure_str = self._get_content_free_state_str()
return self._structure_str
@property
def foreground_page(self) -> Optional[str]:
"""返回当前窗口标题作为页面标识"""
return self._window_title
@property
def search_content(self) -> str:
"""获取用于搜索的文本内容"""
texts = []
for view in self._cv_views:
text = view.get('text', '')
if text:
texts.append(text)
return ' '.join(texts)
# ==================== 私有方法 ====================
def _generate_view_strs(self):
"""为每个视图生成唯一标识符"""
for idx, view in enumerate(self._cv_views):
if 'view_str' not in view or not view['view_str']:
bounds = view.get('bounds', [[0, 0], [0, 0]])
x1, y1 = bounds[0]
x2, y2 = bounds[1]
text = view.get('text', '')[:20] # 截取前20个字符
view['view_str'] = f"win_cv_{idx}_{x1}_{y1}_{x2}_{y2}_{text}"
def _get_state_str(self) -> str:
"""生成状态唯一标识"""
state_raw = self._get_state_str_raw()
return hashlib.md5(state_raw.encode('utf-8')).hexdigest()
def _get_state_str_raw(self) -> str:
"""获取原始状态字符串"""
view_signatures = []
for view in self._cv_views:
sig = self._get_view_signature(view)
view_signatures.append(sig)
view_signatures.sort()
state_str = f"window={self._window_title}&views=" + ','.join(view_signatures)
return state_str
def _get_content_free_state_str(self) -> str:
"""获取内容无关的状态标识"""
view_signatures = []
for view in self._cv_views:
sig = self._get_content_free_view_signature(view)
view_signatures.append(sig)
view_signatures.sort()
state_str = f"window={self._window_title}&structure=" + ','.join(view_signatures)
return hashlib.md5(state_str.encode('utf-8')).hexdigest()
@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)
signature = f"{class_name}:{bounds}:{text}:{clickable}"
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)
# 只使用位置和类型,忽略文本内容
return f"{class_name}:{bounds}:{clickable}"
# ==================== 输入事件 ====================
def get_possible_input(self) -> List:
"""获取当前状态可能的输入事件列表"""
from .windows_input_event import (
WindowsTouchEvent, WindowsScrollEvent, WindowsKeyEvent
)
possible_events = []
# 为每个可点击的视图生成点击事件
for view in self._cv_views:
if view.get('clickable', False) and view.get('visible', True):
touch_event = WindowsTouchEvent(view=view)
possible_events.append(touch_event)
# 添加滚动事件
possible_events.append(WindowsScrollEvent(direction='up'))
possible_events.append(WindowsScrollEvent(direction='down'))
# 添加常用按键事件
possible_events.append(WindowsKeyEvent('ESCAPE'))
possible_events.append(WindowsKeyEvent('ENTER'))
return possible_events
# ==================== 序列化 ====================
def to_dict(self) -> Dict[str, Any]:
"""序列化为字典"""
return {
'tag': self.tag,
'window_title': self._window_title,
'state_str': self.state_str,
'structure_str': self.structure_str,
'foreground_page': self.foreground_page,
'views': self._cv_views,
'screenshot_path': self.screenshot_path,
'width': self.width,
'height': self.height,
}
# ==================== 应用信息 ====================
def get_app_page_depth(self) -> int:
"""
获取应用页面深度
Windows 应用通常没有明确的页面栈概念。
返回 0 表示应用处于正常状态(在应用内),
这样 MemoryGuidedPolicy 的记忆学习和卡住检测才能正常工作。
注意:返回 -1 会导致 Memory._memorize_state() 跳过学习,
且 input_policy.py 中的 stuck 检测也会被跳过。
"""
return 0