diff --git a/cmd/gateway_consumer_normal.go b/cmd/gateway_consumer_normal.go index 6215b302..efe793cf 100644 --- a/cmd/gateway_consumer_normal.go +++ b/cmd/gateway_consumer_normal.go @@ -121,13 +121,13 @@ func processNormalMessage( } displayName := sessionMeta["display_name"] username := sessionMeta["username"] - deps.ContactCollector.EnsureContact(ctx, channelType, msg.Channel, senderNumericID, userID, displayName, username, peerKind, "user") + deps.ContactCollector.EnsureContact(ctx, channelType, msg.Channel, senderNumericID, userID, displayName, username, peerKind, "user", "", "") // Also collect group chat as a contact (for group permission management / merge). // Group IDs (e.g., Telegram "-100456") differ from user IDs — no UNIQUE conflict. if peerKind == string(sessions.PeerGroup) && msg.ChatID != "" { groupTitle := msg.Metadata["chat_title"] // Telegram: message.Chat.Title - deps.ContactCollector.EnsureContact(ctx, channelType, msg.Channel, msg.ChatID, "", groupTitle, "", "group", "group") + deps.ContactCollector.EnsureContact(ctx, channelType, msg.Channel, msg.ChatID, "", groupTitle, "", "group", "group", "", "") } } diff --git a/internal/channels/discord/handler.go b/internal/channels/discord/handler.go index 47827b41..9757faa1 100644 --- a/internal/channels/discord/handler.go +++ b/internal/channels/discord/handler.go @@ -195,7 +195,7 @@ func (c *Channel) handleMessage(_ *discordgo.Session, m *discordgo.MessageCreate // Collect contact even when bot is not mentioned (cache prevents DB spam). if cc := c.ContactCollector(); cc != nil { - cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, senderID, senderName, m.Author.Username, "group", "user") + cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, senderID, senderName, m.Author.Username, "group", "user", "", "") } slog.Debug("discord group message recorded (no mention)", @@ -287,7 +287,7 @@ func (c *Channel) handleMessage(_ *discordgo.Session, m *discordgo.MessageCreate // Collect contact for processed messages (DM + group-mentioned). if cc := c.ContactCollector(); cc != nil { - cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, senderID, senderName, m.Author.Username, peerKind, "user") + cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, senderID, senderName, m.Author.Username, peerKind, "user", "", "") } // Publish directly to bus (to preserve MediaFile MIME types) diff --git a/internal/channels/feishu/bot.go b/internal/channels/feishu/bot.go index 692a82b4..78f5ff19 100644 --- a/internal/channels/feishu/bot.go +++ b/internal/channels/feishu/bot.go @@ -106,7 +106,7 @@ func (c *Channel) handleMessageEvent(ctx context.Context, event *MessageEvent) { // Collect contact even when bot is not mentioned (cache prevents DB spam). if cc := c.ContactCollector(); cc != nil { - cc.EnsureContact(ctx, c.Type(), c.Name(), mc.SenderID, mc.SenderID, senderName, "", "group", "user") + cc.EnsureContact(ctx, c.Type(), c.Name(), mc.SenderID, mc.SenderID, senderName, "", "group", "user", "", "") } slog.Debug("feishu group message recorded (no mention)", @@ -162,7 +162,7 @@ func (c *Channel) handleMessageEvent(ctx context.Context, event *MessageEvent) { // Collect contact for processed messages (DM + group-mentioned). if cc := c.ContactCollector(); cc != nil { - cc.EnsureContact(ctx, c.Type(), c.Name(), mc.SenderID, mc.SenderID, senderName, "", peerKind, "user") + cc.EnsureContact(ctx, c.Type(), c.Name(), mc.SenderID, mc.SenderID, senderName, "", peerKind, "user", "", "") } metadata := map[string]string{ diff --git a/internal/channels/feishu/feishu.go b/internal/channels/feishu/feishu.go index 6c9879bb..bd8c103d 100644 --- a/internal/channels/feishu/feishu.go +++ b/internal/channels/feishu/feishu.go @@ -575,7 +575,7 @@ func (c *Channel) ListGroupMembers(ctx context.Context, chatID string) ([]channe } // Auto-sync member into contact store if cc := c.ContactCollector(); cc != nil { - cc.EnsureContact(ctx, channels.TypeFeishu, c.Name(), m.MemberID, m.MemberID, m.Name, "", "group", "user") + cc.EnsureContact(ctx, channels.TypeFeishu, c.Name(), m.MemberID, m.MemberID, m.Name, "", "group", "user", "", "") } } return result, nil diff --git a/internal/channels/slack/handlers.go b/internal/channels/slack/handlers.go index 7ccc812b..5f750bde 100644 --- a/internal/channels/slack/handlers.go +++ b/internal/channels/slack/handlers.go @@ -200,7 +200,7 @@ func (c *Channel) handleMessage(ev *slackevents.MessageEvent) { // Collect contact even when bot is not mentioned (cache prevents DB spam). if cc := c.ContactCollector(); cc != nil { - cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, senderID, displayName, "", "group", "user") + cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, senderID, displayName, "", "group", "user", "", "") } slog.Debug("slack group message recorded (no mention)", diff --git a/internal/channels/slack/utils.go b/internal/channels/slack/utils.go index 9db56099..7990f174 100644 --- a/internal/channels/slack/utils.go +++ b/internal/channels/slack/utils.go @@ -34,7 +34,7 @@ func (c *Channel) HandleMessage(senderID, chatID, content string, mediaPaths []s // Collect contact for processed messages (DM + group-mentioned). if cc := c.ContactCollector(); cc != nil { ctx := store.WithTenantID(context.Background(), c.TenantID()) - cc.EnsureContact(ctx, c.Type(), c.Name(), userID, userID, metadata["username"], "", peerKind, "user") + cc.EnsureContact(ctx, c.Type(), c.Name(), userID, userID, metadata["username"], "", peerKind, "user", "", "") } c.Bus().PublishInbound(bus.InboundMessage{ diff --git a/internal/channels/telegram/handlers.go b/internal/channels/telegram/handlers.go index 3dda6131..76c6fa82 100644 --- a/internal/channels/telegram/handlers.go +++ b/internal/channels/telegram/handlers.go @@ -41,9 +41,6 @@ func (c *Channel) handleMessage(ctx context.Context, update telego.Update) { userID := fmt.Sprintf("%d", user.ID) senderID := userID - if user.Username != "" { - senderID = fmt.Sprintf("%s|%s", userID, user.Username) - } isGroup := message.Chat.Type == "group" || message.Chat.Type == "supergroup" @@ -336,9 +333,14 @@ func (c *Channel) handleMessage(ctx context.Context, update telego.Update) { // Collect contact even when bot is not mentioned (cache prevents DB spam). if cc := c.ContactCollector(); cc != nil { contactName := strings.TrimSpace(user.FirstName + " " + user.LastName) - cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, userID, contactName, user.Username, "group", "user") + cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, userID, contactName, user.Username, "group", "user", "", "") // Also collect group chat itself as a contact (for group permission / merge). - cc.EnsureContact(ctx, c.Type(), c.Name(), chatIDStr, "", message.Chat.Title, "", "group", "group") + cc.EnsureContact(ctx, c.Type(), c.Name(), chatIDStr, "", message.Chat.Title, "", "group", "group", "", "") + // Collect forum topic as a distinct delivery target (including General). + if isForum && messageThreadID > 0 { + threadStr := fmt.Sprintf("%d", messageThreadID) + cc.EnsureContact(ctx, c.Type(), c.Name(), chatIDStr, "", message.Chat.Title, "", "group", "topic", threadStr, "topic") + } } slog.Debug("telegram group message recorded (no mention)", @@ -591,10 +593,15 @@ func (c *Channel) handleMessage(ctx context.Context, update telego.Update) { // Collect contact for processed messages (DM + group-mentioned). if cc := c.ContactCollector(); cc != nil { contactName := strings.TrimSpace(user.FirstName + " " + user.LastName) - cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, userID, contactName, user.Username, peerKind, "user") + cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, userID, contactName, user.Username, peerKind, "user", "", "") // Also collect group chat itself as a contact (for group permission / merge). if isGroup { - cc.EnsureContact(ctx, c.Type(), c.Name(), chatIDStr, "", message.Chat.Title, "", "group", "group") + cc.EnsureContact(ctx, c.Type(), c.Name(), chatIDStr, "", message.Chat.Title, "", "group", "group", "", "") + // Collect forum topic as a distinct delivery target (including General). + if isForum && messageThreadID > 0 { + threadStr := fmt.Sprintf("%d", messageThreadID) + cc.EnsureContact(ctx, c.Type(), c.Name(), chatIDStr, "", message.Chat.Title, "", "group", "topic", threadStr, "topic") + } } } diff --git a/internal/channels/whatsapp/whatsapp.go b/internal/channels/whatsapp/whatsapp.go index 8ead7afe..3fb305ef 100644 --- a/internal/channels/whatsapp/whatsapp.go +++ b/internal/channels/whatsapp/whatsapp.go @@ -271,7 +271,7 @@ func (c *Channel) handleIncomingMessage(msg map[string]any) { // Collect contact for processed messages. if cc := c.ContactCollector(); cc != nil { - cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, senderID, metadata["user_name"], "", peerKind, "user") + cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, senderID, metadata["user_name"], "", peerKind, "user", "", "") } // Annotate with sender identity so the agent knows who is messaging. diff --git a/internal/channels/zalo/personal/handlers.go b/internal/channels/zalo/personal/handlers.go index 4602e60b..48128a20 100644 --- a/internal/channels/zalo/personal/handlers.go +++ b/internal/channels/zalo/personal/handlers.go @@ -64,7 +64,7 @@ func (c *Channel) handleDM(msg protocol.UserMessage) { // Collect contact for DM messages. if cc := c.ContactCollector(); cc != nil { - cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, senderID, senderName, "", "direct", "user") + cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, senderID, senderName, "", "direct", "user", "", "") } metadata := map[string]string{ @@ -111,7 +111,7 @@ func (c *Channel) handleGroupMessage(msg protocol.GroupMessage) { // Collect contact even when bot is not mentioned (cache prevents DB spam). if cc := c.ContactCollector(); cc != nil { - cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, senderID, senderName, "", "group", "user") + cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, senderID, senderName, "", "group", "user", "", "") } slog.Debug("zalo_personal group message recorded (no mention)", @@ -144,7 +144,7 @@ func (c *Channel) handleGroupMessage(msg protocol.GroupMessage) { // Collect contact for group-mentioned messages. if cc := c.ContactCollector(); cc != nil { - cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, senderID, senderName, "", "group", "user") + cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, senderID, senderName, "", "group", "user", "", "") } metadata := map[string]string{