Cordis 是 dsh 底下的插件框架(源码在 vendor/cordis/,可直接读;设计论文不必读)。全部骨架就五个概念:
插件(plugin)就是一个"实现了 Service 约定的对象",最常用的函数形式:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-plugin' // ① 名字
export const inject = ['tools'] // ② 依赖(可选)
export function apply(ctx: Context) { // ③ 安装函数
// 框架加载插件时调用 apply,把 ctx 递给你;
// 你要注册的一切都在这个函数里完成。
}
apply 就像新员工入职报到:公司(框架)把工牌和门禁卡(ctx)递给你,你拿着它去领电脑、开邮箱(注册你的能力)。另有类形式(继承 Service,适合提供服务,L16 专讲)和对象形式:
// 对象形式:三件套放进一个对象
export default {
name: 'my-plugin',
inject: ['tools'],
apply(ctx: Context) { /* ... */ },
}
// 类形式:继承 Service,适合提供服务(L16)
export default class MyService extends Service {
static inject = ['tools']
constructor(ctx: Context) { super(ctx, 'myService') }
}
上下文(Context,即 ctx)是所有服务的容器,每个服务认领一个稳定的"插座名":
ctx.tools // 工具注册表 —— 模型能调用哪些工具
ctx.llm // 模型适配 —— 怎么调用大模型
ctx.sessions // 会话 —— 日志与恢复
ctx.agents // 智能体 —— 活的对话主体
ctx 是一排标准插座:任何插件想用电(用某项能力),不关心电从哪个发电厂来,插上就行。关键效果:找服务靠名字,不靠 import 具体实现——所以实现可整体替换(L06 的能力缝正建立在这上面)。
inject 声明"我需要哪些服务",框架据此排加载顺序:
export const inject = ['tools']
export function apply(ctx: Context) {
// 能走到这里,说明 ctx.tools 必然就绪
ctx.tools.register(/* ... */)
}
像装修排期:水电工不用和泥瓦工商量谁先来,物业(框架)看依赖自动排期,谁缺料就等着。
ctx.tools ≈ getBean("tools")——按名取、不 import 实现,故可整体替换;inject ≈ @Autowired——容器读依赖图自动定初始化顺序。分工:插座板=全机共享的服务注册表,插座=服务名(tools/llm/sessions),ctx=每个插件手里的延长线——既能消费也能供电(L16)。时序:配置层 disabled: true 在模块加载之前就把行摘掉(插件根本不加载);inject 管的是幸存插件之间的先后——"去不去"归配置,"先后"归依赖。ctx.get(name):inject 声明必需依赖;"有则用之、无则跳过"的服务用 ctx.get('metrics') 拿(拿不到是 undefined,不卡加载)。有血泪教训(docs/postmortem/0001)。插件之间靠事件说话。Cordis 的事件是用 TypeScript 声明合并(declaration merging)登记的类型化事件,发和收都有类型检查;每个事件有一种派发模式(dispatch mode),是事件公开契约的一部分:
| 模式 | 等待? | 顺序 | 返回值 | 通俗理解 |
|---|---|---|---|---|
emit | 否 | 注册顺序 | 无 | 广播:喊一嗓子"下雨了",各自收衣服不用回复 |
waterfall | 否* | 注册顺序 | 有 | 流水线传纸条:每层可改内容再传下去;不传(不调 next)就是自己拍板 |
parallel | 是 | 同时 | 无 | 群发任务:所有部门同时开工,全部干完才散会 |
serial | 是 | 注册顺序 | 有 | 排队办事:挨个到窗口办,谁办成了后面不用办 |
bail | 否 | 注册顺序 | 有 | 短路检查:挨个问"有问题吗",第一个说"有"的就定案 |
* waterfall 的 next 调用是异步的;"等待"列指派发本身是否 await 所有监听器。
// 发:广播"我准备好了"
ctx.emit('my-plugin/ready', { id: 'worker-1' })
// 听:收到就处理
ctx.on('my-plugin/ready', ({ id }) => {
console.log(`${id} 就绪`)
})
(...args, next),必须调用 next() 才会把请求传给下一层;不调而直接 return 就是"短路"——流水线在你这里定案。想改写就先 const result = await next() 再改。L15 用它写权限门。L04 的"注册即效果"落成代码:每个注册自带"撤销器"(disposer):
export function apply(ctx: Context) {
// 方式一:ctx.effect —— 返回撤销函数
ctx.effect(() => {
const conn = openConnection()
return () => conn.close() // 卸载时自动执行
})
// 方式二:ctx.on —— 注册监听器,本身就是效果,自动回收
ctx.on('tools/result', handler) // 插件卸载,监听自动移除
}
经验法则:若卸载顺序重要,把相关注册放进同一个 effect,让撤销按期望顺序展开。
import type { Context } from '@deepseek-ai/cordis'
export const name = 'tool-logger'
export const inject = [] // 无必需依赖
export function apply(ctx: Context) { // ① 插件对象:apply 形式
ctx.on('tools/result', (exec, result) => { // ⑤ 注册即效果
// ④ 事件:观察工具的最终结果
console.log(`[tool] ${exec.name}`)
const text = result.content
.map(b => b.type === 'text' ? b.text : '')
.join('')
console.log(`[result] ${text.slice(0, 100)}`)
})
}
五行代码用全五个要素——一个真实可用的"工具调用日志"插件。
docs/cordis-tutorial/01-first-plugin.zh.md(官方手把手版,与本课互为印证)。vendor/cordis/src/service.ts,找到 Service 类的定义位置。