【deepseek-harness】Cordis 开源项目深度介绍

Cordis 开源项目深度介绍

1. What:这是什么项目

项目定位

Cordis 是一个 TypeScript 元框架(Meta-Framework) ,全称"A Meta-Framework of Spatiotemporal Composability"------时空可组合性元框架。它解决的核心问题是:如何在运行时安全地加载、卸载、重配置组件,且保证副作用完全可逆、依赖关系自动响应

传统框架(Express、NestJS、Fastify)的组件组合是静态的------启动时加载、关闭时释放,运行期不可变。Cordis 把组合本身变成一等公民:组件可以在运行时任意插入、撤回、热替换,框架自动追踪每个组件的副作用并在卸载时逆向回滚,自动解析组件间的依赖拓扑并在依赖变化时通知相关方。

该项目有一篇配套学术论文 A Programming Paradigm for Spatiotemporal Composability,从范畴论角度形式化了"可逆效应(Revertible Effects)"和"反应式协效应(Reactive Coeffects)"两个概念,Cordis 是这套理论的工程实现。

核心能力

能力 说明
可逆效应追踪 ctx.effect() 注册的清理回调在组件卸载时自动逆序执行,保证资源释放、事件解绑、定时器清除
反应式依赖解析 @Inject() 声明依赖,依赖服务就绪时自动激活组件,依赖消失时自动挂起
声明式配置加载 YAML/JSON 配置文件驱动插件加载,支持配置分层叠加、运行时热更新
热模块替换(HMR) 开发期文件变更自动重载插件,不重启进程、不丢失运行时状态
事件系统 五种派发模式(emit/parallel/serial/bail/waterfall),支持跨组件通信
服务注册与发现 Service 基类 + Registry 自动注册,ctx.get()/ctx.set() 动态访问
隔离与拦截 ctx.isolate() 创建隔离上下文,ctx.intercept() 拦截配置层
Fiber 生命周期 组件级四态状态机(PENDING → LOADING → ACTIVE → DISPOSED),支持失败恢复

适用场景

  • 插件化应用:IDE 扩展、聊天机器人、CMS------需要运行时安装/卸载插件的场景
  • AI Agent 框架:DeepSeek Harness(DSH)即基于 Cordis 构建,"一切皆插件"架构
  • 可组合服务端:微服务网关、API 聚合层------需要动态路由和插件链
  • 开发工具链:需要 HMR 和配置热重载的开发时工具

2. Why:为什么选择这个项目

现存痛点

痛点一:插件卸载不干净

VSCode 等插件系统在卸载扩展时,无法完全回滚扩展注册的事件监听器、定时器、全局状态修改。用户只能重启进程来"彻底卸载",运行时状态全部丢失。

痛点二:依赖管理是静态的

传统 DI 容器(如 tsyringe、InversifyJS)在启动时一次性解析依赖图。如果运行时某个依赖服务被卸载或替换,依赖它的组件不会收到通知,继续访问已失效的引用导致崩溃。

痛点三:配置变更需要重启

修改配置文件 → 重启服务 → 丢失运行时状态。这个循环在开发期浪费大量时间,在生产期意味着停机。

项目优势

对比维度 传统框架(NestJS/Express) 插件系统(VSCode/Chrome Ext) Cordis
副作用回滚 手动 cleanup 有限支持 自动追踪 + 逆序回滚
依赖响应 静态注入 extensionDependencies(仅声明) 反应式:就绪激活、消失挂起
配置热更新 不支持 不支持 配置变更自动重载插件
HMR 需要额外工具 不支持 内置 chokidar 文件监听
理论基础 学术论文形式化保证
语言生态 TypeScript 各语言 TypeScript 原生

适合人群

  • 构建插件化应用的架构师和开发者
  • 需要 HMR 开发体验的 TypeScript 服务端项目
  • AI Agent 框架开发者(DSH 即典型用例)
  • 对编程语言理论感兴趣、想用形式化方法指导工程实践的团队

不适合的场景

  • 纯前端 UI 项目(Cordis 面向服务端/工具链,非 React/Vue 替代品)
  • 极简 API 服务(如果只有几个路由,Cordis 的抽象开销不划算)
  • 非 TypeScript 项目(核心 API 深度依赖 TS 类型系统和装饰器)
  • 需要极度稳定的生产环境------Cordis 当前版本 4.0.0-rc.8,API 尚未冻结,官方明确标注"under active development, API may change without notice"

3. How:核心工作原理

Cordis 的核心是一个 Context 对象和围绕它的三个机制:效应追踪协效应解析Fiber 生命周期

效应追踪(Temporal Composability)

每个组件通过 ctx.effect() 注册副作用(如打开的文件句柄、事件监听器、定时器)。Cordis 把这些副作用记录在一个与组件绑定的 DisposableList 中。当组件被卸载时,框架逆序执行所有清理回调,保证后注册的副作用先清理------这与栈的展开顺序一致。

复制代码
组件加载:
  ctx.effect(() => openFile())     → 注册 dispose: closeFile()
  ctx.effect(() => setInterval())  → 注册 dispose: clearInterval()

组件卸载:
  clearInterval()  ← 先清理后注册的
  closeFile()      ← 再清理先注册的

这套机制的学术名称是"可逆效应"------每个上下文变换都携带一个逆变换,运行时自动追踪和执行。

协效应解析(Spatial Composability)

组件通过 @Inject('serviceName') 声明对其他服务的依赖。Cordis 的 Registry 维护服务注册表,当被依赖的服务就绪时,依赖方自动激活;当被依赖的服务消失时,依赖方自动挂起。这不需要手动管理启动顺序------框架自动拓扑排序。

复制代码
组件 A @Inject('database')
  database 服务未就绪 → A 处于 PENDING 状态
  database 服务注册  → A 自动激活
  database 服务卸载  → A 自动挂起,等待重新就绪

Fiber 生命周期

每个组件实例对应一个 Fiber,管理其完整生命周期:

复制代码
PENDING → LOADING → ACTIVE → (reload) → LOADING → ACTIVE ...
                           → (unload) → UNLOADING → DISPOSED
                           → (error)  → FAILED → (retry) → LOADING ...

Fiber 持有组件的副作用列表、依赖状态、配置信息。热重载时,旧 Fiber 的副作用被清理,新 Fiber 重新加载------对使用者而言是"原地替换"。

Context Proxy

Context 本身是一个 ES Proxy。访问 ctx.databasectx.logger 等属性时,Proxy 拦截读取操作,自动从注册表解析对应服务。这使得依赖注入不需要显式调用 container.resolve()------直接访问属性即可,同时框架能追踪到"谁在什么时候访问了什么服务"。


4. 总体架构

#mermaid-svg-8CvbmDL8rUehUEkB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8CvbmDL8rUehUEkB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8CvbmDL8rUehUEkB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8CvbmDL8rUehUEkB .error-icon{fill:#552222;}#mermaid-svg-8CvbmDL8rUehUEkB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8CvbmDL8rUehUEkB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8CvbmDL8rUehUEkB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8CvbmDL8rUehUEkB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8CvbmDL8rUehUEkB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8CvbmDL8rUehUEkB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8CvbmDL8rUehUEkB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8CvbmDL8rUehUEkB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8CvbmDL8rUehUEkB .marker.cross{stroke:#333333;}#mermaid-svg-8CvbmDL8rUehUEkB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8CvbmDL8rUehUEkB p{margin:0;}#mermaid-svg-8CvbmDL8rUehUEkB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8CvbmDL8rUehUEkB .cluster-label text{fill:#333;}#mermaid-svg-8CvbmDL8rUehUEkB .cluster-label span{color:#333;}#mermaid-svg-8CvbmDL8rUehUEkB .cluster-label span p{background-color:transparent;}#mermaid-svg-8CvbmDL8rUehUEkB .label text,#mermaid-svg-8CvbmDL8rUehUEkB span{fill:#333;color:#333;}#mermaid-svg-8CvbmDL8rUehUEkB .node rect,#mermaid-svg-8CvbmDL8rUehUEkB .node circle,#mermaid-svg-8CvbmDL8rUehUEkB .node ellipse,#mermaid-svg-8CvbmDL8rUehUEkB .node polygon,#mermaid-svg-8CvbmDL8rUehUEkB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8CvbmDL8rUehUEkB .rough-node .label text,#mermaid-svg-8CvbmDL8rUehUEkB .node .label text,#mermaid-svg-8CvbmDL8rUehUEkB .image-shape .label,#mermaid-svg-8CvbmDL8rUehUEkB .icon-shape .label{text-anchor:middle;}#mermaid-svg-8CvbmDL8rUehUEkB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8CvbmDL8rUehUEkB .rough-node .label,#mermaid-svg-8CvbmDL8rUehUEkB .node .label,#mermaid-svg-8CvbmDL8rUehUEkB .image-shape .label,#mermaid-svg-8CvbmDL8rUehUEkB .icon-shape .label{text-align:center;}#mermaid-svg-8CvbmDL8rUehUEkB .node.clickable{cursor:pointer;}#mermaid-svg-8CvbmDL8rUehUEkB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8CvbmDL8rUehUEkB .arrowheadPath{fill:#333333;}#mermaid-svg-8CvbmDL8rUehUEkB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8CvbmDL8rUehUEkB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8CvbmDL8rUehUEkB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8CvbmDL8rUehUEkB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8CvbmDL8rUehUEkB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8CvbmDL8rUehUEkB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8CvbmDL8rUehUEkB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8CvbmDL8rUehUEkB .cluster text{fill:#333;}#mermaid-svg-8CvbmDL8rUehUEkB .cluster span{color:#333;}#mermaid-svg-8CvbmDL8rUehUEkB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8CvbmDL8rUehUEkB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8CvbmDL8rUehUEkB rect.text{fill:none;stroke-width:0;}#mermaid-svg-8CvbmDL8rUehUEkB .icon-shape,#mermaid-svg-8CvbmDL8rUehUEkB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8CvbmDL8rUehUEkB .icon-shape p,#mermaid-svg-8CvbmDL8rUehUEkB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8CvbmDL8rUehUEkB .icon-shape .label rect,#mermaid-svg-8CvbmDL8rUehUEkB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8CvbmDL8rUehUEkB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8CvbmDL8rUehUEkB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8CvbmDL8rUehUEkB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 上层应用
外部依赖
插件层 (@cordisjs/*)
Cordis 核心层 (cordis core)
Context

Proxy 对象·统一入口
Fiber

组件生命周期·副作用追踪
Registry

插件注册·依赖声明
EventsService

事件派发·五种模式
ReflectService

属性拦截·服务发现
LoggerService

日志服务
Service 基类

服务抽象基类
plugin-loader

声明式配置加载
plugin-hmr

热模块替换
plugin-include

配置文件包含/补丁
plugin-group

插件分组
plugin-timer

定时器服务
plugin-logger-console

控制台日志输出
cosmokit

工具函数库
@standard-schema

配置校验规范
chokidar

文件监听
DeepSeek Harness

AI Agent 框架
其他 Cordis 应用

机器人/工具链

核心组件解析

Context(上下文)

Cordis 的统一入口。它是一个 Proxy 对象,拦截所有属性读写:读取 ctx.database 时自动从 Registry 解析数据库服务;写入 ctx.set('myService', instance) 时自动注册到 Registry。ctx.extend() 创建子上下文(继承原型链),ctx.isolate() 创建隔离上下文(服务实例互不影响)。Context 本身不存储业务状态,它是服务访问的"总线"。

Fiber(纤维)

每个插件实例对应一个 Fiber,是效应追踪的物理载体。Fiber 内部维护一个 DisposableList------组件通过 ctx.effect() 注册的清理回调都追加到这个列表。Fiber 有四态状态机(PENDING/ACTIVE/DISPOSED/FAILED),_reload() 方法执行热重载(清理旧副作用→重新执行插件函数),_unload() 方法执行卸载(逆序清理所有副作用)。Fiber 的设计确保了:无论组件如何加载/卸载/重载,副作用永远不泄漏

Registry(注册表)

管理所有已注册插件和服务的元数据。@Inject() 装饰器声明的依赖在此解析。Registry 维护 Inject 映射表------当新服务注册时,检查是否有待激活的依赖方;当服务卸载时,通知所有依赖方挂起。插件有三种形态:函数插件 Plugin.Function、构造器插件 Plugin.Constructor、对象插件 Plugin.Object(含 apply 方法),Registry 统一处理。

EventsService(事件服务)

五种派发模式:emit(fire-and-forget)、parallel(并行等待全部完成)、serial(串行等待依次完成)、bail(第一个非空返回值即停止)、waterfall(上一步返回值传给下一步)。事件监听器也通过 ctx.effect() 注册,组件卸载时自动解绑------不会产生事件泄漏。

ReflectService(反射服务)

Context Proxy 的 handler 实现层。拦截 get 操作时,从注册表查找服务实例;拦截 set 操作时,触发服务注册/更新。它还负责 mixin(将服务的方法混入 Context 原型,使 ctx.setTimeout() 直接可用)和 accessor(自定义属性读写逻辑)。

Service(服务基类)

所有服务的抽象基类。继承 Service 并在构造函数中调用 super(ctx, 'serviceName') 即自动注册到 Context。Service 支持 Config schema(基于 @standard-schema 规范)做配置校验,resolveConfig() 方法合并拦截层配置。Service 可以是 callable 的(可当函数调用),通过 createCallable 实现。

plugin-loader(加载器)

声明式配置的入口。读取 YAML/JSON 配置文件,按配置实例化插件,管理配置树(EntryTree)。支持运行时配置变更------修改配置文件后,Loader 自动 diff 变更项,只重载受影响的插件。Loader 内部使用 Node.js 22+ 的 ModuleLoader API(无需 --expose-internals 标志)实现模块加载。

plugin-hmr(热模块替换)

基于 chokidar 监听文件变更。变更发生时,通过 ModuleLoader 的依赖关系图定位受影响的插件,只重载这些插件而非全量重启。外部依赖(node_modules)变更触发全量重载,用户代码变更触发局部重载。依赖 plugin-timer 做防抖。

数据流转流程

复制代码
1. 启动:  Loader 读取配置文件 → 解析配置树 → 逐个实例化插件
2. 注册:  插件调用 super(ctx, 'name') → Registry 记录服务 → ReflectService 更新 Context Proxy
3. 注入:  @Inject('dep') 声明 → Registry 检查 dep 是否就绪 → 就绪则激活,未就绪则 PENDING
4. 运行:  组件通过 ctx.effect() 注册副作用 → Fiber 记录到 DisposableList
5. 通信:  组件通过 ctx.emit/on 触发和监听事件 → EventsService 派发
6. 重载:  配置变更/文件变更 → Loader diff → 旧 Fiber._unload() → 新 Fiber 实例化
7. 卸载:  Fiber._unload() → 逆序执行 DisposableList → Registry 移除服务 → 通知依赖方挂起

5. 部署与安装

前置环境要求

依赖 最低版本 说明
Node.js ≥ 22 Cordis 使用 Node 22+ 的 ModuleLoader 内部 API
包管理器 yarn 4+ 或 pnpm 10+ 仓库使用 yarn 4.14.1(corepack 启用)
TypeScript ≥ 5.9(开发时) 运行时不需要,但类型定义依赖 TS 5.x

方式一:创建新项目(推荐)

sh 复制代码
# 使用官方脚手架创建 Cordis 应用
npx create-cordis my-app

# 进入项目目录
cd my-app

# 安装依赖
yarn install   # 或 pnpm install

# 启动应用
yarn start

脚手架会交互式询问模板类型、包管理器偏好,自动生成项目骨架和配置文件。

方式二:在现有项目中安装

sh 复制代码
# 安装 Cordis 核心
npm install cordis

# 安装常用插件(按需)
npm install @cordisjs/plugin-loader    # 声明式配置加载
npm install @cordisjs/plugin-hmr       # 热模块替换(开发时)
npm install @cordisjs/plugin-timer     # 定时器服务
npm install @cordisjs/plugin-logger-console  # 控制台日志

# 安装工具库
npm install cosmokit

方式三:从源码构建

sh 复制代码
# 克隆仓库
git clone https://github.com/cordiverse/cordis.git
cd cordis

# 启用 corepack(确保 yarn 4 可用)
corepack enable

# 安装依赖
yarn --no-immutable

# 构建(esbuild + tsc 双输出)
yarn build

# 运行测试
yarn test

部署后验证

sh 复制代码
# 验证 cordis 核心包可导入
node -e "import('cordis').then(m => console.log('Cordis loaded:', Object.keys(m).join(', ')))"

# 预期输出类似:
# Cordis loaded: Context, Service, Fiber, Inject, ...

创建最小应用验证:

typescript 复制代码
// app.ts
import { Context } from 'cordis'

const ctx = new Context()

// 注册一个简单插件
ctx.plugin((ctx) => {
  ctx.effect(() => {
    console.log('插件已加载')
    return () => console.log('插件已卸载')
  })
})

// 卸载验证
setTimeout(() => {
  ctx.fiber.dispose()
  console.log('Context 已销毁')
}, 1000)
sh 复制代码
# 运行
npx tsx app.ts

# 预期输出:
# 插件已加载
# 插件已卸载      ← effect 回调自动执行
# Context 已销毁

如果"插件已卸载"正确打印,说明效应追踪机制工作正常。


6. 快速上手使用实战

最小 Demo:插件化计数器

typescript 复制代码
// counter.ts ------ 定义一个 Service 插件
import { Service, Inject, Context } from 'cordis'

export class CounterService extends Service {
  private count = 0

  constructor(ctx: Context) {
    super(ctx, 'counter')  // 注册为 ctx.counter
  }

  increment() {
    this.count++
    this.ctx.emit('counter/change', this.count)
    return this.count
  }

  get value() {
    return this.count
  }
}

// app.ts ------ 主应用
import { Context } from 'cordis'
import { CounterService } from './counter'

const ctx = new Context()

// 注册插件
ctx.plugin(CounterService)

// 使用服务
ctx.on('counter/change', (value) => {
  console.log(`计数器变更: ${value}`)
})

ctx.counter.increment()  // 输出: 计数器变更: 1
ctx.counter.increment()  // 输出: 计数器变更: 2
console.log(ctx.counter.value)  // 输出: 2

声明式配置加载

使用 plugin-loader 实现配置驱动的插件管理:

yaml 复制代码
# cordis.yml ------ 配置文件
$loader:
  baseUrl: .

# 注册定时器服务
$timer: {}

# 注册日志服务
$logger-console: {}

# 注册自定义插件
my-plugin:
  $name: ./plugins/my-plugin
  greeting: Hello, Cordis!
  interval: 5000

# 注册另一个插件,依赖 timer
another-plugin:
  $name: ./plugins/another
  $inject:
    - timer
  message: Running every 10s
typescript 复制代码
// app.ts ------ 加载配置并启动
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import Timer from '@cordisjs/plugin-timer'
import LoggerConsole from '@cordisjs/plugin-logger-console'

const ctx = new Context()
ctx.plugin(Timer)
ctx.plugin(LoggerConsole)
ctx.plugin(Loader, { baseUrl: import.meta.dirname })

// 读取并加载 cordis.yml
ctx.loader.readConfig('cordis.yml')

// 热重载: 修改 cordis.yml 后自动生效

依赖注入实战

typescript 复制代码
import { Service, Inject, Context } from 'cordis'

// 数据库服务
class DatabaseService extends Service {
  data: Record<string, any> = {}

  constructor(ctx: Context) {
    super(ctx, 'database')
  }

  async set(key: string, value: any) {
    this.data[key] = value
    this.ctx.emit('database/write', key, value)
  }

  async get(key: string) {
    return this.data[key]
  }
}

// 缓存服务,依赖数据库
@Inject('database')
class CacheService extends Service {
  cache: Map<string, any> = new Map()

  constructor(ctx: Context) {
    super(ctx, 'cache')
    // database 就绪后自动激活
  }

  async get(key: string) {
    if (this.cache.has(key)) return this.cache.get(key)
    const value = await this.ctx.database.get(key)
    this.cache.set(key, value)
    return value
  }
}

// 使用
const ctx = new Context()

ctx.plugin(CacheService)   // 先注册 Cache,但它依赖 database,处于 PENDING
ctx.plugin(DatabaseService) // 注册 database → Cache 自动激活

await ctx.cache.get('user:1')  // 正常工作,内部调用 database

HMR 热重载配置

yaml 复制代码
# cordis.yml
$loader:
  baseUrl: .

$timer: {}
$logger-console: {}

$hmr:
  $name: @cordisjs/plugin-hmr
  $inject:
    - loader
    - timer
  watch:
    - ./plugins/**/*.ts    # 监听 plugins 目录下的文件变更

启动后,修改 ./plugins/ 下任何 .ts 文件,Cordis 自动重载受影响的插件------无需重启进程,运行时状态保留。

常用 API 速查

API 用途 示例
ctx.plugin(fn) 注册插件 ctx.plugin((ctx) => { ... })
ctx.effect(fn) 注册副作用 ctx.effect(() => { const t = setInterval(...); return () => clearInterval(t) })
ctx.on(name, cb) 监听事件 ctx.on('ready', () => console.log('ready'))
ctx.emit(name, ...args) 触发事件 ctx.emit('custom-event', data)
ctx.get(name) 获取服务 ctx.get('database')
ctx.set(name, value) 注册服务 ctx.set('myService', instance)
ctx.extend(meta) 创建子上下文 const child = ctx.extend({ foo: 'bar' })
ctx.isolate(name) 隔离服务 const isolated = ctx.isolate('database')
ctx.intercept(name, config) 拦截配置 ctx.intercept('logger', { level: 'debug' })
ctx.fiber.dispose() 卸载当前组件 清理所有注册的副作用

常见踩坑点

1. ctx.effect() 必须返回清理函数

typescript 复制代码
// ❌ 错误:没有返回清理函数,定时器永远不会被清理
ctx.effect(() => {
  setInterval(() => console.log('tick'), 1000)
})

// ✅ 正确:返回清理函数
ctx.effect(() => {
  const timer = setInterval(() => console.log('tick'), 1000)
  return () => clearInterval(timer)
})

2. @Inject() 的服务名必须与 super(ctx, 'name') 一致

typescript 复制代码
// 服务注册名
class MyService extends Service {
  constructor(ctx: Context) {
    super(ctx, 'myService')  // 注册名是 'myService'
  }
}

// 依赖声明
@Inject('myService')  // 必须匹配 'myService',大小写敏感
class Consumer extends Service { ... }

3. Node.js 版本必须 ≥ 22

plugin-loader 使用 Node 22+ 的 ModuleLoader 内部 API。Node 20 及以下会报错。如果无法升级 Node 版本,使用 Cordis 3.x 旧版本(但功能受限)。

4. 配置校验是同步的

当前版本(rc.8)的 Standard Schema 校验仅支持同步验证。如果配置校验需要异步操作(如从远程拉取密钥),需要在插件内部处理,不能放在 Config schema 中。

5. 事件监听器在组件卸载时自动解绑------但仅限通过 ctx.on() 注册的

typescript 复制代码
// ✅ 自动解绑
ctx.on('event', callback)

// ❌ 不会自动解绑(直接操作 EventEmitter)
process.on('SIGINT', callback)  // 需要手动在 effect 中清理

7. 总结与展望

项目价值总结

Cordis 的核心价值在于:把"动态组合"从工程难题降级为声明式配置。传统方案中,运行时插件加载/卸载/热替换是高风险操作------副作用泄漏、依赖断裂、状态不一致是家常便饭。Cordis 通过效应追踪(自动回滚副作用)和协效应解析(自动响应依赖变化),让这些操作变得安全且可预测。

它不是又一个 Web 框架,而是一个构建框架的框架------DeepSeek Harness 用它构建了完整的 AI Agent 平台,证明了这套抽象在生产级复杂度下的有效性。配套的学术论文提供了形式化保证,这在 TypeScript 生态中极为罕见。

社区与版本信息

项目 信息
GitHub https://github.com/cordiverse/cordis
npm cordis(164 个版本,最新 4.0.0-rc.8
许可证 MIT
作者 Shigma(同时也是 Koishi 聊天机器人框架的作者)
Node.js 要求 ≥ 22
学术论文 A Programming Paradigm for Spatiotemporal Composability
文档 cordis-primer
上层应用 DeepSeek Harness(DSH)、Koishi

后续发展方向

从 git 分支和 commit 历史可以观察到几个方向:

  • 可重入的 Fiber 生命周期feat/reentrant-fiber-lifecycle 分支)------允许在 Fiber 卸载过程中重新加载,解决热重载时的竞态条件
  • 惰性入口配置解析fix/lazy-entry-config-resolution 分支)------延迟解析配置,优化启动性能
  • 性能优化 ------近期 commit 中有 perf(core): avoid binding callbacks in event dispatch,持续优化事件派发性能
  • API 稳定化------当前处于 RC 阶段,正式 4.0.0 发布后将冻结 API

Cordis 的定位是一个长期演进的底层基础设施。随着 DSH 生态的扩展和 Koishi 等项目的验证,Cordis 有望成为 TypeScript 生态中"动态可组合应用"的标准底座。

相关推荐
随风随梦自在逍遥2 小时前
在 DGX Spark 上部署 MiniMax-H3(记录安装流程)
ai·minimax
d6760158632 小时前
免费开源的视频剪辑工具-UniCut
ai·开源·视频·剪辑
wangruofeng2 小时前
2000+ 小时实战后,我的 Agentic Engineering 全套装备「精译」
aigc·agent·ai编程
阿图灵3 小时前
MakerHub 开发报告:v1.0.0 → v1.1.0(单日 26 提交,图片渲染、目录跟随与数据真实化)
前端·vue·个人网站·deepseek·开发报告
tachibana23 小时前
如何设计多 Agent 的协作与动态切换机制?
网络·人工智能·ai·大模型·llm·agent
ddshub_cc3 小时前
GPT Image 2 Prompt 案例集:可直接套用的出图写法
gpt·ai·prompt·文生图·image2·ai生图·gpt-image-2
啊阿狸不会拉杆3 小时前
《计算机网络-自顶向下方法》1.3 网络核心 读书笔记
网络·人工智能·计算机网络·ai·php
DeepAgent3 小时前
AI Agent 工程实践(39):第一次实现——先做一个最小 Agent
大数据·人工智能·agent