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. // by temporary upstream API failures. Retry stops on success or channel shutdown.
go func() { go func() {
attempt := 0 attempt := 0
timer := time.NewTimer(0)
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
defer timer.Stop()
for { for {
err := register(regCtx, defs) err := register(regCtx, defs)
if err == nil { if err == nil {
@ -69,10 +77,18 @@ func (c *TelegramChannel) startCommandRegistration(ctx context.Context, defs []c
}) })
attempt++ attempt++
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
timer.Reset(delay)
select { select {
case <-regCtx.Done(): case <-regCtx.Done():
return 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(), "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 return true
} }

View file

@ -278,6 +278,11 @@ func (c *WhatsAppChannel) tryHandleCommand(
"error": res.Err.Error(), "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 return res.Matched
} }

View file

@ -422,6 +422,11 @@ func (c *WhatsAppNativeChannel) tryHandleCommand(
"error": res.Err.Error(), "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 return res.Matched
} }

View file

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

View file

@ -59,3 +59,45 @@ func TestDispatcher_MatchTelegramMentionSyntax(t *testing.T) {
t.Fatalf("dispatch result = %+v, called=%v", res, called) 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)
}
}