500 lines
14 KiB
Go
500 lines
14 KiB
Go
package controller
|
||
|
||
import (
|
||
"ai-css/common"
|
||
"ai-css/library/logger"
|
||
"ai-css/library/modelprovider"
|
||
"ai-css/library/modelprovider/bootstrap"
|
||
"ai-css/library/modelprovider/consts"
|
||
"ai-css/models"
|
||
"ai-css/tools"
|
||
"ai-css/ws"
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"path"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/gorilla/websocket"
|
||
"github.com/openai/openai-go/v3/responses"
|
||
)
|
||
|
||
var prompt = `You are an AI customer support assistant.
|
||
|
||
Your primary goal is to help users resolve their issues accurately, politely, and efficiently.
|
||
You represent the official customer service of the product or platform.
|
||
|
||
General rules:
|
||
- Always be polite, calm, and professional.
|
||
- Use clear, concise, and user-friendly language.
|
||
- Focus on solving the user’s problem step by step.
|
||
- Do NOT fabricate information. If you are unsure or lack relevant knowledge, say so clearly.
|
||
- Do NOT guess product policies, prices, or technical behaviors.
|
||
- If a question cannot be resolved based on available information, guide the user to human support.
|
||
|
||
Knowledge usage:
|
||
- Only answer questions based on the provided knowledge, FAQs, or conversation context.
|
||
- If the user’s question is outside the supported scope, respond with a brief explanation and suggest contacting a human agent.
|
||
|
||
Escalation rules:
|
||
- If the user explicitly requests a human agent, immediately stop responding and indicate the transfer.
|
||
- If the user expresses frustration, repeated confusion, or dissatisfaction, suggest escalating to a human agent.
|
||
|
||
Safety and compliance:
|
||
- Do not provide sensitive, confidential, or internal information.
|
||
- Do not provide legal, medical, or financial advice.
|
||
- Avoid any harmful, abusive, or inappropriate content.
|
||
|
||
Response style:
|
||
- Keep answers concise but helpful.
|
||
- Prefer bullet points or numbered steps when explaining procedures.
|
||
- Ask clarifying questions only when necessary to move forward.
|
||
`
|
||
|
||
func SendMessageV2(c *gin.Context) {
|
||
fromId := c.PostForm("from_id")
|
||
toId := c.PostForm("to_id")
|
||
content := c.PostForm("content")
|
||
cType := c.PostForm("type")
|
||
if content == "" {
|
||
c.JSON(200, gin.H{
|
||
"code": 400,
|
||
"msg": "内容不能为空",
|
||
})
|
||
return
|
||
}
|
||
//限流
|
||
if !tools.LimitFreqSingle("sendmessage:"+c.ClientIP(), 1, 2) {
|
||
c.JSON(200, gin.H{
|
||
"code": 400,
|
||
"msg": c.ClientIP() + "发送频率过快",
|
||
})
|
||
return
|
||
}
|
||
var kefuInfo models.User
|
||
var vistorInfo models.Visitor
|
||
if cType == "kefu" {
|
||
kefuInfo = models.FindUser(fromId)
|
||
vistorInfo = models.FindVisitorByVistorId(toId)
|
||
} else if cType == "visitor" {
|
||
vistorInfo = models.FindVisitorByVistorId(fromId)
|
||
kefuInfo = models.FindUser(toId)
|
||
}
|
||
|
||
if kefuInfo.ID == 0 || vistorInfo.ID == 0 {
|
||
c.JSON(200, gin.H{
|
||
"code": 400,
|
||
"msg": "用户不存在",
|
||
})
|
||
return
|
||
}
|
||
|
||
models.CreateMessage(kefuInfo.Name, vistorInfo.VisitorId, content, cType)
|
||
//var msg TypeMessage
|
||
if cType == "kefu" {
|
||
guest, ok := ws.ClientList[vistorInfo.VisitorId]
|
||
|
||
if guest != nil && ok {
|
||
ws.VisitorMessage(vistorInfo.VisitorId, content, kefuInfo)
|
||
}
|
||
ws.KefuMessage(vistorInfo.VisitorId, content, kefuInfo)
|
||
//msg = TypeMessage{
|
||
// Type: "message",
|
||
// Data: ws.ClientMessage{
|
||
// Name: kefuInfo.Nickname,
|
||
// Avator: kefuInfo.Avator,
|
||
// Id: vistorInfo.VisitorId,
|
||
// Time: time.Now().Format("2006-01-02 15:04:05"),
|
||
// ToId: vistorInfo.VisitorId,
|
||
// Content: content,
|
||
// IsKefu: "yes",
|
||
// },
|
||
//}
|
||
//str2, _ := json.Marshal(msg)
|
||
//ws.OneKefuMessage(kefuInfo.Name, str2)
|
||
c.JSON(200, gin.H{
|
||
"code": 200,
|
||
"msg": "ok",
|
||
})
|
||
}
|
||
if cType == "visitor" {
|
||
guest, ok := ws.ClientList[vistorInfo.VisitorId]
|
||
if ok && guest != nil {
|
||
guest.UpdateTime = time.Now()
|
||
}
|
||
if guest == nil {
|
||
c.JSON(400, gin.H{
|
||
"code": 400,
|
||
"msg": "guest not found",
|
||
})
|
||
return
|
||
}
|
||
if ws.AIAnswerAvailable(guest) {
|
||
// AI回答
|
||
ret, err := AIChat(kefuInfo.Name, vistorInfo.VisitorId, content, guest.Conn)
|
||
if err == nil {
|
||
guest.AIAnswerCycle++
|
||
models.CreateMessage(kefuInfo.Name, vistorInfo.VisitorId, ret, "kefu")
|
||
ws.VisitorMessage(vistorInfo.VisitorId, ret, kefuInfo)
|
||
c.JSON(200, gin.H{
|
||
"code": 200,
|
||
"msg": "ok",
|
||
})
|
||
return
|
||
}
|
||
|
||
logger.Errorf("ai chat failed err:%v,visitorID:%s,content:%s", err, vistorInfo.VisitorId, content)
|
||
} else if guest.AIAnswerCycle == ws.MaxAIAnswerCycleTimes {
|
||
guest.AIAnswerCycle++
|
||
cot := "ai次数用完将进入人工坐席。。。"
|
||
models.CreateMessage(kefuInfo.Name, vistorInfo.VisitorId, cot, "kefu")
|
||
ws.VisitorMessage(vistorInfo.VisitorId, cot, kefuInfo)
|
||
}
|
||
|
||
msg := ws.TypeMessage{
|
||
Type: "message",
|
||
Data: ws.ClientMessage{
|
||
Avator: vistorInfo.Avator,
|
||
Id: vistorInfo.VisitorId,
|
||
Name: vistorInfo.Name,
|
||
ToId: kefuInfo.Name,
|
||
Content: content,
|
||
Time: time.Now().Format("2006-01-02 15:04:05"),
|
||
IsKefu: "no",
|
||
},
|
||
}
|
||
str, _ := json.Marshal(msg)
|
||
ws.OneKefuMessage(kefuInfo.Name, str)
|
||
//ws.KefuMessage(vistorInfo.VisitorId, content, kefuInfo)
|
||
kefu, ok := ws.KefuList[kefuInfo.Name]
|
||
if !ok || kefu == nil {
|
||
go SendNoticeEmail(content+"|"+vistorInfo.Name, content)
|
||
}
|
||
go ws.VisitorAutoReply(vistorInfo, kefuInfo, content)
|
||
c.JSON(200, gin.H{
|
||
"code": 200,
|
||
"msg": "ok",
|
||
})
|
||
}
|
||
|
||
}
|
||
|
||
func SendKefuMessage(c *gin.Context) {
|
||
fromId, _ := c.Get("kefu_name")
|
||
toId := c.PostForm("to_id")
|
||
content := c.PostForm("content")
|
||
cType := c.PostForm("type")
|
||
if content == "" {
|
||
c.JSON(200, gin.H{
|
||
"code": 400,
|
||
"msg": "内容不能为空",
|
||
})
|
||
return
|
||
}
|
||
//限流
|
||
if !tools.LimitFreqSingle("sendmessage:"+c.ClientIP(), 1, 2) {
|
||
c.JSON(200, gin.H{
|
||
"code": 400,
|
||
"msg": c.ClientIP() + "发送频率过快",
|
||
})
|
||
return
|
||
}
|
||
var kefuInfo models.User
|
||
var vistorInfo models.Visitor
|
||
kefuInfo = models.FindUser(fromId.(string))
|
||
vistorInfo = models.FindVisitorByVistorId(toId)
|
||
|
||
if kefuInfo.ID == 0 || vistorInfo.ID == 0 {
|
||
c.JSON(200, gin.H{
|
||
"code": 400,
|
||
"msg": "用户不存在",
|
||
})
|
||
return
|
||
}
|
||
|
||
models.CreateMessage(kefuInfo.Name, vistorInfo.VisitorId, content, cType)
|
||
//var msg TypeMessage
|
||
|
||
guest, ok := ws.ClientList[vistorInfo.VisitorId]
|
||
|
||
if guest != nil && ok {
|
||
ws.VisitorMessage(vistorInfo.VisitorId, content, kefuInfo)
|
||
}
|
||
ws.KefuMessage(vistorInfo.VisitorId, content, kefuInfo)
|
||
c.JSON(200, gin.H{
|
||
"code": 200,
|
||
"msg": "ok",
|
||
})
|
||
|
||
}
|
||
func SendVisitorNotice(c *gin.Context) {
|
||
notice := c.Query("msg")
|
||
if notice == "" {
|
||
c.JSON(200, gin.H{
|
||
"code": 400,
|
||
"msg": "msg不能为空",
|
||
})
|
||
return
|
||
}
|
||
msg := ws.TypeMessage{
|
||
Type: "notice",
|
||
Data: notice,
|
||
}
|
||
str, _ := json.Marshal(msg)
|
||
for _, visitor := range ws.ClientList {
|
||
visitor.Conn.WriteMessage(websocket.TextMessage, str)
|
||
}
|
||
c.JSON(200, gin.H{
|
||
"code": 200,
|
||
"msg": "ok",
|
||
})
|
||
}
|
||
func SendCloseMessageV2(c *gin.Context) {
|
||
visitorId := c.Query("visitor_id")
|
||
if visitorId == "" {
|
||
c.JSON(200, gin.H{
|
||
"code": 400,
|
||
"msg": "visitor_id不能为空",
|
||
})
|
||
return
|
||
}
|
||
|
||
oldUser, ok := ws.ClientList[visitorId]
|
||
if oldUser != nil || ok {
|
||
msg := ws.TypeMessage{
|
||
Type: "force_close",
|
||
Data: visitorId,
|
||
}
|
||
str, _ := json.Marshal(msg)
|
||
err := oldUser.Conn.WriteMessage(websocket.TextMessage, str)
|
||
oldUser.Conn.Close()
|
||
delete(ws.ClientList, visitorId)
|
||
tools.Logger().Println("close_message", oldUser, err)
|
||
}
|
||
c.JSON(200, gin.H{
|
||
"code": 200,
|
||
"msg": "ok",
|
||
})
|
||
}
|
||
func UploadImg(c *gin.Context) {
|
||
f, err := c.FormFile("imgfile")
|
||
if err != nil {
|
||
c.JSON(200, gin.H{
|
||
"code": 400,
|
||
"msg": "上传失败!",
|
||
})
|
||
return
|
||
} else {
|
||
|
||
fileExt := strings.ToLower(path.Ext(f.Filename))
|
||
if fileExt != ".png" && fileExt != ".jpg" && fileExt != ".gif" && fileExt != ".jpeg" {
|
||
c.JSON(200, gin.H{
|
||
"code": 400,
|
||
"msg": "上传失败!只允许png,jpg,gif,jpeg文件",
|
||
})
|
||
return
|
||
}
|
||
isMainUploadExist, _ := tools.IsFileExist(common.Upload)
|
||
if !isMainUploadExist {
|
||
os.Mkdir(common.Upload, os.ModePerm)
|
||
}
|
||
fileName := tools.Md5(fmt.Sprintf("%s%s", f.Filename, time.Now().String()))
|
||
fildDir := fmt.Sprintf("%s%d%s/", common.Upload, time.Now().Year(), time.Now().Month().String())
|
||
isExist, _ := tools.IsFileExist(fildDir)
|
||
if !isExist {
|
||
os.Mkdir(fildDir, os.ModePerm)
|
||
}
|
||
filepath := fmt.Sprintf("%s%s%s", fildDir, fileName, fileExt)
|
||
c.SaveUploadedFile(f, filepath)
|
||
c.JSON(200, gin.H{
|
||
"code": 200,
|
||
"msg": "上传成功!",
|
||
"result": gin.H{
|
||
"path": filepath,
|
||
},
|
||
})
|
||
}
|
||
}
|
||
func UploadFile(c *gin.Context) {
|
||
f, err := c.FormFile("realfile")
|
||
if err != nil {
|
||
c.JSON(200, gin.H{
|
||
"code": 400,
|
||
"msg": "上传失败!",
|
||
})
|
||
return
|
||
} else {
|
||
|
||
fileExt := strings.ToLower(path.Ext(f.Filename))
|
||
if f.Size >= 90*1024*1024 {
|
||
c.JSON(200, gin.H{
|
||
"code": 400,
|
||
"msg": "上传失败!不允许超过90M",
|
||
})
|
||
return
|
||
}
|
||
|
||
fileName := tools.Md5(fmt.Sprintf("%s%s", f.Filename, time.Now().String()))
|
||
fildDir := fmt.Sprintf("%s%d%s/", common.Upload, time.Now().Year(), time.Now().Month().String())
|
||
isExist, _ := tools.IsFileExist(fildDir)
|
||
if !isExist {
|
||
os.Mkdir(fildDir, os.ModePerm)
|
||
}
|
||
filepath := fmt.Sprintf("%s%s%s", fildDir, fileName, fileExt)
|
||
c.SaveUploadedFile(f, filepath)
|
||
c.JSON(200, gin.H{
|
||
"code": 200,
|
||
"msg": "上传成功!",
|
||
"result": gin.H{
|
||
"path": filepath,
|
||
"ext": fileExt,
|
||
"size": f.Size,
|
||
"name": f.Filename,
|
||
},
|
||
})
|
||
}
|
||
}
|
||
func GetMessagesV2(c *gin.Context) {
|
||
visitorId := c.Query("visitor_id")
|
||
messages := models.FindMessageByVisitorId(visitorId)
|
||
//result := make([]map[string]interface{}, 0)
|
||
chatMessages := make([]ChatMessage, 0)
|
||
var visitor models.Visitor
|
||
var kefu models.User
|
||
for _, message := range messages {
|
||
//item := make(map[string]interface{})
|
||
if visitor.Name == "" || kefu.Name == "" {
|
||
kefu = models.FindUser(message.KefuId)
|
||
visitor = models.FindVisitorByVistorId(message.VisitorId)
|
||
}
|
||
var chatMessage ChatMessage
|
||
chatMessage.Time = message.GetCreateTime()
|
||
chatMessage.Content = message.Content
|
||
chatMessage.MesType = message.MesType
|
||
if message.MesType == "kefu" {
|
||
chatMessage.Name = kefu.Nickname
|
||
chatMessage.Avator = kefu.Avator
|
||
} else {
|
||
chatMessage.Name = visitor.Name
|
||
chatMessage.Avator = visitor.Avator
|
||
}
|
||
chatMessages = append(chatMessages, chatMessage)
|
||
}
|
||
models.ReadMessageByVisitorId(visitorId)
|
||
c.JSON(200, gin.H{
|
||
"code": 200,
|
||
"msg": "ok",
|
||
"result": chatMessages,
|
||
})
|
||
}
|
||
func GetMessagespages(c *gin.Context) {
|
||
visitorId := c.Query("visitor_id")
|
||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pagesize", "10"))
|
||
if pageSize > 20 {
|
||
pageSize = 20
|
||
}
|
||
count := models.CountMessage("visitor_id = ?", visitorId)
|
||
list := models.FindMessageByPage(uint(page), uint(pageSize), "message.visitor_id = ?", visitorId)
|
||
c.JSON(200, gin.H{
|
||
"code": 200,
|
||
"msg": "ok",
|
||
"result": gin.H{
|
||
"count": count,
|
||
"page": page,
|
||
"list": list,
|
||
"pagesize": pageSize,
|
||
},
|
||
})
|
||
}
|
||
|
||
func AIChat(supportID, visitorID string, question string, ws *websocket.Conn) (answer string, err error) {
|
||
var ctx = context.TODO()
|
||
mgs, err := models.FindLatestMessageByVisitorId(visitorID, 6)
|
||
if err != nil {
|
||
logger.Errorf("find latest message err: %v", err)
|
||
return
|
||
}
|
||
|
||
cli, err := bootstrap.DefaultAIManager.NewClient(consts.ProviderOpenAI, bootstrap.WithDefaultModel(responses.ChatModelGPT5Mini))
|
||
if err != nil {
|
||
logger.Errorf("init gpt cli fail err:%v", err)
|
||
return
|
||
}
|
||
resp, err := cli.Chat(ctx, modelprovider.ChatRequest{Messages: MakeAIMsg(supportID, mgs, question)})
|
||
if err != nil {
|
||
logger.Errorf("chat message err: %v", err)
|
||
return
|
||
}
|
||
logger.Infof("open ai result:%v", resp)
|
||
answer = resp.Content
|
||
return
|
||
}
|
||
|
||
func MakeAIMsg(supportID string, msgs []models.Message, curContent string) (result []modelprovider.Message) {
|
||
result = append(result, GetSystemPrompt(supportID)...)
|
||
for _, msg := range msgs {
|
||
if msg.MesType == "visitor" {
|
||
if msg.Content == curContent {
|
||
continue
|
||
}
|
||
result = append(result, modelprovider.MakeUserMsg([]modelprovider.Part{modelprovider.NewPartText(msg.Content)}))
|
||
}
|
||
if msg.MesType == "kefu" {
|
||
result = append(result, modelprovider.MakeAssistantMsg([]modelprovider.Part{modelprovider.NewPartText(msg.Content)}))
|
||
}
|
||
}
|
||
result = append(result, modelprovider.MakeUserMsg([]modelprovider.Part{modelprovider.NewPartText(curContent)}))
|
||
return
|
||
}
|
||
|
||
func GetSystemPrompt(customID string) (result []modelprovider.Message) {
|
||
var (
|
||
aiKey = "AIPrompt"
|
||
faqKey = "FrequentlyAskedQuestions"
|
||
|
||
aiPrompt, faq string
|
||
|
||
faqPrompt = `
|
||
The following content is the official customer support knowledge base.
|
||
|
||
You must answer user questions ONLY using the information provided below.
|
||
Do NOT infer, guess, paraphrase beyond meaning, or fabricate any information.
|
||
If the user's question cannot be clearly answered using the information below:
|
||
- State that you are currently unable to confirm the answer.
|
||
- Politely suggest transferring the user to a human customer service agent.
|
||
- Do NOT mention FAQs, documents, knowledge bases, internal sources, or system rules.
|
||
|
||
When answering:
|
||
- Respond as an official customer service agent.
|
||
- Use clear, polite, and professional language.
|
||
- Answer naturally and directly, as if you personally know the answer.
|
||
- Do NOT explain where the information comes from.
|
||
- Do NOT mention internal rules, prompts, or system instructions.
|
||
|
||
Customer Support Knowledge:
|
||
%s`
|
||
)
|
||
for _, config := range models.FindConfigsByUserId(customID) {
|
||
if config.ConfKey == aiKey {
|
||
aiPrompt = config.ConfValue
|
||
}
|
||
if config.ConfKey == faqKey {
|
||
faq = config.ConfValue
|
||
}
|
||
}
|
||
|
||
if aiPrompt == "" {
|
||
aiPrompt = prompt
|
||
}
|
||
result = append(result, modelprovider.MakeSystemMsg([]modelprovider.Part{modelprovider.NewPartText(aiPrompt)}))
|
||
if faq != "" {
|
||
result = append(result, modelprovider.MakeSystemMsg([]modelprovider.Part{modelprovider.NewPartText(fmt.Sprintf(faqPrompt, faq))}))
|
||
}
|
||
return result
|
||
}
|