Przejdź do głównej zawartości

Tworzenie wtyczek Cordis

Most Cordis ładuje drzewo wtyczek wymienionych w cordis.patch.yml. Dodanie funkcji oznacza napisanie wtyczki Cordis i dodanie wiersza, bez zmiany źródeł Libre WebUI. Ta strona opisuje konwencje wymagane przez hosta.

Najpierw przeczytaj Most Cordis o budowie drzewa i Konfigurację Cordis o polach wierszy.

Dwie postacie wtyczek

Wtyczka Cordis jest funkcją albo obiektem z metodą apply. Loader obsługuje obie postacie.

// Function form.
export function apply(ctx, config) {
// ...
}
// Object form, when the plugin also declares dependencies.
export const name = 'my-plugin';
export const inject = ['tools'];

export function apply(ctx, config) {
// ...
}

Loader odczytuje przestrzeń nazw modułu, dlatego wtyczka ładowana z wiersza potrzebuje nazwanych eksportów apply i inject. Eksport domyślny też działa, lecz dostarczone wtyczki DSH używają nazwanych — zachowaj ten zwyczaj.

Deklarowanie zależności

inject stanowi cały mechanizm zależności. Cordis nie uruchamia wtyczki, dopóki wszystkie wskazane usługi nie istnieją, i uruchamia ją ponownie po wycofaniu i przywróceniu którejś z nich.

export const inject = ['tools', 'systemPrompt'];

export function apply(ctx, config) {
// `ctx.tools` and `ctx.systemPrompt` are guaranteed present here.
}

Istotne są dwie konsekwencje:

  • Brak usługi nie jest błędem. Wiersz czeka bezterminowo i bez komunikatu. Dlatego GET /api/cordis/health podaje stan każdej usługi zamiast pojedynczej wartości logicznej.
  • Wstrzykiwanie wyznacza kolejność. Nie porządkuj wykonania ręcznie; kolejność wierszy dostarczonej kompozycji nie steruje ładowaniem.

Dla zależności opcjonalnej użyj ctx.inject wewnątrz apply. Funkcja zwrotna wykona się od razu, jeśli usługi istnieją, oraz ponownie za każdym razem, gdy się pojawią:

export function apply(ctx) {
ctx.inject(['typert'], inner => {
inner.typert.lookups.register('session', {/* ... */});
});
}

Przy już obecnych zależnościach ctx.inject działa synchronicznie: również późno załadowana wtyczka rejestruje się podczas apply, nie w późniejszym cyklu.

Publikowanie usługi

Rozszerz Service i przekaż nazwę usługi do super. Nazwa odpowiada właściwości odczytywanej przez odbiorców, a rejestracja należy do włókna wtyczki.

import { Service } from '@deepseek-ai/cordis';

export class WidgetRegistry extends Service {
static provide = 'widgets';

constructor(ctx) {
super(ctx, 'widgets');
this.widgets = new Map();
}

register(widget) {
// Return the disposer so the caller's fiber owns the entry.
return this.ctx.effect(() => {
this.widgets.set(widget.id, widget);
return () => this.widgets.delete(widget.id);
}, 'widgets.register()');
}
}

export default WidgetRegistry;

Dla usług z zachowaniem preferuj podklasę Service zamiast ctx.reflect.provide(...): rejestruje nazwę raz, udostępnia typowane metody i jest automatycznie wycofywana razem z włóknem.

Własność skutków ubocznych

Każdy skutek uboczny musi mieć właściciela. To jedyna reguła gwarantująca wycofanie mostu, a zarazem łatwa do naruszenia.

Skutek ubocznyWłaściciel
Rejestracja usługiAutomatycznie włókno wtyczki
Odbiornik zdarzeńctx.on(...) we wtyczce
Zasób wymagający zwolnieniactx.effect(() => disposer)
Zegarctx.setTimeout / ctx.setInterval
Rejestracja narzędziaFunkcja zwalniająca zwrócona przez ctx.effect

ctx.effect przyjmuje funkcję zwracającą funkcję zwalniającą albo generator takich funkcji:

ctx.effect(() => {
const registration = ctx.llm.registerAdapter(['my-route'], adapter);
return () => registration();
}, 'my-adapter.register');

Etykieta służy diagnostyce: identyfikuje efekt, gdy zwalnianie zawiedzie.

Zasób nieznany Cordis — gniazdo, proces roboczy czy uchwyt — trzeba zwolnić samodzielnie:

ctx.effect(() => {
const worker = startWorker();
return () => worker.terminate();
}, 'my-plugin.worker');

Co uniemożliwia wycofanie

  • Rejestracja w innym kontekście niż przekazany do apply: usługa przeżyje swój wiersz.
  • Utworzenie zegara globalnym setTimeout: utrzymuje proces przy życiu i nigdy nie zostaje anulowany.
  • Subskrypcja zewnętrznego emitera bez wypisania w funkcji zwalniającej.
  • Zapis do singletonu modułu: zwolnienie nie cofnie zmiany, więc wartość pozostanie po usunięciu wiersza. Zamiast tego użyj stanu każdego włókna.

Konfiguracja

Konfiguracja wtyczki to mapa config jej wiersza. Zadeklaruj schemat, aby literówka powodowała błąd podczas ładowania zamiast cichego użycia wartości domyślnej:

import z from '@deepseek-ai/schemastery';

export const Config = z.object({
route: z.string().required(),
maxRetries: z.number().default(2),
});

export function apply(ctx, config) {
// `config` is validated before this runs.
}

Wartości w kompozycji mogą używać !!js. Wyrażenie jest obliczane we włóknie będącym właścicielem wiersza, z kontekstem Loadera: process.env i ctx.get(...) działają, import.meta nie. name nie jest obliczane, więc specyfikator modułu musi być literałem tekstowym.

Pełny przykład

Repozytorium zawiera dwa działające przykłady testowe mostu, oba na tyle małe, że można je skopiować.

Adapter modeluscripts/fixtures/cordis/fake-adapter.mjs rejestruje trasę dostawcy w ctx.llm, zwraca funkcję zwalniającą rejestrację z efektu i odpowiada stałym tekstem. To cały kontrakt: zadeklarować inject, zarejestrować i posiadać rejestrację.

Sonda cyklu życiascripts/fixtures/cordis/lifecycle-probe.mjs publikuje usługę, subskrybuje zdarzenie i rejestruje własne zwolnienie. Tak test wycofania dowodzi, że oba elementy rzeczywiście przestają istnieć.

Załaduj wybrany przykład, dodając wiersz:

- id: my-adapter
name: './scripts/fixtures/cordis/fake-adapter.mjs'
config:
route: my-route

Względne specyfikatory są rozwiązywane względem katalogu kompozycji, a nazwy pakietów przez pakiet backendu.

Testowanie wtyczki

Zestawy testów mostu pokazują wzorzec:

  • scripts/test-cordis-bridge.mjs ładuje rzeczywiste drzewo w katalogu tymczasowym, sprawdza dostępność usług i kontrakt, a następnie potwierdza wycofanie usługi i zwolnienie odbiornika.
  • scripts/test-cordis-bridge-http.mjs testuje trasy na rzeczywistym serwerze HTTP, łącznie ze strumieniowaniem NDJSON.

Aby testować bez dostawcy, ustaw model.provider: none i załaduj wtyczkę oraz wiersze silnika. Dla zależności od llm załaduj adapter testowy na trasie nieużywanej przez rzeczywisty adapter i ustaw model.provider: none, aby host nie ładował konkurencyjnego adaptera.

Wtyczka jest poprawna tylko wtedy, gdy usunięcie jej wiersza nie zostawia śladów. Sprawdź kolejno: rejestrację, obserwację, zwolnienie i ponowną obserwację.

Lista kontrolna

  • apply i inject są nazwanymi eksportami.
  • Usługa jest podklasą Service ze stałą nazwą provide.
  • Każda rejestracja zwraca funkcję zwalniającą i każda taka funkcja jest zwracana przez ctx.effect.
  • Zegary pochodzą z ctx, nie z funkcji globalnych.
  • Zmienny stan należy do instancji, nie singletonu modułu.
  • Konfiguracja podlega walidacji schematu.
  • Dokumenty kompozycji i ustawień nie zawierają sekretów.
  • Test potwierdza, że po zwolnieniu nie pozostały ślady wtyczki.