CH 19 · Tres Formas de Plugin: Función / Objeto / Clase
Objetivo del capítulo
CH 18 fue la forma más común de escribir un plugin. Este capítulo cubre por completo las tres formas de plugin (función, objeto, clase) de una vez: qué aspecto tiene cada una, cuándo usar cuál, y por qué la forma clase es "abrir una ventana pública" — es la clave para la colaboración entre plugins. Tras leer este capítulo, sabrás qué forma debe usar un plugin, en lugar de solo poder usar una plantilla.
Las tres formas de un vistazo
Una tabla para la visión general, los detalles se desglosan uno por uno debajo:
| Forma | Aspecto | Entiende en una línea | Cuándo usarla |
|---|---|---|---|
| Función | export function apply(ctx) {} | Tú mismo vas a manejar una cosa | Opción por defecto, el noventa por ciento de los casos es suficiente |
| Objeto | export default { name, inject, apply(ctx) } | Empaqueta el nombre y el flujo juntos | Cuando quieres darle al plugin algunas declaraciones estáticas |
| Clase | export default class extends Service {} | Abre una ventana pública, cualquiera puede venir a manejar cosas | Cuando quieres que otros plugins llamen a tus capacidades |
Un juicio es suficiente: usa función para añadir capacidades al Agent; usa clase para que otros plugins dependan de ti. La forma objeto está en el medio, funcionalmente básicamente igual a la función, solo con más metadatos estáticos como name.
Forma función: ya la conoces
El hello-plugin de CH 18 es la forma función:
export function apply(ctx) {
// Registra tus capacidades aquí
}El framework llama a apply(ctx) al cargar el plugin, entregándote el contexto. La gran mayoría de plugins — registrar herramientas, escuchar eventos, montar timers — esta es suficiente. Hasta que estés seguro de que otros plugins dependerán de ti, escribe siempre primero la forma función.
Forma objeto: empaqueta el nombre y el flujo
La forma objeto consiste en poner name, inject, apply en un objeto y exportarlos juntos:
export default {
name: 'my-plugin',
inject: ['tools'],
apply(ctx) {
// Aquí ctx.tools está garantizado que estará disponible
},
}Es casi equivalente a la forma función, la única diferencia es organizar las declaraciones estáticas (name, dependencias) y apply en un "spec empaquetado". ¿Cuándo usarla? Cuando quieres que el "quién soy, qué servicios necesito, qué hago" de tu plugin quede claro de un vistazo, la forma objeto es más limpia. Funcionalmente no hay nada que la forma función no pueda hacer.
Forma clase: cuando el plugin quiere "proveer un servicio"
Esta sección es el foco de este capítulo. Primero establece un concepto:
Un Service es una capacidad con nombre montada en
ctx. Usas servicios cada día —ctx.tools(herramientas),ctx.llm(modelo),ctx.agents(subagents) son todos servicios. Cualquier plugin puede proveer sus propios servicios para que otros plugins los llamen.
La forma función/objeto es "ve a manejar cosas tú mismo"; la forma clase es abrir una ventana pública, cualquiera puede venir a ti a manejar cosas. Una analogía: la forma función es como ir tú a una sala de servicios a manejar tus propios asuntos; la forma clase es como abrir una ventana en la sala, otros (otros plugins) vienen a tu ventana a entregar formularios y obtener resultados.
Provider: escribe una subclase de Service
import { Service } from '@deepseek-ai/cordis'
export default class MetricsService extends Service {
constructor(ctx) {
super(ctx, 'metrics') // Registra un servicio llamado metrics
}
record(event, value) {
console.log('[metrics]', event, value)
}
}Dos puntos clave:
extends Service+super(ctx, 'metrics'): monta el nombremetricsenctx, otros plugins pueden acceder a él víactx.metrics.record(event, value)es el método que este servicio provee externamente — otros llamando actx.metrics.record(...)llegarán aquí.
Consumer: declara dependencias con inject
export const name = 'consumer'
export const inject = ['metrics']
export function apply(ctx) {
ctx.metrics.record('plugin_loaded', 1)
}inject: ['metrics'] declara "quiero usar el servicio metrics". El framework garantiza: cuando apply se ejecute, los servicios declarados en inject están definitivamente listos. Si el servicio no está listo, tu plugin esperará y no se ejecutará antes de tiempo.
Un diagrama para entender la colaboración
El provider monta el servicio en ctx, el consumer declara dependencias y llama directamente. Capacidades integradas como tools, llm, agents son esencialmente el mismo mecanismo — cuando las usas, eres un "consumer".
Práctica: haz que dos plugins hablen realmente
Lo de arriba fue todo concepto. Ahora vamos a escribir un provider y un consumer y hacer que hablen realmente. En un workspace donde quieras poner plugins (primero cd allí) crea un directorio:
New-Item -ItemType Directory -Path "service-demo\src" -ForcePaso 1: instala dependencias en el directorio del plugin
La forma clase necesita import el paquete @deepseek-ai/cordis del framework. Tu workspace no lo tiene, cargarlo directamente reportará Cannot find package '@deepseek-ai/cordis'. Así que ve al directorio del plugin e instálalo primero:
cd "E:\software-workspace\DeepSeek harness demo\service-demo" # sustituye por tu directorio
npm init -y
npm install @deepseek-ai/cordisTras la instalación, modifica package.json y añade una línea "type": "module" al objeto raíz — si no, Node adivinará el formato del módulo cada vez que cargue el plugin y spammeará avisos:
{
"name": "service-demo",
"version": "1.0.0",
"type": "module"
}Paso 2: escribe el Provider
Crea service-demo\src\provider.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)
}
}Paso 3: escribe el Consumer
Crea service-demo\src\consumer.js:
export const name = 'consumer'
export const inject = ['metrics']
export function apply(ctx) {
ctx.metrics.record('plugin_loaded', 1)
}Paso 4: declara y verifica
Crea service-demo\cordis.yml (sustituye la ruta por la tuya, fíjate en que los espacios deben escribirse como %20):
- insert:
- id: provider
name: 'file:///E:/tu-workspace/service-demo/src/provider.js'
- id: consumer
name: 'file:///E:/tu-workspace/service-demo/src/consumer.js'Usa headless para verificación rápida:
cd tu-directorio-de-workspace
dsh --profile headless --patch "./service-demo/cordis.yml" "Just reply: hi"En la salida verás:
[metrics] plugin_loaded 1
hi[metrics] plugin_loaded 1 es el consumer llamando al método del provider — tus dos plugins hablaron realmente. En este punto, has verificado personalmente las tres formas: función, objeto, clase.

La línea amarilla [metrics] plugin_loaded 1 en la salida de la terminal es el consumer llamando al método del provider vía ctx.metrics.record(...) — los dos plugins hablaron realmente.
Deja que dsh lo haga: un prompt lo resuelve
Los directorios, dependencias y dos archivos de arriba los creaste manualmente tú. Como siempre, escribir plugins es algo que dsh puede hacer por sí mismo — y conoce "cómo deben definirse los servicios, cómo deben inyectarse las dependencias" mejor que tú.
Envía este prompt directamente en el cuadro de entrada de la Web UI:
In my current workspace, help me write a "service form" plugin example: a service-providing plugin (Service subclass) and a plugin that uses it (inject depending on it), make them actually talk. First read the official dsh plugin dev docs to understand how to define services, how to inject dependencies, implement per the official spec, verify it can load and call normally, and finally tell me the result and where the files are.Irá a consultar los docs oficiales por sí mismo, instalará dependencias por sí mismo, escribirá dos plugins por sí mismo, verificará si pueden hablarse entre sí por sí mismo. Tú solo verificas qué escribió.

Errores comunes
| Problema | Qué está pasando | Cómo manejarlo |
|---|---|---|
Reporta Cannot find package '@deepseek-ai/cordis' | El directorio del plugin no instaló dependencias | npm install @deepseek-ai/cordis en el directorio del plugin |
Un montón de avisos MODULE_TYPELESS_PACKAGE_JSON | El package.json no declaró el tipo de módulo | Añade "type": "module" |
| El consumer no puede obtener el servicio | El nombre del servicio no coincide | Comprueba que el nombre en super(ctx, 'xxx') y inject: ['xxx'] son exactamente iguales |
| El plugin clase falla al cargar | Sin extends Service o no llamó a super | La forma clase debe heredar Service y super(ctx, 'service-name') |
ctx.metrics es undefined | El consumer no declaró inject | En el consumer escribe export const inject = ['metrics'] |
Lo que aprendiste en este capítulo
Apruebas si puedes completar los puntos de abajo:
- [ ] Enuncias qué aspecto tiene cada una de las tres formas, y cuándo usar cuál
- [ ] Sabes que service = una capacidad con nombre montada en
ctx;tools/llm/agentsson todos servicios - [ ] Escribes una subclase
Servicecomo provider,super(ctx, 'name')para registrar el servicio - [ ] Escribes un consumer con
inject, llamas directamente víactx.service-name.method()enapply - [ ] Sabes que el plugin en forma clase debe instalar la dependencia
@deepseek-ai/cordisen el directorio del plugin primero
