diff --git a/pkg/channels/feishu/common.go b/pkg/channels/feishu/common.go index fbe085b73..b1c9504b5 100644 --- a/pkg/channels/feishu/common.go +++ b/pkg/channels/feishu/common.go @@ -69,18 +69,42 @@ func extractFileKey(content string) string { return extractJSONStringField(conte // extractFileName extracts the file_name from a Feishu file message content JSON. func extractFileName(content string) string { return extractJSONStringField(content, "file_name") } -// stripMentionPlaceholders removes @_user_N placeholders from the text content. -// These are inserted by Feishu when users @mention someone in a message. -func stripMentionPlaceholders(content string, mentions []*larkim.MentionEvent) string { +// stripMentionPlaceholders replaces @_user_N placeholders in the text content. +// Bot mentions are removed; other user mentions are replaced with @Name(open_id:xxx) +// so downstream tools can still extract the user ID. +func stripMentionPlaceholders(content string, mentions []*larkim.MentionEvent, botOpenID string) string { if len(mentions) == 0 { return content } for _, m := range mentions { - if m.Key != nil && *m.Key != "" { - content = strings.ReplaceAll(content, *m.Key, "") + if m.Key == nil || *m.Key == "" { + continue } + // If this mention is the bot itself, strip it. + if botOpenID != "" && m.Id != nil && m.Id.OpenId != nil && *m.Id.OpenId == botOpenID { + content = strings.ReplaceAll(content, *m.Key, "") + continue + } + // Replace with @Name(open_id:xxx) to preserve identity for downstream use. + replacement := "" + name := "" + if m.Name != nil { + name = *m.Name + } + openID := "" + if m.Id != nil && m.Id.OpenId != nil { + openID = *m.Id.OpenId + } + if name != "" && openID != "" { + replacement = "@" + name + "(open_id:" + openID + ")" + } else if name != "" { + replacement = "@" + name + } else if openID != "" { + replacement = "@user(open_id:" + openID + ")" + } + content = strings.ReplaceAll(content, *m.Key, replacement) } - // Also clean up any remaining @_user_N patterns + // Clean up any remaining @_user_N patterns not covered by mentions list content = mentionPlaceholderRegex.ReplaceAllString(content, "") return strings.TrimSpace(content) } diff --git a/pkg/channels/feishu/common_test.go b/pkg/channels/feishu/common_test.go index fefc9f7c1..05dd5567e 100644 --- a/pkg/channels/feishu/common_test.go +++ b/pkg/channels/feishu/common_test.go @@ -230,6 +230,8 @@ func TestBuildMarkdownCard(t *testing.T) { func TestStripMentionPlaceholders(t *testing.T) { strPtr := func(s string) *string { return &s } + botOpenID := "ou_bot123" + tests := []struct { name string content string @@ -243,21 +245,37 @@ func TestStripMentionPlaceholders(t *testing.T) { want: "Hello world", }, { - name: "single mention", + name: "bot mention is stripped", content: "@_user_1 hello", mentions: []*larkim.MentionEvent{ - {Key: strPtr("@_user_1")}, + {Key: strPtr("@_user_1"), Id: &larkim.UserId{OpenId: strPtr(botOpenID)}, Name: strPtr("Bot")}, }, want: "hello", }, { - name: "multiple mentions", - content: "@_user_1 @_user_2 hey", + name: "user mention preserved with name and id", + content: "@_user_1 hello", mentions: []*larkim.MentionEvent{ - {Key: strPtr("@_user_1")}, - {Key: strPtr("@_user_2")}, + {Key: strPtr("@_user_1"), Id: &larkim.UserId{OpenId: strPtr("ou_user456")}, Name: strPtr("张三")}, }, - want: "hey", + want: "@张三(open_id:ou_user456) hello", + }, + { + name: "mixed bot and user mentions", + content: "@_user_1 给我和 @_user_2 拉个群", + mentions: []*larkim.MentionEvent{ + {Key: strPtr("@_user_1"), Id: &larkim.UserId{OpenId: strPtr(botOpenID)}, Name: strPtr("Bot")}, + {Key: strPtr("@_user_2"), Id: &larkim.UserId{OpenId: strPtr("ou_user789")}, Name: strPtr("李四")}, + }, + want: "给我和 @李四(open_id:ou_user789) 拉个群", + }, + { + name: "user mention with name only", + content: "@_user_1 hello", + mentions: []*larkim.MentionEvent{ + {Key: strPtr("@_user_1"), Name: strPtr("张三")}, + }, + want: "@张三 hello", }, { name: "empty content", @@ -283,7 +301,7 @@ func TestStripMentionPlaceholders(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := stripMentionPlaceholders(tt.content, tt.mentions) + got := stripMentionPlaceholders(tt.content, tt.mentions, botOpenID) if got != tt.want { t.Errorf("stripMentionPlaceholders(%q, ...) = %q, want %q", tt.content, got, tt.want) } diff --git a/pkg/channels/feishu/feishu_64.go b/pkg/channels/feishu/feishu_64.go index 5217dd4e9..c7d6fcbad 100644 --- a/pkg/channels/feishu/feishu_64.go +++ b/pkg/channels/feishu/feishu_64.go @@ -400,6 +400,19 @@ func (c *FeishuChannel) handleMessageReceive(ctx context.Context, event *larkim. if sender != nil && sender.TenantKey != nil { metadata["tenant_key"] = *sender.TenantKey } + if senderID != "" { + metadata["sender_open_id"] = senderID + } + // Store the full open_id for API use (senderID may be user_id instead) + if sender != nil && sender.SenderId != nil && sender.SenderId.OpenId != nil && *sender.SenderId.OpenId != "" { + metadata["sender_open_id"] = *sender.SenderId.OpenId + } + + // Replace mention placeholders for all chat types: bot mentions are stripped, others become @Name(open_id:xxx) + if len(message.Mentions) > 0 { + knownBotID, _ := c.botOpenID.Load().(string) + content = stripMentionPlaceholders(content, message.Mentions, knownBotID) + } var peer bus.Peer if chatType == "p2p" { @@ -410,11 +423,6 @@ func (c *FeishuChannel) handleMessageReceive(ctx context.Context, event *larkim. // Check if bot was mentioned isMentioned := c.isBotMentioned(message) - // Strip mention placeholders from content before group trigger check - if len(message.Mentions) > 0 { - content = stripMentionPlaceholders(content, message.Mentions) - } - // In group chats, apply unified group trigger filtering respond, cleaned := c.ShouldRespondInGroup(isMentioned, content) if !respond { @@ -423,6 +431,17 @@ func (c *FeishuChannel) handleMessageReceive(ctx context.Context, event *larkim. content = cleaned } + // Prepend sender identity so the LLM knows who sent the message. + if sender != nil && sender.SenderId != nil { + openID := "" + if sender.SenderId.OpenId != nil { + openID = *sender.SenderId.OpenId + } + if openID != "" { + content = fmt.Sprintf("[sender: open_id=%s] %s", openID, content) + } + } + logger.InfoCF("feishu", "Feishu message received", map[string]any{ "sender_id": senderID, "chat_id": chatID,