All checks were successful
技能自动化发布 / release (push) Successful in 15s
基于 skill-template 脚手架开发,包含: - 5 张业务表(教职工、请假政策、考勤日历、请假记录、假期余额) - 请假核算引擎(扣薪计算、审批层级判定、年假抵扣) - CLI 入口(import-employees / calculate-leave / render-html) - HTML 审批与扣薪报告生成 - 示例数据与完整测试套件(295 tests passed)
128 lines
4.1 KiB
Python
128 lines
4.1 KiB
Python
"""作用域内 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))
|