From 6993c2669bfa1c08aeb4eda26300f1b582adc41a Mon Sep 17 00:00:00 2001 From: mingmxren Date: Sun, 1 Mar 2026 16:11:22 +0800 Subject: [PATCH] refactor(channels): forward generic commands to agent-centric executor path --- pkg/channels/telegram/telegram_dispatch.go | 39 +---------- .../telegram/telegram_dispatch_test.go | 50 +++++++++++++- pkg/channels/whatsapp/whatsapp.go | 20 +----- .../whatsapp/whatsapp_command_test.go | 52 +++++++++++--- .../whatsapp_native/whatsapp_command_test.go | 67 ++++++++++++++++--- .../whatsapp_native/whatsapp_native.go | 20 +----- 6 files changed, 157 insertions(+), 91 deletions(-) diff --git a/pkg/channels/telegram/telegram_dispatch.go b/pkg/channels/telegram/telegram_dispatch.go index 57ae7951d..1bb79c467 100644 --- a/pkg/channels/telegram/telegram_dispatch.go +++ b/pkg/channels/telegram/telegram_dispatch.go @@ -2,12 +2,10 @@ package telegram import ( "context" - "strconv" "github.com/mymmrac/telego" "github.com/sipeed/picoclaw/pkg/commands" - "github.com/sipeed/picoclaw/pkg/logger" ) func (c *TelegramChannel) DispatchCommand(ctx context.Context, req commands.Request) commands.Result { @@ -18,38 +16,7 @@ func (c *TelegramChannel) DispatchCommand(ctx context.Context, req commands.Requ } func (c *TelegramChannel) dispatchCommand(ctx context.Context, message telego.Message) bool { - senderID := "" - if message.From != nil { - senderID = strconv.FormatInt(message.From.ID, 10) - } - - res := c.DispatchCommand(ctx, commands.Request{ - Channel: "telegram", - ChatID: strconv.FormatInt(message.Chat.ID, 10), - SenderID: senderID, - Text: message.Text, - MessageID: strconv.Itoa(message.MessageID), - Reply: func(text string) error { - _, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{ - ChatID: telego.ChatID{ID: message.Chat.ID}, - Text: text, - ReplyParameters: &telego.ReplyParameters{ - MessageID: message.MessageID, - }, - }) - return err - }, - }) - if !res.Matched { - return false - } - - if res.Err != nil { - logger.ErrorCF("telegram", "Command execution failed", map[string]any{ - "command": res.Command, - "error": res.Err.Error(), - }) - } - - return true + // Generic slash commands are now executed in the agent-centric command path. + // Channel adapters must not consume them locally. + return false } diff --git a/pkg/channels/telegram/telegram_dispatch_test.go b/pkg/channels/telegram/telegram_dispatch_test.go index 1e187f5ac..d63782eb3 100644 --- a/pkg/channels/telegram/telegram_dispatch_test.go +++ b/pkg/channels/telegram/telegram_dispatch_test.go @@ -3,13 +3,16 @@ package telegram import ( "context" "testing" + "time" "github.com/mymmrac/telego" + "github.com/sipeed/picoclaw/pkg/bus" + "github.com/sipeed/picoclaw/pkg/channels" "github.com/sipeed/picoclaw/pkg/commands" ) -func TestDispatchCommand_UsesDispatcher(t *testing.T) { +func TestDispatchCommand_DoesNotConsumeGenericCommandsLocally(t *testing.T) { ch := &TelegramChannel{} called := false ch.dispatcher = commands.DispatchFunc(func(context.Context, commands.Request) commands.Result { @@ -26,7 +29,50 @@ func TestDispatchCommand_UsesDispatcher(t *testing.T) { } handled := ch.dispatchCommand(context.Background(), msg) - if !handled || !called { + if handled { + t.Fatalf("handled=%v", handled) + } + if called { t.Fatalf("handled=%v called=%v", handled, called) } } + +func TestHandleMessage_DoesNotConsumeGenericCommandsLocally(t *testing.T) { + messageBus := bus.NewMessageBus() + ch := &TelegramChannel{ + BaseChannel: channels.NewBaseChannel("telegram", nil, messageBus, nil), + chatIDs: make(map[string]int64), + ctx: context.Background(), + } + + msg := &telego.Message{ + Text: "/new", + MessageID: 9, + Chat: telego.Chat{ + ID: 123, + Type: "private", + }, + From: &telego.User{ + ID: 42, + FirstName: "Alice", + }, + } + + if err := ch.handleMessage(context.Background(), msg); err != nil { + t.Fatalf("handleMessage error: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + inbound, ok := messageBus.ConsumeInbound(ctx) + if !ok { + t.Fatal("expected inbound message to be forwarded") + } + if inbound.Channel != "telegram" { + t.Fatalf("channel=%q", inbound.Channel) + } + if inbound.Content != "/new" { + t.Fatalf("content=%q", inbound.Content) + } +} diff --git a/pkg/channels/whatsapp/whatsapp.go b/pkg/channels/whatsapp/whatsapp.go index c1a95eb5f..48adde335 100644 --- a/pkg/channels/whatsapp/whatsapp.go +++ b/pkg/channels/whatsapp/whatsapp.go @@ -262,23 +262,9 @@ func (c *WhatsAppChannel) tryHandleCommand( ctx context.Context, text, chatID, senderID, messageID string, ) bool { - res := c.DispatchCommand(ctx, commands.Request{ - Channel: "whatsapp", - ChatID: chatID, - SenderID: senderID, - Text: text, - MessageID: messageID, - Reply: func(text string) error { - return c.Send(ctx, bus.OutboundMessage{ChatID: chatID, Content: text}) - }, - }) - if res.Err != nil { - logger.WarnCF("whatsapp", "Command execution failed", map[string]any{ - "command": res.Command, - "error": res.Err.Error(), - }) - } - return res.Matched + // Generic slash commands are now executed in the agent-centric command path. + // Channel adapters must not consume them locally. + return false } func (c *WhatsAppChannel) DispatchCommand(ctx context.Context, req commands.Request) commands.Result { diff --git a/pkg/channels/whatsapp/whatsapp_command_test.go b/pkg/channels/whatsapp/whatsapp_command_test.go index 1de2f2712..69dbd4595 100644 --- a/pkg/channels/whatsapp/whatsapp_command_test.go +++ b/pkg/channels/whatsapp/whatsapp_command_test.go @@ -3,11 +3,15 @@ package whatsapp import ( "context" "testing" + "time" + "github.com/sipeed/picoclaw/pkg/bus" + "github.com/sipeed/picoclaw/pkg/channels" "github.com/sipeed/picoclaw/pkg/commands" + "github.com/sipeed/picoclaw/pkg/config" ) -func TestTryHandleCommand_UsesDispatcher(t *testing.T) { +func TestTryHandleCommand_DoesNotConsumeGenericCommandsLocally(t *testing.T) { ch := &WhatsAppChannel{} called := false ch.dispatcher = commands.DispatchFunc(func(context.Context, commands.Request) commands.Result { @@ -16,19 +20,49 @@ func TestTryHandleCommand_UsesDispatcher(t *testing.T) { }) handled := ch.tryHandleCommand(context.Background(), "/help", "chat1", "user1", "mid1") - if !handled || !called { + if handled { + t.Fatalf("handled=%v", handled) + } + if called { t.Fatalf("handled=%v called=%v", handled, called) } } -func TestTryHandleCommand_MatchedWithoutHandler_DoesNotFallThrough(t *testing.T) { - ch := &WhatsAppChannel{} - ch.dispatcher = commands.DispatchFunc(func(context.Context, commands.Request) commands.Result { - return commands.Result{Matched: true, Handled: false, Command: "unknown"} +func TestHandleIncomingMessage_DoesNotConsumeGenericCommandsLocally(t *testing.T) { + messageBus := bus.NewMessageBus() + called := false + ch := &WhatsAppChannel{ + BaseChannel: channels.NewBaseChannel("whatsapp", config.WhatsAppConfig{}, messageBus, nil), + dispatcher: commands.DispatchFunc(func(context.Context, commands.Request) commands.Result { + called = true + return commands.Result{Matched: true, Handled: true} + }), + ctx: context.Background(), + } + + ch.handleIncomingMessage(map[string]any{ + "type": "message", + "id": "mid1", + "from": "user1", + "chat": "chat1", + "content": "/help", }) - handled := ch.tryHandleCommand(context.Background(), "/unknown", "chat1", "user1", "mid1") - if !handled { - t.Fatal("expected matched command to be treated as handled") + if called { + t.Fatal("expected generic command dispatch to be bypassed") + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + inbound, ok := messageBus.ConsumeInbound(ctx) + if !ok { + t.Fatal("expected inbound message to be forwarded") + } + if inbound.Channel != "whatsapp" { + t.Fatalf("channel=%q", inbound.Channel) + } + if inbound.Content != "/help" { + t.Fatalf("content=%q", inbound.Content) } } diff --git a/pkg/channels/whatsapp_native/whatsapp_command_test.go b/pkg/channels/whatsapp_native/whatsapp_command_test.go index 3fe7e31bd..5ff5ec4ca 100644 --- a/pkg/channels/whatsapp_native/whatsapp_command_test.go +++ b/pkg/channels/whatsapp_native/whatsapp_command_test.go @@ -5,11 +5,20 @@ package whatsapp import ( "context" "testing" + "time" + "go.mau.fi/whatsmeow/proto/waE2E" + "go.mau.fi/whatsmeow/types" + "go.mau.fi/whatsmeow/types/events" + "google.golang.org/protobuf/proto" + + "github.com/sipeed/picoclaw/pkg/bus" + "github.com/sipeed/picoclaw/pkg/channels" "github.com/sipeed/picoclaw/pkg/commands" + "github.com/sipeed/picoclaw/pkg/config" ) -func TestTryHandleCommand_UsesDispatcher(t *testing.T) { +func TestTryHandleCommand_DoesNotConsumeGenericCommandsLocally(t *testing.T) { ch := &WhatsAppNativeChannel{} called := false ch.dispatcher = commands.DispatchFunc(func(context.Context, commands.Request) commands.Result { @@ -18,19 +27,57 @@ func TestTryHandleCommand_UsesDispatcher(t *testing.T) { }) handled := ch.tryHandleCommand(context.Background(), "/help", "chat1", "user1", "mid1") - if !handled || !called { + if handled { + t.Fatalf("handled=%v", handled) + } + if called { t.Fatalf("handled=%v called=%v", handled, called) } } -func TestTryHandleCommand_MatchedWithoutHandler_DoesNotFallThrough(t *testing.T) { - ch := &WhatsAppNativeChannel{} - ch.dispatcher = commands.DispatchFunc(func(context.Context, commands.Request) commands.Result { - return commands.Result{Matched: true, Handled: false, Command: "unknown"} - }) +func TestHandleIncoming_DoesNotConsumeGenericCommandsLocally(t *testing.T) { + messageBus := bus.NewMessageBus() + called := false + ch := &WhatsAppNativeChannel{ + BaseChannel: channels.NewBaseChannel("whatsapp_native", config.WhatsAppConfig{}, messageBus, nil), + dispatcher: commands.DispatchFunc(func(context.Context, commands.Request) commands.Result { + called = true + return commands.Result{Matched: true, Handled: true} + }), + runCtx: context.Background(), + } - handled := ch.tryHandleCommand(context.Background(), "/unknown", "chat1", "user1", "mid1") - if !handled { - t.Fatal("expected matched command to be treated as handled") + evt := &events.Message{ + Info: types.MessageInfo{ + MessageSource: types.MessageSource{ + Sender: types.NewJID("1001", types.DefaultUserServer), + Chat: types.NewJID("1001", types.DefaultUserServer), + }, + ID: "mid1", + PushName: "Alice", + }, + Message: &waE2E.Message{ + Conversation: proto.String("/new"), + }, + } + + ch.handleIncoming(evt) + + if called { + t.Fatal("expected generic command dispatch to be bypassed") + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + inbound, ok := messageBus.ConsumeInbound(ctx) + if !ok { + t.Fatal("expected inbound message to be forwarded") + } + if inbound.Channel != "whatsapp_native" { + t.Fatalf("channel=%q", inbound.Channel) + } + if inbound.Content != "/new" { + t.Fatalf("content=%q", inbound.Content) } } diff --git a/pkg/channels/whatsapp_native/whatsapp_native.go b/pkg/channels/whatsapp_native/whatsapp_native.go index 0f093775f..f801f7e07 100644 --- a/pkg/channels/whatsapp_native/whatsapp_native.go +++ b/pkg/channels/whatsapp_native/whatsapp_native.go @@ -406,23 +406,9 @@ func (c *WhatsAppNativeChannel) tryHandleCommand( ctx context.Context, text, chatID, senderID, messageID string, ) bool { - res := c.DispatchCommand(ctx, commands.Request{ - Channel: "whatsapp_native", - ChatID: chatID, - SenderID: senderID, - Text: text, - MessageID: messageID, - Reply: func(text string) error { - return c.Send(ctx, bus.OutboundMessage{ChatID: chatID, Content: text}) - }, - }) - if res.Err != nil { - logger.WarnCF("whatsapp", "Command execution failed", map[string]any{ - "command": res.Command, - "error": res.Err.Error(), - }) - } - return res.Matched + // Generic slash commands are now executed in the agent-centric command path. + // Channel adapters must not consume them locally. + return false } func (c *WhatsAppNativeChannel) DispatchCommand(ctx context.Context, req commands.Request) commands.Result {