Cordis 플러그인 작성
Cordis 브리지는 cordis.patch.yml의 행에 지정된 플러그인 트리를 마운트합니다. 기능을 추가하려면 Cordis 플러그인을 작성하고 행을 추가하면 됩니다. Libre WebUI 소스를 수정할 필요는 없습니다. 이 문서는 호스트가 전제로 삼는 규칙을 설명합니다.
먼저 Cordis 브리지에서 트리 구성을 살펴보고, 행의 필드는 Cordis 설정을 참조하세요.
두 가지 플러그인 형태
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.
}
다음 두 가지 결과를 알아두세요.
- 서비스가 없어도 오류가 아닙니다. 해당 행은 별도 알림 없이 계속 대기합니다. 따라서
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은 평가하지 않으므로 행의 모듈 지정자는 문자열 리터럴이어야 합니다.
완전한 예제
저장소에는 브리지 테스트에서 사용하는 실행 가능한 픽스처 두 개가 포함되어 있습니다. 둘 다 복사해서 활용하기에 충분히 작습니다.
모델 어댑터: 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가 이름 있는 내보내기입니다. - 서비스가 안정적인
provide이름을 가진Service하위 클래스입니다. - 모든 등록이 정리 함수를 반환하며, 모든 정리 함수는
ctx.effect에서 반환됩니다. - 타이머는 전역 함수가 아니라
ctx에서 생성합니다. - 변경 가능한 상태는 모듈 싱글턴이 아닌 인스턴스에 둡니다.
- 스키마로 설정을 검증합니다.
- 구성 문서나 설정 문서에 비밀 정보를 넣지 않습니다.
- 해제 후 아무것도 남지 않는지 테스트로 검증합니다.