ai_scheduler/internal/biz/handle/dingtalk/send_card.go

288 lines
9.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package dingtalk
import (
"ai_scheduler/internal/data/constants"
"ai_scheduler/internal/pkg"
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
"time"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
dingtalkim_1_0 "github.com/alibabacloud-go/dingtalk/im_1_0"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
"github.com/gofiber/fiber/v2/log"
"github.com/google/uuid"
)
const DefaultInterval = 100 * time.Millisecond
const HeardBeatX = 50
type SendCardClient struct {
Auth *Auth
CardClient *sync.Map
mu sync.RWMutex // 保护 CardClient 的并发访问
logger log.AllLogger // 日志记录
botOption *Bot
}
func NewSendCardClient(auth *Auth, logger log.AllLogger) *SendCardClient {
return &SendCardClient{
Auth: auth,
CardClient: &sync.Map{},
logger: logger,
botOption: &Bot{},
}
}
// initClient 初始化或复用 DingTalk 客户端
func (s *SendCardClient) initClient(robotCode string) (*dingtalkim_1_0.Client, error) {
if client, ok := s.CardClient.Load(robotCode); ok {
return client.(*dingtalkim_1_0.Client), nil
}
s.botOption.BotCode = robotCode
config := &openapi.Config{
Protocol: tea.String("https"),
RegionId: tea.String("central"),
}
client, err := dingtalkim_1_0.NewClient(config)
if err != nil {
s.logger.Error("failed to init DingTalk client")
return nil, fmt.Errorf("init client failed: %w", err)
}
s.CardClient.Store(robotCode, client)
return client, nil
}
func (s *SendCardClient) NewCard(ctx context.Context, cardSend *CardSend) error {
// 参数校验
if (len(cardSend.ContentSlice) == 0 || cardSend.ContentSlice == nil) && cardSend.ContentChannel == nil {
return errors.New("卡片内容不能为空")
}
if cardSend.UpdateInterval == 0 {
cardSend.UpdateInterval = DefaultInterval // 默认更新间隔
}
if cardSend.Title == "" {
cardSend.Title = "钉钉卡片"
}
//替换标题
replace, err := pkg.SafeReplace(string(cardSend.Template), "${title}", cardSend.Title)
if err != nil {
return err
}
cardSend.Template = constants.CardTemp(replace)
// 初始化客户端
client, err := s.initClient(cardSend.RobotCode)
if err != nil {
return fmt.Errorf("初始化client失败: %w", err)
}
// 生成卡片实例ID
cardInstanceId, err := uuid.NewUUID()
if err != nil {
return fmt.Errorf("创建uuid失败: %w", err)
}
// 构建初始请求
request, err := s.buildBaseRequest(cardSend, cardInstanceId.String())
if err != nil {
return fmt.Errorf("请求失败: %w", err)
}
// 发送初始卡片
if _, err := s.SendInteractiveCard(ctx, request, cardSend.RobotCode, client); err != nil {
return fmt.Errorf("发送初始卡片失败: %w", err)
}
// 处理切片内容(同步)
if len(cardSend.ContentSlice) > 0 {
if err := s.processContentSlice(ctx, cardSend, cardInstanceId.String(), client); err != nil {
return fmt.Errorf("内容同步失败: %w", err)
}
}
// 处理通道内容(异步)
if cardSend.ContentChannel != nil {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
s.processContentChannel(ctx, cardSend, cardInstanceId.String(), client)
}()
wg.Wait()
}
return nil
}
// buildBaseRequest 构建基础请求
func (s *SendCardClient) buildBaseRequest(cardSend *CardSend, cardInstanceId string) (*dingtalkim_1_0.SendRobotInteractiveCardRequest, error) {
cardData := fmt.Sprintf(string(cardSend.Template), "") // 初始空内容
request := &dingtalkim_1_0.SendRobotInteractiveCardRequest{
CardTemplateId: tea.String("StandardCard"),
CardBizId: tea.String(cardInstanceId),
CardData: tea.String(cardData),
RobotCode: tea.String(cardSend.RobotCode),
SendOptions: &dingtalkim_1_0.SendRobotInteractiveCardRequestSendOptions{},
PullStrategy: tea.Bool(false),
}
switch cardSend.ConversationType {
case constants.ConversationTypeGroup:
request.SetOpenConversationId(cardSend.ConversationId)
case constants.ConversationTypeSingle:
receiver, err := json.Marshal(map[string]string{"userId": cardSend.SenderStaffId})
if err != nil {
return nil, fmt.Errorf("数据整理失败: %w", err)
}
request.SetSingleChatReceiver(string(receiver))
default:
return nil, errors.New("未知的聊天场景")
}
return request, nil
}
// processContentChannel 处理通道内容(异步更新)
func (s *SendCardClient) processContentChannel(ctx context.Context, cardSend *CardSend, cardInstanceId string, client *dingtalkim_1_0.Client) {
defer func() {
if r := recover(); r != nil {
s.logger.Error("panic in processContentChannel")
}
}()
ticker := time.NewTicker(cardSend.UpdateInterval)
defer ticker.Stop()
heartbeatTicker := time.NewTicker(time.Duration(HeardBeatX) * DefaultInterval)
defer heartbeatTicker.Stop()
var (
contentBuilder strings.Builder
lastUpdate time.Time
)
for {
select {
case content, ok := <-cardSend.ContentChannel:
if !ok {
// 通道关闭,发送最终内容
if contentBuilder.Len() > 0 {
if err := s.updateCardContent(ctx, cardSend, cardInstanceId, contentBuilder.String(), client); err != nil {
s.logger.Errorf("更新卡片失败1:%s", err.Error())
}
}
return
}
contentBuilder.WriteString(content)
if contentBuilder.Len() > 0 {
if err := s.updateCardContent(ctx, cardSend, cardInstanceId, contentBuilder.String(), client); err != nil {
s.logger.Errorf("更新卡片失败2%s", err.Error())
}
}
lastUpdate = time.Now()
case <-heartbeatTicker.C:
if time.Now().Unix()-lastUpdate.Unix() >= HeardBeatX {
return
}
case <-ctx.Done():
s.logger.Info("context canceled, stop channel processing")
return
}
}
}
// processContentSlice 处理切片内容(同步更新)
func (s *SendCardClient) processContentSlice(ctx context.Context, cardSend *CardSend, cardInstanceId string, client *dingtalkim_1_0.Client) error {
var contentBuilder strings.Builder
for _, content := range cardSend.ContentSlice {
contentBuilder.WriteString(content)
err := s.updateCardRequest(ctx, &UpdateCardRequest{
Template: string(cardSend.Template),
Content: contentBuilder.String(),
Client: client,
RobotCode: cardSend.RobotCode,
CardInstanceId: cardInstanceId,
})
if err != nil {
return fmt.Errorf("更新卡片失败: %w", err)
}
time.Sleep(cardSend.UpdateInterval) // 控制更新频率
}
return nil
}
// updateCardContent 封装卡片更新逻辑
func (s *SendCardClient) updateCardContent(ctx context.Context, cardSend *CardSend, cardInstanceId, content string, client *dingtalkim_1_0.Client) error {
err := s.updateCardRequest(ctx, &UpdateCardRequest{
Template: string(cardSend.Template),
Content: content,
Client: client,
RobotCode: cardSend.RobotCode,
CardInstanceId: cardInstanceId,
})
return err
}
func (s *SendCardClient) updateCardRequest(ctx context.Context, updateCardRequest *UpdateCardRequest) error {
content, err := pkg.SafeReplace(updateCardRequest.Template, "%s", updateCardRequest.Content)
if err != nil {
return err
}
updateRequest := &dingtalkim_1_0.UpdateRobotInteractiveCardRequest{
CardBizId: tea.String(updateCardRequest.CardInstanceId),
CardData: tea.String(content),
}
_, err = s.UpdateInteractiveCard(ctx, updateRequest, updateCardRequest.RobotCode, updateCardRequest.Client)
return err
}
// UpdateInteractiveCard 更新交互卡片(封装错误处理)
func (s *SendCardClient) UpdateInteractiveCard(ctx context.Context, request *dingtalkim_1_0.UpdateRobotInteractiveCardRequest, robotCode string, client *dingtalkim_1_0.Client) (*dingtalkim_1_0.UpdateRobotInteractiveCardResponse, error) {
authInfo, err := s.Auth.GetTokenFromBotOption(ctx, WithBot(s.botOption))
if err != nil {
return nil, fmt.Errorf("get token failed: %w", err)
}
headers := &dingtalkim_1_0.UpdateRobotInteractiveCardHeaders{
XAcsDingtalkAccessToken: tea.String(authInfo.AccessToken),
}
response, err := client.UpdateRobotInteractiveCardWithOptions(request, headers, &util.RuntimeOptions{})
if err != nil {
return nil, fmt.Errorf("API call failed: %w,request:%v", err, request.String())
}
return response, nil
}
// SendInteractiveCard 发送交互卡片(封装错误处理)
func (s *SendCardClient) SendInteractiveCard(ctx context.Context, request *dingtalkim_1_0.SendRobotInteractiveCardRequest, robotCode string, client *dingtalkim_1_0.Client) (res *dingtalkim_1_0.SendRobotInteractiveCardResponse, err error) {
err = s.Auth.GetBotConfigFromModel(s.botOption)
if err != nil {
return nil, fmt.Errorf("初始化bot失败: %w", err)
}
authInfo, err := s.Auth.GetTokenFromBotOption(ctx, WithBot(s.botOption))
if err != nil {
return nil, fmt.Errorf("get token failed: %w", err)
}
headers := &dingtalkim_1_0.SendRobotInteractiveCardHeaders{
XAcsDingtalkAccessToken: tea.String(authInfo.AccessToken),
}
response, err := client.SendRobotInteractiveCardWithOptions(request, headers, &util.RuntimeOptions{})
if err != nil {
return nil, fmt.Errorf("API call failed: %w", err)
}
return response, nil
}