SSD Nodes Learn Hosting plans →
Guias Matt ConnorPor Matt Connor

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 um plugin dsh realmente é

Um plugin 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 é necessário aprender primeiro um SDK de plugins separado. 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 nós na mesma árvore de plugins à qual o seu pacote se junta.

Cordis é um framework geral de composição, desenvolvido de forma independente e usado 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 específico sobre agentes. Tudo o que está relacionado com agentes vem dos pacotes do harness colocados sobre ele, razão pela qual a estrutura do plugin abaixo é tão pequena. A maior parte do que recebe é 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. O primeiro plugin é quase sempre apenas para o host, por isso trate a parte do browser como opcional até precisar dela.

Este guia foi escrito para a versão @deepseek-ai/dsh 0.1.0-rc.7, a tag npm latest em 19 de agosto de 2026. dsh está em pré-visualização para programadores, e o seu próprio README indica que haverá alterações incompatíveis. Todos os nomes de chaves abaixo foram consultados na documentação upstream e no repositório nessa data. Quando este guia foi revisto novamente em 12 de outubro de 2026, a tag npm latest era 0.2.0-rc.2, e as secções abaixo indicam onde essa versão se comporta de forma diferente. Volte a consultar os nomes das chaves antes de depender de uma delas, porque uma API de pré-visualização pode alterar os nomes dos campos entre release candidates. Se o harness ainda não estiver em execução, configure-o primeiro com DeepSeek Harness num VPS e a chave 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 o primeiro plugin realmente utiliza

A lista completa de pontos de integração é longa. Estes dois abrangem quase todos os primeiros plugins.

Os eventos de conversação são o fluxo persistente registado. Em 0.1.0-rc.7, os nomes eram session/event, turn/start, turn/end, step/start, step/end, user/message, assistant/message, assistant/chunk, tool/call e tool/result. A versão 0.2.0-rc.2 não tem assistant/chunk. O streaming em tempo real foi movido para agent/assistant-stream. Os respetivos frames permanecem no processo e nunca são registados. Um novo evento persistente, assistant/attempt, mantém as tentativas falhadas, repetidas e canceladas. Ligue 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 o formato do payload é a parte de uma API em preview que mais muda.

O segundo ponto de extensão é o 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 do modelo por nada, e o agente para nesse ponto, porque o short-circuiting é o comportamento previsto para um plugin de gateway que recusa deliberadamente um pedido. Esta diferença causa a maior parte da confusão nos primeiros plugins. Escreva a chamada next() antes de escrever qualquer coisa à sua volta.

agent/request envolve a própria chamada do modelo. O 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 abort desse turno. É isso que o torna o ponto de integração correto para um logger de pedidos ou um limitador de taxa. Os waterfalls tools/* têm a mesma estrutura um nível abaixo. tools/pre-execute permite, recusa 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 imutável.

Empacote-o como um bundle que outras pessoas podem 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 provisório e algo que pode ser instalado.

{
  "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, por isso escolha algo estável e nunca o reutilize para outro plugin.

files tem de listar cordis.patch.yml. Se o omitir, o tarball publicado contém um dsh.bundle.patch que aponta para um ficheiro que nunca foi incluído, pelo que o pacote é instalado mas não contribui para a árvore.

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

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

dsh plugin --profile <name> encaminha o resto dos argumentos para o pnpm dentro desse diretório de perfil, por isso add e remove comportam-se como no pnpm. Desinstale com dsh plugin --profile demo remove dsh-plugin-hello. Os perfis web, headless, sdk, sdk-minimal e acp fornecidos são criados a partir de templates na primeira utilização. Na versão 0.2.0-rc.2, qualquer outro nome é criado de uma de duas formas. dsh plugin inicia-o apenas com o bundle base. dsh --profile <name> --from-default-profile <template> copia a lista de bundles de um template fornecido. Iniciar um perfil personalizado que ainda não existe falha e apresenta uma indicação para executar 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 tudo

Em 0.1.0-rc.7, a resposta para a parte do host era simples: reiniciar. O bundle da aplicação Web dessa versão vinha com a linha partilhada de recarregamento a quente de módulos desativada, e o ficheiro continha uma nota a indicar que seria reativada depois de o ciclo de recarregamento ser testado. Em 0.2.0-rc.2, essa substituição desapareceu. O bundle base monta @deepseek-ai/dsh-hmr em todos os perfis com root: [], sob o comentário "Profile configuration reloads by default; module roots are opt-in." As edições válidas aos ficheiros de configuração do perfil e de patch do diretório home são agora reaplicadas sem reiniciar. O código-fonte do seu plugin continua sem ser monitorizado, a menos que adicione o respetivo diretório a root dessa linha. Adicionar, remover ou atualizar um bundle continua a exigir um reinício. A cadeia de recarregamento do cliente é sempre montada, mas permanece inativa até um monitor de compilação reescrever os bundles do cliente. Por isso, também não tem efeito sobre a parte Node.

Torne o reinício rápido, em vez de depender de um recarregamento que tem de ativar. Mantenha o plugin num único ficheiro. Carregue-o com --patch, em vez de o instalar num perfil, para que não exista nenhum passo de compilação nem nenhum passo 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 diretamente em ctx.effect() com um disposer real, porque o sintoma habitual da ausência de um disposer é a segunda execução falhar numa porta que a primeira ainda mantém ocupada.

Se desenvolver com um harness executado num servidor, em vez de no seu portátil, nada do que foi descrito acima muda, mas a ligação da Web UI é relevante. Um arranque local disponibiliza a Web UI em http://127.0.0.1:3080 e abre-a no navegador predefinido, ou ignora o navegador se passar --no-open. Um arranque através de SSH apenas imprime o URL do host, por isso nada é aberto na sua máquina. A ligação loopback na porta 3080 explica por que motivo a página não abre nesse caso e o que fazer.

A parte do browser e o grau de confiança adequado

Adicione isto apenas quando o plugin precisar da sua própria interface. A declaração fica 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 manifest, 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 cada registo ocorre dentro de apply através de ctx.slots.register. Não são permitidos efeitos secundários ao nível do módulo nesse contexto.

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 manifest do cliente é documentação, não agendamento: regista as dependências ao nível do pacote e não controla a ordem de ativação. external declara pedidos de módulos fora da baseline, mas as notas atuais do cliente indicam que não é um mecanismo de dependências para um plugin de funcionalidade e reservam-no para infraestrutura, transporte e código de assembly gerado. Deixe-o vazio num plugin de funcionalidade. Esta é a parte que muda mais rapidamente na preview. 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 consultar 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 do dsh?

Não. Um caminho do sistema de ficheiros numa sobreposição cordis.yml, carregado 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ó é relevante 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 tocar num registry.

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

Execute primeiro dsh --profile demo --dump-config. Se o id da sua 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, verifique export const inject = ['tools']. As entradas de uma configuração Cordis arrancam em simultâneo, por isso 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 quando ctx.tools ainda não está disponível para efetuar 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 delas, que identifica as linhas pelo 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 por uma ordem fixa: primeiro cada bundle pela ordem indicada no profile, depois o ficheiro de patch do profile, depois $DSH_HOME/cordis.patch.yml e, por fim, qualquer sobreposição --patch. As camadas posteriores prevalecem.

Posso fazer hot reload de um plugin dsh enquanto o agente está em execução?

Não em 0.1.0-rc.7, cujo web bundle tinha a linha partilhada de hot module reload desativada. Em 0.2.0-rc.2, essa linha é montada em todos os profiles, mas, por predefinição, apenas recarrega a configuração do profile. As raízes dos módulos são opcionais através da lista root, por isso o código do seu plugin continua a exigir um restart, a menos que adicione uma raiz. Em qualquer dos casos, conceba o plugin para um restart rápido: um único ficheiro, carregado através de --patch sem uma etapa de build, e com todos os registos efetuados através de ctx, para que nada fique retido entre execuções. Use ctx.effect() com um disposer para os recursos que o Cordis não consiga limpar sozinho.