From c174ab683adbdf50024fa76c94aa3fc9fabb0052 Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Tue, 3 Feb 2026 18:50:51 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A1.=E8=B0=83=E6=95=B4=E9=92=89?= =?UTF-8?q?=E9=92=89=E5=8D=95=E8=81=8A=E9=97=AE=E9=A2=98=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E8=B4=9F=E8=B4=A3=E4=BA=BA=E6=95=B4=E4=BD=93=E9=80=BB=E8=BE=91?= =?UTF-8?q?=202.=E5=A2=9E=E5=8A=A0=E6=9C=BA=E5=99=A8=E4=BA=BA=E4=B8=AD?= =?UTF-8?q?=E9=97=B4=E5=9B=9E=E5=A4=8D=203.=20=E5=8D=95=E5=85=83=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=EF=BC=8C=E6=8F=90=E7=A4=BA=E8=AF=8D=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/biz/ding_talk_bot.go | 259 +++++++++++++++++---------------- internal/biz/do/handle.go | 18 ++- internal/biz/group_config.go | 2 +- internal/data/impl/bot_user.go | 6 +- 4 files changed, 155 insertions(+), 130 deletions(-) diff --git a/internal/biz/ding_talk_bot.go b/internal/biz/ding_talk_bot.go index 5ae8f27..7f7a251 100644 --- a/internal/biz/ding_talk_bot.go +++ b/internal/biz/ding_talk_bot.go @@ -8,6 +8,7 @@ import ( "ai_scheduler/internal/data/constants" "ai_scheduler/internal/data/impl" "ai_scheduler/internal/data/model" + "ai_scheduler/internal/domain/tools/common/knowledge_base" "ai_scheduler/internal/entitys" "ai_scheduler/internal/pkg" "ai_scheduler/internal/tools" @@ -162,22 +163,27 @@ func (d *DingTalkBotBiz) Do(ctx context.Context, requireData *entitys.RequireDat // 先不接意图识别-仅提供问题处理 func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) { // 1. 获取用户信息 - requireData.UserInfo, err = d.dingTalkUser.GetUserInfoFromBot(ctx, requireData.Req.SenderStaffId, dingtalk.WithId(1)) + user, err := d.botUserImpl.GetByStaffId(requireData.Req.SenderStaffId) if err != nil { return } - requireData.ID = int32(requireData.UserInfo.UserId) + requireData.ID = int32(user.UserID) + requireData.UserInfo = &entitys.DingTalkUserInfo{ + UserId: int(user.UserID), + StaffId: user.StaffID, + Name: user.Name, + } // 2. 检查会话状态 (Redis) - statusKey := fmt.Sprintf("ai_bot:session:status:%s", requireData.Req.SenderStaffId) - status, _ := d.redisCli.Get(ctx, statusKey).Result() + // statusKey := fmt.Sprintf("ai_bot:session:status:%s", requireData.Req.SenderStaffId) + // status, _ := d.redisCli.Get(ctx, statusKey).Result() - if status == "WAITING_FOR_SYS_CONFIRM" { - // 用户回复了系统名称 - sysName := requireData.Req.Text.Content - d.redisCli.Del(ctx, statusKey) - return d.handleWithSpecificSys(ctx, requireData, sysName) - } + // if status == "WAITING_FOR_SYS_CONFIRM" { + // // 用户回复了系统名称 + // sysName := requireData.Req.Text.Content + // d.redisCli.Del(ctx, statusKey) + // return d.handleWithSpecificSys(ctx, requireData, sysName) + // } // 3. 获取历史记录 (最近6轮用户输入) userHist, err := d.getRecentUserHistory(ctx, constants.ConversationTypeSingle, requireData.ID, 6) @@ -192,76 +198,96 @@ func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *enti queryText = rewrittenQuery } - // 构造识别对象 - rec := &entitys.Recognize{ - Ch: requireData.Ch, - SystemPrompt: d.defaultPrompt(), - UserContent: &entitys.RecognizeUserContent{ - Text: queryText, - }, - } - - // 5. 调用知识库 - isRetrieved, err := d.groupConfigBiz.handleKnowledge(ctx, rec, nil, requireData.Req) + // 分类 + resolveResult, err := d.resolveSystemAndIssueType(ctx, requireData, queryText) if err != nil { return err } + switch resolveResult.IssueType.Code { + case "knowledge_qa": + // 知识库问答 + return d.handleKnowledgeQA(ctx, requireData, queryText, resolveResult) + default: // 其他问题类型 + // 系统为空,再次询问 + if resolveResult.Sys.SysID == 0 { + entitys.ResText(requireData.Ch, "", "\n抱歉,我无法确定您咨询的是哪个系统。请告诉我具体系统名称(如:直连天下系统、货易通系统),以便我为您准确解答或安排对应的技术支持。") + return nil + } + return d.fallbackToGroupCreation(ctx, requireData, resolveResult) + } +} + +// 知识库问答 +func (d *DingTalkBotBiz) handleKnowledgeQA(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, queryText string, resolveResult *resolveSystemAndIssueTypeResult) error { + // 获取租户ID + tenantId := constants.KnowledgeTenantIdDefault + if resolveResult.Sys.KnowlegeTenantKey != "" { + tenantId = resolveResult.Sys.KnowlegeTenantKey + } + + // 获取知识库结果 + isRetrieved, err := d.getKnowledgeAnswer(ctx, requireData, tenantId, queryText) + if err != nil { + return err + } if isRetrieved { return nil } - // 6. Fallback: 分类 -> 规则 -> 拉群 - return d.fallbackToGroupCreation(ctx, requireData) -} - -// handleWithSpecificSys 处理用户明确指定的系统 -func (d *DingTalkBotBiz) handleWithSpecificSys(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, sysName string) error { - // 1. 查找系统 - var sys model.AiSy - cond := builder.NewCond().And(builder.Eq{"sys_name": sysName}) - err := d.sysImpl.GetOneBySearchToStrut(&cond, &sys) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - entitys.ResText(requireData.Ch, "", "抱歉,我还是没有找到名为“"+sysName+"”的系统。请联系管理员确认系统名称。") - return nil - } - return err + // 未匹配&全局 -> 明确具体系统 + if !isRetrieved && resolveResult.Sys.SysID == 0 { + entitys.ResText(requireData.Ch, "", "\n抱歉,知识库未命中,无法回答您的问题。\n若您的问题是某一具体系统的,请告诉我具体系统名称(如:直连天下系统、货易通系统),以便我为您准确解答。") + return nil + } + // 未匹配&指定系统 -> 拉群卡片 + if !isRetrieved && resolveResult.Sys.SysID != 0 { + entitys.ResText(requireData.Ch, "", fmt.Sprintf("\n抱歉,%s知识库未命中,无法回答您的问题。即将为您创建群聊解答。", resolveResult.Sys.SysName)) + return d.fallbackToGroupCreation(ctx, requireData, resolveResult) } - // 2. 既然已经明确了系统,直接尝试拉群(这里假设问题类型为“其他”或由LLM再次分析) - // 为简化,这里再次调用分类逻辑,但带上已确定的系统 - return d.fallbackToGroupCreationWithSys(ctx, requireData, &sys) + return nil } -// getRecentUserHistory 获取最近的用户输入历史 -func (d *DingTalkBotBiz) getRecentUserHistory(ctx context.Context, conversationType constants.ConversationType, id int32, limit int) ([]model.AiBotChatHi, error) { - var his []model.AiBotChatHi - cond := builder.NewCond(). - And(builder.Eq{"his_type": conversationType}). - And(builder.Eq{"id": id}). - And(builder.Eq{"role": "user"}) - - _, err := d.chatHis.GetListToStruct(&cond, &dataTemp.ReqPageBo{Limit: limit}, &his, "his_id desc") +// 获取知识库问答结果 +func (d *DingTalkBotBiz) getKnowledgeAnswer(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, tenantId string, queryText string) (bool, error) { + // 请求知识库工具 + knowledgeBase := knowledge_base.New(d.conf.KnowledgeConfig) + knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{ + TenantID: tenantId, // 后续动态接参 + Query: queryText, + Mode: constants.KnowledgeModeMix, + Stream: true, + Think: false, + OnlyRAG: true, + }) if err != nil { - return nil, err + return false, fmt.Errorf("请求知识库工具失败,err: %v", err) } - return his, nil + + // 读取知识库SSE数据 + return d.groupConfigBiz.readKnowledgeSSE(knowledgeResp, requireData.Ch, true) } -// fallbackToGroupCreation 分类并拉群 -func (d *DingTalkBotBiz) fallbackToGroupCreation(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) error { +type resolveSystemAndIssueTypeResult struct { + Sys model.AiSy + IssueType model.AiIssueType + Classification *do.IssueClassification +} + +// 解析系统和问题类型 +func (d *DingTalkBotBiz) resolveSystemAndIssueType(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, queryText string) (*resolveSystemAndIssueTypeResult, error) { // 1. 获取所有系统和问题类型用于分类 allSys, err := d.sysImpl.FindAll() if err != nil { - return err + return nil, err } sysNames := slice.Map(allSys, func(_ int, sys model.AiSy) string { return sys.SysName }) allIssueTypes, err := d.issueImpl.IssueType.FindAll() if err != nil { - return err + return nil, err } issueTypeNames := slice.Map(allIssueTypes, func(_ int, it model.AiIssueType) string { return it.Name @@ -270,8 +296,7 @@ func (d *DingTalkBotBiz) fallbackToGroupCreation(ctx context.Context, requireDat // 2. LLM 分类 classification, err := d.handle.ClassifyIssue(ctx, sysNames, issueTypeNames, requireData.Req.Text.Content) if err != nil { - // 分类失败,使用兜底 - return d.createDefaultGroup(ctx, requireData, "系统无法识别") + return nil, err } // 3. 匹配系统 @@ -292,68 +317,58 @@ func (d *DingTalkBotBiz) fallbackToGroupCreation(ctx context.Context, requireDat } } - if sys.SysID == 0 { - - // 判断全局是否存在该规则 - _, found, _ := d.issueImpl.IssueAssignRule.FindOne( - d.issueImpl.WithSysID(0), - d.issueImpl.WithIssueTypeID(issueType.ID), - d.issueImpl.WithStatus(1), - ) - if !found { - // 无法明确系统,且全局无该能力,询问用户 - statusKey := fmt.Sprintf("ai_bot:session:status:%s", requireData.Req.SenderStaffId) - d.redisCli.Set(ctx, statusKey, "WAITING_FOR_SYS_CONFIRM", time.Hour) - entitys.ResText(requireData.Ch, "", "抱歉,我无法确定您咨询的是哪个系统。请告诉我具体系统名称(如:直连天下系统、货易通系统),以便我为您安排对应的技术支持。") - return nil - } - - } - - return d.fallbackToGroupCreationWithSys(ctx, requireData, &sys) + return &resolveSystemAndIssueTypeResult{ + Sys: sys, + IssueType: issueType, + Classification: classification, + }, nil } -// fallbackToGroupCreationWithSys 在已知系统的情况下进行分类并拉群 -func (d *DingTalkBotBiz) fallbackToGroupCreationWithSys(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, sys *model.AiSy) error { - // 1. 获取所有问题类型 - allIssueTypes, err := d.issueImpl.IssueType.FindAll() - if err != nil { - return err - } - issueTypeNames := slice.Map(allIssueTypes, func(_ int, it model.AiIssueType) string { - return it.Name - }) +// handleWithSpecificSys 处理用户明确指定的系统 +// func (d *DingTalkBotBiz) handleWithSpecificSys(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, sysName string) error { +// // 1. 查找系统 +// var sys model.AiSy +// cond := builder.NewCond().And(builder.Eq{"sys_name": sysName}) +// err := d.sysImpl.GetOneBySearchToStrut(&cond, &sys) +// if err != nil { +// if errors.Is(err, sql.ErrNoRows) { +// entitys.ResText(requireData.Ch, "", "抱歉,我还是没有找到名为“"+sysName+"”的系统。请联系管理员确认系统名称。") +// return nil +// } +// return err +// } - // 2. LLM 再次分类(确定问题类型和简述) - classification, err := d.handle.ClassifyIssue(ctx, []string{sys.SysName}, issueTypeNames, requireData.Req.Text.Content) - if err != nil { - return d.createDefaultGroup(ctx, requireData, "问题类型识别失败") - } - issueType, found, err := d.issueImpl.IssueType.FindOne(d.issueImpl.WithName(classification.IssueTypeName)) - if !found { - log.Errorf("issue type %s not found; err: %v", classification.IssueTypeName, err) - return fmt.Errorf("问题类型 %s 不存在", classification.IssueTypeName) - } +// // 2. 既然已经明确了系统,直接尝试拉群(这里假设问题类型为“其他”或由LLM再次分析) +// // 为简化,这里再次调用分类逻辑,但带上已确定的系统 +// return d.fallbackToGroupCreationWithSys(ctx, requireData, &sys) +// } - // 3. 查找分配规则 - rule, found, err := d.issueImpl.IssueAssignRule.FindOne( - d.issueImpl.WithSysID(sys.SysID), - d.issueImpl.WithIssueTypeID(issueType.ID), +// getRecentUserHistory 获取最近的用户输入历史 +func (d *DingTalkBotBiz) getRecentUserHistory(ctx context.Context, conversationType constants.ConversationType, id int32, limit int) ([]model.AiBotChatHi, error) { + var his []model.AiBotChatHi + cond := builder.NewCond(). + And(builder.Eq{"his_type": conversationType}). + And(builder.Eq{"id": id}). + And(builder.Eq{"role": "user"}) + + _, err := d.chatHis.GetListToStruct(&cond, &dataTemp.ReqPageBo{Limit: limit}, &his, "his_id desc") + if err != nil { + return nil, err + } + return his, nil +} + +// 在已知系统&问题类型的情况下进行分类并拉群 +func (d *DingTalkBotBiz) fallbackToGroupCreation(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, resolveResult *resolveSystemAndIssueTypeResult) error { + // 查找分配规则 + rule, found, _ := d.issueImpl.IssueAssignRule.FindOne( + d.issueImpl.WithSysID(resolveResult.Sys.SysID), + d.issueImpl.WithIssueTypeID(resolveResult.IssueType.ID), d.issueImpl.WithStatus(1), ) if !found { - // 创建默认分配规则 - 暂不考虑并发,有唯一索引 - rule = model.AiIssueAssignRule{ - SysID: sys.SysID, - IssueTypeID: issueType.ID, - Status: 1, - } - if err := d.issueImpl.IssueAssignRule.Create(&rule); err != nil { - log.Errorf("create assign rule for sys %s and issue type %s failed; err: %v", sys.SysName, issueType.Name, err) - return fmt.Errorf("创建分配规则 %s-%s 失败", sys.SysName, issueType.Name) - } else { - log.Infof("create assign rule for sys %s and issue type %s success; rule id: %d", sys.SysName, issueType.Name, rule.ID) - } + entitys.ResText(requireData.Ch, "", fmt.Sprintf("\n抱歉,当前系统未配置路由规则 %s-%s,请联系管理员配置。", resolveResult.Sys.SysName, resolveResult.IssueType.Name)) + return nil } var staffIds []string @@ -392,15 +407,15 @@ func (d *DingTalkBotBiz) fallbackToGroupCreationWithSys(ctx context.Context, req // 合并提问者 staffIds = append([]string{requireData.Req.SenderStaffId}, staffIds...) - // 4. 发送确认卡片 - groupName := fmt.Sprintf("[%s]-%s", classification.IssueTypeName, classification.Summary) + // 发送确认卡片 + groupName := fmt.Sprintf("[%s]-%s", resolveResult.IssueType.Name, resolveResult.Classification.Summary) return d.SendGroupCreationConfirmCard(ctx, &SendGroupCreationConfirmCardParams{ RobotCode: requireData.Req.RobotCode, ConversationId: requireData.Req.ConversationId, SenderStaffId: requireData.Req.SenderStaffId, UserIds: staffIds, GroupName: groupName, - Summary: classification.Summary, + Summary: resolveResult.Classification.Summary, }) } @@ -759,7 +774,7 @@ func (d *DingTalkBotBiz) SendGroupCreationConfirmCard(ctx context.Context, param } // 构建卡片 OutTrackId - outTrackId := constants.BuildCardOutTrackId(params.ConversationId, params.RobotCode) + outTrackId := constants.BuildCardOutTrackId(params.SenderStaffId, params.RobotCode) // 准备可见人员列表 var recipients []*string @@ -804,13 +819,13 @@ func (d *DingTalkBotBiz) SendGroupCreationConfirmCard(ctx context.Context, param "target_user_ids": tea.String(strings.Join(params.UserIds, ",")), }, }, - ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{ - SupportForward: tea.Bool(false), + OpenSpaceId: tea.String("dtv1.card//IM_ROBOT." + params.SenderStaffId), + ImRobotOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImRobotOpenDeliverModel{ + SpaceType: tea.String("IM_ROBOT"), + RobotCode: tea.String(params.RobotCode), }, - OpenSpaceId: tea.String("dtv1.card//im_group." + params.ConversationId), - ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{ - RobotCode: tea.String(params.RobotCode), - Recipients: recipients, + ImRobotOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImRobotOpenSpaceModel{ + SupportForward: tea.Bool(false), }, }) return err diff --git a/internal/biz/do/handle.go b/internal/biz/do/handle.go index 6508a69..588f57c 100644 --- a/internal/biz/do/handle.go +++ b/internal/biz/do/handle.go @@ -132,15 +132,23 @@ type IssueClassification struct { // ClassifyIssue 问题分类分析 func (r *Handle) ClassifyIssue(ctx context.Context, systems []string, issueTypes []string, userInput string) (*IssueClassification, error) { - systemPrompt := fmt.Sprintf(`你是一个技术支持路由专家。请分析用户的输入,并将其归类到最合适的系统和问题类型中。 + systemPrompt := fmt.Sprintf(`## 角色 +你是一个技术支持路由专家。输出必须是 JSON 格式。 +## 任务 +请分析用户的输入,并将其归类到最合适的系统和问题类型中。 +- 系统名称:必须是可用系统列表中的一个,若未提及可用系统关键词,则为"全局",不要臆想! +- 问题类型名称:必须是可用问题类型列表中的一个,若未提及可用问题类型关键词,则为空字符串 +- 问题简述:15字以内的问题简述(用于群聊命名) +- 分类判断理由:对系统名称和问题类型名称的判断理由 +## 背景与材料: 可用系统列表: [%s] 可用问题类型: [%s] - +## 输出 请仅以 JSON 格式回复,包含以下字段: -- sys_name: 系统名称,若未提及系统关键词,则为"全局" +- sys_name: 系统名称 - issue_type_name: 问题类型名称 -- summary: 15字以内的问题简述(用于群命名) -- reason: 分类判断理由;系统名称判断理由`, strings.Join(systems, ", "), strings.Join(issueTypes, ", ")) +- summary: 问题简述 +- reason: 分类判断理由`, strings.Join(systems, ", "), strings.Join(issueTypes, ", ")) messages := []api.Message{ {Role: "system", Content: systemPrompt}, diff --git a/internal/biz/group_config.go b/internal/biz/group_config.go index 102ea84..ce7589f 100644 --- a/internal/biz/group_config.go +++ b/internal/biz/group_config.go @@ -542,7 +542,7 @@ func (g *GroupConfigBiz) readKnowledgeSSE(resp io.ReadCloser, channel chan entit // 知识库未命中 输出提示后中断 if delta.XRagStatus == constants.KnowledgeRagStatusMiss { - var missContent string = "知识库未检测到匹配信息,即将为您创建群聊解决问题。" + var missContent string = "知识库未检测到匹配信息。" entitys.ResStream(channel, "", missContent) return false, nil } diff --git a/internal/data/impl/bot_user.go b/internal/data/impl/bot_user.go index ce47281..3a8f5fd 100644 --- a/internal/data/impl/bot_user.go +++ b/internal/data/impl/bot_user.go @@ -31,8 +31,10 @@ func (k BotUserImpl) GetByStaffId(staffId string) (*model.AiBotUser, error) { func (k BotUserImpl) GetByUserIds(userIds []int32) ([]model.AiBotUser, error) { var data []model.AiBotUser cond := builder.NewCond() - cond = cond.And(builder.In("user_id", userIds)) - _, err := k.GetListToStruct2(&cond, nil, &data) + for _, userId := range userIds { + cond = cond.Or(builder.Eq{"user_id": userId}) + } + _, err := k.GetListToStruct(&cond, nil, &data, "user_id") return data, err }