跳转至

02 · Monorepo 分层与依赖方向

pnpm workspace依赖图Server / UI / CLI扩展包

一句话结论: Paperclip 的真正边界不是顶层目录,而是“共享契约 → 数据与适配器基础设施 → Server 编排 → UI/CLI 消费”的依赖方向;插件和 catalog 作为旁路扩展进入。

1. 仓库规模先给一个直觉

快照包含约 3,202 个 TypeScript/TSX 文件、1,229 个测试文件、111 个 schema 文件和 210 个迁移 SQL。主要源码目录的文件量为:

目录 文件数 主要职责
server/src 819 API、业务服务、调度、恢复、授权、插件宿主
ui/src 1,278 React 产品界面、API client、实时缓存
cli/src 151 安装、配置、诊断与控制面命令
packages/ 1,416 DB、共享契约、adapter、插件、catalog、MCP server
doc/ + docs/ 269 内部设计/runbook 与用户文档

这决定了正确的阅读方式:先找跨层 contract,再顺着一次业务流纵向走,不能按目录逐文件扫描。

2. 顶层布局

paperclip-master/
├── server/                 # Node/Express 控制平面
├── ui/                     # React/Vite Board UI
├── cli/                    # paperclipai 命令行
├── packages/
│   ├── shared/             # 常量、DTO、Zod、配置 schema
│   ├── db/                 # Drizzle schema、迁移、连接
│   ├── adapter-utils/      # adapter 执行契约和通用引擎
│   ├── adapters/           # 各运行时实现
│   ├── plugins/            # SDK、示例与 provider 插件
│   ├── mcp-server/         # Paperclip 自身 MCP server
│   ├── skills-catalog/     # 只读技能目录
│   └── teams-catalog/      # 团队模板目录
├── skills/                 # 随源码维护的 Paperclip 操作技能
├── scripts/                # 构建、迁移、发布、smoke、截图
├── tests/                  # Playwright E2E / release smoke / visual
├── doc/                    # 内部规范、计划、runbook
└── docs/                   # 面向用户的文档站源文件

3. 依赖方向

flowchart TB
  SHARED["@paperclipai/shared\n常量 · 类型 · Zod"]
  DB["@paperclipai/db\nSchema · Migration · Connection"]
  AU["@paperclipai/adapter-utils\n执行契约 · 通用引擎"]
  ADAPTERS["Adapters\nClaude · Codex · Gemini · ..."]
  SDK["Plugin SDK / Catalog packages"]
  SERVER["@paperclipai/server\n业务编排与宿主"]
  UI["@paperclipai/ui\nBoard UI"]
  CLI["paperclipai\nSetup + API client"]

  SHARED --> DB
  SHARED --> AU
  SHARED --> ADAPTERS
  AU --> ADAPTERS
  DB --> SERVER
  SHARED --> SERVER
  AU --> SERVER
  ADAPTERS --> SERVER
  SDK --> SERVER
  SHARED --> UI
  SHARED --> CLI
  SERVER -. "HTTP / WebSocket" .-> UI
  SERVER -. "HTTP" .-> CLI

箭头表达“被依赖”。两个要点:

  • UI 不直接依赖 Server 内部类型,而主要消费 shared 契约和 HTTP 响应;
  • Adapter 不应该反向引用 Server 业务服务,它通过 adapter-utils 定义的 context、callbacks 和 result 与 Server 协作。

4. packages/shared:跨层词汇表

shared 同时承担四种职责:

  1. constants.ts:状态枚举、角色、风险级别、事件类型;
  2. types/:API/领域 DTO;
  3. validators/:Zod 输入校验及 z.infer 类型源;
  4. config-schema.ts:CLI 与 Server 共用的配置合法性。

这是一种“显式共享内核”。好处是 Server/UI/CLI 对 HeartbeatRunStatus、IssueStatus 等不会各造一套字符串。风险是 shared 过度膨胀会成为全局耦合点,所以它应保存稳定契约,而不是业务执行逻辑。

5. packages/db:数据库才是最终事实

这里不是只放 ORM model:

  • src/schema/*.ts 描述表、外键、索引和 check;
  • src/migrations/*.sql 保存真实演进历史;
  • connection 与 migration helper 处理外部/embedded PostgreSQL;
  • Server service 直接使用 Drizzle query builder 和必要的 SQL 锁。

Paperclip 许多核心保证来自数据库条件,而不是 TypeScript 类型。例如:

  • profile、routine、origin 等幂等键使用条件唯一索引;
  • document revision 使用 (documentId, revisionNumber) 唯一键;
  • pipeline case 使用 case key、request key 与 automation attempt 约束重复;
  • checkout 使用带旧值前置条件的更新,并在部分释放路径使用 FOR UPDATE。

6. adapter-utils 与 adapters/

adapter-utils 是执行面 SPI:

ServerAdapterModule
├─ execute(context) -> result
├─ testEnvironment(...)
├─ session codec
├─ skills sync / materialization
├─ model metadata
├─ optional local JWT support
└─ config schema / instructions capabilities

具体包再实现运行时差异:

类别 包例子 特点
本地 CLI claude-local、codex-local、gemini-local、pi-local 子进程、流式事件、session resume、技能注入
Cloud/API cursor-cloud 远端执行与 provider 状态
Gateway openclaw-gateway、hermes-gateway 把外部长期运行 Agent 纳入控制面
通用 process、http(Server 内建) 低假设执行入口

UI 也有 adapter display registry,用于配置表单和 transcript parser;CLI 有 formatter registry。这三面必须保持 metadata 一致。

7. Server 内部再分五层

flowchart LR
  MW["middleware\n身份 / 日志 / 防护"] --> ROUTE["routes\nHTTP 形状 / 校验 / 授权入口"]
  ROUTE --> SERVICE["services\n业务状态机 / transaction"]
  SERVICE --> DB2["db\n持久化事实"]
  SERVICE --> EXT["adapter / plugin / storage / provider"]
  SERVICE --> EVT["live event / activity / audit"]

现实中边界并非绝对:大路由文件会包含查询编排,巨型 service 会直接处理很多基础设施细节。但总体约束是:route 决定请求语义,service 保持跨入口一致的业务不变量。

最值得优先读的 Server 文件:

文件 约行数 原因
services/heartbeat.ts 19,115 唤醒、执行、结果、重试、清理的总编排
routes/issues.ts 11,979 Issue 及其子资源的 HTTP 总入口
services/issues.ts 9,010 Issue 状态、checkout、依赖、释放
services/tool-access.ts 7,771 连接、catalog、profile、OAuth、runtime
routes/openapi.ts 7,365 大量共享 Zod → OpenAPI 映射
services/tool-gateway.ts 6,316 调用时策略、审批与审计
services/recovery/service.ts 5,813 孤儿、stale lock、liveness 恢复

8. UI 不是薄皮 CRUD

UI 有三类关键层:

pages/components
      ↓
api/*.ts + queryKeys.ts
      ↓
TanStack Query cache
      ↑
LiveUpdatesProvider ← company WebSocket

页面 mutation 成功会按 query key 失效缓存;实时事件则先对高频 run 状态做 setQueryData 精准修补,再对受影响资源批量 invalidateQueries。这一设计避免每条 log 都触发全屏 refetch。

LiveUpdatesProvider.tsx 本身很大,因为它同时处理:

  • 单 socket 连接、鉴权与指数退避重连;
  • company 过滤;
  • run/issue/agent/activity cache patch;
  • 可见页面的 toast 抑制;
  • component subscriber fan-out;
  • 断线后无法回放时的一次对账 refetch。

9. CLI 有两种人格

paperclipai 既是实例管理器,又是控制面 client:

install、update、onboard、doctor、configure、run、service、db:backup。

它管理本地 payload、配置、embedded PostgreSQL、健康检查和后台服务。

company、issue、agent、approval、routine、pipeline、skills、teams、workspace 等。

它解析 context/profile/API key,再调用与 UI 相同的 HTTP 控制面。

CLI 测试中大量使用“API parity”命名,说明维护目标不是做一个功能缩水的旁路,而是让关键操作在命令行也可达。

10. 插件与 catalog 是旁路扩展

平台模块和插件要分开:

  • 平台模块:可信、进程内,扩展 adapter/storage/secret/log 等深层能力;
  • 插件:独立 worker 进程,manifest 声明 capability、job、tool、webhook、UI slot 等加法式能力;
  • catalog 包:构建期把目录树编译成只读 manifest,安装时复制进 company library。

这三类扩展拥有不同信任级别,不能仅因为都位于 packages/ 就视为同一插件机制。

11. 构建与发布拓扑

Root package scripts 用 pnpm workspace 驱动:

flowchart LR
  INSTALL["pnpm install"] --> GEN["manifest / generated files"]
  GEN --> TC["typecheck"]
  GEN --> TEST["Vitest groups / serialized tests"]
  GEN --> BUILD["packages + server + UI + CLI"]
  BUILD --> E2E["Playwright / Docker smoke"]
  E2E --> RELEASE["canary / stable packages + tags"]

Release manifest 明确控制要发布的 workspace 包,脚本还检查 lockfile、禁止隐式 git push、验证包可独立安装。发布不是简单执行一次 npm publish。

12. 如何在这个仓库里定位功能

以“Issue checkout”为例:

  1. 在 shared 搜输入 schema 和状态常量;
  2. 在 routes/issues.ts 找 /checkout;
  3. 跳到 services/issues.ts :: checkout;
  4. 看 db/schema/issues.ts 的锁字段和索引;
  5. 搜 checkoutRunId 的 heartbeat/recovery 消费者;
  6. 看 UI API 与相关 mutation;
  7. 先读 issues-service.test.ts 中 checkout 相关测试名。

这套顺序比从 UI 点击处理函数一路盲跳更容易看到完整不变量。

13. 可复用经验

  1. 为共享状态词汇建立单独包,但避免把业务逻辑放进去。
  2. Adapter SPI 应同时定义执行、事件、会话、技能和诊断,而不只是 run()。
  3. UI 与 CLI 都作为控制面消费者,避免读取 Server 私有实现。
  4. 把扩展分信任等级:进程内平台模块、隔离 worker、只读 catalog 不应共享一套权限假设。
  5. 对巨型状态机按阶段和不变量拆解,不必机械追求“每文件一个职责”的表面整洁。

源码入口

  • pnpm-workspace.yaml、根 package.json:workspace 与构建命令
  • packages/shared/src/index.ts:共享契约出口
  • packages/db/src/schema/index.ts:数据库 schema 出口
  • packages/adapter-utils/src/index.ts:adapter 基础能力
  • server/src/app.ts、server/src/index.ts:Server 组合根
  • ui/src/App.tsx、ui/src/context/LiveUpdatesProvider.tsx:UI 组合与实时更新
  • cli/src/index.ts:CLI 命令总注册

下一章从目录进入业务:03 · 领域模型与公司边界。