Cordis プラグインの作成
Cordis ブリッジは、cordis.patch.yml の各行で指定されたプラグインツリーをマウントします。機能を追加する際は Cordis プラグインを作成して行を追加します。Libre WebUI のソースを変更する必要はありません。このページでは、ホストが前提とする規約を説明します。
まず Cordis ブリッジでツリーの構成を確認し、各行のフィールドについては Cordis の設定を参照してください。
プラグインの 2 つの形式
Cordis プラグインは、関数または 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 はモジュールの名前空間を読むため、設定行から読み込むプラグインは apply(および inject)を名前付きでエクスポートする必要があります。デフォルトエクスポートも使えますが、同梱の DSH プラグインは名前付き形式を使用しているため、統一することを推奨します。
依存関係の宣言
依存関係はすべて inject で指定します。指定したすべてのサービスが存在するまで Cordis はプラグインを実行せず、サービスが撤回された後に復元されると再実行します。
export const inject = ['tools', 'systemPrompt'];
export function apply(ctx, config) {
// `ctx.tools` and `ctx.systemPrompt` are guaranteed present here.
}
次の 2 点が重要です。
- サービスの欠落はエラーではありません。 行は通知なしで待機し続けます。そのため、
GET /api/cordis/healthは単一の真偽値ではなく、サービスごとの状態を返します。 - 注入が実行順序を決めます。 行の順序を自分で制御する必要はありません。同梱構成の行順には、読み込み順を決める意味はありません。
作成時点で任意の依存関係とする場合は、apply の中で ctx.inject を使用します。サービスがすでに存在すればコールバックを直ちに実行し、その後もサービスが利用可能になるたびに実行します。
export function apply(ctx) {
ctx.inject(['typert'], inner => {
inner.typert.lookups.register('session', {/* ... */});
});
}
依存先がすでに存在する場合、ctx.inject は同期的に実行されます。そのため、後からマウントされたプラグインも次のイベントループを待たず、apply の実行中に登録できます。
サービスの公開
Service を継承し、サービス名を super に渡します。その名前は利用側が読むプロパティになり、登録はプラグインのファイバーによって管理されます。
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;
動作を持つサービスには、ctx.reflect.provide(...) より Service のサブクラスを推奨します。名前を一度だけ登録し、型付きのメソッドを公開し、所属するファイバーとともに自動的に撤回されます。
副作用の管理
すべての副作用には管理主体が必要です。 これはブリッジのロールバック保証を成立させる基本規則であり、最も破りやすい規則でもあります。
| 副作用 | 管理主体 |
|---|---|
| サービスの登録 | プラグインのファイバーが自動管理 |
| イベントリスナー | プラグイン内の ctx.on(...) |
| 後処理が必要なリソース | ctx.effect(() => disposer) |
| タイマー | ctx.setTimeout / ctx.setInterval |
| ツールの登録 | ctx.effect から破棄関数を返す |
ctx.effect には、破棄関数を返す関数、または破棄関数を yield するジェネレーターを渡します。
ctx.effect(() => {
const registration = ctx.llm.registerAdapter(['my-route'], adapter);
return () => registration();
}, 'my-adapter.register');
ラベルは装飾ではなく診断用です。破棄に失敗した際に、副作用を識別する名前になります。
ソケット、ワーカー、ハンドルなど、Cordis が把握できないリソースを引き受ける場合は、自分で破棄処理を行います。
ctx.effect(() => {
const worker = startWorker();
return () => worker.terminate();
}, 'my-plugin.worker');
ロールバックを壊す操作
applyに渡されたものと異なるコンテキストに登録する。行を削除した後もサービスが残ります。- グローバルな
setTimeoutでタイマーを作成する。プロセスが生き続け、タイマーはキャンセルされません。 - 外部エミッターを購読し、破棄関数で購読を解除しない。
- モジュール単位のシングルトンに書き込む。破棄では元に戻せず、行を削除しても値が残ります。ファイバーごとの状態として保持してください。
設定
プラグインの設定は、その行の config マッピングです。スキーマを宣言し、入力ミスがあれば暗黙に既定値を使うのではなく、マウント時にエラーにします。
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 を使用できます。式は所属する行のファイバー内で評価され、Loader のコンテキストを参照できます。process.env と ctx.get(...) は使えますが、import.meta は使えません。name は評価されないため、行のモジュール指定子には文字列リテラルが必要です。
完全な例
リポジトリには、ブリッジのテストスイートで使用する 2 つの動作するフィクスチャがあります。どちらも小さく、コピーして利用できます。
モデルアダプター:scripts/fixtures/cordis/fake-adapter.mjs は ctx.llm にプロバイダールートを登録し、その登録の破棄関数をエフェクトから返し、すべてのリクエストに固定のテキストを返します。プロバイダーの規約は、inject の宣言、登録、その登録の管理だけです。
ライフサイクルプローブ:scripts/fixtures/cordis/lifecycle-probe.mjs はサービスを公開し、イベントを購読し、自身の破棄を記録します。ロールバックテストはこれにより、サービスと購読の両方が確実になくなることを確認します。
どちらも、行を追加すればマウントできます。
- id: my-adapter
name: './scripts/fixtures/cordis/fake-adapter.mjs'
config:
route: my-route
相対指定子は構成ファイルのディレクトリを基準に解決され、裸の指定子はバックエンドのパッケージを通じて解決されます。
プラグインのテスト
ブリッジのテストスイートでは、次の方法を使用しています。
scripts/test-cordis-bridge.mjsは一時ディレクトリに実際のツリーをマウントし、サービスの利用可能性とインターフェイスの規約を検証します。さらに、破棄によってサービスが撤回され、リスナーが解放されたことを確認します。scripts/test-cordis-bridge-http.mjsは実際の HTTP サーバーを介してルートを呼び出し、NDJSON ストリーミングも検証します。
プロバイダーなしでプラグインをテストするには、model.provider: none を設定し、プラグインとエンジンの行をマウントします。llm に依存するプラグインの場合は、実際のアダプターが使用していないルートにフィクスチャのアダプターをマウントし、model.provider: none を設定して、ホストが競合するアダプターを同じルートにマウントしないようにします。
行を削除した後に何も残らないことが、正しいプラグインの条件です。登録、観測、破棄、再観測の一連の流れを検証してください。
チェックリスト
-
applyとinjectが名前付きエクスポートになっている。 - サービスが
Serviceのサブクラスで、安定したprovide名を持つ。 - すべての登録が破棄関数を返し、その関数が
ctx.effectから返される。 - タイマーはグローバル関数ではなく
ctxから作成する。 - 可変状態はモジュールのシングルトンではなくインスタンスで保持する。
- スキーマで設定を検証する。
- 構成ドキュメントや設定ドキュメントに秘密情報を含めない。
- 破棄後に何も残らないことをテストで確認する。