Skip to content

第二章 Opencode 核心架构

← 返回目录 | 上一章:概述 | 下一章:Opencode Agent 系统 →


2.1 项目结构:Effect-TS 驱动的 Monorepo

Opencode 采用 pnpm monorepo 组织,核心包位于 packages/opencode/

opencode/
├── packages/
│   ├── opencode/          # 核心 CLI(本文重点)
│   │   └── src/
│   │       ├── agent/     # Agent 定义与模型映射
│   │       ├── session/   # 会话管理、消息处理、压缩
│   │       ├── tool/      # 工具注册与执行
│   │       ├── provider/  # 20+ LLM 提供商集成
│   │       ├── mcp/       # MCP 协议客户端
│   │       ├── lsp/       # 22+ 语言服务器管理
│   │       ├── skill/     # Skill 文件发现
│   │       ├── config/    # 配置加载与合并
│   │       └── plugin/    # 插件加载器
│   ├── app/               # Web 客户端 (Next.js)
│   ├── console/           # 后端 API 服务
│   ├── desktop/           # Electron 桌面客户端
│   ├── plugin/            # 插件 SDK 类型定义
│   └── sdk/               # JavaScript SDK

2.2 Effect-TS:为什么选择它?

Opencode 的一个显著技术决策是全面采用 Effect-TS 作为核心运行时框架,而非传统的 async/await 风格。这决定了整个代码库的编程范式。

Effect-TS 是一个 TypeScript 函数式副作用管理库,核心理念是将副作用(IO、错误、依赖)编码在类型系统中。你会在源码中大量看到以下模式:

typescript
// 典型的 Effect-TS 生成器模式
const result = yield* Effect.gen(function* () {
  const config = yield* Config;           // 依赖注入
  const session = yield* SessionService;  // 服务解析
  const result = yield* session.create(); // 副作用操作
  return result;
});

为什么这很重要? 因为 AI 编程工具的核心挑战恰好是:

  • 多层依赖管理:LLM 提供商、MCP 服务器、LSP 进程、文件系统、数据库——每一层都可能失败
  • 可组合的错误处理:工具执行失败不应该崩溃整个会话,而是需要优雅降级
  • 生命周期管理:LSP 服务器、MCP 连接、子进程都需要启动和清理
  • 可测试性:Effect 的 Layer 机制天然支持依赖替换

Opencode 使用 Effect 的 Layer 系统构建服务依赖图:

typescript
// 简化的依赖注入示例
const AppLayer = Layer.mergeAll(
  ConfigLayer,        // 配置服务
  DatabaseLayer,      // SQLite 数据库
  ProviderLayer,      // LLM 提供商
  SessionLayer,       // 会话管理
  ToolLayer,          // 工具注册
  McpLayer,           // MCP 服务器管理
  LspLayer,           // LSP 服务器管理
  PluginLayer,        // 插件加载
);

2.3 Provider 系统:统一的模型接入层

Opencode 通过 Vercel AI SDK 统一了 20+ LLM 提供商的接入。每个提供商以 loader 函数注册:

typescript
// provider/provider.ts 中的提供商列表(部分)
const BUNDLED_PROVIDERS = [
  "anthropic",     // Claude 系列
  "openai",        // GPT 系列
  "google",        // Gemini 系列
  "aws",           // Bedrock
  "azure",         // Azure OpenAI
  "xai",           // Grok 系列
  "groq",          // Groq 加速推理
  "mistral",       // Mistral 系列
  "deepseek",      // DeepSeek
  "fireworks",     // Fireworks AI
  // ... 共 20+ 个
];

每个提供商实现统一接口,包括:

  • 模型发现models() 返回可用模型列表(含 ID、名称、价格)
  • 聊天创建chat() 返回 AI SDK 兼容的语言模型实例
  • 成本追踪:输入/输出 token 的单价,用于实时费用计算

模型选择链路:用户配置 → Agent 默认模型 → Provider 解析 → AI SDK 实例化。每个 Agent 可以绑定不同的模型,OmO 正是利用这一点为每个 Agent 分配最适合的模型。

2.4 插件系统:8 个 Hook 接口

Opencode 的插件系统是 OmO 存在的基础。插件通过 npm 包分发,暴露一个工厂函数:

typescript
// 插件接口(来自 packages/plugin/)
interface OpenCodePlugin {
  name: string;
  config?: (input) => config;                    // 修改配置
  tool?: () => Tool[];                           // 注册自定义工具
  "chat.message"?: (msg) => msg;                 // 拦截消息
  "chat.params"?: (params) => params;            // 修改 LLM 请求参数
  "chat.headers"?: (headers) => headers;         // 修改 HTTP 头
  event?: (event) => void;                       // 监听事件流
  "tool.execute.before"?: (tool, input) => any;  // 工具执行前拦截
  "tool.execute.after"?: (tool, result) => any;  // 工具执行后拦截
  "experimental.chat.messages.transform"?: (...) => messages; // 变换消息序列
}

关键设计:插件的 config Hook 可以完全重写 Opencode 的配置——包括 Agent 定义、模型绑定、工具白名单。OmO 正是通过这个 Hook 注入了 12 个 Agent 和 26 个工具的完整定义。


2.5 事件总线架构

2.5.1 Effect PubSub——类型安全的事件系统

Opencode 的事件系统基于 Effect-TS 的 PubSub 原语构建,提供了类型安全的发布-订阅机制。这是整个平台的"神经系统"——所有组件间通信都通过事件总线完成。

架构设计

typescript
// 双通道设计
type State = {
  wildcard: PubSub<Payload>          // 通配订阅:接收所有事件
  typed: Map<string, PubSub<Payload>> // 类型订阅:按事件类型分发
}

每个事件发布时同时推送到两个通道:

typescript
function publish(def, properties) {
  const payload = { type: def.type, properties }
  // 1. 推送到类型特定的 PubSub(如果有订阅者)
  const ps = state.typed.get(def.type)
  if (ps) yield* PubSub.publish(ps, payload)
  // 2. 推送到通配 PubSub(所有订阅者都能收到)
  yield* PubSub.publish(state.wildcard, payload)
  // 3. 推送到全局 EventEmitter(跨实例)
  GlobalBus.emit("event", { directory, payload })
}

2.5.2 事件定义——Zod Schema 驱动

每个事件通过 BusEvent.define() 工厂函数定义,使用 Zod Schema 确保类型安全:

typescript
// 事件定义示例
const Idle = BusEvent.define("session.idle", z.object({
  info: SessionInfo,
}))

const Error = BusEvent.define("session.error", z.object({
  info: SessionInfo,
  error: z.unknown(),
}))

所有已定义的事件会注册到全局 registry,并通过 BusEvent.payloads() 生成 Zod 判别联合类型(Discriminated Union),用于运行时验证和 TypeScript 类型推导。

2.5.3 事件全景——42 个事件类型

Opencode 定义了 42 个事件类型,覆盖系统各层面:

领域事件用途
Sessionsession.status, session.idle, session.diff, session.error, session.compactedSession 状态机,驱动 OmO 续航/恢复
消息message.partDelta流式消息增量(用于 TUI 实时渲染)
Todotodo.updatedTodo 状态变更(驱动 Boulder 续航)
文件file.edited, file.watcher.updated文件修改事件(驱动 LSP 诊断)
PTYpty.created, pty.updated, pty.exited, pty.deleted终端进程生命周期
权限permission.asked, permission.replied用户权限交互
问题question.asked, question.replied, question.rejectedAgent 向用户提问
MCPmcp.tools.changed, mcp.browser.open.failedMCP 工具变更
LSPlsp.updated, lsp.diagnostics语言服务器状态
项目project.updated, vcs.branch.updated项目和版本控制
工作树worktree.ready, worktree.failedGit 工作树准备状态
命令command.executed命令执行记录
TUItui.prompt.append, tui.command.execute, tui.toast.show, tui.session.selectUI 交互
安装installation.updated, installation.update.available自动更新
服务器server.connected, server.instance.disposed, global.disposed服务器生命周期
控制面板workspace.ready, workspace.failed工作区状态
IDEide.installedIDE 扩展安装

2.5.4 三层传播机制

┌─────────────────────────────────────────────┐
│ 第1层: Effect PubSub (实例内)                 │
│   typed Map + wildcard → 同实例订阅者          │
├─────────────────────────────────────────────┤
│ 第2层: GlobalBus (跨实例)                     │
│   Node.js EventEmitter → 所有打开的项目实例    │
├─────────────────────────────────────────────┤
│ 第3层: SSE Endpoint (跨进程)                  │
│   HTTP Server-Sent Events → Web/Desktop 客户端│
└─────────────────────────────────────────────┘
  • 实例内:通过 InstanceState 确保每个项目目录有独立的 PubSub 实例,项目 A 的事件不会干扰项目 B
  • 跨实例GlobalBus(基于 Node.js EventEmitter)将事件广播到同进程的所有实例
  • 跨进程:SSE 端点将所有事件序列化为 JSON 流,推送给远程客户端

2.5.5 OmO 如何利用事件总线

OmO 的 event Hook 订阅 Opencode 的通配事件流,是整个插件系统的"感知层":

typescript
// OmO 的 plugin event handler 接收所有 Opencode 事件
event: async ({ event }) => {
  // 分发给 48 个 Hook 中所有注册了 event 处理器的 Hook
  for (const hook of eventHooks) {
    await hook.event({ event })
  }
}

关键的 OmO 事件消费者:

  • Atlas Hook:监听 session.idle 驱动任务续航
  • Ralph Loop:监听 session.idle 驱动迭代循环
  • Runtime Fallback:监听 message.updatedsession.status 检测 API 错误
  • Session Recovery:监听 session.error 触发自动恢复
  • Background Notification:监听后台任务完成事件通知用户
  • Think Mode:监听 session.deleted 清理状态

2.6 基础设施补充

前文聚焦于事件总线和 Provider 等上层系统。本节补充 Opencode 平台层中同样重要的基础设施模块。

2.6.1 配置系统

Opencode 的配置采用三层合并架构(config/config.ts,1576 行),优先级从低到高:

层级 1: 内置默认值(代码中的 Zod schema 默认值)
  ↓ 被覆盖
层级 2: 全局用户配置(~/.config/opencode/config.jsonc)
  ↓ 被覆盖
层级 3: 项目配置(项目根目录 .opencode/config.jsonc)
  ↓ 被覆盖
层级 4: 企业托管配置(/Library/Application Support/opencode/ 或 /etc/opencode/)

关键设计

  • JSONC 格式:使用 jsonc-parser 解析,允许注释——对复杂配置友好
  • Zod Schema 验证:所有配置字段都有 Zod schema 定义,确保类型安全
  • 数组合并策略plugin 数组在多层间拼接(而非替换),确保全局和项目的插件都被加载
  • 深度合并:对象字段递归合并(mergeDeep),数组字段合并去重
  • 热重载:配置变更通过事件总线广播,相关服务自动响应

配置管理的是几乎所有可变行为:Agent 定义、Provider 认证、工具开关、MCP 服务器、Skill 路径、LSP 服务器映射、权限策略、TUI 主题等。

2.6.2 TUI/CLI 客户端架构

Opencode 的终端界面(cli/cmd/tui/app.tsx,913 行)基于 SolidJS + Ink 构建——这是一个非常规但高效的选择:

  • SolidJS:用于状态管理和响应式 UI(createSignal, createEffect, createMemo
  • Ink/opentui:将 SolidJS 组件渲染到终端(而非浏览器 DOM)
  • 路由系统RouteProvider 管理页面导航(Home、Session 等)
  • 对话框系统:模型选择、MCP 配置、主题切换、命令面板等弹窗
  • 键盘快捷键KeybindProvider 统一的快捷键管理
TUI 架构层次:
┌──────────────────────────────────────┐
│ App.tsx (根组件)                      │
├─ ArgsProvider → SDKProvider          │
├─ SyncProvider → LocalProvider        │
├─ ThemeProvider → TuiConfigProvider   │
├─ DialogProvider → CommandProvider    │
├─ KeybindProvider → ExitProvider      │
├─ ToastProvider → PromptHistoryProv   │
└─ RouteProvider → Home | Session      │

Worker 架构:TUI 运行在一个独立的 Worker 线程(worker.ts)中,通过 RPC 与主进程通信。这隔离了 UI 渲染和 LLM 处理,防止长时间运行的 LLM 调用阻塞 UI 更新。

2.6.3 Provider 适配与消息变换

不同 LLM 提供商的 API 存在细微但关键的差异。ProviderTransformprovider/transform.ts,1045 行)负责统一处理这些差异:

消息规范化normalizeMessages):

  • Anthropic:拒绝空内容消息——系统自动过滤空字符串和空 text/reasoning 部分
  • Claude:工具调用 ID 中不允许特殊字符——系统自动替换为下划线
  • 通用:清理尾部空助手消息、合并连续同角色消息

Schema 适配

  • 按提供商映射 SDK key(@ai-sdk/anthropic"anthropic"@ai-sdk/openai"openai" 等)
  • 设置 OUTPUT_TOKEN_MAX(默认 32,000)限制最大输出 token 数
  • 模态能力检测(image/audio/video/pdf),对不支持的模态自动降级

为什么这很重要:没有这一层,Agent 在切换模型时会频繁遇到 API 错误。Provider Transform 让 Agent 系统可以透明地使用 20+ 个提供商的任意模型,而不需要 Agent 自身关心提供商差异。

2.6.4 文件监视器

FileWatcherfile/watcher.ts)使用 @parcel/watcher 原生绑定监控项目文件变化:

  • 平台适配:macOS 使用 FSEvents、Linux 使用 inotify、Windows 使用 Windows 原生 API
  • 事件类型:add(新建)、change(修改)、unlink(删除)
  • 事件总线集成:文件变化通过 file.watcher.updated 事件发布到全局 Bus
  • Git 忽略支持:自动排除 .gitignore 规则匹配的文件
  • 受保护路径:对 Opencode 自身的管理文件(如 SQLite 数据库)不触发变更事件
  • InstanceState 集成:每个打开的项目维护独立的 watcher 实例,项目关闭时自动清理

文件监视器的输出驱动了多个下游功能:LSP 服务器的文件同步、VCS 状态更新、TUI 文件树刷新等。

2.6.5 Git/VCS 集成

Opencode 通过多个模块实现 Git 集成(非通过 Git 工具给 Agent 用,而是平台自身需要的 VCS 感知能力):

  • VCS Serviceproject/vcs.ts):检测当前分支、监听分支变化(通过 FileWatcher 间接触发)、发布 vcs.branch.updated 事件
  • Git Utilityutil/git.ts):底层 git 命令执行封装
  • Worktree 管理worktree/index.ts):管理 git worktree(多分支并行开发场景),创建/列出/删除 worktree 目录

这些集成使得 Opencode 能够在系统提示中动态注入当前 Git 状态(分支名、是否有未提交变更等),帮助 Agent 理解项目上下文。

2.6.6 自动更新与安装管理

Installationinstallation/index.ts)管理 Opencode 自身的版本更新:

  • 支持多种安装方式检测(npm、bun、homebrew、手动安装等)
  • 版本更新检查与通知
  • 发布类型区分(stable、canary 等)
  • 更新事件通过 Bus 广播,TUI 可显示更新提示

← 返回目录 | 上一章:概述 | 下一章:Opencode Agent 系统 →