【免费下载链接】terminal-mac
Entire MacOS running in your terminal.
本文以 terminal-mac 随包发布的 Agent 技能文档为主线,讲清楚一个 computer-use Agent 如何在一台独立的虚拟 macOS 上完成「观察 → 操作 → 复核」的闭环:优先通过 MCP 工具驱动,没有 MCP 时退回 CLI,并在遇到登录界面、系统权限弹窗等情形时把控制权交还给用户。读完后你将掌握各工具/命令的准确参数语义、坐标系统、失败判定方式,以及源码层面这些行为是如何被实现和约束的。
一、定位:为什么要一台"别人的 Mac"
terminal-mac在 Apple silicon 上通过 Apple Virtualization 运行一个真实的 macOS guest。辅助程序(helper)跑在 guest 内部,由它截取 guest 屏幕、投递输入事件,因此 Agent 的每一次点击、每一次键入都发生在虚拟机里,不会移动物理 Mac 的指针,也不会改写宿主机剪贴板。
这个设计直接决定了技能文档的三条基本假设:
- guest 是一台独立的 Mac,有自己的应用、文件、账户和权限,不继承宿主机正在运行的应用与已登录会话;
- 宿主机侧的屏幕与输入 API不参与Agent 操作,没有"回退到宿主机"的路径;
- 因此 guest 未登录、helper 未运行、权限未授予时,Agent 只能报告状态并请用户处理,而不能绕过。
技能文档的适用场景写得很明确:需要真实 macOS GUI 的任务——测试 Mac 应用、使用某个网站或桌面应用、以及任何希望被沙箱化的 computer-use 任务。运行前提(Apple silicon、macOS 14 或更高、首次运行需联网下载恢复镜像)在 README.md 中有完整说明。
二、第一优先级:使用 MCP 工具而不是拼命令
如果当前环境已经暴露了来自terminal-macMCP server 的screenshot、click、type、key等工具,技能文档要求优先使用它们。原因不只是调用更短:MCP 这一层还额外负责把 guest 返回的 PNG 压缩成更小的图片、在动作后追加一次截图、以及把"没有登录会话"这类超时翻译成可读的处置建议。
2.1 四步标准循环
技能文档给出的顺序是固定的:
status:确认 VM 正在运行,且 guest 的两项权限都是true;screenshot:之后你传给其他工具的所有 x/y,都是这张图里的像素,原点在左上角;- 执行动作:
click(clicks: 2双击、button: "right"右键)、move、drag、scroll(负dy向下滚动)、type(字面文本)或key(return、tab、escape、cmd+space、cmd+c、cmd+shift+4等); - 再看一次:给动作传
screenshot: true,或单独调用screenshot,然后才决定下一步。
2.2 工具清单与参数边界
下表是技能文档提到的那组工具的完整参数边界,取值范围来自 Sources/TerminalMac/AgentServer.swift 中的 JSON Schema:
| 工具 | 必填参数 | 可选参数 | 取值约束 | 语义 |
|---|---|---|---|---|
screenshot | 无 | 无 | 只读 | 截取 guest 屏幕,返回图片与"用它作为坐标"的提示 |
status | 无 | 无 | 只读 | VM 是否运行 + guest 两项隐私权限 |
click | x,y | button,clicks,screenshot | button仅left/right;clicks1–3 | 单/双/三击 |
move | x,y | screenshot | 坐标 ≥ 0 | 移动指针,用于展开 hover 菜单或提示 |
drag | x,y,toX,toY | screenshot | 坐标 ≥ 0 | 按下左键、拖到目标、释放 |
scroll | x,y,dy | dx,screenshot | dy/dx在 -1000…1000 | 负dy向下滚(相当于上滑) |
type | text | screenshot | 字面文本 | 向焦点输入框输入文本 |
key | key | screenshot | 组合键字符串 | 回车、Tab、方向键与快捷键 |
技能文档同时强调:type只用于字面文本,回车、Tab 和快捷键必须走key。这不是风格问题——源码里两者走完全不同的实现路径(见第四节)。
2.3 源码视角:坐标、双击与"再看一眼"
几个容易踩坑的行为,在源码里都有明确落点:
- 坐标语义:MCP 的
screenshot()注释写明返回的是"point-sized screenshot",即图片像素等于 guest 的屏幕点,其他工具接收的 x/y 因此可以直接使用;返回的文本也会带上Screen: W×H. Use these pixel coordinates.这句提示(Sources/TerminalMac/AgentServer.swift)。 - 图片被转成 JPEG:guest 交回的是 PNG,MCP 层用 0.85 的质量重新编码为 JPEG 以缩小消息体,因此模型看到的分辨率不变、体积更小。CLI 路径则原样落盘 PNG。
- 双击不是一个"标志位":
click的clicks: n会被展开成 n 个请求,第 k 个请求带count: k(从 1 开始逐个续接),guest 侧把这些事件写入mouseEventClickState,macOS 才会识别为双击/三击(Sources/TerminalMac/AgentServer.swift、Sources/TerminalMac/Guest.swift)。 screenshot: true有 400 毫秒延迟:注释说明是为了"给动画和重绘一点时间",随后才截图,避免拿到动作生效前的旧帧(Sources/TerminalMac/AgentServer.swift)。- 超时预算各自独立:
click/move/key/scroll为 10 秒,drag15 秒,type30 秒,status5 秒。长文本输入因此比点一下更"贵"。
2.4 注册 MCP server
技能文档假设工具已经可用;把它接进客户端的方式在 README.md 的 "Use with agents" 一节:
claude mcp add terminal-mac -- terminal-mac mcp terminal-mac skills install # → ~/.claude/skills/terminal-mac/SKILL.md其他 MCP 客户端(Cursor、VS Code、Codex、Windsurf 等)把terminal-mac mcp --config的输出粘进客户端的 MCP 设置即可;驱动非default的 VM 用terminal-mac mcp NAME。
--config的实现有一个值得注意的细节:它输出的是启动器路径而不是版本化 bundle 的路径,这样升级后配置不需要改。见 Sources/TerminalMac/CLI.swift。输出的 JSON 形如:
{ "mcpServers": { "terminal-mac": { "command": "/path/to/terminal-mac", "args": ["mcp"] } } }handshake 阶段,server 会返回serverInfo(名称terminal-mac与当前版本)以及一段instructions,其中复述了同样的规则:先截图、坐标以截图为基准、动作后再截图、遇到登录或权限提示请用户处理(Sources/TerminalMac/AgentServer.swift、Sources/TerminalMac/AgentServer.swift)。
三、没有 MCP 时:CLI 的等价能力
技能文档给出的 CLI 形态非常克制,核心只有一句话:每条命令都打印 JSON,失败时以非零状态退出;NAME默认是default。
terminal-mac status NAME # stopped → terminal-mac start NAME terminal-mac action NAME status terminal-mac action NAME screenshot --output /tmp/guest.png # then view the PNG terminal-mac action NAME click X Y [--double] [--button right] terminal-mac action NAME move X Y terminal-mac action NAME drag X Y X2 Y2 terminal-mac action NAME type 'literal text' terminal-mac action NAME key cmd+space terminal-mac action NAME scroll --dy -300 --x X --y Y3.1 命令名与 VM 名的解析顺序
action子命令的位置参数解析有点特殊:如果第一个位置参数本身就是一个动作名(status、screenshot、snapshot、click、move、drag、scroll、type、key之一),那么 VM 名取默认值default;否则第一个位置参数被当作 VM 名。这样action screenshot和action default screenshot都能工作(Sources/TerminalMac/CLI.swift)。
VM 名不是随便什么字符串都行:必须匹配^[a-zA-Z0-9][a-zA-Z0-9_-]{0,47}$,即 1–48 位字母、数字、下划线或连字符,且以字母或数字开头。这个校验同时用于防止路径穿越,测试用例里../host、/tmp、a/b、a b、49 个字符的名字都会被拒绝(Sources/TerminalMacCore/Models.swift、Tests/TerminalMacCoreTests/CoreTests.swift)。
3.2 参数细节与默认可值
| 选项 | 适用动作 | 说明 |
|---|---|---|
--output PATH | screenshot/snapshot | 落盘路径,默认terminal-mac-NAME.png;支持~展开 |
--double | click | 等价于双击 |
--button right | click | 右键 |
--text 'value' | type | type 'literal text'的另一种写法 |
--key | key | 显式指定组合键 |
--dx,--dy | scroll | 像素级滚动量,--dy为负表示向下 |
--timeout | 全部动作 | 1–120 秒,默认 15 |
-- | 全部 | 结束选项解析,用于文本以-开头的场景 |
screenshot与snapshot目前是同一个动作:snapshot只是别名,不是accessibility tree 快照——这一点技能文档与 README.md 都特意声明,避免 Agent 误以为能拿到控件树。
截图回传也有大小上限:CLI 侧读取上限 32 MiB,MCP 侧 64 MiB(Sources/TerminalMac/CLI.swift、Sources/TerminalMac/AgentServer.swift)。
3.3 打开应用:优先用键盘
技能文档给出的示例是:
terminal-mac action NAME key cmd+space terminal-mac action NAME type "Safari" terminal-mac action NAME key return并在旁边补了一句"优先使用键盘快捷键,而不是去追小目标"。原因很实在:菜单栏、工具栏里的小图标在缩放开销与命中误差上都更脆弱,而 Spotlight 的输入路径稳定、可重复。
四、输入语义:为什么type与剪贴板无关
技能文档对type的描述是"Text is sent as Unicode keyboard events, not through the clipboard. No shell-execution action exists." 这两句话都很关键:前者保证不会污染 guest 的剪贴板(也不会反向影响宿主机),后者说明这套工具没有任何执行 shell 的入口——Agent 只能"像人一样"操作 GUI。
落到源码,type的约束更加具体:
- 文本以 UTF-8 字节计数,上限32 KiB,超出即报错;
- 文本按 UTF-16 code unit 切块,每块最多16 个单元就随一次按键按下/抬出发送,避免超出 CGEvent 的单次字符串容量;
- 事件源为
nil,通过keyboardSetUnicodeString携带字符,投递到.cghidEventTap——整条链路不触碰任何剪贴板 API(Sources/TerminalMac/Guest.swift)。
key则是虚拟键码加修饰键的组合:修饰键识别cmd/command、ctrl/control、alt/option、shift,键名覆盖字母、数字、常用符号、return/enter、tab、space、backspace、escape/esc、home、end、pageup、pagedown、delete、四个方向键与f1–f12。未知键或未知修饰键会返回明确错误,并给出cmd+space这样的示例(Sources/TerminalMac/Guest.swift)。
drag的实现不是"起点一次、终点一次",而是在两点之间按距离插值出 2–30 个中间点,每步间隔约 8 毫秒,最后才释放左键——这更接近真人拖动,也是文本选择、窗口移动能成功的原因(Sources/TerminalMac/Guest.swift)。scroll则构造像素单位的滚轮事件,dy与dx都在 -1000…1000 之间校验。
五、什么时候必须停下来问用户
技能文档把"停下来"的条件写成清单,这几条对应的是 Agent无法自行绕过的边界:
- guest 停在登录界面,或 helper 没有运行(工具报告 "no logged-in session"):请用户登录,例如执行
terminal-mac view NAME后按 Ctrl+O。让 guest 开启自动登录可以避免后续每次启动都卡在这里。 - guest 内出现 macOS 权限弹窗、密码、双因素验证码或支付界面。
- VM 根本不存在:
terminal-mac up NAME会从 Apple 下载数 GB 的 macOS,并且首次设置必须由用户完成。
这三条在实现上都有对应判定:
超时的两种含义被区分开了。MailboxTimeout带一个pickedUp标记:请求文件从未被读取说明根本没有 helper 在轮询(典型情况是 guest 还在登录界面);请求被读取但没有回音,则说明 helper 在忙或卡住。MCP 层据此给出不同文案,第一种直接建议用户用terminal-mac view NAME加 Ctrl+O 登录(Sources/TerminalMacCore/Mailbox.swift、Sources/TerminalMac/AgentServer.swift)。
"VM 不存在"和"VM 已停止"是两条不同的错误。requireRunning()每次调用都会检查,因此 MCP server 可以在还没有任何 VM 时就启动并列出工具;真正调用时若找不到 VM,会提示用户运行terminal-mac up;若 VM 存在但未运行,则提示terminal-mac start NAME(Sources/TerminalMac/AgentServer.swift)。
就绪状态是"两项权限都真"才算真。GuestReadiness的判定逻辑是screenRecording == "true" && accessibility == "true"才为ready,任一为假都归为permissions;而在没有任何回复时是unavailable。测试用例覆盖了这四种组合(Sources/TerminalMacCore/Setup.swift、Tests/TerminalMacCoreTests/CoreTests.swift)。
安装是否完成的判定同样来自命令路径。start会先检查machine.json中的installed字段,未完成安装时给出明确失败提示,而不是尝试启动一台半成品 VM(Sources/TerminalMac/CLI.swift)。
六、安全边界:三条"不要"与它们的实现依据
技能文档的安全条款简短但强硬:
- 不要在宿主机上运行
terminal-mac guest,也绝不回退到宿主机的截图或输入工具; - VM 里的应用、文件、账户与用户的 Mac 相互独立,除非用户要求,不要把密钥材料移进去;
stop是请求关机;stop --force可能丢失 guest 内未保存的工作,只在正常关机无法完成时使用。
第一条不是约定,而是代码强制的:GuestGuard.requireVirtualMac()读取hw.model,要求其以VirtualMac开头,否则直接抛错。guest子命令、GuestRuntime初始化、以及每个请求的处理入口都会调用它(Sources/TerminalMac/Guest.swift、Sources/TerminalMac/Guest.swift)。入口层还会在启动阶段先做一次检查,失败即退出并返回非零状态(Sources/TerminalMac/Entry.swift)。需要说明的是,这个检查的目的是防止"误在宿主机上执行",官方文档也明确它不是硬件证明,也不构成对已被攻陷宿主机的防御。
第三条对应的是两段不同的实现:stop调用vm.requestStop()并设置 60 秒期限,超时后提示可使用--force;stop --force走vm.stop()并立即退出进程(Sources/TerminalMac/VirtualMachine.swift)。
宿主与 guest 之间只有两条显式通道:可读写的control(请求/应答 JSON 与截图)与只读的tools(helper 应用)。从源码结构看,共享目录就是在配置阶段按这两个名字注册的,宿主机 home、项目目录、剪贴板、麦克风与摄像头都不在共享列表中(Sources/TerminalMac/VirtualMachine.swift)。
控制通道本身也没有网络监听:它是一组平铺的 JSON 文件,目录句柄用O_NOFOLLOW打开并被"钉住",读取时校验必须是普通文件、硬链接数为 1、大小不超过上限,文件名禁止/、..与空字符,应答 ID 必须与请求匹配,过期请求直接丢弃并在遍历时删除。这些行为都有对应测试(Sources/TerminalMacCore/Mailbox.swift、Tests/TerminalMacCoreTests/CoreTests.swift)。token 与凭据不会经过这条通道,宿主也从不执行 guest 提供的内容。
七、把技能装进 Agent 的几种方式
技能文档本身就是"可安装的资产"。随包发布的构建里,SKILL.md位于应用 bundle 的Resources/skills/terminal-mac/SKILL.md;开发构建则从可执行文件所在目录向上最多 6 层查找skills/terminal-mac/SKILL.md(Sources/TerminalMac/CLI.swift)。
terminal-mac skills get # 打印与本版本匹配的技能全文 terminal-mac skills path # 打印技能文件所在路径 terminal-mac skills install # 默认安装到 ~/.claude/skills/terminal-mac/SKILL.md terminal-mac skills install --dir PATH # 安装到任意 skills 目录install带一个保护:如果目标位置已存在一份不是terminal-mac 的技能(判定方式是内容里没有\nname: terminal-mac\n),会拒绝覆盖并报错,避免误删别的技能(Sources/TerminalMac/CLI.swift)。
对使用AGENTS.md的 Agent,README.md 建议加一行"在使用虚拟 Mac 之前先运行terminal-mac skills get",这样 Agent 拿到的指令永远与已安装的版本一致,而不是散落在提示词里的一段过期文本。docs/AGENT.md 也说明完整指南就是这份技能文件,并保留了 CLI 参考。
八、排错速查
| 现象 | 可能原因 | 处置 |
|---|---|---|
| 工具报 "no logged-in session" | guest 在登录界面,或 helper 未运行 | 请用户terminal-mac view NAME后按 Ctrl+O 登录 |
| 工具报权限相关错误 | guest 内未授予 Screen Recording / Accessibility | 请用户在 guest 内点 helper 的两个 Allow 按钮 |
status显示 VM 停止 | 被stop或未启动 | terminal-mac start NAME |
| 找不到 VM | 尚未创建 | 请用户执行terminal-mac up NAME(需下载 macOS 并完成首次设置) |
| 坐标点偏 | 用了旧截图的坐标或忽略了 HiDPI | 重新screenshot,严格使用新图左上角原点 |
| 输入没反应 | 只授予了屏幕录制,没有辅助功能 | 检查status中accessibility是否为true |
其中最后一条在终端查看器里有更直观的提示:一旦有帧但输入权限缺失,头部会显示 "⚠ input needs Accessibility";而 helper 的状态应答同时提供screenRecording与accessibility两个布尔值,正是status工具的判断依据(Sources/TerminalMac/Viewer.swift、Sources/TerminalMac/Guest.swift)。
九、验证边界:哪些结论可以当真
技能文档描述的是"应该怎么用",不等于"全部已在真机验证"。仓库的验证记录明确区分了两类结论:
- 已完成的自动化检查:Debug 构建与 14 个 XCTest 协议/输入/设置测试全部通过;Release 构建通过;打包应用通过严格递归签名校验;宿主 entitlements 含
com.apple.security.virtualization而 guest 不含;guest --check在物理宿主机上拒绝运行并以非零状态退出。 - 尚未完成的真机验收:下载恢复镜像并创建 VM、在原生窗口完成 Setup Assistant、在 guest 内授予隐私权限、验证 Retina/非 Retina 下的坐标对齐、在 Ghostty/Kitty/iTerm2 中实测查看器、暂停/恢复与强制关机等。
因此 docs/VALIDATION.md 特别声明:在这些检查完成之前,内联终端渲染、guest 隐私授权、guest 截图与 guest 输入行为都不应被描述为"已实测通过"。自动化测试覆盖的是邮箱往返、过期与清理、文件边界防御、名称与选项校验、分片 UTF-8 与终端输入序列——它们是协议级证据,不是"VM 已成功安装、启动并被操控"的证据。把这条边界带上,Agent 在真实环境中才不会被过度承诺误导。
【免费下载链接】terminal-mac
Entire MacOS running in your terminal.
相关推荐
如何用一句话指挥 AI 做 SQL 注入测试:CyberStrikeAI 技能系统完整指南
如何用一句话指挥 AI 做 SQL 注入测试:CyberStrikeAI 技能系统完整指南 当你对 CyberStrikeAI 说一句"对这个登录接口做 SQL
三步跑通 DataEase 内网离线部署:无网络环境完整安装指南
三步跑通 DataEase 内网离线部署:无网络环境完整安装指南 你负责的那台服务器完全隔离外网,想在上面跑一套开源 BI 工具 DataEase 来做数据可视
数据可视化数据分析后端前端XCOM 2模组管理终极指南:Alternative Mod Launcher完整解决方案
XCOM 2模组管理终极指南:Alternative Mod Launcher完整解决方案 厌倦了XCOM 2官方启动器的限制?Alternative Mod
桌面应用游戏开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考