这是一篇「踩坑实录」而非架构设计文档。每个方案都经历了至少一次推翻重来,最终沉淀为稳定运行的自动化系统。
1. 场景:为什么需要闪信自动化
闪信(WeChat Linux 版)放在一台无外接显示器的 Linux 服务器上运行,通过 Xvfb 虚拟显示 + AT-SPI 无障碍接口,实现无人值守的消息检测与自动回复。
几个核心需求:
- 7×24 消息检测,漏一条都不行
- 智能回复 --- 闲聊秒回,正事需调用 LLM 生成回复
- 全链路不稳定可自愈 --- 闪信崩溃、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=15到45→ 全部超时- 不同模型(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 环境下。