首页 / DeepSeek Harness 入门教程 / Cordis 元框架入门

DeepSeek Harness 入门教程

Cordis 元框架入门

本教程共 32 篇 · 第 6 篇 · 更新于 2026-08-15 · 约 6 分钟阅读

Cordis元框架插件上下文事件effect

本节目标:看懂 dsh 的底层框架 Cordis——插件树、共享上下文服务、类型化事件、可逆副作用,并知道 dsh 为什么选它。

dsh 的标语是「Everything is a Plugin(一切皆插件)」。这不是营销口号,是字面描述:模型适配器、工具注册表、会话日志,连 agent loop 本身——全是插件。默认 web profile 会挂载 133 个插件(hello-dsh 教程实测,以你本机插件列表为准)。这个产品,就是自己用插件拼出来的。

把这么多插件组织起来的,是一个叫 Cordis 的插件框架。dsh 没有重复造轮子。它把开源项目 Cordis(cordiverse/cordis)vendor 进来,改名 @deepseek-ai/cordis,固定版本使用。当前版本基线:@deepseek-ai/dsh 0.1.0-rc.6,底层 Cordis 4.0.0-rc.8。

Cordis 的设计思想来自论文《A Programming Paradigm for Spatiotemporal Composability》(时空可组合性编程范式)。名字很学术,核心只有一句话:程序由可挂载、可撤销的插件组合而成。可以想成乐高:每一块都能装上,也能拆下来,拆了不影响别的块。下面把五个核心概念逐个讲清。

插件:三种形态

在 Cordis 里,插件是「实现 Service 的对象」。最常见的形态是函数:

import type { Context } from '@deepseek-ai/cordis'

export const name = 'hello'

export function apply(ctx: Context) {
  console.log('hello from my first plugin')
}

Cordis 加载模块时调用 apply(ctx)ctx 就是共享上下文。除了函数形态,还有两种写法:带 apply 方法的对象,以及 Service 子类(用于对外提供服务,后面章节会细讲)。name 导出项是可选元数据,用于在诊断信息里标识插件。

上下文:服务的容器

ctx 是「服务的容器」。一个服务占据一个稳定的 ctx.<key>,例如 ctx.toolsctx.llmctx.sessions。其他插件按 key 查找服务,而不是导入具体实现。这就像住酒店:你不用知道洗衣房在哪一层,拨「洗衣服务」分机就行。换一家洗衣供应商,你的打电话方式不变。

插件用 inject 字段声明依赖:

export const inject = ['tools']

export function apply(ctx: Context) {
  // 走到这里时,ctx.tools 一定已经就绪
}

声明后,Cordis 会等 ctx.tools 就绪才执行 apply。加载顺序由依赖关系决定,不由配置文件里的先后顺序决定。依赖消失时,消费方插件会跟着卸载,服务恢复后再重新加载。

插件树:没有特权内核

插件可以挂子插件(ctx.plugin(child)),于是整个应用长成一棵树。dsh 架构最独特的一点:不存在需要打补丁的特权内核。想扩展 dsh,就在别的插件旁边挂一个新插件;想移除能力,就卸载对应插件。一切注册都是可逆副作用,卸载时自动回退。

每个已加载的插件实例都拥有一个 fiber(运行时句柄),状态机如下:

PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
                 ↘ FAILED

依赖未就绪时停在 PENDING,apply 抛错进入 FAILED。初学者常在这里栽跟头:插件没有任何输出,多半是卡在 PENDING——它声明的服务没人提供。

类型化事件:插件间通信

服务适合直接调用;事件适合「发出通知,但不知道谁在听」。事件名通过 TypeScript 声明合并注册,监听和触发都有完整类型:

declare module '@deepseek-ai/cordis' {
  interface Events {
    'stats/report'(name: string, count: number): void
  }
}

分发模式是事件契约的一部分,决定监听器能否返回值、能否并行、能否短路:

模式是否等待分发顺序返回值
emit按注册顺序
parallel全部并行
serial按注册顺序第一个非 null/false/undefined 值胜出
bail否(同步)按注册顺序serial 的同步版本
waterfall是(环绕)按注册顺序后一个监听器包住前一个,必须调 next()

waterfall 是实现拦截的关键。监听器收到 (...args, next):调用 next() 委托下游,不调用就是有意短路。官方有一条常设纪律:只负责观察或记录的 waterfall 监听器必须调用 next(),否则会悄无声息地吞掉所有下游行为。

可逆副作用:卸载即清理

插件注册的一切——事件监听、工具、适配器、定时器——都通过 ctx 完成,卸载时自动撤销。你不必手动维护 removeListener。对于框架没管理的资源(网络连接、文件 watcher),用 ctx.effect() 包一层:

ctx.effect(() => {
  const timer = setInterval(() => console.log('tick'), 200)
  return () => clearInterval(timer) // disposer:卸载时执行
})

ctx.effect() 返回的清理函数叫 disposer,在插件卸载时执行。热重载、依赖消失、显式 fiber.dispose() 走同一条清理路径。disposer 按注册顺序的逆序执行,多个异步 disposer 并发运行。若拆除有先后要求,就放进同一个 disposer 里依次等待。

为什么 dsh 选择 Cordis

为什么偏偏是 Cordis?三个理由都藏在 dsh 自己的需求里。

理念对得上。「无特权内核 + 一切皆插件」正是 Cordis 的时空可组合性:空间上,插件挂在哪决定了它影响什么;时间上,任意插件可随时卸载重载。热模块替换(HMR)之所以安全,是因为旧实例的所有 effect 先完整回卷,新代码再加载,不会出现两份注册叠加。

生态现成。 loader(配置加载)、schemastery(配置校验)、HMR 插件都是现成的,dsh 把它们连同 Cordis 一起 vendor 进 @deepseek-ai 作用域,并做了加固:修复 fiber 重入释放漏洞、补全销毁语义文档、loader 的 update() 支持返回 waterfall 结果。

可替换。 产品的每一部分都由配置选择,换模型适配器、换工具、换存储都不需要改内核代码。你在后续章节会看到,ctx.* 服务几乎都能被插件替换。

Tip

想亲眼看看这棵树?在已安装 dsh 的机器上运行 dsh --profile web --dump-config,打印出的就是启动时实际挂载的配置树。本章只是认识框架,完整命令在第 9 章讲解。

Warning

项目处于 Developer Preview 阶段,当前版本基线 @deepseek-ai/dsh 0.1.0-rc.6,底层 Cordis 4.0.0-rc.8。框架 API 会快速变化,动手前先跑 dsh --version 确认实际版本。

小结

  • dsh 是插件拼出来的产品,组织者是底层框架 Cordis。
  • 插件有三种形态,apply(ctx) 是入口,ctx 是服务容器。
  • 事件有五种分发模式;waterfall 是环绕中间件,观察型监听器必须调 next()
  • 一切注册都是可逆 effect,卸载即清理。
  • 选 Cordis 的三个理由:理念契合、生态现成、可替换。