• 深度拆解 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?: 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 下,WriteEdit 被替换成 guarded handler,只允许改当前 .plan/.md 文件。

    流程如下:

    EnterPlanMode
      -> 创建或恢复 plan
      -> 在工作目录生成 .plan/.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>()
    
    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.json
    ~/.open-cowork/teams//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 操作平台。

  • 相关阅读:
    TensorFlow 介绍 及其简单应用 附实例
    QT 常用知识
    Tekla添加零件ContourPlate
    C# GC机制
    Python:每日一题之最少砝码
    【Java基础】Java8新特性
    厂家解读新标准GB21148-2020《足部防护 安全鞋》的变化有哪些
    python常见爬虫库以及案例
    Java中什么是多态?多态的优势和劣势是什么?
    【学习笔记】 - 基础数据结构 :Link-Cut Tree(进阶篇)
  • 原文地址:https://www.cnblogs.com/token-ai/p/20175241