深度拆解 OpenCoWork:一个本地多智能体桌面平台的架构设计与实现

OpenCoWork 是一个开源桌面多智能体 AI 协作平台。它的定位不是再做一个聊天窗口,而是把大模型、文件系统、Shell、SSH、MCP、定时任务、办公 IM 插件和多 Agent 编排放进一个本地桌面运行时里,让 Agent 真正进入开发者的工作环境。

这篇文章不做功能清单罗列,重点拆它的技术实现:Electron 如何分层、Agent runtime 如何落地、工具系统如何注册与执行、Plan Mode 如何约束写文件、Cron Agent 如何后台运行、MCP 和办公消息插件又是怎样接入的。

项目地址:https://github.com/AIDotNet/OpenCoWork
本文基于当前代码结构分析,核心技术栈包括 Electron 36、React 19、TypeScript、Zustand、better-sqlite3、node-cron、MCP SDK、xterm.js、Monaco Editor 等。


1. OpenCoWork 解决的不是“聊天”,而是“本地执行”

传统 LLM 产品最明显的问题是环境割裂:代码在 IDE,日志在终端,需求在聊天软件,文件在本地目录,而 Agent 只能在浏览器里给建议。OpenCoWork 的架构目标很直接:让 Agent 可以在用户授权下访问本地上下文,并执行真实动作。

它可以读取代码、搜索文件、修改文件、跑命令、开 SSH、处理文档、调用 MCP 工具、给飞书/钉钉/微信/Telegram 等渠道发送结果。所以它的核心不是“模型对话 UI”,而是一个本地 Agent 操作系统雏形:

用户意图 -> 会话模式 -> Agent Runtime -> 工具系统 -> 本地/远程/插件能力 -> 结果回流 UI 或消息渠道

这也是它选择 Electron 的原因:既要有桌面 UI,又要拿到 Node.js 层的系统能力。


2. 四层 Electron 架构:把高权限能力关进主进程

OpenCoWork 采用典型但并不简单的四层 Electron 架构:

Renderer React UI
        ↓ ipcRenderer.invoke
Preload contextBridge
        ↓ ipcMain.handle
Main Process IPC / DB / FS / Shell / SSH / Channels / Cron
        ↓
Main-process Agent Runtime / MCP / Provider Adapter

对应代码非常清晰:

  • src/main/index.ts:Electron 主进程入口,负责窗口生命周期、IPC handler 注册、渠道插件注册、MCP、Cron、SSH、数据库关闭等。
  • src/preload/index.ts:通过 contextBridge.exposeInMainWorld 暴露极少量 API。
  • src/renderer/src/App.tsx:React 入口,初始化 provider、viewer、工具系统、插件监听、Agent stream 等。
  • src/main/ipc/js-agent-runtime.ts 与 src/main/cron/cron-agent-background.ts:主进程侧 Agent runtime 与后台 Agent loop 的核心实现。

2.1 Main Process:所有危险能力都在这里

src/main/index.ts 里可以看到大量能力注册:

registerFsHandlers()
registerShellHandlers()
registerSettingsHandlers()
registerSkillsHandlers()
registerSshHandlers()
registerChannelHandlers(channelManager)
registerMcpHandlers(mcpManager)
registerCronHandlers()
registerBrowserHandlers()
registerGitHandlers()
registerTeamRuntimeHandlers()
registerTeamWorkerHandlers()

这意味着文件系统、Shell、SSH、数据库、MCP、Cron、消息插件等高权限能力都集中在主进程,通过 IPC 对渲染进程开放。

这样做的好处是边界明确:Renderer 不直接拿 Node 权限,用户界面和系统能力之间隔着 preload + IPC。风险也明显:main/index.ts 容易膨胀成“超级入口”。OpenCoWork 当前已经聚合了很多模块,后续如果继续增长,最好进一步拆成 domain service,例如 runtime-servicechannel-servicemcp-servicejob-service

2.2 Preload:窄桥接,不把 Node 能力裸露出去

src/preload/index.ts 的核心就是:

contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)

它暴露的自定义 API 主要包括图片下载/读取/剪贴板、team runtime 创建/删除/快照/消息追加、isolated team worker 的启动/停止。

这种做法符合 Electron 安全设计:Renderer 不应直接访问 fschild_process,而是通过受控 IPC 进入主进程。


3. 渲染器不是简单 UI,它负责工具目录和会话编排

OpenCoWork 的 Renderer 层不只是页面。src/renderer/src/App.tsx 在启动阶段会做几件关键事:

registerAllProviders()
registerAllViewers()
initProviderStore()
initAppPluginStore()
attachRendererToolBridge()
attachRendererProviderBridge()
agentStream.attach()
registerAllTools()

也就是说,前端层承担了模型 Provider 注册、预览器注册、工具系统注册、Agent stream 事件接收、运行时同步事件处理,以及 SubAgent / Team / Task / Plan 状态映射。

App.tsx 中对 runtime sync event 的处理也很关键,比如 task_add / task_update 同步任务面板,team_event / team_snapshot 同步团队运行态,subagent_event 同步子 Agent 状态,resolve_approval 处理工具审批结果。

这说明 OpenCoWork 的前端不是“薄 UI”,而是 Agent 工作台的状态中枢。


4. 工具系统:从统一注册到运行时上下文

工具系统入口在:

src/renderer/src/lib/tools/index.ts

registerAllTools() 的注册顺序很有代表性:

registerTaskTools()
registerFsTools()
registerSearchTools()
registerBashTools()
registerWidgetTools()
registerAskUserTools()
registerPlanTools()
registerCronTools()
registerNotifyTool()
registerGoalTools()
registerMemoryTools()
await refreshDynamicToolCatalog()
registerCodeCompatibleTools()
registerTeamTools()

这里可以看到 OpenCoWork 的工具分为几类:基础工具包括文件读写、搜索、Shell;协作工具包括任务、提问、计划、目标、记忆;自动化工具包括 Cron、Notify;动态工具包括 Skill、SubAgent、Wiki、WebSearch;兼容层工具面向 code-agent 风格 alias;Team 工具负责多 Agent 团队编排。

工具的统一接口在 src/renderer/src/lib/tools/tool-types.ts

export interface ToolContext {
  sessionId?: string
  workingFolder?: string
  sshConnectionId?: string
  signal: AbortSignal
  ipc: IPCClient
  readFileHistory?: Map<string, FileReadSnapshot>
  inlineToolHandlers?: Record<string, ToolHandler>
  agentRunId?: string
  pluginId?: string
  pluginChatId?: string
  sharedState?: { deliveryUsed?: boolean; bashCwd?: string }
}

export interface ToolHandler {
  definition: ToolDefinition
  execute: (input, ctx) => Promise<ToolResultContent>
  requiresApproval?: (input, ctx) => boolean
}

这个设计抓住了 Agent 工具系统的关键:工具不是孤立函数,而是带上下文执行。上下文里包含当前会话、工作目录、SSH 连接、IPC 客户端、本轮已读文件快照、当前 Agent run id、插件消息来源和可变共享状态。

所以同一个 ReadBashGrep 工具,在本地会话、SSH 会话、Cron 后台任务、微信自动回复上下文里,可以有不同执行语义。


5. Agent Runtime:主进程里的统一执行循环

src/main/ipc/js-agent-runtime.ts 定义了 JsAgentRuntimeManager。它对外暴露类似 RPC 的方法:

case 'initialize'
case 'ping'
case 'shutdown'
case 'capabilities/check'
case 'agent/run'
case 'agent/append-messages'
case 'agent/cancel'

其中 agent/run 最终进入 startRun(),构造 runIdAbortControllerRuntimeMessageQueueToolContext、renderer fallback tool executor 和 renderer approval probe。

关键点在这里:主进程 Runtime 可以自己执行后台能力,但遇到需要渲染器参与的工具、审批或 UI 状态时,会通过桥接回到 Renderer。

这是一种混合架构:

Main Runtime 负责 Agent loop 和系统执行
Renderer 负责工具目录、审批、UI 状态和部分工具 fallback

优点是灵活,兼容现有前端工具生态;缺点是边界更复杂,需要严格治理同名工具、审批逻辑和上下文同步。


6. Plan Mode:不是提示词约束,而是工具层硬约束

很多 Agent 产品所谓“计划模式”,本质只是提示词告诉模型“先别改代码”。这不可靠。

OpenCoWork 的 Plan Mode 做得更硬:它在工具层限制写入能力。核心文件是:

src/renderer/src/lib/tools/plan-tool.ts

关键逻辑是 createGuardedPlanFileHandler()

const currentPlanFilePath = getCurrentPlanFilePath(ctx)
const resolvedPath = resolveToolPath(input.file_path, ctx.workingFolder)

if (normalizeComparablePath(resolvedPath) !== normalizeComparablePath(currentPlanFilePath)) {
  return encodeToolError(
    `In plan mode, ${toolName} is restricted to the current plan file`
  )
}

也就是说,在 Plan Mode 下,Write 和 Edit 被替换成 guarded handler,只允许改当前 .plan/<planId>.md 文件。

流程如下:

EnterPlanMode
  -> 创建或恢复 plan
  -> 在工作目录生成 .plan/<planId>.md
  -> 开启 UI plan mode
  -> 注入 inline tool handlers

Write/Edit
  -> 检查目标路径是否等于当前 plan file
  -> 不等则拒绝

ExitPlanMode
  -> 读取 plan file
  -> 提取标题
  -> 状态改为 awaiting_review
  -> 要求等待用户审核

这比单纯 prompt 约束可靠得多。因为限制落在工具执行层,而不是模型自觉层。


7. Cron Agent:定时任务不是提醒,而是后台 Agent 执行器

OpenCoWork 的 Cron 能力不只是“到点发通知”。它真正调度的是 Agent。

核心文件:

src/main/cron/cron-scheduler.ts
src/main/cron/cron-agent-background.ts
src/main/ipc/cron-handlers.ts

cron-scheduler.ts 中定义了三种 schedule:

schedule_kind: 'at' | 'every' | 'cron'

对应一次性定时、固定间隔和标准 cron 表达式。它还实现了并发保护:

let maxConcurrentRuns = 2
const activeRunJobIds = new Set<string>()
Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐