Cordis 插件的依赖、生命周期与资源清理
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)
})
}
代码中有两个生命周期:
ctx.plugin(observeSession)挂载子插件,并返回该子插件的 Fiber。- 子插件内的
ctx.on()注册会话事件监听器,ctx.effect()管理心跳定时器。 - 父插件使用另一个
ctx.effect()管理停止定时器,避免父插件提前卸载后,定时回调仍然触发。 - 调用
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() 在其中的作用。
参考资料
- DeepSeek Harness Plugins and Lifecycle 文档
- DeepSeek Harness Services and Dependencies 文档
- DeepSeek Harness Cordis Lifecycle and Effects 教程
- DeepSeek Harness Your First Plugin 开发文档
- DeepSeek Harness 架构文档
- 点赞
- 收藏
- 关注作者
评论(0)