Skip to main content

ส่งต่อให้เจ้าหน้าที่ (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 พร้อมแปลงเป็นตัวพิมพ์เล็ก

  1. คำนวณ lineOaId จาก config และดึง userId จาก event.source.userId
  2. อ่าน agent_mode ด้วย HGETALL โดยถือว่าอยู่ใน agent mode เมื่อ hash ไม่ว่าง หากอ่าน error จะ log แล้วหยุดทั้ง pipeline
  3. parse keyword 2 ชุดจาก config คือ mboxExitKeywords และ mboxAgentKeywords ซึ่งเป็น JSON array ของ string และถูกแปลงเป็นตัวพิมพ์เล็กทั้งหมด หาก parse ไม่ได้จะ fallback ไปอ่านแบบ comma-separated พร้อมตัด whitespace และค่าว่างทิ้ง

กรณี A1 — ผู้ใช้อยู่ใน agent mode อยู่แล้ว

  1. หากข้อความตรงกับ exit keyword จะ publish ลงคิว mbox_handoff ด้วย type: "exit" จากนั้น worker-go จะไปปิด conversation ใน Chatwoot และลบ agent_mode key
  2. หากไม่ตรง จะ forward ข้อความไปยัง mboxLineWebhookUrl (ดู ส่งต่อ Webhook ไปยังระบบลูกค้า) แล้วเขียน lastActivity เป็น epoch หน่วยมิลลิวินาทีลงใน agent_mode โดยไม่ตั้ง TTL
  3. ไม่ว่าจะเป็นทางใด ระบบจะ return ทันทีโดยไม่ publish ลง line_webhook ซึ่งมีผลเป็นการปิดปากบอท

กรณี A2 — ผู้ใช้ยังไม่อยู่ใน agent mode

  1. หากข้อความตรงกับ agent keyword เช่น "คุยกับเจ้าหน้าที่" จะเข้าสู่โหมด handoff
    • เมื่อ mboxDepartmentPickerEnabled ไม่เท่ากับ "1" จะ publish ด้วย type: "handoff" พร้อม mboxConfig ปกติ
    • เมื่อ mboxDepartmentPickerEnabled เท่ากับ "1" จะ publish ด้วย type: "department_picker" พร้อม mboxConfig ที่แนบ block departmentPicker ซึ่งประกอบด้วย enabled, headerText, generalLabel และ departments โดย departments parse มาจาก JSON string ในฟิลด์ mboxDepartments หาก parse ไม่ได้จะได้ array ว่าง
    • จากนั้น return โดยไม่ส่งต่อให้บอท
  2. หากไม่ตรง keyword ใดเลย จะตกลงสู่เส้นทางปกติ คือ publish ลงคิว line_webhook ให้บอททำงาน

B. Postback event — การเลือกแผนก

  1. อ่าน postback.data ของ postback event ตัวแรก
  2. หากขึ้นต้นด้วย mbox_team: จะดึงค่าที่อยู่หลังเครื่องหมาย colon มาเป็น teamId
  3. หากมี source.userId จะ publish ลงคิว mbox_handoff ด้วย type: "handoff" พร้อม teamId แล้ว return
  4. หากไม่มี 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.ProcessLinedispatcher หลัก ครอบคลุมทั้งขั้น message และขั้น postback
buildMboxConfigmap ข้อมูลจาก 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_mode key ส่งข้อความ 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