Cordis 元框架入门
本教程共 32 篇 · 第 6 篇 · 更新于 2026-08-15 · 约 6 分钟阅读
本节目标:看懂 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.tools、ctx.llm、ctx.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/dsh0.1.0-rc.6,底层 Cordis 4.0.0-rc.8。框架 API 会快速变化,动手前先跑dsh --version确认实际版本。
小结
- dsh 是插件拼出来的产品,组织者是底层框架 Cordis。
- 插件有三种形态,
apply(ctx)是入口,ctx是服务容器。 - 事件有五种分发模式;waterfall 是环绕中间件,观察型监听器必须调
next()。 - 一切注册都是可逆 effect,卸载即清理。
- 选 Cordis 的三个理由:理念契合、生态现成、可替换。