DeepSeek Harness: 自动清理与 ctx.effect()
最后更新:2026-08-31
Cordis 最强大的设计之一是自动清理——插件卸载时,通过 ctx 注册的所有资源自动回收,无需手动释放。但当资源不由 ctx 直接管理时,ctx.effect() 就是你手动清理的入口。
📋 前置知识:已完成 11-first-plugin.md,理解 apply 函数和 Context
1. 你将学到
- 自动清理原理:ctx 注册的所有资源自动回收
ctx.effect():手动资源清理注册- 返回清理函数的模式
- setInterval/setTimeout 的正确清理
- 网络连接的清理模式
- 常见清理错误与规避
2. 自动清理原理
(1) ctx 是资源注册中心
每个 ctx 实例维护一个注册表,记录该插件注册的所有可清理资源:
class Context {
private _disposables: Disposable[] = []
register(disposable: Disposable) {
this._disposables.push(disposable)
}
dispose() {
for (const d of this._disposables.reverse()) {
d.dispose()
}
}
}
当插件卸载时,ctx.dispose() 按注册的逆序依次回收所有资源。
(2) 自动清理的资源类型
通过 ctx 注册的以下资源全部自动清理:
| 注册方式 | 清理行为 |
|---|---|
ctx.on('event', handler) |
移除事件监听器 |
ctx.setInterval(fn, ms) |
清除定时器 |
ctx.setTimeout(fn, ms) |
清除定时器 |
ctx.command('name') |
注销命令 |
ctx.service('name', impl) |
注销服务 |
(3) ▶ 示例 3
import { Context } from '@deepseek-ai/cordis'
export const name = 'auto-cleanup-demo'
export function apply(ctx: Context) {
// 以下注册全部自动清理
ctx.on('session/created', (s) => {
ctx.logger.info(`session: ${s.id}`)
})
ctx.setInterval(() => {
ctx.logger.info('tick')
}, 10000)
ctx.command('demo')
.action(() => 'demo command')
}
// 插件卸载时:监听器移除 + 定时器清除 + 命令注销,零手动代码
3. ctx.effect():手动资源清理
(1) 为什么需要手动清理
并非所有资源都能通过 ctx 直接注册。例如:
- 第三方库创建的连接(如 WebSocket、数据库连接池)
- 原生 Node.js API 创建的资源(如
net.Server) - 全局状态修改(如
process.env临时变量)
这时就需要 ctx.effect():
ctx.effect(() => {
// 返回清理函数
return () => {
// 清理逻辑
}
})
(2) ▶ 示例 2
import { Context } from '@deepseek-ai/cordis'
export const name = 'manual-cleanup'
export function apply(ctx: Context) {
const connection = createExternalConnection()
ctx.effect(() => {
return () => {
connection.close()
ctx.logger.info('connection closed')
}
})
}
ctx.effect() 接收一个工厂函数,该函数返回清理函数。插件卸载时,Cordis 调用清理函数释放资源。
(3) ▶ 示例 3
// 写法 1:返回清理函数(推荐)
ctx.effect(() => {
const ws = new WebSocket('ws://localhost:8080')
return () => ws.close()
})
// 写法 2:传入清理函数引用
const cleanup = () => { /* ... */ }
ctx.effect(cleanup)
写法 1 的优势是资源创建和清理在同一个闭包中,逻辑内聚。
4. 返回清理函数模式
(1) 标准模式
ctx.effect(() => {
const resource = acquireResource()
return () => {
releaseResource(resource)
}
})
这种"获取-释放"模式类似 try-finally:
// 等价的 try-finally 思维模型
try {
const resource = acquireResource()
// 使用 resource
} finally {
releaseResource(resource)
}
(2) 多个资源的清理
export function apply(ctx: Context) {
ctx.effect(() => {
const db = openDatabase()
const cache = openCache()
return () => {
cache.close() // 先关依赖方
db.close() // 再关被依赖方
}
})
}
⚠️ 清理顺序很重要——先关闭依赖其他资源的对象,再关闭被依赖的资源。
(3) 清理函数中的错误处理
ctx.effect(() => {
const conn = createConnection()
return () => {
try {
conn.close()
} catch (e) {
ctx.logger.warn('cleanup error:', e)
}
}
})
清理函数中的异常不应中断其他资源的清理。Cordis 内部对每个清理函数都有 try-catch 保护,但显式处理更安全。
5. setInterval/setTimeout 的正确清理
(1) 自动清理方式(推荐)
export function apply(ctx: Context) {
// 使用 ctx.setInterval — 自动清理
ctx.setInterval(() => {
ctx.logger.info('heartbeat')
}, 30000)
}
(2) 原生 API + ctx.effect()
如果必须使用原生 setInterval:
export function apply(ctx: Context) {
const timer = setInterval(() => {
ctx.logger.info('heartbeat')
}, 30000)
ctx.effect(() => {
return () => clearInterval(timer)
})
}
(3) 对比
| 方式 | 代码量 | 可靠性 | 推荐 |
|---|---|---|---|
ctx.setInterval |
1 行 | 高(自动) | ✅ |
原生 + ctx.effect() |
3 行 | 中(需手动) | ⚠️ |
| 原生(不清理) | 1 行 | 低(泄漏) | ❌ |
(4) setTimeout 的陷阱
// ❌ 错误:卸载后 setTimeout 仍然触发
export function apply(ctx: Context) {
setTimeout(() => {
ctx.logger.info('delayed action') // 插件可能已卸载!
}, 5000)
}
// ✅ 正确:使用 ctx.setTimeout
export function apply(ctx: Context) {
ctx.setTimeout(() => {
ctx.logger.info('delayed action') // 卸载后不会触发
}, 5000)
}
6. 网络连接的清理模式
(1) HTTP 服务器
import { createServer } from 'http'
export function apply(ctx: Context) {
const server = createServer((req, res) => {
res.end('ok')
})
server.listen(3456)
ctx.effect(() => {
return () => {
server.close()
ctx.logger.info('HTTP server closed')
}
})
}
(2) WebSocket 连接
import WebSocket from 'ws'
export function apply(ctx: Context) {
const ws = new WebSocket('ws://localhost:8080')
ws.on('open', () => {
ctx.logger.info('ws connected')
})
ctx.effect(() => {
return () => {
if (ws.readyState === WebSocket.OPEN) {
ws.close()
}
}
})
}
(3) 数据库连接池
import { Pool } from 'pg'
export function apply(ctx: Context) {
const pool = new Pool({
connectionString: 'postgresql://localhost/mydb',
max: 10
})
ctx.effect(() => {
return async () => {
await pool.end()
ctx.logger.info('db pool closed')
}
})
}
⚠️ 清理函数可以是异步的。Cordis 会 await 异步清理完成后再继续后续清理。
(4) 事件监听器清理
export function apply(ctx: Context) {
const emitter = getExternalEmitter()
const handler = (data: any) => {
ctx.logger.info('event:', data)
}
emitter.on('data', handler)
ctx.effect(() => {
return () => {
emitter.off('data', handler)
}
})
}
7. 常见清理错误
(1) 忘记注册清理
// ❌ 泄漏:卸载后定时器仍在运行
export function apply(ctx: Context) {
setInterval(() => {
console.log('orphan timer')
}, 1000)
}
修正:改用 ctx.setInterval 或注册 ctx.effect()。
(2) 清理顺序错误
// ❌ 先关数据库,再关依赖数据库的缓存
ctx.effect(() => {
const db = openDB()
const cache = new Cache(db)
return () => {
db.close() // 先关了 db
cache.close() // cache 内部访问 db 会报错
}
})
修正:反向清理。
(3) 清理中抛出未捕获异常
// ❌ 清理函数抛异常,中断后续清理
ctx.effect(() => {
return () => {
throw new Error('cleanup failed') // 其他 effect 可能不会执行
}
})
修正:try-catch 包裹清理逻辑。
(4) 闭包引用过期
// ❌ 引用了外部变量,卸载后变量可能已失效
let globalRef: SomeObject | null = new SomeObject()
export function apply(ctx: Context) {
ctx.effect(() => {
return () => {
globalRef!.cleanup() // globalRef 可能已被其他代码置 null
}
})
}
修正:在 effect 闭包内捕获引用。
❓ 常见问题
ctx.on() 注册事件监听器,卸载时自动移除。ctx.effect() 注册任意清理函数,卸载时调用。两者互补:ctx.on 处理事件,ctx.effect 处理其他资源。 ---📖 小节
- 自动清理是 Cordis 的核心特性:通过 ctx 注册的资源在卸载时自动回收
ctx.effect()用于手动资源的清理注册,返回清理函数- 清理函数按注册逆序执行(LIFO),注意依赖顺序
- 优先使用
ctx.setInterval/setTimeout而非原生 API - 网络连接、数据库连接池等必须用
ctx.effect()注册清理 - 清理函数中要 try-catch 防止异常中断后续清理
📝 作业
1. ⭐ 基础题:编写一个插件,用 ctx.setInterval 每秒输出一次计数,启动后确认卸载时定时器被正确清理。
2. ⭐⭐ 进阶题:编写一个插件,创建一个 HTTP 服务器监听 3456 端口,用 ctx.effect() 注册清理。启动后测试 HTTP 请求,然后卸载插件,确认端口已释放。
3. ⭐⭐⭐ 挑战题:编写一个插件,同时管理 WebSocket 连接和数据库连接池两种资源,确保清理时先关 WebSocket 再关数据库,并在清理函数中处理可能的异常。测试:故意在数据库关闭时抛出错误,验证 WebSocket 仍然被正确关闭。