- Introduced Weixin integration support with new configuration options for WeChat iLink Bot. - Updated existing adapters (DingTalk, Discord, Feishu, Telegram) to include sender_id and app_id in message metadata for improved context handling. - Enhanced dispatcher logic to accommodate the new Weixin adapter and ensure proper initialization and shutdown processes. - Improved message handling across integrations to support typing indicators, providing a more interactive user experience.
138 lines
3.3 KiB
Go
138 lines
3.3 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
|
|
}
|
|
parts = append(parts, map[string]interface{}{
|
|
"type": "file",
|
|
"file_url": mi.Wrapper,
|
|
"mime_type": mi.MimeType,
|
|
"file_name": 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), "/")
|
|
}
|