Viết plugin Cordis
Cầu nối Cordis nạp cây plugin với các dòng được khai báo trong cordis.patch.yml. Thêm khả năng nghĩa là viết plugin Cordis và thêm dòng, không sửa nguồn Libre WebUI. Trang này mô tả quy ước host dựa vào.
Đọc Cầu nối Cordis trước để hiểu cách ghép cây, rồi Cấu hình Cordis để biết các trường của dòng.
Hai dạng plugin
Plugin Cordis là một hàm hoặc một đối tượng có phương thức apply. Loader phân giải cả hai dạng.
// 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 đọc namespace của mô-đun, nên plugin được nạp từ dòng cần xuất apply và inject theo tên. Xuất mặc định cũng hoạt động, nhưng plugin DSH đi kèm dùng dạng có tên; hãy ưu tiên để nhất quán.
Khai báo phụ thuộc
inject là toàn bộ cơ chế phụ thuộc. Cordis chỉ chạy plugin khi mọi dịch vụ được nêu tồn tại, và chạy lại nếu một dịch vụ bị thu hồi rồi khôi phục.
export const inject = ['tools', 'systemPrompt'];
export function apply(ctx, config) {
// `ctx.tools` and `ctx.systemPrompt` are guaranteed present here.
}
Hai hệ quả quan trọng:
- Dịch vụ thiếu không phải lỗi. Dòng âm thầm chờ vô hạn. Vì vậy
GET /api/cordis/healthbáo trạng thái từng dịch vụ thay vì một boolean duy nhất. - Injection quyết định thứ tự. Bạn không tự sắp lịch các dòng; thứ tự trong cấu trúc đi kèm không quyết định việc nạp.
Với phụ thuộc tùy chọn lúc viết plugin, dùng ctx.inject trong apply. Callback chạy ngay nếu dịch vụ đã tồn tại, rồi chạy lại mỗi khi chúng xuất hiện:
export function apply(ctx) {
ctx.inject(['typert'], inner => {
inner.typert.lookups.register('session', {/* ... */});
});
}
ctx.inject chạy đồng bộ khi phụ thuộc đã có, nên plugin nạp muộn vẫn đăng ký trong apply, không đợi tick sau.
Công bố dịch vụ
Kế thừa Service và truyền tên dịch vụ cho super. Tên là thuộc tính bên sử dụng đọc; đăng ký thuộc fiber của 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;
Ưu tiên lớp con Service thay ctx.reflect.provide(...) cho mọi thành phần có hành vi. Lớp con đăng ký tên một lần, cung cấp phương thức có kiểu và tự thu hồi cùng fiber.
Sở hữu hiệu ứng phụ
Mọi hiệu ứng phụ phải có chủ sở hữu. Đây là quy tắc duy nhất làm bảo đảm hoàn tác của cầu nối đúng, cũng là quy tắc dễ vi phạm nhất.
| Hiệu ứng phụ | Chủ sở hữu |
|---|---|
| Đăng ký dịch vụ | Fiber plugin tự động sở hữu |
| Trình nghe sự kiện | ctx.on(...) trong plugin |
| Tài nguyên cần dọn | ctx.effect(() => disposer) |
| Timer | ctx.setTimeout / ctx.setInterval |
| Đăng ký công cụ | Trả hàm giải phóng từ ctx.effect |
ctx.effect nhận hàm trả về hàm giải phóng, hoặc generator sinh các hàm giải phóng:
ctx.effect(() => {
const registration = ctx.llm.registerAdapter(['my-route'], adapter);
return () => registration();
}, 'my-adapter.register');
Nhãn dùng để chẩn đoán, không phải trang trí: nó xác định hiệu ứng khi dọn dẹp thất bại.
Sử dụng tài nguyên Cordis không biết, chẳng hạn socket, worker hoặc handle, nghĩa là bạn phải tự giải phóng:
ctx.effect(() => {
const worker = startWorker();
return () => worker.terminate();
}, 'my-plugin.worker');
Điều gì làm hỏng hoàn tác
- Đăng ký trên ngữ cảnh khác với ngữ cảnh nhận trong
apply. Khi đó dịch vụ sống lâu hơn dòng của nó. - Tạo timer bằng
setTimeouttoàn cục. Nó giữ tiến trình hoạt động và không bao giờ bị hủy. - Đăng ký emitter bên ngoài mà không hủy đăng ký trong hàm giải phóng.
- Ghi vào singleton cấp mô-đun. Giải phóng không hoàn tác được, nên giá trị vẫn hiện sau khi dòng bị xóa. Hãy dùng trạng thái riêng theo fiber.
Cấu hình
Cấu hình plugin là ánh xạ config của dòng. Khai báo schema để lỗi gõ sai làm nạp thất bại thay vì âm thầm dùng mặc định:
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.
}
Giá trị cấu hình có thể dùng !!js trong tài liệu thành phần. Biểu thức được đánh giá trong fiber sở hữu dòng với ngữ cảnh Loader khả dụng. process.env và ctx.get(...) dùng được, import.meta không. name không bao giờ được đánh giá, nên định danh mô-đun phải là chuỗi literal.
Ví dụ hoàn chỉnh
Repository cung cấp hai fixture thực tế dùng trong bộ kiểm thử cầu nối. Cả hai đủ nhỏ để sao chép.
Adapter mô hình: scripts/fixtures/cordis/fake-adapter.mjs đăng ký route nhà cung cấp trên ctx.llm, trả hàm giải phóng đăng ký từ một hiệu ứng và trả văn bản cố định cho mỗi yêu cầu. Đó là toàn bộ hợp đồng: khai báo inject, đăng ký và sở hữu đăng ký.
Bộ dò vòng đời: scripts/fixtures/cordis/lifecycle-probe.mjs công bố dịch vụ, đăng ký sự kiện và ghi lại quá trình dọn dẹp. Kiểm thử hoàn tác nhờ đó xác minh cả hai thực sự biến mất.
Nạp một trong hai bằng cách thêm dòng:
- id: my-adapter
name: './scripts/fixtures/cordis/fake-adapter.mjs'
config:
route: my-route
Định danh tương đối phân giải từ thư mục tài liệu thành phần; tên gói phân giải qua gói backend.
Kiểm thử plugin
Các bộ kiểm thử cầu nối thể hiện cách làm:
scripts/test-cordis-bridge.mjsnạp cây thật trong thư mục tạm, kiểm tra dịch vụ, thử hợp đồng và xác minh giải phóng đã thu hồi dịch vụ cùng trình lắng nghe.scripts/test-cordis-bridge-http.mjskiểm thử route qua server HTTP thật, kể cả truyền NDJSON.
Để thử không có nhà cung cấp, đặt model.provider: none rồi nạp plugin cùng các dòng bộ máy. Với plugin phụ thuộc llm, nạp fixture adapter trên route không adapter thật nào chiếm và đặt model.provider: none để host không nạp adapter cạnh tranh.
Plugin chỉ đúng nếu xóa dòng không để lại dấu vết. Hãy kiểm tra trình tự: đăng ký, quan sát, giải phóng, quan sát lại.
Danh sách kiểm tra
-
applyvàinjectđược xuất theo tên. - Dịch vụ là lớp con
Servicevới tênprovideổn định. - Mọi đăng ký trả hàm giải phóng và hàm này được trả từ
ctx.effect. - Timer đến từ
ctx, không từ hàm toàn cục. - Trạng thái thay đổi nằm trên instance, không trong singleton mô-đun.
- Schema kiểm tra cấu hình.
- Không có bí mật trong tài liệu thành phần hoặc cài đặt.
- Kiểm thử xác nhận plugin không để lại gì sau giải phóng.