Munder Difflin 架构解析:两个数据平面如何驱动一个多智能体办公室渲染器
【免费下载链接】munder-difflinA local multi-agent harness that works with your existing Claude Code, Codex subscriptions, allows you to run an office of agents项目地址: https://gitcode.com/GitHub_Trending/mu/munder-difflin
本文基于仓库根目录下的 docs/ARCHITECTURE.md(架构与设计系统指南,README 的延伸、贡献者的第一张地图)展开。Munder Difflin 是一个本地多智能体编排工具(multi-agent harness),复用你已有的 Claude Code、Codex 等订阅,把多个 AI 智能体当作一个"办公室"来运行。读完本文,你将理解其"两个数据平面喂一个渲染器"的核心架构、主进程 / 预加载 / 渲染进程三层代码结构,以及贯穿全产品的像素风设计系统,并能在实际阅读源码、提交 PR 或排查多智能体编排问题时直接按图索骥。
架构总览:两个数据平面,一个渲染器
Munder Difflin 的核心架构可以浓缩为一句话:两个数据平面(Event Plane 与 Terminal Plane)喂给同一个 Electron Renderer(React)。渲染器内部再分成两块视图:由 Pixi.js 驱动的Office Floor(办公室地板),以及由 xterm.js 驱动的Terminal + Command Bar(终端与命令栏,含 Files / Git 标签页)。
┌───────────────────────────────────────────────────────────────┐ │ Electron Renderer (React) │ │ ┌──────────────────┐ ┌──────────────────────────────┐ │ │ │ Office Floor │ │ Terminal + Command Bar │ │ │ │ (Pixi.js) │ │ Files + Git tabs (xterm.js) │ │ │ └─────────▲────────┘ └────────────▲─────────────────┘ │ │ │ avatar state │ pty bytes / fs / git │ └─────────────┼──────────────────────────┼───────────────────────┘ │ IPC (contextBridge: window.cth) ┌──────┴──────────┐ ┌──────┴─────────────┐ │ Event Plane │ │ Terminal Plane │ │ hooks / hive │ │ node-pty PTYs │ │ router + GOD │ │ + fs + git │ └────────▲────────┘ └──────▲─────────────┘ │ hook payloads │ stdin / stdout └─────────┬──────────────┘ ┌──────┴──────────────┐ │ claude / agy / codex│ └─────────────────────┘两张平面各司其职,渲染器只通过一条类型化的 IPC 桥(window.cth)与主进程通信:
- Terminal Plane(终端平面):主进程持有
PtyManager,把每个智能体 spawn 成一个node-pty进程,并通过按 id 隔离的 IPC 通道(pty:data:<id>)把输出流推给渲染器。渲染器不直接接触任何子进程,只能访问类型化的window.cth桥(见 src/preload/index.ts),该桥同时暴露沙箱化的文件系统与 git 辅助函数。 - Event Plane(事件平面 / Hive):
hive.ts是磁盘上的多智能体协作层;hooks.ts运行 hook 服务器,让各提供商桥接器把生命周期负载 POST 进来(Claude Code 走cth-hook,Antigravity 走agy-hook)。memory.ts封装语义记忆 CLI。路由器负责投递消息、排空各提供商的 outbox,GOD 智能体负责仲裁,空闲 / 收件箱唤醒机制保证 worker 持续处理邮件。
为什么把两条平面分开
从 src/main/index.ts 的主进程装配代码可以看到,这两条平面的生命周期与关注点完全不同:PTY 平面关心"进程级的 stdin/stdout/几何尺寸/退出信号",事件平面关心"智能体之间的消息、上下文与成本"。PtyManager与HiveManager、HookServer在主进程中并列实例化,互不依赖,却在teardownPty(src/main/index.ts)处汇合——无论智能体是被显式 kill、自然退出还是崩溃,都会归档其 hive 智能体、移除隔离 git worktree、清理断路器与遥测状态。这种"平面隔离、生命周期统一"的设计是理解整个主进程的钥匙。
Terminal Plane 深入:PtyManager 与 node-pty 会话管理
终端平面由 src/main/pty.ts 的PtyManager类实现。它维护一个Map<id, PtySession>会话表,每个会话记录proc(node-pty 的IPty)、cwd、command、输出流的归属窗口(owner)、最近输出时间lastOutputAt、以及一个有界环形缓冲tail(保留最近 8192 字节输出,用于崩溃诊断——provider 启动即崩溃时,屏幕上最后打印的说明不会丢失)。
核心操作:spawn / write / resize / kill
PtyManager对渲染器暴露的四个核心操作与语义如下:
| 操作 | 语义 | 关键实现细节 |
|---|---|---|
spawn(opts, owner) | 为智能体启动一个新的 PTY 会话 | 先expandTilde展开~,校验cwd存在;resolveCommand把裸命令名(如claude)解析为绝对路径;buildPtyEnv构造子进程环境;返回{ok, error}而非抛异常 |
write(id, data) | 向该 PTY 写入 stdin(即向智能体"打字") | 会话不存在时返回{ok:false, error:'no pty: <id>'} |
resize(id, cols, rows)/redraw(id) | 调整终端几何尺寸 / 请求重绘 | redraw用"同尺寸二次 resize"触发首帧输出,解决启动输出早于渲染器订阅的问题 |
kill(id) | 终止会话 | 调用proc.kill()后追加ensureKilled(pid)校验并清扫进程组,杜绝 PID 泄漏 |
spawn 过程中有一个值得注意的细节:PtyManager维护一个命令解析缓存(resolvedCommands)。每次which探测都会触发一次交互式 shell 启动($SHELL -ilc,通常要 source 整个 zshrc,耗时约 1 秒且发生在主进程上),所以成功命中的解析结果会被缓存;但"未找到"的结果刻意不缓存——缺失 CLI 的自动安装路径必须能在重装后立刻看到新二进制。
退出处理:自然退出与显式 kill 走同一条 teardown
PtyManager.setExitHandler(src/main/pty.ts)注册"自然退出"回调。在 src/main/index.ts 中,这个 handler 会:
- 先记录异常退出(
recordAgentExit,携带 exitCode、signal、tail、command); - 检查是否存在
pendingInstallRelaunch(缺失 CLI 的自动安装路径):若安装器以 0 退出,则在同一 PTY/窗口中自动重启并继续(携带noAutoInstall防止死循环); - 否则调用
teardownPty(id),与显式 kill 完全一致:撤销集成代理能力令牌、归档 hive 智能体、移除隔离 worktree、清理 watchdog / breaker / 遥测状态。
一个关键的安全细节:kill 后 node-pty 的异步 exit 回调会因会话身份守卫(this.sessions.get(opts.id) !== session)被丢弃,避免"旧进程的残留字节污染新会话画面"和"假退出杀死新会话"两类竞态。
Windows 上的特殊处理:cmd.exe 与新行陷阱
src/main/pty.ts 花了大量篇幅处理 Windows 平台。核心问题是:cmd.exe 没有反斜杠转义,且把换行当作语句分隔符,任何多行参数都会被截断。hive 协议提示词是一个约 6.1k 字符、11 行、62 个括号的参数——经过 cmd.exe 后从第一个换行处被截断,导致 Windows 上的智能体"看起来健康却从未收到 HIVE PROTOCOL 块,互相之间永远无法通信"。
解决方案分两层:
parseNpmCmdShim(src/main/pty.ts):纯函数解析 npm 生成的.cmdshim(现代 / 经典 / 远古三种形态),取出真正的解释器(node/bun/deno,严格的允许清单)+ JS 脚本路径,直接以argv 数组spawn,绕开 cmd.exe 的字符串解析。换行在 CRT 引号参数里只是普通字符,上限也从 cmd 的 8191 字符放宽到 CreateProcess 的 32767。buildCmdCommandLine(src/main/pty.ts)作为兜底:/d /s /c "<target> <args>"预转义整条命令行,只用于无法解码的 target。兜底路径会大声警告"多行参数将被截断",而不是像以前那样静默失败。
Event Plane 深入:Hive 磁盘协作层与 Hook 服务器
事件平面是 Munder Difflin 的灵魂——它让多个独立 CLI 智能体像同一个办公室的雇员一样协作,全部基于磁盘上的文件 + git 提交,智能体本身从不直接调用 git。
HiveManager:mailbox 模型与路由器
HIVE.md 定义了完整设计,实现位于 src/main/hive.ts。Hive 目录在<harnessHome>/hive/下,是一个只有主进程提交的单一 git 仓库,包含:
- 每个智能体的工作区:
identity.md、memory.md、inbox/、outbox/、cursor.json; - hive 身份注册表
registry.json(id/role/cwd/session,智能体读取它来认识彼此),与 UI 层的地板名单<harnessHome>/roster.json分离; - 共享黑板
board.md、任务账本,以及一个追加式事件日志log.jsonl; - 一个路由器:排空每个智能体的 outbox,投递到收件人的 inbox;
- 单一提交者 git,带重试 / 退避与陈旧锁恢复。
消息模型是HiveMessage:act取值为request | inform | propose | query | agree | refuse | done,携带hops(跳数上限HOP_CAP = 12)、requires_reply、needs_human、conversation等字段。向 "human" 投递的消息会被路由到GOD 智能体——人类在办公室地板上的代理(GOD 由 src/shared/godIdentity.ts 解析名称)。人的介入天然存在于每个 Claude Code 会话内:权限提示出现在智能体自己的终端里,可通过/remote-control远程批准,Hive 不维护独立的审批队列。
值得注意的两个实现细节:
redactSecrets(src/main/hive.ts):主进程侧的隐私闸门。消息体可能引用 API key、JWT、PEM 私钥或 bearer token,因此每条 subject/body 在跨 IPC 前都要过一遍保守的凭证形状正则清洗([redacted]替换),渲染器持有零清洗策略。测试 test/voice-messages.test.cjs 与这些正则逐字符锁步,保证凭证形状一定会被剥离。- 恶意输入防御:worker 的 scratch 目录(
HIVE_ROOT/agents/<workerId>)在删除前会做路径安全校验,解析后的路径必须精确落在agents/根下且 basename 等于 id,防止被构造的 id 逃逸出 agents 根目录。
HookServer:生命周期事件如何驱动渲染器
src/main/hooks.ts 的HookServer是事件平面的入口。每个 spawn 的智能体都以--settings指向一个钩子 shim(见 src/main/hive.ts 的HOOK_SHIM),把 hook payload 转发到主进程监听的Unix domain socket。服务器收到后:
- 用
PreToolUse/PostToolUse/Notification等事件驱动地板上角色的状态; - 报告生命周期边界,供渲染器侧的受保护队列在会话进入安全的空闲提示后才投递 inbox 工作;
- 解析
context_window字段,记录每个智能体当前上下文 token 数与真实窗口大小(200k 还是 1M,只有 statusLine shim 能暴露)——渲染器通过hive:contextUpdate实时收到,主进程侧也保留最后值供语音读取层查询。
HookServer 还与其他子系统联动:操作员控制注册表(ControlRegistry,pause/gate/steer/halt,通过 hook 返回值生效)、断路器(CircuitBreaker,在每次PostToolUse喂入recordToolUse)、常驻目标(standing goal,从磁盘 roster 读取并在会话开始时注入)以及 worker 唤醒看门狗(workerWake,用于避免向正停在权限提示上的智能体打字)。
同平面的其他主进程服务
从 src/main/index.ts 的装配代码可以看到,事件平面还挂着多个服务:
- memory.ts:语义记忆层(CLI 封装,
degrade-to-noop降级),受config.semanticMemory !== false与embeddingModel(默认minilm)控制; - reflect.ts:
MemoryReflector记忆冷凝,把每个智能体的memory.md限制在界内(Haiku 尾部摘要 → 备份 → 校验 → 原子替换)。默认每 1800 秒跑一次,byteTriggerPct50、sectionTrigger50、minBytes16384; - breaker.ts / control.ts:成本 / 失控断路器(steer/constrain/stop)与 HITL 闸门;
telemetry.onApiError会把 OTel 的 api_error span 喂给断路器触发; - telemetry.ts / transcript.ts:实时 OTel 采集器 + 从
~/.claude/projects/的 JSONL 转录读取真实 token/成本遥测(transcript.ts),二者组成UsageProviderseam,供断路器和成本账本消费; - db.ts:SQLite 持久层(窗口边界、命令历史、持久成本账本);
- github.ts:通过
ghCLI 摄取 GitHub issue 与 CI 运行; - fs.ts / git.ts:沙箱化文件系统与 git 桥;shellEnv.ts:为子进程解析 PATH 与 shell 环境。
渲染器与 IPC:window.cth 类型化桥
渲染器与主进程之间唯一的通道是预加载脚本暴露的window.cth(src/preload/index.ts)。它基于 ElectroncontextBridge+ipcRenderer构建,导出一整套类型化的接口与类型定义,包括:
HiveAgentMeta/HiveRegistry/AgentDirectory:hive 智能体元数据与目录(id、name、provider、role、capabilities、cwd、isGod、isAssistant、sessionId、archived 等);HiveMessage/VoiceMessage:hive 消息与其"语音读取层"的脱敏形态——注意VoiceMessage的 subject/body 已在主进程 REDACTED,渲染器永远不会收到原始 body 或密钥;HiveTask:任务看板卡片(status: todo/doing/blocked/done、dependsOn依赖数组、humanQA人机问答历史、Slack 线程来源、webhook 令牌哈希);RosterSnapshot、IntegrationRecordView(secretRef 被红染为hasSecret布尔,践行"密钥值永不通过 IPC 返回"的写保护契约)等。
渲染器侧的布局与接线在 src/renderer/src/App.tsx,其组件库覆盖了完整的操作界面:
CommandCenterPanel:Michael 的控制台(Terminal / Floor / Memory / Activity / Tasks / Triggers / Handbook 标签页);ToolWaterfall:按智能体展示工具调用的瀑布流(可观测性视图);TasksKanban:感知依赖的看板(Tasks 标签页);ThreadsPanel:hive 消息会话查看器(Messages 标签页);MessageQueueComposer:为繁忙智能体暂存消息;ApprovalsPanel、AgentDetailPanel、PixelPanel等基础面板组件。
Office Floor:Pixi.js 场景层
办公室地板位于 src/renderer/src/scene/office/,由OfficeFloor.tsx(src/renderer/src/scene/office/OfficeFloor.tsx)驱动,配合Character、Camera、cast(角色造型配方)、pathfinding(A* 寻路)、TiledMapRenderer、MessageEnvelope(信件投递动画)、ThoughtBubble/ToolBubble(气泡)与glRecovery(WebGL 上下文丢失恢复)。地板上还实现了一套"咖啡经济"与"咖啡闲聊"小系统:空闲智能体会去倒咖啡、在茶水间偶遇闲聊(cafeteriaLines.ts提供台词),老板(Michael)走过时还会表演式地"谄媚"(SUCK_UP_KEYS),背后则有真实的done-task计数插值——信息通过运动传达,一个角色走过去本身就是状态。
项目结构:贡献者的地图
docs/ARCHITECTURE.md给出了完整的目录树,以下按"主进程 → 预加载 → 渲染器 → 文档与配套"四层组织:
src/ main/ Electron 主进程(Node) index.ts window、IPC handlers、退出守卫、全部服务装配 pty.ts node-pty 管理器(spawn/write/resize/kill/stream) hive.ts 磁盘多智能体层(memory、mailboxes、router) hooks.ts hook 服务器 + provider hook shim(cth-hook、agy-hook) memory.ts 语义记忆层(CLI 封装,degrade-to-noop 降级) config.ts harness 配置持久化 + home 目录设置 transcript.ts 读取 ~/.claude/projects/ JSONL 转录,用于真实 token/成本遥测 telemetry.ts 实时 OTel 采集器 + usage/cost 数据源 usage.ts / pricing.ts UsageProvider seam + 按模型成本归因 breaker.ts / control.ts 成本/失控断路器(steer/constrain/stop)+ HITL 闸门 reflect.ts MemoryReflector —— 记忆冷凝 db.ts SQLite 持久层(窗口边界 + 历史)+ 持久成本账本 github.ts GitHub issue + CI 运行摄取(gh CLI) shellEnv.ts 为子进程解析 PATH 与 shell 环境 fs.ts / git.ts 沙箱化文件系统 + git 桥 preload/ contextBridge → 类型化 window.cth API renderer/src/ App.tsx 顶层布局与接线 design/ tokens.css / tokens.ts / global.css(设计源数据) components/ PixelPanel、AgentDetailPanel、CommandBar、ApprovalsPanel、MemoryPanel、… CommandCenterPanel Michael 的控制面(Terminal/Floor/Memory/Activity/Tasks/Triggers/Handbook 标签) ToolWaterfall 按智能体工具跨度瀑布流(可观测性视图) TasksKanban 感知依赖的看板(Tasks 标签) ThreadsPanel hive 消息会话查看器(Messages 标签) MessageQueueComposer 为繁忙智能体暂存消息 scene/office/ Pixi 办公室地板:OfficeFloor、Character、Camera、cast、寻路、… store/ · hooks/ zustand store、事件循环、PTY 解析器、typewriter assets/ 图块集、地图、角色表 docs/ logo.png、banner.png、落地页(GitHub Pages → munderdiffl.in) docs/media/ og.png(社交预览图)+ 渲染的 Remotion 片段 landing-remotion/ Remotion 项目,渲染落地页 "how it works" 片段 HIVE.md · SPEC.md · DESIGN.md 多智能体 · 终端/事件 · 视觉设计 docs/message-queue.md 谁可以在何时向智能体的终端打字主进程入口 src/main/index.ts(约 5400 行)是所有服务的装配点:PtyManager、HiveManager、HookServer、CircuitBreaker、TelemetryCollector、MemoryManager、MemoryReflector、PersistStore、RosterStore、WorkerWakeWatchdog、IntegrationBroker在此实例化并互相注入。还有若干值得单独留意的细节:
- 异常存活:主进程注册了
uncaughtException/unhandledRejection处理器并保持进程存活——harness 是多智能体监管者,一个孤立的 throw(比如 node-pty 的 ConPTY 控制台 helper 在快速退出的智能体 CLI 控制台已消失时崩溃)绝不能拖垮整个应用和所有运行中的智能体; - 多窗口:
allWindows注册表跟踪主窗口 + 多个 floor 窗口,floorSeq为每个 floor 生成唯一会话分区,PtyManager.countByOwner/killByOwner让每个 floor 的终端流互相隔离; - 隔离 worktree:
isolate: true的智能体会获得专用 git worktree(worktreePaths映射),worker 结束但 worktree 持有未整合工作时,teardown 会保留它并通知 GOD 整合,由 GC 扫描在证明已整合后回收; - 任务调度:
syncMissions从持久配置重建定时器,支持 interval 与 weekly(周几 + 时刻,考虑 DST)两种形态,heartbeat 任务用自适应节奏自调度; - 保持唤醒:只要存在存活 PTY,就持有
powerSaveBlocker(默认prevent-app-suspension,可选strongKeepalive升级到prevent-display-sleep),防止系统休眠冻结整个 hive,空闲时自动释放。
设计系统:Animal Crossing × Earthbound × SNES 菜单
设计规范见 DESIGN.md(架构文档声明其为权威规范:任何新组件都必须从设计令牌派生)。美学基调是Animal Crossing × Earthbound × SNES 菜单 UI——像素对齐、厚实、友好。Munder Difflin 品牌在之上叠加了Dunder-Mifflin 酒红#6E1423与金色#F4D35E,用于 logo 与 chrome;15 个头像即《办公室》剧组演员阵容,靠发型 / 肤色 / 衬衫配方区分。
令牌体系(design source of truth)
令牌同时存在于两个同步文件:src/renderer/design/tokens.css(CSS 自定义属性)与src/renderer/design/tokens.ts(Pixi.js 与内联样式使用的 TS 对象)。关键令牌族:
- 色彩:cream 系(面板填充)、ink 系(文本/描边,正文禁用纯
#000)、六种 agent 强调色(coral/mint/sky/lemon/lilac/peach)、状态色(status-idle/status-thinking/status-working/status-blocked/status-looping等,其中status-looping表示断路器已武装的失控态)、世界色(草地/木地板/墙 3px 描边)。禁用渐变(除标题栏的垂直两档)。 - 字体:
Press Start 2P(展示,仅标题)、Pixelify Sans(UI)、VT323(终端)。单字重,永远不加粗,强调靠颜色或徽章;永远不加字距。 - 间距与栅格:基准 4px,一切间距都是 4 的倍数;窗格最小 1280×800;地板块 32×32;缩放只允许整数倍(1×/2×/3×)。
- 边框:SNES 三层边框(外 ink-900 2px / 中 cream-200 2px / 内 ink-700 1px),用嵌套
box-shadow inset而非嵌套 DOM 实现,总边框重 5px/边,无圆角。 - 动效:UI 基本静止,动画属于游戏层;唯一允许的阴影是 4px/4px 硬偏移无模糊。
prefers-reduced-motion时关闭角色浮动、粒子,行走变瞬移。
角色与办公室地板规范
- 头像精灵:24×24 单元,4 帧行走循环(idle/step-A/idle/step-B)@ 8fps,四朝向;每个角色恰好 4 色(skin/hair/primary/accent + ink-900 轮廓)。内置科学家 / 法师 / 宇航员 / 猫村民 / 黑客 / 忍者六种原型。
- 状态覆盖层:thinking 三圆点、blocked 脉冲
!、success 四帧星爆、ghost 半透明。 - 工作台(Stations):64×64 的结构物——文件架(Read/Edit/Write)、终端台(Bash)、Web 传送门、MCP 角落、任务板(TodoWrite)、信箱(通知,旗子竖起)。每个台子有 idle / in use / highlighted 三态;工具结果会以"手持令牌"动画带回(Bash 是
>_终端块、Web 是地球、Read 是折纸等)。 - 文案:用人名而非"the agent",系统反馈 12 词以内,禁 emoji、禁感叹号(完成与通知除外)。"blocked" 对用户显示为 "needs you","typing" 显示为 "your draft"(那是你未发送的草稿,正压着它的消息队列,详见 docs/message-queue.md)。
测试与配套文档:如何继续深入
如果要在提交 PR 前验证对架构的理解,仓库提供了与上述子系统一一对应的测试(test/目录):
- PTY 与会话生命周期:test/agent-exit-record.test.cjs、test/agent-exit-signal-e2e.test.cjs、test/quit-sweep.electron.test.cjs、test/terminal-recovery.test.cjs;
- Hive 协作层:test/hive-cwd.test.cjs、test/hive-malformed-outbox.test.cjs、test/hive-roster-injection.test.cjs、test/hive-unknown-recipient.test.cjs、test/queue-delivery.test.cjs、test/voice-messages.test.cjs(redactSecrets 锁步测试);
- Hook 事件契约:test/hook-event-contract.test.cjs、test/hooks-framing.test.cjs、test/hooks-notification.test.cjs;
- 断路器 / 控制 / 成本:test/breaker.test.cjs、test/control.test.cjs、test/cost-lifetime.test.cjs、test/usage-cache-benchmark.manual.cjs;
- 渲染与场景:test/office-gl-recovery.test.cjs、test/ide-image.test.cjs、test/focus-mode.test.cjs;
- Windows / 终端细节:test/win-cmd-shim.test.cjs(对应
parseNpmCmdShim)、test/arabic-terminal.test.cjs、test/ime-guard.test.cjs。
配套规范文档还包括 HIVE.md(多智能体层完整设计)、SPEC.md(终端/事件平面)、DESIGN.md(视觉设计)、docs/message-queue.md("谁可以在何时向智能体的终端打字")与 MEMORY_GRAPH_SPEC.md。
小结
Munder Difflin 的架构可以归纳为三点:主进程负责一切有副作用的编排(PTY、Hive 文件层、hook 事件、成本与断路器、git/fs 沙箱),渲染器只消费类型化 IPC(Pixi 地板 + xterm 终端),两个平面在智能体生命周期上统一收口(teardownPty)。这种设计让每个智能体仍然是一个标准的claude/codexCLI 进程——它们只读写 inbox/outbox 文件并响应 hook——而协作、仲裁、记忆与成本控制全部由外层 harness 承担。理解了这两条平面和它们之间的window.cth桥,你就能在源码中快速定位任何一个"智能体状态为什么这样显示"或"消息为什么没送达"的问题。
【免费下载链接】munder-difflinA local multi-agent harness that works with your existing Claude Code, Codex subscriptions, allows you to run an office of agents项目地址: https://gitcode.com/GitHub_Trending/mu/munder-difflin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考