第 1 卷
张嘉熙 · DeepSeek Harness 前沿工程实践



你好,我是张嘉熙。
前三讲,我们看着一棵树慢慢长成:根由 new Context() 造出(顺带也造出根 fiber),此后每调一次 ctx.plugin() 就长出一个节点,这棵树我们称之为插件树,也就是这一讲的主
角。
第一讲,它怎么长:ctx.plugin() 挂上可拆卸的枝条,ctx.effect() 声明可撤销的副作用。第二讲,树上怎么协作:inject 声明依赖,服务一上线就自动激活。第三讲,节点之
间怎么通信:事件在 ctx 上传播,五种派发方式各有分工。
机制都认识了,可现实中通常大量插件并存:谁挂在哪一层、各自带什么参数、怎么按租户分组——这些装配的活儿全交给代码一行行写死,就成了又长又机械的流水线:加个插件要改代
码,换个参数要改代码,多一个租户还是要改代码。
和谁隔离、谁拦截谁,全都写在这一份 YAML 里。你只需要描述“树应该长成什么样”,框架负责把它变成实物——这就是所谓声明式配置(cordis.yml + Loader),与前三讲那种“一
步步指挥框架干活”的命令式写法正好相对。前三讲我们用纯代码一步步看清底层,这一讲我们将重点转向 Cordis 的声明式 YAML 配置。
那么问题来了:一份 YAML,怎么就能长成一棵运行时插件树?我们先从一份最小的配置开始,把链路完整跑通。
从配置树到插件树
先跑起来:最小配置,最小插件
先不搞复杂。我们把 cordis.yml 改成最小版——只有一个条目:
复制代码 1# 最小版:只有一个条目,先把链路跑通*
2*- id: logger*
| 3 | * | name:'./plugins/logger.ts'* |
|---|---|---|
| 4 | * | config:* |
| 5 | * | level: info |
跟着我一步步把它扩充,最终版见本讲下半部分。
这里我们先分清这一讲会出现的两棵树:
配置树:写在 YAML 里的配置,它是图纸
插件树:等配置跑起来长成的那棵,它是实物
我们再看看这份配置对应的加载逻辑:建根、挂 Loader、把 include 当配置树第一根枝条挂
3import { fileURLToPath, pathToFileURL } from 'node:url' 4import Loader from '@deepseek-ai/cordis-plugin-loader'
5 6const baseUrl = pathToFileURL(fileURLToPath(new URL('.', import.meta.url))).href
7
| 8 | constctx =newContext() | //①造出根 |
|---|---|---|
| 9 | ctx.baseUrl= baseUrl | // 告诉它相对路径从哪找 |
| 10 | awaitctx.plugin(Loader, { baseUrl,enableLogs:true})//②挂上Loader:在ctx上 |
11
| 12 | //③把include插件本身当成配置树的第一根枝条挂上去: |
|---|---|
| 13 | // 它的config指向cordis.yml——include读文件、解析成条目列表 |
| 14 | // 再把这整棵配置树挂载成运行时插件树 |
| 15 | awaitctx.loader.create({ |
| 16 | name:'@deepseek-ai/cordis-plugin-include', |
| 17 | config: {path:'./cordis.yml'}, |
| 18 | }) |
19 20await new Promise((resolve) => setTimeout(resolve, 300))// 等所有条目挂完再退出
输出只有一行:
复制代码 1[logger] 启动(level=info)
就这一行,但整条链路已经走完了:YAML 里的一行条目 → 一个 Entry(配置树节点)→ 一次 registry.plugin() 调用 → 一个 Fiber(运行节点)→ 插件 apply() 跑起来 → 打
印。