支持访客实时输入草稿展示与联系方式自动识别
坐席输入框上方显示「对方正在输入」草稿;从草稿/消息提取手机微信邮箱QQ并追加入库,不覆盖已有联系方式。
This commit is contained in:
@@ -4,13 +4,25 @@ import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"kefu-cloud/server/internal/model"
|
||||
)
|
||||
|
||||
// ProcessDraftContacts 由 handler 包注入,避免 ws ↔ handler 循环依赖。
|
||||
// tenantID, customerID, sessionID, draftText
|
||||
var ProcessDraftContacts func(tenantID, customerID, sessionID uint, text string)
|
||||
|
||||
func processDraftContacts(tenantID, customerID, sessionID uint, text string) {
|
||||
if ProcessDraftContacts != nil {
|
||||
ProcessDraftContacts(tenantID, customerID, sessionID, text)
|
||||
}
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
Subprotocols: []string{"kefu-v1", "kefu-visitor-v1"},
|
||||
@@ -36,6 +48,8 @@ type Event struct {
|
||||
type ClientEvent struct {
|
||||
Type string `json:"type"`
|
||||
SessionID uint `json:"session_id"`
|
||||
// Text 访客输入框草稿(type=input_draft 或 typing 附带)
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
type Hub struct {
|
||||
@@ -166,8 +180,14 @@ func (h *Hub) BroadcastToSessionStaff(tenantID uint, agentID *uint, message []by
|
||||
}
|
||||
}
|
||||
|
||||
const maxDraftTextRunes = 500
|
||||
|
||||
func handleClientEvent(client *Client, event ClientEvent) {
|
||||
if event.Type != "typing" || event.SessionID == 0 {
|
||||
if event.SessionID == 0 {
|
||||
return
|
||||
}
|
||||
// typing:仅指示;input_draft:带正文草稿
|
||||
if event.Type != "typing" && event.Type != "input_draft" {
|
||||
return
|
||||
}
|
||||
var session model.Session
|
||||
@@ -175,7 +195,7 @@ func handleClientEvent(client *Client, event ClientEvent) {
|
||||
return
|
||||
}
|
||||
|
||||
// 访客输入中 → 通知可接待的客服
|
||||
// 访客输入中 / 草稿 → 通知坐席
|
||||
if client.Kind == "visitor" {
|
||||
if client.SessionID == nil || *client.SessionID != event.SessionID {
|
||||
return
|
||||
@@ -183,6 +203,34 @@ func handleClientEvent(client *Client, event ClientEvent) {
|
||||
if session.Status == "ended" || session.Status == "archived" {
|
||||
return
|
||||
}
|
||||
|
||||
if event.Type == "input_draft" {
|
||||
text := strings.TrimSpace(event.Text)
|
||||
if utf8.RuneCountInString(text) > maxDraftTextRunes {
|
||||
text = string([]rune(text)[:maxDraftTextRunes])
|
||||
}
|
||||
now := time.Now()
|
||||
_ = model.DB.Model(&session).Updates(map[string]interface{}{
|
||||
"draft_text": text,
|
||||
"draft_updated_at": now,
|
||||
}).Error
|
||||
|
||||
// 异步提取联系方式(不阻塞 WS)
|
||||
go processDraftContacts(session.TenantID, session.CustomerID, session.ID, text)
|
||||
|
||||
payload, err := NewEvent("input_draft", session.ID, map[string]interface{}{
|
||||
"from": "visitor",
|
||||
"text": text,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// 排队中也让租户坐席能看到
|
||||
DefaultHub.BroadcastToTenantStaff(session.TenantID, payload)
|
||||
return
|
||||
}
|
||||
|
||||
// 兼容旧 typing(无正文)
|
||||
payload, err := NewEvent("typing", session.ID, map[string]string{"from": "visitor"})
|
||||
if err != nil {
|
||||
return
|
||||
@@ -195,8 +243,8 @@ func handleClientEvent(client *Client, event ClientEvent) {
|
||||
return
|
||||
}
|
||||
|
||||
// 客服输入中 → 通知访客
|
||||
if client.Kind != "agent" {
|
||||
// 客服输入中 → 通知访客(不传草稿内容)
|
||||
if client.Kind != "agent" || event.Type != "typing" {
|
||||
return
|
||||
}
|
||||
if client.Role == "agent" && (session.AgentID == nil || *session.AgentID != client.UserID) {
|
||||
@@ -217,7 +265,7 @@ func HandleWebSocket(client *Client) {
|
||||
client.Conn.Close()
|
||||
}()
|
||||
|
||||
client.Conn.SetReadLimit(1024)
|
||||
client.Conn.SetReadLimit(4096) // 允许访客草稿正文
|
||||
client.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
client.Conn.SetPongHandler(func(string) error {
|
||||
client.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
|
||||
Reference in New Issue
Block a user