Μετάβαση στο κύριο περιεχόμενο

Δημιουργία plugins Cordis

Η γέφυρα φορτώνει δέντρο plugins με εγγραφές στο cordis.patch.yml. Μια νέα δυνατότητα απαιτεί plugin Cordis και νέα εγγραφή, όχι αλλαγή πηγαίου Libre WebUI. Εδώ περιγράφονται οι συμβάσεις του host.

Διαβάστε πρώτα Γέφυρα Cordis για τη δομή και Ρύθμιση Cordis για τα πεδία.

Οι δύο μορφές plugin

Ένα plugin είναι συνάρτηση ή αντικείμενο με μέθοδο apply. Ο Loader επιλύει και τις δύο μορφές.

// 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 διαβάζει το namespace του module, άρα χρειάζονται ονομασμένα exports apply και inject. Λειτουργεί και default export, αλλά τα παρεχόμενα DSH plugins χρησιμοποιούν ονομασμένα· προτιμήστε συνέπεια.

Δήλωση εξαρτήσεων

Το inject είναι όλος ο μηχανισμός εξαρτήσεων. Το Cordis περιμένει όλες τις υπηρεσίες και επανεκτελεί το plugin όταν κάποια αποσυρθεί και επιστρέψει.

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

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

Δύο σημαντικές συνέπειες:

  • Η απουσία υπηρεσίας δεν είναι σφάλμα. Η εγγραφή μένει σιωπηλά pending για πάντα. Γι’ αυτό το GET /api/cordis/health δείχνει κατάσταση ανά υπηρεσία αντί ενός boolean.
  • Το injection καθορίζει σειρά. Δεν ακολουθείτε χειροκίνητη αλληλουχία· η θέση των εγγραφών στη σύνθεση δεν καθορίζει φόρτωση.

Για προαιρετική εξάρτηση χρησιμοποιήστε ctx.inject μέσα στο apply. Το callback τρέχει αμέσως αν υπάρχουν υπηρεσίες και ξανά όταν εμφανιστούν:

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

Το ctx.inject είναι σύγχρονο όταν οι εξαρτήσεις υπάρχουν. Ακόμη και αργά φορτωμένο plugin καταχωρίζει στο apply, όχι σε επόμενο tick.

Δημοσίευση υπηρεσίας

Επεκτείνετε το Service και δώστε το όνομα στο super. Αυτό είναι η ιδιότητα που διαβάζουν οι καταναλωτές· η καταχώριση ανήκει στο fiber του 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;

Για συμπεριφορά προτιμήστε υποκλάση Service αντί ctx.reflect.provide(...): καταχωρίζει το όνομα μία φορά, παρέχει τυποποιημένες μεθόδους και αποσύρεται αυτόματα μαζί με το fiber.

Ιδιοκτησία παρενεργειών

Κάθε παρενέργεια χρειάζεται ιδιοκτήτη. Αυτός ο εύθραυστος κανόνας εξασφαλίζει την εγγύηση αναίρεσης.

ΠαρενέργειαΙδιοκτήτης
Καταχώριση υπηρεσίαςΑυτόματα το fiber του plugin
Listener συμβάντοςctx.on(...) μέσα στο plugin
Πόρος που χρειάζεται καθαρισμόctx.effect(() => disposer)
Χρονοδιακόπτηςctx.setTimeout / ctx.setInterval
Καταχώριση εργαλείουDisposer που επιστρέφει το ctx.effect

Το ctx.effect δέχεται συνάρτηση που επιστρέφει disposer ή generator που παράγει disposers:

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

Η ετικέτα είναι διαγνωστική: ονομάζει το effect αν αποτύχει ο καθαρισμός.

Πόρο άγνωστο στο Cordis — socket, worker ή handle — πρέπει να τον αποδεσμεύετε εσείς:

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

Τι χαλάει την αναίρεση

  • Καταχώριση σε διαφορετικό context από του apply. Η υπηρεσία επιζεί της εγγραφής.
  • Timer με global setTimeout. Κρατά το process ενεργό και δεν ακυρώνεται.
  • Συνδρομή σε εξωτερικό emitter χωρίς αποσύνδεση στον disposer.
  • Εγγραφή σε singleton module. Η αποδέσμευση δεν την αναιρεί και η τιμή παραμένει μετά την αφαίρεση· χρησιμοποιήστε κατάσταση ανά fiber.

Ρύθμιση

Η ρύθμιση του plugin είναι το mapping config της εγγραφής. Δηλώστε schema ώστε τυπογραφικό λάθος να αποτυγχάνει στη φόρτωση αντί να επιλέγει σιωπηρά default:

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

Οι τιμές μπορούν να χρησιμοποιούν !!js. Η έκφραση αξιολογείται στο fiber της εγγραφής με το context Loader διαθέσιμο: process.env και ctx.get(...) λειτουργούν, import.meta όχι. Το name δεν αξιολογείται ποτέ· απαιτείται κυριολεκτικός προσδιοριστής module.

Πλήρες παράδειγμα

Το repository παρέχει δύο λειτουργικά fixtures των δοκιμών, αρκετά μικρά για αντιγραφή.

Adapter μοντέλου — το scripts/fixtures/cordis/fake-adapter.mjs καταχωρίζει διαδρομή στο ctx.llm, επιστρέφει τον disposer μέσω effect και απαντά σταθερό κείμενο. Αυτή είναι η σύμβαση: δήλωση inject, καταχώριση, ιδιοκτησία της καταχώρισης.

Probe κύκλου ζωής — το scripts/fixtures/cordis/lifecycle-probe.mjs δημοσιεύει υπηρεσία, εγγράφεται σε event και καταγράφει την αποδέσμευσή του, ώστε η δοκιμή να αποδεικνύει ότι και τα δύο παύουν να υπάρχουν.

Φορτώστε το επιλεγμένο fixture προσθέτοντας εγγραφή:

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

Οι σχετικές αναφορές επιλύονται ως προς τον φάκελο σύνθεσης, τα απλά ονόματα μέσω του πακέτου backend.

Δοκιμή plugin

Οι σουίτες δείχνουν το μοτίβο:

  • Το scripts/test-cordis-bridge.mjs φορτώνει πραγματικό δέντρο σε προσωρινό φάκελο, ελέγχει διαθεσιμότητα και σύμβαση και επιβεβαιώνει απόσυρση υπηρεσίας/listener.
  • Το scripts/test-cordis-bridge-http.mjs ελέγχει τις διαδρομές σε πραγματικό HTTP server, μαζί με NDJSON.

Χωρίς πάροχο, ορίστε model.provider: none και φορτώστε plugin και εγγραφές μηχανής. Για εξάρτηση από llm, χρησιμοποιήστε fixture adapter σε αδέσμευτη διαδρομή και model.provider: none, ώστε ο host να μη φορτώσει ανταγωνιστικό adapter.

Plugin είναι σωστό μόνο αν αφαίρεση της εγγραφής δεν αφήνει ίχνος. Ελέγξτε: καταχώριση, παρατήρηση, αποδέσμευση, νέα παρατήρηση.

Λίστα ελέγχου

  • Τα apply και inject είναι ονομασμένα exports.
  • Η υπηρεσία επεκτείνει Service με σταθερό όνομα provide.
  • Κάθε καταχώριση επιστρέφει disposer και κάθε disposer επιστρέφεται από ctx.effect.
  • Οι timers προέρχονται από ctx, όχι globals.
  • Η μεταβλητή κατάσταση ανήκει στο instance, όχι σε singleton.
  • Schema ελέγχει τη ρύθμιση.
  • Κανένα μυστικό δεν βρίσκεται στα έγγραφα σύνθεσης ή ρυθμίσεων.
  • Δοκιμή επιβεβαιώνει μηδενικά ίχνη μετά την αποδέσμευση.