fix:1. 增加钉钉card客户端,增加card创建并投放卡片方法 2.增加交互卡片回调方法 3.增加钉钉建群方法,建群demo

This commit is contained in:
fuzhongyun 2026-01-20 18:39:24 +08:00
parent 44864cc7f0
commit 17d7b01fdf
11 changed files with 252 additions and 32 deletions

View File

@ -28,8 +28,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/alibabacloud-go/dingtalk/card_1_0"
"github.com/alibabacloud-go/tea/tea"
"github.com/coze-dev/coze-go" "github.com/coze-dev/coze-go"
"github.com/gofiber/fiber/v2/log" "github.com/gofiber/fiber/v2/log"
"github.com/google/uuid"
"xorm.io/builder" "xorm.io/builder"
) )
@ -43,8 +46,7 @@ type GroupConfigBiz struct {
toolManager *tools.Manager toolManager *tools.Manager
conf *config.Config conf *config.Config
rdb *utils.Rdb rdb *utils.Rdb
dingtalkOauth2Client *dingtalk.Oauth2Client dingtalkCardClient *dingtalk.CardClient
dingtalkRobotClient *dingtalk.RobotClient
} }
// NewDingTalkBotBiz // NewDingTalkBotBiz
@ -57,8 +59,7 @@ func NewGroupConfigBiz(
reportDailyCacheImpl *impl.ReportDailyCacheImpl, reportDailyCacheImpl *impl.ReportDailyCacheImpl,
rdb *utils.Rdb, rdb *utils.Rdb,
toolManager *tools.Manager, toolManager *tools.Manager,
dingtalkOauth2Client *dingtalk.Oauth2Client, dingtalkCardClient *dingtalk.CardClient,
dingtalkRobotClient *dingtalk.RobotClient,
) *GroupConfigBiz { ) *GroupConfigBiz {
return &GroupConfigBiz{ return &GroupConfigBiz{
botTools: tools.BootTools, botTools: tools.BootTools,
@ -69,8 +70,7 @@ func NewGroupConfigBiz(
reportDailyCacheImpl: reportDailyCacheImpl, reportDailyCacheImpl: reportDailyCacheImpl,
rdb: rdb, rdb: rdb,
toolManager: toolManager, toolManager: toolManager,
dingtalkOauth2Client: dingtalkOauth2Client, dingtalkCardClient: dingtalkCardClient,
dingtalkRobotClient: dingtalkRobotClient,
} }
} }
@ -500,16 +500,49 @@ func (g *GroupConfigBiz) handleKnowledgeV2(ctx context.Context, rec *entitys.Rec
return return
} }
// var DingtalkGroupCaseOwner = map[string][]string{
// "cidwP24PLZhLVOS2dVIkEawLw==": {"付仲云", "贺泽琨", "任志远"},
// }
// 未检索到匹配信息,询问是否拉群 // 未检索到匹配信息,询问是否拉群
if !isRetrieved { 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 { if err != nil {
return fmt.Errorf("发送钉钉卡片失败err: %v", err) return fmt.Errorf("发送钉钉卡片失败err: %v", err)
} }
// entitys.ResStream(rec.Ch, "", fmt.Sprintf("已发送卡片查询ID: %s", queryKey))
return return
} }
@ -544,6 +577,7 @@ func (g *GroupConfigBiz) connectAndReadSSE(resp *http.Response, channel chan ent
// 未检索到,直接返回 // 未检索到,直接返回
dataStr := strings.TrimSpace(strings.TrimPrefix(line, "data:")) dataStr := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
if dataStr != "retrieved" { if dataStr != "retrieved" {
entitys.ResStream(channel, "", fmt.Sprintf("知识库未检测到匹配信息,即将为您创建群聊解决问题?"))
return false, nil return false, nil
} }
continue continue

View File

@ -78,3 +78,8 @@ const (
] ]
}` }`
) )
// 交互卡片回调事件类型
const (
CardActionTypeCreateGroup string = "create_group" // 创建群聊
)

View File

@ -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
}

View File

@ -34,10 +34,16 @@ func NewOauth2Client(config *config.Config, rds *utils.Rdb) (*Oauth2Client, erro
return &Oauth2Client{config: config, cli: c, redisCli: rds.Rdb}, nil return &Oauth2Client{config: config, cli: c, redisCli: rds.Rdb}, nil
} }
func (c *Oauth2Client) GetAccessToken() (string, error) { type AppKey struct {
// 去cache AppKey string `json:"appKey"`
AppSecret string `json:"appSecret"`
}
// GetAccessToken 获取access token
func (c *Oauth2Client) GetAccessToken(req AppKey) (string, error) {
// 取cache
ctx := context.Background() 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 { if err == nil {
fmt.Println("get access token from cache:", accessToken) fmt.Println("get access token from cache:", accessToken)
return accessToken, nil return accessToken, nil
@ -46,9 +52,10 @@ func (c *Oauth2Client) GetAccessToken() (string, error) {
return "", err return "", err
} }
// 调用API
resp, err := c.cli.GetAccessToken(&oauth2.GetAccessTokenRequest{ resp, err := c.cli.GetAccessToken(&oauth2.GetAccessTokenRequest{
AppKey: tea.String("ding5wwvnf9hxeyjau7t"), AppKey: tea.String(req.AppKey),
AppSecret: tea.String("FxXVlTzxrKXvJ8h-9uK0s5TjaBfOJSXumpmrHal-NmQAtku9wOPxcss0Af6WHoAK"), AppSecret: tea.String(req.AppSecret),
}) })
if err != nil { if err != nil {
return "", err return "", err
@ -58,7 +65,8 @@ func (c *Oauth2Client) GetAccessToken() (string, error) {
return "", errorcode.ParamErrf("empty response body") 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 return *resp.Body.AccessToken, nil
} }

View File

@ -111,3 +111,41 @@ func (c *OldClient) QueryUserDetailsByMobile(ctx context.Context, mobile string)
func (c *OldClient) GetAccessToken() (string, error) { func (c *OldClient) GetAccessToken() (string, error) {
return c.atm.GetAccessToken() 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
}

View File

@ -3,6 +3,7 @@ package dingtalk
import ( import (
"ai_scheduler/internal/config" "ai_scheduler/internal/config"
errorcode "ai_scheduler/internal/data/error" errorcode "ai_scheduler/internal/data/error"
"encoding/json"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
robot "github.com/alibabacloud-go/dingtalk/robot_1_0" robot "github.com/alibabacloud-go/dingtalk/robot_1_0"
@ -30,24 +31,22 @@ func NewRobotClient(config *config.Config) (*RobotClient, error) {
} }
type SendGroupMessagesReq struct { type SendGroupMessagesReq struct {
FullMatchField int32 MsgKey string
QueryWord string MsgParam map[string]any
Offset int32 OpenConversationId string
Size int32 RobotCode string
} }
type SendGroupMessagesResp struct { func (c *RobotClient) SendGroupMessages(accessToken string, req *SendGroupMessagesReq) (string, error) {
Body interface{}
}
func (c *RobotClient) SendGroupMessages(accessToken string, name string) (string, error) {
headers := &robot.OrgGroupSendHeaders{} headers := &robot.OrgGroupSendHeaders{}
headers.XAcsDingtalkAccessToken = tea.String(accessToken) headers.XAcsDingtalkAccessToken = tea.String(accessToken)
msgParamBytes, _ := json.Marshal(req.MsgParam)
msgParamJson := string(msgParamBytes)
resp, err := c.cli.OrgGroupSendWithOptions(&robot.OrgGroupSendRequest{ resp, err := c.cli.OrgGroupSendWithOptions(&robot.OrgGroupSendRequest{
MsgKey: tea.String("sampleText"), MsgKey: tea.String(req.MsgKey),
MsgParam: tea.String("{\"content\":\"今天吃肘子\"}"), MsgParam: tea.String(msgParamJson),
OpenConversationId: tea.String("cidwP24PLZhLVOS2dVIkEawLw=="), OpenConversationId: tea.String(req.OpenConversationId),
RobotCode: tea.String("ding5wwvnf9hxeyjau7t"), RobotCode: tea.String(req.RobotCode),
}, headers, &util.RuntimeOptions{}) }, headers, &util.RuntimeOptions{})
if err != nil { if err != nil {
return "", err return "", err

View File

@ -21,8 +21,9 @@ var ProviderSetClient = wire.NewSet(
dingtalk.NewOldClient, dingtalk.NewOldClient,
dingtalk.NewContactClient, dingtalk.NewContactClient,
dingtalk.NewNotableClient, dingtalk.NewNotableClient,
dingtalk.NewRobotClient, // dingtalk.NewRobotClient,
dingtalk.NewOauth2Client, dingtalk.NewOauth2Client,
dingtalk.NewCardClient,
utils_oss.NewClient, utils_oss.NewClient,
lsxd.NewLogin, lsxd.NewLogin,

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"sync" "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/chatbot"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/client" "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/client"
"github.com/go-kratos/kratos/v2/log" "github.com/go-kratos/kratos/v2/log"
@ -15,6 +16,7 @@ import (
type DingBotServiceInterface interface { type DingBotServiceInterface interface {
GetServiceCfg() ([]entitys.DingTalkBot, error) GetServiceCfg() ([]entitys.DingTalkBot, error)
OnChatBotMessageReceived(ctx context.Context, data *chatbot.BotCallbackDataModel) (content []byte, err 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 { type DingTalkBotServer struct {
@ -51,8 +53,12 @@ func NewDingTalkBotServer(
func ProvideAllDingBotServices( func ProvideAllDingBotServices(
dingBotSvc *services.DingBotService, dingBotSvc *services.DingBotService,
// dingCardSvc *services.DingtalkCardService,
) []DingBotServiceInterface { ) []DingBotServiceInterface {
return []DingBotServiceInterface{dingBotSvc} return []DingBotServiceInterface{
dingBotSvc,
// dingCardSvc,
}
} }
func (d *DingTalkBotServer) Run(ctx context.Context, botIndex string) { 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) { func DingBotServerInit(clientId string, clientSecret string, service DingBotServiceInterface) (cli *client.StreamClient) {
cli = client.NewStreamClient(client.WithAppCredential(client.NewAppCredentialConfig(clientId, clientSecret))) cli = client.NewStreamClient(client.WithAppCredential(client.NewAppCredentialConfig(clientId, clientSecret)))
cli.RegisterChatBotCallbackRouter(service.OnChatBotMessageReceived) cli.RegisterChatBotCallbackRouter(service.OnChatBotMessageReceived)
cli.RegisterCardCallbackRouter(service.OnCardMessageReceived)
return return
} }

View File

@ -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
}

View File

@ -3,12 +3,16 @@ package services
import ( import (
"ai_scheduler/internal/biz" "ai_scheduler/internal/biz"
"ai_scheduler/internal/config" "ai_scheduler/internal/config"
"ai_scheduler/internal/data/constants"
"ai_scheduler/internal/entitys" "ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg/dingtalk"
"context" "context"
"encoding/json"
"log" "log"
"sync" "sync"
"time" "time"
"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/chatbot"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
@ -16,12 +20,14 @@ import (
type DingBotService struct { type DingBotService struct {
config *config.Config config *config.Config
dingTalkBotBiz *biz.DingTalkBotBiz 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{ return &DingBotService{
config: config, config: config,
dingTalkBotBiz: dingTalkBotBiz, dingTalkBotBiz: dingTalkBotBiz,
dingTalkOld: dingTalkOld,
} }
} }
@ -140,3 +146,27 @@ func (d *DingBotService) runBackgroundTasks(ctx context.Context, data *chatbot.B
return nil 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
}

View File

@ -15,4 +15,5 @@ var ProviderSetServices = wire.NewSet(
NewHistoryService, NewHistoryService,
NewCapabilityService, NewCapabilityService,
NewCronService, NewCronService,
// NewDingtalkCardService,
) )