├── .gitignore ├── .vscode └── launch.json ├── README.md ├── attach ├── 2882303761518338059_1.info.etd └── 2882303761518338059_2.info.etd ├── conf ├── app.go └── conf.yaml ├── grpcc └── client.go ├── main.go ├── mimc.log ├── robot ├── handler_message.go ├── handler_mimc_token.go ├── handler_robot.go ├── handler_status.go └── handler_token.go └── services ├── admin.go ├── contact.go ├── knowledge_base.go ├── message.go ├── robot.go ├── statistical.go └── user.go /.gitignore: -------------------------------------------------------------------------------- 1 | mimc.log 2 | nohup.out 3 | .vscode 4 | attach -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Package", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "debug", 12 | "program": "${workspaceFolder}" 13 | } 14 | 15 | ] 16 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 客服系统开发者QQ交流群: 623661658 2 | 3 | # 开源客服系统-机器人 v2.0.0 4 | 客服系统仓库地址: [前往>>>][1] 5 | 6 | ## 以下是v2.0.0版本的重要更新 7 | - 对前面版本进行重构,分离业务逻辑与机器人的混搭运行弊端 8 | - 做了大量优化,在很大程度上提升了性能,并代码松耦合, 9 | - 业务系统支持负载均衡了,这是对v1.0的重大里程碑更新, 对接海量客户不再愁了 10 | - 增加工单系统能力,无在线客服接待?不用怕,工单来给您解决一切问题 11 | - 代码可读性大大提高,初学者都能看懂的代码,还有什么理由不学习一下呢 12 | - 定时清理无接入人工记录的用户,避免数据沉淀 13 | - H5客户端增加了重连机制 14 | - 客户端只保留30天聊天记录,已分表处理 15 | 16 | ## 目录文件结构说明 17 | | | | 18 | | :-------- | :-------- | 19 | | - conf | 项目配置文件 | 20 | | - grpcc | rpc链接层 | 21 | | - robot | 机器人相关操作 | 22 | | - services | 服务提供者-通过rpc调取服务端获取数据 | 23 | | - processguard_robot.sh | Linux 进程守护shell脚本 | 24 | 25 | 26 | ## 安装 27 | - cd $GOPATH/src && git clone https://github.com/chenxianqi/kefu_go_robot 28 | - cd kefu_go_robot && go get 29 | 30 | ## 启动 31 | 请先启动服务端再启动机器人 32 | go run main.go 33 | 34 | 35 | ## 打包发布 linux (其它运行环境编译请自行search baidu) 36 | - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go 37 | 编译完成将生成一个、main 可执行文件,改名成 kefu_go_robot 38 | 新建一个robot目录将编译后改名的kefu_go_robot文件放进去,然后在robot目录创建一个conf文件夹,将conf/conf.yaml文件拷贝一份进来,最后将processguard_robot.sh可执行文件也拷贝进来,robot项目就打包完成了 39 | 40 | - 启动:先启动服务端后 cd robot && nohub ./processguard_robot.sh & 41 | 42 | 43 | ## LICENSE 44 | 45 | Copyright 2019 keith 46 | 47 | Licensed to the Apache Software Foundation (ASF) under one or more contributor 48 | license agreements. See the NOTICE file distributed with this work for 49 | additional information regarding copyright ownership. The ASF licenses this 50 | file to you under the Apache License, Version 2.0 (the "License"); you may not 51 | use this file except in compliance with the License. You may obtain a copy of 52 | the License at 53 | 54 | http://www.apache.org/licenses/LICENSE-2.0 55 | 56 | Unless required by applicable law or agreed to in writing, software 57 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 58 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 59 | License for the specific language governing permissions and limitations under 60 | the License. 61 | 62 | 63 | 64 | [1]: https://github.com/chenxianqi/kefu_server 65 | 66 | -------------------------------------------------------------------------------- /attach/2882303761518338059_1.info.etd: -------------------------------------------------------------------------------- 1 | {"appAccount":"1","appId":2882303761518338059,"appPackage":"com.cmp520.WanChe3","chid":9,"feAddress":["111.202.1.248:443","111.202.1.250:443","123.125.102.214:5222","123.125.102.213:5222","183.84.5.53:5222","183.84.6.155:5222","58.83.177.220:80","58.83.177.235:80"],"feDomain":"app.chat.xiaomi.net","resource":"mimc_go_ceAiaUGwND","securityKey":"RHvcdA4btfGHwok0dm2NhQ==","token":"bJRLeg7AgtSh0T13YjL/IDlYdxk/PPVP4KKNz2PDl7fHvGMgjiYWWB5T8fgQFEB8ATwV8AOv6mTaabs5MRZPoYLGcHvG4E0t+JF2UGgP+PrO0hET5Ehg3ezEAG3jZ+xlFfhWgu2wn1E4FHDn6r6+ywIdAM4Psdx9gQOeIIYFEMiYTcauAfyny03VYWJFRU6vYJ+SBZB8sXwhn6ld2gg+M3geWYOyFxmMW+766vsPMXpCEBKbDkeYWn9SwGt7DiqoDqP7FGibwZcTkGvnBSExoun3qWytj8OiqeFjzjr+d1cpuOt5nxyhGVGjVW6xdEyisaVp6lAHmyGOksLpouPW3Ez0wrUOQ7zwSYI/dsmwPZGP7iMjRDSMFo4rIzukDpcDyUsOjXV414xuPX3WXq5eF3jeHFpwO6XdQTtusm0HR9Nqk3DkcnaYzVN1W0BZkMM5l2u2zAlxHJaxf+Ym1maukA==","uuid":26320908851609602} -------------------------------------------------------------------------------- /attach/2882303761518338059_2.info.etd: -------------------------------------------------------------------------------- 1 | {"appAccount":"2","appId":2882303761518338059,"appPackage":"com.cmp520.WanChe3","chid":9,"feAddress":["111.202.1.250:443","111.202.1.248:443","123.125.102.214:5222","123.125.102.213:5222","183.84.5.53:5222","183.84.6.155:5222","58.83.177.220:80","58.83.177.235:80"],"feDomain":"app.chat.xiaomi.net","resource":"mimc_go_tRhXKZvIil","securityKey":"jdwu5qn2xL31garHLL/XYQ==","token":"bJRLeg7AgtSh0T13YjL/IDlYdxk/PPVP4KKNz2PDl7fHvGMgjiYWWB5T8fgQFEB8ATwV8AOv6mTaabs5MRZPoYLGcHvG4E0t+JF2UGgP+PrO0hET5Ehg3ezEAG3jZ+xlFfhWgu2wn1E4FHDn6r6+ywIdAM4Psdx9gQOeIIYFEMj6Oeqd8am4VoZM8Z3wL4YG0/vbMZ3iYy5VvcdFcvGnid3/cM4i020NPqygmwV4qW6MrE20CF17MMACM0Bk5i9aNE33uMzlsGqbanQ7Dk6dQ7i6LNTRkbGj0h17A2TzxjtW8qz9dy9ktuZxnU9T4i0oH1vJUNLCrSGaqixoP3MsVaJn19206P8PpFhMvN7g92ZtWlXlvnIg+/+qQEJLVx+Q4DWb/GZeP4S/yS3OQ64AW2mOgLxuuZrg5WezXjw5EKn0j+RAMtvYYzAb8AREIYJ1SA983amJIHXDx3IssfaZcQ==","uuid":26321144806375426} -------------------------------------------------------------------------------- /conf/app.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | 7 | "gopkg.in/yaml.v2" 8 | ) 9 | 10 | // Cionfigs struct 11 | type Cionfigs struct { 12 | MiHost string `yaml:"mi_host"` 13 | MiAppID string `yaml:"mi_app_id"` 14 | MiAppKey string `yaml:"mi_app_key"` 15 | MiAppSecret string `yaml:"mi_app_secret"` 16 | GatewayHost string `yaml:"gateway_host"` 17 | GrpcHost string `yaml:"grpc_host"` 18 | GrpcPort string `yaml:"grpc_port"` 19 | } 20 | 21 | // GetConfigs instance 22 | func (c *Cionfigs) GetConfigs() *Cionfigs { 23 | yamlFile, err := ioutil.ReadFile("conf/conf.yaml") 24 | if err != nil { 25 | fmt.Println(err.Error()) 26 | } 27 | err = yaml.Unmarshal(yamlFile, c) 28 | if err != nil { 29 | fmt.Println(err.Error()) 30 | } 31 | return c 32 | } 33 | -------------------------------------------------------------------------------- /conf/conf.yaml: -------------------------------------------------------------------------------- 1 | 2 | # see https://admin.mimc.chat.xiaomi.net/docs/02-createapp.html 3 | 4 | # 网关APP 5 | gateway_host: http://localhost:8080 6 | 7 | # 小米消息云签名信息 8 | mi_host: "https://mimc.chat.xiaomi.net/api/account/token" 9 | mi_app_id: 2882303761518338059 10 | mi_app_key: "5201833828059" 11 | mi_app_secret: "wjLFWivIORCFsi3tHr9wHQ==" 12 | 13 | # kefu server gorc 14 | grpc_host: 127.0.0.1 15 | grpc_port: 8028 -------------------------------------------------------------------------------- /grpcc/client.go: -------------------------------------------------------------------------------- 1 | package grpcc 2 | 3 | import ( 4 | "fmt" 5 | "kefu_go_robot/conf" 6 | "kefu_server/grpcs" 7 | "sync" 8 | "sync/atomic" 9 | "unsafe" 10 | 11 | "google.golang.org/grpc" 12 | ) 13 | 14 | var ( 15 | globalClientConn unsafe.Pointer 16 | lck sync.Mutex 17 | ) 18 | 19 | // GrpcClient get grpc cline instance 20 | func GrpcClient() grpcs.KefuClient { 21 | conn, err := initConn() 22 | if err != nil { 23 | fmt.Print("grpcClient grpc CONNECT err:" + err.Error()) 24 | return (grpcs.NewKefuClient)(nil) 25 | } 26 | return grpcs.NewKefuClient(conn) 27 | } 28 | 29 | // initConn get connect 30 | func initConn() (*grpc.ClientConn, error) { 31 | if atomic.LoadPointer(&globalClientConn) != nil { 32 | return (*grpc.ClientConn)(globalClientConn), nil 33 | } 34 | lck.Lock() 35 | defer lck.Unlock() 36 | if atomic.LoadPointer(&globalClientConn) != nil { 37 | return (*grpc.ClientConn)(globalClientConn), nil 38 | } 39 | cli, err := newGrpcConn() 40 | if err != nil { 41 | return nil, err 42 | } 43 | atomic.StorePointer(&globalClientConn, unsafe.Pointer(cli)) 44 | return cli, nil 45 | } 46 | 47 | // newGrpcConn 48 | func newGrpcConn() (*grpc.ClientConn, error) { 49 | config := new(conf.Cionfigs).GetConfigs() 50 | host := config.GrpcHost + ":" + config.GrpcPort 51 | conn, err := grpc.Dial( 52 | host, 53 | grpc.WithInsecure(), 54 | ) 55 | if err != nil { 56 | return nil, err 57 | } 58 | fmt.Printf("grpcClient grpc success") 59 | return conn, nil 60 | } 61 | 62 | // Run grpc client 63 | func Run() { 64 | initConn() 65 | } 66 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "kefu_go_robot/grpcc" 5 | "kefu_go_robot/robot" 6 | "time" 7 | 8 | "github.com/astaxie/beego/logs" 9 | ) 10 | 11 | func main() { 12 | 13 | // robot log 14 | logs.SetLogger(logs.AdapterFile, `{"filename":"project_robot.log","level":6,"maxlines":0,"maxsize":0,"daily":true,"maxdays":10,"color":true}`) 15 | logs.EnableFuncCallDepth(true) 16 | 17 | // grpcc init 18 | grpcc.Run() 19 | 20 | // RobotRun 21 | robot.Run() 22 | 23 | // Restart all robots every 60 minutes 24 | c := time.Tick(60 * 60 * time.Second) 25 | for { 26 | <-c 27 | go robot.Run() 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /mimc.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenxianqi/kefu_go_robot/03bf4b827ffa52747639b351bfbef2da2e6f355f/mimc.log -------------------------------------------------------------------------------- /robot/handler_message.go: -------------------------------------------------------------------------------- 1 | package robot 2 | 3 | import ( 4 | "container/list" 5 | "encoding/base64" 6 | "encoding/json" 7 | "kefu_go_robot/services" 8 | "kefu_server/models" 9 | "kefu_server/utils" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "github.com/Xiaomi-mimc/mimc-go-sdk" 15 | msg "github.com/Xiaomi-mimc/mimc-go-sdk/message" 16 | "github.com/astaxie/beego/logs" 17 | ) 18 | 19 | // create a message 20 | func createMessage( 21 | bizType string, 22 | fromAccount int64, 23 | toAccount int64, 24 | payload string, 25 | transferAccount int64, 26 | platform int64, 27 | read int, 28 | ) models.Message { 29 | return models.Message{ 30 | FromAccount: fromAccount, 31 | ToAccount: toAccount, 32 | BizType: bizType, 33 | Timestamp: time.Now().Unix() + 1, 34 | Key: time.Now().Unix(), 35 | TransferAccount: transferAccount, 36 | Platform: platform, 37 | Payload: payload, 38 | Read: read, 39 | } 40 | } 41 | 42 | // adminData struct 43 | type adminData struct { 44 | ID int64 `json:"id"` 45 | NickName string `json:"nickname"` 46 | Avatar string `json:"avatar"` 47 | } 48 | 49 | // HandleMessage ... 50 | func (c MsgHandler) HandleMessage(packets *list.List) { 51 | for ele := packets.Front(); ele != nil; ele = ele.Next() { 52 | 53 | // 收到的原始消息 54 | p2pMsg := ele.Value.(*msg.P2PMessage) 55 | 56 | // get message 57 | var message models.Message 58 | msgContentByte, _ := base64.StdEncoding.DecodeString(string(p2pMsg.Payload())) 59 | json.Unmarshal(msgContentByte, &message) 60 | 61 | // toAccount == 1 return 62 | if message.FromAccount == 1 { 63 | return 64 | } 65 | 66 | // 消息入库 67 | if message.BizType == "into" { 68 | messageString := utils.InterfaceToString(message.Payload) 69 | services.GetMessageRepositoryInstance().InsertMessage(messageString) 70 | return 71 | } 72 | 73 | // 当前服务机器人 74 | var mcUserRobot *mimc.MCUser 75 | robot := GetRunRobotInfo(message.ToAccount) 76 | if robot == nil { 77 | logs.Info("robot info == nil \r\n") 78 | return 79 | } 80 | if robot != nil && robot.ID == message.FromAccount { 81 | return 82 | } 83 | for _, mcRbt := range MCUserRobots { 84 | rbtID, _ := strconv.ParseInt(mcRbt.AppAccount(), 10, 64) 85 | if robot != nil && rbtID == robot.ID { 86 | mcUserRobot = mcRbt 87 | break 88 | } 89 | } 90 | 91 | // 撤销消息 92 | if message.BizType == "cancel" { 93 | key, _ := strconv.ParseInt(message.Payload, 10, 64) 94 | services.GetMessageRepositoryInstance().CancelMessage( 95 | models.RemoveMessageRequestDto{ 96 | FromAccount: message.FromAccount, 97 | ToAccount: message.ToAccount, 98 | Key: key, 99 | }) 100 | return 101 | } 102 | // 返回给对方的消息内容 103 | var messageContent string = "" 104 | var bizType string = "text" 105 | knowledgeBaseRepository := services.GetKnowledgeBaseRepositoryInstance() 106 | // 搜索知识库 107 | if message.BizType == "search_knowledge" { 108 | bizType = "search_knowledge" 109 | payload := strings.Trim(strings.ToLower(message.Payload), " ") 110 | if payload == "" { 111 | return 112 | } 113 | knowledgeTitles := knowledgeBaseRepository.SearchKnowledgeTitles(models.KnowledgeBaseTitleRequestDto{ 114 | Payload: payload, 115 | KeyWords: robot.KeyWord, 116 | Platform: message.Platform, 117 | IsSerachSub: false, 118 | Limit: 5, 119 | }) 120 | knowledgeTitlesByte, _ := json.Marshal(knowledgeTitles) 121 | messageContent = string(knowledgeTitlesByte) 122 | 123 | } else if message.BizType == "handshake" { 124 | 125 | // 与机器人握手 126 | messageContent = robot.Welcome 127 | bizType = "welcome" 128 | 129 | } else { 130 | 131 | // 判断是否符合转人工 132 | artificial := strings.Split(strings.Trim(robot.Artificial, "|"), "|") 133 | isTransfer := false 134 | if message.Payload == "人工" { 135 | isTransfer = true 136 | } else { 137 | for i := 0; i < len(artificial); i++ { 138 | if artificial[i] == message.Payload { 139 | isTransfer = true 140 | break 141 | } 142 | } 143 | } 144 | // 符合 145 | if isTransfer { 146 | admins := services.GetAdminRepositoryInstance().GetOnlineAdmins() 147 | if len(admins) <= 0 { 148 | messageContent = robot.NoServices 149 | // 发送一条工单提醒 150 | go func() { 151 | time.Sleep(time.Second * 1) 152 | newMsg := models.Message{} 153 | newMsg.Platform = message.Platform 154 | newMsg.BizType = "workorder" 155 | newMsg.FromAccount = robot.ID 156 | newMsg.ToAccount = message.FromAccount 157 | newMsg.Timestamp = time.Now().Unix() 158 | newMsg.Read = 1 159 | newMsg.Payload = "当前暂无客服在线,您可以发送工单留言~" 160 | newMsgBase64 := utils.InterfaceToString(newMsg) 161 | mcUserRobot.SendMessage(strconv.FormatInt(message.FromAccount, 10), []byte(newMsgBase64)) 162 | }() 163 | } else { 164 | 165 | // 平均分配客服 166 | admin := admins[0] 167 | adminDataJSON, _ := json.Marshal(adminData{ID: admin.ID, NickName: admin.NickName, Avatar: admin.Avatar}) 168 | messageContent = string(adminDataJSON) 169 | 170 | // 发送一条消息告诉客服端 171 | var newMsgBase64 string 172 | newMsg := models.Message{} 173 | newMsg.Platform = message.Platform 174 | newMsg.BizType = "transfer" 175 | newMsg.FromAccount = message.FromAccount 176 | newMsg.ToAccount = admin.ID 177 | newMsg.Timestamp = time.Now().Unix() 178 | newMsg.TransferAccount = admin.ID 179 | newMsg.Read = 1 180 | newMsg.Payload = "系统将客户分配给您" 181 | newMsgBase64 = utils.InterfaceToString(newMsg) 182 | 183 | // 发送与消息入库 184 | services.GetMessageRepositoryInstance().InsertMessage(utils.InterfaceToString(newMsgBase64)) 185 | mcUserRobot.SendMessage(strconv.FormatInt(admin.ID, 10), []byte(newMsgBase64)) 186 | 187 | newMsg.FromAccount = robot.ID 188 | newMsg.ToAccount = message.FromAccount 189 | newMsg.Payload = messageContent 190 | newMsgBase64 = utils.InterfaceToString(newMsg) 191 | mcUserRobot.SendMessage(strconv.FormatInt(message.FromAccount, 10), []byte(newMsgBase64)) 192 | 193 | // 发送与消息入库 194 | go func() { 195 | 196 | // 帮助客服发送欢迎语 197 | newMsg.BizType = "text" 198 | newMsg.Payload = admin.AutoReply 199 | newMsg.ToAccount = message.FromAccount 200 | newMsg.FromAccount = admin.ID 201 | newMsgBase64 = utils.InterfaceToString(newMsg) 202 | 203 | time.Sleep(time.Second * 1) 204 | mcUserRobot.SendMessage(strconv.FormatInt(admin.ID, 10), []byte(newMsgBase64)) 205 | mcUserRobot.SendMessage(strconv.FormatInt(message.FromAccount, 10), []byte(newMsgBase64)) 206 | services.GetMessageRepositoryInstance().InsertMessage(utils.InterfaceToString(newMsgBase64)) 207 | 208 | // 推送列表给客服 209 | services.GetContactRepositoryInstance().PushNewContacts(strconv.FormatInt(admin.ID, 10)) 210 | 211 | // 更新用户信息 212 | services.GetUserRepositoryInstance().Update(models.User{ID: message.FromAccount, IsService: 1}) 213 | 214 | }() 215 | 216 | // 转接入库用于统计服务次数 217 | servicesStatistical := models.ServicesStatistical{UserAccount: message.FromAccount, ServiceAccount: admin.ID, Platform: message.Platform, TransferAccount: robot.ID, CreateAt: time.Now().Unix()} 218 | services.GetStatisticalRepositoryInstance().Add(servicesStatistical) 219 | return 220 | } 221 | 222 | } else { 223 | 224 | // 完全匹配知识库 225 | _knowledgeBase := knowledgeBaseRepository.GetKnowledgeBaseWithTitleAndPlatform(message.Payload, message.Platform) 226 | if _knowledgeBase != nil { 227 | bizType = "text" 228 | messageContent = _knowledgeBase.Content 229 | } else { 230 | 231 | bizType = "knowledge" 232 | // 找主标题 233 | knowledgeTitles := knowledgeBaseRepository.SearchKnowledgeTitles(models.KnowledgeBaseTitleRequestDto{ 234 | Payload: strings.Trim(strings.ToLower(message.Payload), " "), 235 | KeyWords: "", 236 | IsSerachSub: false, 237 | Platform: message.Platform, 238 | Limit: 4, 239 | }) 240 | if len(knowledgeTitles) > 0 { 241 | messageContentByte, _ := json.Marshal(knowledgeTitles) 242 | messageContent = string(messageContentByte) 243 | } else { 244 | 245 | // 找副标题 246 | knowledgeTitles := knowledgeBaseRepository.SearchKnowledgeTitles(models.KnowledgeBaseTitleRequestDto{ 247 | Payload: strings.Trim(strings.ToLower(message.Payload), " "), 248 | KeyWords: "", 249 | IsSerachSub: true, 250 | Platform: message.Platform, 251 | Limit: 4, 252 | }) 253 | if len(knowledgeTitles) > 0 { 254 | messageContentByte, _ := json.Marshal(knowledgeTitles) 255 | messageContent = string(messageContentByte) 256 | } else { 257 | knowledgeTitles := knowledgeBaseRepository.SearchKnowledgeTitles(models.KnowledgeBaseTitleRequestDto{ 258 | Payload: strings.Trim(strings.ToLower(message.Payload), " "), 259 | KeyWords: robot.KeyWord, 260 | Platform: message.Platform, 261 | IsSerachSub: true, 262 | Limit: 4, 263 | }) 264 | if len(knowledgeTitles) > 0 { 265 | bizType = "knowledge" 266 | messageContentByte, _ := json.Marshal(knowledgeTitles) 267 | messageContent = string(messageContentByte) 268 | } else { 269 | bizType = "text" 270 | messageContent = robot.Understand 271 | } 272 | } 273 | 274 | } 275 | 276 | } 277 | 278 | } 279 | 280 | } 281 | // 消息体 282 | _message := models.Message{} 283 | _message.BizType = bizType 284 | _message.FromAccount = message.ToAccount 285 | _message.Timestamp = time.Now().Unix() + 1 286 | _message.ToAccount = message.FromAccount 287 | _message.Key = time.Now().Unix() 288 | _message.Platform = message.Platform 289 | _message.Payload = messageContent 290 | messageString := utils.InterfaceToString(_message) 291 | 292 | // 发给用户 293 | mcUserRobot.SendMessage(strconv.FormatInt(message.FromAccount, 10), []byte(messageString)) 294 | 295 | // 消息入库 296 | _messageString := utils.InterfaceToString(messageString) 297 | services.GetMessageRepositoryInstance().InsertMessage(_messageString) 298 | 299 | } 300 | 301 | } 302 | 303 | // HandleGroupMessage ... 304 | func (c MsgHandler) HandleGroupMessage(packets *list.List) { 305 | //for ele := packets.Front(); ele != nil; ele = ele.Next() { 306 | // p2tmsg := ele.Value.(*msg.P2TMessage) 307 | // logger.Info("[%v] [handle p2t msg]%v -> %v: %v, pcktId: %v, timestamp: %v.", c.appAccount, *(p2tmsg.FromAccount()), *(p2tmsg.GroupId()), string(p2tmsg.Payload()), *(p2tmsg.PacketId()), *(p2tmsg.Timestamp())) 308 | //} 309 | } 310 | 311 | // HandleServerAck ... 312 | func (c MsgHandler) HandleServerAck(packetID *string, sequence, timestamp *int64, errMsg *string) { 313 | //logs.Info("[%v] [handle server ack] packetId:%v, seqId: %v, timestamp:%v.", c.appAccount, *packetId, *sequence, *timestamp) 314 | } 315 | 316 | // HandleSendMessageTimeout ... 317 | func (c MsgHandler) HandleSendMessageTimeout(message *msg.P2PMessage) { 318 | //logs.Info("[%v] [handle p2pmsg timeout] packetId:%v, msg:%v, time: %v.", c.appAccount, *(message.PacketId()), string(message.Payload()), time.Now()) 319 | } 320 | 321 | // HandleSendGroupMessageTimeout ... 322 | func (c MsgHandler) HandleSendGroupMessageTimeout(message *msg.P2TMessage) { 323 | // logger.Info("[%v] [handle p2tmsg timeout] packetId:%v, msg:%v.", c.appAccount, *(message.PacketId()), string(message.Payload())) 324 | } 325 | -------------------------------------------------------------------------------- /robot/handler_mimc_token.go: -------------------------------------------------------------------------------- 1 | package robot 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "kefu_go_robot/conf" 8 | "net/http" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | // TokenHandler ... 14 | type TokenHandler struct { 15 | httpURL string 16 | AppID int64 `json:"appId"` 17 | AppKey string `json:"appKey"` 18 | AppSecret string `json:"appSecret"` 19 | AppAccount string `json:"appAccount"` 20 | } 21 | 22 | // GetMiMcToken ... 23 | func GetMiMcToken(accountID string) (string, error) { 24 | config := new(conf.Cionfigs).GetConfigs() 25 | tokenHandler := new(TokenHandler) 26 | tokenHandler.httpURL = config.MiHost 27 | tokenHandler.AppID, _ = strconv.ParseInt(config.MiAppID, 10, 64) 28 | tokenHandler.AppKey = config.MiAppKey 29 | tokenHandler.AppSecret = config.MiAppSecret 30 | tokenHandler.AppAccount = accountID 31 | jsonBytes, err := json.Marshal(*tokenHandler) 32 | if err != nil { 33 | return "", err 34 | } 35 | requestJSONBody := bytes.NewBuffer(jsonBytes).String() 36 | request, err := http.Post(tokenHandler.httpURL, "application/json", strings.NewReader(requestJSONBody)) 37 | if err != nil { 38 | return "", err 39 | } 40 | defer request.Body.Close() 41 | body, err := ioutil.ReadAll(request.Body) 42 | if err != nil { 43 | return "", err 44 | } 45 | token := string(body) 46 | return token, nil 47 | } 48 | -------------------------------------------------------------------------------- /robot/handler_robot.go: -------------------------------------------------------------------------------- 1 | package robot 2 | 3 | import ( 4 | "kefu_go_robot/conf" 5 | "kefu_go_robot/services" 6 | "kefu_server/models" 7 | "strconv" 8 | 9 | "github.com/Xiaomi-mimc/mimc-go-sdk" 10 | "github.com/astaxie/beego/logs" 11 | ) 12 | 13 | // NewMsgHandler ... 14 | func NewMsgHandler(appAccount string) *MsgHandler { 15 | return &MsgHandler{appAccount} 16 | } 17 | 18 | // MsgHandler ... 19 | type MsgHandler struct { 20 | appAccount string 21 | } 22 | 23 | // MCUserRobots 工作中的机器人 24 | var MCUserRobots []*mimc.MCUser 25 | 26 | // Robots 机器人资料列表 27 | var Robots []*models.Robot 28 | 29 | // CreateRobot 创建机器人 30 | func CreateRobot(appAccount string) *mimc.MCUser { 31 | config := new(conf.Cionfigs).GetConfigs() 32 | appID, _ := strconv.ParseInt(config.MiAppID, 10, 64) 33 | mcUser := mimc.NewUser(uint64(appID), appAccount) 34 | mcUser.RegisterStatusDelegate(NewStatusHandler(appAccount)) 35 | mcUser.RegisterTokenDelegate(NewTokenHandler(appAccount)) 36 | mcUser.RegisterMessageDelegate(NewMsgHandler(appAccount)) 37 | mcUser.InitAndSetup() 38 | return mcUser 39 | } 40 | 41 | // GetRunRobotInfo Get current robot 42 | func GetRunRobotInfo(id int64) *models.Robot { 43 | for _, robot := range Robots { 44 | if robot.ID == id { 45 | return robot 46 | } 47 | } 48 | return nil 49 | } 50 | 51 | // GetOnlineRobots get robot all 52 | func GetOnlineRobots() []*models.Robot { 53 | RobotRepository := services.GetRobotRepositoryInstance() 54 | robots := RobotRepository.GetOnlineAllRobots() 55 | return robots 56 | } 57 | 58 | // Run init 59 | func Run() { 60 | 61 | // Log out if any robot is working 62 | if len(MCUserRobots) > 0 { 63 | for _, robot := range MCUserRobots { 64 | robot.Logout() 65 | robot.Destory() 66 | } 67 | MCUserRobots = []*mimc.MCUser{} 68 | } 69 | Robots = GetOnlineRobots() 70 | var tempRobots []*mimc.MCUser 71 | for _, robot := range Robots { 72 | if robot.Switch == 1 { 73 | rb := CreateRobot(strconv.FormatInt(robot.ID, 10)) 74 | tempRobots = append(tempRobots, rb) 75 | rb.Login() 76 | } 77 | } 78 | MCUserRobots = tempRobots 79 | 80 | logs.Info("Robot run success is online robot count ==%v \r\n", len(Robots)) 81 | 82 | } 83 | -------------------------------------------------------------------------------- /robot/handler_status.go: -------------------------------------------------------------------------------- 1 | package robot 2 | 3 | import ( 4 | "github.com/astaxie/beego/logs" 5 | ) 6 | 7 | // StatusHandler struct 8 | type StatusHandler struct { 9 | appAccount string 10 | } 11 | 12 | // NewStatusHandler newStatusHandler 13 | func NewStatusHandler(appAccount string) *StatusHandler { 14 | return &StatusHandler{appAccount} 15 | } 16 | 17 | // HandleChange handleChange 18 | func (c StatusHandler) HandleChange(isOnline bool, errType, errReason, errDescription *string) { 19 | if isOnline { 20 | logs.Info("机器人:%v 上线 status changed: online. \r\n", c.appAccount) 21 | } else { 22 | // 有机器人掉线,重新登录 23 | logs.Info("[机器人:%v 挂掉了] status changed: offline,errType:%v, errReason:%v, errDes:%v\r\n", c.appAccount, *errType, *errReason, *errDescription) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /robot/handler_token.go: -------------------------------------------------------------------------------- 1 | package robot 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "kefu_go_robot/conf" 8 | "net/http" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/astaxie/beego/logs" 13 | ) 14 | 15 | // NewTokenHandler ... 16 | func NewTokenHandler(appAccount string) *TokenHandler { 17 | config := new(conf.Cionfigs).GetConfigs() 18 | tokenHandler := new(TokenHandler) 19 | tokenHandler.httpURL = config.MiHost 20 | tokenHandler.AppID, _ = strconv.ParseInt(config.MiAppID, 10, 64) 21 | tokenHandler.AppKey = config.MiAppKey 22 | tokenHandler.AppSecret = config.MiAppSecret 23 | tokenHandler.AppAccount = appAccount 24 | return tokenHandler 25 | } 26 | 27 | // FetchToken ... 28 | func (c *TokenHandler) FetchToken() *string { 29 | jsonBytes, err := json.Marshal(*c) 30 | if err != nil { 31 | logs.Info("FetchToken error==%v", err) 32 | return nil 33 | } 34 | requestJSONBodygo := bytes.NewBuffer(jsonBytes).String() 35 | request, err := http.Post(c.httpURL, "application/json", strings.NewReader(requestJSONBodygo)) 36 | if err != nil { 37 | logs.Info("http get FetchToken error==%v", err) 38 | return nil 39 | } 40 | defer request.Body.Close() 41 | body, err := ioutil.ReadAll(request.Body) 42 | if err != nil { 43 | logs.Info("ioutil.ReadAll(request.Body) FetchToken error==%v", err) 44 | return nil 45 | } 46 | token := string(body) 47 | return &token 48 | } 49 | -------------------------------------------------------------------------------- /services/admin.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "kefu_go_robot/grpcc" 6 | "kefu_server/grpcs" 7 | "kefu_server/models" 8 | "kefu_server/utils" 9 | 10 | "github.com/astaxie/beego/logs" 11 | ) 12 | 13 | // AdminRepository struct 14 | type AdminRepository struct{} 15 | 16 | // GetAdminRepositoryInstance get instance 17 | func GetAdminRepositoryInstance() *AdminRepository { 18 | instance := new(AdminRepository) 19 | return instance 20 | } 21 | 22 | // GetOnlineAdmins get online all admin 23 | func (r *AdminRepository) GetOnlineAdmins() []models.Admin { 24 | grpcClient := grpcc.GrpcClient() 25 | res, err := grpcClient.GetOnlineAdmins(context.Background(), &grpcs.Request{Data: ""}) 26 | if err != nil { 27 | logs.Info("SearchKnowledgeTitles get titles res==%v", err) 28 | return nil 29 | } 30 | var admins []models.Admin 31 | utils.StringToInterface(res.Data, &admins) 32 | return admins 33 | } 34 | -------------------------------------------------------------------------------- /services/contact.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "kefu_go_robot/grpcc" 6 | "kefu_server/grpcs" 7 | 8 | "github.com/astaxie/beego/logs" 9 | ) 10 | 11 | // ContactRepository struct 12 | type ContactRepository struct{} 13 | 14 | // GetContactRepositoryInstance get instance 15 | func GetContactRepositoryInstance() *ContactRepository { 16 | instance := new(ContactRepository) 17 | return instance 18 | } 19 | 20 | // PushNewContacts Contact 21 | func (r *ContactRepository) PushNewContacts(uid string) { 22 | grpcClient := grpcc.GrpcClient() 23 | _, err := grpcClient.PushNewContacts(context.Background(), &grpcs.Request{Data: uid}) 24 | if err != nil { 25 | logs.Info("PushNewContacts Contact err==%v", err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /services/knowledge_base.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "kefu_go_robot/grpcc" 7 | "kefu_server/grpcs" 8 | "kefu_server/models" 9 | "kefu_server/utils" 10 | "strconv" 11 | 12 | "github.com/astaxie/beego/logs" 13 | ) 14 | 15 | // KnowledgeBaseRepository struct 16 | type KnowledgeBaseRepository struct{} 17 | 18 | // GetKnowledgeBaseRepositoryInstance get instance 19 | func GetKnowledgeBaseRepositoryInstance() *KnowledgeBaseRepository { 20 | instance := new(KnowledgeBaseRepository) 21 | return instance 22 | } 23 | 24 | // GetKnowledgeBaseWithTitleAndPlatform get with title 25 | func (r *KnowledgeBaseRepository) GetKnowledgeBaseWithTitleAndPlatform(title string, platform int64) *models.KnowledgeBase { 26 | request := make(map[string]string) 27 | request["title"] = title 28 | request["platform"] = strconv.FormatInt(platform, 10) 29 | byteData, _ := json.Marshal(request) 30 | grpcClient := grpcc.GrpcClient() 31 | res, err := grpcClient.GetKnowledgeBaseWithTitleAndPlatform(context.Background(), &grpcs.Request{Data: string(byteData)}) 32 | if err != nil { 33 | logs.Info("SearchKnowledgeTitles get titles res==%v", err) 34 | } 35 | var knowledgeBase *models.KnowledgeBase 36 | utils.StringToInterface(res.Data, &knowledgeBase) 37 | return knowledgeBase 38 | } 39 | 40 | // SearchKnowledgeTitles get titles 41 | func (r *KnowledgeBaseRepository) SearchKnowledgeTitles(request models.KnowledgeBaseTitleRequestDto) []models.KnowledgeBaseTitleDto { 42 | grpcClient := grpcc.GrpcClient() 43 | res, err := grpcClient.SearchKnowledgeTitles(context.Background(), &grpcs.Request{Data: utils.InterfaceToString(request)}) 44 | if err != nil { 45 | logs.Info("SearchKnowledgeTitles get titles res==%v", err) 46 | return nil 47 | } 48 | var KnowledgeBaseTitles []models.KnowledgeBaseTitleDto 49 | utils.StringToInterface(res.Data, &KnowledgeBaseTitles) 50 | if KnowledgeBaseTitles == nil { 51 | return []models.KnowledgeBaseTitleDto{} 52 | } 53 | return KnowledgeBaseTitles 54 | } 55 | -------------------------------------------------------------------------------- /services/message.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "kefu_go_robot/grpcc" 6 | "kefu_server/grpcs" 7 | "kefu_server/models" 8 | "kefu_server/utils" 9 | 10 | "github.com/astaxie/beego/logs" 11 | ) 12 | 13 | // MessageRepository struct 14 | type MessageRepository struct{} 15 | 16 | // GetMessageRepositoryInstance get instance 17 | func GetMessageRepositoryInstance() *MessageRepository { 18 | instance := new(MessageRepository) 19 | return instance 20 | } 21 | 22 | // InsertMessage Push Message 23 | func (r *MessageRepository) InsertMessage(payload string) { 24 | grpcClient := grpcc.GrpcClient() 25 | _, err := grpcClient.InsertMessage(context.Background(), &grpcs.Request{Data: payload}) 26 | if err != nil { 27 | logs.Info("InsertMessage Push Message err==%v", err) 28 | } 29 | } 30 | 31 | // CancelMessage Cancel Message 32 | func (r *MessageRepository) CancelMessage(request models.RemoveMessageRequestDto) { 33 | grpcClient := grpcc.GrpcClient() 34 | _, err := grpcClient.CancelMessage(context.Background(), &grpcs.Request{Data: utils.InterfaceToString(request)}) 35 | if err != nil { 36 | logs.Info("CancelMessage Cancel Message err==%v", err) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /services/robot.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "kefu_go_robot/grpcc" 6 | "kefu_server/grpcs" 7 | "kefu_server/models" 8 | "kefu_server/utils" 9 | 10 | "github.com/astaxie/beego/logs" 11 | ) 12 | 13 | // RobotRepository struct 14 | type RobotRepository struct{} 15 | 16 | // GetRobotRepositoryInstance get instance 17 | func GetRobotRepositoryInstance() *RobotRepository { 18 | instance := new(RobotRepository) 19 | return instance 20 | } 21 | 22 | // GetOnlineAllRobots auth 23 | func (r *RobotRepository) GetOnlineAllRobots() []*models.Robot { 24 | grpcClient := grpcc.GrpcClient() 25 | res, err := grpcClient.GetOnlineAllRobots(context.Background(), &grpcs.Request{Data: ""}) 26 | if err != nil { 27 | logs.Info("GetOnlineAllRobots auth use grpcClient res==%v", err) 28 | return nil 29 | } 30 | var robots []*models.Robot 31 | utils.StringToInterface(res.Data, &robots) 32 | return robots 33 | } 34 | -------------------------------------------------------------------------------- /services/statistical.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "kefu_go_robot/grpcc" 6 | "kefu_server/grpcs" 7 | "kefu_server/models" 8 | "kefu_server/utils" 9 | 10 | "github.com/astaxie/beego/logs" 11 | ) 12 | 13 | // StatisticalRepository struct 14 | type StatisticalRepository struct{} 15 | 16 | // GetStatisticalRepositoryInstance get instance 17 | func GetStatisticalRepositoryInstance() *StatisticalRepository { 18 | instance := new(StatisticalRepository) 19 | return instance 20 | } 21 | 22 | // Add Statistical 23 | func (r *StatisticalRepository) Add(request models.ServicesStatistical) { 24 | grpcClient := grpcc.GrpcClient() 25 | _, err := grpcClient.InsertStatistical(context.Background(), &grpcs.Request{Data: utils.InterfaceToString(request)}) 26 | if err != nil { 27 | logs.Info("Add Statistical err==%v", err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /services/user.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "kefu_go_robot/grpcc" 6 | "kefu_server/grpcs" 7 | "kefu_server/models" 8 | "kefu_server/utils" 9 | 10 | "github.com/astaxie/beego/logs" 11 | ) 12 | 13 | // UserRepository struct 14 | type UserRepository struct{} 15 | 16 | // GetUserRepositoryInstance get instance 17 | func GetUserRepositoryInstance() *UserRepository { 18 | instance := new(UserRepository) 19 | return instance 20 | } 21 | 22 | // Update update user 23 | func (r *UserRepository) Update(user models.User) { 24 | grpcClient := grpcc.GrpcClient() 25 | _, err := grpcClient.UpdateUser(context.Background(), &grpcs.Request{Data: utils.InterfaceToString(user)}) 26 | if err != nil { 27 | logs.Info("Update update user==%v", err) 28 | } 29 | } 30 | --------------------------------------------------------------------------------