服务与依赖注入
本教程共 32 篇 · 第 22 篇 · 更新于 2026-08-15 · 约 5 分钟阅读
本节目标:掌握服务的提供与消费——用 Service 基类声明服务、用 inject 声明依赖,并理解 fiber 生命周期与 effect 可逆性。
服务是什么
服务是一个插件向其他插件公开的具名能力,挂在 ctx 上。ctx.tools、ctx.llm、ctx.agents 都是服务。消费方只指定 'tools' 这样的能力名,不导入提供方的实现——所以配置可以换提供方,消费方代码不用改。
任何插件都可以提供服务,供其他插件使用。这是 harness 组织可替换能力的根基。
提供服务:Service 基类
import { Service, type Context } from '@deepseek-ai/cordis'
export class GreeterService extends Service {
constructor(ctx: Context) {
super(ctx, 'greeter') // 以 'greeter' 为名注册服务
}
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——提供方卸载时,服务自动移除。Service 子类还可以声明 static inject = ['llm'],表示它自己也依赖其他服务。
类型声明:declare module 合并
declare module '@deepseek-ai/cordis' {
interface Context {
greeter: GreeterService
}
}
declare module 块用 TypeScript 声明合并,把 greeter 加进 Context 接口,让 ctx.greeter 处处有类型提示。它不生成任何代码。没有声明时服务在运行时照常工作,但消费方失去类型安全,拼错服务名也不会被编译期发现。
消费服务:inject
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 里的加载顺序无关紧要——决定插件何时启动的是依赖关系,不是文件顺序。
Note依赖是硬性的:把提供方从配置里删掉,消费方会停在 PENDING,不输出、不崩溃、不半运行。PENDING 的 fiber 不会让 Node 事件循环保持活跃,进程可能静默以状态码 0 退出——这是「我的插件为什么没输出」最常见的答案。
可选依赖:ctx.get()
服务可有可无时,跳过 inject,在使用处查询:
export function apply(ctx: Context) {
const metrics = ctx.get('metrics') // 无提供方时为 undefined
metrics?.record('plugin_loaded', 1)
}
ctx 键与命名空间
所有服务名共用一个扁平命名空间。harness 已占用 tools、llm 等普通名字,给自有服务加有辨识度的前缀或命名空间。完整清单不需要手工维护:各子系统页面自动生成的 cordis-surface 区块列出 harness 注册的每个服务名、方法与事件。开发插件时以这些生成区块和服务自带的 TypeScript 接口为准,不要维护一份自己的静态清单。
生命周期:fiber 状态机
每个已加载的插件实例都拥有一个 fiber,状态依次迁移:
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
↘ FAILED
- PENDING:已声明,所需服务未就绪。
- LOADING / ACTIVE:
apply正在运行/已经完成。 - FAILED:
apply或配置校验抛出异常。 - UNLOADING / DISPOSED:disposer 运行中/一切已拆除。
插件既不执行也不报告时,先检查它的 fiber 状态——PENDING 是合法状态,提供方可能稍后才挂载。
effect:可逆的注册
通过 Cordis API 建立的注册都是 effect,插件卸载时自动撤销。ctx.on 的监听器、ctx.tools.register 的工具、服务注册,全都不用手动清理。这正是热重载与依赖替换能安全生效的前提。
Cordis 管不到的资源(定时器、连接、watcher)用 ctx.effect() 包装:
ctx.effect(() => {
const timer = setInterval(() => console.log('tick'), 200)
return () => clearInterval(timer) // disposer:卸载时执行
})
effect 主体在加载期间运行,返回的 disposer 在卸载期间运行。disposer 按注册顺序的逆序启动,多个异步 disposer 并发运行。有顺序依赖的清理步骤,必须放进同一个 disposer 里依次等待。
依赖驱动让生命周期自动可逆:运行期间必需服务消失(提供方被卸载或热替换),依赖插件自动 dispose;服务恢复后自动重新加载。这也正是配置可以替换服务的原因——卸载 dsh-bash-local、挂上另一个 shell 提供方,所有注入 'shell' 的插件都会重启并使用新实现,不会残留对旧服务的引用。
服务隔离:isolate
同一个服务可以有多个实例,不同插件组看到不同实例:
- id: group-a
name: '@deepseek-ai/cordis-plugin-group'
group: true
isolate:
shell: true
config:
- name: '@deepseek-ai/dsh-bash-local'
config:
timeoutMs: 5000
- name: './src/plugin-a.ts'
group-a 与 group-b 各看各的 Bash 实例,超时配置互不影响。适用场景:快速交互的任务希望 Bash 快速超时,长任务希望给足时间。隔离机制适用于 tools、shell、fs、llm 等任何服务。
小结
- 服务是挂在
ctx上的具名能力,提供方与消费方解耦。 Service子类 +declare module声明合并 = 类型安全的服务。inject声明硬依赖;可选依赖用ctx.get()。- fiber 状态机:PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED(↘ FAILED)。
- 注册都是 effect,卸载自动撤销;
isolate可做服务实例隔离。