Initialize cc-haha project mirror.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,607 @@
|
||||
# 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>
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 一、补丁环境总览
|
||||
|
||||
Claude Code 原版的 Computer Use 功能(内部代号 **Chicago**)依赖三个不可公开获取的组件:
|
||||
|
||||
| 组件 | 作用 | 获取方式 |
|
||||
|------|------|----------|
|
||||
| `@ant/computer-use-swift` | 屏幕截图、显示器枚举 | Anthropic 私有 npm 包 |
|
||||
| `@ant/computer-use-input` | 鼠标/键盘模拟 | Anthropic 私有 npm 包 |
|
||||
| GrowthBook 远程配置 | 灰度控制、功能开关 | Anthropic 内部服务 |
|
||||
|
||||
我们的方案是:**保留原始 MCP 工具定义和安全机制不变,仅替换底层执行层和灰度控制**。
|
||||
|
||||

|
||||
|
||||
### 我们做了什么
|
||||
|
||||
```
|
||||
原始 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<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 通过 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 层安全关卡**:
|
||||
|
||||

|
||||
|
||||
### 关卡详解
|
||||
|
||||
#### 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 机制
|
||||
|
||||

|
||||
|
||||
### 架构设计
|
||||
|
||||
原始 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) — 其他修复和补丁的详细记录
|
||||
@@ -0,0 +1,322 @@
|
||||
# Computer Use 功能指南
|
||||
|
||||
|
||||
> **魔改说明**:本功能是基于 Claude Code 泄露源码中的 Computer Use(内部代号 "Chicago")进行的**深度改造版本**。官方实现依赖 Anthropic 内部私有原生模块(`@ant/computer-use-swift`、`@ant/computer-use-input`),无法公开获取。我们**替换了整个底层操作层**,使用 Python bridge 实现所有系统交互——macOS 使用 `pyautogui` + `mss` + `pyobjc`,Windows 使用 `pyautogui` + `mss` + `win32gui` + `psutil`,使得任何人都可以在 macOS 和 Windows 上运行 Computer Use 功能。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [功能简介](#功能简介)
|
||||
- [支持的设备与平台](#支持的设备与平台)
|
||||
- [工作原理](#工作原理)
|
||||
- [快速开始](#快速开始)
|
||||
- [使用方式](#使用方式)
|
||||
- [安全机制](#安全机制)
|
||||
- [环境变量](#环境变量)
|
||||
- [技术架构详解](#技术架构详解)
|
||||
- [我们尝试过的方案](#我们尝试过的方案)
|
||||
- [已知限制](#已知限制)
|
||||
- [参考项目与致谢](#参考项目与致谢)
|
||||
|
||||
---
|
||||
|
||||
## 功能简介
|
||||
|
||||
Computer Use 让 AI 模型能够**直接控制你的电脑**——截屏、移动鼠标、点击按钮、输入文字、管理应用窗口。
|
||||
|
||||
支持的操作(共 24 个 MCP 工具):
|
||||
|
||||
| 类别 | 工具 |
|
||||
|------|------|
|
||||
| 截屏 | `screenshot`、`zoom` |
|
||||
| 鼠标 | `left_click`、`right_click`、`middle_click`、`double_click`、`triple_click`、`left_click_drag`、`mouse_move`、`left_mouse_down`、`left_mouse_up`、`cursor_position`、`scroll` |
|
||||
| 键盘 | `type`、`key`、`hold_key` |
|
||||
| 应用 | `open_application`、`switch_display` |
|
||||
| 权限 | `request_access`、`list_granted_applications` |
|
||||
| 剪贴板 | `read_clipboard`、`write_clipboard` |
|
||||
| 其他 | `wait`、`computer_batch` |
|
||||
|
||||
---
|
||||
|
||||
## 支持的设备与平台
|
||||
|
||||
| 平台 | 芯片 | 状态 | 说明 |
|
||||
|------|------|------|------|
|
||||
| macOS | Apple Silicon (M1/M2/M3/M4) | ✅ 完整支持 | 推荐平台 |
|
||||
| macOS | Intel x86_64 | ✅ 完整支持 | |
|
||||
| Windows | x64 | ✅ 完整支持 | 使用 `win32gui` + `psutil` + `pyperclip` + `screeninfo` 替代 macOS 专有 API |
|
||||
| Linux | 任意 | ⚠️ 理论可行 | 需替换平台专有部分为 `wmctrl` + `xdotool`,当前未适配 |
|
||||
|
||||
### 运行环境要求
|
||||
|
||||
- [Bun](https://bun.sh) >= 1.1.0
|
||||
- Python >= 3.8(首次运行自动创建 venv 并安装依赖)
|
||||
- **macOS**:系统权限 Accessibility(辅助功能)+ Screen Recording(屏幕录制)
|
||||
- **Windows**:无需额外权限配置
|
||||
|
||||
---
|
||||
|
||||
## 工作原理
|
||||
|
||||
Computer Use 的核心是一个**截图-分析-操作**的闭环:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ AI 模型(Claude / 其他 Anthropic 协议模型) │
|
||||
│ │
|
||||
│ 1. 收到用户请求 "打开网易云搜索喜欢你" │
|
||||
│ 2. 调用 screenshot 工具 → 收到屏幕截图 │
|
||||
│ 3. 模型分析截图像素,识别 UI 元素位置 │
|
||||
│ → "搜索框在 (756, 342)" │
|
||||
│ 4. 调用 left_click { coordinate: [756, 342] } │
|
||||
│ 5. 调用 type { text: "喜欢你" } │
|
||||
│ 6. 再次 screenshot → 确认结果 → 下一步... │
|
||||
└──────────────┬───────────────────────────────┘
|
||||
│ MCP Tool Call
|
||||
▼
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ TypeScript 工具层 │
|
||||
│ (vendor/computer-use-mcp) │
|
||||
│ │
|
||||
│ - 安全检查(应用白名单、TCC 权限) │
|
||||
│ - 坐标模式转换(pixels / normalized) │
|
||||
│ - 工具分发 → executor │
|
||||
└──────────────┬───────────────────────────────┘
|
||||
│ callPythonHelper()
|
||||
▼
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ Python Bridge │
|
||||
│ macOS: runtime/mac_helper.py │
|
||||
│ Windows: runtime/win_helper.py │
|
||||
│ │
|
||||
│ pyautogui.click(756, 342) ← 鼠标控制 │
|
||||
│ mss.grab(monitor) ← 截图 │
|
||||
│ macOS: NSWorkspace.open() ← 应用管理 │
|
||||
│ Windows: win32gui / psutil ← 应用管理 │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**关键:坐标分析完全由模型的视觉能力完成**——模型"看"截图就像人看屏幕一样,直接从像素中识别按钮、输入框等 UI 元素的位置。
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
### 2. 确保 Python 3 可用
|
||||
|
||||
```bash
|
||||
python3 --version # 需要 >= 3.8
|
||||
```
|
||||
|
||||
> Python 依赖会在首次使用 Computer Use 时**自动安装**到 `.runtime/venv/`,无需手动操作。
|
||||
|
||||
### 3. 授予 macOS 权限
|
||||
|
||||
#### Accessibility(辅助功能)
|
||||
|
||||
```bash
|
||||
open "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
|
||||
```
|
||||
|
||||
将你的**终端应用**(如 iTerm、Terminal、Ghostty 等)添加到允许列表。
|
||||
|
||||
#### Screen Recording(屏幕录制)
|
||||
|
||||
```bash
|
||||
open "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"
|
||||
```
|
||||
|
||||
同样添加你的终端应用。授权后可能需要**重启终端**。
|
||||
|
||||
### 4. 启动
|
||||
|
||||
```bash
|
||||
./bin/claude-haha
|
||||
```
|
||||
|
||||
### 5. 使用
|
||||
|
||||
在对话中用自然语言请求即可:
|
||||
|
||||
```
|
||||
> 帮我打开网易云音乐,搜索一首歌
|
||||
> 截个屏看看当前桌面
|
||||
> 帮我在 VS Code 里打开终端
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用方式
|
||||
|
||||
首次使用 Computer Use 时,系统会弹出**应用授权对话框**,你需要选择允许 Claude 操作的应用。
|
||||
|
||||
- 模型会先调用 `request_access` 请求权限
|
||||
- 你在终端中确认允许哪些应用
|
||||
- 之后模型就可以截图、点击、输入了
|
||||
|
||||
---
|
||||
|
||||
## 安全机制
|
||||
|
||||
| 机制 | 说明 |
|
||||
|------|------|
|
||||
| **应用白名单** | 每次会话需要明确授权允许操作的应用 |
|
||||
| **并发保护** | 同一时间只有一个 Claude 会话可使用 Computer Use(文件锁机制) |
|
||||
| **剪贴板保护** | 通过剪贴板输入文本时会自动保存和恢复原始剪贴板内容 |
|
||||
| **操作确认** | 敏感操作(如系统快捷键)需要额外授权 |
|
||||
|
||||
> 注意:由于底层改为 Python bridge,原生方案中的全局 Escape 快捷键中止和操作前自动隐藏应用功能暂不可用。可使用 `Ctrl+C` 中止。
|
||||
|
||||
---
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `CLAUDE_COMPUTER_USE_ENABLED` | `1` | 设为 `0` 可禁用 Computer Use |
|
||||
| `CLAUDE_COMPUTER_USE_COORDINATE_MODE` | `pixels` | 坐标模式:`pixels` 或 `normalized_0_100` |
|
||||
| `CLAUDE_COMPUTER_USE_CLIPBOARD_PASTE` | `1` | 是否启用剪贴板粘贴输入 |
|
||||
| `CLAUDE_COMPUTER_USE_MOUSE_ANIMATION` | `1` | 是否启用鼠标动画 |
|
||||
| `CLAUDE_COMPUTER_USE_HIDE_BEFORE_ACTION` | `0` | 操作前是否隐藏其他窗口 |
|
||||
| `CLAUDE_COMPUTER_USE_DEBUG` | `0` | 调试模式 |
|
||||
|
||||
---
|
||||
|
||||
## 技术架构详解
|
||||
|
||||
### 整体分层
|
||||
|
||||
```
|
||||
src/
|
||||
├── vendor/computer-use-mcp/ ← MCP 工具定义与分发(12 个文件)
|
||||
│ ├── tools.ts ← 24 个工具的 schema 定义
|
||||
│ ├── toolCalls.ts ← 安全检查 + 工具分发
|
||||
│ ├── mcpServer.ts ← MCP 服务器创建
|
||||
│ ├── types.ts ← 全部类型定义
|
||||
│ └── ...
|
||||
├── utils/computerUse/
|
||||
│ ├── executor.ts ← 执行器(调用 Python bridge)
|
||||
│ ├── pythonBridge.ts ← Python 子进程管理
|
||||
│ ├── hostAdapter.ts ← 权限检查适配器
|
||||
│ ├── gates.ts ← 功能开关(已绕过灰度)
|
||||
│ ├── wrapper.tsx ← MCP 工具覆写层
|
||||
│ ├── setup.ts ← MCP 配置初始化
|
||||
│ └── ...
|
||||
└── runtime/
|
||||
├── mac_helper.py ← macOS Python 实现
|
||||
├── win_helper.py ← Windows Python 实现
|
||||
├── requirements.txt ← macOS Python 依赖
|
||||
└── requirements-win.txt ← Windows Python 依赖
|
||||
```
|
||||
|
||||
### 灰度控制绕过
|
||||
|
||||
官方 Claude Code 中 Computer Use 通过三层门控限制访问:
|
||||
|
||||
| 层级 | 原始机制 | 我们的处理 |
|
||||
|------|----------|-----------|
|
||||
| 编译时 | `feature('CHICAGO_MCP')` (Bun 编译宏) | 替换为 `true` |
|
||||
| 订阅检查 | `hasRequiredSubscription()` (Max/Pro) | `getChicagoEnabled()` 直接返回 `true` |
|
||||
| 远程配置 | GrowthBook `tengu_malort_pedway` | 同上,不再依赖远程配置 |
|
||||
| 默认禁用 | `isDefaultDisabledBuiltin('computer-use')` | `isDefaultDisabledBuiltin()` 返回 `false` |
|
||||
|
||||
### Python Bridge 工作机制
|
||||
|
||||
```typescript
|
||||
// pythonBridge.ts
|
||||
async function callPythonHelper<T>(command: string, payload: object): Promise<T> {
|
||||
await ensureBootstrapped() // 首次调用自动创建 venv + pip install
|
||||
|
||||
// 调用: python3 runtime/mac_helper.py <command> --payload '{...}'
|
||||
const result = execFile(pythonBin, ['mac_helper.py', command, '--payload', JSON.stringify(payload)])
|
||||
|
||||
return JSON.parse(result.stdout) // { ok: true, result: T }
|
||||
}
|
||||
```
|
||||
|
||||
首次运行自动完成:
|
||||
1. 创建 Python 虚拟环境 (`.runtime/venv/`)
|
||||
2. 安装 pip
|
||||
3. 按平台安装依赖:
|
||||
- **macOS**: `mss`, `Pillow`, `pyautogui`, `pyobjc-*`(`requirements.txt`)
|
||||
- **Windows**: `mss`, `Pillow`, `pyautogui`, `win32gui`, `psutil`, `pyperclip`, `screeninfo`(`requirements-win.txt`)
|
||||
4. SHA256 哈希验证(仅 requirements 变更时重新安装)
|
||||
|
||||
---
|
||||
|
||||
## 我们尝试过的方案
|
||||
|
||||
### 方案一:从 Claude Code 二进制提取原生 .node 模块 ❌
|
||||
|
||||
**思路**:从已安装的 Claude Code 二进制 (`~/.local/share/claude/versions/2.1.91`,189MB Mach-O) 中定位并提取嵌入的原生 NAPI 模块。
|
||||
|
||||
**实施**:
|
||||
- 成功从 Bun `$bunfs` 虚拟文件系统中提取了 `computer-use-swift.node` (ARM64 424KB + x64 430KB) 和 `computer-use-input.node` (ARM64 836KB + x64 821KB)
|
||||
- 同步方法(TCC 权限检查、显示枚举)正常工作
|
||||
- 创建了 npm 包装包并通过 workspace 注册
|
||||
|
||||
**失败原因**:
|
||||
- Swift 异步方法(`screenshot.captureExcluding`)的 continuation 永远不会 resume
|
||||
- 根因:提取的 .node 文件是针对 Claude Code 内置的 Bun 运行时编译的,与用户系统的 Bun 版本的 N-API 异步实现不兼容
|
||||
- 错误信息:`SWIFT TASK CONTINUATION MISUSE: captureScreenWithExclusion leaked its continuation without resuming it`
|
||||
|
||||
### 方案二:创建空 Stub 包 ❌
|
||||
|
||||
**思路**:为 `@ant/computer-use-mcp`、`@ant/computer-use-input`、`@ant/computer-use-swift` 创建最小化的 stub 包,使代码能编译加载。
|
||||
|
||||
**失败原因**:代码能编译但 MCP 服务器注册后无法执行任何实际操作——截图、点击等全部报错。
|
||||
|
||||
### 方案三:Python Bridge 替代原生模块 ✅(当前方案)
|
||||
|
||||
**思路**:参考 [wimi321/macos-computer-use-skill](https://github.com/wimi321/macos-computer-use-skill),用 Python 子进程替代所有原生模块调用。
|
||||
|
||||
**优势**:
|
||||
- 零二进制依赖,不依赖特定 Bun/Node 版本
|
||||
- 纯 Python 实现,首次运行自动安装
|
||||
- 截图、鼠标、键盘、应用管理全部可用
|
||||
- macOS ARM64 + x86_64 均支持
|
||||
|
||||
---
|
||||
|
||||
## 已知限制
|
||||
|
||||
| 限制 | 说明 |
|
||||
|------|------|
|
||||
| 不支持 Linux | 需要适配平台专有 API 部分 |
|
||||
| 无全局 Escape 中止 | 原生方案用 CGEventTap 实现,Python 版暂不支持,用 `Ctrl+C` 代替 |
|
||||
| 操作前不自动隐藏窗口 | 原生方案的 `prepareDisplay` 依赖 Swift,Python 版未实现 |
|
||||
| 性能略低 | Python 进程启动 ~100ms 开销,但模型思考时间通常是秒级,用户感知不到 |
|
||||
| 像素验证关闭 | `pixelValidation` 默认关闭 |
|
||||
|
||||
---
|
||||
|
||||
## 参考项目与致谢
|
||||
|
||||
本功能的实现参考了以下开源项目,在此致以感谢:
|
||||
|
||||
| 项目 | 许可证 | 贡献 |
|
||||
|------|--------|------|
|
||||
| [wimi321/macos-computer-use-skill](https://github.com/wimi321/macos-computer-use-skill) | MIT | Python bridge 架构、`mac_helper.py` 运行时、`executor.ts` 适配方案。该项目从 Claude Code 工作流中提取了可复用的 TypeScript 逻辑,并用完全公开的 Python 库替代了私有原生模块 |
|
||||
| [domdomegg/computer-use-mcp](https://github.com/domdomegg/computer-use-mcp) | MIT | 独立的 Computer Use MCP 服务器实现(基于 nut.js),跨平台可用。在方案调研阶段提供了参考 |
|
||||
| [paoloanzn/free-code](https://github.com/paoloanzn/free-code) | - | Feature flag 系统分析和构建系统参考 |
|
||||
| [oboard/claude-code-rev](https://github.com/oboard/claude-code-rev) | - | 泄露源码的早期恢复工作,提供了 stub 包的参考实现 |
|
||||
|
||||
### 底层依赖
|
||||
|
||||
| 库 | 平台 | 用途 |
|
||||
|----|------|------|
|
||||
| [pyautogui](https://github.com/asweigart/pyautogui) | 跨平台 | 鼠标和键盘控制 |
|
||||
| [mss](https://github.com/BoboTiG/python-mss) | 跨平台 | 屏幕截图 |
|
||||
| [Pillow](https://github.com/python-pillow/Pillow) | 跨平台 | 图像处理和压缩 |
|
||||
| [pyobjc](https://github.com/ronaldoussoren/pyobjc) | macOS | Cocoa/Quartz 框架绑定(应用管理、显示枚举) |
|
||||
| [pywin32](https://github.com/mhammond/pywin32) | Windows | Win32 API 绑定(窗口管理) |
|
||||
| [psutil](https://github.com/giampaolo/psutil) | Windows | 进程管理(应用列表、进程操作) |
|
||||
| [pyperclip](https://github.com/asweigart/pyperclip) | Windows | 剪贴板操作 |
|
||||
| [screeninfo](https://github.com/rr-/screeninfo) | Windows | 显示器信息(多屏支持) |
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 888 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 523 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 912 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 800 KiB |
Reference in New Issue
Block a user