ai_scheduler/internal/biz/ding_talk_bot.go

631 lines
21 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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/tools"
"ai_scheduler/internal/tools/bbxt"
"ai_scheduler/tmpl/dataTemp"
"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"
"github.com/alibabacloud-go/dingtalk/card_1_0"
"github.com/alibabacloud-go/tea/tea"
"github.com/gofiber/fiber/v2/log"
"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
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
}
// NewDingTalkBotBiz
func NewDingTalkBotBiz(
do *do.Do,
handle *do.Handle,
botConfigImpl *impl.BotConfigImpl,
botGroupImpl *impl.BotGroupImpl,
dingTalkUser *dingtalk.User,
chatHis *impl.BotChatHisImpl,
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,
) *DingTalkBotBiz {
return &DingTalkBotBiz{
do: do,
handle: handle,
botConfigImpl: botConfigImpl,
replier: chatbot.NewChatbotReplier(),
dingTalkUser: dingTalkUser,
groupConfigBiz: groupConfigBiz,
botGroupImpl: botGroupImpl,
toolManager: toolManager,
chatHis: chatHis,
conf: conf,
cardSend: cardSend,
reportDailyCacheImpl: reportDailyCacheImpl,
macro: macro,
dingtalkOauth2Client: dingtalkOauth2Client,
dingTalkOld: dingTalkOld,
dingtalkCardClient: dingtalkCardClient,
}
}
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
}
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
//}
//requireData.ID=requireData.UserInfo.UserID
////如果不是管理或者不是老板,则进行权限判断
//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, 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.Desc,
TaskConfigDetail: taskConfig, // 直接使用解析后的配置,避免重复构建
})
}
}
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 中已第一人称的角度提醒用户需要适用何种工具(例:"请问您是要查询订单还是商品呢")。
1. **参数提取**
- 根据 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)
// 获取新群聊人员
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,
},
}
}