Linux 下 Qt 应用无障碍自动化:记一次WX无人值守系统的架构演进

这是一篇「踩坑实录」而非架构设计文档。每个方案都经历了至少一次推翻重来,最终沉淀为稳定运行的自动化系统。


1. 场景:为什么需要闪信自动化

闪信(WeChat Linux 版)放在一台无外接显示器的 Linux 服务器上运行,通过 Xvfb 虚拟显示 + AT-SPI 无障碍接口,实现无人值守的消息检测与自动回复

几个核心需求:

  1. 7×24 消息检测,漏一条都不行
  2. 智能回复 --- 闲聊秒回,正事需调用 LLM 生成回复
  3. 全链路不稳定可自愈 --- 闪信崩溃、AT-SPI 树塌陷、DBus 故障都要自动恢复

听起来像是 watchdog + Python + D-Bus 的标准组合,但实际踩坑远超预期。下面按照踩坑顺序,逐个展开。


2. 技术栈

技术
虚拟显示 Xvfb :99 + GLX + RENDER + RANDR
无障碍接口 AT-SPI2 (Assistive Technology Service Provider Interface)
D-Bus at-spi bus(辅助功能专用总线) / session bus
GUI 自动化 xdotool + xclip + Python gi.repository.Atspi
免扫码登录 AT-SPI 定位「进入微信」按钮 → xdotool 点击
LLM 回复 hermes chat (Agent 主进程)
调度 Friday Gateway Cron(每分钟)

3. 第一坑:Qt 应用的 AT-SPI 注册

现象

闪信启动后,AT-SPI 无障碍树为空(Desktop child_count = 0),所有检测脚本看不到闪信窗口。

排查

闪信是 Qt 应用 。Linux 下 GTK 应用通过 GTK_MODULES=gail:atk-bridge 即可注册到 AT-SPI。实测中加了 GTK_MODULES=gail:atk-bridge,闪信稳如泰山------就是 AT-SPI 树里找不到它。

搜索 Qt 无障碍文档后发现:Qt 应用需要单独设置 QT_ACCESSIBILITY=1

bash 复制代码
# 错误配置(GTK_MODULES 对 Qt 应用无效)
GTK_MODULES=gail:atk-bridge DISPLAY=:99 /opt/wechat/wechat

# 正确配置
QT_ACCESSIBILITY=1 GTK_MODULES=gail:atk-bridge DISPLAY=:99 /opt/wechat/wechat

加上 QT_ACCESSIBILITY=1 后,AT-SPI 树中立刻出现 wechat 应用节点。

教训

Qt 和 GTK 的无障碍注册机制完全独立。 只配 GTK_MODULES 对 Qt 应用没有任何作用。这是很多人配完 Xvfb + AT-SPI 后发现 GUI 窗口「看不见」的根本原因。

完整环境变量

bash 复制代码
DISPLAY=:99
QT_ACCESSIBILITY=1              # Qt 应用必须
QT_QUICK_BACKEND=software        # 软件渲染(无 GPU)
QMLSCENE_DEVICE=softwarecontext
LIBGL_ALWAYS_SOFTWARE=1
GTK_MODULES=gail:atk-bridge      # GTK 桥接
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus

4. 第二坑:子进程中调用 LLM 必然挂死

现象

在守护进程 handle_unread.py 中用 subprocess.run(['hermes', 'chat', ...]) 调用 LLM 生成回复,进程永远超时,不给任何输出。

排查路径(全部失败)

  • 绝对路径 → 挂
  • shell=True → 挂
  • timeout=1545 → 全部超时
  • 不同模型(deepseek-chat / v4-flash / qwen3.7-max)→ 全部挂
  • 在 cron 中直接调用 → 同样挂
  • 唯一正常 :在终端中直接执行 hermes chat

根因

handle_unread.py 在主进程中通过 gi.repository.Atspi 持有了 AT-SPI D-Bus 连接subprocess.run 创建的子进程继承了父进程的全部文件描述符和 D-Bus 连接,导致 GLib/D-Bus 资源冲突。

这是 Python 中 subprocess 的经典陷阱------父进程持有的资源不一定能被安全共享。AT-SPI 连接的内部状态(包括注册表缓存、事件监听器)在 fork 时被复制,子进程试图创建新的 AT-SPI 连接时产生死锁。

解决方案:队列 + 独立进程

架构变更

bash 复制代码
# 旧架构(子进程调 LLM → 挂死)
handle_unread.py → subprocess.run(['hermes', 'chat', ...]) → 挂死

# 新架构(队列 + Cron Agent)
handle_unread.py → 闲聊秒回(本地 regex)
                 → 非闲聊 → 写入 /tmp/boss_incoming.json
Friday Cron → 每分钟读取队列 → hermes chat → 回复

关键设计:

  • handle_unread.py 不再调 LLM,只做检测和简单回复
  • 需要智能回复的消息写入 JSON 队列
  • Cron Job 运行在 Hermes Gateway 的 Agent 主进程中,不持有 AT-SPI 资源
  • Agent 的 terminal 工具启动的是独立 shell 进程,D-Bus 无冲突

5. 第三坑:DBus 总线选择

表象

系统运行数小时后,AT-SPI 树突然塌陷(Desktop child_count = 0),恢复脚本重启闪信后依然无法注册。

排查

系统中有两条 AT-SPI 总线:

  • unix:path=/home/cronuser/.cache/at-spi/bus_99 --- 早期专用总线
  • unix:path=/run/user/1000/bus --- session bus(含 org.a11y.Bus 服务)

最初所有脚本硬编码 at-spi/bus_99。在系统被多次 pkill -9 后,这条总线上的 registryd 进程状态损坏,新启动的闪信无法注册。

切换到 session bus 后问题消失:

python 复制代码
# 不稳定的配置
os.environ['DBUS_SESSION_BUS_ADDRESS'] = 'unix:path=/home/cronuser/.cache/at-spi/bus_99'

# 稳定的配置(session bus)
os.environ['DBUS_SESSION_BUS_ADDRESS'] = 'unix:path=/run/user/1000/bus'

教训

不要把应用注册到自定义的 AT-SPI 总线 上。session bus 上的 org.a11y.Bus 服务由系统自动管理,容错性好得多。


6. 完整架构(v5.22 最终版)

关键脚本

脚本 职责
check_unread.py 守护进程,AT-SPI 扫描未读会话
handle_unread.py 消费未读,本地闲聊秒回,非闲聊入队
reply_boss.py 读队列 → hermes chat → 发送
login.py 免扫码登录(点击「进入微信」)
start-xvfb.sh Xvfb :99 统一入口,幂等

7. 踩坑清单

# 根因 修复
1 AT-SPI 树为空 Qt 应用缺 QT_ACCESSIBILITY=1 环境变量
2 hermes chat 子进程挂死 子进程继承 AT-SPI 连接 → 冲突 队列+Cron 隔离
3 at-spi/bus_99 不稳定 registryd 状态易损坏 改用 session bus
4 Cron Job 存在 Jarvis Profile 下 Jarvis 无 Gateway 迁移到 Friday
5 symlink 脚本被 Cron 拦截 调度器解析真实路径 复制为独立文件
6 恢复流程中微信重启也缺 QT_ACCESSIBILITY 漏改了 check_unread.py 补上

8. 总结

这个系统的演进过程验证了一个原则:在 Linux 桌面自动化的深水区,最危险的不是复杂逻辑,而是你对底层机制(Qt/GTK 差异、D-Bus 进程模型、subprocess 的 fork 语义)的不完全理解。

三行代码的环境变量缺失,排查了三天。子进程调 LLM 挂死,表面是超时问题,根因在 D-Bus 资源继承。最终方案的核心不是「更复杂的重试逻辑」,而是「彻底隔离进程边界」。

如果你也在做类似的 Linux GUI 自动化,希望这几条教训能帮你少踩坑。


全文代码经过实际部署验证,运行在 Ubuntu 22.04 + Python 3.10 + AT-SPI2 环境下。

相关推荐
starrysky8108 小时前
AI Agent 长期记忆系统实战:Hindsight + vLLM 全本地 GPU 部署
angular.js
光影少年14 天前
大前端框架生态
前端·javascript·flutter·react.js·前端框架·鸿蒙·angular.js
shmily麻瓜小菜鸡1 个月前
在 VSCode 里遇到报红是因为 Angular 编译器无法识别
ide·vscode·angular.js
~ rainbow~2 个月前
前端转型全栈(二)——NestJS 入门指南:从 Angular 开发者视角理解后端架构
前端·javascript·angular.js
Keep Running *2 个月前
Angular_学习笔记
笔记·学习·angular.js
云原生指北2 个月前
超越 Copilot Chat:用 SDK 打造专属 AI Agent(实战四场景)
angular.js
云原生指北2 个月前
测试文章 #95 — 平台发布验证(51CTO/OSCHINA/Juejin)
angular.js
汪子熙2 个月前
TS2320 错误的本质、触发场景与在 Angular / RxJS 项目中的系统化应对
前端·javascript·angular.js
LeeYaMaster3 个月前
15个例子熟练异步框架 Zone.js
前端·angular.js