Messages and delivery
بازآرایی چرخه عمر پیام
چرا این بازآرایی انجام شد
پشته کانال از چندین اصلاح محلی رشد کرد: یاریگرهای ورودی جداگانه برای هر
سطح بلوغ (runtime.channel.inbound.run برای آداپتورهای ساده،
runtime.channel.inbound.runPreparedReply برای آداپتورهای غنی)، یاریگرهای قدیمی ارسال پاسخ
(dispatchInboundReplyWithBase، recordInboundSessionAndDispatchReply)،
پخش جریانی پیشنمایش مختص هر کانال، و دوام تحویل نهایی که به
مسیرهای موجود محموله پاسخ وصله شده بود. این ساختار مفاهیم عمومی بیشازحد و
نقاط بیشازحدی ایجاد کرد که معناشناسی تحویل میتوانست در آنها منحرف شود.
شکاف اطمینانپذیری که بازطراحی را ناگزیر کرد:
بهروزرسانی نظرسنجی 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 و بیشتر کاربردهای عمومی
نمای خروجی قدیمی. 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 در اتاقهای مشترک دارای ربات به سازوکار برچسبگذاری مبدأ که در ابتدا برنامهریزی شده بود، یک قرارداد سادهتر برای هر کانال نیاز دارد، یا خارج از محدوده است.
- کدام کانالها برای سرکوب پژواک میانرباتی از مبدأ/فرادادهٔ بومی پشتیبانی میکنند و کدامیک به یک دفتر ثبت خروجی پایدار نیاز دارند.