Creare plugin Cordis
Il bridge Cordis monta un albero di plugin elencati in cordis.patch.yml. Per aggiungere una funzionalità si scrive un plugin Cordis e si aggiunge una riga, senza modificare i sorgenti di Libre WebUI. Questa pagina descrive le convenzioni su cui si basa l’host.
Leggi prima Bridge Cordis per capire la struttura dell’albero e Configurazione Cordis per i campi delle righe.
Le due forme di plugin
Un plugin Cordis è una funzione oppure un oggetto con metodo apply. Il Loader risolve entrambe le 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) {
// ...
}
Il Loader legge lo spazio dei nomi del modulo: un plugin caricato da una riga deve quindi esportare apply e inject con nome. Funziona anche un’esportazione predefinita, ma i plugin DSH forniti usano quella con nome: preferiscila per coerenza.
Dichiarare le dipendenze
inject è l’intero meccanismo delle dipendenze. Cordis esegue il plugin solo quando esistono tutti i servizi indicati e lo riesegue se uno viene ritirato e ripristinato.
export const inject = ['tools', 'systemPrompt'];
export function apply(ctx, config) {
// `ctx.tools` and `ctx.systemPrompt` are guaranteed present here.
}
Due conseguenze sono importanti:
- Un servizio mancante non è un errore. La riga resta in attesa indefinitamente, senza avvisi. Per questo
GET /api/cordis/healthriporta lo stato per servizio invece di un singolo booleano. - L’iniezione determina l’ordine. Non ordinare manualmente l’esecuzione delle righe: la loro posizione nella composizione fornita non determina il caricamento.
Per una dipendenza facoltativa in fase di sviluppo, usa ctx.inject dentro apply. Il callback viene eseguito subito se i servizi esistono e nuovamente quando diventano disponibili:
export function apply(ctx) {
ctx.inject(['typert'], inner => {
inner.typert.lookups.register('session', {/* ... */});
});
}
Quando le dipendenze sono già presenti, ctx.inject è sincrono: anche un plugin montato tardi registra durante apply, non in un ciclo successivo.
Pubblicare un servizio
Estendi Service e passa il nome a super. Il nome identifica la proprietà letta dai consumatori; la registrazione appartiene alla fibra del plugin.
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;
Per servizi con comportamento, preferisci una sottoclasse Service a ctx.reflect.provide(...): registra il nome una volta, espone metodi tipizzati e viene ritirata automaticamente con la fibra.
Gestire gli effetti collaterali
Ogni effetto deve avere un proprietario. È la regola che rende possibile il ripristino garantito dal bridge, ed è facile violarla.
| Effetto | Proprietario |
|---|---|
| Registrazione di un servizio | La fibra del plugin, automaticamente |
| Listener di un evento | ctx.on(...) nel plugin |
| Risorsa da rilasciare | ctx.effect(() => disposer) |
| Timer | ctx.setTimeout / ctx.setInterval |
| Registrazione di uno strumento | Il disposer restituito da ctx.effect |
ctx.effect accetta una funzione che restituisce un disposer oppure un generatore che produce disposer:
ctx.effect(() => {
const registration = ctx.llm.registerAdapter(['my-route'], adapter);
return () => registration();
}, 'my-adapter.register');
L’etichetta serve alla diagnostica: identifica l’effetto quando il rilascio fallisce.
Una risorsa sconosciuta a Cordis, come un socket, un worker o un handle, deve essere rilasciata esplicitamente:
ctx.effect(() => {
const worker = startWorker();
return () => worker.terminate();
}, 'my-plugin.worker');
Cosa impedisce il ripristino
- Registrarsi su un contesto diverso da quello passato a
apply: il servizio sopravvive alla riga. - Creare un timer con
setTimeoutglobale: mantiene vivo il processo e non viene annullato. - Sottoscrivere un emettitore esterno senza annullare la sottoscrizione nel disposer.
- Scrivere in un singleton di modulo: il rilascio non può annullarlo e il valore resta visibile dopo la rimozione della riga. Usa invece uno stato per fibra.
Configurazione
La configurazione del plugin è la mappa config della sua riga. Dichiara uno schema affinché un errore di battitura fallisca al montaggio anziché usare silenziosamente un valore predefinito:
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.
}
I valori possono usare !!js nel documento di composizione. L’espressione viene valutata nella fibra proprietaria con il contesto del Loader: process.env e ctx.get(...) funzionano, import.meta no. name non viene mai valutato: lo specificatore del modulo deve essere una stringa letterale.
Un esempio completo
Il repository include due fixture funzionanti usate dai test del bridge, entrambe abbastanza piccole da copiare.
Adattatore di modello: scripts/fixtures/cordis/fake-adapter.mjs registra una route provider su ctx.llm, restituisce il disposer della registrazione da un effetto e risponde con testo fisso. Questo è l’intero contratto: dichiarare inject, registrarsi e possedere la registrazione.
Sonda del ciclo di vita: scripts/fixtures/cordis/lifecycle-probe.mjs pubblica un servizio, sottoscrive un evento e registra il proprio rilascio; il test di ripristino dimostra così che entrambi cessano di esistere.
Monta una delle due aggiungendo una riga:
- id: my-adapter
name: './scripts/fixtures/cordis/fake-adapter.mjs'
config:
route: my-route
Gli specificatori relativi si risolvono rispetto alla directory della composizione; quelli di pacchetto tramite il pacchetto backend.
Testare un plugin
Le suite del bridge mostrano il modello:
scripts/test-cordis-bridge.mjsmonta un albero reale in una directory temporanea, verifica disponibilità e contratto dei servizi e controlla che il rilascio ritiri il servizio e liberi il listener.scripts/test-cordis-bridge-http.mjsesercita le route con un server HTTP reale, incluso lo streaming NDJSON.
Per testare senza provider, imposta model.provider: none e monta il plugin insieme alle righe del motore. Se dipende da llm, monta un adattatore fixture su una route non usata da un adattatore reale e imposta model.provider: none, così l’host non carica un concorrente.
Un plugin è corretto solo se rimuoverne la riga non lascia tracce. Verifica la sequenza: registra, osserva, rilascia, osserva di nuovo.
Lista di controllo
-
applyeinjectsono esportazioni con nome. - Il servizio è una sottoclasse di
Servicecon nomeprovidestabile. - Ogni registrazione restituisce un disposer e ogni disposer viene restituito da
ctx.effect. - I timer provengono da
ctx, non da funzioni globali. - Lo stato mutabile appartiene all’istanza, non a un singleton di modulo.
- Uno schema convalida la configurazione.
- Nei documenti di composizione o impostazioni non compaiono segreti.
- Un test verifica che il rilascio non lasci tracce del plugin.