fix(commands): address review feedback on parsing and retry flow

This commit is contained in:
mingmxren 2026-03-03 16:54:51 +08:00
parent 554aa40d09
commit b4471d6394
6 changed files with 111 additions and 7 deletions

View file

@ -53,6 +53,14 @@ func (c *TelegramChannel) startCommandRegistration(ctx context.Context, defs []c
// by temporary upstream API failures. Retry stops on success or channel shutdown.
go func() {
attempt := 0
timer := time.NewTimer(0)
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
defer timer.Stop()
for {
err := register(regCtx, defs)
if err == nil {
@ -64,15 +72,23 @@ func (c *TelegramChannel) startCommandRegistration(ctx context.Context, defs []c
delay := commandRegistrationBackoff[min(attempt, len(commandRegistrationBackoff)-1)]
logger.WarnCF("telegram", "Telegram command registration failed; will retry", map[string]any{
"error": err.Error(),
"error": err.Error(),
"retry_after": delay.String(),
})
attempt++
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
timer.Reset(delay)
select {
case <-regCtx.Done():
return
case <-time.After(delay):
case <-timer.C:
}
}
}()

View file

@ -53,6 +53,11 @@ func (c *TelegramChannel) dispatchCommand(ctx context.Context, message telego.Me
"error": res.Err.Error(),
})
}
if res.Matched && !res.Handled {
logger.DebugCF("telegram", "Command matched without handler; passing to normal flow", map[string]any{
"command": res.Command,
})
}
return true
}

View file

@ -278,6 +278,11 @@ func (c *WhatsAppChannel) tryHandleCommand(
"error": res.Err.Error(),
})
}
if res.Matched && !res.Handled {
logger.DebugCF("whatsapp", "Command matched without handler; passing to normal flow", map[string]any{
"command": res.Command,
})
}
return res.Matched
}

View file

@ -422,6 +422,11 @@ func (c *WhatsAppNativeChannel) tryHandleCommand(
"error": res.Err.Error(),
})
}
if res.Matched && !res.Handled {
logger.DebugCF("whatsapp", "Command matched without handler; passing to normal flow", map[string]any{
"command": res.Command,
})
}
return res.Matched
}

View file

@ -37,6 +37,8 @@ func (f DispatchFunc) Dispatch(ctx context.Context, req Request) Result {
return f(ctx, req)
}
var commandPrefixes = []string{"/", "!"}
// NewDispatcher binds the unified parser/executor flow to one command registry.
func NewDispatcher(reg *Registry) *Dispatcher {
return &Dispatcher{reg: reg}
@ -52,7 +54,7 @@ func (d *Dispatcher) Dispatch(ctx context.Context, req Request) Result {
}
for _, def := range d.reg.Definitions() {
if def.Name != cmdName && !contains(def.Aliases, cmdName) {
if !matchesCommand(def, cmdName) {
continue
}
if def.Handler == nil {
@ -73,24 +75,53 @@ func firstToken(input string) string {
return parts[0]
}
// parseCommandName accepts both "/name" and "/name@bot", then normalizes to "name".
// parseCommandName accepts "/name", "!name", and Telegram's "/name@bot", then
// normalizes to lowercase command names.
func parseCommandName(input string) (string, bool) {
token := firstToken(input)
if token == "" || !strings.HasPrefix(token, "/") {
if token == "" {
return "", false
}
name := strings.TrimPrefix(token, "/")
name, ok := trimCommandPrefix(token)
if !ok {
return "", false
}
if i := strings.Index(name, "@"); i >= 0 {
name = name[:i]
}
name = strings.TrimSpace(name)
name = normalizeCommandName(name)
if name == "" {
return "", false
}
return name, true
}
func trimCommandPrefix(token string) (string, bool) {
for _, prefix := range commandPrefixes {
if strings.HasPrefix(token, prefix) {
return strings.TrimPrefix(token, prefix), true
}
}
return "", false
}
func normalizeCommandName(name string) string {
return strings.ToLower(strings.TrimSpace(name))
}
func matchesCommand(def Definition, cmdName string) bool {
if normalizeCommandName(def.Name) == cmdName {
return true
}
for _, alias := range def.Aliases {
if normalizeCommandName(alias) == cmdName {
return true
}
}
return false
}
func contains(items []string, target string) bool {
for _, item := range items {
if item == target {

View file

@ -59,3 +59,45 @@ func TestDispatcher_MatchTelegramMentionSyntax(t *testing.T) {
t.Fatalf("dispatch result = %+v, called=%v", res, called)
}
}
func TestDispatcher_MatchBangPrefix(t *testing.T) {
called := false
d := NewDispatcher(NewRegistry([]Definition{
{
Name: "help",
Handler: func(context.Context, Request) error {
called = true
return nil
},
},
}))
res := d.Dispatch(context.Background(), Request{
Channel: "telegram",
Text: "!help",
})
if !res.Matched || !res.Handled || !called || res.Err != nil {
t.Fatalf("dispatch result = %+v, called=%v", res, called)
}
}
func TestDispatcher_CommandMatchingIsCaseInsensitive(t *testing.T) {
called := false
d := NewDispatcher(NewRegistry([]Definition{
{
Name: "show",
Handler: func(context.Context, Request) error {
called = true
return nil
},
},
}))
res := d.Dispatch(context.Background(), Request{
Channel: "telegram",
Text: "/SHOW",
})
if !res.Matched || !res.Handled || !called || res.Err != nil {
t.Fatalf("dispatch result = %+v, called=%v", res, called)
}
}