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%s", title, article.Id, title) } } return result } //