把官方 7 篇教程,揉成一份能直接上手的手册
本教程浓缩自官方 cordis-tutorial/01~07 七篇文档,并逐章对照 deepseek-harness 本仓库的真实代码(文件路径与片段均可在 packages/、vendor/cordis/、examples/ 中查证)。目标:少术语、多图示、每个概念都有「本项目里谁在这么写」作锚点。
心智 先记住三件事
cordis.yml)+ 一堆插件(apply(ctx) 函数)。框架负责把插件按依赖关系组装、启动、卸载、热替换。
ctx 是一块插线板。插件是插头,插上去就通过 apply(ctx) 通电。服务(Service)是插线板上固定编号的「专用插座」(如 ctx.tools、ctx.llm),别的插头想用电就认插座号,不关心谁在供电。
cordis.yml 是接线清单:每行写一个要挂的插件。谁先通电不由清单顺序决定,而由「谁依赖谁」(inject)决定——像电路里的上下游。
三个必须刻进肌肉记忆的 API
cordis.yml 的书写顺序启动的。如果你的插件 inject: ['tools'] 但清单里没有人提供 tools,它不会报错,而是安静地停在 PENDING 状态——进程可能悄无声息地以 0 退出。第 6 章教你怎么诊断这种「明明写了却没输出」的情况。
▶ 怎么跑起来
教程的 7 个最小示例都放在一个 tmp/cordis-tutorial/ 目录里,用裸 Cordis loader 启动:
node --import tsx ../../vendor/cordis/bin.js
而在 本仓库里,你不会直接敲这条命令——harness 把它包进了 CLI。等价入口是:
pnpm dsh --profile headless "帮我看看这个仓库的 README"
两条命令的底层完全一致:都是「创建根 Context → 挂 Loader 插件 → 读 cordis.yml → 逐个挂载子插件 → 调用每个插件的 apply(ctx)」。理解前者,就理解了后者。
01 第一个插件
apply(ctx) 的函数;cordis.yml 决定把它挂不挂、挂哪个。最小插件(教程示例)
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello'
export function apply(ctx: Context) {
console.log('hello from my first plugin')
}
用一份清单把它挂上去
- name: './hello.ts'
运行后输出 hello from my first plugin。注意:你的文件里没有任何框架启动代码——插件只描述自己的贡献,组合交给 cordis.yml。本仓库的 packages/bundle/base/cordis.patch.yml 就是一份长得多的「插件接线清单」(见第 6 章)。
插件有三种长相
// 1. 函数插件(你刚写的,最常用)
export function apply(ctx: Context) {}
// 2. 对象插件:带 apply 方法的对象
export const objectPlugin = {
name: 'object-plugin',
apply(ctx: Context) {},
}
// 3. 类插件:Service 的子类(第 3 章才用)
export class MyService extends Service {
constructor(ctx: Context) {
super(ctx, 'myTutorialService')
}
}
ctx.xxx 服务」的场景准备的(第 3 章)。
本项目里到处都是这种函数插件
/** Cordis 函数插件名 */
export const name = 'schedule'
/** 依赖的服务:这些就绪后本插件才会启动 */
export const inject = ['agents', 'sessions', 'tools', 'sessionPersistence']
export function apply(ctx: Context): void {
// ...挂载定时提醒能力...
}
这正是教程里 hello.ts 的「生产版」:同样的 name / inject / apply(ctx) 三件套,只是 inject 列出了它需要的服务。
apply 内部抛异常,进程会直接挂掉。但如果只是 cordis.yml 里的模块路径 / 包名拼错(解析失败),Cordis 只会打一条日志、不会崩溃——而且这条日志可能在 console 导出器就绪前就丢了。所以「加了配置项却毫无反应」时,先查拼写。
02 生命周期与 effect
ctx.effect() 并返回一个「清理函数」。把资源关进 effect
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 () => { // ← disposer:卸载时运行
clearInterval(timer)
console.log('heartbeat cleaned up')
}
})
}
export function apply(ctx: Context) {
const fiber = ctx.plugin(heartbeat) // 把函数当插件挂上去,返回 fiber
ctx.effect(() => {
const timer = setTimeout(async () => {
await fiber.dispose() // 等 heartbeat 全部清理完才结束
console.log('disposed')
process.exit(0)
}, 700)
return () => clearTimeout(timer)
})
}
运行输出:heartbeat plugin loading → tick ×3 → heartbeat cleaned up → disposed。三个要点:
ctx.plugin(heartbeat)把「来自代码的函数」挂成插件——和 YAML loader 对每个配置项做的事一模一样。它返回一个 fiber(已加载插件实例的运行时句柄)。- effect 的主体在加载期运行,返回的 disposer 在卸载期运行。生命周期和插件一致的资源,你永远不用手动调 disposer。
fiber.dispose()会等该插件的所有清理(含异步)完成,并递归卸载它挂的子插件。
Fiber 状态机
每个已加载的插件实例都有一个 fiber,在这些状态间流转。当你排查「为什么没输出」时,这张图就是答案。
apply 在跑;FAILED 表示 apply 或配置校验抛错。你会在第 6 章反复遇到 PENDING。哪些操作「本来就是 effect」,不用你手写
ctx.on(event, listener):监听器随插件卸载自动移除(第 4 章)。ctx.plugin(child):子插件随父插件一起 dispose。- 服务注册、以及 harness 的注册表(如
ctx.tools.register(...))都会把返回的 disposer 挂到调用插件上——卸载时自动注销(第 7 章)。
await。
真实代码:嵌套 effect(schedule 插件)
export function apply(ctx: Context): void {
ctx.effect(() => { // 外层:插件级生命周期
const stopCreated = ctx.on('agent/created', ({ agent }) => {
if (/* 不是根 agent 等条件 */) return
const cleanup = agent.ctx.effect(() => { // 内层:单个 agent 的生命周期
const disposeTools = registerScheduleTools(ctx, agent.ctx, agent, ...)
const stopStatus = agent.ctx.on('agent/status', ...)
runtime.start()
return async () => { // 该 agent 消失时清理
stopStatus()
disposeTools()
await runtime.dispose()
}
}, 'schedule.runtime()')
runtimes.set(agent, cleanup)
})
return async () => { // 插件卸载:取消所有监听并清理
stopCreated()
await Promise.allSettled([...runtimes.values()].map(c => c()))
}
}, 'schedule.lifecycle()')
}
这就是教程 lifecycle.ts 的「工程版」:外层 effect 负责「新 agent 出现就给它装能力」,内层 effect 负责「单个 agent 消失就拆掉它的定时器/工具」。父子两层 effect 自动级联清理。
03 服务 Service
ctx 消费的具名能力(如 ctx.tools、ctx.llm)。消费方只认能力名、不认提供方,所以换实现不用改消费方。提供服务:写一个 Service 子类
import { Service, type Context } from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis' {
interface Context {
greeter: GreeterService // 编译时:让 ctx.greeter 有类型
}
}
export class GreeterService extends Service {
constructor(ctx: Context) {
super(ctx, 'greeter') // 运行时:以 'greeter' 注册到 ctx
}
greet(who: string) {
return `Hello, ${who}!`
}
}
export const name = 'greeter'
export function apply(ctx: Context) {
ctx.plugin(GreeterService) // Service 子类本身就是插件
}
两段协同:运行时靠 super(ctx, 'greeter') 注册,之后任何插件都能 ctx.greeter 访问它,且注册是 effect,提供方卸载时服务自动消失;编译时靠 declare module 的声明合并把 greeter 加进 Context 接口,拿到类型安全(不写也能跑,只是没类型)。
消费服务:用 inject
export const name = 'consumer'
export const inject = ['greeter'] // 声明硬依赖
export function apply(ctx: Context) {
console.log(ctx.greeter.greet('world')) // 这里保证 greeter 已就绪
}
inject 列出本插件需要的服务,Cordis 会让它保持 PENDING 直到每一项都存在——所以在 apply 里可以放心用 ctx.greeter。清单里两行的先后无关紧要:决定启动时机的是依赖,不是文件顺序。把 ./greeter.ts 整行删掉,消费方就永远 PENDING、不输出、不报错。
真实代码:harness 里的服务
import { Context, Service } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
declare module '@deepseek-ai/cordis' {
interface Context { e2b: E2BRuntime }
}
export class E2BRuntime extends Service {
static Config: z<Config> = z.object({
apiKey: z.string(),
cwd: z.string().default('/home/user/workspace'),
timeoutMs: z.number().default(300_000),
})
constructor(ctx: Context, config: Config) {
super(ctx, 'e2b') // 注册为 ctx.e2b
}
}
再看 harness 最核心的服务之一——工具注册表 ctx.tools:
export class ToolRuntime extends Service {
static inject = ['systemPrompt'] // 工具要向系统提示词贡献 schema
static Config: z<Config> = z.object({
mode: z.union(['native', 'code', 'both'] as const).default('native'),
maxParallelSubCalls: z.natural().min(1).default(10),
})
constructor(ctx: Context, config: Config = {}) {
super(ctx, 'tools') // 注册为 ctx.tools
ctx.systemPrompt.tools(context => this.wireSchemas(context.scope))
}
}
inject 不是一次性启动检查。运行中若服务消失(提供方被卸载 / 热替换),每个依赖它的插件会跟着卸载;服务恢复后又会自动重新加载。结合 effect,这能防止消费方持有已失效服务的引用。
inject。某项功能「没有也能跑」时,跳过 inject,在使用处探测:const greeter = ctx.get('greeter'),为 undefined 就走降级路径。
tools、llm 等普通名)。生成的 cordis-surface 区块列出了 harness 注册的所有名称。
04 事件 Events
声明、发出、监听
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) // 发出
}
}
import type {} from './stats.ts' // 仅为让 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')
}
输出 [stats] tool_call -> 1 / 2 和 [stats] prompt -> 1。因为 ctx.on() 是 effect,监听器随插件一起消失,永远不用手动 removeListener。interface Events 的声明合并和上一章 interface Context 对应:它声明事件名与签名,使 emit / on 都带完整类型。
五种分发模式
事件用哪种模式是「约定的一部分」,决定监听器能否返回值、能否并发、能否互相短路:
| 模式 | 调用 | 语义 |
|---|---|---|
emit | ctx.emit(name, ...args) | 同步广播;不等待、不收集返回值 / promise。 |
parallel | await ctx.parallel(name, ...) | 所有监听器并发跑,一起等待(任一个 reject 则 AggregateError)。 |
serial | await ctx.serial(name, ...) | 按顺序、逐个 await;第一个非 null/false/undefined 返回值「胜出」并停止后续。 |
bail | ctx.bail(name, ...) | serial 的同步版。 |
waterfall | ctx.waterfall(name, ...next) | 环绕式中间件(见下),用于「转换或短路」。 |
下面这段就是 Cordis 源码里 waterfall 的真实实现——它把监听器排成「从外到内」的链,每层都能拿到 next():
waterfall(...args: any[]) {
const cbs = this.dispatch('waterfall', args)
const inner = args.pop() // 最内层的默认行为
const next = () => {
const cb = cbs.shift() ?? inner // 没有更多监听器就跑默认行为
return cb(...args)
}
args.push(next)
return next() // 从最外层监听器开始
}
waterfall:转换 or 否决(veto)
每个监听器拿到参数和一个 next() 续体:可以「转换 next() 的返回值」,也可以「不调 next() 直接返回」来短路整条链(Cordis 称之为否决)。
declare module '@deepseek-ai/cordis' {
interface Events {
'demo/transform'(input: string, next: () => Promise<string>): Promise<string>
}
}
export function apply(ctx: Context) {
ctx.on('demo/transform', async (input, next) => {
const downstream = await next() // 先让下游跑
return downstream.toUpperCase() // 再包装结果
})
ctx.on('demo/transform', async (input, next) => {
if (input.includes('blocked')) return '** blocked **' // 否决:不调 next()
return next()
})
// 输出:HELLO / ** BLOCKED **
}
next();不调用就返回 = 有意短路。如果某个日志监听器忘了调 next(),会悄无声息吞掉所有下游默认行为。这是本仓库的常设规则。
真实代码:harness 的 tools/result 事件
ctx.on('tools/result', (exec: ToolExecution, result: ToolExecutionResult) => {
// 每次有工具跑完,这里都能拿到执行信息与结果
})
harness 还用 waterfall 做「协作插件可以包装或回答的决策」:agent/request 允许插件替换模型调用配置,approval/request 允许策略代替用户作答。这正是 waterfall 否决模式的用武之地。
05 配置 Config
cordis.yml 里每个插件项可带 config 块;插件导出一个 Schema,在 apply 之前校验它。配置不合法就加载失败并给精确错误——插件绝不会在配置残缺时启动。可配置插件
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[]
}
// 同名 Config:既是 TS 类型,又是运行时校验器
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}!`)
}
}
- name: './config-demo.ts'
config:
targets: ['alpha', 'beta']
没给 greeting,schema 默认值补上 → 输出 Hello, alpha! / Hello, beta!。导出的 Config 既是 TypeScript 接口,也是同名运行时 schema:消费方拿类型,Cordis 拿校验器。本仓库用 Schemastery;Cordis 本身接受任意 Standard Schema 校验器——所以「把普通对象导出成 Config」是行不通的。
明确报错
- name: './config-demo.ts'
config:
targets: 'not-an-array' # 类型错了
报错:ValidationError: invalid config: - $.targets expected array but got not-an-array。fiber 进入 FAILED,启动器打印错误后以状态码 1 退出。
真实代码:本仓库的 Schema 长这样
static Config: z<Config> = z.object({
mode: z.union(['native', 'code', 'both'] as const).default('native'),
maxParallelSubCalls: z.natural().min(1).default(10),
})
和教程的 Schema.object({...}) 是同一套思想,只是本仓库统一用 @deepseek-ai/schemastery 的 z.object / z.string / z.union / z.natural 写法。
计算得到的配置值:!!js 标签
- name: './config-demo.ts'
config:
greeting: !!js process.env.DEMO_GREETING ?? 'Hello'
!!js 仅在 config 与条目 disabled 字段内有效。本仓库大量用它做「按环境 / 平台门控」,例如 examples/headless-agent/cordis.yml 里:
- id: agent-spine
name: '@deepseek-ai/dsh-agent-spine-demo'
config:
agents:
- id: main
cwd: !!js process.cwd() # 加载时求值
- id: persistence
name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
compression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'"
06 组合 · HMR · 诊断
cordis.yml 选择插件树。本章改组合、热替换一个插件、并诊断「永远加载不出来」的插件。Cordis 配置项不止有名字
- id: greeter # 配置项的稳定身份
name: './greeter.ts'
- id: consumer
name: './consumer.ts'
disabled: true # 保留条目但不挂载
id:给配置项稳定标识,loader 由此区分「改了现有项」还是「删了再添」。disabled: true:卸载插件但不删配置项;改回原值后,它和所有因依赖它而 PENDING 的插件会重新加载。- 组(group)可嵌套子列表整体加载 / 卸载;
isolate为组提供某服务的独立实例——两个组各自看到配置不同的shell提供方,互不干扰。
热模块替换(HMR)
卸载会释放 effect(第 2 章),加载遵循依赖(第 3 章),所以 HMR 可以先卸载、再加载来替换运行中的插件。@deepseek-ai/cordis-plugin-hmr 会监视文件,保存时自动执行这套流程。它依赖 logger 服务记录日志、inject timer 服务做去抖——所以清单里要带上这两个辅助插件,否则 HMR 会永远停在 PENDING 且不发声。
id 的配置项每次读取都会生成一个新 id,于是只要配置文件有任何编辑(哪怕自身文本没变),它也会被当成「先删再添」而重新挂载。显式写 id 才能做到「只挂载 / 卸载真正变化的部分」。
诊断:我的插件为什么一直 PENDING?
依赖驱动加载的另一面:inject 指了没人提供的服务,它就一直等、不输出。这不是错误(PENDING 是合法状态)。你可以直接枚举插件注册表看状态:
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 is PENDING — a required service is missing。往清单加 - name: '@deepseek-ai/cordis-plugin-timer' 它就活了。不加 PENDING 过滤地遍历,你还会看到 loader 自身的插件(Loader、Include)处于 ACTIVE。
真实代码:base bundle 的长清单
教程那个一行清单,放大到本仓库就是 packages/bundle/base/cordis.patch.yml——它一次性插入几十个带 id 的核心插件,后续各 mode 的 bundle 按 id 覆盖某一行的 config:
- insert:
- id: timer
name: '@deepseek-ai/cordis-plugin-timer'
- id: hmr
name: '@deepseek-ai/cordis-plugin-hmr'
config:
root: ['.']
- id: llm
name: '@deepseek-ai/dsh-llm'
- id: session
name: '@deepseek-ai/dsh-session'
- id: agent
name: '@deepseek-ai/dsh-agent'
# ...(共数十行,按 id 可被后续 bundle 覆盖)
07 进入 harness
ctx.tools 注册一个模型可调用的工具,通过真实工具流水线执行它,并监听 tools/result 事件。本例无需密钥、不调模型。工具插件:注册 + 执行
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}!`
},
}))
// 代替模型,直接驱动一次调用走真实执行流水线
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 章),卸载即注销。defineTool 把 parameters 规约为给模型看的 JSON Schema、推导 args 类型、并在 execute 前校验模型给的参数;output.render 是 Native renderer,把规范值转成可持久化的内容块。
观察插件:监听工具结果
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}`)
})
}
组合并运行
- 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。运行输出:
[tool-logger] greet -> Hello, Cordis!
tool replied: [{"type":"text","text":"Hello, Cordis!"}]
logger 先触发:tools/result 在结果「物化过程中」发出,早于 execute 返回的 promise 兑现。两个插件互不知道对方存在——它们由「注册表服务」和「事件」连接。
真实代码:defineTool 与 register 在做什么
export function defineTool(options: DefineToolOptions): ToolDefinition {
const parameters = parameterSchemaSpecToJsonSchema(options.parameters)
const outputSchema = valueSchemaSpecToJsonSchema(options.output.schema)
const validate = (args) => validateJsonSchemaValue(parameters, args, '')
return {
name: options.name,
description: options.description,
parameters,
output: {
schema: outputSchema,
render: (args, value) => options.output.render(args, value),
},
async execute(args, exec) {
const violations = validate(args)
if (violations.length) throw new ToolArgsError(violations) // 校验模型参数
return options.execute(args, exec)
},
}
}
register(definition: ToolDefinition): () => void {
const name = definition.name
if (name === RUN_CODE_NAME) throw new Error(`tool name "${RUN_CODE_NAME}" is reserved ...`)
// 注册本身是 effect:返回的 disposer 会随插件卸载自动注销工具
return this.layers.effect(
this.ctx,
layer => layer.tools.insert(name, definition),
{ label: 'tools.register()' },
)
}
这就把第 2 章「注册会自动撤销」和第 3 章「ctx.tools 是个服务」连起来了:defineTool 负责把你的函数变成规范定义,ToolRuntime.register 用 effect 把它插进注册表——插件一卸载,工具自动消失。
★ 从零到一个 Agent
examples/headless-agent/cordis.yml 就是一个完整的一次性编码 agent。你已经能逐行读懂它了:
- id: settings name: '@deepseek-ai/dsh-settings-file' # 用户设置
- id: credentials name: '@deepseek-ai/dsh-credentials-local' # 密钥
- id: llm-deepseek name: '@deepseek-ai/dsh-llm-deepseek' # LLM 适配器
config: { thinking: enabled, reasoningEffort: max, models: [...] }
- id: subprocess name: '@deepseek-ai/dsh-subprocess-local'
- id: bash name: '@deepseek-ai/dsh-bash-local' # bash 能力
- id: agent-spine name: '@deepseek-ai/dsh-agent-spine-demo' # agent 主脊
- id: persistence name: '@deepseek-ai/dsh-session-persistence-jsonl'
- id: compaction-basic name: '@deepseek-ai/dsh-compaction-basic' # 上下文压缩
- id: subagent name: '@deepseek-ai/dsh-subagent' # 子智能体
- id: todo / fs / workflow / ralph ... # 各类工具
- id: fs-local name: '@deepseek-ai/dsh-fs-local' # 文件系统
把教程里的 greet-tool.ts 加进这份清单的副本,你就拥有了一个「会打招呼」的真实 agent。后续可深入:构建工具(defineTool 的呈现与更丰富 schema)、三层能力设计(harness 如何组织可替换能力)、各 子系统页面 上生成的 cordis-surface 区块(可注入 / 可监听的一切)。
greet-tool 等 ctx.tools 就绪才启动)。tool-logger 与 greet-tool 互不认识,靠 tools/result 事件连接。≡ API 速查表
| 你要做什么 | 怎么做 | 章节 |
|---|---|---|
| 写一个插件 | export function apply(ctx) {} + cordis.yml 里一行 name | 01 |
| 管一个定时器 / 连接 / watcher | ctx.effect(() => { const h = ...; return () => cleanup(h) }) | 02 |
| 挂一个子插件并拿到句柄 | const fiber = ctx.plugin(Foo); fiber.dispose() | 02 |
| 对外提供一项能力 | class X extends Service { constructor(ctx){ super(ctx,'x') } } + declare module | 03 |
| 声明我依赖某服务 | export const inject = ['tools'](缺了会 PENDING) | 03 |
| 发通知 / 广播 | ctx.emit('ns/action', ...args) | 04 |
| 监听通知 | ctx.on('ns/action', (...args) => {})(自动随插件移除) | 04 |
| 包裹 / 短路决策 | ctx.waterfall('ns/action', ...next),监听器可 next() 或否决 | 04 |
| 给插件加配置 + 校验 | 导出 Config schema(Schemastery / Standard Schema) | 05 |
| 加载时计算配置 | config: !!js process.env.X ?? 'default' | 05 |
| 给配置项稳定身份 | 写 id;临时关掉写 disabled: true | 06 |
| 诊断 PENDING | 遍历 ctx.registry,找 fiber.state === FiberState.PENDING | 06 |
| 注册一个模型工具 | ctx.tools.register(defineTool({ name, parameters, output, execute })) | 07 |
本教程引用到的真实文件索引
vendor/cordis/bin.js— 裸 Cordis loader 入口vendor/cordis/src/events.ts—emit/parallel/serial/bail/waterfall真实实现vendor/cordis/src/service.ts—Service基类与注册机制packages/core/tools/src/index.ts—ToolRuntime(ctx.tools服务)、registerpackages/core/tools/src/schema.ts—defineToolpackages/e2b/e2b/src/index.ts—E2BRuntime(真实 Service 子类 + Schema 配置)packages/schedule/schedule/src/index.ts— 嵌套ctx.effect的真实范例packages/bundle/base/cordis.patch.yml— 生产级插件组合清单examples/headless-agent/cordis.yml— 一个完整 agent 的组合