mountChannels() قنواتك كلها خلف معالج واحد من الشكل (req, res)، وتعالج كل
طلب بالطريقة نفسها:
WebhookTriggerAdapter يستخدم webhookChannel() في المصادقة
والتحليل. وتضيف القنوات ما تفتقر إليه المُشغِّلات: كل محادثة على الواجهة هي
جلسة، والموافقات والأسئلة تعود إلى الواجهة نفسها.
البدء السريع
httpChannel() هي القناة المرجعية: تستقبل { sessionKey, input } وتُخرج JSON.
true إذا خدم الطلب، وfalse (دون أن يكتب شيئًا) لأي مسار
آخر، مثل مسارات /chat في lousho dev، فيمكنك تركيبه إلى جانب مساراتك الخاصة.
العقد
defineChannel({ ... }) تتحقق من الاسم وتُرجع التعريف:
يتيح
ctx (ChannelContext) لـ parse أن يسأل المضيف بدل الاحتفاظ بحالة: ctx.approval(id) (الاستدعاء المعلّق)، وctx.sessionId(key) وctx.hasSession(key) (أي أن جلسة لهذا المفتاح محفوظة في store). يمكن أن يحمل { decision } الحقل inbound (المحادثة كما تسمّيها النقرة نفسها، فيبقى التوقف المؤقت قائمًا بعد إعادة التشغيل) والحقل approver (مَن اتخذ القرار؛ يُمرَّر إلى mountChannels({ onDecision }) لسجل التدقيق لديك).
req هو ChannelRequest مستقل عن أي إطار عمل: method وurl وheaders
(بأسماء بأحرف صغيرة) وrawBody (البايتات كما وصلت: تحقق من التواقيع عليها هي،
لا على JSON أُعيدت سَلسَلته) وtext وnative (طلب المضيف الأصلي).
الرد الذي لا يستدعي respond يتبعه 200 {"ok":true}؛ فالواجهة التي تسلّم
الردود خارج الطلب (بالنشر عبر واجهة برمجية لتطبيق محادثة) يكفيها الإقرار
باستلام الطلب. والخطأ الذي يرميه parse يُردّ عليه بـ 400 إن كان
SyntaxError (JSON غير صالح) وبـ 500 فيما عدا ذلك؛ والجسم الذي يتجاوز 1MB يُردّ عليه بـ 413.
الجلسات
الرسائل التي تحملsessionKey نفسه على القناة نفسها تتشارك جلسة واحدة، فيرى
الوكيل ما سبق من تبادلات؛ ودورات الجلسة الواحدة تُنفَّذ واحدة تلو الأخرى.
معرّفات الجلسات لا تقبل إلا A-Za-z0-9_-، فالمعرّف الذي يحوي محارف أخرى
(والافتراضي `${name}:${sessionKey}` يحوي : دائمًا) تُستبدل بها
_ ويُلحق به تجزئة (hash) للمعرّف الأصلي: sms:+1555 يصبح sms__1555-<hash>.
أرجِع معرّفًا صالحًا من sessionId() ليُستخدم كما هو.
تُحفظ سجلات المحادثات في mountChannels(agent, channels, { store }): وهو
SessionStore أو { sessions, checkpoints } مثل SqliteStore (مرّر
المخزن نفسه الذي أعطيته لـ createAgent({ store })). ومن دون store يحتفظ
بها المعالج في الذاكرة.
الموافقات والأسئلة
حين تتوقف دورة مؤقتًا عند أداة تتطلب موافقة، أو عند استدعاءask_question، يستدعي المعالج onApproval (والافتراضي هو
reply مع نص طلب مثل Approve send_email {"to":"sam@example.com"}? (approval id: ...)). ويُتخذ القرار بإحدى طريقتين؛ وتعود متابعة التشغيل عبر
reply الخاص بالقناة نفسها (أو onApproval من جديد إن توقفت مرة أخرى):
handler.resolveApproval({ id, approved, note? })أوhandler.resolveApproval({ id, answer })من شيفرتك، مثلًا من دالة رد النداء لزر في الواجهة.POST <basePath>/<name>/approvals/<id>مع{ approved, note? }أو{ answer }. يُنفَّذverifyالخاص بالقناة أولًا؛ والمعرّف الذي لم تتوقف عنده القناة يُردّ عليه بـ 404.
القنوات المضمّنة
webhookChannel({ secret }) تتحقق من توقيع HMAC-SHA256 للجسم الخام في
x-signature-256: sha256=<hex>؛ ويقبل auth أي
مصادقة webhook (خيارات HMAC مع
الحماية من إعادة الإرسال، أو رمز bearer، أو مصادقة مخصصة). والفحوص والرد 401
العام هي نفسها التي يستخدمها WebhookTriggerAdapter.
في مجلد الوكيل
يمكن لـ مجلد الوكيل أن يضع قنواته فيchannels/*.ts، قناة واحدة مُصدَّرة افتراضيًا (default export) في كل ملف
(وتُسمّى باسم الملف ما لم تحدد القناة اسمًا). تُرجعها resolveAgentDir() في
channels، ويركّبها خادم node عبر createDeployedServer(agent, { channels }).
من يحق له الموافقة (Slack وDiscord)
تقبل القناتان الخيارapprovers: قائمة بمعرّفات المستخدمين على المنصة، أو دالة
(user: { id, name?, roles? }, { toolName, input, sessionId }) => boolean | Promise<boolean>
(يملأ Discord الحقل roles بمعرّفات أدوار العضو). نقرة أي شخص آخر لا تحسم
الموافقة: تصله رسالة مؤقتة لا يراها غيره تفيد بأنه “غير مسموح له”، وتبقى
الموافقة معلّقة.
ملاحظة أمنية. من دون approvers، لا يحق الموافقة إلا للمستخدم الذي بدأ
الدورة (كاتب الرسالة على Slack، ومستخدم الأمر على Discord). في
الإصدارات السابقة كان ذلك متاحًا لكل من يرى الرسالة؛ والقيمة approvers: () => true
تعيد ذلك السلوك، ولا ينبغي أن تفعل ذلك إلا في قناة خاصة. وapprovers
بصيغة الدالة يرفض احتياطًا (fail closed) حين لا تعرف العملية الاستدعاء المعلّق
(بعد إعادة التشغيل)؛ فاستخدم صيغة القائمة أو السلوك الافتراضي للموافقات التي
يجب أن تبقى صالحة بعدها. أما إجابات ask_question فغير مقيَّدة: الرسالة التالية (Slack)
أو أمر /ask التالي (Discord) في المحادثة هو الإجابة. وهوية من اتخذ القرار تُمرَّر إلى
mountChannels(agent, channels, { onDecision({ approver, decision, sessionId, channel }) {} }).
الإخفاقات التي تقع بعد الإقرار بالاستلام تذهب إلى onError (خيار في القناة،
أو في mountChannels)؛ انظر العقد أعلاه.
Slack
slackChannel({ signingSecret, botToken, name?, fetch? }) تربط تطبيق Slack.
كل سلسلة رسائل (thread) في Slack هي جلسة واحدة: الإشارة إلى البوت تبدأ جلسة
سلسلتها (أو تتابعها)، ومفتاحها الفريق والقناة وقيمة ts لرسالة السلسلة الأولى؛
والرسائل اللاحقة في سلسلة لها جلسة من قبل تتابعها دون
إشارة (يُسأل مخزن الجلسات عن ذلك، فيبقى هذا السلوك قائمًا بعد إعادة التشغيل). والرسالة المباشرة
إلى البوت تبدأ جلسة مفتاحها قناة الرسائل المباشرة. تُنشر الردود في السلسلة عبر chat.postMessage (بـ
fetch وحدها، دون Slack SDK؛ مرّر fetch لحقن بديل عنها في الاختبارات).
- يُتحقق من توقيع كل طلب (HMAC-SHA256 بصيغة
v0عبر Web Crypto، مع نافذة حماية من إعادة الإرسال مدتها خمس دقائق)؛ والإخفاق يُردّ عليه بـ 401. - يُردّ على الطلب فورًا (
200، أو تحديurl_verification) وتُنفَّذ الدورة بعد الاستجابة، ضمن حد الثواني الثلاث الذي يفرضه Slack. - إعادات المحاولة (
X-Slack-Retry-Num)، ورسائل البوتات (ومنها رسائل البوت نفسه)، ونسخةmessageالمكررة من الإشارة: يُقرّ باستلامها كلها وتُتجاوز، فلا تُنفَّذ أي دورة مرتين. - تُنشر موافقة الأداة في السلسلة مع زرَّي Approve وDeny؛
والنقرة تستأنف الجلسة وتُنشر متابعة التشغيل في
السلسلة. تُستبدل بالرسالة التي نُقر زرها النتيجةُ (“Approved by @user”)،
فتختفي أزرارها. وتحمل النقرة قناتها وسلسلتها والمستخدم الذي
يحق له القرار، فتعمل بعد إعادة التشغيل. أما
ask_questionفيُنشر نصًا، والرسالة التالية في السلسلة هي الإجابة.
- OAuth & Permissions: نطاقات رمز البوت
app_mentions:readوchat:write، وchannels:history(معgroups:historyللقنوات الخاصة) لرسائل المتابعة دون إشارة، وim:historyللرسائل المباشرة. ثبّت التطبيق وانسخ رمز البوت (xoxb-...). - Event Subscriptions: عنوان الطلب (Request URL) هو
https://<host>/channels/slack؛ وأحداث البوتapp_mentionوmessage.channels(وmessage.groupsللقنوات الخاصة)، وmessage.imللرسائل المباشرة (وفعّل أيضًا Messages Tab في App Home ليتمكن المستخدمون من مراسلة البوت). - Interactivity & Shortcuts: عنوان الطلب (Request URL) هو
https://<host>/channels/slack(المسار نفسه). - Basic Information: انسخ سر التوقيع (signing secret).
store الذي تمرّره إلى mountChannels().
والموافقات المعلّقة تُحسم من النقرة نفسها. ولا يعيش في ذاكرة المعالج إلا
ask_question المعلّق (أي: أيُّ سلسلة تنتظر إجابة): إعادة
التشغيل تُنسيه إياه، فتكون الرسالة التالية دورة جديدة. وبعد إعادة التشغيل
تُنشر متابعة التشغيل، لكنها لا تُضاف إلى سجل محادثة الجلسة، لأن نسخة الوكيل
الجديدة ليست لديها جلسة مرتبطة بالموافقة.
Discord
discordChannel({ publicKey, applicationId, botToken?, name?, fetch? })
تربط تطبيق Discord عبر نقطة HTTP Interactions (دون اتصال gateway
ودون مكتبة Discord؛ Web Crypto وfetch فقط، فتعمل أيضًا على
Workers). الأمر المتوقع هو /ask prompt:<text>: خيار نصي واحد،
والخيار prompt (أو أول خيار نصي) هو الرسالة.
- يُتحقق في كل طلب من
X-Signature-Ed25519/X-Signature-Timestampعلىtimestamp + body؛ والتوقيع المفقود أو الخاطئ يُردّ عليه بـ 401، كما يشترط Discord. ويُردّ علىPINGبـPONG. - يُقرّ باستلام الأمر فورًا باستجابة مؤجَّلة (ضمن حد الثواني الثلاث الذي يفرضه
Discord)؛ ثم يعدّل الردُّ الاستجابةَ الأصلية
(
PATCH /webhooks/{applicationId}/{token}/messages/@original). والنص الذي يتجاوز 2000 محرف يُكمَل في رسائل متابعة. - الأوامر في القناة نفسها تتشارك جلسة واحدة، مفتاحها الخادم (guild) والقناة (والسلسلة، إن كانت في سلسلة).
- تُنشر موافقة الأداة مع زرَّي Approve وDeny (ولا يحق النقر عليهما إلا
لـ
approvers، وهو افتراضيًا مستخدم الأمر)؛ والنقرة تستأنف الجلسة وتأتي متابعة التشغيل في رسالة متابعة. وتحمل النقرة المحادثة، فتعمل بعد إعادة التشغيل. أماask_questionفيُنشر نصًا، وأمر/askالتالي في تلك القناة هو الإجابة. - رموز التفاعل (interaction tokens) تبقى صالحة 15 دقيقة، فالرد (أو النقرة) بعد ذلك
يفشل. و
botTokenمحجوز لاستدعاءات REST الخاصة بالبوت؛ والردود لا تحتاج إلا إلى رمز التفاعل.
- General Information: انسخ معرّف التطبيق والمفتاح العام.
- Interactions Endpoint URL:
https://<host>/channels/discord(يرسل Discord طلبPINGموقَّعًا عند الحفظ). - Installation: يكفي النطاق
applications.commands(أضفbotفقط إن أردت أيضًا وجود مستخدم البوت في الخادم). - سجّل الأمر مرة واحدة (أمر الخادم (guild) يظهر فورًا؛ واستخدم
/applications/{id}/commandsلأمر عام):
ask_question المعلّق (وإعادة التشغيل
تُنسيه إياه)؛ أما الموافقات المعلّقة فتُحسم من نقرة الزر ومن مخزن
الموافقات. وكما في Slack، متابعة التشغيل بعد إعادة التشغيل لا تُضاف إلى
سجل محادثة الجلسة.
ملفات channels/*.ts في مجلد الوكيل تُحمَّل
قنواتٍ هي أيضًا، ويركّبها خادم node. وما زال SlackTriggerAdapter وverifySlackSignature()
(انظر المُشغِّلات) يعملان للردود لمرة واحدة
عبر webhook وارد.