在插件中使用 ctx.effect() 注册清理逻辑并用 inject 获取内置服务,验证卸载时清理被调用。
上一节结束时,你从上下文/注册表中确认了服务可见——最小插件挂载成功,生命周期各阶段也有了 Fiber 状态机的解释。现在把场景再推进一步:插件挂载时做的事往往不止注册服务,它可能启动定时器、打开文件、写入日志。问题来了——插件卸载时,这些资源谁来收?
如果靠手动清理,你得覆盖每一条卸载路径。漏掉一条,运行中的 dsh 就多一份残留。DeepSeek Harness 不让你这样干:插件的注册都以效应形式登记,卸载时由内核统一回卷。
核心概念
effect:可逆效应
想象退房。入住时你挂出“请勿打扰”牌,前台记下这件事;退房时,服务员自动收回牌子、还原房间。你不必在离开前打电话叮嘱,系统会按登记自动撤销。
在 DeepSeek Harness 中,effect(效应)就是这种可逆的注册。你用 ctx.effect() 登记一段副作用;当所属插件(fiber)卸载时,内核自动执行反向清理,这个回卷过程叫 unwind。插件的注册都以效应形式登记,卸载时一起回卷,不依赖你手动收尾。
在插件入口函数里调用 ctx.effect()。你传入的函数会先执行一次,作为注册动作;它返回的清理函数,在插件卸载时由内核调用:
import { Context } from 'cordis'
export const name = 'cleanup-demo'
export function apply(ctx: Context) {
ctx.effect(() => {
console.log('[cleanup-demo] effect 已注册')
return () => {
console.log('[cleanup-demo] 清理已执行')
}
})
}挂载时控制台出现 [cleanup-demo] effect 已注册;卸载时出现 [cleanup-demo] 清理已执行。清理由内核触发,你不需要在任何卸载分支里调用它。
核心概念
inject:声明依赖
想象点餐。你不进后厨翻找食材,只报菜名;厨房按菜单备好再端给你。你声明要什么,系统负责给什么。
inject 就是插件的点餐单。你用 ctx.inject() 声明对某个服务的依赖,内核解析依赖、把服务交到你的代码里,并保证在依赖就绪后才执行你的逻辑。上一节你已经见过共享上下文里的服务;inject 让你显式声明“我要用哪个服务”,而不是假设它一直存在。
用 ctx.inject() 声明依赖。以内置服务 sessions(会话日志)为例:
import { Context } from 'cordis'
export const name = 'inject-demo'
export function apply(ctx: Context) {
ctx.inject(['sessions'], () => {
console.log('[inject-demo] sessions 服务就绪')
})
}sessions 是 dsh 的内置服务,负责追加式会话日志。声明之后,内核会在服务就绪后再执行回调,你不需要关心它何时出现。
插件卸载的路径不止一条:配置补丁覆盖会触发重载,依赖变更会让旧 fiber 失效。你不可能在每个分支都补一次清理。把清理逻辑集中注册为 effect,卸载时由内核统一 unwind,遗漏自然消失。
挂载并卸载插件,验证清理执行
- 1
新建文件 cleanup.ts,写入同时使用 effect 与 inject 的插件。
typescriptimport { Context } from 'cordis' export const name = 'cleanup-demo' export function apply(ctx: Context) { ctx.inject(['sessions'], () => { console.log('[cleanup-demo] sessions 服务就绪') }) ctx.effect(() => { console.log('[cleanup-demo] effect 已注册') return () => { console.log('[cleanup-demo] 清理已执行') } }) } - 2
沿用上一节的挂载方式,把 cleanup-demo 挂载到运行中的 dsh。
- 3
观察控制台,应依次出现 [cleanup-demo] sessions 服务就绪 与 [cleanup-demo] effect 已注册。
- 4
沿用上一节的卸载方式,卸载 cleanup-demo。
- 5
观察控制台,应出现 [cleanup-demo] 清理已执行。
- 6
再次挂载、再次卸载,确认每次卸载都输出清理日志,且顺序一致。
你应该看到
每次卸载 cleanup-demo,控制台都会输出 [cleanup-demo] 清理已执行。插件卸载前后,effect 注册的副作用出现可观察的变化,且全程不需要手动调用清理函数。
遇到问题:卸载插件后,控制台没有出现 [cleanup-demo] 清理已执行。
可能原因
- 插件并未真正卸载,只是当前会话失效,fiber 仍然存活。
- ctx.effect() 写在条件分支里,挂载时没有执行到,效应从未注册。
- 清理函数没有从 effect 回调中返回,unwind 时无清理可执行。
排查步骤
- 按上一节学过的 fiber 生命周期状态,确认插件确实进入卸载状态。
- 把 ctx.effect() 放在 apply 顶层,保证挂载必经。
- 在 effect 回调中用 return 返回清理函数。
自检:插件卸载时,如何保证 ctx.effect() 注册的清理逻辑一定被执行?
在插件入口顶层调用 ctx.effect(),并在回调里 return 清理函数。插件卸载时内核自动 unwind 该效应,调用返回的清理函数;不要在卸载分支里手动补清理,那会漏掉路径。