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 }