DeepSeek Harness Cordis 教程:从零到接入真实 harness

本教程通过 7 个可运行的动手示例,带你掌握 Cordis 插件框架,并最终把插件接入真实的 harness 服务。


一、Cordis 是什么?

Cordis 是 DeepSeek Harness 底层的插件框架:它是一个小型运行时,其中每项能力——包括工具(tool)、 LLM 适配器、文件访问,乃至 agent loop(智能体循环)本身——都是挂载到共享上下文中的插件

换句话说,你可以把整个 Harness 想象成一张由插件组成的树:

每个插件通过一个 apply(ctx) 函数描述自己贡献的内容,而 cordis.yml 负责把这些插件"组合"成一个应用。

本教程面向 agent 开发者。你不需要深入掌握 TypeScript,每章都会给出确切的命令和预期输出。


二、准备工作(Setup)

克隆仓库并安装依赖:

1
git clone https://github.com/deepseek-ai/deepseek-harness.git cd deepseek-harness pnpm install

本教程不需要 API 密钥,所有示例均可在无密钥环境中运行。

创建各章使用的临时目录(tmp/ 已被 git 忽略,不会进入版本控制):

1
mkdir -p tmp/cordis-tutorial cd tmp/cordis-tutorial

每一章都从该目录运行同一条命令:

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

这个单文件启动器(vendor/cordis/bin.js)会:

  1. 创建根 Context
  2. 挂载 Loader 插件;
  3. 让 Loader 从当前目录加载 ./cordis.yml

其余所有内容——包括有哪些插件、如何配置它们——都来自你稍后将编写的 YAML 文件。--import tsx 标志让 Node 无需构建步骤即可运行配置所指向的 TypeScript 文件。


三、第 1 章:你的第一个插件

插件是一个函数,由 loader 挂载。创建 hello.ts

1
import type { Context } from '@deepseek-ai/cordis' export const name = 'hello' export function apply(ctx: Context) { console.log('hello from my first plugin') }

name 导出项是可选的显示元数据,用于在诊断信息中标识插件。

接着创建 cordis.yml 来组合应用:

1
- name: './hello.ts'

cordis.yml 是一组 Cordis 配置项的列表。name 是模块指定符,可以是相对路径或 NPM 包名。各项会并发启动,所以列表顺序不保证加载先后;加载顺序由服务依赖(inject)决定,而非文件中的位置。

运行:

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

预期输出:

hello from my first plugin

当没有任何内容继续运行时,进程会自行退出。整个过程中,你的文件里没有框架启动代码:插件只描述自己的贡献,cordis.yml 负责组合应用。例如 dsh base 就是一份更长的插件组合。

其他两种插件形态

函数是最常见的形式,但 Cordis 接受三种形态:

1
import { Service, type Context } from '@deepseek-ai/cordis' // 1. Function plugin(函数插件,刚写的那种) export function apply(ctx: Context) {} // 2. Object plugin:一个带 apply 方法的对象 export const objectPlugin = { name: 'object-plugin', apply(ctx: Context) {}, } // 3. Class plugin:Service 子类(第 3 章会介绍) export class MyService extends Service { constructor(ctx: Context) { super(ctx, 'myTutorialService') } }

在你需要公开服务之前,请一直使用函数形态;第 3 章会介绍何时应当使用类形态。

尝试制造错误

apply 抛出异常:

1
export function apply(ctx: Context) { throw new Error('apply exploded') }

再次运行:进程会因该错误而终止。插件加载失败会明确报错,不会仅跳过该配置项

一个例外:如果某个配置项的模块无法被解析(例如路径或包名拼写错误),Cordis 会通过 logger 服务报告错误,而不会使进程崩溃——而且这条报告在启动阶段可能早于 logger 导出器开始观察而丢失。如果新增配置项似乎没有任何效果,请先检查拼写。


四、第 2 章:生命周期与 effect

Cordis 插件可能因修改配置、热重载、显式资源释放或所需服务消失而卸载。通过 Cordis API 建立的注册属于 effect,会在所属插件卸载时自动撤销;在这些 API 之外管理的资源必须包装在 ctx.effect() 中。

Effect

对于 Cordis 尚未管理的资源——例如定时器、连接或 watcher——应将其包装在 ctx.effect() 中并返回 disposer(资源释放函数)。

创建 lifecycle.ts

1
import type { Context } from '@deepseek-ai/cordis' export const name = 'lifecycle-demo' function heartbeat(ctx: Context) { console.log('heartbeat plugin loading') ctx.effect(() => { const timer = setInterval(() => console.log('tick'), 200) return () => { clearInterval(timer) console.log('heartbeat cleaned up') } }) } export function apply(ctx: Context) { // Mount a child plugin and keep its fiber to dispose it later. const fiber = ctx.plugin(heartbeat) // The demo timer is itself an effect: if THIS plugin is unloaded first, // the pending callback is cancelled instead of firing on a dead app. ctx.effect(() => { const timer = setTimeout(async () => { await fiber.dispose() console.log('disposed') process.exit(0) }, 700) return () => clearTimeout(timer) }) }

cordis.yml 指向该文件:

1
- name: './lifecycle.ts'

运行后会得到:

heartbeat plugin loading
tick
tick
tick
heartbeat cleaned up
disposed

请留意三点:

  • ctx.plugin(heartbeat) 会把一个来自代码的函数挂载为插件,这与 YAML loader 为每个配置项执行的操作相同。函数插件不需要 apply 方法,Cordis 会直接调用该函数;只有对象形态才要求 apply 方法。调用会返回一个 fiber,即一个已加载插件实例的运行时句柄。
  • effect 主体在加载期间运行;它返回的 disposer 在卸载期间运行。对于生命周期与插件一致的资源,你绝不需要自行调用 disposer。
  • fiber.dispose() 会等该插件的所有清理工作(包括异步 disposer)完成后才结束,并递归卸载它挂载的所有子插件。

Fiber 状态机

每个已加载插件实例都拥有一个 fiber,并在以下状态之间转换:

PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
                 ↘ FAILED
  • PENDING:已经声明,但所需服务(第 3 章)尚不可用。
  • LOADING / ACTIVEapply 正在运行 / 已经完成。
  • FAILEDapply 或配置校验抛出异常。
  • UNLOADING / DISPOSED:disposer 正在运行 / 一切均已拆除。

你会在第 6 章再次遇到 PENDING,它通常就是"为什么我的插件没有输出"的答案。

已经属于 effect 的操作

你很少需要亲自编写 ctx.effect(),因为内置注册 API 本身已经是 effect:

  • ctx.on(event, listener):监听器会在卸载时移除(第 4 章);
  • ctx.plugin(child):子插件会随父插件一同 dispose;
  • 服务注册属于 effect,ctx.tools.register(...) 等 harness 注册表也会把返回的 disposer 附着到调用插件上,因此会自动撤销(第 7 章)。

顺序注意事项:disposer 会按注册顺序的逆序启动,但多个异步 disposer 会并发运行。如果拆除步骤必须按顺序执行,请把它们放在同一个 disposer 中,并在其中依次等待每步完成。


五、第 3 章:服务

服务是一个插件提供、其他插件通过 ctx 消费的具名能力。在 harness 中,ctx.toolsctx.llmctx.agents 都是服务。消费方只指定 'tools' 之类的能力,而不导入其提供方,因此配置可以选择提供方,无需修改消费方

提供服务

创建 greeter.ts

1
import { Service, type Context } from '@deepseek-ai/cordis' declare module '@deepseek-ai/cordis' { interface Context { greeter: GreeterService } } export class GreeterService extends Service { constructor(ctx: Context) { super(ctx, 'greeter') } greet(who: string) { return `Hello, ${who}!` } } export const name = 'greeter' export function apply(ctx: Context) { ctx.plugin(GreeterService) }

两部分协同工作:

  • 运行时super(ctx, 'greeter') 以名称 greeter 注册该实例。此后,任何插件都可以通过 ctx.greeter 访问它。注册属于 effect,卸载提供方时会移除该服务。
  • 编译时declare module '@deepseek-ai/cordis' 块使用 TypeScript 声明合并,把 greeter 加入 Context 接口,使 ctx.greeter 在各处都能通过类型检查。它不会生成代码;没有该声明时,服务在运行时仍能工作,但消费方会失去类型安全。

Service 子类本身就是插件(第 1 章介绍的类形态),因此 ctx.plugin(GreeterService) 会像挂载其他插件一样挂载它。

使用 inject 消费服务

创建 consumer.ts

1
import type { Context } from '@deepseek-ai/cordis' export const name = 'consumer' export const inject = ['greeter'] export function apply(ctx: Context) { console.log(ctx.greeter.greet('world')) }

inject 列出该插件需要的服务。Cordis 会让插件保持 PENDING,直到列出的每项服务都存在,因此在 apply 内可以保证 ctx.greeter 已经就绪。cordis.yml 中的加载顺序无关紧要:决定插件何时启动的是依赖关系,而不是文件顺序

组合并运行:

1
- name: './greeter.ts' - name: './consumer.ts'
Hello, world!

交换 cordis.yml 中两行的顺序后重新运行,输出仍然相同。尝试彻底移除 ./greeter.ts:消费方会保持 PENDING,不输出任何内容,既不崩溃,也不会只运行一部分。处于 PENDING 的 fiber 也不会让 Node 的事件循环保持活跃,因此如果组合中没有其他运行项,进程会静默地以状态码 0 退出(第 6 章介绍如何诊断这种状态)。

加载后仍会跟踪依赖关系

inject 并非一次性的启动检查。如果应用运行期间所需服务消失——例如提供方被卸载或热替换——每个依赖插件也会随之卸载,并在服务恢复后再次加载。结合 effect(第 2 章),这能防止运行中的消费方保留对不可用服务的引用:依赖消失时,它自己的注册也会撤销。

这也是配置中可以替换服务的原因:卸载 Cordis 配置项 dsh-bash-local,挂载另一个 shell 提供方,所有注入 'shell' 的插件都会重新启动并使用新实现。

可选依赖

inject 用于硬性依赖。如果某项功能缺失时插件仍可运行,请跳过 inject,并在使用处探测:

1
export function apply(ctx: Context) { // undefined when no provider is loaded; the plugin still runs. const greeter = ctx.get('greeter') console.log(greeter?.greet('maybe') ?? 'no greeter available') }

命名

每个应用中的服务名称共用一个扁平命名空间。请为自有服务添加有辨识度的前缀或命名空间(harness 已占用 toolsllm 等普通名称);子系统页面上生成的 cordis-surface 区块会列出 harness 注册的每个名称。


六、第 4 章:事件

服务支持直接调用;事件让插件无需知道有哪些插件正在监听,就能发出通知。harness 使用事件处理工具结果、模型请求和审批决定等交互。

声明、发出与监听

创建 stats.ts——一项负责计数并在每次变化时发出通知的服务:

1
import { Service, type Context } from '@deepseek-ai/cordis' declare module '@deepseek-ai/cordis' { interface Context { stats: StatsService } interface Events { 'stats/report'(name: string, count: number): void } } export class StatsService extends Service { private counts = new Map<string, number>() constructor(ctx: Context) { super(ctx, 'stats') } bump(name: string) { const next = (this.counts.get(name) ?? 0) + 1 this.counts.set(name, next) this.ctx.emit('stats/report', name, next) } } export const name = 'stats' export function apply(ctx: Context) { ctx.plugin(StatsService) }

interface Events 合并与第 3 章的 interface Context 合并在事件系统中相互对应:它声明事件名称及其监听器签名,因此 ctx.emitctx.on 都具有完整类型。namespace/action 命名约定让扁平的事件命名空间保持易读。

创建 reporter.ts

1
import type { Context } from '@deepseek-ai/cordis' import type {} from './stats.ts' export const name = 'reporter' export const inject = ['stats'] export function apply(ctx: Context) { ctx.on('stats/report', (name, count) => { console.log(`[stats] ${name} -> ${count}`) }) ctx.stats.bump('tool_call') ctx.stats.bump('tool_call') ctx.stats.bump('prompt') }

import type {} from './stats.ts'不会在运行时导入任何内容;它的作用是让 TypeScript 看到声明合并。

组合并运行:

1
- name: './stats.ts' - name: './reporter.ts'
[stats] tool_call -&gt; 1
[stats] tool_call -&gt; 2
[stats] prompt -&gt; 1

因为 ctx.on() 属于 effect,监听器会随插件一同消失,绝不需要手动维护 removeListener

分发模式

emit5 种分发模式之一。事件采用哪种模式是其约定的一部分,决定了监听器能否返回值、能否并发运行,以及能否彼此短路:

模式调用方式行为
emitctx.emit(name, ...args)同步广播;不会等待或收集返回的 promise 与值
parallelawait ctx.parallel(name, ...args)所有监听器并发运行,并一同等待
serialawait ctx.serial(name, ...args)监听器按顺序运行并等待;第一个非 null/false/undefined 返回值胜出,并停止后续监听器
bailctx.bail(name, ...args)serial 的同步版本
waterfall(瀑布式事件)ctx.waterfall(name, ...args, next)环绕中间件,见下文

每个 harness 事件都会在其所属子系统页面自动生成的参考文档中记录其模式。

waterfall:转换或短路

waterfall 是实现拦截的模式。每个监听器都会收到参数和一个 next() continuation;它可以转换 next() 的返回值,也可以不调用 next() 就直接返回,从而短路链条的其余部分——Cordis 文档把后一种行为称为否决(veto)

创建 waterfall-demo.ts

1
import type { Context } from '@deepseek-ai/cordis' declare module '@deepseek-ai/cordis' { interface Events { 'demo/transform'(input: string, next: () => Promise<string>): Promise<string> } } export const name = 'waterfall-demo' export function apply(ctx: Context) { // Listener 1: wrap the downstream result. ctx.on('demo/transform', async (input, next) => { const downstream = await next() return downstream.toUpperCase() }) // Listener 2: short-circuit when it owns the decision. ctx.on('demo/transform', async (input, next) => { if (input.includes('blocked')) return '** blocked **' return next() }) void (async () => { console.log(await ctx.waterfall('demo/transform', 'hello', async () => 'hello')) console.log(await ctx.waterfall('demo/transform', 'blocked words', async () => 'blocked words')) })() }

cordis.yml 只指向该文件并运行:

HELLO
** BLOCKED **

按顺序看第二行如何产生:监听器 1 先运行并调用 next(),从而调用监听器 2;监听器 2 看到 blocked 后直接返回而不调用 next(),因此最内层默认逻辑(传给 ctx.waterfall 的函数)从未运行;返回途中,监听器 1 再把替换消息转换为大写。

由此得到一项纪律:只负责观察或标注的 waterfall 监听器必须调用 next();不调用就直接返回代表有意短路。如果日志监听器忘记调用 next(),会悄无声息地吞掉所有下游的默认行为。

harness 使用 waterfall 处理协作插件可以包装或回答的决策:


七、第 5 章:配置

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

可配置插件

创建 config-demo.ts

1
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 获得验证器。本仓库使用 Schemastery 定义 schema;Cordis 本身接受任意 Standard Schema 验证器,因此将普通对象导出为 Config 无法工作

对其进行配置:

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

运行:

Hello, alpha!
Hello, beta!

未提供 greeting,因此 schema 默认值会将其补齐:apply 始终会收到完整且经过验证的配置

明确报错

现在向它传入无效内容:

1
- 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 验证,但其中指定的资源或提供方不可用,该插件也应当在能解析该引用时立即拒绝

计算得到的配置值

本仓库使用的 loader 支持 !!js 标签,用于必须在加载时计算的配置值:

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

!!js 仅在 config 与条目 disabled 字段内有效。disabled: !!js ... 在每次挂载决策时基于 loader 上下文求值(本仓库的扩展),可以按平台或环境门控一行;其余元数据(nameidinject 等)保持静态,其中的表达式是普通真值数据。


八、第 6 章:组合与 HMR(热模块替换)

到目前为止构建的每项能力都是插件,cordis.yml 则选择应用的插件树。本章会改变这种组合、热重载一个插件,并诊断始终无法加载的插件。

Cordis 配置项不只有名称

Cordis 配置项除了 nameconfig,还接受其他元数据:

1
- id: greeter # stable identity for this entry name: './greeter.ts' - id: consumer name: './consumer.ts' disabled: true # keep the entry, skip mounting it
  • id:为 Cordis 配置项提供稳定标识,使 loader 能区分"修改现有配置项"与"先删除再添加"。
  • disabled: true:会卸载插件而不删除其配置项;改回原值后,插件以及所有因依赖其服务而处于 PENDING 的插件都会再次加载。
  • 组(group):可以嵌套一份配置项子列表,并将其作为一个单元加载和卸载;
  • isolate:为一个组提供某项服务名称的独立实例,因此两个组可以各自看到配置不同的 shell 提供方,互不影响。

热模块替换

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

tmp/cordis-tutorial 中编写 cordis.yml

1
- 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 服务记录日志,因此没有控制台导出器时看不到其消息;
  • 它还会 inject timer 服务来实现去抖,如果没有 @deepseek-ai/cordis-plugin-timer,它就会永远停在 PENDING,而且不发出任何提示

HMR 通过 Loader 的原生辅助工具读取 Node 的 loader 内部结构,请在 tsx 下运行 Cordis:

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

现在编辑 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 再次运行。 按 Ctrl-C 停止进程。

编辑 cordis.yml 本身也会触发更新:loader 按 id 比较配置项,只挂载、卸载或重新配置发生变化的部分。这就是上述配置项显式携带 id 的原因:不带 id 的配置项在每次读取时都会获得一个新 id,所以只要配置文件发生任何编辑,即使自身文本未变,它也会被视为"先删除再添加"并重新挂载。

诊断始终无法加载的插件

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

你可以直接查看这些状态。每个上下文都能枚举插件注册表,创建 diagnose.ts

1
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

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

运行它(直接执行 node --import tsx ../../vendor/cordis/bin.js,按 Ctrl-C 停止):

needs-timer is PENDING — a required service is missing

inject: ['timer'] 没有提供方。向列表添加 - name: '@deepseek-ai/cordis-plugin-timer' 后,插件就会加载。

调试小技巧:如果插件既不执行任何操作,也不报告任何内容,请检查其 fiber 状态。不加 PENDING 过滤条件进行迭代时,还会看到 loader 自身的插件(Loader、Include)处于 ACTIVE,因为配置文件本身也是通过插件挂载的。


九、第 7 章:进入 harness

本章会向 harness 的 tools 服务注册一个可由模型调用的工具,通过 harness 工具流水线执行它,并观察结果事件。整个示例无需密钥,也不会调用模型

工具插件

创建 greet-tool.ts

1
import type { Context } from '@deepseek-ai/cordis' import { defineTool } from '@deepseek-ai/dsh-tools' import { CallId } from '@deepseek-ai/dsh-llm' export const name = 'greet-tool' export const inject = ['tools'] export function apply(ctx: Context) { ctx.tools.register(defineTool({ name: 'greet', description: 'Greet the named person.', parameters: { name: { type: 'string', required: true, description: 'Who to greet' }, }, output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }], }, async execute(args) { return `Hello, ${args.name}!` }, })) // Drive one call through the real execution pipeline, standing in for // the model. CallId brands the correlation id a provider would issue. void (async () => { const result = await ctx.tools.execute({ callId: CallId('demo-1'), name: 'greet', arguments: { name: 'Cordis' }, signal: new AbortController().signal, }) console.log('tool replied:', JSON.stringify(result.content)) })() }

这里的每个模式都来自前几章:

  • inject: ['tools'](第 3 章)会让插件等待工具注册表就绪;
  • ctx.tools.register(...) 会把注册 disposer 附着到插件(第 2 章),因此卸载时会注销工具
  • defineToolparameters 规约转换为向模型展示的 JSON Schema,推导 args 的类型,并在 execute 运行时校验它们。

观察插件

创建 tool-logger.ts。这是一个独立插件,通过 harness 的 tools/result 事件观察应用中的每次工具调用:

1
import type { Context } from '@deepseek-ai/cordis' import type {} from '@deepseek-ai/dsh-tools' export const name = 'tool-logger' export const inject = ['tools'] export function apply(ctx: Context) { ctx.on('tools/result', (exec, result) => { const text = result.content .map(block => (block.type === 'text' ? block.text : '')) .join('') console.log(`[tool-logger] ${exec.name} -> ${text}`) }) }

import type {} from '@deepseek-ai/dsh-tools' 行会引入该包的声明合并,使 'tools/result' 及其 payload 具有类型——这与第 4 章导入 stats.ts 的做法相同,只是扩展到了包级别。

组合并运行

1
- name: '@deepseek-ai/dsh-system-prompt' - name: '@deepseek-ai/dsh-tools' - name: './tool-logger.ts' - name: './greet-tool.ts'

@deepseek-ai/dsh-tools 会注入 systemPrompt 服务,因为工具需要向系统提示词贡献 schema,所以组合中也要列出该服务的提供方。缺少提供方时,工具插件会像第 6 章所述那样保持 PENDING。

1
node --import tsx ../../vendor/cordis/bin.js
[tool-logger] greet -&gt; Hello, Cordis!
tool replied: [{"type":"text","text":"Hello, Cordis!"}]

注意 logger 触发:tools/result 在结果物化过程中发出,发生在 execute 向调用方返回的 promise 兑现之前。两个插件都不知道另一个插件存在,它们由注册表服务和事件连接。

从这里走向完整 agent

真实 agent 就是这套组合再加上更多插件:LLM 适配器、agent loop、持久化和运行入口。对照 examples/headless-agent/cordis.yml,你现在已经可以读懂其中每个配置项——将 greet-tool.ts 加入该文件的副本即可。

后续可以阅读:


十、TypeScript 说明

这些示例使用了普通现代 JavaScript 之外的三项 TypeScript 功能:

  1. 类型注解描述值,但不会改变运行时行为:ctx: Context 表示 ctx 具备 Cordis 上下文 API,who: string 接受文本,而 string[] 表示字符串数组。
  2. import type { Context } from '@deepseek-ai/cordis' 只导入类型信息。它在运行时会消失,因此仅为类型注解使用 Context 的插件文件不会增加运行时依赖。
  3. 声明合并declare module '@deepseek-ai/cordis' { ... })会为 Cordis 已经声明的接口添加你的条目,例如新 ctx.greeter 属性的类型或事件名称。它不会生成任何运行时接线;插件必须另行提供服务或发出事件(第 3、4 章完整展示了该模式)。

第 5 章还会使用 interface 描述配置对象的字段,并使用 Schema<Config> 这类泛型表示 schema 校验哪些对象字段。你可以直接照写这些声明;周围的正文会解释每项声明连接了什么。


十一、总结

章节核心概念一句话要点
1插件插件是函数,由 loader 挂载,cordis.yml 组合应用
2生命周期与 effect未被管理的资源要包装在ctx.effect() 中,卸载时自动回卷
3服务ctx 上公开能力,用 inject 声明依赖
4事件类型化事件、广播分发与 waterfall 的短路行为
5配置读取经过 schema 校验的配置,输入错误时明确报错
6组合与 HMR把配置文件当作插件树,支持热重载,可诊断 PENDING 插件
7进入 harness基于真实 harness 服务注册模型可调用的工具

Cordis 的精髓可以概括为一句话:把能力拆成可组合、可替换、可热更新的插件。掌握了这 7 章,你就拥有了阅读、修改和扩展 DeepSeek Harness 的核心能力。