refactor(channels): forward generic commands to agent-centric executor path
This commit is contained in:
parent
72bf2e0901
commit
6993c2669b
6 changed files with 157 additions and 91 deletions
|
|
@ -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 {
|
||||
// Generic slash commands are now executed in the agent-centric command path.
|
||||
// Channel adapters must not consume them locally.
|
||||
return false
|
||||
}
|
||||
|
||||
if res.Err != nil {
|
||||
logger.ErrorCF("telegram", "Command execution failed", map[string]any{
|
||||
"command": res.Command,
|
||||
"error": res.Err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue