メインコンテンツまでスキップ

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.envctx.get(...) は使えますが、import.meta は使えません。name は評価されないため、行のモジュール指定子には文字列リテラルが必要です。

完全な例

リポジトリには、ブリッジのテストスイートで使用する 2 つの動作するフィクスチャがあります。どちらも小さく、コピーして利用できます。

モデルアダプターscripts/fixtures/cordis/fake-adapter.mjsctx.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 を設定して、ホストが競合するアダプターを同じルートにマウントしないようにします。

行を削除した後に何も残らないことが、正しいプラグインの条件です。登録、観測、破棄、再観測の一連の流れを検証してください。

チェックリスト

  • applyinject が名前付きエクスポートになっている。
  • サービスが Service のサブクラスで、安定した provide 名を持つ。
  • すべての登録が破棄関数を返し、その関数が ctx.effect から返される。
  • タイマーはグローバル関数ではなく ctx から作成する。
  • 可変状態はモジュールのシングルトンではなくインスタンスで保持する。
  • スキーマで設定を検証する。
  • 構成ドキュメントや設定ドキュメントに秘密情報を含めない。
  • 破棄後に何も残らないことをテストで確認する。