From bb26d18757da16d23ea04136cba848b42431e1d9 Mon Sep 17 00:00:00 2001 From: Acbox Date: Wed, 11 Mar 2026 19:05:55 +0800 Subject: [PATCH] fix(command): add missing command handler wiring and lint fixes Wire SetCommandHandler into ChannelInboundProcessor so slash commands are intercepted before reaching the LLM. Also apply lint fixes across command package (strconv.Itoa, comment formatting, unused code removal) and remove obsolete tool-call-browser.vue component. --- cmd/memoh/serve.go | 2 +- .../channel/adapters/feishu/quoted_message.go | 2 +- internal/channel/inbound/channel.go | 49 ++++++++++++++----- internal/command/email_cmd.go | 3 +- internal/command/formatter.go | 16 +++--- internal/command/handler.go | 20 ++++---- internal/command/handler_test.go | 6 +-- internal/command/parser.go | 2 +- internal/command/schedule.go | 5 +- internal/command/settings.go | 11 +---- 10 files changed, 64 insertions(+), 52 deletions(-) diff --git a/cmd/memoh/serve.go b/cmd/memoh/serve.go index 5c6da830..ff8ece39 100644 --- a/cmd/memoh/serve.go +++ b/cmd/memoh/serve.go @@ -25,6 +25,7 @@ import ( "github.com/memohai/memoh/internal/bind" "github.com/memohai/memoh/internal/boot" "github.com/memohai/memoh/internal/bots" + "github.com/memohai/memoh/internal/browsercontexts" agentruntime "github.com/memohai/memoh/internal/bun/runtime" "github.com/memohai/memoh/internal/channel" "github.com/memohai/memoh/internal/channel/adapters/discord" @@ -48,7 +49,6 @@ import ( emailmailgun "github.com/memohai/memoh/internal/email/adapters/mailgun" "github.com/memohai/memoh/internal/handlers" "github.com/memohai/memoh/internal/healthcheck" - "github.com/memohai/memoh/internal/browsercontexts" channelchecker "github.com/memohai/memoh/internal/healthcheck/checkers/channel" mcpchecker "github.com/memohai/memoh/internal/healthcheck/checkers/mcp" "github.com/memohai/memoh/internal/heartbeat" diff --git a/internal/channel/adapters/feishu/quoted_message.go b/internal/channel/adapters/feishu/quoted_message.go index aa2f1b43..d3c59955 100644 --- a/internal/channel/adapters/feishu/quoted_message.go +++ b/internal/channel/adapters/feishu/quoted_message.go @@ -57,7 +57,7 @@ func (a *FeishuAdapter) enrichQuotedMessage(ctx context.Context, cfg channel.Cha a.logger.Debug("feishu quoted message fetch empty", slog.String("parent_id", parentID), slog.Int("code", code), - slog.String("msg", respMsg), + slog.String("response_msg", respMsg), ) } return diff --git a/internal/channel/inbound/channel.go b/internal/channel/inbound/channel.go index 2fcbf3e1..fa73479d 100644 --- a/internal/channel/inbound/channel.go +++ b/internal/channel/inbound/channel.go @@ -19,6 +19,7 @@ import ( "github.com/memohai/memoh/internal/auth" "github.com/memohai/memoh/internal/channel" "github.com/memohai/memoh/internal/channel/route" + "github.com/memohai/memoh/internal/command" "github.com/memohai/memoh/internal/conversation" "github.com/memohai/memoh/internal/conversation/flow" "github.com/memohai/memoh/internal/inbox" @@ -55,18 +56,19 @@ type mediaIngestor interface { // ChannelInboundProcessor routes channel inbound messages to the chat gateway. type ChannelInboundProcessor struct { - runner flow.Runner - routeResolver RouteResolver - message messagepkg.Writer - mediaService mediaIngestor - reactor channelReactor - inboxService *inbox.Service - registry *channel.Registry - logger *slog.Logger - jwtSecret string - tokenTTL time.Duration - identity *IdentityResolver - observer channel.StreamObserver + runner flow.Runner + routeResolver RouteResolver + message messagepkg.Writer + mediaService mediaIngestor + reactor channelReactor + inboxService *inbox.Service + commandHandler *command.Handler + registry *channel.Registry + logger *slog.Logger + jwtSecret string + tokenTTL time.Duration + identity *IdentityResolver + observer channel.StreamObserver } // NewChannelInboundProcessor creates a processor with channel identity-based resolution. @@ -146,6 +148,15 @@ func (p *ChannelInboundProcessor) SetInboxService(service *inbox.Service) { p.inboxService = service } +// SetCommandHandler configures the slash command handler for intercepting +// /command messages before they reach the LLM. +func (p *ChannelInboundProcessor) SetCommandHandler(handler *command.Handler) { + if p == nil { + return + } + p.commandHandler = handler +} + // HandleInbound processes an inbound channel message through identity resolution and chat gateway. func (p *ChannelInboundProcessor) HandleInbound(ctx context.Context, cfg channel.ChannelConfig, msg channel.InboundMessage, sender channel.StreamReplySender) error { if p.runner == nil { @@ -195,6 +206,19 @@ func (p *ChannelInboundProcessor) HandleInbound(ctx context.Context, cfg channel } identity := state.Identity + + // Intercept slash commands before they reach the LLM. + if p.commandHandler != nil && p.commandHandler.IsCommand(text) { + reply, err := p.commandHandler.Execute(ctx, strings.TrimSpace(identity.BotID), strings.TrimSpace(identity.ChannelIdentityID), text) + if err != nil { + reply = "Error: " + err.Error() + } + return sender.Send(ctx, channel.OutboundMessage{ + Target: strings.TrimSpace(msg.ReplyTarget), + Message: channel.Message{Text: reply}, + }) + } + resolvedAttachments := p.ingestInboundAttachments(ctx, cfg, msg, strings.TrimSpace(identity.BotID), msg.Message.Attachments) attachments := mapChannelToChatAttachments(resolvedAttachments) text = buildInboundQuery(msg.Message, attachments) @@ -837,7 +861,6 @@ func buildChannelMessage(output conversation.AssistantOutput, capabilities chann return msg } - func contentPartHasValue(part conversation.ContentPart) bool { if strings.TrimSpace(part.Text) != "" { return true diff --git a/internal/command/email_cmd.go b/internal/command/email_cmd.go index 4d9fc6e3..951f70c2 100644 --- a/internal/command/email_cmd.go +++ b/internal/command/email_cmd.go @@ -1,7 +1,6 @@ package command import ( - "fmt" "strings" ) @@ -91,5 +90,5 @@ func buildPermString(read, write, del bool) string { if len(parts) == 0 { return "none" } - return fmt.Sprintf("%s", strings.Join(parts, ", ")) + return strings.Join(parts, ", ") } diff --git a/internal/command/formatter.go b/internal/command/formatter.go index 43d3f211..ec813aca 100644 --- a/internal/command/formatter.go +++ b/internal/command/formatter.go @@ -11,13 +11,13 @@ import ( // // Example output: // -// - mybot -// Description: A helpful assistant -// ID: abc123 +// - mybot +// Description: A helpful assistant +// ID: abc123 // -// - another -// Description: Something else -// ID: def456 +// - another +// Description: Something else +// ID: def456 func formatItems(items [][]kv) string { if len(items) == 0 { return "" @@ -42,8 +42,8 @@ func formatItems(items [][]kv) string { // // Example output: // -// - ID: abc123 -// - Name: mybot +// - ID: abc123 +// - Name: mybot func formatKV(pairs []kv) string { var b strings.Builder for _, p := range pairs { diff --git a/internal/command/handler.go b/internal/command/handler.go index 52ecbb0d..28cb1e38 100644 --- a/internal/command/handler.go +++ b/internal/command/handler.go @@ -49,17 +49,17 @@ type Handler struct { mcpConnService *mcp.ConnectionService inboxService *inbox.Service - modelsService *models.Service - providersService *providers.Service - memProvService *memprovider.Service - searchProvService *searchproviders.Service - browserCtxService *browsercontexts.Service - emailService *emailpkg.Service + modelsService *models.Service + providersService *providers.Service + memProvService *memprovider.Service + searchProvService *searchproviders.Service + browserCtxService *browsercontexts.Service + emailService *emailpkg.Service emailOutboxService *emailpkg.OutboxService - heartbeatService *heartbeat.Service - queries *dbsqlc.Queries - skillLoader SkillLoader - containerFS ContainerFS + heartbeatService *heartbeat.Service + queries *dbsqlc.Queries + skillLoader SkillLoader + containerFS ContainerFS logger *slog.Logger } diff --git a/internal/command/handler_test.go b/internal/command/handler_test.go index 4f09c6be..eaface1e 100644 --- a/internal/command/handler_test.go +++ b/internal/command/handler_test.go @@ -29,10 +29,6 @@ type fakeSubagentService struct { items []subagent.Subagent } -func (f *fakeSubagentService) list() []subagent.Subagent { - return f.items -} - type fakeScheduleService struct { items []schedule.Schedule } @@ -325,7 +321,7 @@ func TestNewCommands_NilServices(t *testing.T) { } } -// suppress unused warnings +// suppress unused warnings. var ( _ = fakeSubagentService{items: []subagent.Subagent{{ID: "1", Name: "test", CreatedAt: time.Now(), UpdatedAt: time.Now()}}} _ = fakeScheduleService{items: []schedule.Schedule{{ID: "1", Name: "test"}}} diff --git a/internal/command/parser.go b/internal/command/parser.go index a9642077..7f043905 100644 --- a/internal/command/parser.go +++ b/internal/command/parser.go @@ -13,7 +13,7 @@ type ParsedCommand struct { } // Parse parses a raw command string into its components. -// Expected format: /resource [action] [args...] +// Expected format: /resource [action] [args...]. func Parse(text string) (ParsedCommand, error) { text = strings.TrimSpace(text) if !strings.HasPrefix(text, "/") { diff --git a/internal/command/schedule.go b/internal/command/schedule.go index 99a0a64c..e7d01fa6 100644 --- a/internal/command/schedule.go +++ b/internal/command/schedule.go @@ -2,6 +2,7 @@ package command import ( "fmt" + "strconv" "strings" "github.com/memohai/memoh/internal/schedule" @@ -45,7 +46,7 @@ func (h *Handler) buildScheduleGroup() *CommandGroup { } maxCalls := "unlimited" if item.MaxCalls != nil { - maxCalls = fmt.Sprintf("%d", *item.MaxCalls) + maxCalls = strconv.Itoa(*item.MaxCalls) } return formatKV([]kv{ {"Name", item.Name}, @@ -54,7 +55,7 @@ func (h *Handler) buildScheduleGroup() *CommandGroup { {"Command", item.Command}, {"Enabled", boolStr(item.Enabled)}, {"Max Calls", maxCalls}, - {"Current Calls", fmt.Sprintf("%d", item.CurrentCalls)}, + {"Current Calls", strconv.Itoa(item.CurrentCalls)}, {"Created", item.CreatedAt.Format("2006-01-02 15:04:05")}, {"Updated", item.UpdatedAt.Format("2006-01-02 15:04:05")}, }), nil diff --git a/internal/command/settings.go b/internal/command/settings.go index 894cb4dc..3aab317c 100644 --- a/internal/command/settings.go +++ b/internal/command/settings.go @@ -23,8 +23,8 @@ func (h *Handler) buildSettingsGroup() *CommandGroup { {"Language", s.Language}, {"Allow Guest", boolStr(s.AllowGuest)}, {"Max Context Load Time", fmt.Sprintf("%d min", s.MaxContextLoadTime)}, - {"Max Context Tokens", fmt.Sprintf("%d", s.MaxContextTokens)}, - {"Max Inbox Items", fmt.Sprintf("%d", s.MaxInboxItems)}, + {"Max Context Tokens", strconv.Itoa(s.MaxContextTokens)}, + {"Max Inbox Items", strconv.Itoa(s.MaxInboxItems)}, {"Reasoning Enabled", boolStr(s.ReasoningEnabled)}, {"Reasoning Effort", s.ReasoningEffort}, {"Heartbeat Enabled", boolStr(s.HeartbeatEnabled)}, @@ -126,13 +126,6 @@ func settingsUpdateUsage() string { "- --heartbeat_model_id " } -func valueOrNone(s string) string { - if s == "" { - return "(none)" - } - return s -} - // resolveModelName resolves a model UUID to "model_name (provider_name)". func (h *Handler) resolveModelName(cc CommandContext, modelID string) string { if modelID == "" {