kefu/service/gpt-wework/wechat.go

499 lines
14 KiB
Go
Raw Permalink Normal View History

2024-12-10 02:50:12 +00:00
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
}
//