Plugins
API ข้อความของช่องทาง
Plugin ช่องทางควรเปิดเผยอะแดปเตอร์ message หนึ่งตัวจาก
openclaw/plugin-sdk/channel-message อะแดปเตอร์จะอธิบายวงจรชีวิตข้อความแบบเนทีฟ
ที่แพลตฟอร์มรองรับ:
receive -> route and record -> agent turn -> durable final send
send -> render batch -> platform I/O -> receipt -> lifecycle side effects
live preview -> final edit or fallback -> receipt
แกนหลักรับผิดชอบคิว ความคงทน นโยบายลองใหม่ทั่วไป hooks ใบรับ และเครื่องมือ
message ที่ใช้ร่วมกัน Plugin รับผิดชอบการเรียกส่ง/แก้ไข/ลบแบบเนทีฟ การทำให้เป้าหมายเป็นมาตรฐาน
เธรดของแพลตฟอร์ม คำพูดอ้างอิงที่เลือก แฟล็กการแจ้งเตือน สถานะบัญชี
และผลข้างเคียงเฉพาะแพลตฟอร์ม
ใช้หน้านี้ร่วมกับ การสร้าง Plugin ช่องทาง
ซับพาธ channel-message ถูกตั้งใจให้เบาพอสำหรับไฟล์บูตสแตรป Plugin ที่เป็น hot path
เช่น channel.ts: โดยเปิดเผยสัญญาอะแดปเตอร์ หลักฐานความสามารถ ใบรับ
และฟาซาดความเข้ากันได้ โดยไม่โหลดการนำส่งขาออก
ตัวช่วยการนำส่งขณะรันไทม์มีให้จาก
openclaw/plugin-sdk/channel-message-runtime สำหรับเส้นทางโค้ด monitor/send
ที่กำลังทำ I/O ข้อความแบบอะซิงโครนัสอยู่แล้ว
อะแดปเตอร์ขั้นต่ำ
Plugin ช่องทางใหม่ส่วนใหญ่เริ่มจากอะแดปเตอร์ขนาดเล็กได้:
defineChannelMessageAdapter,
createMessageReceiptFromOutboundResults,
} from "openclaw/plugin-sdk/channel-message";
export const demoMessageAdapter = defineChannelMessageAdapter({
id: "demo",
durableFinal: {
capabilities: {
text: true,
replyTo: true,
thread: true,
messageSendingHooks: true,
},
},
send: {
text: async ({ cfg, to, text, accountId, replyToId, threadId, signal }) => {
const sent = await sendDemoMessage({
cfg,
to,
text,
accountId: accountId ?? undefined,
replyToId: replyToId ?? undefined,
threadId: threadId == null ? undefined : String(threadId),
signal,
});
return {
receipt: createMessageReceiptFromOutboundResults({
results: [{ channel: "demo", messageId: sent.id, conversationId: to }],
kind: "text",
threadId: threadId == null ? undefined : String(threadId),
replyToId: replyToId ?? undefined,
}),
};
},
},
});
จากนั้นแนบเข้ากับ Plugin ช่องทาง:
export const demoPlugin = createChatChannelPlugin({
base: {
id: "demo",
message: demoMessageAdapter,
// other channel plugin fields
},
});
ประกาศเฉพาะความสามารถที่อะแดปเตอร์คงรักษาไว้ได้จริงเท่านั้น ทุกความสามารถที่ประกาศ ควรมีการทดสอบสัญญา
บริดจ์ขาออก
หากช่องทางมีอะแดปเตอร์ outbound ที่เข้ากันได้อยู่แล้ว ให้เลือกสร้างอะแดปเตอร์
ข้อความจากอะแดปเตอร์นั้นแทนการทำโค้ดส่งซ้ำ:
const demoMessageAdapter = createChannelMessageAdapterFromOutbound({
id: "demo",
outbound: demoOutboundAdapter,
});
บริดจ์จะแปลงผลลัพธ์การส่งขาออกแบบเก่าเป็นค่า MessageReceipt โค้ดใหม่
ควรส่งใบรับไปตลอดทาง และสร้าง id แบบเดิมเฉพาะที่ขอบความเข้ากันได้
ด้วย listMessageReceiptPlatformIds(...) หรือ
resolveMessageReceiptPrimaryId(...)
หากไม่ได้ระบุนโยบายการรับ createChannelMessageAdapterFromOutbound(...)
จะใช้นโยบายการยืนยันการรับแบบ manual ซึ่งทำให้การยืนยันการรับของแพลตฟอร์มที่ Plugin เป็นเจ้าของ
ชัดเจน โดยไม่เปลี่ยนช่องทางที่ยืนยัน webhooks, sockets หรือ polling offsets
นอกบริบทการรับทั่วไป
การส่งผ่านเครื่องมือข้อความ
เส้นทาง message(action="send") ที่ใช้ร่วมกันควรใช้วงจรชีวิตการนำส่งของแกนหลักแบบเดียวกับ
คำตอบสุดท้าย หากช่องทางต้องปรับรูปแบบเฉพาะผู้ให้บริการสำหรับการส่งของเครื่องมือ
ให้ใช้งาน actions.prepareSendPayload(...) แทนการส่งจาก
actions.handleAction(...)
prepareSendPayload(...) จะได้รับ ReplyPayload ของแกนหลักที่ทำให้เป็นมาตรฐานแล้ว พร้อมกับ
บริบท action แบบเต็ม ให้คืน payload ที่มีข้อมูลเฉพาะช่องทางใน
payload.channelData.<channel> แล้วให้แกนหลักเรียก sendMessage(...),
deliverOutboundPayloads(...), คิว write-ahead, hooks การส่งข้อความ,
การลองใหม่ การกู้คืน และการล้าง ack
คืนค่า null เฉพาะเมื่อการส่งไม่สามารถแทนเป็น payload ที่คงทนได้
ตัวอย่างเช่น เพราะมี component factory ที่ทำให้เป็นอนุกรมไม่ได้ แกนหลักจะคง
fallback ของ plugin action แบบเดิมไว้เพื่อความเข้ากันได้ แต่ฟีเจอร์ส่งผ่านช่องทางใหม่
ควรแสดงออกเป็นข้อมูล payload ที่คงทนได้
export const demoActions: ChannelMessageActionAdapter = {
describeMessageTool: () => ({ actions: ["send"], capabilities: ["presentation"] }),
prepareSendPayload: ({ ctx, payload }) => {
if (ctx.action !== "send") {
return null;
}
return {
...payload,
channelData: {
...payload.channelData,
demo: {
...(payload.channelData?.demo as object | undefined),
nativeCard: ctx.params.card,
},
},
};
},
};
จากนั้นอะแดปเตอร์ขาออกจะอ่าน payload.channelData.demo ภายใน sendPayload
วิธีนี้คงการเรนเดอร์เฉพาะแพลตฟอร์มไว้ใน Plugin ในขณะที่แกนหลักยังคงรับผิดชอบ
การคงบันทึก การลองใหม่ การกู้คืน hooks และ ack
payload ของ message(action="send") ที่เตรียมแล้ว และการนำส่งคำตอบสุดท้ายแบบทั่วไปใช้
การนำส่งของแกนหลักพร้อมคิวแบบ best-effort ตามค่าเริ่มต้น คิวแบบคงทนที่เป็นข้อกำหนด
จะใช้ได้หลังจากแกนหลักตรวจสอบแล้วเท่านั้นว่าช่องทางสามารถปรับคืนการส่งที่ไม่ทราบผลลัพธ์
หลังเกิดการล่มได้ หากอะแดปเตอร์ไม่สามารถใช้งาน reconcileUnknownSend ได้
ให้คงเส้นทางส่งที่เตรียมไว้เป็น best-effort; แกนหลักจะยังพยายามใช้คิว write-ahead
แต่ความคงอยู่ของคิวหรือการกู้คืนจากการล่มที่ไม่แน่นอนไม่ใช่ส่วนหนึ่งของ
สัญญาการนำส่งที่บังคับใช้
ความสามารถ final แบบคงทน
การนำส่ง final แบบคงทนเป็นการเลือกใช้รายผลข้างเคียง แกนหลักจะใช้เฉพาะ การนำส่งแบบคงทนทั่วไปเมื่ออะแดปเตอร์ประกาศความสามารถทุกอย่างที่ payload และตัวเลือกการนำส่งต้องใช้
| ความสามารถ | ประกาศเมื่อ |
|---|---|
text |
อะแดปเตอร์ส่งข้อความและคืนใบรับได้ |
media |
การส่งสื่อคืนใบรับสำหรับทุกข้อความแพลตฟอร์มที่มองเห็นได้ |
payload |
อะแดปเตอร์คงความหมายของ rich reply payload ไม่ใช่แค่ข้อความและ URL สื่อหนึ่งรายการ |
replyTo |
เป้าหมายการตอบกลับแบบเนทีฟไปถึงแพลตฟอร์ม |
thread |
เป้าหมายเธรด หัวข้อ หรือเธรดช่องทางแบบเนทีฟไปถึงแพลตฟอร์ม |
silent |
การระงับการแจ้งเตือนไปถึงแพลตฟอร์ม |
nativeQuote |
เมทาดาทาคำพูดอ้างอิงที่เลือกไปถึงแพลตฟอร์ม |
messageSendingHooks |
hooks การส่งข้อความของแกนหลักสามารถยกเลิกหรือเขียนเนื้อหาใหม่ก่อน I/O แพลตฟอร์ม |
batch |
batch ที่เรนเดอร์แบบหลายส่วนสามารถ replay เป็นแผนคงทนเดียวได้ |
reconcileUnknownSend |
อะแดปเตอร์แก้การกู้คืน unknown_after_send ได้โดยไม่ replay แบบสุ่ม |
afterSendSuccess |
ผลข้างเคียง after-send ภายในช่องทางทำงานหนึ่งครั้ง |
afterCommit |
ผลข้างเคียง after-commit ภายในช่องทางทำงานหนึ่งครั้ง |
การนำส่ง final แบบ best-effort ไม่ต้องใช้ reconcileUnknownSend; โดยใช้
วงจรชีวิตที่ใช้ร่วมกันเมื่ออะแดปเตอร์คงความหมายที่มองเห็นได้ของ payload และ
fallback เป็น I/O แพลตฟอร์มโดยตรงหากความคงอยู่ของคิวไม่พร้อมใช้งาน การนำส่ง
final แบบคงทนที่เป็นข้อกำหนดต้องกำหนด reconcileUnknownSend อย่างชัดเจน หาก
อะแดปเตอร์ไม่สามารถระบุได้ว่าการส่งที่เริ่มแล้ว/ไม่ทราบสถานะไปถึงแพลตฟอร์มหรือไม่
อย่าประกาศความสามารถนั้น; แกนหลักจะปฏิเสธการนำส่งแบบคงทนที่เป็นข้อกำหนด
ก่อนเข้าคิว
เมื่อผู้เรียกต้องการการนำส่งแบบคงทน ให้คำนวณข้อกำหนดแทนการสร้าง map เอง:
const requiredCapabilities = deriveDurableFinalDeliveryRequirements({
payload,
replyToId,
threadId,
silent,
payloadTransport: true,
extraCapabilities: {
nativeQuote: hasSelectedQuote(payload),
},
});
messageSendingHooks เป็นข้อกำหนดตามค่าเริ่มต้น ตั้งค่า messageSendingHooks: false
เฉพาะสำหรับเส้นทางที่ตั้งใจให้ไม่สามารถรัน hooks การส่งข้อความส่วนกลางได้
สัญญาการส่งแบบคงทน
การส่ง final แบบคงทนมีความหมายที่เข้มงวดกว่าการนำส่งแบบเดิมที่ช่องทางเป็นเจ้าของ:
- สร้าง intent แบบคงทนก่อน I/O แพลตฟอร์ม
- หากการนำส่งแบบคงทนคืนผลลัพธ์ที่จัดการแล้ว อย่า fallback ไปยังการส่งแบบเดิม
- ถือว่าการยกเลิกจาก hook และผลลัพธ์ no-send เป็นสถานะสิ้นสุด
- ถือว่า
unsupportedเป็นผลลัพธ์ก่อน intent เท่านั้น - สำหรับความคงทนที่เป็นข้อกำหนด ให้ล้มเหลวก่อน I/O แพลตฟอร์มหากคิวไม่สามารถบันทึก ว่าการส่งไปยังแพลตฟอร์มเริ่มแล้ว
- สำหรับการนำส่ง final ที่เป็นข้อกำหนด และการส่งเครื่องมือข้อความที่เตรียมไว้ซึ่งเป็นข้อกำหนด
ให้ preflight
reconcileUnknownSend; การกู้คืนต้องสามารถ ack ข้อความที่ส่งแล้ว หรือ replay เฉพาะหลังจากอะแดปเตอร์พิสูจน์ว่าการส่งเดิม ไม่เกิดขึ้น - สำหรับ
best_effortความล้มเหลวในการเขียนคิวอาจ fallback ไปยัง I/O แพลตฟอร์มโดยตรง - ส่งต่อสัญญาณยกเลิกไปยังการโหลดสื่อและการส่งของแพลตฟอร์ม
- รัน hooks after-commit หลัง queue ack; fallback โดยตรงแบบ best-effort จะรัน hooks เหล่านี้ หลัง I/O แพลตฟอร์มสำเร็จ เพราะไม่มี commit ของคิวแบบคงทน
- คืนใบรับสำหรับ id ของข้อความแพลตฟอร์มที่มองเห็นได้ทุกข้อความ
- ใช้
reconcileUnknownSendเมื่อแพลตฟอร์มสามารถตรวจสอบได้ว่าการส่งที่ไม่แน่นอน ไปถึงผู้ใช้แล้วหรือยัง
สัญญานี้หลีกเลี่ยงการส่งซ้ำหลังเกิดการล่ม และหลีกเลี่ยงการข้าม hooks การยกเลิกการส่งข้อความ
ใบรับ
MessageReceipt คือระเบียนภายในใหม่ของสิ่งที่แพลตฟอร์มยอมรับ:
type MessageReceipt = {
primaryPlatformMessageId?: string;
platformMessageIds: string[];
parts: MessageReceiptPart[];
threadId?: string;
replyToId?: string;
editToken?: string;
deleteToken?: string;
sentAt: number;
raw?: readonly MessageReceiptSourceResult[];
};
ใช้ createMessageReceiptFromOutboundResults(...) เมื่อปรับใช้ผลลัพธ์การส่งที่มีอยู่
ใช้ createPreviewMessageReceipt(...) เมื่อข้อความ live preview กลายเป็นใบรับสุดท้าย
หลีกเลี่ยงการเพิ่มฟิลด์ messageIds แบบ owner-local ใหม่
ChannelDeliveryResult.messageIds แบบเดิมยังถูกสร้างที่ขอบความเข้ากันได้
Live preview
ช่องทางที่สตรีม draft previews หรือ progress updates ควรประกาศความสามารถ live:
const demoMessageAdapter = defineChannelMessageAdapter({
id: "demo",
live: {
capabilities: {
draftPreview: true,
previewFinalization: true,
progressUpdates: true,
quietFinalization: true,
},
finalizer: {
capabilities: {
finalEdit: true,
normalFallback: true,
discardPending: true,
previewReceipt: true,
retainOnAmbiguousFailure: true,
},
},
},
});
ใช้ defineFinalizableLivePreviewAdapter(...) และ
deliverWithFinalizableLivePreviewAdapter(...) สำหรับการ finalization ขณะรันไทม์
finalizer จะตัดสินใจว่าคำตอบสุดท้ายจะแก้ไข preview ตรงที่เดิม ส่ง
fallback ปกติ ละทิ้งสถานะ preview ที่ค้างอยู่ เก็บการแก้ไขที่ล้มเหลวแบบกำกวมไว้
โดยไม่ทำข้อความซ้ำ และคืนใบรับสุดท้ายหรือไม่
นโยบาย receive ack
ตัวรับขาเข้าที่ควบคุมเวลาการยืนยันการรับของแพลตฟอร์มควรประกาศ นโยบาย receive:
const demoMessageAdapter = defineChannelMessageAdapter({
id: "demo",
receive: {
defaultAckPolicy: "after_agent_dispatch",
supportedAckPolicies: ["after_receive_record", "after_agent_dispatch"],
},
});
อะแดปเตอร์ที่ไม่ประกาศนโยบาย receive จะใช้ค่าเริ่มต้นเป็น:
{
receive: {
defaultAckPolicy: "manual",
supportedAckPolicies: ["manual"],
},
}
ใช้ค่าเริ่มต้นเมื่อแพลตฟอร์มไม่มีการตอบรับให้เลื่อนออกไป ได้ตอบรับแล้วก่อนการประมวลผลแบบอะซิงโครนัส หรือต้องใช้ความหมายการตอบสนองเฉพาะโปรโตคอล ประกาศนโยบายแบบแบ่งระยะรายการใดรายการหนึ่งเฉพาะเมื่อผู้รับใช้บริบทการรับจริง ๆ เพื่อเลื่อนการตอบรับของแพลตฟอร์มออกไป
นโยบาย:
| นโยบาย | ใช้เมื่อ |
|---|---|
after_receive_record |
แพลตฟอร์มสามารถได้รับการตอบรับหลังจากเหตุการณ์ขาเข้าถูกแยกวิเคราะห์และบันทึกแล้ว |
after_agent_dispatch |
แพลตฟอร์มควรรอจนกว่าการส่งต่อไปยัง agent จะได้รับการยอมรับแล้ว |
after_durable_send |
แพลตฟอร์มควรรอจนกว่าการส่งมอบสุดท้ายจะมีการตัดสินใจแบบคงทน |
manual |
Plugin เป็นเจ้าของการตอบรับ เพราะความหมายของแพลตฟอร์มไม่ตรงกับระยะทั่วไป |
ใช้ createMessageReceiveContext(...) ในตัวรับที่เลื่อนสถานะการตอบรับออกไป และใช้
shouldAckMessageAfterStage(...) เมื่อตัวรับต้องทดสอบว่าระยะหนึ่งเป็นไปตามนโยบายที่กำหนดค่าไว้หรือไม่
การทดสอบสัญญา
การประกาศความสามารถเป็นส่วนหนึ่งของสัญญา Plugin ให้รองรับด้วยการทดสอบ:
verifyChannelMessageAdapterCapabilityProofs,
verifyChannelMessageLiveCapabilityAdapterProofs,
verifyChannelMessageLiveFinalizerProofs,
verifyChannelMessageReceiveAckPolicyAdapterProofs,
} from "openclaw/plugin-sdk/channel-message";
it("backs declared message capabilities", async () => {
await expect(
verifyChannelMessageAdapterCapabilityProofs({
adapterName: "demo",
adapter: demoMessageAdapter,
proofs: {
text: async () => {
const result = await demoMessageAdapter.send!.text!(textCtx);
expect(result.receipt.platformMessageIds).toContain("msg-1");
},
replyTo: async () => {
await demoMessageAdapter.send!.text!({ ...textCtx, replyToId: "parent-1" });
expect(sendDemoMessage).toHaveBeenCalledWith(
expect.objectContaining({
replyToId: "parent-1",
}),
);
},
messageSendingHooks: () => {
expect(demoMessageAdapter.durableFinal!.capabilities!.messageSendingHooks).toBe(true);
},
},
}),
).resolves.toContainEqual({ capability: "text", status: "verified" });
});
เพิ่มชุดพิสูจน์แบบ live และ receive เมื่อ adapter ประกาศฟีเจอร์เหล่านั้น การพิสูจน์ที่ขาดหายควรทำให้การทดสอบล้มเหลว แทนที่จะขยายพื้นผิวแบบคงทนอย่างเงียบ ๆ
API ความเข้ากันได้ที่เลิกใช้แล้ว
API เหล่านี้ยังคงนำเข้าได้เพื่อความเข้ากันได้กับบุคคลที่สาม อย่าใช้สำหรับโค้ดช่องทางใหม่
| API ที่เลิกใช้แล้ว | สิ่งที่ใช้แทน |
|---|---|
openclaw/plugin-sdk/channel-reply-pipeline |
openclaw/plugin-sdk/channel-message |
createChannelTurnReplyPipeline(...) |
createChannelMessageReplyPipeline(...) สำหรับ dispatcher เพื่อความเข้ากันได้ หรือ adapter message สำหรับโค้ดช่องทางใหม่ |
deliverDurableInboundReplyPayload(...) |
deliverInboundReplyWithMessageSendContext(...) จาก openclaw/plugin-sdk/channel-message-runtime |
dispatchInboundReplyWithBase(...) |
dispatchChannelMessageReplyWithBase(...) สำหรับ dispatcher เพื่อความเข้ากันได้เท่านั้น |
recordInboundSessionAndDispatchReply(...) |
recordChannelMessageReplyDispatch(...) สำหรับ dispatcher เพื่อความเข้ากันได้เท่านั้น |
resolveChannelSourceReplyDeliveryMode(...) |
resolveChannelMessageSourceReplyDeliveryMode(...) |
deliverFinalizableDraftPreview(...) |
defineFinalizableLivePreviewAdapter(...) ร่วมกับ deliverWithFinalizableLivePreviewAdapter(...) |
DraftPreviewFinalizerDraft |
LivePreviewFinalizerDraft |
DraftPreviewFinalizerResult |
LivePreviewFinalizerResult |
dispatcher เพื่อความเข้ากันได้ยังสามารถใช้ createReplyPrefixContext(...),
createReplyPrefixOptions(...), และ createTypingCallbacks(...) ผ่าน facade
ข้อความได้ โค้ด lifecycle ใหม่ควรหลีกเลี่ยง subpath เก่า
channel-reply-pipeline
เช็กลิสต์การย้าย
- เพิ่ม
message: defineChannelMessageAdapter(...)หรือmessage: createChannelMessageAdapterFromOutbound(...)ไปยัง Plugin ช่องทาง - ส่งคืน
MessageReceiptจากการส่งข้อความ สื่อ และ payload - ประกาศเฉพาะความสามารถที่รองรับด้วยพฤติกรรม native และการทดสอบ
- แทนที่แผนที่ข้อกำหนดแบบคงทนที่เขียนเองด้วย
deriveDurableFinalDeliveryRequirements(...) - ย้ายการทำ preview finalization ผ่านตัวช่วย live preview เมื่อช่องทาง แก้ไขข้อความฉบับร่างในที่เดิม
- ประกาศนโยบายการตอบรับฝั่งรับเฉพาะเมื่อตัวรับสามารถเลื่อนการตอบรับของแพลตฟอร์มออกไปได้จริง
- เก็บตัวช่วยการ dispatch reply แบบ legacy ไว้เฉพาะที่ขอบความเข้ากันได้เท่านั้น