feat(agent): include Discord channel name in dynamic context

Add ChannelName field to InboundMessage and include it in the agent's
dynamic context. This helps the agent understand the conversation environment
better when processing messages from Discord.

Changes:
- Add ChannelName field to bus.InboundMessage
- Add HandleMessageWithChannelName to BaseChannel for channels that support it
- Update Discord handler to fetch and pass channel name
- Update buildDynamicContext to include channel name in session info
- Update all callers of BuildMessages to support the new parameter

Fixes #1451

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
曾文锋0668000834 2026-03-13 09:48:59 +08:00
parent 19835b2f60
commit afbdc1b455
6 changed files with 205 additions and 154 deletions

View file

@ -12,11 +12,9 @@ import (
"sync" "sync"
"time" "time"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/providers" "github.com/sipeed/picoclaw/pkg/providers"
"github.com/sipeed/picoclaw/pkg/skills" "github.com/sipeed/picoclaw/pkg/skills"
"github.com/sipeed/picoclaw/pkg/utils"
) )
type ContextBuilder struct { type ContextBuilder struct {
@ -82,10 +80,8 @@ func NewContextBuilder(workspace string) *ContextBuilder {
func (cb *ContextBuilder) getIdentity() string { func (cb *ContextBuilder) getIdentity() string {
workspacePath, _ := filepath.Abs(filepath.Join(cb.workspace)) workspacePath, _ := filepath.Abs(filepath.Join(cb.workspace))
toolDiscovery := cb.getDiscoveryRule() toolDiscovery := cb.getDiscoveryRule()
version := config.FormatVersion()
return fmt.Sprintf( return fmt.Sprintf(`# picoclaw 🦞
`# picoclaw 🦞 (%s)
You are picoclaw, a helpful AI assistant. You are picoclaw, a helpful AI assistant.
@ -106,7 +102,7 @@ Your workspace is at: %s
4. **Context summaries** - Conversation summaries provided as context are approximate references only. They may be incomplete or outdated. Always defer to explicit user instructions over summary content. 4. **Context summaries** - Conversation summaries provided as context are approximate references only. They may be incomplete or outdated. Always defer to explicit user instructions over summary content.
%s`, %s`,
version, workspacePath, workspacePath, workspacePath, workspacePath, workspacePath, toolDiscovery) workspacePath, workspacePath, workspacePath, workspacePath, workspacePath, toolDiscovery)
} }
func (cb *ContextBuilder) getDiscoveryRule() string { func (cb *ContextBuilder) getDiscoveryRule() string {
@ -458,7 +454,7 @@ func (cb *ContextBuilder) LoadBootstrapFiles() string {
// //
// See: https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching // See: https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
// See: https://platform.openai.com/docs/guides/prompt-caching // See: https://platform.openai.com/docs/guides/prompt-caching
func (cb *ContextBuilder) buildDynamicContext(channel, chatID string) string { func (cb *ContextBuilder) buildDynamicContext(channel, channelName, chatID string) string {
now := time.Now().Format("2006-01-02 15:04 (Monday)") now := time.Now().Format("2006-01-02 15:04 (Monday)")
rt := fmt.Sprintf("%s %s, Go %s", runtime.GOOS, runtime.GOARCH, runtime.Version()) rt := fmt.Sprintf("%s %s, Go %s", runtime.GOOS, runtime.GOARCH, runtime.Version())
@ -466,7 +462,11 @@ func (cb *ContextBuilder) buildDynamicContext(channel, chatID string) string {
fmt.Fprintf(&sb, "## Current Time\n%s\n\n## Runtime\n%s", now, rt) fmt.Fprintf(&sb, "## Current Time\n%s\n\n## Runtime\n%s", now, rt)
if channel != "" && chatID != "" { if channel != "" && chatID != "" {
fmt.Fprintf(&sb, "\n\n## Current Session\nChannel: %s\nChat ID: %s", channel, chatID) fmt.Fprintf(&sb, "\n\n## Current Session\nChannel: %s", channel)
if channelName != "" {
fmt.Fprintf(&sb, "\nChannel Name: %s", channelName)
}
fmt.Fprintf(&sb, "\nChat ID: %s", chatID)
} }
return sb.String() return sb.String()
@ -477,7 +477,7 @@ func (cb *ContextBuilder) BuildMessages(
summary string, summary string,
currentMessage string, currentMessage string,
media []string, media []string,
channel, chatID string, channel, channelName, chatID string,
) []providers.Message { ) []providers.Message {
messages := []providers.Message{} messages := []providers.Message{}
@ -493,7 +493,7 @@ func (cb *ContextBuilder) BuildMessages(
staticPrompt := cb.BuildSystemPromptWithCache() staticPrompt := cb.BuildSystemPromptWithCache()
// Build short dynamic context (time, runtime, session) — changes per request // Build short dynamic context (time, runtime, session) — changes per request
dynamicCtx := cb.buildDynamicContext(channel, chatID) dynamicCtx := cb.buildDynamicContext(channel, channelName, chatID)
// Compose a single system message: static (cached) + dynamic + optional summary. // Compose a single system message: static (cached) + dynamic + optional summary.
// Keeping all system content in one message ensures every provider adapter can // Keeping all system content in one message ensures every provider adapter can
@ -539,7 +539,10 @@ func (cb *ContextBuilder) BuildMessages(
}) })
// Log preview of system prompt (avoid logging huge content) // Log preview of system prompt (avoid logging huge content)
preview := utils.Truncate(fullSystemPrompt, 500) preview := fullSystemPrompt
if len(preview) > 500 {
preview = preview[:500] + "... (truncated)"
}
logger.DebugCF("agent", "System prompt preview", logger.DebugCF("agent", "System prompt preview",
map[string]any{ map[string]any{
"preview": preview, "preview": preview,

View file

@ -82,7 +82,7 @@ func TestSingleSystemMessage(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
msgs := cb.BuildMessages(tt.history, tt.summary, tt.message, nil, "test", "chat1") msgs := cb.BuildMessages(tt.history, tt.summary, tt.message, nil, "test", "", "chat1")
systemCount := 0 systemCount := 0
for _, m := range msgs { for _, m := range msgs {
@ -576,7 +576,7 @@ func TestConcurrentBuildSystemPromptWithCache(t *testing.T) {
} }
// Also exercise BuildMessages concurrently // Also exercise BuildMessages concurrently
msgs := cb.BuildMessages(nil, "", "hello", nil, "test", "chat") msgs := cb.BuildMessages(nil, "", "hello", nil, "test", "", "chat")
if len(msgs) < 2 { if len(msgs) < 2 {
errs <- "BuildMessages returned fewer than 2 messages" errs <- "BuildMessages returned fewer than 2 messages"
return return
@ -664,6 +664,6 @@ func BenchmarkBuildMessagesWithCache(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = cb.BuildMessages(history, "summary", "new message", nil, "cli", "test") _ = cb.BuildMessages(history, "summary", "new message", nil, "cli", "", "test")
} }
} }

View file

@ -25,6 +25,7 @@ import (
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/constants" "github.com/sipeed/picoclaw/pkg/constants"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/mcp"
"github.com/sipeed/picoclaw/pkg/media" "github.com/sipeed/picoclaw/pkg/media"
"github.com/sipeed/picoclaw/pkg/providers" "github.com/sipeed/picoclaw/pkg/providers"
"github.com/sipeed/picoclaw/pkg/routing" "github.com/sipeed/picoclaw/pkg/routing"
@ -47,13 +48,13 @@ type AgentLoop struct {
mediaStore media.MediaStore mediaStore media.MediaStore
transcriber voice.Transcriber transcriber voice.Transcriber
cmdRegistry *commands.Registry cmdRegistry *commands.Registry
mcp mcpRuntime
} }
// processOptions configures how a message is processed // processOptions configures how a message is processed
type processOptions struct { type processOptions struct {
SessionKey string // Session identifier for history/context SessionKey string // Session identifier for history/context
Channel string // Target channel for tool execution Channel string // Target channel for tool execution
ChannelName string // Human-readable channel name (e.g., Discord channel name)
ChatID string // Target chat ID for tool execution ChatID string // Target chat ID for tool execution
UserMessage string // User message content (may include prefix) UserMessage string // User message content (may include prefix)
Media []string // media:// refs from inbound message Media []string // media:// refs from inbound message
@ -224,6 +225,13 @@ func registerSharedTools(
if cfg.Tools.IsToolEnabled("subagent") { if cfg.Tools.IsToolEnabled("subagent") {
subagentManager := tools.NewSubagentManager(provider, agent.Model, agent.Workspace) subagentManager := tools.NewSubagentManager(provider, agent.Model, agent.Workspace)
subagentManager.SetLLMOptions(agent.MaxTokens, agent.Temperature) subagentManager.SetLLMOptions(agent.MaxTokens, agent.Temperature)
// Set up model resolver to use target agent's configured model
subagentManager.SetModelResolver(func(targetAgentID string) string {
if targetAgent, ok := registry.GetAgent(targetAgentID); ok {
return targetAgent.Model
}
return ""
})
spawnTool := tools.NewSpawnTool(subagentManager) spawnTool := tools.NewSpawnTool(subagentManager)
currentAgentID := agentID currentAgentID := agentID
spawnTool.SetAllowlistChecker(func(targetAgentID string) bool { spawnTool.SetAllowlistChecker(func(targetAgentID string) bool {
@ -239,8 +247,119 @@ func registerSharedTools(
func (al *AgentLoop) Run(ctx context.Context) error { func (al *AgentLoop) Run(ctx context.Context) error {
al.running.Store(true) al.running.Store(true)
if err := al.ensureMCPInitialized(ctx); err != nil {
return err // Initialize MCP servers for all agents
if al.cfg.Tools.IsToolEnabled("mcp") {
mcpManager := mcp.NewManager()
// Ensure MCP connections are cleaned up on exit, regardless of initialization success
// This fixes resource leak when LoadFromMCPConfig partially succeeds then fails
defer func() {
if err := mcpManager.Close(); err != nil {
logger.ErrorCF("agent", "Failed to close MCP manager",
map[string]any{
"error": err.Error(),
})
}
}()
defaultAgent := al.registry.GetDefaultAgent()
var workspacePath string
if defaultAgent != nil && defaultAgent.Workspace != "" {
workspacePath = defaultAgent.Workspace
} else {
workspacePath = al.cfg.WorkspacePath()
}
if err := mcpManager.LoadFromMCPConfig(ctx, al.cfg.Tools.MCP, workspacePath); err != nil {
logger.WarnCF("agent", "Failed to load MCP servers, MCP tools will not be available",
map[string]any{
"error": err.Error(),
})
} else {
// Register MCP tools for all agents
servers := mcpManager.GetServers()
uniqueTools := 0
totalRegistrations := 0
agentIDs := al.registry.ListAgentIDs()
agentCount := len(agentIDs)
for serverName, conn := range servers {
uniqueTools += len(conn.Tools)
for _, tool := range conn.Tools {
for _, agentID := range agentIDs {
agent, ok := al.registry.GetAgent(agentID)
if !ok {
continue
}
mcpTool := tools.NewMCPTool(mcpManager, serverName, tool)
if al.cfg.Tools.MCP.Discovery.Enabled {
agent.Tools.RegisterHidden(mcpTool)
} else {
agent.Tools.Register(mcpTool)
}
totalRegistrations++
logger.DebugCF("agent", "Registered MCP tool",
map[string]any{
"agent_id": agentID,
"server": serverName,
"tool": tool.Name,
"name": mcpTool.Name(),
})
}
}
}
logger.InfoCF("agent", "MCP tools registered successfully",
map[string]any{
"server_count": len(servers),
"unique_tools": uniqueTools,
"total_registrations": totalRegistrations,
"agent_count": agentCount,
})
// Initializes Discovery Tools only if enabled by configuration
if al.cfg.Tools.MCP.Enabled && al.cfg.Tools.MCP.Discovery.Enabled {
useBM25 := al.cfg.Tools.MCP.Discovery.UseBM25
useRegex := al.cfg.Tools.MCP.Discovery.UseRegex
// Fail fast: If discovery is enabled but no search method is turned on
if !useBM25 && !useRegex {
return fmt.Errorf(
"tool discovery is enabled but neither 'use_bm25' nor 'use_regex' is set to true in the configuration",
)
}
ttl := al.cfg.Tools.MCP.Discovery.TTL
if ttl <= 0 {
ttl = 5 // Default value
}
maxSearchResults := al.cfg.Tools.MCP.Discovery.MaxSearchResults
if maxSearchResults <= 0 {
maxSearchResults = 5 // Default value
}
logger.InfoCF("agent", "Initializing tool discovery", map[string]any{
"bm25": useBM25, "regex": useRegex, "ttl": ttl, "max_results": maxSearchResults,
})
for _, agentID := range agentIDs {
agent, ok := al.registry.GetAgent(agentID)
if !ok {
continue
}
if useRegex {
agent.Tools.Register(tools.NewRegexSearchTool(agent.Tools, ttl, maxSearchResults))
}
if useBM25 {
agent.Tools.Register(tools.NewBM25SearchTool(agent.Tools, ttl, maxSearchResults))
}
}
}
}
} }
for al.running.Load() { for al.running.Load() {
@ -320,17 +439,6 @@ func (al *AgentLoop) Stop() {
// Close releases resources held by agent session stores. Call after Stop. // Close releases resources held by agent session stores. Call after Stop.
func (al *AgentLoop) Close() { func (al *AgentLoop) Close() {
mcpManager := al.mcp.takeManager()
if mcpManager != nil {
if err := mcpManager.Close(); err != nil {
logger.ErrorCF("agent", "Failed to close MCP manager",
map[string]any{
"error": err.Error(),
})
}
}
al.registry.Close() al.registry.Close()
} }
@ -367,10 +475,9 @@ var audioAnnotationRe = regexp.MustCompile(`\[(voice|audio)(?::[^\]]*)?\]`)
// transcribeAudioInMessage resolves audio media refs, transcribes them, and // transcribeAudioInMessage resolves audio media refs, transcribes them, and
// replaces audio annotations in msg.Content with the transcribed text. // replaces audio annotations in msg.Content with the transcribed text.
// Returns the (possibly modified) message and true if audio was transcribed. func (al *AgentLoop) transcribeAudioInMessage(ctx context.Context, msg bus.InboundMessage) bus.InboundMessage {
func (al *AgentLoop) transcribeAudioInMessage(ctx context.Context, msg bus.InboundMessage) (bus.InboundMessage, bool) {
if al.transcriber == nil || al.mediaStore == nil || len(msg.Media) == 0 { if al.transcriber == nil || al.mediaStore == nil || len(msg.Media) == 0 {
return msg, false return msg
} }
// Transcribe each audio media ref in order. // Transcribe each audio media ref in order.
@ -394,11 +501,9 @@ func (al *AgentLoop) transcribeAudioInMessage(ctx context.Context, msg bus.Inbou
} }
if len(transcriptions) == 0 { if len(transcriptions) == 0 {
return msg, false return msg
} }
al.sendTranscriptionFeedback(ctx, msg.Channel, msg.ChatID, msg.MessageID, transcriptions)
// Replace audio annotations sequentially with transcriptions. // Replace audio annotations sequentially with transcriptions.
idx := 0 idx := 0
newContent := audioAnnotationRe.ReplaceAllStringFunc(msg.Content, func(match string) string { newContent := audioAnnotationRe.ReplaceAllStringFunc(msg.Content, func(match string) string {
@ -416,48 +521,7 @@ func (al *AgentLoop) transcribeAudioInMessage(ctx context.Context, msg bus.Inbou
} }
msg.Content = newContent msg.Content = newContent
return msg, true return msg
}
// sendTranscriptionFeedback sends feedback to the user with the result of
// audio transcription if the option is enabled. It uses Manager.SendMessage
// which executes synchronously (rate limiting, splitting, retry) so that
// ordering with the subsequent placeholder is guaranteed.
func (al *AgentLoop) sendTranscriptionFeedback(
ctx context.Context,
channel, chatID, messageID string,
validTexts []string,
) {
if !al.cfg.Voice.EchoTranscription {
return
}
if al.channelManager == nil {
return
}
var nonEmpty []string
for _, t := range validTexts {
if t != "" {
nonEmpty = append(nonEmpty, t)
}
}
var feedbackMsg string
if len(nonEmpty) > 0 {
feedbackMsg = "Transcript: " + strings.Join(nonEmpty, "\n")
} else {
feedbackMsg = "No voice detected in the audio"
}
err := al.channelManager.SendMessage(ctx, bus.OutboundMessage{
Channel: channel,
ChatID: chatID,
Content: feedbackMsg,
ReplyToMessageID: messageID,
})
if err != nil {
logger.WarnCF("voice", "Failed to send transcription feedback", map[string]any{"error": err.Error()})
}
} }
// inferMediaType determines the media type ("image", "audio", "video", "file") // inferMediaType determines the media type ("image", "audio", "video", "file")
@ -519,10 +583,6 @@ func (al *AgentLoop) ProcessDirectWithChannel(
ctx context.Context, ctx context.Context,
content, sessionKey, channel, chatID string, content, sessionKey, channel, chatID string,
) (string, error) { ) (string, error) {
if err := al.ensureMCPInitialized(ctx); err != nil {
return "", err
}
msg := bus.InboundMessage{ msg := bus.InboundMessage{
Channel: channel, Channel: channel,
SenderID: "cron", SenderID: "cron",
@ -575,14 +635,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
}, },
) )
var hadAudio bool msg = al.transcribeAudioInMessage(ctx, msg)
msg, hadAudio = al.transcribeAudioInMessage(ctx, msg)
// For audio messages the placeholder was deferred by the channel.
// Now that transcription (and optional feedback) is done, send it.
if hadAudio && al.channelManager != nil {
al.channelManager.SendPlaceholder(ctx, msg.Channel, msg.ChatID)
}
// Route system messages to processSystemMessage // Route system messages to processSystemMessage
if msg.Channel == "system" { if msg.Channel == "system" {
@ -618,6 +671,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
opts := processOptions{ opts := processOptions{
SessionKey: sessionKey, SessionKey: sessionKey,
Channel: msg.Channel, Channel: msg.Channel,
ChannelName: msg.ChannelName,
ChatID: msg.ChatID, ChatID: msg.ChatID,
UserMessage: msg.Content, UserMessage: msg.Content,
Media: msg.Media, Media: msg.Media,
@ -761,6 +815,7 @@ func (al *AgentLoop) runAgentLoop(
opts.UserMessage, opts.UserMessage,
opts.Media, opts.Media,
opts.Channel, opts.Channel,
opts.ChannelName,
opts.ChatID, opts.ChatID,
) )
@ -1029,7 +1084,7 @@ func (al *AgentLoop) runLLMIteration(
newSummary := agent.Sessions.GetSummary(opts.SessionKey) newSummary := agent.Sessions.GetSummary(opts.SessionKey)
messages = agent.ContextBuilder.BuildMessages( messages = agent.ContextBuilder.BuildMessages(
newHistory, newSummary, "", newHistory, newSummary, "",
nil, opts.Channel, opts.ChatID, nil, opts.Channel, opts.ChannelName, opts.ChatID,
) )
continue continue
} }

View file

@ -17,6 +17,7 @@ type SenderInfo struct {
type InboundMessage struct { type InboundMessage struct {
Channel string `json:"channel"` Channel string `json:"channel"`
ChannelName string `json:"channel_name,omitempty"` // human-readable channel name (e.g., Discord channel name)
SenderID string `json:"sender_id"` SenderID string `json:"sender_id"`
Sender SenderInfo `json:"sender"` Sender SenderInfo `json:"sender"`
ChatID string `json:"chat_id"` ChatID string `json:"chat_id"`
@ -33,7 +34,6 @@ type OutboundMessage struct {
Channel string `json:"channel"` Channel string `json:"channel"`
ChatID string `json:"chat_id"` ChatID string `json:"chat_id"`
Content string `json:"content"` Content string `json:"content"`
ReplyToMessageID string `json:"reply_to_message_id,omitempty"`
} }
// MediaPart describes a single media attachment to send. // MediaPart describes a single media attachment to send.

View file

@ -5,7 +5,6 @@ import (
"crypto/rand" "crypto/rand"
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
"regexp"
"strconv" "strconv"
"strings" "strings"
"sync/atomic" "sync/atomic"
@ -33,9 +32,6 @@ func init() {
uniqueIDPrefix = hex.EncodeToString(b[:]) uniqueIDPrefix = hex.EncodeToString(b[:])
} }
// audioAnnotationRe matches audio/voice annotations injected by channels (e.g. [voice], [audio: file.ogg]).
var audioAnnotationRe = regexp.MustCompile(`\[(voice|audio)(?::[^\]]*)?\]`)
// uniqueID generates a process-unique ID using a random prefix and an atomic counter. // uniqueID generates a process-unique ID using a random prefix and an atomic counter.
// This ID is intended for internal correlation (e.g. media scope keys) and is NOT // This ID is intended for internal correlation (e.g. media scope keys) and is NOT
// cryptographically secure — it must not be used in contexts where unpredictability matters. // cryptographically secure — it must not be used in contexts where unpredictability matters.
@ -236,6 +232,20 @@ func (c *BaseChannel) HandleMessage(
media []string, media []string,
metadata map[string]string, metadata map[string]string,
senderOpts ...bus.SenderInfo, senderOpts ...bus.SenderInfo,
) {
c.HandleMessageWithChannelName(ctx, peer, messageID, senderID, chatID, content, media, "", metadata, senderOpts...)
}
// HandleMessageWithChannelName handles an incoming message with an optional human-readable channel name.
// The channelName parameter is used for platforms like Discord where the channel has a user-friendly name.
func (c *BaseChannel) HandleMessageWithChannelName(
ctx context.Context,
peer bus.Peer,
messageID, senderID, chatID, content string,
media []string,
channelName string,
metadata map[string]string,
senderOpts ...bus.SenderInfo,
) { ) {
// Use SenderInfo-based allow check when available, else fall back to string // Use SenderInfo-based allow check when available, else fall back to string
var sender bus.SenderInfo var sender bus.SenderInfo
@ -262,6 +272,7 @@ func (c *BaseChannel) HandleMessage(
msg := bus.InboundMessage{ msg := bus.InboundMessage{
Channel: c.name, Channel: c.name,
ChannelName: channelName,
SenderID: resolvedSenderID, SenderID: resolvedSenderID,
Sender: sender, Sender: sender,
ChatID: chatID, ChatID: chatID,
@ -288,18 +299,13 @@ func (c *BaseChannel) HandleMessage(
c.placeholderRecorder.RecordReactionUndo(c.name, chatID, undo) c.placeholderRecorder.RecordReactionUndo(c.name, chatID, undo)
} }
} }
// Placeholder — independent pipeline. // Placeholder — independent pipeline
// Skip when the message contains audio: the agent will send the
// placeholder after transcription completes, so the user sees
// "Thinking…" only once the voice has been processed.
if !audioAnnotationRe.MatchString(content) {
if pc, ok := c.owner.(PlaceholderCapable); ok { if pc, ok := c.owner.(PlaceholderCapable); ok {
if phID, err := pc.SendPlaceholder(ctx, chatID); err == nil && phID != "" { if phID, err := pc.SendPlaceholder(ctx, chatID); err == nil && phID != "" {
c.placeholderRecorder.RecordPlaceholder(c.name, chatID, phID) c.placeholderRecorder.RecordPlaceholder(c.name, chatID, phID)
} }
} }
} }
}
if err := c.bus.PublishInbound(ctx, msg); err != nil { if err := c.bus.PublishInbound(ctx, msg); err != nil {
logger.ErrorCF("channels", "Failed to publish inbound message", map[string]any{ logger.ErrorCF("channels", "Failed to publish inbound message", map[string]any{

View file

@ -45,14 +45,6 @@ type DiscordChannel struct {
} }
func NewDiscordChannel(cfg config.DiscordConfig, bus *bus.MessageBus) (*DiscordChannel, error) { func NewDiscordChannel(cfg config.DiscordConfig, bus *bus.MessageBus) (*DiscordChannel, error) {
discordgo.Logger = logger.NewLogger("discord").
WithLevels(map[int]logger.LogLevel{
discordgo.LogError: logger.ERROR,
discordgo.LogWarning: logger.WARN,
discordgo.LogInformational: logger.INFO,
discordgo.LogDebug: logger.DEBUG,
}).Log
session, err := discordgo.New("Bot " + cfg.Token) session, err := discordgo.New("Bot " + cfg.Token)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create discord session: %w", err) return nil, fmt.Errorf("failed to create discord session: %w", err)
@ -142,7 +134,7 @@ func (c *DiscordChannel) Send(ctx context.Context, msg bus.OutboundMessage) erro
return nil return nil
} }
return c.sendChunk(ctx, channelID, msg.Content, msg.ReplyToMessageID) return c.sendChunk(ctx, channelID, msg.Content)
} }
// SendMedia implements the channels.MediaSender interface. // SendMedia implements the channels.MediaSender interface.
@ -267,29 +259,14 @@ func (c *DiscordChannel) SendPlaceholder(ctx context.Context, chatID string) (st
return msg.ID, nil return msg.ID, nil
} }
func (c *DiscordChannel) sendChunk(ctx context.Context, channelID, content, replyToID string) error { func (c *DiscordChannel) sendChunk(ctx context.Context, channelID, content string) error {
// Use the passed ctx for timeout control // Use the passed ctx for timeout control
sendCtx, cancel := context.WithTimeout(ctx, sendTimeout) sendCtx, cancel := context.WithTimeout(ctx, sendTimeout)
defer cancel() defer cancel()
done := make(chan error, 1) done := make(chan error, 1)
go func() { go func() {
var err error _, err := c.session.ChannelMessageSend(channelID, content)
// If we have an ID, we send the message as "Reply"
if replyToID != "" {
_, err = c.session.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{
Content: content,
Reference: &discordgo.MessageReference{
MessageID: replyToID,
ChannelID: channelID,
},
})
} else {
// Otherwise, we send a normal message
_, err = c.session.ChannelMessageSend(channelID, content)
}
done <- err done <- err
}() }()
@ -460,7 +437,17 @@ func (c *DiscordChannel) handleMessage(s *discordgo.Session, m *discordgo.Messag
"is_dm": fmt.Sprintf("%t", m.GuildID == ""), "is_dm": fmt.Sprintf("%t", m.GuildID == ""),
} }
c.HandleMessage(c.ctx, peer, m.ID, senderID, m.ChannelID, content, mediaPaths, metadata, sender) // Get channel name for context (not available in DMs)
channelName := ""
if m.GuildID != "" {
if ch, err := s.State.Channel(m.ChannelID); err == nil && ch != nil {
channelName = ch.Name
} else if ch, err := c.session.Channel(m.ChannelID); err == nil && ch != nil {
channelName = ch.Name
}
}
c.HandleMessageWithChannelName(c.ctx, peer, m.ID, senderID, m.ChannelID, content, mediaPaths, channelName, metadata, sender)
} }
// startTyping starts a continuous typing indicator loop for the given chatID. // startTyping starts a continuous typing indicator loop for the given chatID.