package biz import ( "ai_scheduler/internal/biz/do" "ai_scheduler/internal/biz/handle/dingtalk" "ai_scheduler/internal/biz/tools_regis" "ai_scheduler/internal/data/constants" "ai_scheduler/internal/data/impl" "ai_scheduler/internal/data/model" "ai_scheduler/internal/entitys" "ai_scheduler/internal/tools" "context" "database/sql" "encoding/json" "errors" "fmt" "strings" "github.com/gofiber/fiber/v2/log" "github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot" "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 botTools []model.AiBotTool botGroupImpl *impl.BotGroupImpl toolManager *tools.Manager } // NewDingTalkBotBiz func NewDingTalkBotBiz( do *do.Do, handle *do.Handle, botConfigImpl *impl.BotConfigImpl, botGroupImpl *impl.BotGroupImpl, dingTalkUser *dingtalk.User, tools *tools_regis.ToolRegis, toolManager *tools.Manager, ) *DingTalkBotBiz { return &DingTalkBotBiz{ do: do, handle: handle, botConfigImpl: botConfigImpl, replier: chatbot.NewChatbotReplier(), dingTalkUser: dingTalkUser, botTools: tools.BootTools, botGroupImpl: botGroupImpl, toolManager: toolManager, } } 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.BotIndex 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.ResText(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) } return } func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) { entitys.ResLog(requireData.Ch, "", "个人聊天暂未开启,请期待后续更新") return //requireData.UserInfo, err = d.dingTalkUser.GetUserInfoFromBot(ctx, requireData.Req.SenderStaffId, dingtalk.WithId(1)) //if err != nil { // return //} ////如果不是管理或者不是老板,则进行权限判断 //if requireData.UserInfo.IsSenior == constants.IsSeniorFalse && requireData.UserInfo.IsBoss == constants.IsBossFalse { // //} //return } func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) { group, err := d.initGroup(ctx, requireData.Req.ConversationId, requireData.Req.ConversationTitle) if err != nil { return } groupTools, err := d.getGroupTools(ctx, group) if err != nil { return } rec, err := d.recognize(ctx, requireData, groupTools) if err != nil { return } return d.handleMatch(ctx, rec) } func (d *DingTalkBotBiz) initGroup(ctx context.Context, conversationId string, conversationTitle string) (group *model.AiBotGroup, err error) { group, err = d.botGroupImpl.GetByConversationId(conversationId) if err != nil { if !errors.Is(err, sql.ErrNoRows) { return } } if group.GroupID == 0 { group = &model.AiBotGroup{ ConversationID: conversationId, Title: conversationTitle, ToolList: "", } //如果不存在则创建 d.botGroupImpl.Add(group) } return } func (d *DingTalkBotBiz) getGroupTools(ctx context.Context, group *model.AiBotGroup) (tools []model.AiBotTool, err error) { if len(d.botTools) == 0 { return } var ( groupRegisTools map[string]struct{} ) if group.ToolList != "" { groupList := strings.Split(group.ToolList, ",") for _, tool := range groupList { groupRegisTools[tool] = struct{}{} } } for _, v := range d.botTools { if v.PermissionType == constants.PermissionTypeNone { tools = append(tools, v) continue } if _, ex := groupRegisTools[v.Index]; ex { tools = append(tools, v) } } 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 nil, err } rec = &entitys.Recognize{ Ch: requireData.Ch, SystemPrompt: d.defaultPrompt(), UserContent: userContent, } 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.Desc, TaskConfigDetail: taskConfig, // 直接使用解析后的配置,避免重复构建 }) } } err = d.handle.Recognize(ctx, rec, &do.WithDingTalkBot{}) return } 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) defaultPrompt() string { return `{"system":"智能路由系统,精准解析用户意图并路由至任务模块,遵循以下规则:","rule":{"返回格式":"{\\"index\\":\\"工具索引\\",\\"confidence\\":\\"0.0-1.0\\",\\"reasoning\\":\\"判断理由\\",\\"parameters\\":\\"转义JSON参数\\",\\"is_match\\":true|false,\\"chat\\":\\"追问内容\\"}","工具匹配":["用工具parameters匹配,区分必选(required)和可选(optional)参数","无法匹配时,is_match=false,chat提醒用户适用工具(例:'请问您要查询订单还是商品?')"],"参数提取":["从用户输入提取parameters中明确提及的参数","必须参数仅用用户直接提及的,缺失时is_match=false,chat提醒补充(例:'需补充XX信息')"],"格式要求":["所有字段值为字符串(含confidence)","parameters为转义JSON字符串(如\\"{\\\\"key\\\\":\\\\"value\\\\"}\\")"]}}` } func (d *DingTalkBotBiz) handleMatch(ctx context.Context, rec *entitys.Recognize) (err error) { if !rec.Match.IsMatch { if len(rec.Match.Chat) != 0 { entitys.ResText(rec.Ch, "", rec.Match.Chat) } else { entitys.ResText(rec.Ch, "", rec.Match.Reasoning) } return } var pointTask *model.AiBotTool for _, task := range d.botTools { if task.Index == rec.Match.Index { pointTask = &task break } } if pointTask == nil || pointTask.Index == "other" { return d.otherTask(ctx, rec) } switch constants.TaskType(pointTask.Type) { //case constants.TaskTypeApi: //return d.handleApiTask(ctx, requireData, pointTask) case constants.TaskTypeFunc: return d.handleTask(ctx, rec, pointTask) default: return d.otherTask(ctx, rec) } return } func (d *DingTalkBotBiz) handleTask(ctx context.Context, rec *entitys.Recognize, task *model.AiBotTool) (err error) { var configData entitys.ConfigDataTool err = json.Unmarshal([]byte(task.Config), &configData) if err != nil { return } err = d.toolManager.ExecuteTool(ctx, configData.Tool, rec) if err != nil { return } return } func (d *DingTalkBotBiz) otherTask(ctx context.Context, rec *entitys.Recognize) (err error) { entitys.ResText(rec.Ch, "", rec.Match.Reasoning) return } func (d *DingTalkBotBiz) HandleRes(ctx context.Context, data *chatbot.BotCallbackDataModel, resp entitys.Response) error { switch resp.Type { case entitys.ResponseText: return d.replyText(ctx, data.SessionWebhook, resp.Content) case entitys.ResponseStream: return d.replySteam(ctx, data.SessionWebhook, resp.Content) case entitys.ResponseImg: return d.replyImg(ctx, data.SessionWebhook, resp.Content) case entitys.ResponseFile: return d.replyFile(ctx, data.SessionWebhook, resp.Content) case entitys.ResponseMarkdown: return d.replyMarkdown(ctx, data.SessionWebhook, resp.Content) case entitys.ResponseActionCard: return d.replyActionCard(ctx, data.SessionWebhook, resp.Content) default: return nil } } func (d *DingTalkBotBiz) replySteam(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) 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)) }