OpenWork 桌面应用与 OpenCode 内核:能力分发层的边界

2026-08-04

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

OpenWork 这个开源桌面应用根本没有自己写 Agent 内核,它是把 OpenCode 当成子进程拉起来、通过本地 HTTP 说话的。 所以拿这两个项目比「谁的 Agent 更能干」是个问错的问题——一个在做会话执行内核,一个在做能力的打包与分发,中间隔着一条清晰的进程边界。这条边界具体划在哪、两边各自放弃了什么,是本文要拆的东西。

先做名字消歧:这里说的 OpenWork 指 different-ai/openwork 这个开源桌面应用项目,跟同名的职场点评网站没有关系,也不是「开放工作」这类泛指。下文出现的 OpenWork 一律是专有名词。

站内已经有几篇相邻的文章,分工不一样:开源 Agent 项目三体对比 横向铺开若干开源 Agent 的定位差异,Hermes 四方对比 站在常驻自托管 Agent 的视角比,Agent 框架对比 讲的是写代码时选哪套框架。本篇不做横评,只做一件事:把 OpenWork 与它实际引用的那个引擎之间的那条缝,按仓库文件逐段量出来。

一、两边各自怎么定义自己

OpenWork 仓库 README 第一句这样定位自己:一个用于分享 AI 工作流的免费开源桌面应用,并且写着它是 Claude Cowork 和 Codex 的开源替代,支持 macOS、Windows、Linux。这是项目自己的说法,不是本文的判断——对标谁属于项目方的市场定位,读者自己去看那句原文即可。

紧接着 README 说了一句更值得注意的话:桌面应用是「你想要一个专属工作区时才需要」,并不是必须的,你可以从已有的 Agent 里直接用 OpenWork。这句话把这个项目的重心暴露了——它的核心资产不是那个桌面壳,而是壳里那套可以被搬走的能力。

对面那个仓库的 README 只有一句定位:开源的 AI 编程 Agent。它的 README 绝大部分篇幅是安装方式、桌面版下载、以及两个内置 agent(build 是默认的全权限开发 agent,plan 是只读的分析与探索 agent,用 Tab 键切换),外加一个用 @general 调用的通用子 agent。

真正说明它在关心什么的是它的 AGENTS.md。那份文件里写的是:运行时依赖方向必须从 Schema 指向 Core 和 Protocol,再从 Core 和 Protocol 指向 Server;Client 的运行时代码可以依赖 Schema 和 Protocol,但绝不能依赖 Core 或 Server。往下还有一整节 V2 Session Core,讲的是持久化的 prompt 接纳要和模型执行分开、SessionV2.prompt(...) 先落一行持久输入再去调度 SessionExecution.wake(sessionID)、每个 provider 回合保留一次显式的 llm.stream(request) 调用、SessionRunCoordinator 怎么合并同一会话的唤醒。

这就是内核层在乎的东西:一次输入怎么被可靠地接住、怎么被调度、怎么在崩溃与重试之间不丢不重。它不关心这次输入背后那个技能是谁发布的、你有没有权限用。

二、OpenWork 是怎么把引擎拉起来的

这部分不用猜,apps/server/src/managed-opencode.ts 这个文件从头到尾就干这一件事。

它导出一个 createManagedOpencodeServer,接收 bincwdhostnameportexcludedPortstimeoutMsenv 这些选项。默认 hostname 是 127.0.0.1,端口通过一个 findFreePort 拿——先起一个临时的 net.createServer() 监听 0 端口,读回系统分配的端口号再关掉,最多试 20 次以避开排除集。

拉起来的命令行参数是写死的:

const args = ["serve", "--hostname", hostname, "--port", String(port), "--cors", "*"];

也就是说它用的是引擎的 serve 模式,把引擎当一个本地 HTTP 服务用。认证靠一对随机口令,生成方式是两个去掉横杠的 randomUUID() 拼接,分别注入成 OPENCODE_SERVER_USERNAMEOPENCODE_SERVER_PASSWORD 两个环境变量。apps/server/src/opencode-connection.ts 里再把这对用户名口令拼成 Basic 认证头,挂在每次请求上。

启动完成的判定也很朴素:监听子进程的 stdout,逐行找以 opencode server listening 开头的那行,再用正则从里面抠出 URL。超时默认 15000 毫秒,超时就抛错。关停走的是两段式——先 SIGTERM,最多等 1000 毫秒,还活着再 SIGKILL,再等 500 毫秒。

另一个细节值得工程师留意:这个文件里有一条 SECRET_ENV_PATTERN 正则,匹配 TOKENPASSWORDUSERNAMEAUTHSECRETKEYCREDENTIAL 这些片段,凡是名字命中的注入环境变量,在对外暴露的执行快照里一律替换成 <redacted>。快照本身(命令、参数、工作目录、脱敏后的环境变量列表)会通过 apps/server/src/embedded.ts 的返回值往外抛,供诊断用。这是个很实在的设计:既让你能看清引擎到底是怎么被拉起来的,又不至于把口令打进日志。

embedded.ts 是把这一切串起来的地方。它做的事按顺序是:解析服务端配置,找到那个被托管引擎的工作区,写一份服务端托管的运行时配置文件并保持它随运行时数据库的写入而刷新,清扫历史遗留的引擎配置,拉起引擎,把引擎的 URL 与账号口令回填进配置和每个工作区条目,把这个引擎进程登记为可信进程,启动自己的 HTTP 服务,最后尽力把所有工作区在运行时数据库里登记的 MCP 推给引擎。

注入给引擎的环境变量里,除了那对口令,还有四个键:OPENWORK_SERVER_URLOPENWORK_SERVER_TOKENOPENCODE_CONFIGOPENCODE_MODELS_URL。前两个让引擎能回头调 OpenWork 服务端,第三个把引擎的配置文件指向 OpenWork 生成的那份运行时配置。文件里有一段注释把设计意图写得很直白:这份配置文件由服务端托管,引擎每次重建实例时都会从磁盘重新读,所以每次都能拿到当前状态。

如果不用桌面壳,apps/server/src/cli.ts 走的是同一条路,只是开关换成环境变量:OPENWORK_MANAGE_OPENCODE 设为 1 才托管,引擎二进制路径读 OPENWORK_OPENCODE_BIN,工作目录读 OPENWORK_MANAGED_OPENCODE_CWD

三、能力分发层到底在分发什么

引擎之上那一层,才是这个项目真正的产品面。

README 说得很清楚:给 Codex、Claude Code、Cursor 或其它兼容 Agent 加一个 OpenWork MCP,就能在这些工具、同事和机器之间复用同一套技能、MCP 连接与已接服务。这个远程 MCP 服务只暴露两个工具,search_capabilities 负责找出你能用什么,execute_capability 负责执行它。

packages/docs/model-context-protocol/opencode.mdx 这份接入文档把理由讲明白了:只暴露这两个工具,客户端就是先搜索自己有权访问的东西、再按精确的能力名执行,而不是(照文档原话)把上百条工具定义装进上下文。这个取舍值得单独琢磨,可以对照 MCP 工具数量 那篇看。同一份文档给出了对面这个引擎的接入配置:

{
  "mcp": {
    "openwork": {
      "type": "remote",
      "enabled": true,
      "url": "https://api.openworklabs.com/mcp/agent",
      "oauth": {}
    }
  }
}

加完之后跑 opencode mcp auth openwork 走浏览器登录,选定组织;换组织要先 opencode mcp logout openwork 再重新认证。文档里有一条提示说,这个客户端是经过验证的 OpenWork Connect 客户端,原生远程 MCP OAuth 通过了端到端实现测试。

架构设计文档 docs/marketplace-capabilities-architecture.md 把这一层的扩展方式写成了一条规则:能力源可以增加(文档把市场插件能力称作这条通道上的第四类能力源,排在 Den 的 REST 目录、外部 MCP 连接、原生 provider 能力之后;Google Workspace 这类原生能力就是挂成 Den REST 路由再自动进目录的,记忆库那侧则是靠执行一条被搜到的能力来取,而不是另注册一个专用工具),但那句「MCP 工具面不增长」是硬约束——永远只有 search_capabilitiesexecute_capability 两个工具。同一份文档还写了一句挺关键的话:把文件复制进 .opencode/ 这种安装动作,变成了离线使用和版本固定的优化项,而不是使用组织内容的前提。

下面这张表是这个仓库里几块的分工,路径都是实际存在的:

组成部分它负责什么对应仓库位置你什么时候会碰到它
引擎托管拉起引擎子进程、生成随机口令、探活与两段式关停apps/server/src/managed-opencode.ts桌面应用启动、或本地自己跑服务端时
内嵌服务端入口把配置解析、引擎托管、启动 HTTP 服务合成一次调用并返回句柄apps/server/src/embedded.ts把 OpenWork 服务端嵌进别的进程时
命令行入口用环境变量决定要不要托管引擎apps/server/src/cli.ts不用桌面壳、直接跑服务端时
引擎连接解析把工作区或全局的账号口令拼成 Basic 认证头apps/server/src/opencode-connection.ts排查工作区连不上引擎时
工作区文件布局约定技能、命令、插件与 OpenWork 配置的落盘位置apps/server/src/workspace-files.ts手写或排查一个技能时
可移植配置裁剪导出配置时只保留白名单里的顶层键apps/server/src/portable-opencode.ts分享或导出工作区配置时
客户端接入文档十份不同 MCP 客户端的接入指南packages/docs/model-context-protocol/不装桌面应用、从已有 Agent 接入时
团队控制面成员、团队、市场、推理供给、桌面策略ee/apps/ 下的十个应用组织侧发布和分配能力时

工作区的落盘约定在 workspace-files.ts 里是四行代码就能读完的事:技能在工作区根目录下的 .opencode/skills,命令在 .opencode/commands,插件在 .opencode/plugins,OpenWork 自己那份配置是 .opencode/openwork.json。注意它是挂在引擎的目录约定之下、而不是另起炉灶——这本身就是外壳寄生在内核约定上的一个证据。

portable-opencode.ts 里那份白名单更能说明边界:可移植的引擎配置只保留九个顶层键,agentcommandinstructionsmcppermissionpluginsharetoolswatcher,其余一律丢弃。换句话说,能跟着你走的配置是被显式枚举出来的,不在名单里的东西不会被带走。

四、设计取向上的四处差异

每条差异都能在两边文件里各自找到落点,这里不排座次,只描述取向。

第一处在关心的粒度上。 对面 AGENTS.md 花大量篇幅约束包与包之间的依赖方向、要求在包目录里跑 bun typecheck 而不能在仓库根目录跑测试(有一个叫 do-not-run-tests-from-root 的守卫),还规定了 Drizzle schema 字段用 snake_case、避免 else、避免 try/catch、避免星号导入这类风格条款。这是一个把内核代码质量当命门的仓库。OpenWork 的 docs/ 那 20 份 md 是另一副面孔:AWS EKS、Azure AKS、GCP GKE 的 Helm 部署,Microsoft Entra 的 SSO 与 SCIM,桌面应用策略,外部 MCP 的 OAuth 管理。同一个词「架构」,两边指的完全不是一件事。

第二处在工具面的形状上。 内核那侧的工具注册表是随作用域走的,AGENTS.md 里明确要求把 SessionRunner、模型解析、工具注册表、权限、文件系统都按 Location 作用域来管。分发层这侧则反过来把工具面压到两个,靠一次搜索加一次执行来控制上下文成本。

第三处在信任边界上。 引擎被当作一个本地进程对待:绑 127.0.0.1、临时随机口令、进程被显式登记为可信、停止时再显式清除。而 docs/external-mcp-oauth.md 写得更直接——OpenWork 发现和授权由控制面管理的外部 MCP 连接时,不把 provider 凭据放进 Agent 引擎;令牌、刷新令牌、客户端密钥、PKCE 校验串和待处理的授权事务都加密留在控制面。这是一条明确的架构红线,跟内核那侧「会话内的东西尽量自洽」的取向是两种思路。

第四处在配置的所有权上。 引擎侧的配置本来是用户自己写的 opencode.json.opencode/ 目录。托管模式下这份配置改由服务端生成并持续刷新,通过 OPENCODE_CONFIG 指过去。好处是组织给你分配了什么能立刻反映到引擎;代价是你手改的那份配置在托管路径下未必是最终生效的那份。

五、边界与代价:它明确不管什么

许可证是分层的,不能笼统说成 MIT。 仓库根目录的 LICENSE 写得很清楚:/ee 目录下的全部内容按 ee/LICENSE 定义的 Fair Source 许可证;第三方组件按各自原始许可证;这两者之外的部分才是 MIT,Copyright 2026 Different AI。而 ee/LICENSE 打开是 Functional Source License, Version 1.1, MIT Future License(缩写 FSL-1.1-MIT),Copyright 2026 Different AI Inc。这一点在实践中很要紧,因为团队控制面那十个应用(ee/apps/ 下的 den-api、den-controller、den-gateway、den-web、den-worker-proxy、den-worker-runtime、inference 等)全在 /ee 下。本文不提供法律意见,能不能商用、能不能改、改完能不能分发,一律以许可证原文为准。

它不替你解决内核层的问题。 会话怎么持久、崩溃后怎么恢复、每个 provider 回合怎么流式调用,这些都在引擎那侧,OpenWork 只是把引擎拉起来并往里注配置。引擎行为不对,翻分发层的代码没用。

凭据集中保管本身就是暴露面。 把第三方服务授权从每台机器收拢到一个控制面,好处是能撤销、能审计、能按人分配;代价是这个控制面成了高价值目标。要判断值不值,得先想清楚你的权限模型,Agent 最小权限设计 那篇讲的原则在这里同样适用。

企业侧看得到的东西比很多人以为的多。 docs/desktop-app-policies.md 写着桌面策略配置是从云端通过 GET /v1/me/desktop-config 拉下来的,策略目录集中定义在 packages/types/src/den/desktop-policies.tsdesktopPolicyDefinitions,布尔策略键跨匹配策略做 OR 合并,allowedDesktopVersions 还能限制组织可用的应用版本。README 那一节也直说了:控制面可以设桌面策略、限制本地模型访问、控制版本。这些能力本身是中性的,但装之前你应该知道它存在。

推理供给与模型访问这类规则会变。 README 写的是控制面负责规模化提供推理、并控制哪些成员和团队能用哪个模型 provider;具体到怎么分、按什么维度分,属于会随版本调整的部分,以官方最新说明为准。

路线图只是文档里的说法。 packages/docs/roadmap.mdx 自己就分了 Live、Partial、Building、Next、Exploring 五档,并写明 Exploring 是方向而非承诺。隔离沙箱工作区在那份表里标的是 Partial,注明今天还有平台与配置上的限制。引用到这类内容时,最好按「文档里这么写」来读,别当成已经交付的能力。

六、上手与避坑清单

多 worktree 并行开发会撞在一起。 为什么会踩:README 说单个 checkout 直接 pnpm dev 就行,会复用共享的开发档案;但两个 worktree 同时跑就会抢同一份档案和同一个调试端口,抢不到锁的那个实例会直接退出。怎么避:用 pnpm dev:worktree,它把 OPENWORK_DEV_PROFILE 设成 auto,按 worktree 路径推导出稳定的档案名,并让 Electron 和 Vite 各自挑空闲端口。也可以手动指定档案名,同时把远程调试端口和 PORT 都设成 0。

macOS 上新档案会被钥匙串弹窗卡死主循环。 为什么会踩:全新档案没有已存凭据,Chromium 一旦持久化带认证的 cookie 就会触发系统钥匙串弹窗,那个模态框会阻塞主循环。怎么避:dev:worktree 默认把 OPENWORK_ELECTRON_USE_MOCK_KEYCHAIN 设为 1;只有你确实要在隔离档案里用系统钥匙串时,才把它设回 0。

找不到引擎进程时先看启动横幅。 为什么会踩:托管起来的引擎端口是运行时随机挑的,你按固定端口去连必然扑空。怎么避:开发启动会打印一行横幅,里面带着档案名和 CDP 地址,用它去定位档案目录和调试入口;服务端这侧则可以读 embedded.ts 返回的那份执行快照,里面有命令、参数、工作目录和脱敏后的环境变量。

引擎二进制没在 PATH 里会以超时的形式报出来。 为什么会踩:托管逻辑默认直接执行名为 opencode 的命令,找不到时的表现是子进程异常退出或等不到那行监听输出,读起来像是启动慢。怎么避:显式设置 OPENWORK_OPENCODE_BIN 指向真实路径,别依赖 PATH。

手改的引擎配置在托管模式下可能不生效。 为什么会踩:托管路径下配置文件由服务端生成,并且会随运行时数据库的写入持续刷新,引擎每次重建实例都从磁盘重读。怎么避:托管模式下把 OpenWork 的运行时状态当成配置的唯一来源;需要临时手工验证时,走非托管路径、自己给出引擎地址。

从已有 Agent 接入时,组织是绑在令牌上的。 为什么会踩:接入文档写明 OAuth 登录后你选的那个组织会被固定进令牌里,选错了不会自动纠正。怎么避:按文档先登出再重新认证,在浏览器里重新选组织。

导出配置会丢东西,而且是故意的。 为什么会踩:可移植配置只保留那九个白名单顶层键,不在名单里的字段直接不带走,你在目标机器上会以为配置没生效。怎么避:导出前先对照白名单确认你依赖的字段在不在里面,不在的部分另行处理。

收束

判断这类项目,别从「它能不能替代某个工具」入手,从「它在哪一层」入手。OpenWork 这个开源桌面应用把自己放在能力的打包、分发与治理这一层,内核那一层它直接复用了别人的实现,还留出了非托管路径让你把引擎地址换成自己的。这种分层本身没有优劣,只有匹配不匹配。

想自己核一遍,建议按这个顺序读四个文件:先 apps/server/src/managed-opencode.ts 看进程边界怎么划,再 apps/server/src/embedded.ts 看配置和凭据怎么流动,然后 apps/server/src/portable-opencode.ts 看什么东西被允许跟着你走,最后 docs/external-mcp-oauth.md 看第三方凭据停在哪一层。读完这四份,你对这条边界的判断就不用依赖任何人的转述了。

本篇属于一个把开源AI 工作流桌面应用 OpenWork逐层拆开讲的系列,整体地图见 OpenWork 是什么:把技能与 MCP 打包成能力的开源桌面应用;沿着这条线往下,还可以看 开源项目 OpenWork 怎么保质量:一条评测正路加一道只问安全的闸门OpenWork 开源桌面应用仓库导读:改一处功能该从哪个目录进去

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