ByteNoteByteNote
Cordis 教程 6:组合配置与 HMR 热更新
字

字节笔记本

2026年10月6日 · 约 7 分钟读完

Cordis 教程 6:组合配置与 HMR 热更新

API中转
¥120

本文是 Cordis 系列的第六篇。Cordis 是 DeepSeek 开源的 agent 框架 deepseek-harness(github.com/deepseek-ai/deepseek-harness,MIT 协议)底层的插件化框架,这一篇讲三件事:怎么用 cordis.yml 组织应用的插件树,怎么让代码在保存的瞬间完成热替换,以及一个插件始终不加载、又不报任何错时该怎么定位。

配置条目不只是一个名字

cordis.yml 里的每个条目,除了 name 和 config,还接受更多元数据:

yaml
- id: greeter          # 稳定身份
  name: './greeter.ts'
- id: consumer
  name: './consumer.ts'
  disabled: true       # 保留条目,跳过挂载

id 给条目一个稳定身份,loader 因此能区分“修改了一个既有条目”和“删掉一个再加一个”。disabled: true 把条目留在文件里但不挂载;改回去,这个插件连同所有在等它服务的插件会一起重新加载。groups 可以把一组条目作为一个整体挂载和卸载;isolate 则让一个组拥有某个服务名的独立实例,两个组可以各自持有一份配置不同的 shell 提供方,互不影响。

热模块替换:保存即生效

Cordis 有两个基础性质:卸载插件时会释放它注册的全部 effect,加载则严格按依赖关系推进。把两者连起来,“先卸载、再加载”就能把一个正在运行的插件整体换掉。@deepseek-ai/cordis-plugin-hmr 做的就是这件事:监视文件,保存时自动完成这一循环。

在 cordis.yml 里加入 HMR 和它的两个伙伴:

yaml
- id: logger
  name: '@deepseek-ai/cordis-plugin-logger-console'
- id: timer
  name: '@deepseek-ai/cordis-plugin-timer'
- id: hmr
  name: '@deepseek-ai/cordis-plugin-hmr'
  config:
    root: ['.']
- id: hello
  name: './hello.ts'

为什么多挂了两个插件:HMR 的日志走 Cordis 的 logger 服务,没有 console 导出器就什么都看不到;它还 inject 了 timer 服务用来做保存去抖,缺了 timer 提供方,它会一直停在 PENDING,一声不吭。这种沉默正是下一节的主题。

HMR 通过 Loader 的原生辅助读取 Node 的 loader 内部机制,因此要在 tsx 下运行:

sh
node --import tsx vendor/cordis/bin.js

现在编辑 hello.ts,改一行日志再保存:

text
hello from my first plugin
[I] hmr watching [ '.' ]
[I] hmr reload plugin at hello.ts
hello from my EDITED plugin

旧实例先卸载,它注册的所有 effect 逐一回卷;新代码随后加载,apply 重新执行,全程不用重启进程。

Cordis 组合配置与 HMR 热重载

改 cordis.yml 本身同样会被捕获:loader 按 id 对条目做 diff,只挂载、卸载或重新配置真正变化的部分。这也解释了上面为什么每个条目都显式写了 id:不带 id 的条目每次读取都会拿到新生成的 id,任何一次配置编辑都会被当成“先删除再新增”,哪怕它的内容一字未动,也会被整体重挂。

诊断一个永远不加载的插件

依赖驱动加载有另一面:插件的 inject 点名了一个没人提供的服务,它就会永远等下去,一个字都不输出。这不是报错,PENDING 是合法状态,提供方可能只是晚一点才挂载。

好在状态可以直接查。每个上下文都能枚举插件注册表,写一个诊断插件:

ts
import { FiberState, type Context } from '@deepseek-ai/cordis'

export const name = 'diagnose'

export function apply(ctx: Context) {
  setTimeout(() => {
    for (const runtime of ctx.registry.values()) {
      for (const fiber of runtime.fibers) {
        if (fiber.state === FiberState.PENDING) {
          console.log(`${fiber.name} is PENDING: a required service is missing`)
        }
      }
    }
  }, 500)
}

再配一个依赖无法满足的插件:

ts
import type { Context } from '@deepseek-ai/cordis'

export const name = 'needs-timer'
export const inject = ['timer']

export function apply(ctx: Context) {
  console.log('needs-timer loaded')
}

把两者挂进配置运行,半秒后就能看到:

needs-timer is PENDING: a required service is missing

给 inject: ['timer'] 补上 @deepseek-ai/cordis-plugin-timer,插件立刻加载。经验法则:一个插件毫无动静又不报错时,先看它的 fiber 状态。不加 PENDING 过滤直接遍历注册表,还会看到 loader 自己的插件(Loader、Include)是 ACTIVE,因为配置文件本身也是以插件形式挂载的。

PENDING 静默等待的定位流程

小结

组合、热更新、诊断,其实是同一条线上的三件事:cordis.yml 决定插件树长什么样,id 让配置变化可以被 diff,依赖关系决定每个插件何时加载。HMR 把“改代码要重启”压缩成“保存即生效”,而 PENDING 枚举是依赖系统的必要补偿,机制静默的时候,得有办法自己开口问。整个项目在 GitHub 上以 MIT 协议开源,教程与源码可以对照着读。

相关文章

分享: