首页 / DeepSeek Harness 入门教程 / 服务与依赖注入

DeepSeek Harness 入门教程

服务与依赖注入

本教程共 32 篇 · 第 22 篇 · 更新于 2026-08-15 · 约 5 分钟阅读

服务依赖注入injectService生命周期effectfiber服务隔离

本节目标:掌握服务的提供与消费——用 Service 基类声明服务、用 inject 声明依赖,并理解 fiber 生命周期与 effect 可逆性。

服务是什么

服务是一个插件向其他插件公开的具名能力,挂在 ctx 上。ctx.toolsctx.llmctx.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 已占用 toolsllm 等普通名字,给自有服务加有辨识度的前缀或命名空间。完整清单不需要手工维护:各子系统页面自动生成的 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 快速超时,长任务希望给足时间。隔离机制适用于 toolsshellfsllm 等任何服务。

小结

  • 服务是挂在 ctx 上的具名能力,提供方与消费方解耦。
  • Service 子类 + declare module 声明合并 = 类型安全的服务。
  • inject 声明硬依赖;可选依赖用 ctx.get()
  • fiber 状态机:PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED(↘ FAILED)。
  • 注册都是 effect,卸载自动撤销;isolate 可做服务实例隔离。