# @wangyong1972/dsh-computer-use-macos

[English](./README.md) | [中文](#中文)

[![npm version](https://img.shields.io/npm/v/@wangyong1972/dsh-computer-use-macos)](https://www.npmjs.com/package/@wangyong1972/dsh-computer-use-macos)
[![npm downloads](https://img.shields.io/npm/dm/@wangyong1972/dsh-computer-use-macos)](https://www.npmjs.com/package/@wangyong1972/dsh-computer-use-macos)
[![license](https://img.shields.io/npm/l/@wangyong1972/dsh-computer-use-macos)](./LICENSE)

<a id="中文"></a>

一个 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai) 插件，为模型提供
Anthropic "Computer Use" 能力的原生 macOS 实现：一个 `computer` 工具，可以对真实
显示器截图、驱动真实的鼠标/键盘，仅使用 macOS 自带的命令行工具（`screencapture`、
`sips`、`osascript`）——**零安装依赖，零网络调用，v1 中零编译辅助二进制文件**。

## 功能说明

该插件注册一个工具 `computer`，让模型可以：

- 对真实屏幕截图，并在对话中内联查看；
- 移动鼠标、在某个像素坐标处左键/右键/双击；
- 输入文本、发送组合键（`Return`、`cmd+c`、`ctrl+shift+t` 等）；
- 上下左右滚动；
- 读取当前鼠标光标位置（尽力而为）；
- 等待一段有限的、有上限的时长。

每一次与操作系统的交互都通过 `execFile('screencapture', [...])`、
`execFile('sips', [...])` 或
`execFile('osascript', ['-l', 'JavaScript', ...])` 以 argv 数组方式执行——绝不会
用模型输入拼接 shell 字符串。详见下方"安全态势"一节。

**截图会在发送给模型之前自动被缩小**：任何长边超过 1568 像素的截图都会原地缩放
（通过 macOS 自带的 `sips -Z`，保持宽高比），使其安全地低于常见视觉模型的单边像
素上限——这正是让 `screenshot` 在 Retina 或 6K 显示器上也能正常工作的关键，否则
原始截图往往会被直接拒绝（"Image exceeds the configured per-side pixel
limit"）。**没有任何配置项可以更改这个目标尺寸。** 如果缩放后屏幕上的文字或控件
变得难以辨认，请在**被控制的应用内部**放大（调大字号、放大视图等），而不要尝试
更改显示器分辨率或本插件的缩放目标——这与
[Claude Code 自身文档中记录的行为](https://code.claude.com/docs/zh-CN/computer-use)
（其原生 macOS computer-use 工具）保持一致。模型读取或提供的每一个 `coordinate`
始终处于这张*缩放后*截图自身的像素空间中，绝不是显示器的原生像素空间——坐标换算
由本插件内部完成。

**与 Claude Code 对齐的 UX 特性**（均可单独开关，见下方配置表）：机器级全局锁，
防止同一台 Mac 上的第二个 DSH 进程同时操控鼠标/键盘；可选的"操作时隐藏其他应
用"；可选的"截图时排除宿主/终端窗口"；工具开始/停止操作时的 macOS 系统通知；以
及当前台应用是终端/IDE、Finder 或系统设置类应用时，在审批提示中附加额外警告文
字。经过调研后认定全局 Esc 中止热键在本插件的架构下不可行——完整分析见
`DESIGN.md` §10.5。

## 环境要求

- 仅支持 macOS（`"os": ["darwin"]`——在其他平台上安装会直接失败，不会静默安装一个
  只能在运行时报错的插件）。
- 挂载了 `tools` 与 `attachments` 服务的 DSH 宿主（在 DSH web/桌面 profile 中是标
  配）。
- `screenshot` 动作特别需要一个具备视觉能力的模型路由（纯文本路由会收到明确的错
  误提示，而不是白白浪费一次截图）。

## 一次性 macOS 权限设置

在该插件能够执行任何操作之前，macOS 要求两项手动的一次性权限授予。**该插件无法
自行授予这些权限**——macOS 出于设计故意让 TCC（隐私）授权无法被请求进程自动脚本
化。这与你之前为需要"完全磁盘访问"或"屏幕录制"权限的应用所经历的一次性流程是同
一类操作。

1. **辅助功能（Accessibility）**——每一个鼠标/键盘动作
   （`mouse_move`、`left_click`、`right_click`、`double_click`、`type`、`key`、
   `scroll`，以及尽力而为的 `cursor_position`）都需要它。
   打开 **系统设置 → 隐私与安全性 → 辅助功能**，并启用相关应用。
2. **屏幕录制（Screen Recording）**——`screenshot` 需要它才能返回真实像素，而不
   是一张空白图片（近期的 macOS 版本在缺少该权限时会静默返回空白截图，且不会报
   错）。
   打开 **系统设置 → 隐私与安全性 → 屏幕录制**，并启用相关应用。

**哪个应用会出现在上述列表中，取决于 DSH 是如何启动的。** 该插件的每一个鼠标/键
盘/截图动作都通过 `osascript`（截图则是 `screencapture`）执行，因此 TCC 会把权
限归属到实际执行该命令的进程——在实践中，这可能是 `osascript` 本身，也可能是负责
进程链中的终端/宿主应用，具体取决于 macOS 版本以及 DSH 的启动方式。**请先触发一
次 `computer` 动作**（它会因缺少权限而失败，并给出明确提示），**然后**再去检查系
统设置——正确的条目只有在第一次尝试之后才会出现在列表中。请留意查找
`osascript`、`Terminal` 或你使用的终端应用。

两项权限都授予后，重试刚才失败的动作即可。

## 安装到 DSH profile

**方式一：npm（推荐）**

```sh
dsh plugin --profile <name> add @wangyong1972/dsh-computer-use-macos
```

npm 包：[@wangyong1972/dsh-computer-use-macos](https://www.npmjs.com/package/@wangyong1972/dsh-computer-use-macos)

**方式二：本地源码（开发/贡献）**

```sh
dsh plugin --profile <name> add /path/to/dsh-computer-use-macos
```

该插件自带 `cordis.patch.yml`（由 `package.json` 的 `dsh.bundle.patch` 字段引
用），因此添加它会自动以合理的默认配置注册 `computer-use-macos` 插件。

## 配置参考

| 字段 | 默认值 | 说明 |
|---|---|---|
| `enabled` | `true` | 总开关。为 `false` 时完全不注册 `computer` 工具——零 prompt token 开销。 |
| `requireConfirmation` | `true` | 每一个改变状态的动作（click/move/type/key/scroll）在执行前都经过审批环节。`screenshot`/`cursor_position`/`wait` 永远不受此限制。未挂载审批服务时默认拒绝（fail closed）。 |
| `allowedDisplayIndex` | `0` | 向后兼容的默认 0-based 显示器索引；动作未传 `display` 时沿用它。每次动作都可独立覆盖，无需修改配置。 |
| `screenshotFormat` | `'png'` | 为未来格式预留；v1 只支持 `png`。 |
| `actionTimeoutMs` | `10000` | 每一次 `screencapture`/`sips`/`osascript`/`ps` 子进程调用的硬超时时间。 |
| `maxTypeTextLength` | `4096` | 超过该 UTF-16 码元数的 `action=type` 调用会被拒绝（拒绝而非截断）。 |
| `enableMachineLock` | `true` | 在改变状态的动作执行期间持有一个机器级锁文件，防止同一台 Mac 上的第二个 DSH 进程同时操控真实鼠标/键盘。失效的锁（持有者进程已死，或持有过久）会被自动回收。 |
| `hideOtherAppsWhileActing` | `false` | 在改变状态的动作执行期间隐藏所有其他可见应用，只保留本插件自身的宿主进程可见，结束后恢复原样。默认关闭——这是明显的界面侵入行为，需要主动开启。 |
| `excludeHostFromScreenshots` | `false` | 仅在 `screencapture` 调用期间短暂隐藏宿主进程，使 `screenshot` 不会截到终端窗口而不是真正的目标应用。默认关闭，原因同上。 |
| `enableSessionNotifications` | `true` | 工具开始/停止操作时发出一条 macOS 系统通知。尽力而为，绝不会导致底层动作失败。 |
| `enableAppRiskWarnings` | `true` | 当前台应用是终端/IDE、Finder 或系统设置类应用时，在审批提示中附加额外警告文字。纯粹是附加信息——绝不改变审批结果，`requireConfirmation` 为 `false` 时无效。 |

## 点击诊断与日志

每一次 `mouse_move`/`left_click`/`right_click`/`double_click` 调用都会通过 DSH
自带的 `ctx.logger()` 机制发出一条结构化、隐私安全的诊断记录（绝不是自建日志文
件，也绝不使用 `console.*`）——日志名为 `computer-use-macos`，方便宿主自己的日
志导出器按名称过滤。示例行（真实格式，已针对真实的 `@deepseek-ai/cordis`
`Context` 实测验证）：

```
computer.left_click display=0 pixel=[500,400] point=(500,400) pid=69234 exit=0 elapsed=112ms outcome=ok cursorVerified=true
```

字段包括：动作名称、`allowedDisplayIndex`、请求的像素坐标、解析出的 Quartz 全
局点、`osascript` 子进程的 pid/退出码、耗时、一个粗粒度的结果分类（`ok` /
`accessibility-denied` / `timeout` / `aborted` / `other-error`），以及一次
"点击后光标位置自检"是否确认光标真的到达了请求的点。**绝不会记录**：原始
stderr/stdout 文本、输入的 `text` 内容，或组合键内容——完整的隐私约定见
`DESIGN.md` §11.2。除了干净且已验证的结果外都会以 `warn` 级别记录，其余为
`info` 级别。

**这能证明什么、不能证明什么**：光标自检可以确认一次点击底层的 `CGEventPost`
调用确实到达了操作系统层，且光标已经跳转到了正确的屏幕坐标——但它**无法**确认
目标应用真的接收或响应了这次点击（那需要针对具体应用的 accessibility 树内省能
力，超出本插件范围，详见 `DESIGN.md` §11.3）。若需要确认点击的实际效果，请在
点击后再调用一次 `screenshot` 自行判断。

## 支持的动作（v1）

`list_displays`、`screenshot`、`left_click`、`right_click`、`double_click`、
`mouse_move`、`type`、`key`、`scroll`、`wait`、`cursor_position`（尽力而为）。

### 选择显示器

调用 `list_displays` 可取得每个活动显示器的 0-based `index`、数字
`CGDirectDisplayID`（`id`）、是否主屏、Quartz 原点/尺寸、缩放倍数，以及显式的
1-based `screenshotOrdinal`。
`screenshot`、`cursor_position`、鼠标移动/点击动作及 `scroll` 接受可选的
`display` 字段：

- 方位字符串：`main`、`leftmost`、`rightmost`、`topmost`、`bottommost`；
- JSON 数字表示 0-based 活动显示器索引；
- 十进制字符串表示 `CGDirectDisplayID`，也可加 `id:` 前缀。

省略 `display` 时完全保留现有 `allowedDisplayIndex` 行为。每次调用都会重新发现显示器；
无效或越界选择器会在发送任何 OS 输入事件前失败；坐标始终是所选显示器截图的局部空间。
`main` 必须恰好解析到一台显示器；若两个显示器共享同一个方位极值则视为歧义并安全失败。
`scroll` 未给 `coordinate` 时虽接受 `display`，但不会移动鼠标，仍在当前光标位置滚动。

**截图身份限制：** macOS 的 `screencapture -D` 只接受 1-based ordinal，不接受
`CGDirectDisplayID`。`screenshotOrdinal` 是从同一份 `NSScreen` 清单得到并显式传递的
当前最佳映射，但 Apple 并未保证 `NSScreen` 与 `screencapture` 排序永远一致，因此它
不是身份安全保证。鼠标/光标几何通过 `CGDisplayBounds` 按 ID 获取；在换用直接
CoreGraphics 截图后端之前，不应宣称按 CG ID 选择的截图绝对安全。

**暂不支持（推迟到 v2）：** `left_click_drag`、`middle_click`、
`triple_click`、`hold_key`、`zoom`、对 macOS "自然滚动"偏好设置的补偿，以及基于
窗口/应用（accessibility 树）的定位。请不要为这些已知的、有意为之的 v1 范围裁剪
提交 bug——它们不是疏漏。

## 安全态势

- **任何地方都不存在 shell 字符串执行。** 每一条操作系统命令都通过 Node 的
  `execFile` 以 argv 数组方式调用；每一个鼠标/键盘动作中不受信任的输入
  （`text`、`coordinate`）都以单个 JSON 字符串的形式穿过 JXA 子进程边界，绝不
  会被拼接进 AppleScript 或 shell 源文本。
- **该插件不发起任何网络调用。**
- **所有数值输入都会在真实、实时查询到的屏幕边界范围内被校验和裁剪**，然后才会
  用于合成任何鼠标/键盘事件——即便模型只是回显此前展示给它的坐标，也绝不会被直
  接信任。
- **不接受任意文件路径。** 该插件仅会接触两类文件系统路径：它自己在同一次工具调
  用内创建、（必要时通过 `sips` 原地缩放）并删除的随机命名临时截图文件，以及一个
  固定路径的机器级锁文件（§10.1）。
- **机器级锁在发生争用时会拒绝失败**——第二个进程持有该锁是硬性拒绝，绝不会静默
  放行；失效锁的回收同时受"持有者进程是否存活"与"绝对时长上限"双重约束。
- **应用隐藏功能只会恢复自己隐藏过的应用**，绝不是无差别地"全部显示"，因此用户
  自己提前隐藏的应用不会被意外唤出。
- **按应用的风险警告纯粹是附加信息**——它们绝不能把一次"需要确认"的审批变成自动
  "允许"，且风险分级表是固定的，不受模型或配置驱动。
- **每一个改变状态的动作都可通过 `requireConfirmation` 进行门控**，在未挂载审批
  服务时默认拒绝（fail closed）——绝不会默认放行（fail open）。
- **`enabled: false` 会完全禁用插件**：不注册工具，零 token 开销。
- **绝不包含 `postinstall`/`preinstall` 脚本。**
- **对所有 `@deepseek-ai/*` 包使用 `peerDependencies` 而非 `dependencies`。**

## 开发

```sh
pnpm install
pnpm run build       # tsc -> lib/，然后原样复制 JXA 辅助脚本
pnpm run typecheck    # tsc --noEmit
pnpm run verify       # 静态检查，不产生 macOS 副作用，可在 CI 中运行
pnpm test             # 针对 lib/ 的单元测试（key-spec、validate、permission-errors、screenshot-scale、lock、app-tiers、notify）
```

任何需要真实辅助功能/屏幕录制权限的测试，都记录在 `tests/e2e.manual.md` 的手动
检查清单中。

## 许可证

MIT
