用 ctx.effect() 与 inject 管理清理和依赖

约 25 分钟

在插件中使用 ctx.effect() 注册清理逻辑并用 inject 获取内置服务,验证卸载时清理被调用。

上一节结束时,你从上下文/注册表中确认了服务可见——最小插件挂载成功,生命周期各阶段也有了 Fiber 状态机的解释。现在把场景再推进一步:插件挂载时做的事往往不止注册服务,它可能启动定时器、打开文件、写入日志。问题来了——插件卸载时,这些资源谁来收?

如果靠手动清理,你得覆盖每一条卸载路径。漏掉一条,运行中的 dsh 就多一份残留。DeepSeek Harness 不让你这样干:插件的注册都以效应形式登记,卸载时由内核统一回卷。

核心概念

effect:可逆效应

想象退房。入住时你挂出“请勿打扰”牌,前台记下这件事;退房时,服务员自动收回牌子、还原房间。你不必在离开前打电话叮嘱,系统会按登记自动撤销。

在 DeepSeek Harness 中,effect(效应)就是这种可逆的注册。你用 ctx.effect() 登记一段副作用;当所属插件(fiber)卸载时,内核自动执行反向清理,这个回卷过程叫 unwind。插件的注册都以效应形式登记,卸载时一起回卷,不依赖你手动收尾。

在插件入口函数里调用 ctx.effect()。你传入的函数会先执行一次,作为注册动作;它返回的清理函数,在插件卸载时由内核调用:

typescript
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(会话日志)为例:

typescript
import { Context } from 'cordis'

export const name = 'inject-demo'

export function apply(ctx: Context) {
  ctx.inject(['sessions'], () => {
    console.log('[inject-demo] sessions 服务就绪')
  })
}

sessions 是 dsh 的内置服务,负责追加式会话日志。声明之后,内核会在服务就绪后再执行回调,你不需要关心它何时出现。

挂载并卸载插件,验证清理执行

  1. 1

    新建文件 cleanup.ts,写入同时使用 effect 与 inject 的插件。

    typescript
    import { 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. 2

    沿用上一节的挂载方式,把 cleanup-demo 挂载到运行中的 dsh。

  3. 3

    观察控制台,应依次出现 [cleanup-demo] sessions 服务就绪 与 [cleanup-demo] effect 已注册。

  4. 4

    沿用上一节的卸载方式,卸载 cleanup-demo。

  5. 5

    观察控制台,应出现 [cleanup-demo] 清理已执行。

  6. 6

    再次挂载、再次卸载,确认每次卸载都输出清理日志,且顺序一致。

你应该看到

每次卸载 cleanup-demo,控制台都会输出 [cleanup-demo] 清理已执行。插件卸载前后,effect 注册的副作用出现可观察的变化,且全程不需要手动调用清理函数。

遇到问题:卸载插件后,控制台没有出现 [cleanup-demo] 清理已执行。

可能原因

  • 插件并未真正卸载,只是当前会话失效,fiber 仍然存活。
  • ctx.effect() 写在条件分支里,挂载时没有执行到,效应从未注册。
  • 清理函数没有从 effect 回调中返回,unwind 时无清理可执行。

排查步骤

  1. 按上一节学过的 fiber 生命周期状态,确认插件确实进入卸载状态。
  2. 把 ctx.effect() 放在 apply 顶层,保证挂载必经。
  3. 在 effect 回调中用 return 返回清理函数。
自检:插件卸载时,如何保证 ctx.effect() 注册的清理逻辑一定被执行?

在插件入口顶层调用 ctx.effect(),并在回调里 return 清理函数。插件卸载时内核自动 unwind 该效应,调用返回的清理函数;不要在卸载分支里手动补清理,那会漏掉路径。