feat(channels): add MediaSender optional interface for outbound media

Add outbound media sending capability so the agent can publish media
attachments (images, files, audio, video) through channels via the bus.

- Add MediaPart and OutboundMediaMessage types to bus
- Add PublishOutboundMedia/SubscribeOutboundMedia bus methods
- Add MediaSender interface discovered via type assertion by Manager
- Add media dispatch/worker in Manager with shared retry logic
- Extend ToolResult with Media field and MediaResult constructor
- Publish outbound media from agent loop on tool results
- Implement SendMedia for Telegram, Discord, Slack, LINE, OneBot, WeCom
This commit is contained in:
Hoshina
2026-02-23 03:10:57 +08:00
parent d1551dc423
commit 4c7a5df307
12 changed files with 809 additions and 15 deletions
+34 -7
View File
@@ -10,17 +10,19 @@ import (
var ErrBusClosed = errors.New("message bus closed")
type MessageBus struct {
inbound chan InboundMessage
outbound chan OutboundMessage
done chan struct{}
closed atomic.Bool
inbound chan InboundMessage
outbound chan OutboundMessage
outboundMedia chan OutboundMediaMessage
done chan struct{}
closed atomic.Bool
}
func NewMessageBus() *MessageBus {
return &MessageBus{
inbound: make(chan InboundMessage, 100),
outbound: make(chan OutboundMessage, 100),
done: make(chan struct{}),
inbound: make(chan InboundMessage, 100),
outbound: make(chan OutboundMessage, 100),
outboundMedia: make(chan OutboundMediaMessage, 100),
done: make(chan struct{}),
}
}
@@ -74,6 +76,31 @@ func (mb *MessageBus) SubscribeOutbound(ctx context.Context) (OutboundMessage, b
}
}
func (mb *MessageBus) PublishOutboundMedia(ctx context.Context, msg OutboundMediaMessage) error {
if mb.closed.Load() {
return ErrBusClosed
}
select {
case mb.outboundMedia <- msg:
return nil
case <-mb.done:
return ErrBusClosed
case <-ctx.Done():
return ctx.Err()
}
}
func (mb *MessageBus) SubscribeOutboundMedia(ctx context.Context) (OutboundMediaMessage, bool) {
select {
case msg, ok := <-mb.outboundMedia:
return msg, ok
case <-mb.done:
return OutboundMediaMessage{}, false
case <-ctx.Done():
return OutboundMediaMessage{}, false
}
}
func (mb *MessageBus) Close() {
if mb.closed.CompareAndSwap(false, true) {
close(mb.done)
+16
View File
@@ -24,3 +24,19 @@ type OutboundMessage struct {
ChatID string `json:"chat_id"`
Content string `json:"content"`
}
// MediaPart describes a single media attachment to send.
type MediaPart struct {
Type string `json:"type"` // "image" | "audio" | "video" | "file"
Ref string `json:"ref"` // media store ref, e.g. "media://abc123"
Caption string `json:"caption,omitempty"` // optional caption text
Filename string `json:"filename,omitempty"` // original filename hint
ContentType string `json:"content_type,omitempty"` // MIME type hint
}
// OutboundMediaMessage carries media attachments from Agent to channels via the bus.
type OutboundMediaMessage struct {
Channel string `json:"channel"`
ChatID string `json:"chat_id"`
Parts []MediaPart `json:"parts"`
}