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.
This commit is contained in:
Acbox
2026-03-11 19:05:55 +08:00
parent ab82a72639
commit bb26d18757
10 changed files with 64 additions and 52 deletions
+1 -1
View File
@@ -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"
@@ -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
+36 -13
View File
@@ -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
+1 -2
View File
@@ -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, ", ")
}
+8 -8
View File
@@ -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 {
+10 -10
View File
@@ -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
}
+1 -5
View File
@@ -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"}}}
+1 -1
View File
@@ -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, "/") {
+3 -2
View File
@@ -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
+2 -9
View File
@@ -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 <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 == "" {