From 9f54f95264aef3f7d613d00c4789bb30d830134a Mon Sep 17 00:00:00 2001 From: qhd <1766646056@qq.com> Date: Thu, 16 Apr 2026 15:48:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=AE=A2=E6=9C=8D?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E5=8F=8A=E5=BB=B6=E8=BF=9F=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- consts/account/platform.go | 14 +-- consts/account/vector_status.go | 28 ------ consts/public/msg_key.go | 6 +- consts/public/redis_key.go | 15 --- consts/public/session_tool.go | 18 ++++ consts/scriptedSpeech/scene_type.go | 15 ++- go.mod | 2 +- go.sum | 1 + service/account_http_service.go | 12 --- service/session_tool_service.go | 137 +++++++++++++++++++--------- 10 files changed, 132 insertions(+), 116 deletions(-) delete mode 100644 consts/account/vector_status.go delete mode 100644 consts/public/redis_key.go create mode 100644 consts/public/session_tool.go diff --git a/consts/account/platform.go b/consts/account/platform.go index da374f0..95965aa 100644 --- a/consts/account/platform.go +++ b/consts/account/platform.go @@ -6,11 +6,6 @@ var ( PlatformXHS = newPlatform(gconv.PtrString("xiaohongshu"), "小红书") PlatformDY = newPlatform(gconv.PtrString("douyin"), "抖音") PlatformKS = newPlatform(gconv.PtrString("kuaishou"), "快手") - platformMap = map[Platform]platform{ - gconv.PtrString("xiaohongshu"): PlatformXHS, - gconv.PtrString("douyin"): PlatformDY, - gconv.PtrString("kuaishou"): PlatformKS, - } ) type Platform *string @@ -32,8 +27,13 @@ func newPlatform(code Platform, desc string) platform { } func GetDescByCode(code Platform) string { - if p, ok := platformMap[code]; ok { - return p.Desc() + switch *code { + case *PlatformXHS.Code(): + return PlatformXHS.Desc() + case *PlatformDY.Code(): + return PlatformDY.Desc() + case *PlatformKS.Code(): + return PlatformKS.Desc() } return "未知平台" } diff --git a/consts/account/vector_status.go b/consts/account/vector_status.go deleted file mode 100644 index f0028e4..0000000 --- a/consts/account/vector_status.go +++ /dev/null @@ -1,28 +0,0 @@ -package account - -import "github.com/gogf/gf/v2/util/gconv" - -var ( - VectorStatusPending = newVectorStatus(gconv.PtrInt8(1), "pending") - VectorStatusProcessing = newVectorStatus(gconv.PtrInt8(2), "processing") - VectorStatusCompleted = newVectorStatus(gconv.PtrInt8(3), "completed") - VectorStatusFailed = newVectorStatus(gconv.PtrInt8(4), "failed") -) - -type VectorStatus *int8 - -type vectorStatus struct { - code VectorStatus - desc string -} - -func (s vectorStatus) Code() VectorStatus { - return s.code -} -func (s vectorStatus) Desc() string { - return s.desc -} - -func newVectorStatus(code VectorStatus, desc string) vectorStatus { - return vectorStatus{code: code, desc: desc} -} diff --git a/consts/public/msg_key.go b/consts/public/msg_key.go index 98af16e..2be79de 100644 --- a/consts/public/msg_key.go +++ b/consts/public/msg_key.go @@ -2,7 +2,11 @@ package public const GmqMsgPluginsName = "gmq_msg" -const AccountDialogKeyUserId = "account:dialog:%s" +const ( + AccountMsgKey = "account:%s:%s:%s" + AccountDialogHistoryKey = "account:dialog:history:%s" + AccountGreetingOptionsKey = "account:greeting:options:%s" +) const ( AccountFollowupTopic = "account:followup:stream" // 请求 Stream 键名(与发消息的key一致) diff --git a/consts/public/redis_key.go b/consts/public/redis_key.go deleted file mode 100644 index 695f37b..0000000 --- a/consts/public/redis_key.go +++ /dev/null @@ -1,15 +0,0 @@ -package public - -const KnowledgeLockEsKey = "rag:knowledge:lock:knowledgeIdEs-%v" -const KnowledgeLockSqlKey = "rag:knowledge:lock:knowledgeIdSql-%v" -const KnowledgeContentHashEsKey = "rag:knowledge:knowledgeId:contentHashEs-%v" -const KnowledgeContentHashSqlKey = "rag:knowledge:knowledgeId:contentHashSql-%v" - -const KnowledgeDocumentChunkTopic = "knowledge:document:chunk:stream" // 请求 Stream 键名(与发消息的key一致) - -const ( - KnowledgeDocumentVectorStatusTopic = "knowledge:document:vector:status:stream" - KnowledgeDocumentVectorStatusConsumer = "knowledge-document-vector-status-consumer" - KnowledgeDocumentVectorStatusBatchSize = 1 - KnowledgeDocumentVectorStatusAutoAck = false -) diff --git a/consts/public/session_tool.go b/consts/public/session_tool.go new file mode 100644 index 0000000..8f8a002 --- /dev/null +++ b/consts/public/session_tool.go @@ -0,0 +1,18 @@ +package public + +// 欢迎语 +const ( + GreetingBegin = "您好,很高兴为您服务!请问有什么可以帮您?" + GreetingBetween = "💗回复数字就好~" + GreetingEnd = "🌟也可直接点击下方咨询专业老师~" +) + +// 追问 +const ( + SceneOpeningRemark = "宝子,刚才给您发的信息您有看到吗?有任何问题都能直接问我,加微信也能更方便沟通~" + SceneDialog = "看您暂时没回复,是不是还有什么疑问?加微信我详细给您说明~" + SceneCardSend = "宝子,加上没~要及时加哦,不然卡片容易失效哒✨" +) + +// 对话超时时间 +const DialogTimeout = 10 diff --git a/consts/scriptedSpeech/scene_type.go b/consts/scriptedSpeech/scene_type.go index c0318b3..cb5723a 100644 --- a/consts/scriptedSpeech/scene_type.go +++ b/consts/scriptedSpeech/scene_type.go @@ -6,12 +6,6 @@ var ( SceneTypeOpeningRemark = newSceneType(gconv.PtrInt8(1), "开场白无回应") SceneTypeDialog = newSceneType(gconv.PtrInt8(2), "对话中途无回应") SceneTypeCardSend = newSceneType(gconv.PtrInt8(3), "卡片发送后无回应") - - sceneTypeMap = map[SceneType]sceneType{ - gconv.PtrInt8(1): SceneTypeOpeningRemark, - gconv.PtrInt8(2): SceneTypeDialog, - gconv.PtrInt8(3): SceneTypeCardSend, - } ) type SceneType *int8 @@ -33,8 +27,13 @@ func newSceneType(code SceneType, desc string) sceneType { } func GetDescByCode(code SceneType) string { - if p, ok := sceneTypeMap[code]; ok { - return p.Desc() + switch *code { + case *SceneTypeOpeningRemark.Code(): + return SceneTypeOpeningRemark.Desc() + case *SceneTypeDialog.Code(): + return SceneTypeDialog.Desc() + case *SceneTypeCardSend.Code(): + return SceneTypeCardSend.Desc() } return "未知场景类型" } diff --git a/go.mod b/go.mod index 16f1722..5ed4322 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0 github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0 github.com/gogf/gf/v2 v2.10.0 - github.com/gorilla/websocket v1.5.3 + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 ) require ( diff --git a/go.sum b/go.sum index d7d2211..1a40c79 100644 --- a/go.sum +++ b/go.sum @@ -146,6 +146,7 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= diff --git a/service/account_http_service.go b/service/account_http_service.go index c57cc0c..56b1618 100644 --- a/service/account_http_service.go +++ b/service/account_http_service.go @@ -2,13 +2,9 @@ package service import ( "context" - "customer-server/consts/public" "customer-server/model/dto" "fmt" - gmq "github.com/bjang03/gmq/core/gmq" - "github.com/bjang03/gmq/mq" - "github.com/bjang03/gmq/types" "github.com/gogf/gf/v2/frame/g" ) @@ -18,14 +14,6 @@ var ( type accountHttpService struct{} -func (s *accountHttpService) DeleteDelayMsg(ctx context.Context) (err error) { - return gmq.GetGmq(public.GmqMsgPluginsName).GmqDeleteDelay(ctx, &mq.NatsDelMessage{ - DelMessage: types.DelMessage{ - Topic: public.AccountFollowupTopic, - }, - }) -} - func (s *accountHttpService) Connect(ctx context.Context, req *dto.AccountHttpConnectReq) (res *dto.AccountHttpConnectRes, err error) { // 获取客服账号信息 accountInfo, err := SessionToolService.GetAccountInfo(ctx, req.AccountCode) diff --git a/service/session_tool_service.go b/service/session_tool_service.go index 5a25774..8aac4e7 100644 --- a/service/session_tool_service.go +++ b/service/session_tool_service.go @@ -21,6 +21,7 @@ import ( gmq "github.com/bjang03/gmq/core/gmq" "github.com/bjang03/gmq/mq" "github.com/bjang03/gmq/types" + "github.com/gogf/gf/v2/container/gvar" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" @@ -33,7 +34,7 @@ type sessionToolService struct{} func (s *sessionToolService) PushOpeningRemark(ctx context.Context, userId string, accountInfo *dto.AccountVO, headers map[string]string) (content string, err error) { content = "" var sceneType = scriptedSpeech.SceneTypeOpeningRemark - var key = fmt.Sprintf("account:%s:%s:%s", accountInfo.AccountCode, account.GetDescByCode(accountInfo.Platform), userId) + var key = fmt.Sprintf(public.AccountMsgKey, accountInfo.AccountCode, account.GetDescByCode(accountInfo.Platform), userId) get, err := g.Redis().Get(ctx, key) if err != nil { return @@ -50,13 +51,17 @@ func (s *sessionToolService) PushOpeningRemark(ctx context.Context, userId strin err = fmt.Errorf("数据集不存在") return } - var datasetDescriptions []string + var datasetDescriptions [][]string for _, dataset := range datasetInfo.List { - datasetDescriptions = append(datasetDescriptions, dataset.Name) + datasetDescriptions = append(datasetDescriptions, []string{dataset.Name, gconv.String(dataset.Id)}) } - content = SessionToolService.BuildMenuContent(accountInfo.Greeting, datasetDescriptions, len(accountInfo.DatasetIds)) + content, err = SessionToolService.BuildGreeting(ctx, userId, accountInfo.Greeting, datasetDescriptions, len(accountInfo.DatasetIds)) } else { - content = SessionToolService.BuildMenuContent(accountInfo.Greeting, accountInfo.KeywordOption, len(accountInfo.DatasetIds)) + var datasetDescriptions [][]string + for _, keyword := range accountInfo.KeywordOption { + datasetDescriptions = append(datasetDescriptions, []string{keyword, gconv.String(accountInfo.DatasetIds[0])}) + } + content, err = SessionToolService.BuildGreeting(ctx, userId, accountInfo.Greeting, datasetDescriptions, len(accountInfo.DatasetIds)) } err = s.pushDelayMsg(ctx, key, sceneType.Code(), sceneType.Desc(), accountInfo.DatasetIds) if err != nil { @@ -67,19 +72,52 @@ func (s *sessionToolService) PushOpeningRemark(ctx context.Context, userId strin } func (s *sessionToolService) PushDialog(ctx context.Context, userId string, questionContent string, accountInfo *dto.AccountVO, headers map[string]string) (content string, err error) { - sceneType := scriptedSpeech.SceneTypeDialog - // 删除延迟消息 - //err = s.DeleteDelayMsg(ctx) - //if err != nil { - // return nil, err - //} content = "" - var key = fmt.Sprintf("account:%s:%s:%s", accountInfo.AccountCode, account.GetDescByCode(accountInfo.Platform), userId) + // 删除延迟消息 + if err = s.DeleteDelayMsg(ctx); err != nil { + return + } + var key = fmt.Sprintf(public.AccountMsgKey, accountInfo.AccountCode, account.GetDescByCode(accountInfo.Platform), userId) get, err := g.Redis().Get(ctx, key) if err != nil { return } if !g.IsEmpty(get) { + + sceneType := scriptedSpeech.SceneTypeDialog + var datasetIds []int64 + + var optionsMap *gvar.Var + optionsMap, err = g.Redis().Get(ctx, fmt.Sprintf(public.AccountGreetingOptionsKey, userId)) + if err != nil { + return + } + jsonStr := gconv.String(optionsMap) + var data map[string]interface{} + if err = gconv.Scan(jsonStr, &data); err != nil { + return + } + for i, item := range data { + // 把每一项转成 map + if i == questionContent { + m := gconv.Map(item) + questionContent = gconv.String(m["datasetName"]) + datasetIds = gconv.Int64s(m["datasetId"]) + } + } + + if g.IsEmpty(datasetIds) { + var datasetRes []int64 + datasetRes, err = s.getDatasetIdsByKeywords(ctx, questionContent, headers) + if err != nil { + return + } + if len(datasetRes) > 0 { + datasetIds = datasetRes + } else { + datasetIds = accountInfo.DatasetIds + } + } // 获取用户对话上下文 var history []*dto.Message history, err = SessionToolService.GetUserHistory(ctx, userId) @@ -127,10 +165,10 @@ func (s *sessionToolService) PushDialog(ctx context.Context, userId string, ques } } } - if sceneType.Code() != scriptedSpeech.SceneTypeCardSend.Code() { + if *sceneType.Code() != *scriptedSpeech.SceneTypeCardSend.Code() { // 通过HTTP调用rag服务的RAG查询接口 var ragQuery *dto.RagQueryRes - ragQuery, err = SessionToolService.GetRagQuery(ctx, questionContent, accountInfo.DatasetIds, history, headers) + ragQuery, err = SessionToolService.GetRagQuery(ctx, questionContent, datasetIds, history, headers) if err != nil { err = fmt.Errorf("调用rag服务的RAG查询接口失败: %w", err) return @@ -148,7 +186,7 @@ func (s *sessionToolService) PushDialog(ctx context.Context, userId string, ques } } - err = s.pushDelayMsg(ctx, key, sceneType.Code(), sceneType.Desc(), accountInfo.DatasetIds) + err = s.pushDelayMsg(ctx, key, sceneType.Code(), sceneType.Desc(), datasetIds) if err != nil { return } @@ -157,7 +195,7 @@ func (s *sessionToolService) PushDialog(ctx context.Context, userId string, ques } func (s *sessionToolService) pushDelayMsg(ctx context.Context, key string, sceneTypeCode scriptedSpeech.SceneType, sceneTypeDesc string, datasetIds []int64) (err error) { - err = g.Redis().SetEX(ctx, key, sceneTypeDesc, gconv.Int64(10*time.Second)) + err = g.Redis().SetEX(ctx, key, sceneTypeDesc, gconv.Int64(public.DialogTimeout*time.Second)) if err != nil { return err } @@ -168,19 +206,19 @@ func (s *sessionToolService) pushDelayMsg(ctx context.Context, key string, scene if err != nil { return fmt.Errorf("获取追问话术内容失败: %w", err) } - if g.IsEmpty(scriptedSpeechInfo) { - if sceneTypeCode == scriptedSpeech.SceneTypeOpeningRemark.Code() { - msg = "宝子,刚才给您发的信息您有看到吗?有任何问题都能直接问我,加微信也能更方便沟通~" - } else if sceneTypeCode == scriptedSpeech.SceneTypeDialog.Code() { - msg = "看您暂时没回复,是不是还有什么疑问?加微信我详细给您说明~" - } else if sceneTypeCode == scriptedSpeech.SceneTypeCardSend.Code() { - msg = "宝子,加上没~要及时加哦,不然卡片容易失效哒✨" - } - } msg = scriptedSpeechInfo.QuestionContent - } else { - msg = "宝子,刚才给您发的信息您有看到吗?有任何问题都能直接问我,加微信也能更方便沟通~" } + + if g.IsEmpty(msg) { + if *sceneTypeCode == *scriptedSpeech.SceneTypeOpeningRemark.Code() { + msg = public.SceneOpeningRemark + } else if *sceneTypeCode == *scriptedSpeech.SceneTypeDialog.Code() { + msg = public.SceneDialog + } else if *sceneTypeCode == *scriptedSpeech.SceneTypeCardSend.Code() { + msg = public.SceneCardSend + } + } + var msgMap = map[string]string{ "key": key, "data": msg, @@ -197,6 +235,14 @@ func (s *sessionToolService) pushDelayMsg(ctx context.Context, key string, scene return } +func (s *sessionToolService) DeleteDelayMsg(ctx context.Context) (err error) { + return gmq.GetGmq(public.GmqMsgPluginsName).GmqDeleteDelay(ctx, &mq.NatsDelMessage{ + DelMessage: types.DelMessage{ + Topic: public.AccountFollowupTopic, + }, + }) +} + // GetAccountInfo 获取客服账号信息 func (s *sessionToolService) GetAccountInfo(ctx context.Context, accountCode string) (res *dto.AccountVO, err error) { r, err := dao.Account.GetByAccountCode(ctx, &dto.GetByAccountCodeReq{ @@ -248,31 +294,34 @@ func (s *sessionToolService) GetDatasetInfo(ctx context.Context, datasetIds []in return } -// BuildMenuContent 生成菜单话术内容 -func (s *sessionToolService) BuildMenuContent(greeting string, options []string, datasetCount int) string { +// BuildGreeting 构建问候语 +func (s *sessionToolService) BuildGreeting(ctx context.Context, userId, greeting string, options [][]string, datasetCount int) (content string, err error) { var sb strings.Builder // 问候语 - if datasetCount > 1 { - greeting = "您好,很高兴为您服务!请问咨询什么方面问题?" - } else { - if greeting == "" { - greeting = "您好,很高兴为您服务!请问有什么可以帮您?" - } + if datasetCount > 1 || greeting == "" { + greeting = public.GreetingBegin } sb.WriteString(greeting) sb.WriteByte('\n') // 拼接选项 1、xx 2、xx... + var optionsMap = make(map[string]map[string]string, len(options)) for i, opt := range options { + optionsMap[gconv.String(i+1)] = map[string]string{ + "datasetId": opt[1], + "datasetName": opt[0], + } sb.WriteString(fmt.Sprintf("%d、%s\n", i+1, opt)) if i == len(options)-1 { - sb.WriteString(fmt.Sprintf("%s\n", "💗回复数字就好~")) + sb.WriteString(fmt.Sprintf("%s\n", public.GreetingBetween)) } } // 固定结尾 - sb.WriteString("🌟也可直接点击下方咨询专业老师~") + sb.WriteString(public.GreetingEnd) + content = sb.String() + err = g.Redis().SetEX(ctx, fmt.Sprintf(public.AccountGreetingOptionsKey, userId), optionsMap, gconv.Int64(public.DialogTimeout*time.Second)) - return sb.String() + return } // GetScriptedSpeechContent 获取话术内容 @@ -304,7 +353,7 @@ func (s *sessionToolService) GetRagQuery(ctx context.Context, questionContent st // SaveUserHistory 保存用户对话历史到Redis func (s *sessionToolService) SaveUserHistory(ctx context.Context, userKey string, newMessages []*dto.Message) (err error) { - key := fmt.Sprintf(public.AccountDialogKeyUserId, userKey) + key := fmt.Sprintf(public.AccountDialogHistoryKey, userKey) // 1. 先读旧历史 var oldMessages []*dto.Message @@ -327,7 +376,7 @@ func (s *sessionToolService) SaveUserHistory(ctx context.Context, userKey string if err != nil { return err } - return g.Redis().SetEX(ctx, key, data, gconv.Int64(15*time.Second)) + return g.Redis().SetEX(ctx, key, data, gconv.Int64(public.DialogTimeout*time.Second)) } // GetUserHistory 从Redis获取用户历史 @@ -346,19 +395,19 @@ func (s *sessionToolService) GetUserHistory(ctx context.Context, key string) ([] // ClearUserHistory 清空历史(可选) func (s *sessionToolService) ClearUserHistory(ctx context.Context, userKey string) (int64, error) { - key := fmt.Sprintf(public.AccountDialogKeyUserId, userKey) + key := fmt.Sprintf(public.AccountDialogHistoryKey, userKey) return g.Redis().Del(ctx, key) } // getDatasetIdsByKeywords 通过关键词查询数据集ID -func (s *sessionToolService) getDatasetIdsByKeywords(ctx context.Context, content string, headers map[string]string) (res []int64, err error) { +func (s *sessionToolService) getDatasetIdsByKeywords(ctx context.Context, questionContent string, headers map[string]string) (res []int64, err error) { // 1. 提取关键词 - keywords := s.extractKeywords(content) + keywords := s.extractKeywords(questionContent) g.Log().Infof(ctx, "提取关键词: %v", keywords) // 通过HTTP调用rag服务的关键词查询接口 respKeyword := &dto.RAGListKeywordRes{} - if err = http.Get(ctx, "rag/keyword/listKeyword", headers, &respKeyword, &dto.RAGListKeywordReq{ + if err = http.Get(ctx, "rag/keyword/list", headers, &respKeyword, &dto.RAGListKeywordReq{ Words: keywords, }); err != nil { jaeger.RecordError(ctx, err, "RAG查询关键词失败")