基于openclaw+RPA实现数字员工

AI 时代程序员必备技能

Codex、Claude Code、Cursor、Hermes Agent、OpenClaw等工程化实战专栏 ,讲透 AI 如何接管脏活累活

当前 Agent 框架已趋于成熟——OpenClaw、Hermes 等方案能力稳定,第三方生态(如 QClaw、WorkBuddy)也在快速涌现。但多数 Agent 仍停留在「能聊」的阶段,离「能干活」还有距离。

本文分享一套 OpenClaw + RPA 的数字员工实现思路:用 Agent 负责理解与决策,用 RPA 负责点击与操作,让 AI 真正替你完成工作。

技术栈

  • Agent:OpenClaw
  • RPA:影刀 RPA

二者可以互补:

  • OpenClaw:绑定企业微信等办公 IM,承担对话入口与任务理解
  • 影刀 RPA:负责界面点击、表单填写、流程自动化等「手」上的操作

组合起来,就是「OpenClaw 想、RPA 做」的数字员工模式。

安装与绑定说明

影刀 RPA、OpenClaw 的安装配置,以及与企业微信等办公软件的绑定步骤,网上已有大量教程,本文不再赘述。

影刀 CLI 与 OpenClaw 的对接思路

影刀在最新版本中已开放 CLI,但目前 仅企业版可用,个人版尚未上线。

企业微信 / 用户
      ↓
  OpenClaw Agent(理解任务、编排)
      ↓ WebSocket / CLI
  yingdao_openclaw_send.py  ←→  影刀 RPA 流程
      ↓
  openYingDaoRPA.py / openRPA.py  (拉起 RPA 客户端)
      ↓
  yingdao_flow_python.py  (UI 自动化操作 ERP)
      ↓
  yingdao_extract_sd.py / _win_ocr.ps1  (OCR 识别结果)

在没有 CLI 的前提下,仍可通过脚本让 OpenClaw 拉起影刀 RPA,并结合 OCR 识别 实现模拟点击,从而完成自动化操作。示例代码如下:

根据自己的安装地址修改,默认为库存查询,使用方式为:告诉open claw 如果我需要执行rpa任务,则open claw去执行:python C:\Users\xxxx\.openclaw\workspace\skills\yingdao-rpa\scripts\openRPA.py  我这边是做成了skill。也可以直接执行下面代码拉起RPA。

import io
import os
import subprocess
import sys
import time
from pathlib import Path
from typing import Optional

from pywinauto import Application, Desktop
from pywinauto.findwindows import ElementNotFoundError
from pywinauto.mouse import click as mouse_click

FLOW_NAME = "库存查询"


def _ensure_stdio_safe() -> None:
    """技能/OpenClaw 常在 GBK 控制台运行:强制 stdout/stderr 用 utf-8 + replace,避免 print  Unicode 崩溃。"""
    for name in ("stdout", "stderr"):
        stream = getattr(sys, name, None)
        if stream is None:
            continue
        try:
            if hasattr(stream, "reconfigure"):
                stream.reconfigure(encoding="utf-8", errors="replace")
                continue
        except Exception:
            pass
        try:
            buf = getattr(stream, "buffer", None)
            if buf is not None:
                setattr(
                    sys,
                    name,
                    io.TextIOWrapper(
                        buf,
                        encoding="utf-8",
                        errors="replace",
                        line_buffering=True,
                    ),
                )
        except Exception:
            pass


def _flush_stdio() -> None:
    for stream in (sys.stdout, sys.stderr):
        try:
            stream.flush()
        except Exception:
            pass


def _safe_print(*parts: object) -> None:
    try:
        msg = " ".join(_safe_console_str(p) for p in parts)
        print(msg)
    except Exception:
        print("[输出已省略:控制台编码异常]", file=sys.stderr)
    _flush_stdio()


def _resolve_flow_name(flow_name: Optional[str]) -> str:
    """优先:参数 > 环境变量 FLOW_NAME > 模块默认常量。"""
    if flow_name is not None and str(flow_name).strip():
        return str(flow_name).strip()
    env = os.environ.get("FLOW_NAME", "").strip()
    if env:
        return env
    return FLOW_NAME

# 在流程名上右键后,用相对坐标点击右键菜单里的「运行」(菜单在下方,Y 为正向下)
# 若点偏,只改这两个数即可
RUN_MENU_OFFSET_X = 72
RUN_MENU_OFFSET_Y = 24


def _env_bool(name: str) -> bool:
    return os.environ.get(name, "").strip().lower() in ("1", "true", "yes", "on")


def _should_skip_running_check() -> bool:
    """默认跳过:影刀常驻后台,不再重复「是否在运行 / 是否启动」。"""
    if _env_bool("YINGDAO_CHECK_RUNNING"):
        return False
    if os.environ.get("YINGDAO_SKIP_RUNNING_CHECK", "").strip().lower() in (
        "0",
        "false",
        "no",
        "off",
    ):
        return False
    return True


def _apply_speed_profile() -> None:
    """YINGDAO_FAST=1 或 --fast:缩短 exists/激活/右键后等待(不强制关掉 UIA,避免少根 HWND 扫不到)。"""
    if not _env_bool("YINGDAO_FAST"):
        return
    os.environ.setdefault("YINGDAO_WRAP_EXISTS_TIMEOUT", "0.22")
    os.environ.setdefault("YINGDAO_AFTER_FOCUS_SEC", "0.12")
    os.environ.setdefault("YINGDAO_AFTER_RCLICK_SEC", "0.22")


def _wrap_exists_timeout() -> float:
    try:
        return float(os.environ.get("YINGDAO_WRAP_EXISTS_TIMEOUT", "0.2"))
    except ValueError:
        return 0.2


def _after_focus_sleep() -> float:
    try:
        return float(os.environ.get("YINGDAO_AFTER_FOCUS_SEC", "0.18"))
    except ValueError:
        return 0.18


def _after_rclick_sleep() -> float:
    try:
        return float(os.environ.get("YINGDAO_AFTER_RCLICK_SEC", "0.28"))
    except ValueError:
        return 0.28

def candidate_exes() -> list[Path]:
    local = os.environ.get("LOCALAPPDATA", "")
    prog = os.environ.get("PROGRAMFILES", "")
    prog86 = os.environ.get("PROGRAMFILES(X86)", "")
    userprofile = Path(os.environ.get("USERPROFILE", str(Path.home())))
    env_exe = os.environ.get("YINGDAO_EXE", "").strip()

    return [
        Path(env_exe) if env_exe else Path(),
        Path(r"C:\Users\user_ff\ShadowBot\ShadowBot.exe"),
        Path(r"C:\Users\T490S\ShadowBot\ShadowBot.exe"),
        userprofile / "ShadowBot" / "ShadowBot.exe",
        userprofile / "AppData" / "Local" / "ShadowBot" / "ShadowBot.exe",
        Path(local) / "ShadowBot" / "ShadowBot.exe",
        Path(prog) / "ShadowBot" / "ShadowBot.exe",
        Path(prog86) / "ShadowBot" / "ShadowBot.exe",
    ]


def _safe_console_str(text: object) -> str:
    """GBK 等控制台打印含零宽字符时会崩;去掉后再编码输出。"""
    s = str(text or "")
    for ch in ("\u200b", "\u200c", "\u200d", "\ufeff"):
        s = s.replace(ch, "")
    enc = getattr(sys.stdout, "encoding", None) or "utf-8"
    try:
        return s.encode(enc, errors="replace").decode(enc, errors="replace")
    except Exception:
        return repr(s)


def _yingdao_title_hwnds() -> list[int]:
    """标题含「影刀」的可见顶层 HWND(Win32 枚举,轻量)。"""
    hits: list[int] = []
    try:
        import ctypes
        from ctypes import wintypes

        user32 = ctypes.windll.user32

        @ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HWND, wintypes.LPARAM)
        def _enum(hwnd, _lparam):
            if not user32.IsWindowVisible(hwnd):
                return True
            n = user32.GetWindowTextLengthW(hwnd)
            if n <= 0:
                return True
            buf = ctypes.create_unicode_buffer(n + 2)
            user32.GetWindowTextW(hwnd, buf, n + 2)
            t = buf.value.strip()
            if "影刀" in t and "登录" not in t:
                hits.append(int(hwnd))
            return True

        user32.EnumWindows(_enum, 0)
    except Exception:
        pass
    return hits


def _yingdao_process_window_hwnds() -> list[int]:
    """ShadowBot 进程内标题含影刀的窗口(补充 EnumWindows 漏掉的)。"""
    out: list[int] = []
    try:
        exe = resolve_yingdao_exe()
    except FileNotFoundError:
        return out
    # Win32 连接通常更快;若已枚举到标题匹配的 HWND 则不再连 UIA(省一次 connect)
    try:
        cto = int(float(os.environ.get("YINGDAO_CONNECT_TIMEOUT", "3")))
    except ValueError:
        cto = 3
    cto = max(1, min(cto, 30))
    for backend in ("win32", "uia"):
        try:
            app = Application(backend=backend).connect(path=str(exe), timeout=cto)
            for w in app.windows():
                try:
                    title = (w.window_text() or "").strip()
                    h = int(w.handle)
                except Exception:
                    continue
                if "影刀" in title and "登录" not in title:
                    out.append(h)
        except Exception:
            continue
        if out:
            break
    return list(dict.fromkeys(out))


def _enum_child_hwnds(parent_hwnd: int, *, max_children: int = 128) -> list[int]:
    """一层子窗口句柄(客户区、分层窗等常在子 HWND 上)。"""
    found: list[int] = []
    try:
        import ctypes
        from ctypes import wintypes

        user32 = ctypes.windll.user32

        @ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HWND, wintypes.LPARAM)
        def cb(hwnd, _lparam):
            if len(found) >= max_children:
                return False
            found.append(int(hwnd))
            return True

        user32.EnumChildWindows(wintypes.HWND(parent_hwnd), cb, 0)
    except Exception:
        pass
    return found


def _expand_hwnds_for_scan(base_hwnds: list[int]) -> list[int]:
    """子 HWND 优先(列表多在客户区子窗),最后再扫主壳,减少无谓的全树遍历。"""
    cap = int(os.environ.get("YINGDAO_SCAN_HWND_CAP", "256"))
    per_parent = int(os.environ.get("YINGDAO_CHILD_HWND_PER_PARENT", "96"))
    out: list[int] = []
    seen: set[int] = set()
    for h in base_hwnds:
        children = _enum_child_hwnds(h, max_children=per_parent)
        for ch in children:
            if ch not in seen:
                seen.add(ch)
                out.append(ch)
            if len(out) >= cap:
                return out
        if h not in seen:
            seen.add(h)
            out.append(h)
        if len(out) >= cap:
            break
    return out


def _wrap_scan_roots(hwnds: list[int]) -> list:
    """
    同一 HWND 用 Win32 + UIA 各挂一份:Electron/WPF 壳上 UIA 常为空,Win32 仍有子控件。
    顺序默认 win32 优先(可用环境变量 YINGDAO_UI_BACKENDS=win32,uia)。
    """
    raw = os.environ.get("YINGDAO_UI_BACKENDS", "win32,uia")
    backends = [b.strip() for b in raw.split(",") if b.strip()]
    if not backends:
        backends = ["win32", "uia"]
    roots: list = []
    seen_pair: set[tuple[int, str]] = set()
    for h in hwnds:
        for backend in backends:
            key = (h, backend)
            if key in seen_pair:
                continue
            try:
                w = Desktop(backend=backend).window(handle=h)
                if w.exists(timeout=_wrap_exists_timeout()):
                    seen_pair.add(key)
                    roots.append(w)
            except Exception:
                continue
    return roots


def _resolve_yingdao_roots() -> list:
    """候选 HWND = 标题匹配(优先)+ 进程内窗口(兜底),展开一级子窗;再按 Win32/UIA 包装为扫描根。"""
    base = _yingdao_title_hwnds()
    if not base:
        base = _yingdao_process_window_hwnds()
    else:
        base = list(dict.fromkeys(base))
    if not base:
        return []
    expanded = _expand_hwnds_for_scan(base)
    return _wrap_scan_roots(expanded)


def _existing_yingdao_window(*, verbose: bool = True):
    """若影刀已打开,返回用于展示的主窗口句柄包装;否则 None。"""
    if verbose:
        _safe_print("正在查找影刀主窗口…")
    order = list(dict.fromkeys(_yingdao_title_hwnds() + _yingdao_process_window_hwnds()))
    for h in order:
        for backend in ("win32", "uia"):
            try:
                w = Desktop(backend=backend).window(handle=h)
                if w.exists(timeout=_wrap_exists_timeout()):
                    return w
            except Exception:
                continue
    return None


def resolve_yingdao_exe() -> Path:
    for p in candidate_exes():
        if p and p.is_file():
            return p.resolve()
    raise FileNotFoundError("未找到影刀主程序 ShadowBot.exe")


def launch_yingdao() -> Path:
    exe = resolve_yingdao_exe()
    subprocess.Popen([str(exe)], cwd=str(exe.parent), shell=False)
    return exe


def wait_yingdao_main_window(timeout: int = 40):
    start = time.time()
    first_scan = True
    while time.time() - start < timeout:
        w = _existing_yingdao_window(verbose=first_scan)
        first_scan = False
        if w is not None:
            return w
        left = max(0, int(timeout - (time.time() - start)))
        _safe_print(f"尚未发现影刀主窗口,约 {left}s 内再扫…")
        time.sleep(2)
    raise TimeoutError("等待影刀主窗口超时")


def _window_wait_seconds() -> int:
    try:
        return int(os.environ.get("YINGDAO_WINDOW_TIMEOUT", "90"))
    except ValueError:
        return 90


def _norm(s: str) -> str:
    return "".join(str(s or "").split())


def _root_debug_label(root) -> str:
    try:
        h = int(root.handle)
    except Exception:
        h = -1
    try:
        bk = root.backend.name  # type: ignore[attr-defined]
    except Exception:
        bk = "?"
    try:
        tx = (root.window_text() or "")[:40]
    except Exception:
        tx = ""
    return f"hwnd={h} {bk} 「{_safe_console_str(tx)}」"


# 流程列表控件多时加大上限;仍可能被虚拟列表截断(见下方说明)
_FLOW_SCAN_MAX = int(os.environ.get("YINGDAO_FLOW_SCAN_MAX", "25000"))


def _find_flow_in_roots(roots: list, flow_name: str):
    """在多个候选根中找包含流程名的控件;规范化后全等则立即返回,避免扫完整棵树。"""
    _safe_print(
        f"正在流程列表中定位控件(共 {len(roots)} 个候选根,命中即停)…"
    )
    t0 = time.perf_counter()
    best = None
    best_nt_len = 10**9
    needle = _norm(flow_name)
    needle_len = len(needle)
    for root in roots:
        try:
            it = root.descendants()
        except Exception:
            continue
        for i, ctrl in enumerate(it):
            if i > _FLOW_SCAN_MAX:
                break
            try:
                text = (ctrl.window_text() or "").strip()
            except Exception:
                continue
            if not text:
                continue
            nt = _norm(text)
            if needle not in nt:
                continue
            nlen = len(nt)
            # 全名一致:不可能更短,立刻返回(省掉后续上万次枚举)
            if nlen == needle_len:
                if _env_bool("YINGDAO_TIMING"):
                    _safe_print(
                        f"[timing] 流程扫描 {time.perf_counter() - t0:.2f}s (精确命中)"
                    )
                return ctrl
            if nlen < best_nt_len:
                best_nt_len = nlen
                best = ctrl
    if _env_bool("YINGDAO_TIMING"):
        _safe_print(f"[timing] 流程扫描 {time.perf_counter() - t0:.2f}s")
    return best


def debug_flow_candidates(roots: list, flow_name: str, *, max_lines: int = 120) -> None:
    """找不到流程时调用:打印名称中含关键字片段的控件(排查虚拟列表/名称不一致)。"""
    needle = _norm(flow_name)
    hint = needle[:2] if len(needle) >= 2 else needle
    _safe_print(
        f"[调试] 扫描 {len(roots)} 个候选根的子控件(每个最多 {_FLOW_SCAN_MAX} 个),"
        f"查找与「{flow_name}」相关的文案…"
    )
    hits: list[str] = []
    total_scanned = 0
    for ri, root in enumerate(roots):
        try:
            it = root.descendants()
        except Exception as e:
            _safe_print(f"[调试] 候选根#{ri + 1} 无法枚举子控件:", e)
            continue
        scanned = 0
        for i, ctrl in enumerate(it):
            if i > _FLOW_SCAN_MAX:
                break
            scanned = i + 1
            try:
                text = (ctrl.window_text() or "").strip()
            except Exception:
                continue
            if not text:
                continue
            nt = _norm(text)
            if needle and needle in nt:
                hits.append(text)
            elif hint and hint in nt:
                hits.append(text)
        total_scanned += scanned
        _safe_print(
            f"[调试] 候选根#{ri + 1} {_root_debug_label(root)} 扫描控件数: {scanned}"
        )
    _safe_print(f"[调试] 共扫描控件数: {total_scanned}")
    if hits:
        uniq = list(dict.fromkeys(hits))
        _safe_print("[调试] 名称中包含流程关键字或前两字的控件(截断显示):")
        for i, t in enumerate(uniq):
            if i >= max_lines:
                _safe_print(f"… 其余省略(共 {len(uniq)} 条去重)")
                break
            _safe_print(f"  - {_safe_console_str(t[:200])}")
    else:
        _safe_print(
            "[调试] 未命中任何文案。常见原因:"
            "① 流程在折叠分组里或列表未滚动,虚拟列表不暴露未显示行;"
            "② 界面名称与参数不完全一致(空格、全角符号);"
            "③ 当前焦点不在「流程」页或左侧列表。"
        )
        if total_scanned == 0:
            _safe_print(
                "[调试] 若所有候选根扫描数均为 0:界面可能为自绘/Chromium 且未暴露 Win32 子控件文案,"
                "脚本无法通过控件名启动流程;请改用影刀自带命令行/HTTP 调度或使用图像/OCR 定位。"
            )


def _prepare_main_window(window, *, verbose: bool = True) -> None:
    """任务栏有图标但主窗口常最小化:尽量还原并抢前台,便于右键坐标命中。"""
    if verbose:
        _safe_print("尝试还原/激活影刀主窗口…")
    for fn in ("restore", "maximize"):
        try:
            m = getattr(window, fn, None)
            if callable(m):
                m()
        except Exception:
            pass
    try:
        window.set_focus()
    except Exception:
        pass
    time.sleep(_after_focus_sleep())


def run_flow(roots: list, flow_name: str) -> None:
    # 同一 hwnd 的 win32/uia 两份包装只还原一次
    prepared: set[int] = set()
    first_verbose = True
    for w in roots:
        try:
            hw = int(w.handle)
        except Exception:
            hw = id(w)
        if hw in prepared:
            continue
        _prepare_main_window(w, verbose=first_verbose)
        first_verbose = False
        prepared.add(hw)

    target = _find_flow_in_roots(roots, flow_name)
    if not target:
        raise ElementNotFoundError(f"未在界面中找到流程: {flow_name}")

    rect = target.rectangle()
    cx = int((rect.left + rect.right) / 2)
    cy = int((rect.top + rect.bottom) / 2)

    mouse_click(button="right", coords=(cx, cy))
    time.sleep(_after_rclick_sleep())
    mouse_click(
        button="left",
        coords=(cx + RUN_MENU_OFFSET_X, cy + RUN_MENU_OFFSET_Y),
    )


def main(flow_name: Optional[str] = None, *, debug_only: bool = False) -> None:
    _ensure_stdio_safe()
    _apply_speed_profile()
    name = _resolve_flow_name(flow_name)
    attach_only = os.environ.get("YINGDAO_ATTACH_ONLY", "").strip() in (
        "1",
        "true",
        "yes",
        "on",
    )

    if _should_skip_running_check():
        _safe_print("假定影刀已在后台,跳过运行检测,直接定位流程列表…")
    else:
        window = _existing_yingdao_window(verbose=True)
        if window is not None:
            _safe_print("已检测到影刀已运行,跳过启动新进程:", window.window_text())
        elif attach_only:
            raise RuntimeError(
                "未找到已打开的影刀主窗口,且已设置 YINGDAO_ATTACH_ONLY=1(不启动新实例)。请先手动打开影刀。"
            )
        else:
            exe = launch_yingdao()
            _safe_print("已启动影刀:", exe)
            time.sleep(3)
            wait_yingdao_main_window(timeout=_window_wait_seconds())

    roots = _resolve_yingdao_roots()
    if not roots:
        raise RuntimeError(
            "无法枚举影刀顶层窗口(ShadowBot)。请确认影刀已登录主界面后重试。"
        )
    try:
        wt0 = roots[0].window_text() or ""
    except Exception:
        wt0 = ""
    if not str(wt0).strip():
        wt0 = "(无标题子窗)"
    _safe_print(
        "找到窗口:",
        wt0,
        f"(候选扫描根 {len(roots)} 个;设 YINGDAO_FAST=1 或 --fast 可再加速)",
    )
    if debug_only:
        debug_flow_candidates(roots, name)
        return
    try:
        run_flow(roots, name)
    except ElementNotFoundError:
        _safe_print("未找到流程,以下为调试扫描(也可单独运行: python dianJi.py --debug 流程名)…")
        debug_flow_candidates(roots, name)
        raise
    _safe_print("已触发运行:", name)


if __name__ == "__main__":
    _ensure_stdio_safe()
    import argparse

    parser = argparse.ArgumentParser(description="启动影刀并在指定流程上右键运行")
    parser.add_argument(
        "flow_name",
        nargs="?",
        default=None,
        help="流程名称(与界面列表一致);可省略,此时使用环境变量 FLOW_NAME 或脚本内默认",
    )
    parser.add_argument(
        "--debug",
        action="store_true",
        help="只扫描 UI 文案并打印候选,不右键运行(排查找不到流程)",
    )
    parser.add_argument(
        "--fast",
        action="store_true",
        help="快速模式:更短 exists/激活/右键等待(仍扫 Win32+UIA;想极限省时可设 YINGDAO_UI_BACKENDS=win32 自行承担漏检)",
    )
    parser.add_argument(
        "--check-running",
        action="store_true",
        help="启用「查找主窗口/未运行则启动」检测(默认已跳过,影刀常驻后台时不必开)",
    )
    parser.add_argument(
        "--skip-running-check",
        action="store_true",
        help="(已默认开启) 跳过运行检测;保留此参数仅为兼容旧调用",
    )
    args = parser.parse_args()
    if args.fast:
        os.environ["YINGDAO_FAST"] = "1"
    if args.check_running:
        os.environ["YINGDAO_CHECK_RUNNING"] = "1"
    main(args.flow_name, debug_only=args.debug)

RPA 应用执行完毕后,桌面右下角会弹出执行完成提示(默认匹配「运行成功」)。

弹窗不关闭时,后续流程无法再次触发,因此应在「运行应用」之后追加一步关闭弹窗:

from __future__ import annotations
import argparse
import ctypes
import time
from ctypes import wintypes
from typing import Optional

from pywinauto import Application
from pywinauto.mouse import click as mouse_click, move as mouse_move

user32 = ctypes.windll.user32
SM_CXSCREEN, SM_CYSCREEN = 0, 1


class _RECT(ctypes.Structure):
    _fields_ = (
        ("left", wintypes.LONG),
        ("top", wintypes.LONG),
        ("right", wintypes.LONG),
        ("bottom", wintypes.LONG),
    )


def _screen() -> tuple[int, int]:
    return int(user32.GetSystemMetrics(SM_CXSCREEN)), int(user32.GetSystemMetrics(SM_CYSCREEN))


def _corner_toast_ok(r: _RECT, sw: int, sh: int) -> bool:
    w, h = r.right - r.left, r.bottom - r.top
    if w < 80 or h < 24 or w > 1000 or h > 500:
        return False
    cx = (r.left + r.right) // 2
    cy = (r.top + r.bottom) // 2
    return cx >= int(sw * 0.48) and cy >= int(sh * 0.42)


def _enum_top_level_hwnds() -> list[int]:
    out: list[int] = []

    @ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
    def cb(hwnd, _lp):
        if user32.IsWindowVisible(hwnd) and user32.GetParent(hwnd) == 0:
            out.append(int(hwnd))
        return True

    user32.EnumWindows(cb, 0)
    return out


def _rect(hwnd: int) -> Optional[_RECT]:
    r = _RECT()
    if not user32.GetWindowRect(hwnd, ctypes.byref(r)):
        return None
    return r


def _uia_window(hwnd: int):
    return Application(backend="uia").connect(handle=hwnd).window(handle=hwnd)


def _find(keyword: str) -> Optional[object]:
    sw, sh = _screen()
    for hwnd in _enum_top_level_hwnds():
        r = _rect(hwnd)
        if r is None or not _corner_toast_ok(r, sw, sh):
            continue
        try:
            top = _uia_window(hwnd)
        except Exception:
            continue
        if keyword in (top.window_text() or ""):
            return top
        for i, c in enumerate(top.descendants(control_type="Text")):
            if i >= 400:
                break
            if keyword in (c.window_text() or ""):
                try:
                    return c.top_level_parent()
                except Exception:
                    return top
    return None


def _click(x: int, y: int) -> None:
    print(f"点击 ({x}, {y})")
    mouse_move(coords=(x, y))
    time.sleep(0.12)
    mouse_click(button="left", coords=(x, y))


def _dismiss(w: object, keyword: str) -> None:
    try:
        w.set_focus()
    except Exception:
        pass
    try:
        b = w.child_window(title="关闭", control_type="Button")
        if b.exists(timeout=0.35):
            r = b.rectangle()
            _click((r.left + r.right) // 2, (r.top + r.bottom) // 2)
            return
    except Exception:
        pass
    try:
        for i, c in enumerate(w.descendants(control_type="Text")):
            if i >= 400:
                break
            if keyword in (c.window_text() or ""):
                r = c.rectangle()
                _click(r.left + 315, r.top - 68)
                return
    except Exception:
        pass
    rect = w.rectangle()
    _click(rect.right + 1, rect.top - 6)


def run(keyword: str, timeout: float, interval: float) -> bool:
    deadline = time.time() + timeout
    while time.time() < deadline:
        w = _find(keyword)
        if w is not None:
            _dismiss(w, keyword)
            return True
        time.sleep(interval)
    return False


def main() -> None:
    p = argparse.ArgumentParser()
    p.add_argument("--keyword", default="运行成功")
    p.add_argument("--timeout", type=float, default=12.0)
    p.add_argument("--interval", type=float, default=0.25)
    args = p.parse_args()
    if run(args.keyword, args.timeout, args.interval):
        print("已处理。")
    else:
        print("未在右下角附近找到该提示。")


if __name__ == "__main__":
    main()

到这里,从对话到执行的完整闭环已经打通。后续若有新需求,只需录制对应的 RPA 流程并接入 Agent,即可实现:你说一句话,AI 帮你把活干完。

AI 时代程序员必备技能

Codex、Claude Code、Cursor、Hermes Agent、OpenClaw等工程化实战专栏 ,讲透 AI 如何接管脏活累活

内容概要:本文介绍了一个关于三相桥式全控整流及有源逆变电路的实验仿真模型,重点研究三相整流器与逆变器在Simulink环境下的建模与仿真技术。内容涵盖电力电子变换器的工作原理、控制策略设计、系统动态响应分析,并进一步扩展至10kV配电网中不同中性点接地方式(中性点不接地、经小电阻接地、经消弧线圈接地)下的单相、两相短路接地及相间短路故障的仿真研究,全面呈现了电力系统典型故障的暂态特性。此外,文档还整合了丰富的科研资源,涵盖电力系统优化、新能源并网、故障诊断、微电网调度等多个前沿方向,充分体现了Matlab/Simulink在电气工程仿真中的核心地位和广泛应用价值。; 适合人群:电气工程、自动化、电力电子等相关专业的高校学生、科研人员及工程技术人员,具备一定的电路理论基础和仿真软件操作经验者更佳。; 使用场景及目标:①用于教学实验中帮助理解三相整流与逆变电路的工作机制;②支撑科研项目中对电力系统故障特性的建模与分析;③作为开发新型控制算法(如PWM控制、低电压穿越等)的仿真验证平台;④辅助完成毕业设计、课题研究或工程方案评估; 阅读建议:此资源以Simulink仿真实现为核心,强调理论与实践结合,建议读者在学习过程中同步搭建模型,动手调试参数,深入理解各模块功能与系统整体行为,同时可参考文中提供的完整资源链接拓展研究视野。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值