DeepSeek Harness 的基石:从源码理解 Cordis

Cordis 是一个面向 TypeScript / Node.js 应用的元框架。它不规定你必须做机器人、Web 服务还是命令行工具;它提供的是更底层的能力:让插件、服务、事件、配置与资源生命周期能够安全地组合起来。

如果只看使用方式,Cordis 很简单:

scss 复制代码
constctx =newContext()

awaitctx.plugin(plugin)
ctx.emit('event-name')

但它真正的价值不在这几行 API,而在其运行机制:插件的依赖如何等待、服务如何按作用域解析、资源如何自动清理,以及服务变化后依赖插件为什么能够自动重载。

理解 Cordis,可以从四个对象开始:ContextFiberReflectRegistry

javascript 复制代码
Context(应用上下文)
├─ Registry:管理插件
├─ Reflect:管理服务
├─ Events:管理事件
└─ Fiber:管理一次插件运行的生命周期

Context:看起来是对象,实际上是服务入口

Cordis 中最常见的变量是ctx

csharp 复制代码
ctx.database
ctx.logger
ctx.on('message', callback)

表面上,它像一个普通 JavaScript 对象;但在实现上,Context会被Proxy包装。因此访问ctx.database时,框架并不只是读取一个对象字段,而会进行一次服务解析。

大致流程如下:

php 复制代码
读取 ctx.database
 ↓
Context 是否本身拥有该属性?
 ├─ 是:直接返回
 └─ 否:将 database 视为服务名
      ↓
   在当前插件 Fiber 中查找
      ↓
   沿父 Fiber 向上查找
      ↓
   找到对应作用域内的服务后返回

这使得插件不必关心服务实例在哪里创建,也不需要从其他文件手动导入一个全局单例。它只要使用ctx.database,Cordis 就会在当前上下文中寻找正确的服务。

这也是 Cordis 能支持多个服务实例、多个租户或多个机器人实例的基础。

源码入口:packages/core/src/context.ts在创建Context时建立 Proxy;packages/core/src/reflect.ts负责处理服务属性的读取与写入。

Fiber:一次插件安装,就是一个可管理的运行单元

调用:

ini 复制代码
constfiber =awaitctx.plugin(plugin)

Cordis 不会简单执行plugin(ctx)然后结束。它会创建一个Fiber

Fiber 可以理解为"这个插件的运行实例",它保存了:

  • 插件本身及其配置
  • 父级 Context
  • 声明的依赖服务
  • 插件当前状态
  • 该插件创建的全部资源
  • 该插件提供的全部服务

它的生命周期可以简化为:

复制代码
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED

其中:

  • PENDING:依赖尚未准备好,暂时不运行;
  • LOADING:依赖已满足,正在初始化;
  • ACTIVE:插件正在正常工作;
  • UNLOADING:依赖消失、配置更新或插件主动卸载;
  • DISPOSED:所有资源已释放。

所以ctx.plugin()返回 Fiber 很重要:

ini 复制代码
constfiber =awaitctx.plugin(plugin)

awaitfiber.dispose()

调用dispose()并不是"把插件函数关掉"这么简单,而是销毁该插件整个生命周期内建立的资源。

插件注册和 Fiber 创建的代码位packages/core/src/registry.ts,生命周期状态与执行逻辑位于packages/core/src/fiber.ts

inject:Cordis 的依赖注入是一套持续生效的依赖关系

普通依赖注入框架经常只在初始化时把对象传给你。Cordis 更进一步:它会持续观察依赖是否可用。

例如:

javascript 复制代码
constplugin = {
inject: ['database'],

apply(ctx) {
  ctx.database.query('SELECT 1')
 },
}

这里的inject不只是类型声明,它告诉 Cordis:只有database服务可用时,这个插件才可以运行。

如果数据库尚未安装,插件不会报错,也不会提前执行,而是留在等待状态:

复制代码
database 尚不存在
 ↓
plugin 保持 PENDING

当数据库服务出现:

scss 复制代码
database 已提供
 ↓
plugin 开始 LOADING
 ↓
执行 apply(ctx)
 ↓
plugin 进入 ACTIVE

如果数据库后来断开、卸载或被替换:

复制代码
database 失效
 ↓
依赖它的 plugin 卸载
 ↓
清理它创建的资源
 ↓
等待新的 database,或保持 PENDING

实现上,Fiber 会把依赖服务对应的运行实例 ID 组合成 epoch(版本标识)。只要依赖集合变化,epoch 就会变化,Fiber 会据此执行卸载或重新加载。

这意味着 Cordis 管理的不是一次性的对象注入,而是"服务可用性与插件运行状态"的关系。

Service:把能力提供到 Context 中

Cordis 通常通过Service暴露长期存在的能力:

typescript 复制代码
classDatabaseextendsService{
constructor(ctx:Context) {
 super(ctx,'database')
 }

query(sql:string) {
 // 执行查询
 }
}

安装后:

scss 复制代码
awaitctx.plugin(Database)

ctx.database.query('SELECT 1')

super(ctx, 'database')做的事情是把当前实例注册为database服务。之后其他插件只需声明:

vbnet 复制代码
inject: ['database']

然后就可以通过ctx.database使用它。

服务与 Fiber 是绑定的。如果提供数据库服务的插件被卸载,ctx.database也会自动失效;依赖数据库的插件则会随之重载或停止。

因此,Cordis 不鼓励无边界的全局变量。服务总有明确的提供者、作用域与生命周期。

effect:把副作用挂到生命周期树上

插件经常会创建必须清理的资源:

例如原生定时器:

ini 复制代码
consttimer =setInterval(task,1000)

如果插件卸载后忘记执行clearInterval(timer),定时器仍会继续运行,这就是资源泄漏。

Cordis 使用ctx.effect()解决这个问题:

scss 复制代码
ctx.effect(() =>{
consttimer =setInterval(task,1000)

return() =>{
 clearInterval(timer)
 }
},'periodic task')

其中返回的函数就是清理逻辑。

当所属插件被卸载时,Cordis 会自动执行它:

javascript 复制代码
安装插件
 ↓
创建定时器
 ↓
将 clearInterval 注册为 effect
 ↓
卸载插件
 ↓
自动执行 clearInterval

ctx.effect()支持同步清理函数、异步清理函数、生成器和异步生成器。Fiber 卸载时会以反向顺序释放已登记资源,保证后创建的资源先关闭。

事件监听也是同样的机制:

csharp 复制代码
ctx.on('message', callback)

Cordis 在内部把"取消这个监听器"的函数注册到当前 Fiber。因此插件卸载后,它注册的事件监听器不会残留。

如果插件还安装了子插件,资源关系会形成一棵树:

复制代码
父插件
├─ 事件监听
├─ 定时器
└─ 子插件
   └─ WebSocket

卸载父插件时,Cordis 会按反向顺序释放资源,确保后创建的依赖资源先被关闭。

isolate:同名服务为什么可以有多个实例

在很多应用中,同一个服务名不一定只对应一个实例。

例如,多租户系统中每个租户可能有独立数据库:

ini 复制代码
consttenantA = root.isolate('database')
consttenantB = root.isolate('database')

接着分别提供服务:

arduino 复制代码
tenantA.provide('database', databaseA)
tenantB.provide('database', databaseB)

然后:

arduino 复制代码
tenantA.database// databaseA
tenantB.database// databaseB

虽然两边都访问ctx.database,但底层并不是同一个服务。

Cordis 会为被隔离的服务名分配不同的内部标识,因此查找服务时能够确定当前 Context 应该看到哪个实例:

复制代码
root
├─ tenantA
│  └─ database → databaseA
│
└─ tenantB
   └─ database → databaseB

同一个插件可以被安装到两个上下文中:

scss 复制代码
awaittenantA.plugin(plugin)
awaittenantB.plugin(plugin)

插件代码无需判断自己属于哪个租户;它只需要依赖ctx.database。这就是作用域带来的组合能力。

Events:事件监听器也是有作用域的资源

Cordis 的事件系统支持:

  • emit():同步广播;
  • parallel():并发执行并等待;
  • serial():按顺序异步执行,得到第一个有效结果;
  • bail():同步短路;
  • waterfall()中间件式调用链。

监听器并不是简单地存放在全局数组中。每个监听器会记录它属于哪个 Context;在事件分发时,Cordis 会根据当前上下文和隔离规则决定哪些监听器可以响应。

这带来两个结果:

  1. 一个插件卸载后,它注册的监听器自动消失;
  2. 不同作用域的插件不会意外处理彼此的事件。

因此,事件系统与服务系统一样,都不是全局无边界的。

配置更新和热更新为什么可行

Cordis 的插件不是一次性执行完就失去控制。每个插件都有自己的 Fiber,因此当配置、依赖或代码发生变化时,框架可以执行一套确定的过程:

php 复制代码
旧 Fiber 卸载
 ↓
释放旧事件、计时器、连接与子插件
 ↓
保留新的配置或依赖状态
 ↓
重新执行插件
 ↓
生成新的资源树

这正是 LoaderHMR 能工作的前提。

Loader 负责根据配置文件找到并加载插件;HMR 负责在开发时发现模块变化并重装插件。它们之所以不容易产生重复监听、旧计时器残留等问题,是因为 Cordis 已经把副作用收拢到 Fiber 生命周期中。

总结

Cordis 的核心思想可以归纳为一句话:

把插件的依赖、服务、事件和副作用都放进可追踪的生命周期树中。

它通过:

  • Context + Proxy解析当前作用域中的服务;
  • Fiber代表一次插件运行;
  • inject维护服务依赖与插件状态;
  • effect自动追踪和释放副作用;
  • isolate让同名服务可以在不同上下文独立存在;
  • Events管理可作用域化、可自动取消的监听器。

所以 Cordis 的本质不是"提供一堆 API",而是提供一种应用组织方式:模块能被安装、等待依赖、提供能力、创建资源、重载,并在退出时干净地消失。

建议的源码阅读顺序

  1. packages/core/src/context.ts
  2. packages/core/src/registry.ts
  3. packages/core/src/fiber.ts
  4. packages/core/src/reflect.ts
  5. packages/core/src/events.ts
相关推荐
Leslie1651 小时前
模型调价之后,如何把 Token 账单变成可回滚的预算策略
人工智能
企鹅的企1 小时前
2027北京AI健康科技与智慧医疗展官方链接产业资源
人工智能·科技
独隅1 小时前
KMP 全栈进化:Koog 框架打造纯 Kotlin AI Agent 实战效果
开发语言·人工智能·kotlin
zhy295631 小时前
【DNN】Llama 3.2 1B模型LORA微调与QAIRT部署
人工智能·dnn·llama
腾视科技-AIoT1 小时前
私有云时代来临:AI NAS如何重塑你的数字生活?
人工智能·ai·生活·nas·ai算力模组·ainas·腾视科技
fthux1 小时前
装闭 RenoPit 源码解析(12):从AI分析结果到React避坑报告
人工智能·ai·开源·github·open source·renopit
老余说AI1 小时前
TikTok Shop东南亚上线“内容授权工具“,搬运内容可合法化
人工智能
o_insist1 小时前
从 Vue 生命周期与 Spring AOP 理解 LangChain Middleware
人工智能·agent
o_insist1 小时前
AI Agent 如何动态选择工具:Skill 匹配与三种筛选模式
人工智能·agent