feat: 钉钉消息添加表情回复 TypingIndicator 支持
收到消息自动贴 👌 表情,回复完成贴 ✅ 表情。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
@@ -31,7 +32,8 @@ type replyContext struct {
|
||||
conversationId string
|
||||
senderStaffId string
|
||||
isGroup bool
|
||||
proactive bool // true when constructed by ReconstructReplyCtx (no sessionWebhook)
|
||||
proactive bool // true when constructed by ReconstructReplyCtx (no sessionWebhook)
|
||||
msgId string // message ID, used for emotion reactions
|
||||
}
|
||||
|
||||
// richTextContent mirrors the full structure of the DingTalk "text" JSON field,
|
||||
@@ -299,6 +301,7 @@ func (p *Platform) onMessage(data *chatbot.BotCallbackDataModel, richText *richT
|
||||
sessionWebhook: data.SessionWebhook,
|
||||
conversationId: data.ConversationId,
|
||||
senderStaffId: data.SenderStaffId,
|
||||
msgId: data.MsgId,
|
||||
},
|
||||
}
|
||||
p.handler(p, msg)
|
||||
@@ -335,6 +338,7 @@ func (p *Platform) onMessage(data *chatbot.BotCallbackDataModel, richText *richT
|
||||
conversationId: data.ConversationId,
|
||||
senderStaffId: data.SenderStaffId,
|
||||
isGroup: data.ConversationType == "2",
|
||||
msgId: data.MsgId,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -414,6 +418,7 @@ func (p *Platform) handleAudioMessage(data *chatbot.BotCallbackDataModel, sessio
|
||||
conversationId: data.ConversationId,
|
||||
senderStaffId: data.SenderStaffId,
|
||||
isGroup: data.ConversationType == "2",
|
||||
msgId: data.MsgId,
|
||||
},
|
||||
FromVoice: true,
|
||||
}
|
||||
@@ -438,6 +443,7 @@ func (p *Platform) handleAudioMessage(data *chatbot.BotCallbackDataModel, sessio
|
||||
conversationId: data.ConversationId,
|
||||
senderStaffId: data.SenderStaffId,
|
||||
isGroup: data.ConversationType == "2",
|
||||
msgId: data.MsgId,
|
||||
},
|
||||
FromVoice: true,
|
||||
Audio: &core.AudioAttachment{
|
||||
@@ -513,6 +519,7 @@ func (p *Platform) handleImageMessage(data *chatbot.BotCallbackDataModel, sessio
|
||||
sessionWebhook: data.SessionWebhook,
|
||||
conversationId: data.ConversationId,
|
||||
senderStaffId: data.SenderStaffId,
|
||||
msgId: data.MsgId,
|
||||
},
|
||||
Images: []core.ImageAttachment{{
|
||||
MimeType: mimeType,
|
||||
@@ -832,6 +839,8 @@ func (p *Platform) SendImage(ctx context.Context, rctx any, img core.ImageAttach
|
||||
var _ core.ImageSender = (*Platform)(nil)
|
||||
var _ core.StreamingCardPlatform = (*Platform)(nil)
|
||||
var _ core.ReplyContextReconstructor = (*Platform)(nil)
|
||||
var _ core.TypingIndicator = (*Platform)(nil)
|
||||
var _ core.TypingIndicatorDone = (*Platform)(nil)
|
||||
|
||||
// CreateStreamingCard creates a new streaming card for the given reply context.
|
||||
// Implements core.StreamingCardPlatform.
|
||||
@@ -1456,6 +1465,92 @@ func (p *Platform) sendProactiveMessage(ctx context.Context, rc replyContext, co
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartTyping adds a "thinking" emoji reaction to the user's message.
|
||||
// Implements core.TypingIndicator.
|
||||
func (p *Platform) StartTyping(ctx context.Context, replyCtx any) func() {
|
||||
rc, ok := replyCtx.(replyContext)
|
||||
if !ok || rc.msgId == "" || rc.conversationId == "" {
|
||||
return func() {}
|
||||
}
|
||||
|
||||
err := p.addEmotion(ctx, rc.msgId, rc.conversationId, "👌", "")
|
||||
if err != nil {
|
||||
slog.Warn("dingtalk: failed to add thinking reaction", "error", err)
|
||||
}
|
||||
|
||||
return func() {
|
||||
log.Println("回答完毕")
|
||||
}
|
||||
}
|
||||
|
||||
// AddDoneReaction adds a done emoji on top of the thinking emoji.
|
||||
// Implements core.TypingIndicatorDone.
|
||||
func (p *Platform) AddDoneReaction(replyCtx any) {
|
||||
rc, ok := replyCtx.(replyContext)
|
||||
if !ok || rc.msgId == "" || rc.conversationId == "" {
|
||||
return
|
||||
}
|
||||
|
||||
cctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Add done emoji
|
||||
if err := p.addEmotion(cctx, rc.msgId, rc.conversationId, "✅", ""); err != nil {
|
||||
slog.Debug("dingtalk: failed to add done reaction", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// addEmotion adds an emoji reaction to a message via DingTalk's emotion API.
|
||||
func (p *Platform) addEmotion(ctx context.Context, msgId, convId, emoji, _ string) error {
|
||||
token, err := p.getAccessToken()
|
||||
if err != nil {
|
||||
return fmt.Errorf("dingtalk: addEmotion get token: %w", err)
|
||||
}
|
||||
|
||||
body := map[string]any{
|
||||
"robotCode": p.robotCode,
|
||||
"openMsgId": msgId,
|
||||
"openConversationId": convId,
|
||||
"emotionType": 2,
|
||||
"emotionName": emoji,
|
||||
"textEmotion": map[string]string{
|
||||
"emotionName": emoji,
|
||||
"text": emoji,
|
||||
"backgroundId": "im_bg_2",
|
||||
},
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dingtalk: addEmotion marshal: %w", err)
|
||||
}
|
||||
|
||||
reqCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
req, err := http.NewRequestWithContext(reqCtx, http.MethodPost,
|
||||
"https://api.dingtalk.com/v1.0/robot/emotion/reply",
|
||||
bytes.NewReader(bodyBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("dingtalk: addEmotion create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("x-acs-dingtalk-access-token", token)
|
||||
|
||||
resp, err := p.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dingtalk: addEmotion do request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("dingtalk: addEmotion failed: status=%d, body=%s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// preprocessDingTalkMarkdown adapts content for DingTalk's markdown renderer:
|
||||
// - Leading spaces → non-breaking spaces (prevents markdown from stripping indentation)
|
||||
// - Single \n between non-empty lines → trailing two-space forced line break
|
||||
|
||||
Reference in New Issue
Block a user