DeepSeek Harness: Primeiro Plugin

Última atualização: 2026-08-31

Escrever seu primeiro plugin é o passo-chave para entender profundamente o DSH — saltando de "usar o framework" para "estender o framework." Esta lição começa criando um projeto local e progressivamente completa um plugin Cordis carregável e executável.

💡 Dica: O protocolo central de um plugin DSH é mínimo — basta exportar name e apply. Quando o framework chama apply(ctx), o plugin registra capacidades através de ctx; quando o plugin é descarregado, recursos registrados em ctx são automaticamente recuperados.

📋 Pré-requisitos: Ter completado 08-community-plugins.md, familiarizado com a visão geral do ecossistema de plugins

1. O Que Você Vai Aprender

Estrutura de Diretório do Plugin


2. Criando um Projeto Local

(1) Inicializando o Diretório do Projeto

Cada plugin DSH é essencialmente um pacote Node.js. Vamos configurar um do zero:

BASH
mkdir -p scratch-plugin/src
cd scratch-plugin
pnpm init

O package.json resultante:

JSON
{
  "name": "scratch-plugin",
  "version": "0.1.0",
  "main": "src/index.ts"
}

(2) Instalando Dependências

BASH
pnpm add -D @deepseek-ai/cordis typescript

Estrutura do projeto:

TEXT 📖 Somente leitura
scratch-plugin/
├── src/
│   └── index.ts      ← Entrada principal do plugin
├── package.json
└── node_modules/

(3) Configuração TypeScript

Crie tsconfig.json:

JSON
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

3. Protocolo Central do Plugin

(1) Plugin Mínimo

Um plugin DSH só precisa satisfazer duas condições:

  1. Exportar uma string name — o identificador único do plugin
  2. Exportar uma função apply — o ponto de entrada do plugin

▶ Exemplo 1: Plugin Mínimo

TYPESCRIPT
import { Context } from '@deepseek-ai/cordis'

export const name = 'my-plugin'

export function apply(ctx: Context) {
  ctx.logger.info('my-plugin loaded!')
}

Este é um plugin completo. Após o framework carregá-lo, ele chama apply(ctx), e ctx.logger.info() emite um log.

(2) Ciclo de Vida de Carregamento do Plugin

100%
graph LR
    LOAD[Framework Carrega Plugin] --> CALL[Chama apply<br/>ctx é o "mundo" do plugin]
    CALL --> RUN[Plugin em Execução]
    UNLOAD[Descarregar Plugin] --> CLEAN[Recursos registrados em ctx<br/>recuperados automaticamente]

apply é chamado apenas uma vez quando o plugin carrega. Se o plugin precisa executar continuamente, registre timers, listeners, etc. dentro de apply.

(3) Propósito do name

name é a identidade do plugin, usada para:

TYPESCRIPT
export const name = 'my-plugin'

⚠️ name deve ser globalmente único; duplicar o nome de um plugin existente causará falha no carregamento.


4. Três Formas de Plugin

Cordis suporta três estilos de escrita de plugin. São funcionalmente equivalentes; escolha com base na complexidade:

(1) Forma Função (Mais Simples)

▶ Exemplo 2: Plugin na Forma Função

TYPESCRIPT
import { Context } from '@deepseek-ai/cordis'

export const name = 'hello-fn'

export function apply(ctx: Context) {
  ctx.logger.info('hello from function plugin')
}

Caso de uso: Ferramentas simples, registro único.

(2) Forma Objeto

TYPESCRIPT
import { Context } from '@deepseek-ai/cordis'

export default {
  name: 'hello-obj',
  apply(ctx: Context) {
    ctx.logger.info('hello from object plugin')
  }
}

Caso de uso: Plugins de complexidade média que precisam exportar múltiplos campos (ex.: Config, inject).

(3) Forma Classe

TYPESCRIPT
import { Context } from '@deepseek-ai/cordis'

export default class HelloClass {
  static name = 'hello-class'

  constructor(private ctx: Context) {
    ctx.logger.info('hello from class plugin')
  }
}

Caso de uso: Plugins complexos que precisam de gerenciamento de estado interno ou implementam classes base de serviço.

(4) Comparação das Três Formas

Dimensão Função Objeto Classe
Complexidade Baixa Média Alta
Gerenciamento de estado Closures Closures Propriedades de instância
Exportar Config Export separado Campo do objeto Propriedade estática
Herança Não suportado Não suportado Suportado
Melhor para Plugins de ferramenta Plugins padrão Plugins de serviço

5. Fazendo o Plugin "Fazer Algo"

(1) Registrando um Log Periódico

▶ Exemplo 3: Plugin Heartbeat com Timer

TYPESCRIPT
import { Context } from '@deepseek-ai/cordis'

export const name = 'heartbeat'

export function apply(ctx: Context) {
  ctx.setInterval(() => {
    ctx.logger.info('heartbeat tick')
  }, 60000)
}

Timers registrados com ctx.setInterval são automaticamente limpos quando o plugin é descarregado — esta é a vantagem central da auto-limpeza do Cordis.

(2) Ouvindo Eventos

TYPESCRIPT
import { Context } from '@deepseek-ai/cordis'

export const name = 'welcome'

export function apply(ctx: Context) {
  ctx.on('session/created', (session) => {
    ctx.logger.info(`new session: ${session.id}`)
  })
}

Listeners registrados com ctx.on também são automaticamente removidos ao descarregar.

(3) Registrando Comandos

TYPESCRIPT
import { Context } from '@deepseek-ai/cordis'

export const name = 'hello-cmd'

export function apply(ctx: Context) {
  ctx.command('hello <name:text>')
    .action(({ session }, name) => {
      return `Hello, ${name}!`
    })
}

6. Registrando em cordis.yml e Carregando

(1) Configuração cordis.yml

Registre o plugin local no cordis.yml na raiz do projeto DSH:

YAML
plugins:
  my-plugin:
    $insert: /absolute/path/to/scratch-plugin

$insert injeta um plugin local na lista de plugins. O caminho deve ser absoluto.

(2) Caminhos Absolutos vs. Relativos

YAML
plugins:
  my-plugin:
    $insert: /home/alice/plugins/scratch-plugin   # ✅ Caminho absoluto
    # $insert: ./scratch-plugin                    # ⚠️ Caminho relativo funciona mas não é recomendado

Razões para preferir caminhos absolutos:

(3) Iniciando e Carregando

BASH
pnpm dsh web --patch

O parâmetro --patch diz ao DSH para ler $insert e outras operações de substituição do cordis.yml, sobrepondo plugins locais à configuração padrão.

(4) Verificando o Carregamento

Após iniciar, verifique o log no terminal:

TEXT 📖 Somente leitura
[my-plugin] loaded!

Ou busque por my-plugin na lista de plugins da Web UI.


▶ Exemplo 7:Plugin Greeter

Combine o conhecimento acima em um exemplo completo:

TYPESCRIPT
import { Context } from '@deepseek-ai/cordis'

export const name = 'greeter'

export function apply(ctx: Context) {
  ctx.logger.info('greeter plugin loaded')

  ctx.on('session/created', (session) => {
    ctx.logger.info(`session started: ${session.id}`)
  })

  ctx.setInterval(() => {
    ctx.logger.info('greeter heartbeat')
  }, 300000)
}

Configuração cordis.yml:

YAML
plugins:
  greeter:
    $insert: /home/alice/projects/scratch-plugin

Inicie e verifique:

BASH
pnpm dsh web --patch
# [greeter] greeter plugin loaded
# [greeter] session started: abc-123

❓ Perguntas Frequentes

P O nome do plugin pode conter hífens?
R Sim, my-plugin é um nome válido. Recomendamos letras minúsculas e hífens; evite camelCase.
P A função apply pode ser async?
R Sim. async function apply(ctx) é perfeitamente válida; o framework aguardará o apply assíncrono. Nota: até o apply assíncrono completar, o plugin está em estado pendente e plugins que dependem dele não serão carregados.
P O que acontece se o caminho $insert estiver errado?
R O DSH reportará um erro e pulará o plugin na inicialização; não causará crash da aplicação inteira. O terminal mostrará algo como [error] plugin not found: /wrong/path/to/plugin.
P Como exportar Config de um plugin na forma função?
R Exporte separadamente: typescript export const name = 'my-plugin' export const Config = Schema.object({ ... }) export function apply(ctx: Context) { ... }
P O mesmo plugin pode ser carregado múltiplas vezes?
R Não por padrão — name é globalmente único. Se precisar de múltiplas instâncias, use configuração isolate para criar escopos isolados (veja 20-scope.md).
P Preciso reiniciar toda vez que alterar o código durante desenvolvimento local?
R Sim, pnpm dsh web --patch não suporta hot reload. Durante o desenvolvimento, você pode usar --dump-config para verificar a configuração, ou veja 18-hot-reload.md para mecanismos HMR.

📖 Resumo


📝 Exercícios

1. ⭐ Básico: Siga os passos desta lição para criar um plugin hello-world na forma função que exiba o log "hello world!" em apply, registre-o no cordis.yml e verifique iniciando.

2. ⭐⭐ Intermediário: Reescreva o plugin hello-world nas formas objeto e classe, carregue cada um separadamente e confirme que todos os três produzem a mesma saída.

3. ⭐⭐⭐ Desafio: Escreva um plugin uptime que registre o tempo de carregamento do plugin e exiba "Executando por N minutos" a cada minuto via ctx.setInterval. Pense: o timer deve ser reiniciado se o plugin for descarregado e recarregado? Por quê?

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%