那我问你,在 Ubuntu 22.04 这个桌面, GNOME G N O M E, 我那个 Firefox 它是 snap 版本,那么火狐上面这些控件,这些操作, Accessibility 能支持吗?就是如果能支持,我们要怎么做呢?
CV+键鼠仿真实现RPA的技术方案与实操
导出时间:2026/5/27 19:40:19
来源地址:https://www.doubao.com/chat/38416831551168002
消息数量:4
搜索 6 个关键词,参考 31 篇资料
可以支持,但需在系统与 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。 - 限制:默认未开启系统与应用的无障碍开关,需手动启用。
⚙️ 配置步骤
-
系统全局开启(GNOME) 打开 设置 → 辅助功能 → 开启「辅助功能菜单」与任意一项辅助功能(如屏幕键盘、高对比度)。 或终端执行:bash运行
gsettings set org.gnome.desktop.interface toolkit-accessibility true -
Firefox 内启用无障碍 打开 Firefox,地址栏输入
about:config,接受风险后搜索并设置:accessibility.force_enabled→true(强制启用无障碍)devtools.accessibility.enabled→true(开发工具无障碍)accessibility.blockautorefresh→false(按需配置,避免自动刷新干扰)
-
验证与调试 安装调试工具: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-accessibility为true。 - 权限报错:检查
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)” 的混合架构。
- 导航:使用 AT-SPI 精准点击左侧历史对话标题。
- 加载:监听滚动条状态,确认对话内容完全加载至顶端。
- 下载:通过浏览器脚本触发下载,并通过文件监控等待下载完成。
- 重命名:获取对话标题,批量重命名文件。
一、环境准备:启用 AT-SPI 与安装依赖
首先,确保你的环境权限配置正确,并安装所需的 Python 库。
1. 系统权限配置 (已确认)
- 系统无障碍:已开启
org.gnome.desktop.interface toolkit-accessibility true。 - Firefox Snap 权限:确保
snap connections firefox中desktop-legacy和personal-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("下载监控超时或失败。")
模块四:获取对话标题与重命名文件
这是最后一步。我们需要从页面上获取对话标题,然后用它来重命名刚刚下载的文件。
两种方案:
- AT-SPI 方案:通过 Accessibility API 读取页面标题元素的文本。
- 浏览器脚本方案 (更可靠):直接在 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)
三、完整自动化流程整合
现在,我们将以上四个模块整合成一个完整的自动化脚本。
最终脚本逻辑
- 遍历所有对话标题列表(你需要维护一个列表,或通过 AT-SPI 读取左侧所有标题)。
- 对每个标题:
a. 调用
select_chat_history(title)导航并加载。 b. 调用trigger_bookmark_download()触发下载。 c. 调用wait_for_download_complete()等待文件下载完成。 d. 解析标题,调用rename_file()重命名文件。 - 处理下一个对话。
四、关键挑战与解决方案总结
表格
| 挑战 | 解决方案 |
|---|---|
| 左侧对话导航 | 使用 AT-SPI (pyatspi) 精准定位并点击列表项。 |
| 右侧软加载 | 监听滚动面板的 value 属性,直到滚动到最小值(顶端)。 |
| 下载完成检测 | 使用 inotify 监控下载目录,监听文件创建 / 完成事件。这是最可靠的方式。 |
| 获取与重命名 | 在下载脚本中直接将标题写入文件名,避免复杂的 DOM 读取。 |
五、行动建议
- 优先优化下载脚本:这是整个自动化的基石。请修改你的书签脚本,使其在保存 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); } - 调试 AT-SPI 元素:使用
accerciser工具,精确找出左侧对话列表、右侧滚动面板、以及对话标题元素的 role 和 name,并替换到代码的配置中。 - 整合与测试:将所有模块整合,进行单步调试,确保每一步都能正确执行。
这个方案是完全可行的,它充分利用了操作系统和浏览器的标准接口,避免了脆弱的视觉识别,是企业级 RPA 的标准实践。