Hop til hovedindhold

Udvikling af Cordis-plugins

Cordis-broen indlæser et plugintræ med rækker navngivet i cordis.patch.yml. En ny funktion tilføjes ved at skrive et Cordis-plugin og en række, ikke ved at ændre Libre WebUI's kildekode. Denne side beskriver værtens konventioner.

Læs først Cordis-bro om træets opbygning og Cordis-konfiguration om rækkefelterne.

De to pluginformer

Et Cordis-plugin er enten en funktion eller et objekt med en apply-metode. Loader håndterer begge.

// 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 læser modulets navnerum, så et plugin fra en række skal have apply og inject som navngivne eksporter. En standardeksport virker også, men de medfølgende DSH-plugins bruger navngivne eksporter. Vælg derfor den form for konsistens.

Deklarér afhængigheder

inject er hele afhængighedsmekanismen. Cordis kører først pluginet, når alle navngivne tjenester findes, og kører det igen, hvis en trækkes tilbage og gendannes.

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

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

To konsekvenser er vigtige:

  • En manglende tjeneste er ikke en fejl. Rækken bliver stille ved med at afvente. Derfor viser GET /api/cordis/health tjenesternes tilstand i stedet for én boolesk værdi.
  • Injection bestemmer rækkefølgen. Du skal ikke selv sekvensere rækker. Deres rækkefølge i den medfølgende komposition har ingen indlæsningsbetydning.

Brug ctx.inject inde i apply til en afhængighed, der er valgfri ved udvikling. Callbacken kører straks, hvis tjenesterne allerede findes, og igen, hver gang de bliver tilgængelige:

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

ctx.inject kører synkront, når afhængighederne allerede er til stede. Et sent indlæst plugin registrerer sig derfor under apply frem for i et senere tick.

Udstil en tjeneste

Udvid Service, og send tjenestenavnet til super. Navnet er den egenskab, forbrugere læser, og registreringen tilhører pluginets fiber.

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;

Foretræk en Service-underklasse frem for ctx.reflect.provide(...) til noget med adfærd. Underklassen registrerer navnet én gang, giver typede metoder og trækkes automatisk tilbage med sin fiber.

Ejerskab af sideeffekter

Hver sideeffekt skal have en ejer. Det er reglen, der gør broens tilbagerulningsgaranti sand, og den letteste at bryde.

SideeffektEjer
TjenesteregistreringAutomatisk pluginets fiber
Hændelseslytterctx.on(...) inde i pluginet
Ressource, der kræver oprydningctx.effect(() => disposer)
Timerctx.setTimeout / ctx.setInterval
VærktøjsregistreringReturnér oprydningsfunktionen fra ctx.effect

ctx.effect modtager en funktion, der returnerer en oprydningsfunktion, eller en generator, der giver oprydningsfunktioner:

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

Etiketten er diagnostik, ikke pynt. Den navngiver effekten, når oprydningen fejler.

En ressource, som Cordis ikke kender, såsom en socket, worker eller et handle, skal du selv frigive:

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

Hvad ødelægger tilbagerulning

  • Registrering på en anden kontekst end den, der gives til apply. Tjenesten overlever så sin række.
  • Oprettelse af en timer med global setTimeout. Den holder processen i live og bliver aldrig annulleret.
  • Abonnement på en ekstern emitter uden at afmelde i oprydningsfunktionen.
  • Skrivning til en singleton på modulniveau. Oprydning kan ikke fortryde det, så værdien er synlig efter fjernelsen. Brug tilstand pr. fiber i stedet.

Konfiguration

Et plugins konfiguration er rækkens config-mapping. Deklarér et skema, så en slåfejl fejler ved indlæsning frem for at bruge en standard i stilhed:

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

Konfigurationsværdier kan bruge !!js i kompositionsdokumentet. Udtrykket evalueres i rækkens fiber med Loader-konteksten tilgængelig. process.env og ctx.get(...) virker, men import.meta gør ikke. name evalueres aldrig, så modulspecifikatoren skal være en bogstavelig streng.

Et komplet eksempel

Repositoryet har to fungerende fixtures, der bruges af broens testsuite. Begge er små nok til at kopiere.

En modeladapter: scripts/fixtures/cordis/fake-adapter.mjs registrerer en udbyderrute på ctx.llm, returnerer registreringens oprydningsfunktion fra en effekt og besvarer alle anmodninger med fast tekst. Det er hele udbyderkontrakten: deklarér inject, registrér, og ej registreringen.

En livscyklusprobe: scripts/fixtures/cordis/lifecycle-probe.mjs udstiller en tjeneste, abonnerer på en hændelse og registrerer sin egen oprydning. Dermed kontrollerer tilbagerulningstesten, at begge faktisk forsvinder.

Indlæs en af dem ved at tilføje en række:

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

Relative specifikatorer opløses mod kompositionsfilens mappe; pakkenavne opløses gennem backendpakken.

Test et plugin

Broens testsuiter viser mønsteret:

  • scripts/test-cordis-bridge.mjs indlæser et rigtigt træ i en midlertidig mappe, kontrollerer tjenestetilgængelighed og kontrakten og sikrer, at oprydning fjernede tjenesten og frigav lytteren.
  • scripts/test-cordis-bridge-http.mjs afprøver ruter via en rigtig HTTP-server, inklusive NDJSON-streaming.

Test uden udbyder ved at sætte model.provider: none og indlæse dit plugin samt motorrækkerne. Afhænger pluginet af llm, skal en fixtureadapter indlæses på en rute, som ingen rigtig adapter bruger. Sæt model.provider: none, så værten ikke indlæser en konkurrerende adapter på ruten.

Et plugin er kun korrekt, hvis fjernelse af rækken ikke efterlader spor. Kontrollér forløbet: registrér, observer, ryd op, observer igen.

Tjekliste

  • apply og inject er navngivne eksporter.
  • Tjenesten er en Service-underklasse med et stabilt provide-navn.
  • Alle registreringer returnerer en oprydningsfunktion, som returneres fra ctx.effect.
  • Timere kommer fra ctx, ikke globale funktioner.
  • Muterbar tilstand ligger på instansen, ikke en modul-singleton.
  • Konfiguration valideres af et skema.
  • Ingen hemmeligheder står i kompositionen eller indstillingsdokumentet.
  • En test kontrollerer, at pluginet intet efterlader efter oprydning.