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/healtharată 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.
| Efect | Proprietar |
|---|---|
| Înregistrare de serviciu | Automat, fibra pluginului |
| Listener de eveniment | ctx.on(...) în plugin |
| Resursă ce trebuie eliberată | ctx.effect(() => disposer) |
| Temporizator | ctx.setTimeout / ctx.setInterval |
| Înregistrare de instrument | Disposerul î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
setTimeoutglobal; ț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 model — scripts/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.mjsmontează un arbore real temporar, verifică disponibilitatea și contractul și confirmă că eliberarea retrage serviciul și listenerul.scripts/test-cordis-bridge-http.mjsverifică 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șiinjectsunt exporturi numite. - Serviciul extinde
Servicecu numeprovidestabil. - 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.