Zelf een dsh-plugin schrijven voor DeepSeek Harness
Leer hoe u een dsh-plugin bouwt vanaf een lege map. Ontdek de essentiële package.json velden, het patch-bestand voor de koppeling en de twee hooks die u nodig heeft voor uw tool.
Wat een dsh-plugin precies is
Een dsh-plugin is een npm-pakket dat een apply-functie exporteert en één klein YAML-bestand bevat dat DeepSeek Harness instrueert om de plugin te laden. Er is geen afzonderlijke plugin-SDK die u eerst moet leren. dsh is een Cordis-applicatie en "alles is een plugin" moet u letterlijk nemen: het toolregister, de agent-loop, de sessie-opslag en de webserver zijn allemaal onderdelen in dezelfde plugin-boom waar uw pakket deel van uitmaakt.
Cordis is een algemeen compositieframework dat onafhankelijk is ontwikkeld en jarenlang als basis voor het Koishi-chatbotframework heeft gediend. Het beheert het laden en ontladen van plugins en lost de afhankelijkheden tussen plugins op. Het heeft geen kennis van agents. Alles wat met agents te maken heeft, is afkomstig van de harness-pakketten die hierbovenop zijn geplaatst; daarom is de structuur van een plugin hieronder zo compact. Het meeste wat u gebruikt, is overgeërfd.
Een plugin bestaat uit twee helften. De host-helft draait in Node, registreert tools en event listeners, en kan eigen services aanbieden. De browser-helft draait in de Web UI en registreert interface-slots. Een eerste plugin is bijna altijd alleen een host-plugin; beschouw de browser-helft daarom als optioneel totdat u deze nodig heeft.
Deze handleiding is geschreven voor @deepseek-ai/dsh versie 0.1.0-rc.7, de npm latest-tag op 19 augustus 2026. dsh is een developer preview en de bijbehorende README vermeldt dat er wijzigingen zullen plaatsvinden die de compatibiliteit verbreken. Elke sleutelnaam hieronder is op die datum uit de upstream-documentatie en de repository overgenomen. Toen deze handleiding op 12 oktober 2026 opnieuw werd gecontroleerd, was de npm latest-tag 0.2.0-rc.2; de secties hieronder geven aan waar die release zich anders gedraagt. Controleer de sleutelnamen opnieuw voordat u ergens op vertrouwt, aangezien een preview-API velden hernoemt tussen release candidates. Als de harness nog niet draait, configureer deze dan eerst met DeepSeek Harness op een VPS en de dsh API-sleutel en modelconfiguratie, en keer daarna hier terug.
Laad één scratch-bestand voordat u iets verpakt
Eerst verpakken is de trage manier om dit te leren. Laad één enkel bestand, bewijs dat de runtime uw code aanroept en verpak het daarna pas.
Maak een map aan buiten de harness-checkout en plaats daar één bestand in.
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 is metadata die wordt gebruikt om de plugin te labelen in diagnostische gegevens. apply is het volledige contract: Cordis roept dit één keer aan en geeft een context door die is gescopeerd aan uw plugin. Alles wat u op die context registreert, wordt ongedaan gemaakt wanneer de plugin wordt verwijderd.
Schrijf daarnaast cordis.yml.
- insert:
- id: hello
name: '/absolute/path/to/scratch-plugin/hello.ts'Start nu een profiel met dat bestand als overlay.
dsh web --patch ./scratch-plugin/cordis.ymlAls dsh niet in uw PATH staat, doet npx @deepseek-ai/dsh web --patch ./scratch-plugin/cordis.yml hetzelfde. Die npx-route kan u een gecachte oudere release candidate geven in plaats van de versie die deze handleiding beschrijft. Als de harness een gedocumenteerde flag direct afwijst, doorloop dan de oplossingen voor dsh-installatie- en versie-fouten voordat u aan uw eigen bestand gaat twijfelen. U zou [hello-plugin] plugin loaded moeten zien in de terminal die dsh heeft gestart. Als er niets verschijnt, is de rij niet opgelost.
Het veld name accepteert een npm-pakketnaam of een bestandssysteempad, en de upstream-documentatie stelt dat het pad absoluut moet zijn. Een relatief ./hello.ts is het eerste wat u moet controleren wanneer een scratch-plugin geen output genereert. Het tweede is de bestandsextensie. De gedocumenteerde loop wordt uitgevoerd als pnpm dsh web --patch ... vanuit een kloon van de harness-repository, waar TypeScript-entries worden geladen via tsx. Als uw dsh afkomstig is van npm, verwijs de rij dan naar gewone JavaScript of bouw het bestand eerst.
--patch is een launcher-flag en de overlay ervan wordt als laatste toegepast, na elke bundle en na uw eigen profielpatch. Een scratch-overlay wint daarom altijd, wat precies is wat u wilt terwijl u itereert.
Schrijf de kleinste tool die iets nuttigs doet
Een logregel bewijst dat de plugin wordt geladen. Een tool bewijst dat de plugin onderdeel is van de 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'] is de regel die mensen vaak vergeten. Items in een Cordis-configuratie starten gelijktijdig, dus de positie van een rij in het bestand garandeert niets over de laadvolgorde. Volgorde wordt bepaald door gedeclareerde afhankelijkheden. inject vertelt Cordis dat het moet wachten tot ctx.tools bestaat voordat het uw apply aanroept; zonder dit kan uw code worden uitgevoerd op een moment dat het register nog niet beschikbaar is om in te registreren.
De rest van het object is het contract dat het model ziet. parameters is het argumentschema en execute ontvangt argumenten die al aan de hand daarvan zijn geparseerd. output.schema beschrijft de waarde die execute retourneert, terwijl render die waarde omzet in de inhoudsblokken die het model leest. Door die twee gescheiden te houden, kan de interface het ene tonen terwijl het model het andere leest.
Start het profiel en vraag de assistent om iemand bij naam te begroeten. Het antwoord komt terug via uw execute. Registratie via ctx is omkeerbaar, dus het verwijderen van de plugin heft de registratie van de tool voor u op. Voor zaken waar Cordis niets van kan weten, zoals een socket of een file handle, roept u ctx.effect() aan en geeft u een disposer mee.
De twee extensiepunten waar een eerste plugin daadwerkelijk mee te maken krijgt
De volledige lijst met aansluitpunten is lang. Twee daarvan dekken bijna elke eerste plugin.
Conversatie-events vormen de duurzame, gelogde stroom. In 0.1.0-rc.7 waren de namen session/event, turn/start, turn/end, step/start, step/end, user/message, assistant/message, assistant/chunk, tool/call en tool/result. De 0.2.0-rc.2 release bevat geen assistant/chunk. Live streaming is verplaatst naar agent/assistant-stream, waarvan de frames in het proces blijven en nooit worden gelogd, en een nieuw duurzaam assistant/attempt-event houdt mislukte, opnieuw geprobeerde en geannuleerde pogingen bij. U koppelt hier een gewone listener aan.
ctx.on('tool/call', (payload) => {
console.log('[my-plugin] tool/call', JSON.stringify(payload))
})Print de payload één keer en lees deze. Kopieer geen veldnamen van de payload uit een handleiding, inclusief deze, omdat de vorm van de payload het onderdeel van een preview-API is dat het meest verandert.
Het tweede extensiepunt is de waterval. agent/pre-step, agent/request, agent/request-error, llm/stream en de tools/*-events zijn watervallen, en een waterval-listener heeft een andere signatuur. Deze vereist een next-callback, en de keten gaat alleen door als deze wordt aangeroepen.
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
})Als u await next() vergeet, heeft u geen hook toegevoegd. U heeft de model-aanroep vervangen door niets, en de agent stopt daar, omdat kortsluiting het beoogde gedrag is voor een gateway-plugin die een verzoek bewust weigert. Dat ene verschil veroorzaakt de meeste verwarring bij eerste plugins. Schrijf de next()-aanroep voordat u er iets omheen schrijft.
agent/request omhult de model-aanroep zelf. De payload bevat de agent die de aanroep doet, het nummer van de open beurt, de stap waar het verzoek bij hoort en het abort-signaal van die beurt; dit maakt het het juiste aansluitpunt voor een request logger of een rate limiter. De tools/*-watervallen hebben dezelfde vorm één laag dieper. tools/pre-execute staat toe, weigert of vraagt om goedkeuring vóór verzending. tools/execute omhult de verzending. tools/post-execute kan het genormaliseerde resultaat vervangen of blokkeren. tools/result observeert alleen het bevroren resultaat.
Verpak het als een bundle die anderen kunnen installeren
Een bundle is een npm-pakket waarvan de package.json een dsh.bundle-veld bevat dat naar het patchbestand verwijst. Die declaratie is het enige verschil tussen een scratch-bestand en iets dat installeerbaar is.
{
"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"
}
}De cordis.patch.yml die ernaast staat is kort.
- insert:
- id: dsh-plugin-hello
name: dsh-plugin-helloDe regel name is de pakketnaam, dus die twee strings moeten overeenkomen. De regel id is het doelwit van een latere laag wanneer een gebruiker uw configuratie overschrijft; kies dus iets stabiels en gebruik het nooit opnieuw voor een andere plugin.
files moet cordis.patch.yml bevatten. Laat u dit weg, dan bevat de gepubliceerde tarball een dsh.bundle.patch die verwijst naar een bestand dat nooit is ingepakt, waardoor het pakket wel installeert maar niets toevoegt aan de tree.
Installeer het in een profiel vanuit de map die uw plugin-folder bevat.
dsh plugin --profile demo add ./dsh-plugin-hello
dsh --profile demo --dump-config
dsh --profile demodsh plugin --profile <name> stuurt de rest van de argumenten door naar pnpm binnen die profielmap, dus add en remove gedragen zich zoals pnpm dat doet. Verwijder de installatie met dsh plugin --profile demo remove dsh-plugin-hello. De meegeleverde web, headless, sdk, sdk-minimal en acp profielen maken zichzelf aan op basis van templates bij het eerste gebruik. In 0.2.0-rc.2 wordt elke andere naam op een van de twee volgende manieren aangemaakt. dsh plugin start het uitsluitend op basis van de basis-bundle. dsh --profile <name> --from-default-profile <template> kopieert de bundle-lijst van een meegeleverd template. Het opstarten van een aangepast profiel dat nog niet bestaat, mislukt met een hint om dsh plugin uit te voeren.
Waarom uw rij ontbreekt in de samengestelde boomstructuur
De samenstelling begint met een lege lijst met items en stapelt lagen in een vaste volgorde. Eerst elk bundle-bestand dat in de dsh.profile.bundles van het profiel wordt genoemd, in de vermelde volgorde. Daarna de eigen cordis.patch.yml van het profiel. Vervolgens $DSH_HOME/cordis.patch.yml. Daarna eventuele --patch-overlays vanaf de opdrachtregel. Latere lagen vervangen eerdere rijen op basis van id.
Profielen bevinden zich onder $DSH_HOME/profiles/<name>. Een profielmap bevat een package.json met het dsh.profile-manifest en de bijbehorende geordende bundles-lijst, plus het eigen patchbestand van de gebruiker. Bundle-namen worden eerst opgelost vanuit de dsh-installatie en daarna vanuit de node_modules van het profiel; dit is de locatie waar pnpm een plugin buiten de boomstructuur plaatst.
dsh --profile demo --dump-config geeft de volledig samengestelde boomstructuur weer zonder iets op te starten, en die uitvoer vormt de scheidslijn voor het debuggen. Als uw rij-id ontbreekt, ligt het probleem bij de samenstelling: een naam die niet wordt opgelost, of een patchbestand dat nooit is ingepakt. Als de rij wel aanwezig is en er gebeurt niets, ligt het probleem in uw code. Beantwoord die vraag eerst om het meeste giswerk te vermijden.
Waar laadfouten daadwerkelijk zichtbaar worden
Een fout die binnen apply optreedt, is duidelijk zichtbaar. Het proces stopt met die uitzondering en u krijgt een stack trace die naar uw eigen regel wijst.
Oplossingsfouten zijn stil. De loader rapporteert een module die hij niet kan oplossen via de Cordis-logger in plaats van te crashen. De upstream-handleiding waarschuwt dat deze berichten bij het opstarten verloren kunnen lijken te gaan, omdat ze worden verzonden voordat console-exporters zijn gekoppeld. Een typefout in een pad ziet er daarom precies zo uit als een plugin die wel is geladen maar niets heeft gedaan. Daarom is het de moeite waard om de --dump-config-controle hierboven uit te voeren voordat u code leest.
Houd tijdens het ontwikkelen een console.log als eerste instructie in apply aan. De afwezigheid ervan vertelt u met welke helft van het probleem u te maken heeft, en het kost niets om deze later te verwijderen. Voer op een server de harness uit op de voorgrond terwijl u itereert, in plaats van onder een service manager. Zo bereikt de output van de loader uw terminal in plaats van een logboek dat u achteraf moet uitlezen.
Itereren zonder de hele wereld opnieuw op te starten
In 0.1.0-rc.7 was het antwoord voor de host-helft simpel: u start opnieuw op. De webapplicatie-bundle van die release werd geleverd met de gedeelde hot module reload-rij uitgeschakeld, en het bestand bevatte een notitie dat deze opnieuw zou worden ingeschakeld zodra de reload-levenscyclus was getest. In 0.2.0-rc.2 is die override verdwenen. De basis-bundle mount @deepseek-ai/dsh-hmr in elk profiel met root: [], onder de opmerking "Profielconfiguratie laadt standaard opnieuw; module-roots zijn opt-in." Geldige wijzigingen aan de profiel- en home-patchbestanden worden nu opnieuw toegepast zonder herstart. De broncode van uw eigen plugin wordt nog steeds niet in de gaten gehouden, tenzij u de map toevoegt aan de root van die rij. Het toevoegen, verwijderen of bijwerken van een bundle vereist nog steeds een herstart. De client-side reload-keten is altijd gemount, maar blijft inactief totdat een rebuild-watcher de client-bundles herschrijft, dus het doet ook niets voor uw Node-helft.
Maak de herstart goedkoop in plaats van te vertrouwen op een reload waarvoor u moet kiezen. Houd de plugin in één bestand. Laad deze met --patch in plaats van de plugin in een profiel te installeren, zodat er geen build-stap en geen pnpm-stap tussen een bewerking en een uitvoering zitten. Registreer alles via ctx, zodat een herstart geen dubbele tool of een verouderde listener achterlaat. Verpak alles wat u zelf toewijst in ctx.effect() met een echte disposer, omdat het gebruikelijke symptoom van een ontbrekende disposer is dat de tweede uitvoering faalt op een poort die de eerste uitvoering nog bezet houdt.
Als u ontwikkelt tegen een harness die op een server draait in plaats van op uw laptop, verandert er niets aan het bovenstaande, maar de Web UI-binding is wel van belang. Een lokale start serveert de Web UI op http://127.0.0.1:3080 en opent deze in uw standaardbrowser, of slaat de browser over als u --no-open meegeeft. Een start via SSH print alleen de host-URL, dus er opent niets op uw machine. De loopback-bind op poort 3080 legt uit waarom de pagina in dat geval niet opent en wat u daaraan kunt doen.
Het browsergedeelte en de mate van vertrouwen
Voeg dit alleen toe wanneer uw plugin een eigen interface vereist. Dit wordt gedeclareerd in hetzelfde dsh-veld als de bundle.
{
"dsh": {
"client": {
"platform": "web",
"inject": [],
"external": [],
"immediately": false
}
},
"exports": {
".": "./src/index.ts",
"./client": "./src/client/apply.ts",
"./package.json": "./package.json"
}
}"platform": "web" is vereist en de scanner geeft een foutmelding als het pakket geen ./client-export bevat; de export map is dus onderdeel van het manifest in plaats van een gemaksvoorziening. De client entry ontvangt de Cordis Context, verbreed met het client runtime-type, en elke registratie vindt plaats binnen apply via ctx.slots.register. Side-effects op moduleniveau zijn daar niet toegestaan.
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)
}Twee details zijn het vermelden waard voordat u begint. inject in het client-manifest is bedoeld voor documentatie en niet voor scheduling: het legt afhankelijkheidsrelaties op pakketniveau vast en stuurt de activeringsvolgorde niet aan. external declareert module-aanvragen buiten de baseline, maar de huidige client-notities geven aan dat dit geen afhankelijkheidsmechanisme is voor een feature-plugin; het is gereserveerd voor infrastructuur, transport en gegenereerde assembly-code. Laat dit leeg in een feature-plugin. Dit is het meest veranderlijke onderdeel van de preview; lees daarom packages/client/AGENTS.md in de harness-repository op de dag dat u de code schrijft, niet op de dag dat u een handleiding hierover leest.
Publiceren en aangeven welke onderdelen uw plugin raakt
Het toevoegen van het dsh-plugin-onderwerp aan een GitHub-repository plaatst deze in de lijst die gebruikers doorzoeken wanneer zij op zoek zijn naar plugins. Dit is een beroep op het vertrouwen van een vreemde en brengt verplichtingen met zich mee. Deze verplichtingen zijn het spiegelbeeld van wat onze handleiding voor het controleren van een dsh-plugin voordat u deze installeert gebruikers adviseert te controleren. Schrijven volgens deze checklist is daarom de eenvoudigste manier om aan de eisen te voldoen.
- Pin uw dependencies. Een caret-bereik voor een transitieve dependency is de manier waarop een pakket dat vorige week veilig was, deze week andere code uitvoert; dit is precies het mechanisme achter npm supply chain-aanvallen op een server.
- Zorg dat het manifest aangeeft wat u raakt. Uw
inject-lijst is een eerlijke, machineleesbare samenvatting van welke harness-services u gebruikt. Een reviewer leest dit in enkele seconden en vormt er een oordeel over. - Geen stille netwerkaanroepen. Als een tool een API aanroept, vermeld de host dan in de README en maak het eindpunt configureerbaar. Een plugin die contact maakt met een server die niet is vermeld, wordt verwijderd door de personen die deze zaken auditen.
- Houd
filesbeperkt. Het publiceren van een volledige werkmap is de manier waarop een zwervend inlogbestand in het register terechtkomt. - Geef git-installateurs een
prepare-script dat bouwt zonder aannames over dev-only omgevingen, en vermeld in de README dat zij die build moeten toevoegen aan de allowlist in hunpnpm-workspace.yaml-profiel. - Voorzie de README van een datumstempel die overeenkomt met de release candidate waarmee u heeft gebouwd en getest. Gebruikers van een preview-API moeten weten welke versie u heeft gebruikt.
Om te zien hoe een voltooide plugin er van buitenaf uitziet, leest u de dsh-plugins die het installeren waard zijn en let u op wat elke README u vertelt voordat u deze installeert. Als u extensies voor een andere agent heeft geschreven, is hoe Claude Code-plugins zijn opgebouwd een nuttig contrast. De harness geeft u een live objectgrafiek en omkeerbare registratie; dit biedt meer mogelijkheden dan een manifest van bestanden, maar brengt ook meer verantwoordelijkheid met zich mee.
FAQ
Moet ik publiceren naar npm om een dsh-plugin te schrijven?
Nee. Een bestandspad in een cordis.yml-overlay, geladen met dsh web --patch ./scratch-plugin/cordis.yml, is voldoende om uw eigen code binnen de harness uit te voeren. Het pad moet absoluut zijn. Packaging is alleen van belang wanneer iemand anders de plugin installeert, en zelfs dan kunt u een lokale map installeren met dsh plugin --profile demo add ./my-plugin om de verpakte vorm te testen zonder een registry aan te raken.
Waarom laadt mijn plugin wel, maar verschijnt de tool nooit?
Voer eerst dsh --profile demo --dump-config uit. Als uw rij-id ontbreekt in die uitvoer, is de plugin nooit gemount en ligt de oorzaak bij de samenstelling in plaats van bij de code. Als de rij wel aanwezig is, controleer dan op export const inject = ['tools']. Inzendingen in een Cordis-configuratie starten gelijktijdig, dus de bestandsvolgorde bepaalt niet de laadvolgorde. Zonder die declaratie wacht Cordis niet op de tool-registry, en kan uw apply op een moment draaien waarop ctx.tools nog niet beschikbaar is om bij te registreren.
Wat is het verschil tussen cordis.yml en cordis.patch.yml?
cordis.yml is een volledige lijst met inzendingen. cordis.patch.yml is een laag die boven op een andere wordt toegepast, waarbij rijen op id worden getarget om nieuwe in te voegen of een bestaande configuratie te vervangen. Een bundle wijst naar zijn eigen patch-bestand via dsh.bundle.patch in package.json. Lagen worden in een vaste volgorde toegepast: elke bundle in de volgorde zoals vermeld in het profiel, daarna het patch-bestand van het profiel, vervolgens $DSH_HOME/cordis.patch.yml, en tot slot elke --patch-overlay. Latere lagen krijgen voorrang.
Kan ik een dsh-plugin hot reloaden terwijl de agent draait?
Niet in 0.1.0-rc.7, waarvan de web-bundle de gedeelde hot module reload-rij uitgeschakeld had. In 0.2.0-rc.2 is die rij in elk profiel gemount, maar standaard wordt alleen de profielconfiguratie opnieuw geladen. Module-roots zijn opt-in via de root-lijst, dus uw plugin-code vereist nog steeds een herstart, tenzij u er zelf een toevoegt. Ontwerp in ieder geval voor een snelle herstart: één bestand, geladen via --patch zonder build-stap, en elke registratie uitgevoerd via ctx zodat er niets lekt van de ene run naar de volgende. Gebruik ctx.effect() met een disposer voor resources die Cordis niet zelfstandig kan opruimen.