package biz import ( "ai_scheduler/internal/biz/do" "ai_scheduler/internal/biz/handle/dingtalk" "ai_scheduler/internal/biz/handle/qywx" "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" "ai_scheduler/internal/tools" "ai_scheduler/internal/tools/bbxt" "ai_scheduler/tmpl/dataTemp" "ai_scheduler/utils" "context" "database/sql" "encoding/json" "errors" "fmt" "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" "ai_scheduler/internal/pkg/util" "github.com/alibabacloud-go/dingtalk/card_1_0" "github.com/alibabacloud-go/tea/tea" "github.com/duke-git/lancet/v2/slice" "github.com/gofiber/fiber/v2/log" "github.com/redis/go-redis/v9" "xorm.io/builder" ) // AiRouterBiz 智能路由服务 type DingTalkBotBiz struct { do *do.Do handle *do.Handle botConfigImpl *impl.BotConfigImpl replier *chatbot.ChatbotReplier log log.Logger dingTalkUser *dingtalk.User botGroupImpl *impl.BotGroupImpl botGroupConfigImpl *impl.BotGroupConfigImpl botGroupQywxImpl *impl.BotGroupQywxImpl toolManager *tools.Manager chatHis *impl.BotChatHisImpl botUserImpl *impl.BotUserImpl conf *config.Config cardSend *dingtalk.SendCardClient qywxGroupHandle *qywx.Group groupConfigBiz *GroupConfigBiz reportDailyCacheImpl *impl.ReportDailyCacheImpl macro *do.Macro dingtalkOauth2Client *dingtalkPkg.Oauth2Client dingTalkOld *dingtalkPkg.OldClient dingtalkCardClient *dingtalkPkg.CardClient redisCli *redis.Client issueImpl *impl.IssueImpl sysImpl *impl.SysImpl } // NewDingTalkBotBiz func NewDingTalkBotBiz( do *do.Do, handle *do.Handle, botConfigImpl *impl.BotConfigImpl, botGroupImpl *impl.BotGroupImpl, botGroupConfigImpl *impl.BotGroupConfigImpl, dingTalkUser *dingtalk.User, chatHis *impl.BotChatHisImpl, botUserImpl *impl.BotUserImpl, reportDailyCacheImpl *impl.ReportDailyCacheImpl, toolManager *tools.Manager, conf *config.Config, cardSend *dingtalk.SendCardClient, groupConfigBiz *GroupConfigBiz, macro *do.Macro, dingtalkOauth2Client *dingtalkPkg.Oauth2Client, dingTalkOld *dingtalkPkg.OldClient, dingtalkCardClient *dingtalkPkg.CardClient, rdb *utils.Rdb, issueImpl *impl.IssueImpl, sysImpl *impl.SysImpl, ) *DingTalkBotBiz { return &DingTalkBotBiz{ do: do, handle: handle, botConfigImpl: botConfigImpl, replier: chatbot.NewChatbotReplier(), dingTalkUser: dingTalkUser, groupConfigBiz: groupConfigBiz, botGroupImpl: botGroupImpl, botGroupConfigImpl: botGroupConfigImpl, toolManager: toolManager, chatHis: chatHis, botUserImpl: botUserImpl, conf: conf, cardSend: cardSend, reportDailyCacheImpl: reportDailyCacheImpl, macro: macro, dingtalkOauth2Client: dingtalkOauth2Client, dingTalkOld: dingTalkOld, dingtalkCardClient: dingtalkCardClient, redisCli: rdb.Rdb, issueImpl: issueImpl, sysImpl: sysImpl, } } func (d *DingTalkBotBiz) GetDingTalkBotCfgList() (dingBotList []entitys.DingTalkBot, err error) { botConfig := make([]model.AiBotConfig, 0) cond := builder.NewCond() cond = cond.And(builder.Eq{"status": constants.Enable}) cond = cond.And(builder.Eq{"bot_type": constants.BotTypeDingTalk}) err = d.botConfigImpl.GetRangeToMapStruct(&cond, &botConfig) for _, v := range botConfig { var config entitys.DingTalkBot err = json.Unmarshal([]byte(v.BotConfig), &config) if err != nil { d.log.Info("初始化“%s”失败:%s", v.BotName, err.Error()) } config.BotIndex = v.RobotCode dingBotList = append(dingBotList, config) } return } func (d *DingTalkBotBiz) InitRequire(ctx context.Context, data *chatbot.BotCallbackDataModel) (requireData *entitys.RequireDataDingTalkBot, err error) { requireData = &entitys.RequireDataDingTalkBot{ Req: data, Ch: make(chan entitys.Response, 2), } return } func (d *DingTalkBotBiz) Do(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) { //entitys.ResLoading(requireData.Ch, "", "收到消息,正在处理中,请稍等") //defer close(requireData.Ch) switch constants.ConversationType(requireData.Req.ConversationType) { case constants.ConversationTypeSingle: err = d.handleSingleChat(ctx, requireData) case constants.ConversationTypeGroup: err = d.handleGroupChat(ctx, requireData) default: err = errors.New("未知的聊天类型:" + requireData.Req.ConversationType) } if err != nil { entitys.ResText(requireData.Ch, "", err.Error()) } return } // handleSingleChat 单聊处理 // 先不接意图识别-仅提供问题处理 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)) if err != nil { return } requireData.ID = int32(requireData.UserInfo.UserId) // 2. 检查会话状态 (Redis) 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) } // 3. 获取历史记录 (最近6轮用户输入) userHist, err := d.getRecentUserHistory(ctx, constants.ConversationTypeSingle, requireData.ID, 6) if err != nil { return err } // 4. 改写 Query (Query Rewriting) rewrittenQuery, err := d.handle.RewriteQuery(ctx, userHist, requireData.Req.Text.Content) var queryText = requireData.Req.Text.Content if err == nil && rewrittenQuery != "" { 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) 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 } // 2. 既然已经明确了系统,直接尝试拉群(这里假设问题类型为“其他”或由LLM再次分析) // 为简化,这里再次调用分类逻辑,但带上已确定的系统 return d.fallbackToGroupCreationWithSys(ctx, requireData, &sys) } // 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 } // fallbackToGroupCreation 分类并拉群 func (d *DingTalkBotBiz) fallbackToGroupCreation(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) error { // 1. 获取所有系统和问题类型用于分类 allSys, err := d.sysImpl.FindAll() if err != nil { return 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 } issueTypeNames := slice.Map(allIssueTypes, func(_ int, it model.AiIssueType) string { return it.Name }) // 2. LLM 分类 classification, err := d.handle.ClassifyIssue(ctx, sysNames, issueTypeNames, requireData.Req.Text.Content) if err != nil { // 分类失败,使用兜底 return d.createDefaultGroup(ctx, requireData, "系统无法识别") } // 3. 匹配系统 var sys model.AiSy for _, s := range allSys { if s.SysName == classification.SysName { sys = s break } } // 4. 匹配问题类型 var issueType model.AiIssueType for _, it := range allIssueTypes { if it.Name == classification.IssueTypeName { issueType = it break } } 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) } // 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 }) // 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) } // 3. 查找分配规则 rule, found, err := d.issueImpl.IssueAssignRule.FindOne( d.issueImpl.WithSysID(sys.SysID), d.issueImpl.WithIssueTypeID(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) } } var staffIds []string if rule.ID != 0 { // 获取配置的用户 assignUsers, err := d.issueImpl.IssueAssignUser.FindAll(d.issueImpl.WithRuleID(rule.ID)) if len(assignUsers) == 0 { log.Errorf("assign user not found for rule %d; err: %v", rule.ID, err) return fmt.Errorf("分配用户 %d 不存在", rule.ID) } userIds := slice.Map(assignUsers, func(_ int, au model.AiIssueAssignUser) int32 { return au.UserID }) // 获取有效用户 botUsers, err := d.botUserImpl.GetByUserIds(userIds) if err != nil { return err } // 仅获取有效用户的 staff_id for _, au := range assignUsers { botUser, found := slice.Find(botUsers, func(_ int, bu model.AiBotUser) bool { return bu.UserID == au.UserID }) if found && botUser.StaffID != "" { staffIds = append(staffIds, botUser.StaffID) } } } // 兜底处理人 if len(staffIds) == 0 { staffIds = []string{"17415698414368678"} } // 合并提问者 staffIds = append([]string{requireData.Req.SenderStaffId}, staffIds...) // 4. 发送确认卡片 groupName := fmt.Sprintf("[%s]-%s", classification.IssueTypeName, 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, }) } // createDefaultGroup 兜底发送确认卡片 func (d *DingTalkBotBiz) createDefaultGroup(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, reason string) error { userIds := []string{requireData.Req.SenderStaffId, "17415698414368678"} groupName := fmt.Sprintf("[未知]-%s", reason) return d.SendGroupCreationConfirmCard(ctx, &SendGroupCreationConfirmCardParams{ RobotCode: requireData.Req.RobotCode, ConversationId: requireData.Req.ConversationId, SenderStaffId: requireData.Req.SenderStaffId, UserIds: userIds, GroupName: groupName, Summary: reason, }) } func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) { group, err := d.initGroup(ctx, requireData.Req.ConversationId, requireData.Req.ConversationTitle, requireData.Req.RobotCode) if err != nil { return } groupConfig, err := d.groupConfigBiz.GetGroupConfig(ctx, group.ConfigID) if err != nil { return } //宏 sucMsg, err, isFinal := d.macro.Router(ctx, requireData.Req.Text.Content, groupConfig) if err != nil { entitys.ResText(requireData.Ch, "", err.Error()) return } if len(sucMsg) > 0 { entitys.ResText(requireData.Ch, "", sucMsg) } if isFinal { return } requireData.ID = group.GroupID groupTools, err := d.groupConfigBiz.getGroupTools(ctx, groupConfig) if err != nil { return } rec, err := d.recognize(ctx, requireData, groupTools) if err != nil { return } return d.groupConfigBiz.handleMatch(ctx, rec, groupConfig, requireData.Req) } func (d *DingTalkBotBiz) initGroup(ctx context.Context, conversationId string, conversationTitle string, robotCode string) (group *model.AiBotGroup, err error) { group, err = d.botGroupImpl.GetByConversationIdAndRobotCode(conversationId, robotCode) if err != nil { if !errors.Is(err, sql.ErrNoRows) { return } } if group.GroupID == 0 { group = &model.AiBotGroup{ ConversationID: conversationId, Title: conversationTitle, RobotCode: robotCode, } //如果不存在则创建 _, err = d.botGroupImpl.Add(group) } return } func (d *DingTalkBotBiz) recognize(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, tools []model.AiBotTool) (rec *entitys.Recognize, err error) { userContent, err := d.getUserContent(requireData.Req.Msgtype, requireData.Req.Text.Content) if err != nil { return } rec = &entitys.Recognize{ Ch: requireData.Ch, SystemPrompt: d.defaultPrompt(), UserContent: userContent, } //历史记录 rec.ChatHis, err = d.getHis(ctx, constants.ConversationType(requireData.Req.ConversationType), requireData.ID) if err != nil { return } //工具注册 if len(tools) > 0 { rec.Tasks = make([]entitys.RegistrationTask, 0, len(tools)) for _, task := range tools { taskConfig := entitys.TaskConfigDetail{} if err = json.Unmarshal([]byte(task.Config), &taskConfig); err != nil { log.Errorf("解析任务配置失败: %s, 任务ID: %s", err.Error(), task.Index) continue // 解析失败时跳过该任务,而不是直接返回错误 } rec.Tasks = append(rec.Tasks, entitys.RegistrationTask{ Name: task.Index, Desc: task.TempPrompt, TaskConfigDetail: taskConfig, // 直接使用解析后的配置,避免重复构建 }) } } rec.Ext = pkg.JsonByteIgonErr(&entitys.TaskExt{ UserName: requireData.Req.SenderNick, }) err = d.handle.Recognize(ctx, rec, &do.WithDingTalkBot{}) return } func (d *DingTalkBotBiz) getHis(ctx context.Context, conversationType constants.ConversationType, Id int32) (content entitys.ChatHis, err error) { var ( his []model.AiBotChatHi ) cond := builder.NewCond() cond = cond.And(builder.Eq{"his_type": conversationType}) cond = cond.And(builder.Eq{"id": Id}) _, err = d.chatHis.GetListToStruct(&cond, &dataTemp.ReqPageBo{Limit: d.conf.Sys.SessionLen}, &his, "his_id desc") if err != nil { return } messages := make([]entitys.HisMessage, 0, len(his)) for _, v := range his { if v.Role != "user" { continue } messages = append(messages, entitys.HisMessage{ Role: constants.Caller(v.Role), // 用户角色 Content: v.Content, // 用户输入内容 Timestamp: v.CreateAt.Format(time.DateTime), }) } return entitys.ChatHis{ SessionId: fmt.Sprintf("%s_%d", conversationType, Id), Messages: messages, Context: entitys.HisContext{ UserLanguage: constants.LangZhCN, // 默认中文 SystemMode: constants.SystemModeTechnicalSupport, // 默认技术支持模式 }, }, nil } func (d *DingTalkBotBiz) getUserContent(msgType string, msgContent interface{}) (content *entitys.RecognizeUserContent, err error) { switch constants.BotMsgType(msgType) { case constants.BotMsgTypeText: content = &entitys.RecognizeUserContent{ Text: msgContent.(string), } default: return nil, errors.New("未知的消息类型:" + msgType) } return } func (d *DingTalkBotBiz) HandleStreamRes(ctx context.Context, data *chatbot.BotCallbackDataModel, content chan string) (err error) { err = d.cardSend.NewCard(ctx, &dingtalk.CardSend{ RobotCode: data.RobotCode, ConversationType: constants.ConversationType(data.ConversationType), Template: constants.CardTempDefault, ContentChannel: content, // 指定内容通道 ConversationId: data.ConversationId, SenderStaffId: data.SenderStaffId, Title: data.Text.Content, }) return } func (d *DingTalkBotBiz) SendReport(ctx context.Context, groupInfo *model.AiBotGroup, report *bbxt.ReportRes) (err error) { if report == nil { return errors.New("report is nil") } reportChan := make(chan string, 10) defer close(reportChan) reportChan <- report.Title reportChan <- fmt.Sprintf("![图片](%s)", report.Url) err = d.HandleStreamRes(ctx, &chatbot.BotCallbackDataModel{ RobotCode: groupInfo.RobotCode, ConversationType: constants.ConversationTypeGroup, ConversationId: groupInfo.ConversationID, Text: chatbot.BotCallbackDataTextModel{ Content: report.ReportName, }, }, reportChan) return } func (d *DingTalkBotBiz) GetGroupInfo(ctx context.Context, groupId int) (group model.AiBotGroup, err error) { cond := builder.NewCond() cond = cond.And(builder.Eq{"group_id": groupId}) cond = cond.And(builder.Eq{"status": constants.Enable}) err = d.botGroupImpl.GetOneBySearchToStrut(&cond, &group) return } func (d *DingTalkBotBiz) ReplyText(ctx context.Context, SessionWebhook string, content string, arg ...string) error { msg := content if len(arg) > 0 { msg = fmt.Sprintf(content, arg) } return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg)) } func (d *DingTalkBotBiz) replyImg(ctx context.Context, SessionWebhook string, content string, arg ...string) error { msg := content if len(arg) > 0 { msg = fmt.Sprintf(content, arg) } return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg)) } func (d *DingTalkBotBiz) replyFile(ctx context.Context, SessionWebhook string, content string, arg ...string) error { msg := content if len(arg) > 0 { msg = fmt.Sprintf(content, arg) } return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg)) } func (d *DingTalkBotBiz) replyMarkdown(ctx context.Context, SessionWebhook string, content string, arg ...string) error { msg := content if len(arg) > 0 { msg = fmt.Sprintf(content, arg) } return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg)) } func (d *DingTalkBotBiz) replyActionCard(ctx context.Context, SessionWebhook string, content string, arg ...string) error { msg := content if len(arg) > 0 { msg = fmt.Sprintf(content, arg) } return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg)) } func (d *DingTalkBotBiz) SaveHis(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, chat []string) (err error) { if len(chat) == 0 { return } his := []*model.AiBotChatHi{ { HisType: requireData.Req.ConversationType, ID: requireData.ID, Role: "user", Content: requireData.Req.Text.Content, }, { HisType: requireData.Req.ConversationType, ID: requireData.ID, Role: "system", Content: strings.Join(chat, "\n"), }, } _, err = d.chatHis.Add(his) return err } func (d *DingTalkBotBiz) defaultPrompt() string { now := time.Now().Format(time.DateTime) return `[system] 你是一个智能路由系统,核心职责是 **精准解析用户意图并路由至对应任务模块**。请严格遵循以下规则: [rule] 1. **返回格式**: 仅输出以下 **严格格式化的 JSON 字符串**(禁用 Markdown): { "index": "工具索引index", "confidence": 0.0-1.0,"reasoning": "判断理由","parameters":"jsonstring |提取参数","is_match":true||false,"chat": "追问内容"} 关键规则(按优先级排序): 2. **工具匹配**: - 若匹配到工具,使用工具的 parameters 作为模板做参数匹配 - 注意区分 parameters 中的 必须参数(required) 和 可选参数(optional),按下述参数提取规则处理。 - 若**完全无法匹配**,立即设置 is_match: false,并在 chat 中已第一人称的角度提醒用户需要适用何种工具(例:"请问您是要查询订单还是商品呢")。 3. **参数提取**: - 根据 parameters 字段列出的参数名,从用户输入中提取对应值。 - **仅提取**明确提及的参数,忽略未列出的内容。 - 必须参数仅使用用户直接提及的参数,不允许从上下文推断。 - 若必须参数缺失,立即设置 is_match: false,并在 chat 中已第一人称的角度提醒用户提供缺少的参数追问(例:"需要您补充XX信息")。 4. 格式强制要求: -所有字段值必须是**字符串**(包括 confidence)。 -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) // 获取操作状态 status := data.CardActionData.CardPrivateData.Params["status"] if status == "confirm" { // 获取新群聊人员 (从卡片参数中统一解析) targetUserIdsStr := data.CardActionData.CardPrivateData.Params["target_user_ids"].(string) var userIds []string if targetUserIdsStr != "" { userIds = strings.Split(targetUserIdsStr, ",") } if len(userIds) == 0 { return nil, errors.New("target_user_ids 参数不能为空") } // 创建群聊及群初始化(异步响应) util.SafeGo("CreateIssueHandlingGroupAndInit", func() { err := d.createIssueHandlingGroupAndInit(ctx, data.CardActionData.CardPrivateData.Params, spaceId, botId, userIds) if err != nil { log.Errorf("创建群聊及群初始化失败: %v", err) } }) } // 构建关闭创建群组卡片按钮的响应 return d.buildCreateGroupCardResp(), nil } type SendGroupCreationConfirmCardParams struct { RobotCode string ConversationId string SenderStaffId string UserIds []string GroupName string Summary string IsGroupChat bool } // SendGroupCreationConfirmCard 发送创建群聊确认卡片 func (d *DingTalkBotBiz) SendGroupCreationConfirmCard(ctx context.Context, params *SendGroupCreationConfirmCardParams) error { // 获取人员姓名用于展示 var userNames []string for _, uid := range params.UserIds { if uid == params.SenderStaffId { continue } user, err := d.botUserImpl.GetByStaffId(uid) if err == nil && user != nil { userNames = append(userNames, "@"+user.Name) } else { userNames = append(userNames, "@"+uid) } } issueOwnerStr := strings.Join(userNames, "、") // 获取应用配置 appKey, err := d.botConfigImpl.GetRobotAppKey(params.RobotCode) if err != nil { return err } // 构建卡片 OutTrackId outTrackId := constants.BuildCardOutTrackId(params.ConversationId, params.RobotCode) // 准备可见人员列表 var recipients []*string if params.IsGroupChat { // 群聊:提问者 + 负责人可见 for _, uid := range params.UserIds { recipients = append(recipients, tea.String(uid)) } // 确保提问者也在可见列表中 foundSender := false for _, uid := range params.UserIds { if uid == params.SenderStaffId { foundSender = true break } } if !foundSender { recipients = append(recipients, tea.String(params.SenderStaffId)) } } else { // 单聊:仅提问者可见 recipients = append(recipients, tea.String(params.SenderStaffId)) } // 发送钉钉卡片 _, err = d.dingtalkCardClient.CreateAndDeliver( appKey, &card_1_0.CreateAndDeliverRequest{ CardTemplateId: tea.String(d.conf.Dingtalk.Card.Template.CreateGroupApprove), OutTrackId: tea.String(outTrackId), CallbackType: tea.String("STREAM"), CardData: &card_1_0.CreateAndDeliverRequestCardData{ CardParamMap: map[string]*string{ "title": tea.String("创建群聊提醒"), "content": tea.String(fmt.Sprintf("**确认创建群聊?**\n\n将邀请以下成员加入群聊:\n\n%s", issueOwnerStr)), "remark": tea.String("注:如若无需,忽略即可"), "button_left": tea.String("创建群聊"), "button_right": tea.String("忽略"), "action_id": tea.String("create_group"), "button_display": tea.String("true"), "group_scope": tea.String(params.Summary), "target_user_ids": tea.String(strings.Join(params.UserIds, ",")), }, }, ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{ SupportForward: tea.Bool(false), }, OpenSpaceId: tea.String("dtv1.card//im_group." + params.ConversationId), ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{ RobotCode: tea.String(params.RobotCode), Recipients: recipients, }, }) return err } // 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, groupName string, userIds []string) (chatId, openConversationId string, err error) { // 是否使用模板群开关 var useTemplateGroup bool = true if groupName == "" { groupName = "问题处理群" } // 创建内部群会话 if !useTemplateGroup { return d.dingTalkOld.CreateInternalGroupConversation(ctx, accessToken, groupName, userIds) } // 根据群模板ID创建群 if useTemplateGroup { return d.dingTalkOld.CreateSceneGroupConversation(ctx, accessToken, groupName, 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, }, } }