fix(commands): normalize parsing and remove routing metadata literals

This commit is contained in:
mingmxren 2026-03-03 17:01:48 +08:00
parent a0db5ea03e
commit aa56870875
4 changed files with 78 additions and 15 deletions

View file

@ -58,7 +58,17 @@ type processOptions struct {
NoHistory bool // If true, don't load session history (for heartbeat) NoHistory bool // If true, don't load session history (for heartbeat)
} }
const defaultResponse = "I've completed processing but have no response to give. Increase `max_tool_iterations` in config.json." const (
defaultResponse = "I've completed processing but have no response to give. Increase `max_tool_iterations` in config.json."
sessionKeyAgentPrefix = "agent:"
switchCommandToken = "/switch"
commandMentionSeparator = "@"
metadataKeyAccountID = "account_id"
metadataKeyGuildID = "guild_id"
metadataKeyTeamID = "team_id"
metadataKeyParentPeerKind = "parent_peer_kind"
metadataKeyParentPeerID = "parent_peer_id"
)
func NewAgentLoop( func NewAgentLoop(
cfg *config.Config, cfg *config.Config,
@ -498,11 +508,11 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
func (al *AgentLoop) resolveMessageRoute(msg bus.InboundMessage) (routing.ResolvedRoute, *AgentInstance, error) { func (al *AgentLoop) resolveMessageRoute(msg bus.InboundMessage) (routing.ResolvedRoute, *AgentInstance, error) {
route := al.registry.ResolveRoute(routing.RouteInput{ route := al.registry.ResolveRoute(routing.RouteInput{
Channel: msg.Channel, Channel: msg.Channel,
AccountID: msg.Metadata["account_id"], AccountID: inboundMetadata(msg, metadataKeyAccountID),
Peer: extractPeer(msg), Peer: extractPeer(msg),
ParentPeer: extractParentPeer(msg), ParentPeer: extractParentPeer(msg),
GuildID: msg.Metadata["guild_id"], GuildID: inboundMetadata(msg, metadataKeyGuildID),
TeamID: msg.Metadata["team_id"], TeamID: inboundMetadata(msg, metadataKeyTeamID),
}) })
agent, ok := al.registry.GetAgent(route.AgentID) agent, ok := al.registry.GetAgent(route.AgentID)
@ -517,7 +527,7 @@ func (al *AgentLoop) resolveMessageRoute(msg bus.InboundMessage) (routing.Resolv
} }
func resolveScopeKey(route routing.ResolvedRoute, msgSessionKey string) string { func resolveScopeKey(route routing.ResolvedRoute, msgSessionKey string) string {
if msgSessionKey != "" && strings.HasPrefix(msgSessionKey, "agent:") { if msgSessionKey != "" && strings.HasPrefix(msgSessionKey, sessionKeyAgentPrefix) {
return msgSessionKey return msgSessionKey
} }
return route.SessionKey return route.SessionKey
@ -1430,10 +1440,10 @@ func (al *AgentLoop) handleCommand(
return "", false return "", false
} }
cmd := parts[0] cmd := parts[0]
if at := strings.Index(cmd, "@"); at > 0 { if at := strings.Index(cmd, commandMentionSeparator); at > 0 {
cmd = cmd[:at] cmd = cmd[:at]
} }
if cmd != "/switch" { if cmd != switchCommandToken {
return "", false return "", false
} }
@ -1529,10 +1539,17 @@ func extractPeer(msg bus.InboundMessage) *routing.RoutePeer {
return &routing.RoutePeer{Kind: msg.Peer.Kind, ID: peerID} return &routing.RoutePeer{Kind: msg.Peer.Kind, ID: peerID}
} }
func inboundMetadata(msg bus.InboundMessage, key string) string {
if msg.Metadata == nil {
return ""
}
return msg.Metadata[key]
}
// extractParentPeer extracts the parent peer (reply-to) from inbound message metadata. // extractParentPeer extracts the parent peer (reply-to) from inbound message metadata.
func extractParentPeer(msg bus.InboundMessage) *routing.RoutePeer { func extractParentPeer(msg bus.InboundMessage) *routing.RoutePeer {
parentKind := msg.Metadata["parent_peer_kind"] parentKind := inboundMetadata(msg, metadataKeyParentPeerKind)
parentID := msg.Metadata["parent_peer_id"] parentID := inboundMetadata(msg, metadataKeyParentPeerID)
if parentKind == "" || parentID == "" { if parentKind == "" || parentID == "" {
return nil return nil
} }

View file

@ -16,6 +16,8 @@ type Request struct {
Reply func(text string) error Reply func(text string) error
} }
var commandPrefixes = []string{"/", "!"}
func firstToken(input string) string { func firstToken(input string) string {
parts := strings.Fields(strings.TrimSpace(input)) parts := strings.Fields(strings.TrimSpace(input))
if len(parts) == 0 { if len(parts) == 0 {
@ -24,24 +26,41 @@ 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 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

@ -64,8 +64,13 @@ func (e *Executor) Execute(ctx context.Context, req Request) ExecuteResult {
} }
func matchesCommand(def Definition, cmdName string) bool { func matchesCommand(def Definition, cmdName string) bool {
if def.Name == cmdName { if normalizeCommandName(def.Name) == cmdName {
return true return true
} }
return contains(def.Aliases, cmdName) for _, alias := range def.Aliases {
if normalizeCommandName(alias) == cmdName {
return true
}
}
return false
} }

View file

@ -153,3 +153,25 @@ func TestExecutor_HandlerErrorIsPropagated(t *testing.T) {
t.Fatalf("err=%v, want=%v", res.Err, wantErr) t.Fatalf("err=%v, want=%v", res.Err, wantErr)
} }
} }
func TestExecutor_SupportsBangPrefixAndCaseInsensitiveCommand(t *testing.T) {
called := false
defs := []Definition{
{
Name: "help",
Handler: func(context.Context, Request) error {
called = true
return nil
},
},
}
ex := NewExecutor(NewRegistry(defs))
res := ex.Execute(context.Background(), Request{Channel: "telegram", Text: "!HELP"})
if res.Outcome != OutcomeHandled {
t.Fatalf("outcome=%v, want=%v", res.Outcome, OutcomeHandled)
}
if !called {
t.Fatalf("expected handler to be called")
}
}