跳到主要内容

数据截至 (上游 commit a9c304f343f1)

OpenWork — 架构与原理

30 秒导读: OpenWork 是一款开源桌面 App(macOS / Windows / Linux),让非程序员也能在自己电脑上、对着自己的文件用 AI agent 干活——它是 Claude Cowork / Codex 的开源替代。底层的"agent 大脑"直接复用开源引擎 OpenCode;OpenWork 在它外面套了一层外壳 + 中间层:Electron 窗口负责界面并在主进程内直接起 openwork-server(不 spawn 子进程),服务器以托管方式拉起带随机凭据的 opencode serve 子进程,并用带权限作用域的反向代理在 OpenCode 之上补齐会话管理、权限审批、工作区挂载、技能 / MCP 装配。默认只绑 127.0.0.1 本地跑,想协作时再显式打开远程;组织侧由 Den 控制面把能力发布成一个 OpenWork MCP,任何兼容 agent 加一个 URL 就能按需取用。


1. 这是什么(零基础也能懂)

一句话定义。 OpenWork = OpenCode(agent 引擎)+ 一层安全、好用、可分享的产品外壳

它解决谁的什么问题。 现有的 opencode CLI/GUI 是给开发者用的:满屏 file diff、工具名、要靠命令行才能扩展。OpenWork 把这套能力产品化给普通人:

  • 假设你不是工程师,但想让 AI 帮你整理一个装满合同/表格/笔记的文件夹——OpenWork 让你在一个桌面 App 里选中那个文件夹、发一句话,就能安全地让 agent 动手,而且每一步危险操作都会弹出审批让你点"允许 / 拒绝"。
  • 假设你是团队管理员,想把"谁能用哪些技能、连哪些 MCP、用什么模型"集中管理——Den 控制面把这些发布成组织能力,成员在任意 agent(Codex、Claude Code、Cursor…)里加一个 OpenWork MCP 即可(README "Use OpenWork from any agent")。

它能做什么(功能)。 对应到代码:

功能白话
桌面工作区选一个本地文件夹,一键让 agent 在里面干活
会话(Sessions)创建 / 切换会话、发提示词,SSE 实时刷进展
权限(Permissions)把权限请求弹给人:允许一次 / 总是允许 / 拒绝
技能 / MCP / 插件在 UI 里点几下装能力(第 4 章)
Connect(实验)Google Workspace / Microsoft 365 能力接入
OpenWork MCP云端组织能力面:search_capabilities / execute_capability
OpenWork Den(EE)组织控制面:成员/团队、市场、托管推理、桌面策略

用起来什么样(最小示例)。 桌面 App 是主入口;不装桌面,也能直接跑服务器 CLI(apps/server/src/cli.ts):

# 起一个 openwork-server,并让它托管一个 OpenCode 引擎
OPENWORK_MANAGE_OPENCODE=1 openwork-server --workspace /path/to/workspace
# 日志: OpenWork server listening on http://127.0.0.1:8787
# Managed OpenCode listening on http://127.0.0.1:<随机口>

桌面 App 做的事本质一样,只是把服务器塞进 Electron 主进程内起,并把这套编排藏在了一键按钮后面。

一句话直觉 / 类比。 把 OpenWork 想成"给 OpenCode 引擎装的一台整车":OpenCode 是发动机(会推理、会调工具、会改文件),但发动机不能直接给人开。OpenWork 加了车身(Electron 界面)、电控(进程内 openwork-server + 托管引擎)、安全带(权限审批、默认只绑本地),让不懂机械的人也能安全上路。

本节不谈底层代码。记住一件事:OpenWork 自己不实现 agent 循环,它包装并治理 OpenCode。


2. 顶层全景(它大概怎么转)

2.1 一张图看懂"谁拉起谁、谁挡在谁前面"

怎么读这张图: 从上到下是"外壳 → 服务 → 引擎"的层次;实线是进程拉起 / 模块加载,openwork-server 是所有请求进 OpenCode 的唯一闸门

┌─────────────────────────────────────────────┐
│ Electron 桌面外壳 apps/desktop │
│ · 主进程开窗 + IPC 桥 │
│ · 内嵌单一 React UI (apps/app) │
└───────────────┬─────────────────────────────┘
│ engineStart → import() 进程内加载

┌─────────────────────────────────────────────┐
│ openwork-server (apps/server) │
│ · 作用域鉴权反向代理 + 审批 + 文件系统 API │
│ · 托管引擎:spawn opencode serve │
│ (随机凭据 + stdout 就绪 + 注册表/蓝绿池) │
└───────────────┬─────────────────────────────┘
│ spawn(受管子进程)

┌─────────────────────────────────────────────┐
│ opencode (agent 引擎, 外部二进制) │
└─────────────────────────────────────────────┘

云侧: Den 控制面(ee/apps/den-*) ──发布──▶ OpenWork MCP(/mcp/agent)
任何 MCP 客户端 ◀──search_capabilities / execute_capability──

第二张图放大请求如何穿过闸门——因为这是整个项目工程含量最高的一环:

UI / 远程客户端
│ 带 token: Bearer <client> 或 X-OpenWork-Host-Token

openwork-server
│ 1. 认出 Actor + 作用域 (owner / collaborator / viewer)
│ 2. 匹配路由 (registry) 或识别 /w/:id/opencode/* 挂载
│ 3. assertOpencodeProxyAllowed: viewer 只能 GET/HEAD、
│ 不能自批权限;越权 → 403

proxyOpencodeRequest ──▶ opencode serve (真正干活的 agent)

2.2 部件一句话职责

以下是主要部件及其所在文件(基于 pnpm workspace apps/* + packages/* + ee/apps/*):

部件干什么在哪
Electron 外壳开窗、菜单、IPC 桥、引导运行时、自动更新apps/desktop/electron/main.mjs
运行时管理器串行化启停、token/端口持久化、进程内起服务器apps/desktop/electron/runtime.mjs(createRuntimeManager)
openwork-serverOpenCode 之上的作用域鉴权反向代理 + 审批 + 工作区注册表 + 文件系统 API + 技能/MCP/插件装配 + 引擎托管apps/server/src/server.ts
托管引擎机制spawn opencode serve、随机凭据、stdout 就绪、注册表、蓝绿池apps/server/src/managed-opencode.tsengine-registry.tsengine-pool.ts
opencode(外部)真正的 agent 引擎(推理、工具调用、改文件)外部二进制,非本仓库
React UI单一前端,桌面与 Web 共用;SSE 实时渲染会话apps/app/src/index.react.tsx
OpenWork Cloud(den, EE)控制面、组织能力市场、OpenWork MCP、托管推理ee/apps/den-*

历史注记: 旧架构里有一个独立的 openwork-orchestrator CLI(监管 opencode / openwork-server / opencode-router 三个 sidecar)和 Slack/Telegram 消息桥 opencode-router——两者均已被上游移除;本文所有章节按当前源码记述,退场痕迹见第 2 章 §4 与第 6 章 §7。

2.3 主线走一遍(不进代码)

  1. 你在桌面 App 里选一个项目文件夹;主进程调用 runtimeManager.engineStart(workspaceRoot, …)(main.mjs:1339bootRuntimeForSelectedWorkspaceruntime.mjs:2066engineStart)。
  2. engineStart 把一个打包好的 embedded 服务器模块 import() 进 Electron 主进程,起 HTTP 服务(runtime.mjs:1849startOpenworkServer),并带着 manageOpencode: true——由服务器去 spawn 并托管 OpenCode 引擎(第 2 章);它会为这个工作区签发/复用作用域 token
  3. UI(内嵌在 Electron 窗口里)通过 @opencode-ai/sdk v2 客户端连到 openwork-server,列会话、发提示词、订阅 SSE 事件流。
  4. agent 每要做一件敏感事(改文件、跑命令),openwork-server审批服务把请求挂起、弹给你点允许/拒绝(apps/server/src/approvals.ts)。
  5. 想让同事也能用?打开远程访问,openwork-server127.0.0.1 改绑 0.0.0.0 并打印 LAN / mDNS 连接 URL(runtime.mjs:1880 的绑定选择、buildConnectUrls);或从云端 Den 深链接一键连接远程 worker(第 6 章)。

3. 阅读地图(建议顺序)

OpenWork 是个大型 monorepo,单文件讲不完。按"外→内、本地→云"的顺序分成 6 章,建议顺序阅读:

  1. 桌面外壳与启动流程 — Electron 主进程怎么开窗、通过 contextBridge 暴露 invokeDesktop IPC 桥(apps/desktop/electron/preload.mjs:62)、以及"选文件夹 → 引导运行时"的完整启动链。先读这章建立入口直觉。
  2. 主机运行时:openwork-server 如何托管引擎 — 托管 spawn(createManagedOpencodeServer)、随机凭据与 stdout 就绪信号、引擎注册表与孤儿回收、蓝绿池(EnginePool)、worker 心跳,以及旧 orchestrator 的退场考古。
  3. openwork-server:OpenCode 之上的鉴权代理与文件系统 API本项目的核心。作用域鉴权(owner / collaborator / viewer)、代理闸门(assertOpencodeProxyAllowed)、审批服务、工作区注册表、文件会话与批量读写。
  4. 可扩展性:技能、MCP、插件与内建扩展.opencode/skills 技能扫描、runtime 配置里的 MCP 与 opencode 插件装配、云插件市场与 Claude 插件兼容、内建扩展(affordance 元工具)。
  5. 前端:单一 React UI 与实时会话渲染 — 一套 apps/app 如何同时服务桌面(HashRouter,跑在 Electron 窗口里)与 Web(BrowserRouter)、通过 SSE 实时刷会话、kernel/domains 分层。
  6. 远程与云:远程工作区、OpenWork MCP 与 Den — Connect remote(远程工作区)、签名的 connect-link、Den 的 agent MCP(search_capabilities/execute_capability)、桌面↔云资源同步、托管推理与 worker 心跳。

4. 巧妙之处(值得带走的设计)

这一节挑几处不显然但很聪明的决策,先白话点出妙在哪,再给源码锚点。

4.1 「包装而非重写」:OpenCode 当引擎,自己只做治理层

OpenWork 刻意不实现 agent 循环,直接把 OpenCode 当外部引擎拉起,自己只在外面做鉴权、审批、UI、分享。好处是"OpenCode 能做的,OpenWork 都能做,哪怕还没做 UI"。代价是要精确管理一个它不拥有的子进程——这也是 runtime.mjs 里生命周期串行队列(withRuntimeLifecycle,runtime.mjs:1361)与 sidecar 清理(cleanupPackagedSidecars,runtime.mjs:1769)存在的原因。

4.2 三级 token 作用域 + 「唯一闸门」代理

所有进 OpenCode 的请求都必须穿过 openwork-server,它把调用者归为三档作用域(TokenScope = owner | collaborator | viewer,apps/server/src/types.ts:9):

  • owner — 主机令牌(X-OpenWork-Host-Token)持有者,能签发 token、答复审批;
  • collaborator — 客户端令牌(Bearer,即 OPENWORK_TOKEN),SPA 唯一的凭据;
  • viewer — 只读。

闸门函数 assertOpencodeProxyAllowed(apps/server/src/server.ts:932)有一处踩过坑后修正的细节:viewer 只能发 GET/HEAD,且不能自批 OpenCode 的权限请求(拦截 /permission/:id/reply)。而 collaborator 必须放行——因为 SPA 只有 collaborator 令牌,曾经"只允许 owner 答复"导致所有交互式权限弹窗都无法点(403,工具调用卡死)。这行注释是活的设计史。

4.3 默认本地、显式远程:安全的"逐步放开"

openwork-server 默认绑 127.0.0.1,只有在打开远程访问时才改绑 0.0.0.0(runtime.mjs:1880),此时才计算并打印 LAN / mDNS 连接 URL(buildConnectUrls,runtime.mjs:504)。托管引擎的凭据也是同思路:每次启动现造随机用户名/密码、经环境变量注入、从不下发到 UI(managed-opencode.tsrandomSecret)。这是"本地优先、可远程共享"哲学落到实处的安全默认。

4.4 用户环境变量的"防污染"分层

桌面加载用户 env.json 注入子进程时,保留前缀 OPENWORK_ / OPENCODE_ 被强制剥离,这样一个被篡改的用户配置文件永远无法遮蔽/伪造框架自己的环境变量(runtime.mjs:689USER_ENV_RESERVED_PREFIXES + loadUserEnvFile:647)。托管引擎侧还有一道对偶防线:引擎环境删掉 OPENWORK_ENCRYPTION_KEY(managed-opencode.ts:162-164)——引擎需要 provider 环境,但不能拿到能解密 OpenWork OAuth 凭据的钥匙。

4.5 一套 UI,两种宿主

apps/app单一 React 代码库,靠一个开关同时服务桌面和 Web:桌面用 HashRouter(跑在 Electron 里,没有真实 URL),Web 用 BrowserRouter(apps/app/src/index.react.tsx:40,isDesktopRuntime() 判定)。桌面通过 preload 暴露的 __OPENWORK_ELECTRON__.invokeDesktop 走 IPC 调主进程命令,Web 则纯走 HTTP——UI 逻辑不重复。


5. 代码地图(导航索引)

给要读源码的人 / agent 的跳转表。优先用符号名 grep 定位(比行号抗上游漂移)。所有引用 as-of sourceCommit a9c304f

主题文件路径关键符号
Electron 入口、开窗、IPC 分发apps/desktop/electron/main.mjshandleDesktopInvokebootRuntimeForSelectedWorkspaceipcMain.handle("openwork:desktop")
渲染进程 ↔ 主进程桥apps/desktop/electron/preload.mjscontextBridge.exposeInMainWorld("__OPENWORK_ELECTRON__")invokeDesktop
运行时编排:串行启停 + 进程内服务器apps/desktop/electron/runtime.mjscreateRuntimeManagerwithRuntimeLifecycleengineStartstartOpenworkServercleanupPackagedSidecars
用户 env 防污染分层apps/desktop/electron/runtime.mjsloadUserEnvFileUSER_ENV_RESERVED_PREFIXESbuildChildEnv
托管引擎(spawn/凭据/就绪)apps/server/src/managed-opencode.tscreateManagedOpencodeServercreateManagedProcessCloserandomSecret
引擎注册表 / 蓝绿池apps/server/src/engine-registry.tsengine-pool.tsregisterEngineInstancereapOrphanEngineInstancesEnginePool
服务器 CLI / 内嵌入口apps/server/src/cli.tsembedded.ts顶层引导脚本、startEmbeddedServer
鉴权反向代理闸门apps/server/src/server.tsassertOpencodeProxyAllowedproxyOpencodeRequestrequireClientrequireHostToken
Token 作用域apps/server/src/tokens.tstypes.tsTokenServiceTokenScopenormalizeScope
权限审批服务apps/server/src/approvals.tsApprovalServicerequestApprovalrespond
路由表 / 鉴权模式apps/server/src/routes/registry.tsaddRoutematchRouteAuthMode(none/client/host/host-token)
文件系统 API(会话/批量读写)apps/server/src/routes/files.ts/files/sessions/:sessionId/read-batchwrite-batch/workspace/:id/files/content
会话 / 会话组apps/server/src/routes/sessions.ts/workspace/:id/sessionssession-groups
工作区注册表apps/server/src/routes/workspaces.ts/workspaces/local/workspaces/remote/workspaces/:id/activate
技能扫描 / 增删apps/server/src/skills.tslistSkillsupsertSkilldeleteSkill(SKILL.md 布局)
MCP / opencode 插件装配apps/server/src/mcp.tsplugins.tsaddMcp/removeMcpaddPlugin/listPlugins(写 runtime 配置)
云插件安装/卸载apps/server/src/cloud-plugins.tsinstallCloudPluginremoveCloudPlugingetPluginObjectInstallPath
桌面↔云资源同步apps/server/src/desktop-cloud-sync.tscloud-provider-sync.tssyncDesktopCloudResourcesResourceSnapshotparseCloudProviderDenSession
内建扩展 / 元工具apps/server/src/extensions/opencode-plugins/google-workspace.tsopenai-image-generation.tscloud-uploads.tsopenwork_context/query/execute
前端入口 / 双宿主路由apps/app/src/index.react.tsxisDesktopRuntimeHashRouter/BrowserRoutercreateDefaultPlatform
前端内核(状态 / SDK / 同步)apps/app/src/react-app/kernel/global-sdk-provider.tsxglobal-sync-provider.tsxplatform.tsx
远程工作区 / 深链接apps/desktop/electron/remote-workspace.mjsconnect-link.mjsselectOpenworkWorkspaceForConnectionverifyConnectLinkToken
OpenWork Cloud(EE)ee/apps/den-api(mcp/agent.ts)、den-webden-worker-runtimeinference

一处诚实说明: 仓库 README.md 早期版本的 "Architecture" 段落描述的是 Tauri 外壳,当前代码的桌面外壳是 Electron——apps/desktop/package.jsonmain 指向 electron/main.mjs,打包走 electron-builder;本文以源码为准记为 Electron。此外,apps/orchestrator(独立监工 CLI)与 apps/opencode-router(Slack/Telegram 桥)已在旧锁之后被整体移除,相关机制分别由"服务器托管引擎"(第 2 章)与"OpenWork MCP + 远程工作区"(第 6 章)接替。