yao/agent/robot/events/integrations/telegram/message.go
Max 421d946971 feat(buffer): refine step retrieval for resume functionality
- Updated the GetStepsForResume method to return only steps with valid resume statuses (failed or interrupted), improving recovery context handling.
- Enhanced error handling in message processing across various integrations, including friendly error messages for users in different locales.
- Modified file handling in message integrations (Dingtalk, Discord, Feishu, Telegram, Weixin) to differentiate between image and file types, ensuring appropriate content delivery.
2026-05-08 10:14:26 +08:00

149 lines
3.6 KiB
Go

package telegram
import (
"context"
"fmt"
"strconv"
"strings"
agentcontext "github.com/yaoapp/yao/agent/context"
events "github.com/yaoapp/yao/agent/robot/events"
"github.com/yaoapp/yao/event"
tgapi "github.com/yaoapp/yao/integrations/telegram"
)
// handleMessages builds a single event payload from a batch of ConvertedMessages
// belonging to the same chat. Consecutive user messages are merged into one to
// keep the messages array clean for the LLM.
func (a *Adapter) handleMessages(ctx context.Context, entry *botEntry, cms []*tgapi.ConvertedMessage) {
if len(cms) == 0 {
return
}
var allParts []interface{}
var lastCM *tgapi.ConvertedMessage
for _, cm := range cms {
if cm == nil {
continue
}
if isBotCommand(cm) {
continue
}
dedupKey := fmt.Sprintf("tg:%s:%d", entry.robotID, cm.UpdateID)
if !a.dedup.markSeen(dedupKey) {
continue
}
parts := buildContentParts(cm)
if len(parts) == 0 {
continue
}
allParts = append(allParts, parts...)
lastCM = cm
}
if len(allParts) == 0 || lastCM == nil {
return
}
content := mergeContentParts(allParts)
msgPayload := events.MessagePayload{
RobotID: entry.robotID,
Messages: []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: content},
},
Metadata: &events.MessageMetadata{
Channel: "telegram",
MessageID: strconv.FormatInt(lastCM.UpdateID, 10),
AppID: entry.appID,
ChatID: strconv.FormatInt(lastCM.ChatID, 10),
SenderID: strconv.FormatInt(lastCM.SenderID, 10),
SenderName: lastCM.SenderName,
Locale: events.NormalizeLocale(lastCM.LanguageCode),
Extra: map[string]any{
"tg_message_id": lastCM.MessageID,
"sender_id": strconv.FormatInt(lastCM.SenderID, 10),
"app_id": entry.appID,
},
},
}
if _, err := event.Push(ctx, events.Message, msgPayload); err != nil {
log.Error("telegram adapter: event.Push robot.message failed robot=%s: %v", entry.robotID, err)
}
}
// buildContentParts extracts content parts from a single ConvertedMessage.
func buildContentParts(cm *tgapi.ConvertedMessage) []interface{} {
var parts []interface{}
if cm.HasText() {
parts = append(parts, map[string]interface{}{
"type": "text",
"text": cm.Text,
})
}
for _, mi := range cm.MediaItems {
if mi.Wrapper == "" {
continue
}
if strings.HasPrefix(mi.MimeType, "image/") {
parts = append(parts, map[string]interface{}{
"type": "image_url",
"image_url": map[string]interface{}{
"url": mi.Wrapper,
"detail": "auto",
},
})
} else {
parts = append(parts, map[string]interface{}{
"type": "file",
"file": map[string]interface{}{
"url": mi.Wrapper,
"filename": mi.FileName,
},
})
}
}
return parts
}
// mergeContentParts merges collected parts into a single content value.
// If all parts are text-only, they are joined with newlines into a plain string.
// Otherwise the full parts array is returned.
func mergeContentParts(parts []interface{}) interface{} {
allText := true
for _, p := range parts {
m, ok := p.(map[string]interface{})
if !ok || m["type"] != "text" {
allText = false
break
}
}
if allText {
var buf strings.Builder
for i, p := range parts {
if i > 0 {
buf.WriteString("\n")
}
m := p.(map[string]interface{})
buf.WriteString(m["text"].(string))
}
return buf.String()
}
return parts
}
// isBotCommand returns true if the message is a Telegram bot command (text starting with "/").
func isBotCommand(cm *tgapi.ConvertedMessage) bool {
return !cm.HasMedia() && strings.HasPrefix(strings.TrimSpace(cm.Text), "/")
}