02. Monorepo 与依赖边界
本章回答两个问题:为什么仓库要拆成这么多包,以及修改某类行为时应该进入哪一层。目录很多并不可怕,真正危险的是把“可共享领域逻辑”“服务端资源所有权”“Electron 特权能力”和“纯 UI”混在一起。
2.1 根工程
Section titled “2.1 根工程”根 package.json 定义 Bun workspaces:
{ "name": "craft-agent", "version": "0.11.2", "workspaces": ["packages/*", "apps/*", "!apps/online-docs"]}主要技术栈:
- TypeScript ESM + Bun;
- Electron 39 + React 18 + Jotai;
- Claude Agent SDK
0.3.197; - Pi AI/Coding Agent
0.80.6; - MCP SDK
1.29; - Zod、Vite、Tailwind、TipTap、Shiki;
ws风格 WebSocket RPC;- Bun test + TypeScript typecheck + 自定义结构 lint。
根脚本把构建分成 main、preload、renderer、resources 和两个 subprocess bundle,说明打包边界与源码包边界并不完全相同。
2.2 包清单
Section titled “2.2 包清单”| 包/应用 | 近似 TS/TSX 行数 | 角色 | 能否拥有 OS/网络资源 |
|---|---|---|---|
packages/core |
1k | 最底层纯类型和值对象 | 否 |
packages/shared |
116k | 领域逻辑、Agent backend、存储、source、automation | 受控,可以持有 backend/MCP |
packages/server-core |
27k | SessionManager、RPC handlers、WS transport、TaskRunner | 是,服务端资源所有者 |
packages/server |
0.5k | headless 可执行入口 | 是 |
packages/pi-agent-server |
5k | Pi SDK 隔离子进程 | 是,仅子进程内部 |
packages/session-tools-core |
8.6k | session tool schema 与纯/可注入 handler | 受控 |
packages/session-mcp-server |
0.6k | session tools 的 MCP 子进程包装 | 是,仅桥接 |
packages/ui |
30k | 可复用 React 展示组件 | 否 |
packages/messaging-gateway |
14k | 平台适配、绑定、路由、输出渲染 | 是 |
packages/messaging-whatsapp-worker |
1.8k | WhatsApp 隔离 worker | 是 |
apps/electron |
131k | 桌面 main/preload/renderer 与浏览器 pane | 是,Electron 特权层 |
apps/cli |
3.7k | RPC CLI 与自包含 run | 客户端网络 |
apps/webui |
0.9k | headless server 的轻量 Web UI | 浏览器沙箱 |
apps/viewer |
0.7k | 公开分享只读表面 | 浏览器沙箱 |
数字只是阅读优先级提示:apps/electron 最大,但核心语义并不在最大 UI 文件里;SessionManager.ts 单文件约 9,000 行,是运行行为最密集的地方。
2.3 依赖图
Section titled “2.3 依赖图”根据各包 package.json 的内部依赖,可以画出这张简化图:
flowchart TD CORE["@craft-agent/core"] STC["session-tools-core"] SHARED["@craft-agent/shared"] SC["server-core"] UI["@craft-agent/ui"] MG["messaging-gateway"] MW["whatsapp-worker"] SERVER["headless server"] ELECTRON["Electron"] CLI["CLI"] WEB["WebUI"] VIEWER["Viewer"] PIS["pi-agent-server"] SMS["session-mcp-server"]
CORE --> SHARED STC --> SHARED SHARED --> SC CORE --> SC CORE --> UI SHARED --> UI CORE --> MG SHARED --> MG SC --> MG MW --> MG SHARED --> SERVER SC --> SERVER MG --> SERVER SHARED --> ELECTRON SC --> ELECTRON UI --> ELECTRON MG --> ELECTRON SHARED --> CLI SC --> CLI SHARED --> WEB CORE --> VIEWER UI --> VIEWER STC --> SMS SHARED --> SMS箭头表示“下游依赖上游”。几个值得注意的点:
core不依赖shared,保证通用消息/工具/工作区类型不被运行时拖重。server-core不依赖 Electron,因此同一个 SessionManager 可在桌面 main 和 headless server 使用。ui可被 Electron 与 Viewer 复用,但不拥有 IPC 或 session runtime。session-tools-core被单独抽出,是为了让 Claude in-process tool 与 Pi/MCP bridge 共用 schema/handler,而不制造循环依赖。pi-agent-server作为可执行 artifact 被PiAgent通过进程协议使用,不需要成为普通静态依赖边。
2.4 core:值对象和协议底座
Section titled “2.4 core:值对象和协议底座”packages/core/CLAUDE.md 把这里定义为“shared type layer”。核心内容包括:
Message/StoredMessage;AgentEvent;- tool/auth/permission 基础类型;
- workspace 基础类型;
- 少量不依赖运行环境的 helper。
为什么要独立?因为 renderer、viewer、server、messaging 都需要理解消息,却不应该被迫加载 credential manager、Claude SDK 或 Node 存储实现。
设计规则可以概括为:
如果一个类型描述“跨进程要传什么”,优先放 core/protocol;如果描述“服务端如何做到”,不要放 core。
2.5 shared:名字叫 shared,实际上是领域内核
Section titled “2.5 shared:名字叫 shared,实际上是领域内核”packages/shared 是最大、最复杂的包。它包含:
- Agent:
BaseAgent、ClaudeAgent、PiAgent、backend factory; - Prompt:system prompt、context builder、conversation summary;
- 数据:workspace/session/project/source/skill/config storage;
- 集成:MCP pool、API tools、OAuth、credentials;
- 规则:permissions、statuses、labels、tasks schema;
- 自动化:event bus、conditions、handlers、history;
- 协议:channels、DTO、routing;
- 纯工具:路径、文件、图片、二进制、大响应保护等。
它同时承担“纯领域逻辑”和“可跨 host 复用的 Node 实现”,所以不是严格意义的 Clean Architecture entity 层。读者不要因 shared 名称误以为所有代码都可在浏览器运行;大量模块依赖 fs、child_process 和 SDK。
2.6 server-core:资源所有权与应用服务
Section titled “2.6 server-core:资源所有权与应用服务”它主要拥有三类东西:
- 生命周期资源:
SessionManager、agent、MCP pool、browser host pin、background task registry。 - 传输入口:WS server/client、handler registration、RPC DTO 到领域调用的转换。
- 跨会话编排:TaskRunner、transfer、automation prompt execution。
server-core 中 handlers 不应重新实现领域规则。例如 task 创建由 createTaskFromSpec 共享给 RPC 和 session tool callback,避免同一动作因入口不同而漂移。
2.7 server:组合根,而不是第二套后端
Section titled “2.7 server:组合根,而不是第二套后端”packages/server/src/index.ts 只有约 500 行,职责是:
- 读取环境变量与 TLS;
- 选择 headless platform;
- 注入
SessionManager、handlers、model refresh; - 把 WebUI HTTP handler 与 WS server 合并到同端口;
- 初始化 messaging registry;
- 处理 SIGINT/SIGTERM graceful shutdown。
它没有复制 SessionManager。这是组合根应有的形态:选择具体依赖,尽量不承载业务分支。
2.8 apps/electron:四个子世界
Section titled “2.8 apps/electron:四个子世界”Electron 应按进程再拆:
apps/electron/src/├── main/ OS、窗口、SessionManager、本地 server、browser pane├── preload/ 安全桥、RPC client、capability dispatcher├── renderer/ React/Jotai、event projection、交互└── shared/ main 与 renderer 可共用的本应用类型/route helper有 Node/Electron 权限。创建本地服务、窗口、托盘、更新器、浏览器 WebContentsView,并实现只应在宿主机执行的能力。
Preload
Section titled “Preload”是信任边界。它不应把任意 IPC 原样暴露给页面,而是构造受控 API。启动时根据环境选择:
- 正常桌面:local RPC + 可选 remote workspace RPC,经
RoutedClient分流; - thin client:直接连远端 WS;
- 注册浏览器、打开 URL、dialog 等 client capabilities。
源码:apps/electron/src/preload/bootstrap.ts。
Renderer
Section titled “Renderer”只把 server events 投影成 session/UI state。虽然 App.tsx 和 AppShell.tsx 很大,Agent 语义仍应通过 RPC/typed event 进入,不能在组件里另造后端状态。
Shared
Section titled “Shared”放应用路由、Electron 侧 DTO 等。它不等于仓库级 packages/shared。
2.9 两个子进程包为什么存在
Section titled “2.9 两个子进程包为什么存在”pi-agent-server
Section titled “pi-agent-server”Pi 依赖是 ESM、体积大,而且运行时/打包条件与 Electron 主进程不同。独立进程提供:
- 依赖隔离;
- 崩溃隔离;
- stdout JSONL 协议;
- 可单独打包的 runtime;
- parent 可强杀,不让 provider runtime 污染 host。
session-mcp-server
Section titled “session-mcp-server”某些 backend 更容易消费 MCP server 而非进程内 callback。该包把 session-tools-core 的统一定义包装成 JSONL/MCP 可执行服务,保持 tool schema 单一来源。
2.10 session-tools-core:解决循环依赖的一个案例
Section titled “2.10 session-tools-core:解决循环依赖的一个案例”session tool 同时需要:
- 被 Claude SDK 以 Zod schema 注册;
- 被 Pi 以 JSON Schema 注册;
- 调回 SessionManager 改 session;
- 使用 source/config 验证逻辑。
若直接写在 shared/agent,很容易形成 shared → server-core → shared。项目采用 callback registry + SessionToolContext:工具定义依赖抽象 context,SessionManager 在 runtime 注册具体 callback。
tool-defs.ts 明确把 Zod schema、描述、handler registry 定为单一事实源,再派生 Claude shape 与 Pi JSON Schema。
2.11 包边界中仍然存在的压力
Section titled “2.11 包边界中仍然存在的压力”shared 过大
Section titled “shared 过大”它既有纯类型,也有 Node 存储、provider adapter、凭据和 UI 可读 DTO。好处是复用直接,坏处是循环依赖风险高、bundle 边界不清,代码中出现不少 lazy import 和 callback registry 来解环。
SessionManager 过度集中
Section titled “SessionManager 过度集中”集中让不变量易统一,但 9,000 行类同时处理消息、branch、browser、automation、source refresh、transfer,修改时认知负担很大。仓库已经开始抽 domain/*、tasks/*、runtime config helper,这是继续拆分的方向。
Electron renderer 组件偏大
Section titled “Electron renderer 组件偏大”AppShell、ChatDisplay、FreeFormInput 等大文件融合布局、快捷键、数据请求和局部状态。event processor 的纯函数化做得很好,但 UI composition 仍有继续模块化空间。
2.12 修改导航
Section titled “2.12 修改导航”| 需求 | 首选入口 | 不应直接改 |
|---|---|---|
| 新 provider | shared/agent/backend + adapter |
renderer switch |
| 新 session 字段 | sessions/types.ts persistent fields + protocol DTO + projection |
只改 UI type |
| 新 RPC | protocol/channels/dto + server-core/handlers + routing |
raw IPC send |
| 新 session tool | session-tools-core/tool-defs.ts + handler/context |
Claude、Pi 各写一份 schema |
| 新 source auth | sources/types/server-builder + credential manager/OAuth |
明文写 config |
| 新 UI 卡片 | packages/ui + renderer parser/event state |
provider SDK event 直达组件 |
| 新自动化 action | shared/automations/handlers + schema/validation |
renderer 执行业务动作 |
| 新桌面特权能力 | main 实现 + preload capability/RPC | renderer 直接 Node API |
2.13 本章小结
Section titled “2.13 本章小结”这个 monorepo 的主依赖方向是:
core / session-tools-core → shared domain/runtime → server-core application services → server/electron/cli composition
core/shared → ui → electron/viewer surfaces下一章看进程边界。理解 RPC 之后,才知道同一个 SessionManager 为什么能同时服务桌面、本地 CLI、远程 WebUI 和消息平台。