Building plugins

ฮุกของ Plugin

ฮุกของ Plugin เป็นจุดขยายภายในโปรเซสสำหรับ Plugin ของ OpenClaw ซึ่งใช้ตรวจสอบหรือ เปลี่ยนแปลงการทำงานของเอเจนต์ การเรียกใช้เครื่องมือ ลำดับการไหลของข้อความ วงจรชีวิตของเซสชัน การกำหนดเส้นทาง ซับเอเจนต์ การติดตั้ง หรือการเริ่มต้น Gateway

สำหรับสคริปต์ HOOK.md ขนาดเล็กที่ผู้ดำเนินการติดตั้งและตอบสนองต่อเหตุการณ์คำสั่งและ Gateway เช่น /new, /reset, /stop, agent:bootstrap หรือ gateway:startup ให้ใช้ ฮุกภายใน แทน

เริ่มต้นอย่างรวดเร็ว

ลงทะเบียนฮุกที่กำหนดชนิดด้วย api.on(...) จากจุดเริ่มต้นของ Plugin:

typescript
 export default definePluginEntry({  id: "tool-preflight",  name: "ตรวจสอบเครื่องมือล่วงหน้า",  register(api) {    api.on(      "before_tool_call",      async (event) => {        if (event.toolName !== "web_search") {          return;        }         return {          requireApproval: {            title: "เรียกใช้การค้นหาเว็บ",            description: `อนุญาตคำค้นหา: ${String(event.params.query ?? "")}`,            severity: "info",            timeoutMs: 60_000,          },        };      },      { priority: 50 },    );  },});

ตัวจัดการที่สามารถส่งคืนการตัดสินใจหรือการแก้ไขจะทำงานตามลำดับโดยเรียงค่า priority จากมากไปน้อย ส่วนตัวจัดการที่มีลำดับความสำคัญเท่ากันจะคงลำดับการลงทะเบียนไว้ ตัวจัดการที่ใช้สังเกตการณ์เท่านั้นจะทำงานพร้อมกัน และการส่งการสังเกตการณ์แบบไม่รอผล อาจซ้อนทับกับเหตุการณ์ที่เกิดภายหลัง อย่าใช้ลำดับความสำคัญเพื่อจัดลำดับ ผลข้างเคียงจากการสังเกตการณ์

api.on(name, handler, opts?) รองรับ:

ตัวเลือก ผล
priority ลำดับการทำงาน ค่าที่สูงกว่าจะทำงานก่อน
timeoutMs งบเวลารอของแต่ละฮุก เมื่อหมดเวลา OpenClaw จะหยุดรอตัวจัดการนั้นและดำเนินการต่อ โดยจะไม่ยกเลิกตัวจัดการหรือผลข้างเคียงของตัวจัดการ ละไว้เพื่อใช้ระยะหมดเวลาเริ่มต้นต่อฮุกของตัวรัน

ผู้ดำเนินการสามารถกำหนดงบเวลาของฮุกได้โดยไม่ต้องแก้ไขโค้ด Plugin:

json
{  "plugins": {    "entries": {      "my-plugin": {        "hooks": {          "timeoutMs": 30000,          "timeouts": {            "before_prompt_build": 90000,            "agent_end": 60000          }        }      }    }  }}

hooks.timeouts.<hookName> มีผลเหนือ hooks.timeoutMs ซึ่งมีผลเหนือค่า api.on(..., { timeoutMs }) ที่ผู้เขียน Plugin กำหนด แต่ละค่าต้องเป็น จำนวนเต็มบวกไม่เกิน 600000 ms ควรใช้ค่าที่กำหนดเฉพาะต่อฮุกสำหรับ ฮุกที่ทราบว่าทำงานช้า เพื่อไม่ให้ Plugin หนึ่งได้รับงบเวลาที่ยาวขึ้นในทุกตำแหน่ง

พรอมิสของตัวจัดการที่หมดเวลาจะยังคงทำงานต่อ เนื่องจากคอลแบ็กของฮุกไม่ได้ รับสัญญาณการยกเลิก การส่งฮุกอาจคืนสิทธิ์การรับงานของ Gateway ในขณะที่งานของ Plugin นั้นยังดำเนินอยู่ Plugin ที่เป็นเจ้าของ งานซึ่งทำงานเป็นเวลานานต้องจัดเตรียมวงจรชีวิตสำหรับการยกเลิกและปิดระบบของตนเอง

ฮุกแก้ไขขาออก message_sending และ reply_payload_sending ใช้ค่าเริ่มต้น 15 วินาทีต่อตัวจัดการ หากตัวใดหมดเวลา OpenClaw จะบันทึกข้อผิดพลาดของ Plugin และดำเนินการต่อด้วยเพย์โหลดล่าสุด เพื่อให้ช่องทางการส่งมอบแบบเรียงลำดับ สิ้นสุดได้ ให้กำหนดงบเวลาต่อฮุกที่มากขึ้นสำหรับ Plugin ที่ตั้งใจทำงานช้ากว่า ก่อนส่งมอบ

Plugin ของช่องทางที่ใช้ createReplyDispatcher สามารถประกาศ งบเวลาต่อขั้นที่เป็นจำนวนบวกและมากขึ้นด้วย beforeDeliverOptions: { timeoutMs } หรือเมื่อ เพิ่มงานต่อท้ายด้วย dispatcher.appendBeforeDeliver(handler, { timeoutMs }) ได้เช่นกัน หากไม่มีงบเวลาที่เจ้าของประกาศ คอลแบ็กเหล่านั้นจะใช้ค่าเริ่มต้น 15 วินาทีเหมือนกัน เพื่อไม่ให้คอลแบ็กที่ค้างยึดช่องทางการส่งมอบแบบเรียงลำดับไว้

แต่ละฮุกจะได้รับ event.context.pluginConfig ซึ่งเป็นการกำหนดค่าที่แก้ไขเสร็จแล้วสำหรับ Plugin ที่ลงทะเบียนตัวจัดการนั้น OpenClaw จะแทรกค่านี้แยกตามตัวจัดการโดยไม่ แก้ไขออบเจ็กต์เหตุการณ์ที่ใช้ร่วมกันซึ่ง Plugin อื่นมองเห็น

แค็ตตาล็อกฮุก

ฮุกถูกจัดกลุ่มตามส่วนที่ฮุกขยาย ชื่อที่เป็น ตัวหนา รองรับผลลัพธ์การตัดสินใจ (บล็อก ยกเลิก แทนที่ หรือต้องได้รับการอนุมัติ) ส่วนที่เหลือใช้สำหรับ การสังเกตการณ์เท่านั้น

รอบการทำงานของเอเจนต์

ฮุก วัตถุประสงค์
before_model_resolve แทนที่ผู้ให้บริการหรือโมเดลก่อนโหลดข้อความของเซสชัน
agent_turn_prepare ใช้ข้อมูลที่ Plugin รอแทรกในรอบการทำงาน และเพิ่มบริบทในรอบเดียวกันก่อนฮุกของพรอมต์
before_prompt_build เพิ่มบริบทแบบไดนามิกหรือข้อความพรอมต์ระบบก่อนเรียกโมเดล
before_agent_run ตรวจสอบพรอมต์สุดท้ายและข้อความของเซสชันก่อนส่งไปยังโมเดล และสามารถบล็อกการทำงานได้
before_agent_reply ลัดวงจรรอบการทำงานของโมเดลด้วยคำตอบสังเคราะห์หรือไม่ส่งคำตอบ
before_agent_finalize ตรวจสอบคำตอบสุดท้ายตามธรรมชาติและขอให้โมเดลประมวลผลเพิ่มอีกหนึ่งรอบ
agent_end สังเกตข้อความสุดท้าย สถานะความสำเร็จ และระยะเวลาการทำงาน
heartbeat_prompt_contribution เพิ่มบริบทเฉพาะ Heartbeat สำหรับ Plugin ตรวจสอบเบื้องหลังและวงจรชีวิต

การสังเกตการณ์การสนทนา

ฮุก วัตถุประสงค์
model_call_started / model_call_ended เมทาดาทาการเรียกผู้ให้บริการ/โมเดลที่ผ่านการล้างข้อมูลแล้ว ได้แก่ เวลา ผลลัพธ์ และแฮช ID คำขอแบบจำกัด โดยไม่มีเนื้อหาพรอมต์หรือคำตอบ
llm_input อินพุตของผู้ให้บริการ ได้แก่ พรอมต์ระบบ พรอมต์ และประวัติ
llm_output เอาต์พุตของผู้ให้บริการ การใช้งาน และ contextTokenBudget ที่แก้ไขเสร็จแล้วเมื่อมี

เครื่องมือ

ฮุก วัตถุประสงค์
before_tool_call เขียนพารามิเตอร์เครื่องมือใหม่ บล็อกการดำเนินการ หรือต้องได้รับการอนุมัติ
after_tool_call สังเกตผลลัพธ์ ข้อผิดพลาด และระยะเวลาของเครื่องมือ
resolve_exec_env เพิ่มตัวแปรสภาพแวดล้อมที่ Plugin เป็นเจ้าของให้แก่ exec
tool_result_persist เขียนข้อความผู้ช่วยที่สร้างจากผลลัพธ์ของเครื่องมือใหม่
before_message_write ตรวจสอบหรือบล็อกการเขียนข้อความที่กำลังดำเนินอยู่ (พบไม่บ่อย)

ข้อความและการส่งมอบ

ฮุก วัตถุประสงค์
inbound_claim รับช่วงข้อความขาเข้าก่อนกำหนดเส้นทางไปยังเอเจนต์ (คำตอบสังเคราะห์)
channel_pairing_requested สังเกตคำขอจับคู่ DM ที่เพิ่งสร้าง
message_received สังเกตเนื้อหาขาเข้า ผู้ส่ง เธรด และเมทาดาทา
message_sending เขียนเนื้อหาขาออกใหม่หรือยกเลิกการส่งมอบ
reply_payload_sending แก้ไขหรือยกเลิกเพย์โหลดคำตอบที่ปรับให้อยู่ในรูปแบบมาตรฐานก่อนส่งมอบ
message_sent สังเกตความสำเร็จหรือความล้มเหลวของการส่งมอบขาออก
before_dispatch ตรวจสอบหรือเขียนการส่งต่อขาออกใหม่ก่อนส่งมอบให้ช่องทาง
reply_dispatch มีส่วนร่วมในไปป์ไลน์การส่งต่อคำตอบขั้นสุดท้าย

เซสชันและ Compaction

ฮุก วัตถุประสงค์
session_start / session_end ติดตามขอบเขตวงจรชีวิตของเซสชัน reason เป็นหนึ่งใน new, reset, idle, daily, compaction, deleted, shutdown, restart หรือ unknown โดย shutdown/restart จะทำงานจากตัวดำเนินการปิดระบบขั้นสุดท้ายของ Gateway เมื่อโปรเซสหยุดหรือเริ่มใหม่ขณะที่มีเซสชันที่ใช้งานอยู่ เพื่อให้ Plugin (หน่วยความจำ พื้นที่จัดเก็บทรานสคริปต์) สามารถปิดแถวค้างได้แทนที่จะปล่อยให้เปิดค้างข้ามการเริ่มระบบใหม่ ตัวดำเนินการขั้นสุดท้ายนี้มีการจำกัดเวลา เพื่อไม่ให้ Plugin ที่ทำงานช้าบล็อก SIGTERM/SIGINT
before_compaction / after_compaction สังเกตหรือใส่คำอธิบายประกอบรอบ Compaction
before_reset สังเกตเหตุการณ์รีเซ็ตเซสชัน (/reset, การรีเซ็ตโดยโปรแกรม)

สำหรับการเรียก sessions.create ที่มี parentSessionKey และ emitCommandHooks: true เซสชันลูกที่แยกต่างหากจะได้รับ session_start เสมอ ผู้เรียกจะระบุว่าเซสชันแม่ได้รับ session_end ขั้นสุดท้ายด้วยหรือไม่ผ่าน succeedsParent: true หมายถึงตัวสืบทอด ส่วน false หมายถึงเซสชันลูกแบบขนาน การละค่าไว้จะคงพฤติกรรมเดิมในการสับเปลี่ยนเซสชันแม่ ฮุก command:new และ before_reset ยังคงอธิบายการดำเนินการ /new ที่ร้องขอในทั้งสองกรณี

ซับเอเจนต์

  • subagent_spawned / subagent_ended - สังเกตการณ์การเริ่มต้นและการเสร็จสิ้นของเอเจนต์ย่อย
  • subagent_delivery_target - ฮุกความเข้ากันได้สำหรับส่งผลการเสร็จสิ้นเมื่อไม่มีการผูกเซสชันหลักที่สามารถฉายเส้นทางได้
  • subagent_spawning - ฮุกความเข้ากันได้ที่เลิกใช้แล้ว ขณะนี้ส่วนหลักเตรียมการผูกเอเจนต์ย่อย thread: true ผ่านอะแดปเตอร์การผูกเซสชันของช่องทางก่อนที่ subagent_spawned จะทำงาน
  • subagent_spawned มี resolvedModel และ resolvedProvider เมื่อ OpenClaw ระบุโมเดลดั้งเดิมของเซสชันลูกได้แล้วก่อนเริ่มต้น
  • subagent_ended มี targetSessionKey (ข้อมูลประจำตัว - ตรงกับ subagent_spawned.childSessionKey), targetKind ("subagent" หรือ "acp"), reason, outcome ซึ่งเป็นตัวเลือก ("ok", "error", "timeout", "killed", "reset" หรือ "deleted"), error ซึ่งเป็นตัวเลือก, runId, endedAt, accountId และ sendFarewell โดย ไม่มี agentId หรือ childSessionKey; ใช้ targetSessionKey เพื่อเชื่อมโยงกับเหตุการณ์ subagent_spawned ที่ตรงกัน

วงจรชีวิต

ฮุก วัตถุประสงค์
gateway_start / gateway_stop เริ่มหรือหยุดบริการที่ Plugin เป็นเจ้าของพร้อมกับ Gateway
deactivate นามแฝงความเข้ากันได้ที่เลิกใช้แล้วสำหรับ gateway_stop; ใช้ gateway_stop ใน Plugin ใหม่
cron_reconciled กระทบยอดกับสถานะ Cron ทั้งหมดของ Gateway หลังเริ่มต้นหรือโหลดใหม่
cron_changed สังเกตการณ์การเปลี่ยนแปลงวงจรชีวิต Cron ที่ Gateway เป็นเจ้าของ (เพิ่ม อัปเดต ลบ เริ่ม เสร็จสิ้น กำหนดเวลา)
before_install ตรวจสอบวัสดุติดตั้ง Skills หรือ Plugin ที่จัดเตรียมไว้จากรันไทม์ Plugin ที่โหลดแล้ว

คำขอจับคู่ช่องทาง

ใช้ channel_pairing_requested เมื่อ Plugin ต้องแจ้งผู้ปฏิบัติงานหรือ เขียนบันทึกการตรวจสอบหลังจากผู้ส่ง DM ที่ยังไม่ได้จับคู่สร้างคำขอจับคู่ที่รอดำเนินการ ฮุกจะถูกเรียกเมื่อสร้างคำขอแล้ว การส่งการตอบกลับการจับคู่ผ่านช่องทาง จะไม่ล่าช้าเพราะตัวจัดการฮุกที่ทำงานช้าหรือล้มเหลว

typescript
api.on("channel_pairing_requested", async (event) => {  await notifyOperator({    text: `คำขอจับคู่ ${event.channel} ใหม่จาก ${event.senderId}: ${event.code}`,  });});

ฮุกนี้ใช้เพื่อสังเกตการณ์เท่านั้น โดยจะไม่อนุมัติ ปฏิเสธ ระงับ หรือเขียน การตอบกลับการจับคู่ใหม่ เพย์โหลดประกอบด้วยช่องทาง, accountId ซึ่งเป็นตัวเลือก, senderId ที่มีขอบเขตตามช่องทาง, code สำหรับการจับคู่ และข้อมูลเมตาของช่องทาง ให้ถือว่า รหัสการจับคู่เป็นข้อมูลรับรองการอนุมัติแบบใช้ครั้งเดียวที่ยังใช้งานอยู่ และส่งเฉพาะไปยัง ปลายทางของผู้ปฏิบัติงานที่เชื่อถือได้ ให้ถือว่า metadata เป็นข้อความข้อมูลประจำตัว ที่ผู้ส่งซึ่งไม่น่าเชื่อถือเป็นผู้ระบุ ฮุกไม่มีเนื้อหาหรือสื่อของข้อความขาเข้า

ฮุกรันไทม์สำหรับดีบัก

ใช้ before_model_resolve เพื่อเปลี่ยนผู้ให้บริการหรือโมเดลสำหรับรอบการทำงานของเอเจนต์ โดยจะ ทำงานก่อนการระบุโมเดล ส่วน llm_output จะทำงานหลังจากความพยายามใช้โมเดล สร้างเอาต์พุตของผู้ช่วยแล้วเท่านั้น

หากต้องการพิสูจน์โมเดลที่เซสชันใช้จริง ให้ตรวจสอบการลงทะเบียนรันไทม์ แล้ว ใช้ openclaw sessions หรือพื้นผิวเซสชัน/สถานะของ Gateway หากต้องการดีบัก เพย์โหลดของผู้ให้บริการ ให้เริ่ม Gateway ด้วย --raw-stream และ --raw-stream-path <path> เพื่อเขียนเหตุการณ์สตรีมดิบของโมเดลลงในไฟล์ jsonl

นโยบายการเรียกใช้เครื่องมือ

before_tool_call รับ:

  • event.toolName
  • event.params
  • event.toolKind และ event.toolInputKind ซึ่งเป็นตัวเลือก โดยเป็นตัวจำแนกที่โฮสต์มีอำนาจกำหนด สำหรับเครื่องมือที่ตั้งใจใช้ชื่อร่วมกัน ตัวอย่างเช่น การเรียก exec ในโหมดโค้ดชั้นนอกจะใช้ toolKind: "code_mode_exec" และมี toolInputKind: "javascript" | "typescript" เมื่อทราบภาษาของอินพุต
  • event.derivedPaths ซึ่งเป็นตัวเลือก เป็นคำใบ้เส้นทางเป้าหมายที่โฮสต์หาให้แบบพยายามอย่างดีที่สุด สำหรับซองเครื่องมือที่รู้จักกันดี เช่น apply_patch; เส้นทางเหล่านี้อาจ ไม่ครบถ้วนหรือประมาณสิ่งที่เครื่องมือจะเข้าถึงจริงกว้างเกินไป (เช่น เมื่ออินพุตผิดรูปแบบหรือไม่ครบถ้วน)
  • event.runId ซึ่งเป็นตัวเลือก
  • event.toolCallId ซึ่งเป็นตัวเลือก
  • ฟิลด์บริบท เช่น ctx.agentId, ctx.sessionKey, ctx.sessionId, ctx.runId, ctx.toolKind, ctx.toolInputKind และ ctx.trace สำหรับการวินิจฉัย
  • ctx.requester ซึ่งเป็นตัวเลือก คือผู้ร้องขอที่โฮสต์ระบุว่าเป็นผู้เริ่ม การเรียกใช้ข้อความปัจจุบัน โดยอาจมี channel, accountId, senderId, senderIsOwner และ roleIds แบบดั้งเดิมของผู้ให้บริการ ฟิลด์ที่ขาดหายไปหมายถึงยังไม่มีหลักฐาน ไม่ใช่คำยืนยันว่าไม่เป็นเช่นนั้น ให้ปฏิเสธโดยค่าเริ่มต้นเมื่อนโยบายกำหนดให้ต้องมีฟิลด์เหล่านั้น

สามารถส่งคืน:

typescript
type BeforeToolCallResult = {  params?: Record<string, unknown>;  block?: boolean;  blockReason?: string;  requireApproval?: {    title: string;    description: string;    severity?: "info" | "warning" | "critical";    timeoutMs?: number;    /** @deprecated การอนุมัติที่ยังไม่ได้ข้อยุติจะถูกปฏิเสธเสมอ */    timeoutBehavior?: "allow" | "deny";    allowedDecisions?: Array<"allow-once" | "allow-always" | "deny">;    pluginId?: string;    onResolution?: (      decision: "allow-once" | "allow-always" | "deny" | "timeout" | "cancelled",    ) => Promise<void> | void;  };};

ลักษณะการป้องกันสำหรับฮุกวงจรชีวิตแบบมีชนิด:

  • block: true เป็นผลลัพธ์สุดท้ายและข้ามตัวจัดการที่มีลำดับความสำคัญต่ำกว่า
  • block: false ถือว่าไม่มีการตัดสินใจ
  • params เขียนพารามิเตอร์ของเครื่องมือใหม่เพื่อใช้ดำเนินการ
  • requireApproval หยุดการเรียกใช้เอเจนต์ชั่วคราวและถามผู้ใช้ผ่านการอนุมัติของ Plugin /approve สามารถอนุมัติได้ทั้งการอนุมัติ exec และ Plugin ในรีเลย์ PreToolUse แบบดั้งเดิมของโหมดรายงานในเซิร์ฟเวอร์แอป Codex การดำเนินการนี้จะส่งต่อไปยัง คำขออนุมัติของเซิร์ฟเวอร์แอปที่ตรงกัน โปรดดู รันไทม์ชุดควบคุม Codex
  • block: true ที่มีลำดับความสำคัญต่ำกว่ายังคงบล็อกได้หลังจากฮุกที่มีลำดับความสำคัญสูงกว่า ร้องขอการอนุมัติแล้ว
  • onResolution รับผลการตัดสินใจที่ได้ข้อยุติแล้ว ได้แก่ allow-once, allow-always, deny, timeout หรือ cancelled

นโยบายที่รับรู้ผู้ส่งในไฟล์เดียว

ไฟล์ Plugin แบบสแตนด์อโลนสามารถเก็บนโยบายเฉพาะการปรับใช้ไว้ในโค้ด แทนการเพิ่มสคีมาการกำหนดค่าอีกชุด ตัวอย่างนี้อนุญาตให้เจ้าของใช้เครื่องมือทุกอย่าง ให้ผู้ดูแลที่กำหนดค่าไว้ใช้ชุดเครื่องมือและการดำเนินการกับข้อความแบบระมัดระวัง และเปิดให้ผู้ส่งที่ได้รับอนุญาตจากการกำหนดค่าช่องทางแล้วใช้ /fix:

typescript
 const AGENT_ID = "maintenance-agent";const MAINTAINER_SCOPES = [  {    channel: "discord",    accountId: "operations",    senderIds: new Set(["maintainer-user-id"]),    roleIds: new Set(["maintainer-role-id"]),  },];const MAINTAINER_TOOLS = new Set(["read", "web_fetch", "web_search", "session_status", "message"]);const MAINTAINER_MESSAGE_ACTIONS = new Set(["react", "reply", "thread-create", "thread-reply"]); export default definePluginEntry({  id: "maintenance-access",  name: "การเข้าถึงเพื่อบำรุงรักษา",  description: "ใช้นโยบายเครื่องมือที่รับรู้ผู้ส่งกับเอเจนต์บำรุงรักษา",  register(api) {    api.on("before_tool_call", (event, ctx) => {      if (ctx.agentId !== AGENT_ID) {        return;      }       const requester = ctx.requester;      if (requester?.senderIsOwner === true) {        return;      }       const maintainerScope = requester        ? MAINTAINER_SCOPES.find(            (scope) =>              scope.channel === requester.channel && scope.accountId === requester.accountId,          )        : undefined;      const isMaintainer =        maintainerScope !== undefined &&        ((requester?.senderId !== undefined && maintainerScope.senderIds.has(requester.senderId)) ||          requester?.roleIds?.some((roleId) => maintainerScope.roleIds.has(roleId)) === true);      if (!isMaintainer) {        return { block: true, blockReason: "ต้องมีสิทธิ์เข้าถึงของผู้ดูแล" };      }       if (event.toolName === "message") {        const action = typeof event.params.action === "string" ? event.params.action : "";        if (MAINTAINER_MESSAGE_ACTIONS.has(action)) {          return;        }        return { block: true, blockReason: `ต้องเป็นเจ้าของจึงจะใช้ message.${action || "unknown"} ได้` };      }       if (MAINTAINER_TOOLS.has(event.toolName)) {        return;      }      return { block: true, blockReason: `ต้องเป็นเจ้าของจึงจะใช้ ${event.toolName} ได้` };    });     api.registerCommand({      name: "fix",      description: "ขอให้เอเจนต์บำรุงรักษาตรวจสอบและแก้ไขปัญหา",      acceptsArgs: true,      requireAuth: true,      handler: async (ctx) =>        ctx.agentId === AGENT_ID          ? { continueAgent: true }          : { text: "คำสั่งนี้ใช้ได้เฉพาะในการสนทนาสำหรับการบำรุงรักษาเท่านั้น" },    });  },});

โหลดไฟล์โดยตรงแล้วเริ่ม Gateway ใหม่:

json5
{  agents: {    list: [      {        id: "maintenance-agent",        workspace: "~/.openclaw/workspace-maintenance",      },    ],  },  bindings: [    {      agentId: "maintenance-agent",      match: {        channel: "discord",        accountId: "operations",        peer: { kind: "channel", id: "maintenance-channel-id" },      },    },  ],  plugins: {    load: { paths: ["~/.openclaw/policies/maintenance-access.ts"] },  },}

AGENT_ID ต้องระบุชื่อเอเจนต์ที่ผูกกับการสนทนาสำหรับการบำรุงรักษา การผูกจะเลือกเอเจนต์นั้นสำหรับข้อความปกติและ /fix; ไฟล์แบบสแตนด์อโลน ยังคงเป็นเจ้าของเพียงรายเดียวของนโยบายเครื่องมือที่แบ่งแยกระหว่างเจ้าของกับผู้ดูแล

requireAuth: true ใช้การรับผู้ส่งที่มีอยู่ของแต่ละช่องทางซ้ำ สำหรับ Discord รายการอนุญาต users/roles ของกิลด์หรือช่องทางสามารถให้สิทธิ์ กลุ่มผู้ใช้สำหรับการบำรุงรักษาได้ ช่องทางอื่นสามารถใช้รหัสผู้ส่งที่คงที่ จากนั้นฮุก จะใช้การตัดสินใจที่ละเอียดกว่าในระดับเครื่องมือกับทุกการเรียกใช้เครื่องมือระหว่างการทำงาน รวมถึงการเรียก PreToolUse แบบดั้งเดิมของ Codex ฮุกสามารถยับยั้งเครื่องมือที่โมเดลมองเห็นได้ แต่ไม่สามารถ เพิ่มเครื่องมือที่โฮสต์ละไว้ได้ นโยบายแซนด์บ็อกซ์ การอนุมัติ exec เครื่องมือหลัก สำหรับเจ้าของเท่านั้น และนโยบายช่องทางที่มีอยู่ยังคงมีผล ฮุกไม่สามารถให้สิทธิ์ข้ามนโยบายเหล่านั้นได้

กำหนดขอบเขตรหัสผู้ส่งและรหัสบทบาทให้ตรงกับคู่ช่องทาง/บัญชีอย่างแม่นยำตามที่แสดง ทั้งสองอย่างเป็น เนมสเปซภายในผู้ให้บริการ เก็บรายการอนุญาตให้รัดกุม เพิ่มเครื่องมือเขียนหรือ ดำเนินการเฉพาะเมื่อนโยบายแซนด์บ็อกซ์และการอนุมัติของการปรับใช้ ทำให้ปลอดภัยเท่านั้น สำหรับการทำงานแบบอัตโนมัติหรือของระบบ ให้ตัดสินใจอย่างชัดเจนว่า ควรอนุญาตเมื่อไม่มี ctx.requester หรือไม่ ตัวอย่างนี้ปฏิเสธกรณีดังกล่าวสำหรับเอเจนต์ที่กำหนดขอบเขตไว้

โปรดดู คำขอสิทธิ์ของ Plugin สำหรับ การกำหนดเส้นทางการอนุมัติ ลักษณะการตัดสินใจ และกรณีที่ควรใช้ requireApproval แทน เครื่องมือแบบเลือกได้หรือการอนุมัติ exec

Plugin ที่ต้องการนโยบายระดับโฮสต์สามารถลงทะเบียนนโยบายเครื่องมือที่เชื่อถือได้ด้วย api.registerTrustedToolPolicy(...) นโยบายเหล่านี้ทำงานก่อนฮุก before_tool_call ทั่วไปและก่อนการตัดสินใจของฮุกตามปกติ นโยบายที่เชื่อถือได้ ของ Plugin ที่รวมมาในชุดจะทำงานก่อน ตามด้วยนโยบายที่เชื่อถือได้ของ Plugin ที่ติดตั้ง ตามลำดับการโหลด Plugin ส่วนฮุก before_tool_call ทั่วไปจะทำงานหลังจากนั้น Plugin ที่รวมมาในชุด ยังคงใช้เส้นทางนโยบายที่เชื่อถือได้เดิม Plugin ที่ติดตั้งต้องเปิดใช้งานอย่างชัดเจน และประกาศรหัสนโยบายทุกรหัสใน contracts.trustedToolPolicies; รหัสที่ไม่ได้ประกาศ จะถูกปฏิเสธก่อนการลงทะเบียน รหัสนโยบายมีขอบเขตตาม Plugin ที่ลงทะเบียน ดังนั้น Plugin ต่างกันจึงสามารถใช้รหัสภายในเดียวกันซ้ำได้ ใช้ระดับนี้เฉพาะ สำหรับเกตที่โฮสต์เชื่อถือ เช่น นโยบายเวิร์กสเปซ การบังคับใช้งบประมาณ หรือ ความปลอดภัยของเวิร์กโฟลว์ที่สงวนไว้

ฮุกสภาพแวดล้อม Exec

resolve_exec_env ช่วยให้ Plugin เพิ่มตัวแปรสภาพแวดล้อมให้กับการเรียกใช้เครื่องมือ exec ก่อนรันคำสั่ง โดยจะได้รับ:

  • event.sessionKey
  • event.toolName ซึ่งปัจจุบันเป็น "exec" เสมอ
  • event.host ซึ่งเป็นหนึ่งใน "gateway", "sandbox" หรือ "node"
  • ฟิลด์บริบท เช่น ctx.agentId, ctx.sessionKey, ctx.messageProvider และ ctx.channelId

ส่งคืน Record<string, string> เพื่อผสานเข้ากับสภาพแวดล้อม Exec ตัวจัดการจะทำงาน ตามลำดับความสำคัญ โดยผลลัพธ์ที่มาทีหลังจะแทนที่ผลลัพธ์ก่อนหน้าสำหรับคีย์เดียวกัน

เอาต์พุตของฮุกจะถูกกรองตามนโยบายคีย์สภาพแวดล้อม Exec ของโฮสต์ก่อน ผสาน PATH จะถูกตัดออกเสมอ (การแก้ไขพาธคำสั่งและการตรวจสอบไบนารีที่ปลอดภัย ขึ้นอยู่กับค่านี้) คีย์ที่ไม่ถูกต้องและคีย์อันตรายที่เขียนทับค่าของโฮสต์ เช่น LD_*, DYLD_*, NODE_OPTIONS, ตัวแปรพร็อกซี (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, NO_PROXY) และตัวแปรเขียนทับ TLS (NODE_TLS_REJECT_UNAUTHORIZED, SSL_CERT_FILE และตัวแปรที่คล้ายกัน) จะถูกตัดออก สภาพแวดล้อมของ Plugin ที่ผ่านการกรองจะรวมอยู่ ในข้อมูลเมตาการอนุมัติ/ตรวจสอบของ Gateway และส่งต่อไปยังคำขอดำเนินการ บนโฮสต์ Node

การคงผลลัพธ์ของเครื่องมือ

ผลลัพธ์ของเครื่องมือสามารถมี details แบบมีโครงสร้างสำหรับการเรนเดอร์ UI การวินิจฉัย การกำหนดเส้นทางสื่อ หรือข้อมูลเมตาที่ Plugin เป็นเจ้าของ ให้ถือว่า details เป็นข้อมูลเมตาขณะทำงาน ไม่ใช่เนื้อหาพรอมต์:

  • OpenClaw จะตัด toolResult.details ออกก่อนเล่นซ้ำไปยังผู้ให้บริการและก่อนป้อนข้อมูลให้ Compaction เพื่อไม่ให้ข้อมูลเมตากลายเป็นบริบทของโมเดล
  • รายการเซสชันที่คงไว้จะเก็บเฉพาะ details ที่มีขนาดจำกัด รายละเอียดที่ใหญ่เกินไปจะถูก แทนที่ด้วยข้อมูลสรุปแบบย่อและ persistedDetailsTruncated: true
  • tool_result_persist และ before_message_write จะทำงานก่อน ขีดจำกัดขั้นสุดท้ายของการคงข้อมูล รักษา details ที่ส่งคืนให้มีขนาดเล็ก และหลีกเลี่ยงการใส่ ข้อความที่เกี่ยวข้องกับพรอมต์ไว้เฉพาะใน details; ให้ใส่เอาต์พุตเครื่องมือที่โมเดลมองเห็นได้ไว้ใน content

ฮุกพรอมต์และโมเดล

ใช้ฮุกเฉพาะระยะต่อไปนี้สำหรับ Plugin ใหม่:

  • before_model_resolve: รับเฉพาะพรอมต์ปัจจุบันและข้อมูลเมตาของไฟล์แนบ ส่งคืน providerOverride หรือ modelOverride
  • agent_turn_prepare: รับพรอมต์ปัจจุบัน ข้อความเซสชันที่จัดเตรียมไว้ และการแทรกข้อมูลในคิวแบบครั้งเดียวที่ระบายออกมาสำหรับเซสชันนี้ ส่งคืน prependContext หรือ appendContext
  • before_prompt_build: รับพรอมต์ปัจจุบันและข้อความเซสชัน ส่งคืน prependContext, appendContext, systemPrompt, prependSystemContext หรือ appendSystemContext
  • heartbeat_prompt_contribution: ทำงานเฉพาะในรอบ Heartbeat และส่งคืน prependContext หรือ appendContext มีไว้สำหรับตัวตรวจสอบเบื้องหลังที่ ต้องสรุปสถานะปัจจุบันโดยไม่เปลี่ยนแปลงรอบที่ผู้ใช้เป็นผู้เริ่ม

before_agent_run ทำงานหลังสร้างพรอมต์และก่อนอินพุตใด ๆ เข้าสู่โมเดล รวมถึงการโหลดรูปภาพเฉพาะพรอมต์และการสังเกตการณ์ llm_input โดยรับ อินพุตผู้ใช้ปัจจุบันเป็น prompt พร้อมประวัติเซสชันที่โหลดแล้วใน messages และพรอมต์ระบบที่ใช้งานอยู่ ส่งคืน { outcome: "block", reason, message? } เพื่อหยุดการทำงานก่อนโมเดลอ่านพรอมต์ reason ใช้ภายใน ส่วน message เป็นข้อความทดแทนที่ผู้ใช้เห็น รองรับเฉพาะผลลัพธ์ pass และ block; รูปแบบการตัดสินใจที่ไม่รองรับจะปฏิเสธโดยค่าเริ่มต้น

เมื่อการทำงานถูกบล็อก OpenClaw จะจัดเก็บเฉพาะข้อความทดแทนใน message.content พร้อมข้อมูลเมตาการบล็อกที่ไม่ละเอียดอ่อน เช่น ID ของ Plugin ที่บล็อกและการประทับเวลา ข้อความต้นฉบับของผู้ใช้จะไม่ถูกเก็บไว้ในทรานสคริปต์ หรือบริบทในอนาคต เหตุผลการบล็อกภายในจะถือว่าเป็นข้อมูลละเอียดอ่อนและ ไม่รวมอยู่ในเพย์โหลดทรานสคริปต์ ประวัติ การกระจายข้อความ บันทึก และการวินิจฉัย การสังเกตการณ์ควรใช้ฟิลด์ที่ผ่านการทำให้ปลอดภัย เช่น ID ผู้บล็อก ผลลัพธ์ การประทับเวลา หรือหมวดหมู่ที่ปลอดภัย

ฮุกของรอบเอเจนต์รวมถึง agent_end จะมี event.runId เมื่อ OpenClaw สามารถ ระบุการทำงานที่ใช้งานอยู่ได้ โดยค่าเดียวกันจะอยู่ใน ctx.runId ด้วย การทำงานที่ขับเคลื่อนด้วย Cron จะแสดง ctx.jobId (ID ของงาน Cron ต้นทาง) ในบริบทของรอบเอเจนต์ด้วย เพื่อให้ฮุกจำกัดขอบเขตเมตริก ผลข้างเคียง หรือสถานะไว้กับงานตามกำหนดการงานหนึ่งได้ ctx.jobId ไม่ได้เป็นส่วนหนึ่งของบริบทเครื่องมือ before_tool_call

สำหรับการทำงานที่มาจากช่องทาง ctx.channel และ ctx.messageProvider จะระบุ พื้นผิวผู้ให้บริการ เช่น discord หรือ telegram ขณะที่ ctx.channelId คือ ตัวระบุเป้าหมายการสนทนาเมื่อ OpenClaw สามารถอนุมานได้จากคีย์เซสชัน หรือข้อมูลเมตาการส่งมอบ

เมื่อมีข้อมูลประจำตัวของผู้ส่ง บริบทฮุกเอเจนต์จะมีข้อมูลต่อไปนี้ด้วย:

  • ctx.senderId - ID ผู้ส่งที่กำหนดขอบเขตตามช่องทาง (เช่น Feishu open_id, ID ผู้ใช้ Discord) จะมีค่าเมื่อการทำงานมาจากข้อความผู้ใช้ที่มี ข้อมูลเมตาผู้ส่งที่ทราบ
  • ctx.chatId - ตัวระบุการสนทนาแบบเนทีฟของระบบขนส่ง (เช่น Feishu chat_id, Telegram chat_id) จะมีค่าเมื่อช่องทางต้นทาง มี ID การสนทนาแบบเนทีฟ
  • ctx.channelContext.sender.id - ID ผู้ส่งเดียวกับ ctx.senderId ภายใต้ ออบเจ็กต์ที่ช่องทางเป็นเจ้าของ ซึ่ง Plugin สามารถขยายด้วยฟิลด์เฉพาะช่องทางได้
  • ctx.channelContext.chat.id - ID การสนทนาเดียวกับ ctx.chatId ภายใต้ออบเจ็กต์ที่ช่องทางเป็นเจ้าของ ซึ่ง Plugin สามารถขยายด้วยฟิลด์เฉพาะ ช่องทางได้

แกนหลักกำหนดเฉพาะฟิลด์ id ที่ซ้อนอยู่ Plugin ช่องทางที่ส่งข้อมูลเมตา ผู้ส่งหรือแชตที่มีรายละเอียดมากขึ้นผ่านตัวช่วยขาเข้าสามารถเสริม PluginHookChannelSenderContext หรือ PluginHookChannelChatContext จาก openclaw/plugin-sdk/channel-inbound:

ts
declare module "openclaw/plugin-sdk/channel-inbound" {  interface PluginHookChannelSenderContext {    unionId?: string;    userId?: string;  }}

Plugin ช่องทางส่งฟิลด์เหล่านั้นผ่านตัวช่วย SDK ขาเข้า:

ts
buildChannelInboundEventContext({  // ...  channelContext: {    sender: { id: senderOpenId, unionId, userId },    chat: { id: chatId },  },});

ฟิลด์เหล่านี้เป็นฟิลด์เสริมและจะไม่มีอยู่ในการทำงานที่มาจากระบบ (Heartbeat, Cron, เหตุการณ์ Exec)

ctx.senderExternalId ยังคงอยู่ในฐานะฟิลด์ความเข้ากันได้ระดับซอร์สที่เลิกใช้แล้วสำหรับ Plugin รุ่นเก่า แกนหลักจะไม่กำหนดค่าให้ฟิลด์นี้ ข้อมูลประจำตัวผู้ส่งเฉพาะช่องทางใหม่ ควรอยู่ภายใต้ ctx.channelContext.sender ผ่านการเสริม โมดูล

agent_end เป็นฮุกสังเกตการณ์ พาธ Gateway และพาธชุดทดสอบที่คงอยู่จะเรียกใช้ แบบไม่รอผลหลังจบรอบ ขณะที่พาธ CLI แบบครั้งเดียวที่มีอายุสั้นจะรอ พรอมิสของฮุกก่อนล้างกระบวนการ เพื่อให้ Plugin ที่เชื่อถือได้สามารถส่งข้อมูล การสังเกตการณ์เทอร์มินัลให้เสร็จหรือบันทึกสถานะได้ ตัวเรียกใช้ฮุกกำหนดเวลาหมดที่ 30 วินาที เพื่อไม่ให้ Plugin หรือปลายทางการฝังที่ค้างทำให้พรอมิสของฮุก ค้างอยู่ตลอดไป ระบบจะบันทึกเวลาหมดและ OpenClaw จะทำงานต่อ โดยจะไม่ ยกเลิกงานเครือข่ายที่ Plugin เป็นเจ้าของ เว้นแต่ Plugin จะใช้สัญญาณยกเลิก ของตนเองด้วย

ใช้ model_call_started และ model_call_ended สำหรับเทเลเมทรีการเรียกผู้ให้บริการ ที่ไม่ควรได้รับพรอมต์ดิบ ประวัติ การตอบกลับ ส่วนหัว เนื้อหาคำขอ หรือ ID คำขอของผู้ให้บริการ ฮุกเหล่านี้มีข้อมูลเมตาที่เสถียร เช่น runId, callId, provider, model, api/transport ที่เป็นทางเลือก, durationMs/outcome ขั้นสุดท้าย และ upstreamRequestIdHash เมื่อ OpenClaw สามารถอนุมาน แฮช ID คำขอของผู้ให้บริการที่มีขนาดจำกัดได้ เมื่อรันไทม์แก้ไข ข้อมูลเมตาหน้าต่างบริบทแล้ว เหตุการณ์และบริบทของฮุกจะมี contextTokenBudget ซึ่งเป็นงบประมาณโทเค็นที่มีผลหลังใช้ขีดจำกัด ของโมเดล/การกำหนดค่า/เอเจนต์ รวมถึง contextWindowSource และ contextWindowReferenceTokens เมื่อมีการใช้ ขีดจำกัดที่ต่ำกว่า

before_agent_finalize ทำงานเฉพาะเมื่อชุดทดสอบกำลังจะยอมรับคำตอบสุดท้ายตามธรรมชาติ จากผู้ช่วย ไม่ใช่พาธการยกเลิก /stop และจะไม่ ทำงานเมื่อผู้ใช้ยกเลิกรอบ ส่งคืน { action: "revise", reason } เพื่อขอ ให้ชุดทดสอบเรียกโมเดลอีกหนึ่งรอบก่อนสรุปผล, { action: "finalize", reason? } เพื่อบังคับสรุปผล หรือไม่ส่งคืนผลลัพธ์เพื่อดำเนินการต่อ ตัวจัดการมีงบประมาณเริ่มต้น 15 วินาที เมื่อหมดเวลา OpenClaw จะบันทึกความล้มเหลวและ ดำเนินการต่อด้วยคำตอบสุดท้ายเดิม ฮุก Stop แบบเนทีฟของ Codex จะถูกถ่ายทอดไปยังฮุกนี้ในฐานะการตัดสินใจ before_agent_finalize ของ OpenClaw

เมื่อส่งคืน action: "revise" Plugin สามารถรวมข้อมูลเมตา retry เพื่อ ทำให้การเรียกโมเดลเพิ่มเติมมีขอบเขตจำกัดและปลอดภัยต่อการเล่นซ้ำ:

typescript
type BeforeAgentFinalizeRetry = {  instruction: string;  idempotencyKey?: string;  maxAttempts?: number;};

instruction จะถูกต่อท้ายเหตุผลการแก้ไขที่ส่งไปยังชุดทดสอบ idempotencyKey ช่วยให้โฮสต์นับการลองใหม่สำหรับคำขอ Plugin เดียวกัน ตลอดการตัดสินใจสรุปผลที่เทียบเท่ากัน และ maxAttempts จำกัดจำนวนรอบเพิ่มเติม ที่โฮสต์จะอนุญาตก่อนดำเนินการต่อด้วยคำตอบสุดท้ายตามธรรมชาติ

Plugin ที่ไม่ได้รวมมากับระบบและต้องการฮุกการสนทนาดิบ (before_model_resolve, before_agent_reply, llm_input, llm_output, before_agent_finalize, agent_end หรือ before_agent_run) ต้องตั้งค่า:

json
{  "plugins": {    "entries": {      "my-plugin": {        "hooks": {          "allowConversationAccess": true        }      }    }  }}

ฮุกที่เปลี่ยนแปลงพรอมต์และการแทรกข้อมูลถาวรในรอบถัดไปสามารถปิดใช้งานแยกตาม Plugin ได้ด้วย plugins.entries.<id>.hooks.allowPromptInjection=false

ส่วนขยายเซสชันและการแทรกข้อมูลในรอบถัดไป

Plugin เวิร์กโฟลว์สามารถคงสถานะเซสชันขนาดเล็กที่เข้ากันได้กับ JSON ด้วย api.session.state.registerSessionExtension(...) และอัปเดตผ่านเมธอด sessions.pluginPatch ของ Gateway แถวเซสชันจะฉายสถานะส่วนขยายที่ลงทะเบียนไว้ผ่าน pluginExtensions ทำให้ Control UI และไคลเอ็นต์อื่น สามารถเรนเดอร์สถานะที่ Plugin เป็นเจ้าของได้โดยไม่ต้องรับรู้รายละเอียดภายในของ Plugin api.registerSessionExtension(...) ยังคงทำงานได้แต่เลิกใช้แล้ว โดยแนะนำให้ใช้ เนมสเปซ api.session.state แทน

ใช้ api.session.workflow.enqueueNextTurnInjection(...) เมื่อ Plugin ต้องการ ให้บริบทถาวรไปถึงรอบโมเดลถัดไปเพียงครั้งเดียว (ฟิลด์ระดับบนสุด api.enqueueNextTurnInjection(...) เป็นนามแฝงที่เลิกใช้แล้วและมี พฤติกรรมเดียวกัน) OpenClaw จะระบายการแทรกข้อมูลที่เข้าคิวไว้ก่อนฮุกพรอมต์ ตัด การแทรกข้อมูลที่หมดอายุ และขจัดรายการซ้ำตาม idempotencyKey แยกตาม Plugin นี่คือ จุดเชื่อมต่อที่เหมาะสมสำหรับการดำเนินการต่อหลังอนุมัติ ข้อมูลสรุปนโยบาย ความเปลี่ยนแปลงจากตัวตรวจสอบ เบื้องหลัง และการดำเนินคำสั่งต่อที่ควรให้โมเดลมองเห็นใน รอบถัดไป แต่ไม่ควรกลายเป็นข้อความถาวรในพรอมต์ระบบ

ความหมายของการล้างข้อมูลเป็นส่วนหนึ่งของสัญญา การล้างส่วนขยายเซสชันและ คอลแบ็กการล้างวงจรชีวิตรันไทม์จะได้รับ reset, delete, disable หรือ restart โฮสต์จะลบสถานะส่วนขยายเซสชันถาวร และการแทรกข้อมูลรอบถัดไปที่รอดำเนินการของ Plugin เจ้าของสำหรับการรีเซ็ต/ลบ/ปิดใช้งาน ส่วนการรีสตาร์ต จะรักษาสถานะเซสชันถาวรไว้ ขณะที่คอลแบ็กการล้างข้อมูลช่วยให้ Plugin ปล่อย งานตัวจัดกำหนดการ บริบทการทำงาน และทรัพยากรนอกแบนด์อื่น ๆ ของ รันไทม์รุ่นเก่าได้

ฮุกข้อความ

ใช้ฮุกข้อความสำหรับการกำหนดเส้นทางและนโยบายการส่งมอบระดับช่องทาง:

  • message_received: สังเกตเนื้อหาขาเข้า ผู้ส่ง threadId, messageId, senderId, ความสัมพันธ์ของการทำงาน/เซสชันที่เป็นทางเลือก และข้อมูลเมตา
  • message_sending: เขียน content ใหม่หรือส่งคืน { cancel: true }
  • reply_payload_sending: เขียนออบเจ็กต์ ReplyPayload ที่ทำให้เป็นมาตรฐานแล้วใหม่ (รวมถึง presentation, delivery, การอ้างอิงสื่อ และข้อความ) หรือส่งคืน { cancel: true }
  • message_sent: สังเกตความสำเร็จหรือความล้มเหลวขั้นสุดท้าย

สำหรับการตอบกลับ TTS แบบเสียงเท่านั้น content อาจมีทรานสคริปต์คำพูด ที่ซ่อนอยู่ แม้เพย์โหลดของช่องทางจะไม่มีข้อความ/คำบรรยายที่มองเห็นได้ การเขียน content ใหม่นั้นจะอัปเดตเฉพาะทรานสคริปต์ที่ฮุกมองเห็น โดยจะไม่ เรนเดอร์เป็นคำบรรยายสื่อ

เหตุการณ์ reply_payload_sending อาจมี usageState ซึ่งเป็นสแนปช็อตโมเดล/การใช้งาน/บริบท แบบสดต่อรอบตามความพยายามที่ดีที่สุด การส่งมอบแบบถาวร การเล่นซ้ำที่กู้คืนแล้ว และ การตอบกลับที่ไม่มีความสัมพันธ์กับการทำงานอย่างแม่นยำจะไม่มีข้อมูลนี้

บริบทของ message hook จะเปิดเผยฟิลด์สหสัมพันธ์ที่เสถียรเมื่อมี: ctx.sessionKey, ctx.runId, ctx.messageId, ctx.senderId, ctx.trace, ctx.traceId, ctx.spanId, ctx.parentSpanId และ ctx.callDepth นอกจากนี้ บริบทขาเข้า และ before_dispatch ยังเปิดเผยข้อมูลเมตาของการตอบกลับเมื่อช่องทาง มีข้อมูลข้อความที่อ้างถึงซึ่งผ่านการกรองตามสิทธิ์การมองเห็นแล้ว ได้แก่ replyToId, replyToIdFull, replyToBody, replyToSender และ replyToIsQuote ให้เลือกใช้ฟิลด์ระดับ เฟิร์สคลาสเหล่านี้ก่อนอ่านข้อมูลเมตาแบบเดิม

ให้เลือกใช้ฟิลด์ threadId และ replyToId ที่มีชนิดข้อมูลกำกับก่อนใช้ข้อมูลเมตา เฉพาะช่องทาง

กฎการตัดสินใจ:

  • message_sending ที่มี cancel: true ถือเป็นสถานะสิ้นสุด
  • message_sending ที่มี cancel: false ถือว่าไม่มีการตัดสินใจ
  • content ที่เขียนใหม่จะดำเนินต่อไปยัง hook ที่มีลำดับความสำคัญต่ำกว่า เว้นแต่ hook ในภายหลัง จะยกเลิกการส่ง
  • reply_payload_sending ทำงานหลังจากปรับ payload ให้เป็นมาตรฐานและก่อนส่งไปยังช่องทาง รวมถึงการตอบกลับที่กำหนดเส้นทางกลับไปยังช่องทางต้นทาง handler จะทำงานตามลำดับ และแต่ละ handler จะเห็น payload ล่าสุดที่สร้างโดย handler ที่มีลำดับความสำคัญสูงกว่า
  • payload ของ reply_payload_sending จะไม่เปิดเผยเครื่องหมายความน่าเชื่อถือขณะรัน เช่น trustedLocalMedia; Plugin สามารถแก้ไขรูปแบบ payload ได้ แต่ไม่สามารถมอบความน่าเชื่อถือ ของสื่อภายในเครื่อง
  • message_sending สามารถส่งคืน cancelReason และ metadata ที่มีขอบเขตจำกัดพร้อมกับ การยกเลิกได้ API วงจรชีวิตข้อความแบบใหม่จะแสดงผลนี้เป็นผลลัพธ์การส่ง ที่ถูกระงับ พร้อมเหตุผล cancelled_by_message_sending_hook; การส่งโดยตรง แบบเดิมยังคงส่งคืนอาร์เรย์ผลลัพธ์ว่างเพื่อความเข้ากันได้
  • message_sent ใช้สำหรับการสังเกตการณ์เท่านั้น ความล้มเหลวของ handler จะถูกบันทึกในล็อกและไม่ เปลี่ยนผลลัพธ์การส่ง

ติดตั้ง hook

ใช้ security.installPolicy สำหรับการตัดสินใจอนุญาต/บล็อกที่ผู้ปฏิบัติงานเป็นเจ้าของ นโยบายดังกล่าวทำงานจากการกำหนดค่า OpenClaw ครอบคลุมเส้นทางการติดตั้งและอัปเดตผ่าน CLI และ ปฏิเสธโดยอัตโนมัติเมื่อเปิดใช้งานแต่ไม่พร้อมใช้งาน

before_install เป็น hook ของวงจรชีวิตรันไทม์ Plugin โดยจะทำงานหลังจาก security.installPolicy เฉพาะภายในโปรเซส OpenClaw ที่โหลด hook ของ Plugin เรียบร้อยแล้ว เช่น ขั้นตอนการติดตั้งที่ทำงานผ่าน Gateway ซึ่งมีประโยชน์สำหรับ การสังเกตการณ์ คำเตือน และการตรวจสอบความเข้ากันได้ที่ Plugin เป็นเจ้าของ แต่ไม่ใช่ ขอบเขตความปลอดภัยหลักระดับองค์กรหรือโฮสต์สำหรับการติดตั้ง ฟิลด์ builtinScan ยังคงอยู่ใน payload ของเหตุการณ์เพื่อความเข้ากันได้ แต่ OpenClaw จะไม่เรียกใช้การบล็อกโค้ดอันตรายขณะติดตั้งที่มีมาให้ในตัวอีกต่อไป ดังนั้นจึง เป็นผลลัพธ์ ok ที่ว่างเปล่า ส่งคืนข้อค้นพบเพิ่มเติมหรือ { block: true, blockReason } เพื่อหยุดการติดตั้งในโปรเซสนั้น

block: true ถือเป็นสถานะสิ้นสุด block: false ถือว่าไม่มีการตัดสินใจ ความล้มเหลวของ handler จะบล็อกการติดตั้งตามหลักปฏิเสธโดยอัตโนมัติ

วงจรชีวิต Gateway

ใช้ gateway_start เพื่อเริ่มบริการทั่วไปของ Plugin และ gateway_stop เพื่อ ล้างทรัพยากรที่ทำงานระยะยาว ตัวจัดกำหนดการ Cron อาจยังอยู่ระหว่างการโหลดเมื่อ gateway_start ทำงาน ดังนั้นอย่าใช้เป็นสัญญาณฐานสำหรับการฉายภาพ Cron ภายนอก

อย่าพึ่งพา hook ภายใน gateway:startup สำหรับบริการรันไทม์ ที่ Plugin เป็นเจ้าของ

cron_reconciled จะทำงานหลังจากตัวจัดกำหนดการ Cron ของ Gateway และตัวเฝ้าดูเมื่อออก กระทบยอดสถานะถาวรเรียบร้อยแล้ว โดยจะทำงานทั้งในการเริ่มต้น ครั้งแรกและการแทนที่ตัวจัดกำหนดการระหว่างการโหลดการกำหนดค่าใหม่ เหตุการณ์จะรายงาน reason (startup หรือ reload) และสถานะ enabled ที่มีผล แม้ปิดใช้งาน Cron ก็ยังปล่อยเหตุการณ์พร้อม enabled: false ทำให้การฉายภาพภายนอกสามารถ ล้างการปลุกที่ล้าสมัยได้ ใช้ ctx.getCron?.() สำหรับอินสแตนซ์ตัวจัดกำหนดการที่แน่นอนซึ่ง กระทบยอดเสร็จสิ้น การโหลดใหม่ในภายหลังจะไม่เปลี่ยนเป้าหมายของ callback นั้น ctx.abortSignal เป็นเจ้าของสแนปช็อตตัวจัดกำหนดการเดียวกัน Gateway จะยกเลิกสัญญาณทันที ที่ตัวจัดกำหนดการใหม่พร้อมทำงานหรือเมื่อเริ่มปิดระบบ ส่งสัญญาณนี้ผ่านทุก ผลข้างเคียงแบบถาวร และอย่ายอมรับสแนปช็อตหลังจากสัญญาณถูกยกเลิก นี่เป็นสัญญาณวงจรชีวิตของตัวจัดกำหนดการ ไม่ใช่สัญญาณการเปิดใช้งาน Plugin: การโหลดใหม่แบบร้อนเฉพาะ Plugin จะไม่เรียกซ้ำ ผู้ใช้ที่เพิ่งเปิดใช้งานจะได้รับ ข้อมูลฐานชุดแรกในการแทนที่ตัวจัดกำหนดการครั้งถัดไปหรือเมื่อ Gateway เริ่มทำงาน

เช่นเดียวกับ hook สำหรับการสังเกตการณ์อื่นๆ callback ของ gateway_start และ cron_reconciled อาจทำงานทับซ้อนกัน หาก handler ทั้งสองใช้การเริ่มต้น Plugin ร่วมกัน ให้ประสานงาน ด้วย readiness promise ภายใน Plugin แทนการพึ่งพาลำดับ callback

cron_changed จะทำงานสำหรับเหตุการณ์วงจรชีวิต Cron ที่ Gateway เป็นเจ้าของ โดยมี payload เหตุการณ์ที่มีชนิดข้อมูลกำกับ ครอบคลุมเหตุผล added, updated, removed, started, finished และ scheduled เหตุการณ์จะมีสแนปช็อต PluginHookGatewayCronJob (รวมถึง state.nextRunAtMs, state.lastRunStatus และ state.lastError เมื่อมี) พร้อมกับ PluginHookGatewayCronDeliveryStatus ของ not-requested | delivered | not-delivered | unknown เหตุการณ์ที่นำออก จะเกิดหลังการคอมมิต โดยจะทำงานหลังจากลบข้อมูลถาวรสำเร็จเท่านั้น และยังคงมี สแนปช็อตงานที่ถูกลบ เพื่อให้ตัวจัดกำหนดการภายนอกสามารถกระทบยอดสถานะได้

เหตุการณ์ scheduled เกิดหลังการคอมมิต โดยจะทำงานหลังจากการเขียนแบบถาวรสำเร็จ เปลี่ยนค่า nextRunAtMs ที่มีผลของงานที่มีอยู่เท่านั้น โดยไม่รวมเหตุการณ์วงจรชีวิต added, updated หรือ removed ที่ระบุชัดเจนของงานนั้น event.nextRunAtMs ระดับบนสุดคือการปลุกครั้งถัดไปที่คอมมิตแล้ว เมื่อไม่มีค่านี้ งานจะ ไม่มีการปลุกครั้งถัดไป ให้ถือว่าเหตุการณ์เหล่านี้เป็นคำใบ้สำหรับการกระทบยอด ไม่ใช่ล็อก เดลตาที่เรียงลำดับ ใช้เป็นคำใบ้ที่รวมกันได้เพื่ออ่านตัวจัดกำหนดการที่ cron_reconciled บันทึกไว้ล่าสุดอีกครั้ง อย่านำตัวจัดกำหนดการจากบริบท cron_changed มาใช้ ให้ OpenClaw เป็นแหล่งข้อมูลจริงสำหรับการตรวจสอบงานที่ถึงกำหนดและการดำเนินการ

การฉายภาพ Cron ภายนอกอย่างปลอดภัย

ฉายภาพสแนปช็อตการปลุกที่สมบูรณ์แทนการส่งต่อเดลตาของเหตุการณ์ Cron การดำเนินการ replaceAll ของอะแดปเตอร์ภายนอกต้องเป็นแบบอะตอมมิกและทำซ้ำได้โดยให้ผลลัพธ์เดิม และต้อง เสร็จสิ้นหลังจากโฮสต์ยอมรับสแนปช็อตแบบถาวรแล้วเท่านั้น นอกจากนี้ยังต้อง เคารพสัญญาณยกเลิกที่ให้มา หากสัญญาณถูกยกเลิกก่อนการยอมรับแบบถาวร อะแดปเตอร์ต้องไม่ยอมรับสแนปช็อตนั้น

รูปแบบนี้ทำให้มี worker สำหรับสถานะล่าสุดเพียงหนึ่งรายการกำลังทำงานอยู่ เฉพาะ cron_reconciled เท่านั้นที่นำอินสแตนซ์ตัวจัดกำหนดการมาใช้ ส่วน cron_changed เพียงขอให้ worker นั้นอ่าน อินสแตนซ์ที่เป็นแหล่งข้อมูลจริงอีกครั้ง ดังนั้นคำใบ้ที่มาช้าจึงไม่สามารถกู้คืนตัวจัดกำหนดการรุ่นเก่าได้ รีวิชันที่ใหม่กว่าจะยกเลิกความพยายามของโฮสต์ที่กำลังทำงาน ก่อนที่จะยอมรับสแนปช็อต ที่ล้าสมัยได้

typescript
  type ExternalWake = { jobId: string; runAtMs: number }; type ExternalWakeHost = {  replaceAll(wakes: readonly ExternalWake[], options: { signal: AbortSignal }): Promise<void>;  close(): Promise<void>;}; type CronReader = {  list(options: { includeDisabled: true }): Promise<    Array<{      id: string;      enabled?: boolean;      state?: { nextRunAtMs?: number };    }>  >;}; export function registerCronProjection(api: OpenClawPluginApi, host: ExternalWakeHost) {  const lifecycle = new AbortController();  let cron: CronReader | undefined;  let enabled = false;  let hasBaseline = false;  let reconciliationSignal: AbortSignal | undefined;  let requestedRevision = 0;  let appliedRevision = 0;  let worker = Promise.resolve();  let activeAttempt: AbortController | undefined;   const projectLatest = async () => {    let retryMs = 1_000;     while (!lifecycle.signal.aborted && appliedRevision < requestedRevision) {      const ownerSignal = reconciliationSignal;      if (!ownerSignal || ownerSignal.aborted) {        return;      }      const targetRevision = requestedRevision;      const attempt = new AbortController();      const signal = AbortSignal.any([lifecycle.signal, ownerSignal, attempt.signal]);      activeAttempt = attempt;       try {        const jobs = enabled && cron ? await cron.list({ includeDisabled: true }) : [];        if (signal.aborted || targetRevision !== requestedRevision) {          continue;        }        const wakes = jobs          .flatMap((job): ExternalWake[] => {            const runAtMs = job.enabled === false ? undefined : job.state?.nextRunAtMs;            return runAtMs === undefined ? [] : [{ jobId: job.id, runAtMs }];          })          .sort((a, b) => a.runAtMs - b.runAtMs || a.jobId.localeCompare(b.jobId));         await host.replaceAll(wakes, { signal });        if (signal.aborted || targetRevision !== requestedRevision) {          continue;        }        appliedRevision = targetRevision;        retryMs = 1_000;      } catch {        if (lifecycle.signal.aborted || ownerSignal.aborted) {          return;        }        if (attempt.signal.aborted) {          continue;        }        api.logger.warn(`การฉายภาพ Cron ภายนอกล้มเหลว จะลองอีกครั้งใน ${retryMs}ms`);        try {          await sleep(retryMs, undefined, { signal });        } catch {          if (lifecycle.signal.aborted) {            return;          }          if (attempt.signal.aborted) {            continue;          }        }        retryMs = Math.min(retryMs * 2, 30_000);      } finally {        if (activeAttempt === attempt) {          activeAttempt = undefined;        }      }    }  };   const requestProjection = () => {    const targetRevision = ++requestedRevision;    activeAttempt?.abort();    worker = worker.then(async () => {      if (!lifecycle.signal.aborted && appliedRevision < targetRevision) {        await projectLatest();      }    });    return worker;  };   api.on("cron_reconciled", (event, ctx) => {    const reconciledCron = ctx.getCron?.();    if (event.enabled && !reconciledCron) {      api.logger.warn("การกระทบยอด Cron ไม่ได้เปิดเผยตัวจัดกำหนดการ");      return;    }    cron = reconciledCron;    enabled = event.enabled;    hasBaseline = true;    reconciliationSignal = ctx.abortSignal;    return requestProjection();  });   api.on("cron_changed", () => {    if (hasBaseline) {      return requestProjection();    }  });   api.on("gateway_stop", async () => {    lifecycle.abort();    await worker;    await host.close();  });}

เมื่อ cron_reconciled รายงาน enabled: false เส้นทางเดียวกันจะเรียก replaceAll([]) และล้างการปลุกภายนอกที่ล้าสมัย การลองใหม่/การหน่วงเวลาแบบเพิ่มขึ้นในตัวอย่างนี้ อยู่ภายในโปรเซสและถือว่าความล้มเหลวของอะแดปเตอร์รันไทม์เป็นเหตุการณ์ชั่วคราว ให้ตรวจสอบ การกำหนดค่าที่ไม่ควรลองใหม่ก่อนลงทะเบียน OpenClaw ไม่มี outbox สำหรับผลกระทบของ hook ของ Plugin หากโปรเซสจบก่อนการยอมรับแบบถาวร การเริ่ม Gateway ครั้งถัดไปจะปล่อยสแนปช็อต cron_reconciled ใหม่ที่เป็นแหล่งข้อมูลจริง gateway_stop จะยกเลิกงานของโฮสต์ที่กำลังดำเนินอยู่ รอให้ worker เสร็จสิ้น แล้วจึง ปิดอะแดปเตอร์

การเลิกใช้งานที่กำลังจะมาถึง

พื้นผิวบางส่วนที่เกี่ยวข้องกับ hook ถูกเลิกใช้แล้วแต่ยังคงรองรับอยู่ ให้ย้าย ก่อนรุ่นหลักถัดไป:

  • เอนเวโลปช่องแบบข้อความธรรมดา ในแฮนด์เลอร์ inbound_claim และ message_received ให้อ่าน BodyForAgent และบล็อกบริบทผู้ใช้แบบมีโครงสร้าง แทนการแยกวิเคราะห์ข้อความเอนเวโลปแบบแบน ดู เอนเวโลปช่องแบบข้อความธรรมดา → BodyForAgent
  • subagent_spawning ยังคงมีไว้เพื่อความเข้ากันได้กับ Plugin รุ่นเก่า แต่ Plugin ใหม่ไม่ควรส่งคืนการกำหนดเส้นทางเธรดจากส่วนนี้ Core จะเตรียม การผูก subagent ของ thread: true ผ่านอะแดปเตอร์การผูกเซสชันของช่อง ก่อนที่ subagent_spawned จะทำงาน
  • deactivate ยังคงเป็นนามแฝงสำหรับการล้างข้อมูลที่เลิกใช้งานแล้วเพื่อความเข้ากันได้ จนถึง หลังวันที่ 2026-08-16 Plugin ใหม่ควรใช้ gateway_stop
  • onResolution ใน before_tool_call ตอนนี้ใช้ยูเนียนแบบระบุชนิด PluginApprovalResolution (allow-once / allow-always / deny / timeout / cancelled) แทน string แบบรูปแบบอิสระ
  • api.registerSessionExtension / api.enqueueNextTurnInjection ยังคง เป็นนามแฝงระดับบนสุดเพื่อความเข้ากันได้ Plugin ใหม่ควรใช้ api.session.state.registerSessionExtension(...) และ api.session.workflow.enqueueNextTurnInjection(...)

สำหรับรายการทั้งหมด ได้แก่ การลงทะเบียนความสามารถด้านหน่วยความจำ โปรไฟล์การคิดของผู้ให้บริการ ผู้ให้บริการการยืนยันตัวตนภายนอก ชนิดการค้นหาผู้ให้บริการ ตัวเข้าถึงรันไทม์ของงาน และการเปลี่ยนชื่อ command-authcommand-status โปรดดู การย้าย Plugin SDK → รายการที่เลิกใช้งานอยู่

เนื้อหาที่เกี่ยวข้อง

Was this useful?
On this page

On this page