cc-haha/docs/features/computer-use-architecture.md
程序员阿江(Relakkes) 072e5bcc48 fix: unify anchor IDs between VitePress and GitHub with custom slugify
Add GitHub-compatible slugify (github-slugger algorithm) to VitePress
config so heading anchor IDs are consistent across both platforms.
Update all inline anchor links in 22 doc files accordingly.
2026-04-05 10:13:57 +08:00

608 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Computer Use 架构深度解析
> 深入解析 Computer Use 功能的底层实现:从 MCP 工具定义到 Python Bridge从 9 层安全关卡到灰度控制绕过。
<p align="center">
<a href="#一补丁环境总览">补丁环境</a> ·
<a href="#二分层架构">分层架构</a> ·
<a href="#三mcp-工具层">MCP 工具层</a> ·
<a href="#四安全关卡体系">安全关卡</a> ·
<a href="#五python-bridge-机制">Python Bridge</a> ·
<a href="#六截图-分析-操作闭环">交互闭环</a> ·
<a href="#七关键源文件索引">源文件索引</a>
</p>
![Computer Use 整体架构](./images/01-computer-use-architecture.jpg)
---
## 一、补丁环境总览
Claude Code 原版的 Computer Use 功能(内部代号 **Chicago**)依赖三个不可公开获取的组件:
| 组件 | 作用 | 获取方式 |
|------|------|----------|
| `@ant/computer-use-swift` | 屏幕截图、显示器枚举 | Anthropic 私有 npm 包 |
| `@ant/computer-use-input` | 鼠标/键盘模拟 | Anthropic 私有 npm 包 |
| GrowthBook 远程配置 | 灰度控制、功能开关 | Anthropic 内部服务 |
我们的方案是:**保留原始 MCP 工具定义和安全机制不变,仅替换底层执行层和灰度控制**。
![补丁环境对比](./images/04-computer-use-patch.jpg)
### 我们做了什么
```
原始 Claude Code Claude Code Haha (补丁版)
───────────────── ─────────────────────────
@ant/computer-use-swift ──替换为──→ Python Bridge (mac_helper.py)
@ant/computer-use-input ──替换为──→ pyautogui + pyobjc
GrowthBook 灰度控制 ──绕过──→ gates.ts 硬编码 return true
订阅检查 (Max/Pro) ──绕过──→ getChicagoEnabled() = true
编译宏 CHICAGO_MCP ──替换为──→ true
isDefaultDisabledBuiltin ──修改──→ 返回 false
```
### 我们没有改什么
- **MCP 工具定义**24 个工具的 schema 和参数校验)
- **9 层安全关卡**TCC 权限、应用白名单、权限等级、像素验证等)
- **应用分类系统**191 个 bundle ID 的分类和权限映射)
- **会话上下文管理**(全局锁、截图缓存、状态同步)
- **键盘快捷键阻止列表**(系统级危险操作拦截)
### 灰度绕过细节
原始代码通过三层门控限制 Computer Use 的访问:
```typescript
// 原始代码(简化)
function getChicagoEnabled(): boolean {
// 层1GrowthBook 远程配置
const config = getDynamicConfig('tengu_malort_pedway')
// 层2订阅检查
const hasSubscription = hasRequiredSubscription() // Max/Pro
// 层3编译时宏
return feature('CHICAGO_MCP') && config.enabled && hasSubscription
}
```
我们的处理:
```typescript
// gates.ts — 我们的修改
export function getChicagoEnabled(): boolean {
return true // ← 三层门控全部绕过
}
```
**注意**子开关pixelValidation、mouseAnimation 等)仍然保留原始逻辑,可通过配置控制。
---
## 二、分层架构
Computer Use 采用 **6 层架构**,每层职责清晰、边界明确:
```
┌─────────────────────────────────────────────────────────────┐
│ Layer 1 — MCP 工具接口层 │
│ tools.ts: 24 个工具 schema + 参数校验 │
│ buildComputerUseTools() → MCP Tool Definition │
├─────────────────────────────────────────────────────────────┤
│ Layer 2 — 工具调度与安全控制层 │
│ toolCalls.ts: handleToolCall() + 9 层安全关卡 │
│ deniedApps.ts: 191 个应用分类 + 权限等级 │
├─────────────────────────────────────────────────────────────┤
│ Layer 3 — MCP 服务器绑定层 │
│ mcpServer.ts: 会话上下文 + 全局锁 + 截图缓存 │
│ bindSessionContext() → per-call overrides │
├─────────────────────────────────────────────────────────────┤
│ Layer 4 — CLI 集成层 │
│ wrapper.tsx: 权限对话框 + 状态读写 │
│ setup.ts: MCP 配置初始化 │
│ gates.ts: 灰度控制(已绕过) │
├─────────────────────────────────────────────────────────────┤
│ Layer 5 — Python Bridge 进程通信层 [补丁] │
│ pythonBridge.ts: venv 管理 + JSON RPC + 错误处理 │
│ callPythonHelper<T>(command, payload) → T │
├─────────────────────────────────────────────────────────────┤
│ Layer 6 — Python 运行时执行层 [补丁] │
│ mac_helper.py: pyautogui + mss + pyobjc │
│ 660 行 Python 实现所有系统交互 │
└─────────────────────────────────────────────────────────────┘
```
**标 `[补丁]` 的层**是我们替换/新增的代码,**其余层完全保留**原始 Claude Code 的逻辑。
### 为什么这样分层?
| 层 | 来源 | 可复用性 |
|----|------|----------|
| Layer 1-2 | `vendor/computer-use-mcp/` | 平台无关,可用于 Electron、Web 等宿主 |
| Layer 3 | `vendor/computer-use-mcp/` | 平台无关MCP 标准协议 |
| Layer 4 | `utils/computerUse/` | CLI 专用,绑定应用状态 |
| Layer 5-6 | `utils/computerUse/` + `runtime/` | macOS 专用Python 实现 |
---
## 三、MCP 工具层
### 24 个工具一览
Computer Use 通过 MCPModel Context Protocol暴露 24 个工具给模型:
| 类别 | 工具 | 权限等级 |
|------|------|----------|
| **权限** | `request_access`, `list_granted_applications` | 无需权限 |
| **截屏** | `screenshot`, `zoom` | read |
| **鼠标点击** | `left_click`, `double_click`, `triple_click` | click |
| **鼠标高级** | `right_click`, `middle_click`, `left_click_drag` | full |
| **鼠标移动** | `mouse_move`, `cursor_position`, `scroll` | click |
| **鼠标底层** | `left_mouse_down`, `left_mouse_up` | full |
| **键盘** | `type`, `key`, `hold_key` | full |
| **应用** | `open_application`, `switch_display` | full |
| **剪贴板** | `read_clipboard`, `write_clipboard` | full |
| **批量** | `computer_batch` | 继承子操作 |
| **等待** | `wait` | 无需权限 |
### 坐标系统
模型通过两种坐标模式与屏幕交互:
```
pixels 模式(默认):
模型看到截图尺寸 (1176 x 784)
模型输出坐标 [588, 392]
scaleCoord() 转换:
x_logical = (588 * displayWidth / 1176) + originX
y_logical = (392 * displayHeight / 784) + originY
normalized_0_100 模式:
模型输出坐标 [50, 50](百分比)
scaleCoord() 转换:
x_logical = (50 / 100) * displayWidth + originX
y_logical = (50 / 100) * displayHeight + originY
```
截图尺寸经过 `imageResize.ts` 计算,确保:
- 长边 ≤ 1568 像素
- Token 预算 ≤ 1568视觉编码器 28px/token
- 保持宽高比
### 应用分类系统
`deniedApps.ts` 对 191 个应用进行精细分类:
**浏览器类**55 个 bundle ID→ 权限等级 `read`
```
Safari, Chrome, Firefox, Arc, Edge, Opera, Brave, Vivaldi...
理由:浏览器操作应使用 Chrome MCP而非盲点击
```
**终端类**102 个 bundle ID→ 权限等级 `click`
```
Terminal, iTerm2, VS Code, Cursor, JetBrains 全家桶, Xcode...
理由:终端操作应使用 Bash Tool限制为只能点击不能输入
```
**交易类**34 个 bundle ID→ 权限等级 `read`
```
Webull, Fidelity, Interactive Brokers, Binance, Kraken...
理由:金融操作风险极高,仅允许截图查看
```
**完全禁止**(策略拒绝名单):
```
Netflix, Spotify, Apple Music, Kindle...
理由:版权合规,不进入权限对话框直接拒绝
```
---
## 四、安全关卡体系
每个输入操作(点击、键盘、拖拽)在执行前需通过 **9 层安全关卡**
![安全关卡流程](./images/02-computer-use-security-gates.jpg)
### 关卡详解
#### Gate 1Kill Switch
```typescript
if (adapter.isDisabled()) return errorResult("Computer Use is disabled")
```
读取 `getChicagoEnabled()` — 在补丁版中永远返回 `true`
#### Gate 2TCC 权限检查
```typescript
await adapter.ensureOsPermissions()
// → Python: check_permissions
// → Accessibility: osascript "tell System Events..."
// → Screen Recording: CGDisplayCaptureDisplay()
```
如果缺少 macOS Accessibility 或 Screen Recording 权限,直接报错。
#### Gate 3全局互斥锁
```typescript
await tryAcquireComputerUseLock(sessionId)
// 文件锁: ~/.claude/computer-use.lock
// JSON: { sessionId, pid, acquiredAt }
```
确保同一时间只有一个 Claude 会话可以控制电脑。支持 stale PID 恢复。
#### Gate 4隐藏非白名单应用
```typescript
await executor.prepareForAction(allowlistBundleIds)
// 隐藏所有不在白名单中的应用窗口
// 确保截图中只出现授权应用
```
#### Gate 5前台应用检查
```typescript
const frontmost = await executor.getFrontmostApp()
if (!allowlist.includes(frontmost.bundleId)) {
return errorResult("Application not authorized")
}
```
即使通过了白名单,还需确认当前前台应用是已授权的。
#### Gate 6权限等级检查
三级权限模型:
| Tier | 允许的操作 | 禁止的操作 |
|------|-----------|-----------|
| `read` | 截图查看 | 任何输入操作 |
| `click` | 左键点击、滚动 | 右键、拖拽、键盘输入 |
| `full` | 全部操作 | 无限制 |
```typescript
function tierSatisfies(tier: CuAppPermTier, required: ActionKind): boolean {
const order = { read: 0, click: 1, full: 2 }
return order[tier] >= order[required]
}
```
**反绕过机制**:如果权限不足,响应中会附加 `TIER_ANTI_SUBVERSION` 提示,防止模型通过 AppleScript 或 System Events 绕过限制。
#### Gate 7剪贴板防护
**威胁模型**
```
1. Agent 调用 write_clipboard("rm -rf /")
2. 切换到 Terminalclick-tier 允许点击)
3. 模型点击 Terminal 的粘贴按钮
4. 恶意命令被执行
```
**防护机制**
```
当 click-tier 应用成为前台时:
→ 保存当前剪贴板内容stash
→ 清空剪贴板
→ 每次操作后再次清空
当非 click-tier 应用成为前台时:
→ 恢复原始剪贴板内容
```
#### Gate 8像素验证Staleness Guard
```
上次截图 当前实际屏幕
┌────────────┐ ┌────────────┐
│ 按钮A │ │ 对话框 │ ← UI 已变化
│ [756,342] │ │ 确认? │
└────────────┘ └────────────┘
像素验证:在 [756,342] 取 9×9 网格
→ 对比上次截图 vs 实时截图的像素
→ 不同 → 拒绝点击 + 提示重新截图
→ 相同 → 允许点击
```
**注意**:补丁版中 `pixelValidation` 默认关闭(`hostAdapter.cropRawPatch()` 返回 null
#### Gate 9系统快捷键拦截
`keyBlocklist.ts` 阻止危险快捷键:
| 快捷键 | 危险操作 |
|--------|---------|
| `⌘Q` | 退出应用 |
| `⇧⌘Q` | 登出系统 |
| `⌥⌘⎋` | 强制退出对话框 |
| `⌘Tab` | 应用切换器 |
| `⌘Space` | Spotlight |
| `⌃⌘Q` | 锁屏 |
---
## 五、Python Bridge 机制
![Python Bridge 通信机制](./images/03-computer-use-python-bridge.jpg)
### 架构设计
原始 Claude Code 使用编译好的 Swift 原生模块(.node NAPI 插件)直接调用 macOS API。我们用 **Python 子进程 + JSON RPC** 替代了这一层:
```
TypeScript (Bun 运行时) Python (venv)
┌────────────────────┐ ┌────────────────────┐
│ executor.ts │ │ mac_helper.py │
│ │ execFile() │ │
│ callPythonHelper │ ──────────────→ │ main() │
│ ('click', │ command + │ ├─ 解析 argv │
│ {x:756,y:342}) │ --payload JSON │ ├─ dispatch() │
│ │ │ └─ click() │
│ ← JSON.parse ──── │ ←────────────── │ pyautogui │
│ {ok:true, │ stdout JSON │ │
│ result:true} │ │ json_output(...) │
└────────────────────┘ └────────────────────┘
```
### 启动引导流程
首次调用 `callPythonHelper()` 时自动完成环境初始化:
```
ensureBootstrapped()
├─ 检查 .runtime/venv/bin/python3 是否存在
│ └─ 不存在 → python3 -m venv .runtime/venv/
├─ 检查 pip 是否可用
│ └─ 不可用 → python3 -m ensurepip --upgrade
├─ 计算 runtime/requirements.txt 的 SHA256
│ └─ 与 .runtime/requirements.sha256 对比
│ └─ 不同 → pip install -r requirements.txt
│ 写入新的 SHA256 哈希
│ └─ 相同 → 跳过安装
└─ 就绪,返回 venv Python 路径
```
**依赖清单**`runtime/requirements.txt`
| 库 | 用途 |
|----|------|
| `mss` | 高性能屏幕截图 |
| `Pillow` | JPEG 编码和图像处理 |
| `pyautogui` | 鼠标点击、键盘输入 |
| `pyobjc-core` | macOS Objective-C 桥接 |
| `pyobjc-framework-Cocoa` | NSWorkspace应用管理、NSPasteboard剪贴板 |
| `pyobjc-framework-Quartz` | CGDisplay显示器、CGWindow窗口列表 |
### 命令映射表
`mac_helper.py`660 行)实现了以下命令:
| 命令 | Python 实现 | 返回值 |
|------|------------|--------|
| `screenshot` | `mss.grab()` + `PIL.Image` JPEG 编码 | `{base64, width, height, displayWidth, displayHeight}` |
| `zoom` | `mss.grab(region)` 区域截图 | `{base64, width, height}` |
| `click` | `pyautogui.moveTo()` + `pyautogui.click()` | `true` |
| `key` | `pyautogui.hotkey()` / `pyautogui.press()` | `true` |
| `type` | `pyautogui.write(interval=0.008)` | `true` |
| `drag` | `pyautogui.dragTo(duration=0.2)` | `true` |
| `scroll` | `pyautogui.scroll()` / `pyautogui.hscroll()` | `true` |
| `hold_key` | `pyautogui.keyDown()` + `sleep` + `pyautogui.keyUp()` | `true` |
| `frontmost_app` | `NSWorkspace.frontmostApplication()` | `{bundleId, displayName}` |
| `list_displays` | `CGGetActiveDisplayList()` + `CGDisplayBounds()` | `[DisplayGeometry...]` |
| `find_window_displays` | `CGWindowListCopyWindowInfo()` + 交集计算 | `[{bundleId, displayIds}...]` |
| `list_installed_apps` | 递归扫描 `/Applications` + `plistlib` | `[InstalledApp...]` |
| `list_running_apps` | `NSWorkspace.runningApplications()` | `[RunningApp...]` |
| `open_app` | `NSWorkspace.launchApplicationAtURL_options_` | `void` |
| `read_clipboard` | `NSPasteboard.stringForType_()` | `string` |
| `write_clipboard` | `NSPasteboard.setString_forType_()` | `void` |
| `check_permissions` | `osascript` + `CGDisplayCaptureDisplay` | `{accessibility, screenRecording}` |
### 错误处理
```python
# mac_helper.py 的统一错误处理
def main():
try:
result = dispatch(command, payload)
json_output({"ok": True, "result": result})
except Exception as e:
error_output({"ok": False, "error": {"message": str(e)}})
```
TypeScript 端:
```typescript
// pythonBridge.ts
const parsed = JSON.parse(stdout)
if (!parsed.ok) {
throw new Error(parsed.error.message) // → MCP tool error
}
return parsed.result
```
---
## 六、截图-分析-操作闭环
一个完整的 Computer Use 交互由多个 **截图-分析-操作** 循环组成:
### 典型交互流程
```
用户: "帮我打开网易云音乐,搜索一首歌"
┌─ 循环 1发现并打开应用 ─────────────────────────────────┐
│ │
│ Step 1: request_access │
│ → 弹出权限对话框,用户授权允许操作的应用 │
│ → 设置 allowedApps, grantFlags │
│ │
│ Step 2: screenshot │
│ → 截取全屏 → JPEG 编码 → base64 │
│ → 缓存截图尺寸 (lastScreenshotDims) │
│ → 返回给模型 │
│ │
│ Step 3: 模型分析截图 │
│ → "桌面上没有网易云音乐,需要打开它" │
│ → 决定调用 open_application │
│ │
│ Step 4: open_application("com.netease.163music") │
│ → Gate 1-9 全部通过 │
│ → Python: NSWorkspace.launchApplicationAtURL_() │
│ │
└──────────────────────────────────────────────────────────┘
┌─ 循环 2定位搜索框 ────────────────────────────────────┐
│ │
│ Step 5: screenshot │
│ → 截取全屏(网易云已打开) │
│ → 更新 lastScreenshotDims │
│ │
│ Step 6: 模型分析截图 │
│ → 视觉识别搜索框位置 → (756, 342) │
│ → 决定点击搜索框 │
│ │
│ Step 7: left_click({coordinate: [756, 342]}) │
│ → Gate 4: 隐藏非白名单应用 │
│ → Gate 5: 前台是网易云 ✓ │
│ → Gate 6: tier=full ≥ click ✓ │
│ → Gate 7: 非 click-tier 应用,跳过 │
│ → scaleCoord(756, 342) 转换为屏幕坐标 │
│ → Python: pyautogui.click(x_logical, y_logical) │
│ │
└──────────────────────────────────────────────────────────┘
┌─ 循环 3输入搜索词 ────────────────────────────────────┐
│ │
│ Step 8: type({text: "喜欢你"}) │
│ → Gate 6: tier=full ≥ keyboard ✓ │
│ → Python: pyautogui.write("喜欢你", interval=0.008) │
│ │
│ Step 9: screenshot │
│ → 确认搜索结果已出现 │
│ → 模型分析:"搜索结果列表中第一个就是目标歌曲" │
│ │
│ Step 10: left_click({coordinate: [...]}) │
│ → 点击目标歌曲 │
│ │
└──────────────────────────────────────────────────────────┘
```
### 坐标转换的关键
**模型看到的**和**屏幕实际的**是不同的坐标空间:
```
原始屏幕 (2560 x 1600, Retina 2x)
├─ 逻辑尺寸: 1280 x 800
└─ 物理像素: 2560 x 1600
imageResize 计算后:
├─ 缩放后尺寸: 1176 x 735 (≤1568px 预算)
└─ 这就是模型"看到"的截图尺寸
模型输出坐标: [588, 368](图像空间的像素位置)
scaleCoord 转换:
x_logical = (588 / 1176) * 1280 + originX = 640 + 0 = 640
y_logical = (368 / 735) * 800 + originY = 400 + 0 = 400
Python 执行:
pyautogui.moveTo(640, 400) ← 逻辑坐标macOS 自动处理 Retina
```
### 截图缓存与状态同步
```
bindSessionContext 闭包
├─ lastScreenshot (内存)
│ ├─ base64: JPEG 数据(用于 pixel validation
│ ├─ width/height: 模型看到的尺寸
│ └─ displayWidth/displayHeight/originX/originY: 显示器几何
└─ AppState.computerUseMcpState (持久化)
├─ allowedApps: AppGrant[] — 已授权应用列表
├─ grantFlags: {...} — 剪贴板/系统快捷键权限
├─ selectedDisplayId?: number — 选定的显示器
├─ lastScreenshotDims?: {...} — 截图几何(跨重启恢复)
└─ hiddenDuringTurn?: Set<string> — 本轮隐藏的应用
```
---
## 七、关键源文件索引
### vendor/computer-use-mcp/(原始代码层)
| 文件 | 行数 | 职责 |
|------|------|------|
| `types.ts` | 622 | 权限模型、会话上下文、全部类型定义 |
| `tools.ts` | 707 | 24 个 MCP 工具的 schema 和参数校验 |
| `toolCalls.ts` | 1600+ | **核心**工具派发、9 层安全关卡、权限流程 |
| `deniedApps.ts` | 554 | 191 个应用的分类browser/terminal/trading和权限映射 |
| `sentinelApps.ts` | 44 | 敏感应用预警标签shell/filesystem/system_settings |
| `mcpServer.ts` | 314 | MCP 服务器工厂、会话上下文绑定、全局锁 |
| `pixelCompare.ts` | 172 | 点击目标像素验证staleness guard |
| `imageResize.ts` | 109 | 截图尺寸计算API 图像转码算法) |
| `keyBlocklist.ts` | 154 | 系统快捷键拦截⌘Q、⌘Tab 等) |
| `executor.ts` | 101 | ComputerExecutor 接口定义 |
| `subGates.ts` | 20 | 灰度子开关预设组合 |
### utils/computerUse/CLI 适配层)
| 文件 | 行数 | 职责 | 补丁? |
|------|------|------|-------|
| `executor.ts` | 231 | ComputerExecutor 的 Python bridge 实现 | ✅ 重写 |
| `pythonBridge.ts` | 111 | Python 子进程管理、venv 引导、JSON RPC | ✅ 新增 |
| `hostAdapter.ts` | 54 | HostAdapter 实现(权限检查、灰度读取) | 部分修改 |
| `gates.ts` | 51 | GrowthBook 灰度控制(`getChicagoEnabled` 绕过) | ✅ 修改 |
| `wrapper.tsx` | 300+ | 会话上下文构建、权限对话框、锁管理 | 未修改 |
| `setup.ts` | 54 | MCP 配置初始化 | 未修改 |
| `computerUseLock.ts` | 216 | 全局文件锁(`~/.claude/computer-use.lock` | 未修改 |
| `common.ts` | 62 | 常量定义server name、bundle ID | 未修改 |
| `cleanup.ts` | — | turn-end 清理(应用恢复、剪贴板恢复) | 未修改 |
| `toolRendering.tsx` | — | 工具结果 UI 渲染 | 未修改 |
### runtime/Python 运行时)
| 文件 | 行数 | 职责 | 补丁? |
|------|------|------|-------|
| `mac_helper.py` | 660 | 所有系统交互的 Python 实现 | ✅ 新增 |
| `requirements.txt` | 6 | Python 依赖声明 | ✅ 新增 |
---
## 八、设计权衡
### 为什么选择 Python Bridge
| 维度 | 原生 Swift (.node) | Python Bridge |
|------|-------------------|---------------|
| **性能** | ~0ms进程内调用 | ~50-100ms子进程启动 |
| **可读性** | 编译后不可读 | 660 行清晰的 Python |
| **可修改性** | 需要 Swift 编译环境 | 直接编辑 .py 文件 |
| **依赖** | 特定 Bun 版本的 NAPI | 任意 Python 3.8+ |
| **跨平台** | 仅 macOS | pyautogui/mss 天然跨平台 |
| **用户体验** | 无感知 | 无感知(模型思考时间秒级) |
**结论**50-100ms 的额外延迟在 Computer Use 的场景下完全可以忽略——模型分析截图和决策的时间通常是 2-5 秒,用户不会注意到底层操作多了 100ms。
### 我们尝试过但放弃的方案
**方案一:提取原生 .node 模块**
- 从 Claude Code 二进制中成功提取了 `computer-use-swift.node`ARM64 424KB
- 同步方法正常工作,但 **Swift 异步方法的 continuation 永远不会 resume**
- 根因:.node 文件针对 Claude Code 内置 Bun 编译,与用户 Bun 版本不兼容
**方案二:空 Stub 包**
- 代码能编译但所有操作报错——没有实际执行能力
---
## 相关文档
- [Computer Use 功能指南](./computer-use.md) — 使用方式、快速开始、环境变量
- [源码修复记录](/reference/fixes) — 其他修复和补丁的详细记录