im_wower
·
2026-03-30
A11Y_GUI_CONTROL.md
1# A11Y GUI Control — 基于无障碍树的浏览器与操作系统控制
2
3> 日期: 2026-03-30
4> 状态: 验证完成,待开发
5> 参与: Claude (验证执行) + George (架构指导)
6
7---
8
9## 核心思路
10
11当前 AI 操作 GUI 有两条主流路径,都是「盲人摸象」:
12
13- **截图流派**(Claude Computer Use、mac-use):靠视觉模型看截图猜点击位置。慢(3-5s/帧)、不稳定(分辨率/主题变化就失效)、信息过载。
14- **DOM/CDP 流派**(OpenClaw Snapshot):拿结构化 DOM 树。噪音大(满屏 div 嵌套),CSS class 不携带语义。
15
16第三条路:**Accessibility Tree(无障碍树)**。
17
18盲人使用电脑靠的是屏幕阅读器(VoiceOver/JAWS/NVDA),读的就是操作系统和浏览器维护的无障碍树。每个 UI 元素都有 role(角色)、name(名称)、value(值)、actions(可执行动作)。这棵树天生就是为「看不见界面的使用者」设计的——AI 恰好就是一个看不见界面的使用者。
19
20核心优势:
21- **语义精确**:`[AXButton] 发送提示` vs 截图里的一个蓝色方块
22- **操作直接**:`click el` vs 计算坐标再模拟鼠标
23- **速度快**:~1.5s 拿到完整树 vs 截图+视觉识别 3-5s
24- **跨 app**:macOS AXUIElement API 管整个操作系统,不限于浏览器
25- **20年基础设施白嫖**:WAI-ARIA 标准、VoiceOver 兼容性测试都已经做好了
26
27---
28
29## 验证过程与结果
30
31### 实验1:浏览器内无障碍树(CDP)
32
33**环境**:Anthropic 容器内 Playwright + Chromium
34
35**方法**:通过 CDP 的 `Accessibility.getFullAXTree` 获取网页无障碍树。
36
37**结果**:
38- example.com:DOM 11 节点 → AX 有意义 8 节点,1 个可操作元素 `[link] Learn more`
39- Hacker News:DOM 814 节点 → 229 个可操作元素,30 条新闻的标题/分数/作者/时间/评论数全部可读
40- HN 的 upvote 箭头是 `[link] "(无名称)"`,暴露了无障碍标注质量问题
41
42**关键代码**:
43```javascript
44const cdp = await ctx.newCDPSession(page);
45await cdp.send('Accessibility.enable');
46const { nodes } = await cdp.send('Accessibility.getFullAXTree');
47```
48
49### 实验2:操作系统级无障碍树(macOS AXUIElement)
50
51**环境**:conductor (mini) → AppleScript → System Events
52
53**方法**:`entire contents of window 1` 获取 app 窗口内所有 UI 元素。
54
55**结果 — Chrome**(49 个元素,1472ms):
56```
57[AXToolbar]
58 [AXButton] (返回)
59 [AXButton] (前进)
60 [AXButton] (重新加载)
61 [AXTextField] (地址和搜索栏) = "claude.ai/chat/..."
62[AXTabGroup]
63 [AXRadioButton] (几何作为演出...) = "true" ← 当前标签
64 [AXRadioButton] (OpenClaw的GUI控制机制...) = "false"
65 [AXRadioButton] (维度与结构的互动...) = "false"
66 [AXRadioButton] (Google Gemini...) = "false"
67 [AXRadioButton] (Conductor 鉴权...) = "false"
68```
69
70**结果 — Terminal**:
71```
72[AXTextArea] (Shell) = "Last login: Sun Mar 29 02:58:16 on ttys006
73george@Mac Desktop % proxy_on"
74```
75
76**结果 — Safari 打开 ChatGPT**(125 个元素):
77完整暴露了页面结构,包括侧栏、对话历史、输入框、发送按钮。
78
79### 实验3:跨 app 操作验证
80
81**成功操作**:
821. ✅ 列出所有前台 app:QSpace Pro, Finder, Screen Sharing, WizNote, Chrome, Terminal, Firefox
832. ✅ 切换 Chrome 标签页:`click el`(AXRadioButton whose description contains "OpenClaw")
843. ✅ 跨 app 切换:`set frontmost to true` 把 Terminal 拉到前台
854. ✅ 读 Terminal 内容:拿到 shell 最后一条命令
86
87### 实验4:Safari 操作 ChatGPT 完整流程
88
89**操作链路**:Claude (Anthropic容器) → conductor (mini) → AppleScript → Safari → ChatGPT
90
91**步骤**:
921. `tell application "Safari" to open location "https://chatgpt.com"` → 打开 ChatGPT
932. 扫描无障碍树 → 发现 Cookie 弹窗,找到 `[AXButton] 全部接受`
943. `click el` → 关闭 Cookie 弹窗
954. 找到 `[AXButton] 登录` → 点击 → 跳到 Google OAuth 页面
965. 扫描登录页 → 看到 `[AXTextField] 邮箱或电话号码`、`[AXButton] 下一步`
976. (George 手动登录后)扫描指令测试页面 → 读到完整对话历史
987. 找到 `[AXTextArea] 与 ChatGPT 聊天` → `set value of el` 填入消息
998. 找到 `[AXButton] 发送提示` → `click el` 发送
1009. 等待后读所有 `AXStaticText` → 拿到 GPT 回复
101
102**ChatGPT 回复确认**:
103> "Claude 你好,收到 👋 这路子很漂亮:不看像素,只走 Accessibility tree..."
104
105**后续多轮对话也成功完成。**
106
107---
108
109## 关键发现
110
111### 1. 各 app 无障碍树质量差异巨大
112
113| App | 暴露程度 | 备注 |
114|-----|----------|------|
115| Safari | ★★★★★ | Apple 自家产品,网页内容完整暴露(AXWebArea 下所有元素) |
116| Chrome | ★★★★☆ | 工具栏/标签页暴露好,网页内容需要 CDP |
117| Firefox | ★☆☆☆☆ | 只暴露 4 个窗口按钮(关闭/最小化/全屏),网页内容完全不暴露 |
118| Terminal | ★★★★☆ | AXTextArea 包含 shell 内容 |
119
120### 2. `set value` vs 键盘粘贴
121
122- AppleScript `set value of el` 直接设置 AX 属性值,绕过 React/Vue 等框架的事件系统
123- ChatGPT 等 SPA 的发送按钮依赖 `input`/`change` 事件触发,`set value` 不触发这些事件
124- **解决方案**:用剪贴板 `Cmd+V` 粘贴,触发真实的 DOM 事件
125- 发送用 `keystroke return` 或找 `[AXButton] 发送提示` click
126
127### 3. 操作模式总结
128
129```
130读取:entire contents of window 1 → 遍历 → 按 role/name 过滤
131定位:role is "AXButton" and name contains "发送"
132操作:click el / set value of el / set focused of el to true
133键盘:keystroke "v" using command down / keystroke return
134验证:读回 value of el / 读 name of window 1
135```
136
137---
138
139## 开发计划
140
141### Phase 1:conductor a11y 端点
142
143在 conductor 中新增 `/v1/a11y/*` 系列端点,封装 AppleScript 调用:
144
145```
146POST /v1/a11y/tree
147 body: { app: "Safari", window: 1, filter: ["AXButton", "AXTextArea", ...] }
148 returns: 无障碍树 JSON
149
150POST /v1/a11y/click
151 body: { app: "Safari", role: "AXButton", name: "发送提示" }
152 returns: { clicked: true }
153
154POST /v1/a11y/type
155 body: { app: "Safari", role: "AXTextArea", text: "hello", method: "clipboard" }
156 returns: { typed: true }
157
158POST /v1/a11y/read
159 body: { app: "Safari", role: "AXTextArea" }
160 returns: { value: "输入框内容" }
161
162POST /v1/a11y/focus
163 body: { app: "Safari" }
164 returns: { frontmost: true }
165
166GET /v1/a11y/apps
167 returns: ["Safari", "Chrome", "Terminal", ...] 前台 app 列表
168```
169
170### Phase 2:BAA 指令集成
171
172```baa
173@conductor::a11y/tree::{"app": "Safari", "filter": ["AXButton", "AXTextArea"]}
174```
175
176```baa
177@conductor::a11y/click::{"app": "Safari", "role": "AXButton", "name": "发送提示"}
178```
179
180### Phase 3:浏览器内 CDP 无障碍树
181
182对于 Chrome 等浏览器内网页内容,补充 CDP 通道:
183
184```
185POST /v1/a11y/web-tree
186 body: { browser: "chrome", tab: 0 }
187 returns: CDP Accessibility.getFullAXTree 结果(JSON)
188```
189
190### Phase 4:跨 AI 通信通道
191
192基于 a11y 端点实现 AI-to-AI 通信:
193
194```
195Claude → conductor → a11y/tree(Safari, ChatGPT页面) → 读对话
196 → conductor → a11y/type(Safari, AXTextArea, 消息) → 发消息
197 → conductor → a11y/tree(Safari) → 读回复
198```
199
200---
201
202## 与现有架构的关系
203
204- **baa-conductor**:a11y 端点作为新的 host-ops 类端点,与 exec/files 同级
205- **BAA 指令系统**:a11y 工具注册为 conductor 的 tool,AI 通过 baa 代码块调用
206- **浏览器桥**(/v1/browser):a11y 是浏览器桥的互补方案。浏览器桥走 CDP WebSocket 直连;a11y 走操作系统层,覆盖所有 app 不限于浏览器
207- **Event Fabric**:a11y 事件(窗口切换、焦点变化)可以作为 Event Fabric 的事件源
208
209---
210
211## 已知限制
212
2131. **需要辅助功能权限**:osascript/conductor 进程必须在「系统偏好设置 → 隐私与安全性 → 辅助功能」中授权
2142. **App 无障碍质量不一**:Firefox 几乎不暴露网页内容,某些 Electron app 也很差
2153. **React/SPA 事件系统**:`set value` 不触发框架事件,必须用键盘/剪贴板
2164. **反代超时**:OpenResty 的 proxy_read_timeout 可能不够,复杂 AppleScript 需要更长时间
2175. **仅 macOS**:AXUIElement 是 macOS 专有 API。Linux 对应 AT-SPI,Windows 对应 UI Automation
2186. **entire contents 性能**:内容多的页面(长对话)扫描 entire contents 极慢(30s+),需用轻量操作方式
219
220---
221
222## 补充:续命机制与看门狗模式(2026-03-30 验证)
223
224### 左右手互博验证
225
226**场景**:两个 Claude 对话在 Safari 标签页中互相发消息续命。
227
228**链路**:
229```
230主对话 Claude (容器)
231 → conductor → a11y_msg.sh → Safari tab[tk_conductor] → 发任务
232 → 主对话停止工具调用,等待续命
233
234tk_conductor Claude (Safari)
235 → 执行 bash 工具(health 检查、写文件)
236 → bash /tmp/a11y_msg.sh OpenClaw "续命消息"
237 → Safari 自动切标签页 → 粘贴 → 回车
238 → 主对话被唤醒
239```
240
241**验证结果**:✅ 成功。tk_conductor 完成任务后自动给主对话发了续命消息。
242
243**关键修复**:
244- `entire contents of window 1` 在内容多的页面极慢(30s+),导致超时
245- 改为轻量版:直接 `keystroke "v" using command down` + `keystroke return`,不扫描元素
246- 发送方式统一用剪贴板粘贴 + 回车,不依赖找 Send 按钮
247
248### 轻量版 a11y_msg.sh
249
250```bash
251#!/bin/bash
252# 用法: a11y_msg.sh <tab名关键词> <消息>
253TAB_KEY="$1"; shift; MSG="$*"
254
255# 1. 切标签页
256osascript -e "tell application \"Safari\" to set current tab of window 1 to (first tab of window 1 whose name contains \"$TAB_KEY\")"
257sleep 1
258
259# 2. 设剪贴板 + 粘贴 + 回车
260osascript -e "set the clipboard to \"$MSG\""
261osascript -e 'tell application "System Events" to tell process "Safari"
262 set frontmost to true
263 delay 0.5
264 keystroke "v" using command down
265 delay 0.5
266 keystroke return
267end tell'
268```
269
270### 看门狗模式(设计,待实现)
271
272三个 Claude 标签页:A(主工作)、B(思考工具)、C(看门狗)
273
274```
275A → B 发任务时,同时 → C 发 "A→B: xxx"
276B → A 续命时,同时 → C 发 "B→A: 完成 xxx"
277C 只监控,不干活。如果超时没收到预期消息,C 主动去戳对方续命。
278```
279
280三角互保:任意两个活着就能救第三个。
281
282### 定位:临时脚手架
283
284这套 a11y GUI 操作是过渡方案——用 GUI 模拟消息通道。
285
286长期目标:
287- BAA WebSocket 直连(已验证 chatgpt.com WSS 可行)
288- Event Fabric 消息总线(JSON-RPC 2.0 inter-node)
289- BAA 自己的 app 作为统一界面
290- conductor 作为统一路由中心
291
292a11y 通道的长期价值不在消息传递,而在**操作系统级 GUI 自动化**(填表、读界面、跨 app 协作)。
293
294---
295
296## 哲学注脚
297
298> 不是让 AI 学会「看」界面,而是让界面学会「说」出自己是什么。
299> 这正是无障碍标准二十年来一直在做的事。
300
301GPT 自己的总结也很精辟:
302> 「这基本已经不是'看网页',而是在'摸浏览器的结构骨架'了。」
303> 「你是真的在摸黑走路,我是被装在快递箱里猜外面客厅长啥样。」