SSD Nodes Learn 🎉 VPS desde $5.50/mês
Guias Matt ConnorPor Matt Connor · Atualizado 2026-08-21

Como criar um plugin dsh para o DeepSeek Harness

Aprenda a criar um plugin dsh do zero: campos essenciais do package.json, patch YAML de carregamento, uma ferramenta real e os dois hooks necessários.

O que é, na prática, um plugin do dsh

Um plugin do dsh é um pacote npm que exporta uma função apply e inclui um pequeno ficheiro YAML que indica ao DeepSeek Harness para o carregar. Não existe um SDK de plugins separado que seja necessário aprender primeiro. O dsh é uma aplicação Cordis, e a expressão "tudo é um plugin" é literal: o registo de ferramentas, o ciclo do agente, o armazenamento de sessões e o servidor web são todos elementos da mesma árvore de plugins à qual o seu pacote se junta.

O Cordis é um framework geral de composição, desenvolvido de forma independente e utilizado há anos como base do framework de chatbot Koishi. Gere o carregamento e o descarregamento e resolve as dependências entre plugins. Não tem conhecimento sobre agentes. Tudo o que está relacionado com agentes vem dos pacotes do harness que são dispostos sobre ele. É por isso que a estrutura do plugin abaixo é tão pequena. A maior parte do que obtém é herdada.

Um plugin tem duas partes. A parte do host é executada no Node, regista ferramentas e listeners de eventos e pode fornecer os seus próprios serviços. A parte do browser é executada dentro da Web UI e regista slots da interface. Um primeiro plugin é quase sempre apenas para o host. Considere a parte do browser opcional até precisar dela.

Este guia foi escrito com base na versão @deepseek-ai/dsh 0.1.0-rc.7, a tag npm latest de 19 August 2026. O dsh é uma versão de pré-visualização para programadores, e o seu próprio README informa que haverá alterações incompatíveis. Cada nome de chave abaixo foi consultado na documentação upstream e no repositório nessa data. Consulte-os novamente antes de depender de algum deles, porque uma API de pré-visualização pode mudar o nome dos campos entre release candidates. Se o harness ainda não estiver em execução, configure-o primeiro com DeepSeek Harness numa VPS e a chave de API e a configuração do modelo do dsh e, depois, volte aqui.

Carregue um único ficheiro de teste antes de empacotar qualquer coisa

Começar pelo empacotamento é a forma mais lenta de aprender isto. Carregue um único ficheiro, confirme que o runtime chama o seu código e só depois faça o empacotamento.

Crie uma pasta fora do checkout do harness e coloque nela um ficheiro.

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

export const name = 'hello-plugin'

export function apply(ctx: Context) {
  console.log('[hello-plugin] plugin loaded')
}

export const name são metadados usados para identificar o plugin nos diagnósticos. apply é o contrato completo: o Cordis chama-o uma vez e passa um contexto associado ao seu plugin. Tudo o que registar nesse contexto é desfeito automaticamente quando o plugin é eliminado.

Ao lado dele, escreva cordis.yml.

- insert:
    - id: hello
      name: '/absolute/path/to/scratch-plugin/hello.ts'

Agora inicie um perfil com esse ficheiro aplicado por cima.

dsh web --patch ./scratch-plugin/cordis.yml

Se dsh não estiver no seu PATH, npx @deepseek-ai/dsh web --patch ./scratch-plugin/cordis.yml faz o mesmo trabalho. Essa opção com npx pode fornecer uma release candidate antiga em cache em vez da versão descrita neste guia. Por isso, se o harness rejeitar imediatamente uma flag documentada, consulte as correções para erros de instalação e versão do dsh antes de suspeitar do seu próprio ficheiro. Deve ver [hello-plugin] plugin loaded no terminal que iniciou o dsh. Se nada aparecer, a linha não foi resolvida.

O campo name aceita o nome de um pacote npm ou um caminho do sistema de ficheiros. A documentação upstream indica que o caminho tem de ser absoluto. Um ./hello.ts relativo é a primeira coisa a verificar quando um plugin de teste não produz saída. A segunda é a extensão do ficheiro. O fluxo documentado é executado como pnpm dsh web --patch ... a partir de um clone do repositório do harness, onde as entradas TypeScript são carregadas através de tsx. Se o seu dsh veio do npm, aponte a linha para JavaScript simples ou faça primeiro o build do ficheiro.

--patch é uma flag do launcher e a sua sobreposição é aplicada por último, depois de cada bundle e da sua própria alteração de perfil. Por isso, uma sobreposição de teste ganha sempre, que é exatamente o comportamento pretendido enquanto faz iterações.

Escreva a menor ferramenta que faça algo útil

Uma linha de log prova que o plugin foi carregado. Uma ferramenta prova que o plugin faz parte do agente.

import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'greet-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'greet',
    description: 'Greet someone by name.',
    parameters: {
      name: { type: 'string', required: true, description: 'The name to greet' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      return `Hello, ${args.name}!`
    },
  }))
}

export const inject = ['tools'] é a linha que costuma ser omitida. As entradas de uma configuração do Cordis são iniciadas em simultâneo, portanto a posição de uma linha no ficheiro não garante nada sobre a ordem de carregamento. A ordem vem das dependências declaradas. inject diz ao Cordis para esperar até ctx.tools existir antes de chamar apply, e sem isso o seu código pode ser executado quando o registo ainda não está disponível para receber o registo da ferramenta.

O resto do objeto define o contrato que o modelo vê. parameters é o esquema dos argumentos, e execute recebe os argumentos já analisados de acordo com esse esquema. output.schema descreve o valor devolvido por execute, enquanto render converte esse valor nos blocos de conteúdo que o modelo lê. Manter estes dois elementos separados permite que a interface apresente uma coisa enquanto o modelo lê outra.

Inicie o perfil e peça ao assistente para cumprimentar alguém pelo nome. A resposta regressa através do seu execute. O registo através de ctx é reversível, portanto eliminar o plugin anula automaticamente o registo da ferramenta. Para qualquer recurso que o Cordis não consiga conhecer, como um socket ou um identificador de ficheiro, chame ctx.effect() e passe-lhe uma função de eliminação.

Os dois pontos de extensão que um primeiro plugin realmente utiliza

A lista completa de pontos de extensão é longa. Dois deles abrangem quase todos os primeiros plugins.

Os eventos de conversação são o fluxo persistente e registado. Os nomes são session/event, turn/start, turn/end, step/start, step/end, user/message, assistant/message, assistant/chunk, tool/call e tool/result. Associe um listener normal.

ctx.on('tool/call', (payload) => {
  console.log('[my-plugin] tool/call', JSON.stringify(payload))
})

Imprima o payload uma vez e leia-o. Não copie os nomes dos campos do payload de nenhum guia, incluindo este, porque a estrutura dos payloads é a parte de uma API de pré-visualização que mais muda.

O segundo ponto de extensão é a waterfall. agent/pre-step, agent/request, agent/request-error, llm/stream e os eventos tools/* são waterfalls, e um listener de waterfall tem uma assinatura diferente. Recebe um callback next, e a cadeia só continua se o callback for chamado.

ctx.on('agent/request', async (payload, next) => {
  const startedAt = Date.now()
  const downstream = await next()
  console.log('[my-plugin] model request took', Date.now() - startedAt, 'ms')
  return downstream
})

Se esquecer await next(), não adicionou um hook. Substituiu a chamada ao modelo por nada, e o agente para nesse ponto, porque o curto-circuito é o comportamento previsto para um plugin de gateway que nega deliberadamente um pedido. Esta diferença causa a maior parte da confusão com os primeiros plugins. Escreva a chamada next() antes de escrever qualquer outra coisa à sua volta.

agent/request envolve a própria chamada ao modelo. O seu payload contém o agente que faz a chamada, o número do turno aberto, o passo a que o pedido pertence e o sinal de cancelamento desse turno, o que faz deste o ponto adequado para um logger de pedidos ou um limitador de taxa. As waterfalls tools/* têm a mesma estrutura um nível abaixo. tools/pre-execute permite, nega ou solicita aprovação antes do dispatch. tools/execute envolve o dispatch. tools/post-execute pode substituir ou bloquear o resultado normalizado. tools/result apenas observa o resultado congelado.

Empacote-o como um bundle que outras pessoas possam instalar

Um bundle é um pacote npm cujo package.json declara um campo dsh.bundle que aponta para o ficheiro de patch. Essa declaração é toda a diferença entre um ficheiro temporário e algo instalável.

{
  "name": "dsh-plugin-hello",
  "version": "0.1.0",
  "type": "module",
  "main": "lib/index.js",
  "files": ["lib", "cordis.patch.yml", "README.md", "LICENSE"],
  "engines": { "node": "^22.19 || >=24", "dsh": ">=0.1.0-rc.6" },
  "dsh": { "bundle": { "patch": "./cordis.patch.yml" } },
  "keywords": ["dsh-plugin", "deepseek-harness"],
  "scripts": { "build": "tsdown", "prepare": "pnpm run build" },
  "exports": {
    ".": { "types": "./lib/index.d.ts", "default": "./lib/index.js" },
    "./cordis.patch.yml": "./cordis.patch.yml",
    "./package.json": "./package.json"
  }
}

O cordis.patch.yml que fica ao lado é curto.

- insert:
    - id: dsh-plugin-hello
      name: dsh-plugin-hello

A linha name é o nome do pacote, por isso essas duas strings têm de coincidir. A linha id é o alvo de uma camada posterior quando um utilizador substitui a sua configuração. Escolha um valor estável e nunca o reutilize para outro plugin.

files tem de listar cordis.patch.yml. Se o omitir, o tarball publicado inclui um dsh.bundle.patch que aponta para um ficheiro que nunca foi empacotado. Como resultado, o pacote é instalado, mas não contribui para a árvore.

Instale-o num profile a partir do diretório que contém a pasta do plugin.

dsh plugin --profile demo add ./dsh-plugin-hello
dsh --profile demo --dump-config
dsh --profile demo

dsh plugin --profile <name> encaminha o restante dos argumentos para o pnpm dentro desse diretório do profile. Por isso, add e remove comportam-se como no pnpm. Desinstale com dsh plugin --profile demo remove dsh-plugin-hello. Os profiles web e headless são criados automaticamente a partir dos templates fornecidos na primeira utilização. Qualquer outro nome de profile tem de ser criado através de dsh plugin.

Por que a sua linha está ausente da árvore composta

A composição começa com uma lista de entradas vazia e empilha as camadas numa ordem fixa. Primeiro, cada bundle indicado em dsh.profile.bundles no perfil, pela ordem apresentada. Depois, o próprio cordis.patch.yml do perfil. Em seguida, $DSH_HOME/cordis.patch.yml. Por fim, qualquer overlay --patch da linha de comandos. As camadas posteriores substituem as linhas anteriores pelo id.

Os perfis ficam em $DSH_HOME/profiles/<name>. Um diretório de perfil contém um package.json com o manifesto dsh.profile e a lista ordenada bundles, além do ficheiro de patch do utilizador. Os nomes dos bundles são resolvidos primeiro a partir da instalação do dsh e depois a partir de node_modules do perfil. É nesse local que o pnpm instala um plugin fora da árvore principal.

dsh --profile demo --dump-config apresenta a árvore totalmente composta sem iniciar nada, e esse resultado define a separação para a depuração. Se o id da sua linha estiver ausente, o problema está na composição: um nome não é resolvido ou um ficheiro de patch nunca foi empacotado. Se a linha estiver presente e nada acontecer, o problema está no seu código. Responda primeiro a essa pergunta para evitar a maior parte das tentativas sem direção.

Onde os erros de carregamento realmente aparecem

Um erro lançado dentro de apply é evidente. O processo termina com essa exceção, e recebe um stack trace que aponta para a sua própria linha.

As falhas de resolução são silenciosas. O carregador comunica um módulo que não consegue resolver através do logger do Cordis, em vez de terminar o processo. O tutorial upstream avisa que estas mensagens podem passar despercebidas durante o arranque, porque são emitidas antes de os exporters da consola serem associados. Por isso, um erro no caminho parece exatamente um plugin que foi carregado mas não fez nada. É por isso que vale a pena executar a verificação de --dump-config acima antes de ler qualquer código.

Mantenha um console.log como primeira instrução em apply enquanto desenvolve. A ausência dele indica qual das duas partes do problema está afetada e não custa nada removê-lo mais tarde. Num servidor, execute o harness em primeiro plano enquanto faz alterações, em vez de o executar através de um gestor de serviços. Assim, a saída do carregador chega ao seu terminal, em vez de ficar num journal que terá de consultar.

Iterar sem reiniciar todo o ambiente

A resposta honesta para a parte do host hoje é reiniciar. O pacote da aplicação Web é lançado com a recarga a quente partilhada de módulos desativada, e o ficheiro contém uma nota a indicar que será reativada depois de o ciclo de vida da recarga ser testado. A cadeia de recarga do cliente está sempre montada, mas permanece inativa até um watcher de compilação reescrever os bundles do cliente. Por isso, também não faz nada pela sua parte Node.

Torne o reinício rápido em vez de tentar obter uma recarga que ainda não existe. Mantenha o plugin num único ficheiro. Carregue-o com --patch em vez de o instalar num perfil, para que não haja uma etapa de compilação nem uma etapa de pnpm entre uma edição e uma execução. Registe tudo através de ctx, para que um reinício não deixe uma ferramenta duplicada nem um listener obsoleto. Envolva tudo o que alocar por sua conta em ctx.effect() com um disposer real, porque o sintoma habitual de um disposer em falta é a segunda execução falhar numa porta que a primeira ainda mantém ocupada.

Se desenvolver com um harness executado num servidor, e não no seu portátil, nada do que foi referido acima muda. No entanto, a ligação da Web UI é importante. A ligação loopback na porta 3080 explica por que a página não abre sozinha e o que fazer a esse respeito.

A parte do navegador e até que ponto confiar nela

Adicione isto apenas quando o seu plugin precisar de uma interface própria. É declarado no mesmo campo dsh que o bundle.

{
  "dsh": {
    "client": {
      "platform": "web",
      "inject": [],
      "external": [],
      "immediately": false
    }
  },
  "exports": {
    ".": "./src/index.ts",
    "./client": "./src/client/apply.ts",
    "./package.json": "./package.json"
  }
}

"platform": "web" é obrigatório, e o scanner gera um erro se o pacote não tiver uma exportação ./client. Por isso, o mapa de exportações faz parte do manifesto, e não é apenas uma conveniência. A entrada do cliente recebe o Context do Cordis alargado com o tipo de runtime do cliente, e todos os registos acontecem dentro de apply através de ctx.slots.register. Não são permitidos efeitos secundários ao nível do módulo.

import type { Context } from 'cordis'
import type { DshClientContext } from '@deepseek-ai/dsh-client-runtime'

export async function apply(ctx: Context & DshClientContext) {
  ctx.slots.register({ name: 'domain.entry.slot' }, MyComponent)
}

Há dois detalhes que deve conhecer antes de começar. inject no manifesto do cliente é documentação, não agendamento: regista as dependências do pacote, mas não controla a ordem de ativação. external é onde declara pedidos de módulos fora da base, para que sejam materializados antes de o seu plugin os solicitar. Esta é a parte que muda mais rapidamente na versão de pré-visualização. Por isso, leia packages/client/AGENTS.md no repositório do harness no dia em que escrever o código, e não no dia em que ler um guia sobre o assunto.

Publique e informe o que o seu plugin acede

Adicionar o tópico dsh-plugin a um repositório GitHub coloca-o na lista que as pessoas consultam quando procuram plugins. Isso representa uma reivindicação de confiança por parte de desconhecidos e implica responsabilidades. Essas responsabilidades são o reflexo do que o nosso guia para avaliar um plugin dsh antes de o instalar recomenda verificar. Por isso, escrever tendo essa lista de verificação como referência é a forma mais simples de cumprir esses requisitos.

  • Fixe as dependências. Um intervalo com acento circunflexo numa dependência transitiva pode fazer com que um pacote que era seguro na semana passada execute código diferente esta semana. Esse é precisamente o mecanismo por trás dos ataques à cadeia de fornecimento do npm contra um servidor.
  • Faça o manifesto indicar o que o plugin acede. A sua lista inject é um resumo honesto e legível por máquinas dos serviços do harness que utiliza. Um revisor consegue lê-la em poucos segundos e formar uma opinião a partir dela.
  • Não faça chamadas de rede silenciosas. Se uma ferramenta chamar uma API, indique o host no README e torne o endpoint configurável. Um plugin que contacta um servidor não mencionado será removido da lista pelas pessoas que auditam este tipo de software.
  • Mantenha files restrito. Publicar uma pasta de trabalho completa é uma forma de fazer chegar um ficheiro de credenciais deixado por engano ao registry.
  • Dê aos instaladores Git um script prepare que faça o build sem pressupor dependências exclusivas de desenvolvimento. Indique também no README que devem permitir esse build na lista de permissões de pnpm-workspace.yaml do perfil.
  • Registe no README a data correspondente ao release candidate contra o qual o plugin foi compilado e testado. Quem utiliza uma API em preview precisa de saber qual versão foi utilizada.

Para ver como um plugin concluído é apresentado externamente, leia os plugins dsh que vale a pena instalar e observe o que cada README informa antes da instalação. Se já escreveu extensões para outro agente, como os plugins do Claude Code são estruturados oferece um contraste útil. O harness fornece um grafo de objetos ativo e um registo reversível. Isso dá mais poder do que um manifesto de ficheiros e também implica mais responsabilidade.

FAQ

Preciso publicar no npm para escrever um plugin dsh?

Não. Um caminho de sistema de ficheiros numa sobreposição cordis.yml, carregada com dsh web --patch ./scratch-plugin/cordis.yml, é suficiente para executar o seu próprio código dentro do harness. O caminho tem de ser absoluto. O empacotamento só é importante quando outra pessoa instala o plugin. Mesmo nesse caso, pode instalar uma pasta local com dsh plugin --profile demo add ./my-plugin para testar a forma empacotada sem aceder a um registry.

Por que o meu plugin é carregado, mas a ferramenta nunca aparece?

Execute primeiro dsh --profile demo --dump-config. Se o seu id de linha não aparecer nessa saída, o plugin nunca foi montado e a causa está na composição, não no código. Se a linha estiver presente, procure export const inject = ['tools']. As entradas numa configuração do Cordis iniciam em simultâneo, portanto a ordem dos ficheiros não determina a ordem de carregamento. Sem essa declaração, o Cordis não espera pelo registo de ferramentas, e o seu apply pode ser executado num momento em que ctx.tools ainda não está disponível para fazer o registo.

Qual é a diferença entre cordis.yml e cordis.patch.yml?

cordis.yml é uma lista completa de entradas. cordis.patch.yml é uma camada aplicada sobre uma lista, que procura linhas por id para inserir novas linhas ou substituir uma configuração existente. Um bundle aponta para o seu próprio ficheiro de patch através de dsh.bundle.patch em package.json. As camadas são aplicadas numa ordem fixa: primeiro todos os bundles pela ordem indicada no perfil, depois o ficheiro de patch do perfil, depois $DSH_HOME/cordis.patch.yml e, por fim, qualquer sobreposição --patch. As camadas posteriores prevalecem.

Posso recarregar um plugin dsh enquanto o agente está em execução?

Não para a parte do host no perfil web, a partir de 0.1.0-rc.7. Esse bundle fornece a linha partilhada de hot module reload desativada, com uma nota no ficheiro que indica que ela será reativada depois de o ciclo de vida do recarregamento ser testado. Em vez disso, planeie um reinício rápido: use um único ficheiro, carregado através de --patch sem uma etapa de build, e faça todos os registos através de ctx para que nada permaneça entre uma execução e a seguinte. Use ctx.effect() com um disposer para recursos que o Cordis não consegue limpar sozinho.