Utveckla Cordis-plugins
Cordis-bryggan laddar ett pluginträd vars rader namnges i cordis.patch.yml. Att lägga till en funktion innebär att skriva ett Cordis-plugin och en rad, inte att ändra Libre WebUI:s källkod. Sidan beskriver konventionerna som värden förlitar sig på.
Läs först Cordis-brygga om trädets uppbyggnad och Cordis-konfiguration om radfälten.
De två pluginformerna
Ett Cordis-plugin är antingen en funktion eller ett objekt med metoden apply. Loader hanterar båda formerna.
// 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 modulens namnrymd, så ett plugin från en rad behöver apply och inject som namngivna exporter. En standardexport fungerar också, men DSH:s medföljande plugins använder den namngivna formen. Föredra den för konsekvens.
Deklarera beroenden
inject är hela beroendemekanismen. Cordis kör inte pluginet förrän varje namngiven tjänst finns och kör det igen om en tjänst dras tillbaka och återställs.
export const inject = ['tools', 'systemPrompt'];
export function apply(ctx, config) {
// `ctx.tools` and `ctx.systemPrompt` are guaranteed present here.
}
Två följder är viktiga:
- En saknad tjänst är inget fel. Raden förblir tyst väntande utan tidsgräns. Därför visar
GET /api/cordis/healthstatus per tjänst i stället för en enda boolean. - Injection avgör ordningen. Du ordnar inte radernas körning själv; radordningen i kompositionen bestämmer inte laddningen.
Använd ctx.inject inuti apply för ett beroende som är valfritt vid utveckling. Återanropet körs direkt om tjänsterna finns och på nytt varje gång de blir tillgängliga:
export function apply(ctx) {
ctx.inject(['typert'], inner => {
inner.typert.lookups.register('session', {/* ... */});
});
}
ctx.inject körs synkront när beroendena redan finns. Ett sent laddat plugin registreras därför under apply, inte i ett senare tick.
Publicera en tjänst
Utöka Service och skicka tjänstenamnet till super. Namnet är egenskapen konsumenterna läser; registreringen ägs av 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;
Föredra en Service-underklass framför ctx.reflect.provide(...) för allt med beteende. Underklassen registrerar namnet en gång, exponerar typade metoder och dras automatiskt tillbaka med sin fiber.
Ägarskap för sidoeffekter
Varje sidoeffekt måste ha en ägare. Den regeln gör bryggans återställningsgaranti möjlig och är lättast att bryta.
| Sidoeffekt | Ägare |
|---|---|
| Tjänsteregistrering | Automatiskt pluginets fiber |
| Händelselyssnare | ctx.on(...) inne i pluginet |
| Resurs som behöver städas | ctx.effect(() => disposer) |
| Timer | ctx.setTimeout / ctx.setInterval |
| Verktygsregistrering | Returnera städfunktionen från ctx.effect |
ctx.effect tar en funktion som returnerar en städfunktion, eller en generator som ger städfunktioner:
ctx.effect(() => {
const registration = ctx.llm.registerAdapter(['my-route'], adapter);
return () => registration();
}, 'my-adapter.register');
Etiketten är diagnostik, inte dekoration. Den identifierar effekten när städning misslyckas.
En resurs som Cordis inte känner till, exempelvis en socket, worker eller ett handtag, måste du själv frigöra:
ctx.effect(() => {
const worker = startWorker();
return () => worker.terminate();
}, 'my-plugin.worker');
Vad som hindrar återställning
- Registrera på en annan kontext än den som ges till
apply. Tjänsten överlever då sin rad. - Skapa en timer med globala
setTimeout. Den håller processen vid liv och avbryts aldrig. - Prenumerera på en extern emitter utan att avsluta prenumerationen i städfunktionen.
- Skriva till en singleton på modulnivå. Städning kan inte ångra detta, så värdet förblir synligt efter att raden tagits bort. Använd tillstånd per fiber i stället.
Konfiguration
Ett plugins konfiguration är radens config-mappning. Deklarera ett schema så att stavfel ger fel vid laddning i stället för ett tyst standardvärde:
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.
}
Konfiguration kan använda !!js i kompositionsdokumentet. Uttrycket utvärderas i ägarradens fiber med Loader-kontexten tillgänglig. process.env och ctx.get(...) fungerar, men inte import.meta. name utvärderas aldrig, så modulens specificerare måste vara en bokstavlig sträng.
Ett fullständigt exempel
Repositoryt innehåller två fungerande fixtures för bryggans tester. Båda är små nog att kopiera.
En modelladapter: scripts/fixtures/cordis/fake-adapter.mjs registrerar en leverantörsrutt i ctx.llm, returnerar registreringens städfunktion från en effekt och besvarar varje anrop med fast text. Det är hela leverantörskontraktet: deklarera inject, registrera och äg registreringen.
En livscykelsond: scripts/fixtures/cordis/lifecycle-probe.mjs publicerar en tjänst, prenumererar på en händelse och registrerar sin städning. Återställningstestet verifierar därmed att båda verkligen upphör.
Ladda endera genom att lägga till en rad:
- id: my-adapter
name: './scripts/fixtures/cordis/fake-adapter.mjs'
config:
route: my-route
Relativa specificerare löses mot kompositionsfilens katalog; paketnamn löses via backendpaketet.
Testa ett plugin
Bryggans testsviter visar mönstret:
scripts/test-cordis-bridge.mjsladdar ett riktigt träd i en tillfällig katalog, kontrollerar tjänstetillgänglighet, testar kontraktet och verifierar att städning drog tillbaka tjänsten och frigjorde lyssnaren.scripts/test-cordis-bridge-http.mjsanvänder rutterna genom en riktig HTTP-server, inklusive NDJSON-strömning.
För test utan leverantör anger du model.provider: none och laddar pluginet och motorraderna. Beror pluginet på llm, ladda en fixtureadapter på en rutt som ingen riktig adapter använder. Ange model.provider: none så värden inte laddar en konkurrerande adapter på rutten.
Ett plugin är korrekt först när borttagning av raden inte lämnar spår. Verifiera ordningen: registrera, observera, avveckla, observera igen.
Checklista
-
applyochinjectär namngivna exporter. - Tjänsten är en
Service-underklass med ett stabiltprovide-namn. - Varje registrering returnerar en städfunktion och varje städfunktion returneras från
ctx.effect. - Timer kommer från
ctx, inte globala funktioner. - Föränderligt tillstånd finns på instansen, inte en modul-singleton.
- Konfiguration valideras av ett schema.
- Inga hemligheter finns i kompositionen eller inställningsdokumentet.
- Ett test verifierar att pluginet inte lämnar något efter avveckling.