SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor

Come creare un plugin dsh per DeepSeek Harness

Guida da una cartella vuota: i campi essenziali di package.json, il file YAML di caricamento, uno strumento reale e i due hook necessari per il plugin dsh.

Che cos'è realmente un plugin dsh

Un plugin dsh è un pacchetto npm che esporta una funzione apply e include un piccolo file YAML che indica a DeepSeek Harness di caricarlo. Non esiste un SDK separato per i plugin da imparare prima. dsh è un'applicazione Cordis e l'espressione «tutto è un plugin» va intesa alla lettera: il registro degli strumenti, il ciclo dell'agente, l'archivio delle sessioni e il server Web sono tutti elementi dello stesso albero di plugin a cui si aggiunge il tuo pacchetto.

Cordis è un framework generico per la composizione, sviluppato in modo indipendente e utilizzato da anni come base del framework per chatbot Koishi. Gestisce il caricamento e lo scaricamento dei plugin e ne risolve le dipendenze. Non sa nulla degli agenti. Tutto ciò che riguarda gli agenti proviene dai pacchetti harness sovrapposti, perciò la struttura del plugin descritta sotto è così ridotta. La maggior parte delle funzionalità viene ereditata.

Un plugin ha due parti. La parte host viene eseguita in Node, registra strumenti e listener di eventi e può fornire servizi propri. La parte browser viene eseguita all'interno dell'interfaccia Web e registra gli slot dell'interfaccia. Un primo plugin è quasi sempre solo host, quindi considera facoltativa la parte browser finché non ti serve.

Questa guida è stata scritta per la versione @deepseek-ai/dsh 0.1.0-rc.7, il tag npm latest del 19 agosto 2026. dsh è una versione di anteprima per sviluppatori e il relativo README indica che ci saranno modifiche incompatibili. Ogni nome di chiave riportato sotto è stato verificato nella documentazione upstream e nel repository in quella data. Quando questa guida è stata ricontrollata il 12 ottobre 2026, il tag npm latest era 0.2.0-rc.2 e le sezioni seguenti indicano i punti in cui quella release si comporta in modo diverso. Verifica nuovamente i nomi delle chiavi prima di basarti su una di esse, perché un'API in anteprima può rinominare i campi tra una release candidate e l'altra. Se harness non è ancora in esecuzione, configuralo prima seguendo DeepSeek Harness su un VPS e la configurazione della chiave API e del modello dsh, quindi torna qui.

Carica un file di prova prima di creare il pacchetto

Creare prima il pacchetto è il modo più lento per verificare il funzionamento. Carica un solo file, verifica che il runtime richiami il tuo codice, quindi crea il pacchetto.

Crea una directory fuori dal checkout di harness e inserisci un file.

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 contiene i metadati usati per identificare il plugin nei messaggi diagnostici. apply definisce l'intero contratto: Cordis lo richiama una volta e passa un contesto associato al tuo plugin. Tutto ciò che registri su quel contesto viene annullato automaticamente quando il plugin viene rilasciato.

Nella stessa directory, scrivi cordis.yml.

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

Ora avvia un profilo applicando quel file come overlay.

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

Se dsh non è presente nel tuo PATH, npx @deepseek-ai/dsh web --patch ./scratch-plugin/cordis.yml svolge la stessa funzione. Questo percorso tramite npx può fornire una release candidate memorizzata nella cache e più vecchia rispetto alla versione descritta in questa guida. Se harness rifiuta direttamente un flag documentato, consulta le soluzioni per gli errori di installazione e versione di dsh prima di dubitare del file. Nel terminale da cui hai avviato dsh dovresti vedere [hello-plugin] plugin loaded. Se non compare nulla, la riga non è stata risolta.

Il campo name accetta il nome di un pacchetto npm o un percorso del filesystem. La documentazione upstream specifica che il percorso deve essere assoluto. Un ./hello.ts relativo è il primo elemento da controllare quando un plugin di prova non produce output. Il secondo è l'estensione del file. Il flusso documentato viene eseguito come pnpm dsh web --patch ... da un clone del repository harness. In questo contesto, le entry TypeScript vengono caricate tramite tsx. Se hai installato dsh da npm, indica nella riga un file JavaScript semplice oppure esegui prima la build del file.

--patch è un flag del launcher e il relativo overlay viene applicato per ultimo, dopo ogni bundle e dopo la patch del profilo. Un overlay di prova ha quindi sempre la precedenza. Questo è esattamente il comportamento desiderato durante le iterazioni.

Scrivere lo strumento più piccolo che svolga un'operazione utile

Una riga di log dimostra che il plugin viene caricato. Uno strumento dimostra che il plugin fa parte dell'agent.

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'] è la riga che viene spesso omessa. Le voci di una configurazione Cordis vengono avviate contemporaneamente, quindi la posizione di una riga nel file non garantisce alcun ordine di caricamento. L'ordine deriva dalle dipendenze dichiarate. inject indica a Cordis di attendere che ctx.tools esista prima di chiamare il tuo apply; senza questa dipendenza, il codice può essere eseguito quando il registro non è ancora disponibile per la registrazione.

Il resto dell'oggetto definisce il contratto che il modello utilizza. parameters è lo schema degli argomenti e execute riceve argomenti già analizzati in base a tale schema. output.schema descrive il valore restituito da execute, mentre render converte quel valore nei blocchi di contenuto che il modello legge. Mantenere separati questi due elementi consente all'interfaccia di mostrare un contenuto mentre il modello ne legge un altro.

Avvia il profilo e chiedi all'assistente di salutare qualcuno usando il nome. La risposta passa attraverso il tuo execute. La registrazione tramite ctx è reversibile, quindi la dismissione del plugin annulla automaticamente la registrazione dello strumento. Per qualsiasi risorsa che Cordis non può gestire, come un socket o un file handle, chiama ctx.effect() e passagli una funzione di dismissione.

I due punti di estensione che un primo plugin usa davvero

L’elenco completo dei punti di intervento è lungo. Due di questi coprono quasi ogni primo plugin.

Gli eventi della conversazione sono il flusso persistente e registrato. Nella versione 0.1.0-rc.7 i nomi erano session/event, turn/start, turn/end, step/start, step/end, user/message, assistant/message, assistant/chunk, tool/call e tool/result. La release 0.2.0-rc.2 non contiene assistant/chunk. Lo streaming in tempo reale è stato spostato in agent/assistant-stream, i cui frame restano nel processo e non vengono mai registrati; un nuovo evento persistente, assistant/attempt, conserva i tentativi falliti, ritentati e annullati. A questo punto si collega un listener ordinario.

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

Stampa il payload una volta e leggilo. Non copiare i nomi dei campi del payload da una guida, inclusa questa, perché la struttura del payload è l’elemento di una preview API che cambia più spesso.

Il secondo punto di estensione è la waterfall. agent/pre-step, agent/request, agent/request-error, llm/stream e gli eventi tools/* sono waterfall, e un listener waterfall ha una firma diversa. Riceve un callback next, e la catena continua soltanto se lo invoca.

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 dimentichi await next(), non hai aggiunto un hook. Hai sostituito la chiamata al modello con nulla, quindi l’agente si arresta in quel punto, perché il short circuit è il comportamento previsto per un plugin gateway che nega intenzionalmente una richiesta. Questa differenza causa la maggior parte dei problemi con i primi plugin. Scrivi la chiamata next() prima di aggiungere qualsiasi altra istruzione intorno.

agent/request racchiude la chiamata al modello. Il suo payload contiene l’agente che effettua la chiamata, il numero del turno aperto, lo step a cui appartiene la richiesta e il segnale di abort di quel turno. Per questo è il punto di intervento corretto per un logger delle richieste o per un rate limiter. Le waterfall tools/* hanno la stessa struttura, ma a un livello inferiore. tools/pre-execute consente, nega o richiede un’approvazione prima del dispatch. tools/execute racchiude il dispatch. tools/post-execute può sostituire o bloccare il risultato normalizzato. tools/result osserva soltanto l’esito congelato.

Creare un bundle installabile da altri

Un bundle è un pacchetto npm il cui package.json dichiara un campo dsh.bundle che punta al file di patch. Questa dichiarazione è l’unica differenza tra un file temporaneo e un pacchetto installabile.

{
  "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"
  }
}

Il cordis.patch.yml che si trova accanto è breve.

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

La riga name contiene il nome del pacchetto, quindi le due stringhe devono corrispondere. La riga id è il valore a cui punta un livello successivo quando un utente sovrascrive la configurazione. Scegli un valore stabile e non riutilizzarlo per un plugin diverso.

files deve elencare cordis.patch.yml. Se lo ometti, il tarball pubblicato contiene un dsh.bundle.patch che punta a un file mai incluso nel pacchetto. Di conseguenza, il pacchetto viene installato ma non aggiunge nulla all’albero.

Installalo in un profilo dalla directory che contiene la cartella del plugin.

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

dsh plugin --profile <name> inoltra gli argomenti rimanenti a pnpm nella directory del profilo, quindi add e remove si comportano come in pnpm. Disinstalla con dsh plugin --profile demo remove dsh-plugin-hello. I profili web, headless, sdk, sdk-minimal e acp inclusi vengono creati automaticamente dai template al primo utilizzo. Nella versione 0.2.0-rc.2, ogni altro nome viene creato in uno dei due modi seguenti. dsh plugin lo avvia usando soltanto il bundle di base. dsh --profile <name> --from-default-profile <template> copia l’elenco dei bundle da un template incluso. L’avvio di un profilo personalizzato che non esiste ancora non riesce e mostra un suggerimento per eseguire dsh plugin.

Perché la riga non è presente nell'albero composto

La composizione parte da un elenco di voci vuoto e applica i layer in un ordine fisso. Prima viene applicato ogni bundle indicato in dsh.profile.bundles del profilo, nell'ordine specificato. Poi viene applicato cordis.patch.yml del profilo. Seguono $DSH_HOME/cordis.patch.yml e qualsiasi overlay --patch passato dalla riga di comando. I layer successivi sostituiscono le righe precedenti con lo stesso ID.

I profili si trovano in $DSH_HOME/profiles/<name>. Una directory del profilo contiene un file package.json che include il manifest dsh.profile con l'elenco ordinato bundles, oltre al file di patch dell'utente. I nomi dei bundle vengono risolti prima dall'installazione di dsh e poi da node_modules del profilo; è qui che pnpm installa un plugin esterno all'albero principale.

dsh --profile demo --dump-config stampa l'albero completamente composto senza avviare nulla. Questo output separa i due ambiti del troubleshooting. Se l'ID della riga è assente, il problema riguarda la composizione: un nome non viene risolto oppure un file di patch non è mai stato incluso nel pacchetto. Se la riga è presente ma non accade nulla, il problema è nel codice. Verifica prima questo aspetto per evitare la maggior parte delle ipotesi.

Dove si manifestano effettivamente gli errori di caricamento

Un errore generato all'interno di apply è evidente. Il processo termina con quell'eccezione e viene mostrato uno stack trace che indica la riga del codice.

Gli errori di risoluzione sono silenziosi. Il loader segnala tramite il logger di Cordis il modulo che non riesce a risolvere, invece di terminare il processo. Il tutorial upstream avverte che questi messaggi possono non essere visibili all'avvio, perché vengono emessi prima che vengano collegati gli exporter della console. Un errore di battitura nel percorso appare quindi esattamente come un plugin caricato ma inattivo. Per questo vale la pena eseguire il controllo --dump-config precedente prima di esaminare il codice.

Durante lo sviluppo, mantieni un console.log come prima istruzione in apply. Se manca, puoi capire quale delle due parti del problema stai affrontando, e in seguito puoi rimuoverlo senza conseguenze. Su un server, esegui l'harness in primo piano mentre apporti modifiche, invece di usare un service manager. In questo modo l'output del loader raggiunge il terminale anziché un journal che dovresti consultare separatamente.

Iterare senza riavviare l'intero ambiente

In 0.1.0-rc.7 la soluzione per la parte host era semplice: riavviare. Il bundle dell'applicazione Web di quella release veniva distribuito con la riga condivisa per il ricaricamento a caldo dei moduli disabilitata e il file conteneva una nota secondo cui sarebbe stata riabilitata dopo aver testato il ciclo di vita del ricaricamento. In 0.2.0-rc.2 quell'override è stato rimosso. Il bundle di base monta @deepseek-ai/dsh-hmr in ogni profilo con root: [], come indicato dal commento "Il caricamento della configurazione del profilo viene eseguito per impostazione predefinita; le radici dei moduli sono abilitate esplicitamente." Le modifiche valide ai file di configurazione del profilo e alle patch della home vengono ora riapplicate senza riavviare. Il codice sorgente del plugin non viene ancora monitorato, a meno che la relativa directory non venga aggiunta a root di quella riga. Per aggiungere, rimuovere o aggiornare un bundle è ancora necessario un riavvio. La catena di ricaricamento lato client viene sempre montata, ma resta inattiva finché un watcher della compilazione non riscrive i bundle client; di conseguenza non ha alcun effetto neppure sulla parte Node.

Rendi rapido il riavvio invece di affidarti a un ricaricamento che devi abilitare esplicitamente. Mantieni il plugin in un unico file. Caricalo con --patch invece di installarlo in un profilo, così tra una modifica e un'esecuzione non sono necessari né un passaggio di compilazione né un passaggio con pnpm. Registra tutto tramite ctx, in modo che un riavvio non lasci duplicati dello strumento o listener obsoleti. Racchiudi ogni risorsa allocata direttamente da te in ctx.effect() e assegna un disposer effettivo, perché il sintomo tipico dell'assenza di un disposer è il fallimento della seconda esecuzione su una porta ancora occupata dalla prima.

Se sviluppi con un harness in esecuzione su un server anziché sul tuo laptop, nulla di quanto detto sopra cambia, ma l'associazione della Web UI è importante. Un avvio locale pubblica la Web UI su http://127.0.0.1:3080 e la apre nel browser predefinito, oppure non apre il browser se passi --no-open. Un avvio tramite SSH stampa soltanto l'URL dell'host, quindi sul tuo computer non viene aperta alcuna pagina. L'associazione al loopback sulla porta 3080 spiega perché in questo caso la pagina non si apre e come risolvere il problema.

La parte del browser e il livello di affidabilità

Aggiungilo solo quando il plugin richiede un'interfaccia propria. Viene dichiarato nello stesso campo dsh del bundle.

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

"platform": "web" è obbligatorio e lo scanner genera un errore se il pacchetto non espone alcun export ./client. Per questo la mappa degli export fa parte del manifest e non è un elemento facoltativo. L'entry client riceve il Context di Cordis ampliato con il tipo del runtime client, e ogni registrazione avviene all'interno di apply tramite ctx.slots.register. In questo contesto non sono consentiti effetti collaterali a livello di modulo.

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)
}

Prima di iniziare, è utile conoscere due dettagli. Nel manifest client, inject ha valore documentale e non gestisce la pianificazione: registra le dipendenze a livello di pacchetto, ma non controlla l'ordine di attivazione. external dichiara richieste di moduli esterne alla baseline, ma le note client attuali specificano che non è un meccanismo di dipendenza per un feature plugin e lo riservano all'infrastruttura, al trasporto e al codice di assemblaggio generato. In un feature plugin lascialo vuoto. Questa è la parte che cambia più rapidamente nella preview. Consulta quindi packages/client/AGENTS.md nel repository dell'harness il giorno in cui scrivi il codice, non il giorno in cui leggi una guida sull'argomento.

Pubblicare e dichiarare cosa usa il plugin

Aggiungere l'argomento dsh-plugin a un repository GitHub lo inserisce nell'elenco consultato dalle persone che cercano plugin. Questo implica chiedere la fiducia di persone che non ti conoscono e comporta degli obblighi. Questi obblighi sono il riflesso di ciò che la nostra guida alla verifica di un plugin dsh prima dell'installazione chiede ai lettori di controllare; seguire quella checklist durante la scrittura è quindi il modo più semplice per superare la verifica.

  • Blocca le dipendenze. Un intervallo con accento circonflesso in una dipendenza transitiva può fare sì che un pacchetto sicuro la settimana scorsa esegua codice diverso questa settimana. Questo è il meccanismo alla base degli attacchi alla supply chain di npm contro un server.
  • Fai dichiarare al manifest ciò che usi. L'elenco inject è un riepilogo onesto e leggibile dalle macchine dei servizi dell'harness che utilizzi. Un revisore lo legge in pochi secondi e si fa un'opinione sulla base di queste informazioni.
  • Nessuna chiamata di rete silenziosa. Se uno strumento chiama un'API, indica l'host nel README e rendi configurabile l'endpoint. Un plugin che contatta un server che non ha mai dichiarato verrà rimosso dall'elenco dalle persone che verificano questi aspetti.
  • Mantieni files essenziale. Pubblicare un'intera cartella di lavoro è il modo in cui un file contenente credenziali può finire nel registry.
  • Fornisci agli installer Git uno script prepare che esegua la build senza presupporre la presenza di componenti necessari solo allo sviluppo, e indica nel README che devono inserire esplicitamente quella build nella allowlist del pnpm-workspace.yaml del loro profilo.
  • Inserisci nel README la data della release candidate che hai compilato e utilizzato per i test. Chi legge la documentazione di un'API in anteprima deve sapere quale versione hai utilizzato.

Per vedere come appare un plugin completato dall'esterno, leggi i plugin dsh che vale la pena installare e osserva cosa comunica ogni README prima dell'installazione. Se hai scritto estensioni per un altro agent, come sono strutturati i plugin di Claude Code offre un utile confronto. L'harness ti fornisce un grafo di oggetti attivo e una registrazione reversibile. Questo offre più potere rispetto a un manifest di file e comporta anche maggiori responsabilità.

FAQ

Devo pubblicare su npm per scrivere un plugin dsh?

No. Un percorso del filesystem in un overlay cordis.yml, caricato con dsh web --patch ./scratch-plugin/cordis.yml, è sufficiente per eseguire il proprio codice all'interno dell'harness. Il percorso deve essere assoluto. Il packaging è importante solo quando il plugin deve essere installato da altri. Anche in quel caso, è possibile installare una directory locale con dsh plugin --profile demo add ./my-plugin per testare il pacchetto senza interagire con un registry.

Perché il plugin viene caricato, ma lo strumento non compare mai?

Esegui prima dsh --profile demo --dump-config. Se l'id della riga non compare nell'output, il plugin non è mai stato montato e la causa riguarda la composizione, non il codice. Se la riga è presente, verifica export const inject = ['tools']. Le voci di una configurazione Cordis vengono avviate contemporaneamente, quindi l'ordine dei file non determina l'ordine di caricamento. Senza tale dichiarazione, Cordis non attende il registro degli strumenti e il tuo apply può essere eseguito quando ctx.tools non è ancora disponibile per registrarlo.

Qual è la differenza tra cordis.yml e cordis.patch.yml?

cordis.yml è un elenco completo di voci. cordis.patch.yml è un livello applicato sopra il primo e individua le righe tramite id per inserirne di nuove o sostituire una configurazione esistente. Un bundle indica il proprio file di patch tramite dsh.bundle.patch in package.json. I livelli vengono applicati in un ordine fisso: prima ogni bundle nell'ordine indicato dal profilo, poi il file di patch del profilo, quindi $DSH_HOME/cordis.patch.yml e infine qualsiasi overlay --patch. I livelli successivi hanno precedenza.

È possibile ricaricare a caldo un plugin dsh mentre l'agent è in esecuzione?

Non nella versione 0.1.0-rc.7, il cui web bundle includeva disabilitata la riga condivisa per il ricaricamento a caldo dei moduli. Nella versione 0.2.0-rc.2 quella riga viene montata in ogni profilo, ma per impostazione predefinita ricarica solo la configurazione del profilo. Le radici dei moduli sono facoltative e vengono definite nell'elenco root. Di conseguenza, per ricaricare il codice del plugin è ancora necessario un riavvio, a meno che non se ne aggiunga una. In ogni caso, progetta il plugin per consentire un riavvio rapido: usa un solo file, caricato tramite --patch senza una fase di build, ed effettua ogni registrazione tramite ctx, così nulla resta tra un'esecuzione e l'altra. Usa ctx.effect() con un disposer per le risorse che Cordis non può ripulire autonomamente.

Questo articolo ti è stato utile?