mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat: support attachment send to tool send
This commit is contained in:
@@ -378,18 +378,14 @@ func normalizeAttachmentRefs(attachments []Attachment, defaultPlatform ChannelTy
|
||||
item.URL = strings.TrimSpace(item.URL)
|
||||
item.PlatformKey = strings.TrimSpace(item.PlatformKey)
|
||||
item.ContentHash = strings.TrimSpace(item.ContentHash)
|
||||
item.Base64 = strings.TrimSpace(item.Base64)
|
||||
item.SourcePlatform = strings.TrimSpace(item.SourcePlatform)
|
||||
if item.SourcePlatform == "" && item.PlatformKey != "" {
|
||||
item.SourcePlatform = defaultPlatform.String()
|
||||
}
|
||||
if item.URL == "" && item.PlatformKey == "" && item.ContentHash == "" {
|
||||
if item.URL == "" && item.PlatformKey == "" && item.ContentHash == "" && item.Base64 == "" {
|
||||
return nil, fmt.Errorf("attachment reference is required")
|
||||
}
|
||||
// content_hash-only attachments require media resolution before dispatch.
|
||||
// Adapters expect url or platform_key; fail loudly if neither is available.
|
||||
if item.URL == "" && item.PlatformKey == "" && item.ContentHash != "" {
|
||||
return nil, fmt.Errorf("attachment %s has content_hash but no sendable url or platform_key; media resolution required before dispatch", item.ContentHash)
|
||||
}
|
||||
normalized = append(normalized, item)
|
||||
}
|
||||
return normalized, nil
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/memohai/memoh/internal/channel"
|
||||
@@ -31,25 +32,40 @@ type ChannelTypeResolver interface {
|
||||
ParseChannelType(raw string) (channel.ChannelType, error)
|
||||
}
|
||||
|
||||
// AssetMeta holds resolved metadata for a media asset.
|
||||
type AssetMeta struct {
|
||||
ContentHash string
|
||||
Mime string
|
||||
SizeBytes int64
|
||||
StorageKey string
|
||||
}
|
||||
|
||||
// AssetResolver looks up persisted media assets by storage key.
|
||||
type AssetResolver interface {
|
||||
GetByStorageKey(ctx context.Context, botID, storageKey string) (AssetMeta, error)
|
||||
}
|
||||
|
||||
// Executor exposes send and react as MCP tools.
|
||||
type Executor struct {
|
||||
sender Sender
|
||||
reactor Reactor
|
||||
resolver ChannelTypeResolver
|
||||
logger *slog.Logger
|
||||
sender Sender
|
||||
reactor Reactor
|
||||
resolver ChannelTypeResolver
|
||||
assetResolver AssetResolver
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewExecutor creates a message tool executor.
|
||||
// reactor may be nil; the react tool will not be listed when reactor is unavailable.
|
||||
func NewExecutor(log *slog.Logger, sender Sender, reactor Reactor, resolver ChannelTypeResolver) *Executor {
|
||||
// reactor and assetResolver may be nil.
|
||||
func NewExecutor(log *slog.Logger, sender Sender, reactor Reactor, resolver ChannelTypeResolver, assetResolver AssetResolver) *Executor {
|
||||
if log == nil {
|
||||
log = slog.Default()
|
||||
}
|
||||
return &Executor{
|
||||
sender: sender,
|
||||
reactor: reactor,
|
||||
resolver: resolver,
|
||||
logger: log.With(slog.String("provider", "message_tool")),
|
||||
sender: sender,
|
||||
reactor: reactor,
|
||||
resolver: resolver,
|
||||
assetResolver: assetResolver,
|
||||
logger: log.With(slog.String("provider", "message_tool")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +74,7 @@ func (p *Executor) ListTools(ctx context.Context, session mcpgw.ToolSessionConte
|
||||
if p.sender != nil && p.resolver != nil {
|
||||
tools = append(tools, mcpgw.ToolDescriptor{
|
||||
Name: toolSend,
|
||||
Description: "Send a message to a channel or session. Supports text, structured messages, attachments, and replies.",
|
||||
Description: "Send a message to a channel or session. Supports text, attachments, and replies.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
@@ -82,6 +98,11 @@ func (p *Executor) ListTools(ctx context.Context, session mcpgw.ToolSessionConte
|
||||
"type": "string",
|
||||
"description": "Message ID to reply to. The reply will reference this message on the platform.",
|
||||
},
|
||||
"attachments": map[string]any{
|
||||
"type": "array",
|
||||
"description": "File paths or URLs to attach. Each item is a container path (e.g. /data/media/ab/file.jpg), an HTTP URL, or an object with {path, url, type, name}.",
|
||||
"items": map[string]any{},
|
||||
},
|
||||
"message": map[string]any{
|
||||
"type": "object",
|
||||
"description": "Structured message payload with text/parts/attachments",
|
||||
@@ -160,10 +181,25 @@ func (p *Executor) callSend(ctx context.Context, session mcpgw.ToolSessionContex
|
||||
messageText := mcpgw.FirstStringArg(arguments, "text")
|
||||
outboundMessage, parseErr := parseOutboundMessage(arguments, messageText)
|
||||
if parseErr != nil {
|
||||
return mcpgw.BuildToolErrorResult(parseErr.Error()), nil
|
||||
// Allow empty message when attachments are provided.
|
||||
if rawAtt, ok := arguments["attachments"]; !ok || rawAtt == nil {
|
||||
return mcpgw.BuildToolErrorResult(parseErr.Error()), nil
|
||||
}
|
||||
outboundMessage = channel.Message{Text: strings.TrimSpace(messageText)}
|
||||
}
|
||||
|
||||
// Resolve top-level attachments parameter.
|
||||
if rawAttachments, ok := arguments["attachments"]; ok && rawAttachments != nil {
|
||||
if arr, ok := rawAttachments.([]any); ok && len(arr) > 0 {
|
||||
resolved := p.resolveAttachments(ctx, botID, arr)
|
||||
outboundMessage.Attachments = append(outboundMessage.Attachments, resolved...)
|
||||
}
|
||||
}
|
||||
|
||||
if outboundMessage.IsEmpty() {
|
||||
return mcpgw.BuildToolErrorResult("message or attachments required"), nil
|
||||
}
|
||||
|
||||
// Attach reply reference if reply_to is provided.
|
||||
if replyTo := mcpgw.FirstStringArg(arguments, "reply_to"); replyTo != "" {
|
||||
outboundMessage.Reply = &channel.ReplyRef{MessageID: replyTo}
|
||||
}
|
||||
@@ -281,6 +317,140 @@ func (p *Executor) resolvePlatform(arguments map[string]any, session mcpgw.ToolS
|
||||
return p.resolver.ParseChannelType(platform)
|
||||
}
|
||||
|
||||
// --- attachment resolution ---
|
||||
|
||||
// resolveAttachments converts raw attachment arguments (strings or objects)
|
||||
// into channel.Attachment values, resolving container media paths when possible.
|
||||
func (p *Executor) resolveAttachments(ctx context.Context, botID string, items []any) []channel.Attachment {
|
||||
var result []channel.Attachment
|
||||
for _, item := range items {
|
||||
switch v := item.(type) {
|
||||
case string:
|
||||
if att := p.resolveAttachmentRef(ctx, botID, strings.TrimSpace(v), "", ""); att != nil {
|
||||
result = append(result, *att)
|
||||
}
|
||||
case map[string]any:
|
||||
path := mcpgw.FirstStringArg(v, "path")
|
||||
url := mcpgw.FirstStringArg(v, "url")
|
||||
attType := mcpgw.FirstStringArg(v, "type")
|
||||
name := mcpgw.FirstStringArg(v, "name")
|
||||
ref := path
|
||||
if ref == "" {
|
||||
ref = url
|
||||
}
|
||||
if ref == "" {
|
||||
continue
|
||||
}
|
||||
if att := p.resolveAttachmentRef(ctx, botID, ref, attType, name); att != nil {
|
||||
result = append(result, *att)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// resolveAttachmentRef resolves a single path or URL to a channel.Attachment.
|
||||
func (p *Executor) resolveAttachmentRef(ctx context.Context, botID, ref, attType, name string) *channel.Attachment {
|
||||
ref = strings.TrimSpace(ref)
|
||||
if ref == "" {
|
||||
return nil
|
||||
}
|
||||
lower := strings.ToLower(ref)
|
||||
|
||||
// HTTP/HTTPS URL — pass through.
|
||||
if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") {
|
||||
t := channel.AttachmentType(attType)
|
||||
if t == "" {
|
||||
t = inferAttachmentTypeFromExt(ref)
|
||||
}
|
||||
return &channel.Attachment{
|
||||
Type: t,
|
||||
URL: ref,
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
|
||||
// Data URL — pass through.
|
||||
if strings.HasPrefix(lower, "data:") {
|
||||
t := channel.AttachmentType(attType)
|
||||
if t == "" {
|
||||
t = channel.AttachmentImage
|
||||
}
|
||||
return &channel.Attachment{
|
||||
Type: t,
|
||||
Base64: ref,
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
|
||||
// Container media path — resolve via asset storage.
|
||||
const mediaMarker = "/data/media/"
|
||||
if idx := strings.Index(ref, mediaMarker); idx >= 0 && p.assetResolver != nil {
|
||||
storageKey := ref[idx+len(mediaMarker):]
|
||||
asset, err := p.assetResolver.GetByStorageKey(ctx, botID, storageKey)
|
||||
if err == nil {
|
||||
t := channel.AttachmentType(attType)
|
||||
if t == "" {
|
||||
t = inferAttachmentTypeFromMime(asset.Mime)
|
||||
}
|
||||
att := channel.Attachment{
|
||||
Type: t,
|
||||
ContentHash: asset.ContentHash,
|
||||
Mime: asset.Mime,
|
||||
Size: asset.SizeBytes,
|
||||
Name: name,
|
||||
Metadata: map[string]any{
|
||||
"bot_id": botID,
|
||||
"storage_key": asset.StorageKey,
|
||||
},
|
||||
}
|
||||
return &att
|
||||
}
|
||||
if p.logger != nil {
|
||||
p.logger.Warn("resolve media path failed", slog.String("path", ref), slog.Any("error", err))
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown container path — pass through with the path as URL.
|
||||
t := channel.AttachmentType(attType)
|
||||
if t == "" {
|
||||
t = inferAttachmentTypeFromExt(ref)
|
||||
}
|
||||
return &channel.Attachment{
|
||||
Type: t,
|
||||
URL: ref,
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func inferAttachmentTypeFromMime(mime string) channel.AttachmentType {
|
||||
mime = strings.ToLower(strings.TrimSpace(mime))
|
||||
switch {
|
||||
case strings.HasPrefix(mime, "image/"):
|
||||
return channel.AttachmentImage
|
||||
case strings.HasPrefix(mime, "audio/"):
|
||||
return channel.AttachmentAudio
|
||||
case strings.HasPrefix(mime, "video/"):
|
||||
return channel.AttachmentVideo
|
||||
default:
|
||||
return channel.AttachmentFile
|
||||
}
|
||||
}
|
||||
|
||||
func inferAttachmentTypeFromExt(path string) channel.AttachmentType {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg":
|
||||
return channel.AttachmentImage
|
||||
case ".mp3", ".wav", ".ogg", ".flac", ".aac":
|
||||
return channel.AttachmentAudio
|
||||
case ".mp4", ".webm", ".avi", ".mov":
|
||||
return channel.AttachmentVideo
|
||||
default:
|
||||
return channel.AttachmentFile
|
||||
}
|
||||
}
|
||||
|
||||
func parseOutboundMessage(arguments map[string]any, fallbackText string) (channel.Message, error) {
|
||||
var msg channel.Message
|
||||
if raw, ok := arguments["message"]; ok && raw != nil {
|
||||
|
||||
@@ -44,7 +44,7 @@ func (f *fakeResolver) ParseChannelType(raw string) (channel.ChannelType, error)
|
||||
// --- send tests ---
|
||||
|
||||
func TestExecutor_ListTools_NilDeps(t *testing.T) {
|
||||
exec := NewExecutor(nil, nil, nil, nil)
|
||||
exec := NewExecutor(nil, nil, nil, nil, nil)
|
||||
tools, err := exec.ListTools(context.Background(), mcpgw.ToolSessionContext{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -58,7 +58,7 @@ func TestExecutor_ListTools(t *testing.T) {
|
||||
sender := &fakeSender{}
|
||||
reactor := &fakeReactor{}
|
||||
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
||||
exec := NewExecutor(nil, sender, reactor, resolver)
|
||||
exec := NewExecutor(nil, sender, reactor, resolver, nil)
|
||||
tools, err := exec.ListTools(context.Background(), mcpgw.ToolSessionContext{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -77,7 +77,7 @@ func TestExecutor_ListTools(t *testing.T) {
|
||||
func TestExecutor_ListTools_OnlySender(t *testing.T) {
|
||||
sender := &fakeSender{}
|
||||
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
||||
exec := NewExecutor(nil, sender, nil, resolver)
|
||||
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
||||
tools, err := exec.ListTools(context.Background(), mcpgw.ToolSessionContext{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -93,7 +93,7 @@ func TestExecutor_ListTools_OnlySender(t *testing.T) {
|
||||
func TestExecutor_CallTool_NotFound(t *testing.T) {
|
||||
sender := &fakeSender{}
|
||||
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
||||
exec := NewExecutor(nil, sender, nil, resolver)
|
||||
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
||||
_, err := exec.CallTool(context.Background(), mcpgw.ToolSessionContext{BotID: "bot1"}, "other_tool", nil)
|
||||
if err != mcpgw.ErrToolNotFound {
|
||||
t.Errorf("expected ErrToolNotFound, got %v", err)
|
||||
@@ -101,7 +101,7 @@ func TestExecutor_CallTool_NotFound(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestExecutor_CallTool_NilDeps(t *testing.T) {
|
||||
exec := NewExecutor(nil, nil, nil, nil)
|
||||
exec := NewExecutor(nil, nil, nil, nil, nil)
|
||||
result, err := exec.CallTool(context.Background(), mcpgw.ToolSessionContext{BotID: "bot1"}, toolSend, map[string]any{
|
||||
"platform": "feishu", "target": "t1", "text": "hi",
|
||||
})
|
||||
@@ -116,7 +116,7 @@ func TestExecutor_CallTool_NilDeps(t *testing.T) {
|
||||
func TestExecutor_CallTool_NoBotID(t *testing.T) {
|
||||
sender := &fakeSender{}
|
||||
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
||||
exec := NewExecutor(nil, sender, nil, resolver)
|
||||
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
||||
result, err := exec.CallTool(context.Background(), mcpgw.ToolSessionContext{}, toolSend, map[string]any{
|
||||
"platform": "feishu", "target": "t1", "text": "hi",
|
||||
})
|
||||
@@ -131,7 +131,7 @@ func TestExecutor_CallTool_NoBotID(t *testing.T) {
|
||||
func TestExecutor_CallTool_BotIDMismatch(t *testing.T) {
|
||||
sender := &fakeSender{}
|
||||
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
||||
exec := NewExecutor(nil, sender, nil, resolver)
|
||||
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
||||
session := mcpgw.ToolSessionContext{BotID: "bot1"}
|
||||
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
||||
"bot_id": "other", "platform": "feishu", "target": "t1", "text": "hi",
|
||||
@@ -147,7 +147,7 @@ func TestExecutor_CallTool_BotIDMismatch(t *testing.T) {
|
||||
func TestExecutor_CallTool_NoPlatform(t *testing.T) {
|
||||
sender := &fakeSender{}
|
||||
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
||||
exec := NewExecutor(nil, sender, nil, resolver)
|
||||
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
||||
session := mcpgw.ToolSessionContext{BotID: "bot1"}
|
||||
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
||||
"target": "t1", "text": "hi",
|
||||
@@ -163,7 +163,7 @@ func TestExecutor_CallTool_NoPlatform(t *testing.T) {
|
||||
func TestExecutor_CallTool_PlatformParseError(t *testing.T) {
|
||||
sender := &fakeSender{}
|
||||
resolver := &fakeResolver{err: errors.New("unknown platform")}
|
||||
exec := NewExecutor(nil, sender, nil, resolver)
|
||||
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
||||
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "feishu"}
|
||||
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
||||
"platform": "bad", "target": "t1", "text": "hi",
|
||||
@@ -179,7 +179,7 @@ func TestExecutor_CallTool_PlatformParseError(t *testing.T) {
|
||||
func TestExecutor_CallTool_NoMessage(t *testing.T) {
|
||||
sender := &fakeSender{}
|
||||
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
||||
exec := NewExecutor(nil, sender, nil, resolver)
|
||||
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
||||
session := mcpgw.ToolSessionContext{BotID: "bot1"}
|
||||
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
||||
"platform": "feishu", "target": "t1",
|
||||
@@ -195,7 +195,7 @@ func TestExecutor_CallTool_NoMessage(t *testing.T) {
|
||||
func TestExecutor_CallTool_NoTarget(t *testing.T) {
|
||||
sender := &fakeSender{}
|
||||
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
||||
exec := NewExecutor(nil, sender, nil, resolver)
|
||||
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
||||
session := mcpgw.ToolSessionContext{BotID: "bot1"}
|
||||
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
||||
"platform": "feishu", "text": "hi",
|
||||
@@ -211,7 +211,7 @@ func TestExecutor_CallTool_NoTarget(t *testing.T) {
|
||||
func TestExecutor_CallTool_SendError(t *testing.T) {
|
||||
sender := &fakeSender{err: errors.New("send failed")}
|
||||
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
||||
exec := NewExecutor(nil, sender, nil, resolver)
|
||||
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
||||
session := mcpgw.ToolSessionContext{BotID: "bot1", ReplyTarget: "t1"}
|
||||
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
||||
"platform": "feishu", "text": "hi",
|
||||
@@ -227,7 +227,7 @@ func TestExecutor_CallTool_SendError(t *testing.T) {
|
||||
func TestExecutor_CallTool_Success(t *testing.T) {
|
||||
sender := &fakeSender{}
|
||||
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
||||
exec := NewExecutor(nil, sender, nil, resolver)
|
||||
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
||||
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "feishu", ReplyTarget: "chat1"}
|
||||
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
||||
"text": "hello",
|
||||
@@ -253,7 +253,7 @@ func TestExecutor_CallTool_Success(t *testing.T) {
|
||||
func TestExecutor_CallTool_ReplyTo(t *testing.T) {
|
||||
sender := &fakeSender{}
|
||||
resolver := &fakeResolver{ct: channel.ChannelType("telegram")}
|
||||
exec := NewExecutor(nil, sender, nil, resolver)
|
||||
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
||||
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "telegram", ReplyTarget: "123"}
|
||||
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
||||
"text": "reply text",
|
||||
@@ -276,7 +276,7 @@ func TestExecutor_CallTool_ReplyTo(t *testing.T) {
|
||||
func TestExecutor_CallTool_NoReplyTo(t *testing.T) {
|
||||
sender := &fakeSender{}
|
||||
resolver := &fakeResolver{ct: channel.ChannelType("telegram")}
|
||||
exec := NewExecutor(nil, sender, nil, resolver)
|
||||
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
||||
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "telegram", ReplyTarget: "123"}
|
||||
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
||||
"text": "no reply",
|
||||
@@ -295,7 +295,7 @@ func TestExecutor_CallTool_NoReplyTo(t *testing.T) {
|
||||
// --- react tests ---
|
||||
|
||||
func TestExecutor_React_NilReactor(t *testing.T) {
|
||||
exec := NewExecutor(nil, nil, nil, nil)
|
||||
exec := NewExecutor(nil, nil, nil, nil, nil)
|
||||
result, err := exec.CallTool(context.Background(), mcpgw.ToolSessionContext{BotID: "bot1"}, toolReact, map[string]any{
|
||||
"platform": "telegram", "target": "123", "message_id": "456", "emoji": "👍",
|
||||
})
|
||||
@@ -310,7 +310,7 @@ func TestExecutor_React_NilReactor(t *testing.T) {
|
||||
func TestExecutor_React_NoMessageID(t *testing.T) {
|
||||
reactor := &fakeReactor{}
|
||||
resolver := &fakeResolver{ct: channel.ChannelType("telegram")}
|
||||
exec := NewExecutor(nil, nil, reactor, resolver)
|
||||
exec := NewExecutor(nil, nil, reactor, resolver, nil)
|
||||
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "telegram", ReplyTarget: "123"}
|
||||
result, err := exec.CallTool(context.Background(), session, toolReact, map[string]any{
|
||||
"emoji": "👍",
|
||||
@@ -326,7 +326,7 @@ func TestExecutor_React_NoMessageID(t *testing.T) {
|
||||
func TestExecutor_React_NoTarget(t *testing.T) {
|
||||
reactor := &fakeReactor{}
|
||||
resolver := &fakeResolver{ct: channel.ChannelType("telegram")}
|
||||
exec := NewExecutor(nil, nil, reactor, resolver)
|
||||
exec := NewExecutor(nil, nil, reactor, resolver, nil)
|
||||
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "telegram"}
|
||||
result, err := exec.CallTool(context.Background(), session, toolReact, map[string]any{
|
||||
"message_id": "456", "emoji": "👍",
|
||||
@@ -342,7 +342,7 @@ func TestExecutor_React_NoTarget(t *testing.T) {
|
||||
func TestExecutor_React_Success(t *testing.T) {
|
||||
reactor := &fakeReactor{}
|
||||
resolver := &fakeResolver{ct: channel.ChannelType("telegram")}
|
||||
exec := NewExecutor(nil, nil, reactor, resolver)
|
||||
exec := NewExecutor(nil, nil, reactor, resolver, nil)
|
||||
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "telegram", ReplyTarget: "123"}
|
||||
result, err := exec.CallTool(context.Background(), session, toolReact, map[string]any{
|
||||
"message_id": "456", "emoji": "👍",
|
||||
@@ -371,7 +371,7 @@ func TestExecutor_React_Success(t *testing.T) {
|
||||
func TestExecutor_React_Remove(t *testing.T) {
|
||||
reactor := &fakeReactor{}
|
||||
resolver := &fakeResolver{ct: channel.ChannelType("telegram")}
|
||||
exec := NewExecutor(nil, nil, reactor, resolver)
|
||||
exec := NewExecutor(nil, nil, reactor, resolver, nil)
|
||||
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "telegram", ReplyTarget: "123"}
|
||||
result, err := exec.CallTool(context.Background(), session, toolReact, map[string]any{
|
||||
"message_id": "456", "remove": true,
|
||||
@@ -394,7 +394,7 @@ func TestExecutor_React_Remove(t *testing.T) {
|
||||
func TestExecutor_React_Error(t *testing.T) {
|
||||
reactor := &fakeReactor{err: errors.New("reaction failed")}
|
||||
resolver := &fakeResolver{ct: channel.ChannelType("telegram")}
|
||||
exec := NewExecutor(nil, nil, reactor, resolver)
|
||||
exec := NewExecutor(nil, nil, reactor, resolver, nil)
|
||||
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "telegram", ReplyTarget: "123"}
|
||||
result, err := exec.CallTool(context.Background(), session, toolReact, map[string]any{
|
||||
"message_id": "456", "emoji": "👍",
|
||||
|
||||
Reference in New Issue
Block a user