diff --git a/internal/biz/group_config.go b/internal/biz/group_config.go index 0783a0f..738c4f0 100644 --- a/internal/biz/group_config.go +++ b/internal/biz/group_config.go @@ -28,8 +28,11 @@ import ( "strings" "time" + "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" + "github.com/google/uuid" "xorm.io/builder" ) @@ -43,8 +46,7 @@ type GroupConfigBiz struct { toolManager *tools.Manager conf *config.Config rdb *utils.Rdb - dingtalkOauth2Client *dingtalk.Oauth2Client - dingtalkRobotClient *dingtalk.RobotClient + dingtalkCardClient *dingtalk.CardClient } // NewDingTalkBotBiz @@ -57,8 +59,7 @@ func NewGroupConfigBiz( reportDailyCacheImpl *impl.ReportDailyCacheImpl, rdb *utils.Rdb, toolManager *tools.Manager, - dingtalkOauth2Client *dingtalk.Oauth2Client, - dingtalkRobotClient *dingtalk.RobotClient, + dingtalkCardClient *dingtalk.CardClient, ) *GroupConfigBiz { return &GroupConfigBiz{ botTools: tools.BootTools, @@ -69,8 +70,7 @@ func NewGroupConfigBiz( reportDailyCacheImpl: reportDailyCacheImpl, rdb: rdb, toolManager: toolManager, - dingtalkOauth2Client: dingtalkOauth2Client, - dingtalkRobotClient: dingtalkRobotClient, + dingtalkCardClient: dingtalkCardClient, } } @@ -500,16 +500,49 @@ func (g *GroupConfigBiz) handleKnowledgeV2(ctx context.Context, rec *entitys.Rec return } + // var DingtalkGroupCaseOwner = map[string][]string{ + // "cidwP24PLZhLVOS2dVIkEawLw==": {"付仲云", "贺泽琨", "任志远"}, + // } + // 未检索到匹配信息,询问是否拉群 if !isRetrieved { - // 获取dingtalk accessToken - accessToken, _ := g.dingtalkOauth2Client.GetAccessToken() // 发送钉钉卡片 - _, err = g.dingtalkRobotClient.SendGroupMessages(accessToken, rec.UserContent.Text) + uuid := uuid.New().String() + _, err = g.dingtalkCardClient.CreateAndDeliver(dingtalk.AppKey{ + AppKey: "ding5wwvnf9hxeyjau7t", + AppSecret: "FxXVlTzxrKXvJ8h-9uK0s5TjaBfOJSXumpmrHal-NmQAtku9wOPxcss0Af6WHoAK", + }, &card_1_0.CreateAndDeliverRequest{ + CardTemplateId: tea.String("faad6d5d-726d-467f-a6ba-28c1930aa5f3.schema"), + OutTrackId: tea.String(uuid), + CallbackType: tea.String("STREAM"), + CardData: &card_1_0.CreateAndDeliverRequestCardData{ + CardParamMap: map[string]*string{ + "title": tea.String("群创建提醒"), + "content": tea.String("**确认创建群聊?**\n\n将邀请以下成员加入群聊:\n\n@张三、@李四"), + "remark": tea.String("注:如若无需,忽略即可"), + "button_left": tea.String("点击进群"), + "button_left_link": tea.String(""), + "button_right": tea.String("忽略"), + "button_right_link": tea.String(""), + "action_id": tea.String("create_group"), + "_CARD_DEBUG_TOOL_ENTRY": tea.String("show"), + }, + }, + ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{ + SupportForward: tea.Bool(false), + }, + OpenSpaceId: tea.String("dtv1.card//im_group.cidwP24PLZhLVOS2dVIkEawLw=="), + ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{ + RobotCode: tea.String("ding5wwvnf9hxeyjau7t"), + // Recipients: []*string{ + // tea.String("17415698414368678"), + // }, + }, + }) if err != nil { return fmt.Errorf("发送钉钉卡片失败,err: %v", err) } - // entitys.ResStream(rec.Ch, "", fmt.Sprintf("已发送卡片,查询ID: %s", queryKey)) + return } @@ -544,6 +577,7 @@ func (g *GroupConfigBiz) connectAndReadSSE(resp *http.Response, channel chan ent // 未检索到,直接返回 dataStr := strings.TrimSpace(strings.TrimPrefix(line, "data:")) if dataStr != "retrieved" { + entitys.ResStream(channel, "", fmt.Sprintf("知识库未检测到匹配信息,即将为您创建群聊解决问题?")) return false, nil } continue diff --git a/internal/data/constants/dingtalk.go b/internal/data/constants/dingtalk.go index fbbc7b8..0dc95fe 100644 --- a/internal/data/constants/dingtalk.go +++ b/internal/data/constants/dingtalk.go @@ -78,3 +78,8 @@ const ( ] }` ) + +// 交互卡片回调事件类型 +const ( + CardActionTypeCreateGroup string = "create_group" // 创建群聊 +) diff --git a/internal/pkg/dingtalk/card_client.go b/internal/pkg/dingtalk/card_client.go new file mode 100644 index 0000000..aa498ab --- /dev/null +++ b/internal/pkg/dingtalk/card_client.go @@ -0,0 +1,60 @@ +package dingtalk + +import ( + "ai_scheduler/internal/config" + 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 { + config *config.Config + cli *card.Client + oauth2Client *Oauth2Client +} + +func NewCardClient(config *config.Config, oauth2Client *Oauth2Client) (*CardClient, 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"), + } + c, err := card.NewClient(cfg) + if err != nil { + return nil, err + } + + return &CardClient{config: config, cli: c, oauth2Client: oauth2Client}, nil +} + +// 创建并投放卡片 +func (c *CardClient) CreateAndDeliver(req AppKey, cardData *card.CreateAndDeliverRequest) (bool, error) { + // 获取token + accessToken, err := c.oauth2Client.GetAccessToken(req) + 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 +} diff --git a/internal/pkg/dingtalk/oauth2_client.go b/internal/pkg/dingtalk/oauth2_client.go index 409bba0..859db0c 100644 --- a/internal/pkg/dingtalk/oauth2_client.go +++ b/internal/pkg/dingtalk/oauth2_client.go @@ -34,10 +34,16 @@ func NewOauth2Client(config *config.Config, rds *utils.Rdb) (*Oauth2Client, erro return &Oauth2Client{config: config, cli: c, redisCli: rds.Rdb}, nil } -func (c *Oauth2Client) GetAccessToken() (string, error) { - // 去cache +type AppKey struct { + AppKey string `json:"appKey"` + AppSecret string `json:"appSecret"` +} + +// GetAccessToken 获取access token +func (c *Oauth2Client) GetAccessToken(req AppKey) (string, error) { + // 取cache ctx := context.Background() - accessToken, err := c.redisCli.Get(ctx, "dingtalk:oauth2:access_token").Result() + 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 @@ -46,9 +52,10 @@ func (c *Oauth2Client) GetAccessToken() (string, error) { return "", err } + // 调用API resp, err := c.cli.GetAccessToken(&oauth2.GetAccessTokenRequest{ - AppKey: tea.String("ding5wwvnf9hxeyjau7t"), - AppSecret: tea.String("FxXVlTzxrKXvJ8h-9uK0s5TjaBfOJSXumpmrHal-NmQAtku9wOPxcss0Af6WHoAK"), + AppKey: tea.String(req.AppKey), + AppSecret: tea.String(req.AppSecret), }) if err != nil { return "", err @@ -58,7 +65,8 @@ func (c *Oauth2Client) GetAccessToken() (string, error) { return "", errorcode.ParamErrf("empty response body") } - c.redisCli.Set(ctx, "dingtalk:oauth2:access_token", *resp.Body.AccessToken, time.Duration(*resp.Body.ExpireIn)*time.Second) + // 缓存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 } diff --git a/internal/pkg/dingtalk/old_client.go b/internal/pkg/dingtalk/old_client.go index 7742d3a..6fb7a49 100644 --- a/internal/pkg/dingtalk/old_client.go +++ b/internal/pkg/dingtalk/old_client.go @@ -111,3 +111,41 @@ 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, groupName string, userIds []string) (string, 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) + res, err := c.do(ctx, http.MethodPost, "/chat/create", b) + if err != nil { + return "", err + } + 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, &resp); err != nil { + return "", err + } + if resp.Code != 0 { + return "", errors.New(resp.Msg) + } + return resp.OpenConversationId, nil +} diff --git a/internal/pkg/dingtalk/robot_client.go b/internal/pkg/dingtalk/robot_client.go index 3f1f40f..e81f3ac 100644 --- a/internal/pkg/dingtalk/robot_client.go +++ b/internal/pkg/dingtalk/robot_client.go @@ -3,6 +3,7 @@ package dingtalk import ( "ai_scheduler/internal/config" 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" @@ -30,24 +31,22 @@ func NewRobotClient(config *config.Config) (*RobotClient, error) { } type SendGroupMessagesReq struct { - FullMatchField int32 - QueryWord string - Offset int32 - Size int32 + MsgKey string + MsgParam map[string]any + OpenConversationId string + RobotCode string } -type SendGroupMessagesResp struct { - Body interface{} -} - -func (c *RobotClient) SendGroupMessages(accessToken string, name string) (string, error) { +func (c *RobotClient) SendGroupMessages(accessToken string, req *SendGroupMessagesReq) (string, error) { headers := &robot.OrgGroupSendHeaders{} headers.XAcsDingtalkAccessToken = tea.String(accessToken) + msgParamBytes, _ := json.Marshal(req.MsgParam) + msgParamJson := string(msgParamBytes) resp, err := c.cli.OrgGroupSendWithOptions(&robot.OrgGroupSendRequest{ - MsgKey: tea.String("sampleText"), - MsgParam: tea.String("{\"content\":\"今天吃肘子\"}"), - OpenConversationId: tea.String("cidwP24PLZhLVOS2dVIkEawLw=="), - RobotCode: tea.String("ding5wwvnf9hxeyjau7t"), + MsgKey: tea.String(req.MsgKey), + MsgParam: tea.String(msgParamJson), + OpenConversationId: tea.String(req.OpenConversationId), + RobotCode: tea.String(req.RobotCode), }, headers, &util.RuntimeOptions{}) if err != nil { return "", err diff --git a/internal/pkg/provider_set.go b/internal/pkg/provider_set.go index b7120c6..76bab52 100644 --- a/internal/pkg/provider_set.go +++ b/internal/pkg/provider_set.go @@ -21,8 +21,9 @@ var ProviderSetClient = wire.NewSet( dingtalk.NewOldClient, dingtalk.NewContactClient, dingtalk.NewNotableClient, - dingtalk.NewRobotClient, + // dingtalk.NewRobotClient, dingtalk.NewOauth2Client, + dingtalk.NewCardClient, utils_oss.NewClient, lsxd.NewLogin, diff --git a/internal/server/ding_talk_bot.go b/internal/server/ding_talk_bot.go index 2eb31c6..4453d17 100644 --- a/internal/server/ding_talk_bot.go +++ b/internal/server/ding_talk_bot.go @@ -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 { @@ -51,8 +53,12 @@ func NewDingTalkBotServer( func ProvideAllDingBotServices( dingBotSvc *services.DingBotService, + // dingCardSvc *services.DingtalkCardService, ) []DingBotServiceInterface { - return []DingBotServiceInterface{dingBotSvc} + return []DingBotServiceInterface{ + dingBotSvc, + // dingCardSvc, + } } func (d *DingTalkBotServer) Run(ctx context.Context, botIndex string) { @@ -103,5 +109,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 } diff --git a/internal/services/dingtalk_card.go b/internal/services/dingtalk_card.go new file mode 100644 index 0000000..f549f37 --- /dev/null +++ b/internal/services/dingtalk_card.go @@ -0,0 +1,37 @@ +package services + +// 当前不用,后续切钉钉新sdk考虑使用 + +import ( + "ai_scheduler/internal/biz" + "ai_scheduler/internal/config" + "ai_scheduler/internal/entitys" + "context" + + "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card" + "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot" +) + +type DingtalkCardService struct { + config *config.Config + dingtalkCardBiz *biz.DingTalkBotBiz +} + +func NewDingtalkCardService(config *config.Config, dingtalkCardBiz *biz.DingTalkBotBiz) *DingtalkCardService { + return &DingtalkCardService{ + config: config, + dingtalkCardBiz: dingtalkCardBiz, + } +} + +func (d *DingtalkCardService) GetServiceCfg() (config []entitys.DingTalkBot, err error) { + return +} + +func (d *DingtalkCardService) OnChatBotMessageReceived(ctx context.Context, data *chatbot.BotCallbackDataModel) (content []byte, err error) { + return +} + +func (d *DingtalkCardService) OnCardMessageReceived(ctx context.Context, data *card.CardRequest) (resp *card.CardResponse, err error) { + return +} diff --git a/internal/services/dtalk_bot.go b/internal/services/dtalk_bot.go index 177406b..a2e5f4e 100644 --- a/internal/services/dtalk_bot.go +++ b/internal/services/dtalk_bot.go @@ -3,12 +3,16 @@ package services import ( "ai_scheduler/internal/biz" "ai_scheduler/internal/config" + "ai_scheduler/internal/data/constants" "ai_scheduler/internal/entitys" + "ai_scheduler/internal/pkg/dingtalk" "context" + "encoding/json" "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" ) @@ -16,12 +20,14 @@ import ( type DingBotService struct { config *config.Config dingTalkBotBiz *biz.DingTalkBotBiz + dingTalkOld *dingtalk.OldClient } -func NewDingBotService(config *config.Config, dingTalkBotBiz *biz.DingTalkBotBiz) *DingBotService { +func NewDingBotService(config *config.Config, dingTalkBotBiz *biz.DingTalkBotBiz, dingTalkOld *dingtalk.OldClient) *DingBotService { return &DingBotService{ config: config, dingTalkBotBiz: dingTalkBotBiz, + dingTalkOld: dingTalkOld, } } @@ -140,3 +146,27 @@ func (d *DingBotService) runBackgroundTasks(ctx context.Context, data *chatbot.B return nil } + +func (d *DingBotService) OnCardMessageReceived(ctx context.Context, data *card.CardRequest) (resp *card.CardResponse, err error) { + // 解content + var cardActionData card.PrivateCardActionData + if err = json.Unmarshal([]byte(data.Content), &cardActionData); err != nil { + return nil, err + } + + // action 处理 + for _, actionId := range cardActionData.CardPrivateData.ActionIdList { + switch actionId { + case constants.CardActionTypeCreateGroup: + if cardActionData.CardPrivateData.Params["status"] != "confirm" { + continue + } + // 创建群聊 + if _, err = d.dingTalkOld.CreateInternalGroupConversation(ctx, "问题处理群", []string{"17415698414368678", "17101201090101570"}); err != nil { + return nil, err + } + } + } + + return &card.CardResponse{}, nil +} diff --git a/internal/services/provider_set.go b/internal/services/provider_set.go index 55eed7a..4eac29c 100644 --- a/internal/services/provider_set.go +++ b/internal/services/provider_set.go @@ -15,4 +15,5 @@ var ProviderSetServices = wire.NewSet( NewHistoryService, NewCapabilityService, NewCronService, + // NewDingtalkCardService, )