yao/agent/robot/events/integrations/weixin/polling.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

173 lines
4.1 KiB
Go

package weixin
import (
"context"
"fmt"
"strings"
"time"
agentcontext "github.com/yaoapp/yao/agent/context"
events "github.com/yaoapp/yao/agent/robot/events"
"github.com/yaoapp/yao/event"
weixinapi "github.com/yaoapp/yao/integrations/weixin"
)
const (
maxConsecutiveFailures = 3
backoffDuration = 30 * time.Second
retryDuration = 2 * time.Second
sessionPauseDuration = 30 * time.Minute
defaultTimeoutMs = 35_000
)
func (a *Adapter) pollLoop(ctx context.Context, entry *botEntry) {
syncBuf := loadSyncBuf(entry.accountID)
nextTimeoutMs := defaultTimeoutMs
failures := 0
for {
select {
case <-ctx.Done():
return
default:
}
resp, err := entry.bot.GetUpdates(ctx, syncBuf, nextTimeoutMs)
if err != nil {
if ctx.Err() != nil {
return
}
failures++
if failures >= maxConsecutiveFailures {
failures = 0
sleep(ctx, backoffDuration)
} else {
sleep(ctx, retryDuration)
}
continue
}
if resp.ErrCode == weixinapi.SessionExpiredErrCode || resp.Ret == weixinapi.SessionExpiredErrCode {
log.Warn("weixin session expired, pausing %s robot=%s", sessionPauseDuration, entry.robotID)
failures = 0
sleep(ctx, sessionPauseDuration)
continue
}
isApiError := (resp.Ret != 0) || (resp.ErrCode != 0)
if isApiError {
failures++
if failures >= maxConsecutiveFailures {
failures = 0
sleep(ctx, backoffDuration)
} else {
sleep(ctx, retryDuration)
}
continue
}
failures = 0
if resp.LongPollingTimeoutMs > 0 {
nextTimeoutMs = resp.LongPollingTimeoutMs
}
if resp.GetUpdatesBuf != "" && resp.GetUpdatesBuf != syncBuf {
syncBuf = resp.GetUpdatesBuf
saveSyncBuf(entry.accountID, syncBuf)
}
for i := range resp.Msgs {
a.handleMessage(ctx, entry, &resp.Msgs[i])
}
}
}
func (a *Adapter) handleMessage(ctx context.Context, entry *botEntry, msg *weixinapi.WeixinMessage) {
var dedupKey string
switch {
case msg.MessageID != 0:
dedupKey = fmt.Sprintf("wx:%s:mid:%d", entry.robotID, msg.MessageID)
case msg.Seq != 0:
dedupKey = fmt.Sprintf("wx:%s:seq:%d", entry.robotID, msg.Seq)
default:
dedupKey = fmt.Sprintf("wx:%s:%s:%d", entry.robotID, msg.FromUserID, msg.CreateTimeMs)
}
if !a.dedup.markSeen(dedupKey) {
return
}
log.Info("incoming msg from=%s context_token=%s", msg.FromUserID, msg.ContextToken)
groups := []string{"weixin", entry.accountID}
content, mediaItems := convertMessage(ctx, entry.bot, msg.ItemList, groups)
if content == "" && len(mediaItems) == 0 {
return
}
var msgContent interface{}
if len(mediaItems) == 0 {
msgContent = content
} else {
parts := make([]interface{}, 0, 1+len(mediaItems))
if content != "" {
parts = append(parts, map[string]interface{}{"type": "text", "text": content})
}
for _, m := range mediaItems {
if strings.HasPrefix(m.MimeType, "image/") {
parts = append(parts, map[string]interface{}{
"type": "image_url",
"image_url": map[string]interface{}{
"url": m.Wrapper,
"detail": "auto",
},
})
} else {
parts = append(parts, map[string]interface{}{
"type": "file",
"file": map[string]interface{}{
"url": m.Wrapper,
"filename": m.FileName,
},
})
}
}
msgContent = parts
}
messageID := ""
if msg.MessageID != 0 {
messageID = fmt.Sprintf("%d", msg.MessageID)
}
payload := events.MessagePayload{
RobotID: entry.robotID,
Messages: []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: msgContent},
},
Metadata: &events.MessageMetadata{
Channel: "weixin",
MessageID: messageID,
AppID: entry.accountID,
ChatID: msg.FromUserID,
SenderID: msg.FromUserID,
Locale: "zh-cn",
Extra: map[string]any{
"context_token": msg.ContextToken,
"sender_id": msg.FromUserID,
"app_id": entry.accountID,
},
},
}
if _, err := event.Push(ctx, events.Message, payload); err != nil {
log.Error("weixin adapter: event.Push failed robot=%s: %v", entry.robotID, err)
}
}
func sleep(ctx context.Context, d time.Duration) {
select {
case <-ctx.Done():
case <-time.After(d):
}
}