kefu/service/gpt-wework/wechat.go

499 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
//