CV+键鼠仿真实现RPA的技术方案与实操

保存时间:2026/3/29 22:11:59


CV+键鼠仿真实现RPA的技术方案与实操
内容由豆包 AI 生成
这个是gemini针对我的问题的回答: 你的观察非常敏锐。从工程角度看,通过“计算机视觉(CV)+ 鼠标键盘仿真”来实现 RPA(机器人流程自动化)确实是一个非常直观且符合人类直觉的方案。 事实上,这种技术路线并不“古老”,它被称为 **“无侵入式 RPA”** 或 **“基于图像识别的 RPA”**。目前市面上其实有成熟的解决方案,但之所以你感觉它“难做”或者“不常用”,主要是因为在实际工程落地时,有几个深层挑战: --- ### 1. 为什么“看图说话”在工程上很麻烦? 虽然人类能轻松识别 Word 的菜单,但对于程序来说,GUI 的复杂性远超想象: * **分辨率与缩放(DPI)问题:** 同样的 Word 菜单,在 1080P 和 4K 屏幕下,或者系统缩放是 100% 还是 125% 时,像素特征完全不同。传统的模板匹配(Template Matching)会瞬间失效。 * **动态渲染:** 现在的 UI 经常有阴影、淡入淡出动画、透明度(Acrylic/Mica 效果)。如果 OCR 抓取时菜单正在弹出,识别率会大幅下降。 * **跨平台差异:** 同样是“文件”菜单,在 Ubuntu 的 GTK、Qt 应用和 Windows 的 WinUI 中,字体、间距、高亮颜色完全不同。 * **层级遮挡:** 弹窗、通知栏、甚至是鼠标指针本身遮挡了文字,都会导致识别失败。 ### 2. 成熟的解决方案其实一直存在 你提到的这个技术栈,其实已经有几个著名的“老牌”和“新锐”工具: * **SikuliX (最经典):** 这是一个完全基于 OpenCV 的自动化工具。它的核心逻辑就是:`click("search_button.png")`。它通过匹配屏幕上的小图来定位。 * **PyAutoGUI:** Python 里的国民级库。它支持 `locateOnScreen()`,虽然底层原理简单,但配合 OCR(如 Tesseract 或 PaddleOCR)就是你描述的方案。 * **AutoHotkey (AHK) + ImageSearch:** 脚本界的常青树,很多人用它做游戏脚本或办公自动化。 * **现代商业 RPA (UiPath/Automation Anywhere):** 它们内部其实集成了这种能力,称为 **"AI Computer Vision"**。当底层控件(如 HTML DOM 或 Windows UIA)无法读取时,它们会自动切换到“视觉识别”模式。 ### 3. 为什么主流 RPA 优先使用“控件识别”? 现在的系统(Windows, Linux/X11/Wayland, macOS)都提供了一套 **Accessibility API(辅助功能接口)**。 | 方案 | 识别方式 | 优点 | 缺点 | | --- | --- | --- | --- | | **控件识别 (Object-based)** | 读取 UI 树(如 `Button_ID: 102`) | **极快、极准**。不受分辨率、窗口位置、遮挡影响。 | 某些自绘 UI(如游戏、老旧 ERP)不支持。 | | **视觉识别 (CV-based)** | 像素匹配 + OCR | **通用性强**。只要人眼能看,它就能做。 | **慢**。受环境干扰大,鲁棒性(健壮性)较差。 | ### 4. 如果你想自己做一个小脚本 既然你是为了完成“固定重复的工作”,你的思路是完全可行的。我建议你不要从零写 OpenCV 底层,直接用现成的积木: 1. **定位:** 使用 `PyAutoGUI` 的 `locateCenterOnScreen`。你可以截取一个小按钮的图片作为模板。 2. **文字:** 如果需要识别“另存为”等文字,集成 **PaddleOCR**(目前中文识别最强的轻量级库之一)。 3. **逻辑确认:** 就像你说的,这是一个“置信度”问题。在代码里设置一个 Threshold(阈值),比如相似度 > 0.8 就执行点击。 ### 建议的技术栈推荐 如果你在 Ubuntu 环境下: * **语言:** Python * **视觉定位:** `PyAutoGUI` 或 `python-opencv` * **OCR 引擎:** `RapidOCR` (基于 PaddleOCR,安装极其简单) * **操作执行:** `xdotool` (底层调用) 或 `PyAutoGUI` (跨平台封装) **总结:** 这种方式不古老,反而因为深度学习(Deep Learning)的加入正焕发第二春。你感觉“不难做”是因为你的场景相对固定,而通用软件要解决的是“在所有人的电脑上都能跑”,那才是工程上的噩梦。
从 Gemini 的回答中可以看出,你关注的 “CV + 键鼠仿真实现 RPA” 是一个非常有价值的技术方向,我会基于这个回答,为你梳理出可落地的实操方案核心要点,帮你把这个思路转化为实际可用的代码。

一、快速上手:Ubuntu 环境下的 CV-RPA 最小实现

下面是一个完整的示例脚本,实现 “识别屏幕上的‘保存’按钮并点击” 的核心功能,你可以直接复制使用。

1. 前置依赖安装

首先在 Ubuntu 终端执行以下命令安装所需库:
bash
运行
# 安装系统级工具(用于键鼠操作)
sudo apt-get install -y xdotool scrot

# 安装Python依赖
pip install pyautogui opencv-python rapidocr-onnxruntime pillow
  • xdotool:Ubuntu 下模拟键鼠操作的底层工具
  • scrot:截图工具(PyAutoGUI 依赖)
  • rapidocr-onnxruntime:轻量级中文 OCR 库(比 Tesseract 更易安装)
  • pyautogui:跨平台键鼠控制 + 图像匹配

2. 完整代码实现

python
运行
import pyautogui
import cv2
import numpy as np
from rapidocr_onnxruntime import RapidOCR

# -------------------------- 核心配置 --------------------------
# 截图模板路径(你需要自己截取目标按钮的小图,比如save_button.png)
TEMPLATE_PATH = "save_button.png"
# 图像匹配置信度阈值(0-1,越高越严格)
MATCH_THRESHOLD = 0.8
# 等待时间(防止操作过快)
DELAY_TIME = 1

# -------------------------- 初始化工具 --------------------------
# 初始化OCR引擎(用于识别文字)
ocr = RapidOCR()
# 设置PyAutoGUI的暂停时间(防止操作太快)
pyautogui.PAUSE = DELAY_TIME
# 开启失败保护(鼠标移到屏幕左上角会终止脚本)
pyautogui.FAILSAFE = True

# -------------------------- 核心函数 --------------------------
def find_element_by_image(template_path, threshold):
    """
    通过图像匹配定位屏幕上的元素
    :param template_path: 模板图片路径
    :param threshold: 匹配阈值
    :return: 元素中心坐标 (x, y) 或 None
    """
    # 读取模板图片
    template = cv2.imread(template_path, 0)
    if template is None:
        print(f"错误:无法读取模板图片 {template_path}")
        return None
    
    # 获取模板尺寸
    h, w = template.shape
    
    # 截取当前屏幕并转为灰度图
    screenshot = pyautogui.screenshot()
    screenshot_gray = cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2GRAY)
    
    # 模板匹配
    result = cv2.matchTemplate(screenshot_gray, template, cv2.TM_CCOEFF_NORMED)
    loc = np.where(result >= threshold)
    
    # 提取第一个匹配的中心坐标
    if len(loc[0]) > 0:
        x = loc[1][0] + w // 2
        y = loc[0][0] + h // 2
        return (x, y)
    else:
        print("未找到匹配的元素")
        return None

def recognize_text_in_region(region=None):
    """
    识别屏幕指定区域的文字(region格式:(x1, y1, width, height))
    :param region: 识别区域,None则识别全屏
    :return: 识别到的文字列表
    """
    # 截取指定区域屏幕
    if region:
        screenshot = pyautogui.screenshot(region=region)
    else:
        screenshot = pyautogui.screenshot()
    
    # OCR识别
    result, _ = ocr(np.array(screenshot))
    if result:
        # 提取文字部分
        text_list = [item[1] for item in result]
        return text_list
    else:
        return []

def click_element(x, y):
    """模拟鼠标点击指定坐标"""
    pyautogui.moveTo(x, y, duration=0.2)  # 平滑移动鼠标
    pyautogui.click(x, y)
    print(f"已点击坐标:({x}, {y})")

# -------------------------- 主逻辑 --------------------------
if __name__ == "__main__":
    # 1. 尝试通过图像匹配定位“保存”按钮
    target_pos = find_element_by_image(TEMPLATE_PATH, MATCH_THRESHOLD)
    if target_pos:
        click_element(*target_pos)
    else:
        # 2. 备用方案:识别屏幕上的“保存”文字并点击(示例)
        print("尝试通过文字识别定位...")
        text_list = recognize_text_in_region()
        if "保存" in text_list:
            # 这里可以扩展:根据文字位置定位并点击
            print("识别到“保存”文字,可扩展点击逻辑")
        else:
            print("未识别到目标文字")

二、关键代码解释

  1. 图像匹配核心(find_element_by_image)
    • 先读取你预先截取的目标按钮模板(比如save_button.png
    • 截取当前屏幕并转为灰度图(减少颜色干扰)
    • 使用 OpenCV 的matchTemplate进行模板匹配,通过阈值过滤低相似度结果
    • 返回第一个匹配元素的中心坐标(避免点击到边缘)
  2. OCR 文字识别(recognize_text_in_region)
    • 基于 RapidOCR 实现中文识别,无需复杂配置
    • 支持指定识别区域(减少识别范围,提高速度和准确率)
    • 返回识别到的所有文字列表,方便判断是否存在目标文字(如 “另存为”)
  3. 键鼠操作(click_element)
    • 使用pyautogui.moveTo平滑移动鼠标(更接近人类操作)
    • 开启FAILSAFE保护:鼠标移到屏幕左上角可终止脚本,防止无限循环

三、实操注意事项(解决 Gemini 提到的工程挑战)

  1. 分辨率 / 缩放问题
    • 截取模板图片时,务必在实际运行环境中截取(和目标屏幕分辨率 / 缩放一致)
    • 可在代码中添加分辨率适配:先获取屏幕分辨率,再缩放模板图片
  2. 动态渲染 / 遮挡问题
    • 操作前添加pyautogui.sleep(1),等待 UI 加载完成
    • 匹配失败时,可多次尝试(比如循环 3 次),避免单次遮挡导致失败
  3. 提高匹配准确率
    • 截取模板时,只截取目标元素的核心区域(比如按钮的图标部分),不要包含过多背景
    • 调整MATCH_THRESHOLD:固定场景下可设为 0.7-0.8,复杂场景可降到 0.6

总结

  1. 核心技术栈:Ubuntu 下用 Python + PyAutoGUI(图像匹配 / 键鼠操作) + RapidOCR(文字识别)即可快速实现 CV-RPA,无需从零写 OpenCV 底层;
  2. 核心挑战:实际落地需解决分辨率、动态 UI、遮挡问题,核心方案是 “固定运行环境 + 多次重试 + 合理阈值”;
  3. 优先级选择:固定场景下,CV-RPA 完全够用;通用场景优先考虑控件识别(如 Windows 的 UIA、网页的 DOM),CV 作为兜底方案。
你可以先从这个最小示例入手,替换成你需要操作的目标元素(比如 Word 的 “另存为” 按钮),测试后再逐步扩展逻辑(比如增加多元素识别、流程判断等)。
首先一个 Accessibility API 这个东西是操作系统级别的,应该是 Windows Linux 它通通都是广泛支持,就是说不论你这个应用程序是怎么样子的,你是用什么 QT 开发的也好啊,是 X11 的应用也好,这个东西它的底层应该都支持吧?就这个残障人士的这个这个 GUI 界面这个支持,是不是它是操作系统内置的,不需要你这个应用程序,就是透明的都可以支持?是不是这样?啊
你的理解非常精准,这正是 Accessibility API(辅助功能接口)最核心的价值。
简单直接的结论是:是的,它是操作系统原生内置的基础设施,对于绝大多数标准应用来说,是“透明”且“强制”支持的。
为了让你彻底搞清楚这个机制,我们可以从以下三个层面来拆解:

1. 它是操作系统的“原生皮肤”,而非应用的“外挂”

无论是 Windows 还是 Linux,Accessibility API 就像是操作系统给所有软件穿的一套“统一制服”。
  • 操作系统层面(Provider): 微软的 MSAA / UI Automation(Windows)、Linux 的 AT-SPI(基于 D-Bus)、macOS 的 AX API。这些都是系统内核或桌面环境(如 GNOME/KDE)提供的服务,常驻在后台。
  • 应用层面(Consumer): 你的自动化工具(RPA 软件)只需要调用操作系统提供的这个接口,就能读取系统内所有软件的信息。
关键点: 软件开发者不需要为了支持这个接口去专门修改代码。只要软件遵循了操作系统的标准 UI 库(如 Windows 的 Win32 API、Qt、GTK),它在编译时就自动被系统“注册”进了这个辅助体系中。

2. 为什么它能“通吃” Qt / GTK / X11?

你提到的 Qt、GTK 应用,甚至是 Java 开发的跨平台软件,之所以能被支持,是因为它们内部已经集成了对系统接口的适配:
  • Qt: Qt 框架内置了对 UIA (Windows) 和 AT-SPI (Linux) 的原生支持。你写的 Qt 程序,运行时会自动向操作系统注册自己的按钮、菜单是谁。
  • GTK: 同样,GTK 基于 Linux 的 AT-SPI 标准实现,完美适配桌面环境。
  • X11/Wayland: 这是底层显示协议。Accessibility API 是构建在显示协议之上的语义层。比如,X11 负责把像素画在屏幕上,而 AT-SPI 负责告诉系统“这个像素块是个关闭按钮,位置在哪”。
所以,不需要应用程序本身做特殊处理,只要它是用正规的 UI 框架开发的,操作系统就能透过底层的像素,精准地读取到它的控件层级、ID、类型、属性

3. 唯一的例外:自绘控件(Owner-Drawn)

这也是 Gemini 回答中提到的“某些自绘 UI 不支持”的原因,但这不是 API 的问题,而是应用破坏了规则
  • 情况: 如果一个程序(比如某些游戏、极其定制的 ERP 软件、或者黑客开发的工具)完全不使用系统标准控件,而是直接在画布上自己画像素(Manually Drawing Pixels),不向系统注册任何辅助功能信息。
  • 结果: 此时 Accessibility API 确实“抓瞎”,因为它没有收到这个程序的状态报告。
  • 对比: 这种情况下,CV-based(视觉识别)RPA 反而成为了唯一解决方案,因为它只看像素,不管你是怎么画出来的。

总结你的判断

你的直觉完全正确:
  1. 全覆盖性: 只要是用 Qt、GTK、Win32 这些正规框架写的软件,Accessibility API 都能支持,不需要应用开发者额外写代码
  2. 系统级依赖: 它是操作系统的一部分,你只需要确保你的自动化工具开启了系统对应的辅助功能权限(比如 Windows 里的“允许辅助功能工具访问”)。
  3. 效率优势: 这就是为什么控件识别(Object-based)比视觉识别(CV-based)快、准、稳的根本原因——它直接读取数据库,而不是去猜图片。
所以,在你的自动化方案中,优先打通 Accessibility API 是性价比最高的选择。只有在遇到那些“流氓”自绘软件时,才需要切回你擅长的 CV 视觉方案。
那我问你,在 Ubuntu 22.04 这个桌面, GNOME G N O M E, 我那个 Firefox 它是 snap 版本,那么火狐上面这些控件,这些操作, Accessibility 能支持吗?就是如果能支持,我们要怎么做呢?
可以支持,但需在系统与 Firefox 内分别开启权限与配置。Ubuntu 22.04 GNOME + Snap Firefox 的 Accessibility(AT-SPI)默认受限,需按以下步骤配置。

🔍 支持性说明

  • 系统层:Ubuntu 22.04 GNOME 内置 AT-SPI 无障碍栈,Snap Firefox 已通过 desktop-legacy 接口(含 a11y 访问)自动连接
  • 应用层:Snap Firefox 基于 GTK3,通过 at-spi2-atk 桥接暴露控件信息至 AT-SPI。
  • 限制:默认未开启系统与应用的无障碍开关,需手动启用。

⚙️ 配置步骤

  1. 系统全局开启(GNOME)
    打开 设置 → 辅助功能 → 开启「辅助功能菜单」与任意一项辅助功能(如屏幕键盘、高对比度)。
    或终端执行:
    bash
    运行
    gsettings set org.gnome.desktop.interface toolkit-accessibility true
    ```{insert\_element\_1\_}
    
  2. Firefox 内启用无障碍
    打开 Firefox,地址栏输入 about:config,接受风险后搜索并设置:
    • accessibility.force_enabledtrue(强制启用无障碍)
    • devtools.accessibility.enabledtrue(开发工具无障碍)
    • accessibility.blockautorefreshfalse(按需配置,避免自动刷新干扰)
  3. 验证与调试
    安装调试工具:
    bash
    运行
    sudo apt install at-spi2-core accerciser
    
    运行 accerciser,可浏览 Firefox 控件树与事件,确认无障碍正常工作。

🚀 自动化工具接入示例(Python + pyatspi)

python
运行
import pyatspi

def find_firefox_button(button_name):
    registry = pyatspi.Registry()
    desktop = registry.getDesktop(0)
    for app in desktop:
        if app.name == "Firefox":
            for child in app:
                if child.role == pyatspi.ROLE_PUSH_BUTTON and child.name == button_name:
                    return child
    return None

if __name__ == "__main__":
    button = find_firefox_button("搜索")
    if button:
        button.doAction(0)  # 模拟点击

⚠️ 常见问题与处理

  • 仍无法识别:重启 Firefox 与会话,确认 toolkit-accessibilitytrue
  • 权限报错:检查 snap connections firefox,确保 desktop-legacy 已连接
  • 性能影响:仅在需要时启用,无需长期开启。

💡 总结

核心是系统全局开启无障碍 + Firefox 内启用无障碍。配置完成后,Snap Firefox 的控件可被 AT-SPI 正常读取,自动化工具可通过 AT-SPI 实现精准操作,优于视觉识别。
需要我按你的具体自动化场景(如点击“搜索”“下载”按钮)给出完整 Python 脚本吗?
我的应用要求其实很简单,就是在对话,就是豆包的这个网页对话历史里面,去一个一个的选取这个历史对话记录。然后选左边的这个对话历史选择标题选择之后右边的这个历史对话它是因为是软加载,所以需要用鼠标或者键盘一直移到最顶端。而这个移到最顶端的这个时间可能是很长,因为有的对话非常的长,所以要多次尝试,直到不能够再向上移为止。然后呢使用一个书签,这个书签是已经设置好了这个备份下载当前对话的这个 HTML 文件。的这个 Java 脚 Java script 的这个脚本,所以只要去按一下那个书签的这个按钮,就会开启这个下载的过程。那这个过程的话需要等待这个下载完毕,它是有个进度条,但是这观察这个进进度条就很困难。你怎么样去识别这个下载已经完成呢?就这个事情是一个比较比较现在还没想好怎么做到,因为它确实是有一个进度条,但这个东西你就变成说要去识别,而且这个下载这个进度还是比较长的。意思就是说怎样在 GUI 界面,就是让程序这种 accessibility 这个,它有没有什么能够识别说下载,就是 Firefox 这个浏览器网页,它那个下载已经 ready, 它那个确实是有个 ready, 就是那个下载的那个进度条那个当它完成我们要怎么知道这个有 API 可以获得它的状态吗?啊当然这个下面就是简单了,就是下载完之后需要把这个下载的文件进行文件名修改,这个倒是可以简单的写脚本,就是它都是固定位置下载,然后备份到一个地方。还有一个就是要去把当前对话的这个这个对话的这个标题啊,就是要取得这个对话标题。这个我通常手动做法是去修改这个对话标题,然后就可以把这个对话标题进行拷贝出来,然后去重命名下载的这个 HTML 文件,去修改这个文件名,因为对话太多了。所以这一系列的操作能自动化吗?你想想看。
完全可以自动化,而且这是一个非常典型的 “GUI 自动化 + 浏览器脚本 + 文件系统监控” 组合场景。你的需求逻辑非常清晰,我将为你拆解为 四个核心模块,并提供基于 Ubuntu 22.04 / GNOME / Snap Firefox 的完整技术实现方案。

核心方案概览

我们将采用 “Accessibility API (AT-SPI) + 浏览器脚本 (JavaScript) + 文件系统监控 (inotify)” 的混合架构。
  1. 导航:使用 AT-SPI 精准点击左侧历史对话标题。
  2. 加载:监听滚动条状态,确认对话内容完全加载至顶端。
  3. 下载:通过浏览器脚本触发下载,并通过文件监控等待下载完成。
  4. 重命名:获取对话标题,批量重命名文件。

一、环境准备:启用 AT-SPI 与安装依赖

首先,确保你的环境权限配置正确,并安装所需的 Python 库。

1. 系统权限配置 (已确认)

  • 系统无障碍:已开启 org.gnome.desktop.interface toolkit-accessibility true
  • Firefox Snap 权限:确保 snap connections firefoxdesktop-legacypersonal-files 接口已连接。

2. 安装 Python 依赖

我们将使用 pyatspi 操作 AT-SPI,pyautogui 作为兜底,inotify 监控文件变化。
bash
运行
pip install pyatspi python-dotenv pyautogui inotify

二、模块拆解与实现

模块一:导航与加载(AT-SPI + 滚动监听)

这是最核心的部分。我们需要精准定位并点击左侧的对话标题,然后等待其右侧内容完全加载。
关键技术点
  • 定位左侧对话列表:通过 AT-SPI 找到 Firefox 中左侧的对话列表容器。
  • 遍历 / 选择标题:根据你的需求,可能需要按顺序遍历所有对话,或指定某个标题。
  • 监听滚动条:这是解决 “软加载” 的关键。我们需要持续检测右侧内容区域的滚动条是否还能向上滚动,直到无法滚动为止。
Python 代码示例 (核心逻辑)
python
运行
import pyatspi
import time
import pyautogui

# --- 配置 ---
# 你需要根据实际情况调整这些角色和名称
LEFT_PANEL_ROLE = pyatspi.ROLE_LIST  # 左侧对话列表角色
RIGHT_PANEL_ROLE = pyatspi.ROLE_SCROLL_PANE  # 右侧内容滚动面板角色
BROWSER_NAME = "Firefox"

def wait_for_scroll_to_top(role):
    """
    等待滚动面板滚动到最顶端。
    :param role: 滚动面板的 accessible role 对象
    """
    print("等待内容加载至顶端...")
    while True:
        # 获取滚动值 (value 属性)
        value = pyatspi.Accessible(role.queryValue())
        current_value = value.currentValue
        min_value = value.minimumValue
        
        # 当当前值接近最小值时,认为已到顶端
        if abs(current_value - min_value) < 1:  # 允许微小误差
            print("已到达顶端,加载完成。")
            break
        time.sleep(0.5)  # 降低CPU占用

def select_chat_history(chat_title):
    """
    通过标题选择左侧的对话历史。
    :param chat_title: 对话标题名称
    """
    registry = pyatspi.Registry()
    desktop = registry.getDesktop(0)

    for app in desktop:
        if app.name == BROWSER_NAME:
            # 递归遍历找到左侧对话列表
            left_panel = find_child_by_role(app, LEFT_PANEL_ROLE)
            if not left_panel:
                print("未找到左侧对话列表。")
                return

            # 在左侧列表中查找目标标题
            target_item = find_child_by_name(left_panel, chat_title)
            if target_item:
                print(f"找到对话标题: {chat_title}")
                # 模拟点击
                target_item.doAction(0)
                # 等待右侧内容加载
                right_panel = find_child_by_role(app, RIGHT_PANEL_ROLE)
                if right_panel:
                    wait_for_scroll_to_top(right_panel)
                return True
            else:
                print(f"未找到标题: {chat_title}")
                return False

    print(f"未找到 {BROWSER_NAME} 应用。")
    return False

# --- 辅助函数 ---
def find_child_by_name(accessible, name):
    """递归查找指定名称的子元素"""
    if accessible.name == name:
        return accessible
    for child in accessible:
        result = find_child_by_name(child, name)
        if result:
            return result
    return None

def find_child_by_role(accessible, role):
    """递归查找指定角色的子元素"""
    if accessible.role == role:
        return accessible
    for child in accessible:
        result = find_child_by_role(child, role)
        if result:
            return result
    return None

# --- 执行 ---
if __name__ == "__main__":
    # 替换为你要选择的第一个对话标题
    target_chat_title = "你的第一个对话标题"
    select_chat_history(target_chat_title)

模块二:触发下载(浏览器书签脚本)

你已经有了一个下载 HTML 的书签脚本,这部分非常简单,只需确保它能被正确触发。
优化建议
将你的 JavaScript 书签代码封装成一个函数,并通过 AT-SPI 模拟点击书签栏的按钮。或者,更简单直接的方式是使用 pyautogui 模拟快捷键(如 Ctrl+Shift+B 显示书签栏,然后移动鼠标点击)。

模块三:监控下载完成(文件系统 inotify)

这是解决 “等待下载完成” 的最佳方案。我们不依赖视觉识别进度条,而是直接监控 Firefox 的下载目录。
核心原理
  • Firefox 的下载目录默认是 ~/Snap/Firefox/current/Downloads
  • 文件下载时会先创建 .part 临时文件,下载完成后 .part 文件消失,出现完整文件。
  • 使用 inotify 监控这个目录,当检测到目标文件(或 .part 文件消失)事件时,即可判定下载完成。
Python 代码示例
python
运行
import inotify.adapters
import os
import time

# --- 配置 ---
DOWNLOAD_DIR = os.path.expanduser("~/Snap/Firefox/common/Downloads")  # Snap Firefox 下载目录
TARGET_FILENAME_PATTERN = "豆包对话_"  # 你的书签脚本生成的文件名前缀

def wait_for_download_complete():
    """
    监控下载目录,直到出现目标文件。
    :return: 下载完成的文件路径
    """
    print(f"开始监控下载目录: {DOWNLOAD_DIR}")
    
    # 初始化 inotify 监控器
    inotify_obj = inotify.adapters.Inotify()
    inotify_obj.add_watch(DOWNLOAD_DIR)

    try:
        for event in inotify_obj.event_gen(yield_nones=False):
            (_, type_names, path, filename) = event

            # 监控文件创建事件
            if 'IN_CREATE' in type_names:
                # 排除 .part 临时文件
                if filename.endswith('.part'):
                    continue
                # 检查是否是我们要的文件
                if filename.startswith(TARGET_FILENAME_PATTERN):
                    downloaded_file = os.path.join(path, filename)
                    print(f"检测到下载完成: {downloaded_file}")
                    return downloaded_file

            # 监控文件移动/删除事件 (用于处理部分下载完成的情况)
            if 'IN_MOVED_TO' in type_names or 'IN_DELETE' in type_names:
                for f in os.listdir(path):
                    if f.startswith(TARGET_FILENAME_PATTERN) and not f.endswith('.part'):
                        downloaded_file = os.path.join(path, f)
                        print(f"检测到下载完成: {downloaded_file}")
                        return downloaded_file
    finally:
        inotify_obj.remove_watch(DOWNLOAD_DIR)
        inotify_obj = None

# --- 执行 ---
if __name__ == "__main__":
    downloaded_file = wait_for_download_complete()
    if downloaded_file:
        print(f"文件已准备好: {downloaded_file}")
    else:
        print("下载监控超时或失败。")

模块四:获取对话标题与重命名文件

这是最后一步。我们需要从页面上获取对话标题,然后用它来重命名刚刚下载的文件。
两种方案
  1. AT-SPI 方案:通过 Accessibility API 读取页面标题元素的文本。
  2. 浏览器脚本方案 (更可靠):直接在 Firefox 中执行 JavaScript 来获取标题。
推荐使用浏览器脚本方案,因为它绕过了复杂的 DOM 遍历,直接与页面交互。
Python 代码示例 (结合 pyautogui 或 AT-SPI 触发 JS)
python
运行
import subprocess
import os

# --- 配置 ---
# 你的 Firefox 可执行文件路径 (snap 版本)
FIREFOX_BIN = "/snap/bin/firefox"

def get_chat_title_via_js():
    """
    通过在 Firefox 中执行 JavaScript 获取对话标题。
    注意:这需要一个额外的辅助脚本或使用 devtools 协议。
    一个更简单的方式是,在你的 HTML 书签脚本中,将标题作为文件名的一部分,
    这样你就不需要单独获取标题了,直接从文件名解析。
    """
    # 这里提供一个思路:使用 Firefox 的远程调试协议 (CDP) 来执行 JS
    # 但配置较复杂。对于自动化场景,强烈建议在下载脚本中处理。
    
    # 替代方案 (推荐):
    # 修改你的下载书签脚本,在保存 HTML 时,直接将对话标题作为文件名的一部分。
    # 例如: document.title + ".html"
    # 这样,你在模块三中拿到的文件名就已经包含了标题,无需额外获取。
    
    # 以下是一个模拟,假设你已经在文件名中包含了标题
    downloaded_file = wait_for_download_complete()
    if downloaded_file:
        # 从文件名解析标题 (假设格式为 "标题 - 豆包对话.html")
        title = os.path.basename(downloaded_file).replace(" - 豆包对话.html", "")
        return title, downloaded_file
    return None, None

def rename_file(original_path, new_name):
    """
    重命名文件。
    :param original_path: 原文件路径
    :param new_name: 新文件名 (不含路径)
    """
    if not original_path or not new_name:
        return
    dirname = os.path.dirname(original_path)
    new_path = os.path.join(dirname, new_name)
    try:
        os.rename(original_path, new_path)
        print(f"文件已重命名为: {new_path}")
    except OSError as e:
        print(f"重命名失败: {e}")

# --- 执行 ---
if __name__ == "__main__":
    title, downloaded_file = get_chat_title_via_js()
    if title and downloaded_file:
        new_filename = f"{title}.html"
        rename_file(downloaded_file, new_filename)

三、完整自动化流程整合

现在,我们将以上四个模块整合成一个完整的自动化脚本。
最终脚本逻辑
  1. 遍历所有对话标题列表(你需要维护一个列表,或通过 AT-SPI 读取左侧所有标题)。
  2. 对每个标题:
    a. 调用 select_chat_history(title) 导航并加载。
    b. 调用 trigger_bookmark_download() 触发下载。
    c. 调用 wait_for_download_complete() 等待文件下载完成。
    d. 解析标题,调用 rename_file() 重命名文件。
  3. 处理下一个对话。

四、关键挑战与解决方案总结

表格
挑战解决方案
左侧对话导航使用 AT-SPI (pyatspi) 精准定位并点击列表项。
右侧软加载监听滚动面板的 value 属性,直到滚动到最小值(顶端)。
下载完成检测使用 inotify 监控下载目录,监听文件创建 / 完成事件。这是最可靠的方式
获取与重命名在下载脚本中直接将标题写入文件名,避免复杂的 DOM 读取。

五、行动建议

  1. 优先优化下载脚本:这是整个自动化的基石。请修改你的书签脚本,使其在保存 HTML 时,直接使用对话标题作为文件名。例如:
    javascript
    运行
    // 你的书签脚本核心逻辑
    function saveChatAsHTML() {
        const title = document.querySelector('对话标题的CSS选择器').innerText; // 获取标题
        const html = document.documentElement.outerHTML;
        const blob = new Blob([html], { type: 'text/html' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `${title}.html`; // 直接使用标题命名
        a.click();
        URL.revokeObjectURL(url);
    }
    
  2. 调试 AT-SPI 元素:使用 accerciser 工具,精确找出左侧对话列表、右侧滚动面板、以及对话标题元素的 rolename,并替换到代码的配置中。
  3. 整合与测试:将所有模块整合,进行单步调试,确保每一步都能正确执行。
这个方案是完全可行的,它充分利用了操作系统和浏览器的标准接口,避免了脆弱的视觉识别,是企业级 RPA 的标准实践。
如何使用AT-SPI精准点击左侧历史对话标题?
如何监听滚动条状态?