本教程基于
@deepseek-ai/cordis@4.x,采用渐进式(由简到繁)的方式,最终构建出一个完整支持动态插件管理的应用程序。教程中所有代码均经过实际运行验证。运行环境:Bun(
bun run xxx.ts)或任何支持 TypeScript 直译的环境。
目录
- 第1步:最小应用 ------ 创建 Context 与事件系统初体验
- 第2步:事件系统进阶 ------ once / parallel / serial / bail / waterfall
- 第3步:服务(Service)------ 在 Context 上挂载命名 API
- 第4步:插件(Plugin)------ 函数式 / 对象式 / 类插件三种形态
- 第5步:依赖注入 ------ inject 声明、ctx.inject() 与 @Inject 装饰器
- 第6步:生命周期 ------ Fiber、清理函数与 ctx.effect
- 第7步:配置系统 ------ Config 校验(zod)与热重载
- 第8步:日志服务 ------ 内置 logger 与 exporter 扩展
- 第9步:上下文作用域 ------ extend 与 isolate 隔离域
- 第10步:完整应用 ------ 动态插件管理的计算器应用
第1步:最小应用 ------ 创建 Context 与事件系统初体验
Cordis 的一切都从 Context(上下文)开始。Context 是一个代理对象,充当整个应用的依赖容器 与事件总线 。直接 new 即可创建,内置的日志、事件、插件注册等服务会随构造自动安装。
ts
// step1.ts ------ 最小 Cordis 应用
import * as Cordis from '@deepseek-ai/cordis'
// 创建根上下文(内置服务随构造安装)
const ctx = new Cordis.Context()
// 声明自定义事件的类型(获得完备的类型提示)
declare module '@deepseek-ai/cordis' {
interface Events {
'app/ready'(): void
'user/login'(name: string): void
}
}
// 注册事件监听器
ctx.on('app/ready', () => {
console.log('[应用] 已就绪!')
})
ctx.on('user/login', (name) => {
console.log(`[应用] 用户 ${name} 登录了`)
})
// 广播事件
ctx.emit('app/ready')
ctx.emit('user/login', '张三')
运行输出:
css
[应用] 已就绪!
[应用] 用户 张三 登录了
要点:
Context可直接new,无需工厂函数。- 通过
declare module向Events接口做声明合并,事件名与回调参数都有类型约束。 ctx.on返回一个注销函数 ,调用即可取消监听:const off = ctx.on(...); off()。
第2步:事件系统进阶 ------ once / parallel / serial / bail / waterfall
Cordis 的事件总线支持 5 种分发策略,覆盖从简单通知到管道加工的全部场景。
ts
// step2.ts ------ 五种事件分发策略
import * as Cordis from '@deepseek-ai/cordis'
const ctx = new Cordis.Context()
declare module '@deepseek-ai/cordis' {
interface Events {
'app/tick'(): void
// bail 场景:返回是否放行;undefined 表示"不表态"(继续询问下一个监听器)
'app/permission'(): boolean | undefined
'app/transform'(input: string, next: (s: string) => string): string // waterfall 场景
}
}
// ========== 1. once:只触发一次 ==========
let count = 0
ctx.once('app/tick', () => { count++; console.log('[once] 触发,次数:', count) })
ctx.emit('app/tick') // 触发
ctx.emit('app/tick') // 不再触发
console.log('[once] 最终计数:', count) // 1
// ========== 2. parallel:并发等待所有异步监听器 ==========
ctx.on('app/tick', async () => {
await new Promise(r => setTimeout(r, 30))
console.log('[parallel] 慢监听器完成')
})
ctx.on('app/tick', async () => {
console.log('[parallel] 快监听器完成')
})
await ctx.parallel('app/tick') // 等全部监听器跑完再继续
console.log('[parallel] 全部完成')
// ========== 3. bail:依次调用,遇到首个"有效返回值"即停止 ==========
// 有效返回值 = 非 null / 非 false / 非 undefined(可用 Cordis.isBailed 判断)
ctx.on('app/permission', () => { console.log('[bail] 监听器1:不表态'); return undefined })
ctx.on('app/permission', () => { console.log('[bail] 监听器2:放行'); return true })
ctx.on('app/permission', () => { console.log('[bail] 监听器3(不会被执行)'); return false })
const allowed = ctx.bail('app/permission')
console.log('[bail] 最终结果:', allowed) // true(监听器2给出后立即停止)
// ========== 4. waterfall:链式管道,监听器像中间件一样层层包装 ==========
// next 由框架注入,调用 next(input) 即调用下一层;最后一个参数是兜底行为
ctx.on('app/transform', (input, next) => `【A】${next(input)}【A】`) // 外层
ctx.on('app/transform', (input, next) => next(input).toUpperCase()) // 内层
const result = ctx.waterfall('app/transform', 'hello', (s) => s)
console.log('[waterfall] 结果:', result) // 【A】HELLO【A】
运行输出:
less
[once] 触发,次数: 1
[once] 最终计数: 1
[parallel] 快监听器完成
[parallel] 慢监听器完成
[parallel] 全部完成
[bail] 监听器1:不表态
[bail] 监听器2:放行
[bail] 最终结果: true
[waterfall] 结果: 【A】HELLO【A】
要点:
| 策略 | 语义 | 典型用途 |
|---|---|---|
emit |
同步广播,忽略返回值 | 简单通知 |
parallel |
并发执行并 await 全部 | 异步副作用收集 |
serial |
按序 await,可 bail | 有顺序依赖的处理链 |
bail |
同步,首个有效返回值即停止 | 权限拦截、协商取值 |
waterfall |
中间件式层层包装 | 请求/响应加工管道 |
注意: bail 中 false/undefined/null 都视为"未表态"(继续询问下一个监听器);waterfall 中不调用 next() 即否决整条链的后续环节。
第3步:服务(Service)------ 在 Context 上挂载命名 API
服务是 Cordis 的核心抽象:一个带名字的对象,注册后即可通过 ctx.服务名 访问。继承 Cordis.Service 是定义服务的标准方式------构造即注册,随所属 fiber 的卸载自动注销。
ts
// step3.ts ------ 服务定义与注册
import * as Cordis from '@deepseek-ai/cordis'
export class CalculatorService extends Cordis.Service {
// 可选方法占位:后续将由插件动态挂载/卸载(第10步会用到)
pow?: (base: number, exponent: number) => number
constructor(ctx: Cordis.Context) {
// 第二个参数即服务名------构造完成的同时完成注册
super(ctx, 'calculator')
}
add(a: number, b: number): number { return a + b }
multiply(a: number, b: number): number { return a * b }
}
// 声明合并:让 Context 获得类型安全的 calculator 属性(主流用法,见第10步)
declare module '@deepseek-ai/cordis' {
interface Context {
calculator: CalculatorService
}
}
const ctx = new Cordis.Context()
// 构造即注册,ctx.calculator 立即可用
new CalculatorService(ctx)
console.log('[main] 1 + 2 =', ctx.calculator.add(1, 2))
console.log('[main] 3 * 4 =', ctx.calculator.multiply(3, 4))
// 同一隔离域内重复注册会被框架直接抛错拦截(无需手工防御)
try {
new CalculatorService(ctx)
} catch (e) {
console.log('[main] 重复注册被拦截:', (e as Error).message)
}
// 也可以不用类,直接用 ctx.provide 注册纯对象服务
declare module '@deepseek-ai/cordis' {
interface Context { clock: { now(): string } }
}
const dispose = ctx.provide('clock', {
now: () => new Date().toISOString(),
})
console.log('[main] 当前时间:', ctx.clock.now())
dispose() // 注销服务
console.log('[main] 注销后 ctx.clock =', (ctx as any).clock)
运行输出(时间为示例):
ini
[main] 1 + 2 = 3
[main] 3 * 4 = 12
[main] 重复注册被拦截: service "calculator" has been registered at <root>
[main] 当前时间: 2026-08-17T10:00:00.000Z
[main] 注销后 ctx.clock = undefined
要点:
super(ctx, '服务名')完成构造即注册;服务随所属 fiber 卸载自动注销(fiber 详见第6步)。- 插件上下文中读取服务必须先
inject声明 (第5步),否则抛cannot get property "xxx" without inject。根上下文直接读取没有限制。 ctx.provide(name, value)可注册纯对象服务,返回注销函数;同一隔离域内同名服务重复注册会抛错。ctx.get('服务名')可绕过 inject 限制做只读探测(返回undefined表示未提供)。
第4步:插件(Plugin)------ 函数式 / 对象式 / 类插件三种形态
插件 = 可独立加载/卸载的功能单元 。Cordis 支持三种插件形态,加载统一走 ctx.plugin()。
ts
// step4.ts ------ 三种插件形态
import * as Cordis from '@deepseek-ai/cordis'
const ctx = new Cordis.Context()
declare module '@deepseek-ai/cordis' {
interface Events { 'app/hello'(from: string): void }
}
ctx.on('app/hello', (from) => console.log(`[事件] 收到来自 ${from} 的问候`))
// ========== 形态1:函数式插件 ==========
const functionPlugin = (ctx: Cordis.Context) => {
console.log('[函数插件] 加载')
ctx.emit('app/hello', '函数插件')
return () => console.log('[函数插件] 清理') // 返回清理函数(可选)
}
// ========== 形态2:对象式插件 { name, apply } ==========
const objectPlugin = {
name: 'object-plugin', // 展示名(诊断信息、日志名会用到)
apply(ctx: Cordis.Context) {
console.log('[对象插件] 加载')
ctx.emit('app/hello', '对象插件')
return () => console.log('[对象插件] 清理')
}
}
// ========== 形态3:类插件 ==========
class ClassPlugin {
static name = 'class-plugin'
constructor(ctx: Cordis.Context) {
console.log('[类插件] 构造(相当于 apply)')
ctx.emit('app/hello', '类插件')
}
// 构造后钩子:其返回值会被收集为清理函数(类插件的清理方式)
[Cordis.Service.init]() {
console.log('[类插件] init 钩子')
return () => console.log('[类插件] 清理')
}
}
async function main() {
// ctx.plugin() 返回 Fiber(Promise-like):await 等待加载完成
const f1 = ctx.plugin(functionPlugin)
await f1
const f2 = ctx.plugin(objectPlugin)
await f2
const f3 = ctx.plugin(ClassPlugin)
await f3
// 卸载:dispose() 会自动执行各插件的清理函数
console.log('--- 开始卸载 ---')
await f1.dispose()
await f2.dispose()
await f3.dispose()
console.log('--- 全部卸载完成 ---')
}
main()
运行输出:
css
[函数插件] 加载
[事件] 收到来自 函数插件 的问候
[对象插件] 加载
[事件] 收到来自 对象插件 的问候
[类插件] 构造(相当于 apply)
[事件] 收到来自 类插件 的问候
[类插件] init 钩子
--- 开始卸载 ---
[函数插件] 清理
[对象插件] 清理
[类插件] 清理
--- 全部卸载完成 ---
要点:
ctx.plugin()返回 Fiber (插件运行时实例),不是清理函数。await fiber等待加载完成;await fiber.dispose()卸载。- 清理函数的写法:函数式/对象式插件
apply的返回值 ;类插件通过[Cordis.Service.init]()钩子的返回值 (实例上的dispose()方法不会被自动调用)。 - Fiber 卸载后可以重新安装 (再次
ctx.plugin()),这是热更新与插件市场的基础。 apply/构造函数里用ctx.on注册的监听器,会随 fiber 卸载自动注销------无需手动管理。
第5步:依赖注入 ------ inject 声明、ctx.inject() 与 @Inject 装饰器
插件之间通过服务协作时,必须声明依赖。声明后 Cordis 保证:依赖的服务全部就绪时插件才加载;依赖消失时插件自动卸载、恢复时自动重载。
ts
// step5.ts ------ 三种依赖注入方式
import * as Cordis from '@deepseek-ai/cordis'
const ctx = new Cordis.Context()
declare module '@deepseek-ai/cordis' {
interface Context { greeter: Greeter }
}
class Greeter extends Cordis.Service {
constructor(ctx: Cordis.Context) { super(ctx, 'greeter') }
greet(name: string) { return `你好, ${name}!` }
}
// ========== 方式1:插件级 inject 声明(对象式/函数式/类式通用) ==========
const consumerPlugin = {
name: 'consumer',
inject: ['greeter'], // 声明依赖的服务名数组
apply(ctx: Cordis.Context) {
// 走到这里时 greeter 必然就绪------框架保证
console.log('[consumer] greeter 可用:', ctx.greeter.greet('插件级注入'))
return () => console.log('[consumer] 卸载')
}
}
async function main() {
// 先安装消费者------greeter 尚未提供,插件处于 PENDING 状态
const fiber = ctx.plugin(consumerPlugin)
await new Promise(r => setTimeout(r, 20)) // 给调度器一点时间
console.log('[main] 此时插件仍在等待依赖(无 consumer 输出)')
// 依赖出现 -> 插件自动加载!
new Greeter(ctx)
await new Promise(r => setTimeout(r, 20))
console.log('--- 依赖就绪,插件已自动加载 ---')
// ========== 方式2:ctx.inject() 快捷方式(轻量回调,无需定义插件) ==========
// 返回值是 Fiber:await 等待执行,.dispose() 卸载回调
const inj = ctx.inject(['greeter'], (ctx) => {
console.log('[inject] 回调执行:', ctx.greeter.greet('快捷方式'))
return () => console.log('[inject] 回调卸载')
})
await new Promise(r => setTimeout(r, 20))
await inj.dispose()
// ========== 方式3:@Inject 装饰器(类插件的方法级延迟调用) ==========
class LatePlugin {
static inject = ['greeter']
// 方法级 @Inject 依赖 this.ctx------类插件必须把 ctx 存为实例属性
constructor(public ctx: Cordis.Context) {
console.log('[LatePlugin] 构造(greeter 可能尚未就绪)')
}
@Cordis.Inject('greeter') // 此方法延迟到 greeter 就绪后才执行
start() {
console.log('[LatePlugin] 就绪后执行:', this.ctx.greeter.greet('装饰器'))
}
}
await ctx.plugin(LatePlugin)
await new Promise(r => setTimeout(r, 20))
}
main()
运行输出:
csharp
[main] 此时插件仍在等待依赖(无 consumer 输出)
[consumer] greeter 可用: 你好, 插件级注入!
--- 依赖就绪,插件已自动加载 ---
[inject] 回调执行: 你好, 快捷方式!
[inject] 回调卸载
[LatePlugin] 构造(greeter 可能尚未就绪)
[LatePlugin] 就绪后执行: 你好, 装饰器!
要点:
- 未声明 inject 就读服务会抛错 :
cannot get property "greeter" without inject(插件上下文中)。这是框架的刻意设计------强制显式声明依赖关系。 - inject 是响应式的:依赖消失时依赖方自动卸载,依赖恢复时自动重载(fiber 状态机在 PENDING/LOADING/ACTIVE 间流转)。
- 三种方式按场景选择:完整功能用插件级
inject;一段小逻辑用ctx.inject();类插件中推迟某方法执行用@Inject装饰器。 - 多依赖写法:
inject: ['calculator', 'history']------全部就绪才加载。
第6步:生命周期 ------ Fiber、清理函数与 ctx.effect
每个 ctx.plugin() 调用产生一个 Fiber(插件运行时实例),它有完整的状态机,并托管插件内一切"需要清理的资源"。
markdown
PENDING(等待依赖)→ LOADING(执行 apply)→ ACTIVE(运行中)
↓
UNLOADING(执行清理)→ DISPOSED(已移除,可重新安装)
ts
// step6.ts ------ Fiber 生命周期与资源管理
import * as Cordis from '@deepseek-ai/cordis'
const ctx = new Cordis.Context()
declare module '@deepseek-ai/cordis' {
interface Events { 'app/status'(text: string): void }
}
// ========== 1. 插件的清理函数:卸载时自动执行 ==========
const workerPlugin = {
name: 'worker',
apply(ctx: Cordis.Context) {
// ========== 2. ctx.effect:托管任意资源(定时器、连接、句柄......) ==========
// 写法一:直接在 effect 内创建资源并返回清理函数
const disposeTimer = ctx.effect(() => {
const timer = setInterval(() => console.log('[worker] 心跳'), 50)
return () => { clearInterval(timer); console.log('[worker] 定时器已清理') }
}, '心跳定时器')
// effect 返回的 disposer 也可以单独调用(提前释放)
// await disposeTimer() ------ 此处不调用,交给 fiber 卸载时统一清理
// 事件监听同样是 effect 托管的资源:fiber 卸载自动注销
ctx.on('app/status', (text) => console.log('[worker] 状态:', text))
return () => console.log('[worker] 插件级清理函数执行')
}
}
async function main() {
const fiber = ctx.plugin(workerPlugin)
// await:等待加载完成(依赖就绪 + apply 执行完)
// fiber.state: 0=PENDING 1=LOADING 2=ACTIVE 3=FAILED 4=DISPOSED 5=UNLOADING
await fiber
console.log('[main] worker 已加载,状态:', fiber.state) // 2 (ACTIVE)
ctx.emit('app/status', '运行中')
await new Promise(r => setTimeout(r, 120))
console.log('--- 开始卸载 ---')
await fiber.dispose() // 逆序执行所有清理
console.log('[main] worker 已卸载,状态:', fiber.state) // 4 (DISPOSED)
// 卸载后事件监听也已注销
ctx.emit('app/status', '应无输出')
console.log('[main] 结束')
// ========== 3. 重新安装:DISPOSED 的 fiber 对应的插件可以再次安装 ==========
const fiber2 = ctx.plugin(workerPlugin)
await fiber2
console.log('[main] 重新安装成功,状态:', fiber2.state) // 2 (ACTIVE)
await fiber2.dispose()
}
main()
运行输出(节选):
csharp
[main] worker 已加载,状态: 2
[worker] 状态: 运行中
[worker] 心跳
[worker] 心跳
--- 开始卸载 ---
[worker] 插件级清理函数执行
[worker] 定时器已清理
[main] worker 已卸载,状态: 4
[main] 结束
[main] 重新安装成功,状态: 2
...
要点:
- 清理顺序是逆序的 (与注册/收集顺序相反),模拟栈式展开。上例中
apply返回的插件级清理函数最后被收集、因此最先 执行,ctx.effect注册的定时器随后清理。 ctx.on、ctx.provide、ctx.effect注册的一切都由 fiber 托管------插件作者几乎不需要手写注销逻辑。ctx.effect(fn)的fn返回清理函数(或清理函数的Promise/迭代器,用于批量注册);返回的 disposer 可提前调用。await fiber只等加载;await fiber.dispose()等卸载完成(异步清理也会被等待)。
第7步:配置系统 ------ Config 校验(zod)与热重载
插件可声明 Config 校验器(遵循 Standard Schema V1 规范)。zod v4 原生实现了该规范 ------schema 直接作为 Config 使用,无需任何适配。校验失败会在加载时抛 ValidationError;fiber.update() 可用新配置热重载插件(自动卸载再加载)。
bash
bun add zod # 先安装 zod
ts
// step7.ts ------ 配置校验(zod)与热重载
import * as Cordis from '@deepseek-ai/cordis'
import { z } from 'zod'
const ctx = new Cordis.Context()
// zod schema 直接作为 Config:类型校验、默认值、错误消息一站搞定
const greetConfig = z.object({
greeting: z.string().min(1, '问候语不能为空'),
times: z.number().int().min(1).max(10).default(1), // 默认值:未提供时取 1
})
// 配置类型分两个视角:
// - z.input:用户传入的原始类型(带默认值的字段可省略)
// - z.output(即 z.infer):校验后的最终类型(默认值已填充)------apply 的 config 用它
type GreetConfig = z.output<typeof greetConfig>
const greetPlugin = {
name: 'greet',
Config: greetConfig,
apply(ctx: Cordis.Context, config: GreetConfig) {
for (let i = 0; i < config.times; i++) {
console.log(`[greet] ${config.greeting} (${i + 1}/${config.times})`)
}
return () => console.log('[greet] 清理(热重载前也会执行)')
}
}
async function main() {
// ctx.plugin 的配置参数类型跟随 apply 声明的类型(output:times 必填)。
// 依赖默认值时,在边界处用 parse 预解析:输入 input(times 可省)→ 得到 output
const fiber = ctx.plugin(greetPlugin, greetConfig.parse({ greeting: '你好' }))
await fiber
// 热重载:先执行清理,再用新配置重新加载
await fiber.update({ greeting: '早上好', times: 2 })
// 非法配置:抛 ValidationError(含字段路径),且插件保持原配置运行
try {
await fiber.update({ greeting: '', times: 99 })
} catch (e) {
console.log('[main] 校验失败:', (e as Error).message)
}
await fiber.dispose()
}
main()
运行输出:
scss
[greet] 你好 (1/1)
[greet] 清理(热重载前也会执行)
[greet] 早上好 (1/2)
[greet] 早上好 (2/2)
[main] 校验失败: invalid config:
- 问候语不能为空 (at greeting)
- Too big: expected number to be <=10 (at times)
[greet] 清理(热重载前也会执行) ← fiber.dispose() 触发的最终清理
要点:
- zod v4 的 schema 自带
'~standard'标记,直接赋给Config即可;TypeBox、Valibot 等同样支持。 - 校验失败抛
ValidationError(含逐条 issue 与字段路径),插件不会用坏配置启动。 z.inputvsz.output:带.default()的字段在 input 中可省略、在 output 中必填。apply的 config 参数用 output 类型;调用处若依赖默认值,用schema.parse()在边界预解析。fiber.update(config)= 卸载 + 用新配置重载,全程保持 fiber 身份不变,是实现配置热更新/HMR 的基础。internal/update事件允许你在重载前介入(拦截、改写或否决更新)。
第8步:日志服务 ------ 内置 logger 与 exporter 扩展
Cordis 内置了结构化日志服务:ctx.logger(名字) 返回命名日志外观 (facade),按子系统区分输出;通过注册 exporter 决定日志去向(控制台、文件、远程......)。
ts
// step8.ts ------ 内置 logger
import * as Cordis from '@deepseek-ai/cordis'
const ctx = new Cordis.Context()
// ========== 1. 注册 exporter(默认不输出任何内容,必须显式注册!) ==========
// 注意:默认导出阈值为 1(info),warn(2)/debug(3) 会被过滤------
// 要收到全部级别,显式配置 levels: { default: 3 }
ctx.logger.exporter({
levels: { default: 3 },
export(message) {
// message: { sn, ts, name, type, level, args }
console.log(`[日志] (${message.type}) [${message.name}] ${message.args.join(' ')}`)
},
})
// ========== 2. 命名 logger:按子系统区分 ==========
const appLogger = ctx.logger('app')
const dbLogger = ctx.logger('db')
appLogger.info('应用启动')
dbLogger.warn('数据库连接较慢')
dbLogger.error('查询失败:表不存在')
// 未命名调用直接使用 ctx.logger
ctx.logger.info('未命名日志(归属当前 fiber 的名字)')
// 内置缓冲区:最近日志(供诊断/崩溃回溯)
console.log('[main] 缓冲区条数:', ctx.logger.buffer.length)
运行输出:
scss
[日志] (info) [app] 应用启动
[日志] (warn) [db] 数据库连接较慢
[日志] (error) [db] 查询失败:表不存在
[日志] (info) [root] 未命名日志(归属当前 fiber 的名字)
[main] 缓冲区条数: 3
缓冲区条数是 3 而非 4:内置 buffer exporter 同样按默认阈值 1 过滤,warn 不入缓冲区。
要点:
- 默认没有任何输出 ------必须注册 exporter。这是常见坑:
logger.info()看不到内容不是 bug。 - 日志方法四级:
error(0) /info(1) /warn(2) /debug(3)。exporter 默认阈值为 1 ------warn/debug默认被过滤,需配置levels: { default: 3 }(或按日志名levels: { db: 3 })才能收到。 - exporter 可按
levels配置阈值过滤,可注册多个(同时输出到控制台和文件)。 ctx.logger.buffer缓存最近日志(条数由bufferSize控制,默认 1000),崩溃时可供诊断工具回溯。内置 buffer exporter 同样受默认阈值 1 过滤(warn/debug 不入缓冲)。- 在插件内调用
ctx.logger.info(...)时日志名自动归属插件名------天然区分日志来源。
第9步:上下文作用域 ------ extend 与 isolate 隔离域
Context 是树形的:extend() 派生子上下文(共享父级服务),isolate(name) 派生对指定服务隔离的子上下文(可拥有同名服务的独立实现)。
ts
// step9.ts ------ 上下文隔离
import * as Cordis from '@deepseek-ai/cordis'
const root = new Cordis.Context()
declare module '@deepseek-ai/cordis' {
interface Context { database: { query(sql: string): string } }
}
// 根上下文注册默认服务
root.provide('database', { query: (sql) => `[生产库] ${sql}` })
// ========== extend:普通子上下文,共享父级服务 ==========
const child = root.extend()
console.log('[child] 直接用父级服务:', child.database.query('SELECT 1'))
// ========== isolate:为 database 创建独立隔离域 ==========
const sandbox = root.isolate('database')
// 在沙箱里可以注册同名服务,不冲突、不覆盖根上下文
const dispose = sandbox.provide('database', { query: (sql) => `[测试库] ${sql}` })
console.log('[root] 查询:', root.database.query('SELECT 1'))
console.log('[sandbox] 查询:', sandbox.database.query('SELECT 1'))
// 撤销沙箱实现:隔离域内该服务不再有实现,读取返回 undefined
// 注意:provide 的 disposer 是异步的------调用后需 await 再读取
await dispose()
console.log('[sandbox] 撤销后读取:', sandbox.database)
// 根上下文不受任何影响
console.log('[root] 仍正常:', root.database.query('SELECT 2'))
运行输出:
ini
[child] 直接用父级服务: [生产库] SELECT 1
[root] 查询: [生产库] SELECT 1
[sandbox] 查询: [测试库] SELECT 1
[sandbox] 撤销后读取: undefined
[root] 仍正常: [生产库] SELECT 2
要点:
extend():子上下文继承父级一切服务与配置,自身注册的服务不影响父级。isolate('服务名'):为该服务开辟独立隔离域------同名服务可在不同域中并存(例如多租户、测试沙箱、插件市场隔离)。- 隔离是彻底的 :
isolate('database')后,该子上下文读取database只查隔离域内的实现,域内实现被撤销后不会回退 到父级(返回undefined)。要"可回退"的继承语义,用extend()即可。 - 这套机制是 Cordis 实现"多实例隔离"(如 Koishi 的 multi-user 场景)的基础。
第10步:完整应用 ------ 动态插件管理的计算器应用
最后把前面所有能力组装成一个完整应用:可动态安装/卸载插件的计算器程序。综合运用:服务注册、依赖注入、事件解耦、Fiber 动态管理、AOP 方法增强、内置 logger、跨服务读写协作。
本步代码即项目中的
src/index.ts(已验证可运行)。
ts
// src/index.ts ------ 完整应用
import * as Cordis from '@deepseek-ai/cordis'
// ============ 类型声明 ============
declare module '@deepseek-ai/cordis' {
interface Context {
calculator: CalculatorService
history: HistoryService
}
interface Events {
'calculation/performed'(operation: string, result: number): void
'plugin/registered'(name: string): void
'plugin/unregistered'(name: string): void
}
}
// ============ 基础服务:计算器 ============
export class CalculatorService extends Cordis.Service {
pow?: (base: number, exponent: number) => number
constructor(ctx: Cordis.Context) {
super(ctx, 'calculator')
ctx.logger('calculator').info('计算器服务已构造')
}
add(a: number, b: number): number {
const result = a + b
this.ctx.emit('calculation/performed', `add(${a}, ${b})`, result)
return result
}
multiply(a: number, b: number): number {
const result = a * b
this.ctx.emit('calculation/performed', `multiply(${a}, ${b})`, result)
return result
}
}
// ============ 配套服务:计算历史 ============
export class HistoryService extends Cordis.Service {
private records: string[] = []
constructor(ctx: Cordis.Context) {
super(ctx, 'history')
ctx.logger('history').info('计算历史服务已构造')
// 订阅事件自动采集(观察者模式,DRY:所有插件无需手动记录)
ctx.on('calculation/performed', (operation, result) => {
this.records.push(`${operation} = ${result}`)
})
}
/** 按操作描述查询历史结果(读取方向的跨服务协作) */
findResult(operation: string): number | undefined {
const prefix = `${operation} = `
const entry = this.records.find(r => r.startsWith(prefix))
return entry === undefined ? undefined : Number(entry.slice(prefix.length))
}
getHistory(): string[] {
return [...this.records] // 副本,防止外部篡改
}
}
// ============ 插件定义 ============
// 1. 计算器插件(服务提供者:直接 new Service,构造即注册)
export const calculatorPlugin = {
name: 'calculator',
provide: 'calculator',
apply(ctx: Cordis.Context) {
console.log('[插件] 计算器插件正在应用...')
new CalculatorService(ctx)
ctx.emit('plugin/registered', 'calculator')
console.log('[插件] 计算器服务注册完成')
}
}
// 2. 计算历史插件
export const historyPlugin = {
name: 'history',
provide: 'history',
apply(ctx: Cordis.Context) {
console.log('[插件] 计算历史插件正在应用...')
new HistoryService(ctx)
ctx.emit('plugin/registered', 'history')
console.log('[插件] 计算历史服务注册完成')
}
}
// 3. 增强计算器插件(单一依赖 + AOP 方法增强)
export const enhancedCalculatorPlugin = {
name: 'enhanced-calculator',
inject: ['calculator'],
apply(ctx: Cordis.Context) {
console.log('[插件] 增强计算器插件正在应用...')
if (!ctx.calculator) return
// 保存原始方法(bind 锁定 this),替换为带日志的包装(开闭原则)
const originalAdd = ctx.calculator.add.bind(ctx.calculator)
const originalMultiply = ctx.calculator.multiply.bind(ctx.calculator)
ctx.calculator.add = (a: number, b: number) => {
console.log(`[增强] 即将计算加法 ${a} + ${b}`)
const result = originalAdd(a, b)
console.log(`[增强] 计算结果: ${result}`)
return result
}
ctx.calculator.multiply = (a: number, b: number) => {
console.log(`[增强] 即将计算乘法 ${a} * ${b}`)
const result = originalMultiply(a, b)
console.log(`[增强] 计算结果: ${result}`)
return result
}
ctx.emit('plugin/registered', 'enhanced-calculator')
// 清理函数:卸载时恢复原始方法(可逆性)
return () => {
ctx.calculator.add = originalAdd
ctx.calculator.multiply = originalMultiply
console.log('[插件] 增强计算器已清理(原始方法已恢复)')
}
}
}
// 4. 科学计算器插件(多依赖 + 记忆化)
export const scientificCalculatorPlugin = {
name: 'scientific-calculator',
inject: ['calculator', 'history'], // 多依赖:全部就绪才加载
apply(ctx: Cordis.Context) {
console.log('[插件] 科学计算器插件正在应用...')
if (!ctx.calculator || !ctx.history) return
const calculator = ctx.calculator
const history = ctx.history
calculator.pow = (base: number, exponent: number) => {
const result = Math.pow(base, exponent)
ctx.emit('calculation/performed', `pow(${base}, ${exponent})`, result)
return result
}
// factorial 通过 history 查询历史实现记忆化(读方向的协作)
calculator.factorial = (n: number) => {
if (n < 0) throw new Error('阶乘要求输入为非负数')
const cached = history.findResult(`factorial(${n})`)
if (cached !== undefined) {
ctx.logger('scientific').info(`阶乘命中历史缓存: ${n}! = ${cached}`)
return cached
}
let result = 1
for (let i = 2; i <= n; i++) result *= i
ctx.emit('calculation/performed', `factorial(${n})`, result)
return result
}
ctx.emit('plugin/registered', 'scientific-calculator')
return () => {
delete calculator.pow
delete calculator.factorial
console.log('[插件] 科学计算器已清理(动态方法已移除)')
}
}
}
// 5. 监控插件(纯订阅者,展示事件解耦)
export const monitorPlugin = {
name: 'monitor',
apply(ctx: Cordis.Context) {
console.log('[插件] 监控插件正在启动...')
ctx.on('calculation/performed', (operation, result) => {
console.log(`[监控] 计算操作: ${operation} = ${result}`)
})
ctx.on('plugin/registered', (name) => {
console.log(`[监控] 插件已注册: ${name}`)
})
ctx.on('plugin/unregistered', (name) => {
console.log(`[监控] 插件已注销: ${name}`)
})
ctx.emit('plugin/registered', 'monitor')
console.log('[插件] 监控插件注册完成')
}
}
// ============ 动态插件管理器 ============
export class PluginManager {
private ctx: Cordis.Context
private activePlugins: Set<string> = new Set()
private fibers: Map<string, Cordis.Fiber> = new Map() // 名字 -> Fiber 句柄
constructor(ctx: Cordis.Context) {
this.ctx = ctx
}
async installPlugin(plugin: any, name: string): Promise<void> {
if (this.activePlugins.has(name)) return // 幂等
console.log(`[管理器] 正在安装插件: ${name}`)
const fiber = this.ctx.plugin(plugin)
try {
await fiber // 等待依赖就绪、apply 执行完成
} catch (e) {
console.error(`[管理器] 插件 ${name} 加载失败:`, e)
return
}
this.fibers.set(name, fiber)
this.activePlugins.add(name)
console.log(`[管理器] 插件 ${name} 安装成功`)
}
async uninstallPlugin(name: string): Promise<void> {
if (!this.activePlugins.has(name)) return
console.log(`[管理器] 正在卸载插件: ${name}`)
const fiber = this.fibers.get(name)
if (fiber) {
await fiber.dispose() // 自动执行清理函数
this.fibers.delete(name)
}
this.activePlugins.delete(name)
this.ctx.emit('plugin/unregistered', name)
console.log(`[管理器] 插件 ${name} 卸载成功`)
}
getActivePlugins(): string[] {
return Array.from(this.activePlugins)
}
}
// ============ 主程序 ============
async function main() {
console.log('='.repeat(60))
console.log('启动 Cordis 应用(支持动态插件管理)')
console.log('='.repeat(60))
// 1. 创建 Context
const ctx = new Cordis.Context()
// 注册控制台 exporter(内置 logger 默认不输出!)
ctx.logger.exporter({
export(message) {
console.log(`[日志] (${message.type}) [${message.name}] ${message.args.join(' ')}`)
}
})
const logger = ctx.logger('app')
logger.info('Context 创建成功')
// 2. 核心插件统一经由管理器安装
const manager = new PluginManager(ctx)
await manager.installPlugin(calculatorPlugin, 'calculator')
await manager.installPlugin(historyPlugin, 'history')
await manager.installPlugin(monitorPlugin, 'monitor')
// 3. 验证服务
console.log(`[初始化] 计算器服务可用: ${!!ctx.calculator}`)
console.log(`[初始化] 计算历史服务可用: ${!!ctx.history}`)
// 4. 演示1:动态安装(单一依赖)
console.log('\n' + '='.repeat(60))
console.log('演示 1:动态安装插件(单一依赖场景)')
console.log('='.repeat(60))
await manager.installPlugin(enhancedCalculatorPlugin, 'enhanced-calculator')
console.log('\n[演示] 测试增强版计算器:')
console.log(`[演示] 10 + 20 = ${ctx.calculator.add(10, 20)}`)
// 5. 演示2:动态安装(多依赖 + 记忆化)
console.log('\n' + '='.repeat(60))
console.log('演示 2:动态安装插件(多依赖场景)')
console.log('='.repeat(60))
await manager.installPlugin(scientificCalculatorPlugin, 'scientific-calculator')
console.log('\n[演示] 测试科学计算器:')
console.log(`[演示] 2^8 = ${ctx.calculator.pow!(2, 8)}`)
console.log(`[演示] 5! = ${ctx.calculator.factorial!(5)}`)
console.log(`[演示] 再次 5! = ${ctx.calculator.factorial!(5)}(命中历史缓存)`)
// 6. 演示3:动态卸载(清理函数恢复原始方法)
console.log('\n' + '='.repeat(60))
console.log('演示 3:动态卸载插件')
console.log('='.repeat(60))
console.log(`[演示] 当前激活插件: ${manager.getActivePlugins().join(', ')}`)
await manager.uninstallPlugin('enhanced-calculator')
console.log(`[演示] 卸载后验证: 5 + 3 = ${ctx.calculator.add(5, 3)}(增强日志应已消失)`)
// 7. 演示4:重新安装(Fiber 卸载后可重装)
console.log('\n' + '='.repeat(60))
console.log('演示 4:重新安装已卸载的插件')
console.log('='.repeat(60))
await manager.installPlugin(enhancedCalculatorPlugin, 'enhanced-calculator')
console.log(`[演示] 6 * 7 = ${ctx.calculator.multiply(6, 7)}(增强日志重新生效)`)
await manager.uninstallPlugin('enhanced-calculator')
// 8. 最终状态
console.log('\n' + '='.repeat(60))
console.log('应用最终状态')
console.log('='.repeat(60))
console.log(`激活插件: ${manager.getActivePlugins().join(', ')}`)
console.log('\n[演示] 最近的计算历史:')
ctx.history.getHistory().slice(-5).forEach(entry => console.log(` ${entry}`))
logger.info('应用运行成功!')
return ctx
}
main().catch(console.error)
运行输出(节选,完整输出见实际运行):
ini
============================================================
启动 Cordis 应用(支持动态插件管理)
============================================================
[日志] (info) [app] Context 创建成功
[管理器] 正在安装插件: calculator
[插件] 计算器插件正在应用...
[管理器] 插件 calculator 安装成功
...
演示 1:动态安装插件(单一依赖场景)
[增强] 即将计算加法 10 + 20
[监控] 计算操作: add(10, 20) = 30
[增强] 计算结果: 30
[演示] 10 + 20 = 30
...
演示 2:动态安装插件(多依赖场景)
[监控] 计算操作: pow(2, 8) = 256
[演示] 2^8 = 256
[日志] (info) [scientific] 阶乘命中历史缓存: 5! = 120
[演示] 再次 5! = 120(命中历史缓存)
...
演示 3:动态卸载插件
[插件] 增强计算器已清理(原始方法已恢复)
[监控] 插件已注销: enhanced-calculator
...
应用最终状态
激活插件: calculator, history, monitor, scientific-calculator
[演示] 最近的计算历史:
add(10, 20) = 30
pow(2, 8) = 256
factorial(5) = 120
add(5, 3) = 8
multiply(6, 7) = 42
架构总览:
scss
┌────────────────────────────────┐
│ PluginManager(动态管理) │
│ install / uninstall / fibers │
└──────────────┬─────────────────┘
│ ctx.plugin() / fiber.dispose()
▼
┌────────────────────────── Context(容器 + 事件总线)──────────────────────────┐
│ │
│ 服务层 事件层 日志层 │
│ ┌────────────┐ ┌────────────────┐ ┌─────────────┐ │
│ │ calculator │◄─inject─│ calculation/ │ │ ctx.logger │ │
│ │ history │◄─inject─│ performed │ │ (内置服务) │ │
│ └─────┬──────┘ │ plugin/registered│ └─────────────┘ │
│ │提供 └────────┬─────────┘ │
│ ▼ │ 订阅 │
│ calculatorPlugin historyPlugin │ monitorPlugin │
│ enhancedCalculatorPlugin (AOP) │ scientificCalculatorPlugin (多依赖+记忆化) │
└────────────────────────────────────────────────────────────────────────────────┘
本步综合运用的特性清单:
| 特性 | 应用位置 |
|---|---|
| 服务注册(Service 构造即注册) | CalculatorService / HistoryService |
| 依赖注入(单/多依赖) | enhancedCalculatorPlugin / scientificCalculatorPlugin |
| 事件解耦 | calculation/performed 贯穿监控、历史采集 |
| Fiber 动态安装/卸载 | PluginManager.installPlugin / uninstallPlugin |
| 清理函数(可逆性) | 增强插件恢复原方法、科学插件移除动态方法 |
| AOP 方法增强 | enhancedCalculatorPlugin 包装 add/multiply |
| 内置 logger + exporter | 控制台 exporter、命名日志外观 |
| 插件可重装 | 演示 4(Fiber dispose 后再次安装) |
| 跨服务读写协作 | factorial 通过 history.findResult 记忆化 |
附录:常见坑速查
| 现象 | 原因与解决 |
|---|---|
logger.info() 没有任何输出 |
内置 logger 默认无 exporter,需手动 ctx.logger.exporter({...}) 注册 |
logger.warn() / logger.debug() 没有输出 |
exporter 默认阈值为 1(info),需配置 levels: { default: 3 } |
cannot get property "xxx" without inject |
插件上下文读服务必须先声明 inject: ['xxx'];或用 ctx.get('xxx') 探测 |
service "xxx" has been registered |
同一隔离域重复注册同名服务;提供者插件不要做自检,框架会拦截 |
ctx.plugin() 返回值不是清理函数 |
4.x 返回 Fiber :await fiber 等加载、await fiber.dispose() 卸载 |
类插件的 dispose() 方法没被调用 |
类插件的清理函数是 [Service.init]() 的返回值 ,不是 dispose 方法 |
| 依赖服务的插件 "没加载" | 处于 PENDING 状态等依赖;提供依赖的服务注册后自动触发加载 |
waterfall 的 next is not a function |
next 由框架作为最后一个参数注入,事件类型声明需包含它 |
| 自定义 Config 校验器不生效 | 必须实现 Standard Schema V1('~standard' 标记内含 validate);推荐直接用 zod v4 / TypeBox,原生兼容 |
| zod 带默认值的配置报"字段缺失"类型错 | ctx.plugin 参数类型取自 apply 的 config(output 视角);调用处用 schema.parse() 预解析,或显式传全字段 |
| 卸载后事件监听仍在触发 | 不会------fiber 托管的一切监听随卸载自动注销;若仍在触发,检查是否用了非托管注册方式 |
总结
从第1步的 20 行最小应用,到第10步的完整插件化系统,核心脉络是:
- Context 是容器 + 事件总线(第1步)
- 事件 提供五撮合策略的解耦通信(第2步)
- 服务 是命名的能力单元(第3步)
- 插件 是可装卸的功能单元(第4步)
- 依赖注入 让插件间协作显式化、响应式化(第5步)
- Fiber 托管一切资源的生命周期(第6步)
- 配置 + 热重载 支撑运行时演化(第7步)
- logger 是结构化可观测性的入口(第8步)
- 隔离域 解决多实例并存(第9步)
- 最终组装成动态可演化的应用程序(第10步)
Cordis 的设计哲学:一切皆插件,插件皆可插拔,插拔皆可逆。