DeepSeek Harness: 自动清理与 ctx.effect()

最后更新:2026-08-31

Cordis 最强大的设计之一是自动清理——插件卸载时,通过 ctx 注册的所有资源自动回收,无需手动释放。但当资源不由 ctx 直接管理时,ctx.effect() 就是你手动清理的入口。

💡 提示:自动清理是 Cordis 的安全网,ctx.effect() 是你的补充手段。原则是:能用 ctx 注册的就用 ctx,不能的才用 ctx.effect()。

📋 前置知识:已完成 11-first-plugin.md,理解 apply 函数和 Context

1. 你将学到


2. 自动清理原理

Effect 清理机制

(1) ctx 是资源注册中心

每个 ctx 实例维护一个注册表,记录该插件注册的所有可清理资源:

TYPESCRIPT
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

TYPESCRIPT
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 直接注册。例如:

这时就需要 ctx.effect()

TYPESCRIPT
ctx.effect(() => {
  // 返回清理函数
  return () => {
    // 清理逻辑
  }
})

(2) ▶ 示例 2

TYPESCRIPT
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

TYPESCRIPT
// 写法 1:返回清理函数(推荐)
ctx.effect(() => {
  const ws = new WebSocket('ws://localhost:8080')
  return () => ws.close()
})

// 写法 2:传入清理函数引用
const cleanup = () => { /* ... */ }
ctx.effect(cleanup)

写法 1 的优势是资源创建和清理在同一个闭包中,逻辑内聚。


4. 返回清理函数模式

(1) 标准模式

TYPESCRIPT
ctx.effect(() => {
  const resource = acquireResource()
  
  return () => {
    releaseResource(resource)
  }
})

这种"获取-释放"模式类似 try-finally:

TYPESCRIPT
// 等价的 try-finally 思维模型
try {
  const resource = acquireResource()
  // 使用 resource
} finally {
  releaseResource(resource)
}

(2) 多个资源的清理

TYPESCRIPT
export function apply(ctx: Context) {
  ctx.effect(() => {
    const db = openDatabase()
    const cache = openCache()
    
    return () => {
      cache.close()  // 先关依赖方
      db.close()     // 再关被依赖方
    }
  })
}

⚠️ 清理顺序很重要——先关闭依赖其他资源的对象,再关闭被依赖的资源。

(3) 清理函数中的错误处理

TYPESCRIPT
ctx.effect(() => {
  const conn = createConnection()
  return () => {
    try {
      conn.close()
    } catch (e) {
      ctx.logger.warn('cleanup error:', e)
    }
  }
})

清理函数中的异常不应中断其他资源的清理。Cordis 内部对每个清理函数都有 try-catch 保护,但显式处理更安全。


5. setInterval/setTimeout 的正确清理

(1) 自动清理方式(推荐)

TYPESCRIPT
export function apply(ctx: Context) {
  // 使用 ctx.setInterval — 自动清理
  ctx.setInterval(() => {
    ctx.logger.info('heartbeat')
  }, 30000)
}

(2) 原生 API + ctx.effect()

如果必须使用原生 setInterval

TYPESCRIPT
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 的陷阱

TYPESCRIPT
// ❌ 错误:卸载后 setTimeout 仍然触发
export function apply(ctx: Context) {
  setTimeout(() => {
    ctx.logger.info('delayed action') // 插件可能已卸载!
  }, 5000)
}
TYPESCRIPT
// ✅ 正确:使用 ctx.setTimeout
export function apply(ctx: Context) {
  ctx.setTimeout(() => {
    ctx.logger.info('delayed action') // 卸载后不会触发
  }, 5000)
}

6. 网络连接的清理模式

(1) HTTP 服务器

TYPESCRIPT
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 连接

TYPESCRIPT
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) 数据库连接池

TYPESCRIPT
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) 事件监听器清理

TYPESCRIPT
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) 忘记注册清理

TYPESCRIPT
// ❌ 泄漏:卸载后定时器仍在运行
export function apply(ctx: Context) {
  setInterval(() => {
    console.log('orphan timer')
  }, 1000)
}

修正:改用 ctx.setInterval 或注册 ctx.effect()

(2) 清理顺序错误

TYPESCRIPT
// ❌ 先关数据库,再关依赖数据库的缓存
ctx.effect(() => {
  const db = openDB()
  const cache = new Cache(db)
  return () => {
    db.close()      // 先关了 db
    cache.close()   // cache 内部访问 db 会报错
  }
})

修正:反向清理。

(3) 清理中抛出未捕获异常

TYPESCRIPT
// ❌ 清理函数抛异常,中断后续清理
ctx.effect(() => {
  return () => {
    throw new Error('cleanup failed')  // 其他 effect 可能不会执行
  }
})

修正:try-catch 包裹清理逻辑。

(4) 闭包引用过期

TYPESCRIPT
// ❌ 引用了外部变量,卸载后变量可能已失效
let globalRef: SomeObject | null = new SomeObject()

export function apply(ctx: Context) {
  ctx.effect(() => {
    return () => {
      globalRef!.cleanup()  // globalRef 可能已被其他代码置 null
    }
  })
}

修正:在 effect 闭包内捕获引用。


❓ 常见问题

Q ctx.effect() 的清理函数可以是 async 吗?
A 可以。Cordis 支持 async 清理函数,会 await 完成后再继续。但注意:如果清理耗时过长,会延迟整个插件的卸载。
Q 多个 ctx.effect() 的执行顺序是什么?
A 按注册的逆序执行(类似栈的 LIFO)。后注册的先清理,确保依赖关系正确。
Q 插件崩溃后资源会被清理吗?
A 会。Cordis 在插件异常退出时仍会尝试调用所有已注册的清理函数,包括 ctx.effect() 注册的。这是 Cordis 的安全保障。
Q 可以在 apply 外注册 ctx.effect() 吗?
A 技术上可以(只要持有 ctx 引用),但不推荐。在 apply 外注册的 effect 不属于任何插件生命周期,可能导致不可预期的清理行为。
Q ctx.on() 和 ctx.effect() 有什么区别?
A ctx.on() 注册事件监听器,卸载时自动移除。ctx.effect() 注册任意清理函数,卸载时调用。两者互补:ctx.on 处理事件,ctx.effect 处理其他资源。 ---

📖 小节


📝 作业

1. ⭐ 基础题:编写一个插件,用 ctx.setInterval 每秒输出一次计数,启动后确认卸载时定时器被正确清理。

2. ⭐⭐ 进阶题:编写一个插件,创建一个 HTTP 服务器监听 3456 端口,用 ctx.effect() 注册清理。启动后测试 HTTP 请求,然后卸载插件,确认端口已释放。

3. ⭐⭐⭐ 挑战题:编写一个插件,同时管理 WebSocket 连接和数据库连接池两种资源,确保清理时先关 WebSocket 再关数据库,并在清理函数中处理可能的异常。测试:故意在数据库关闭时抛出错误,验证 WebSocket 仍然被正确关闭。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏