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 و بیشتر استفادههای عمومی
از 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 در اتاقهای مشترکِ دارای ربات، به سازوکار برچسبگذاری مبدأ که در ابتدا برنامهریزی شده بود نیاز دارد، به یک قرارداد سادهتر برای هر کانال، یا خارج از دامنه است.
- کدام کانالها برای جلوگیری از پژواک میانرباتی از مبدأ/فرادادهٔ بومی پشتیبانی میکنند و کدامیک به یک دفتر ثبت خروجی ماندگار نیاز دارند.