Messages and delivery

إعادة هيكلة دورة حياة الرسالة

لماذا حدثت إعادة الهيكلة هذه

نشأت حزمة القنوات من عدة إصلاحات محلية: مساعدات واردة منفصلة لكل مستوى نضج (runtime.channel.inbound.run للمهايئات البسيطة، وruntime.channel.inbound.runPreparedReply للمهايئات الغنية)، ومساعدات قديمة لتوزيع الردود (dispatchInboundReplyWithBase وrecordInboundSessionAndDispatchReply)، وبث معاينة خاص بكل قناة، ومتانة التسليم النهائي المضافة إلى مسارات حمولات الرد الموجودة. نتج عن هذا الشكل عدد مفرط من المفاهيم العامة ومواضع كثيرة جدًا يمكن أن تنحرف فيها دلالات التسليم.

فجوة الموثوقية التي فرضت إعادة التصميم:

text
تم الإقرار بتحديث استطلاع Telegram  -> النص النهائي للمساعد موجود  -> أُعيد تشغيل العملية قبل نجاح sendMessage  -> فُقد الرد النهائي

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

ما أُطلق

يوجد النطاق الداخلي في src/channels/message/*:

الملف ما يتولاه
types.ts عقود أنواع المهايئ وسياق الإرسال والإيصال والنية المتينة
send.ts withDurableMessageSendContext / sendDurableMessageBatch — سياق الإرسال المتين
receive.ts createMessageReceiveContext — آلة حالات سياسة الإقرار للوارد
live.ts حالة المعاينة المباشرة ومنطق الإنهاء في الموضع أو الرجوع إلى البديل
state.ts classifyDurableSendRecoveryState — تصنيف الاسترداد بعد الانقطاع
receipt.ts يوحّد نتائج إرسال المنصة في MessageReceipt
capabilities.ts يستنتج إمكانات الإتمام المتين المطلوبة من الحمولة
contracts.ts التحقق من إثبات العقود لإمكانات المهايئ المعلنة
adapter.ts defineChannelMessageAdapter
outbound-bridge.ts createChannelMessageAdapterFromOutbound — يغلّف دوال sendText/sendMedia/sendPayload/sendPoll القديمة
ingress-queue.ts createChannelIngressQueue — طابور أحداث واردة متين
durable-receive.ts createDurableInboundReceiveJournal — سجل القبول/الانتظار/الإكمال/التحرير لإزالة تكرار الوارد
inbound-reply-dispatch.ts dispatchChannelInboundReply والأغلفة ذات الأسماء القديمة
reply-pipeline.ts createChannelReplyPipeline ومساعدات بادئة الرد واستدعاء الكتابة

الواجهة العامة: openclaw/plugin-sdk/channel-outbound (مساعدات الإرسال/الإيصال/المتانة/البث المباشر/مسار الرد) وopenclaw/plugin-sdk/channel-inbound (سياق الوارد وrunChannelInboundEvent وdispatchChannelInboundReply). راجع هاتين الصفحتين للاطلاع على أمثلة المهايئات وأسماء الأنواع الحالية وملاحظات الترحيل — فهما مصدر الحقيقة لشكل API، وليس التصورات أدناه.

سياق الإرسال

تمنح withDurableMessageSendContext شيفرة القناة خطوات render وpreviewUpdate وsend وedit وdelete وcommit وfail حول رسالة صادرة واحدة. أما sendDurableMessageBatch فهو غلاف الحالة الشائعة: التصيير ثم الإرسال، ثم التثبيت عند sent/suppressed أو تسجيل الفشل عند حدوث خطأ.

تعيد sendDurableMessageBatch نتيجة مميّزة واحدة:

الحالة المعنى
sent تم تسليم رسالة مرئية واحدة على الأقل إلى المنصة
suppressed لا ينبغي اعتبار أي رسالة على المنصة مفقودة (ألغيت بخطاف، أو تشغيل تجريبي، إلخ)
partial_failed تم تسليم رسالة واحدة على الأقل قبل فشل حمولة لاحقة أو أثر جانبي
failed لم يُنتج أي إيصال من المنصة

تكون المتانة واحدة من required أوbest_effort أو disabled (MessageDurabilityPolicy في src/channels/message/types.ts). يفشل required بشكل مغلق عندما يتعذر تدوين النية المتينة؛ ويتابع best_effort إلى إرسال مباشر عندما لا يتوفر التخزين المستمر؛ بينما يحتفظ disabled بسلوك الإرسال المباشر السابق لإعادة الهيكلة. تستخدم مساعدات التوافق القديمة القيمة disabled افتراضيًا ولا تستنتج required لمجرد أن القناة تحتوي على مهايئ صادر عام.

الحد الذي يظل خطيرًا: بعد نجاح استدعاء المنصة وقبل تثبيت الإيصال. إذا توقفت العملية في تلك اللحظة، فلن تتمكن النواة من معرفة ما إذا كانت رسالة المنصة موجودة ما لم يعلن المهايئ reconcileUnknownSend. يصنّف ذلك الخطاف إرسالًا منقطعًا على أنه sent أو not_sent أو unresolved؛ ولا يسمح بإعادة التشغيل إلا not_sent. ترجع القنوات التي لا تدعم المطابقة إلى حالة unknown_after_send (src/channels/message/state.ts وsrc/infra/outbound/delivery-queue-recovery.ts)، وقد تختار إعادة التشغيل لمرة واحدة على الأقل فقط إذا كانت الرسائل المرئية المكررة مقايضة مقبولة وموثقة لتلك القناة.

سياق الاستقبال

يتتبّع createMessageReceiveContext حالة الإقرار/الرفض لكل حدث وارد باستخدام ack() آمنة عند التكرار وnack(error) صريحة. تكون سياسة الإقرار (ChannelMessageReceiveAckPolicy) واحدة من:

السياسة وقت الإقرار
after_receive_record خزّنت النواة ما يكفي من بيانات الوارد الوصفية لإزالة تكرار إعادة التسليم أو توجيهها
after_agent_dispatch تم توزيع تشغيل الوكيل
after_durable_send تم تثبيت الإرسال الصادر المتين لهذه الدورة
manual يتحكم المستدعي صراحةً في توقيت الإقرار (وهو الافتراضي للمهايئات التي لا تعلن سياسة)

يستخدم استطلاع Telegram هذا لتخزين علامة مائية آمنة للتحديثات المكتملة (safeCompletedUpdateId في extensions/telegram/src/bot-update-tracker.ts): يواصل grammY ملاحظة كل تحديث عند دخوله سلسلة البرامج الوسيطة، لكن OpenClaw لا يقدّم علامة إعادة التشغيل المخزنة بعد التحديثات إلا إذا أنهت التوزيع، ولذلك تُعاد التحديثات الفاشلة أو التي لا تزال معلقة بعد إعادة التشغيل. ولا يزال إزاحة getUpdates الأصلية في Telegram مملوكة لـ grammY؛ ولم يُبنَ مصدر استطلاع متين بالكامل يتحكم في إعادة التسليم على مستوى المنصة بعد هذه العلامة المائية (راجع الأسئلة المفتوحة).

المعاينة المباشرة

يصوغ src/channels/message/live.ts المعاينة/التحرير/الإنهاء كدورة حياة واحدة: createLiveMessageState وmarkLiveMessagePreviewUpdated وmarkLiveMessageFinalized وmarkLiveMessageCancelled و deliverFinalizableLivePreviewAdapter (إنشاء تحرير نهائي من مسودة، وتطبيقه، والرجوع إلى إرسال عادي عندما يتعذر التحرير أو يفشل). تكون LiveMessageState.phase واحدة من idle | previewing | finalizing | finalized | cancelled؛ وتحدد canFinalizeInPlace ما إذا كان يمكن أن تصبح المعاينة الرسالة النهائية عبر التحرير بدلًا من إرسال جديد.

الإيصالات المتينة

يوحّد MessageReceipt (src/channels/message/types.ts) معرّف رسالة واحدًا أو أكثر من المنصة ناتجًا من إرسال منطقي واحد في platformMessageIds بالإضافة إلى parts لكل جزء (النوع، والفهرس، ومعرّف سلسلة المحادثة، ومعرّف الرسالة المُجاب عنها). ويُحتفظ بمعرّف أساسي لإنشاء سلسلة المحادثة والتحريرات اللاحقة. وهذا ما يجعل عمليات التسليم متعددة الأجزاء (النص مع الوسائط، والنص المجزأ، والرجوع إلى البطاقة) قابلة لإعادة التشغيل وإزالة التكرار بعد إعادة التشغيل.

تقليص SDK العام

استوعبت إعادة الهيكلة أو أوقفت استخدام: مساعدات reply-runtime وreply-dispatch-runtime وreply-reference وreply-chunking وreply-payload المكشوفة بوصفها API عامًا، وinbound-reply-dispatch وchannel-reply-pipeline ومعظم الاستخدامات العامة لـoutbound-runtime. أصبح src/plugin-sdk/channel-message.ts الآن حاوية إعادة تصدير @deprecated تشير إلى channel-outbound / channel-inbound؛ وأزيلت الأسماء المستعارة لوقت تشغيل channel.turn، وأصبحت صفحة التوثيق القديمة /plugins/sdk-channel-turn تعيد التوجيه إلى API الوارد للقنوات. ينبغي لشيفرة Plugin الجديدة استهداف channel-outbound وchannel-inbound مباشرةً.

مواضع اختلاف التنفيذ عن التصميم الأصلي

لم يُطلق تصور التصميم أدناه حرفيًا كما هو موصوف. يُحتفظ بالسجل من أجل الدقة التاريخية؛ فلا تتعامل مع أسماء الأنواع هذه على أنها API حالي.

  • لا يوجد MessageOrigin / shouldDropOpenClawEcho. دعت الخطة الأصلية إلى وسم أصل source: "openclaw" على رسائل فشل Gateway بالإضافة إلى مسند مشترك يتجاهل أصداء الروبوت الموسومة والمؤلفة بواسطة الروبوت في الغرف المشتركة قبل تفويض allowBots. لا يوجد ذلك النوع ولا ذلك المسند في قاعدة الشيفرة. أما allowBots نفسه فهو مفتاح إعداد حقيقي لكل قناة (Slack وDiscord وGoogle Chat وغيرها)، لكن آلية وسم الأصل التي كان يُفترض أن تحميه لم تُبنَ قط. ولا يزال منع صدى فشل Gateway في الغرف التي تتيح الروبوتات فجوة مفتوحة، وليس ضمانًا مُطلقًا.
  • لا توجد مساحة أسماء موحدة core.messages.receive/send/live/state. توجد الدوال المُطلقة مباشرةً في src/channels/message/* (withDurableMessageSendContext وcreateMessageReceiveContext وcreateLiveMessageState وclassifyDurableSendRecoveryState) بدلًا من وجودها خلف واجهة core.messages.*.
  • لا يوجد نوع رسائل موحّد عام ChannelMessage / MessageTarget / MessageRelation. لا تزال النواة تمرر حمولات رد ملموسة (ReplyPayload) وسياقات خاصة بالقنوات عبر مهايئات الإرسال بدلًا من شكل رسالة واحد محايد للمنصة مع علاقة kind: "reply" | "followup" | "broadcast" | "system".
  • تختلف أسماء سياسة الإقرار عن التصور. المُطلق: after_receive_record | after_agent_dispatch | after_durable_send | manual. استخدم التصور الأصلي immediate | after-record | after-durable-send | manual مع حقل لسبب انتهاء مهلة Webhook؛ ولم يُبنَ ذلك الشكل.
  • حلّت مفاتيح إمكانات DurableFinalDeliveryRequirementMap محل كائن MessageCapabilities المتصور. الإمكانات أعلام منطقية مسطحة (text وmedia وpoll وpayload وsilent وreplyTo وthread وnativeQuote وmessageSendingHooks وbatch وreconcileUnknownSend وafterSendSuccess وafterCommit) يجري التحقق منها عبر verifyDurableFinalCapabilityProofs بدلًا من بنية متداخلة على نمط text.chunking / attachments.voice.

مخاطر الترحيل الملموسة (لا تزال ذات صلة)

هذه الآثار الجانبية الخاصة بكل قناة تسبق إعادة الهيكلة ويجب أن تستمر في العمل عبر مسارات الإرسال الجديدة. وهي ليست افتراضية: فكل واحد منها مُنفَّذ ويؤدي دورًا أساسيًا اليوم.

  • iMessage (extensions/imessage/src/monitor/echo-cache.ts, persisted-echo-cache.ts): تسجّل أداة المراقبة الرسائل المرسلة في ذاكرة تخزين مؤقت للصدى بعد نجاح الإرسال. ويجب أن تستمر عمليات الإرسال النهائية الدائمة في ملء ذاكرة التخزين المؤقت تلك، وإلا فقد يعيد OpenClaw إدخال ردوده الخاصة بوصفها رسائل مستخدم واردة.
  • Tlon (extensions/tlon/src/monitor/index.ts): تُلحق توقيعًا اختياريًا للنموذج وتسجّل سلاسل المحادثات التي تمت المشاركة فيها بعد الردود الجماعية. ويجب ألا يتجاوز التسليم الدائم هذه الآثار.
  • Discord وأدوات التوزيع المُعَدّة الأخرى تتولى بالفعل التسليم المباشر وسلوك المعاينة. ولا تكون القناة دائمة من طرف إلى طرف حتى توجّه أداة التوزيع المُعَدّة الخاصة بها الرسائل النهائية صراحةً عبر سياق الإرسال؛ فلا تفترض أن المهايئ العام وحده يوفر التغطية.
  • يجب أن يسلّم التسليم الاحتياطي الصامت في Telegram مصفوفة الحمولة المتوقعة كاملةً، وليس الحمولة الأولى فقط، بعد التجزئة/الإسقاط الاحتياطي.
  • يمكن أن تتضمن مسارات المساعدة في LINE وZalo وNostr وما شابهها معالجة رموز الرد، أو تمرير الوسائط عبر وكيل، أو ذاكرات تخزين مؤقت للرسائل المرسلة، أو أهدافًا لا تعمل إلا عبر ردود النداء. وتظل خاضعة للتسليم الذي تملكه القناة حتى تمثَّل هذه الدلالات في مهايئ الإرسال وتغطيها الاختبارات.
  • يمكن أن تتضمن أدوات الرسائل المباشرة الخاصة رد نداء للرد يكون هدف النقل الصحيح الوحيد. ويجب ألا يخمّن الإرسال الصادر العام هدفًا من حقول المنصة الخام ويتجاوز رد النداء ذاك.

تصنيف الإخفاقات

تصنّف المهايئات إخفاقات النقل ضمن فئات مغلقة على نمط DeliveryFailureKind (عابر، حد المعدل، المصادقة، الإذن، غير موجود، حمولة غير صالحة، تعارض، ملغى، غير معروف). سياسة النواة:

  • أعد المحاولة عند الإخفاقات العابرة وإخفاقات حد المعدل.
  • لا تعد المحاولة عند إخفاقات الحمولة غير الصالحة ما لم يتوفر بديل للتصيير.
  • لا تعد المحاولة عند إخفاقات المصادقة أو الإذن حتى يتغير الإعداد.
  • عند عدم العثور، اسمح للإنهاء المباشر بالانتقال من التعديل إلى إرسال جديد عندما تعلن القناة أن ذلك آمن.
  • عند التعارض، استخدم حالة الإيصال/تكرارية التنفيذ لتحديد ما إذا كانت الرسالة موجودة بالفعل.
  • يتحول أي خطأ يحدث بعد أن تكون استدعاءات المنصة قد نجحت، ولكن قبل تثبيت الإيصال، إلى unknown_after_send ما لم يثبت المهايئ أن عملية المنصة لم تحدث.

أسئلة مفتوحة

  • ما إذا كان ينبغي لـ Telegram أن يستبدل في نهاية المطاف مشغّل الاستقصاء grammY (1.43.0) بمصدر استقصاء دائم بالكامل يتحكم في إعادة التسليم على مستوى المنصة، وليس فقط علامة إعادة التشغيل الدائمة في OpenClaw (safeCompletedUpdateId).
  • ما إذا كان ينبغي لحالة المعاينة المباشرة أن تكون في السجل نفسه الخاص بقصد الإرسال النهائي أم في مخزن شقيق للحالة المباشرة.
  • ما إذا كان منع الصدى عند إخفاق Gateway في الغرف المشتركة المُمكَّن فيها الروبوت يحتاج إلى آلية وسم الأصل التي كان مخططًا لها في الأصل، أو إلى عقد أبسط خاص بكل قناة، أم أنه خارج النطاق.
  • أي القنوات تدعم الأصل/البيانات الوصفية دعمًا أصليًا لمنع الصدى بين الروبوتات، وأيها يحتاج إلى سجل دائم للرسائل الصادرة.

ذو صلة

Was this useful?
On this page

On this page