第 7 章 动手实战(三):配置、组合、HMR 与诊断

本章对应官方教程第 5、6 章,覆盖三件事:配置校验、把 cordis.yml 当作应用来组合与热重载、诊断"永远不加载"的插件。

7.1 配置校验

cordis.yml 中的每个配置项都可携带 config 块;插件声明一个 schema,在运行 apply 之前验证它。配置错误会导致加载失败并给出准确报错——插件绝不会在配置不完整时启动。

创建 config-demo.ts:

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

export const name = 'config-demo'

export interface Config {
  greeting: string
  targets: string[]
}

export const Config: Schema<Config> = Schema.object({
  greeting: Schema.string().default('Hello'),
  targets: Schema.array(String).default(['world']),
})

export function apply(ctx: Context, config: Config) {
  for (const target of config.targets) {
    console.log(`${config.greeting}, ${target}!`)
  }
}

关键点:导出的 Config 既是 TypeScript 接口,也是同名的运行时 schema——消费方获得类型,Cordis 获得验证器。dsh 使用 Schemastery;Cordis 本身接受任意 Standard Schema 验证器,因此把普通对象导出为 Config 是不行的。

配置并运行:

- name: './config-demo.ts'
  config:
    targets: ['alpha', 'beta']
Hello, alpha!
Hello, beta!

未提供 greeting,schema 默认值补齐——apply 始终收到完整且经过验证的配置。

明确报错

传入无效内容:

- name: './config-demo.ts'
  config:
    targets: 'not-an-array'
ValidationError: invalid config:
  - $.targets expected array but got not-an-array (at targets)

插件的 fiber 进入 FAILED 状态,启动器打印错误后以状态码 1 退出。若配置通过了 schema 但引用的资源/提供方不可用,插件也应在能解析该引用时立即拒绝。

计算得到的配置值:!!js

- name: './config-demo.ts'
  config:
    greeting: !!js process.env.DEMO_GREETING ?? 'Hello'

!!js 仅在 config 与条目 disabled 字段内有效;disabled: !!js ... 在每次挂载决策时基于 loader 上下文求值,可按平台/环境门控一行配置;其余元数据保持静态。

7.2 组合:配置项不只有 name

Cordis 配置项还接受其他元数据:

- id: greeter          # 稳定标识
  name: './greeter.ts'
- id: consumer
  name: './consumer.ts'
  disabled: true       # 保留条目但跳过挂载
  • id 为条目提供稳定标识,让 loader 能区分"修改现有条目"与"先删除再添加"。

  • disabled: true 卸载插件但不删除条目;改回后插件及所有因依赖其服务而 PENDING 的插件都会重新加载。

  • 组(group)可嵌套一份子列表并作为整体加载/卸载;isolate 为组提供某项服务名的独立实例——两个组可以各自看到配置不同的 shell 提供方,互不影响。

7.3 热模块替换(HMR)

卸载会释放 effect,加载遵循依赖关系,因此 HMR 可以"先卸载、再加载"来替换运行中的插件。@deepseek-ai/cordis-plugin-hmr 监视文件并在保存时执行这一过程。

- 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'

两个陷阱要注意:

  1. HMR 通过 Cordis logger 服务记录日志——没有 console logger 就看不到它的消息;

  2. HMR 会 inject timer 服务做去抖——没有 timer 插件它就永远停在 PENDING,且不发出任何提示。

在 tsx 下运行,编辑 hello.ts 保存:

hello from my first plugin
2026-07-22 15:44:36 [I] hmr watching [ '.' ]
2026-07-22 15:44:39 [I] hmr reload plugin at hello.ts
hello from my EDITED plugin

旧实例先卸载(effect 回卷),新代码加载,apply 重新运行。编辑 cordis.yml 本身也会触发更新:loader 按 id 比较条目,只挂载/卸载/重配发生变化的部分。这就是要显式写 id 的原因——不带 id 的条目每次读取都会获得新 id,配置文件任何编辑都会让它被视为"先删除再添加"而重新挂载。

7.4 诊断:永远无法加载的插件

依赖驱动加载的另一面:如果 inject 指定了无人提供的服务,插件会一直等待、不输出任何内容。这不是错误——PENDING 是合法状态,提供方可能稍后才挂载。

创建 diagnose.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)
}

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

// needs-timer.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')
}
- name: './needs-timer.ts'
- name: './diagnose.ts'

运行输出:

needs-timer is PENDING — a required service is missing

修复:向列表添加 - name: '@deepseek-ai/cordis-plugin-timer',插件即加载。

排查口诀:插件既不执行也不报错 → 检查 fiber 状态。不加 PENDING 过滤遍历注册表时,还会看到 Loader、Include 自身处于 ACTIVE——配置文件本身也是通过插件挂载的。


依据官方教程第 5、6 章(05-config、06-composition-and-hmr)整理。

DeepSeek Harness / 第 7 章 动手实战(三):配置、组合、HMR 与诊断 0 字 0 行 cosolar
2026-09-17T10:12:13.987568996Z 2026-09-17T10:39:03.730434253Z