Merge branch 'v4-fzy' into test

This commit is contained in:
fuzhongyun 2026-01-29 17:14:37 +08:00
commit a7ac1610bb
36 changed files with 2096 additions and 77 deletions

View File

@ -4,7 +4,7 @@ server:
host: "0.0.0.0"
ollama:
base_url: "http://192.168.6.109:11434"
base_url: "http://192.168.6.115:11434"
model: "qwen3-coder:480b-cloud"
generate_model: "qwen3-coder:480b-cloud"
mapping_model: "deepseek-v3.2:cloud"
@ -147,6 +147,26 @@ dingtalk:
# 机器人群组
bot_group_id:
bbxt: 23
# 互动卡片
card:
# 卡片回调路由key - https://gateway.dev.cdlsxd.cn/zltx_api/aitest/api/v1//callback/dingtalk-card
callback_route_key: "gateway.dev.cdlsxd.cn-dingtalk-card"
# 卡片调试工具 [show展示 hide隐藏]
debug_tool_entry_show: "hide"
# 卡片模板
template:
# 基础消息卡片(title + content)
base_msg: "291468f8-a048-4132-a37e-a14365e855e9.schema"
# 内容收集卡片title + textarea + button
content_collect: "3a447814-6a3e-4a02-b48a-92c57b349d77.schema"
# 创建群聊申请title + content + button
create_group_approve: "faad6d5d-726d-467f-a6ba-28c1930aa5f3.schema"
# 场景群
scene_group:
# 问题处理群模板ID
group_template_id_issue_handling: "aa3aa4fe-e709-4491-b24b-c3d5b27e86d0"
# 问题处理群模板机器人ID
group_template_robot_id_issue_handling: "VqgJYpB91j3RnB217690607273471011"
qywx:
corp_id: "ww48151f694fb8ec67"
@ -169,6 +189,16 @@ default_prompt:
若图片为文档类(如合同、发票、收据),请结构化输出关键字段(如客户名称、金额、开票日期等)。
'
user_prompt: '识别图片内容'
# 权限配置
permissionConfig:
permission_url: "http://api.test.user.1688sup.cn:8001/v1/menu/myCodes?systemId="
# 知识库配置
knowledge_config:
base_url: "http://192.168.6.115:9600"
tenant_id: "default"
mode: "naive"
stream: true
think: false
only_rag: true

View File

@ -154,6 +154,26 @@ dingtalk:
# 机器人群组
bot_group_id:
bbxt: 23
# 互动卡片
card:
# 卡片回调路由key
callback_route_key: "gateway.dev.cdlsxd.cn-dingtalk-card"
# 卡片调试工具 [show展示 hide隐藏]
debug_tool_entry_show: "hide"
# 卡片模板
template:
# 基础消息卡片(title + content)
base_msg: "291468f8-a048-4132-a37e-a14365e855e9.schema"
# 内容收集卡片title + textarea + button
content_collect: "3a447814-6a3e-4a02-b48a-92c57b349d77.schema"
# 创建群聊申请title + content + button
create_group_approve: "faad6d5d-726d-467f-a6ba-28c1930aa5f3.schema"
# 场景群
scene_group:
# 问题处理群模板ID
group_template_id_issue_handling: "aa3aa4fe-e709-4491-b24b-c3d5b27e86d0"
# 问题处理群模板机器人ID
group_template_robot_id_issue_handling: "VqgJYpB91j3RnB217690607273471011"
qywx:
corp_id: "ww48151f694fb8ec67"
@ -179,6 +199,15 @@ default_prompt:
permissionConfig:
permission_url: "http://api.test.user.1688sup.cn:8001/v1/menu/myCodes?systemId="
# 知识库配置
knowledge_config:
base_url: "http://192.168.6.115:9600"
tenant_id: "default"
mode: "naive"
stream: true
think: false
only_rag: true
# llm 服务配置
llm:
providers:

290
internal/biz/callback.go Normal file
View File

@ -0,0 +1,290 @@
package biz
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/data/constants"
"ai_scheduler/internal/data/impl"
"ai_scheduler/internal/domain/tools/common/knowledge_base"
"ai_scheduler/internal/pkg/dingtalk"
"ai_scheduler/internal/pkg/util"
"ai_scheduler/internal/pkg/utils_ollama"
"context"
"encoding/json"
"fmt"
"io"
"strings"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
"github.com/alibabacloud-go/dingtalk/card_1_0"
"github.com/alibabacloud-go/tea/tea"
"github.com/gofiber/fiber/v2/log"
"github.com/ollama/ollama/api"
)
type CallbackBiz struct {
cfg *config.Config
ollamaClient *utils_ollama.Client
dingtalkCardClient *dingtalk.CardClient
botConfigImpl *impl.BotConfigImpl
}
func NewCallbackBiz(
cfg *config.Config,
ollamaClient *utils_ollama.Client,
dingtalkCardClient *dingtalk.CardClient,
botConfigImpl *impl.BotConfigImpl,
) *CallbackBiz {
return &CallbackBiz{
cfg: cfg,
ollamaClient: ollamaClient,
dingtalkCardClient: dingtalkCardClient,
botConfigImpl: botConfigImpl,
}
}
// IssueHandlingGroup 问题处理群机器人回调
// 能力1 通过[内容提取] 宏分析用户QA问题调出QA表单卡片
// 能力2 通过[QA收集] 宏,收集用户反馈,写入知识库
// 能力3 通过[知识库查询] 宏,查询知识库,返回答案
func (c *CallbackBiz) IssueHandlingGroup(data chatbot.BotCallbackDataModel) error {
// 能力1、2分析用户QA问题写入知识库
if strings.Contains(data.Text.Content, "[内容提取]") || strings.Contains(data.Text.Content, "[QA收集]") {
c.issueHandlingExtractContent(data)
}
// 能力3查询知识库返回答案
if strings.Contains(data.Text.Content, "[知识库查询]") {
c.issueHandlingQueryKnowledgeBase(data)
}
return nil
}
// 问题处理群机器人内容提取
func (c *CallbackBiz) issueHandlingExtractContent(data chatbot.BotCallbackDataModel) {
// 1.提取用户输入
prompt := fmt.Sprintf(constants.IssueHandlingExtractContentPrompt, data.Text.Content)
log.Infof("问题提取提示词: %s", prompt)
// LLM 提取
generateResp, err := c.ollamaClient.Generation(context.Background(), &api.GenerateRequest{
Model: c.cfg.Ollama.GenerateModel,
Prompt: prompt,
Stream: util.AnyToPoint(false),
})
if err != nil {
log.Errorf("问题提取失败: %v", err)
return
}
// 解析 JSON 响应
var resp struct {
Items []struct {
Question string `json:"question"`
Answer string `json:"answer"`
Confidence string `json:"confidence"`
} `json:"items"`
}
if err := json.Unmarshal([]byte(generateResp.Response), &resp); err != nil {
log.Errorf("解析 JSON 响应失败: %v", err)
return
}
// 2.构建文本域内容
cardContentTpl := "问题:%s \n答案%s"
var cardContentList []string
for _, item := range resp.Items {
cardContentList = append(cardContentList, fmt.Sprintf(cardContentTpl, item.Question, item.Answer))
}
cardContent := strings.Join(cardContentList, "\n\n")
// 3.获取应用AppKey
appKey, err := c.botConfigImpl.GetRobotAppKey(data.RobotCode)
if err != nil {
log.Errorf("获取应用配置失败: %v", err)
return
}
// 4.创建并投放卡片
outTrackId := constants.BuildCardOutTrackId(data.ConversationId, data.RobotCode) // 构建卡片 OutTrackId
_, err = c.dingtalkCardClient.CreateAndDeliver(
appKey,
&card_1_0.CreateAndDeliverRequest{
CardTemplateId: tea.String(c.cfg.Dingtalk.Card.Template.ContentCollect),
OutTrackId: tea.String(outTrackId),
CallbackType: tea.String("HTTP"),
CallbackRouteKey: tea.String(c.cfg.Dingtalk.Card.CallbackRouteKey),
CardData: &card_1_0.CreateAndDeliverRequestCardData{
CardParamMap: map[string]*string{
"title": tea.String("QA知识收集"),
"button_display": tea.String("true"),
"QA_details_now": tea.String(cardContent),
"textarea_display": tea.String("normal"),
"action_id": tea.String("collect_qa"),
"tenant_id": tea.String(constants.KnowledgeTenantIdDefault),
"_CARD_DEBUG_TOOL_ENTRY": tea.String(c.cfg.Dingtalk.Card.DebugToolEntryShow), // 调试字段
},
},
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
SupportForward: tea.Bool(false),
},
OpenSpaceId: tea.String("dtv1.card//im_group." + data.ConversationId),
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
RobotCode: tea.String(c.cfg.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling),
},
},
)
}
// 问题处理群机器人查询知识库
func (c *CallbackBiz) issueHandlingQueryKnowledgeBase(data chatbot.BotCallbackDataModel) {
// 获取应用配置
appKey, err := c.botConfigImpl.GetRobotAppKey(data.RobotCode)
if err != nil {
log.Errorf("应用机器人配置不存在: %s, err: %v", data.RobotCode, err)
return
}
// 创建卡片
outTrackId := constants.BuildCardOutTrackId(data.ConversationId, data.RobotCode)
_, err = c.dingtalkCardClient.CreateAndDeliver(
appKey,
&card_1_0.CreateAndDeliverRequest{
CardTemplateId: tea.String(c.cfg.Dingtalk.Card.Template.BaseMsg),
CardData: &card_1_0.CreateAndDeliverRequestCardData{
CardParamMap: map[string]*string{
"title": tea.String(data.Text.Content),
"markdown": tea.String("知识库检索中..."),
},
},
OutTrackId: tea.String(outTrackId),
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
SupportForward: tea.Bool(false),
},
OpenSpaceId: tea.String("dtv1.card//im_group." + data.ConversationId),
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
RobotCode: tea.String(data.RobotCode),
},
},
)
// 查询知识库
knowledgeBase := knowledge_base.New(c.cfg.KnowledgeConfig)
knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{
TenantID: constants.KnowledgeTenantIdDefault,
Query: data.Text.Content,
Mode: constants.KnowledgeModeMix,
Stream: false,
Think: false,
OnlyRAG: true,
})
if err != nil {
log.Errorf("查询知识库失败: %v", err)
return
}
knowledgeRespBytes, err := io.ReadAll(knowledgeResp)
if err != nil {
log.Errorf("读取知识库响应失败: %v", err)
return
}
// 卡片更新
message, isRetrieved, err := knowledge_base.ParseOpenAIHTTPData(string(knowledgeRespBytes))
if err != nil {
log.Errorf("读取知识库 SSE 数据失败: %v", err)
return
}
content := message.Content
if !isRetrieved {
content = "知识库未检测到匹配信息,请核查知识库数据是否正确。"
}
// 卡片更新
_, err = c.dingtalkCardClient.UpdateCard(
appKey,
&card_1_0.UpdateCardRequest{
OutTrackId: tea.String(outTrackId),
CardData: &card_1_0.UpdateCardRequestCardData{
CardParamMap: map[string]*string{
"markdown": tea.String(content),
},
},
CardUpdateOptions: &card_1_0.UpdateCardRequestCardUpdateOptions{
UpdateCardDataByKey: tea.Bool(true),
},
},
)
if err != nil {
log.Errorf("更新卡片失败: %v", err)
return
}
}
// IssueHandlingCollectQA 问题处理群机器人 QA 收集回调
func (c *CallbackBiz) IssueHandlingCollectQA(data card.CardRequest) *card.CardResponse {
// 确认提交,文本写入知识库
if data.CardActionData.CardPrivateData.Params["submit"] == "submit" {
content := data.CardActionData.CardPrivateData.Params["QA_details"].(string)
tenantID := data.CardActionData.CardPrivateData.Params["tenant_id"].(string)
// 协程执行耗时操作,防止阻塞
util.SafeGo("inject_knowledge_base", func() {
knowledgeBase := knowledge_base.New(c.cfg.KnowledgeConfig)
err := knowledgeBase.IngestText(&knowledge_base.IngestTextRequest{
TenantID: tenantID,
Text: content,
})
if err != nil {
log.Errorf("注入知识库失败: %v", err)
} else {
log.Infof("注入知识库成功: tenantID=%s", tenantID)
}
// 解析当前卡片的 ConversationId 和 robotCode
conversationId, robotCode := constants.ParseCardOutTrackId(data.OutTrackId)
// 获取应用配置
appKey, err := c.botConfigImpl.GetRobotAppKey(robotCode)
if err != nil {
log.Errorf("获取应用机器人配置失败: %v", err)
return
}
// 发送卡片通知用户注入成功
outTrackId := constants.BuildCardOutTrackId(conversationId, robotCode)
c.dingtalkCardClient.CreateAndDeliver(
appKey,
&card_1_0.CreateAndDeliverRequest{
CardTemplateId: tea.String(c.cfg.Dingtalk.Card.Template.BaseMsg),
OutTrackId: tea.String(outTrackId),
CardData: &card_1_0.CreateAndDeliverRequestCardData{
CardParamMap: map[string]*string{
"title": tea.String("QA知识收集结果"),
"markdown": tea.String("[Get] **成功**"),
},
},
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
SupportForward: tea.Bool(false),
},
OpenSpaceId: tea.String("dtv1.card//im_group." + conversationId),
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
RobotCode: tea.String(robotCode),
},
},
)
})
}
// 取消提交,禁用输入框
resp := &card.CardResponse{
CardUpdateOptions: &card.CardUpdateOptions{
UpdateCardDataByKey: true,
},
CardData: &card.CardDataDto{
CardParamMap: map[string]string{
"textarea_display": "disabled",
},
},
}
return resp
}

View File

@ -21,8 +21,13 @@ import (
"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"
)
@ -46,6 +51,9 @@ type DingTalkBotBiz struct {
groupConfigBiz *GroupConfigBiz
reportDailyCacheImpl *impl.ReportDailyCacheImpl
macro *do.Macro
dingtalkOauth2Client *dingtalkPkg.Oauth2Client
dingTalkOld *dingtalkPkg.OldClient
dingtalkCardClient *dingtalkPkg.CardClient
}
// NewDingTalkBotBiz
@ -54,6 +62,7 @@ func NewDingTalkBotBiz(
handle *do.Handle,
botConfigImpl *impl.BotConfigImpl,
botGroupImpl *impl.BotGroupImpl,
botGroupConfigImpl *impl.BotGroupConfigImpl,
dingTalkUser *dingtalk.User,
chatHis *impl.BotChatHisImpl,
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
@ -62,6 +71,9 @@ func NewDingTalkBotBiz(
cardSend *dingtalk.SendCardClient,
groupConfigBiz *GroupConfigBiz,
macro *do.Macro,
dingtalkOauth2Client *dingtalkPkg.Oauth2Client,
dingTalkOld *dingtalkPkg.OldClient,
dingtalkCardClient *dingtalkPkg.CardClient,
) *DingTalkBotBiz {
return &DingTalkBotBiz{
do: do,
@ -71,12 +83,16 @@ func NewDingTalkBotBiz(
dingTalkUser: dingTalkUser,
groupConfigBiz: groupConfigBiz,
botGroupImpl: botGroupImpl,
botGroupConfigImpl: botGroupConfigImpl,
toolManager: toolManager,
chatHis: chatHis,
conf: conf,
cardSend: cardSend,
reportDailyCacheImpl: reportDailyCacheImpl,
macro: macro,
dingtalkOauth2Client: dingtalkOauth2Client,
dingTalkOld: dingTalkOld,
dingtalkCardClient: dingtalkCardClient,
}
}
@ -172,7 +188,7 @@ func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entit
return
}
return d.groupConfigBiz.handleMatch(ctx, rec, groupConfig)
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) {
@ -252,6 +268,9 @@ func (d *DingTalkBotBiz) getHis(ctx context.Context, conversationType constants.
}
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, // 用户输入内容
@ -411,3 +430,209 @@ func (d *DingTalkBotBiz) defaultPrompt() string {
-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,
},
}
}

View File

@ -8,6 +8,7 @@ import (
errors "ai_scheduler/internal/data/error"
"ai_scheduler/internal/data/impl"
"ai_scheduler/internal/data/model"
"ai_scheduler/internal/domain/tools/common/knowledge_base"
"ai_scheduler/internal/domain/workflow/runtime"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/gateway"
@ -19,6 +20,7 @@ import (
"ai_scheduler/internal/pkg/util"
"ai_scheduler/internal/tools"
"ai_scheduler/internal/tools/public"
"bufio"
errorsSpecial "errors"
"io"
"net/http"
@ -248,6 +250,101 @@ func (r *Handle) handleKnowle(ctx context.Context, rec *entitys.Recognize, task
return
}
// 知识库V2 - lightRAG自建
func (r *Handle) handleKnowleV2(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) {
// 获取用户session信息
ext, err := rec_extra.GetTaskRecExt(rec)
if err != nil {
return
}
// 获取租户ID 形式为 {biz-user} 比如 "zltx-platform"
tenantID := ext.Sys.KnowlegeTenantKey
// 请求知识库工具
knowledgeBase := knowledge_base.New(r.conf.KnowledgeConfig)
knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{
TenantID: tenantID, // 后续动态接参
Query: rec.UserContent.Text,
Mode: constants.KnowledgeModeMix,
Stream: true,
Think: false,
OnlyRAG: true,
})
if err != nil {
return fmt.Errorf("请求知识库工具失败err: %v", err)
}
// 读取知识库SSE数据
err = r.readKnowledgeSSE(knowledgeResp, rec.Ch, false)
if err != nil {
return
}
return
}
// 读取知识库 SSE 数据
func (r *Handle) readKnowledgeSSE(resp io.ReadCloser, channel chan entitys.Response, useParagraphMode bool) (err error) {
scanner := bufio.NewScanner(resp)
var buffer strings.Builder
var taskIndex string = "knowledgeBase"
for scanner.Scan() {
line := scanner.Text()
delta, done, err := knowledge_base.ParseOpenAIStreamData(line)
if err != nil {
return fmt.Errorf("解析SSE数据失败: %w", err)
}
if done {
break
}
if delta == nil {
continue
}
// 推理内容
if delta.ReasoningContent != "" {
entitys.ResStream(channel, taskIndex, delta.ReasoningContent)
continue
}
// 输出内容 - 段落
if delta.Content != "" && useParagraphMode {
// 存入缓冲区
buffer.WriteString(delta.Content)
content := buffer.String()
// 检查是否有换行符,按段落输出
if idx := strings.LastIndex(content, "\n"); idx != -1 {
// 发送直到最后一个换行符的内容
toSend := content[:idx+1]
entitys.ResStream(channel, taskIndex, toSend)
// 重置缓冲区,保留剩余部分
remaining := content[idx+1:]
buffer.Reset()
buffer.WriteString(remaining)
}
}
// 输出内容 - 逐字
if delta.Content != "" && !useParagraphMode {
entitys.ResStream(channel, taskIndex, delta.Content)
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("读取SSE流中断: %w", err)
}
// 发送缓冲区剩余内容(仅在段落模式下需要)
if useParagraphMode && buffer.Len() > 0 {
entitys.ResStream(channel, taskIndex, buffer.String())
}
return nil
}
// bot 临时实现,后续转到 eino 工作流
func (r *Handle) HandleBot(ctx context.Context, rec *entitys.Recognize, task *entitys.Task) (err error) {
if task.Index == "bug_optimization_submit" {
@ -333,7 +430,7 @@ func (r *Handle) getUserDingtalkUnionId(ctx context.Context, accessToken, sessio
func (r *Handle) getUserDingtalkUnionIdWithUserName(ctx context.Context, accessToken, userName string) (unionId string) {
// 获取创建者uid 用户名 -> dingtalk uid
creatorId, err := r.dingtalkContactClient.SearchUserOne(accessToken, userName)
creatorId, err := r.dingtalkContactClient.SearchUserOne(dingtalk.AppKey{AccessToken: accessToken}, userName)
if err != nil {
log.Warnf("search dingtalk user one failed: %v", err)
return

View File

@ -138,20 +138,24 @@ func (f *WithDingTalkBot) CreatePrompt(ctx context.Context, rec *entitys.Recogni
mes = append(prompt, api.Message{
Role: "system", // 系统角色
Content: rec.SystemPrompt, // 系统提示内容
// }, api.Message{ // 助手回复无需
// Role: "assistant", // 助手角色
// Content: "### 聊天记录:" + pkg.JsonStringIgonErr(rec.ChatHis), // 助手回复内容
}, api.Message{
Role: "assistant", // 助手角色
Content: "### 聊天记录:" + pkg.JsonStringIgonErr(rec.ChatHis), // 助手回复内容
Role: "assistant", // 助手角色
Content: "用户历史输入:" + pkg.JsonStringIgonErr(rec.ChatHis), // 用户历史输入
}, api.Message{
Role: "user", // 用户角色
Content: content.String(), // 用户输入内容
})
fmt.Printf("[意图识别]最终prompt:%v", mes)
return
}
func (f *WithDingTalkBot) getUserContent(ctx context.Context, rec *entitys.Recognize) (content strings.Builder, err error) {
var hasFile bool
if rec.UserContent.File != nil && len(rec.UserContent.File) > 0 {
if len(rec.UserContent.File) > 0 {
hasFile = true
}
content.WriteString(rec.UserContent.Text)
@ -165,11 +169,10 @@ func (f *WithDingTalkBot) getUserContent(ctx context.Context, rec *entitys.Recog
content.WriteString(rec.UserContent.Tag)
}
if len(rec.ChatHis.Messages) > 0 {
content.WriteString("\n")
content.WriteString("### 引用历史聊天记录:\n")
content.WriteString(pkg.JsonStringIgonErr(rec.ChatHis))
}
// if len(rec.ChatHis.Messages) > 0 {
// content.WriteString("### 引用历史聊天记录:\n")
// content.WriteString(pkg.JsonStringIgonErr(rec.ChatHis))
// }
return
}

View File

@ -7,15 +7,19 @@ import (
"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/domain/workflow/recharge"
"ai_scheduler/internal/domain/workflow/runtime"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/dingtalk"
"ai_scheduler/internal/pkg/l_request"
"ai_scheduler/internal/pkg/lsxd"
"ai_scheduler/internal/pkg/utils_oss"
"ai_scheduler/internal/tools"
"ai_scheduler/internal/tools/bbxt"
"ai_scheduler/utils"
"bufio"
"context"
"encoding/json"
"errors"
@ -26,6 +30,9 @@ import (
"strings"
"time"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
"github.com/alibabacloud-go/dingtalk/card_1_0"
"github.com/alibabacloud-go/tea/tea"
"github.com/coze-dev/coze-go"
"github.com/gofiber/fiber/v2/log"
"xorm.io/builder"
@ -35,6 +42,7 @@ import (
type GroupConfigBiz struct {
botGroupConfigImpl *impl.BotGroupConfigImpl
reportDailyCacheImpl *impl.ReportDailyCacheImpl
botConfigImpl *impl.BotConfigImpl
ossClient *utils_oss.Client
workflowManager *runtime.Registry
botTools []model.AiBotTool
@ -43,6 +51,7 @@ type GroupConfigBiz struct {
rdb *utils.Rdb
macro *do.Macro
handle *do.Handle
dingtalkCardClient *dingtalk.CardClient
}
// NewDingTalkBotBiz
@ -50,6 +59,7 @@ func NewGroupConfigBiz(
tools *tools_regis.ToolRegis,
ossClient *utils_oss.Client,
botGroupConfigImpl *impl.BotGroupConfigImpl,
botConfigImpl *impl.BotConfigImpl,
workflowManager *runtime.Registry,
conf *config.Config,
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
@ -57,11 +67,13 @@ func NewGroupConfigBiz(
macro *do.Macro,
toolManager *tools.Manager,
handle *do.Handle,
dingtalkCardClient *dingtalk.CardClient,
) *GroupConfigBiz {
return &GroupConfigBiz{
botTools: tools.BootTools,
ossClient: ossClient,
botGroupConfigImpl: botGroupConfigImpl,
botConfigImpl: botConfigImpl,
workflowManager: workflowManager,
conf: conf,
reportDailyCacheImpl: reportDailyCacheImpl,
@ -69,6 +81,7 @@ func NewGroupConfigBiz(
macro: macro,
toolManager: toolManager,
handle: handle,
dingtalkCardClient: dingtalkCardClient,
}
}
@ -235,7 +248,7 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz
return nil
}
func (g *GroupConfigBiz) handleMatch(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig) (err error) {
func (g *GroupConfigBiz) handleMatch(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig, callback *chatbot.BotCallbackDataModel) (err error) {
if !rec.Match.IsMatch {
if len(rec.Match.Chat) != 0 {
@ -269,6 +282,8 @@ func (g *GroupConfigBiz) handleMatch(ctx context.Context, rec *entitys.Recognize
return g.handleReport(ctx, rec, pointTask, groupConfig)
case constants.TaskTypeCozeWorkflow:
return g.handleCozeWorkflow(ctx, rec, pointTask)
case constants.TaskTypeKnowle: // 知识库lightRAG版本
return g.handleKnowledge(ctx, rec, groupConfig, callback)
default:
return g.otherTask(ctx, rec)
}
@ -426,3 +441,210 @@ func (g *GroupConfigBiz) otherTask(ctx context.Context, rec *entitys.Recognize)
entitys.ResText(rec.Ch, "", rec.Match.Reasoning)
return
}
func (g *GroupConfigBiz) GetReportCache(ctx context.Context, day time.Time, totalDetail []*bbxt.ResellerLoss, bbxtObj *bbxt.BbxtTools) error {
var ResellerProductRelation map[int32]*bbxt.ResellerLossSumProductRelation
dayDate := day.Format(time.DateOnly)
cond := builder.NewCond()
cond = cond.And(builder.Eq{"cache_index": bbxt.IndexLossSumDetail})
cond = cond.And(builder.Eq{"cache_key": dayDate})
var cache model.AiReportDailyCache
err := g.reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &cache)
if err != nil {
return err
}
if cache.ID == 0 {
ResellerProductRelation, err = bbxtObj.GetResellerLossMannagerAndLossReasonFromApi(ctx, totalDetail)
if err != nil {
return err
}
cache = model.AiReportDailyCache{
CacheKey: dayDate,
CacheIndex: bbxt.IndexLossSumDetail,
Value: pkg.JsonStringIgonErr(ResellerProductRelation),
}
_, err = g.reportDailyCacheImpl.Add(&cache)
} else {
err = json.Unmarshal([]byte(cache.Value), &ResellerProductRelation)
}
if err != nil {
return err
}
for _, v := range totalDetail {
if _, ex := ResellerProductRelation[v.ResellerId]; !ex {
continue
}
v.Manager = ResellerProductRelation[v.ResellerId].AfterSaleName
for _, vv := range v.ProductLoss {
if _, ex := ResellerProductRelation[v.ResellerId].Products[vv.ProductId]; !ex {
continue
}
vv.LossReason = ResellerProductRelation[v.ResellerId].Products[vv.ProductId].LossReason
}
}
return nil
}
// handleKnowledge 处理知识库V2版本
func (g *GroupConfigBiz) handleKnowledge(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig, callback *chatbot.BotCallbackDataModel) (err error) {
// 请求知识库工具
knowledgeBase := knowledge_base.New(g.conf.KnowledgeConfig)
knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{
TenantID: constants.KnowledgeTenantIdDefault, // 后续动态接参
Query: rec.UserContent.Text,
Mode: constants.KnowledgeModeMix,
Stream: true,
Think: false,
OnlyRAG: true,
})
if err != nil {
return fmt.Errorf("请求知识库工具失败err: %v", err)
}
// 读取知识库SSE数据
isRetrieved, err := g.readKnowledgeSSE(knowledgeResp, rec.Ch, true)
if err != nil {
return
}
// 未检索到匹配信息,询问是否拉群
if !isRetrieved {
g.shouldCreateIssueHandlingGroup(ctx, rec, groupConfig, callback)
return nil
}
return
}
// 读取知识库 SSE 数据
func (g *GroupConfigBiz) readKnowledgeSSE(resp io.ReadCloser, channel chan entitys.Response, useParagraphMode bool) (isRetrieved bool, err error) {
scanner := bufio.NewScanner(resp)
var buffer strings.Builder
for scanner.Scan() {
line := scanner.Text()
delta, done, err := knowledge_base.ParseOpenAIStreamData(line)
if err != nil {
return false, fmt.Errorf("解析SSE数据失败: %w", err)
}
if done {
break
}
if delta == nil {
continue
}
// 知识库未命中 输出提示后中断
if delta.XRagStatus == constants.KnowledgeRagStatusMiss {
var missContent string = "知识库未检测到匹配信息,即将为您创建群聊解决问题。"
entitys.ResStream(channel, "", missContent)
return false, nil
}
// 推理内容
if delta.ReasoningContent != "" {
entitys.ResStream(channel, "", delta.ReasoningContent)
continue
}
// 输出内容 - 段落
if delta.Content != "" && useParagraphMode {
// 存入缓冲区
buffer.WriteString(delta.Content)
content := buffer.String()
// 检查是否有换行符,按段落输出
if idx := strings.LastIndex(content, "\n"); idx != -1 {
// 发送直到最后一个换行符的内容
toSend := content[:idx+1]
entitys.ResStream(channel, "", toSend)
// 重置缓冲区,保留剩余部分
remaining := content[idx+1:]
buffer.Reset()
buffer.WriteString(remaining)
}
}
// 输出内容 - 逐字
if delta.Content != "" && !useParagraphMode {
entitys.ResStream(channel, "", delta.Content)
}
}
if err := scanner.Err(); err != nil {
return true, fmt.Errorf("读取SSE流中断: %w", err)
}
// 发送缓冲区剩余内容(仅在段落模式下需要)
if useParagraphMode && buffer.Len() > 0 {
entitys.ResStream(channel, "", buffer.String())
}
return true, nil
}
// 询问是否创建群聊处理问题
func (g *GroupConfigBiz) shouldCreateIssueHandlingGroup(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig, callback *chatbot.BotCallbackDataModel) error {
// 获取群问题处理人
type issueOwnerType struct {
UserId string `json:"userid"`
Name string `json:"name"`
}
var issueOwner []issueOwnerType
if err := json.Unmarshal([]byte(groupConfig.IssueOwner), &issueOwner); err != nil {
return fmt.Errorf("解析群问题处理人失败err: %v", err)
}
// 合并所有name、Id
userNames := make([]string, 0, len(issueOwner))
userIds := make([]*string, 0, len(issueOwner))
for _, owner := range issueOwner {
userNames = append(userNames, "@"+owner.Name)
userIds = append(userIds, tea.String(owner.UserId))
}
issueOwnerStr := strings.Join(userNames, "、")
// 获取应用配置
appKey, err := g.botConfigImpl.GetRobotAppKey(callback.RobotCode)
if err != nil {
return fmt.Errorf("获取机器人配置失败err: %v", err)
}
// 构建卡片 OutTrackId
outTrackId := constants.BuildCardOutTrackId(callback.ConversationId, callback.RobotCode)
// 发送钉钉卡片
_, err = g.dingtalkCardClient.CreateAndDeliver(
appKey,
&card_1_0.CreateAndDeliverRequest{
CardTemplateId: tea.String(g.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(strings.TrimSpace(rec.UserContent.Text)),
// "_CARD_DEBUG_TOOL_ENTRY": tea.String(g.conf.Dingtalk.Card.DebugToolEntryShow), // 调试字段
},
},
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
SupportForward: tea.Bool(false),
},
OpenSpaceId: tea.String("dtv1.card//im_group." + callback.ConversationId),
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
RobotCode: tea.String(callback.RobotCode),
Recipients: append(userIds, tea.String(callback.SenderStaffId)),
},
})
if err != nil {
return fmt.Errorf("发送钉钉卡片失败err: %v", err)
}
return nil
}

View File

@ -20,7 +20,7 @@ import (
)
const DefaultInterval = 100 * time.Millisecond
const HeardBeatX = 100
const HeardBeatX = 1000
type SendCardClient struct {
Auth *Auth

View File

@ -21,4 +21,5 @@ var ProviderSetBiz = wire.NewSet(
NewQywxAppBiz,
NewGroupConfigBiz,
do.NewMacro,
NewCallbackBiz,
)

View File

@ -10,9 +10,11 @@ import (
"ai_scheduler/internal/domain/repo"
"ai_scheduler/internal/domain/workflow"
"ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/dingtalk"
"ai_scheduler/internal/pkg/lsxd"
"ai_scheduler/internal/pkg/utils_ollama"
"ai_scheduler/internal/pkg/utils_oss"
"ai_scheduler/internal/tools"
"ai_scheduler/utils"
"context"
"testing"
@ -53,6 +55,11 @@ func run() {
registry := workflow.NewRegistry(configConfig, client, repos, components)
botGroupConfigImpl := impl.NewBotGroupConfigImpl(db)
botConfigImpl := impl.NewBotConfigImpl(db)
qywxAppBiz = NewQywxAppBiz(configConfig, botGroupQywxImpl, group, other)
groupConfigBiz = NewGroupConfigBiz(toolRegis, utils_ossClient, botGroupConfigImpl, registry, configConfig)
reportDailyCacheImpl := impl.NewReportDailyCacheImpl(db)
toolManager := tools.NewManager(configConfig, client)
oauth2Client, _ := dingtalk.NewOauth2Client(rdb)
dingtalkCardClient, _ := dingtalk.NewCardClient(oauth2Client)
groupConfigBiz = NewGroupConfigBiz(toolRegis, utils_ossClient, botGroupConfigImpl, botConfigImpl, registry, configConfig, reportDailyCacheImpl, rdb, toolManager, dingtalkCardClient)
}

View File

@ -24,6 +24,7 @@ type Config struct {
Oss Oss `mapstructure:"oss"`
DefaultPrompt SysPrompt `mapstructure:"default_prompt"`
PermissionConfig PermissionConfig `mapstructure:"permissionConfig"`
KnowledgeConfig KnowledgeConfig `mapstructure:"knowledge_config"`
LLM LLM `mapstructure:"llm"`
Dingtalk DingtalkConfig `mapstructure:"dingtalk"`
Qywx QywxConfig `mapstructure:"qywx"`
@ -71,10 +72,12 @@ type LLMCapabilityConfig struct {
// DingtalkConfig 钉钉配置
type DingtalkConfig struct {
ApiKey string `mapstructure:"api_key"`
ApiSecret string `mapstructure:"api_secret"`
TableDemand AITableConfig `mapstructure:"table_demand"`
BotGroupID map[string]int `mapstructure:"bot_group_id"` // 机器人群组
ApiKey string `mapstructure:"api_key"`
ApiSecret string `mapstructure:"api_secret"`
TableDemand AITableConfig `mapstructure:"table_demand"`
BotGroupID map[string]int `mapstructure:"bot_group_id"` // 机器人群组
Card CardConfig `mapstructure:"card"` // 互动卡片
SceneGroup SceneGroupConfig `mapstructure:"scene_group"` // 场景群
}
// QywxConfig 企业微信配置
@ -96,6 +99,34 @@ type AITableConfig struct {
SheetIdOrName string `mapstructure:"sheet_id_or_name"`
}
// CardConfig 互动卡片配置
type CardConfig struct {
// 卡片回调路由key
CallbackRouteKey string `mapstructure:"callback_route_key"`
// 卡片调试工具 [show展示 hide隐藏]
DebugToolEntryShow string `mapstructure:"debug_tool_entry_show"`
// 卡片模板
Template CardTemplateConfig `mapstructure:"template"`
}
// CardTemplateConfig 卡片模板配置
type CardTemplateConfig struct {
// 基础消息卡片(title + content)
BaseMsg string `mapstructure:"base_msg"`
// 内容收集卡片title + textarea + button
ContentCollect string `mapstructure:"content_collect"`
// 创建群聊申请title + content + button
CreateGroupApprove string `mapstructure:"create_group_approve"`
}
// SceneGroupConfig 场景群配置
type SceneGroupConfig struct {
// 问题处理群模板ID
GroupTemplateIDIssueHandling string `mapstructure:"group_template_id_issue_handling"`
// 问题处理群模板机器人ID
GroupTemplateRobotIDIssueHandling string `mapstructure:"group_template_robot_id_issue_handling"`
}
// SysConfig 系统配置
type SysConfig struct {
SessionLen int `mapstructure:"session_len"`
@ -253,6 +284,20 @@ type PermissionConfig struct {
PermissionURL string `mapstructure:"permission_url"`
}
// KnowledgeConfig 知识库配置
type KnowledgeConfig struct {
// 知识库地址
BaseURL string `mapstructure:"base_url"`
// 默认租户ID
TenantID string `mapstructure:"tenant_id"`
// 模式
Mode string `mapstructure:"mode"`
// 是否思考
Think bool `mapstructure:"think"`
// 是否仅RAG
OnlyRAG bool `mapstructure:"only_rag"`
}
// LoadConfig 加载配置
func LoadConfig(configPath string) (*Config, error) {
viper.SetConfigFile(configPath)

View File

@ -1,6 +1,11 @@
package constants
import "net/url"
import (
"net/url"
"strings"
"github.com/google/uuid"
)
const DingTalkBseUrl = "https://oapi.dingtalk.com"
@ -78,3 +83,83 @@ const (
]
}`
)
// 交互卡片回调
const (
// 回调类型
CardActionCallbackTypeAction string = "actionCallback" // 交互卡片回调事件类型
// 回调事件类型
CardActionTypeCreateGroup string = "create_group" // 创建群聊
)
// dingtalk 卡片 OutTrackId 模板
const CardOutTrackIdTemplate string = "{space_id}:{bot_id}:{uuid}"
func BuildCardOutTrackId(spaceId string, botId string) (outTrackId string) {
uuid := uuid.New().String()
outTrackId = strings.ReplaceAll(CardOutTrackIdTemplate, "{space_id}", spaceId)
outTrackId = strings.ReplaceAll(outTrackId, "{bot_id}", botId)
outTrackId = strings.ReplaceAll(outTrackId, "{uuid}", uuid)
return
}
func ParseCardOutTrackId(outTrackId string) (spaceId string, botId string) {
parts := strings.Split(outTrackId, ":")
if len(parts) != 3 {
return
}
spaceId, botId, _ = parts[0], parts[1], parts[2]
return
}
// 问题处理群机器人 - LLM 提示词
const IssueHandlingExtractContentPrompt string = `你是一个问题与答案生成助手
你的职责是
- 分析用户输入的内容
- 识别其中隐含或明确的问题
- 基于输入内容本身生成对应的问题与答案
当用户输入为多条群聊聊天记录
- 结合问题主题判断聊天记录中正在讨论或试图解决的问题
- 一个群聊中可能包含多个相互独立的问题但它们都围绕着一个主题一般为用户提出的第一个问题尽可能总结为一个问题
- 若确实问题很独立需要分别识别对每个问题整理出清晰可复用的问题描述对应答案
生成答案时的原则
- 答案必须来源于聊天内容中已经给出的信息或共识
- 不要引入外部知识不要使用聊天记录中真实人名或敏感信息适当总结
- 若聊天中未形成明确答案应明确标记为暂无明确结论
- 若存在多种不同观点应分别列出不要擅自合并或裁决
JSON 输出原则
- 你的最终输出必须是**合法的 JSON**
- 不得输出任何额外解释性文字
- JSON 结构必须严格符合以下约定
JSON 结构约定
{
"items": [
{
"question": "清晰、独立、可复用的问题描述",
"answer": "基于聊天内容整理出的答案;如无结论则为“暂无明确结论”",
"confidence": "high | medium | low"
}
]
}
字段说明
- items问题与答案列表若未识别到有效问题则返回空数组 []
- question抽象后的标准问题表述不包含具体聊天语句
- answer整理后的答案不得引入聊天之外的信息
- confidence根据聊天中信息的一致性和明确程度给出判断
如果无法从输入中识别出任何有效问题返回
{ "items": [] }
用户输入
%s
`

View File

@ -20,3 +20,24 @@ func GetKnowledgeId(caller Caller) KnowledgeId {
}
return CallerKnowledgeIdMap[caller]
}
// 知识库
const (
KnowledgeTenantIdDefault = "default"
)
// 知识库模式
const (
KnowledgeModeBypass = "bypass" // 绕过知识库,直接返回用户输入
KnowledgeModeNaive = "naive" // 简单模式,直接返回知识库答案
KnowledgeModeLocal = "local" // 本地模式,仅使用本地知识库
KnowledgeModeGlobal = "global" // 全局模式,使用全局知识库
KnowledgeModeHybrid = "hybrid" // 混合模式,结合本地和全局知识库
KnowledgeModeMix = "mix" // 混合模式,结合本地、全局和知识库
)
// 知识库命中状态
const (
KnowledgeRagStatusHit = "hit" // 知识库命中
KnowledgeRagStatusMiss = "miss" // 知识库未命中
)

View File

@ -2,8 +2,13 @@ package impl
import (
"ai_scheduler/internal/data/model"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg/dingtalk"
"ai_scheduler/tmpl/dataTemp"
"ai_scheduler/utils"
"encoding/json"
"xorm.io/builder"
)
type BotConfigImpl struct {
@ -15,3 +20,33 @@ func NewBotConfigImpl(db *utils.Db) *BotConfigImpl {
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiBotConfig)),
}
}
// GetRobotConfig 获取机器人配置
func (b *BotConfigImpl) GetRobotConfig(robotCode string) (*entitys.DingTalkBot, error) {
// 获取机器人配置
var botConfig model.AiBotConfig
cond := builder.NewCond().And(builder.Eq{"robot_code": robotCode}).And(builder.Eq{"status": 1})
err := b.GetOneBySearchToStrut(&cond, &botConfig)
if err != nil {
return nil, err
}
// 解出 config
var config entitys.DingTalkBot
err = json.Unmarshal([]byte(botConfig.BotConfig), &config)
if err != nil {
return nil, err
}
return &config, nil
}
// GetRobotAppKey 获取机器人应用ID
func (b *BotConfigImpl) GetRobotAppKey(robotCode string) (dingtalk.AppKey, error) {
// 获取机器人配置
dingTalkBotConfig, err := b.GetRobotConfig(robotCode)
if err != nil {
return dingtalk.AppKey{}, err
}
return dingTalkBotConfig.GetAppKey(), nil
}

View File

@ -19,7 +19,7 @@ func NewBotGroupImpl(db *utils.Db) *BotGroupImpl {
func (k BotGroupImpl) GetByConversationIdAndRobotCode(staffId string, robotCode string) (*model.AiBotGroup, error) {
var data model.AiBotGroup
err := k.Db.Model(k.Model).Where("conversation_id = ? and robot_code = ?", staffId, robotCode).Find(&data).Error
err := k.Db.Model(k.Model).Where("conversation_id = ? and robot_code = ? and status = 1", staffId, robotCode).Find(&data).Error
if data.GroupID == 0 {
err = sql.ErrNoRows
}

View File

@ -11,6 +11,7 @@ type AiBotGroupConfig struct {
ConfigID int32 `gorm:"column:config_id;primaryKey;autoIncrement:true" json:"config_id"`
ToolList string `gorm:"column:tool_list;not null" json:"tool_list"`
ProductName string `gorm:"column:product_name;not null" json:"product_name"`
IssueOwner string `gorm:"column:issue_owner;comment:群组问题处理人" json:"issue_owner"` // 群组问题处理人
}
// TableName AiBotGroupConfig's table name

View File

@ -0,0 +1,112 @@
package knowledge_base
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/pkg/l_request"
"fmt"
"io"
"net/http"
"strings"
)
type Client struct {
cfg config.KnowledgeConfig
}
func New(cfg config.KnowledgeConfig) *Client {
return &Client{cfg: cfg}
}
// 查询知识库
func (c *Client) Query(req *QueryRequest) (io.ReadCloser, error) {
if req == nil {
return nil, fmt.Errorf("req is nil")
}
if req.TenantID == "" {
return nil, fmt.Errorf("tenantID is empty")
}
if req.Query == "" {
return nil, fmt.Errorf("query is empty")
}
if req.Mode == "" {
req.Mode = c.cfg.Mode
}
if !req.Think {
req.Think = c.cfg.Think
}
if !req.OnlyRAG {
req.OnlyRAG = c.cfg.OnlyRAG
}
baseURL := strings.TrimRight(c.cfg.BaseURL, "/")
rsp, err := (&l_request.Request{
Method: "POST",
Url: baseURL + "/query",
Headers: map[string]string{
"Content-Type": "application/json",
"X-Tenant-ID": req.TenantID,
"Accept": "text/event-stream",
},
Json: map[string]interface{}{
"query": req.Query,
"mode": req.Mode,
"stream": req.Stream,
"think": req.Think,
"only_rag": req.OnlyRAG,
},
}).SendNoParseResponse()
if err != nil {
return nil, err
}
if rsp == nil || rsp.Body == nil {
return nil, fmt.Errorf("empty response")
}
if rsp.StatusCode != http.StatusOK {
defer rsp.Body.Close()
bodyPreview, _ := io.ReadAll(io.LimitReader(rsp.Body, 4096))
if len(bodyPreview) > 0 {
return nil, fmt.Errorf("knowledge base returned status %d: %s", rsp.StatusCode, string(bodyPreview))
}
return nil, fmt.Errorf("knowledge base returned status %d", rsp.StatusCode)
}
return rsp.Body, nil
}
// IngestText 向知识库中注入文本
func (c *Client) IngestText(req *IngestTextRequest) error {
if req == nil {
return fmt.Errorf("req is nil")
}
if req.TenantID == "" {
return fmt.Errorf("tenantID is empty")
}
if req.Text == "" {
return fmt.Errorf("text is empty")
}
baseURL := strings.TrimRight(c.cfg.BaseURL, "/")
rsp, err := (&l_request.Request{
Method: "POST",
Url: baseURL + "/ingest/text",
Headers: map[string]string{
"Content-Type": "application/json",
"X-Tenant-ID": req.TenantID,
},
Json: map[string]interface{}{
"text": req.Text,
},
}).Send()
if err != nil {
return err
}
if rsp.StatusCode != http.StatusOK {
return fmt.Errorf("knowledge base returned status %d: %s", rsp.StatusCode, rsp.Text)
}
return nil
}

View File

@ -0,0 +1,63 @@
package knowledge_base
import (
"ai_scheduler/internal/config"
"bufio"
"strings"
"testing"
)
func TestCall(t *testing.T) {
req := &QueryRequest{
TenantID: "admin_test_qa",
Query: "lightRAG 的优势?",
Mode: "naive",
Stream: true,
Think: false,
OnlyRAG: true,
}
client := New(config.KnowledgeConfig{BaseURL: "http://127.0.0.1:9600"})
resp, err := client.Query(req)
if err != nil {
t.Errorf("Call failed: %v", err)
}
if resp == nil {
t.Error("Response is nil")
}
defer resp.Close()
scanner := bufio.NewScanner(resp)
var outThinking strings.Builder
var outContent strings.Builder
for scanner.Scan() {
line := scanner.Text()
delta, done, err := ParseOpenAIStreamData(line)
if err != nil {
t.Fatalf("parse openai stream failed: %v", err)
}
if delta == nil {
continue
}
if done {
break
}
if delta.XRagStatus != "" {
t.Logf("XRagStatus: %s", delta.XRagStatus)
}
if delta.Content != "" {
outContent.WriteString(delta.Content)
}
if delta.ReasoningContent != "" {
outThinking.WriteString(delta.ReasoningContent)
}
}
if err := scanner.Err(); err != nil {
t.Fatalf("scan failed: %v", err)
}
t.Logf("Thinking: %s", outThinking.String())
t.Logf("Content: %s", outContent.String())
}

View File

@ -0,0 +1,75 @@
package knowledge_base
import (
"encoding/json"
"fmt"
"strings"
)
type openAIChunk struct {
Choices []struct {
Delta *Delta `json:"delta"`
Message *Message `json:"message"`
FinishReason *string `json:"finish_reason"`
} `json:"choices"`
}
type Delta struct {
ReasoningContent string `json:"reasoning_content"` // 推理内容
Content string `json:"content"` // 内容
XRagStatus string `json:"x_rag_status"` // rag命中状态 hit|miss
}
type Message struct {
Role string `json:"role"` // 角色
Content string `json:"content"` // 内容
XRagStatus string `json:"x_rag_status"` // rag命中状态 hit|miss
}
func ParseOpenAIStreamData(dataLine string) (delta *Delta, done bool, err error) {
data := strings.TrimSpace(strings.TrimPrefix(dataLine, "data:"))
if data == "" {
return nil, false, nil
}
data = strings.TrimSpace(data)
if data == "" {
return nil, false, nil
}
if data == "[DONE]" {
return nil, true, nil
}
var chunk openAIChunk
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
return nil, false, fmt.Errorf("unmarshal openai stream chunk failed: %w", err)
}
for _, c := range chunk.Choices {
if c.Delta != nil {
return c.Delta, false, nil // 只输出第一个delta
}
}
return nil, false, nil
}
func ParseOpenAIHTTPData(body string) (message *Message, done bool, err error) {
data := strings.TrimSpace(body)
if data == "" {
return nil, false, nil
}
var resp openAIChunk
if err := json.Unmarshal([]byte(data), &resp); err != nil {
return nil, false, fmt.Errorf("unmarshal openai stream chunk failed: %w", err)
}
for _, c := range resp.Choices {
if c.Message != nil {
return c.Message, true, nil // 只输出第一个message
}
}
return nil, false, nil
}

View File

@ -0,0 +1,15 @@
package knowledge_base
type QueryRequest struct {
TenantID string // 租户 ID
Query string // 查询内容
Mode string // 模式,默认 naive 可选:[bypass|naive|local|global|hybrid|mix]
Stream bool // 仅支持流式输出
Think bool // 是否开启思考模式
OnlyRAG bool // 是否仅开启 RAG 模式
}
type IngestTextRequest struct {
TenantID string // 租户 ID
Text string // 要注入的文本内容
}

View File

@ -4,6 +4,7 @@ import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/domain/tools/common/excel_generator"
"ai_scheduler/internal/domain/tools/common/image_converter"
"ai_scheduler/internal/domain/tools/common/knowledge_base"
"ai_scheduler/internal/domain/tools/hyt/goods_add"
"ai_scheduler/internal/domain/tools/hyt/goods_brand_search"
"ai_scheduler/internal/domain/tools/hyt/goods_category_add"
@ -25,6 +26,7 @@ type Manager struct {
type CommonTools struct {
ExcelGenerator *excel_generator.Client
ImageConverter *image_converter.Client
KnowledgeBase *knowledge_base.Client
}
type HytTools struct {
@ -60,6 +62,7 @@ func NewManager(cfg *config.Config) *Manager {
Common: &CommonTools{
ExcelGenerator: excel_generator.New(),
ImageConverter: image_converter.New(cfg.EinoTools.Excel2Pic),
KnowledgeBase: knowledge_base.New(cfg.KnowledgeConfig),
},
}
}

View File

@ -2,6 +2,7 @@ package entitys
import (
"ai_scheduler/internal/data/model"
"ai_scheduler/internal/pkg/dingtalk"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
)
@ -25,3 +26,10 @@ type DingTalkBot struct {
type Task struct {
Index string `json:"bot_index"`
}
func (d *DingTalkBot) GetAppKey() dingtalk.AppKey {
return dingtalk.AppKey{
AppKey: d.ClientId,
AppSecret: d.ClientSecret,
}
}

View File

@ -0,0 +1,81 @@
package dingtalk
import (
errorcode "ai_scheduler/internal/data/error"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
card "github.com/alibabacloud-go/dingtalk/card_1_0"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
)
type CardClient struct {
cli *card.Client
oauth2Client *Oauth2Client
}
func NewCardClient(oauth2Client *Oauth2Client) (*CardClient, error) {
cfg := &openapi.Config{
Protocol: tea.String("https"),
RegionId: tea.String("central"),
}
c, err := card.NewClient(cfg)
if err != nil {
return nil, err
}
return &CardClient{cli: c, oauth2Client: oauth2Client}, nil
}
// 创建并投放卡片
func (c *CardClient) CreateAndDeliver(appKey AppKey, cardData *card.CreateAndDeliverRequest) (bool, error) {
// 获取token
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
if err != nil {
return false, err
}
// 调用API
resp, err := c.cli.CreateAndDeliverWithOptions(
cardData,
&card.CreateAndDeliverHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
&util.RuntimeOptions{},
)
if err != nil {
return false, err
}
if resp.Body == nil {
return false, errorcode.ParamErrf("empty response body")
}
if !*resp.Body.Success {
return false, errorcode.ParamErrf("create and deliver failed")
}
return true, nil
}
// 更新卡片
func (c *CardClient) UpdateCard(appKey AppKey, cardData *card.UpdateCardRequest) (bool, error) {
// 获取token
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
if err != nil {
return false, err
}
// 调用API
resp, err := c.cli.UpdateCardWithOptions(
cardData,
&card.UpdateCardHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
&util.RuntimeOptions{},
)
if err != nil {
return false, err
}
if resp.Body == nil {
return false, errorcode.ParamErrf("empty response body")
}
return *resp.Body.Success, nil
}

View File

@ -1,7 +1,6 @@
package dingtalk
import (
"ai_scheduler/internal/config"
errorcode "ai_scheduler/internal/data/error"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
@ -11,22 +10,20 @@ import (
)
type ContactClient struct {
config *config.Config
cli *contact.Client
cli *contact.Client
oauth2Client *Oauth2Client
}
func NewContactClient(config *config.Config) (*ContactClient, error) {
func NewContactClient(oauth2Client *Oauth2Client) (*ContactClient, error) {
cfg := &openapi.Config{
AccessKeyId: tea.String(config.Tools.DingTalkBot.APIKey),
AccessKeySecret: tea.String(config.Tools.DingTalkBot.APISecret),
Protocol: tea.String("https"),
RegionId: tea.String("central"),
Protocol: tea.String("https"),
RegionId: tea.String("central"),
}
c, err := contact.NewClient(cfg)
if err != nil {
return nil, err
}
return &ContactClient{config: config, cli: c}, nil
return &ContactClient{cli: c, oauth2Client: oauth2Client}, nil
}
type SearchUserReq struct {
@ -40,15 +37,23 @@ type SearchUserResp struct {
Body interface{}
}
func (c *ContactClient) SearchUserOne(accessToken string, name string) (string, error) {
headers := &contact.SearchUserHeaders{}
headers.XAcsDingtalkAccessToken = tea.String(accessToken)
resp, err := c.cli.SearchUserWithOptions(&contact.SearchUserRequest{
FullMatchField: tea.Int32(1),
QueryWord: tea.String(name),
Offset: tea.Int32(0),
Size: tea.Int32(1),
}, headers, &util.RuntimeOptions{})
func (c *ContactClient) SearchUserOne(appKey AppKey, name string) (string, error) {
// 获取token
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
if err != nil {
return "", err
}
resp, err := c.cli.SearchUserWithOptions(
&contact.SearchUserRequest{
FullMatchField: tea.Int32(1),
QueryWord: tea.String(name),
Offset: tea.Int32(0),
Size: tea.Int32(1),
},
&contact.SearchUserHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
&util.RuntimeOptions{},
)
if err != nil {
return "", err
}

View File

@ -0,0 +1,78 @@
package dingtalk
import (
errorcode "ai_scheduler/internal/data/error"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
im "github.com/alibabacloud-go/dingtalk/im_1_0"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
)
type ImClient struct {
cli *im.Client
oauth2Client *Oauth2Client
}
func NewImClient(oauth2Client *Oauth2Client) (*ImClient, error) {
cfg := &openapi.Config{
Protocol: tea.String("https"),
RegionId: tea.String("central"),
}
c, err := im.NewClient(cfg)
if err != nil {
return nil, err
}
return &ImClient{cli: c, oauth2Client: oauth2Client}, nil
}
// 创建并投放卡片
func (c *ImClient) AddRobotToConversation(appKey AppKey, imData *im.AddRobotToConversationRequest) (string, error) {
// 获取token
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
if err != nil {
return "", err
}
// 调用API
resp, err := c.cli.AddRobotToConversationWithOptions(
imData,
&im.AddRobotToConversationHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
&util.RuntimeOptions{},
)
if err != nil {
return "", err
}
if resp.Body == nil {
return "", errorcode.ParamErrf("empty response body")
}
return *resp.Body.ChatBotUserId, nil
}
// 创建场景群 不返回chatid如果没有获取群聊分享链接的诉求可以使用该接口
func (c *ImClient) CreateSceneGroup(appKey AppKey, req *im.CreateSceneGroupConversationRequest) (openConversationId string, err error) {
// 获取token
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
if err != nil {
return "", err
}
// 调用API
resp, err := c.cli.CreateSceneGroupConversationWithOptions(
req,
&im.CreateSceneGroupConversationHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
&util.RuntimeOptions{},
)
if err != nil {
return "", err
}
if resp.Body == nil {
return "", errorcode.ParamErrf("empty response body")
}
return *resp.Body.OpenConversationId, nil
}

View File

@ -1,7 +1,6 @@
package dingtalk
import (
"ai_scheduler/internal/config"
errorcode "ai_scheduler/internal/data/error"
"encoding/json"
"time"
@ -13,22 +12,20 @@ import (
)
type NotableClient struct {
config *config.Config
cli *notable.Client
cli *notable.Client
oauth2Client *Oauth2Client
}
func NewNotableClient(config *config.Config) (*NotableClient, error) {
func NewNotableClient(oauth2Client *Oauth2Client) (*NotableClient, error) {
cfg := &openapi.Config{
AccessKeyId: tea.String(config.Tools.DingTalkBot.APIKey),
AccessKeySecret: tea.String(config.Tools.DingTalkBot.APISecret),
Protocol: tea.String("https"),
RegionId: tea.String("central"),
Protocol: tea.String("https"),
RegionId: tea.String("central"),
}
c, err := notable.NewClient(cfg)
if err != nil {
return nil, err
}
return &NotableClient{config: config, cli: c}, nil
return &NotableClient{cli: c, oauth2Client: oauth2Client}, nil
}
type UpdateRecordReq struct {
@ -43,9 +40,13 @@ type UpdateRecordsserResp struct {
Body interface{}
}
func (c *NotableClient) UpdateRecord(accessToken string, req *UpdateRecordReq) (bool, error) {
headers := &notable.UpdateRecordsHeaders{}
headers.XAcsDingtalkAccessToken = tea.String(accessToken)
func (c *NotableClient) UpdateRecord(appKey AppKey, req *UpdateRecordReq) (bool, error) {
// 获取token
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
if err != nil {
return false, err
}
resp, err := c.cli.UpdateRecordsWithOptions(
tea.String(req.BaseId),
tea.String(req.SheetId),
@ -63,7 +64,10 @@ func (c *NotableClient) UpdateRecord(accessToken string, req *UpdateRecordReq) (
Id: tea.String(req.RecordId),
},
},
}, headers, &util.RuntimeOptions{})
},
&notable.UpdateRecordsHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
&util.RuntimeOptions{},
)
if err != nil {
return false, err
}

View File

@ -0,0 +1,74 @@
package dingtalk
import (
errorcode "ai_scheduler/internal/data/error"
"ai_scheduler/utils"
"context"
"fmt"
"time"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
oauth2 "github.com/alibabacloud-go/dingtalk/oauth2_1_0"
"github.com/alibabacloud-go/tea/tea"
"github.com/redis/go-redis/v9"
)
type Oauth2Client struct {
cli *oauth2.Client
redisCli *redis.Client
}
func NewOauth2Client(rds *utils.Rdb) (*Oauth2Client, error) {
cfg := &openapi.Config{
Protocol: tea.String("https"),
RegionId: tea.String("central"),
}
c, err := oauth2.NewClient(cfg)
if err != nil {
return nil, err
}
return &Oauth2Client{cli: c, redisCli: rds.Rdb}, nil
}
type AppKey struct {
AppKey string `json:"appKey"`
AppSecret string `json:"appSecret"`
AccessToken string `json:"accessToken"`
}
// GetAccessToken 获取access token
func (c *Oauth2Client) GetAccessToken(req AppKey) (string, error) {
// 兼容直接传入 access token 场景
if req.AccessToken != "" {
return req.AccessToken, nil
}
// 取cache
ctx := context.Background()
accessToken, err := c.redisCli.Get(ctx, fmt.Sprintf("dingtalk:oauth2:%s:access_token", req.AppKey)).Result()
if err == nil {
fmt.Println("get access token from cache:", accessToken)
return accessToken, nil
}
if err != redis.Nil {
return "", err
}
// 调用API
resp, err := c.cli.GetAccessToken(&oauth2.GetAccessTokenRequest{
AppKey: tea.String(req.AppKey),
AppSecret: tea.String(req.AppSecret),
})
if err != nil {
return "", err
}
if resp.Body == nil {
return "", errorcode.ParamErrf("empty response body")
}
// 缓存token
c.redisCli.Set(ctx, fmt.Sprintf("dingtalk:oauth2:%s:access_token", req.AppKey), *resp.Body.AccessToken, time.Duration(*resp.Body.ExpireIn)*time.Second)
return *resp.Body.AccessToken, nil
}

View File

@ -4,6 +4,7 @@ package dingtalk
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/pkg/l_request"
"bytes"
"context"
"encoding/json"
@ -12,6 +13,7 @@ import (
"net/http"
"net/url"
"os"
"strings"
"github.com/faabiosr/cachego/file"
"github.com/fastwego/dingding"
@ -111,3 +113,141 @@ func (c *OldClient) QueryUserDetailsByMobile(ctx context.Context, mobile string)
func (c *OldClient) GetAccessToken() (string, error) {
return c.atm.GetAccessToken()
}
// CreateInternalGroupConversation 创建企业内部群聊
func (c *OldClient) CreateInternalGroupConversation(ctx context.Context, accessToken, groupName string, userIds []string) (chatId, openConversationId string, err error) {
body := struct {
Name string `json:"name"`
Owner string `json:"owner"`
UserIds []string `json:"useridlist"`
ShowHistoryType int `json:"showHistoryType"`
Searchable int `json:"searchable"`
ValidationType int `json:"validationType"`
MentionAllAuthority int `json:"mentionAllAuthority"`
ManagementType int `json:"managementType"`
ChatBannedType int `json:"chatBannedType"`
}{
Name: groupName,
Owner: userIds[0],
UserIds: userIds,
}
b, _ := json.Marshal(body)
req := l_request.Request{
Method: "POST",
JsonByte: b,
Url: "https://oapi.dingtalk.com/chat/create?access_token=" + accessToken,
Headers: map[string]string{
"Content-Type": "application/json",
},
}
res, err := req.Send()
if err != nil {
return
}
var resp struct {
Code int `json:"errcode"`
Msg string `json:"errmsg"`
ChatId string `json:"chatid"`
OpenConversationId string `json:"openConversationId"`
ConversationTag int `json:"conversationTag"`
}
if err = json.Unmarshal(res.Content, &resp); err != nil {
return
}
if resp.Code != 0 {
return "", "", errors.New(resp.Msg)
}
return resp.ChatId, resp.OpenConversationId, nil
}
// CreateSceneGroupConversation 创建场景群-基于群模板
func (c *OldClient) CreateSceneGroupConversation(ctx context.Context, accessToken, groupName string, userIds []string, templateId string) (chatId, openConversationId string, err error) {
body := struct {
Title string `json:"title"` // 群名称
TemplateId string `json:"template_id"` // 群模板ID
OwnerUserID string `json:"owner_user_id"` // 群主的userid。
UserIds string `json:"user_ids"` // 群成员userid列表。
SubAdminIds string `json:"subadmin_ids"` // 群管理员userid列表。
UUID string `json:"uuid"` // 建群去重的业务ID由接口调用方指定。
Icon string `json:"icon"` // 群头像格式为mediaId。需要调用上传媒体文件接口上传群头像获取mediaId。
MentionAllAuthority int `json:"mention_all_authority"` // @all 权限0默认所有人都可以@all 1仅群主可@all
ShowHistoryType int `json:"show_history_type"` // 新成员是否可查看聊天历史消息0默认不可以查看历史记录 1可以查看历史记录
ValidationType int `json:"validation_type"` // 入群是否需要验证0默认不验证入群 1入群验证
Searchable int `json:"searchable"` // 群是否可搜索0默认不可搜索 1可搜索
ChatBannedType int `json:"chat_banned_type"` // 是否开启群禁言0默认不禁言 1全员禁言
ManagementType int `json:"management_type"` // 管理类型0默认所有人可管理 1仅群主可管理
OnlyAdminCanDing int `json:"only_admin_can_ding"` // 群内发DING权限0默认所有人可发DING 1仅群主和管理员可发DING
AllMembersCanCreateMcsConf int `json:"all_members_can_create_mcs_conf"` // 群会议权限0仅群主和管理员可发起视频和语音会议 1默认所有人可发起视频和语音会议
AllMembersCanCreateCalendar int `json:"all_members_can_create_calendar"` // 群日历权限0仅群主和管理员可创建群日历 1默认所有人可创建群日历
GroupEmailDisabled int `json:"group_email_disabled"` // 群邮件权限0默认群内成员可以对本群发送群邮件 1群内成员不可对本群发送群邮件
OnlyAdminCanSetMsgTop int `json:"only_admin_can_set_msg_top"` // 置顶群消息权限0默认所有人可置顶群消息 1仅群主和管理员可置顶群消息
AddFriendForbidden int `json:"add_friend_forbidden"` // 群成员私聊权限0默认所有人可私聊 1普通群成员之间不能够加好友、单聊且部分功能使用受限管理员与非管理员之间不受影响
GroupLiveSwitch int `json:"group_live_switch"` // 群直播权限0仅群主与管理员可发起直播 1默认群内任意成员可发起群直播
MembersToAdminChat int `json:"members_to_admin_chat"` // 是否禁止非管理员向管理员发起单聊0默认非管理员可以向管理员发起单聊 1禁止非管理员向管理员发起单聊
}{
Title: groupName,
TemplateId: templateId,
OwnerUserID: userIds[0],
UserIds: strings.Join(userIds, ","),
SubAdminIds: strings.Join(userIds, ","),
}
b, _ := json.Marshal(body)
req := l_request.Request{
Method: "POST",
JsonByte: b,
Url: "https://oapi.dingtalk.com/topapi/im/chat/scenegroup/create?access_token=" + accessToken,
Headers: map[string]string{
"Content-Type": "application/json",
},
}
res, err := req.Send()
if err != nil {
return
}
var resp struct {
Code int `json:"errcode"`
Msg string `json:"errmsg"`
Result struct {
ChatId string `json:"chat_id"`
OpenConversationId string `json:"open_conversation_id"`
} `json:"result"`
}
if err = json.Unmarshal(res.Content, &resp); err != nil {
return
}
if resp.Code != 0 {
return "", "", errors.New(resp.Msg)
}
return resp.Result.ChatId, resp.Result.OpenConversationId, nil
}
// 获取入群二维码链接
func (c *OldClient) GetJoinGroupQrcode(ctx context.Context, chatId, userId string) (string, error) {
body := struct {
ChatId string `json:"chatid"`
UserId string `json:"userid"`
}{ChatId: chatId, UserId: userId}
b, _ := json.Marshal(body)
res, err := c.do(ctx, http.MethodPost, "/topapi/chat/qrcode/get", b)
if err != nil {
return "", err
}
var resp struct {
Code int `json:"errcode"`
Msg string `json:"errmsg"`
Result string `json:"result"`
}
if err := json.Unmarshal(res, &resp); err != nil {
return "", err
}
if resp.Code != 0 {
return "", errors.New(resp.Msg)
}
return resp.Result, nil
}

View File

@ -0,0 +1,65 @@
package dingtalk
import (
errorcode "ai_scheduler/internal/data/error"
"encoding/json"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
robot "github.com/alibabacloud-go/dingtalk/robot_1_0"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
)
type RobotClient struct {
cli *robot.Client
oauth2Client *Oauth2Client
}
func NewRobotClient(oauth2Client *Oauth2Client) (*RobotClient, error) {
cfg := &openapi.Config{
Protocol: tea.String("https"),
RegionId: tea.String("central"),
}
c, err := robot.NewClient(cfg)
if err != nil {
return nil, err
}
return &RobotClient{cli: c, oauth2Client: oauth2Client}, nil
}
type SendGroupMessagesReq struct {
MsgKey string
MsgParam map[string]any
OpenConversationId string
RobotCode string
}
func (c *RobotClient) SendGroupMessages(appKey AppKey, req *SendGroupMessagesReq) (string, error) {
// 获取token
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
if err != nil {
return "", err
}
msgParamBytes, _ := json.Marshal(req.MsgParam)
msgParamJson := string(msgParamBytes)
resp, err := c.cli.OrgGroupSendWithOptions(
&robot.OrgGroupSendRequest{
MsgKey: tea.String(req.MsgKey),
MsgParam: tea.String(msgParamJson),
OpenConversationId: tea.String(req.OpenConversationId),
RobotCode: tea.String(req.RobotCode),
},
&robot.OrgGroupSendHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
&util.RuntimeOptions{},
)
if err != nil {
return "", err
}
if resp.Body == nil {
return "", errorcode.ParamErrf("empty response body")
}
return *resp.Body.ProcessQueryKey, nil
}

View File

@ -21,6 +21,10 @@ var ProviderSetClient = wire.NewSet(
dingtalk.NewOldClient,
dingtalk.NewContactClient,
dingtalk.NewNotableClient,
dingtalk.NewRobotClient,
dingtalk.NewOauth2Client,
dingtalk.NewCardClient,
dingtalk.NewImClient,
utils_oss.NewClient,
lsxd.NewLogin,

37
internal/pkg/util/json.go Normal file
View File

@ -0,0 +1,37 @@
package util
import (
"encoding/json"
"fmt"
"strconv"
)
type FlexibleType string
func (ft *FlexibleType) UnmarshalJSON(data []byte) error {
var v interface{}
if err := json.Unmarshal(data, &v); err != nil {
return err
}
switch val := v.(type) {
case string:
*ft = FlexibleType(val)
case float64:
*ft = FlexibleType(strconv.FormatFloat(val, 'f', -1, 64))
case bool:
*ft = FlexibleType(strconv.FormatBool(val))
default:
*ft = FlexibleType(fmt.Sprintf("%v", val))
}
return nil
}
func (ft FlexibleType) Int() int {
if ft == "" {
return 0
}
i, _ := strconv.Atoi(string(ft))
return i
}

View File

@ -0,0 +1,62 @@
package util
import (
"os"
"runtime/debug"
"sync"
"time"
"github.com/bytedance/gopkg/util/gopool"
"github.com/go-kratos/kratos/v2/log"
)
var (
logger *log.Helper
once sync.Once
)
// getLogger 懒加载获取日志器
func getLogger() *log.Helper {
once.Do(func() {
// 如果没有手动初始化,使用默认的标准输出日志器
if logger == nil {
stdLogger := log.With(log.NewStdLogger(os.Stdout),
"ts", log.DefaultTimestamp,
"caller", log.DefaultCaller,
"component", "safe_pool",
)
logger = log.NewHelper(stdLogger)
}
})
return logger
}
// InitSafePool 初始化安全协程池(可选,如果不调用会使用默认日志器)
func InitSafePool(l log.Logger) {
logger = log.NewHelper(l)
}
// SafeGo 安全执行协程
// taskName: 协程任务名称,用于日志记录
// fn: 要执行的函数
func SafeGo(taskName string, fn func()) {
gopool.Go(func() {
defer func() {
if r := recover(); r != nil {
stack := debug.Stack()
getLogger().Errorf("协程 [%s] 发生panic: %v\n堆栈信息:\n%s", taskName, r, string(stack))
}
}()
// 记录协程开始执行
getLogger().Infof("协程 [%s] 开始执行", taskName)
start := time.Now()
// 执行用户函数
fn()
// 记录协程执行完成
duration := time.Since(start)
getLogger().Infof("协程 [%s] 执行完成,耗时: %v", taskName, duration)
})
}

View File

@ -7,6 +7,7 @@ import (
"fmt"
"sync"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/client"
"github.com/go-kratos/kratos/v2/log"
@ -15,6 +16,7 @@ import (
type DingBotServiceInterface interface {
GetServiceCfg() ([]entitys.DingTalkBot, error)
OnChatBotMessageReceived(ctx context.Context, data *chatbot.BotCallbackDataModel) (content []byte, err error)
OnCardMessageReceived(ctx context.Context, data *card.CardRequest) (resp *card.CardResponse, err error)
}
type DingTalkBotServer struct {
@ -38,7 +40,7 @@ func NewDingTalkBotServer(
}
cli := DingBotServerInit(serviceConf.ClientId, serviceConf.ClientSecret, service)
if cli == nil {
log.Info("%s客户端初始失败:%s", serviceConf.BotIndex, err.Error())
log.Infof("%s客户端初始失败:%s", serviceConf.BotIndex, err.Error())
continue
}
clients[serviceConf.BotIndex] = cli
@ -52,7 +54,9 @@ func NewDingTalkBotServer(
func ProvideAllDingBotServices(
dingBotSvc *services.DingBotService,
) []DingBotServiceInterface {
return []DingBotServiceInterface{dingBotSvc}
return []DingBotServiceInterface{
dingBotSvc,
}
}
func (d *DingTalkBotServer) Run(ctx context.Context, botIndex string) {
@ -103,5 +107,6 @@ func (d *DingTalkBotServer) Run(ctx context.Context, botIndex string) {
func DingBotServerInit(clientId string, clientSecret string, service DingBotServiceInterface) (cli *client.StreamClient) {
cli = client.NewStreamClient(client.WithAppCredential(client.NewAppCredentialConfig(clientId, clientSecret)))
cli.RegisterChatBotCallbackRouter(service.OnChatBotMessageReceived)
cli.RegisterCardCallbackRouter(service.OnCardMessageReceived)
return
}

View File

@ -1,6 +1,7 @@
package services
import (
"ai_scheduler/internal/biz"
"ai_scheduler/internal/biz/handle/qywx/json_callback/wxbizjsonmsgcrypt"
"ai_scheduler/internal/config"
"ai_scheduler/internal/data/constants"
@ -19,9 +20,10 @@ import (
"strings"
"time"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/gofiber/fiber/v2/middleware/proxy"
)
// CallbackService 统一回调入口
@ -31,17 +33,39 @@ type CallbackService struct {
dingtalkOldClient *dingtalk.OldClient
dingtalkContactClient *dingtalk.ContactClient
dingtalkNotableClient *dingtalk.NotableClient
dingtalkCardClient *dingtalk.CardClient
callbackManager callback.Manager
dingTalkBotBiz *biz.DingTalkBotBiz
callbackBiz *biz.CallbackBiz
// ollamaClient *utils_ollama.Client
// botConfigImpl *impl.BotConfigImpl
}
func NewCallbackService(cfg *config.Config, gateway *gateway.Gateway, dingtalkOldClient *dingtalk.OldClient, dingtalkContactClient *dingtalk.ContactClient, dingtalkNotableClient *dingtalk.NotableClient, callbackManager callback.Manager) *CallbackService {
func NewCallbackService(
cfg *config.Config,
gateway *gateway.Gateway,
dingtalkOldClient *dingtalk.OldClient,
dingtalkContactClient *dingtalk.ContactClient,
dingtalkNotableClient *dingtalk.NotableClient,
dingtalkCardClient *dingtalk.CardClient,
callbackManager callback.Manager,
dingTalkBotBiz *biz.DingTalkBotBiz,
callbackBiz *biz.CallbackBiz,
// ollamaClient *utils_ollama.Client,
// botConfigImpl *impl.BotConfigImpl,
) *CallbackService {
return &CallbackService{
cfg: cfg,
gateway: gateway,
dingtalkOldClient: dingtalkOldClient,
dingtalkContactClient: dingtalkContactClient,
dingtalkNotableClient: dingtalkNotableClient,
dingtalkCardClient: dingtalkCardClient,
callbackManager: callbackManager,
dingTalkBotBiz: dingTalkBotBiz,
callbackBiz: callbackBiz,
// ollamaClient: ollamaClient,
// botConfigImpl: botConfigImpl,
}
}
@ -271,7 +295,7 @@ func (s *CallbackService) handleBugOptimizationSubmitUpdate(ctx context.Context,
// 获取创建者uid
accessToken, _ := s.dingtalkOldClient.GetAccessToken()
creatorId, err := s.dingtalkContactClient.SearchUserOne(accessToken, data.Creator)
creatorId, err := s.dingtalkContactClient.SearchUserOne(dingtalk.AppKey{AccessToken: accessToken}, data.Creator)
if err != nil {
return "", errorcode.ParamErrf("invalid data type: %v", err)
}
@ -287,7 +311,7 @@ func (s *CallbackService) handleBugOptimizationSubmitUpdate(ctx context.Context,
unionId := userDetails.UnionID
// 更新记录
ok, err := s.dingtalkNotableClient.UpdateRecord(accessToken, &dingtalk.UpdateRecordReq{
ok, err := s.dingtalkNotableClient.UpdateRecord(dingtalk.AppKey{AccessToken: accessToken}, &dingtalk.UpdateRecordReq{
BaseId: data.BaseId,
SheetId: data.SheetId,
RecordId: data.RecordId,
@ -370,30 +394,70 @@ func getString(str, endstr string, start int, msg *string) int {
// 钉钉 callbackRouteKey: gateway.dev.cdlsxd.cn-dingtalk-robot
// 钉钉 apiSecret: aB3dE7fG9hI2jK4L5M6N7O8P9Q0R1S2T
func (s *CallbackService) CallbackDingtalkRobot(c *fiber.Ctx) (err error) {
// 代理到本地
target := "http://192.168.6.94:8090/api/v1/callback/dingtalk-robot"
if err = proxy.Do(c, target); err != nil {
log.Errorf("proxy failed: %v", err)
return err
// 获取body中的参数
body := c.Request().Body()
var data chatbot.BotCallbackDataModel
if err := json.Unmarshal(body, &data); err != nil {
return fmt.Errorf("invalid body: %v", err)
}
// token 校验 ? token 好像没带?
// 通过机器人ID路由到不同能力
switch data.RobotCode {
case s.cfg.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling:
// 问题处理群机器人
// err := s.issueHandling(data)
err := s.callbackBiz.IssueHandlingGroup(data)
if err != nil {
return fmt.Errorf("IssueHandlingGroup failed: %v", err)
}
default:
// 其他机器人
return nil
}
c.Locals("skip_response_wrap", true)
return nil
}
// CallbackDingtalkCard 钉钉卡片回调
// CallbackDingtalkCard 处理钉钉卡片回调
// 钉钉 callbackRouteKey: gateway.dev.cdlsxd.cn-dingtalk-card
// 钉钉 apiSecret: aB3dE7fG9hI2jK4L5M6N7O8P9Q0R1S2T
func (s *CallbackService) CallbackDingtalkCard(c *fiber.Ctx) (err error) {
// 代理到本地
target := "http://192.168.6.94:8090/api/v1/callback/dingtalk-card"
func (s *CallbackService) CallbackDingtalkCard(c *fiber.Ctx) error {
// 获取body中的参数
body := c.Request().Body()
if err = proxy.Do(c, target); err != nil {
log.Errorf("proxy failed: %v", err)
return err
// HTTP 回调结构与SDK结构体不符包装结构体
tmp := struct {
card.CardRequest // 嵌入原结构体
UserIdType util.FlexibleType `json:"userIdType"` // 重写type字段
}{}
if err := json.Unmarshal(body, &tmp); err != nil {
return fmt.Errorf("invalid body: %v", err)
}
// 异常字段覆盖
data := tmp.CardRequest
data.UserIdType = tmp.UserIdType.Int()
if err := json.Unmarshal([]byte(data.Content), &data.CardActionData); err != nil {
return fmt.Errorf("invalid content: %v", err)
}
// 非回调类型不处理
if data.Type != constants.CardActionCallbackTypeAction {
return nil
}
// 处理卡片回调
var resp *card.CardResponse
for _, actionId := range data.CardActionData.CardPrivateData.ActionIdList {
switch actionId {
case "collect_qa":
// 问题处理群机器人 QA 收集
resp = s.callbackBiz.IssueHandlingCollectQA(data)
}
}
// 跳过响应包装
c.Locals("skip_response_wrap", true)
return nil
return c.JSON(resp)
}

View File

@ -3,12 +3,14 @@ package services
import (
"ai_scheduler/internal/biz"
"ai_scheduler/internal/config"
"ai_scheduler/internal/data/constants"
"ai_scheduler/internal/entitys"
"context"
"log"
"sync"
"time"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
"golang.org/x/sync/errgroup"
)
@ -18,7 +20,10 @@ type DingBotService struct {
dingTalkBotBiz *biz.DingTalkBotBiz
}
func NewDingBotService(config *config.Config, dingTalkBotBiz *biz.DingTalkBotBiz) *DingBotService {
func NewDingBotService(
config *config.Config,
dingTalkBotBiz *biz.DingTalkBotBiz,
) *DingBotService {
return &DingBotService{
config: config,
dingTalkBotBiz: dingTalkBotBiz,
@ -140,3 +145,26 @@ func (d *DingBotService) runBackgroundTasks(ctx context.Context, data *chatbot.B
return nil
}
// OnCardMessageReceived 处理卡片回调
func (d *DingBotService) OnCardMessageReceived(ctx context.Context, data *card.CardRequest) (resp *card.CardResponse, err error) {
// 非回调类型暂不接受
if data.Type != constants.CardActionCallbackTypeAction {
return nil, nil
}
// action 处理 - 这里先只处理第一个匹配的actionId
for _, actionId := range data.CardActionData.CardPrivateData.ActionIdList {
switch actionId {
case constants.CardActionTypeCreateGroup:
resp, err = d.dingTalkBotBiz.CreateIssueHandlingGroupAndInit(ctx, data)
if err != nil {
return nil, err
}
return
}
}
return &card.CardResponse{}, nil
}

View File

@ -93,10 +93,14 @@ func run() {
ollamaService := llm_service.NewOllamaGenerate(client, utils_vllmClient, configConfig, chatHisImpl)
// 初始化工具管理器
manager := tools.NewManager(configConfig, client)
// 初始化钉钉认证客户端
oauth2Client, _ := dingtalk.NewOauth2Client(rdb)
// 初始化钉钉联系人客户端
contactClient, _ := dingtalk.NewContactClient(configConfig)
contactClient, _ := dingtalk.NewContactClient(oauth2Client)
// 初始化钉钉记事本客户端
notableClient, _ := dingtalk.NewNotableClient(configConfig)
notableClient, _ := dingtalk.NewNotableClient(oauth2Client)
// 初始化钉钉卡片客户端
cardClient, _ := dingtalk.NewCardClient(oauth2Client)
// 初始化工具注册
toolRegis := tools_regis.NewToolsRegis(botToolsImpl)
// 初始化机器人聊天历史实现层
@ -120,8 +124,9 @@ func run() {
group := qywx.NewGroup(botGroupQywxImpl, qywxAuth)
other := qywx.NewOther(qywxAuth)
qywxAppBiz := biz.NewQywxAppBiz(configConfig, botGroupQywxImpl, group, other)
groupConfigBiz := biz.NewGroupConfigBiz(toolRegis, utils_ossClient, botGroupConfigImpl, registry, configConfig, impl.NewReportDailyCacheImpl(db), rdb)
dingTalkBotBiz := biz.NewDingTalkBotBiz(doDo, handle, botConfigImpl, botGroupImpl, user, botChatHisImpl, impl.NewReportDailyCacheImpl(db), manager, configConfig, sendCardClient, groupConfigBiz)
groupConfigBiz := biz.NewGroupConfigBiz(toolRegis, utils_ossClient, botGroupConfigImpl, botConfigImpl, registry, configConfig, impl.NewReportDailyCacheImpl(db), rdb, manager, cardClient)
macro := do.NewMacro(botGroupImpl, impl.NewReportDailyCacheImpl(db))
dingTalkBotBiz := biz.NewDingTalkBotBiz(doDo, handle, botConfigImpl, botGroupImpl, botGroupConfigImpl, user, botChatHisImpl, impl.NewReportDailyCacheImpl(db), manager, configConfig, sendCardClient, groupConfigBiz, macro, oauth2Client, oldClient, cardClient)
// 初始化钉钉机器人服务
cronService = NewCronService(configConfig, dingTalkBotBiz, qywxAppBiz, groupConfigBiz)
}