第 2 章 Cordis 插件基础
Cordis 是 DSH 底层的插件框架:每项能力——工具、LLM 适配器乃至 agent loop 本身——都是挂载到共享上下文中的插件。本章讲解编写 DSH 插件必须掌握的核心概念,并给出最小插件骨架。
本章目标
- 理解插件、Context、inject 三个核心概念
- 掌握类型化事件与五种分发模式,理解 waterfall 的
next()与短路 - 理解"注册是可逆副作用"与 fiber / loader 机制
插件是"实现 Service 的对象"
插件是导出 apply 的 TypeScript 模块(也可以是普通函数、带 apply 的对象、或 Service 子类)。框架加载时调用 apply 并传入 ctx,你通过 ctx 注册能力。最小骨架:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin' // 可选的显示元数据,用于诊断信息
export const inject = ['tools'] // 声明依赖:tools 服务就绪后才会调用 apply
export function apply(ctx: Context) {
// 在这里注册能力:事件监听、工具、定时器……
console.log('[hello-plugin] plugin loaded!')
}未公开服务前用函数形态;需要向其他插件提供服务时改用 Service 子类。
Context 是服务的容器
ctx 是服务的容器,插件在这里注册一切贡献。每个服务占据稳定的 ctx.<key> 挂载点——ctx.tools、ctx.llm、ctx.agents 都是服务。插件按 key 查找服务而非导入具体实现,配置可替换提供方而无需改消费方。
inject 声明依赖
inject 列出插件需要的服务;Cordis 让插件保持 PENDING 直到服务就绪,apply 内可放心使用 ctx.tools。加载顺序因此无关紧要——决定启动时机的是依赖而非文件顺序。运行期间服务消失(提供方被卸载或热替换),依赖插件随之卸载,服务恢复后再加载。可选依赖跳过 inject,用 ctx.get('key') 探测。
类型化事件与声明合并
事件让插件无需知道监听者就能通信,DSH 用它处理工具结果、模型请求、审批决定。服务通过 TypeScript 声明合并注册事件名与监听器签名:
declare module '@deepseek-ai/cordis' {
interface Events {
'stats/report'(name: string, count: number): void
}
}声明只提供类型、不生成运行时接线——插件必须另行 emit。消费方用 import type {} from './stats.ts' 引入合并,ctx.emit / ctx.on 便具备完整类型。事件名遵循 namespace/action 约定。
五种分发模式
每个事件都有一种分发模式,只能通过对应方法分发:
| 模式 | 调用 | 是否 await | 有返回值? | 语义 |
|---|---|---|---|---|
| emit | ctx.emit(name, ...args) | 否 | 否 | 同步广播;监听器按注册顺序观察 |
| parallel | await ctx.parallel(name, ...args) | 是 | 否 | 所有监听器并行运行并一同等待 |
| serial | await ctx.serial(name, ...args) | 是 | 是 | 按序运行;第一个非 null/false/undefined 返回值胜出并停止后续 |
| bail | ctx.bail(name, ...args) | 否 | 是 | serial 的同步版本 |
| waterfall | ctx.waterfall(name, ...args, next) | 否 | 是 | 环绕中间件,见下 |
waterfall 是拦截模式。监听器收到 (...args, next):next() 执行下游并把返回值带回本层,可包装后向外返回;不调用 next() 直接返回则短路——最内层默认逻辑不会运行。单决策事件的短路是设计意图:策略监听器有决策权时可不调 next(),只做观察的监听器必须委托。纪律:日志监听器忘记调用 next(),会悄悄吞掉所有下游默认行为。
// 监听器 1:包装下游结果
ctx.on('demo/transform', async (input, next) => {
const downstream = await next() // 委托给下游
return downstream.toUpperCase() // 包装后向外返回
})
// 监听器 2:拥有决策权时短路
ctx.on('demo/transform', async (input, next) => {
if (input.includes('blocked')) return '** blocked **' // 不调 next():否决
return next()
})注册是可逆副作用
通过 ctx 注册的一切(事件监听、工具、定时器)卸载时都会自动清理,无需手动 removeListener 或 clearInterval。ctx.on()、ctx.plugin(child)、ctx.tools.register() 等本身就是 effect;未管理的资源(网络连接、watcher)包进 ctx.effect() 并返回 disposer:
ctx.effect(() => {
const timer = setInterval(() => console.log('heartbeat'), 5000)
return () => clearInterval(timer) // 插件卸载时调用
})配置修改、热重载(HMR)或显式 dispose 都触发卸载,effect 按注册逆序撤销(异步 disposer 并发;需按序拆除时,把步骤放进同一 disposer 依次 await)。
fiber 与 loader
每个已加载的插件实例都拥有一个 fiber,在 PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED 间转换(apply 或配置校验抛异常则进入 FAILED)。PENDING 表示插件已声明但所需服务尚不可用——"为什么我的插件没有输出"最常见的答案。loader 读取 cordis.yml(配置项列表,name 为模块指定符)并并发挂载每一项,与 ctx.plugin(child) 一致。
小结
- 插件 = 带
apply(ctx)的对象;ctx.<key>是稳定服务挂载点;inject以依赖而非顺序决定加载。 - 事件经声明合并获得类型;五种分发模式中,waterfall 的
next()委托与不调用即短路是拦截核心。 - 一切注册可逆:effect 在卸载、HMR、dispose 时自动撤销。
- fiber 状态机与 loader 解释了插件何时启动、为何静默。
下一步
掌握了 Cordis 基础,下一章 编写第一个工具 将定义第一个模型可调用的工具并用 --patch 挂进 Web UI,examples/greet-tool 是该章的完整示例。