ตัวจัดการภายนอก

ตัวจัดการภายนอกคือสะพานเชื่อมระหว่างโค้ดของคุณกับ Telegram เมื่อลอจิกของตัวสร้างไม่เพียงพอ คุณมอบรีแอคชันที่แยกต่างหากให้กับเซิร์ฟเวอร์ของคุณ: เซิร์ฟเวอร์รับเหตุการณ์และตอบด้วยคำสั่งในรูปแบบ Telegram Bot API ในขณะที่ GetMyBot จัดการการขนส่ง การส่งมอบ และ Telegram ทั้งหมด

คืออะไร

รีแอคชันใน GetMyBot สร้างด้วยภาพ: ทริกเกอร์ → เงื่อนไข → การกระทำ ลอจิกที่ซับซ้อนหรือไม่ได้มาตรฐาน — ฐานข้อมูลของคุณเอง การคำนวณ การเรียกระบบอื่น ML สถานการณ์ที่มีหลายสาขา — นั้นยากที่จะแสดงออกในตัวสร้าง ตัวจัดการภายนอกขจัดเพดานนี้: คุณเชื่อมต่อแบ็กเอนด์ของคุณเองและมอบรีแอคชันทั้งหมดให้มันจัดการ

บริการภายนอกรับเหตุการณ์จาก GetMyBot (ทริกเกอร์ทำงาน ผู้ใช้ตอบข้อความ กดปุ่ม) และในการตอบสนองจะสั่งบอทว่าจะส่งอะไร GetMyBot ดำเนินการคำสั่งด้วยโทเค็นบอทของตัวเอง — rate-limit ลองใหม่ ยกเว้นซ้ำ และบันทึกไดอะล็อกทำงานอยู่เบื้องหลัง

โมเดลการทำงาน

  • GetMyBot คือ WebSocket เซิร์ฟเวอร์ บริการของคุณเชื่อมต่อกับ GetMyBot เองและไม่จำเป็นต้องมีที่อยู่สาธารณะหรือ webhook endpoint GetMyBot ไม่ได้สร้างการเชื่อมต่อขาออกไปยังบริการของคุณ ดังนั้นจึงไม่มีพื้นผิว SSRF
  • การพิสูจน์ตัวตนด้วยโทเค็นการเชื่อมต่อ บริการแสดงโทเค็นเมื่อเชื่อมต่อ GetMyBot ตรวจสอบกับแฮชและรักษาการเชื่อมต่อ
  • เหตุการณ์และคำสั่งในรูปแบบ Telegram Bot API บริการรับเหตุการณ์และตอบด้วยอาร์เรย์การเรียกในรูปแบบ «เมธอด + พารามิเตอร์» เหมือนกับการทำงานกับบอทปกติทุกประการ
  • GetMyBot ดำเนินการคำสั่งด้วยโทเค็นของตัวเอง บริการไม่เคยสื่อสารกับ Telegram โดยตรง: คำสั่งผ่านการตรวจสอบและเข้าสู่ outbox ภายในของ GetMyBot ซึ่งส่งด้วยโทเค็นบอทพร้อม rate-limit ลองใหม่ ยกเว้นซ้ำ และบันทึก

การเชื่อมต่อเดียวรองรับไดอะล็อกทั้งหมดของการเชื่อมต่อหนึ่ง — เหตุการณ์ของผู้ใช้ต่าง ๆ ถูกมัลติเพล็กซ์และแยกแยะด้วยช่อง session_id

การตั้งค่าการเชื่อมต่อ

  1. เปิดหัวข้อ การเชื่อมต่อ ในเมนูด้านซ้ายและคลิก เพิ่มการเชื่อมต่อ
  2. เลือกบริการ ตัวจัดการภายนอก และตั้งชื่อ
  3. หลังสร้างแล้ว การ์ดจะแสดง:
    • ที่อยู่ WebSocket — ในรูปแบบ wss://api.mybot.app/ext/ws/<integrationID> โดยที่ <integrationID> คือ UUID ของการเชื่อมต่อนี้ บริการของคุณเชื่อมต่อผ่านที่อยู่นี้ การเชื่อมต่อจะไม่ถูกสร้างหากไม่มี id ในเส้นทาง
    • โทเค็นการเชื่อมต่อ — แสดง ครั้งเดียว เมื่อสร้าง คัดลอกและบันทึกในที่ปลอดภัย ไม่สามารถดูโทเค็นซ้ำได้ (จัดเก็บเป็นแฮชเท่านั้น)
    • ออกโทเค็นใหม่ — ปุ่มสร้างโทเค็นใหม่และทำให้โทเค็นเก่าหมดอายุทันที หลังออกใหม่ ให้อัปเดตโทเค็นในบริการของคุณ
    • ตัวบ่งชี้ออนไลน์ — แสดงว่าบริการของคุณกำลังรักษาการเชื่อมต่อที่ใช้งานอยู่หรือไม่

พารามิเตอร์เพิ่มเติมของการเชื่อมต่อที่มีผลต่อโปรโตคอล:

  • session_ttl_seconds — ระยะเวลาของเซสชันเป็นวินาที (ค่าเริ่มต้น 3600)
  • on_unavailable_reaction_id — รีแอคชันสำรองที่ทำงานหากบริการออฟไลน์เมื่อรีแอคชันทำงาน (ดู ความเชื่อถือได้)

การผูกกับรีแอคชัน

ตัวจัดการภายนอกเชื่อมต่อไม่ใช่เป็นทริกเกอร์แยกต่างหาก แต่เป็น การกระทำ ในตัวแก้ไขรีแอคชัน ให้เพิ่มการกระทำ ส่งไปยังตัวจัดการภายนอก และเลือกการเชื่อมต่อที่ต้องการ

ทริกเกอร์ GetMyBot ที่มีอยู่ใด ๆ สามารถเปิดไดอะล็อกได้ — คำสั่ง ข้อความ การกดปุ่ม พารามิเตอร์จากลิงก์ คำขอเว็บขาเข้า ตารางเวลา เมื่อทริกเกอร์ดังกล่าวทำงานและถึงการกระทำ GetMyBot จะเปิดเซสชันพร็อกซีและส่งเหตุการณ์ session.open ให้บริการ

หากในขณะนั้นบริการไม่ได้เชื่อมต่อ GetMyBot จะเรียกใช้ รีแอคชันสำรอง จากการตั้งค่าการเชื่อมต่อ (on_unavailable_reaction_id) หากไม่ได้กำหนดรีแอคชันสำรอง — จะไม่มีอะไรเกิดขึ้น (เงียบ ไม่มีข้อผิดพลาดถึงผู้ใช้)

การเชื่อมต่อและการพิสูจน์ตัวตน

Endpoint: GET /ext/ws/{integrationID} ผ่าน WebSocket (wss) integrationID — UUID ของการเชื่อมต่อประเภท «ตัวจัดการภายนอก» จำกัดขนาดเฟรม — 256 KiB

สามารถพิสูจน์ตัวตนได้หนึ่งในสองวิธี

1. ส่วนหัวระหว่าง handshake (แนะนำ) ส่งโทเค็นในส่วนหัว Authorization:

Authorization: Bearer <token>

2. Auth เฟรม หาก Bearer header ไม่ได้ส่ง ให้ส่งเฟรมแรกภายใน 10 วินาที หลังสร้างการเชื่อมต่อ:

{ "type": "auth", "token": "<token>" }

หาก auth เฟรมไม่ถูกส่งภายใน 10 วินาที — การเชื่อมต่อจะปิดด้วยรหัส 4401 โทเค็นที่ไม่ถูกต้องในวิธีใดก็ตาม — ปิดด้วย 4401 เช่นกัน

โทเค็นถูกสร้างเมื่อสร้างการเชื่อมต่อ แสดงครั้งเดียวและจัดเก็บเป็นแฮชเท่านั้น (sha256) การตรวจสอบแบบ constant-time รองรับการหมุนเวียน:

POST /api/bots/{botID}/integrations/{integrationID}/rotate-token

Endpoint ส่งคืนโทเค็น plaintext ใหม่ โทเค็นเก่าจะหยุดทำงานทันที

เหตุการณ์: บอท → บริการ

แต่ละเหตุการณ์คือ JSON เฟรม (EventEnvelope) ช่อง type — หนึ่งใน: session.open, message, callback, session.cancel, session.expired

ชุดช่องทั้งหมดของ envelope:

{
  "type": "session.open",
  "session_id": "string",
  "bot_id": "string",
  "user": {
    "tg_user_id": 123456789,
    "first_name": "Ivan",
    "username": "ivan",
    "params": { "utm": "promo" }
  },
  "chat": { "id": 123456789, "type": "private" },
  "update": { "...": "raw Telegram Update (raw JSON)" },
  "event": { "...": "normalized GetMyBot event (raw JSON)" }
}
  • session_id — ตัวระบุเซสชัน (ไดอะล็อก)
  • bot_id — ตัวระบุบอท
  • user — ข้อมูลผู้ใช้: tg_user_id (int64) first_name ไม่บังคับ username และ params — พารามิเตอร์ผู้ใช้ (รวมถึงจากลิงก์) map คีย์→ค่า
  • chat — แชท: id (int64) และ type (เช่น "private")
  • update — raw Telegram Update (raw JSON) มีสำหรับ session.open, message, callback
  • event — เหตุการณ์ GetMyBot ที่ normalize แล้ว (raw JSON)

เนื้อหาแตกต่างกันตามประเภท:

  • session.open — ทริกเกอร์ทำงาน เปิดไดอะล็อกใหม่ มี update (raw Update) และ event (เหตุการณ์ normalize แบบสมบูรณ์) user.params ถูกกรอก
  • message — มาเมื่อบริการ «รอ» (expect = text/any) และผู้ใช้ส่งข้อความ เนื้อหาเหมือนกับ session.open
  • callback — ผู้ใช้กดปุ่มที่บริการส่งมาก่อนหน้า update เป็น raw; ใน user มีเพียง tg_user_id; event มีเฉพาะ callback_data ต้นฉบับที่บริการกำหนดในปุ่ม:
{ "callback_data": "<ค่าต้นฉบับที่บริการกำหนดในปุ่ม>" }
  • session.cancel / session.expired — envelope ขั้นต่ำ: session_id, bot_id, user.tg_user_id, chat.id (สำหรับ cancel ยังมี chat.type) ไม่มีช่อง update/event เหตุการณ์เหล่านี้ส่งได้ เฉพาะเมื่อบริการออนไลน์ — ไม่ถูกใส่ในคิวออฟไลน์

คำสั่ง: บริการ → บอท

คำสั่งคือ JSON เฟรม (CommandEnvelope) ช่อง type — หนึ่งใน: execute, session.close, auth, ping, pong

{
  "type": "execute",
  "session_id": "string",
  "methods": [
    { "method": "sendMessage", "params": { "text": "สวัสดีจากตัวจัดการภายนอก!" } }
  ],
  "expect": "none"
}
  • session_id — เซสชันที่คำสั่งเกี่ยวข้อง
  • token — เฉพาะสำหรับ type: "auth" (ดู การเชื่อมต่อและการพิสูจน์ตัวตน)
  • methods — อาร์เรย์ของออบเจกต์ { "method": "<ชื่อเมธอด Telegram Bot API>", "params": { ... } } ชื่อเมธอดตรงกับ Telegram Bot API (เช่น sendMessage) และ params คือออบเจกต์พารามิเตอร์ของเมธอดนั้น
  • expect — บริการรอการตอบกลับของผู้ใช้หรือไม่: "none", "text" หรือ "any" (ค่าว่างถือเป็น none)
  • session.close — ปิดไดอะล็อกอย่างชัดเจน

คำสั่ง ไม่ไปยัง Telegram โดยตรง: GetMyBot ตรวจสอบและใส่ใน outbox ของตัวเอง ซึ่งส่งด้วยโทเค็นบอท (พร้อม rate-limit ลองใหม่ ยกเว้นซ้ำ และบันทึก)

วิธีจัดการ execute

คำสั่ง execute จะถูกทิ้งทั้งหมดหากว่า: session_id ว่าง; เซสชันไม่พบหรือไม่อยู่ในสถานะ open; เซสชันเป็นของการเชื่อมต่ออื่น; เซสชันหมดอายุ

จากนั้น:

  • Rate-limit: สูงสุด 60 execute ต่อนาทีต่อเซสชัน (หน้าต่างเลื่อน) เกินกว่านั้น — ทิ้ง
  • สูงสุด 30 เมธอด ในหนึ่ง execute; เมธอดที่เกินจะถูกตัดทิ้งเงียบ ๆ
  • แต่ละเมธอดตรวจสอบ whitelist (ดู เมธอดที่อนุญาต) — เมธอดที่ไม่อยู่ในรายการจะถูกปฏิเสธ
  • Chat-guard: หาก params มี chat_id หรือ from_chat_id ที่มีค่าไม่เท่ากับ 0 และไม่เท่ากับ chat_id ของเซสชัน — เมธอดทั้งหมดจะถูกปฏิเสธ (ไม่สามารถส่งไปยังแชทอื่น) สามารถไม่ระบุ chat_id: GetMyBot จะกำหนดแชทของเซสชันให้โดยบังคับ
  • ปุ่ม inline ที่มี callback_data จะถูก tokenize โดยอัตโนมัติ (รูปแบบภายใน x:<token>); ระยะเวลาของปุ่ม = จนกว่าเซสชันจะหมดอายุ ปุ่ม url/webapp/switch_inline จะไม่ถูกแตะต้อง

เมธอดที่อนุญาต

บริการสั่งบอท ดังนั้นชุดเมธอดจำกัดด้วย whitelist — เฉพาะการส่งและทำงานกับข้อความของไดอะล็อกปัจจุบัน เมธอดที่ไม่อยู่ในรายการ จะถูกละเว้นเงียบ ๆ (ไม่ใช่ข้อผิดพลาด)

  • การส่ง: sendMessage, sendPhoto, sendDocument, sendVideo, sendAudio, sendMediaGroup, sendAnimation, sendVoice, sendLocation, sendChatAction
  • การแก้ไขและลบ: editMessageText, editMessageCaption, editMessageReplyMarkup, deleteMessage
  • การตอบการกด: answerCallbackQuery
  • การส่งต่อและคัดลอก: forwardMessage, copyMessage
  • การปักหมุด: pinChatMessage, unpinChatMessage

เมธอดระดับบัญชี (setWebhook, getUpdates, logOut, close, setMyCommands เป็นต้น) ไม่ได้รับอนุญาต

การรอและวงจรชีวิตของเซสชัน

หลังจากแต่ละ execute ชะตากรรมของเซสชันขึ้นอยู่กับ expect และว่ามีปุ่ม inline ที่มี callback_data หรือไม่:

  • expect = text หรือ any — ข้อความถัดไปของผู้ใช้จะถูกส่งไปยังบริการเป็นเหตุการณ์ message
  • expect = none และคำสั่งไม่มีปุ่ม inline ที่มี callback — เซสชันจะปิดโดยอัตโนมัติ นี่คือข้อความ «สิ้นสุด»
  • expect = none แต่มีปุ่ม callback — เซสชันยังคงเปิดอยู่: การกดปุ่มจะถูกจับ ตราบเท่าที่โทเค็นปุ่มยังมีชีวิต (จนถึง TTL)

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

ขอบเขตของไดอะล็อก

บริบทของไดอะล็อกมีชีวิตอยู่ตราบเท่าที่เซสชันเปิดอยู่:

  • การตอบกลับด้วยข้อความเมื่อรอใช้งาน (expect = text/any) — ส่งไปยังบริการเป็นเหตุการณ์ message ไม่ใช่เรียกใช้รีแอคชันปกติของบอท
  • การกดปุ่มที่ tokenize — ส่งไปยังบริการเป็นเหตุการณ์ callback พร้อม callback_data ต้นฉบับ GetMyBot ยืนยันการกดเสมอ (answerCallbackQuery); ตรวจสอบว่าการกดเป็นของเซสชัน (บอท + ผู้ใช้ + แชท) การกดปุ่ม ไม่ปิดเซสชัน
  • /cancel (รวมถึง /cancel@bot และ «ยกเลิก») เมื่อมีเซสชันภายนอกที่ใช้งานอยู่ — เซสชันจะปิด ส่ง session.cancel ให้บริการ และส่ง «ยกเลิกแล้ว» ให้ผู้ใช้
  • ไดอะล็อกเดียวต่อผู้ใช้ การเปิดเซสชันใหม่จะแทนที่เซสชันที่เปิดอยู่ก่อนหน้าของผู้ใช้ — โดยส่ง session.cancel ให้เซสชันนั้น
  • การหมดอายุ TTL เมื่อหมดเวลา เซสชันจะถูกทำเครื่องหมายว่า expired และ (หากบริการออนไลน์) จะส่ง session.expired ให้บริการ

ข้อจำกัดและหมดเวลา

  • ขนาดเฟรม: 256 KiB
  • TTL เซสชัน: ค่าเริ่มต้น 3600 วินาที (ปรับได้ด้วยพารามิเตอร์ session_ttl_seconds ของการเชื่อมต่อ)
  • ความถี่คำสั่ง: 60 execute ต่อนาทีต่อเซสชัน (หน้าต่างเลื่อน)
  • เมธอดในหนึ่ง execute: สูงสุด 30 (เมธอดที่เกินจะถูกทิ้ง)
  • เซสชันที่เปิดต่อการเชื่อมต่อ: สูงสุด 1000
  • คิวออฟไลน์: สูงสุด 100 เหตุการณ์ต่อเซสชัน
  • Heartbeat: ping ทุก 30 วินาที; หมดเวลาหากไม่มีกิจกรรม — 60 วินาที
  • หมดเวลา auth เฟรม: 10 วินาที

ความเชื่อถือได้ (heartbeat, ออฟไลน์, คิว, fallback)

  • Heartbeat เซิร์ฟเวอร์ส่งเฟรม {"type":"ping"} ทุก 30 วินาที หากไม่มีกิจกรรมจากไคลเอนต์นานกว่า 60 วินาที — การเชื่อมต่อจะปิด ไคลเอนต์สามารถส่ง ping/pong ของตัวเองเพื่อรักษากิจกรรม การตัดการเชื่อมต่อ ไม่ทำลายไดอะล็อก: เซสชันมีชีวิตอยู่ในฐานข้อมูลจนถึง TTL หรือการเชื่อมต่อใหม่
  • การเชื่อมต่อใหม่ — ฝั่งบริการ หากการเชื่อมต่อขาด บริการของคุณต้องเชื่อมต่อใหม่ เซสชันที่เปิดอยู่ในฐานข้อมูลจะรอดชีวิตจากการรีสตาร์ทจนกว่า TTL จะหมด
  • Fallback เมื่อออฟไลน์ตั้งแต่เริ่ม หากบริการไม่ได้เชื่อมต่อเมื่อรีแอคชันทำงาน จะเรียกใช้รีแอคชันสำรองจากการตั้งค่าการเชื่อมต่อ (on_unavailable_reaction_id) หากไม่ได้กำหนด — จะไม่มีอะไรเกิดขึ้น (เงียบ)
  • คิวออฟไลน์ เหตุการณ์ message/callback ที่ไม่ได้ส่งเนื่องจากออฟไลน์จะถูกใส่ในคิวออฟไลน์ (สูงสุด 100 เหตุการณ์ FIFO เก็บไว้จนเซสชันหมดอายุ) และจะถูกส่งเมื่อเชื่อมต่อใหม่ เหตุการณ์ session.open/session.cancel/session.expired ไม่ถูกใส่ในคิว
  • การรับประกันการส่ง — at-most-once เมื่อคิวเต็มหรือหมดอายุ เหตุการณ์จะถูกทิ้ง ออกแบบลอจิกให้การข้ามเหตุการณ์เดียวไม่ทำลายสถานการณ์

ตัวบ่งชี้ออนไลน์

ตรวจสอบว่าบริการกำลังรักษาการเชื่อมต่อที่ใช้งานอยู่ได้ผ่าน endpoint:

GET /api/bots/{botID}/integrations/{integrationID}/status

ผลลัพธ์:

{ "online": true }

ตัวบ่งชี้เดียวกันอยู่บนการ์ดการเชื่อมต่อในแดชบอร์ด ใน MCP เทียบเท่ากับเครื่องมือ get_integration_status

ความปลอดภัย

GetMyBot ดำเนินการคำสั่งของบริการภายนอกด้วยโทเค็นของตัวเอง ดังนั้นการป้องกันจึงเข้มงวด

  • ไม่มีพื้นผิว SSRF GetMyBot ทำหน้าที่เป็น WebSocket เซิร์ฟเวอร์ และไม่ได้สร้างการเชื่อมต่อขาออกไปยังบริการ — บริการเป็นผู้เชื่อมต่อกับ GetMyBot ไม่มี URL ที่ควบคุมจากภายนอกที่แพลตฟอร์มจะเรียกถึง
  • โทเค็นจัดเก็บเป็นแฮช (sha256) การตรวจสอบแบบ constant-time รองรับการหมุนเวียน
  • การแยกผู้เช่า คำสั่งผูกแน่นกับการเชื่อมต่อของตน เซสชันผูกกับบอท + ผู้ใช้ + แชท เหตุการณ์ของบอทอื่นจะไม่เข้าสู่ซ็อกเก็ตของคุณ
  • Whitelist เมธอด + บังคับ chat_id ป้องกันไม่ให้บอทกลายเป็นผู้ส่งข้อความไปยังแชทใด ๆ ก็ตาม
  • โทเค็นบอทไม่ถูกส่งออกไปภายนอก — บริการไม่เคยสื่อสารกับ Telegram โดยตรง
  • การปกปิดความลับ โทเค็นการเชื่อมต่อและค่าที่ละเอียดอ่อนจะถูกซ่อนในบันทึกและ log

ตัวอย่าง

ตัวจัดการขั้นต่ำ: เมื่อเปิดไดอะล็อกจะส่งข้อความและปิดเซสชัน (expect: "none") ใส่ id การเชื่อมต่อและโทเค็น ที่อยู่ต้องมี <integrationID> ในเส้นทางเสมอ

Node.js

import WebSocket from "ws";

const URL = `wss://api.mybot.app/ext/ws/${process.env.MYBOT_INTEGRATION_ID}`;
const ws = new WebSocket(URL, {
  headers: { Authorization: `Bearer ${process.env.MYBOT_TOKEN}` },
});

ws.on("message", (raw) => {
  const ev = JSON.parse(raw);
  if (ev.type === "session.open") {
    ws.send(JSON.stringify({
      type: "execute",
      session_id: ev.session_id,
      expect: "none",
      methods: [{ method: "sendMessage", params: { text: "สวัสดีจากตัวจัดการภายนอก!" } }],
    }));
  }
});

Python

import json, os, asyncio, websockets

async def main():
    url = f"wss://api.mybot.app/ext/ws/{os.environ['MYBOT_INTEGRATION_ID']}"
    headers = {"Authorization": f"Bearer {os.environ['MYBOT_TOKEN']}"}
    async with websockets.connect(url, additional_headers=headers) as ws:
        async for raw in ws:
            ev = json.loads(raw)
            if ev["type"] == "session.open":
                await ws.send(json.dumps({
                    "type": "execute",
                    "session_id": ev["session_id"],
                    "expect": "none",
                    "methods": [
                        {"method": "sendMessage", "params": {"text": "สวัสดีจากตัวจัดการภายนอก!"}}
                    ],
                }))

asyncio.run(main())

ปุ่ม inline และการจัดการ callback

เพื่อจับการกด ให้ส่งปุ่มที่มี callback_data (GetMyBot จะ tokenize โดยอัตโนมัติ) เมื่อใช้ expect: "none" กับปุ่ม เซสชันจะยังเปิดอยู่ตราบเท่าที่โทเค็นปุ่มยังมีชีวิต — การกดจะมาเป็นเหตุการณ์ callback โดยที่ event.callback_data เท่ากับค่าต้นฉบับ ไม่จำเป็นต้องยืนยันการกด (answerCallbackQuery) — GetMyBot ทำให้เอง

if (ev.type === "session.open") {
  ws.send(JSON.stringify({
    type: "execute",
    session_id: ev.session_id,
    expect: "none",
    methods: [{
      method: "sendMessage",
      params: {
        text: "กดปุ่ม",
        reply_markup: { inline_keyboard: [[{ text: "ไปเลย", callback_data: "go" }]] },
      },
    }],
  }));
} else if (ev.type === "callback" && ev.event.callback_data === "go") {
  ws.send(JSON.stringify({
    type: "execute",
    session_id: ev.session_id,
    expect: "none",
    methods: [{ method: "sendMessage", params: { text: "กดปุ่มแล้ว!" } }],
  }));
}

ขั้นตอนถัดไป