Cordis 内核到底在管什么?五要素我带你看源码

Cordis 内核到底在管什么?五要素我带你看源码

上一篇我去魅了 dsh (DeepSeek Harness) 的微内核 (Cordis),但只讲到概念级。 这一篇把上次的「五要素」逐个翻成源码。看完你应该能自己读 vendor/cordis 任何一行。 代码快照于 2026-08-19,仓库 commit 99f6f02f (dsh 0.1.0-rc.7),cordis v4.0.1 后续版本可能变动,请以 clone 后的源码为准。

0. 地图:把 dsh 拆开,cordis 在哪一层

dsh 是个 monorepo。仓库根 package.json 名字是 @deepseek-ai/dsh-root,版本 0.1.0-rc.7 (Developer Preview),workspaces 字段把 vendor/* packages/*/* apps/* 都包进来。cordis 并不在 packages/ 下,而是被 vendor 进来的一颗第三方依赖级微内核:

text 复制代码
@deepseek-ai/dsh-root  v0.1.0-rc.7  (MIT)
├── vendor/cordis      @deepseek-ai/cordis  v4.0.1   ← 本篇源码拆解对象
├── vendor/*           cosmokit 等其他被 vendor 的依赖
└── packages/*         dsh 应用层 (client / runtime / providers ...)

vendor/cordis/src/ 下的 9 个 ts 文件 (我用 wc -l 实测):

文件 行数 角色
index.ts 14 聚合导出
context.ts 146 Context 类与稳定键接口
events.ts 352 事件总线 + 5 种分发
fiber.ts 754 生命周期 + 可逆副作用
logger.ts 270 命名 logger
reflect.ts 418 服务/依赖查找与注入
registry.ts 337 插件 + @Inject 装饰器
service.ts 115 Service 抽象基类
utils.ts 287 内部工具 (DisposableList, symbols, Tracker)
合计 2693

数字会随版本变;我把它当 2026-08-19 这一刻的快照。下文所有的 file:line 都是这个快照。

第一个易错点:仓库名里的 Harness 是个产品,cordis 是它的微内核。上一篇我在图里把 cordis 画成 dsh 的一部分,其实cordis 是个独立的 npm 包 (@deepseek-ai/cordis),只是被 dsh 锁进 vendor/

1. Plugin:能力注册的入口

Cordis 不发明新东西。一个插件就是一个「挂了 ctx 的对象」,挂法有两条路:

类路径 :继承 Service (service.ts:11),构造时自动注册。

ts 复制代码
// service.ts:42-58
constructor(protected ctx: Context, name: string) {
  name ??= this.constructor['provide'] as string
  // ...
  self.ctx = ctx
  self.name = name
  defineProperty(self, symbols.tracker, tracker)
  self.ctx.reflect.provide(name, self, this[symbols.check])
  return self
}

对象路径{ inject?, apply }inject 声明依赖 (registry.ts:19 是 Inject 类型),apply 拿到 ctx 干活 (registry.ts:8-10 isApplicable 检查)。

两种路径最后都汇到 ctx.reflect.provide(name, ...),而 provide 内部是这样:

ts 复制代码
// reflect.ts:277-280
provide(name: string, value?: any, check?: () => boolean) {
  return this.ctx.fiber.effect(() => {
    // ... 写 this.store[isolate]、注册 cleanup
  })
}

这条链就是 Cordis 的关键 :插件注册不是一个赋值操作,而是一个 fiber.effect(...)。意思是你写的 new MyService(ctx, 'tools') 这一行,副作用的撤销已经被预定了。等 fiber 卸载,service 自动摘掉。这就是为什么 dsh 能「干净卸载一个插件」的源头。

2. Context:稳定键的共享仓库

Context 是个 Proxy。context.ts:16-33 是它的公开接口:

ts 复制代码
// context.ts:16-33
export interface Context {
  [symbols.isolate]: Dict<symbol>          // 隔离表:服务名 -> 作用域标签
  [symbols.intercept]: Dict                // 拦截表:服务名 -> per-plugin 配置
  root: this                                // 根 ctx (experimental)
  baseUrl?: string
  events: EventsService                     // 事件总线
  logger: LoggerService                     // 命名 logger
  reflect: ReflectService                   // 反射层 (provide / get / mixin)
  registry: RegistryService                 // 插件注册 (plugin / inject)
}

只有这 6 个键是 Cordis 内置的。 你日常写的 ctx.tools / ctx.llm / ctx.fs / ctx.shell / ctx.sessions不是 Cordis 自带的 ,是 dsh 上层包用 ctx.provide('tools', ...)ctx.provide('llm', ...) 之类的代码注入的。

我特意把这件事标出来:上一篇把 ctx.tools 画在 Cordis 的 ctx 圆里是不严谨的,画法应该分开两圈:

两圈的分界意味着什么? 它意味着 Capability Seam (能力接缝) 这件事的根在 dsh 上层,cordis 只提供了「按名挂载/查找/撤销」三件套。模型后端能换、沙箱能换,是因为 dsh 把每个能力都当成「在某个 fiber 上注册的服务」。

3. Service:可被替换的具体能力

Service 抽象类 (service.ts:11) 是个非常薄的壳:

ts 复制代码
// service.ts:11, 42
export abstract class Service<out T = never> {
  // ...
  constructor(protected ctx: Context, name: string) { /* ... */ }
  // symbols.filter: 在事件分发时决定本服务实例要不要响应
  protected [symbols.filter](ctx: Context) { /* ... */ }
  // symbols.resolveConfig: 合并祖先 ctx 的 intercept config
  [symbols.resolveConfig](base?: T, head?: T): T { /* ... */ }
}

所有重量都委托给 reflect.provide (第 1 节那行)。它有两个常被忽视的钩子:

  • symbols.filter (service.ts:61):决定这个实例在哪个隔离作用域里生效。dsh 的能力切换经常用 isolate 标签做「模型 A vs 模型 B 的两个实现并存」。
  • symbols.resolveConfig (service.ts:86):自动从父 ctx 链上收集这个服务的 intercept 配置,按 root→head 的优先级合并。这就是首篇讲的 Trajectory 不变量的物理基础:同一服务在不同的父 ctx 下拿到的 config 不同,行为可被隔离审计。

Service 类的 Symbol.hasInstance (service.ts:104) 还做了一件贴心的事:判断 instance instanceof MyService 时会沿原型链查,而不是只看 instance.constructor,这样 Service 子类的继承也兼容。

4. Typed Events:5 种分发策略

ts 复制代码
// events.ts:32
export type DispatchMode = 'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall'

bailserial 区别在于:serial await 串行等到首个 bail 值;bail 同步派发到首个 bail 值就立刻 return。bailserial 那个长链路里省掉了所有 awaitbail 的判定在 events.ts:13:

ts 复制代码
// events.ts:13-15
export function isBailed(value: any) {
  return value !== null && value !== false && value !== undefined
}

也就是「返回值是 null / false / undefined 都视为没命中,否则命中」。这个判定是其它几种分发共享的「短路」信号。

5 种分发在 events.ts:165 处的 dispatch() 汇聚:

ts 复制代码
// events.ts:183, 194, 204, 217, 234
// parallel (L183)  Promise.allSettled, 全部跑完不报错
// emit    (L194)  同步派发, 忽略返回值
// serial  (L204)  await 串行, 首个 bail 值就停
// bail    (L217)  同步派发, 首个 bail 值就停
// waterfall(L234) 把监听器压栈成 next 续体, 不调 next 即 veto

waterfall 是最特殊的一种。每个监听器收到 (args, next)next 是「下一个监听器」的续体;监听器不调 next() 就等于否决了整条链。这跟 Express/Koa 的中间件一样,但 cordis 的 waterfall 不依赖 Promise 链。它是 plain function 接力的纯同步机制。

on 注册路径简单但关键:

ts 复制代码
// events.ts:288-309 (节选)
on(name, listener) {
  this.assertActive()
  // ... 权限过滤
  return this.register(name, listener.bind(this.ctx))
}

每个 on 都会 assertActive()fiber 不在 ACTIVE 状态时,事件注册直接抛错

5. Effects:可逆副作用

Cordis 真正的杀招是 fiber.effect。我在 0 节已经剧透了它一半的代码:

ts 复制代码
// fiber.ts:415-418
effect(execute: () => SyncEffect, label?: string): Disposable<Promise<void>>
effect(execute: () => Effect, label?: string): AsyncDisposable<Promise<void>>
effect(execute: () => Effect, label = 'anonymous'): any {
  this.assertActive()
  if (this.state === FiberState.UNLOADING) {
    throw new CordisError('INACTIVE_EFFECT')
  }
  // ...
}

构造时新建一个空 disposables[]body 执行期间,任何内部嵌套的 ctx.fiber.effect(child) 都会通过 runner.collect(dispose) 把子 effect 的 disposer 推进这个数组。父吃子,子吃孙,一棵树就这样在 effect 数组里套起来。

卸载时:

ts 复制代码
// fiber.ts:675-686
private async _unload() {
  await Promise.all(this._disposables.clear().map(async (dispose) => {
    try {
      await composeError(async (info) => {
        await Promise.resolve()
        info.error = new Error()
        await runDisposable(dispose)
      }, this._runner.getOuterStack)
    } catch (reason) {
      this.ctx.logger.error(reason)
    }
  }))
  this.store = undefined
  // ...
}

注意三件事:

  1. _unloadthis._disposables.clear()外层 disposers 一次性取走 (顺序无所谓,因为接下来每个 effect 自己再逆序);
  2. 每个 effect 内部的 dispose() (L427-442) 用 disposables.splice(0).reverse()LIFO,最后登记的先撤销
  3. 任何 disposer 抛错都被 this.ctx.logger.error 吞掉,不会影响兄弟 disposer 跑完

把所有东西串起来:reflect.provide (reflect.ts:278) + events.register (events.ts:256) + Service 构造 (service.ts:57) + ctx.inject 解析 (registry.ts:300) + ctx.on 注册 (events.ts:288) 全部走 ctx.fiber.effect(...)。所以卸载 fiber = 一键撤销整条插件链,没有手动 cleanup。

6. Fiber 6 态 = 插件生命周期

一个 dsh 插件的「在世」就是它所挂载的 fiber 的生命周期。FiberState 是个 const enum,6 个状态 (fiber.ts:147-154):

ts 复制代码
// fiber.ts:147-154
const enum FiberState {
  PENDING,    // 已创建, 等依赖就绪
  LOADING,    // 依赖已就绪, body 在跑
  ACTIVE,     // 跑完, 监听中
  FAILED,     // 初始化失败
  DISPOSED,   // 已销毁
  UNLOADING,  // 正在 _unload
}

assertActive() (events.ts 内部、effect 入口、provide 入口到处都有) 是状态机的硬门:LOADING 期间不能挂新 effect (fiber.ts:420-422 直接 throw INACTIVE_EFFECT)。这逼着所有副作用都得在「明确 ACTIVE 之后」做,避免在初始化中途被外部事件触发脏写。

update() (fiber.ts:736) 走 internal/update 这个 waterfall 事件 (L748),允许在已 ACTIVE 的 fiber 上做「热更新」:重新跑 body 但保留 disposers,再合并新收集到的 disposers。

7. 决策产出:这套设计给你什么约束和好处

读源码后我修正了上一篇的若干结论。这五条全部有 file:line 当后盾

  1. 稳定键是硬约束Context 接口 (context.ts:16-33) 只暴露 6 个 Cordis 内置键。你想用 ctx.tools,必须先有人 provide('tools', ...),否则 ctx 代理会返回 undefinedctx.tools?.foo 会在所有用了它的插件里埋雷。(依据:context.ts:16-33 + reflect.ts:277)

  2. 每个副作用都必须经 fiber.effect 登记 。如果你的 Service 子类在构造时开了个 setInterval、开了个 fs.watch、挂了个全局事件监听器而没收进 effect 数组,fiber 卸载时它们会泄漏 。dsh 上层包自己就是这么写 Service 的。(依据:service.ts:57 + reflect.ts:278)

  3. 同一接口多实例靠 isolate 标签隔离[symbols.isolate] (context.ts:18) + Service.[symbols.filter] (service.ts:61) 一起决定哪个实例响应哪个 ctx 的事件。这是 dsh 跑「模型 A vs 模型 B 并存对比」的物理基础。(依据:context.ts:18, service.ts:61)

  4. 5 种分发不要瞎选emit 适合广播;parallel 适合互不依赖;serial 适合有顺序依赖;bail 是 serial 的同步快路径;waterfall 是「不调 next 就否决」的拦截器语义。上一篇我画成 4 种是错的(依据:events.ts:32, 183, 194, 204, 217, 234)

  5. Trajectory 不变量靠 Service 的 [symbols.resolveConfig] 物理保证 。每个 Service 实例拿到的 config 都是「root 到当前 ctx 的整条 intercept 链」按 root→head 优先级合并 (service.ts:86-102),不同 ctx 下同一服务的 config 必然可追溯、可审计。(依据:service.ts:86-102)

这些约束不轻松,但好处也明确:插件可独立测试 (ctx.isolate() 出子 ctx)、可安全卸载 (ctx.dispose() 整链逆序)、可做消融 (临时 ctx.intercept 屏蔽某服务)。

收尾

下一篇我打算用这篇读到的代码作为底座:写一个真实的最小插件,跑过 dsh dev,再把它拆掉。如果你想看哪一部分被拆开讲:ctx.inject 的依赖解析、waterfall 拦截器的实战写法、还是 Service 子类在 dsh 里的真实形状。评论区留言,分享你的问题和心得吧。


相关推荐
Jackson~Y1 小时前
豆包在抖音场景下的智能创作效果实测
人工智能
iNeuOS工业互联网1 小时前
iNeuOS_Vision_视觉分析,增加SAM3模型对实例分割和目标检测AI自动标注图片样本功能
人工智能·算法·目标检测
Dovis(誓平步青云)1 小时前
DevEco Studio 6.1.1 Windows 安装实录:从下载校验到首次启动
android·开发语言·数据库·人工智能·windows·harmonyos
悟天特斯1 小时前
智慧楼宇自控系统重构:从传统BA到AI自适应控制的架构演进
人工智能·物联网·重构·架构
跨境小彭1 小时前
店群运营实操复盘:批量活动申报自动化优化方案
大数据·运维·人工智能·自动化·跨境电商·temu·temu电商运营
NPE~1 小时前
[图挖掘]GCN模型训练——从0到1
人工智能·ai·数据挖掘·教程·风控·图挖掘
产品设计大观1 小时前
实测墨刀AI生成光伏储能管理后台,快速交付高保真界面和React源码
人工智能·react.js·墨刀
Eric.462 小时前
Stable Diffusion+ComfyUI+OpenClaw AI漫剧量产提示词工程:结构化Prompt、负面词脱敏、权重锁定防画面崩坏全方案
大数据·人工智能·stable diffusion·prompt·comfyui·ai漫剧
u0103055272 小时前
鸿蒙手机如何将网站转为带百度搜索的APP
人工智能