281 lines
8.9 KiB
Plaintext
281 lines
8.9 KiB
Plaintext
package dingtalk
|
||
|
||
import (
|
||
"ai_scheduler/internal/data/constants"
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||
dingtalkcard_1_0 "github.com/alibabacloud-go/dingtalk/card_1_0"
|
||
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) (*dingtalkcard_1_0.Client, error) {
|
||
if client, ok := s.CardClient.Load(robotCode); ok {
|
||
return client.(*dingtalkcard_1_0.Client), nil
|
||
}
|
||
s.botOption.BotCode = robotCode
|
||
config := &openapi.Config{
|
||
Protocol: tea.String("https"),
|
||
RegionId: tea.String("central"),
|
||
}
|
||
client, err := dingtalkcard_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 = "钉钉卡片"
|
||
}
|
||
//替换标题
|
||
cardSend.Template = constants.CardTemp(strings.Replace(string(cardSend.Template), "${title}", cardSend.Title, 1))
|
||
// 初始化客户端
|
||
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) (*dingtalkcard_1_0.StreamingUpdateRequest, error) {
|
||
cardData := fmt.Sprintf(string(cardSend.Template), "") // 初始空内容
|
||
request := &dingtalkcard_1_0.StreamingUpdateRequest{
|
||
OutTrackId: tea.String("your-out-track-id"),
|
||
Guid: tea.String("0F714542-0AFC-2B0E-CF14-E2D39F5BFFE8"),
|
||
Key: tea.String("your-ai-param"),
|
||
Content: tea.String("test"),
|
||
IsFull: tea.Bool(false),
|
||
IsFinalize: tea.Bool(false),
|
||
IsError: 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 {
|
||
|
||
updateRequest := &dingtalkim_1_0.UpdateRobotInteractiveCardRequest{
|
||
CardBizId: tea.String(updateCardRequest.CardInstanceId),
|
||
CardData: tea.String(fmt.Sprintf(updateCardRequest.Template, updateCardRequest.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
|
||
}
|