02 · Monorepo 分层与依赖方向¶
一句话结论: 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 同时承担四种职责:
constants.ts:状态枚举、角色、风险级别、事件类型;types/:API/领域 DTO;validators/:Zod 输入校验及z.infer类型源;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”为例:
- 在
shared搜输入 schema 和状态常量; - 在
routes/issues.ts找/checkout; - 跳到
services/issues.ts :: checkout; - 看
db/schema/issues.ts的锁字段和索引; - 搜
checkoutRunId的 heartbeat/recovery 消费者; - 看 UI API 与相关 mutation;
- 先读
issues-service.test.ts中 checkout 相关测试名。
这套顺序比从 UI 点击处理函数一路盲跳更容易看到完整不变量。
13. 可复用经验¶
- 为共享状态词汇建立单独包,但避免把业务逻辑放进去。
- Adapter SPI 应同时定义执行、事件、会话、技能和诊断,而不只是
run()。 - UI 与 CLI 都作为控制面消费者,避免读取 Server 私有实现。
- 把扩展分信任等级:进程内平台模块、隔离 worker、只读 catalog 不应共享一套权限假设。
- 对巨型状态机按阶段和不变量拆解,不必机械追求“每文件一个职责”的表面整洁。
源码入口¶
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 · 领域模型与公司边界。