当前位置: 首页 > news >正文

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

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

深度拆解 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.tssrc/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?: stringworkingFolder?: stringsshConnectionId?: stringsignal: AbortSignalipc: IPCClientreadFileHistory?: Map<string, FileReadSnapshot>inlineToolHandlers?: Record<string, ToolHandler>agentRunId?: stringpluginId?: stringpluginChatId?: stringsharedState?: { deliveryUsed?: boolean; bashCwd?: string }
}export interface ToolHandler {definition: ToolDefinitionexecute: (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 下,WriteEdit 被替换成 guarded handler,只允许改当前 .plan/<planId>.md 文件。

流程如下:

EnterPlanMode-> 创建或恢复 plan-> 在工作目录生成 .plan/<planId>.md-> 开启 UI plan mode-> 注入 inline tool handlersWrite/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>()if (activeRunJobIds.has(jobId)) return false
if (activeRunJobIds.size >= maxConcurrentRuns) return false

这点很重要。后台 Agent 不是普通函数,可能跑 Shell、读文件、请求模型、发消息。如果没有并发控制,很容易把本地机器或模型额度打爆。

当任务触发时,核心链路是:

node-cron / setTimeout / setInterval-> onJobFired(job)-> 更新 last_fired_at 和 fire_count-> sendToRenderer('cron:fired') 更新 UI-> runCronAgentInBackground(...)-> markFinished(job.id)

cron-agent-background.ts 里还能看到后台 Agent 的工具白名单:

Read, Write, Edit, LS, Glob, Grep, Bash,
Notify, PluginSendMessage, PluginReplyMessage, SubmitReport

这说明 Cron Agent 被设计成真正的自动执行单元:它可以巡检日志、跑构建、生成报告,再通过桌面通知或 IM 插件交付。


8. Team Runtime:用文件系统实现轻量多 Agent 协作状态

多 Agent 团队运行时核心在:

src/main/ipc/team-runtime-handlers.ts
src/main/ipc/team-worker-handlers.ts

team-runtime-handlers.ts 把团队状态放在:

~/.open-cowork/teams/<team>/team.json
~/.open-cowork/teams/<team>/messages.json

team.json 保存 team name、leadAgentId、leadSessionId、backend 类型、permissionMode、teamAllowedPaths、members 和 tasks。messages.json 保存团队消息。

它还实现了简单文件锁:

const LOCK_RETRY_DELAYS_MS = [25, 50, 100, 200, 400]
fs.promises.open(lockPath, 'wx')

这个方案非常工程化:不引入复杂基础设施,用 JSON + lock 就能支撑桌面单机多 Agent 协作。

代价也明确:它不适合高并发,不适合复杂查询,锁文件异常残留需要兜底,多机器共享目录场景风险较高。但对一个本地桌面应用来说,它的取舍是合理的:简单、可调试、易迁移。


9. MCP 接入:多传输协议 + 能力缓存 + 自动降级

OpenCoWork 的 MCP 客户端在:

src/main/mcp/mcp-client.ts
src/main/mcp/mcp-manager.ts
src/main/ipc/mcp-handlers.ts

McpClientWrapper 支持三类 transport:stdiossestreamable-http。实现里有一个很实用的可靠性设计:

if (this.config.transport === 'streamable-http' && this.config.autoFallback !== false) {await this.tryConnect('sse')this._usedFallback = true
}

也就是说,如果 Streamable HTTP 连接失败,可以自动回退到 SSE。

连接成功后,它会缓存三类能力:

_tools
_resources
_prompts

并通过分页拉取:

fetchAllTools()
fetchAllResources()
fetchAllPrompts()

这让 OpenCoWork 可以把外部 MCP Server 暴露的工具、资源、Prompt 纳入本地 Agent 工具链。换句话说,OpenCoWork 自己是一个桌面 Agent 容器,MCP 则是它连接外部工具生态的协议层。


10. 办公消息插件:把 Agent 输出送到真实协作场景

OpenCoWork 支持飞书、钉钉、Telegram、Discord、WhatsApp、企业微信、QQ、微信公众号等渠道。

src/main/index.ts 中可以看到插件注册:

channelManager.registerFactory('feishu-bot', createFeishuService)
channelManager.registerFactory('dingtalk-bot', createDingTalkService)
channelManager.registerFactory('telegram-bot', createTelegramService)
channelManager.registerFactory('discord-bot', createDiscordService)
channelManager.registerFactory('whatsapp-bot', createWhatsAppService)
channelManager.registerFactory('wecom-bot', createWeComService)
channelManager.registerFactory('qq-bot', createQQService)
channelManager.registerFactory('weixin-official', createWeixinService)

技术意义在于:Agent 的入口和出口不再局限于桌面窗口。典型链路可以是:

微信群/飞书群收到消息-> Channel Plugin 解析消息-> 自动回复会话构造 Agent 上下文-> Agent 调用本地工具或 MCP-> PluginSendMessage / ReplyMessage 返回结果

这让 OpenCoWork 更像“本地 Agent 网关”:它既连接本地环境,也连接团队沟通环境。


11. 持久化:SQLite 管业务数据,文件系统管用户可编辑资产

OpenCoWork 的本地数据目录是:

~/.open-cowork/

里面包括 SQLite 数据库 data.db、agents、commands、prompts、skills、teams、MCP 配置和运行时数据。

项目使用 better-sqlite3 做本地持久化,适合桌面应用:同步 API 简单、性能足够、部署成本低。另一个设计点是:用户可编辑资产很多不是硬编码在应用里,而是放到文件系统,比如 agents、prompts、skills。这样用户可直接编辑,便于备份迁移,便于动态加载,也不必每次扩展都改代码。

这也是 OpenCoWork 能支持 Markdown skill、动态 SubAgent catalog 的基础。


12. 我认为最值得借鉴的 5 个实现点

第一,Plan Mode 应该约束工具,而不是只约束提示词

OpenCoWork 通过 inline guarded tool handler 限制 Write/Edit 只能改计划文件,这是非常正确的方向。Agent 产品要做安全能力,不能只靠 system prompt。

第二,Cron Agent 要有并发保护和交付通道

后台 Agent 不是普通定时器。它需要并发限制、运行记录、失败状态、交付模式。OpenCoWork 的 maxConcurrentRunsfire_countdelivery_modeplugin_chat_id 都是工程上必须考虑的字段。

第三,MCP 客户端要做能力缓存和传输降级

MCP Server 的稳定性不可控。OpenCoWork 的 Streamable HTTP -> SSE fallback 是很实用的可靠性设计。

第四,多 Agent 状态不一定一上来就上数据库

Team Runtime 使用 JSON + lock 文件,虽然不是长期最强方案,但对本地桌面应用足够直接。早期追求可观察、可调试,比过早引入复杂分布式存储更重要。

第五,Renderer 和 Main 的边界要清楚,但可以保留桥接弹性

OpenCoWork 把危险能力放在 Main,把交互和审批放在 Renderer。中间通过 IPC 和 runtime sync 通信。这个方向是对的,但也需要持续治理工具执行边界,避免 Renderer tool 和 Main runtime tool 形成双轨复杂度。


13. 当前架构的风险与改进方向

OpenCoWork 已经具备完整平台形态,但从技术演进看,有几个风险需要提前压住。

13.1 主进程入口膨胀

src/main/index.ts 注册了大量模块。短期没问题,长期建议进一步抽 domain service,形成更清晰的启动编排层。

13.2 IPC 面过大

本地 Agent 产品天然需要很多 IPC,但 IPC surface 越大,安全审计成本越高。建议维护一份 IPC 权限清单,对高风险 channel 做参数校验、来源限制和审批策略。

13.3 工具系统存在双运行上下文

工具定义主要在 Renderer,Agent Runtime 又在 Main。当前通过 bridge/fallback 解决,但长期最好抽象统一 tool contract,明确哪些工具可在 Main 原生执行,哪些必须回 Renderer。

13.4 Team Runtime 的 JSON 持久化需要演进边界

JSON + lock 很适合本地单机,但当团队消息和任务增多时,需要索引、分页、压缩或迁移到 SQLite。

13.5 自动化能力越强,权限模型越关键

OpenCoWork 已经支持文件写入、Shell、SSH、消息发送、Cron 后台执行。越接近“本地自动执行平台”,越需要细粒度权限、审计日志和回滚机制。


结语

OpenCoWork 的价值不在于又做了一个 AI Chat,而在于它把 Agent 放进了真实工作环境:本地文件、终端、SSH、MCP、定时任务、消息渠道、多 Agent 编排,这些能力组合起来,才像一个真正可执行的 AI 协作平台。

从架构上看,它最核心的判断是:

UI 在 Renderer,系统能力在 Main,安全边界在 Preload/IPC,Agent Runtime 尽量靠近系统能力运行。

这个判断是成立的。如果后续继续演进,我会优先关注三件事:主进程服务拆分,降低入口复杂度;IPC 与工具权限模型系统化;Agent Runtime、Cron、Team、MCP 之间形成更统一的运行时契约。

做到这一步,OpenCoWork 就不只是一个桌面 Agent 工具,而会更接近一个可扩展的本地 AI Agent 操作平台。

http://www.jsqmd.com/news/891878/

相关文章:

  • Taotoken Token Plan套餐如何帮助团队更可控地管理AI成本
  • Python异常处理实战:从语法错误到生产级容错
  • 【光波仿真实践】基于MATLAB的厄米特-高斯光束模式可视化与光强分析
  • 模拟电路版图工具PK:Synopsys Custom Compiler、LAYGO2、Berkeley BAG2、ALIGN、MAGICAL(包括维护时间)
  • SDL2核心函数到底怎么用?从SDL_Init到SDL_Quit,一篇讲透初始化与资源管理的最佳实践
  • 知识图谱补全技术赋能工业FMEA:从文本到可推理知识网络的实践
  • 关联规则挖掘实战:从超市货架到电商推荐的商业逻辑
  • WinThumbsPreloader:重新定义Windows资源管理效率的智能革命
  • 淄博汽车贴膜哪家好?临淄车主都在找的贴膜老店:完美车饰-15 年贴膜老店 - 资讯快报
  • 终于搞懂 XSS 为什么能盗号了:Cookie、Session、HttpOnly 一次讲明白
  • 从重复劳动到智能助手:如何用Auto.js实现Android自动化革命
  • 5分钟上手U-Net:用深度学习轻松实现医学图像细胞膜分割
  • Java实战:手把手教你用Spring Boot集成海康综合安防平台API(附完整代码)
  • 购物篮分析实战:用Apriori挖掘高价值商品关联规则
  • 4.2 咖啡师不需要十年功底,兼职一周上手
  • 国内游戏动画培训排名前十机构推荐2026 - 资讯快报
  • 如何通过 Python 调用 Taotoken 的多模型 API 快速构建应用
  • CS2_External游戏内存操作框架深度解析与实战指南
  • House of Cat
  • 手把手教你用Vivado和ZYNQ7000玩转PS与PL通信:一个GPIO控制的完整实战
  • AI工具协同失效诊断手册:用3个指标(响应熵值、上下文衰减率、意图偏移度)秒判工作流亚健康
  • 蓝桥杯单片机选手必看:STC15F2K60S2上DS18B20驱动移植与调试避坑指南
  • SQL 转 ER 图在线工具:一键自动生成实体关系ER图 + 系统整体ER图
  • 老旧设备系统兼容性完整指南:让过时硬件焕发新生
  • KityMinder脑图工具:5个超实用技巧让你工作效率翻倍
  • 多项式插值算法
  • 3分钟掌握BetterNCM安装器:一键解锁网易云音乐完整潜力
  • 面壁智能开源低比特大模型训练成果 BitCPM-CANN,推理阶段释放约 6 倍显存红利
  • 在ubuntu上配置taotoken作为python开发环境的默认大模型服务
  • 武汉圣擎航空:一站式机票酒店签证包车出行服务,高效省心出行优选 - 土星买买买