乐于分享
好东西不私藏

opencode源码拆解第1篇 29 packages 全景图:monorepo 设计哲学

opencode源码拆解第1篇 29 packages 全景图:monorepo 设计哲学

🔭 如果让你设计一个几十个包组成的 CLI 工具,你会怎么组织代码仓库?

一个直觉是把所有代码写在一个大包里——简单粗暴,但随着功能增加,构建越来越慢,依赖越来越乱,谁都不敢改公共模块。

另一个直觉是每个功能独立仓库——解耦彻底,但版本同步变成噩梦:A 包改了接口,B 包没升级,CI 一片红。

opencode 的作者选了第三条路——monorepo,但不是一个简单的 monorepo,而是一套精心设计的 29 个 packages 的分层体系,用 Bun 的 workspace 协议把每个包的版本牢牢锁在一起。

这篇文章带你从头看到尾,理解这套设计背后的权衡。


【问题】为什么需要 monorepo?

单体应用的困境

一个生产级 AI Coding Agent 涉及的能力很广:

  • CLI 入口、HTTP 服务、TUI 界面
  • LLM 调用、工具执行、会话管理
  • 权限控制、配置管理、插件系统
  • 桌面应用、Web 前端、VS Code 扩展
  • SaaS 控制台、企业功能、Slack 集成

如果所有代码塞进一个包里,随着功能增长必然面临:

❌ 构建慢:改一行代码要等全量编译❌ 依赖乱:全局 package.json 膨胀到 200+ 依赖❌ 不敢重构:公共模块改了,不知道影响了谁❌ 发布粒度粗:改个小功能也要发整个版本

多仓库的困境

拆成独立仓库能解决单体的局部性问题,但引入了新的系统性风险:

❌ 版本同步:A 包 v2.1 需要 B 包 v1.8,手工维护版本矩阵❌ 原子发布难:改一个跨包 feature,要依次发布 N 个包❌ 开发体验差:改一个包要切 repo、装依赖、跑测试、提 PR❌ CI 重复:每个 repo 都要配置一模一样的 CI,出问题 N 倍排查成本

monorepo 的解

monorepo = 一个仓库管理所有包,兼具单体的集中性和多仓的模块化。

opencode 选择的路线是一条清晰的"三层契约":

  • Bun 作为包管理器(取代 pnpm/yarn/npm)
  • catalog 统一版本锁定(根 package.json 一言九鼎)
  • workspace:* 协议跨包引用(编译期保证版本一致)

【设计】29 个 packages 的分层架构

全景数据

运行仓库附带的 demo/monorepo-stats.sh,可以直接看到全貌:

关键数据:

  • 29 个 workspace packages(不含 VS Code 扩展,其中 25 个来自顶层目录、4 个来自 console/stats 嵌套子包)
  • 包管理器: Bun 1.3.14
  • 版本锚点: effect 4.0.0-beta.74、typescript 5.8.2、zod 4.1.8、ai 6.0.168
  • 依赖锁定: 所有子包通过 "catalog:" 协议引用同一个版本

六层架构

这 29 个包按职责分为 6 层,每层只能依赖下层:

  层6: 发布与文档      containers/*, web, storybook                  ↑  层5: 平台 SaaS       console-*, enterprise, slack, stats-*                  ↑  层4: UI 与应用       app, ui, desktop, tui                  ↑  层3: CLI 与服务      opencode, cli, server                  ↑  层2: 扩展与 SDK      sdk, plugin, script, vscode                  ↑  层1: 基础设施        core, llm, function, effect-drizzle-sqlite, http-recorder

核心包一览

层级
包名
职责
技术栈
L1
@opencode-ai/core
基础设施:运行时、日志、配置、错误类型
Effect-ts
L1
@opencode-ai/llm
LLM 客户端:多 Provider 调用、流式响应
Effect-ts
L1
@opencode-ai/effect-drizzle-sqlite
数据库抽象层:Drizzle ORM + Effect
Effect-ts
L2
@opencode-ai/sdk
TypeScript SDK:REST API 客户端
Effect-ts
L2
@opencode-ai/plugin
插件 API:类型定义与扩展点
TypeScript
L3
opencode
核心 CLI:Agent 循环、工具执行、会话管理
Effect-ts
L3
@opencode-ai/cli
CLI 入口:yargs 构建、子命令分发
TypeScript
L3
@opencode-ai/server
HTTP 服务:REST API、WebSocket
Hono
L4
@opencode-ai/app
桌面 UI:SolidJS 应用、组件组合
SolidJS
L4
@opencode-ai/ui
UI 组件库:聊天消息、Diff 视图、主题
SolidJS
L4
@opencode-ai/tui
终端 UI:命令行交互界面
TypeScript
L5
@opencode-ai/console-*
控制台平台:SaaS 用户管理、计费
SolidJS + SST
L5
@opencode-ai/enterprise
企业功能:SSO、审计、团队管理
TypeScript
L5
@opencode-ai/slack
Slack 集成机器人
TypeScript
L6
packages/web
官网与文档(Astro 构建)
Astro

依赖方向

设计上强制了 单向依赖:

L1 基础设施 → L2 扩展/SDK → L3 CLI/服务 → L4 UI → L5 平台 → L6 发布

L1 只依赖第三方库,不依赖任何 workspace 包。L3 可以依赖 L1/L2,但不能依赖 L4。跨层依赖(如 L3 依赖 L4 的 tui)在功能紧密耦合时可以破例,但不鼓励。

真实的依赖关系

从核心包 opencode(L3)的 package.json 中可以看到它的 workspace 依赖:

opencode 依赖于:  @opencode-ai/llm       ← L1:LLM 调用  @opencode-ai/plugin    ← L2:插件 API  @opencode-ai/script    ← L2:脚本工具  @opencode-ai/sdk       ← L2:SDK 客户端  @opencode-ai/server    ← L3:HTTP 服务(同层)  @opencode-ai/tui       ← L4:终端界面(⚠️ 跨层例外)

注意到 tui 是 L4 层,但 opencode(L3)直接依赖它——说明单向依赖不是死规定,当功能紧密耦合时可以合理破例。设计原则在真实工程中允许例外。

下图展示了跨包之间的核心依赖链路:

分层收益

  • 基础层稳定:core、llm 等底层包的变更不会波及所有上层
  • 并行开发:UI 团队改 app,CLI 团队改 opencode,互不阻塞
  • 可测试性:任意层都可以 mock 下层进行独立单元测试

【源码】root package.json — monorepo 的指挥中心

workspaces 声明

monorepo 的核心配置在文件 package.json(仓库根目录)的 workspaces 字段中:

{"workspaces":{"packages":["packages/*","packages/console/*","packages/stats/*","packages/sdk/js","packages/slack"],"catalog":{"effect":"4.0.0-beta.74","typescript":"5.8.2","zod":"4.1.8","ai":"6.0.168","solid-js":"1.9.10","drizzle-orm":"1.0.0-rc.2","hono":"4.10.7","shiki":"3.20.0"}}}

文件路径:package.json — 第 24–92 行

机制一:workspaces.packages — 包发现

packages/* 匹配顶层所有目录,packages/console/* 深入嵌套的 console 子包,packages/sdk/js 精确指定单个包。组合使用让目录结构兼顾规则性和灵活性。

Bun 在 bun install 时会扫描这些目录,自动建立内部链接——不需要 npm link、不需要 yalc、不需要手工配置。

机制二:catalog — 版本锚点

Bun 的 catalog 机制(类似 pnpm 的 pnpm-workspace.yaml)将关键依赖的版本统一在根 package.json 中定义。每个子包引用时用 "catalog:" 协议:

// packages/opencode/package.json{"dependencies":{"effect":"catalog:","typescript":"catalog:"}}

当一个版本定义、全仓库生效——避免了 A 包用 zod 4.1.0、B 包用 zod 4.1.8 的版本碎片化。

catalog vs 直接写版本号的区别:

方式
优势
风险
各包独立版本
灵活升级
版本碎片化,CI 偶发兼容问题
catalog 统一锁定
全仓库一致
升级需全局评估,但可通过 Bun 的 bun update 逐步推进

机制三:workspace:* — 跨包依赖

包之间引用使用 workspace:* 协议:

// packages/opencode/package.json{"dependencies":{"@opencode-ai/llm":"workspace:*","@opencode-ai/plugin":"workspace:*","@opencode-ai/script":"workspace:*","@opencode-ai/sdk":"workspace:*","@opencode-ai/server":"workspace:*","@opencode-ai/tui":"workspace:*"}}

* 表示"当前 workspace 的最新版本",Bun 在安装时自动链接成本地文件系统链接。发布时,bun publish 自动将 workspace:* 替换为实际版本号——开发者无需手动维护版本映射表。


【权衡】Monorepo vs Polyrepo:选择的本质

决策树

opencode 选 monorepo 的核心原因是:

29 个包中大部分随 CLI 一起发布,共同演化频率极高。

场景
适合模式
理由
包独立迭代,互不依赖
Polyrepo
灵活发布,独立版本号,互不影响
2–5 个包偶尔同步
Monorepo 轻量
pnpm workspaces + 手动版本管理即可
20+ 个包频繁同步发布Monorepo 重度Bun + catalog + workspace: 三件套
*
需外部开发者独立消费
独立仓库 + SDK 发布
对外提供稳定 API,不暴露内部包

版本管理对比

Monorepo 的做法:

catalog 一个版本锚点控制所有 29 个包。升级 effect 版本时,只需要改 root package.json 的 catalog.effect,所有子包自动继承。

# 一行命令升级全仓库的 typescriptsed -i 's/"typescript": "5.8.2"/"typescript": "5.9.0"/' package.jsonbun install

Polyrepo 的做法:

# 每个 repo 都要改,然后依次发布cd core && npm publishcd llm && npm update @opencode-ai/core && npm publishcd opencode && npm update @opencode-ai/llm && npm publish# ... 10 个仓库重复同样操作

构建编排对比:

  • Monorepo:Bun 原生 workspaces + Turbo 增量构建,只编译变更的包
  • Polyrepo:每个 repo 独立 CI/CD,配置重复但互不干扰

开发体验对比:

  • Monorepo:bun run --cwd packages/opencode dev,同一仓库内改 core → opencode 即时生效
  • Polyrepo:每个包本地 npm link 或 yalc,改 core → npm publish → opencode npm update

差异的本质

共同演化的频率决定选型。

如果 29 个包每个月才同步一次,polyrepo 更合适——各发各的版本,偶尔发个升级 PR 更新依赖。

但 opencode 的 29 个包中有 20+ 个随 CLI 一起发布,每次 Release 都要同步。monorepo 的 catalog + workspace 协议让这种高频同步从"手工操作"变成了"Bun 自动处理"。


【锚点】

Monorepo 的核心不是代码共享,而是保证共同演化的包始终同步。

本篇总结

知识点
一句话
为什么要 monorepo?
29 个包高频同步,polyrepo 的版本矩阵不可维护
怎么组织?
6 层单向依赖,每层只能依赖下层
怎么锁定版本?
catalog + workspace:* 双协议
选了哪些工具?
Bun + Turbo + catalog

下篇预告

这篇文章的背景知识是后续所有章节的基础。接下来深入每个 package:

篇目
聚焦的 package
核心主题
1.2
全局
调试环境搭建:从源码跑起来到断点切入
1.3
全局
Effect-ts 预备课:理解运行时基础
2.1
packages/cli
yargs CLI 构建:从 index.ts 到所有子命令
2.2
packages/opencode
bootstrap 初始化流程:环境检测与运行时
3.1
packages/opencode
斜杠命令系统:/review、/commit、/diff
4.1
packages/opencode
Agent 定义与注册:Info Schema、SubAgent
5.1
packages/opencode
Session 数据模型:行模型与版本演进
6.1
packages/opencode
Tool 接口设计:Builder 模式与 Zod Schema

相关学习资料