DeepSeek Harness 插件定时器实战:@deepseek-ai/cordis-plugin-timer 的 Disposal-Aware 定时 API 全解
【免费下载链接】deepseek-harness
DeepSeek Harness: Everything is a Plugin.
本篇技术指南围绕 DeepSeek Harness 仓库中 vendored 的 @deepseek-ai/cordis-plugin-timer 插件展开,它向 Cordis 上下文注入了一组随插件生命周期自动清理的定时器 API(timeout / interval / throttle / debounce)。在 DeepSeek Harness 的插件体系中,任何定时任务——心跳上报、超时兜底、节流限频、防抖收尾——只要注册在插件 fiber 上,就会在插件被卸载、热重载或配置变更时被框架自动清除,彻底杜绝"定时器泄漏"与"回调作用在已销毁插件上"的经典隐患。读完本文,你将掌握该定时器服务的安装注册方式、四个核心 API 的双重形态(回调形态与 Promise/异步迭代器形态)、废弃别名的兼容策略,以及其在 Cordis fiber/effect 机制下的底层清理原理。
为什么要一个 "Disposal-Aware" 的定时器
在 Node.js / 浏览器环境中,原生 setTimeout / setInterval 返回的是一个句柄,你需要手动调用 clearTimeout / clearInterval 来释放。而在 Cordis 这类插件化框架里,插件的生命周期由框架统一管理:插件可能因配置修改、热重载(HMR)、显式卸载或依赖服务缺失而被销毁(详见 Cordis 生命周期与 effect 教程)。如果插件在 apply 里创建了一个原生定时器,却没有在卸载时清理,就会出现两类问题:
- 资源泄漏:定时器句柄一直挂在事件循环上,插件反复重载会导致定时器越积越多;
- 野回调(zombie callback):定时器在插件销毁后才触发,回调访问的上下文、服务或 DOM 可能已经不存在,造成难以排查的运行时错误。
@deepseek-ai/cordis-plugin-timer 解决这个问题的思路是:把定时器注册为当前 fiber 上的一个 effect。如 插件 README 所述:"Timer handles are registered on the current fiber, so they are cleared automatically when the plugin that created them is disposed."(定时器句柄注册在当前 fiber 上,因此当创建它们的插件被销毁时会自动被清除。)
在 DeepSeek Harness 中,这个插件作为 vendored 依赖存放在 vendor/timer 目录下,包名为 @deepseek-ai/cordis-plugin-timer(见 vendor/timer/package.json,当前版本 1.1.3),其 peer dependency 为 @deepseek-ai/cordis。
安装与注册
在 Cordis 应用中,定时器不是内置服务,而是一个需要显式装载的插件。基本用法如下(继承自 README 的示例):
import { Context } from 'cordis'
import Timer from '@deepseek-ai/cordis-plugin-timer'
const root = new Context()
await root.plugin(Timer)
const dispose = root.timeout(() => {
root.logger.info('done')
}, 1000)
dispose()
要点说明:
Timer是TimerService的默认导出(见 vendor/timer/src/index.ts#L147),作为一个插件装载到根上下文(或任意子上下文)即可生效;- 装载后,
ctx.timeout、ctx.interval、ctx.throttle、ctx.debounce四个方法会被直接混入上下文(同时混入的还有废弃别名ctx.setTimeout、ctx.setInterval); - 装载后还注册了名为
timer的 Service 实例,可通过ctx.timer访问(见 vendor/timer/src/index.ts#L3-L7 的类型声明)。
从源码结构看,混入行为由构造函数中的 ctx.mixin('timer', [...]) 完成(vendor/timer/src/index.ts#L15),这是 Cordis 将服务方法扁平化注入上下文的统一机制;TimerService extends Service(vendor/cordis/src/service.ts)则保证了服务实例随其所属 fiber 卸载时自动注销。
由于定时器并非 Builtin 服务,在 DeepSeek Harness 插件开发规范中,插件需要先声明对 timer 的依赖再使用(详见下文"在 Harness 插件中使用"一节)。
API 全解:四种定时器,两种形态
README 用一张表格概括了全部 API,这里逐一展开并深入源码实现。
| API | 说明 |
|---|---|
ctx.timeout(callback, delay) | 延迟 delay 毫秒后执行一次回调,返回一个 disposer(释放函数) |
ctx.timeout(delay) | 返回一个在 delay 毫秒后 resolve 的 Promise |
ctx.interval(callback, delay) | 每隔 delay 毫秒重复执行回调,返回一个 disposer |
ctx.interval(delay) | 返回一个每次间隔产生一个 tick 的异步迭代器 |
ctx.throttle(callback, delay, noTrailing?) | 返回一个节流函数,附带 .dispose() |
ctx.debounce(callback, delay) | 返回一个防抖函数,附带 .dispose() |
ctx.setTimeout() 与 ctx.setInterval() 作为 ctx.timeout() 与 ctx.interval() 的废弃别名保留(源码中带有 @deprecated 标记,见 vendor/timer/src/index.ts#L18-L26),仅做委托转发,不引入新行为。
ctx.timeout:一次性延迟
timeout 有两个重载(vendor/timer/src/index.ts#L29-L54):
回调形态 timeout(callback, delay):返回一个 disposer。底层通过 this.ctx.effect() 注册:
const dispose = this.ctx.effect(() => {
const timer = setTimeout(() => {
dispose()
callback()
}, delay)
return () => clearTimeout(timer)
}, 'ctx.timeout()')
值得注意的细节是:回调触发前会先调用 dispose() 自身,即定时器触发后这个 effect 就被认为已结束,随后才执行业务回调——这意味着一次性的 timeout 在触发后即自动释放,无需手动清理。而在插件卸载时,effect 返回的 clearTimeout(timer) 会被 fiber 执行,将尚未触发的定时器取消。
Promise 形态 timeout(delay):返回一个 Promise<void>,它在 delay 毫秒后 resolve。若插件在定时器触发前被销毁,该 Promise 会被 reject,错误信息为 'Context has been disposed':
const { promise, resolve, reject } = Promise.withResolvers<void>()
const dispose = this.ctx.effect(() => {
const timer = setTimeout(resolve, delay)
return () => {
clearTimeout(timer)
reject(new Error('Context has been disposed'))
}
}, 'ctx.timeout()')
return promise.finally(dispose)
从实现看,返回值还通过 promise.finally(dispose) 与 disposer 绑定,保证 Promise 落定后 effect 一并释放。这一形态非常适合"等待一段时间再继续"的流程,例如给某次异步调用设置超时兜底:await Promise.race([doWork(), ctx.timeout(5000)])——但需要注意此时竞速失败方可能是一个 rejected Promise,应配合 catch 处理上下文销毁场景。
ctx.interval:周期执行
interval 同样有两个重载(vendor/timer/src/index.ts#L57-L104):
回调形态 interval(callback, delay):返回 disposer,内部是标准 setInterval + 卸载时 clearInterval,并以 ctx.interval() 作为 effect 标签。
异步迭代器形态 interval(delay):返回一个 AsyncIterableIterator<void>,每次间隔产生一个 undefined tick。这是最" Cordis 风格"的用法——可以直接在 for await 中消费:
for await (const _ of ctx.interval(1000)) {
// 每秒执行一次
}
从源码看,该迭代器是手写的(非生成器),关键行为包括:
- 卸载时:底层 effect 的清理函数会
clearInterval(timer),并把待定的next()Promise 以Error('Context has been disposed')reject 掉,同时记录done = { kind: 'throw', ... },使后续的next()调用持续抛出同一错误; - 迭代器自身实现了
return()与throw()方法:调用它们会终止迭代、resolve 待定的next()并立即dispose(),从而在for await中break或提前退出时也能干净地释放定时器(vendor/timer/src/index.ts#L87-L98)。
也就是说,for await 循环正常结束、被 break 或迭代器被 throw 时,定时器都会随之停止;只有插件被销毁这一路径是以 reject 形式终止迭代的。在使用时建议对可能抛出的 'Context has been disposed' 错误做捕获(例如在循环外加 try/catch),或者依赖上层框架的错误日志。
ctx.throttle:节流
throttle(callback, delay, noTrailing?) 返回一个节流后的函数,该函数保留原回调的全部参数类型,并附带 .dispose() 方法(vendor/timer/src/index.ts#L121-L136)。
节流语义(与 _schedule 辅助函数配合,见 vendor/timer/src/index.ts#L106-L118):
- 以
lastCall = -Infinity初始化,因此首次调用立即执行; - 之后每次调用会计算
remaining = delay - now + lastCall:- 若
remaining <= 0,立即执行回调并更新lastCall; - 否则安排一个延迟
remaining的定时器来补执行(trailing 执行);
- 若
noTrailing = true时,若当前仍处于节流窗口内,则不安排补执行,直接丢弃该次调用(源码中else if (!isDisposed)分支在noTrailing为真时不会进入——isDisposed初始值即传入的noTrailing)。
const throttled = ctx.throttle((n: number) => {
console.log('processing', n)
}, 500)
// 在 500ms 窗口内多次调用,最多执行首尾两次
throttled(1)
throttled(2)
throttled(3)
ctx.debounce:防抖
debounce(callback, delay) 返回一个防抖后的函数,附带 .dispose()(vendor/timer/src/index.ts#L139-L144)。每次调用都会 clearTimeout 并重新安排 delay 毫秒后的执行,因此只有停止调用 delay 毫秒后回调才会真正执行——典型的"输入停顿后收尾"场景:
const debounced = ctx.debounce(() => {
console.log('save')
}, 300)
debounced()
debounced() // 重置定时器,300ms 内未再调用才触发 save
防抖/节流函数之所以"Disposal-Aware",是因为其内部定时器同样通过 ctx.effect 注册:插件卸载时,_schedule 注册的清理函数会把 isDisposed 置为 true 并 clearTimeout,此后即使有人误调用该函数,也不会再触发原生回调(防抖分支直接 return,节流分支因 isDisposed 不再补执行)。wrapper.dispose = dispose 也允许在插件存活期间手动提前释放。
底层原理:fiber、effect 与自动清理
理解这四个 API 的自动清理能力,关键在于 Cordis 的 fiber 与 effect 机制。
fiber:插件运行实例
在 Cordis 中,每个加载的插件实例都拥有一个 fiber(见 vendor/cordis/src/fiber.ts),它承载插件的依赖状态、校验后的配置与生命周期。fiber 沿着如下状态机迁移(详见 docs/cordis-tutorial/02-lifecycle-and-effects.md):
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
↘ FAILED
插件卸载(_unload)时,fiber 会执行其注册的所有 disposer(vendor/cordis/src/fiber.ts#L675-L696),定时器插件的清理函数就在这批 disposer 中。
effect:把"框架不管理的资源"交给框架
Cordis 的 ctx.effect(execute, label) 用于包裹框架不原生管理的资源:execute 在装载时立即执行,其返回的 disposer 被收集,并在调用返回的 disposer 或 fiber 卸载时(以先到者为准)逆序执行(vendor/cordis/src/fiber.ts#L415-L561)。disposer 可以是异步的,卸载过程会等待其落定;若在已销毁的 fiber 上调用 effect,会抛出 CordisError('INACTIVE_EFFECT')。
TimerService 的每一个定时器 API 都是 ctx.effect 的一层封装,effect 标签分别为 'ctx.timeout()'、'ctx.interval()'、'ctx.throttle()'、'ctx.debounce()'——这些标签会出现在 fiber.getEffects() 的诊断树中,便于排查泄漏。从实现看,"当前 fiber"即 this.ctx 所属的 fiber:插件在 apply(ctx) 中调用 ctx.timeout(...),定时器就注册在该插件自己的 fiber 上,从而实现了"谁创建、谁拥有、随谁清理"的生命周期绑定。
这也是 docs/cordis-api/inherited.md 中将 ctx.timer 及其四个混入方法列为 Cordis 核心 API 之一的原因:timer 服务在运行时提供,四个 helper 通过 Pick 声明直接混入 ctx。
为什么这是正确做法
对比 Cordis 教程第 2 章 的手写范式:
ctx.effect(() => {
const timer = setInterval(() => console.log('tick'), 200)
return () => {
clearInterval(timer)
console.log('heartbeat cleaned up')
}
})
定时器插件只是把这套范式封装成了语义化 API,并额外提供了 Promise / 异步迭代器、节流、防抖等常用形态。插件作者无需自己记忆"用完要 clear、卸载要 dispose"的约定,框架保证热重载、配置变更、依赖丢失等所有卸载路径都覆盖到。
在 DeepSeek Harness 插件中使用
DeepSeek Harness 的插件开发规范(cordis-plugin-development SKILL)对定时器使用提出了明确的工程约束,可直接作为实战参考:
-
宿主端与客户端行为一致:在 host 与 client 两个平台上,定时器都是名为
timer的 Service(接口相同),但它不是 Builtin。使用前应通过Service.listService查询{ "service": "timer" },并在插件中声明inject: ['timer']后再使用定时器 mixin——这与 Cordis 的依赖注入机制(见 docs/cordis-tutorial/03-services.md)一致,能保证插件在定时器服务可用前不会提前激活。 -
一次性延迟(UI 交互场景,如点击后延迟反馈):
return {
inject: ['timer'],
apply(ctx) {
const onClick = () => {
ctx.timeout(() => console.log('done'), 300)
}
// 将 onClick 交给某个 Slot 的 UI
},
}
- React 组件内的周期任务(利用组件卸载与 fiber 卸载的生命周期对应关系):
return {
inject: ['timer'],
apply(ctx) {
function Clock() {
React.useEffect(() => ctx.interval(() => console.log('tick'), 1000), [])
return React.createElement('div', null, 'Running')
}
// 在某个 Slot 中注册 Clock
},
}
- 不要制造进程级/页面级副作用:规范明确要求不要在模块顶层或
apply()之外创建全局定时器;插件停止、更新或移除后,其所有贡献都必须被移除。凡是 Cordis 的 Service、Tool、Slot、timer 与 theme API 返回的 disposer,都应妥善保留以便释放。
在 Harness 仓库内部,定时器效果同样被广泛用于需要"等待外部资源安静下来"的实现中——例如 packages/acp/acp/src/index.ts 中 ctx.effect(() => quiesce, 'acp.connection') 这类把异步清理包装为 effect 的模式,其思想与定时器插件的 effect 封装一脉相承。
最佳实践与注意事项
结合 README 的 API 契约与 vendor/timer/src/index.ts 的实现细节,总结如下实践要点:
- 回调形态务必使用返回的 disposer:
timeout/interval的回调形态会返回一个 disposer。插件存活期间想提前取消,直接调用它即可;不调用也没关系,卸载时框架会代为清理。但不要再调用原生的clearTimeout去操作内部句柄——那是未公开的实现细节。 - Promise 形态的销毁语义是 reject:
ctx.timeout(delay)在上下文销毁时会以'Context has been disposed'拒绝,for await (const _ of ctx.interval(delay))也会在销毁时抛出该错误。在长生命周期应用中,建议对这类路径做兜底捕获,避免未处理的 Promise rejection。 throttle首调用立即执行,末尾补执行可控:noTrailing缺省为false(有 trailing 补执行);传true则窗口内的后续调用被直接丢弃。根据业务对"最后一次调用是否必须生效"的要求选择合适的取值。debounce只保末尾:高频触发时只有停止触发后的一次回调会执行,适合保存、搜索建议、输入校验等场景;若业务要求"至少以固定频率执行",应改用throttle。- deprecated 别名:
ctx.setTimeout/ctx.setInterval只是兼容旧代码的别名,新代码应使用ctx.timeout/ctx.interval;这两个别名未来可能被移除。 - 与 effect 的标签配合排障:定时器 effect 以
ctx.xxx()命名,若怀疑定时器泄漏,可通过 fiber 的getEffects()诊断接口检查当前存活的 effect 树。
参考资源
- @deepseek-ai/cordis-plugin-timer README(本文主体文档)
- TimerService 源码实现(含
timeout/interval/throttle/debounce与_schedule的全部实现细节) - timer 包清单(包名、版本、peerDependency 与导出结构)
- Cordis fiber 与 effect 实现(
ctx.effect、disposer 逆序执行、INACTIVE_EFFECT) - Cordis 生命周期与 effect 教程(fiber 状态机与"哪些注册本身已是 effect")
- Cordis 继承 API 清单(
ctx.timer在核心 API 中的位置) - Harness 插件开发规范:定时器章节(
inject: ['timer']与宿主/客户端双平台用法)
【免费下载链接】deepseek-harness
DeepSeek Harness: Everything is a Plugin.
更多推荐



所有评论(0)