ai_scheduler/internal/biz/ding_talk_bot.go

972 lines
32 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/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
}
}
if sys.SysID == 0 {
// 无法明确系统,询问用户
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 {
log.Errorf("assign rule not found for sys %s and issue type %s; err: %v", sys.SysName, issueType.Name, err)
return fmt.Errorf("分配规则 %s-%s 不存在", sys.SysName, issueType.Name)
}
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,
},
}
}