
字节笔记本
2026年10月6日 · 约 6 分钟读完
Cordis 教程 3:插件服务与 inject
本文是 Cordis 插件框架实战教程的第 3 篇。Cordis 是 DeepSeek 开源 agent 框架 DeepSeek Harness(仓库 deepseek-ai/deepseek-harness,MIT 协议)底层的插件运行时,官方教程每章都是一个可运行的动手实验,本篇把其中「服务」一章整理成中文导读,聚焦这个框架最核心的抽象。

服务:写在 ctx 上的具名能力
服务(Service)是由某个插件提供、其他插件通过 ctx 消费的具名能力。harness 里的 ctx.tools、ctx.llm、ctx.agents 都是服务。它解决的问题是解耦:消费方只写能力名的字符串,比如 'tools',并不导入提供方的代码,因此配置文件可以随时更换提供方,消费方一行代码都不用改。
提供服务:运行时注册与编译时声明缺一不可
官方教程用一个 greeter 示例讲清了两块配合:
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 块,用到 TypeScript 的声明合并:它把 greeter 字段加进 Context 接口,让 ctx.greeter 在任何文件里都能通过类型检查。这个块不生成任何代码,缺了它服务照样能跑,只是消费方失去类型提示。
还有一个省心的细节:Service 子类本身就是一个插件(类形态),所以 ctx.plugin(GreeterService) 像挂载普通插件一样挂载它,不需要额外的接线。
消费服务:inject 声明依赖,加载顺序无关
消费方只需导出一个 inject 数组:
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'))
}框架会把这类插件保持在 PENDING 状态,直到 inject 列出的每一项服务都真实存在,才执行它的 apply。因此在 apply 内部,ctx.greeter 一定已经就绪。cordis.yml 里两个插件的书写顺序因此无关紧要:决定插件何时启动的是依赖关系,不是文件先后。把两行配置对调,输出仍是 Hello, world!。
把 greeter 从配置里整个删掉会怎样?消费方停在 PENDING,什么都不打印,不崩溃,也不会跑一半。教程还点出一个容易踩的坑:PENDING 的 fiber 不会让 Node 事件循环保持活跃,如果组合里没有其他在运行的东西,进程会静默地以状态码 0 退出,看起来就像什么都没发生过,排查时容易一愣。
依赖在运行期仍被持续跟踪
inject 不是一次性的启动检查。应用运行中,如果所需服务消失,比如提供方被卸载或热替换,每个依赖它的插件也会被一并卸载,等服务恢复后再自动重新加载。配合 effect 机制(插件卸载时自动撤销它注册过的东西),这保证运行中的消费方不会攥着一个已不可用的服务引用:依赖消失时,它自己的注册也会被逐层撤销。
这正是「服务可以在配置里替换」能成立的原因:卸载 dsh-bash-local 这条配置项,挂上另一个 shell 提供方,所有注入 'shell' 的插件都会干净地重启到新实现上,不需要改任何消费方代码。

可选依赖与命名建议
inject 表达的是硬依赖。如果某项能力缺席时插件也能活下去,就不要写 inject,改为在使用处探测:
export function apply(ctx: Context) {
const greeter = ctx.get('greeter')
console.log(greeter?.greet('maybe') ?? 'no greeter available')
}ctx.get 在没有提供方时返回 undefined,插件照常运行,用可选链兜底即可。
命名方面,每个应用里的服务名共用一个扁平命名空间,harness 已经占用了 tools、llm 这类普通名字,自己的服务应当加上有辨识度的前缀或命名空间。harness 文档的子系统页面上有一个由工具生成的 cordis-surface 区块,列出框架注册的每一个服务名,动手写插件前先查一遍,可以避免撞名。
整体看,这一章给出的模式相当实用:能力用名字暴露,依赖用数组声明,替换靠配置完成。对于想给 agent 框架写扩展的人来说,这是一套可以直接照搬的解耦思路。



