SSD Nodes Learn 🎉 VPS od $5.50/mies.
Przewodniki Matt ConnorAutor: Matt Connor · Zaktualizowano 2026-08-21

Jak napisać własną wtyczkę dla DeepSeek Harness

Tworzenie wtyczki dsh krok po kroku. Dowiedz się, jak skonfigurować plik package.json, przygotować patch montujący oraz zaimplementować dwa kluczowe hooki dla frameworka Cordis.

Czym w rzeczywistości jest wtyczka dsh

Wtyczka dsh to pakiet npm, który eksportuje funkcję apply i zawiera jeden niewielki plik YAML, informujący DeepSeek Harness o konieczności jej załadowania. Nie istnieje osobny zestaw SDK wtyczek, którego trzeba się uczyć. dsh jest aplikacją typu Cordis, a stwierdzenie „wszystko jest wtyczką” należy rozumieć dosłownie: rejestr narzędzi, pętla agenta, magazyn sesji oraz serwer WWW to wiersze w tym samym drzewie wtyczek, do którego dołącza Twój pakiet.

Cordis to ogólny framework kompozycji, zbudowany niezależnie i używany od lat jako podstawa frameworka chatbotowego Koishi. Obsługuje on ładowanie i wyładowywanie oraz rozwiązuje zależności między wtyczkami. Nie posiada wiedzy na temat agentów. Wszystko, co ma postać agenta, pochodzi z pakietów harness nałożonych na niego, dlatego struktura wtyczki poniżej jest tak niewielka. Większość funkcjonalności jest dziedziczona.

Wtyczka składa się z dwóch części. Część hosta działa w Node, rejestruje narzędzia i detektory zdarzeń oraz może dostarczać własne usługi. Część przeglądarkowa działa wewnątrz Web UI i rejestruje sloty interfejsu. Pierwsza wtyczka prawie zawsze składa się tylko z hosta, więc traktuj część przeglądarkową jako opcjonalną, dopóki nie stanie się potrzebna.

Ten przewodnik został napisany dla wersji @deepseek-ai/dsh 0.1.0-rc.7, oznaczonej tagiem npm latest w dniu 19 sierpnia 2026. dsh znajduje się w fazie developer preview, a jego plik README ostrzega przed zmianami naruszającymi kompatybilność. Każda nazwa klucza poniżej została odczytana z dokumentacji upstream oraz repozytorium w tym dniu. Sprawdź je ponownie, zanim zaczniesz na nich polegać, ponieważ API w wersji preview zmienia nazwy pól pomiędzy kolejnymi release candidate. Jeśli harness jeszcze nie działa, skonfiguruj go najpierw zgodnie z DeepSeek Harness na VPS oraz kluczem API dsh i konfiguracją modelu, a następnie wróć tutaj.

Wczytaj jeden plik roboczy przed spakowaniem czegokolwiek

Pakowanie na początku to powolna metoda nauki. Wczytaj pojedynczy plik, sprawdź, czy środowisko uruchomieniowe wywołuje Twój kod, a dopiero potem go spakuj.

Utwórz folder poza katalogiem roboczym środowiska testowego i umieść w nim jeden plik.

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 to metadane używane do oznaczenia wtyczki w diagnostyce. apply stanowi pełną umowę: Cordis wywołuje ją raz i przekazuje kontekst ograniczony do Twojej wtyczki. Wszystko, co zarejestrujesz w tym kontekście, zostanie automatycznie cofnięte po usunięciu wtyczki.

Obok utwórz plik cordis.yml.

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

Teraz uruchom profil z nałożonym na wierzch tym plikiem.

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

Jeśli dsh nie znajduje się w Twoim PATH, npx @deepseek-ai/dsh web --patch ./scratch-plugin/cordis.yml wykonuje to samo zadanie. Ścieżka przez npx może dostarczyć starszą wersję kandydującą zamiast wersji opisanej w tym przewodniku, więc jeśli środowisko testowe odrzuci udokumentowaną flagę, wykonaj poprawki instalacji i błędów wersji dsh przed podważaniem poprawności własnego pliku. W terminalu, w którym uruchomiono dsh, powinieneś zobaczyć [hello-plugin] plugin loaded. Jeśli nic się nie pojawia, wiersz nie został poprawnie rozpoznany.

Pole name przyjmuje nazwę pakietu npm lub ścieżkę w systemie plików, a dokumentacja nadrzędna stanowi, że ścieżka musi być bezwzględna. Względna ścieżka ./hello.ts to pierwsza rzecz do sprawdzenia, gdy wtyczka robocza nie generuje żadnych danych wyjściowych. Drugą jest rozszerzenie pliku. Udokumentowana pętla jest uruchamiana jako pnpm dsh web --patch ... z klonu repozytorium środowiska testowego, gdzie wpisy TypeScript są ładowane przez tsx. Jeśli Twój dsh pochodzi z npm, wskaż w wierszu zwykły plik JavaScript lub najpierw zbuduj plik.

--patch to flaga uruchamiania, a jej nakładka jest stosowana na samym końcu, po wszystkich pakietach i po Twojej własnej poprawce profilu. Nakładka robocza ma zatem zawsze pierwszeństwo, co jest pożądanym zachowaniem podczas iteracji.

Napisz najmniejsze narzędzie, które wykonuje użyteczne zadanie

Wiersz dziennika potwierdza załadowanie wtyczki. Narzędzie potwierdza, że wtyczka jest częścią agenta.

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'] to wiersz, który ludzie pomijają. Wpisy w konfiguracji Cordis uruchamiają się współbieżnie, więc pozycja wiersza w pliku nie gwarantuje kolejności ładowania. Kolejność wynika z zadeklarowanych zależności. inject instruuje Cordis, aby zaczekał na istnienie ctx.tools przed wywołaniem Twojego apply; bez tego Twój kod może zostać uruchomiony w momencie, gdy rejestr nie jest jeszcze dostępny.

Reszta obiektu stanowi kontrakt widoczny dla modelu. parameters to schemat argumentów, a execute otrzymuje argumenty już przetworzone zgodnie z nim. output.schema opisuje wartość zwracaną przez execute, podczas gdy render konwertuje tę wartość na bloki treści odczytywane przez model. Rozdzielenie tych dwóch elementów pozwala interfejsowi wyświetlać jedną rzecz, podczas gdy model odczytuje inną.

Uruchom profil i poproś asystenta o powitanie kogoś z imienia. Odpowiedź wraca przez Twoje execute. Rejestracja przez ctx jest odwracalna, więc usunięcie wtyczki automatycznie wyrejestrowuje narzędzie. W przypadku wszystkiego, o czym Cordis nie może wiedzieć, jak gniazdo lub uchwyt pliku, wywołaj ctx.effect() i przekaż mu dyspozytor.

Dwa punkty rozszerzeń, z którymi styka się pierwsza wtyczka

Pełna lista punktów styku jest długa. Dwa z nich obejmują niemal każdą pierwszą wtyczkę.

Zdarzenia konwersacji (conversation events) stanowią trwały, rejestrowany strumień. Nazwy to session/event, turn/start, turn/end, step/start, step/end, user/message, assistant/message, assistant/chunk, tool/call oraz tool/result. Dołącza się do nich zwykły słuchacz (listener).

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

Wydrukuj ładunek (payload) jeden raz i odczytaj go. Nie kopiuj nazw pól ładunku z żadnego przewodnika, w tym z tego, ponieważ struktury ładunku są częścią API w wersji zapoznawczej, która zmienia się najczęściej.

Drugim punktem rozszerzeń jest kaskada (waterfall). Zdarzenia agent/pre-step, agent/request, agent/request-error, llm/stream oraz tools/* to kaskady, a słuchacz kaskady ma inną sygnaturę. Przyjmuje on wywołanie zwrotne next, a łańcuch jest kontynuowany tylko wtedy, gdy zostanie ono wywołane.

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

Jeśli zapomnisz o await next(), nie dodasz haka (hook). Zastąpisz wywołanie modelu niczym, a agent zatrzyma się w tym miejscu, ponieważ zwarcie (short circuiting) jest zachowaniem zaprojektowanym dla wtyczki bramy, która celowo odrzuca żądanie. Ta jedna różnica powoduje najwięcej zamieszania przy tworzeniu pierwszych wtyczek. Napisz wywołanie next(), zanim napiszesz cokolwiek wokół niego.

agent/request otacza samo wywołanie modelu. Jego ładunek przenosi agenta wykonującego wywołanie, numer otwartej tury, krok, do którego należy żądanie, oraz sygnał przerwania tej tury. To sprawia, że jest to właściwy punkt styku dla rejestratora żądań lub ogranicznika szybkości (rate limiter). Kaskady tools/* mają ten sam kształt o jedną warstwę niżej. tools/pre-execute zezwala, odrzuca lub wymaga zatwierdzenia przed wysłaniem. tools/execute otacza wysyłkę. tools/post-execute może zastąpić lub zablokować znormalizowany wynik. tools/result jedynie obserwuje utrwalony rezultat.

Pakowanie jako zestaw do instalacji przez innych użytkowników

Zestaw to pakiet npm, którego package.json zawiera pole dsh.bundle wskazujące na plik poprawki (patch). Ta deklaracja stanowi jedyną różnicę między plikiem roboczym a komponentem gotowym do instalacji.

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

Plik cordis.patch.yml znajdujący się obok jest krótki.

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

Wiersz name określa nazwę pakietu, dlatego oba te ciągi znaków muszą być identyczne. Wiersz id to cel, do którego odwołuje się kolejna warstwa, gdy użytkownik nadpisuje konfigurację. Należy wybrać nazwę stabilną i nigdy nie używać jej ponownie dla innej wtyczki.

Pole files musi zawierać cordis.patch.yml. Pominięcie tego elementu spowoduje, że opublikowane archiwum będzie zawierać dsh.bundle.patch wskazujące na plik, który nie został spakowany. W rezultacie pakiet zainstaluje się, ale nie wprowadzi żadnych zmian w drzewie zależności.

Instalację w profilu należy przeprowadzić z katalogu zawierającego folder wtyczki.

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

Polecenie dsh plugin --profile <name> przekazuje pozostałe argumenty do narzędzia pnpm wewnątrz katalogu profilu, dzięki czemu add oraz remove działają zgodnie ze specyfikacją pnpm. Odinstalowanie odbywa się za pomocą dsh plugin --profile demo remove dsh-plugin-hello. Profile web oraz headless tworzą się automatycznie z dostarczonych szablonów przy pierwszym użyciu, natomiast każdą inną nazwę profilu należy utworzyć za pomocą dsh plugin.

Dlaczego wiersz nie pojawia się w złożonym drzewie

Proces kompozycji rozpoczyna się od pustej listy wpisów, a następnie nakłada warstwy w ustalonej kolejności. Najpierw dodawane są wszystkie pakiety wymienione w dsh.profile.bundles profilu, zgodnie z podaną kolejnością. Następnie przetwarzane są cordis.patch.yml profilu. Kolejnym krokiem jest $DSH_HOME/cordis.patch.yml. Na końcu nakładane są wszelkie nakładki --patch z wiersza poleceń. Późniejsze warstwy zastępują wcześniejsze wiersze o tym samym identyfikatorze.

Profile znajdują się w $DSH_HOME/profiles/<name>. Katalog profilu zawiera plik package.json z manifestem dsh.profile, który przechowuje uporządkowaną listę bundles oraz plik poprawek użytkownika. Nazwy pakietów są rozwiązywane najpierw w instalacji dsh, a następnie w node_modules profilu, gdzie pnpm umieszcza wtyczki spoza drzewa.

Polecenie dsh --profile demo --dump-config wyświetla w pełni złożone drzewo bez uruchamiania jakichkolwiek procesów; wynik tego polecenia stanowi granicę diagnostyczną. Jeśli identyfikator wiersza jest nieobecny, problem leży w kompozycji: nazwa nie została rozwiązana lub plik poprawki nie został spakowany. Jeśli wiersz jest obecny, a mimo to nic się nie dzieje, problem dotyczy kodu. Rozstrzygnięcie tej kwestii na początku pozwala wyeliminować większość domysłów.

Gdzie faktycznie pojawiają się błędy ładowania

Błąd zgłoszony wewnątrz apply jest wyraźnie widoczny. Proces kończy działanie z tym wyjątkiem, a użytkownik otrzymuje ślad stosu wskazujący na konkretną linię kodu.

Błędy rozpoznawania modułów są ciche. Ładowacz zgłasza moduł, którego nie może rozpoznać, za pośrednictwem loggera Cordis, zamiast powodować awarię. Dokumentacja źródłowa ostrzega, że komunikaty te mogą zostać pominięte podczas uruchamiania, ponieważ są emitowane przed podłączeniem eksporterów konsoli. Literówka w ścieżce wygląda zatem dokładnie tak samo, jak wtyczka, która załadowała się, ale nie wykonała żadnego działania. Dlatego warto wykonać sprawdzenie --dump-config przed przystąpieniem do analizy kodu.

Podczas programowania należy umieścić console.log jako pierwszą instrukcję w apply. Jej brak pozwala określić, z którą częścią problemu mamy do czynienia, a usunięcie tego fragmentu później nie generuje żadnych kosztów. Na serwerze, podczas iteracji, należy uruchamiać środowisko testowe na pierwszym planie, zamiast korzystać z menedżera usług. Dzięki temu dane wyjściowe ładowacza trafią bezpośrednio do terminala, zamiast do dziennika systemowego, który trzeba osobno przeglądać.

Iteracja bez restartu całego środowiska

Szczera odpowiedź dotycząca części serwerowej brzmi: obecnie wymagany jest restart. Pakiet aplikacji webowej jest dostarczany z wyłączoną funkcją hot module reload, a w pliku znajduje się adnotacja, że zostanie ona ponownie włączona po przetestowaniu cyklu życia przeładowania. Łańcuch przeładowania po stronie klienta jest zawsze zamontowany, ale pozostaje nieaktywny, dopóki mechanizm śledzący zmiany nie przebuduje pakietów klienta, więc nie wpływa on również na część Node.

Zamiast szukać mechanizmu przeładowania, który jeszcze nie istnieje, należy zoptymalizować proces restartu. Wtyczkę należy przechowywać w jednym pliku. Należy ją ładować za pomocą --patch, zamiast instalować w profilu, co eliminuje kroki budowania oraz pnpm między edycją a uruchomieniem. Wszystkie elementy należy rejestrować przez ctx, aby restart nie pozostawiał zduplikowanych narzędzi ani nieaktualnych procesów nasłuchujących. Każdy przydzielony zasób należy opakować w ctx.effect() z odpowiednią funkcją usuwającą, ponieważ typowym objawem braku poprawnego zwolnienia zasobów jest błąd drugiego uruchomienia wynikający z zajętości portu przez poprzednią instancję.

W przypadku programowania z wykorzystaniem środowiska testowego działającego na serwerze, a nie na lokalnym komputerze, powyższe zasady pozostają niezmienne, jednak istotne staje się powiązanie interfejsu webowego. Powiązanie loopback na porcie 3080 wyjaśnia, dlaczego strona nie otwiera się automatycznie i jakie kroki należy podjąć w tej sytuacji.

Część przeglądarkowa i poziom zaufania do niej

Dodaj tę sekcję tylko wtedy, gdy wtyczka wymaga własnego interfejsu. Deklaruje się ją w tym samym polu dsh, co pakiet.

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

Pole "platform": "web" jest wymagane, a skaner zgłasza błąd, jeśli pakiet nie posiada eksportu ./client, dlatego mapa eksportów stanowi część manifestu, a nie udogodnienie. Punkt wejścia klienta otrzymuje Cordis Context rozszerzony o typ środowiska uruchomieniowego klienta, a każda rejestracja odbywa się wewnątrz apply poprzez ctx.slots.register. Efekty uboczne na poziomie modułu są tam niedozwolone.

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

Przed rozpoczęciem warto poznać dwa szczegóły. Pole inject w manifeście klienta pełni funkcję dokumentacyjną, a nie harmonogramu: rejestruje krawędzie zależności na poziomie pakietu i nie kontroluje kolejności aktywacji. Pole external służy do deklarowania żądań modułów spoza bazy, dzięki czemu są one materializowane, zanim wtyczka o nie poprosi. Jest to najszybciej zmieniający się obszar wersji zapoznawczej, dlatego zapoznaj się z packages/client/AGENTS.md w repozytorium harness w dniu pisania kodu, a nie w dniu czytania poradnika na ten temat.

Publikacja i określenie zakresu działania wtyczki

Dodanie tematu dsh-plugin do repozytorium GitHub umieszcza je na liście przeglądanej przez użytkowników poszukujących wtyczek. Jest to deklaracja zaufania, która wiąże się z określonymi obowiązkami. Obowiązki te stanowią lustrzane odbicie punktów, które czytelnicy powinni sprawdzać zgodnie z naszym przewodnikiem weryfikacji wtyczek dsh przed instalacją, dlatego pisanie zgodnie z tą listą kontrolną jest najprostszym sposobem na przejście procesu weryfikacji.

  • Przypinaj zależności. Używanie zakresów z daszkiem (caret) dla zależności przechodnich sprawia, że pakiet, który był bezpieczny w zeszłym tygodniu, może uruchamiać inny kod w tym tygodniu; jest to dokładnie ten sam mechanizm, który wykorzystują ataki na łańcuch dostaw npm na serwerze.
  • W manifeście określaj, do czego uzyskujesz dostęp. Twoja lista inject to uczciwe, czytelne dla maszyny podsumowanie usług harness, z których korzystasz. Recenzent odczytuje ją w kilka sekund i na tej podstawie wyrabia sobie opinię.
  • Brak ukrytych wywołań sieciowych. Jeśli narzędzie wywołuje API, podaj nazwę hosta w pliku README i spraw, aby punkt końcowy był konfigurowalny. Wtyczka, która łączy się z serwerem niewymienionym w dokumentacji, zostanie usunięta z listy przez osoby zajmujące się audytem.
  • Utrzymuj files w ścisłych granicach. Publikowanie całego folderu roboczego to najczęstsza przyczyna przypadkowego wysłania plików z poświadczeniami do rejestru.
  • Dostarcz instalatorom git skrypt prepare, który buduje projekt bez założeń dotyczących środowiska deweloperskiego, i poinformuj ich w pliku README, że muszą dodać ten proces budowania do listy dozwolonych w pliku pnpm-workspace.yaml swojego profilu.
  • Oznacz plik README datą wydania kandydującego (release candidate), na którym przeprowadzono budowanie i testy. Użytkownicy wersji zapoznawczej API muszą wiedzieć, z której wersji korzystałeś.

Aby zobaczyć, jak gotowa wtyczka wygląda z perspektywy użytkownika, zapoznaj się z wtyczkami dsh wartymi instalacji i zwróć uwagę na informacje, które każdy plik README przekazuje przed instalacją. Jeśli tworzyłeś rozszerzenia dla innego agenta, sposób budowy wtyczek Claude Code stanowi przydatny punkt odniesienia. Harness udostępnia aktywny graf obiektów i odwracalną rejestrację, co daje większe możliwości niż zwykły manifest plików, ale wiąże się z większą odpowiedzialnością.

FAQ

Czy muszę publikować w npm, aby napisać wtyczkę dsh?

Nie. Ścieżka systemowa w nakładce cordis.yml, wczytana za pomocą dsh web --patch ./scratch-plugin/cordis.yml, wystarczy do uruchomienia własnego kodu wewnątrz środowiska testowego. Ścieżka musi być bezwzględna. Pakowanie ma znaczenie tylko wtedy, gdy ktoś inny instaluje wtyczkę, a nawet wtedy można zainstalować lokalny folder za pomocą dsh plugin --profile demo add ./my-plugin, aby przetestować spakowaną formę bez korzystania z rejestru.

Dlaczego moja wtyczka się wczytuje, ale narzędzie się nie pojawia?

Uruchom najpierw dsh --profile demo --dump-config. Jeśli identyfikator wiersza nie znajduje się w tym wyniku, wtyczka nie została zamontowana, a przyczyna leży w kompozycji, a nie w kodzie. Jeśli wiersz jest obecny, sprawdź export const inject = ['tools']. Wpisy w konfiguracji Cordis uruchamiają się współbieżnie, więc kolejność plików nie decyduje o kolejności wczytywania. Bez tej deklaracji Cordis nie czeka na rejestr narzędzi, a Twój apply może zostać uruchomiony w momencie, gdy ctx.tools nie jest jeszcze dostępny do rejestracji.

Jaka jest różnica między cordis.yml a cordis.patch.yml?

cordis.yml to pełna lista wpisów. cordis.patch.yml to warstwa nakładana na istniejącą konfigurację, celująca w wiersze według identyfikatora w celu wstawienia nowych lub zastąpienia istniejących. Pakiet wskazuje na własny plik poprawki poprzez dsh.bundle.patch w package.json. Warstwy nakładają się w ustalonej kolejności: każdy pakiet w kolejności wymienionej w profilu, następnie plik poprawki profilu, potem $DSH_HOME/cordis.patch.yml, a na końcu każda nakładka --patch. Późniejsze warstwy mają pierwszeństwo.

Czy mogę przeładować wtyczkę dsh na gorąco podczas pracy agenta?

Nie w przypadku części hosta w profilu web, według stanu na wersję 0.1.0-rc.7. Ten pakiet dostarcza współdzielony wiersz przeładowania modułów na gorąco w stanie wyłączonym, z notatką w pliku informującą, że funkcja powróci po przetestowaniu cyklu życia przeładowania. Zaprojektuj rozwiązanie pod kątem szybkiego restartu: jeden plik, wczytywany przez --patch bez etapu budowania, z każdą rejestracją wykonaną przez ctx, aby nic nie wyciekało z jednego uruchomienia do drugiego. Użyj ctx.effect() z funkcją usuwającą (disposer) dla zasobów, których Cordis nie potrafi wyczyścić samodzielnie.