ByteNoteByteNote
Cordis 服务入门:提供、注入与依赖跟踪
字

字节笔记本

2026年10月6日 · 约 7 分钟读完

Cordis 服务入门:提供、注入与依赖跟踪

API中转
¥120

本文整理自 DeepSeek 开源 agent 框架 deepseek-harness 官方仓库中的 Cordis 入门教程,对应教程里的服务一章。Cordis 是这个框架的插件内核,整套教程从第一个插件讲到事件与组合,而服务是插件之间协作最核心的一环:能力如何对外提供,又如何被别的插件安全地用起来。

Cordis 服务双平面:运行时注册与编译时类型检查

服务是什么

在 Cordis 里,服务是一个插件提供出来、供其他插件通过 ctx 消费的具名能力。harness 中的 ctx.tools、ctx.llm 和 ctx.agents 都是服务。消费方只声明自己需要 'tools' 这样的能力名,并不导入具体提供方,于是配置层可以随时更换提供方,消费方一行代码都不用改。这是典型的控制反转:能力由谁提供是配置的事,要不要用是插件自己的事,两边只靠一个名字对接。

提供一个服务

创建 greeter.ts,放在 tmp/cordis-tutorial 目录中:

ts
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 消费服务

创建 consumer.ts:

ts
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 里的加载顺序无关紧要,决定插件何时启动的是依赖关系,而不是文件在配置里的先后。

yaml
- name: './greeter.ts'
- name: './consumer.ts'

组合运行,输出 Hello, world!。把 yml 里两行对调重跑,结果不变。要是干脆删掉 ./greeter.ts,消费方会一直停在 PENDING:不输出任何内容,不崩溃,也不会只跑一半。这里有个容易踩的坑,处于 PENDING 的 fiber 不会让 Node 的事件循环保持活跃,如果组合里没有其他运行项,进程会静默地以状态码 0 退出,看起来就像什么都没发生。诊断这种静默待命需要组合层面的专门手段,本文不展开。

inject 依赖生命周期:PENDING 等待、跟随卸载与恢复重载

加载之后,依赖仍然被跟踪

inject 不是一次性的启动检查。应用运行期间,如果所需服务消失,比如提供方被卸载或热替换,每个依赖它的插件也会随之卸载;服务恢复后,它们再被加载回来。结合 effect 机制,这保证了运行中的消费方不会攥着一份已经不可用的服务引用:依赖消失时,它自己的注册也会一并撤销。

这也解释了为什么配置里可以替换服务:卸载 Cordis 配置项 dsh-bash-local,换另一个 shell 提供方挂上去,所有注入 'shell' 的插件都会重新启动,改用新实现。

可选依赖

inject 表达的是硬依赖。如果某项功能缺失时插件仍可运行,就跳过 inject,在使用处探测:

ts
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 已经占用了 tools、llm 这类普通名字。仓库子系统文档里生成的 cordis-surface 区块列出了 harness 注册的每一个服务名,起名之前先去查一遍,避免撞车。

概括成一句话:Cordis 用一个名字把能力的需求方和提供方解耦,inject 负责在正确的时间把它们接通,effect 负责在提供方退场时把连接干净地断开。

相关文章

分享: