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.
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
- Criando a estrutura de projeto de plugin local
- A essência de um plugin: um módulo TypeScript exportando uma função apply
- O significado de
export const nameeexport function apply(ctx) - Três formas de plugin: função, objeto, classe
- Registrando em cordis.yml e carregando
- Iniciando e verificando com
pnpm dsh web --patch
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:
mkdir -p scratch-plugin/src
cd scratch-plugin
pnpm init
O package.json resultante:
{
"name": "scratch-plugin",
"version": "0.1.0",
"main": "src/index.ts"
}
(2) Instalando Dependências
pnpm add -D @deepseek-ai/cordis typescript
Estrutura do projeto:
scratch-plugin/
├── src/
│ └── index.ts ← Entrada principal do plugin
├── package.json
└── node_modules/
(3) Configuração TypeScript
Crie tsconfig.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:
- Exportar uma string
name— o identificador único do plugin - Exportar uma função
apply— o ponto de entrada do plugin
▶ Exemplo 1: Plugin Mínimo
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
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:
- Prefixo de log:
[my-plugin] loaded! - Namespace de configuração:
plugins.my-plugin.config - Declaração de dependência: outros plugins referenciam por nome
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
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
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
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
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
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
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:
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
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:
- Resolução de caminho não é afetada pelo diretório de trabalho
- Comportamento consistente em diferentes métodos de inicialização
- Claro e sem ambiguidade durante depuração
(3) Iniciando e Carregando
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:
[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:
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:
plugins:
greeter:
$insert: /home/alice/projects/scratch-plugin
Inicie e verifique:
pnpm dsh web --patch
# [greeter] greeter plugin loaded
# [greeter] session started: abc-123
❓ Perguntas Frequentes
my-plugin é um nome válido. Recomendamos letras minúsculas e hífens; evite camelCase.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.[error] plugin not found: /wrong/path/to/plugin.typescript export const name = 'my-plugin' export const Config = Schema.object({ ... }) export function apply(ctx: Context) { ... } 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
- Um plugin é um módulo TypeScript exportando
name+apply(ctx); o framework chama apply ao carregar - Através de
ctx, registre timers, listeners de eventos, comandos, etc.; todos são automaticamente recuperados ao descarregar - Três formas de plugin: função (mais simples), objeto (padrão), classe (complexo/precisa de herança)
- Use
$insert+ caminho absoluto nocordis.ymlpara registrar plugins locais pnpm dsh web --patchinicia e carrega configuração de sobreposição- name deve ser globalmente único; nomes duplicados causam falha no carregamento
📝 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ê?