跳转到内容

02. Monorepo 与依赖边界

本章回答两个问题:为什么仓库要拆成这么多包,以及修改某类行为时应该进入哪一层。目录很多并不可怕,真正危险的是把“可共享领域逻辑”“服务端资源所有权”“Electron 特权能力”和“纯 UI”混在一起。

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,说明打包边界与源码包边界并不完全相同。

包/应用 近似 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 行,是运行行为最密集的地方。

根据各包 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

箭头表示“下游依赖上游”。几个值得注意的点:

  1. core 不依赖 shared,保证通用消息/工具/工作区类型不被运行时拖重。
  2. server-core 不依赖 Electron,因此同一个 SessionManager 可在桌面 main 和 headless server 使用。
  3. ui 可被 Electron 与 Viewer 复用,但不拥有 IPC 或 session runtime。
  4. session-tools-core 被单独抽出,是为了让 Claude in-process tool 与 Pi/MCP bridge 共用 schema/handler,而不制造循环依赖。
  5. pi-agent-server 作为可执行 artifact 被 PiAgent 通过进程协议使用,不需要成为普通静态依赖边。

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:BaseAgentClaudeAgentPiAgent、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 名称误以为所有代码都可在浏览器运行;大量模块依赖 fschild_process 和 SDK。

2.6 server-core:资源所有权与应用服务

Section titled “2.6 server-core:资源所有权与应用服务”

它主要拥有三类东西:

  1. 生命周期资源SessionManager、agent、MCP pool、browser host pin、background task registry。
  2. 传输入口:WS server/client、handler registration、RPC DTO 到领域调用的转换。
  3. 跨会话编排: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。这是组合根应有的形态:选择具体依赖,尽量不承载业务分支。

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,并实现只应在宿主机执行的能力。

是信任边界。它不应把任意 IPC 原样暴露给页面,而是构造受控 API。启动时根据环境选择:

  • 正常桌面:local RPC + 可选 remote workspace RPC,经 RoutedClient 分流;
  • thin client:直接连远端 WS;
  • 注册浏览器、打开 URL、dialog 等 client capabilities。

源码:apps/electron/src/preload/bootstrap.ts

只把 server events 投影成 session/UI state。虽然 App.tsxAppShell.tsx 很大,Agent 语义仍应通过 RPC/typed event 进入,不能在组件里另造后端状态。

放应用路由、Electron 侧 DTO 等。它不等于仓库级 packages/shared

Pi 依赖是 ESM、体积大,而且运行时/打包条件与 Electron 主进程不同。独立进程提供:

  • 依赖隔离;
  • 崩溃隔离;
  • stdout JSONL 协议;
  • 可单独打包的 runtime;
  • parent 可强杀,不让 provider runtime 污染 host。

某些 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。

它既有纯类型,也有 Node 存储、provider adapter、凭据和 UI 可读 DTO。好处是复用直接,坏处是循环依赖风险高、bundle 边界不清,代码中出现不少 lazy import 和 callback registry 来解环。

集中让不变量易统一,但 9,000 行类同时处理消息、branch、browser、automation、source refresh、transfer,修改时认知负担很大。仓库已经开始抽 domain/*tasks/*、runtime config helper,这是继续拆分的方向。

AppShellChatDisplayFreeFormInput 等大文件融合布局、快捷键、数据请求和局部状态。event processor 的纯函数化做得很好,但 UI composition 仍有继续模块化空间。

需求 首选入口 不应直接改
新 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

这个 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 和消息平台。