# MIT License # Copyright (c) 2024 """执行层:根据 DSL 进行 UI 自动化。""" import re import time from dataclasses import dataclass from typing import Any, Dict, List, Optional import uiautomation as auto # type: ignore from .schema import DSLSpec @dataclass class ExecContext: """执行上下文。""" allow_title: str dry_run: bool = False def _match_window(allow_title: str) -> Optional[auto.Control]: """仅在窗口标题匹配白名单时返回前台窗口。""" ctrl = auto.GetForegroundControl() if ctrl is None: return None if ctrl.Name is None: return None if not re.search(allow_title, ctrl.Name): return None return ctrl def _find_control(root: auto.Control, locator: Dict[str, Any], timeout: float) -> Optional[auto.Control]: """根据 locator 在 root 下查找控件。""" start = time.time() while time.time() - start <= timeout: try: conds = [] if "AutomationId" in locator: conds.append(auto.Control.AutomationId == locator["AutomationId"]) if "Name" in locator: conds.append(auto.Control.Name == locator["Name"]) if "ClassName" in locator: conds.append(auto.Control.ClassName == locator["ClassName"]) if "ControlType" in locator: conds.append(auto.Control.ControlTypeName == locator["ControlType"]) if conds: ctrl = root.Control(searchDepth=4, condition=auto.AndCondition(*conds)) else: ctrl = root if ctrl: return ctrl except Exception: pass time.sleep(0.5) return None def _do_action(ctrl: auto.Control, step: Dict[str, Any], dry_run: bool) -> None: """执行单步动作。""" action = step.get("action") text = step.get("text", "") if dry_run: print(f"[dry-run] {action} -> target={step.get('target')} text={text}") return if action == "click": ctrl.Click() elif action == "type": ctrl.SetFocus() auto.SendKeys(text) elif action == "set_value": try: ctrl.GetValuePattern().SetValue(text) except Exception: ctrl.SendKeys(text) elif action == "assert_exists": assert ctrl is not None, "控件未找到" elif action == "wait_for": # wait_for 仅等待存在 time.sleep(float(step.get("waits", {}).get("appear", 1.0))) def execute_spec(spec: DSLSpec, ctx: ExecContext) -> None: """执行完整的 DSL。""" root = _match_window(ctx.allow_title) if root is None: raise RuntimeError(f"前台窗口标题未匹配白名单: {ctx.allow_title}") def run_steps(steps: List[Any]) -> None: for step in steps: if "for_each" in step: # 简单遍历列表参数 iterable = spec.params.get(step["for_each"], []) for item in iterable: run_steps(step.get("steps", [])) elif "if_condition" in step: cond = step["if_condition"] if spec.params.get(cond): run_steps(step.get("steps", [])) else: run_steps(step.get("else_steps", [])) else: target = step.get("target", {}) timeout = float(step.get("waits", {}).get("appear", spec.waits.get("appear", 5.0))) retry = step.get("retry_policy", spec.retry_policy) attempts = int(retry.get("max_attempts", 1)) interval = float(retry.get("interval", 1.0)) last_err: Optional[Exception] = None for _ in range(attempts): ctrl = _find_control(root, target, timeout) try: if ctrl is None: raise RuntimeError("控件未找到") _do_action(ctrl, step, ctx.dry_run) last_err = None break except Exception as e: # noqa: BLE001 last_err = e time.sleep(interval) if last_err: raise last_err run_steps(spec.steps)