SSD Nodes Learn 🎉 VPS vanaf $5.50/mnd
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-08-21

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 daadwerkelijk 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 gebouwd 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 framework heeft geen kennis van agents. Alles wat met agents te maken heeft, is afkomstig van de harness-pakketten die er bovenop zijn geplaatst; daarom ziet de onderstaande plugin-structuur er zo compact uit. Het meeste van 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 zelf 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 van 19 augustus 2026. dsh bevindt zich in een developer preview en de README vermeldt expliciet dat er wijzigingen zullen plaatsvinden die de compatibiliteit verbreken. Elke hieronder genoemde sleutelnaam is op die datum uit de upstream-documentatie en de repository overgenomen. Controleer deze opnieuw voordat u ervan afhankelijk wordt, aangezien een preview-API velden kan hernoemen 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.yml

Als 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 in aanraking komt

De volledige lijst met aansluitpunten is lang. Twee daarvan dekken bijna elke eerste plugin.

Conversatie-events vormen de duurzame, gelogde stroom. De namen zijn session/event, turn/start, turn/end, step/start, step/end, user/message, assistant/message, assistant/chunk, tool/call en tool/result. 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 door. Kopieer geen veldnamen van de payload uit een handleiding, inclusief deze, omdat de structuur van de payload het onderdeel van een preview-API is dat het vaakst verandert.

Het tweede extensiepunt is de waterfall. agent/pre-step, agent/request, agent/request-error, llm/stream en de tools/* events zijn waterfalls, en een waterfall-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 anders omheen schrijft.

agent/request omhult de model-aanroep zelf. De payload bevat de agent die de aanroep doet, het nummer van de openstaande 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/* waterfalls 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 enkel het definitieve 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-hello

De 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-map bevat.

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

dsh plugin --profile <name> stuurt de rest van zijn 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 profielen web en headless worden bij het eerste gebruik automatisch aangemaakt op basis van meegeleverde templates, en elke andere profielnaam moet worden aangemaakt via dsh plugin.

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 te herstarten

Het eerlijke antwoord voor de serverzijde is op dit moment dat u een herstart uitvoert. De webapplicatie-bundel wordt geleverd met de gedeelde hot module reload-rij uitgeschakeld, en het bestand bevat een notitie dat deze opnieuw wordt ingeschakeld zodra de reload-levenscyclus is getest. De client-side reload-keten is altijd gemount, maar blijft inactief totdat een rebuild-watcher de client-bundels herschrijft; deze doet dus ook niets voor uw Node-gedeelte.

Maak de herstart goedkoop in plaats van te jagen op een reload-functie die nog niet beschikbaar is. 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 of pnpm-stap tussen een wijziging en de uitvoering zit. Registreer alles via ctx, zodat een herstart geen dubbele tool of verouderde listener achterlaat. Verpak alles wat u zelf toewijst in ctx.effect() met een correcte disposer, omdat het gebruikelijke symptoom van een ontbrekende disposer is dat de tweede run faalt op een poort die nog door de eerste run wordt bezet.

Als u ontwikkelt tegen een harness dat 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. De loopback-bind op poort 3080 legt uit waarom de pagina niet uit zichzelf opent en wat u daaraan kunt doen.

De browser-helft 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 genereert een foutmelding als het pakket geen ./client-export heeft; 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 documentatie in plaats van planning: het legt afhankelijkheidsrelaties op pakketniveau vast en regelt niet de activeringsvolgorde. external is de plek waar u module-aanvragen buiten de baseline declareert, zodat deze worden gematerialiseerd voordat uw plugin erom vraagt. 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 erover 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 files beperkt. 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 hun pnpm-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 niet?

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 compositie in plaats van bij de code. Als de rij 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 het tool-register, en kan uw apply draaien op een moment dat 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 bovenop een configuratie wordt toegepast, waarbij rijen op ID worden getarget om nieuwe in te voegen of een bestaande configuratie te vervangen. Een bundle verwijst naar zijn eigen patchbestand 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 patchbestand van het profiel, vervolgens $DSH_HOME/cordis.patch.yml, en tot slot elke --patch-overlay. Latere lagen overschrijven eerdere.

Kan ik een dsh-plugin hot reloaden terwijl de agent draait?

Niet voor de host-helft in het webprofiel, sinds 0.1.0-rc.7. Die bundle levert de gedeelde hot module reload-rij uitgeschakeld, met een notitie in het bestand dat deze terugkeert zodra de reload-lifecycle is getest. Ontwerp in plaats daarvan voor een snelle herstart: één bestand, geladen via --patch zonder build-stap, waarbij elke registratie verloopt 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.