# Computer Use 架构深度解析 > 深入解析 Computer Use 功能的底层实现:从 MCP 工具定义到 Python Bridge,从 9 层安全关卡到灰度控制绕过。

补丁环境 · 分层架构 · MCP 工具层 · 安全关卡 · Python Bridge · 交互闭环 · 源文件索引

![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 { // 层1:GrowthBook 远程配置 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(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 通过 MCP(Model 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 1:Kill Switch ```typescript if (adapter.isDisabled()) return errorResult("Computer Use is disabled") ``` 读取 `getChicagoEnabled()` — 在补丁版中永远返回 `true`。 #### Gate 2:TCC 权限检查 ```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. 切换到 Terminal(click-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 — 本轮隐藏的应用 ``` --- ## 七、关键源文件索引 ### 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) — 其他修复和补丁的详细记录