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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/mymmrac/telego"
|
"github.com/mymmrac/telego"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/commands"
|
"github.com/sipeed/picoclaw/pkg/commands"
|
||||||
"github.com/sipeed/picoclaw/pkg/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *TelegramChannel) DispatchCommand(ctx context.Context, req commands.Request) commands.Result {
|
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 {
|
func (c *TelegramChannel) dispatchCommand(ctx context.Context, message telego.Message) bool {
|
||||||
senderID := ""
|
// Generic slash commands are now executed in the agent-centric command path.
|
||||||
if message.From != nil {
|
// Channel adapters must not consume them locally.
|
||||||
senderID = strconv.FormatInt(message.From.ID, 10)
|
return false
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,16 @@ package telegram
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mymmrac/telego"
|
"github.com/mymmrac/telego"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/channels"
|
||||||
"github.com/sipeed/picoclaw/pkg/commands"
|
"github.com/sipeed/picoclaw/pkg/commands"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDispatchCommand_UsesDispatcher(t *testing.T) {
|
func TestDispatchCommand_DoesNotConsumeGenericCommandsLocally(t *testing.T) {
|
||||||
ch := &TelegramChannel{}
|
ch := &TelegramChannel{}
|
||||||
called := false
|
called := false
|
||||||
ch.dispatcher = commands.DispatchFunc(func(context.Context, commands.Request) commands.Result {
|
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)
|
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)
|
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,
|
ctx context.Context,
|
||||||
text, chatID, senderID, messageID string,
|
text, chatID, senderID, messageID string,
|
||||||
) bool {
|
) bool {
|
||||||
res := c.DispatchCommand(ctx, commands.Request{
|
// Generic slash commands are now executed in the agent-centric command path.
|
||||||
Channel: "whatsapp",
|
// Channel adapters must not consume them locally.
|
||||||
ChatID: chatID,
|
return false
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WhatsAppChannel) DispatchCommand(ctx context.Context, req commands.Request) commands.Result {
|
func (c *WhatsAppChannel) DispatchCommand(ctx context.Context, req commands.Request) commands.Result {
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,15 @@ package whatsapp
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"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/commands"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTryHandleCommand_UsesDispatcher(t *testing.T) {
|
func TestTryHandleCommand_DoesNotConsumeGenericCommandsLocally(t *testing.T) {
|
||||||
ch := &WhatsAppChannel{}
|
ch := &WhatsAppChannel{}
|
||||||
called := false
|
called := false
|
||||||
ch.dispatcher = commands.DispatchFunc(func(context.Context, commands.Request) commands.Result {
|
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")
|
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)
|
t.Fatalf("handled=%v called=%v", handled, called)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTryHandleCommand_MatchedWithoutHandler_DoesNotFallThrough(t *testing.T) {
|
func TestHandleIncomingMessage_DoesNotConsumeGenericCommandsLocally(t *testing.T) {
|
||||||
ch := &WhatsAppChannel{}
|
messageBus := bus.NewMessageBus()
|
||||||
ch.dispatcher = commands.DispatchFunc(func(context.Context, commands.Request) commands.Result {
|
called := false
|
||||||
return commands.Result{Matched: true, Handled: false, Command: "unknown"}
|
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 called {
|
||||||
if !handled {
|
t.Fatal("expected generic command dispatch to be bypassed")
|
||||||
t.Fatal("expected matched command to be treated as handled")
|
}
|
||||||
|
|
||||||
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"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/commands"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTryHandleCommand_UsesDispatcher(t *testing.T) {
|
func TestTryHandleCommand_DoesNotConsumeGenericCommandsLocally(t *testing.T) {
|
||||||
ch := &WhatsAppNativeChannel{}
|
ch := &WhatsAppNativeChannel{}
|
||||||
called := false
|
called := false
|
||||||
ch.dispatcher = commands.DispatchFunc(func(context.Context, commands.Request) commands.Result {
|
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")
|
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)
|
t.Fatalf("handled=%v called=%v", handled, called)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTryHandleCommand_MatchedWithoutHandler_DoesNotFallThrough(t *testing.T) {
|
func TestHandleIncoming_DoesNotConsumeGenericCommandsLocally(t *testing.T) {
|
||||||
ch := &WhatsAppNativeChannel{}
|
messageBus := bus.NewMessageBus()
|
||||||
ch.dispatcher = commands.DispatchFunc(func(context.Context, commands.Request) commands.Result {
|
called := false
|
||||||
return commands.Result{Matched: true, Handled: false, Command: "unknown"}
|
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")
|
evt := &events.Message{
|
||||||
if !handled {
|
Info: types.MessageInfo{
|
||||||
t.Fatal("expected matched command to be treated as handled")
|
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,
|
ctx context.Context,
|
||||||
text, chatID, senderID, messageID string,
|
text, chatID, senderID, messageID string,
|
||||||
) bool {
|
) bool {
|
||||||
res := c.DispatchCommand(ctx, commands.Request{
|
// Generic slash commands are now executed in the agent-centric command path.
|
||||||
Channel: "whatsapp_native",
|
// Channel adapters must not consume them locally.
|
||||||
ChatID: chatID,
|
return false
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WhatsAppNativeChannel) DispatchCommand(ctx context.Context, req commands.Request) commands.Result {
|
func (c *WhatsAppNativeChannel) DispatchCommand(ctx context.Context, req commands.Request) commands.Result {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue