跳到主要内容

OpenWork — 架构与原理

30 秒导读: OpenWork 是一款开源桌面 App(macOS / Windows / Linux),让非程序员也能在自己电脑上、对着自己的文件用 AI agent 干活——它是 Claude Cowork / Codex 的开源替代。底层的"agent 大脑"直接复用开源引擎 OpenCode;OpenWork 在它外面套了一层外壳 + 中间层:Electron 窗口负责界面,一个进程编排器(orchestrator)一键拉起并看管进程栈,中间的 openwork-server带权限作用域的反向代理在 OpenCode 之上补齐会话管理、权限审批、工作区挂载、技能 / MCP 装配。默认只绑 127.0.0.1 本地跑,想协作时再显式打开远程,一条链接就能把整套配置分享给同事,或接到 Slack / Telegram、扩展到云端 worker。


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

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

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

  • 假设你不是工程师,但想让 AI 帮你整理一个装满合同/表格/笔记的文件夹——OpenWork 让你在一个桌面 App 里选中那个文件夹、发一句话,就能安全地让 agent 动手,而且每一步危险操作都会弹出审批让你点"允许 / 拒绝"。
  • 假设你已经调好了一套 agent 工作流(装了哪些技能、连了哪些 MCP、什么权限),想让整个团队复用——OpenWork 让你把这套 setup 打包成一条链接分享出去。

它能做什么(功能)。 摘自 README.md 的 "What's Included",逐条对应到代码:

功能白话
Host mode / Client mode在本机跑 opencode,或用 URL 连到已有的 OpenCode 服务器
会话(Sessions)创建 / 切换会话、发提示词
实时流(Live streaming)订阅 SSE /event,把 agent 的进展实时刷到界面
执行计划(Execution plan)把 OpenCode 的 todo 渲染成时间线
权限(Permissions)把权限请求弹给人:允许一次 / 总是允许 / 拒绝
模板(Templates)存下常用工作流、一键重跑
技能管理(Skills)列出 / 导入 .opencode/skills 里的技能包

用起来什么样(最小示例)。 不装桌面 App,也能用 CLI 主机模式起同样的进程栈(apps/orchestrator/README.md):

npm install -g openwork-orchestrator
openwork start --workspace /path/to/workspace --approval auto

这条命令会下载并拉起 opencodeopenwork-serveropencode-router 三个 sidecar,在 TTY 里显示一个带服务健康度、端口、连接信息的仪表盘。桌面 App 做的事本质一样,只是把这套编排藏在了一键按钮后面。

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

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


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

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

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

┌─────────────────────────────────────────────┐
│ Electron 桌面外壳 apps/desktop │
│ · 主进程开窗 + IPC 桥 │
│ · 内嵌单一 React UI (apps/app) │
└───────────────┬─────────────────────────────┘
│ 一键"引导运行时"

┌─────────────────────────────────────────────┐
│ 运行时编排 (二选一) │
│ A. orchestrator 守护进程(CLI 主机的默认) │
│ B. 桌面内嵌 openwork-server(in-process) │
└───────────────┬─────────────────────────────┘
│ 监管 / 内嵌
┌───────────────────┼───────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ openwork- │ │ opencode │ │ opencode-router │
│ server │─▶│ (agent 引擎, │ │ (可选:Slack / │
│ 鉴权反向代理 │ │ 外部二进制) │ │ Telegram 连接器)│
│ + 文件系统 API│ │ `opencode serve` │ └──────────────────┘
└───────────────┘ └──────────────────┘
▲ 所有 UI / 远程请求都经它进 OpenCode,按 token 作用域放行

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

UI / Slack / 远程客户端
│ 带 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
运行时管理器决定用哪种运行时、拉起 / 停止 / 重启进程栈apps/desktop/electron/runtime.mjs(createRuntimeManager)
orchestratorCLI 主机:下载并监管 opencode / openwork-server / opencode-router 三 sidecarapps/orchestrator/src/cli.ts
openwork-serverOpenCode 之上的作用域鉴权反向代理 + 审批 + 工作区注册表 + 文件系统 API + 技能/MCP/插件装配apps/server/src/server.ts
opencode(外部)真正的 agent 引擎(推理、工具调用、改文件),opencode serve 起的独立进程外部二进制,非本仓库
opencode-router把 Slack / Telegram 消息桥接到某个 opencode 服务器,并按目录路由apps/opencode-router/src/
React UI单一前端,桌面与 Web 共用;SSE 实时渲染会话apps/app/src/index.react.tsx
OpenWork Cloud(den)托管 worker、控制面、推理代理(企业/云)ee/apps/den-*

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

  1. 你在桌面 App 里选一个项目文件夹;主进程调用 runtimeManager.engineStart(workspaceRoot, …)(main.mjs:585bootRuntimeForSelectedWorkspaceruntime.mjs:1381engineStart)。
  2. 运行时管理器准备干净的进程栈,起一个 openwork-server 并让它托管一个 opencode serve 子进程(runtime.mjs:1069startOpenworkServer,manageOpencode: true);它会为这个工作区签发/复用作用域 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:248buildConnectUrls);或者启用 opencode-router 接 Slack / Telegram;或连到云端托管 worker。

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

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

  1. 桌面外壳与启动流程 — Electron 主进程怎么开窗、通过 contextBridge 暴露 invokeDesktop IPC 桥(apps/desktop/electron/preload.mjs:51)、以及"选文件夹 → 引导运行时"的完整启动链。先读这章建立入口直觉。
  2. 主机运行时:orchestrator 如何监管三个 sidecaropenwork CLI 如何按 SHA-256 manifest 下载 sidecar、拉起并监管 opencode / openwork-server / opencode-router(SidecarName,apps/orchestrator/src/cli.ts:172),以及沙箱(Docker / Apple container)、热重载。
  3. openwork-server:OpenCode 之上的鉴权代理与文件系统 API本项目的核心。作用域鉴权(owner / collaborator / viewer)、代理闸门(assertOpencodeProxyAllowed)、审批服务、工作区注册表、文件会话与批量读写。
  4. 可扩展性:技能、MCP、插件与内建扩展.opencode/skills 技能扫描与从 GitHub hub 安装、opencode.json 里的 MCP 与 opencode 插件装配、内建扩展(Google Workspace、OpenAI 图像)。
  5. 前端:单一 React UI 与实时会话渲染 — 一套 apps/app 如何同时服务桌面(HashRouter,跑在 Electron 窗口里)与 Web(BrowserRouter)、通过 SSE 实时刷会话、kernel/domains 分层。
  6. 远程与云:消息连接器与托管 workeropencode-router 的 Slack / Telegram 桥接与目录路由(path-scope.ts),以及 ee/apps/den-*(OpenWork Cloud:den-api / den-web / den-worker-runtime / inference)。

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

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

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

OpenWork 刻意不实现 agent 循环,直接把 OpenCode 当外部引擎(opencode serve)拉起,自己只在外面做鉴权、审批、UI、分享。好处是"OpenCode 能做的,OpenWork 都能做,哪怕还没做 UI"(README "Core Philosophy" 的 Ejectable)。代价是要精确管理一个它不拥有的子进程——这也是 runtime.mjs 里大量端口探测、健康检查、僵尸进程清理代码存在的原因(cleanupPackagedSidecars,runtime.mjs:978)。

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

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

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

闸门函数 assertOpencodeProxyAllowed(apps/server/src/server.ts:631)有一处踩过坑后修正的细节: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:1078),此时才计算并打印 LAN / mDNS 连接 URL。而且 orchestrator 默认从 stdout 隐藏实时凭据、只打印配对 URL,避免泄进 shell 历史或日志(apps/orchestrator/README.md "The command prints pairing URLs by default…")。这是"本地优先、可远程共享"哲学落到实处的安全默认。

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

桌面加载用户 env.json 注入子进程时,保留前缀 OPENWORK_ / OPENCODE_ 被强制剥离,这样一个被篡改的用户配置文件永远无法遮蔽/伪造框架自己的环境变量(runtime.mjs:461USER_ENV_RESERVED_PREFIXES + loadUserEnvFile)。同一策略在 openwork-server 与 orchestrator 三处必须字节级一致(代码里有互相指认的注释)。

4.5 一套 UI,两种宿主

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


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

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

主题文件路径关键符号
Electron 入口、开窗、IPC 分发apps/desktop/electron/main.mjshandleDesktopInvokebootRuntimeForSelectedWorkspaceipcMain.handle("openwork:desktop")
渲染进程 ↔ 主进程桥apps/desktop/electron/preload.mjscontextBridge.exposeInMainWorld("__OPENWORK_ELECTRON__")invokeDesktop
运行时编排:起 / 停 / 重启进程栈apps/desktop/electron/runtime.mjscreateRuntimeManagerengineStartstartOpenworkServerstartOrchestratorRuntimestartDirectRuntimecleanupPackagedSidecars
用户 env 防污染分层apps/desktop/electron/runtime.mjsloadUserEnvFileUSER_ENV_RESERVED_PREFIXESbuildChildEnv
CLI 主机 / sidecar 监管apps/orchestrator/src/cli.tsSidecarNameRuntimeServiceNamespawnProcess(daemon run / start / serve 命令)
鉴权反向代理闸门apps/server/src/server.tsassertOpencodeProxyAllowedproxyOpencodeRequestparseWorkspaceOpencodeMountrequireClientrequireHostToken
Token 作用域apps/server/src/tokens.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 布局)
从 GitHub hub 安装技能apps/server/src/skill-hub.tslistHubSkillsinstallHubSkill(默认 repo openwork-hub)
MCP / opencode 插件装配apps/server/src/mcp.tsplugins.tsaddMcp/removeMcpaddPlugin/listPlugins(写 opencode.json)
内建扩展 / opencode 插件apps/server/src/extensions/opencode-plugins/google-workspace.tsopenai-image-generation.tsopenwork-anthropic-adaptive-thinking.ts
前端入口 / 双宿主路由apps/app/src/index.react.tsxisDesktopRuntimeHashRouter/BrowserRoutercreateDefaultPlatform
前端内核(状态 / SDK / 同步)apps/app/src/react-app/kernel/global-sdk-provider.tsxglobal-sync-provider.tsxplatform.tsx
Slack / Telegram 连接器apps/opencode-router/src/slack.tstelegram.tsbridge.tspath-scope.ts
OpenWork Cloud(托管)ee/apps/den-apiden-webden-worker-runtimeinference

一处诚实说明: 仓库 README.md 的 "Architecture" 与 "Folder Picker" 段落仍描述 Tauri(Rust),但当前代码的桌面外壳已是 Electron——apps/desktop/package.jsonmain 指向 electron/main.mjs,打包走 electron-builder。本文以源码为准记为 Electron;README 该段属上游未同步的陈述。此外,桌面 engineStart 当前默认走内嵌 in-process openwork-server(自身托管 opencode)这条路径(runtime.mjs 内标为 direct 运行时),而 openwork-orchestrator 守护进程是独立 CLI 主机的默认监管者;两条路径共存,essence 描述的"orchestrator 监管三 sidecar"对应后者。细节见第 1、2 章。