feat(telegram): register bot commands asynchronously with retry on startup

This commit is contained in:
mingmxren 2026-03-01 02:29:31 +08:00
parent 041a40e804
commit 5db50ee5ca
3 changed files with 123 additions and 0 deletions

View file

@ -0,0 +1,84 @@
package telegram
import (
"context"
"time"
"github.com/mymmrac/telego"
"github.com/sipeed/picoclaw/pkg/commands"
"github.com/sipeed/picoclaw/pkg/logger"
)
var commandRegistrationBackoff = []time.Duration{
5 * time.Second,
15 * time.Second,
60 * time.Second,
5 * time.Minute,
10 * time.Minute,
}
// RegisterCommands registers bot commands on Telegram platform.
func (c *TelegramChannel) RegisterCommands(ctx context.Context, defs []commands.Definition) error {
botCommands := make([]telego.BotCommand, 0, len(defs))
for _, def := range defs {
if def.Name == "" || def.Description == "" {
continue
}
botCommands = append(botCommands, telego.BotCommand{
Command: def.Name,
Description: def.Description,
})
}
return c.bot.SetMyCommands(ctx, &telego.SetMyCommandsParams{
Commands: botCommands,
})
}
func (c *TelegramChannel) startCommandRegistration(ctx context.Context, defs []commands.Definition) {
if len(defs) == 0 {
return
}
register := c.registerFunc
if register == nil {
register = c.RegisterCommands
}
regCtx, cancel := context.WithCancel(ctx)
c.commandRegCancel = cancel
go func() {
attempt := 0
for {
err := register(regCtx, defs)
if err == nil {
logger.InfoCF("telegram", "Telegram commands registered", map[string]any{
"count": len(defs),
})
return
}
delay := commandRegistrationBackoff[minInt(attempt, len(commandRegistrationBackoff)-1)]
logger.WarnCF("telegram", "Telegram command registration failed; will retry", map[string]any{
"error": err.Error(),
"retry_after": delay.String(),
})
attempt++
select {
case <-regCtx.Done():
return
case <-time.After(delay):
}
}
}()
}
func minInt(a, b int) int {
if a < b {
return a
}
return b
}

View file

@ -0,0 +1,30 @@
package telegram
import (
"context"
"errors"
"testing"
"time"
"github.com/sipeed/picoclaw/pkg/commands"
)
func TestStartCommandRegistration_DoesNotBlock(t *testing.T) {
ch := &TelegramChannel{}
started := make(chan struct{}, 1)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch.registerFunc = func(context.Context, []commands.Definition) error {
started <- struct{}{}
return errors.New("temporary failure")
}
ch.startCommandRegistration(ctx, []commands.Definition{{Name: "help"}})
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("registration did not start asynchronously")
}
}

View file

@ -18,6 +18,7 @@ import (
"github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels" "github.com/sipeed/picoclaw/pkg/channels"
"github.com/sipeed/picoclaw/pkg/commands"
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/identity" "github.com/sipeed/picoclaw/pkg/identity"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/logger"
@ -47,6 +48,9 @@ type TelegramChannel struct {
chatIDs map[string]int64 chatIDs map[string]int64
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
registerFunc func(context.Context, []commands.Definition) error
commandRegCancel context.CancelFunc
} }
func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChannel, error) { func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChannel, error) {
@ -141,6 +145,8 @@ func (c *TelegramChannel) Start(ctx context.Context) error {
"username": c.bot.Username(), "username": c.bot.Username(),
}) })
c.startCommandRegistration(c.ctx, commands.NewRegistry(commands.BuiltinDefinitions(c.config)).ForChannel("telegram"))
go bh.Start() go bh.Start()
return nil return nil
@ -159,6 +165,9 @@ func (c *TelegramChannel) Stop(ctx context.Context) error {
if c.cancel != nil { if c.cancel != nil {
c.cancel() c.cancel()
} }
if c.commandRegCancel != nil {
c.commandRegCancel()
}
return nil return nil
} }