Lewati ke konten utama

Menulis Plugin Cordis

Jembatan Cordis memasang pohon plugin yang barisnya ditentukan dalam cordis.patch.yml. Untuk menambah kemampuan, tulis plugin Cordis dan tambahkan baris, tanpa mengubah sumber Libre WebUI. Halaman ini menjelaskan konvensi yang diandalkan host.

Baca Jembatan Cordis terlebih dahulu untuk memahami susunan pohon, lalu Konfigurasi Cordis untuk kolom tiap baris.

Dua bentuk plugin

Plugin Cordis berupa fungsi atau objek dengan metode apply. Loader dapat memuat keduanya.

// 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 membaca namespace modul, sehingga plugin yang dimuat dari baris memerlukan apply dan inject sebagai ekspor bernama. Ekspor default juga bisa, tetapi plugin DSH bawaan memakai ekspor bernama; gunakan bentuk tersebut agar konsisten.

Mendeklarasikan dependensi

inject adalah seluruh mekanisme dependensi. Cordis hanya menjalankan plugin setelah semua layanan yang disebutkan tersedia, lalu menjalankannya kembali jika layanan ditarik dan dipulihkan.

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

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

Ada dua konsekuensi penting:

  • Layanan yang tidak ada bukan kesalahan. Baris akan terus menunggu tanpa pemberitahuan. Karena itu, GET /api/cordis/health melaporkan status per layanan, bukan satu boolean.
  • Injeksi menentukan urutan. Anda tidak mengatur urutan eksekusi baris sendiri; urutan baris komposisi bawaan tidak menentukan urutan pemuatan.

Untuk dependensi yang opsional saat penulisan, gunakan ctx.inject di dalam apply. Callback langsung berjalan jika layanan sudah tersedia, lalu kembali berjalan setiap kali layanan muncul:

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

ctx.inject berjalan secara sinkron jika dependensinya sudah ada. Plugin yang dipasang belakangan tetap dapat mendaftar selama apply, tanpa menunggu putaran event berikutnya.

Menyediakan layanan

Turunkan Service dan berikan nama layanan kepada super. Konsumen membaca properti dengan nama tersebut, sedangkan pendaftaran dimiliki oleh 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;

Untuk layanan dengan perilaku, utamakan turunan Service daripada ctx.reflect.provide(...). Turunan itu mendaftarkan nama sekali, menyediakan metode bertipe, dan otomatis ditarik bersama fibernya.

Kepemilikan efek samping

Setiap efek samping harus memiliki pemilik. Aturan inilah yang menjamin rollback jembatan, sekaligus yang paling mudah dilanggar.

Efek sampingPemilik
Pendaftaran layananFiber plugin, secara otomatis
Pendengar eventctx.on(...) di dalam plugin
Sumber daya yang perlu dibersihkanctx.effect(() => disposer)
Timerctx.setTimeout / ctx.setInterval
Pendaftaran alatKembalikan fungsi pembersih dari ctx.effect

ctx.effect menerima fungsi yang mengembalikan fungsi pembersih, atau generator yang menghasilkan fungsi pembersih:

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

Label adalah informasi diagnostik, bukan hiasan. Label mengidentifikasi efek ketika pembersihan gagal.

Jika mengambil alih sumber daya yang tidak diketahui Cordis, seperti soket, worker, atau handle, Anda harus membersihkannya sendiri:

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

Hal yang merusak rollback

  • Mendaftar pada konteks yang berbeda dari konteks yang diberikan ke apply. Layanan akan bertahan setelah baris dihapus.
  • Membuat timer dengan setTimeout global. Timer menjaga proses tetap hidup dan tidak dibatalkan.
  • Berlangganan emitter eksternal tanpa berhenti berlangganan dalam fungsi pembersih.
  • Menulis ke singleton tingkat modul. Pembersihan tidak dapat membatalkannya, sehingga nilai tetap terlihat setelah baris dihapus. Simpan status per fiber sebagai gantinya.

Konfigurasi

Konfigurasi plugin adalah pemetaan config pada barisnya. Deklarasikan skema agar salah ketik menyebabkan kegagalan saat pemasangan, bukan diam-diam memakai 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.
}

Nilai dalam dokumen komposisi dapat memakai !!js. Ekspresi dievaluasi dalam fiber baris pemilik dengan akses ke konteks Loader. process.env dan ctx.get(...) berfungsi, tetapi import.meta tidak. name tidak pernah dievaluasi, sehingga penentu modul harus berupa string literal.

Contoh lengkap

Repositori menyertakan dua fixture yang berfungsi dan digunakan oleh pengujian jembatan. Keduanya cukup kecil untuk disalin sebagai contoh.

Adaptor model: scripts/fixtures/cordis/fake-adapter.mjs mendaftarkan rute penyedia pada ctx.llm, mengembalikan pembersih pendaftaran melalui efek, dan menjawab semua permintaan dengan teks tetap. Itulah seluruh kontrak penyedia: deklarasikan inject, daftarkan, dan miliki pendaftarannya.

Probe siklus hidup: scripts/fixtures/cordis/lifecycle-probe.mjs menyediakan layanan, berlangganan event, dan mencatat pembersihannya. Pengujian rollback menggunakannya untuk memastikan layanan dan langganan benar-benar hilang.

Pasang salah satunya dengan menambahkan baris:

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

Penentu relatif diselesaikan berdasarkan direktori berkas komposisi. Nama paket tanpa jalur diselesaikan melalui paket backend.

Menguji plugin

Rangkaian pengujian jembatan menunjukkan pola berikut:

  • scripts/test-cordis-bridge.mjs memasang pohon nyata di direktori sementara, memeriksa ketersediaan layanan dan kontraknya, serta memastikan pembersihan menarik layanan dan membebaskan pendengar.
  • scripts/test-cordis-bridge-http.mjs menguji rute melalui server HTTP nyata, termasuk streaming NDJSON.

Untuk pengujian tanpa penyedia, atur model.provider: none dan pasang plugin bersama baris mesin. Jika plugin bergantung pada llm, pasang adaptor fixture pada rute yang tidak dipakai adaptor nyata, lalu gunakan model.provider: none agar host tidak memasang adaptor pesaing pada rute yang sama.

Plugin baru benar jika menghapus barisnya tidak meninggalkan sumber daya. Uji urutannya: daftar, amati, bersihkan, lalu amati lagi.

Daftar pemeriksaan

  • apply dan inject adalah ekspor bernama.
  • Layanan merupakan turunan Service dengan nama provide yang tetap.
  • Setiap pendaftaran mengembalikan pembersih, dan setiap pembersih dikembalikan dari ctx.effect.
  • Timer berasal dari ctx, bukan fungsi global.
  • Status yang dapat berubah disimpan pada instans, bukan singleton modul.
  • Konfigurasi divalidasi dengan skema.
  • Tidak ada rahasia dalam dokumen komposisi atau pengaturan.
  • Pengujian memastikan plugin tidak menyisakan apa pun setelah dibersihkan.