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'affermazione «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 nodi dello stesso albero di plugin a cui si aggiunge il pacchetto.
Cordis è un framework generale per la composizione, sviluppato in modo indipendente e utilizzato da anni come base del framework chatbot Koishi. Gestisce il caricamento e lo scaricamento e risolve le dipendenze tra i plugin. Non conosce nulla degli agenti. Tutto ciò che riguarda gli agenti proviene dai pacchetti harness sovrapposti, perciò la struttura del plugin riportata di seguito è 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 della Web UI e registra gli slot dell'interfaccia. Il primo plugin è quasi sempre solo host; considera quindi opzionale la parte browser finché non ti serve.
Questa guida è stata scritta usando la versione @deepseek-ai/dsh 0.1.0-rc.7, il tag npm latest del 19 agosto 2026. dsh è una developer preview e il relativo README specifica che sono previste modifiche incompatibili. Ogni nome di chiave riportato di seguito è stato verificato nella documentazione upstream e nel repository in quella data. Verificali nuovamente prima di basarti su uno di essi, perché una API in anteprima può rinominare i campi tra una release candidate e l'altra. Se harness non è ancora in esecuzione, configuralo con DeepSeek Harness su un VPS e la chiave API e la configurazione 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.ymlSe 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 utilizza davvero
L’elenco completo dei punti di estensione è lungo. Due di questi coprono quasi ogni primo plugin.
Gli eventi della conversazione sono il flusso persistente e registrato. I nomi sono session/event, turn/start, turn/end, step/start, step/end, user/message, assistant/message, assistant/chunk, tool/call e tool/result. A questi 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 dei payload è la parte di una preview API soggetta ai cambiamenti più frequenti.
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 richiama.
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 e l’agent 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 della confusione quando si crea il primo plugin. Scrivi la chiamata next() prima di aggiungere qualsiasi altra istruzione intorno.
agent/request avvolge direttamente la chiamata al modello. Il payload contiene l’agent che esegue la chiamata, il numero del turno aperto, lo step a cui appartiene la richiesta e il segnale di abort del turno. Per questo è il punto di estensione adatto 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 l’approvazione prima del dispatch. tools/execute avvolge il dispatch. tools/post-execute può sostituire o bloccare il risultato normalizzato. tools/result osserva soltanto l’esito definitivo.
Impacchettalo come bundle installabile da altri
Un bundle è un pacchetto npm il cui campo package.json dichiara un campo dsh.bundle che punta al file della patch. Questa dichiarazione è l’unica differenza tra un file temporaneo e qualcosa che può essere installato.
{
"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-helloLa riga name contiene il nome del pacchetto, quindi queste due stringhe devono corrispondere. La riga id è il valore a cui si riferisce un livello successivo quando un utente sovrascrive la configurazione. Scegli quindi un valore stabile e non riutilizzarlo per un plugin diverso.
files deve contenere 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 apporta alcuna modifica 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 demodsh 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 e headless vengono creati automaticamente dai template inclusi al primo utilizzo. Qualsiasi altro nome di profilo deve essere creato tramite 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 tutto
Per la parte host, oggi la risposta corretta è riavviare. Il bundle dell'applicazione web viene distribuito con il meccanismo condiviso di hot module reload disabilitato e il file contiene una nota che ne prevede la riattivazione dopo aver testato il ciclo di vita del reload. La catena di reload lato client è sempre montata, ma resta inattiva finché un watcher della build non riscrive i bundle client. Di conseguenza, non produce alcun effetto nemmeno sulla parte Node.
Rendi economico il riavvio invece di inseguire un reload che non è ancora disponibile. 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 build né un passaggio con pnpm. Registra tutto tramite ctx, in modo che un riavvio non lasci un tool duplicato o un listener obsoleto. Racchiudi ogni risorsa che allochi direttamente in ctx.effect() e associala a un disposer reale, perché il sintomo tipico dell'assenza di un disposer è il fallimento della seconda esecuzione su una porta ancora occupata dalla prima.
Se sviluppi usando un harness in esecuzione su un server invece che sul tuo laptop, nulla di quanto detto sopra cambia. Tuttavia, il binding della Web UI è importante. Il binding loopback sulla porta 3080 spiega perché la pagina non si apre automaticamente e come intervenire.
La parte del browser e quanto fidarsi di essa
Aggiungila solo quando il plugin richiede un'interfaccia propria. Viene dichiarata 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 segnala un errore se il pacchetto non esporta ./client. Per questo la mappa delle esportazioni fa parte del manifest e non è un elemento facoltativo. L'entry point 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 serve come documentazione e non per la pianificazione: registra le dipendenze a livello di pacchetto, ma non controlla l'ordine di attivazione. external è il campo in cui dichiari le richieste di moduli al di fuori della baseline, in modo che vengano materializzate prima che il plugin le richieda. Questa è l'area che cambia più rapidamente nella preview. Perciò, leggi 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.
Pubblica il plugin e dichiara cosa modifica
Aggiungere il topic dsh-plugin a un repository GitHub lo inserisce nell'elenco che gli utenti consultano quando cercano plugin. Significa chiedere la fiducia di persone che non conosci e comporta obblighi precisi. Questi obblighi rispecchiano i controlli indicati nella nostra guida alla verifica di un plugin dsh prima dell'installazione, quindi seguire quella checklist è il modo più semplice per rispettarli.
- Blocca le dipendenze su versioni precise. Un intervallo con caret per una dipendenza transitiva può fare eseguire oggi a un pacchetto, sicuro la settimana scorsa, codice diverso. È esattamente il meccanismo alla base degli attacchi alla supply chain di npm contro un server.
- Indica nel manifest cosa modifichi. L'elenco
injectè un riepilogo onesto e leggibile dalle macchine dei servizi harness che utilizzi. Un revisore lo legge in pochi secondi e si forma un'opinione sulla base di queste informazioni. - Nessuna chiamata di rete nascosta. Se uno strumento chiama un'API, indica l'host nel README e rendi configurabile l'endpoint. Un plugin che contatta un server mai dichiarato verrà rimosso dall'elenco dagli utenti che eseguono questi controlli.
- Mantieni
filesessenziale. Pubblicare un'intera directory di lavoro può far finire per errore un file con credenziali nel registry. - Fornisci agli installer Git uno script
prepareche esegua la build senza presupporre la presenza di dipendenze disponibili solo in fase di sviluppo, e indica nel README che devono consentire esplicitamente questa build nelpnpm-workspace.yamldel loro profilo. - Inserisci nel README la data relativa al release candidate con cui hai eseguito build e test. Chi utilizza un'API in anteprima deve sapere quale versione hai usato.
Per vedere come appare un plugin completato dall'esterno, leggi i plugin dsh che vale la pena installare e osserva quali informazioni fornisce ogni README prima dell'installazione. Se hai scritto estensioni per un altro agent, come sono strutturati i plugin Claude Code offre un confronto utile. L'harness fornisce un object graph 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. È sufficiente un percorso del filesystem in un overlay cordis.yml, caricato con dsh web --patch ./scratch-plugin/cordis.yml, per eseguire il proprio codice all'interno dell'harness. Il percorso deve essere assoluto. Il packaging è importante solo quando il plugin viene installato da altri; anche in questo caso è possibile installare una cartella locale con dsh plugin --profile demo add ./my-plugin per testare il pacchetto senza interagire con un registry.
Perché il mio plugin viene caricato, ma lo strumento non compare mai?
Eseguire 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, verificare 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 quella dichiarazione, Cordis non attende il registro degli strumenti e apply può essere eseguito quando ctx.tools non è ancora disponibile per la registrazione.
Qual è la differenza tra cordis.yml e cordis.patch.yml?
cordis.yml è un elenco completo di voci. cordis.patch.yml è un livello applicato sopra a uno di questi elenchi, che 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 la precedenza.
È possibile ricaricare a caldo un plugin dsh mentre l'agent è in esecuzione?
Non per la componente host nel profilo web, a partire da 0.1.0-rc.7. Quel bundle distribuisce disabilitata la riga condivisa per il ricaricamento a caldo dei moduli, con una nota nel file che ne prevede il ripristino dopo il test del relativo ciclo di ricaricamento. È preferibile progettare un riavvio rapido: un solo file, caricato tramite --patch senza una fase di build, e ogni registrazione eseguita tramite ctx, così nulla rimane da un'esecuzione alla successiva. Usare ctx.effect() con una funzione di rilascio per le risorse che Cordis non può eliminare autonomamente.