diff --git a/internal/biz/callback.go b/internal/biz/callback.go new file mode 100644 index 0000000..40b8e40 --- /dev/null +++ b/internal/biz/callback.go @@ -0,0 +1,290 @@ +package biz + +import ( + "ai_scheduler/internal/config" + "ai_scheduler/internal/data/constants" + "ai_scheduler/internal/data/impl" + "ai_scheduler/internal/domain/tools/common/knowledge_base" + "ai_scheduler/internal/pkg/dingtalk" + "ai_scheduler/internal/pkg/util" + "ai_scheduler/internal/pkg/utils_ollama" + "context" + "encoding/json" + "fmt" + "io" + "strings" + + "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card" + "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot" + "github.com/alibabacloud-go/dingtalk/card_1_0" + "github.com/alibabacloud-go/tea/tea" + "github.com/gofiber/fiber/v2/log" + "github.com/ollama/ollama/api" +) + +type CallbackBiz struct { + cfg *config.Config + ollamaClient *utils_ollama.Client + dingtalkCardClient *dingtalk.CardClient + botConfigImpl *impl.BotConfigImpl +} + +func NewCallbackBiz( + cfg *config.Config, + ollamaClient *utils_ollama.Client, + dingtalkCardClient *dingtalk.CardClient, + botConfigImpl *impl.BotConfigImpl, +) *CallbackBiz { + return &CallbackBiz{ + cfg: cfg, + ollamaClient: ollamaClient, + dingtalkCardClient: dingtalkCardClient, + botConfigImpl: botConfigImpl, + } +} + +// IssueHandlingGroup 问题处理群机器人回调 +// 能力1: 通过[内容提取] 宏,分析用户QA问题,调出QA表单卡片 +// 能力2: 通过[QA收集] 宏,收集用户反馈,写入知识库 +// 能力3: 通过[知识库查询] 宏,查询知识库,返回答案 +func (c *CallbackBiz) IssueHandlingGroup(data chatbot.BotCallbackDataModel) error { + // 能力1、2:分析用户QA问题,写入知识库 + if strings.Contains(data.Text.Content, "[内容提取]") || strings.Contains(data.Text.Content, "[QA收集]") { + c.issueHandlingExtractContent(data) + } + // 能力3:查询知识库,返回答案 + if strings.Contains(data.Text.Content, "[知识库查询]") { + c.issueHandlingQueryKnowledgeBase(data) + } + + return nil +} + +// 问题处理群机器人内容提取 +func (c *CallbackBiz) issueHandlingExtractContent(data chatbot.BotCallbackDataModel) { + // 1.提取用户输入 + prompt := fmt.Sprintf(constants.IssueHandlingExtractContentPrompt, data.Text.Content) + log.Infof("问题提取提示词: %s", prompt) + // LLM 提取 + generateResp, err := c.ollamaClient.Generation(context.Background(), &api.GenerateRequest{ + Model: c.cfg.Ollama.GenerateModel, + Prompt: prompt, + Stream: util.AnyToPoint(false), + }) + if err != nil { + log.Errorf("问题提取失败: %v", err) + return + } + // 解析 JSON 响应 + var resp struct { + Items []struct { + Question string `json:"question"` + Answer string `json:"answer"` + Confidence string `json:"confidence"` + } `json:"items"` + } + if err := json.Unmarshal([]byte(generateResp.Response), &resp); err != nil { + log.Errorf("解析 JSON 响应失败: %v", err) + return + } + + // 2.构建文本域内容 + cardContentTpl := "问题:%s \n答案:%s" + var cardContentList []string + for _, item := range resp.Items { + cardContentList = append(cardContentList, fmt.Sprintf(cardContentTpl, item.Question, item.Answer)) + } + cardContent := strings.Join(cardContentList, "\n\n") + + // 3.获取应用AppKey + appKey, err := c.botConfigImpl.GetRobotAppKey(data.RobotCode) + if err != nil { + log.Errorf("获取应用配置失败: %v", err) + return + } + + // 4.创建并投放卡片 + outTrackId := constants.BuildCardOutTrackId(data.ConversationId, data.RobotCode) // 构建卡片 OutTrackId + _, err = c.dingtalkCardClient.CreateAndDeliver( + appKey, + &card_1_0.CreateAndDeliverRequest{ + CardTemplateId: tea.String(c.cfg.Dingtalk.Card.Template.ContentCollect), + OutTrackId: tea.String(outTrackId), + CallbackType: tea.String("HTTP"), + CallbackRouteKey: tea.String(c.cfg.Dingtalk.Card.CallbackRouteKey), + CardData: &card_1_0.CreateAndDeliverRequestCardData{ + CardParamMap: map[string]*string{ + "title": tea.String("QA知识收集"), + "button_display": tea.String("true"), + "QA_details_now": tea.String(cardContent), + "textarea_display": tea.String("normal"), + "action_id": tea.String("collect_qa"), + "tenant_id": tea.String(constants.KnowledgeTenantIdDefault), + "_CARD_DEBUG_TOOL_ENTRY": tea.String(c.cfg.Dingtalk.Card.DebugToolEntryShow), // 调试字段 + }, + }, + ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{ + SupportForward: tea.Bool(false), + }, + OpenSpaceId: tea.String("dtv1.card//im_group." + data.ConversationId), + ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{ + RobotCode: tea.String(c.cfg.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling), + }, + }, + ) + +} + +// 问题处理群机器人查询知识库 +func (c *CallbackBiz) issueHandlingQueryKnowledgeBase(data chatbot.BotCallbackDataModel) { + // 获取应用配置 + appKey, err := c.botConfigImpl.GetRobotAppKey(data.RobotCode) + if err != nil { + log.Errorf("应用机器人配置不存在: %s, err: %v", data.RobotCode, err) + return + } + // 创建卡片 + outTrackId := constants.BuildCardOutTrackId(data.ConversationId, data.RobotCode) + _, err = c.dingtalkCardClient.CreateAndDeliver( + appKey, + &card_1_0.CreateAndDeliverRequest{ + CardTemplateId: tea.String(c.cfg.Dingtalk.Card.Template.BaseMsg), + CardData: &card_1_0.CreateAndDeliverRequestCardData{ + CardParamMap: map[string]*string{ + "title": tea.String(data.Text.Content), + "markdown": tea.String("知识库检索中..."), + }, + }, + OutTrackId: tea.String(outTrackId), + ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{ + SupportForward: tea.Bool(false), + }, + OpenSpaceId: tea.String("dtv1.card//im_group." + data.ConversationId), + ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{ + RobotCode: tea.String(data.RobotCode), + }, + }, + ) + + // 查询知识库 + knowledgeBase := knowledge_base.New(c.cfg.KnowledgeConfig) + knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{ + TenantID: constants.KnowledgeTenantIdDefault, + Query: data.Text.Content, + Mode: constants.KnowledgeModeMix, + Stream: false, + Think: false, + OnlyRAG: true, + }) + if err != nil { + log.Errorf("查询知识库失败: %v", err) + return + } + knowledgeRespBytes, err := io.ReadAll(knowledgeResp) + if err != nil { + log.Errorf("读取知识库响应失败: %v", err) + return + } + + // 卡片更新 + message, isRetrieved, err := knowledge_base.ParseOpenAIHTTPData(string(knowledgeRespBytes)) + if err != nil { + log.Errorf("读取知识库 SSE 数据失败: %v", err) + return + } + + content := message.Content + if !isRetrieved { + content = "知识库未检测到匹配信息,请核查知识库数据是否正确。" + } + + // 卡片更新 + _, err = c.dingtalkCardClient.UpdateCard( + appKey, + &card_1_0.UpdateCardRequest{ + OutTrackId: tea.String(outTrackId), + CardData: &card_1_0.UpdateCardRequestCardData{ + CardParamMap: map[string]*string{ + "markdown": tea.String(content), + }, + }, + CardUpdateOptions: &card_1_0.UpdateCardRequestCardUpdateOptions{ + UpdateCardDataByKey: tea.Bool(true), + }, + }, + ) + if err != nil { + log.Errorf("更新卡片失败: %v", err) + return + } +} + +// IssueHandlingCollectQA 问题处理群机器人 QA 收集回调 +func (c *CallbackBiz) IssueHandlingCollectQA(data card.CardRequest) *card.CardResponse { + // 确认提交,文本写入知识库 + if data.CardActionData.CardPrivateData.Params["submit"] == "submit" { + content := data.CardActionData.CardPrivateData.Params["QA_details"].(string) + tenantID := data.CardActionData.CardPrivateData.Params["tenant_id"].(string) + + // 协程执行耗时操作,防止阻塞 + util.SafeGo("inject_knowledge_base", func() { + knowledgeBase := knowledge_base.New(c.cfg.KnowledgeConfig) + err := knowledgeBase.IngestText(&knowledge_base.IngestTextRequest{ + TenantID: tenantID, + Text: content, + }) + if err != nil { + log.Errorf("注入知识库失败: %v", err) + } else { + log.Infof("注入知识库成功: tenantID=%s", tenantID) + } + + // 解析当前卡片的 ConversationId 和 robotCode + conversationId, robotCode := constants.ParseCardOutTrackId(data.OutTrackId) + + // 获取应用配置 + appKey, err := c.botConfigImpl.GetRobotAppKey(robotCode) + if err != nil { + log.Errorf("获取应用机器人配置失败: %v", err) + return + } + + // 发送卡片通知用户注入成功 + outTrackId := constants.BuildCardOutTrackId(conversationId, robotCode) + c.dingtalkCardClient.CreateAndDeliver( + appKey, + &card_1_0.CreateAndDeliverRequest{ + CardTemplateId: tea.String(c.cfg.Dingtalk.Card.Template.BaseMsg), + OutTrackId: tea.String(outTrackId), + CardData: &card_1_0.CreateAndDeliverRequestCardData{ + CardParamMap: map[string]*string{ + "title": tea.String("QA知识收集结果"), + "markdown": tea.String("[Get] **成功**"), + }, + }, + ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{ + SupportForward: tea.Bool(false), + }, + OpenSpaceId: tea.String("dtv1.card//im_group." + conversationId), + ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{ + RobotCode: tea.String(robotCode), + }, + }, + ) + }) + } + + // 取消提交,禁用输入框 + resp := &card.CardResponse{ + CardUpdateOptions: &card.CardUpdateOptions{ + UpdateCardDataByKey: true, + }, + CardData: &card.CardDataDto{ + CardParamMap: map[string]string{ + "textarea_display": "disabled", + }, + }, + } + + return resp +} diff --git a/internal/biz/ding_talk_bot.go b/internal/biz/ding_talk_bot.go index 1c23246..b832256 100644 --- a/internal/biz/ding_talk_bot.go +++ b/internal/biz/ding_talk_bot.go @@ -20,8 +20,13 @@ import ( "strings" "time" + "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card" "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot" + dingtalkPkg "ai_scheduler/internal/pkg/dingtalk" + + "github.com/alibabacloud-go/dingtalk/card_1_0" + "github.com/alibabacloud-go/tea/tea" "github.com/gofiber/fiber/v2/log" "xorm.io/builder" ) @@ -45,6 +50,9 @@ type DingTalkBotBiz struct { groupConfigBiz *GroupConfigBiz reportDailyCacheImpl *impl.ReportDailyCacheImpl macro *do.Macro + dingtalkOauth2Client *dingtalkPkg.Oauth2Client + dingTalkOld *dingtalkPkg.OldClient + dingtalkCardClient *dingtalkPkg.CardClient } // NewDingTalkBotBiz @@ -61,6 +69,9 @@ func NewDingTalkBotBiz( cardSend *dingtalk.SendCardClient, groupConfigBiz *GroupConfigBiz, macro *do.Macro, + dingtalkOauth2Client *dingtalkPkg.Oauth2Client, + dingTalkOld *dingtalkPkg.OldClient, + dingtalkCardClient *dingtalkPkg.CardClient, ) *DingTalkBotBiz { return &DingTalkBotBiz{ do: do, @@ -76,6 +87,9 @@ func NewDingTalkBotBiz( cardSend: cardSend, reportDailyCacheImpl: reportDailyCacheImpl, macro: macro, + dingtalkOauth2Client: dingtalkOauth2Client, + dingTalkOld: dingTalkOld, + dingtalkCardClient: dingtalkCardClient, } } @@ -408,3 +422,209 @@ func (d *DingTalkBotBiz) defaultPrompt() string { -parameters 必须是 **转义后的 JSON 字符串**(如 "{\"product_name\": \"京东月卡\"}")。 当前时间:` + now + `,所有的时间识别精确到秒` } + +// CreateIssueHandlingGroupAndInit 创建问题处理群聊并初始化 +func (d *DingTalkBotBiz) CreateIssueHandlingGroupAndInit(ctx context.Context, data *card.CardRequest) (resp *card.CardResponse, err error) { + + // 解析 OutTrackId 以获取 SpaceId 和 BotId + spaceId, botId := constants.ParseCardOutTrackId(data.OutTrackId) + + // 获取新群聊人员 + var userIds []string + userIds, err = d.buildNewGroupUserIds(spaceId, botId, data.UserId) + if err != nil { + return nil, err + } + + // 创建群聊及群初始化(异步响应) + if data.CardActionData.CardPrivateData.Params["status"] == "confirm" { + go func() { + err := d.createIssueHandlingGroupAndInit(ctx, data.CardActionData.CardPrivateData.Params, spaceId, botId, userIds) + if err != nil { + log.Errorf("创建群聊及群初始化失败: %v", err) + } + }() + } + + // 构建关闭创建群组卡片按钮的响应 + return d.buildCreateGroupCardResp(), nil +} + +// buildNewGroupUserIds 构建新群聊人员列表 +func (d *DingTalkBotBiz) buildNewGroupUserIds(spaceId, botId, groupOwner string) ([]string, error) { + // 群id+机器人id确认一个群配置 + botGroup, err := d.botGroupImpl.GetByConversationIdAndRobotCode(spaceId, botId) + if err != nil { + return nil, err + } + + // 获取群配置 + var groupConfig model.AiBotGroupConfig + cond := builder.NewCond().And(builder.Eq{"config_id": botGroup.ConfigID}) + err = d.botGroupConfigImpl.GetOneBySearchToStrut(&cond, &groupConfig) + if err != nil { + return nil, err + } + + // 获取处理人列表 + issueOwnerJson := groupConfig.IssueOwner + type issueOwnerType struct { + UserId string `json:"userid"` + Name string `json:"name"` + } + var issueOwner []issueOwnerType + if err = json.Unmarshal([]byte(issueOwnerJson), &issueOwner); err != nil { + return nil, err + } + + // 合并所有userid + userIds := []string{groupOwner} // 当前用户为群主 + for _, owner := range issueOwner { + userIds = append(userIds, owner.UserId) + } + + return userIds, nil +} + +// createIssueHandlingGroupAndInit 创建问题处理群聊及群初始化 +func (d *DingTalkBotBiz) createIssueHandlingGroupAndInit(ctx context.Context, callbackParams map[string]any, spaceId, botId string, userIds []string) error { + // 获取应用配置 + appKey, err := d.botConfigImpl.GetRobotAppKey(botId) + if err != nil { + return err + } + + // 获取 access_token + accessToken, err := d.dingtalkOauth2Client.GetAccessToken(appKey) + if err != nil { + return err + } + appKey.AccessToken = accessToken + + // 创建群聊 + _, openConversationId, err := d.createIssueHandlingGroup(ctx, accessToken, userIds) + if err != nil { + return err + } + + // 添加当前机器人到新群 - SDK 有问题,后续再考虑使用 + // _, err = d.dingtalkImClient.AddRobotToConversation( + // appKey, + // &im_1_0.AddRobotToConversationRequest{ + // OpenConversationId: tea.String(openConversationId), + // RobotCode: tea.String(botId), + // }) + // if err != nil { + // fmt.Printf("添加机器人到会话失败: %v", err) + // } + + // 返回新群分享链接,直接进群 - SDK 有问题,后续再考虑使用 + // newGroupShareLink, err = d.dingTalkOld.GetJoinGroupQrcode(ctx, chatId, data.UserId) + // if err != nil { + // fmt.Printf("获取入群二维码失败: %v", err) + // } + + // 初始化群聊 + groupScope := callbackParams["group_scope"].(string) // 群主题 + d.initIssueHandlingGroup(appKey, openConversationId, groupScope) + + return nil +} + +// createIssueHandlingGroup 创建问题处理群聊会话 +func (d *DingTalkBotBiz) createIssueHandlingGroup(ctx context.Context, accessToken string, userIds []string) (chatId, openConversationId string, err error) { + // 是否使用模板群开关 + var useTemplateGroup bool = true + + // 创建内部群会话 + if !useTemplateGroup { + return d.dingTalkOld.CreateInternalGroupConversation(ctx, accessToken, "问题处理群", userIds) + } + + // 根据群模板ID创建群 + if useTemplateGroup { + return d.dingTalkOld.CreateSceneGroupConversation(ctx, accessToken, "问题处理群", userIds, d.conf.Dingtalk.SceneGroup.GroupTemplateIDIssueHandling) + } + + return +} + +// initIssueHandlingGroup 初始化问题处理群聊 +func (d *DingTalkBotBiz) initIssueHandlingGroup(appKey dingtalkPkg.AppKey, openConversationId, groupScope string) error { + // 1.开场白 + outTrackId := constants.BuildCardOutTrackId(openConversationId, d.conf.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling) + _, err := d.dingtalkCardClient.CreateAndDeliver( + appKey, + &card_1_0.CreateAndDeliverRequest{ + CardTemplateId: tea.String(d.conf.Dingtalk.Card.Template.BaseMsg), + OutTrackId: tea.String(outTrackId), + CallbackType: tea.String("HTTP"), + CardData: &card_1_0.CreateAndDeliverRequestCardData{ + CardParamMap: map[string]*string{ + "title": tea.String("当前会话主题"), + "markdown": tea.String("问题:" + groupScope), + }, + }, + ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{ + SupportForward: tea.Bool(false), + }, + OpenSpaceId: tea.String("dtv1.card//im_group." + openConversationId), + ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{ + RobotCode: tea.String(d.conf.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling), + AtUserIds: map[string]*string{ + "@ALL": tea.String("@ALL"), + }, + }, + }, + ) + if err != nil { + return err + } + + // 2. 机器人能力 + // 构建卡片 OutTrackId + outTrackId = constants.BuildCardOutTrackId(openConversationId, d.conf.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling) + _, err = d.dingtalkCardClient.CreateAndDeliver( + appKey, + &card_1_0.CreateAndDeliverRequest{ + CardTemplateId: tea.String(d.conf.Dingtalk.Card.Template.BaseMsg), + OutTrackId: tea.String(outTrackId), + CallbackType: tea.String("HTTP"), + CardData: &card_1_0.CreateAndDeliverRequestCardData{ + CardParamMap: map[string]*string{ + "title": tea.String("当前机器人能力"), + "markdown": tea.String("- 聊天内容提取(@机器人 [内容提取]{聊天记录/问答描述}) \n - QA知识收集(卡片信息收集) \n - QA知识问答(@机器人 [知识库查询]{问题描述})"), + }, + }, + ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{ + SupportForward: tea.Bool(false), + }, + OpenSpaceId: tea.String("dtv1.card//im_group." + openConversationId), + ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{ + RobotCode: tea.String(d.conf.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling), + AtUserIds: map[string]*string{ + "@ALL": tea.String("@ALL"), + }, + }, + }, + ) + if err != nil { + return err + } + + return nil +} + +// buildCreateGroupCardResp 构建关闭创建群组卡片按钮 +func (d *DingTalkBotBiz) buildCreateGroupCardResp() *card.CardResponse { + return &card.CardResponse{ + CardData: &card.CardDataDto{ + CardParamMap: map[string]string{ + "button_display": "false", + }, + }, + CardUpdateOptions: &card.CardUpdateOptions{ + UpdateCardDataByKey: true, + }, + } +} diff --git a/internal/biz/group_config.go b/internal/biz/group_config.go index 32a6b76..2aecd0e 100644 --- a/internal/biz/group_config.go +++ b/internal/biz/group_config.go @@ -476,7 +476,7 @@ func (g *GroupConfigBiz) GetReportCache(ctx context.Context, day time.Time, tota return nil } -// handleKnowledgeV2 处理知识库V2版本 +// handleKnowledge 处理知识库V2版本 func (g *GroupConfigBiz) handleKnowledge(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig, callback *chatbot.BotCallbackDataModel) (err error) { // 请求知识库工具 knowledgeBase := knowledge_base.New(g.conf.KnowledgeConfig) diff --git a/internal/services/callback.go b/internal/services/callback.go index 267bf49..1b07006 100644 --- a/internal/services/callback.go +++ b/internal/services/callback.go @@ -6,31 +6,24 @@ import ( "ai_scheduler/internal/config" "ai_scheduler/internal/data/constants" errorcode "ai_scheduler/internal/data/error" - "ai_scheduler/internal/data/impl" "ai_scheduler/internal/domain/component/callback" - "ai_scheduler/internal/domain/tools/common/knowledge_base" "ai_scheduler/internal/entitys" "ai_scheduler/internal/gateway" "ai_scheduler/internal/pkg" "ai_scheduler/internal/pkg/dingtalk" "ai_scheduler/internal/pkg/util" - "ai_scheduler/internal/pkg/utils_ollama" "ai_scheduler/internal/tool_callback" "context" "encoding/json" "fmt" - "io" "net/url" "strings" "time" "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card" "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot" - "github.com/alibabacloud-go/dingtalk/card_1_0" - "github.com/alibabacloud-go/tea/tea" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/log" - "github.com/ollama/ollama/api" ) // CallbackService 统一回调入口 @@ -43,8 +36,9 @@ type CallbackService struct { dingtalkCardClient *dingtalk.CardClient callbackManager callback.Manager dingTalkBotBiz *biz.DingTalkBotBiz - ollamaClient *utils_ollama.Client - botConfigImpl *impl.BotConfigImpl + callbackBiz *biz.CallbackBiz + // ollamaClient *utils_ollama.Client + // botConfigImpl *impl.BotConfigImpl } func NewCallbackService( @@ -56,8 +50,9 @@ func NewCallbackService( dingtalkCardClient *dingtalk.CardClient, callbackManager callback.Manager, dingTalkBotBiz *biz.DingTalkBotBiz, - ollamaClient *utils_ollama.Client, - botConfigImpl *impl.BotConfigImpl, + callbackBiz *biz.CallbackBiz, + // ollamaClient *utils_ollama.Client, + // botConfigImpl *impl.BotConfigImpl, ) *CallbackService { return &CallbackService{ cfg: cfg, @@ -68,8 +63,9 @@ func NewCallbackService( dingtalkCardClient: dingtalkCardClient, callbackManager: callbackManager, dingTalkBotBiz: dingTalkBotBiz, - ollamaClient: ollamaClient, - botConfigImpl: botConfigImpl, + callbackBiz: callbackBiz, + // ollamaClient: ollamaClient, + // botConfigImpl: botConfigImpl, } } @@ -411,9 +407,10 @@ func (s *CallbackService) CallbackDingtalkRobot(c *fiber.Ctx) (err error) { switch data.RobotCode { case s.cfg.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling: // 问题处理群机器人 - err := s.issueHandling(c, data) + // err := s.issueHandling(data) + err := s.callbackBiz.IssueHandlingGroup(data) if err != nil { - return fmt.Errorf("issueHandling failed: %v", err) + return fmt.Errorf("IssueHandlingGroup failed: %v", err) } default: // 其他机器人 @@ -423,190 +420,6 @@ func (s *CallbackService) CallbackDingtalkRobot(c *fiber.Ctx) (err error) { return nil } -// issueHandling 问题处理群机器人回调 -// 能力1: 通过[内容提取] 宏,分析用户QA问题,调出QA表单卡片 -// 能力2: 通过[QA收集] 宏,收集用户反馈,写入知识库 -// 能力3: 通过[知识库查询] 宏,查询知识库,返回答案 -func (s *CallbackService) issueHandling(c *fiber.Ctx, data chatbot.BotCallbackDataModel) error { - // 能力1、2:分析用户QA问题,写入知识库 - if strings.Contains(data.Text.Content, "[内容提取]") || strings.Contains(data.Text.Content, "[QA收集]") { - s.issueHandlingExtractContent(data) - } - // 能力3:查询知识库,返回答案 - if strings.Contains(data.Text.Content, "[知识库查询]") { - s.issueHandlingQueryKnowledgeBase(data) - } - - return nil -} - -// 问题处理群机器人内容提取 -func (s *CallbackService) issueHandlingExtractContent(data chatbot.BotCallbackDataModel) { - // 1.提取用户输入 - prompt := fmt.Sprintf(constants.IssueHandlingExtractContentPrompt, data.Text.Content) - log.Infof("问题提取提示词: %s", prompt) - // LLM 提取 - generateResp, err := s.ollamaClient.Generation(context.Background(), &api.GenerateRequest{ - Model: s.cfg.Ollama.GenerateModel, - Prompt: prompt, - Stream: util.AnyToPoint(false), - }) - if err != nil { - log.Errorf("问题提取失败: %v", err) - return - } - // 解析 JSON 响应 - var resp struct { - Items []struct { - Question string `json:"question"` - Answer string `json:"answer"` - Confidence string `json:"confidence"` - } `json:"items"` - } - if err := json.Unmarshal([]byte(generateResp.Response), &resp); err != nil { - log.Errorf("解析 JSON 响应失败: %v", err) - return - } - - // 2.构建文本域内容 - cardContentTpl := "问题:%s \n答案:%s" - var cardContentList []string - for _, item := range resp.Items { - cardContentList = append(cardContentList, fmt.Sprintf(cardContentTpl, item.Question, item.Answer)) - } - cardContent := strings.Join(cardContentList, "\n\n") - - // 3.获取应用AppKey - appKey, err := s.botConfigImpl.GetRobotAppKey(data.RobotCode) - if err != nil { - log.Errorf("获取应用配置失败: %v", err) - return - } - - // 4.创建并投放卡片 - outTrackId := constants.BuildCardOutTrackId(data.ConversationId, data.RobotCode) // 构建卡片 OutTrackId - _, err = s.dingtalkCardClient.CreateAndDeliver( - appKey, - &card_1_0.CreateAndDeliverRequest{ - CardTemplateId: tea.String(s.cfg.Dingtalk.Card.Template.ContentCollect), - OutTrackId: tea.String(outTrackId), - CallbackType: tea.String("HTTP"), - CallbackRouteKey: tea.String(s.cfg.Dingtalk.Card.CallbackRouteKey), - CardData: &card_1_0.CreateAndDeliverRequestCardData{ - CardParamMap: map[string]*string{ - "title": tea.String("QA知识收集"), - "button_display": tea.String("true"), - "QA_details_now": tea.String(cardContent), - "textarea_display": tea.String("normal"), - "action_id": tea.String("collect_qa"), - "tenant_id": tea.String(constants.KnowledgeTenantIdDefault), - "_CARD_DEBUG_TOOL_ENTRY": tea.String(s.cfg.Dingtalk.Card.DebugToolEntryShow), // 调试字段 - }, - }, - ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{ - SupportForward: tea.Bool(false), - }, - OpenSpaceId: tea.String("dtv1.card//im_group." + data.ConversationId), - ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{ - RobotCode: tea.String(s.cfg.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling), - }, - }, - ) - -} - -// 问题处理群机器人查询知识库 -func (s *CallbackService) issueHandlingQueryKnowledgeBase(data chatbot.BotCallbackDataModel) { - // // 获取应用主机器人 - // mainRobotCode := data.RobotCode - // if robotCode, ok := constants.GroupTemplateRobotIdMap[data.RobotCode]; ok { - // mainRobotCode = robotCode - // } - - // 获取应用配置 - appKey, err := s.botConfigImpl.GetRobotAppKey(data.RobotCode) - if err != nil { - log.Errorf("应用机器人配置不存在: %s, err: %v", data.RobotCode, err) - return - } - // 创建卡片 - outTrackId := constants.BuildCardOutTrackId(data.ConversationId, data.RobotCode) - _, err = s.dingtalkCardClient.CreateAndDeliver( - appKey, - &card_1_0.CreateAndDeliverRequest{ - CardTemplateId: tea.String(s.cfg.Dingtalk.Card.Template.BaseMsg), - CardData: &card_1_0.CreateAndDeliverRequestCardData{ - CardParamMap: map[string]*string{ - "title": tea.String(data.Text.Content), - "markdown": tea.String("知识库检索中..."), - }, - }, - OutTrackId: tea.String(outTrackId), - ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{ - SupportForward: tea.Bool(false), - }, - OpenSpaceId: tea.String("dtv1.card//im_group." + data.ConversationId), - ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{ - RobotCode: tea.String(data.RobotCode), - }, - }, - ) - - // 查询知识库 - knowledgeBase := knowledge_base.New(s.cfg.KnowledgeConfig) - knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{ - TenantID: constants.KnowledgeTenantIdDefault, - Query: data.Text.Content, - Mode: constants.KnowledgeModeMix, - Stream: false, - Think: false, - OnlyRAG: true, - }) - if err != nil { - log.Errorf("查询知识库失败: %v", err) - return - } - knowledgeRespBytes, err := io.ReadAll(knowledgeResp) - if err != nil { - log.Errorf("读取知识库响应失败: %v", err) - return - } - - // 卡片更新 - message, isRetrieved, err := knowledge_base.ParseOpenAIHTTPData(string(knowledgeRespBytes)) - if err != nil { - log.Errorf("读取知识库 SSE 数据失败: %v", err) - return - } - - content := message.Content - if !isRetrieved { - content = "知识库未检测到匹配信息,请核查知识库数据是否正确。" - } - - // 卡片更新 - _, err = s.dingtalkCardClient.UpdateCard( - appKey, - &card_1_0.UpdateCardRequest{ - OutTrackId: tea.String(outTrackId), - CardData: &card_1_0.UpdateCardRequestCardData{ - CardParamMap: map[string]*string{ - "markdown": tea.String(content), - }, - }, - CardUpdateOptions: &card_1_0.UpdateCardRequestCardUpdateOptions{ - UpdateCardDataByKey: tea.Bool(true), - }, - }, - ) - if err != nil { - log.Errorf("更新卡片失败: %v", err) - return - } - - return -} - // CallbackDingtalkCard 处理钉钉卡片回调 // 钉钉 callbackRouteKey: gateway.dev.cdlsxd.cn-dingtalk-card // 钉钉 apiSecret: aB3dE7fG9hI2jK4L5M6N7O8P9Q0R1S2T @@ -640,7 +453,7 @@ func (s *CallbackService) CallbackDingtalkCard(c *fiber.Ctx) error { switch actionId { case "collect_qa": // 问题处理群机器人 QA 收集 - resp = s.issueHandlingCollectQA(data) + resp = s.callbackBiz.IssueHandlingCollectQA(data) } } @@ -648,79 +461,3 @@ func (s *CallbackService) CallbackDingtalkCard(c *fiber.Ctx) error { c.Locals("skip_response_wrap", true) return c.JSON(resp) } - -// 问题处理群机器人 QA 收集 -func (s *CallbackService) issueHandlingCollectQA(data card.CardRequest) *card.CardResponse { - // 确认提交,文本写入知识库 - if data.CardActionData.CardPrivateData.Params["submit"] == "submit" { - content := data.CardActionData.CardPrivateData.Params["QA_details"].(string) - tenantID := data.CardActionData.CardPrivateData.Params["tenant_id"].(string) - - // 协程执行耗时操作,防止阻塞 - util.SafeGo("inject_knowledge_base", func() { - knowledgeBase := knowledge_base.New(s.cfg.KnowledgeConfig) - err := knowledgeBase.IngestText(&knowledge_base.IngestTextRequest{ - TenantID: tenantID, - Text: content, - }) - if err != nil { - log.Errorf("注入知识库失败: %v", err) - } else { - log.Infof("注入知识库成功: tenantID=%s", tenantID) - } - - // 解析当前卡片的 ConversationId 和 robotCode - conversationId, robotCode := constants.ParseCardOutTrackId(data.OutTrackId) - - // 获取主应用机器人(这里可能是群模板机器人) - // mainRobotId := robotCode - // if robotCode, ok := constants.GroupTemplateRobotIdMap[robotCode]; ok { - // mainRobotId = robotCode - // } - - // 获取应用配置 - appKey, err := s.botConfigImpl.GetRobotAppKey(robotCode) - if err != nil { - log.Errorf("获取应用机器人配置失败: %v", err) - return - } - - // 发送卡片通知用户注入成功 - outTrackId := constants.BuildCardOutTrackId(conversationId, robotCode) - s.dingtalkCardClient.CreateAndDeliver( - appKey, - &card_1_0.CreateAndDeliverRequest{ - CardTemplateId: tea.String(s.cfg.Dingtalk.Card.Template.BaseMsg), - OutTrackId: tea.String(outTrackId), - CardData: &card_1_0.CreateAndDeliverRequestCardData{ - CardParamMap: map[string]*string{ - "title": tea.String("QA知识收集结果"), - "markdown": tea.String("[Get] **成功**"), - }, - }, - ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{ - SupportForward: tea.Bool(false), - }, - OpenSpaceId: tea.String("dtv1.card//im_group." + conversationId), - ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{ - RobotCode: tea.String(robotCode), - }, - }, - ) - }) - } - - // 取消提交,禁用输入框 - resp := &card.CardResponse{ - CardUpdateOptions: &card.CardUpdateOptions{ - UpdateCardDataByKey: true, - }, - CardData: &card.CardDataDto{ - CardParamMap: map[string]string{ - "textarea_display": "disabled", - }, - }, - } - - return resp -} diff --git a/internal/services/dtalk_bot.go b/internal/services/dtalk_bot.go index 3fcc186..7245f18 100644 --- a/internal/services/dtalk_bot.go +++ b/internal/services/dtalk_bot.go @@ -4,57 +4,29 @@ import ( "ai_scheduler/internal/biz" "ai_scheduler/internal/config" "ai_scheduler/internal/data/constants" - "ai_scheduler/internal/data/impl" - "ai_scheduler/internal/data/model" "ai_scheduler/internal/entitys" - "ai_scheduler/internal/pkg/dingtalk" "context" - "encoding/json" "log" "sync" "time" "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card" "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot" - "github.com/alibabacloud-go/dingtalk/card_1_0" - "github.com/alibabacloud-go/tea/tea" "golang.org/x/sync/errgroup" - "xorm.io/builder" ) type DingBotService struct { - config *config.Config - dingTalkBotBiz *biz.DingTalkBotBiz - dingTalkOld *dingtalk.OldClient - dingtalkCardClient *dingtalk.CardClient - dingtalkImClient *dingtalk.ImClient - dingtalkOauth2Client *dingtalk.Oauth2Client - botGroupConfigImpl *impl.BotGroupConfigImpl - botGroupImpl *impl.BotGroupImpl - botConfigImpl *impl.BotConfigImpl + config *config.Config + dingTalkBotBiz *biz.DingTalkBotBiz } func NewDingBotService( config *config.Config, dingTalkBotBiz *biz.DingTalkBotBiz, - dingTalkOld *dingtalk.OldClient, - dingtalkCardClient *dingtalk.CardClient, - dingtalkImClient *dingtalk.ImClient, - dingtalkOauth2Client *dingtalk.Oauth2Client, - botGroupConfigImpl *impl.BotGroupConfigImpl, - botGroupImpl *impl.BotGroupImpl, - botConfigImpl *impl.BotConfigImpl, ) *DingBotService { return &DingBotService{ - config: config, - dingTalkBotBiz: dingTalkBotBiz, - dingTalkOld: dingTalkOld, - dingtalkCardClient: dingtalkCardClient, - dingtalkImClient: dingtalkImClient, - dingtalkOauth2Client: dingtalkOauth2Client, - botGroupConfigImpl: botGroupConfigImpl, - botGroupImpl: botGroupImpl, - botConfigImpl: botConfigImpl, + config: config, + dingTalkBotBiz: dingTalkBotBiz, } } @@ -174,7 +146,7 @@ func (d *DingBotService) runBackgroundTasks(ctx context.Context, data *chatbot.B return nil } -// **一把梭先搞,后续规范化** +// OnCardMessageReceived 处理卡片回调 func (d *DingBotService) OnCardMessageReceived(ctx context.Context, data *card.CardRequest) (resp *card.CardResponse, err error) { // 非回调类型暂不接受 if data.Type != constants.CardActionCallbackTypeAction { @@ -185,206 +157,14 @@ func (d *DingBotService) OnCardMessageReceived(ctx context.Context, data *card.C for _, actionId := range data.CardActionData.CardPrivateData.ActionIdList { switch actionId { case constants.CardActionTypeCreateGroup: - // 解析 OutTrackId 以获取 SpaceId 和 BotId - spaceId, botId := constants.ParseCardOutTrackId(data.OutTrackId) - - // 获取新群聊人员 - var userIds []string - userIds, err = d.buildNewGroupUserIds(ctx, spaceId, botId, data.UserId) + resp, err = d.dingTalkBotBiz.CreateIssueHandlingGroupAndInit(ctx, data) if err != nil { return nil, err } - // 创建群聊及群初始化(ws中,直接协程) - if data.CardActionData.CardPrivateData.Params["status"] == "confirm" { - go func() { - err := d.createIssueHandlingGroupAndInit(ctx, data.CardActionData.CardPrivateData.Params, spaceId, botId, userIds) - if err != nil { - log.Printf("创建群聊及群初始化失败: %v", err) - } - }() - } - - // 构建关闭创建群组卡片按钮的响应 - resp = d.buildCreateGroupCardResp() return } } return &card.CardResponse{}, nil } - -// buildNewGroupUserIds 构建新群聊人员列表 -func (d *DingBotService) buildNewGroupUserIds(ctx context.Context, spaceId, botId, groupOwner string) ([]string, error) { - // 群id+机器人id确认一个群配置 - botGroup, err := d.botGroupImpl.GetByConversationIdAndRobotCode(spaceId, botId) - if err != nil { - return nil, err - } - - // 获取群配置 - var groupConfig model.AiBotGroupConfig - cond := builder.NewCond().And(builder.Eq{"config_id": botGroup.ConfigID}) - err = d.botGroupConfigImpl.GetOneBySearchToStrut(&cond, &groupConfig) - if err != nil { - return nil, err - } - - // 获取处理人列表 - issueOwnerJson := groupConfig.IssueOwner - type issueOwnerType struct { - UserId string `json:"userid"` - Name string `json:"name"` - } - var issueOwner []issueOwnerType - if err = json.Unmarshal([]byte(issueOwnerJson), &issueOwner); err != nil { - return nil, err - } - - // 合并所有userid - userIds := []string{groupOwner} // 当前用户为群主 - for _, owner := range issueOwner { - userIds = append(userIds, owner.UserId) - } - - return userIds, nil -} - -// createIssueHandlingGroupAndInit 创建问题处理群聊及群初始化 -func (d *DingBotService) createIssueHandlingGroupAndInit(ctx context.Context, callbackParams map[string]any, spaceId, botId string, userIds []string) error { - // 获取机器人配置 - botConfig, err := d.botConfigImpl.GetRobotConfig(botId) - if err != nil { - return err - } - - appKey := dingtalk.AppKey{ - AppKey: botConfig.ClientId, - AppSecret: botConfig.ClientSecret, - } - - // 获取 access_token - accessToken, err := d.dingtalkOauth2Client.GetAccessToken(appKey) - if err != nil { - return err - } - appKey.AccessToken = accessToken - - // 创建群聊 - _, openConversationId, err := d.createIssueHandlingGroup(ctx, accessToken, spaceId, botId, userIds) - if err != nil { - return err - } - - // 添加当前机器人到新群 - SDK 有问题,后续再考虑使用 - // _, err = d.dingtalkImClient.AddRobotToConversation( - // appKey, - // &im_1_0.AddRobotToConversationRequest{ - // OpenConversationId: tea.String(openConversationId), - // RobotCode: tea.String(botId), - // }) - // if err != nil { - // fmt.Printf("添加机器人到会话失败: %v", err) - // } - - // 返回新群分享链接,直接进群 - SDK 有问题,后续再考虑使用 - // newGroupShareLink, err = d.dingTalkOld.GetJoinGroupQrcode(ctx, chatId, data.UserId) - // if err != nil { - // fmt.Printf("获取入群二维码失败: %v", err) - // } - - // 初始化群聊 - // 1.开场白 - - // 群主题 - groupScope := callbackParams["group_scope"].(string) - // 构建卡片 OutTrackId - outTrackId := constants.BuildCardOutTrackId(openConversationId, d.config.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling) - _, err = d.dingtalkCardClient.CreateAndDeliver( - appKey, - &card_1_0.CreateAndDeliverRequest{ - CardTemplateId: tea.String(d.config.Dingtalk.Card.Template.BaseMsg), - OutTrackId: tea.String(outTrackId), - CallbackType: tea.String("HTTP"), - CardData: &card_1_0.CreateAndDeliverRequestCardData{ - CardParamMap: map[string]*string{ - "title": tea.String("当前会话主题"), - "markdown": tea.String("问题:" + groupScope), - }, - }, - ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{ - SupportForward: tea.Bool(false), - }, - OpenSpaceId: tea.String("dtv1.card//im_group." + openConversationId), - ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{ - RobotCode: tea.String(d.config.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling), - AtUserIds: map[string]*string{ - "@ALL": tea.String("@ALL"), - }, - }, - }, - ) - - // 2. 机器人能力 - // 构建卡片 OutTrackId - outTrackId = constants.BuildCardOutTrackId(openConversationId, d.config.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling) - _, err = d.dingtalkCardClient.CreateAndDeliver( - appKey, - &card_1_0.CreateAndDeliverRequest{ - CardTemplateId: tea.String(d.config.Dingtalk.Card.Template.BaseMsg), - OutTrackId: tea.String(outTrackId), - CallbackType: tea.String("HTTP"), - CardData: &card_1_0.CreateAndDeliverRequestCardData{ - CardParamMap: map[string]*string{ - "title": tea.String("当前机器人能力"), - "markdown": tea.String("- 聊天内容提取(@机器人 [内容提取]{聊天记录/问答描述}) \n - QA知识收集(卡片信息收集) \n - QA知识问答(@机器人 [知识库查询]{问题描述})"), - }, - }, - ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{ - SupportForward: tea.Bool(false), - }, - OpenSpaceId: tea.String("dtv1.card//im_group." + openConversationId), - ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{ - RobotCode: tea.String(d.config.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling), - AtUserIds: map[string]*string{ - "@ALL": tea.String("@ALL"), - }, - }, - }, - ) - - return nil -} - -// createGroupV1 创建普通内部群会话 -// 这里用的是“统一登录平台”这个应用的接口加入群聊 - 这里用的是“统一登录平台”这个应用的接口 -func (d *DingBotService) createIssueHandlingGroup(ctx context.Context, accessToken, spaceId, botId string, userIds []string) (chatId, openConversationId string, err error) { - // 是否使用模板群开关 - var useTemplateGroup bool = true - - // 创建内部群会话 - if !useTemplateGroup { - return d.dingTalkOld.CreateInternalGroupConversation(ctx, accessToken, "问题处理群", userIds) - } - - // 根据群模板ID创建群 - if useTemplateGroup { - return d.dingTalkOld.CreateSceneGroupConversation(ctx, accessToken, "问题处理群", userIds, d.config.Dingtalk.SceneGroup.GroupTemplateIDIssueHandling) - } - - return -} - -// buildCreateGroupCardResp 构建关闭创建群组卡片按钮 -func (d *DingBotService) buildCreateGroupCardResp() *card.CardResponse { - return &card.CardResponse{ - CardData: &card.CardDataDto{ - CardParamMap: map[string]string{ - "button_display": "false", - }, - }, - CardUpdateOptions: &card.CardUpdateOptions{ - UpdateCardDataByKey: true, - }, - } -} diff --git a/internal/services/dtalk_bot_test.go b/internal/services/dtalk_bot_test.go index 989cba2..e7b2041 100644 --- a/internal/services/dtalk_bot_test.go +++ b/internal/services/dtalk_bot_test.go @@ -126,7 +126,7 @@ func run() { qywxAppBiz := biz.NewQywxAppBiz(configConfig, botGroupQywxImpl, group, other) groupConfigBiz := biz.NewGroupConfigBiz(toolRegis, utils_ossClient, botGroupConfigImpl, botConfigImpl, registry, configConfig, impl.NewReportDailyCacheImpl(db), rdb, manager, cardClient) macro := do.NewMacro(botGroupImpl, impl.NewReportDailyCacheImpl(db)) - dingTalkBotBiz := biz.NewDingTalkBotBiz(doDo, handle, botConfigImpl, botGroupImpl, user, botChatHisImpl, impl.NewReportDailyCacheImpl(db), manager, configConfig, sendCardClient, groupConfigBiz, macro) + dingTalkBotBiz := biz.NewDingTalkBotBiz(doDo, handle, botConfigImpl, botGroupImpl, user, botChatHisImpl, impl.NewReportDailyCacheImpl(db), manager, configConfig, sendCardClient, groupConfigBiz, macro, oauth2Client, oldClient, cardClient) // 初始化钉钉机器人服务 cronService = NewCronService(configConfig, dingTalkBotBiz, qywxAppBiz, groupConfigBiz) }