Cordis 插件的依赖、生命周期与资源清理

举报
yd_229544978 发表于 2026/11/10 23:01:21 2026/11/10
【摘要】 DeepSeek Harness 20 天实战(05):Cordis 插件的依赖、生命周期与资源清理本文是「DeepSeek Harness 20 天实战」的第 5 篇。上一篇我们注册了一个工具;这一次,我们看看承载这些能力的 Cordis 插件是如何加载和卸载的。本文基于截至 2026 年 11 月的公开资料。DeepSeek Harness 仍处于开发者预览阶段,具体接口和行为可能随版...

DeepSeek Harness 20 天实战(05):Cordis 插件的依赖、生命周期与资源清理

本文是「DeepSeek Harness 20 天实战」的第 5 篇。上一篇我们注册了一个工具;这一次,我们看看承载这些能力的 Cordis 插件是如何加载和卸载的。

本文基于截至 2026 年 11 月的公开资料。DeepSeek Harness 仍处于开发者预览阶段,具体接口和行为可能随版本变化。

上一篇,我们用 ctx.tools.register() 向 Agent 注册了一个文本统计工具。

但还有几个问题没有展开:插件什么时候开始运行?它依赖的工具服务如果尚未准备好,会发生什么?插件卸载后,注册的工具和事件监听器又由谁清理?

这些问题指向 DeepSeek Harness 底层的插件框架:Cordis。

一、Cordis 在 DeepSeek Harness 中做什么?

Cordis 为插件提供了一套组织能力的方式。插件可以通过上下文 ctx 使用服务、注册事件监听器、挂载子插件,并管理需要清理的资源。

在 DeepSeek Harness 中,模型适配器、工具注册表、会话能力和默认 Agent Loop 都可以通过插件组合。启动时采用哪些能力,取决于当前加载的插件及其配置。

可以从三个角度理解 Cordis:

概念 作用
插件 向运行环境贡献能力或行为
服务 向其他插件提供可调用的能力
生命周期 管理插件加载、运行和卸载的过程

插件可以消费服务,也可以提供服务。例如,上一篇文章里的工具插件使用 ctx.tools 注册工具;tools 就是它需要的服务。

二、一个插件最基本的形态

在 Harness 的本地插件示例中,插件可以是一个导出 apply 函数的 TypeScript 模块:

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

export const name = 'my-plugin'

export function apply(ctx: Context) {
  console.log('[my-plugin] loaded')
}

加载插件时,框架会调用 apply,并传入 ctx。插件可以在这里注册工具、监听事件,或初始化自己管理的资源。

name 有助于标识插件;它不等同于插件注册的工具名称。一个插件可以注册工具,也可以提供其他类型的能力。

三、用 inject 声明服务依赖

如果插件需要使用工具服务,可以声明:

export const inject = ['tools']

这表示:插件需要等待 tools 服务就绪,再运行 apply。类似地,Harness 中的 llm、agents 和 sessions 也是可供相应插件使用的服务。

依赖声明影响的不只是启动顺序。如果运行期间某个必需服务消失,依赖它的插件会被卸载;当服务重新可用时,插件可以重新加载。

因此,排查“插件为什么没有执行”时,除了检查代码,还要检查当前插件组合是否提供了声明的服务。

四、插件实例有哪些生命周期状态?

Cordis 为加载的插件实例管理一个 Fiber。可以把 Fiber 理解为该实例的生命周期和资源管理范围。

常见状态包括:

状态 含义
PENDING 插件已声明,但必需依赖尚未就绪
LOADING 正在执行加载逻辑
ACTIVE 插件已加载并运行
FAILED 加载或配置校验失败
UNLOADING 正在卸载并清理资源
DISPOSED 插件实例已完成卸载

这组状态帮助我们区分两类问题:插件迟迟没有启动,可能是在等待依赖;插件启动时报错,则需要查看加载错误和配置。

五、哪些资源会自动清理?

插件卸载时,通过框架管理的注册通常会随所属插件一起清理。例如:

  • 通过 ctx.on() 注册的事件监听器。
  • 通过 ctx.plugin() 挂载的子插件。
  • 通过支持生命周期管理的 Harness 注册接口添加的能力,例如 ctx.tools.register() 注册的工具。

但普通 JavaScript 创建的资源,不会因为写在 apply 里就自动被框架识别。例如,直接调用 setInterval() 创建的定时器,需要提供对应的清理逻辑。

这时可以使用 ctx.effect():在回调中创建资源,并返回一个负责释放资源的函数。

ctx.effect(() => {
  const timer = setInterval(() => {
    console.log('working')
  }, 2000)

  return () => clearInterval(timer)
})

当所属插件卸载时,框架会调用返回的清理函数。网络连接、文件监听器等自主管理的资源,也需要以适合其 API 的方式释放。

六、实践:观察子插件的启动与卸载

下面的示例会挂载一个子插件。子插件每两秒输出一次心跳,同时监听会话事件;大约七秒后,父插件主动卸载这个子插件。

这个实验不需要发起模型请求。它假设你已经准备好 DeepSeek Harness 源码,并在仓库根目录完成 pnpm install 和 pnpm run build。

首先创建目录:

mkdir -p scratch-plugin/src

创建 scratch-plugin/src/day05-lifecycle.ts:

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

export const name = 'day05-lifecycle-demo'
export const inject = ['sessions']

function observeSession(ctx: Context) {
  console.log('[day05] child started')

  ctx.on('session/event', (session, event) => {
    if (event.type === 'turn/start') {
      console.log('[day05] turn started:', session.id)
    }
  })

  ctx.effect(() => {
    const timer = setInterval(() => {
      console.log('[day05] heartbeat')
    }, 2000)

    return () => {
      clearInterval(timer)
      console.log('[day05] timer cleared')
    }
  })
}

export function apply(ctx: Context) {
  const child = ctx.plugin(observeSession)

  ctx.effect(() => {
    const stopTimer = setTimeout(async () => {
      try {
        await child.dispose()
        console.log('[day05] child disposed')
      } catch (error) {
        console.error('[day05] child disposal failed', error)
      }
    }, 7000)

    return () => clearTimeout(stopTimer)
  })
}

代码中有两个生命周期:

  1. ctx.plugin(observeSession) 挂载子插件,并返回该子插件的 Fiber。
  2. 子插件内的 ctx.on() 注册会话事件监听器,ctx.effect() 管理心跳定时器。
  3. 父插件使用另一个 ctx.effect() 管理停止定时器,避免父插件提前卸载后,定时回调仍然触发。
  4. 调用 child.dispose() 后,子插件所属的监听器和定时器会被清理。等待其返回后,再输出卸载完成。

接着创建 scratch-plugin/day05.patch.yml,把路径替换为插件文件的实际绝对路径:

- insert:
    - id: day05-lifecycle-demo
      name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/day05-lifecycle.ts'

在仓库根目录启动:

pnpm dsh web --patch ./scratch-plugin/day05.patch.yml

如果之前的 Web 服务仍占用同一端口,先停止旧进程,或按当前版本的配置方式使用其他端口。

启动成功后,终端中应先出现 child started 和若干次 heartbeat。大约七秒后,会看到 timer cleared 和 child disposed;此后这个子插件不应再输出心跳。

心跳出现的准确次数可能受到启动和调度时间影响。七秒内如果恰好有会话开始新的 Turn,监听器还可能输出该会话的标识;这一步不是验证定时器清理所必需的。

七、生命周期管理中的几个常见误区

1. 把原生资源当成自动清理资源

ctx.on() 的监听器由框架管理,但普通 setInterval()、网络连接或第三方文件监听器需要自己的清理方式。应当确认资源由谁创建、由谁负责释放。

2. 假设所有异步清理都严格串行

清理函数会按注册的相反顺序开始执行,但多个异步清理函数可能并发运行。如果两个关闭步骤有严格先后关系,应把它们放在同一个清理函数中,并按顺序等待。

3. 假设所有运行配置都会自动热重载

热重载是否生效,取决于当前 Profile 和相关插件配置。不要仅凭修改源码就假设正在运行的实例已经更新;应检查当前配置及启动日志。

4. 把插件机制当成安全隔离

插件是在运行环境中执行的代码。生命周期管理可以帮助回收资源,但它本身不能证明插件可信,也不能替代权限控制和隔离措施。

八、这一篇需要记住什么?

Cordis 让插件能够通过服务依赖和生命周期协作:

  • inject 声明插件需要哪些服务。
  • apply 在依赖就绪后注册能力和资源。
  • Fiber 管理一个插件实例及其生命周期。
  • 框架管理的注册会随插件卸载而清理;自主管理的资源需要提供对应的释放逻辑。

理解这些机制后,再看上一篇的工具插件,就能明白它不仅要考虑“如何注册”,也要考虑“什么时候可用”和“什么时候退出”。

**下一篇预告:**我们将深入 Cordis 事件系统,区分普通通知与可拦截的处理链,并理解 next() 在其中的作用。

参考资料

  1. DeepSeek Harness Plugins and Lifecycle 文档
  2. DeepSeek Harness Services and Dependencies 文档
  3. DeepSeek Harness Cordis Lifecycle and Effects 教程
  4. DeepSeek Harness Your First Plugin 开发文档
  5. DeepSeek Harness 架构文档
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。