开源桌面应用 OpenWork 让 Agent 开浏览器干活的两条路:内置面板与 macOS 屏幕操作

2026-08-04

本文基于 openwork 仓库 commit 3b41381(2026-08-03)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/different-ai/openwork 最新代码与文档为准。

**OpenWork 这个开源桌面应用让 Agent 操作浏览器,用的不是另起一个无头浏览器进程,而是把一块 Electron 视图直接挂在应用窗口里,让它对你可见、共享同一份登录态,再把这块视图的调试协议句柄交给 Agent。**这一条决定了它后面所有的优点和所有的代价——你能看着它点,也意味着它没法在你背后悄悄跑;它能直接用你刚登过的账号,也意味着任务之间的账号边界比你想的模糊。

站内已经有几篇相邻的文章:browser-use 是什么 讲的是那套独立的网页自动化框架本身,什么时候别用 browser-use 讲的是选型的反面,Agent 工具设计 讲的是通用的工具面设计原则;本篇只盯一件事——OpenWork 这个具体项目把浏览器能力塞进桌面应用之后,链路上多出了哪些别处没有的机制和风险。

一、它想解决的是”任务在你眼皮底下跑”这件事

先看官方文档怎么说。packages/docs/start-here/do-work-with-it/control-the-browser.mdx 第一句就把范围画死了:OpenWork 里的 computer-use 目前通过内置的 OpenWork Browser 工作,还不意味着能完整控制 Ubuntu、macOS 或 Windows 的桌面应用。文档接着说明它能做的事:打开页面、点击、填表单、读取页面内容、截图,全都发生在那个内置浏览器里;如果你连的是远程 OpenWork server,浏览器就跑在那台远端 worker 上。

这个定位和纯脚本式的网页自动化不一样。脚本式方案里,浏览器是一个你启动、你销毁、你注入 cookie 的从属进程;在 OpenWork 里,浏览器是产品界面的一部分,用户会在会话右侧看见它。仓库里那份内置扩展的说明文案写得很直白——apps/app/src/react-app/domains/settings/browser-extension-config.tsx 里那段 UI 文字说,OpenWork Browser 跑在应用内部,为浏览器任务可见地打开,并且是 OpenWork 中受支持的浏览器自动化路径。

“可见”不是宣传词,是被写进代码的约束。apps/desktop/electron/browser-panel.mjs 里有一段注释说得很清楚:Agent 通过调试协议驱动的导航可能打到一个视图已经被摘下来的后台标签,所以要把那个标签重新选中拉到屏幕上,否则导航”成功了”而可见标签还停在空白页。为了保证你看得见,它宁可打断你当前正在看的标签。

二、内置浏览器面板:标签是视图,句柄靠自造的标记页认领

browser-panel.mjs 是一个工厂函数 createBrowserPanel,主进程只负责建窗口,标签状态、视图生命周期、代理配置、以及一整套浏览器 IPC 注册都在这里。几个关键点值得单独拎出来。

**每个标签是一个 WebContentsView,统一挂在同一个持久化会话分区上。**分区名是 persist:openwork-browser。建标签时它把 sandbox 打开、contextIsolation 打开、nodeIntegration 关掉,并挂一个内容预加载脚本 browser-content-preload.cjs。新建视图后会立刻加载 about:blank,代码注释解释了原因:抢在持久化会话恢复之前把文档占住,而 cookie 挂在会话对象上不挂文档,所以这么做不会把你的登录态洗掉。

**Agent 拿到的不是标签对象,是一个调试协议句柄。**这是整条链路里最值得看的一段。openBrowserUrlForAutomation 的流程是:建一个空白标签 → 先把这个标签导航到一个自造的 data: 页面 → 在调试协议的目标列表里轮询找到这个页面 → 再把标签导航到真实 URL → 把句柄返回给调用方。返回的形状是:

return {
  provider: "builtin",
  browser_url: cdpBrowserUrl(),
  target_id: targetId,
  tab_id: tab.tabId,
  url,
};

为什么要绕那个 data: 页面?因为调试协议的目标列表里没有”哪个 target 对应哪个标签”的稳定映射。它的解法是给标签发一张身份证:browserTargetMarkerUrl 生成一段内联 HTML,标题、meta 标签和 body 里都写上 openwork-browser-tab:${tabId},然后在目标列表里按 URL 里包含这个标记、且类型是 page 来认领。轮询有硬上限,超时和间隔都是模块顶部的常量(BROWSER_TARGET_RESOLVE_TIMEOUT_MSBROWSER_TARGET_RESOLVE_INTERVAL_MS),认不到就抛 Could not resolve built-in browser CDP target.

这套”先占位再认领”的做法,和用会话 ID 直连一个远端浏览器是两种思路,取舍可以对照 browser-use 的 session 与 CDP 连接 那篇看。

provider 参数目前只有一个真实取值。openBrowserUrlForAutomation 第二个参数是 provider,但它只接受 autobuiltin,其它值直接抛 Browser provider is not available yet:。渲染层那个控制动作 browser.open_url(定义在 apps/app/src/react-app/domains/session/chat/session-page.tsx)在参数描述里写明 external 是留给未来的。你现在写调用方,别为 external 留分支。

**几处外溢口子被显式收窄了。**标签里的 window.open 一律被拒,改成交给系统浏览器打开;主窗口自身的跨源 http(s) 导航会被判定拦下,然后路由进内置浏览器;标签导航如果命中 openwork://openwork-dev:// 这类自定义协议,会被截下来交给深链回调处理,再把标签退回空白页并收起面板——代码注释特别说明了这里要延迟处理,避免在导航事件里同步关标签把渲染进程搞崩。

代理是会话级的,凭据可以只写一个名字。setBrowserProxy 拿到输入后解析成规则,作用在 persist:openwork-browser 这个会话上,并且把本地地址设为绕过:

await browserSession.setProxy({ proxyRules: parsed.rules, proxyBypassRules: "<local>" });

三个细节:一是输入支持 env:NAME 形式,实际读的环境变量名会被拼成 OPENWORK_BROWSER_PROXY_ 加大写名字,读不到就报错提示你去设;二是代理必须同时带主机和端口,缺一个直接抛错;三是设完代理后会调用会话的 closeAllConnections(),代码注释写明意图是断掉长连接,防止已经开着的标签绕过新代理。代理的账号密码不在状态里回传,browserProxyState 只回规则和一个是否需要认证的布尔值,真正填账密发生在 Electron 的 login 事件回调里。

三、屏幕操作支线:能力更大,门槛全在系统权限那一层

第二条路是 Computer Use,桌面侧的胶水代码在 apps/desktop/electron/computer-use.mjs。这个文件本身不实现任何点击,它干四件事:找到随包分发的 helper 应用、检查权限、列出正在运行的应用、打开权限设置界面。

**helper 是一个独立的 macOS 应用包。**常量写着 OpenWork Computer Use.app,可执行文件名是 ComputerUse。查找顺序是先看环境变量(OPENWORK_COMPUTER_USE_BINARY 指二进制,OPENWORK_COMPUTER_USE_APP 指应用包),再看安装包资源目录下的 helpers,最后落到相对路径。打开权限设置界面时优先用应用包,注释写明了用意:让 macOS 把它当成一个有自己 Dock 图标和权限身份的真实应用,而不是一个转瞬即逝的 Node 或 Swift 进程。这一步不是审美问题——系统权限是按应用身份授予的,身份不稳定,权限就每次都要重授。

权限检查故意不做常驻服务。checkComputerUsePermissions 每次都新起一个进程,用 --check 参数跑一次,从标准输出读一段 JSON,解析出 okaccessibilityscreenRecording 三个布尔值,超时五秒。文件里那段注释直接写了理由:新进程等于一次新鲜的系统权限读取,永远准确,不需要 HTTP 服务。这是个很实在的取舍——用一点进程开销换掉”缓存的权限状态和真实状态不一致”这类最难查的 bug。

列应用这条路不需要权限。listRunningApps--list-apps 参数拿正在运行的应用名,非 macOS 平台直接返回空。注释说明它不需要系统权限,所以在 Computer Use 还没配好之前就能用,服务的是会话输入框里 @App 那种提及。

MCP 命令有三级回退。getComputerUseMcpCommand 先用找到的 helper 二进制加 mcp 子命令;打包版找不到 helper 就直接抛 OpenWork Computer Use is missing from this OpenWork build.;开发模式下(OPENWORK_DEV_MODE1)走仓库里的入口脚本;再不然落到 npx@openwork/handsfreemcp

底下那层控制能力,packages/handsfree/README.md 描述得比较细:语义化的辅助功能快照配上 {e1} 这类紧凑引用、严格后台模式、按目标窗口截图、通过 CGEvent.postToPid 把输入投递给指定进程和窗口。README 里还写了一个容易被误解的点——helper 会画一个轻量的第二光标叠层让你看见 Agent 在哪儿操作,但那个叠层只是视觉的,严格模式下不会移动你真实的系统光标。核心运行时暴露的直接接口是 snapshotclicktypeTextpressKeyscrollwaitsetValueperformAction 这一组,MCP 只是一层薄薄的标准输入输出包装。另有一条按坐标操作的动作面在 packages/handsfree/src/cua-runner.mjs 里,把模型返回的动作翻译成 cua_clickcua_typecua_scrollcua_keypresscua_drag 这类调用。

内置扩展清单(apps/app/src/app/extensions.ts 里的 BUILT_IN_OPENWORK_EXTENSION_MANIFESTS)把两条路的差别摆得很清楚:openwork-browser 默认启用、三大平台都支持;computer-use 标了 preview: true、平台只写 darwin,并且启用条件是三道门——MCP 已连接、辅助功能权限已授予、屏幕录制权限已授予。

四、四个部件的分工

组成部分它负责什么对应仓库位置你什么时候会碰到它
内置浏览器面板标签生命周期、视图挂载与尺寸换算、调试协议目标解析、代理配置、一整套浏览器 IPCapps/desktop/electron/browser-panel.mjsAgent 要开网页、你要给浏览流量套代理、排查”导航了但页面没变”时
会话侧的控制动作把开链接和设代理包成带参数校验的动作暴露给上层,标注副作用类型apps/app/src/react-app/domains/session/chat/session-page.tsx你想知道 Agent 到底能调哪些浏览器动作、参数怎么校验时
Computer Use 桌面胶水定位 helper 应用、每次重新检查系统权限、列运行中应用、拉起权限设置界面apps/desktop/electron/computer-use.mjs权限一直显示没给、打包版报缺 helper、@App 提及列不出应用时
原生控制运行时辅助功能快照与引用、严格后台输入、按窗口截图、坐标动作面packages/handsfree/(README 与 src/cua-runner.mjs你要判断它到底怎么点、会不会抢你的鼠标焦点时

内置扩展清单本身在 apps/app/src/app/extensions.ts,两条路的默认开关、平台限制和启用条件都写在那里,改造前先读它比读代码快。

五、边界与代价:它明确不管的那些事

**它放弃了”无头”和”后台静默”。**内置浏览器是产品窗口的一部分,前面说过,Agent 驱动的导航打到后台标签时会把标签拉到前台。这对”我要在夜里并发跑两百个页面”这类需求是硬伤——那类活儿本来就该交给专门的无头方案。它换来的是排障成本骤降:任务卡在哪一步,你直接看得见。

**登录态是共享的,不是隔离的。**所有标签共用一个持久化会话分区。好处是 Agent 天然继承你刚登过的账号,不用你导 cookie;代价是任务之间没有账号边界——同一个分区里,A 任务能碰到 B 任务登的那个账号。要按任务隔离身份,这套结构现在不给你这个开关。这一点和 Agent 最小权限设计 那篇讲的原则是直接冲突的,你得自己在任务编排层补。

**远程模式下页面内容不在你本机。**文档写明了连接远程 OpenWork server 时浏览器跑在远端 worker 上。你让它打开的每一个页面、它读到的每一段内容、截的每一张图,都在那台机器上产生。涉及内部系统时,这一条要单独过一遍。

**屏幕操作那条路要的是机器上最重的两个权限。**辅助功能加屏幕录制,合起来意味着它能看见你屏幕上的一切、能替你操作任何应用。清单里给它标了预览状态、限定了 macOS,这已经是一种自我提示。装之前先想清楚这台机器上还开着什么。

**凭据是集中保管的。**桌面应用替你保管模型服务商的密钥、第三方服务的授权、代理的账号密码。集中保管带来便利,也把暴露面收拢到一个点上:一个应用被攻破,等于全部授权被攻破。代理密码从环境变量或 URL 里读,环境变量会被同机上的其它进程读到,这是操作系统层面的事实,不是这个项目能修的。

**团队控制面那块的许可证不是 MIT。**仓库根目录的 LICENSE 写的是分层授权:/ee 目录下的内容按 ee/LICENSE 里定义的那套条款,其余部分才是 MIT(Copyright 2026 Different AI)。这里还有一个细节值得你自己去看原文——根 LICENSE 在提到 /ee 时括注的是 Fair Source License,而打开 ee/LICENSE 这个文件,抬头写的是 Functional Source License, Version 1.1, MIT Future License,缩写 FSL-1.1-MIT。两处措辞并不完全一致,这恰好说明为什么许可证这种事不能听二手转述,包括不能听本文的。这个项目面向组织的那套控制平面(README 把它称作管理团队与组织的控制平面)代码就在 ee/ 目录里,走的是前一套条款而不是 MIT。所以”OpenWork 是 MIT 开源项目”这句话本身就不成立——它是分层的。能不能商用、能不能改、改完能不能对外提供服务,一律以这两份许可证原文为准,本文不提供法律意见,落地前请让法务读原件。

**它明确不管的:**站点的验证码与风控策略、目标站点的服务条款、凭据轮换、以及”这个操作合不合规”的判断。这些不在代码里,也不该指望代码替你兜。

六、上手与避坑清单

**别把仓库里那条开发用的调试链路当成用户任务链路。**仓库里有一份浏览器自动化的技能说明文件,讲的是开发期怎么用调试协议驱动 OpenWork 应用本身的界面做冒烟测试。它自己在末尾写了一句注解:那条链路是给开发测试工具用的,用户的浏览器任务应该走内置 OpenWork Browser 的目标。会踩是因为两者都叫”用 CDP 控制浏览器”,看起来一模一样;避法是先分清你在驱动的是应用外壳还是应用里的网页面板。

**调试端口的默认值别照抄文档。**同一个仓库里两份材料给的默认端口不一致:技能说明文件里写的是一个值,evals/browser-extension-flows.md 的预检步骤里写的是另一个值。会踩是因为你搜到哪份就信哪份;避法是启动后自己确认监听端口,或者用 OPENWORK_ELECTRON_REMOTE_DEBUG_PORT 显式指定,别依赖默认值。

**provider 传 external 会直接报错,不是静默降级。**会踩是因为参数描述里提到了 external,读起来像是可选值之一;避法是只传 builtinauto,并且在你的调用方把错误信息原样透出来——它报的是”这个 provider 还不可用”,不是”URL 不对”,两者的排查方向完全不同。

**代理串必须同时带主机和端口。**只写主机会抛错,错误信息里直接给了两个正确样例的形状。会踩是因为很多工具允许省略默认端口;避法是养成写全的习惯,或者干脆用 env:NAME 那种间接引用,把完整串放在环境变量里,避免代理密码出现在会话记录中。

**权限”明明给了却不生效”,先重新触发那次检查。**会踩是因为很多应用把系统权限状态缓存在内存里,你在系统设置里勾了,应用不知道。这个项目的做法是每次都新起进程读一次,所以你该做的是重新触发一次检查,而不是重启整个应用碰运气。如果打包版报的是缺 helper(错误信息里明说这个构建里没有那个 helper),那就不是权限问题,是安装包本身的问题,重授多少次都没用。

**别指望它在 macOS 之外有屏幕操作能力。**会踩是因为内置浏览器那条路三大平台都能用,容易被推广成”Computer Use 也一样”;避法是记住清单里的平台字段——列运行中应用那个函数在非 macOS 上直接返回空,这是代码层面的硬边界。

**改浏览器代理之后,别假设你自己的其它请求也跟着换了。**这里的代理只作用在内置浏览器那个会话分区上,本地地址还被设成绕过。会踩是因为”我设了代理”这句话在人的直觉里是全局的;避法是分清哪些流量走这块面板、哪些走你自己的代码。

收个尾

判断这套东西适不适合你,问三个问题就够了:任务需不需要被人看着跑(需要就是它的主场,不需要就用无头方案)、任务之间要不要账号隔离(要,你得自己在上层补)、机器上能不能接受多两个系统级权限(不能,那第二条路直接排除)。

要继续往下读代码,顺序建议是:先 apps/app/src/app/extensions.ts 看两条路的默认状态和启用条件,再 apps/desktop/electron/browser-panel.mjs 看标签与句柄怎么产生,最后 apps/desktop/electron/computer-use.mjspackages/handsfree/README.md 看原生那层的能力边界。文档侧只有一页 control-the-browser.mdx,短,但把范围划得比代码还清楚——先读它能省不少弯路。

本篇属于一个把开源AI 工作流桌面应用 OpenWork逐层拆开讲的系列,整体地图见 OpenWork 是什么:把技能与 MCP 打包成能力的开源桌面应用;沿着这条线往下,还可以看 OpenWork 开源桌面应用的跨会话记忆:一层你能读能改的明文记忆开源桌面应用 OpenWork:什么在烧 token,账单怎么长出来

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。