Ga naar hoofdinhoud

Cordis-plugins schrijven

De Cordis-brug laadt een pluginboom met rijen in cordis.patch.yml. Een mogelijkheid toevoegen betekent een Cordis-plugin schrijven en een rij toevoegen, zonder Libre WebUI-broncode te wijzigen. Deze pagina beschrijft de conventies waarop de host vertrouwt.

Lees eerst Cordis-brug over de opbouw en Cordis-configuratie over de rijvelden.

De twee pluginvormen

Een Cordis-plugin is een functie of een object met een apply-methode. De Loader verwerkt beide.

// 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) {
// ...
}

De Loader leest de modulenaamruimte. Een plugin uit een rij heeft daarom apply en inject als benoemde exports nodig. Een standaardexport werkt ook, maar de meegeleverde DSH-plugins gebruiken benoemde exports; volg dat patroon.

Afhankelijkheden declareren

inject vormt het hele afhankelijkheidsmechanisme. Cordis voert de plugin pas uit zodra alle genoemde services bestaan en opnieuw wanneer een service wordt teruggetrokken en hersteld.

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

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

Twee gevolgen zijn van belang:

  • Een ontbrekende service is geen fout. De rij blijft stilzwijgend onbeperkt wachten. Daarom toont GET /api/cordis/health een status per service in plaats van één boolean.
  • Injectie bepaalt de volgorde. U ordent de uitvoering van rijen nooit zelf; hun volgorde in de meegeleverde compositie bepaalt het laden niet.

Gebruik voor een optionele afhankelijkheid ctx.inject binnen apply. De callback draait direct wanneer de services al bestaan en opnieuw wanneer ze verschijnen:

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

Bij aanwezige afhankelijkheden werkt ctx.inject synchroon: ook een later geladen plugin registreert tijdens apply, niet pas in een volgende tick.

Een service publiceren

Breid Service uit en geef de servicenaam door aan super. Die naam is de eigenschap die consumenten lezen; de registratie behoort aan de pluginfiber.

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;

Kies voor gedrag liever een Service-subklasse dan ctx.reflect.provide(...): deze registreert de naam eenmaal, biedt getypeerde methoden en wordt automatisch met de fiber teruggetrokken.

Neveneffecten beheren

Elk neveneffect moet een eigenaar hebben. Dit is de regel die de rollbackgarantie waarmaakt en die gemakkelijk wordt geschonden.

NeveneffectEigenaar
ServiceregistratieAutomatisch de pluginfiber
Eventlistenerctx.on(...) binnen de plugin
Op te ruimen resourcectx.effect(() => disposer)
Timerctx.setTimeout / ctx.setInterval
ToolregistratieDe disposer teruggegeven door ctx.effect

ctx.effect ontvangt een functie die een disposer teruggeeft of een generator die disposers oplevert:

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

Het label is diagnostisch: het identificeert het effect als opruimen mislukt.

Een resource die Cordis niet kent, zoals een socket, worker of handle, moet u zelf opruimen:

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

Wat rollback doorbreekt

  • Registreren op een andere context dan die aan apply is doorgegeven. De service overleeft dan de rij.
  • Een timer maken met globale setTimeout. Die houdt het proces in leven en wordt nooit geannuleerd.
  • Een externe emitter volgen zonder de inschrijving in de disposer te beëindigen.
  • Schrijven naar een modulesingleton. Opruimen draait dat niet terug; de waarde blijft na rijverwijdering zichtbaar. Gebruik toestand per fiber.

Configuratie

Pluginconfiguratie is de config-mapping van zijn rij. Declareer een schema zodat een typefout bij het laden faalt in plaats van stilzwijgend een standaard te gebruiken:

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

Waarden mogen !!js gebruiken in de compositie. De expressie wordt geëvalueerd in de fiber van de rij met de Loader-context in scope: process.env en ctx.get(...) werken, import.meta niet. name wordt nooit geëvalueerd; de modulespecificatie moet een letterlijke tekenreeks zijn.

Een volledig voorbeeld

Het repository bevat twee werkende fixtures uit de brugtests. Beide zijn klein genoeg om te kopiëren.

Modeladapter: scripts/fixtures/cordis/fake-adapter.mjs registreert een providerroute op ctx.llm, geeft de registratiedisposer terug vanuit een effect en antwoordt met vaste tekst. Dat is het gehele contract: inject declareren, registreren en de registratie bezitten.

Levenscyclusprobe: scripts/fixtures/cordis/lifecycle-probe.mjs publiceert een service, abonneert zich op een event en registreert zijn eigen opruiming. Daarmee bewijst de rollbacktest dat beide werkelijk ophouden te bestaan.

Laad een fixture door een rij toe te voegen:

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

Relatieve specificaties worden opgelost vanuit de compositiemap; kale pakketspecificaties via het backendpakket.

Een plugin testen

De brugtests tonen het patroon:

  • scripts/test-cordis-bridge.mjs laadt een echte boom in een tijdelijke map, controleert servicebeschikbaarheid en het contract en bevestigt dat opruimen de service en listener verwijdert.
  • scripts/test-cordis-bridge-http.mjs test de routes via een echte HTTP-server, inclusief NDJSON-streaming.

Zet voor testen zonder provider model.provider: none en laad uw plugin plus de enginerijen. Voor een afhankelijkheid van llm laadt u een fixtureadapter op een ongebruikte route; stel model.provider: none in zodat de host geen concurrerende adapter laadt.

Een plugin is alleen correct als het verwijderen van zijn rij geen sporen nalaat. Test: registreren, observeren, opruimen en opnieuw observeren.

Controlelijst

  • apply en inject zijn benoemde exports.
  • De service is een Service-subklasse met een stabiele provide-naam.
  • Iedere registratie geeft een disposer terug en iedere disposer wordt door ctx.effect teruggegeven.
  • Timers komen uit ctx, niet uit globals.
  • Wijzigbare toestand hoort bij de instantie, niet bij een modulesingleton.
  • Een schema valideert de configuratie.
  • Compositie- en instellingendocumenten bevatten geen geheimen.
  • Een test bevestigt dat de plugin na opruiming niets achterlaat.