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) ทำให้ ID ข้อความบนแพลตฟอร์มตั้งแต่หนึ่งรายการขึ้นไป
จากการส่งเชิงตรรกะครั้งเดียวอยู่ในรูปแบบมาตรฐานเป็น platformMessageIds พร้อม
parts ต่อส่วน (ชนิด ดัชนี ID เธรด ID ข้อความที่ตอบกลับ) โดยเก็บ ID หลักไว้
สำหรับการสร้างเธรดและการแก้ไขภายหลัง นี่คือสิ่งที่ทำให้การส่งหลายส่วน (ข้อความ
พร้อมสื่อ ข้อความที่แบ่งเป็นส่วน การย้อนกลับไปใช้การ์ด) สามารถเล่นซ้ำและขจัดรายการซ้ำได้หลัง
เริ่มใหม่
การลดพื้นผิว 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 และเส้นทางตัวช่วยที่คล้ายกันอาจมีการจัดการโทเค็นตอบกลับ การพร็อกซีสื่อ แคชข้อความที่ส่งแล้ว หรือเป้าหมายที่ใช้ได้เฉพาะคอลแบ็ก เส้นทางเหล่านี้จะยังคงใช้การนำส่งที่ช่องทางเป็นผู้ควบคุม จนกว่าความหมายเชิงพฤติกรรมเหล่านั้นจะแสดงอยู่ใน อะแดปเตอร์การส่งและมีการทดสอบครอบคลุม
- ตัวช่วย DM โดยตรง อาจมีคอลแบ็กตอบกลับที่เป็น เป้าหมายการรับส่งข้อมูลเดียวที่ถูกต้อง ระบบขาออกทั่วไปต้องไม่คาดเดาเป้าหมายจาก ฟิลด์ดิบของแพลตฟอร์มแล้วข้ามคอลแบ็กนั้น
การจำแนกความล้มเหลว
อะแดปเตอร์จำแนกความล้มเหลวของการรับส่งข้อมูลเป็นหมวดหมู่แบบปิดในรูปแบบ DeliveryFailureKind
(ชั่วคราว, จำกัดอัตรา, การยืนยันตัวตน, สิทธิ์, ไม่พบ,
เพย์โหลดไม่ถูกต้อง, ข้อขัดแย้ง, ยกเลิกแล้ว, ไม่ทราบ) นโยบายหลัก:
- ลองใหม่เมื่อเกิดความล้มเหลวชั่วคราวและความล้มเหลวจากการจำกัดอัตรา
- อย่าลองใหม่เมื่อเพย์โหลดไม่ถูกต้อง เว้นแต่จะมีเส้นทางสำรองสำหรับการเรนเดอร์
- อย่าลองใหม่เมื่อการยืนยันตัวตนหรือสิทธิ์ล้มเหลว จนกว่าจะมีการเปลี่ยนแปลงการกำหนดค่า
- เมื่อไม่พบ ให้การสรุปผลแบบสดเปลี่ยนจากการแก้ไขไปเป็นการส่งใหม่ เมื่อช่องทางประกาศว่าสามารถดำเนินการดังกล่าวได้อย่างปลอดภัย
- เมื่อเกิดข้อขัดแย้ง ให้ใช้สถานะใบรับและการทำงานซ้ำโดยให้ผลเหมือนเดิมเพื่อพิจารณาว่า ข้อความมีอยู่แล้วหรือไม่
- ข้อผิดพลาดใดๆ ที่เกิดหลังจากการเรียกแพลตฟอร์มอาจสำเร็จแล้ว แต่ก่อนที่จะคอมมิตใบรับ
จะกลายเป็น
unknown_after_sendเว้นแต่อะแดปเตอร์จะพิสูจน์ได้ว่า การดำเนินการบนแพลตฟอร์มไม่ได้เกิดขึ้น
คำถามที่ยังไม่มีข้อสรุป
- ในท้ายที่สุด Telegram ควรแทนที่ตัวรันการโพล grammY (
1.43.0) ด้วยแหล่งการโพลแบบคงทนอย่างสมบูรณ์ที่ควบคุมการนำส่งซ้ำในระดับแพลตฟอร์มหรือไม่ แทนที่จะควบคุมเพียงวอเตอร์มาร์กการเริ่มระบบใหม่ที่ OpenClaw จัดเก็บไว้ (safeCompletedUpdateId) - สถานะการแสดงตัวอย่างแบบสดควรอยู่ในเรคคอร์ดเดียวกับเจตนาในการส่งข้อความสุดท้าย หรืออยู่ในที่เก็บสถานะแบบสดที่เป็นเรคคอร์ดคู่กัน
- การระงับข้อความสะท้อนกลับเมื่อ Gateway ล้มเหลวในห้องที่ใช้ร่วมกันและเปิดใช้บอต จำเป็นต้องใช้กลไกการติดแท็กต้นทางที่วางแผนไว้เดิม สัญญารายช่องทางที่เรียบง่ายกว่า หรืออยู่นอกขอบเขต
- ช่องทางใดบ้างที่รองรับต้นทาง/ข้อมูลเมตาโดยกำเนิดสำหรับการระงับข้อความสะท้อนกลับ ข้ามบอต และช่องทางใดต้องใช้รีจิสทรีขาออกที่จัดเก็บไว้อย่างคงทน