mirror of
https://github.com/HugeFrog24/go-telegram-bot.git
synced 2026-08-28 22:11:38 +00:00
Debounce and caching
This commit is contained in:
+93
-20
@@ -28,6 +28,9 @@ func (b *Bot) handleVoiceMessage(ctx context.Context, message *models.Message, u
|
||||
return
|
||||
}
|
||||
|
||||
stopTyping := b.startChatAction(ctx, chatID, businessConnectionID, models.ChatActionTyping)
|
||||
defer stopTyping()
|
||||
|
||||
transcript, err := b.transcribeVoice(ctx, message.Voice.FileID)
|
||||
if err != nil {
|
||||
ErrorLogger.Printf("Error transcribing voice message from user %d: %v", userID, err)
|
||||
@@ -62,6 +65,12 @@ func (b *Bot) handleVoiceMessage(ctx context.Context, message *models.Message, u
|
||||
return
|
||||
}
|
||||
|
||||
// Switch the indicator once the model is done and synthesis begins, so the
|
||||
// client shows "recording audio" rather than "typing" for a voice reply.
|
||||
stopTyping()
|
||||
stopRecording := b.startChatAction(ctx, chatID, businessConnectionID, models.ChatActionUploadVoice)
|
||||
defer stopRecording()
|
||||
|
||||
audioReader, err := b.generateSpeech(ctx, response)
|
||||
if err != nil {
|
||||
ErrorLogger.Printf("Error generating speech, falling back to text: %v", err)
|
||||
@@ -111,6 +120,11 @@ func (b *Bot) handlePhotoMessage(
|
||||
return
|
||||
}
|
||||
|
||||
// Covers the Files API uploads as well as the model turn; on an album this
|
||||
// is the longest wait in the bot.
|
||||
stopTyping := b.startChatAction(ctx, chatID, businessConnectionID, models.ChatActionTyping)
|
||||
defer stopTyping()
|
||||
|
||||
uploaded := make([]string, len(items))
|
||||
caption := ""
|
||||
g, gctx := errgroup.WithContext(ctx)
|
||||
@@ -188,6 +202,47 @@ func (b *Bot) handlePhotoMessage(
|
||||
}
|
||||
}
|
||||
|
||||
// respondToChat runs one assistant turn against the chat's current memory and
|
||||
// streams the reply back. Both the immediate path and the debounced flush go
|
||||
// through here, so a coalesced turn is byte-for-byte the same request as a
|
||||
// single-message one: the messages were already written to memory at intake, and
|
||||
// the model simply sees more of them.
|
||||
func (b *Bot) respondToChat(
|
||||
ctx context.Context,
|
||||
chatID, userID int64,
|
||||
isEmojiOnly bool,
|
||||
username, firstName, lastName string,
|
||||
isPremium bool,
|
||||
languageCode string,
|
||||
messageTime int,
|
||||
businessConnectionID string,
|
||||
) {
|
||||
stopTyping := b.startChatAction(ctx, chatID, businessConnectionID, models.ChatActionTyping)
|
||||
defer stopTyping()
|
||||
|
||||
chatMemory := b.getOrCreateChatMemory(chatID)
|
||||
contextMessages := b.prepareContextMessages(chatMemory)
|
||||
|
||||
joined, err := b.getAnthropicResponse(
|
||||
ctx, chatID, contextMessages, isEmojiOnly,
|
||||
username, firstName, lastName, isPremium, languageCode, messageTime,
|
||||
func(seg string) error {
|
||||
return b.sendOneSegment(ctx, chatID, seg, businessConnectionID)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
ErrorLogger.Printf("Error getting Anthropic response: %v", err)
|
||||
if sendErr := b.sendResponse(ctx, chatID, b.anthropicErrorResponse(err, userID), businessConnectionID); sendErr != nil {
|
||||
ErrorLogger.Printf("Error sending response: %v", sendErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if _, storeErr := b.screenOutgoingMessage(chatID, joined); storeErr != nil {
|
||||
ErrorLogger.Printf("Error recording assistant turn: %v", storeErr)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) anthropicErrorResponse(err error, userID int64) string {
|
||||
isElevated := b.hasScope(userID, ScopeModelSet)
|
||||
|
||||
@@ -268,12 +323,17 @@ func (b *Bot) handleUpdate(ctx context.Context, tgBot *bot.Bot, update *models.U
|
||||
}
|
||||
}
|
||||
|
||||
// Media never waits on the text debounce window. Cancelling here does not
|
||||
// discard the buffered text: those messages are already in chat memory, so
|
||||
// the turn this media triggers answers them too.
|
||||
if message.MediaGroupID != "" && len(message.Photo) > 0 {
|
||||
b.cancelIntake(chatID)
|
||||
b.bufferAlbumItem(ctx, message, chatID, userID, username, firstName, lastName,
|
||||
isPremium, languageCode, messageTime, businessConnectionID)
|
||||
return
|
||||
}
|
||||
if len(message.Photo) > 0 {
|
||||
b.cancelIntake(chatID)
|
||||
if !b.checkRateLimits(userID) {
|
||||
b.sendRateLimitExceededMessage(ctx, chatID, businessConnectionID)
|
||||
return
|
||||
@@ -422,14 +482,14 @@ func (b *Bot) handleUpdate(ctx context.Context, tgBot *bot.Bot, update *models.U
|
||||
}
|
||||
|
||||
if message.Voice != nil {
|
||||
b.cancelIntake(chatID)
|
||||
b.handleVoiceMessage(ctx, message, userMsg, chatID, userID, username, firstName, lastName, isPremium, languageCode, messageTime, businessConnectionID)
|
||||
return
|
||||
}
|
||||
|
||||
chatMemory := b.getOrCreateChatMemory(chatID)
|
||||
contextMessages := b.prepareContextMessages(chatMemory)
|
||||
|
||||
if message.Sticker != nil {
|
||||
b.cancelIntake(chatID)
|
||||
contextMessages := b.prepareContextMessages(b.getOrCreateChatMemory(chatID))
|
||||
b.handleStickerMessage(ctx, chatID, userMsg, message, contextMessages, businessConnectionID)
|
||||
return
|
||||
}
|
||||
@@ -441,24 +501,18 @@ func (b *Bot) handleUpdate(ctx context.Context, tgBot *bot.Bot, update *models.U
|
||||
|
||||
isEmojiOnly := isOnlyEmojis(text)
|
||||
|
||||
joined, err := b.getAnthropicResponse(
|
||||
ctx, chatID, contextMessages, isEmojiOnly,
|
||||
username, firstName, lastName, isPremium, languageCode, messageTime,
|
||||
func(seg string) error {
|
||||
return b.sendOneSegment(ctx, chatID, seg, businessConnectionID)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
ErrorLogger.Printf("Error getting Anthropic response: %v", err)
|
||||
if sendErr := b.sendResponse(ctx, chatID, b.anthropicErrorResponse(err, userID), businessConnectionID); sendErr != nil {
|
||||
ErrorLogger.Printf("Error sending response: %v", sendErr)
|
||||
}
|
||||
// Plain text is the only thing that debounces: it is what users fragment
|
||||
// across several sends, and it is the only kind whose meaning survives being
|
||||
// read as one turn.
|
||||
if b.config.DebounceWindow() > 0 {
|
||||
b.bufferIntake(ctx, chatID, userID, username, firstName, lastName,
|
||||
isPremium, languageCode, messageTime, businessConnectionID, isEmojiOnly)
|
||||
return
|
||||
}
|
||||
|
||||
if _, storeErr := b.screenOutgoingMessage(chatID, joined); storeErr != nil {
|
||||
ErrorLogger.Printf("Error recording assistant turn: %v", storeErr)
|
||||
}
|
||||
b.respondToChat(ctx, chatID, userID, isEmojiOnly,
|
||||
username, firstName, lastName, isPremium, languageCode, messageTime,
|
||||
businessConnectionID)
|
||||
}
|
||||
|
||||
func (b *Bot) sendRateLimitExceededMessage(ctx context.Context, chatID int64, businessConnectionID string) {
|
||||
@@ -469,7 +523,7 @@ func (b *Bot) sendRateLimitExceededMessage(ctx context.Context, chatID int64, bu
|
||||
|
||||
func (b *Bot) handleStickerMessage(ctx context.Context, chatID int64, userMessage Message, message *models.Message, contextMessages []anthropic.BetaMessageParam, businessConnectionID string) {
|
||||
|
||||
response, err := b.generateStickerResponse(ctx, userMessage, contextMessages)
|
||||
response, err := b.generateStickerResponse(ctx, userMessage, contextMessages, businessConnectionID)
|
||||
if err != nil {
|
||||
ErrorLogger.Printf("Error generating sticker response: %v", err)
|
||||
if message.Sticker.IsAnimated {
|
||||
@@ -487,7 +541,10 @@ func (b *Bot) handleStickerMessage(ctx context.Context, chatID int64, userMessag
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) generateStickerResponse(ctx context.Context, message Message, contextMessages []anthropic.BetaMessageParam) (string, error) {
|
||||
func (b *Bot) generateStickerResponse(ctx context.Context, message Message, contextMessages []anthropic.BetaMessageParam, businessConnectionID string) (string, error) {
|
||||
stopTyping := b.startChatAction(ctx, message.ChatID, businessConnectionID, models.ChatActionTyping)
|
||||
defer stopTyping()
|
||||
|
||||
if message.StickerFileID != "" {
|
||||
messageTime := int(message.Timestamp.Unix())
|
||||
response, err := b.getAnthropicResponse(ctx, message.ChatID, contextMessages, true, message.Username, "", "", false, "", messageTime, nil)
|
||||
@@ -569,6 +626,22 @@ func (b *Bot) clearChatHistory(ctx context.Context, chatID int64, currentUserID
|
||||
return
|
||||
}
|
||||
|
||||
// Drop any armed intake buffer for the same chat before clearing memory.
|
||||
// Otherwise the debounce timer fires moments later and repopulates the chat
|
||||
// with the very messages that were just deleted — the openclaw/openclaw#51046
|
||||
// failure mode, but with a privacy consequence rather than a stray reply.
|
||||
clearedChatID := chatID
|
||||
if targetUserID != currentUserID {
|
||||
clearedChatID = targetChatID
|
||||
if clearedChatID == 0 {
|
||||
clearedChatID = targetUserID
|
||||
}
|
||||
}
|
||||
if discarded := b.cancelIntake(clearedChatID); discarded > 0 {
|
||||
InfoLogger.Printf("[%s] discarded %d buffered message(s) for chat %d on history clear",
|
||||
b.config.ID, discarded, clearedChatID)
|
||||
}
|
||||
|
||||
b.chatMemoriesMu.Lock()
|
||||
if targetUserID == currentUserID {
|
||||
delete(b.chatMemories, chatID)
|
||||
|
||||
Reference in New Issue
Block a user