保存时间:2026/3/29 22:11:59
# 安装系统级工具(用于键鼠操作)
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:跨平台键鼠控制 + 图像匹配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("未识别到目标文字")
save_button.png)matchTemplate进行模板匹配,通过阈值过滤低相似度结果pyautogui.moveTo平滑移动鼠标(更接近人类操作)FAILSAFE保护:鼠标移到屏幕左上角可终止脚本,防止无限循环pyautogui.sleep(1),等待 UI 加载完成MATCH_THRESHOLD:固定场景下可设为 0.7-0.8,复杂场景可降到 0.6desktop-legacy 接口(含 a11y 访问)自动连接。at-spi2-atk 桥接暴露控件信息至 AT-SPI。gsettings set org.gnome.desktop.interface toolkit-accessibility true
```{insert\_element\_1\_}
about:config,接受风险后搜索并设置:accessibility.force_enabled → true(强制启用无障碍)devtools.accessibility.enabled → true(开发工具无障碍)accessibility.blockautorefresh → false(按需配置,避免自动刷新干扰)sudo apt install at-spi2-core accerciser
accerciser,可浏览 Firefox 控件树与事件,确认无障碍正常工作。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) # 模拟点击
toolkit-accessibility 为 true。snap connections firefox,确保 desktop-legacy 已连接。org.gnome.desktop.interface toolkit-accessibility true。snap connections firefox 中 desktop-legacy 和 personal-files 接口已连接。pyatspi 操作 AT-SPI,pyautogui 作为兜底,inotify 监控文件变化。pip install pyatspi python-dotenv pyautogui inotify
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)
pyautogui 模拟快捷键(如 Ctrl+Shift+B 显示书签栏,然后移动鼠标点击)。~/Snap/Firefox/current/Downloads。.part 临时文件,下载完成后 .part 文件消失,出现完整文件。inotify 监控这个目录,当检测到目标文件(或 .part 文件消失)事件时,即可判定下载完成。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("下载监控超时或失败。")
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)
select_chat_history(title) 导航并加载。b. 调用 trigger_bookmark_download() 触发下载。c. 调用 wait_for_download_complete() 等待文件下载完成。d. 解析标题,调用 rename_file() 重命名文件。| 挑战 | 解决方案 |
|---|---|
| 左侧对话导航 | 使用 AT-SPI (pyatspi) 精准定位并点击列表项。 |
| 右侧软加载 | 监听滚动面板的 value 属性,直到滚动到最小值(顶端)。 |
| 下载完成检测 | 使用 inotify 监控下载目录,监听文件创建 / 完成事件。这是最可靠的方式。 |
| 获取与重命名 | 在下载脚本中直接将标题写入文件名,避免复杂的 DOM 读取。 |
// 你的书签脚本核心逻辑
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);
}
accerciser 工具,精确找出左侧对话列表、右侧滚动面板、以及对话标题元素的 role 和 name,并替换到代码的配置中。