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 }