"""作用域内 UIA 唯一定位、真实点击和逐字输入。""" from __future__ import annotations import random from typing import Any, Iterator from service.desktop_session import DesktopRpaError, WindowCandidate, activate_window from util.selectors import ElementSpec def require_uiautomation(): try: import uiautomation as auto except ImportError as exc: raise DesktopRpaError("UIA_UNAVAILABLE", "缺少 uiautomation 依赖") from exc return auto def root_from_window(window: WindowCandidate): auto = require_uiautomation() return auto.ControlFromHandle(window.hwnd) def control_value(control: Any) -> str: try: auto = require_uiautomation() raw = control.GetPropertyValue(auto.PropertyId.ValueValueProperty) return str(raw or "").strip() except Exception: return "" def _walk(root: Any, *, depth: int = 0, max_depth: int = 25) -> Iterator[Any]: if depth > max_depth: return yield root if depth == max_depth: return try: children = root.GetChildren() except Exception: children = [] for child in children: yield from _walk(child, depth=depth + 1, max_depth=max_depth) def _child_has_value(control: Any, child_name: str, expected_value: str, *, max_depth: int = 10) -> bool: for node in _walk(control, max_depth=max_depth): if (getattr(node, "Name", None) or "") != child_name: continue if control_value(node) == expected_value: return True return False def _matches(control: Any, spec: ElementSpec) -> bool: if spec.name is not None and (getattr(control, "Name", None) or "") != spec.name: return False if spec.value is not None and not spec.child_value_name: if control_value(control) != spec.value: return False if spec.automation_id is not None and ( getattr(control, "AutomationId", None) or "" ) != spec.automation_id: return False if spec.control_type is not None and ( (getattr(control, "ControlTypeName", None) or "").removesuffix("Control") != spec.control_type.removesuffix("Control") ): return False if spec.class_name is not None and ( getattr(control, "ClassName", None) or "" ) != spec.class_name: return False if spec.child_value_name and spec.value: if not _child_has_value(control, spec.child_value_name, spec.value): return False return True def locate_unique(root: Any, spec: ElementSpec, *, max_depth: int = 25): search_root = root if spec.ancestor_name: ancestor_spec = ElementSpec( logical_name=f"{spec.logical_name}.ancestor", name=spec.ancestor_name, ) search_root = locate_unique(root, ancestor_spec, max_depth=max_depth) matched = [ control for control in _walk(search_root, max_depth=max_depth) if _matches(control, spec) ] if not matched: raise DesktopRpaError("ELEMENT_NOT_FOUND", f"{spec.logical_name} 未找到") if len(matched) != spec.expected_count: raise DesktopRpaError( "ELEMENT_AMBIGUOUS", f"{spec.logical_name} 期望 {spec.expected_count} 个,实际 {len(matched)} 个", ) return matched[0] def click_control( control: Any, *, window: WindowCandidate, maximize: bool = False, ) -> None: activate_window(window, maximize=maximize) rect = control.BoundingRectangle if rect.width() <= 0 or rect.height() <= 0 or rect.left < -10000: raise DesktopRpaError("ELEMENT_NOT_READY", "目标元素不可见或边界矩形无效") control.Click( x=random.randint(-2, 2), y=random.randint(-2, 2), simulateMove=True, ) def type_text(control: Any, text: str, *, window: WindowCandidate) -> None: auto = require_uiautomation() click_control(control, window=window, maximize=False) auto.SendKeys("{Ctrl}a", waitTime=0.05) auto.SendKeys("{Delete}", waitTime=0.05) for char in text: auto.SendKeys(char, waitTime=random.uniform(0.04, 0.12))