DeepSeek Harness: نظام الأحداث

آخر تحديث: 2026-08-31

الأحداث هي آلية Cordis الأساسية للتواصل слаб الاقتران بين الإضافات — الإضافات لا تستدعي بعضها مباشرة، بل تتعاون عبر بث الأحداث والاشتراك. أربعة أنماط أحداث تغطي جميع السيناريوهات من الإشعارات البسيطة إلى خطوط الأنابيب المعقدة.

💡 نصيحة: السؤال الأساسي لاختيار نمط حدث هو "هل تحتاج اعتراض؟" — إشعار بحت يستخدم emit، قابل للمقاطعة يستخدم bail، معالجة متسلسلة يستخدم serial، تمرير متسلسل يستخدم waterfall.

📋 المتطلبات المسبقة: أكمل 14-inject.md، تفهم حقن الاعتماديات

1. ما ستتعلمه

أوضاع إرسال الأحداث


2. emit: بث أحداث عام

(1) الاستخدام الأساسي

emit هو أبسط نمط حدث — بث إشعار؛ جميع المستمعين يتلقونه، قيم الإرجاع تُتجاهل:

TYPESCRIPT
// إصدار حدث
ctx.emit('session/created', { id: 'abc-123', user: 'Alice' })

// الاستماع للحدث
ctx.on('session/created', (data) => {
  ctx.logger.info(`new session: ${data.id}`)
})

(2) الخصائص

الخاصية الوصف
بث جميع المستمعين يُستدعون
بدون قيمة إرجاع قيم إرجاع المستمعين تُتجاهل
بدون مقاطعة المستمعون لا يمكنهم منع تنفيذ المستمعين اللاحقين
متسلسل المستمعون يُنفذون بترتيب التسجيل

(3) السيناريوهات النموذجية

(4) ▶ مثال 4

TYPESCRIPT
ctx.on('session/created', (data) => {
  ctx.logger.info(`logger: ${data.id}`)
})

ctx.on('session/created', (data) => {
  ctx.metrics.inc('session_count')
})

ctx.on('session/created', (data) => {
  ctx.cache.set(`session:${data.id}`, data)
})

ctx.emit('session/created', { id: 'abc', user: 'Alice' })
// جميع المستمعين الثلاثة يُنفذون

3. bail: أحداث قابلة للمقاطعة

(1) الاستخدام الأساسي

bail حدث قابل للمقاطعة — عندما يعيد مستمع قيمة غير undefined، المستمعون اللاحقون لا يُنفذون:

TYPESCRIPT
// المستمع يمكنه "اعتراض" الحدث
ctx.on('tool/beforeExecute', (data) => {
  if (data.tool === 'shell' && data.params.command.includes('rm')) {
    return { denied: true, reason: 'dangerous command' }
  }
})

ctx.bail('tool/beforeExecute', { tool: 'shell', params: { command: 'rm -rf /' } })
// → يُعيد { denied: true, reason: 'dangerous command' }
// المستمعون اللاحقون لا يُنفذون

(2) الخصائص

الخاصية الوصف
قابل للمقاطعة إعادة قيمة غير undefined تقطع الانتشار
دائرة قصيرة أول مستمع يعيد قيمة يُنهي الانتشار
له قيمة إرجاع استدعاء bail يُعيد القيمة المُعترضة
حساس للترتيب المستمعون المُسجّلون أسبق يعترضون أولاً

(3) السيناريوهات النموذجية

(4) ▶ مثال 4

TYPESCRIPT
// إضافة سياسة الموافقة
ctx.on('tool/beforeExecute', (data) => {
  const policy = getApprovalPolicy(data.tool)
  if (policy === 'deny') {
    return { denied: true, reason: `${data.tool} is denied by policy` }
  }
  if (policy === 'ask') {
    return { pending: true, requiresApproval: true }
  }
  // إعادة undefined → لا تعترض، استمر في الانتشار
})

// إضافة Sandbox (بعد الموافقة)
ctx.on('tool/beforeExecute', (data) => {
  if (!isInSandbox(data.params.cwd)) {
    return { denied: true, reason: 'execution outside sandbox' }
  }
})

(5) ▶ مثال 5

TYPESCRIPT
const result = ctx.bail('tool/beforeExecute', data)
if (result) {
  // عُترض
  ctx.logger.warn('tool execution denied:', result.reason)
} else {
  // لم يُعترض، يمكن التنفيذ
  await executeTool(data)
}

4. serial: أحداث متسلسلة

(1) الاستخدام الأساسي

serial يُنفذ المستمعين غير المتزامنين بالترتيب، كل واحد ينتظر اكتمال السابق:

TYPESCRIPT
ctx.on('session/initialized', async (data) => {
  await loadUserPreferences(data.userId)
})

ctx.on('session/initialized', async (data) => {
  await setupWorkspace(data.workspaceId)
})

ctx.on('session/initialized', async (data) => {
  await warmCache(data.projectPath)
})

// ثلاثة مستمعين يُنفذون بالتسلسل
await ctx.serial('session/initialized', { userId: 'alice', workspaceId: 'ws-1' })

(2) الخصائص

الخاصية الوصف
متسلسل المستمعون يُنفذون واحداً تلو الآخر بالترتيب
غير متزامن يدعم مستمعين غير متزامنين
ينتظر الاكتمال استدعاء serial ينتظر اكتمال جميع المستمعين
بدون مقاطعة المستمعون لا يمكنهم منع التنفيذ اللاحق

(3) السيناريوهات النموذجية

(4) الفرق عن emit

TYPESCRIPT
// emit: متوازي (لا ينتظر)
ctx.emit('session/created', data)   // لا ينتظر اكتمال المستمعين

// serial: متسلسل (ينتظر)
await ctx.serial('session/created', data)  // ينتظر اكتمال جميع المستمعين

5. waterfall: أحداث تمرير متسلسل

(1) الاستخدام الأساسي

waterfall يمرر قيمة إرجاع المستمع السابق للتالي، مُشكّلاً سلسلة:

TYPESCRIPT
ctx.on('message/format', async (data, next) => {
  data.text = data.text.trim()
  return next(data)
})

ctx.on('message/format', async (data, next) => {
  data.text = data.text.replace(/\s+/g, ' ')
  return next(data)
})

ctx.on('message/format', async (data, next) => {
  data.text = data.text.substring(0, 4096)
  return next(data)
})

const result = await ctx.waterfall('message/format', { text: '  hello   world  ' })
// result.text === 'hello world' (trim → collapse → truncate)

(2) الخصائص

الخاصية الوصف
تمرير متسلسل مخرج السابق هو مدخل التالي
استدعاء next() المستمعون يجب أن يستدعوا next() للتمرير للتالي
قابل للتعديل كل مستمع يمكنه تعديل البيانات
قابل للمقاطعة عدم استدعاء next() يقطع السلسلة

(3) دالة next()

كل مستمع waterfall يستقبل مُعامل next:

TYPESCRIPT
ctx.on('event/name', async (data, next) => {
  // تعديل البيانات
  data.field = newValue
  
  // استدعاء next للتمرير للمستمع التالي
  return next(data)
  
  // عدم استدعاء next → السلسلة مُقطعة، البيانات لا تُمرر أبعد
})

(4) السيناريوهات النموذجية

(5) مقاطعة السلسلة

TYPESCRIPT
ctx.on('request/process', async (data, next) => {
  if (!data.authenticated) {
    return { error: 'unauthenticated' }  // لا تستدعي next، السلسلة مُقطعة
  }
  return next(data)
})

6. نطاقات الأحداث

(1) تقسيم النطاقات

أحداث Cordis مُقسّمة حسب النطاق، مفصولة بـ /:

TEXT 📖 للعرض فقط
session/created        → نطاق الجلسة
session/destroyed      → نطاق الجلسة
tool/beforeExecute     → نطاق الأدوات (نطاق فرعي للقدرات)
tool/afterExecute      → نطاق الأدوات
agent/initialized      → نطاق الوكيل
llm/request            → نطاق LLM

(2) نطاقات الأحداث الأساسية

النطاق البادئة أحداث نموذجية
session session/ created, destroyed, forked
agent agent/ initialized, stopped, error
tool tool/ beforeExecute, afterExecute, error
llm llm/ request, response, stream, error
fiber fiber/ created, active, disposing, disposed, errored
config config/ updated, validated

(3) غرض النطاقات

نطاقات الأحداث ليست سكراً نحوياً — الإطار يُحسّن بناءً على النطاقات:

(4) الاشتراك في نطاق مُحدد

TYPESCRIPT
// اشترك في جميع أحداث نطاق الأدوات
ctx.on('tool/*', (eventName, data) => {
  ctx.logger.info(`tool event: ${eventName}`)
})

7. أحداث مخصصة وأمان الأنواع

(1) التصريح بأحداث مخصصة

TYPESCRIPT
// events.ts
interface MyPluginEvents {
  'my-plugin/data-loaded': { source: string; count: number }
  'my-plugin/data-error': { source: string; error: Error }
}

declare module '@deepseek-ai/cordis' {
  interface Events extends MyPluginEvents {}
}

(2) إصدار أحداث آمن الأنواع

TYPESCRIPT
ctx.emit('my-plugin/data-loaded', { source: 'api', count: 42 })  // ✅ نوع صحيح
ctx.emit('my-plugin/data-loaded', { wrong: true })                // ❌ خطأ نوع

(3) استماع آمن الأنواع

TYPESCRIPT
ctx.on('my-plugin/data-loaded', (data) => {
  // data يُستنتج تلقائياً كـ { source: string; count: number }
  ctx.logger.info(`loaded ${data.count} items from ${data.source}`)
})

(4) نمط تعريف أنواع الأحداث

TYPESCRIPT
// أحداث داخلية للإضافة
interface InternalEvents {
  'cache/hit': { key: string; age: number }
  'cache/miss': { key: string }
  'cache/evicted': { key: string; reason: string }
}

// توسيع واجهة Events العامة
declare module '@deepseek-ai/cordis' {
  interface Events extends InternalEvents {}
}

// تصدير لاستخدامه بإضافات أخرى
export type CacheEvents = InternalEvents

(5) اصطلاحات تسمية الأحداث

TEXT 📖 للعرض فقط
{نطاق}/{فعل-ماضي}    ✅ session/created
{نطاق}/{فعل-مضارع}    ✅ tool/execute (قيد التنفيذ)
{نطاق}/before{فعل}    ✅ tool/beforeExecute (خطاف قبلي)
{نطاق}/after{فعل}     ✅ tool/afterExecute (خطاف بعدي)
{نطاق}/{اسم-حالة}     ✅ fiber/errored (حالة)

❓ أسئلة شائعة

س هل يمكن لـ emit و bail مشاركة نفس اسم الحدث؟
ج غير مُوصى به. رغم الإمكانية تقنياً، خلط مستمعي emit و bail تحت نفس الاسم يُسبب لبساً. استخدم أسماء أحداث مختلفة.
س ماذا يحدث إذا نسيت استدعاء next() في waterfall؟
ج السلسلة تُقطع؛ المستمعون اللاحقون لا يُنفذون. استدعاء waterfall يُعيد البيانات الحالية. قد يكون هذا مقصوداً (مقاطعة شرطية) أو خللاً (نسيان الاستدعاء).
س هل يمكن تسجيل مستمعي أحداث عدة مرات؟
ج نعم. نفس دالة المستمع المُسجّلة عدة مرات ستُستدعى عدة مرات. استخدم ctx.off() للإزالة — يتطلب نفس مرجع الدالة.
س هل يمكن التحكم بترتيب تنفيذ مستمعي الأحداث؟
ج الافتراضي هو ترتيب التسجيل. بعض الأطر تدعم مُعاملات أولوية، لكن Cordis حالياً يستخدم ترتيب FIFO.
س هل يُنتظر مستمعون غير متزامنين في emit؟
ج لا. emit لا ينتظر اكتمال المستمعين غير المتزامنين. استخدم serial إذا احتجت الانتظار.
س كيف أعرض جميع مستمعي الأحداث المُسجّلين؟
ج typescript ctx.logger.info('listeners:', ctx.listenerCount('tool/beforeExecute'))

📖 ملخص


📝 تمارين

1. ⭐ أساسي: اكتب إضافة تبث حدث my-plugin/loaded عبر emit، واستمع له في إضافة أخرى مع إخراج سجل.

2. ⭐⭐ متوسط: نفّذ معترض موافقة باستخدام bail على tool/beforeExecute لاعتراض أوامر shell التي تحتوي rm. الاختبار: ls يُنفذ بشكل طبيعي، rm -rf / يُعترض.

3. ⭐⭐⭐ تحدٍ: استخدم waterfall لتنفيذ خط أنابيب معالجة رسائل: trim → إزالة كلمات حساسة → اقتطاع نص مُفرط. كل خطوة مستمع مستقل؛ الخطوات الوسيطة يمكنها تعديل البيانات، والخطوة الأخيرة تُعيد النتيجة النهائية. اكتب اختبارات للتحقق من سلوك خط الأنابيب.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%