Tvorba pluginů Cordis
Most načítá strom pluginů podle cordis.patch.yml. Nová schopnost znamená napsat plugin Cordis a přidat řádek, ne upravovat zdroj Libre WebUI. Tato stránka popisuje konvence hostitele.
Nejprve si přečtěte Most Cordis o struktuře a Konfiguraci Cordis o polích řádků.
Dvě podoby pluginu
Plugin Cordis je funkce nebo objekt s metodou apply. Loader umí obojí.
// 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 čte jmenný prostor modulu, takže řádek vyžaduje pojmenované exporty apply a inject. Výchozí export také funguje, ale dodané pluginy DSH používají pojmenované; držte se jejich vzoru.
Deklarace závislostí
inject je celý mechanismus závislostí. Cordis plugin nespustí, dokud neexistují všechny služby, a spustí jej znovu po odebrání a navrácení některé z nich.
export const inject = ['tools', 'systemPrompt'];
export function apply(ctx, config) {
// `ctx.tools` and `ctx.systemPrompt` are guaranteed present here.
}
Dva důležité důsledky:
- Chybějící služba není chyba. Řádek tiše čeká donekonečna. Proto
GET /api/cordis/healthuvádí stav každé služby místo jediného booleovského údaje. - Injekce určuje pořadí. Běh řádků sami neřadíte; pořadí v dodané kompozici nemá význam pro načítání.
Pro volitelnou závislost použijte ctx.inject v apply. Callback běží ihned, pokud služby existují, a znovu při jejich objevení:
export function apply(ctx) {
ctx.inject(['typert'], inner => {
inner.typert.lookups.register('session', {/* ... */});
});
}
Pokud závislosti existují, ctx.inject je synchronní: i pozdě načtený plugin se registruje při apply, ne až v dalším cyklu.
Zveřejnění služby
Rozšiřte Service a předejte název do super. Název je vlastnost čtená konzumenty; registrace patří vláknu pluginu.
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;
Pro chování upřednostněte podtřídu Service před ctx.reflect.provide(...): registruje název jednou, vystavuje typované metody a automaticky se odebere s vláknem.
Vlastnictví vedlejších účinků
Každý účinek musí mít vlastníka. Tato snadno porušitelná zásada zajišťuje možnost úplného vrácení mostu.
| Účinek | Vlastník |
|---|---|
| Registrace služby | Automaticky vlákno pluginu |
| Posluchač událostí | ctx.on(...) v pluginu |
| Prostředek vyžadující úklid | ctx.effect(() => disposer) |
| Časovač | ctx.setTimeout / ctx.setInterval |
| Registrace nástroje | Disposer vrácený z ctx.effect |
ctx.effect přijímá funkci vracející disposer nebo generátor disposerů:
ctx.effect(() => {
const registration = ctx.llm.registerAdapter(['my-route'], adapter);
return () => registration();
}, 'my-adapter.register');
Štítek je diagnostika: pojmenuje účinek při selhání úklidu.
Prostředek neznámý Cordis, jako socket, worker nebo handle, musíte uvolnit sami:
ctx.effect(() => {
const worker = startWorker();
return () => worker.terminate();
}, 'my-plugin.worker');
Co narušuje vrácení
- Registrace na jiném kontextu než předaném do
apply: služba přežije řádek. - Časovač přes globální
setTimeout: drží proces při životě a nikdy se nezruší. - Přihlášení k externímu emitteru bez odhlášení v disposeru.
- Zápis do singletonu modulu: úklid jej nevrátí a hodnota zůstane po odstranění řádku. Použijte stav pro každé vlákno.
Konfigurace
Nastavení pluginu je mapa config jeho řádku. Deklarujte schéma, aby překlep způsobil chybu při načítání, ne tiché použití výchozí hodnoty:
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.
}
Konfigurace smí používat !!js. Výraz se hodnotí ve vlákně řádku s kontextem Loaderu: process.env a ctx.get(...) fungují, import.meta ne. name se nikdy nevyhodnocuje; specifikátor modulu musí být literál.
Úplný příklad
Repozitář dodává dva malé funkční příklady používané testy mostu, vhodné ke kopírování.
Adaptér modelu — scripts/fixtures/cordis/fake-adapter.mjs registruje cestu poskytovatele na ctx.llm, vrací její disposer z efektu a odpovídá pevným textem. To je celý kontrakt: deklarovat inject, registrovat a vlastnit registraci.
Sonda životního cyklu — scripts/fixtures/cordis/lifecycle-probe.mjs zveřejní službu, přihlásí se k události a zaznamená vlastní úklid. Test tak prokáže, že obě skutečně zmizely.
Příklad načtěte přidáním řádku:
- id: my-adapter
name: './scripts/fixtures/cordis/fake-adapter.mjs'
config:
route: my-route
Relativní specifikátory se řeší vůči složce kompozice, prosté názvy přes balíček backendu.
Testování pluginu
Sady mostu ukazují postup:
scripts/test-cordis-bridge.mjsnačte skutečný strom v dočasné složce, ověří dostupnost a kontrakt a potvrdí, že úklid odebral službu i posluchač.scripts/test-cordis-bridge-http.mjstestuje skutečný HTTP server včetně NDJSON.
Bez poskytovatele nastavte model.provider: none a načtěte plugin s řádky modulu. Pro závislost llm použijte testovací adaptér na neobsazené cestě a model.provider: none, aby hostitel nepřidal konkurenční adaptér.
Plugin je správný, pouze pokud odstranění jeho řádku nezanechá stopy. Ověřte: registrace, pozorování, úklid, nové pozorování.
Kontrolní seznam
-
applyainjectjsou pojmenované exporty. - Služba je podtřída
Servicese stálým názvemprovide. - Každá registrace vrací disposer a každý disposer se vrací z
ctx.effect. - Časovače pocházejí z
ctx, ne z globálních funkcí. - Měnitelný stav patří instanci, ne singletonu modulu.
- Konfigurace se ověřuje schématem.
- Dokumenty kompozice a nastavení neobsahují tajné údaje.
- Test potvrzuje, že úklid pluginu nezanechává nic.