fix(feishu): preserve mention identity and sender open_id in messages

- Replace @_user_N placeholders with @Name(open_id:xxx) instead of
  stripping them, so downstream tools can still extract user IDs
- Bot mentions are still stripped for trigger detection
- Fix mention handling not applied in p2p (direct) chats
- Inject sender open_id into message content and metadata so the LLM
  knows who sent the message
This commit is contained in:
青柠 2026-03-09 23:09:15 +08:00
parent b6d60a4e9d
commit 86018abbd9
3 changed files with 80 additions and 19 deletions

View file

@ -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 != "" {
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
}
// Also clean up any remaining @_user_N patterns
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)
}
// Clean up any remaining @_user_N patterns not covered by mentions list
content = mentionPlaceholderRegex.ReplaceAllString(content, "")
return strings.TrimSpace(content)
}

View file

@ -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)
}

View file

@ -394,6 +394,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" {
@ -404,11 +417,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 {
@ -417,6 +425,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,