1016 lines
34 KiB
Go
1016 lines
34 KiB
Go
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/domain/tools/common/knowledge_base"
|
||
"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. 获取用户信息
|
||
user, err := d.botUserImpl.GetByStaffId(requireData.Req.SenderStaffId)
|
||
if err != nil {
|
||
return
|
||
}
|
||
requireData.ID = int32(user.UserID)
|
||
requireData.UserInfo = &entitys.DingTalkUserInfo{
|
||
UserId: int(user.UserID),
|
||
StaffId: user.StaffID,
|
||
Name: user.Name,
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// 分类
|
||
resolveResult, err := d.resolveSystemAndIssueType(ctx, requireData, queryText)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
switch resolveResult.IssueType.Code {
|
||
case "knowledge_qa":
|
||
// 知识库问答
|
||
return d.handleKnowledgeQA(ctx, requireData, queryText, resolveResult)
|
||
default: // 其他问题类型
|
||
// 系统为空,再次询问
|
||
if resolveResult.Sys.SysID == 0 {
|
||
entitys.ResText(requireData.Ch, "", "\n抱歉,我无法确定您咨询的是哪个系统。请告诉我具体系统名称(如:直连天下系统、货易通系统),以便我为您准确解答或安排对应的技术支持。")
|
||
return nil
|
||
}
|
||
return d.fallbackToGroupCreation(ctx, requireData, resolveResult)
|
||
}
|
||
}
|
||
|
||
// 知识库问答
|
||
func (d *DingTalkBotBiz) handleKnowledgeQA(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, queryText string, resolveResult *resolveSystemAndIssueTypeResult) error {
|
||
// 获取租户ID
|
||
tenantId := constants.KnowledgeTenantIdDefault
|
||
if resolveResult.Sys.KnowlegeTenantKey != "" {
|
||
tenantId = resolveResult.Sys.KnowlegeTenantKey
|
||
}
|
||
|
||
// 获取知识库结果
|
||
isRetrieved, err := d.getKnowledgeAnswer(ctx, requireData, tenantId, queryText)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if isRetrieved {
|
||
return nil
|
||
}
|
||
|
||
// 未匹配&全局 -> 明确具体系统
|
||
if !isRetrieved && resolveResult.Sys.SysID == 0 {
|
||
entitys.ResText(requireData.Ch, "", "\n抱歉,知识库未命中,无法回答您的问题。\n若您的问题是某一具体系统的,请告诉我具体系统名称(如:直连天下系统、货易通系统),以便我为您准确解答。")
|
||
return nil
|
||
}
|
||
// 未匹配&指定系统 -> 拉群卡片
|
||
if !isRetrieved && resolveResult.Sys.SysID != 0 {
|
||
entitys.ResText(requireData.Ch, "", fmt.Sprintf("\n抱歉,%s知识库未命中,无法回答您的问题。即将为您创建群聊解答。", resolveResult.Sys.SysName))
|
||
return d.fallbackToGroupCreation(ctx, requireData, resolveResult)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// 获取知识库问答结果
|
||
func (d *DingTalkBotBiz) getKnowledgeAnswer(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, tenantId string, queryText string) (bool, error) {
|
||
// 请求知识库工具
|
||
knowledgeBase := knowledge_base.New(d.conf.KnowledgeConfig)
|
||
knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{
|
||
TenantID: tenantId, // 后续动态接参
|
||
Query: queryText,
|
||
Mode: constants.KnowledgeModeMix,
|
||
Stream: true,
|
||
Think: false,
|
||
OnlyRAG: true,
|
||
})
|
||
if err != nil {
|
||
return false, fmt.Errorf("请求知识库工具失败,err: %v", err)
|
||
}
|
||
|
||
// 读取知识库SSE数据
|
||
return d.groupConfigBiz.readKnowledgeSSE(knowledgeResp, requireData.Ch, true)
|
||
}
|
||
|
||
type resolveSystemAndIssueTypeResult struct {
|
||
Sys model.AiSy
|
||
IssueType model.AiIssueType
|
||
Classification *do.IssueClassification
|
||
}
|
||
|
||
// 解析系统和问题类型
|
||
func (d *DingTalkBotBiz) resolveSystemAndIssueType(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, queryText string) (*resolveSystemAndIssueTypeResult, error) {
|
||
// 1. 获取所有系统和问题类型用于分类
|
||
allSys, err := d.sysImpl.FindAll()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
sysNames := slice.Map(allSys, func(_ int, sys model.AiSy) string {
|
||
return sys.SysName
|
||
})
|
||
allIssueTypes, err := d.issueImpl.IssueType.FindAll()
|
||
if err != nil {
|
||
return nil, 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 nil, err
|
||
}
|
||
|
||
// 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
|
||
}
|
||
}
|
||
|
||
return &resolveSystemAndIssueTypeResult{
|
||
Sys: sys,
|
||
IssueType: issueType,
|
||
Classification: classification,
|
||
}, nil
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// 在已知系统&问题类型的情况下进行分类并拉群
|
||
func (d *DingTalkBotBiz) fallbackToGroupCreation(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, resolveResult *resolveSystemAndIssueTypeResult) error {
|
||
// 查找分配规则
|
||
rule, found, _ := d.issueImpl.IssueAssignRule.FindOne(
|
||
d.issueImpl.WithSysID(resolveResult.Sys.SysID),
|
||
d.issueImpl.WithIssueTypeID(resolveResult.IssueType.ID),
|
||
d.issueImpl.WithStatus(1),
|
||
)
|
||
if !found {
|
||
entitys.ResText(requireData.Ch, "", fmt.Sprintf("\n抱歉,当前系统未配置路由规则 %s-%s,请联系管理员配置。", resolveResult.Sys.SysName, resolveResult.IssueType.Name))
|
||
return nil
|
||
}
|
||
|
||
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...)
|
||
|
||
// 发送确认卡片
|
||
groupName := fmt.Sprintf("[%s]-%s", resolveResult.IssueType.Name, resolveResult.Classification.Summary)
|
||
return d.SendGroupCreationConfirmCard(ctx, &SendGroupCreationConfirmCardParams{
|
||
RobotCode: requireData.Req.RobotCode,
|
||
ConversationId: requireData.Req.ConversationId,
|
||
SenderStaffId: requireData.Req.SenderStaffId,
|
||
UserIds: staffIds,
|
||
GroupName: groupName,
|
||
Summary: resolveResult.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("", 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.SenderStaffId, 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, ",")),
|
||
},
|
||
},
|
||
OpenSpaceId: tea.String("dtv1.card//IM_ROBOT." + params.SenderStaffId),
|
||
ImRobotOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImRobotOpenDeliverModel{
|
||
SpaceType: tea.String("IM_ROBOT"),
|
||
RobotCode: tea.String(params.RobotCode),
|
||
},
|
||
ImRobotOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImRobotOpenSpaceModel{
|
||
SupportForward: tea.Bool(false),
|
||
},
|
||
})
|
||
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,
|
||
},
|
||
}
|
||
}
|