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/healtheen 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.
| Neveneffect | Eigenaar |
|---|---|
| Serviceregistratie | Automatisch de pluginfiber |
| Eventlistener | ctx.on(...) binnen de plugin |
| Op te ruimen resource | ctx.effect(() => disposer) |
| Timer | ctx.setTimeout / ctx.setInterval |
| Toolregistratie | De 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
applyis 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.mjslaadt 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.mjstest 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
-
applyeninjectzijn benoemde exports. - De service is een
Service-subklasse met een stabieleprovide-naam. - Iedere registratie geeft een disposer terug en iedere disposer wordt door
ctx.effectteruggegeven. - 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.