ส่งต่อให้เจ้าหน้าที่ (Mbox Agent Handoff)
ภาพรวม
"Mbox" คือระบบ live chat ที่เปิดให้เจ้าหน้าที่ซึ่งเป็นมนุษย์เข้ามาสนทนากับผู้ใช้ LINE แทนบอท โดยเบื้องหลังใช้ Chatwoot ฟีเจอร์นี้คือ logic ที่ตัดสินใจว่าข้อความหนึ่ง ๆ ควรให้บอทตอบหรือควรส่งต่อให้เจ้าหน้าที่ ซึ่งต้องอยู่ใน webhook-go เพราะจำเป็นต้องตัดสินใจ ก่อน ที่ข้อความจะไหลเข้าสู่บอท
หัวใจของกลไกนี้คือ Redis key agent_mode:{lineOaId}:{userId} โดยการมี key อยู่หมายความว่า
ผู้ใช้กำลังคุยกับเจ้าหน้าที่ ในช่วงเวลานั้นบอทต้องเงียบสนิท คือไม่ publish ลงคิว line_webhook
ทั้งหมดนี้ทำงานเฉพาะเมื่อ webhook_config.mboxEnabled == "1" เท่านั้น
หากปิดอยู่ event จะไหลผ่านไปยังเส้นทาง default ตามปกติ
Business Flow
A. Message event
ระบบพิจารณาเฉพาะ event แรก ที่เป็น message และอ่าน message.text แบบ trim
พร้อมแปลงเป็นตัวพิมพ์เล็ก
- คำนวณ
lineOaIdจาก config และดึงuserIdจากevent.source.userId - อ่าน
agent_modeด้วยHGETALLโดยถือว่าอยู่ใน agent mode เมื่อ hash ไม่ว่าง หากอ่าน error จะ log แล้วหยุดทั้ง pipeline - parse keyword 2 ชุดจาก config คือ
mboxExitKeywordsและmboxAgentKeywordsซึ่งเป็น JSON array ของ string และถูกแปลงเป็นตัวพิมพ์เล็กทั้งหมด หาก parse ไม่ได้จะ fallback ไปอ่านแบบ comma-separated พร้อมตัด whitespace และค่าว่างทิ้ง
กรณี A1 — ผู้ใช้อยู่ใน agent mode อยู่แล้ว
- หากข้อความตรงกับ exit keyword จะ publish ลงคิว
mbox_handoffด้วยtype: "exit"จากนั้น worker-go จะไปปิด conversation ใน Chatwoot และลบagent_modekey - หากไม่ตรง จะ forward ข้อความไปยัง
mboxLineWebhookUrl(ดู ส่งต่อ Webhook ไปยังระบบลูกค้า) แล้วเขียนlastActivityเป็น epoch หน่วยมิลลิวินาทีลงในagent_modeโดยไม่ตั้ง TTL - ไม่ว่าจะเป็นทางใด ระบบจะ return ทันทีโดยไม่ publish ลง
line_webhookซึ่งมีผลเป็นการปิดปากบอท
กรณี A2 — ผู้ใช้ยังไม่อยู่ใน agent mode
- หากข้อความตรงกับ agent keyword เช่น "คุยกับเจ้าหน้าที่" จะเข้าสู่โหมด handoff
- เมื่อ
mboxDepartmentPickerEnabledไม่เท่ากับ"1"จะ publish ด้วยtype: "handoff"พร้อมmboxConfigปกติ - เมื่อ
mboxDepartmentPickerEnabledเท่ากับ"1"จะ publish ด้วยtype: "department_picker"พร้อมmboxConfigที่แนบ blockdepartmentPickerซึ่งประกอบด้วย enabled, headerText, generalLabel และ departments โดย departments parse มาจาก JSON string ในฟิลด์mboxDepartmentsหาก parse ไม่ได้จะได้ array ว่าง - จากนั้น return โดยไม่ส่งต่อให้บอท
- เมื่อ
- หากไม่ตรง keyword ใดเลย จะตกลงสู่เส้นทางปกติ คือ publish ลงคิว
line_webhookให้บอททำงาน
B. Postback event — การเลือกแผนก
- อ่าน
postback.dataของ postback event ตัวแรก - หากขึ้นต้นด้วย
mbox_team:จะดึงค่าที่อยู่หลังเครื่องหมาย colon มาเป็นteamId - หากมี
source.userIdจะ publish ลงคิวmbox_handoffด้วยtype: "handoff"พร้อมteamIdแล้ว return - หากไม่มี userId จะ ไม่ return แต่ปล่อยให้ตกไปตรวจเงื่อนไข
appt_cancel:ต่อ (ดู ยกเลิกนัดหมายผ่าน Postback)
รูปร่าง payload ที่ส่งลงคิว mbox_handoff
{
"type": "handoff | department_picker | exit",
"lineOaId": 123,
"userId": "LINE user id",
"teamId": "optional, เฉพาะกรณี mbox_team",
"webhookPayload": { "webhookId": "...", "headers": {}, "body": {} },
"mboxConfig": {
"enabled": true,
"baseUrl": "...",
"apiToken": "...",
"accountId": "...",
"inboxId": "...",
"lineWebhookUrl": "...",
"timeoutMinutes": 30,
"warningMinutes": 25,
"greetingMessage": "...",
"warningMessage": "...",
"endMessage": "...",
"timeoutMessage": "...",
"exitKeywords": [],
"agentKeywords": [],
"departmentPicker": {}
}
}
:::warning ความแตกต่างที่ไม่ได้ตั้งใจ (parity ที่ควรทราบ)
branch handoff ที่เกิดจาก agent keyword จะส่ง BodyRaw ซึ่งเป็น bytes ดิบไปด้วย
แต่ branch exit และ mbox_team ไม่ได้เซ็ต BodyRaw ทำให้ body ที่ออกไป
ถูก marshal ใหม่จาก map และมีการเรียง key ใหม่ ลายเซ็นจึงใช้ไม่ได้ในสองกรณีนี้
(ดู การส่งต่อ Raw Body และลายเซ็น)
:::
ไฟล์และฟังก์ชันหลัก
โค้ดทั้งหมดอยู่ใน internal/line/service.go โดยไม่มีแพ็กเกจแยกต่างหาก
และเข้าถึงผ่าน endpoint POST /api/line/:id
(ดู ประตูรับ Webhook จาก LINE)
| ฟังก์ชัน | หน้าที่ |
|---|---|
Service.ProcessLine | dispatcher หลัก ครอบคลุมทั้งขั้น message และขั้น postback |
buildMboxConfig | map ข้อมูลจาก Redis hash เข้าสู่ struct config |
Service.deptPickerConfig | ขยาย mboxConfig แล้วแนบ block departmentPicker |
parseKeywords | แปลง JSON array เป็น slice ของ string ตัวพิมพ์เล็ก และ fallback แบบ comma-separated |
contains, messageText, postbackData, sourceUserID, splitSecond | ฟังก์ชันช่วย |
type mboxHandoffPayload, mboxConfig, webhookPayload | โครงสร้าง payload |
จุดเชื่อมต่อกับ Service อื่น
- Redis — ใช้
webhook_configสำหรับ config mbox ทั้งหมด และagent_modeสำหรับสถานะ session (ดู Redis Cache ของ Webhook Config) - RabbitMQ — คิว
mbox_handoffที่กำหนดผ่านRABBITMQ_QUEUE_MBOX_HANDOFF - worker-go — เป็น consumer ของคิว
mbox_handoffทำหน้าที่สร้างและปิด conversation ใน Chatwoot สร้างและลบagent_modekey ส่งข้อความ greeting, warning, end และ timeout รวมถึงจัดการ timeout ของ session - Chatwoot (Mbox) — ระบบภายนอก โดย credential ทั้ง baseUrl, apiToken, accountId และ inboxId ถูกส่งไปกับ payload ทุกครั้ง service นี้ไม่ได้เรียก Chatwoot API เอง
- ขา callback กลับ — Chatwoot ยิงกลับมาที่
POST /api/mbox/callback/:oaHash(ดู รับ Callback จาก Chatwoot) - cms-api — หน้าตั้งค่า mbox ต่อ OA ทั้ง keyword ข้อความ และแผนก
อยู่ในโมดูล
line-oa-management