Sari la conținutul principal

Crearea pluginurilor Cordis

Puntea Cordis montează un arbore de pluginuri declarate în cordis.patch.yml. O capacitate nouă cere un plugin Cordis și un rând, nu modificarea surselor Libre WebUI. Aici sunt convențiile pe care se bazează gazda.

Citiți întâi Puntea Cordis despre structură și Configurarea Cordis despre câmpurile rândurilor.

Cele două forme de plugin

Un plugin este o funcție sau un obiect cu metoda apply. Loaderul rezolvă ambele forme.

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

Loaderul citește spațiul de nume al modulului; de aceea apply și inject trebuie exportate cu nume. Funcționează și exportul implicit, dar pluginurile DSH livrate folosesc exporturi numite; preferați-le pentru consecvență.

Declararea dependențelor

inject este întregul mecanism. Cordis așteaptă toate serviciile numite și reexecută pluginul dacă unul este retras și restaurat.

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

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

Două consecințe importante:

  • Serviciul lipsă nu este o eroare. Rândul așteaptă silențios la nesfârșit. De aceea GET /api/cordis/health arată fiecare serviciu, nu un singur boolean.
  • Injecția stabilește ordinea. Nu secvențiați manual rândurile; ordinea compoziției livrate nu controlează încărcarea.

Pentru dependență opțională folosiți ctx.inject în apply. Callbackul rulează imediat dacă serviciile există și din nou când apar:

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

Cu dependențe prezente, ctx.inject este sincron: și un plugin montat târziu înregistrează în apply, nu într-un tick ulterior.

Publicarea unui serviciu

Extindeți Service și transmiteți numele la super. Numele este proprietatea citită de consumatori; înregistrarea aparține fibrei pluginului.

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;

Pentru comportament preferați o subclasă Service față de ctx.reflect.provide(...): înregistrează o singură dată numele, oferă metode tipizate și se retrage automat cu fibra.

Proprietatea efectelor secundare

Orice efect trebuie să aibă proprietar. Este regula care permite revenirea completă și este ușor de încălcat.

EfectProprietar
Înregistrare de serviciuAutomat, fibra pluginului
Listener de evenimentctx.on(...) în plugin
Resursă ce trebuie eliberatăctx.effect(() => disposer)
Temporizatorctx.setTimeout / ctx.setInterval
Înregistrare de instrumentDisposerul întors din ctx.effect

ctx.effect acceptă o funcție care întoarce un disposer sau un generator de disposere:

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

Eticheta este diagnostică: identifică efectul când eliberarea eșuează.

O resursă necunoscută Cordis — socket, worker, handle — trebuie eliberată explicit:

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

Ce strică revenirea

  • Înregistrarea pe alt context decât cel dat lui apply; serviciul supraviețuiește rândului.
  • Crearea temporizatorului cu setTimeout global; ține procesul activ și nu este anulat.
  • Abonarea la un emitter extern fără dezabonare în disposer.
  • Scrierea într-un singleton de modul; eliberarea nu o poate anula, deci valoarea rămâne după ștergerea rândului. Folosiți stare per fibră.

Configurare

Configurația pluginului este maparea config a rândului. Declarați o schemă, astfel încât o greșeală să eșueze la montare, nu să folosească implicit o valoare:

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

Valorile pot utiliza !!js. Expresia se evaluează în fibra proprietară cu contextul Loader disponibil: process.env și ctx.get(...) funcționează, import.meta nu. name nu se evaluează; specificatorul modulului trebuie să fie literal.

Un exemplu complet

Repozitoriul livrează două fixture funcționale folosite de testele punții, suficient de mici pentru copiere.

Adaptor de modelscripts/fixtures/cordis/fake-adapter.mjs înregistrează o rută pe ctx.llm, întoarce disposerul înregistrării dintr-un efect și răspunde cu text fix. Contractul este: declarați inject, înregistrați și dețineți înregistrarea.

Sondă de ciclu de viațăscripts/fixtures/cordis/lifecycle-probe.mjs publică un serviciu, se abonează la un eveniment și își înregistrează eliberarea, demonstrând în test că ambele dispar.

Montați oricare exemplu adăugând un rând:

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

Specificatorii relativi se rezolvă față de directorul compoziției; numele simple prin pachetul backend.

Testarea unui plugin

Suitele punții oferă modelul:

  • scripts/test-cordis-bridge.mjs montează un arbore real temporar, verifică disponibilitatea și contractul și confirmă că eliberarea retrage serviciul și listenerul.
  • scripts/test-cordis-bridge-http.mjs verifică rutele printr-un server HTTP real, inclusiv NDJSON.

Fără furnizor setați model.provider: none și montați pluginul plus rândurile motorului. Pentru dependență llm, montați un adaptor fixture pe o rută neocupată și model.provider: none, ca gazda să nu creeze un adaptor concurent.

Pluginul este corect numai dacă eliminarea rândului nu lasă urme. Verificați: înregistrare, observare, eliberare, observare din nou.

Listă de verificare

  • apply și inject sunt exporturi numite.
  • Serviciul extinde Service cu nume provide stabil.
  • Fiecare înregistrare întoarce disposer, fiecare disposer vine din ctx.effect.
  • Temporizatoarele vin din ctx, nu din globale.
  • Starea mutabilă aparține instanței, nu unui singleton.
  • Configurația este validată prin schemă.
  • Documentele nu conțin secrete.
  • Un test verifică absența urmelor după eliberare.