init push

This commit is contained in:
2026-05-21 19:52:45 +08:00
commit e3f75311ab
1280 changed files with 179173 additions and 0 deletions

View File

@@ -0,0 +1,188 @@
package wechat_service
import (
"context"
"sync"
"time"
"github.com/chaitin/panda-wiki/log"
"github.com/chaitin/panda-wiki/repo/pg"
)
type WechatServiceConfig struct {
Ctx context.Context
CorpID string
Token string
EncodingAESKey string
kbID string
Secret string
logger *log.Logger
containKeywords []string
equalKeywords []string
logoUrl string
// db
WeRepo *pg.WechatRepository
}
// 存储ai知识库获取的cursor值以客服为标准方便拉取用户的消息
var KfCursors = &sync.Map{}
// 微信客服发送的消息
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 MsgRequest struct {
Cursor string `json:"cursor"`
Token string `json:"token"`
Limit int `json:"limit"`
VoiceFormat int `json:"voice_format"`
OpenKfid string `json:"open_kfid"`
}
type MsgRet struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
NextCursor string `json:"next_cursor"` // 游标
MsgList []Msg `json:"msg_list"`
HasMore int `json:"has_more"`
}
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"`
}
// send msg to user with message
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"`
}
// send msg to user with url
type ReplyMsgUrl struct {
Touser string `json:"touser,omitempty"`
OpenKfid string `json:"open_kfid,omitempty"`
Msgid string `json:"msgid,omitempty"`
Msgtype string `json:"msgtype,omitempty"`
Link Link `json:"link,omitempty"`
}
type Link struct {
Title string `json:"title,omitempty"`
Desc string `json:"desc,omitempty"`
Url string `json:"url,omitempty"`
ThumbMediaID string `json:"thumb_media_id,omitempty"`
}
// Upload file response
type MediaUploadResponse struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
MediaType string `json:"type"`
MediaID string `json:"media_id"`
CreatedAt string `json:"created_at"`
}
// 获取用户消息应该得到的响应
type WechatCustomerResponse struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
CustomerList []Customer `json:"customer_list"`
InvalidExternalUserIDs []string `json:"invalid_external_userid"`
}
type Customer struct {
ExternalUserID string `json:"external_userid"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Gender int `json:"gender"`
UnionID string `json:"unionid"`
}
type UerInfoRequest struct {
UserID []string `json:"external_userid_list"`
SessionContext int `json:"need_enter_session_context"`
}
// chat status
type Status struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
ServiceState int `json:"service_state"`
ServiceUserId string `json:"servicer_userid"`
}
type HumanList struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
ServicerList []ServicerList `json:"servicer_list"`
}
type ServicerList struct {
UserID string `json:"userid"`
Status int `json:"status"`
}
type TokenCache struct {
AccessToken string
TokenExpire time.Time
Mutex sync.Mutex
}
// Map-based token cache keyed by app credentials
var tokenCacheMap = make(map[string]*TokenCache)
var tokenCacheMapMutex = sync.Mutex{}
// Generate a key for the token cache based on app credentials
func getTokenCacheKey(kbID, secret string) string {
return kbID + ":" + secret
}
type UserImageCache struct {
ImageID string
ImagePath string
ImageExpire time.Time
Mutex sync.Mutex
}
var UImageCache = &UserImageCache{}
type DefaultImageCache struct {
ImageID string
ImageExpire time.Time
Mutex sync.Mutex
}
var DImageCache = &DefaultImageCache{}

View File

@@ -0,0 +1,329 @@
package wechat_service
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"path"
"regexp"
"strings"
"time"
)
// 读取 cursor以客服账号的消息作为key返回对应的cursor值
func getCursor(openKfId string) string {
cursorValue, _ := KfCursors.Load(openKfId)
cursor, _ := cursorValue.(string)
return cursor
}
// 存储 cursor
func setCursor(openKfId, cursor string) {
KfCursors.Store(openKfId, cursor)
}
func CheckSessionState(token, extrenaluserid, kfId string) (int, error) {
var statusrequest struct {
OpenKfId string `json:"open_kfid"`
ExternalUserid string `json:"external_userid"`
}
statusrequest.OpenKfId = kfId
statusrequest.ExternalUserid = extrenaluserid
// 将请求体转换为JSON
jsonBody, err := json.Marshal(statusrequest)
if err != nil {
return 0, err
}
// 获取状态信息
url := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/kf/service_state/get?access_token=%s", token)
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonBody))
if err != nil {
return 0, fmt.Errorf("发送请求失败: %v", err)
}
defer resp.Body.Close()
// 读取响应体
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, fmt.Errorf("读取响应失败: %v", err)
}
var response Status
if err := json.Unmarshal(body, &response); err != nil {
return 0, fmt.Errorf("解析响应失败: %v", err)
}
// 得到用户的状态
if response.ErrCode != 0 {
return 0, fmt.Errorf("获取会话状态失败: %s", response.ErrMsg)
}
return response.ServiceState, nil
}
func ChangeState(token, extrenaluserId, kfId string, state int, serviceId string) error {
var changestate struct {
OpenKfId string `json:"open_kfid"`
ExternalUserid string `json:"external_userid"`
ServiceState int `json:"service_state"`
ServicerUserId string `json:"servicer_userid"`
}
changestate.OpenKfId = kfId
changestate.ExternalUserid = extrenaluserId
changestate.ServiceState = state
changestate.ServicerUserId = serviceId
jsonBody, err := json.Marshal(changestate)
if err != nil {
return err
}
// 发送请求
url := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/kf/service_state/trans?access_token=%s", token)
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonBody))
if err != nil {
return fmt.Errorf("发送请求失败: %v", err)
}
defer resp.Body.Close()
// 读取响应体
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("读取响应失败: %v", err)
}
// 解析响应
var response struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
MsgCode string `json:"msg_code"`
}
if err := json.Unmarshal(body, &response); err != nil {
return fmt.Errorf("解析响应失败: %v", err)
}
// 得到用户的状态
if response.ErrCode != 0 {
return fmt.Errorf("改变用户状态失败: %s", response.ErrMsg)
}
return nil
}
func GetUserInfo(userid string, accessToken string) (*Customer, error) {
userInfoRequest := UerInfoRequest{
UserID: []string{userid},
SessionContext: 0,
}
// 请求获取用户信息的url
url := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/kf/customer/batchget?access_token=%s", accessToken)
jsonBody, err := json.Marshal(userInfoRequest)
if err != nil {
return nil, err
}
// post获取用户的消息信息
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonBody))
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var userInfo WechatCustomerResponse
if err := json.Unmarshal(body, &userInfo); err != nil {
return nil, err
}
if userInfo.ErrCode != 0 {
return nil, fmt.Errorf("获取用户信息失败: %d, %s", userInfo.ErrCode, userInfo.ErrMsg)
}
return &userInfo.CustomerList[0], nil
}
// get image id
func GetUserImageID(accessToken, filePath string) (string, error) {
UImageCache.Mutex.Lock()
defer UImageCache.Mutex.Unlock()
if UImageCache.ImageID != "" && (UImageCache.ImagePath == filePath) && time.Now().Before(UImageCache.ImageExpire.Add(-5*time.Minute)) {
return UImageCache.ImageID, nil
}
// URL
mediaID, err := UploadMediaFromURL(accessToken, filePath)
if err != nil {
return "", err
}
UImageCache.ImagePath = filePath
UImageCache.ImageID = mediaID
UImageCache.ImageExpire = time.Now().Add(72 * time.Hour) // 3 days
return UImageCache.ImageID, nil
}
// get image id
func GetDefaultImageID(accessToken, ImageBase64 string) (string, error) {
DImageCache.Mutex.Lock()
defer DImageCache.Mutex.Unlock()
if DImageCache.ImageID != "" && time.Now().Before(DImageCache.ImageExpire.Add(-5*time.Minute)) {
return DImageCache.ImageID, nil
}
// Base64编码
mediaID, err := UploadMediaFromBase64(accessToken, ImageBase64)
if err != nil {
return "", err
}
DImageCache.ImageID = mediaID
DImageCache.ImageExpire = time.Now().Add(72 * time.Hour) // 3 days
return DImageCache.ImageID, nil
}
// upload media to wechat server from URL
func UploadMediaFromURL(accessToken, fileURL string) (string, error) {
// 处理URL
resp, err := http.Get(fileURL)
if err != nil {
return "", fmt.Errorf("下载图片失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("下载图片失败,状态码: %d", resp.StatusCode)
}
reader := resp.Body
fileName := "image.png" // 默认文件名
// 从URL中提取文件名
if u, err := url.Parse(fileURL); err == nil && u.Path != "" {
if path.Base(u.Path) != "/" && path.Base(u.Path) != "." {
fileName = path.Base(u.Path)
}
}
return uploadMediaToWechat(accessToken, reader, fileName)
}
// upload media to wechat server from Base64
func UploadMediaFromBase64(accessToken, base64Data string) (string, error) {
// 处理Base64编码的图片
parts := strings.SplitN(base64Data, ",", 2)
if len(parts) != 2 {
return "", fmt.Errorf("无效的Base64图片数据")
}
// 解码Base64数据
decodedData, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
return "", fmt.Errorf("解码Base64图片数据失败: %w", err)
}
reader := bytes.NewReader(decodedData)
fileName := "image.png" // const
return uploadMediaToWechat(accessToken, reader, fileName)
}
// upload media to wechat server - common function
func uploadMediaToWechat(accessToken string, reader io.Reader, fileName string) (string, error) {
// 上传文件 req
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("media", fileName)
if err != nil {
return "", err
}
// 将图片数据复制到表单中
_, err = io.Copy(part, reader)
if err != nil {
return "", fmt.Errorf("复制图片数据失败: %w", err)
}
if err := writer.Close(); err != nil {
return "", err
}
url := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=image", accessToken)
req, err := http.NewRequest("POST", url, body)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
client := &http.Client{}
httpResp, err := client.Do(req)
if err != nil {
return "", err
}
defer httpResp.Body.Close()
var result MediaUploadResponse
if err := json.NewDecoder(httpResp.Body).Decode(&result); err != nil {
return "", err
}
if result.ErrCode != 0 {
return "", fmt.Errorf("上传失败: [%d] %s", result.ErrCode, result.ErrMsg)
}
return result.MediaID, nil
}
func getMsgs(accessToken string, msg *WeixinUserAskMsg) (*MsgRet, error) {
var msgRet MsgRet
// 拉取消息的路由
url := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/kf/sync_msg?access_token=%s", accessToken)
cursor := getCursor(msg.OpenKfId)
msgBody := MsgRequest{
OpenKfid: msg.OpenKfId,
Token: msg.Token,
Limit: 1000,
VoiceFormat: 0,
Cursor: cursor,
}
jsonBody, _ := json.Marshal(msgBody)
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonBody)) // 得到对应的回复
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// 反序列化之后
if err := json.Unmarshal([]byte(string(body)), &msgRet); err != nil {
return nil, err
}
return &msgRet, nil
}
// markdowntotext
func MarkdowntoText(md string) string {
md = regexp.MustCompile(`(?m)^#+\s*(.*)$`).ReplaceAllString(md, "$1")
md = regexp.MustCompile(`\*\*([^*]+)\*\*`).ReplaceAllString(md, "$1")
md = regexp.MustCompile(`(?m)^>\s*(.*)$`).ReplaceAllString(md, "【引用】$1")
md = regexp.MustCompile(`(?m)^-{3,}$`).ReplaceAllString(md, "─────────")
md = regexp.MustCompile(`\n{3,}`).ReplaceAllString(md, "\n\n")
md = regexp.MustCompile(`\[\[(\d+)\]\([^)]+\)\]`).ReplaceAllString(md, "[$1]")
md = regexp.MustCompile(`\[(\d+)\]\.\s*\[([^\]]+)\]\([^)]+\)`).ReplaceAllString(md, "[$1]. $2")
md = regexp.MustCompile(`(?m)^【引用】\[(\d+)\].\s*([^\n(]+)\s*\([^)]+\)`).ReplaceAllString(md, "【引用】[$1]. $2")
return strings.TrimSpace(md)
}

View File

@@ -0,0 +1,403 @@
package wechat_service
import (
"bytes"
"context"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
"net/http"
"slices"
"strings"
"time"
"unicode/utf8"
"github.com/google/uuid"
"github.com/samber/lo"
"github.com/sbzhu/weworkapi_golang/wxbizmsgcrypt"
"github.com/chaitin/panda-wiki/domain"
"github.com/chaitin/panda-wiki/log"
"github.com/chaitin/panda-wiki/pkg/bot"
)
func NewWechatServiceConfig(ctx context.Context, logger *log.Logger, KbId, CorpID, Token, EncodingAESKey, secret, logo string, containKeywords, equalKeywords []string) (*WechatServiceConfig, error) {
return &WechatServiceConfig{
Ctx: ctx,
kbID: KbId,
CorpID: CorpID,
Token: Token,
EncodingAESKey: EncodingAESKey,
Secret: secret,
logger: logger,
containKeywords: containKeywords,
equalKeywords: equalKeywords,
logoUrl: logo,
}, nil
}
func (cfg *WechatServiceConfig) VerifyUrlWechatService(signature, timestamp, nonce, echostr string) ([]byte, error) {
wxcpt := wxbizmsgcrypt.NewWXBizMsgCrypt(
cfg.Token,
cfg.EncodingAESKey,
cfg.CorpID,
wxbizmsgcrypt.XmlType,
)
// 验证URL并解密echostr
decryptEchoStr, errCode := wxcpt.VerifyURL(signature, timestamp, nonce, echostr)
if errCode != nil {
return nil, errors.New("server serve fail wechat")
}
// success
return decryptEchoStr, nil
}
func (cfg *WechatServiceConfig) Wechat(msg *WeixinUserAskMsg, getQA bot.GetQAFun) error {
// 获取accesstoken 方便给用户发送消息
token, err := cfg.GetAccessToken()
if err != nil {
return err
}
// 主动拉去用户发送的消息
msgRet, err := getMsgs(token, msg)
if err != nil {
return err
}
if msgRet.NextCursor != "" {
setCursor(msg.OpenKfId, msgRet.NextCursor)
}
err = cfg.Processmessage(msgRet, msg, getQA)
if err != nil {
cfg.logger.Error("send to ai failed!")
return err
}
return nil
}
// forwardToBackend
func (cfg *WechatServiceConfig) Processmessage(msgRet *MsgRet, Kfmsg *WeixinUserAskMsg, GetQA bot.GetQAFun) error {
// err message
cfg.logger.Info("get user message", log.Int("msgRet.Errcode", msgRet.Errcode), log.String("msg.Errmsg", msgRet.Errmsg))
size := len(msgRet.MsgList)
if size < 1 {
return fmt.Errorf("no message received")
}
// 如果是用户刚刚进入会话的事件,那么不需要发送消息给用户
if msgRet.MsgList[size-1].Msgtype == "event" && msgRet.MsgList[size-1].Event.EventType == "enter_session" {
return nil
}
// 每次只是拿去最新的数据
current := msgRet.MsgList[size-1]
userId := current.ExternalUserid
openkfId := current.OpenKfid
content := current.Text.Content
token, _ := cfg.GetAccessToken()
state, err := CheckSessionState(token, userId, openkfId)
if err != nil {
cfg.logger.Error("check session state failed", log.Error(err))
return err
}
if state == 3 { // 人工状态 ---已经是人工,那么就不要需要发消息给用户
cfg.logger.Info("the customer has already in human service")
return nil
}
if len(cfg.equalKeywords) > 0 || len(cfg.containKeywords) > 0 {
if slices.Contains(cfg.equalKeywords, content) || lo.SomeBy(cfg.containKeywords, func(sub string) bool {
return strings.Contains(content, sub)
}) {
// 改变状态为人工接待
// 非人工 ->转人工
humanList, err := cfg.GetKfHumanList(token, openkfId)
if err != nil {
cfg.logger.Error("get human list failed", log.Error(err))
return err
}
// 遍历找到可以接待的员工
for _, servicer := range humanList.ServicerList {
if servicer.Status == 0 { // 可以接待
err := ChangeState(token, userId, openkfId, 3, servicer.UserID)
if err != nil {
cfg.logger.Error("change state to human failed", log.Error(err))
return err
}
cfg.logger.Info("change state to human successful") // 转人工成功
return nil
}
}
// 失败
cfg.logger.Info("no human available")
return cfg.SendResponseToKfTxt(userId, openkfId, "当前没有可用的人工客服", token)
}
}
// 1. first response to user
if err := cfg.SendResponseToKfTxt(userId, openkfId, "正在思考您的问题,请稍等...", token); err != nil {
return err
}
// 获取用户的详细信息
customer, err := GetUserInfo(userId, token)
if err != nil {
cfg.logger.Error("get user info failed", log.Error(err))
}
cfg.logger.Info("customer info", log.Any("customer", customer))
id, err := uuid.NewV7()
if err != nil {
cfg.logger.Error("failed to generate conversation uuid", log.Error(err))
id = uuid.New()
}
conversationID := id.String()
wccontent, err := GetQA(cfg.Ctx, content, domain.ConversationInfo{UserInfo: domain.UserInfo{
UserID: customer.ExternalUserID, // 用户对话的id
NickName: customer.Nickname, //用户微信的昵称
Avatar: customer.Avatar, // 用户微信的头像
From: domain.MessageFromPrivate,
}}, conversationID)
if err != nil {
return err
}
//2. get baseurl and image path
info, err := cfg.WeRepo.GetWechatStatic(cfg.Ctx, cfg.kbID, domain.AppTypeWeb)
if err != nil {
return err
}
//2. go send to ai and store in map--> get conversation-id
if _, ok := domain.ConversationManager.Load(conversationID); !ok {
state := &domain.ConversationState{
Question: content,
NotificationChan: make(chan string), // notification channel
IsVisited: false,
}
domain.ConversationManager.Store(conversationID, state)
go cfg.SendQuestionToAI(conversationID, wccontent)
}
// 3. second send url to user
return cfg.SendResponseToKfUrl(userId, openkfId, conversationID, token, content, info.BaseUrl, info.ImagePath)
}
func (cfg *WechatServiceConfig) getImageID(token, image string) (string, error) {
const minioPrefix = "http://panda-wiki-minio:9000"
// 优先使用配置的logoUrl
if cfg.logoUrl != "" {
image = cfg.logoUrl
}
var imageId string
var err error
switch {
case image == "":
case strings.HasPrefix(image, "data:image/"):
imageId, err = GetDefaultImageID(token, image)
default:
imageId, err = GetUserImageID(token, fmt.Sprintf("%s%s", minioPrefix, image))
}
if imageId != "" && err == nil {
return imageId, nil
}
if err != nil {
cfg.logger.Error("failed to get image ID, using default", log.Error(err))
}
return GetDefaultImageID(token, domain.DefaultPandaWikiIconB64)
}
func (cfg *WechatServiceConfig) SendResponseToKfUrl(userId, openkfId, conversationID, token, question, baseUrl, image string) error {
imageId, err := cfg.getImageID(token, image)
if err != nil {
return err
}
if utf8.RuneCountInString(question) > 35 {
question = string([]rune(question)[:35]) + "......"
}
reply := ReplyMsgUrl{
Touser: userId,
OpenKfid: openkfId,
Msgtype: "link",
Link: Link{
Url: fmt.Sprintf("%s/h5-chat?id=%s", baseUrl, conversationID),
Desc: "本回答由 PandaWiki 基于 AI 生成,仅供参考。",
Title: question,
ThumbMediaID: imageId,
},
}
jsonData, err := json.Marshal(reply)
if err != nil {
return fmt.Errorf("json Marshal failed: %w", err)
}
return cfg.SendMessage(jsonData, token)
}
func (cfg *WechatServiceConfig) SendResponseToKfTxt(userId string, openkfId string, response string, token string) error {
// send text data to user
reply := ReplyMsg{
Touser: userId,
OpenKfid: openkfId,
Msgtype: "text",
Text: struct {
Content string `json:"content,omitempty"`
}{Content: response},
}
jsonData, err := json.Marshal(reply)
if err != nil {
return fmt.Errorf("json Marshal failed: %w", err)
}
return cfg.SendMessage(jsonData, token)
}
func (cfg *WechatServiceConfig) SendMessage(jsonData []byte, token string) error {
// 发送消息给客服
url := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/kf/send_msg?access_token=%s", token)
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("post to wechatservice failed: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read response body failed: %w", err)
}
var res struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
MsgID string `json:"msgid"`
}
if err := json.Unmarshal(body, &res); err != nil {
cfg.logger.Error("解析响应失败", log.Error(err))
return err
}
if res.ErrCode != 0 {
cfg.logger.Error("发送给微信客服消息失败", log.Any("errcode", res.ErrCode), log.Any("errmsg", res.ErrMsg), log.Any("jsonData", string(jsonData)))
return err
}
// 发送消息给微信客服成功
s := string(body)
cfg.logger.Info("response from wechatservice success", log.Any("body", s))
return nil
}
func (cfg *WechatServiceConfig) GetAccessToken() (string, error) {
// Generate cache key based on app credentials
cacheKey := getTokenCacheKey(cfg.kbID, cfg.Secret)
// Get or create token cache for this app
tokenCacheMapMutex.Lock()
tokenCache, exists := tokenCacheMap[cacheKey]
if !exists {
tokenCache = &TokenCache{}
tokenCacheMap[cacheKey] = tokenCache
}
tokenCacheMapMutex.Unlock()
// Lock the specific token cache for this app
tokenCache.Mutex.Lock()
defer tokenCache.Mutex.Unlock()
if tokenCache.AccessToken != "" && time.Now().Before(tokenCache.TokenExpire) {
cfg.logger.Debug("access token has existed and is valid")
return tokenCache.AccessToken, nil
}
if cfg.Secret == "" || cfg.CorpID == "" {
return "", errors.New("secret or corpid is not right")
}
// get AccessToken--请求微信客服token
url := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s", cfg.CorpID, cfg.Secret)
resp, err := http.Get(url)
if err != nil {
return "", errors.New("get wechatservice accesstoken failed")
}
defer resp.Body.Close()
var tokenResp AccessToken // 获取到token消息
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return "", errors.New("json decode wechat resp failed")
}
if tokenResp.Errcode != 0 {
return "", errors.New("get wechat access token failed")
}
// success
cfg.logger.Info("wechatservice get accesstoken success", log.Any("info", tokenResp.AccessToken))
tokenCache.AccessToken = tokenResp.AccessToken
tokenCache.TokenExpire = time.Now().Add(time.Duration(tokenResp.ExpiresIn-300) * time.Second)
return tokenCache.AccessToken, nil
}
// 解析微信客服消息
func (cfg *WechatServiceConfig) UnmarshalMsg(decryptMsg []byte) (*WeixinUserAskMsg, error) {
var msg WeixinUserAskMsg
err := xml.Unmarshal([]byte(decryptMsg), &msg)
return &msg, err
}
func (cfg *WechatServiceConfig) GetKfHumanList(token string, KfId string) (*HumanList, error) {
url := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/kf/servicer/list?access_token=%s&open_kfid=%s", token, KfId)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var servicerResp HumanList
if err := json.Unmarshal(body, &servicerResp); err != nil {
return nil, err
}
if servicerResp.ErrCode != 0 {
return nil, fmt.Errorf("获取客服列表失败: %d, %s", servicerResp.ErrCode, servicerResp.ErrMsg)
}
return &servicerResp, nil
}
// answer set into redis queue and set useful time
func (cfg *WechatServiceConfig) SendQuestionToAI(conversationID string, wccontent chan string) {
// send message
val, _ := domain.ConversationManager.Load(conversationID)
state := val.(*domain.ConversationState)
for content := range wccontent {
state.Mutex.Lock()
if state.IsVisited {
state.NotificationChan <- content // notify has new data
}
state.Buffer.WriteString(content)
state.Mutex.Unlock()
}
// end sent notification
defer func() {
close(state.NotificationChan)
domain.ConversationManager.Delete(conversationID)
}()
}