夜雨聆风学习资料网

ARTICLE · 977530

我把整台 Mac 交给 AI 的源码扒了一遍,发现 6 个坑

我把整台 Mac 交给 AI 的源码扒了一遍,发现 6 个坑
我的电脑没被 AI 控制过。这篇不是我的实验记录。

是一个叫 macOS Harness 的开源项目(browser-use 团队出的,就是那个 11 万 star 的让 AI 操作浏览器的 browser-use 的另一个项目)。它干的事特别大胆——把整台 Mac 直接交给 AI:让 AI 看你的屏幕、点你的鼠标、敲你的键盘、跑你的脚本。

我把它源码扒了很多遍。越扒越觉得,真正有意思的不是"AI 能控制电脑",而是为了让 AI 能控制电脑,背后要处理多少你根本想不到的边角

这篇就把我扒出来的 6 个坑讲给你。这些不是 bug——是作者踩过之后才故意做对的地方,每个坑背后,都藏着一个第一次做这件事的人会掉进去的坑。我尽量讲清楚每个坑"为什么存在、源码里怎么处理的、换你会怎么掉进去",顺便给一份能直接抄的检查表。

如果你也在做类似的事——让 AI 用电脑、让程序模拟用户操作、做任何形式的"程序控制鼠标键盘"——这篇能让你少撞几面墙。

坑 1:权限不是开一个,是开三个,开错一个全部静默罢工

谁做这种事,第一反应都是"给 AI 开一个随便用电脑的开关"。

错。macOS 把"AI 用电脑"拆成了三个互相独立的权限,一个都不能少:

  • 辅助功能(Accessibility):AI 才能读界面、发按键
  • 屏幕录制(Screen Recording):AI 才能"看见"屏幕(截屏)
  • 投递事件(后台发输入):AI 才能把手上的动作发出去

源码里这三样是三个互相独立的检查(screen_recordingpost_events、Accessibility),缺哪一个,AI 就在那一个环节静默失灵,而且报的错还不一样:点不了是辅助功能,看不见是录屏,发不出动作是投递权限。

更坑的是,这三个还得授权给正确的宿主进程——你在终端里跑的,就授权终端;在 Codex 里跑的,就授权给 Codex。开错一个,另一个继续装死。

所以作者专门写了个 doctor 命令帮你一次性体检权限。但这也侧面证明了一件事:第一天用的人,几乎都是在这三个设置页里来回横跳、缺哪个试哪个。 反推作者自己写的时候也踩过,才明白必须把"要哪几个权限、缺了报什么、去哪开、开给谁"讲清楚。

教训:做"程序要碰系统敏感权限"的事,第一步一定是把"要哪几个权限、缺了报什么、去哪开、开给谁"一次列清楚给用户。 让用户一个一个试错,是最蠢的设计。

对照:Linux 没这套,但换成 Wayland 权限、X 授权、DBus。本质都一样——"想碰系统,先过一道你猜不透的关"。

坑 2:AI 看到的屏幕,和真实屏幕隔着"一层缩放"

要 AI 点击,得先告诉它点哪。谁都会想:给个坐标就行。

但你看源码里的 _screen_point,会发现坐标不是"一个像素对应一个像素"——这里藏了三套坐标系:真实屏幕的坐标、某个窗口自己的坐标、还有 AI 看到的那张截图的坐标。AI 看到的不是屏幕,是一张截图——截图是被压缩过的,坐标和真实位置对不上。AI 说"点截图第 300,200 像素",你得先换算回真实屏幕坐标,才点得准。

反推作者为什么把这套换算写得这么细,几乎可以确定:早期版本里 AI 老是点偏,作者排查到最后发现是坐标没换算,才被迫把这三套坐标分得清清楚楚。 这套换算不做,AI 每次点偏,你还以为是 AI 傻了,其实是坐标没对上。

雪上加霜的是 Retina 屏。截图上记录的 scale_xscale_y(截图宽÷窗口宽)往往不是 1,甚至整数倍。忘了除以这个缩放,AI 看到的和点到的,就差了倍数,直接点到另一个地方。

教训:任何"让程序在视觉里定位真实世界"的系统,都必须把"视觉坐标"和"真实坐标"当成两套东西建模。 截图、Retina、多屏——每一层都是误差来源。

对照:浏览器里 getBoundingClientRect 和 devicePixelRatio 的关系,一模一样。做过前端缩放的都懂。

坑 3:AI 操作"后台窗口",但它绝不能抢你的焦点

场景:AI 在改一个后台 app,可你正在前台码字。这种时候,程序员最省事的写法是——先把 app 拽到前台,干完再切回来。

但那会抢走你屏幕。你正打字,屏幕突然跳走,体验极差,还可能误操作。

这个项目有个特别漂亮的设计(_guard_focus + FocusChangedError):操作前先记住"当前前台是谁",然后用 CGEventPostToPid 把事件直接发给目标进程的 PID,不走"先激活再操作"的路径。每操作一下,就检查前台有没有变——如果后台 app 意外变成前台、抢了焦点,它宁愿直接抛错停下来,也不继续。

也就是说:AI 可以操作后台窗口,但一旦发现自己在抢用户的焦点,它宁可认怂停手,也不打扰你。

教训:让程序碰用户当前正在用的东西,"别打扰用户"永远排在"把事情做完"前面。 很多自动化翻车,不是没做完,是抢得我行我素。

对照:浏览器后台标签被限流、一激活才恢复——系统保护前台体验,你的程序必须认这个优先级。

坑 4:AI 的鼠标是"假的",它压根不动你那个真光标

你以为 AI 动鼠标,就是动你真鼠标。这个项目偏不——它做了个虚拟指针

代码里 move 方法的注释写得清清楚楚:

"Animate the virtual pointer without moving the physical cursor." (让虚拟指针动起来,但绝不移动物理光标。)

为什么?因为 AI 如果每次都真去挪你的真实鼠标,你开着视频会议,鼠标突然自己动,谁能忍。所以它把两件事拆开了:

  • 真实点击:走 CGEventPostToPid 把事件直接发给目标进程,根本不经过真实鼠标的物理移动
  • 视觉反馈:画一个合成的箭头 overlay,让你和 AI 都"看到"鼠标在动,但这个箭头是画在独立进程的专用窗口里

这个"假鼠标"有多讲究?它不是普通窗口,是一个独立的 AppKit helper 进程,主进程靠 JSON over stdin 跟它通信(发 {"cmd":"move","x":..,"y":..} 这种),还得处理它崩了重启、退出清理。

教训:当"用户体验"和"实现最顺手"打架,选用户体验,但为代价做好准备。 那个 helper 进程的所有麻烦,都是"为了不碰你真鼠标"买单。

对照:前端做虚拟拖拽手柄、游戏里 UI 层逻辑层分离,一个道理——别让 UI 状态直接改全局,中间放层解耦。

坑 5:让 AI 打字,得先手搓一张键盘映射表

AI 要打字,得发键盘事件。这有个坑:macOS 没有"输入一段字符串"的现成 API。 我是读了源码才发现——作者只能手动把字符串一个字符一个字符拆开去处理。

代码里是把字符串一个字符一个字符拆开,每个字符自己去查一张硬编码的 _KEYCODES 表,转成 keycode,再拼上它需要的修饰键,最后 down/up 两个事件发出去。

这里全是坑:

  1. 大写字母没有独立按键——得用 shift + 小写字母模拟(character.isupper() 时自动加 shift 标志)
  2. 有些字符根本不在键盘上——只能直接注入 Unicode 字符串兜底
  3. 键盘布局不同——美国、法国、德国布局 keycode 完全不一样,硬编码表只对一种成立
  4. 空格、回车、Tab——都得手动映射

教训:凡是"模拟人工操作",最疼的不是主流程,是那些"看似简单实则全是边界"的细节——大小写、空格、组合键、国际化布局。 主流程一天写完,边界磨你一周。

对照:Java 的 Robot 发 KeyEvent、Selenium 的 SendKeys 处理特殊键,同一个问题——总有些字符没有直接键、要组合、甚至发不出去。

坑 6:Chrome 的安全弹窗,程序得自己伸手去点

最后一个坑最野,也最典型。

AI 要操作真实浏览器。浏览器开远程调试时,Chrome 会弹个安全窗:"Allow remote debugging?"(允许远程调试?)。**这个窗必须有人点"Allow"**,调试才生效。可它出现的时机不可预测,AI 正在全自动跑,没人去点。

于是这项目作者干了一件很"野"的事:写了个自动点头工具,**用系统级辅助功能 API 去后台找这个弹窗,然后自动按"Allow"**。

怎么找?它枚举系统所有窗口,筛出标题是"Allow remote debugging?"的那个 Chrome 窗,然后在窗口右下角用辅助功能遍历找按钮,找到 AXRole == "AXButton" 且描述是"Allow"的就按(AXPress)。更野的是它还有个兜底——先精确点一下,点不中就网格扫描整个右下角,总有一个能扫到 Allow。

教训:做自动化,永远要料到一个事实——现实里总有你控制不了的"没有 API"的组件,然后准备最笨但有效的兜底。 一半工程是干净架构,另一半是硬刚这种狗屎现实。

对照:爬虫过验证码、Selenium 反爬弹窗,同一路数——没有标准解,只有具体情况具体崩。

扒完源码之后

回头看这 6 个坑,其实是同一件事的两个面:

你要给 AI 真正的"自由",就得先给它够"约束"。

  • 权限要管(坑1)——自由建立在授权之上
  • 坐标要算(坑2)——AI 看到的世界和真实世界隔了一层
  • 不能抢焦点(坑3)——自由不能骑在你正在用的东西上
  • 假鼠标(坑4)——自由要有不打扰人的"手感"
  • 键盘手搓(坑5)——自由建立在对抗边界细节上
  • 弹窗硬刚(坑6)——自由要处理现实世界的脏活

"让 AI 干活"这件事,真正的难点从来不是"让 AI 会干活",而是"让系统愿意放权、而且放得安全"。

扒完源码我最深的体会:模型已经在飞快变聪明,可从"会想"到"能动手"之间,是另一片从头到尾都要自己踩一遍的雷区。谁把这层"手"做好,谁就真把 AI 从秘书变成了同事。

相关学习资料

返回首页浏览学习资料