
这是什么
一个写给 macOS 的 computer use agent,用 Python 写成,目标是让一步大约花掉 $0.0002,而前沿模型读同一张截图要 $0.032。它确定性地读屏——对最前面那个窗口的裁剪区域跑 Vision OCR,再叠上无障碍树里带标签的控件——把模型本来要自己算的事实交给代码算好,然后发一次 TypeSafe 请求,回答三个选择题:哪一类动作、哪一个条目、哪一个网站。写作模型只在需要自由文本和最后作答时被叫上,从不负责挑动作。十三个动作键覆盖点击、按下不在屏幕上的控件、输入、浏览器跳转、键盘、滚动、等待和停止。每一步都会写下一个 run 目录,装着截图、发出去的 payload,以及分类器返回的每一个概率。另有一个实验性的 Windows 适配器(UI Automation 与 Windows.Media.Ocr)、一个可选、完全走 DOM 而不看像素的浏览器后端,以及一个在 OSWorld 里和 OSWorld 自带的 GPT agent 跑同一道题的 agent。MIT 许可,1,102 个星。
谁做的一位软件工程师,2026 年 9 月以自己的名字发布这个仓库,116 次提交里 109 次出自他手;另外七次来自七个人,每人一次。历史里带着 112 条共同作者尾注,其中 105 条写的是 Claude 模型——86 条「Claude Fable 5.1」、13 条「Claude Opus 5.5」,另有六条标着 1M 上下文。
它是怎么搭起来的
组成 · 6一个分层的循环,只有一条组织原则:分类器负责挑,代码负责定事实,写作模型只负责写自由文本。凡是模型本来要自己算的东西都不留给它——某个块上的日期和它距今多远、某个输入框是不是有焦点、一个 URL 干不干净、一个重复出现的标签在第几行、以及这一步之前已经在这个屏幕上试过哪些动作——全部由代码算进一个状态对象,交到模型面前的只有一道选择题。这道题以一次请求的形式到达,回答三个选择题——类别、条目、网站——各自返回完整的概率分布和一个可以用来设阈值的置信度,而赢家由一段处理函数执行,那段函数返回这一步的历史行。感知在它前面,并且刻意做成两个来源而不是一个:OCR 看得见文字但看不见图标,无障碍树看得见图标但只覆盖那些愿意发布它的应用,而每个条目都带着自己来自哪个来源。再往后是 platform_adapter.desktop,通向操作系统的唯一通道:树的遍历把子节点、属性和可执行动作都当成可调用对象传进去,于是只有 macOS 和 Windows 两套绑定不同,程序里别的地方都不知道自己跑在哪个系统上。在它前面,写作模型被四面围住:它从不挑动作,只用结构化回复,它给的 URL 和填进去的值都由代码校验,它宣布的一次停止还可能被针对那次输入的第二次 TypeSafe 调用推翻。
- typesafe_computer_use/perception.py
- 读的那一侧,33 KB,是包里最大的模块:截图、它被读取的裁剪区域、跨步骤复用 OCR 行的变化格子缓存、文本块合并、丢掉与目标重复的行的过滤器、无障碍条目来源,以及把两者合并成一张编号列表——每个条目都带着来源,遇到重复标签时还带着它所在的行。
- typesafe_computer_use/decide.py 与 writer.py
- 两个模型客户端,刻意分开:12 KB 那个负责构造状态与判定条件、组装三个选择题的请求,并在输入框被填之后跑第二次校验;19 KB 那个装着写作模型、它的结构化回复、URL 校验,以及带焦点或问题的最终答案,旁边还有
openai_writer.py负责把同样的请求发到兼容 OpenAI 的端点上。 - platform_adapter.py、macos.py、windows.py、ax_walk.py
- 通向平台的唯一通道。macOS 那套是 Quartz、无障碍 API、AppleScript 与 Vision OCR;Windows 那套是 UI Automation、SendInput 与 Windows.Media.Ocr,并标着实验性;有界的树遍历两边共用,带节点与时间上限,也带一张「框架会撒谎」的剪枝清单。多接一个平台,就是多一个适配器加一行代码。
- runner.py、actions.py 与几本记账簿
- 带着停止规则的步骤循环,以及交给写作模型再交回来的那条来回;每个动作一个处理函数,各自返回那一步的历史行,而历史行是一个动作「到底做出了什么」的唯一记录;
outcome.py把动作之后的截图和做这个动作时的截图作比较;calls.py在两个客户端上按模型数请求;timing.py记各阶段秒表与那行 timing;report.py负责标注过的截图。 - typesafe_computer_use/browser/ 与 typesafe_computer_use/osworld/
- 两条不走桌面的入口。可选的浏览器后端通过 Chrome DevTools Protocol 读 DOM,自己在一份临时 profile 上开一个 Chrome,也是唯一跟浏览器说话的地方;基准那一包把这个循环变成 OSWorld 的 agent,里面有 reset 与 predict 两个入口、把观测转回输入代码的适配层,以及一段跑在虚拟机里、只走一个应用的轻量树遍历。
- tests/、scripts/ 与 infra/gcp/
tests/world.py里有一台模拟电脑,由真正的步骤循环驱动,用一段策略顶替分类器;它上面压着 80 KB 的场景测试——测试树里最大的文件,比包里任何一个模块都大——在那里一次失败被当成一次架构结论。scripts/osworld与scripts/osworld-gcp驱动那台跑基准的机器,而infra/gcp用 Terraform 把那台机器描述出来。
取舍,以及它替代了什么
每一步让分类器回答一个选择题 替代 让前沿模型读截图并给每一步做计划
大多数步骤并不需要计划,只需要从一张短名单里做一个又快又便宜、还带一个可以设阈值的置信度的选择,而 README 把差距算成了每次决策 $0.0002 对 $0.032。代价也写在同一处:大模型是直接从像素上读出活动日期并自己比较的,所以那份解析必须被重建成确定性状态。
先读结构,再读像素,OCR 只是兜底 替代 把像素当成读屏的主要来源
VISION.md把页面标记、WebMCP 与无障碍树放在前面,并说明了原因:OCR 只看得见文字,分不清链接和它周围的句子,也看不见图标与 logo。浏览器后端把这件事量到了尽头——在三个页面上,OCR 只还原出 DOM 标签的 13 个里的 7 个、88 个里的 7 个、85 个里的 3 个,真实网站上不到一成——于是它的结论是:在 OCR 文本块之间做选择的分类器,是在一堆乱码之间做选择。让动作集合保持互斥,并把一次决策拆成三个问题 替代 在一次决策里回答一张更长的动作清单
两个意思相同的选项会分散票数、被读成低置信度,因为置信度衡量的是集中程度,而做这个项目的过程中发现的每一次卡住都来自这样一对选项。最好的例子是屏幕上那个「返回」按钮:它让点击和键盘后退分成 0.44 对 0.40,去掉那个重复项之后,下一次决策变成了一次 0.71 的点击。
说一个条目「在弹窗下面」,而不是「被弹窗盖住」 替代 把盖住它的那个窗口写进描述里
在 81 份带弹窗屏幕的历史请求回放里,「被盖住」会把分类器往 Esc 那边拉并压低它的置信度,而「在下面」两者都不动。措辞背后的行为是:挑中这样一个条目时,会先关掉弹窗——用它自己的关闭按钮,没有的话用 Esc,绝不用它别的按钮,因为「恢复」会把上一次会话重新打开——然后在同一步里点中那个条目。
绝不输入密码 替代 给写作模型开一条凭据通道
凭据字段回来时是「什么都不填」,浏览器后端从不采集输入框的值、也不拿它给任何元素命名,于是页面上的密码到不了分类器、写作模型或磁盘;而
CONTRIBUTING.md最后一条基本规则,就是绝不新增一条会输入密码的路径。README 则直接告诉读者:靠浏览器的密码管理器,或者一个 OCR 读得出来的登录按钮。
依据docs/how-a-step-works.md(16 KB)、docs/layout.md、docs/browser-backend.md、docs/known-limits.md、VISION.md、CONTRIBUTING.md,以及 pyproject.toml 里的依赖清单、入口点与 lint 配置。
制作过程
6 个阶段- 01
一步五十分之一美分,以及这个数字站在什么上面
价格写在 README 的第一句里——把 Mac 推向一句白话写下的目标,「大约五十分之一美分一步」——而它下面那张表就是这个说法的测量依据:同一张截图、同一个目标,各做一次决策。输入那一侧是刻意对齐的,jev 4,882 个 token,对照 Claude Opus 5 裸看截图 4,785 个,表里给这一行标的是「same」。输入既然相等,差距就全在输出侧,README 也讲明了原因:TypeSafe 用几百毫秒回答一个最多 255 个选项的
Choice,给出完整的概率分布和一个校准过的置信度,而输出 token 是免费的。于是每次决策 $0.0002 对 $0.032,便宜 155 倍;十二步的任务 $0.003 对 $0.40 到 $0.90;端到端算上截图与 OCR 约 1.5 秒,对约 5.5 秒。材料里没有的是算式本身。整个仓库里找不到每 token 的费率、找不到乘法、也找不到一张账单:4,882 个输入 token 加上免费的输出 token 是怎么变成 $0.0002 的,任何一处都没有写。这个数字是以测量值的形式发布的——输入摆在那里,公式不在页面上——而量级能从另一个方向得到旁证:benchmarks/osworld/下提交进仓库的基准行里带着每个任务的费用,PR 53 里记下的那批六个任务从 $0.0013 到 $0.0054。 - 02
是读屏,不是看图
仓库的第一次提交叫「Screen OCR to TypeSafe Choice to mouse click prototype」。README 的理由是:前沿模型每一步都要把截图发出去、再等上几秒换一个计划,而大多数步骤只需要从一张短名单里做一个又快又便宜、还带一个可以用来设阈值的置信度的选择。这个取舍的代价写在同一份文档里:大模型是直接从像素上读出活动日期并自己比较的,而分类器需要
dates.py里的日期解析,因为「前沿模型免费做掉的每一段推理,在这里都必须重建成确定性的状态」。感知之所以是两个来源,是因为没有哪个够用:在一台 Mac 上量十个应用,无障碍树给 Finder 屏幕上 100% 的控件打了标签,Chrome 88%,而 Spotify 是 0——它的 CEF 外壳只暴露三个窗口按钮——所以树是「额外来源,从不是替代品」。OCR 是贵的那一半,约占一步的三分之二,而它按文字量而不是像素数计费,于是少读一点屏幕就是唯一的省钱办法:把最前面的窗口裁出来,再加上同样列宽上的菜单栏条,再用一份缓存沿用两次截图之间没变化的格子里那些 OCR 文本行。两种架构的差别是被量出来的,不是被断言的:那个可选的浏览器后端读的是 DOM 而不是像素,在三个页面上拿 OCR 和它对照,OCR 逐字还原出的 DOM 标签只有 13 个里的 7 个、88 个里的 7 个、85 个里的 3 个。 - 03
三个选择题,和一条「不许有两个意思相同的选项」的规矩
一次请求回答三个选择题——哪一类动作、哪一个条目、哪一个网站——当有隐藏控件可给时再加第四个。动作类别从点击、按下不在屏幕上的控件,到输入、浏览器跳转、键盘、滚动,再到等待、完成和停止,一共十三个键。设计文档里解释这套拆分的句子是:「做这个项目的过程中发现的每一次卡住,都来自两个意思相同的选项。置信度衡量的是集中程度,所以重叠的选项永远会被读成犹豫。让动作集合保持互斥。」这条规矩是
CONTRIBUTING.md里的第一条基本规则,而材料里最清楚的案例,是一位读者针对它提交的 bug。系统设置里那个窗口自带一个带标签的「返回」按钮,于是「点这一项」和键盘后退其实是同一个动作:在一次以「返回」为焦点的决策里,类别问题分成了 0.44 对 0.40。只要条目里出现真正带标签的「Back」或「Go Back」无障碍按钮——而不是屏幕上刚好被 OCR 读成这两个词的文字——就把键盘后退去掉,下一次决策就变成了 0.71 的点击。两位贡献者各自独立写了这个修复;浏览器循环里也有同一种直觉的另一种形式:循环执行不了的选项会在分类器看到之前就被过滤掉,因为一次板上钉钉的卡住会被读成模型在犹豫。 - 04
误点是怎么兜住的,以及还有两处没兜住
能不用鼠标点,就不用鼠标点:来自无障碍树的条目是通过树按下去的,「这样这一按落在控件上,而不是落在盖住它的东西上」;实在不行才按方框中心做一次鼠标点击,而且要在关掉挡在前面的弹窗之后。产生这条规则的 bug 被写了下来。在某道基准题上,三次试验里 Chrome 的「要恢复页面吗?」气泡都压在做书签管理器右上角,分类器挑中了页面上的 Organise 按钮,结果这一击关掉的是气泡;这次运行花了两个步骤、大约十三秒才发现这件事。修法是把弹窗当成浏览器窗口旁边的一个独立窗口,用弹窗自己的关闭按钮关掉它,没有的话用 Esc,绝不用它别的按钮,因为「恢复」会把上一次会话重新打开。措辞本身也是调过的:在 81 份带弹窗屏幕的历史请求回放里,说某个条目被弹窗「盖住」会把分类器往 Esc 那边拉、并压低它的置信度,而说它「在弹窗下面」则两者都不动。更粗的一层网是卡住规则:每一步都留一份屏幕签名,连续三次动作之后屏幕跟原来一样就停,或者出现两次此前已经在同一屏幕上做过的动作也停。这些规则刻意偏向另一边——如果每一步的变化都超过这个程度,那一次毫无进展的运行会一直跑到步数上限,因为规则「宁可继续跑,也绝不肯停下一次正在推进的运行」。它还允许用 Ctrl-C、或者把鼠标甩到屏幕左上角来停下一步运行,这两个检查发生在每一次点击、按键和滚动之前,也发生在输入的字符之间,所以一次运行可以停在半个词上。材料里还有两处缺口被报告出来且仍未合上:一个输入框在值读不回来时会退回键盘输入,而那次输入打给的是当时持有焦点的东西;浏览器循环在分类器自己的满足度到 0.5 时就结束运行,有一份报告说这是抛硬币,而且它会盖过一次有把握的点击。
- 05
十三天、一个 beta 版本,和一份从 725 行砍到 146 行的 README
仓库建于 2026-09-16,写这份记录之前最后一次提交落在 2026-09-29:十三天里 116 次提交,其中 109 次出自作者。它一共只有一个 release,2026-09-22 发布,标题是「v0.2.0 (beta)」,带着 prerelease 标记;tag 只有
v0.1.0和v0.2.0,项目文件里写的版本是 0.2.0。beta 提示写得很直白:「这是一个重度开发中的东西。……它会驱动你真实的鼠标和键盘,所以先从干跑开始。」文档被有意重组过一次,连数字都记了下来:README 从 725 行砍到 146 行,只留下新人头两分钟需要的东西,其余全部搬进docs/,一个主题一页。因为这个工具驱动的是真电脑——而且通常就是维护者自己的 Mac、还常常是他正在用的时候——所以写给「在这个仓库上干活的 agent」的规则要求:在任何会接管屏幕、输入、应用或云上机器的操作之前,都必须先问,每一次都要问,而一次批准只覆盖那一条命令,不覆盖这一类命令。测试靠tests/conftest.py里的一道护栏与真机隔开:任何会碰到机器的调用在测试期间一律拒绝,而每新增一个这样的调用,必须在同一次改动里加进护栏。循环做不到的任务,会被写成tests/world.py那台模拟电脑上的场景,由真正的步骤循环驱动,用一段策略顶替分类器,并在循环能做到之前一直挂着失败——因为「循环里的每一条停止规则都是这样被发现或修好的」。边界也被写下来,而不是留给别人去撞:只截主显示器;终端、canvas 或 Spotify 里那种只有图标的按钮,两个来源都够不着;而运行期间你继续用这台机器,就会跟它抢焦点和光标。 - 06
基准、一次被留在记录里的失败,以及外面的贡献者
jev 在 OSWorld 里作为一个 agent 跑,身旁是 OSWorld 自带的 GPT agent 跑同一道题,同样五十步、同样每个动作之后等两秒。第一次真跑某道 Chrome 题时,四次运行和一串修复之后,jev 以 1.0 的成绩、六次点击完成了任务,而对照的那个 agent 一次都没跑起来,因为账号对那台机器的访问在会话中途失效了。作者自己主动压着不合的那处改动更有意思。OSWorld 在每个动作之后固定等两秒,而这是按步计费的,等于在惩罚那种「步数更多但更便宜」的 agent,于是这个等待被改成 0;但 jev 自己的「等待」动作一直指向同一个常量,改到 0 之后,等待就什么都不等了。在 0 秒下,那次运行六道题只解出一道,而基线是六道全解,原因被原样记下来而不是被概括掉:截图是在 Chrome 还没画完时就拍的;jev 的树又是在那张截图之后取的,于是两者说法不一致;而被拒绝的动作什么都不发,于是没有新的观测进来。那条 pull request 在正文里写着,它是「应维护者要求压着不合的:速度和准确率要连同接下来的两处改动一起称过,然后才谈合并」,而失败的那几行数据就趁这段时间提交进了仓库。与之对照的,是作者为那些不给自己留面子的比较专门留的一个文件:jev 只看到自己最近八次动作的文本,判断不出一个动作到底有没有生效;分类器对同一个请求挑中同一个条目,大约只有 95% 的时候。外面的贡献很薄:作者之外七个人,每人一次提交,而最近三十条 issue 与 pull request(编号 31 到 60)大多出自他们。一位读者提了六个带编号的 bug;另一位在三个小任务里找出七个 macOS bug——计算器里的一次乘法、文本编辑器里的新建文件、浏览器里点一个链接——附上 740 个通过的测试。第七个是只有真机器才会长出来的那种:一个键盘快捷键按完之后把 Command 留在了按下状态,此后输入的每一个字母都变成 Command 组合键,文档里什么都没进去,而下一次点击落成了一个 Command 点击。
相关档案
全部档案 →第 078 号
OpenChatCut
一个本地优先的视频剪辑器,剪辑方式是跟它说话:内置 agent 与外部 Codex、Claude Code 会话调用的是界面自己在用的同一套剪辑工具,于是每一处改动都落在一条真实的多轨时间线上——是片段、转场、字幕、特效或音频,仍然能拖、能撤销、能导出。工程与素材留在本机,预览与最终渲染都出自 Remotion。
第 113 号
Chat On Steroids
一个给 ChatGPT 装上本地工具的桌面工作台。它用 MCP 提供文件、shell、终端和整个桌面,再用一个 Chrome 扩展通过隧道驱动用户自己那一个 ChatGPT 对话页面:模型因此能改真实项目,而 app 记录每一次工具调用,并在旧对话装不下时把会话搬进一个新对话。
第 109 号
Whiteboard
一个桌面应用,让 agent 和人共用同一块画布。agent 把审查画出来——时序图、实体关系图、钉在某次提交上的代码引用——而画布上的每个形状都能跳回它被画出来时所指的那段代码。