470 lines
13 KiB
Go
470 lines
13 KiB
Go
|
package ws
|
|||
|
|
|||
|
import (
|
|||
|
"encoding/json"
|
|||
|
"fmt"
|
|||
|
"github.com/gin-gonic/gin"
|
|||
|
"github.com/gorilla/websocket"
|
|||
|
"kefu/lib"
|
|||
|
"kefu/models"
|
|||
|
"kefu/tools"
|
|||
|
"log"
|
|||
|
"net/http"
|
|||
|
"time"
|
|||
|
)
|
|||
|
|
|||
|
type VisitorConnection struct {
|
|||
|
Conn *websocket.Conn
|
|||
|
Name string
|
|||
|
Id string
|
|||
|
Avatar string
|
|||
|
ToId string
|
|||
|
EntId string
|
|||
|
ClientIp string
|
|||
|
UpdateTime time.Time
|
|||
|
}
|
|||
|
|
|||
|
func NewVisitorServer(c *gin.Context) {
|
|||
|
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
|||
|
if err != nil {
|
|||
|
http.NotFound(c.Writer, c.Request)
|
|||
|
log.Println("upgrade error:", err)
|
|||
|
return
|
|||
|
}
|
|||
|
//获取GET参数,创建WS
|
|||
|
toId := c.Query("to_id")
|
|||
|
visitorId := c.Query("visitor_id")
|
|||
|
if toId == "" || visitorId == "" {
|
|||
|
errStr := "访客websocket的GET参数为空"
|
|||
|
log.Println(errStr)
|
|||
|
conn.WriteMessage(websocket.TextMessage, []byte(errStr))
|
|||
|
conn.Close()
|
|||
|
return
|
|||
|
}
|
|||
|
vistorInfo := models.FindVisitorByCondition("visitor_id = ? and to_id = ?", visitorId, toId)
|
|||
|
if vistorInfo.VisitorId == "" {
|
|||
|
errStr := "该客服下访客不存在:" + visitorId + "," + toId
|
|||
|
log.Println(errStr)
|
|||
|
conn.WriteMessage(websocket.TextMessage, []byte(errStr))
|
|||
|
conn.Close()
|
|||
|
return
|
|||
|
}
|
|||
|
//验证访客黑名单
|
|||
|
black := models.FindVisitorBlack("visitor_id = ?", visitorId)
|
|||
|
if black.Id != 0 {
|
|||
|
errStr := "访客加入黑名单:" + visitorId + "," + toId
|
|||
|
log.Println(errStr)
|
|||
|
conn.WriteMessage(websocket.TextMessage, []byte(errStr))
|
|||
|
conn.Close()
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
oldUser, ok := ClientList[visitorId]
|
|||
|
if ok && oldUser.Conn == conn {
|
|||
|
return
|
|||
|
}
|
|||
|
connection := &VisitorConnection{
|
|||
|
Conn: conn,
|
|||
|
Name: vistorInfo.Name,
|
|||
|
Avatar: vistorInfo.Avator,
|
|||
|
Id: visitorId,
|
|||
|
ToId: toId,
|
|||
|
EntId: vistorInfo.EntId,
|
|||
|
UpdateTime: time.Now(),
|
|||
|
ClientIp: c.ClientIP(),
|
|||
|
}
|
|||
|
AddVisitorToList(connection)
|
|||
|
VisitorOnline(toId, vistorInfo)
|
|||
|
|
|||
|
for {
|
|||
|
//接受消息
|
|||
|
var receive []byte
|
|||
|
messageType, receive, err := conn.ReadMessage()
|
|||
|
if err != nil {
|
|||
|
log.Println("ws/visitor.go conn.ReadMessage:", visitorId, err, receive, messageType)
|
|||
|
conn.Close()
|
|||
|
visitor, ok := ClientList[visitorId]
|
|||
|
if ok && visitor.Conn == conn {
|
|||
|
delete(ClientList, visitor.Id)
|
|||
|
VisitorOffline(visitor.ToId, visitor.Id, visitor.Name)
|
|||
|
}
|
|||
|
return
|
|||
|
}
|
|||
|
//消息体
|
|||
|
//message := &Message{
|
|||
|
// conn: conn,
|
|||
|
// content: receive,
|
|||
|
// context: c,
|
|||
|
// messageType: messageType,
|
|||
|
//}
|
|||
|
|
|||
|
var typeMsg TypeMessage
|
|||
|
err = json.Unmarshal(receive, &typeMsg)
|
|||
|
if err != nil || typeMsg.Type == nil || typeMsg.Data == nil {
|
|||
|
log.Println("ws/visitor.go message json unmarshal error:", err, receive)
|
|||
|
continue
|
|||
|
}
|
|||
|
msgType := typeMsg.Type.(string)
|
|||
|
|
|||
|
switch msgType {
|
|||
|
//心跳
|
|||
|
case "ping":
|
|||
|
msg := TypeMessage{
|
|||
|
Type: "pong",
|
|||
|
}
|
|||
|
str, _ := json.Marshal(msg)
|
|||
|
//message.Mux.Lock()
|
|||
|
conn.WriteMessage(websocket.TextMessage, str)
|
|||
|
//message.Mux.Unlock()
|
|||
|
case "inputing":
|
|||
|
data := typeMsg.Data.(map[string]interface{})
|
|||
|
from := data["from"].(string)
|
|||
|
to := data["to"].(string)
|
|||
|
//限流
|
|||
|
if tools.LimitFreqSingle("inputing:"+from, 1, 2) {
|
|||
|
OneKefuMessage(to, receive)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
}
|
|||
|
func AddVisitorToList(connection *VisitorConnection) {
|
|||
|
//用户id对应的连接
|
|||
|
oldUser, ok := ClientList[connection.Id]
|
|||
|
if ok {
|
|||
|
VisitorOffline(oldUser.ToId, oldUser.Id, oldUser.Name)
|
|||
|
closemsg := TypeMessage{
|
|||
|
Type: "close",
|
|||
|
Data: connection.Id,
|
|||
|
}
|
|||
|
closeStr, _ := json.Marshal(closemsg)
|
|||
|
oldUser.Conn.WriteMessage(websocket.TextMessage, closeStr)
|
|||
|
oldUser.Conn.Close()
|
|||
|
delete(ClientList, connection.Id)
|
|||
|
}
|
|||
|
ClientList[connection.Id] = connection
|
|||
|
}
|
|||
|
|
|||
|
// func AddVisitorToRoom(roomId, visitorId string) {
|
|||
|
// //用户id对应的连接
|
|||
|
// user, ok := ClientList[visitorId]
|
|||
|
// if user != nil && ok {
|
|||
|
// members, _ := Room.GetMembers(roomId)
|
|||
|
// members = append(members, user)
|
|||
|
// Room.SetMembers(roomId, members)
|
|||
|
// }
|
|||
|
// }
|
|||
|
func VisitorOnline(kefuId string, visitor models.Visitor) {
|
|||
|
go models.UpdateUserRecNum(kefuId, 1)
|
|||
|
lastMessage := models.FindLastMessageByVisitorId(visitor.VisitorId)
|
|||
|
unreadMap := models.FindUnreadMessageNumByVisitorIds([]string{visitor.VisitorId}, "visitor")
|
|||
|
var unreadNum uint32
|
|||
|
if num, ok := unreadMap[visitor.VisitorId]; ok {
|
|||
|
unreadNum = num
|
|||
|
}
|
|||
|
|
|||
|
userInfo := make(map[string]string)
|
|||
|
userInfo["uid"] = visitor.VisitorId
|
|||
|
userInfo["visitor_id"] = visitor.VisitorId
|
|||
|
userInfo["username"] = fmt.Sprintf("#%d%s", visitor.ID, visitor.Name)
|
|||
|
userInfo["avator"] = visitor.Avator
|
|||
|
userInfo["last_message"] = lastMessage.Content
|
|||
|
userInfo["unread_num"] = fmt.Sprintf("%d", unreadNum)
|
|||
|
userInfo["last_time"] = lastMessage.CreatedAt.Format("2006-01-02 15:04:05")
|
|||
|
//if userInfo["last_message"] == "" {
|
|||
|
// userInfo["last_message"] = "新访客"
|
|||
|
//}
|
|||
|
if userInfo["last_time"] == "0001-01-01 00:00:00" {
|
|||
|
userInfo["last_time"] = visitor.UpdatedAt.Format("2006-01-02 15:04:05")
|
|||
|
}
|
|||
|
msg := TypeMessage{
|
|||
|
Type: "userOnline",
|
|||
|
Data: userInfo,
|
|||
|
}
|
|||
|
str, _ := json.Marshal(msg)
|
|||
|
OneKefuMessage(kefuId, str)
|
|||
|
}
|
|||
|
func VisitorOffline(kefuId string, visitorId string, visitorName string) {
|
|||
|
go models.UpdateUserRecNum(kefuId, -1)
|
|||
|
userInfo := make(map[string]string)
|
|||
|
userInfo["uid"] = visitorId
|
|||
|
userInfo["visitor_id"] = visitorId
|
|||
|
userInfo["name"] = visitorName
|
|||
|
msg := TypeMessage{
|
|||
|
Type: "userOffline",
|
|||
|
Data: userInfo,
|
|||
|
}
|
|||
|
str, _ := json.Marshal(msg)
|
|||
|
//新版
|
|||
|
OneKefuMessage(kefuId, str)
|
|||
|
}
|
|||
|
func VisitorNotice(visitorId string, notice string) {
|
|||
|
msg := TypeMessage{
|
|||
|
Type: "notice",
|
|||
|
Data: notice,
|
|||
|
}
|
|||
|
str, _ := json.Marshal(msg)
|
|||
|
visitor, ok := ClientList[visitorId]
|
|||
|
if !ok || visitor == nil || visitor.Conn == nil {
|
|||
|
return
|
|||
|
}
|
|||
|
visitor.Conn.WriteMessage(websocket.TextMessage, str)
|
|||
|
}
|
|||
|
func VisitorCustomMessage(visitorId string, notice TypeMessage) {
|
|||
|
str, _ := json.Marshal(notice)
|
|||
|
visitor, ok := ClientList[visitorId]
|
|||
|
if !ok || visitor == nil || visitor.Conn == nil {
|
|||
|
return
|
|||
|
}
|
|||
|
visitor.Conn.WriteMessage(websocket.TextMessage, str)
|
|||
|
}
|
|||
|
func VisitorTransfer(visitorId string, kefuId string) {
|
|||
|
msg := TypeMessage{
|
|||
|
Type: "transfer",
|
|||
|
Data: kefuId,
|
|||
|
}
|
|||
|
str, _ := json.Marshal(msg)
|
|||
|
visitor, ok := ClientList[visitorId]
|
|||
|
if !ok || visitor == nil || visitor.Conn == nil {
|
|||
|
return
|
|||
|
}
|
|||
|
visitor.Conn.WriteMessage(websocket.TextMessage, str)
|
|||
|
}
|
|||
|
func VisitorChangeId(visitorId, newId string) {
|
|||
|
msg := TypeMessage{
|
|||
|
Type: "change_id",
|
|||
|
Data: newId,
|
|||
|
}
|
|||
|
str, _ := json.Marshal(msg)
|
|||
|
visitor, ok := ClientList[visitorId]
|
|||
|
if !ok || visitor == nil || visitor.Conn == nil {
|
|||
|
return
|
|||
|
}
|
|||
|
visitor.Conn.WriteMessage(websocket.TextMessage, str)
|
|||
|
}
|
|||
|
func UpdateVisitorName(visitorId string, visitorName string) {
|
|||
|
visitor, ok := ClientList[visitorId]
|
|||
|
if !ok || visitor == nil || visitor.Conn == nil {
|
|||
|
return
|
|||
|
}
|
|||
|
visitor.Name = visitorName
|
|||
|
}
|
|||
|
func VisitorMessage(visitorId, content string, kefuInfo models.User) {
|
|||
|
msgId := tools.Now()
|
|||
|
msg := TypeMessage{
|
|||
|
Type: "message",
|
|||
|
Data: ClientMessage{
|
|||
|
MsgId: uint(msgId),
|
|||
|
Name: kefuInfo.Nickname,
|
|||
|
Avator: kefuInfo.Avator,
|
|||
|
Id: kefuInfo.Name,
|
|||
|
Time: time.Now().Format("2006-01-02 15:04:05"),
|
|||
|
ToId: visitorId,
|
|||
|
Content: content,
|
|||
|
IsKefu: "no",
|
|||
|
},
|
|||
|
}
|
|||
|
str, _ := json.Marshal(msg)
|
|||
|
visitor, ok := ClientList[visitorId]
|
|||
|
if !ok || visitor == nil || visitor.Conn == nil {
|
|||
|
return
|
|||
|
}
|
|||
|
visitor.Conn.WriteMessage(websocket.TextMessage, str)
|
|||
|
}
|
|||
|
func VisitorMessageSameMsgId(msgId uint, visitorId, content string, kefuInfo models.User) {
|
|||
|
msg := TypeMessage{
|
|||
|
Type: "message",
|
|||
|
Data: ClientMessage{
|
|||
|
MsgId: msgId,
|
|||
|
Name: kefuInfo.Nickname,
|
|||
|
Avator: kefuInfo.Avator,
|
|||
|
Id: kefuInfo.Name,
|
|||
|
Time: time.Now().Format("2006-01-02 15:04:05"),
|
|||
|
ToId: visitorId,
|
|||
|
Content: content,
|
|||
|
IsKefu: "no",
|
|||
|
},
|
|||
|
}
|
|||
|
str, _ := json.Marshal(msg)
|
|||
|
visitor, ok := ClientList[visitorId]
|
|||
|
if !ok || visitor == nil || visitor.Conn == nil {
|
|||
|
return
|
|||
|
}
|
|||
|
visitor.Conn.WriteMessage(websocket.TextMessage, str)
|
|||
|
}
|
|||
|
func VisitorToKefuMessage(visitorInfo models.Visitor, kefuName, content string) {
|
|||
|
msg := TypeMessage{
|
|||
|
Type: "message",
|
|||
|
Data: ClientMessage{
|
|||
|
Name: visitorInfo.Name,
|
|||
|
Avator: visitorInfo.Avator,
|
|||
|
Id: visitorInfo.VisitorId,
|
|||
|
Time: time.Now().Format("2006-01-02 15:04:05"),
|
|||
|
ToId: kefuName,
|
|||
|
Content: content,
|
|||
|
IsKefu: "no",
|
|||
|
},
|
|||
|
}
|
|||
|
str, _ := json.Marshal(msg)
|
|||
|
OneKefuMessage(kefuName, str)
|
|||
|
}
|
|||
|
func VisitorAutoReply(vistorInfo models.Visitor, kefuInfo models.User, content string) (string, bool) {
|
|||
|
|
|||
|
//var entInfo models.User
|
|||
|
//if fmt.Sprintf("%v", kefuInfo.Pid) == fmt.Sprintf("%d", 1) {
|
|||
|
// entInfo = kefuInfo
|
|||
|
//} else {
|
|||
|
// entInfo = models.FindUserByUid(kefuInfo.Pid)
|
|||
|
//}
|
|||
|
entId := vistorInfo.EntId
|
|||
|
//查询机器人信息
|
|||
|
configs := models.GetEntConfigsMap(entId, "RobotName", "RobotAvator", "RobotStatus", "KeywordFinalReply")
|
|||
|
robotName := configs["RobotName"]
|
|||
|
robotAvator := configs["RobotAvator"]
|
|||
|
if configs["RobotStatus"] == "3" {
|
|||
|
return "", false
|
|||
|
}
|
|||
|
|
|||
|
systemHotQaBussinesId := models.FindConfig("SystemHotQaBussinesId")
|
|||
|
if systemHotQaBussinesId != "" {
|
|||
|
entId = systemHotQaBussinesId
|
|||
|
}
|
|||
|
title := content
|
|||
|
replyContent := ""
|
|||
|
//if strings.HasPrefix(content, "@") {
|
|||
|
// apiKeys := strings.Split(strings.Replace(content, ":", ":", 1), ":")
|
|||
|
// title = apiKeys[0]
|
|||
|
//}
|
|||
|
//匹配方式
|
|||
|
replys := models.FindArticleRows("id,title,search_type", "ent_id = ? ", entId)
|
|||
|
|
|||
|
//按分值计算
|
|||
|
var exactReply []lib.MatchData
|
|||
|
var matchReply []lib.MatchData
|
|||
|
for _, row := range replys {
|
|||
|
//精准
|
|||
|
if row.SearchType == 2 {
|
|||
|
exactReply = append(exactReply, lib.MatchData{
|
|||
|
ID: row.Id,
|
|||
|
Text: row.Title,
|
|||
|
})
|
|||
|
}
|
|||
|
//包含
|
|||
|
if row.SearchType == 1 {
|
|||
|
matchReply = append(matchReply, lib.MatchData{
|
|||
|
ID: row.Id,
|
|||
|
Text: row.Title,
|
|||
|
})
|
|||
|
}
|
|||
|
//通配符
|
|||
|
if row.Title == "*" {
|
|||
|
exactReply = append(exactReply, lib.MatchData{
|
|||
|
ID: row.Id,
|
|||
|
Text: row.Title,
|
|||
|
})
|
|||
|
}
|
|||
|
}
|
|||
|
matchRes, ok := lib.NewMatcher(exactReply).Match(title, true)
|
|||
|
if ok {
|
|||
|
r := models.FindArticleRow("id = ?", matchRes.ID)
|
|||
|
replyContent = r.Content
|
|||
|
article := models.Article{
|
|||
|
Score: r.Score + 1,
|
|||
|
}
|
|||
|
go article.SaveArticle("ent_id = ? and id = ?", entId, r.Id)
|
|||
|
}
|
|||
|
|
|||
|
if replyContent == "" {
|
|||
|
matchRes, ok = lib.NewMatcher(matchReply).Match(title, false)
|
|||
|
if ok {
|
|||
|
r := models.FindArticleRow("id = ?", matchRes.ID)
|
|||
|
replyContent = r.Content
|
|||
|
article := models.Article{
|
|||
|
Score: r.Score + 1,
|
|||
|
}
|
|||
|
go article.SaveArticle("ent_id = ? and id = ?", entId, r.Id)
|
|||
|
}
|
|||
|
}
|
|||
|
if replyContent == "" {
|
|||
|
replyContent = configs["KeywordFinalReply"]
|
|||
|
}
|
|||
|
//reply := models.FindArticleRow("ent_id = ? and title like ?", entId, "%"+title+"%")
|
|||
|
//reply = models.FindReplyItemByUserIdTitle(entInfo.Name, content)
|
|||
|
//调用第三方接口
|
|||
|
//if reply.ApiUrl != "" {
|
|||
|
// params := strings.TrimSpace(strings.Replace(content, reply.Title, "", 1))
|
|||
|
// url := fmt.Sprintf("%s?ent_id=%s&kefu_id=%s&visitor_id=%s&keyword=%s&content=%s", reply.ApiUrl, entId, kefuInfo.Name, vistorInfo.VisitorId, reply.Title, params)
|
|||
|
// log.Println(vistorInfo.VisitorId, content, url)
|
|||
|
// res := tools.Get(url)
|
|||
|
// if res != "" {
|
|||
|
// reply.Content = res
|
|||
|
// }
|
|||
|
//}
|
|||
|
//gpt流式回复
|
|||
|
isGpt := false
|
|||
|
if replyContent == "" && models.CheckVisitorRobotReply(vistorInfo.State) {
|
|||
|
replyContent = Gpt3Knowledge(entId, vistorInfo.VisitorId, kefuInfo, content)
|
|||
|
if replyContent != "" {
|
|||
|
isGpt = true
|
|||
|
}
|
|||
|
}
|
|||
|
//调用chatGPT
|
|||
|
//if replyContent == "" {
|
|||
|
// replyContent = Gpt3dot5Message(entId, vistorInfo.VisitorId, kefuInfo, content)
|
|||
|
// if replyContent != "" {
|
|||
|
// isGpt = true
|
|||
|
// }
|
|||
|
//}
|
|||
|
|
|||
|
if replyContent != "" && !isGpt {
|
|||
|
|
|||
|
kefuInfo.Nickname = tools.Ifelse(robotName != "", robotName, kefuInfo.Nickname).(string)
|
|||
|
kefuInfo.Avator = tools.Ifelse(robotAvator != "", robotAvator, kefuInfo.Avator).(string)
|
|||
|
|
|||
|
VisitorMessage(vistorInfo.VisitorId, replyContent, kefuInfo)
|
|||
|
KefuMessage(vistorInfo.VisitorId, replyContent, kefuInfo)
|
|||
|
models.CreateMessage(kefuInfo.Name, vistorInfo.VisitorId, replyContent, "kefu", vistorInfo.EntId, "read")
|
|||
|
}
|
|||
|
return replyContent, isGpt
|
|||
|
}
|
|||
|
func VisitorCount(kefuName string) uint {
|
|||
|
num := 0
|
|||
|
for _, visitor := range ClientList {
|
|||
|
if visitor.ToId == kefuName {
|
|||
|
num = num + 1
|
|||
|
}
|
|||
|
}
|
|||
|
return uint(num)
|
|||
|
}
|
|||
|
|
|||
|
//func CleanVisitorExpire() {
|
|||
|
// go func() {
|
|||
|
// log.Println("cleanVisitorExpire start...")
|
|||
|
// for {
|
|||
|
// for _, user := range ClientList {
|
|||
|
// diff := time.Now().Sub(user.UpdateTime).Seconds()
|
|||
|
// if diff >= common.VisitorExpire {
|
|||
|
// entConfig := models.FindEntConfig(user.Ent_id, "CloseVisitorMessage")
|
|||
|
// if entConfig.ConfValue != "" {
|
|||
|
// kefu := models.FindUserByUid(user.Ent_id)
|
|||
|
// VisitorMessage(user.Id, entConfig.ConfValue, kefu)
|
|||
|
// }
|
|||
|
// msg := TypeMessage{
|
|||
|
// Type: "auto_close",
|
|||
|
// Data: user.Id,
|
|||
|
// }
|
|||
|
// str, _ := json.Marshal(msg)
|
|||
|
// if err := user.Conn.WriteMessage(websocket.TextMessage, str); err != nil {
|
|||
|
// user.Conn.Close()
|
|||
|
// delete(ClientList, user.Id)
|
|||
|
// VisitorOffline(user.To_id, user.Id, user.Name)
|
|||
|
// }
|
|||
|
// log.Println(user.Name + ":cleanVisitorExpire finshed")
|
|||
|
// }
|
|||
|
// }
|
|||
|
// t := time.NewTimer(time.Second * 5)
|
|||
|
// <-t.C
|
|||
|
// }
|
|||
|
// }()
|
|||
|
//}
|