一、引言:为什么读 Cordis 的源码
Cordis(拉丁语"心")是一个只有约 2000 行 TypeScript 的元框架,却支撑了 Koishi(4000+ 插件的聊天机器人框架)和 DeepSeek Harness(深度求索的 Agent 运行时)两个重量级项目。
它的核心命题很简单,但实现极难:
如何让软件的组件可以安全地组合、热插拔,并在卸载时完整逆转其所有副作用?
传统框架的插件系统往往是"能装不能卸"------注册容易,清理困难。Cordis 用一套精巧的代码结构,将"可逆副作用"从设计原则落实到了每一行实现。这篇分享将带你逐层拆解它的代码核心。

二、代码仓库概览与目录结构
2.1 仓库结构
Cordis 采用 monorepo 结构,核心代码集中在 packages/core/ 下:
cordis/
├── packages/
│ ├── core/ # 核心框架 (~2000 行 TS)
│ │ ├── src/
│ │ │ ├── context.ts # Context 类:服务容器与 API 入口
│ │ │ ├── fiber.ts # Fiber 类:插件生命周期状态机
│ │ │ ├── service.ts # Service 基类:服务提供方抽象
│ │ │ ├── events.ts # 类型化事件系统(4 种分发模式)
│ │ │ ├── registry.ts # 插件注册表与依赖解析
│ │ │ └── index.ts # 导出聚合
│ │ └── package.json
│ ├── loader/ # 配置驱动加载器
│ ├── hmr/ # 热模块替换
│ └── timer/ # disposal-aware 定时器
├── docs/
└── package.json
2.2 在 DeepSeek Harness 中的引入方式
DeepSeek Harness 没有通过 npm 依赖 Cordis,而是采用 vendor 方式将源码直接纳入仓库:
yaml
# vendor/README.md 中的包映射
cordis/ → @deepseek-ai/cordis (v4.0.0-rc.7)
loader/ → @deepseek-ai/cordis-plugin-loader
hmr/ → @deepseek-ai/cordis-plugin-hmr
这种引入方式让 Harness 可以:
- 审计:每一行框架代码都在仓库内
- 打补丁:针对 Agent 场景做局部增强
- 锁定版本:避免上游 breaking change 影响生产
Vendor(收编/内嵌) 是一种软件工程实践,指将第三方依赖的源代码直接复制到自己的项目代码库中,而不是通过包管理器(如 npm、pip、cargo)以外部依赖的形式引用。
三、核心数据结构:Context、Fiber、Service
Cordis 的整个架构可以概括为三个核心对象的协作。
3.1 Context ------ 服务的容器与插件的"世界"
Context 是 Cordis 中最核心的类。它同时承担三个角色:
- 依赖注入容器 :通过
ctx.<key>访问服务 - 插件 API 入口 :
ctx.plugin()、ctx.effect()、ctx.on()等 - 作用域边界:子 Context 继承父 Context,但可以被隔离
typescript
// 伪代码示意 Context 的核心结构
class Context {
// 当前 fiber(插件运行时实例)
fiber: Fiber
// 父上下文
parent: Context | null
// 服务存储(内部 Map)
private services: Map<string, any>
// 核心 API
plugin(plugin: Plugin, config?: any): Fiber
effect(execute: () => Effect, label?: string): Disposable
on<K extends keyof Events>(event: K, listener: Listener): Disposable
provide(name: string, value: any): Disposable
get(name: string): any
}
关键设计 :Context 是插件中唯一的可变对象。 所有副作用都通过 Context 注册,所有服务都通过 Context 访问。这使得 Cordis 可以精确追踪"谁做了什么",从而在卸载时精准回滚。
3.2 Fiber ------ 插件的生命周期状态机
Fiber 是 Cordis 中最精妙的设计之一。每个被加载的插件实例都对应一个 Fiber,它管理着该插件的完整生命周期。
typescript
class Fiber {
// 唯一标识(root 为 0,dispose 后为 null)
uid: number | null
// 插件运行的上下文
ctx: Context
// 验证后的配置
config: any
// 当前生命周期状态
state: 'pending' | 'loading' | 'active' | 'unloading' | 'disposed' | 'failed'
// 进行中的加载/卸载过渡(防止竞态)
inertia: Promise<void> | undefined
// 依赖的服务实现快照
store: Dict<Impl> | undefined
// 核心方法
effect(execute, label?): Disposable // 注册副作用
dispose(): Promise<void> // 卸载插件
restart(): Promise<void> // 重载插件
update(config, noSave?): Promise<void> // 更新配置并重启
await(): Promise<this> // 等待生命周期稳定
}
Fiber 的状态转换图:

关键状态说明:
| 状态 | 含义 |
|---|---|
| PENDING | 已声明,但所需服务尚未就绪。插件在此状态下不会执行 apply,也不会阻止进程退出。 |
| LOADING | apply 函数正在执行。 |
| ACTIVE | apply 已成功完成,所有副作用已注册。 |
| FAILED | apply 或配置验证抛出异常。 |
| UNLOADING | 正在执行 disposers,按注册逆序清理副作用。 |
| DISPOSED | 所有清理已完成,资源已释放。 |
3.3 Service ------ 服务的提供方抽象
Service 是一个基类,任何希望向其他插件暴露能力的插件都可以继承它。
typescript
class Service {
// 声明依赖(静态属性)
static inject: string[]
// 构造函数中注册服务
constructor(ctx: Context, name: string, immediate?: boolean) {
// 内部调用 ctx.provide(name, this)
// 注册是一个 effect,卸载时自动注销
}
}
运行时与编译时分离:
typescript
// 运行时:super(ctx, 'greeter') 将实例注册到 ctx.greeter
class GreeterService extends Service {
constructor(ctx: Context) {
super(ctx, 'greeter')
}
}
// 编译时:通过 TypeScript 声明合并,让 ctx.greeter 有类型
declare module 'cordis' {
interface Context {
greeter: GreeterService
}
}
四、核心机制一:Effect 系统------可逆副作用的实现
Effect 系统是 Cordis 的灵魂。它回答了这个问题:"我注册了一个资源,怎么保证它一定会被释放?"
4.1 基本用法
typescript
ctx.effect(() => {
// 1. 执行阶段:立即运行
const conn = createConnection()
const timer = setInterval(heartbeat, 5000)
// 2. 返回 disposer(释放函数)
return () => {
clearInterval(timer)
conn.close()
console.log('cleaned up')
}
})
规则:
execute立即运行- 返回的
disposer被收集到 Fiber 的副作用栈中 - 卸载时,disposers 按注册逆序执行
- 调用 disposer 两次是 no-op(幂等)
4.2 Effect 的多种形态
Cordis 的 Effect 类型非常灵活:
typescript
// 形态 1:同步 disposer
ctx.effect(() => {
const resource = acquire()
return () => resource.release()
})
// 形态 2:异步 disposer(返回 Promise)
ctx.effect(() => {
return async () => {
await gracefulShutdown()
}
})
// 形态 3:Generator(逐个产出 disposer)
ctx.effect(function* () {
const a = acquireA()
yield () => a.release()
const b = acquireB()
yield () => b.release()
})
4.3 内置 API 即 Effect
你很少需要直接写 ctx.effect(),因为 Cordis 的内置 API 已经都是 Effect 了:
| API | 副作用 | 撤销行为 |
|---|---|---|
ctx.on('event', handler) |
注册事件监听 | 移除监听 |
ctx.plugin(child) |
挂载子插件 | 卸载子插件 |
ctx.provide(name, value) |
注册服务 | 注销服务 |
ctx.effect(fn) |
自定义资源 | 执行 disposer |
4.4 源码层面的关键设计
Fiber 内部维护一个 Effect 栈(后进先出):
typescript
// 伪代码:Fiber 内部的 effect 管理
class Fiber {
private effects: EffectMeta[] = []
effect(execute: () => Effect, label?: string): Disposable {
// 1. 立即执行 effect body
const result = execute()
// 2. 解析 result(可能是函数、Promise、Generator)
const disposers = normalizeEffect(result)
// 3. 压入栈
const meta: EffectMeta = { label, children: [] }
this.effects.push(meta)
// 4. 返回一次性 disposer
return () => {
if (alreadyDisposed) return
// 按逆序执行所有 disposers
for (const d of disposers.reverse()) d()
}
}
}
关键工程细节(来自 DeepSeek Harness 的本地增强):
- setup 前注册 wrapper:防止 setup 内部触发卸载时丢失 cleanup
- 同步失败回滚:setup 抛出异常时,已收集的 cleanup 立即执行
- 异步 cleanup 可见性:异步 disposer 在完成前保持 owner 可见
- UNLOADING 状态拒绝新 effect:防止 cleanup 阶段注册新副作用逃逸
五、核心机制二:Fiber 生命周期状态机
Fiber 的状态机不是简单的布尔标志,而是一套完整的并发安全机制。
5.1 状态转换的并发控制
typescript
class Fiber {
// 进行中的过渡(关键!防止竞态)
inertia: Promise<void> | undefined
async transition(targetState: State) {
// 等待当前过渡完成
await this.inertia
// 开始新过渡
this.inertia = this.doTransition(targetState)
await this.inertia
this.inertia = undefined
}
}
为什么需要 inertia?
想象这个场景:插件 A 正在加载(LOADING),此时用户修改配置触发了热重载。如果没有 inertia,加载和卸载可能并发执行,导致:
- 副作用被重复注册
- disposer 在资源未创建时就被调用
- 内存泄漏或空指针异常
inertia 确保:任何时刻只有一个生命周期过渡在进行。
5.2 配置更新的事务性
typescript
async update(config: any, noSave = false) {
// 1. 先运行 internal/update waterfall,允许 hook 拦截
await this.ctx.waterfall('internal/update', this, config)
// 2. 验证新配置
const validated = await this.validate(config)
// 3. 事务性重启:失败则回滚
try {
await this.dispose()
this.config = validated
await this.load()
} catch (e) {
// 回滚到旧配置
await this.load()
throw e
}
}
事务性保证:
- 新配置验证失败 → 不触发任何变更
apply中途失败 → 已注册的副作用自动回滚,旧插件保持运行- 不会出现"半加载"的中间状态
六、核心机制三:依赖注入与服务发现
6.1 inject 声明
插件通过 inject 静态属性声明依赖:
typescript
export const inject = ['llm', 'tools', 'sessions']
export function apply(ctx: Context) {
// 保证可用:Cordis 会等到所有依赖就绪才执行 apply
ctx.llm.chat(...)
ctx.tools.register(...)
}
6.2 依赖追踪的动态性
inject 不是一次性检查,而是持续追踪:
时间线:
t0: [GreeterService] 提供 greeter 服务
↓
t1: [Consumer] inject: ['greeter'] → 进入 ACTIVE
↓
t2: [GreeterService] 被卸载 → greeter 消失
↓
t3: [Consumer] 自动进入 UNLOADING → 清理所有副作用
↓
t4: [NewGreeter] 提供新的 greeter 服务
↓
t5: [Consumer] 自动重新加载,使用新的 greeter
这种设计使得服务替换 成为可能:卸载旧的 shell provider,挂载新的 e2b provider,所有依赖 'shell' 的插件自动重启,无需手动干预。
6.3 底层 API:provide / get / set
typescript
// 提供方:注册服务(返回 disposer)
const dispose = ctx.provide('metrics', new MetricsService())
// 消费方:按名获取(不触发依赖等待)
const metrics = ctx.get('metrics') // 可能为 undefined
// 提供方:更新自己的服务实现(仅限提供方调用)
ctx.set('metrics', newImpl)
七、核心机制四:类型化事件系统
Cordis 的事件系统不是简单的 EventEmitter,而是带类型约束、多种分发语义的通信机制。
7.1 四种分发模式
| 模式 | 是否 await | 分发顺序 | 返回值 | 典型用途 |
|---|---|---|---|---|
emit |
否 | 注册顺序 | 无 | 广播通知 |
waterfall |
否 | 注册顺序 | 有 | 中间件/拦截器 |
parallel |
是 | 并行 | 无 | 并发扇出 |
serial |
是 | 注册顺序 | 有 | 顺序决策 |
7.2 waterfall:最核心的模式
waterfall 是 Cordis 事件系统的精髓,它实现了**环绕中间件(around-middleware)**模式:
typescript
// 派发方
const output = await ctx.waterfall('agent/pre-step', messages, async () => messages)
// 监听方 1:修改请求
ctx.on('agent/pre-step', async (messages, next) => {
messages.push({ role: 'system', content: 'You are a helpful assistant.' })
return next() // 必须调用 next() 委托下游
})
// 监听方 2:策略拦截
ctx.on('agent/pre-step', async (messages, next) => {
if (isBlocked(messages)) {
return { error: 'blocked' } // 不调用 next() = 短路
}
return next()
})
关键规则:
- 观察型监听器必须调用
next() - 策略型监听器可以不调用
next()直接返回,实现短路 - 返回值通过
next()反向传播,形成洋葱模型
7.3 事件即 Effect
所有事件监听都是 Effect:
typescript
const dispose = ctx.on('event-name', handler)
// dispose() 移除监听,插件卸载时自动调用
这意味着事件监听天然享受:
- 自动清理(卸载时移除)
- 生命周期追踪(在
fiber.getEffects()中可见) - 热重载支持(重载时重新注册)
八、核心机制五:配置热更新与事务回滚
8.1 Loader 配置系统
Cordis 通过 @cordisjs/plugin-loader 支持从 YAML/JSON 配置驱动插件树:
yaml
# cordis.yml
- name: './greeter.ts'
- name: './consumer.ts'
config:
greeting: 'Hello'
8.2 懒加载配置解析
DeepSeek Harness 对 Cordis 做了一项重要增强------懒加载配置解析:
传统方式:配置在插件加载前完全解析,如果配置中包含依赖其他服务的动态值(如 ${``{ ctx.port }}),解析时服务可能尚未就绪。
懒加载方式:保留原始配置,等到 inject 声明的依赖全部激活后,再通过 internal/config 解析。这使得配置可以安全地引用运行时服务。
8.3 HMR(热模块替换)
@cordisjs/plugin-hmr 监听文件变更,自动重载受影响插件:
typescript
// hmr 插件的核心逻辑
watch(fileChanges => {
for (const changedFile of fileChanges) {
const affectedPlugins = dependencyGraph.get(changedFile)
for (const plugin of affectedPlugins) {
plugin.fiber.restart() // 事务性重启
}
}
})
关键特性:
- 细粒度依赖分析:只重载真正受影响的插件
- 事务性保护:新代码加载失败时自动回滚到旧版本
- 副作用自动清理:旧插件的 disposers 全部执行后再加载新代码
九、实战:从零写一个 Cordis 插件
让我们把以上概念串联起来,写一个完整的插件。
9.1 提供服务
typescript
// metrics.ts
import { Service, type Context } from 'cordis'
// 类型声明合并
declare module 'cordis' {
interface Context {
metrics: MetricsService
}
}
export class MetricsService extends Service {
private counters = new Map<string, number>()
constructor(ctx: Context) {
super(ctx, 'metrics') // 注册到 ctx.metrics
}
record(event: string, value: number = 1) {
const current = this.counters.get(event) || 0
this.counters.set(event, current + value)
}
getCounter(event: string): number {
return this.counters.get(event) || 0
}
}
export const name = 'metrics'
export function apply(ctx: Context) {
ctx.plugin(MetricsService)
}
9.2 消费服务
typescript
// agent-monitor.ts
import type { Context } from 'cordis'
export const name = 'agent-monitor'
export const inject = ['metrics'] // 声明依赖
export function apply(ctx: Context) {
// 保证可用:metrics 已就绪
// 1. 注册事件监听(自动清理)
ctx.on('agent/request', (req) => {
ctx.metrics.record('request_count')
ctx.metrics.record('token_usage', req.tokens)
})
// 2. 注册自定义副作用
ctx.effect(() => {
const timer = setInterval(() => {
console.log('Total requests:', ctx.metrics.getCounter('request_count'))
}, 60000)
return () => clearInterval(timer)
})
// 3. 注册工具(假设通过某服务注册)
ctx.effect(() => {
const disposeTool = ctx.tools.register('get-metrics', () => {
return {
requests: ctx.metrics.getCounter('request_count'),
tokens: ctx.metrics.getCounter('token_usage')
}
})
return disposeTool // 工具注册的返回值本身就是 disposer
})
}
9.3 配置驱动加载
yaml
# cordis.yml
- name: './metrics.ts'
- name: './agent-monitor.ts'
9.4 运行与验证
bash
node --import tsx ./node_modules/cordis/bin.js
验证行为:
- 启动后,
metrics服务先加载,agent-monitor等待依赖就绪后加载 - 每 60 秒打印请求统计
- 修改
agent-monitor.ts保存 → HMR 自动重载,定时器被清理后重新创建 - 卸载
metrics→agent-monitor自动卸载,所有定时器和监听被清理
十、设计哲学与适用场景
10.1 Cordis 解决了什么问题
| 传统插件系统 | Cordis |
|---|---|
| 注册容易,清理困难 | 注册即 effect,卸载自动逆序清理 |
| 依赖关系隐式,启动顺序手写 | inject 声明,框架自动拓扑排序 |
| 热更新 = 重启进程 | 事务性重载,失败自动回滚 |
| 全局状态污染 | Context 作用域隔离 |
| 事件 = 无类型广播 | 类型化事件,四种语义明确的分发模式 |
10.2 适用场景
Cordis 特别适合以下场景:
- 长期运行的服务:聊天机器人、Agent 运行时、游戏服务器
- 需要频繁热更新的系统:开发环境、A/B 测试、动态策略
- 高度模块化的架构:模型、工具、策略均可替换的 AI 系统
- 多租户/隔离需求:不同上下文需要独立的服务实例
十一、总结
Cordis 用约 2000 行 TypeScript 代码,构建了一套完整的时空可组合性基础设施:
- 时间维度:Effect 系统确保任何副作用都可逆,Fiber 状态机确保生命周期转换的确定性
- 空间维度 :Context 提供作用域隔离,
inject实现依赖声明式组合
它的代码核心可以浓缩为三个对象(Context、Fiber、Service)和四个机制(Effect、inject、Events、HMR)。理解这些,你就理解了 Cordis 的一切。
对于正在构建 Agent 基础设施、聊天机器人框架或任何需要模块化 + 热更新 + 可逆副作用的系统,Cordis 的源码是一份极佳的参考。它不是银弹,但在它瞄准的领域里,它做到了极致。