تأليف إضافات Cordis
يحمّل جسر Cordis شجرة إضافات تُسمّى صفوفها في cordis.patch.yml. تتطلب إضافة إمكانية كتابة إضافة Cordis وإضافة صف، لا تعديل مصادر Libre WebUI. تغطي هذه الصفحة الأعراف التي يعتمد عليها المضيف.
اقرأ أولًا جسر Cordis لفهم تركيب الشجرة، ثم تهيئة Cordis للتعرف على حقول الصفوف.
شكلا الإضافات
إضافة Cordis إما دالة أو كائن يملك دالة apply. يحل المحمّل كلا الشكلين.
// 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) {
// ...
}
يقرأ المحمّل نطاق أسماء الوحدة، لذلك تحتاج الإضافة المحمّلة من صف إلى تصدير 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عن حالة كل خدمة بدلًا من قيمة منطقية واحدة. - الحقن هو آلية الترتيب. لا ترتّب تشغيل الصفوف بنفسك؛ فترتيبها في التركيب المرفق لا يحدد ترتيب تحميلها.
للاعتماد الاختياري عند التأليف، استخدم ctx.inject داخل apply بدلًا من ذلك. يشغّل دالة الاستدعاء فورًا إذا كانت الخدمات موجودة، ثم كلما ظهرت:
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;
فضّل صنفًا فرعيًا من Service على ctx.reflect.provide(...) لكل ما يملك سلوكًا: يسجّل الصنف الاسم مرة واحدة، ويعرض دوال ذات أنواع، ويُسحب تلقائيًا مع الليفة التابعة له.
ملكية الآثار الجانبية
يجب أن يملك كل أثر جانبي جهة مسؤولة عنه. هذه القاعدة الواحدة تجعل ضمان التراجع في الجسر صحيحًا، وهي أسهل قاعدة يمكن انتهاكها.
| الأثر الجانبي | الجهة المالكة |
|---|---|
| تسجيل خدمة | ليفة الإضافة تلقائيًا |
| مستمع حدث | ctx.on(...) داخل الإضافة |
| مورد يحتاج إلى تنظيف | ctx.effect(() => disposer) |
| مؤقت | ctx.setTimeout / ctx.setInterval |
| تسجيل أداة | إعادة دالة التنظيف من ctx.effect |
يقبل ctx.effect دالة تعيد دالة تنظيف، أو مولّدًا ينتج دوال تنظيف:
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 في وثيقة التركيب. يُقيّم التعبير ضمن ليفة الصف المالك مع إتاحة سياق المحمّل: يعمل 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بالاسم. - الخدمة صنف فرعي من
Serviceباسمprovideثابت. - يعيد كل تسجيل دالة تنظيف، وتُعاد كل دالة تنظيف من
ctx.effect. - تأتي المؤقتات من
ctxلا من الدوال العامة. - توجد الحالة القابلة للتغيير على النسخة، لا في كائن وحيد على مستوى الوحدة.
- تُتحقق التهيئة باستخدام مخطط.
- لا يظهر أي سر في وثيقة التركيب أو الإعدادات.
- يتحقق اختبار من أن الإضافة لا تترك شيئًا بعد التنظيف.