Skip to content

源码拆解 02 · Cordis 内核:Context 代理与 Fiber

本篇拆解 vendored Cordis 的核心机制。源码位于 vendor/cordis/src/(约 2700 行),我们只读四个文件:context.tsevents.tsfiber.tsreflect.ts。读完你会明白第 2 章的五个概念在代码里到底是什么。

2.1 Context:一个被代理的对象

context.ts 里最重要的三行(第 74–75 行):

ts
constructor() {
  // …初始化 isolate/intercept 映射…
  const self = new Proxy<this>(this, ReflectService.handler)
  this.root = self
  // …
  return self
}

Context 的运行时形态就是一个 Proxy。它只做一件事:把属性读取转交给 ReflectService.handlerreflect.ts 第 135 行):

ts
static handler: ProxyHandler<Context> = {
  get(target, prop, ctx) {
    if (typeof prop === 'string' && !(prop in target)) {
      return getTraceable(ctx, Reflect.get(target, prop, ctx))
    }
    return getTraceable(ctx, Reflect.get(target, prop, ctx))
  },
}

翻译成白话:读 ctx.llm 时,如果 ctx 上还没有这个属性,就按名字去服务注册表里找。这就是"其他插件通过 key 查找服务,而非 import 实现"的代码级实现——ctx.llm 不是一个早就在那儿的字段,而是"此刻恰好有人提供 llm 服务"的解析结果。

三个派生操作(context.ts 第 99–145 行)也都只是"造一个新的轻量对象":

ts
extend(meta = {}) {
  // Object.create:原型继承父上下文的一切,meta 成为自有属性遮蔽继承值
  const self = Object.create(getTraceable(this, this))
  for (const prop of Reflect.ownKeys(meta)) { /* 拷贝 meta */ }
  return self
}

isolate(name: string, label?: symbol) {
  const shadow = Object.create(this[symbols.isolate])   // 原型链拷贝隔离表
  shadow[name] = label ?? Symbol(name)
  return this.extend({ [symbols.isolate]: shadow })
}

intercept(name: string, config: any) {
  const intercept = Object.create(this[symbols.intercept])
  intercept[name] = config
  return this.extend({ [symbols.intercept]: intercept })
}

值得注意的实现选择:子上下文用原型链(Object.create)而不是深拷贝。派生一个 agent 的作用域上下文,开销是一个对象分配——这也解释了为什么 dsh 敢给每个 agent、甚至每个子代理建独立上下文。

2.2 事件总线:五模式的真实代码

events.ts 的分发实现短得惊人,值得整段读:

ts
emit(...args) {
  this.dispatch('emit', args).map(cb => cb(...args))       // 同步广播,忽略返回值
}

async serial(...args) {
  for (const cb of this.dispatch('serial', args)) {        // 顺序执行
    const result = await cb(...args)
    if (isBailed(result)) return result                    // 非 null/false/undefined 即"拍板"
  }
}

waterfall(...args) {
  const cbs = this.dispatch('waterfall', args)
  const inner = args.pop()                                 // 最后一个参数:内置行为
  const next = () => { const cb = cbs.shift() ?? inner; return cb(...args) }
  args.push(next)
  return next()                                            // 从最外层开始调用
}

waterfall 的实现就是教科书式的中间件链:next() 每次从队列头部弹一个监听器调用,队列空了就落到 inner(内置行为)。不调 next() = 链条在这里断掉 = 否决。

再看监听器注册(第 288–301 行),注意它怎么落实"注册即 effect":

ts
on(name, listener, options) {
  this.ctx.fiber.assertActive()                    // 已卸载的 fiber 不能注册
  listener = this.ctx.reflect.bind(listener)       // 绑定反射追踪
  const result = this.bail(this.ctx, 'internal/listener', name, listener, options)
  if (result) return result                        // 框架内部事件有特殊拦截
  const hooks = this._hooks[name] ||= []
  const label = `ctx.on(${JSON.stringify(name)})`
  return this.register(label, hooks, listener, options)
}
// register() 内部:
//   this.ctx.fiber.effect(() => { hooks.push(record); return () => unregister(...) }, label)

监听器被作为 fiber effect 登记:插入钩子表的同时返回一个"从钩子表移除"的 disposer。fiber 卸载时按逆序执行所有 disposer——第 2 章说的"自动移除"就是这么来的。label 还会出现在 fiber 诊断树(getEffects())里,热重载出问题时能看到"到底是谁没拆干净"。

2.3 Fiber:effect 的账本

fiber.ts 是生命周期的核心。每个插件实例对应一个 fiber,状态机(PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED)驱动着"等依赖、执行、服务、拆解"。

effect 的实现(第 415 行起,节选):

ts
effect(execute: () => Effect, label = 'anonymous') {
  // …执行 execute(),收集它返回的 disposer(可同步/异步/生成器)
  const wrapper = () => {
    // 幂等:重复调用无副作用
    this._disposables.delete(dispose)
    return task ? disposeAfter(task) : dispose()
  }
  this._disposables.push(wrapper)   // 按注册顺序入账
  return wrapper
}

// 卸载时(第 676 行附近):
await Promise.all(this._disposables.clear().map(async (dispose) => { /* 执行 */ }))

逆序来自 DisposableList 的清空顺序(后注册的先拆)。所以"先开连接、再挂文件锁"的插件,卸载时文件锁先放、连接后关——清理顺序天然正确。

生成器 effect 值得一提:如果 effect 体是 async generator,每 yield 一个 disposer 就立刻入账。这让"边注册边追加清理"的写法成为可能(dsh 的 agent factory 就用复合 effect 保证"注册 agent → 宣布创建 → 逆序回收"的原子生命周期)。

2.4 服务解析:provide / get

reflect.ts 第 277 行附近是 provide 的要点:

ts
provide(name: string, value?: any, check?: () => boolean) {
  // …登记服务实现…
  // 关键:调用方 fiber 被 effect 挂接 —— 提供者卸载,服务自动消失
  // ctx.provide(name, impl) 的返回 disposer 同样幂等
}

读服务则走 ReflectService.get(name, strict):从当前上下文的作用域链(含 isolate 表)里找实现。第 2 章说的"依赖变化触发级联重载"就发生在这里的上下游:fiber 的 inject 声明让它在服务出现时被唤醒(_refresh),服务消失时被卸载。

2.5 一个完整的读代码练习:追一次 ctx.on 调用

把本节串起来,追一次最普通的调用:

一条链上验证了:上下文代理(2.1)、服务解析(2.4)、事件注册(2.2)、effect 记账(2.3)。这就是 Cordis 的全部魔法——没有运行时黑盒,每一环都是普通函数调用。

2.6 为什么 vendor:读源码前值得知道的事

dsh 没有把 Cordis 当普通 npm 依赖,而是 vendor(内嵌源码)进仓库(vendor/cordis/),并定期与上游 cordiverse/cordis 同步(见 vendor/README.md)。原因在官方文档里说得很直白:harness 插件作者依赖的是这套精确语义(waterfall 的短路约定、effect 的逆序清理),vendor 让 dsh 能锁定并深度定制这些语义,而不被上游发版节奏带着走。代价是仓库多了一份代码,收益是"插件框架"这个地基完全可控。

2.7 本章小结

Cordis 内核的四个事实:Context 是 Proxy,服务是运行时解析的;事件分发的五种模式实现都是几行代码;fiber 是 effect 的账本,卸载按逆序结算;provide/get 构成服务的作用域解析。 下一篇开始拆 dsh 自己的核心包。

小练习:手写一个 waterfall

不看源码,用 10 行以内的 TypeScript 实现一个 waterfall:输入监听器数组和最终 next,返回组合后的函数;监听器可以调 next() 委托,也可以直接返回短路。写完后与 events.ts 第 234–243 行对照。这个练习做完,你对"中间件"的理解就落地了。

基于 DeepSeek Harness(开发者预览版 0.1.0-rc.6)与 Cordis 撰写