Skip to content

CH 19 · 插件三形态:函数 / 对象 / 类

全文字数2871 字预估耗时约 20 分钟前置CH 18(第一个插件)难度可照做

本章目标

CH 18 我们写的是插件最常用的一种写法。这一章把插件的三种形态(函数、对象、类)一次性讲透:它们各自长什么样、什么时候用哪种,以及类形态为什么是"开一个公共窗口"——它是插件之间互相协作的关键。看完这章,你就知道一个插件该用哪种形态写,而不是只会套一种模板。

三种形态先看全貌

一张表先兜个底,细节下面逐个拆:

形态长什么样一句话理解什么时候用
函数export function apply(ctx) {}你自己去办一件事默认选择,九成情况够用
对象export default { name, inject, apply(ctx) }名字和流程打包带走想给插件带点静态声明
export default class extends Service {}开一个公共窗口,谁都能来办事要让别的插件调用你的能力

记住一个判断就够:只给 Agent 加能力用函数;要让别的插件依赖你,用类。对象形态夹在中间,功能上基本等于函数,只是多了个 name 等静态元数据。

函数形态:你已会了

CH 18 写的 hello-plugin 就是函数形态:

js
export function apply(ctx) {
  // 在这里注册你的能力
}

框架加载插件时调用 apply(ctx),把上下文交给你。绝大多数插件——注册工具、监听事件、挂定时器——这一种就够了。在你确定要被别的插件依赖之前,永远先写函数形态。

对象形态:名字和流程打包

对象形态就是把 nameinjectapply 放进一个对象里一起导出:

js
export default {
  name: 'my-plugin',
  inject: ['tools'],
  apply(ctx) {
    // 这里 ctx.tools 一定可用
  },
}

它和函数形态几乎等价,区别只是把静态声明(名字、依赖)和 apply 组织成一份"打包好的说明书"。什么时候用?当你的插件希望一眼看清"我是谁、我要用哪些服务、我干什么"时,用对象更整洁。功能上没有函数形态做不到的事。

类形态:当插件要"提供服务"

这一节是本篇的重点。先建立一个概念:

服务(Service)是挂在 ctx 上的命名能力。 你每天都在用服务——ctx.tools(工具)、ctx.llm(模型)、ctx.agents(子代理)都是服务。任何插件都可以提供自己的服务,供其他插件调用。

函数/对象形态是"自己去办事";类形态是开一个公共窗口,谁都能来你这里办事。打个比方:函数形态像你跑一趟办事大厅办自己的事;类形态像你在大厅里开一个窗口,别人(别的插件)来你窗口递单子、拿结果。

提供方:写一个 Service 子类

js
import { Service } from '@deepseek-ai/cordis'

export default class MetricsService extends Service {
  constructor(ctx) {
    super(ctx, 'metrics')  // 注册一个叫 metrics 的服务
  }

  record(event, value) {
    console.log('[metrics]', event, value)
  }
}

两个关键点:

  • extends Service + super(ctx, 'metrics'):把 metrics 这个名字挂到 ctx 上,别的插件就能 ctx.metrics 访问它。
  • record(event, value) 是这个服务对外提供的方法——别人调用 ctx.metrics.record(...) 就会走到这里。

消费方:inject 声明依赖

js
export const name = 'consumer'
export const inject = ['metrics']

export function apply(ctx) {
  ctx.metrics.record('plugin_loaded', 1)
}

inject: ['metrics'] 声明"我要用 metrics 这个服务"。框架保证:apply 执行时,inject 声明的服务一定已经就绪。如果服务还没准备好,你的插件会等着,不会提前跑。

一张图看懂协作关系

提供方把服务挂到 ctx 上,消费方声明依赖后直接调用。toolsllmagents 这些内置能力,本质就是同一套机制——你在用它们的时候,就是"消费方"。

动手:让两个插件真的对上话

前面都是概念。下面亲手写一个提供方和一个消费方,让它们真的通上话。在一个你想放插件的工作区里(先 cd 到那里)建目录:

powershell
New-Item -ItemType Directory -Path "service-demo\src" -Force

第 1 步:给插件目录装依赖

类形态要 import 框架的 @deepseek-ai/cordis 包。你的工作区里没有它,直接加载会报 Cannot find package '@deepseek-ai/cordis'。所以先进插件目录把它装上:

powershell
cd "E:\software-workspace\DeepSeek harness demo\service-demo"   # 换成你自己的目录
npm init -y
npm install @deepseek-ai/cordis

装完改一下 package.json,给根对象加一行 "type": "module"——否则 Node 每次加载插件都要先猜模块格式,打一堆 warning:

json
{
  "name": "service-demo",
  "version": "1.0.0",
  "type": "module"
}

第 2 步:写提供方

新建 service-demo\src\provider.js

js
import { Service } from '@deepseek-ai/cordis'

export default class MetricsService extends Service {
  constructor(ctx) {
    super(ctx, 'metrics')
  }

  record(event, value) {
    console.log('[metrics]', event, value)
  }
}

第 3 步:写消费方

新建 service-demo\src\consumer.js

js
export const name = 'consumer'
export const inject = ['metrics']

export function apply(ctx) {
  ctx.metrics.record('plugin_loaded', 1)
}

第 4 步:声明并验证

新建 service-demo\cordis.yml(路径换成你自己的,注意空格要写成 %20):

yaml
- insert:
    - id: provider
      name: 'file:///E:/你的工作区/service-demo/src/provider.js'
    - id: consumer
      name: 'file:///E:/你的工作区/service-demo/src/consumer.js'

用 headless 快速验证:

powershell
cd 你的工作区目录
dsh --profile headless --patch "./service-demo/cordis.yml" "只回复:hi"

输出里你会看到:

text
[metrics] plugin_loaded 1
hi

[metrics] plugin_loaded 1 就是消费方调到了提供方的方法——你的两个插件真的对上了话。至此,函数、对象、类三种形态,你都亲手验证过了。

终端输出里黄色的那行 [metrics] plugin_loaded 1,就是消费方通过 ctx.metrics.record(...) 调到了提供方的方法——两个插件真的对上了话。

让 dsh 自己搭:一条提示词搞定

上面这些目录、依赖、两个文件都是你手动建的。老规矩,写插件这种活 dsh 自己也能干——而且它比你更熟"服务该怎么定义、依赖该怎么注入"。

直接在 Web UI 的输入框发这条提示词:

text
在我当前的工作区帮我写一个"服务形态"的插件示例:一个提供服务的插件(Service 子类)和一个使用它的插件(inject 依赖它),让它们真的对上话。你先去 dsh 官方插件开发文档读清楚怎么定义服务、怎么注入依赖,按官方规范实现并验证能正常加载调用,最后告诉我结果和文件放在哪。

它会自己去查官方文档、自己装依赖、自己写两个插件、自己验证能不能对上话。你只负责验收它写出来的东西。

常见坑

问题怎么回事怎么处理
Cannot find package '@deepseek-ai/cordis'插件目录没装依赖在插件目录 npm install @deepseek-ai/cordis
一堆 MODULE_TYPELESS_PACKAGE_JSON 警告package.json 没声明模块类型"type": "module"
消费方拿不到服务服务名对不上检查 super(ctx, 'xxx') 里的名字和 inject: ['xxx'] 完全一致
类插件加载报错extends Service 或没调 super类形态必须继承 Servicesuper(ctx, '服务名')
ctx.metrics 是 undefined消费方没声明 inject在消费方写 export const inject = ['metrics']

这一章你学到了什么

能自己完成下面几条,就算过关:

  • [ ] 能说出三种形态各自长什么样、什么时候用哪种
  • [ ] 知道服务 = 挂在 ctx 上的命名能力,tools/llm/agents 都是服务
  • [ ] 会写一个 Service 子类当提供方,super(ctx, '名字') 注册服务
  • [ ] 会写一个 inject 消费方,apply 里直接 ctx.服务名.方法() 调用
  • [ ] 知道类形态插件要先在插件目录装 @deepseek-ai/cordis 依赖

Open Source · MIT · Community Driven