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:
parent
aaf99d7a30
commit
b989f94122
3 changed files with 80 additions and 19 deletions
|
|
@ -69,18 +69,42 @@ func extractFileKey(content string) string { return extractJSONStringField(conte
|
||||||
// extractFileName extracts the file_name from a Feishu file message content JSON.
|
// extractFileName extracts the file_name from a Feishu file message content JSON.
|
||||||
func extractFileName(content string) string { return extractJSONStringField(content, "file_name") }
|
func extractFileName(content string) string { return extractJSONStringField(content, "file_name") }
|
||||||
|
|
||||||
// stripMentionPlaceholders removes @_user_N placeholders from the text content.
|
// stripMentionPlaceholders replaces @_user_N placeholders in the text content.
|
||||||
// These are inserted by Feishu when users @mention someone in a message.
|
// Bot mentions are removed; other user mentions are replaced with @Name(open_id:xxx)
|
||||||
func stripMentionPlaceholders(content string, mentions []*larkim.MentionEvent) string {
|
// so downstream tools can still extract the user ID.
|
||||||
|
func stripMentionPlaceholders(content string, mentions []*larkim.MentionEvent, botOpenID string) string {
|
||||||
if len(mentions) == 0 {
|
if len(mentions) == 0 {
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
for _, m := range mentions {
|
for _, m := range mentions {
|
||||||
if m.Key != nil && *m.Key != "" {
|
if m.Key == nil || *m.Key == "" {
|
||||||
content = strings.ReplaceAll(content, *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, "")
|
content = mentionPlaceholderRegex.ReplaceAllString(content, "")
|
||||||
return strings.TrimSpace(content)
|
return strings.TrimSpace(content)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,8 @@ func TestBuildMarkdownCard(t *testing.T) {
|
||||||
func TestStripMentionPlaceholders(t *testing.T) {
|
func TestStripMentionPlaceholders(t *testing.T) {
|
||||||
strPtr := func(s string) *string { return &s }
|
strPtr := func(s string) *string { return &s }
|
||||||
|
|
||||||
|
botOpenID := "ou_bot123"
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
content string
|
content string
|
||||||
|
|
@ -243,21 +245,37 @@ func TestStripMentionPlaceholders(t *testing.T) {
|
||||||
want: "Hello world",
|
want: "Hello world",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "single mention",
|
name: "bot mention is stripped",
|
||||||
content: "@_user_1 hello",
|
content: "@_user_1 hello",
|
||||||
mentions: []*larkim.MentionEvent{
|
mentions: []*larkim.MentionEvent{
|
||||||
{Key: strPtr("@_user_1")},
|
{Key: strPtr("@_user_1"), Id: &larkim.UserId{OpenId: strPtr(botOpenID)}, Name: strPtr("Bot")},
|
||||||
},
|
},
|
||||||
want: "hello",
|
want: "hello",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple mentions",
|
name: "user mention preserved with name and id",
|
||||||
content: "@_user_1 @_user_2 hey",
|
content: "@_user_1 hello",
|
||||||
mentions: []*larkim.MentionEvent{
|
mentions: []*larkim.MentionEvent{
|
||||||
{Key: strPtr("@_user_1")},
|
{Key: strPtr("@_user_1"), Id: &larkim.UserId{OpenId: strPtr("ou_user456")}, Name: strPtr("张三")},
|
||||||
{Key: strPtr("@_user_2")},
|
|
||||||
},
|
},
|
||||||
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",
|
name: "empty content",
|
||||||
|
|
@ -283,7 +301,7 @@ func TestStripMentionPlaceholders(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) {
|
||||||
got := stripMentionPlaceholders(tt.content, tt.mentions)
|
got := stripMentionPlaceholders(tt.content, tt.mentions, botOpenID)
|
||||||
if got != tt.want {
|
if got != tt.want {
|
||||||
t.Errorf("stripMentionPlaceholders(%q, ...) = %q, want %q", tt.content, got, tt.want)
|
t.Errorf("stripMentionPlaceholders(%q, ...) = %q, want %q", tt.content, got, tt.want)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -400,6 +400,19 @@ func (c *FeishuChannel) handleMessageReceive(ctx context.Context, event *larkim.
|
||||||
if sender != nil && sender.TenantKey != nil {
|
if sender != nil && sender.TenantKey != nil {
|
||||||
metadata["tenant_key"] = *sender.TenantKey
|
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
|
var peer bus.Peer
|
||||||
if chatType == "p2p" {
|
if chatType == "p2p" {
|
||||||
|
|
@ -410,11 +423,6 @@ func (c *FeishuChannel) handleMessageReceive(ctx context.Context, event *larkim.
|
||||||
// Check if bot was mentioned
|
// Check if bot was mentioned
|
||||||
isMentioned := c.isBotMentioned(message)
|
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
|
// In group chats, apply unified group trigger filtering
|
||||||
respond, cleaned := c.ShouldRespondInGroup(isMentioned, content)
|
respond, cleaned := c.ShouldRespondInGroup(isMentioned, content)
|
||||||
if !respond {
|
if !respond {
|
||||||
|
|
@ -423,6 +431,17 @@ func (c *FeishuChannel) handleMessageReceive(ctx context.Context, event *larkim.
|
||||||
content = cleaned
|
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{
|
logger.InfoCF("feishu", "Feishu message received", map[string]any{
|
||||||
"sender_id": senderID,
|
"sender_id": senderID,
|
||||||
"chat_id": chatID,
|
"chat_id": chatID,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue