refactor(commands): address code review findings

- Remove dead ExecuteResult.Reply field and unused branch in loop.go
- Extract shared agentsHandler for /show agents and /list agents
- Remove redundant firstToken/secondToken (use nthToken instead)
- Simplify Telegram startup: pass BuiltinDefinitions directly
- Centralize req.Reply nil guard in executeDefinition
- Extract unavailableMsg constant (was duplicated 5 times)
- Remove unused MessageID from Request
- Remove stale "reserved for Phase 2" comment on Deps.Config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mingmxren 2026-03-03 23:38:42 +08:00
parent ef861488ab
commit c2fc9f3e2a
12 changed files with 47 additions and 96 deletions

View file

@ -1533,9 +1533,6 @@ func (al *AgentLoop) handleCommand(
if commandReply != "" { if commandReply != "" {
return commandReply, true return commandReply, true
} }
if result.Reply != "" {
return result.Reply, true
}
return "", true return "", true
default: default:
return "", false return "", false

View file

@ -130,7 +130,7 @@ func (c *TelegramChannel) Start(ctx context.Context) error {
"username": c.bot.Username(), "username": c.bot.Username(),
}) })
c.startCommandRegistration(c.ctx, commands.NewRegistry(commands.BuiltinDefinitions(&commands.Deps{Config: c.config})).Definitions()) c.startCommandRegistration(c.ctx, commands.BuiltinDefinitions(&commands.Deps{Config: c.config}))
go func() { go func() {
if err = bh.Start(); err != nil { if err = bh.Start(); err != nil {

View file

@ -0,0 +1,21 @@
package commands
import (
"context"
"fmt"
"strings"
)
// agentsHandler returns a shared handler for both /show agents and /list agents.
func agentsHandler(deps *Deps) Handler {
return func(_ context.Context, req Request) error {
if deps.ListAgentIDs == nil {
return req.Reply(unavailableMsg)
}
ids := deps.ListAgentIDs()
if len(ids) == 0 {
return req.Reply("No agents registered")
}
return req.Reply(fmt.Sprintf("Registered agents: %s", strings.Join(ids, ", ")))
}
}

View file

@ -12,9 +12,6 @@ func helpCommand(deps *Deps) Definition {
Description: "Show this help message", Description: "Show this help message",
Usage: "/help", Usage: "/help",
Handler: func(_ context.Context, req Request) error { Handler: func(_ context.Context, req Request) error {
if req.Reply == nil {
return nil
}
defs := BuiltinDefinitions(deps) defs := BuiltinDefinitions(deps)
return req.Reply(formatHelpMessage(defs)) return req.Reply(formatHelpMessage(defs))
}, },

View file

@ -15,11 +15,8 @@ func listCommand(deps *Deps) Definition {
Name: "models", Name: "models",
Description: "Configured models", Description: "Configured models",
Handler: func(_ context.Context, req Request) error { Handler: func(_ context.Context, req Request) error {
if req.Reply == nil {
return nil
}
if deps.GetModelInfo == nil { if deps.GetModelInfo == nil {
return req.Reply("Command unavailable in current context.") return req.Reply(unavailableMsg)
} }
name, provider := deps.GetModelInfo() name, provider := deps.GetModelInfo()
if provider == "" { if provider == "" {
@ -35,11 +32,8 @@ func listCommand(deps *Deps) Definition {
Name: "channels", Name: "channels",
Description: "Enabled channels", Description: "Enabled channels",
Handler: func(_ context.Context, req Request) error { Handler: func(_ context.Context, req Request) error {
if req.Reply == nil {
return nil
}
if deps.GetEnabledChannels == nil { if deps.GetEnabledChannels == nil {
return req.Reply("Command unavailable in current context.") return req.Reply(unavailableMsg)
} }
enabled := deps.GetEnabledChannels() enabled := deps.GetEnabledChannels()
if len(enabled) == 0 { if len(enabled) == 0 {
@ -51,19 +45,7 @@ func listCommand(deps *Deps) Definition {
{ {
Name: "agents", Name: "agents",
Description: "Registered agents", Description: "Registered agents",
Handler: func(_ context.Context, req Request) error { Handler: agentsHandler(deps),
if req.Reply == nil {
return nil
}
if deps.ListAgentIDs == nil {
return req.Reply("Command unavailable in current context.")
}
ids := deps.ListAgentIDs()
if len(ids) == 0 {
return req.Reply("No agents registered")
}
return req.Reply(fmt.Sprintf("Registered agents: %s", strings.Join(ids, ", ")))
},
}, },
}, },
} }

View file

@ -3,7 +3,6 @@ package commands
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
) )
func showCommand(deps *Deps) Definition { func showCommand(deps *Deps) Definition {
@ -15,11 +14,8 @@ func showCommand(deps *Deps) Definition {
Name: "model", Name: "model",
Description: "Current model and provider", Description: "Current model and provider",
Handler: func(_ context.Context, req Request) error { Handler: func(_ context.Context, req Request) error {
if req.Reply == nil {
return nil
}
if deps.GetModelInfo == nil { if deps.GetModelInfo == nil {
return req.Reply("Command unavailable in current context.") return req.Reply(unavailableMsg)
} }
name, provider := deps.GetModelInfo() name, provider := deps.GetModelInfo()
return req.Reply(fmt.Sprintf("Current Model: %s (Provider: %s)", name, provider)) return req.Reply(fmt.Sprintf("Current Model: %s (Provider: %s)", name, provider))
@ -29,28 +25,13 @@ func showCommand(deps *Deps) Definition {
Name: "channel", Name: "channel",
Description: "Current channel", Description: "Current channel",
Handler: func(_ context.Context, req Request) error { Handler: func(_ context.Context, req Request) error {
if req.Reply == nil {
return nil
}
return req.Reply(fmt.Sprintf("Current Channel: %s", req.Channel)) return req.Reply(fmt.Sprintf("Current Channel: %s", req.Channel))
}, },
}, },
{ {
Name: "agents", Name: "agents",
Description: "Registered agents", Description: "Registered agents",
Handler: func(_ context.Context, req Request) error { Handler: agentsHandler(deps),
if req.Reply == nil {
return nil
}
if deps.ListAgentIDs == nil {
return req.Reply("Command unavailable in current context.")
}
ids := deps.ListAgentIDs()
if len(ids) == 0 {
return req.Reply("No agents registered")
}
return req.Reply(fmt.Sprintf("Registered agents: %s", strings.Join(ids, ", ")))
},
}, },
}, },
} }

View file

@ -8,9 +8,6 @@ func startCommand() Definition {
Description: "Start the bot", Description: "Start the bot",
Usage: "/start", Usage: "/start",
Handler: func(_ context.Context, req Request) error { Handler: func(_ context.Context, req Request) error {
if req.Reply == nil {
return nil
}
return req.Reply("Hello! I am PicoClaw 🦞") return req.Reply("Hello! I am PicoClaw 🦞")
}, },
} }

View file

@ -15,11 +15,8 @@ func switchCommand(deps *Deps) Definition {
Description: "Switch to a different model", Description: "Switch to a different model",
ArgsUsage: "to <name>", ArgsUsage: "to <name>",
Handler: func(_ context.Context, req Request) error { Handler: func(_ context.Context, req Request) error {
if req.Reply == nil {
return nil
}
if deps.SwitchModel == nil { if deps.SwitchModel == nil {
return req.Reply("Command unavailable in current context.") return req.Reply(unavailableMsg)
} }
// Parse: /switch model to <value> // Parse: /switch model to <value>
value := nthToken(req.Text, 3) // tokens: [/switch, model, to, <value>] value := nthToken(req.Text, 3) // tokens: [/switch, model, to, <value>]
@ -38,11 +35,8 @@ func switchCommand(deps *Deps) Definition {
Description: "Switch to a different channel", Description: "Switch to a different channel",
ArgsUsage: "to <name>", ArgsUsage: "to <name>",
Handler: func(_ context.Context, req Request) error { Handler: func(_ context.Context, req Request) error {
if req.Reply == nil {
return nil
}
if deps.SwitchChannel == nil { if deps.SwitchChannel == nil {
return req.Reply("Command unavailable in current context.") return req.Reply(unavailableMsg)
} }
value := nthToken(req.Text, 3) value := nthToken(req.Text, 3)
if nthToken(req.Text, 2) != "to" || value == "" { if nthToken(req.Text, 2) != "to" || value == "" {

View file

@ -7,7 +7,7 @@ import "github.com/sipeed/picoclaw/pkg/config"
// invocation time, not at construction time, so late-bound values // invocation time, not at construction time, so late-bound values
// (e.g. channelManager set after NewAgentLoop) are visible. // (e.g. channelManager set after NewAgentLoop) are visible.
type Deps struct { type Deps struct {
Config *config.Config // reserved for Phase 2 session commands Config *config.Config
GetModelInfo func() (name, provider string) GetModelInfo func() (name, provider string)
ListAgentIDs func() []string ListAgentIDs func() []string
GetEnabledChannels func() []string GetEnabledChannels func() []string

View file

@ -12,24 +12,17 @@ type Request struct {
ChatID string ChatID string
SenderID string SenderID string
Text string Text string
MessageID string
Reply func(text string) error Reply func(text string) error
} }
var commandPrefixes = []string{"/", "!"} const unavailableMsg = "Command unavailable in current context."
func firstToken(input string) string { var commandPrefixes = []string{"/", "!"}
parts := strings.Fields(strings.TrimSpace(input))
if len(parts) == 0 {
return ""
}
return parts[0]
}
// parseCommandName accepts "/name", "!name", and Telegram's "/name@bot", then // parseCommandName accepts "/name", "!name", and Telegram's "/name@bot", then
// normalizes to lowercase command names. // normalizes to lowercase command names.
func parseCommandName(input string) (string, bool) { func parseCommandName(input string) (string, bool) {
token := firstToken(input) token := nthToken(input, 0)
if token == "" { if token == "" {
return "", false return "", false
} }
@ -57,18 +50,10 @@ func trimCommandPrefix(token string) (string, bool) {
return "", false return "", false
} }
func secondToken(input string) string {
parts := strings.Fields(strings.TrimSpace(input))
if len(parts) < 2 {
return ""
}
return parts[1]
}
// HasCommandPrefix returns true if the input starts with a recognized // HasCommandPrefix returns true if the input starts with a recognized
// command prefix (e.g. "/" or "!"). // command prefix (e.g. "/" or "!").
func HasCommandPrefix(input string) bool { func HasCommandPrefix(input string) bool {
token := firstToken(input) token := nthToken(input, 0)
if token == "" { if token == "" {
return false return false
} }

View file

@ -17,7 +17,6 @@ const (
type ExecuteResult struct { type ExecuteResult struct {
Outcome Outcome Outcome Outcome
Command string Command string
Reply string
Err error Err error
} }
@ -51,6 +50,11 @@ func (e *Executor) Execute(ctx context.Context, req Request) ExecuteResult {
} }
func (e *Executor) executeDefinition(ctx context.Context, req Request, def Definition) ExecuteResult { func (e *Executor) executeDefinition(ctx context.Context, req Request, def Definition) ExecuteResult {
// Ensure Reply is always non-nil so handlers don't need to check.
if req.Reply == nil {
req.Reply = func(string) error { return nil }
}
// Simple command — no sub-commands // Simple command — no sub-commands
if len(def.SubCommands) == 0 { if len(def.SubCommands) == 0 {
if def.Handler == nil { if def.Handler == nil {
@ -61,11 +65,9 @@ func (e *Executor) executeDefinition(ctx context.Context, req Request, def Defin
} }
// Sub-command routing // Sub-command routing
subName := secondToken(req.Text) subName := nthToken(req.Text, 1)
if subName == "" { if subName == "" {
if req.Reply != nil {
_ = req.Reply("Usage: " + def.EffectiveUsage()) _ = req.Reply("Usage: " + def.EffectiveUsage())
}
return ExecuteResult{Outcome: OutcomeHandled, Command: def.Name} return ExecuteResult{Outcome: OutcomeHandled, Command: def.Name}
} }
@ -81,8 +83,6 @@ func (e *Executor) executeDefinition(ctx context.Context, req Request, def Defin
} }
// Unknown sub-command // Unknown sub-command
if req.Reply != nil {
_ = req.Reply(fmt.Sprintf("Unknown parameter: %s. Usage: %s", subName, def.EffectiveUsage())) _ = req.Reply(fmt.Sprintf("Unknown parameter: %s. Usage: %s", subName, def.EffectiveUsage()))
}
return ExecuteResult{Outcome: OutcomeHandled, Command: def.Name} return ExecuteResult{Outcome: OutcomeHandled, Command: def.Name}
} }

View file

@ -55,9 +55,6 @@ func TestShowListHandlers_ChannelPolicy(t *testing.T) {
if passthrough.Command != "foo" { if passthrough.Command != "foo" {
t.Fatalf("whatsapp /foo command=%q, want=%q", passthrough.Command, "foo") t.Fatalf("whatsapp /foo command=%q, want=%q", passthrough.Command, "foo")
} }
if passthrough.Reply != "" {
t.Fatalf("whatsapp /foo reply=%q, want empty", passthrough.Reply)
}
} }
func TestShowListHandlers_ListHandledOnAllChannels(t *testing.T) { func TestShowListHandlers_ListHandledOnAllChannels(t *testing.T) {