499 lines
14 KiB
Go
499 lines
14 KiB
Go
package kefuWework
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"encoding/xml"
|
||
"fmt"
|
||
"github.com/tidwall/gjson"
|
||
"io/ioutil"
|
||
"kefu/lib"
|
||
"kefu/models"
|
||
"kefu/service"
|
||
"kefu/tools"
|
||
"kefu/ws"
|
||
"log"
|
||
"net/http"
|
||
"net/url"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/patrickmn/go-cache"
|
||
)
|
||
|
||
// 验证企业微信回调的token
|
||
//var token = "taooshihan"
|
||
//
|
||
//// 验证企业微信回调的key
|
||
//var encodingAesKey = "ORSH1J7CaQtQi2b4dGY5gAJDUJFDOd5cO7Yd7knIxUF"
|
||
//
|
||
//// 企业微信企业id
|
||
//var corpid = "wwa4266261c4ea2c08"
|
||
//
|
||
//// 企业微信secret
|
||
//var corpsecret = "SPU2s8t5cmI3GfkAuggiTyPk5LsuPY22hgep4cGBzlQ"
|
||
|
||
// 企业微信的重试缓存,如果服务器延迟低,可以去掉该变量以及 isRetry 逻辑
|
||
var retryCache = cache.New(60*time.Minute, 10*time.Minute)
|
||
|
||
// 企业微信 token 缓存,请求频次过高可能有一些额外的问题
|
||
var tokenCache = cache.New(5*time.Minute, 5*time.Minute)
|
||
|
||
// 上下文对话能力,默认是 3, 可以根据需要修改对话长度
|
||
var weworkConversationSize = 3
|
||
|
||
type WeixinUserAskMsg struct {
|
||
ToUserName string `xml:"ToUserName"`
|
||
CreateTime int64 `xml:"CreateTime"`
|
||
MsgType string `xml:"MsgType"`
|
||
Event string `xml:"Event"`
|
||
Token string `xml:"Token"`
|
||
OpenKfId string `xml:"OpenKfId"`
|
||
}
|
||
|
||
type AccessToken struct {
|
||
Errcode int `json:"errcode"`
|
||
Errmsg string `json:"errmsg"`
|
||
AccessToken string `json:"access_token"`
|
||
ExpiresIn int `json:"expires_in"`
|
||
}
|
||
|
||
type MsgRet struct {
|
||
Errcode int `json:"errcode"`
|
||
Errmsg string `json:"errmsg"`
|
||
NextCursor string `json:"next_cursor"`
|
||
MsgList []Msg `json:"msg_list"`
|
||
}
|
||
type Msg struct {
|
||
Msgid string `json:"msgid"`
|
||
SendTime int64 `json:"send_time"`
|
||
Origin int `json:"origin"`
|
||
Msgtype string `json:"msgtype"`
|
||
Event struct {
|
||
EventType string `json:"event_type"`
|
||
Scene string `json:"scene"`
|
||
OpenKfid string `json:"open_kfid"`
|
||
ExternalUserid string `json:"external_userid"`
|
||
WelcomeCode string `json:"welcome_code"`
|
||
} `json:"event"`
|
||
Text struct {
|
||
Content string `json:"content"`
|
||
} `json:"text"`
|
||
OpenKfid string `json:"open_kfid"`
|
||
ExternalUserid string `json:"external_userid"`
|
||
}
|
||
|
||
type ReplyMsg struct {
|
||
Touser string `json:"touser,omitempty"`
|
||
OpenKfid string `json:"open_kfid,omitempty"`
|
||
Msgid string `json:"msgid,omitempty"`
|
||
Msgtype string `json:"msgtype,omitempty"`
|
||
Text struct {
|
||
Content string `json:"content,omitempty"`
|
||
} `json:"text,omitempty"`
|
||
}
|
||
|
||
// 定义微信客服API的封装结构体
|
||
type KefuWework struct {
|
||
corpid string
|
||
corpsecret string
|
||
Token string
|
||
EncodingAESKey string
|
||
}
|
||
|
||
// 创建微信客服API的封装结构体实例
|
||
func NewKefuWework(corpid, corpsecret, Token, EncodingAESKey string) *KefuWework {
|
||
return &KefuWework{
|
||
corpid: corpid,
|
||
corpsecret: corpsecret,
|
||
Token: Token,
|
||
EncodingAESKey: EncodingAESKey,
|
||
}
|
||
}
|
||
|
||
// 微信客服消息
|
||
func TalkWeixin(c *gin.Context) {
|
||
entId := c.Param("entId")
|
||
verifyMsgSign := c.Query("msg_signature")
|
||
verifyTimestamp := c.Query("timestamp")
|
||
verifyNonce := c.Query("nonce")
|
||
|
||
//获取配置
|
||
configs := models.GetEntConfigsMap(entId, "kefuWeworkCorpid", "kefuWeworkSecret", "kefuWeworkToken", "kefuWeworkEncodingAESKey")
|
||
kefuWework := NewKefuWework(configs["kefuWeworkCorpid"], configs["kefuWeworkSecret"], configs["kefuWeworkToken"], configs["kefuWeworkEncodingAESKey"])
|
||
//读取xml数据
|
||
bodyBytes, _ := ioutil.ReadAll(c.Request.Body)
|
||
crypt := NewWXBizMsgCrypt(kefuWework.Token, kefuWework.EncodingAESKey, kefuWework.corpid, 1)
|
||
data, _ := crypt.DecryptMsg(verifyMsgSign, verifyTimestamp, verifyNonce, bodyBytes)
|
||
var weixinUserAskMsg WeixinUserAskMsg
|
||
err := xml.Unmarshal(data, &weixinUserAskMsg)
|
||
if err != nil {
|
||
log.Println("微信客服xml解析失败,error: " + err.Error())
|
||
return
|
||
}
|
||
accessToken, err := kefuWework.accessToken()
|
||
if err != nil {
|
||
log.Println("微信客服获取access_token失败,error: " + err.Error())
|
||
return
|
||
}
|
||
//获取最新的消息
|
||
msgToken := weixinUserAskMsg.Token
|
||
msgRet, err := getMsgs(accessToken, msgToken)
|
||
if err != nil {
|
||
log.Println("微信客服获取最新消息失败,error: " + err.Error())
|
||
return
|
||
}
|
||
if isRetry(verifyMsgSign) {
|
||
log.Println("微信客服重试机制 ")
|
||
return
|
||
}
|
||
//访客入库
|
||
handleVisitor(msgRet, entId, c, kefuWework)
|
||
//返回消息
|
||
go handleMsgRet(msgRet, entId, kefuWework)
|
||
c.JSON(200, "ok")
|
||
}
|
||
|
||
func (this *KefuWework) TalkToUser(external_userid, open_kfid, content string) {
|
||
reply := ReplyMsg{
|
||
Touser: external_userid,
|
||
OpenKfid: open_kfid,
|
||
Msgtype: "text",
|
||
Text: struct {
|
||
Content string `json:"content,omitempty"`
|
||
}{Content: content},
|
||
}
|
||
atoken, err := this.accessToken()
|
||
if err != nil {
|
||
return
|
||
}
|
||
callTalk(reply, atoken)
|
||
}
|
||
|
||
// 获取最新的消息处理是否自动回复
|
||
func handleMsgRet(msgRet MsgRet, entId string, wework *KefuWework) {
|
||
size := len(msgRet.MsgList)
|
||
if size < 1 {
|
||
return
|
||
}
|
||
current := msgRet.MsgList[size-1]
|
||
userId := current.ExternalUserid
|
||
kfId := current.OpenKfid
|
||
//消息类型
|
||
msgtype := current.Msgtype
|
||
//事件消息
|
||
if msgtype == "event" {
|
||
//用户进入会话事件
|
||
//current.Event.WelcomeCode
|
||
}
|
||
//文本消息
|
||
content := current.Text.Content
|
||
if content == "" {
|
||
go wework.TalkToUser(userId, kfId, "非常抱歉,当前仅支持文本消息,请输入文本。")
|
||
return
|
||
}
|
||
|
||
//查询机器人信息
|
||
configs := models.GetEntConfigsMap(entId, "QiyeWechatKefuPreRobot", "RobotStatus", "chatGPTSecret", "QdrantAIStatus")
|
||
if configs["RobotStatus"] == "3" {
|
||
return
|
||
}
|
||
kefuInfo := models.FindUser(kfId)
|
||
if kefuInfo.ID == 0 {
|
||
kefuInfo = models.FindUserByUid(entId)
|
||
}
|
||
visitorId := fmt.Sprintf("wxkf|%s|%s", entId, userId)
|
||
vistorInfo := models.FindVisitorByVistorId(visitorId)
|
||
ret := SearchLearn(entId, content)
|
||
//判断chatGPT
|
||
if ret == "" && configs["QdrantAIStatus"] == "true" && models.CheckVisitorRobotReply(vistorInfo.State) {
|
||
if configs["QiyeWechatKefuPreRobot"] != "" {
|
||
go wework.TalkToUser(userId, kfId, configs["QiyeWechatKefuPreRobot"])
|
||
}
|
||
ret = Gpt3Knowledge(entId, visitorId, kefuInfo, content)
|
||
}
|
||
//if ret == "" && configs["chatGPTSecret"] != "" {
|
||
// ret = service.Gpt3dot5Message(entId, "robot_message", content)
|
||
// //ret, _ = AskOnConversation(configs["chatGPTSecret"], content, userId, weworkConversationSize)
|
||
// //TalkToUser(userId, kfId, content, "该问题暂时无法回答,已收录到学习库")
|
||
//}
|
||
if ret == "" {
|
||
return
|
||
}
|
||
|
||
models.CreateMessage(kefuInfo.Name, visitorId, ret, "kefu", entId, "read")
|
||
go ws.KefuMessage(visitorId, ret, kefuInfo)
|
||
wework.TalkToUser(userId, kfId, ret)
|
||
}
|
||
func handleVisitor(msgRet MsgRet, entId string, c *gin.Context, wework *KefuWework) {
|
||
size := len(msgRet.MsgList)
|
||
if size < 1 {
|
||
return
|
||
}
|
||
current := msgRet.MsgList[size-1]
|
||
userId := current.ExternalUserid
|
||
kfId := current.OpenKfid
|
||
content := current.Text.Content
|
||
if content == "" {
|
||
return
|
||
}
|
||
|
||
//查找访客插入访客表
|
||
visitorId := fmt.Sprintf("wxkf|%s|%s", entId, userId)
|
||
vistorInfo := models.FindVisitorByVistorId(visitorId)
|
||
if vistorInfo.ID == 0 {
|
||
accessToken, _ := wework.accessToken()
|
||
ret, _ := getVisitor(accessToken, userId)
|
||
visitorName := gjson.Get(ret, "customer_list.0.nickname").String()
|
||
avator := gjson.Get(ret, "customer_list.0.avatar").String()
|
||
if visitorName == "" {
|
||
visitorName = "微信客服用户"
|
||
}
|
||
if avator == "" {
|
||
avator = "/static/images/we-chat-wx.png"
|
||
}
|
||
vistorInfo = models.Visitor{
|
||
Model: models.Model{
|
||
CreatedAt: time.Now(),
|
||
UpdatedAt: time.Now(),
|
||
},
|
||
Name: visitorName,
|
||
Avator: avator,
|
||
ToId: kfId,
|
||
VisitorId: visitorId,
|
||
Status: 1,
|
||
Refer: "来自微信客服",
|
||
EntId: entId,
|
||
VisitNum: 0,
|
||
City: "微信客服",
|
||
ClientIp: c.ClientIP(),
|
||
}
|
||
vistorInfo.AddVisitor()
|
||
} else {
|
||
go models.UpdateVisitorStatus(visitorId, 3)
|
||
}
|
||
|
||
kefuInfo := models.FindUser(kfId)
|
||
if kefuInfo.ID == 0 {
|
||
kefuInfo = models.FindUserByUid(entId)
|
||
}
|
||
go ws.VisitorOnline(kefuInfo.Name, vistorInfo)
|
||
go ws.VisitorSendToKefu(kefuInfo.Name, vistorInfo, content)
|
||
go service.SendWorkWechatMesage(vistorInfo.EntId, vistorInfo, kefuInfo, content, c)
|
||
}
|
||
func isRetry(signature string) bool {
|
||
var base = "retry:signature:%s"
|
||
key := fmt.Sprintf(base, signature)
|
||
_, found := retryCache.Get(key)
|
||
if found {
|
||
return true
|
||
}
|
||
retryCache.Set(key, "1", 1*time.Minute)
|
||
return false
|
||
}
|
||
|
||
func getMsgs(accessToken, msgToken string) (MsgRet, error) {
|
||
var msgRet MsgRet
|
||
url := "https://qyapi.weixin.qq.com/cgi-bin/kf/sync_msg?access_token=" + accessToken
|
||
method := "POST"
|
||
payload := strings.NewReader(fmt.Sprintf(`{"token" : "%s"}`, msgToken))
|
||
client := &http.Client{}
|
||
req, err := http.NewRequest(method, url, payload)
|
||
if err != nil {
|
||
fmt.Println(err)
|
||
return msgRet, err
|
||
}
|
||
req.Header.Add("Content-Type", "application/json")
|
||
|
||
res, err := client.Do(req)
|
||
if err != nil {
|
||
return msgRet, err
|
||
}
|
||
defer res.Body.Close()
|
||
|
||
body, err := ioutil.ReadAll(res.Body)
|
||
if err != nil {
|
||
log.Println("微信客服读取消息失败,error:", err)
|
||
return msgRet, err
|
||
}
|
||
json.Unmarshal(body, &msgRet)
|
||
return msgRet, nil
|
||
}
|
||
func getVisitor(accessToken, visitorId string) (string, error) {
|
||
var respData string
|
||
url := "https://qyapi.weixin.qq.com/cgi-bin/kf/customer/batchget?access_token=" + accessToken
|
||
method := "POST"
|
||
reqData := map[string]interface{}{
|
||
"external_userid_list": []string{
|
||
visitorId,
|
||
},
|
||
}
|
||
reqBody, err := json.Marshal(reqData)
|
||
if err != nil {
|
||
return respData, err
|
||
}
|
||
|
||
client := &http.Client{}
|
||
req, err := http.NewRequest(method, url, bytes.NewReader(reqBody))
|
||
if err != nil {
|
||
fmt.Println(err)
|
||
return respData, err
|
||
}
|
||
req.Header.Add("Content-Type", "application/json")
|
||
|
||
res, err := client.Do(req)
|
||
if err != nil {
|
||
return respData, err
|
||
}
|
||
defer res.Body.Close()
|
||
|
||
body, err := ioutil.ReadAll(res.Body)
|
||
if err != nil {
|
||
fmt.Println(err)
|
||
return respData, err
|
||
}
|
||
return string(body), nil
|
||
}
|
||
func (this *KefuWework) accessToken() (string, error) {
|
||
var tokenCacheKey = "tokenCache"
|
||
data, found := tokenCache.Get(tokenCacheKey)
|
||
if found {
|
||
return fmt.Sprintf("%v", data), nil
|
||
}
|
||
urlBase := "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s"
|
||
url := fmt.Sprintf(urlBase, this.corpid, this.corpsecret)
|
||
method := "GET"
|
||
client := &http.Client{}
|
||
req, err := http.NewRequest(method, url, nil)
|
||
if err != nil {
|
||
fmt.Println(err)
|
||
return "", err
|
||
}
|
||
res, err := client.Do(req)
|
||
if err != nil {
|
||
fmt.Println(err)
|
||
return "", err
|
||
}
|
||
defer res.Body.Close()
|
||
|
||
body, err := ioutil.ReadAll(res.Body)
|
||
if err != nil {
|
||
fmt.Println(err)
|
||
return "", err
|
||
}
|
||
s := string(body)
|
||
var accessToken AccessToken
|
||
json.Unmarshal([]byte(s), &accessToken)
|
||
token := accessToken.AccessToken
|
||
tokenCache.Set(tokenCacheKey, token, 5*time.Minute)
|
||
return token, nil
|
||
}
|
||
func Gpt3Knowledge(entId, visitorId string, kefuInfo models.User, content string) string {
|
||
config := models.GetEntConfigsMap(entId, "chatGPTUrl", "chatGPTSecret", "QdrantAIStatus", "QdrantAICollect", "chatGPTSystem", "chatGPTPrompt",
|
||
"RobotName", "RobotAvator",
|
||
"chatGPTHistory")
|
||
api := models.FindConfig("BaseGPTKnowledge")
|
||
|
||
messageContent := ""
|
||
if config["QdrantAIStatus"] == "true" && config["QdrantAICollect"] != "" && api != "" {
|
||
kefuInfo.Nickname = tools.Ifelse(config["RobotName"] != "", config["RobotName"], kefuInfo.Nickname).(string)
|
||
path := fmt.Sprintf("%s/%s/searchStream", api, config["QdrantAICollect"])
|
||
data := url.Values{}
|
||
data.Set("keywords", content)
|
||
system := fmt.Sprintf("我希望你扮演%s客服人员", kefuInfo.Nickname)
|
||
if config["chatGPTSystem"] != "" {
|
||
system = config["chatGPTSystem"]
|
||
}
|
||
data.Set("system", system)
|
||
data.Set("prompt", config["chatGPTPrompt"])
|
||
data.Set("gptUrl", config["chatGPTUrl"])
|
||
data.Set("gptSecret", config["chatGPTSecret"])
|
||
//是否传递历史记录
|
||
if config["chatGPTHistory"] == "true" {
|
||
messages := models.FindMessageByQueryPage(1, 6, "visitor_id = ?", visitorId)
|
||
gptMessages := make([]lib.Gpt3Dot5Message, 0)
|
||
for i := len(messages) - 1; i >= 1; i-- {
|
||
reqContent := messages[i].Content
|
||
if messages[i].MesType == "visitor" {
|
||
gptMessages = append(gptMessages, lib.Gpt3Dot5Message{
|
||
Role: "user",
|
||
Content: reqContent,
|
||
})
|
||
} else {
|
||
gptMessages = append(gptMessages, lib.Gpt3Dot5Message{
|
||
Role: "assistant",
|
||
Content: reqContent,
|
||
})
|
||
}
|
||
}
|
||
historyJson, _ := json.Marshal(gptMessages)
|
||
data.Set("history", string(historyJson))
|
||
}
|
||
messageContent, _ = tools.PostForm(path, data)
|
||
}
|
||
return messageContent
|
||
}
|
||
|
||
func callTalk(reply ReplyMsg, accessToken string) error {
|
||
url := "https://qyapi.weixin.qq.com/cgi-bin/kf/send_msg?access_token=" + accessToken
|
||
method := "POST"
|
||
data, err := json.Marshal(reply)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
reqBody := string(data)
|
||
fmt.Println(reqBody)
|
||
payload := strings.NewReader(reqBody)
|
||
client := &http.Client{}
|
||
req, err := http.NewRequest(method, url, payload)
|
||
|
||
if err != nil {
|
||
return err
|
||
}
|
||
req.Header.Add("Content-Type", "application/json")
|
||
|
||
res, err := client.Do(req)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer res.Body.Close()
|
||
body, err := ioutil.ReadAll(res.Body)
|
||
if err != nil {
|
||
fmt.Println(err)
|
||
return err
|
||
}
|
||
s := string(body)
|
||
fmt.Println(s)
|
||
return nil
|
||
}
|
||
|
||
// 查询自己知识库内容
|
||
func SearchLearn(entId, content string) string {
|
||
articles := models.FindArticleList(1, 10, "score desc", "ent_id= ? and title like ?", entId, "%"+content+"%")
|
||
|
||
result := ""
|
||
articleLen := len(articles)
|
||
//未匹配,进入学习库
|
||
if articleLen == 0 {
|
||
if len([]rune(content)) > 1 {
|
||
go service.AddUpdateLearn(entId, content)
|
||
}
|
||
} else if articleLen == 1 {
|
||
result, _ = tools.ReplaceStringByRegex(articles[0].Content, `<[^a>]+>|target="[^"]+"`, "")
|
||
article := models.Article{
|
||
Score: articles[0].Score + 1,
|
||
}
|
||
go article.SaveArticle("ent_id = ? and id = ?", entId, articles[0].Id)
|
||
} else if articleLen > 1 {
|
||
result = "您是不是想问:"
|
||
for _, article := range articles {
|
||
title := strings.Split(strings.ReplaceAll(article.Title, ",", ","), ",")[0]
|
||
result += fmt.Sprintf("\r\n<a href='weixin://bizmsgmenu?msgmenucontent=%s&msgmenuid=%d'>%s</a>", title, article.Id, title)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
//
|