现在位置: 首页 > DeepSeek Harness > 正文

DeepSeek Harness 插件生命周期:Fiber 状态机

在前面的章节中,我们已经学习了如何注册事件监听器、工具,以及使用 ctx.effect() 管理资源。

但这些注册什么时候开始生效?插件卸载时,事件监听器和工具又是如何被清理的?

DeepSeek Harness 使用 Cordis 插件模型管理这些过程。每个插件实例都有一个对应的 Fiber,负责跟踪插件状态、依赖关系和需要清理的资源。

本章节介绍 Fiber 生命周期、依赖驱动加载、自动清理机制,以及插件的手动卸载和热替换。

Fiber 是什么?

Fiber 是 Cordis 中表示一个插件运行实例的对象。

当插件被加载时,框架会创建对应的 Fiber,用于管理插件的生命周期。

一个 Fiber 主要负责以下工作:

  • 记录插件当前的生命周期状态。
  • 保存经过校验的插件配置。
  • 跟踪插件需要的服务依赖。
  • 管理插件注册的事件、工具和其他资源。
  • 在插件卸载时执行清理操作。

可以通过 ctx.fiber 获取当前插件所属的 Fiber。

例如:

export function apply(ctx: Context) {
  const fiber = ctx.fiber

  console.log(fiber.state)
}

其中,fiber.state 表示当前生命周期状态。

注意:一个插件模块和一个 Fiber 并不完全相同。同一个插件可以被应用到不同上下文中,每个加载实例可以拥有独立的 Fiber。

Fiber 状态机

Fiber 使用状态机管理插件从加载到卸载的过程。

一个正常加载并最终卸载的插件,通常经历以下状态:

PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED

如果插件加载过程中发生错误,则可能进入 FAILED 状态。

DeepSeek Harness Fiber 生命周期状态机

各状态的含义如下:

状态 含义 发生时机
PENDING 等待加载 插件已经声明,但所需的服务依赖尚未全部就绪
LOADING 正在加载 依赖已经满足,正在执行插件的初始化逻辑
ACTIVE 运行中 插件初始化完成,处于正常运行状态
FAILED 加载失败 插件初始化或相关配置校验过程中发生错误
UNLOADING 正在卸载 框架正在撤销注册并释放插件资源
DISPOSED 已卸载 插件实例已完成处置,相关生命周期资源已经清理

例如,一个正常加载的插件会先进入 LOADING,执行 apply() 中的初始化操作,成功后进入 ACTIVE。

当插件需要卸载时,会进入 UNLOADING,执行相应的资源清理,完成后进入 DISPOSED。

如果初始化失败,插件可能进入 FAILED。具体错误需要通过日志或框架提供的诊断接口检查,不能将所有加载失败都理解为缺少依赖。

依赖驱动的加载

Cordis 支持根据服务依赖决定插件的加载时机。

插件可以通过 inject 声明自己依赖哪些服务。

例如,一个插件需要使用工具注册服务 tools 和大语言模型服务 llm。

实例

// 文件:scratch-plugin/src/my-plugin.ts

import type { Context } from '@deepseek-ai/cordis'

// 声明必需的服务依赖
export const inject = ['tools', 'llm']

export function apply(ctx: Context) {
  // 只有必需服务就绪后,插件才会加载
  console.log('插件开始加载')

  // 可以使用 ctx.tools 和 ctx.llm
  console.log('依赖服务已就绪')
}

当 tools 或 llm 尚未就绪时,插件会等待依赖,不会立即执行 apply()。

等到必需服务全部就绪后,Cordis 才会加载插件。

这样开发者就不需要手动编写复杂的初始化顺序。

依赖消失时会怎样?

Cordis 的依赖机制不仅影响插件首次加载,也会影响已经运行的插件。

例如:

  1. 插件依赖 tools 和 llm 服务。
  2. 两个服务都就绪后,插件进入运行状态。
  3. 运行过程中,llm 服务被卸载或替换。
  4. Cordis 自动卸载依赖该服务的插件,并清理旧注册。
  5. 当所需服务重新就绪后,插件可以重新加载。

这意味着,依赖服务发生变化时,框架可以自动管理相关插件的卸载与重新初始化。

需要注意,自动重新加载不等于恢复旧插件中的所有运行时数据。如果插件需要在重新加载后保留状态,应通过适当的外部存储或状态管理机制实现。

自动清理机制

插件运行时可能注册事件监听器、工具、LLM 适配器,也可能创建定时器或网络连接。

如果卸载时不清理这些资源,可能导致重复监听、重复注册或资源泄漏。

Cordis 的生命周期机制会跟踪通过受管理的注册 API 创建的资源,并在插件卸载时撤销相应注册。

常见操作如下:

注册操作 卸载时的行为
ctx.on(event, handler) 自动移除该插件注册的事件监听器
ctx.tools.register(tool) 撤销该插件注册的工具
ctx.llm.registerAdapter(...) 撤销该插件注册的 LLM 适配器
ctx.plugin(child) 父插件卸载时,关联的子插件也会被处置
ctx.effect(...) 执行注册的资源释放函数

因此,通过 Cordis 生命周期管理的注册,通常不需要开发者再手动编写对应的卸载逻辑。

但对于直接使用 Node.js API 创建的定时器、文件监听器或网络连接,框架并不一定能够自动追踪,需要使用 ctx.effect() 显式管理。

ctx.effect() 的工作原理

ctx.effect() 用于注册具有自动清理能力的操作。

它接收一个函数,该函数在注册时立即执行,并返回用于释放资源的处置器(Disposer)。

例如:

实例

import type { Context } from '@deepseek-ai/cordis'

export function apply(ctx: Context) {
  console.log('插件开始加载')

  ctx.effect(() => {
    console.log('资源初始化')

    // 返回清理函数
    return () => {
      console.log('资源已清理')
    }
  })
}

加载时输出:

插件开始加载
资源初始化

插件卸载时输出:

资源已清理

这里需要区分两个执行时机:

  • 注册阶段:ctx.effect() 接收到的函数立即开始执行。
  • 清理阶段:返回的处置函数在需要释放资源时执行。

因此,ctx.effect() 并不是一个只在卸载时执行的回调接口。

实例:管理定时器

下面使用 ctx.effect() 创建一个定时器,并在插件卸载时自动清理。

实例

// 文件:scratch-plugin/src/timer-plugin.ts

import type { Context } from '@deepseek-ai/cordis'

export const name = 'timer-plugin'

export function apply(ctx: Context) {
  ctx.effect(() => {
    // 创建定时器
    const timer = setInterval(() => {
      console.log('timer running')
    }, 1000)

    console.log('timer started')

    // 插件卸载时清理
    return () => {
      clearInterval(timer)
      console.log('timer stopped')
    }
  })
}

插件加载后,控制台会输出:

timer started
timer running
timer running
...

当插件卸载时,清理函数调用 clearInterval(timer) 停止定时器,并输出:

timer stopped

这种方式将资源创建和释放放在同一个位置,能够减少遗漏清理操作的风险。

处置器的执行顺序

插件可能注册多个需要清理的资源。

在 Fiber 卸载时,处置器会按照注册顺序的逆序开始执行。

例如:

实例

import type { Context } from '@deepseek-ai/cordis'

export function apply(ctx: Context) {
  ctx.effect(() => {
    console.log('注册资源 A')

    return () => {
      console.log('清理资源 A')
    }
  })

  ctx.effect(() => {
    console.log('注册资源 B')

    return () => {
      console.log('清理资源 B')
    }
  })
}

加载时输出:

注册资源 A
注册资源 B

卸载时,两个同步处置器通常按以下顺序执行:

清理资源 B
清理资源 A

这里的规则是逆序启动清理,而不是所有异步清理任务都严格串行完成。

异步清理需要注意什么?

如果处置器包含异步操作,框架会等待相关异步清理任务完成,但多个异步处置器不保证按照注册顺序逐个执行完毕。

例如,需要先关闭会话,再关闭数据库连接时,不应将两个步骤拆分为相互独立的异步处置器。

更可靠的方式是将存在顺序依赖的清理过程放在同一个处置器中。

实例

import type { Context } from '@deepseek-ai/cordis'

// 以下函数为示意,需由实际应用提供
declare function createConnection(): {
  startSession(): {
    close(): Promise<void>
  }
  close(): Promise<void>
}

export function apply(ctx: Context) {
  ctx.effect(() => {
    // 创建连接和会话
    const connection = createConnection()
    const session = connection.startSession()

    // 按顺序执行清理操作
    return async () => {
      await session.close()
      await connection.close()
    }
  })
}

以上代码中:

  1. 先执行 session.close()。
  2. 等待会话关闭完成。
  3. 再执行 connection.close()。

需要注意,createConnection() 是示意函数,并非 Cordis 内置 API。

手动卸载插件

除了依赖变化和热替换,Cordis 也支持主动卸载插件。

通过 ctx.plugin() 加载插件时,可以获取对应的 Fiber,并在需要时调用 dispose()。

实例

import type { Context } from '@deepseek-ai/cordis'

function myPlugin(ctx: Context) {
  console.log('child plugin loaded')

  ctx.effect(() => {
    return () => {
      console.log('child plugin cleaned up')
    }
  })
}

// 以下演示在已有 Context 中加载和卸载插件
async function demo(ctx: Context) {
  // 加载插件,得到 Fiber
  const fiber = ctx.plugin(myPlugin)

  // 手动卸载插件
  await fiber.dispose()

  console.log('child plugin disposed')
}

在插件已经正常加载并执行上述流程的情况下,输出为:

child plugin loaded
child plugin cleaned up
child plugin disposed

fiber.dispose() 返回 Promise,等待该 Promise 完成,表示相关卸载和异步清理操作已经结束。

它还会递归处置该插件拥有的子插件。

父插件与子插件

Cordis 允许插件通过 ctx.plugin() 加载其他插件。

每个子插件拥有自己的 Fiber,因此可以独立管理生命周期和资源。

例如:

实例

import type { Context } from '@deepseek-ai/cordis'

function childPlugin(ctx: Context) {
  console.log('子插件加载')

  ctx.effect(() => {
    return () => {
      console.log('子插件清理')
    }
  })
}

export function apply(ctx: Context) {
  console.log('父插件加载')

  // 注册子插件
  ctx.plugin(childPlugin)
}

子插件通过父插件的上下文创建,但拥有独立的运行实例。

当父插件卸载时,框架会同时处置其关联的子插件,避免子插件继续占用资源。

如果子插件需要独立提前卸载,可以保存 ctx.plugin() 返回的 Fiber,再调用它的 dispose()。

热替换(HMR)与生命周期

HMR(Hot Module Replacement,热模块替换)允许开发者在修改插件代码后更新运行中的插件,而不必每次都手动重启整个应用。

在 DeepSeek Harness 中,如果已经通过 cordis.yml 加载并配置 @deepseek-ai/dsh-hmr,修改受监控的插件源码后,可以触发热替换。

一次典型的热替换包含以下步骤:

  1. 检测插件源文件发生变化。
  2. 卸载旧插件实例。
  3. 移除旧实例注册的事件、工具等资源。
  4. 加载更新后的插件代码。
  5. 重新执行插件的初始化逻辑。

例如,一个插件注册了工具 my_tool,然后开发者修改了该工具的实现。

如果热替换成功,框架会先清理旧插件的工具注册,再加载新版工具,避免由于旧注册没有释放而产生重复注册问题。

但需要注意,热替换不代表插件内部状态会被自动持久化。对于必须保留的数据,应使用独立的状态存储或恢复机制。

实例:观察插件加载与卸载

下面通过一个最小示例观察插件加载及清理时的日志。

实例

// 文件:scratch-plugin/src/lifecycle-log.ts

import type { Context } from '@deepseek-ai/cordis'

export const name = 'lifecycle-log'

export function apply(ctx: Context) {
  console.log('runoob plugin loading')

  ctx.effect(() => {
    console.log('runoob effect registered')

    return () => {
      console.log('runoob effect cleaned up')
    }
  })
}

当插件被正常加载时,输出:

runoob plugin loading
runoob effect registered

插件卸载时,输出:

runoob effect cleaned up

从输出可以看出:

  • apply() 在插件加载阶段执行。
  • ctx.effect() 中的初始化逻辑立即执行。
  • ctx.effect() 返回的处置器在卸载时执行。

可以进一步在这个实例中加入定时器、事件监听器或子插件,观察资源在卸载时如何被释放。

常见问题

1、插件没有执行 apply()?

首先检查是否声明了 inject,以及对应的必需服务是否已经就绪。

如果依赖尚未就绪,插件可能停留在 PENDING 状态。

如果依赖已经满足,则应继续检查配置校验和插件初始化过程中是否发生异常。

2、插件卸载后定时器还在运行?

如果定时器是直接通过 setInterval() 创建的,而没有注册对应的清理函数,Cordis 可能无法自动管理它。

应将定时器创建过程放入 ctx.effect(),并返回调用 clearInterval() 的处置器。

3、调用 ctx.effect() 后,清理函数什么时候执行?

ctx.effect() 的执行函数会立即开始运行,返回的处置器则在插件卸载或提前释放该效果时执行。

处置器按照生命周期机制被管理,同一个资源不会因为重复处置而重复清理。

4、异步清理是否会按顺序完成?

不保证。

虽然处置器按照逆注册顺序开始调用,但多个异步处置器可能并发运行。

如果清理步骤存在顺序依赖,应放到同一个异步处置器中,并使用 await 控制执行顺序。

5、依赖服务恢复后,需要手动重新加载插件吗?

通常不需要。对于通过 Cordis 依赖机制管理的插件,当必需服务恢复后,框架可以重新加载相关插件实例。

不过,如果插件本身发生配置错误或初始化异常,仍需修复对应问题。


总结

Fiber 是 DeepSeek Harness 基于 Cordis 管理插件生命周期的核心运行时对象。

它负责跟踪插件的加载状态、依赖服务和注册资源,并在插件卸载时执行清理。

理解 Fiber 生命周期,需要掌握以下几个重点:

  • 状态管理:Fiber 包含 PENDING、LOADING、ACTIVE、FAILED、UNLOADING 和 DISPOSED 等状态。
  • 依赖加载:插件可以通过 inject 声明必需服务,等待依赖就绪后再加载。
  • 自动清理:通过 Cordis 生命周期管理的注册会在插件卸载时撤销。
  • ctx.effect():用于管理定时器、连接等需要自行释放的资源。
  • 异步处置:清理操作逆序启动,但异步任务不保证依次完成。
  • 插件卸载:可以通过 fiber.dispose() 主动卸载,或由依赖变化及 HMR 触发。

对于插件开发者来说,最重要的实践是:在获取资源时,同时定义如何释放资源。

只要正确使用 Cordis 提供的注册 API 和 ctx.effect(),就能减少插件重复加载、热替换和卸载过程中出现的资源泄漏问题。

官方参考资料: